在 STM32 微控制器中,能够输出 PWM(脉冲宽度调制)信号的引脚并非固定不变,而是严格取决于具体的芯片型号(如 STM32F103、STM32F407 等)以及所使用的定时器通道。通常情况下,任何支持输出比较(Output Compare)功能的定时器通道所对应的 GPIO 引脚,经过复用配置后均可作为 PWM 输出引脚。例如,在常见的 STM32F103C8T6 最小系统板中,通用定时器 TIM2 的通道 1 对应 PA0,TIM3 的通道 1 对应 PA6,通道 2 对应 PA7;而高级定时器 TIM1 的通道 1 则对应 PA8 。要实现对这些引脚的精准控制,开发者主要采用传统的标准库(Standard Peripheral Library)或现代化的 HAL 库(Hardware Abstraction Layer)。以下将结合具体代码示例,通俗易懂地拆解两种库的实现步骤。

一、核心概念与引脚映射逻辑

PWM 的本质是通过调节信号的占空比(高电平时间与周期的比值)来控制平均电压,进而实现电机调速或 LED 亮度渐变(呼吸灯) 。在 STM32 硬件架构中,这需要通过定时器的“输出比较”模式,将内部计数值与寄存器中的比较值进行匹配,从而翻转 GPIO 电平。

特性维度 标准库 (Standard Lib) HAL 库 (Hardware Abstraction Layer)
适用场景 旧项目维护、需深度理解寄存器机制 新项目开发、配合 CubeMX 工具、跨平台移植
配置流程 繁琐,需手动开启时钟、配置多个结构体 简洁,函数封装度高,逻辑语义化强
引脚配置 需手动指定 GPIO_Mode_AF_PP 需指定 GPIO_Alternate 复用功能号
动态调整 直接操作 TIM_SetCompareX 函数 使用 __HAL_TIM_SET_COMPARE
推荐指数 ⭐⭐⭐ (适合学习底层) ⭐⭐⭐⭐⭐ (适合工程落地)

二、方案推演:标准库实现步骤详解

标准库的配置过程虽然繁琐,但能清晰展示硬件初始化的每一个环节。其核心逻辑是:开时钟 -> 配 GPIO -> 配时基 -> 配通道 -> 启动

操作流程解析

  1. 开启时钟:必须同时开启定时器外设时钟(如 TIM3 挂在 APB1)和对应 GPIO 端口时钟(如 GPIOA 挂在 APB2) 。
  2. GPIO 配置:这是新手最容易出错的地方。引脚模式必须设置为复用推挽输出GPIO_Mode_AF_PP),否则定时器信号无法输出到引脚上。
  3. 定时器时基:设置预分频系数(Prescaler)和自动重装载值(ARR)。频率计算公式为:$F_{pwm} = \frac{F_{clk}}{(PSC+1) \times (ARR+1)}$。
  4. 输出通道配置:设置脉冲值(CCR,即占空比)、输出极性(高电平有效还是低电平有效)和 PWM 模式(模式 1 或模式 2) 。
  5. 启动:使能定时器,若是高级定时器还需使能主输出(MOE)。

代码示例(标准库)

#include "stm32f10x.h"

/**
 * @brief 标准库初始化 PWM (以 TIM3 CH1 PA6 为例)
 * 目标频率:1kHz, 初始占空比:50%
 */
void PWM_Init_StdLib(void) {
    // 1. 开启时钟:TIM3 属于 APB1, GPIOA 属于 APB2 
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // 2. 配置 GPIO (PA6 为 TIM3_CH1)
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 【关键】必须设为复用推挽
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 3. 配置定时器时基
    // 假设系统时钟 72MHz, PSC=71, ARR=999 -> 频率 = 72M / 72 / 1000 = 1kHz
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_TimeBaseStructure.TIM_Period = 999;       // 自动重装载值 (ARR)
    TIM_TimeBaseStructure.TIM_Prescaler = 71;     // 预分频系数 (PSC)
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

    // 4. 配置输出比较通道 (初始化占空比为 50%)
    TIM_OCInitTypeDef TIM_OCInitStructure;
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM 模式 1: 计数<CCR 时有效
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 500;              // 比较值 (CCR), 500/1000 = 50%
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 高电平有效
    TIM_OC1Init(TIM3, &TIM_OCInitStructure);          // 初始化通道 1

    // 5. 使能预装载寄存器并启动定时器
    TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
    TIM_Cmd(TIM3, ENABLE);
    
    // 如果是高级定时器 (如 TIM1), 还需调用 TIM_CtrlPWMOutputs(TIM1, ENABLE);
}

三、方案推演:HAL 库实现步骤详解

HAL 库极大地简化了流程,通常配合 STM32CubeMX 图形化工具生成初始化代码。其核心逻辑是:**定义句柄 -> 底层初始化 **(MspInit)。

操作流程解析

  1. 句柄定义:定义一个 TIM_HandleTypeDef 结构体变量,用于存储定时器的所有状态和配置。
  2. 底层初始化:在 HAL_TIM_PWM_MspInit 回调函数中配置 GPIO 和时钟。注意 HAL 库中需要指定具体的复用功能号(Alternate Function),如 GPIO_AF2_TIM3
  3. 参数配置:填充时基结构体和输出比较结构体,调用初始化函数。
  4. 启动输出:调用 HAL_TIM_PWM_Start 开始产生波形 。

代码示例(HAL 库)

#include "main.h" // 包含 HAL 库头文件

TIM_HandleTypeDef htim3; // 定义定时器句柄

/**
 * @brief HAL 库底层初始化回调函数
 * 此函数通常在 stm32fxxx_hal_msp.c 中,或在主文件中重写
 */
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef* tim_pwmHandle) {
    if(tim_pwmHandle->Instance == TIM3) {
        // 1. 开启时钟 
        __HAL_RCC_TIM3_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();

        // 2. 配置 GPIO (PA6 -> TIM3_CH1)
        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_LOW;
        GPIO_InitStruct.Alternate = GPIO_AF2_TIM3; // 【关键】指定复用映射关系
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    }
}

/**
 * @brief HAL 库 PWM 初始化主函数
 */
void PWM_Init_HAL(void) {
    // 3. 配置时基参数
    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 71;            // 72MHz / (71+1) = 1MHz 计数频率
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim3.Init.Period = 999;              // 1MHz / (999+1) = 1kHz PWM 频率
    htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    
    // 初始化定时器 
    if (HAL_TIM_PWM_Init(&htim3) != HAL_OK) {
        // 错误处理,实际项目中可加入 Error_Handler()
        while(1);
    }

    // 4. 配置通道参数 (占空比控制)
    TIM_OC_InitTypeDef sConfigOC = {0};
    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 500;                // 初始占空比 50%
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;

    // 配置通道 1
    if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) {
        while(1);
    }

    // 5. 启动 PWM 输出 
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}

/**
 * @brief 动态调整占空比示例
 * @param duty: 0-1000 之间的值
 */
void Set_DutyCycle(uint32_t duty) {
    // 直接修改 CCR 寄存器即可实时改变占空比,无需重新初始化 
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty); 
}

四、总结与应用建议

对于初学者而言,HAL 库是更优的选择。它不仅代码结构清晰,而且通过 __HAL_TIM_SET_COMPARE 宏可以极其方便地在运行时动态调整占空比,非常适合制作呼吸灯或电机调速系统 。而标准库虽然配置繁琐,但有助于深入理解寄存器层面的工作原理,适合需要极致优化或维护旧代码的场景。

在实际应用中,引脚的选择务必查阅具体芯片的数据手册(Datasheet)中的"Pin Definitions"章节。例如,若需控制 SG90 舵机,通常选用任意通用定时器通道,将 PWM 频率设定为 50Hz(周期 20ms),然后通过改变高电平时间(0.5ms 对应 0 度,2.5ms 对应 180 度)即可精确控制角度 。无论使用哪种库,核心都在于正确计算预分频系数自动重装载值以确定频率,并通过修改比较值(Pulse/CCR)来调整占空比。​​​​

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐