Skip to main content

I2C Communication

In this chapter, we will learn how to communicate with external devices using the I2C bus at the application layer.

Sample program : Code.zip

1. I2C Subsystem

In the Linux operating system, the I2C subsystem is a crucial driver framework used to manage and control various external devices connected via the I2C bus. More detailed information about the I2C subsystem can be found in the <Linux Kernel Source>/Documentation/i2c directory. Key components of the I2C subsystem include:

  1. Sysfs Interface: The I2C subsystem provides a user-space interface through the sysfs file system, allowing users to access and configure information related to I2C devices. The /sys/bus/i2c/devices directory is used to manage and configure the properties and status information of I2C devices. Users can read and write sysfs files to obtain device information or control devices.
  2. I2C Device Nodes: Typically, character device nodes like /dev/i2c-3 are created in the /dev directory. These nodes enable communication between user space and specific I2C devices or I2C adapters. Through these nodes, users can send and receive data to interact with I2C devices.

2. I2C Testing(Shell)

2.1 Pin Distribution

To enable I2C, refer to the PWM section.Development boards have the I2C3 interface enabled by default. You can determine the corresponding pins through the pinout diagram. For example, on the LuckFox Pico board, the I2C3 interface corresponds to pins 58 and 59.

  1. LuckFox Pico Diagram:

  2. LuckFox Pico Mini A/B Diagram:

  3. LuckFox Pico Plus Diagram:
    2.2 View Devices

In the /sys/bus/i2c/devices directory, each I2C device has its own folder. The folder names typically include 'i2c' and the device number. For example, /sys/bus/i2c/devices/i2c-3 represents an I2C bus with the number 3. If you want to see the I2C buses available in your system, you can use the following command:

# ls /sys/bus/i2c/devices/
4-0030 4-0030-1 i2c-4 4-0030-2 i2c-3

2.3 I2C Testing

  1. To see the devices on the I2C-3 interface:

    i2cdetect -a -y 3
  2. To read all registers of a specific device:

    i2cdump  -f -y 3 0x68
  3. To read a specific register of a specific I2C device, for example, to read register 0x01 from a device at address 0x68:

    i2cget -f -y 3 0x68 0x01
  4. To write a value to a specific register of a specific I2C device, for example, to set register 0x01 of a device at address 0x68 to 0x6f:

    i2cset -f -y 3 0x68 0x01 0x6f

3. I2C Communication (Python Program)

3.1 Complete Code

With the following program, scanning devices on the I2C-3 bus can be achieved.

import smbus

def main():
data = [0x01, 0x02]

try:
i2c_bus = smbus.SMBus(3)

print("i2cdetect addr: ", end="")
for address in range(0x7F):
try:
i2c_bus.write_i2c_block_data(address, 0, data)
print("0x{:02X},".format(address), end="")
except OSError:
pass
print()

except Exception as e:
print(f"An error occurred: {e}")

finally:
if i2c_bus:
i2c_bus.close()

if __name__ == "__main__":
main()

3.2 Open I2C Device

In this code, the SMBus class from the smbus library is used to open an I2C device. SMBus provides a simple interface for communication with I2C devices. In this example, an instance i2c_bus is opened for the I2C bus number 3, which will be used for subsequent I2C communication.

i2c_bus = smbus.SMBus(3)  

3.3 Send Data

This code uses the write_i2c_block_data method to send a block of data to all possible I2C addresses. By iterating over range(0x7F), it attempts to send the data block to each I2C address. If successful, it prints the corresponding I2C address (0x00 to 0x7F). If sending fails due to an OSError exception, it ignores the exception and continues to try the next address.

for address in range(0x7F):
try:
i2c_bus.write_i2c_block_data(address, 0, data)
print("0x{:02X},".format(address), end="")
except OSError:
pass

3.4 Run the Program

  1. Use the vi tool to open the file, paste the code, and save it.

    # nano i2c.py
  2. Run the program.

    # python3 i2c.py
  3. Experimental Observations

    The program successfully communicates with the device at address 0x3C in the first run. In the second run, no device is connected (if the device is still not detected after connection, please refer to section 5.1, and configure the pins as pull-up).
    image

4. I2C Communication (C Program)

4.1 ioctl Function

When writing an application, you need to use the ioctl function to configure I2C-related settings. Its function prototype is as follows:

 #include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);

When using the ioctl function for I2C communication, common values for the request parameter include:

  • I2C_SLAVE: Used to set the I2C slave address, with the parameter being the integer value of the slave address.
  • I2C_SLAVE_FORCE: Similar to I2C_SLAVE, but it doesn't check whether the device exists. If an invalid I2C slave address is used, it won't return an error.
  • I2C_TENBIT: Used to enable or disable 10-bit address mode. The parameter is an integer, where 0 means disabled, and non-zero means enabled.
  • I2C_RDWR: Performs I2C read and write operations, with the parameter being a pointer to a struct i2c_rdwr_ioctl_data structure that contains a series of read and write operations.
  • I2C_SMBUS: Used to perform read and write operations using the SMBus protocol, with the parameter being a pointer to a struct i2c_smbus_ioctl_data structure that describes the SMBus operation.
  • I2C_RETRIES: This is a request parameter for the ioctl function and is used to set the number of retries for I2C bus transactions. In I2C communication, there may be communication failures due to various reasons (e.g., bus conflicts, device not responding), and this parameter allows you to specify how many times to retry before giving up.

4.2 Sample Program

With the following program, you can perform a device scan on the I2C-3 bus.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>

#define I2C_DEVICE_PATH "/dev/i2c-3"

int main() {
uint8_t data[2] = {0x01,0x02};

const char *i2c_device = I2C_DEVICE_PATH;
int i2c_file;

if ((i2c_file = open(i2c_device, O_RDWR)) < 0) {
perror("Failed to open I2C device");
return -1;
}

ioctl(i2c_file, I2C_TENBIT, 0);
ioctl(i2c_file, I2C_RETRIES, 5);

printf("i2cdetect addr : ");
for (int x = 0; x < 0x7f; x++)
{
if (ioctl(i2c_file, I2C_SLAVE, x) < 0) {
perror("Failed to set I2C slave address");
close(i2c_file);
return -1;
}

if (write(i2c_file, data, 2) == 2)
{
printf("0x%x,", x);
}
}

close(i2c_file);
printf("\r\n");

return 0;
}

4.3 File Path

This line of code defines a macro to store the path of the I2C device file.

#define I2C_DEVICE_PATH "/dev/i2c-3"

4.4 Open I2C Device

This section of code opens the specified I2C device file for reading and writing.

if ((i2c_file = open(i2c_device, O_RDWR)) < 0) {
perror("Failed to open I2C device");
return -1;
}

4.5 Configure I2C

This code uses the ioctl function to configure I2C communication. First, it sets the I2C bus to standard 7-bit address mode with ioctl(i2c_file, I2C_TENBIT, 0). Then, it sets the number of retries for I2C communication to 5 with ioctl(i2c_file, I2C_RETRIES, 5).

ioctl(i2c_file, I2C_TENBIT, 0);
ioctl(i2c_file, I2C_RETRIES, 5);

4.6 Send Data

In this code section, it sets the I2C slave address to x using ioctl(i2c_file, I2C_SLAVE, x). Then, it uses the write function to write a data block data containing 2 bytes of data to the I2C device. If the 2 bytes of data are successfully written, it prints the address of the I2C device, indicating that the data has been sent successfully.

printf("i2cdetect addr : ");
for (int x = 0; x < 0x7f; x++)
{
if (ioctl(i2c_file, I2C_SLAVE, x) < 0) {
perror("Failed to set I2C slave address");
close(i2c_file);
return -1;
}

if (write(i2c_file, data, 2) == 2)
{
printf("0x%x,", x);
}
}

4.7 Cross-Compilation

  1. Specify the Cross-Compilation Tool

    First, you need to add the path to the cross-compilation tool to the system's PATH environment variable so that you can use the cross-compilation tool from anywhere. You can add the following line to your shell configuration file (usually ~/.bashrc or ~/.bash_profile or ~/.zshrc, depending on your shell). Note that the path after PATH= should point to the directory where the cross-compilation tool is located.

    • gcc path

      <SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc
    • Open the shell configuration file.

      nano ~/.bashrc 
    • Add the path of the cross-compilation tool to the system's PATH environment variable. Replace <SDK Directory> with your own SDK path, such as /home/luckfox/luckfox-pico/.

      export PATH=<SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:$PATH
    • Reload the shell configuration file to apply the changes:

      source ~/.bashrc  
  2. Compile the Program Using the Cross-Compilation Tool

    arm-rockchip830-linux-uclibcgnueabihf-gcc i2c.c -o i2c
  3. After successful cross-compilation, an executable file that can run on the development board will be generated in the current directory.

    # ls
    i2c i2c.c

4.8 Running the Program

  1. File Transfer

    First, transfer the i2c program from the virtual machine to Windows, and then transfer it to the development board via TFTP or ADB. Here are the steps to transfer the file from Windows to the development board using ADB:

    adb push path_to_file destination_on_development_board

    eg: (Transferring the `i2c` file from the current directory to the root directory of the development board)
    adb push i2c /
  2. Running the Program

    Modify the permissions of the i2c file and then run the program:

    # chmod 777 i2c
    # ./i2c
  3. Experimental Observations

    The first run successfully communicated with the device with address 0x3c, but the second time the device was not connected:
    image