stm32学习第十二天

115 阅读12分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

记录I2C(I2代表I的平方)学习相关笔记。

一、I2C 协议简介

I2C 通讯协议(Inter-Integrated Circuit)是由 Phiilps 公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、CAN 等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。 I2C和USART类似分为物理层和协议层

二、I2C协议物理层

在这里插入图片描述

1)物理层就是一个支持设备的总线,“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。当然也有数量限制,但是一般不会超出总线,因为一般不会在一个总线上挂载百多个外设。 2)从图上可以看到,I2C有两条总线一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。 3)主机想要访问外设,是通过外设地址来访问,每个外设都有独立的地址。 4)总线通过上拉电阻接到电源,当I2C设备空闲时候,会输出高阻态(相当于断路),需要的外设挂载到总线上,这样就不会产生访问冲突。当所有外设都空闲的时候,都输出高阻态时,由上拉电阻把总线拉成高电平。 5)多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。(类似优先级) 6)具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I 2 C 设备尚不支持高速模式。 7)连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。

三、I2C协议层

I2C 的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。(与USART类似) 下面框图讲解: 阴影部分代表主机主动的,空白代表从机(外设)主动的。

3.1 I2C协议的写过程

写过程就是主机向外设写入东西的过程,这里的0就是标志位。一般我们数据都是以字节来算,是8位,但是I2C总线上外设地址是7位,最后1位 用0来代表写、用1来代表读。

这里解释第4步: 第二步主机在总线上发起广播,我要找到对应的地址,总线的外设就自己检查自己的地址,是否和主机的一样,如果一样,那就回复主机,你要找的是我。 在这里插入图片描述

3.2 I2C协议的读过程

在这里插入图片描述

3.2 I2C协议的复合读写过程

在这里插入图片描述

四、通讯的起始和停止信号

在这里插入图片描述 前文中提到的起始(S)和停止(P)信号是两种特殊的状态,见图 24-5。当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当 SCL 是高电平时 SDA线由低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。

五、数据有效性

在这里插入图片描述 I2C使用 SDA信号线来传输数据,使用 SCL信号线进行数据同步。见图 24-6。SDA数据线在 SCL的每个时钟周期传输一位数据。传输时,SCL为高电平的时候 SDA表示的数据有效,即此时的SDA为高电平时表示数据“1”,为低电平时表示数据“0”。当SCL为低电平时,SDA的数据无效,一般在这个时候 SDA进行电平切换,为下一次表示数据做好准备。 也就是说在SCL为低电平的时候,SDA的数据可能进行交换。 每次数据传输都以字节为单位,每次传输的字节数不受限制。

六、地址及数据方向

在这里插入图片描述 I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向,它是数据方向位(R/W——),第 8位或第 11位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。这就是刚才说的读或则写的标志位。

七、响应

数据传输一定要有对应的响应,响应包括“应答(ACK)”和“非应答(NACK)”两种信号。(我还行,我不行了)作为数据接收端时,当设备(无论主从机)接收到 I2C传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。

在这里插入图片描述 传输时主机产生时钟,在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制 SDA,若 SDA 为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。

八、STM32的 I2C架构图详解

STM32 的 I2C 外设可用作通讯的主机及从机,支持 100Kbit/s 和 400Kbit/s 的速率,支持 7 位、10 位设备地址,支持 DMA 数据传输,并具有数据校验功能。

在这里插入图片描述

8.1 通讯引脚

I 2 C 的所有硬件架构都是根据图中左侧 SCL 线和 SDA 线展开的(其中的 SMBA 线用于SMBUS的警告信号,I2C通讯没有使用)。STM32芯片有多个 I2C外设,它们的 I2C通讯信号引出到不同的 GPIO 引脚上,使用时必须配置到这些指定的引脚。

引脚I2C1I2C2
SCLPB5 / PB8(重映射)PB10
SDAPB6 / PB9重映射)PB11

8.2 时钟控制逻辑

SCL线的时钟信号,由 I 2 C 接口根据时钟控制寄存器(CCR)控制,控制的参数主要为时钟频率。配置 I2C的 CCR 寄存器可修改通讯速率相关的参数 1)可选择 I2C 通讯的“标准/快速”模式,这两个模式分别 I2C 对应 100/400Kbit/s 的通讯速率。 2)在快速模式下可选择 SCL 时钟的占空比,可选 Tlow/Thigh=2 或 Tlow/Thigh=16/9模式,我们知道 I2C 协议在 SCL 高电平时对 SDA 信号采样,SCL 低电平时 SDA准备下一个数据,修改 SCL 的高低电平比会影响数据采样,但其实这两个模式的比例差别并不大,若不是要求非常严格,这里随便选就可以了。 3)CCR 寄存器中还有一个 12 位的配置因子 CCR,它与 I2C 外设的输入时钟源共同作用,产生 SCL 时钟,STM32 的 I2C 外设都挂载在 APB1 总线上,使用 APB1 的时钟源 PCLK1,SCL信号线的输出时钟公式如下: 在这里插入图片描述

8.3 数据控制逻辑

I2C 的 SDA 信号主要连接到数据移位寄存器上,数据移位寄存器的数据来源及目标是数据寄存器(DR)、地址寄存器(OAR)、PEC 寄存器以及 SDA 数据线。当向外发送数据的时候,数据移位寄存器以“数据寄存器”为数据源,把数据一位一位地通过 SDA 信号线发送出去;当从外部接收数据的时候,数据移位寄存器把 SDA 信号线采样到的数据一位一位地存储到“数据寄存器”中。 若使能了数据校验,接收到的数据会经过 PCE 计算器运算,运算结果存储在“PEC 寄存器”中。当 STM32 的 I2C 工作在从机模式的时候,接收到设备地址信号时,数据移位寄存器会把接收到的地址与 STM32 的自身的“I2C 地址寄存器”的值作比较,以便响应主机的寻址。STM32 的自身 I2C 地址可通过修改“自身地址寄存器”修改,支持同时使用两个 I2C设备地址,两个地址分别存储在 OAR1和 OAR2中。 在这里插入图片描述

数据寄存器就像一个仓库,发送的数据从这里开始,接收的数据在这里存放。

九、通讯过程

9.1 主发送器

在这里插入图片描述蓝色的框子就像流程,红色的框子就像流程对应的时间,需要这个事件来检测上一步是否完成。

流程说明: 1)控制产生起始信号(S),当发生起始信号后,它产生事件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已经发送; 2)紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”及“EV8”,这时 SR1 寄存器的“ADDR”位及“TXE”位被置 1,ADDR 为 1表示地址已经发送,TXE 为 1表示数据寄存器为空; 3)以上步骤正常执行并对 ADDR 位清零后,我们往 I2C 的“数据寄存器 DR”写入要发送的数据,这时TXE位会被重置0,表示数据寄存器非空,I2C外设通过SDA信号线一位位把数据发送出去后,又会产生“EV8”事件,即 TXE 位被置 1,重复这个过程,就可以发送多个字节数据了; 4)当我们发送数据完成后,控制 I2C 设备产生一个停止信号(P),这个时候会产生EV8_2 事件,SR1 的 TXE位及 BTF位都被置 1,表示通讯结束。

9.2 主接收器

在这里插入图片描述 流程说明:

  1. 同主发送流程,起始信号(S)是由主机端产生的,控制发生起始信号后,它产生事件“EV5”,并会对 SR1寄存器的“SB”位置 1,表示起始信号已经发送;
  2. 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”这时SR1 寄存器的“ADDR”位被置 1,表示地址已经发送。
  3. 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生“EV7”事件,SR1寄存器的 RXNE被置 1,表示接收数据寄存器非空,我们读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时我们可以控制I2C 发送应答信号(ACK)或非应答信号(NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输; (4) 发送非应答信号后,产生停止信号(P),结束传输。

十、编程实战

10.1 I2C结构体详解

typedef struct {
	uint32_t I2C_ClockSpeed; /*!< 设置 SCL 时钟频率,此值要低于 400000*/
	uint16_t I2C_Mode; /*!< 指定工作模式,可选 I2C 模式及 SMBUS 模式 */
	uint16_t I2C_DutyCycle; /*指定时钟占空比,可选 low/high = 2:1 及 16:9 模式*/
	uint16_t I2C_OwnAddress1; /*!< 指定自身的 I2C 设备地址 */
	uint16_t I2C_Ack; /*!< 使能或关闭响应(一般都要使能) */
	uint16_t I2C_AcknowledgedAddress; /*!< 指定地址的长度,可为 7 位及 10 位 */
	} I2C_InitTypeDef;

10.2 编程要点

  1. 配置通讯使用的目标引脚为开漏模式;
  2. 使能 I2C外设的时钟;
  3. 配置 I2C外设的模式、地址、速率等参数并使能 I2C外设;
  4. 编写基本 I2C按字节收发的函数;
  5. 编写读写 EEPROM 存储内容的函数;
  6. 编写测试程序,对读写数据进行校验。

10.3 根据结构体编程

根据霸道的原理图可以看到这样一个电路图,

在这里插入图片描述 GPIOB6和GPIOB7代表输入,我们需要打开对应的时钟。I2C我们也要打开对应的时钟。 在这里插入图片描述

对应APB1和APB2的时钟。 然后就是初始化CPIO口,配置I2C模式。

//配置GPIO口
static void I2C_GPIO_Config(void)
{
  GPIO_InitTypeDef  GPIO_InitStructure; 

	/* 使能与 I2C 有关的时钟 */
	EEPROM_I2C_APBxClock_FUN ( EEPROM_I2C_CLK, ENABLE );
	EEPROM_I2C_GPIO_APBxClock_FUN ( EEPROM_I2C_GPIO_CLK, ENABLE );
	
    
  /* I2C_SCL、I2C_SDA*/
  GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;	       // 开漏输出
  GPIO_Init(EEPROM_I2C_SCL_PORT, &GPIO_InitStructure);
	
  GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_PIN;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;	       // 开漏输出
  GPIO_Init(EEPROM_I2C_SDA_PORT, &GPIO_InitStructure);	

}
//配置I2C
static void I2C_Mode_Configu(void)
{
  I2C_InitTypeDef  I2C_InitStructure;
  /* I2C 配置 */
  I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
	/* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
  I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
  I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7; 
  I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
	/* I2C的寻址模式 */
  I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	/* 通信速率 */
  I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
	/* I2C 初始化 */
  I2C_Init(EEPROM_I2Cx, &I2C_InitStructure);
	/* 使能 I2C */
  I2C_Cmd(EEPROM_I2Cx, ENABLE);   
}

这里有一些宏定义

#define             EEPROM_I2Cx                                I2C1
#define             EEPROM_I2C_APBxClock_FUN                   RCC_APB1PeriphClockCmd
#define             EEPROM_I2C_CLK                             RCC_APB1Periph_I2C1
#define             EEPROM_I2C_GPIO_APBxClock_FUN              RCC_APB2PeriphClockCmd
#define             EEPROM_I2C_GPIO_CLK                        RCC_APB2Periph_GPIOB     
#define             EEPROM_I2C_SCL_PORT                        GPIOB   
#define             EEPROM_I2C_SCL_PIN                         GPIO_Pin_6
#define             EEPROM_I2C_SDA_PORT                        GPIOB 
#define             EEPROM_I2C_SDA_PIN                         GPIO_Pin_7

接下来就是向外设传输数据,传输数据需要找到这个外设的地址。 我们先传输一个简单的字节。 所以:

//传输地址和传输的数据
void EEPROM_Byte_Write(uint8_t addr,uint8_t data)
{
		//产生起始信号
		 I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
		/* 设置超时等待时间 */
  	I2CTimeout = I2CT_FLAG_TIMEOUT;  
  	/* 判断这个起始信号是否发送完成,并进行超时处理*/	
		/* 检测 EV5 事件并清除标志*/
  	while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))  
  	{
   	 if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
  	} 
  	/* 设置超时等待时间 */
  	I2CTimeout = I2CT_FLAG_TIMEOUT;
		I2CTimeout = I2CT_FLAG_TIMEOUT;
  	/* 发送 EEPROM 设备地址 */
 		I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
 		/* 检测 EV6 事件并清除标志*/
  	while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
  	{
   		 if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
  	}  
  	/* 发送要写入的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */
  	I2C_SendData(EEPROM_I2Cx, WriteAddr);
  	I2CTimeout = I2CT_FLAG_TIMEOUT;
  	/* 检测 EV8 事件并清除标志 */
  	while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
  	{
    	if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
  	} 
  	/*检测 EV8 事件并清除标志*/
  	I2C_SendData(EEPROM_I2Cx, *pBuffer); 
  	I2CTimeout = I2CT_FLAG_TIMEOUT;  
  	/* 检测 EV8 事件并清除标志*/
  	while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
  	{
    	if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
  	} 
	  /* 发送停止信号 */
  	I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
  	return 1;
}
}

剩下的代码就不写了,因为我自己没写,只是按照视频分析。详细可以去看stm32IIC第5-7节。