刚拿到 STM32MP135DK 这块板子的时候,我的第一反应是“又得先养一套 Linux 系统”,默认出厂 SD 卡插进去,串口能看到 U-Boot,再跑起完整 Linux 也没问题。但真到做项目时,问题就来了:我只想点个灯,或者写一段类似 51 单片机那样的循环逻辑,凭什么非要先烧系统、配 Yocto、搞根文件系统?
后来翻了 ST 的参考手册才发现,STM32MP135 里远不止一颗 1GHz 的 Cortex-A7,它内部还藏着一颗最高 209MHz 的 Cortex-M4。这颗 M4 并不是摆设,完全可以当成 STM32F4 这样的普通单片机来写裸机程序:操作 GPIO、跑 PWM、挂 RTOS,统统不依赖 Linux。更妙的是,让 A7 侧的 U-Boot 在启动阶段把 M4 固件从 SD 卡读进 DDR,再释放并启动 M4 内核,整个开发板就能实现真正的“脱机运行”。也就是说,编译好的 LED 程序放进 SD 卡,上电自动执行,不连电脑、不接调试器,也不需要往芯片里烧死固件。
这篇文章就把这个玩法从头到尾拆开讲:为什么 MP135 能这么用、CubeIDE 工程怎么建、U-Boot 怎么从 SD 卡加载 M4 程序、如何固化启动脚本、以及我实操中踩过的坑。适合手里有 MP13 系列开发板、又暂时不想折腾大型 Linux 镜像的人;也适合刚接触 MPU 但依赖 CubeMX 生态的嵌入式工程师。
1. 为什么能把 MP135DK 当成单片机玩
1.1 它不是 MCU,但 M4 核心是真舵机
STM32MP135 属于异构多核处理器,默认主视角都集中在 Cortex-A7 上,毕竟一颗 1GHz 的 A7 跑 Linux 才是它的“本职工作”。但芯片内部其实还集成了一颗 Cortex-M4,这颗 M4 和传统 MCU 里的核心本质相同,只是被放到了 MPU 里,和 A7 共享外设控制器、DDR 控制器和中断控制器。
这意味着,你写 M4 程序时的体验和普通 STM32F4 几乎没区别。可以用 HAL 库、可以操作寄存器、可以跑裸机任务,也可以移植 FreeRTOS。由于它和 A7 共享同一套存储体系,M4 代码甚至可以直接放在 DDR 里执行,内存空间比一般片内 SRAM 大得多,这反而成了优势。
但它的“单片机属性”不是默认打开的。芯片上电后,Cortex-M4 内核默认处于复位状态,需要由 A7 侧引导代码(通常是 U-Boot)或者 Linux 里的 remoteproc 框架把它“解除复位”并加载固件。所以我们的核心思路就变成了:让 A7 只扮演引导器的角色,U-Boot 起来之后不启动 Linux,而是从 SD 卡把 M4 固件搬到指定内存,再启动 M4。
1.2 脱机运行的完整链路:SD → ROM → FSBL → U-Boot → M4
要理解“脱机”二字,得先清楚 MP13 的启动流程。芯片复位后,内部 BootROM 根据 BOOT 引脚电平决定从哪个介质读取第一段引导代码。如果配置为 SD 卡启动,BootROM 会从 SD 卡的 FSBL 分区读取 U-Boot SPL,这段代码完成最基础的 DDR 初始化和时钟初始化,然后加载 FIP 固件,包括可信固件 TF-A 和真正的 U-Boot 主程序。
U-Boot 主程序跑起来之后,默认会尝试在 bootfs 分区里查找 boot.scr 脚本或者启动 Linux 镜像。我们要做的,就是在这一层插入自定义逻辑:从 SD 卡读取 M4 固件文件,加载到指定 RAM 地址,随后释放 M4 的复位,让 M4 开始执行裸机代码。
这里有个关键点:为什么必须由 A7 先启动?因为 M4 的裸机程序通常放在外部 DDR 里,而 DDR 初始化是在 A7 引导阶段完成的。也就是说,即便你只想用 M4 点灯,A7 侧的引导代码仍然必须跑起来,它负责给 M4 “铺好路”。理清这条链路,后面所有操作逻辑就顺了。
1.3 这套玩法适合谁
如果你手头的任务只是“周期翻转一个 GPIO”“驱动一个小型传感器”“跑一段实时控制算法”,完全没必要把 Linux 拉下水。用 MP135DK 当单片机玩,核心收益是能复用成熟但复杂的 MPU 硬件外设,同时保持 MCU 级别的开发效率。
当然也要承认边界。这套方式不是官方主推的 Linux + M4 协同开发模式,A7 侧只做了极简启动,相当于一个“高级 bootloader”。M4 程序里不会被 Linux 动态管理,固件升级完全靠替换 SD 卡里的文件完成。如果你的应用最终要和 Linux 生态深度整合,比如 Web 服务、视觉处理、AI 推理,那还是老老实实跑完整系统再用 remoteproc 加载 M4 更稳妥。
2. 动手前的软硬件清单
2.1 硬件准备与板载资源确认
这次实验我用的是 ST 官方 MP135DK 开发板,典型配置:单核 A7 1GHz + M4 209MHz、带 LCD 显示屏接口、以太网口、USB Host/Device、SD 卡槽、板载 ST-LINK 调试器。
做“脱机跑 LED”实验,其实硬件要求不高:
- MP135DK 开发板一块。
- 一张 Micro SD 卡,官方原始 Linux 镜像卡最好,后续操作会省很多事。
- USB 转 Type-C 数据线,用于连接 ST-LINK 给板子供电并查看串口日志。
- 可选:一张 USB 读卡器,方便把 SD 卡插到电脑上改文件。
动手的第一步不是写代码,而是去官网下载 MP135DK 的《用户手册》和《数据手册》,把原理图翻出来。重点确认两个信息:BOOT 拨码开关的位置与默认状态、板载用户 LED 的 GPIO 连接。以我们自己操作的这块板为例,板上用户可编程 LED 丝印标号和 M4 可访问的 GPIO 引脚在原理图里都能查到。这里必须强调一句:不同批次或版本的人板子引脚可能存在差异,通电之前先看原理图,别照抄别人的工程就当完事了。
2.2 软件工具链选型
- STM32CubeIDE:官方集成开发环境,自带 GCC 交叉编译工具链,创建 M4 裸机工程最方便。
- STM32CubeProgrammer:虽然本方案不依赖它烧写,但偶尔看链接文件、擦空卡、调试 BOOT 配置时会用到。
- u-boot-tools:里面提供 mkimage 工具,用于把 boot.cmd 脚本打包成 boot.scr。
- 串口终端工具:我用的是 minicom,Windows 下用 PuTTY 也行,主要看 U-Boot 日志。
这里多说一句工具链背后的选择逻辑。很多人上来就在 Linux 主机里配一套 buildroot 或 Yocto,但“把 MP135 当单片机”玩法的优势恰恰是能完全活在 STM32CubeIDE 里。IDE 自动生成链接脚本、启动文件、HAL 驱动,你只需要关心用户代码和 U-Boot 侧的几个命令。
2.3 SD 卡与 BOOT 启动模式
拿到手的一张官方 MP135DK SD 卡,默认已经被刷成 Linux 启动镜像。它内部用的是 GPT 分区表,一般包含 fsbl1、fsbl2、fip、bootfs、rootfs 等分区。我们不需要重新分区,也不需要重新下载整卡镜像,只要往 bootfs 分区里放 M4 固件和启动脚本即可。
BOOT 拨码开关通常有 4 位,对应 BOOT0 ~ BOOT3,芯片复位时 BootROM 依据这组电平选择启动介质。具体组合和丝印位置要查手册,但在默认出厂状态下,MP135DK 一般配置为 SD 卡启动。如果你曾经改过拨码去搞 USB DFU 烧写,记得先拨回 SD 卡启动状态。最容易踩的坑就是这个:明明 SD 卡里啥都对,上电后串口却没有任何 U-Boot 输出,十有八九是 BOOT 拨码不对。
3. 用 CubeIDE 创建 M4 裸机工程
3.1 新建工程时的几个关键选择
打开 STM32CubeIDE,新建 STM32 Project,器件选择 STM32MP135F-DK。如果无法选择 MP13 系列,说明本地 CubeMX 包还没有安装对应的 MP13 支持包,需要先在 Help 里更新固件包。
工程向导会让你在 Cortex-A7 裸机、Cortex-M4 裸机、Linux 应用开发这类模板里做选择。这里直接选 Cortex-M4 裸机即可,不要勾选设备树或 Linux 相关组件。选完之后,IDE 会生成一个面向 Cortex-M4 的独立工程,启动文件和链接脚本都已经按 MP13 的 M4 拓扑配置好了。
重点观察链接脚本,不同版本生成的 M4 固件基地址并不完全一样。有些工程把代码放在 DDR 保留区域,有些放在 MCU SRAM。后面 U-Boot 加载时,我们会选择 ELF 文件交给 rproc 框架解析,它会按照 ELF 段把代码和数据放到对应地址。所以在 CubeIDE 里我们基本不用手算加载地址,但必须知道自己代码最终落在哪里,避免与 U-Boot 或 Linux 预留区冲突。
3.2 点亮板载 LED 的工程配置
M4 裸机系统初始化流程和 STM32F4 很像。在main.c里需要完成 HAL_Init、时钟配置、GPIO 初始化,然后进入主循环翻转 LED 引脚。
时钟配置是容易被忽略的地方。MP13 的 M4 支持最高 209MHz 运行,CubeIDE 会基于 HSE 或者 HSI 经过 PLL 得到一个系统时钟。建议在 CubeMX 的 Clock Configuration 页面直接确认 SYSCLK 是否为 209MHz,并留意 M4 与 A7 GPIO 时钟域是否同步。点灯这种简单任务不会暴露性能问题,但如果你后面接定时器、串口或 ADC,时钟树不清爽会让我们调到头大。
GPIO 部分,我在工程里选择的是板载用户 LED 对应的引脚。由于 M4 和 A7 共享大部分 GPIO 控制器,理论上这个引脚可能同时被 U-Boot 或者 Linux 设备树占用。但在“只跑 U-Boot 不跑 Linux”的场景下,我们不需要操心设备树,只要确认 U-Boot 没有在启动阶段把对应引脚反复拉低即可。
一个可行的main.c主循环代码逻辑如下:
#include "main.h" int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); while (1) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); HAL_Delay(200); } }这段代码看着和 F4 开发板的点灯程序几乎一样,这也是这个玩法最吸引我的地方:只需要把目标器件换成 MP135F-DK,生成初始化代码,剩下的逻辑完全复用 MCU 开发经验。编译后,会在工程 Debug 目录生成.elf文件,这个文件就是 SD 卡所需的 M4 固件。
3.3 链接脚本里的坑
第一次编译生成 M4 工程时,我被链接脚本的报错卡了一阵。主要原因是 CubeIDE 自动生成的 M4 链接脚本里,代码段起始地址如果和调试器下载配置不匹配,构建阶段不会报错,但上电后极易跑飞。尤其当你用的是极简 U-Boot 加载方式,而不是 ST-LINK 直接下载时,这个问题会被放大。
我的建议是每次生成工程后,先打开.ld文件,确认 MEMORY 区域里RAM或DDR的起始地址与你预期一致。手上这块板的例子里,M4 代码被链接到 DDR 的一段保留区域,这段区域由 U-Boot 通过内存映射留给 M4,Linux 不使用它。这样既保证了 M4 能享受大容量内存,又不会在后续升级 Linux 时互相踩踏。
4. 让 U-Boot 把固件装进 M4
4.1 手工验证:先走一遍 U-Boot 命令
工程编译通过后,第一步不要急着做自动化启动脚本,先在 U-Boot 命令行里手动验证整条链路。把生成的.elf文件拷贝到 SD 卡的 bootfs 分区。如果你的电脑能直接挂载 FAT 分区,直接拖进去;如果 bootfs 是 ext4 格式,就在 Linux 主机上用dd配合挂载,或者索性把 bootfs 分区格式化为 FAT32。
开发板上电,在串口终端进入 U-Boot 控制台(开机时按任意键打断自动启动)。先用part list mmc 0查看 SD 卡分区,确认 bootfs 分区的编号,不同镜像布局可能有差异,不能死记。
看到分区后,常用命令序列如下:
# 确认 SD 卡分区编号,例如 bootfs 是 mmc 0:4 part list mmc 0 # 查看 bootfs 分区根目录 fatls mmc 0:4 / # 把 M4 固件读取到 DDR 的临时加载地址 setenv m4_addr 0xC4000000 fatload mmc 0:4 ${m4_addr} m4-led.elf # 初始化 remoteproc 框架 rproc init # 加载 ELF 固件到 M4 实例 0 rproc load 0 ${m4_addr} ${filesize} # 启动 M4 rproc start 0如果一切顺利,命令执行完的瞬间,开发板上的 LED 就开始按主循环里的节奏闪烁。这里rproc load会解析 ELF 结构,并不需要你手动指定目的地址,固件会按照 ELF 段中的地址属性搬到正确位置。
如果手动链路跑不通,问题大概率出在三个地方:文件没放在正确的 bootfs 分区、固定地址被 U-Boot 自己占用、或者生成的 ELF 不是 Cortex-M4 的可执行文件。先逐个排除,再推进到下一步。
4.2 写 boot.scr 实现完全脱机
手动敲命令只能算调试,真正“脱机”的关键是让 U-Boot 每次上电都自动执行同样的动作。U-Boot 默认支持启动脚本boot.scr,它原本是给 Linux 镜像准备引导参数用的,但完全能被我“借”来加载 M4 固件。
我在bootfs分区根目录放了一个boot.cmd文件,内容如下:
setenv m4_addr 0xC4000000 fatload mmc 0:4 ${m4_addr} m4-led.elf if itest.s $? == 0; then rproc init rproc load 0 ${m4_addr} ${filesize} rproc start 0 else echo "M4 firmware load failed!" fi然后把boot.cmd使用 mkimage 工具打包成boot.scr:
mkimage -T script -C none -n "MP13 M4 boot" -d boot.cmd boot.scr把生成的boot.scr复制到 SD 卡 bootfs 分区根目录。再次上电,U-Boot 会在默认启动流程里找到并执行 boot.scr,自动加载并启动 M4 固件,然后停在 U-Boot 控制台。
这里有个细节值得注意:默认正式镜像的 U-Boot 默认 bootcmd 确实会寻找 boot.scr,但如果你用的是自己编译的 U-Boot,可能要检查环境变量bootcmd是否包含类似于loadbootscript的逻辑。没有的话,需要用setenv bootcmd 'fatload mmc 0:4 0xC4000000 boot.scr; source 0xC4000000'这类环境变量兜底,再saveenv保存。
4.3 分区格式与手工建卡建议
如果你手头没有官方刷好的 Linux SD 卡,需要从零制作一张启动卡,我不会建议你完全手工建 GPT 分区。最好使用 ST 提供的脚本create_sdcard_from_stm32mp1_boards.sh刷官方镜像,刷完之后再挂载 bootfs 分区,替换里头的boot.scr与 M4 固件。
严格要求之下,SD 卡格式至少有两点要确认。一是分区表,官方是基于 GPT 的,至少包含fsbl1、fsbl2、fip、bootfs、rootfs;二是 bootfs 分区格式,常见为 FAT32,但不同脚本版本也可能生成 ext4。我的建议是让 bootfs 用 FAT32,因为 Windows 下直接拷贝文件最省事,U-Boot 读取也稳定。如果格式不对,轻则 fatload 命令失败,重则整个启动流程卡在 FSBL 阶段。
5. 脱机运行与问题排查
5.1 实测脱机效果
把 boot.scr 和 m4-led.elf 都放好后,实验场景是:板子完全断电,拔掉调试线,只保留 SD 卡,重新上电。供电这个过程其实仍需要从 ST-LINK 的 USB 口取电,但程序运行不再依赖 ST-LINK 的调试会话。
此时打开串口终端,能看到 U-Boot 和 FSBL 的启动日志,等待 boot.scr 执行后,LED 开始自行闪烁。即使连续按复位键,或者断电再上电,行为依旧一致,因为程序源头在 SD 卡,不会因内存掉电而丢失。这就算完成了“SD 卡脱机运行 LED 程序”。
如果你想确认加载地址有没有问题,还可以在 U-Boot 里手动执行md 0xC4000000查看内存头部数据,MDK 里也看不出什么,但至少能验证固件确实被读进了预期位置。
5.2 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 上电后串口无任何输出 | BOOT 拨码不在 SD 卡启动档 | 查用户手册确认 BOOT0~BOOT3 |
| SD 卡可启动 Linux,但找不到 m4-led.elf | bootfs 分区号不对 | 用part list mmc 0确认分区编号 |
| fatload 报文件未找到 | 文件没拷贝成功或大小写不对 | 检查 FAT 分区根目录,确认文件名完全一致 |
| rproc load 报地址错误 | ELF 链接地址与目标 RAM 不符 | 检查 CubeIDE 生成的链接脚本 |
| rproc load 成功但 LED 无反应 | M4 未启动或 GPIO 配置被 U-Boot 干扰 | 检查 rproc start 返回值,确认 GPIO 时钟 |
| 手动命令正常,重启后失效 | boot.scr 未被自动执行 | 确认 boot.scr 名字、位置、bootcmd 环境变量 |
| 切回 Linux 后 M4 程序不跑 | boot.scr 被 Linux 启动流程覆盖 | 删除或重命名自定义 boot.scr 即可恢复 |
5.3 排查技巧与调试建议
我最常用的一条技巧是:把 M4 程序的入口处,开头第一件事翻转一个空闲 GPIO,并用示波器或者万用表观察。这样能快速判断 M4 有没有真的跑起来,而不至于在 HAL_Delay 或者时钟配置里盲找问题。
如果 M4 程序放的是串口 debug 信息,也要注意 MP135DK 上标准调试串口默认是 A7 控制台,M4 想输出日志需要额外配置对应的 UART 外设。比如我用 USART4 做 M4 日志输出,波特率 115200,把它接入同一个 USB-TTL 转换器就能看到。没有串口输出不一定代表 M4 没有运行,先看 GPIO 状态最直接。
另一个实用技巧是:调通首版点灯程序前,不要急着写复杂的 Boot 脚本。先用 U-Boot 手动敲命令验证,确认链路没问题,再打包 boot.scr。否则一旦有变量环境、路径或权限问题,排查起来会和排地雷一样。
5.4 进阶思路
这套机制跑通后,能玩的事情其实很多。M4 上可以移植 FreeRTOS,任务调度、信号量、队列这些都和普通 MCU 一样;再用 M4 的定时器做 PWM,输出给电机驱动或者调光电路,实时性比 Linux 侧好得多。
更进一步的玩法是让 M4 和 A7 侧保持“半协同”。A7 跑着 U-Boot 停在控制台,M4 负责一个实时任务,两者之间如果后续需要通信,可以在共享内存里定义一块协议区域,M4 写状态,A7 通过 U-Boot 命令读取内存检查状态。当然这只是一种轻量协同,真正复杂场景还是得回到 Linux 的 remoteproc 框架。
我个人实际操作里的最大体会是:这条路径适合快速验证“MPU 外设能力 + MCU 开发效率”的组合,但它更适合作为项目前期的快速原型,而不是长期生产系统。如果你只想用一颗芯片同时承担跑 Linux 服务和裸机实时控制,还是要认真设计内存映射和 IPC 方案,参考 ST 官方推荐的 Linux + M4 协作方式。不过单就“把 MP135 当单片机玩”这件事来说,SD 卡脱机跑点灯,绝对是一个性价比极高的入门实验,让我在一晚上之内彻底理解了这个异构芯片的启动闭环。