STM32H743 + CMSIS-RTOS2 + Mongoose 网络协议栈移植教程
一、概述
本文档详细介绍如何在 STM32H743 平台上移植 Mongoose 自带 TCP/IP 协议栈,配合 CMSIS-RTOS2(基于 FreeRTOS)实现网络通信功能。
硬件平台 :STM32H743 + LAN8720 PHY(RMII 接口)
软件栈 :STM32 HAL + CMSIS-RTOS V2 + Mongoose
功能特性:
- 支持 DHCP 和静态 IP 两种模式
- 网线拔插检测(PHY 链路状态轮询)
- 运行时配置网络参数(保存至 Flash)
- 网络状态事件回调
二、目录结构
project_root/
├── app/ # 应用代码
│ ├── mongoose_net.c # Mongoose 网络管理模块(核心)
│ ├── mongoose_net.h # 网络接口头文件
│ ├── main.c # 主入口
│ ├── main.h # 中断优先级定义
│ ├── FreeRTOSConfig.h # FreeRTOS 配置
│ ├── app_param.h # 网络参数结构体定义
│ ├── stm32h7xx_hal_msp.c # HAL MSP 初始化
│ └── bsp_system.c # 系统初始化(MPU配置等)
├── mongoose/ # Mongoose 库
│ ├── mongoose.c # Mongoose 主源码
│ └── mongoose_config.h # Mongoose 配置文件
├── libraries/ # HAL/CMSIS 库
│ ├── STM32H7xx_HAL_Driver/
│ └── CMSIS/
├── freertos/ # FreeRTOS 源码
│ └── Source/
│ └── CMSIS_RTOS_V2/ # CMSIS-RTOS2 适配层
└── project/
└── stm32_h7.sct # 链接脚本
三、移植步骤
3.1 准备基础工程
-
使用 STM32CubeMX 生成基础工程:
- 选择 STM32H743 系列芯片
- 启用 ETH(RMII 模式)
- 启用 FreeRTOS(CMSIS-RTOS V2 接口)
- 配置时钟(系统时钟 400MHz,PLLN=100, PLLM=1, PLLP=2)
-
添加必要的 HAL 驱动文件到工程:
stm32h7xx_hal_eth.cstm32h7xx_hal_rcc.cstm32h7xx_hal_gpio.c
3.2 添加 Mongoose 库
-
下载 Mongoose 源码,将
mongoose.c和mongoose.h放入mongoose/目录 -
创建
mongoose_config.h配置文件,关键配置如下:
c
#define MG_ARCH MG_ARCH_CMSIS_RTOS2
#define MG_ENABLE_DRIVER_STM32H 1
#define MG_ENABLE_TCPIP 1
#define MG_ENABLE_TCPIP_DRIVER_INIT 0
#define MG_IO_SIZE 2048
#define MG_ENABLE_FREERTOS 1
#define NET_DRIVER_MDC_CR 4
#define NET_PHY_ADDR 0
#define __sync_synchronize() __dsb(0xf)
#define MG_ETH_RAM __attribute__((section(".RAM_D1")))
#define MGUID ((uint32_t *) 0x1ff1e800)
#define MG_SET_MAC_ADDRESS(mac) \
do { \
mac[0] = 2; \
mac[1] = MGUID[0] & 255; \
mac[2] = (MGUID[0] >> 10) & 255; \
mac[3] = (MGUID[0] >> 19) & 255; \
mac[4] = MGUID[1] & 255; \
mac[5] = MGUID[2] & 255; \
} while (0)
配置说明:
| 宏定义 | 说明 |
|---|---|
MG_ARCH_CMSIS_RTOS2 |
使用 CMSIS-RTOS2 架构适配 |
MG_ENABLE_DRIVER_STM32H |
启用 STM32H 内置以太网驱动 |
MG_ENABLE_TCPIP_DRIVER_INIT 0 |
关键 :禁止 mg_mgr_init() 自动初始化驱动,手动调用 mg_tcpip_init() |
NET_DRIVER_MDC_CR |
MDC 时钟分频(根据 PHY 调整,LAN8720 用 4) |
NET_PHY_ADDR |
PHY 地址(LAN8720 默认 0) |
MG_ETH_RAM |
DMA 描述符放入非缓存 RAM(.RAM_D1 段) |
__sync_synchronize() |
ARMCC/Keil 编译器内存屏障,确保 DMA 操作的缓存一致性;GCC 编译器可替换为 __sync_synchronize() 或 __dsb(0xf) |
MG_PHY_CLOCKS_MAC |
PHY 时钟源配置,LAN8720 使用 MAC 侧提供时钟;使用 DP83848 等需外部时钟的 PHY 时改为 MG_PHY_CLOCKS_PHY |
3.3 配置链接脚本
在 .sct 文件中添加 .RAM_D1 段,必须放在非缓存 RAM 区域:
plaintext
LR_IROM1 0x08000000 0x00200000 {
ER_IROM1 0x08000000 0x00200000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00020000 { ; DTCM 128KB
.ANY (+RW +ZI)
}
RW_IRAM2 0x24000000 0x00080000 { ; AXI SRAM 512KB
.ANY (+RW +ZI)
}
RW_IRAM3 0x30000000 0x00040000 { ; SRAM1+SRAM2 256KB
.ANY (+RW +ZI)
}
RW_IRAM4 0x30040000 0x00008000 { ; SRAM3 32KB - Non-Cacheable
*(.RAM_D1) ; DMA 描述符必须放这里!
}
RW_IRAM5 0x38000000 0x00010000 { ; SRAM4 64KB
*(.RAM_D2)
}
}
关键 :.RAM_D1 段必须放在非缓存 RAM(SRAM3),否则 DMA 描述符缓存不一致会导致接收中断无法正常工作。
3.4 配置 MPU
在 bsp_system.c 中配置 MPU,确保 SRAM3 为非缓存:
c
void mpu_config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct;
HAL_MPU_Disable();
/* SRAM3 - 非缓存区域,用于 DMA 描述符 */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30040000;
MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_32KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER2;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
3.4.1 D-Cache 写透配置
在 stm32h7xx_hal_msp.c 的 HAL_MspInit() 中添加 D-Cache 写透配置,这对 ETH DMA 缓存一致性至关重要:
c
void HAL_MspInit(void)
{
__HAL_RCC_SYSCFG_CLK_ENABLE();
HAL_NVIC_SetPriority(PendSV_IRQn, 15, 0);
SCB_EnableICache();
SCB_EnableDCache();
SCB->CACR |= 1 << 2;
}
说明 :SCB->CACR |= 1 << 2 强制 D-Cache 写透模式,确保 CPU 写入的数据能立即写回内存,避免 DMA 读取到过期的缓存数据。
3.5 编写 ETH MSP 初始化
在 stm32h7xx_hal_msp.c 中实现 HAL_ETH_MspInit(),只做硬件初始化:
c
void HAL_ETH_MspInit(ETH_HandleTypeDef* heth)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(heth->Instance==ETH)
{
__HAL_RCC_ETH1MAC_CLK_ENABLE();
__HAL_RCC_ETH1TX_CLK_ENABLE();
__HAL_RCC_ETH1RX_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/* RMII GPIO 配置 - AF11 */
GPIO_InitStruct.Pin = GPIO_PIN_1|GPIO_PIN_4|GPIO_PIN_5; /* PC1=MDC, PC4=RXD0, PC5=RXD1 */
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_1|GPIO_PIN_2|GPIO_PIN_7; /* PA1=REF_CLK, PA2=MDIO, PA7=CRS_DV */
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13; /* PB11=TX_EN, PB12=TXD0, PB13=TXD1 */
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* PHY 硬件复位 - PB0 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
HAL_Delay(100);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
HAL_Delay(100);
}
}
3.6 编写网络管理模块
创建 mongoose_net.h:
c
#ifndef __MONGOOSE_NET_H__
#define __MONGOOSE_NET_H__
#include "stdint.h"
typedef enum {
NET_LINK_DOWN = 0,
NET_LINK_UP,
NET_IP_READY
} net_link_state_t;
void mongoose_net_init(void);
void mongoose_net_task(void* param);
net_link_state_t mongoose_net_get_link_state(void);
#endif
创建 mongoose_net.c,这是移植的核心:
c
#include "mongoose_net.h"
#include "mongoose.h"
#include "app_param.h"
#include "main.h"
#include <string.h>
#include <stdio.h>
ETH_HandleTypeDef heth;
#define NET_POLL_TIMEOUT_MS 100
struct mg_mgr g_mgr;
static struct mg_tcpip_if s_ifp;
static struct mg_tcpip_driver_stm32h_data s_driver_data;
static volatile net_link_state_t s_link_state = NET_LINK_DOWN;
static void net_event_handler(struct mg_tcpip_if* ifp, int ev, void* ev_data)
{
switch (ev)
{
case MG_TCPIP_EV_STATE_CHANGE:
{
uint8_t state = ifp->state;
switch (state)
{
case MG_TCPIP_STATE_DOWN:
s_link_state = NET_LINK_DOWN;
printf("[NET] Link DOWN\r\n");
break;
case MG_TCPIP_STATE_LINK_UP:
case MG_TCPIP_STATE_UP:
s_link_state = NET_LINK_UP;
printf("[NET] Link UP\r\n");
break;
case MG_TCPIP_STATE_REQ:
printf("[NET] DHCP requesting...\r\n");
break;
case MG_TCPIP_STATE_IP:
{
uint8_t* ip = (uint8_t*)&ifp->ip;
printf("[NET] IP: %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
}
break;
case MG_TCPIP_STATE_READY:
s_link_state = NET_IP_READY;
printf("[NET] Network READY\r\n");
break;
}
break;
}
}
}
void mongoose_net_init(void)
{
uint8_t mac[6];
const net_config_t* cfg = get_netif_param();
heth.Instance = ETH;
HAL_ETH_MspInit(&heth);
HAL_SYSCFG_ETHInterfaceSelect(SYSCFG_ETH_RMII);
HAL_NVIC_SetPriority(ETH_IRQn, PRO_ETH, 0);
HAL_NVIC_EnableIRQ(ETH_IRQn);
mg_mgr_init(&g_mgr);
get_mac_param(mac);
memcpy(s_ifp.mac, mac, 6);
s_driver_data.mdc_cr = NET_DRIVER_MDC_CR;
s_driver_data.phy_addr = NET_PHY_ADDR;
s_driver_data.phy_conf = MG_PHY_CLOCKS_MAC;
s_ifp.driver = &mg_tcpip_driver_stm32h;
s_ifp.driver_data = &s_driver_data;
if (cfg->dhcp)
{
s_ifp.ip = 0;
s_ifp.mask = 0;
s_ifp.gw = 0;
s_ifp.enable_dhcp_client = true;
printf("[NET] DHCP mode enabled\r\n");
}
else
{
s_ifp.ip = MG_IPV4(cfg->ip[0], cfg->ip[1], cfg->ip[2], cfg->ip[3]);
s_ifp.mask = MG_IPV4(cfg->mask[0], cfg->mask[1], cfg->mask[2], cfg->mask[3]);
s_ifp.gw = MG_IPV4(cfg->gw[0], cfg->gw[1], cfg->gw[2], cfg->gw[3]);
s_ifp.enable_dhcp_client = false;
printf("[NET] Static IP: %u.%u.%u.%u\r\n",
cfg->ip[0], cfg->ip[1], cfg->ip[2], cfg->ip[3]);
}
s_ifp.fn = net_event_handler;
mg_tcpip_init(&g_mgr, &s_ifp);
}
void mongoose_net_task(void* param)
{
(void)param;
for (;;)
{
mg_mgr_poll(&g_mgr, NET_POLL_TIMEOUT_MS);
}
}
net_link_state_t mongoose_net_get_link_state(void)
{
return s_link_state;
}
3.7 编写网络参数结构体
在 app_param.h 中定义网络配置结构体:
c
typedef struct
{
uint8_t dhcp;
uint8_t ip[4];
uint8_t mask[4];
uint8_t gw[4];
} net_config_t;
typedef struct
{
net_config_t net_config;
uint8_t mac[6];
// ... 其他参数
} app_param_t;
const net_config_t* get_netif_param(void);
void set_netif_param(net_config_t* netif_info);
void get_mac_param(uint8_t* mac);
3.8 主函数集成
在 main.c 中初始化 RTOS 并创建网络任务:
c
#include "main.h"
#include "mongoose_net.h"
#include "cmsis_os2.h"
#include "app_param.h"
void init_task(void* param);
int main(void)
{
HAL_Init();
mpu_config();
SystemClock_Config();
dev_param_init();
osKernelInitialize();
const osThreadAttr_t init_attr = {
.name = "Init",
.stack_size = 1024,
.priority = osPriorityAboveNormal,
};
osThreadNew(init_task, NULL, &init_attr);
osKernelStart();
for(;;);
}
void init_task(void* param)
{
dbg_uart_init(115200);
mongoose_net_init();
const osThreadAttr_t net_attr = {
.name = "net",
.stack_size = 8192,
.priority = osPriorityNormal2,
};
osThreadNew(mongoose_net_task, NULL, &net_attr);
osThreadExit();
}
四、关键注意事项
4.1 严禁调用 HAL_ETH_Init()
这是最容易踩坑的地方!
HAL_ETH_Init() 会执行以下操作:
- ETH DMA 软件复位
- MAC 配置
- 设置 HAL DMA 描述符
而 mg_tcpip_driver_stm32h_init() 又会执行一次:
- ETH DMA 软件复位
- 设置 Mongoose 自己的 DMA 描述符
双重初始化会导致 ETH 状态不一致,RX 中断无法正常工作。
正确做法 :只调用 HAL_ETH_MspInit() 做硬件初始化。
4.2 ETH_IRQHandler 由 Mongoose 内部定义
ETH_IRQHandler 在 mongoose.c 中已经实现,负责接收中断处理,通过 mg_tcpip_qwrite 将数据推入队列。
不需要在 stm32h7xx_it.c 中重新定义。
4.3 中断优先级配置
c
#define PRO_ETH 6
在 main.h 中定义 ETH 中断优先级为 6。由于 CMSIS-RTOS2 下 MG_ENABLE_ETH_IRQ() 为空宏,需要手动使能中断:
c
HAL_NVIC_SetPriority(ETH_IRQn, PRO_ETH, 0);
HAL_NVIC_EnableIRQ(ETH_IRQn);
中断优先级必须低于 configMAX_SYSCALL_INTERRUPT_PRIORITY(FreeRTOSConfig.h 中配置为 5),否则中断中调用 FreeRTOS API 会出问题。
4.4 网络配置从 Flash 读取
网络配置(IP/MASK/GW/DHCP 模式)必须从 Flash 参数读取,不能硬编码在头文件中。
修改 mongoose_config.h,注释掉硬编码的 IP 宏:
c
// IP/MASK/GW are read from Flash parameters at runtime
// Default to 0.0.0.0 (DHCP mode) via mongoose.h's default definitions
4.5 网络轮询任务栈大小
网络任务需要较大的栈空间(建议 8192 字节),因为 mg_mgr_poll() 内部会处理多种网络事件。
五、状态机说明
Mongoose 网络驱动每秒轮询 PHY 链路状态,状态变化时触发 MG_TCPIP_EV_STATE_CHANGE 事件:
| 状态值 | 状态宏 | 说明 |
|---|---|---|
| 0 | MG_TCPIP_STATE_DOWN |
物理链路断开(网线拔出) |
| 1 | MG_TCPIP_STATE_LINK_UP |
物理链路已连接 |
| 2 | MG_TCPIP_STATE_UP |
L2 层就绪 |
| 3 | MG_TCPIP_STATE_REQ |
DHCP 请求中 |
| 4 | MG_TCPIP_STATE_IP |
IP 已分配 |
| 5 | MG_TCPIP_STATE_READY |
完全就绪(网关 MAC 已解析) |
六、模式切换
DHCP 模式
c
s_ifp.ip = 0;
s_ifp.mask = 0;
s_ifp.gw = 0;
s_ifp.enable_dhcp_client = true;
静态 IP 模式
c
s_ifp.ip = MG_IPV4(192, 168, 1, 100);
s_ifp.mask = MG_IPV4(255, 255, 255, 0);
s_ifp.gw = MG_IPV4(192, 168, 1, 1);
s_ifp.enable_dhcp_client = false;
运行时修改配置后,调用 set_netif_param() 保存到 Flash,重新上电生效。
七、快速上手示例
移植完成后,可通过以下示例快速验证网络功能:
7.1 创建简单的 HTTP 服务器
在 mongoose_net_init() 之后添加 HTTP 监听:
c
#include "mongoose.h"
static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
mg_http_reply(c, 200, "", "Hello from STM32H743!\r\n");
}
(void) fn_data;
}
void mongoose_net_init(void)
{
// ... 前面的初始化代码 ...
mg_http_listen(&g_mgr, "http://0.0.0.0:80", fn, NULL);
printf("[NET] HTTP server listening on port 80\r\n");
}
使用浏览器访问开发板 IP,即可看到 "Hello from STM32H743!" 响应。
7.2 创建 TCP Client 连接
c
#include "mongoose.h"
static void tcp_fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
if (ev == MG_EV_CONNECT) {
if (((struct mg_connect_result *) ev_data)->is_success) {
printf("[NET] TCP connected\r\n");
mg_send(c, "Hello from STM32H743!\r\n", 24);
} else {
printf("[NET] TCP connect failed\r\n");
}
} else if (ev == MG_EV_READ) {
printf("[NET] Received: %.*s\r\n", (int) c->recv.len, c->recv.buf);
}
(void) fn_data;
}
void start_tcp_client(void)
{
mg_connect(&g_mgr, "tcp://192.168.1.10:8080", tcp_fn, NULL);
}
7.3 FreeRTOS 堆大小配置
Mongoose 需要动态分配内存用于网络连接和缓冲区,需确保 FreeRTOSConfig.h 中堆大小足够:
c
#define configTOTAL_HEAP_SIZE ((size_t)65536)
内存大的话,建议至少配置 64KB 堆空间。
八、调试技巧
- 串口日志 :通过
printf输出网络状态变化 - 网线拔插测试 :观察
NET_LINK_DOWN/NET_LINK_UP状态切换 - DHCP 获取测试 :连接路由器,观察
DHCP requesting...和IP assigned日志 - 静态 IP 测试:设置静态 IP 后,用 ping 命令测试连通性

