Interfacing with the I2C driver

Starting the driver

This guide uses the Raspberry Pi 4 as the target. Hence, it uses the i2c-bcm2711 driver. Different boards might require a different driver, but interfacing with the driver should be similar across different boards.

To start the I2C resource manager on your target if it's not running already, you can use the command:

i2c-bcm2711

You can use the use command to display the various options you have to customize the driver settings:

root@qnxpi:/data/home/qnxuser# use i2c-bcm2711
BCM2711 I2C manager

i2c-bcm2711 [options]

Options:
-p addr         I2C base address(Default: 0)
-i irq          I2C interrupt (Default: 149)
-c clock        Default to 5000(100KHz)
-v              Set verbose(Default: 2)
-s slave addr   Slave address(only 7-bit addr supported. Default: 0)


Generic options:
--b bus_speed       Default bus speed. (Default: 100000)
--u unit            Unit number. Number to append to device name
                    prefix (/dev/i2c). (Default: 0)

Example:
i2c-bcm2711  -p0xfe804000 --b100000 --u1

Send data to the I2C device

Once you've started the I2C driver, it will expose a path under /dev/i2c*. You can use the devctl() command to interface with the I2C driver and can find it's format in hw/i2c.h. The example uses this sample I2C code to explain the interface.

The following example reads a value from a slave device to explain the interface. Start with defining the read data structure in your application:

struct i2c_recv_data_msg_t
{
    i2c_sendrecv_t hdr;
    uint8_t bytes[0];
};

Fill out the i2c_sendrecv_t structure with relevant data:

typedef struct {
    i2c_addr_t          slave;      /* slave address */
    _Uint32t            send_len;   /* length of send data in bytes */
    _Uint32t            recv_len;   /* length of receive data in bytes */
    _Uint32t            stop;       /* set stop when complete? */
} i2c_sendrecv_t;

Fill out the i2c_addr_t structure, which is set up for the slave device:

typedef struct {
    _Uint32t            addr;   /* I2C address */
    _Uint32t            fmt;    /* 7- or 10-bit format */
} i2c_addr_t;

Filling up the data may look like the following:

// Allocate memory for the message
struct i2c_recv_data_msg_t *msg = NULL;
msg = malloc(sizeof(struct i2c_recv_data_msg_t) + MIN_READ_BYTES); // allocate enough memory for both the calling information and received data
if (!msg)
{
    perror("alloc failed");
    return I2C_ERROR_ALLOC_FAILED;
}

// Assign which register
msg->bytes[0] = register_val;

// Assign the I2C device and format of message
msg->hdr.slave.addr = i2c_address;
msg->hdr.slave.fmt = I2C_ADDRFMT_7BIT;
msg->hdr.send_len = 1;
msg->hdr.recv_len = 1;
msg->hdr.stop = 1;

Once you've filled out the data, you can finally send the data to the resource manager using the devctl() command:

int status; // status information about the devctl() call
int err = devctl(smbus_fd[bus_number], DCMD_I2C_SENDRECV, msg, sizeof(*msg) + 1, (&status)); // specify DCMD_I2C_SENDRECV to indicate that we are using the i2c_sendrecv_t data structure

Refer to the smbus_read_data_byte() function in the i2c.c file for more detailed information. You can implement a similar approach for other devctl() DCMDs.

You can find a list of the devctl() commands in the "Resource manager interface" section of the Customizing a BSP guide.

Sample I2C device

Now that you have the necessary functions to communicate with your I2C device in the rpi_i2c folder, take a look at this example user application code that talks with the I2C device SN3218A.

Here is the specification of SN3218A:

  • I2C Address: 0x54

  • Register Map:

    • 0x00: Shutdown register (0 = shutdown, 1 = normal operation)

    • 0x01-0x12: PWM brightness registers (18 registers, one per channel)

    • 0x13-0x15: LED enable registers (3 registers, each controlling 6 channels)

    • 0x16: Update register (write any value to apply brightness changes)

    • 0x17: Reset register (write 0xFF to reset the chip)

Now that you have the device specification, you can control the device by writing values to it. For example, to reset the chip and get the device out of shutdown mode, you can use the function smbus_write_byte_data():

// 1) Reset the chip
rc = sn3218_reset();
if (rc != I2C_SUCCESS) {
    fprintf(stderr, "Error resetting SN3218.\n");
    return rc;
}

// 2) Exit shutdown (write 0x01 to REG_SHUTDOWN)
rc = sn3218_write_byte(REG_SHUTDOWN, 0x01);
if (rc != I2C_SUCCESS) {
    fprintf(stderr, "Error enabling SN3218 output.\n");
    return rc;
}

You can also utilize the smbus_write_byte_data() to set the brightness of every LED to maximum (255):

// 3) Enable all 18 channels.
// Each LED control register (0x13, 0x14, 0x15) controls 6 channels (bits D5:D0).
// To enable each channel, write 0x3F (binary 00111111) to each register.
int rc;

// Enable all channels (registers 0x13, 0x14, 0x15)
for (int i = 0; i < 3; i++) {
    rc = smbus_write_byte_data(1, 0x54, 0x13 + i, 0x3F);
    if (rc != I2C_SUCCESS) {
        fprintf(stderr, "Error enabling SN3218 channels");
        return rc;
    }
    usleep(1000); // 1 ms delay between writes
}

// 4) Set all channels to maximum brightness (255)
uint8_t g_brightness[18]; // 18 channels
for (int i = 0; i < 18; i++) {
    g_brightness[i] = 255;
}

// Write brightness values to all 18 PWM registers (0x01-0x12)
for (int i = 0; i < 18; i++) {
    rc = smbus_write_byte_data(1, 0x54, 0x01 + i, g_brightness[i]);
    if (rc != I2C_SUCCESS) {
        fprintf(stderr, "Error writing brightness for channel %d.\n", i);
        return rc;
    }
    usleep(1000); // 1 ms delay between writes
}

// 5) Latch changes by writing to REG_UPDATE (0x16).
rc = smbus_write_byte_data(1, 0x54, 0x16, 0x00);
if (rc != I2C_SUCCESS) {
    fprintf(stderr, "Error updating SN3218.\n");
    return rc;
}

Check out the sample application for a more detailed implementation with testing functions.

Porting Linux I2C device to QNX

When porting an I2C device from Linux to QNX, the key difference lies in how the I2C bus is accessed. In Linux, communication typically involves ioctl() calls with commands like I2C_SLAVE and I2C_RDWR to set the slave address and perform read/write operations. Additionally, Linux allows direct use of read() and write() system calls for basic I2C communication. In contrast, QNX doesn't support read() or write() for I2C communication; instead, it relies exclusively on devctl() with commands such as DCMD_I2C_SEND and DCMD_I2C_SENDRECV. It's also important to note that QNX and Linux uses different message structures to communicate with the I2C device. For example, Linux may use i2c_rdwr_ioctl_data and i2c_msg for their data struct to write, while QNX may use i2c_sendrecv_t and i2c_addr_t.

Page updated: