【零基础嵌入式学习日志】Day28:Proteus 仿真 STM32F103C8 实现 LED 闪烁,Day29:STM32F103C8 四路 LED 流水灯与位运算
【零基础嵌入式学习日志】Day28:Proteus 仿真 STM32F103C8 实现 LED 闪烁
前言
在前面的学习中,我主要使用 Linux、GCC 和纯 C 语言模拟 GPIO、定时器、中断以及通信协议。虽然这种方式有利于理解程序逻辑和模块化设计,但缺少单片机寄存器、引脚电平与外部电路之间的实际联系。
从本次实验开始,学习内容由纯 C 软件模拟进入 STM32F103C8 与 Proteus 联合仿真阶段。本文使用 Keil 编写寄存器级程序,通过配置 RCC、GPIOA 和 SysTick,实现 PA0 控制 LED 周期闪烁。同时使用逻辑探针、直流电压表和虚拟示波器验证 GPIO 输出状态,并记录调试过程中遇到的时钟缩放、LED 显示和测量节点等问题。
一、实验目标
本实验主要完成以下内容:
- 在 Proteus 中搭建 STM32F103C8 LED 闪烁仿真电路。
- 使用寄存器方式配置 PA0 为通用推挽输出。
- 使用 SysTick 建立 1 ms 时间基准。
- 控制 LED 周期性亮灭。
- 使用逻辑探针、电压表和示波器验证 PA0 输出。
- 理解 Proteus 中
OSC Frequency与Clock Scale的作用。
二、开发环境
操作系统:Windows
单片机:STM32F103C8
开发工具:Keil μVision
仿真软件:Proteus 9 Professional
编程语言:C
编程方式:寄存器级编程
系统时钟:内部 HSI 8 MHz
LED 限流电阻:330 Ω
Proteus 中 STM32F103C8 的关键属性设置如下:
Program File:Keil 生成的 HEX 文件
OSC Frequency:8 MHz
Clock Scale:(Default)
OSC32 Frequency:0
注意:不要将
Clock Scale设置为8 Times。否则 Proteus 会额外放大程序执行速度,导致程序中的延时明显变快。
三、硬件连接
本实验使用 PA0 驱动一只红色 LED,连接方式如下:
PA0 → 330 Ω 限流电阻 → LED 阳极
LED 阴极 → GND
采用这种连接方式时,LED 为高电平有效:
PA0 = 1:LED 点亮
PA0 = 0:LED 熄灭
完整原理图中主要包括:
- STM32F103C8
- PA0 输出支路
- 330 Ω 限流电阻
- LED
- NRST 上拉电阻
- BOOT0 接地
- VDD、VDDA、VSS 和 VSSA
- 逻辑探针
- 直流电压表
- 示波器 A 通道
四、GPIO 配置原理
STM32F103C8 的 GPIOA 外设默认没有开启时钟,因此在访问 GPIOA 寄存器之前,必须先开启 GPIOA 时钟:
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
PA0 至 PA7 由 GPIOA->CRL 寄存器控制,每个引脚占 4 位。
对于 PA0,其最低 4 位由 MODE0 和 CNF0 组成:
CNF0 = 00:通用推挽输出
MODE0 = 10:最大输出速度 2 MHz
因此,PA0 对应的 4 位配置值为:
0010,即 0x2
对应代码如下:
GPIOA->CRL &= ~0x0000000FUL;
GPIOA->CRL |= 0x00000002UL;
第一行用于清除 PA0 原来的配置,第二行写入新的输出模式。
五、SysTick 时间基准
SysTick 是 Cortex-M3 内核自带的 24 位递减计数器,可以用于建立周期性时间基准。
本实验将 SysTick 配置为每 1 ms 完成一次计数:
static void systick_init_1ms(void)
{
SystemCoreClockUpdate();
SysTick->LOAD = (SystemCoreClock / 1000U) - 1U;
SysTick->VAL = 0U;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_ENABLE_Msk;
}
其中:
SystemCoreClockUpdate();
用于根据当前 RCC 时钟配置更新 SystemCoreClock。
当系统时钟为 8 MHz 时:
8 000 000 ÷ 1000 = 8000
因此,SysTick 每计数 8000 次约对应 1 ms。
本实验没有开启 SysTick 中断,而是通过轮询 COUNTFLAG 判断一次计数是否结束。
六、毫秒延时函数
static void delay_ms(uint32_t milliseconds)
{
while (milliseconds > 0U)
{
while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0U)
{
/* 等待 1 ms */
}
milliseconds--;
}
}
每当 SysTick 完成一次递减计数,COUNTFLAG 会置 1。程序检测到该标志后,将毫秒计数减 1。
例如:
delay_ms(1000U);
表示等待 1000 个 1 ms 时间片,即约 1 秒。
这种方式属于阻塞式延时。在延时期间,CPU 无法执行其他任务。它适合当前基础实验,后续可以改为基于系统 Tick 和状态变量的非阻塞调度。
七、完整程序
#include "stm32f10x.h"
/*
* 配置 SysTick,每 1 ms 产生一次 COUNTFLAG。
*/
static void systick_init_1ms(void)
{
SystemCoreClockUpdate();
SysTick->LOAD = (SystemCoreClock / 1000U) - 1U;
SysTick->VAL = 0U;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_ENABLE_Msk;
}
/*
* 阻塞式毫秒延时。
*/
static void delay_ms(uint32_t milliseconds)
{
while (milliseconds > 0U)
{
while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0U)
{
/* 等待 1 ms */
}
milliseconds--;
}
}
int main(void)
{
/*
* 开启 GPIOA 外设时钟。
*/
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
/*
* PA0 配置为 2 MHz 通用推挽输出。
*/
GPIOA->CRL &= ~0x0000000FUL;
GPIOA->CRL |= 0x00000002UL;
/*
* 初始输出低电平。
*/
GPIOA->BRR = GPIO_BRR_BR0;
systick_init_1ms();
while (1)
{
/*
* 翻转 PA0 输出状态。
*/
GPIOA->ODR ^= GPIO_ODR_ODR0;
/*
* 当前状态保持 1 秒。
*/
delay_ms(1000U);
}
}
八、程序执行流程
程序执行流程如下:
系统复位
↓
进入 main 函数
↓
开启 GPIOA 时钟
↓
配置 PA0 为推挽输出
↓
配置 SysTick 为 1 ms 时间基准
↓
翻转 PA0
↓
延时 1000 ms
↓
再次翻转 PA0
↓
循环执行
PA0 每隔 1 秒翻转一次,因此:
LED 亮 1 秒
LED 灭 1 秒
完整闪烁周期约为 2 秒
九、仿真结果验证
1. 逻辑探针
将逻辑探针连接到 PA0 后,可以看到其状态在 0 和 1 之间周期切换。
逻辑探针适合连接在 GPIO 数字输出节点,不适合连接在 LED 和电阻之间。
LED 导通时,中间节点可能处于约 1.5 V,既不是标准逻辑 0,也不是标准逻辑 1,因此探针可能显示:
?
这并不表示程序错误。
2. 直流电压表
电压表正端连接 PA0,负端连接 GND。
正常情况下,电压表在以下数值之间切换:
低电平:约 0.00 V
高电平:约 3.20 V
3. 示波器
示波器 A 通道连接 PA0。Proteus 中的虚拟示波器不需要单独连接 GND,其默认参考原理图中的全局地。
建议设置如下:
Channel A:DC
垂直档位:约 1 V/div
Horizontal:200 ms/div
Trigger:Auto
Source:A
此处插入图2:PA0 输出电压的示波器波形
示波器中可以看到 PA0 在约 0 V 和 3.2 V 之间周期切换,说明 GPIO 配置和延时程序能够正常工作。
十、调试过程中遇到的问题
1. 固定输出 0 和 1 正常,但闪烁速度异常
最初将 Proteus 器件属性设置为:
OSC Frequency = 8 MHz
Clock Scale = 8 Times
虽然程序根据 8 MHz 系统时钟计算 SysTick,但 Proteus 又将执行速度放大了 8 倍。
例如程序设置 500 ms 延时,实际观察时间约为:
500 ÷ 8 = 62.5 ms
因此 LED 看起来闪烁得非常快。
解决方法如下:
OSC Frequency = 8 MHz
Clock Scale = (Default)
2. LED 两端电压为 0,但元件仍显示红色
Proteus 中的 LED-RED 元件在熄灭状态下仍会保留暗红色外观,因此不能仅根据元件颜色判断 LED 是否点亮。
判断 LED 状态应结合:
- PA0 逻辑探针
- PA0 对地电压
- LED 内部亮度变化
- 支路电流
- 示波器波形
当 PA0 为 0 且 LED 两端电压为 0 V 时,LED 实际上已经熄灭。
3. 电阻看似放在导线上,但没有真正接入电路
Proteus 中元件必须通过端点真正连接。若电阻两端仍显示短红色引脚,说明电阻悬空,没有接入导线。
正确串联关系必须为:
PA0 → 电阻 → LED → GND
不能存在绕过电阻的并联导线。
4. 示波器中只能看到一条直线
如果示波器时基设置得过小,例如使用 1 ms/div 观察秒级信号,在当前屏幕范围内只能看到一段高电平或低电平,因此会表现为水平直线。
本实验将水平时基设置为:
200 ms/div
这样可以在屏幕中观察到完整的高低电平变化。
十一、实验总结
通过本实验,我完成了从纯 C 软件模拟到 STM32 寄存器级仿真的第一步,主要掌握了:
- STM32F103C8 的 GPIOA 时钟使能方法。
GPIOA->CRL中 MODE 和 CNF 位的配置方法。- 使用 ODR、BSRR 和 BRR 控制 GPIO 输出。
- 使用 SysTick 建立 1 ms 时间基准。
- 使用逻辑探针、电压表和示波器验证 GPIO 输出。
- Proteus 中
Clock Scale对仿真速度的影响。 - 数字节点和模拟节点测量方式的区别。
下一步将在 PA0 至 PA3 连接 4 只 LED,通过位移运算实现流水灯。
GitHub 项目
- 仓库:Embedded-C-Learning-Projects
- 项目目录:
day28
【零基础嵌入式学习日志】Day29:STM32F103C8 四路 LED 流水灯与位运算
前言
在 Day28 中,我完成了 STM32F103C8 单路 GPIO 控制 LED 闪烁,并使用 SysTick 建立毫秒级时间基准。
本次实验在此基础上,将 PA0 至 PA3 配置为通用推挽输出,并分别连接 4 只 LED。程序通过位移运算生成不同的 GPIO 输出状态,使 4 只 LED 按照 PA0、PA1、PA2、PA3 的顺序依次点亮。
本实验重点包括多引脚寄存器配置、GPIO 输出位清除、位移运算以及流水灯序号循环控制。
一、实验目标
本实验主要完成以下内容:
- 配置 PA0 至 PA3 为通用推挽输出。
- 使用 4 个独立限流电阻驱动 4 只 LED。
- 使用位移运算选择当前点亮的 LED。
- 实现 4 路 LED 循环流水效果。
- 使用逻辑探针和示波器验证 GPIO 状态。
- 理解多引脚配置和单比特控制方法。
二、开发环境
操作系统:Windows
单片机:STM32F103C8
开发工具:Keil μVision
仿真软件:Proteus 9 Professional
编程语言:C
编程方式:寄存器级编程
系统时钟:内部 HSI 8 MHz
LED 限流电阻:330 Ω
Proteus 中 STM32F103C8 的关键属性设置如下:
Program File:Keil 生成的 HEX 文件
OSC Frequency:8 MHz
Clock Scale:(Default)
OSC32 Frequency:0
三、硬件连接
4 只 LED 分别连接到 PA0 至 PA3:
PA0 → 330 Ω → LED0 → GND
PA1 → 330 Ω → LED1 → GND
PA2 → 330 Ω → LED2 → GND
PA3 → 330 Ω → LED3 → GND
每一只 LED 必须使用独立限流电阻,不能让 4 只 LED 共用一个电阻。
本实验采用高电平点亮方式:
GPIO 输出 1:对应 LED 点亮
GPIO 输出 0:对应 LED 熄灭
完整仿真电路主要包括:
- STM32F103C8
- PA0 至 PA3 输出支路
- 4 个 330 Ω 限流电阻
- 4 只 LED
- NRST 上拉电阻
- BOOT0 接地
- 电源和地
- 逻辑探针
- 示波器
此处插入图1:STM32F103C8 四路 LED 流水灯 Proteus 仿真电路
四、PA0 至 PA3 寄存器配置
PA0 至 PA3 都位于 GPIOA->CRL 寄存器中,每个引脚占 4 位。
每个引脚均配置为:
CNF = 00:通用推挽输出
MODE = 10:最大输出速度 2 MHz
配置值 = 0x2
PA0 至 PA3 对应位置如下:
PA0:CRL[3:0]
PA1:CRL[7:4]
PA2:CRL[11:8]
PA3:CRL[15:12]
将 4 个配置值组合后得到:
0x00002222
对应代码如下:
GPIOA->CRL &= ~0x0000FFFFUL;
GPIOA->CRL |= 0x00002222UL;
第一行清除 PA0 至 PA3 原来的配置,第二行写入 4 组推挽输出配置。
五、流水灯位运算原理
流水灯的核心表达式为:
1UL << led_index
其中,led_index 的取值范围为 0 至 3。
led_index |
运算结果 | 二进制 | 点亮引脚 |
|---|---|---|---|
| 0 | 1 << 0 |
0001 |
PA0 |
| 1 | 1 << 1 |
0010 |
PA1 |
| 2 | 1 << 2 |
0100 |
PA2 |
| 3 | 1 << 3 |
1000 |
PA3 |
例如:
1UL << 2
相当于将二进制 0001 左移 2 位,结果为:
0100
因此,PA2 输出高电平。
使用位移运算后,不需要分别编写 4 组固定的 GPIO 控制代码,只需修改 led_index 的数值,就可以选择需要点亮的引脚。
六、清除 PA0 至 PA3 输出
在点亮下一只 LED 之前,需要先关闭当前所有 LED:
GPIOA->ODR &= ~0x0000000FUL;
0x0F 对应最低 4 位:
0000 1111
取反后,最低 4 位变为 0,再与 ODR 进行按位与运算,就可以将 PA0 至 PA3 清零,同时保留 GPIOA 其他输出位的状态。
随后使用:
GPIOA->ODR |= (1UL << led_index);
将当前需要点亮的引脚置 1。
两条语句组合后的作用为:
先关闭 PA0 至 PA3
再点亮 led_index 对应的一个引脚
七、完整程序
#include "stm32f10x.h"
/*
* 配置 SysTick,每 1 ms 产生一次 COUNTFLAG。
*/
static void systick_init_1ms(void)
{
SystemCoreClockUpdate();
SysTick->LOAD = (SystemCoreClock / 1000U) - 1U;
SysTick->VAL = 0U;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_ENABLE_Msk;
}
/*
* 阻塞式毫秒延时。
*/
static void delay_ms(uint32_t milliseconds)
{
while (milliseconds > 0U)
{
while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0U)
{
/* 等待 1 ms */
}
milliseconds--;
}
}
int main(void)
{
uint32_t led_index = 0U;
/*
* 开启 GPIOA 时钟。
*/
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
/*
* PA0 至 PA3 配置为 2 MHz 通用推挽输出。
*/
GPIOA->CRL &= ~0x0000FFFFUL;
GPIOA->CRL |= 0x00002222UL;
/*
* 初始关闭 4 只 LED。
*/
GPIOA->BRR = GPIO_BRR_BR0 |
GPIO_BRR_BR1 |
GPIO_BRR_BR2 |
GPIO_BRR_BR3;
systick_init_1ms();
while (1)
{
/*
* 清除 PA0 至 PA3。
*/
GPIOA->ODR &= ~0x0000000FUL;
/*
* 点亮当前 LED。
*/
GPIOA->ODR |= (1UL << led_index);
/*
* 当前 LED 保持点亮 300 ms。
*/
delay_ms(300U);
/*
* 切换到下一只 LED。
*/
led_index++;
if (led_index >= 4U)
{
led_index = 0U;
}
}
}
八、程序执行过程
程序执行顺序如下:
led_index = 0
PA0 点亮
延时 300 ms
↓
led_index = 1
PA1 点亮
延时 300 ms
↓
led_index = 2
PA2 点亮
延时 300 ms
↓
led_index = 3
PA3 点亮
延时 300 ms
↓
led_index 重新变为 0
因此,流水灯顺序为:
PA0 → PA1 → PA2 → PA3 → PA0
完整循环周期约为:
4 × 300 ms = 1200 ms
九、仿真结果
运行 Proteus 后,可以看到 4 只 LED 依次点亮,每次仅有一只 LED 对应的 GPIO 输出高电平。
此处插入图2:四路 LED 流水灯仿真运行状态
从仿真结果可以观察到:
PA0 点亮后,PA1、PA2 和 PA3 保持低电平
PA1 点亮后,PA0、PA2 和 PA3 保持低电平
PA2 点亮后,PA0、PA1 和 PA3 保持低电平
PA3 点亮后,PA0、PA1 和 PA2 保持低电平
这说明程序每次只设置一个输出位,流水灯状态控制正确。
十、示波器波形分析
将示波器 A 通道连接 PA0。
由于完整循环中 PA0 只点亮一次,因此 PA0 波形为:
高电平约 300 ms
低电平约 900 ms
完整周期约为:
1200 ms
PA0 的理论占空比为:
300 ÷ 1200 = 25%
因此,在示波器中可以观察到一段较短的高电平和一段较长的低电平。
如果分别将示波器的 A、B、C、D 通道连接 PA0、PA1、PA2 和 PA3,还可以观察到 4 路方波依次错开 300 ms。
十一、调试过程中需要注意的问题
1. 每只 LED 应使用独立限流电阻
不能让 4 只 LED 共用一个限流电阻,否则不同 LED 的电流会相互影响,实际硬件中也可能出现亮度不一致的问题。
正确连接方式为:
一个 GPIO
一个电阻
一只 LED
2. LED 方向必须一致
本实验采用:
GPIO → 电阻 → LED 阳极
LED 阴极 → GND
因此高电平点亮。
如果某只 LED 的方向与其他 LED 相反,它将无法正常点亮。
3. Proteus 时钟缩放保持默认
STM32F103C8 的属性应设置为:
OSC Frequency = 8 MHz
Clock Scale = (Default)
如果将 Clock Scale 设置为 8 Times,流水灯切换速度会明显快于程序中设定的 300 ms。
4. 原理图中的元件必须真正连接
在 Proteus 中,元件放在导线上并不一定代表已经连接。
如果元件端点仍显示悬空的红色短线,说明导线没有吸附到引脚端点,需要重新连接。
十二、程序改进方向
1. 反向流水灯
实现以下顺序:
PA3 → PA2 → PA1 → PA0
可以将点亮语句修改为:
GPIOA->ODR |= (1UL << (3U - led_index));
2. 往返流水灯
实现以下顺序:
PA0 → PA1 → PA2 → PA3 → PA2 → PA1 → PA0
可以增加一个方向变量:
int32_t direction = 1;
当序号到达 0 或 3 时改变方向。
3. 按键控制流水灯
下一步可以增加独立按键,实现:
按键 1:暂停或继续
按键 2:切换流水方向
按键 3:调整流水速度
4. 非阻塞流水灯
当前 delay_ms() 属于阻塞式延时。在延时期间,CPU 无法处理按键和其他任务。
后续可以使用以下结构实现非阻塞流水灯:
SysTick 中断
系统毫秒计数
主循环时间判断
状态机
这样程序可以同时处理按键、串口和其他外设。
十三、实验总结
本实验完成了 4 路 LED 流水灯,主要掌握了:
- PA0 至 PA3 多引脚寄存器配置。
GPIOA->CRL中多个 4 位配置字段的组合。- 使用按位与运算清除指定 GPIO。
- 使用左移运算产生单比特输出。
- 使用序号变量控制流水灯状态。
- 使用 SysTick 阻塞延时控制流水速度。
- 使用示波器分析流水灯输出周期和占空比。
本次实验将前面学习过的 GPIO 位操作、系统 Tick 和状态变量真正应用到了 STM32 与 Proteus 仿真中。
下一步将增加独立按键,实现流水灯暂停、继续和方向切换。
GitHub 项目
- 仓库:Embedded-C-Learning-Projects
- 项目目录:
day29
更多推荐


所有评论(0)