STM32H743移植FreeRTOS+LwIP+FreeModbus TCP实战

发布时间:2026/10/21 6:05:39
STM32H743移植FreeRTOS+LwIP+FreeModbus TCP实战 简介这份资源面向正在学习STM32H743网络通信与工业协议开发的嵌入式工程师及高校学生基于正点原子阿波罗开发板将FreeRTOS实时操作系统、LWIP协议栈与FreeModbus-TCP/IP三者整合移植解决裸机环境下多任务网络通信与Modbus主从站协同的工程难题。压缩包共1040个文件约57.73MB以375个h头文件、306个c源文件为核心辅以105个o目标文件、105个d依赖文件及104个crf编译中间文件另含uvprojx工程、sct分散加载、hex固件与map映射等完整保留Keil工程结构与编译产物。资源整合了正点原子《lwIP例程11 lwIP_SOCKET_TCP客户端实验》与《基于STM32的ModBus实现(二)移植FreeMODBUSTCP》两套代码读者可据此掌握SOCKET接口收发、Modbus寄存器映射与任务调度配合快速搭建可运行的TCP客户端与Modbus服务端。目前已有1400人学习下载适合需要打通网络协议栈与工业总线移植的中高级开发者参考。1. 从裸机到协议栈为什么这套 STM32H743 移植方案值得拆一遍去年帮一个做工业网关的朋友看项目他们用 STM32H743 跑裸机Modbus TCP 靠手动拼以太网帧结果并发一上来就丢包客户现场天天打电话。后来换成 FreeRTOS LwIP FreeModbus 的组合同样的板子二十个客户端同时轮询稳如老狗。这件事让我意识到STM32 网络协议这块很多人卡的不是芯片性能而是协议栈的移植和任务划分。这份资源做的就是这件事在 STM32H743 上把 FreeRTOS、LwIP 和 FreeModbus-TCP 三个东西揉到一起跑通一个能用的 Modbus TCP 从站。它适合两类人一是手里有 H743 板子、想从裸机网络过渡到 RTOS 协议栈的嵌入式开发者二是做工业数据采集网关、需要快速搭出 Modbus TCP 服务端的工程师。整套代码基于 STM32CubeMX 生成框架HAL 库打底ETH 外设驱动 LwIP 协议栈 FreeModbus 应用层三层分工明确改起来不费劲。2. 架构拆解三个协议栈怎么塞进一颗 H7432.1 任务划分与优先级设计FreeRTOS 跑起来之后第一件事是决定谁跑在哪个任务里。这套方案里LwIP 的协议处理放在tcpip_thread里这是 LwIP 自带的优先级设成 osPriorityHighFreeModbus 的轮询放在单独的任务里优先级 osPriorityAboveNormal应用层的 Modbus 寄存器更新放在 osPriorityNormal。这样网络收包不会被应用逻辑堵住Modbus 轮询也不会饿死。/* 创建 Modbus 轮询任务栈大小 512 字优先级 AboveNormal */ osThreadDef(ModbusPoll, ModbusPollTask, osPriorityAboveNormal, 0, 512); modbusThreadId osThreadCreate(osThread(ModbusPoll), NULL); /* LwIP 的 tcpip_thread 由 MX_LWIP_Init 内部创建优先级在 lwipopts.h 里配 */ #define TCPIP_THREAD_PRIO osPriorityHigh #define TCPIP_THREAD_STACKSIZE 1024这里有个细节TCPIP_THREAD_STACKSIZE别抠1024 字是底线H743 内存够给 2048 更稳。栈溢出在 RTOS 里是玄学问题的头号来源我一般直接翻倍给。2.2 内存池与 PBUF 配置LwIP 的内存策略直接决定网络吞吐和稳定性。这套方案用的是PBUF_POOL加MEM_SIZE的混合模式PBUF_POOL_SIZE设成 16PBUF_POOL_BUFSIZE设成 1524刚好放一个标准以太网帧。MEM_SIZE给 16KB用于协议栈内部的动态分配。/* lwipopts.h 关键配置 */ #define MEM_SIZE (16 * 1024) #define PBUF_POOL_SIZE 16 #define PBUF_POOL_BUFSIZE 1524 #define TCP_SND_BUF (4 * TCP_MSS) #define TCP_WND (4 * TCP_MSS) #define MEMP_NUM_TCP_PCB 8 #define MEMP_NUM_TCP_PCB_LISTEN 4MEMP_NUM_TCP_PCB决定能同时维护多少个 TCP 连接。Modbus TCP 场景下8 个够用但如果你的网关要接几十个客户端这个值得往上加同时PBUF_POOL_SIZE也要跟着涨不然会出现连接建立了但收不到数据的情况。2.3 ETH 外设与 PHY 驱动对接H743 的 ETH 外设通过 RMII 接口接 PHY 芯片CubeMX 里配好引脚后生成的ethernetif.c负责在 LwIP 和 HAL 之间传数据。关键函数是low_level_output和low_level_input前者把 PBUF 里的数据通过HAL_ETH_Transmit发出去后者在中断回调里把收到的帧塞进 PBUF。/* 接收中断回调收到帧后通知 LwIP 的 tcpip_thread */ void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) { /* 释放信号量让 ethernetif 的接收线程去处理 */ osSemaphoreRelease(s_xSemaphore); } /* ethernetif_input 里循环读取直到没有新帧 */ void ethernetif_input(void *argument) { struct pbuf *p; for (;;) { if (osSemaphoreWait(s_xSemaphore, osWaitForever) osOK) { p low_level_input(gnetif); if (p ! NULL) { if (gnetif.input(p, gnetif) ! ERR_OK) { pbuf_free(p); } } } } }这里用信号量而不是直接轮询是为了让接收线程在没数据时让出 CPU。H743 的 ETH 中断优先级要设成configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY以下否则在中断里调 FreeRTOS API 会直接硬件错误。3. FreeModbus 移植从串口到 TCP 的适配层3.1 port 层文件替换FreeModbus 原版是给串口用的移植到 TCP 上核心是替换portserial.c和porttimer.c换成porttcp.c。这个文件要实现xMBTCPPortInit、xMBTCPPortGetRequest、xMBTCPPortSendResponse三个函数。/* porttcp.c 里初始化监听 socket */ BOOL xMBTCPPortInit(USHORT usTCPPort) { struct sockaddr_in addr; int opt 1; iListenSocket socket(AF_INET, SOCK_STREAM, 0); if (iListenSocket 0) return FALSE; setsockopt(iListenSocket, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); addr.sin_family AF_INET; addr.sin_addr.s_addr INADDR_ANY; addr.sin_port htons(usTCPPort); if (bind(iListenSocket, (struct sockaddr *)addr, sizeof(addr)) 0) { closesocket(iListenSocket); return FALSE; } listen(iListenSocket, 4); return TRUE; }SO_REUSEADDR这个选项一定要加不然板子重启后端口还处于 TIME_WAIT 状态bind 会失败现象就是「第一次能跑复位后连不上」。3.2 接收与发送的数据流xMBTCPPortGetRequest负责从已建立的连接里读一帧 Modbus 报文xMBTCPPortSendResponse负责把响应写回去。这两个函数在 FreeModbus 的主循环里被调用所以它们必须是阻塞的但阻塞时间要可控。/* 接收一帧返回帧长度失败返回 0 */ BOOL xMBTCPPortGetRequest(UCHAR **ppucMBTCPFrame, USHORT *usTCPLength) { int iBytesReceived; struct timeval tv { .tv_sec 1, .tv_usec 0 }; if (iClientSocket 0) { /* 没有客户端连接时accept 等待新连接 */ iClientSocket accept(iListenSocket, NULL, NULL); if (iClientSocket 0) return FALSE; setsockopt(iClientSocket, SOL_SOCKET, SO_RCVTIMEO, tv, sizeof(tv)); } iBytesReceived recv(iClientSocket, ucTCPFrame, MB_TCP_PSEUDO_ADDRESS MB_TCP_LEN MB_TCP_UID, 0); if (iBytesReceived 0) { closesocket(iClientSocket); iClientSocket -1; return FALSE; } *ppucMBTCPFrame ucTCPFrame; *usTCPLength iBytesReceived; return TRUE; }SO_RCVTIMEO设成 1 秒是为了防止客户端连上不发数据导致任务卡死。超时后关闭连接回到 accept 状态等下一个客户端。这个逻辑在工业现场很实用因为上位机软件经常抽风连上不干活。3.3 寄存器映射与回调实现FreeModbus 用回调函数的方式让应用层处理寄存器读写。这套方案里保持寄存器和输入寄存器各映射了 100 个线圈和离散输入各 64 个。/* 保持寄存器回调上位机读 40001 开始的数据 */ eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { USHORT i; if (eMode MB_REG_READ) { for (i 0; i usNRegs; i) { /* 从本地数组取数据大端序发给上位机 */ pucRegBuffer[i * 2] (UCHAR)(usHoldingRegs[usAddress i] 8); pucRegBuffer[i * 2 1] (UCHAR)(usHoldingRegs[usAddress i] 0xFF); } } else { for (i 0; i usNRegs; i) { usHoldingRegs[usAddress i] (pucRegBuffer[i * 2] 8) | pucRegBuffer[i * 2 1]; } } return MB_ENOERR; }注意地址偏移Modbus 协议里保持寄存器从 40001 开始但代码里数组下标从 0 开始所以usAddress传进来的时候已经减过 1 了。这个坑我踩过上位机读 40001 结果返回的是 40002 的数据查了半天才发现是偏移没对齐。4. 跑起来从 CubeMX 配置到 ping 通4.1 时钟与 ETH 引脚配置H743 的 ETH 时钟来自 PLL2CubeMX 里要确保 PLL2 的输出是 50MHz 给 RMII 参考时钟。引脚方面RMII 模式用到 PA1REF_CLK、PA2MDIO、PA7CRS_DV、PC1MDC、PC4RXD0、PC5RXD1、PB11TX_EN、PB12TXD0、PB13TXD1。# CubeMX 里 ETH 配置要点 # PHY 地址根据原理图填常见是 0 或 1 # 自动协商使能 # 速率100Mbps 全双工 # RX 中断使能 # 生成代码时勾选 Generate peripheral initialization as a pair of .c/.h filesPHY 地址填错是最常见的翻车点现象是HAL_ETH_Init返回 HAL_ERROR或者初始化过了但 ping 不通。用示波器看 MDC/MDIO 有没有波形有波形但读不到 ID就是地址不对。4.2 LwIP 初始化顺序LwIP 的初始化必须在 FreeRTOS 启动调度器之前完成但tcpip_thread是在MX_LWIP_Init里创建的所以顺序是HAL_Init → 时钟配置 → ETH 初始化 → LwIP 初始化 → 创建应用任务 → 启动调度器。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ETH_Init(); MX_FREERTOS_Init(); /* 创建 Modbus 任务 */ MX_LWIP_Init(); /* 初始化 LwIP创建 tcpip_thread */ /* 设置静态 IP或者用 DHCP */ IP4_ADDR(gnetif.ipaddr, 192, 168, 1, 100); IP4_ADDR(gnetif.netmask, 255, 255, 255, 0); IP4_ADDR(gnetif.gw, 192, 168, 1, 1); netif_set_default(gnetif); netif_set_up(gnetif); osKernelStart(); while (1); }如果要用 DHCP把netif_set_up之前调dhcp_start(gnetif)然后在MX_LWIP_Init里把LWIP_DHCP设成 1。静态 IP 在工业现场更常见因为上位机通常按固定 IP 配。4.3 验证 Modbus TCP 通信板子跑起来后先用 ping 确认网络通再用 Modbus Poll 或者mbpoll工具读寄存器。# 用 mbpoll 读保持寄存器从站地址 1读 10 个寄存器 mbpoll -m tcp -a 1 -t 4 -r 1 -c 10 192.168.1.100 # 输出示例 # [1]: 0 # [2]: 0 # ... # 说明通信正常寄存器值取决于应用层写入如果 ping 通但 Modbus 读不到先看xMBTCPPortInit里的端口是不是 502再看防火墙有没有拦。Linux 下用ss -tlnp | grep 502确认监听状态。5. 避坑排查移植过程中最容易翻车的五个点5.1 ping 不通但 ETH 初始化没报错现象HAL_ETH_Init返回 HAL_OK但板子 ping 不通网口灯不亮或者只亮一个。原因PHY 芯片的复位引脚没拉低再拉高或者 RMII 参考时钟没出来。H743 的 ETH 需要 50MHz 时钟如果用的是外部晶振直接给 PHYCubeMX 里要选对时钟源。解决用示波器测 PA1 有没有 50MHz 波形没有就检查 PLL2 配置。PHY 复位引脚在MX_GPIO_Init里加一段拉低 10ms 再拉高的代码。5.2 LwIP 跑起来后 HardFault现象调度器启动后几秒内进 HardFault调用栈指向mem_malloc或pbuf_alloc。原因MEM_SIZE或PBUF_POOL_SIZE设得太小协议栈分配内存失败后返回 NULL但上层没判空直接用了。或者 ETH 中断优先级高于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY在中断里调了 FreeRTOS API。解决把MEM_SIZE加到 32KBPBUF_POOL_SIZE加到 32。ETH 中断优先级设成 5 或更低数值越大优先级越低确保不高于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY。5.3 Modbus 连接建立后收不到响应现象上位机显示已连接但发请求后超时板子这边recv返回 0 或 -1。原因xMBTCPPortGetRequest里recv的缓冲区大小算错了或者MB_TCP_PSEUDO_ADDRESS和MB_TCP_LEN的偏移没对齐。Modbus TCP 报文前 7 个字节是 MBAP 头FreeModbus 期望的缓冲区是从事务标识符开始的。解决确认ucTCPFrame数组大小至少是MB_TCP_PSEUDO_ADDRESS MB_TCP_LEN MB_TCP_UID 256recv的第三个参数用这个总长度。抓包看板子有没有回响应有回但上位机收不到就是发送缓冲区没 flush。5.4 多个客户端同时连接时崩溃现象一个客户端正常两个客户端同时连就死机或者响应错乱。原因xMBTCPPortGetRequest里只维护了一个iClientSocket第二个客户端连上来时覆盖了第一个的 socket 描述符导致数据流混乱。解决FreeModbus 原版设计就是单连接的要支持多客户端得在porttcp.c里用select或者多线程管理多个 socket每个 socket 对应一个 Modbus 实例。或者简单点用 LwIP 的netconnAPI 自己写一个多连接的分发层。5.5 复位后端口被占用现象板子第一次跑正常按复位键后bind失败Modbus 连不上。原因TCP 连接关闭后进入 TIME_WAIT 状态默认持续 2 分钟左右期间端口不能被重新绑定。解决setsockopt加SO_REUSEADDR这个前面提过。如果还不行在xMBTCPPortInit里加SO_LINGER选项设成l_onoff1, l_linger0强制关闭时不进入 TIME_WAIT。6. 二次开发把 Modbus 寄存器接到你的业务逻辑上这套代码跑通之后真正的工作量在于把寄存器映射到实际业务。我一般会在eMBRegHoldingCB里做一层转换把 Modbus 地址映射到结构体字段而不是直接操作数组。这样业务逻辑改起来不用动 Modbus 回调。/* 定义业务数据结构 */ typedef struct { float temperature; float pressure; uint16_t status; uint32_t total_count; } DeviceData_t; static DeviceData_t gDeviceData; /* 保持寄存器回调里做映射 */ eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { if (eMode MB_REG_READ) { /* 地址 0-1 映射温度2-3 映射压力4 映射状态5-6 映射计数 */ if (usAddress 0 usNRegs 2) { uint32_t temp *(uint32_t *)gDeviceData.temperature; pucRegBuffer[0] temp 24; pucRegBuffer[1] temp 16; pucRegBuffer[2] temp 8; pucRegBuffer[3] temp; } /* 其他地址类似处理 */ } return MB_ENOERR; }浮点数转寄存器要注意字节序Modbus 默认大端但不同上位机对浮点的解析顺序不一样。有的按 ABCD有的按 CDAB。我一般会在文档里写清楚或者加一个配置寄存器让上位机切换字节序。另一个改造点是增加 Modbus 功能码。FreeModbus 默认支持 01/02/03/04/05/06/15/16如果要支持 23读写多个寄存器得在mbfunc.h里加声明在mbfuncother.c里实现处理函数然后在eMBInit里注册。验证改造是否成功我习惯用两步先用mbpoll确认基本读写正常再用 Python 的pymodbus写个脚本模拟并发请求看长时间跑会不会内存泄漏。from pymodbus.client import ModbusTcpClient import time client ModbusTcpClient(192.168.1.100, port502) client.connect() for i in range(1000): rr client.read_holding_registers(0, 10, slave1) if rr.isError(): print(f第 {i} 次读取失败) break time.sleep(0.01) client.close()这个脚本跑一晚上如果板子没死机、内存没涨基本就稳了。从那以后我每次移植完协议栈都强制走一遍「ping → 单次读写 → 并发压测 → 复位重连」这个流程少一步都可能在现场翻车。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询