行业资讯

STM32定时器输出比较原理与PWM配置实战指南

发布时间:2026/7/31 3:12:26
STM32定时器输出比较原理与PWM配置实战指南 1. 项目概述从“定时器输出比较”到精准的PWM控制如果你正在玩STM32尤其是想用它来驱动舵机、控制电机转速或者生成任意占空比的方波信号那么“定时器输出比较”这个功能就是你绕不开的核心技术。很多朋友第一次接触时可能会被数据手册里复杂的寄存器、各种模式搞得一头雾水感觉配置起来步骤繁琐一不小心就没波形输出。其实它的核心思想非常直观就是让定时器的计数值CNT不断地和我们预先设定好的一个“比较值”CCR去比大小根据比较的结果来翻转某个特定引脚比如PA8的电平从而生成我们想要的PWM波形。我最初学STM32那会儿也是跟着江科大的教程一步步过来的。他们的视频讲得很细但有时候看完了自己动手写代码还是容易卡壳尤其是在配置通道极性、输出模式这些细节上。所以我想结合自己这些年调试电机驱动、LED调光等项目的实际经验把“定时器输出比较”这个功能掰开了、揉碎了讲清楚。我们不光要会调用库函数更要明白库函数背后每个参数的意义以及当没有PWM波形输出时应该从哪几个方向去排查。这篇文章的目标就是让你看完后能独立、自信地配置出任意频率和占空比的PWM信号并且知道为什么这么配置。2. 核心原理拆解定时器如何“比较”并“输出”要理解输出比较我们得先回顾一下STM32通用定时器以TIM2、TIM3、TIM4等为例的基本工作框架。你可以把它想象成一个不断向上累加的计数器CNT它从0开始每来一个时钟脉冲就加1一直加到我们设定的“自动重装载值”ARR然后清零重新开始如此循环。这个循环的周期就决定了我们生成波形的频率。2.1 输出比较的核心机制输出比较单元就是在这个循环过程中安插的一个“裁判”。我们事先告诉这个裁判一个“比较值”CCR。在计数器CNT累加的每一刻这个裁判都在默默地将CNT的值与CCR进行比较。根据我们设定的“比赛规则”输出比较模式裁判会决定对应输出引脚比如CH1对应PA8的电平状态。最常用、也是最核心的模式是PWM模式1和PWM模式2。它们的规则很简单PWM模式1当CNT CCR时输出有效电平通常我们设置为高电平当CNT ≥ CCR时输出无效电平低电平。在向上计数时这就产生了一个在计数值小于比较值时高大于等于时低的波形。PWM模式2规则正好相反。当CNT CCR时输出无效电平当CNT ≥ CCR时输出有效电平。这里的“有效电平”具体是高还是低还可以通过另一个参数“输出极性”来单独设置。这就给了我们极大的灵活性。举个例子我们通常用PWM模式1极性为高即有效电平高。那么在一个计数周期内引脚输出波形就是从0计数到CCR-1输出高电平从CCR计数到ARR输出低电平。那么高电平时间占总周期的比例也就是占空比就等于 (CCR / (ARR1)) * 100%。通过修改CCR的值我们就能实时、平滑地调整占空比而波形的频率由ARR和时钟源频率共同决定。2.2 关键寄存器与库函数映射理解了原理我们再来看HAL库或标准库是如何封装这些操作的。你不需要死记硬背寄存器但了解其对应关系调试时看寄存器值会非常有用。TIMx_ARR (自动重装载寄存器)决定了计数器的上限即周期。TIMx_CCRy (捕获/比较寄存器y, y1,2,3,4)这就是我们设定的比较值CCR直接控制占空比。TIMx_CCMRy (捕获/比较模式寄存器)这里配置通道是输入还是输出对于PWM肯定是输出以及选择PWM模式1还是模式2。TIMx_CCER (捕获/比较使能寄存器)这里使能具体的输出通道并设置输出极性是高电平有效还是低电平有效。TIMx_CR1 (控制寄存器1)最重要的作用是使能定时器计数器CEN位。在HAL库中这些配置被整合到了一个结构体TIM_OC_InitTypeDef和几个关键函数里HAL_TIM_PWM_Init(): 初始化定时器的PWM输出基础模式。HAL_TIM_PWM_ConfigChannel(): 配置特定通道的PWM参数包括模式、极性、CCR初始值等。HAL_TIM_PWM_Start(): 启动指定通道的PWM输出。注意很多新手会忘记最后一步HAL_TIM_PWM_Start()或者错误地调用了HAL_TIM_Base_Start()这个只启动计数不启动输出。这是导致“配置都对但引脚没波形”的最常见原因之一。3. 完整配置流程与代码实战以TIM3_CH1复用至PA6为例理论说再多不如一行代码。我们以STM32F103C8T6的TIM3通道1为例将其输出映射到PA6引脚生成一个频率1kHz占空比初始为50%的PWM波。这里使用HAL库和CubeMX配合讲解因为这是目前最高效的开发方式。3.1 硬件与时钟树配置首先在CubeMX中完成基础配置选择芯片STM32F103C8Tx。配置时钟RCCHSE外部高速时钟选择“Crystal/Ceramic Resonator”。对于F103通常外接8MHz晶振。在Clock Configuration标签页将系统时钟SYSCLK通过PLL倍频到72MHz。这是F103的典型最高主频。APB1总线时钟TIM3挂载于此会自动设置为36MHz。但注意如果APB1预分频系数不为1定时器时钟会倍频。此处默认配置下APB1预分频器为236MHz因此TIM3的实际时钟是APB1时钟的2倍即72MHz。这一点至关重要是计算ARR和PSC的依据。配置定时器TIM3在“Pinout Configuration”标签页找到TIM3。将Channel1设置为“PWM Generation CH1”。此时PA6或PB4取决于重映射会自动被配置为复用推挽输出。进入“Parameter Settings”子标签页Prescaler (PSC): 预分频器。设置71。因为定时器时钟是72MHz72MHz / (711) 1MHz。这意味着计数器CNT每1微秒加1。Counter Mode: 计数模式。选择“Up”向上计数。Counter Period (ARR): 自动重装载值。设置999。结合上一步PWM频率 1MHz / (999 1) 1kHz。公式定时器时钟频率 / ((PSC1)*(ARR1))。Internal Clock Division (CKD): 时钟分频选“No Division”。auto-reload preload: 自动重装载预装载建议使能Enable。这样只有在更新事件发生时新写入的ARR值才生效防止在周期中间产生毛刺。进入“PWM Generation Channel 1”子标签页或NVIC等标签页取决于CubeMX版本Pulse (CCR1): 初始比较值。设置500。因为ARR是999占空比 500 / (9991) 50%。Mode: PWM模式。选择“PWM mode 1”。Fast Mode: 快速模式通常禁用。CH Polarity: 通道极性。选择“High”。这意味着我们定义的“有效电平”是高电平。结合PWM mode 1输出波形就是CNTCCR时高CNTCCR时低。3.2 生成代码与关键函数解析生成代码后我们重点关注main.c中的用户代码区。/* 在main函数初始化部分系统会自动生成以下代码 */ MX_TIM3_Init(); // 初始化TIM3 /* 在main函数的while(1)循环前启动PWM输出 */ HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); /* 后续在程序中你可以动态修改占空比 */ __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 300); // 将占空比改为 300/1000 30% // 或者使用 htim3.Instance-CCR1 800; // 直接操作寄存器将占空比改为80%让我们深入看一下MX_TIM3_Init()函数内部在tim.c文件中的关键内容static void MX_TIM3_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; 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; if (HAL_TIM_Base_Init(htim3) ! HAL_OK) // 初始化时基单元 { Error_Handler(); } sClockSourceConfig.ClockSource TIM_CLOCKSOURCE_INTERNAL; // 使用内部时钟 if (HAL_TIM_ConfigClockSource(htim3, sClockSourceConfig) ! HAL_OK) { Error_Handler(); } if (HAL_TIM_PWM_Init(htim3) ! HAL_OK) // 初始化PWM模式 { Error_Handler(); } sMasterConfig.MasterOutputTrigger TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(htim3, sMasterConfig) ! HAL_OK) { Error_Handler(); } sConfigOC.OCMode TIM_OCMODE_PWM1; // 设置为PWM模式1 sConfigOC.Pulse 500; // 初始CCR值即初始占空比 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; // 输出极性高 sConfigOC.OCFastMode TIM_OCFAST_DISABLE; if (HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1) ! HAL_OK) // 配置通道1 { Error_Handler(); } HAL_TIM_MspPostInit(htim3); // 此函数中完成了GPIO的复用配置 }实操心得HAL_TIM_MspPostInit是一个弱函数CubeMX在main.c文件底部重写了它里面包含了GPIO_Init配置将PA6初始化为复用推挽输出。如果你是自己手写代码或者移植工程千万不能遗漏GPIO的初始化并且要将其模式设置为复用功能GPIO_MODE_AF_PP而不是普通的推挽输出。4. 高级应用与深度优化技巧掌握了基础PWM输出后我们可以玩一些更高级的功能这些在实际项目中非常有用。4.1 互补输出与死区插入驱动电机H桥在驱动直流有刷电机或步进电机时我们经常使用H桥电路。H桥的同侧两个MOS管如上管和下管不能同时导通否则会造成电源短路俗称“直通”或“shoot-through”。定时器的高级控制功能可以完美解决这个问题。以TIM1或TIM8这类高级定时器为例它们支持“互补输出”和“死区插入”。互补输出你可以配置CH1主输出和CH1N互补输出。这两个通道的输出电平通常是反相的。死区插入为了防止开关器件MOS管在状态切换时由于关断延迟导致的短暂共同导通需要在互补的PWM信号中插入一个两者都为低电平或无效电平的“死区时间”。在CubeMX中配置高级定时器的互补通道时会多出一个“Dead Time”的配置项。死区时间通常以纳秒ns为单位其计算与定时器时钟频率相关。例如72MHz时钟下一个计数周期是13.89ns。如果你设置死区时间为0x4F根据数据手册的编码格式可能对应着大约1.1us的死区时间这对于多数MOS管来说已经足够。// 在高级定时器初始化后启动带死区的互补PWM输出 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); // 启动互补通道CH1N注意事项使用互补输出时务必检查对应的GPIO引脚是否正确映射如TIM1_CH1N可能对应PA7或PB13需要查数据手册并且其驱动能力是否足够。死区时间需要根据你使用的MOS管或IGBT的开关特性特别是关断延迟时间来仔细计算和调整太小了不能防止直通太大了会降低有效电压利用率。4.2 多通道同步与主从模式有时我们需要多个PWM通道完全同步或者让一个定时器去触发另一个定时器。例如用三个相位互差120度的PWM驱动无刷直流电机BLDC或者生成复杂的多路同步信号。这需要用到定时器的“主从模式”和“触发输出/输入”功能。主定时器配置为主模式将其更新事件UEV或比较匹配事件作为触发输出TRGO。从定时器配置为从模式触发源TS选择为“ITR0”、“ITR1”等对应内部连接线或者外部触发引脚。从模式设置为“触发模式”Slave Mode Trigger Mode或“门控模式”。这样主定时器的每一次溢出更新都会复位或启动从定时器确保两者的计数周期严格同步。在CubeMX的“Master/Slave Mode”配置部分可以图形化地设置这些关联。4.3 动态修改频率与占空比动态修改占空比很简单直接修改CCR寄存器即可如前文所示。但动态修改频率即ARR值则需要格外小心。因为ARR寄存器可能带有缓冲预装载。在PWM输出过程中直接写入ARR新的值可能不会立即生效而是要等到下一个更新事件。如果你在计数器运行到中间时写入一个比当前CNT值还小的ARR可能会导致本次周期异常缩短产生一个“毛刺”脉冲。安全做法在修改ARR前可以先停止PWM输出HAL_TIM_PWM_Stop修改后再启动。但这会导致输出有短暂中断。更好的方法是利用“预装载”功能。确保AutoReloadPreload ENABLE。这样你可以在任何时候安全地写入新的ARR值到预装载寄存器它只会在下一个更新事件时被加载到影子寄存器中生效从而保证当前周期的完整性。HAL库的__HAL_TIM_SET_AUTORELOAD()宏就是安全的。// 安全地动态改变PWM频率将频率改为2kHz __HAL_TIM_SET_AUTORELOAD(htim3, 499); // 新的ARR值 1MHz / (4991) 2kHz // 注意改变ARR后如果占空比CCR值不变其实际占空比会变化。通常需要同步按比例调整CCR。 uint32_t new_period __HAL_TIM_GET_AUTORELOAD(htim3) 1; uint32_t new_pulse (htim3.Instance-CCR1 * new_period) / (old_period 1); // 计算新CCR以保持占空比近似 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, new_pulse);5. 无PWM波形输出的终极排查指南这是新手甚至老手最常遇到的问题。配置看起来都对代码也编译通过了但用示波器或者逻辑分析仪在PA6上就是看不到波形。别慌按照以下清单从硬件到软件从外到内逐一排查99%的问题都能解决。5.1 硬件与连接检查供电与芯片确保MCU正常供电已经正常启动。可以点个LED灯测试。引脚连接确认你的示波器探头或逻辑分析仪夹子确实接触到了正确的引脚PA6并且地线连接良好。引脚复用冲突STM32的引脚有多个复用功能。检查PA6是否被其他外设如SPI1_MISO, ADC等占用。在CubeMX的引脚图中如果PA6显示为黄色已配置但不是你想要的TIM3_CH1那就是冲突了。特别要注意JTAG/SWD调试接口默认情况下PA13、PA14、PA15、PB3、PB4是调试引脚如果你使能了JTAGPB4也可能是TIM3_CH1的复用引脚就无法作为GPIO使用。通常我们选择“Serial Wire”调试模式它只占用PA13和PA14。GPIO模式输出PWM的引脚必须配置为复用推挽输出Alternate Function Push-Pull而不是普通的推挽输出。检查MX_GPIO_Init函数或HAL_TIM_MspPostInit函数中对相应引脚的配置。5.2 软件配置检查定时器时钟使能在RCC中对应的定时器外设时钟如__HAL_RCC_TIM3_CLK_ENABLE()必须开启。CubeMX生成的代码通常会做好但手写代码容易遗漏。定时器未启动这是最高频的错误你初始化了定时器配置了PWM通道但没有调用启动函数。必须调用HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1)。调用HAL_TIM_Base_Start只能让计数器跑起来但输出比较模块和引脚没有激活。ARR或PSC为0如果ARR设置为0那么计数器从0到0几乎没有周期。如果PSC设置过大或为0导致时钟频率极高也可能在示波器上难以稳定捕获。确保ARR和PSC是合理的值。CCR值大于ARR在PWM模式下如果初始CCR值大于ARR根据规则PWM mode 1 高电平有效输出将恒为有效电平高。你可能误以为没有波形其实是一条直线。确保CCR ARR。输出极性配置错误如果你配置了“低电平有效”但示波器探头默认在寻找上升沿触发或者你心里预期的是高电平脉冲也可能误判。检查OCPolarity设置。CubeMX代码生成后未编译修改CubeMX配置后必须点击“Generate Code”并确保成功覆盖了旧文件然后回到IDE如Keil中重新编译整个工程。有时候IDE的缓存会导致旧代码仍在运行。5.3 使用调试器进行在线诊断如果以上都没问题祭出终极武器——调试器。查看寄存器在调试模式下Keil/IAR暂停程序查看外设寄存器窗口。查看TIM3-CR1寄存器确保CEN位bit 0为1计数器已使能。查看TIM3-CCER寄存器确保对应通道如CH1的CC1E位bit 0为1输出使能。查看TIM3-CCMR1寄存器确保OC1M位段bit 6:4是110PWM mode 1或111PWM mode 2。查看TIM3-CCR1寄存器确认其值是否为你设定的值。查看TIM3-CNT寄存器看它是否在0和ARR之间循环跳动。如果不跳说明计数器没跑检查时钟和使能。查看GPIO寄存器查看GPIOA-CRL寄存器确认PA6CNF6和MODE6位段被正确配置为复用输出模式。单步调试在HAL_TIM_PWM_Start函数调用后设置断点单步执行观察是否有错误返回值。按照这个清单从硬件连接到寄存器状态层层递进几乎可以定位所有PWM无输出的问题。记住嵌入式调试耐心和系统性的排查方法比盲目尝试更重要。