4、高级定时器与编码器接口:高级定时器TIM1/8特性、互补PWM与死区插入、正交编码器接口配置、实战:电机转速测量
各位做电机控制或者数字电源的同行,今天咱们来啃一块硬骨头——CH32的高级定时器。说实话,TIM1和TIM8这两个外设,是沁恒RISC-V芯片里最复杂的模块之一。我刚开始接触时也绕了不少弯路,今天把关键点掰开揉碎了讲清楚。
4.1 高级定时器TIM1/8核心特性
先看个对比表格,快速建立认知:
| 特性 | TIM1 | TIM8 | 通用定时器 |
|---|---|---|---|
| 计数器位数 | 16位 | 16位 | 16位 |
| 独立通道 | 4通道 | 4通道 | 2~4通道 |
| 互补输出 | 支持(CHx/CHxN) | 支持(CHx/CHxN) | 不支持 |
| 死区插入 | 可编程(8位寄存器) | 可编程(8位寄存器) | 不支持 |
| 刹车输入 | 支持(BKIN/BKIN2) | 支持(BKIN/BKIN2) | 不支持 |
| 编码器接口 | 支持(模式1/2/3) | 支持(模式1/2/3) | 部分支持 |
| 重复计数器 | 支持 | 支持 | 不支持 |
说白了,高级定时器就是通用定时器的「Pro Max」版本。多了互补输出、死区、刹车这些工业控制必备的功能。我做过一个伺服驱动器项目,如果没有这些硬件特性,光靠软件模拟PWM死区,CPU占用率直接飙到40%以上。
关键认知:高级定时器的核心价值在于「硬件自动化的安全机制」。死区插入、刹车保护这些功能,一旦配置好,即使CPU跑飞了,硬件依然能保证输出安全。这在工业现场是保命的设计。
4.2 互补PWM与死区插入
互补PWM,就是一对高低电平相反的PWM信号。一个控制上桥臂,一个控制下桥臂。但问题来了——功率管开关需要时间。如果上管还没完全关断,下管就导通了,那就直通短路了。
死区插入就是解决这个问题的。在上下管切换时,插入一段「同时关断」的时间。嗯,这个时间就是死区时间。
4.2.1 死区寄存器配置
CH32的死区控制寄存器是TIMx_BDTR。它的结构是这样的:
// TIMx_BDTR 寄存器位定义
// bit[7:0] : DTG[7:0] 死区发生器配置
// bit[12:8] : LOCK[1:0] 锁定配置
// bit[13] : OSSI 空闲模式输出状态
// bit[14] : OSSR 运行模式输出状态
// bit[15] : BKE 刹车输入使能
// bit[16] : BKP 刹车输入极性
// bit[17] : AOE 自动输出使能
// bit[18] : MOE 主输出使能
死区时间的计算公式:
// 如果 DTG[7:5] = 0xx
// 死区时间 = DTG[7:0] × t_dts
// 如果 DTG[7:5] = 10x
// 死区时间 = (64 + DTG[5:0]) × 2 × t_dts
// 如果 DTG[7:5] = 110
// 死区时间 = (32 + DTG[4:0]) × 8 × t_dts
// 如果 DTG[7:5] = 111
// 死区时间 = (32 + DTG[4:0]) × 16 × t_dts
举个例子,假设t_dts = 125ns(系统时钟8分频),想要500ns的死区时间:
// DTG = 500ns / 125ns = 4
// 所以 DTG[7:0] = 0x04
TIM1->BDTR = (4 << 0) | TIM_BDTR_MOE; // 使能主输出
我的经验:死区时间不是越大越好。太大会导致波形畸变,电机噪音增大。我一般从器件手册推荐值的1.5倍开始调,然后用示波器看波形,逐步减小到刚好没有直通风险为止。对于IGBT,通常1~3μs;对于MOSFET,100~500ns比较常见。
4.2.2 互补PWM配置示例
下面是一个完整的互补PWM配置代码,带死区插入:
void TIM1_ComplementaryPWM_Init(uint16_t arr, uint16_t psc, uint8_t deadtime)
{
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. GPIO配置:PA8(TIM1_CH1), PA9(TIM1_CH1N)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 时基配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = arr;
TIM_TimeBaseStructure.TIM_Prescaler = psc;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
// 4. 输出比较配置
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; // 使能互补输出
TIM_OCInitStructure.TIM_Pulse = arr / 2; // 50%占空比
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; // 互补输出极性
TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset;
TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
TIM_OC1Init(TIM1, &TIM_OCInitStructure);
// 5. 死区配置
TIM_BDTRInitTypeDef TIM_BDTRInitStructure;
TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable;
TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable;
TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_OFF;
TIM_BDTRInitStructure.TIM_DeadTime = deadtime; // 死区时间
TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable;
TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High;
TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure);
// 6. 使能主输出和计数器
TIM_CtrlPWMOutputs(TIM1, ENABLE);
TIM_Cmd(TIM1, ENABLE);
}
我曾经踩过的坑:配置完TIM1后,死活看不到互补输出。查了半天,发现是忘了调用TIM_CtrlPWMOutputs(TIM1, ENABLE)。这个函数使能主输出控制位(MOE),没有它,所有PWM输出都是高阻态。嗯,这个坑我至少帮三个同事排查过。
4.3 正交编码器接口配置
正交编码器,说白了就是两个相位差90°的脉冲信号。一个叫A相,一个叫B相。通过判断谁先跳变,就能知道电机是正转还是反转。
CH32的高级定时器支持三种编码器模式:
- 模式1:只在TI1(A相)边沿计数
- 模式2:只在TI2(B相)边沿计数
- 模式3:在TI1和TI2的每个边沿都计数(4倍频)
我个人习惯用模式3,分辨率最高。但要注意,计数器溢出频率也高了4倍,需要合理选择预分频。
4.3.1 编码器接口配置代码
void TIM1_Encoder_Init(uint16_t period)
{
// 1. GPIO配置:PA6(TIM1_BKIN), PA7(TIM1_CH1N) 作为编码器输入
// 实际编码器A相接PA6,B相接PA7
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 2. 时基配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = period; // 编码器最大计数值
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
// 3. 编码器模式配置
TIM_EncoderInterfaceConfig(TIM1,
TIM_EncoderMode_TI12, // 模式3:双沿计数
TIM_ICPolarity_Rising, // TI1极性
TIM_ICPolarity_Rising);// TI2极性
// 4. 使能计数器
TIM_Cmd(TIM1, ENABLE);
}
// 读取编码器位置
int16_t Encoder_GetCount(void)
{
return (int16_t)TIM1->CNT;
}
// 清零编码器计数值
void Encoder_ClearCount(void)
{
TIM1->CNT = 0;
}
关键点:编码器模式下,计数器是自动加减的。正转时递增,反转时递减。读取CNT寄存器时,一定要用int16_t类型,因为计数值是有符号的。我曾经见过有人用uint16_t读取,结果反转时计数值从0跳到了65535,速度计算直接爆炸。
4.4 实战:电机转速测量
好了,理论说完了,咱们来做个实战。用编码器测电机转速,这是工业控制里最基础也最常用的功能。
4.4.1 测量原理
转速测量有两种常用方法:
| 方法 | 原理 | 适用场景 | 精度 |
|---|---|---|---|
| M法(测频法) | 固定时间内计脉冲数 | 高速(>1000 RPM) | 高速时精度高 |
| T法(测周法) | 测量两个脉冲的时间间隔 | 低速(<100 RPM) | 低速时精度高 |
| M/T法 | 结合M法和T法 | 全速范围 | 综合最优 |
我这次用M法,简单实用。编码器线数500线,模式3(4倍频),所以每转脉冲数 = 500 × 4 = 2000。
4.4.2 完整实现代码
#define ENCODER_LINES 500 // 编码器线数
#define ENCODER_MULTIPLIER 4 // 倍频数(模式3)
#define PULSES_PER_REV (ENCODER_LINES * ENCODER_MULTIPLIER) // 2000
#define SPEED_MEASURE_MS 100 // 测量周期100ms
volatile int16_t g_encoder_count = 0;
volatile int16_t g_encoder_last_count = 0;
volatile float g_speed_rpm = 0.0f;
// 定时器中断:每100ms计算一次转速
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 读取当前编码器计数值
int16_t current_count = Encoder_GetCount();
// 计算脉冲差(处理溢出)
int16_t pulse_diff = current_count - g_encoder_last_count;
// 计算转速:RPM = (脉冲差 / 每转脉冲数) × (60 / 测量周期秒数)
// 测量周期100ms = 0.1s
g_speed_rpm = (float)pulse_diff / PULSES_PER_REV * 600.0f;
// 更新保存值
g_encoder_last_count = current_count;
g_encoder_count = current_count;
}
}
// 主函数中初始化
int main(void)
{
// 系统初始化
SystemCoreClockUpdate();
Delay_Init();
// 初始化编码器接口
TIM1_Encoder_Init(0xFFFF); // 最大计数值65535
// 初始化定时器2,100ms中断
TIM2_Init(1000 - 1, 144 - 1); // 假设系统时钟144MHz
while(1)
{
// 打印转速
printf("Current Speed: %.2f RPM\r\n", g_speed_rpm);
Delay_Ms(500);
}
}
避坑指南:编码器计数溢出处理是新手最容易忽略的。如果电机正转,计数值从65530增加到10,差值应该是(10 - 65530) = -65520,但实际物理上是增加了20个脉冲。我处理这个问题的办法是:用int16_t类型,利用C语言的整数溢出特性自动处理。65530 + 20 = 65550,截断为16位就是10,差值10 - 65530 = -65520,但用int16_t计算时,编译器会正确处理。嗯,这个技巧省了我不少代码量。
4.4.3 知识结构图
下面这张图总结了本章的核心知识体系:
4.5 调试要点与常见问题
最后,分享几个我在调试中遇到的典型问题:
- 互补输出波形不对称:检查死区寄存器配置是否正确。我曾经把死区时间设成了0,结果上下管同时导通,MOSFET直接冒烟。嗯,那次教训很深刻。
- 编码器计数方向反了:交换A相和B相的输入引脚,或者在配置时交换TI1和TI2的极性。不用改硬件,软件就能搞定。
- 转速测量值跳动大:加一个简单的滑动平均滤波。我一般取最近4次测量值做平均,效果就很好了。
- 刹车功能不生效:检查BKIN引脚的配置和极性。刹车输入默认是高电平有效,如果你的传感器是低电平触发,记得在BDTR寄存器里翻转极性。
安全提醒:调试互补PWM时,强烈建议先用示波器看波形,确认死区时间正确后再接功率电路。我见过太多因为死区问题烧驱动板的案例了。安全第一,不要偷懒。