jjzjj

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

柯西的彷徨 2023-04-12 原文
STM32
Contents
IIC协议
物理层
协议层
STM32硬件IIC
硬件IIC框架
主要寄存器
CubeMx配置
HAL库函数

1 IIC协议

1.1 物理层

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

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

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

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

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

1.2 协议层

相关术语:

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

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

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

1.2.1 数据有效性

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

1.2.2 起始与停止信号

  • 起始信号:当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)

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

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

1.2.4 数据传输

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

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

  • 写数据

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

  • 读数据

读操作与写操作类似。

下面为发送/接收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


当数据发送端传送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 仲裁

  • 时钟同步仲裁

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

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

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

  • 数据传输仲裁

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

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

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

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

从机不参与上述仲裁。

2 STM32硬件IIC

2.1 硬件IIC框架

2.2 主要寄存器

2.3 CubeMx配置

时间配置:

  • 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

有关[015] [STM32] IIC协议详解与HAL库相关函数分析的更多相关文章

  1. ruby - 在没有 sass 引擎的情况下使用 sass 颜色函数 - 2

    我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re

  2. ruby-on-rails - 在 ruby​​ 中使用 gsub 函数替换单词 - 2

    我正在尝试用ruby​​中的gsub函数替换字符串中的某些单词,但有时效果很好,在某些情况下会出现此错误?这种格式有什么问题吗NoMethodError(undefinedmethod`gsub!'fornil:NilClass):模型.rbclassTest"replacethisID1",WAY=>"replacethisID2andID3",DELTA=>"replacethisID4"}end另一个模型.rbclassCheck 最佳答案 啊,我找到了!gsub!是一个非常奇怪的方法。首先,它替换了字符串,所以它实际上修改了

  3. ruby - 在 Ruby 中有条件地定义函数 - 2

    我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin

  4. ruby - 在 Ruby 中按名称传递函数 - 2

    如何在Ruby中按名称传递函数?(我使用Ruby才几个小时,所以我还在想办法。)nums=[1,2,3,4]#Thisworks,butismoreverbosethanI'dlikenums.eachdo|i|putsiend#InJS,Icouldjustdosomethinglike:#nums.forEach(console.log)#InF#,itwouldbesomethinglike:#List.iternums(printf"%A")#InRuby,IwishIcoulddosomethinglike:nums.eachputs在Ruby中能不能做到类似的简洁?我可以只

  5. STM32读取串口传感器数据(颗粒物传感器,主动上传) - 2

    文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,

  6. C51单片机——实现用独立按键控制LED亮灭(调用函数篇) - 2

    说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时

  7. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  8. ruby-on-rails - 将字符串转换为 ruby​​-on-rails 中的函数 - 2

    我需要一个通过输入字符串进行计算的方法,像这样function="(a/b)*100"a=25b=50function.something>>50有什么方法吗? 最佳答案 您可以使用instance_eval:function="(a/b)*100"a=25.0b=50instance_evalfunction#=>50.0请注意,使用eval本质上是不安全的,尤其是当您使用外部输入时,因为它可能包含注入(inject)的恶意代码。另请注意,a设置为25.0而不是25,因为如果它是整数a/b将导致0(整数)。

  9. ruby - 在 ruby​​ 中使用 .try 函数和 .map 函数 - 2

    我需要从json记录中获取一些值并像下面这样提取curr_json_doc['title']['genre'].map{|s|s['name']}.join(',')但对于某些记录,curr_json_doc['title']['genre']可以为空。所以我想对map和join()使用try函数。我试过如下curr_json_doc['title']['genre'].try(:map,{|s|s['name']}).try(:join,(','))但是没用。 最佳答案 你没有正确传递block。block被传递给参数括号外的方法

  10. ruby - 是否可以从也在该模块中的类内部调用模块函数 - 2

    在这段Ruby代码中:ModuleMClassC当我尝试运行时出现“'M:Module'的未定义方法'helper'”错误c=M::C.new("world")c.work但直接从另一个类调用M::helper("world")工作正常。类不能调用在定义它们的同一模块中定义的模块函数吗?除了将类移出模块外,还有其他解决方法吗? 最佳答案 为了调用M::helper,你需要将它定义为defself.helper;结束为了进行比较,请查看以下修改后的代码段中的helper和helper2moduleMclassC

随机推荐