资讯动态

STM32纯软件仿真入门:零硬件跑通GPIO、PWM、UART与ADC

发布时间:2026/10/13 5:27:49 来源:尧图企业网站定制
很多人一听到“学 STM32”条件反射就是去搜开发板然后下单前先纠结买哪个型号、要不要配下载器、要不再加个显示屏。这个思路本身没什么问题但作为一个帮好几拨朋友带过入门、自己也折腾过好几块板子的人我必须说一句实在话如果你的目标只是把 GPIO、定时器、串口、ADC 这些外设一个个跑通建立对单片机最基本的感觉那么一块板子都不用买。用 STM32 仿真的方式你在电脑上就能把整个入门闭环走完而且过程中踩的坑还比真机少。这篇文章就是我自己这套“纯软件版”STM32 入门的完整记录。从环境搭建开始到第一个流水灯、再到定时器 PWM、串口打印、ADC 采样每一步都有配置细节、代码和仿真操作。全程不需要硬件投入唯一需要的就是一台能装软件的电脑。如果你是零基础想试水、或者学生党暂时没有预算可以直接照着做。1. 为什么我坚持把入门做成“纯软件版”1.1 先拆解一个常见顾虑仿真到底靠不靠谱每次提到用仿真学单片机总有人第一反应是“仿真会不会是骗人的”“跟实际板子差距是不是很大”。我的回答分两层。第一层对入门阶段的大部分实验来说仿真不仅功能完整而且比真实板子更适合学习。你可以随时暂停程序去看某根引脚此刻是高电平还是低电平可以打开示波器观察 PWM 波形可以在代码里改一个参数点一下运行立刻看到结果。这些反馈在真机上反而不容易做到你需要额外接逻辑分析仪、示波器还要处理下载器连接问题。第二层仿真不是“假跑”。Keil 编译生成的 hex 文件里是真正编译出的 ARM 机器码Proteus 里的虚拟芯片也是按照指令集解释执行的。换句话说同一个工程仿真里跑通之后烧到真板上只要引脚对得上行为基本一致。这在“逻辑验证”这个层面是完全有效的。当然仿真不能替代一切硬件调试。真实的信号噪声、电源波动、晶振起振时间这些电气特性仿真里都是理想化的。所以我的建议很明确入门阶段用仿真建立概念等确定自己真的要深入做项目了再买开发板不迟。1.2 工具选型我为什么最终选了 CubeMX Keil Proteus 这个组合市面上 STM32 仿真的方案不止一种但我带过的朋友里成功率最高、反馈最直观的组合是这三个软件STM32CubeMX意法半导体官方的初始化代码生成器负责引脚分配、时钟树、外设参数配置。Keil MDK编译环境把代码编译成 ARM 可执行的 hex 文件。Proteus电路仿真平台在虚拟面包板上摆芯片、LED、电阻、虚拟终端然后加载 hex 运行。有人会问为什么不用纯软件指令模拟器或者直接用调试器的模拟模式原因很简单——不直观。纯指令模拟器只能看寄存器和内存变化你很难把“寄存器某一位等于 1”和“LED 亮了”联系起来。Proteus 里直接摆上 LED、按键、示波器、虚拟串口终端代码跑了之后眼前是真实的电路行为这对建立“代码控制物理引脚”的映射感帮助极大。1.3 这套方案适合什么人不适合什么人先说适合的群体。完全零基础、不确定自己要不要深入嵌入式的人先用仿真花几百块钱的时间成本试水是最划算的方式。还有就是学生党做课程设计、想快速验证某个外设代码逻辑仿真也很方便。我自己做项目时如果新用到某个不熟悉的外设也经常先在 Proteus 里搭一下确认时序逻辑没问题再上真机。不适合的情况也有。如果你要做模拟电路噪声分析、跑多任务实时调度并且关心时序抖动、或者要验证某颗芯片在某块自研板上的实际兼容性那仿真帮不了你必须上真板和示波器。另外如果手头已经有开发板了那就用真板学仿真对你反而是弯路。这个工具是给“暂时不想花钱”或者“想快速验证”的人准备的。2. 一小时的准备量把仿真环境真正搭起来2.1 软件清单与安装顺序这三个软件的安装顺序我建议固定为CubeMX、Keil、Proteus。理由很简单后面两个软件都需要安装包而 CubeMX 生成的代码在 Keil 里编译Keil 又要设置编译器路径先装好工具链能减少后续配置的折腾。版本方面CubeMX 持续更新没问题Keil 需要注意版本兼容性。Proteus 我用的是 8.10 以上版本对 STM32F103 系列的支持比较稳定。安装过程本身不复杂但有几个细节容易出问题CubeMX 首次运行会要求配置固件包路径默认在用户目录下。Keil 装完后需要在 Pack Installer 里确认已经装了 STM32F1 系列的支持包。Proteus 如果提示缺库一般是因为对应型号的器件模型没装全重装完整版可以解决。2.2 晶振和时钟仿真电路里最容易被忽略的一环这条我要单独拎出来因为带过的朋友里十有七八卡在这里。STM32F103 的 CubeMX 工程里我们通常会配置 HSE 外部高速晶振作为时钟源然后通过 PLL 倍频到 72MHz。但在 Proteus 里如果你只在电路图上放了一颗 STM32F103C8 芯片没有放外部晶振那 HSE 根本起振不了。具体表现非常迷惑代码编译没错误Proteus 也能加载 hex但跑起来后延时时间完全不对、串口乱码、PWM 频率不对。当时我排查了老半天才发现系统时钟可能已经退回内部 HSI 了。解决办法也简单一种是在 Proteus 电路里手动放一个 8MHz 晶振两边各加一个 22pF 的电容接到地另一种是 CubeMX 里直接用 HSI 内部时钟把系统时钟配置到 8MHz 或者 64MHz不再依赖外部晶振。考虑到仿真速度我自己的习惯是入门实验全部走 HSI系统时钟 8MHz省事又跑得快。如果要严格验证某个和时钟强相关的功能再上外部晶振方案。2.3 从 CubeMX 到 Keil 的完整编译链路第一次让 hex 跑起来不管做哪个外设实验最初的链路必须跑通。这里给一套适合新手的操作流程。CubeMX 新建工程选芯片 STM32F103C8T6。RCC 页面HSE 选择 Crystal/Ceramic Resonator如果走内部时钟这里可以不用动。SYS 页面Debug 选择 Serial Wire这一步不是必须但提前配置好以后调试方便。时钟树页面按刚才说的配置 HSI 8MHz或者 HSE PLL 到 72MHz。Project Manager 页面Toolchain 选择 MDK-ARM然后 Generate Code。Keil 打开生成的工程菜单 Options for Target - Output勾选 Create HEX File。编译看到 0 Error 0 Warning。Proteus 里放一颗 STM32F103C8双击芯片在 Program File 里选中刚才生成的 hex 文件。第一次跑通时不妨先做个最简单的 GPIO 实验把一个引脚拉高拉低。Proteus 运行时引脚上会显示颜色红色代表高电平蓝色代表低电平。即使没有接任何外部元件你也能直观看到代码确实在控制引脚翻转。这个反馈很重要它意味着整个工具链打通了。3. GPIO 实验从点灯开始建立“代码到引脚”的映射感3.1 流水灯的需求拆解与时序设计GPIO 是 STM32 最基本的外设也是理解“寄存器映射”最好的切入点。我先说一个典型的流水灯需求8 个 LED 接在 PA0 到 PA7每隔 200ms 点亮下一个循环往复。LED 的接法有两种。一种是 LED 正极接 3.3V负极通过电阻接到引脚这样引脚输出低电平灯亮另一种是 LED 负极接 GND正极通过电阻接引脚引脚输出高电平灯亮。Proteus 里两种都行我习惯用高电平点亮、引脚直接推挽输出的方式逻辑更直白引脚输出 1 灯亮输出 0 灯灭。限流电阻别忘了220 到 330 欧姆都可以。你说 Proteus 里不接电阻灯也亮确实能亮但这不是好习惯仿真里烧不坏芯片真机里极有可能伤害引脚。入门阶段动手习惯就要建立好。3.2 流水灯代码的两种组织思路移位查表与寄存器直写第一种思路HAL 库配合位操作。PA0 到 PA7 分别对应 GPIO_PIN_0 到 GPIO_PIN_7可以用移位的方式生成一次次向左移动的位掩码。uint8_t led 0x01; while (1) { HAL_GPIO_WritePin(GPIOA, 0xFF, GPIO_PIN_SET); // 先全部熄灭 HAL_GPIO_WritePin(GPIOA, led, GPIO_PIN_RESET); // 点亮当前目标灯 HAL_Delay(200); led 1; if (led 0x00) { led 0x01; } }这段代码里led 变量从 0000 0001 开始每次左移一位。到 1000 0000 后再左移变成 0000 0000这时恢复到 0000 0001。第二种思路直接操作寄存器。HAL_GPIO_WritePin 底层也是往 GPIOA-ODR 寄存器写值所以你完全可以像下面这样写GPIOA-ODR 0x55; HAL_Delay(200);看起来更“底层”但我建议新手还是先用 HAL 库。HAL 库帮你封装了引脚模式初始化寄存器直写虽然代码短但要求你一次性把 MODER、OTYPER、OSPEEDR 这些寄存器都配对否则极易出错。先通过 HAL 库跑通再去源码里看它到底写了哪些寄存器这个路径更平滑。3.3 在 Proteus 里看到灯光流动之后还能做什么流水灯跑通后可以顺手做几个小改动来加深理解。改延时值观察灯光节奏变化把移动方向改一下体会位运算方向再比如加一个按键按下时流水方向反转。这就能引出 GPIO 输入模式和外部中断的概念。GPIO 输出模式除了推挽还有开漏模式。开漏模式必须靠外部上拉电阻才能输出高电平这在 I2C 通信里很常见。你在 Proteus 里把引脚模式改成开漏、给 LED 接一个上拉电阻对比一下和推挽模式的差异这个概念很容易扎根。4. 定时器与 PWM让外设从“会亮”到“会动”4.1 定时器概念为什么软件延时不够用前面流水灯用的 HAL_Delay本质是让 CPU 阻塞在 SysTick 中断里死等。200ms 内 CPU 什么都干不了。一旦程序里同时要处理按键扫描、串口接收、OLED 刷新这种阻塞式延时就会拖垮整个系统。定时器是硬件外设独立于 CPU 计数。你配置好分频和自动重载值定时器自己计数计数到设定值后可以触发中断、改变引脚状态全程不需要 CPU 介入。这也是为什么呼吸灯用软件延时很难做、而用定时器 PWM 很自然的根本原因。4.2 呼吸灯实现从 ARR 和 CCR 两个参数说起PWM 需要理解两个关键寄存器ARR自动重载值和 CCR比较捕获值。定时器计数从 0 开始和 CCR 比较小于 CCR 时输出高电平大于等于 CCR 小于 ARR 时输出低电平。所以 PWM 频率由 ARR 决定占空比由 CCR 和 ARR 的相对大小决定。假设定时器时钟是 72MHz想让 PWM 频率为 1kHz频率 定时器时钟 / ((PSC 1) * (ARR 1))取 PSC 71ARR 999得到 72MHz / 72 / 1000 1kHz。此时 CCR 取值 0 到 999占空比就是 CCR / 1000。CubeMX 里配置一个定时器通道输出 PWM 很简单选 TIM2 的 CH1配置 PSC、ARR然后在 Keil 里用 __HAL_TIM_SET_COMPARE 改变占空比。呼吸灯就是让 CCR 从 0 慢慢加到 999再慢慢减到 0。完整代码如下uint16_t duty 0; int8_t dir 1; HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); while (1) { __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, duty); duty dir; if (duty 999) { dir -1; } if (duty 0) { dir 1; } HAL_Delay(2); }看起来简单但里面有一个常见的坑我在 Proteus 中仿真时如果系统时钟配成 72MHz 且仿真帧率较低整个循环看起来会一顿一顿的。把系统时钟降到 8MHz或者减少 HAL_Delay 的间隔视觉上会顺滑很多。仿真是解释执行的性能本来就比真机差调试时对这些“看起来像 bug 的现象”要有预期。4.3 示波器接上去用波形读懂占空比Proteus 里有一个值得充分利用的虚拟仪器示波器。在画布左侧工具栏找 Virtual Instruments拖一个 Digital Oscilloscope 出来把它的 A 通道接到 PA0 上。运行时你会看到清晰的方波。把 __HAL_TIM_SET_COMPARE 里的 duty 固定成 500示波器上应该显示 50% 占空比。然后改成 250占空比变成 25%。这个过程比用任何文字描述都直观也是我强烈推荐入门阶段就用仿真做 PWM 实验的原因——真机上你要额外借示波器仿真里白送。有一个细节Proteus 示波器上显示的波形和理论值之间有时会出现频率偏移。这不是代码错了而是仿真引擎的步长导致采样精度受限。调整仿真步长Debug - Configure Simulation Options可以改善但入门阶段不用太在意看趋势和量级就够了。5. UART 串口用虚拟终端“看见”单片机说话5.1 串口的基础接线与 CubeMX 配置单片机和外界沟通信息串口是最常用的通道。STM32F103 的 USART1 引脚一般固定在 PA9 作为 TX、PA10 作为 RX。CubeMX 里把 USART1 设为异步模式波特率选 115200数据位 8、停止位 1无校验。Proteus 里接虚拟终端左侧工具栏找 Virtual Instruments选 Virtual Terminal。终端上只有 RXD 和 TXD 两个引脚注意连接方向——单片机的 TXPA9接虚拟终端的 RXD单片机的 RXPA10接终端的 TXD。接反了是串口调试最常见的问题之一仿真里接反了不会损坏设备但你看不到任何数据。双击虚拟终端把波特率设置为 115200和代码保持一致。5.2 发送、接收与重定向 printf 的完整实现发送一帧数据最简单的方式char msg[] Hello STM32 Sim\r\n; HAL_UART_Transmit(huart1, (uint8_t *)msg, strlen(msg), HAL_MAX_DELAY);但这样打印变量值很麻烦。更常用的做法是重定向 printf让工程里所有 printf 都从串口输出。在 Keil 里只需实现 fputc同时注意勾选 MicroLIBint fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, HAL_MAX_DELAY); return ch; }之后就能这样直接打印uint16_t count 0; while (1) { printf(count %d\r\n, count); HAL_Delay(500); }虚拟终端里应该每隔 500ms 滚动一行。这个过程对初学者而言第一次看到“单片机在说话”会有一种难以形容的成就感而它背后的原理其实只是电平按波特率高低翻转。接收方向的轮询写法uint8_t rx; HAL_UART_Receive(huart1, rx, 1, HAL_MAX_DELAY); HAL_UART_Transmit(huart1, rx, 1, HAL_MAX_DELAY);这段代码实现最简单的回显你在虚拟终端键盘上敲一个字符单片机立刻原样发回来。别看功能简单它是一切串口指令交互的基础。5.3 波特率不对时出现的经典乱码案例乱码是串口调试最经典的现象我几乎每次带新手都会遇到。最典型的一种是代码里配置 115200虚拟终端的波特率却是 9600屏幕上出现一片“锟斤拷”一样的乱码。排查思路按顺序走检查虚拟终端波特率是否和代码一致。检查接线方向是否 TX 接 RXD、RX 接 TXD。检查电路里外部晶振是否缺失导致系统时钟跑偏、波特率产生偏差。检查 printf 重定向是否真的被调用可以用 HAL_UART_Transmit 发一个固定字符串做对照排除重定向本身的问题。我见过不少案例前面三步都没问题最后发现是 Keil 里 MicroLIB 没有勾选printf 库实现有问题。这个选项在 Options for Target - Target 页面底部新手容易忽略。6. ADC 采样模拟电压如何变成数字量6.1 从电位器到 GPIOProteus 里搭一条模拟链路之前实验处理的都是数字信号只有 0 和 1。而现实世界的温度、光照、电压都是连续变化的模拟量单片机处理模拟量要靠 ADC。Proteus 里做个最简单的实验放一个电位器 POT-HG两端分别接 3.3V 和 GND中间抽头接 STM32 的 PA0。PA0 对应 ADC1 的通道 0。运行时拖动电位器滑块PA0 的电压在 0 到 3.3V 之间连续变化。CubeMX 里配置 ADC1勾选 IN0分辨率 12 位。这里我建议把采样时间从默认值调大一点比如 55.5 Cycles仿真结果会更稳定。虽然对最终学习影响不大但可以避免后面看到采样值跳来跳去时产生无谓的困惑。6.2 采样代码与虚拟终端联动转动电位器看数据变化ADC 单次轮询采样的核心代码HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, HAL_MAX_DELAY); uint16_t adc_val HAL_ADC_GetValue(hadc1); float voltage adc_val * 3.3f / 4095.0f; printf(ADC %d, Voltage %.2f V\r\n, adc_val, voltage); HAL_Delay(300);12 位分辨率意味着采样结果在 0 到 4095 之间对应 0 到 3.3V。把电压换算公式写清楚然后通过串口打印到虚拟终端。转动电位器时你应该能看到 ADC 值和电压值同步变化。这个实验算是把“模拟世界”和“数字世界”之间的桥搭起来了。一个小技巧在 Proteus 里旋转电位器可以先选中电位器组件然后用鼠标拖动滑块或者右键选择修改属性。拖动时终端里的数值应该是平滑变化而不是跳变如果出现跳变多半是采样时间太短或者 Proteus 仿真步长太长的问题。6.3 仿真里的采样值和真实板子的差异心里要有数仿真环境里电源是理想电压电位器也是理想器件采样值线性度很高。真实板子上参考电压可能有微小波动电源纹波会耦合进 ADC 引脚采样值必然存在噪声。这也是为什么做真实项目时通常要加软件滤波比如连续采 10 次取平均值或者加硬件 RC 低通滤波器。另外要注意ADC 引脚的输入电压不能超过 VDDA 加 0.3V否则有损坏芯片的风险。仿真里永远不会烧芯片但这个习惯要养成以后用真板时才会下意识去检查输入范围。7. 仿真排坑实录我踩过的 8 个典型问题7.1 启动运行类问题问题 1Proteus 加载 hex 后点击运行芯片毫无反应或者提示地址异常。先检查芯片是否加载了正确的 hex 文件。双击芯片在 Program File 里确认路径。如果没有显示加载的文件运行时会直接出错。另一种情况是芯片型号问题Proteus 里和 STM32F103C8 相近的型号有很多引脚定义和存储器配置不一样务必选对。问题 2程序能跑但仿真速度极慢点一下运行LED 进展像蜗牛。Proteus 是解释执行PC 性能直接决定仿真速度。最有效的办法是把系统时钟降下来。如果 CubeMX 里配置了 72MHz改成 8MHz 后仿真速度会有明显提升。别担心行为会变逻辑完全一样。问题 3Keil 编译报错提示找不到某些符号或头文件。这类问题八成出在 CubeMX 生成工程后首次用 Keil 打开时没正确解析 HAL 库路径。确认 Pack Installer 里 STM32F1 系列支持包已安装然后检查工程包含路径是否完整。宁可重新生成一次工程也不要手工去改一堆路径配置。7.2 外设功能类问题问题 4虚拟终端乱码。详见 5.3 的排查顺序这里补充一句经验所有参数都检查完还乱码试着把系统时钟从 HSI 8MHz 切到 HSE 72MHz或者反过来。时钟源不同UART 波特率计数器分频计算也可能不同切换时钟源有时候能意外解决问题。问题 5LED 亮了一下就再也不亮。先看代码是不是进入了死循环或 HardFault。HardFault 在仿真里很常见原因是非法访问地址或不正确的时钟配置。Proteus 运行时芯片引脚颜色变红说明输出 1变蓝说明输出 0。如果程序卡死引脚电平会定格在某个状态。这时候在 Keil 里进入仿真模式调试或者用 Keil 的软件调试模式单步执行定位是哪个函数导致异常。问题 6ADC 采样值一直为 0 或者一直 4095。先确认电位器的三个引脚是否都接对了尤其是抽头是否真的接到了 PA0。然后确认 CubeMX 里 ADC 使能的通道号、引脚对应关系。最后检查是不是循环里每次采样前都调用了 HAL_ADC_Start。如果初始化只 Start 了一次后面轮询时就需要重新启动一次。7.3 速度与稳定性问题问题 7整个 Proteus 工程运行时一卡一卡影响观察。除了降低芯片主频还可以调整仿真帧率和动画选项。从菜单 Debug - Configure Simulation Options 里把帧率调低到 5 到 10 帧每秒实际观察体验反而更好。仿真速度并不代表真实速度不必追求和真机一致的实时性。问题 8PWM 示波器波形和理论计算对不上。多数情况是因为仿真步长太长导致波形量化后频率有偏差。把仿真步长减小波形会还原得更好。另外示波器的采样时间和波形显示长度也要配合观察 1kHz 的 PWM时基设在 1ms 到 5ms 比较合适。这套纯软件 STM32 入门方案我自己完整带过多轮最大的体会是它把学习门槛降低了很多。没有硬件报错、没有接线问题、花了钱却吃灰的风险也降到最低唯一需要投入的就是时间。以我个人的经验等你把这些基础外设在仿真里都跑通了再买一块真开发板去复现同样的实验速度会快得惊人因为单片机基础概念里 80% 的坑你已经提前踩完了。最后分享一个我保持至今的习惯每个实验做完在工程目录下放一个 README记录当时的时钟配置、关键参数、仿真遇到的问题以及最终解决办法。这个动作看着不起眼但后来我做真实项目时翻回这些记录救回过很多次。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑