RT-Thread下FreeModbus与EasyFlash死锁排查:CmBacktrace实战定位

发布时间:2026/10/15 21:25:34
RT-Thread下FreeModbus与EasyFlash死锁排查:CmBacktrace实战定位 1. 问题现场还原与排查思路1.1 卡死现象的具体表现先说清楚我遇到的现场。项目基于 RT-Thread 操作系统跑的是 FreeModbus 协议栈做从机参数存储用的是 EasyFlash。正常运行时一切平稳但一旦触发参数写入操作比如上位机下发指令让从机保存新的波特率或从机地址系统就会在某个不确定的时刻彻底卡死。串口不再响应任何 Modbus 请求调试串口也没有任何异常打印看门狗如果没开就是无限期挂起开了就是不断复位。这种卡死最让人头疼的地方在于它的不确定性。有时候连续写十几次都没事有时候第一次写就挂。用调试器挂上去看发现主线程停在某个循环里出不来但调用栈信息因为优化等级和栈回溯的限制根本看不全。一开始怀疑是 Flash 操作时间过长导致 Modbus 超时但把超时时间放大后问题依旧说明根因不在协议层。1.2 为什么常规调试手段失效在裸机环境下HardFault 通常有明确的异常向量入口可以打印 LR、PC 等寄存器定位。但在 RT-Thread 这种多线程环境下问题变得复杂得多。线程栈是独立的异常发生时 CPU 可能正在执行任意一个线程的代码而 RT-Thread 的线程切换又会让现场变得难以还原。更麻烦的是EasyFlash 的擦写操作会关中断或者进入临界区如果此时恰好有中断触发或者线程调度被阻塞就会形成死锁。传统的 printf 调试在这里基本没用因为卡死的时候连打印都出不来。用 J-Link 单步调试又因为时序敏感往往单步能过、全速就挂典型的海森堡 bug。1.3 引入 CmBacktrace 的决策逻辑我需要一个能在现场还原调用栈的工具而不是靠猜。CmBacktrace 是 RT-Thread 社区里比较成熟的栈回溯组件它能在 HardFault 或者断言失败时自动打印出函数调用关系、当前线程信息、甚至局部变量的地址。这比单纯看寄存器值直观太多了。选它的理由很直接第一它和 RT-Thread 集成度高直接通过软件包就能拉进来第二它支持 ARM Cortex-M 系列的内核正好匹配我用的芯片第三它输出的信息足够详细能直接定位到是哪个函数、哪一行触发了异常。相比自己写 HardFault 处理函数去手动解析栈帧CmBacktrace 省去了大量底层汇编的适配工作。注意CmBacktrace 依赖编译时生成的符号表文件通常是 .axf 或 .elf如果编译时开了高等级优化函数可能被内联栈回溯的准确度会下降。建议在排查阶段把优化等级降到 -O0 或 -Og。2. CmBacktrace 的移植与配置要点2.1 在 RT-Thread 中集成 CmBacktrace集成过程本身不复杂但有几个坑点需要提前说清楚。在 RT-Thread Studio 或者 Env 工具里通过 menuconfig 进入软件包列表找到 CmBacktrace 并勾选。它会自动把源码加入工程但串口输出设备需要手动指定。/* 在 rtconfig.h 中确认以下宏定义 */ #define PKG_USING_CMBACKTRACE #define CMBACKTRACE_USING_DEVICE #define CMBACKTRACE_USING_DEVICE_NAME uart1这里的uart1要换成你实际用于调试打印的串口设备名。如果写错了异常发生时什么都不会输出你会以为 CmBacktrace 没生效其实是打印通道没配对。另外CmBacktrace 需要一个非易失存储区域来保存异常现场方便复位后读取。通常做法是在 Flash 中划出一小块区域通过 FAL 分区表来管理。如果项目里已经用了 EasyFlash可以直接复用它的分区但要注意不要和参数存储区冲突。2.2 关键配置项与参数解读CmBacktrace 的配置项不多但每一个都影响回溯效果。下面这张表是我实际调试后总结的推荐值配置项推荐值说明CMBACKTRACE_PRINTF_LINE1打印行号定位更精确CMBACKTRACE_PRINTF_MSG1打印自定义断言信息CMBACKTRACE_PRINTF_ELF1打印 ELF 文件路径方便 addr2lineCMBACKTRACE_PRINTF_STACK1打印栈内容用于深度分析CMBACKTRACE_USING_OSRT-Thread指定操作系统类型其中CMBACKTRACE_PRINTF_STACK会输出一大段十六进制数据看起来乱但当你需要分析栈溢出或者野指针时这些原始数据就是救命稻草。我一般只在深度排查时打开平时关掉以减少串口输出量。2.3 符号表与地址解析的配合CmBacktrace 输出的调用栈是一串地址比如0x08001A3C你需要用工具把这些地址翻译成函数名和行号。RT-Thread 提供了addr2line工具在 Env 环境下可以直接调用addr2line -e rtthread.elf -f -p 0x08001A3C输出会类似HardFault_Handler at .../context_rvds.S:123这样你就知道异常发生在哪个函数了。如果地址解析出来是??说明符号表不匹配大概率是编译后没有重新生成 elf 文件或者烧录的固件和 elf 不是同一次编译的产物。实操心得每次修改代码后务必保存对应的 elf 文件并按版本号归档。我吃过亏现场抓到的地址用新编译的 elf 去解析结果完全对不上白白浪费了半天时间。3. 卡死根因的定位与代码分析3.1 抓取异常现场的第一手数据配置好 CmBacktrace 后重新复现卡死。这次串口输出了关键信息[HardFault] Current thread: tshell Stack frame: 0x0800A1B2 0x0800C3D4 0x0800E5F6 ...看到Current thread: tshell时我愣了一下因为 Modbus 从机线程是独立的为什么异常发生在 shell 线程继续往下看调用栈发现最顶层的地址解析后指向easyflash_env_set函数再往下是fal_flash_write然后是rt_mutex_take。这就很清楚了EasyFlash 在写入时尝试获取一个互斥量但这个互斥量已经被其他线程持有且持有者无法释放。而 shell 线程恰好也在操作 EasyFlash两者形成了竞争。3.2 FreeModbus 与 EasyFlash 的线程竞争分析FreeModbus 的从机线程在收到写参数指令后会调用 EasyFlash 的 API 保存数据。EasyFlash 内部使用 RT-Thread 的互斥量来保护 Flash 操作。问题在于FreeModbus 的线程优先级和 shell 线程优先级设置不当。我的配置里Modbus 线程优先级是 12shell 线程是 20数值越小优先级越高。正常情况下 Modbus 线程会抢占 shell 线程不会出问题。但 EasyFlash 在擦除 Flash 时会关闭中断并进入临界区此时如果 shell 线程正在等待同一个互斥量而 Modbus 线程又在临界区内等待一个被 shell 线程持有的资源就形成了死锁。更隐蔽的是EasyFlash 的env_set操作不是原子的它可能分多次写入。如果在两次写入之间发生了线程切换而切换后的线程又去读同一个环境变量就会读到中间状态触发断言失败进而进入 HardFault。3.3 从调用栈到代码行的精确定位用 addr2line 解析出具体行号后我定位到 EasyFlash 的ef_env_set函数中有一处/* 伪代码示意 */ rt_mutex_take(ef_lock, RT_WAITING_FOREVER); /* 擦除操作 */ fal_flash_erase(...); /* 写入操作 */ fal_flash_write(...); rt_mutex_unlock(ef_lock);问题出在RT_WAITING_FOREVER上。如果互斥量因为某种原因没有被释放这里就会永久阻塞。而 FreeModbus 线程在等待这个互斥量时又持有 Modbus 协议栈的内部锁导致整个协议栈瘫痪。进一步排查发现shell 线程在某个命令中调用了ef_get_env而ef_get_env内部也去拿同一个互斥量。但 shell 线程在拿锁之前已经持有了控制台输出的锁。Modbus 线程在写 Flash 时如果恰好触发了日志输出就会去拿控制台锁形成AB-BA 死锁。4. 解决方案与代码修改实录4.1 互斥量超时机制的引入最直接的修复是给所有 EasyFlash 的互斥量操作加上超时而不是RT_WAITING_FOREVER。修改后的代码if (rt_mutex_take(ef_lock, rt_tick_from_millisecond(500)) ! RT_EOK) { rt_kprintf(EasyFlash lock timeout!\n); return -RT_ETIMEOUT; }这样即使发生竞争也不会永久阻塞而是返回错误码让上层决定重试还是放弃。500ms 这个值是根据 Flash 擦除时间估算的一般扇区擦除在几十毫秒量级留足余量即可。注意超时时间不能设得太短否则在 Flash 操作繁忙时会频繁超时影响正常功能。也不能太长否则失去了防止死锁的意义。4.2 线程优先级与临界区的调整光加超时还不够得从根源上减少竞争。我把 Modbus 线程的优先级调整为 8shell 线程调整为 15确保 Modbus 操作 Flash 时不会被 shell 打断。同时在 EasyFlash 的擦写操作外层用rt_enter_critical和rt_exit_critical包起来禁止线程调度rt_enter_critical(); ef_set_env(baudrate, 115200); rt_exit_critical();但这里有个权衡临界区太长会影响系统实时性。所以我把一次大的写入拆成多次小的写入每次写入后退出临界区让其他线程有机会运行。4.3 参数写入的原子性保障EasyFlash 本身提供了ef_set_env的原子性保证但前提是调用者不并发操作。我在应用层加了一个写入队列所有参数修改请求先入队由一个专门的低优先级线程串行处理。这样彻底避免了多线程同时写 Flash 的情况。/* 简化的队列处理逻辑 */ while (1) { if (rt_mb_recv(ef_mb, msg, RT_WAITING_FOREVER) RT_EOK) { rt_mutex_take(ef_lock, RT_WAITING_FOREVER); ef_set_env(msg.key, msg.value); rt_mutex_release(ef_lock); } }这个改动虽然增加了代码量但稳定性提升非常明显。实测连续写入一千次参数没有再出现卡死。4.4 验证与压力测试结果修改完成后我做了三轮测试。第一轮是正常功能测试Modbus 读写参数各一百次全部通过。第二轮是并发压力测试用脚本同时触发 shell 命令和 Modbus 写参数持续运行两小时没有死锁。第三轮是异常注入测试人为在 Flash 操作时触发中断观察系统是否能恢复结果 CmBacktrace 正确捕获了异常并打印了调用栈系统复位后正常工作。测试过程中CmBacktrace 的输出帮我确认了每次异常的具体位置没有它的话这些测试只能靠“跑通了”和“跑挂了”来判断效率低太多。5. 常见问题速查与避坑指南5.1 CmBacktrace 输出乱码或不输出这是最常见的问题原因通常有三个。第一串口波特率不匹配检查CMBACKTRACE_USING_DEVICE_NAME对应的设备是否初始化正确。第二中断优先级配置错误HardFault 的优先级如果低于串口中断打印会被阻塞。第三栈空间不足CmBacktrace 本身需要一定的栈来格式化输出如果异常发生时栈已经溢出它也无法工作。排查方法先在正常运行时调用cm_backtrace_init并手动触发一次cm_backtrace_assert看能否正常输出。如果正常说明配置没问题问题出在异常现场本身。5.2 地址解析出来是问号地址解析失败九成是 elf 文件不匹配。确认三点编译时是否生成了 elf 文件烧录的固件是否来自这次编译addr2line 的路径是否指向正确的 elf。如果用的是 RT-Thread Studio可以在项目属性里勾选“生成调试信息”确保-g编译选项开启。另外如果函数被内联或者优化掉了地址可能指向一个不存在的符号。这时候把优化等级降到 -O0 重新编译再复现一次通常就能解析出正确的函数名。5.3 卡死问题反复出现但调用栈不同这种情况说明系统存在多个竞争点不是单一 bug。我的经验是先把所有涉及共享资源的操作列出来包括 Flash、串口、全局变量、互斥量然后逐一检查是否有嵌套加锁的情况。特别要注意在持有锁的时候调用可能阻塞的函数比如rt_kprintf、rt_malloc、rt_thread_delay这些都是死锁的常见诱因。如果资源太多理不清可以先用 CmBacktrace 抓几次异常看看调用栈有没有共同的函数。通常竞争会集中在某几个热点函数上优先解决这些问题就能缓解大半。5.4 EasyFlash 写入耗时过长导致 Modbus 超时EasyFlash 的擦除操作在有些 Flash 芯片上需要几百毫秒而 Modbus 的超时时间通常只有几十毫秒。这时候即使没有死锁也会因为响应太慢而被上位机判定为超时。解决办法有两个一是把参数写入放到后台线程Modbus 线程立即返回“写入中”的状态写完后再通知二是使用 EasyFlash 的ef_set_env_blob批量写入减少擦除次数。我采用的是第一种方案配合一个状态机来管理写入进度上位机轮询查询写入结果。5.5 常见问题速查表现象可能原因排查方法解决措施CmBacktrace 无输出串口设备名错误检查 rtconfig.h修改为正确设备名地址解析为 ??elf 不匹配核对编译时间重新编译并保存 elf卡死在互斥量死锁查看调用栈加超时、调整优先级写入后系统复位栈溢出检查线程栈大小增大栈或减少局部变量Modbus 超时Flash 擦除慢测量擦除时间异步写入或批量写入最后分享一个我踩过的坑CmBacktrace 的栈回溯依赖编译器生成的栈帧信息如果用了-fomit-frame-pointer优化回溯会失败。排查阶段务必关闭这个选项等定位到问题后再根据实际情况决定是否开启。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询