1. 项目概述
TIM(定时器)是STM32微控制器中一个极其重要的外设模块,在嵌入式开发中有着广泛的应用场景。这次我们要深入探讨的是TIM模块的输出比较功能,以及如何利用这一功能实现呼吸灯、舵机控制和直流电机驱动这三个经典应用。
作为一名嵌入式开发者,我经常需要在项目中用到这些基础但至关重要的功能模块。记得刚开始学习STM32时,光是理解定时器的各种工作模式就让我头疼不已。经过多个项目的实战积累,现在我已经能够熟练运用TIM模块解决各种实际问题。本文将分享我在输出比较功能上的实战经验,希望能帮助初学者少走弯路。
呼吸灯效果看似简单,但要用硬件定时器精准实现PWM调光,需要深入理解定时器的计数机制;舵机控制对PWM信号的脉冲宽度有严格要求,差之毫厘就会导致舵机转动异常;而直流电机的调速控制更是工业自动化中的基础需求。这三个案例由浅入深,基本涵盖了输出比较功能的典型应用场景。
2. 硬件准备与环境搭建
2.1 所需硬件清单
要实现这三个实验,我们需要准备以下硬件设备:
- STM32开发板(本文以STM32F103C8T6最小系统板为例)
- LED灯及限流电阻(用于呼吸灯实验)
- SG90舵机(常见的小型9g舵机)
- L298N电机驱动模块+直流电机
- 杜邦线若干
- USB转TTL串口模块(用于调试输出)
注意:选择L298N驱动模块时要注意其最大驱动电流,一般小型直流电机在12V/2A以内的都可以胜任。如果驱动更大功率电机,建议选用MOSFET组成的驱动电路。
2.2 开发环境配置
软件方面需要:
- Keil MDK-ARM开发环境(或STM32CubeIDE)
- STM32CubeMX图形化配置工具
- 串口调试助手(如Putty、SecureCRT等)
在开始编程前,先用STM32CubeMX进行外设初始化配置可以事半功倍。新建工程选择对应型号后,我们需要重点配置TIM模块:
- 启用TIM2(或其他可用定时器)
- 选择Clock Source为Internal Clock
- 配置Channel1为PWM Generation CH1
- 根据主频设置Prescaler和Counter Period
例如,要实现1kHz的PWM频率(周期1ms),在72MHz系统时钟下:
- Prescaler = 72-1 (将时钟分频到1MHz)
- Counter Period = 1000-1 (ARR寄存器值)
这样得到的实际频率就是1MHz/1000 = 1kHz
3. TIM输出比较原理详解
3.1 定时器基本工作原理
STM32的通用定时器主要由以下几部分组成:
- 时基单元(包括预分频器Prescaler、计数器Counter、自动重装载寄存器ARR)
- 输入捕获模块
- 输出比较模块
- 中断和DMA控制逻辑
在输出比较模式下,定时器的工作流程是:
- 计数器从0开始递增
- 当计数值与比较寄存器CCR的值匹配时,输出引脚电平翻转
- 计数值达到ARR时自动清零并重新计数
- 通过调节CCR值可以改变输出波形的占空比
3.2 PWM生成机制
PWM(脉冲宽度调制)是输出比较最典型的应用。要生成PWM信号需要配置以下参数:
- PWM频率 = 定时器时钟频率 / ((Prescaler+1)*(ARR+1))
- 占空比 = CCR / (ARR+1)
例如,要生成频率1kHz、占空比50%的PWM:
- 设定时器时钟72MHz
- 取Prescaler=71,ARR=999
- 则PWM频率=72MHz/(72*1000)=1kHz
- 设置CCR=500,得到50%占空比
3.3 STM32的PWM模式
STM32的定时器提供两种PWM模式:
- PWM模式1:CNT<CCR时通道有效,CNT≥CCR时无效
- PWM模式2:与模式1极性相反
在CubeMX中配置时,需要注意以下参数:
- Pulse:即CCR寄存器的初始值
- Output Compare Preload:建议启用,可以避免运行时修改CCR导致的毛刺
- Fast Mode:快速模式可以加快输出响应,但会增加功耗
- CH Polarity:设置输出极性,决定有效电平是高还是低
4. 呼吸灯实现
4.1 硬件连接
将LED正极通过220Ω限流电阻连接到TIM2_CH1对应的GPIO(如PA0),负极接地。LED亮度将通过PWM占空比调节实现渐变效果。
4.2 代码实现
首先在CubeMX中配置TIM2:
- 时钟源:内部时钟
- Channel1:PWM Generation CH1
- Prescaler:71 (72分频)
- Counter Period:255 (8位分辨率)
- Pulse初始值:0
生成代码后,添加呼吸灯效果实现:
c复制// 在main.c中添加
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
uint8_t brightness = 0;
int8_t direction = 1;
while (1) {
brightness += direction;
if(brightness == 0 || brightness == 255) {
direction = -direction;
}
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, brightness);
HAL_Delay(10);
}
4.3 效果优化技巧
- 非线性亮度调节:人眼对亮度的感知是非线性的,可以使用gamma校正表来优化:
c复制const uint8_t gamma_table[256] = {0,0,0,0,1,1,1,2,...};
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, gamma_table[brightness]);
- 使用定时器中断代替HAL_Delay,提高系统响应性
- 增加更多的LED,实现跑马灯等复杂效果
实测发现,当PWM频率低于100Hz时,人眼会观察到明显的闪烁现象。建议呼吸灯的PWM频率保持在200Hz以上。
5. 舵机控制实现
5.1 舵机工作原理
常见的小型舵机(如SG90)通常需要20ms周期的PWM信号,其中:
- 0.5ms脉宽对应0度位置
- 1.5ms脉宽对应90度中立位置
- 2.5ms脉宽对应180度位置
5.2 硬件连接
舵机有三根线:
- 红色:VCC(接5V)
- 棕色:GND
- 橙色:信号线(接TIM2_CH1)
注意:开发板的3.3V GPIO可以直接驱动舵机信号线,但电源最好使用外部5V供电,避免开发板稳压芯片过载。
5.3 代码实现
配置TIM2生成50Hz PWM(周期20ms):
- Prescaler:7199 (72MHz/7200=10kHz)
- Counter Period:199 (10kHz/200=50Hz)
控制代码:
c复制// 设置舵机角度函数
void Set_Servo_Angle(TIM_HandleTypeDef *htim, uint32_t Channel, uint8_t angle) {
uint16_t pulse = 50 + angle * 200 / 180; // 0.5ms-2.5ms对应50-250
__HAL_TIM_SET_COMPARE(htim, Channel, pulse);
}
// 在主循环中调用
Set_Servo_Angle(&htim2, TIM_CHANNEL_1, 90); // 设置到90度位置
5.4 调试要点
-
舵机抖动问题:可能是电源不稳定导致,建议:
- 增加1000μF以上的滤波电容
- 使用单独的5V电源供电
- 检查地线连接是否良好
-
角度不准问题:
- 不同品牌舵机的脉宽范围可能有差异
- 可以先用示波器测量实际输出波形
- 通过微调脉冲宽度参数校准
-
多舵机控制:可以使用一个定时器的多个通道同时控制多个舵机,注意总电流不要超过电源供应能力。
6. 直流电机控制
6.1 硬件设计
使用L298N驱动模块的典型连接方式:
- IN1、IN2:接STM32 GPIO,控制电机转向
- ENA:接TIM2_CH1,PWM调速
- 电机电源:建议使用7-12V外接电源
- 逻辑电源:可以接5V(与STM32共地)
6.2 代码实现
配置TIM2生成10kHz PWM(适合电机控制):
- Prescaler:71 (72MHz/72=1MHz)
- Counter Period:99 (1MHz/100=10kHz)
电机控制函数:
c复制// 电机转向控制
#define MOTOR_FORWARD() {HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_SET); \
HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_RESET);}
#define MOTOR_BACKWARD() {HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_RESET); \
HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_SET);}
#define MOTOR_STOP() {HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_RESET); \
HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_RESET);}
// 设置电机速度(0-100%)
void Set_Motor_Speed(uint8_t speed) {
uint16_t pulse = speed * 100 / 100; // 转换为CCR值
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse);
}
6.3 进阶技巧
- 加速减速控制:避免突然的速度变化导致大电流冲击
c复制void Motor_Speed_Ramp(uint8_t target_speed, uint16_t duration_ms) {
uint8_t current_speed = __HAL_TIM_GET_COMPARE(&htim2, TIM_CHANNEL_1);
int8_t step = (target_speed > current_speed) ? 1 : -1;
while(current_speed != target_speed) {
current_speed += step;
Set_Motor_Speed(current_speed);
HAL_Delay(duration_ms / abs(target_speed - current_speed));
}
}
-
电流检测:可以在电机回路中串联小电阻,通过ADC检测电压降来估算电流,实现过流保护。
-
堵转检测:监测电机转速(通过编码器或反电动势),当设定速度与实际速度差异过大时判断为堵转。
7. 常见问题与解决方案
7.1 无PWM输出
排查步骤:
- 检查GPIO是否配置为复用推挽输出
- 确认定时器时钟已使能(__HAL_RCC_TIMx_CLK_ENABLE)
- 检查是否调用了HAL_TIM_PWM_Start()
- 用示波器测量引脚信号,确认不是硬件连接问题
7.2 PWM频率不准
可能原因:
- 系统时钟配置错误,检查RCC配置
- Prescaler和ARR值计算错误
- 定时器时钟源选择不正确
7.3 电机响应异常
典型现象及解决:
- 电机不转但发热:可能H桥上下管直通,检查控制信号逻辑
- 转速不稳定:PWM频率可能过低,建议提高到10kHz以上
- 一个方向正常另一个方向不转:检查方向控制信号线连接
7.4 舵机角度偏差
校准方法:
- 用示波器测量控制信号脉宽
- 记录0度和180度时的实际脉冲宽度
- 修改代码中的映射关系:
c复制// 替换原来的线性映射
pulse = angle * (max_pulse - min_pulse) / 180 + min_pulse;
8. 性能优化建议
8.1 使用DMA更新CCR
对于需要频繁更新PWM占空比的场景(如音频输出),可以使用DMA自动传输CCR值:
c复制// 配置DMA循环传输CCR寄存器
HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_1, (uint32_t *)ccr_buffer, buffer_length);
8.2 定时器同步
多个定时器同步可以确保PWM相位对齐,在电机控制中尤为重要:
c复制// 配置主从定时器
TIM_MasterConfigTypeDef sMasterConfig = {
.MasterOutputTrigger = TIM_TRGO_UPDATE,
.MasterSlaveMode = TIM_MASTERSLAVEMODE_ENABLE
};
HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig);
TIM_SlaveConfigTypeDef sSlaveConfig = {
.SlaveMode = TIM_SLAVEMODE_TRIGGER,
.InputTrigger = TIM_TS_ITR0
};
HAL_TIM_SlaveConfigSynchronization(&htim2, &sSlaveConfig);
8.3 互补PWM输出
在电机驱动中,常需要使用互补PWM(带死区控制):
- 在CubeMX中启用互补通道
- 配置死区时间(Dead Time)
- 使用HAL_TIMEx_PWMN_Start()启动互补通道
死区时间计算:通常取100ns-1μs,具体取决于功率器件特性。太短可能导致上下管直通,太长会增加损耗。
