關於STM32的I2C接口死鎖在BUSY狀態無法恢復的現象,網上已有很多討論,看早幾年比較老的貼子,有人提到復位MCU也無法恢復、只有斷電才行的狀況,那可是相當嚴重的問題。類似復位也無法恢復的情況是存在的,技術支持矢口否認問題存在,並不是正確面對問題的態度。比如我用這款F439芯片的SDRAM控制器,在錯誤操作后進入HardFault狀態,復位無法恢復,JTAG也無法聯機,只能斷電重來,官方的Erratasheet里也提到了。
如果I2C接口無法可靠工作,那么所做的設計將存在嚴重隱患,不可能要求用戶用斷電的方法恢復系統。如果像某些網友提到棄用硬件I2C,轉為GPIO模擬I2C時序,那么首先I2C時鍾頻率不易確定,因為STM32的時鍾頻率可以動態調節;此外不用硬件I2C,無法用中斷、DMA等高級模式,會嚴重降低ARM內核效率。所以務須確認和解決這個問題。
一.問題存在
我用STM32F439IGT,為了確定問題存在,讓I2C控制器作Master,先人為產生I2C總線故障。產生I2C總線故障的方法簡單而粗暴:在I2C總線工作過程中,用鑷子把SCL和SDA兩個信號短路一下,很容易進入BUSY死鎖狀態。長時間短路也可能產生超時。HAL_I2C_Init()、HAL_I2C_Master_Transmit()、HAL_I2C_Master_Receive()等函數返回值分別為HAL_BUSY(0x02)、HAL_TIMEOUT(0x03)。
試着用MCU復位,是可以恢復的,說明硬件沒死穴。又測試不用MCU復位,而是在程序中依次調用STM32Cube_FW_F4_V1.5.0固件庫提供的如下兩個初始化函數:HAL_I2C_DeInit(&hi2c1)、HAL_I2C_Init(&hi2c1),並不能保證一定恢復正常。
BUSY死鎖時,用萬用表測試I2C信號電壓,SCL、SDA均為低電平。如果調用函數:HAL_I2C_DeInit(&hi2c1),會函數釋放IO口回到GPIO的默認狀態(Input),此時再測SCL、SDA電壓,均為高電平。這說明總線是被MCU這邊的Master拉低的,而不是被Slave拉低的。當然也存在Slave剛好輸出低電平拉低SDA的可能。
二.出錯代碼位置跟蹤
單步運行,可以看到進入stm32f4xx_hal_i2c.c程序中I2C讀寫函數不遠處(如圖陰影所在行),讀BUSY位,總會得到SET的結果,無法繼續執行后續程序而返回。
三.參考文獻
讀了網上很多解決方案,其中比較有啟發意義的有這幾篇:
1. 百度文庫,這個好像是ST官方客服提供的,關於死鎖的可能機理和解決方案做了說明:
http://wenku.baidu.com/link?url=KB9p-TYrQcmVu1azHG66BXAcG6Pe6Bm2kWF_9ERSU35EOA8obiTVTDrZ6fZ3IOjfVAb71RCvJIiAODo4p4Sr0fUPDy0kQyyqWWJgxjfYHzO
2. STM社區,這個提到了初始化I2C引腳前應該先置為OUT及高電平。這在上電初始化時無虞,因為MCU復位后IO口為輸入,並由外部上拉電阻拉為高電平。但在做故障恢復時很重要,因為此時IO口可能正被Master或Slave拉成低電平。http://www.stmcu.org/module/forum/thread-518463-1-1.html
3. 這個解決方案和上面思想兩個相仿,但是寫了太多代碼,又有放置位置的要求,看起來頭大。僅作參考:http://bbs.ednchina.com/BLOG_ARTICLE_2154168.HTM
4. 最重要的說明,在ST官方提供的STM32F4xx用戶指南:RM0090 Reference manualRev9,第845頁,關於I2C_CR1,SWRST位的Note,提到解決BUSY死鎖問題:
意思是說SWRST位可以在出錯或死鎖時,用於復位I2C控制器,例如眾所周知的BUSY位問題。我沒有看其它老STM型號的手冊,至少STM32F4xx有SWRST位,STM32L0xx用戶指南提到可以用PE位復位。
四.問題的解決方案
按照ST手冊的提示,經過各種嘗試,本着盡量少改動代碼、盡量不改動固件庫里只讀文件的原則,我的解決方案如下所述。假設主程序里有如下的代碼,返回值ret不等於0表示出錯,按stm32f4xx_hal_def.h頭文件中的錯誤代碼定義,返回值為0x02是HAL_BUSY,0x03是HAL_TIMEOUT,這兩個返回值都可能得到。下面程序里紅色的兩行是錯誤處理必須的:
4.1 主程序改動,加錯誤處理代碼2行:
unsigned char ret = Sensor_ReadData(uint8* buf); // I2C讀寫函數
if (ret != 0) { //I2C故障處理
HAL_I2C_DeInit(&hi2c1); //釋放IO口為GPIO,復位句柄狀態標志
HAL_I2C_Init(&hi2c1); //這句重新初始化I2C控制器
}
else {
// 。。。。I2C無錯誤時的正常程序
}
4.2 子程序的改動,加7行代碼:
上面HAL_I2C_Init(&hi2c1)函數會調用HAL_I2C_MspInit(hi2c)函數,這個函數在stm32f4xx_hal_msp.c文件中實現,主要是初始化IO口以及外設,由STM32CubeMX工具生成或用戶自行編寫,非只讀文件。以下節選該函數第一段,其中I2C端口用哪個pin,是由用戶自己設定的,我這里用的PB6、PB7。紅、綠底色的幾行是為了處理BUSY死鎖問題專門插入的。
void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c)
{
GPIO_InitTypeDef GPIO_InitStruct;
if(hi2c->Instance==I2C1)
{
__I2C1_CLK_ENABLE();
// PB6 ----> I2C1_SCL
// PB7 ----> I2C1_SDA
// strong pull-uphigh to recover from locking in BUSY state
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7; //此行原有
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; //GPIO配置為輸出
GPIO_InitStruct.Speed = GPIO_SPEED_HIGH; //強上拉
HAL_GPIO_Init(GPIOB,&GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOB, 6, GPIO_PIN_SET); //拉高SCL
HAL_GPIO_WritePin(GPIOB, 7, GPIO_PIN_SET); //拉高SDA
hi2c->Instance->CR1= I2C_CR1_SWRST; //復位I2C控制器
hi2c->Instance->CR1= 0; //解除復位(不會自動清除)
// 以下是原有代碼
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_FAST;
GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
//。。。
}
上面程序中,把I2C端口配置成GPIO-OUTPUT,並強制拉高,是必需的。注意到手冊里關於SWRST位說明的第一句:“When set, the I2C isunder reset state. Before resetting this bit,make sure the I2C lines are released and the bus is free.” 意思就是置位SWRST,會使I2C控制器保持在復位狀態。解除復位前,確保I2C總線已經釋放到空閑狀態,即SCL、SDA均為高電平,再恢復I2C控制器。所以解除復位是用戶來做的,硬件不會自動清除該位。
五.結論
我用這款STM32F439IGT單片機,I2C部分沒有出現斷電才能解除BUSY死鎖的嚴重問題,看來STM已經意識到這個硬BUG,並在后期產品里逐步進行了改進。
在沒有硬件死穴的情況下,我這里僅增加10行程序,就可以用軟件恢復故障。多次嘗試,觸發I2C故障時,一次就可以恢復,無需加延時等語句,也未改動現有固件庫代碼。
Circuitlife
2015年6月3日