呼吸灯案例
概述
最小化的呼吸灯案例:只用 CMSIS 寄存器直接操作 GPIO,配合软件延时逐级调整 PWM 占空比,让 PA0 上的 LED 呈现「渐亮 → 渐灭」的呼吸效果,不依赖 HAL 库,也不用硬件定时器。
- 引脚:PA0 配置为推挽输出,高电平点亮 LED(外接 LED + 限流电阻到 GND)。
- 时钟:复位后默认 HSI 8 MHz 作 SYSCLK,不配 PLL,跑起来最简单。
- 原理:软件 PWM —— 一个周期里高电平占
on拍、低电平占period - on拍,缓慢改变on就能得到连续变化的亮度。
引脚分析:为什么是 PA0
这个案例最小只用到三类信号,其余引脚是实物接线时才需要的:
- 电源:
VDD/VSS供电,必接,但代码里不用写。 - 调试:
PA13(SWDIO)、PA14(SWCLK)默认被调试占用,本案例不碰,留着烧录调试。 - 输出:选
PA0作推挽输出,高电平点亮 LED。
代码里那几条寄存器操作,都是沿「PA0 → GPIOA → 总线」这条链推出来的:
| 要做什么 | 依据 | 对应代码 |
|---|---|---|
| 让 GPIOA 可被访问 | GPIOA 挂在 APB2 总线上,由 RCC 时钟门控 | `RCC->APB2ENR |
| 把 PA0 配成推挽输出 | CRL 低 4 位控制 PA0:CNF=00 推挽、MODE=10 2 MHz 输出 | `GPIOA->CRL = (GPIOA->CRL & ~0xFu) |
| 输出高 / 低电平 | BSRR / BRR 是原子写,比「读改写 ODR」更不易被中断打乱 | GPIOA->BSRR = (1u << 0) / GPIOA->BRR = (1u << 0) |
剩下「怎么变成呼吸灯」是纯软件的事:不靠硬件定时器,用一个空转延时 delay_cycles() 控制高 / 低电平的拍数,逐级改占空比即可,见下方代码。
上面用到的寄存器定义见《STM32F10xxx 参考手册(中文版)》RM0008:RCC_APB2ENR 在 6.3.7,GPIOx_CRL / GPIOx_BSRR / GPIOx_BRR 分别在 8.2.1 / 8.2.5 / 8.2.6,原子位设置/清除见 8.1.2。
GPIO 输出方式:推挽与开漏
GPIOx_CRL / CRH 里每个引脚占 4 位,拆成两段:MODE[1:0] 定方向与速度,CNF[1:0] 定电路形态。只要 MODE 非 00,引脚就是输出,此时 CNF 有 4 种搭配(参考手册 8.1.8、表 17):
| CNF | 输出方式 | 说明 |
|---|---|---|
00 | 通用推挽输出 | 既能主动拉高、也能主动拉低,接 LED(阳极接 IO)最常用 |
01 | 通用开漏输出 | 只能主动拉低,拉高靠外部上拉;多设备线与、接 5 V 器件常用 |
10 | 复用推挽输出 | 输出交给外设(USART_TX、PWM 等),驱动方式同推挽 |
11 | 复用开漏输出 | 输出交给外设,驱动方式同开漏(如 I2C 的 SCL / SDA) |
「通用」和「复用」的区别只在于电平由谁控制:通用输出由你写 ODR / BSRR / BRR 决定;复用输出则把引脚交给片上外设(定时器 PWM、串口 TX 等)驱动。推挽和开漏的区别在输出级的 MOS 管:
- 推挽:上 P-MOS、下 N-MOS 两个管子。写
1时 P-MOS 导通拉到VDD,写0时 N-MOS 导通拉到VSS,高低电平都能主动驱动,带载能力强。 - 开漏:只有下 N-MOS 一个管子。写
0时拉到地,写1时 N-MOS 关断、引脚变高阻,必须靠外部上拉电阻把电平抬起来;好处是多个开漏输出可以直接「线与」,也便于和 5 V 器件做电平转换。
呼吸灯这里 LED 阳极接 PA0、阴极经限流电阻到 GND(高电平点亮),需要 PA0 能主动输出 1 点亮,所以选通用推挽输出(CNF=00)。对应代码里的 GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x2u:低 4 位写成 0b0010,即 CNF=00、MODE=10(最大 2 MHz)。MODE 只是限制最大翻转速率,呼吸灯这种低速场景 2 MHz 足够。
源代码
/**
* @file main.c
* @brief 应用入口(本工程唯一自写业务代码)。
*
* CMSIS 寄存器操作 PA0:软件 PWM 呼吸灯。
* 时钟:默认复位后 HSI(8MHz),不配 PLL。
*/
#include "stm32f1xx.h" /* 引入 STM32F1 CMSIS 头文件,提供 RCC/GPIO 等寄存器定义 */
/**
* 粗延时:空转指定拍数,时长随内核时钟变化,仅用于演示。
* @param count 循环次数;声明为 volatile,防止编译器把空循环优化掉。
*/
static void delay_cycles(volatile uint32_t count)
{
while (count--) { /* 每轮减一,直到减到 0 后退出 */
__NOP(); /* 插入一条空操作指令,消耗少量时钟周期 */
}
}
/**
* 软件 PWM 一个周期:先高电平 on 拍,再低电平 (period-on) 拍。
* @param on 高电平持续拍数(占空比分子)
* @param period 一个 PWM 周期总拍数(占空比分母)
*/
static void pwm_cycle(uint32_t on, uint32_t period)
{
GPIOA->BSRR = (1u << 0); /* 写 BSRR 低 16 位:原子置位 PA0=1,点亮 LED */
delay_cycles(on); /* 保持高电平 on 个空转拍 */
GPIOA->BRR = (1u << 0); /* 写 BRR:原子清零 PA0=0,熄灭 LED */
delay_cycles(period - on); /* 保持低电平,凑满一个 period 周期 */
}
/**
* 配置 PA0 为推挽输出,并以软件 PWM 做呼吸灯(高电平点亮)。
*/
int main(void)
{
/* 1. 使能 GPIOA 外设时钟:未开时钟则读写 GPIOA 寄存器无效 */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; /* APB2ENR 位置位 IOPAEN,打开 GPIOA 时钟 */
/* 2. 配置 PA0 模式:CRL 低 4 位控制 PA0;CNF=00 推挽,MODE=10 输出 2MHz */
GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x2u; /* 清掉旧 CNF/MODE,再写入 0b0010 */
/* 3. 呼吸参数:period 决定亮度阶数,hold 决定每个亮度停留多久 */
const uint32_t period = 200u; /* PWM 周期拍数;越大亮度分级越细,循环越慢 */
const uint32_t hold = 15u; /* 每个占空比重复次数;越大呼吸越慢、越平滑 */
while (1) { /* 主循环永不退出,持续呼吸 */
/* 3.1 渐亮:占空比 d 从 0 增到 period */
for (uint32_t d = 0; d <= period; d++) { /* d 即当前高电平拍数 */
for (uint32_t i = 0; i < hold; i++) { /* 同一亮度多打几轮 PWM,让眼睛能跟上 */
pwm_cycle(d, period); /* 按当前占空比输出一个 PWM 周期 */
}
}
/* 3.2 渐灭:占空比 d 从 period 减到 1(回到 0 由下一轮渐亮衔接) */
for (uint32_t d = period; d > 0; d--) { /* 逐级降低高电平时间 */
for (uint32_t i = 0; i < hold; i++) { /* 同样在每个亮度上停留 hold 轮 */
pwm_cycle(d, period); /* 输出更暗一点的 PWM 周期 */
}
}
}
}