
最近我把 STM32 入门的全套例程硬是改成了纯软件仿真版不插开发板、不焊排针只要电脑里装上一两个工具就能把 GPIO、串口、定时器、中断这些外设一块一块地跑起来。这就是标题里说的 STM32 仿真入门路线。如果你正处于“想学 STM32但还在犹豫要不要先买板子”的观望期这篇文章就是给你准备的。你不需要立刻下单硬件也能先掌握大部分外设的配置思路和调试习惯。等板子真到了手里你会发现代码迁移成本很低缺的只是“把线接对”和“看实际波形”这两件事。我会把整套方案的选型逻辑、环境配置、跑通过程、踩坑记录全部摊开讲。你可以直接照着操作也可以把这篇文章当做一个“纯软件学习路线图”来规划自己的入门节奏。1. 为什么我要把 STM32 入门做成“纯软件”版本1.1 零硬件起步先拆掉心理门槛很多新手卡在“学嵌入式”和“买板子”之间怕板子吃灰、怕自己坚持不下来、怕快递还没到就没了热情。我见过太多人买了板子之后只跟着视频点了个灯就再也没打开过。真正的问题不是硬件贵而是“开始”这一步太重了。把入门流程改成纯软件化之后整个过程轻了很多你只需要在电脑上写代码、编译、点一下调试按钮然后观察寄存器变化或者看虚拟示波器上的波形。没有接线错误、没有驱动装不上、没有烧录失败导致板子变砖的恐惧。这个门槛一拆掉学习的连续性会好很多我今天写一半明天打开电脑还能接着来。1.2 仿真和真板并不是二选一而是学习路径上的两站有人会质疑仿真而已能学到什么我的看法是仿真和真板解决的是不同阶段的问题。入门阶段的核心矛盾是“外设配置逻辑”和“C 语言操作寄存器”的理解。GPIO 应该配成推挽还是开漏、定时器的预分频和重装载值怎么算、串口的波特率寄存器为什么是那个值——这些东西本质上都是“逻辑问题”它们完全可以在仿真环境里验证。波动的高电平、真实的电流、示波器探头的负载效应那是真板才能教你的东西。所以我把学习路径拆成两站先用纯软件跑通逻辑建立对 STM32 外设模型的基本认知等逻辑扎实了再上真板去处理“逻辑之外的物理世界”。这有点像考驾照之前先玩模拟器你不需要通过模拟器学会判断路况但你需要通过它把油门、刹车、档位这些操作练成肌肉记忆。1.3 这条路对基础的要求并没有想象中高走纯软件仿真路线最舒服的是有 C 语言基础的人会写函数、会用结构体指针就够了。如果再懂一点点数字电路基础比如高电平、低电平、上拉电阻是什么意思那就更好不懂也没关系遇到再查。我不建议完全零基础的人一上来就直接冲寄存器操作。你可以先用库函数或者类似 Arduino 的写法在仿真环境里点灯感受“代码控制硬件”这件事之后再逐步进入寄存器层面。仿真环境的好处是代码写错了不会烧芯片你可以反复试怎么折腾都不心疼。2. 三条路线怎么选我最终留下的仿真组合2.1 Keil MDK 内置 Simulator最纯粹的代码级仿真第一条路线是 Keil MDK 自带的 Debug 模拟器。它不是虚拟硬件电路而是在 PC 上模拟一个 Cortex-M 内核让编译出来的机器码直接在模拟 CPU 上执行。你可以打开外设寄存器窗口查看每一个寄存器的比特位变化这是真板上很难做到的。这个方案的最大优势是零依赖。你只需要装好 Keil把调试目标从硬件调试器切换到 Simulator就能跑代码。对于验证寄存器配置、研究启动流程、排查中断向量这类场景它比真板还方便因为你可以在任意一条指令上停下来一步步看内核的状态变化。缺点是它没有“图形化的电路”。你看不见 LED 在闪、按键被按下只能通过寄存器数值变化来间接判断。对新手来说这种体验不够直观所以通常我建议把它作为逻辑验证的第一站。2.2 Proteus 虚拟硬件把外设“看得见”第二条路线是 Proteus它做的是虚拟电路仿真。你可以在画布上放一个 STM32 芯片模型接着放电阻、LED、按键、虚拟串口终端、示波器然后把 Keil 编译出来的 hex 文件加载到芯片模型里运行。程序跑起来之后LED 真的会亮会灭串口终端真的能收到打印信息示波器能看到 PWM 的波形和占空比。这才是真正意义上“不花钱买板子也有板子玩”的方案。视觉反馈非常直接特别适合入门阶段建立信心。不过要注意并不是所有 STM32 外设都有对应的仿真模型部分型号的外设支持也不够完整。它适合验证 GPIO、UART、TIM、EXTI 这些常见外设遇到更复杂的通信外设或者传感器时序可能就会力不从心。2.3 浏览器里的 Wokwi 在线仿真适合快速分享和碎片时间第三条路线是 Wokwi 这类在线仿真工具。直接在浏览器里打开网页选一个芯片型号可以连线、写代码、看波形甚至能生成一个链接发给别人对方打开就能看到你的仿真运行效果。它对 STM32 的支持目前不如 Proteus 那么全面比较适合做快速 Demo、验证一个很小的想法或者给朋友演示某个外设的工作原理。我一般不会把它作为系统学习的主环境但碎片时间想看一眼、临时验证某个逻辑时它很顺手。2.4 我的搭配建议不同阶段切不同工具路线需要安装适合验证的外设主要局限Keil SimulatorKeil MDKGPIO、UART、TIM、NVIC、SysTick没有图形化电路反馈不直观Proteus 虚拟硬件Proteus KeilGPIO、UART、TIM、EXTI、ADC 基础场景芯片模型覆盖有限HSE 可能卡死Wokwi 在线仿真浏览器GPIO、UART 等基础外设模型不完整不适合复杂工程我的习惯是新写一个外设驱动先在 Keil Simulator 里验证配置逻辑寄存器状态对不对、中断有没有进来确认没问题之后再到 Proteus 里搭一个虚拟电路看视觉效果和波形最后有需要才上真板。这套组合等于把学习成本摊到了三个台阶上每一级都有明确的验证目标。提示Proteus 我只建议用正规渠道的评估版本不要去找来路不明的破解包工程文件损坏的风险比激活失败更麻烦。3. 第一站用 Keil 模拟器跑通 GPIO 点灯理解外设寄存器3.1 从一个空工程开始先把 Debug 切到 Simulator先用 Keil 新建一个空工程芯片选 STM32F103C8然后确认启动文件已经加进工程里。如果你选择芯片时 Keil 问是否自动添加启动文件就选“是”如果没有提示手动把 startup_stm32f10x_md.s 这一个文件加入工程即可。Cortex-M3 没有启动文件代码是跑不起来的因为它要负责初始化堆栈、中断向量表最后跳转到 main。接下来是关键步骤打开 Options for Target进入 Debug 标签页把调试器从默认的硬件调试器切换到 Use Simulator。这一步很多人会忘导致代码编译成功但一点调试按钮就报找不到目标设备。切换到位之后你会发现右侧有个 Dialog DLL 参数这里建议改成和芯片匹配的配置Dialog DLL: DARMSTM.DLL Parameter: -pSTM32F103C8如果不填这一项很多 STM32 系列的外设寄存器窗口可能根本显示不出来你会看到一个空荡荡的外设列表误以为模拟器不支持。这算是 Keil Simulator 最经典的隐藏坑之一。3.2 点灯代码逐行拆解寄存器到底在改什么下面这代码是寄存器版本的点灯例程。我故意不用库函数因为仿真环境里窗口里能直接看到的就是这些寄存器。#include stm32f10x.h void SystemInit(void) {} static void delay(void) { volatile int i; for (i 0; i 200000; i); } int main(void) { // 1. 打开 GPIOC 的时钟GPIOC 挂在 APB2 总线上bit4 是 IOPCEN RCC-APB2ENR | (1 4); // 2. 配置 PC13CRH 管理 Pin8~Pin15PC13 对应 (13-8)*4 bit20 GPIOC-CRH ~(0xF 20); // 先清空这 4 位 GPIOC-CRH | (0x3 20); // 写入 0x3 通用推挽输出50MHz while (1) { GPIOC-ODR ^ (1 13); // 翻转 PC13 的电平 delay(); } }第一件事是开启外设时钟。STM32 的外设默认是不上电的你不打开对应总线的时钟写寄存器等于白写。GPIO 挂在 APB2 总线上所以我们要操作 RCC-APB2ENR 的第 4 位也就是 GIO 端口 C 的时钟使能位。第二件事是配置引脚模式。STM32 的端口配置寄存器分成 CRL 和 CRHCRL 管 Pin0~Pin7CRH 管 Pin8~Pin15。PC13 属于后者每个引脚占 4 位。我想把它配成通用推挽输出 50MHz对应模式位的值就是 0x3。好多人一开始都容易栽在“到底该改 CRL 还是 CRH”“偏移多少位”上用仿真器可以直接看寄存器变化比看手册空想靠谱得多。3.3 在模拟器里观察 GPIO 状态这就是比你买板子还爽的地方代码编译通过后点进入调试模式。打开 Peripherals 菜单找到 GPIO 下面的 GPIOC你就能看到一张寄存器表。单步执行到GPIOC-CRH | (0x3 20);这一行再看寄存器表CRH 对应的比特位会从 0 变成 0x00300000。这个“亲眼看见寄存器被修改”的瞬间是理解外设配置的最佳时机。你再继续执行到 while 循环里每次执行完GPIOC-ODR ^ (1 13);ODR 寄存器的 bit13 都会在 0 和 1 之间跳变。真板上你要点灯只能看到 LED 亮灭很难直观感受到寄存器层面的变化。在 Keil Simulator 里这些变化都是实时、精确、可断点的你可以逐条指令地看。这对于建立“代码控制硬件”的心智模型作用非常大。3.4 模拟器不显示的坑代码能跑但你什么都看不见如果按上面的步骤操作发现 Peripherals 菜单里根本没有 GPIO 选项或者有选项但窗口里一片空白先回 Debug 设置里检查 Dialog DLL 参数。我之前就因为换了工程没调整参数在那折腾了半小时最后发现是-pSTM32F103C8这个参数没填对。还有一点Keil Simulator 模拟的是内核指令集和外设寄存器它不会真的计算 LED 电流也不会考虑外部上拉电阻。你在这个阶段验证的是“寄存器配置对不对、程序流程对不对”而不是“实际电路能不能点亮”。后面到了 Proteus我们才补上“看得到光”这一步。4. 第二站串口、定时器、中断把最小系统跑出内容4.1 串口 printf 输出到虚拟终端验证你写的代码真的打印出来了点灯只是开始学习过程中你会越来越需要“反馈信息”。串口是最常见的反馈通道在仿真环境里同样可以跑。以下代码用 USART1 发送字符串到 Keil 的虚拟串口窗口。我按内部时钟 8MHz 来计算波特率寄存器如果你用的是 72MHz 主频BRR 需要按比例调整我会在代码后面解释。#include stdio.h #include stm32f10x.h void SystemInit(void) {} void UART1_Init(void) { RCC-APB2ENR | (1 14); // USART1 时钟APB2 的 bit14 RCC-APB2ENR | (1 2); // GPIOA 时钟 // PA9 TXPA10 RX GPIOA-CRH ~(0xFF 4); // 清空 PA9、PA10 的配置 GPIOA-CRH | (0x0B 4); // PA9复用推挽输出 50MHz GPIOA-CRH ~(0xF 8); // 清空 PA10 GPIOA-CRH | (0x4 8); // PA10输入浮空 // 内核时钟 8MHz115200 波特率BRR 0x45 USART1-BRR 0x45; USART1-CR1 (1 13) | (1 3) | (1 2); // UE | TE | RE USART1-CR2 0; } int fputc(int ch, FILE *f) { while (!(USART1-SR (1 7))); // 等 TXE 标志位置位 USART1-DR (uint8_t)ch; return ch; } int main(void) { UART1_Init(); printf(\r\nHello, STM32 Simulator!\r\n); while (1); }BRR 的计算方式是这样的USARTDIV 时钟频率 / (16 × 波特率)。8MHz 除以 16×115200约等于 4.34整数部分是 4小数 0.34 乘以 16 后约等于 5所以 BRR 0x45。如果你用的是 72MHz 主频同样的 115200 波特率BRR 则是 0x271。这个差异是仿真环境里串口乱码的最常见根源。编译前还要记得在工程选项里勾选 Use MicroLIB否则 printf 的重定向默认走半主机模式程序会卡死在系统调用上串口什么都输出不了。这也是 Keil 里第一次跑 printf 必踩的坑。进入调试后在 View 菜单里打开 Serial Windows选 UART #1然后全速运行窗口里就会看到那行 “Hello, STM32 Simulator!”。这一步跑通你会真正体会到“仿真也可以有输入输出”的感觉。4.2 SysTick 定时中断理解“代码是事件驱动的”串口能打印了接着就可以看中断。下面用 SysTick 做定时器每 5 毫秒翻转一次 PC13效果就是 LED 以固定频率闪烁。SysTick 是 Cortex-M 内核自带的定时器仿真器支持得最完整不容易踩坑。#include stm32f10x.h void SystemInit(void) {} volatile int tick 0; void SysTick_Handler(void) { tick; if (tick 5) { tick 0; GPIOC-ODR ^ (1 13); } } int main(void) { RCC-APB2ENR | (1 4); GPIOC-CRH ~(0xF 20); GPIOC-CRH | (0x3 20); // 假设内核时钟 8MHzLOAD 8000 - 1即 1ms 一次中断 SysTick-LOAD 8000 - 1; SysTick-VAL 0; SysTick-CTRL 0x07; // ENABLE TICKINT CLKSOURCE while (1); }这里有个重要认知中断驱动的程序主循环看起来可能很“空”但这正是嵌入式系统最常见的状态。CPU 平时要么在低功耗待机要么在等待事件真正干活的代码都在中断服务函数里。你把断点打在 SysTick_Handler 的 tick 那一行全速运行程序每次进入中断都会停下来你就可以观察 CALL STACK 和寄存器区。这种“看到 CPU 如何被硬件打断、跳到中断向量、执行完毕后返回主循环”的体验是仿真环境比真板更容易获得的因为你可以随时暂停也可以单步执行。4.3 追踪中断现场一个仿真器独有的调试技巧Keil Simulator 里调试界面底部有寄存器和栈窗口。当 CPU 跳进 SysTick_Handler 时你可以看到当前的通用寄存器组和 xPSR 状态寄存器。单步执行时你会注意到 SP 指针的变化以及从 Handler 返回时 LR 被设置成特殊值这个机制就是 CM3 内核的异常返回模型。很多书籍讲 Cortex-M 中断时会画一堆流程图但你自己亲眼看着它跳转一次理解效率完全不同。这也是为什么我认为纯软件仿真可以承担“入门”任务它把抽象概念变成了可见的指令级操作学习曲线反而更友好。5. 第三站Proteus 虚拟硬件让外设“看得见”5.1 搭建一个最小虚拟板芯片、LED、按键Keil Simulator 虽然强大但它没有图形电路。所以我会把同一个工程再拿到 Proteus 里跑一遍这样可以看 LED 亮灭、按键状态和虚拟示波器的波形。打开 Proteus 后新建一个设计文件在元件库搜索 STM32F103 系列我常用的型号是 STM32F103C6 或者 STM32F103R6不同版本的 Proteus 里列表会稍有出入按你自己看到的型号选择即可。把芯片放到画布上双击芯片找到 Clock Frequency 这个属性设为 8000000这一步对应我们代码里内部 8MHz 时钟的假设。接着从元件库拉一个 LED、一个 330 欧姆电阻和一个普通按键。LED 通过电阻接到某个 GPIO比如 PC13按键一端接 PB0另一端接地再给 PB0 加一个上拉电阻到 3.3V。这里其实不需要额外接电源符号Proteus 的 STM32 模型默认已经处理了 VDD/VSS这一点比初学者想象的要省事。5.2 加载 KEIL 编译出来的 hex 文件要先确保 Keil 工程里已经勾选了 Create HEX File编译之后在 output 目录里就能找到 .hex 文件。回到 Proteus双击芯片在 Program File 一栏里选择这个 hex 文件。然后点击左下角的运行按钮。如果运行后 LED 没有亮灭动作最常见的原因是程序一启动就停在了某个等待循环里比如 HAL 库初始化时等待 HSE 就绪。Proteus 里 HSE 外接晶振的模型和真实晶振行为有差异最容易的处理方式是在代码配置里把时钟源从 HSE 改成 HSI改成内部 8MHz 时钟避免卡死在时钟就绪等待上。还有一个容易被忽略的点Proteus 加载 hex 之后有时不会自动复位代码状态是空的。此时点击暂停按钮再按一下芯片旁边的复位按钮重新运行灯通常就会闪起来。这种加载顺序问题仿真用多了就会形成肌肉记忆。5.3 虚拟终端和示波器看到输出和波形串口这部分和 Keil Simulator 的验证目标不同。在 Proteus 里我们会放一个叫 Virtual Terminal 的元件它的作用相当于一个串口调试助手。连接方法是芯片的 PA9 也就是 TX 接到终端的 RXD芯片的 PA10 也就是 RX 接到终端的 TXD波特率在终端属性里选 115200运行后 printf 的内容就会实时显示在终端界面里。示波器则用来验证 PWM。你可以按住 Ctrl 搜索 Oscilloscope 元件把示波器探头接到输出 PWM 的引脚比如 PA0。然后用第三重点灯代码同类的 TIM2 PWM 配置代码初始化运行之后示波器上会出现一个固定频率和占空比的方波。我给一组最精简的 TIM2 PWM 配置方便你在 Proteus 里快速看到波形RCC-APB1ENR | (1 0); // TIM2 时钟 RCC-APB2ENR | (1 2); // GPIOA 时钟 GPIOA-CRL ~(0xF 0); GPIOA-CRL | (0xB 0); // PA0 复用推挽输出 50MHz TIM2-PSC 8 - 1; // 8MHz/8 1MHz 计数时钟 TIM2-ARR 1000 - 1; // PWM 频率约 1kHz TIM2-CCR1 500; // 占空比 50% TIM2-CCMR1 0x60; // CH1 PWM 模式 1 TIM2-CCER 1; // CH1 输出使能 TIM2-CR1 1; // 启动计数在示波器里看到方波的那一刻其实已经一只脚踏进“看得见硬件行为”的门了。读代码的时候总觉得 PWM 就是“高低电平变化占空比”但真正看到波形上的那一条条方波线才会把“时基、占空比、频率”这三个词跟实物对上号。5.4 Proteus 与真实板子的差异清单在 Proteus 里跑通了不代表真板一定没问题因为仿真模型做了很多理想化处理。我把踩过的差异列在这里场景Proteus 表现真实板子可能表现HSE 外部晶振容易卡死或行为差异明显需要外部电路配合GPIO 翻转速度模型支持指令级连续翻转受负载电容、走线影响串口乱码只要波特率对一般稳定可能受外部噪声、电平不匹配影响ADC 采样理想电压输入有噪声、参考电压精度问题I2C/SPI 时序模型大体支持器件时序参数必须实测这些差异不意味着 Proteus 没用而是提醒你仿真负责验证“逻辑正确”真板负责验证“物理正确”。两者不可互相替代但可以很好地衔接。6. 仿真跑不出来的部分和我的“回到实板”判断标准6.1 三类仿真一定失真电气、时序、极端场景第一类是电气特征。仿真器不会告诉你 GPIO 输出能力够不够驱动某个负载LED 的真实亮度、功耗这些也都算不出来。这些数据只能在数据手册和真实电路里得到。第二类是外部器件时序。比如一个温湿度传感器要求主机在某个时刻必须精确拉低总线 18 毫秒以上这种时序在 Proteus 里如果模型不支持你的程序逻辑再对也很难验证。到了这一步你需要的已经不是仿真了而是示波器和真传感器。第三类是异常和边界情况。按键抖动、电源纹波、看门狗在异常情况下的行为、多外设同时触发中断的优先级竞争这些问题都属于“真实世界复杂性”。仿真环境通常会忽略噪声和随机性所以永远发现不了松动的杜邦线能带来多少奇奇怪怪的 bug。6.2 我踩过的高频坑汇总纯软件仿真这条路真正容易卡住你的不是代码而是环境和工具链配置。我把出现频率最高的坑统一列出来可以省你不少时间Keil Simulator 的 Dialog DLL 参数没配对导致外设寄存器窗口空白。Proteus 里加载 hex 后忘记复位代码看起来“没运行”。HAL 库工程默认配 HSE在仿真里一直等时钟就绪程序卡死在启动阶段。使用 printf 但没开 MicroLIB程序一跑到输出就卡住。BRR 波特率寄存器按 72MHz 算Keil Simulator 里却用的是 8MHz串口输出乱码。启动文件选错容量比如把 C8 的中等容量工程配成 HD 启动文件中断向量错位后行为诡异。这些坑如果你第一次遇到会相当头疼但经历过一次之后它们就变成了你环境配置的“肌肉记忆”。这也是为什么我把它们集中写在最后这一章目的就是希望你的仿真之路比我当时顺畅一点。6.3 什么时候果断上真板虽然这篇文章主题是纯软件仿真但我必须诚实地说仿真不是万能终点至少这几种情况一定需要真板。第一你想真正理解硬件电路。LED 为什么要串电阻、按键为什么要防抖、晶振起振需要时间这些内容仿真体会不深。第二你想接真实传感器。就算 Proteus 里能找到传感器模型真实器件的时序、噪声、供电问题仍然只有硬件环境能暴露。第三你想做能给别人演示的完整作品。面试、竞赛、公司项目最终都是要跑在真实硬件上的。我给大多数学习者的建议是先用两三周时间在纯软件环境里把 GPIO、UART、TIM、中断跑熟期间顺手复习 C 语言和看原理图等串口 printf 能稳定输出、定时器中断能周期触发、PWM 波形能调占空比之后再下单买板子。这样板子到的时候你已经不是零基础小白而是带着明确目标去验证已有的知识进度会快很多。我现在的工作习惯也保留了这条思路新外设先在 Keil Simulator 里验证寄存器配置再到 Proteus 里看波形最后才上真板。这套流程帮我省掉的不仅是快递费还有大量因为低级配置错误导致的调试时间。如果你也准备把 STM32 入门做成“纯软件”版本希望这篇文章能成为你的第一份踩坑地图。