STM32F407平台FX3U兼容源码通讯故障定位与移植避坑

发布时间:2026/10/30 10:20:42
STM32F407平台FX3U兼容源码通讯故障定位与移植避坑

简介:面向三菱FX3U兼容开发与STM32嵌入式工控学习的底层源码包,基于STM32F407平台构建,采用全新程序架构,指令集较完整且注释详细,适合希望研究PLC底层实现、以太网与4G模块接入及串口协议扩展的开发者参考。包内共7个文件,以txt文本说明、doc文档、html页面和jpg示意图为主,压缩包约423KB,内容围绕源码版本说明、功能更新与模块应用展开。资源记录了V50.0版的多项改进:修复D8000~D255在线监视异常及监视卡死问题,新增一路485口并支持编程口协议与Modbus RTU按D8120切换,还补充ZCPP、MOVP、FMOVP、XCHP、ADDP、DIVP、SFTR、WSFR等120多条指令。已有129人学习,可帮助读者理解FX3U指令扩展、通信口配置与STM32F407底层移植思路。

1. 网口灯亮着,ping 却一个包都不回:这类底层故障为什么最费时间

板子上电,RJ45 的 Link 灯稳稳亮着,Act 灯偶尔闪一下,看着一切正常,可 ping 就是 100% 丢包——这是我第一次把基于 STM32F407 的 FX3U 兼容底层源码烧进自制板时遇到的场景。以太网、4G 模块这类外设,硬件灯亮不等于链路通,链路通不等于协议栈跑起来,协议栈跑起来也不等于 PLC 的通讯指令真能读到数据,中间隔着 PHY 时序、DMA 描述符、内存池、中断优先级四道关。FX3U 兼容方案里的通讯类故障之所以折腾人,是因为它把三层的责任叠在一起:指令层、协议栈层、芯片外设层,任何一层出问题,现象都长得一模一样——连不上、读不到数、超时。这份 FX3U V50.0 源码跑在 STM32F407 上,架构重写过,指令表全,注释也写得细,但真让人卡住的从来不是看不懂代码,而是不知道从哪一层下手。下面按我踩坑的先后顺序拆开讲。

2. 以太网连不上:PHY、协议栈、软元件三层定位法

以太网在 FX3U 兼容方案里一般承担三件事:编程口下载、Modbus TCP 服务端、以及和 4G 模块共存的透传通道。它出问题时最忌讳的做法是从协议栈代码往下读,因为绝大多数"连不上"根本不在协议栈里。我习惯的切法是自下而上打三个点:PHY 寄存器能不能读到、LwIP 的池子和描述符够不够、PLC 侧的软元件映射对不对。三个点各自有独立的验证手段,任何一个点不通,上面两个点都不用看。

顺序不能反。曾经有一次我在协议栈里加日志查了整整一个下午,最后发现是板子上 PHY 地址的 strap 电阻贴错了,MDIO 从头到尾没收到过回应。从那以后我坚持先读寄存器,三行代码能确认的事不做推理。

2.1 先看 PHY:三个寄存器判定链路死在哪一档

STM32F407 通过 SMI(MDC/MDC 两根线)访问 PHY 的寄存器,读寄存器本身用位翻转或者硬件 MAC 的 MII 接口都行。关键是读哪几个、读到什么值代表什么状态。我固定读 PHY ID1、PHY ID2 和 BMSR,前两个判断"有没有人应答",后一个判断"链路起没起来"。

/* 通过 SMI 读 PHY 寄存器,一次性判定链路处于哪一档 */ static uint16_t phy_read(uint8_t phy_addr, uint8_t reg); /* 位翻转实现,略 */ void eth_link_check(void) { /* 0x02/0x03 是 PHY ID,厂商固化,读出来全 0 或全 1 都说明通信异常 */ uint16_t id1 = phy_read(PHY_ADDR, 0x02); uint16_t id2 = phy_read(PHY_ADDR, 0x03); /* 0x01 是 BMSR,bit2 = Link Status,注意这一位是 latch-low */ uint16_t bmsr = phy_read(PHY_ADDR, 0x01); uint16_t bmsr2 = phy_read(PHY_ADDR, 0x01); /* 必须复读一次才拿到当前值 */ if (id1 == 0xFFFF && id2 == 0xFFFF) { /* 典型现象:MDIO 无人应答。先查 PHYAD 上拉,再查复位脚时序 */ fault_log(FAULT_PHY_NO_ACK, 0); return; } if ((bmsr & 0x0004) == 0) { /* 有 PHY 但无链路:查变压器中心抽头、RJ45、对端自协商 */ fault_log(FAULT_LINK_DOWN, 0); return; } /* bit13 = 100Mbit 全双工,协商失败会退到 10M,吞吐会掉一个量级 */ fault_log(FAULT_NONE, (bmsr2 & 0x2000) ? 100 : 10); }

这段代码里三个参数值得解释。PHY_ADDR由芯片上电瞬间的 RXER/CRS 等引脚电平锁定,硬件拉错就永远是 0xFFFF,改软件没用;0x01的 bit2 是 latch-low 位,读一次清零,第二次读才是实时状态,这是很多人"明明插着网线却报 Link Down"的直接原因;0x2000判断的是速率位,如果一直停在 10M,多半是变压器或者走线阻抗的问题,不是协议栈配置。

还有两个高频点。MDC 时钟不能超过 2.5MHz,F407 上按 HCLK 分频算,配大了 PHY 会间歇性不应答,现象是 PHY ID 有时读到有时读不到,非常像接触不良。RMII 的 50MHz 参考时钟必须从 PA1 外部输入,如果误配成 MCO 输出或者分频系数写错,PHY 的灯照样亮,但收发全是错帧——这种故障用示波器量 PA1 一秒就能确认。

提示:PHY ID 读出来是 0x0000 而不是 0xFFFF,说明 MDIO 被持续拉低,先查上拉电阻和总线是否短路,别怀疑芯片坏。

2.2 ping 不通但链路是 Up:LwIP 内存池与 DMA 描述符

链路起来之后下一个坑在协议栈。LwIP 的默认配置是给资源宽裕的平台准备的,直接搬到 F407 上,会出现「ping 通、TCP 连上就断」「大包传一半卡死」「跑十几分钟死机」这三类症状,每一类对应不同的参数。

/* lwipopts.h 关键项:FX3U 的 Modbus TCP 一次读写常见 200+ 字节 */ #define MEM_SIZE (16 * 1024) /* 协议栈堆,太小会在申请 pbuf 时返回 NULL */ #define PBUF_POOL_SIZE 16 /* pbuf 数量,按并发连接数 × 2 估 */ #define PBUF_POOL_BUFSIZE 1524 /* 单帧上限,别卡着 1500 省这点 RAM */ #define TCP_SND_BUF (4 * 1440) /* 发送缓冲区,小了会导致大包反复重传 */ #define TCP_WND (4 * 1440) /* 接收窗口,与 SND_BUF 保持同量级 */ #define MEMP_NUM_TCP_PCB 8 /* 同时存在的 TCP 连接数,含 TIME_WAIT */ #define ETH_RXBUFNB 4 /* DMA 接收描述符个数,双缓冲起步 */ #define ETH_TXBUFNB 4

参数对应的症状很明确:ping 通但 TCP 一建立就掉,先看MEMP_NUM_TCP_PCB,PLC 编程软件加调试助手加 4G 透传,三条连接就把 4 个 PCB 吃满了;大包传一半卡住,看TCP_SND_BUF和PBUF_POOL_BUFSIZE,窗口小于单次 Modbus 报文长度时会一直重传;跑一段时间死机,看MEM_SIZE和ETH_RXBUFNB,描述符给少了,突发帧来的时候硬件直接丢描述符,协议栈收不到任何中断。

F407 是 Cortex-M4,没有 D-Cache,所以不用操心缓存一致性,这是它比 M7 平台省事的地方。但 DMA 描述符必须放在 SRAM1/SRAM2 且四字节对齐,不能落在 CCM 那块 64KB 上——CCM 只能被内核访问,DMA 读不到。加__attribute__((aligned(4))),并且确认链接脚本没有把它分配到 CCM 段,这一步不做,现象是发送正常、接收永远是空。

2.3 PLC 侧读不到数:从 socket 到软元件的映射核对

最迷惑人的一类是「网络调试助手能连上、也能读到数据,但 PLC 程序里 D100 一直是 0」。这时候网络层是好的,问题在报文到软元件的那一段映射上。我通常按四步核对:功能码、基址、字节序、软元件分区。

/* Modbus 寄存器号 → PLC 软元件地址,基址错一位是最常见的"读得到但值不对" */ #define MODBUS_D_BASE 0x0000 /* D0 对应保持寄存器 0,报文里的地址是 0-based */ #define MODBUS_M_BASE 0x2000 /* M0 起始,按字打包 */ #define MODBUS_X_BASE 0x4000 static uint16_t *modbus_reg_to_dev(uint16_t reg, uint16_t *max_len) { if (reg < MODBUS_M_BASE) { *max_len = D_REG_NUM; return &D_reg[reg]; /* reg 直接用,不要再减 1 */ } if (reg < MODBUS_X_BASE) { *max_len = M_REG_NUM / 16; return &M_reg[(reg - MODBUS_M_BASE) * 2]; } *max_len = 0; return NULL; /* 越界返回 NULL,调用方必须判空 */ }

这里的坑集中在两处。基址:D100 在报文里到底是 99 还是 100,不同上位机给的偏移不一样,差一位的现象就是「值能读到,但整体偏移了一个寄存器」,看数据像乱码,其实是错位。功能码:03 读保持寄存器、04 读输入寄存器,服务端如果只实现了 03,用 04 去读会返回异常码 0x02,而有些调试工具把异常码吞掉了,看起来像超时。

快速定位可以靠一张对照表,按现象锁定层,别一上来就翻协议栈:

现象首查层验证动作
Link 灯不亮硬件 / PHY读 BMSR bit2
PHY ID 读出全 FSMI / 地址 strap核对 PHYAD 上拉与复位时序
ping 通、TCP 连不上协议栈池看 MEMP_NUM_TCP_PCB 是否耗尽
能连、读回全 0软元件映射用调试助手读同一地址比对
跑几分钟就断内存 / 描述符打印 memp 统计与丢包计数

3. 4G 模块拨号失败:AT 指令到底卡在哪一步

4G 模块在 FX3U 兼容方案里当远传通道用,拨号失败是最难查的一类,因为它把三件事串成了一条链:串口能不能完整收到模组的回复、AT 状态机有没有走完、运营商网络有没有附着成功。链上任何一环断掉,最终现象都是「连不上服务器」,而日志里往往只有一句干巴巴的 ERROR。

我的做法是把这条链拆成可观测的三段:先保证串口收帧不出错,再把拨号过程做成带超时和重试的状态机,最后用正则表达式把 AT 日志里的失败点捞出来。三段都做完,一次拨号失败通常五分钟内能定位到具体指令。

3.1 串口收不全:DMA 加空闲中断的正确写法

AT 指令的回复是不定长文本,用「收一个字节进一次中断」的写法,波特率一高就丢字节;用「定长接收」的写法,长度对不上就切不开帧。工业现场更实际的做法是 DMA 搬运加 IDLE 空闲中断切帧,DMA 负责不丢字节,IDLE 负责判断一帧结束。

/* USART1 + DMA1_Stream5 接收,IDLE 中断切帧,适合 AT 这类不定长文本 */ #define AT_BUF_SIZE 512 static uint8_t at_dma_buf[AT_BUF_SIZE] __attribute__((aligned(4))); static uint8_t at_frame[AT_BUF_SIZE]; static volatile uint16_t at_frame_len; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) { /* 清 IDLE 标志的标准动作:先读 SR 再读 DR,少一步标志清不掉 */ volatile uint32_t tmp = USART1->SR; tmp = USART1->DR; (void)tmp; uint16_t len = AT_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Stream5); if (len > 0 && len < AT_BUF_SIZE) { memcpy(at_frame, at_dma_buf, len); /* 拷出来,别在中断里解析 */ at_frame_len = len; } /* 重启 DMA 必须按 DISABLE → 设计数 → ENABLE 的顺序,反过来计数器不生效 */ DMA_Cmd(DMA1_Stream5, DISABLE); DMA_SetCurrDataCounter(DMA1_Stream5, AT_BUF_SIZE); DMA_Cmd(DMA1_Stream5, ENABLE); } }

三个参数各对应一类故障。AT_BUF_SIZE要大于单条最长响应,查询类指令一般不到 100 字节,但透传模式下一帧可能几百字节,卡着 128 写会在透传时截断;读 SR 再读 DR 这两步是硬件要求,漏了会导致 IDLE 中断反复触发,看起来像串口在刷屏;DMA 重启的三步顺序反了,计数器保持旧值,下一次收帧长度就永远是 0。

排查时看两个指标。at_frame_len一直是 0,说明 IDLE 中断压根没进,去查 NVIC 使能和USART_CR1的 IDLEIE 位;at_frame_len偶尔比预期少一两个字节,基本都是波特率误差,F407 在 8 倍过采样下跑 115200 没问题,跑到 921600 就得按 PCLK 重新算 BRR,误差超过 2% 就会间歇性丢位。

3.2 拨号状态机:超时、重试与信号质量

拿到完整的 AT 回复之后,拨号的本质就是一个带超时的状态机。写死顺序执行、每步while死等的代码在现场必翻车,因为模组上电到能响应 AT 需要几秒,搜网时间又取决于信号,任何一步卡死都会把整个扫描周期拖垮。

typedef enum { AT_IDLE, AT_SYNC, AT_SIM, AT_CSQ, AT_ATTACH, AT_PDP, AT_READY, AT_FAIL } at_state_t; /* 每步一条指令 + 一个期望应答 + 超时时间,表驱动比 if-else 好维护 */ static const struct { const char *cmd; const char *expect; uint16_t timeout_ms; } at_step[] = { [AT_SYNC] = { "AT\r\n", "OK", 500 }, [AT_SIM] = { "AT+CPIN?\r\n", "+CPIN: READY", 2000 }, [AT_CSQ] = { "AT+CSQ\r\n", "+CSQ:", 2000 }, [AT_ATTACH] = { "AT+CEREG?\r\n", "0,1", 3000 }, [AT_PDP] = { "AT+CGACT=1,1\r\n", "OK", 5000 }, };

timeout_ms的取值有讲究:同步指令给 500ms 足够,SIM 卡检测给 2s,PDP 激活给 5s。模组在弱信号下激活 PDP 可能要十几秒,超时给太短会误判成失败,然后不停重试,反而把模组拖进更差的状态。重试策略我一般设成单步 3 次,整轮失败 2 次就拉 PWRKEY 硬复位模组(低电平保持 500ms 以上再放开),比继续发指令有效。

信号质量是最容易被忽略的判断依据。AT+CSQ返回的第一个数低于 10,PDP 激活失败是常态,不是代码写错了;返回 99 表示未知,通常意味着天线没接好或者频段不匹配。AT+CEREG?长时间停在0,2(正在搜网),说明根本没附着上,这时候再怎么改 APN 都没用。

注意:模组上电瞬间的峰值电流能到 2A,供电走线细或者共用一路 LDO,会表现为周期性掉网——每隔几十秒断一次,日志里全是重新拨号,看着特别像协议栈的锅。

3.3 用正则表达式从 AT 日志里捞失败点

现场抓回来的 AT 日志动辄上千行,混着模组回显、URC 主动上报和我们的指令,肉眼找失败点效率极低。这一步我固定用正则表达式过滤,一次把所有错误线索捞干净。

# 从 at_log.txt 里一次捞全失败线索: # ^\s*\+CME ERROR: 模组回报的错误码 # ^\s*\+CMS ERROR: SIM 相关错误 # NO CARRIER 拨号被对端断开 grep -nP '^\s*(\+CM[SE] ERROR: *\d+|ERROR|NO CARRIER)' at_log.txt

这条正则的几个部分各有用途。^配合grep -P的多行模式让每行都算行首,避免把回显里的字符串也匹配上;\s*吃掉模组回复里常见的前导空格,不同厂商的模组这里空格数量不一样;\+转义加号,加号在正则里是量词,不转义会匹配出莫名其妙的结果;CM[SE]用一个字符类同时覆盖 CME 和 CMS 两类错误码;\d+只取数字部分,方便后续统计。

如果想把错误码做成频次统计,看哪个错得最多,用一段 Python 更省事:

import re, collections # 一条正则同时兼容 "+CME ERROR: 3" / "+CME ERROR:3" / "+CME ERROR: 3,xx" pat = re.compile(r'^\s*\+CM[SE] ERROR:\s*(\d+)', re.M) text = open('at_log.txt', encoding='utf-8', errors='ignore').read() for code, n in collections.Counter(pat.findall(text)).most_common(): print(f'error {code:>3} x{n}')

re.M让^匹配每一行的行首而不是整个文本的开头,这是最关键的一个标志位,漏了就只能匹配到第一条。(\d+)是捕获组,findall在有捕获组时只返回组内内容,正好是要统计的错误码。常见错误码的语义要背下来:3 是操作不允许(通常是状态不对,比如没附着就激活 PDP),10 是 SIM 未插入,13 是 SIM 失败,30 是无网络服务。

一个反过来的坑:别用.*去匹配整行内容做归类,长行上贪婪匹配会把两条相邻记录吃成一条,统计结果直接失真。定位失败点时用「行首锚定 + 精确错误码」比「模糊匹配整行」可靠得多。

4. 移植到 F407 就翻车:时钟、映射、指令解释器三个高发点

把 FX3U 兼容源码从一块板子挪到另一块 F407 板子上,编译通过是最没有信息量的信号。V50.0 这套架构把指令解释器和硬件抽象分得比较开,好处是移植面小,坏处是硬件层的错误会被抽象层掩盖,表现出来全是指令行为异常。我遇到的翻车集中在三处:定时器基准、软元件映射、指令取指的边界。

4.1 定时器基准不对,所有指令时序都跟着偏

最典型的现象是定时器指令不准:程序里写T0 K100,按 100ms 时基应该是 10 秒,实际跑出来是 7 秒或者 13 秒。很多人第一反应是解释器算错了,去翻指令实现,其实根因在时基。

/* 1ms 基准:F407 主频 168MHz 时,APB1 定时器时钟为 84MHz */ void tim_base_init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); /* 84MHz / 84 = 1MHz 计数频率,即 1us 一格 */ TIM6->PSC = 84 - 1; /* 计 1000 格 = 1ms 触发一次更新中断,作为整个扫描周期的时基 */ TIM6->ARR = 1000 - 1; TIM6->DIER |= TIM_DIER_UIE; TIM6->CR1 |= TIM_CR1_CEN; }

PSC要按 APB1 定时器时钟算,注意 APB1 分频系数不为 1 时,定时器时钟是 APB1 时钟的两倍,照着 APB1 直接算会差一倍。ARR决定中断频率,改了主频这两个数必须同步改,只改一个会出现「时基对但周期不对」的怪现象。

判断是不是基准问题有个很实用的办法:看偏差比例是否固定。如果 K100 稳定跑出 7 秒,10/7≈1.43,说明是固定倍率错误,问题在时钟配置;如果同样是 K100,有时 9.8 秒有时 10.6 秒,那是扫描周期抖动,方向要转到中断优先级和长任务拆分上去。这个区分能省掉大量无效排查。

4.2 软元件地址映射与掉电保持区

F407 的 RAM 分成几块,SRAM1 112KB、SRAM2 16KB、CCM 64KB,其中 CCM 只能被内核访问,DMA 碰不到。软元件区如果被链接脚本分配到 CCM,普通读写没问题,一旦走 DMA 搬运(比如通讯刷新的输入区)就读回随机值。

/* 软元件分区:掉电保持区单独放一段,普通区上电清零 */ __attribute__((section(".keep_ram"), aligned(4))) uint16_t D_keep[D_KEEP_NUM]; /* D200~D511 掉电保持 */ uint16_t D_reg[D_REG_NUM]; /* 普通数据寄存器,启动时 memset 清零 */

section(".keep_ram")这个段必须在链接脚本里有定义,没定义时链接器会给出 warning 然后把它丢到默认段,看起来编译通过,实际掉电保持完全失效。aligned(4)是为了让 DMA 和字访问都不出问题,半字对齐在批量搬运时会触发 HardFault。

还有一个容易忽略的点:普通 RAM 掉电就丢,真正的掉电保持要么靠备份寄存器(容量很小,只够存几十个字),要么外挂 EEPROM 定时刷。源码里如果有「掉电保持」的宏,先确认它背后到底是哪种实现,别默认它一定生效。

4.3 指令解释器越界:一个下标写错,整个扫描周期崩

最高频的 HardFault 来自指令取指。跳转类指令的操作数如果不做范围校验,程序计数器很容易飞到指令区之外,然后解释器拿着一个越界地址去取指,直接进 HardFault。事后查这种问题很痛苦,因为栈已经翻了。

/* 取指前先卡边界,比事后查 HardFault 便宜得多 */ static inline const instr_t *fetch_instr(uint32_t pc) { if (pc >= INSTR_MEM_WORDS) { /* PC 飞了:八成是跳转指令的操作数没校验 */ fault_log(FAULT_PC_RANGE, pc); return NULL; } return &instr_pool[pc]; }

配合一个能把栈帧打出来的 HardFault 处理函数,定位速度会快很多:

/* 从异常栈帧取 PC,配合 .map 或 addr2line 反查出事的那条指令 */ void HardFault_Handler(void) { __asm volatile ( "tst lr, #4 \n" /* 判断异常前用的是 MSP 还是 PSP */ "ite eq \n" "mrseq r0, msp \n" "mrsne r0, psp \n" "b hardfault_dump \n" ); }

tst lr, #4看的是 EXC_RETURN 的第 2 位,为 0 表示进异常前用的是主栈指针,为 1 是进程栈指针。取到栈帧基址后,偏移 0x18 处就是出错时的 PC,把它打出来再反查:

arm-none-eabi-addr2line -e build/fx3u.elf -f -C 0x08004a3c

反查出来的函数名能直接把范围缩到某几条指令的实现里。我的经验是,十次里有七八次落在跳转、变址寻址和高速计数器这几类带地址运算的指令上,共同点都是操作数来自程序区、没做校验。

5. 避坑记录:五条让我加班到凌晨的坑

下面这五条都是现场真碰到的,现象看着五花八门,拆开之后原因都很朴素。写在一起是因为它们的共同点是「第一直觉指向的地方全是错的」,容易把人带偏。

5.1 4G 每隔几十秒掉一次网

现象:拨号成功、连上服务器、传数据正常,但每隔 30 到 60 秒断一次,日志里全是重新拨号,看起来像模组不稳定。

原因:供电。模组发射瞬间的峰值电流能到 2A,共用一路 LDO 或者走线太细时电压被拉塌,模组自己复位。软件侧看不出任何异常,因为模组复位后重新初始化,一切从零开始,日志里只有「连接断开」。

解决:给模组单独一路电源,输入端并 470uF 以上的电解电容,走线宽度按峰值电流算。改完再抓一次日志,掉网周期消失,说明确实是电源。这条坑我后来写进了硬件验收清单,软件调不出来。

5.2 以太网跑几分钟就断,重启又正常

现象:上电后通讯正常,跑三到十分钟不等就彻底断,ping 不通,重新上电恢复,循环往复。

原因:DMA 接收描述符耗尽。ETH_RXBUFNB配得太少,突发帧一来硬件来不及处理就丢描述符,丢了之后没有回收逻辑,跑一段时间缓冲区全空。

解决:描述符数量翻倍到 4 组以上,并在接收中断里确认每个描述符用完都归还给 DMA。另外把 DMA 缓冲区从 CCM 段挪到 SRAM1,之前有一版被链接脚本分到 CCM,DMA 写不进去,现象也是跑一会儿就断,但根因完全不同。

5.3 注释写着 V50.0,行为却像旧版本

现象:明明换了新源码,编译烧录后某些指令的行为和旧版一致,比如新增的指令报「不支持」。

原因:板级配置头文件没跟着换。V50.0 的架构把硬件相关的东西收进了几个宏里,如果工程里还留着旧板子的配置头,编译能过,但功能开关是旧的。

解决:换源码时把整个工程目录对比一遍,重点看板级配置和链接脚本。我现在的习惯是每次导入新版本先 grep 一遍版本号宏和板型宏,确认全工程的版本标识一致,再开始编译。

5.4 下载完程序,PLC 不进 RUN

现象:程序下载成功,指示灯停在 STOP,手动切 RUN 也起不来,或者起来几秒又回 STOP。

原因:看门狗。窗口看门狗或者独立看门狗在初始化阶段就被打开,喂狗点放在扫描周期的末尾,一旦某个周期里出现长任务(比如大批量通讯),喂狗被拖后,看门狗先复位了。

解决:确认复位源,读 RCC 的复位标志,如果是看门狗复位,第一件事是把喂狗点从扫描周期末尾挪到定时器中断里,第二件事是检查有没有死循环里的阻塞等待。日志里如果能看到有规律的重启周期,基本就是这条。

5.5 串口助手看得见数据,PLC 里读不到

现象:用串口助手接同一路串口能看到完整的报文,接到 PLC 的通讯口上就是读不到,或者偶尔读到半帧。

原因:中断优先级。通讯接收中断优先级低于扫描周期定时器或者高速计数中断,高优先级中断长时间占用,接收 FIFO 溢出,硬件丢字节。

解决:把串口接收中断的抢占优先级提到高于扫描定时器,但低于真正需要硬实时的脉冲输出中断。改完用长报文压测,连发一千帧统计错误率,错误率归零才算过。

6. 五分钟缩小范围:一套固定的排查顺序

故障来的时候最耗时间的不是修,是判断该往哪看。我给自己定了一条固定路线,不管现场多乱都按这个顺序走一遍,五分钟内基本能把范围锁到一层。路线只有四步:先看复位源和电源、再看链路、再看协议栈计数、最后看软元件映射。

第一步看复位源,读 RCC 的复位标志寄存器,判断是上电复位、看门狗复位还是软件复位。这一步能看到很多「莫名其妙就好了」的真相,尤其是周期性重启类故障。第二步看链路,以太网读 PHY 的 BMSR,4G 看 CSQ 和 CEREG,两个数字就把物理层的死活说清楚了。第三步看协议栈计数,LwIP 的 memp 统计、丢包计数、连接数,跑起来之后采样两次,看有没有单调增长的计数器。第四步才轮到软元件映射和指令逻辑。

固化成一张表贴在工作台上,比记在脑子里可靠:

顺序检查项命令 / 动作判定
1复位源读 RCC_CSR看门狗复位说明有阻塞
2供电示波器量模组供电脚掉到阈值以下即电源问题
3链路读 BMSR bit2 / AT+CSQ无链路不进协议栈
4协议栈memp 统计两次采样计数只增不减即泄漏
5映射调试助手读同地址值不同即映射层问题

日志侧我固定留一条正则,编译产物里的警告和错误一次性捞干净,避免带着隐患上线:

# 编译日志里只留 warning 和 error,附带行号,方便直接跳到源码 grep -nE '(warning|error):' build/build.log | grep -vE '(unused|deprecated)' | head -50

-nE打开扩展正则并带行号,(warning|error):精确匹配编译器输出格式,grep -vE把 unused、deprecated 这类噪音过滤掉,head -50防止一次刷屏看不完。这几条组合下来,日志从几千行缩到几十行,剩下的每一条都值得看一眼。

说个我自己的教训。刚接触这套 FX3U 兼容源码的时候,我一遇到通讯故障就从协议栈往下读代码,最长的一次连着看了两天,最后发现是板子上一个上拉电阻没贴。从那以后我每次动手改代码之前,都强制走一遍上面这张表,哪怕心里已经笃定是哪一层的问题,也要让寄存器里的数字先开口。这个习惯救过我很多次,也让我少写了不少本来不需要写的代码。希望帮到你。

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