免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

STM32CubeIDE多工程联调实战:双MCU调试与串口通信

STM32CubeIDE多工程联调实战:双MCU调试与串口通信 LAT1480 这个项目做到联调阶段我最大的感受是STM32CubeIDE 在工程联调这件事上比 Keil 顺手了不止一个档次。工程联调如果只在单机单工程的思维里打转你会发现大量时间都耗在切换窗口、复制固件、手动对时序上调试两块 MCU 之间的通信简直像在打地鼠。这篇文章把 LAT1480 实际跑过的联调方案完整拆出来包含多工程工作区、双目标同时调试、串口联调以及几个让我差点通宵的坑。适合正在用 STM32CubeIDE 做多板、多工程、BootApp 项目的工程师也适合准备从 Keil 迁过来的人参考。1. 先把 LAT1480 的联调需求掰开揉碎1.1 这个项目为什么要搞“工程联调”LAT1480 是一台设备端主控模块整个固件被我拆成了三个工程Boot_F411主控 MCU 的引导程序负责固件升级和跳转 App。主控用的是 STM32F411CEU6。App_F411主控业务逻辑负责协议解析、命令分发还要通过 USB-TTL 和上位机通信。Sensor_F103协处理器用的是 STM32F103C8T6负责传感器采集和电控输出和主控之间走 USART 串口协议。三个工程之间有两条联调链路一是 Boot 和 App 的跳转关系地址要对得上中断向量表要切得过来二是 App 和 Sensor 之间的串口协议一帧数据从传感器端发出来到主控解析完再送到上位机整个链路都要一起跑。如果用单工程、单目标的方式开发一次只能看一个固件两个 MCU 之间的时序问题根本看不到。串口帧丢没丢、校验对不对、对方到底有没有来得及应答这些都需要两边同时暂停、同时看状态才能定位。所以联调的本质就是把多个固件放到同一个 IDE 工作区里让它们同时跑、同时停数据流彻底打通。1.2 联调环境的硬件组成与软件版本先列一下我实际用的环境方便你对照STM32CubeIDE 1.15.1版本不用完全一致1.15 以上都行。HAL 库F4 系列 1.28.0F1 系列 1.8.5。调试器两个 J-Link V11。如果你手头只有一个 J-Link 加一个 ST-Link 也完全没问题STM32CubeIDE 支持混用。目标板两块独立核心板各自引出 SWDIO、SWCLK、GND。联调之前有个细节必须先做两块目标板必须共地。如果各自用独立电源GND 不连在一起SWD 连接会间歇性失败串口更是直接出乱码。我第一次联调时忽略了这个问题浪费了半天后来用一根杜邦线把两块板的 GND 接起来所有诡异现象都消失了。2. 在同一个工作区里管理三个工程2.1 一个 Workspace 下建立多工程的正确姿势STM32CubeIDE 默认会把每个新工程放到当前 workspace 目录下。很多人在工程创建时习惯“每次新建一个 workspace”结果开发机上一堆 workspace最后联调时三个工程分散在不同目录里根本没法统一管理。正确做法是启动 STM32CubeIDE 时指定一个总的 workspace比如LAT1480然后在同一个 workspace 里连续创建 Boot_F411、App_F411、Sensor_F103 三个工程。创建工程时工程名不要用中文和空格否者后面链接脚本、调试配置里会出现莫名其妙的路径问题。创建完之后Project Explorer 里就能同时看到三个工程。此时它们还是互相独立的需要继续做依赖关系配置否则改一个工程里的共享头文件另一个工程不会自动重新编译联调时就会出现“我明明改了协议结构体对方固件里还是旧布局”的诡异问题。2.2 Project References 与构建顺序让依赖关系自动跑我的做法是把两个 MCU 共用的协议头文件、通用工具函数单独抽成一个静态库工程命名为Shared_Lib。然后让 App_F411 和 Sensor_F103 都引用这个库工程。操作路径右键 App_F411 Properties Project References勾选Shared_Lib。Sensor_F103 同样操作。做完这一步还要检查构建顺序。在 Project Properties C/C Build Build Order 里确认Shared_Lib排在 App_F411 和 Sensor_F103 之前。这样当你修改了 Shared_Lib 里的协议定义再编译 App_F411 时IDE 会先自动重新编译 Shared_Lib再编译 App_F411。这个依赖关系的价值在联调阶段特别明显。项目初期我偷懒没有抽公共库直接把协议头文件各拷贝一份到两个工程里。结果改了一次协议字段只更新了主控那边协处理器还是旧定义两边通信一直对不上。后来把代码合并到 Shared_Lib通过 Project References 统一管理才根治了这个问题。2.3 两段固件地址怎么分配才不打架Boot 和 App 都在同一颗 F411 Flash 上必须把 Flash 地址切开。我的分配方案是工程起始地址长度说明Boot_F4110x0800000032KB引导程序App_F4110x08008000剩余 FlashF411CEU6 为 512KB业务代码用 CubeMX 生成工程后默认链接脚本STM32F411CEUX_FLASH.ld里 FLASH 起始是0x08000000长度 512K。需要手动改成FLASH (rx) : ORIGIN 0x08008000, LENGTH 480KApp 工程里还要在 main 函数最早期设置中断向量表偏移SCB-VTOR 0x08008000;我建议放在SystemInit之后、任何外设初始化之前执行。如果等到 HAL 库初始化完了才设置中间任何一个中断触发都会跳到错误地址。另外联调 Boot 跳转时我还踩过一个坑Boot 里开了 UART 中断跳转前没有关闭App 启动后一初始化串口就 HardFault。原因很简单——跳转后中断向量表换了但 Boot 里遗留的中断使能状态还开着一旦对应中断事件到来CPU 会拿着旧的函数指针跳到一个已经不属于当前程序的地址。所以 Boot 在跳转前一定要把所有外设中断关干净外设 DeInit然后关中断再跳。3. 双目标同时调试一个 IDE 控制两块板子3.1 配置两组不同调试器的 Debug Configuration双目标联调的核心是在 STM32CubeIDE 里建立两个不同的调试会话让两块板子同时处于调试状态。这里有个关键两个调试会话必须使用不同的 Debug Configuration不能共用一个配置。如果两个工程都叫工程名 Debug第二次点击启动时IDE 会把第一个会话停掉永远无法同时调试。我实际使用的配置步骤点 Run Debug Configurations。选中 App_F411 工程下的 “STM32 C/C Application” 配置右键选择 Duplicate重命名为Debug_App_F411_SWD。在 Main 标签页确认 C/C Application 指向Debug/App_F411.elf。在 Debugger 标签页Debugger 下拉框选择J-LINK。STM32CubeIDE 直接支持 J-Link不需要额外装 IDE 插件但电脑上要先装好 SEGGER 的 J-Link 驱动否则系统识别不到调试器。接口选择 SWD速率可以选 4000kHz如果线材比较差就降低到 1000kHz。如果电脑上同时插了两个同型号 J-Link必须在 Debugger 设置里填写目标调试器的序列号。不填的话系统可能随机选一个导致连接失败或者连到另一块板子。对 Sensor_F103 重复以上步骤创建Debug_Sensor_F103_SWD指定另一个调试器的序列号。两个调试器的配置对照如下配置名称目标 MCU调试器类型序列号烧录文件Debug_App_F411_SWDSTM32F411CEU6J-Link V11xxxxxxx1Debug/App_F411.elfDebug_Sensor_F103_SWDSTM32F103C8T6J-Link V11xxxxxxx2Debug/Sensor_F103.elf3.2 两个调试会话之间的同步操作技巧两个会话都启动后Debug 视图里会出现两个 Target 节点。此时你点 Resume、Suspend、单步这些操作只对当前活动会话生效。STM32CubeIDE 没有提供“所有目标一键同时运行”的默认按钮我实际用的同步办法是断点对齐在 App_F411 的串口接收回调函数里设一个断点。在 Sensor_F103 的串口发送函数里也设一个断点。先 Resume App_F411它跑到断点停下。再 Resume Sensor_F103它也跑到对应断点停下。两个目标都处于暂停状态时可以同时查看两边寄存器、变量、存储器内容逐个对比。如果只是要“同时恢复运行”我的操作习惯是先选中一个 Target 按 F8Resume然后马上切到另一个 Target 再按 F8。这种方式精度可能在毫秒级对协议联调完全够用。如果应用场景要求两个 MCU 严格同步发脉冲那就不该靠 IDE 手动操作而是要用外部硬件同步信号。还有一个小技巧在 Window Preferences General Keys 里给 Resume、Suspend 设置快捷键配合 Debug 视图上方的 Target 下拉切换框操作效率会高很多。我现在联调时基本不用鼠标点工具栏全靠键盘切会话、按 F8、按 F9。3.3 SWV/SWO 在双目标场景下的取舍Cortex-M3/M4 内核基本都支持 SWOSerial Wire Output可以在调试时通过 ITM 口输出 trace 信息不用占用 UART。但联调双目标时SWO 并没有想象中好用。我在 LAT1480 里试过给 F411 开 SWOSensor_F103 因为板子没有引出 SWO 引脚只能走串口。结果两边日志输出方式不一致调 trace 格式就花了不少时间。而且 SWO 在 IDE 内部对双会话的支持比较有限经常需要切换调试会话费劲。我的建议是双目标联调阶段统一用 UART 输出日志让两个固件用同一种方式打印调试信息代码里也更容易统一开关。SWO 更适合单目标、需要低开销 trace 的场景。4. 串口联调让数据在 MCU 与上位机之间跑起来4.1 printf 重定向到串口串口是嵌入式联调最常用的数据通道。LAT1480 里F411 和 F103 之间用 USART 通信F411 又通过 USB-TTL 和上位机通信。为了让调试信息直接打印到串口终端我在两个工程里都做了 printf 重定向。在 STM32CubeIDE 里最简单的方式是在usart.c或main.c中实现#include stdio.h int __io_putchar(int ch) { HAL_UART_Transmit(huart2, (uint8_t *)ch, 1, HAL_MAX_DELAY); return ch; }然后就可以在代码里直接用printf打印。注意 F411 的调试串口用huart2F103 用huart1按你自己 CubeMX 配置的实际句柄来。还有一个非常容易踩的坑STM32CubeIDE 的工程默认使用 newlib-nanoprintf默认不支持浮点数。如果你直接打印printf(%.2f, voltage);编译能通过运行输出却是0.00或者死机。解决办法是右键工程 Properties C/C Build Settings MCU Settings勾选Use float with printf from newlib-nano。这个选项藏得比较深我第一次找了好久。4.2 抓到一个真实联调 Bug缓冲溢出导致丢帧这个 Bug 是双目标联调时暴露的典型问题单工程单板测试根本发现不了。现象F103 以 230400bps 每 20ms 向 F411 发送一帧 32 字节的数据。F411 收到数据后通过调试串口打印日志结果发现帧尾经常缺 4 字节而且不是每次都丢是偶发。排查链路先怀疑波特率误差把双方降到 115200照样丢。用逻辑分析仪抓 F103 TX 引脚的波形发现 32 字节全部完整发出去了问题不在发送端。查 F411 的接收代码发现用的是HAL_UART_Receive_IT给的缓冲区长度是 16 字节。问题清楚了F103 一次发 32 字节F411 的接收中断第一次只接收 16 字节剩余 16 字节到达时会触发 OREOverrun Error溢出中断被 HAL 库直接丢弃。修复方案把接收缓冲区数组长度改成大于最大帧长我改成了 64并且在接收完成回调里立刻重新调用HAL_UART_Receive_IT确保持续接收。更稳妥的长期方案是改成 DMA 加 IDLE 空闲中断接收不定长帧这个后续可以单独写一篇。联调日志里我加上了帧序号和时间戳这个经验很重要。没有序号的话缺帧问题很难看出来——数据只有一两个字节的偏差肉眼盯屏幕根本盯不住。加上[SEQ%d]之后丢帧定位就非常直观了。4.3 终端选型与波特率那些事STM32CubeIDE 自带 Terminal 视图在 Window Show View Terminal 里可以打开支持直接选择串口 COM 口。它的优点是方便能在 IDE 里边调试边看日志不用切换窗口。但它的功能比较基础不能保存日志文件、不能发十六进制帧、没有时间戳。联调进入正式协议测试阶段后我更推荐用第三方串口工具或者直接写一段 Python 脚本做收发校验。比如让 F103 循环发帧用 Python 读串口统计 CRC 错误率和丢帧率这比肉眼高效得多。波特率选择上两个 MCU 之间我用过 115200、230400、1M。115200 是最稳妥的230400 在两边都用 HSE 晶振时也没问题。如果用了内部 HSI长时间通信会有偶发乱码风险。选波特率前先在 CubeMX 的 Clock 配置界面看一眼实际波特率误差一般要求控制在 0.2% 以内。接线时需要特别注意的是两个 MCU 之间 USART 要交叉连接也就是 F411 的 TX 接 F103 的 RXF411 的 RX 接 F103 的 TX。PC 上的 USB-TTL 模块和板子之间也必须共地否则刚开始正常跑十几分钟后就开始冒乱码非常隐蔽。5. 联调阶段最容易翻车的四件事5.1 看门狗在断点处咬死板子LAT1480 的 App 固件里开了 IWDG独立看门狗。联调时我设了一个断点停在某个协议解析函数里想查看数据结果还没看出个所以然板子自己复位了。调试会话虽然没有完全断开但已经被迫重新启动程序。原因很简单断点暂停的是 CPU但 IWDG 走的是独立 LSI 时钟不受 CPU 暂停影响。断点停得越久看门狗计数越接近零最后直接复位。我的解决办法是在工程里加一个调试宏#ifdef DISABLE_IWDG_IN_DEBUG // 不初始化 IWDG #else MX_IWDG_Init(); #endif然后在 Debug 配置的预处理宏里加上DISABLE_IWDG_IN_DEBUGRelease 配置不加。这样联调阶段狗完全关闭发布时自动恢复。千万别在联调时尝试“让调试器帮忙喂狗”暂停状态下调试器无法干预 IWDG 外设的运行。5.2 优化等级改变了调试行为STM32CubeIDE 的优化等级位置在工程右键 Properties C/C Build Settings MCU GCC Compiler Optimization。Debug 配置默认是-O0或-Og但有些人做性能测试时会手动改成-O2改完之后调试就变得很诡异。我实际遇到过一次F103 协处理器开了-O2后一个用于协议状态机的全局变量g_rx_index在中断里更新主循环里判断if (g_rx_index 0)来决定要不要处理数据。调式时用变量监视窗口看g_rx_index在收到串口数据后还是一直显示 0但逻辑分析仪明明抓到 UART 有数据进来。后来排查了半天发现是编译器把g_rx_index优化进了寄存器副本中断里更新了内存变量主循环读到的却是寄存器里的旧值。给变量加上volatile关键字后恢复正常。优化等级在-O0下基本不会出现这种问题但-O2下代码行为可能和源码表达不一致。所以我的原则是联调阶段只用-O0或-Og性能压测时才切-O2并且保留一版-O0镜像用于复现问题。你还要知道的是优化等级开高之后原本能命中的断点可能因为代码被重排而失效IDE 会提示“断点位置不可用”这时候别硬调切回低优化等级再查。5.3 复位引脚、低功耗与调试器连接冲突双目标联调时两个调试器同时工作目标板任何一块进入低功耗模式都可能导致调试器下次连接失败。现象就是点击 Debug 后进度条卡在连接阶段最后报错Cannot access target。这类情况下优先在 Debug Configuration Debugger Reset behavior 里选择Connect under reset。这个选项会让调试器先拉低 NRST 再连接目标复位后立即暂停避免低功耗模式把调试口也关掉。还有一种情况是 NRST 引脚被设计成普通 GPIO或者板子上挂了滤波电容导致 SWD 复位时序不对。这时候改用Software reset或直接取消 NRST 连接一般也能连上。另外我遇到过两个板子供电不共地导致的调试器间歇性掉线。当时两块板各插一个 USB 供电调试器虽然也接了 GND但实际地电位不一致。最后换成同一个 5V 电源给两块板供电问题立刻消失。联调前先统一电源和地能省掉很多玄学问题。5.4 时钟不一致导致对方收到乱码串口联调时最迷惑的一个问题双方配置明明都是 115200可就是偶发丢字节或者乱码。用逻辑分析仪看波形数据位和停止位都是对的但接收端采样点偏移越来越严重。根因通常是两边的串口时钟源不一致。比如 F411 用 HSI 16MHzF103 用 HSE 8MHz 倍频到 64MHz内部 RC 的精度本来就在 1% 左右两边误差叠加之后一帧数据 10 个位累计偏差可能超过 2%接收端采样就容易错位。联调之前最好先核对两边的时钟配置在 CubeMX 的 Clock 页面确认USART 波特率误差越小越好。尽量让两块板子都用 HSE 晶振或者都用一个稳定的外部时钟源。如果板子硬件上根本没有 HSE那长帧传输就要留足时间余量协议里建议加上帧校验和重传机制。还有一个隐蔽问题NVIC 中断优先级分组不一致。F411 的HAL_Init()默认把优先级分组设置为NVIC_PRIORITYGROUP_4即 4 位抢占优先级。如果 F103 代码里改成了NVIC_PRIORITYGROUP_2两边对中断优先级的理解就会完全错位。联调时如果发现串口数据在高优先级中断场景下会被莫名其妙打断优先检查两边的优先级分组是否一致。统一调用HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)就能解决。6. 联调过后把这些配置固化下来6.1 Debug Configuration 的复制与命名经验联调阶段我把所有调试配置重新整理过一遍规则是不要用默认的“工程名 Debug”而是创建独立的、名字里有明确含义的配置比如Debug_App_F411_SWD、Debug_Sensor_F103_SWD。这样即使两个工程都开着也不会因为配置冲突把上一个会话挤掉。.launches文件是 Eclipse 的启动配置文本默认存放在工程.launches目录下可以提交到 Git。但注意里面记录了本机绝对路径和调试器序列号团队共享时很容易在多台电脑上产生冲突。我的做法是不提交.launches文件而是在 README 里写清楚两个配置的关键参数调试器类型、序列号用途、SWD 速率、Reset behavior。换电脑后重新建一次配置十分钟就能搞定。还有一个防止误烧录的经验联调时调试器多、目标板多很容易手滑把 A 工程的固件烧到 B 板上。我实际犯过一次把 Boot_F411 的 elf 下到了 F103 上F103 跑飞浪费了几分钟才意识到。后来我在每个 Debug Configuration 的名称后面加了目标 MCU 后缀并且在 Debugger 设置里再次确认序列号类似事故再没出现过。6.2 把日志模块做成可复用组件LAT1480 联调完成后我单独抽出了一个dbg_log小模块核心是一个带级别的打印宏#define LOG_LEVEL_ERROR 0 #define LOG_LEVEL_WARN 1 #define LOG_LEVEL_INFO 2 #if (ENABLE_DBG_LOG LOG_LEVEL_INFO) #define DBG_INFO(fmt, ...) printf([INFO] fmt \r\n, ##__VA_ARGS__) #else #define DBG_INFO(fmt, ...) do { } while(0) #endifENABLE_DBG_LOG在 Debug 配置里定义为有效值Release 配置里定义为 0。这样联调时看完整日志发布时不占代码空间也不影响运行速度。这个模块的收益在后面几个项目里体现得很明显。每换一块板子只需要改掉dbg_log.c里的 UART 句柄其他代码全部复用。联调时最怕的就是每个工程一套日志风格一旦遇到问题两边对日志的格式都解释不一致排查成本直接翻倍。大概就是这些。LAT1480 的联调从最初两块板子各调各的到后来在同一个 IDE 里跑通 Boot 跳转、串口协议、上位机通信STM32CubeIDE 的多工程和多会话能力确实省了很多力气。最后再分享一个小技巧联调时如果发现双方数据总差那么一点点优先查波特率误差和中断优先级配置这两个问题的隐蔽性比看门狗和优化等级还高也是最容易被忽略的。
返回列表