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