議棧方案:從SPI驅(qū)動(dòng)到LWIP移植)
簡介這是一份面向STM32嵌入式網(wǎng)絡(luò)開發(fā)者的LWIPW5500組合參考工程。W5500內(nèi)置硬件TCP/IP協(xié)議棧LWIP負(fù)責(zé)上層協(xié)議管理兩者結(jié)合可在資源受限MCU上快速搭建TCP/UDP以太網(wǎng)通信適用于物聯(lián)網(wǎng)網(wǎng)關(guān)、工業(yè)數(shù)據(jù)采集等場(chǎng)景適合已有一定STM32基礎(chǔ)、希望學(xué)習(xí)輕量協(xié)議棧移植的開發(fā)者。壓縮包共188個(gè)文件大小3.48MB以C源文件、H頭文件為主體配套Keil工程配置、啟動(dòng)匯編、MAP/LST輔助文件及PDF/TXT文檔其中.uvproj/.uvopt可直接打開工程.hex/.bin為固件輸出.o/.axf等為編譯中間文件有助于完整復(fù)現(xiàn)構(gòu)建過程目錄結(jié)構(gòu)較為完整。已有1012人瀏覽學(xué)習(xí)。工程內(nèi)包含W5500 SPI底層驅(qū)動(dòng)、LWIP網(wǎng)絡(luò)接口對(duì)接與基礎(chǔ)示例可對(duì)照學(xué)習(xí)MAC地址配置、IP/子網(wǎng)掩碼/網(wǎng)關(guān)設(shè)置、TCP/UDP連接建立流程同時(shí)保留鏈接腳本與編譯中間產(chǎn)物便于復(fù)現(xiàn)構(gòu)建過程或二次開發(fā)對(duì)理解硬件協(xié)議棧與軟件協(xié)議棧的分工很有幫助。1. 為什么STM32以太網(wǎng)方案里W5500比軟件協(xié)議棧更省心做嵌入式網(wǎng)絡(luò)開發(fā)最尷尬的不是寫不出應(yīng)用層邏輯而是底層TCP/IP協(xié)議棧占掉了寶貴的Flash和RAM還要在MCU中斷里頻繁處理報(bào)文重傳、超時(shí)計(jì)時(shí)、分片重組這些臟活。很多工程師在STM32F103上跑裸機(jī)LWIP配合DM9000或ENC28J60時(shí)都會(huì)遇到同一個(gè)問題主頻72MHz的M3核既要跑控制邏輯又要扛協(xié)議棧稍微有點(diǎn)并發(fā)數(shù)據(jù)量CPU占用率就直奔50%以上。而W5500的核心思路完全不同它把TCP/IP協(xié)議棧做成了硬件邏輯固化在芯片內(nèi)部MCU只需要通過SPI讀寫socket緩沖區(qū)即可完成收發(fā)CPU占用率通常能控制在10%以內(nèi)。這個(gè)項(xiàng)目的價(jià)值在于它提供了一套完整的STM32 W5500 LWIP組合參考工程且文件列表里能看到os_core.c、os_cpu_a.asm這類RTOS內(nèi)核文件意味著這套代碼在uC/OS-II下做過適配LWIP是以操作系統(tǒng)封裝層sys_arch的方式跑在RTOS之上。與裸機(jī)輪詢方式不同這種架構(gòu)下每個(gè)TCP連接可以由獨(dú)立任務(wù)維護(hù)阻塞式API調(diào)用也不會(huì)卡死整個(gè)系統(tǒng)。適合的對(duì)象是需要在STM32平臺(tái)上快速落地TCP/UDP通信、但不想從零調(diào)協(xié)議棧的開發(fā)者以及準(zhǔn)備評(píng)估硬件協(xié)議棧和軟件協(xié)議棧在資源占用、實(shí)時(shí)性、吞吐量方面差異的工程師。2. SPI驅(qū)動(dòng)層W5500寄存器訪問與可變長度數(shù)據(jù)幀2.1 W5500的SPI幀格式與三個(gè)關(guān)鍵區(qū)段W5500的SPI接口與普通SPI從機(jī)最大的區(qū)別在于它采用三元組幀結(jié)構(gòu)地址段、控制段、數(shù)據(jù)段。地址段占16位控制段占8位數(shù)據(jù)段長度由控制段中的讀寫標(biāo)志和塊選擇決定。讀取和寫入的區(qū)別只在于控制段的Bit2讀為1寫為0但傳輸時(shí)序上讀操作需要先發(fā)送地址和控制段再拉低CS持續(xù)接收數(shù)據(jù)字節(jié)而寫操作則是在地址和控制段之后緊跟數(shù)據(jù)字節(jié)。uint8_t w5500_read_byte(uint16_t addr) { uint8_t tx_buf[3], rx_buf[2]; tx_buf[0] addr 8; tx_buf[1] addr 0xFF; tx_buf[2] (0x00 2) | (0x01 1) | 0x01; // BSB000, RWB1, OM01 // HAL_SPI_TransmitReceive阻塞模式CS由片選引腳控制 }控制段0x01在讀取場(chǎng)景下表示塊選擇BSB為000即通用寄存器區(qū)讀標(biāo)志位RWB置1偏移量OM固定為01B。如果訪問的是socket寄存器區(qū)BSB需要相應(yīng)調(diào)整例如訪問socket 0的寄存器時(shí)BSB的低5位對(duì)應(yīng)socket編號(hào)00000。忽略O(shè)M字段的取值可能導(dǎo)致讀取量與預(yù)期不符特別是當(dāng)需要讀取socket RX buffer中的數(shù)據(jù)時(shí)。2.2 變長數(shù)據(jù)幀模式VDM的時(shí)序處理W5500的socket收發(fā)緩沖區(qū)訪問并不走通用寄存器地址而是通過Socket TX/RX Buffer地址映射。硬件上支持兩種訪問模式固定數(shù)據(jù)幀模式FDM和可變數(shù)據(jù)幀模式VDM后者是吞吐量優(yōu)化的關(guān)鍵。FDM模式適合訪問寄存器這類長度固定的數(shù)據(jù)而VDM模式允許在控制段中附加16位數(shù)據(jù)長度使一次SPI傳輸就能讀寫長達(dá)64KB的數(shù)據(jù)。void w5500_send_data(SOCKET s, uint8_t *buf, uint16_t len) { uint16_t free_size; // 先讀Sn_TX_FSR確認(rèn)發(fā)送緩沖區(qū)剩余空間 free_size w5500_read_socket_reg(s, Sn_TX_FSR); // 構(gòu)造VDM幀頭地址段為Sn_TX_BASE控制段使能VDM并指定socket編號(hào) uint8_t header[6]; header[0] (Sn_TX_BASE(s) 8) 0xFF; header[1] Sn_TX_BASE(s) 0xFF; header[2] 0x14; // BSB101(表示TX buffer), RWB0, OM10(VDM) header[3] (len 8) 0xFF; header[4] len 0xFF; }header[2]的0x14拆開看bit7-3為BSB段值10100對(duì)應(yīng)socket 0的TX bufferbit2為RWB寫標(biāo)志bit1-0為OM值10B即VDM模式。此時(shí)幀尾緊跟兩個(gè)字節(jié)的數(shù)據(jù)長度值。使用VDM模式有一個(gè)坑硬件不檢查寫入長度是否超出Sn_TX_FSR剩余空間一旦溢出會(huì)覆蓋其他socket的緩沖區(qū)數(shù)據(jù)且不產(chǎn)生任何錯(cuò)誤標(biāo)志必須在軟件層預(yù)先比較待發(fā)送長度和剩余空間大小。2.3 復(fù)位時(shí)序與PHY配置要點(diǎn)W5500上電后需要等待至少10ms讓內(nèi)部PLL穩(wěn)定然后通過寫模式寄存器MR的RST位觸發(fā)軟復(fù)位。復(fù)位完成后必須重新寫入PHY配置常見做法是將PHYCFGR的RST位清0然后設(shè)置OPMDC字段選擇10M/100M全雙工自動(dòng)協(xié)商模式。不少開發(fā)者碰到鏈路不通的問題其實(shí)就是沒有等待PHY鏈接寄存器PHYSR的LINK位變?yōu)?就匆忙對(duì)socket執(zhí)行l(wèi)isten或connect操作。3. LWIP移植netif接口對(duì)接W5500驅(qū)動(dòng)3.1 lwipopts.h關(guān)鍵配置項(xiàng)移植LWIP到STM32 W5500組合時(shí)lwipopts.h的配置直接決定后續(xù)開發(fā)順不順暢。重點(diǎn)不是堆內(nèi)存大小而是協(xié)議棧內(nèi)部描述性的結(jié)構(gòu)體消耗。對(duì)于W5500這種硬件協(xié)議卸載方案LWIP實(shí)際只保留IP層以上的邏輯TCP分包重排序、校驗(yàn)和計(jì)算全部由硬件完成因此內(nèi)存池可以相對(duì)保守。#define MEM_ALIGNMENT 4 #define MEM_SIZE (32 * 1024) #define MEMP_NUM_PBUF 16 #define MEMP_NUM_TCP_SEG 32 #define PBUF_POOL_SIZE 32 #define PBUF_POOL_BUFSIZE 1512 #define LWIP_DHCP 1 #define LWIP_NETCONN 1 #define LWIP_SOCKET 1 #define NO_SYS 0 // 與RTOS配合時(shí)置0 #define SYS_LIGHTWEIGHT_PROT 1PBUF_POOL_BUFSIZE設(shè)置成1512而不是1500是因?yàn)橐蕴W(wǎng)幀最大載荷為1500字節(jié)但netif結(jié)構(gòu)需要額外的14字節(jié)頭部空間用于MAC地址解封裝。NO_SYS必須與工程實(shí)際使用的RTOS匹配如果源碼里有os_core.c卻把NO_SYS置1直接編譯就會(huì)報(bào)sys_now未定義錯(cuò)誤。3.2 netif結(jié)構(gòu)體與low_level_output實(shí)現(xiàn)LWIP的netif結(jié)構(gòu)是協(xié)議棧和底層驅(qū)動(dòng)的橋梁驅(qū)動(dòng)側(cè)需要實(shí)現(xiàn)兩個(gè)核心回調(diào)low_level_init負(fù)責(zé)初始化接口并填充netif的MAC地址、MTU、硬件地址長度等字段low_level_output則在協(xié)議棧有報(bào)文要發(fā)送時(shí)被回調(diào)。報(bào)文在協(xié)議棧內(nèi)部以pbuf鏈形式存在驅(qū)動(dòng)需要把鏈中每個(gè)節(jié)點(diǎn)按順序拼成一個(gè)完整幀寫入W5500的TX ring buffer。static err_t low_level_output(struct netif *netif, struct pbuf *p) { struct pbuf *q; uint16_t total_len 0; uint8_t *tx_buf malloc(p-tot_len); // 遍歷pbuf鏈表將各節(jié)點(diǎn)拷貝到連續(xù)緩沖區(qū) for (q p; q ! NULL; q q-next) { memcpy(tx_buf total_len, q-payload, q-len); total_len q-len; } // 寫W5500發(fā)送緩沖區(qū)并觸發(fā)發(fā)送命令 w5500_send_data(0, tx_buf, total_len); w5500_socket_cmd(0, SEND); }這段代碼把多段pbuf拼接成連續(xù)數(shù)據(jù)再發(fā)送好處是W5500不需要支持分散/聚集DMA壞處是存在一次額外的memcpy。對(duì)大部分?jǐn)?shù)據(jù)包小于512字節(jié)的MQTT或Modbus TCP場(chǎng)景性能影響可以忽略。如果追求極致吞吐可以改寫w5500_send_data支持多段寫入逐段調(diào)用SPI傳輸并不釋放CS這樣省掉拼接緩沖但代碼復(fù)雜度明顯上升。3.3 裸機(jī)輪詢模型與RTOS信號(hào)量模型的取舍這套參考工程里同時(shí)存在裸機(jī)和RTOS兩套調(diào)度可能。裸機(jī)方案下主循環(huán)需要周期調(diào)用ethernetif_poll函數(shù)內(nèi)部檢查W5500的socket interrupt寄存器收到數(shù)據(jù)就往LWIP的tcpip_input傳包。RTOS方案下驅(qū)動(dòng)在中斷里只置位信號(hào)量由一個(gè)專用網(wǎng)絡(luò)任務(wù)等待信號(hào)量后調(diào)用tcpip_input這能降低主循環(huán)輪詢帶來的延遲抖動(dòng)。// RTOS方案SPI中斷中僅做事件標(biāo)記 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { osSemaphoreRelease(netif_semaphore); // uC/OS-II的信號(hào)量 } }uC/OS-II下信號(hào)量釋放操作可能觸發(fā)任務(wù)調(diào)度需注意不能在中斷服務(wù)里調(diào)用可能導(dǎo)致阻塞的API。LWIP要求裸機(jī)下tcpip_input在鎖保護(hù)下調(diào)用RTOS下則必須通過郵箱或信號(hào)量跨越任務(wù)邊界直接把報(bào)文指針作為消息傳給網(wǎng)絡(luò)任務(wù)而不是在中斷上下文中直接調(diào)用協(xié)議棧函數(shù)。很多不穩(wěn)定現(xiàn)象如隨機(jī)丟包、偶爾死機(jī)多半是中斷上下文調(diào)用協(xié)議棧API造成的臨界區(qū)競(jìng)爭(zhēng)。4. Socket配置實(shí)戰(zhàn)TCP Server與UDP收發(fā)調(diào)通4.1 三路socket初始化的代碼骨架W5500內(nèi)部有8個(gè)獨(dú)立的socket每個(gè)可以獨(dú)立配置為TCP客戶端、TCP服務(wù)端或UDP模式。常見做法是socket 0用作TCP Server監(jiān)聽socket 1用作UDP透?jìng)鱯ocket 2預(yù)留作為TCP Client主動(dòng)上報(bào)。初始化時(shí)先統(tǒng)一軟復(fù)位再逐個(gè)socket寫模式寄存器。void network_init(void) { // 復(fù)位W5500并等待PHY鏈接 w5500_soft_reset(); while (!(w5500_read_phy_reg(PHYSR) 0x01)); // LINK位 // 配置MAC和IP地址 w5500_write_reg(SHAR, mac_addr, 6); w5500_write_reg(SIPR, ip_addr, 4); // socket 0作為TCP Server監(jiān)聽8080端口 w5500_socket_init(0, Sn_MR_TCP, 8080, Sn_MR_SOCK_INT); w5500_socket_cmd(0, LISTEN); // socket 1作為UDP綁定5000端口 w5500_socket_init(1, Sn_MR_UDP, 5000, Sn_MR_SOCK_INT); }while輪詢PHYSR寄存器是個(gè)簡單粗暴但有效的方式超時(shí)保護(hù)不可少否則PHY芯片異常時(shí)會(huì)卡死整個(gè)初始化流程。建議加一個(gè)計(jì)數(shù)上限比如循環(huán)500次約50ms后跳出并打印錯(cuò)誤日志。Sn_MR_SOCK_INT位決定是否使能socket中斷實(shí)際使用中如果走輪詢模式可以關(guān)掉中斷節(jié)省資源。4.2 TCP分段接收與斷開重連處理TCP數(shù)據(jù)到達(dá)W5500的RX buffer后讀取Sn_RX_RSR可以拿到已接收但未被應(yīng)用讀取的字節(jié)數(shù)。關(guān)鍵在于一次讀取不一定拿到完整應(yīng)用層報(bào)文TCP是字節(jié)流協(xié)議上層報(bào)文可能被拆成多個(gè)TCP段分次到達(dá)。因此必須維護(hù)一個(gè)應(yīng)用層接收緩沖區(qū)把每次讀到的數(shù)據(jù)追加進(jìn)去再根據(jù)自定義協(xié)議幀格式做解幀。uint16_t tcp_recv_handler(SOCKET s, uint8_t *buf, uint16_t max_len) { uint16_t rx_size, ret 0; rx_size w5500_read_socket_reg(s, Sn_RX_RSR); if (rx_size 0) { uint16_t read_len (rx_size max_len) ? max_len : rx_size; w5500_recv_data(s, buf, read_len); w5500_socket_cmd(s, RECV); // 告知硬件緩沖區(qū)已釋放 ret read_len; } return ret; }RECV命令必須在數(shù)據(jù)讀取完成后立即發(fā)送否則W5500認(rèn)為接收緩沖區(qū)仍被占用后續(xù)收不到新報(bào)文。斷開檢測(cè)通過Sn_SR狀態(tài)寄存器判斷當(dāng)狀態(tài)變?yōu)镾OCK_CLOSE_WAIT或SOCK_CLOSED時(shí)需要主動(dòng)調(diào)用DISCON和CLOSE命令再重新執(zhí)行LISTEN進(jìn)入監(jiān)聽狀態(tài)。這里有個(gè)容易踩的坑LISTEN命令不適用于已有連接的socket必須先把socket切換到SOCK_INIT狀態(tài)。4.3 與STM32集成的調(diào)試手段邏輯分析儀與wireshark最有效的聯(lián)調(diào)方法是用USB轉(zhuǎn)以太網(wǎng)模塊把W5500接到PCwireshark抓包看三層交互是否正常同時(shí)用邏輯分析儀抓SPI信號(hào)確認(rèn)底層時(shí)序。SPI時(shí)序方面重點(diǎn)看CS拉低期間SCLK的時(shí)鐘個(gè)數(shù)是否符合預(yù)期寫寄存器場(chǎng)景是16位地址8位控制8位數(shù)據(jù)共32個(gè)時(shí)鐘VDM發(fā)送模式下則是16816長度N字節(jié)數(shù)據(jù)。邏輯分析儀如果抓到的時(shí)鐘個(gè)數(shù)不匹配優(yōu)先檢查SPI的CPOL/CPHA配置W5500要求CPOL1、CPHA1SPI模式3與很多低速傳感器默認(rèn)的模式0不同初始化SPI時(shí)稍不留神就是全亂碼。5. 性能調(diào)優(yōu)與實(shí)測(cè)從吞吐量到內(nèi)存占用的驗(yàn)證方法5.1 內(nèi)存預(yù)算分析LWIP協(xié)議棧吃掉了多少RAM用Keil的map文件統(tǒng)計(jì)內(nèi)存占用是最直接的方法。穩(wěn)定運(yùn)行后通過串口把mallinfo或自定義統(tǒng)計(jì)信息打印出來能看到堆剩余量和各內(nèi)存池水位。LWIP的MEMP_NUM_TCP_SEG乘上MEMP_SIZE加上PBUF_POOL_SIZE乘上PBUF_POOL_BUFSIZE就是核心內(nèi)存消耗。以第3章的配置估算僅這兩塊就需約64KB。// 在串口調(diào)試任務(wù)中周期性打印內(nèi)存水位 struct mem_stats_t { uint16_t heap_free; uint16_t pbuf_used; };實(shí)際經(jīng)驗(yàn)是STM32F103ZET6的64KB RAM跑滿TCP UDP部分功能時(shí)剩余空間約8~10KB。如果剩余空間低于4KB建議主動(dòng)削減PBUF_POOL_SIZE和MEMP_NUM_TCP_SEG雖然會(huì)降低并發(fā)連接數(shù)但保證長時(shí)間運(yùn)行的穩(wěn)定性更重要。LWIP頻繁重建和釋放連接時(shí)若內(nèi)存不足會(huì)直接返回ERR_MEM導(dǎo)致connect失敗或listen拒絕。5.2 吞吐量驗(yàn)證iperf與TCP窗口大小的關(guān)系W5500的硬件TCP/IP卸載雖然減輕了CPU負(fù)擔(dān)但吞吐量的上限受限于SPI時(shí)鐘和socket緩沖區(qū)大小。實(shí)測(cè)72MHz主頻下SPI時(shí)鐘配到18MHz單TCP連接通過iperf測(cè)得的吞吐量大約在8~10Mbps。如果調(diào)高SPI到36MHz部分芯片在長線傳輸時(shí)可能出現(xiàn)數(shù)據(jù)錯(cuò)位需要加長CS拉低之前的建立時(shí)間。參數(shù)影響范圍建議值SPI時(shí)鐘直接影響傳輸速率18MHz穩(wěn)定/ 36MHz需驗(yàn)證Socket TX/RX buffer大小決定TCP窗口上限8KB/8KB 或 16KB/16KBLWIP TCP_WND協(xié)議棧接收窗口與RX buffer匹配8 * TCP_MSSMEMP_NUM_TCP_SEG并發(fā)TCP段數(shù)量16~32TCP_WND和W5500的RX buffer大小如果不匹配會(huì)出現(xiàn)奇怪的現(xiàn)象pc發(fā)送數(shù)據(jù)永遠(yuǎn)只有滿窗口大小而應(yīng)用層明明沒有及時(shí)讀取。這是因?yàn)閃5500硬件收到數(shù)據(jù)后放在RX buffer里L(fēng)WIP的TCP窗口通告由netif驅(qū)動(dòng)讀取數(shù)據(jù)并交給協(xié)議棧后才能擴(kuò)窗如果RX buffer比TCP_WND小硬件層面就已經(jīng)丟包了。5.3 裸機(jī)向RTOS遷移時(shí)的時(shí)間關(guān)鍵點(diǎn)最后說一個(gè)容易被忽略的細(xì)節(jié)關(guān)閉SPI的CRC校驗(yàn)。STM32的SPI外設(shè)默認(rèn)CRC功能關(guān)閉但一旦在CubeMX中誤開了CRC每個(gè)數(shù)據(jù)幀尾部會(huì)多出兩個(gè)字節(jié)的CRCW5500不識(shí)別這類幀結(jié)構(gòu)表現(xiàn)為寄存器讀寫偶爾錯(cuò)位且無固定規(guī)律。排查方法是對(duì)同一寄存器連續(xù)讀三次看返回值是否一致。LWIP下的UDP發(fā)送如果帶寬要求高建議把W5500 socket的發(fā)送緩沖區(qū)開大同時(shí)用macOS的Network Link Conditioner模擬弱網(wǎng)環(huán)境測(cè)試丟包重傳。硬件協(xié)議棧的重傳機(jī)制完全由W5500內(nèi)部狀態(tài)機(jī)負(fù)責(zé)與LWIP的RTO定時(shí)器無關(guān)這既是個(gè)優(yōu)點(diǎn)也是個(gè)約束重傳策略不可調(diào)但確定性很好。基于這套工程做產(chǎn)品時(shí)建議把W5500中斷引腳接到STM32的EXTI通過事件驅(qū)動(dòng)代替主動(dòng)輪詢RTOS下可以節(jié)省約1%的CPU占用裸機(jī)則能顯著降低主循環(huán)延遲。本文還有配套的精品資源點(diǎn)擊獲取