资讯动态

STM32H743移植Modbus TCP:D-Cache与MPU排错

发布时间:2026/10/26 2:18:58 来源:尧图企业网站定制
简介面向嵌入式网络通信开发者的 STM32H743 综合移植工程基于正点原子阿波罗开发板把 FreeRTOS 实时内核、LwIP 协议栈与 FreeModbus TCP/IP 从机整合进同一套可编译运行的 Keil 工程解决裸机网络例程难以承载多任务 Modbus 通信的问题。工程以正点原子《lwIP_SOCKET_TCP 客户端实验》作为网络底层并参照公开的 FreeMODBUSTCP 移植笔记完成协议对接适合已熟悉 HAL 库与外设驱动、希望进阶工业以太网通信的学习者。压缩包共 1040 个文件、约 57.73MB以 375 个 .h 头文件与 306 个 .c 源文件为主体涵盖 HAL 外设驱动、LwIP 与 FreeModbus 协议源码及 FreeRTOS 内核 tasks.c另有 .o、.d、.crf 等编译中间文件以及 .uvprojx 工程、.hex、.axf、.map 输出和少量 readme 说明打开即可复现调试。目前已有 1407 人学习下载。1. 第一次在 STM32H743 上凑齐 FreeRTOS、LwIP、FreeModbus TCP我卡在 D-Cache 上做 STM32 网络协议这套东西STM32H743 几乎是绕不开的一颗芯片480MHz 主频、自带以太网 MAC、1MB SRAM跑 FreeRTOS 加 LwIP 再加一个 Modbus TCP 从站资源绰绰有余。我第一次移植时以为工作量全在协议栈源码上结果真正卡住两天的是 L1 D-Cache 和 MPU。现象很统一ping 通502 端口也连得上Modbus 一读保持寄存器全是 0换板子、换网线、换上位机都一样。H7 跟 F4 不是一个路数DMA 搬运的以太网帧和 CPU 看到的缓冲区之间隔着一层缓存一致性缓存没配好协议栈逻辑写得再对也白搭。这篇记录的是我从空工程一路装到能用 Modbus Poll 稳定读数的完整过程包含三个中间件的版本取舍、关键配置参数、任务划分以及装坏之后怎么一步步救回来。适合手里已经有 H743 板子、卡在“网口能通但协议跑不起来”阶段的人新手照着顺序走也能走通熟手可以重点看参数取舍和排查那几章。2. 环境清点与工程骨架H743 上三件套先定版本再动手移植这种事最怕的不是写代码是版本对不上。CubeMX 生成的 FreeRTOS 和 LwIP 是跟固件包绑定的FreeModbus 又是一份独立源码包三者对 lwIP API 的假设不一样。我一般先把版本表列出来写在工程根目录的 README 里后面出问题第一件事就是回头看这张表。2.1 版本对照表与硬件前提这一次用的组合如下都是我当时实际跑通的版本不是建议你非要用这个而是让你知道版本差异大概在哪项目版本说明芯片STM32H743ZIT62MB Flash、1MB RAM片上 ETH MAC固件包STM32CubeH7 1.11.0内含 HAL、LwIP 2.1.3、FreeRTOS 10.3.1工程生成STM32CubeMX 6.x只用来生成时钟、ETH、FreeRTOS 骨架FreeRTOS10.3.1CubeMX 自带换 10.5.1 也能用接口兼容LwIP2.1.3中间件里勾选NETCONN 模式FreeModbus1.6.0独立源码包只用 TCP 部分编译器arm-none-eabi-gcc 10.3 或 MDK AC6两个都试过差异在链接脚本段名调试手段调试器下载 串口日志 Wireshark串口日志比单步调试管用得多提示CubeH7 固件包里的 LwIP 版本跟你装的 CubeMX 版本绑定换 CubeMX 大版本后 LwIP 的lwipopts.h模板会变不要直接拿旧文件覆盖。硬件上还有两个前提必须确认PHY 的地址和复位引脚以及 ETH 参考时钟是 25MHz 还是 50MHz。这两项配错MDIO 读出来全是 0xFFFF看起来像 PHY 坏了其实是 RMII 时钟没起来。2.2 目录规划把厂商代码和你自己的代码分开我踩过的坑是一开始把 FreeModbus 源码直接扔进Middlewares/CubeMX 重新生成工程时被删了一半。后来固定成这种结构生成的代码和你手写的东西物理隔离Project/ ├── Core/ # CubeMX 生成只放初始化 ├── Drivers/ # HAL 与 CMSIS ├── Middlewares/ │ ├── Third_Party/FreeRTOS/ │ └── Third_Party/LwIP/ ├── App/ # 手写CubeMX 不会碰 │ ├── modbus/ │ │ ├── freemodbus/ # 从源码包拷过来的核心 │ │ └── port/ # 我改的 porttcp.c、portevent.c │ ├── app_modbus.c │ └── app_regs.c └── README.md # 版本表写这里FreeModbus 源码包里TCP 模式真正需要的是modbus/下的mb.c、mbtcp.c、mbfunc*.c这一组加上port/下的porttcp.c和portevent.c。portserial.c和porttimer.c在 TCP 模式下不参与编译但很多教程照着 RTU 的例子把这两个也加进去结果链接阶段报一堆未定义符号。2.3 CubeMX 里先把这四件事配好再谈移植打开 CubeMX 之后不要急着勾中间件顺序错了后面会返工。我的顺序是时钟、ETH、FreeRTOS、时基。时钟树按 HSE 25MHz、PLL 到 400MHz 或 480MHz 配我这里用 400MHzSYSCLK 400MHz、HCLK 200MHzETH 走 RMII需要 50MHz 参考时钟由 PLL2 或外部有源晶振提供。CubeMX 里 ETH 的 PHY 地址填 0自动协商勾上RX/TX 描述符数量先用默认的 4 个后面不够再改。FreeRTOS 在 Middlewares 里选 CMSIS_V2 或者 CMSIS_V1 都行我习惯 V1接口简单。关键是把USE_PREEMPTION打开、TOTAL_HEAP_SIZE先给 64KB后面看xPortGetFreeHeapSize()再调。时基这一项最容易被忽略。CubeMX 里SYS → Timebase Source默认是 SysTick但 SysTick 要被 FreeRTOS 接管HAL 的HAL_Delay()就会跟着 FreeRTOS 的调度走在中断里调用会直接死锁。常见做法是把 HAL 时基改成 TIM6 或 TIM7让两个定时器各管各的。/* stm32h7xx_it.c 里 TIM6 中断的处理保持默认即可 */ void TIM6_DAC_IRQHandler(void) { HAL_TIM_IRQHandler(htim6); }这段是 CubeMX 自动生成的不用改。逻辑很直白HAL 的 tick 由 TIM6 提供FreeRTOS 的 tick 由 SysTick 提供两边互不干扰。参数上 TIM6 的预分频和重装载值由 CubeMX 按时钟算出只要保证 1ms 中断一次就行我一般看一眼htim6.Init.Prescaler和Period乘积是不是等于当前 APB 时钟除以 1000。配完这四件事先别急着加 LwIP编译下载一次确认能跑进main的while(1)串口能打出第一行日志。这个“最小可运行骨架”是后面所有排查的基线没有它后面出的问题你分不清是协议栈的还是时钟的。3. 移植 FreeRTOS 与接入 LwIP先立任务再通网口骨架有了接下来是把 RTOS 和网络栈立起来。这两块一起讲是因为 LwIP 在 H7 上必须跑在 FreeRTOS 线程里两者的优先级和中断配置是耦合的拆开讲反而容易配错。3.1 FreeRTOSConfig.h 里必须改的几项CubeMX 生成的FreeRTOSConfig.h大部分能用但有几项跟 H7 的 Cortex-M7 中断模型直接相关不改必翻车/* Cortex-M7 有 4 位优先级共 16 级 */ #define configPRIO_BITS 4 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 /* 内核与系统调用可用的最高优先级左移到高 4 位 */ #define configKERNEL_INTERRUPT_PRIORITY \ (configLIBRARY_LOWEST_INTERRUPT_PRIORITY (8 - configPRIO_BITS)) #define configMAX_SYSCALL_INTERRUPT_PRIORITY \ (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY (8 - configPRIO_BITS)) #define configTOTAL_HEAP_SIZE ((size_t)(96 * 1024)) #define configMAX_PRIORITIES 7 #define configUSE_TIME_SLICING 1 #define configCHECK_FOR_STACK_OVERFLOW 2逻辑说明configMAX_SYSCALL_INTERRUPT_PRIORITY是分水岭数值上比它小优先级更高的中断不允许调用任何FromISR结尾的 API调了就是 hardfault。configCHECK_FOR_STACK_OVERFLOW设成 2 会在任务切换时检查栈底填充字配合自己实现的vApplicationStackOverflowHook能第一时间抓到哪个任务栈溢出了。堆大小 96KB 是我这边的经验值LwIP 自己在lwipopts.h里还有一块独立内存池FreeRTOS 堆主要给任务栈、队列和netconn结构体。任务栈我一般给 Modbus 任务 2KB、LwIP 线程 3KB、应用任务 4KB加起来不到 10KB剩下的留给动态分配。3.2 中断优先级分组与 ETH 中断的落点Cortex-M7 的中断优先级分组必须在HAL_Init()里设成 4 组也就是全部 4 位都给抢占优先级没有子优先级。CubeMX 默认就是这么配的但如果你从别的工程拷了HAL_Init进来一定要确认这一行在HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);然后 ETH 中断的优先级必须设置成数值大于等于 5比如 6 或 7这样它才能调用FromISR版本的 API。LwIP 在ethernetif.c里通过信号量通知接收线程这个信号量的 give 操作就是在 ETH 中断里做的。/* ethernetif.c 的接收线程等信号量而不是轮询 */ static void ethernetif_input(void *argument) { struct pbuf *p; for (;;) { if (sys_arch_sem_wait(s_xSemaphore, 0) 0) { do { p low_level_input(gnetif); if (p NULL) break; if (gnetif.input(p, gnetif) ! ERR_OK) { pbuf_free(p); } } while (1); } } }这里sys_arch_sem_wait的超时参数给 0 表示永久等待只有中断给了信号量才醒。如果你发现网络吞吐上不去、CPU 占用却很高八成是这里用了轮询加延时改成信号量驱动立刻就好。3.3 MPU 与 D-CacheLwIP 的缓冲区放在哪这是整篇文章里最关键的一节。H7 的 L1 D-Cache 默认是开的main里调了SCB_EnableDCache()之后CPU 访问内存先走 cache。但 ETH 的 DMA 是直接往物理内存写的不经过 cache。结果就是DMA 把帧写进接收缓冲CPU 从 cache 里读到的是旧数据或者 CPU 写好的发送缓冲还在 cache 里没刷到内存DMA 发出去的是旧内容。解决办法是在 MPU 里划一块 non-cacheable 区域把 ETH 描述符和 LwIP 的收发缓冲都放进去。我一般放在 D2 SRAM地址0x30000000开始大小 32KBstatic void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct {0}; HAL_MPU_Disable(); /* Region 0: 0x30000000 起 32KB关闭 cache共享可缓冲 */ MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.Number MPU_REGION_NUMBER0; MPU_InitStruct.BaseAddress 0x30000000; MPU_InitStruct.Size MPU_REGION_SIZE_32KB; MPU_InitStruct.SubRegionDisable 0x0; MPU_InitStruct.TypeExtField MPU_TEX_LEVEL1; MPU_InitStruct.AccessPermission MPU_REGION_FULL_ACCESS; MPU_InitStruct.DisableExec MPU_INSTRUCTION_ACCESS_DISABLE; MPU_InitStruct.IsShareable MPU_ACCESS_SHAREABLE; MPU_InitStruct.IsCacheable MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsBufferable MPU_ACCESS_BUFFERABLE; HAL_MPU_ConfigRegion(MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); }参数说明TypeExtField用TEX_LEVEL1配合IsCacheable NOT_CACHEABLE、IsBufferable BUFFERABLE得到的是“写通、不缓存”的属性这是 DMA 缓冲区最常见的组合。IsShareable设成 SHAREABLE 是为了让 CPU 和 DMA 之间对这块内存的顺序有一致性保证。DisableExec关掉取指防止这块区域被当成代码执行。光有 MPU 还不够链接脚本里必须把 ETH 描述符和接收池显式放进去。CubeMX 生成的.ld文件里已经有类似这样的段但很多人换编译器或者自己改链接脚本时把它弄丢了.lwip_sec (NOLOAD) : { . ABSOLUTE(0x30000000); *(.RxDecripSection) *(.TxDecripSection) *(.Rx_PoolSection) } RAM_D2对应的 C 代码里描述符数组要带 section 属性ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT] __attribute__((section(.RxDecripSection))); ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT] __attribute__((section(.TxDecripSection))); uint8_t Rx_Buff[ETH_RX_DESC_CNT][ETH_MAX_PACKET_SIZE] __attribute__((section(.Rx_PoolSection)));还有一步别漏MPU 配置必须在SCB_EnableDCache()之前调用否则已经缓存的数据不会被重新映射。main里的顺序是MPU_Config()→SCB_EnableICache()→SCB_EnableDCache()→ 外设初始化。3.4 lwipopts.h 参数与描述符数量lwipopts.h是 LwIP 的大脑参数乱给会导致内存不够或者吞吐上不去。我这边跑 Modbus TCP 用不到太复杂的场景参数给得比较保守#define MEM_SIZE (24 * 1024) /* LwIP 自己管理的堆 */ #define MEMP_NUM_PBUF 16 #define MEMP_NUM_TCP_PCB 8 #define MEMP_NUM_TCP_SEG 32 #define PBUF_POOL_SIZE 16 #define PBUF_POOL_BUFSIZE 1536 #define LWIP_NETCONN 1 /* FreeModbus 的 porttcp 依赖 */ #define LWIP_SOCKET 0 #define LWIP_TCP 1 #define LWIP_DHCP 0 /* 固定 IPDHCP 调试时再开 */ #define TCP_MSS 1460 #define TCP_SND_BUF (4 * TCP_MSS) #define TCP_WND (4 * TCP_MSS) #define TCP_SND_QUEUELEN ((4 * TCP_SND_BUF) / TCP_MSS)MEM_SIZE是 LwIP 内部mem_malloc的池子Modbus TCP 每帧最多 260 字节左右24KB 足够十几个并发连接。PBUF_POOL_SIZE和 ETH 接收描述符数量要匹配描述符给 8 个池子至少 16 个否则高流量下会丢帧。LWIP_NETCONN必须为 1FreeModbus 的porttcp.c用的是 netconn 接口关掉就编不过。ETH 描述符在 CubeMX 生成的ethernetif.c顶部#define ETH_RX_DESC_CNT 8 #define ETH_TX_DESC_CNT 8这两个数不是越大越好。8 个描述符加上 8 个 1536 字节缓冲占内存大约 12KB加上 LwIP 池 24KBD2 SRAM 的 32KB 区域刚好够。你要开到 16 个那块 MPU 区域就得扩到 64KB重新算链接脚本。3.5 任务切分与优先级分配三件套跑起来之后系统里至少有三个任务在跑LwIP 的接收线程由tcpip_init创建、Modbus 协议轮询任务、应用层任务。优先级我给成这样任务优先级栈大小职责LwIP tcpip 线程53KB收发帧、协议栈内部处理Modbus 轮询42KB调用eMBPoll()处理请求应用任务24KB更新寄存器、串口日志空闲任务0默认系统自带LwIP 线程优先级高于 Modbus 任务保证网络中断来的帧能及时被协议栈收掉Modbus 任务优先级高于应用任务保证请求响应延迟稳定在毫秒级。eMBPoll()内部是阻塞等待事件没有请求的时候这个任务基本不占 CPU所以优先级给高一点不会饿死别人。4. 移植 FreeModbus TCP把从站挂到 LwIP 的 netconn 上FreeModbus 的源码结构比较简单核心是一个状态机加一组寄存器回调跟具体网络栈的耦合全在port目录里。TCP 模式下难的不是协议本身是把porttcp.c里那套 socket 语义改成 LwIP netconn 语义。4.1 源码裁剪与 mbconfig.h从源码包拷过来之后先改mbconfig.h只留 TCP把 RTU 和 ASCII 关掉#define MB_RTU_ENABLED 0 #define MB_ASCII_ENABLED 0 #define MB_TCP_ENABLED 1 #define MB_TCP_PORT 502 #define MB_FUNC_READ_HOLDING_ENABLED 1 #define MB_FUNC_WRITE_HOLDING_ENABLED 1 #define MB_FUNC_READ_INPUT_ENABLED 1 #define MB_FUNC_WRITE_MULTIPLE_ENABLED 1 #define MB_FUNC_READ_COILS_ENABLED 1 #define MB_FUNC_WRITE_COILS_ENABLED 1只开你实际用到的功能码。全开的话 Flash 占用会多出 10KB 左右不是大事但每个功能码都对应一个回调函数没实现的回调容易在调试时被误触发。我一般先只开保持寄存器的读写跑通之后再加线圈。编译文件清单是这样的加到工程里的时候按组分开别一股脑塞进一个 groupfreemodbus/ ├── modbus/ │ ├── mb.c │ ├── mbtcp.c │ ├── mbfuncbits.c │ ├── mbfunccoils.c │ ├── mbfuncdisc.c │ ├── mbfuncholding.c │ ├── mbfuncinput.c │ └── mbutils.c └── port/ ├── porttcp.c └── portevent.cportserial.c和porttimer.c在 TCP 模式下一个符号都用不上加进去反而要你补一堆空实现。portevent.c是必须的它提供xMBPortEventPost和xMBPortEventGetTCP 模式下用 FreeRTOS 的队列实现最省事。4.2 porttcp.c 对接 LwIP netconn这是整个移植里唯一需要真正写代码的文件。FreeModbus 1.6.0 自带的porttcp.c是基于 BSD socket 的H7 上开 socket 层会多占内存我改成 netconn#include port.h #include mbconfig.h #include lwip/api.h #include lwip/opt.h static struct netconn *xListenConn NULL; static struct netconn *xClientConn NULL; static struct netbuf *xRxNetBuf NULL; BOOL xMBTCPPortInit(USHORT usTCPPort) { err_t xErr; xListenConn netconn_new(NETCONN_TCP); if (xListenConn NULL) { return FALSE; } /* 监听端口绑定到任意本地地址 */ xErr netconn_bind(xListenConn, IP_ADDR_ANY, usTCPPort); if (xErr ! ERR_OK) { netconn_delete(xListenConn); xListenConn NULL; return FALSE; } /* 只允许一个客户端排队Modbus 主站一般单连接 */ netconn_listen(xListenConn); return TRUE; }逻辑说明netconn_new(NETCONN_TCP)创建一个 TCP 控制块netconn_bind绑到 502 端口netconn_listen进入监听状态。这里没有调netconn_accept因为 FreeModbus 是轮询模型接受连接的动作放在xMBTCPPortGetRequest里做每次eMBPoll检查一次有没有新连接。接收和发送这两个函数是核心BOOL xMBTCPPortGetRequest(UCHAR **ppucMBTCPFrame, USHORT *pusTCPLength) { if (xClientConn NULL) { /* 没有客户端时非阻塞地尝试接受一个连接 */ if (netconn_accept(xListenConn, xClientConn) ! ERR_OK) { return FALSE; } netconn_set_recvtimeout(xClientConn, 1); } if (xRxNetBuf ! NULL) { netbuf_delete(xRxNetBuf); xRxNetBuf NULL; } /* 阻塞等待一帧超时 1ms交给 FreeRTOS 调度 */ if (netconn_recv(xClientConn, xRxNetBuf) ! ERR_OK) { return FALSE; } netbuf_data(xRxNetBuf, (void **)ppucMBTCPFrame, pusTCPLength); return TRUE; } BOOL xMBTCPPortSendResponse(const UCHAR *pucMBTCPFrame, USHORT usTCPLength) { struct netbuf *xTxBuf; void *pData; if (xClientConn NULL) { return FALSE; } xTxBuf netbuf_new(); if (xTxBuf NULL) { return FALSE; } pData netbuf_alloc(xTxBuf, usTCPLength); if (pData NULL) { netbuf_delete(xTxBuf); return FALSE; } MEMCPY(pData, pucMBTCPFrame, usTCPLength); if (netconn_send(xClientConn, xTxBuf) ! ERR_OK) { netbuf_delete(xTxBuf); return FALSE; } netbuf_delete(xTxBuf); return TRUE; }netconn_set_recvtimeout设成 1ms 是刻意为之Modbus 从站必须在主站超时之前响应但eMBPoll又不能永久阻塞否则应用任务没机会跑。1ms 的超时让这个任务每次醒来检查一下事件队列代价是每秒 1000 次上下文切换在 400MHz 的 H7 上几乎看不出来。4.3 寄存器回调与从站参数协议栈跑起来之后真正跟业务打交道的是回调函数。保持寄存器的读写回调长这样eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { USHORT i; USHORT *pusRegs app_get_holding_regs(); /* 指向应用层寄存器数组 */ if ((usAddress 1) || (usAddress usNRegs APP_REG_NUM 1)) { return MB_ENOREG; /* 地址越界回异常码 02 */ } if (eMode MB_REG_READ) { for (i 0; i usNRegs; i) { /* Modbus 是大端H7 是小端高低字节要换 */ *pucRegBuffer (UCHAR)(pusRegs[usAddress - 1 i] 8); *pucRegBuffer (UCHAR)(pusRegs[usAddress - 1 i] 0xFF); } } else { for (i 0; i usNRegs; i) { pusRegs[usAddress - 1 i] (USHORT)(*pucRegBuffer 8); pusRegs[usAddress - 1 i] | (USHORT)(*pucRegBuffer); } } return MB_ENOERR; }usAddress是从 1 开始的数组下标从 0 开始所以统一减 1。字节序必须手动换LwIP 的htons只处理协议头寄存器数据是应用层的事。地址越界返回MB_ENOREGFreeModbus 会自动组异常响应主站那边看到的就是异常码 02比直接不回包好排查得多。从站地址在 TCP 模式下其实不参与寻址但报文里还带着单元标识符我给固定值 1跟主站配置对上就行。初始化顺序是eMBTCPInit(MB_TCP_PORT); /* 内部会调 xMBTCPPortInit */ eMBEnable(); /* 状态机进入就绪 */ /* 之后在任务循环里不断调用 */ eMBPoll();eMBTCPInit返回MB_ENOERR才算绑定成功返回MB_EPORTERR就是内存不够或者 netconn 创建失败。我一般在这里加一行串口打印把返回值打出来比单步调试快。4.4 轮询周期与线程安全eMBPoll()放在独立任务里跑任务体的结构很简单static void vModbusTask(void *pvParameters) { for (;;) { eMBPoll(); /* 内部处理接收、解析、回包 */ vTaskDelay(pdMS_TO_TICKS(2)); } }延时给 2ms 是权衡给 0 会让这个任务占满优先级 4 的 CPU 时间应用任务只能等它主动让出给 10ms 又会让主站的 100ms 超时余量变薄。2ms 到 5ms 之间都行看你的主站扫描周期。线程安全这块要说清楚应用任务写寄存器、Modbus 任务读寄存器两边会打架。最省事的做法是给寄存器数组加一把互斥锁但 Modbus 回调在协议栈任务上下文里执行加锁要考虑不能在中断里调用。更轻的做法是寄存器全部用uint16_t单个 16 位读写在 Cortex-M7 上是原子的只有多寄存器批量写的时候才需要临界区taskENTER_CRITICAL(); /* 批量更新寄存器 */ taskEXIT_CRITICAL();临界区里不要放耗时操作超过几十微秒就会影响网络中断的响应。5. 常见问题排查跑通前踩过的五笔账这一章记录的都是我自己踩过、并且能复现的问题。每一条按现象、原因、解决三段写你对照着看能省下不少时间。5.1 ping 通、502 能连Modbus 读出来全是 0现象电脑 ping 板子正常用 Modbus Poll 连 502 端口提示连接成功读保持寄存器返回全 0换功能码也一样从站不报异常。原因D-Cache 没关。CPU 读到的接收缓冲是缓存里的旧数据协议栈解析出来的报文长度和内容都不对但 LwIP 层不报错所以连接看起来是好的。另一种可能是寄存器回调里的字节序没换读出来是 0x0000 或者字节颠倒。解决先确认 MPU 区域配了 non-cacheable再确认链接脚本把.RxDecripSection放进了那块区域。用调试器看DMARxDscrTab的地址是不是0x30000000附近如果不是说明链接脚本没生效。字节序的问题在回调里打印原始pucRegBuffer前四个字节就能看出来。5.2 一收包就进 HardFault现象板子上电正常主站一发请求程序直接跳 HardFault串口最后一行日志停在 ETH 中断里。原因ETH 中断的优先级数值小于 5在中断里调用了FromISR版本的信号量释放函数。Cortex-M7 对这条规则执行得很严格违反了直接升级成 HardFault不会给你任何提示。解决在 CubeMX 的 NVIC 配置里把 ETH 中断优先级改成 6 或 7重新生成。同时检查所有外设中断凡是会调用 FreeRTOS API 的优先级数值都必须大于等于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY。5.3 连接建立后几秒钟就断开现象Modbus Poll 连上之后能读一两次然后提示连接超时重新连接又能读一两次。原因xMBTCPPortGetRequest里的netconn_recv超时设得太短或者eMBPoll的调用周期比主站超时还长。还有一种可能是netconn_accept之后没有正确保存客户端连接每次轮询都重新 accept把老连接挤掉了。解决接收超时给 1ms 到 5mseMBPoll的调用周期给 2ms 左右。accept 只在xClientConn NULL的时候调一次连接建立后不要重复调用。5.4 跑几个小时就死机重启后正常现象短时间测试一切正常挂一晚上第二天看板子没响应复位后又好了。原因内存泄漏。netconn_recv拿到的netbuf没有netbuf_delete或者发送失败的分支里netbuf_new出来的结构体没释放。FreeModbus 的 TCP port 里这种分支特别多每一条return FALSE前面都要检查有没有申请过资源。解决在xMBTCPPortGetRequest开头先释放上一次的xRxNetBuf发送函数里每个失败分支都加netbuf_delete。跑之前把MEM_SIZE调大一点做对照如果调大之后存活时间变长基本可以确认是泄漏。用mem_malloc的统计接口定期打印剩余内存能更早发现。5.5 编译报 RAM 区域溢出删代码也没用现象加了 LwIP 之后链接报region RAM overflowed by xxx bytes把应用代码删掉一大半还是溢出。原因H7 的 RAM 分了好几块DTCM、AXI SRAM、D2 SRAM 各自独立链接脚本默认只把.data和.bss放在 DTCM 或者 AXI SRAM 里而 LwIP 的缓冲被强制放到了 D2 SRAM那块区域大小有限。你删应用代码释放的是 AXI SRAM跟溢出的区域不是同一块。解决打开.map文件看是哪一段溢出了。如果是RAM_D2就减小 ETH 描述符数量或者 LwIP 的MEM_SIZE如果是RAMAXI就把大的常量数组挪到 Flash或者把不常用的缓冲手动指定到 D3 SRAM。改链接脚本之前先备份MEMORY段的长度改错会让程序连启动都启动不了。6. 装完怎么验证三板斧从链路层一路验到应用层移植完成不等于能用必须按层验证。我的习惯是从下往上打三层任何一层不过就停在那里查不要跳过去看上层不然出了问题分不清是谁的锅。第一板斧是链路层。板子上电网口灯亮不亮交换机端口有没有协商到 100M 全双工。用ifconfig或者路由器后台看有没有拿到 IP我给的是静态地址192.168.1.100掩码255.255.255.0。命令行里直接ping -c 20 192.168.1.10020 个包不能丢延迟稳定在 1ms 以内。丢包或者延迟忽高忽低说明描述符数量不够或者中断优先级配错了回到第 3 章查。第二板斧是传输层。用telnet 192.168.1.100 502或者nc -vz 192.168.1.100 502确认端口能连上。连上之后不要急着关同时开 Wireshark 抓包过滤器写tcp.port 502看三次握手是不是正常完成有没有 RST。有 RST 通常是netconn的生命周期管理有问题连接被意外释放了。第三板斧是应用层。我一般用命令行工具mbpoll比图形界面的 Modbus Poll 更适合写进脚本# -m tcp 走 Modbus TCP-a 1 从站地址-t 4 保持寄存器 # -r 1 起始地址-c 10 读 10 个-1 表示从地址 1 开始不是 0 mbpoll -m tcp -a 1 -t 4 -r 1 -c 10 -1 192.168.1.100 -p 502参数说明-t 4是保持寄存器-t 3是输入寄存器-t 1是线圈-r和-c决定读哪一段-1这个开关很多人漏掉不加的话它按协议地址 0 开始算读出来的数据整体偏移一位。读到的值应该跟你代码里初始化的数组一致我一般初始化成0x0001到0x000A读出来对不上就回头查字节序。三层都过之后还有一件事要做连续跑。让mbpoll每隔 200ms 读一次挂在那儿跑两小时同时串口每隔 10 秒打印一次xPortGetFreeHeapSize()和mem_get_free()。两个数字都应该稳定在某个值附近小幅波动如果持续下降就是第 5.4 条说的内存泄漏别等它挂。还有个小技巧把eMBPoll()的返回值忽略掉是常规做法但调试阶段可以在里面加一行统计记录收到多少帧、回包多少帧、异常多少帧串口打出来。主站发了 1000 次请求从站回了 998 次差的 2 次是丢包还是超时一看数字就知道往哪个方向查。从那以后我每次移植这套组合都会先按第 6 章这三层跑一遍再交给别人哪怕时间再紧也不跳步。D-Cache 和中断优先级这两个坑吃过一次就够了第二次还栽在上面只能怪自己。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑