
1. 为什么我要把 STM32 入门做成纯软件版本刚接触 STM32 的人十有八九卡在同一个地方板子还没买教程已经看了一堆环境装了一半结果发现光是一个点灯程序就得先解决下载器驱动、串口识别、供电跳线这一堆破事。我见过太多人热情就消耗在“等板子到货”的那几天里等板子真到了反而没那个心气儿了。所以我就琢磨能不能把 STM32 的入门过程完全搬到软件里跑。不买开发板不接杜邦线不插下载器就靠一台电脑把 GPIO、定时器、串口、ADC、PWM 这些外设一个一个跑通。这个想法听起来有点“脱裤子放屁”但实际上它解决了一个非常现实的问题学习路径的启动成本。STM32 仿真这件事核心就是用软件去模拟单片机的行为。你在电脑上写代码、编译、然后在一个虚拟的 MCU 环境里运行观察寄存器的变化、外设的输出、中断的触发。它不能完全替代真板子但用来理解外设的工作原理、验证代码逻辑、熟悉寄存器配置绰绰有余。尤其是对于刚入门的人来说仿真环境能让你把注意力集中在“代码怎么写、寄存器怎么配”上而不是“为什么串口又识别不到了”。这篇文章适合三类人第一类是完全零基础、还没买板子但想先学起来的第二类是买了板子但手头没有、想先预习的第三类是有板子但想快速验证某个外设逻辑、不想每次都烧录的。我会从工具选型开始把整个纯软件环境的搭建、外设的逐个跑通过程、以及我踩过的坑全部拆开讲清楚。2. 仿真方案选型与核心工具链拆解2.1 为什么选 Keil MDK 仿真器组合市面上能跑 STM32 仿真的方案不止一种我试过几种组合最后稳定下来的方案是Keil MDK 配合其内置的仿真器。原因很简单Keil 的仿真器对 STM32 的外设模拟支持比较完整尤其是 GPIO、定时器、串口这些常用外设基本能做到“代码怎么写仿真就怎么跑”。而且 Keil 本身就是 STM32 开发的主流 IDE你在这个环境里写的代码将来直接烧到真板子上几乎不需要改动。另一个常见的方案是STM32CubeIDE 配合其调试器但 CubeIDE 的仿真功能相对弱一些尤其是对外设的模拟不够直观。还有一些纯软件模拟器比如Proteus它可以模拟整个电路包括外部的 LED、按键、示波器等等但 Proteus 的 STM32 模型库更新慢有些型号支持不好而且它和实际代码的调试体验是割裂的——你在 Keil 里写代码在 Proteus 里看现象中间隔了一层。我最终选 Keil 的方案核心逻辑是学习阶段代码和调试要在一个环境里完成。你在 Keil 里写代码直接在 Keil 里启动仿真看寄存器、看变量、看外设状态整个流程是连贯的。等你把外设都跑通了再买板子把同样的代码烧进去你会发现除了个别时序相关的细节大部分逻辑都是通的。2.2 仿真器到底模拟了什么很多人对仿真有误解以为仿真就是“假装在跑”。其实 Keil 的仿真器做的事情很具体它模拟了 STM32 内核的指令执行模拟了内存和寄存器的读写模拟了外设的行为。比如你配置了 GPIO 的输出寄存器仿真器会把对应的引脚状态更新你可以在仿真界面里看到这个引脚的电平变化。你配置了定时器仿真器会按照你设置的预分频和重装载值模拟计数器的递增和溢出。但它也有局限。仿真器模拟的是理想情况下的外设行为它不会模拟真实的电气特性比如引脚上的毛刺、电源的波动、外部器件的响应延迟。所以有些和硬件强相关的场景比如精确的 PWM 死区控制、高速 ADC 采样、外部中断的抖动仿真结果只能作为参考不能完全当真。但对于入门来说这些局限不影响你理解外设的工作原理。注意仿真器对外设的模拟程度取决于你选的芯片型号。有些新型号的 STM32仿真器可能只支持部分外设。选型的时候尽量选那些“经典款”比如 STM32F103 系列仿真支持最成熟。2.3 芯片型号的选择逻辑选哪个型号来仿真这里面有讲究。我的建议是选 STM32F103C8T6 或者 STM32F103RB。原因有三第一这两个型号是 STM32 入门最经典的型号资料最多仿真支持最完善第二它们的寄存器手册和参考手册非常详细你遇到问题容易查到答案第三它们的价格便宜将来你买真板子成本也低。如果你选了一个很新的型号比如 STM32H7 或者 STM32G4仿真器可能不支持某些外设或者模拟的行为和实际有偏差反而增加学习成本。入门阶段不要追求“最新最强”要追求“最稳最通”。3. 纯软件环境的搭建与工程配置3.1 Keil MDK 的安装与器件包配置Keil MDK 的安装本身不复杂去官网下载安装包一路下一步就行。但有一个坑安装路径不要有中文和空格。我见过有人装在“D:\学习资料\Keil”下面结果编译的时候各种报错查了半天才发现是路径问题。所以安装路径就用默认的或者手动改成“D:\Keil_v5”这种纯英文无空格的路径。安装完之后需要安装器件包。Keil 的器件包是通过 Pack Installer 管理的。打开 Pack Installer找到 STM32F1 系列安装对应的 Device Family Pack。这里要注意不要只装一个型号的包要把整个系列的包都装上。因为有些仿真器的配置依赖系列包里的公共文件只装单个型号的包可能会缺文件。器件包装好之后新建工程的时候就能选到 STM32F103C8 这个型号了。选完之后Keil 会问你要不要自动添加启动文件选“是”。启动文件是汇编写的负责在 main 函数之前初始化堆栈、设置中断向量表这些你不需要改但要知道它的存在。3.2 仿真器的关键配置项工程建好之后点击“Options for Target”进入调试配置页面。这里有几个关键设置第一Debug 选项卡里要选“Use Simulator”而不是“Use ST-Link Debugger”之类的硬件调试器。选了 Simulator 之后下面的参数一般不用改但有一个地方要注意Dialog DLL 和 Parameter 这两项默认是“DARMSTM.DLL”和“-pSTM32F103C8”如果你选的型号不同这里的参数要对应改。改错了会导致仿真启动失败。第二在 Utilities 选项卡里要取消“Use Debug Driver”的勾选。这个选项是给硬件调试用的仿真的时候不需要。如果不取消有时候会提示找不到调试器。第三在 Target 选项卡里要确认晶振频率设置正确。默认是 8MHz如果你用的是外部晶振要改成实际频率。这个频率影响定时器的计算设错了定时器的时间就不对。配置好之后点击“Start/Stop Debug Session”按钮就能进入仿真界面了。如果一切正常你会看到代码窗口旁边出现寄存器的状态底部出现命令窗口说明仿真器已经跑起来了。3.3 仿真环境下的时钟配置时钟是 STM32 的“心跳”仿真环境下时钟的配置和真板子基本一致但有一个区别仿真器不依赖外部晶振。真板子上你如果用的是外部晶振需要配置 RCC 寄存器把时钟源切换到 HSE仿真环境下你可以直接用 HSI内部高速时钟也可以模拟 HSE但不需要外部电路。我建议在仿真阶段先用 HSI 把外设跑通等代码逻辑验证完了再改成 HSE 配置。因为 HSI 的配置更简单不需要等待晶振起振仿真启动更快。等你把代码烧到真板子上的时候再把时钟源改成 HSE这时候你只需要改 RCC 的配置其他外设的代码不用动。时钟配置的核心是SystemInit 函数这个函数在启动文件里被调用负责设置时钟树。Keil 的器件包里自带了一个 SystemInit 的模板你可以直接用也可以自己写。我建议自己写一遍因为写的过程能让你理解 PLL 的倍频、AHB 和 APB 的分频这些概念。4. 外设逐个跑通的实操过程4.1 GPIO 输出从寄存器到库函数的两种写法GPIO 是入门的第一站。在仿真环境下跑 GPIO你不需要接 LED只需要看寄存器的状态变化。我建议先用直接操作寄存器的方式写一遍再用标准库函数写一遍对比两者的区别。直接操作寄存器的写法是这样的先使能 GPIO 的时钟通过 RCC_APB2ENR 寄存器然后配置 GPIO 的模式寄存器比如 CRL 或 CRH设置成推挽输出、50MHz最后操作 ODR 寄存器把对应的位拉高或拉低。在仿真界面里你可以打开“Peripherals”菜单找到“General Purpose I/O”然后选中你配置的那个 GPIO 端口就能看到每个引脚的状态。你写 ODR 的时候对应的引脚会变成高电平仿真界面里会显示成红色或者绿色。标准库函数的写法更简单调用 RCC_APB2PeriphClockCmd 使能时钟调用 GPIO_Init 配置模式调用 GPIO_SetBits 或 GPIO_ResetBits 控制电平。库函数的好处是可读性好坏处是你不知道底层到底操作了哪些寄存器。所以我建议两种都写一遍写完之后对比一下你就明白库函数到底帮你做了什么。实操心得在仿真环境下GPIO 的翻转速度可以设置断点来观察。你可以在 GPIO_SetBits 后面加一个断点然后单步执行看 ODR 寄存器的变化。这样你能直观地看到“写寄存器”这个动作到底发生了什么。4.2 定时器仿真环境下的时间基准验证定时器是 STM32 最核心的外设之一也是仿真环境下最容易验证的外设。因为定时器的行为是纯数字的仿真器可以精确模拟计数器的递增和溢出。配置定时器的步骤使能定时器时钟设置预分频器PSC和自动重装载寄存器ARR配置计数模式使能定时器。在仿真界面里打开“Peripherals”菜单找到“TIM”选中你配置的定时器就能看到计数器的当前值、预分频器的值、以及各种状态标志位。这里有一个关键点仿真环境下的时间基准和真实时间是有偏差的。仿真器模拟的是指令周期但你的电脑 CPU 执行仿真指令的速度和真实 MCU 执行指令的速度不一样。所以你不能用仿真环境来测量“定时器实际跑了多少毫秒”但你可以验证“定时器的计数逻辑是否正确”。比如你设置 PSC7199ARR9999在 72MHz 的时钟下定时器应该是 1 秒溢出一次。在仿真环境里你可以观察计数器的递增过程确认它确实是从 0 数到 9999然后溢出触发更新事件。我建议在定时器中断里翻转一个 GPIO然后在仿真界面里观察这个 GPIO 的状态变化。这样你能直观地看到“定时器溢出 - 触发中断 - 执行中断服务函数 - 翻转 GPIO”这个完整的链路。4.3 串口仿真环境下的数据收发模拟串口是仿真环境下比较特殊的外设。真板子上串口需要外部电路配合比如 USB 转串口芯片、电平转换电路。仿真环境下Keil 的仿真器提供了一个虚拟串口你可以把串口的数据输出到 Keil 的命令窗口也可以从命令窗口输入数据给串口。配置串口的步骤使能串口时钟和 GPIO 时钟配置 GPIO 的复用功能配置串口的波特率、数据位、停止位、校验位使能串口。在仿真界面里打开“Peripherals”菜单找到“USART”选中你配置的串口就能看到发送和接收的数据寄存器。这里有一个技巧在 Keil 的命令窗口里可以用“UART #1”来模拟串口的输入输出。你可以在命令窗口里输入字符仿真器会把这些字符送到串口的接收寄存器你往串口的发送寄存器写数据仿真器会把这些数据显示在命令窗口里。这样你就能在纯软件环境下测试串口的收发逻辑不需要任何外部硬件。注意仿真环境下的串口波特率是“逻辑波特率”不是真实的物理波特率。你设置 115200仿真器不会真的以 115200 的速率传输数据它只是按照这个配置来模拟串口的行为。所以你不能用仿真环境来测试串口的实际传输速率但可以测试数据格式和收发逻辑。4.4 ADC仿真环境下的模拟量输入ADC 是仿真环境下比较难模拟的外设因为真实的 ADC 需要外部模拟电压输入。Keil 的仿真器提供了一个虚拟 ADC 输入你可以在仿真界面里手动设置 ADC 输入通道的电压值然后观察 ADC 数据寄存器的变化。配置 ADC 的步骤使能 ADC 时钟和 GPIO 时钟配置 GPIO 为模拟输入模式配置 ADC 的采样时间、通道、转换模式使能 ADC启动转换。在仿真界面里打开“Peripherals”菜单找到“ADC”选中你配置的 ADC就能看到转换结果和状态标志。这里的关键是在仿真界面里你可以手动修改 ADC 输入通道的电压值。比如你设置通道 0 的电压为 1.65V在 12 位分辨率、3.3V 参考电压下ADC 的转换结果应该是 2048 左右。你可以通过修改电压值观察 ADC 数据寄存器的变化验证你的转换公式是否正确。我建议在 ADC 转换完成的中断里把转换结果通过串口输出到命令窗口。这样你能看到“电压变化 - ADC 转换 - 数据输出”这个完整的链路。4.5 PWM 输出仿真环境下的波形观察PWM 是定时器的一个高级功能仿真环境下可以观察 PWM 的占空比和频率。配置 PWM 的步骤使能定时器时钟和 GPIO 时钟配置 GPIO 为复用推挽输出配置定时器的 PSC 和 ARR配置 PWM 模式使能定时器和 PWM 输出。在仿真界面里打开“Peripherals”菜单找到“TIM”选中你配置的定时器就能看到 PWM 的占空比和频率。你还可以在“Analysis”菜单里打开“Logic Analyzer”把 PWM 输出的引脚加进去就能看到波形的变化。Logic Analyzer 是仿真环境下非常实用的工具它可以同时显示多个引脚的电平变化帮你分析时序关系。实操心得Logic Analyzer 的采样率是有限的如果你设置的 PWM 频率太高波形可能会失真。我建议在仿真阶段把 PWM 频率设置在 1kHz 到 10kHz 之间这样 Logic Analyzer 能清晰地显示波形。5. 常见问题与排查技巧实录5.1 仿真启动失败找不到器件或调试器这是最常见的问题表现是点击“Start/Stop Debug Session”之后弹出一个错误框说找不到器件或者调试器。原因通常是器件包没装全或者 Debug 选项卡里的 Dialog DLL 参数不对。排查步骤第一打开 Pack Installer确认你选的型号对应的 Device Family Pack 已经安装并且是最新版本。第二打开 Options for Target - Debug确认“Use Simulator”被选中Dialog DLL 是“DARMSTM.DLL”Parameter 是“-p”加上你的型号。第三如果还是不行试着新建一个工程重新选型号重新配置。我遇到过一种情况器件包装了但 Keil 的版本太老不支持这个器件包。解决办法是升级 Keil 到最新版本或者换一个老一点的器件包。5.2 仿真运行异常程序跑飞或卡死仿真运行的时候程序跑飞或者卡死通常是因为访问了未初始化的指针或者中断向量表配置错误。在仿真环境下这些问题比真板子上更容易排查因为你可以随时暂停查看寄存器和内存。排查步骤第一在 main 函数的开头加一个断点确认程序能跑到 main。第二如果跑不到 main检查启动文件里的堆栈大小是否够用中断向量表是否对齐。第三如果跑到了 main 但卡死单步执行看卡在哪个函数里。第四打开“Peripherals”菜单查看外设的状态确认时钟是否使能寄存器配置是否正确。我踩过的一个坑在仿真环境下未使能外设时钟就操作外设寄存器会导致程序卡死。真板子上这种情况可能只是外设不工作但仿真器会直接报错。所以每次操作外设之前一定要确认时钟已经使能。5.3 外设不工作寄存器配置检查清单外设不工作最常见的原因是寄存器配置漏了或者配错了。我整理了一个检查清单每次外设不工作的时候按这个清单过一遍检查项常见问题解决方法时钟使能忘记使能外设时钟检查 RCC 寄存器确认对应外设的时钟位被置 1GPIO 配置模式配错比如该用复用功能却配成了普通输出检查 GPIO 的 CRL/CRH 寄存器确认模式位正确中断配置忘记使能中断或者优先级配错检查 NVIC 寄存器确认中断使能位和优先级寄存器顺序先写数据寄存器再使能外设按照参考手册的顺序配置寄存器仿真器限制某些外设在仿真环境下不支持查仿真器文档确认该外设是否被支持提示仿真环境下如果外设不工作先检查时钟和 GPIO 配置这两个是最容易出问题的地方。如果这两个都没问题再检查外设本身的配置寄存器。5.4 仿真与真板子的差异哪些能信哪些不能信仿真跑通了不代表真板子一定能跑通。有些差异是必须知道的第一时序相关的代码仿真结果只能参考。比如你写了一个精确的延时函数仿真环境下可能跑得很快真板子上可能因为晶振精度、指令流水线等原因实际延时和预期有偏差。第二中断响应时间仿真和真板子不一样。仿真器模拟的是理想的中断响应真板子上中断响应受优先级、当前指令、流水线状态的影响。第三外部器件的交互仿真无法模拟。比如你接了一个 I2C 的传感器仿真环境下你只能模拟 I2C 的时序但传感器的实际响应、ACK 信号、数据格式仿真器无法完全模拟。所以我的建议是仿真阶段重点验证代码逻辑和寄存器配置真板子阶段重点验证时序和外部器件交互。两者结合才能把 STM32 学透。6. 从仿真到真板子的过渡建议仿真跑通之后下一步就是买板子、烧代码。这个过渡阶段有几个地方需要注意。第一时钟配置要改。仿真阶段我建议用 HSI真板子上如果你用的是外部晶振要把时钟源改成 HSE并且修改 PLL 的配置。改完之后所有依赖时钟的外设比如定时器、串口、ADC都要重新计算分频参数。第二GPIO 的复用功能要确认。仿真环境下GPIO 的复用功能是“逻辑上”的真板子上你需要确认引脚的实际复用功能是否和你的配置一致。比如有些引脚默认是 JTAG 功能你要用普通 GPIO 的话需要先禁用 JTAG。第三下载和调试方式要改。仿真环境下你用的是 Simulator真板子上你要用 ST-Link 或者 J-Link 之类的硬件调试器。在 Options for Target - Debug 里把“Use Simulator”改成“Use ST-Link Debugger”然后配置下载算法和调试参数。第四供电和跳线要检查。真板子上供电方式、跳线帽的位置、BOOT 引脚的设置都会影响程序的运行。我见过有人代码没问题但 BOOT 引脚设成了从 RAM 启动结果程序跑不起来查了半天才发现是跳线的问题。实操心得从仿真过渡到真板子的时候先烧一个最简单的点灯程序确认板子能正常工作。然后再把仿真阶段跑通的外设代码一个一个烧进去每烧一个验证一个。不要一次性把所有代码都烧进去出了问题不好定位。7. 我个人在实际操作中的体会这套纯软件的学习路径我带着几个人走过效果比我想象的好。最大的好处是启动成本低不需要等板子、不需要买配件、不需要担心硬件损坏。你可以在任何一台电脑上装好 Keil建好工程就开始跑外设。对于还在犹豫要不要学 STM32 的人来说这是一个很好的“试水”方式。但仿真终究是仿真它不能替代真板子。我的建议是用仿真入门用真板子进阶。仿真阶段你把外设的原理、寄存器的配置、代码的逻辑都搞明白真板子阶段你去感受真实的时序、真实的电气特性、真实的外部器件交互。两者结合你才能真正把 STM32 学透。最后分享一个小技巧在仿真环境下你可以把常用的外设配置写成一个个独立的函数比如 GPIO_Config、TIM_Config、USART_Config每个函数只负责一个外设的配置。这样你在真板子上移植的时候只需要改时钟配置和 GPIO 复用配置其他代码基本不用动。这个习惯我从仿真阶段就养成了后来做项目的时候代码的复用率非常高。