缘由
最近在 STM32 上开发一个使用 I2C 通信的传感器的驱动,发现经常会读不到数据,严重的时候甚至会 I2C 出错需要强制恢复。这样的话数据帧率很难保证,很是苦恼。
都说 STM32 的 I2C 很烂,有很多的问题。但是问题严重性上来了,还是需要研究一下。
分析
首先用最原始的方法,加日志,来定位出问题的原因。然而,在压力测试中发现,出问题的时机很随机,并没能找到共同点。因此我们怀疑是传感器器件可能有稳定性问题。
然后有人提议,用一块 EEPROM 来重复读写 1-16 个字节的数据来进行压力测试,确认一下 STM32 的 I2C 本身没有问题。
这个过程持续了几天。其中出现过问题,我们一度认为就是 STM32 的 I2C 也存在一定的稳定性问题。
然而有一个现象难以解释就是,使用了这个传感器器件会更容易出现问题。因此确实有可能是我们使用上存在着问题。
接下来我们把 I2C 信号接上了示波器,在压力测试的脚本中加入了测试失败的触发信号,把出现问题的几次波形抓了下来。
这次问题定位出来了,是 STM32 主机在不合理的地方主动生成了一个停止信号,还存在刚生成一个起始信号之后直接来了一个停止信号,通信就中断了。
尝试
STM32 生成停止信号的条件是在 I2C 的 CR1 寄存器中有一个位叫 STOP 位,当其设置为 1 时就会生成一个停止信号,然后硬件自动复位为 0。
因此第一个尝试就是在把 STOP 位设置为 1 之前读一下 CR1 寄存器,如果还是 1,那就不动。
这个尝试之后依然是漫长的一轮压力测试。原本以为问题就是这么简单,在问题发生的频率显著下降之后,还是会出现问题。依然是示波器抓波形,这回就剩下了一种情况,起始信号后马上接了一个停止信号。
起始信号的生成方式也是这个 CR1 寄存器,其中的一位叫 START 位,写 1。因此第二个尝试也很自然,在给 START 位写 1 之前,强制给 STOP 位清 0。
这个尝试之后的压力测试就没见过问题了。
深究
到了这里其实我们还是抱有很大的疑惑的,因为我们搜索了大量的资料,并没有人说需要这样操作,虽然都说 STM32 的 I2C 很烂。
于是我把芯片手册看了一遍又一遍,直至看到这么一段话:
When the STOP, START or PEC bit is set, the software must not perform any write access to I2C_CR1 before this bit is cleared by hardware. Otherwise there is a risk of setting a second STOP, START or PEC request.
看来坑是在这里,我们一定是在 STOP 位还是 1 的时候又写了一下 CR1 寄存器。
整理了一下 CR1 寄存器可能会用到的位:
位 | 名称 | 用途 |
---|---|---|
15 | SWRST | 软复位 |
12 | PEC | 启用校验 |
11 | POS | ACK 位置为当前还是下一字节 |
10 | ACK | 是否启用 ACK |
9 | STOP | 停止信号 |
8 | START | 起始信号 |
0 | PE | 使能 |
这里面只有 POS/ACK/STOP/START 会在通信中途需要改变值的位,而显然代码里 START 和 STOP 是一一匹配的,很大概率是 POS/ACK 导致的问题。
仔细查阅了一遍代码,只有一个很常见的情况,就是在 I2C 读数据最后一个字节的时候,按定义来说不能回复 ACK,所以会先关掉 ACK,当通信结束发 STOP 之后为了方便,会默认再设置 ACK,并恢复 POS 为当前字节。
于是我们在这个逻辑的地方增加了一个等待,如果设置 ACK/POS 之前 STOP 位还没有复位,就空等一下。我们把之前尝试的逻辑还原之后进行压力测试,也依然没有出现问题。
但是这个逻辑的空等会有点浪费。我们重新审查了代码逻辑之后发现,实际上这个 ACK/POS 的设置可以提前到设置 STOP 之前,这样就能完全规避掉这个风险了。
回顾
回过头来总结一下这一次的问题处理,发现嵌入式的驱动开发真的很需要仔细阅读芯片手册,一丁点小的说明都有可能是解决问题的思路。
然后就是吐槽一下 STM32 的 I2C 的坑,按手册上说的来看,我们在 STOP 位还是 1 的时候操作了 CR1 寄存器,存在重复生成停止信号的风险,但是!它生成停止信号一定是在下一次生成起始信号之后进行的!这一次通信就直接结束了!