第四章 定时器与PWM生成
各位同学,今天我们来聊聊电机控制里最核心的部分——定时器和PWM。说实话,我刚开始做嵌入式那会儿,总觉得定时器就是个计时的玩意儿,没什么大不了的。直到有一次做窗帘电机项目,电机转起来咔咔响,速度还忽快忽慢,我才意识到:定时器用不好,电机就玩不转。
4.1 系统滴答定时器(SysTick)实现延时
SysTick是Cortex-M内核自带的24位递减计数器。说白了,它就是个硬件计时器,专门给操作系统或者裸机程序提供心跳。我个人习惯把它当作延时工具来用,简单又可靠。
核心原理:SysTick每来一个时钟脉冲,计数值减1。减到0时,触发中断(或者置位标志位)。我们只要设置好重装载值,就能得到精确的延时。
举个例子,系统时钟72MHz,我想延时1ms:
// SysTick配置函数
void SysTick_Init(void)
{
// 重装载值 = 系统时钟 / 1000 - 1
// 72MHz / 1000 = 72000,减1是因为从0开始计数
SysTick->LOAD = 72000 - 1;
SysTick->VAL = 0; // 清空当前计数值
SysTick->CTRL = 0x01; // 使能定时器,不使能中断
}
// 毫秒级延时函数
void delay_ms(uint32_t ms)
{
for(uint32_t i = 0; i < ms; i++)
{
SysTick->VAL = 0; // 每次重新清零
// 等待COUNTFLAG置位(计数值减到0)
while(!(SysTick->CTRL & (1 << 16)));
}
}
我的经验:用SysTick做延时,记得每次都要清空VAL寄存器。我曾经踩过坑——不清VAL直接等标志位,结果第一次延时正常,第二次就翻倍了。原因?VAL在上次延时结束后可能不是0,导致计数起点不对。
4.2 通用定时器配置
SysTick虽然好用,但功能太单一。真正要控制电机,还得靠通用定时器。STM32的通用定时器(比如TIM2、TIM3)有4个独立通道,可以输出PWM、测量脉宽、做输入捕获等等。
配置通用定时器,我一般按这个步骤来:
- 使能时钟——定时器挂在APB1或APB2总线上,先开时钟
- 设置预分频器——把系统时钟分频到想要的频率
- 设置自动重装载值——决定PWM的周期
- 配置输出通道——选择PWM模式、极性等
- 使能定时器——开始干活
// 以TIM3为例,配置输出PWM
void TIM3_PWM_Init(void)
{
// 1. 使能TIM3时钟
RCC->APB1ENR |= (1 << 1);
// 2. 设置预分频器:72MHz / 72 = 1MHz
TIM3->PSC = 72 - 1;
// 3. 设置自动重装载值:1MHz / 1000 = 1KHz
TIM3->ARR = 1000 - 1;
// 4. 配置通道1为PWM模式1
TIM3->CCMR1 |= (6 << 4); // 输出比较模式:PWM模式1
TIM3->CCMR1 |= (1 << 3); // 预装载使能
TIM3->CCER |= (1 << 0); // 使能通道1输出
// 5. 使能定时器
TIM3->CR1 |= (1 << 0);
}
注意:预分频器PSC和自动重装载值ARR都是16位的,最大值65535。如果你要产生很低的频率(比如0.1Hz),需要把PSC设大一点。我遇到过有人把ARR设成100000,结果溢出后定时器直接罢工了。
4.3 PWM输出原理
PWM,全称脉冲宽度调制。你想想看,电机要转起来,需要的是电压。但单片机只能输出0或3.3V,怎么模拟出中间值?答案就是:快速开关。
PWM的核心参数就两个:
- 频率——开关的快慢。频率太低,电机会嗡嗡响;频率太高,MOS管发热严重。窗帘电机一般用1KHz~20KHz。
- 占空比——高电平时间占整个周期的比例。占空比50%,等效电压就是1.65V。
| 占空比 | 等效电压(3.3V系统) | 电机状态 |
|---|---|---|
| 0% | 0V | 停止 |
| 25% | 0.825V | 微转 |
| 50% | 1.65V | 半速 |
| 75% | 2.475V | 中速 |
| 100% | 3.3V | 全速 |
为什么会这样?因为电机是感性负载,电流不能突变。PWM波经过电机线圈的滤波作用,就变成了平滑的直流电。占空比越高,平均电压越高,电机转得越快。
4.4 电机速度控制(占空比调节)
控制电机速度,说白了就是改占空比。在STM32里,改占空比就是改捕获/比较寄存器(CCR)的值。
// 设置占空比函数
void Motor_SetSpeed(uint8_t duty)
{
// duty范围:0~100
// 对应CCR值:0~999(因为ARR=999)
uint16_t ccr_value = (uint16_t)(duty * 10);
TIM3->CCR1 = ccr_value;
}
嗯,这里要注意:占空比不能突变。我见过有人直接让电机从0%跳到100%,结果电流瞬间飙升,把MOS管烧了。正确的做法是:
- 启动时:占空比从0逐渐增加到目标值(软启动)
- 停止时:占空比从当前值逐渐减到0(软停止)
- 变速时:每次只改变5%~10%,中间加一点延时
避坑指南:我曾经在量产测试时发现,同一批电机,同样的占空比,转速居然不一样。后来查了半天,发现是电机个体差异导致的。解决办法:加一个闭环控制,用霍尔传感器测实际转速,然后微调占空比。
4.5 呼吸灯效果实现
呼吸灯,说白了就是让LED的亮度像呼吸一样慢慢变亮再慢慢变暗。这个效果在窗帘电机上有什么用?嗯,其实没什么实际用途。但用来展示PWM控制能力,效果非常直观。
实现思路很简单:
- 定时器产生PWM波,驱动LED
- 每隔一段时间改变占空比
- 占空比从0增加到100%,再从100%减到0%
- 循环往复
// 呼吸灯主循环
void BreathLight(void)
{
uint8_t duty = 0;
int8_t step = 1; // 步进方向:1为增加,-1为减少
while(1)
{
Motor_SetSpeed(duty);
delay_ms(10); // 每10ms改变一次占空比
duty += step;
// 到达边界时反转方向
if(duty >= 100)
{
step = -1;
}
else if(duty <= 0)
{
step = 1;
}
}
}
我的小技巧:呼吸灯要想效果好,步进值不能是固定的。人眼对亮度的感知不是线性的——低亮度时变化敏感,高亮度时变化迟钝。所以我会用指数曲线来映射占空比:
// 指数映射,让呼吸更自然
uint8_t duty_map[101] = {0, 1, 2, 4, 6, 9, 13, ...};
这样看起来,呼吸就柔和多了。
好了,这一章的内容就到这里。定时器和PWM是电机控制的基石,你把这些搞明白了,后面讲PID控制、速度闭环什么的,就轻松多了。下一章我们聊聊电机驱动电路——嗯,那又是另一个有意思的话题。