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