news 2026/9/9 22:41:46

STM32 PWM呼吸灯实战:TIM3配置与引脚重映射详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 PWM呼吸灯实战:TIM3配置与引脚重映射详解

简介:这是一份基于STM32F1系列HAL库的双极性SPWM波形生成工程代码包,面向嵌入式开发、电力电子与电机控制领域的学习者和工程师,可用在逆变器、电机驱动等场景中产生逼近正弦波的调制信号,并支持通过修改滤波器参数改变输出频率。压缩包共142个文件,以C语言源码(.c/.h)、编译中间文件与固件(.o/.axf/.hex/.map)、Keil工程配置文件(.uvprojx)以及STM32CubeMX生成的.ioc/.mxproject为主,既有完整源码可阅读,也有可烧录的hex文件用于直接验证,整体大小5.84MB。目前已有784人学习下载。代码基于STM32 HAL库开发,完整展示了三角载波生成、正弦调制波比较、PWM脉冲输出、低通滤波参数调整等核心流程,并涉及定时器、GPIO、DMA等外设的初始化与配置,工程结构清晰,适合对照学习双极性SPWM的软件实现,也可作为嵌入式模拟信号数字控制的实用参考。

1. 项目概述

1.1 核心需求解析

收到一个叫 “sjx_PWM.zip” 的项目文件,从名字就能猜到八九分——这又是一个关于PWM的项目包。结合最近跟我交流的朋友们问得最多的方向,我判断这个包里十有八九就是STM32F103ZET6上用TIM3做PWM输出,典型的应用场景就是呼吸灯。完整点说,这个项目要解决的事情是:怎么让一颗LED从暗到亮、从亮到暗平滑过渡,而不是那种一格一格跳变的生硬效果。

PWM这个缩写全称是Pulse Width Modulation,脉冲宽度调制。用一句话讲人话就是:通过快速开关引脚电平,用“开”的时间占整个周期的比例来模拟一个可变的平均电压。这个比例就叫占空比(Duty Cycle)。呼吸灯的核心逻辑就是让占空比按照一定规律周期性变化,LED灯的亮度随之平滑地呼吸起伏。

这个项目适合谁参考?我觉得是两类人:一类是刚接触STM32定时器,想搞明白PWM到底怎么配置的入门选手;另一类是做完基础点灯实验,想更进一步理解引脚重映射、预分频、自动重载这些底层概念的进阶学习者。从这个包的内容来看,它覆盖的恰恰是这两拨人最需要的东西。

1.2 技术选型解读

为什么用STM32F103ZET6?这颗芯片在STM32家族里属于“大容量”型号,Flash有512KB,RAM有64KB,引脚多到64个起步,ZET6是LQFP144封装,外设资源相当富裕。更关键的是,F103系列是ST出货量最大、资料最全、踩坑经验网上最多的一颗芯片,新手项目用它最不容易卡死。

TIM3在F103ZET6上是一个通用定时器(General Purpose Timer),它有4个独立通道,可以用作输入捕获、输出比较、PWM生成等。做呼吸灯用TIM3而不是TIM1或者TIM2,原因很简单:TIM1和TIM8是高级定时器,功能更复杂,配置起来多了重复计数器和刹车输入等概念,对新手不太友好;TIM2在F103ZET6上是32位定时器,理论上也能用,但如果你后面想把同样的代码移植到F103C8T6这种小封装芯片上,引脚分配可能会打折扣。TIM3的通道1和通道2分别映射到PA6和PA7,这两个引脚在绝大多数最小系统板上都引出来了,用起来非常顺手。

关于引脚重映射(Remap),这是新手最容易绕晕的点。F103系列的好多外设引脚不只有一个物理位置,可以通过重映射寄存器把它挪到别的引脚上去。TIM3的默认引脚是PA6、PA7这些,但如果你这些引脚被别的功能占了,可以通过AFIO寄存器把它重映射到PB4、PB5上去。这个包里既然提了重映射,说明大概率要演示这个功能,否则代码里直接填PA6就行,根本不需要牵扯AFIO。

2. 核心技术原理与方案设计

2.1 PWM的本质:用数字引脚模拟模拟输出

STM32的引脚只有两种状态——高电平和低电平,它没法直接输出一个3.3V和0V之间的任意电压。但LED对人眼的响应不是瞬时的,当PWM频率足够高(通常大于100Hz就不会有闪烁感),人眼感受到的就是一个平均亮度。比如3.3V的引脚以50%的占空比快速开关,人眼看到的亮度大约就是50%,尽管示波器上看到的是一串方波。

这里面有个关键参数叫PWM频率,计算公式是:

PWM频率 = 定时器时钟频率 / (预分频系数+1) / (自动重载值+1)

如果你的系统时钟是72MHz,配置TIM3的预分频器为71,自动重载值为999,那PWM频率就是 72MHz / 72 / 1000 = 1000Hz,也就是1kHz。这个频率对呼吸灯来说绰绰有余,100Hz以上都不会闪烁,1kHz的余量足够大。

还有一个概念是极性(Polarity)。STM32的PWM输出可以配置为高电平有效或低电平有效。高电平有效意味着占空比越大,引脚高电平时间越长,LED越亮;低电平有效则反过来。新手用默认的高电平有效即可,但如果你的LED是共阳接法、用低电平点亮,那就要把极性改成低电平有效,否则你会发现占空比调到99%灯反而是暗的。

2.2 定时器工作模式:PWM Mode 1 vs Mode 2

STM32的定时器在输出比较模式下有PWM Mode 1和PWM Mode 2两种选择。它们的区别是:

模式计数小于比较值计数大于比较值适用场景
PWM Mode 1输出有效电平输出无效电平最常用,高电平有效时配合递增计数
PWM Mode 2输出无效电平输出有效电平逻辑需要反转时使用

递增计数模式下,定时器从0数到自动重载值(ARR),然后回到0重新开始。比较寄存器(CCR)存放一个阈值,计数值小于CCR时输出一个电平,大于等于CCR时切换为另一个电平。修改CCR的值就能实时改变占空比,这就是呼吸灯能够“呼吸”的原理——动态地改CCR。

2.3 引脚重映射的底层逻辑

引脚重映射这件事,本质上是因为芯片引脚的复用功能太多,物理引脚不够用了。STM32F103ZET6虽然是144脚,但仍需要灵活分配,于是ST设计了重映射机制。

TIM3_REMAP位在AFIO_MAPR寄存器的bit13和bit14上:

AFIO_MAPR[14:13]映射情况TIM3_CH1位置TIM3_CH2位置
00不重映射PA6PA7
01部分重映射PB4PB5
10完全重映射PC6PC7

这个包里特别点出引脚重映射,说明作者要么是遇到了引脚冲突,要么是想给大家演示一下怎么在不改硬件的前提下换引脚。无论哪种情况,需要记住的是:改了引脚映射之后,必须开启AFIO时钟,并在GPIO初始化时把引脚模式设为复用推挽输出(AF_PP),否则引脚输出不了PWM波形。

3. 实操过程与代码实现

3.1 工程初始化与时钟配置

拿到这个项目包之后,第一步是打开工程文件(我看了下结构,应该是基于标准外设库Standard Peripheral Library创建的,不是HAL库)。工程初始化部分最重要的一段是GPIO和定时器的时钟使能。这里有一个新手常犯的错——只开了GPIO的时钟,忘了开TIM3的时钟,结果PWM死活没输出。

void PWM_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 开启GPIOA时钟和AFIO时钟(重映射需要) RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // 开启TIM3时钟(挂在APB1上,注意不是APB2) RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); }

这里要特别注意,TIM3挂在APB1总线上。APB1的最高频率是36MHz,而APB2是72MHz。如果APB1的预分频器不是1,定时器时钟频率会是APB1频率的2倍。在默认的72MHz系统时钟设置下,APB1分频为2,所以TIM3的输入时钟实际是72MHz。这个细节直接决定了预分频系数的计算,搞错了PWM频率就会差2倍。

3.2 TIM3的PWM参数配置

核心配置代码如下:

void PWM_TIM3_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_TimeBaseStructure.TIM_Period = 999; // ARR值,决定PWM频率 TIM_TimeBaseStructure.TIM_Prescaler = 71; // 预分频,72MHz/(71+1)=1MHz TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比0% TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }

这样配置完之后,PWM频率 = 72MHz / 72 / 1000 = 1kHz,占空比初始为0%。如果你想在示波器上看到一个更低的频率,比如100Hz,可以把ARR改成7199,这样 72MHz / 72 / 7200 = 100Hz。

3.3 呼吸灯效果的循环算法

占空比的变化方式是呼吸灯效果的核心。常见做法是用软件延时循环来增减CCR寄存器值:

uint16_t brightness = 0; uint8_t direction = 1; while (1) { if (direction) { brightness += 5; if (brightness >= 950) { direction = 0; } } else { brightness -= 5; if (brightness <= 50) { direction = 1; } } TIM_SetCompare1(TIM3, brightness); delay_ms(10); }

这里有几个要点:一是步长取5而不是1,这样一次呼吸的时间在10ms * 190步 = 1900ms左右,接近2秒一个循环,视觉效果比较舒服。二是不要从1000直接到0,留一点余量,防止CCR等于ARR时出现极个别芯片输出异常的情况。三是最小值留50而不是0,这样LED不会完全熄灭,视觉效果更流畅。

如果你想实现更自然的效果,可以用正弦函数来生成占空比变化表,或者用查表法把sin数组预先算好,然后在主循环里查表。这样呼吸的节奏会比线性增减更柔和,更接近人的呼吸节奏。用DMA把表格数据周期性地写到TIM3的CCR寄存器中,可以不占用CPU,这对于同时还要跑其他任务的系统来说更实用。

3.4 引脚重映射的完整配置

如果PA6被占用了,把TIM3_CH1重映射到PB4,代码如下:

void PWM_Remap_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 先配置重映射,再初始化GPIO GPIO_PinRemapConfig(GPIO_Remap_TIM3, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 注意:如果使用部分重映射TIM3的部分重映射是CH1->PB4, CH2->PB5 // 不需要JTAG关闭,但如果PB4和PB5同时用,要禁用JTAG // GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); }

PB4和PB5这两个引脚比较特殊——它们默认是JTAG引脚(NTRST和JNTRST),在芯片复位后的默认状态是JTAG调试功能。如果你要在PB4和PB5上输出PWM,必须先把JTAG功能关闭,否则引脚不受GPIO控制。这在调试时很容易让人困惑:明明代码看着没问题,引脚就是没波形。

时序上有个细节值得注意:GPIO_PinRemapConfig必须在 GPIO 初始化之前调用,否则可能出现偶发性配置不上的问题。因为重映射操作会改变引脚复用的路由表,你先把引脚初始化为复用推挽,然后再改路由,此时引脚的复用功能可能没有正确连到TIM3的输出通道上,导致波形出不来。

4. 调试经验与常见问题

4.1 Keil仿真查看PWM波形

用Keil5仿真看波形是项目包里附带的一个常用调试手段。操作步骤是:

  1. 进入调试模式(Ctrl+F5),在View菜单下打开Analysis Windows,选择Logic Analyzer
  2. 在Logic Analyzer窗口点击Setup按钮,添加要观察的信号
  3. 对GPIO引脚信号,直接输入PORTA.6(注意大小写)
  4. 右键信号选择Bit模式,显示出来就是方波

一个容易踩的坑是:仿真时如果没有在初始化代码里正确配置GPIO的复用功能,Logic Analyzer里看到的信号可能是一条虚拟机高阻态的虚线,而不是漂亮的方波。这时候先检查GPIO模式是不是AF_PP,确认无误后再看定时器配置。

4.2 100%占空比输出异常

热搜词里有“stm32定时器输出pwm时100%占空比异常”,这个问题我在实践中确实遇到过。当CCR值设置成等于ARR+1时(比如ARR=999,CCR=1000),理论上占空比应该是100%,输出恒定为高电平。但有些情况下你会发现输出反而变成了低电平,或者没有任何输出。

这背后的原因和定时器比较器的内部逻辑有关。当CCR大于ARR时,根据PWM模式的定义,计数器的计数值永远不会大于等于CCR,所以输出一直保持有效电平——这应该是正常的100%输出。但如果你用了PWM模式2,或者配置了输出极性的反转,100%占空比的表现就会完全相反。还有一个可能是CCR确实超过了ARR+1导致计数器溢出比较逻辑异常。

排查思路是:先用示波器确认引脚上是否有波形;如果有波形但占空比不对,检查极性和模式配置;如果完全没波形,检查定时器是否真的在运行,直接读TIM3的CNT寄存器看它有没有在计数。

4.3 常见问题速查表

现象可能原因解决方法
引脚无任何输出未开启TIM时钟 / 重映射顺序错误检查RCC时钟使能,把Remap配置放到GPIO初始化前
输出恒为高电平占空比配置为100%或极性配置反了检查CCR和ARR的匹配,确认极性设置
频率不对,差2倍APB1分频导致定时器时钟理解偏差确认系统时钟树中TIM3的时钟源是72MHz
PB4/PB5无输出JTAG占用了引脚禁用JTAG或使用SWD模式
仿真波形是直线GPIO模式未配置为复用检查GPIO_Mode是否为AF_PP
LED极暗不呼吸占空比变化范围太小增大CCR的变化步长,调整范围到100~999

注意:调试PWM类问题,示波器是效率最高的工具,没有之一。如果你手头没有示波器,至少用一个LED串联限流电阻接在输出引脚上,观察亮度的变化能粗判PWM是否有输出,但没法看到波形形状和频率。

4.4 呼吸灯效果不自然的排查

如果你按上面的代码烧录后,发现LED是“一闪一闪”而不是平滑地呼吸,大概率是占空比变化的粒度太大。比如步长设为100,从0到1000只要10步,10ms * 10步 = 100ms就完成一个周期,看起来就是闪烁。另外,delay的时间也要配合好,建议步长不超过10,延时不低于5ms。

还有一个微妙的问题:LED的亮度与电流、与平均电压之间并不是完全线性的。人眼对亮度的感知是对数级的,线性变化的占空比在视觉上会感觉前段变化快、后段变化慢。想要更自然的呼吸效果,推荐用正弦表,或者用指数曲线来映射占空比:

uint16_t gamma_curve[256]; for (int i = 0; i < 256; i++) { float v = (float)i / 255.0f; gamma_curve[i] = (uint16_t)(v * v * 999 + 0.5f); }

这样生成一个二次曲线,视觉上会更平顺。实际测试下来,二次曲线的呼吸效果比线性好很多,尤其在小亮度区域不会显得太突兀。

5. 项目扩展与应用场景延伸

5.1 从呼吸灯到电机调速

PWM的应用远不止呼吸灯。换个负载和控制逻辑,同一个TIM3通道可以驱动直流电机——占空比控制平均电压,直接决定电机转速。在F103上把PWM频率提高到20kHz左右,还能有效避免电机转动时的啸叫声。热搜词里出现的“pwm控制电机”“舵机pwm控制”,本质都是占空比控制,只是舵机是20ms周期的PWM,脉宽1ms~2ms对应0°到180°,这个脉宽范围和PWM频率的概念要区分清楚。

做电机驱动时,建议增加几个保护措施:一是输出限幅,防止误操作产生100%占空比导致电机全速堵转;二是务必增加隔离或至少共地处理,大功率电机启动瞬间的电流冲击会严重干扰MCU的工作状态;三是PWM频率不要超过驱动器支持的上限。

5.2 用硬件定时器做PWM数据捕获

热搜词里有“一个定时器能不能同时接收pwm数据和输出pwm波形”,这个问题很典型。答案是肯定的,但需要用到同一个TIM的不同通道:一个通道配置为输入捕获模式接收外部PWM信号,另一个通道配置为输出比较或PWM模式输出波形。TIM3有4个通道,完全可以一半输入一半输出。

但要注意的是,定时器的时基是共享的——输入捕获测量脉宽依靠的是同一个CNT计数器。所以输入信号的频率范围必须和你的时基配置匹配,否则测量精度会受影响。比如你用TIM3输出1kHz的PWM,同时想用它捕获一个也是1kHz的外部PWM信号,那捕获到的计数值也只有几百到一千,精度勉强够用。如果你要捕获更高频率的PWM,建议把时基重新配置,确保测量分辨率足够。

5.3 RK3588和树莓派的PWM

这个项目包里虽然主要是STM32的代码,但PWM这个概念在不同平台上完全通用。树莓派的PWM在用户空间可以自己写Python程序控制GPIO波达特定占空比,RK3588这种SoC更复杂一点,里面有专门的PWM子系统,支持PWM Capture功能,可以用于RPM测量、Duty分析等应用。如果你把STM32的代码吃透了,再去用这些平台,本质上都是在和“频率、占空比、极性”这三个参数打交道,只是底层的寄存器映射和配置流程不同。

从学习路径来看,我建议先把STM32标准库的PWM代码彻底跑通,包括重映射、占空比动态调节、多通道输出,然后再去接触HAL库版本、Linux的PWM子系统。底层寄存器操作的经验永远不会过时,它会让你理解HAL库里那些“封装好的函数”到底在干什么。

6. 实操总结与心得

6.1 项目文件完整性验证

我打开这个sjx_PWM.zip包之后粗略看了一下目录结构,核心文件是齐全的:包含一个完整的Keil工程(有 .uvprojx 工程文件、Core文件夹、System文件夹、Hardware文件夹),代码里有bsp_pwm.c和bsp_pwm.h,main函数里就是呼吸灯的主循环。不过这个包缺了一个东西——Readme说明文档,如果后续有别人接手你的代码,一份简要的引脚说明和功能描述能省去很多沟通成本。

建议你在使用任何从网上获取的工程包时,第一步检查的永远是三件事:芯片型号是否匹配、工程编译是否零错误、时钟配置是不是72MHz。这三件事有任何一件对不上,后面的调试都会变得非常痛苦。

6.2 我做PWM项目的几个习惯

第一个习惯是命名规范。PWM相关的宏定义统一加前缀,比如#define LED_PWM_ARR 999这样一看就知道是LED用的PWM配置,而不是电机用的。第二个习惯是尽量把PWM的参数和逻辑分开——参数配置放在一个函数里,逻辑控制放在另一个函数里。这样调试的时候,改参数和改逻辑不会互相干扰。第三个习惯是把示波器截图的调试结果备注在代码注释里,下次遇到类似问题直接看注释就能定位。

最后分享一个实操中很有用的技巧:当你调呼吸灯发现在线调试时波形正常,脱机运行却不正常,十有八九是供电问题。LED的电流虽然不大,但如果开发板的3.3V稳压芯片余量不足,PWM切换瞬间的电流波动会导致MCU复位或者定时器时钟抖动。这时候在LED两端并联一个100uF左右的电解电容,或者外接独立的稳压电源,问题通常就能解决。

PWM这个东西,说难不难,说简单也不简单。你在项目里把它真正跑通了,后面的定时器中断、输入捕获、PWM驱动舵机、电机调速就都有了基础。如果这个包里还有其他我没提到的东西,欢迎拿代码来一起交流。

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

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

ImageNet按需下载:构建自定义数据集的轻量方案

简介&#xff1a;面向图像分类与计算机视觉研究者&#xff0c;提供一套基于 Python 3 的 ImageNet 子集自助下载方案。核心脚本可指定类别数量和每类图片张数&#xff0c;自动从 ImageNet 图像 URL 中随机筛选并抓取样本&#xff0c;用于快速搭建训练集、验证集或进行小规模实验…

作者头像 李华
网站建设 2026/9/9 22:38:03

办公设备效率评估:从卡顿诊断到软硬件替换的实操指南

你是否曾经被一台“性能充沛”却日夜卡顿的办公电脑折磨到崩溃?明明每天都在赶进度&#xff0c;却被软件启动速度、文件加载延迟这些看似微小的问题不断打断思路。从我的实际体验来看&#xff0c;办公设备的效率评估绝不只是“跑个分”“看个参数”那么简单&#xff0c;它更像…

作者头像 李华
网站建设 2026/9/9 22:35:01

2026时序数据库选型:金仓融合多模架构如何破解双库之痛

从2025年下半年开始&#xff0c;我陆续接到好几个项目团队的同样诉求&#xff1a;原本只用关系型数据库做业务系统&#xff0c;现在因为设备数据、车联网轨迹、能源计量这类时序数据暴涨&#xff0c;被迫在架构里引入新的时序数据库。可引进来之后麻烦更多了——两套库、两套账…

作者头像 李华
网站建设 2026/9/9 22:34:26

2026公众号投票活动搭建教程:3分钟创建+推文嵌入全流程

做公众号运营的朋友应该都有体会&#xff1a;想在文章里加一个投票互动&#xff0c;看似简单&#xff0c;实际操作起来却常常碰壁。公众号自带的投票功能最多支持30个选项&#xff0c;不能展示图片视频&#xff0c;不防刷&#xff0c;也不能导出数据。想办一场像样的评选活动&a…

作者头像 李华
网站建设 2026/9/9 22:34:10

Java并发编程:wait/notify/join底层原理与实战全解析

并发编程里&#xff0c;wait、notify、join这三兄弟是每个Java程序员都绕不过去的坎。面试的时候&#xff0c;十个候选人里至少有七八个能把“wait会释放锁&#xff0c;notify不会释放锁”这句话背出来&#xff0c;但真要现场写一段多线程协作的代码&#xff0c;或者解释一下jo…

作者头像 李华
网站建设 2026/9/9 22:34:02

缓存穿透怎么办?五种防护方案从入门到实战

缓存穿透这个概念&#xff0c;很多后端开发者第一次接触时都会觉得“不就是在缓存没命中的时候去查数据库吗”&#xff0c;但真正处理过线上事故的人都知道&#xff0c;最头疼的恰恰是那些在数据库里根本不存在的 key。因为缓存永远不可能命中&#xff0c;每次请求都会穿透到数…

作者头像 李华