1. 为什么我要把 STM32 入门做成纯软件版本
刚接触 STM32 的人,十有八九会被同一个问题卡住:到底要不要先买一块开发板。淘宝上一搜,从十几块的核心板到几百块的综合实验板,价格跨度大得离谱,评论区还经常有人说“买回来吃灰”。我当年也是这么过来的,板子买了两三块,焊台、下载器、杜邦线配了一堆,结果真正跑通的第一个工程还是靠仿真软件完成的。后来带新人,我发现一个很现实的问题:很多人连 GPIO 输出高低电平都没搞明白,就先被硬件连接、供电跳线、下载器驱动折腾得没了脾气。
所以这次我干脆把 STM32 入门做成了一个“纯软件”版本。核心思路很简单:不买板子,不接任何外部电路,只用一台电脑,把 STM32 的常用外设一个一个在仿真环境里跑通。你可能会问,仿真能跑外设吗?能跑,而且跑得比你想的完整。GPIO 电平翻转、定时器中断、串口收发、ADC 采样、PWM 输出、I2C 读写,这些在仿真环境里都有对应的模型,代码写对了就能看到结果,写错了也能立刻定位问题。
这个方案适合谁?第一类是完全零基础、还在犹豫要不要投入硬件成本的人;第二类是有板子但外设一直调不通、想先排除软件问题的人;第三类是带学生的老师或者带新人的老手,想找一个统一的环境让大家先跑通逻辑,再上真实硬件。说白了,仿真不是替代硬件,而是让你在花钱之前先把“代码怎么写、寄存器怎么配、中断怎么进”这些事搞明白。
我实测下来,用纯软件方式把 STM32 的 GPIO、定时器、串口、ADC、PWM 这几个核心外设跑一遍,大概需要两到三个晚上,每天两小时左右。这个时间投入比等快递、焊排针、装驱动要划算得多。下面我就把这套流程拆开讲,包括环境怎么搭、每个外设怎么配、仿真里能看到什么现象、以及我踩过的那些坑。
2. 仿真环境选型与搭建思路
2.1 为什么选这套组合而不是别的
市面上能跑 STM32 仿真的方案不止一种,我试过几种之后,最终固定在一套组合上:Keil MDK + 对应芯片的仿真模型。原因有三个。第一,Keil 的调试界面里自带外设寄存器视图,你写进去的值和寄存器实际值能对上,这对理解底层很有帮助。第二,它的仿真模型覆盖了 STM32 常用外设,GPIO、定时器、串口、ADC 这些都有,不需要额外装插件。第三,网上关于 Keil 的教程最多,遇到问题搜一下基本都能找到答案。
有人会问,为什么不直接用 Proteus 加 Keil 联合仿真?Proteus 确实能画电路图、接虚拟示波器,看起来更“真实”。但它的坑在于:元件库版本和 Keil 工程配置经常对不上,新手光解决“为什么仿真里没反应”就要花掉大量时间。而且 Proteus 的 STM32 模型对中断和时钟树的模拟并不总是准确,有时候代码没问题但仿真就是不动。所以我建议第一阶段先用 Keil 自带的仿真把代码逻辑跑通,等逻辑没问题了,再考虑要不要上 Proteus 看波形。
还有一个选择是纯软件模拟器,比如某些开源项目。这类工具轻量,但外设支持参差不齐,文档也少,出了问题很难排查。对于入门来说,稳定比轻量重要,所以我最终选了 Keil 这条路线。
2.2 安装与工程创建的关键步骤
安装 Keil MDK 本身没什么好说的,一路下一步就行。但有几个地方要注意。第一,安装路径不要有中文和空格,否则后面编译和仿真都可能出问题。第二,安装完成后要装对应芯片系列的 Device Family Pack,比如你用的是 STM32F1 系列,就装 F1 的包。这个包在 Keil 的 Pack Installer 里可以直接下载,不用去别的地方找。
创建工程的时候,芯片型号选一个最基础的,比如 STM32F103C8 或者 STM32F103RB。为什么选 F1 系列?因为它的仿真模型最成熟,资料也最多。选太新的型号,仿真支持可能不完整,反而给自己找麻烦。
工程创建好之后,有一个关键设置必须改:在 Options for Target 的 Debug 选项卡里,把调试器从默认的硬件调试器改成Use Simulator。这一步不做,你点调试就会提示找不到硬件。改完之后,再在同一个选项卡里点 Settings,确认仿真器能识别到芯片。如果这里显示正常,说明仿真环境已经就绪。
注意:每次新建工程都要检查 Debug 选项卡是不是 Simulator,Keil 不会记住你上一次的选择,默认永远是硬件调试器。我因为这个设置漏改,白白折腾了半小时。
2.3 仿真里能看到什么、不能看到什么
仿真环境能看到的:寄存器值的变化、变量值的变化、GPIO 引脚电平(在逻辑分析仪窗口里)、串口输出的字符、定时器计数器的增减、中断触发记录。这些足够你验证代码逻辑对不对。
仿真环境看不到的:真实电路的电气特性,比如上拉电阻够不够、信号有没有毛刺、电源纹波大不大。这些必须上真实硬件才能测。所以我的建议是:仿真阶段只关心“代码逻辑对不对”,不要纠结“信号质量好不好”。等逻辑跑通了,再上板子调硬件,效率会高很多。
还有一个限制:仿真里的时钟频率是模拟的,不是真实的。你设 72MHz,仿真里跑起来的时间感受和真实芯片不一样。所以做延时的时候,仿真里看到的延时长度只能作为参考,不能当作精确值。这一点在调串口波特率的时候尤其要注意,仿真里能收到正确字符,不代表真实硬件上波特率就准。
3. GPIO 与定时器:从点灯到精确延时
3.1 GPIO 输出:仿真里的“点灯”
点灯是嵌入式的 Hello World,但在仿真里没有真实的 LED,怎么看结果?答案是看寄存器。以 STM32F103 的 PA5 引脚为例,你要做的是使能 GPIOA 时钟、配置 PA5 为推挽输出、然后循环翻转 PA5 的输出电平。
代码写完之后进入调试模式,打开 Peripherals 菜单里的 GPIO 窗口,找到 GPIOA。你会看到 ODR 寄存器的第 5 位在 0 和 1 之间来回跳。这个跳变就相当于真实硬件上 LED 的亮灭。如果你在代码里加了延时,还能看到跳变的间隔和你的延时设置对应。
这里有一个新手常犯的错误:忘了使能 GPIO 时钟。在 STM32 里,任何外设在使用前都必须先开时钟,GPIO 也不例外。代码里就是一句RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);。忘了这句,寄存器窗口里 GPIOA 的配置全是 0,你怎么改都没反应。我当年第一次写的时候就是漏了这句,对着屏幕找了半天,最后发现是时钟没开。
实操心得:在仿真里调 GPIO 的时候,先把 ODR 窗口打开,然后单步执行。每执行一步就看一眼 ODR 的值,这样能非常直观地理解“写寄存器”和“引脚电平”之间的关系。比看教程上的文字描述管用十倍。
3.2 定时器中断:仿真里的“心跳”
GPIO 翻转用延时循环实现,简单但占 CPU。真正做项目的时候,定时任务都是用定时器中断。在仿真里调定时器,最大的好处是你能清楚地看到中断服务函数到底有没有进去、进去几次、间隔对不对。
以 TIM2 为例,配置步骤是:使能 TIM2 时钟、设置预分频器和自动重装载值、开启更新中断、配置 NVIC、使能定时器。预分频器和重装载值的计算是重点。假设系统时钟 72MHz,你想要 1ms 中断一次,计算过程是:72MHz 除以预分频器加一,再除以重装载值加一,结果等于 1000Hz。比如预分频器设 7199,那么 72MHz / 7200 = 10kHz,再除以 10 得到 1kHz,所以重装载值设 9。
在仿真里跑起来之后,你可以在中断服务函数里翻转一个 GPIO,然后用逻辑分析仪窗口看这个 GPIO 的波形。如果波形周期是 2ms(因为中断一次翻转一次,所以周期是中断间隔的两倍),说明配置正确。如果波形周期不对,就回去检查预分频器和重装载值的计算。
这里有个坑:仿真里的定时器计数速度和你电脑的性能有关,电脑卡的时候波形会抖。所以看波形只看周期对不对,不要纠结边沿是否绝对整齐。另外,NVIC 的优先级配置在仿真里也能看到效果,你可以设两个不同优先级的定时器中断,观察哪个先执行。
3.3 仿真与真实的差异:延时和时钟
前面提到仿真里的时钟是模拟的,这里展开说一下。在真实芯片上,你写delay_ms(1000),用示波器测就是精确的 1 秒(前提是延时函数写对了)。但在仿真里,这个 1 秒可能跑成 1.5 秒甚至 2 秒,取决于你电脑的 CPU 性能和 Keil 的仿真精度。
所以我在仿真阶段调延时的时候,从来不纠结绝对时间,只看相对关系。比如我要确认“先点灯再延时再灭灯”这个顺序对不对,仿真里完全能看出来。但我要确认“延时是不是精确的 500ms”,仿真给不了答案,必须上真实硬件用示波器测。
这个差异还影响串口通信。仿真里调串口的时候,波特率设 9600,你能看到字符正确收发,但这不代表真实硬件上 9600 的波特率就准。真实硬件上波特率不准,收到的就是乱码。所以串口调试的最终验证一定要在真实硬件上做,仿真只能验证“发送逻辑”和“接收逻辑”对不对。
4. 串口、ADC 与 PWM:仿真里的进阶外设
4.1 串口收发:仿真里的“打印调试”
串口是嵌入式调试最常用的工具,在仿真里也能用。配置步骤是:使能 USART 和对应 GPIO 的时钟、配置 TX 为复用推挽输出、RX 为浮空输入或上拉输入、设置波特率、数据位、停止位、使能 USART。
仿真里怎么看串口输出?Keil 的仿真环境有一个 UART 窗口,在 View 菜单里可以打开。你调用发送函数之后,UART 窗口里会显示你发出来的字符。这个功能非常实用,相当于给你一个不用接硬件的“串口助手”。
接收怎么测?在 UART 窗口里有一个输入框,你可以手动输入字符,然后触发接收中断。仿真里能看到中断服务函数被调用,接收到的字符存在你指定的变量里。这样你就能完整地测试“发送”和“接收”两条路径。
注意事项:仿真里的串口没有真实的电平转换和波特率误差,所以不要用仿真来验证“波特率设 115200 能不能稳定通信”。仿真只能验证你的发送逻辑和接收逻辑写对了没有。真实硬件上,波特率误差、线缆质量、干扰都会影响通信,这些必须实测。
我踩过的一个坑是:忘了配置 GPIO 的复用功能。STM32 的 USART 引脚默认不是串口功能,必须把 GPIO 配置成复用推挽输出(TX)和浮空输入(RX)。忘了这一步,UART 窗口里什么都不会显示。这个错误在真实硬件上也是一样的,所以仿真里养成的检查习惯,上板子之后直接能用。
4.2 ADC 采样:仿真里的“虚拟电压”
ADC 在仿真里也能跑。配置步骤是:使能 ADC 和 GPIO 时钟、配置 ADC 引脚为模拟输入、设置 ADC 的通道、采样时间、转换模式、使能 ADC、校准、启动转换。
仿真里的 ADC 输入值是可以手动设置的。在 Keil 的仿真环境里,打开 ADC 的外设窗口,你可以直接修改输入电压的值。比如你设 1.65V,参考电压 3.3V,12 位 ADC 的转换结果应该是 2048 左右。你可以在代码里读转换结果,然后通过串口打印出来,或者存在变量里在 Watch 窗口看。
这个功能对理解 ADC 的工作原理非常有帮助。你可以手动改输入电压,观察转换结果的变化,验证“输入电压和转换结果是不是线性关系”。真实硬件上你要用可调电阻或者信号发生器才能做这个实验,仿真里动动鼠标就行了。
这里要注意:仿真里的 ADC 没有噪声,转换结果非常干净。真实硬件上 ADC 会有噪声,低位会跳。所以仿真里调通了 ADC 读取,不代表真实硬件上读出来的值就稳定。真实硬件上通常要做多次采样取平均,或者加滤波电路。这些在仿真阶段可以先不做,等上板子之后再补。
4.3 PWM 输出:仿真里的“呼吸灯”
PWM 是定时器的一个高级功能,在仿真里也能看到效果。配置步骤是:使能定时器和 GPIO 时钟、配置 GPIO 为复用推挽输出、设置定时器的预分频器和重装载值(决定 PWM 频率)、设置比较值(决定占空比)、配置 PWM 模式、使能定时器和 PWM 输出。
仿真里怎么看 PWM?打开逻辑分析仪窗口,把对应的 GPIO 引脚加进去,就能看到方波。改比较值,方波的占空比会跟着变。如果你在代码里让比较值循环增减,就能看到占空比来回变化的“呼吸灯”效果。
PWM 频率的计算和定时器中断的计算类似。假设系统时钟 72MHz,预分频器设 71,重装载值设 999,那么 PWM 频率是 72MHz / 72 / 1000 = 1kHz。比较值设 500,占空比就是 50%。这些计算在仿真里可以立刻验证,改一个参数就能看到波形变化,比在真实硬件上改代码、重新编译、下载、用示波器测要快得多。
实操心得:调 PWM 的时候,先把频率算对,再调占空比。频率不对,后面占空比怎么调都不对。我见过有人把预分频器和重装载值搞反了,结果频率差了上千倍,波形根本看不出来。仿真里逻辑分析仪的时间轴可以缩放,放大之后能看清周期,这个功能要善用。
5. 常见问题与排查技巧实录
5.1 仿真里代码跑不起来怎么办
这是最常见的问题。代码编译通过了,但一进调试就卡在启动文件里,或者直接跑飞。排查顺序是这样的:
第一,检查 Debug 选项卡是不是选了 Use Simulator。这个前面说过,但还是要再强调一遍,因为太容易漏。
第二,检查芯片型号和工程里选的型号是不是一致。不一致的话,仿真模型对不上,代码可能跑飞。
第三,检查启动文件里的堆栈大小。仿真环境对堆栈的模拟和真实硬件有差异,堆栈设太小可能在仿真里就溢出了。把堆栈改大一点试试。
第四,检查有没有用到仿真不支持的指令。比如某些芯片特有的协处理器指令,仿真模型可能不支持。这种情况只能换芯片型号或者换仿真方案。
5.2 外设寄存器写了没反应
代码里明明写了配置寄存器的语句,但外设窗口里看寄存器还是 0。原因通常是时钟没开。STM32 的外设时钟默认是关闭的,用之前必须使能。GPIO 在 APB2 上,定时器在 APB1 或 APB2 上,串口在 APB2 上,ADC 在 APB2 上。每个外设的时钟使能函数不一样,写代码的时候要查参考手册确认。
还有一个可能是寄存器写保护。有些寄存器需要先解锁才能写,比如某些配置寄存器。这个在 F1 系列里不常见,但在其他系列里要注意。
5.3 中断进不去或者进去就出不来
中断进不去,先检查 NVIC 配置。NVIC 的优先级分组、使能位、优先级设置都要对。仿真里可以在 NVIC 窗口看到中断的挂起状态和使能状态,对照着查。
中断进去就出不来,通常是忘了清中断标志位。在中断服务函数里,处理完业务逻辑之后必须清除对应的中断标志,否则中断会一直触发,程序就卡在中断里了。仿真里能看到中断触发次数疯狂增加,这就是标志位没清的典型现象。
5.4 串口仿真收不到字符
先检查 GPIO 配置。TX 要配成复用推挽输出,RX 要配成浮空输入或上拉输入。配成普通推挽输出的话,串口功能出不来。
再检查波特率。仿真里的波特率和真实硬件不一样,但如果你设了一个特别离谱的值,比如 1Mbps,仿真可能也收不到。先用 9600 试,确认逻辑通了再改。
最后检查 UART 窗口有没有打开。这个窗口默认不显示,要在 View 菜单里手动打开。我见过有人代码没问题,就是没开窗口,以为仿真坏了。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 调试卡在启动文件 | Debug 没选 Simulator | 检查 Options for Target 的 Debug 选项卡 |
| 寄存器写了没反应 | 外设时钟没使能 | 检查 RCC 使能语句 |
| 中断进不去 | NVIC 没配置 | 检查 NVIC 窗口的使能位 |
| 中断出不来 | 中断标志没清 | 检查中断服务函数里的清标志语句 |
| 串口收不到 | GPIO 模式配错 | 检查 TX 和 RX 的 GPIO 配置 |
| PWM 没波形 | 定时器没使能 | 检查定时器使能语句和 PWM 输出使能 |
| ADC 读数不变 | 转换没启动 | 检查 ADC 启动转换的语句 |
6. 从仿真到真实硬件的过渡建议
仿真跑通之后,下一步就是上真实硬件。这个过渡不是简单的“把代码下载进去就行”,有几个地方需要调整。
第一,时钟配置。仿真里的时钟是模拟的,真实硬件上要配置外部晶振和 PLL。如果你在仿真里用的是默认的内部时钟,上板子之后要改成外部晶振,否则串口波特率会不准。
第二,延时函数。仿真里的延时和真实硬件不一样,上板子之后要用 SysTick 或者定时器重新校准延时。我通常的做法是:在仿真里验证逻辑,上板子之后用示波器测延时,然后调整延时函数的参数。
第三,GPIO 驱动能力。仿真里 GPIO 输出高低电平没有电流限制,真实硬件上 GPIO 的驱动能力有限,直接驱动 LED 要加限流电阻,驱动继电器要加三极管。这些在仿真阶段不用管,但上板子之前要补上。
第四,电源和复位电路。仿真里没有电源和复位,真实硬件上电源不稳或者复位电路设计不当,程序可能跑不起来。这个属于硬件设计范畴,仿真帮不上忙,但至少仿真能帮你排除软件问题,让你在调硬件的时候心里有底。
我个人的习惯是:仿真里跑通一个外设,就上板子验证一个外设。不要等所有外设都在仿真里跑通了再上板子,那样一旦出问题,排查范围太大。一个一个来,仿真和真实硬件交替验证,效率最高。
最后分享一个小技巧:在仿真里调代码的时候,把关键变量加到 Watch 窗口,把关键寄存器加到 Peripherals 窗口,把关键引脚加到 Logic Analyzer 窗口。这三个窗口同时开着,代码执行到哪一步、变量变成什么值、引脚电平怎么变,一目了然。这个习惯我从仿真阶段一直保持到真实硬件调试,只是真实硬件上把 Logic Analyzer 换成了示波器。