問題描述
最近在復用以前STM32F407控制OLED的代碼,移植到STM32F103 上,使用硬件 I2C 通信方式。按照常規流程,先發送 OLED 的從機地址,OLED 有正常應答,但當發送第一個控制命令(0xAE)前的控制字節(0x00)時,程序卡在了while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_TXE))這一行,始終等待不到 TXE 標志置位。
更讓人疑惑的是,同樣的 OLED 模塊,用 ESP32 通過 U8G2 庫控制時完全正常,說明 OLED 硬件本身沒問題,問題大概率出在 STM32 的 I2C 配置或通信邏輯上。
解決過程
嘗試解決
一開始,我以為是控制字節發送錯誤,反復確認后發現發送的是正確的 0x00(命令控制字節),排除了這個可能。
接著懷疑是 I2C 地址錯誤,用示波器抓取波形,確認從機地址發送正確且 OLED 有應答,這一步也沒問題。
然后檢查 I2C 初始化配置,時鐘頻率、ACK 使能等參數都設置正確,GPIO 也配置成了復用開漏輸出模式,硬件使用 ESP32說明也沒問題。
最后把目光聚焦在 I2C 狀態標志的處理上。代碼流程大概是這樣的:
static void iic1_send_byte(uint8_t addr, uint8_t data)
{while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));// 起始信號I2C_GenerateSTART(I2C1, ENABLE);while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_SB));// 發送從機地址sI2C_Send7bitAddress(I2C1, 0x78, I2C_Direction_Transmitter);while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_ADDR));// 發送模式I2C_SendData(I2C1, addr);while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_TXE));I2C_SendData(I2C1, data); while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_TXE));// 發送停止信號I2C_GenerateSTOP(I2C1, ENABLE);
}
正確的解決方法
在反復查閱 STM32 參考手冊并嘗試多種方法后,發現問題出在ADDR標志的清除方式上。STM32 的 I2C 硬件設計規定,ADDR標志必須通過先讀取 SR1 寄存器,再讀取 SR2 寄存器的方式才能清除。
修改后的代碼如下:
static void iic1_send_byte(uint8_t addr, uint8_t data)
{while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));// 起始信號I2C_GenerateSTART(I2C1, ENABLE);while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_SB));// 發送從機地址sI2C_Send7bitAddress(I2C1, 0x78, I2C_Direction_Transmitter);while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_ADDR));//! 清除I2C_FLAG_ADDR標志// ----------------------------------------------I2C_ReadRegister(I2C1, I2C_Register_SR1);I2C_ReadRegister(I2C1, I2C_Register_SR2);// ----------------------------------------------// 發送模式I2C_SendData(I2C1, addr);while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_TXE));I2C_SendData(I2C1, data); while(!I2C_GetFlagStatus(I2C1, I2C_FLAG_TXE));// 發送停止信號I2C_GenerateSTOP(I2C1, ENABLE);
}
增加讀取 SR2 寄存器的操作后,程序不再卡死,TXE 標志能正常置位,控制命令也順利發送給了 OLED,問題終于解決。
思考:為什么不這么做可以
解決問題后,我心中產生了一個疑問:為什么網上很多 STM32 控制 OLED 的教程代碼里,都沒有顯式地同時讀取 SR1 和 SR2 寄存器,代碼卻能正常工作,經過分析,主要有以下幾個原因:
1. 使用 HAL 庫封裝了底層操作
現在很多教程采用 HAL 庫進行開發,HAL 庫的 I2C 相關函數(如HAL_I2C_Master_Transmit)內部已經封裝了ADDR標志的清除邏輯,包括先讀 SR1 再讀 SR2 的操作。用戶在調用這些庫函數時,不需要關心底層寄存器的操作,所以教程代碼里看不到相關讀取操作,但實際上底層已經處理了。
2. 采用軟件 I2C 方式
其實在CSDN中大部分教程也是軟件I2C,為了簡化操作,采用軟件模擬 I2C 時序的方式控制 OLED。軟件 I2C 通過直接操作 GPIO 引腳來模擬 I2C 通信,不涉及 STM32 的 I2C 硬件外設,自然不需要處理 SR1 和 SR2 寄存器。
3. 不規范代碼的偶然工作
極少數情況下,一些不規范的代碼(比如只讀取 SR1 寄存器,甚至不讀取任何寄存器)可能在特定條件下偶然工作。這可能是因為 I2C 總線速率極低,或者使用的 STM32 型號存在硬件特性,使得ADDR標志在未正確清除的情況下,狀態機也能進入數據發送階段。但這種情況很不穩定,換個環境或芯片型號可能就會出現問題。
總的來說,直接操作 STM32 I2C 硬件外設時,嚴格按照參考手冊的要求,通過先讀 SR1 再讀 SR2 的方式清除ADDR標志,才是規范且可靠的做法。