又来写I2C通信了......真是换一个新库就要花时间重新调试一下,这次用的是最新的官方库STM32Cube_FW_F0_V1.10.0 ,开发平台用的也是新的STM32CubeIDE。
(一)
需要完成的任务是模拟一个电池包被动发送信息给充电器,调试阶段我用STM32F030R8板子上的I2C2做主模拟充电器,I2C1做从模拟电池包。充电器读取电池包信息时通信时序是:先发送从机地址(模拟的电池包地址)+ Write标志,将需要读取信息的寄存器值发送给从机,发送完成后再发送一次从机地址+ Read标志,等待从机发送相关的信息回来。
主机我用的的读取函数是
HAL_I2C_Mem_Read(&hi2c2, (uint16_t)BAT_IC_ADDRESS, BAT_REG_TEMP, I2C_MEMADD_SIZE_8BIT, Tempe, 2, 500);
因为我需要根据不同的寄存器值来发送不同的信息,所以我在第二次地址匹配时应该先判断上次读取的寄存器值,再选择发送哪种类型的数据 。因此,从机采用中断接收模式。
研究了下官方库,总的来说,I2C如果要实现中断,有一个基本的函数调用顺序:
1、配置I2C初始化设置,使能I2C中断
2、当发生事件中断时会调用EV_IRQHandler,而如果发生错误会调用ER_IRQHandler。EV_IRQHandler中,会判断中断ISR以及中断源类型,并根据I2C是主机还是从机来决定调用I2C_Matser_IT还是I2C_Slave_IT
3、在I2C_Matser/Slave_IT函数中,有具体的区分不同的中断类型:地址匹配中断(ADDR/ADDRIT)、发送中断(TXIS/TXI)、接收中断(RXNE/RXI)、AF/NACKI、STOPF/STOPI,不同的中断类型有不同的中断处理回调函数
然而,实际调试中发现,如果需要在地址匹配时自己做另外处理,则需要重写HAL_I2C_AddrCallback函数,而这个函数只有在满足 ((hi2c->State & HAL_I2C_STATE_LISTEN) == HAL_I2C_STATE_LISTEN)条件时才会调用,找到源头确定满足该条件即需要使用HAL_I2C_Slave_Sequential_Receive_IT函数进行中断接收。而如果需要该函数能正常发生中断,在初始化时还需使能ListenIT即HAL_I2C_EnableListen_IT(&hi2c1);
在完成这些配置后即可重写地址匹配回调函数HAL_I2C_AddrCallback,如下,我在地址匹配中断发生后判断主机的读写方向,只有当主机发送的是读时,从机才开始发送数据。
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode)
{
uint8_t Direction = TransferDirection;
uint8_t Temp[2] = {0xFE,0x03};
__HAL_I2C_CLEAR_FLAG(hi2c, I2C_FLAG_ADDR);
if(hi2c == &hi2c1)
{
if(Direction == 1)//Read
{
HAL_I2C_Slave_Sequential_Transmit_IT(&hi2c1, Temp, 2, I2C_FIRST_FRAME);
}
}
}
实际调试过程中发现由有问题,定位到调用HAL_I2C_AddrCallback()的函数I2C_ITAddrCplt(),有一段可以看到当7位地址匹配时,会先Disable_IRQ(hi2c, I2C_XFER_LISTEN_IT),这样会导致第一地址匹配后,第二次再发地址就接收不到中断了。一开始我的解决办法是在后面重新使能中断,即调用I2C_Enable_IRQ(hi2c, I2C_XFER_LISTEN_IT);这样操作后,我发现如果我发两个数据(比如0xFE、0x03),但实际接收到的是0xB6、0x00,DEBUG后发现I2C1中的hi2c->pBuffPtr 的数据在调用I2C_Enable_IRQ(hi2c, I2C_XFER_LISTEN_IT)后会发生变化(具体为什么还未知),这样导致TXDR中的数据变化,发出去的数据也就是错的。最后,我将I2C_Disable_IRQ(hi2c, I2C_XFER_LISTEN_IT);和I2C_Enable_IRQ(hi2c, I2C_XFER_LISTEN_IT)都注释掉,即解决了问题。
else
{
/* Disable ADDR Interrupts */
I2C_Disable_IRQ(hi2c, I2C_XFER_LISTEN_IT);
/* Process Unlocked */
__HAL_UNLOCK(hi2c);
/* Call Slave Addr callback */
HAL_I2C_AddrCallback(hi2c, transferdirection, slaveaddrcode);
}
}
/* Else clear address flag only */
else
{
/* Clear ADDR flag */
__HAL_I2C_CLEAR_FLAG(hi2c, I2C_FLAG_ADDR);
/* Process Unlocked */
__HAL_UNLOCK(hi2c);
}
(二)
在调通一次传输后,我想实现I2C的循环传输,然后发现用以上的方式I2C只通讯一次就停止传输了,具体的现象表现为主机的TXDR内的数据没有被清除,TXIS标志一直不置位,导致出现等待TimeOut,继续查找问题,花了蛮久时间搞懂这个库。
主机Master读取
1、HAL_I2C_Mem_Read
该函数采用的是blocking的传输方式,将devAddress设备地址和MemAddress寄存器地址正确写入即可,其中寄存器地址可根据实际选择8 bit或16bit。
2、HAL_I2C_Master_Sequential_Transmit_IT
该函数采用中断的方式传输,且每次传输一个序列,如果需要读取寄存器内的值,则该函数的调用顺序应该如下
HAL_I2C_Master_Sequential_Transmit_IT(&hi2c2, devAddress, ®_address, 1, I2C_FIRST_FRAME);使用I2C_FIRST_FRAME目的是为了产生restart信号,以继续后面的传输
然后重写发送完成函数HAL_I2C_MasterTxCpltCallback,在函数内开启Master读取
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
if(hi2c == &hi2c2)
{
HAL_I2C_Master_Sequential_Receive_IT(&hi2c2, devAddress, pData, Size, I2C_LAST_FRAME);
}
}
从机Slave发送
前面说了我为了在地址匹配中断时能进行自己的操作所以使用了HAL_I2C_Slave_Sequential_Receive_IT函数接收,但是在主机循环发送时用逻辑分析仪捕捉发现只有一次传输的信息,调试后发现当一次传输完成后,在I2C_Slave_ISR_IT中,会调用I2C_ITSlaveCplt(hi2c, ITFlags);在该函数内会清除ADDR、STOPF FLAG以及失能所有中断。
/* Check if STOPF is set */
if (((ITFlags & I2C_FLAG_STOPF) != RESET) && ((ITSources & I2C_IT_STOPI) != RESET))
{
/* Call I2C Slave complete process */
I2C_ITSlaveCplt(hi2c, ITFlags);
}
/* Process Unlocked */
__HAL_UNLOCK(hi2c);
return HAL_OK;
所以如果要重新发送数据需要重新配置,在I2C_ITSlaveCplt(hi2c, ITFlags)函数最后,如果传输没有发生错误会调用I2C_ITListenCplt(hi2c, ITFlags);Listen传输完成回调函数HAL_I2C_ListenCpltCallback也在里面,因此我们可以重写回调函数HAL_I2C_ListenCpltCallback,在里面重新配置开启Slave发送,记得这时也要重新使能ListenIT
void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c)
{
if(hi2c == &hi2c1)
{
HAL_I2C_EnableListen_IT(&hi2c1);
HAL_I2C_Slave_Sequential_Receive_IT(&hi2c1, Command_Type, 1, I2C_FIRST_AND_NEXT_FRAME);
}
}
总结来说,Sequential_Transmit/Receive函数应该只是用于一次传输(序列传输),如果要实现循环传输,必须要重新配置重新使能中断,至于配置和使能的位置则需要仔细查找对应的回调函数,在正确的位置调用。