摘要:本文深入浅出地介绍如何发现I2C总线死锁,分析了各种可能的失效模式,最终找到解决方案。对于硬件工程师如何分析解决问题有一定的参考价值。
关键词:I2C,BUSY,死锁
1.概述
江湖上一直有一个传说,那就是ST的I2C不可靠。ST作为ARM构架32位处理器的老大哥,我是不愿意相信这种鬼话的。这么多年下来,一直用I2C总线接口的EEPROM,从来没有出过问题。直到最近我玩票了一款I2C的OLED显示屏,种种迹象表明,那些遥远的传说,也许是真的。
考虑到我们越来越多的产品需要带显示器,弄了一个0.96寸I2C接口的OLED显示屏耍耍。该显示屏驱动芯片用的是SOLOMON公司的SSD1306,单色128*64分辨率。
这个芯片的驱动程序就是利用ST的HAL库函数写SSD1306命令寄存器或者显存数据,移植过程比较简单,与写EEPROM接口函数一模一样,只是从设备地址不一样而已。点亮屏幕后,调用字符串显示函数,效果如图1所示。
图1 0.96寸OLED显示效果
在接下来动态刷屏的过程中,发现屏幕偶尔会死机。一开始怀疑I2C的速度太快,出现误码引起的,把I2C时钟频率从400kHz降到100kHz,故障率没有改变。
后来怀疑,是不是显示的数据太多,I2C屛显示的东西太多,通讯过程容易故障,对驱动程序一番优化,大大减少刷新次数和数据,可是在翻页的时候,还是会出现死机。
2. 寻道
为了解决死机的问题,只好先采取一些临时措施,比如说:没有按键操作,时间超过5分钟,就关掉屏幕,也就是说停止I2C通讯,从而避免出错,但是在一些需要屏幕一直点亮的场合,这肯定行不通。
第二个方案相对可行写,激活独立看门狗,万一显示任务崩溃了,通过看门复位主程序,当然,这个也只是治标不治本。有些应用场合,也是不允许轻易复位的,容易造成数据丢失。
是时候追寻真正的原因了!
首先,通过J-Link RTT Viewer监视I2C通讯失败返回值,如图2所示:
图2 监视I2C通讯失败返回值
通过查看函数HAL_I2C_Mem_Write返回值宏定义可以知道,2是HAL_BUSY的枚举值。也就是说,屏幕之所以死机,是因为写SSD1306的时候,I2C检测到总线忙,所以数据发不出去,换句话说,就是总线“忙死”了!
3. 悟道
既然I2C是因为状态为BUSY导致无法使用,首先想到的是加长超时等待时间,既然你忙,我慢慢等你呗,把timeout从10ms加长到2000ms,但是故障依然没有消除。
我也尝试采用do-while结构,一直检查写I2C返回值,结果直到看门狗复位,都没有等到I2C闲下来。看来,真的是忙死掉了。
接下来,我想通过复位I2C状态来解决这个问题,代码如Listing 01:
/** * @brief write SSD1306 * @param pucData, writen to ssd1306 * @param size, data quantity * @retval void */ static void prvWriteData(unsigned char *pucData, uint16_t size) { HAL_StatusTypeDef xRetVal; do { /* write data to SSD1306 */ xRetVal = HAL_I2C_Mem_Write(&hi2c2,OLED_SA,MEM_DATA,I2C_MEMADD_SIZE_8BIT,pucData,size,5);
if(xRetVal != HAL_OK) { /* output return value by RTT */ SEGGER_RTT_printf(0,”i2c write data fail:%dn”,xRetVal);
/* release SDA and SCL */ HAL_I2C_DeInit(&hi2c2);
/* initialize again */ HAL_I2C_Init(&hi2c2); } } while(xRetVal != HAL_OK); } |
Listing 01 重新初始化I2C |
我想,不管你有多忙,我重新初始化I2C外设了,你总归回复正常了吧。但是,并没有解决这个问题。
既然重新复位了I2C仍然没有解决问题,这个时候,我开始怀疑是不是屛的质量了,毕竟10来块钱的东西,有点质量问题也是难免的。
然后对屛进行了若干改造,减少I2C 上拉电阻,增加滤波电容,各种骚操作,但是于事无补。
上网花了10块钱又买了一个SPI接口屏幕,移植程序,发现一切都是OK的,驱动芯片都是SSD1306,这把问题又引回来I2C。
用逻辑分析仪监视I2C总线,发现屏幕死机之前,所有数据解析都是正常的,而且没有出现过误码之类的错误,这说明I2C时钟频率跑400kHz是没问题的。但是一旦出现BUSY锁死之后,发现总线SDA确实被拉倒低电平了。而且再也回不去了。这等于是I2C控制器永远失去了对I2C的控制权,所以就出现了前面所描述的故障。
百度了一下I2C BUSY关键字,结果弹出一堆这个问题,如图3所示:
图3 I2C BUSY检索结果
基本上都是说ST的F1系列,I2C控制器可能存在缺陷,容易造成BUSY死锁,甚至复位微处理器都解决不了。解决方案也很简单:
(1)把SCL和SDA两个GPIO口重新配置成普通输出
(2)强制把SCL和SDA拉回高电平
(3)软件复位I2C控制器
(4)释放复位
(5)重新配置I2C
源代码如Listing 02所示:
/** * @brief Fix i2c Busy Deadlock hardware bug * @param handle pointer of i2c * @retval void * @remark call this function in HAL_I2C_MspInit() defined in i2c.c **/ void HAL_I2C2_FixBusyDeadlock(I2C_HandleTypeDef* i2cHandle) { GPIO_InitTypeDef GPIO_InitStruct = {0};
/* step 1, config GPIO SCL and SDA as output */ GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* step 2, pull up GPIO SCL and SDA */ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET);
/* step 3, software reset I2C controller */ i2cHandle->Instance->CR1 = I2C_CR1_SWRST;
/* step 4, release reset */ i2cHandle->Instance->CR1 = 0; } |
Listing 解除I2C BUSY 死锁 |
至此,问题得到了解决。在运行的过程会,偶尔发生了BUSY 死锁,通过上面的补丁,I2C马上又重新获取了总线控制权,通过RTT监视的数据也可以看出来,发生过BUSY报错,但是马上又解除了,如图4所示。
图4 发生BUSY报错后又自动消除
4小结:
我一直都是ST的忠实粉丝,觉得生态环境做得真好。但是I2C这个事情,可能真的要重视一下,死锁的后果是很严重的。有一些人宁愿用GPIO口模拟I2C时序,也不要用ST自带的I2C控制器,说明不认可的人还是蛮多的。
鄙人还是以前那个观点,能用I2C控制器还是优先用控制器,毕竟ST的HAL库写得还是非常不错的,不使用控制器,中断和DMA就没法用了,一些总线状态寄存器也没法用了。
回到I2C BUSY死锁这个问题,解决方案就是监视写I2C返回值,一旦发现是HAL_BUSY,就调用我写的HAL_I2C2_FixBusyDeadlock补丁,然后重新初始I2C总线。
没有回复内容