Useful I2C from the HPS on the A10 SoC Devkit board

There are several ways to approach getting an I2C interface to work from the Arria10 HPS processors, some of them work, some of them not so much. I went a few rounds with the problem and ended up making my own set of functions to perform I2C with the type of interface I was used to when making I2C on microprocessors.

Along the way I used the read() and write() functions, which work but are a bit clumsy. I wanted to be able to set the I2C bus speed which is out of reach for a simple read() and write() so I investigated the alt_i2c routines supplied by Altera, but could never get them to work. The alt_i2c interface works from a block of 256 bytes of FPGA memory containing 42 registers (for each of the A10’s i2c busses) that control many aspects of the i2c interface (see section 21.6.1 of the Arria10 Hard Processor System Technical Reference Manual, on page 4168). The one I was interested in was to control the SCL speed, which has finer resolution in the A10 than just 100kHz or 400kHz. This should have worked but alas I would need to mmap() to the i2c memory block at 0xffc02300 (for i2c1, which is the i2c bus connected to the HPS i2c-0 bus, and this was not possible. I checked with Altera and they suggested that using the sysfs interface in Linux would be a better way to get my interface working.

I am using Angstrom Linux (same as original build from RocketBoards), Quartus v17.0.2 and DS-5 v5.26 for Linux compiles using Eclipse. If your versions differ you could see some differences from what I show.

The sysfs interface creates a directory and file system to represent hardware elements where the directories and files represented are not really files. You can check the speed the i2c bus that is connected to your A10’s HPS by entering the command below at your Linux prompt.

od -b /sys/class/i2c-adapter/i2c-0/device/of_node/clock-frequency

Linux responded with;
000000 000 001 206 240
000004

This is four octal numbers 0, 1, 206, 240, which convert to hex as 0x00 01 86 a0. 0x000186a0 converts to 100,000 in decimal so my i2c bus is running at 100kHz. This was better than expected since I thought the Devkit’s LCD was very slow so the i2c would be running at 40kHz. I did not end up altering the speed but I saw that it can be done using a modprobe command. I will probably do that after the rest of the hardware I plan to attach to the i2c comes in (this i2c bus extends out on the FMC-B connector, where I will access it).

In my microprocessor work I used simple i2c routines so I used the ioctl() instruction as described n i2c.h and i2c-dev.h to create a few routines that worked for me as shown below. The i2c demo program for the alt_i2c interface (which I never got to work) excercised the LCD and the EEPROM on the Devkit so my routines below do the same. (I did not create any code to make the HPS an i2c slave device since I do not need this but I see that it can be done).

The first program (below) performs an LCD screen clear and includes three functions i2c_send(), i2c_recv(), and i2c_sendrecv(). For each of these you pass in the i2c address you want to talk to and the unsigned char buffer of data you want to send along with a count of number of bytes to send. In the case of recieving data you pass an empty buffer and the number of bytes you want to receive. The i2c_sendrecv() routine sends byte(s) then performs the receive collection. Often this is the case with i2c devices since you may need to indicate a register or send a command to get the return bytes you want. Preparing the incoming buffers is a job for the calling side of the exchange, as is handling the returned data.

/*

  • lcd_i2c.c
  • Created on: Apr 18, 2018
  •  Author: jburleso
    
  •  Test for I2C routines using LCD on A10 SoC DevKit board
    

*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <math.h>
#include <getopt.h>

#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int i2c_recv(unsigned char address,
unsigned char *receiveData,
int numRecvBytes)
{ // returns zero if good, everything else is bad
int file;
struct i2c_rdwr_ioctl_data rdwrData;
struct i2c_msg clearMsg;
int retval = 0;

if ((file = open("/dev/i2c-0",O_RDWR)) < 0)
{
    printf("Failed to open the bus.");
    retval = errno;
    return retval;
}

clearMsg.addr = (__u16)address;
clearMsg.flags = 0x0001;
clearMsg.len = (__u16)numRecvBytes;
clearMsg.buf = receiveData;

rdwrData.msgs = &(clearMsg);
rdwrData.nmsgs = 1;

if (ioctl(file,I2C_RDWR,&rdwrData) < 0)
{
    printf("Failed to acquire bus access and/or talk to slave.\n");
    retval = errno;
    close(file);
    return retval;
}
close(file);
return retval;

}

int i2c_sendrecv(unsigned char address,
unsigned char *sendData,
int numSendBytes,
unsigned char *receiveData,
int numRecvBytes)
{ // returns zero if good, everything else is bad
int file;
struct i2c_rdwr_ioctl_data rdwrData;
struct i2c_msg clearMsg[2];
int retval = 0;

if ((file = open("/dev/i2c-0",O_RDWR)) < 0)
{
    printf("Failed to open the bus.");
    retval = errno;
    return retval;
}

clearMsg[0].addr = (__u16)address;
clearMsg[0].flags = 0x0000;
clearMsg[0].len = (__u16)numSendBytes;
clearMsg[0].buf = sendData;
clearMsg[1].addr = (__u16)address;
clearMsg[1].flags = 0x0001;
clearMsg[1].len = (__u16)numRecvBytes;
clearMsg[1].buf = receiveData;

rdwrData.msgs = (struct i2c_msg *)&(clearMsg);
rdwrData.nmsgs = 2;

if (ioctl(file,I2C_RDWR,&rdwrData) < 0)
{
    printf("Failed to acquire bus access and/or talk to slave.\n");
    retval = errno;
    close(file);
    return retval;
}
close(file);
return retval;

}

int i2c_send(unsigned char address, unsigned char *sendData, int numBytes)
{ // returns zero if good, everything else is bad
int file;
struct i2c_rdwr_ioctl_data rdwrData;
struct i2c_msg clearMsg;
int retval = 0;

if ((file = open("/dev/i2c-0",O_RDWR)) < 0)
{
    printf("Failed to open the bus.");
    retval = errno;
    return retval;
}

clearMsg.addr = (__u16)address;
clearMsg.flags = 0x0000;
clearMsg.len = (__u16)numBytes;
clearMsg.buf = sendData;

rdwrData.msgs = &(clearMsg);
rdwrData.nmsgs = 1;

if (ioctl(file,I2C_RDWR,&rdwrData) < 0)
{
    printf("Failed to acquire bus access and/or talk to slave.\n");
    retval = errno;
    close(file);
    return retval;
}
close(file);
return retval;

}

int main(void)
{
int status;
unsigned char dataBuf[10] = {0};

puts("LCD I2C command");
dataBuf[0] = 0xfe;  // escape
dataBuf[1] = 0b01010001;  // clear screen
status = i2c_send(0x28, dataBuf, 2);

// usleep(1000); // wait 1 milisec
// status = lcd_clear();
// usleep(1000); // wait 1 milisec
// status = lcd_clear();
if(status == 0)
return EXIT_SUCCESS;
else
return status;
}

One thing I noticed in using the LCD is that repeat commands need a little time before the LCD is ready for the next command. I found that 1 milliSec was enough (though it could be less, I didn’t test it that thoroughly - and it may differ with the command used. You can get the LCD data sheet NHD-0216K3Z-NSW-BBW-V3.pdffrom the internet for a complete list of commands that can be sent to operate the display.

If you need to implement special i2c conditions like using no stop condition at the end of repeated transfers, or additional restarts before an activity you can add them (and others) by editing the .flags field in the i2c_msg structures in the three i2c functions. If you add this I would suggest cloning the function so you have one with the special capability and one that operates in the standard i2c manner. The protocol modifications that can be implemented this way are listed in the i2c.h header file. The structures used for the ioctl() calls are in the i2c-dev.h file.

The next program uses the same three i2c_send(), i2c_recv() and i2c_sendrecv() routines so I am not repeating them below. In my code I close() the file id opened by the open() command after I am done with it. In all the examples I see they never close it. Maybe this doesn’t need to be done but it looks right to me. The error values returned can be identified using perror(), or you can insert your own message if it was not successful. This program tests the EEPROM on the Devkit board to write and read a single byte, and for a 32 byte block write/read. For block read/write the address you supply needs to be at a block boundary (so at a 32 byte boundary, so if the low five bits of address are zeros you are OK). In using the EEPROM I found that the EEPROM needs about a 5 milliSec rest between i2c commands. The program below lists the main() routine only to perform the tests and call the i2c functions.

/*

  • ee_i2c.c
  • Created on: Apr 18, 2018
  •  Author: jburleso
    
  •  Test for I2C routines using EEPROM on A10 SoC DevKit board
    

*/
// using same include statements
int main(void)
{
int status = 0;
unsigned char dataBuf[40] = {0};
unsigned char recvBuf[40] = {0};
int i;

puts("EEPROM I2C command");
printf("Write 1 byte (0x82) to addr 0x0234\n");
dataBuf[0] = 0x02;  // address hibyte
dataBuf[1] = 0x34;  // address lobyte
dataBuf[2] = 0x82;  // data byte
status = i2c_send(0x51, dataBuf, 3);
usleep(5000);  // wait 1 milisec
printf("\nRead 1 byte from addr 0x0234\n");
dataBuf[0] = 0x02;  // address hibyte
dataBuf[1] = 0x34;  // address lobyte
status = i2c_sendrecv(0x51, dataBuf, 2, recvBuf, 1);
usleep(5000);  // wait 1 milisec
printf("Data read was = 0x%2.2x\n\n",recvBuf[0]);
printf("Write 32 byte page (data 0-31) to addr 0x0100\n");
dataBuf[0] = 0x01;  // address hibyte
dataBuf[1] = 0x00;  // address lobyte
for(i=2; i<35; i++)
{
	dataBuf[i] = (unsigned char) i-2;  // data byte
}
status = i2c_send(0x51, dataBuf, 34);
usleep(5000);  // wait 1 milisec
printf("\nRead 32 byte page from addr 0x0100\n");
dataBuf[0] = 0x01;  // address hibyte
dataBuf[1] = 0x00;  // address lobyte
status = i2c_sendrecv(0x51, dataBuf, 2, recvBuf, 32);
for(i=0; i<32; i++)
{
    printf("Data read was = 0x%2.2x\n",recvBuf[i]);
}
return status;

}

There is more that the EEPROM (a 24LC32A EEPROM from Microchip Technology) can do so you can get a data sheet from the internet and create a more full test. One thing that is often added is a write then verify when writing data to be sure there was no error.

The i2c.h and i2c-dev.h routines appear in two places in the Altera installation (one under the DS-5 directory tree and one under the embedded software Linux kernel. I looked at the files and they dont appear to have any significant different so I believe you can compile and link correctly with either one. One other advantage I see with this small scale approach is the file size. Using the alt-i2c routines I was able to get a compile and link (it just would not run without segmentation errors) but the simple program to write a few bytes was almost 100kBytes, a tad bulky for my tastes.

I have a DE-1 board with a Cyclone V w/HPS on it and I am sure this approach would work there also with few changes. It is a bit different in capability but the sysfs interface is similar. I’ll have to try that sometime, but the test program Terasic supplied (gsensor) for Accelerometer control actually works OK as delivered so there isn’t a missing capability that needs covering as far as I see.

There is no Quartus code in this project, I assume you have a Quartus project with the HPS processors instantiated with an I2C bus and you have a Linux prompt to execute the programs. Now that your I2C bus is working someone can put together a complete library of LCD control routines and EEPROM control routines and share them with the rest of us.

Happy Programming!

1 Like