跳到主要内容

I2C 通信

在本章中,我们将学习如何在应用层中使用 I2C 总线与外部设备的通讯。

示例程序:Code.zip

1.I2C子系统

在 Linux 操作系统中,I2C 子系统是一个关键的驱动框架,用于管理和控制通过 I2C 总线连接的各种外部设备,有关 I2C 子系统的更多详细信息可以在 <Linux内核源码>/Documentation/i2c 目录中找到。I2C 子系统的关键组成部分:

  1. sysfs 接口:I2C 子系统通过 sysfs 文件系统提供用户空间接口,以便用户可以访问和配置与 I2C 设备相关的信息。其中 /sys/bus/i2c/devices 目录用于管理和配置 I2C 设备的属性和状态信息。用户可以通过读写 sysfs 文件来获取设备信息或控制设备。
  2. I2C 设备节点:在 /dev 目录下,通常会创建类似 /dev/i2c-3 的字符设备节点,这些节点允许用户在用户空间与特定的 I2C 设备或 I2C 适配器进行通信。通过这些节点,用户可以发送和接收数据,以实现对 I2C 设备的操作。

2.I2C测试(Shell)

2.1 引脚分布

开启 I2C 的方法参考 PWM 部分。LuckFox Pico Ultra/Ultra W 引脚图:

2.2 查看设备

/sys/bus/i2c/devices 目录中,每个 I2C 设备都有自己的文件夹。这些文件夹的名称通常包含 i2c 和设备编号,例如 /sys/bus/i2c/devices/i2c-3 表示 I2C 总线编号为 3 的设备。如果您想查看系统存在的 I2C 总线,可以使用如下命令:

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

2.3 I2C测试

  1. 查看i2c-3接口上的设备(若连接设备后检测不到请查看5.1的内容,将引脚配置为上拉)

    i2cdetect -a -y 3
  2. 读取指定设备的全部寄存器的值

    i2cdump  -f -y 3 0x68
  3. 读取指定IIC设备的某个寄存器的值,如下读取地址为0x68器件中的0x01寄存器值

    i2cget -f -y 3 0x68 0x01
  4. 写入指定IIC设备的某个寄存器的值,如下设置地址为0x68器件中的0x01寄存器值为0x6f

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

3.I2C通讯(Python程序)

3.1 完整代码

通过以下程序,可以实现 i2c-3 总线上的设备扫描。

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 打开I2C设备

在这段代码中,使用 smnbus 库的 SMBus 类打开了一个I2C设备。SMBus 提供了一个简单的接口来与I2C设备进行通信。在此例中,通过 SMBus(3) 打开了I2C总线号为3的设备。这个实例 i2c_bus 被用于后续的I2C通信。

i2c_bus = smbus.SMBus(3)  

3.3 发送数据

这段代码通过 write_i2c_block_data 方法向所有可能的I2C地址发送数据块。通过迭代 range(0x7F) ,尝试向每个I2C地址发送 data 数组中的数据块。如果成功发送,会打印出相应的I2C地址(0x00到0x7F),如果由于 OSError 异常发送失败,则忽略该异常,继续尝试下一个地址。

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 运行程序

  1. 使用 nano 工具打开文件,粘贴并保存代码

    # nano i2c.py
  2. 运行程序

    # python3 i2c.py
  3. 实验现象

    第一次运行成功与地址为0x3c的设备通信,第二次未连接设备(若连接设备后依然检测不到请查看5.1的内容,将引脚配置为上拉):
    image

4.I2C通讯(C程序)

4.1 ioctl函数

在编写应用程序时需要使用ioctl函数设置i2c相关配置,其函数原型如下

 #include <sys/ioctl.h>

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

当使用ioctl函数进行I2C通信时,常用的request参数主要有以下几种:

  • I2C_SLAVE:用于设置I2C从机地址,参数是从机地址的整数值。
  • I2C_SLAVE_FORCE:类似于 I2C_SLAVE,但是不会检查设备是否存在。如果使用了一个不存在的I2C从机地址,也不会返回错误。
  • I2C_TENBIT:用于启用或禁用10位地址模式。参数为一个整数,0表示禁用,非零表示启用。
  • I2C_RDWR:执行I2C读写操作,参数是一个指向 struct i2c_rdwr_ioctl_data 结构的指针,该结构包含一系列读写操作。
  • I2C_SMBUS:用于执行SMBus协议的读写操作,参数是一个指向 struct i2c_smbus_ioctl_data 结构的指针,该结构描述了SMBus操作。
  • I2C_RETRIES:是ioctl函数的一个请求参数,用于设置I2C总线事务的重试次数。在I2C通信中,由于各种原因(例如总线冲突、设备未响应等),可能会出现通信失败的情况,该参数允许您指定在放弃通信之前尝试多少次重试。

4.2 完整代码

通过以下程序,可以实现 i2c-3 总线上的设备扫描。

#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 文件路径

这行代码定义了一个宏,用于存储 I2C 设备文件的路径。

#define I2C_DEVICE_PATH "/dev/i2c-3"

4.4 打开I2C设备

这部分代码使用读写的方式打开指定的 I2C 设备文件。

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

4.5 配置I2C

这段代码使用 ioctl 函数来配置 I2C 通信。首先,通过 ioctl(i2c_file, I2C_TENBIT, 0) 设置了 I2C 总线为标准 7 位地址模式,而后,通过 ioctl(i2c_file, I2C_RETRIES, 5) 设置了 I2C 通信的重试次数为 5 次。

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

4.6 发送数据

在这段代码中,首先,通过 ioctl(i2c_file, I2C_SLAVE, x) 将 I2C 从机地址设置为 x。接着使用 write 函数向 I2C 设备写入了一个包含 2 字节数据的数据块 data。如果成功写入了 2 字节数据,会打印该 I2C 设备的地址,表示数据已成功发送。

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 交叉编译

  1. 指定交叉编译工具

    首先,我们要将交叉编译工具的路径添加到系统的 PATH 环境变量中,以便可以在任何地方使用交叉编译工具,您可以在shell配置文件中添加以下行(通常是 ~/.bashrc~/.bash_profile~/.zshrc,具体取决于您使用的shell),注意 PATH= 后的路径为交叉编译工具所在的目录。

    • gcc路径

      <SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc
    • 打开shell配置文件

      vi ~/.bashrc 
    • 将交叉编译工具的路径添加到系统的PATH环境变量中,将 <SDK Directory> 修改为自己的 SDK 路径,如 /home/luckfox/luckfox-pico/

      export PATH=<SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:$PATH
    • 重新加载shell配置文件,使更改生效

      source ~/.bashrc  
  2. 使用交叉编译工具编译程序

    arm-rockchip830-linux-uclibcgnueabihf-gcc i2c.c -o i2c
  3. 交叉编译成功后,将在当前目录下生成可在开发板运行的可执行文件

    # ls
    i2c i2c.c

4.8 运行程序

  1. 文件传输

    先将 i2c 从虚拟机传输到 Windows,再通过 TFTP 或 ADB 传输到开发板,将文件从 Windows 通过 ADB 将文件传输到开发板的步骤如下:

    adb push 文件所在路径 开发板存储路径

    eg:(将当前目录下i2c文件传输到开发板的根目录)
    adb push i2c /
  2. 运行程序

    修改 i2c 文件的操作权限后运行程序

    # chmod +x i2c
    # ./i2c