ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FPGA上构建支持IPv6的MicroBlaze+lwIP HTTP服务器

2026/9/25 1:01:43 拓冰建站 浏览量
FPGA上构建支持IPv6的MicroBlaze+lwIP HTTP服务器 上周接到一个小需求用户要求手里的米联客MA703FA开发板能上电后自行启动一个Web页面用来配置设备参数还顺带问了一句——能不能支持IPv6访问说真的要在FPGA上跑网络服务器第一反应是硬写RTL工作量直接劝退第二个选择就是在MicroBlaze软核上套一个轻量协议栈也就是本文这套方案。这篇文章把我从Vivado工程搭建、SDK工程、lwIP HTTP服务器到IPv6支持与QSPI固化的完整过程整理了出来适合已经有Vivado基础、但第一次在MicroBlaze上碰以太网的朋友文章尽量做到每一步都能照着走。先说结论这套方案可复现、可裁剪开发板通电后自动从QSPI加载FPGA配置和应用程序MicroBlaze作为TCP服务器对外提供HTTP页面同时通过升级lwIP协议栈实现了IPv6访问能力。整个过程涉及的知识点比较密集但拆开看并不复杂PL侧搭一个软核处理器千兆MACPS侧跑裸机lwIP协议栈。下面按我自己调试的顺序一步步讲。1. 方案选型与整体架构1.1 为什么用 MicroBlaze lwIP 做嵌入式服务器FPGA上做网络通信通常有三条路一是纯RTL实现TCP/IP协议栈比如用Xilinx的XAPP1026或者开源项目这条路性能最好、时延最低但调试难度极高改一个字段就要重新综合一个TCP状态机写下来能让人脱层皮二是直接用Zynq的硬核ARM跑Linux或者裸机这当然最舒服但MA703FA这种纯FPGA芯片没有硬核处理器第三条路就是用MicroBlaze软核软件协议栈这也是目前绝大多数Artix-7项目在用的方案。MicroBlaze虽然主频不如硬核但处理几百Kbps到几十Mbps的控制面流量绰绰有余。配合AXI Ethernet硬核MAC和AXI DMA数据包搬运不占CPUMicroBlaze只需要处理TCP重传、ACK、应用逻辑这些“脑力活”。对于Web配置页面、传感器数据上报、固件升级这类嵌入式服务器场景这套组合在开发效率和性能之间达到了很好的平衡。而且lwIP是开源协议栈BSD套接字风格的API用起来非常顺手网上资料也极多。1.2 MA703FA 板卡资源盘点米联客MA703FA的核心器件是Xilinx Artix-7系列FPGA具体型号以板卡丝印和原理图为准我手上这块是XC7A35T级别。这类板卡的典型配置包括资源说明FPGAArtix-7 XC7A35T / XC7A100T内存板载DDR3通常是256MB/512MB颗粒存储QSPI Flash用于固化bit和elf以太网千兆RGMII PHY常见型号是RTL8211E系列调试接口USB-UART、JTAG这里有一个很重要的习惯拿到板卡第一件事不是急着开Vivado而是先把原理图里PHY芯片的型号、MDIO地址、复位引脚、时钟来源查清楚。不同板卡的PHY地址不一样常见是0x01或0x00如果地址配错后面MDIO读写不到PHY寄存器网口永远Link不上排查起来非常痛苦。1.3 整体数据通路与软硬件分工整个系统的数据通路可以这样理解以太网帧从外部网线进入PHY芯片经过RGMII接口进入FPGA内部的AXI Ethernet MACMAC把帧解析成AXI4-Stream流再由AXI DMA直接写入DDR3内存。MicroBlaze CPU通过lwIP协议栈处理这些数据需要回复时把待发送数据交给DMADMA从DDR3搬运到MAC打成帧发出去。用一句话概括网络收发是“硬件加速”的协议解析是“软件处理”的。这样的好处是即使MicroBlaze主频不高也不会因为大量数据搬运而忙不过来。实际调优时DMA描述符数量、缓存对齐、MAC接收缓冲大小都会直接影响吞吐这部分我在后面单独讲。2. Vivado工程搭建MicroBlaze最小系统与千兆MAC2.1 Block Design 中的 IP 清单在Vivado 2018.3里新建工程后创建Block Design下面这些IP基本是跑不掉的MicroBlaze8.5版本左右AXI Interconnect 或 AXI SmartConnect用于连接所有外设AXI Ethernet IP注意是axi_ethernet不是axi_ethernetliteAXI DMA如果AXI Ethernet选择外部DMA模式才需要单独添加MIG 7 Series DDR3控制器提供应用运行内存AXI UARTlite用于串口打印日志AXI GPIO控制LED或者读取拨码开关AXI Timer用于系统时钟tick和延时AXI Interrupt Controller汇总中断Processor System Reset统一复位Clocking Wizard生成各模块需要的时钟需要注意的是MicroBlaze配置时Local Memory尽量给足比如64KB以上用于启动阶段和堆栈。因为DDR3初始化需要时间MicroBlaze的启动代码和lwIP初始化可能依赖BRAM里的代码先跑起来。2.2 AXI Ethernet 与 PHY 芯片对接的细节AXI Ethernet IP的配置里有几个关键项接口类型选RGMII速度选1000MbpsDMA类型建议选“AXI DMA”让IP内部集成分散收集引擎。如果选了外部DMA则要额外添加AXI DMA IP并手动连接连线一下子复杂不少。RGMII接口的关键信号包括信号方向说明rgmii_td[3:0]输出发送数据DDR沿采样rgmii_tx_ctl输出发送控制信号rgmii_txc输出发送时钟1000M时为125MHzrgmii_rd[3:0]输入接收数据rgmii_rx_ctl输入接收控制信号rgmii_rxc输入接收时钟由PHY恢复mdio / mdc双向管理接口读写PHY寄存器phy_rst_n输出PHY复位连线时最容易踩坑的是时钟。RGMII工作在千兆模式时发送时钟125MHz必须稳定提供给PHY这个时钟通常由Clocking Wizard产生。具体来说AXI Ethernet IP会有一个gtx_clk或者gmii_to_rgmii所需的clk输入我的做法是用MMCM产生125MHz专用时钟同时保证该时钟与MAC逻辑时钟的相位关系满足IP的约束要求。Vivado的IP集成器里如果时钟连错综合后时序大概率过不了或者干脆运行时报错。PHY的MDIO地址在IP的“PHY Address”参数里设置。以RTL8211E为例常见地址是0x00或0x01具体看板卡原理图中PHY_AD[2:0]引脚上下拉。我建议在SDK里写一个简单的MDIO读写函数上电后先回读PHY寄存器0x0看能不能读到OUI和型号这样可以快速定位PHY是否工作。2.3 时钟与复位链路设计时钟是整个设计最容易翻车的地方。我给MicroBlaze系统推荐一组时钟规划200MHz或150MHz系统时钟用于MicroBlaze、AXI互联、DDRDDR本身还要额外参考时钟125MHz GTX时钟用于千兆RGMII发送125MHz ref_clk用于AXI Ethernet内部逻辑UART波特率时钟一般用系统时钟分频即可复位链路也很关键。Clocking Wizard输出的locked信号要接到Processor System Reset的aux_reset_in同时PHY的phy_rst_n也要在系统启动一段时间后再释放不要在MMCM锁定之前就拉高。否则PHY可能没有完成上电复位导致Link状态无法建立。2.4 DDR3 与地址映射规划DDR3部分我用的是MIG 7 Series IP。创建MIG时需要选择DDR3芯片颗粒型号这个必须从原理图或者板卡手册里查到比如常见的MT41K256M16 HA-125等。选错颗粒会导致内存读写不稳定系统跑起来随机死机。DDR3在Block Design里的地址分配需要手动规划。我通常把DDR放在0x80000000起始大小按板载容量设置。MicroBlaze的本地BRAM放在0x00000000UART、GPIO、Timer、Ethernet等外设放在0x40000000区段附近。地址规划的原则是高频数据通路DDR、DMA尽量地址对齐避免跨4KB边界。3. SDK软件工程从空白工程到 lwIP 通网3.1 导出硬件并创建SDK工程Vivado里综合、实现完成后File - Export Hardware勾选Include bitstream然后Launch SDK。SDK里会自动生成standalone BSP里面包含了MicroBlaze的所有驱动。这一步一般没问题但有一点要注意导出的硬件工程路径不要有中文或者空格否则后面编译lwIP这种大型库时会出现各种奇怪的include路径错误。创建应用工程时模板选择“lwIP Echo Server”或者“Empty Application”。我建议第一次先选lwIP Echo Server因为它已经把网络通路调通了编译下载后如果串口能打印IP并且主机能ping通说明PL侧、BSP、驱动全部正常。这一步是验证L0层的好办法不要在没跑通echo server之前就急着写HTTP服务器。3.2 BSP配置中的关键参数在BSP设置里勾选lwip141库后有几个参数值得关注参数位置建议值API modeBSP SettingRAW或Socket建议RAWEnable DHCPBSP Setting调试时关掉用静态IPIP Addresslwipopts.h或config192.168.1.10Netmask / Gateway同上按实际网络填lwIP的内存配置集中在lwipopts.h里。第一次测试时MEM_SIZE、PBUF_POOL_SIZE用默认值即可跑稳定后再逐步调小以节省BRAM。如果板卡DDR3可用lwIP的内存可以分配到DDR上没必要死磕BRAM容量。3.3 第一个例程Echo Server 跑通直接用模板生成的工程编译下载后串口通常会打印类似“TCP echo server started”的信息然后就是初始化DHCP或者静态IP。host电脑配好同一网段的IP后ping 192.168.1.10应该能通。如果ping不通优先排查PL侧问题不要先怀疑lwIP代码。具体排查思路我在第6章详细写。Echo Server只能说明TCP数据通路通了下一个建议是跑TCP Throughput Server模板一边用iperf测性能一边确认DMA在多包大数据流下是否稳定。这一步能暴露很多潜在问题比如DMA描述符不足、中断风暴、内存对齐不对等。3.4 中断、DMA与内存对齐的实际理解MicroBlaze的AXI Ethernet中断路径是MAC收到帧后置中断-AXI DMA完成描述符更新-DMA中断上报到中断控制器-MicroBlaze进入ISR-调用lwIP的回调函数处理数据。这里的核心是“中断里不要做重活”ISR里只做数据接收的登记真正的协议栈处理放到主循环的sys_check_timeouts和poll机制里去跑。内存对齐是另一个容易被忽略的点。AXI DMA要求缓冲区地址按总线宽度对齐一般需要4字节甚至更高对齐。lwIP的PBUF池在分配时通常会保证对齐但如果自己申请内存做收发缓冲务必用Xilinx提供的XIL_CACHE_ALIGN或类似宏对齐。我遇到过数据帧偶尔CRC错误查了半天发现是buffer地址非对齐导致的。4. 在 lwIP 上搭建千兆 HTTP 服务器4.1 选择实现方式用 socket 还是用 httpdlwIP自带一个httpd模块支持SSI和CGI但1.4.1版本的httpd配置比较复杂而且fsdata生成也很麻烦。对大部分嵌入式设备来说Web页面只是显示状态和接收配置我建议直接用lwIP的raw API实现一个极简HTTP服务器代码清晰逻辑也容易控制。raw API的核心是注册一个tcp_pcb设置accept、recv、err回调在收到HTTP请求后解析第一行然后拼装HTTP响应报文发回去。这种方式不需要额外的文件系统页面直接硬编码在C数组里简单粗暴但非常可靠。4.2 一个最小 HTTP 响应代码下面是我实际使用的核心回调功能是当浏览器请求“/”时返回一个状态页请求“/data”时返回一段JSONstatic err_t http_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t err) { tcp_recv(newpcb, http_recv_cb); return ERR_OK; } static err_t http_recv_cb(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { if (p NULL) { tcp_close(pcb); return ERR_OK; } /* 只取第一行请求例如 GET / HTTP/1.1 */ if (p-len 0) { char *req (char *)p-payload; if (strncmp(req, GET /data, 9) 0) { const char *json {\temp\:26.5,\hum\:43}; char hdr[128]; int len sprintf(hdr, HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n, strlen(json)); tcp_write(pcb, hdr, len, TCP_WRITE_FLAG_COPY); tcp_write(pcb, json, strlen(json), TCP_WRITE_FLAG_COPY); } else { const char *html htmlbodyh1MA703FA HTTP Server/h1/body/html; char hdr[128]; int len sprintf(hdr, HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\nConnection: close\r\n\r\n, strlen(html)); tcp_write(pcb, hdr, len, TCP_WRITE_FLAG_COPY); tcp_write(pcb, html, strlen(html), TCP_WRITE_FLAG_COPY); } } tcp_close(pcb); pbuf_free(p); return ERR_OK; }这里有两个细节一是必须设置Content-Length否则浏览器无法判断响应边界二是TCP_WRITE_FLAG_COPY因为tcp_write是异步的数据需要拷贝到lwIP的内存池中不能直接传栈上指针。否则等发送真正执行时栈上的数据已经被覆盖了。主函数里初始化协议栈后监听80端口struct tcp_pcb *pcb tcp_new(); tcp_bind(pcb, IP_ADDR_ANY, 80); pcb tcp_listen(pcb); tcp_accept(pcb, http_accept_cb);这样浏览器访问 http://192.168.1.10 就能看到页面了。4.3 SSI/CGI 与动态页面如果你的项目想做更复杂的动态页面我建议用lwIP自带httpd里的SSI功能。SSI的思路是网页模板里写!--#temp--这样的标签lwIP在发送页面时自动调用用户注册的SSI回调函数把标签替换成实时值。这个方案的好处是页面和逻辑分离改HTML结构不需要重新编译C代码只要重新生成fsdata。在1.4.1版本里要先在lwipopts.h里打开LWIP_HTTPD_SSI宏然后实现httpd_ssi_handler函数。新版lwIP 2.x的接口略有变化但思路一致。4.4 性能观测与优化方向HTTP服务器跑通后可以用ab命令或者浏览器多开几个页面观察响应速度。MicroBlaze处理HTTP请求的瓶颈一般在TCP发送窗口和DMA描述符上。如果并发请求多建议把lwIP的TCP_SND_BUF和TCP_WND调大同时增加DMA描述符数量。我实测下来DMA描述符从16增加到64后大文件下载速度有肉眼可见的提升。千兆环境中MicroBlaze的CPU主频是主要瓶颈。如果你要做高性能网关可以考虑把lwIP跑在DDR中同时为MicroBlaze开启I-Cache和D-Cache能提升不少性能。Cache配置在MicroBlaze IP核里勾选SDK侧不需要额外改动。5. 让 MicroBlaze 支持 IPv65.1 为什么 SDK 自带的 lwIP 1.4.1 对 IPv6 不友好这里要先说实话Vivado 2018.3的SDK自带的lwIP库是1.4.1版本这个版本对IPv6的支持只处于实验状态。虽然lwipopts.h里也能看到LWIP_IPV6宏但真正打开后很多核心模块ICMPv6、NDP、地址自动配置根本不完整编译能过功能基本不能正常用。所以如果你必须做IPv6有两条路可以选。5.2 路线一升级工具链用官方新库开启双栈最省力的路线是换用Vivado 2020.2及以上版本从2019.x开始Xilinx已经将lwIP升级到了2.1.xIPv6已经具备基本可用性。Block Design的搭建步骤和2018.3几乎一致SDK里创建应用工程时选择lwIP模板然后在BSP设置里打开IPv6开关。以lwIP 2.1.2为例lwipopts.h中需要关注#define LWIP_IPV6 1 #define LWIP_IPV6_AUTOCONFIG 1 #define LWIP_ND6 1 #define LWIP_DNS 1 #define LWIP_ICMP6 1 #define LWIP_IPV6_MLD 1 #define LWIP_IPV6_DHCP6 0打开这些宏后在main里调用netif_create_ip6_linklocal_address(netif, 1); netif.ip6_autoconfig_enabled 1;开发板上电后lwIP会自动生成一个fe80::开头的链路本地地址同时如果有支持SLAAC的路由器还能自动获取全球单播地址。串口打印出地址后主机用ping6和浏览器就能访问。5.3 路线二在 2018.3 工程内手动移植 lwIP 2.1.2如果你因为历史原因必须留在2018.3也可以把lwIP 2.1.2手动移植进现有工程。这是个体力活主要步骤包括从lwIP官网下载lwIP 2.1.2源码复制src下的core、api、netif、include到工程的lwip目录下。参考SDK自带lwip141的arch实现重写cc.h、sys_arch.h、sys_arch.c等平台相关文件。将BSP设置里的lwip库移除改为从应用工程直接包含lwIP源码编译。把lwipopts.h换成2.x版本的配置模板确保LWIP_IPV6宏打开。重点检查网络驱动Xilinx的xaxiemac/xaxiethernet和lwIP2.x之间的适配层需要自己写或者从官方新版本SDK中移植。这条路线最大的工作量在驱动适配层。lwIP 1.x到2.xnetif结构体、etharp、pbuf接口都有变化Xilinx驱动原生是为1.x写的直接编译会报很多错。如果非要用我建议从Vivado 2020.2的BSP里把xemacpsif.c、axiethernetif.c这些适配文件整个拷贝过来改比自己重写靠谱得多。5.4 Ubuntu 主机侧 IPv6 验证配置开发板侧支持IPv6后主机侧也需要配置IPv6。Ubuntu Server 24.04默认是开启IPv6的但需要确认一下sysctl net.ipv6.conf.eth0.disable_ipv6 # 返回 0 表示开启如果返回 1执行 sudo sysctl -w net.ipv6.conf.eth0.disable_ipv60如果需要静态IPv6地址编辑/etc/netplan/下的yamlnetwork: ethernets: eth0: addresses: - 192.168.1.100/24 - 2001:db8::100/64 routes: - to: default via: 192.168.1.1然后netplan apply。验证开发板和主机是否能互通IPv6最直接的就是ping6ping6 -I eth0 fe80::1如果开发板是SLAAC自动获得地址也可以用浏览器直接访问http://[2001:db8::1]注意IPv6地址在URL中必须加方括号。这一步成功就说明整条IPv6链路已经打通了。6. 常见问题与调试经验6.1 网络不通先查 PHY 复位与 MDIO最典型的故障是SDK打印了IP地址但主机ping不通。排查思路按顺序来第一步用万用表或者示波器确认PHY的复位引脚是否在启动后正常拉高以及时钟是否有125MHz输出。第二步在SDK里写一个裸机程序直接读取PHY寄存器0x0到0x5看能不能读到正常的PHY ID。如果读到0xFFFF说明MDIO通路是断的可能是PHY地址不对也可能是MDIO引脚复用配置错了。第三步查看AXI Ethernet的link状态寄存器确认PHY是否完成了自动协商。这类问题90%出在PHY复位时序和MDIO地址上不要一上来就怀疑lwIP代码。6.2 lwIP 内存不足导致的随机崩溃lwIP在内存不足时不会像Linux那样报个错就完事而是直接断言或者随机跑飞。典型表现是系统跑几分钟后突然死机重启后又能跑一段时间。排查方法是先看串口有没有lwIP的assert打印然后在lwipopts.h里把MEM_SIZE、PBUF_POOL_SIZE、PBUF_POOL_BUFSIZE这三个参数调大。特别是如果开了TCP窗口扩大和大量并发连接内存池很快会被耗尽。我的建议是内存充足时MEM_SIZE至少配到几百KBPBUF_POOL_SIZE不小于32。在MicroBlaze平台这部分内存默认会放在DDR上不会太紧张。6.3 程序固化到 QSPI Flash 的完整流程调试阶段通过JTAG下载bit和elf就够用了但产品化肯定要固化。MicroBlaze的固化流程和Zynq不同不需要FSBL但要处理好bitstream和elf的打包。在SDK中菜单Xilinx - Create Boot Image添加分区时先加入FPGA bitstream再加入应用程序elf。如果应用运行在DDR3上务必确认elf的加载地址和链接地址一致。生成MCS文件后用Xilinx - Program Flash Memory选择生成的MCSFlash Type选QSPIConfig Rate可以按板卡默认来。烧写完成后把板卡启动模式跳线拨到Flash模式重新上电MicroBlaze会自动加载FPGA配置并启动应用。有一个容易踩的坑如果启动后串口正常打印但网络ping不通多半是固化时PHY复位时序和JTAG下加载不一样需要在应用代码里增加延时等待PHY稳定。6.4 中断优先级与裸机主循环最后说一个容易被初学者忽略的点MicroBlaze裸机跑lwIP不要把所有逻辑都放在中断里。AXI Ethernet的DMA中断触发频率很高如果在中断回调里直接解析HTTP请求、发送大量数据会导致其他中断被阻塞甚至出现新的以太网帧无法及时接收形成丢包循环。正确做法是中断里只做数据到达通知主循环里轮询处理lwIP的sys_check_timeouts和网络数据。lwIP 1.4.1的RAW API模式下可以在主循环里不断调用tcp_poll和tcp_recv的回调虽然看起来像个大轮询但实际速度和稳定性都远好于在ISR里泡太久。我在实际使用中的体会是这一整套链路里PL侧的时钟和复位设计决定了链路通不通而软件侧的DMA描述符、lwIP内存配置决定了链路稳不稳。如果你刚开始跑建议严格按照“先回读PHY ID再ping通IPv4最后再搞HTTP和IPv6”的节奏来。先让最小系统稳定再逐步加功能这样每次出问题都能快速定位。最后再分享一个小技巧如果你在调IPv6时发现主机ping6一直不通先不要查协议栈先把开发板的IPv6地址通过串口完整打出来对比一下系统打印的地址和主机路由表里的地址是否在同一个前缀下。IPv6的排错第一步永远是确认地址第二步才是确认路由别问我怎么知道的这条路上我栽过太多次。