简介:一套面向零基础或 51 单片机转学者的 STM32 入门仿真工程,对应教程第一讲 LED 点亮与闪烁。以 Proteus 无实物方式搭建 STM32F103R6 最小验证环境,重点解决没有开发板也能完成 IO 输出初始化、延时与闪烁控制学习的问题。工程文件共 163 个,主体为 Keil 的 C/H 源码、uvproj/uvopt 工程配置、编译生成的 o/axf/hex 可执行文件,以及用于 Proteus 仿真的 pdsprj 工程与封装备份,压缩包仅 2.74MB,结构轻量、便于下载。源码刻意独立成单个 LED 程序文件,不依赖复杂库调用,并配有 delay 函数实现固定间隔闪烁;仿真已验证可运行,后续也可直接迁移到实物开发板继续调试。已有 4287 人学习下载,适合作为 Proteus 与 STM32 流程入门、IO 控制练习和课程实验参考。
1. 无实物学STM32,先从Proteus里点亮一颗LED开始
第一次学STM32,很多人卡在硬件准备上:开发板还在路上、调试器驱动装不上、杜邦线接触不良,甚至芯片本身是翻新货,跑不起来也不知道是代码问题还是板子问题。其实点灯这一步,软件仿真已经完全够用。Proteus里放一片STM32F103C8,接一颗LED和限流电阻,用Keil写好代码生成hex文件,双击芯片加载进去,点击运行,LED就亮了。整个过程不碰任何实物,但RCC开时钟、GPIO配模式、输出数据寄存器置位这些底层操作一样不落,反而因为少了硬件干扰,更容易看清楚每一步在干什么。这套路线适合零基础自学,也适合手头暂时没有板子的在职者先建立整体流程;等真板子到手,需要补的只是下载器接线和烧录驱动。下面从Proteus环境准备开始,一路走到LED闪烁,顺带把最容易卡住的参数设置和坑位讲清楚。
2. Proteus仿真环境搭建:芯片包、元件与最小电路
2.1 先确认Proteus版本与STM32芯片包
装了Proteus之后搜不到STM32,是新手遇到的第一个墙。打开元件选择窗口,在关键字栏输入STM32F103C8,如果列表里一片空白,基本可以判断是元件库不全,不是操作问题。Proteus不同安装包对MCU模型库的裁剪程度不一样,有些精简版连STM32的模型都没带进去。
补芯片包常见做法是:下载对应版本的STM32库文件,把扩展名为.IDX和.LIB的两个文件复制到Proteus安装目录下的LIBRARY文件夹中,然后重启软件再搜索。这里要注意库文件版本和Proteus主程序版本尽量匹配,8系列的库硬放到9系列下,打开工程时可能报library not found,比搜不到元件更麻烦。
提示:如果确实搜不到STM32F103C8,可以用STM32F103R6或STM32F103ZET6代替。点灯用到的GPIO、RCC逻辑完全一致,差别只在引脚数量、Flash大小和封装形式,零基础阶段不用卡在“必须和教程完全同型号”上。
2.2 元件清单与参数选择
在Proteus里画最小系统电路,先列一份元件清单。下面这张表是按“接近真实硬件”的标准配的,仿真能不能跑和它无关,但以后移植到实物时可以直接照抄。
| 元件 | Proteus搜索关键字 | 参数 | 作用 |
|---|---|---|---|
| 主控 | STM32F103C8 | LQFP48 | 运行点灯程序 |
| LED | LED-RED | 红色 | 显示亮灭状态 |
| 电阻 | RES | 220Ω | 限制LED电流 |
| 晶振 | CRYSTAL | 8MHz | 提供外部高速时钟 |
| 电容 | CAP | 20pF | 晶振负载电容 |
| 电阻 | RES | 10kΩ | NRST复位脚上拉 |
限流电阻为什么选220Ω而不是直接短路:红色LED正向压降约2V,STM32 GPIO输出高电平为3.3V,按220Ω计算电流约6mA,既能明显点亮又不会超过引脚最大灌电流。Proteus仿真本身不会真烧芯片,但按真实值画,后面接实物时就不会因为“仿真能跑、实物冒烟”而返工。
晶振和两个20pF电容在仿真里画不画都能运行,原因是STM32上电后默认走内部HSI时钟,不依赖外部晶振。但既然叫“最小系统”,建议把晶振电路画上。这样后面学到时钟树配置、PLL倍频时,可以直接在这个电路上验证HSE通路,不用再改图。
2.3 最小电路引脚连接
打开Proteus的元件放置界面,按下面的连接清单接线。在仿真软件里没有“接线错误”的自动提示,只能自己对着数据手册核对引脚编号,这是Proteus仿真和实物最大的一点区别。
3.3V 电源轨 -> VDD(24脚) / VDDA(9脚) GND 电源轨 -> VSS(23脚) / VSSA(8脚) / BOOT0(44脚) 3.3V 电源轨 -> NRST(7脚) 经10kΩ电阻上拉 OSC_IN(5脚) <-> 8MHz晶振第1脚 OSC_OUT(6脚) <-> 8MHz晶振第2脚 晶振两端分别经20pF电容接地 PB0(18脚) -> 220Ω电阻 -> LED阳极 LED阴极 -> GNDVDD和VDDA是两个独立的电源引脚,在Proteus里经常被忽略。只给VDD供电、VDDA悬空,仿真可能正常,但一旦涉及ADC模块就会出诡异问题。建议一开始就把两个电源脚都接到3.3V,养成习惯。BOOT0直接接地,让芯片从主Flash启动,这是最常规的启动方式。NRST经10kΩ上拉到3.3V,防止复位脚受干扰误触发。
电容值可以按经验选,20pF是8MHz晶振的常见负载配置。如果后面改用12MHz晶振,负载电容要跟着调整,不过点灯阶段不用太过纠结这个值。
2.4 电路画完后的检查清单
画完电路先别急着写代码,花一分钟做三件事:第一,确认LED阳极接的是PB0而不是PB1,引脚编号和原理图网络标号对不上会让后面所有排查走弯路;第二,确认电阻串在LED和GPIO之间,而不是LED和GND之间,虽然两种接法都能亮,但前者更符合常规设计;第三,确认电源网络标签是“3.3V”而不是“VCC”,Proteus里电源端子选错了电压值,芯片是不会工作的。
都确认好,把工程命名保存。文件路径不要带中文,也不要带空格。后面Keil生成的hex文件要给Proteus加载,路径里有中文时,部分Proteus版本会直接加载失败,或者加载后仿真速度异常,这是很多教程没提过的坑。
3. Keil MDK工程与GPIO配置:让代码控制LED
3.1 新建Keil工程:芯片选型、启动文件与头文件路径
打开Keil MDK后,通过Project -> New uVision Project新建工程,在弹出的芯片选择窗口里输入STM32F103C8,选中对应型号。接下来Keil会询问是否复制启动文件到工程目录,选择“是”。
STM32F103C8属于中容量产品,对应启动文件是startup_stm32f10x_md.s。选错启动文件的情况很常见:有人复制了大容量型号的hd启动文件进工程,编译能过,但仿真时程序计数器会跑飞,LED完全没反应。零基础阶段直接让Keil自动添加,不要手动换。
标准库的加入方式这里展开说一下。从ST官网或第三方下载STM32F10x_StdPeriph_Lib后,不需要把所有文件都塞进工程,只要保证以下几点:工程里包含stm32f10x_gpio.c、stm32f10x_rcc.c两个源文件;头文件路径通过魔术棒Options for Target -> C/C++ -> Include Paths加入库的Libraries/inc目录;编译器宏定义里加上USE_STDPERIPH_DRIVER。没有这个宏,stm32f10x.h会默认走纯寄存器模式,库函数全部不可用。
3.2 标准库还是寄存器:零基础先选标准库
学STM32点灯,代码风格有三条路:纯寄存器操作、标准外设库、STM32CubeMX生成的HAL库。纯寄存器代码最短,但需要对着参考手册查寄存器位定义,RCC配置、GPIO配置、数据输出各占几行,对新手不友好;HAL库是ST现在主推的方向,初始化代码量大,中间层厚,虽然CubeMX图形化配置很直观,但出了问题想调试,调用栈很深,零基础容易迷失。
标准库恰好在这两者中间。GPIO初始化结构体里每个字段对应什么含义,看代码就能理解,同时底层寄存器操作也没有被完全封装掉。网上现成的STM32入门教程八成以上基于标准库,遇到问题搜索时匹配度最高。因此后续代码统一用标准库风格。
3.3 GPIO初始化代码:时钟、模式、速度各起什么作用
新建main.c,写入下面这段GPIO初始化代码。这是整个点灯流程的核心,建议逐行读懂再继续往下做。
#include "stm32f10x.h" void LED_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 开启GPIOB外设时钟,STM32外设时钟默认全部关闭 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 配置PB0为推挽输出,最大翻转速度50MHz GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); } int main(void) { LED_GPIO_Config(); while (1) { GPIO_SetBits(GPIOB, GPIO_Pin_0); // PB0输出高电平,点亮LED } }RCC_APB2PeriphClockCmd这行最容易漏。STM32为了降低功耗,所有外设时钟默认关闭,GPIOB也不例外。不开启时钟,后面配置寄存器的写操作不会生效。初学者常犯的错误是把芯片选成STM32F103C8,却在代码里开启RCC_APB2Periph_GPIOA,然后电路上接的是PB0,结果LED当然不亮。
GPIO_Mode_Out_PP是推挽输出模式,表示GPIO可以直接输出强高电平和强低电平,驱动LED这种负载足够。下面的表格列出了标准库中常见的GPIO模式,点灯阶段只需要关注推挽输出即可。
| 模式宏 | 作用 | 点灯场景是否常用 |
|---|---|---|
| GPIO_Mode_AIN | 模拟输入 | 否,ADC场景使用 |
| GPIO_Mode_IN_FLOATING | 浮空输入 | 否,外部电平不确定 |
| GPIO_Mode_IPD | 下拉输入 | 否,按键检测可能用 |
| GPIO_Mode_IPU | 上拉输入 | 按键检测常用 |
| GPIO_Mode_Out_OD | 开漏输出 | I2C等总线场景 |
| GPIO_Mode_Out_PP | 推挽输出 | 点亮LED最常用 |
GPIO_Speed_50MHz在仿真中看不出差别,但按真实硬件习惯带上。因为GPIO翻转速度设置过低时,高频PWM波形会变形,点灯虽不涉及,但后面做呼吸灯或PWM调光时就有影响。
3.4 让LED先保持常亮,验证整个链路
main函数里只有一个GPIO_SetBits,这个函数的作用是把指定引脚输出寄存器置1,也就是让PB0输出高电平。LED阳极通过220Ω电阻接PB0,阴极接地,因此高电平点亮。如果电路里LED接法是反的,比如阴极接PB0、阳极接3.3V,那就要用GPIO_ResetBits让引脚输出低电平才亮。
先跑通常亮再考虑闪烁。常亮状态下,只要Proteus里能看到LED亮起,就说明RCC配置、GPIO配置、hex加载、电路连接这四个环节全部正确,后续做闪烁只是在main函数里增加延时和翻转而已。
4. 编译生成hex并加载到Proteus:LED点亮的那一步
4.1 编译前必须勾选的HEX输出选项
Keil默认不生成hex文件,只生成用于调试的axf文件。Proteus不认识axf,所以编译前需要手动修改一个选项。
打开Options for Target -> Output选项卡,在Create HEX File前打勾,然后重新编译。编译完成后,Build Output窗口会出现类似下面的输出:
Build Target 'Target 1' linking... Program Size: Code=1416 RO-data=8 RW-data=0 ZI-data=1024 FromELF: creating hex file... "Objects\LED_Blink.hex" - 0 Error(s), 0 Warning(s).看到0 Error(s)是硬性要求,warning可以暂时不管。最常见的warning是声明了变量但没有使用,这类warning不影响hex生成。如果出现Error: L6218E: Undefined symbol,多半是标准库的stm32f10x_gpio.c或stm32f10x_rcc.c没有加进工程,返回第3章检查源文件列表。
hex文件生成在工程目录下的Objects子目录里,文件名与Target名称一致。可以在Windows命令行里用dir命令确认文件是否生成成功:
dir Objects\LED_Blink.hex4.2 将HEX加载到Proteus芯片并启动仿真
回到Proteus,双击原理图中的STM32F103C8芯片,打开属性窗口,在Program File一栏点击文件夹图标,选择刚才编译出来的hex文件。CKSYS一栏填8MHz,这个值要和代码里的时钟配置匹配。
点击Proteus左下角的运行按钮,如果一切正常,LED会亮起。注意Proteus默认在实时仿真模式下运行,点击按钮后应该立刻看到效果。如果LED亮但亮度很暗,检查电阻是不是用错了阻值;如果LED完全不亮,则继续看下面的排查顺序。
4.3 LED不亮按这个顺序排查
仿真不亮时,按表格里的顺序排查,每一步都确认后再进下一步,不要跳着试。
| 现象 | 排查顺序 |
|---|---|
| LED完全不亮 | 1. 芯片属性里hex路径是否正确 2. CKSYS是否与代码匹配 3. LED极性是否接反 4. PB0引脚编号是否接对 5. GPIOB时钟是否开启 |
| 编译报错缺头文件 | Include Paths是否指向标准库Libraries/inc目录 |
| Keil没有生成hex | 是否勾选Create HEX File,勾选后是否重新编译 |
| 仿真运行时提示无法加载 | hex文件路径不能有中文和空格,换到纯英文目录再试 |
最隐蔽的问题是LED极性接反。Proteus里LED元件是有方向的,三角形箭头指向的一端是阴极,另一端是阳极。有人从元件库拖出LED,旋转了几次之后方向变了,却没有注意,结果阳极接了GND、阴极接电阻到PB0,程序输出高电平反而让LED熄灭。遇到不亮的情况,先把LED方向作为第一怀疑对象。
另一个高频错误是程序里操作的是GPIO_Pin_0,但电路上把电阻接到了PB1。原理图上网络标签不会报错,仿真也能正常运行,只是LED无反应。这种接线和代码不一致的问题,在实物上同样会发生,排查时需要养成“代码引脚和原理图引脚逐个对照”的习惯。
5. 从点亮到闪烁:延时调优与仿真观察
5.1 用延时函数让LED闪烁
常亮跑通后,把main函数改成下面这样,LED就会按500ms间隔闪烁。这里用了最简单的软件延时,没有占用任何外设资源,逻辑清晰:
#include "stm32f10x.h" void Delay_MS(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 7200; j++) ; } int main(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); while (1) { GPIO_SetBits(GPIOB, GPIO_Pin_0); // 点亮LED Delay_MS(500); GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 熄灭LED Delay_MS(500); } }Delay_MS里的7200是按72MHz主频粗略估算出来的。内层循环一次大约执行几个机器周期,7200次大约对应1ms。这个延时不精确,因为编译器优化等级改变时会直接影响循环体执行时间,同一段代码在不同优化级别下延时长度不一样。点灯阶段可以先用它,后面学到SysTick定时器或用定时器中断做延时后再替换。理解软件延时的缺点,本身就是学习的一部分。
调整Delay_MS的参数,闪烁节奏会相应变化。下面这组参数比较常用,仿真中可以快速切换观察:
| 延时参数 | 闪烁节奏 | 适用场景 |
|---|---|---|
| 1000 | 1秒亮、1秒灭 | 入门观察,节奏最舒服 |
| 500 | 500ms亮、500ms灭 | 标准状态指示灯 |
| 200 | 200ms亮、200ms灭 | 快速闪烁,接近报警灯 |
5.2 用虚拟示波器观察闪烁波形
肉眼观察LED亮灭只能判断“有没有闪”,看不清高电平持续时间和占空比。Proteus左边工具栏有虚拟仪器模式,里面提供OSCILLOSCOPE示波器。把示波器的A通道接到PB0引脚,地线接GND,运行仿真后可以看到PB0上的方波信号。方波高电平持续时间就是Delay_MS(500)的延时长度,低电平时间同理。
用示波器还有一个好处:它能直观验证GPIO翻转是否真的发生了。有时候LED坏了或电阻太大,视觉上以为程序没运行,但示波器上波形完全正常,问题就被快速隔离到LED这一路。如果想统计精确的闪烁频率,可以用计数光标或直接在示波器面板上读周期。
5.3 调闪烁频率时最实用的一个技巧
反复修改代码里的延时参数再重新编译,效率太低。更快的办法是把延时参数做成变量,然后用Keil的Debug模式配合仿真器观察。
在Keil里点击Debug -> Start/Stop Debug Session,选择Proteus VSM仿真器作为调试目标,运行程序后打开Peripherals -> General Purpose IO -> GPIOB,可以看到PB0引脚的ODR寄存器值随程序翻转。此时在Delay_MS调用处设置断点,运行到断点后直接修改延时变量的值,继续运行,闪烁频率就会实时改变。整个过程不需要退出仿真,也不需要重新编译。把延时值依次改成100、200、500、1000,再回到示波器上看波形,你对软件延时的认知会比单纯改代码深刻得多。
本文还有配套的精品资源,点击获取