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:
- 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. - 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.
LuckFox Pico Diagram:
LuckFox Pico Mini A/B Diagram:
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
To see the devices on the I2C-3 interface:
i2cdetect -a -y 3
To read all registers of a specific device:
i2cdump -f -y 3 0x68
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
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
Use the vi tool to open the file, paste the code, and save it.
# nano i2c.py
Run the program.
# python3 i2c.py
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).
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 toI2C_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 astruct 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 astruct i2c_smbus_ioctl_data
structure that describes the SMBus operation.I2C_RETRIES
: This is a request parameter for theioctl
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
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 afterPATH=
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
Compile the Program Using the Cross-Compilation Tool
arm-rockchip830-linux-uclibcgnueabihf-gcc i2c.c -o i2c
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
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 /Running the Program
Modify the permissions of the
i2c
file and then run the program:# chmod 777 i2c
# ./i2cExperimental Observations
The first run successfully communicated with the device with address 0x3c, but the second time the device was not connected: