協(xié)議棧入門)
從近似0基礎(chǔ)開始啃 FPGA 開發(fā)走到第 10 篇終于摸到了網(wǎng)絡(luò)通信這塊硬骨頭。前面幾篇我們把 Verilog 語法、時序邏輯、狀態(tài)機(jī)、FIFO、跨時鐘域這些基礎(chǔ)攢得差不多了但真要在板子上跑通一條 UDP 鏈路光靠手寫是遠(yuǎn)遠(yuǎn)不夠的——你要處理 PHY 接口時序、MAC 幀格式、IP 頭校驗和、ARP 地址解析、UDP 偽首部校驗任何一個環(huán)節(jié)錯了抓包工具里就是一片沉默。這時候把 verilog-ethernet 這個開源工程拿過來讀性價比是最高的。它是一個純 Verilog 實現(xiàn)的以太網(wǎng)協(xié)議棧支持 1G/2.5G/10G 速率覆蓋 GMII、RGMII、SGMII、XGMII 這些常見 PHY 接口內(nèi)部用 AXI-Stream 做統(tǒng)一的數(shù)據(jù)通路把 ARP、ICMP、IPv4、UDP 拆成獨(dú)立模塊哪一層出問題就單獨(dú)盯哪一層。我寫這篇的目的很直接給同樣處在入門階段、手里有一塊帶網(wǎng)口的 FPGA 板子、想把 UDP 收發(fā)跑起來的人一份能照著抄的工程學(xué)習(xí)筆記。不管你是剛學(xué)完 Verilog 語法還是已經(jīng)能點(diǎn)燈但沒碰過網(wǎng)絡(luò)這篇里的選型思路、模塊拆解、仿真流程和踩坑記錄應(yīng)該都能省你幾天時間。1. 為什么最后選了 verilog-ethernet 這個工程1.1 從零手寫 UDP 協(xié)議棧會撞上哪幾堵墻剛開始我也想過自己從 MAC 層往上擼一遍覺得不就是拼幾個字節(jié)嘛。真正動手才發(fā)現(xiàn)第一堵墻是 PHY 接口時序RGMII 是 DDR 雙邊沿采樣4 位數(shù)據(jù)在時鐘上下沿各傳一次你還得在 FPGA 側(cè)做輸出延遲和輸入延遲對齊不同廠商的 PHY 芯片要求還不一樣差個幾百皮秒就開始丟包。第二堵墻是幀結(jié)構(gòu)以太網(wǎng)幀頭 14 字節(jié)、前導(dǎo)碼 7 字節(jié)、SFD 1 字節(jié)、最小幀 64 字節(jié)、CRC32 校驗?zāi)闵偬钜粋€字段對面直接當(dāng)垃圾幀扔掉。第三堵墻是校驗和IP 頭的 checksum 是 16 位反碼求和再取反UDP 還要額外算偽首部涉及到字節(jié)序和進(jìn)位回卷手寫特別容易出錯。第四堵墻是 ARP你得先發(fā)廣播問“誰是 192.168.1.100”等對方回了 MAC 地址才能發(fā)數(shù)據(jù)中間還有超時重試和緩存老化。這四堵墻疊在一起沒有一兩個月根本理不順而且你調(diào)試的時候根本不知道是自己邏輯錯了還是對面沒收全憑猜。1.2 verilog-ethernet 的定位和分層設(shè)計verilog-ethernet 這個工程最舒服的地方在于它的分層特別干凈。最底下是 PHY 接口層負(fù)責(zé)把 RGMII/GMII 的物理信號轉(zhuǎn)換成 8 位或 32 位的內(nèi)部數(shù)據(jù)流往上是 MAC 層處理前導(dǎo)碼、幀間隔、CRC 生成和校驗、最小幀填充再往上是以太網(wǎng)幀解析層把幀頭剝掉把 payload 交給上層最上面才是 ARP、ICMP、IPv4、UDP 這幾個協(xié)議模塊。每一層之間統(tǒng)一用 AXI-Stream 連接也就是valid、ready、data、last這四根線的握手機(jī)制。這個設(shè)計的好處是你可以只關(guān)心自己那一層想驗證 UDP 收發(fā)邏輯就繞開 PHY直接在仿真里往 UDP 模塊灌 AXI-Stream 數(shù)據(jù)想驗證 PHY 時序就發(fā)固定幀看波形。我第一次跑通 ping 的時候才意識到這種“每層可獨(dú)立測試”的結(jié)構(gòu)對一個初學(xué)者有多重要——它把排錯空間從整個鏈路壓縮到了單個模塊。1.3 為什么不直接用 lwIP 或軟核方案有人會問直接上一個軟核跑 lwIP 不香嗎何必在 Verilog 里遭這個罪。這里得說清楚場景差異。lwIP 是跑在 CPU 上的軟件協(xié)議棧適合那些需要 TCP、HTTP、DHCP 這類復(fù)雜協(xié)議的場合代價是它要占處理器資源和內(nèi)存吞吐還受 CPU 主頻限制。而純 Verilog 協(xié)議棧是硬件并行跑一個時鐘周期處理一批數(shù)據(jù)1Gbps 線速下幾乎不占 CPU延遲穩(wěn)定在微秒級。如果你的板子上本來就沒有軟核或者你的應(yīng)用就是高速采集、實時控制這類對延遲敏感的場景那硬件協(xié)議棧才是對的選擇。verilog-ethernet 還有個優(yōu)勢是純 RTL、無廠商 IP 依賴Xilinx 和 Altera 都能綜合這也讓它成了很多教學(xué)和開源項目的默認(rèn)底座。1.4 上手前要先具備哪些前置知識雖然標(biāo)題寫的是“近似 0 基礎(chǔ)”但讀這個工程還是需要一點(diǎn)鋪墊的。我個人建議至少掌握這幾樣再進(jìn)來一是能看懂a(chǎn)lways (posedge clk)和阻塞、非阻塞賦值的行為差異二是理解同步復(fù)位和異步復(fù)位的區(qū)別三是知道 FIFO 是干什么用的、讀寫指針為什么不能直接拿來當(dāng)握手信號四是最好在仿真里跑過一個帶跨時鐘域的小設(shè)計。缺了這些直接沖進(jìn)來看 RGMII 的 DDR 原語會很痛苦因為那部分代碼里混著廠商原語例化和時序約束?;A(chǔ)不牢的時候讀開源工程最容易犯的錯是“看不懂就跳過”結(jié)果跳過的全是最關(guān)鍵的握手邏輯最后上板不通又回頭補(bǔ)時間反而花得更多。2. 工程目錄與核心模塊逐個拆解2.1 頂層封裝怎么選從 FIFO 版本切入最省事打開工程源碼目錄你會看到rtl下面按功能排了一長串文件第一次看容易懵。我的建議是從頂層封裝文件開始看也就是那些名字里帶_fifo或者_(dá)64bit后綴的封裝。以 1G 速率、RGMII 接口為例eth_mac_1g_rgmii_fifo這個頂層把 PHY 接口、MAC、FIFO 全包進(jìn)去了對外只暴露一個標(biāo)準(zhǔn)的 AXI-Stream 收發(fā)接口。這樣你幾乎不用管 RGMII 的時序細(xì)節(jié)只要把tx_axis和rx_axis接上自己的邏輯就行。FIFO 版本里有一堆參數(shù)值得注意TARGET用來指定廠商原語Xilinx 填XILINXAltera 填A(yù)LTERADATA_WIDTH決定內(nèi)部數(shù)據(jù)位寬8 位是標(biāo)配但可以升到 32 位來降時鐘壓力ENABLE_PADDING控制小于 60 字節(jié)的幀是否自動填充到最小幀長TX_FIFO_DEPTH和RX_FIFO_DEPTH決定緩沖深度跑滿 1G 線速的時候這兩個值別設(shè)太小我一般起始給 4096跑通了再往下調(diào)。2.2 PHY 與 MAC 層GMII 和 RGMII 到底差在哪GMII 和 RGMII 是兩塊最常打交道的接口。GMII 簡單粗暴8 位數(shù)據(jù)配一路 125MHz 時鐘上升沿就是上升沿采樣邏輯一目了然缺點(diǎn)是引腳多8 根數(shù)據(jù)加控制信號占一片 IO。RGMII 為了省引腳把數(shù)據(jù)砍到 4 位同時在時鐘的上升沿和下降沿各傳一次數(shù)據(jù)還額外引入了 TXC 和 RXC 兩根時鐘線。代價是 FPGA 側(cè)必須用 DDR 原語來收發(fā)Xilinx 上對應(yīng)ODDR和IDDR而且要在建立保持時間上做延遲補(bǔ)償通常通過約束或 PHY 內(nèi)部延遲來調(diào)。工程里對這兩種接口都有現(xiàn)成實現(xiàn)我的經(jīng)驗是初學(xué)階段如果你的板子兩種都支持優(yōu)先用 GMII 調(diào)試出問題時波形好讀等邏輯跑通了再切 RGMII 去省引腳切過去之后只需要重點(diǎn)盯 DDR 原語和延遲約束這兩塊。理解這一層的關(guān)鍵是搞清楚數(shù)據(jù)從 PHY 進(jìn)來時是什么節(jié)拍進(jìn)來之后被拼成了幾個字節(jié)、什么時候開始有效這個信息直接決定你后面協(xié)議層模塊的數(shù)據(jù)流怎么接。2.3 AXI-Stream 握手整個工程的通用語言AXI-Stream 這個接口協(xié)議是這個工程的骨架所有模塊之間都靠它說話。它其實就四根關(guān)鍵線valid表示發(fā)送方數(shù)據(jù)有效ready表示接收方可以收data是數(shù)據(jù)總線last表示這是一幀的最后一個數(shù)據(jù)。握手規(guī)則是只有當(dāng)valid和ready在同一個時鐘上升沿同時為高時數(shù)據(jù)才算真正傳輸成功。這個規(guī)則看起來簡單但初學(xué)最容易犯的錯是把它當(dāng)普通使能信號用比如只看valid就開始處理數(shù)據(jù)或者last和valid不同步拉高。正確的心智模型是“一次握手一筆交易”ready沒來之前valid和data都要保持不動不能因為時鐘走了一拍就把數(shù)據(jù)換掉。我剛開始寫接收邏輯時last位置判斷錯了一位結(jié)果整幀數(shù)據(jù)永遠(yuǎn)差一個字節(jié)抓包看到長度不對又找不到原因最后對著波形一拍一拍數(shù)才定位到。讀懂 AXI-Stream這個工程就通了一半。2.4 ARP、IP、UDP、ICMP 四塊積木怎么拼協(xié)議層這塊工程拆得特別清楚最大的一個封裝叫udp_complete名字就直接告訴你它把 UDP 相關(guān)的東西全裝好了。里面主要包含四塊arp負(fù)責(zé)地址解析收到廣播請求就回自己的 MAC同時把別人的 IP-MAC 對應(yīng)關(guān)系緩存下來ARP_CACHE_SIZE默認(rèn)給 8 條夠小網(wǎng)絡(luò)用ip負(fù)責(zé) IPv4 頭解析和生成包括版本號、頭長度、TTL、協(xié)議號這些字段ip_tx負(fù)責(zé)把用戶數(shù)據(jù)加上 IP 頭再算校驗和udp是核心業(yè)務(wù)層udp_rx剝掉 UDP 頭把 payload 往外送udp_tx加上源端口、目的端口、長度和校驗和icmp單獨(dú)拿出來專門處理 ping 請求回了 ping 包才算網(wǎng)絡(luò)層通了。這四塊拼裝的順序是以太網(wǎng)幀進(jìn)ipip看協(xié)議號分流到udp或icmp。我第一次看的時候不知道從哪下手后來發(fā)現(xiàn)只要記住“數(shù)據(jù)從哪進(jìn)、往哪出、不匹配的分流到哪”這三件事整個數(shù)據(jù)流就理順了。2.5 參數(shù)化設(shè)計里的幾個關(guān)鍵配置這個工程全程參數(shù)化很多行為靠參數(shù)開關(guān)。我挑幾個實際會改的說說。UDP_CHECKSUM_ENABLE控制 UDP 校驗和是否生成如果你的上位機(jī)不校驗關(guān)掉能省點(diǎn)邏輯但網(wǎng)絡(luò)里混有校驗的設(shè)備時最好開著不然對面可能拒收。IPV4_ADDR和IPV4_MAC_ADDR是本機(jī)地址必須和上位機(jī)配成同一網(wǎng)段我踩過把板子 IP 設(shè)成 192.168.1.x 而電腦是 192.168.0.x 的坑ping 了半天才發(fā)現(xiàn)網(wǎng)段不通。ARP_REQUEST_RETRY_INTERVAL決定多久重發(fā)一次 ARP 請求網(wǎng)絡(luò)稍微慢點(diǎn)就把它調(diào)大。還有MII_*一堆參數(shù)跟 PHY 管理接口有關(guān)用于配置 PHY 的速率和雙工模式如果你的板子用 MDIO 配置 PHY這組參數(shù)得對著手冊填。3. 實操過程從仿真到上板的完整鏈路3.1 仿真環(huán)境搭建先讓數(shù)據(jù)流在波形里跑起來我強(qiáng)烈建議第一步不要上板先在仿真里把協(xié)議層跑通。工程自帶的 testbench 通常都放在tb或者sim目錄下里面會例化 UDP 頂層然后造一幀數(shù)據(jù)從tx灌進(jìn)去再從rx讀出來對比??上Т蟛糠珠_源工程的 testbench 不完整需要你自己補(bǔ)一點(diǎn)。我的做法是寫一個最簡單的激勵給udp_tx的 AXI-Stream 灌一幀固定內(nèi)容比如0x11 0x22 0x33 ...同時在udp_rx側(cè)監(jiān)聽輸出用$display或者寫文件的方式把收到的 payload 打印出來。仿真跑通之后再往上層加 ARP 和 IP。這一步的核心目的是驗證你的握手時序理解對不對因為仿真是唯一能看到每一拍信號的地方上板抓波形成本太高。這里有個小技巧用$dumpfile和$dumpvars存 VCD再用 GTKWave 打開把valid、ready、last加進(jìn)波形組這樣握手有沒有問題是肉眼可見的。3.2 引腳、時鐘、復(fù)位三件套怎么接上板前的準(zhǔn)備工作里引腳約束、時鐘、復(fù)位是最容易出問題的三樣。先說時鐘RGMII 方案通常需要一個 125MHz 的參考時鐘給 PHY有的板子由 PHY 芯片回傳 RXC有的需要 FPGA 自己產(chǎn)生。如果工程配置成 FPGA 輸出時鐘那你要在頂層例化一個 MMCM 或 PLL把晶振頻率倍頻到 125MHz 并且注意相位。引腳約束要嚴(yán)格對著板子原理圖來我就因為把 RGMII 的 TXD 和 TXC 順序?qū)懛凑垓v了一晚上。復(fù)位這塊工程用的是同步復(fù)位所以你不能把外部按鍵直接接到rst上中間要加兩級同步器消抖和打拍否則按鍵抖動會造成狀態(tài)機(jī)誤復(fù)位。另外注意上電順序PHY 芯片需要時間完成內(nèi)部初始化主邏輯復(fù)位釋放得太早第一批數(shù)據(jù)會丟我一般讓復(fù)位信號保持幾百毫秒再釋放或者干脆等 PHY 鏈路上的 RXC 穩(wěn)定后再放。3.3 與 PC 聯(lián)調(diào)先把 ping 跑通再談 UDP上板后第一件事不是發(fā) UDP 數(shù)據(jù)而是 ping。為什么先 ping因為 ping 走的是 ICMP它不需要 ARP 之外的其他解析路徑最短能通就說明物理層、MAC 層、IP 層、ARP 全部沒問題后面 UDP 出問題就可以只在 UDP 模塊里找。聯(lián)調(diào)步驟是這樣先把板子和電腦用網(wǎng)線直連電腦端把 IP 設(shè)成和板子同網(wǎng)段比如板子是 192.168.1.10電腦設(shè) 192.168.1.100掩碼 255.255.255.0。然后在電腦上ping 192.168.1.10同時用抓包工具看有沒有 ARP 請求發(fā)出去。如果 ARP 有請求但板子沒回檢查IPV4_MAC_ADDR參數(shù)是不是填錯了如果 ARP 回了但 ping 不通檢查 ICMP 模塊的校驗和計算。我第一次 ping 通的那一刻真的挺激動因為從那之后所有的問題都變成了局部問題。3.4 用 UDP 打流驗證數(shù)據(jù)通路ping 通之后開始測 UDP。最土但最有效的辦法是寫一個簡單的上位機(jī)腳本用 Python 的 socket 往板子發(fā)數(shù)據(jù)同時板子把收到的數(shù)據(jù)原樣回發(fā)看內(nèi)容對不對。這里要注意端口號板子的udp_rx側(cè)只認(rèn)自己配置的目的端口上位機(jī)發(fā)的目標(biāo)端口要對上。發(fā)送頻率也從慢到快先一秒鐘一包跑通了再加到線速。如果一開始就滿速發(fā)緩沖來不及處理會丟包你反而以為是邏輯錯了。我實測下來1G 鏈路上用 32 位數(shù)據(jù)位寬、FIFO 深度給夠丟包率可以做到 0。另外建議在板子側(cè)加一個計數(shù)器統(tǒng)計正確收到的包數(shù)和校驗失敗的包數(shù)用 LED 或者串口打出來這樣你在不看抓包的情況下也能判斷鏈路健康度。3.5 關(guān)鍵的時序約束和跨時鐘域處理上板跑通之后如果想跑穩(wěn)時序約束是繞不過去的。RGMII 的收發(fā)路徑上PHY 時鐘和內(nèi)部邏輯時鐘是不同源的屬于典型的跨時鐘域場景。工程里通常用異步 FIFO 來做隔離你要做的是給這兩個時鐘域分別建時鐘約束并且給跨域的路徑加set_false_path或者set_clock_groups把它們分開否則工具會去優(yōu)化那些本來不該優(yōu)化的路徑反而引入問題。我第一次沒加約束綜合報了上百個時序違例后來加了時鐘組就好了。另外如果時鐘是自己用 MMCM 生成的別忘了給生成的時鐘建約束還要檢查 MMCM 的鎖定信號是否參與了復(fù)位釋放邏輯。4. 常見問題與排查技巧實錄4.1 問題速查表現(xiàn)象可能原因排查方向完全 ping 不通IP 不同網(wǎng)段、MAC 參數(shù)錯、ARP 沒工作抓包看是否有 ARP 請求和回復(fù)ARP 通但 ping 不通ICMP 校驗和錯、TTL 為 0看 ICMP 模塊生成的回包字段ping 通但 UDP 收不到目的端口不匹配、UDP 校驗和錯抓包看 UDP 頭是否符合預(yù)期偶爾丟包緩沖太淺、時鐘約束缺失加大 FIFO 深度、補(bǔ)時序約束收到的數(shù)據(jù)偏移一位last判斷位置錯、握手采錯拍波形逐拍核對valid/ready/last上電后無反應(yīng)復(fù)位釋放時機(jī)早、PHY 未初始化檢查復(fù)位時長和 MDIO 配置這張表是我自己踩坑攢下來的遇到問題先對照著縮小范圍比盲目改代碼快得多。特別是“收到的數(shù)據(jù)偏移一位”這類問題八成是握手邏輯寫錯了先在波形里確認(rèn)last拉高的那一拍對應(yīng)的是不是最后一個字節(jié)再回頭改代碼。4.2 波形定位三步法怎么用波形快速定位問題我總結(jié)了三步。第一步看頂層的valid和ready如果有一方一直不拉高說明卡在握手上了往下找是哪個模塊沒準(zhǔn)備好。第二步看last一幀數(shù)據(jù)里last只應(yīng)該出現(xiàn)一次如果出現(xiàn)多次或者根本沒出現(xiàn)說明長度信息有問題。第三步看數(shù)據(jù)內(nèi)容的前幾個字節(jié)正常以太網(wǎng)幀開頭應(yīng)該是目的 MAC 地址如果開頭全是 0 或者亂的說明數(shù)據(jù)流接錯了源頭。這三步走下來八成的問題都能定位到具體模塊。我個人的習(xí)慣是把關(guān)鍵信號提前在仿真里打標(biāo)記上板時再用集成邏輯分析工具抓同樣的信號兩邊對比著看效率會高很多。4.3 幾個讓我印象深刻的坑第一個坑是字節(jié)序。Verilog 里數(shù)據(jù)總線通常按大端排列但我在拼 IP 頭的時候按小端思維把地址填反了結(jié)果包發(fā)出去了但對面不認(rèn)。后來改成嚴(yán)格按照字段定義從高字節(jié)往低字節(jié)擺問題就沒了。第二個坑是 UDP 長度字段它指的是“UDP 頭 數(shù)據(jù)”的總長度不包括以太網(wǎng)頭和 IP 頭我一開始把整個幀長填進(jìn)去了被抓包工具標(biāo)紅。第三個坑是 ARP 緩存的老化時間默認(rèn)幾分鐘就過期長時間不發(fā)數(shù)據(jù)之后突然要發(fā)會先卡在 ARP 請求上表現(xiàn)出來就是“隔一會兒第一包丟”。解決方式是讓應(yīng)用層在發(fā)送前檢查 ARP 狀態(tài)或者把緩存超時調(diào)長。這些坑網(wǎng)上文檔基本不講都是自己撞出來的寫出來給后面的人省點(diǎn)時間。4.4 關(guān)于仿真授權(quán)和工具鏈的小提醒另外提一句工具鏈的問題。有些人跑仿真時會遇到授權(quán)報錯比如拿不到仿真 license這種情況一般是環(huán)境變量或者工具配置問題不是代碼本身的問題。我的建議是仿真優(yōu)先用開源工具鏈配合iverilog或者 Verilator 做功能驗證綜合和上板再用廠商工具這樣能避開一堆授權(quán)和版本兼容的坑。verilog-ethernet 本身是純 RTL跨工具綜合能力不錯但你要注意它里面用到的廠商原語在仿真時可能需要對應(yīng)的庫支持遇到找不到模塊的報錯先確認(rèn)是不是原語庫沒加到仿真路徑里。5. 跑通之后還能往哪擴(kuò)展把基礎(chǔ) UDP 收發(fā)跑通之后這個工程其實是個很好的起點(diǎn)。往性能方向走可以試著把DATA_WIDTH從 8 位提到 32 位甚至 64 位看看吞吐能不能翻倍同時觀察 FIFO 深度和時鐘頻率的配合往協(xié)議方向走工程里已經(jīng)有不少上層接口的預(yù)留你可以在 UDP 之上自己定義一個簡單的應(yīng)用層協(xié)議加個序號和校驗字段做成可靠傳輸往應(yīng)用方向走把數(shù)據(jù)采集邏輯掛到udp_tx前面就變成了一個網(wǎng)絡(luò)化的采集節(jié)點(diǎn)比如把 ADC 數(shù)據(jù)或者圖像數(shù)據(jù)打 UDP 包發(fā)出去實時性比軟核方案好很多。我自己下一步打算做的是把 UDP 接收和一個小型狀態(tài)機(jī)結(jié)合根據(jù)收到的命令字去控制板子上的外設(shè)相當(dāng)于一個最簡的網(wǎng)絡(luò)控制節(jié)點(diǎn)。整個過程里最值錢的其實不是代碼本身而是排錯那套方法先分層、再仿真、再抓波形、最后對照速查表這套流程走到哪都通用。