朋友上个月问我学 STM32 是不是非得先花一百多块买开发板再配个下载器还得折腾驱动、接线才能开始。我反手就把最近整理好的一套东西发给了他——STM32 仿真的纯软件版本。不花钱买板子不碰任何硬件也能把 GPIO、定时器、UART、I2C 这些外设一个个跑通。他花了半天装好工具当天晚上就把 LED 点起来了。这篇文章就是把这套“纯软件入门 STM32”的完整做法写出来。适合预算有限的学生、手边暂时没板子的朋友以及只想先验证一下自己是否适合学嵌入式、不想一上来就花钱的人。我会按实际操作的顺序讲方案怎么选、环境怎么搭、外设怎么逐个跑通、仿真里会踩到哪些坑。全部都是我实测下来比较稳的组合。1. 为什么“纯软件学 STM32”这条路走得通1.1 仿真学 STM32 和真板学 STM32到底差在哪先说结论差在“电气层面”不差在“逻辑层面”。仿真平台里的 STM32 模型并不是简单画了个芯片外形给你看。GPIO 寄存器读写、时钟树配置、定时器分频、UART 波特率计算、中断向量表跳转这些行为都按真芯片的逻辑建模。你在 Keil 里写的代码在仿真器里执行的路径和真芯片几乎一致同样是往寄存器地址写值同样是响应中断同样是按照时钟周期推进状态。区别在于仿真平台把“物理世界”的部分简化掉了。比如不关心引脚实际输出的电压波形、不考虑电源噪声、不涉及上电时序。对于入门阶段来说这些恰恰是不需要先学的部分。先用仿真把代码控制硬件的方法论建立起来之后再碰真板只补电气和仪器层面的经验就行。一个比较贴切的类比是驾校的模拟驾驶舱。方向盘、油门刹车、换挡逻辑都是真的只是在屋里跑。你在模拟舱里练熟了起步停车真上车还要适应路面和车感但操作规则你已经会了。STM32 仿真的价值就在这里。1.2 三条纯软件路线横向对比完全不需要硬件的 STM32 学习路线目前主流有三条路线可视化程度外设覆盖上手难度适合场景Proteus Keil MDK高电路和元件直观可见丰富LED、数码管、LCD、虚拟终端中等入门首选、系统性学外设STM32CubeIDE 自带软件模拟器低主要看寄存器窗口有限较低纯逻辑练习、调试代码在线网页仿真平台中鼠标拖拽元件取决于平台低快速体验、临时验证三条我都试过。CubeIDE 的模拟器适合跑纯逻辑比如算个算法、测个状态机但想“看到”LED 闪烁、数码管跳动它做不到。在线平台胜在零安装但建工程、拖元件每次都要重新来外设模型也不一定全跑复杂实验容易卡。我最终推荐 Proteus Keil MDK 的组合。原因是它把“电路”也带回来了你不仅写代码还能看到芯片引脚、电阻、按键、虚拟终端之间怎么连这对建立“程序控制硬件”的直觉非常重要。就像学电路时先看面包板一个道理。1.3 这条路值不值得花时间走一遍很多反对意见说“仿真终究是仿真不真实”。这话对但不该成为新手不用的理由。我的看法是仿真覆盖了入门阶段至少七成知识点剩下三成是平台差异和仪器使用经验属于进阶内容。更关键的是仿真把起步成本降到了零。不少人真正劝退的不是 STM32 本身而是前置流程——比价、下单、等快递、装驱动、插线、担心烧板子。这些事每一样都在消耗学习热情。纯软件方案把这些全部抹掉了你打开电脑就能进入学习状态。等你在仿真里把外设一个个摸熟再决定要不要买板子决定会理性得多。2. 整套环境搭建软件、工程和最小电路2.1 软件版本选择和避坑先说结论Keil MDK 5.x 加 Proteus 8.x这是一套经过大量人验证的组合文档多、问题答案好搜。Keil 装好后记得装对应的器件包 Device Family Pack。装错了或者没装选芯片型号时根本找不到 STM32F103 系列。Proteus 8 版本比较多我用的是较新的 8.x对 STM32F103 系列的支持已经比较完善不用特意找旧版绿色版反而容易缺模型。避一个大坑不要折腾 Keil 与 Proteus 的联调。网上有教程教你在 Keil 里配置 Proteus VSM 仿真器实现边编译边看效果。这个方法在 51 单片机上比较成熟到 ARM 内核上经常卡在版本兼容、驱动缺失上一折腾就是半天新手基本劝退。正确做法是各干各的Keil 负责编译出 hex 文件Proteus 把芯片模型和最小电路搭好然后加载 hex 文件运行。流程简单、稳定、可复现。加载 hex 的路径是在 Proteus 里双击 STM32 芯片弹出属性框在 Program File 一栏选择你 Keil 生成的 hex 文件。就这么简单。2.2 新建 Keil 工程最容易栽的三个跟头建 Keil 工程本身不复杂但有三个细节没注意后面会浪费大量时间。第一步工程创建时一定要勾选 Output 选项卡里的 Create HEX File。这个选项默认不勾很多人在 Keil 里编译通过、以为万事大吉结果 Proteus 加载项目文件找不到目标文件回头查了一圈才发现根本没生成 hex。路径是 Options for Target - Output - Create HEX File编译前确认勾上。第二步器件型号要选对。STM32F103 系列按容量分为低密度、中密度、高密度对应不同的 Flash 大小、不同启动文件。Proteus 案例里最常用的是 STM32F103C6 或 STM32F103C8这属于中低容量。选错型号外设寄存器地址可能对不上或者生成工程时启动文件不匹配直接表现就是仿真时程序跑飞。第三步启动文件 startup 文件要和器件对应。新建工程时系统会让你选 Startup File不同容量对应不同文件。如果用错了程序可能从错误地址启动现象是点灯都不亮、单步调试进去全是汇编跳转看不懂。这三个坑都属于“前期花一分钟后期省一小时”的类型。我在带人入门时总结过80% 的“仿真没反应”问题都能归到这三类里。2.3 在 Proteus 里搭出能跑的最小系统Proteus 不同于实物开发板不会默认帮你把最小系统画好需要手动搭。步骤不复杂但漏一个就可能仿真失败。首先放置 STM32F103C6 芯片。在元件库搜索 STM32F103C6 或 STM32F103C8 就能找到。放好后接电源VDD 和 VDDA 引脚接 3.3VVSS 和 VSSA 引脚接 GND。VBAT 引脚也不要漏接 3.3V。Proteus 对引脚空置会有检查电源引脚接不全时芯片直接不工作或报仿真错误。然后是复位电路。NRST 引脚接一个 10kΩ 电阻到 VDD再对地接一个 100nF 电容。这个组合和真机上的复位电路是一模一样的逻辑。仿真里可以不加复位按键但电阻电容建议保留保持电路的“真实感”。接着是外部晶振。OSC_IN 和 OSC_OUT 之间接一个 8MHz 晶振两端各接 20pF 电容到地。仿真里有时简化掉电容也能跑但我还是建议按标准画法接因为这样换到真板时电路习惯一致。如果仿真中发现时钟相关外设定时器、UART表现诡异首先检查晶振有没有接、频率够不够。有同学问 BOOT 引脚要不要设置。仿真里不需要默认状态就是从主 Flash 启动不用管。SWD 调试接口也可以不接我们是通过加载 hex 文件来“烧录”的不需要调试器。最小系统搭好后电路骨架就是完整的。2.4 第一个工程点亮第一颗 LED 的完整流程硬件接线是这样PA0 引脚接 LED 阳极LED 阴极通过一个 220Ω 限流电阻接到 GND。这样用推挽输出、低电平点亮。之所以用低电平点亮是因为单片机引脚灌电流能力通常比拉电流能力更好LED 接法上有很多讲究但这里记住“PA0 输出低电平LED 亮”就够了。代码我用 HAL 库风格写因为现在工业项目主流就是 HAL 库入门用它能少走弯路。#include stm32f1xx_hal.h int main(void) { HAL_Init(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); while (1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); HAL_Delay(500); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); HAL_Delay(500); } }编译生成 hex在 Proteus 里加载点运行就能看到 LED 以 1 秒间隔闪烁。这里必须解释一下新手最容易困惑的一句话为什么点灯之前要“打开 GPIOA 的时钟”这相当于一个房间里的灯即使灯泡接好了如果总电闸没合上电流过不去灯就不会亮。单片机为了省电外设默认是断电的必须通过 RCC 寄存器给对应外设送电。__HAL_RCC_GPIOA_CLK_ENABLE()就是合上 GPIOA 这个房间的电闸。不理解这一步后面学 UART、TIM 的时候会一直懵。3. 外设逐个跑通的实操记录3.1 GPIO 输入按键控制 LED仿真里同样要练消抖GPIO 输出玩明白了下一步就是输入。按键接法是按键一端接 PB0另一端接 GND。PB0 配置为上拉输入模式这样按键没按下时引脚读到高电平按下时接地读到低电平。代码逻辑是这样if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_RESET) { HAL_Delay(20); if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_RESET) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_RESET); } }中间那 20 毫秒的HAL_Delay是软件消抖。这里有个值得说的点Proteus 仿真里的按键模型本身不会产生机械抖动不像真按键按下的一瞬间电平会跳变好几次。那你为什么还要写消抖因为仿真只是跳板不是终点。你现在写的代码以后要原封不动逻辑迁移到真板上。真板上按键按下、松开接触弹片会在一两毫秒内反复通断如果不消抖一次按键可能被当成几次触发LED 状态就乱跳。在仿真里就把消抖写进去以后上真板不用返工。这是仿真学习法一个很重要的心法不是仿真器需要什么而是代码要在真机上健壮。我见过不少人在仿真里为了图省事不写消抖结果后面真板按键测试翻车调了半天还以为是硬件问题。这属于典型的“仿真骗自己”案例。3.2 定时器从定时中断到 PWM 呼吸灯定时器是 STM32 外设里最值得花时间搞懂的模块没有之一。它不只是一个延时工具更是 PWM 输出、输入捕获、编码器模式这些高级功能的基础。我的建议是分两步练。第一步用定时器做固定周期中断用标志位代替延时。以 TIM2 为例先使能 TIM2 时钟然后配置预分频器和自动重装载寄存器。F103 的 TIM2 挂载在 APB1 总线上系统时钟经过预分频后定时器时钟是 72MHz。要产生 100Hz 的中断频率公式是中断频率 定时器时钟 / 预分频 1 / 自动重装载值 1。所以设 PSC 7199ARR 9972MHz 除以 7200 再除以 100正好是 100Hz也就是 10 毫秒一次中断。TIM_HandleTypeDef htim2 {0}; htim2.Instance TIM2; htim2.Init.Prescaler 7199; htim2.Init.Period 99; htim2.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_Base_Init(htim2); HAL_TIM_Base_Start_IT(htim2);注意HAL_TIM_Base_Init之前必须先__HAL_RCC_TIM2_CLK_ENABLE()并且用中断方式要配置 NVIC 优先级并且使能中断。中断回调里做事情void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { flag_10ms 1; } }这种“定时中断置标志主循环查标志”的做法比习惯性 HAL_Delay 优秀得多。主循环不会被阻塞后面要同时处理按键、串口这种多任务逻辑时这是必经之路。第二步是 PWM 呼吸灯。思路是用定时器的 PWM 输出通道不断改变占空比LED 亮度就会由暗到亮、由亮到暗地变化。核心代码是把比较寄存器 CCR 的值循环调整for (uint16_t duty 0; duty 999; duty 10) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, duty); HAL_Delay(10); }在仿真里呼吸灯的效果非常丝滑和真板几乎看不出差别。而且因为是仿真环境你可以随时暂停看 CCR 寄存器实时值理解占空比和亮度的对应关系——这在真板调试时反而没这么方便。3.3 UART 串口用虚拟终端代替串口调试助手串口是被低估的调试神器。没有显示屏的情况下串口是程序最直接的“嘴巴”。Proteus 里的虚拟终端组件 Virtual Terminal 可以直接当串口助手用。接线方式USART1 的发送引脚 PA9 接到虚拟终端的 RXUSART1 的接收引脚 PA10 接到虚拟终端的 TX。虚拟终端的属性里把波特率改成和代码一致默认可能不一致。代码初始化好 UART1 后发送字符串uint8_t msg[] Hello STM32\r\n; HAL_UART_Transmit(huart1, msg, strlen((char *)msg), 1000);最后一个参数是超时时间单位毫秒。如果串口总线异常函数会超时返回不会一直卡死。虚拟终端在仿真里有个特点从键盘输入到虚拟终端的内容会直接送到 RX 引脚。用来测试回显功能很方便。想让输入的内容在屏幕上显示出来需要在代码里把收到的数据再发出去uint8_t rx_byte; HAL_UART_Receive(huart1, rx_byte, 1, 100); HAL_UART_Transmit(huart1, rx_byte, 1, 100);注意一点仿真里的虚拟终端和真机的串口助手不同它不关心电平信号是否符合 RS232 标准直接模拟的是单片机里的 UART 外设逻辑。所以这段练习的重点应该放在波特率计算、数据位、停止位这些“通信协议参数”上而不是电气连接。我实测下来UART 在仿真里是最容易上手、反馈最快的外设。如果学习时间有限建议优先掌握它。3.4 I2C 与显示外设最值得在仿真里练的协议类外设I2C 协议在刚接触时容易懵因为涉及起始条件、停止条件、应答 ACK、地址位还有主机和从机的角色切换。真板调 I2C 时如果没有逻辑分析仪基本靠猜。而在仿真里可以直接放一个 I2C 调试器组件它会把 SCL、SDA 上的电平变化以时序图的形式显示出来你甚至能看到每一帧 ACK 是在哪一拍产生的。我用的经典组合是 STM32 的 I2C1 接口接一个 24C02 EEPROM。SCL 接 PB6SDA 接 PB7。这两个引脚各自需要接一个上拉电阻到 VDD。真机上 I2C 是开漏输出必须靠上拉电阻把线拉高仿真里虽然不按照真实电气特性跑但如果不接上拉模型有时会报总线异常。所以还是老老实实接上养成好习惯。写一个字节到 EEPROM 指定地址标准库函数是这样HAL_I2C_Mem_Write(hi2c1, 0xA0, 0x00, 1, data, 1, 1000);第一个参数是 I2C 句柄第二个参数是设备地址左移一位后的写地址第三个参数是 EEPROM 内部存储地址第四个是地址长度24C02 用 1 字节地址第五个是数据缓冲。如果是真板调试通常要对着 datasheet 查半天但仿真里你可以配合 I2C 调试器边跑边看把协议时序彻底看懂再切真板效率高很多。除了 EEPROMProteus 的 LM016L 字符液晶也是练习的好工具。四线模式接线稍多但仿真里照着框图接就行。这块容易踩的坑是液晶初始化时序仿真模型对时序比较敏感步骤之间要加延时否则屏上不出字符。调试方法是在关键步骤后加断点在单步执行时观察液晶数据引脚的电平状态。在仿真里练 I2C 和显示外设的最大价值是你能在一个完全可控的环境里反复修改时序和参数看到直接结果。这种试错成本几乎为零的练习方式在真板上很难获得。3.5 中断与观察窗口仿真器调试中断比真板还方便中断是嵌入式开发的灵魂但也是新手最难建立直觉的地方。实话说真板上调试中断需要接调试器、设断点还要时刻小心断点会不会影响中断时序。仿真就轻松多了。我推荐练外部中断 EXT还是用那个按键。按键接 PB0配置为 EXTI0 的外部中断输入下降沿触发。然后 NVIC 里使能 EXTI0 中断并设置优先级HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn);中断回调函数里做状态翻转void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_0) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); } }为什么要把处理逻辑放在回调里而不是在中断服务函数里直接写因为中断服务函数是给系统厂的框架层用的用户代码应该写在回调里保持框架整洁且便于复用。仿真里看中断现场很方便程序停在断点时可以打开寄存器窗口和外设状态窗口看中断标志位、挂起状态、优先级组的配置结果。甚至可以用单步执行的方式一步步看着中断从触发到进入向量表、跳到回调函数、执行完再返回主循环。这个过程把“中断到底怎么工作”演得清清楚楚。真板接调试器也能看但调试器本身会干扰实时性且初学者往往不知道看哪里。4. 仿真踩坑实录与排查技巧4.1 常见问题速查表把自己和身边人踩过的坑整理一下做成速查表后面遇到类似问题直接对号入座现象可能原因解决办法仿真完全没反应Keil 没勾选生成 hex或加载的 hex 不是最新编译产物勾选 Create HEX File 后重新编译再重新加载LED 不亮GPIO 初始化引脚写错或电路连线 NET 标号不匹配核对初始化代码里的引脚号和原理图里的网络标号虚拟终端显示乱码代码波特率和虚拟终端属性不一致把两处波特率设为一致一般 9600 或 115200定时器周期明显不准外部晶振没接或 RCC 时钟树配置不正确检查最小系统晶振部分核对自己的时钟初始化I2C 通讯报错SCL/SDA 缺少上拉电阻加上 2.2kΩ 到 4.7kΩ 上拉到 VDD程序启动即跑飞启动文件或器件型号选错重新对照器件容量检查 startup 文件仿真速度很慢动画模式开了太多实时渲染效果在 System - Animation Options 里关闭不必要的显示效果降低刷新频率液晶不显示字符初始化时序不够稳缺少延时初始化过程每步之间增加延时必要时单步逐步验证4.2 仿真和真板之间到底差多少这个问题如果搞不清楚会误判自己的水平。能练到的能力相当多寄存器配置、外设初始化流程、中断优先级设计、协议时序理解、状态机架构、调试思维方式。这些占了嵌入式工程师日常工作的主要部分。你在仿真里搭出来的 LED 流水灯、串口回显、EEPROM 读写逻辑完全是真实可用的。练不到的呢主要是“电气”层面的内容。真实芯片的上电时序、引脚驱动能力、信号完整性、电源噪声、干扰、ADC 参考电压的温漂、下载器连接失败排查、示波器和逻辑分析仪的使用等等。这些在仿真界面里被抽象掉了它们更像“硬件工程师”的活或者说是嵌入式工程师进阶之后必须补的经验。所以我的建议是仿真学成功只能算拿到软件控制外设的核心能力别觉得自己已经能裸板上手所有项目。后续如果有机会碰真板第一次点灯的心跳加速感谁也替代不了。但如果你只是为了建立知识框架、横扫入门障碍仿真完全够用。4.3 我给新手的学习节奏建议结合带人经验整理了一套四阶段走法每一阶段都有明确产出不至于学完没感觉。第一阶段点亮 LED、按键控制、外部中断。目标是吃透 GPIO 的输入输出、上下拉、时钟使能、中断响应。产出一个小项目按键切换 LED 状态。第二阶段定时器中断、PWM 输出、UART 收发。目标是理解分频计数原理、中断回调机制、串口协议参数。产出呼吸灯加串口状态打印。第三阶段I2C 和 SPI 协议类外设。目标是能独立读 datasheet、看懂时序图。产出读写 EEPROM并在虚拟终端打印回读结果。第四阶段综合练习。做一个相对完整的系统比如带按键、数码管、串口的状态机小项目开始考虑代码结构、模块划分。到这里你已经具备了学习 RTOS 之前的大部分基础。每个阶段都在仿真里完成不需要硬件的介入。最理想的节奏是每晚抽一两个小时一个阶段大概两三周能结束。等到仿真体系建立完毕再决定要不要投资硬件。到时候你的起点就不是零了。我自己实测带过几个完全没有硬件基础的同学最快的从装软件到按键控制 LED 跑通只花了一个晚上。仿真最大的意义不是替代开发板而是抹平了“从零到一”的门槛。很多人不是学不会 STM32而是一开始就被买板子、装驱动、接线、怕烧板这些事吓退了。等真正在仿真里把外设跑过一遍再上手板子你会发现之前想不明白的时钟树、中断优先级、寄存器配置其实早就通过一次次的仿真练习刻在脑子了。最后提醒一句如果仿真里跑通了程序但换了真板不工作第一反应别怀疑是芯片坏了先检查最小系统电路和硬件连线。仿真帮你屏蔽了太多硬件细节真板会把它们加倍奉还。这不算坏消息这只是说明你进入了一个新的、更真实的阶段。