news 2026/9/10 0:38:00

STM32 PWM呼吸灯实战:从TIM3配置到引脚重映射的完整解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 PWM呼吸灯实战:从TIM3配置到引脚重映射的完整解析

简介:这是一个面向STM32F1微控制器的双极性SPWM波形生成代码工程,适用于逆变器、电机驱动、开关电源等需要正弦波输出的应用场景,也适合正在学习电力电子与嵌入式实时控制的开发者参考,可作为高校相关课程设计与毕业设计的参考模板。压缩包共142个文件、约5.84MB,工程内既包含Keil项目文件(.uvprojx/.uvoptx)、CubeMX图形化配置(.ioc),也包含19个.c与46个.h的HAL库源码,以及.hex/.axf等已编译产物和.map/.lst等映射文件,整体是一套可直接导入重新编译的完整Keil工程。已有784人学习/下载。代码展示了从三角载波与正弦调制波比较生成SPWM,到通过低通滤波提取近似正弦信号的完整流程;通过修改载波频率、调制比和滤波参数,即可调节最终输出频率。对深入理解SPWM数字控制、STM32定时器比较输出以及逆变器前级波形生成都有实际帮助。 最近拿到一个叫sjx_PWM.zip的工程包,解压后看到熟悉的CoreHARDWARESYSTEMOBJ目录,我就知道这是个标准的 STM32F103ZET6 工程,而且核心功能大概率躲不开 PWM。果不其然,LED 呼吸灯、TIM3 输出、引脚重映射、占空比调节全都集成在一个看似不起眼的压缩包里。对很多刚接触定时器输出的朋友来说,PWM 的概念不难理解,但真要在工程里跑起来,从引脚复用、时钟配置到波形调试,每一步都藏着坑。这篇文章就借这个sjx_PWM.zip把 PWM 从原理到实操完整拆一遍。

如果你正在学 STM32,想搞懂 PWM 输出的完整链路,或者做完了一个简单的 LED 点亮实验但不知道接下来怎么深入,这个工程值得认真研究。它不只是一个呼吸灯,更像一个 PWM 知识的小型集合体,把重映射、占空比渐变、故障保护、波形调试这些点都串了起来。

1. 项目解读:一个呼吸灯zip里装了什么

1.1 为什么小小的呼吸灯能串联这么多知识点

呼吸灯的逻辑本身非常简单:让 LED 的亮度从暗到亮、再从亮到暗循环。但这个效果背后牵扯的东西远比你想象的多。LED 要呼吸得顺滑,输出 PWM 的定时器频率要足够高,否则会有肉眼可感知的闪烁;占空比需要一步一步渐变,步进值刹车太猛会看到明显跳变,太慢又显得死气沉沉;引脚必须复用到正确的通道上,不然你在寄存器里写了一堆配置,信号却根本出不来;如果后面还想控制电机或大功率负载,故障保护和死区配置又是绕不开的话题。

sjx_PWM.zip这个工程名义上是做呼吸灯,实际上把这些 PWM 开发里的关键点都覆盖了一遍,属于那种“麻雀虽小五脏俱全”的教学型代码。所以我建议拿到这个工程的朋友别只顾着编译下载看灯亮,而是先把整个数据流走一遍:时钟树怎么给的、TIM3 挂在哪条总线上、引脚怎么从默认位置映射到目标引脚、CCR 寄存器的值怎么改变输出脉宽,把这些都理清楚,你才算真正学会了 PWM。

1.2 定时器选型:TIM3 和引脚重映射背后的考量

做呼吸灯,大部分人的第一反应是用 GPIO 翻转加延时来做软件 PWM,代码确实简单,但 CPU 会被完全占死,而且如果频率高一点,延时函数带来的抖动会让波形很不稳定。硬件定时器 PWM 的好处在于,定时器一旦启动,引脚翻转完全由硬件完成,CPU 只需要修改比较寄存器 CCR 的值,就能实时改变占空比。这也是工程里选 TIM3 而不是软件模拟的根本原因。

那为什么偏偏是 TIM3,而不是 TIM1、TIM2 或者 TIM4?STM32F103ZET6 的资源非常充裕,TIM1 是高级定时器,带互补输出和死区,功能强,但对这个实验来说有点牛刀杀鸡;TIM2 和 TIM3 都是通用定时器,TIM3 的四个通道分布在 PA6、PA7、PB0、PB1,其中一个有意思的地方是有部分引脚默认被 JTAG 占用。把这个工程选 TIM3 的一个重要原因,就是顺理成章地引出“引脚重映射”这个很多新手非常困惑的操作。

方案优点缺点适用场景
GPIO 软件翻转简单直观CPU 占用高,频率不稳定临时验证
TIM3 硬件 PWM稳定、资源占用低需要配置重映射和复用呼吸灯、电机调速
TIM1 高级定时器带刹车/死区/互补输出配置复杂电机控制、逆变

如果你的 PCB 上已经把 LED 接到了 PB4/PB5 这类非默认引脚,就要通过重映射让 TIM3 的通道信号跑到你需要的引脚上去。这个设计思路在实际项目里非常常见:芯片引脚规划从来不是“这个定时器只能用这几个引脚”,而是通过 AFIO 重新分配功能,让硬件设计更灵活。

2. PWM 原理与关键参数计算

2.1 从 LED 亮度到占空比:PWM 的本质

PWM(脉宽调制)的本质,是用一个固定频率的方波,通过改变高电平在一个周期内所占的比例,来改变输出的平均电压。人眼对高频率亮灭切换并不敏感,看到的只是平均亮度:占空比越高,等效电压越高,LED 越亮;占空比越低,LED 越暗。呼吸灯的任务就是让占空比随时间周期性变化。

为了更容易理解,可以把定时器想象成一个不断往杯子里倒水再倒掉的循环:自动重装载寄存器 ARR 决定杯子能装多少水,比较寄存器 CCR 决定倒到哪个位置时打开阀门。当计数值 CNT 小于 CCR 时,引脚输出高电平;当计数超过 CCR 但还没超过 ARR 时,引脚输出低电平。CNT 到达 ARR 后溢出重新从零开始,下一个周期继续重复这个过程。改变 CCR,就是改变阀门打开的位置,也就是改变占空比。

2.2 引脚重映射细节:PB4/PB5 与 JTAG 的冲突

很多人在做sjx_PWM.zip这个工程时,最头疼的就是明明照着代码配了,引脚却没有任何波形输出。这时候十有八九是踩了 JTAG 和重映射的坑。

STM32F103 的 TIM3 通道默认映射是:CH1 在 PA6,CH2 在 PA7,CH3 在 PB0,CH4 在 PB1。但如果你查看芯片数据手册的 AFIO 部分,会发现 TIM3 支持部分重映射和完全重映射。比如完全重映射后,CH3 可以挪到 PB4,CH4 可以挪到 PB5。问题在于,PB4 和 PB3 在芯片复位后的默认功能是 JTAG 的 NJTRST 和 JTDO。

要让这两个引脚变成普通 GPIO 或者复用功能,必须先关闭 JTAG 功能,保留 SWD 下载调试口。工程里常见的写法是:

GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_TIM3, ENABLE);

第一条语句把 JTAG 关掉,释放 PB3、PB4、PA15 这几个被 JTAG 占用的引脚;第二条语句把 TIM3 的功能重映射到新的引脚组合上。顺序不能反,而且要注意操作 AFIO 时钟也要打开,否则寄存器配置不生效。如果你在 CubeMX 里操作,就不用管这个细节了,但用标准外设库的老工程里,这两行是很多问题的根源。

2.3 时序参数计算:频率、占空比和呼吸节奏怎么搭配

sjx_PWM.zip里默认用的时钟是外部 8MHz 晶振,经过 PLL 倍频到 72MHz,TIM3 挂载在 APB1 总线上,所以定时器输入时钟就是 72MHz。

PWM 频率、占空比、呼吸节奏三者是互相关联的。以sjx_PWM.zip的常用配置为例,PSC 设为 71,ARR 设为 999,这样 72MHz 先被分频到 1MHz,再计数 1000 次溢出,得到 1kHz 的 PWM 频率。占空比计算公式为:

占空比 = CCR / (ARR + 1) * 100%

呼吸灯节奏则取决于 CCR 的更新频率和步进值。如果每 10ms 让 CCR 加 1,从 0 加到 999 需要差不多 10 秒,呼吸一次太长,灯看起来会很慵懒。工程里一般把步进值设成 5 或者 10,也就是每 10ms 让 CCR 加 5,这样从暗到亮大概需要 2 秒,视觉上刚刚好。

PWM频率 = 72MHz / ((PSC+1) * (ARR+1))

举个例子,如果 PSC = 71,ARR = 999,那么 PWM 频率 = 72MHz / (72 * 1000) = 1kHz。这个频率驱动 LED 完全没有问题,但如果驱动舵机,一般要用 50Hz,那就需要把 ARR 拉大或者调 PSC,参数计算逻辑是一样的,只是目标频率不同。

3. 从 CubeMX 配置到跑通呼吸灯

3.1 关键配置项逐项说明

如果用 CubeMX 新建工程,配置步骤并不复杂,但有几个地方容易漏。

第一步,在 RCC 里把 HSE 设为 Crystal/Ceramic Resonator,外部时钟源选好;第二步,在 SYS 里把 Debug 设置为 Serial Wire,这一步非常关键,否则一旦关闭 JTAG,下次烧录可能连不上芯片;第三步,在定时器 TIM3 里把 Clock Source 设为 Internal Clock,Channel1 模式选 PWM Generation CH1;第四步,把引脚从默认位置拖到目标引脚,比如 PB4,CubeMX 会自动生成重映射相关代码;第五步,在 TIM3 的参数配置里填入 PSC、ARR,Pulse 初始值可以设 500,这样上电时 LED 正好是一半亮度,方便确认配置是否生效。

这里特别提醒一下:引脚重映射在 CubeMX 里看似只是拖拽,但工具自动帮你处理了 AFIO 和重映射寄存器。如果手写标准外设库代码,千万不要漏掉RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE)这一条。

3.2 核心代码解读

初始化部分,HAL 库的写法比较固定:

void MX_TIM3_Init(void) { TIM_OC_InitTypeDef sConfigOC = {0}; htim3.Instance = TIM3; htim3.Init.Prescaler = 71; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 999; htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(&htim3); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 500; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); }

呼吸灯的主循环逻辑不复杂,关键是怎么让 CCR 平滑地来回变化。我推荐用一个目标值和方向标志位的思路,比单纯两个 for 循环嵌套更干净:

uint16_t ccr = 0; uint16_t target = 999; uint16_t step = 5; while (1) { if (ccr < target) { ccr += step; if (ccr >= target) { target = 0; } } else if (ccr > target) { ccr -= step; if (ccr <= target) { target = 999; } } __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, ccr); HAL_Delay(10); }

为什么用target而不是 dash 两个 for 循环?因为目标值方式更容易扩展:以后你想让灯停在某个亮度再开始呼吸,只需要修改 target 和 ccr 的初始值;反复 while 循环嵌套也可以,但代码嵌套深了可读性差。步进值 5 是我实测比较舒服的数值,既不会看到灯光跳变,也不会因为变化太慢让人犯困。

3.3 故障保护与死区:给 PWM 加一道“保险”

PWM 不只用来点灯,更多场合是用来驱动电机、加热器等功率设备。在这些应用里,“故障保护”就非常重要了。

STM32 的高级定时器 TIM1/TIM8 支持刹车输入(Break Input),可以把过流、过压等故障信号接到 BKIN 引脚。一旦该信号有效,定时器输出会被硬件强制拉低或拉高,同时触发中断通知 CPU 处理。这种保护不受程序卡死影响,是纯硬件级别的响应,比软件里判断一个 GPIO 再手动关闭 PWM 快得多。

热词里还提到“28335 配置的 PWM 死区 DBFED 和 DBRED”。这让我想到很多做电机控制的朋友会混淆死区配置。STM32 上 DTG 寄存器负责死区插入,而 TI 的 28335 DSP 把死区分成了上升沿延时(DBRED)和下降沿延时(DBFED)两个独立寄存器。虽然命名和寄存器架构不同,但目的一致:在互补 PWM 的上下桥臂切换之间插入一段“同时关断”的安全时间,防止上下管直通烧毁。如果你将来做 H 桥或者三相电机驱动,这个知识点几乎必定会遇到。

回到sjx_PWM.zip这个工程,如果只是点灯,硬件刹车用不上,但代码里仍然可以写一个软保护逻辑:检测到某个按键或外部信号时,立刻把 CCR 清零并停止定时器,这个习惯值得养成。我见过很多新手在点灯阶段不去想保护,真到驱动电机时又没有保护意识,结果一上电就烧板子。从点灯阶段开始建立故障保护思维,成本非常低。

4. 调试实录:常见问题与排查技巧

4.1 输出恒高或 100% 占空比异常的真相

我在用 STM32 做 PWM 的时候,遇到过好几次 100% 占空比输出异常的情况。表现形式是:理论上满占空比应该输出恒高电平,但示波器上看波形偶尔会突然出现一个很窄的低电平毛刺,或者灯亮度不是满亮,而是略偏暗。

排查到最后,问题往往出在 CCR 的值超过了 ARR。定时器比较逻辑是 CNT 等于 CCR 的时候翻转,如果 CCR 被设置成大于 ARR 的数,硬件行为会变得不可预测。另外,如果在更新 CCR 时没有考虑 ARR 的值,比如把 CCR 设成了 1000,而 ARR 只有 999,某些芯片版本可能出现异常波形。解决办法很简单:更新 CCR 之前做一次钳位,确保ccr <= ARR。在sjx_PWM.zip这类工程里,呼吸灯代码通常不会出错,但如果你改过 ARR 却没有同步修改 step 和 target,就要特别注意了。

4.2 Keil 仿真和逻辑分析仪怎么看波形

很多人来问 Keil 5 怎么查看 PWM 波形。Keil 的 Debug 模式里确实有逻辑分析窗口,可以添加变量并配置成 bit 显示,能看到 CNT 和 CCR 的变化,但如果想看引脚真实的电平波形,我更推荐用逻辑分析仪直接量引脚。

具体在 Keil 里操作:进入调试模式后,打开 View -> Watch 窗口,添加TIM3->CNTTIM3->CCR1,再打开 View -> Analysis Windows -> Logic Analyzer,把这两个变量拖进去,配置格式选择 Bit 或 Analog,就能看到时间轴上的波形变化。这个方式适合确认程序执行的逻辑,但它模拟的是寄存器状态,不是芯片引脚上实际输出的电平。真正验证 PWM 波形,还是用示波器或逻辑分析仪量 PB4(或你重映射后的目标引脚)最直接。

我个人的习惯是:先在逻辑分析仪里看到干净的 1kHz 方波,再去看 LED 亮度变化。如果波形正常但灯不亮,那是硬件问题;如果灯忽明忽暗但波形一直正常,那是占空比渐变逻辑问题。按这个思路排查,能把问题快速分成“软件逻辑”和“硬件连接”两个方向。

4.3 引脚没波形的排查顺序

如果代码编译下载后引脚上没有波形,按顺序做这几步检查。第一步,确认定时器时钟有没有打开,标准外设库工程最容易漏掉这一点;第二步,确认 GPIO 模式是否设置成复用推挽输出(AF_PP),如果你不小心配置成通用推挽输出,信号是出不去;第三步,确认重映射是否生效,PB4 这种引脚如果不关闭 JTAG,永远是高阻或具体芯片默认状态;第四步,确认初始化和 PWM 启动函数有没有执行,如果你在初始化之后不小心把 CCR 清 0 或者关闭了输出,波形自然没有。

还有一个小细节:接 LED 时要注意引脚极性。PWM 高电平有效时,LED 正极接引脚、负极经限流电阻接地;如果你把 LED 接反了,占空比越小反而越亮,容易误判成程序逻辑反了。这个低级错误我在带着学生做实验时遇到过不止一次。

4.4 呼吸不顺畅、闪烁和亮度不均匀的问题

呼吸灯如果看起来一顿一顿的,问题大概率出在步进值和延时时间的配合上。比如你每 50ms 才更新一次 CCR,每次只加 1,LED 的变化就会非常生硬。我的经验是:让 CCR 每 10ms 变化 5 到 10,基本能达到平滑呼吸的效果。但如果 PWM 频率太低,比如低于 500Hz,即使占空比变化再平滑,人眼还是能看到高频闪烁。这时候优先调高 PWM 频率,而不是拼命改步进。

亮度不均匀则是另一个问题。人眼对亮度的感知不是线性的,CCR 从 0 加到 100 的亮度变化,在人眼看来可能比从 500 加到 600 更明显。如果追求完美的呼吸效果,可以对占空比做对数或指数校正,但工程精度要求不高的话,线性渐变反而更有呼吸灯原本的神韵。

注意:用HAL_Delay做主循环延时非常简单,但它会阻塞主循环。如果工程里以后要同时处理按键、显示等其他任务,建议把占空比更新放到定时器中断里做,或者用非阻塞的状态机,避免延时期间系统完全没有响应。

最后再分享一个我在实际调试这个工程时收获很大的小技巧:不要一上来就调代码,先在初始化函数里把 CCR 设成固定值,比如 500,确认 LED 以固定亮度点亮。这一步能快速验证硬件链路和 PWM 基本输出是否正常。固定亮度没问题之后,再改成渐变逻辑去调呼吸节奏。把一个大目标拆成两个“必现”状态来验证,比直接跑完整程序、出了问题一头雾水要高效得多。sjx_PWM.zip这个工程值得你反复烧录几遍,每次改一个参数,观察现象变化,很快你就能把 PWM 玩明白了。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 0:36:54

基于ESP8266的WiFi网络授时时钟设计与实现

简介&#xff1a;一份完整的STM32网络授时时钟工程代码&#xff0c;以STM32F103C8T6为主控、ESP-12F为WiFi模块&#xff0c;配合PCF8563时钟芯片、按键和OLED显示屏&#xff0c;实现联网获取天气与时间信息并定时刷新。工程按功能拆分为多个可读性较强的模块&#xff1a;bsp_es…

作者头像 李华
网站建设 2026/9/10 0:36:03

PS2手柄+STM32四电机麦克纳姆轮小车:从接线到PID调试全解析

简介&#xff1a;这是一套基于STM32RCT6微控制器、利用PS2手柄控制四轮全向轮小车的完整工程资源。项目从手柄信号解析、电机PWM调速到全向轮运动逻辑均有详细代码实现&#xff0c;适合学习STM32库函数开发、嵌入式电机控制及无线遥控小车的开发者参考。压缩包共233个文件&…

作者头像 李华
网站建设 2026/9/10 0:32:41

毕业设计论文写作:那些吞噬时间的隐形黑洞与我的破局之道

1. 引言&#xff1a;一场与时间的拉锯战 作为一名正在进行毕业设计的大学生&#xff0c;我深知论文写作中的繁琐和耗时。在这个过程中&#xff0c;许多环节都让我感到无比耗费时间&#xff0c;尤其是在参考文献格式、中英文混排、文本修改、人工核对和任务交接等方面。于是&am…

作者头像 李华
网站建设 2026/9/10 0:32:21

温控风扇工程实战:DS18B20、PWM与PID调速的嵌入式设计全解析

简介&#xff1a;温控风扇工程文件.zip是一份完整的嵌入式温控项目资料&#xff0c;面向电子设计初学者、单片机开发者及课程设计学生&#xff0c;围绕温度采集、控制逻辑与风扇驱动展开。压缩包共8个文件、仅996KB&#xff0c;涵盖C语言源码、HEX固件、Protues仿真工程、原理图…

作者头像 李华