玩转STM32-I2C通信协议(详细-慢工出细活)

文章目录

一、I2C总线原理(掌握)

1.1 硬件构成

I2C总线由串行数据线SDA和串行时钟线CL构成,总线上的每个器件都有一个唯一的地址。I2C总线规范要求SDA和SCL可双向通信,即一个器件既可以接收,也可以发送数据或时钟,因此I2C信号线SDA和SCL采用开集电极输出或开漏极输出方式。I2C总线必须通过上拉电阻或电流源才能够正确收发数据。

I2C总线接口内部等效电路包括输入缓冲电路与开集电极输出晶体管或开漏极MOS管。当总线处于空闲状态时,由于上拉电阻的作用,总线呈现高电平,如果某个芯片需要输出数据,可以通过输出驱动实现数据传输。开集电极出书电路有一个缺点:随着总线长度增加,输出等效电容也随之增加,上拉电阻将严重影响总线通信速度。原因是信号变化要通过RC充放电回路,从而降低了信号的转换速度。为了克服I2C总线这个缺点,NXP公司开了有源I2C总线终端,它采用两个互联的充电泵来等效上拉电阻,信号变化瞬间有源器件可以提供相当大的充放电电流,加快信号转换速率,降低寄生电容的影响。

1.2 传输位

  1. 数据有效性
    I2C总线以串行方式传输数据,数据传输是按照时钟节拍进行的。时钟线每产生一个时钟脉冲,数据线传输一位数据。I2C总线协议标准规定,SDA线上的数据必须在时钟线为高电平时保持稳定,数据线电平状态只能在时钟线位低电平时改变,在标准模式下,高低电平宽度必须不小于4.7us,I2C数据有效示意图如下:
  2. 起始条件和停止条件
    当时钟线为高电平时,如果SDA数据为逻辑高电平,则代表数字1,如果SDA数据线为低电平时,则代表数据0.除此之外,在SCL为高电平时,还会有数据线SDA出现上升沿后下降沿等两种状态。I2C总线协议规定,SCL时钟线为高电平时且SDA为下降沿表示起始信号,SCL时钟线为高电平且SDA为上升沿表示停止信号。I2C总线数据传输必须以起始信号启动传输,以停止信号结束一次数据传输,I2C起始位和停止位如下:
  3. 重复开始信号
    在I2C总线上,由主机发送一个起始位,启动一次数据传输后,在发送停止位前,主机可以再发送一次起始位,这个信号称为重复起始位。它可以帮助主机再不丧失总线控制权的前提下改变数据传输方向或切换到与其他从机通信,它的实现方法是再时钟信号为高电平时,SDA由高电平向低电平跳变,产生一个重复起始位,它本质上就是一个起始位。
  4. 应答信号与非应答信号
    I2C总线协议规定,发送器每发送一个字节(8bit)数据,接收器必须产生一个应答信号或非应答信号。实现方法是,发送器发送完8位数据后,第9个时钟信号将数据线置高电平,接收器根据通信状态可以将数据线拉低,产生一个应答信号;或保持数据线为高电平,产生一个非应答信号。

1.3数据传输格式

一般情况下,一个标准I2C通信由四部分组成:起始信号、从机地址、数据传输、停止信号。I2C通信由主机发送一个起始信号来启动,然后由主机对从机寻址并决定数据传输方向。I2C总线上传输数据的最小单位是一个字节,首先发送数据位最高位,每传送完一个字节,接收器必须发送一个应答位,如果数据接收器来不及处理数据,可以通过拉低时钟线SCL来通知数据发送器暂停传输;每次通信的数据字节数是没有限制的;全部数据传送结束后,由主机发送停止信号,结束通信。I2C通信时序如下:

  1. I2C总线寻址约定
    I2C总线采用软件方法实现从机寻址来简化总线连接,I2C总线采用了独特的寻址约定,规定了起始信号后的第一个字节位寻址字节,用来寻址被控器件,并规定数据传输方向。目前I2C支持7位寻址方式和10位寻址方式,为了使读者更容易理解I2C操作方式,重点解释7位寻址模式,再掌握7位寻址模式后,可以很容易的理解10位寻址模式。
    在7位寻址模式中,寻址字节由从机的7位地址位(D7~D1)和1位读写位(D0)组成。当读写位D0=1时,表示从下一个字节开始主机从从机读取数据;当读写位D0=0时,表示从下一个字节开始主机将数据传输给从机。主机发送起始信号后立即传送寻址字节,总线上的所有器件都将寻址字节中的7位地址与自己的地址比较,如果两者相同,则该器件认为被主机寻址,并发送应答信号,寻址字节中的读写位决定了主机和从机时发送器还是接收器。
    主机作为被控器时,其7位地址在I2C总线地址寄存器中给出,为软件地址,而非单片机类型的外围器件地址,完全由器件类型与引脚电平给定。在I2C总线中,不允许有两个地址相同的器件,否则就会造成传输错误。
  2. 数据传世模式
    1)主机从从机读取N个字节
    主机首先产生起始信号,然后发送寻址字节,寻址字节传输完毕,主机释放数据线(数据线拉高),并产生一个时钟信号,等待被寻址器件应答信号。
    被寻址器件一旦检测到寻址地址与自己的地址相同则产生一个应答信号,从机发送完应答信号后,开始发送数据。从机每发送完一个字节数据,主机产生一个应答信号。
    当数据传送完毕后,主机产生一个非应答信号结束数据传输,然后主机产生一个停止信号结束通信或产生一个重复起始信号进入下一次数据传输。
    在数据传输过程中,主机随时可以产生非应答信号来提前结束本次数据传输。
    2)主机向从机写N个字节
    主机首先产生起始信号,然后发送寻址字节,寻址自己传输完成后,根据D0为判断时读取还是发送数据,主机产生一个时钟信号,等待从记得应答信号。
    被寻址器件一旦检测到寻址地址与自身的地址相同,则产生一个应答信号,主机收到应答信号后,开始发送数据。主机没发送一个字节的数据,从机产生一个应答信号。
    当数据传送完毕后,主机产生一个停止信号结束数据传输或产生一个重复起始信号进入下一次数据传输。
    3)重复起始位
    当主机在访问类似存储器器件时,主机除了发送寻址地址字节来确定从机外,还要发送存储单元地址内容;如果需要读取存储单元数据,存在着先写后读的情况,为了解决这个问题,可以利用重复起始信号来实现这个过程:
    主机首先按照(2)中的主机向从机写入多字节数据,将存储单元地址写入从机,数据传输结束后并不产生停止信号而是产生一个重复起始位,然后发送寻址字节。寻址字节中,读写位D0=1,然后等待从机应答,从机发完应答位后,开始将数据传输给主机,然后执行过程和(1)中相同。
    重复起始位开可以让主机在不丧失总线控制权的情况下,寻址下一个器件,与另外一个从机进行通信。
    4)冲裁与同步
    所有主机在SCL线上产生自己的时钟来传输,I2C总线上的数据只有适中的高电平周期有限,因此需要一个确定的时钟进行逐位仲裁。

二、STM32的I2C特性和结构

STM32的I2C模块具有4中工作模式,即主发送器模式、主接收器模式、从发送器模式、从接收器模式。下图为I2C内部结构:

三、STM32的I2C通信实现(硬件实现方式)

3.1 I2C主模式

在主模式时,,I2C接口启动数据传输并产生时钟信号。串行数据传输总是以起始条件开始并以停止条件结束。当通过START位在总线上产生了起始条件,设备就进入了主模式。 以下是主模式所要求的操作顺序:

● 在I2C_CR2寄存器中设定该模块的输入时钟以产生正确的时序

● 配置时钟控制寄存器

● 配置上升时间寄存器

● 编程I2C_CR1寄存器启动外设

● 置I2C_CR1寄存器中的START位为1,产生起始条件

I2C模块的输入时钟频率必须至少是:

● 标准模式下为:2MHz

● 快速模式下为:4MHz

  1. 发送起始条件
    当总线空闲(BUSY = 0)时,发送起始信号(START = 1),I2C接口将产生一个起始信号并切换至主模式。在主模式下,设置START位将在当前字节传输完成后由硬件产生一个重开始条件,起始信号一旦发出,SB位被硬件置位,如果中断未屏蔽,则会产生一个中断。然后主设备等待读状态寄存器SR1,接着将从地址写入DR寄存器。
  2. 从地址的发送
    从地址通过内部移位寄存器被送到SDA线上。下图中,在7位地址模式时,只需送出一个地址字节。一旦该地址字节被送出,ADDR位被硬件置位,如果中断允许,则产生一个中断。然后主设备等待一次读SR1寄存器,读SR2寄存器。
  3. 发送数据
    在发送地址和清除ADDR位后,将等待发送的数据写入数据寄存器DR,I2C模块通过内部移位寄存器将数据字节从DR寄存器发送到SDA线上。主设备等待发送完毕即TxE被清除,如下图EV8事件。
    当收到应答脉冲时,TxE位被硬件置位,如果允许中断,则产生一个中断。如果TxE位置位并且在上一次数据发送结束之前没有写入新的数据字节到DR寄存器,则BTF被置位,I2C模块拉长时钟线等待数据写入DR数据寄存器,数据写入后将BTF清除,I2C继续发送数据。
  4. 停止和结束
    在DR寄存器中写入最后一个字节后,通过设置STOP位产生一个停止条件,如下图EV8_2,然后I2C接口将自动回到从模式。
    下图是I2C主模式下的数据发送示意图:

四、应用实例

  1. 利用I2C控制OLED显示屏
  2. 实例代码
    1)I2C硬件配置
c 复制代码
void OledDriver_Init(void)
{
	OledDriver_GPIO_Configuration();
	OledDriver_I2C1_Configuration();
}
//=============================================================================
//文件名称:OledDriver_GPIO_Configuration
//功能概要:OLED显示屏引脚配置
//参数说明:无
//函数返回:无
//=============================================================================
void OledDriver_GPIO_Configuration(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB , ENABLE);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_OLED_I2C1_SCL_PIN | GPIO_OLED_I2C1_SDA_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIO_OLED_I2C1_PORT, &GPIO_InitStructure);
}
//=============================================================================
//文件名称:OledDriver_I2C2_Configuration
//功能概要:OLED显示屏I2C配置
//参数说明:无
//函数返回:无
//=============================================================================
void OledDriver_I2C1_Configuration(void)
{
	I2C_InitTypeDef I2C1_InitStructure;
	RCC_APB1PeriphClockCmd( RCC_APB1Periph_I2C1, ENABLE); 
	
	I2C1_InitStructure.I2C_Mode = I2C_Mode_I2C;
	I2C1_InitStructure.I2C_ClockSpeed = I2C1_SPEED;
	I2C1_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
	I2C1_InitStructure.I2C_OwnAddress1 = I2C1_OWN_ADDRESS1;
	I2C1_InitStructure.I2C_Ack = I2C_Ack_Enable;
	I2C1_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	
	I2C_Init(OLED_I2C,&I2C1_InitStructure);
	I2C_Cmd(OLED_I2C,ENABLE);
	I2C_AcknowledgeConfig(OLED_I2C, ENABLE);
}
//=============================================================================
//文件名称:I2C1_Byte_Write
//功能概要:I2C写一个字节
//参数说明:无
//函数返回:无
//=============================================================================
void I2C1_Byte_Write(uint8_t addr, uint8_t data)
{
	// 发送开始信号
	I2C_GenerateSTART(OLED_I2C,ENABLE);
	// 检查EV5事件
	while(!I2C_CheckEvent(OLED_I2C,I2C_EVENT_MASTER_MODE_SELECT));  // 检测是否作为主机,开始信号是否成功
	// 发送设备写地址
	I2C_Send7bitAddress(OLED_I2C,OLED_SLAVE_WRITE_ADDR,I2C_Direction_Transmitter);
	// 检查EV6事件
	while(I2C_CheckEvent(OLED_I2C,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR);  // 检查地址是否发送成功
	// 发送设备内部地址
	I2C_SendData(OLED_I2C,addr);
	// 检查EV8_1事件
	while(I2C_CheckEvent(OLED_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR);  // 检查移位寄存器中是否还有数据
	// 发送数据
	I2C_SendData(OLED_I2C,data);
	// 检查EV8_2事件
	while(I2C_CheckEvent(OLED_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR);  // 检查移位寄存器中是否还有数据
	// 发送停止信号
	I2C_GenerateSTOP(OLED_I2C,ENABLE);
}
//=============================================================================
//文件名称:I2C1_Page_Write
//功能概要:I2C写多个字节
//参数说明:无
//函数返回:无
//=============================================================================
void I2C1_Page_Write(uint8_t addr, uint8_t *pdata,uint16_t Num_ByteToWite)
{
//	I2CTimeout = I2CT_LONG_TIMEOUT;
	// 发送开始信号
	I2C_GenerateSTART(OLED_I2C,ENABLE);
	// 检查EV5事件
	while(I2C_CheckEvent(OLED_I2C,I2C_EVENT_MASTER_MODE_SELECT) == ERROR); 
	// 发送设备写地址
	I2C_Send7bitAddress(OLED_I2C,OLED_SLAVE_WRITE_ADDR,I2C_Direction_Transmitter);
	// 检查EV6事件
	while(I2C_CheckEvent(OLED_I2C,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR);  // 检查地址是否发送成功
	// 发送设备内部地址
	I2C_SendData(OLED_I2C,addr);
	// 检查EV8_1事件
	while(I2C_CheckEvent(OLED_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR);  // 检查移位寄存器中是否还有数据
	while(Num_ByteToWite)
	{
		I2C_SendData(OLED_I2C,*pdata);
		Num_ByteToWite--;
		pdata++;
	}
	// 检查EV8_2事件
	while(I2C_CheckEvent(OLED_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR); 
	// 发送停止信号
	I2C_GenerateSTOP(OLED_I2C,ENABLE);
}
相关推荐
FreakStudio1 小时前
全网最适合入门的面向对象编程教程:56 Python字符串与序列化-正则表达式和re模块应用
python·单片机·嵌入式·面向对象·电子diy
EVERSPIN4 小时前
分享国产32位单片机的电机控制方案
单片机·嵌入式硬件
每天一杯冰美式oh4 小时前
51单片机的家用煤气报警系统【proteus仿真+程序+报告+原理图+演示视频】
嵌入式硬件·51单片机·proteus
芯橦7 小时前
【瑞昱RTL8763E】音频
单片机·嵌入式硬件·mcu·物联网·音视频·visual studio code·智能手表
夜间去看海11 小时前
基于单片机的智能浇花系统
单片机·嵌入式硬件·智能浇花
VirtuousLiu11 小时前
LM74912-Q1用作电源开关
单片机·嵌入式硬件·ti·电源设计·lm74912·电源开关
打地基的小白11 小时前
软件I2C-基于江科大源码进行的原理解析和改造升级
stm32·单片机·嵌入式硬件·通信模式·i2c
Echo_cy_12 小时前
STM32 DMA+AD多通道
stm32·单片机·嵌入式硬件
朴人12 小时前
【从零开始实现stm32无刷电机FOC】【实践】【7.2/7 完整代码编写】
stm32·单片机·嵌入式硬件·foc
追梦少年时12 小时前
STM32中断——外部中断
stm32·单片机·嵌入式硬件