MicroBlaze软核TCP服务器:FPGA千兆以太网与IPv6双栈实战
拿到米联客MA703FA开发板之后我大概花了两周才从“只会点灯”过渡到“能联网”。原因倒不是MicroBlaze难学而是网上的教程基本都停在UART和FIFO真正能把千兆以太网服务器跑起来、并且把IPv6一并打开的完整案例少之又少。这篇实战记录就是来补这个缺口的我会用Vivado 2018.3 SDK在这块Artix-7板卡上搭建一个基于MicroBlaze软核的TCP服务器底层走AXI Ethernet与AXI DMA协议栈用lwIP并重点演示IPv6双栈的开启方式与调试方法。做完之后这个板子就从一个FPGA学习板变成了一台可以挂在交换机上的微型服务器支持通过IPv4或IPv6地址远程访问能做的事一下子就多了。不管是FPGA入门者、想跑软核网络协议的嵌入式工程师还是正在做设备远程采集的DIY玩家这篇记录都值得你从头读到尾。1. 选型判断MA703FA为什么适合做MicroBlaze网络服务器1.1 板卡核心资源盘点MA703FA在米联客的Artix-7产品线里属于“麻雀虽小五脏俱全”的型号。我手头这块的关键参数如下项目规格FPGAXC7A35T-2FGG484I逻辑单元33KBlock RAM约1800KbDSP48E1共90个板载DDR3一颗DDR3容量因批次而异我手上是256MB千兆PHYRealtek RTL8211FDRGMII接口支持10/100/1000M自适应串口USB-UART用于调试和日志其他外设HDMI、SD卡、SPI Flash、LED、按键、扩展排针这里要特别提一下扩展排针。很多资料把MA703FA的大部分IO都引到了排针上意味着RGMII这类高速信号如果走线质量不够理想是跑不稳千兆的。所以这个项目里要老老实实用板载RTL8211FD千万不要为了省事自己飞线接PHY飞线出来的高速信号光是时序和串扰就够折腾几天。板载PHY的走线是米联客画好且验证过的MII/RGMII引脚约束也是现成的这是能快速跑通网络的关键。1.2 为什么选MicroBlaze软核而不是硬核很多朋友第一反应是要做以太网服务器为什么不用Zynq的硬核ARM或者干脆上一块Linux开发板这个说法没毛病但要看场景。MZ7030、Zynq这类带硬核的芯片优势是能跑完整Linux性能强可开发复杂度、成本和学习门槛也明显更高。MicroBlaze是Xilinx的软核处理器本质上是FPGA里用LUT和BRAM“搭”出来的CPU。选择它的核心原因有三个身在FPGA工程里和自定义逻辑的交互最自然。网络协议栈、软件代码可以和Verilog/VHDL硬件模块共享同一片FPGA不用像Zynq那样在PS和PL之间来回考虑接口。学习价值高。搭一个包含AXI总线、DMA、中断控制器、外设IP的完整SoC能够把计算机体系结构里“CPU怎么通过总线访问外设”这个抽象概念彻底落地。成本低。很多入门级Artix-7板卡都能跑MicroBlazeMA703FA属于比较经济的选择。当然软核的缺点也非常明显CPU主频只有100到200MHz跑TCP/IP协议栈时吞吐量远达不到千兆线速。软核方案从来不是为了跑满千兆而是为了在硬件逻辑和网络功能之间找一个平衡点。1.3 三种以太网实现路径的取舍开始动手前我把可能的实现路径都列了一遍方案特点适用场景AXI Ethernet Lite内部带FIFO通过AXI4-Lite直接读写无需DMA简单、小型百兆量级性能适合熟悉协议栈AXI Ethernet AXI DMA lwIP千兆数据通路DMA搬运帧CPU只处理协议栈追求性能、要跑TCP/IP的正式方案自定义MAC 用户逻辑直接处理帧完全自己控制但TCP/IP栈基本没法复现只做自定义帧协议的场景我最终选择的是第二套AXI Ethernet全功能版本 AXI DMA lwIP协议栈。这是Xilinx官方支持和维护最成熟的一条路线SDK里甚至直接提供了lwIP的模板工程省去大量从零移植的体力活。2. Vivado硬件工程搭建MicroBlaze与RGMII时钟的联动关系2.1 Block Design的IP清单与数据通路打开Vivado 2018.3新建工程后先创建一个Block Design。这个设计里需要放到Block Design里的IP如下MicroBlaze (100MHz) ├── AXI Interconnect │ ├── AXI DMA │ │ └── AXI Ethernet (RGMII 1G) │ ├── AXI UartLite │ ├── AXI Timer │ ├── AXI GPIO │ ├── AXI Intc │ └── DDR3控制器 或 BRAM控制器 └── LMB总线 - 本地存储器数据通路的逻辑是以太网PHY收到的RMII/RGMII信号进入AXI Ethernet核转为AXI-Stream流再由AXI DMA搬运到内存DDR3或BRAMMicroBlaze跑lwIP协议栈对内存中的帧做处理发送方向则完全反过来。2.2 时钟设计100MHz、125MHz、200MHz各管什么这是整个硬件工程里最容易翻车的地方。MA703FA的MicroBlaze系统里至少需要三路时钟CPU和大部分AXI外设时钟100MHz给MicroBlaze、AXI Interconnect、AXI DMA、AXI UartLite等使用。AXI Ethernet的GTX时钟125MHz用于RGMII接口的125MHz发送时钟。千兆以太网的RGMII模式收发时钟都是125MHz数据在时钟上下沿都采样。IDELAYCTRL参考时钟200MHz。AXI Ethernet在RGMII模式下需要IDELAYCTRL来控制RX路径上的延时这个200MHz参考时钟必须连上。很多朋友做完硬件发现收不到数据或者丢包严重往往是忘了给idelay_ctrl_refclk接时钟。我在Block Design里用了一个Clocking Wizard输出三路时钟clk_out1100MHz、clk_out2125MHz、clk_out3200MHz。其中125MHz给axi_ethernet_0/gtx_clk200MHz给axi_ethernet_0/idelay_ctrl_refclk100MHz给MicroBlaze和AXI外设。还要注意复位逻辑。AXI Ethernet的phy_rst_n是输出引脚接到RTL8211FD的复位脚。这个复位信号在Block Design里通过AXI GPIO控制会更灵活但如果你不想用GPIO也可以直接把phy_rst_n引出去。我习惯用GPIO控制这样软件里可以随时给PHY做硬复位。2.3 AXI DMA、中断与PHY复位的连接AXI DMA在这里承担数据搬运工作它的mm2s_introut和s2mm_introut两个中断要连接到AXI Intc的不同输入通道。MicroBlaze的interrupt信号接AXI Intc的输出。除此之外UartLite、Timer的中断也要一并接进来。这里有个经验中断引脚顺序会决定SDK里XPAR_AXI_INTC_0_..._VEC_ID的编号连线时尽量按固定顺序排好不然查问题的时候脑子容易乱。PHY复位建议用AXI GPIO比如gpio0输出控制。MA703FA板上有PHY复位引脚Low有效软件里先拉低至少10ms再释放确保PHY完成上电初始化。我踩过坑上电后立刻访问PHY寄存器经常读到不确定的值就是因为PHY还没完成内部自检。2.4 引脚约束与上板前检查清单引脚约束来自MA703FA的原理图。RGMII接口在Block Design里作为外部端口引出后需要逐个约束到FPGA引脚并设置IO电平标准。典型的约束片段如下set_property PACKAGE_PIN T23 [get_ports {rgmii_txd[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {rgmii_txd[0]}] # 其余rgmii_txd、rgmii_rxd、rgmii_tx_ctl、rgmii_rx_ctl、rgmii_rxc、rgmii_txc # mdio、mdc、phy_rst_n等信号同理注意RGMII信号的电平标准要看板卡PHY侧的供电MA703FA的PHY侧通常是3.3V或2.5V。盲目照抄别人的约束很可能烧PHY或者跑不稳。生成比特流并下载后上板前先检查三件事PHY复位引脚电平是否正常链路Link灯是否点亮。用调试工具或串口打印读MDIO寄存器确认CPU能通过MDIO访问到PHY。插上网线看PHY是否协商上千兆。如果PHY只协商成100M优先怀疑125MHz时钟或者RGMII延时配置。3. SDK工程lwIP协议栈的双栈配置与TCP服务器骨架3.1 创建BSP时选择lwIP版本和IPv6选项硬件导出后打开SDK新建一个Application Project选择Empty Application。在Board Support Package设置里重点是给BSP添加lwIP库。Vivado 2018.3的SDK里通常提供两个lwIP版本lwip141和lwip202。建议选lwip202它对IPv6的支持要完整得多。lwIP 1.4.1虽然也能开IPv6但很多细节和后续更新都不如2.0.x。BSP界面里有一个“ipv6”的配置项把它勾上。如果在界面里找不到这项也别慌后面直接改lwipopts.h也行。改完BSP后点重新生成再右键应用工程选择到新BSP上。3.2 lwipopts.h里的关键开关打开BSP包里的lwipopts.h这是整个lwIP协议栈的配置总闸。针对双栈服务器我一般会确认以下几个宏#define LWIP_IPV6 1 // 开启IPv6 #define LWIP_IPV4 1 // 保留IPv4组成双栈 #define LWIP_ICMP6 1 // IPv6 ICMPping6要用 #define LWIP_ND6 1 // IPv6邻居发现 #define LWIP_SOCKET 1 // 使用socket API #define LWIP_NETCONN 1 // 使用netconn APIsocket底层依赖 #define NO_SYS 0 // 使用操作系统/线程Xilinx lwIP示例依赖它内存方面IPv6的PCB和ND表项比IPv4占更多RAM。MEM_SIZE、MEMP_NUM_TCP_PCB、MEMP_NUM_NETBUF这些参数不能沿用单栈的默认值。我在MA703FA上的配置是MEM_SIZE 4*1024*1024MEMP_NUM_TCP_PCB 16MEMP_NUM_ND6_QUEUE 10。如果板子只有BRAM没有DDR3内存紧张时优先保证TCP_PCB和PBUFsocket数量可以少开一些。一个重要的提醒修改lwipopts.h后需要在工程上执行Clean再重新编译。很多时候改了配置没生效就是BSP库没有重新编译导致的。3.3 用socket API写出可同时监听v4/v6的服务器lwIP的示例工程自带一种模板化的TCP服务器框架我用socket API重写了一遍。下面是一个只监听IPv6的TCP服务线程骨架IPv4的写法完全对称#include xil_printf.h #include lwip/init.h #include lwip/sockets.h #include lwip/netif.h #include netif/xadapter.h static struct netif netif_static; static unsigned char mac_address[] {0x00, 0x0A, 0x35, 0x00, 0x01, 0x02}; void tcp6_server_thread(void *arg) { int listen_sock lwip_socket(AF_INET6, SOCK_STREAM, 0); if (listen_sock 0) { xil_printf(IPv6 socket create failed\r\n); return; } struct sockaddr_in6 server6; memset(server6, 0, sizeof(server6)); server6.sin6_family AF_INET6; server6.sin6_port htons(8080); server6.sin6_addr in6addr_any; // 监听所有IPv6地址 if (lwip_bind(listen_sock, (struct sockaddr *)server6, sizeof(server6)) 0) { xil_printf(IPv6 bind failed\r\n); lwip_close(listen_sock); return; } lwip_listen(listen_sock, 5); xil_printf(IPv6 TCP server listening on port 8080\r\n); while (1) { struct sockaddr_in6 client6; socklen_t client_len sizeof(client6); int client_sock lwip_accept(listen_sock, (struct sockaddr *)client6, client_len); if (client_sock 0) { continue; } // 收发数据... char recv_buf[256]; int len lwip_recv(client_sock, recv_buf, sizeof(recv_buf), 0); if (len 0) { lwip_send(client_sock, recv_buf, len, 0); } lwip_close(client_sock); } }在主函数里初始化lwIP注册网络接口然后启动tcpip线程和服务器线程#include lwip/tcpip.h #include lwip/dhcp.h #include xparameters.h void start_application(void) { sys_thread_new(tcp6_server, tcp6_server_thread, NULL, DEFAULT_THREAD_STACKSIZE, DEFAULT_THREAD_PRIO); } int main(void) { struct netif *netif; ip_addr_t ipaddr, netmask, gw; lwip_init(); IP4_ADDR(ipaddr, 192, 168, 1, 20); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1); netif xemac_add(netif_static, ipaddr, netmask, gw, mac_address, 0); if (!netif) { xil_printf(xemac_add failed\r\n); return -1; } netif_set_default(netif); netif_set_up(netif); // 这里还会创建IPv6地址见下一节 start_application(); while (1) { sys_check_timeouts(); } return 0; }3.4 主机端快速验证ping、nc、curl板子烧录后插上网线在PC上先测试IPv4连通性ping 192.168.1.20 nc 192.168.1.20 8080能通说明IP层和TCP层工作正常。对于IPv6先用下一节的方法给板子配上地址然后在Linux主机上测试ping6 -I eth0 fe80::xxxx nc -6 fe80::xxxx%eth0 8080 curl -6 http://[2001:db8::1]:8080/注意在Linux上ping link-local地址必须指定网卡用%eth0后缀或者在ping命令里加-I这是很多人第一次测IPv6最容易卡住的地方。4. IPv6地址从无到有link-local、SLAAC与静态地址4.1 为什么网口都通了却拿不到IPv6地址很多人在IPv4通了之后开始折腾IPv6结果发现板子只有一个IPv4地址IPv6地址完全消失。这里要理解IPv6地址不是“插上网线就有”的即便lwIP打开了IPv6编译选项也还需要一个“邻居发现”的过程。在IPv6网络中设备会先自动生成一个link-local地址fe80::/10这个地址不需要路由器也不需要DHCP任何接口只要启用IPv6就会生成。但真正能跨网段访问的全局单播地址则需要通过三种方式之一获得SLAAC无状态自动配置设备发起路由器请求RS路由器回复RA报文设备根据RA里的前缀自己生成IPv6地址。DHCPv6有状态的地址分配。静态配置手工指定IPv6地址。lwIP 2.0.x对SLAAC有基础实现但Xilinx的BSP默认并不保证启用。多数情况下板子插上去只有link-local地址没有全局地址原因就是没有路由器在发RA或者lwIP的自动配置开关没打开。很多家用路由器默认关闭IPv6或者刷了精简版固件后把IPv6功能砍掉了这种情况下板子自然“拿不到”全局地址。4.2 代码里手工创建link-local与全局IPv6地址最简单的调试方式是不依赖路由器直接在代码里手工配置IPv6地址。注册网络接口后继续调用lwIP的IPv6相关API#include lwip/ip6_addr.h #include lwip/netif.h // 在xemac_add之后调用 netif_create_ip6_linklocal_address(netif, 1); netif_ip6_addr_set_state(netif, 0, IP6_ADDR_VALID); // 手工添加一个全局单播地址比如2001:db8::1/64 ip6_addr_t ip6addr; IP6_ADDR(ip6addr, 0x2001, 0x0db8, 0, 0x0001); s8_t idx 1; err_t err netif_add_ip6_address(netif, ip6addr, idx); if (err ERR_OK) { netif_ip6_addr_set_state(netif, idx, IP6_ADDR_VALID); }这段代码里netif_create_ip6_linklocal_address会根据MAC地址自动生成fe80::开头的link-local地址netif_add_ip6_address负责添加全局地址。如果BSP里的lwIP版本API略有差异以实际头文件为准但思路是通用的先有地址再把地址状态置为VALID协议栈才会真正使用它。如果想走SLAAC自动获取可以在lwipopts.h中打开LWIP_IPV6_AUTOCONFIG然后确保网络环境中有路由器定期发送RA报文。实测下来家用路由器开IPv6后板子过几秒就能拿到2001:开头的全局地址。4.3 真机联调Linux和Windows下的IPv6测试命令常见的主机侧调试命令如下建议收藏Linux侧ip -6 addr show # 查看本机IPv6地址 ip -6 route show # 查看IPv6路由表 ping6 -I eth0 fe80::xxxx # ping开发板link-local地址 nc -6 -v 2001:db8::1 8080 # IPv6 TCP连接测试Windows侧netsh interface ipv6 show prefixpolicies ping -6 fe80::xxxx%12Windows上ping link-local地址后要加%接口索引可以用netsh interface ipv6 show address查到开发板所连网卡对应的索引。热搜里经常看到有人问“netsh interface ipv6 show prefixpolicies”是干什么的这个命令本质上是查看IPv6前缀优先级的策略表做双栈路由选择时确实很关键。4.4 双栈socket的注意点如果服务器需要同时支持IPv4和IPv6最稳妥的做法并不是依赖所谓的“IPv4-mapped IPv6地址”而是直接开两个socket、两个监听线程一个用AF_INET监听IPv4一个用AF_INET6监听IPv6。这样做的好处是逻辑清晰不依赖lwIP是否编译了IPv4映射IPv6的选项。实测中我遇到过只开一个AF_INET6socket结果IPv4客户端始终连不上来的情况。后来直接在main里启动两个线程各自监听8080端口的v4和v6流量问题立刻消失。如果你对端口资源不敏感双线程双监听是最省心的方案。5. 千兆吞吐实测与一连串故障的排查过程5.1 实测数据MicroBlaze的带宽上限MicroBlaze跑TCP服务器的性能受CPU主频、内存带宽、lwIP配置、DMA模式等因素影响很大。我基于MA703FA的默认配置CPU 100MHzDDR3内存AXI DMA简单模式用iperf做过一轮简单测试测试项实测结果TCP发送板子到PC约40 ~ 70 MbpsTCP接收PC到板子约35 ~ 65 MbpsUDP发送约80 ~ 120 Mbps这个数据比很多人预期的“千兆”低不少但这就是软核方案的现实。MicroBlaze本质上是一个通用处理器跑协议栈需要大量内存拷贝和中断处理不可能像硬核ARM或ASIC那样满速转发。性能优化方向后面会讲。5.2 故障一Link灯不亮MDIO读不到PHY这是排查链路的第一关。如果插上网线后PHY的Link灯完全不亮首先要确认的是MDIO能不能访问PHY寄存器。排错顺序如下检查PHY复位是否释放。很多板子的PHY复位信号低有效如果GPIO没有输出高电平PHY一直处于复位状态。检查MDC频率是否太高。MDIO最高支持2.5MHz左右有些设计里直接把100MHz的时钟接到了MDC上导致读取失败。用串口打印PHY ID寄存器。RTL8211FD的PHY ID可以通过MDIO读取能读出正确的ID值说明MDIO物理链路是通的。如果MDIO能读通但Link灯还是不亮再检查RGMII的125MHz时钟和PHY的供电。5.3 故障二能Link但ping不通Link灯亮了说明物理层没问题问题出在MAC层或IP层。常见的排查链路如下看串口日志里lwIP是否打印了xemac_add成功的信息。如果xemac_add返回空指针多半是内存不足或MAC地址没有正确传入。确认MAC地址没有和局域网里的其他设备冲突。MicroBlaze这类软核开发板经常被人用默认MAC地址比如00:0a:35:00:01:02如果你手头有多块板子全用同一个MAC就会出各种诡异问题。在PC上抓包开发板ping PC看有没有ARP请求发出。如果板上没有发出ARP通常是lwIP初始化时序不对或者网络接口没有正确netif_set_up。检查中断连接。AXI DMA的接收中断如果没有接到AXI Intc板子收不到数据自然ping不通。这类问题最费时间的往往是中断。Vivado里连线看起来没问题但SDK里的XPAR宏和实际连接顺序不一致时中断处理的回调函数地址就会错位。我建议在出问题时先把中断全部打印出来中断号、回调地址、触发状态逐一核对。5.4 故障三IPv6地址一直不出现IPv6地址不出现不要急着改代码先分清楚是“地址压根没生成”还是“地址生成了却不可达”如果netif的IPv6地址列表是空的检查LWIP_IPV6是否真的被编译进去。BSP设置界面勾选后有时候并不会重新生成lwip库必须手动改lwipopts.h并clean编译。如果link-local地址存在但没有全局地址说明SLAAC没有生效。要么打开LWIP_IPV6_AUTOCONFIG要么手工加全局地址。如果地址存在但ping不通检查路由器和交换机是否开启了IPv6转发。很多设备的IPv6默认不开需要在管理页面里把IPv6功能打开。在PC上用ip -6 neigh查看邻居缓存如果开发板的MAC没有出现在邻居表里说明邻居发现报文没有到达PC。这时查交换机的IPv6组播组成员配置很多老交换机对IPv6组播报文支持不完整会直接丢弃。5.5 性能优化方向如果你对吞吐量有更高要求可以从以下几个方向入手提高MicroBlaze主频。MA703FA上的XC7A35T在-2速度等级下跑到125MHz甚至150MHz完全可行适当加快CPU会让TCP/IP协议栈的处理速度明显提升。打开MicroBlaze的I-Cache和D-Cache。对lwIP这类大量数据搬运的负载Cache命中率上来了性能提升非常明显。使用AXI DMA的Scatter Gather模式。SG模式能减少CPU介入描述符管理的次数吞吐量一般能提升10%到30%。增大lwIP的TCP发送和接收窗口比如TCP_WND设为65535TCP_MSS保持1460配合更大的TCP_SND_BUF可以减少丢包和重传。换用裸机下的raw API代替socket API。socket API方便但每一层封装都有代价。如果项目不复杂用raw API的tcp_writetcp_output能省下不少CPU周期。6. 固化到板载FlashMMI、ELF与BIT合并生成MCS6.1 MMI文件的作用项目调试通过后总不能每次上电都开着Vivado重新下载比特流。MicroBlaze和Zynq不一样它是纯FPGA逻辑上电后需要从配置Flash把比特流加载到FPGA里处理器运行的程序则要放到启动镜像中。问题在于MicroBlaze的ELF程序是独立编译出来的而FPGA比特流只是硬件逻辑的映射。想让板子上电后自动运行软件就必须把ELF文件合并到比特流里去。Xilinx的方案就是利用MMI文件来完成这一步。MMIMemory Map Information文件记录了FPGA内BRAM或者存储器的地址映射信息描述了ELF里的数据段应该放到哪个物理地址。Vivado综合之后工程目录下会生成一个以工程名命名的.mmi文件。这个文件是后面updatemem命令的输入没有它工具不知道ELF的内容该合并到比特流的什么位置。6.2 两条命令完成合并与转换在Vivado的Tcl Console里先确认当前打开的工程然后执行updatemem -meminfo system.mmi -data test_app.elf -bit system.bit \ -proc system_i/microblaze_0 -out system_merged.bit参数说明-meminfo指定Vivado生成的MMI文件路径。-data指定SDK编译出来的ELF文件路径。-bit指定未合并的原始比特流文件路径。-proc指定工程里的MicroBlaze实例名比如system_i/microblaze_0。如果设计里有多个MicroBlaze这个参数不能省。-out指定合并后的比特流名称。合并完成后再把合并比特流转换成MCS格式才能烧写到SPI Flash里。以常见的SPI x4模式、板载W25Q64为例write_cfgmem -format mcs -interface spix4 -size 64 \ -loadbit up 0x0 system_merged.bit -file system_merged.mcs-size的单位是MbW25Q64对应64。如果板载Flash容量不同根据实际型号调整。生成MCS之后这一步会产生一个.prm文件和一个.mcs文件烧写时选MCS文件即可。6.3 烧写与上电自启动验证烧写Flash的路径是菜单栏Flow - Open Hardware Manager - 右键目标FPGA设备 - Add Configuration Memory Device。在弹出的窗口里选择对应的SPI Flash型号然后加载编译好的MCS文件点击Program。等待烧写完成断电重新上电。如果一切正常串口会在启动后打印出lwIP的初始化日志以及服务器监听信息IPv4和IPv6地址都能被主机ping通。至此这块MA703FA就不再是吃灰学习板而是一台上电自启、v4/v6可用的微型网络服务器了。