1. 项目概述
最近在调试STM32F103的PWM输出功能时,发现很多新手在使用RT-Thread操作系统配置PWM时容易踩坑。本文将基于正点原子MINI STM32 V2.0开发板(STM32F103RB芯片),详细讲解如何通过RT-Thread操作系统配置三个PWM输出通道,实现LED呼吸灯效果。
这个项目看似简单,但涉及RT-Thread的设备驱动框架、STM32定时器配置、PWM参数计算等多个关键点。我在实际项目中多次调试后,总结出一套稳定可靠的配置方法,特别适合刚接触RT-Thread和STM32的开发者参考。
2. 硬件准备与原理分析
2.1 硬件连接说明
使用正点原子MINI STM32 V2.0开发板,核心芯片为STM32F103RB。我们需要使用以下引脚输出PWM信号:
- PA0:TIM2_CH1
- PA6:TIM3_CH1
- PA7:TIM3_CH2
这三个PWM输出可以通过跳线连接到板载LED DS0或DS1,实现LED亮度渐变效果。这种硬件连接方式既方便验证PWM输出功能,又能直观观察效果。
2.2 PWM工作原理
PWM(脉冲宽度调制)是通过调节脉冲信号的占空比来控制输出功率的技术。在STM32中,PWM由定时器模块产生,主要涉及以下参数:
- 时钟源:STM32的定时器时钟来自APB总线,通常为72MHz
- 预分频器(Prescaler):用于降低定时器时钟频率
- 自动重装载值(Period):决定PWM周期
- 脉冲宽度(Pulse):决定PWM占空比
计算公式:
code复制PWM频率 = 定时器时钟 / (Prescaler + 1) / (Period + 1)
占空比 = (Pulse + 1) / (Period + 1) * 100%
2.3 RT-Thread PWM设备框架
RT-Thread提供了统一的PWM设备驱动框架,开发者只需:
- 在board.h中启用对应PWM设备
- 实现硬件初始化代码
- 通过设备API控制PWM输出
这种设计使得应用代码与硬件解耦,提高了可移植性。
3. 详细实现步骤
3.1 工程环境配置
首先确保已正确安装以下工具:
- RT-Thread Studio或Env工具
- ARM GCC工具链
- STM32CubeMX(可选,用于生成初始化代码)
在RT-Thread Settings中启用PWM驱动:
code复制Hardware Drivers Config -> On-chip Peripheral Drivers -> Enable PWM
3.2 硬件初始化代码
3.2.1 board.h配置
在board.h中添加以下宏定义:
c复制#define BSP_USING_PWM2
#define BSP_USING_PWM3
#define BSP_USING_PWM2_CH1
#define BSP_USING_PWM3_CH1
#define BSP_USING_PWM3_CH2
3.2.2 board.c配置
在board.c中添加定时器和GPIO初始化代码:
c复制void HAL_TIM_MspPostInit(TIM_HandleTypeDef* htim)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(htim->Instance==TIM2) {
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
else if(htim->Instance==TIM3) {
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
3.2.3 定时器初始化
添加TIM2和TIM3的初始化函数:
c复制void MX_TIM2_Init(void)
{
TIM_HandleTypeDef htim2;
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71; // 72MHz/(71+1) = 1MHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999; // 1MHz/(999+1) = 1kHz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_PWM_Init(&htim2) != HAL_OK) {
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) {
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500; // 初始占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
if (HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) {
Error_Handler();
}
HAL_TIM_MspPostInit(&htim2);
}
TIM3的初始化类似,注意通道配置:
c复制// TIM3初始化代码
htim3.Instance = TIM3;
htim3.Init.Prescaler = 71;
htim3.Init.Period = 999;
// ...其他配置与TIM2类似
// 通道1配置
sConfigOC.Pulse = 300;
if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) {
Error_Handler();
}
// 通道2配置
sConfigOC.Pulse = 700;
if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) {
Error_Handler();
}
3.3 主程序实现
在main.c中添加PWM控制代码:
c复制#include <rtthread.h>
#include <rtdevice.h>
#define PWM_DEV_NAME1 "pwm2" // TIM2
#define PWM_DEV_NAME2 "pwm3" // TIM3
#define PWM_DEV_CH1 1 // 通道1
#define PWM_DEV_CH2 2 // 通道2
static void pwm_led_sample(void)
{
rt_device_t pwm_dev1, pwm_dev2;
struct rt_pwm_configuration cfg1, cfg2_ch1, cfg2_ch2;
rt_uint32_t pulse;
// 获取PWM设备
pwm_dev1 = rt_device_find(PWM_DEV_NAME1);
pwm_dev2 = rt_device_find(PWM_DEV_NAME2);
// 初始化PWM配置
cfg1.period = 1000; // 1kHz
cfg1.pulse = 500; // 初始占空比50%
cfg1.channel = PWM_DEV_CH1;
cfg2_ch1 = cfg2_ch2 = cfg1;
cfg2_ch1.channel = PWM_DEV_CH1;
cfg2_ch2.channel = PWM_DEV_CH2;
// 启用PWM输出
rt_device_control(pwm_dev1, PWM_CMD_ENABLE, &cfg1);
rt_device_control(pwm_dev2, PWM_CMD_ENABLE, &cfg2_ch1);
rt_device_control(pwm_dev2, PWM_CMD_ENABLE, &cfg2_ch2);
// 呼吸灯效果
while (1) {
for (pulse = 1; pulse < 1000; pulse++) {
cfg1.pulse = pulse;
rt_device_control(pwm_dev1, PWM_CMD_SET_PULSE, &cfg1);
rt_thread_mdelay(5);
}
for (pulse = 1000; pulse > 0; pulse--) {
cfg1.pulse = pulse;
rt_device_control(pwm_dev1, PWM_CMD_SET_PULSE, &cfg1);
rt_thread_mdelay(5);
}
}
}
int main(void)
{
MX_TIM2_Init();
MX_TIM3_Init();
pwm_led_sample();
return 0;
}
4. 常见问题与调试技巧
4.1 PWM无输出排查步骤
-
检查时钟配置:
- 确认APB1定时器时钟已使能(默认72MHz)
- 使用示波器测量定时器输入时钟
-
验证GPIO配置:
- 确保GPIO模式设置为复用推挽输出(GPIO_MODE_AF_PP)
- 检查GPIO时钟是否使能
-
检查定时器配置:
- 确认Prescaler和Period值计算正确
- 确保PWM模式(TIM_OCMODE_PWM1/PWM2)设置正确
-
RT-Thread设备注册:
- 检查PWM设备是否成功注册到设备框架
- 使用
list_device命令查看设备列表
4.2 参数计算技巧
-
频率选择:
- LED控制:1kHz-5kHz(避免可见闪烁)
- 电机控制:10kHz-20kHz(超出人耳范围)
-
占空比精度:
- Period值越大,占空比调节精度越高
- 但Period过大会降低PWM频率
-
推荐配置:
c复制// 1kHz PWM,占空比分辨率0.1% htim.Init.Prescaler = 71; // 72MHz/72 = 1MHz htim.Init.Period = 999; // 1MHz/1000 = 1kHz
4.3 性能优化建议
-
使用DMA:
对于需要频繁更新PWM脉冲值的应用,可以配置DMA减轻CPU负担:c复制HAL_TIM_PWM_Start_DMA(&htim, TIM_CHANNEL_1, (uint32_t*)pulse_values, count); -
互补输出:
STM32定时器支持互补输出,可用于电机驱动等场景:c复制
sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH; sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET; sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET; -
中断使用:
可以启用定时器中断,在周期结束时执行特定操作:c复制
HAL_TIM_Base_Start_IT(&htim);
5. 进阶应用示例
5.1 多通道同步控制
如果需要多个PWM通道同步输出,可以使用定时器的主从模式:
c复制// 配置TIM2为主定时器
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_ENABLE;
// 配置TIM3为从定时器
sSlaveConfig.SlaveMode = TIM_SLAVEMODE_TRIGGER;
sSlaveConfig.InputTrigger = TIM_TS_ITR1; // TIM2触发TIM3
5.2 硬件死区插入
对于电机驱动等需要防止上下管直通的场景,可以配置死区时间:
c复制TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};
sBreakDeadTimeConfig.DeadTime = 100; // 死区时间值
sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
HAL_TIMEx_ConfigBreakDeadTime(&htim, &sBreakDeadTimeConfig);
5.3 使用RT-Thread PWM设备API
RT-Thread提供了更简洁的PWM控制API:
c复制// 设置PWM周期和占空比
rt_pwm_set(pwm_dev, channel, period, pulse);
// 启用/禁用PWM输出
rt_pwm_enable(pwm_dev, channel);
rt_pwm_disable(pwm_dev, channel);
在实际项目中,我发现合理组合使用这些高级功能可以大幅提升系统性能和可靠性。特别是在电机控制应用中,正确的死区时间配置和同步触发设置至关重要。
