第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接口去驱动一个实际的直流电机,到时候会涉及一些电机控制的细节,比如死区时间、互补输出这些。到时候见。