第4章:HAL层实现:以STM32为例,实现一个通用的PWM输出接口,支持多通道配置
好,咱们进入正题。上一章我们把HAL层的设计思路讲清楚了,这一章就来点实在的——用STM32做个通用的PWM输出接口。说实话,PWM这东西在电机控制里太常见了,从最简单的直流电机调速,到复杂的FOC控制,都离不开它。
我个人习惯,写这种底层接口时,先想清楚要暴露什么给上层。你想想看,上层应用关心的是「我要输出多少占空比的PWM」,而不是「我要配置哪个定时器的哪个通道」。所以,我们的接口设计要足够抽象。
4.1 接口设计思路
先问个问题:一个通用的PWM接口,最少需要几个函数?
我的答案是三个:初始化、设置占空比、启动/停止。嗯,其实启动停止可以合并到初始化里,但分开更灵活。我在项目中遇到过,有些场景需要动态启停PWM输出,比如刹车功能。
来看看我设计的接口原型:
/* pwm_hal.h */
#ifndef __PWM_HAL_H
#define __PWM_HAL_H
#include <stdint.h>
/* PWM通道配置结构体 */
typedef struct {
uint8_t channel; /* 通道号:0~N-1 */
uint32_t frequency; /* 频率,单位Hz */
uint16_t duty_cycle; /* 占空比,单位0.1%,范围0~1000 */
uint8_t polarity; /* 极性:0-高有效,1-低有效 */
} PWM_ChannelConfig_t;
/* PWM设备句柄 */
typedef struct {
uint8_t channel_count; /* 通道数量 */
PWM_ChannelConfig_t *configs; /* 通道配置数组指针 */
void *hal_handle; /* 硬件相关句柄,内部使用 */
} PWM_Handle_t;
/* 接口函数 */
int32_t PWM_Init(PWM_Handle_t *handle);
int32_t PWM_SetDuty(PWM_Handle_t *handle, uint8_t channel, uint16_t duty);
int32_t PWM_Start(PWM_Handle_t *handle);
int32_t PWM_Stop(PWM_Handle_t *handle);
#endif
这里有个细节:占空比我用的是0.1%为单位,范围0~1000。为什么不用百分比?说白了,就是为了精度。电机控制里,有时候0.1%的差异都会影响性能。我曾经在伺服项目里,就因为占空比精度不够,导致低速时抖动明显。
4.2 STM32定时器配置要点
STM32的定时器功能很强大,但配置起来也有不少坑。我挑几个关键点说说。
核心配置参数:
- 预分频器(PSC):决定定时器时钟频率
- 自动重装载值(ARR):决定PWM周期
- 比较值(CCR):决定占空比
计算公式很简单:
- PWM频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)
- 占空比 = CCR / (ARR + 1) × 100%
举个例子,假设定时器时钟是72MHz,想要20kHz的PWM:
/* 计算PSC和ARR */
/* 目标频率:20kHz */
/* 72MHz / 20kHz = 3600 */
/* 取PSC = 0, ARR = 3599,则频率 = 72M / 1 / 3600 = 20kHz */
#define PWM_FREQUENCY 20000
#define TIM_CLOCK 72000000
#define PWM_PSC 0
#define PWM_ARR (TIM_CLOCK / (PWM_PSC + 1) / PWM_FREQUENCY - 1)
我的经验:预分频器尽量设小一点,这样ARR值就大,占空比调节的步进就更精细。比如上面例子,如果PSC设成71,ARR就只剩49了,占空比只能调50档,电机控制根本不够用。
4.3 多通道配置实现
多通道配置,说白了就是让一个定时器的多个通道同时工作。STM32的定时器通常有4个通道,每个通道可以独立输出PWM。
来看看具体实现:
/* pwm_hal_stm32.c */
#include "pwm_hal.h"
#include "stm32f4xx_hal.h"
/* 内部结构体,用于映射通道和定时器 */
typedef struct {
TIM_HandleTypeDef htim;
uint32_t channel_map[4]; /* 通道映射表 */
uint8_t active_channels;
} PWM_STM32_Internal_t;
int32_t PWM_Init(PWM_Handle_t *handle)
{
PWM_STM32_Internal_t *internal;
TIM_OC_InitTypeDef sConfigOC = {0};
uint8_t i;
if (handle == NULL || handle->configs == NULL) {
return -1; /* 参数错误 */
}
/* 分配内部结构体内存 */
internal = (PWM_STM32_Internal_t *)malloc(sizeof(PWM_STM32_Internal_t));
if (internal == NULL) {
return -2; /* 内存不足 */
}
/* 配置定时器基础参数 */
/* 这里以TIM1为例,实际项目中应根据handle中的信息选择定时器 */
internal->htim.Instance = TIM1;
internal->htim.Init.Prescaler = PWM_PSC;
internal->htim.Init.CounterMode = TIM_COUNTERMODE_UP;
internal->htim.Init.Period = PWM_ARR;
internal->htim.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
internal->htim.Init.RepetitionCounter = 0;
internal->htim.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_PWM_Init(&internal->htim) != HAL_OK) {
free(internal);
return -3; /* 初始化失败 */
}
/* 配置每个通道 */
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
for (i = 0; i < handle->channel_count; i++) {
uint32_t tim_channel;
/* 根据通道号映射到定时器通道 */
switch (handle->configs[i].channel) {
case 0: tim_channel = TIM_CHANNEL_1; break;
case 1: tim_channel = TIM_CHANNEL_2; break;
case 2: tim_channel = TIM_CHANNEL_3; break;
case 3: tim_channel = TIM_CHANNEL_4; break;
default: continue; /* 不支持的通道 */
}
/* 计算比较值 */
sConfigOC.Pulse = (uint32_t)(PWM_ARR + 1) * handle->configs[i].duty_cycle / 1000;
if (HAL_TIM_PWM_ConfigChannel(&internal->htim, &sConfigOC, tim_channel) != HAL_OK) {
/* 通道配置失败,继续配置其他通道 */
continue;
}
internal->channel_map[i] = tim_channel;
internal->active_channels++;
}
handle->hal_handle = (void *)internal;
return 0;
}
注意:上面的代码为了演示做了简化。实际项目中,你需要考虑以下几点:
- 定时器资源管理:多个PWM实例可能共用同一个定时器
- 内存分配:嵌入式系统里malloc要慎用,建议用静态分配
- 错误处理:每个HAL函数调用都要检查返回值
4.4 占空比动态调整
电机控制中,占空比需要实时调整。这个函数要够快,最好能在中断里调用。
int32_t PWM_SetDuty(PWM_Handle_t *handle, uint8_t channel, uint16_t duty)
{
PWM_STM32_Internal_t *internal;
uint32_t compare_value;
if (handle == NULL || handle->hal_handle == NULL) {
return -1;
}
if (channel >= 4) {
return -2; /* 通道号错误 */
}
internal = (PWM_STM32_Internal_t *)handle->hal_handle;
/* 计算比较值 */
compare_value = (uint32_t)(PWM_ARR + 1) * duty / 1000;
/* 直接操作寄存器,速度更快 */
switch (internal->channel_map[channel]) {
case TIM_CHANNEL_1:
TIM1->CCR1 = compare_value;
break;
case TIM_CHANNEL_2:
TIM1->CCR2 = compare_value;
break;
case TIM_CHANNEL_3:
TIM1->CCR3 = compare_value;
break;
case TIM_CHANNEL_4:
TIM1->CCR4 = compare_value;
break;
default:
return -3;
}
return 0;
}
为什么这里直接操作寄存器而不是用HAL库函数?因为HAL_TIM_PWM_SetCompareValue()函数里有很多检查,调用开销大。在电机控制的中断服务程序里,每微秒都很宝贵。我曾经在一个项目里,就因为用了HAL库的PWM设置函数,导致中断响应时间超标,电机在高速时失控了。
4.5 启动与停止
int32_t PWM_Start(PWM_Handle_t *handle)
{
PWM_STM32_Internal_t *internal;
uint8_t i;
if (handle == NULL || handle->hal_handle == NULL) {
return -1;
}
internal = (PWM_STM32_Internal_t *)handle->hal_handle;
/* 启动所有已配置的通道 */
for (i = 0; i < handle->channel_count; i++) {
if (internal->channel_map[i] != 0) {
HAL_TIM_PWM_Start(&internal->htim, internal->channel_map[i]);
}
}
return 0;
}
int32_t PWM_Stop(PWM_Handle_t *handle)
{
PWM_STM32_Internal_t *internal;
uint8_t i;
if (handle == NULL || handle->hal_handle == NULL) {
return -1;
}
internal = (PWM_STM32_Internal_t *)handle->hal_handle;
/* 停止所有通道 */
for (i = 0; i < handle->channel_count; i++) {
if (internal->channel_map[i] != 0) {
HAL_TIM_PWM_Stop(&internal->htim, internal->channel_map[i]);
}
}
return 0;
}
4.6 使用示例
最后,看看上层怎么用这个接口:
/* main.c */
#include "pwm_hal.h"
/* 定义三个通道的配置 */
PWM_ChannelConfig_t motor_configs[3] = {
{.channel = 0, .frequency = 20000, .duty_cycle = 500, .polarity = 0}, /* 50%占空比 */
{.channel = 1, .frequency = 20000, .duty_cycle = 300, .polarity = 0}, /* 30%占空比 */
{.channel = 2, .frequency = 20000, .duty_cycle = 800, .polarity = 0} /* 80%占空比 */
};
PWM_Handle_t motor_pwm = {
.channel_count = 3,
.configs = motor_configs,
.hal_handle = NULL
};
int main(void)
{
/* 初始化硬件 */
HAL_Init();
/* 初始化PWM */
if (PWM_Init(&motor_pwm) != 0) {
/* 错误处理 */
while(1);
}
/* 启动PWM输出 */
PWM_Start(&motor_pwm);
while (1) {
/* 动态调整占空比 */
PWM_SetDuty(&motor_pwm, 0, 600); /* 通道0改为60% */
HAL_Delay(1000);
PWM_SetDuty(&motor_pwm, 0, 400); /* 通道0改为40% */
HAL_Delay(1000);
}
}
你看,上层代码完全不知道底层用的是STM32的哪个定时器,也不知道寄存器怎么配。这就是HAL层抽象的价值——把硬件细节藏起来,让上层专注于业务逻辑。
嗯,这一章就到这里。下一章我们会讲怎么用这个PWM接口去驱动一个实际的直流电机,到时候会涉及一些电机控制的细节,比如死区时间、互补输出这些。到时候见。