4. 定时器基础:通用定时器时基单元、PWM模式配置、呼吸灯实现
定时器这东西,说白了就是单片机的“心跳”。没有它,很多时序控制都玩不转。CH32V307 的定时器资源挺丰富,通用定时器(TIM2~TIM5)是我们做工业控制最常用的。
我个人习惯,拿到一个新片子,先看它的定时器框图。别急着写代码,把硬件结构摸透了,后面调参才不慌。
4.1 通用定时器时基单元
时基单元是定时器的“心脏”。它决定了定时器怎么计数、什么时候溢出、什么时候触发中断。
核心组成:
- 预分频器(PSC): 把系统时钟分频,降低计数频率。16位,可配置1~65536分频。
- 自动重装载寄存器(ARR): 设定计数上限。16位,最大65535。
- 计数器(CNT): 真正干活的寄存器,向上、向下、或中央对齐计数。
我在项目里遇到过一个问题:电机控制需要20kHz的PWM,但直接用72MHz时钟计数,ARR值太小,精度不够。后来把PSC设为72-1,计数频率降到1MHz,ARR设为50-1,刚好20kHz,分辨率也上去了。
时基单元的配置流程,我总结了一个口诀:“先时钟,再分频,后重载,清标志”。
// 时基单元初始化示例(TIM2)
void TIM2_Base_Init(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 开时钟
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz / (71+1) = 1MHz
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
TIM_TimeBaseStructure.TIM_Period = 999; // 1ms 中断周期
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update); // 清更新标志
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 使能更新中断
TIM_Cmd(TIM2, ENABLE); // 启动定时器
}
小技巧: 预分频器是 PSC+1 分频,别算错了。我刚开始就犯过这个低级错误,查了半天。
4.2 PWM模式配置
PWM(脉宽调制)是定时器最常用的功能之一。CH32V307 的通用定时器支持两种PWM模式:模式1 和 模式2。
说白了,模式1是CNT小于CCR时输出有效电平,模式2则相反。我一般用模式1,符合直觉。
| 参数 | 说明 | 典型值 |
|---|---|---|
| PSC | 预分频,决定PWM频率 | 71(1MHz计数) |
| ARR | 自动重装载,决定PWM周期 | 999(1kHz) |
| CCR | 捕获比较值,决定占空比 | 0~999 |
| Polarity | 输出极性,高/低有效 | 高有效 |
配置PWM时,有个坑要注意:CCR值不能大于ARR。我曾经在调试舵机时,不小心把CCR设成了1200,而ARR只有999,结果输出一直满占空比,舵机直接打到极限位置,差点把机械结构搞坏。
// PWM模式配置示例(TIM2_CH1,PA0)
void TIM2_PWM_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// PA0 复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 时基配置:1kHz
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Prescaler = 71;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 999;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// PWM模式1,输出高有效
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 500; // 50% 占空比
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
}
注意: 如果输出没有波形,先检查GPIO是否配置为复用功能。我见过太多人卡在这里。
4.3 呼吸灯实现
呼吸灯是PWM的经典应用。原理很简单:占空比从0逐渐增加到100%,再从100%逐渐减小到0,循环往复。
你想想看,人眼对亮度的变化不是线性的。所以直接线性改变CCR,看起来会感觉亮的时候变化快,暗的时候变化慢。我一般用指数曲线来补偿,效果更自然。
// 呼吸灯主循环
uint16_t brightness = 0;
uint8_t direction = 1; // 1: 增加, 0: 减少
while (1) {
TIM_SetCompare1(TIM2, brightness);
if (direction) {
brightness += 2;
if (brightness >= 999) {
brightness = 999;
direction = 0;
}
} else {
if (brightness <= 2) {
brightness = 0;
direction = 1;
} else {
brightness -= 2;
}
}
Delay_Ms(5); // 每5ms调整一次,控制呼吸速度
}
这段代码里,我用了 brightness += 2 而不是 += 1,是为了让呼吸节奏更快一些。实际项目中,你可以根据LED的亮度和人眼感知,调整步长和延时。
优化建议: 用定时器中断来更新CCR,而不是在主循环里死等。这样CPU可以干别的事,呼吸灯在后台自己跑。
4.4 知识体系总览
下面这张图,把定时器从时钟到输出的完整链路画出来了。你对照着看,思路会清晰很多。
4.5 避坑指南
做定时器开发,有几个坑我踩过,分享给你:
- 时钟没开: 定时器不工作,先查RCC时钟使能。我调试时经常忘了这步。
- GPIO模式不对: PWM输出必须用复用推挽输出(AF_PP),普通推挽输出不行。
- CCR溢出: CCR值不能超过ARR,否则输出会异常。
- 中断标志没清: 更新中断触发后,一定要在中断服务函数里清标志,否则会一直进中断。
- 预装载没使能: 修改ARR或CCR时,建议使能预装载功能,否则可能在计数周期中间生效,导致波形异常。
曾经有一次: 我在调试一个伺服驱动,PWM频率设成了20kHz,但电机就是抖。查了半天,发现是ARR和CCR的预装载没开,每次更新占空比时,波形会乱一个周期。开了预装载后,问题立刻解决。
嗯,定时器基础就讲到这里。你先把时基单元和PWM配置跑通,呼吸灯调到自己看着舒服的节奏。这些基础打牢了,后面做编码器接口、PWM捕获、步进电机控制,都会顺手很多。