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