资讯动态

STM32 TIM1高级定时器PWM输出配置详解与避坑指南

发布时间:2026/10/5 6:06:38 来源:尧图企业网站定制
TIM1 在 STM32 里属于「高级定时器」很多人用标准库写 PWM 的时候TIM2、TIM3 一配就出波形一到 TIM1 就死活没输出或者波形乱跳。其实不是芯片有问题而是 TIM1 比通用定时器多了一堆「高级功能」死区、互补输出、刹车、重复计数、主输出使能。这些功能在标准库里面全都要显式配置漏掉任何一个结果就是没波形或者波形不对。这篇教程我直接按我实际调通的流程来写用标准库芯片以 STM32F103 为例目标是用 TIM1 的通道 1 输出一路可调占空比的 PWM。我会把每一步的代码、计算过程、寄存器背后的逻辑都讲清楚最后把我踩过的坑集中列出来。新手照着抄一遍基本能通老手也可以当成一份排查清单用。1. TIM1 和普通定时器到底差在哪1.1 TIM1 的「高级」不仅仅是名字好听STM32F103 里面TIM1 和 TIM8 是高级定时器TIM2、TIM3、TIM4、TIM5 是通用定时器。PWM 输出这件事通用定时器也能干但高级定时器是为电机控制、电源转换这类场景设计的所以多了一套「输出保护」和「多通道协同」的机制。具体来说TIM1 比通用定时器多出这几样东西互补输出和死区插入一个通道可以输出两路反相的 PWM比如 CH1 和 CH1N这两路之间还能插入一个「死区时间」。死区的意思就是上下桥臂的驱动信号一个关断之后另一个要延迟一小段时间再打开防止瞬间直通烧管子。这在 BLDC 驱动、全桥/半桥电路里是刚需。刹车功能Break可以配一个刹车输入引脚一旦刹车信号有效输出立刻被强制拉到安全电平。这是硬件级的保护不需要 CPU 干预。重复计数器Repetition Counter通用定时器是每次更新事件就产生中断TIM1 可以设置每隔 N 次更新才产生一次中断或更新事件。做 PWM 的时候配合 DMA 或者精准脉冲控制很有用。主输出使能MOE这是最坑人的一个。TIM1 的 PWM 输出除了要开 TIM_Cmd还要单独开一个主输出使能位TIM_CtrlPWMOutputs如果只开 TIM_Cmd 不开 MOE引脚上永远是低电平看起来就像完全没配置。我见过很多人卡在这个点上包括我自己第一次调 TIM1 时也栽过。1.2 为什么标准库依然值得写现在很多教程都推荐用 CubeMX 生成 HAL 库代码图形化配置确实方便但我觉得标准库对于理解 STM32 的运行机制还是很有价值的尤其是学习阶段。标准库的优点在于它的每一个初始化函数基本都对应着一组寄存器操作你看代码就知道自己在配什么。比如 TIM_TimeBaseInit 配的是 PSC、ARR、CR1TIM_OC1Init 配的是 CCMR1、CCER、CCR1TIM_BDTRInit 配的是 BDTR 寄存器。这种「函数-寄存器」的映射关系能帮你建立非常扎实的底层认知。等你以后再切到 HAL 库遇到问题翻寄存器手册也会从容得多。另外在不少公司里存量项目依然是标准库写的F103、F407 这类芯片的产品生命周期又很长学会标准库不是学了个没有的东西而是一项能直接用在项目里的技能。所以这篇教程我全部用标准库函数写并且每一步都告诉你对应的是什么寄存器、什么位这样你既能抄作业也能真正理解作业在干什么。2. 动手前必懂PWM 频率、占空比和极性计算2.1 参数计算的三个关键公式TIM1 的时基配置和通用定时器完全一样核心就三个寄存器PSC预分频、ARR自动重装载值、CCR捕获比较值。计数器的时钟频率由 PSC 决定定时器计数频率 定时器输入时钟 / (PSC 1)PWM 输出频率由 ARR 决定PWM频率 定时器计数频率 / (ARR 1)占空比由 CCR 决定占空比 CCR / (ARR 1) × 100%对 F103 来说TIM1 挂在 APB2 总线上。如果系统主频是 72MHzAPB2 时钟也是 72MHz那 TIM1 的输入时钟就是 72MHz。这个前提很重要很多人的频率算不对就是忘了 TIM1 不是 TIM2、TIM3 那样挂 APB1 的 36MHz。举个例子我要输出 20kHz 的 PWM占空比 50%希望波形细腻一点那就可以让计数频率高一些。比如选 PSC 71这样计数频率是 72MHz / 72 1MHz也就是每微秒计一个数。然后 ARR 49这样 PWM 周期是 50 × 1us 50us对应频率就是 20kHz。CCR 设为 25占空比就是 50%。这个参数组合在人眼和多数示波器上都挺直观而且 20kHz 也是很多开关电源、电机驱动常用的频率有实际参考意义。TIM_TimeBaseInit 函数里对应填数就是这样TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC 71 TIM_TimeBaseStructure.TIM_Period 49; // ARR 49 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInitTypeDef 里还有个 TIM_RepetitionCounter用 TIM1 时必须显式赋值有人漏了这个导致整个结构体初始化异常。我一般直接给 0用不到重复计数就关掉。2.2 PWM1 和 PWM2 到底该选哪个标准库配置输出比较模式时会看到两个选项TIM_OCMode_PWM1和TIM_OCMode_PWM2。两者的区别只在「有效电平」的定义上逻辑其实是反着的PWM1 模式向上计数时当 CNT CCR 时输出有效电平CNT CCR 时输出无效电平。PWM2 模式向上计数时当 CNT CCR 时输出无效电平CNT CCR 时输出有效电平。换句话说同样的 CCR 和同样的输出极性配置下PWM1 和 PWM2 输出的波形相位正好相反占空比是互补的。比如 CCR 填 25ARR 是 49PWM1 模式是 50% 高电平PWM2 模式就是 50% 低电平从波形上看就是反相了。实际项目里绝大多数情况用 PWM1 就够了PWM2 用的场景相对少。但如果你发现输出波形和预期相反不要急着改代码逻辑先看一眼是不是这里选错了。另外还要注意极性位TIM_OCPolarity_High和TIM_OCPolarity_Low它会影响输出的有效电平高低。有人配置的是高电平有效但示波器量出来发现反了就是极性和 PWM 模式叠加后的结果。最佳实践是先固定用 PWM1 High 极性把基本波形调通再根据实际电路决定要不要改极性。3. 完整代码实现从 GPIO 到 PWM 输出3.1 工程准备时钟、引脚和 AFIO先说一下我用的目标板STM32F103C8T6 最小系统板外部 8MHz 晶振系统时钟通过 PLL 倍频到 72MHz。TIM1 的通道 1CH1默认映射在 PA8 引脚。新建工程这里不啰嗦了标准库的工程模板网上到处都有核心就是把标准库的源码和启动文件加进 Keil 工程然后把 system_stm32f10x.c 里的时钟配置跑起来。我这里重点讲 PWM 相关的代码因为这才是 TIM1 的核心。初始化第一步是开启相关时钟。注意TIM1 是 APB2 外设GPIOA 也在 APB2 上而 AFIO 时钟也得开尤其是做引脚重映射的时候。代码这样写RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);GPIO 的配置用复用推挽输出。标准库里面 GPIOA 的配置不复杂GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);有人会问不用上拉或者下拉吗其实 PWM 输出直接配置成复用推挽就够用了不需要上下拉。外部电路如果想保证默认电平可以在硬件上加不要在引脚里加因为 GPIO 配置会影响输出级和预期的波形可能对不上。如果你想把 TIM1_CH1 重映射到别的引脚比如 PA7那就需要先调用GPIO_PinRemapConfig(GPIO_Remap_TIM1, ENABLE)这个函数也是因为开了 AFIO 时钟才能用。我很少用重映射因为 PA8 本来就是 TIM1_CH1 的默认位置布线方便但大家了解一下有这个操作。3.2 完整初始化代码直接抄下面是 TIM1 通道 1 输出 PWM 的完整初始化函数。我加了不少注释因为这里的每一行都是以前踩坑换来的。void TIM1_PWM_Init(u16 arr, u16 psc, u16 ccr) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; // 1. 开启时钟TIM1 和 GPIOA 都在 APB2 上 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置 PA8 为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 时基配置 TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_RepetitionCounter 0; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); // 4. 输出比较配置PWM1 模式 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; // CH1 主输出 TIM_OCInitStructure.TIM_OutputNState TIM_OutputNState_Disable; // 互补输出不用就关闭 TIM_OCInitStructure.TIM_Pulse ccr; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState TIM_OCIdleState_Reset; TIM_OCInitStructure.TIM_OCNIdleState TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, TIM_OCInitStructure); // 5. 死区配置BDTR 寄存器 TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Disable; TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime 0; // 不用死区就填 0 TIM_BDTRInitStructure.TIM_Break TIM_Break_Disable; // 不用刹车 TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_Low; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Disable; TIM_BDTRConfig(TIM1, TIM_BDTRInitStructure); // 6. 使能捕获/比较寄存器预装载避免更新时波形毛刺 TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); // 7. 使能 TIM1 主输出和定时器 TIM_CtrlPWMOutputs(TIM1, ENABLE); // 这就是 MOE漏了就没波形 TIM_Cmd(TIM1, ENABLE); }调用方式TIM1_PWM_Init(49, 71, 25);这就是 20kHz、50% 占空比的 PWM。你用示波器量 PA8应该能看到一串干净的方波。3.3 动态调节占空比的实战写法很多时候我们要在程序里动态改变占空比比如电机转速调整、LED 呼吸灯、电源输出电压调节。标准库提供了直接写 CCR 的接口函数TIM_SetCompare1(TIM1, 10); // 设置占空比为 10 / (ARR 1)这个函数内部其实就是写 CCR1 寄存器很简单。要改频率的话就改 ARR用TIM_SetAutoreload函数但改 ARR 的时候建议配合预装载使能ARRPreload否则计数到一半突然改周期当前周期会异常。注意改 ARR 后占空比会跟着变因为占空比是个比值CCR 没变 ARR 变了百分比就不一样了。所以项目里调参时我习惯先算好 ARR 和 CCR 的关系再一起更新。下面是一个动态调节占空比的示例逻辑比如做一个从 0% 到 100% 循环变化的呼吸灯while (1) { for (uint16_t i 0; i 49; i) { TIM_SetCompare1(TIM1, i); delay_ms(10); } for (uint16_t i 49; i 0; i--) { TIM_SetCompare1(TIM1, i); delay_ms(10); } }实际用中 CCR 不建议直接给 0 或满量程因为极端占空比下波形沿会比较抖尤其是频率较高的时候。如果需要长时间输出 0% 或 100%可以考虑直接关闭通道输出而不是靠 CCR0/ARR 来硬顶。4. 避坑合集这些坑我都踩过4.1 最大的坑忘记使能主输出这个坑我必须放在第一个讲因为十个 TIM1 无输出问题里至少六七个都是因为这个。TIM1 的输出级设计上有一个总开关叫 MOEMain Output Enable在 BDTR 寄存器里。标准库里对应的函数是TIM_CtrlPWMOutputs。你必须显式调用它并且传入ENABLE主输出才会真正接通。很多人只调了TIM_Cmd(TIM1, ENABLE)以为定时器跑起来了输出就来了结果 PA8 上永远是一根低电平线。我一开始也犯过这个错当时折腾了很久代码逻辑看了又看GPIO 配置也确认无误最后查参考手册才发现 BDTR 寄存器里还有这么一位。从那时起我就在初始化流程里加上一行注释提醒自己TIM1 有双重开关TIM_Cmd 管定时器TIM_CtrlPWMOutputs 管输出级。之后再也没漏过。4.2 死区和刹车配置不能跳过我见过有人用 TIM1 输出 PWM 时沿用通用定时器的习惯只定义结构体配了时基和输出比较然后初始化一下就不管了。TIM1 如果不去调用TIM_BDTRConfigBDTR 寄存器保持默认复位值其实也能输出但前提是你必须用TIM_BDTRInitStructure里的默认值把该关的功能关干净。最典型的例子是死区时间不配置、刹车功能不配置如果你不去调这个函数有些库版本里相关位是随机的可能意外触发刹车保护输出被硬件拉低而且你根本不知道为什么。我自己的习惯是即使不用死区不用刹车也照写一段完整的 BDTR 配置把该关的全部关掉。因为这样代码是「显式」的别人看代码或者你两个月后回头看都知道你的设计意图是不用这些功能不会误以为你忘了配。4.3 占空比反了的排查套路如果你把代码烧进去发现波形有但占空比和预期完全反了——比如预期 50% 高电平实测是 50% 低电平或者 CCR 调大占空比反而变小那多半是 PWM 模式和极性这两个地方出了问题。排查顺序我建议是先看TIM_OCInitStructure.TIM_OCMode配的是 PWM1 还是 PWM2再看TIM_OCPolarity是高有效还是低有效。这两处组合在一起会决定最终的输出逻辑。我看过很多人调了半天波形最后把所有库函数和寄存器翻了个遍才发现是 PWM2 Low 极性的组合效果等于 PWM1 High 极性看起来波形一样但代码语义上是反的。统一用 PWM1 High 极性可以让问题简单很多。4.4 频率不对先查时钟树功率器件对 PWM 频率很敏感频率错了MOSFET 的开关损耗、电感饱和程度都会变。如果你用示波器量出来频率和计算值差一倍甚至完全不对别急着怀疑公式先确认时钟源。这里的关键是TIM1 在 APB2 上而 APB2 最高是 72MHz。但如果你的系统主频不是 72MHz比如外部晶振是 8MHzPLL 没正确配置那 TIM1 的实际输入时钟就不是 72MHz。系统时钟设置里常见的错误是把 PCLK2 设成了 36MHz那计数器时钟就跟着减半。检查方法很简单在调试器里读一下 RCC-CFGR 寄存器确认 PPRE2 的分频位。如果你用的是库函数SystemInit()默认配置就是 72MHz一般不会错但如果你自己改了时钟初始化那就得小心这里。另外 TIM_CKD_DIV1 这个参数也容易让人误解它改变的是数字滤波器的采样时钟不是计数器的预分频跟 PWM 频率无关。有人以为这是第二级分频调了发现频率没变化其实它不影响 PWM 频率。4.5 波形抖动和毛刺问题输出波形频率对、占空比对但波形沿口毛刺多或者 PWM 波形会有偶发的跳变这通常是两个原因一是软件里改了 CCR 或 ARR 但没有开预装载导致计数到一半时更新生效波形出现「半个周期异常」二是电源或者地线干扰示波器探头夹子没夹好量出来的毛刺其实是测量引入的。软件层面该做的是TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable)和TIM_ARRPreloadConfig(TIM1, ENABLE)这两行我前面初始化代码里已经写了。它们的作用是修改 CCR 和 ARR 时数值先写入影子寄存器等计数器的更新事件发生时才真正生效。如果没有这个机制你在 while 循环里调用TIM_SetCompare1相当于随时改波形和 PWM 周期不同步波形自然乱。硬件层面就一句话量 PWM 波形时示波器的地线夹子要尽量短最好直接夹在芯片的 GND 引脚附近不要夹在板子另一端的接地端子上否则测到的噪声会误导你的判断。4.6 常见问题速查表现象可能原因排查办法PA8 完全无输出且一直是低电平没调用 TIM_CtrlPWMOutputs(TIM1, ENABLE)检查初始化函数里 MOE 是否使能PA8 无输出但电平不是低是高/悬空GPIO 配置错误或通道选择错误确认 PA8 是否复用推挽输出有波形但频率只有预期的一半TIM1 输入时钟不对APB2 被分频检查 RCC 时钟配置确保 APB272MHz占空比反了PWM 模式或极性配错统一用 PWM1 High再验证波形毛刺明显未开启 CCR/ARR 预装载补上 TIM_OC1PreloadConfig 和 TIM_ARRPreloadConfig改占空比之后 PWM 频率也跟着变ARR 和 CCR 没有一起更新先更新 ARR再更新 CCR或提前计算好5. 进阶TIM1 的互补输出和死区到底怎么配5.1 死区时间计算的硬核方法如果你的项目要驱动电机或者全桥电路比如用 TIM1_CH1 和 TIM1_CH1N 输出一对互补 PWM那就必须配死区时间。死区时间由 BDTR 寄存器的 DTG 位控制标准库里对应TIM_BDTRInitStructure.TIM_DeadTime这个成员。DTG 是 8 位值分三段区间计算方式完全不线性DTG 的 bit7 0 时T_DTS T_CK_INT × DTG范围 0 到 127 个周期DTG 的 bit7 1 且 bit6 0 时T_DTS (64 DTG[5:0]) × 2 × T_CK_INTDTG 的 bit7 1 且 bit6 1 且 bit5 0 时T_DTS (32 DTG[4:0]) × 8 × T_CK_INTDTG 的 bit7 1 且 bit6 1 且 bit5 1 时T_DTS (32 DTG[4:0]) × 16 × T_CK_INT公式看着烦实际用起来不用背很多标准库的底层函数已经封装好了你只需要根据你想要的死区时间和定时器时钟去反推 DTG 值就行。举个例子TIM1 输入时钟 72MHzT_CK_INT 13.89ns。我想要 1us 死区时间那就是约 72 个时钟周期。因为 72 在 0~127 范围内所以选第一段公式DTG 72直接填TIM_DeadTime 72就行。如果你想配置 3us72MHz 下是 216 个周期超过 127 了就得用第二段64 DTG[5:0] 的范围最大是 127再乘以 2也就是可以覆盖 128~254 个周期216 ≈ 64 44所以 DTG 的 bit71、bit60、bit5:0 44最终 DTG 值 0x80 | 0x2C 0xAC。这个计算过程看起来繁琐但电机驱动里死区配错了后果很严重死区太短上下桥臂直通烧管子死区太长输出波形有效占空比损失太多电机噪音大、效率低。调试时最好用示波器同时量两路互补输出看切换沿之间的间隔是不是符合预期。互补输出的初始化代码也很简单把TIM_OCInitStructure里TIM_OutputNState改为Enable再用TIM_OC1Init就好了。同时你还需要把互补输出引脚对应 GPIO 也配置成复用推挽。TIM1_CH1N 默认在 PB13 上。5.2 重复计数器到底怎么用TIM1 的重复计数器RCR在标准库里对应TIM_RepetitionCounter。它的作用是计数器溢出 N1 次后才产生一次更新事件。普通定时器是每次溢出都更新有了 RCR 之后你可以让 PWM 输出连续几个周期完全一致然后再发生一次更新。这种机制在某些场景下很有用比如你想用定时器产生「每输出 100 个 PWM 脉冲就触发一次中断」那就在 RCR 里填 99然后使能更新中断硬件会自动完成计数CPU 不用频繁响应中断。不过如果你只是简单输出一路 PWMRCR 填 0 就行保持默认行为。NULL 值也不行结构体成员必须初始化。5.3 刹车功能是硬件保护的最后一道防线刹车功能值得多说一句。TIM1 的刹车输入可以配置一个外部引脚当电平有效时PWM 输出会立刻变成预设的安全状态由 OSSI/OSSR 和 IDLE 状态控制。这个响应是硬件级的延迟只有几百纳秒远比「ADC 检测过流 - 中断 - 软件关输出」快。标准库里对应三个配置成员TIM_Break刹车功能使能开关TIM_BreakPolarity刹车的有效电平TIM_AutomaticOutput刹车释放后是否自动恢复输出做电机驱动时我强烈建议把刹车功能用起来。即使你现在只是学习 PWM也可以把这段配置留好等以后做实际硬件时不用重新学一遍。6. 写在最后的小经验TIM1 的 PWM 输出整体看来并不复杂但在实际调试过程中很容易被那些「附加功能」绊住脚。我个人觉得学这类高级外设最好的方式是先把最简功能跑通再逐步叠加特性。比如这篇教程里先什么都不管在 PA8 上输出一个固定频率和占空比的 PWM用示波器确认波形正确然后再去尝试动态改占空比、加死区、开互补、加刹车。每叠加一个功能都重新验证一次出问题的时候定位范围就小得多。最后分享一个小技巧标准库工程里如果你不确定某个结构体字段到底该填什么鼠标点进头文件的注释看一遍再看参考手册对应寄存器的位定义基本就不会错。STM32 的开发资料确实多但真正核心的永远是参考手册和库头文件这两份网上博客可以帮你快速上手但只有手册能帮你解决剩下的 20% 奇怪问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑