首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

STM32 軟體模擬 I2C 讀取 PX4FLOW 光流感測器資料:解決死機問題的關鍵時序修正

     編輯:關於C語言

STM32 軟體模擬 I2C 讀取 PX4FLOW 光流感測器資料:解決死機問題的關鍵時序修正

在參加全國光電設計大賽期間,專案採用 PX4FLOW 光流感測器。初期使用軟體模擬 I2C(GPIO 模擬)讀取資料,卻發生不定期死機現象。深入查閱 PX4FLOW 官方原始碼後發現,該感測器內部採用 STM32 硬體 I2C 外設,對時序要求極嚴格。軟體模擬若延遲不足或時序不精確,極易導致通訊鎖死。本文記錄在「原子哥」I2C 範例碼基礎上,透過調整關鍵延遲參數,成功解決死機問題的完整修正過程與程式碼。

問題背景

  • 現象: 軟體 I2C 讀取 PX4FLOW 運作一段時間後,感測器停止回應 ACK,主控制器陷入無限等待。
  • 原因分析: PX4FLOW 內部使用硬體 I2C,時序容忍度低;軟體模擬的 delay_us() 精度不足、中斷干擾、時鐘拉高/拉低邊沿順序錯誤,皆會導致從機誤判狀態。
  • 解決策略: 參考官方硬體 I2C 時序圖,對啟始/停止訊號、ACK 等待、位元傳輸延遲進行微調,並加入超時保護機制。

核心修正點總覽

函式關鍵修改說明
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 再拉高採樣符合資料輸出延遲規格

完整修正後程式碼(myiic.c)

#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;
}

驗證結果

  • 修正前:平均運行 10~30 分鐘死機一次。
  • 修正後:連續壓力測試 48 小時零死機,資料讀取穩定。
  • 關鍵差異在於 每個位元週期固定 2µs 延遲 以及 ACK 等待超時機制,確保總線不會永久鎖死。

💡 經驗分享: 軟體模擬 I2C 在高速、時序敏感的從機面前極其脆弱。若條件允許,優先使用硬體 I2C 外設;必須用軟體模擬時,請務必示波器驗證時序,並加入完善的超時重試邏輯。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved