STM32硬件框架介绍
首先我们来看IIC通信的硬件架构
可以看出,可以分为以上4部分。
第一部分:通信接口
SDA信号和SCL信号由此产生或输入
第二部分:时钟部分
时钟信号由此产生或由此读取
第三部分:数据部分
通信时,数据从缓冲区放入DR寄存器,再由SR寄存器将其一位一位移出到SDA发送。这个过程持续直到数据发送完毕。
第四部分:控制部分
控制部分和初始化密切相关,控制部分设置IIC的工作模式和使能等各种内部配置
我们主要需要理解的部分为1、3部分。内部的时钟和控制由HAL库帮我们管理好了。接下来我们就由此展开讲解如何用硬件方式实现IIC通信,利用IIC接口和STM32中的片内外设,而不是软件模拟的方式来实现。
在这里我也稍微提及一下软件模拟方式实现IIC通信与硬件方式的差别和利弊。软件模拟时,需要优化IO口类型,由于数据经常发生变化,这种类型的数据尽可能的需要放入CPU的寄存器中(__IO关键字),方便使用,这无形降低了CPU寄存器的数量,从而拖慢了运算速度。另外,软件模拟方式,需要CPU实时改变电平,CPU运算负担较大,不合算。再者,使用软件控制电平变化,需要我们仔细理解协议。这个协议是简单的IIC协议,若是比较复杂的协议,我们也要如此模拟产生电平吗?
显然,这体现了一个道理——封装。我们使用协议,一般而言,不需要了解协议内部的具体规定,只要我们用符合规格的设备,就可以实现数据通信。若我们使用协议,还要了解协议内部如何实现,增大了使用协议通信的成本,也浪费了时间。是一种“重复发明轮子”的行为。正如我们使用电子元器件不需要了解内部的物理实现,使用芯片时不需要了解内部电路一样。不同的部件由不同的人管理,这样才是将生产力最大化的方式,也是封装的意义所在。
软件模拟方式的益处在于代码简单明了,干净整洁。但是!笔者十分不推荐这种破坏封装性的编程方式,这种简洁,是牺牲了CPU运算效率换来的。以往,51单片机的内部并没有集成IIC接口,所以需要用软件模拟,前几年,据网上的文献资料表示,STM32的库为了规避IIC的专利,用了较为复杂的方式去控制STM32的IIC接口和协议,所以错误较多。但如今这种错误已经没有了,IIC接口可以正常使用,而且HAL库的封装度极高,在各种配置都较为充裕的STM32中,我十分推荐使用HAL库内部封装的IIC接口。
此举增加的代码的模块性。在移植方面,HAL库抽象出了MSP层,而移植只需要修改MSP层的代码即可(改一下管脚)和软件模拟的移植难度一样低。也降低了CPU的资源占用。长时间模拟电平变化消耗的运算资源还是十分多的。其实笔者也认为,之所以串口通信不是电平模拟,而IIC通信是依赖电平模拟,很大的原因不是硬件实现的方式不行,而是前几年STM32的代码有问题,再早一些的51又不含IIC的接口,所以导致大家基本都使用软件模拟的方式,使用硬件实现的文献也很少,查阅资料也很不方便。但既然现在STM32内部封装好的库已经可以正常使用,我们没有理由占用CPU资源。这也是我要介绍硬件实现的原因。
若读者对软件模拟的方式感兴趣,可以自行查阅网络文献和书籍资料,软件模拟的教程在网络上和书籍中都十分常见,这里不再赘述。即使不用软件模拟,了解算法加深对IIC通信协议的理解,也是很好的行为,所以笔者也推荐去了解一下软件实现IIC通信。
IIC初始化
首先将stm32fxxx_hal_i2c.c,stm32fxxx_hal_i2c_ex.c加入文件中。再在main.h中的stm32fxxx_hal.h中找到stm32fxxx_hal_conf.h,将其中对#define HAL_I2C_MODULE_ENABLED的注释取消。若不知道如何加入文件和清除注释,请参考笔者上一篇文章STM32串口初始化与使用详解(基于HAL库实现)的串口初始化部分,此处不再赘述。
初始化使用的函数介绍:
| HAL库函数 | 具体功能 |
|---|---|
| HAL_StatusTypeDef HAL_I2C_Init(I2C_HandleTypeDef *hi2c) | 初始化函数 |
| HAL_StatusTypeDef HAL_I2C_DeInit(I2C_HandleTypeDef *hi2c) | 取消初始化函数 |
| __weak void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c) | 初始化MSP层 |
| __weak void HAL_I2C_MspDeInit(I2C_HandleTypeDef *hi2c) | 取消对MSP层的初始化 |
__weak关键字:
这个关键字告诉编译器,这个定义是弱定义,若在编译时,有一处函数定义不含此关键字,则所有该函数的调用都指向那一个非weak型的函数定义。这个操作叫函数重定向。因为在系统头文件里,MSP层是没有声明的,MSP层是具体对应每一个MCU的物理状态,系统文件不会声明也不能声明,但在单片机做上电初始化时,有一些函数又必须调用,所以系统文件采取了这种用户可以重定向的写法。我们在初始化串口时,需要自己写一个MSPInit,而且不含weak。
这是笔者在上一篇文章介绍__weak关键字时的描述,放在此处供参考用。
HAL_I2C_Init()函数做的事情使将用户传入的I2C句柄初始化配置到具体的I2C设备中,并且调用MSP层初始化。
只要我们初始化了IIC和它的MSP层,这个设备就将变得可用。
初始化:
void I2C1Init(void)
{
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
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;
if (HAL_I2C_Init(&hi2c1) != HAL_OK) //按要求配置IIC并初始化
{
Error_Handler();
}
}
初始化MSP层:
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(hi2c->Instance==I2C1)
{
__HAL_RCC_GPIOB_CLK_ENABLE(); //初始化对应GPIO时钟
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
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); //按上述要求初始化GPIO口
__HAL_RCC_I2C1_CLK_ENABLE();
HAL_NVIC_SetPriority(I2C1_EV_IRQn, 0, 0); //分别配置EV和ER的中断优先级且使能中断
HAL_NVIC_EnableIRQ(I2C1_EV_IRQn);
HAL_NVIC_SetPriority(I2C1_ER_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(I2C1_ER_IRQn);
}
}
在MAIN函数中调用后,IIC硬件就实现了初始化。
如何实现通信?通信过程是如何实现的?
从缓冲区中将数据放入DR寄存器,再由SR寄存器指挥发送,循环这个过程直到缓冲区内无数据。收数据则同理。
接下来介绍函数(一般而言我们都将MCU做主设备,控制从设备的芯片,所以接下来介绍的收发方式都是主机模式下的,从机模式下同理)
| HAL库函数 | 具体功能 |
|---|---|
| HAL_I2C_Master_Transmit | 阻塞模式下的IIC发送函数 |
| HAL_I2C_Master_Receive | 阻塞模式下的IIC接收函数 |
| HAL_I2C_Master_Transmit_IT | 非阻塞模式下的IIC发送配置函数 |
| HAL_I2C_Master_Receive_IT | 非阻塞模式下的IIC接收配置函数 |
| HAL_I2C_Mem_Write | 阻塞模式下的IIC寄存器写函数 |
| HAL_I2C_Mem_Read | 阻塞模式下的IIC寄存器读函数 |
不论是读写寄存器还是iic设备,都有轮询、中断和DMA方式,篇幅原因,这里就介绍这些抛砖引玉
阻塞模式下的发送代码如下,笔者将笔者认为比较主要的部分留了下来方便阅读。
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
//…
//…
if (hi2c->State == HAL_I2C_STATE_READY)
{
if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) != HAL_OK)
{
return HAL_BUSY;
}
/* Check if the I2C is already enabled */
if ((hi2c->Instance->CR1 & I2C_CR1_PE) != I2C_CR1_PE)
{
__HAL_I2C_ENABLE(hi2c);
}
//发送时关于总线和其他标志位的配置
hi2c->State = HAL_I2C_STATE_BUSY_TX;
hi2c->Mode = HAL_I2C_MODE_MASTER;
hi2c->ErrorCode = HAL_I2C_ERROR_NONE;
hi2c->pBuffPtr = pData;
hi2c->XferCount = Size;
hi2c->XferSize = hi2c->XferCount;
hi2c->XferOptions = I2C_NO_OPTION_FRAME;
/* 发送从机地址 */
if (I2C_MasterRequestWrite(hi2c, DevAddress, Timeout, tickstart) != HAL_OK)
{
return HAL_ERROR;
}
/* 检验地址标志 */
__HAL_I2C_CLEAR_ADDRFLAG(hi2c);
while (hi2c->XferSize > 0U)
{
//…
//… 循环发送过程。直到发送完毕
}
/* Generate Stop */
SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP);
//释放对总线的控制和其他标志位配置
hi2c->State = HAL_I2C_STATE_READY;
hi2c->Mode = HAL_I2C_MODE_NONE;
return HAL_OK;
}
else
{
return HAL_BUSY;
}
}
可以发现,阻塞模式下的发送函数。若配置完毕且各种状态正常,声明占用总线,标志各种标志位,循环发送数据直到数据全部发送出去,发送完毕后取消对总线占用的声明,并返回HAL_OK。
阻塞模式下的接收也同理,这里不再赘述。
中断模式下,函数只配置了相关的中断标志位,并没有真正的实现数据发送。真正的数据发送在中断服务函数中
函数代码如下:
HAL_StatusTypeDef HAL_I2C_Master_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size)
{
__IO uint32_t count = 0U;
if (hi2c->State == HAL_I2C_STATE_READY)
{
/* Wait until BUSY flag is reset */
count = I2C_TIMEOUT_BUSY_FLAG * (SystemCoreClock / 25U / 1000U);
do
{
count--;
if (count == 0U)
{
hi2c->PreviousState = I2C_STATE_NONE;
hi2c->State = HAL_I2C_STATE_READY;
hi2c->Mode = HAL_I2C_MODE_NONE;
hi2c->ErrorCode |= HAL_I2C_ERROR_TIMEOUT;
/* Process Unlocked */
__HAL_UNLOCK(hi2c);
return HAL_ERROR;
}
}
while (__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BUSY) != RESET);
/* Process Locked */
__HAL_LOCK(hi2c);
/* Check if the I2C is already enabled */
if ((hi2c->Instance->CR1 & I2C_CR1_PE) != I2C_CR1_PE)
{
/* Enable I2C peripheral */
__HAL_I2C_ENABLE(hi2c);
}
/* Disable Pos */
CLEAR_BIT(hi2c->Instance->CR1, I2C_CR1_POS);
hi2c->State = HAL_I2C_STATE_BUSY_TX;
hi2c->Mode = HAL_I2C_MODE_MASTER;
hi2c->ErrorCode = HAL_I2C_ERROR_NONE;
/* Prepare transfer parameters */
hi2c->pBuffPtr = pData;
hi2c->XferCount = Size;
hi2c->XferSize = hi2c->XferCount;
hi2c->XferOptions = I2C_NO_OPTION_FRAME;
hi2c->Devaddress = DevAddress;
/* Generate Start */
SET_BIT(hi2c->Instance->CR1, I2C_CR1_START);
/* Process Unlocked */
__HAL_UNLOCK(hi2c);
/* Note : The I2C interrupts must be enabled after unlocking current process
to avoid the risk of I2C interrupt handle execution before current
process unlock */
/* Enable EVT, BUF and ERR interrupt */
__HAL_I2C_ENABLE_IT(hi2c, I2C_IT_EVT | I2C_IT_BUF | I2C_IT_ERR);
return HAL_OK;
}
else
{
return HAL_BUSY;
}
}
可以发现,函数和阻塞模式一样配置了各种配置,但没有发送的部分,只有对中断使能的一行函数__HAL_I2C_ENABLE_IT(hi2c, I2C_IT_EVT | I2C_IT_BUF | I2C_IT_ERR);,真正的发送在中断执行进入的中断服务函数中。接收中断配置函数也同理。
与串口收发不同,这里很常见的需要对其中的寄存器进行读写,从而读出/写入数据,或者实现对设备配置和控制。所以存在寄存器读写的函数。这里也只介绍读取的函数,写入函数完全一样,只是R/W标志位不同。
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句柄,设备地址,寄存器地址,寄存器地址位数,输出缓冲区,输出大小,超时返回时间。从而实现对内部寄存器的读写。这个函数的功能很明确,具体实现与普通的写函数其实差不多,所以不把函数列出来了。有需要可以自己去阅读内部的代码。
了解了IIC硬件初始化和收发函数,就可以具体实践了。笔者使用LM75A进行温度采集,IIC通信传给MCU。MCU可对采集到的数据进行其他处理。相关代码和步骤如下
I2C.h。在这里声明了LM75A的地址和相关的寄存器地址,不同芯片不同,需要得知这些信息请阅读相关芯片的芯片手册。
#ifndef __I2C_H_
#define __I2C_H_
#include "main.h"
#define LM75ADDR 0x90 //LM75I2C地址
#define LM75TEMP 0x00 //内部温度寄存器地址,一共11位,高5位无效,高位在前,低位在后。读温度时需要读两次
#define LM75CONF 0x01 //配置寄存器地址。可用来配置相关OS模式(具体参考芯片手册)
#define LM75LAGR 0x02 //滞后寄存器地址。滞后寄存器可以用来配置温度下限相关的数据,具体参考芯片手册
#define LM75OVER 0x03 //过温寄存器地址。可用来配置温度上限的相关数据,具体参考芯片手册
extern I2C_HandleTypeDef hi2c1;
int GetTemp(void); //从温度寄存器地址中获取温度并返回
void GetDataFromReg(uint8_t regAddress,uint8_t data); //从某一寄存器中读取1字节的数值
void PutDataForReg(uint8_t regAddress,uint8_t data); //往温度传感器的某一寄存器中写入1字节的数值
uint8_t PutMulDataForReg(uint8_t regAddress,uint8_t * data, uint8_t len); //从温度传感器的某一寄存器中读取len个字节的数值
uint8_t GetMulDataFromReg(uint8_t regAddress,uint8_t * data,uint8_t len);
HAL_StatusTypeDef IICGetDataFromReg(uint8_t I2CADDR,uint8_t regAddress,uint8_t data);
HAL_StatusTypeDef IICPutDataForReg(uint8_t I2CADDR,uint8_t regAddress,uint8_t data);
HAL_StatusTypeDef IICGetMulDataFromReg(uint8_t I2CADDR,uint8_t regAddress,uint8_t len,uint8_t * data);
HAL_StatusTypeDef IICPutMulDataForReg(uint8_t I2CADDR,uint8_t regAddress,uint8_t len,uint8_t * data);
#endif
I2C.c
#include "I2C.h"
#include "usart.h"
//#define I2CDEBUG //定义这行宏定义,关于I2C的debug信息就会输出到串口
I2C_HandleTypeDef hi2c1;
uint8_t I2C1RxHandle[200];
void I2C1Init(void)
{
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
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;
if (HAL_I2C_Init(&hi2c1) != HAL_OK)
{
Error_Handler();
}
}
int GetTemp()
{
int i = 0;
uint8_t tempData[2];
unsigned int thisTemp;
unsigned int thisTempH; //以移位形式存储的高位
unsigned int thisTempR; //以移位形式存储的低位
tempData[0] = 0;
tempData[1] = 0;
GetMulDataFromReg(LM75TEMP,tempData,2);
thisTempH = tempData[0] * 256;
thisTempR = tempData[1];
thisTemp = (thisTempH + thisTempR) / 32 / 8;
#ifdef I2CDEBUG
u1_printf("%d",thisTemp);
#endif
return thisTemp;
}
void GetDataFromReg(uint8_t regAddress,uint8_t data)
{
while(IICGetDataFromReg(LM75ADDR,regAddress,data) != HAL_OK)
{
#ifdef I2CDEBUG
u1_printf("ERR:GetData is error,func is GetDataFromReg\r\n");
#endif
continue;
}
}
void PutDataForReg(uint8_t regAddress, uint8_t data)
{
while(IICPutDataForReg(LM75ADDR,regAddress,data) != HAL_OK)
{
#ifdef I2CDEBUG
u1_printf("ERR:PutData is error, func is PutDataForReg\r\n");
#endif
continue;
}
}
uint8_t PutMulDataForReg(uint8_t regAddress,uint8_t * data, uint8_t len)
{
if(IICPutMulDataForReg(LM75ADDR,regAddress,len,data) == HAL_OK)
{
#ifdef I2CDEBUG
u1_printf("send data success, data is:");
u1_printf("%s\r\n",data);
#endif
return 1;
}
else
{
#ifdef I2CDEBUG
u1_printf("ERR:MUL SENDdata is error,func is PutMulDataForReg\r\n");
#endif
return 0;
}
}
uint8_t GetMulDataFromReg(uint8_t regAddress,uint8_t * data,uint8_t len)
{
if(IICGetMulDataFromReg(LM75ADDR,regAddress,len,data) == HAL_OK)
{
#ifdef I2CDEBUG
u1_printf("get data success");
#endif
return 1;
}
else
{
#ifdef I2CDEBUG
u1_printf("ERR:MUL GETdata is error,func is PutMulDataForReg\r\n");
#endif
return 0;
}
}
HAL_StatusTypeDef IICGetDataFromReg(uint8_t I2CADDR,uint8_t regAddress,uint8_t data)
{
return HAL_I2C_Mem_Read(&hi2c1,I2CADDR,regAddress,8,&data,1,100);
}
HAL_StatusTypeDef IICPutDataForReg(uint8_t I2CADDR,uint8_t regAddress,uint8_t data)
{
return HAL_I2C_Mem_Write(&hi2c1,I2CADDR,regAddress,8,&data,1,100);
}
HAL_StatusTypeDef IICGetMulDataFromReg(uint8_t I2CADDR,uint8_t regAddress,uint8_t len,uint8_t * data)
{
return HAL_I2C_Mem_Read(&hi2c1,I2CADDR,regAddress,8,data,len,200);
}
HAL_StatusTypeDef IICPutMulDataForReg(uint8_t I2CADDR,uint8_t regAddress,uint8_t len,uint8_t * data)
{
return HAL_I2C_Mem_Write(&hi2c1,I2CADDR,regAddress,8,data,len,200);
}
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(hi2c->Instance==I2C1)
{
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
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_RCC_I2C1_CLK_ENABLE();
HAL_NVIC_SetPriority(I2C1_EV_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(I2C1_EV_IRQn);
HAL_NVIC_SetPriority(I2C1_ER_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(I2C1_ER_IRQn);
}
}
void HAL_I2C_MspDeInit(I2C_HandleTypeDef* hi2c)
{
if(hi2c->Instance==I2C1)
{
/* USER CODE BEGIN I2C1_MspDeInit 0 */
/* USER CODE END I2C1_MspDeInit 0 */
/* Peripheral clock disable */
__HAL_RCC_I2C1_CLK_DISABLE();
/**I2C1 GPIO Configuration
PB6 ------> I2C1_SCL
PB7 ------> I2C1_SDA
*/
HAL_GPIO_DeInit(GPIOB, GPIO_PIN_6|GPIO_PIN_7);
/* I2C1 interrupt DeInit */
HAL_NVIC_DisableIRQ(I2C1_EV_IRQn);
HAL_NVIC_DisableIRQ(I2C1_ER_IRQn);
/* USER CODE BEGIN I2C1_MspDeInit 1 */
/* USER CODE END I2C1_MspDeInit 1 */
}
}
void I2C1_EV_IRQHandler(void)
{
HAL_I2C_EV_IRQHandler(&hi2c1);
}
void I2C1_ER_IRQHandler(void)
{
HAL_I2C_ER_IRQHandler(&hi2c1);
}
其中u1_printf();是笔者封装的串口1的发送函数,如果需要将获得的数据打印到串口,自己实现即可
函数逻辑十分简单,层层封装,最高层的是GetTemp();函数从LM75A中读取温度并返回,供上级调用。内部运算可以采取浮点型减小误差,这种运算简单但不能应对所有情形,有更高要求请重新设计。因为这不在IIC介绍的范围内,也不做赘述。
由于没有其他的需求,主逻辑也十分简单
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
I2C1Init();
Usart1Init(115200);
LedInit();
while (1)
{
GetTemp();
}
}
综上所述,使用硬件方式的IIC十分简单,只需要初始化IIC,MSP层,调用相关的读写函数即可。而且不占用CPU的资源。是一种比较有优势的通信方式
前篇:IIC通信协议
因为本人水平实在有限,文章内容若有错误,敬请谅解