4、MCU外设驱动开发(二):定时器驱动——PWM生成(控制风机/边刷电机)、输入捕获(编码器测速)
好,我们接着聊定时器。上一节我们把定时器的基础定时功能讲透了,这一节要上点硬菜了——PWM生成和输入捕获。这两个东西,说白了就是扫地机运动控制的两条腿:一条腿负责“动”(控制电机转多快),另一条腿负责“感知”(知道轮子实际转了多少)。
我个人习惯把PWM和输入捕获放在一起讲,因为它们在硬件上往往共用同一个定时器的不同通道。你想想看,一个定时器既能输出PWM驱动电机,又能捕获编码器信号测速,这多省资源。我在项目中就经常这么干,一个TIM2搞定一个轮子的驱动和测速。
4.1 PWM生成:控制风机与边刷电机
PWM,脉冲宽度调制。说白了就是通过调节高电平占一个周期时间的比例,来模拟一个可变的电压。你给电机一个50%占空比的PWM,它感受到的平均电压就是电源电压的一半,转速也就差不多是一半。
扫地机里哪些地方用PWM?
- 风机电机:需要无级调速,吸力大小全靠PWM占空比控制
- 边刷电机:转速通常固定,但需要可调,适应不同地面材质
- 滚刷电机:同样需要调速,防止缠绕时堵转
4.1.1 PWM的硬件原理
定时器产生PWM的核心机制,就是自动重装载值(ARR)和比较值(CCR)的配合。ARR决定了PWM的周期,CCR决定了占空比。
举个例子:ARR设为999,定时器时钟频率为72MHz,预分频器设为71,那么计数器每计数一次是1微秒。从0数到999,周期就是1毫秒,也就是1kHz的PWM频率。如果CCR设为500,那么前500微秒输出高电平,后500微秒输出低电平,占空比就是50%。
核心公式:
PWM频率 = 定时器时钟 / (预分频器 + 1) / (自动重装载值 + 1)
占空比 = 比较值 / (自动重装载值 + 1) × 100%
4.1.2 代码实现:PWM初始化
以STM32为例,我们配置TIM2的通道1输出PWM,控制风机。嗯,这里要注意,不同MCU的库函数接口不一样,但思路完全一致。
// PWM初始化函数
void 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. 配置GPIO为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // TIM2_CH1
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 = 999; // ARR = 999
TIM_TimeBaseStructure.TIM_Prescaler = 71; // 预分频 72MHz/72 = 1MHz
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 4. 配置PWM模式
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比0%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
// 5. 使能定时器
TIM_Cmd(TIM2, ENABLE);
}
// 设置占空比函数
void PWM_SetDuty(uint16_t duty)
{
if(duty > 999) duty = 999; // 限幅保护
TIM_SetCompare1(TIM2, duty);
}
避坑指南:我曾经在项目里犯过一个低级错误——初始化PWM后忘了使能定时器,结果示波器上死活看不到波形。查了半天才发现TIM_Cmd没调用。嗯,这种低级错误,犯过一次就再也不会忘了。
4.1.3 风机与边刷电机的PWM控制策略
实际项目中,风机和边刷的PWM控制策略不太一样:
| 电机类型 | PWM频率 | 控制策略 | 注意事项 |
|---|---|---|---|
| 风机电机 | 20kHz-25kHz | PID闭环调速 | 频率太低会有啸叫声 |
| 边刷电机 | 1kHz-10kHz | 开环固定占空比 | 注意堵转保护 |
| 滚刷电机 | 10kHz-20kHz | 恒功率控制 | 检测电流防止缠绕 |
为什么风机要用20kHz以上的PWM频率?因为人耳能听到的频率范围是20Hz到20kHz。低于20kHz,电机线圈会发出尖锐的啸叫声,用户体验极差。我在第一版样机里用了10kHz,结果测试时同事说“这机器怎么跟蚊子似的嗡嗡响”……后来改到22kHz,世界清净了。
4.2 输入捕获:编码器测速
光有PWM输出还不够,你得知道电机实际转了多少。这就是编码器的活。编码器输出两路相位差90度的方波信号,通过测量这两路信号的脉冲数和相位关系,就能算出转速和转向。
输入捕获模式,就是用来精确测量外部信号频率或脉宽的。对于编码器测速,我们通常用定时器的编码器模式,它硬件上自动处理两路信号的边沿检测和计数,比纯软件轮询高效得多。
4.2.1 编码器模式原理
定时器的编码器模式,说白了就是让硬件自动根据两路输入信号的相位关系,决定计数器是加还是减。当A相超前B相时,计数器递增;B相超前A相时,计数器递减。
编码器模式有三种配置方式:
- TI1模式:只在TI1(A相)的边沿计数
- TI2模式:只在TI2(B相)的边沿计数
- TI1和TI2模式:在A相和B相的边沿都计数,分辨率最高
我个人建议用TI1和TI2模式,也就是4倍频模式。为什么?因为同样的编码器线数,4倍频能获得4倍的分辨率。比如一个500线的编码器,4倍频后每圈能产生2000个脉冲,测速精度高很多。
4.2.2 代码实现:编码器模式配置
// 编码器模式初始化
void Encoder_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置GPIO为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; // TIM3_CH1, CH2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置定时器时基
TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 16位最大计数值
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// 4. 配置编码器模式
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0F; // 滤波,防止抖动
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0F;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
// 5. 设置为编码器模式(TI1和TI2都计数)
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
// 6. 使能定时器
TIM_Cmd(TIM3, ENABLE);
}
// 读取编码器计数值
int16_t Encoder_GetCount(void)
{
return (int16_t)TIM_GetCounter(TIM3);
}
// 清零编码器计数值
void Encoder_ClearCount(void)
{
TIM_SetCounter(TIM3, 0);
}
注意:编码器模式下的计数器是16位的,范围是0到65535。如果电机高速旋转,计数器可能会溢出。我建议在定时中断里定期读取计数值,并手动清零,或者使用32位定时器。我曾经因为没处理溢出,导致机器人定位偏差越来越大,最后撞墙了……
4.2.3 速度计算与滤波
有了编码器脉冲数,怎么算速度?公式很简单:
转速(RPM) = (脉冲数 / 编码器线数 / 倍频数) × (60 / 测量周期秒数)
举个例子:编码器线数500,4倍频,每圈2000个脉冲。如果在100毫秒内测到500个脉冲,那么:
转速 = (500 / 2000) × (60 / 0.1) = 150 RPM
实际项目中,我还会加一个简单的低通滤波,防止速度值跳变太大:
// 一阶低通滤波计算速度
#define SPEED_FILTER_COEFF 0.3f // 滤波系数
float Speed_Calculate(int16_t pulse_cnt, float dt)
{
float instant_speed;
static float filtered_speed = 0.0f;
// 计算瞬时速度
instant_speed = (float)pulse_cnt / 2000.0f / dt * 60.0f;
// 一阶低通滤波
filtered_speed = filtered_speed * (1 - SPEED_FILTER_COEFF) +
instant_speed * SPEED_FILTER_COEFF;
return filtered_speed;
}
经验之谈:滤波系数怎么选?我一般这样试:先设成1.0(不滤波),看原始速度波形。如果噪声太大,就逐步减小系数。0.3到0.5之间通常比较合适。太小了响应太慢,机器人会感觉“肉肉的”。
4.3 PWM与编码器的协同工作
讲到这里,你可能已经发现了:PWM控制电机转,编码器测量电机转没转。两者配合,就能实现闭环控制。
实际扫地机中,每个驱动轮都有一个独立的PWM通道和编码器通道。左轮用TIM2的CH1输出PWM,TIM3的编码器模式测速;右轮用TIM2的CH2输出PWM,TIM4的编码器模式测速。这样两个轮子就能独立控制,实现差速转向。
嗯,这里要提醒一下:PWM和编码器的定时器不要共用同一个。为什么?因为编码器模式会占用定时器的全部四个通道,而PWM输出也需要通道资源。强行共用会导致资源冲突。我见过有人为了省定时器,把PWM和编码器放在同一个定时器上,结果调试了三天没搞定……
好了,这一节的内容就到这里。PWM生成和输入捕获是嵌入式驱动开发中的基本功,也是扫地机运动控制的核心。下一节我们会把这些知识串起来,讲一讲电机驱动的完整实现——包括PID控制器怎么写,以及怎么处理堵转、过流这些异常情况。