做异构MPU的工程师应该都有过这种经历:M33的裸机代码在逻辑上明明没毛病,一放到真实平台上就各种花式崩,但想单独调试它,Linux 又是开机自启,A35 抢先把整个系统带跑了。ST 新出的 STM32MP257F-EV1 尤其如此——双 Cortex-A35 加一颗 Cortex-M33,默认启动流程一路从 ROM code 跑 FSBL 再跑 SSBL,最后 Linux 起来,M33 早就被 Linux 侧 remoteproc 接管了,你根本碰不到。标题里这个问题,我最近正好完整踩过一遍,整理成这篇实操记录。
这篇文章适合两类人:一类是拿到 STM32MP257F-EV1 之后想把 M33 当独立单片机用、暂时不想碰 Linux 的裸机开发者;另一类是已经在 A35 侧跑 Linux、但 M33 固件需要独立迭代验证的嵌入式工程师。下面所有内容都是基于实际开发板上的操作,不是 PPT 层面的理论。
1. 为什么要绕开 Linux,单独调 M33
1.1 异构 MPU 的调试优先级之争
STM32MP257F-EV1 的开发板默认上电流程是:ROM code 根据启动引脚选择从 SD 卡、eMMC 或 QSPI 加载 FSBL,FSBL 拉起 DDR 初始化,然后引导 SSBL(通常是 U-Boot),最后 U-Boot 启动 Linux。整个链条里没有人会等 M33,Linux 一旦跑起来,M33 的固件加载、复位控制、内存映射全部由 A35 侧通过 remoteproc 框架接管。
这个流程对量产场景是合理的,但对 M33 裸机开发者来说非常痛苦。你在 Keil 或 STM32CubeIDE 里编译好的 M33 工程,想下载到板子上调试,发现自己根本控制不了这个核心——调试器连上去,要么 halt 不了,要么 halt 之后 M33 的时钟/电源已经被 A35 侧的驱动设置了,你后续操作全都是错的。
关键点在于:M33 这个核心从复位开始的默认行为其实和你用 STM32H7 这类单片机没有本质区别,它有默认的向量表、默认的时钟源、默认的执行入口。只要 A35 不去干扰它,M33 完全可以独立跑起来。问题是怎么让 A35“不来捣乱”。
1.2 哪些场景必须走 Standalone 调试路线
不是所有 M33 开发都需要绕开 Linux,但下面这几类场景基本躲不掉:
第一类是 M33 固件开发早期,你还在调启动代码、PLL 配置、GPIO 复用这类底层东西。这些寄存器一旦被 Linux 侧驱动抢先初始化,你再用调试器改就会看到各种奇怪的恢复动作,很难判断是你代码的问题还是 Linux 侧配置的干扰。
第二类是实时控制类应用,比如用 M33 做电机控制、双脉冲测试、或者跑 EtherCAT/工业协议栈。这类应用要求 M33 从最早期就独占某些外设和中断,不能用 remoteproc 那套“系统起来之后才加载”的流程。
第三类是纯软件开发迭代,你的 M33 业务逻辑可能只依赖 UART 和几个 GPIO,明明可以在几分钟内完成编译-下载-验证的循环,但每次都要等整个 Linux 启动完毕才能动 M33,效率太低。实测下来,走 Standalone 调试流程,一次完整调试循环可以控制在几十秒内,比走 Linux 完整启动流程至少快 5 到 10 倍。
2. 动手前的准备工作:硬件、工具和启动模式
2.1 硬件连接与调试器选型
STM32MP257F-EV1 这块板子我手头的版本板载了 ST-LINK/V3,通过板上 USB 口可以同时提供调试接口和虚拟串口。调试 STM32MP25 这种异构芯片时要注意:ST-LINK 默认连接的调试总线是 JTAG/SWD,可以同时访问 A35 和 M33 两个核心的调试端口(DP)。但实际上,M33 的调试访问点和 A35 不完全相同,使用官方 ST-LINK 时,调试器会枚举出两个 AP(Access Port)。你要找的是连接到 Cortex-M33 的那个 AP,而不是 A35 的 AP。
这里有个容易忽略的硬件细节:如果你手里的 EV1 板子不带板载 ST-LINK(有些批次或客户定制版本只有调试排针),你需要外接一个 ST-LINK/V2 或 J-Link。J-Link 对 STM32MP25 的支持在较新版本的 J-Link 软件里已经很完整,但 J-Link 连 M33 时同样需要选择正确的 core,否则默认连到 A35 上,你会一脸懵。
另外,电源和 BOOT 引脚的供电顺序要留意。板子刚上电时,M33 的 VDD 域由 PMIC 管理,如果电源控制逻辑没走对,M33 可能处于复位或者断电状态。保险做法是先用板载默认跳线让 PMIC 完成全板供电,再动手切启动模式。
2.2 关键一步:把开发板切到串行启动模式
要绕开 Linux,最干净的思路是让 ROM code 不要从外部存储器加载 FSBL/SSBL,而是进入一种“工程模式”,等主机通过 USB/UART 把代码灌进去。ST 把这套模式叫 Engineering Boot Mode,不同系列叫法略有差异,在 STM32MP25 的文档里对应 ROM code 的 serial boot 路径。
具体操作方法是找到 EV1 板上的 BOOT 配置拨码开关(不同批次位置可能不同,通常在板子一角,丝印标着 BOOT0/BOOT1/BOOT2 或类似标识)。参考《STM32MP257F-EV1 Evaluation board user manual》里的启动模式表格,把组合切到 USB 串行启动或者 UART 串行启动。我板上实测:拨成“USB serial boot”后,用 Type-C 线连接板子的 USB_DFU 口,PC 端会枚举出一个 STM32 BOOTLOADER 设备。
这一步看着简单,但有两个坑必须提:一是拨码开关的切换必须在上电之前完成,不要热切换,否则 ROM code 可能已经按错误的启动源初始化了部分时钟,进入串行模式后行为不稳定;二是确认你用的是专门的 DFU 口,不是板卡的 USB Host 口。EV1 板上有多个 USB 口,只有丝印标注 DFU/OTG 的那个口才连接到了 ROM bootloader 的 USB 引脚上。我第一次就插错了口,CubeProgrammer 死活识别不到设备,后来看原理图才发现接的是 USB Host 口。
2.3 确认 M33 是否在运行:从复位向量说起
把启动模式切到串行模式之后,M33 实际上是处于一种“默认复位后未运行”的状态。这时你可以用调试器连接并读取 M33 的寄存器,用来确认调试链路已经打通。
判断 M33 是否真的“活着”,最直接的指标是读取它的 PC(程序计数器)和 MSP(主栈指针)。Cortex-M33 复位后,MSP 从地址 0x00000000 加载,PC 从地址 0x00000004 加载。但注意,STM32MP25 内部没有固定的 Flash 映射到 0x00000000,这个地址在芯片内部是别名或者被 ROM 重映射掉的,所以复位后的 PC 不一定是你 IntFlash 的习惯入口。别慌,这是正常的,关键是你能通过调试器读出来这些寄存器的值,说明 M33 的 DP/AP 已经响应了。
另一个确认 M33 状态的方法是看 DBGMCU 里的 IDCODE,M33 的 CoreSight 组件有独立的 ROM table 和 component ID。用 STM32CubeProgrammer 或者 OpenOCD 扫描调试总线时,能看到两个核心的 ID,其中 A35 的 ID 是 ARM 的 A-class Debug Architecture,M33 则是 v8-M 的 CoreSight,区分度很高。看到 M33 的 ID 就说明调试链路正常,可以进入下一步。
3. 方案一:用 STM32CubeProgrammer + CubeIDE 快速跑通 M33 裸机调试
3.1 生成并编译一个最小的 M33 工程
先准备好一个能编译通过的 M33 裸机工程。最快的办法是用 STM32CubeMX(现在叫 STM32CubeIDE 里的配置器)选择 STM32MP257F 这颗芯片的 Cortex-M33 core 初始化一个空工程,选一个时钟源和一组调试串口。这里需要注意的是:STM32CubeMX 里 STM32MP25 的 M33 工程有两种类型,一种是带 TrustZone 的,一种是不带的。如果只是做裸机验证,建议先选不带 TrustZone 的工程,TrustZone 开启状态下 M33 的 Secure/Non-Secure 状态会引入额外的调试和内存属性问题,没必要在第一步就给自己上难度。
我实测的最小工程只需要三部分:一个时钟初始化(可以先全用内部时钟,甚至不初始化时钟直接跑,M33 上电默认时钟可以运行)、一个 GPIO 输出(驱动 EV1 板上的 LED,这个非常关键,是你确认程序“真的在跑”的最直观证据)、一个 UART 打印(用于后续加日志辅助调试)。把这个工程编译成 ELF 文件,注意 Output 选项里勾选生成 .elf 格式,同时确认链接脚本里的 RAM 起始地址和长度和你要加载的物理内存一致。
这一步容易出现的坑在链接脚本。STM32MP25 内部有两块 RAM 可以用:一块是 SYSRAM(系统 RAM,通常在 0x2FFC0000 附近),另一块是 M33 专用的紧耦合内存。如果 CubeIDE 自动生成的链接脚本默认指向外部 DDR,而你又不打算初始化 DDR,下载后就会直接 HardFault。我的做法是手工改链接脚本,把 FLASH 和 RAM 都指到 SYSRAM 上,代码和数据都放 SYSRAM,简单粗暴且验证充分,后续要搬 DDR 再单独调。
3.2 用 CubeProgrammer 加载固件到内部 RAM
编译好 elf 之后,打开 STM32CubeProgrammer。当前版本(我这边用的是 2.16 以上)已经支持 STM32MP25 系列。操作路径是:左侧选择“MCU”模式,接口选择 USB,连上之后点击“连接”,软件会扫描到两个 core。在界面上你会看到类似 “Device: STM32MP257Fxx - Cortex-M33” 的条目。这里关键步骤:先在左边列表选中 Cortex-M33 核心,然后再加载文件。
加载文件时有两种选择。第一种是直接加载 ELF,CubeProgrammer 会解析 ELF 里的段信息,自动把代码放到链接脚本指定的地址。第二种是加载 bin 文件,这种方式需要你手动填写起始地址。我自己的经验是用 ELF,省心,且不会出现 bin 加载时地址填错导致 PC 跳到空白区的情况。
加载完成后,CubeProgrammer 里有个“运行”按钮,点一下之后 M33 应该直接跑起来。这时候观察 EV1 板上的 LED 是否按你的预期翻转,如果亮了或者闪了,恭喜,M33 已经在没有 Linux 的情况下独立运行了。但我强烈建议在点击“运行”之前,先用 CubeProgrammer 的寄存器窗口看一眼 M33 的 PC 是否落在你期望的复位入口,以及 MSP 初值是否等于链接脚本里定义的栈顶。这个排查习惯能帮你提前发现内存链接错误,而不是等到 LED 不亮再去盲查。
有一个非常实用的进阶操作:不要在 CubeProgrammer 里直接“运行”,而是保持 halt 状态,然后转去 CubeIDE 里用调试器 attach 到这个 M33 核心上,这样你能拥有完整的变量查看、断点、单步能力。
3.3 CubeIDE 调试配置中容易忽略的三个选项
在 CubeIDE 里新建一个 Debug Configuration,目标接口选 ST-LINK,调试类型选“STM32 Cortex-M33 C/C++ Application”。这里有几个选项非常影响体验,我逐个说。
第一个是“Reset behavior”。默认配置可能是“Reset and halt”,但你的代码是从 SYSRAM 启动的,没有真正的 Flash 复位向量,硬件复位后 ROM code 会重新接管系统,把启动模式又跑一遍,导致你的调试会话直接失效。正确做法是选择“Connect under reset”或“Core Reset and halt”,具体看 CubeIDE 版本的措辞——你需要的是只复位 M33 这个核心,而不是整个芯片系统复位。如果不确定,选 “Core Reset” 通常比全片复位更安全。
第二个是“Download”下面“Enable flash download”的勾选状态。如果你打算保持 CubeProgrammer 已经加载好的 RAM 镜像不变,只在 CubeIDE 里做在线调试,就把 flash download 关掉,去掉“Download to flash”的勾选。否则 CubeIDE 会尝试按 Flash 算法去下载,而 SYSRAM 根本不是 Flash,必然报错。反过来,如果你希望 CubeIDE 直接完成加载和调试一体化的流程,就要在 Debug Configuration 里加一个 RAM 下载算法,这个在 ST 的 Wiki 和 CubeIDE 文档里都有示例,但默认不会自动配好,新手容易卡在这。
第三个是“Startup”里的“Set PC to”选项。有时候你希望程序停在某个自定义入口,而不是链接脚本规定的 _start,可以在 startup 脚本里加一行 set {int}0xE000EDF0 = 之类的汇编或者直接设置寄存器,但更简单的做法是勾选 “Reset and halt” 里的“Set PC to”并手动填入你的入口地址。这个选项在调试 bootloader 或者从 RAM 恢复执行时非常有用。
4. 方案二:OpenOCD + GDB 命令行调试 M33
4.1 环境准备和 OpenOCD 配置
CubeIDE 集成的调试器底层其实也走了 OpenOCD 那一套,但如果你想脱离 IDE 做更灵活的自动化调试,直接用命令行 OpenOCD + GDB 是更顺手的方案。STM32MP25 的调试点在 ST 的开放社区里已经有不错的支持,不过你需要确保 OpenOCD 版本足够新——我这边用的是从官方 git 拉下来编译的 0.12.0+,旧版本对 STM32MP25 的 target 描述文件可能不完整或完全缺失。
核心配置文件里需要做两件事:定义调试总线和定义 target。ST-LINK 部分可以直接用source [find interface/stlink.cfg],传输协议选hla_swd还是jtag取决于你板子如何接线,EV1 板载 ST-LINK 默认走 SWD 没问题。target 部分,新版 OpenOCD 里搜索stm32mp25x.cfg或者类似的 target 文件,这个文件内部已经定义了 A35 和 M33 两个 target,并给它们分配了不同的名字。你需要在配置里显式把 M33 设为主 target,避免 GDB 默认连接 A35。
一个更精细化的做法是不要直接 source 官方的 target 配置,而是自己创建一个最小化的 M33 专属配置:手动指定 DAP 的 AP 编号、M33 的 CoreSight 基地址、以及所需的内存区域描述。这种写法维护成本高,但控制力强,尤其是当你想要精细控制 M33 的复位信号而完全不触碰 A35 时,官方 target 文件里“全片复位”的行为有时候会干扰你的调试。
4.2 连接 M33 的 GDB 会话示例
配置文件准备好后,启动 OpenOCD:
openocd -f interface/stlink.cfg -f target/stm32mp25x_m33_only.cfgOpenOCD 起来后监听 3333 端口,另开终端启动 arm-none-eabi-gdb,加载你的 M33 固件:
arm-none-eabi-gdb your_m33_app.elf (gdb) target extended-remote localhost:3333 (gdb) info registers这里有两个关键 GDB 命令值得多说。第一是monitor targets,用来查看 OpenOCD 当前识别到的 target 列表,确认 M33 的 target 名称和编号。第二是monitor reset halt,这个命令会只对当前选中的 target 做复位并暂停,对于 Standalone 调试来说,这个“只复位 M33”的能力是核心需求。如果你的 OpenOCD target 文件里把复位行为绑定到了系统级复位,那你需要修改 target 配置里的cortex_m33 reset_config,改成srst或者自定义的vectreset,具体语义取决于你板子的复位信号连接。
还有一个使用细节:当你用 GDB 加载完 ELF 之后,不要直接continue,先执行monitor reset halt让 M33 停在复位向量处,然后stepi单步几条指令,观察它是否按预期跳转到Reset_Handler。这一步能帮你确认 PC 取指地址和你的链接脚本一致。如果 PC 跳到了 0xFFFFFFFF 之类的地址,说明链接脚本的存放地址或者 ROM 的映射有问题,趁早回查,别等程序跑飞了才回头。
4.3 通过启动脚本跳过 Linux 引导
如果你在工程里需要反复执行“加载固件 -> 重置 M33 -> 暂停到指定位置”的流程,手敲 GDB 命令太累。我习惯写一个 GDB 启动脚本,每次省掉重复劳动。下面是一段可以直接抄走改地址的示例脚本:
target extended-remote localhost:3333 file your_m33_app.elf monitor reset halt load monitor reset halt break Reset_Handler continue脚本里的两次reset halt是有讲究的:第一次 halt,目的是把 M33 从 ROM code 的接管状态中拉出来,让调试器拿到控制权;load之后第二次 reset halt,是为了让 PC 回到复位向量处,确保加载的代码从头开始执行。然后在Reset_Handler处下断点,再 continue,这样程序就会稳定停在你的入口代码。我实测这套流程在 STM32MP257F-EV1 上非常稳定,而且没有任何 Linux 组件参与,整个过程从 OpenOCD 启动到停在 Reset_Handler,熟练后 20 秒内搞定。
GDB 脚本的优势还体现在变量查看上。M33 裸机代码里那些优化后的局部变量,在-O2编译后可能看不全,但你在 GDB 里可以随时切汇编级单步,配合info registers观察内核寄存器状态。对于 Cortex-M33 的特有寄存器,比如 CONTROL、PRIMASK、BASEPRI、FAULTMASK,GDB 命令info reg不一定全部输出,你可以直接用p/x $control这类形式访问,非常直观。调试裸机 M33 时,这个比 IDE 里看变量来得更直接。
5. 常见问题与排查技巧实录
5.1 M33 一直 halt,怎么判断是否被 A35 复位锁住
我遇到最多的现象是:调试器能连上 M33,但程序一直停在0xFFFFFFFE或者某个奇怪的地址,单步不走。这种情况大概率不是你的代码 Bug,而是 M33 的复位域或电源域被 A35 侧的复位逻辑锁住了。
排查方法很直接:用 OpenOCD 的monitor reset halt和reg命令反复比较 M33 的 DHCSR(Debug Halting Control and Status Register)状态位。正常情况下,复位后 M33 的 DHCSR 里 S_HALT 位应该被置位,表示内核已经暂停。如果 S_HALT 一直置不了位,说明调试器的 halt 请求没能真正控制内核,这时候检查是不是芯片处于 secure 状态,调试访问被 Security 模块拦截了。另一个检查点是 RCC 的复位状态寄存器,看看 M33 是否被独立复位信号一直拉在复位状态,如果是,你需要通过 NRST 控制或复位配置解除这个锁定。
更隐蔽的情况是:你的 M33 程序里用了 WFI(Wait For Interrupt)指令等待事件,此时内核处于 sleep 状态,调试器能连接但指令不执行。这不是死锁,是内核睡着了。解决方法是设置DBGMCU->CR里的 DBG_STANDBY 等位,让内核在调试时即使执行 WFI 也不会进入深睡眠,这个选项在 CubeMX 生成的初始化代码里通常不会默认打开,需要你自己补。
5.2 下载后 PC 跑飞,时钟和电源配置背锅
如果你从 CubeProgrammer 点击“运行”后 M33 立刻跑飞,或者 GDB 里continue之后程序跳到了未定义指令,九成原因是时钟树配置问题——具体说,M33 在复位后默认使用内部时钟,但你外设初始化代码里直接切到了 PLL,而这个 PLL 的输入源或者分频配置在没有 Linux 辅助初始化时其实没准备好。你在带 Linux 启动的环境下从来没遇到过这个问题,是因为 A35 侧的 FSBL 已经把整个时钟系统初始化完了,M33 切 PLL 只是“借用”现成的稳定时钟。
解决办法有两个方向。一是把所有外设时钟都挂在默认时钟上,先不切 PLL,验证程序逻辑,等整体功能调通后再逐个外设开启 PLL 输出。二是自己写一段 M33 的时钟初始化代码,显式配置 RCC 寄存器,把 PLL1/PLL2 之类的时钟源启动起来。这时候高度建议用 STM32CubeMX 生成初始代码,因为它生成的时钟树配置会按你选的频率正确计算分频和倍频系数,省去手算的麻烦。但要注意,CubeMX 默认生成的代码可能假设 A35 侧已经初始化过 DDR,如果你没有 DDR 初始化,有些默认时钟配置会引用外部 DDR 控制器,需要屏蔽。
5.3 TrustZone 和调试使能导致的“无法连接”
STM32MP25 的 M33 核心支持 TrustZone,这是和 M4 调试的最大区别之一。如果芯片里保留了出厂安全配置,或者你在 CubeMX 里打开了 TrustZone,那么 M33 的 Non-Secure 调试接口是访问不到 Secure 内存和 Secure 外设的。表现出来就是:变量窗口里看到 Secure 内存区域全是 0xFFFFFFFF,或者直接无法 halt。
这个问题的排查思路是:先用 STM32CubeProgrammer 连接,读一下 SAU(Security Attribution Unit)和 IDAU 的区域配置,确认 M33 当前处于什么安全状态。如果确认安全配置没问题,再看看调试器的权限——对于某些工程样片或者量产芯片,调试端口可能被永久禁用,你需要在 Keil/CubeIDE 里配置为“允许调试访问”,对应到寄存器是DBGMCU->CR和 CoreSight 的锁。ST 官方有一个专门的应用笔记讲 STM32MP25 的安全调试,遇到这类问题直接查那篇文档最快。
一个常用的小技巧是:用 STM32CubeProgrammer 的“Option Bytes”界面读一下 RDP(Read Protection)等级。如果 RDP 等级不是 Level 0,比如被设置成 Level 1 甚至 Level 2,就会限制调试访问。量产板子出现这种问题多半是因为之前有人烧录过保护配置。RDP Level 1 时你可以尝试用全片擦除来降级,但如果到了 Level 2,芯片就永久锁死了,只能换板子。所以拿到手先查一遍这个选项,养成习惯。
5.4 两种方案如何取舍,一张表说清
选择 CubeIDE 图形化方案还是 OpenOCD 命令行方案,取决于你的调试场景和熟练度。我用下面这张表总结一下,方便你快速决策:
- 调试能力:CubeIDE/STM32CubeProgrammer 提供完整图形界面,OpenOCD+GDB 命令行操作但完全可控
- 变量查看:CubeIDE 在 IDE 中可视化查看,OpenOCD+GDB 需要 GDB 命令或 TUI 模式
- 自动化:CubeIDE 需要手动点击或编写脚本,OpenOCD+GDB 可通过启动脚本和批处理实现自动化
- M33 核心控制:CubeIDE 集成度高,但部分复位行为封装不够透明,OpenOCD+GDB 通过 target 配置精细控制复位行为
- 对新手友好度:CubeIDE 对新手友好,入门快,OpenOCD+GDB 学习曲线陡但灵活
- 适合场景:CubeIDE 适合交互式单步调试,OpenOCD+GDB 适合自动化测试和批量验证
我的习惯是两种配合用:日常单步、看代码逻辑用 CubeIDE;要跑自动化回归测试,或者需要精确控制复位时序时,用 OpenOCD 写脚本批量执行。两条路都能做到“不启动 Linux 直接调试 M33”,没有绝对优劣。
5.5 最后一个值得记住的细节:把调试配置固化下来
如果你以后要反复做 M33 Standalone 调试,强烈建议把这套流程固化成文档或脚本,不然换台电脑、换块板子,又要重新踩一遍启动模式、CubeProgrammer 连接、OpenOCD 配置的坑。我现在的做法是:在工程仓库里建一个debug/目录,放好 OpenOCD 配置、GDB 启动脚本、CubeProgrammer 的命令行脚本,以及一份简短的 README 记录板子的 BOOT 拨码正确位置。
另外,实测中我发现 STM32CubeProgrammer 其实也支持命令行模式,这个比图形界面更适合集成到构建流程里。你可以写一条命令:STM32_Programmer_CLI -c port=USB1 mode=UR -el your_m33_app.elf -run,这样编译完成后直接执行命令就能加载运行,几秒钟就完成一次代码验证循环。Linux 没启动,M33 照样运行,整个流程干净利落。