STM32 HAL庫 硬件I2C 從機主機防BUG程序

前言

最近死磕了5天的STM32F1硬件I2C從機的程序,天天早上8點到凌晨,幾乎全程心流狀態。終於在結合各方資料及自己的思考後,做出了穩定的硬件I2C代碼(這個文章中應該是目前爲止能查到的最詳述可用的硬件I2C代碼),經過I2C主機發出的各種奇怪的信號蹂躪後,通訊都可以恢復正常,不會被卡死。證明該方案擁有極高穩定性。

需要注意我這次使用的是 STM32F103C8T6 的兼容型號 GD32F103C8T6 。要問他的兼容性有多強,連I2C bug都能做到一樣,哈哈。我當初用GD想着硬件I2C應該能舒服用了,萬萬沒想到,兆易連i2c 硬件BUG都複製了。

大家不要糾結於單片機的型號,我推測應該STM32FXXX 家族硬件I2C應該都是這個樣子,具體情況我也無法一一測試,如果大家看了文章在自己的系統上測試成功後別忘了留個言,說下自己系統的配置,方便後來人~

STM32 硬件I2C BUG簡述

相信很多接觸STM32的用戶在嘗試使用I2C代碼時都被警告過stm32 硬件I2C有bug,那麼這個bug具體是什麼,又是如何發生的呢,我們具體來分析一下,給我們之後代碼解決這個問題來做個鋪墊。

首先我們來看一下STM32 I2C的一個神奇的寄存器,SR2BUSY 位,具體他是做什麼的參考手冊中已經很清晰的描述,我就不多說了,我直接說它的問題。

當STM32 硬件I2C模塊在通訊(無論做主機還是從機)中遇到總線被佔用時,使得 STM32 在接管總線時發生總線仲裁失敗,進而失去對總線的控制,導致BUSY位被置位,且無法通過使用官方驅動庫自動清除。而後即表現爲鎖死狀態。

該情況多會出現在I2C通訊錯誤,或者從機程序接收到了非預定的I2C指令時產生。
關於這個情況,在官方的一篇 《2C 接口進入 Busy 狀態不能退出.pdf》 文檔中也有描述,也正是這篇文檔給了我思路,讓我解決了這個問題。
在這裏插入圖片描述在這裏插入圖片描述

BUG實例

由於我使用的是硬件I2C從機,所以以下內容均以I2C從機爲主要內容講述,(如果你使用的是主機也請耐心看完,因爲主機也可以按照該思路修改後解決)也可能之後我會補充主機內容(主要是I2C做主機時,從機大多時候都成熟器件,想要人爲產生BUG情況也蠻困難的)

首先我們來看一段我用邏輯分析儀抓取的硬件系統的波形, 和造成該波形的測試代碼(以下I2C寫指令由主機發出,主機爲其他公司的單片機承擔,不建議使用兩個STM32 硬件I2C對傳測試,否則都出BUG找問題都找不到)
在這裏插入圖片描述

    while (1)
    {
        while(HAL_I2C_Slave_Receive_IT(&hi2c1, &i2c_buff, 1) != HAL_OK)
        {
        }
        while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
        {
        }
        delay_ms(10);
    }
  • 問題分析:

我們可以看到,由於我的代碼中只有對一個I2C寫指令接收的內容,而後就進入了延時等待狀態,從而錯過了主機連着發送的第二條寫指令,我們的單片機甚至都沒有對其做ACK響應。此時我們用調試器查看STM32寄存器,就會發現 BUSY進入了鎖死狀態。第二次進入的HAL_I2C_Slave_Receive_IT() 函數中會在等待 BUSY 清零的查詢操作中超時退出,而後在之後的循環中再次循環這個過程。

而如果我們把循環中的延時去掉,我們會發現I2C的通訊就會變得正常了。即, 由於我們沒能對主機發出的第二條的I2C指令及時處理,STM32 I2C就會出問題。而這種情況在使用中是不可能避免的,主機任何一次的誤操作或誤編程都可能會導致我們的I2C鎖死。

解決方案

放上代碼

void i2c_reset()
{
    /* 開漏輸出,關閉I2C輸入通道,並嘗試將總線拉高 */
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8 | GPIO_PIN_9, GPIO_PIN_SET);

    // SCL PB8 拉高
    for (uint8_t i = 0; i < 10; i++) {

        if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8) == GPIO_PIN_SET)
        {
            rt_kprintf("retry %d\n", i);
            break;
        }
        /* 該延時循環的週期和時長,請根據你的實際主機對I2C通訊出錯的處理來修改 */
        rt_thread_mdelay(10);
    }

    /* 歸還總線控制權 */
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    /* 復位I2C */
    hi2c1.Instance->CR1 |= I2C_CR1_SWRST;
    hi2c1.Instance->CR1 &= ~I2C_CR1_SWRST;

    /* 重新初始化I2C */
    MX_I2C1_Init();
}

/*
 * I2C最大的問題在於如果主機隨意發送內容,而stm32作爲從機時, 接收到程序意圖之外的通訊內容後會卡死BUSY位。
 * 發生該情況後,可配置SCL SDA爲開漏輸出,關閉I2C輸入通道防止錯誤再次被觸發,而後嘗試拉高SCL SDA線,或者
 * 等待總線被主機釋放,這裏的情況依據具體主機的不同可根據實際操作判斷,最終我們需要等待I2C總線爲空閒即SCL
 * SDA線爲高電平保持時。將SCL SDA配回IIC,釋放總線控制權 ,使用 SWRST復位I2C,清除全部I2C寄存器內容,
 * (BUSY位也會在該操作中被清除) 並重新初始化I2C總線。
 */
void i2c_salve_thread(void *parameter)
{
    while (1)
    {
        if(HAL_I2C_Slave_Receive_IT(&hi2c1, &i2c_buff, 1) != HAL_OK)
        {
        	// I2C設備出現故障無法開啓接收
            i2c_reset();
        }

		// 檢測標誌位,防止I2C被二次開啓,導致BUG
        while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
        {
            rt_thread_mdelay(1);
        }
        rt_thread_mdelay(10);
    }
}

void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode)
{
    direction = TransferDirection;
}


如代碼中註釋,我們在檢測 HAL_I2C_Slave_Receive_IT() 返回超時或錯誤時,使用 i2c_reset() 函數來複位I2C硬件模塊以重啓I2C,使得STM32可以在主機恢復發送正常設定的I2C命令後可以恢復通訊。

i2c_reset() 函數核心操作思路如下:

  1. 配置 對應IO爲開漏輸出,以關閉I2C模塊的硬件輸入通道,防止後續的通訊繼續觸發BUG,並嘗試將總線拉高。

  2. 嘗試讓從機釋放總線,或等待主機釋放總線(此步驟根據你的實際系統爲準,最好查一下波形,看看具體故障表現是什麼,如果STM32是從機,那麼主機在通訊失敗後,大部分會在一定時間後超時並釋放總線。所以我的代碼中只是在一定時間內檢測總線是否被釋放。如果你是主機的話則可以按照 《2C 接口進入 Busy 狀態不能退出.pdf》 文檔中的方法,在SCL線上發送脈衝來使得從機釋放I2C總線)還是一句話:該步驟根據你的實際系統爲準,我們在這步中核心需求就是讓總線被釋放。

  3. 將引腳配置回I2C的模式,將總線歸還STM32的I2C模塊

  4. 將CR1寄存器的SWRST置位後再清零,以復位I2C模塊,在此過程中I2C除DR寄存器外所有寄存器都會被清空,包括BUSY位 (此處記憶有些模糊,不確定DR寄存器是否會被清空。DR爲I2C收發數據緩存寄存器)

  5. 重新初始化I2C,原因是我們上一步的復位操作清空了I2C的配置

加入這些代碼後,我們可以再次使用邏輯分析儀觀察波形,或通過串口打印來檢測程序運行。(由於邏輯分析儀波形太長,圖片放不下就不做展示了)我們可以看到,不管主機如何發送誘發錯誤的信息,我們的代碼都能讓 stm32在i2c復位後的第一次接收時工作正常。而如果主機按照預定協議,間隔發送指令時,通訊就會完全恢復正常,不會觸發i2c_reset() 函數。

小計

  1. 如果使用 HAL_I2C_Slave_Receive_IT() 函數接收了主機發送的讀取指令,並不會觸發BUG,主機會讀取回 0x00的數據。

  2. 例子中我使用了 RT-thread 實時操作系統,所以延時不大一樣。使用RTOS的延時時還可以讓我們在等待時,將CPU調度到其他線程使用,防止一個I2C佔用全部CPU週期。

  3. 有了故障處理程序後,我們就可以使用 HAL 庫中自帶的 I2C_TwoBoards_AdvComIT 例程來處理收發數據了。我使用的官方HAL庫例程路徑:

     D:\ST\STM32Cube\Repository\STM32Cube_FW_F1_V1.7.0\Projects\STM32F103RB-Nucleo\Examples\I2C\I2C_TwoBoards_AdvComIT
    
  4. i2c_reset() 核心操作思路的步驟順序有嚴格要求,隨意變更1~5操作的前後順序會導致BUG再次觸發

  5. IIC總線調試具有特殊性,我們最好還是準備一個邏輯分析儀來抓取波形,結合DEBUG時對寄存器的查看來分析解決故障

  6. 接上一條,在調試IIC前,我們應該確保自己對STM IIC的各個寄存器和位功能以及I2C波形時序的熟悉,以求在出現問題時能夠找到問題所在,新版的HAL庫IIC內容很清晰,只要瞭解寄存器,通過DEBUG+查看波形 的方法可以很快定位解決錯誤。

  7. 示例代碼僅提供思路,具體使用需要修改爲你的配置。

  8. 注意一定要使用新版本的HAL庫,我已經被舊版的有明顯錯誤的HAL I2C庫坑過了(居然直接對只讀的標誌位賦值,來想要清除標誌位)

總結

STM32的IIC模塊確實存在BUG, 具體表現就是在我們代碼沒有處理預設之外的IIC指令或數據時會發生由於總線仲裁失敗,導致BUSY位鎖死的問題。出現這種問題,只要我們能夠用代碼監測到此情況的發生,並使用上述的 i2c_reset()核心操作思路就可以解決,從而讓我們在實際工程應用中使用。畢竟軟件也許可以方便的模擬100khz的IIC主機,但對400khz I2c通訊來說,無論是主機還是從機都很難實現(從機的軟件模擬技巧性很強,且CPU消耗也大)。

最後

如果這個文章,解決了你的問題,請留個言交流下,因爲我目前精力有限也只是在自己的硬件上測試了這個內容,讓我知道你的問題被解決會讓我很開心,也會讓之後的朋友不在爲這個問題困擾~

因爲目前我的工程涉及到自己公司的產品,不方便直接發出,之後有空了,我會做一個示例工程掛到 githubgitee 上,方便大家修改使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章