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° | 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 本章小结
这一章内容不少,但核心就三点:
- 定时器配置:PSC和ARR算清楚,时钟源别搞错。
- PWM生成:向上计数模式,占空比映射到脉宽。
- 舵机控制:脉宽决定角度,记得加延时或状态检测。
下一章咱们要讲传感器数据采集了。分拣机能不能「看见」物料,就看那章了。到时候我会分享一个我调试光电传感器时差点把板子烧了的经历...嗯,先卖个关子。
各位,动手试试吧。把舵机接上,写个程序让它从0°转到180°,再转回来。看着舵机臂来回摆动,那种成就感,比打游戏爽多了。