免軟核:W5500硬件協(xié)議棧驅(qū)動(dòng)設(shè)計(jì)與實(shí)測(cè))
簡(jiǎn)介這是一份面向FPGA開(kāi)發(fā)者的W5500驅(qū)動(dòng)實(shí)現(xiàn)與范例工程定位于解決W5500與FPGA之間SPI接口設(shè)計(jì)、狀態(tài)機(jī)驅(qū)動(dòng)及高速數(shù)據(jù)收發(fā)等實(shí)際問(wèn)題。W5500內(nèi)置硬件TCP/IP協(xié)議棧配合FPGA可達(dá)到3.5M字節(jié)/秒的實(shí)測(cè)傳輸速度接口簡(jiǎn)潔易用適用于工業(yè)控制、物聯(lián)網(wǎng)設(shè)備、遠(yuǎn)程監(jiān)控等需要實(shí)時(shí)網(wǎng)絡(luò)通信的場(chǎng)景。壓縮包大小為21.55MB內(nèi)含可直接下載使用的應(yīng)用示例與驅(qū)動(dòng)代碼開(kāi)發(fā)者可據(jù)此快速完成初始化配置、SPI時(shí)序控制、中斷處理及數(shù)據(jù)收發(fā)功能的移植或二次開(kāi)發(fā)。已有1212人學(xué)習(xí)下載工程結(jié)構(gòu)清晰適合具備一定FPGA基礎(chǔ)、希望快速集成以太網(wǎng)通信能力的開(kāi)發(fā)者參考。 搞FPGA以太網(wǎng)這個(gè)需求最怕的不是寫代碼而是協(xié)議棧那一大堆狀態(tài)。之前項(xiàng)目里遇到網(wǎng)絡(luò)功能我第一反應(yīng)是掛軟核跑lwIP后來(lái)?yè)Q用W5500這顆芯片才發(fā)現(xiàn)很多場(chǎng)景下根本不用那么折騰。硬件協(xié)議棧把TCP/IP全部固化在芯片內(nèi)部FPGA這邊只需要通過(guò)SPI接口讀寫寄存器相當(dāng)于把網(wǎng)絡(luò)協(xié)議棧簡(jiǎn)化成一個(gè)“讀寫外部寄存器”的活。我在實(shí)際工程里用它跑下載實(shí)測(cè)速度穩(wěn)定在3.5M字節(jié)/秒左右也就是大概28Mbps的有效吞吐做遠(yuǎn)程配置、數(shù)據(jù)采集、固件升級(jí)這類應(yīng)用完全夠用。這篇文章就圍繞這個(gè)項(xiàng)目梳理一下為什么選W5500而不是軟核方案、SPI協(xié)議里有哪些關(guān)鍵寄存器、FPGA驅(qū)動(dòng)模塊怎么設(shè)計(jì)、3.5M字節(jié)/秒是怎么測(cè)出來(lái)的以及下載下來(lái)的驅(qū)動(dòng)怎樣才能做到“直接用”。如果你是第一次在FPGA上接網(wǎng)絡(luò)功能這篇文章能省你不少查手冊(cè)的時(shí)間。1. 選型邏輯FPGA配W5500而不是CPU軟核跑lwIP1.1 硬件協(xié)議棧把網(wǎng)絡(luò)功能變成了寄存器訪問(wèn)FPGA上實(shí)現(xiàn)以太網(wǎng)其實(shí)有三條路可以走。第一是用內(nèi)嵌MAC加外部PHY自己寫或者移植TCP/IP協(xié)議棧靈活度最高但工作量非常大要處理ARP、IP分片、TCP重傳、超時(shí)管理這些細(xì)節(jié)。第二是在FPGA里跑MicroBlaze或者Nios II軟核再在軟核上跑lwIP協(xié)議棧好處是代碼生態(tài)成熟壞處是軟件調(diào)試鏈路長(zhǎng)而且軟核和緩存會(huì)占用不少片內(nèi)資源。第三就是今天說(shuō)的W5500方案芯片內(nèi)部把MAC、PHY、TCP/IP協(xié)議棧全部做成了硬件邏輯FPGA只負(fù)責(zé)通過(guò)SPI發(fā)命令和搬運(yùn)數(shù)據(jù)。從FPGA開(kāi)發(fā)者的視角看W5500就是一個(gè)SPI從設(shè)備你不需要懂TCP的擁塞控制不需要處理ACK重傳只要按順序操作寄存器協(xié)議棧內(nèi)部會(huì)自動(dòng)把連接狀態(tài)維護(hù)好。這個(gè)思路很像在生活中請(qǐng)了一個(gè)專門處理信件往來(lái)的文員你只需要把信投進(jìn)郵箱剩下寄信、回執(zhí)、催件都是他搞定。對(duì)FPGA工程師來(lái)說(shuō)這種“把復(fù)雜協(xié)議藏起來(lái)”的做法特別解壓因?yàn)闀r(shí)序和狀態(tài)機(jī)才是我們擅長(zhǎng)的事情。1.2 這套組合最適合什么樣的項(xiàng)目我實(shí)際用下來(lái)覺(jué)得這套方案最適合下面幾類場(chǎng)景。一類是吞吐需求在幾十Mbps以內(nèi)就夠用的工業(yè)數(shù)據(jù)采集和遠(yuǎn)程監(jiān)控設(shè)備這類設(shè)備往往既要聯(lián)網(wǎng)又不能把FPGA資源都花在協(xié)議棧上。另一類是FPGA里已經(jīng)跑了很多信號(hào)處理或者圖像算法邏輯剩余資源緊張根本塞不下一個(gè)完整的CPU子系統(tǒng)。還有一類是項(xiàng)目排期緊希望一兩周內(nèi)就能把網(wǎng)絡(luò)功能穩(wěn)定接起來(lái)前期不想花太多時(shí)間在協(xié)議棧上。反過(guò)來(lái)它也有明顯的邊界。如果項(xiàng)目要求萬(wàn)兆或者至少千兆線速轉(zhuǎn)發(fā)W5500完全不夠看如果需要同時(shí)維護(hù)幾十個(gè)TCP連接或者要處理非標(biāo)準(zhǔn)私有協(xié)議硬件協(xié)議棧的靈活性也會(huì)成為限制。W5500最多提供4個(gè)Socket每個(gè)Socket的收發(fā)緩沖是可配置的但協(xié)議棧是固化的想改TCP窗口策略或者加私有選項(xiàng)幾乎不可能。選型時(shí)候要有這個(gè)預(yù)期別拿它和通用處理器去比。2. W5500 SPI協(xié)議要點(diǎn)三段式幀與關(guān)鍵寄存器2.1 三段式SPI幀結(jié)構(gòu)W5500的SPI每次完整訪問(wèn)都分三個(gè)階段。地址階段是16bit最高位表示讀還是寫接著3bit選擇寄存器區(qū)域低12bit是寄存器偏移地址。區(qū)域選擇可以指向通用寄存器、Socket0到Socket3的寄存器塊以及對(duì)應(yīng)的收發(fā)數(shù)據(jù)緩沖區(qū)??刂齐A段是8bit主要用低3位來(lái)表示讀操作的數(shù)據(jù)長(zhǎng)度這也是W5500支持連續(xù)讀的關(guān)鍵如果是連續(xù)傳輸幾十字節(jié)這個(gè)控制字段能告訴芯片接下來(lái)要讀多長(zhǎng)。數(shù)據(jù)階段就是真正要讀寫的那些數(shù)據(jù)字節(jié)。SPI模式要用模式0或者模式3實(shí)際工程里我習(xí)慣配成CPOL0、CPHA0。片選信號(hào)在整次訪問(wèn)期間必須保持低電平訪問(wèn)結(jié)束再拉高。有些調(diào)不通的板子問(wèn)題就出在CS時(shí)序上CS和SCK相對(duì)關(guān)系不對(duì)讀出來(lái)的數(shù)據(jù)全是0xFF。建議上板之前先用邏輯分析儀抓一下CS、SCK、MOSI、MISO四根線確認(rèn)波形和手冊(cè)一致再往下寫驅(qū)動(dòng)邏輯。2.2 初始化、TCP連接、收發(fā)數(shù)據(jù)要關(guān)注哪些寄存器以TCP客戶端為例最小操作序列大概是這樣的。先寫MR寄存器配置芯片做軟復(fù)位然后延時(shí)一段時(shí)間等待芯片穩(wěn)定。接著設(shè)置Sn_MR把當(dāng)前Socket配置成TCP模式往Sn_CR寫OPEN命令。再之后寫目標(biāo)IP地址和端口發(fā)CONNECT命令輪詢Sn_SR直到進(jìn)入SOCK_ESTABLISHED狀態(tài)。發(fā)送數(shù)據(jù)的時(shí)候先讀Sn_TX_FSR確認(rèn)發(fā)送緩沖剩余空間把用戶數(shù)據(jù)連續(xù)寫到Sn_TX_BUF區(qū)域更新Sn_TX_WR寫指針再往Sn_CR發(fā)SEND命令。接收數(shù)據(jù)時(shí)先讀Sn_RX_RSR看接收緩沖有多少字節(jié)有數(shù)據(jù)就連續(xù)讀Sn_RX_BUF讀完更新Sn_RX_RD讀指針最后發(fā)RECV命令釋放緩沖。這個(gè)流程里最容易出錯(cuò)的是指針更新Sn_TX_WR、Sn_RX_RD這類指針最好連續(xù)寫兩次讓芯片內(nèi)部正確鎖存邊界否則會(huì)在高負(fù)載下偶發(fā)丟包。3. FPGA驅(qū)動(dòng)模塊設(shè)計(jì)SPI狀態(tài)機(jī)與對(duì)外接口3.1 SPI主機(jī)狀態(tài)機(jī)的實(shí)現(xiàn)思路驅(qū)動(dòng)模塊的底層需要實(shí)現(xiàn)一個(gè)SPI主機(jī)它負(fù)責(zé)把地址、控制、數(shù)據(jù)三個(gè)階段按位串行送出去。我用移位寄存器加計(jì)數(shù)器來(lái)做一個(gè)發(fā)送移位寄存器、一個(gè)接收移位寄存器、一個(gè)bit計(jì)數(shù)器主狀態(tài)機(jī)在IDLE、ADDR、CTRL、DATA、DONE之間跳轉(zhuǎn)。ADDR階段把16bit地址逐位移出CTRL階段送控制字節(jié)DATA階段如果是寫則將數(shù)據(jù)逐位移出如果是讀則從MISO采樣并拼成字節(jié)。網(wǎng)上很多驅(qū)動(dòng)喜歡把狀態(tài)機(jī)做得特別復(fù)雜用多個(gè)嵌套的條件判斷。我按字節(jié)邊界精簡(jiǎn)狀態(tài)后整個(gè)狀態(tài)機(jī)非常干凈。關(guān)鍵點(diǎn)在于SPI是位流但寄存器是字節(jié)對(duì)齊的所以每次狀態(tài)切換必須發(fā)生在字節(jié)邊界上不能用半字節(jié)判斷。這里用計(jì)數(shù)器對(duì)8取模到0的時(shí)候再切換時(shí)序上不容易出氣泡。3.2 對(duì)外接口寫地址、給數(shù)據(jù)、看結(jié)果驅(qū)動(dòng)模塊設(shè)計(jì)完以后頂層接口我刻意做得非??酥品殖闪巳M。配置類接口就是一組簡(jiǎn)單的讀寫握手cfg_valid、cfg_addr、cfg_wdata、cfg_rdata、cfg_done用戶想讀寫W5500任意寄存器直接給地址給數(shù)據(jù)就行。發(fā)送類接口用了類似AXI-Stream的風(fēng)格有tx_valid、tx_ready、tx_data和tx_last整包數(shù)據(jù)輸入模塊內(nèi)部在包尾自動(dòng)觸發(fā)SEND命令。接收類接口同樣有rx_valid、rx_ready、rx_data、rx_lastW5500收到完整數(shù)據(jù)包后驅(qū)動(dòng)會(huì)把數(shù)據(jù)流水一樣輸出。我給上層用戶留了一組狀態(tài)信號(hào)鏈路建立后會(huì)拉高link_upTCP連接成功后會(huì)拉高socket_established。用這三個(gè)接口用戶完全不需要了解W5500的寄存器映射、SPI幀格式和Socket模式只要在連接建立后往發(fā)送接口喂數(shù)據(jù)有數(shù)據(jù)進(jìn)來(lái)接收接口就會(huì)給valid脈沖需要流控就拉低ready。很多拿到驅(qū)動(dòng)的人第一反應(yīng)是“這就完事了”但確實(shí)不需要更多東西接口做得直接使用門檻才低。4. 實(shí)測(cè)3.5M字節(jié)/秒測(cè)法、限速分析與提速4.1 測(cè)試環(huán)境和測(cè)法測(cè)試環(huán)境是Kintex-7開(kāi)發(fā)板加一塊W5500模塊PC用網(wǎng)線直連模塊的RJ45座PC端跑iperf3或者直接通過(guò)HTTP下載文件。FPGA側(cè)SPI時(shí)鐘最開(kāi)始只配了20MHz實(shí)測(cè)下載速度只有1.2M字節(jié)/秒左右。后來(lái)把SPI時(shí)鐘分頻系數(shù)調(diào)小提升到50MHz之后實(shí)測(cè)速度穩(wěn)定在3.5M字節(jié)/秒。這個(gè)數(shù)字對(duì)應(yīng)約28Mbps有效吞吐雖然和千兆網(wǎng)完全不是一個(gè)量級(jí)但用來(lái)做現(xiàn)場(chǎng)數(shù)據(jù)采集和遠(yuǎn)程運(yùn)維已經(jīng)很夠用。測(cè)量下載速度比ping更有參考價(jià)值因?yàn)橄螺d是連續(xù)的大數(shù)據(jù)包能同時(shí)壓測(cè)SPI總線、內(nèi)部緩沖和協(xié)議棧處理能力。ping只能說(shuō)明連接通斷測(cè)不出吞吐天花板。建議大家在項(xiàng)目驗(yàn)收時(shí)也用iperf這類工具壓一壓收發(fā)通路這樣對(duì)整個(gè)系統(tǒng)的性能上限會(huì)更有數(shù)。4.2 為什么上限在這里以及還能怎么擠理論上SPI時(shí)鐘50MHz下字節(jié)速率是6.25M字節(jié)/秒實(shí)測(cè)3.5M字節(jié)/秒意味著有效負(fù)載只占56%左右其余開(kāi)銷主要在三個(gè)地方。第一個(gè)是寄存器讀寫都要額外傳2字節(jié)地址加1字節(jié)控制如果頻繁單字節(jié)讀寫有效吞吐會(huì)非常低。第二個(gè)是W5500內(nèi)部Socket緩沖操作有同步等待寫指針更新后芯片內(nèi)部需要時(shí)間完成狀態(tài)切換。第三個(gè)是收發(fā)共用同一根SPI總線半雙工機(jī)制決定了不能一邊收一邊發(fā)。實(shí)測(cè)中最有效的優(yōu)化是減少小幀讀寫頻率盡量使用連續(xù)塊傳輸。讀Sn_RX_BUF的時(shí)候一次把整包數(shù)據(jù)讀完而不是分多次小幀讀寫Sn_TX_BUF的時(shí)候把整包數(shù)據(jù)積攢在一起連續(xù)寫幾十甚至幾百字節(jié)再發(fā)SEND命令。同樣條件下連續(xù)讀256字節(jié)比逐字節(jié)讀吞吐提升接近一倍這個(gè)差距在高速采集項(xiàng)目中非常明顯。5. 調(diào)試中踩過(guò)的三個(gè)坑與最終定位5.1 復(fù)位時(shí)序和初始化順序第一個(gè)坑是上電后立刻配置寄存器會(huì)偶發(fā)失敗。W5500內(nèi)部PHY上電需要穩(wěn)定時(shí)間芯片還要做內(nèi)部校準(zhǔn)這個(gè)期間SPI雖然能訪問(wèn)但寄存器的配置可能不生效。后來(lái)在驅(qū)動(dòng)初始化狀態(tài)機(jī)里加了嚴(yán)格時(shí)序先拉低RST引腳保持至少50us釋放后再延時(shí)等待芯片內(nèi)部穩(wěn)定最后才開(kāi)始寫MR軟復(fù)位并執(zhí)行后續(xù)配置。加了這一個(gè)狀態(tài)之后實(shí)測(cè)上電成功率從大概70%提升到了接近100%。這個(gè)坑在仿真階段幾乎發(fā)現(xiàn)不了因?yàn)榉抡婺P筒粫?huì)模擬PHY上電時(shí)序只有在真機(jī)上電時(shí)才暴露。建議拿到開(kāi)發(fā)板先寫一個(gè)最簡(jiǎn)單的寄存器回讀測(cè)試確認(rèn)版本寄存器能讀到正確值再繼續(xù)配置Socket否則問(wèn)題排查時(shí)很難分清是時(shí)序問(wèn)題還是協(xié)議問(wèn)題。5.2 接收緩沖環(huán)形回卷和指針更新第二個(gè)坑出現(xiàn)在接收方向。W5500的接收緩沖本質(zhì)是環(huán)形緩沖區(qū)如果Sn_RX_RSR指示的數(shù)據(jù)量跨越了緩沖末尾直接線性讀取會(huì)讀到回卷邊界之外數(shù)據(jù)就錯(cuò)位了。正確處理是每次接收前先根據(jù)當(dāng)前讀指針計(jì)算到緩沖末尾還剩多少空間再和Sn_RX_RSR取最小值分段讀取讀完后統(tǒng)一更新Sn_RX_RD指針并執(zhí)行RECV命令。我一開(kāi)始圖省事直接按Sn_RX_RSR長(zhǎng)度連續(xù)讀結(jié)果在數(shù)據(jù)量大時(shí)經(jīng)常出現(xiàn)半個(gè)包亂碼。后來(lái)抓了W5500的輸出波形才發(fā)現(xiàn)是環(huán)形回卷導(dǎo)致的問(wèn)題。這個(gè)坑在普通TCP連接測(cè)試中不一定觸發(fā)只有長(zhǎng)時(shí)間高負(fù)載跑數(shù)據(jù)時(shí)才會(huì)暴露排查起來(lái)比較隱蔽。5.3 中斷標(biāo)志位的清除方式第三個(gè)坑是中斷標(biāo)志的清除方式。W5500的中斷寄存器是寫1清除但有些Socket的中斷標(biāo)志位如果清除不干凈會(huì)導(dǎo)致后續(xù)中斷不觸發(fā)。我們遇到過(guò)TCP連接建立后只能收到第一包數(shù)據(jù)之后就完全沒(méi)有中斷的情況最后定位到是清中斷時(shí)用了整字節(jié)覆蓋把不該清的狀態(tài)位也寫掉了。正確做法是讀-改-寫只把當(dāng)前需要處理的中斷位寫1其他位保持原值。這個(gè)坑特別容易出現(xiàn)在復(fù)用了別人代碼的情況下大家在處理中斷標(biāo)志位時(shí)都要養(yǎng)成讀-改-寫的習(xí)慣別直接賦值一個(gè)固定常量。中斷標(biāo)志處理錯(cuò)誤不會(huì)立刻暴露通常在連續(xù)收發(fā)一段時(shí)間后才會(huì)讓協(xié)議??ㄋ蓝ㄎ怀杀竞芨?。6. 下載即可用的移植方式與擴(kuò)展建議6.1 引腳約束和時(shí)鐘分頻配置下載驅(qū)動(dòng)源碼后第一步是核對(duì)W5500模塊的引腳。大多數(shù)W5500模塊是3.3V電平FPGA端IO要對(duì)應(yīng)配置成LVCMOS33。如果開(kāi)發(fā)板IO是2.5V或者1.8V需要加電平轉(zhuǎn)換或選擇兼容的Bank。復(fù)位信號(hào)要連接到模塊的RST引腳片選、SCK、MOSI、MISO四根線對(duì)應(yīng)連接。部分模塊還帶中斷引腳用來(lái)告訴主機(jī)有事件發(fā)生如果不想輪詢中斷標(biāo)志可以把這個(gè)引腳接到FPGA的輸入中斷上。時(shí)鐘分頻參數(shù)SPI_CLK_DIV決定SPI實(shí)際速率。外部輸入時(shí)鐘如果是50MHz建議先配一個(gè)比較大的分頻系數(shù)讓SPI時(shí)鐘穩(wěn)定工作在10MHz到20MHz先把鏈路調(diào)通再逐步提高頻率。不要一上來(lái)就直接跑到W5500支持的上限真機(jī)走線、電平轉(zhuǎn)換都會(huì)影響時(shí)序留點(diǎn)余量更穩(wěn)。6.2 頂層例化與驗(yàn)證路徑驅(qū)動(dòng)模塊的例化非常直接下面是一個(gè)最小化的示例w5500_driver #( .SPI_CLK_DIV(4) ) u_w5500 ( .clk (clk_50m), .rst_n (rst_n), .spi_cs_n (w5500_cs_n), .spi_sck (w5500_sck), .spi_mosi (w5500_mosi), .spi_miso (w5500_miso), .rst_out (w5500_rst), .link_up (link_up), .socket_established (conn_flag), .tx_valid (user_tx_valid), .tx_ready (user_tx_ready), .tx_data (user_tx_data), .tx_last (user_tx_last), .rx_valid (user_rx_valid), .rx_ready (user_rx_ready), .rx_data (user_rx_data), .rx_last (user_rx_last) );接好例化之后建議先做一次簡(jiǎn)單的回環(huán)自測(cè)在FPGA內(nèi)部把接收FIFO的數(shù)據(jù)再回填到發(fā)送接口板上跑起來(lái)后PC端用TCP工具發(fā)送一段數(shù)據(jù)如果FPGA能把數(shù)據(jù)原樣回傳說(shuō)明底層通路已經(jīng)通了一半。然后再跑iperf或者大文件下載驗(yàn)證持續(xù)吞吐。這樣分階段驗(yàn)證出錯(cuò)時(shí)能快速縮小范圍。6.3 后續(xù)擴(kuò)展的幾個(gè)方向驅(qū)動(dòng)本身做得很精簡(jiǎn)后續(xù)要擴(kuò)展也不難。想做多路通信可以繼續(xù)開(kāi)放其他Socket四個(gè)Socket都能獨(dú)立工作。想把驅(qū)動(dòng)掛到復(fù)雜系統(tǒng)里可以把發(fā)送接收接口前面加一個(gè)FIFO做速率匹配避免用戶邏輯偶爾忙不過(guò)來(lái)時(shí)丟包。如果項(xiàng)目里處理器是ZYNQ也可以把底層的SPI主接口換成AXI接口方便ARM端遠(yuǎn)程訪問(wèn)。整體來(lái)看這套W5500的FPGA驅(qū)動(dòng)方案最大的價(jià)值是把網(wǎng)絡(luò)協(xié)議棧的復(fù)雜度屏蔽掉了留下的就是很標(biāo)準(zhǔn)的讀寫、收發(fā)、狀態(tài)檢測(cè)接口。實(shí)測(cè)3.5M字節(jié)/秒的速度在工業(yè)場(chǎng)景里完全夠用接口簡(jiǎn)潔到接上就能跑這也是為什么我們?cè)陧?xiàng)目里把網(wǎng)絡(luò)功能固定成這個(gè)方案的原因。如果你也在給FPGA找網(wǎng)絡(luò)方案建議按這個(gè)思路先跑通一個(gè)最小系統(tǒng)再按實(shí)際需要擴(kuò)展。本文還有配套的精品資源點(diǎn)擊獲取