资讯详情

资讯详情

STM32 GPIO控制LED从闪烁到PWM呼吸灯:时钟、寄存器与定时器实战

简介STM32的LED灯闪烁工程代码面向嵌入式初学者与高校电子专业学生通过GPIO输出与定时器中断实现周期性亮灭是入门微控制器开发最经典的实验之一。资源为ZIP压缩包共176个文件含37个头文件、31个C源文件以及工程配置、分散加载、映像、列表等编译调试产物源码与构建链完整整体大小约1.29MB。工程基于标准外设库和HAL库已将LED引脚设为推挽输出并配置定时器预分频与自动重载值定时器溢出后在中断服务函数中翻转LED形成闪烁效果。已有655人学习下载适合自学或作为单片机课程实验参考。通过研读源码和工程结构能掌握时钟配置、GPIO模式设置、定时器中断编程以及HAL库函数调用修改定时器参数可调整闪烁频率加深对中断触发机制和硬件资源管理的理解为后续学习PWM、外部中断等应用奠定基础。1. 一块板子点亮 LED为什么值得专门写一篇先把结论放在前面STM32的灯闪烁看着是入门第一课实际上却是很多人第一次集中接触寄存器、时钟、上拉下拉和复用功能这几个概念的地方。我见过不少同行在这个例程上翻车——LED 完全不亮、常亮不闪、下载后没反应、频率乱跳问题往往不是代码写错而是 GPIO 的时钟没开、模式没选对或者烧录之后板子没有正确复位。这篇笔记基于一份我在几个实验板上反复调过的stm32 灯工程从硬件连接、GPIO 配置、延时实现讲到按键中断最后给出一版可以直接抄的 PWM 呼吸灯代码。新手可以照着完整流程复现熟手可以直接看第 5 章的排查记录和第 6 章的定时器参数计算。2. GPIO 控制 LED 闪起来时钟、模式与电平翻转2.1 从原理图看 LED 引脚限流电阻与灌电流/拉电流任何 LED 点亮的前提是电流方向和大小都正确。常见实验板上通常有两种接法一是 LED 正极接 3.3V负极通过限流电阻接到 GPIO此时 GPIO 输出低电平点亮 LED电流从电源流入引脚称为灌电流二是 LED 正极接 GPIO负极通过电阻接地GPIO 输出高电平点亮电流从引脚流出称为拉电流。两种接法本身都能正常点亮但要注意 STM32 的 GPIO 灌电流能力通常比拉电流强一些而且灌电流方式下引脚默认输出高电平LED 是熄灭的更符合“上电后不亮”的直觉所以大多数实验板都采用低电平点亮。限流电阻的取值一般按 LED 正向压降和额定电流来算。以常见的红色 LED 为例正向压降大约 1.8V 到 2.0V额定工作电流 5mA 到 10mA电源 3.3V电阻 R (3.3 - 2.0) / 0.005 260Ω 左右。很多实验板为了省事直接放一个 1kΩ 电阻电流被限制到 1mA 左右灯也能点亮只是亮度偏低这说明电阻取值有比较大的宽容度不必太纠结精确值。你真正需要关心的是 LED 接到的是哪一个引脚以及这个引脚在芯片上是否同时承担了其他功能。确认接线之后下一步是确定你用哪一个引脚。以常见的某款 F1 系列芯片为例板载 LED 经常接到 PC13、PA5 或 PB1这些引脚在标准外设库和 HAL 库里的写法都不一样。我建议拿到实验板先查原理图把引脚编号写死在代码注释里后面排查起来会省很多时间。下面这份配置代码用的是 PA5 作为示例如果你的板子不是这个引脚替换成实际引脚并重新查一下数据手册的引脚定义即可。还有一点值得提醒LED 点亮并不需要高速翻转的时候很多人会忽略 GPIO 输出速度这个参数。对于普通的灯闪烁2MHz 和 50MHz 都能正常工作但这会影响信号边沿的陡峭程度和功耗。后面做 PWM 呼吸灯时输出速度太低会让波形边沿变缓影响占空比精度所以这里直接按 50MHz 配置最稳妥。2.2 不使能 GPIO 时钟写再多寄存器都是白写STM32 的所有外设都挂在总线时钟上GPIO 也不例外。F1 系列里 GPIOA 挂在 APB2 总线上控制它的时钟使能位在 RCC 寄存器组的 APB2ENR 里。很多新手第一次写寄存器代码时只配置了 CRL/CRH 和 BSRR/ODR忘记打开时钟结果是程序怎么改灯都不亮因为整个 GPIO 外设根本没有时钟输入寄存器写不进去而且这种错误通常不会有任何编译警告。正确的顺序是先开时钟再配置模式最后操作电平。开 PA 口时钟的代码是RCC-APB2ENR | (1 2);。这一位的具体位置可以在参考手册的 RCC_APB2ENR 寄存器描述里查到不同系列芯片可能不一样不要凭记忆硬背。下面是一段完整的 GPIO 初始化代码// 以 PA5 控制 LED 为例低电平点亮 void LED_Init(void) { // 1. 使能 GPIOA 时钟APB2ENR 寄存器第 2 位对应 GPIOA RCC-APB2ENR | (1 2); // 2. 配置 PA5 为推挽输出最大速度 50MHz // GPIOA-CRL 控制 PA0~PA7每个引脚占 4 位 // MODE5 11 - 输出模式 50MHzCNF5 00 - 通用推挽输出 GPIOA-CRL ~(0xF (4 * 5)); // 先把 PA5 的 4 位配置清空 GPIOA-CRL | (0x3 (4 * 5)); // 写入输出模式 50MHz推挽 }代码逻辑分三步先通过 RCC 把 GPIOA 的时钟打开再操作 CRL 寄存器把 PA5 对应的 4 位配置清空最后写入0x3。这里0x3是 4 位配置的组合结果其中低两位 MODE 写11表示输出模式 50MHz高两位 CNF 写00表示通用推挽输出。注意写入的是0x3而不是0x1如果只写0x1输出频率只有 2MHz虽然点亮普通 LED 也没有问题但后面接 PWM 或串口时会出怪问题。配置完模式之后还需要控制引脚电平。点亮 LED 的操作可以写在主函数里也可以单独封装一个函数。如果你使用的是标准外设库初始化可以简化为GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP;这种方式寄存器操作的底层逻辑是一样的。无论用哪种方式核心顺序都是时钟先于配置。2.3 推挽输出与开漏输出什么时候不能互换推挽输出是 LED 闪烁最常用的模式。它的特点是引脚内部有上下两个驱动管输出高电平时由上面的管子把电平拉到 3.3V输出低电平时由下面的管子把电平拉到 0V逻辑电平明确驱动能力也强。开漏输出则只有下拉管起作用输出高电平时引脚实际处于高阻态必须依靠外部上拉电阻才能把电平拉高通常用在 I2C、电平转换这类需要“线与”逻辑的场合。如果你把 LED 引脚错误地配置成开漏输出且外部没有上拉电阻灯会表现为“输出低电平时亮、输出高电平时灭但亮度明显变暗甚至完全不亮”因为高电平没有被真正驱动。这是 GPIO 模式选错时最典型的故障现象。做 LED 闪烁直接选推挽输出即可只有当这个引脚同时还复用给其他外设时才需要按复用功能表重新配置成复用推挽这件事在第 6 章做呼吸灯时会再次遇到。电源电压也是需要确认的一个参数。如果 LED 和 GPIO 不在同一个电源域比如外围 LED 供电是 5V而 GPIO 是 3.3V直接推挽输出高电平只有 3.3V灯可能偏暗或不亮这时必须加电平转换电路或者用三极管驱动。很多实验板没有注意这一点直接把 LED 接到了 5V 电源上再用 3.3V 的 GPIO 去拉低这种方式虽然勉强可以工作但长期运行的可靠性要打问号。再补充一点电平翻转操作的区别。主循环里让 PA5 在 0 和 1 之间来回切换可以用读改写方式操作 ODR也可以用 BSRR 寄存器单独置位或复位。两种做法在结果上等价但 BSRR 对特定引脚写 1 不会影响其他引脚的状态而 ODR 的读改写操作在中断和主循环同时访问时可能出现丢失更新的问题。LED 闪烁虽然不至于因为这个出大问题但养成用 BSRR 的习惯没有坏处。3. 闪烁节奏靠延时软件延时与 SysTick 定时器的差别3.1 软件延时为什么能闪但不准最简单的闪烁逻辑是主循环里点亮、延时、熄灭、再延时。这个“延时”很多人直接用空循环实现代码如下void Delay_Soft(volatile uint32_t count) { while (count--) { } }这个函数的参数 count 是循环次数循环体为空编译器在不开优化的情况下会老老实实执行 count 次。问题是同样的 count 值在 -O0 和 -O2 优化级别下实际延时差距可能拉大到几倍甚至十几倍因为优化后编译器可能直接把空循环删掉。volatile 关键字能避免循环被完全优化掉但依然不能保证精确时间因为指令周期受编译器版本、芯片流水线和中断打断的影响很大。所以我的经验是软件延时只用来验证逻辑不适合做真正的时间基准。LED 闪烁教学里用它演示没问题但一旦你开始做按键消抖、串口超时、PWM 占空比变化软件延时的误差会变成玄学问题。不同编译器生成的目标代码长度不同同样的Delay_Soft(1000000)在 Keil 和 GCC 下跑出来的时间可能差 30% 以上而且这段代码换一块主频不同的芯片闪烁节奏就完全变了。软件延时的参数其实很难按“毫秒”直接换算。假设系统主频 72MHz一个简单的空循环大约需要 3 到 4 个指令周期循环 100 万次大约耗时几十毫秒但这个估算非常粗糙。更麻烦的是中断影响SysTick、串口中断或者外部中断一旦在延时过程中触发CPU 会跳去执行中断服务函数空循环的执行时间被拉长定时就不准了。如果你非要用软件延时至少要把延时函数声明成volatile参数并且关掉编译器优化来测试但我不建议你在真正的项目里这么干。3.2 SysTick 定时器延时把节奏交给硬件SysTick 是 Cortex-M 内核自带的 24 位递减计数器不需要额外外设时钟配置初始化简单几乎是裸机延时最稳的方案。核心思路是把重装载值设置为 SystemCoreClock / 1000这样计数器每 1ms 从重装载值减到 0 一次同时触发一次 SysTick 中断。延时函数里用一个全局变量记录中断次数主循环等待这个计数变量累加到目标值再继续执行。static volatile uint32_t g_tick 0; void SysTick_Handler(void) { g_tick; } void Delay_Ms(uint32_t ms) { uint32_t start g_tick; while (g_tick - start ms) { // 等待 SysTick 中断累计到 ms 毫秒 } }初始化部分用内核函数SysTick_Config(SystemCoreClock / 1000)它会把重装载值设为 7200072MHz 系统时钟下并开启 SysTick 中断。这里有几个参数值得注意重装载值必须是 1 到 0xFFFFFF 之间的整数系统时钟越高单位时间对应的计数值越大延时分辨率越细中断优先级默认较高如果你的主程序里有其他实时性要求更高的中断建议在 NVIC 里把 SysTick 优先级调低避免它频繁打断关键时序。上面延时函数里的g_tick - start是一种无符号差值写法即使 g_tick 回绕到 0 也不会出错比g_tick start ms这种写法更安全。这个细节在长时间运行的设备里很重要因为 32 位无符号计数在累计到 0xFFFFFFFF 后下一次递增就会回绕到 0如果用大小比较判断就会直接卡死。用差值方式判断则完全不受回绕影响代码也不复杂建议直接照抄。SysTick 延时的误差主要来自两点一是SysTick_Config的参数取整误差比如系统时钟不是整除 1000 时重装载值会向下取整单次 1ms 就有少量偏差二是 SysTick 中断本身的进入退出开销中断响应需要几个指令周期。对 LED 闪烁来说这点误差完全无所谓但如果做时间敏感的逻辑比如捕获脉宽就不能用这种简单延时要改用定时器输入捕获。3.3 电平翻转的三种写法与执行效率闪烁过程中的电平翻转有三种常见写法写 BSRR 寄存器置位或复位、写 ODR 寄存器异或、写 BRR 寄存器复位。推荐优先用 BSRR 或者 ODR 异或因为最简洁。BSRR 寄存器的高 16 位是复位位、低 16 位是置位位向对应位写 1 即生效不会影响其他引脚的电平状态GPIOA-BSRR (1 5); // PA5 输出高电平 GPIOA-BSRR (1 (5 16)); // PA5 输出低电平ODR 异或的写法是GPIOA-ODR ^ (1 5);它把 PA5 的电平直接翻转只占一行代码视觉效果最简洁。但 ODR 的读出再写回过程不是原子操作如果中断和主循环里同时操作同一个引脚可能出现状态不一致比如两次翻转之间插入了一次中断翻转最终电平方向和预期相反。LED 闪烁这门课里两种写法都能用我更建议在中断服务函数里只用 BSRR。还有一个执行效率的问题BSRR 和 BRR 是只写寄存器内核写入后不需要回读确认而 ODR 的“读-改-写”需要额外执行几条指令。如果主循环频率很高比如做上千赫兹的方波输出ODR 异或的额外开销会让波形频率偏低。解决方式有两个一是改用 BSRR 双写二是直接操作 ODR 的低位字节但这只对同一端口所有引脚一起操作时才有效。LED 闪烁不需要处理这种高速场景理解原理即可。4. 从闪到交互按键控制 LED 闪烁与外部中断4.1 轮询方式读按键消抖比读电平更关键很多人在 LED 闪烁例程稳定之后下一步就想加一个按键来控制灯的亮灭。最简单的轮询逻辑是主循环里读 GPIO 输入电平按下时翻转 LED 状态没按时保持。这里的重点其实不在“读”这个动作而在消抖。机械按键在按下和松开的瞬间金属簧片会发生多次弹跳持续几毫秒到十几毫秒期间电平在高低之间来回跳变如果直接读电平判断一次按键可能会被识别成多次。先看按键初始化和扫描函数的标准写法#define KEY_PIN GPIO_Pin_0 // 按键接 PA0 #define LED_PIN GPIO_Pin_5 // LED 接 PA5 void KEY_Init(void) { RCC-APB2ENR | (1 2); // 使能 GPIOA 时钟 GPIOA-CRL ~(0xF (4 * 0)); // PA0 清空配置 GPIOA-CRL | (0x8 (4 * 0)); // CNF10 上拉/下拉输入MODE00 GPIOA-ODR | (1 0); // ODR 写 1选择上拉默认高电平 } uint8_t KEY_Scan(void) { if (GPIOA-IDR (1 0)) { return 0; // 读到高电平没有按下 } Delay_Ms(10); // 第一次读到低延时消抖 if (GPIOA-IDR (1 0)) { return 0; // 抖动结束确认没有按下 } return 1; // 第二次仍为低确认按下 }这个代码里 GPIO 输入配置的要点是CNF10 表示上拉/下拉输入具体是上拉还是下拉由 ODR 对应位的值决定写入 1 表示上拉。按键另一端接地、按下时引脚变为低电平所以配上拉电阻保证没按下时读到高电平。消抖逻辑很简单第一次读到低后等待 10ms再读一次还是低才认为确实是按键按下如果第二次读到高说明第一次是机械弹跳不算有效按下。“参数怎么改”在按键这里非常具体消抖时间 10ms 是机械按键常见的经验值如果你用的是自锁开关或者质量一般的轻触开关建议调到 20ms 到 30ms但如果按键用于快速连击消抖时间太长会导致响应变慢。另外延时消抖会阻塞主循环如果主循环里还有其他需要实时处理的任务比如 LED 闪烁本身也要靠延时驱动阻塞式消抖会积累成明显的响应延迟这时应该改用定时器采样法。4.2 外部中断方式EXTI 配置与中断服务函数轮询方式实现简单但有一个明显的短板如果主循环卡在某个耗时操作里比如软件延时闪烁的正循环按键按下去可能要等好几毫秒甚至十几毫秒才能被响应。更合理的方式是用外部中断让按键电平变化直接触发中断主循环不需要轮询。STM32 的 EXTI 需要把 GPIO 配置成输入模式再通过 AFIO 把引脚连接到 EXTI 线最后配置 NVIC 优先级。以 PA0 为例void EXTI0_Init(void) { RCC-APB2ENR | (1 0); // 使能 AFIO 时钟外部中断映射需要 GPIOA-CRL ~(0xF (4 * 0)); // PA0 输入模式 GPIOA-CRL | (0x8 (4 * 0)); // CNF10 上拉/下拉输入 GPIOA-ODR | (1 0); // 上拉默认高电平 AFIO-EXTICR[0] ~(0xF 0); // PA0 连接到 EXTI0 EXTI-IMR | (1 0); // 打开 EXTI0 中断屏蔽 EXTI-FTSR | (1 0); // 下降沿触发按下时触发 NVIC_EnableIRQ(EXTI0_IRQn); } void EXTI0_IRQHandler(void) { if (EXTI-PR (1 0)) { EXTI-PR (1 0); // 写 1 清除中断标志不能省略 GPIOA-ODR ^ (1 5); // 翻转 LED } }这里有三个关键点。第一EXTI-PR 的中断标志必须由软件写 1 清除如果不清理中断标志一直保持置位中断会反复进入看起来就像 LED 在乱闪。第二AFIO 的 EXTICR 寄存器决定哪个引脚连接到哪条 EXTI 线PA0 要选 EXTI0如果换成 PB0则要配置到 EXTICR 寄存器的对应位组并确认该引脚是否支持这条 EXTI 线。第三中断服务函数里尽量不要做耗时操作直接翻转 LED 没有问题但如果在中断里调用延时函数会拖住整个系统的中断响应下面单独展开这个话题。中断触发沿的选择也需要按按键接法调整。按键按下接地时用下降沿触发如果按键接法相反按下时从低变高就要改用上升沿。有些板子为了省一个上拉电阻把按键接在电源和引脚之间这种情况下继续用下降沿触发按键释放时才触发响应方向会反过来按键手感明显不对。遇到这种情况不要急着改代码先看原理图的按键接法。4.3 中断里到底能不能用延时一个容易翻车的场景“中断里不要用延时”是嵌入式开发里的老生常谈但很多人不理解以为只要延时短就没问题。这里有一个优先级陷阱SysTick 延时本身依赖 SysTick 中断如果当前正在执行的中断服务函数优先级高于 SysTick 中断那么 SysTick 中断会被阻塞g_tick 变量不增长Delay_Ms会在 while 循环里一直出不来。这就是中断里调用延时的第一个直接后果。第二个坑是中断里的按键消抖。假设你在 EXTI 中断服务函数里调用Delay_Ms(10)做消抖这 10ms 内 MCU 无法响应其他中断如果系统里还有串口接收、定时器采样或者另一个按键中断这 10ms 就会造成数据丢失或者另一个按键被吞掉。所以我在裸机演示程序里只用轮询加延时消抖真正的产品代码会改用定时器周期性扫描按键或者用状态机处理消抖逻辑避免在中断里等待。如果你想在中断方式下也做消抖有一个常用变通方案中断服务函数里只清标志、记录一个按键事件标志并翻转 LED消抖交给主循环里每 10ms 扫描一次的逻辑。也就是说按下瞬间触发中断先翻转再在后续扫描里确认状态。对于 LED 闪烁这种教学场景更简单的做法就是回到轮询方式把消抖延时放在主循环里中断方式留给真正需要唤醒 CPU 的低功耗场景。5. LED 不亮、常亮、频率不对高频问题排查记录5.1 下载程序后 LED 完全没反应现象程序编译下载都没有报错但板子上的 LED 没有任何变化用万用表量引脚电压也是 0V 或者悬空状态。原因最常见的是 GPIO 时钟没有使能。很多人写了GPIOA-CRL的配置却没写RCC-APB2ENR | (1 2);。在多数 STM32 系列芯片上外设时钟默认是关闭的直接操作寄存器内容不会生效而且这种问题在编译阶段不会暴露。另一种常见原因是引脚选错LED 实际接在 PC13程序却配置了 PA5代码逻辑完全正确但灯就是不动。解决先查原理图确认 LED 引脚编号再检查初始化代码里时钟使能是否在寄存器配置之前执行。顺序应该是时钟优先。如果你用的是代码模板工程有的模板已经统一开启了所有 GPIO 时钟这时反而要确认你的引脚属于哪个端口以及模板里是否恰好把你要用的引脚配置成别的复用功能。用调试器在初始化之后加断点读一下 GPIO 的 CRL 寄存器实际值能快速看出配置是否被后面的代码覆盖。5.2 LED 一直亮从不熄灭现象上电后 LED 就亮着程序里明明有翻转代码但灯的状态始终不变。原因翻转代码执行了但被复位拉回初始状态或者 GPIO 模式配置错误。更常见的是主循环里的翻转指令根本没有被执行因为前面卡在了一个死循环里。还有一种容易被忽略的情况是误用了开漏输出配置外部恰好有上拉电阻低电平能正常拉亮高电平靠外部上拉也能维持一定亮度翻转的效果自然不明显。解决先用调试器加断点看主循环是否真的跑到了翻转语句单步执行观察 ODR 寄存器对应位是否在变化。如果 ODR 在变但引脚电压不变说明模式配置不对把 CNF 改成 00 推挽输出。如果 ODR 根本不变往前查找是不是有一个while(1);或者某个循环条件永远为真挡在前面。再配合万用表量引脚对地电压翻转正常时应该在 0V 和 3.3V 之间跳变。5.3 闪烁频率和预期差很多现象预设 500ms 翻转一次实际看起来像 200ms 或者 2s 一次拿示波器量发现偏差接近一倍。原因延时函数的时间基准不对。软件延时受编译器优化等级影响大这个前面已经分析过。SysTick 延时的问题多半出在系统时钟宏定义上工程里声明系统时钟是 72MHz但实际时钟树因为外部晶振或者 PLL 配置不同只跑了 64MHz重装载值按 72MHz 计算实际每次中断间隔就比 1ms 短整体延时被压缩闪烁变快。解决先用RCC_GetClocksFreq()这类接口读出系统时钟实际值确认与SysTick_Config的参数一致。调整时先写一个 1000ms 延时用手机秒表观察 LED 一个完整周期是不是大约 2 秒亮 1 秒灭 1 秒用这种粗糙方法先校准数量级再用示波器细调。另外检查是否用了不同型号的芯片引脚兼容的工程模板模板里的时钟初始化文件可能来自另一个主频的芯片这种情况下直接重配整个时钟树更稳妥。5.4 按键按下 LED 没反应或误触发现象接上按键后按下去灯不翻转或者手还没碰到灯就开始乱闪。前者毫无响应后者表现为极其灵敏但不稳定。原因按键引脚没有配置上拉或下拉悬空时电平随机漂移消抖时间过短外部中断没有正确配置触发沿产生的中断事件和实际按键动作不匹配。解决读 GPIO 输入前先确认方向寄存器配置成输入模式并且 ODR 对应位写 1 选择上拉。如果按键接法是按下接地配置上拉后没按下时应读到高电平。中断触发不稳定的情况检查 EXTI-PR 有没有在中断处理里清理标志再确认 FTSR 和 RTSR 配置的是不是正确沿。针对干扰造成的误触发可以在引脚上并联一个 100nF 电容用硬件去掉高频毛刺同时把消抖时间从 10ms 加到 30ms 重新验证。5.5 程序进入 HardFault 或者频繁复位现象下载后板子反复重启调试器停在 HardFault_Handler 里LED 闪烁毫无规律甚至干脆不闪。原因最常见的是中断服务函数里面访问了没有使能的寄存器或者配置外设时引脚模式冲突。还有一个隐蔽原因是把多个 GPIO 引脚映射到同一条 EXTI 线比如 PA0 和 PB0 同时配置到 EXTI0中断标志无法区分来自哪个引脚内部状态乱掉最终触发总线错误进入异常。数组越界、栈溢出这类常规问题也会在复杂一点的工程里出现。解决先断点停在 HardFault_Handler查看调用栈回溯定位是哪个函数触发了异常。常规做法是在 HardFault_Handler 里加一个while(1);再用调试器读 PC 指针和 LR 寄存器对照反汇编找到出错的代码行。平时养成习惯外部中断服务函数里只做标志置位和最小必要操作复杂处理一律放主循环。检查中断向量表里的函数名是否和启动文件一致名字拼错时中断不会进入而是跳到默认 handler表现同样是各种奇怪行为。6. 进阶玩法用定时器输出 PWM把闪烁变成呼吸灯6.1 PWM 原理与定时器参数计算闪烁的本质是让 LED 交替亮灭呼吸灯的本质则是让 LED 的平均亮度按固定规律缓慢变化。实现方式不再是延时翻转电平而是用定时器输出 PWM 信号通过改变占空比控制等效亮度。STM32 的通用定时器输出比较通道可以在一个计数周期内让引脚电平在指定时刻翻转占空比由比较寄存器 CCR 决定。先看参数计算。假设系统时钟 72MHz想输出 1kHz 的 PWM。取预分频 PSC71计数时钟变成 72MHz / (711) 1MHz再取自动重装载值 ARR999PWM 频率等于 1MHz / (9991) 1kHz。计数器从 0 数到 999 循环当计数值小于 CCR 时输出高电平达到或超过 CCR 时输出低电平CCR 从 0 变到 999占空比就从 0% 变成接近 100%。6.2 呼吸灯参考代码定时器 PWM 加主循环调整 CCR下面这份代码以定时器 2 的通道 1 输出 PWM对应引脚通常是 PA0。如果你的实验板按键恰好也在 PA0建议换一个定时器通道避免外设冲突。// 定时器 PWM 呼吸灯示例输出引脚 PA0 (TIM2_CH1) void TIM2_PWM_Init(void) { GPIOA-CRL ~(0xF (4 * 0)); GPIOA-CRL | (0xB (4 * 0)); // CNF10 复用推挽MODE11 50MHz RCC-APB1ENR | (1 0); // 使能 TIM2 时钟 TIM2-PSC 71; // 预分频计数时钟 1MHz TIM2-ARR 999; // 重装载值PWM 频率 1kHz TIM2-CCMR1 | (7 4); // 通道1 PWM 模式1 TIM2-CCER | (1 0); // 使能通道1输出 TIM2-CCR1 0; // 初始占空比 0% TIM2-CR1 | (1 0); // 使能定时器计数 } int main(void) { uint8_t dir 1; TIM2_PWM_Init(); while (1) { if (dir) { TIM2-CCR1; if (TIM2-CCR1 999) dir 0; } else { TIM2-CCR1--; if (TIM2-CCR1 0) dir 1; } Delay_Ms(2); // 调节呼吸快慢 } }代码逻辑是主循环每 2ms 把 CCR1 加 1 或减 1占空比从 0% 走到接近 100% 再走回来形成呼吸效果。参数里最值得调的是三个Delay_Ms(2)决定呼吸速度数值越小呼吸越快CCR1 的步长决定每一步的变化幅度步长为 1 最平滑ARR 的值决定 PWM 频率和亮度等级数量频率低于 500Hz 时人眼会看到闪烁而不是渐变。如果你把 CCR 的变化做成正弦表而不是线性增减呼吸感会更接近真实效果。常见做法是预先生成一张 256 点的正弦表把查表结果映射到 0 到 999 范围主循环按固定间隔更新 CCR。这样亮度变化不是匀速而是先快后慢再快贴近人眼对亮度的感知。6.3 验证方法与参数调整习惯写完呼吸灯代码验证分两层。第一层是逻辑验证LED 亮度应呈现由暗到亮、再由亮到暗的循环周期大约是单程时间的两倍。本代码单程约 999 步乘 2ms接近 2 秒完整周期约 4 秒。如果觉得太慢把延时改成 1ms 或者把步长改成 2周期减半。第二层是波形验证有示波器时直接看 PA0 输出的 PWM 波形确认频率接近 1kHz占空比是否随 CCR 循环变化。没有示波器时用手机慢动作视频也能捕捉到 PWM 波形的明暗变化但只能定性判断。我自己的习惯是每次改完定时器参数都会把 PSC 和 ARR 代入公式手算一遍再确认当前系统时钟是否真的是 72MHz。很多呼吸灯效果不对的案例最后都出在系统时钟宏上你以为固件用的是 72MHz实际上工程模板里配置的 PLL 参数导致系统只跑了 64MHzPWM 频率跟着偏了大半。从那以后我每次新建工程的第一件事就是先读回并确认系统时钟频率再做任何外设配置。希望这份笔记能帮你少走一点弯路从 LED 闪烁到 PWM 呼吸灯一路直接跑通。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →