Diving into the STM32 HAL-----I²C笔记
如今,除了主 MCU 之外,即使是最简单的 PCB 也包含两个或多个数字集成电路 (IC),专门用于特定任务。ADC 和 DAC、EEPROM 存储器、传感器、逻辑 I/O 端口、RTC 时钟、射频电路和专用 LCD 控制器只是专门用于执行单个任务的一小部分可能的 IC。现代数字电子设计就是正确选择(和编程)功能强大、特定且大多数时候价格低廉的 IC,以混合到最终的 PCB 上。
根据这些 IC 的特性,它们通常设计为根据明确定义的通信协议与可编程设备(通常但不限于微控制器)交换消息和数据。板内通信最常用的两种协议是 I²C 和 SPI,它们都可以追溯到 80 年代初,但在电子行业中仍然很普遍,尤其是当通信速度不是严格要求且仅限于 PCB 边界时。尽管存在使用 I²C 和 SPI 协议通过外部电线(通常长度约为米长)交换消息的应用,但这些规范并不能保证在潜在噪声介质上通信的稳健性。因此,它们的应用仅限于单个 PCB。
几乎所有的STM32微控制器都提供专用的硬件外设,能够使用I²C和SPI协议进行通信。本章简要介绍了 I²C 协议和相关的 CubeHAL API 来对此外设进行编程。如果对深化 I²C 协议感兴趣,NXP 的 UM10204 提供了完整和最新的规范。https://bit.ly/3E18iPF
1、I²C 规范简介
内部集成电路(又名 I²C - 发音为 I-squared-C 或很少使用 I-two-C)是飞利浦半导体部门(现为 NXP Semiconductors)于 1982 年开发的硬件规范和协议。它是一种多从机、半双工、单端 8 位定向串行总线规范,仅使用两根线将给定数量的从设备与主机互连。在 2006 年 10 月之前,基于 I²C 的器件的开发需要向 Philips 支付专利费,但这一限制已被取代。I²C 也可以是多主协议,这意味着同一总线上可以存在两个或多个主控,但一次只有一个主控可以接管总线控制,并且由主控来仲裁对总线的访问。在实践中,在嵌入式系统中以多主站模式使用 I²C 的情况很少见。
见上图,构成 I²C 总线的两根线是双向开漏线,分别称为串行数据线 (SDA) 和串行时钟线 (SCL)。I²C 协议规定这两条线路需要用电阻器上拉。这些电阻器的尺寸与总线电容和传输速度直接相关。https://bit.ly/29URjoy 这份文档提供了计算电阻值所需的数学运算。但是,使用接近 4.7KΩ 的电阻器是很常见的。
现代微控制器,如 STM32 微控制器,允许将 GPIO 线路配置为开漏上拉,从而启用内部上拉电阻器。可以使用内部上拉来拉 I²C 线,避免使用专用电阻器是很常见的。然而,在所有 STM32 器件中,内部上拉电阻器的值都接近(甚至高于)20KΩ,以避免不必要的功率泄漏。这样的值会增加总线达到 HIGH 状态所需的时间,从而降低传输速度。如果速度对应用不重要,并且(非常重要)在 MCU 和 IC 之间没有使用长走线(小于 2cm),那么对于许多应用使用内部上拉电阻器是可以的。但是,如果您在 PCB 上有足够的空间来放置几个电阻器,那么强烈建议使用外部和专用的。
STM32F1 微控制器不提供上拉 SDA 和 SCL 线路的能力。它们的 GPIO 必须配置为漏极开路,并且需要两个外部电阻器来上拉 I²C 线路。
由于 I²C 是一种仅基于两条线的协议,因此应该有一种方法可以寻址同一总线上的单个从器件。因此,I²C 定义每个从器件都提供唯一的从器件地址。地址可以是 7 位或 10 位宽(此选项非常罕见)。
即使能够与自定义速度通信的芯片并不少见,I²C 总线速度由协议规范明确定义。常见的 I²C 总线速度是 100kHz(也称为标准模式)和 400kHz(称为快速模式)。该标准的最新修订版可以以更快的速度运行(1MHz,称为快速模式,3.4MHz,称为高速模式,以及 5MHz,称为超快速模式)。
I²C 协议是一种非常简单的协议,因此 MCU 可以在不提供专用 I²C 外设的情况下“模拟”专用 I²C 外设:这种技术称为bit-banging,通常用于低成本 8 位架构,有时不提供专用的 I²C 接口来减少引脚数和/或 IC 成本。
1.1、I²C 协议
在 I²C 协议中,所有传输总是由主机发起和完成。这是在编程(尤其是调试)I²C 器件时要牢记的通信协议中为数不多的规则之一。通过 I²C 总线交换的所有消息都分为两种类型的帧:地址帧(主机指示消息发送到哪个从机),以及一个或多个数据帧,它们是从主机传递到从机的 8 位数据消息,反之亦然。数据在 SCL 变为低电平后放置在 SDA 线上,在 SCL 线变为高电平后进行采样。时钟沿和数据读/写之间的时间由总线上的器件定义,并且因芯片而异。
如前所述,SDA 和 SCL 都是双向线路,通过电流源或上拉电阻器连接到正电源电压。当总线空闲时,两条线路都是 HIGH。连接到总线的器件的输出级必须具有漏极开路或集电极开路才能执行线与功能。总线电容限制了连接到总线的接口数量。如果总线上没有时钟延展器件,对于单个主机应用,则主机的 SCL 输出可以是推挽式驱动器设计。
1.1.1、START 和 STOP
所有传输都以 START 开头,并以 STOP 终止(见下图)。
当 SCL 为高电平时,SDA 线上的 HIGH 到 LOW 转换定义了 START 条件。当 SCL 为高电平时,SDA 线路上的低电平到高电平转换定义了 STOP 条件。
START 和 STOP 条件始终由主机生成。在 START 条件之后,该总线被视为繁忙。在 STOP 条件之后的某个时间,该总线被视为再次空闲。如果生成重复的 START(也称为 RESTART 条件)而不是 STOP 条件,则总线将保持繁忙状态。在这种情况下,START 和 RESTART 条件在功能上是相同的。
1.1.2、字节格式
SDA 线路上传输的每个字都必须有 8 位长,这也包括稍后将看到的地址帧。每次传输可以传输的字节数不受限制。每个字节后面必须跟一个确认 (ACK) 位。首先传输最高有效位 (MSB) 传输数据。如果一个从机在执行其他功能(例如为内部中断提供服务)之前无法接收或发送另一个完整的数据字节,它可以保持时钟线 SCL 低电平以强制主机进入等待状态。当从机准备好接收另一个字节的数据并释放时钟线 SCL 时,数据传输将继续。
1.1.3、地址帧
地址帧在任何新的通信序列中始终是第一个。对于 7 位地址,地址首先被时钟输出最高有效位 (MSB),然后是一个 R/W 位,指示这是读 (1) 还是写 (0) 操作。
在 10 位寻址系统中(见上图),需要两个帧来传输从地址。第一帧将由代码 1111 0XXD 组成,其中 XX 是 10 位从地址的两个 MSB 位,D 是如上所述的 R/W 位。第一帧 ACK 位将由与地址的前两位匹配的所有从站响应。与正常的 7 位传输一样,另一个传输立即开始,此传输包含地址的位 [7:0]。此时,被寻址的从机应使用 ACK 位进行响应。如果没有,则故障模式与 7 位系统相同。
请注意,10 位地址设备可以与 7 位地址设备共存,因为地址的前导 11110 部分不是任何有效 7 位地址的一部分。
1.1.4、确认 (ACK) 和不确认 (NACK)
ACK 在每个字节之后发生。ACK 位允许接收器向发送器发出信号,该字节已成功接收,并且可以发送另一个字节。主机通过 SCL 线生成所有时钟脉冲,包括 ACK 第九个 clock pulse。(请注意,这里我们谈论的是接收器和发射器,因为 ACK/NACK 位都可以由主机和从机设置。)
ACK 信号定义如下:发送器在确认时钟脉冲期间释放 SDA 线,以便接收器可以将 SDA 线拉低,并且接收器在此时钟脉冲的 HIGH 期间它保持稳定的 LOW 。当 SDA 在第 9 个时钟脉冲期间保持高电平时,这被定义为 Not Acknowledge (NACK) 信号。然后,主机可以生成 STOP 条件来中止传输,或者生成 RESTART 条件来启动新的传输。产生 NACK 有五个情况:
1. 总线上没有具有传输地址的接收器,因此没有设备以确认进行响应。
2. 接收器无法接收或发送,因为它正在执行一些实时功能,并且没有准备好开始与主机通信。
3. 在传输过程中,接收方会获得它无法理解的数据或命令。
4. 在传输过程中,接收方无法再接收任何数据字节。
5. 主机接收器必须向从机发送器发出传输结束的信号。
ACK/NACK 位的有效性是由于 I²C 协议的开漏性质。开漏意味着参与传输的主机和从机都可以将相应的信号线拉低,但不能将其驱动为高。如果发射器和接收器之间的一个释放一条线路,如果另一个不将其拉低,则相应的电阻器会自动将其拉高。I²C 协议的漏极开路特性还确保在一个器件试图将线路驱动为高电平而另一个器件试图将其拉低时,不会发生总线争用,从而消除了损坏驱动器或系统中功耗过大的可能性。
1.1.5、数据帧
发送地址帧后,可以开始传输数据。主机将简单地以固定的间隔继续在 SCL 上生成时钟脉冲,数据将由主机或从机放置在 SDA 上,具体取决于 R/W 位是指示读取操作还是写入操作。通常,前两个字节包含要写入/读取的从寄存器的地址。例如,对于 I²C EEPROM,地址帧后面的前两个字节表示传输中涉及的存储器位置的地址。
根据 R/W 位,连续的字节由主机(如果 R/W 位设置为 1)或从机(如果 R/W 位为 0)填充。数据帧的数量是任意的,大多数从设备会自动递增内部寄存器地址,这意味着后续的读或写将来自下一个寄存器地址。这种模式也称为顺序模式或突发模式(见下图),它是一种加快传输速度的方法。
1.1.6、组合传输
I²C 协议基本上是一种简单的通信模式:
• 主机在总线上发送从机的地址;
• R/W 位,即地址字节中的 LSB 位,确定数据流的方向(主机到从机为W,从机到主机为R)
• 根据传输方向,两个对等体中的一个发送多个字节,每个字节都与一个 ACK 位交错,直到出现 STOP 条件。
这种通信模式有很大的陷阱: 如果我们想询问特定从机,我们需要使用两个单独的传输。考虑这个例子。假设我们有一个 I²C EEPROM。通常,这类设备有几个可寻址的存储器位置(64Kbits EEPROM 在 0~0x1FFF范围内可寻址,该值来自 64Kbits 等于 65536 位的事实,但每个内存位置都是 8 位宽,因此 65536/8 = 8192 = 0x2000。由于内存位置从 0 开始,因此最后一个位置具有 0x1FFF 地址。)。要检索内存位置的内容,主设备应执行以下步骤:
• 通过在 I²C 总线上发送从机地址,以写入模式(从机地址的最后一位设置为 0)启动传输,以便 EEPROM 开始对总线消息进行采样;
• 发送两个字节,代表我们想要读取的内存位置;
• 通过发送 STOP 条件结束事务;
• 通过在 I²C 总线上发送从机地址,在读取模式下启动新传输(从机地址的最后一位设置为 1);
• 读取从机发送的 n 字节(如果以随机模式读取内存,通常为 1 个,如果以顺序模式读取内存,则多于 1 个),然后以 STOP 条件结束传输。
为了支持这种通用的通信模式,I²C 协议定义了组合传输,其中数据流的方向在传输了一定数量的字节后反转(通常从从机到主机,反之亦然)。
上图示意图化了这种与从机通信的方式。主机开始以写入模式发送从机地址(注意图中红色粗体的 W),然后发送想要读取的 registers 的地址。然后发送新的 START 条件,而不终止传输:这个额外的 START 条件也称为重复 START 条件(或 RESTART)。主机再次发送从机地址,但这次传输以读取模式启动(注意图中粗体的 R)。从机现在传输想要读取的 registers 的内容,而主机确认发送的每个字节。主机通过发出 NACK 和 STOP 条件来结束传输。
1.1.7、时钟延展
有时,主机数据速率会超过从机提供该数据的能力。发生这种情况是因为数据尚未准备好(例如,从机尚未完成A/D转换),或者因为之前的操作尚未完成(例如,EEPROM 尚未完成对非易失性存储器的写入,需要先完成写入才能为其他请求提供服务)。
在这种情况下,一些从机将执行所谓的时间延展 clock stretching。在 clock stretching 中,从机通过将 SCL 保持为低电平来暂停传输。在 SCL 再次释放 HIGH 之前,传输无法继续。时钟延展是可选的,大多数从机不包括 SCL 驱动器,因此它们无法扩展时钟(主要是为了简化 I²C 接口的硬件布局)。稍后将发现,配置为 I²C 从模式的 STM32 MCU 可以选择实现时钟延展模式。
1.2、STM32 MCU中I²C外设的可用性
根据所使用的系列类型和封装,STM32微控制器可以提供多达四个独立的I²C外设。
2、HAL_I2C 模块
为了对 I²C 外设进行编程,CubeHAL 定义了 C 结构体I2C_HandleTypeDef,其定义方式如下:
typedef struct {I2C_TypeDef *Instance; /* I²C registers base address */I2C_InitTypeDef Init; /* I²C communication parameters */uint8_t *pBuffPtr; /* Pointer to I²C transfer buffer */uint16_t XferSize; /* I²C transfer size */__IO uint16_t XferCount; /* I²C transfer counter */DMA_HandleTypeDef *hdmatx; /* I²C Tx DMA handle parameters */DMA_HandleTypeDef *hdmarx; /* I²C Rx DMA handle parameters */HAL_LockTypeDef Lock; /* I²C locking object */__IO HAL_I2C_StateTypeDef State; /* I²C communication state */__IO HAL_I2C_ModeTypeDef Mode; /* I²C communication mode */__IO uint32_t ErrorCode; /* I²C Error code */
} I2C_HandleTypeDef;
• Instance:是指向我们将要使用的 I²C 描述符的指针。例如,I2C1 是第一个 I²C 外设的描述符。
• Init:是用于配置外围设备的 C 结构I2C_InitTypeDef实例。
• pBuffPtr:指向内部缓冲区的指针,用于临时存储传入和传出 I²C 外设的数据。当 I²C 工作在中断模式时,使用此方法。此时不应从用户代码中修改。
• hdmatx, hdmarx:指向 I²C 外设在 DMA 模式下工作时使用的 DMA_HandleTypeDef 结构实例的指针。
I²C 外设的设置是使用 C 结构体I2C_InitTypeDef实例来执行的,其定义方式如下:
typedef struct {uint32_t ClockSpeed; /* Specifies the clock frequency */uint32_t DutyCycle; /* Specifies the I²C fast mode duty cycle. */uint32_t OwnAddress1; /* Specifies the first device own address. */uint32_t OwnAddress2; /* Specifies the second device own address if dual addressing mode is selected */uint32_t AddressingMode; /* Specifies if 7-bit or 10-bit addressing mode is selected. */uint32_t DualAddressMode; /* Specifies if dual addressing mode is selected. */uint32_t GeneralCallMode; /* Specifies if general call mode is selected. */uint32_t NoStretchMode; /* Specifies if nostretch mode is selected. */
} I2C_InitTypeDef;
• ClockSpeed:此字段指定 I²C 接口的速度,它应对应于 I²C 规范中定义的总线速度(标准模式、快速模式等)。但是,该字段的精确值也是 DutyCycle 的函数。对于支持快速模式的 STM32 MCU,此字段的最大值为 400000 (400kHz)。较新的STM32系列还支持快速模式加(1MHz)。在这些其他 MCU 中, ClockSpeed 字段被另一个称为 Timing 的字段替换。Timing 字段的 configuration 值计算方式不同。https://bit.ly/2bxBoP1解释了如何根据所需的 I²C 总线速度计算该字段的确切值。但是,CubeMX 能够为您生成正确的配置值。
• DutyCycle:此字段仅在不支持快速模式的 MCU 中可用,指定 I²C SCL 线路的 tLOW 和 tHIGH 之间的比率。它可以假设值 I2C_DUTYCYCLE_2 和 I2C_DUTYCYCLE_16_9 来表示占空比等于 2:1 和 16:9。通过选择给定的时钟占空比,我们可以预分频外设时钟以实现所需的 I²C时钟速度。
为了更好地理解这个配置参数的作用,我们需要回顾一下 I²C 总线的一些基本概念。在前面,我们已经看到占空比是信号处于活动状态的时间段(例如 10μs)的百分比。对于每个 I²C 总线速度,I²C 规范精确定义了最小 tLOW 和 tHIGH 值。
上表显示了给定通信速度的 tLOW 和 tHIGH 值(以黄色突出显示)。这两个值的比率是占空比,它与通信速度无关。例如,100kHz的周期相当于10μs,但表中的tHIGH +tLOW小于10μs(4μs+4.7μs=8.7μs)。因此,只要满足 tLOW 和 tHIGH 最小时序 (分别为 4.7μs 和 4μs),实际值的比率就会发生变化。这些比率的重点是说明 I²C 模式之间的 I²C 时序约束不同。它们不是 STM32 I²C 外设需要保持的强制性比率。例如,tHIGH = 4s 和 tLOW = 6s 的比率为 0.67,它仍然与标准模式 (100kHz) 的时序兼容(因为 tHIGH = 4s 和 tLOW > 4.7s,它们的和等于 10μs)。
STM32 MCU中的I²C外设定义了以下占空比(比率)。对于标准模式,比率固定为 1:1。这意味着 tLOW = tHIGH = 5s。对于快速模式,我们可以使用两个比率:2:1 或 16:9。2:1 比率意味着 tLOW = 2.66s 和 tHIGH = 1.33s 时获得 4μs (=400kHz),这两个值都高于表中报告的值(0.6μs 和 1.3μs)。16:9的比率意味着tLOW = 2.56s和tHIGH = 1.44s时获得4μs,并且这两个值仍然高于表中报告的值。何时使用 2:1 的比率而不是 16:9 的比率,反之亦然?它取决于外设时钟 (PCLK1) 频率。2:1 的比率意味着 400MHz 是通过将时钟源除以 3 (1+2) 来实现的。这意味着 PCLK1 必须是 1.2MHz (400kHz * 3) 的倍数。使用 16:9 的比率意味着我们将 PCLK1 除以 25。这意味着当 PCLK1 是 10MHz (400kHz * 25) 的倍数时,我们可以获得最大 I²C 总线频率。因此,正确选择占空比取决于 APB1 总线的有效速度和所需的(最大)I²C SCL 频率。需要强调的是,即使 SCL 频率低于 400kHz(例如,使用等于 16:9 的比率,而 PCLK1 频率为 8MHz,我们可以达到等于 320kHz 的最大通信速度),我们仍然满足 I²C 快速模式规范的要求(400kHz 是上限)。
• OwnAddress1、OwnAddress2:STM32 MCU中的I²C外设可用于开发主I²C和从I²C设备。在开发 I²C 从设备时,OwnAddress1 字段允许指定 I²C 从地址:外设自动检测 I²C 总线上的给定地址,并自动触发所有相关事件(例如,它可以生成相应的中断,以便固件代码可以在总线上启动新的传输)。I²C 外设支持 7 位或 10 位寻址,以及 7 位双寻址模式:在这种情况下,我们可以指定两个不同的 7 位从地址,以便设备能够响应发送到两个地址的请求。
• AddressingMode:该字段可以假设值 I2C_ADDRESSINGMODE_7BIT 或 I2C_ADDRESSINGMODE_10BIT 分别指定 7 位或 10 位寻址模式。
• DualAddressMode:此字段可以假定值 I2C_DUALADDRESS_ENABLE 或 I2C_DUALADDRESS_DISABLE 以启用/禁用 7 位双寻址模式。
• GeneralCallMode:General Call 是 I²C 协议中的一种广播寻址。特殊的 I²C 从机地址 0x0000 000 用于向同一总线上的所有设备发送消息。General Call是一项可选功能,通过将该字段设置为 I2C_GENERALCALL_ENABLE 值,I²C 外设将在General Call地址匹配时生成事件。
• NoStretchMode:此字段可以假定值 I2C_NOSTRETCH_ENABLE 或 I2C_NOSTRETCH_DISABLE 用于禁用/启用可选的时钟延展模式(请注意,将其设置为 I2C_NOSTRETCH_ENABLE 将禁用时钟扩展模式)。
像往常一样,要配置 I²C 外设,我们使用函数:
HAL_StatusTypeDef HAL_I2C_Init(I2C_HandleTypeDef *hi2c);
它接受指向之前看到的I2C_HandleTypeDef实例的指针。
2.1、在主模式下使用 I²C 外设
我们现在将分析 CubeHAL 提供的主要例程,以便在主模式下使用 I²C 外设。为了在写入模式下通过 I²C 总线执行传输,CubeHAL 提供了以下函数:
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
其中:
• Hi2C:指向之前看到的结构I2C_HandleTypeDef实例的指针,用于标识 I²C 外设;
• DevAddress:从机的地址,根据具体的 IC,可以是 7 位或 10 位;
• pData:指向数组的指针,其长度等于 Size 参数,包含我们将要传输的字节序列;
• Timeout:表示我们愿意等待传输完成的最长时间(以毫秒表示)。如果传输未在指定的超时时间内完成,则函数将中止并返回 HAL_TIMEOUT 值;否则,如果没有发生其他错误,它将返回 HAL_OK 值。此外,我们可以传递一个等于 HAL_MAX_DELAY (0xFFFF FFFF) 的超时,以无限期地等待传输完成。
要在读取模式下执行事务,我们可以改用以下函数:
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
前面的两个函数都在轮询模式下执行传输。对于基于中断的传输,我们可以使用以下函数:
HAL_StatusTypeDef HAL_I2C_Master_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_I2C_Master_Receive_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size);
这些函数的工作方式与前面章节中看到的其他例程相同(例如,那些与中断模式下的 UART 传输相关的例程)。为了正确使用它们,我们需要启用相应的 ISR 并调用 HAL_I2C_EV_IRQHandler() 例程,该例程反过来调用 HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) 以写入模式发出传输完成的信号,或者调用 HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)以读取模式下发出传输结束的信号。
除 STM32F0 和 STM32L0 系列外,所有 STM32 MCU 中的 I²C 外设都使用单独的中断来发出错误条件信号(查看与 MCU 相关的矢量表)。因此,在相应的 ISR 中,我们需要调用 HAL_I2C_ER_IRQHandler(),如果出现错误,它又会调用 HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)。
CubeHAL 调用了 10 种不同的回调。下表列出了所有这些,以及调用回调的 ISR。
最后,函数:
HAL_StatusTypeDef HAL_I2C_Master_Transmit_DMA(I2C_HandleTypeDef *hi2c,uint16_t DevAddress, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_I2C_Master_Receive_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size);
允许使用 DMA 执行 I²C 传输。这里的工作示例,我们需要一个能够通过 I²C 总线进行交互的外部设备。因此,我们将使用外部 EEPROM 存储器:24LCxx。这是一个流行的串行 EEPROM 系列,已成为电子行业的一种标准。它们很便宜(成本通常几十美分),它们以多种封装形式生产(从“旧”的THT PDIP 封装到现代和紧凑的 WLCP 封装),它们提供超过 200 年的数据保留,单个页面可以擦除超过 100 万次。此外,许多芯片制造商都有自己的兼容版本(ST 还提供自己的 24LCxx 兼容 EEPROM)。这些记忆芯片与 555 计时器一样受欢迎,我敢打赌它们会存活很多年来进行技术创新。
我们的示例将基于 24LC64 模型,它是一个 64Kbits EEPROM(这意味着内存能够存储 8KB,或者可以存储 8192 字节)。PDIP-8 版本的引脚排列如下图所示。
A0、A1 和 A2 用于设置 I²C 地址的 LSB 位,如图 14.7 所示:
如果其中一个引脚接地,则相应的位设置为 0;如果与 VDD 相连,则该位设置为 1。如果所有三个引脚都接地,则 I²C 地址对应于 0xA0。WP pin 是写保护引脚:如果接地,我们可以在单个存储单元内写入。相反,如果连接到 VDD,则写入操作没有影响。
STM32F1 微控制器不提供上拉 SDA 和 SCL 线路的能力。它们的 GPIO 必须配置为开漏。因此,必须添加两个额外的电阻器来上拉 I²C 线。4K 到 10K 之间的值是经过验证的值。
如前所述,一个 64Kbits 的 EEPROM 有 8192 个地址,从 0x0000 个到 0x1FFF 个不等。通过 I²C 总线发送 EEPROM 地址、内存地址的上半部分和下半部分以及要存储在该单元中的值来执行单个字节写入,最后以 STOP 条件关闭传输。
假设我们想将值 0x4C 存储在EEPROM 0x320 中,那么下图显示了正确的传输序列。
地址 0x320 分为两部分:首先发送等于 0x3 的上半部分,然后发送等于 0x20 的下半部分。然后发送要存储的数据。我们还可以在同一次传输中发送多个字节:内部地址计数器在发送的每个字节时自动递增。这使我们能够减少传输时间并提高总吞吐量。
I²C EEPROM 在最后一个发送字节之后设置的 ACK 位并不意味着数据已有效地存储在存储器中。发送的数据存储在临时缓冲区中,因为 EEPROM 位置存储器是逐页擦除的,而不是单独擦除的。每次写入操作时都会刷新整个页面(由 32 个字节组成),并且传输的字节仅在此操作结束时存储。在擦除时间内,发送到 EEPROM 的每条命令都将忽略。要检测写入操作何时完成,我们需要使用确认轮询。这涉及主设备发送一个 START 条件,后跟从机地址加上写入命令的控制字节(R/W 位设置为 0)。如果设备仍然忙于写入周期,则不会返回 ACK。如果未返回 ACK,则必须重新发送 START 位和控制字节。如果写入周期完成,设备将返回 ACK,然后主机可以继续执行下一个读或写命令。
读取操作的启动方式与写入操作相同,但控制字节的 R/W 位设置为 1。读取操作有三种基本类型:当前地址读取、随机读取和顺序读取。这里主要介绍随机读取模式。随机读取操作允许主机以随机方式访问任何内存位置。要执行这种类型的读取操作,必须先发送内存地址。这是通过在写入操作中将内存地址发送到 24LCxx 来实现的(R/W 位设置为“0”)。发送内存地址后,主机会在 ACK 之后生成一个 RESTART 条件(重复 START)。(24LCxx EEPROM 存储器的设计使其以相同的方式工作,即使我们通过发出 STOP 条件结束传输,然后我们立即在读取模式下启动新的参数。) 这将终止写入操作,但在设置内部地址计数器之前不会终止。然后,主设备再次发出从机地址,但这次 R/W 位设置为 1。然后 24LCxx 将发出 ACK 并发送 8 位数据字。主机将不确认传输并生成 STOP 条件,这会导致 EEPROM 停止传输(见下图)。
在随机读取命令之后,内部地址计数器将指向刚刚读取的地址位置之后的地址位置。
我们将创建两个简单的函数,分别命名为 Read_From_24LCxx() 和 Write_To_24LCxx(),它们允许使用 CubeHAL 从 24LCxx 内存中写入/读取数据。然后,我们将通过在 EEPROM 中存储一个字符串,然后将其读回来测试这些例程:如果原始字符串等于从 EEPROM 读取的字符串,则 LED 开始闪烁。
int main(void) {const char wmsg[] = "We love STM32!";char rmsg[20] = {0};HAL_Init();BSP_Init();MX_I2C1_Init();Write_To_24LCxx(&hi2c1, 0xA0, 0x1AAA, (uint8_t*)wmsg, strlen(wmsg)+1);Read_From_24LCxx(&hi2c1, 0xA0, 0x1AAA, (uint8_t*)rmsg, strlen(wmsg)+1);if(strcmp(wmsg, rmsg) == 0) {while(1) {HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);HAL_Delay(100);}}while(1);
}/* I2C1 init function */
static void MX_I2C1_Init(void) {GPIO_InitTypeDef GPIO_InitStruct;/* Peripheral clock enable */__HAL_RCC_I2C1_CLK_ENABLE();hi2c1.Instance = I2C1;hi2c1.Init.ClockSpeed = 100000;hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;hi2c1.Init.OwnAddress1 = 0x0;hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;hi2c1.Init.OwnAddress2 = 0;hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;GPIO_InitStruct.Pull = GPIO_PULLUP;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);HAL_I2C_Init(&hi2c1);
}
我们来分析一下从 MX_I2C1_Init() 例程开始的上述代码片段。它先启用 I2C1 外设时钟,以便我们可以对其 registers 进行编程。然后我们设置总线速度(在本例中为 100kHz - 在本例中忽略占空比设置,因为当总线以低于或等于 100kHz 的速度运行时,占空比固定为 1:1)。然后,我们配置 PB8 和 PB9 引脚,使它们分别充当 SCL 和 SDA 线路。
main() 例程很简单:它将字符串 “We love STM32!” (注意:长度不要超过一页的字节数)存储在0x1AAA内存位置;然后从 EEPROM 中读回该字符串,并与原始字符串进行比较。我们需要解释为什么我们要存储和读取长度等于 strlen(wmsg)+1 的缓冲区。这是因为 C strlen() 例程会返回跳过字符串终止符 char ('0') 的字符串长度。如果不存储此字符,然后从 EEPROM 读回它,strcmp() 将无法计算字符串的确切长度。
HAL_StatusTypeDef Read_From_24LCxx(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint8_t *pData, uint16_t len) {HAL_StatusTypeDef returnValue;uint8_t addr[2];/* We compute the MSB and LSB parts of the memory address */addr[0] = (uint8_t) ((MemAddress & 0xFF00) >> 8);addr[1] = (uint8_t) (MemAddress & 0xFF);/* First we send the memory location address where start reading data */returnValue = HAL_I2C_Master_Transmit(hi2c, DevAddress, addr, 2, HAL_MAX_DELAY);if(returnValue != HAL_OK)return returnValue;/* Next we can retrieve the data from EEPROM */returnValue = HAL_I2C_Master_Receive(hi2c, DevAddress, pData, len, HAL_MAX_DELAY);return returnValue;
}HAL_StatusTypeDef Write_To_24LCxx(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint8_t *pData, uint16_t len) {HAL_StatusTypeDef returnValue;uint8_t *data;/* First we allocate a temporary buffer to store the destination memory address and the data to store */data = (uint8_t*)malloc(sizeof(uint8_t)*(len+2));/* We compute the MSB and LSB parts of the memory address */data[0] = (uint8_t) ((MemAddress & 0xFF00) >> 8);data[1] = (uint8_t) (MemAddress & 0xFF);/* And copy the content of the pData array in the temporary buffer */memcpy(data+2, pData, len);/* We are now ready to transfer the buffer over the I2C bus */returnValue = HAL_I2C_Master_Transmit(hi2c, DevAddress, data, len + 2, HAL_MAX_DELAY);if(returnValue != HAL_OK)return returnValue;free(data);/* We wait until the EEPROM effectively stores data in memory */while(HAL_I2C_Master_Transmit(hi2c, DevAddress, 0, 0, HAL_MAX_DELAY) != HAL_OK);return HAL_OK;
}
现在,我们可以将注意力集中在使用 24LCxx EEPROM 的两个例程上。它们的参数:
• EEPROM 存储器的 I²C 从机地址 (DevAddress);
• 开始存储/读取数据的内存地址 (MemAddress);
• 指向用于与 EEPROM 交换数据的内存缓冲区的指针 (pData);
• 要存储/读取的数据量 (LEN);
Read_From_24LCxx() 函数开始计算内存地址的两半 (MSB 和 LSB 部分)。然后,它使用 HAL_I2C_Master_Transmit() 例程通过 I²C 总线发送这两个部分。如前所述,24LCxx 内存的设计使其将内部地址计数器设置为传递来的地址。因此,我们可以在读取模式下启动一个新传输,以从 EEPROM 检索数据量。
Write_To_24LCxx() 函数执行类似操作,但方式不同。它必须遵守上面描述的 24LCxx 协议,这意味着不能对内存地址和要存储的数据使用两个单独的传输,我们必须执行唯一的 I²C 传输。因此,我们使用一个临时的动态缓冲区,它包含内存地址的两半以及要存储在 EEPROM 中的数据。因此,我们可以通过 I²C 总线执行传输处理,然后等待 EEPROM 完成内存传输。
如果使用AT24C02实验,则代码为:
#include "main.h"
#include "i2c.h"
#include "usart.h"
#include "gpio.h"
#include "string.h"
#include "stdlib.h"void SystemClock_Config(void);
HAL_StatusTypeDef Read_From_AT24C02(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint8_t *pData, uint16_t len);
HAL_StatusTypeDef Write_To_AT24C02(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint8_t *pData, uint16_t len);int main(void)
{char wmsg[] = "1234567";//"123456789";不行,本实验不能超过8个字节char rmsg[20] = {0};HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USART1_UART_Init();MX_I2C1_Init();Write_To_AT24C02(&hi2c1, 0xA0, 0, (uint8_t*)wmsg, strlen(wmsg)+1);Read_From_AT24C02(&hi2c1, 0xA0, 0, (uint8_t*)rmsg, strlen(wmsg)+1);if(strcmp(wmsg, rmsg) == 0) {while(1) {HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);HAL_Delay(100);}}while (1){}}
HAL_StatusTypeDef Read_From_AT24C02(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint8_t *pData, uint16_t len) {HAL_StatusTypeDef returnValue;uint8_t addr;addr = (uint8_t) (MemAddress & 0xFF);returnValue = HAL_I2C_Master_Transmit(hi2c, DevAddress, &addr, 1, HAL_MAX_DELAY);if(returnValue != HAL_OK)return returnValue;returnValue = HAL_I2C_Master_Receive(hi2c, DevAddress, pData, len, HAL_MAX_DELAY);return returnValue;
}// 这个代码也能工作
/* HAL_StatusTypeDef Write_To_AT24C02(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint8_t *pData, uint16_t len) {HAL_StatusTypeDef returnValue;uint8_t addr;addr = (uint8_t) (MemAddress & 0xFF);returnValue = HAL_I2C_Master_Transmit(hi2c, DevAddress, &addr, 1, HAL_MAX_DELAY);if(returnValue != HAL_OK)return returnValue;returnValue = HAL_I2C_Master_Transmit(hi2c, DevAddress, pData, len, HAL_MAX_DELAY);if(returnValue != HAL_OK)return returnValue;while(HAL_I2C_Master_Transmit(hi2c, DevAddress, 0, 0, HAL_MAX_DELAY) != HAL_OK);return HAL_OK;
} */HAL_StatusTypeDef Write_To_AT24C02(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint8_t *pData, uint16_t len) {HAL_StatusTypeDef returnValue;uint8_t *data;data = (uint8_t*)malloc(sizeof(uint8_t)*(len+1));data[0] = (uint8_t) (MemAddress & 0xFF);memcpy(data+1, pData, len);returnValue = HAL_I2C_Master_Transmit(hi2c, DevAddress, data, len + 1, HAL_MAX_DELAY);if(returnValue != HAL_OK)return returnValue;free(data);while(HAL_I2C_Master_Transmit(hi2c, DevAddress, 0, 0, HAL_MAX_DELAY) != HAL_OK);return HAL_OK;
}
2.1.1、I/O MEM 操作
同所有具有可寻址、可读写的内存寄存器的 I²C 器件一样,24LCxx EEPROM 使用的协议确实是通用的。例如,许多 I²C 传感器,如 ST 的 HTS221 都采用相同的协议。出于这个原因,ST已经在 CubeHAL 中实现了特定的例程,它们可以更好、更快地完成 Read_From_24LCxx() 和 Write_To_24LCxx() 的相同工作。函数:
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);
HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
允许从内存可寻址 I²C 器件存储和检索数据,只有一个显著的区别: HAL_I2C_Mem_Write() 函数不是等待写入周期完成的,就像我们在前面所做的那样。但是,即使对于此操作,HAL 也提供了一个专用且更可移植的例程:
HAL_StatusTypeDef HAL_I2C_IsDeviceReady(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint32_t Trials, uint32_t Timeout);
此函数在返回错误条件之前接受最大数量的 Trial,但如果我们将 HAL_MAX_DELAY 作为 Timeout 值传递给函数,则可以将 1 传递给 Trials 参数。当轮询的 I²C 器件准备就绪时,该函数返回 HAL_OK。否则,它将返回 HAL_BUSY 值。
STM32 HAL 库中的 HAL_I2C_Master_Transmit 与 HAL_I2C_Mem_Write 区别详解_hal i2c master transmit-CSDN博客
因此,之前看到的 main() 函数可以按以下方式重新排列:
int main(void) {char wmsg[] ="We love STM32!";char rmsg[20];HAL_Init();BSP_Init();MX_I2C1_Init();HAL_I2C_Mem_Write(&hi2c1, 0xA0, 0x1AAA, I2C_MEMADD_SIZE_16BIT, (uint8_t*)wmsg, strlen(wmsg)+1, HAL_MAX_DELAY);while(HAL_I2C_IsDeviceReady(&hi2c1, 0xA0, 1, HAL_MAX_DELAY) != HAL_OK);HAL_I2C_Mem_Read(&hi2c1, 0xA0, 0x1AAA, I2C_MEMADD_SIZE_16BIT, (uint8_t*)rmsg, strlen(wmsg)+1, HAL_MAX_DELAY);if(strcmp(wmsg, rmsg) == 0) {while(1) {HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);HAL_Delay(100);}}while(1);
}
同理,如果使用AT24C02实验,则代码为:
int main(void) {char wmsg[] ="1234567";char rmsg[20];HAL_Init();BSP_Init();MX_I2C1_Init();HAL_I2C_Mem_Write(&hi2c1, 0xA0, 0, I2C_MEMADD_SIZE_8BIT, (uint8_t*)wmsg, strlen(wmsg)+1, HAL_MAX_DELAY);while(HAL_I2C_IsDeviceReady(&hi2c1, 0xA0, 1, HAL_MAX_DELAY) != HAL_OK);HAL_I2C_Mem_Read(&hi2c1, 0xA0, 0, I2C_MEMADD_SIZE_8BIT, (uint8_t*)rmsg, strlen(wmsg)+1, HAL_MAX_DELAY);if(strcmp(wmsg, rmsg) == 0) {while(1) {HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);HAL_Delay(100);}}while(1);
}
上述 API 工作在轮询模式下,但 CubeHAL 也提供了相应的例程来在中断和 DMA 模式下执行传输。需要注意的一点是:用于发出传输结束信号的回调函数是 HAL_I2C_MemTxCpltCallback() 和 HAL_I2C_MemRxCpltCallback()。
2.1.2、组合传输
24LCxx EEPROM 存储器中读取操作的传输序列是一个组合传输。在反转 I²C 传输方向(从写入到读取)之前,使用 RESTART 条件。在第一个例子中,我们能够在 Read_From_24LCxx() 中使用两个单独的传输,因为 24LCxx EEPROMs 被设计为以相同的方式工作。这要归功于内部地址计数器:第一个传输将地址计数器设置为所需的位置;第二个步骤在 read 模式下执行,从该位置开始从 EEPROM 检索数据。然而,这不仅降低了可能达到的最大吞吐量,而且更重要的是,通常会导致代码不可移植:存在多个 I²C 设备,它们严格遵守 I²C 协议,并使用 RESTART 条件根据规范实现组合传输(因此它们不能容忍中间的 STOP 条件)。
CubeHAL 提供了两个专用例程来处理组合传输,或者像在 Cube HAL 中调用的那样,顺序传输:
HAL_I2C_Master_Sequential_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size,uint32_t XferOptions);
HAL_I2C_Master_Sequential_Receive_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t XferOptions);
与前面看到的其他例程相比,此处唯一要强调的相关参数是 XferOptions。
它可以采用上表中报告的值之一,并用于在单个传输中驱动 START/RESTART/STOP 条件的生成。这两个函数都以这种方式工作。让我们假设我们想从 24LCxx EEPROM 读取 n 字节。根据 I²C 协议,我们必须执行以下操作:
1.我们必须以写入模式开始一个新的传输,发出 START 条件,然后是从机地址;
2. 然后我们传输两个包含从机内存地址的 MSB 和 LSB 部分的字节;
3. 因此,我们发出 RESTART 条件并发送从机地址,最后一位设置为 1 以指示读取传输;
4. 从机开始逐字节发送数据,直到我们通过发出 NACK 或 STOP 条件结束传输。
使用顺序传输例程,我们可以按以下方式进行:
1. 通过传递从机地址和构成内存位置地址的两个字节来调用 HAL_I2C_Master_Sequential_Transmit_IT() 例程;通过传递值 I2C_FIRST_FRAME 来调用函数,以便它在发送两个字节后生成 START 条件而不发出 STOP 条件;
2. 通过传递从机地址、指向用于存储读取字节的缓冲区的指针、要从 EEPROM 读取的字节数和值 I2C_LAST_FRAME来调用 HAL_I2C_Master_Sequential_Receive_IT() 例程,以便该函数生成 RESTART 条件,并通过发出 STOP 条件在传输结束时终止传输。
STM32 HAL I2C(IIC)通信中序列传输和重启条件详解-物联沃-IOTWORD物联网
2.1.3、关于 STM32F0/L0/L4 系列中 clock configuration 的说明
在 STM32F0/L0 系列中,可以为 I2C1 外设选择不同的时钟源。这是因为在这些系列中,I2C1 外设甚至能够在某些低功耗模式下工作,从而在 I²C 工作在从机模式并且配置的从机地址位于总线上时唤醒 MCU。请参阅 CubeMX 中的 Clock 视图以了解更多信息。在 STM32L4/5 MCU 中,可以为所有 I²C 外设选择时钟源。
2.2、从机模式下使用 I²C 外设
如今,市场上有很多板载系统 (SoB) 模块。这些通常是已经安装了多个 IC 并专门用于执行相关操作的小型 PCB。GPRS 和 GPS 模块或多传感器板是 SoB 模块的示例。然后,这些模块被焊接到主板上,这要归功于它们在侧面暴露出可焊接焊盘的事实,也称为“齿形通孔”或“齿形”。下图显示了 ST 的 STEVAL-STLCS01V1传感器开发套件模块,它是一个集成的可编程模块,带有一个STM32L476、三个高度集成的传感器(6 轴数字电子罗盘和一个 3 轴数字陀螺仪、一个气压计、一个麦克风)和一个低功耗蓝牙 (BLE) 网络处理器 BlueNRG-MS。
这些板上的 MCU 通常预编程有固件,专门用于执行已建立好的任务。主板还包含另一个可编程 IC,可能是另一个 MCU 或类似的东西。主板使用众所周知的通信协议与 SoB 交互,这些协议通常是 UART、CAN 总线、SPI 或 I²C 总线。因此,对 STM32 器件进行编程以使其在 I²C 从机模式下工作是很常见的。
CubeHAL 提供了轻松开发 I²C 从属应用程序所需的所有函数。从机程序与在主机模式下对 I²C 外设进行编程的程序相同。例如,当 I²C 外设用于从机模式时,以下例程用于在中断模式下发送/接收数据:
HAL_StatusTypeDef HAL_I2C_Slave_Transmit_IT(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_I2C_Slave_Receive_IT(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size);
同理,在数据传输/接收结束时调用的回调例程如下:
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c);
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c);
我们现在将分析一个完整的示例,该示例展示了如何使用 CubeHAL 开发 I²C 从属应用程序。我们将实现一种具有 I²C 接口的数字温度传感器,类似于市场上的大多数数字温度传感器(例如,TI 的流行 TMP275 和 ST 的 HT221)。这个“传感器”将仅提供三个寄存器:
• 一个WHO_AM_I寄存器,由主控代码用于检查 I²C 接口是否正常工作;此 register 返回 0xBC 的固定值。
• 两个与温度相关的寄存器,分别命名为 TEMP_OUT_INT 和 TEMP_OUT_FRAC,其中包含所采集温度的整数和小数部分;例如,如果检测到的温度等于 27.34°C,则 TEMP_OUT_INT 寄存器将包含值 27,TEMP_OUT_FRAC寄存器将包含值 34。
我们的传感器将被设计为响应一个非常简单的协议,该协议基于组合传输,如上图所示。当以随机读取模式访问内存时,与 24LCxx EEPROM 使用的协议的唯一显着区别是内存寄存器的大小,在本例中仅为一个字节。
该示例同时提供了 “slave” 和 “master” 实现: 在 project 级别定义的宏 SLAVE_BOARD 驱动这两个部分的编译。该示例需要两个板。不幸的是,当我开始设计这个例子时,我认为可以只使用一块板,将与 I²C 外设相关的引脚连接到另一个 I²C 外设的引脚(例如,直接连接到 I2C3 引脚的 I2C1 引脚)。但是,经过一番挣扎,我得出的结论是,STM32 中的 I²C 外设并不是“真正的异步”,不可能同时使用两个 I²C 外设。因此,要运行此示例,将需要两个板,或者只需要一个板子和另一个开发套件:在这种情况下,需要相应地重新排列主器件。
#ifdef SLAVE_BOARDstatic void MX_ADC1_Init(void);
#endif
static void MX_I2C1_Init(void);volatile uint8_t transferDirection, transferRequested;#define TEMP_OUT_INT_REGISTER 0x0
#define TEMP_OUT_FRAC_REGISTER 0x1
#define WHO_AM_I_REGISTER 0xF
#define WHO_AM_I_VALUE 0xBC
#define TRANSFER_DIR_WRITE 0x1
#define TRANSFER_DIR_READ 0x0
#define I2C_SLAVE_ADDR 0x33int main(void) {char uartBuf[30];uint8_t i2cBuf[2];float ftemp;int8_t t_frac, t_int;HAL_Init();BSP_Init();MX_I2C1_Init();#ifdef SLAVE_BOARDuint16_t rawValue;uint32_t lastConversion;MX_ADC1_Init();HAL_ADCEx_Calibration_Start(&hadc1);HAL_ADC_Start(&hadc1);while(1) {HAL_I2C_EnableListen_IT(&hi2c1);while(!transferRequested) {if(HAL_GetTick() - lastConversion > 1000L) {HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);rawValue = HAL_ADC_GetValue(&hadc1);ftemp = ((float)rawValue) / 4095 * 3300;ftemp = ((ftemp - 1430.0) / 4.3) + 25;t_int = ftemp;t_frac = (ftemp - t_int)*100;sprintf(uartBuf, "Temperature: %.2f\r\n", ftemp);HAL_UART_Transmit(&huart2, (uint8_t*)uartBuf, strlen(uartBuf), HAL_MAX_DELAY);sprintf(uartBuf, "t_int: %d - t_frac: %d\r\n", t_int, t_frac);HAL_UART_Transmit(&huart2, (uint8_t*)uartBuf, strlen(uartBuf), HAL_MAX_DELAY);lastConversion = HAL_GetTick();}}transferRequested = 0;if(transferDirection == TRANSFER_DIR_WRITE) {/* Master is sending register address */HAL_I2C_Slave_Sequential_Receive_IT(&hi2c1, i2cBuf, 1, I2C_FIRST_FRAME);while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_LISTEN);switch(i2cBuf[0]) {case WHO_AM_I_REGISTER:i2cBuf[0] = WHO_AM_I_VALUE;break;case TEMP_OUT_INT_REGISTER:i2cBuf[0] = t_int;break;case TEMP_OUT_FRAC_REGISTER:i2cBuf[0] = t_frac;break;default:i2cBuf[0] = 0xFF;}HAL_I2C_Slave_Sequential_Transmit_IT(&hi2c1, i2cBuf, 1, I2C_LAST_FRAME);while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);}}
main() 函数中HAL_I2C_EnableListen_IT() 例程使能所有与 I²C 外设相关的中断。这意味着当主机放置从机地址(由宏 I2C_SLAVE_ADDR 定义)时,将触发新的中断。HAL_I2C_EV_IRQHandler() 例程将自动调用 HAL_I2C_AddrCallback() 函数。
然后,main() 函数开始每秒执行一次内部温度传感器的 A/D 转换,并将获取的温度(存储在 ftemp 变量中)拆分为两个 8 位整数,t_int 和 t_frac:它们表示温度的整数和小数部分。一旦 transferRequested 变量等于 1,main 函数就会暂时停止 A/D 转换:这个全局变量由 HAL_I2C_AddrCallback() 函数(同时设置transferDirection),其中包含 I²C 的传输方向(读/写)。
如果主机以写模式启动新传输,则意味着它正在传输 register 地址。然后调用 HAL_I2C_Slave_Sequential_Receive_IT()函数:这将导致从主机接收寄存器地址。由于该函数在中断模式下工作,我们需要一种方法来等待传输完成。HAL_I2C_GetState() 返回 HAL 的内部状态,在传输完成之前,该状态等于 HAL_I2C_STATE_BUSY_RX_LISTEN。发生这种情况时,状态将返回 HAL_I2C_STATE_LISTEN,我们可以继续将所需寄存器的内容传输到主机。
继续调用函数 HAL_I2C_Slave_Sequential_Transmit_IT() : 该函数反转传输方向,并将所需寄存器的内容发送给主机。接着我们进行繁忙的自旋,直到 I²C 外设状态等于 HAL_I2C_STATE_READY。为什么我们不像接收时那样根据 HAL_I2C_STATE_LISTEN状态检查外设状态?要理解这方面,我们需要记住合并传输的重要一点。当传输反转到发送方向时, 主机开始确认发送的每个字节。请记住,只有主机知道传输持续多长时间,并且它决定何时停止传输。在组合传输中,主机通过发出 NACK 来结束从从机到主机的传输,这会导致从机发出 STOP 条件。从 I²C 外设的角度来看,STOP 条件会导致外设退出侦听模式(从技术上讲,它会产生一个中止条件 - 如果您实现 HAL_I2C_AbortCpltCallback() 回调,则可以跟踪何时发生这种情况),这就是为什么我们需要检查HAL_I2C_STATE_READY状态并再次将外设置于侦听模式的原因。
#else //Master boardi2cBuf[0] = WHO_AM_I_REGISTER;HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf, 1, I2C_FIRST_FRAME);while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf, 1, I2C_LAST_FRAME);while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);sprintf(uartBuf, "WHO AM I: %x\r\n", i2cBuf[0]);HAL_UART_Transmit(&huart2, (uint8_t*) uartBuf, strlen(uartBuf), HAL_MAX_DELAY);i2cBuf[0] = TEMP_OUT_INT_REGISTER;HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf, 1, I2C_FIRST_FRAME);while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, (uint8_t*)&t_int, 1, I2C_LAST_FRAME);while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);i2cBuf[0] = TEMP_OUT_FRAC_REGISTER;HAL_I2C_Master_Sequential_Transmit_IT(&hi2c1, I2C_SLAVE_ADDR, i2cBuf, 1, I2C_FIRST_FRAME);while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);HAL_I2C_Master_Sequential_Receive_IT(&hi2c1, I2C_SLAVE_ADDR, (uint8_t*)&t_frac, 1, I2C_LAST_FRAME);while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);ftemp = ((float)t_frac)/100.0;ftemp += (float)t_int;sprintf(uartBuf, "Temperature: %.2f\r\n", ftemp);HAL_UART_Transmit(&huart2, (uint8_t*) uartBuf, strlen(uartBuf), HAL_MAX_DELAY);#endifwhile (1);
}
最后,需要强调的是,“从属部分”(“slave”)的实现仍然不够健壮。事实上,我们应该处理所有可能发生的错误情况。例如,主机可能会在两个传输的中间关闭连接。这会使示例变得复杂很多,留给读者来练习。
该示例的 “master part” 代码很简单。这里我们使用 HAL_I2C_Master_Sequential_Transmit_IT() 函数来启动组合传输,使用 HAL_I2C_Master_Sequential_Receive_IT() 从从机检索到所需寄存器的内容。然后,温度的整数和小数部分再次组合成一个浮点数,并将采集的温度打印在 UART2 上。
void I2C1_EV_IRQHandler(void) {HAL_I2C_EV_IRQHandler(&hi2c1);
}void I2C1_ER_IRQHandler(void) {HAL_I2C_ER_IRQHandler(&hi2c1);
}void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode) {UNUSED(AddrMatchCode);if(hi2c->Instance == I2C1) {transferRequested = 1;transferDirection = TransferDirection;}
}
我们需要分析的最后一部分由 ISR 处理程序表示。如前所述,ISR I2C1_EV_IRQHandler() 调用 HAL_I2C_EV_IRQHandler()。这会导致每次主机在总线上发送从机地址时调用 HAL_I2C_AddrCallback() 函数。调用时,回调将接收指向I2C_HandleTypeDef的指针,该指针表示特定 I²C 描述符、传输方向 (TransferDirection) 和匹配的 I²C 地址 (AddrMatchCode):这是必需的,因为在从机模式下工作的 STM32 I²C 外设可以响应两个不同的地址,因此我们有办法根据主机发出的 I²C 地址编写不同条件的代码。
3、使用 CubeMX 配置 I²C 外设
像往常一样,CubeMX 将配置 I²C 外设所需的工作量减少到最低限度。在 Category 列表窗格(从 Pinout 视图)中启用外设后,我们可以从 Configuration 窗格配置所有设置,如下图所示。
默认情况下,当在采用 LQFP-64 封装的 STM32 MCU 中启用 I2C1 外设时,CubeMX 将启用外设 I/O PB7 和 PB6 引脚(分别为 SDA 和 SCL)作为默认。这些不是锁定在连接器上的引脚,但您需要通过单击它们来选择两个替代引脚 PB9 和 PB8,然后从下拉菜单中选择相应的功能,如下图所示。