3. 定时器基础与PWM输出:通用定时器TIM2/3/4配置、PWM模式详解、占空比与频率计算、实战:呼吸灯与舵机控制
各位同学,今天我们来啃一块硬骨头——通用定时器。说实话,在工业控制领域,定时器是使用频率最高的外设之一。我做过一个项目,整块板子用了6个定时器通道,分别控制电机、采集编码器、产生PWM波。如果定时器玩不转,那工业控制基本就无从谈起。
CH32系列基于RISC-V内核,它的定时器模块继承自ARM生态的成熟设计,但又有一些自己的脾气。今天我们就从TIM2/3/4这三个通用定时器入手,把PWM输出彻底讲透。
3.1 通用定时器TIM2/3/4硬件结构
先看整体框架。CH32的通用定时器是一个16位自动重装载计数器,支持向上、向下、中央对齐三种计数模式。每个定时器有4个独立通道,可以配置为输入捕获、输出比较、PWM生成或单脉冲模式。
核心寄存器速记表
| 寄存器 | 功能 | 我常用的别名 |
|---|---|---|
| TIMx_CNT | 当前计数值 | 计数器本体 |
| TIMx_PSC | 预分频器 | 降频器 |
| TIMx_ARR | 自动重装载值 | 周期天花板 |
| TIMx_CCRx | 捕获/比较值 | 阈值开关 |
| TIMx_CCMRx | 通道模式控制 | 模式选择器 |
| TIMx_CCER | 捕获/比较使能 | 通道开关 |
嗯,这里要注意:TIM2/3/4的时钟源来自APB1总线。如果APB1预分频系数不为1,定时器时钟会倍频。我刚开始调试时,算出来的频率总差一倍,查了半天才发现是这个坑。
3.2 PWM模式详解
CH32的定时器支持两种PWM模式:PWM模式1和PWM模式2。区别其实很简单——就看计数器与CCRx比较时,输出电平怎么变。
| 模式 | 向上计数 | 向下计数 |
|---|---|---|
| PWM模式1 | CNT < CCRx 时输出有效电平,否则无效 | CNT > CCRx 时输出无效电平,否则有效 |
| PWM模式2 | CNT < CCRx 时输出无效电平,否则有效 | CNT > CCRx 时输出有效电平,否则无效 |
我个人习惯用PWM模式1,配合高电平有效极性。这样理解起来最直观:CCRx的值就是高电平的宽度。你想想看,占空比 = CCRx / (ARR + 1),是不是一目了然?
经验之谈
我曾经在一个电机驱动项目里,把PWM模式1和模式2搞反了,结果电机反转。后来我养成了一个习惯:初始化时先输出一个50%占空比的波形,用示波器确认极性对不对。这个习惯帮我省了不少排查时间。
3.3 频率与占空比计算
频率计算其实就一个公式:
PWM频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)
举个例子。假设系统时钟72MHz,APB1预分频为2,那么定时器时钟就是72MHz。我想要一个20kHz的PWM,分辨率做到1000级:
ARR + 1 = 72MHz / (PSC + 1) / 20kHz
如果PSC = 0,ARR + 1 = 3600,分辨率约3600级
如果PSC = 71,ARR + 1 = 50,分辨率只有50级
这里有个权衡:PSC越大,ARR越小,频率分辨率越粗。我一般先定ARR,再反推PSC。比如想要10位分辨率(1024级),那就设ARR = 1023,然后算PSC。
注意
ARR和CCRx都是16位寄存器,最大值65535。别算超了。我见过有人把ARR设成70000,结果定时器直接不工作,查了半天寄存器才发现溢出了。
3.4 实战:呼吸灯
呼吸灯的原理很简单:不断改变PWM的占空比,从0%逐渐增加到100%,再逐渐减小到0%。人眼看起来就像灯在呼吸。
我建议用定时器更新中断来实现占空比渐变。主定时器产生PWM,另一个定时器(或者同一个定时器的另一个通道)产生更新事件,在中断里修改CCRx。
// 呼吸灯初始化
void BreathLight_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
TIM_OCInitTypeDef TIM_OCInitStructure = {0};
// 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// GPIO配置:PA6作为TIM3_CH1输出
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);
// 定时器配置:20kHz,1000级分辨率
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure = {0};
TIM_TimeBaseStructure.TIM_Period = 999; // ARR = 999
TIM_TimeBaseStructure.TIM_Prescaler = 3; // PSC = 3
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// PWM模式1配置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
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);
}
// 呼吸效果主循环
uint16_t pulse = 0;
int8_t direction = 1;
void BreathLight_Update(void)
{
TIM_SetCompare1(TIM3, pulse);
pulse += direction;
if(pulse >= 999) direction = -1;
if(pulse == 0) direction = 1;
// 延时约5ms,控制呼吸速度
Delay_Ms(5);
}
实际运行时,你会看到LED从暗到亮再到暗,周期大约2秒。你可以调整延时时间和步进值来改变呼吸速度。
3.5 实战:舵机控制
舵机控制是工业机器人、航模领域的标配。标准舵机的控制信号是50Hz的PWM,高电平宽度0.5ms~2.5ms对应0°~180°。
说白了,就是要把角度映射到CCRx值上。50Hz对应周期20ms,ARR + 1 = 20ms / (1 / 时钟频率)。
// 舵机控制参数
#define SERVO_FREQ 50 // 50Hz
#define SERVO_PERIOD_MS 20 // 20ms周期
#define SERVO_MIN_PULSE 500 // 0.5ms对应0°
#define SERVO_MAX_PULSE 2500 // 2.5ms对应180°
// 计算ARR和PSC
// 目标:50Hz,分辨率尽量高
// 时钟72MHz,PSC = 71,则计数频率1MHz
// ARR + 1 = 1MHz / 50Hz = 20000
// 所以ARR = 19999
void Servo_Init(void)
{
// ... 类似呼吸灯的初始化,但ARR = 19999,PSC = 71
TIM_TimeBaseStructure.TIM_Period = 19999;
TIM_TimeBaseStructure.TIM_Prescaler = 71;
// ...
}
// 角度转CCRx
void Servo_SetAngle(uint8_t angle)
{
// 角度范围0~180
if(angle > 180) angle = 180;
// 线性映射:0°->500us,180°->2500us
// 计数频率1MHz,1us = 1个计数
uint16_t pulse = SERVO_MIN_PULSE + (uint32_t)(SERVO_MAX_PULSE - SERVO_MIN_PULSE) * angle / 180;
TIM_SetCompare1(TIM2, pulse);
}
关键点
- 舵机PWM频率必须精确为50Hz,偏差太大会导致舵机抖动或发热
- 高电平宽度范围要严格控制在0.5ms~2.5ms,超出可能损坏舵机
- 我建议在初始化时先输出一个中位值(1.5ms对应90°),确认舵机回中
3.6 调试经验与避坑指南
做PWM调试,示波器是必备工具。没有示波器,你根本不知道波形长什么样。我曾经只用万用表调PWM,调了三天没搞定,后来借了个示波器,五分钟就发现问题了。
几个常见坑:
- GPIO复用功能没开——PWM输出需要把GPIO配置为复用推挽输出,很多人忘了这一步
- 预装载没使能——修改CCRx时,如果没使能预装载,新值不会立即生效,要等到下一个更新事件
- 极性搞反——有效电平极性配置错误,导致占空比反相
- ARR和CCRx比较逻辑——ARR = 999时,CCRx = 500对应50%占空比,但CCRx = 0对应0%,CCRx = 1000对应100%?不对,CCRx最大只能到ARR,所以100%占空比时CCRx = 1000?实际上CCRx = ARR + 1才对应100%,但寄存器只能设到ARR,所以100%占空比需要特殊处理
100%占空比的陷阱
当CCRx = ARR + 1时才是真正的100%占空比,但CCRx最大只能设到ARR。所以想要100%占空比,要么把输出极性取反然后用0%占空比,要么在ARR + 1时强制拉高GPIO。我一般用第一种方法,简单可靠。
好了,定时器PWM这部分内容就到这里。呼吸灯和舵机控制是两个非常典型的应用场景,一个展示连续渐变,一个展示精确角度控制。把这两个例子吃透,你就能应对大部分PWM相关的工业控制需求了。