:一周攻堅與避坑指南)
1. 為什么I2C值得花一周時間徹底吃透很多人第一次接觸I2C覺得它簡單——兩根線一根時鐘一根數(shù)據(jù)掛幾個從設備寫個時序就能跑。但真正做過項目的人都知道I2C是那種“入門五分鐘精通五年”的協(xié)議。你隨便找個做過嵌入式或者FPGA的工程師問問十有八九都踩過I2C的坑上拉電阻選大了波形爬不上去選小了功耗又扛不住多主設備搶總線的時候仲裁丟了數(shù)據(jù)從設備時鐘拉伸把主設備拖死PCB走線稍微長一點就開始出現(xiàn)莫名其妙的NACK。我自己第一次用Verilog寫I2C控制器的時候覺得不就是個狀態(tài)機嘛結果調(diào)了整整三天波形抓了幾十張最后發(fā)現(xiàn)是開漏輸出沒配對SDA線根本沒有被真正釋放。從那以后我就下定決心把I2C從物理層到協(xié)議層到RTL實現(xiàn)全部捋一遍。這篇文章就是那次“一周攻堅”的完整總結我會把開漏物理層的電氣原理、多主仲裁的逐位競爭機制、RTL狀態(tài)機的設計思路、以及實際調(diào)試中遇到的各種坑全部攤開來講。不管你是剛學Verilog的學生還是正在做項目的嵌入式工程師或者是在調(diào)試I2C總線的硬件工程師這篇內(nèi)容都能幫你把I2C的底層邏輯徹底搞清楚。我不會只給你講“怎么做”還會告訴你“為什么必須這么做”——因為I2C的很多設計決策背后都有非常實際的電氣和邏輯考量。2. 開漏物理層兩根線背后的電氣博弈2.1 開漏輸出的本質(zhì)為什么不能用推挽I2C最容易被忽視的就是物理層。很多人寫代碼的時候只關心時序覺得電平翻轉(zhuǎn)就是0和1的事。但I2C的總線結構決定了它必須用開漏輸出Open-Drain這不是隨便選的而是被總線共享的本質(zhì)需求逼出來的。先說什么叫開漏。一個普通的CMOS推挽輸出內(nèi)部有上管和下管兩個MOSFET輸出高的時候上管導通接到VDD輸出低的時候下管導通接到GND。這種結構輸出阻抗很低驅(qū)動能力強但有一個致命問題你絕對不能把兩個推挽輸出直接連在一起。如果一個設備輸出高、另一個輸出低那就是VDD直接短路到GND瞬間大電流燒毀器件。I2C總線上掛多個設備每個設備的SDA和SCL都連在同一條線上。如果用推挽輸出只要有兩個設備同時想輸出不同電平總線就短路了。所以I2C規(guī)定所有設備的輸出級只能是開漏結構——只能主動拉低不能主動拉高。輸出高的時候MOS管截止引腳呈高阻態(tài)相當于“放手”輸出低的時候MOS管導通把線拉到GND。那高電平從哪來靠上拉電阻。總線上的上拉電阻把線拉到VDD當所有設備都“放手”的時候線就是高電平。任何一個設備拉低線就是低電平。這就實現(xiàn)了“線與”邏輯只要有一個設備拉低總線就是低。注意開漏輸出配合上拉電阻本質(zhì)上實現(xiàn)的是一種“分布式與邏輯”。總線的狀態(tài)是所有設備輸出的邏輯與——全1才是1有0就是0。這個特性是多主仲裁的物理基礎。2.2 上拉電阻的計算不是隨便選個4.7K就行上拉電阻的選擇是I2C硬件設計中最容易出問題的地方。很多人抄別人的原理圖看到4.7K就跟著用結果換個場景就不通信了。上拉電阻的取值需要同時滿足兩個條件上升時間夠快和低電平電流不超標。先說上升時間。I2C總線的上升沿不是理想的階躍而是通過上拉電阻給總線電容充電的RC曲線??偩€電容來自PCB走線、引腳寄生電容、連接器等典型值在10pF到400pF之間。I2C標準規(guī)定標準模式100kHz上升時間不超過1000ns快速模式400kHz不超過300ns快速模式1MHz不超過120ns。上升時間的計算公式是tr ≈ 0.847 × R × C其中R是上拉電阻C是總線電容。假設總線電容是200pF快速模式要求tr ≤ 300ns那么R ≤ 300ns / (0.847 × 200pF) ≈ 1.77kΩ也就是說400kHz下200pF總線電容時上拉電阻不能超過1.77K。如果你用了4.7K上升時間會變成tr 0.847 × 4700 × 200p 796ns這已經(jīng)超過了快速模式的300ns限制波形會變得很“圓”在高速下可能被誤判。再說低電平電流。當設備拉低總線時上拉電阻上的電流會灌入設備的開漏MOS管。I2C標準規(guī)定標準模式和快速模式下VOL低電平輸出電壓最大0.4V灌電流最大3mA。計算最小電阻R ≥ (VDD - VOL) / IOL (3.3V - 0.4V) / 3mA ≈ 967Ω所以上拉電阻的合理范圍是1K到1.7K對于3.3V、200pF、400kHz的場景。但實際中還要考慮功耗如果總線上有多個設備同時拉低電流會疊加嗎不會因為開漏是線與電流只走一條路徑。但每個設備拉低時電流都會流過自己的MOS管所以每個設備的灌電流能力都要滿足。參數(shù)標準模式100kHz快速模式400kHz快速模式1MHz最大上升時間1000ns300ns120ns最大總線電容400pF400pF550pF典型上拉電阻3.3V4.7K-10K1K-2.2K680Ω-1K最大灌電流3mA3mA3mA實操心得如果你不確定總線電容可以用示波器測上升時間然后反推電容。方法是在總線上加一個已知電阻測上升時間用tr0.847RC算出C再根據(jù)目標上升時間選R。我試過幾次比盲猜靠譜得多。2.3 上拉電阻小了不通信可能是這個原因熱搜詞里有個“i2c上拉電阻小了不通信”這個現(xiàn)象很典型。上拉電阻太小比如用了100Ω會出現(xiàn)什么問題第一功耗暴增。3.3V除以100Ω等于33mA這已經(jīng)超過了大多數(shù)I2C設備的灌電流能力。設備拉低時MOS管可能進入線性區(qū)VOL升高導致低電平被誤判為高電平。第二多個設備同時拉低時電流分配不均。雖然理論上電流只走一條路徑但實際PCB上如果有多個設備同時拉低每個設備的MOS管都會導通電流會從VDD經(jīng)過上拉電阻然后分流到各個設備的MOS管。如果某個設備的導通電阻較大它分到的電流小VOL可能正常但如果某個設備導通電阻小它分到的電流大可能過熱。第三上升時間確實快了但下降時間可能變慢。因為開漏輸出的下降沿是MOS管放電如果總線電容大放電時間常數(shù)是Rds_on × CRds_on通常很小幾十歐姆所以下降沿很快。但上拉電阻太小會導致低電平時的靜態(tài)電流很大對電池供電的設備很不友好。所以上拉電阻不是越小越好要綜合上升時間、灌電流、功耗三個因素。我一般會先用1.5K到2.2K試如果波形不好再調(diào)整。3. 多主仲裁逐位競爭的藝術3.1 仲裁的物理基礎線與邏輯的天然優(yōu)勢多主仲裁是I2C最精妙的設計之一。它不需要額外的仲裁線不需要復雜的協(xié)議只靠開漏的線與特性就能實現(xiàn)。原理很簡單所有主設備在發(fā)送數(shù)據(jù)的同時也在監(jiān)聽總線上的實際電平。如果自己發(fā)的是1但總線上是0說明有別的設備在拉低自己就輸了立刻退出。這個過程是逐位進行的。假設兩個主設備A和B同時開始傳輸它們都發(fā)送起始條件然后發(fā)送地址。在每一個時鐘周期它們都把自己的SDA輸出和總線上的SDA比較。只要有一位不一致輸?shù)哪莻€就退出。舉個例子A發(fā)送地址0x50二進制1010000B發(fā)送地址0x52二進制1010010。前六位都一樣第七位A發(fā)0B發(fā)1。A拉低SDAB放手??偩€上是0B讀到0發(fā)現(xiàn)自己發(fā)的是1但總線是0B就輸了退出仲裁。A繼續(xù)傳輸完全不知道曾經(jīng)有競爭。這種仲裁機制的好處是不需要重傳不需要等待輸?shù)脑O備自動變成從設備或者等待下一次總線空閑。而且仲裁是在數(shù)據(jù)位級別進行的不會浪費總線時間。注意仲裁只能發(fā)生在主設備發(fā)送地址或數(shù)據(jù)階段。如果兩個主設備發(fā)送的數(shù)據(jù)完全一樣它們會一直競爭到最后一個位然后其中一個發(fā)送停止條件另一個繼續(xù)。但這種情況很少見因為地址通常不同。3.2 時鐘同步慢設備如何拖住快設備多主仲裁還有一個關鍵機制時鐘同步。I2C的SCL線也是開漏的所有主設備都可以拉低SCL。當多個主設備同時傳輸時它們的時鐘周期可能不同。I2C規(guī)定SCL的低電平周期由所有主設備中最長的那個決定高電平周期由最短的那個決定。具體來說每個主設備在拉低SCL后會開始計時自己的低電平周期。如果某個主設備的低電平周期還沒結束它會繼續(xù)拉低SCL。其他主設備看到SCL還是低就會等待。所以最終SCL的低電平時間是所有主設備低電平周期的最大值。高電平周期則相反每個主設備釋放SCL后上拉電阻把線拉高。但如果有主設備還沒釋放線就是低。所以高電平時間是所有主設備高電平周期的最小值。這種機制保證了慢速設備不會被快速設備“帶偏”整個總線的時鐘頻率由最慢的設備決定。這也是為什么I2C總線上可以混掛不同速度的設備——只要它們都支持標準模式就能一起工作。3.3 仲裁丟失后的處理從主到從的切換當一個主設備在仲裁中失敗時它必須立刻做三件事停止拉低SDA和SCL把自己的輸出級切換到高阻態(tài)完全釋放總線。切換到從設備模式開始監(jiān)聽總線上的地址。如果贏的那個主設備發(fā)送的地址正好是自己的地址它就要響應。等待下一次總線空閑如果它還想發(fā)起傳輸必須等到當前傳輸結束停止條件出現(xiàn)后再重新發(fā)起起始條件。這個過程在RTL實現(xiàn)中需要特別小心。很多初學者寫的I2C控制器在仲裁丟失后沒有正確釋放總線導致總線被卡死。正確的做法是在仲裁丟失的那一刻立刻把SDA和SCL的輸出使能拉低同時把狀態(tài)機切換到從設備接收狀態(tài)。實操心得仲裁丟失是正?,F(xiàn)象不是錯誤。你的控制器必須能正確處理仲裁丟失并且在丟失后能夠重新發(fā)起傳輸。我建議在RTL中加一個仲裁丟失計數(shù)器如果連續(xù)丟失超過一定次數(shù)就主動降低優(yōu)先級或者等待更長時間再試。4. RTL實現(xiàn)從狀態(tài)機到Verilog代碼4.1 狀態(tài)機設計主從一體的架構用Verilog寫I2C控制器最核心的是狀態(tài)機設計。我推薦用主從一體的架構因為I2C設備經(jīng)常需要在主從之間切換。狀態(tài)機可以分成幾個主要狀態(tài)IDLE總線空閑等待啟動條件。START發(fā)送起始條件SDA在SCL高時從高變低。ADDR發(fā)送7位地址加1位讀寫位。ACK1接收從設備的應答。DATA_TX發(fā)送數(shù)據(jù)字節(jié)。ACK2接收從設備的應答。DATA_RX接收數(shù)據(jù)字節(jié)。ACK3發(fā)送應答或非應答。STOP發(fā)送停止條件SDA在SCL高時從低變高。每個狀態(tài)都需要一個位計數(shù)器來跟蹤當前發(fā)送或接收到了第幾位。I2C的時序要求SDA在SCL低電平時變化在SCL高電平時保持穩(wěn)定。所以狀態(tài)機的輸出更新應該發(fā)生在SCL低電平期間。// 簡化的I2C主設備狀態(tài)機片段 localparam IDLE 3d0; localparam START 3d1; localparam ADDR 3d2; localparam ACK1 3d3; localparam DATA_TX 3d4; localparam ACK2 3d5; localparam DATA_RX 3d6; localparam ACK3 3d7; localparam STOP 3d8; reg [2:0] state, next_state; reg [2:0] bit_cnt; reg sda_out, scl_out; reg sda_oe, scl_oe; // 輸出使能1為拉低0為高阻 // SDA和SCL的三態(tài)輸出 assign sda sda_oe ? sda_out : 1bz; assign scl scl_oe ? scl_out : 1bz;注意這里的sda_oe和scl_oe是輸出使能信號。當oe1時輸出out的值當oe0時輸出高阻。這就是開漏輸出的RTL實現(xiàn)方式。4.2 時鐘分頻與時序參數(shù)I2C的時鐘頻率由SCL決定。標準模式100kHz快速模式400kHz。在FPGA中系統(tǒng)時鐘通常是50MHz或100MHz需要分頻得到SCL。分頻系數(shù)的計算假設系統(tǒng)時鐘50MHz目標SCL 400kHz那么分頻系數(shù) 50MHz / 400kHz 125。但I2C的SCL不是50%占空比而是低電平時間可能比高電平時間長。標準規(guī)定標準模式下低電平至少4.7us高電平至少4.0us快速模式下低電平至少1.3us高電平至少0.6us。所以分頻器需要分別控制高電平和低電平的計數(shù)。我一般用兩個計數(shù)器一個計數(shù)高電平周期一個計數(shù)低電平周期。這樣可以根據(jù)需要調(diào)整占空比。// 時鐘分頻器 reg [15:0] clk_cnt; reg scl_r; localparam HIGH_CNT 16d60; // 高電平計數(shù) localparam LOW_CNT 16d65; // 低電平計數(shù) always (posedge clk or negedge rst_n) begin if (!rst_n) begin clk_cnt 0; scl_r 1; end else begin if (clk_cnt (scl_r ? HIGH_CNT : LOW_CNT)) begin clk_cnt clk_cnt 1; end else begin clk_cnt 0; scl_r ~scl_r; end end end注意SCL的高電平時間不能太短否則從設備可能來不及采樣??焖倌J较赂唠娖街辽?.6us對應50MHz時鐘是30個周期。我一般會留一些余量用35到40個周期。4.3 仲裁丟失檢測的RTL實現(xiàn)仲裁丟失檢測是主設備必須實現(xiàn)的功能。原理很簡單在每個SCL高電平期間比較自己輸出的SDA和實際讀回的SDA。如果自己輸出1但讀回0說明仲裁丟失。// 仲裁丟失檢測 reg arb_lost; always (posedge clk or negedge rst_n) begin if (!rst_n) begin arb_lost 0; end else if (state ADDR || state DATA_TX) begin if (scl_r sda_oe sda_out !sda_in) begin arb_lost 1; end end else begin arb_lost 0; end end當arb_lost置位時狀態(tài)機應該立刻切換到從設備接收模式釋放SDA和SCL并等待當前傳輸結束。4.4 從設備時鐘拉伸的處理從設備時鐘拉伸是指從設備在需要更多時間處理數(shù)據(jù)時主動拉低SCL。主設備必須檢測到SCL被拉低并等待SCL釋放后再繼續(xù)。在RTL中主設備在每個SCL高電平期間應該檢測SCL是否真的被上拉電阻拉高了。如果SCL還是低說明從設備在拉伸時鐘主設備應該保持等待。// 時鐘拉伸檢測 reg scl_stretch; always (posedge clk or negedge rst_n) begin if (!rst_n) begin scl_stretch 0; end else if (scl_oe scl_out !scl_in) begin scl_stretch 1; end else if (!scl_oe scl_in) begin scl_stretch 0; end end當scl_stretch為1時主設備的時鐘計數(shù)器應該暫停直到SCL真正變高。5. 常見問題與排查技巧實錄5.1 波形分析邏輯分析儀怎么抓I2C邏輯分析儀是調(diào)試I2C的必備工具。抓I2C波形時要注意幾個關鍵點觸發(fā)條件設置起始條件觸發(fā)SDA在SCL高時從高變低。采樣率至少是SCL頻率的10倍。400kHz的SCL采樣率至少4MHz建議10MHz以上。解碼設置大多數(shù)邏輯分析儀都有I2C解碼功能設置好地址位寬7位或10位和讀寫位。抓到的波形要重點看幾個地方起始條件是否干凈地址和數(shù)據(jù)的建立保持時間是否滿足ACK位是否正確拉低停止條件是否正常。5.2 常見問題速查表現(xiàn)象可能原因排查方法解決方案總線一直低某個設備拉死SDA或SCL逐個斷開設備測電平找到故障設備檢查其開漏輸出起始條件后無ACK從設備地址不對用邏輯分析儀解碼地址核對從設備數(shù)據(jù)手冊的地址波形上升沿太慢上拉電阻太大或總線電容太大測上升時間算RC減小上拉電阻或減少總線負載多主仲裁頻繁丟失多個主設備同時發(fā)起傳輸觀察仲裁丟失時的波形加軟件退避策略錯開發(fā)起時間從設備時鐘拉伸導致超時從設備處理慢測SCL低電平時間增加主設備超時時間或降低SCL頻率數(shù)據(jù)位錯誤建立保持時間不足測SDA和SCL的相位關系調(diào)整RTL中SDA的變化時刻5.3 獨家避坑技巧技巧一上拉電阻并聯(lián)加速。如果上升沿太慢可以在上拉電阻上并聯(lián)一個二極管和一個小電阻。二極管方向是從總線到VDD這樣上升時二極管導通小電阻加速充電下降時二極管截止只有原上拉電阻工作功耗不增加。這個方法我實測有效但要注意二極管的結電容。技巧二總線電容估算。沒有示波器的時候可以用萬用表測總線對地電容。方法是在總線上加一個已知頻率的方波測電流用I C × dV/dt反推C。雖然不太準但能估個大概。技巧三仲裁丟失后不要立刻重試。仲裁丟失后立刻重試很可能再次丟失。我一般會加一個隨機退避時間比如等待總線空閑后再等幾個微秒錯開競爭窗口。技巧四用GPIO模擬I2C調(diào)試。在正式寫RTL之前可以先用FPGA的GPIO模擬I2C時序手動控制SDA和SCL觀察從設備的反應。這樣能快速驗證硬件連接和從設備地址比直接寫RTL調(diào)試快得多。技巧五注意電源域。如果I2C總線上有不同電源域的設備比如3.3V和1.8V混掛需要用電平轉(zhuǎn)換器。電平轉(zhuǎn)換器的選擇要注意方向性和速率有些轉(zhuǎn)換器不支持開漏輸出會導致總線無法拉低。6. 從協(xié)議到實踐一個完整的I2C讀寫EEPROM案例6.1 EEPROM的I2C時序特點以常見的24C02 EEPROM為例它的I2C時序有幾個特點設備地址是1010xxx其中xxx由A2A1A0引腳決定寫操作需要發(fā)送設備地址、字地址、數(shù)據(jù)讀操作需要先寫設備地址和字地址然后重新發(fā)起起始條件發(fā)送讀地址再讀數(shù)據(jù)。寫操作的時序起始條件 → 設備地址寫位 → ACK → 字地址 → ACK → 數(shù)據(jù) → ACK → 停止條件。注意EEPROM在收到停止條件后才會開始內(nèi)部寫周期期間不響應任何請求主設備需要等待5ms左右。讀操作的時序起始條件 → 設備地址寫位 → ACK → 字地址 → ACK → 起始條件 → 設備地址讀位 → ACK → 數(shù)據(jù) → NACK → 停止條件。6.2 Verilog實現(xiàn)的關鍵片段// EEPROM寫操作狀態(tài)機片段 localparam WR_IDLE 4d0; localparam WR_START 4d1; localparam WR_ADDR 4d2; localparam WR_ACK1 4d3; localparam WR_WORD 4d4; localparam WR_ACK2 4d5; localparam WR_DATA 4d6; localparam WR_ACK3 4d7; localparam WR_STOP 4d8; localparam WR_WAIT 4d9; // 寫操作流程 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state WR_IDLE; end else begin case (state) WR_IDLE: if (start) state WR_START; WR_START: state WR_ADDR; WR_ADDR: if (bit_cnt 7) state WR_ACK1; WR_ACK1: if (sda_in 0) state WR_WORD; // 收到ACK WR_WORD: if (bit_cnt 7) state WR_ACK2; WR_ACK2: if (sda_in 0) state WR_DATA; WR_DATA: if (bit_cnt 7) state WR_ACK3; WR_ACK3: if (sda_in 0) state WR_STOP; WR_STOP: state WR_WAIT; WR_WAIT: if (wait_cnt 50000) state WR_IDLE; // 等待5ms endcase end end6.3 實測波形與調(diào)試記錄我第一次調(diào)EEPROM的時候?qū)懖僮饕恢辈怀晒壿嫹治鰞x顯示地址和數(shù)據(jù)的ACK都正常但停止條件后EEPROM就是不響應。后來發(fā)現(xiàn)是等待時間不夠24C02的內(nèi)部寫周期最大5ms我只等了1ms。改成5ms后一切正常。還有一次讀操作時第一個字節(jié)讀出來總是0xFF。查了半天發(fā)現(xiàn)是重新發(fā)起起始條件的時候SDA釋放得太早導致從設備誤判為停止條件。正確的做法是在SCL高電平期間先拉低SDA然后再拉低SCL這樣才能形成重復起始條件。實操心得EEPROM的寫周期等待時間一定要留足余量。數(shù)據(jù)手冊上寫5ms實際可能更長尤其是老化的芯片。我一般等6ms寧可慢一點也不要出錯。7. 進階話題I2C與其他協(xié)議的對比與選型7.1 I2C vs SPI vs UART特性I2CSPIUART線數(shù)242速度100k-3.4M1M-100M通常1M多設備支持地址尋址支持片選不支持點對點時鐘同步同步異步仲裁支持不支持不支持開銷地址ACK無起始停止位I2C的優(yōu)勢在于線少、支持多設備、有仲裁機制。劣勢是速度相對慢上拉電阻和總線電容限制了傳輸距離。SPI速度快但線多每個設備需要單獨的片選。UART簡單但只能點對點。7.2 PMBus與I2C的區(qū)別PMBus是建立在I2C物理層之上的協(xié)議主要用于電源管理。它的物理層和I2C完全一樣但協(xié)議層增加了特定的命令集和故障處理機制。PMBus的時鐘頻率通常固定為100kHz或400kHz支持PEC包錯誤校驗。如果你做電源管理PMBus比裸I2C更方便因為命令集是標準化的。7.3 I2C擴展多路復用和電平轉(zhuǎn)換當總線上設備太多地址沖突或者電容太大時可以用I2C多路復用器如TCA9548A把總線分成多路。每路有獨立的電容和地址空間主設備通過寫多路復用器的控制寄存器來選擇當前通道。電平轉(zhuǎn)換方面如果3.3V和1.8V設備混掛可以用專用的I2C電平轉(zhuǎn)換器如PCA9306它支持開漏輸出和雙向傳輸。注意不要用普通的推挽電平轉(zhuǎn)換器會破壞I2C的線與邏輯。8. 我踩過的那些坑和最后的建議調(diào)I2C的這幾年我踩過的坑真的不少。有一次是上拉電阻用了10K100kHz下勉強能跑換成400kHz就完全不行波形圓得像個饅頭。還有一次是多個主設備競爭仲裁丟失后我的控制器沒有正確釋放總線導致整個總線死鎖所有設備都不通信。最慘的一次是PCB上SDA和SCL走線太長沒有做阻抗匹配反射導致數(shù)據(jù)錯誤查了兩天才發(fā)現(xiàn)是走線問題。如果讓我給正在學I2C的人一個建議那就是不要只寫代碼一定要抓波形。I2C的問題90%都能從波形上看出來。上升沿太慢、建立保持時間不夠、ACK位不對、仲裁丟失這些在波形上都一目了然。邏輯分析儀不貴幾百塊就能買一個但能幫你省下幾十個小時的調(diào)試時間。另外RTL實現(xiàn)的時候一定要把仲裁丟失和時鐘拉伸這兩個功能做進去。很多開源I2C控制器為了簡化把這兩個功能省了結果在多主或者慢速從設備的場景下直接歇菜。這兩個功能不難但能決定你的控制器能不能用在真實項目里。最后再分享一個小技巧如果你不確定從設備的地址可以用邏輯分析儀抓一次成功的通信直接看地址字節(jié)。比翻數(shù)據(jù)手冊快得多而且不會出錯。我現(xiàn)在的習慣是拿到任何I2C設備先抓波形確認地址和時序然后再寫代碼。這個習慣幫我省了無數(shù)次返工。