4. GPIO驱动基础

定时器这东西,说白了就是单片机的“心跳”。没有它,很多时序控制都玩不转。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 知识体系总览

下面这张图,把定时器从时钟到输出的完整链路画出来了。你对照着看,思路会清晰很多。

通用定时器PWM输出链路 系统时钟 72MHz 预分频器 PSC = 71 计数器 CNT 0→999 比较器 CNT vs CCR 自动重装载 ARR = 999 捕获比较 CCR = 0~999 输出控制 极性/使能 GPIO输出 PA0 PWM 关键公式 PWM频率 = 时钟 / (PSC+1) / (ARR+1) 占空比 = CCR / (ARR+1) × 100% 分辨率 = 1 / (ARR+1) × 100%

4.5 避坑指南

做定时器开发,有几个坑我踩过,分享给你:

  • 时钟没开: 定时器不工作,先查RCC时钟使能。我调试时经常忘了这步。
  • GPIO模式不对: PWM输出必须用复用推挽输出(AF_PP),普通推挽输出不行。
  • CCR溢出: CCR值不能超过ARR,否则输出会异常。
  • 中断标志没清: 更新中断触发后,一定要在中断服务函数里清标志,否则会一直进中断。
  • 预装载没使能: 修改ARR或CCR时,建议使能预装载功能,否则可能在计数周期中间生效,导致波形异常。

曾经有一次: 我在调试一个伺服驱动,PWM频率设成了20kHz,但电机就是抖。查了半天,发现是ARR和CCR的预装载没开,每次更新占空比时,波形会乱一个周期。开了预装载后,问题立刻解决。

嗯,定时器基础就讲到这里。你先把时基单元和PWM配置跑通,呼吸灯调到自己看着舒服的节奏。这些基础打牢了,后面做编码器接口、PWM捕获、步进电机控制,都会顺手很多。