【以太网摄像头采集存储终端】驱动 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 以太网硬件分层
- MAC: 负责以太网帧的发送、接收、地址过滤、CRC 等。很多 MCU 片内自带 ETH MAC。
- PHY: 负责物理层收发。LAN8720 属于这一层。
- 变压器 + 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_CLKRX_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 配置
基本都是默认配置。

- 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 全局中断:

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

- 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 连接(控制块)数量。

- 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 等待。

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 解决方案
- 加大 LwIP heap 腾出给 ETH/LwIP 内存区的空间
在 lwipopts.h 中宏定义
/* RX 池占用前 20 KiB,LwIP heap 从其后开始,重新生成时保留布局。 */
#undef LWIP_RAM_HEAP_POINTER
#define LWIP_RAM_HEAP_POINTER 0x30005000
- 并在 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

由于子网掩码为 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

网关(Gateway)通常就是路由器的IP地址。它的唯一作用是帮助设备将数据包发送到本地网段之外(比如访问互联网,或者访问其他网段的设备)。只要设备不访问外网,就不需要网关。
由于目前是电脑直连开发板,所以网关可以留空。
6.2 相关文件
HTTP 应用业务层:
监听 80 端口、解析 GET /status、生成 JSON 和 HTTP 响应。

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

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

| 层次 | 相关文件 | 主要职责 |
|---|---|---|
| 应用层 | 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。依次检查:
- 网线是否连接。
- PHY 是否打印 NET_LINK_UP。
- 电脑 IP 是否处于 192.168.10.x 网段。
- Windows 防火墙是否阻止了对应网络。
- 开发板是否确实烧录到 CM7 A 槽。
- 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

当前只支持:
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 槽构建通过。
博客导航
本文来自博客园,作者:膝盖中箭卫兵,转载请注明原文链接:https://www.cnblogs.com/hai-feng-dev/p/23276877

浙公网安备 33010602011771号
https://orcid.org/0000-0001-5102-772X