前言
脉冲宽度调制(PWM)是嵌入式开发中最常用的技术之一,广泛应用于电机控制、LED调光、电源管理等领域。STM32的定时器外设提供了强大的PWM生成功能,而HAL库则让这一切变得更加简单。本文将带你从零开始,全面掌握使用HAL库配置定时器PWM输出的各种技巧。
一、PWM基础概念回顾
什么是PWM?
PWM(Pulse Width Modulation)即脉冲宽度调制,通过调节脉冲的占空比(高电平时间占整个周期的比例)来模拟不同电压值或控制设备功率。
关键参数:
-
频率:PWM波形的周期倒数(Hz)
-
周期:一个完整波形的时间(秒)
-
占空比:高电平时间占周期的百分比
-
分辨率:占空比可调节的最小步进
二、STM32定时器的PWM模式
STM32的通用/高级定时器支持多种PWM模式:
-
PWM模式1:向上计数时,比较匹配时输出有效电平
-
PWM模式2:向上计数时,比较匹配时输出无效电平
-
互补输出:高级定时器支持带死区控制的互补PWM输出
三、HAL库PWM配置步骤详解
步骤1:CubeMX图形化配置(推荐)
使用CubeMX可以大大简化配置过程:
-
选择定时器:例如TIM1、TIM2、TIM3等
-
选择通道:选择支持PWM输出的通道(如CH1、CH2等)
-
配置参数:
-
Prescaler(预分频器)
-
Counter Period(自动重装载值)
-
Pulse(脉冲值 - 初始占空比)
-
PWM Generation Channel(选择PWM模式)
-
步骤2:手动代码配置方法
如果你更喜欢手动配置,以下是完整流程:
cs
// 1. 定义定时器和PWM配置结构体
TIM_HandleTypeDef htim3;
TIM_OC_InitTypeDef sConfigOC = {0};
// 2. 定时器基础配置
void MX_TIM3_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 84 - 1; // 预分频值,84MHz/84 = 1MHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数模式
htim3.Init.Period = 1000 - 1; // 自动重装载值,1MHz/1000 = 1KHz
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
{
Error_Handler();
}
// 配置时钟源
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig);
// 配置PWM通道
if (HAL_TIM_PWM_Init(&htim3) != HAL_OK)
{
Error_Handler();
}
// 主模式配置
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig);
// 3. PWM通道详细配置
sConfigOC.OCMode = TIM_OCMODE_PWM1; // PWM模式1
sConfigOC.Pulse = 500; // 初始占空比50%(1000周期中的500)
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; // 输出极性:高电平有效
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
// 4. 启动PWM输出
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}
四、关键配置参数详解
1. 时钟源和频率计算
cs
// 假设系统时钟为84MHz
htim3.Init.Prescaler = 84 - 1; // 预分频值
htim3.Init.Period = 1000 - 1; // 自动重装载值
// PWM频率 = 时钟频率 / ((Prescaler + 1) * (Period + 1))
// = 84MHz / (84 * 1000) = 1000Hz = 1KHz
// PWM分辨率 = 1 / (Period + 1) = 1/1000 = 0.1%
2. 占空比设置
cs
// 设置占空比为75%
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 750); // 750/1000 = 75%
五、高级PWM配置技巧
1. 互补PWM输出(带死区控制)
cs
// 高级定时器(如TIM1、TIM8)支持互补输出
TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};
// 配置死区时间
sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
sBreakDeadTimeConfig.DeadTime = 72; // 死区时间,根据系统时钟计算
sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig);
// 启动互补通道
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); // 启动互补通道
2. 多通道同步PWM输出
cs
// 配置多个通道
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2);
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_3);
// 同时启动所有通道
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
// 同时更新所有通道的占空比
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty1);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, duty2);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, duty3);
3. PWM输入捕获联动
cs
// 配置一个通道为输入捕获,另一个为PWM输出
// 可用于测量外部PWM或实现频率同步
void MX_TIM2_Init(void)
{
TIM_IC_InitTypeDef sConfigIC = {0};
// ... 基础配置
// 通道1配置为输入捕获
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 0;
HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);
// 通道2配置为PWM输出
sConfigOC.OCMode = TIM_OCMODE_PWM1;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_2);
}
六、实用函数封装
1. PWM初始化函数
cs
typedef struct {
TIM_HandleTypeDef *htim;
uint32_t channel;
uint32_t frequency;
uint32_t resolution;
float duty_cycle;
} PWM_ConfigTypeDef;
void PWM_Init(PWM_ConfigTypeDef *pwm,
TIM_TypeDef *TIMx,
uint32_t channel,
uint32_t freq_hz,
float init_duty)
{
uint32_t tim_clk = HAL_RCC_GetPCLK1Freq() * 2; // 获取定时器时钟
// 计算预分频和周期值
uint32_t prescaler = (tim_clk / (freq_hz * 1000)) - 1; // 目标分辨率1000
uint32_t period = 1000 - 1;
pwm->htim->Instance = TIMx;
pwm->htim->Init.Prescaler = prescaler;
pwm->htim->Init.Period = period;
pwm->htim->Init.CounterMode = TIM_COUNTERMODE_UP;
pwm->htim->Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
pwm->htim->Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_PWM_Init(pwm->htim);
// 配置通道
TIM_OC_InitTypeDef sConfigOC = {
.OCMode = TIM_OCMODE_PWM1,
.Pulse = (uint32_t)(period * init_duty),
.OCPolarity = TIM_OCPOLARITY_HIGH,
.OCFastMode = TIM_OCFAST_DISABLE
};
HAL_TIM_PWM_ConfigChannel(pwm->htim, &sConfigOC, channel);
pwm->channel = channel;
pwm->frequency = freq_hz;
pwm->resolution = 1000;
pwm->duty_cycle = init_duty;
}
2. 占空比平滑调节函数
cs
void PWM_SetDutySmooth(TIM_HandleTypeDef *htim,
uint32_t channel,
float target_duty,
uint32_t steps,
uint32_t delay_ms)
{
float current_duty = (float)__HAL_TIM_GET_COMPARE(htim, channel) /
(float)(__HAL_TIM_GET_AUTORELOAD(htim) + 1);
float step = (target_duty - current_duty) / steps;
for(uint32_t i = 0; i < steps; i++)
{
current_duty += step;
uint32_t pulse = (uint32_t)(current_duty * (__HAL_TIM_GET_AUTORELOAD(htim) + 1));
__HAL_TIM_SET_COMPARE(htim, channel, pulse);
HAL_Delay(delay_ms);
}
}
七、实际应用示例
示例1:LED呼吸灯
cs
void LED_Breathing_Effect(TIM_HandleTypeDef *htim, uint32_t channel, uint32_t duration_ms)
{
uint32_t period = __HAL_TIM_GET_AUTORELOAD(htim) + 1;
uint32_t steps = 100; // 呼吸平滑度
uint32_t delay = duration_ms / (steps * 2);
// 渐亮
for(uint32_t i = 0; i <= steps; i++)
{
float duty = (float)i / steps;
__HAL_TIM_SET_COMPARE(htim, channel, (uint32_t)(duty * period));
HAL_Delay(delay);
}
// 渐暗
for(uint32_t i = steps; i > 0; i--)
{
float duty = (float)i / steps;
__HAL_TIM_SET_COMPARE(htim, channel, (uint32_t)(duty * period));
HAL_Delay(delay);
}
}
示例2:舵机角度控制
cs
// 舵机PWM标准:周期20ms(50Hz),脉宽0.5ms-2.5ms对应0-180度
void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t channel, float angle)
{
// 限制角度范围
if(angle < 0) angle = 0;
if(angle > 180) angle = 180;
// 计算脉宽(0.5ms - 2.5ms)
float pulse_width_ms = 0.5f + (angle / 180.0f) * 2.0f;
// 转换为计数值
// 假设定时器频率 = 1MHz,周期20ms对应20000计数值
uint32_t period = __HAL_TIM_GET_AUTORELOAD(htim) + 1;
uint32_t pulse = (uint32_t)(pulse_width_ms * 1000); // 1MHz时,1ms=1000计数
__HAL_TIM_SET_COMPARE(htim, channel, pulse);
}
示例3:直流电机控制
cs
// 电机控制结构体
typedef struct {
TIM_HandleTypeDef *htim;
uint32_t channel_a;
uint32_t channel_b;
uint8_t direction; // 0:停止, 1:正转, 2:反转
float speed; // 0.0-1.0
} Motor_TypeDef;
void Motor_SetSpeed(Motor_TypeDef *motor, float speed, uint8_t dir)
{
// 限制速度范围
if(speed < 0) speed = 0;
if(speed > 1.0) speed = 1.0;
uint32_t period = __HAL_TIM_GET_AUTORELOAD(motor->htim) + 1;
uint32_t pulse = (uint32_t)(speed * period);
switch(dir)
{
case 1: // 正转
__HAL_TIM_SET_COMPARE(motor->htim, motor->channel_a, pulse);
__HAL_TIM_SET_COMPARE(motor->htim, motor->channel_b, 0);
break;
case 2: // 反转
__HAL_TIM_SET_COMPARE(motor->htim, motor->channel_a, 0);
__HAL_TIM_SET_COMPARE(motor->htim, motor->channel_b, pulse);
break;
default: // 停止
__HAL_TIM_SET_COMPARE(motor->htim, motor->channel_a, 0);
__HAL_TIM_SET_COMPARE(motor->htim, motor->channel_b, 0);
break;
}
motor->speed = speed;
motor->direction = dir;
}
八、调试技巧和常见问题
1. PWM输出无信号?
cs
// 检查清单:
// 1. GPIO是否正确配置为复用推挽输出
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 必须配置为复用推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF2_TIM3; // 必须正确设置复用功能
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 2. 定时器时钟是否使能
__HAL_RCC_TIM3_CLK_ENABLE();
// 3. PWM是否已启动
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
// 4. 使用示波器或逻辑分析仪测量引脚
2. 频率/占空比不准确?
cs
// 常见原因和解决方案:
// 1. 时钟源配置错误 - 检查RCC配置
SystemClock_Config(); // 确保系统时钟配置正确
// 2. 预分频计算错误
// 使用公式验证:实际频率 = 定时器时钟 / ((Prescaler+1) * (Period+1))
// 3. 自动重载预装载未启用
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
3. PWM输出抖动?
cs
// 可能原因:
// 1. 中断干扰 - 降低中断频率或使用DMA
// 2. 电源噪声 - 添加滤波电容
// 3. 软件延迟 - 使用硬件自动更新
// 使用立即更新模式
void PWM_UpdateImmediately(TIM_HandleTypeDef *htim, uint32_t channel, uint32_t pulse)
{
// 禁用预装载
TIM_OC_InitTypeDef sConfigOC;
HAL_TIM_PWM_GetConfigChannel(htim, &sConfigOC, channel);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = pulse;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
HAL_TIM_PWM_ConfigChannel(htim, &sConfigOC, channel);
HAL_TIM_PWM_Start(htim, channel);
}
九、性能优化建议
-
使用DMA传输:对于需要频繁更新PWM占空比的场景
-
启用预装载寄存器:避免PWM输出抖动
-
合理选择分辨率:平衡精度和频率需求
-
使用高级定时器:需要互补输出或死区控制时
十、总结
通过HAL库配置STM32的PWM输出虽然步骤较多,但结构清晰、易于维护。关键是要理解:
-
时钟配置是基础,决定了PWM的频率精度
-
预分频和自动重载值决定了频率和分辨率
-
比较寄存器的值直接控制占空比
-
HAL库的函数调用顺序很重要
实际项目中,建议先使用CubeMX生成基础代码,然后根据需求进行修改和优化。对于复杂的PWM应用(如电机控制、电源管理等),需要深入理解定时器的各种工作模式和特性。