議實(shí)戰(zhàn)指南:從時(shí)序解析到代碼實(shí)現(xiàn))
1. IIC協(xié)議基礎(chǔ)兩線(xiàn)制通信的藝術(shù)IICInter-Integrated Circuit總線(xiàn)就像城市里的雙向單車(chē)道只用兩根線(xiàn)SDA數(shù)據(jù)線(xiàn)和SCL時(shí)鐘線(xiàn)就能實(shí)現(xiàn)設(shè)備間的對(duì)話(huà)。我在調(diào)試第一塊AT24C02 EEPROM時(shí)曾因?yàn)椴焕斫膺@個(gè)設(shè)計(jì)原理把電路板上的上拉電阻焊成了下拉電阻結(jié)果整個(gè)系統(tǒng)通信癱瘓——這讓我深刻認(rèn)識(shí)到物理層設(shè)計(jì)的重要性。物理層核心三要素SDA雙向數(shù)據(jù)線(xiàn)相當(dāng)于對(duì)話(huà)中的內(nèi)容SCL時(shí)鐘線(xiàn)相當(dāng)于對(duì)話(huà)的節(jié)奏控制器上拉電阻通常3.3K-10K就像給對(duì)話(huà)雙方配的擴(kuò)音器實(shí)際項(xiàng)目中我發(fā)現(xiàn)一個(gè)有趣現(xiàn)象當(dāng)總線(xiàn)上掛載超過(guò)3個(gè)設(shè)備時(shí)上拉電阻值需要重新計(jì)算。有次用4.7K電阻驅(qū)動(dòng)5個(gè)傳感器結(jié)果信號(hào)上升沿出現(xiàn)振鈴后來(lái)用示波器抓波形才發(fā)現(xiàn)問(wèn)題。這里有個(gè)經(jīng)驗(yàn)公式Rp_max (Vcc - 0.4) / (3mA × 設(shè)備數(shù)量)。2. 協(xié)議層時(shí)序的魔鬼細(xì)節(jié)2.1 起止信號(hào)的微妙之處起始信號(hào)START的時(shí)序要求SCL高電平時(shí)SDA出現(xiàn)下降沿這個(gè)操作就像敲門(mén)——力度太輕里面聽(tīng)不見(jiàn)太重可能把門(mén)敲壞。我最早實(shí)現(xiàn)的代碼是這樣的void I2C_Start(void) { SDA_HIGH(); // 先確保SDA高 delay_us(1); SCL_HIGH(); // 再拉高SCL delay_us(4); // 這個(gè)延時(shí)很關(guān)鍵 SDA_LOW(); // 產(chǎn)生下降沿 delay_us(4); SCL_LOW(); // 鉗住總線(xiàn) }有次在-40℃低溫環(huán)境下測(cè)試發(fā)現(xiàn)通信失敗最后發(fā)現(xiàn)是delay_us(4)不夠——溫度會(huì)影響GPIO響應(yīng)速度。現(xiàn)在我的代碼里都會(huì)加環(huán)境補(bǔ)償#if defined(ENV_LOW_TEMP) #define I2C_DELAY 8 #else #define I2C_DELAY 4 #endif2.2 數(shù)據(jù)有效性窗口數(shù)據(jù)必須在SCL高電平期間保持穩(wěn)定這個(gè)要求就像在拍集體照時(shí)喊茄子的瞬間不能眨眼。有個(gè)經(jīng)典錯(cuò)誤案例某次我為了提升速度在SCL上升沿就改變SDA電平結(jié)果從設(shè)備收到的全是亂碼。正確的做法應(yīng)該是void I2C_SendBit(uint8_t bit) { SCL_LOW(); delay_us(2); if(bit) SDA_HIGH(); else SDA_LOW(); delay_us(2); // 數(shù)據(jù)建立時(shí)間 SCL_HIGH(); delay_us(4); // 數(shù)據(jù)保持時(shí)間 SCL_LOW(); // 準(zhǔn)備下一個(gè)bit }3. AT24C02實(shí)戰(zhàn)從讀寫(xiě)失敗到穩(wěn)定運(yùn)行3.1 器件地址的坑AT24C02的器件地址前4位固定是1010但很多新手會(huì)忽略頁(yè)選位。有次我調(diào)試時(shí)發(fā)現(xiàn)只能訪(fǎng)問(wèn)前128字節(jié)后來(lái)才發(fā)現(xiàn)原理圖上A2A1A0全部接地而代碼里卻寫(xiě)了0xA0|0x07。正確的7位地址應(yīng)該是#define EEPROM_ADDR 0x50 // 10100003.2 頁(yè)寫(xiě)入的玄機(jī)AT24C02的頁(yè)寫(xiě)緩沖只有8字節(jié)但最容易犯的錯(cuò)誤是跨頁(yè)寫(xiě)入。我曾寫(xiě)過(guò)這樣的危險(xiǎn)代碼void unsafe_write(uint8_t addr, uint8_t *data, uint8_t len) { I2C_Start(); I2C_SendByte(0xA0); I2C_SendByte(addr); for(int i0; ilen; i) { I2C_SendByte(data[i]); // 可能跨頁(yè) } I2C_Stop(); }安全寫(xiě)法應(yīng)該要處理頁(yè)邊界void safe_write(uint8_t addr, uint8_t *data, uint8_t len) { while(len 0) { uint8_t page_left 8 - (addr % 8); uint8_t write_len (len page_left) ? len : page_left; I2C_Start(); I2C_SendByte(0xA0); I2C_SendByte(addr); for(int i0; iwrite_len; i) { I2C_SendByte(data[i]); } I2C_Stop(); delay_ms(5); // 必須的寫(xiě)入周期等待 addr write_len; data write_len; len - write_len; } }4. 軟件IIC的進(jìn)階技巧4.1 超時(shí)處理機(jī)制原始代碼經(jīng)常缺少超時(shí)判斷我在工業(yè)現(xiàn)場(chǎng)遇到過(guò)總線(xiàn)死鎖的情況。改進(jìn)后的等待ACK函數(shù)應(yīng)該這樣寫(xiě)uint8_t I2C_WaitAck(void) { uint16_t timeout 1000; // 1ms超時(shí) SDA_INPUT(); // 切換為輸入模式 SCL_HIGH(); while(READ_SDA()) { if(--timeout 0) { I2C_Stop(); // 強(qiáng)制終止 return 1; // 超時(shí)錯(cuò)誤 } delay_us(1); } SCL_LOW(); return 0; // 正常應(yīng)答 }4.2 總線(xiàn)掃描工具調(diào)試多設(shè)備系統(tǒng)時(shí)我常備一個(gè)總線(xiàn)掃描函數(shù)void I2C_Scanner(void) { printf(Scanning I2C bus...\n); for(uint8_t addr 0x08; addr 0x78; addr) { I2C_Start(); uint8_t ack I2C_SendByte(addr 1); // 寫(xiě)模式 I2C_Stop(); if(!ack) { printf(Found device at 0x%02X\n, addr); } delay_ms(1); } }這個(gè)工具幫我發(fā)現(xiàn)過(guò)地址沖突的問(wèn)題兩個(gè)相同的溫濕度傳感器因?yàn)锳0引腳處理不當(dāng)出現(xiàn)了地址重疊。5. 硬件IIC與軟件模擬的抉擇STM32的硬件IIC外設(shè)曾經(jīng)臭名昭著但新一代芯片已經(jīng)改善很多。我的選擇原則是用硬件IIC當(dāng)速度要求高400kHz需要DMA支持主控資源緊張用軟件模擬當(dāng)需要跨平臺(tái)移植調(diào)試階段需要靈活修改時(shí)序引腳分配受限時(shí)有個(gè)性能對(duì)比測(cè)試數(shù)據(jù)指標(biāo)軟件模擬(72MHz)硬件IIC100kHz穩(wěn)定性良好優(yōu)秀400kHz實(shí)現(xiàn)勉強(qiáng)穩(wěn)定CPU占用率高達(dá)30%5%代碼復(fù)雜度高低在最近的一個(gè)物聯(lián)網(wǎng)項(xiàng)目中我混合使用兩種方式用硬件IIC處理傳感器數(shù)據(jù)采集用軟件IIC對(duì)接老式EEPROM這種組合方案既保證了性能又兼顧了兼容性。