定时器与PWM生成原理:深入理解STM32通用定时器与高级定时器的结构,掌握PWM输出模式配置,使用CubeMX生成PWM初始化代码。

各位同学,咱们今天来啃一块硬骨头——定时器与PWM。说实话,电机控制里PWM就是命根子。没有它,电机就是个铁疙瘩。我刚开始做电机项目那会儿,总觉得定时器嘛,不就是数数吗?结果一上电机,波形乱跳,电机嗡嗡响,查了三天才发现是定时器配置踩了坑。今天咱们就把这事彻底捋清楚。

一、STM32定时器家族:谁负责什么活?

STM32的定时器分三类:基本定时器、通用定时器、高级定时器。电机控制里,我们主要跟通用和高级打交道。

定时器类型 主要用途 电机控制中的角色
基本定时器(TIM6/7) 基础定时、触发DAC 一般不用在PWM输出
通用定时器(TIM2~5) PWM输出、输入捕获、编码器接口 直流有刷/无刷电机的单路PWM
高级定时器(TIM1/8) 带死区插入的互补PWM、刹车功能 三相无刷电机(BLDC/PMSM)的六步换相或FOC

我个人习惯:做直流有刷电机,用通用定时器就够了。做三相无刷,必须上高级定时器。为什么?因为高级定时器自带硬件死区插入和刹车输入,这些在电机堵转或过流时能救命。

二、定时器的核心结构:计数器、预分频器、自动重装载

说白了,定时器就是个不断计数的硬件。它由三个关键部件组成:

  • 计数器(CNT):每来一个时钟脉冲就加1(或减1)。
  • 预分频器(PSC):把定时器时钟分频后再送给计数器。比如72MHz时钟,PSC=71,计数器就得到1MHz的计数频率。
  • 自动重装载寄存器(ARR):计数器计到ARR的值后,归零重新开始。这个值决定了PWM的周期。

举个例子:你要产生20kHz的PWM,定时器时钟72MHz。先设PSC=71,得到1MHz计数频率。再设ARR=50,计数器从0数到50,正好50个脉冲,1MHz/50=20kHz。嗯,这里要注意:ARR的值决定了PWM分辨率。ARR=50,占空比只能调51级(0~50)。如果ARR=500,分辨率就高10倍。

关键公式:

PWM频率 = 定时器时钟 / ( (PSC+1) * (ARR+1) )

PWM分辨率 = ARR + 1 (级数)

三、PWM输出模式:边沿对齐 vs 中央对齐

STM32的PWM有两种模式:边沿对齐模式和中央对齐模式。我见过不少新手在这上面栽跟头。

模式 计数方式 特点 适用场景
边沿对齐模式1 向上计数 CNT < CCR时输出有效电平 通用PWM,简单高效
边沿对齐模式2 向上计数 CNT > CCR时输出有效电平 与模式1互补,少用
中央对齐模式1/2/3 先向上再向下 每个周期产生两次更新事件 FOC控制、需要对称PWM的场合

做电机控制,我建议优先用边沿对齐模式1。为什么?因为它的更新频率就是PWM频率,计算简单。中央对齐模式虽然对称性好,但更新频率翻倍,对CPU中断负担大。除非你在做FOC电流环,需要对称PWM来降低谐波,否则别自找麻烦。

四、高级定时器的独门绝技:死区插入与互补输出

三相无刷电机驱动,上下桥臂不能同时导通。一旦导通,电源直接短路,MOS管瞬间烧毁。所以需要在上下管切换时插入一段“死区时间”——两管都关断的短暂间隔。

高级定时器(TIM1/TIM8)的互补输出和死区插入是硬件自动完成的。你只需要配置:

  • CHx和CHxN:一对互补输出引脚
  • DTG(死区发生器):设置死区时间长度
  • 刹车输入(BKIN):紧急情况下强制关闭所有输出

我曾经在一个项目中,死区设得太短(只有100ns),结果MOS管驱动芯片响应慢,上下管还是出现了短暂直通。板子冒烟的那一刻,我深刻理解了“死区不是越小越好”这个道理。一般建议死区时间设为MOS管关断延迟的2~3倍。

避坑指南:我曾经在配置高级定时器时,忘了使能互补输出的自动输出功能(MOE位)。结果CHx有波形,CHxN一直低电平。电机缺相运行,抖动得像筛糠。检查了整整半天才发现是TIM1->BDTR寄存器的MOE位没置1。

五、用CubeMX生成PWM初始化代码

手写寄存器配置太容易出错了。我现在都用CubeMX生成基础代码,再根据项目微调。下面以STM32F103C8T6为例,生成一个20kHz的PWM,占空比50%。

CubeMX配置步骤:

  1. 选择TIM2,时钟源选Internal Clock。
  2. Channel1选PWM Generation CH1。
  3. 参数设置:Prescaler=71,Counter Period=499(ARR=499),Auto-reload preload使能。
  4. PWM脉冲:Pulse=250(CCR=250,即50%占空比)。
  5. 生成代码。

生成的初始化代码大概长这样:

// 定时器句柄
TIM_HandleTypeDef htim2;

// 定时器初始化函数
void MX_TIM2_Init(void)
{
  TIM_OC_InitTypeDef sConfigOC = {0};

  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 71;           // 72MHz / (71+1) = 1MHz
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = 499;             // 1MHz / (499+1) = 2kHz
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  HAL_TIM_PWM_Init(&htim2);

  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 250;               // 占空比 = 250/500 = 50%
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
}

// 启动PWM输出
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

注意看,CubeMX生成的代码里,Prescaler和Period都是减1后的值。因为硬件寄存器是从0开始计数的。你设Prescaler=71,实际分频系数是72。这个细节我见过太多人搞反了。

小技巧:调试时先用低频(比如1kHz)验证波形,再用示波器看占空比是否正确。确认无误后再改回目标频率。这样可以避免高频下看不清波形细节。

六、PWM生成的核心流程:一张图看懂

下面这张图展示了从定时器时钟到PWM输出的完整路径。我画这张图时特意把预分频、计数、比较这三个关键节点标出来了。

STM32 PWM生成核心流程 STM32 PWM生成核心流程 定时器时钟 72MHz(典型值) 预分频器 PSC 分频后时钟 = 72M/(PSC+1) 计数器 CNT 从0计数到ARR 比较器 CNT vs CCR 自动重装载 ARR 决定PWM周期 捕获比较 CCR 决定占空比 PWM输出 GPIO引脚 关键参数关系 PWM频率 = 时钟/(PSC+1)/(ARR+1) 占空比 = CCR / (ARR+1) × 100% 分辨率 = ARR + 1(级数) 边沿对齐模式:CNT < CCR 输出高

从这张图可以清楚看到:时钟经过预分频器降频后喂给计数器,计数器从0数到ARR,每数一次就和CCR比较一次。当CNT小于CCR时,输出高电平;大于等于CCR时,输出低电平。这样就在一个周期内产生了一个可调宽度的脉冲。

七、调试中的常见问题与对策

最后,我把自己这些年踩过的坑整理成一张表,希望能帮你少走弯路。

现象 可能原因 解决办法
PWM无输出 GPIO未配置为复用功能 检查CubeMX中GPIO的Alternate Function设置
PWM频率不对 PSC或ARR计算错误 用公式反算:频率=时钟/(PSC+1)/(ARR+1)
占空比无法调到0%或100% CCR值超出0~ARR范围 限制CCR在0到ARR之间
互补输出无波形 MOE位未使能 HAL_TIMEx_PWMN_Start()或手动置位BDTR的MOE
电机启动时抖动 死区时间过短 增大DTG值,建议设为MOS管关断延迟的2~3倍