linux GPIO模拟I2C - 以rk3588为列
一、使用内核模块i2c-gpio.c实现
该文件已经实现gpio模拟i2c的所有流程和功能,对于应用层来说操作都是一样的,无需关心其他问题,个人比较推荐该方式,使用该方式需要配置以下选项:
1.内核开启i2c_gpio支持
Device Drivers->
I2C support --->
I2C Hardware Bus support --->
<*> GPIO-based bitbanging I2C
开启后defconfig中会增加 CONFIG_I2C_GPIO=y
对应指令:
make menuconfig
make savedefconfig
2.设备树配置对应IO
在配置IO之前,请确认该IO没有被使用,并且是GPIO功能,可以使用以下指令查看(以下是我使用的IO)
cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins
配置设备树
aliases {
i2c15 = &i2c15;
};
i2c15:i2c15_gpio{
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
compatible = "i2c-gpio";
gpios = <&gpio2 RK_PB4 GPIO_ACTIVE_HIGH>,//sda
<&gpio2 RK_PB5 GPIO_ACTIVE_HIGH>;//scl
i2c-gpio,delay-us = <2>; /* ~100 kHz */
};
有几个注意点:
- 新增的I2C节点,需要配置到 /{ }设备数的根节点下
- compatible 属性需配置正确,与i2c-gpio.c内属性匹配
- 配置引脚时先SDA再SCL
- aliases属性下增加对应名字,在匹配完成后,会在/dev目录下生成指定的设备节点(/dev/i2c-15)
3.以上配置完成后,重新编译内核并烧录
烧录完成后可以在/dev/目录下找到新加的i2c-15,此时可以使用i2ctool进行调试,与硬件I2C操作一致
二、应用层直接模拟I2C
1.该方式就只需两个普通IO即可,i2c读写时序都由应用层自己控制,下面简单说明时序部分:
I2C的数据格式:
无数据:SCL=1,SDA=1
开始位(Start):当SCL=1时,SDA由1向0跳变;
停止位(Stop):当SCL=1时,SDA由0向1跳变;
数据位:当SCL由0向1跳变时,由发送方控制SDA,此时SDA为有效数 据,不可随意改变SDA;当SCL保持为0时,SDA上的数据可随意改变;
地址位:定义同数据位,但只由Master发给Slave;
应答位(ACK):当发送方传送完8位时,发送方释放SDA,由接收方控制SDA,且SDA=0;
否应答位(NACK):当发送方传送完8位时,发送方释放SDA,由接收方控制SDA,且SDA=1。
数据为单字节传送时,格式为:
开始位,8位地址位(含1位读写位),应答,8位数据,应答,停止位。
数据为一串字节传送时,格式为:
开始位,8位地址位(含1位读写位),应答,8位数据,应答,8位数据,应答,……,8位数据,应答,停止位。
2.I2C读写时ACK和NACK尤为重要(以下做说明):
在I2C通信中,**ACK(Acknowledge)和NAK(Not Acknowledge)**是通过数据线(SDA线)上的信号来实现的。这两个信号用于确认或拒绝接收数据,确保数据传输的正确性和可靠性。
2.1 ACK(确认信号)
- ACK信号表示接收方成功接收了发送方发送的数据。
- 发送方在发送完数据字节后,期望接收方会拉低SDA线(SDA=0),表示接收成功。
- 在I2C协议中,ACK通常发生在每个字节传输后,除了最后一个字节之外。发送完数据字节后,接收方在下一个时钟周期(SCL的高电平)将SDA拉低,以表示接收了该字节,继续接收后续的数据。
2.2 NAK(拒绝信号)
- NAK信号表示接收方没有成功接收数据,或者表示这是最后一个字节的传输。
- 在I2C协议中,NAK通常出现在最后一个数据字节的读取过程中。接收方发送NAK,告诉发送方已经接收完所有需要的数据。
- 在读取数据时,当接收方不希望再接收更多字节时,它会在最后一个字节后发送NAK。此时,SDA保持为高电平(SDA=1),并且发送方会停止传输。
2.3 ACK和NAK的实现机制
2.3.1 写操作时的ACK和NAK
- 在I2C写操作中,发送方向接收方传送字节后,接收方会回应一个ACK或NAK。
- 发送方每发送完一个字节后,都需要等待接收方的响应(ACK或NAK)。接收方如果成功接收到字节,它会通过将SDA拉低(ACK)回应发送方。如果接收方没有成功接收字节,或者它不想接收更多数据,它会通过将SDA保持为高电平(NAK)来回应。
2.3.2 读操作时的ACK和NAK
- 在I2C读操作中,接收方每读一个字节后,也需要回应ACK或NAK。
- 在I2C读取数据的过程中,接收方每读取完一个字节后,会发送一个ACK,告诉发送方它已经成功接收了该字节。如果接收方已经不再需要更多的数据,它会在最后一个字节读取后发送NAK,表示不再接收数据。
2.4 图示解释:
假设我们有一个I2C读操作,操作顺序如下:
Master (发送方) Slave (接收方)
SCL ↑ SCL ↑
SDA ↓ ------------> SDA ↓ <- ACK (接收方确认数据)
SDA ↑ SDA ↑ <- 发送字节
SCL ↓ SCL ↓
...
ACK/NAK的时序:
每传输一个字节(8位数据)后,接收方必须给出ACK
最后一个字节传输后,接收方发送NAK(SDA保持为1),表示数据传输结束
2.5 代码中实现ACK和NAK
-
读取字节时:
如果你正在读取多个字节,通常每个字节都需要发送ACK,除了最后一个字节外,最后一个字节应该发送NAK。 -
写入字节时:
在每个字节传输后,接收方会向发送方发出ACK或NAK。发送方需要等待读取该确认信号后,才能继续下一步操作。 -
ACK/NAK的具体实现:
在每次读取完字节后,如果需要继续读取下一个字节,ack
参数会传递1
,表示发送ACK。
如果是最后一个字节,ack
会传递0
,表示发送NAK,告诉发送方这次读操作结束。
2.6 模拟i2c时钟的选择和计算(100k为列)
- I2C协议要求SCL信号在每个比特的高电平和低电平之间交换,通常我们在每次操作中,会将SCL的高电平和低电平分为均等的部分,这意味着每个状态(高电平或低电平)的时间为5微秒。因此设置I2C scl 半周期为5us来保证SCL的周期为10us,以实现100kHz的I2C时钟
3.以下为个人应用层模拟使用的程序(测试正常)
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
// 定义 I2C 总线的时钟线和数据线对应的 GPIO 引脚编号
#define I2C_SCL 21 // GPIO0_PC5
#define I2C_SDA 20 // GPIO0_PC4
#define I2C_DELAY_US 5 // I2C bit period for 100kHz I2C clock
static int gpio_export(int gpio_num);
static int gpio_unexport(int gpio_num);
static int gpio_set_direction(int gpio_num, const char *in_out);
static int gpio_read_direction(int gpio_num);
static int gpio_set_val(int gpio_num, int val);
static int gpio_read_val(int gpio_num);
static void gpiod_direction_output(int gpio_num, int val);
static void gpiod_direction_input(int gpio_num);
// I2C 起始条件函数
void i2c_start(void)
{
// 将 SCL 和 SDA 引脚设置为输出模式,并初始化为高电平
// 这是 I2C 总线的空闲状态
gpiod_direction_output(I2C_SCL, 1);
gpiod_direction_output(I2C_SDA, 1);
usleep(I2C_DELAY_US);
// 将 SDA 引脚设置为低电平,保持 SCL 为高电平
// 这将产生 I2C 总线的起始条件
gpiod_direction_output(I2C_SDA, 0);
usleep(I2C_DELAY_US);
// 将 SCL 引脚设置为低电平
// 起始条件建立完成
gpiod_direction_output(I2C_SCL, 0);
usleep(I2C_DELAY_US);
}
// I2C 停止条件函数
void i2c_stop(void)
{
// 将 SCL 和 SDA 引脚设置为低电平
gpiod_direction_output(I2C_SCL, 0);
gpiod_direction_output(I2C_SDA, 0);
usleep(I2C_DELAY_US