I2C
连接多个模块的传输方案:I2C,使用两根总线。
两根总线分别是时钟总线 SCL 和数据总线 SDA。
通信过程
现在我们串一遍I2C上一个模块(master)要给另一个模块(slave)发消息的过程。
- MCU 使用一定的方法标识自己开始传输了。
- MCU 发送 LCD slave 的地址+一位读写位,其他模块接收到发现地址不是自己的,就不做处理。
- LCD 接收到后知道目标是自己,于是返回 ack。
- MCU 收到 ACK 后发送一帧数据。
- 发送完 MCU 等着 ACK,收到 ACK 后继续发送下一帧数据。
- 一直发送到发送停止位 stop 结束。
数据长度可以设置,比如789.
总线上的器件是开漏输出的半双工通信。
默认总线是上拉电阻拉成高电平。
当器件输出 out 为低电平时,总线导通到接地,总线被拉低(整条总线都被拉低)。江协科技老师举的例子很好,就像公交车上的一根横杆,有人拉住横杆拽下来,整条横杆都被拉低了,其他人都知道“横杆被一个人拉低了,说明有人正在使用总线”。
然后是总线传输数据的方式,SCL SDA 两根总线在何种情况下表示 start stop 0 1 bit?
首先都是 SCL 为高电平时 SDA 的数值才有意义。
SDA 从高到低,表示 start 位。从低到高,表示 stop 位。
start 位后,SDA 高电平表示1,低电平表示0.
发送完 1byte 数据后,总线保持拉高状态。如果接收方把总线拉低了,发送方发现总线1→0了(不是发送方自己拉的,是接收方给他拉下来的,但是发送方能察觉到),说明接收方成功接收了并且拉了拉总线以示“收到”。如果 SDA 还是保持在高电平,说明接收方没有成功收到或者成功发送 ACK。
问题处理
I2C 是一种很简单的主从通信协议了,但是局限性也很多,比如7 bit 的地址线只允许 2^7 个设备;一次顶多两个设备主从通信;一个设备的快慢会影响到整条总线的通信等。
问题1:从设备处理速度太慢了,赶不及在下一个时钟周期接收新数据帧怎么办?
方法:clock stretching, 拉低一段时间 SCL 假装下一个时钟周期还没到。
问题2:多个设备同时发数据冲突了怎么办?
方法:Bus Aribitation,前面我们知道总线被一个设备拉低了,所有设备都能接收到总线拉低的信号。因此如果两个设备同时开始发信息,前面数据一致都无所谓,等到第一次数据不一致的时候,一个设备发送数据0,一个发送数据1,这时 SDA 总线被 DATA2 的0拉低了。
发送 DATA1 数据的设备就明白了:有人同时在和我一起发数据,因此总线不是我预期的1而是被他拉低为0了。那我 quit,你发吧。然后就只有 DATA2 发送的数据了。
问题3:以上发送的数据每次都是 1byte 8bits 很正好。那如果要发送的地址不是 8bits 呢?
方法:少于 8bits 用一些固定的额外的 start 位填充,多于 8bits 的地址用两个 bytes,不够的也是用额外的 start 位填充。
问题3:如果我 master 发完数据,想紧接着再收数据,变成 slave,可行吗?
方法:通过一个 sr 信号,也就是 repeat start 重发 start 位,来标识自己是 read 而不再是 write 了重新开始通信。
编址格式
slave 地址编址有一些固定格式。
0000 000 0:广播,对所有 slave 结点讲话。如果 slave 无视(NACK),就不会参与广播。如果返回 ACK 就参与进来了。不过多个 slave 都返回 ACK 的话 master 是不知道都有谁回应了的。
第二个 byte 发送一些行为相关,比如:start,clear,reset software
编程应用
slave mode:
- I2C 设备默认工作在 slave mode。
- 外设时钟在 I2C_CR2 寄存器中编程。频率介于 2kHz~100kHz。
- 硬件自动等待发过来的 start 和 addr 信息。
- 如果 addr 信息和 OAR1 中存储的地址相同,说明目标是自己。如果 ACK 位为1,则发送 ack pulse。
- 设置 ADDR 位,1表示匹配。
- 如果 ITEVFEN 就是中断事件 flag 为1,则生成中断。
- TRA 位标明 slave 是 R 还是 T 模式(收 or 发)。
- BTF 位标识收没收完。
这么说起来还是有点混乱 I2C 到底经历了哪些才顺利发送了数据?
首先,从主模式的概念。master 主模式驱动时钟信号,发起传输;slave 从模式响应传输。
主模式
发送:
所有 EV 事件都会拉低 SCL,直到相应软件序列执行完成。
S:start 事件。比如CR2 寄存器中设置外设时钟,配置时钟寄存器,上升时钟寄存器,使能 CR1 来启用时钟,CR1 中设置 start 位,等待总线被拉低表示就绪,发送启动信号,并切换为主模式。
EV5:启动事件成功进行,设置 SB 寄存器=1. SB 寄存器=1后才可以进行地址阶段,执行完地址阶段会自动清除 SB 和 EV5 事件。
Address:地址阶段。传输7位地址+1位读写位,然后等待从机的 ack。收到 ack 进入 EV6.
EV6:设置 addr 位=1代表地址阶段顺利执行, master 收到 ack了。清除 EV6 后自动进入 EV8.
EV8:设置 TxE ,准备写入主机要传入的数据。TxE 表示数据寄存器为空可以写入。每次数据写入 DR 都会清空 TxE 和 EV8 事件。写完数据数据传过去了,主机收到 ack 后继续传输。以 BTF=1 表示数据传输的结尾。
void i2c_write(uint8_t address, uint8_t *buffer, int buff_len) {
int i = 0;
// Send in sequence: Start bit, Contents of buffer 0..buff_len, Stop
while (((I2C1->SR2>>1)&1)); // wait until I2C1 is not busy anymore
I2C_GenerateSTART(I2C1, ENABLE); // Send I2C1 START condition
// wait for I2C1 EV5 --> Slave has acknowledged start condition
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// Send slave Address for write then wait for EV6
I2C_Send7bitAddress(I2C1, address, I2C_Direction_Transmitter);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
while (i < buff_len){
I2C_SendData(I2C1, buffer[i]); // send data then wait for EV8_2
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
i++;
}
I2C_GenerateSTOP(I2C1, ENABLE); // send stop bit
}
接收:
前面和 master transmit 都一样。
TxE 改为 RxE 了,=1标识接收到了数据。
master 自己设置 stop 事件后(发送 NACK)停止接收。
void i2c_read(uint8_t address, uint8_t *buffer, int buff_len) {
int i = 0;
// Start bit, Contents of buffer from 0..buff_len, sending a NACK
// for the last item and an ACK otherwise, Stop bit
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); //EV5
// Send slave Address for write then wait for EV6
I2C_Send7bitAddress(I2C1, address, I2C_Direction_Receiver);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
I2C_AcknowledgeConfig(I2C1, ENABLE); // going to send ACK
while (i < buff_len - 1){
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); //EV7
buffer[i] = I2C_ReceiveData(I2C1); // get data byte
i++;
}
I2C_AcknowledgeConfig(I2C1, DISABLE); // going to send NACK
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); //EV7
buffer[i] = I2C_ReceiveData(I2C1); // get the last byte
I2C_GenerateSTOP(I2C1, ENABLE); // send stop
}
从模式
发送:
start 启动事件由 master 发起。从机校验地址并决定是否发送 ack 位。
EV1:设置 addr 位表示地址匹配。
EV3-1:设置 TxE 位,开始传入数据。一直到主机返回 NACK 表示不想再要数据了,或者 AF=1 说明 ack 失败了为止。
接收:
前面到 EV1 和 slave transmit 都一样。
- 数据从 DR 寄存器中读。
- 读入一个 byte 后,如果 ack 位已经设置,则返回 ack 信息。
- RxE 位是接收数据的状态寄存器。
- 主机生成停止条件时停止。
异常情况:
总线错误,NACK,仲裁失败,时钟异常超时。