IIC协议

协议:
协议简单来说就是主机与从机双方约定一组动作,只要一方做了特定的动作,另一方就可以知道你要干什么,然后就可以给出特定的回复动作,多次重复就可以实现通信。

以下是我总结的IIC读写的步骤,可供参考:
IIC 读数据:

  • 发送起始信号
  • 发送从机地址 - 发送(写入)
  • 发送寄存器地址
  • 起始信号
  • 发送从机地址 - 接收(读取)
  • 接收数据
  • 发送停止信号

IIC 写数据:

  • 发送起始信号
  • 发送从机地址 - 发送(写入)
  • 发送寄存器地址
  • 发送数据
  • 发送停止信号

软件模拟IIC实现与硬件IIC

由于在IIC总线的通信过程中,时钟信号由主设备产生,从设备根据时钟信号进行数据传输(即同步通信)。故可通过代码方便地模拟该协议,也即所谓的模拟IIC协议。

在软件模拟 IICGPIO 一般为开漏模式,支持线与功能;不过由于开漏模式无法输出真正的高电平,所以需要外部上拉(IIC的电平只是通信使用,所以负载强度不大;一般总线上认为,低于0.3Vdd为低电平,高于0.7Vdd为高电平)
推挽输出不能实现线与功能,因为如果两个输出引脚,一个输出高电平P-MOS管导通,一个输出低电平N-MOS管导通,则P-MOS管上方的高电平会经过P-MOS -> N-MOS -> GND,整个通路上没有外接电阻,因此电阻很小相当于高电平直接接到低电平造成了短路。

模拟 I2C是用两条 GPIO管脚的软件模拟的,将一个 GPIO设置为数据线 SDA,另外一个设置为时钟线 SCL
硬件 I2C则是通过一个 I2C控制器实现的,该控制器被建立在微控制器芯片或单独的 I2C芯片中,通过集成的硬件内部逻辑和电路来控制时序和数据格式,实现 I2C总线通信。
软件与硬件的IIC各有优缺点,可根据实际情况选用。

注:在使用软件模拟IIC时,其实IICIO口既可以配置为推挽输出也可以配置为开漏输出,不同之处在于:

  • IO口配置为推挽输出时,发送和接收数据时需要切换IO口的输入输出模式,发送数据时需要将IO口切换为输出模式,接收数据时需要将IO口切换为输入模式。
  • 如果配置开漏输出则不需要切换IO口的输入输出模式。我们知道推挽输出不具有线与的功能,但是由于我们使用软件IIC时通常不会有多个设备连接到一个总线上的情况,所以只有一个从设备的话,也就不会有线与的情况发生了,可以使用推挽输出。但是我们需要根据发送数据和接收数据来切换IO口的工作模式。

模拟IIC代码实现

注:有时候器件规格书上标明的设备地址不一定非得按照该地址进行寻址!

具体来说就是,假如:现有一传感器,使用IIC通信;其规格书标明设备地址可选(0x18、0x19),假如根据硬件连接,我们选用的地址为0x18;这并不意味着我们寻址是直接使用#define SLAVE_ADDR 0x18就行,很可能需要进行一番运算,即:#define SLAVE_ADDR (0x18 << 1)。如果IIC使用7位地址,那么<< 1的作用就很明了了:将最低位让出,用做读写位。

所以如果使用IIC时,根据规格书上的地址寻不到从设备且检查并无其他问题,那么就可以考虑一下该情况!

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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/**
* @brief 初始化
*/
void drv_iic_init(void)
{
rcu_periph_clock_enable(RCU_GPIOF);

// SCL(PF6) SDA(PF7)
gpio_mode_set(GPIOF, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_6 | GPIO_PIN_7);
gpio_output_options_set(GPIOF, GPIO_OTYPE_OD, GPIO_OSPEED_MAX, GPIO_PIN_6 | GPIO_PIN_7);
gpio_bit_set(GPIOF, GPIO_PIN_6 | GPIO_PIN_7);
}

void iic_delay(void)
{
unsigned short i = 20; // 根据主频可算出while(1)一次时间,然后给出合适的值
while (i--)
;
}

void iic_write_scl(bit_status bit_value)
{
gpio_bit_write(GPIOF, GPIO_PIN_6, (bit_status)bit_value);
iic_delay();
}

void iic_write_sda(bit_status bit_value)
{
gpio_bit_write(GPIOF, GPIO_PIN_7, (bit_status)bit_value);
iic_delay();
}

unsigned char iic_read_sda(void)
{
unsigned char value = 0;
value = gpio_input_bit_get(GPIOF, GPIO_PIN_7);
iic_delay();
return value;
}

void iic_start(void)
{
iic_write_sda(SET);
iic_write_scl(SET);
iic_write_sda(RESET);
iic_write_scl(RESET);
}

void iic_stop(void)
{
iic_write_sda(RESET);
iic_write_scl(SET);
iic_write_sda(SET);
}

void iic_send_byte(unsigned char data)
{
unsigned char i = 0;
for (i = 0; i < 8; i++)
{
iic_write_sda((bit_status)(data & (0x80 >> i)));
iic_write_scl(SET);
iic_write_scl(RESET);
}
}

unsigned char iic_read_byte(void)
{
unsigned char i = 0;
unsigned char data = 0;
iic_write_sda(SET);
for (i = 0; i < 8; i++)
{
iic_write_scl(SET);
if (iic_read_sda() == 1)
{
data |= (0x80 >> i);
}
iic_write_scl(RESET);
}
return data;
}

/**
* @brief 本机是否发送ACK信号
* @note RESET:发,SET:不发
*/
void iic_send_ack_or_not(bit_status bit_value)
{
iic_write_sda(bit_value);
iic_write_scl(SET);
iic_write_scl(RESET);
}

/**
* @brief 本机是否收到从机ACK信号
* @note 0:收到,1:未收到
*/
unsigned char iic_read_ack_value(void)
{
unsigned char ack = 0;
iic_write_sda(SET);
iic_write_scl(SET);
ack = iic_read_sda();
iic_write_scl(RESET);
return ack;
}

/**
* @brief 向从机发送数据
* @param[0] 从机地址
* @param[1] 寄存器地址
* @param[2] 待发送数据
*/
iic_hall_status_enum write_data_to_slave(const unsigned char DeviceAddr, const unsigned char ResAddr, const unsigned char data)
{
iic_start();
iic_send_byte(DeviceAddr | 0x00); // WRITE
if (iic_read_ack_value())
{
iic_stop();
return ERROR;
}
iic_send_byte(ResAddr);
if (iic_read_ack_value())
{
iic_stop();
return ERROR;
}
iic_send_byte(data);
if (iic_read_ack_value())
{
iic_stop();
return ERROR;
}
iic_stop();
return DONE;
}

/**
* @brief 从从机读取数据
* @param[0] 从机地址
* @param[1] 寄存器地址
* @param[2] 指向存放数据的地址
* @param[3] 读取的字节数
*/
iic_hall_status_enum read_data_from_slave(const unsigned char DeviceAddr, const unsigned char ResAddr, unsigned char* buf, unsigned char len)
{
iic_start();
iic_send_byte(DeviceAddr | 0x00); // WRITE
if (iic_read_ack_value())
{
iic_stop();
return ERROR;
}
iic_send_byte(ResAddr);
if (iic_read_ack_value())
{
iic_stop();
return ERROR;
}
iic_start();
iic_send_byte(DeviceAddr | 0x01); // READ
if (iic_read_ack_value())
{
iic_stop();
return ERROR;
}
while (1)
{
*buf++ = iic_read_byte();
if (--len == 0)
{
iic_send_ack_or_not((bit_status)SET);
break;
}
iic_send_ack_or_not((bit_status)RESET);
}
iic_stop();
return DONE;
}

硬件IIC通信示例

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
void My_IIC_Init(void)
{
rcu_periph_clock_enable(RCU_I2C1);
rcu_periph_clock_enable(RCU_GPIOB);

gpio_af_set(GPIOB, GPIO_AF_1, GPIO_PIN_10);
gpio_af_set(GPIOB, GPIO_AF_1, GPIO_PIN_11);

gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_10); // SCL
gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_10); // 复用开漏
gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_11); // SDA
gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_11);

/* configure I2C clock */
i2c_clock_config(I2C1, 100000, I2C_DTCY_2);
/* configure I2C address */
i2c_mode_addr_config(I2C1, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, OWN_ADDRESS7); // 主机地址

/* enable I2C1 */
i2c_enable(I2C1);
/* enable acknowledge */
i2c_ack_config(I2C1, I2C_ACK_ENABLE);
}

void I2C_Read_Data(void)
{
uint16_t timeout = 500;
/* 等IIC总线空闲 */
while (i2c_flag_get(I2C1, I2C_FLAG_I2CBSY));
/* 发送起始信号 */
i2c_start_on_bus(I2C1);
/* 等到起始信号发送完毕 */
while (!i2c_flag_get(I2C1, I2C_FLAG_SBSEND));

/* 发送从机地址 - 发送(写入) */
i2c_master_addressing(I2C1, SLA_ADDRESS7, I2C_TRANSMITTER); // 从机地址
/* 发送从机地址 - 若IIC从机在这儿会卡死,需添加返回操作 */
while (!i2c_flag_get(I2C1, I2C_FLAG_ADDSEND))
{
timeout--;
if (timeout == 0)
{
/* 发送结束信号 */
i2c_stop_on_bus(I2C1);
printf("No I2C Device\r\n");
return;
}
}
/* 清除 I2C_FLAG_ADDSEND 标志位 */
i2c_flag_clear(I2C1, I2C_FLAG_ADDSEND);

/* 发送寄存器地址 */
i2c_data_transmit(I2C1, REG_ADDRESS7); // 寄存器地址
while (!i2c_flag_get(I2C1, I2C_FLAG_BTC));

/* 发送起始信号 */
i2c_start_on_bus(I2C1);
/* 等到起始信号发送完毕 */
while (!i2c_flag_get(I2C1, I2C_FLAG_SBSEND));

/* 发送从机地址 - 接收(读取) */
i2c_master_addressing(I2C1, SLA_ADDRESS7, I2C_RECEIVER);
/* 匹配从机地址 */
while (!i2c_flag_get(I2C1, I2C_FLAG_ADDSEND));
/* 清除 ADDSEND 标志位 */
i2c_flag_clear(I2C1, I2C_FLAG_ADDSEND);

/* 读取数据 - 9个字节 */
for (int index = 0; index < 9; index++)
{
if (index == 8)
{
/* wait until the last data byte is received into the shift register */
while (!i2c_flag_get(I2C1, I2C_FLAG_BTC));
/* 失能应答 */
i2c_ack_config(I2C1, I2C_ACK_DISABLE);
}

/* wait until the RBNE bit is set */
while (!i2c_flag_get(I2C1, I2C_FLAG_RBNE));
/* read a data from I2C_DATA */
i2c_recv_buf[index] = i2c_data_receive(I2C1);
}

/* 发送结束信号 */
i2c_stop_on_bus(I2C1);
/* 等到结束信号发送完毕 */
while (I2C_CTL0(I2C1) & I2C_CTL0_STOP);

/* 使能应答 */
i2c_ack_config(I2C1, I2C_ACK_ENABLE);
}

注意

  1. SCLSDA需默认拉高,以释放总线状态,此时为准备好数据传输状态
  2. IIC起始信号:SCL高电平,SDA下降沿(注:起始后需把SCL也拉低,使主机占用总线准备发送数据)IIC结束信号:SCL高电平,SDA上升沿;
  3. 主机发送:每次发送1位,循环8次即可发送一个字节;在发送数据时应注意:SCL高电平时,此时SDA上数据有效,不可被更改;SCL低电平时,SDA上数据可被更改,也只有这时数据可被更改
  4. 从机接收:在接收前,应先拉高SDA,避免主机抢占SDA,导致数据出错;为存储接收到的数据,需拉高SCL,使主机在SCL高电平期间读取SDA,每读取一位就存入提前申请好的变量中;读完后即可得到一个字节的数据。然后拉低SCL,使从机可写入SDA,这样即可实现从机发送。
  5. 应答信号:主机把应答位数据放到SDA线(即:如需应答则拉低SDA,如无需应答则拉高SDA);然后先拉高SCL,以读取应答位;再拉低SCL,开始下一个时序模块;