← ブログ一覧へ
FPGAZynqlwIP以太网HTTPGEM裸机嵌入式网络

Zynq 实战 17|lwIP + 千兆以太网:一个能用的 HTTP Server

この記事は中国語で書かれ、Google 翻訳で自動翻訳されています。
中国語の原文を見る →

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

Pynq-Z2 以太网通路:RTL8211E → GEM0 → lwIP RJ45 1000BASE-T 差分对 MDI RTL8211E 千兆 PHY RGMII 接口 MDC/MDIO (MIO 52/53) MDI RGMII MIO 16-27 PS GEM0 Gigabit Ethernet MAC DMA: BD 环形缓冲区 中断: GIC SPI #22(RX)/#21(TX) AXI PS DDR3 512 MB BD 环 + 以太网帧 buffer 软件层(Vitis 裸机 + lwIP 2.1.x) lwIP 2.1.x IP/ICMP/TCP/UDP RAW API (Callback) netif → xemacif 驱动 HTTP Server 端口 8080 GET /status → JSON tcp_pcb + recv callback 板子状态采集 XADC: 温度(PS 内部传感器) PWM IP MMIO: 占空比回读 XTime_GetTime(): 运行时间
图 1. Pynq-Z2 以太网通路:RTL8211E 通过 RGMII+MIO 连 PS GEM0,lwIP 在 PS DDR 里维护 BD 环

MIO 分配(Pynq-Z2 原理图)

MIO 编号信号方向说明
MIO 16RGMII_TX_CLK输出125 MHz TX 时钟
MIO 17RGMII_TXD[0]输出TX 数据位 0
MIO 18RGMII_TXD[1]输出TX 数据位 1
MIO 19RGMII_TXD[2]输出TX 数据位 2
MIO 20RGMII_TXD[3]输出TX 数据位 3
MIO 21RGMII_TX_CTL输出TX 使能/错误
MIO 22RGMII_RX_CLK输入125 MHz RX 时钟(PHY 提供)
MIO 23RGMII_RXD[0]输入RX 数据位 0
MIO 24RGMII_RXD[1]输入RX 数据位 1
MIO 25RGMII_RXD[2]输入RX 数据位 2
MIO 26RGMII_RXD[3]输入RX 数据位 3
MIO 27RGMII_RX_CTL输入RX 数据有效/错误
MIO 52MDIO_CLK输出MDC(管理接口时钟,<2.5 MHz)
MIO 53MDIO_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_zynq0我们用 GEM(PS MAC),不用 PL AXI Ethernet
use_emaclite_on_zynq0不用 EmacLite
temac_use_jumbo_frames0暂不启用 jumbo(坑,见第 9 节)
tcp_mss1460标准以太网 MTU 1500 - IP 20 - TCP 20
mem_size131072lwIP heap,128KB,够用
memp_n_tcp_pcb16最大并发 TCP 连接数
tcp_snd_buf8192发送缓冲 8KB(影响 TX 吞吐)
tcp_wnd8192接收窗口 8KB

🚧 避坑tcp_wndtcp_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)TCPiperf3 -c940 Mbps
板子 → PC(TX)TCPiperf3 -s880 Mbps
双向同时TCPiperf3 -dRX 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/GEM1PL AXI Ethernet + TEMAC
性能1 Gbps full duplex1 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 在两个核心之间传递消息
  • 实测核间通信延迟和吞吐

参考资料

文档号名称用途
UG585Zynq-7000 SoC TRM第 16 章:GEM 寄存器映射、DMA BD 环结构、MDIO 时序
PG021AXI Ethernet Subsystem Product GuideAXI Ethernet vs PS GEM 功能对比
RTL8211E DSRealtek RTL8211E/EG/EL 数据手册PHY reset 时序(Table 5)、RGMII 接口规范
lwIP WikilwIP Application Developers ManualRAW API vs Sequential API 详细说明
UG4807 Series FPGAs and Zynq-7000 SoC XADC Dual 12-Bit ADC温度传感器 ADC 转换公式(Table 2)
XAPP1026lwIP TCP/IP Stack for ZynqXilinx lwIP BSP 配置参数说明

所有 AMD 文档可在 docs.amd.com 免费下载。lwIP 文档在 savannah.nongnu.org/projects/lwip


这是《Zynq FPGA 嵌入式系统设计实战》系列第 17 篇。 如果你在 PHY reset 时序、DHCP 超时、或 TCP 发送乱码上踩了坑,欢迎留言——把串口输出的最后 10 行一起贴出来,定位速度会快很多。