在学习i2c,uart,spi这样的通信协议的时候,不知道小伙伴们有没有这样的感受,明明硬件协议部分可以理解,hal库给的函数也会调用,为什么总有些无法触碰到底层的感觉。原因就是hal库做的太好了,他把通过寄存器实现通信逻辑的部分全都封装好了。而要真正学懂通信一个通信协议的话,最好的实现方法就是看hal库的源码,看到底是怎么通过配置寄存器来实现完美的通信协议的。
废话少说,直接来看i2c
初始化部分
我们知道,要控制一个外设,需要许多寄存器
初始化部分控制的主要是一部分的控制寄存器,这方面其实不需要很底层,毕竟就是一一赋值的关系,明白各个参数的意思就行了,会用cubemx就行,like this(寄存器也都是一一写值,懂这个过程就行)
本文的重心是通信协议实现的部分,这里就不多赘述了
主模式发送部分
请出第一个主角
HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
最简单的一个函数,主模式循环发送,但是要完全搞懂了他的源码,其他的模式不过添添补补罢了
看看源码
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
/* Init tickstart for timeout management*/
uint32_t tickstart = HAL_GetTick();
if (hi2c->State == HAL_I2C_STATE_READY)
{
/* Wait until BUSY flag is reset */
if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) != HAL_OK)
{
return HAL_BUSY;
}
/* 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;
/* Send Slave Address */
if (I2C_MasterRequestWrite(hi2c, DevAddress, Timeout, tickstart) != HAL_OK)
{
return HAL_ERROR;
}
/* Clear ADDR flag */
__HAL_I2C_CLEAR_ADDRFLAG(hi2c);
while (hi2c->XferSize > 0U)
{
/* Wait until TXE flag is set */
if (I2C_WaitOnTXEFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK)
{
if (hi2c->ErrorCode == HAL_I2C_ERROR_AF)
{
/* Generate Stop */
SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP);
}
return HAL_ERROR;
}
/* Write data to DR */
hi2c->Instance->DR = *hi2c->pBuffPtr;
/* Increment Buffer pointer */
hi2c->pBuffPtr++;
/* Update counter */
hi2c->XferCount--;
hi2c->XferSize--;
if ((__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BTF) == SET) && (hi2c->XferSize != 0U))
{
/* Write data to DR */
hi2c->Instance->DR = *hi2c->pBuffPtr;
/* Increment Buffer pointer */
hi2c->pBuffPtr++;
/* Update counter */
hi2c->XferCount--;
hi2c->XferSize--;
}
/* Wait until BTF flag is set */
if (I2C_WaitOnBTFFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK)
{
if (hi2c->ErrorCode == HAL_I2C_ERROR_AF)
{
/* Generate Stop */
SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP);
}
return HAL_ERROR;
}
}
/* Generate Stop */
SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP);
hi2c->State = HAL_I2C_STATE_READY;
hi2c->Mode = HAL_I2C_MODE_NONE;
/* Process Unlocked */
__HAL_UNLOCK(hi2c);
return HAL_OK;
}
else
{
return HAL_BUSY;
}
}
接下来逐行分析
因为设有最大超时时间,所以先获取滴答定时器的值,用于超时时间计算
然后会读取i2c的BUSY flag
看看
他读取了SR1或SR2寄存器的指定flag标志位
这个函数会循环检测SR2寄存器的busy位,其实就是看现在i2c总线上有没有设备在发消息(scl或sda被拉低了)(这里提一嘴,因为i2c协议需要io口都配置为开漏输出模式,所以默认都是高电平,有线被拉低就说明有设备在使用总线收发数据,busy为被置位)
后面就是看i2c是否被使能了,要是没开就打开
然后把pos位失能
先看一下pos位有什么作用
好像是接收模式是用于选择应答模式的,博主也不要太会,但发送也用不到
重点来了,发送从机地址
看看这里面干了什么
先判断是不是第一次发送,是的话就产生起始条件
产生起始条件,就是让SDA,SCL两条线按照i2c规定的方式变化产生起始信号
后面就是循环等待SB标志位被置位,就是地址已发送事件,告诉cpu可以进行下一步了
熟悉i2c的都知道,下面就是要开始向数据寄存器dr里面放入数据,让移位寄存器根据scl的时钟频率发送了
先判断你是用什么模式发送,七位地址还是十位地址,咱们只关注七位地址,就是简单的在你发的从机地址后加上表示写命令的1
后面循环检查ADDR位是否被置位
但是但是但是!!!!!!!!!!!
原先我并没有很在意这个函数,就导致我有一个地方非常困惑,检测从机信号的ack在哪里?
着重看一下这行
if (__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_AF) == SET)
你小子,真会找地方藏
在循环检测ADDR寄存器是否被置位时,同时循环检测A响应事件的发生
根据这张官方时序图可以得知,A要先于EV6事件(ADDR)发生,所以要在检测ev6事件的函数中检测A
然后清除ADDR位,真就是完全按照手册来,读SR1,再读SR2,真有意思
然后下面就进入了写数据部分了
放数据之前的检测TXE位对于时序EV8_1
当你设定的目标发送数量XferSize还没有发完时就会进入这个大while发送流程里面
这个while里面先判断TXE标志位
发送数据寄存器空,我们就把下一个数据位写入DR,让DR做好准备
指针的加一减一和底层没关系,就是为了hal库能正确操作的
再往下
再检测BTF标志位,就是移位寄存器把你的数据发出去了,会告诉你,我把数据发完了,然后你要求的数据数还没发完的话,就继续把你的数据移到DR里面(数据手册的描述,这位置位的时候DR已经空了,直接写入不会覆盖)BTF在这个过程中就会被归位为0
下一步很有意思,他又检测了BTF位(对应先前会发生一次ack信号的下一次EV_8)
后面就是while里面的循环了,不过这些循环不会再检测到其实条件和没有ACK回应的那次EV_8了
就是对于发送、判断应答的循环
当循环次数够了之后
产生停止信息,解锁
本来还想再看看其他的模式比如中断、mem模式的,但是博主能力有限,就这样吧,希望这篇文章能够告诉大家硬件软件是怎么相互配合实现通信协议的
后面有时间可能会再分析一下spi、can,或者i2c中断模式的源码
这真的挺有意思