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
.