4、定时器与PWM输出:STM32定时器架构、PWM生成原理、配置PWM驱动舵机、控制分拣挡板角度

各位同学,欢迎来到第四章。这一章我们要啃一块硬骨头——定时器与PWM。说实话,在分拣机项目里,挡板能不能精准地「啪」一下打到指定角度,全靠这玩意儿。我当年第一次调舵机,愣是让机械臂在那抽风了半小时,后来才发现是PWM频率没算对。嗯,咱们今天就把这个坑填平。

4.1 STM32定时器架构:不只是个计数器

很多人觉得定时器就是个「数数」的东西。其实没那么简单。STM32的定时器家族分三档:基本定时器、通用定时器、高级定时器。咱们分拣机里驱动舵机,用通用定时器就绰绰有余。

我习惯把定时器想象成一个「带闹钟的秒表」。它内部有几个关键部件:

  • 时基单元:包含预分频器(PSC)、自动重装载寄存器(ARR)、计数器(CNT)。说白了,PSC决定时钟走多快,ARR决定走多远,CNT就是当前走到哪了。
  • 捕获/比较通道:这是PWM的核心。每个通道可以独立输出波形。
  • 输出控制逻辑:决定什么时候拉高、什么时候拉低电平。

核心公式(请刻在脑子里):

PWM频率 = 定时器时钟 / ( (PSC+1) * (ARR+1) )

占空比 = 比较值 / (ARR+1) × 100%

举个例子。我用TIM2,时钟是72MHz。想要50Hz的PWM(舵机标准频率),怎么算?

// 目标:50Hz PWM
// 72MHz / (PSC+1) / (ARR+1) = 50Hz
// 我习惯先定PSC,再算ARR

// 方案一:PSC=71,则计数时钟=1MHz
// ARR = 1MHz / 50 - 1 = 19999
// 这样占空比调节范围是0~20000,精度很高

// 方案二:PSC=1439,则计数时钟=50KHz
// ARR = 50000 / 50 - 1 = 999
// 精度稍差,但适合快速验证

// 我个人推荐方案一,精度够用,计算也方便

我的小习惯:PSC尽量选整数,ARR留出余量。比如算出来ARR=19999,我可能会设成20000,这样占空比0~20000对应0°~180°,映射起来特别顺。

4.2 PWM生成原理:方波里的秘密

PWM,脉冲宽度调制。说白了就是「用方波的宽度传递信息」。你想想看,一个周期里,高电平占多少时间,低电平占多少时间,这个比例就叫占空比。

STM32生成PWM有两种模式:

  • 向上计数模式:CNT从0数到ARR,然后归零。比较值在中间某个点翻转电平。
  • 中央对齐模式:CNT先向上数到ARR,再向下数到0。这种模式产生的PWM对称性更好,但频率减半。

舵机一般用向上计数模式。为什么?因为简单,而且50Hz的频率下,对称性不是关键。

我曾经踩过的坑:一开始没注意TIM的时钟源。STM32的定时器时钟可能来自APB1或APB2,这两个总线的时钟频率可能不同。比如APB1最大36MHz,APB2最大72MHz。如果你把定时器挂在APB1上,却按72MHz算PSC,那出来的频率就全错了。舵机要么不动,要么乱抖。

4.3 配置PWM驱动舵机:手把手实操

好,理论讲完了,咱们直接上代码。我用的是STM32F103C8T6,TIM2的通道1输出PWM,PA0引脚。

舵机控制其实就一个关键点:脉宽决定角度。标准舵机:

角度 脉宽(高电平时间) 占空比(50Hz下)
0.5ms 2.5%
90° 1.5ms 7.5%
180° 2.5ms 12.5%

嗯,这里要注意:不同品牌的舵机,脉宽范围可能略有差异。我遇到过一种舵机,0°对应0.6ms,180°对应2.4ms。所以拿到舵机先看手册,别想当然。

// 定时器初始化代码
void TIM2_PWM_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;

    // 1. 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // 2. 配置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);

    // 3. 配置定时器时基
    TIM_TimeBaseStructure.TIM_Period = 19999;        // ARR = 20000-1
    TIM_TimeBaseStructure.TIM_Prescaler = 71;        // PSC = 72-1
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    // 4. 配置PWM模式1
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 1500;            // 初始占空比:1.5ms -> 90°
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);

    // 5. 使能定时器
    TIM_Cmd(TIM2, ENABLE);
}

// 设置舵机角度函数
void Servo_SetAngle(uint16_t angle)
{
    // angle: 0~180
    // 脉宽范围:500~2500 (对应0.5ms~2.5ms)
    uint16_t pulse = 500 + (angle * 2000) / 180;
    TIM_SetCompare1(TIM2, pulse);
}

调试小技巧:第一次调舵机,别直接上180°。先设成90°,用手轻轻掰一下舵机臂,看它有没有阻力。如果舵机在抖,说明频率不对或者占空比超范围了。我习惯用逻辑分析仪抓一下波形,确认脉宽对不对。

4.4 控制分拣挡板角度:实战应用

分拣机的挡板,说白了就是两个位置:左和右。但实际项目中,我建议你做成多级可调。为什么?因为不同尺寸的物料,需要的挡板角度不一样。

比如分拣苹果和分拣鸡蛋,挡板开合角度能一样吗?肯定不能。所以我在代码里做了个映射表:

// 分拣挡板角度配置
typedef struct {
    uint8_t  material_type;   // 物料类型
    uint16_t angle_left;      // 左挡板角度
    uint16_t angle_right;     // 右挡板角度
} FlapConfig_t;

// 预定义配置
const FlapConfig_t flap_config[] = {
    {0, 30, 150},   // 小件:左30°,右150°
    {1, 45, 135},   // 中件:左45°,右135°
    {2, 60, 120},   // 大件:左60°,右120°
};

// 切换挡板函数
void Flap_SetPosition(uint8_t material_type, uint8_t side)
{
    // side: 0=左, 1=右
    uint16_t angle;
    
    if(side == 0) {
        angle = flap_config[material_type].angle_left;
    } else {
        angle = flap_config[material_type].angle_right;
    }
    
    Servo_SetAngle(angle);
    delay_ms(500);  // 等待舵机到位
}

我曾经犯过的错:一开始没加延时,连续切换挡板时舵机还没到位就发下一个指令,结果挡板卡在半路。后来我加了个状态检测,用定时器捕获舵机反馈信号,确认到位了再执行下一步。不过对于入门项目,500ms延时够用了。

还有一个细节:挡板归零。每次上电,我建议先让舵机回到一个已知位置(比如90°),然后再根据物料类型调整。否则上电瞬间舵机可能乱跳,机械结构容易受损。

4.5 本章小结

这一章内容不少,但核心就三点:

  1. 定时器配置:PSC和ARR算清楚,时钟源别搞错。
  2. PWM生成:向上计数模式,占空比映射到脉宽。
  3. 舵机控制:脉宽决定角度,记得加延时或状态检测。

下一章咱们要讲传感器数据采集了。分拣机能不能「看见」物料,就看那章了。到时候我会分享一个我调试光电传感器时差点把板子烧了的经历...嗯,先卖个关子。

各位,动手试试吧。把舵机接上,写个程序让它从0°转到180°,再转回来。看着舵机臂来回摆动,那种成就感,比打游戏爽多了。