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