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,定时器时钟会倍频。我刚开始调试时,算出来的频率总差一倍,查了半天才发现是这个坑。

通用定时器PWM输出数据流 APB1时钟 PSC预分频 CNT计数器 ARR CCRx比较器 输出极性 GPIO引脚 核心逻辑:CNT从0计数到ARR,与CCRx比较,输出电平翻转 改变ARR调整频率,改变CCRx调整占空比

3.2 PWM模式详解

CH32的定时器支持两种PWM模式:PWM模式1和PWM模式2。区别其实很简单——就看计数器与CCRx比较时,输出电平怎么变。

模式向上计数向下计数
PWM模式1CNT < CCRx 时输出有效电平,否则无效CNT > CCRx 时输出无效电平,否则有效
PWM模式2CNT < 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相关的工业控制需求了。