背景

最近一个项目调试屏幕驱动,通信接口是I2C,由于硬件设计上的原因,不得不使用 GPIO 模拟 I2C 的方式与屏幕通信,所以在网上找了一下模拟 I2C 的实现,最终选择了 github 上的 soft-i2c 项目,链接: https://github.com/liyanboy74/soft-i2c ,借助软件模拟 I2C 代码的实现,再来学习一下 I2C 通信协议。

I2C 介绍

I2C 在硬件上有两个信号信号线,SCL 和 SDA。

  • SCL :时钟信号,由主机控制。
  • SDA: 数据信号,主机和从机都可以控制,SDA 带上拉电阻。

由于只有一根数据线,所以主机和从机之间只能是半双工通信。

I2C 通信的几个阶段:

  • 空闲状态:SCL 和 SDA 都为 1,高电平。

  • 发送起始信号:主机首先拉低 SDA,然后拉低 SCL,表示开始通信。

  • 数据通信:发送数据和应答。发送数据时,在 SCL上升沿采样数据,期间需要保持 SDA 信号不变,然后在 SCL下降沿修改数据。

    每发送一个字节数据都要回应,ACK 是低电平有效,0 表示接收成功,1 表示失败。

    标准通信协议:先发 7 位从机地址和读写标志(0 表示写,1 表示读),后面根据从机协议定义,再发寄存器地址或者其他控制指令。

  • 发送结束信号:主机首先拉高 SCL,然后拉高 SDA,表示通信结束。

其他的特点总结:

  • 采用主从结构,支持一主多从。
  • 可靠性高,每发送一个字节都有应答 ACK 。

接口定义

数据结构定义了硬件相关的一些操作,比如设置 GPIO 的输入输出方向、读写 GPIO 电平 、硬件初始化和微秒级延时。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20

typedef enum
{
    HAL_IO_OPT_SET_SDA_LOW = 0,
    HAL_IO_OPT_SET_SDA_HIGH,
    HAL_IO_OPT_SET_SCL_LOW,
    HAL_IO_OPT_SET_SCL_HIGH,
    HAL_IO_OPT_SET_SDA_INPUT,
    HAL_IO_OPT_SET_SDA_OUTPUT,
    HAL_IO_OPT_SET_SCL_INPUT,
    HAL_IO_OPT_SET_SCL_OUTPUT,
    HAL_IO_OPT_GET_SDA_LEVEL,
    HAL_IO_OPT_GET_SCL_LEVEL,
}hal_io_opt_e;

typedef struct sw_i2c_s {
    int (*hal_init)(void);
    int (*hal_io_ctl)(hal_io_opt_e opt, void *arg);
    void (*hal_delay_us)(uint32_t us);
} sw_i2c_t;

在应用接口定义上,主要是初始化和读写数据的接口。

1
2
3
4
5
6
7
/* functions */
void SW_I2C_initial(sw_i2c_t *d);
uint8_t SW_I2C_Read_8addr(sw_i2c_t *d, uint8_t IICID, uint8_t regaddr, uint8_t *pdata, uint8_t rcnt);
uint8_t SW_I2C_Read_16addr(sw_i2c_t *d, uint8_t IICID, uint16_t regaddr, uint8_t *pdata, uint8_t rcnt);
uint8_t SW_I2C_Write_8addr(sw_i2c_t *d, uint8_t IICID, uint8_t regaddr, uint8_t *pdata, uint8_t rcnt);
uint8_t SW_I2C_Write_16addr(sw_i2c_t *d, uint8_t IICID, uint16_t regaddr, uint8_t *pdata, uint8_t rcnt);
uint8_t SW_I2C_Check_SlaveAddr(sw_i2c_t *d, uint8_t IICID);

接口实现解析

移植

最新的代码剥离了硬件相关的逻辑,方便移植,根据具体平台实现 sw_i2c_t 结构体中的硬件相关的操作即可。下面是展锐 8850 平台的移植代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include "sw_i2c.h"

#include "ql_api_osi.h"
#include "ql_gpio.h"

#define AIP_SPI_DIO GPIO_35
#define AIP_SPI_CLK GPIO_37

#define SW_I2C1_SCL_PIN     AIP_SPI_CLK
#define SW_I2C1_SDA_PIN     AIP_SPI_DIO


static int sw_i2c_port_initial(void)
{
    ql_gpio_init(AIP_SPI_DIO, GPIO_OUTPUT, PULL_NONE, LVL_HIGH); // DIO
    ql_gpio_init(AIP_SPI_CLK, GPIO_OUTPUT, PULL_NONE, LVL_HIGH); // CLK
    ql_delay_us(SW_I2C_WAIT_TIME);
    return 0;
}

static void sw_i2c_port_delay_us(uint32_t us)
{
    ql_delay_us(us);
}

static int sw_i2c_port_io_ctl(uint8_t opt, void *param)
{
    int ret = -1;
    ql_LvlMode l = 0;
    switch (opt)
    {
    case HAL_IO_OPT_SET_SDA_HIGH:
        ql_gpio_set_level(SW_I2C1_SDA_PIN, LVL_HIGH);
        break;
    case HAL_IO_OPT_SET_SDA_LOW:
        ql_gpio_set_level(SW_I2C1_SDA_PIN, LVL_LOW);
        break;
    case HAL_IO_OPT_GET_SDA_LEVEL:
        ql_gpio_get_level(SW_I2C1_SDA_PIN, &l);
        ret = l;
        break;
    case HAL_IO_OPT_SET_SDA_INPUT:
        ql_gpio_set_direction(SW_I2C1_SDA_PIN, GPIO_INPUT);
        ql_gpio_set_pull(SW_I2C1_SDA_PIN, PULL_NONE);
        break;
    case HAL_IO_OPT_SET_SDA_OUTPUT:
        ql_gpio_set_direction(SW_I2C1_SDA_PIN, GPIO_OUTPUT);
        break;
    case HAL_IO_OPT_SET_SCL_HIGH:
        ql_gpio_set_level(SW_I2C1_SCL_PIN, LVL_HIGH);
        break;
    case HAL_IO_OPT_SET_SCL_LOW:
        ql_gpio_set_level(SW_I2C1_SCL_PIN, LVL_LOW);
        break;
    case HAL_IO_OPT_GET_SCL_LEVEL:
        ql_gpio_get_level(SW_I2C1_SCL_PIN, &l);
        ret = l;
        break;
    case HAL_IO_OPT_SET_SCL_INPUT:
        ql_gpio_set_direction(SW_I2C1_SCL_PIN, GPIO_INPUT);
        ql_gpio_set_pull(SW_I2C1_SCL_PIN, PULL_NONE);
        break;
    case HAL_IO_OPT_SET_SCL_OUTPUT:
        ql_gpio_set_direction(SW_I2C1_SCL_PIN, GPIO_OUTPUT);
        break;
    default:
        break;
    }
    return ret;
}


sw_i2c_t sw_i2c_8850 = {
    .hal_init = sw_i2c_port_initial,
    .hal_io_ctl = sw_i2c_port_io_ctl,
    .hal_delay_us = sw_i2c_port_delay_us,
    };

读数据流程

走一遍代码,看看 I2C 读数据的流程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
uint8_t SW_I2C_Read_8addr(sw_i2c_t *d, uint8_t IICID, uint8_t regaddr, uint8_t *pdata, uint8_t rcnt)
{
    uint8_t returnack = TRUE;
    uint8_t index;

    if(d==NULL) return FALSE;

    if(!rcnt) return FALSE;
	
    // 设置 I2C 总线为空闲状态,拉高 SDA SCL
    i2c_port_initial(d);
    
    // 发送起始条件
    i2c_start_condition(d);
    
    // 发送从机地址,写数据
    i2c_slave_address(d, IICID, WRITE_CMD);
    
    // 读从机响应 ACK
    if (!i2c_check_ack(d)) { returnack = FALSE; }
    d->hal_delay_us(SW_I2C_WAIT_TIME);
    
    // 写要读取的寄存器地址
    i2c_register_address(d, regaddr);
  
    // 再等从机 ACK
    if (!i2c_check_ack(d)) { returnack = FALSE; }
    d->hal_delay_us(SW_I2C_WAIT_TIME);
    
    // 发送起始条件
    i2c_start_condition(d);
    // 发送从机地址,这次读数据
    i2c_slave_address(d, IICID, READ_CMD);
    // 检查 ACK
    if (!i2c_check_ack(d)) { returnack = FALSE; }
    if(rcnt > 1)
    {
        for ( index = 0 ; index < (rcnt - 1) ; index++)
        {
            d->hal_delay_us(SW_I2C_WAIT_TIME);
            // 读数据
            pdata[index] = SW_I2C_Read_Data(d);
            // 发 ACK 给从机
            i2c_send_ack(d);
        }
    }
    d->hal_delay_us(SW_I2C_WAIT_TIME);
    pdata[rcnt-1] = SW_I2C_Read_Data(d);
    
    // 表示不想读数据了,发 NACK
    i2c_check_not_ack(d);
    
    // 发送停止条件
    i2c_stop_condition(d);

    return returnack;
}

由主机设备发起,要先写一次要读取的地址,再真正读取数据。

写数据流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
uint8_t SW_I2C_Write_8addr(sw_i2c_t *d, uint8_t IICID, uint8_t regaddr, uint8_t *pdata, uint8_t rcnt)
{
    uint8_t returnack = TRUE;
    uint8_t index;

    if(d==NULL) return FALSE;

    if(!rcnt) return FALSE;
	
    // 设置 I2C 总线为空闲状态,拉高 SDA SCL
    i2c_port_initial(d);
     // 发送起始条件
    i2c_start_condition(d);
    // 发送从机地址,写数据
    i2c_slave_address(d, IICID, WRITE_CMD);
    if (!i2c_check_ack(d)) { returnack = FALSE; }
    d->hal_delay_us(SW_I2C_WAIT_TIME);
    // 写寄存器地址
    i2c_register_address(d, regaddr);
    if (!i2c_check_ack(d)) { returnack = FALSE; }
    d->hal_delay_us(SW_I2C_WAIT_TIME);
    for ( index = 0 ; index < rcnt ; index++)
    {
        // 发数据
        SW_I2C_Write_Data(d, pdata[index]);
        // 读 ACK
        if (!i2c_check_ack(d)) { returnack = FALSE; }
        d->hal_delay_us(SW_I2C_WAIT_TIME);
    }
    // 发送停止条件
    i2c_stop_condition(d);
    return returnack;
}

具体实现

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// 设置 I2C 总线为空闲状态,拉高 SDA SCL
static void i2c_port_initial(sw_i2c_t *d)
{
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_HIGH, NULL);
    d->hal_io_ctl(HAL_IO_OPT_SET_SCL_HIGH, NULL);
}

// 发送起始条件
static void i2c_start_condition(sw_i2c_t *d)
{
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_HIGH, NULL);
    d->hal_io_ctl(HAL_IO_OPT_SET_SCL_HIGH, NULL);
    d->hal_delay_us(SW_I2C_WAIT_TIME);
    // 先拉低 SDA
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_LOW, NULL);
    d->hal_delay_us(SW_I2C_WAIT_TIME);
    // 再拉低 SCL
    d->hal_io_ctl(HAL_IO_OPT_SET_SCL_LOW, NULL);
    d->hal_delay_us(SW_I2C_WAIT_TIME << 1);
}

// 发送停止条件
static void i2c_stop_condition(sw_i2c_t *d)
{
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_LOW, NULL);
    // 先拉高 SCL
    d->hal_io_ctl(HAL_IO_OPT_SET_SCL_HIGH, NULL);
    d->hal_delay_us(SW_I2C_WAIT_TIME);
    // 后拉高 SDA
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_HIGH, NULL);
    d->hal_delay_us(SW_I2C_WAIT_TIME);
}

// 发从机地址
static void i2c_slave_address(sw_i2c_t *d, uint8_t IICID, uint8_t readwrite)
{
    int x;
	
    // 读写标志设置,0 写数据,1 读数据
    if (readwrite)
    {
        IICID |= I2C_READ;
    }
    else
    {
        IICID &= ~I2C_READ;
    }
	
    // SCL 下降沿才能修改 SDA 电平
    d->hal_io_ctl(HAL_IO_OPT_SET_SCL_LOW, NULL);

    for (x = 7; x >= 0; x--)
    {
        // 设置 SDA 电平
        sda_out(d, IICID & (1 << x));
        d->hal_delay_us(SW_I2C_WAIT_TIME);
        // 拉高拉低 SCL
        i2c_clk_data_out(d);

    }
}

// 读 ACK
static uint8_t i2c_check_ack(sw_i2c_t *d)
{
    uint8_t ack;
    int i;
    unsigned int temp;
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_INPUT, NULL);
    d->hal_io_ctl(HAL_IO_OPT_SET_SCL_HIGH, NULL);
    ack = 0;
    d->hal_delay_us(SW_I2C_WAIT_TIME);
    for (i = 10; i > 0; i--)
    {
        // 读到 SDA 为 0 表示应答成功
        temp = !(SW_I2C_ReadVal_SDA(d));
        if (temp)
        {
            ack = 1;
            break;
        }
    }
    d->hal_io_ctl(HAL_IO_OPT_SET_SCL_LOW, NULL);
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_OUTPUT, NULL);
    d->hal_delay_us(SW_I2C_WAIT_TIME);
    return ack;
}

// 发 ACK
static void i2c_send_ack(sw_i2c_t *d)
{
    
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_OUTPUT, NULL);
    // 主要是拉低 SDA
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_LOW, NULL);
    d->hal_delay_us(SW_I2C_WAIT_TIME);
    d->hal_io_ctl(HAL_IO_OPT_SET_SCL_HIGH, NULL);
    d->hal_delay_us(SW_I2C_WAIT_TIME << 1);
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_LOW, NULL);
    d->hal_delay_us(SW_I2C_WAIT_TIME << 1);
    d->hal_io_ctl(HAL_IO_OPT_SET_SCL_LOW, NULL);
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_OUTPUT, NULL);
    d->hal_delay_us(SW_I2C_WAIT_TIME);
}

// 发 NACK
static void i2c_check_not_ack(sw_i2c_t *d)
{
    // SDA 有上拉电阻,默认高电平
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_INPUT, NULL);
    i2c_clk_data_out(d);
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_OUTPUT, NULL);
    d->hal_delay_us(SW_I2C_WAIT_TIME);
}

// 写寄存器地址
static void i2c_register_address(sw_i2c_t *d, uint8_t addr)
{
    int x;
    // 其实就是发数据,一样的逻辑
    d->hal_io_ctl(HAL_IO_OPT_SET_SCL_LOW, NULL);

    for (x = 7; x >= 0; x--)
    {
        sda_out(d, addr & (1 << x));
        d->hal_delay_us(SW_I2C_WAIT_TIME);
        i2c_clk_data_out(d);
    }
}

// 发数据
static void SW_I2C_Write_Data(sw_i2c_t *d, uint8_t data)
{
    int x;
    d->hal_io_ctl(HAL_IO_OPT_SET_SCL_LOW, NULL);
    for (x = 7; x >= 0; x--)
    {
        sda_out(d, data & (1 << x));
        d->hal_delay_us(SW_I2C_WAIT_TIME);
        i2c_clk_data_out(d);
    }
}

// 读数据
static uint8_t SW_I2C_Read_Data(sw_i2c_t *d)
{
    int x;
    uint8_t readdata = 0;
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_INPUT, NULL);
    for (x = 8; x--;)
    {
        // 在 SCL 上升沿的时候采样,这个时候读 SDA 的电平 
        d->hal_io_ctl(HAL_IO_OPT_SET_SCL_HIGH, NULL);
        readdata <<= 1;
        if (SW_I2C_ReadVal_SDA(d))
            readdata |= 0x01;
        d->hal_delay_us(SW_I2C_WAIT_TIME);
        d->hal_io_ctl(HAL_IO_OPT_SET_SCL_LOW, NULL);
        d->hal_delay_us(SW_I2C_WAIT_TIME);
    }
    d->hal_io_ctl(HAL_IO_OPT_SET_SDA_OUTPUT, NULL);
    return readdata;
}

案例

起始条件、结束条件、发从机地址、ACK 这些是标准的协议规定的,但发了从机地址之后,数据该怎么传输,那就要看芯片手册怎么定义的了,后面的这部分可以看作是基于 I2C 协议之上的应用协议。具体可以看看后面的两个案例。

CN91C4S48 段码屏的通信规则

芯片手册描述了通信的起始条件、结束条件,以及从机地址、读写标志和发送命令还是数据的协议规定。

AT24C32 EEPROM 通信规则

起始条件和停止条件的定义。

数据采样和数据修改的时机。

ACK 的波形定义。

写数据的协议格式。

读数据的协议格式。