ARTICLE DETAIL

资讯详情

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

STM32MP135裸机开发实战:不跑Linux的SD卡启动点灯指南

STM32MP135裸机开发实战:不跑Linux的SD卡启动点灯指南 这块板子到我手上第一天就被我改造成了一台“大号单片机”。STM32MP135DK官方定位是跑Linux的入门级微处理器开发板接上屏幕能出桌面、连上网络能跑服务那种。结果我拿到手之后压根没让系统完整启动过一次——我把注意力全放在了另一件事上上电插SD卡让一个LED自己开始闪全程不连调试器、不跑Linux、不给它“解释”的机会。听起来有点暴殄天物但就是这个拧巴的需求让我把A7核的启动链、FIP镜像、链接脚本这些东西彻底搞明白了。这篇内容适合谁玩过STM32F103、C8T6这类Cortex-M单片机的朋友想看看把A7内核的MPU裸奔起来和M内核差异有多大也想给那些希望快速启动、不想为点个灯就背一整套Linux系统的场景找条野路子。我会从SD卡启动原理讲起到手写裸机LED程序再一步步做成脱机运行启动卡最后把我在MP135DK上踩过的坑全部摊开。1. 为什么我放着Linux不用非要和MP135较劲1.1 MP135DK是个什么配置的开发板STM32MP135是意法半导体MP13系列里的入门型号内核是单核Cortex-A7主频可以跑到1GHz板上配了以太网MAC、LCD-TFT控制器、USB、CAN-FD还有一大票和传统STM32同宗同源的外设。MP135DK开发板把这些接口基本都引了出来默认固件是OpenSTLinux板上能直接跑一个带图形界面的Linux发行版。按道理说这种板子就该乖乖跑Linux。但问题在于我想做的事情太简单了就是一个LED翻转。如果走正规Linux路线要么学设备树、交叉编译内核模块要么用libgpiod在用户空间操作GPIO。这一套组合拳打下来光做编译环境就得折腾一晚上启动还要等好几秒。而放在裸机环境下我写几十行HAL代码上电几百毫秒之内LED就能亮起来。这种即时反馈的快感是跑系统给不了的。1.2 MCU和MPU在“点灯”这件事上的区别很多从51单片机或者STM32F1入门的朋友第一次接触A7内核会有点懵因为工作模式完全变了。为了理清思路我把传统单片机和MP135做了个对比。对比项传统STM32单片机STM32MP135内核Cortex-M3/M4/M7Cortex-A7主频几十到几百MHz可达1GHz代码存放内部Flash外部SD/eMMC需ROM引导内存片上SRAM几十到几百KB片外DDR容量可达GB级操作系统裸机或RTOSLinux、裸机或RTOS外设安全无安全隔离概念有RIF安全隔离分Secure/Non-Secure开发工具链MDK、IAR、CubeIDE交叉编译器、FIP打包等表格能直观看到MP135在性能上碾压传统单片机但代价是启动流程和开发方式都复杂了。不过这种复杂主要集中在“怎么把程序跑起来”这个环节一旦程序真正跑起来操作GPIO的HAL接口和STM32F4其实没差太多。这也是我敢把它当单片机玩的最大原因底层逻辑还是熟悉的套路。1.3 裸机玩A7的真正价值把MP135当单片机折腾不是为了说明Linux不好恰恰相反。只有亲手走一遍裸机启动流程你才能真正理解Linux从开机到进系统的每一级发生了什么。ROM Bootloader从哪里找FSBLFSBL怎么初始化DDRFIP里装了什么BL33代表什么——这些东西在跑Linux的时候被重重封装普通开发者根本看不到。裸机点灯就像拆开礼物盒看内部结构虽然过程费劲但收获巨大。2. 准备清单和底层启动原理先把SD卡角色搞清楚2.1 硬件和软件需要准备什么先列一下我这次用到的完整清单硬件部分STM32MP135DK开发板一块16GB microSD卡建议Class10以上先用读卡器在电脑上格式化SD卡读卡器USB Type-C数据线给开发板供电可选ST-LINK/V2或V3调试器用于中间步骤的调试验证可选USB转TTL串口线用于查看启动日志软件部分STM32CubeProgrammer烧写和制作SD卡镜像必用STM32CubeMP135固件包HAL库和官方例程都靠它arm-none-eabi-gcc交叉编译器或者直接用OpenSTLinux SDK里带的工具链串口终端软件比如MobaXterm、PuTTY或者minicom这里有一个容易被忽略的准备工作SD卡格式。有些读卡器插上之后系统能识别但STM32CubeProgrammer不一定认。推荐先用SD Card Formatter这类工具做一次完整格式化再放到开发板上用。我一开始偷懒用Windows自带格式化结果CubeProgrammer识别SD卡时频繁报错格式化成FAT32之后才正常。2.2 A7是怎么找到SD卡上的程序的这是整个项目的核心原理值得多说几句。Cortex-M内核的单片机内部有一块Flash上电后CPU直接从Flash的0x08000000地址开始取指执行整个过程是硬连线的简单直接。但Cortex-A7的MPU不一样芯片内部没有可供用户程序写入的非易失FlashCPU上电时并不知道自己该从哪里执行你的代码。解决这件事的是芯片内部固化的ROM Bootloader。上电后ROM会根据BOOT拨码开关或者引脚配置决定从哪个介质加载程序。当选择SD卡启动时ROM会去SD卡的特定偏移位置读取一个叫做FSBL的镜像也就是TF-A的BL2阶段。FSBL被加载到内部SRAM执行之后会做两件关键事初始化DDR内存然后从FIP包里取出后续镜像把它们加载到合适的内存地址最后跳转执行。FIP是TF-A的标准固件打包格式里面可以放BL31、BL32、BL33等镜像。我们写的裸机LED程序在启动链里就扮演BL33的角色。用一个生活化的类比ROM Bootloader是宿舍楼的管理员他只负责叫醒一层楼长FSBL。楼长起来以后先检查水电初始化DDR和时钟再去叫醒某个具体的学生宿舍这个学生就是我们的裸机程序。SD卡则是一本点名册管理员和楼长都按照册子上写好的位置去找人。2.3 内部SRAM和外部DDR的分工这个过程里还有一个容易混淆的概念FSBL运行在内部SRAM而我们的应用可以放在DDR里运行。MP135内部有一段SRAM具体大小以参考手册RM0476为准容量足够让FSBL完成初始化工作。而外部DDR的物理地址一般从0xC0000000开始裸机程序如果要跑到DDR里链接脚本的加载地址就必须对应这个区域。这里就牵出一个重要问题FSBL里必须包含针对你这块板子的DDR初始化参数。DDR颗粒不同初始化时序和参数就不同。好在MP135DK开发板的DDR参数在官方FSBL里已经配好了我们直接复用就行不用自己调参。如果自己画了板子换了DDR那就得在TF-A里填对应参数否则程序加载到DDR里一定跑飞。3. 裸机LED工程怎么写HAL库、时钟与GPIO配置3.1 从官方GPIO例程改起STM32CubeMP135固件包里其实已经有一个GPIO点灯例程路径通常在STM32Cube_FW_MP13/Projects/STM32MP135F-DK/Examples/GPIO/GPIO_IOToggle。这个例程可以直接打开编译但我建议不要偷懒把它当成一个模板亲手改一改理解每一行代码的作用。打开这个工程会发现HAL库的API和你熟悉的STM32F1/F4系列非常像。HAL_GPIO_TogglePin、HAL_GPIO_WritePin、HAL_Delay这些函数从M内核无缝切换过来。真正的差异在时钟初始化和GPIO安全属性上后面我会单独说。3.2 确认LED引脚先看原理图再写代码开发板的用户LED并不是凭空来的它一定连接在某个GPIO上中间串了一颗限流电阻。MP135DK的板载LED具体连哪个引脚以板子原理图为准。我这里说的思路是通用的打开原理图搜索LED或LED_USER找到网络标号对应的GPIO再在代码里定义宏。在这个例程里我们通常能看到类似这样的宏定义#define LED_GPIO_Port GPIOA #define LED_Pin GPIO_PIN_0具体是GPIO哪个口要看你手上的板子和例程工程的引脚定义是否一致。别想当然地照抄网络上的代码每个版本的开发板可能都不一样。我一开始就是直接抄了个教程的引脚结果LED死活不亮查了半天才发现引脚定义完全不对。3.3 点灯主程序的结构整个裸机程序的核心逻辑非常短#include main.h void SystemClock_Config(void); static void MX_GPIO_Init(void); int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); while (1) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); HAL_Delay(300); } }这段代码和Cortex-M单片机写出来的几乎一样。HAL_Init用来初始化HAL库SystemClock_Config配置系统时钟MX_GPIO_Init初始化GPIO然后就是一个无限循环每隔300毫秒翻转一次LED电平。GPIO初始化函数长这样static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin LED_Pin; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(LED_GPIO_Port, GPIO_InitStruct); }这段代码的核心是先使能GPIO的时钟。注意这里虽然写作__HAL_RCC_GPIOA_CLK_ENABLE()但MP1的RCC机制和M内核系列已经完全不同了。传统STM32是往AHB1ENR寄存器写一个位MP1则分了MC域和MP域两套寄存器具体是RCC_MC_AHBxENSETR这类寄存器。HAL库帮你做了封装所以你不用记底层寄存器名字只需知道必须显式调用这个函数否则GPIO外设根本没有时钟寄存器写了也白写。3.4 关于时钟配置先用内部HSI跑起来SystemClock_Config是很容易被忽略的一个函数。在MP13这种芯片上RCC的时钟树比M内核复杂太多涉及PLL配置、分频系数、总线时钟等一堆东西。第一次折腾的时候我建议用官方例程里现成的SystemClock_Config不要自己重新配置PLL。MP1芯片内部有一个HSI振荡器通常是64MHz上电后默认作为系统时钟使用。如果只想验证点灯这个默认时钟已经足够用了LED翻转速度差一点无所谓。等以后再研究时钟树把PLL配到最高主频去跑性能测试那才是进阶玩法。编译的时候链接脚本也需要对应修改。官方例程通常会提供一个DDR运行的链接脚本把只读数据段、数据段、BSS段的加载地址都放在0xC0000000之后。这是因为FSBL初始化完DDR之后会把程序镜像搬运到这个地址然后再跳过去执行。编译命令大概是这样一个思路arm-none-eabi-gcc -mcpucortex-a7 -mthumb -Os -Tstm32mp135f-dk-ddr.ld \ -o led.elf main.c arm-none-eabi-objcopy -O binary led.elf led.bin生成的led.elf是带调试信息的文件可以用来在调试器里看源码。led.bin是纯二进制后续打包FIP的原料就是它。4. 从“调试器内运行”到“SD卡脱机运行”的落地全流程4.1 中间验证步骤先在DDR里把程序跑起来第一次做这种项目我不建议直接跳到SD卡脱机启动因为链路太长哪一步出问题都不好排查。稳妥的做法是先用调试器把程序加载到DDR里验证一遍。操作方法是连接ST-LINK调试器到开发板的SWD接口打开STM32CubeProgrammer选择对应的外部加载器也就是带DDR初始化参数的external loader脚本。这样CubeProgrammer会先初始化DDR然后把LED程序加载到0xC0000000处直接点击运行。如果这一步LED正常闪烁说明三件事没问题程序逻辑正确、GPIO引脚配置正确、编译生成的镜像没有损坏。如果这一步灯都没反应就不要继续做SD卡了先回第3章排查引脚和时钟问题。这个中间验证步骤能帮你把问题范围缩小一大半省下后面瞎折腾的时间。4.2 制作SD卡启动镜像把led.bin打包成FIP程序在DDR验证通过之后下一步就是把led.bin打包到FIP里再做启动SD卡。FIP是TF-A使用的标准固件包内部可以容纳多个镜像每个镜像都有自己的UUID和加载地址。在我们这个场景里FIP里装的主要是BL31和BL33。BL33就是我们写的led.bin入口地址要放在DDR的0xC0000000处。在TF-A的构建配置里一般通过指定BL33路径和PRELOADED_BL33_BASE来实现。官方STM32CubeMP135包和OpenSTLinux SDK里通常都带了一套FIP生成脚本你需要做的事是编译好led.bin然后在配置项里把BL33指向这个bin文件再执行脚本就会得到一个新的FIP镜像。以你下载的SDK具体版本为准不同版本的配置项名称可能略有差异。这里有一个容易踩的坑默认FIP里塞的是U-Boot也就是Linux启动用的引导程序。我们的需求是裸机程序不是U-Boot所以必须确认FIP里BL33那一段确实是led.bin而不是U-Boot。最简单的方法是看最终生成FIP的大小如果FIP体积好几MB那里面基本还是U-Boot如果FIP很小一两百KB那多半就是裸机程序。4.3 用CubeProgrammer写入SD卡拿到FIP之后还需要一个FSBL镜像也就是TF-A的BL2阶段它负责初始化DDR。这个FSBL一般也用SDK脚本生成不需要自己改。准备好这两个文件就可以做启动SD卡了。打开STM32CubeProgrammer连接开发板和电脑。需要注意烧写SD卡时开发板通常要处于某种下载模式下具体拨码方式看MP135DK的用户手册。把SD卡插到读卡器确保电脑能识别到SD卡设备。在CubeProgrammer界面里找到SD卡烧写功能选择FSBL镜像和FIP镜像然后开始写入。写入的原理要搞清楚CubeProgrammer不是把FIP当作普通文件复制进去而是按固定扇区偏移进行原始写入。ROM在启动时也不是像操作系统一样读取文件系统它会直接跳到SD卡指定偏移位置去读FSBL再把FIP里的内容加载出来。所以你往SD卡里拷一个FIP文件到FAT32根目录是没用的必须用工具写入到正确偏移。烧写完成后把SD卡从读卡器拔出插到MP135DK开发板的SD卡座里。4.4 拨码切换到SD卡启动上电看现象这一步是最激动的把调试器线全拔了用USB Type-C给开发板供电。注意看板子的BOOT拨码开关确保它处于SD卡启动组合的位置。拨码的具体组合不同开发板不一样查用户手册确定不要凭感觉猜。上电之后正常现象是开发板电源LED点亮然后板载用户LED开始以300毫秒的周期闪烁整个过程不需要电脑不需要调试器完全脱机。如果你接了USB转TTL串口线可以同时看到TF-A和FSBL打印的启动日志一行行的初始化信息刷过去最后没有任何内核启动日志因为我们的程序根本没跑Linux直接跳到main函数里点灯了。看到LED稳定闪烁的那一刻恭喜你SD卡脱机运行链路已经完整打通。你手里的MP135DK此刻真的变成了一台“上电就跑”的巨型单片机。5. 踩坑实录LED不亮时我是怎么一条条排查的5.1 现象上电后完全没反应串口也没有任何输出第一个坑就让我卡了一个多小时。插上SD卡上电电源灯亮了但用户LED纹丝不动串口终端也是空的什么日志都没打印出来。我的排查链路是按照启动顺序从后往前推的如果ROM Bootloader工作了至少串口会输出一点东西如果没有输出说明问题在更早阶段。果然检查BOOT拨码开关发现它停留在了默认的eMMC启动位置压根没有切到SD卡启动。把拨码翻到SD卡启动组合重新上电串口立刻刷出FSBL日志LED也开始闪了。这个坑说出来有点不好意思但真的很容易发生。开发板拿到手之后出厂拨码默认值不一定是你需要的。动手之前先花两分钟翻用户手册把每个拨码的含义确认一遍能省下大量排查时间。5.2 现象串口有ROM输出但FSBL加载失败拨码修正之后串口有输出了内容却是加载FSBL失败或者找不到启动镜像。这一步说明ROM已经成功从SD卡找到了启动介质的入口但FSBL的数据读取出了差错。排查重点在SD卡镜像的写入偏移。芯片对SD卡上FSBL所在扇区位置有严格的要求如果写入位置不对ROM就读不到正确数据。用STM32CubeProgrammer烧写时它会自动处理我之前图省事用dd命令手动写结果偏移没算对。这里再次提醒能用官方工具就用官方工具手工操作前先查应用笔记里的偏移表。还有一个小概率原因是SD卡兼容性问题。MP1的ROM对SD卡协议要求比较严格老旧的杂牌卡可能出现识别失败。换一张知名品牌的Class10卡再试能排除掉大量疑难杂症。5.3 现象FSBL加载成功DDR初始化却报错这个问题是在我自己改了TF-A源码之后遇到的。FSBL在启动时会打印DDR初始化相关的日志如果DDR训练失败它会直接停下来无法继续加载FIP。MP135DK用的DDR颗粒参数是官方验证过的正常情况下不应该失败。如果你动了TF-A里的DDR配置或者从别的开发板拷贝了FSBL来用就会碰到这种情况。这时不要瞎调参数把官方FSBL恢复回去确认FIP里的BL33还是我们的led.bin。还有一个隐藏点如果程序的链接地址和DDR的映射地址不匹配程序会加载成功但一运行就hardfault。比如链接脚本里用了0x20000000这种内部SRAM的地址而FSBL把程序加载到了0xC0000000程序直接崩溃表现为LED完全不亮。检查链接脚本的地址确认是DDR基地址。5.4 现象裸机程序能跑但LED始终高电平或者始终低电平如果LED一直亮着但不闪烁问题大概率在GPIO配置本身而不是启动链。先用调试器确认代码确实执行到了while循环里然后检查GPIO的推挽模式、初始电平设置。在这类MPU上还要多考虑一层那就是RIF安全隔离。MP13的GPIO端口中有一组通常被称为GPIOZ在某些启动配置下默认被安全世界持有访问权限。如果你的程序运行在非安全状态而对一个被安全保护的GPIO写了输出寄存器写入会被RIF挡住引脚电平根本不会变化。这和你预期点灯却完全没反应的现象很像。解决思路是在FSBL阶段或者程序初始化时把GPIO的访问权限赋给非安全世界操作GPIOSECR这类安全配置寄存器。注意这一步一定要修改安全配置而不是单改MODER寄存器。这个知识点在官方参考手册RM0476里有详细说明遇到时值得深挖一下。5.5 现象LED闪了但闪烁频率完全不对这个坑是我自找的。为了图快我写SystemClock_Config时只初始化了部分PLL结果系统时钟跑得比预期低很多HAL_Delay(300)实际变成了3000多毫秒LED变成了慢悠悠地呼吸闪。排查思路是HAL_Delay是基于SysTick计数的而SysTick的时钟源自系统时钟。系统时钟没配对延时自然不准。最省事的方法是复用官方例程里的SystemClock_Config先不要自己精简。等点灯流程跑通之后再慢慢研究怎样裁剪时钟配置来缩短启动时间。5.6 现象供电不稳导致偶发不启动这类问题最恶心因为它不是每次都复现。如果把开发板直接用USB线插在电脑前置的USB口上供电电压可能不够尤其是在同时插着调试器、SD卡、串口线的情况下。SD卡写入电流波动、LED负载变化都可能导致FSBL复位重启。我的解决办法是换成5V/2A的独立电源适配器给开发板供电。干净、稳定、容量充足一次性解决所有偶发问题。以后再遇到诡异的不稳定问题先问一句供电干净吗6. 点完灯之后还能怎么折腾6.1 把熟悉的单片机外设一个个搬上来点灯只是开始。A7的裸机环境下UART、SPI、I2C、定时器PWM、外部中断、ADC这些外设的HAL接口和M内核系列非常相似几乎可以无缝切换。接下来可以按这个顺序扩展串口打印俗称裸机printf调试利器按键检测配合外部中断和上下拉电阻消抖PWM输出驱动一个LED呼吸灯或者蜂鸣器定时器计算程序运行时间可以顺便测试A7在1GHz下的真实算力亲手把STM32F103上的经典实验在MP135上重新实现一遍你会发现外设操作逻辑没变但时钟、电源和安全域管理的复杂度上了一个大台阶这正是MPU和MCU差异最直观的体现。6.2 加一个FreeRTOS或者裸机状态机LED点完了程序还不够复杂可以引入一个更工程化的架构。喜欢裸机极致简单风格的人可以自己写一个状态机让LED根据按键状态切换闪烁模式比如快闪、慢闪、SOS闪。这种方式在工控领域很常用Modbus RTU之类的帧接收状态机也可以直接移植到MP135上跑算力绰绰有余。如果想要多任务体验A7上加FreeRTOS比M内核更轻松不用担心内存不够。UART接收任务、LED任务、按键扫描任务各占一个线程配合队列传递消息写起来非常顺手。而且FreeRTOS在A7上的移植库已经相当成熟官方的CubeMP135包或者社区项目都能找到参考。6.3 研究快速启动和极简系统从SD卡启动LED程序这条路走通之后你会发现启动时间其实还有很大的压缩空间。FSBL初始化DDR花费的时间占比很高如果应用体量小完全可以把程序放到内部SRAM运行省去DDR初始化的开销启动时间能压到几百毫秒以内。这个方向很值得深入研究适用场景包括仪表盘、门锁、智能传感器这类要求极短上电响应时间的设备。一旦你把SRAM运行、链接地址调整、FSBL裁剪这三件事研究明白你对整个启动链的理解会比很多开发工程师都深。我还打算下一步给MP135移植一个软核Modbus从站协议栈做成一个通过串口控制LED和继电器的小系统顺便测一测A7核在1GHz下用状态机解析Modbus帧的极限速率。前阵子逛论坛看到有人在讨论单片机的Modbus帧接收写法等我把这套代码适配到MP13上会再写一篇完整记录分享出来。最后再分享两个实际操作中的小技巧折腾完这一轮我最大的体会是别被MPU的复杂度吓住。剥开启动链这层壳里面的HAL库、GPIO操作、链接脚本和单片机世界的套路是一脉相承的。只要肯花一个周末把SD卡启动链路彻底走通一次以后再接触任何A7、A53、A55开发板你都不至于看到启动日志就发怵。顺便一提我给SD卡做镜像之前会先备份一份原始镜像免得哪天想恢复官方系统时还得重新下载好几百MB的固件包这个习惯帮我省了不少事。
返回列表