☰
STM32H743 的 FreeRTOS/LwIP/Modbus TCP 移植
2026/10/26 18:34:03 网站建设 项目流程

简介:这份资源面向已掌握STM32基础、希望进一步打通以太网通信与工业总线协议的嵌入式开发者,基于正点原子STM32H743阿波罗开发板,把FreeRTOS实时操作系统、lwIP协议栈与FreeModbus TCP/IP三者整合到同一工程中,解决裸机TCP客户端例程与Modbus从机协议难以协同调度的实际问题,可作为工业网关、远程数据采集与设备联网类项目的参考框架。压缩包共1040个文件,约57.73MB,以375个h头文件、306个c源文件为主体,囊括HAL外设驱动、lwIP协议栈与Modbus协议实现,另含105个o、105个d、104个crf等Keil编译中间文件及uvprojx、uvoptx工程文件,并附带hex、axf、map与readme说明,便于直接打开编译对照。资源已有1407人学习下载。读者可据此理解在RTOS任务中运行TCP客户端、移植Modbus TCP从机并完成寄存器映射的组织方式,借助任务划分与协议栈配置思路排查连接异常,快速搭建可复用的网络通信验证环境。

1. 从裸机到协议栈:STM32H743 上跑通 FreeRTOS、LwIP 与 FreeModbus TCP/IP 的落地路径

STM32H743 上把 FreeRTOS、LwIP 和 FreeModbus TCP/IP 串成一条可复现的 stm32 网络协议链路,难点不在单独跑通某个中间件,而在三者的节拍对齐:ETH 中断、tcpip_thread、Modbus 轮询任务谁先谁后,决定了现场是稳定运行还是偶发超时。这个移植方案适合已经能让 H743 裸机 ping 通、准备把 Modbus TCP 从站接到板子上的开发者,也适合手里有旧 F4 工程、想迁到 H7 的团队。它解决的是“协议栈能通但跑不稳”的问题,不是从零讲以太网原理。硬件时钟、RTOS 接缝、LwIP 参数、FreeModbus 端口层和现场排查,每一层都有需要定死的参数。

2. 硬件与时钟先定死:ETH 外设、MPU 和中断优先级的选型理由

H743 的以太网移植最容易翻车的地方,不是 LwIP 配置,而是 ETH DMA 和 D-Cache 的边界。F4 上没有 D-Cache,很多人把 F4 的ethernetif.c直接搬到 H7,编译能过,ping 也偶尔能通,但一跑 Modbus 连续读写就开始随机丢包。H7 的 ETH 描述符和缓冲区如果放在可缓存区域,DMA 写入内存后 CPU 读到的是旧数据,或者 CPU 写回后 DMA 读到旧数据,现场表现就是“有时通有时不通”,没有固定规律。

我一般会先把 ETH 描述符和接收缓冲区固定到 D2 SRAM,再用 MPU 把这块区域配成 non-cacheable,而不是到处加SCB_CleanDCache。这样做的代价是这块 RAM 不能当普通缓存用,但换来的是一致性确定,排查时不用把 cache 当成黑匣子。

2.1 为什么 H743 的以太网不能照搬 F4 的配置

H743 的 ETH 挂在 D2 域,DMA 访问描述符和缓冲区的路径与 F4 不同;再加上 H7 有 D-Cache,ETH 收发缓冲区的内存属性必须显式声明。常见做法是把 ETH 描述符放在0x30040000起始的 D2 SRAM,接收缓冲紧跟着放,MPU 配一个 64KB 或 256KB 的 non-cacheable 区域。下面这段是我会在main.c里最先落的 MPU 配置,先于SCB_EnableDCache()和 ETH 初始化执行。

static void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct = {0}; HAL_MPU_Disable(); /* Region 0: ETH 描述符和缓冲区,base 按实际链接脚本调整 */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.BaseAddress = 0x30040000U; MPU_InitStruct.Size = MPU_REGION_SIZE_64KB; MPU_InitStruct.SubRegionDisable = 0x00U; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); }

这段代码的关键不在HAL_MPU_ConfigRegion本身,而在三个参数:BaseAddress必须和链接脚本里 ETH 缓冲段一致,Size要覆盖描述符加所有接收缓冲区,IsCacheable和IsBufferable都关掉。只关 cache 不关 buffer 也可能出问题,尤其是描述符状态位被写缓冲延迟。DisableExec关掉取指,避免 CPU 从数据区取指令。有人会问能不能用SCB_InvalidateDCache_by_Addr代替 MPU,可以,但每一个收包路径都要算地址和长度,Modbus 一压测就容易漏,我倾向一次性把区域属性定死。

注意:MPU 配置必须在开启 D-Cache 之前生效,ETH 初始化之后再改区域属性,已经跑起来的 DMA 可能已经在旧属性下搬过数据。

2.2 时钟树、PHY 地址与 GPIO 的落地检查

H743 的 RMII 接口需要 50MHz 参考时钟,常见来源是 PHY 的 REF_CLK 或 MCU 的 MCO。无论哪种,GPIO 速度都要拉到 very high,RMII_REF_CLK 要选对 AF11。PHY 地址由 PHYAD strap 决定,常见是 0 或 1,代码里写死之前先用 MDIO 读一遍PHY_BSR,确认链路状态和自协商结果。下面是我会保留的 ETH 初始化关键字段,省掉 CubeMX 生成的 GPIO 细节,只留会影响链路的参数。

heth.Instance = ETH; heth.Init.MACAddr[0] = 0x02; heth.Init.MACAddr[1] = 0x00; heth.Init.MACAddr[2] = 0x00; heth.Init.MACAddr[3] = 0x00; heth.Init.MACAddr[4] = 0x00; heth.Init.MACAddr[5] = 0x01; heth.Init.MediaInterface = ETH_MEDIA_INTERFACE_RMII; heth.Init.RxMode = ETH_RXINTERRUPT_MODE; heth.Init.Speed = ETH_SPEED_100M; heth.Init.DuplexMode = ETH_FULLDUPLEX_MODE; heth.Init.PhyAddress = 0; heth.Init.AutoNegotiation = ETH_AUTONEGOTIATION_ENABLE; if (HAL_ETH_Init(&heth) != HAL_OK) { Error_Handler(); }

MACAddr首字节最低位不能是 1,否则会变成组播地址,交换机可能不转发;MediaInterface选 RMII 而不是 MII,H743 板子上常见的是 RMII;RxMode用中断模式,后面ethernetif_input任务才有信号量可等;PhyAddress写 0 之前一定确认硬件 strap,写错的表现是HAL_ETH_Init超时或读不到PHY_ID。AutoNegotiation打开后,HAL_ETH_Init里会等自协商完成,如果网线没插,初始化可能卡住,量产固件里我会把自协商等待改成非阻塞,先让协议栈起来,链路状态交给netif_set_link_up/down。

GPIO 这边,RMII 的 TX_EN、TXD0、TXD1、RXD0、RXD1、CRS_DV、REF_CLK 都要配GPIO_SPEED_FREQ_VERY_HIGH,MDIO 可以低一档。很多人只配了 AF 忘了速度,百兆下勉强能通,一跑满带宽就出 CRC 错误。PHY 复位脚和复位时序也要看,复位太短 PHY 内部寄存器没准备好,HAL_ETH_Init读出来全是 0xFFFF。

2.3 中断优先级和 FreeRTOS 的接口边界

ETH 中断里如果调用xSemaphoreGiveFromISR,它的优先级数值必须大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY。H7 用 4 位优先级,configPRIO_BITS = 4,如果configMAX_SYSCALL_INTERRUPT_PRIORITY设 5,ETH 中断设 5 或 6 都可以,设 4 就会触发 FreeRTOS 的断言。下面这段是 NVIC 配置和中断回调的边界,ETH 中断只做清标志和给信号量,不直接碰 LwIP。

HAL_NVIC_SetPriority(ETH_IRQn, 5, 0); HAL_NVIC_EnableIRQ(ETH_IRQn); void ETH_IRQHandler(void) { HAL_ETH_IRQHandler(&heth); } void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xSemaphoreGiveFromISR(s_xEthRxSem, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }

HAL_NVIC_SetPriority第二个参数是抢占优先级,数值越小优先级越高。ETH 中断设 5,tcpip_thread任务优先级设 3,应用 Modbus 任务设 2,这样收包中断能打断任务,tcpip_thread又能优先处理协议栈。不要在 ETH 中断里调用tcpip_input,它内部会走邮箱和信号量,不属于 ISR 安全路径;也不要在中断里pbuf_alloc,内存池操作可能阻塞。信号量给到ethernetif_input任务后,任务里再做low_level_input和netif->input,这条边界清楚,后面排查也容易定位是中断没进还是任务没跑。

3. FreeRTOS 与 LwIP 的接缝:内存池、tcpip_thread 与 ethernetif 的移植

LwIP 在裸机上跑和挂到 FreeRTOS 下跑,差别不只是多一个tcpip_thread。裸机时ethernetif_input可以在主循环里轮询,挂 RTOS 后收包路径变成中断给信号量、任务取包、tcpip_thread处理,任何一段优先级或内存池配错,表现都是“连接能建,数据不通”。这一层我会先把 FreeRTOS 堆和 LwIP 内存池分开算,再调tcpip_thread的栈和优先级,最后才接 FreeModbus。

3.1 内存池和堆方案:该用 heap_4 还是自己划一块

H743 有 1MB RAM,看起来宽裕,但 LwIP 的MEM_SIZE、PBUF_POOL_SIZE、MEMP_NUM_TCP_PCB和 FreeRTOS 的configTOTAL_HEAP_SIZE是分开算的。FreeRTOS 用heap_4管理任务栈、信号量、队列;LwIP 用mem_malloc管理pbuf和协议控制块。两者都从同一个.bss或自定义段里划,链接脚本没留够,运行到一半就进HardFault。我一般给 FreeRTOS 128KB,LwIP 64KB,PBUF_POOL_SIZE32,PBUF_POOL_BUFSIZE1536,先跑通再按lwip_stats调。

/* FreeRTOSConfig.h 关键项 */ #define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1 #define configMAX_PRIORITIES 7 #define configMINIMAL_STACK_SIZE 512 #define configTOTAL_HEAP_SIZE (128 * 1024) #define configMAX_SYSCALL_INTERRUPT_PRIORITY 5 #define configPRIO_BITS 4 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15

configTOTAL_HEAP_SIZE是 FreeRTOS 自己的堆,任务栈从里面分;configMINIMAL_STACK_SIZE单位是StackType_t,H7 上是 4 字节,512 就是 2KB,空闲任务够用,但tcpip_thread不能只给 512。configMAX_SYSCALL_INTERRUPT_PRIORITY和 NVIC 的 ETH 优先级要对应,前面设 5,这里也是 5。configPRIO_BITS必须和芯片实际优先级位一致,H7 是 4 位,写 8 会让优先级计算全错,中断里给信号量直接断言。

/* lwipopts.h 关键项 */ #define MEM_LIBC_MALLOC 0 #define MEM_SIZE (64 * 1024) #define PBUF_POOL_SIZE 32 #define PBUF_POOL_BUFSIZE 1536 #define MEMP_NUM_TCP_PCB 8 #define MEMP_NUM_TCP_PCB_LISTEN 4 #define MEMP_NUM_NETBUF 16 #define TCP_MSS 1460 #define TCP_SND_BUF (8 * TCP_MSS) #define TCP_WND (8 * TCP_MSS) #define TCPIP_THREAD_STACKSIZE 2048 #define TCPIP_THREAD_PRIO 3 #define LWIP_TCP 1 #define LWIP_DHCP 0

MEM_SIZE是 LwIP 堆,PBUF_POOL_SIZE是固定大小的 pbuf 池,Modbus TCP 的报文很小,32 个池够用,但如果同时有 ARP、ICMP、TCP 重传,池子太小会pbuf_alloc失败,表现是 ping 通但 Modbus 请求偶发超时。MEMP_NUM_TCP_PCB是同时存在的 TCP 连接数,Modbus 主站一般单连接,8 个留余量。TCP_SND_BUF和TCP_WND不要照抄大值,H7 内存够但窗口太大对 Modbus 小报文没收益,反而占内存。TCPIP_THREAD_STACKSIZE单位是字,2048 字就是 8KB,跑 Modbus 加 ICMP 够用,后面用uxTaskGetStackHighWaterMark看水位。

注意:MEM_SIZE和configTOTAL_HEAP_SIZE不是同一个堆,调大一个不会自动调大另一个;链接脚本里给 LwIP 的自定义段也要同步留空间。

3.2 ethernetif.c 的收发链路与信号量

ethernetif.c是 LwIP 和 ETH 驱动之间的接缝。low_level_init里填netif->hwaddr、mtu、flags,ethernetif_input任务等信号量,取到包后交给netif->input。H7 的 HAL 收包 API 和 F4 不一样,常见做法是HAL_ETH_GetRxDataBuffer拿到当前描述符缓冲区,HAL_ETH_GetRxDataLength拿长度,pbuf_alloc(PBUF_RAW, len, PBUF_POOL)分配 pbuf,pbuf_take拷贝,最后HAL_ETH_BuildRxDescriptors把描述符还给 DMA。下面这段是任务侧的核心逻辑,省掉 HAL 细节,保留容易漏的释放和返回值判断。

static void ethernetif_input(void *argument) { struct netif *netif = (struct netif *)argument; struct pbuf *p; for (;;) { if (xSemaphoreTake(s_xEthRxSem, portMAX_DELAY) == pdTRUE) { p = low_level_input(netif); if (p != NULL) { if (netif->input(p, netif) != ERR_OK) { pbuf_free(p); } } } } }

netif->input在 LwIP 里通常指向tcpip_input,它会把 pbuf 投递到tcpip_thread的邮箱。如果这里返回ERR_OK,pbuf 的所有权已经交给协议栈,任务里不能再pbuf_free;返回非ERR_OK才由调用者释放。很多人两种路径都释放,或者都不释放,前者是 double free,后者是内存池慢慢耗尽。low_level_input里pbuf_alloc失败时要清 DMA 描述符并返回 NULL,不能把半包交给协议栈。s_xEthRxSem在 ETH 接收中断里 give,在任务里 take,信号量初值 0,最大计数 1 就够,收包快于处理时后续中断会合并,LwIP 自己能处理。

low_level_init里还要设置netif->flags:NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP。LINK_UP先置上,等 PHY 自协商完成后再用netif_set_link_up更新;如果一直不置,ARP 请求发不出去,表现是能 ping 通自己但 ping 不通主站。netif->mtu设 1500,Modbus TCP 的 MBAP 加 PDU 远小于这个值,但 ARP 和 ICMP 会用到。

3.3 tcpip_thread 优先级和栈深度的实测调整

tcpip_thread是 LwIP 的核心任务,所有netconn、pbuf、TCP 定时器都在它的上下文里跑。它的优先级要高于应用任务,低于 ETH 中断;栈要够放 TCP 控制块和 Modbus 回调里的局部变量。我一般先给优先级 3、栈 2048 字,跑一轮 Modbus 读写后用uxTaskGetStackHighWaterMark看剩余,低于 256 字就加。应用任务优先级 2,Modbus 端口任务也设 2,避免 Modbus 任务抢在tcpip_thread前面处理收包。

sys_thread_new("tcpip", tcpip_thread, NULL, TCPIP_THREAD_STACKSIZE, TCPIP_THREAD_PRIO);

sys_thread_new的第四个参数是栈深度,单位是StackType_t,不是字节;第五个参数是优先级,数值越大优先级越高。TCPIP_THREAD_PRIO设 3,ETH 中断抢占优先级 5,应用任务 2,这样收包中断能打断应用,tcpip_thread能打断应用,应用不会饿死协议栈。tcpip_thread里不要做耗时操作,Modbus 寄存器回调如果太长,会阻塞同一线程里的其他 TCP 连接;回调里只做内存拷贝和边界检查,耗时逻辑放到应用任务。

注意:tcpip_thread栈溢出不一定立刻死机,可能先破坏相邻任务控制块,现场表现是“跑几分钟后随机异常”,用高水位标记比看门狗更早发现。

4. FreeModbus TCP 移植:从回调注册到端口层落地

FreeModbus 的 TCP 端口层本质是把 LwIP 收到的 TCP 字节流按 MBAP 头拆成 Modbus PDU,再交给协议栈处理。裸机上可以用阻塞 socket,挂到 LwIP 上我倾向用netconnAPI,代码比 raw API 直观,阻塞行为也容易控制。关键是端口层只负责收、发、关连接,寄存器映射和异常码交给回调,不要把业务逻辑塞进porttcp.c。

4.1 FreeModbus TCP 在 LwIP 上的两种接法

LwIP 有 raw API 和 netconn API 两套。raw API 在回调里处理,不能阻塞,适合对内存和实时性极敏感的场景;netconn API 用起来像 socket,netconn_accept、netconn_recv可以设超时,适合 Modbus 从站这种“一个连接、低频请求”的场景。我选 netconn,因为 FreeModbus 的端口函数本身就是同步取请求、同步发响应,用 raw API 要自己维护状态机,反而容易漏包。下面这段是端口初始化,创建 TCP 连接、绑定 502、进入监听。

BOOL xMBTCPPortInit(USHORT usTCPPort) { struct netconn *conn; err_t err; conn = netconn_new(NETCONN_TCP); if (conn == NULL) { return FALSE; } err = netconn_bind(conn, IP_ADDR_ANY, usTCPPort); if (err != ERR_OK) { netconn_delete(conn); return FALSE; } err = netconn_listen(conn); if (err != ERR_OK) { netconn_delete(conn); return FALSE; } xModbusTCPConn = conn; return TRUE; }

usTCPPort传 502,IP_ADDR_ANY表示监听所有本地地址;netconn_new(NETCONN_TCP)创建的是 TCP 连接对象,不是监听对象,netconn_listen之后才进入监听状态。netconn_bind失败常见原因是端口被占用或tcpip_thread还没起来;netconn_listen失败常见原因是MEMP_NUM_TCP_PCB_LISTEN不够。返回值用 FreeModbus 的BOOL,失败时要把conn删掉,否则每次重试都泄漏一个 PCB。

注意:xMBTCPPortInit必须在tcpip_init之后调用,最好在tcpip_thread已经运行后创建 Modbus 任务,不要在main里tcpip_init之前就netconn_new。

4.2 porttcp.c 的监听、接受与关闭流程

端口任务的主循环是:netconn_accept等主站连接,netconn_recv收数据,按 MBAP 长度字段截取一帧,交给 FreeModbus 处理,处理完netconn_write发响应,连接断开后netconn_close和netconn_delete。Modbus TCP 允许长连接,也允许短连接,端口层要能处理主站主动断开。下面这段是简化后的处理循环,重点看netbuf_delete和netconn_close的位置。

static void prvMBTCPPortTask(void *pvParameters) { struct netconn *client; struct netbuf *buf; err_t err; for (;;) { if (netconn_accept(xModbusTCPConn, &client) == ERR_OK) { netconn_set_recvtimeout(client, 5000); for (;;) { err = netconn_recv(client, &buf); if (err != ERR_OK) { break; } /* 这里把 buf->p->payload 按 MBAP 长度交给 FreeModbus */ eMBTCPReceive((UCHAR *)buf->p->payload, buf->p->len); netbuf_delete(buf); } netconn_close(client); netconn_delete(client); } } }

netconn_accept返回的client是新的连接对象,监听对象xModbusTCPConn继续监听;netconn_set_recvtimeout设 5 秒,防止主站半开连接把任务卡死;netconn_recv返回的buf必须netbuf_delete,否则MEMP_NUM_NETBUF很快耗尽;netconn_close只关连接,netconn_delete才释放对象,两个都要调。eMBTCPReceive是端口层给协议栈的入口,实际 FreeModbus 的 TCP 端口有自己的接收缓冲区,长度要按 MBAP 头里的长度字段校验,不能直接把buf->p->len全丢进去。

netconn_write发响应时用NETCONN_COPY,让 LwIP 拷贝到自己的发送缓冲区,端口层可以立刻复用接收缓冲;如果用NETCONN_NOCOPY,要等发送完成才能释放,Modbus 小报文没必要省这次拷贝。发送失败要关连接,不要重试,主站会自己重连。

4.3 寄存器回调与保持寄存器映射

FreeModbus 的保持寄存器回调eMBRegHoldingCB是业务和协议栈的边界。读功能码 0x03 时从usRegs数组拷贝到pucRegBuffer,写功能码 0x10 时从pucRegBuffer解到usRegs,地址越界返回MB_ENOREG,协议栈会自动回异常码 0x02。下面这段是常见写法,重点是边界检查和大小端。

eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { USHORT i; if ((usAddress + usNRegs) > REG_HOLDING_NREGS) { return MB_ENOREG; } if (eMode == MB_REG_READ) { for (i = 0; i < usNRegs; i++) { pucRegBuffer[2 * i] = (UCHAR)(usRegs[usAddress + i] >> 8); pucRegBuffer[2 * i + 1] = (UCHAR)(usRegs[usAddress + i] & 0xFF); } } else { for (i = 0; i < usNRegs; i++) { usRegs[usAddress + i] = ((USHORT)pucRegBuffer[2 * i] << 8) | (USHORT)pucRegBuffer[2 * i + 1]; } } return MB_ENOERR; }

usAddress是 Modbus 报文里的起始地址,usNRegs是寄存器数量,REG_HOLDING_NREGS是本地数组长度;判断用usAddress + usNRegs > REG_HOLDING_NREGS,不要用>=,否则最后一个寄存器读不到。读模式先高字节后低字节,写模式反过来拼,Modbus 是大端。MB_ENOREG会让协议栈回异常码 0x02,如果返回MB_ENOERR但数组越界,现场就是内存踩踏,表现比异常码难查得多。

4.4 主站联调:用脚本压一遍 0x03/0x10

板子跑起来后,先用脚本发一帧 0x03 读保持寄存器,再发 0x10 写,确认 MBAP 头、事务号、单元号都对。下面这段 Python 不依赖第三方 Modbus 库,直接拼 socket 报文,适合在现场没有 pip 环境时用。

import socket import struct def read_holding(host, unit, addr, count, tid=1): pdu = struct.pack(">BHH", 0x03, addr, count) mbap = struct.pack(">HHHB", tid, 0, len(pdu) + 1, unit) s = socket.create_connection((host, 502), timeout=3) s.sendall(mbap + pdu) resp = s.recv(256) s.close() return resp def write_holding(host, unit, addr, values, tid=2): pdu = struct.pack(">BHHB", 0x10, addr, len(values), len(values) * 2) pdu += b"".join(struct.pack(">H", v) for v in values) mbap = struct.pack(">HHHB", tid, 0, len(pdu) + 1, unit) s = socket.create_connection((host, 502), timeout=3) s.sendall(mbap + pdu) resp = s.recv(256) s.close() return resp print(read_holding("192.168.1.10", 1, 0, 4)) print(write_holding("192.168.1.10", 1, 0, [0x1234, 0x5678]))

struct.pack(">HHHB", tid, 0, len(pdu) + 1, unit)里tid是事务号,主站每次可以递增,从站原样返回;协议号固定 0;长度字段是“单元号 + PDU”的字节数,所以len(pdu) + 1;单元号在 TCP 里通常填 1,从站不严格校验但主站要一致。读响应前 7 字节是 MBAP,第 8 字节是功能码,第 9 字节是字节数,后面才是寄存器数据。写响应返回前 6 字节和起始地址、数量,可以用来确认写入地址。这个脚本跑通,再上正式主站,能少掉很多“主站配置问题”的扯皮。

5. 避坑与排查:H743 上 Modbus TCP 不通的五个现场

Modbus TCP 不通时,最怕一上来就改 LwIP 参数。我一般按“物理链路、IP 层、TCP 连接、Modbus 帧、寄存器回调”从下往上排,每一层都有明确的观察点。下面五个现场是 H743 移植里出现频率比较高的,现象、原因、解决都按实际调试顺序写,能直接对着查。

5.1 现象:ping 通但 502 端口连不上

现象是板子能回 ICMP,主站ping正常,但 TCP 连接 502 超时或立刻被拒。原因通常是xMBTCPPortInit在tcpip_init之前调用,netconn_new拿不到协议栈上下文,或者监听对象创建了但netconn_listen没成功。另一种情况是端口被上一次没释放的连接占着,netconn_bind返回ERR_USE。

解决方法是把 Modbus 端口初始化放到tcpip_thread启动之后,最稳的是创建一个 Modbus 任务,在任务里先xMBTCPPortInit,再进主循环。下面这段是创建顺序,tcpip_init完成后再建 Modbus 任务。

tcpip_init(NULL, NULL); sys_thread_new("modbus", prvMBTCPPortTask, NULL, 1024, 2);

tcpip_init会创建tcpip_thread和邮箱,之后netconn_new才能把消息投递进去;prvMBTCPPortTask优先级 2,低于tcpip_thread的 3,避免 Modbus 任务在协议栈还没处理完时抢资源。如果netconn_bind返回ERR_USE,先关掉旧连接再重试,不要反复netconn_new。

5.2 现象:连接建立后第一次请求就超时

现象是 TCP 三次握手能完成,主站发了 0x03 请求,板子不回响应,过几秒连接断开。原因常见是ethernetif_input任务没启动,或者netif->input

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询