linux gpio模拟I2C - 以rk3588为列
linux GPIO模拟I2C
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);
// 将 SCL 引脚设置为高电平
gpiod_direction_output(I2C_SCL, 1);
usleep(I2C_DELAY_US);
// 将 SDA 引脚设置为高电平
// 这将产生 I2C 总线的停止条件
gpiod_direction_output(I2C_SDA, 1);
usleep(I2C_DELAY_US);
}
// 发送ACK信号
void i2c_send_ack(int ack)
{
// 设置SDA线为输出模式
gpiod_direction_output(I2C_SDA, 0);
if (ack)
{
// 发送ACK信号, SDA线拉低
gpiod_direction_output(I2C_SDA, 0);
}
else
{
// 发送NACK信号, SDA线拉高
gpiod_direction_output(I2C_SDA, 1);
}
// 拉高SCL线1ms,然后拉低
gpiod_direction_output(I2C_SCL, 1);
usleep(I2C_DELAY_US);
gpiod_direction_output(I2C_SCL, 0);
}
// 接收ACK信号
unsigned char i2c_recv_ack(void)
{
unsigned char value = 0;
// 设置SDA线为输入模式
gpiod_direction_input(I2C_SDA);
// 拉高SCL线1ms
gpiod_direction_output(I2C_SCL, 1);
usleep(I2C_DELAY_US);
// 读取SDA线的电平状态
if (gpio_read_val(I2C_SDA))
{
value = 1; // 接收到NACK信号
}
else
{
value = 0; // 接收到ACK信号
}
// 拉低SCL线
gpiod_direction_output(I2C_SCL, 0);
// 设置SDA线为输出模式并拉高
gpiod_direction_output(I2C_SDA, 1);
return value;
}
void i2c_send_data(unsigned char data)
{
int i;
unsigned char value;
// 设置SCL线为输出模式并拉低
gpiod_direction_output(I2C_SCL, 0);
// 发送8位数据
for (i = 0; i < 8; i++)
{
// 获取当前位的值
value = (data << i) & 0x80;
// 根据当前位的值设置SDA线
if (value)
{
gpiod_direction_output(I2C_SDA, 1);
}
else
{
gpiod_direction_output(I2C_SDA, 0);
}
// 拉高SCL线1ms,然后拉低
gpiod_direction_output(I2C_SCL, 1);
usleep(I2C_DELAY_US);
gpiod_direction_output(I2C_SCL, 0);
usleep(I2C_DELAY_US);
}
}
unsigned char i2c_recv_data(void)
{
int i;
unsigned char temp = 0;
unsigned char data = 0;
// 设置SDA线为输入模式
gpiod_direction_input(I2C_SDA);
usleep(I2C_DELAY_US);
// 接收8位数据
for (i = 0; i < 8; i++)
{
// 拉低SCL线1ms
gpiod_direction_output(I2C_SCL, 0);
usleep(I2C_DELAY_US);
// 拉高SCL线1ms
gpiod_direction_output(I2C_SCL, 1);
usleep(I2C_DELAY_US);
// 读取SDA线的电平状态
data = gpio_read_val(I2C_SDA);
// 根据当前位的值更新接收数据
if (data)
{
temp = (temp << 1) | data;
}
else
{
temp = (temp << 1) & ~data;
}
}
// 拉低SCL线
gpiod_direction_output(I2C_SCL, 0);
usleep(I2C_DELAY_US);
// 设置SDA线为输出模式并拉高
gpiod_direction_output(I2C_SDA, 1);
return temp;
}
void i2c_write_reg(int addr, int reg, unsigned char value)
{
unsigned char ack;
// 开始 I2C 通信
i2c_start();
// 发送触摸屏设备地址(写操作)
i2c_send_data(addr << 1 | 0x00);
ack = i2c_recv_ack();
if (ack)
{
printf("send write + addr error\n");
goto end;
}
// 发送寄存器地址
i2c_send_data(reg);
ack = i2c_recv_ack();
if (ack)
{
printf("send reg error\n");
goto end;
}
// 发送要写入的值
i2c_send_data(value);
ack = i2c_recv_ack();
if (ack)
{
printf("send value error\n");
}
end:
// 结束 I2C 通信
i2c_stop();
}
void i2c_write_reg16(int addr, int reg, unsigned short value)
{
unsigned char ack;
// 开始 I2C 通信
i2c_start();
// 发送触摸屏设备地址(写操作)
i2c_send_data(addr << 1 | 0x00);
ack = i2c_recv_ack();
if (ack)
{
printf("send write + addr error\n");
goto end;
}
// 发送寄存器地址
i2c_send_data(reg);
ack = i2c_recv_ack();
if (ack)
{
printf("send reg error\n");
goto end;
}
// 发送要写入的值
i2c_send_data((value) >> 8 & 0x0FF); // 高字节
ack = i2c_recv_ack();
i2c_send_data(value & 0xFF);
ack = i2c_recv_ack();
if (ack)
{
printf("send value error\n");
}
end:
// 结束 I2C 通信
i2c_stop();
}
unsigned char i2c_read_reg(int addr, int reg)
{
unsigned char ack;
unsigned char data;
// 开始 I2C 通信
i2c_start();
// 发送触摸屏设备地址(写操作)
i2c_send_data(addr << 1 | 0x00);
ack = i2c_recv_ack();
if (ack)
{
printf("send write + addr error\n");
goto end;
}
// 发送要读取的寄存器地址
i2c_send_data(reg);
ack = i2c_recv_ack();
if (ack)
{
printf("send reg error\n");
goto end;
}
// 重新开始 I2C 通信,发送读操作地址
i2c_start();
i2c_send_data(addr << 1 | 0x01);
ack = i2c_recv_ack();
if (ack)
{
printf("send read + addr error\n");
goto end;
}
// 读取寄存器值
data = i2c_recv_data();
// printf("data is %d\n", data);
// 发送 ACK 以结束读操作
i2c_send_ack(0);
end:
// 结束 I2C 通信
i2c_stop();
return data;
}
unsigned short i2c_read_reg16(int addr, int reg)
{
unsigned char ack;
unsigned short data;
// 开始 I2C 通信
i2c_start();
// 发送触摸屏设备地址(写操作)
i2c_send_data(addr << 1 | 0x00);
ack = i2c_recv_ack();
if (ack)
{
printf("send write + addr error\n");
goto end;
}
// 发送要读取的寄存器地址
i2c_send_data(reg);
ack = i2c_recv_ack();
if (ack)
{
printf("send reg error\n");
goto end;
}
// 重新开始 I2C 通信,发送读操作地址
i2c_start();
i2c_send_data(addr << 1 | 0x01);
ack = i2c_recv_ack();
if (ack)
{
printf("send read + addr error\n");
goto end;
}
// 读取寄存器值
data = i2c_recv_data();
// printf("data is %d\n", data);
i2c_send_ack(1);
data = (data << 8) | i2c_recv_data();
// printf("data is %d\n", data);
// 发送 ACK 以结束读操作
i2c_send_ack(0);
end:
// 结束 I2C 通信
i2c_stop();
return data;
}
static int i2c_init(void)
{
gpio_export(I2C_SCL);
gpio_export(I2C_SDA);
// 将 GPIO 引脚设置为输出模式,并初始化为高电平
// 这是 I2C 总线的空闲状态
gpiod_direction_output(I2C_SCL, 1);
gpiod_direction_output(I2C_SDA, 1);
return 0;
}
int main()
{
i2c_init();
while(1)
{
unsigned char data = i2c_read_reg(0x40, 0x00);
printf("read: 0x%02X\n", data);
i2c_write_reg(0x40, 0x00, 0x2c);
data = i2c_read_reg(0x40, 0x00);
printf("read: 0x%02X\n", data);
unsigned short data16 = i2c_read_reg16(0x40, 0x00);
printf("data16: 0x%04X\n", data16);
i2c_write_reg16(0x40, 0x00, 0x2813);
data16 = i2c_read_reg16(0x40, 0x00);
printf("data16: 0x%04X\n", data16);
//0x3220
unsigned short device_id = i2c_read_reg16(0x40, 0xFF);
printf("DEVICE_ID_REG: 0x%04X\n", device_id);
//0x5449
unsigned short mat_id = i2c_read_reg16(0x40, 0xFE);
printf("MANUFACTURER_ID: 0x%04X\n", mat_id);
printf("----------------------------\n");
}
gpio_unexport(I2C_SCL);
gpio_unexport(I2C_SDA);
return 0;
}
static int gpio_export(int gpio_num)
{
FILE *fp = NULL;
char buf[50] = {0};
sprintf(buf, "/sys/class/gpio/export");
fp = fopen(buf, "w");
if (fp == NULL)
{
printf("Cannot open %s.\n", buf);
return -1;
}
fprintf(fp, "%d", gpio_num);
fclose(fp);
return 0;
}
static int gpio_unexport(int gpio_num)
{
FILE *fp = NULL;
char buf[50] = {0};
sprintf(buf, "/sys/class/gpio/unexport");
fp = fopen(buf, "w");
if (fp == NULL)
{
printf("Cannot open %s.\n", buf);
return -1;
}
fprintf(fp, "%d", gpio_num);
fclose(fp);
return 0;
}
static int gpio_set_val(int gpio_num, int val)
{
FILE *fp = NULL;
char file_name[1024] = {0};
unsigned char buf[2] = {0};
if (val != 1 && val != 0)
{
printf("GPIO set value error !\n");
return -1;
}
sprintf(file_name, "/sys/class/gpio/gpio%d/value", gpio_num);
fp = fopen(file_name, "rb+");
if (fp == NULL)
{
printf("Cannot open %s.\n", file_name);
return -1;
}
fprintf(fp, "%d", val);
fclose(fp);
return 0;
}
static int gpio_read_val(int gpio_num)
{
FILE *fp = NULL;
char buf[2] = {0};
char common[50] = {0};
int value = 0;
sprintf(common, "/sys/class/gpio/gpio%d/value", gpio_num);
fp = fopen(common, "rb+");
if (fp == NULL)
{
printf("read gpio fopen failed !\n");
return -1;
}
fread(buf, sizeof(char), sizeof(buf) - 1, fp);
fseek(fp, 0L, SEEK_SET);
fclose(fp);
value = buf[0] - 48;
return value;
}
static int gpio_set_direction(int gpio_num, const char *in_out)
{
FILE *fp = NULL;
char buf[50] = {0};
sprintf(buf, "/sys/class/gpio/gpio%d/direction", gpio_num);
fp = fopen(buf, "rb+");
if (fp == NULL)
{
printf("Cannot open %s.\n", buf);
return -1;
}
fprintf(fp, "%s", in_out);
fclose(fp);
return 0;
}
static int gpio_read_direction(int gpio_num)
{
FILE *fp = NULL;
char buf[3] = {0};
char common[50] = {0};
int value = 0;
sprintf(common, "/sys/class/gpio/gpio%d/direction", gpio_num);
fp = fopen(common, "rb+");
if (fp == NULL)
{
printf("read gpio fopen failed !\n");
return -1;
}
fread(buf, sizeof(char), sizeof(buf), fp);
fseek(fp, 0L, SEEK_SET);
fclose(fp);
if (strcmp(buf, "out") == 0)
value = 0;
if (strncmp(buf, "in", 2) == 0)
value = 1;
return value;
}
static void gpiod_direction_output(int gpio_num, int val)
{
gpio_set_direction(gpio_num, "out");
gpio_set_val(gpio_num, val);
return;
}
static void gpiod_direction_input(int gpio_num)
{
gpio_set_direction(gpio_num, "in");
return;
}
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)