序與狀態(tài)機(jī)設(shè)計(jì)實(shí)戰(zhàn))
簡(jiǎn)介面向FPGA入門與嵌入式存儲(chǔ)應(yīng)用開發(fā)者這份工程以開拓者開發(fā)板上的AT24C64為例完整演示了EEPROM的讀寫時(shí)序與FPGA控制邏輯實(shí)現(xiàn)。EEPROM作為電可擦除可編程只讀存儲(chǔ)器掉電不丟失廣泛用于物聯(lián)網(wǎng)與可穿戴設(shè)備的小量數(shù)據(jù)存儲(chǔ)AT24C64支持百萬(wàn)次擦寫數(shù)據(jù)可保存百年工程從協(xié)議層到寄存器級(jí)展示了如何通過狀態(tài)機(jī)驅(qū)動(dòng)I2C接口完成字節(jié)讀寫與頁(yè)操作。文件包共234個(gè)文件大小6.23MB內(nèi)容以tdf硬件描述語(yǔ)言源碼、Verilog模塊、qpf/qsf工程配置、sof下載文件為主輔以編譯生成的cdb/hdb數(shù)據(jù)庫(kù)與rpt/qmsg報(bào)告文件便于直接查看綜合布局結(jié)果和時(shí)序約束。已有107人學(xué)習(xí)下載。結(jié)合完整工程源碼與目錄結(jié)構(gòu)讀者可掌握地址尋址、讀寫流程、應(yīng)答檢測(cè)及多字節(jié)傳輸?shù)汝P(guān)鍵點(diǎn)并能快速將代碼移植到其他AT24C系列或自定義存儲(chǔ)項(xiàng)目中。1. FPGA讀寫EEPROM為什么這是繞不開的時(shí)序基本功新手做FPGA項(xiàng)目十個(gè)有九個(gè)把第一塊硬骨頭啃在EEPROM讀寫上。仿真波形明明全綠一上板子讀回全0xFF波形抓出來(lái)起始條件都對(duì)了就是ACK不對(duì)好不容易寫進(jìn)去斷電再來(lái)數(shù)據(jù)沒了。這些都是FPGA讀寫EEPROM最常見也最打擊人的場(chǎng)景。這個(gè)標(biāo)題背后其實(shí)就兩件事把I2C總線協(xié)議吃透再把狀態(tài)機(jī)寫成符合時(shí)序的電路。它不需要DDR、PCIe那種高速難題但會(huì)把跨時(shí)鐘域、三態(tài)門、時(shí)序約束這些基本功一次性串起來(lái)。做完這個(gè)模塊你以后接傳感器、接RTC、接各類I2C從設(shè)備全部是同一套思路。適合剛?cè)腴TFPGA、做過LED流水燈但又想真正碰一下總線協(xié)議的開發(fā)者。2. I2C鏈路怎么搭從AT24C系列芯片到FPGA引腳規(guī)劃2.1 AT24C02到AT24C128容量、地址引腳和選型差異市面上FPGA開發(fā)板最常用的EEPROM是AT24C02和AT24C64一個(gè)2Kbit一個(gè)64Kbit都是I2C接口。選型時(shí)最該關(guān)心的是兩個(gè)參數(shù)容量和頁(yè)寫大小。型號(hào)容量字節(jié)數(shù)頁(yè)寫大小器件地址7位AT24C022Kbit256字節(jié)8字節(jié)0x50AT24C044Kbit512字節(jié)16字節(jié)0x50/0x51AT24C1616Kbit2KB16字節(jié)0x50~0x57AT24C6464Kbit8KB32字節(jié)0x50AT24C02在開發(fā)板上最常見因?yàn)榈刂芬_A0/A1/A2全接低電平時(shí)I2C從機(jī)地址固定為0x50。如果你的板子上A0/A1/A2跳線接了高電平地址就要相應(yīng)加上偏移這是很多人忽略的第一步。引腳上除了SCL、SDA、VCC、GND之外還有個(gè)WP寫保護(hù)腳用來(lái)禁止寫操作。如果WP被拉高寫時(shí)序再標(biāo)準(zhǔn)也沒用數(shù)據(jù)進(jìn)不去這個(gè)坑后面細(xì)說(shuō)。AT24C04以上容量頁(yè)寫大小是16字節(jié)起跨頁(yè)地址回卷的坑更容易踩。開發(fā)板上做實(shí)驗(yàn)用AT24C02就夠了項(xiàng)目要存較大參數(shù)時(shí)再考慮AT24C64或更大容量。有一點(diǎn)要注意容量超過16Kbit的型號(hào)如AT24C256器件地址里會(huì)包含高位地址字節(jié)地址是16位的寫流程多一個(gè)地址字節(jié)。2.2 總線幀結(jié)構(gòu)起始位、停止位、ACK與讀寫方向的排布I2C的精髓就一句話SCL為高時(shí)SDA上的跳變是控制信號(hào)SCL為高時(shí)SDA必須保持穩(wěn)定才是數(shù)據(jù)。起始條件是SCL高時(shí)SDA從高拉低停止條件是SCL高時(shí)SDA從低拉高。一次完整寫入幀排布是起始位 → 器件地址7位寫方向位0 → ACK → 字節(jié)地址 → ACK → 數(shù)據(jù)字節(jié) → ACK → 停止位。整個(gè)過程中每發(fā)完一個(gè)字節(jié)主設(shè)備必須釋放SDA總線讓從設(shè)備拉低回應(yīng)一個(gè)ACK。九個(gè)SCL脈沖里前八個(gè)是數(shù)據(jù)第九個(gè)是ACK應(yīng)答。重點(diǎn)在方向位的排布器件地址0x50左移一位得到0xA0是寫方向地址0xA1是讀方向地址。很多人在代碼里直接把0x50當(dāng)成發(fā)送字節(jié)導(dǎo)致地址錯(cuò)位一位整個(gè)通信直接失敗。讀操作更特殊它要先發(fā)一個(gè)偽寫幀把寄存器指針指到目標(biāo)地址再發(fā)一個(gè)重復(fù)起始條件然后換讀方向繼續(xù)讀。這個(gè)流程拆開看就是兩個(gè)起始條件夾一個(gè)地址字節(jié)中間沒有停止位。2.3 FPGA側(cè)的引腳規(guī)劃時(shí)鐘分頻、SCL生成與sda三態(tài)口FPGA這邊連I2C總線和單片機(jī)不同沒有開漏輸出的硬件結(jié)構(gòu)必須自己用三態(tài)門模擬。sda端口要聲明為inout內(nèi)部需要兩個(gè)信號(hào)sda_out_data負(fù)責(zé)輸出數(shù)據(jù)sda_en控制方向。sda_en為1時(shí)FPGA驅(qū)動(dòng)總線為0時(shí)釋放總線給外部上拉。時(shí)鐘分頻是另一個(gè)關(guān)鍵點(diǎn)。I2C標(biāo)準(zhǔn)速率是100kHz和400kHz開發(fā)板主時(shí)鐘常見50MHz或100MHz。做法是先用計(jì)數(shù)器做分頻生成一個(gè)SCL半周期脈沖再用這個(gè)脈沖驅(qū)動(dòng)狀態(tài)機(jī)。比如50MHz要得到100kHz SCL每個(gè)SCL周期是500個(gè)主時(shí)鐘周期半周期是250個(gè)。localparam HALF_CLK 250 - 1; // 50MHz - 100kHz SCL reg [8:0] half_cnt; reg scl_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin half_cnt 9d0; scl_r 1b1; end else if (half_cnt HALF_CLK) begin half_cnt 9d0; scl_r ~scl_r; // 每250個(gè)主時(shí)鐘翻轉(zhuǎn)一次SCL end else begin half_cnt half_cnt 1b1; end end這段代碼生成了低電平有效的SCL翻轉(zhuǎn)脈沖scl_r就是內(nèi)部SCL寄存器scl_r翻轉(zhuǎn)一次代表一個(gè)半周期完成。這里要留意真正的SCL輸出還應(yīng)該在I/O約束里設(shè)置ODT和驅(qū)動(dòng)強(qiáng)度但核心邏輯就是靠這個(gè)翻轉(zhuǎn)信號(hào)推進(jìn)狀態(tài)機(jī)。不要試圖用計(jì)數(shù)器直接算SCL的高電平寬度那會(huì)讓狀態(tài)機(jī)變得瑣碎難調(diào)。用半周期脈沖作為采樣節(jié)拍狀態(tài)機(jī)在scl_r的上升沿和下降沿分別做不同動(dòng)作這是最清晰的寫法。3. 用狀態(tài)機(jī)實(shí)現(xiàn)I2C主機(jī)一個(gè)能跑的基礎(chǔ)Verilog骨架3.1 核心狀態(tài)機(jī)與字節(jié)移位SCL低電平換數(shù)據(jù)、高電平采樣寫I2C主機(jī)狀態(tài)機(jī)我一般采取四個(gè)狀態(tài)IDLE、START、SHIFT、ACK。START狀態(tài)負(fù)責(zé)拉出起始條件SHIFT狀態(tài)按位發(fā)送或接收數(shù)據(jù)ACK狀態(tài)判斷從設(shè)備是否應(yīng)答。下面這段代碼是字節(jié)發(fā)送的核心骨架只保留關(guān)鍵路徑。localparam IDLE 3d0; localparam START_S 3d1; localparam SHIFT_S 3d2; localparam ACK_S 3d3; reg [2:0] fsm; reg [2:0] bit_cnt; reg sda_out; reg sda_en; reg [7:0] tx_data; always (posedge clk or negedge rst_n) begin if (!rst_n) begin fsm IDLE; bit_cnt 3d0; sda_en 1b1; end else begin case (fsm) IDLE: begin sda_out 1b1; sda_en 1b1; if (start_req) fsm START_S; end START_S: begin sda_out 1b0; // SCL為高時(shí)SDA拉低 if (scl_neg_edge) fsm SHIFT_S; end SHIFT_S: begin sda_out tx_data[bit_cnt]; if (scl_neg_edge) begin // SCL低電平期間改寫SDA if (bit_cnt 3d0) begin fsm ACK_S; sda_en 1b0; // 釋放總線等從機(jī)ACK end else begin bit_cnt bit_cnt - 1b1; end end end ACK_S: begin if (scl_rise_edge) begin o_ack_err sda_in; fsm IDLE; end end endcase end end這段代碼的核心觀點(diǎn)是SDA必須在SCL低電平時(shí)變化在SCL高電平時(shí)被從設(shè)備采樣。所以我只在scl_neg_edge時(shí)更新sda_out在scl_rise_edge時(shí)讀取從設(shè)備的ACK。bit_cnt一路遞減到0表示8個(gè)數(shù)據(jù)位全部發(fā)送完畢然后釋放總線、轉(zhuǎn)入ACK狀態(tài)。注意tx_data必須在進(jìn)入SHIFT狀態(tài)前由上層賦值否則第一拍發(fā)送的數(shù)據(jù)是舊值。3.2 ACK檢測(cè)與停止條件主從握手最容易忽略的一拍ACK狀態(tài)是整個(gè)I2C通信最容易翻車的位置。原因很常見發(fā)送完8位數(shù)據(jù)后主設(shè)備沒有釋放SDA總線導(dǎo)致從設(shè)備想拉低ACK卻拉不動(dòng)或者說(shuō)主設(shè)備讀到的SDA永遠(yuǎn)為高誤判為NACK。// ACK正在采樣——該剝除sda延時(shí)的對(duì)齊 reg sda_d1; always (posedge clk) sda_d1 sda_in; // 在SCL高電平中點(diǎn)采樣一個(gè)sda wire ack_sampled (fsm ACK_S) (half_cnt HALF_CLK/2); always (posedge clk) begin if (ack_sampled) o_ack_err sda_d1; end這里連著三個(gè)問題要一起解決。第一采樣時(shí)機(jī)在SCL高電平的中點(diǎn)不要用上升沿上升沿太快容易采到數(shù)據(jù)翻轉(zhuǎn)瞬間的毛刺。第二sda_in經(jīng)過一級(jí)寄存器打拍消除亞穩(wěn)態(tài)風(fēng)險(xiǎn)這是跨時(shí)鐘域的基本動(dòng)作。第三如果o_ack_err為1說(shuō)明從設(shè)備沒應(yīng)答就直接進(jìn)IDLE并向上層報(bào)錯(cuò)不要再繼續(xù)發(fā)后續(xù)數(shù)據(jù)否則總線狀態(tài)全亂。跳線地址不對(duì)、EEPROM芯片焊反、上電時(shí)序沒滿足最終表現(xiàn)形式都是ACK一直為高。所以調(diào)試時(shí)先看ACK位再談數(shù)據(jù)內(nèi)容。3.3 模塊接口設(shè)計(jì)如何把i2c_master掛到上層I2C主機(jī)不要直接讓業(yè)務(wù)邏輯穿梭sda建議封裝成獨(dú)立模塊對(duì)外暴露一個(gè)最簡(jiǎn)單的手握級(jí)接口start_req、rw方向、要發(fā)送的字節(jié)、讀回的數(shù)據(jù)、done脈沖、錯(cuò)誤標(biāo)志。上層只管發(fā)請(qǐng)求狀態(tài)機(jī)內(nèi)部管時(shí)序。module i2c_master #( parameter CLK_DIV 250 )( input wire clk, input wire rst_n, inout wire sda, output wire scl, input wire req_valid, input wire req_rw, // 1:讀 0:寫 input wire [7:0] req_data, output reg [7:0] resp_data, output reg resp_done, output reg resp_ack_err );數(shù)據(jù)線和控制線分離是這套設(shè)計(jì)的關(guān)鍵。你寫EEPROM驅(qū)動(dòng)時(shí)上層模塊關(guān)心的只有地址發(fā)過去沒有、數(shù)據(jù)寫進(jìn)去沒有、讀回來(lái)的字節(jié)是什么。至于起始位寬多少、SCL頻率多少那是i2c_master內(nèi)部的事。以后項(xiàng)目里要接其他I2C設(shè)備OS操作平臺(tái)相同整個(gè)主機(jī)原封不動(dòng)復(fù)用。這種模塊化設(shè)計(jì)還有個(gè)好處仿真時(shí)可以單獨(dú)給i2c_master寫testbench驗(yàn)證完時(shí)序再掛EEPROM邏輯。如果一鍋燉在一個(gè)大模塊里出問題根本分不清是狀態(tài)機(jī)錯(cuò)了還是地址邏輯錯(cuò)了。4. EEPROM讀寫模塊從字節(jié)地址到頁(yè)寫邊界的完整實(shí)現(xiàn)4.1 寫操作控制字節(jié)、寄存器地址與數(shù)據(jù)字節(jié)的排布有了i2c_master做底層寫EEPROM就成了拼幀的過程。AT24C02只有256個(gè)字節(jié)地址一個(gè)字節(jié)就夠。EEPROM寫一個(gè)字節(jié)需要連續(xù)發(fā)送三個(gè)字節(jié)0xA0設(shè)備地址寫方向、地址字節(jié)、數(shù)據(jù)字節(jié)。每次發(fā)送之間都要檢查ACK任何一拍的ACK不對(duì)整個(gè)過程退回IDLE。module eeprom_write_ctrl #( parameter CLK_DIV 250 )( input wire clk, input wire rst_n, inout wire sda, output wire scl, input wire wr_start, input wire [7:0] wr_addr, input wire [7:0] wr_data, output reg wr_done, output reg wr_err ); localparam S_IDLE 3d0; localparam S_DEV 3d1; // 發(fā)送器件地址 localparam S_ADDR 3d2; // 發(fā)送EEPROM內(nèi)部字節(jié)地址 localparam S_DATA 3d3; // 發(fā)送數(shù)據(jù) localparam S_STOP 3d4; // 停止并等待寫周期 reg [2:0] state; reg [1:0] send_cnt; wire m_done; wire m_ack_err; reg req_valid; reg req_rw; reg [7:0] req_data; i2c_master #(.CLK_DIV(CLK_DIV)) u_master ( .clk(clk), .rst_n(rst_n), .sda(sda), .scl(scl), .req_valid(req_valid), .req_rw(req_rw), .req_data(req_data), .resp_data(), .resp_done(m_done), .resp_ack_err(m_ack_err) ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; req_valid 1b0; end else begin case (state) S_IDLE: begin if (wr_start) begin send_cnt 2d0; state S_DEV; req_rw 1b0; req_data 8hA0; // 1010_0000 req_valid 1b1; end end S_DEV: begin if (m_done !m_ack_err send_cnt 2d0) begin send_cnt send_cnt 1b1; req_data wr_addr; state S_ADDR; end else if (m_done m_ack_err) begin state S_IDLE; wr_err 1b1; end end S_ADDR: begin if (m_done !m_ack_err) begin send_cnt send_cnt 1b1; req_data wr_data; state S_DATA; end end S_DATA: begin if (m_done !m_ack_err) begin req_valid 1b0; state S_STOP; end end S_STOP: begin wr_done 1b1; state S_IDLE; end endcase end end endmodule這個(gè)狀態(tài)機(jī)的關(guān)鍵點(diǎn)是每次發(fā)送之前都把req_valid拉高一個(gè)周期等m_done回來(lái)再切狀態(tài)。req_valid不能一直維持高電平因?yàn)閕2c_master每次只處理一個(gè)字節(jié)如果req_valid常高它會(huì)重復(fù)發(fā)送。wr_err標(biāo)志在任意一幀ACK出錯(cuò)時(shí)拉高并且整個(gè)控制狀態(tài)回到IDLE等待上層重新發(fā)起。寫周期這部分我沒在狀態(tài)機(jī)里處理因?yàn)镋EPROM在收到停止位后需要5ms內(nèi)部擦寫時(shí)間這期間不能訪問。4.2 讀操作虛寫地址、重復(fù)起始與連續(xù)讀的搭法讀比寫繞一點(diǎn)。要讀地址0x10的數(shù)據(jù)必須先發(fā)0xA0、0x10這兩個(gè)字節(jié)讓EEPROM內(nèi)部地址指針指向0x10再發(fā)0xA1方向位從頭讀。這個(gè)流程叫偽寫本質(zhì)上是欺騙從設(shè)備把地址裝好再換方向讀。AT24C02支持連續(xù)讀主設(shè)備持續(xù)給時(shí)鐘EEPROM會(huì)自動(dòng)遞增地址往外吐數(shù)據(jù)直到主設(shè)備回一個(gè)NACK再發(fā)停止位。偽寫和真正的寫有兩個(gè)區(qū)別第一偽寫發(fā)完地址后不停止而是發(fā)重復(fù)起始條件第二方向位從0翻成1。重復(fù)起始位不能讓狀態(tài)機(jī)先發(fā)停止位再發(fā)起始位那樣就是兩次獨(dú)立的傳輸EEPROM會(huì)當(dāng)成偽寫已經(jīng)結(jié)束數(shù)據(jù)讀不出來(lái)。搞定重復(fù)起始的i2c_master需要一個(gè)restart功能。實(shí)踐中我在i2c_master里增加一個(gè)restart_req信號(hào)在ACK狀態(tài)結(jié)束時(shí)檢測(cè)到restart_req就直接回START狀態(tài)而不回IDLE。完整讀模塊的思路是寫地址幀 → 拉restart → 發(fā)0xA1 → 連續(xù)讀N個(gè)字節(jié) → 最后一個(gè)字節(jié)回NACK → 停止。4.3 頁(yè)寫與tWR等待EEPROM自己限制的兩個(gè)邊界頁(yè)寫是I2C EEPROM最容易出現(xiàn)玄學(xué)錯(cuò)誤的特性。AT24C02每頁(yè)8字節(jié)寫入時(shí)如果地址從0x07繼續(xù)往0x08寫實(shí)際上不會(huì)寫入0x08而是回卷寫入0x00。這是芯片內(nèi)部硬件行為不是總線協(xié)議能約束的。AT24C64每頁(yè)32字節(jié)寫跨頁(yè)時(shí)同樣回卷。處理辦法只有兩個(gè)要么一次只寫單字節(jié)要么在編程邏輯里檢測(cè)當(dāng)前地址是否在頁(yè)邊界超出就拆成兩次頁(yè)寫。頁(yè)寫確實(shí)能把效率提升好幾倍因?yàn)?字節(jié)或32字節(jié)一次寫周期就完成但對(duì)應(yīng)的風(fēng)險(xiǎn)是數(shù)據(jù)錯(cuò)亂。寫周期tWR也一樣沒人情味。EEPROM接受停止位后內(nèi)部控制邏輯開始擦寫這期間最多5ms不響應(yīng)任何總線操作。如果你不等它寫完就去發(fā)下一個(gè)寫命令地址和數(shù)據(jù)根本進(jìn)不去而且ACK會(huì)異常。一個(gè)常規(guī)做法是在S_STOP狀態(tài)里加一個(gè)延時(shí)計(jì)數(shù)器默認(rèn)等待5ms再回IDLE。也可以做ACK輪詢就是反復(fù)發(fā)一個(gè)偽寫命令直到收到ACK為止但這一般是單片機(jī)才用的省事手段FPGA這邊直接延時(shí)最穩(wěn)妥。5. 避坑記錄從仿真全綠到上板翻車的六條紀(jì)錄5.1 讀回全0xFF先查上拉和WP腳別懷疑芯片現(xiàn)象上板測(cè)試無(wú)論寫什么地址讀回來(lái)全是0xFF。仿真里明明都正常。原因大概率不是你的狀態(tài)機(jī)而是硬件連接。EEPROM的SDA需要外部上拉到VCC如果板上沒接上拉電阻或者FPGA引腳配置成內(nèi)部下拉SDA讀進(jìn)來(lái)永遠(yuǎn)是0。再加上WP寫保護(hù)腳懸空或接高寫入動(dòng)作被芯片拒絕讀回自然就是初始值0xFF。解決先用萬(wàn)用表量SDA引腳靜態(tài)電平總線空閑時(shí)應(yīng)該是高。再把WP腳確認(rèn)拉低排除寫保護(hù)。5.2 時(shí)序仿真完美上板卻卡死sda三態(tài)門方向控制寫錯(cuò)了現(xiàn)象ModelSim波形看著起始條件、ACK、停止一切都對(duì)一上板I2C總線直接掛住SCL正常跳但SDA一直低。原因很多人在仿真里把sda_out和sda_en簡(jiǎn)單寫成了賦值沒有真正模擬open-drain行為。FPGA管腳如果是推挽輸出持續(xù)驅(qū)動(dòng)低電平從設(shè)備拉到低后根本釋放不了。解決檢查三態(tài)門sda_en為低時(shí)必須輸出高阻z而不是強(qiáng)行輸出1。用IOBUF原語(yǔ)是最穩(wěn)的寫法。IOBUF #( .DRIVE(12), .IOSTANDARD(LVCMOS33) ) iobuf_sda ( .O (sda_in), .IO(sda), .I (sda_out), .T (sda_en_n) );5.3 寫第二個(gè)字節(jié)穩(wěn)定第三個(gè)字節(jié)開始丟tWR沒等夠現(xiàn)象連續(xù)寫三個(gè)不同地址前兩個(gè)斷電后驗(yàn)證正常第三個(gè)總是丟。原因EEPROM寫周期tWR是5ms你寫完第一個(gè)字節(jié)沒等夠緊接著發(fā)第二個(gè)字節(jié)的命令結(jié)果第二個(gè)寫操作直接無(wú)效。解決每次寫入結(jié)束加5ms延時(shí)或者在狀態(tài)機(jī)里對(duì)寫命令間隔做最小間隔約束。速度快不是目的數(shù)據(jù)穩(wěn)才是。5.4 頁(yè)寫數(shù)據(jù)串位跨頁(yè)地址回卷是硬件行為不是bug現(xiàn)象用頁(yè)寫模式連續(xù)寫20個(gè)字節(jié)發(fā)現(xiàn)數(shù)據(jù)從第8個(gè)開始錯(cuò)亂但單字節(jié)寫全部正常。原因AT24C02頁(yè)大小8字節(jié)地址位最低3位歸零回卷。解決寫之前做地址判斷當(dāng)前地址距離頁(yè)邊界不足計(jì)劃長(zhǎng)度時(shí)拆開寫。5.5 多設(shè)備總線上A設(shè)備正常B設(shè)備異常跳線地址沖突現(xiàn)象開發(fā)板上同時(shí)掛了EEPROM和一個(gè)溫度傳感器EEPROM讀寫正常傳感器讀不到。原因兩個(gè)設(shè)備硬件地址相同或者EEPROM的A0/A1/A2跳線被撥到了和另一個(gè)器件沖突的位置。解決查兩個(gè)芯片的地址位把EEPROM跳線撥到不沖突的組合總線協(xié)議層面無(wú)解。6. 從仿真到ILA實(shí)測(cè)驗(yàn)證讀寫的三板斧6.1 用testbench快速逼出起始、ACK、停止仿真時(shí)不要一上來(lái)就寫完整讀寫流程先給i2c_master發(fā)一個(gè)單字節(jié)寫請(qǐng)求抓三個(gè)節(jié)點(diǎn)scl_r電平、sda_in、fsm狀態(tài)。起始條件看SDA是否在SCL高時(shí)拉低ACK看第9個(gè)時(shí)鐘SDA是否被從設(shè)備拉低停止看SDA是否在SCL高時(shí)拉高。這三個(gè)點(diǎn)都對(duì)了基本協(xié)議就通了一半。模型里EEPROM用仿真模型注意模型里通常有tWR參數(shù)仿真時(shí)默認(rèn)寫周期極短真正上板才暴露問題。6.2 ILA實(shí)測(cè)抓I2C總線三個(gè)檢查點(diǎn)上板調(diào)試用ILA抓實(shí)際波形比仿真多了兩個(gè)檢查點(diǎn)SCL信號(hào)邊沿是否干凈、SDA的電平是否在SCL高時(shí)穩(wěn)定。抓取時(shí)把sda設(shè)為輸入引腳時(shí)鐘用100MHz深度4096足夠看一次完整讀寫。實(shí)測(cè)中最常見的問題是SCL上升沿上疊加了毛刺導(dǎo)致從設(shè)備誤判時(shí)鐘沿。這本質(zhì)是驅(qū)動(dòng)強(qiáng)度太強(qiáng)加上總線電容導(dǎo)致的過沖在約束文件里把I/O的SLEW降下來(lái)能明顯改善。我自己的調(diào)試習(xí)慣是把ILA觸發(fā)放到ACK錯(cuò)誤標(biāo)志上先看是不是每個(gè)字節(jié)都有ACK再看數(shù)據(jù)內(nèi)容。如果ACK全對(duì)但數(shù)據(jù)錯(cuò)那問題在地址排列如果ACK都錯(cuò)那問題在硬件電平。這個(gè)思路幫我排除過無(wú)數(shù)個(gè)看似玄學(xué)的I2C故障。讀操作驗(yàn)證時(shí)建議用連續(xù)讀模式一次讀16字節(jié)觀察地址自動(dòng)遞增是否正確。只要連續(xù)讀的數(shù)據(jù)和寫入的數(shù)據(jù)完全一致整個(gè)讀寫鏈路就基本閉環(huán)了。你以后接任何I2C從設(shè)備這套調(diào)試方法都能直接復(fù)用希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取