定时器与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配置步骤:
- 选择TIM2,时钟源选Internal Clock。
- Channel1选PWM Generation CH1。
- 参数设置:Prescaler=71,Counter Period=499(ARR=499),Auto-reload preload使能。
- PWM脉冲:Pulse=250(CCR=250,即50%占空比)。
- 生成代码。
生成的初始化代码大概长这样:
// 定时器句柄
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输出的完整路径。我画这张图时特意把预分频、计数、比较这三个关键节点标出来了。
从这张图可以清楚看到:时钟经过预分频器降频后喂给计数器,计数器从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倍 |