ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32MP13x裸跑开发实战:在Cortex-A7上运行无OS应用程序

STM32MP13x裸跑开发实战:在Cortex-A7上运行无OS应用程序 直接以从业者口吻开篇点出STM32MP13x、cortex A、裸跑应用程序、MCU这几个关键词说明这篇博文会讲什么、能解决什么问题、适合谁看。然后按标准骨架展开。1. 项目背景与裸跑方案选型1.1 为什么要在Cortex-A上裸跑应用程序先说结论STM32MP13x是一颗集成了Cortex-A7内核的微处理器MPU平时大家用它都是跑Linux但很多人没意识到它其实可以像MCU一样在不带操作系统的情况下直接裸跑应用程序。这听起来有点反直觉——Cortex-A不就是为了跑高端系统才存在的吗但实测下来这种做法在某些场景下非常香。我最早接触这个想法是因为一个工业控制项目。客户要求用一颗芯片搞定人机交互和实时控制但又不想上复杂的嵌入式Linux因为Linux启动慢、实时性不可控还要养一个专门搞系统的工程师。我当时就想能不能用MPU的高主频和丰富外设但用MCU那套裸机开发方式答案是能而且STM32MP13x把这条路铺得挺平。它在硬件上延续了STM32家族的外设寄存器风格软件层面又有官方提供的裸机例程和链接脚本你甚至能找到类似MCU的HAL库支持。这就让习惯了STM32F4、H7的工程师几乎可以无缝迁移到Cortex-A上。裸跑最大的好处是确定性和低资源占用。没有操作系统的调度开销中断延迟是微秒级的内存占用也固定——你写多少就是多少没有内核偷偷吃掉的几十MB。对于电机控制、数据采集、协议转换这类对实时性有硬要求的场景这比Linux靠谱得多。而且启动时间可以压到几百毫秒甚至更快很多设备上电就要干活没时间等Linux慢慢初始化。当然裸跑也有代价你需要自己处理所有底层硬件包括内存管理、中断控制器、外设时钟甚至启动引导。不像在Linux下设备树和驱动帮你把硬件抽象好了。但反过来想这正是熟悉的MCU开发方式——直接用寄存器操作看参考手册查地址。STM32MP13x的价值在于它给了你一颗600MHz的Cortex-A7带浮点单元和丰富接口但你可以用裸机思路去控制它。这篇文章我会从零开始带你把一个完整的裸跑应用跑起来。1.2 STM32MP13x的硬件特点与裸跑可行性STM32MP13x系列基于单核Cortex-A7主频最高1GHz我看有些型号是600MHz起内置了SRAM和DDR控制器。跟纯MCU最大的区别是它需要外部DDR内存来运行程序但也有内置的128KB SRAM可以应急用。裸跑时你可以直接把代码放在SRAM里省去DDR初始化的麻烦不过如果应用比较复杂还是得用DDR。从外设角度看STM32MP13x配备了两个带FIFO的UART、多个SPI/I2C、定时器、ADC、以太网MAC、USB等等这些外设的寄存器布局和stm32标准库风格一脉相承。我用的时候感觉就像操作F103一样直接找偏移地址写值就行。芯片内部还集成了一套内部稳压器和锁相环你可以用24MHz外部晶振通过寄存器配置出600MHz的CPU频率。裸跑的可行性还取决于启动流程。STM32MP13x内置了ROM bootloader它支持从SD卡、NAND、SPI NOR等介质启动。你可以把编译好的裸机程序放到这些介质上ROM会把代码拷到DDR然后跳转执行。这跟MCU从Flash启动类似只不过现在启动介质和地址映射要你自己负责。官方还提供了一个叫STM32CubeProgrammer的工具可以直接通过USB或UART把裸机固件烧到SD卡里非常方便。我个人的感受是这颗芯片天生就支持裸跑因为它继承了ST多年积累的MCU生态。不要被“MPU”这个标签吓住把它当成一颗内存大一点、频率高一点的MCU来用很多操作就顺了。后面我会详细拆解如何配置时钟、中断、串口把第一行裸机代码跑起来。2. 开发环境搭建与工具链准备2.1 硬件准备开发板、调试器与串口线要复现这套方案首先得有一块STM32MP13x的开发板。我手上这块是ST官方的DK板Discovery Kit不过市面上也有第三方的核心板接口都差不多。开发板重点看三样东西电源通常5V USB供电、启动拨码开关用来选择从SD还是从USB启动、调试接口通常是ST-LINK或JTAG。如果你用的是第三方板一定要看好原理图知道串口和LED接的哪个引脚。调试器方面官方板集成ST-LINK/V2直接USB线连电脑就行。第三方板可能需要外接一个ST-LINK或J-Link。我建议至少备一个ST-LINK因为后面调试裸机程序时单步走、看寄存器都靠它。串口线得备两根一根接调试串口用于打印log一根接用户串口如果应用要通信。买那种带USB转TTL芯片的线注意电平板子一般是3.3V别拿5V的去怼。启动方式要提前想清楚。我习惯用SD卡启动因为烧录简单改程序也方便。准备一张microSD卡格式化成FAT32即可。开机之前把拨码开关拨到SD启动模式——具体位置看板子丝印官方的UG文档里画得很清楚千万不能拨错不然ROM找不到启动介质板子直接挂死。2.2 软件工具链arm-none-eabi-gcc、链接脚本与烧录工具裸机开发的核心工具链是arm-none-eabi-gcc。这套GCC交叉编译器是专门为ARM Cortex内核准备的编译出的二进制不依赖操作系统正好符合裸跑需求。安装方式很简单在Linux下用apt直接拉或者从ARM官网下载tar包解压。Windows下推荐用mingw版不过我还是更建议在Linux环境做开发命令行操作链子更顺畅。除了编译器还得有链接脚本和启动文件。链接脚本.ld文件决定了代码段、数据段、BSS段放在内存的哪些地址。对于STM32MP13x你要根据启动介质和DDR映射来写。官方ST在裸机例程里通常会提供一个.ld模板我强烈建议直接基于那个改不要自己从头写。因为Cortex-A的内存映射比MCU复杂比如DDR地址在0xC0000000附近而SRAM在0x2FFC0000写错了程序根本跑不起来。烧录工具推荐STM32CubeProgrammer。它不仅能烧Linux镜像也能烧裸机固件。你编译出来一个.bin文件用CubeProgrammer选择SD卡作为存储介质把这个bin文件放到SD卡指定位置比如/u-boot/或某个分区的固定偏移然后重新插卡开机ROM就会自动加载它到内存执行。如果是用JTAG调试GDB也可以直接load.elf文件到内存这种方式用于调试更直观。我个人经验是第一次搭环境时一定要把工具链路径和板子的默认启动配置确认好尤其是链接脚本里的内存起始地址。ST官方的STM32CubeMP13包里面有现成的裸机示例建议先把它编译一遍验证环境再改自己的代码。别一上来就自己写所有寄存器操作工程狗的命也是命能省事就省事。2.3 第一个验证例程点亮LED环境搭好之后先做一个最简单的验证——点灯。别小看这一步它能确认你的工具链、烧录路径、启动配置全部正确。我从官方例程里提了一个GPIO控制的代码改了一下引脚编译后直接把二进制放到SD卡上板子上电后LED就闪起来了。这一刻你其实已经完成了一个裸跑应用的完整闭环编译、烧录、执行。具体操作上你得先找到开发板LED对应的GPIO号。以DK板为例LED1通常挂在PH7上具体看数据手册的引脚复用表。GPIO的操作还是老一套先使能GPIO时钟通过RCC寄存器组然后把引脚模式配成输出最后往输出数据寄存器写0或1。时钟使能这块比MCU多了一点复杂度因为MPU有一个叫RCC的寄存器组里面按总线分了AHB、APB等每个外设的时钟门控都要单独打开。我踩过的坑是忘了配置引脚复用功能。在MPU上某些GPIO引脚默认是其他功能必须在GPIO_AFRL或GPIO_AFRH里写对应的复用编号才能让输出信号走到正确的引脚上。MCU上也有类似机制但MPU上更严格不配就全是高阻态LED死活不亮。所以如果你照抄我的代码发现不亮第一步先检查复用配置。3. 裸跑应用程序核心开发步骤3.1 Cortex-A7启动流程从复位向量到C语言入口Cortex-A7的启动流程和Cortex-M完全不同。M内核有硬件自动初始化栈指针和读取向量表但A7没有这么友好。A7从上电复位开始会从0x0或0xFFFF0000取决于异常向量地址配置取第一条指令然后由你提供的启动代码负责后续所有初始化。这意味着你必须自己写一段汇编或者用ST的启动文件完成以下动作设置栈指针、配置时钟、初始化内存、跳转到main。STM32MP13x的ROM bootloader其实已经做了一部分初始化比如把DDR配置好、把代码从SD卡加载到内存里然后再跳转到你的程序。但跳转过去之后CPU运行在SVC模式特权模式栈是空的异常向量还没配外设时钟也是默认状态。所以你的启动代码至少要设置SVC模式的栈指针可选地初始化其它模式的栈然后清BSS段最后调用C语言的main()。这里有个容易忽略的细节Cortex-A7是VMSA虚拟内存系统有MMU。裸跑时可以关掉MMU直接访问物理地址但你必须保证代码里没有用到虚拟地址映射。如果你不显式关闭MMU上电后MMU是关闭的默认直接访问物理内存这倒是省心了。但我还是建议在启动代码里明确写一段不使能MMU的注释后期想加缓存时再打开。内存初始化也是个关键点。虽然ROM帮你初始化了DDR但如果你把裸机程序放在SRAM里跑比如启动直接从SD卡拷到SRAM那DDR用不用都行。我更推荐先用SRAM跑通裸机因为SRAM在0x2FFC0000不需要任何初始化速度也比DDR快适合小代码量练习。等应用变大了再切到DDR。3.2 链接脚本与内存布局设计写链接脚本是裸机工程的核心工作。它的作用是告诉编译器.text放哪.data放哪BSS放哪。对于STM32MP13x如果你用SRAM运行起始地址就是0x2FFC0000大小约128KB。如果你用DDR起始地址一般是0xC0000000地址可能因型号而异看参考手册内存映射图我记得官方DDR bank 0在0xC0000000。我下面以SRAM为例展示一个简单脚本。ENTRY(_start) MEMORY { SRAM (rwx) : ORIGIN 0x2FFC0000, LENGTH 0x20000 } SECTIONS { .text : { *(.text*) } SRAM .rodata : { *(.rodata*) } SRAM .data : { *(.data*) } SRAM .bss : { *(.bss*) } SRAM }这里我把所有段都放在SRAM里因为SRAM是RAM不需要搬运。但如果你代码最终从DDR运行链接地址要写成0xC0000000但启动时ROM把代码加载到DDR的具体地址要和链接地址一致否则跳转就跑到错误的地方了。一个常见错误是链接脚本里用了ORIGIN但烧录时没有按这个地址存放导致PC跑到无效地址。所以务必保证链接地址、烧录地址、实际加载地址三者一致。BSS段清零很重要。C语言里未初始化的全局变量默认是0但这个要求编译器生成的代码在main之前把BSS段全清0。你没清的话那些变量就会带着SRAM里的随机值行为完全不可预测。ST的启动文件里有一段循环清BSS我建议直接沿用或者自己写一个很简单但必不可少。3.3 汇编启动代码与链接器入口启动代码是整个裸机工程的引擎。我提供一个最精简的版本它做的事情就是设置栈、清BSS、跳转main。请确保它作为第一条被链接进.text段靠ENTRY(_start)指定入口。.syntax unified .arm .section .text.startup .global _start _start: /* 设置SVC模式栈指针 */ ldr sp, _stack_top /* 清BSS段 */ ldr r0, __bss_start ldr r1, __bss_end mov r2, #0 bss_loop: cmp r0, r1 bge bss_done str r2, [r0], #4 b bss_loop bss_done: /* 跳转到C入口 */ bl main hang_loop: b hang_loop这个启动代码里_stack_top和__bss_start/__bss_end都是链接脚本导出的符号所以你的链接脚本里要定义它们。我刚开始学的时候老忘了导出这些符号链接直接报错说找不到。简单说你可以在链接脚本里加一行_stack_top ORIGIN(SRAM) LENGTH(SRAM);然后把BSS段结束地址放到那个位置。为什么栈指针要放在SRAM的最高地址因为ARM的栈是向下生长的你把栈顶设为内存最高点栈向低地址增长就不会踩到堆。如果你放在中间栈很容易溢出到代码区一旦出了问题排查起来极度痛苦。用ldr sp, _stack_top这种伪指令让汇编器帮你生成常量加载比直接mov sp, #0x2FFC0000更灵活。此外要注意代码中不能有依赖绝对地址的跳转除非你使用位置无关代码PIC。裸机场景下最好直接使用绝对跳转但你必须保证代码被加载在链接地址指定的地方。我的建议是用ldr加载地址而不是bl直接跳转避免重定位的麻烦。启动代码里就这些讲究写清楚后面就顺了。3.4 外设操作串口打印和GPIO控制启动代码跑起来后C代码里的main()就可以像写MCU那样操作外设了。我第一个做的外设是串口因为调试信息全靠它。STM32MP13x的UART模块跟STM32H7很相似有RCC_APB2ENR里的UART时钟门控有USART_BRR波特率寄存器有USART_ISR状态寄存器。你要做的就是照着参考手册填寄存器。我的串口初始化流程是先打开UART时钟再配置引脚到UART复用功能然后设置波特率、数据位、停止位最后使能UART。波特率计算方式跟MCU一样BRR fck / (16 * baud)比如你要115200时钟是64MHzBRR就是64e6 / (16 * 115200)取整为34误差在可接受范围。注意ARM的整型除法要先把频率放大再除不然误差会很大。GPIO控制更简单。先使能GPIO时钟配置MODER寄存器为输出模式再把OTYPER设为推挽最后往ODR写值。这跟STM32F4的操作几乎一模一样所以我上手特别快。我建议在点灯例程基础上加一个串口打印这样你就能看到程序执行到哪里方便调试。下面是一个简化版的串口发送例程void uart_send_char(char c) { while (!(USART3-ISR (1 7))); // 等待TXE USART3-TDR c; }USART3-ISR的bit7是发送数据寄存器空标志这跟F4的SR寄存器不同要特别留意。F4用的寄存器名是SR和DRMPU上改成了ISR和TDR而且标志位含义也有细微差别。如果你习惯F4的写法直接照搬会卡住因为地址不一样。3.5 编译、链接与生成最终镜像写好代码之后编译命令通常长这样arm-none-eabi-gcc -c -mcpucortex-a7 -mthumb -mfloat-abihard -mfpuneon-vfpv4 start.S main.c -o start.o main.o arm-none-eabi-ld -T stm32mp13.ld start.o main.o -o app.elf arm-none-eabi-objcopy -O binary app.elf app.bin第一行是编译-mcpucortex-a7指定目标内核-mthumb表示使用Thumb指令集但启动代码最好用ARM状态所以我习惯在start.S里用.arm指令强制使用ARM状态C代码可以继续用Thumb。-mfloat-abihard -mfpuneon-vfpv4是开启硬浮点A7自带VFPv4用硬浮点能大幅提升运算性能特别是做信号处理时。第二行是链接-T指定链接脚本。第三行用objcopy把elf转成纯二进制bin这个bin才是可以直接烧录的镜像。注意如果你要在CubeProgrammer里烧录bin文件不需要额外加头部但SD卡启动时可能要求把代码放在特定偏移上具体看参考手册的boot配置。我常犯的一个错误是忘记在链接时加-nostdlib结果链接器试图找入口main或者libc里的一些符号搞得警告满天飞。裸机环境下没有标准库你也没有操作系统调用所以一定要加-nostdlib和-nostartfiles告诉编译器“我全自己来”。否则链接器会引入一堆你不想要的启动代码导致bin文件体积暴增。4. 调试技巧与常见问题排查4.1 串口无输出或乱码的处理我遇到过好几次串口完全没有输出的情况排查到最后基本只有两类原因引脚没配好或者时钟没开对。引脚没配好表现为程序卡在上电后或者串口电平完全没反应时钟没开对则更隐蔽因为程序不报错只是输出全黑。先说引脚。STM32MP13x的串口脚在GPIO控制器里你需要配置AFR寄存器选择复用功能。比如USART3的TX是PB10你得设置GPIOB-AFRH ~(0xF 8); GPIOB-AFRH | (0x7 8);这里7是USART3的复用号。你从参考手册的引脚复用表里能找到每个外设对应的AF号别嫌麻烦这一步错了说什么都不通。再说时钟。UART在APB总线上你得先打开RCC-APB1ENRUSART3挂在APB1上对应位。同时GPIO在AHB总线也要打开RCC-AHB2ENR的GPIOB时钟。这两个时钟不打开写寄存器等于白写硬件根本不理你。乱码的问题多半是波特率算错了或者外部时钟不对。如果板子上的SCLK不是24MHz而是其他频率你按24MHz算出来的BRR就会偏差导致波特率对不上。解决办法是用逻辑分析仪或者示波器量一下TX脚的实际波形也可以先用低速的9600测试把问题缩小。我后来干脆在代码里通过SystemCoreClock变量来读实际配置的时钟频率再动态算BRR这样就不怕板子和手册不一致了。4.2 链接脚本引出的启动失败与对齐问题很多人在裸机启动时遇到HardFault或者直接没反应回头检查代码和寄存器没错最后发现是链接脚本里忘了加对齐。ARM内核要求指令在奇地址时进入Thumb模式但如果你链接地址是偶数、且没有定义入口是Thumb还是ARMPC一取指就可能跑飞。我强烈建议启动代码用.arm强制ARM状态链接脚本也把.text起始地址ALIGN(4)保证4字节对齐。我见过有个同事直接在启动代码里写bl main但链接脚本里没有正确导出符号main导致链接器报错或者跳转到错误地址。虽然编译会报“undefined reference”但有时候你用弱符号或者优化选项它可能悄悄用了一个无效地址然后程序跑飞。这种问题看汇编输出最直观所以当你怀疑启动跳转有问题时就反汇编一下看看bl后面的地址是否在代码段内。另一个容易忽略的点是SRAM大小。如果你程序编出来超过128KB硬塞到SRAM里链接时不会报错但运行时会覆盖到其他内存数据行为完全随机。更好的办法是尽早切到DDR运行或者用LENGTH宏限制一下让链接器在超限时报错。4.3 中断配置与裸机实时性优化裸机做实时控制时中断是最常用的手段。但我第一次在A7上配置中断时差点被绕晕。跟Cortex-M的NVIC不同A7有一个通用中断控制器GICGeneric Interrupt Controller。GIC v2是A7的标准配置它分为两部分GICD分发器和GICCCPU接口。你要做的第一步是使能GIC然后配置中断优先级、使能外设中断、注册中断处理函数。GIC配置的坑在于你不仅要设置外设的中断使能还要配置GICD的优先级掩码和GICC的优先级阈值。否则中断虽然产生了但GIC不会把它送到CPU。我建议裸机初学阶段先不要碰中断用了轮询就挺好等主循环能跑起来了再慢慢加入中断优先级管理的意识。实时性方面裸机最大的优势就是确定性。但前提是你不要在主循环里搞阻塞延时比如while延时。应当尽量用定时器产生节拍配合状态机调度。STM32MP13x有多个高级定时器主频高非常适合做微秒级的时基。我习惯把系统节拍定为1kHz用Systick实现这样延迟和超时判断都统一走这个节拍整个程序的调度就非常干净。4.4 常见问题速查表现象可能原因排查思路板子上电无反应启动介质没选对检查拨码开关确认SD卡或USB模式串口无输出时钟未开启 / 引脚复用错误检查RCC外设时钟、GPIO_AFR串口输出乱码波特率计算错误示波器量波形核实SYSCLK频率LED不亮GPIO模式/复用未配置查MODER、OTYPER、AFR启动跳转HardFault链接脚本地址不匹配检查入口地址和加载地址是否一致代码超过内存程序体积超SRAM用DDR运行或裁剪功能中断不触发GIC未初始化检查GICD和GICC寄存器配置这张表是我做项目时自己整理的每次解决完一个怪问题就更新一次。你们遇到类似现象时可以直接照着排查省去不少翻手册的时间。5. 实操心得与进一步扩展5.1 从MCU迁移到Cortex-A裸跑的经验总结如果你有STM32F103或者F407的开发经验迁移到STM32MP13x裸跑就是“熟悉里有惊喜惊喜里夹杂着一丝别扭”。外设寄存器风格相似所以操作GPIO、UART、I2C这些几乎可以照搬。别扭的地方在于内存映射和启动流程你要适应没有启动文件自动处理一切还要自己面对GIC和MMU这些A核独有机制。我个人的建议是先把“裸跑”当成一个低门槛的入口一旦你的应用需要跑协议栈、文件系统或者图形界面你就得考虑是否要上Linux或者RTOS了。比如你要跑TCP/IP协议栈裸机确实能写但工作量巨大。而如果你只是做一个电机控制器、一个采集卡、一个协议转换器裸跑完全够用还特别稳。5.2 工具链选择与项目模板复用做裸机开发我最后固定下来的工程模板包含三个关键文件start.S、main.c、stm32mp13.ld。每次新项目我就拷贝这个模板然后改外设初始化代码。这样搭建一个新应用的周期能控制在一天以内而不是每次从零开始写底层启动代码。你们做新项目时也建议搞一个自己的“裸机库”把串口、定时器、GPIO封装成通用模块。工具链方面不必非得用ST官方推荐的IDE我一直在用VS Code配Makefile配合cortex-debug插件插上ST-LINK就能单步调试。有人觉得A7裸机调试很难但其实你用GDB直接连ST-LINK的OpenOCD跟调试STM32F4差不多。只要你把.elf文件所在的符号表导入就能看到每个C变量和寄存器状态。这比靠串口log猜问题高效多了。5.3 进一步扩展与RTOS结合、DDR编程、多核协作如果你在裸机上跑了一段时间发现任务一多调度起来心力交瘁可以考虑引入一个轻量级RTOS比如FreeRTOS或者Zephyr。STM32MP13x的Cortex-A7完全能跑FreeRTOS SMP版本多任务调度、信号量、消息队列这些就都有了。不过从裸机到RTOS你仍然自己控制启动代码只是把main里换成osKernelStart()这对已经熟悉裸机的你来说很平滑。另一个扩展方向是DDR编程。当你把代码放到DDR后裸机程序可以享受几十MB甚至上百MB的内存空间跑一些复杂的模型或者大数组都不成问题。但DDR初始化配置比SRAM麻烦你需要在启动阶段通过寄存器初始化DDR控制器或者依赖ROM的预初始化。我建议刚开始别碰DDR先SRAM跑顺了再去研究DDR配置。最后是多核协作。STM32MP13x虽然是单核A7但有些型号好像还带一个Cortex-M4协处理器准确说那些是Linux用的异构方案。如果你手上是异构版本你可以让Cortex-A核跑Linux或者裸机应用同时让M4核做实时控制两个核心通过共享内存通信。这是更高级的玩法适合以后深度开发时再研究。我最后想说的是裸跑Cortex-A并不神秘它不过是用MCU的思维驾驭一颗性能更强的芯片。只要第一步点亮了那个LED你就能慢慢体会到这种开发方式带来的自由和控制感。希望这篇分享能帮你省掉一些我当初踩过的时间坑早点把应用跑起来。
返回列表