4. 定时器(Timer)基础:基本定时器、PWM输出、输入捕获与编码器模式
定时器,说白了就是单片机的“心脏节拍器”。GD32V的定时器资源相当丰富,从简单的定时到复杂的电机控制,全靠它。我个人习惯把定时器分成三类:基本定时器、通用定时器和高级定时器。这一章,咱们就先把最核心的四种玩法摸透。
4.1 基本定时器:最简单的“嘀嗒”
基本定时器功能最单一,就是纯粹的计数和产生中断。它没有外部引脚,不能输出PWM,也不能捕获输入。那它干嘛用?做系统时钟基准、产生周期性中断、或者给RTOS提供心跳。
GD32V的基本定时器(比如TIMER5、TIMER6)是一个16位的向上计数器。核心寄存器就几个:CTL0(控制寄存器)、CNT(当前计数值)、PSC(预分频器)、CAR(自动重装载值)。
工作流程很简单:时钟进来,先经过PSC分频,然后CNT开始从0往上加,加到CAR设定的值,就触发更新事件(溢出中断),然后CNT自动归零,重新开始。
核心公式:定时时间 = (PSC + 1) × (CAR + 1) / 定时器时钟频率
举个例子:系统时钟108MHz,APB1分频后给定时器时钟108MHz。想要1ms中断一次,PSC=107,CAR=999。算一下:(108)×(1000)/108M = 1ms。嗯,刚刚好。
我的小习惯:配置PSC时,我一般先固定PSC让计数频率为1MHz(即1us计数一次),然后CAR直接写需要的微秒数。这样调试时心里特别有数,不用每次重新算分频。
4.2 PWM输出:让LED呼吸,让电机转动
PWM(脉冲宽度调制),说白了就是通过调节高低电平的占空比来模拟模拟量。GD32V的通用定时器都支持PWM模式。
PWM的核心参数就两个:频率和占空比。频率由定时器的时钟和CAR决定,占空比由比较寄存器(CHxCV)决定。
配置PWM输出,我建议按这个步骤来:
- 使能定时器时钟——别漏了GPIO的时钟也要开
- 配置GPIO为复用推挽输出——查数据手册,选对复用功能号
- 配置定时器时基——PSC、CAR、计数模式(一般用向上计数)
- 配置输出比较模式——CHxCTL寄存器,选PWM模式0或模式1
- 设置初始占空比——写CHxCV寄存器
- 使能输出通道——CTL1寄存器里的CHxEN位
- 使能定时器——CTL0里的CEN位
// 以TIMER2_CH0(PA0)输出1kHz、50%占空比的PWM为例
rcu_periph_clock_enable(RCU_TIMER2);
rcu_periph_clock_enable(RCU_GPIOA);
gpio_af_set(GPIOA, GPIO_AF_2, GPIO_PIN_0);
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_0);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
timer_parameter_struct timer_initpara;
timer_struct_para_init(&timer_initpara);
timer_initpara.prescaler = 107; // 108MHz / 108 = 1MHz
timer_initpara.period = 999; // 1MHz / 1000 = 1kHz
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_init(TIMER2, &timer_initpara);
timer_channel_output_struct timer_ocinitpara;
timer_channel_output_struct_para_init(&timer_ocinitpara);
timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_HIGH;
timer_ocinitpara.ocstate = TIMER_CCX_ENABLE;
timer_channel_output_config(TIMER2, TIMER_CH_0, &timer_ocinitpara);
timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_0, 500); // 50%占空比
timer_channel_output_mode_config(TIMER2, TIMER_CH_0, TIMER_OC_MODE_PWM0);
timer_enable(TIMER2);
我曾经踩过的坑:配置完PWM发现输出一直是高电平或低电平,查了半天发现是GPIO的复用功能号选错了。GD32V的复用功能映射比较复杂,同一个引脚可能有多个复用功能,一定要对着数据手册的“Alternate function mapping”表格确认。
4.3 输入捕获:测量脉宽和频率
输入捕获,就是测量外部信号的脉宽或频率。原理很简单:当引脚上检测到指定的边沿(上升沿或下降沿)时,硬件会把当前CNT的值“咔嚓”一下锁存到捕获寄存器里。
测量频率,我常用的方法是:捕获两次上升沿,记下CNT值,差值就是信号周期对应的计数值。再结合定时器时钟频率,就能算出信号频率。
测量脉宽呢?捕获上升沿记下CNT1,再捕获下降沿记下CNT2,差值就是高电平脉宽。
配置步骤和PWM类似,但有几个关键区别:
- GPIO配置为浮空输入——不要上拉也不要下拉
- 选择捕获边沿——TIMER_CH_0对应CH0CTL里的CHxCAPFLT位
- 使能捕获通道——CTL0里的CHxEN位
- 使能定时器——别忘了
// 配置TIMER2_CH0为输入捕获,上升沿触发
timer_input_capture_config(TIMER2, TIMER_CH_0, TIMER_IC_POLARITY_RISING);
timer_channel_input_capture_value(TIMER2, TIMER_CH_0); // 读取捕获值
避坑指南:输入捕获时,如果信号频率很高,要注意CNT可能溢出。我曾经测一个几十kHz的信号,忘了考虑溢出,结果算出来的频率完全不对。解决办法是开启定时器的更新中断,在中断里记录溢出次数,最终周期 = 溢出次数 × (CAR+1) + 当前捕获值。
4.4 编码器模式:让电机知道转了多少圈
编码器模式,是GD32V定时器的一个高级功能。它可以直接连接增量式编码器的A、B两相输出,硬件自动解码出位置和方向。
原理是这样的:编码器转动时,A相和B相会产生相位差90°的方波。定时器通过检测两路信号的边沿顺序,就能判断旋转方向,同时根据脉冲数计算位置。
GD32V的编码器模式支持三种计数方式:
| 模式 | 计数方式 | 每转脉冲数 | 适用场景 |
|---|---|---|---|
| 模式0 | 只在TI1边沿计数 | 1倍 | 简单测速 |
| 模式1 | 只在TI2边沿计数 | 1倍 | 简单测速 |
| 模式2 | TI1和TI2边沿都计数 | 4倍 | 高精度定位 |
配置编码器模式,关键就几步:
- GPIO配置——两个通道的引脚都配成浮空输入
- 配置编码器模式——SMCFG寄存器的SMC位写0b0011
- 配置输入滤波——防止抖动误触发
- 使能定时器——CNT会随着编码器转动自动增减
// 配置TIMER2为编码器模式3(4倍频)
timer_encoder_mode_config(TIMER2, TIMER_ENCODER_MODE_3,
TIMER_IC_POLARITY_RISING, TIMER_IC_POLARITY_RISING);
timer_enable(TIMER2);
// 读取当前位置
int32_t position = (int16_t)timer_counter_read(TIMER2);
注意:编码器模式下,CNT是16位的,范围0~65535。如果电机转很多圈,CNT会溢出。我一般配合定时器的溢出中断,在中断里维护一个32位的软件计数器,这样就能记录任意圈数了。
4.5 知识结构总览
下面这张图,把定时器的四种核心模式串起来了。你可以看到,基本定时器是基础,PWM和输入捕获是通用定时器的两大核心功能,而编码器模式则是输入捕获的一种特殊应用。
4.6 调试小技巧
调试定时器,我有个百试百灵的方法:先用逻辑分析仪看波形。不管是PWM输出还是编码器输入,波形一看就知道配置对不对。如果没有逻辑分析仪,用示波器也行,但要注意探头的地线要夹好,不然波形全是噪声。
另外,GD32V的定时器寄存器比较多,我建议调试时先读一下关键寄存器的值,确认配置是否写进去了。比如PSC、CAR、CHxCV这些,读出来和预期值对比一下,很多问题就暴露了。
总结一下:定时器是嵌入式开发中最常用的外设之一。基本定时器做时基,PWM控制功率,输入捕获测量信号,编码器模式驱动电机。把这四种模式吃透了,大部分定时器相关的需求你都能搞定。