Stub I2C driver and client implementation

Driver

The stub driver creates a /dev/i2cX device, processes client requests returning simulated data when necessary, then logs the requests to standard output.

The driver defines the device structure and implements all required functions, which include:
  • init()
  • fini()
  • send()
  • recv()
  • set_slave_addr()
  • set_bus_speed()
  • version_info()
  • driver_info()
  • bus_reset()
Note:
You can rename these functions, since i2c_master_getfunc() assigns these functions to the correct members of the i2c_master_funcs_t structure.
The following diagram shows how you (the driver developer) communicates with the device:
Figure 1I2C stub driver
You can compile the i2c-stub.c code example using:
qcc -V gcc_ntoaarch64le -o i2c-stub i2c-stub.c -li2c-master -lsecpol
Note:
Make sure to specify appropriate architecture using the -V argument. For the Raspberry Pi 4, use gcc_ntoaarch64le as above.
Copy the i2c-stub binary to your target device, and log in to the device as root user. Navigate to the directory you copied the binary to and execute:
# ./i2c-stub -a 0x25 --b 2100 --u 2              
i2c-stub: Returned implementation table.
i2c-stub: VERSION=1.0.0
i2c-stub: Using device slave address 0x25
i2c-stub: Initialized stub device.
i2c-stub: Using bus speed 2000
i2c-stub: DRIVER=(3:2:0)
# 
You can invoke the I2C stub driver using various command line options:
Stub I2C manager

i2c-stub [options]

Options:
-a slave addr   Slave address(only 7-bit addr supported. Default: 0x55)

Generic options:
--b bus_speed       Default bus speed. Must be a multiple of 100; however,
                    the driver rounds down to the nearest multiple of
                    1000.  (Default: 100000)
--u unit            Unit number. Number to append to device name
                    prefix (/dev/i2c). (Default: 0)

Example:
i2c-stub -a 0x12 --b100000 --u1

Once started, the driver runs in the background until you kill it.

i2c-stub.c code example

#include <hw/i2c.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/dispatch.h>
#include <sys/iofunc.h>
#include <sys/neutrino.h>
#include <time.h>
#include <unistd.h>

#define STRING_LEN 200u

/*
 * functions are implemented by this stub driver
 */
int i2c_master_getfuncs(i2c_master_funcs_t *, int);
void *stub_init(int, char **);
void stub_fini(void *);
i2c_status_t stub_send(void *, void *, unsigned int, unsigned int);
i2c_status_t stub_recv(void *, void *, unsigned int, unsigned int);
int stub_set_bus_speed(void *, unsigned int, unsigned int *);
int stub_set_slave_addr(void *, unsigned int, i2c_addrfmt_t);
int stub_bus_reset(void *hdl);
int stub_driver_info(void *, i2c_driver_info_t *);
int stub_version_info(i2c_libversion_t *);

/*
 * Define stuct (see I2C DDK Developer Guide)
 */
typedef struct stub_dev_s
{
    unsigned number;
    unsigned slave_addr;
    i2c_addrfmt_t slave_add_fmt;
    unsigned int bus_speed;
    int ready;
    char header_output[STRING_LEN];
    unsigned header_len;
    char output[STRING_LEN];
    unsigned output_len;
    int index;
} stub_dev_t;

/*
 * Driver initialization (see I2C DDK Developer Guide)
 */
void *stub_init(int argc, char *argv[])
{

    stub_dev_t *dev;
    int c;
    unsigned long narg;

    // allocate device structure
    dev = calloc(1, sizeof(stub_dev_t));
    if (NULL == dev)
    {
        fprintf(stderr, "i2c-stub: Failed to allocate device memory.\n");
        return NULL;
    }

    // assign default device values
    dev->number = 0;
    dev->slave_addr = 0x55;
    dev->slave_add_fmt = I2C_ADDRFMT_7BIT;
    dev->bus_speed = 1000000u;
    dev->ready = 0;
    strlcpy(dev->header_output, "START:", STRING_LEN);
    dev->header_len = 6;
    strlcpy(dev->output, "abcdefghijklmnopqrstuvwxyz", STRING_LEN);
    dev->output_len = 26;
    dev->index = 0;

    // parse options
    while (-1 != (c = getopt(argc, argv, ":a:")))
    {
        switch (c)
        {
        case ':':
            fprintf(stderr, "Missing slave address.\n");
            goto stub_init_error;
        case 'a':
            narg = strtoul(optarg, &optarg, 0);
            if (0 != stub_set_slave_addr(dev, narg, I2C_ADDRFMT_7BIT))
            {
                fprintf(stderr, "i2c-stub: Could not set the slave address to 0x%lx\n", narg);
                goto stub_init_error;
            }
            break;
        }
    }

    // Thread control setup
    if (-1 == ThreadCtl(_NTO_TCTL_IO, NULL))
    {
        fprintf(stderr, "i2c-stub: Failed to setup thread control.\n");
        goto stub_init_error;
    }

    // Normally we would also map hardware registers and setup interrupt handler here

    dev->ready = !0;
    printf("i2c-stub: Initialized stub device.\n");
    return dev;

stub_init_error:
    free(dev);
    return NULL;
}

/*
 * Set slave address (see I2C DDK Developer Guide)
 */
int stub_set_slave_addr(void *hdl, unsigned int addr, i2c_addrfmt_t fmt)
{
    stub_dev_t *dev = hdl;

    if ((I2C_ADDRFMT_7BIT != fmt))
    {
        fprintf(stderr, "i2c-stub: Invalid address format.\n");
        goto stub_set_slave_addr_error;
    }

    if (addr & ((unsigned)-1 ^ 0x7F))
    {
        fprintf(stderr, "i2c-stub: Invalid address %x.\n", addr);
        goto stub_set_slave_addr_error;
    }

    dev->slave_addr = addr;
    printf("i2c-stub: Using device slave address 0x%x\n", dev->slave_addr);
    return 0;

stub_set_slave_addr_error:
    errno = EINVAL;
    return -1;
}

/*
 * Send Data (see I2C DDK Developer Guide)
 */
i2c_status_t stub_send(void *hdl, void *buf, unsigned int len, unsigned int stop)
{
    (void)hdl;
    char *cbuf = buf;

    printf("i2c-stub: Sending %u bytes: ", len);
    for (unsigned int i = 0; i < len; i++)
    {
        if (cbuf[i] < 20 || cbuf[i] > 128)
        {
            printf(".");
        }
        else
        {
            printf("%c", cbuf[i]);
        }
    }
    if (stop)
    {
        printf(" - STOP");
    }
    printf("\n");
    return I2C_STATUS_DONE;
}

/*
 * Receive Data (see I2C DDK Developer Guide)
 */
i2c_status_t stub_recv(void *hdl, void *buf, unsigned int len, unsigned int stop)
{
    stub_dev_t *dev = hdl;
    char *cbuf = buf;

    printf("i2c-stub: Receiving %u bytes: ", len);
    for (unsigned int i = 0; i < len; i++)
    {
        if (dev->index < dev->header_len)
        {
            cbuf[i] = dev->header_output[dev->index++];
        }
        else
        {
            cbuf[i] = dev->output[dev->index - dev->header_len];
            dev->index = (dev->index + 1 - dev->header_len) % dev->output_len + dev->header_len;
        }
        if (cbuf[i] < 20 || cbuf[i] > 128)
        {
            printf(".");
        }
        else
        {
            printf("%c", cbuf[i]);
        }
    }
    if (stop)
    {
        printf(" - STOP");
    }
    printf("\n");
    return I2C_STATUS_DONE;
}

/*
 * Clean up driver (see I2C DDK Developer Guide)
 */
void stub_fini(void *hdl)
{
    stub_dev_t *dev = hdl;

    if (NULL != dev)
    {
        dev->ready = 0;
    }
    printf("i2c-stub: Cleaned up stub device.\n");
}

/*
 * Integrate the implementations (see I2C DDK Developer Guide)
 */
int i2c_master_getfuncs(i2c_master_funcs_t *funcs, int tabsize)
{
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs, init, stub_init, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs, fini, stub_fini, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs, send, stub_send, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs, recv, stub_recv, tabsize);

    I2C_ADD_FUNC(i2c_master_funcs_t, funcs, set_slave_addr, stub_set_slave_addr, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs, set_bus_speed, stub_set_bus_speed, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs, version_info, stub_version_info, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs, driver_info, stub_driver_info, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs, bus_reset, stub_bus_reset, tabsize);
    printf("i2c-stub: Returned implementation table.\n");
    return 0;
}

/*
 * Other implemented functions (see I2C DDK Samples)
 */

int stub_set_bus_speed(void *hdl, unsigned int speed, unsigned int *ospeed)
{
    stub_dev_t *dev = hdl;

    if (speed % 100u)
    {
        fprintf(stderr, "i2c-stub: Speed must be a multiple of 100, not %u.\n", speed);
        errno = EINVAL;
        return -1;
    }
    dev->bus_speed = speed - speed % 1000u;
    if (NULL != ospeed)
    {
        *ospeed = dev->bus_speed;
    }
    printf("i2c-stub: Using bus speed %u\n", dev->bus_speed);
    return 0;
}

int stub_version_info(i2c_libversion_t *version)
{
    version->major = I2CLIB_VERSION_MAJOR;
    version->minor = I2CLIB_VERSION_MINOR;
    version->revision = I2CLIB_REVISION;
    printf("i2c-stub: VERSION=%u.%u.%u\n", version->major, version->minor, version->revision);
    return 0;
}

int stub_driver_info(void *hdl, i2c_driver_info_t *info)
{
    (void)hdl;

    info->speed_mode = I2C_SPEED_STANDARD | I2C_SPEED_FAST;
    info->addr_mode = I2C_ADDRFMT_7BIT;
    info->verbosity = 0;
    printf("i2c-stub: DRIVER=(%u:%u:%u)\n", info->speed_mode, info->addr_mode, info->verbosity);
    return 0;
}

int stub_bus_reset(void *hdl)
{
    stub_dev_t *dev = hdl;
    dev->index = 0;
    printf("i2c-stub: Reset the bus.\n");
    return EOK;
}

This stub driver uses:

stub_dev_t
  • Defines the device-specific structure.
  • It's members include:
    • number - device number append to /dev/i2c (default = 0, for /dev/i2c0).
    • slave_addr - address of the slave device, (default = 0x55).
    • slave_add_fmt - slave device address format, (default = 7-bit).
    • bus_speed - a number representing bus speed, which does not have any effect for this driver, (default = 1000000).
    • ready - flag indicating that device is initialized and ready (default = 0).
    • header_ouput - data to return after initialization or bus reset (default = "START:").
    • header_len - length of the header_output data (default = 6).
    • output - data to return on recv, repeats from beginning when end is reached (default = "abcdefghijklmnopqrstuvwxyz").
    • output_len - length of the output data (default = 26).
    • index - index of the next byte to be returned. If index < header_len, index represents byte offset in header_output, otherwise index - header_len represents byte offset in output (default = 0).
  • Displays message to standard output.
stub_init()
  • Allocates memory for the device structure.

  • Sets default device values, as described above.

  • Parses options, currently only -a is supported, to specify the slave device address.

  • Sets up thread control.

  • Flips the ready flag in the devices structure to 1.

  • Displays message to standard output.

stub_set_slave_addr()
  • Verifies that address format is 7-bit, as 10-bit addresses aren't supported.

  • Verifies that the address is no longer than 7 bits.

  • Sets the slave_addr member of the device structure.

  • Displays message to standard output.

stub_send()
  • Accepts data to be sent to the device and prints it to standard output, replacing non-printable characters with dots ( . ).

  • Prints additional STOP message if requested.

  • Displays message to standard output.

stub_recv()
  • Fills the buffer up to requested length with bytes from the header_output and output members of the device structure.

  • header_output is only read once after initialization of the driver, and once after every bus_reset() operation, for example:

    • init()

    • recv(3)"STA"

    • recv(20)"RT:abcdefghijklmnopq"

    • recv(20) → "rstuvwxyzabcdefghijk"

    • bus_reset()

    • recv(20)"START:abcdefghijklmn"

    • bus_reset()

    • recv(20)"START:abcdefghijklmn

  • Prints the data to standard output replacing the non-printable characters with dot ( . ).

  • Prints optional stop message if requested.

stub_fini()
  • Flips the ready flag in the device structure to 0.

  • Displays message to standard output.

stub_set_bus_speed()
  • Generates an error if given speed is not a multiple of 100.

  • Rounds the speed down to a nearest multiple of 1000.

  • Displays message to standard output.

stub_version_info()
  • Returns library version 1.0.0.

  • Displays message to standard output.

stub_driver_info()
  • Returns:

    • 100 kHz and 400 kHz speed modes

    • 7-bit slave device address format

    • 0 verbosity

  • Displays message to standard output.

stub_bus_reset()
  • resets the index value in the device structure to 0, which will cause the header_output to be returned again

  • displays message to standard output

i2c_master_getfuncs()
  • Uses I2C_ADD_FUNC() macro to specify the implemented function in the callers structure.

Client

The simple client below connects to the device created and managed by the I2C stub driver and uses devctl() to issue requests to the driver, as shown in the diagram below:
Figure 2I2C stub client

The client parses command line for the device number, opens the device for reading and writing, performs all specified commands, and closes the device. For simplicity, only four operations are supported by the code below:

You can compile the i2c-stub-clent.c code example using:
qcc -V gcc_ntoaarch64le -o i2c-stub-client i2c-stub-client.c
Note:
Make sure to specify appropriate architecture using the -V argument. For the Raspberry Pi 4, use gcc_ntoaarch64le as above.
Copy the i2c-stub-client binary to your target device, and log in to the device as root user. Navigate to the directory you copied the binary to and execute:
# ./i2c-stub-client -n 2 -dr "0x25--20" -s "0x25-+This-is-a-test" 
i2c-stub-client: Using bus /dev/i2c2
i2c-stub: DRIVER=(3:2:0)
i2c-stub-client: DRIVER=(3:2:0)
i2c-stub: Using device slave address 0x25
i2c-stub: Receiving 20 bytes: START:abcdefghijklmn - STOP
i2c-stub-client: received data: START:abcdefghijklmn
i2c-stub: Using device slave address 0x25
i2c-stub: Sending 15 bytes: This-is-a-test.
i2c-stub-client: sent 15 bytes of data: This-is-a-test
i2c-stub-client: Closing bus.
# ./i2c-stub-client -n 2 -r 0x25--20
i2c-stub-client: Using bus /dev/i2c2
i2c-stub: Using device slave address 0x25
i2c-stub: Receiving 20 bytes: opqrstuvwxyzabcdefgh - STOP
i2c-stub-client: received data: opqrstuvwxyzabcdefgh
i2c-stub-client: No command specied.
i2c-stub-client: Closing bus.
# 
You can invoke the I2C stub client using various command line options:
I2C stub client

stub-client [options]

Options:
-n unit         Unit numnber.  Number to append to device name
                prefix /dev/i2c.  (Default: 0)
-b              Reset the device bus.
-d              Retrive driver information - speed modes, address format, and
                verbosity.
-r recv_string  Receive data from driver, using the RECV_STRING specification
                as described below.
-s send_string  Send data to driver, using the SEND_STRING specification as
                described below.

recv_string.    Concatenation of addr ( fmt ( stop ( data )))
                - addr - slave device address, for example: 0x55 or 85
                - fmt  - + - 10-bit
                       - anything else - 7-bit
                - stop - + - no stop requested
                       - anything else - stop requested
                - len  - number of bytes to receive from the driver

                For example: -r "0x25-+20"

                Which indicates receive 20 bytes from slave device address
                0x25, 7-bit address format, not stop reqiested.

send_string     Concatenation of addr ( fmt ( stop ( len )))
                - addr - slave device address, for example: 0x55 or 85
                - fmt  - + - 10-bit
                       - anything else - 7-bit
                - stop - + - no stop requested
                       - anything else - stop requested
                - data - text data to  send to the driver

                For example: -s "0x25+-Test"

                Which indicates send 5 bytes ('T' 'e' 's' 't' '\0') to slave
                device address 0x25, 10-bit address format, stop requested
                after sending.  This request will fail, because stub driver
                does not support 10-bit address format.


Example:
stub-client -n 2 -dr "0x25--20" -s "0x25--This-is-a-test"

i2c-stub-client.c code example

#include <errno.h>
#include <fcntl.h>
#include <hw/i2c.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define STRING_LEN 200u

int main(int argc, char * argv[]) {
    char bus_name[STRING_LEN];
    unsigned bus = 0;
    int c;
    int fd;
    int done = 0;
    int status = EOK;

    i2c_driver_info_t ic2_driver_info;

    struct {
        i2c_recv_t i2c_recv;
        char buf[STRING_LEN+1];
    } recv;

    struct {
        i2c_send_t i2c_send;
        char buf[STRING_LEN+1];
    } send;

    // parser common option only
    while (-1 != (c = getopt(argc, argv, ":n:"))) {
        switch(c) {
            case ':':
                fprintf(stderr, "i2c-stub-client: Missing bus number.\n");
                return EXIT_FAILURE;
            case 'n':
                bus = strtoul(optarg, &optarg, 0);
                break;
            default:
                break;
        }
    }

    snprintf(bus_name, STRING_LEN, "/dev/i2c%u", bus);

    // open the bus
    printf("i2c-stub-client: Using bus %s\n", bus_name);
    if(-1 == (fd = open(bus_name, O_RDWR))) {
        perror("open");
        return EXIT_FAILURE;
    }

    // reset getopt and run commands in order specified
    optind = 1;
    while (EOK == status && -1 != (c = getopt(argc, argv, "n:bdr:s:"))) {
        switch(c) {

            // BUS_RESET
            case 'b':
                done = !0;
                if (EOK != (status = devctl(fd, DCMD_I2C_BUS_RESET, NULL, 0, NULL))) {
                    fprintf(stderr, "i2c-stub-client: Failed to reset bus.\n");
                } else {
                    printf("i2c-stub-client: Bus reset.\n");
                }
                break;

            // DRIVER_INFO
            case 'd':
                done = !0;
                if (EOK != (status = devctl(fd, DCMD_I2C_DRIVER_INFO, &ic2_driver_info, sizeof(i2c_driver_info_t), NULL))) {
                    fprintf(stderr, "i2c-stub-client: Failed to get driver info.\n");
                } else {
                    printf("i2c-stub-client: DRIVER=(%u:%u:%u)\n", ic2_driver_info.speed_mode, ic2_driver_info.addr_mode, ic2_driver_info.verbosity);
                }
                break;

            // RECV
            case 'r':
                recv.i2c_recv.slave.addr = strtoul(optarg, &optarg, 0);
                recv.i2c_recv.slave.fmt = I2C_ADDRFMT_7BIT;
                recv.i2c_recv.len = STRING_LEN;
                recv.i2c_recv.stop = 1;
                if (*optarg) {
                    if ('+' == *optarg++) {
                        recv.i2c_recv.slave.fmt = I2C_ADDRFMT_10BIT;
                    }
                    if (*optarg) {
                        if ('+' == *optarg++) {
                            recv.i2c_recv.stop = 0;
                        }
                        if (*optarg) {
                            recv.i2c_recv.len = strtoul(optarg, &optarg, 0);
                        }
                    }
                }
                if (EOK != (status = devctl(fd, DCMD_I2C_RECV, &recv, sizeof(recv), NULL))) {
                    errno = status;
                    perror("i2c-stub-client: devctl");

                } else {
                    recv.buf[recv.i2c_recv.len] = '\0';
                    printf("i2c-stub-client: received data: %s\n", recv.buf);
                }
                break;

            // SEND
            case 's':
                send.i2c_send.slave.addr = strtoul(optarg, &optarg, 0);
                send.i2c_send.slave.fmt = I2C_ADDRFMT_7BIT;
                strcpy(send.buf, "This is test send data");
                send.i2c_send.len = strlen(send.buf)+1;
                send.i2c_send.stop = 1;
                if (*optarg) {
                    if ('+' == *optarg++) {
                        send.i2c_send.slave.fmt = I2C_ADDRFMT_10BIT;
                    }
                    if (*optarg) {
                        if ('+' == *optarg++) {
                            send.i2c_send.stop = 0;
                        }
                        if (*optarg) {
                            send.i2c_send.len = strlen(optarg);
                            if (send.i2c_send.len > STRING_LEN) {
                                send.i2c_send.len = STRING_LEN;
                                optarg[STRING_LEN] = '\0';
                            }
                            strncpy(send.buf, optarg, send.i2c_send.len);
                            send.i2c_send.len++;
                        }
                    }
                }
                send.buf[send.i2c_send.len-1] = '\0';
                if (EOK != (status = devctl(fd, DCMD_I2C_SEND, &send, sizeof(send), NULL))) {
                    errno = status;
                    perror("i2c-stub-client: devctl");

                } else {
                    recv.buf[send.i2c_send.len] = '\0';
                    printf("i2c-stub-client: sent %u bytes of data: %s\n", send.i2c_send.len, send.buf);
                }
                break;

            // ignore parsed already
            case 'n':
                break;
            default:
                fprintf(stderr, "i2c-stub-client: Unknown option %c.\n", c);
                return EXIT_FAILURE;
        }
    }

    if (!done) {
        fprintf(stderr, "i2c-stub-client: No command specied.\n");
        status = EINVAL;
    }
    
    printf("i2c-stub-client: Closing bus.\n");
    close(fd);

    if(EOK != status) {
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;

}
The above example assumes that the i2c-stub driver was started using the following command in the same shell session, which allows the driver and client output to be interleaved:
./i2c-stub -a 0x25 --b 2100 --u 2
  1. Client is invoked with arguments:
    • -n 2 - use /dev/i2c2 device.
    • -d - get driver information.
    • -r "0x25--20" - receive 20 bytes with stop from slave device with 7-bit address 0x25.
    • -s "0x25-+This-is-a-test" - send "This-is-a-test\0" without stop to slave device with 7-but address 0x25.
  2. Client opens the /dev/i2c2 device.
  3. Driver receives and services client's driver info request.
  4. Client outputs received driver info.
  5. Driver receives and services client's receive request, sets slave device address to 0x25, and receives 20 bytes ('S' 'T' 'A' 'R' 'T' ':' 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n') from the hardware device, followed by a stop.
  6. Client outputs received data.
  7. Driver receives and services client's send request, sets slave device address to 0x25, and sends 15 bytes ('T' 'h' 'i' 's' '-' 'i' 's' '-' 'a' '-' 't' 'e' 's' 't' \0) to the hardware device.
  8. Client outputs sent data.
  9. Client closes the device.
  10. Client is invoked again, this time with arguments:
    • -n 2 - use /dev/i2c2 device.
    • -r 0x25--20 - receive 20 bytes with stop from slave device with 7-bit address 0x25.
  11. Client opens the /dev/i2c2 device.
  12. Driver receives and services client's receive request, sets slave device address to 0x25, and receives 20 bytes ('o' 'p' 'q' 'r' 's' 't' 'u' 'v' 'w' 'x' 'y' 'z' 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h') from the hardware device, followed by a stop.
    • The output is different now. It continues from the a-z sequence from the previous request, rolling back to beginning of the a-z sequence when necessary.

    • START: Isn't received until bus reset request is made or the driver is restarted.

  13. Client outputs received data.
  14. Client closes the device.
Page updated: