嵌入式开发必掌握:定时器与PWM实战精讲(精确定时+电机控制+编码器应用)
标签:嵌入式开发、定时器、PWM、STM32、电机控制、编码器、输入捕获、精确定时、底层驱动
前言
摘要:定时器是嵌入式系统的心脏,PWM是电机控制的核心。本文从定时器工作原理、精确定时配置、PWM输出控制、输入捕获测速、编码器接口应用到电机闭环控制实战,提供全套可直接量产的驱动代码,同时总结定时器配置常见踩坑、PWM频率精度优化、编码器信号抗干扰方案,帮助开发者掌握高效可靠的定时器与PWM编程。
定时器是MCU最基础也是最重要的外设之一,广泛应用于精确定时、PWM控制、脉冲测量、编码器接口等场景。看似简单的定时器配置,实际开发中却容易遇到定时精度不够、PWM频率偏差、编码器计数错误等问题。
文章主要内容:
- 定时器工作原理与架构
- 精确定时配置方法
- PWM输出控制实战
- 输入捕获测速应用
- 编码器接口应用
- 电机控制实战项目
一、定时器工作原理与架构
1.1 STM32定时器分类
STM32定时器分为三类:
定时器对比表:
| 类型 | 计数器位宽 | 通道数 | 特殊功能 | 典型应用 |
|---|---|---|---|---|
| 高级定时器 | 16位 | 4 | 死区、刹车、编码器 | 电机控制、PWM |
| 通用定时器 | 16/32位 | 4 | 输入捕获、编码器 | 定时、PWM、测速 |
| 基本定时器 | 16位 | 0 | DAC触发 | 简单定时 |
1.2 定时器核心寄存器
typedef struct
{
volatile uint32_t CR1;
volatile uint32_t CR2;
volatile uint32_t SMCR;
volatile uint32_t DIER;
volatile uint32_t SR;
volatile uint32_t EGR;
volatile uint32_t CCMR1;
volatile uint32_t CCMR2;
volatile uint32_t CCER;
volatile uint32_t CNT;
volatile uint32_t PSC;
volatile uint32_t ARR;
volatile uint32_t RCR;
volatile uint32_t CCR1;
volatile uint32_t CCR2;
volatile uint32_t CCR3;
volatile uint32_t CCR4;
volatile uint32_t BDTR;
volatile uint32_t DCR;
volatile uint32_t DMAR;
} TIM_TypeDef;
关键寄存器说明:
| 寄存器 | 作用 | 说明 |
|---|---|---|
| PSC | 预分频器 | 时钟分频系数 |
| ARR | 自动重装载值 | 定时周期 |
| CNT | 计数器当前值 | 实时计数值 |
| CCRx | 捕获/比较值 | PWM占空比 |
| DIER | 中断使能 | 配置中断源 |
1.3 定时器时钟源
时钟源选择:
typedef enum {
TIM_CLOCKSOURCE_INTERNAL = 0x00,
TIM_CLOCKSOURCE_ITR0 = 0x01,
TIM_CLOCKSOURCE_ITR1 = 0x02,
TIM_CLOCKSOURCE_ITR2 = 0x03,
TIM_CLOCKSOURCE_TI1ED = 0x04,
TIM_CLOCKSOURCE_TI1 = 0x05,
TIM_CLOCKSOURCE_TI2 = 0x06,
TIM_CLOCKSOURCE_ETRMODE1 = 0x07,
TIM_CLOCKSOURCE_ETRMODE2 = 0x08
} tim_clocksource_t;
二、精确定时配置方法
2.1 定时周期计算
定时周期公式:
T = (PSC + 1) × (ARR + 1) / Fclk
其中:
- T:定时周期(秒)
- PSC:预分频系数
- ARR:自动重装载值
- Fclk:定时器时钟频率
示例:1ms定时配置(系统时钟72MHz):
void tim2_1ms_init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
TIM2->PSC = 72 - 1;
TIM2->ARR = 1000 - 1;
TIM2->DIER |= TIM_DIER_UIE;
TIM2->CR1 |= TIM_CR1_CEN;
NVIC_SetPriority(TIM2_IRQn, 5);
NVIC_EnableIRQ(TIM2_IRQn);
}
2.2 微秒级精确定时
微秒延时函数:
void delay_us(uint32_t us)
{
uint32_t start = TIM2->CNT;
uint32_t wait = us;
while ((TIM2->CNT - start) < wait) {
}
}
高精度定时器配置:
void tim2_us_timer_init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
TIM2->PSC = 72 - 1;
TIM2->ARR = 0xFFFFFFFF;
TIM2->CR1 |= TIM_CR1_CEN;
}
2.3 定时器中断处理
中断服务函数:
volatile uint32_t timer_count = 0;
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR = ~TIM_SR_UIF;
timer_count++;
if (timer_count % 1000 == 0) {
led_toggle();
}
}
}
多定时任务管理:
typedef struct {
uint32_t period;
uint32_t counter;
void (*handler)(void);
uint8_t enable;
} timer_task_t;
#define MAX_TIMER_TASKS 10
static timer_task_t timer_tasks[MAX_TIMER_TASKS];
int register_timer_task(uint32_t period, void (*handler)(void))
{
for (int i = 0; i < MAX_TIMER_TASKS; i++) {
if (!timer_tasks[i].enable) {
timer_tasks[i].period = period;
timer_tasks[i].counter = 0;
timer_tasks[i].handler = handler;
timer_tasks[i].enable = 1;
return i;
}
}
return -1;
}
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR = ~TIM_SR_UIF;
for (int i = 0; i < MAX_TIMER_TASKS; i++) {
if (timer_tasks[i].enable) {
timer_tasks[i].counter++;
if (timer_tasks[i].counter >= timer_tasks[i].period) {
timer_tasks[i].counter = 0;
if (timer_tasks[i].handler) {
timer_tasks[i].handler();
}
}
}
}
}
}
三、PWM输出控制实战
3.1 PWM原理
PWM(脉冲宽度调制):通过改变脉冲宽度控制平均输出电压。
PWM频率与占空比:
频率 = Fclk / ((PSC + 1) × (ARR + 1))
占空比 = CCRx / (ARR + 1) × 100%
3.2 PWM输出配置
基础PWM配置:
void pwm_init(TIM_TypeDef *tim, uint32_t channel,
uint32_t frequency, uint32_t duty_cycle)
{
uint32_t psc = 72 - 1;
uint32_t arr = (72000000 / (psc + 1) / frequency) - 1;
uint32_t ccr = (arr + 1) * duty_cycle / 100;
if (tim == TIM1) RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
else if (tim == TIM2) RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
else if (tim == TIM3) RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
tim->PSC = psc;
tim->ARR = arr;
uint32_t shift = (channel - 1) * 8;
if (channel <= 2) {
tim->CCMR1 &= ~(0xFF << shift);
tim->CCMR1 |= (0x68 << shift);
} else {
shift = (channel - 3) * 8;
tim->CCMR2 &= ~(0xFF << shift);
tim->CCMR2 |= (0x68 << shift);
}
tim->CCER |= (1 << ((channel - 1) * 4));
if (tim == TIM1) {
tim->BDTR |= TIM_BDTR_MOE;
}
tim->CCR1 = (channel == 1) ? ccr : tim->CCR1;
tim->CCR2 = (channel == 2) ? ccr : tim->CCR2;
tim->CCR3 = (channel == 3) ? ccr : tim->CCR3;
tim->CCR4 = (channel == 4) ? ccr : tim->CCR4;
tim->CR1 |= TIM_CR1_CEN;
}
3.3 PWM占空比调节
动态调节占空比:
void pwm_set_duty(TIM_TypeDef *tim, uint32_t channel, uint32_t duty_cycle)
{
uint32_t arr = tim->ARR;
uint32_t ccr = (arr + 1) * duty_cycle / 100;
switch (channel) {
case 1:
tim->CCR1 = ccr;
break;
case 2:
tim->CCR2 = ccr;
break;
case 3:
tim->CCR3 = ccr;
break;
case 4:
tim->CCR4 = ccr;
break;
}
}
呼吸灯效果:
void breathing_led(void)
{
static uint8_t brightness = 0;
static uint8_t direction = 1;
brightness += direction * 2;
if (brightness >= 100) {
direction = 0;
} else if (brightness <= 0) {
direction = 1;
}
pwm_set_duty(TIM2, 1, brightness);
}
3.4 多路PWM同步输出
四路PWM配置:
void multi_pwm_init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
TIM3->PSC = 72 - 1;
TIM3->ARR = 1000 - 1;
TIM3->CCMR1 = (0x68 << 0) | (0x68 << 8);
TIM3->CCMR2 = (0x68 << 0) | (0x68 << 8);
TIM3->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E |
TIM_CCER_CC3E | TIM_CCER_CC4E;
TIM3->CCR1 = 250;
TIM3->CCR2 = 500;
TIM3->CCR3 = 750;
TIM3->CCR4 = 1000;
TIM3->CR1 |= TIM_CR1_CEN;
}
四、输入捕获测速应用
4.1 输入捕获原理
输入捕获:捕获外部信号边沿,记录定时器计数值。
4.2 频率测量
频率测量配置:
typedef struct {
uint32_t last_count;
uint32_t frequency;
uint8_t capture_flag;
} frequency_meter_t;
static frequency_meter_t freq_meter = {0};
void input_capture_init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
GPIOA->MODER &= ~(0x03 << 0);
GPIOA->MODER |= (0x02 << 0);
GPIOA->AFRL &= ~(0x0F << 0);
GPIOA->AFRL |= (0x01 << 0);
TIM2->PSC = 72 - 1;
TIM2->ARR = 0xFFFFFFFF;
TIM2->CCMR1 = 0x01;
TIM2->CCER = TIM_CCER_CC1E | TIM_CCER_CC1P;
TIM2->DIER = TIM_DIER_CC1IE;
TIM2->CR1 = TIM_CR1_CEN;
NVIC_SetPriority(TIM2_IRQn, 5);
NVIC_EnableIRQ(TIM2_IRQn);
}
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_CC1IF) {
TIM2->SR = ~TIM_SR_CC1IF;
uint32_t current_count = TIM2->CCR1;
if (freq_meter.capture_flag) {
uint32_t period = current_count - freq_meter.last_count;
freq_meter.frequency = 1000000 / period;
}
freq_meter.last_count = current_count;
freq_meter.capture_flag = 1;
}
}
4.3 脉宽测量
脉宽测量配置:
typedef struct {
uint32_t rise_count;
uint32_t fall_count;
uint32_t pulse_width;
uint8_t state;
} pulse_meter_t;
static pulse_meter_t pulse_meter = {0};
void pulse_width_measure_init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
TIM2->PSC = 72 - 1;
TIM2->ARR = 0xFFFFFFFF;
TIM2->CCMR1 = 0x01;
TIM2->CCER = TIM_CCER_CC1E;
TIM2->DIER = TIM_DIER_CC1IE;
TIM2->CR1 = TIM_CR1_CEN;
NVIC_SetPriority(TIM2_IRQn, 5);
NVIC_EnableIRQ(TIM2_IRQn);
}
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_CC1IF) {
TIM2->SR = ~TIM_SR_CC1IF;
uint32_t current_count = TIM2->CCR1;
if (GPIOA->IDR & 0x01) {
pulse_meter.rise_count = current_count;
TIM2->CCER |= TIM_CCER_CC1P;
} else {
pulse_meter.fall_count = current_count;
pulse_meter.pulse_width = pulse_meter.fall_count - pulse_meter.rise_count;
TIM2->CCER &= ~TIM_CCER_CC1P;
}
}
}
五、编码器接口应用
5.1 编码器原理
增量式编码器:输出A、B两路正交脉冲信号。
5.2 编码器接口配置
STM32编码器模式配置:
void encoder_interface_init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
GPIOA->MODER &= ~((0x03 << 6) | (0x03 << 7));
GPIOA->MODER |= ((0x02 << 6) | (0x02 << 7));
GPIOA->AFRL &= ~((0x0F << 24) | (0x0F << 28));
GPIOA->AFRL |= ((0x02 << 24) | (0x02 << 28));
TIM3->SMCR = 0x03;
TIM3->CCMR1 = (0x01 << 0) | (0x01 << 8);
TIM3->CCER = 0;
TIM3->ARR = 0xFFFF;
TIM3->CNT = 0;
TIM3->CR1 = TIM_CR1_CEN;
}
int32_t get_encoder_count(void)
{
return (int16_t)TIM3->CNT;
}
int32_t get_encoder_speed(void)
{
static int32_t last_count = 0;
int32_t current_count = get_encoder_count();
int32_t speed = current_count - last_count;
last_count = current_count;
return speed;
}
5.3 编码器方向检测
方向判断:
typedef enum {
ENCODER_DIR_CW = 0,
ENCODER_DIR_CCW = 1
} encoder_dir_t;
encoder_dir_t get_encoder_direction(void)
{
if (TIM3->CR1 & TIM_CR1_DIR) {
return ENCODER_DIR_CCW;
} else {
return ENCODER_DIR_CW;
}
}
六、电机控制实战项目
6.1 项目需求
实现直流电机闭环控制系统:
- PWM调速控制
- 编码器速度反馈
- PID速度闭环
- 串口参数调整
6.2 PID控制器实现
typedef struct {
float kp;
float ki;
float kd;
float target;
float integral;
float last_error;
float output_limit;
} pid_controller_t;
void pid_init(pid_controller_t *pid, float kp, float ki, float kd, float limit)
{
pid->kp = kp;
pid->ki = ki;
pid->kd = kd;
pid->integral = 0;
pid->last_error = 0;
pid->output_limit = limit;
}
float pid_compute(pid_controller_t *pid, float feedback)
{
float error = pid->target - feedback;
pid->integral += error;
if (pid->integral > pid->output_limit / pid->ki) {
pid->integral = pid->output_limit / pid->ki;
} else if (pid->integral < -pid->output_limit / pid->ki) {
pid->integral = -pid->output_limit / pid->ki;
}
float derivative = error - pid->last_error;
float output = pid->kp * error + pid->ki * pid->integral + pid->kd * derivative;
if (output > pid->output_limit) {
output = pid->output_limit;
} else if (output < -pid->output_limit) {
output = -pid->output_limit;
}
pid->last_error = error;
return output;
}
6.3 电机控制主循环
static pid_controller_t speed_pid;
void motor_control_init(void)
{
pwm_init(TIM2, 1, 10000, 0);
encoder_interface_init();
pid_init(&speed_pid, 2.0f, 0.1f, 0.01f, 100.0f);
}
void motor_set_speed(float target_rpm)
{
speed_pid.target = target_rpm;
}
void motor_control_loop(void)
{
static uint32_t last_time = 0;
uint32_t current_time = get_tick();
if (current_time - last_time >= 10) {
last_time = current_time;
int32_t encoder_count = get_encoder_count();
float current_rpm = encoder_count * 60.0f / (10 * 1000);
float pid_output = pid_compute(&speed_pid, current_rpm);
uint32_t pwm_duty = (uint32_t)(50 + pid_output / 2);
if (pwm_duty > 100) pwm_duty = 100;
if (pwm_duty < 0) pwm_duty = 0;
pwm_set_duty(TIM2, 1, pwm_duty);
}
}
6.4 串口参数调整
void uart_command_handler(uint8_t *data, uint32_t length)
{
if (strncmp((char *)data, "SPEED:", 6) == 0) {
float speed = atof((char *)(data + 6));
motor_set_speed(speed);
printf("Set speed: %.2f RPM\n", speed);
}
else if (strncmp((char *)data, "PID:", 4) == 0) {
float kp, ki, kd;
sscanf((char *)(data + 4), "%f,%f,%f", &kp, &ki, &kd);
pid_init(&speed_pid, kp, ki, kd, 100.0f);
printf("Set PID: Kp=%.2f, Ki=%.2f, Kd=%.2f\n", kp, ki, kd);
}
else if (strncmp((char *)data, "INFO", 4) == 0) {
printf("Speed: %.2f RPM\n", speed_pid.target);
printf("PID: Kp=%.2f, Ki=%.2f, Kd=%.2f\n",
speed_pid.kp, speed_pid.ki, speed_pid.kd);
}
}
七、定时器踩坑总结
7.1 常见踩坑
踩坑1:定时精度不够
问题现象:
- 定时周期偏差大
- 定时不稳定
问题分析:
- 预分频和ARR配置不合理
- 时钟源选择错误
- 中断响应延迟
解决方案:
void precise_timer_config(uint32_t period_us)
{
uint32_t psc = 72 - 1;
uint32_t arr = (period_us * (72000000 / (psc + 1)) / 1000000) - 1;
TIM2->PSC = psc;
TIM2->ARR = arr;
}
踩坑2:PWM频率偏差
问题现象:
- PWM频率与预期不符
- 占空比不准确
问题分析:
- 计算公式错误
- 时钟频率配置错误
解决方案:
void pwm_frequency_verify(uint32_t target_freq)
{
uint32_t actual_freq = 72000000 / ((TIM2->PSC + 1) * (TIM2->ARR + 1));
if (abs(actual_freq - target_freq) > target_freq * 0.01) {
printf("PWM频率偏差: 目标=%d, 实际=%d\n", target_freq, actual_freq);
}
}
踩坑3:编码器计数错误
问题现象:
- 编码器计数跳变
- 方向判断错误
问题分析:
- 信号干扰
- 编码器模式配置错误
解决方案:
void encoder_filter_config(void)
{
TIM3->SMCR |= (0x03 << 0);
TIM3->CCMR1 |= (0x03 << 2) | (0x03 << 10);
}
7.2 本章小结
定时器和PWM是嵌入式开发的核心外设,掌握其配置方法和应用技巧至关重要。
关键要点:
- 定时精度:合理配置PSC和ARR
- PWM控制:理解频率和占空比计算
- 输入捕获:精确测量外部信号
- 编码器接口:自动解码正交信号
八、延伸阅读
本文是"嵌入式开发必掌握"系列第七篇,相关文章:
- ARM架构与启动流程:理解从上电到main()的过程
- GPIO与外部中断:掌握基础外设配置
- 本文:定时器与PWM:精确定时和电机控制
- 通信接口开发:UART、SPI、I2C、CAN
下一篇文章将讲解"通信接口开发",深入理解串口、SPI、I2C、CAN通信。
相关文章推荐:
- 嵌入式开发必掌握:ARM架构与启动流程全解析
- 嵌入式开发必掌握:GPIO与外部中断实战
- 嵌入式开发必掌握:数据结构与算法的实战应用
【投票】你开发中最常用的定时器功能是?
- 精确定时(任务调度)
- PWM输出(电机/LED控制)
- 输入捕获(频率/脉宽测量)
- 编码器接口(电机测速)
如果觉得文章有帮助,欢迎点赞、收藏、关注!
互动交流:
- 你在定时器配置时遇到过哪些问题?
- PWM控制有什么优化技巧?
- 欢迎评论区分享经验,我会逐条回复。
5958

被折叠的 条评论
为什么被折叠?



