【以太网摄像头采集存储终端】驱动 LAN8720A 实现基础的 HTTP 服务

1. 引言

或许听过 STM32 不同的联网方案,比较经典的就是 LAN8720A 方案和 W5500 方案,还有就是 ESP8266 WiFi 方案,至于这三种方案有什么不同,后续会进行分析。

由于 STM32H757 内置有 ETH MAC 以太网外设,我们可以通过配置 STM32 的 ETH 外设和 LwIP 中间件代码来实现简单的 HTTP 服务。

2. 不同联网方案

对比维度 LAN8720A 方案 W5500 方案 ESP8266 方案
联网方式 有线以太网 有线以太网 Wi-Fi (2.4GHz)
芯片角色 纯 PHY(物理层收发器) MAC + PHY + 硬件 TCP/IP 协议栈 Wi-Fi SoC(含完整协议栈)
与 STM32 接口 RMII(需 50MHz 参考时钟)+ SMI 管理接口 SPI(最高 80MHz) UART(典型 115200bps,可更高)
协议栈位置 STM32 内部运行(如 LwIP) 硬件芯片内部实现 模块内部实现
对 STM32 的要求 必须自带以太网 MAC(如 STM32F4/F7/H7 系列) 无特殊要求,仅需 SPI 及普通 GPIO 无特殊要求,仅需 UART
理论速率 10/100 Mbps(可接近线速) 10/100 Mbps(受 SPI 瓶颈限制) 802.11 b/g/n,最高 72.2 Mbps(理论)
实测吞吐量 高(百兆线速,取决于 MCU 性能) 中(STM32F103 + 36MHz SPI 下约 3.6 Mbps) 低-中(受串口及 AT 指令交互限制,透传模式可达数十 Mbps 级)
硬件成本(芯片) 约 ¥3~10(纯 PHY 价格低) 约 ¥15~30(集成度高,单价较高) 约 ¥5~15(模块整体成本极低)
软件复杂度 中-高(需移植 LwIP、配置 MAC/DMA、PHY 驱动) 低(硬件协议栈,SPI 读写 Socket 即可) 最低(AT 指令解析,无需移植协议栈)
稳定性/抗干扰 高(有线连接,工业级稳定) 高(有线连接,延迟低) 中(受 Wi-Fi 环境、信号强度、干扰影响)
灵活性 高(可自由使用 LwIP 的全部协议功能) 中(硬件协议栈功能固定,仅支持 TCP/UDP 等基础 Socket) 高(Wi-Fi 接入,便于移动或难以布线的场景)
典型应用场景 工业网关、高速数据采集、需要完整协议栈定制的场景 快速实现以太网连接的中小型项目、Modbus 网关、工控数据采集 智能家居、远程传感节点、移动设备、原型验证
核心优点 速度快、BOM 成本低、协议灵活 开发简单、不占用 MCU 网络处理资源、连线少 无线连接、成本极低、资料丰富、接线简单
核心缺点 要求 MCU 自带 MAC、软件配置复杂、引脚较多 SPI 速率成瓶颈、吞吐量受限、协议灵活性差 稳定性受无线环境影响、AT 指令交互增加软件复杂度、不适合高实时性场景

3. 以太网简述

3.1 以太网硬件分层

  1. MAC: 负责以太网帧的发送、接收、地址过滤、CRC 等。很多 MCU 片内自带 ETH MAC。
  2. PHY: 负责物理层收发。LAN8720 属于这一层。
  3. 变压器 + RJ45: 负责电气隔离和线缆接口。可以外挂,也可以做到网口座子里。

MAC 和 PHY 之间需要标准数字接口,常见的是 MII 和 RMII。LAN8720A 使用的是 RMII。

3.2 MII

MII 全称 Media Independent Interface,是 IEEE 802.3 定义的 MAC 与 PHY 接口。
“媒体无关”的含义是:MAC 不关心对面是铜缆还是光纤,只按统一数字脚收发数据;介质相关工作交给PHY。

3.2.1 据宽度和时钟

  • 发送数据:TXD[3:0],4 bit
  • 接收数据:RXD[3:0],4 bit
  • 100 Mbps:时钟 25 MHz
    4 bit × 25 MHz = 100 Mbps
  • 10 Mbps:时钟 2.5 MHz
    4 bit × 2.5 MHz = 10 Mbps

MII 的发送和接收使用两套独立时钟:

  • TX_CLK
  • RX_CLK

3.2.2 主要信号

信号 方向(相对 MAC) 作用
TX_CLK PHY → MAC 发送时钟
TXD[3:0] MAC → PHY 发送数据
TX_EN MAC → PHY 发送使能
TX_ER MAC → PHY 发送错误,可选
RX_CLK PHY → MAC 接收时钟
RXD[3:0] PHY → MAC 接收数据
RX_DV PHY → MAC 接收数据有效
RX_ER PHY → MAC 接收错误
CRS PHY → MAC 载波侦测
COL PHY → MAC 冲突检测

再加上管理总线:

信号 作用
MDC 管理时钟
MDIO 管理数据,双向

MII 功能完善,但是引脚较多,布线成本较高,对于引脚紧张的 MCU 不友好。

3.3 RMII

RMII 全称 Reduced Media Independent Interface,是 MII 的精简版本,目标是减少引脚数量,同时仍支持 10/100 Mbps.

3.3.1 精简方式

RMII 相对 MII 做了这些调整:

  • 数据位宽从 4 bit 减到 2 bit
  • 100M 时钟从 25 MHz 提高到 50 MHz
  • 收发共用同一个 REF_CLK
  • 将 CRS 和 RX_DV 合并为 CRS_DV
  • 去掉独立的 COL
  • RX_ER 变为可选

100 Mbps 时仍然满足带宽:

2 bit × 50 MHz = 100 Mbps

10 Mbps 时,RMII 通常仍使用 50 MHz 参考时钟,由内部逻辑完成速率适配,不像 MII 那样把时钟降到2.5 MHz。

3.3.2 主要信号

信号 方向(相对 MAC) 作用
REF_CLK 共同参考时钟 50 MHz,收发都参考它
TXD0 / TXD1 MAC → PHY 发送数据
TX_EN MAC → PHY 发送使能
RXD0 / RXD1 PHY → MAC 接收数据
CRS_DV PHY → MAC 载波侦测与数据有效
RX_ER PHY → MAC 接收错误,可选

管理口保持不变:

信号 作用
MDC 管理时钟
MDIO 管理数据

3.4 MII 与 RMII 对比

项目 MII RMII
支持速率 10/100 Mbps 10/100 Mbps
数据位宽 4 bit 2 bit
100M 时钟 25 MHz 50 MHz
时钟结构 TX_CLK + RX_CLK 单一 REF_CLK
数据有效 RX_DV 并入 CRS_DV
载波侦测 CRS 并入 CRS_DV
冲突检测 COL 无独立脚
引脚数量 较多 较少
LAN8720A 不支持完整 MII 仅支持 RMII

3.5 MDIO 管理接口

RMII 负责数据传输,MDIO 负责配置和状态查询。

MDIO 也称为 SMI,只有两根线:

  • MDC:时钟,由 MAC 侧输出
  • MDIO:双向数据线,通常需要上拉

一条 MDIO 总线最多可挂 32 个 PHY,通过 5 bit 地址区分。常见操作包括:

  • 读 PHY ID,确认芯片是否在线
  • 软复位
  • 使能自协商
  • 查询链路是否建立
  • 读取当前速率和双工模式

STM32 一类 MCU 通常提供封装接口,例如:

1  HAL_ETH_ReadPHYRegister(...);
2  HAL_ETH_WritePHYRegister(...);

4. CubeMX 配置

4.1 ETH 配置

基本都是默认配置。

image

  • Ethernet MAC Address: 本机以太网 MAC 地址
  • Tx Descriptor Length: 发送描述符个数
  • First Tx Descriptor Address: 发送描述符起始地址
  • Rx Descriptor Length: 接收描述符个数
  • First Rx Descriptor Address: 接收描述符起始地址
  • Rx Buffers Address: 接收缓冲区起始地址
  • Rx Buffers Length: 每个接收缓冲区的长度(字节)

配置 ETH 全局中断:
image

4.2 LwIP 配置

除了 DHCP 不开,使用自定义的 IP 网段以外,其他基本都是默认配置。

image

  • LWIP Version: 当前集成的 LwIP 协议栈版本号(一般只读)。
  • LWIP_DHCP: 是否启用 DHCP 客户端,自动向网络申请 IP。
  • WITH_RTOS: 是否按带 RTOS 方式运行(tcpip 线程 + 邮箱/信号量等)。
  • CMSIS_VERSION: 对接 CMSIS-RTOS 的 API 版本(需与工程 FreeRTOS 封装一致)。
  • RTOS_USE_NEWLIB_REENTRANT: 多任务下是否启用 Newlib 可重入,保证 C 库线程安全。
  • PHY Driver: 选择以太网 PHY 芯片驱动,用于链路检测与寄存器访问。
  • LWIP_ICMP: 是否启用 ICMP(含 ping 应答等)。
  • LWIP_IGMP: 是否启用 IGMP(IPv4 组播成员管理)。
  • LWIP_DNS: 是否启用 DNS 客户端(域名解析)。
  • LWIP_UDP: 是否启用 UDP 协议。
  • MEMP_NUM_UDP_PCB: 最多同时存在的 UDP 控制块(端点)数量。
  • LWIP_TCP: 是否启用 TCP 协议。
  • MEMP_NUM_TCP_PCB: 最多同时存在的 TCP 连接(控制块)数量。

image

  • LWIP_TCP_SACK_OUT: 是否启用 TCP 选择性确认(SACK)发送能力。
  • TCP_MSS: TCP 最大报文段长度(单段有效载荷上限)。
  • TCP_SND_BUF: TCP 发送缓冲区总大小。
  • TCP_SND_QUEUELEN: 发送队列最多可挂的 pbuf/段数量。
  • LWIP_NETIF_STATUS_CALLBACK: 网卡 up/down 等状态变化时是否回调应用。
  • LWIP_NETIF_EXT_STATUS_CALLBACK: 是否启用扩展状态回调(更细的 netif 事件)。
  • LWIP_NETIF_LINK_CALLBACK: 链路 link up/down 变化时是否回调应用。
  • LWIP_NETIF_LOOPBACK: 是否启用本机回环接口(127.0.0.1 类通信)。
  • TCPIP_THREAD_NAME: tcpip 核心线程的名字。
  • TCPIP_THREAD_STACKSIZE: tcpip 线程栈大小。
  • TCPIP_THREAD_PRIO: tcpip 线程优先级。
  • TCPIP_MBOX_SIZE: 投递给 tcpip 线程的邮箱/消息队列深度。
  • DEFAULT_THREAD_NAME: LwIP 默认应用线程名字(部分移植/示例用)。
  • DEFAULT_THREAD_STACKSIZE: 默认应用线程栈大小。
  • DEFAULT_THREAD_PRIO: 默认应用线程优先级。
  • DEFAULT_RAW_RECVMBOX_SIZE: Raw/Netconn 接收邮箱默认深度。
  • DEFAULT_TCP_RECVMBOX_SIZE: TCP Netconn 接收邮箱默认深度。
  • DEFAULT_ACCEPTMBOX_SIZE: TCP 监听 accept 等待队列邮箱深度。
  • LWIP_NETCONN: 是否启用 Netconn 顺序 API(需 RTOS)。
  • Socket Options: 是否启用类 BSD Socket API(一般建在 Netconn 之上)。
  • LWIP_SOCKET: 是否启用类 BSD Socket API(需 RTOS,通常基于 Netconn)。
  • LWIP_COMPAT_SOCKETS: Socket 命名兼容级别(如是否提供不带 lwip_ 前缀的标准函数名)。
  • LWIP_SOCKET_OFFSET: Socket 文件描述符起始偏移(避免与其它 fd 编号冲突)。
  • LWIP_SOCKET_SELECT: 是否支持 select() 多路 IO 等待。
  • LWIP_SOCKET_POLL: 是否支持 poll() 多路 IO 等待。

image
LAN8720A 这里使用自己的驱动文件,就不用 CubeMX 提供的了,这里选择 "Undefined" 即可。

4.3 ETH/LwIP 内存区与 LwIP Heap 区域冲突

4.3 问题分析

ETH/LwIP 三个内存区放在 D2 SRAM0x30000000
LwIP 的 Heap 堆区起始地址放在0x30004000

ETH/LwIP 三个内存区在 ethernetif.c 中定义出来:

DMARxDscrTab[]      // RX DMA 描述符
DMATxDscrTab[]      // TX DMA 描述符
memp_memory_RX_POOL_base[]  // LwIP RX 缓冲池

它们分别放在:

.RxDescripSection
.TxDescripSection
.Rx_PoolSection

于是就可能变成:

         ETH RX_POOL
             LwIP heap
    ├────────────┤───┤
0x30000000       ↑   0x30005000
            0x30004000 ~ ...
             发生重叠

这种重叠很危险,因为两个模块会认为自己独占同一块内存。

可能出现:

  • 收到网络包后数据被覆盖。
  • HTTP 请求内容随机变化。
  • netconn_recv() 异常。
  • LwIP 链表或内存池被破坏。
  • TCP 连接偶尔断开。
  • ETH DMA 工作一段时间后崩溃。
  • HardFault。
  • HTTP 看起来能启动,但连续访问不稳定。

4.4 解决方案

  1. 加大 LwIP heap 腾出给 ETH/LwIP 内存区的空间
    在 lwipopts.h 中宏定义
/* RX 池占用前 20 KiB,LwIP heap 从其后开始,重新生成时保留布局。 */
#undef LWIP_RAM_HEAP_POINTER
#define LWIP_RAM_HEAP_POINTER 0x30005000
  1. 并在 ld 链接脚本中加上限制 ETH/LwIP 内存区的空间:
SECTIONS
{
  /* ETH DMA 使用的是不可缓存 D2 SRAM,预留 0x30000000 ~ 0x30004FFF (20KB) 区域  */
  .eth_dma 0x30000000 (NOLOAD) :
  {
    . = ALIGN(32);
    *(.RxDescripSection)
    . = ALIGN(32);
    *(.TxDescripSection)
    . = ALIGN(32);
    *(.Rx_PoolSection)
    . = ALIGN(32);
  } >RAM_D2
  ASSERT(SIZEOF(.eth_dma) <= 0x5000, "ETH DMA overlaps LwIP heap")
}

把 ETH/LwIP 使用的三个内存区域集中放到 D2 SRAM 的 0x30000000 起始位置,并限制总大小不能超过 0x5000 字节,并且加上报错信息,避免它们向后增长到 LwIP heap 所在区域。

5. 代码生成

特别注意: 由于 CubeMX 这里生成有 bug,当配置了 LwIP 后,生成的代码会将 eth.c/h 给删除掉,所以我们必须先提前备份 eth.c/h,在生成好代码后,将 eth.c/h 重新手动粘贴回 Core/Src、Core/Inc 当中。

5.1 eth.c

/**
 * @file eth.c
 * @brief CubeMX ETH 初始化(Generate 保留本文件)。
 *        heth / DMA 描述符由 ethernetif.c(Cube LWIP)持有,此处仅 early init + MSP。
 */
#include "eth.h"

extern ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT] __attribute__((section(".RxDescripSection"))); /* Ethernet Rx DMA Descriptors */
extern ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT] __attribute__((section(".TxDescripSection"))); /* Ethernet Tx DMA Descriptors */
extern ETH_BufferTypeDef Txbuffer[ETH_TX_DESC_CNT * 2U];
extern ETH_HandleTypeDef heth;
extern ETH_TxPacketConfig TxConfig;

/* ETH init function */
void MX_ETH_Init(void)
{
    static uint8_t MACAddr[6];

    heth.Instance = ETH;
    MACAddr[0] = 0x00;
    MACAddr[1] = 0x80;
    MACAddr[2] = 0xE1;
    MACAddr[3] = 0x00;
    MACAddr[4] = 0x00;
    MACAddr[5] = 0x00;
    heth.Init.MACAddr = &MACAddr[0];
    heth.Init.MediaInterface = HAL_ETH_RMII_MODE;
    heth.Init.TxDesc = DMATxDscrTab;
    heth.Init.RxDesc = DMARxDscrTab;
    heth.Init.RxBuffLen = 1536;

    if (HAL_ETH_Init(&heth) != HAL_OK)
    {
        Error_Handler();
    }

    memset(&TxConfig, 0, sizeof(ETH_TxPacketConfig));
    TxConfig.Attributes = ETH_TX_PACKETS_FEATURES_CSUM | ETH_TX_PACKETS_FEATURES_CRCPAD;
    TxConfig.ChecksumCtrl = ETH_CHECKSUM_IPHDR_PAYLOAD_INSERT_PHDR_CALC;
    TxConfig.CRCPadCtrl = ETH_CRC_PAD_INSERT;
}

void HAL_ETH_MspInit(ETH_HandleTypeDef *ethHandle)
{

    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (ethHandle->Instance == ETH)
    {
        /* ETH clock enable */
        __HAL_RCC_ETH1MAC_CLK_ENABLE();
        __HAL_RCC_ETH1TX_CLK_ENABLE();
        __HAL_RCC_ETH1RX_CLK_ENABLE();

        __HAL_RCC_GPIOG_CLK_ENABLE();
        __HAL_RCC_GPIOC_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();
        /**ETH GPIO Configuration
        PG11     ------> ETH_TX_EN
        PG12     ------> ETH_TXD1
        PG13     ------> ETH_TXD0
        PC1     ------> ETH_MDC
        PA2     ------> ETH_MDIO
        PA1     ------> ETH_REF_CLK
        PA7     ------> ETH_CRS_DV
        PC4     ------> ETH_RXD0
        PC5     ------> ETH_RXD1
        */
        GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
        GPIO_InitStruct.Pull = GPIO_NOPULL;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
        GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
        HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);

        GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
        GPIO_InitStruct.Pull = GPIO_NOPULL;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
        GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
        HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

        GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_1 | GPIO_PIN_7;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
        GPIO_InitStruct.Pull = GPIO_NOPULL;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
        GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

        /* ETH interrupt Init */
        HAL_NVIC_SetPriority(ETH_IRQn, 5, 0);
        HAL_NVIC_EnableIRQ(ETH_IRQn);
    }
}

void HAL_ETH_MspDeInit(ETH_HandleTypeDef *ethHandle)
{

    if (ethHandle->Instance == ETH)
    {
        /* Peripheral clock disable */
        __HAL_RCC_ETH1MAC_CLK_DISABLE();
        __HAL_RCC_ETH1TX_CLK_DISABLE();
        __HAL_RCC_ETH1RX_CLK_DISABLE();

        /**ETH GPIO Configuration
        PG11     ------> ETH_TX_EN
        PG12     ------> ETH_TXD1
        PG13     ------> ETH_TXD0
        PC1     ------> ETH_MDC
        PA2     ------> ETH_MDIO
        PA1     ------> ETH_REF_CLK
        PA7     ------> ETH_CRS_DV
        PC4     ------> ETH_RXD0
        PC5     ------> ETH_RXD1
        */
        HAL_GPIO_DeInit(GPIOG, GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13);

        HAL_GPIO_DeInit(GPIOC, GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5);

        HAL_GPIO_DeInit(GPIOA, GPIO_PIN_2 | GPIO_PIN_1 | GPIO_PIN_7);

        HAL_NVIC_DisableIRQ(ETH_IRQn);
    }
}

5.2 eth.h

#ifndef __ETH_H__
#define __ETH_H__

#ifdef __cplusplus
extern "C"
{
#endif

#include "main.h"
#include "string.h"

  void MX_ETH_Init(void);

#ifdef __cplusplus
}
#endif

#endif /* __ETH_H__ */

6. 实现 /status 命令

目标定为:CM7 启动 LwIP 后监听 TCP 80 端口,电脑访问 GET /status 能返回当前 IP、链路和双核槽位信息。

6.1 确认电脑和开发板在同一网段

CubeMX 开发板 IP 配置:

IP 地址:192.168.10.2
子网掩码:255.255.255.0
网关:0.0.0.0

image

由于子网掩码为 24(255.255.255.0),所以 IP 地址为 192.168.10.x 的都属于同一个子网 IP。

PC 端以太网 IP 配置:

IP 地址:192.168.10.1
子网掩码:255.255.255.0
网关:0.0.0.0

image

网关(Gateway)通常就是路由器的IP地址。它的唯一作用是帮助设备将数据包发送到本地网段之外(比如访问互联网,或者访问其他网段的设备)。只要设备不访问外网,就不需要网关。

由于目前是电脑直连开发板,所以网关可以留空。

6.2 相关文件

HTTP 应用业务层:
监听 80 端口、解析 GET /status、生成 JSON 和 HTTP 响应。
image

ETH 以太网板级支持层:
根据本板连接方式访问 LAN8720,扫描 PHY 地址、查询网线状态和打印诊断。
image

LwIP 网络协议栈接入层:
初始化网络接口,并把 TCP/IP 协议栈与 STM32 ETH 硬件连接起来。

  • LWIP/App:网络初始化入口。lwip.c 负责启动协议栈、设置 IP、注册网卡和创建链路检测线程。
  • LWIP/Target:面向当前硬件平台的适配。ethernetif.c 负责网卡收发,lan8720.c 负责 PHY 寄存器逻辑,lwipopts.h 负责协议栈配置。

image

层次 相关文件 主要职责
应用层 app_http_server.c/.h 理解 HTTP 请求,处理 /status,生成响应
协议栈与硬件适配层 ethernetif.c/.h, lwipopts.h 连接 LwIP 和 ETH 驱动,配置协议栈
STM32 ETH 外设层 eth.c/.h 配置 MCU 内部的 MAC、DMA、引脚、时钟和中断
PHY 驱动层 eth_phy.c/.h, lan8720.c/.h 初始化外部 LAN8720,查询网线连接、速率和双工状态

6.2.1 eth_phy.c 与 lan8720.c 的区别

eth_phy.c:把本板硬件连接到 LAN8720 驱动,是 PHY 的板级封装和总线适配层。
lan8720.c:LAN8720 芯片寄存器逻辑,负责理解 LAN8720 芯片的寄存器含义。

与 eth_phy.c 的区别是:
eth_phy.c:知道怎样在本板上读写寄存器
lan8720.c:知道寄存器中的各个位代表什么

6.3 实现 app_http_server.c/h

6.3.1 HTTP 整体顺序

main()
  ↓
MX_ETH_Init()                    配置 ETH 时钟、引脚、MAC 和 DMA
  ↓
eth_phy_init()                   扫描并初始化 LAN8720
  ↓
启动 FreeRTOS
  ↓
StartDefaultTask()
  ↓
MX_LWIP_Init()
  ├→ tcpip_init()                启动 TCP/IP 协议栈线程
  ├→ netif_add()
  │    ↓
  │  ethernetif_init()           将 LwIP 与 ETH 驱动连接
  │    ↓
  │  low_level_init()            初始化 DMA 缓冲、信号量和接收线程
  └→ ethernet_link_thread()      检测网线,配置速率并启动 ETH 收发
  ↓
App_HTTP_Server_Thread_Start()
  ↓
Http_Thread()
  ↓
netconn_new() → netconn_bind(80) → netconn_listen()
  ↓
netconn_accept()                 等待电脑连接
  ↓
电脑发送 GET /status
  ↓
LAN8720 → ETH MAC/DMA → ethernetif_input() → LwIP TCP
  ↓
netconn_recv()                   HTTP 任务读取请求
  ↓
http_get_request_path()          提取 /status
  ↓
http_handle_status()             获取状态并生成 JSON
  ↓
http_reply_text()                生成 HTTP 响应头和响应体
  ↓
netconn_write()
  ↓
LwIP TCP/IP → low_level_output() → HAL_ETH_Transmit_IT()
  ↓
ETH MAC/DMA → LAN8720 → 电脑收到 JSON
  ↓
netconn_close() → netconn_delete()
  ↓
回到 netconn_accept(),等待下一次连接

6.3.2 HTTP 任务流程

                    ┌──────────────────────────────┐
                    │  创建 netconn (TCP)           │
                    └──────────────┬───────────────┘
                                   ▼
                    ┌──────────────────────────────┐
                    │  bind 到 IP_ADDR_ANY:端口     │
                    └──────────────┬───────────────┘
                                   ▼
                    ┌──────────────────────────────┐
                    │  listen 进入监听状态           │
                    └──────────────┬───────────────┘
                                   ▼
             ┌────────────► app_http_ota_poll() 轮询 OTA
             │                     ▼
             │            accept 等客户端连接
             │                     │ 成功
             │                     ▼
             │           http_handle_client() 处理请求
             │                     ▼
             │           close + delete 释放连接
             └─────────────────────┘

6.3.3 app_http_server.c

/**
 * @file    app_http_server.c
 * @brief   创建 HTTP 任务、监听端口、接收连接、路由分发
 */
#include "app_http_server.h"
#include "lwip/api.h"
#include "lwip/ip4_addr.h"
#include "lwip/netif.h"
#include "lwip.h"
#include "boot_slot.h"
#include "FreeRTOS.h"
#include "task.h"
#include "mylog.h"
#include <stdio.h>
#include <string.h>
#include <stdint.h>

#define APP_HTTP_PORT 80U              // HTTP 服务器端口
#define APP_HTTP_REQUEST_SIZE 1024U    // HTTP 请求缓冲区大小
#define APP_HTTP_RECV_TIMEOUT_MS 5000U // HTTP 接收超时时间

extern struct netif gnetif; // 网络接口结构体

static char s_request[APP_HTTP_REQUEST_SIZE]; // HTTP 请求缓冲区

static void http_handle_client(struct netconn *conn);
static void http_handle_status(struct netconn *conn);
static int http_get_request_path(const char *request, char *path, size_t path_size);
static int http_read_request(struct netconn *conn, char *buffer, size_t buffer_size);
static void http_reply_text(struct netconn *conn, int status_code, const char *status_text,
                            const char *content_type, const char *body);
static int http_send_all(struct netconn *conn, const void *data, size_t length);

/**
 * @brief   HTTP 服务器任务
 * @param   none
 * @return  none
 */
static void Http_Thread(void *argument)
{
    struct netconn *listen_connection; // 监听连接
    struct netconn *client_connection; // 客户端连接
    err_t error;                       // 错误码

    (void)argument; // 忽略参数

    osDelay(1000);

    /* 创建监听连接 */
    listen_connection = netconn_new(NETCONN_TCP);
    if (listen_connection == NULL)
    {
        LOG_ERROR("CM7", "HTTP: netconn_new failed");
        return;
    }

    /* 绑定端口 */
    error = netconn_bind(listen_connection, IP_ADDR_ANY, APP_HTTP_PORT);
    if (error != ERR_OK)
    {
        LOG_ERROR("CM7", "HTTP: bind port %u failed, error=%d",
                  (unsigned int)APP_HTTP_PORT, (int)error);
        netconn_delete(listen_connection); // 删除监听连接
        return;
    }

    /* 监听连接 */
    error = netconn_listen(listen_connection);
    if (error != ERR_OK)
    {
        LOG_ERROR("CM7", "HTTP: listen port %u failed, error=%d",
                  (unsigned int)APP_HTTP_PORT, (int)error);
        netconn_delete(listen_connection); // 删除监听连接
        return;
    }

    LOG_INFO("CM7", "HTTP: server listening on port %u",
             (unsigned int)APP_HTTP_PORT);

    for (;;)
    {
        client_connection = NULL;
        /* 接受连接 */
        error = netconn_accept(listen_connection, &client_connection);
        if (error != ERR_OK)
        {
            osDelay(100U);
            continue;
        }
        http_handle_client(client_connection); // 处理客户端请求
        netconn_close(client_connection);      // 关闭客户端连接
        netconn_delete(client_connection);     // 删除客户端连接
    }
}

/**
 * @brief   启动 HTTP 服务器
 * @param   none
 * @return  none
 */
void App_HTTP_Server_Thread_Start(void)
{
    BaseType_t xReturn = pdPASS;
    xReturn = xTaskCreate((TaskFunction_t)Http_Thread,
                          (char *)"Http_Thread",
                          (uint16_t)1024,
                          (void *)NULL,
                          (uint8_t)5,
                          (TaskHandle_t *)NULL);
    if (xReturn != pdPASS)
    {
        LOG_ERROR("CM7", "HTTP: Http_Thread creation failed");
        return;
    }
}

/**
 * @brief   处理客户端请求
 * @param   conn  客户端连接
 * @return  none
 */
static void http_handle_client(struct netconn *conn)
{
    char path[64];

    /* 读取 HTTP 请求 */
    if (http_read_request(conn, s_request, sizeof(s_request)) != 0)
    {
        /* 读取请求失败,回复 400 错误码 */
        http_reply_text(conn,
                        400,
                        "Bad Request",
                        "text/plain; charset=utf-8",
                        "bad request\r\n");
        return;
    }

    /* 提取请求路径 */
    if (http_get_request_path(s_request, path, sizeof(path)) != 0)
    {
        /* 提取路径失败,回复 405 错误码 */
        http_reply_text(conn,
                        405,
                        "Method Not Allowed",
                        "text/plain; charset=utf-8",
                        "only GET is supported\r\n");
        return;
    }

    /* 处理 /status 请求 */
    if (strcmp(path, "/status") == 0)
    {
        /* 处理 /status 请求 */
        http_handle_status(conn);
        return;
    }

    http_reply_text(conn,
                    404,
                    "Not Found",
                    "text/plain; charset=utf-8",
                    "not found\r\n");
}

/**
 * @brief   处理 /status 请求
 * @param   conn  客户端连接
 * @return  none
 */
static void http_handle_status(struct netconn *conn)
{
    char body[256];
    char ip_address[IP4ADDR_STRLEN_MAX];
    uint8_t running_slot;
    int body_length;

    /* 获取 IP 地址 */
    ip4addr_ntoa_r(netif_ip4_addr(&gnetif), ip_address, sizeof(ip_address));
    /* 获取运行槽位 */
    running_slot = boot_detect_slot_running();
    /* 构建响应体,包含设备信息、网络状态、IP 地址和 CM7 运行槽位 */
    body_length = snprintf(body,
                           sizeof(body),
                           "{"
                           "\"device\":\"STM32H757\","
                           "\"network\":\"%s\","
                           "\"ip\":\"%s\","
                           "\"cm7_running_slot\":%u"
                           "}\r\n",
                           (netif_is_link_up(&gnetif) &&
                            netif_is_up(&gnetif))
                               ? "up"
                               : "down",
                           ip_address,
                           (unsigned int)running_slot);

    /* 检查响应体长度是否有效 */
    if ((body_length <= 0) || ((size_t)body_length >= sizeof(body)))
    {
        /* 响应体长度无效,回复 500 错误码 */
        http_reply_text(conn,
                        500,
                        "Internal Server Error",
                        "text/plain; charset=utf-8",
                        "status response too large\r\n");
        return;
    }
    /* 响应体长度有效,回复 200 成功码 */
    http_reply_text(conn,
                    200,
                    "OK",
                    "application/json; charset=utf-8",
                    body);
}

/**
 * @brief   获取请求路径
 * @param   request  HTTP 请求字符串
 * @param   path     存储路径的缓冲区
 * @param   path_size  路径缓冲区大小
 * @return  none
 */
static int http_get_request_path(const char *request,
                                 char *path,
                                 size_t path_size)
{
    const char *path_start;
    const char *path_end;
    size_t path_length;

    /* 检查请求是否为 GET 请求 */
    if (strncmp(request, "GET ", 4U) != 0)
    {
        return -1;
    }

    path_start = request + 4U;          // 跳过 "GET "
    path_end = strchr(path_start, ' '); // 查找路径结束位置

    /* 检查路径是否有效 */
    if (path_end == NULL)
    {
        return -1;
    }

    /* 计算路径长度 */
    path_length = (size_t)(path_end - path_start);
    /* 检查路径长度是否有效 */
    if ((path_length == 0U) || (path_length >= path_size))
    {
        return -1;
    }

    /* 复制路径到缓冲区 */
    memcpy(path, path_start, path_length);
    path[path_length] = '\0';

    /* 检查路径是否包含查询参数 */
    path_end = strchr(path, '?');
    if (path_end != NULL)
    {
        path[(size_t)(path_end - path)] = '\0'; // 截断路径,移除查询参数
    }

    return 0;
}

/**
 * @brief   读取客户端请求
 * @param   conn  客户端连接
 * @param   buffer  存储请求的缓冲区
 * @param   buffer_size  缓冲区大小
 * @return  none
 */
static int http_read_request(struct netconn *conn,
                             char *buffer,
                             size_t buffer_size)
{
    struct netbuf *net_buffer = NULL;
    err_t error;
    size_t received = 0U;

    /* 设置接收超时 */
    netconn_set_recvtimeout(conn, APP_HTTP_RECV_TIMEOUT_MS);

    while (received + 1U < buffer_size)
    {
        void *data;
        u16_t data_length;

        /* 接收数据 */
        error = netconn_recv(conn, &net_buffer);
        if (error != ERR_OK)
        {
            return -1;
        }

        do
        {
            data = NULL;
            data_length = 0U;

            /* 处理数据 */
            if (netbuf_data(net_buffer, &data, &data_length) == ERR_OK)
            {
                size_t copy_length = data_length;

                /* 检查是否超出了缓冲区大小 */
                if ((received + copy_length + 1U) > buffer_size)
                {
                    copy_length = buffer_size - received - 1U;
                }

                /* 复制数据到缓冲区 */
                if (copy_length > 0U)
                {
                    memcpy(buffer + received, data, copy_length);
                    received += copy_length;
                    buffer[received] = '\0';
                }
            }

            /* 检查是否接收完整 */
            if (strstr(buffer, "\r\n\r\n") != NULL)
            {
                netbuf_delete(net_buffer); // 释放接收缓冲区
                return 0;
            }
        } while (netbuf_next(net_buffer) >= 0); // 处理下一个数据块

        netbuf_delete(net_buffer); // 释放接收缓冲区
        net_buffer = NULL;         // 重置指针
    }

    return -1;
}

/**
 * @brief   回复文本内容
 * @param   conn  客户端连接
 * @param   status_code  状态码
 * @param   status_text  状态文本
 * @param   content_type  内容类型
 * @param   body  回复内容
 * @return  none
 */
static void http_reply_text(struct netconn *conn,
                            int status_code,
                            const char *status_text,
                            const char *content_type,
                            const char *body)
{
    char header[256];   // HTTP 响应头缓冲区
    int header_length;  // HTTP 响应头长度
    size_t body_length; // 回复内容长度

    body_length = strlen(body); // 计算回复内容长度
    /* 构建 HTTP 响应头,包含状态码、状态文本、内容类型、内容长度、连接类型、缓存控制指令 */
    header_length = snprintf(header,
                             sizeof(header),
                             "HTTP/1.1 %d %s\r\n"
                             "Content-Type: %s\r\n"
                             "Content-Length: %lu\r\n"
                             "Connection: close\r\n"
                             "Cache-Control: no-store\r\n"
                             "\r\n",
                             status_code,
                             status_text,
                             content_type,
                             (unsigned long)body_length);

    /* 检查 HTTP 响应头是否有效 */
    if ((header_length <= 0) || ((size_t)header_length >= sizeof(header)))
    {
        return;
    }

    /* 发送 HTTP 响应头 */
    if (http_send_all(conn, header, (size_t)header_length) != 0)
    {
        return;
    }

    /* 发送回复内容 */
    (void)http_send_all(conn, body, body_length);
}

/**
 * @brief   发送所有数据
 * @param   conn  客户端连接
 * @param   data  要发送的数据
 * @param   length  数据长度
 * @return  none
 */
static int http_send_all(struct netconn *conn, const void *data, size_t length)
{
    /* 发送数据 */
    if (netconn_write(conn, data, length, NETCONN_COPY) != ERR_OK)
    {
        return -1;
    }

    return 0;
}

6.3.4 app_http_server.h

#ifndef __APP_HTTP_SERVER_H__
#define __APP_HTTP_SERVER_H__

void App_HTTP_Server_Thread_Start(void);

#endif

6.3.5 main.c

int main(void)
{
  ......

  MX_ETH_Init();
  {
    LOG_INFO("CM7", "ETH I/O init OK");
    int32_t phy_status = eth_phy_init();
    if (phy_status != LAN8720_STATUS_OK)
    {
      eth_phy_print_diag();
      LOG_ERROR("CM7", "PHY init failed: %s", eth_phy_link_state_str(phy_status));
    }
    else
    {
      eth_phy_print_diag();
      LOG_INFO("CM7", "PHY init OK, link: %s", eth_phy_link_state_str(eth_phy_get_link_state()));
    }
  }
  app_ipc_cm7_init(); // 初始化 IPC 模块
  sdram_init();       // 初始化 SDRAM
  lcd_init();         // 初始化 LCD
  app_ov5640_init();  // 初始化 OV5640 摄像头
  LOG_INFO("CM7", "main() user init OK");

  osKernelInitialize();  /* Call init function for freertos objects (in cmsis_os2.c) */
  MX_FREERTOS_Init();
  osKernelStart();

  while (1)
  {
  }
}

6.3.6 freertos.c

void StartDefaultTask(void *argument)
{
  MX_LWIP_Init();
  App_HTTP_Server_Thread_Start();
  for (;;)
  {
    HAL_GPIO_TogglePin(LED0_GREEN_GPIO_Port, LED0_GREEN_Pin);
    osDelay(1000);
  }
}

6.3 串口打印现象

......
[I][CM7] ETH I/O init OK
[I][CM7] PHY found at addr 1, ID=0x0007/0xC0F1
[I][CM7] Diag: using PHY addr 1, last BCR=0x0000
[I][CM7]   addr 0: ID=0xFFFF/0xFFFF BCR=0xFFFF BSR=0xFFFF
[I][CM7]   addr 1: ID=0x0007/0xC0F1 BCR=0x3100 BSR=0x782D
[I][CM7]   addr 2: ID=0xFFFF/0xFFFF BCR=0xFFFF BSR=0xFFFF
[I][CM7]   addr 3: ID=0xFFFF/0xFFFF BCR=0xFFFF BSR=0xFFFF
[I][CM7] PHY init OK, link: 100M Full
......
[I][CM7] main() user init OK
[I][CM7] FreeRTOS init OK
[I][CM7] NET_START: wait PHY link and IP address
[I][CM7] NET_LINK_UP: 100M Full
[I][CM7] NET_IP: 192.168.10.2
......
[I][CM7] HTTP: server listening on port 80

如果看到:
HTTP: bind port 80 failed
说明 TCP 80 端口绑定失败。

如果看到:
HTTP: netconn_new failed
说明 LwIP 内存池或 TCP 连接资源不足。

如果完全没有:
HTTP: server listening on port 80
说明 HTTP 任务可能没有成功创建或默认任务没有运行到启动 HTTP 的位置。

6.4 测试设备连接情况

6.4.1 Ping 设备

执行ping 192.168.10.2:

PS F:\27559\Projects\STM32-Projects\STM32H7xx\STM32H757XIH6\Camera-Double-CPU\BaseProject> ping 192.168.10.2

正在 Ping 192.168.10.2 具有 32 字节的数据:
来自 192.168.10.2 的回复: 字节=32 时间=13ms TTL=255
来自 192.168.10.2 的回复: 字节=32 时间=1ms TTL=255
来自 192.168.10.2 的回复: 字节=32 时间=1ms TTL=255
来自 192.168.10.2 的回复: 字节=32 时间<1ms TTL=255

192.168.10.2 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 0ms,最长 = 13ms,平均 = 3ms

成功时应看到类似:
来自 192.168.10.2 的回复
如果 Ping 不通,先不要测试 HTTP。依次检查:

  1. 网线是否连接。
  2. PHY 是否打印 NET_LINK_UP。
  3. 电脑 IP 是否处于 192.168.10.x 网段。
  4. Windows 防火墙是否阻止了对应网络。
  5. 开发板是否确实烧录到 CM7 A 槽。
  6. LAN8720 的 PHY 地址和 RMII 时钟是否正确。

6.4.2 使用 PowerShell 请求 /status

执行curl.exe http://192.168.10.2/status:

PS F:\27559\Projects\STM32-Projects\STM32H7xx\STM32H757XIH6\Camera-Double-CPU\BaseProject> curl.exe http://192.168.10.2/status
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}

执行Invoke-WebRequest -Uri http://192.168.10.2/status:

PS F:\27559\Projects\STM32-Projects\STM32H7xx\STM32H757XIH6\Camera-Double-CPU\BaseProject> Invoke-WebRequest -Uri http://192.168.10.2/status


StatusCode        : 200
StatusDescription : OK
Content           : {"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}

RawContent        : HTTP/1.1 200 OK
                    Connection: close
                    Cache-Control: no-store
                    Content-Type: application/json; charset=utf-8
                    Content-Length: 80

                    {"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_…
Headers           : {[Connection, System.String[]], [Cache-Control, System.String[]], [Content-Type, Syst 
                    em.String[]], [Content-Length, System.String[]]}
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 80
RelationLink      : {}

也可以使用浏览器打开:
http://192.168.10.2/status
image

当前只支持:

GET /status

访问下面地址会返回 404:
http://192.168.10.2/
这是当前设计预期,不代表 HTTP 失败。

6.4.3 重复访问测试

执行1..20 | ForEach-Object { curl.exe --silent http://192.168.10.2/status }:

PS F:\27559\Projects\STM32-Projects\STM32H7xx\STM32H757XIH6\Camera-Double-CPU\BaseProject> 1..20 | ForEach-Object {
>>     curl.exe --silent http://192.168.10.2/status
>> }
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}
{"device":"STM32H757","network":"up","ip":"192.168.10.2","cm7_running_slot":0}

观察:

  • 每次都能返回 JSON。
  • 串口没有 HardFault。
  • 串口没有持续打印内存错误。
  • CM4 仍然运行。
  • 摄像头和 LCD 仍然运行。
  • HTTP 任务没有退出。

6.4.4 测试错误路径

执行 curl.exe -i http://192.168.10.2/abc:

PS F:\27559\Projects\STM32-Projects\STM32H7xx\STM32H757XIH6\Camera-Double-CPU\BaseProject> curl.exe -i http://192.168.10.2/abc
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
Content-Length: 11
Connection: close
Cache-Control: no-store

not found

6.4.5 测试 POST

执行 curl.exe -i -X POST http://192.168.10.2/status:

PS F:\27559\Projects\STM32-Projects\STM32H7xx\STM32H757XIH6\Camera-Double-CPU\BaseProject> curl.exe -i -X POST http://192.168.10.2/status
HTTP/1.1 405 Method Not Allowed
Content-Type: text/plain; charset=utf-8
Content-Length: 23
Connection: close
Cache-Control: no-store

only GET is supported

6.4.6 测试重连

网线拔下的时候打印:

[I][CM7] NET_LINK_DOWN: DOWN

网线重新接回的时候打印:

[I][CM7] NET_LINK_UP: 100M Full

6.5 验收标准

当下面条件全部满足时,/status 才算完成:

  • 串口出现 HTTP: server listening on port 80。
  • 电脑可以 Ping 通 192.168.10.2。
  • curl.exe http://192.168.10.2/status 返回 JSON。
  • 连续访问 20 次没有崩溃。
  • 访问不存在路径返回 404。
  • 使用 POST 访问 /status 返回 405。
  • 客户端断开后 HTTP 线程仍可继续接受新连接。
  • 网线拔下后重新插入,链路状态可以恢复。
  • CM4、OpenAMP、摄像头和 LCD 仍能正常运行。
  • CM7 A 槽构建通过。

博客导航

博客导航

posted @ 2026-10-13 08:29  膝盖中箭卫兵  阅读(38)  评论(0)    收藏  举报
ORCID iD icon https://orcid.org/0000-0001-5102-772X