09-TIM高级应用:PWM与输入捕获.md 12 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(进阶篇)V1.0.1 — 第1章:PWM + 配套代码20~27" author: "尚硅谷研究院" date: 2026-07-15

created: 2026-07-15

TIM高级应用:PWM与输入捕获

用生活理解:PWM 就像快速开关灯——如果你的手足够快(每秒 1000 次),眼睛看见的就不是闪烁而是灯光变暗了。开的时间比例(占空比)越大就越亮。呼吸灯就是占空比从 0% 逐渐增加到 100% 再回来,像呼吸一样平滑。输入捕获就像用第二个秒表测量第一个秒表的读数——测量外部信号的频率或脉宽。


PWM 原理

PWM = Pulse Width Modulation(脉冲宽度调制),通过调节脉冲宽度(占空比)控制平均电压。

基本参数

输出: ████░░░░░░░░  频率 = 1KHz, 占空比 = 40%
     ↑CCR    ↑ARR
     └ 高电平 ┘

PWM 频率Freq = CK_PSC / (PSC + 1) / (ARR + 1)

PWM 占空比Duty = CCR / (ARR + 1) × 100%

PWM 分辨率Resolution = 1 / (ARR + 1) × 100%

典型配置示例

目标: 1KHz PWM, 可用占空比 0~999 (0.1% 分辨率)

CK_PSC = 72MHz
PSC = 72 - 1    → CK_CNT = 72M / 72 = 1MHz
ARR = 1000 - 1  → Freq = 1MHz / 1000 = 1KHz
CCR = 0~999     → Duty = 0~99.9%

输出比较模式

TIM 的输出比较模式由 CCMR1/2 寄存器的 OCxM[2:0] 位控制:

OCxM 模式 计数器条件 输出
000 冻结 无影响
001 强制有效 输出高电平
010 强制无效 输出低电平
011 翻转 CNT=CCR 翻转输出
110 PWM 模式1 CNT<CCR 有效, CNT≥CCR 无效 向上计数时前半周期高
111 PWM 模式2 CNT<CCR 无效, CNT≥CCR 有效 向上计数时前半周期低

PWM 模式 1(向上计数)波形

CNT:  0 → 1 → ... → CCR → ... → ARR → 0 → ...
输出: ████████████████░░░░░░░░░░░░░░░████
                      ↑CCR
       ← 占空比(CCR/ARR+1) → ← 剩余 →
       ←—————— 周期(ARR+1) × CK_CNT ——————→

TIM 输出比较寄存器

寄存器 功能
CCMR1 捕获/比较模式寄存器1(CH1/CH2 模式配置、预装载使能)
CCMR2 捕获/比较模式寄存器2(CH3/CH4 模式配置)
CCER 捕获/比较使能寄存器(CCxE 输出使能、CCxP 输出极性)
CCR1~4 捕获/比较值寄存器(PWM 时存占空比,捕获时存事件发生时的 CNT)

参考:参考手册 §14(TIM 寄存器描述)

影子寄存器

TIM 的 ARR 和 CCRx 都有影子寄存器机制:

  • 预装载寄存器:软件可读写(如 ARR, CCR1)
  • 影子寄存器:真正驱动硬件的内部寄存器
  • 更新时机:CR1.ARPE=1 时,影子寄存器在 UEV 时更新(推荐)
  • CR1.ARPE=0 时,预装载值立即传送到影子(可能有毛刺)

建议 ARPE=1,在修改 ARR/CCR 时等待下次更新事件后才生效,避免中途改变造成波形异常。


实验:呼吸灯

软件设计(寄存器版)

项目路径stm32_base/20_tim_led_breathe_register

需求:使用 TIM5_CH1 输出 PWM 到 PA0(LED1),占空比 0~99% 循环渐变。

硬件电路:LED1 = PA0,TIM5_CH1 的输出通道可通过重映射连接到 PA0。

文件:stm32_base/20_tim_led_breathe_register/Hardware/TIM5/tim5.h

#ifndef __TIM5_H
#define __TIM5_H

#include "stm32f10x.h"

void TIM5_Init(void);
void TIM5_Start(void);
void TIM5_SetDutyCycle(uint8_t duty);  // duty: 0~99

#endif

文件:stm32_base/20_tim_led_breathe_register/Hardware/TIM5/tim5.c

#include "tim5.h"

void TIM5_Init(void)
{
    /* ========== 1. 开启时钟 ========== */
    RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;      // TIM5 时钟使能(APB1)
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;       // GPIOA 时钟使能

    /* ========== 2. 配置 PA0 为复用功能推挽输出 ========== */
    // TIM5_CH1 输出到 PA0(需要 AFIO 重映射)
    // CNF=10(复用推挽), MODE=11(50MHz)
    GPIOA->CRL &= ~GPIO_CRL_CNF0;             // &=~: CNF0清0
    GPIOA->CRL |= GPIO_CRL_CNF0_1;            // |=: CNF0[1]=1
    // CNF[1:0] = 10 → 复用功能推挽
    GPIOA->CRL |= GPIO_CRL_MODE0;             // |=: MODE0[1:0]=11

    /* ========== 3. 配置 TIM5 时基 ========== */
    // PWM 频率 = 72MHz / 72 / 1000 = 1KHz
    TIM5->PSC = 72 - 1;                       // 预分频: 72MHz → 1MHz
    TIM5->ARR = 1000 - 1;                     // 重装载: 1MHz/1000 = 1KHz

    /* ========== 4. 配置通道1输出比较模式 ========== */
    // CCMR1 控制 CH1 和 CH2
    // OC1M[2:0] = 110 → PWM 模式1
    TIM5->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2;  // 设置 OC1M bit2=1, bit1=1
    // OC1PE = 1 → 预装载使能(CCR1 修改后等待 UEV 才更新)
    TIM5->CCMR1 |= TIM_CCMR1_OC1PE;           // |=: OC1PE位置1

    // CCER: CC1E = 1 → CH1 输出使能
    // CC1P = 0 → 高电平有效(CNT<CCR 输出高)
    TIM5->CCER |= TIM_CCER_CC1E;              // |=: CC1E位置1

    /* ========== 5. 初始占空比 ========== */
    TIM5->CCR1 = 0;                           // 初始占空比 0%

    /* ========== 6. 使能自动重装载预装载 ========== */
    TIM5->CR1 |= TIM_CR1_ARPE;               // |=: ARPE=1
}

// 启动 TIM5 计数器
void TIM5_Start(void)
{
    TIM5->CR1 |= TIM_CR1_CEN;                 // |=: CEN=1, 开始计数
}

// 设置占空比
// duty: 0~99 对应占空比 0%~99%
// CCR1 = duty × 10(因为 ARR=999, 1% 对应 10 个计数)
void TIM5_SetDutyCycle(uint8_t duty)
{
    if (duty > 99) duty = 99;                 // 限制范围
    TIM5->CCR1 = duty * 10;                  // CCR1 = 0, 10, 20, ..., 990
}

文件:stm32_base/20_tim_led_breathe_register/User/main.c

#include "tim5.h"
#include "delay.h"

int main(void)
{
    TIM5_Init();
    TIM5_Start();

    uint8_t duty = 0;     // 当前占空比 (0~99)
    uint8_t dir  = 0;     // 方向: 0=增加占空比, 1=减小
    uint8_t step = 1;     // 每次改变量

    while (1)
    {
        // 修改占空比
        if (dir == 0) {
            duty += step;
            if (duty >= 99) dir = 1;   // 到 99% 转向减小
        } else {
            duty -= step;
            if (duty <= 0)  dir = 0;   // 到 0% 转向增加
        }

        TIM5_SetDutyCycle(duty);        // 更新 CCR1
        Delay_ms(10);                   // 每 10ms 改变一次,呼吸周期 ≈ 2s
    }
}

实验:PWM 周期/占空比独立控制

项目路径stm32_base/22_tim_pwm_cycle_register(周期控制) 项目路径stm32_base/24_tim_pwm_duty_register(占空比控制)

// 周期控制:固定占空比 50%,改变频率
// 通过修改 PSC 或 ARR 改变频率
void PWM_SetFreq(uint16_t freq)
{
    // Freq = 72MHz / (PSC+1) / (ARR+1)
    // 固定 PSC=72-1, 调整 ARR
    TIM5->ARR = (72000000 / 72 / freq) - 1;
    // 保持占空比 50%: CCR1 = ARR/2
    TIM5->CCR1 = TIM5->ARR / 2;
}

// 占空比控制:固定频率 1KHz,改变占空比
void PWM_SetDuty(uint8_t percent)
{
    // ARR 固定 999, 改变 CCR1
    TIM5->CCR1 = (uint16_t)((uint32_t)999 * percent / 100);
}

实验:输入捕获测频率

输入捕获可测量外部信号的频率或脉宽。原理:记录两次边沿事件发生时的 CNT 值,差值 = 脉冲宽度或周期。

配置流程

1. GPIO 配置为输入(浮空/上拉)
2. TIM 启用输入捕获模式(CCMRx.ICx=1)
3. 配置捕获边沿(CCER.CCxP 上升/下降沿)
4. 使能捕获中断(DIER.CCxIE)
5. 在 ISR 中读取 CCRx,计算差值
// 初始化 TIM2_CH1(PA0) 为输入捕获模式
void TIM2_Capture_Init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    // PA0 浮空输入
    GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);

    // TIM2_CH1 映射到 PA0(默认)
    TIM2->PSC = 72 - 1;             // CK_CNT = 1MHz (分辨率 1us)
    TIM2->ARR = 0xFFFF;             // 最大 16 位

    // CCMR1: CH1 输入捕获模式(IC1=1 映射到 TI1)
    TIM2->CCMR1 |= TIM_CCMR1_CC1S_0;  // CC1S=01: CH1→TI1

    // 捕获边沿: 上升沿
    TIM2->CCER &= ~TIM_CCER_CC1P;      // CC1P=0: 上升沿

    // 使能捕获中断
    TIM2->DIER |= TIM_DIER_CC1IE;

    // NVIC
    NVIC_EnableIRQ(TIM2_IRQn);

    // 启动
    TIM2->CR1 |= TIM_CR1_CEN;
}

// 捕获中断:记录两次上升沿的 CNT 差值
volatile uint16_t cap1, cap2;
volatile uint8_t cap_count = 0;

void TIM2_IRQHandler(void)
{
    if (TIM2->SR & TIM_SR_CC1IF)      // 捕获事件发生
    {
        if (cap_count == 0) {
            cap1 = TIM2->CCR1;         // 第一次捕获值
            cap_count = 1;
        } else {
            cap2 = TIM2->CCR1;         // 第二次捕获值
            cap_count = 2;

            // 频率 = CK_CNT / (cap2 - cap1)
            // = 1MHz / 差值
            uint16_t diff = (cap2 > cap1) ? (cap2 - cap1) : (0xFFFF - cap1 + cap2);
            uint32_t freq = 1000000 / diff;
        }
        TIM2->SR &= ~TIM_SR_CC1IF;     // 清标志
    }
}

HAL 库版 PWM

HAL 库使用 HAL_TIM_PWM_Start() 启动 PWM,用宏 __HAL_TIM_SET_COMPARE() 修改占空比。

// CubeMX 生成: MX_TIM5_Init() 配置 PSC/ARR 和 CH1 输出模式
TIM_HandleTypeDef htim5;

// 启动 PWM 输出
HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_1);
// HAL_TIMEx_PWMN_Start(&htim5, TIM_CHANNEL_1); // 互补输出(TIM1 高级定时器)

// 修改占空比(HAL 宏,比直接写寄存器更可移植)
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, duty);  // duty = 0~ARR

// HAL 库呼吸灯示例
uint8_t duty = 0, dir = 0;
while (1) {
    if (!dir) { duty++;  if (duty >= 99) dir = 1; }
    else      { duty--;  if (duty <= 0)  dir = 0; }
    __HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, duty * 10);
    HAL_Delay(10);
}

核心速查表

操作 寄存器版 HAL 库版
PWM 模式1(CH1) `CCMR1 = OC1M_1 + OC1M_2`
PWM 输出使能 `CCER = CC1E`
修改占空比 CCR1 = value __HAL_TIM_SET_COMPARE(htim, CH, val)
修改频率 ARR = new_value htim.Init.Period 重新 Init
输入捕获 `CCMR1 = CC1S_0`
读取捕获值 CCR1 __HAL_TIM_GET_COMPARE(htim, CH)
呼吸灯参数 频率 占空比范围 渐变步长
PWM 频率 1KHz 0~99% 1%
渐变间隔 10ms
呼吸周期 ~2s

常见问题与避坑

  1. PWM 无输出 → 检查 GPIO 模式(必须复用推挽 CNF=10,不是通用推挽)、CCER.CCxE 是否使能
  2. 占空比 0% 时引脚仍有电平 → 检查 CCER.CCxP 极性配置、CH 是否确实连接到了目标引脚
  3. PWM 频率与预期不符 → 确认 APB1 定时器时钟公式:如果 APB1 预分频≠1 则 TIM 时钟 = APB1×2
  4. 修改 CCR 后占空比不立即变化 → ARPE=1 时需等到下次 UEV;如需立即更新可先清除 ARPE
  5. 呼吸灯非预期渐变 → 确保 PWM 频率 > 100Hz(否则人眼能看见闪烁),推荐 1KHz 以上
  6. 输入捕获值不准 → 捕获事件太频繁(计数溢出),增大 PSC 降低 CK_CNT 分辨率换范围
  7. 输入捕获只进一次中断 → CNT 计数溢出(ARR=0xFFFF 最大),需添加溢出中断 UIF 计数