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 知识结构图

下面这张图总结了本章的核心知识体系:

高级定时器与编码器接口知识体系 高级定时器 TIM1/8 互补PWM输出 死区插入配置 刹车输入保护 正交编码器接口 模式1/2/3配置 计数器溢出处理 电机转速测量 M法/T法/M/T法 转速计算与滤波 核心设计原则 1. 死区时间需根据功率器件特性精确配置 2. 编码器计数溢出处理是可靠性的关键 3. 转速测量方法需根据速度范围选择

4.5 调试要点与常见问题

最后,分享几个我在调试中遇到的典型问题:

  1. 互补输出波形不对称:检查死区寄存器配置是否正确。我曾经把死区时间设成了0,结果上下管同时导通,MOSFET直接冒烟。嗯,那次教训很深刻。
  2. 编码器计数方向反了:交换A相和B相的输入引脚,或者在配置时交换TI1和TI2的极性。不用改硬件,软件就能搞定。
  3. 转速测量值跳动大:加一个简单的滑动平均滤波。我一般取最近4次测量值做平均,效果就很好了。
  4. 刹车功能不生效:检查BKIN引脚的配置和极性。刹车输入默认是高电平有效,如果你的传感器是低电平触发,记得在BDTR寄存器里翻转极性。

安全提醒:调试互补PWM时,强烈建议先用示波器看波形,确认死区时间正确后再接功率电路。我见过太多因为死区问题烧驱动板的案例了。安全第一,不要偷懒。