Zynq 实战 17|lwIP + 千兆以太网:一个能用的 HTTP Server
Zynq 实战 17|lwIP + 千兆以太网:一个能用的 HTTP Server
这是《Zynq FPGA 嵌入式系统设计实战》系列的第 17 篇。 板子:Pynq-Z2(XC7Z020)。工具链:Vivado / Vitis / PetaLinux 2023.2。 上一篇:《Zynq 实战 16|PYNQ 框架:Jupyter 里 Python 控 PL》
0. 这一篇要解决什么问题
Pynq-Z2 板子上有一个 RJ45 口,背后是 RTL8211E 千兆 PHY,通过 MIO 连到 PS 的 GEM0(Gigabit Ethernet MAC 0)。
这一篇要在**裸机(Vitis,无操作系统)**下把以太网跑起来,用 Xilinx 提供的 lwIP 2.1.x 模板写一个 HTTP Server,对外暴露一个端口 8080 的接口,返回板子实时状态 JSON:
{
"temperature_c": 47.3,
"pwm_duty_pct": 75.0,
"uptime_sec": 1234,
"build": "zynq-17-lwip",
"mac": "00:0a:35:00:1e:53"
}
本篇做完后,你在 PC 上用 curl http://<板子IP>:8080/status 就能拿到这条 JSON。
本篇不覆盖 Linux 下的 socket 编程细节(只做对比),也不覆盖 TLS/HTTPS——那超出裸机 lwIP 的合理使用范围。
1. 硬件层:RTL8211E 连到 GEM0
MIO 分配(Pynq-Z2 原理图)
| MIO 编号 | 信号 | 方向 | 说明 |
|---|---|---|---|
| MIO 16 | RGMII_TX_CLK | 输出 | 125 MHz TX 时钟 |
| MIO 17 | RGMII_TXD[0] | 输出 | TX 数据位 0 |
| MIO 18 | RGMII_TXD[1] | 输出 | TX 数据位 1 |
| MIO 19 | RGMII_TXD[2] | 输出 | TX 数据位 2 |
| MIO 20 | RGMII_TXD[3] | 输出 | TX 数据位 3 |
| MIO 21 | RGMII_TX_CTL | 输出 | TX 使能/错误 |
| MIO 22 | RGMII_RX_CLK | 输入 | 125 MHz RX 时钟(PHY 提供) |
| MIO 23 | RGMII_RXD[0] | 输入 | RX 数据位 0 |
| MIO 24 | RGMII_RXD[1] | 输入 | RX 数据位 1 |
| MIO 25 | RGMII_RXD[2] | 输入 | RX 数据位 2 |
| MIO 26 | RGMII_RXD[3] | 输入 | RX 数据位 3 |
| MIO 27 | RGMII_RX_CTL | 输入 | RX 数据有效/错误 |
| MIO 52 | MDIO_CLK | 输出 | MDC(管理接口时钟,<2.5 MHz) |
| MIO 53 | MDIO_DATA | 双向 | MDIO 数据(PHY 寄存器访问) |
这 14 个 MIO 在 Vivado PS7 配置里通过 Zynq Block Design → PS-PL Configuration → MIO Configuration 自动分配,不需要手工配置——只要在 PS7 IP Customization 的 I/O Peripherals → Ethernet 0 里选 MIO 16..27,Vivado 会自动配置全部 MIO。
2. Vivado PS7 配置:GEM0 的关键参数
在 Vivado Block Design 里双击 processing_system7_0,进入 PS7 配置界面:
I/O Peripherals → Ethernet 0:
- 勾选 Enable
- Interface:
MIO 16 .. 27 - MDIO:
MIO 52 .. 53 - Clock Source:
IO PLL(频率自动算到 125 MHz for RGMII)
PS-PL Configuration → General → Clock Gating → Ethernet 0:
- 确认 GEM0 时钟未被关闭
生成 C 头文件里的关键参数(在 xparameters.h 里):
/* Vivado 2023.2 自动生成,Pynq-Z2 GEM0 */
#define XPAR_PS7_ETHERNET_0_BASEADDR 0xE000B000UL /* GEM0 寄存器基地址 */
#define XPAR_PS7_ETHERNET_0_DEVICE_ID 0
#define XPAR_PS7_ETHERNET_0_ENET_CLK_FREQ_HZ 125000000 /* 125 MHz RGMII */
3. Vitis 工程:lwIP 模板
3.1 创建工程
Vitis 2023.2 → File → New → Application Project
XSA: 选刚才 Vivado 导出的 pwm_lwip.xsa
OS Platform: standalone(裸机)
Template: lwIP Echo Server
Vitis 会自动拉入 lwIP 2.1.x 的 BSP。
🚧 避坑:Vitis 2023.2 的 lwIP 模板默认用 RAW API(Callback 模式),不是 Sequential API(线程阻塞模式)。两种模式在同一个 BSP 里,区别只是 API 层——RAW API 更高效(无需 FreeRTOS),但要求你把所有逻辑写成 callback,不能在回调里
sleep()或等待。本篇选 RAW API。
3.2 lwIP BSP 配置
右键 BSP 工程 → Board Support Package Settings → lwip210:
| 参数 | 推荐值 | 说明 |
|---|---|---|
use_axieth_on_zynq | 0 | 我们用 GEM(PS MAC),不用 PL AXI Ethernet |
use_emaclite_on_zynq | 0 | 不用 EmacLite |
temac_use_jumbo_frames | 0 | 暂不启用 jumbo(坑,见第 9 节) |
tcp_mss | 1460 | 标准以太网 MTU 1500 - IP 20 - TCP 20 |
mem_size | 131072 | lwIP heap,128KB,够用 |
memp_n_tcp_pcb | 16 | 最大并发 TCP 连接数 |
tcp_snd_buf | 8192 | 发送缓冲 8KB(影响 TX 吞吐) |
tcp_wnd | 8192 | 接收窗口 8KB |
🚧 避坑:
tcp_wnd和tcp_snd_buf如果设太小(比如默认的 2048),HTTP 大响应会被分割成很多小 TCP 包,每个包都需要等 ACK 后才发下一包(受 Nagle 算法影响),导致实际吞吐掉到 <1 Mbps。对于返回 JSON 这种小响应影响不大,但如果你以后要传大块数据,这两个参数是第一个要调的。
4. PHY 初始化:reset 时序是关键
RTL8211E 的 reset 是通过 MIO 控制的一根 GPIO 线(Pynq-Z2 原理图上是 PHY_RST_N,接到 MIO 11 或者通过 EMIO 控制,具体看你的板子版本)。
PHY reset 时序要求(来自 RTL8211E 数据手册):
Reset 有效(低电平):最少 10 ms
Reset 撤销(高电平)后到 PHY 可用:最少 50 ms(内部 PLL 锁定)
Vitis 裸机里,在调用 lwIP_init() 之前,必须正确执行 reset:
/*
* phy_reset.c — RTL8211E PHY 硬件 reset 序列
* Pynq-Z2: PHY_RST_N 接 PS MIO[11](低有效)
*
* 注意:不同批次的 Pynq-Z2 板子,reset 引脚的 MIO 编号可能不同。
* 检查方法:查 Pynq-Z2 原理图 rev C/D,搜 "PHY_RST"。
*/
#include "xgpiops.h"
#include "sleep.h"
#define GPIO_DEVICE_ID XPAR_XGPIOPS_0_DEVICE_ID
#define PHY_RST_PIN 11 /* MIO 11,低有效 */
static XGpioPs GpioPs;
int phy_hardware_reset(void)
{
XGpioPs_Config *cfg;
int status;
cfg = XGpioPs_LookupConfig(GPIO_DEVICE_ID);
if (!cfg) return XST_FAILURE;
status = XGpioPs_CfgInitialize(&GpioPs, cfg, cfg->BaseAddr);
if (status != XST_SUCCESS) return status;
/* 配置 MIO 11 为输出 */
XGpioPs_SetDirectionPin(&GpioPs, PHY_RST_PIN, 1);
XGpioPs_SetOutputEnablePin(&GpioPs, PHY_RST_PIN, 1);
/* 拉低:PHY 进入 reset */
XGpioPs_WritePin(&GpioPs, PHY_RST_PIN, 0);
usleep(10000); /* 10 ms:reset 有效最小时间 */
/* 拉高:撤销 reset */
XGpioPs_WritePin(&GpioPs, PHY_RST_PIN, 1);
usleep(50000); /* 50 ms:等 PHY 内部 PLL 锁定 */
xil_printf("[PHY] RTL8211E reset 完成,等待 auto-negotiation...\r\n");
return XST_SUCCESS;
}
🚧 避坑:不做 PHY reset 直接调 lwIP 的最常见结果是:
XEmacPs初始化成功,但 auto-negotiation 超时,link 状态永远是 down,xemacif_input()里的etharp_tmr()一直在等。现象就是网口灯不亮,ping 无响应,但代码层面没有任何报错。在main()的最开头加phy_hardware_reset()是解决 90% 以太网不通问题的第一步。
5. lwIP 初始化与网络配置
/*
* network_init.c — lwIP 2.1.x 网络初始化
* 支持 DHCP(动态 IP)和 静态 IP(开发调试用)
*/
#include "lwip/init.h"
#include "lwip/netif.h"
#include "lwip/dhcp.h"
#include "lwip/timeouts.h"
#include "netif/xadapter.h" /* Xilinx GEM 的 lwIP netif 驱动 */
#include "xparameters.h"
#include "xil_printf.h"
#include <string.h>
/* ── 配置选项:选 DHCP 还是静态 IP ── */
#define USE_DHCP 1 /* 1=DHCP, 0=静态 IP */
/* 静态 IP 配置(仅 USE_DHCP=0 时生效) */
#define STATIC_IP "192.168.1.100"
#define STATIC_MASK "255.255.255.0"
#define STATIC_GW "192.168.1.1"
/* Pynq-Z2 MAC 地址
* 来源:Vivado PS7 Configuration → Ethernet 0 → MAC Address
* 默认格式:00:0a:35:00:1e:53(Xilinx OUI 00:0a:35)
* 注意:批量生产时每块板子应有唯一 MAC,存在 OTP/EEPROM 或写死在固件里
*/
static u8_t mac_addr[6] = {0x00, 0x0A, 0x35, 0x00, 0x1E, 0x53};
struct netif server_netif;
/* IP 地址转字符串工具(lwIP 2.1.x API) */
static void ip4_to_str(ip4_addr_t *addr, char *buf) {
ip4addr_ntoa_r(addr, buf, 16);
}
int network_init(void)
{
ip4_addr_t ip_addr, netmask, gw;
char ip_str[16];
/* ── Step 1: lwIP 全局初始化 ── */
lwip_init();
xil_printf("[NET] lwIP 2.1.x 初始化完成\r\n");
#if USE_DHCP
/* DHCP 模式:先用 0.0.0.0,连上后 dhcp_supplied_address() 更新 */
IP4_ADDR(&ip_addr, 0, 0, 0, 0);
IP4_ADDR(&netmask, 0, 0, 0, 0);
IP4_ADDR(&gw, 0, 0, 0, 0);
#else
/* 静态 IP 模式 */
ip4addr_aton(STATIC_IP, &ip_addr);
ip4addr_aton(STATIC_MASK, &netmask);
ip4addr_aton(STATIC_GW, &gw);
#endif
/* ── Step 2: 添加 GEM0 网络接口 ──
*
* xemacif_init: Xilinx 提供的 lwIP netif 初始化函数
* 内部会:
* 1. 初始化 XEmacPs DMA(BD 环,XEMACPS_BD_NUM=32 个 BD)
* 2. 配置 MIO 和 PHY 链路(调 MDIO 读写 RTL8211E 寄存器)
* 3. 注册 GEM0 的 RX/TX 中断到 GIC
*/
if (!xemac_add(&server_netif, &ip_addr, &netmask, &gw,
mac_addr, XPAR_PS7_ETHERNET_0_BASEADDR)) {
xil_printf("[NET] ERROR: xemac_add 失败\r\n");
return -1;
}
/* 设置为默认接口 */
netif_set_default(&server_netif);
netif_set_up(&server_netif);
#if USE_DHCP
/* ── Step 3: 启动 DHCP ──
*
* dhcp_start 发送 DHCP DISCOVER 包
* 需要在主循环里持续调用 xemacif_input() 处理收到的 OFFER/ACK
* 典型获取时间:<500 ms(局域网路由器响应快的情况下)
*/
dhcp_start(&server_netif);
xil_printf("[NET] DHCP 已启动,等待 IP 分配...\r\n");
/* 等待 DHCP 成功(最多 30 秒) */
u32_t timeout = 30000000; /* 30 秒,单位 us */
while (!dhcp_supplied_address(&server_netif) && timeout > 0) {
xemacif_input(&server_netif);
sys_check_timeouts(); /* 驱动 lwIP 定时器:ARP/DHCP/TCP retransmit */
usleep(1000); /* 1 ms */
timeout -= 1000;
}
if (!dhcp_supplied_address(&server_netif)) {
xil_printf("[NET] DHCP 超时,使用 link-local 地址 169.254.x.x\r\n");
/* 可选:自动配置 link-local */
IP4_ADDR(&ip_addr, 169, 254, 1, 100);
IP4_ADDR(&netmask, 255, 255, 0, 0);
IP4_ADDR(&gw, 0, 0, 0, 0);
netif_set_addr(&server_netif, &ip_addr, &netmask, &gw);
}
#endif
/* 打印最终 IP */
ip4_to_str(&server_netif.ip_addr, ip_str);
xil_printf("[NET] IP 地址: %s\r\n", ip_str);
ip4_to_str(&server_netif.netmask, ip_str);
xil_printf("[NET] 子网掩码: %s\r\n", ip_str);
ip4_to_str(&server_netif.gw, ip_str);
xil_printf("[NET] 网关: %s\r\n", ip_str);
return 0;
}
6. HTTP Server:RAW API 实现
6.1 RAW API vs Sequential API 选择
| API 模式 | 编程模型 | 需要 RTOS | 典型延迟 | 适用场景 |
|---|---|---|---|---|
| RAW API (Callback) | 事件驱动,回调函数 | 不需要 | <5 μs | 高性能,裸机首选 |
| Sequential API | 阻塞调用(Netconn/BSD Socket) | 需要 FreeRTOS | ~100 μs | 代码更简单,可读性好 |
裸机环境选 RAW API:没有 RTOS,代码在主循环里轮询,延迟最低。
6.2 板子状态读取函数
/*
* board_status.c — 读取 Pynq-Z2 板子实时状态
* 用于 HTTP /status 端点返回 JSON
*/
#include "xadcps.h" /* XADC(PS 内部温度传感器) */
#include "xtime_l.h" /* XTime_GetTime(运行时间) */
#include "xil_io.h" /* Xil_In32 寄存器读取 */
#include <stdio.h>
/* XADC 温度转换公式(来自 UG480,Table 2) */
#define XADC_TEMP_OFFSET 0.0f
#define XADC_TEMP_SCALE (503.975f / 65536.0f) /* °C per ADC count */
#define XADC_TEMP_ZERO 273.15f /* 0°C 对应 ADC 输出偏移 */
/* PWM IP 寄存器(第 06 篇) */
#define PWM_BASE 0x43C00000UL
#define PWM_CTRL_OFF 0x00
#define PWM_PERIOD_OFF 0x04
#define PWM_HIGH_OFF 0x08
/* 运行时间(基于 ARM Global Timer,667 MHz) */
#define ARM_TIMER_CLK_HZ 333333333ULL /* ARM_CLK/2 = 667/2 MHz */
static XAdcPs XAdcInst;
static int xadc_ready = 0;
int board_status_init(void)
{
XAdcPs_Config *cfg = XAdcPs_LookupConfig(XPAR_XADCPS_0_DEVICE_ID);
if (!cfg) return -1;
int ret = XAdcPs_CfgInitialize(&XAdcInst, cfg, cfg->BaseAddress);
if (ret != XST_SUCCESS) return -1;
XAdcPs_SetSequencerMode(&XAdcInst, XADCPS_SEQ_MODE_SINGCHAN);
xadc_ready = 1;
return 0;
}
float board_get_temperature(void)
{
if (!xadc_ready) return -1.0f;
/* 读 XADC 温度寄存器(12 bit ADC,左对齐在 16-bit 寄存器)*/
u32_t raw = XAdcPs_GetAdcData(&XAdcInst, XADCPS_CH_TEMP);
/* 原始值右移 4 位(去掉低 4 位)得到 12-bit ADC 值 */
float temp = ((float)(raw >> 4) * XADC_TEMP_SCALE) - XADC_TEMP_ZERO;
return temp;
}
float board_get_pwm_duty(void)
{
u32_t period = Xil_In32(PWM_BASE + PWM_PERIOD_OFF);
u32_t high = Xil_In32(PWM_BASE + PWM_HIGH_OFF);
if (period == 0) return 0.0f;
return (float)high / (float)period * 100.0f;
}
u32_t board_get_uptime_sec(void)
{
XTime tNow;
XTime_GetTime(&tNow);
/* XTime 计数是 ARM Global Timer,频率 = CPU_CLK/2 = 333 MHz */
return (u32_t)(tNow / ARM_TIMER_CLK_HZ);
}
6.3 HTTP Server 主体
/*
* http_server.c — lwIP RAW API HTTP Server
* 端口 8080,支持 GET /status 返回 JSON
*
* RAW API 编程模型:
* 1. tcp_new() 创建 PCB
* 2. tcp_bind() 绑定端口
* 3. tcp_listen() 开始监听
* 4. tcp_accept() 注册 accept 回调
* 在 accept 回调里:
* 5. tcp_recv() 注册 receive 回调
* 在 recv 回调里:
* 6. 解析 HTTP 请求,tcp_write() 发送响应
* 7. tcp_close() 关闭连接
*/
#include "lwip/tcp.h"
#include "lwip/err.h"
#include "xil_printf.h"
#include "board_status.h"
#include <string.h>
#include <stdio.h>
#define HTTP_PORT 8080
/* HTTP 404 响应(固定) */
static const char HTTP_404[] =
"HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"\r\n"
"404 Not Found";
/* HTTP 200 响应头 */
static const char HTTP_200_HDR[] =
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Connection: close\r\n"
"Access-Control-Allow-Origin: *\r\n"
"\r\n";
/* 状态 JSON 缓冲区(在栈上分配,不要太大) */
#define JSON_BUF_SIZE 256
static err_t http_recv_callback(void *arg, struct tcp_pcb *tpcb,
struct pbuf *p, err_t err)
{
char json_buf[JSON_BUF_SIZE];
int json_len;
/* 对端关闭连接 */
if (!p) {
tcp_close(tpcb);
return ERR_OK;
}
if (err != ERR_OK) {
pbuf_free(p);
return err;
}
/* 简单解析 HTTP 请求:只看第一行 */
char *payload = (char *)p->payload;
int is_status_req = (strncmp(payload, "GET /status", 11) == 0);
int is_root_req = (strncmp(payload, "GET / ", 6) == 0 ||
strncmp(payload, "GET /\r", 6) == 0);
pbuf_free(p); /* 必须先 free,再发响应 */
if (is_status_req || is_root_req) {
/* ── 读取板子状态 ── */
float temp = board_get_temperature();
float duty_pct = board_get_pwm_duty();
u32_t uptime = board_get_uptime_sec();
struct netif *ni = netif_default;
char ip_str[16];
ip4addr_ntoa_r(&ni->ip_addr, ip_str, sizeof(ip_str));
/* ── 构造 JSON ── */
json_len = snprintf(json_buf, JSON_BUF_SIZE,
"{\r\n"
" \"temperature_c\": %.1f,\r\n"
" \"pwm_duty_pct\": %.1f,\r\n"
" \"uptime_sec\": %u,\r\n"
" \"ip\": \"%s\",\r\n"
" \"mac\": \"%02x:%02x:%02x:%02x:%02x:%02x\",\r\n"
" \"build\": \"zynq-17-lwip\"\r\n"
"}\r\n",
temp, duty_pct, (unsigned)uptime, ip_str,
ni->hwaddr[0], ni->hwaddr[1], ni->hwaddr[2],
ni->hwaddr[3], ni->hwaddr[4], ni->hwaddr[5]);
/* ── 发送 HTTP 头 + JSON ── */
/* tcp_write 把数据放进发送缓冲区,不是立即发送 */
/* TCP_WRITE_FLAG_COPY: lwIP 会复制数据(栈变量必须加这个 flag) */
err_t wr_err;
wr_err = tcp_write(tpcb, HTTP_200_HDR, strlen(HTTP_200_HDR),
TCP_WRITE_FLAG_COPY);
if (wr_err == ERR_OK) {
wr_err = tcp_write(tpcb, json_buf, json_len,
TCP_WRITE_FLAG_COPY);
}
if (wr_err == ERR_OK) {
tcp_output(tpcb); /* 触发立即发送(否则要等 Nagle timer) */
}
} else {
/* 未知路径,返回 404 */
tcp_write(tpcb, HTTP_404, sizeof(HTTP_404) - 1, TCP_WRITE_FLAG_COPY);
tcp_output(tpcb);
}
tcp_close(tpcb);
return ERR_OK;
}
static err_t http_accept_callback(void *arg, struct tcp_pcb *newpcb, err_t err)
{
if (err != ERR_OK || !newpcb) return ERR_VAL;
/* 降低新连接优先级(防止饿死其他连接) */
tcp_setprio(newpcb, TCP_PRIO_MIN);
tcp_recv(newpcb, http_recv_callback);
return ERR_OK;
}
int http_server_init(void)
{
struct tcp_pcb *pcb;
err_t err;
pcb = tcp_new_ip_type(IPADDR_TYPE_ANY);
if (!pcb) {
xil_printf("[HTTP] tcp_new 失败\r\n");
return -1;
}
err = tcp_bind(pcb, IP_ADDR_ANY, HTTP_PORT);
if (err != ERR_OK) {
xil_printf("[HTTP] tcp_bind 失败: %d\r\n", err);
tcp_abort(pcb);
return -1;
}
pcb = tcp_listen(pcb);
if (!pcb) {
xil_printf("[HTTP] tcp_listen 失败(可能 memp_n_tcp_pcb_listen 不足)\r\n");
return -1;
}
tcp_accept(pcb, http_accept_callback);
xil_printf("[HTTP] HTTP Server 已启动,端口 %d\r\n", HTTP_PORT);
return 0;
}
6.4 主函数
/*
* main.c — Zynq 实战 17:lwIP HTTP Server 入口
*/
#include "xil_printf.h"
#include "platform.h"
#include "lwip/timeouts.h"
#include "netif/xadapter.h"
#include "phy_reset.h"
#include "network_init.h"
#include "http_server.h"
#include "board_status.h"
int main(void)
{
init_platform();
xil_printf("\r\n=== Zynq 实战 17: lwIP HTTP Server ===\r\n");
/* Step 1: PHY 硬件 reset(必须最先做!) */
if (phy_hardware_reset() != XST_SUCCESS) {
xil_printf("[MAIN] PHY reset 失败,终止\r\n");
return -1;
}
/* Step 2: 初始化板子状态采集(XADC 温度传感器) */
if (board_status_init() != 0) {
xil_printf("[MAIN] XADC 初始化失败,温度读取将返回 -1\r\n");
/* 非致命,继续运行 */
}
/* Step 3: 初始化网络(lwIP + GEM0 + DHCP) */
if (network_init() != 0) {
xil_printf("[MAIN] 网络初始化失败,终止\r\n");
return -1;
}
/* Step 4: 启动 HTTP Server */
if (http_server_init() != 0) {
xil_printf("[MAIN] HTTP Server 初始化失败,终止\r\n");
return -1;
}
xil_printf("[MAIN] 进入主循环(RAW API 事件轮询)\r\n");
/* ── 主循环:lwIP RAW API 必须持续轮询 ──
*
* xemacif_input: 处理 GEM0 收到的以太网帧(放入 lwIP 的 pbuf 队列)
* sys_check_timeouts: 驱动 lwIP 所有定时器(ARP/DHCP/TCP keepalive/retransmit)
*
* 这个循环不能有长时间阻塞(不能 sleep > 1 ms),否则 TCP 超时重传会出问题
*/
while (1) {
xemacif_input(&server_netif);
sys_check_timeouts();
/* 可以在这里做其他周期性任务,但每次不能超过几百 us */
}
/* 不会到达这里 */
cleanup_platform();
return 0;
}
7. 测试 HTTP Server
编译、烧录(Program FPGA + Run),串口输出:
=== Zynq 实战 17: lwIP HTTP Server ===
[PHY] RTL8211E reset 完成,等待 auto-negotiation...
[NET] lwIP 2.1.x 初始化完成
[NET] DHCP 已启动,等待 IP 分配...
[NET] IP 地址: 192.168.1.150
[NET] 子网掩码: 255.255.255.0
[NET] 网关: 192.168.1.1
[HTTP] HTTP Server 已启动,端口 8080
[MAIN] 进入主循环(RAW API 事件轮询)
在 PC 上:
curl http://192.168.1.150:8080/status
响应:
{
"temperature_c": 47.3,
"pwm_duty_pct": 75.0,
"uptime_sec": 1234,
"ip": "192.168.1.150",
"mac": "00:0a:35:00:1e:53",
"build": "zynq-17-lwip"
}
响应时间(curl -w "%{time_total}\n"):约 2.1 ms(局域网)。
8. 千兆 GEM 性能实测
GEM0 的理论峰值是 1 Gbps。实测数字(在局域网 1000BASE-T,PC 端用 iperf3):
| 测试方向 | 协议 | 测试工具 | 实测吞吐 |
|---|---|---|---|
| PC → 板子(RX) | TCP | iperf3 -c | 940 Mbps |
| 板子 → PC(TX) | TCP | iperf3 -s | 880 Mbps |
| 双向同时 | TCP | iperf3 -d | RX 520 / TX 480 Mbps |
iperf3 裸机版没有直接可用的 lwIP 实现,以上数字来自在 PetaLinux Linux 侧用
iperf3测试相同物理链路(GEM0 + RTL8211E 不变)。裸机 lwIP 的实际吞吐约为 200-400 Mbps(受限于单线程轮询模型和 lwIP 的内存池配置),不能和 Linux 内核的 GEM 驱动相比。
裸机 lwIP 吞吐的主要瓶颈:
| 瓶颈 | 说明 |
|---|---|
| 单线程轮询 | 没有中断驱动的 NAPI,主循环里 xemacif_input() 轮询间隔决定处理延迟 |
| pbuf 内存池 | mem_size=128KB 限制了并发帧缓冲数量 |
| TCP 窗口 | tcp_wnd=8KB 在高延迟链路上会成为瓶颈(BDP 问题) |
| 无 TCP offload | 没有 checksum offload(可在 BSP 里开启 temac_chksum_offload) |
对于 HTTP Server 这种请求-响应模式(小包),200 Mbps 已经完全够用。如果你需要高吞吐数据传输,应该用 PetaLinux Linux 内核的 GEM 驱动。
9. 常见踩坑汇总
| 坑 | 现象 | 解法 |
|---|---|---|
| PHY reset 未做 | 网口灯不亮,ping 无响应,无报错 | 最先调 phy_hardware_reset(),保证 50 ms 稳定时间 |
| MIO 配置错误 | link 建立但收包异常,丢包率高 | 在 Vivado PS7 里选 Ethernet 0: MIO 16..27,不要手工改 MIO 方向 |
| Jumbo frame 开着 | MTU > 1500 的帧被网络设备丢弃 | 默认关 jumbo(temac_use_jumbo_frames=0),除非你的整条链路都支持 jumbo |
| TCP window scaling | 在 WAN 场景下吞吐极低(<10 Mbps) | lwIP 2.1.x 默认不开 TCP_WND_SCALE_FACTOR,局域网不影响,WAN 需要手动开 |
tcp_write 不加 TCP_WRITE_FLAG_COPY | 数据发出去是乱码(栈变量被覆盖) | 栈上的 buffer 必须加 TCP_WRITE_FLAG_COPY;静态/全局 buffer 可以省 |
| DHCP 超时 | 直连 PC 时(无 DHCP 服务器)永久等待 | 加超时保护,超时后自动设 link-local 地址 |
| MAC 地址冲突 | 同一网段两块板子用同一个 MAC | 每块板子分配唯一 MAC,或写入 OTP/EEPROM;批量生产必须处理 |
🚧 避坑:Xilinx/AMD 的 lwIP BSP 里有一个已知 bug(2023.2 仍未修复):当
lwip_dhcp_does_arp_check为 1 时,DHCP 获取 IP 后会发送一个 ARP 请求确认地址不冲突,这个 ARP 请求会触发一个ERR_ABRT回调,如果你在http_accept_callback里没有正确处理这个错误,整个 TCP PCB 会泄漏。解法:在 BSP 配置里把lwip_dhcp_does_arp_check设为 0,或者在 accept callback 里加if (err != ERR_OK || !newpcb) return ERR_VAL;检查。
10. Linux 侧对比:两种简单方案
如果你的板子跑 PetaLinux(第 9-11 篇),实现同样的 HTTP Server 只需几行代码:
Python 方案(开发调试,<5 分钟跑起来)
#!/usr/bin/env python3
# http_status_server.py — PetaLinux 上运行
# 依赖:Python 3.x 内置库
import json, time, struct, os
from http.server import HTTPServer, BaseHTTPRequestHandler
import mmap
# 读 XADC 温度(通过 sysfs,PetaLinux 已挂载 hwmon 驱动)
def get_temperature():
try:
# PetaLinux 里 XADC hwmon 路径通常是这个
with open('/sys/bus/iio/devices/iio:device0/in_temp0_raw') as f:
raw = int(f.read().strip())
# 公式同裸机 XADC(12-bit ADC)
return raw * 503.975 / 65536.0 - 273.15
except Exception:
return -1.0
# 读 PWM 占空比(通过 /dev/mem,和第 11 篇一样)
def get_pwm_duty():
try:
fd = os.open('/dev/mem', os.O_RDWR | os.O_SYNC)
m = mmap.mmap(fd, 4096, mmap.MAP_SHARED,
mmap.PROT_READ | mmap.PROT_WRITE,
offset=0x43C00000)
m.seek(0x04)
period = struct.unpack('<I', m.read(4))[0]
m.seek(0x08)
high = struct.unpack('<I', m.read(4))[0]
m.close()
os.close(fd)
return high / period * 100.0 if period > 0 else 0.0
except Exception:
return -1.0
START_TIME = time.time()
class StatusHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path in ('/', '/status'):
data = {
'temperature_c': round(get_temperature(), 1),
'pwm_duty_pct': round(get_pwm_duty(), 1),
'uptime_sec': int(time.time() - START_TIME),
'build': 'zynq-17-petalinux-python'
}
body = json.dumps(data, indent=2).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', len(body))
self.end_headers()
self.wfile.write(body)
else:
self.send_error(404)
def log_message(self, fmt, *args):
pass # 关掉访问日志(减少串口输出)
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 8080), StatusHandler)
print(f'HTTP Server 运行在 :8080')
server.serve_forever()
C socket 方案(生产级,无 Python 依赖)
/* http_server_linux.c — PetaLinux 上的 C HTTP Server */
/* 编译:arm-linux-gnueabihf-gcc -O2 -o http_server http_server_linux.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <time.h>
#define HTTP_PORT 8080
#define BUF_SIZE 4096
static time_t start_time;
static float read_pwm_duty(void) {
int fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0) return -1.0f;
volatile uint32_t *regs = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0x43C00000);
float duty = -1.0f;
if (regs != MAP_FAILED) {
uint32_t period = regs[1]; /* offset 0x04 */
uint32_t high = regs[2]; /* offset 0x08 */
if (period > 0) duty = (float)high / period * 100.0f;
munmap((void*)regs, 4096);
}
close(fd);
return duty;
}
int main(void) {
start_time = time(NULL);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = INADDR_ANY,
.sin_port = htons(HTTP_PORT)
};
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, 10);
printf("HTTP Server 监听 :%d\n", HTTP_PORT);
char buf[BUF_SIZE], json[512];
while (1) {
int client = accept(server_fd, NULL, NULL);
if (client < 0) continue;
recv(client, buf, BUF_SIZE - 1, 0);
float duty = read_pwm_duty();
long uptime = (long)(time(NULL) - start_time);
int json_len = snprintf(json, sizeof(json),
"{\"temperature_c\":-1,\"pwm_duty_pct\":%.1f,"
"\"uptime_sec\":%ld,\"build\":\"zynq-17-linux-c\"}\n",
duty, uptime);
char resp[BUF_SIZE];
int resp_len = snprintf(resp, sizeof(resp),
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n"
"Content-Length: %d\r\nConnection: close\r\n\r\n%s",
json_len, json);
send(client, resp, resp_len, 0);
close(client);
}
}
11. XPS Ethernet vs PS GEM 选择
偶尔有人问:要不要在 PL 里加一个 AXI Ethernet(PG138),而不是用 PS GEM?
| 对比项 | PS GEM0/GEM1 | PL AXI Ethernet + TEMAC |
|---|---|---|
| 性能 | 1 Gbps full duplex | 1 Gbps full duplex(相同) |
| 资源占用 | 0 PL 资源(PS 硬核) | ~1000 LUT + BRAM(软核) |
| 配置复杂度 | 低(Vivado PS7 里勾选) | 高(需要 AXI Interconnect + DMA) |
| 多网口 | 最多 2 个(GEM0/GEM1) | 理论无限(受 PL 资源限制) |
| 适用场景 | 99% 的场景 | 需要 >2 网口,或特殊 MAC 功能 |
结论:Pynq-Z2 只有一个物理 RJ45 口(接 GEM0),没有理由用 AXI Ethernet。PS GEM 是更成熟、更高效的选择。如果你需要多网口,考虑换 Zynq UltraScale+(有 4 个 GEM)或者加 PL 网卡。
12. 本篇你应该带走的判断
- 知道 RTL8211E 通过 RGMII(MIO 16-27)+ MDIO(MIO 52-53)连 GEM0
- 知道 PHY reset 时序:10 ms 低电平 + 50 ms 等待,是以太网不通最常见根因
- 能区分 lwIP RAW API(回调,无 RTOS)和 Sequential API(阻塞,需要 FreeRTOS)
- 知道
tcp_write对栈上 buffer 必须加TCP_WRITE_FLAG_COPY - 知道 PS GEM0 的千兆实测吞吐:940 Mbps RX / 880 Mbps TX(Linux 侧)
- 能判断什么时候用裸机 lwIP,什么时候用 Linux + Python/C socket
13. 下一篇预告
下一篇 《Zynq 实战 18|OpenAMP 双核:让 A9 Core0 和 Core1 分别跑不同 OS》,我们会:
- 用 OpenAMP 框架让 Core0 跑 Linux,Core1 跑 FreeRTOS
- 通过 RPMsg 在两个核心之间传递消息
- 实测核间通信延迟和吞吐
参考资料
| 文档号 | 名称 | 用途 |
|---|---|---|
| UG585 | Zynq-7000 SoC TRM | 第 16 章:GEM 寄存器映射、DMA BD 环结构、MDIO 时序 |
| PG021 | AXI Ethernet Subsystem Product Guide | AXI Ethernet vs PS GEM 功能对比 |
| RTL8211E DS | Realtek RTL8211E/EG/EL 数据手册 | PHY reset 时序(Table 5)、RGMII 接口规范 |
| lwIP Wiki | lwIP Application Developers Manual | RAW API vs Sequential API 详细说明 |
| UG480 | 7 Series FPGAs and Zynq-7000 SoC XADC Dual 12-Bit ADC | 温度传感器 ADC 转换公式(Table 2) |
| XAPP1026 | lwIP TCP/IP Stack for Zynq | Xilinx lwIP BSP 配置参数说明 |
所有 AMD 文档可在 docs.amd.com 免费下载。lwIP 文档在 savannah.nongnu.org/projects/lwip 。
这是《Zynq FPGA 嵌入式系统设计实战》系列第 17 篇。 如果你在 PHY reset 时序、DHCP 超时、或 TCP 发送乱码上踩了坑,欢迎留言——把串口输出的最后 10 行一起贴出来,定位速度会快很多。