STM32 IIC(I2C)总线协议

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

在这里插入图片描述
从机端接收到正确的字符。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值