1.I2C概览
2.I2C通信时序图
3.STM32 I2C模块解析
4.STM32 I2C通信代码解析
5.主机发送,主机接收 EEPROM实例
6.封装I2C库函数
7.STM32F407x(主机) , STM32F103C8T6 (从机)双机通信实验
概览
IIC总线,有两条线构成 。利用开漏结构构成的总线,低 开漏,高 上拉。
SDA数据线,用于在总线上传输数据,时钟高数据有效。数据线上拉电阻拉高,IC通信口默认高阻态。
SCL时钟线,时钟信号。驱动移位寄存器,提供数据传输的节拍。时钟默认上拉电阻拉高。IC口默认高阻态。
通信速率与要求
通信过程
START:数据线默认高电平,在SCL为高的状态下拉低数据线打破默认状态。表示有数据即将发送,总线各设备做好准备。处于休眠中的设备会在这个时刻唤醒。
P-STOP:在SCL为高的状态下数据线恢复静默高电平。表示本次通信结束。总线数据线恢复到静默状态。
数据传输最先传输最高有效位
SLAVE ADDRESS:从机地址,表示接下来的数据归属,总线上N个设备,该数据发送给谁。这个设备被动接收数据。
R/W:信号是读还是写
A:回复确认信号有效为 低 (非静默状态)
过程:有设备需要使用总线发送数据。
1.设备输出SCL,SCL总线开始。
2.设备发送START bit,此时CLK保持高有效
3.CLK 拉低 开始第一个时钟
4.在时钟节拍下,SDA数据按照MSB 依次发送。
5.发送器每发送一个字节后,在第9个时钟脉冲期间释放数据线。发送完成后,主机等待从机ACK–A。
几种模式下的参数要求
术语
总线仲裁。
多个主机发起通信需求 ,最先出现1的设备被淘汰,因为静默电平位1
可以理解为SCL被拉低时,相当于SCL接地,那么其他设备无法把SCL拉高。
同理数据,SDA一旦被拉低,发出的高信号无法传达至总线,所以信息传递失败,故仲裁出局。
主设备发送数据 0 时,直接拉低SDA数据线通过上图的方式。所以这个时候其他设备发出高,SDA数据线上的电平任然是0.
读写时序
主机发起通信,写数据
1.起始 ,SDA和SCL都为高的状态下。SDA由高变低打破静默状态
这是传输数据的一个开始
2.主机需要把传输的数据(地址信息),放到数据寄存器中。然后在硬件的主导下按照SCL的节拍依次把移位寄存器的内容发送到SDA总线上。从机需要在SCL高电平期间读取SDA信号。在 硬件的主导下按照SCL的节拍依次读取电平装入移位寄存器。
主机需要给 数据寄存器装填数据。硬件检查数据装填完成后,开始发送数据。
3.从机在硬件主导下开始计数,在第八个时钟下降沿处,准备做应答ACK。主机这个时候需要读取ACK消息,所以此时主机要释放SDA线(浮空),这时从机的 ACK (低)输出到SDA上,主机在SCL高电平上读取ACK
这个过程不需要CPU的 参与所以,硬件完全主导。
4.下个时刻主机开始传输第二字节的数据,那么需要 CPU或者DMA的参与把 数据装入数据寄存器中。装载完成后开始数据 传输。
读时序
同理
深刻的理解通信时序后,对 硬件主导的I2C机制理解会轻松许多 。
STM32的I2C
STM32 I2C结构
I2C核心为数据移位寄存器,按照SCLK移入或者发出数据。用于数据缓存的数据寄存器。
默认工作在从机模式下,当 接收到起始位后,移位 寄存器接收SDA线上的数据,接收7位后与 自己的地址进行比较。
头或地址不匹配:接口会忽略它并等待下一个起始位。
地址匹配:接口会依次:
● 发出应答脉冲(如果 ACK 位置 1)
● ADDR 位会由硬件置 1 并在 ITEVFEN 位置 1 时生成一个中断。
● 如果 ENDUAL=1,则软件必须读取 DUALF 位状态来核对哪些从地址进行了应答。
IIC主模式
模块默认工作在从机模式下。要将工作模式由默认的从模式切换为主模式,需要生成一个起始位。
即会选中主模式。
I2C_GenerateSTART(I2C1,ENABLE);
主模式要控制时钟线,并且生成时钟信号。
START 位置 1 后,接口会在 BUSY 位清零后生成一个起始位并切换到主模式(M/SL 位置 1)
/* --EV5 */
#define I2C_EVENT_MASTER_MODE_SELECT ((uint32_t)0x00030001) /* BUSY, MSL and SB flag */
EV5 会检查位就是 BUSY主从位,start bit 位
1.起始位发送
2.获取总线并发送了起始位
3.主模式开启
接下来就会等待地址的写入,
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
接下来从地址会通过内部移位寄存器发送到 SDA 线。
显然需要等待地址发送完成。
地址字节被发出后,
— ADDR 位会由硬件置 1 并在 ITEVFEN 位置 1 时生成一个中断。
#define I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ((uint32_t)0x00070082) /* BUSY, MSL, ADDR, TXE and TRA flags */
1.总线非忙。地址发送正常
2.主模式
3.地址发送完成并被应答
4.数据寄存器为空
5.下个状态为发送器
I2C_CheckEvent 对SR1,SR2都执行读操作。
写入发送数据到DR,主设备会一直等待,直到首个数据字节被写入 I2C_DR 为止。模块自动开始传输数据到SDA。
应为没有写入前,没有数据可以发送。
接收到应答脉冲后, TxE 位会由硬件置 1 并在 ITEVFEN 和 ITBUFEN 位均置 1 时生成一个中断。
表示数据已经被从设备接收,并且回复了ACK。
如果在上一次数据传输结束之前 TxE 位已置 1 但数据字节尚未写入 DR 寄存器,则 BTF 位
会置 1,而接口会一直延长 SCL 低电平,等待I2C_DR 寄存器被写入,以将 BTF 清零。
意思就是说,在下一个数据写入前,SCL都会被拉低,这样从设备就不会开始接收。
/* --EV6 */
#define I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ((uint32_t)0x00070082) /* BUSY, MSL, ADDR, TXE and TRA flags */
/*EV8*/
#define I2C_EVENT_MASTER_BYTE_TRANSMITTING ((uint32_t)0x00070080) /* TRA, BUSY, MSL, TXE flags */
#define I2C_EVENT_MASTER_BYTE_TRANSMITTED ((uint32_t)0x00070084) /* TRA, BUSY, MSL, TXE and BTF flags */
BTF 表示新数据未 装填,从而执行装填动作。
当最后一个字节写入 DR 寄存器后,软件会将 STOP 位置 1 以生成一个停止位
再来看一下传输过程.
等待事件有一个原则也就是上个阶段的内容完成,下一个阶段的输入还未就绪。一但输入条件(写入数据)达成,开始下一个阶段。
在等待下一个阶段的输入条件中,主设备拉低SCK使得总线处于等待。
I2C_GenerateSTART(I2C1,ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
{
/*time tout*/
}
/*send IIC slave address*/
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
/*Wait EV6*/
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
/*time tout*/
}
I2C_SendData(I2C1, 0x01);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED ))
{
/*time tout*/
}
I2C_SendData(I2C1, 0x77);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
/*time tout*/
}
I2C_GenerateSTOP(I2C1, ENABLE);
以上的主传输过程。
主接收其在发送完成地址后,表明自己是读。从机回复ACK后,主设备释放SCL,SDA的控制。
EV6
#define I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED ((uint32_t)0x00030002) /* BUSY, MSL and ADDR flags */
只需要关注总线地址是否发送完成,并且对方已经回复ACK。
下个时刻主接收会接收来自从机的数据,关注是否接收到数据
EV7
#define I2C_EVENT_MASTER_BYTE_RECEIVED ((uint32_t)0x00030040) /* BUSY, MSL and RXNE flags */
关注的是RXNE
如果不再接收那么,不回复ACK即可,所以在最后一个数据接收前。设置下个数据不回复即可。
为了在最后一个接收数据字节后生成非应答脉冲,必须在读取倒数第二个数据字节后
(倒数第二个 RxNE 事件之后)立即将 ACK 位清零。
下代码是在按键的中断函数中发送IIC。
第一段 写入数据
第二段,写入数据并且回读三字节。代码在等待EV过程中没有超时机制。
代码仅做示意。
void EXTI2_IRQHandler(void)
{
char data_rec;
char bute_cnt=0;
if(phase_IIc %2 ==0)
{
I2C_GenerateSTART(I2C1,ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
{
/*time tout*/
}
/*send IIC slave address*/
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
/*Wait EV6*/
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
/*time tout*/
}
I2C_SendData(I2C1, 0x01);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED ))
{
/*time tout*/
}
I2C_SendData(I2C1, 0x77);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
/*time tout*/
}
I2C_GenerateSTOP(I2C1, ENABLE);
}
else
{
/*read data*/
/*master send*/
I2C_GenerateSTART(I2C1,ENABLE);
/*.EV5*/
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
{
}
/*send IIC slave address*/
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
/*Wait EV6*/
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
}
/*EEPROM data address*/
I2C_SendData(I2C1, 0x01);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING))
{
}
/*---master recevie---*/
I2C_GenerateSTART(I2C1,ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
{
}
/*send IIC slave address*/
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Receiver);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
{
}
/*recevie 3 byte*/
while (bute_cnt < 3)
{
bute_cnt++;
data_rec =I2C_ReceiveData(I2C1);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))
{
}
if(bute_cnt ==2)
{
I2C_AcknowledgeConfig(I2C1, DISABLE);
I2C_GenerateSTOP(I2C1, ENABLE);
}
}
}
phase_IIc++;
GPIO_ToggleBits(GPIOF,GPIO_Pin_8);
EXTI_ClearITPendingBit(EXTI_Line2);
}
phase 1
phase2
从机
默认情况下, I2C 接口在从模式下工作。
检测到起始位后,便会立即接收到来自 SDA 线的地址并将其送到移位寄存器。之后,会将
其与接口地址 (OAR1) 和 OAR2(如果 ENDUAL=1)或者广播呼叫地址(如果 ENGC = 1)
进行比较。
头或地址不匹配:接口会忽略它并等待下一个起始位。
每个地址都会接收,一但和自己的地址一致,就会主动发送ACK。
ADDR 位会由硬件置 1 并在 ITEVFEN 位置 1 时生成一个中断。
以上部分都是又硬件完成,产生中断的意义是表明,接下来的数据需要接收。
接收需要确认,数据寄存器为空,以免覆盖
TRA 位指示从设备是处于接收模式还是处于发送模式。通过TRA可以知道下次是读还是写。
EV1 表示从机接收到了地址,并且匹配
EV1
/* --EV1 (all the events below are variants of EV1) */
/* 1) Case of One Single Address managed by the slave */
#define I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED ((uint32_t)0x00020002) /* BUSY and ADDR flags */
#define I2C_EVENT_SLAVE_TRANSMITTER_ADDRESS_MATCHED ((uint32_t)0x00060082) /* TRA, BUSY, TXE and ADDR flags */
/* 2) Case of Dual address managed by the slave */
#define I2C_EVENT_SLAVE_RECEIVER_SECONDADDRESS_MATCHED ((uint32_t)0x00820000) /* DUALF and BUSY flags */
#define I2C_EVENT_SLAVE_TRANSMITTER_SECONDADDRESS_MATCHED ((uint32_t)0x00860080) /* DUALF, TRA, BUSY and TXE flags */
/* 3) Case of General Call enabled for the slave */
#define I2C_EVENT_SLAVE_GENERALCALLADDRESS_MATCHED ((uint32_t)0x00120000) /* GENCALL and BUSY flags */
表示接收到了地址
EV2 表示数据接收完成
/* --EV2 */
#define I2C_EVENT_SLAVE_BYTE_RECEIVED ((uint32_t)0x00020040) /* BUSY and RXNE flags */
如果在下一次数据接收结束之前 RxNE 位已置 1 但 DR 寄存器中的数据尚未读取,则 BTF
位会置 1,而接口会一直延长 SCL 低电平,直到软件通过读取 I2C_DR 寄存器来把 BTF 清
零
也就是说读取后,BTF为0,那么主机才可以继续发送数据。
关闭从设备通信
传输完最后一个数据字节之后,主设备会生成一个停止位。接口会检测此条件并:
● 将 STOPF 位置 1 并在 ITEVFEN 位置 1 时生成一个中断。
通过先读取 SR1 寄存器然后写入 CR1 寄存器的方式将 STOPF 位清零
主机发送停止后,会产生事件可以变为中断。
![在这里插入图片描述](https://img-blog.csdnimg.cn/cbd5f9dec8fb4001a4809a75e8006da2.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAZGVuZ2ppbmdn,size_20,color_FFFFFF,t_70,g_se,x_16)
TRA 位指示从设备是处于接收模式还是处于发送模式。
从发送器在接收到地址并将 ADDR 清零后,从设备会通过内部移位寄存器将 DR 寄存器中的字节发送
到 SDA 线。从设备会延长 SCL 低电平时间,直到 ADDR 位清零且 DR 寄存器中填满待发送数据为止。
```c
/* --EV3 */
#define I2C_EVENT_SLAVE_BYTE_TRANSMITTED ((uint32_t)0x00060084) /* TRA, BUSY, TXE and BTF flags */
#define I2C_EVENT_SLAVE_BYTE_TRANSMITTING ((uint32_t)0x00060080) /* TRA, BUSY and TXE flags */
/* --EV3_2 */
#define I2C_EVENT_SLAVE_ACK_FAILURE ((uint32_t)0x00000400) /* AF flag */
读到这里会发现不断的等待状态。没错I2C就是不断的检查事件
上个阶段完成,下个阶段输入就绪。
双机I2C实验
两块MCU的 SCK,SDA分别相连。注意I2C需要有上拉电阻,硬件必须满足通信规格。
首先分立测试
1.封装一个主机写函数
写入data 地址后的len个元素
static user_i2c_wait(void)
{
int i=1000;
while (i>0)
{
i--;
}
}
int user_i2c_master_write(uint8_t address,uint8_t *data,uint8_t len)
{
uint8_t send_inedx=0;
int timeout;
I2C_GenerateSTART(I2C1,ENABLE);
timeout = 100;
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
{
timeout--;
user_i2c_wait();
if (timeout < 0)
{
I2C_GenerateSTOP(I2C1, ENABLE);
return STARTBIT_ERROR;
break;
}
}
/*send IIC slave address*/
I2C_Send7bitAddress(I2C1, address, I2C_Direction_Transmitter);
/*Wait EV6*/
timeout = 100;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
timeout--;
user_i2c_wait();
if (timeout < 0)
{
I2C_GenerateSTOP(I2C1, ENABLE);
return EV6_ERROR;
break;
}
}
if( len <= 0)
{
I2C_GenerateSTOP(I2C1, ENABLE);
return;
}
timeout = 100;
while(send_inedx < len)
{
I2C_SendData(I2C1, data[send_inedx]);
send_inedx ++;
timeout = 100;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED ))
{
timeout--;
user_i2c_wait();
if (timeout < 0)
{
I2C_GenerateSTOP(I2C1, ENABLE);
return EV8_2_ERROR;
break;
}
}
}
I2C_GenerateSTOP(I2C1, ENABLE);
}
unsigned char senddata[5]={1,‘a’,‘b’,‘c’,‘d’};
user_i2c_master_write(0xA0,senddata,5);
测试发送时序,先使用IIC EEPROM测试
主机要求从机发送数据并接收,使用IIC EPROM 测试
int user_master_read(uint8_t addres,uint8_t *data,uint8_t len)
{
uint8_t rece_inedx=0;
int timeout;
I2C_GenerateSTART(I2C1,ENABLE);
timeout = 100;
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
{
timeout--;
user_i2c_wait();
if (timeout < 0)
{
I2C_GenerateSTOP(I2C1, ENABLE);
return STARTBIT_ERROR;
break;
}
}
/*send IIC slave address*/
I2C_Send7bitAddress(I2C1, addres, I2C_Direction_Receiver);
/*Wait EV6*/
timeout = 100;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
{
timeout--;
user_i2c_wait();
if (timeout < 0)
{
I2C_GenerateSTOP(I2C1, ENABLE);
return EV6_ERROR;
break;
}
}
/*recevie 3 byte*/
while (rece_inedx < len)
{
data[rece_inedx] =I2C_ReceiveData(I2C1);
rece_inedx++;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))
{
}
if(rece_inedx >=len -1)
{
I2C_AcknowledgeConfig(I2C1, DISABLE);
I2C_GenerateSTOP(I2C1, ENABLE);
return I2C_REC_OK;
}
}
}
user_i2c_master_write(0xA0,senddata,1);
user_master_read(0xA0,recdata,5);
从机发送函数
接收过程是一但发现总线上的地址和自己地址匹配,就会发起中断。中断中开始接收数据
先把接收的数据存入BUFFER中,等到结束位产生,通知应用层任务处理接收到的数据。
I2C 在默认情况下是处于从机接收模式
从机STM32F103C8T6。开始双机通信
定义从机地址为0x80
/*PB6 I->I2C1_SCL PB7-> I2C1_SDA */
void user_I2C_GPIO_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*init GPIOA CLK*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
void user_I2C_it_init(void)
{
NVIC_InitTypeDef iic_rec_it_init_struct;
iic_rec_it_init_struct.NVIC_IRQChannel = I2C1_EV_IRQn;
iic_rec_it_init_struct.NVIC_IRQChannelCmd =ENABLE;
iic_rec_it_init_struct.NVIC_IRQChannelPreemptionPriority = 14;
iic_rec_it_init_struct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&iic_rec_it_init_struct);
I2C_ITConfig(I2C1, I2C_IT_EVT, ENABLE);
}
void user_I2C_init(void)
{
user_I2C_GPIO_init();
user_I2C_cfgInit();
user_I2C_it_init();
}
void I2C1_EV_IRQHandler(void)
{
uint8_t send_inedx=0;
int timeout;
timeout=10;
while (!I2C_CheckEvent(I2C1, I2C_EVENT_SLAVE_BYTE_RECEIVED))
{
timeout--;
user_i2c_wait();
if (timeout < 0)
{
I2C_GenerateSTOP(I2C1, ENABLE);
return STARTBIT_ERROR;
break;
}
}
recrvie_buffer[rev_index++] = I2C_ReceiveData(I2C1);
}
修改主机端发送代码
unsigned char senddata[5]={1,'a','b','c','d'};
user_i2c_master_write(0x80,senddata,5);
从机端接收到正确的字符。