
最近不少朋友问我不买开发板到底能不能学 STM32说实话以前我肯定摇头因为很多外设、时序、引脚电平这些东西不摸实物总觉得不踏实。但这两年纯软件仿真方案确实成熟了不少我在自己的入门教学里也实验过只要选对工具链不花一分钱买板子照样能把 GPIO、串口、定时器这些外设一个一个跑通而且调试起来甚至比真板子还方便。这篇就把我折腾出来的这套“纯软件版本”STM32 入门路线以及踩过的坑完整分享出来。先说明适用范围。这个方案不是要替代真实开发板适合三类人一是学生党预算有限、想先验证兴趣二是做上位机或应用层的开发者想快速理解寄存器和外设机制三是被硬件折腾到崩溃只想安安静静写代码调逻辑的软件派。当然如果你准备做实际产品还是得回到真板子上验证电气特性但作为入门和原理验证这套软件方案完全够用。1. 方案选型四条纯软件路线的横向对比市面上能做到“不买板子跑 STM32”的方案我实际用下来主要有四条路线各有明显取舍。先给结论想最接近真实开发体验选 QEMU 裸机工程调试体验最接近真机想最快看图形化效果选 Proteus想零配置十分钟跑通直接去网页版仿真平台想顺手把 RTOS 也学了那还是 QEMU 更合适。方案仿真核心上手难度外设覆盖调试能力适合场景QEMU 裸机工程指令级模拟中中等GPIO/UART/Timer基本覆盖断点、单步、寄存器查看正经学寄存器编程、跑RTOSProteus电路级模拟中低丰富含液晶屏、传感器模型弱基本靠LED/串口输出就看个波形、看个灯亮STM32CubeIDE 内置模拟器软件模拟低极简可调寄存器验证代码逻辑网页仿真平台云端模拟极低常见外设弱快速体验、演示教学1.1 QEMU 为什么是首选QEMU 本身是开源模拟器对 ARM 内核的模拟已经非常成熟。它厉害在指令级模拟也就是你写的每条汇编、每个寄存器读写都是在一个模拟出来的 CPU 上真实执行不是“看起来像真的”。这意味着你调试时看到的寄存器值、内存布局和真芯片几乎没有区别。我用的是 QEMU 的-machine netduinoplus2或-machine stm32vldiscovery参数来模拟不同开发板。前者模拟的是带有 STM32F405 的板子后者是 STM32F100。对于入门来说F100 的库函数资料多F405 的主频更高、外设更丰富看个人偏好。我的建议是先选 F100 对应的stm32vldiscovery因为配套的启动文件和链接脚本在网络上能找到一堆踩坑时容易搜到答案。1.2 Proteus 的本质电路级仿真Proteus 最大的优势是它能连虚拟示波器、虚拟 LED、虚拟按键看起来像在做硬件。你甚至可以用鼠标点按键看到 LED 亮灭。这点确实比 QEMU 直观。但它的短板也很明显对 STM32 的库支持老旧新版的 HAL 库代码通常跑不顺基本只能跑标准外设库而且调试能力弱想查一个寄存器值变化过程很痛苦基本是靠“看现象”。我实测下来Proteus 适合做“演示”而非“学习”。比如你想给同学展示串口收发是啥效果用 Proteus 加一个虚拟终端特别直观。但如果你是抱着“我要学会写寄存器”的心态Proteus 会给你一种无力感因为每次改完代码你得重新烧录、重新看图效率低很多。1.3 关于 STM32CubeIDE 自带模拟器和网页方案STM32CubeIDE 自带一个软件模拟器严格来说它只是“CPU 模拟”模拟的是 Cortex-M 内核指令不模拟具体芯片外设。意思是你可以跑空循环、算算术、看变量值但只要一碰寄存器操作比如写 GPIOA-ODR它就会停住或者直接忽略。所以这个方案只适合验证算法逻辑不适合学外设编程。网页仿真平台胜在零配置打开浏览器就能写代码但因为云端环境受限加载稍慢而且工程文件管理不灵活多人协作也麻烦。适合给完全零基础的人建立第一印象。2. 环境搭建QEMU 交叉编译链 调试器的完整流程如果确定走 QEMU 路线环境搭建是最关键的一步。很多人一开始不信这方案就是因为环境没搭对导致一个点灯程序跑了三天都没亮然后回头骂仿真不靠谱。其实真不是仿真的锅十有八九是工具链断点没配对。2.1 工具清单与版本选择QEMU建议用 7.0 以上版本对 Cortex-M 的模拟更完善。Linux 环境可用系统包管理器安装qemu-system-arm这个组件。交叉编译工具链arm-none-eabi-gcc这个没有替代品建议用 10.3 或 11.2 版本新版对 Cortex-M 支持更好编译出来的代码体积更小。调试器gdb-multiarch或arm-none-eabi-gdb配合 QEMU 的-S -gdb tcp::1234参数可以像调试真板子一样打断点、看变量和控制单步。烧录工具QEMU 不需要烧录镜像文件直接通过-kernel参数加载进模拟内存。这里和真机体验差异比较大但也少了很多“烧录失败”的烦恼。2.2 启动一个最小工程一个最小 STM32 工程需要四个文件启动文件startup_stm32f10x.s、链接脚本stm32_flash.ld、主程序main.c以及必要的头文件。这些文件在 ST 官方标准外设库的模板工程里都能找到。你要做的事情就是下载后稍微改下链接脚本的内存布局。我的链接脚本默认长这样仅作示意MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 128K RAM (rwx) : ORIGIN 0x20000000, LENGTH 20K }这是 F100 系列的布局。如果你的芯片是 F405RAM 可能要到 128K。如果这里写错程序运行时会出现奇怪的硬错误HardFault而且 QEMU 不一定能正确提示你只会看到qemu: unhandled CPU exception非常迷惑。2.3 我的编译和运行命令编译命令如下关键路径和文件名根据实际目录调整arm-none-eabi-gcc -c -mcpucortex-m3 -mthumb startup_stm32f10x.s -o startup.o arm-none-eabi-gcc -c -mcpucortex-m3 -mthumb main.c -o main.o arm-none-eabi-gcc -T stm32_flash.ld -nostdlib startup.o main.o -o firmware.elf arm-none-eabi-objcopy -O binary firmware.elf firmware.bin qemu-system-arm -M stm32vldiscovery -kernel firmware.bin -nographic第一次跑通的时候黑窗口里如果能看到串口打印信息那种成就感不亚于真板子点灯。注意最后一行-nographic是把串口输出重定向到终端这个参数非常关键很多教程里不写你运行时就完全看不到任何现象。2.4 调试器的接法如果想打断点再加-S参数让 QEMU 启动后停在第一行指令qemu-system-arm -M stm32vldiscovery -kernel firmware.bin -nographic -S -gdb tcp::1234然后另开一个终端启动 GDB 连接arm-none-eabi-gdb firmware.elf target remote :1234 continue这时你就像有了魔法可以在main函数设断点用info registers看寄存器用x/10x 0x40010c00看 GPIO 控制寄存器的内存值。这个体验真的比真板子用 J-Link 还直观因为你是直接看内存不用额外接线。注意QEMU 的 GDB 调试有一个老坑——第一次continue之前必须先设置断点否则程序会直接跑完再按CtrlC只能停住当前的程序计数器。如果你发现按了CtrlC没反应大概率是程序已经跑到while(1);死循环里了。3. 实战拆解点亮一颗“虚拟 LED”的完整过程有了环境第一个例程当然是从点亮 LED 开始。这一步的价值在于理清 STM32 的 GPIO 编程模型——不是背代码而是搞清楚为什么要把引脚配置为输出、为什么要开时钟。这些在文档和视频里讲过无数遍但在仿真环境里你自己一步步操作一遍理解深度完全不同。3.1 看懂 GPIO 的底层逻辑先说底层关系。STM32 的 GPIO 引脚工作之前必须先打开所在端口的时钟这个时钟开关在 RCC 寄存器组的对应位。如果不打开时钟写 GPIO 配置寄存器是无效的这在仿真里也一样QEMU 会忠实地模拟这个行为。比如 F100 系列GPIOA 端口挂在 APB2 总线上时钟控制的对应位是 RCC-APB2ENR 的第 2 位。然后是配置模式。你要让一个引脚输出高或低电平先要把引脚配置为通用推挽输出。这对应 GPIOA-CRL 寄存器的低四位对应 PA0其中 MODE 位设为0b0011输出模式最大速度 50MHzCNF 位设为0b00通用推挽输出。整个寄存器的操作在 QEMU 仿真里完全生效。这也是为什么 QEMU 适合学原理——你可以逐位修改寄存器然后用 GDB 观察内存变化一次就把 CRL 和 ODR 的关系看懂了。3.2 纯寄存器点灯代码这是一个最简的寄存器版点灯程序#include stm32f10x.h void delay_loop(void) { volatile int i; for (i 0; i 1000000; i); } int main(void) { RCC-APB2ENR | RCC_APB2ENR_IOPAEN; GPIOA-CRL ~GPIO_CRL_CNF0; GPIOA-CRL | GPIO_CRL_MODE0_1 | GPIO_CRL_MODE0_0; while (1) { GPIOA-BSRR GPIO_BSRR_BS0; delay_loop(); GPIOA-BSRR GPIO_BSRR_BR0; delay_loop(); } }这段代码即使没接触过 STM32 也能看懂先开时钟再配置模式然后循环置位/复位 BS0。BSRR 寄存器的设计非常精妙写 1 到高 16 位就是复位输出写 1 到低 16 位就是置位输出。它省去了读-改-写的过程是硬件工程师多年的经验结晶。在 QEMU 里这段代码直接可用编译后烧进去GPIOA 的引脚电平会真实翻转。3.3 HAL 库能不能在 QEMU 里跑很多新学者一上来用的是 HAL 库。我也测试过在 QEMU 里跑 HAL 工程结论是可以但前提是你得把系统时钟初始化部分仔细改一下。HAL 库默认会调用HAL_RCC_ClockConfig等一系列函数配置 PLLQEMU 对部分外设时钟模型的精度不够可能导致卡死。我的建议是入门阶段先写寄存器跑通裸机流程后再切到 HAL。这不是说 HAL 不好而是如果你一上来就 HAL你会把“GPIO 的具体配置过程”当成黑盒遇到问题无法排查。3.4 在 QEMU 里观察 LED 状态的小技巧由于没有真实 LED你怎么知道灯亮了最直接的办法在点灯位置打一个断点然后用 GDB 查看GPIOA-ODR寄存器p/x GPIOA-ODR如果输出是0x1说明 PA0 输出高电平灯虚拟的亮了。也可以写一段代码把引脚电平变化直接映射到串口打印比如每次翻转就发送一个字符*这样运行的时候终端会不断输出星号节奏就是闪烁的周期。实测下来这个方法非常爽因为你相当于用一个示波器在看 GPIO 波形只不过它是打字的。4. 串口、定时器、中断把外设逐个攻破点灯只是热身。软件仿真真正厉害的地方在于串口和中断这两块在真板子上往往需要示波器和逻辑分析仪辅助观察但在 QEMU 里串口输出直接进终端中断触发逻辑可以用调试器精确控制。下面我按我的教学顺序带大家把外设一个个过一遍。4.1 串口 UART从寄存器配置到格式化输出串口是最有成就感的。配置 USART1 的步骤其实也固定打开 USART1 时钟和 GPIOA 时钟把 PA9 配为复用推挽输出、PA10 配为浮空输入然后配置波特率、数据位、停止位。代码写起来就是一个 USART1 初始化函数void uart_init(void) { RCC-APB2ENR | RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN; GPIOA-CRH ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9); GPIOA-CRH | GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9_0 | GPIO_CRH_MODE9_1; GPIOA-CRH ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10); GPIOA-CRH | GPIO_CRH_CNF10_0; USART1-BRR 0x0271; // 假设 8MHz 主频9600 波特率 USART1-CR1 USART_CR1_UE | USART_CR1_TE; }其中 BRR 的计算方法值得单独聊聊。在 8MHz 主频下要得到 9600 波特率分频系数就是 8000000 / 9600 833.3换算成 16 位分数形式往寄存器里填结果近似于0x0271。我不建议你背这个值只要理解了“波特率就是时钟源除以分频系数”这个关系以后换任何芯片都能自己算出来。QEMU 里设置好 USART1 后直接printf重定向到串口所有调试信息都能在终端看到。4.2 定时器 TIM用 SysTick 做精确延时点灯练习里的delay_loop是不精确的软件延时但实际项目中需要精确计时。软件仿真里最合适的实践对象是 SysTick它是 Cortex-M 内核自带的定时器不依赖具体芯片厂商QEMU 对它的模拟非常准确。SysTick 操作极其简单往 LOAD 寄存器写重装值清空 VAL把 CTRL 的 ENABLE 位置 1定时器就开始倒计时。当计到 0COUNTFLAG 变为 1。在 8MHz 主频下如果你想要 1ms 的精确延时就把 LOAD 设为 8000。因为 SysTick 是 24 位计数器最大重装值是 0xFFFFFF所以用这个方式最长可以精确延时约 2 秒。更长延时就用循环嵌套。这个机制和 QEMU 配合得很好实测延时误差几乎为零。4.3 外部中断 EXTI理解中断优先级与 NVIC外部中断是 STM32 入门的一个小分水岭。你要配置 EXTI、NVIC还要写中断服务函数。仿真环境下最大的好处是你可以一步步看中断是怎么触发的。比如给 PA0 配置一个下降沿触发的中断在 GDB 里手动修改输入数据寄存器来模拟按键按下然后立刻停在中断向量表入口这个过程在真板子上是看不到的因为你没法精确控制引脚电平变化的时间点而在 QEMU 里一切都可观测。这里尤其要注意 NVIC 的优先级分组。很多新手第一次写中断代码明明中断触发条件对了程序也走进了 ISR但总被另一个高优先级中断打断导致逻辑混乱。在 QEMU 里你可以用调试器查看NVIC-IPR寄存器的值直观看到抢占优先级和子优先级的比较这是很大的学习优势。4.4 跑通一个像样的综合 Demo当 GPIO、串口、定时器、外部中断一个个跑通之后就可以串起来做一个综合实验比如定时器 1ms 中断驱动一个虚拟计数器串口每 1 秒打印一次计数外部中断按下时清零。这样一个 Demo 做完ST 的入门知识框架基本就建立起来了。我在某个开发者的分享里看到过同样案例他用这种综合 Demo 给零基础学员展示“看似深奥的嵌入式开发其实就是几个外设逻辑的组合”。在我自己的教学里这个说法太贴切了。5. 常见问题排查那些年我踩过的坑仿真环境没有硬件但坑一点不比真板子少。我带朋友踩过不少雷这里专门整理一下方便大家遇到问题时快速定位。现象可能原因解决方式编译通过但运行无任何输出启动文件或链接脚本内存配置与芯片不符核对 FLASH/RAM 地址范围用info files查看加载的段程序停在 HardFault_Handler时钟配置初始化卡死检查系统时钟初始化仿真里直接把时钟源设为内部 HSI绕开 PLL串口输出乱码主频假设和 BRR 计算不一致确认当前工程的实际主频重新计算 BRR调试器continue后程序跑飞未在 main 入口设断点或用了优化编译编译加-O0启动后先break main中断服务函数不执行NVIC 未使能或优先级分组异常用 GDB 查看NVIC-ISER和NVIC-IPR寄存器运行速度比想象中慢很多QEMU 在模拟外设时会有性能开销正常现象可以用-cpu max参数提升一点性能5.1 编译优化导致的诡异现象我特别想展开的是编译优化这个坑。曾经有人点灯代码在真板上好好的到了 QEMU 里怎么也看不到串口输出查了半天发现是编译时用了-O2编译器把delay_loop直接优化没了因为循环体是空的没有任何副作用。这个在真板上因为物理时间存在你感觉不到但在仿真里优化后整个延时变成零GPIO 翻转频率极高终端输出刷不过来了。解决方式很简单延时循环的变量必须加volatile或者直接统一用-O0。我自己的习惯是入门阶段全部-O0等理解了代码行为再循序渐进引入优化否则很容易引起不必要的挫败感。5.2 链接脚本导致的内存陷阱还有一种情况是链接脚本的栈大小设置太小。QEMU 对栈溢出不报警告但你调用多层函数或开局部数组时程序就会莫名其妙跳进 HardFault。这个真板子上也一样但在仿真里更难察觉。我教你一个排查技巧在 GDB 里看sp栈指针和链接脚本里的 RAM 上限如果sp地址超过了 RAM 范围那必然是栈溢出。你可以用p/x $sp查看栈指针当前地址用info registers看更多上下文。另外QEMU 里没有经过真实的上电复位波形有些依赖复位后初始化状态的外设行为可能和真板略有差异。但这不是问题反而是个学习机会——它告诉你在嵌入式中不要依赖任何“隐形的初始状态”所有外设使用前必须显式配置。6. 进阶玩法从仿真走向系统级学习如果你已经完成了上面的外设实验还想更近一步纯软件方案也能支持。这里分享两个我验证过的进阶方向。6.1 在 QEMU 上跑 FreeRTOSFreeRTOS 的 Cortex-M 移植代码很成熟QEMU 可以完美运行。我实测跑过两个任务一个点灯一个串口打印调度正常。这个进阶的价值在于任务切换、优先级抢占、信号量这些抽象概念在 QEMU 的调试器里是可以逐步观察的。你甚至可以在任务切换的瞬间打断点查看当前运行的任务控制块。这种级别的可观察性在真板子上很难做到因为任务切换太快了。跑 FreeRTOS 的技巧需要给链接脚本增加一块堆空间FreeRTOS 的configTOTAL_HEAP_SIZE不要设太大8K 足够跑三四个任务。注意在 QEMU 的-M stm32vldiscovery上F100 只有 8K RAM所以任务栈尽量小一点否则会 settle 到内存溢出。6.2 用 CI 自动跑嵌入式测试另一个有意思的玩法把 QEMU 集成到 CI 流程里。每次提交代码后自动编译、自动启动 QEMU、自动跑一段断言脚本验证某个 GPIO 是否按预期翻转某个串口是否输出了特定字符串。这就是某种意义上的“嵌入式单元测试”。虽然在真板子上也能做类似事情但接入 CI 的便利性差异很大尤其对团队协作来说这能把回归测试的成本降到非常低。我试过一个简单的做法在 CI 脚本里用timeout命令限制 QEMU 运行时间然后把串口输出和预期文本做 diff。这个方案在真板子上通常需要一套硬件在环测试环境而在 QEMU 里一行脚本搞定。对学习者来说这提供了一个不碰硬件也能养成“测试思维”的机会。7. 最后的个人体会我把这套纯软件路线带过不少人入门最大的收获是它把“害怕焊板子、害怕烧录失败”的焦虑感彻底拿掉了。学习 STM32 的阻力很多时候不是知识本身而是环境搭建和排错太打击人。软件仿真虽然不能替代项目的最后验证但对于理解处理器如何工作、外设如何映射到寄存器、中断如何被内核响应它提供的可视化体验是实打实的。如果你刚开始学 STM32或者被硬件问题弄得原地踏步不妨先花几天把 QEMU 环境搭起来从点灯开始一步步把外设跑通。等你在虚拟世界里游刃有余了再拿起开发板的时候你会发现那些曾经陌生而可怕的寄存器名字已经像老朋友一样熟悉了。仿真只是起点但一个好的起点足以让你走得很远。