[015] [STM32] IIC协议详解与HAL库相关函数分析

STM32
Contents
IIC协议
物理层
协议层
STM32硬件IIC
硬件IIC框架
主要寄存器
CubeMx配置
HAL库函数

1 IIC协议

1.1 物理层

image-20220329152929493

IIC(Inter Integrated Circuit)总线在物理层由SDA(Serial data, 串行数据线)、SCL(Serial clock line,串行时钟线)和上拉电阻组成。

  • 每个连接到总线的设备都有一个独立的地址,主机可以利用此地址进行不同设备之间的访问
  • 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制
  • 为了避免总线信号的混乱,要求各设备连接到总线的输出端时必须是**漏极开路(OD)输出或集电极开路(OC)**输出(IIC的空闲状态只能有外部上拉, 而此时空闲设备被拉到了高阻态,也就是相当于断路, 整个IIC总线只有开启了的设备才会正常进行通信,而不会干扰到其他设备)
  • 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式(线与特性)决定由哪个设备占用总线
  • 主机与从机之间的数据传输只在SDA一根线上完成,不能同时发送和接收数据,所以IIC是一种半双工的通信协议

高阻态:高阻状态是三态门电路的一种状态。逻辑门的输出除有高、低电平两种状态外,还有第三种状态——高阻状态的门电路。电路分析时高阻态可做开路理解。

漏极开路/集电极开路即为高阻态,若需要产生高电平,则需使用外部上拉电阻。

SDA 和SCL通过一个电流源或上拉电阻连接到正的电源电压,当总线空闲时,这两条线都是高电平,内部电平如下图所示:

image-20220329163158433

1.2 协议层

相关术语:

image-20220329153103314

IIC协议标准规定发起通信(控制时钟线,即控制SCL的电平高低变换)的设备称为设备,主设备发起一次通信后,其它设备均为设备。

发送器与接收器的角色,与主机和从机没有关系:

  1. 假设微控制器A 要发送信息到微控制器B:
    • 微控制器A(主机)寻址微控制器B(从机)
    • 微控制器A (主机-发送器)发送数据到微控制器B(从机-接收器
    • 微控制器A 终止传输
  2. 如果微控制器A 想从微控制器B 接收信息:
    • 微控制器A(主机)寻址微控制器B(从机)
    • 微控制器A (主机-接收器)发送数据到微控制器B(从机-发送器
    • 微控制器A 终止传输

1.2.1 数据有效性

image-20220329163214452

  • SCL为高电平的时候SDA表示的数据有效
  • SCL低电平时SDA进行电平切换,为下一次表示数据做好准备。因此,数据在SCL上升沿到来之前要准备好,在SCL下降沿前必须保持稳定。

1.2.2 起始与停止信号

image-20220329163321837

  • 起始信号:当SCL为高电平时,SDA由高到底的跳变。(SDA低电平延时>4.7us后,SCL变为低电平)
  • 停止信号:当SCL为高电平时,SDA由底到高的跳变。(SDA高电平延时需>4us)
/**
  * @brief I2C起始信号
  * SDA -> Output
  */
void I2CStart(void)
{
    
    
    SDA = 1;		// 确保SCL拉高前SDA为高电平
    delay_us(5);
    SCL = 1;
    delay_us(5);
	SDA = 0;
    delay_us(5);
    SCL = 0;		// SCL变低,起始信号结束
    delay_us(5);
}
/**
  * @brief I2C停止信号
  * SDA -> Output
  */
void I2CStop(void)
{
    
    
    SCL = 0;
    SDA = 0;
    delay_us(5); 
    SCL = 1;		// SCL拉高,释放总线控制权,停止接收数据
	SDA = 1;
    delay_us(5);
}

1.2.3 设备地址与数据传输方向(R/W)

image-20220329164610243

主机发起通讯时(产生起始信号),首先会通过SDA线发送设备地址(7位或10位)来查找从机,LSB位用来表示数据传输方向:

  • LSB = 1:主机向从机数据
  • LSB = 0:主机向从机数据

1.2.4 数据传输

只有当SCL为高电平时才能传输数据,且SDA线上的数据必须保持稳定(不允许高低跳变),只有当SCL为低时,SDA线上的数据才允许切换状态。

数据传输时先传输MSB,输出到SDA线上的数据必须为8位(传输设备地址时,7位地址+1位表示读/写),且每个字节后面必须紧跟一位应答位(即一帧数据9位

  • 写数据

主机确定了从机的设备地址后,生成一个开始信号,然后向IIC总线上面发送设备的地址和读写方向标志。从机检测到该地址和自己设备地址相对应后,回复主机一个应答信号。主机接收到应答信号后就开始向这个设备以字节为单位发送数据,每一个字节后面都会带有从机的应答信号,直到主机发送完成最后一个数据后生成一个停止信号结束此次数据的传输。

image-20220329190442812

image-20220329190122135

  • 读数据

读操作与写操作类似。

下面为发送/接收8位数据,不包括起始/停止条件:

/**
  * @brief 发送字节数据
  * SDA -> Output
  */
void I2CSendByte(uint8_t data)
{
    
    
    uint8_t i = 8;
    while (i--)
    {
    
    
        SCL = 0;
        delay_us(2);
        SDA = !!(data & 0x80);	// 将数据转换为bool型
        data <<= 1;
        delay_us(2);
        SCL = 1;
        delay_us(2);
    }
    SCL = 0;
    delay_us(2);
}
/**
  * @brief 读取字节数据
  * SDA -> Input
  */
uint8_t I2CReadByte(uint8_t ack)
{
    
    
    uint8_t i = 8, data = 0;
    SDA_Input_Mode();
    while (i--)
    {
    
    
        SCL = 0;
        delay_us(2);
        SCL = 1;
        delay_us(1);
        data <<= 1;		// 第一次右移为0,然后依次移位7次
        data |= SDA;
    }
    SDA_Output_Mode();
    if (ack)
        I2CSendACK();	// 发送应答
    else
        I2CSendNACK();	// 发送非应答
    return data;
}

1.2.5 响应 NACK/ACK

image-20220329163556303
当数据发送端传送8位数据结束后,在第9个时钟时,数据发送端会将SDA线拉高(释放SDA的控制权),防止数据冲突,由数据接收端控制SDA,此时:

  • SDA为高电平,表示非应答信号(NACK),说明数据接收端已成功地接收了该字节
  • SDA为低电平,表示应答信号(ACK),说明数据接收端接收该字节未成功
/**
  * @brief 发送应答信号
  * SDA -> Output
  */
void I2CSendACK(void)
{
    
    
    SCL = 0;
    SDA = 0;		//拉低SDA,产生应答信号
    delay_us(2);
    SCL = 1;
    delay_us(5);
    SCL = 0;
}
/**
  * @brief 发送非应答信号
  * SDA -> Output
  */
void I2CSendNACK(void)
{
    
    
    SCL = 0;
    SDA = 1;		//拉高SDA,不产生应答信号
    delay_us(2);
    SCL = 1;
    delay_us(5);
    SCL = 0;
}

当 发送器 需要等待并接收 接收器 的应答信号时,需要将发生器SDA数据线由输出模式修改为输入模式:

/**
  * @brief 等待应答信号
  * SDA -> Input
  */
int I2CSendACK(void)
{
    
    
    uint8_t timeout = 5;
    SDA_Input_Mode();  // 将主机SDA引脚GPIO变为输入模式
    delay_us(2);
    SCL = 1;
    delay_us(2);
    while (SDA)			// 读取SDA总线电平, 若接收器应答则会低电平退出循环;否则将超时错误返回
    {
    
    
        timeout--;
        delay_us(1);
        if (0 == timeout)
        {
    
    
            SDA_Output_Mode();	// 将主机SDA引脚GPIO变为输出模式
            I2CStop();
            return ERROR;
        }
    }
    SDA_Output_Mode();
    SCL = 0;
    delay_us(2);
    return SUCCESS;
}

1.2.6 仲裁

  • 时钟同步仲裁

image-20220329155856221

IIC的仲裁机制得益于其开漏的输入输出结构。当SCL线上挂载的多个设备,其中的MCU2的SCL输出低电平,那么这条IIC总线SCL就会被MCU2拉低,体现线与特性。

如下图所示,CLK1和CLK2都是连接在一条SCL线上的设备同时产生的时钟信号,由于IIC总线存在“线与”的特性,同为高电平才能输出高电平,有1个为低电平则全部为低电平,因此同一条SCL总线上面的时钟都是相同的。
image-20220329160431534

由此可知:产生的同步SCL 时钟的低电平周期由低电平时钟周期最长的器件决定,而高电平周期由高电平时钟周期最短的器件决定。

  • 数据传输仲裁

SDA仲裁也是基于“线与”的特性,因为SCL高电平时才能传输数据,所以SCL高电平期间开始仲裁。

下图显示了两个主机的仲裁过程,在第1个和第2个周期内DATA1和DATA2的数据都是相同的,当在第2个时钟周期时DATA1与SDA的数据不一致,这个时候设备1就会停止发送数据,转而启动接收模式(变为高电平)。这样SDA的数据就会与DATA2的数据保持一致,并且设备1停止发送数据也不会影响SDA的数据。

image-20220329162004478

注意:在串行传输时当重复起始条件或停止条件发送到I2C 总线的时侯仲裁过程仍在进行,即一帧数据完全相同,此时相关主机必须发送重复起始条件或停止条件,因此仲裁在不能下面情况进行:

  • 重复起始条件和数据位
  • 停止条件和数据位
  • 重复起始条件和停止条件

从机不参与上述仲裁。

2 STM32硬件IIC

2.1 硬件IIC框架

image-20220329201028198

2.2 主要寄存器

在这里插入图片描述

2.3 CubeMx配置

image-20220329201157230

时间配置:

  • I2C Speed Mode:
    • Standard Mode标准模式(100K)
    • Fast Mode快速模式(400K)
    • Fast Plus Mode高速模式(1000K)
  • Rise time:增加SDA和SCL升沿延时时间 ns(I2C->TIMINGR bit[23:20] SCLDEL[3:0]位)
  • Fall time:增加SDA和SCL升沿延时时间 ns(I2C->TIMINGR bit[19:16] SDADEL[3:0]位) — 需关闭时钟拉伸才生效
  • Coefficient of Digital Filter:配置模拟噪声滤波器系数,在SDA和SCL输入端有一个模拟噪声滤波器,要求在快模式和快模式Plus中抑制脉冲宽度高达50 ns的尖峰。通过I2C->CR1ANFOFF位使能/禁用模拟噪声滤波器,通过I2C->CR1DNF[3:0]位选择数字滤波器系数。
  • Analog Filter:即为模拟噪声滤波器使能/禁用,注意复位默认使能,即ANFOFF位为0表示使能

从机配置:

  • Clock No Stretch Mode:配置时钟拉伸模式(I2C->CR1 bit17 NOSTRETCH位),clock stretching通过将SCL线拉低来暂停一个传输,直到释放SCL线为高电平,传输才继续进行,一般不用。
  • General Call Address Detection:通用呼叫地址检测。在Slave模式下,接口能够识别自己的地址(7位或10位)和通用呼叫地址
  • Primary Address Length selection: 从设备地址长度 一般为7位,通讯时7位地址+1位读写做开头
  • Dual Address Acknowledged: 双地址确认,当主地址是7位长度时,可以有一个双地址
  • Primary slave address:从设备初始地址(地址值从0到127,且生成的地址值左移1位,因为LSB需表示R/W)

2.4 HAL库函数

HAL函数模型有轮询、中断、DMA三种,下面仅分析轮询模式函数

2.4.1 主机写/读数据

  • 写数据
/**
  * @brief  在主机模式下以阻塞模式传输数据
  * @param  hi2c 
  * @param  DevAddress  目标设备地址: 7位地址, 必须向左移1位!!!
  * @param  pData 		指针数据缓冲区的指针(写入过程中指针会根据字字数自增)
  * @param  Size 		要发送数据的字节数
  * @param  Timeout 	超时时间
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData,
                                          uint16_t Size, uint32_t Timeout)
  • 读数据
/**
  * @brief  在主机模式下以阻塞模式接收数据
  * @param  hi2c
  * @param  DevAddress  目标设备地址: 7位地址, 必须向左移1位!!!
  * @param  pData 		指针数据缓冲区的指针(读取过程中指针会根据字字数自增)
  * @param  Size        要接收数据的字节数
  * @param  Timeout 	超时时间	
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData,
                                         uint16_t Size, uint32_t Timeout)

2.4.2 从机写/读数据

  • 写数据
/**
  * @brief  在从机模式下以阻塞模式接收数据
  * @param  hi2c
  * @param  pData 		指针数据缓冲区的指针
  * @param  Size        要接收数据的字节数
  * @param  Timeout 	超时时间	
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_I2C_Slave_Transmit(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size,
                                         uint32_t Timeout)

HAL_I2C_Slave_Receive读数据与之类似。和主机相比,少了设备地址参数。

2.4.3 向从机特定的内存地址写/读入数据

  • 写数据
/**
  * @brief  在阻塞模式下向从机特定的内存地址写/读入数据
  * @param  hi2c 
  * @param  DevAddress  目标设备地址: 7位地址, 必须向左移1位!!!
  * @param  MemAddress 	从机寄存器地址(写入过程中会自加)
  * @param  MemAddSize  从机寄存器地址的大小(8位或16位)
  * @param  pData 		指针数据缓冲区的指针
  * @param  Size        要接收数据的字节数
  * @param  Timeout 	超时时间
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress,
                                    uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout)

该函数适用于IIC外设里面还有子地址寄存器的设备,如AT24CXX E2PROM存储器,除了设备地址,每个存储字节都有其对应的地址。

其中MemAddSize可选宏:

#define I2C_MEMADD_SIZE_8BIT            (0x00000001U)
#define I2C_MEMADD_SIZE_16BIT           (0x00000002U)

使用**HAL_I2C_Mem_Write等于先使用HAL_I2C_Master_Transmit传输第一个寄存器地址,再用HAL_I2C_Master_Transmit**传输写入第一个寄存器的数据:

uint8_t Cmd_Code[2] = {
    
    0x00, 0x00};
uint8_t Data_Code[2] = {
    
    0x40, 0x00};
extern I2C_HandleTypeDef hi2c3;
void OLED_Write(uint8_t type, uint8_t data)
{
    
    
    if (type == TYPE_COMMAND)
    {
    
    
        Cmd_Code[1] = data;
        // HAL_I2C_Master_Transmit(&hi2c3, 0x78, Cmd_Code, 2, 100);
        HAL_I2C_Mem_Write(&hi2c3, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, &data,  1, 100);
    }
    else
    {
    
    
        Data_Code[1] = data;
        // HAL_I2C_Master_Transmit(&hi2c3, 0x78, Data_Code, 2, 100);
        HAL_I2C_Mem_Write(&hi2c3, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, &data,  1, 100);
    }
}

HAL_I2C_Mem_Read读数据与写数据函数类似。


参考:

END

猜你喜欢

转载自blog.csdn.net/kouxi1/article/details/123834448