計(jì)到LwIP TCP數(shù)據(jù)上傳)
簡(jiǎn)介面向需要實(shí)現(xiàn)PL與PS協(xié)同工作的FPGA開(kāi)發(fā)者、嵌入式工程師和高校學(xué)生這份資源圍繞Xilinx ZYNQ平臺(tái)以網(wǎng)口TCP/IP協(xié)議為數(shù)據(jù)出口系統(tǒng)梳理了從硬件架構(gòu)、總線通信到上位機(jī)收發(fā)的工程方案。內(nèi)容基于AXI、LWIP、以太網(wǎng)MAC與DMA等關(guān)鍵環(huán)節(jié)覆蓋接口選型、協(xié)議棧配置、數(shù)據(jù)通路搭建、實(shí)時(shí)性與帶寬優(yōu)化以及調(diào)試測(cè)試方法可幫助讀者建立起清晰的設(shè)計(jì)與排錯(cuò)思路。壓縮包整體約56.6MB文件清單與類型明細(xì)暫未提供但方案說(shuō)明與關(guān)鍵步驟的銜接仍可作為完整項(xiàng)目參考。該資源已有10883人學(xué)習(xí)適合需要從整體理解PL/PS通信鏈路并通過(guò)TCP完成網(wǎng)絡(luò)數(shù)據(jù)傳輸實(shí)踐的工程師也可為ZYNQ網(wǎng)口通信類設(shè)計(jì)提供直接借鑒。1. 把 ZYNQ PL 與 PS 通信講透TCP 上行數(shù)據(jù)的關(guān)鍵在于寄存器接口設(shè)計(jì)用 ZYNQ 做數(shù)據(jù)采集然后通過(guò)網(wǎng)口把數(shù)據(jù)送到上位機(jī)這個(gè)需求在項(xiàng)目里出現(xiàn)的頻率比想象中高得多。很多開(kāi)發(fā)者第一次接觸 ZYNQ 時(shí)都有同樣的困惑PL 和 PS 到底怎么交換數(shù)據(jù)TCP 協(xié)議棧跑在哪一端實(shí)際做下來(lái)你會(huì)發(fā)現(xiàn)TCP 協(xié)議本身是現(xiàn)成的LwIP 協(xié)議棧一掛就能跑真正的難點(diǎn)在于 PL 端的寄存器讀寫(xiě)接口設(shè)計(jì)以及數(shù)據(jù)從 PL 到 PS 再到網(wǎng)口的這條鏈路怎么打通。這份資源的價(jià)值就在于此它不只是一個(gè) TCP 例程而是把 PL 與 PS 通信的完整鏈路拆開(kāi)從 AXI 接口設(shè)計(jì)到 LwIP 網(wǎng)絡(luò)發(fā)送每一步都有可落地的代碼和驗(yàn)證方法。2. AXI 接口選型與寄存器映射PL 與 PS 通信的第一步是定數(shù)據(jù)通道2.1 四種 AXI 接口模式的適用邊界ZYNQ 的 PS 端通過(guò) AXI 總線訪問(wèn) PL 端的邏輯資源常見(jiàn)的有 AXI4、AXI4-Lite、AXI4-Stream 和 AXI-Full 四種接口類型。初學(xué)者最容易犯的錯(cuò)誤是一上來(lái)就選 AXI4-Stream 做寄存器讀寫(xiě)結(jié)果數(shù)據(jù)通路建立不起來(lái)卡在握手信號(hào)上。選型之前先明確一個(gè)原則控制面用 AXI4-Lite數(shù)據(jù)面用 AXI4-Stream兩頭都要走就用 AXI-Full。AXI4-Lite 的特點(diǎn)是一次只傳一筆數(shù)據(jù)地址和數(shù)據(jù)通道獨(dú)立時(shí)序簡(jiǎn)單非常適合讀寫(xiě) PL 端的寄存器。比如你要配置采集模塊的采樣率、啟動(dòng)停止標(biāo)志位、查詢狀態(tài)寄存器這些都屬于低頻控制操作用 AXI4-Lite 就夠了。AXI4-Stream 走的是流式數(shù)據(jù)傳輸沒(méi)有地址階段數(shù)據(jù)像流水線一樣連續(xù)往外送適用于 ADC 采集數(shù)據(jù)、DMA 搬運(yùn)這種高吞吐場(chǎng)景。AXI-Full 則同時(shí)支持突發(fā)傳輸和地址映射適合需要 PS 端頻繁讀寫(xiě) PL 端大塊存儲(chǔ)空間的場(chǎng)景但是在 ZYNQ 上實(shí)現(xiàn)起來(lái)復(fù)雜度明顯更高。加上中斷機(jī)制后AXI4-Lite 加 AXI4-Stream 的組合在絕大多數(shù)數(shù)據(jù)采集上傳項(xiàng)目中夠用了。中斷在 ZYNQ 里的實(shí)現(xiàn)不復(fù)雜PL 端拉高一個(gè)中斷信號(hào)PS 端注冊(cè)中斷處理函數(shù)就能做到數(shù)據(jù)到達(dá)即通知 CPU。很多人在這里有個(gè)誤區(qū)以為 PS 輪詢 PL 寄存器更簡(jiǎn)單實(shí)際項(xiàng)目里數(shù)據(jù)量一大輪詢會(huì)拖垮 CPU 占用率。2.2 寄存器映射的地址分配策略用 AXI4-Lite 實(shí)現(xiàn)寄存器讀寫(xiě)時(shí)地址分配要做長(zhǎng)遠(yuǎn)規(guī)劃。通常的做法是把一個(gè)自定義 IP 核的寄存器空間劃分為四個(gè)段控制寄存器區(qū)偏移 0x00 - 0x0F、狀態(tài)寄存器區(qū)偏移 0x10 - 0x1F、數(shù)據(jù)緩沖描述區(qū)偏移 0x20 - 0x2F、擴(kuò)展保留區(qū)偏移 0x30 以上。拿一個(gè)典型的 ADC 采集上傳項(xiàng)目舉例寄存器映射可以做這樣的分配偏移地址讀/寫(xiě)功能說(shuō)明0x00寫(xiě)控制寄存器bit0 啟動(dòng)采集bit1 復(fù)位bit2 開(kāi)啟 FIFO 寫(xiě)使能0x04寫(xiě)采樣率配置寫(xiě)入分頻系數(shù)實(shí)際采樣率 時(shí)鐘頻率 / (分頻系數(shù) 1)0x10讀狀態(tài)寄存器bit0 FIFO 空標(biāo)志bit1 FIFO 滿標(biāo)志bit2 采集忙標(biāo)志0x14讀數(shù)據(jù)計(jì)數(shù)寄存器當(dāng)前 FIFO 中積累的數(shù)據(jù)字節(jié)數(shù)0x20讀數(shù)據(jù)讀取寄存器每讀一次FIFO 出一個(gè)數(shù)據(jù)Vivado 中自定義 IP 核時(shí)會(huì)默認(rèn)生成頂層模塊和 AXI 接口的示例代碼。建議在自動(dòng)生成的模板基礎(chǔ)上修改保留 AXI 接口部分把用戶邏輯區(qū)域換成自己的設(shè)計(jì)。我一般會(huì)把地址解碼邏輯全部放在top_module中用一個(gè)reg [31:0] reg_file [N]數(shù)組做寄存器陣列這樣后續(xù)擴(kuò)展寄存器方便維護(hù)成本低。module adc_ctrl_ip # ( parameter integer C_S_AXI_DATA_WIDTH 32, parameter integer C_S_AXI_ADDR_WIDTH 8 ) ( // 自定義端口 —— 采集數(shù)據(jù)輸入 input wire [31:0] adc_data_in, input wire adc_data_valid, // 自定義端口 —— 中斷輸出 output wire irq_out, // AXI-Lite 接口端口自動(dòng)生成部分略 ... ); // 寄存器文件定義 reg [31:0] reg_ctrl; // 偏移 0x00 控制寄存器 reg [31:0] reg_samp_cfg; // 偏移 0x04 采樣率配置 reg [31:0] reg_status; // 偏移 0x10 狀態(tài)寄存器 reg [31:0] reg_data_cnt; // 偏移 0x14 數(shù)據(jù)計(jì)數(shù) reg [31:0] reg_fifo_data; // 偏移 0x20 數(shù)據(jù)讀取 // 狀態(tài)寄存器 bit1FIFO 滿標(biāo)志 wire fifo_full (fifo_wr_count FIFO_DEPTH); assign reg_status[1] fifo_full; assign reg_status[2] adc_run_flag; // 本地 FIFO 寫(xiě)控制 always (posedge clk or negedge rst_n) begin if (!rst_n) begin fifo_wr_en 1b0; end else if (adc_data_valid reg_ctrl[0]) begin fifo_wr_en 1b1; end end ... endmodule這里有幾個(gè)參數(shù)需要重點(diǎn)理解C_S_AXI_ADDR_WIDTH 8表示地址寬度 8 位尋址空間 256 字節(jié)按 32 位寄存器對(duì)齊后最多支持 64 個(gè)寄存器一般項(xiàng)目夠用了。adc_data_valid是外部 ADC 模塊送來(lái)的數(shù)據(jù)有效標(biāo)志這個(gè)信號(hào)被拉高時(shí)FIFO 的寫(xiě)使能才有效。注意代碼中把reg_ctrl[0]作為采集總開(kāi)關(guān)這是典型的設(shè)計(jì)模式——所有模塊的啟動(dòng)、停止都由統(tǒng)一的控制寄存器管理后續(xù)上位機(jī)命令分發(fā)時(shí)只需往一個(gè)地址寫(xiě)數(shù)據(jù)避免多個(gè)寄存器協(xié)同操作帶來(lái)的時(shí)序問(wèn)題。2.3 FIFO 深度的工程權(quán)衡PL 端 FIFO 深度的選擇直接決定數(shù)據(jù)傳輸鏈路會(huì)不會(huì)因?yàn)樗查g阻塞而丟數(shù)據(jù)。FIFO 深度太小PS 來(lái)不及讀數(shù)據(jù)時(shí)就會(huì)溢出FIFO 深度太大占用 BRAM 資源影響其他邏輯的綜合布局。從工程經(jīng)驗(yàn)上看FIFO 深度至少為 PS 端 DMA 突發(fā)長(zhǎng)度的兩倍。如果 PS 端 DMA 配置的突發(fā)長(zhǎng)度是 64 個(gè) 32 位字那么 FIFO 深度建議 128 以上。這個(gè)參數(shù)怎么推導(dǎo)的DMA 讀數(shù)據(jù)時(shí)總線協(xié)議要求一次突發(fā)傳輸中間不能被中斷如果 FIFO 中的數(shù)據(jù)少于突發(fā)長(zhǎng)度DMA 就要等待 FIFO 攢夠數(shù)據(jù)等待期間總線處于閑置狀態(tài)。FIFO 深度大于兩倍突發(fā)長(zhǎng)度就能保證 DMA 連續(xù)讀取時(shí)幾乎不需要等待。另外還要考慮 FIFO 數(shù)據(jù)寬度與 DMA 數(shù)據(jù)寬度的匹配問(wèn)題。如果 PL 端每拍產(chǎn)生 16 位數(shù)據(jù)而 PS 端 DMA 的讀寫(xiě)位寬是 32 位中間需要做位寬轉(zhuǎn)換。一種方式是直接用 Xilinx FIFO IP 核的位寬轉(zhuǎn)換功能輸入 16 位、輸出 32 位另一種方式是用兩個(gè) 16 位 FIFO 交替存儲(chǔ)讀出時(shí)拼接。前者簡(jiǎn)單后者可以節(jié)省資源但代碼復(fù)雜度上升。如果數(shù)據(jù)速率不高我通常選擇 Xilinx FIFO IP 核自帶的標(biāo)準(zhǔn)模式轉(zhuǎn)化功能設(shè)置好讀寫(xiě)位寬就能用。3. PS 端工程配置與 LwIP 網(wǎng)絡(luò)發(fā)送數(shù)據(jù)到 PS 之后誰(shuí)來(lái)接管3.1 SDK 工程的 BSP 配置與 GIC 中斷初始化Vivado 硬件導(dǎo)出到 SDK 后工程模板會(huì)自動(dòng)生成 BSP 包但是 LwIP 協(xié)議棧默認(rèn)不是開(kāi)啟的。在 SDK 環(huán)境下進(jìn)入 BSP 設(shè)置窗口找到lwip141庫(kù)選項(xiàng)把lwip勾上。這里有個(gè)版本兼容的坑Vivado 2018.3 之前的版本對(duì)應(yīng)的 LwIP 版本是 141Vivado 2020.1 之后的版本 LwIP 升級(jí)到 211兩者的 API 接口有細(xì)微差異后續(xù)代碼編譯報(bào)錯(cuò)時(shí)優(yōu)先檢查這里。GIC 中斷控制器初始化是另一個(gè)核心點(diǎn)。PS 端要接收 PL 發(fā)來(lái)的中斷信號(hào)就必須在 GIC 里注冊(cè)中斷服務(wù)函數(shù)。GIC 有 SPID 和 PPID 兩個(gè)中斷源組PL 端的中斷一般連接到 GIC 的 SPI 中斷號(hào)上。在 ZYNQ-7000 系列中PL 到 PS 的中斷線有 16 條對(duì)應(yīng) GIC 的中斷號(hào)從 61 開(kāi)始遞增。#include xscugic.h static XScuGic Gic; static XScuGic_Config *GicConfig; int Gic_Init(void) { GicConfig XScuGic_LookupConfig(0); if (GicConfig NULL) { return XST_FAILURE; } XScuGic_CfgInitialize(Gic, GicConfig, GicConfig-CpuBaseAddress); // 初始化異常處理必須在 GIC 初始化之后、中斷注冊(cè)之前完成 Xil_ExceptionInit(); Xil_ExceptionRegisterHandler( XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, Gic ); Xil_ExceptionEnable(); return XST_SUCCESS; }這段代碼里最容易被忽略的是Xil_ExceptionInit()調(diào)用的位置。很多人把異常初始化放在系統(tǒng)啟動(dòng)階段最先處理但在 ZYNQ 上正確的順序是先初始化 GIC 控制器再把 CPU 的異常處理函數(shù)指向 GIC 的中斷分發(fā)入口。如果順序搞反了中斷信號(hào)到了 CPU 側(cè)沒(méi)有一個(gè)統(tǒng)一的入口去查找中斷號(hào)程序就會(huì)跑飛。3.2 LwIP 協(xié)議棧的移植與網(wǎng)絡(luò)接口配置LwIP 在 ZYNQ PS 端的移植已經(jīng)比較成熟BSP 包里包含了 AXI Ethernet 驅(qū)動(dòng)但我們這里用的是 PS 自帶的千兆以太網(wǎng)控制器BSP 生成時(shí)會(huì)自動(dòng)加入相應(yīng)驅(qū)動(dòng)。需要手動(dòng)調(diào)整的是lwipopts.h文件中幾個(gè)全局配置宏#define MEM_SIZE (8 * 1024 * 1024) // 內(nèi)存堆大小 #define MEMP_NUM_PBUF 64 // PBUF 結(jié)構(gòu)體數(shù)量 #define MEMP_NUM_TCP_SEG 128 // TCP 報(bào)文段緩沖區(qū)數(shù)量 #define TCP_SND_BUF_DEFAULT (16 * 1024) // 發(fā)送緩沖 #define TCP_WND_DEFAULT (16 * 1024) // 接收窗口 #define LWIP_DHCP 0 // 關(guān)掉 DHCP用靜態(tài) IP #define LWIP_NETCONN 1 // 啟用中斷/輪詢模式 APIMEM_SIZE決定整個(gè) LwIP 協(xié)議棧運(yùn)行時(shí)能夠使用的內(nèi)存總量如果這個(gè)值太小TCP 連接建立時(shí)可能出現(xiàn)內(nèi)存分配失敗表現(xiàn)為上位機(jī)連接后立即斷開(kāi)。TCP_SND_BUF_DEFAULT 直接影響單次發(fā)送的數(shù)據(jù)包大小這個(gè)值可以按 MTU 的整數(shù)倍來(lái)配置常見(jiàn)取 16KB 是因?yàn)?TCP 窗口和 MTU 協(xié)商后16KB 的緩沖空間可以容納約 10 個(gè) MSS最大分段大小的數(shù)據(jù)量。網(wǎng)絡(luò)接口初始化的代碼固定框架其中關(guān)鍵在網(wǎng)絡(luò)參數(shù)的結(jié)構(gòu)體填充。我們是設(shè)備端服務(wù)器上位機(jī)是客戶端主動(dòng)連過(guò)來(lái)所以本地 IP、端口配置好后只需開(kāi)啟監(jiān)聽(tīng)。#include lwip/init.h #include lwip/netif.h #include lwip/tcp.h #include xparameters.h #include xil_printf.h // 靜態(tài) IP 配置 #define TCP_SERVER_PORT 8080 #define DEVICE_IP_ADDR 192.168.1.100 #define DEVICE_NETMASK 255.255.255.0 #define DEVICE_GATEWAY 192.168.1.1 static struct tcp_pcb *server_pcb; static struct netif eth0_netif; void Network_Init(void) { struct ip_addr ipaddr, netmask, gw; IP4_ADDR(ipaddr, 192, 168, 1, 100); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1); lwip_init(); netif_add(eth0_netif, ipaddr, netmask, gw, NULL, ethernetif_init, ethernet_input); netif_set_default(eth0_netif); netif_set_up(eth0_netif); // 創(chuàng)建 TCP 控制塊并綁定到監(jiān)聽(tīng)端口 server_pcb tcp_new(); if (server_pcb NULL) { xil_printf(TCP PCB create error\r\n); return; } tcp_bind(server_pcb, IP_ADDR_ANY, TCP_SERVER_PORT); server_pcb tcp_listen(server_pcb); tcp_accept(server_pcb, Server_Accept_Callback); }這里有一個(gè)關(guān)鍵的設(shè)計(jì)選擇netif_add的最后一個(gè)參數(shù)ethernet_input是 LwIP 的網(wǎng)卡數(shù)據(jù)輸入函數(shù)這個(gè)函數(shù)在 ZYNQ 的裸機(jī) BSP 環(huán)境中對(duì)應(yīng)的是emacps_rx_thread之類的回調(diào)。務(wù)必確認(rèn) BSP 中以太網(wǎng)驅(qū)動(dòng)的接收中斷已經(jīng)打開(kāi)否則ethernet_input永遠(yuǎn)收不到數(shù)據(jù)TCP 就建立不起連接。3.3 數(shù)據(jù)發(fā)送的阻塞與非阻塞選擇數(shù)據(jù)從 PL 讀到 PS 后通過(guò) TCP 發(fā)送給上位機(jī)。發(fā)送路徑上有兩種思路阻塞發(fā)送和回調(diào)發(fā)送。阻塞發(fā)送最簡(jiǎn)單——直接調(diào)用tcp_write加tcp_output數(shù)據(jù)就能發(fā)出去。問(wèn)題在于tcp_write只是把數(shù)據(jù)拷貝到內(nèi)核緩沖真正的網(wǎng)絡(luò)發(fā)送發(fā)生在tcp_output之后如果緩沖區(qū)滿tcp_write會(huì)返回ERR_MEM。很多初學(xué)者的程序在這里出現(xiàn)偶發(fā)丟包。正確做法是利用 LwIP 的發(fā)送完成回調(diào)機(jī)制。給 TCP 控制塊注冊(cè)一個(gè)sent回調(diào)函數(shù)當(dāng)系統(tǒng)確認(rèn)數(shù)據(jù)已經(jīng)發(fā)送并得到對(duì)端 ACK 后才釋放標(biāo)志位允許下一批數(shù)據(jù)寫(xiě)入。static uint32_t tx_busy_flag; static uint8_t send_buffer[8192]; void Send_Data_To_Host(uint8_t *data, uint16_t len) { err_t err; // 等待上次數(shù)據(jù)發(fā)送完成 while (tx_busy_flag) { // 非阻塞等待發(fā)送完成回調(diào)會(huì)清除標(biāo)志位 } memcpy(send_buffer, data, len); err tcp_write(server_pcb, send_buffer, len, TCP_WRITE_FLAG_COPY); if (err ERR_OK) { err tcp_output(server_pcb); if (err ERR_OK) { tx_busy_flag 1; } } } static err_t Server_Sent_Callback(void *arg, struct tcp_pcb *tpcb, u16_t len) { tx_busy_flag 0; return ERR_OK; }這段代碼的邏輯亮點(diǎn)在TCP_WRITE_FLAG_COPY標(biāo)志。不使用這個(gè)標(biāo)志時(shí)tcp_write只是把指針存到待發(fā)送隊(duì)列如果上層緩沖在 TCP 報(bào)文段真正發(fā)出前被修改會(huì)產(chǎn)生錯(cuò)誤數(shù)據(jù)。使用 COPY 標(biāo)志后LwIP 會(huì)先將數(shù)據(jù)拷貝到內(nèi)部 PBUF函數(shù)返回后就可以安全地重用本地緩沖區(qū)。代價(jià)是多了一次內(nèi)存拷貝但在千兆網(wǎng)口下這個(gè)開(kāi)銷可以接受。需要注意tx_busy_flag的等待循環(huán)不是嚴(yán)格意義上的阻塞因?yàn)?LwIP 協(xié)議棧需要持續(xù)調(diào)用tcp_tmr來(lái)處理定時(shí)器事件。在裸機(jī)環(huán)境下通常的做法是在主循環(huán)中刷新 LwIP 的定時(shí)器否則 TCP 的 ACK 超時(shí)重傳機(jī)制無(wú)法工作while (1) { tcp_tmr(); // 刷新 LwIP 定時(shí)器處理重傳、超時(shí)邏輯 // 檢查 FIFO 數(shù)據(jù)計(jì)數(shù)寄存器有數(shù)據(jù)則讀取并發(fā)送 if (Data_Is_Ready()) { Read_FIFO_And_Send(); } }4. 上位機(jī) TCP 客戶端與數(shù)據(jù)對(duì)齊鏈接建好了數(shù)據(jù)格式才是下一道關(guān)卡4.1 上位機(jī)的 TCP 接收實(shí)現(xiàn)要點(diǎn)上位機(jī)端推薦直接使用 Python 的 socket 模塊快速驗(yàn)證先確認(rèn)鏈路通不通再去做復(fù)雜的界面封裝。很多開(kāi)發(fā)者在這個(gè)項(xiàng)目上的做法是邊寫(xiě) Qt 界面邊調(diào)試網(wǎng)絡(luò)問(wèn)題排查起來(lái)非常痛苦——網(wǎng)絡(luò)不同時(shí)你很難判斷是 PL 端邏輯壞了還是界面線程阻塞了。先用一個(gè)純命令行腳本驗(yàn)證收發(fā)再把網(wǎng)絡(luò)模塊移植到圖形界面中調(diào)通概率高很多。import socket import struct import time def recv_tcp_data(port: int, timeout: float 5.0) - None: TCP 客戶端接收上位機(jī)數(shù)據(jù)按固定幀格式解析 client socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.settimeout(timeout) server_addr (192.168.1.100, port) try: client.connect(server_addr) print(f[INFO] connected to {server_addr}) # 數(shù)據(jù)接收緩沖區(qū)避免一次 recv 零散切割造成解析錯(cuò)誤 buffer b while True: try: chunk client.recv(4096) if not chunk: print([WARN] connection closed by device) break buffer chunk # 按幀頭 0xAA55 長(zhǎng)度字段協(xié)議解析 while len(buffer) 6: if buffer[0] 0xAA and buffer[1] 0x55: frame_len struct.unpack(H, buffer[2:4])[0] if len(buffer) 4 frame_len: break # 數(shù)據(jù)不完整繼續(xù)等待 # 提取一幀有效數(shù)據(jù) payload buffer[4:4 frame_len] print(f[INFO] received frame len{frame_len}, fdata_head{payload[:16].hex()}) buffer buffer[4 frame_len:] else: buffer buffer[1:] # 丟一個(gè)字節(jié)重新找?guī)^ except socket.timeout: print([WARN] recv timeout, no data in last period) break except ConnectionRefusedError: print([ERROR] device not listening, check IP and port) finally: client.close() if __name__ __main__: recv_tcp_data(port8080)這個(gè)腳本同時(shí)演示了 TCP 流式協(xié)議的拆包處理。TCP 是字節(jié)流協(xié)議兩次send的數(shù)據(jù)可能在一次recv中混在一起也可能一次send被拆成多次recv。如果沒(méi)有幀結(jié)構(gòu)設(shè)計(jì)上位機(jī)收到的數(shù)據(jù)就是亂序的。代碼中0xAA55幀頭加長(zhǎng)度字段的方案是工程最常用的做法先找?guī)^再按長(zhǎng)度解幀能有效對(duì)抗粘包和半包問(wèn)題。4.2 大小端與數(shù)據(jù)對(duì)齊的隱性戰(zhàn)爭(zhēng)PL 端是 ARM 架構(gòu)的 ZYNQ 雙核上位機(jī)通常跑在 X86 架構(gòu)上ARM 默認(rèn)小端序X86 也是小端序很多情況下不用考慮大小端轉(zhuǎn)換問(wèn)題。但問(wèn)題是數(shù)據(jù)從 PL 硬件 FPGA 出來(lái)時(shí)開(kāi)發(fā)者習(xí)慣整理成大端格式。一個(gè) 32 位的數(shù)據(jù)在 PL 端被拆成 4 個(gè)字節(jié)發(fā)送如果上位機(jī)按小端序解析數(shù)字就完全對(duì)不上。import struct # PL 端以特定字節(jié)序發(fā)送 4 字節(jié)數(shù)據(jù) # 場(chǎng)景1PL 端按小端發(fā)送上位機(jī)用 符號(hào)解析 raw_small b\x01\x00\x00\x00 value struct.unpack(I, raw_small)[0] print(f小端解析結(jié)果: {value}) # 輸出 1 # 場(chǎng)景2PL 端按大端發(fā)送上位機(jī)必須用 符號(hào)解析否則出錯(cuò) raw_big b\x00\x00\x00\x01 value_wrong struct.unpack(I, raw_big)[0] value_right struct.unpack(I, raw_big)[0] print(f錯(cuò)誤解析: {value_wrong}) # 輸出 16777216 print(f正確解析: {value_right}) # 輸出 1處理字節(jié)序最穩(wěn)妥的做法是 PL 端發(fā)送時(shí)遵循固定的網(wǎng)絡(luò)字節(jié)序大端上位機(jī)統(tǒng)一用!或符號(hào)解析。網(wǎng)絡(luò)協(xié)議棧內(nèi)部的 IP 頭、TCP 頭都是網(wǎng)絡(luò)字節(jié)序如果應(yīng)用層數(shù)據(jù)也統(tǒng)一用大端整個(gè)鏈路的數(shù)據(jù)格式就一致了。另外一種做法是 PL 端發(fā)送數(shù)據(jù)前把各字節(jié)按照上位機(jī)的預(yù)期存儲(chǔ)順序排列好上位機(jī)不做任何轉(zhuǎn)換直接解析。這種方式效率最高但沒(méi)有通用性一旦上位機(jī)換了平臺(tái)所有解析邏輯都要改。我建議應(yīng)用層還是定義大端格式協(xié)議頭統(tǒng)一用struct.pack(!I, value)打包雖然多花幾行代碼但未來(lái)?yè)Q上位機(jī)平臺(tái)時(shí)不會(huì)翻車。5. 避坑與排查從連接失敗到數(shù)據(jù)錯(cuò)亂的五個(gè)典型問(wèn)題5.1 TCP 連接建立不了網(wǎng)絡(luò)一直 ping 不通抓包軟件看不到設(shè)備發(fā)出的 ARP 請(qǐng)求。排查發(fā)現(xiàn) BSP 配置時(shí)沒(méi)有正確生成xemacps_g.c驅(qū)動(dòng)文件導(dǎo)致 PS 端以太網(wǎng)控制器根本沒(méi)有初始化。原因是 SDK 中 BSP 包生成時(shí)沒(méi)有勾選 Standalone 的emacps驅(qū)動(dòng)或者勾選了但 BSP 沒(méi)有重新生成。解決方法是右鍵點(diǎn)擊 BSP 工程選擇BSP Settings查找drivers分支下的ps7_ethernet_0是否為emacps不是則改為emacps然后重新生成 BSP。5.2 數(shù)據(jù)突發(fā)丟包TCP 連接正常但數(shù)據(jù)不完整現(xiàn)象是上位機(jī)連接正常但是數(shù)據(jù)每隔幾秒會(huì)有一批缺失丟包數(shù)量沒(méi)有規(guī)律。用 Wireshark 抓包發(fā)現(xiàn) TCP 報(bào)文段重復(fù) ACK 頻繁出現(xiàn)。原因大概率在 PS 到 PL 的數(shù)據(jù)讀取路徑上。PS 讀取 FIFO 數(shù)據(jù)時(shí)如果采用輪詢方式但輪詢間隔太長(zhǎng)FIFO 溢出就會(huì)丟數(shù)據(jù)。此外LwIP 的tcp_write緩沖區(qū)設(shè)置太小發(fā)送緩沖滿后新數(shù)據(jù)被丟棄。解決辦法是把 FIFO 讀取從輪詢模式改成中斷模式讓 PL 端在 FIFO 半滿時(shí)觸發(fā)中斷PS 立即響應(yīng)讀取。LwIP 參數(shù)調(diào)整同時(shí)把TCP_SND_BUF_DEFAULT從 4KB 提到 16KB給發(fā)送鏈路更多緩沖余量。5.3 AXI 讀寫(xiě)寄存器一直返回錯(cuò)誤值在 Vivado 的 Hardware Manager 里通過(guò) JTAG 讀 IP 核的寄存器狀態(tài)寄存器一直顯示 0xFFFFFFFF。檢查 AXI 地址映射發(fā)現(xiàn)IP 核的基地址配置成了 0x40000000但 vivado 中地址編輯器的段分配把寄存器起始地址設(shè)在了 0x40001000兩者錯(cuò)位導(dǎo)致訪問(wèn)落到了未實(shí)現(xiàn)的空間上。解決方法是打開(kāi) Address Editor確認(rèn)adc_ctrl_ip的偏移地址是否從 0x0 開(kāi)始建議把 Base Address 固定為 0x40000000Range 設(shè)為 64KB這樣簡(jiǎn)單干凈。類似問(wèn)題在多個(gè) IP 掛在同一個(gè) AXI 總線上時(shí)更容易出現(xiàn)排查時(shí)要逐個(gè)確認(rèn)地址段沒(méi)有重疊。5.4 上位機(jī)收到的數(shù)據(jù)是反的幀頭識(shí)別成功數(shù)據(jù)長(zhǎng)度也對(duì)但是數(shù)值完全不對(duì)。比如理論上應(yīng)該收到 10000實(shí)際收到 2055208960十六進(jìn)制 0x7A690000??酥谱∪ゼm結(jié) LwIP 代碼的沖動(dòng)先檢查字節(jié)序。這是最典型的字節(jié)序問(wèn)題。PL 端 Verilog 代碼里把 32 位數(shù)據(jù){data[31:16], data[15:0]}做了拼接發(fā)送相當(dāng)于整體交換了兩個(gè) 16 位半字實(shí)際就是大小端處理混亂。統(tǒng)一按大端處理后問(wèn)題不再出現(xiàn)。5.5 開(kāi)發(fā)板偶爾死機(jī)復(fù)位后恢復(fù)設(shè)備運(yùn)行一段時(shí)間后網(wǎng)口 ping 不通串口也輸出不了任何信息。檢查時(shí)鐘配置和復(fù)位序列發(fā)現(xiàn) PL 端使用了獨(dú)立的 MMCM 時(shí)鐘域但 FIFO IP 核的復(fù)位引腳連接的是 AXI 接口的 rst 信號(hào)兩個(gè)時(shí)鐘域的復(fù)位時(shí)序不一致。解決方法是把 PL 端所有模塊的復(fù)位信號(hào)統(tǒng)一使用proc_sys_resetIP 生成的interconnect_aresetn并檢查 FIFO IP 的reset_pol設(shè)置是否與復(fù)位模塊輸出極性一致。在開(kāi)發(fā)板上加一個(gè)看門(mén)狗復(fù)位超時(shí)自動(dòng)重啟整個(gè)系統(tǒng)也能規(guī)避偶發(fā)的死機(jī)問(wèn)題。6. 進(jìn)階從千兆帶寬利用到雙通道設(shè)計(jì)實(shí)戰(zhàn)性能再翻倍TCP 上傳數(shù)據(jù)鏈路打通之后可以往下做兩件縮小系統(tǒng)瓶頸的事一是吞吐量調(diào)優(yōu)二是多路復(fù)用擴(kuò)展。吞吐量調(diào)優(yōu)的核心指標(biāo)是計(jì)算實(shí)際有效帶寬。ZYNQ PS 網(wǎng)口硬件理論帶寬是 1000Mbps但是應(yīng)用層實(shí)測(cè)通常只有 300 到 500Mbps原因在于 LwIP 裸機(jī)協(xié)議棧的運(yùn)行機(jī)制限制以及 TCP ACK 往返時(shí)延帶來(lái)的窗口占用。網(wǎng)絡(luò)帶寬利用率的計(jì)算公式為有效帶寬 發(fā)送數(shù)據(jù)量 / 總耗時(shí)。在嵌入式設(shè)備上還會(huì)有一個(gè)額外限制LwIP 的tcp_write每調(diào)一次都要經(jīng)歷和本地內(nèi)存系統(tǒng)打交道大量交互會(huì)消耗 CPU 時(shí)間。有一個(gè)簡(jiǎn)單有效的優(yōu)化是加大單次發(fā)送包長(zhǎng)。把TCP_SND_BUF_DEFAULT提升到 64KB同時(shí)在應(yīng)用層使用tcp_write連續(xù)發(fā)送多個(gè)待發(fā)塊配合tcp_output做一次集中刷出。這種策略下即使單塊數(shù)據(jù)包較小協(xié)議棧也會(huì)積攢到足夠數(shù)據(jù)后一次性發(fā)出有效降低網(wǎng)口 TX 中斷觸發(fā)頻率。雙通道設(shè)計(jì)適合需要同時(shí)上傳兩路數(shù)據(jù)的場(chǎng)景。做一個(gè) AXI-Stream 到 AXI4-Lite 的橋接模塊將兩路 ADC 數(shù)據(jù)交替寫(xiě)入同一個(gè) DMA 描述符環(huán)。上位機(jī)按幀號(hào)區(qū)分兩路數(shù)據(jù)幀格式中前三字節(jié)固定為數(shù)據(jù)通道標(biāo)識(shí)。這種設(shè)計(jì)比雙 TCP 連接簡(jiǎn)潔PS 端只需維護(hù)一個(gè)tcp_pcb數(shù)據(jù)到達(dá)后按照既定的幀號(hào)交錯(cuò)發(fā)送即可。實(shí)踐中的驗(yàn)證方法也很直接先用開(kāi)發(fā)板配置一個(gè)固定數(shù)據(jù)源比如計(jì)數(shù)器計(jì)數(shù)值替代真實(shí)采集數(shù)據(jù)上位機(jī)統(tǒng)計(jì)接收總數(shù)和發(fā)送端計(jì)數(shù)對(duì)比。兩端計(jì)數(shù)值一致說(shuō)明鏈路無(wú)損不一致就對(duì)中間過(guò)程進(jìn)行逐段 debug先看 PL FIFO 讀寫(xiě)指針差值再看 PS 讀出的數(shù)據(jù)累計(jì)值最后看 LwIP 發(fā)送字節(jié)統(tǒng)計(jì)量哪個(gè)環(huán)節(jié)對(duì)不上就重點(diǎn)檢查哪個(gè)環(huán)節(jié)。那次調(diào)通雙通道方案后我給自己立了一條規(guī)矩每次把代碼從仿真環(huán)境挪到裸機(jī)網(wǎng)絡(luò)環(huán)境先花 5 分鐘核對(duì)三個(gè)關(guān)鍵參數(shù)——FIFO 寫(xiě)使能極性、TCP 發(fā)送緩沖大小、字節(jié)序解析格式。這三樣沒(méi)有問(wèn)題的情況下網(wǎng)絡(luò)異常往往不出在通信本身而是出在系統(tǒng)初始化的細(xì)節(jié)順序。ZYNQ 的中斷初始化順序、LwIP 的內(nèi)存堆大小、PHY 芯片的復(fù)位時(shí)序這三個(gè)位置是翻車高發(fā)區(qū)每次排查都從它們開(kāi)始驗(yàn)證。希望這次拆解能幫你少走一段彎路。本文還有配套的精品資源點(diǎn)擊獲取