動(dòng)開發(fā)實(shí)戰(zhàn):從協(xié)議原理到排障指南)
1. 從一根線說起I2C 到底解決了什么問題搞嵌入式開發(fā)的人遲早會(huì)跟 I2C 打交道。不管你是在 OpenHarmony 上接一顆溫濕度傳感器還是在 RK3568 上調(diào)試一顆觸摸屏I2C 幾乎無處不在。但很多人對(duì)它的理解停留在“兩根線一根 SCL 一根 SDA掛一堆設(shè)備”這個(gè)層面真到了設(shè)備不通、數(shù)據(jù)讀不出來的時(shí)候就抓瞎了。這篇內(nèi)容我打算把 I2C 從協(xié)議原理到 OpenHarmony 上的實(shí)戰(zhàn)用法再到排障思路完整地捋一遍。適合誰看如果你正在做 OpenHarmony 驅(qū)動(dòng)開發(fā)或者剛接觸嵌入式外設(shè)調(diào)試又或者你已經(jīng)在用 I2C 但遇到問題不知道怎么下手那這篇內(nèi)容應(yīng)該能幫到你。我會(huì)盡量用大白話把時(shí)序、設(shè)備樹配置、HDI 接口這些容易讓人懵的東西講清楚同時(shí)給出可以直接參考的操作步驟和排查方法。先說結(jié)論I2C 的核心價(jià)值在于用最少的引腳掛最多的設(shè)備。兩根線理論上可以掛 112 個(gè)設(shè)備7 位地址去掉保留地址每個(gè)設(shè)備有自己的地址主設(shè)備通過地址來區(qū)分跟誰說話。這在引腳資源緊張的嵌入式場(chǎng)景里簡(jiǎn)直是救命稻草。但代價(jià)是什么代價(jià)是它比 SPI 慢比 UART 復(fù)雜而且一旦總線上某個(gè)設(shè)備抽風(fēng)可能把整條總線拉死。所以會(huì)用 I2C 只是第一步會(huì)排障才是真正的分水嶺。2. I2C 協(xié)議核心機(jī)制拆解別被時(shí)序圖嚇到2.1 兩根線背后的電氣邏輯I2C 只有兩根信號(hào)線SCL串行時(shí)鐘線和SDA串行數(shù)據(jù)線。這兩根線都是開漏輸出什么意思呢就是每個(gè)設(shè)備只能把線拉低不能主動(dòng)拉高。線要變高靠的是上拉電阻。這就好比一個(gè)會(huì)議室里所有人只能舉手反對(duì)不能舉手贊成默認(rèn)狀態(tài)是“贊成”誰反對(duì)誰舉手把線拉低。這個(gè)設(shè)計(jì)帶來的好處是多設(shè)備可以同時(shí)掛在一根線上不會(huì)因?yàn)槟硞€(gè)設(shè)備輸出高電平而跟另一個(gè)輸出低電平的設(shè)備打架。因?yàn)檎l都不能輸出高電平只能拉低或者釋放釋放后由上拉電阻把線拉高。上拉電阻選多大這是第一個(gè)容易踩坑的地方。典型值是 4.7kΩ但這不是固定的。電阻越小上升沿越陡能跑的速度越高但功耗越大電阻越大功耗越低但上升沿變緩高速通信時(shí)可能還沒到高電平就被拉低了。經(jīng)驗(yàn)做法是標(biāo)準(zhǔn)模式100kHz用 4.7kΩ 到 10kΩ快速模式400kHz用 2.2kΩ 到 4.7kΩ快速模式1MHz用 1kΩ 左右。如果你總線上掛了比較多設(shè)備電容負(fù)載增大上拉電阻也要相應(yīng)減小。注意有些模塊自帶上拉電阻你再去主板上加一對(duì)并聯(lián)之后阻值變小可能導(dǎo)致上升沿過沖。調(diào)試時(shí)先用示波器看一眼波形比盲目換電阻靠譜得多。2.2 起始、停止、應(yīng)答三個(gè)必須刻在腦子里的動(dòng)作I2C 通信的每一個(gè)字節(jié)傳輸都圍繞三個(gè)基本動(dòng)作展開起始條件STARTSCL 保持高電平的時(shí)候SDA 從高變低。這個(gè)動(dòng)作只能由主設(shè)備發(fā)起意思是“我要開始說話了大家注意聽”。停止條件STOPSCL 保持高電平的時(shí)候SDA 從低變高。意思是“我說完了總線釋放”。應(yīng)答ACK/NACK每傳輸完 8 位數(shù)據(jù)接收方要在第 9 個(gè)時(shí)鐘周期把 SDA 拉低表示“收到了”。如果接收方不拉低就是 NACK表示“沒收到”或者“不要再發(fā)了”。這三個(gè)動(dòng)作是 I2C 排障時(shí)最直接的觀察點(diǎn)。用邏輯分析儀抓波形第一眼就看起始條件有沒有、地址發(fā)出去之后有沒有 ACK。如果地址階段就 NACK說明從設(shè)備根本沒響應(yīng)要么地址錯(cuò)了要么設(shè)備沒上電要么線沒接好。如果數(shù)據(jù)階段 NACK可能是從設(shè)備忙不過來或者寄存器地址不合法。2.3 7 位地址和 10 位地址實(shí)際用哪個(gè)I2C 支持 7 位和 10 位兩種地址格式。7 位地址是主流一個(gè)字節(jié)里高 7 位是地址最低位是讀寫標(biāo)志位0 寫1 讀。10 位地址用得少一般只在 7 位地址不夠分配的時(shí)候才用。實(shí)際開發(fā)中你拿到的傳感器手冊(cè)上寫的地址通常是 7 位地址。但有些手冊(cè)給的是 8 位地址已經(jīng)把讀寫位算進(jìn)去了這時(shí)候你要自己右移一位。比如手冊(cè)寫 0x92實(shí)際 7 位地址是 0x49。這個(gè)坑我踩過不止一次地址對(duì)不上怎么調(diào)都不通最后發(fā)現(xiàn)是手冊(cè)給的是 8 位格式。2.4 時(shí)鐘拉伸從設(shè)備也會(huì)“拖堂”I2C 有一個(gè)很人性化的機(jī)制叫時(shí)鐘拉伸Clock Stretching。從設(shè)備如果處理不過來可以在接收完一個(gè)字節(jié)后把 SCL 拉低強(qiáng)制主設(shè)備等待。等從設(shè)備準(zhǔn)備好了再釋放 SCL通信繼續(xù)。這個(gè)機(jī)制在調(diào)試低速傳感器時(shí)經(jīng)常遇到。比如某些溫濕度傳感器轉(zhuǎn)換一次需要幾十毫秒它就會(huì)在轉(zhuǎn)換期間拉低 SCL。如果你用的主控不支持時(shí)鐘拉伸或者驅(qū)動(dòng)里沒處理這個(gè)情況就會(huì)讀出錯(cuò)數(shù)據(jù)。排查時(shí)如果發(fā)現(xiàn) SCL 被長(zhǎng)時(shí)間拉低先別急著懷疑硬件壞了看看是不是從設(shè)備在忙。3. OpenHarmony 下的 I2C 驅(qū)動(dòng)框架HDI 接口怎么用3.1 從 HDF 到 HDIOpenHarmony 的驅(qū)動(dòng)分層OpenHarmony 的驅(qū)動(dòng)框架叫 HDFHardware Driver Foundation它把驅(qū)動(dòng)分成內(nèi)核態(tài)和用戶態(tài)兩部分。I2C 作為一種標(biāo)準(zhǔn)總線在 HDF 里有對(duì)應(yīng)的抽象層。而 HDIHardware Device Interface是給上層應(yīng)用提供的統(tǒng)一接口讓應(yīng)用不需要關(guān)心底層是哪個(gè) SoC、哪個(gè) I2C 控制器。具體來說OpenHarmony 的 I2C 驅(qū)動(dòng)棧大概是這樣最底層是 SoC 的 I2C 控制器驅(qū)動(dòng)負(fù)責(zé)操作寄存器、產(chǎn)生時(shí)序。中間是 HDF 的 I2C 核心層提供統(tǒng)一的 I2C 設(shè)備管理。上層是 HDI 接口暴露給應(yīng)用層調(diào)用。你在應(yīng)用層調(diào)用的I2cOpen、I2cTransfer這些接口最終會(huì)走到內(nèi)核里的控制器驅(qū)動(dòng)完成實(shí)際的波形收發(fā)。3.2 設(shè)備樹配置I2C 設(shè)備怎么“掛上去”在 OpenHarmony 里I2C 設(shè)備的配置通常寫在設(shè)備樹Device Tree里。設(shè)備樹的作用是告訴內(nèi)核哪個(gè) I2C 控制器使能了、總線上掛了哪些設(shè)備、每個(gè)設(shè)備的地址是多少、用哪個(gè)驅(qū)動(dòng)。一個(gè)典型的 I2C 設(shè)備樹節(jié)點(diǎn)長(zhǎng)這樣i2c1 { status okay; clock-frequency 400000; sensor48 { compatible vendor,tmp102; reg 0x48; status okay; }; };這里幾個(gè)關(guān)鍵點(diǎn)status okay表示這個(gè) I2C 控制器使能了。如果寫成disabled整條總線都不工作。clock-frequency是總線時(shí)鐘頻率單位 Hz。400000 就是 400kHz。reg 0x48是從設(shè)備的 7 位地址。compatible是驅(qū)動(dòng)匹配字符串內(nèi)核靠它找到對(duì)應(yīng)的驅(qū)動(dòng)。常見坑點(diǎn)地址寫錯(cuò)、頻率設(shè)太高導(dǎo)致通信不穩(wěn)定、status忘了改。我遇到過好幾次設(shè)備樹里status默認(rèn)是disabled改完忘了編譯進(jìn)內(nèi)核結(jié)果怎么調(diào)都不通。3.3 HDI 接口調(diào)用流程在 OpenHarmony 應(yīng)用層或 HAL 層通過 HDI 接口操作 I2C 的大致流程是I2cOpen(busNum)打開指定編號(hào)的 I2C 控制器拿到一個(gè)句柄。構(gòu)造I2cMsg數(shù)組每個(gè) Msg 包含從設(shè)備地址、讀寫標(biāo)志、數(shù)據(jù)緩沖區(qū)、數(shù)據(jù)長(zhǎng)度。I2cTransfer(handle, msgs, count)執(zhí)行傳輸。I2cClose(handle)關(guān)閉句柄。一個(gè)讀寄存器的典型操作是兩次傳輸先寫寄存器地址再讀數(shù)據(jù)。有些驅(qū)動(dòng)支持組合傳輸Repeated START可以在一次 Transfer 里完成。I2cMsg msgs[2]; msgs[0].addr 0x48; msgs[0].flags 0; // 寫 msgs[0].buf regAddr; msgs[0].len 1; msgs[1].addr 0x48; msgs[1].flags I2C_FLAG_READ; msgs[1].buf readBuf; msgs[1].len 2; I2cTransfer(handle, msgs, 2);提示組合傳輸時(shí)兩次 Msg 之間會(huì)產(chǎn)生 Repeated START而不是 STOP 再 START。這個(gè)區(qū)別在有些從設(shè)備上是致命的必須用 Repeated START 才能正確讀取。4. 實(shí)操在 RK3568 上點(diǎn)亮一顆 I2C 傳感器4.1 硬件準(zhǔn)備和接線檢查我拿 RK3568 開發(fā)板接一顆 TMP102 溫度傳感器來演示。接線很簡(jiǎn)單VCC 接 3.3VGND 接 GNDSCL 接 I2C1_SCLSDA 接 I2C1_SDAADD0 接地決定地址是 0x48接完線第一件事不是上電寫代碼而是用萬用表測(cè)一下 SCL 和 SDA 對(duì)地的電阻。如果阻值接近 0說明線短路了先查線。正常應(yīng)該有上拉電阻的阻值比如 4.7kΩ 左右。然后上電用示波器或者邏輯分析儀看 SCL 和 SDA 的靜態(tài)電平。正常應(yīng)該是高電平。如果是低電平說明總線被某個(gè)設(shè)備拉死了可能是設(shè)備壞了或者地址沖突。4.2 設(shè)備樹修改和內(nèi)核編譯在 RK3568 的 SDK 里找到對(duì)應(yīng)的設(shè)備樹文件通常是kernel/arch/arm64/boot/dts/rockchip/rk3568-xxx.dts。找到i2c1節(jié)點(diǎn)改成i2c1 { status okay; clock-frequency 100000; tmp10248 { compatible ti,tmp102; reg 0x48; status okay; }; };這里我故意把頻率設(shè)成 100kHz因?yàn)榈谝淮握{(diào)試低速更穩(wěn)。等通了再往上提。編譯內(nèi)核和設(shè)備樹燒錄重啟。然后進(jìn)系統(tǒng)用i2cdetect工具掃一下總線i2cdetect -y 1如果看到地址 0x48 處顯示48說明設(shè)備被識(shí)別到了。如果顯示--說明沒識(shí)別到回去查接線和設(shè)備樹。4.3 用 HDI 接口讀取溫度數(shù)據(jù)設(shè)備識(shí)別到之后寫一個(gè)簡(jiǎn)單的測(cè)試程序通過 HDI 接口讀溫度寄存器TMP102 的溫度寄存器地址是 0x00int16_t read_temperature(int handle) { uint8_t reg 0x00; uint8_t buf[2] {0}; I2cMsg msgs[2]; msgs[0].addr 0x48; msgs[0].flags 0; msgs[0].buf reg; msgs[0].len 1; msgs[1].addr 0x48; msgs[1].flags I2C_FLAG_READ; msgs[1].buf buf; msgs[1].len 2; if (I2cTransfer(handle, msgs, 2) ! 0) { return -1; } int16_t raw (buf[0] 8) | buf[1]; return raw 4; // TMP102 是 12 位數(shù)據(jù)右移 4 位 }讀出來的 raw 值乘以 0.0625 就是攝氏度。比如 raw 是 400溫度就是 25°C。4.4 邏輯分析儀抓波形驗(yàn)證代碼跑通之后我習(xí)慣用邏輯分析儀抓一段波形確認(rèn)時(shí)序沒問題。重點(diǎn)看幾個(gè)地方起始條件是否干凈地址 0x48 寫操作后是否有 ACK寄存器地址 0x00 寫完后是否有 Repeated START讀操作的兩個(gè)字節(jié)后主設(shè)備是否發(fā)了 NACK 再 STOP如果這些都對(duì)說明通信完全正常。如果哪里不對(duì)波形會(huì)直接告訴你問題出在哪個(gè)階段。5. I2C 排障實(shí)戰(zhàn)從現(xiàn)象到根因的排查路徑5.1 常見問題速查表現(xiàn)象可能原因排查方法掃描不到設(shè)備接線錯(cuò)誤、設(shè)備沒上電、地址不對(duì)萬用表測(cè)電壓、示波器看波形、確認(rèn)地址格式地址階段 NACK從設(shè)備地址錯(cuò)誤、設(shè)備未就緒核對(duì)手冊(cè)地址、檢查上電時(shí)序數(shù)據(jù)階段 NACK寄存器地址不合法、設(shè)備忙查手冊(cè)確認(rèn)寄存器、增加延時(shí)讀數(shù)據(jù)全 0 或全 FF時(shí)序問題、時(shí)鐘拉伸未處理邏輯分析儀抓波形、降低時(shí)鐘頻率總線被拉死某個(gè)設(shè)備故障、地址沖突逐個(gè)斷開設(shè)備、測(cè)靜態(tài)電平通信偶爾出錯(cuò)上拉電阻不合適、線太長(zhǎng)調(diào)整上拉電阻、縮短走線5.2 典型排障案例GT911 觸摸屏 I2C 通信失敗GT911 是一顆常見的觸摸屏控制器用 I2C 通信。我遇到過好幾次 GT911 通信失敗的情況總結(jié)下來大概有幾種原因第一種上電時(shí)序不對(duì)。GT911 對(duì)復(fù)位和上電的順序有要求如果復(fù)位引腳和電源的上電順序錯(cuò)了芯片可能不響應(yīng) I2C。解決方法是嚴(yán)格按照手冊(cè)的時(shí)序圖先給電再釋放復(fù)位中間加足夠的延時(shí)。第二種地址沖突。GT911 的 I2C 地址可以通過 INT 引腳在上電時(shí)決定是 0x5D 還是 0x14。如果 INT 引腳狀態(tài)不對(duì)地址就錯(cuò)了。排查時(shí)先用i2cdetect掃一遍看看到底出現(xiàn)在哪個(gè)地址。第三種中斷引腳配置錯(cuò)誤。GT911 用中斷引腳通知主控有觸摸事件如果中斷引腳沒配置好雖然 I2C 能通但讀不到數(shù)據(jù)。這時(shí)候要檢查設(shè)備樹里中斷引腳的配置。5.3 總線死鎖最頭疼的情況I2C 總線死鎖是嵌入式開發(fā)里最讓人頭疼的問題之一?,F(xiàn)象是 SCL 或 SDA 被某個(gè)設(shè)備一直拉低主設(shè)備無法發(fā)起新的通信。造成死鎖的常見原因主設(shè)備在從設(shè)備還沒釋放 SDA 的時(shí)候就發(fā)了 STOP或者從設(shè)備在傳輸過程中復(fù)位了導(dǎo)致它一直拉著 SDA 不放?;謴?fù)方法給從設(shè)備發(fā) 9 個(gè)時(shí)鐘脈沖讓它在第 9 個(gè)脈沖后釋放 SDA然后發(fā)一個(gè) STOP 條件。很多 SoC 的 I2C 控制器支持總線恢復(fù)功能可以在驅(qū)動(dòng)里配置。實(shí)操心得如果總線上掛了多個(gè)設(shè)備建議每個(gè)設(shè)備的電源單獨(dú)控制出問題時(shí)可以逐個(gè)斷電排查。另外在 SDA 和 SCL 上預(yù)留測(cè)試點(diǎn)方便接邏輯分析儀。5.4 時(shí)鐘頻率和上拉電阻的聯(lián)合調(diào)試很多時(shí)候通信不穩(wěn)定不是代碼問題而是時(shí)鐘頻率和上拉電阻不匹配。頻率越高對(duì)上升沿的要求越陡上拉電阻就要越小。但電阻太小功耗又上去了。我的經(jīng)驗(yàn)做法是先用 100kHz 和 4.7kΩ 跑通然后逐步提高頻率同時(shí)用示波器觀察上升沿。如果上升沿變緩就減小上拉電阻。最終找到一個(gè)穩(wěn)定工作的組合。另外總線電容也是影響因素。線越長(zhǎng)、掛的設(shè)備越多電容越大上升沿越緩。如果總線電容超過 400pF標(biāo)準(zhǔn)模式都可能跑不穩(wěn)。這時(shí)候要么縮短線要么用 I2C 緩沖器/中繼器。6. 進(jìn)階話題I2C 擴(kuò)展和多路復(fù)用6.1 地址沖突怎么辦I2C 多路復(fù)用器當(dāng)你需要掛多個(gè)相同型號(hào)的傳感器時(shí)地址沖突就來了。比如你要接 4 顆同樣的溫度傳感器它們的地址都是 0x48沒法直接掛在一起。解決方案是用I2C 多路復(fù)用器比如 TCA9548A。它本身是一個(gè) I2C 從設(shè)備有 8 個(gè)下游通道。你通過寫它的寄存器來選擇哪個(gè)通道導(dǎo)通這樣每個(gè)通道上掛一個(gè) 0x48 的傳感器互不干擾。在 OpenHarmony 里使用多路復(fù)用器需要在設(shè)備樹里把多路復(fù)用器作為 I2C 設(shè)備配好然后在驅(qū)動(dòng)里先寫多路復(fù)用器選擇通道再操作下游設(shè)備。6.2 軟件 I2C vs 硬件 I2C有些場(chǎng)景下硬件 I2C 控制器不夠用或者引腳被占用了就需要用 GPIO 模擬 I2C也就是軟件 I2C。軟件 I2C 的優(yōu)點(diǎn)是靈活任意 GPIO 都能用缺點(diǎn)是占用 CPU速度慢時(shí)序精度不如硬件。在 OpenHarmony 里如果要用軟件 I2C需要自己實(shí)現(xiàn)時(shí)序控制或者用內(nèi)核提供的i2c-gpio驅(qū)動(dòng)。我的建議是能用硬件 I2C 就用硬件軟件 I2C 只作為備選方案。特別是高速通信場(chǎng)景軟件 I2C 很容易出問題。6.3 I2C 和 SMBus 的區(qū)別SMBus 是基于 I2C 的一個(gè)子集主要用于電源管理和系統(tǒng)監(jiān)控。它比 I2C 多了超時(shí)機(jī)制時(shí)鐘頻率固定在 10kHz 到 100kHz 之間。實(shí)際開發(fā)中很多電源管理芯片用的是 SMBus但物理層跟 I2C 兼容。如果你用 I2C 驅(qū)動(dòng)去操作 SMBus 設(shè)備大部分情況下能通但要注意超時(shí)和協(xié)議細(xì)節(jié)的差異。7. 幾個(gè)容易忽略的細(xì)節(jié)和實(shí)操建議7.1 地址格式的坑再?gòu)?qiáng)調(diào)一遍手冊(cè)給的地址可能是 8 位格式實(shí)際用的時(shí)候要右移一位。我見過太多人在這上面浪費(fèi)時(shí)間。拿到地址后先確認(rèn)是 7 位還是 8 位不確定就用i2cdetect掃一遍看實(shí)際出現(xiàn)在哪個(gè)地址。7.2 上電順序和復(fù)位時(shí)序很多 I2C 設(shè)備對(duì)電源和復(fù)位的順序有要求。比如某些傳感器要求先給電等電源穩(wěn)定后再釋放復(fù)位中間要延時(shí)幾毫秒。如果順序錯(cuò)了設(shè)備可能不響應(yīng) I2C。排查時(shí)先用示波器看電源和復(fù)位引腳的波形確認(rèn)時(shí)序符合手冊(cè)要求。7.3 中斷引腳不要忘很多 I2C 設(shè)備有中斷引腳用來通知主控?cái)?shù)據(jù)準(zhǔn)備好了。如果你只配了 I2C 沒配中斷雖然能輪詢讀數(shù)據(jù)但效率低而且可能錯(cuò)過快速變化的事件。設(shè)備樹里記得把中斷引腳也配好。7.4 邏輯分析儀是必備工具調(diào)試 I2C邏輯分析儀比萬用表有用得多。它能直接告訴你起始條件、地址、ACK、數(shù)據(jù)、停止條件一眼就能看出問題在哪。便宜的邏輯分析儀幾十塊錢但能省你幾個(gè)小時(shí)甚至幾天的調(diào)試時(shí)間。7.5 設(shè)備樹編譯要確認(rèn)改完設(shè)備樹一定要確認(rèn)編譯進(jìn)了內(nèi)核。有時(shí)候改了 dts 但沒重新編譯或者編譯了但燒錄的不是新的都會(huì)導(dǎo)致配置不生效。我習(xí)慣改完之后用fdtdump或者/proc/device-tree確認(rèn)一下實(shí)際生效的配置。8. 寫在最后一些個(gè)人體會(huì)I2C 這個(gè)東西入門容易精通難。協(xié)議本身不復(fù)雜但實(shí)際調(diào)試中遇到的問題千奇百怪。我的經(jīng)驗(yàn)是遇到問題先別改代碼先用工具看波形。波形不會(huì)騙人它會(huì)直接告訴你問題出在哪個(gè)階段。另外設(shè)備樹配置和硬件接線是兩大高頻問題源。很多時(shí)候代碼沒問題就是設(shè)備樹里某個(gè)參數(shù)寫錯(cuò)了或者接線松了。養(yǎng)成“先查硬件再查軟件”的習(xí)慣能省很多時(shí)間。最后分享一個(gè)小技巧如果你在 OpenHarmony 上調(diào)試 I2C 設(shè)備可以先用 Linux 下的i2c-tools驗(yàn)證硬件通路確認(rèn)設(shè)備能掃到、能讀寫再去寫 HDI 代碼。這樣能把硬件問題和軟件問題分開排查起來更有條理。