在參加全國光電設計大賽期間,專案採用 PX4FLOW 光流感測器。初期使用軟體模擬 I2C(GPIO 模擬)讀取資料,卻發生不定期死機現象。深入查閱 PX4FLOW 官方原始碼後發現,該感測器內部採用 STM32 硬體 I2C 外設,對時序要求極嚴格。軟體模擬若延遲不足或時序不精確,極易導致通訊鎖死。本文記錄在「原子哥」I2C 範例碼基礎上,透過調整關鍵延遲參數,成功解決死機問題的完整修正過程與程式碼。
delay_us() 精度不足、中斷干擾、時鐘拉高/拉低邊沿順序錯誤,皆會導致從機誤判狀態。| 函式 | 關鍵修改 | 說明 |
|---|---|---|
IIC_Start() | 調整 SDA/SCL 邊沿切換順序與延遲 | 確保 START 訊號在 SCL 高電位時 SDA 由高變低,滿足.setup time |
IIC_Stop() | 增加 SCL 高電位期間的 SDA 上升延遲 | 符合 STOP 訊號 hold time 需求 |
IIC_Wait_Ack() | 加入 ucErrTime 超時計數器(250 迴路) | 避免從機不回 ACK 時主機永久阻塞,超時自動發送 STOP 並返回錯誤 |
IIC_Send_Byte() | 每個位元傳輸週期統一 delay_us(2) | 確保資料建立/保持時間,特別針對 TEA5767 等嚴格裝置驗證過 |
IIC_Read_Byte() | 讀取位元前拉低 SCL、延遲 2us 再拉高採樣 | 符合資料輸出延遲規格 |
#include "myiic.h"
#include "delay.h"
// 初始化 IIC GPIO(PB10=SCL, PB11=SDA)
void IIC_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
IIC_SCL = 1;
IIC_SDA = 1;
}
// 產生 START 訊號
void IIC_Start(void) {
SDA_OUT();
IIC_SDA = 1;
delay_us(1);
IIC_SCL = 1;
delay_us(2); // 修正:增加 setup time
IIC_SDA = 0; // START: SCL high, SDA high->low
delay_us(2); // 修正:增加 hold time
IIC_SCL = 0; // 鉗住總線
}
// 產生 STOP 訊號
void IIC_Stop(void) {
SDA_OUT();
IIC_SCL = 0;
delay_us(1);
IIC_SDA = 0;
delay_us(2);
IIC_SCL = 1;
delay_us(1);
IIC_SDA = 1; // STOP: SCL high, SDA low->high
delay_us(2);
}
// 等待 ACK,超時返回 1
u8 IIC_Wait_Ack(void) {
u8 ucErrTime = 0;
SDA_IN();
IIC_SDA = 1;
delay_us(1);
IIC_SCL = 1;
delay_us(1);
while (READ_SDA) {
ucErrTime++;
if (ucErrTime > 250) { // 超時保護
IIC_Stop();
return 1;
}
}
IIC_SCL = 0;
return 0;
}
// 發送 ACK
void IIC_Ack(void) {
IIC_SCL = 0;
delay_us(1);
SDA_OUT();
IIC_SDA = 0;
delay_us(2);
IIC_SCL = 1;
delay_us(2);
IIC_SCL = 0;
}
// 發送 NACK
void IIC_NAck(void) {
IIC_SCL = 0;
delay_us(1);
SDA_OUT();
IIC_SDA = 1;
delay_us(2);
IIC_SCL = 1;
delay_us(2);
IIC_SCL = 0;
}
// 發送一個位元組
void IIC_Send_Byte(u8 txd) {
u8 t;
SDA_OUT();
IIC_SCL = 0;
for (t = 0; t < 8; t++) {
IIC_SDA = (txd & 0x80) >> 7;
txd <<= 1;
delay_us(2); // 關鍵延遲
IIC_SCL = 1;
delay_us(2);
IIC_SCL = 0;
delay_us(2);
}
}
// 讀取一個位元組
u8 IIC_Read_Byte(unsigned char ack) {
unsigned char i, receive = 0;
SDA_IN();
for (i = 0; i < 8; i++) {
IIC_SCL = 0;
delay_us(2);
IIC_SCL = 1;
receive <<= 1;
if (READ_SDA) receive++;
delay_us(1);
}
if (!ack) IIC_NAck(); else IIC_Ack();
return receive;
}
💡 經驗分享: 軟體模擬 I2C 在高速、時序敏感的從機面前極其脆弱。若條件允許,優先使用硬體 I2C 外設;必須用軟體模擬時,請務必示波器驗證時序,並加入完善的超時重試邏輯。