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;
}

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐