09-TIM高级应用:PWM与输入捕获.md 25 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/20_tim_led_breathe_register

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

硬件电路:LED1 = PA1,TIM5_CH2 默认映射到 PA1(无需重映射)。

文件:stm32/20_tim_led_breathe_register/Hardware/TIM/tim5.h

#ifndef __TIM5_H
#define __TIM5_H

#include "stm32f10x.h"

// 初始化
void TIM5_Init(void);

// 定时器的启动和关闭
void TIM5_Start(void);
void TIM5_Stop(void);

// 设置占空比,传入百分比值
void TIM5_SetDutyCycle(uint8_t duty);

#endif

文件:stm32/20_tim_led_breathe_register/Hardware/TIM/tim5.c

#include "tim5.h"

// 初始化
void TIM5_Init(void)
{
    // 1. 开启时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    // 2. GPIO配置,复用推挽输出CNF = 10, MODE = 11
    GPIOA->CRL |= GPIO_CRL_MODE1;
    GPIOA->CRL |= GPIO_CRL_CNF1_1;
    GPIOA->CRL &= ~GPIO_CRL_CNF1_0;

    // 3. 定时器时基单元
    // 3.1 预分频值 7199,得到 10000Hz,计数周期 0.1ms
    TIM5->PSC = 7199;

    // 3.2 自动重装值 99,每100次计数溢出一次,10ms
    TIM5->ARR = 99;

    // 3.3 计数方向默认递增
    TIM5->CR1 &= ~TIM_CR1_DIR;

    // 4. 定时器输出比较部分
    // 4.1 通道2设置为输出模式,CC2S = 00
    TIM5->CCMR1 &= ~TIM_CCMR1_CC2S;

    // 4.2 配置通道2输出比较模式,OC2M = 110,PWM模式1
    TIM5->CCMR1 |= TIM_CCMR1_OC2M_2;
    TIM5->CCMR1 |= TIM_CCMR1_OC2M_1;
    TIM5->CCMR1 &= ~TIM_CCMR1_OC2M_0; 

    // 4.3 配置通道极性
    TIM5->CCER &= ~TIM_CCER_CC2P;

    // 4.4 使能输出通道
    TIM5->CCER |= TIM_CCER_CC2E;
}

// 定时器的启动和关闭
void TIM5_Start(void)
{
    TIM5->CR1 |= TIM_CR1_CEN;
}

void TIM5_Stop(void)
{
    TIM5->CR1 &= ~TIM_CR1_CEN;
}

// 设置占空比,传入百分比值
void TIM5_SetDutyCycle(uint8_t duty)
{
    TIM5->CCR2 = duty;
}

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

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

int main(void)
{
	// 初始化
	USART_Init();
	TIM5_Init();

	printf("Hello world!\n");

	// 启动定时器
	TIM5_Start();

	// 定义一个变量保存当前的占空比
	uint8_t duty = 0;
	// 定义占空比变化方向和步长
	uint8_t dir = 0;	// 0 - 增加 1 - 减小
	uint8_t step = 1;

	while (1)
	{
		// 判断方向,计算新的占空比
		if (dir == 0)
		{
			// 占空比增加
			duty += step;
			// 增加到99就反转方向
			if (duty >= 99)
			{
				dir = 1;
			}
		}
		else
		{
			// 占空比减小
			duty -= step;
			// 减小到0就反转方向
			if (duty <= 0)
			{
				dir = 0;
			}
		}
		
		TIM5_SetDutyCycle(duty);

		Delay_ms(10);
	}
}

实验:呼吸灯(HAL 库版)

软件设计(HAL 库版)

项目路径stm32/21_led_breathe_hal

文件:stm32/21_led_breathe_hal/Core/Src/tim.c(MX_TIM5_Init + TIM5_SetDutyCycle)

TIM_HandleTypeDef htim5;

void MX_TIM5_Init(void)
{
  htim5.Instance = TIM5;
  htim5.Init.Prescaler = 7199;
  htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim5.Init.Period = 99;
  htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  HAL_TIM_Base_Init(&htim5);
  HAL_TIM_PWM_Init(&htim5);

  TIM_OC_InitTypeDef sConfigOC = {0};
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 0;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2);
}

void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef* timHandle)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(timHandle->Instance==TIM5)
  {
    __HAL_RCC_TIM5_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitStruct.Pin = GPIO_PIN_1;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  }
}

void TIM5_SetDutyCycle(uint8_t dutyCycle)
{
  __HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_2, dutyCycle);
}

文件:stm32/21_led_breathe_hal/Core/Src/main.c

#include "main.h"
#include "tim.h"
#include "gpio.h"

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM5_Init();

  HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);

  uint8_t dutyCycle = 1;
  int8_t step = -1;

  while (1)
  {
    if (dutyCycle <= 1 || dutyCycle >= 99)
    {
      step = -step;
    }
    dutyCycle += step;
    TIM5_SetDutyCycle(dutyCycle);
    HAL_Delay(20);
  }
}

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

项目路径stm32/22_tim_pwm_cycle_register(周期捕获) 项目路径stm32/24_tim_pwm_duty_register(占空比捕获)

两个项目共用同一份 TIM5 输出 + TIM4 输入捕获代码。区别仅在 main.c 的测量逻辑。

文件:stm32/22_tim_pwm_cycle_register/User/main.c

#include "usart.h"
#include "delay.h"
#include "tim5.h"
#include "tim4.h"

int main(void)
{
	// 初始化
	USART_Init();
	TIM5_Init();
	TIM4_Init();

	printf("Hello world!\n");

	// 启动定时器
	TIM5_Start();
	TIM4_Start();

	TIM5_SetDutyCycle(50);

	while (1)
	{
		// 测量当前周期和频率
		printf("T = %.2f ms, f = %.2f Hz\n", TIM4_GetPWMCycle(), TIM4_GetPWMFreq());
		Delay_ms(1000);
	}
}

文件:stm32/24_tim_pwm_duty_register/User/main.c

#include "usart.h"
#include "delay.h"
#include "tim5.h"
#include "tim4.h"

int main(void)
{
	// 初始化
	USART_Init();
	TIM5_Init();
	TIM4_Init();

	printf("Hello world!\n");

	// 启动定时器
	TIM5_Start();
	TIM4_Start();

	TIM5_SetDutyCycle(32);

	while (1)
	{
		// 测量当前周期、频率和占空比
		printf("T = %.2f ms, f = %.2f Hz\n, duty = %.2f %%\n", 
			TIM4_GetPWMCycle(), TIM4_GetPWMFreq(), TIM4_GetPWMDuty() * 100);
		Delay_ms(1000);
	}
}

软件设计(HAL 库版 — 周期捕获)

项目路径stm32/23_pwm_cycle_hal

文件:stm32/23_pwm_cycle_hal/Core/Src/tim.c(MX_TIM4_Init + 回调)

TIM_HandleTypeDef htim4;
TIM_HandleTypeDef htim5;

void MX_TIM4_Init(void)
{
  htim4.Instance = TIM4;
  htim4.Init.Prescaler = 71;
  htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim4.Init.Period = 65535;
  htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  HAL_TIM_Base_Init(&htim4);
  HAL_TIM_IC_Init(&htim4);

  TIM_IC_InitTypeDef sConfigIC = {0};
  sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
  sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
  sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
  sConfigIC.ICFilter = 0;
  HAL_TIM_IC_ConfigChannel(&htim4, &sConfigIC, TIM_CHANNEL_1);
}

void MX_TIM5_Init(void)
{
  htim5.Instance = TIM5;
  htim5.Init.Prescaler = 7199;
  htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim5.Init.Period = 99;
  htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  HAL_TIM_Base_Init(&htim5);
  HAL_TIM_PWM_Init(&htim5);

  TIM_OC_InitTypeDef sConfigOC = {0};
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 60;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2);
}

double TIM4_GetPWMCycle(void)
{
  return __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1) / 1000.0;
}
double TIM4_GetPWMFreq(void)
{
  return 1000000.0 / __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1);
}

文件:stm32/23_pwm_cycle_hal/Core/Src/stm32f1xx_it.c(输入捕获回调)

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM4)
  {
    __HAL_TIM_SetCounter(&htim4, 0);
  }
}

文件:stm32/23_pwm_cycle_hal/Core/Src/main.c

#include "main.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM4_Init();
  MX_TIM5_Init();
  MX_USART1_UART_Init();

  HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
  HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_1);

  while (1)
  {
    printf("T = %.2f ms, f = %.2f Hz\n", TIM4_GetPWMCycle(), TIM4_GetPWMFreq());
    HAL_Delay(1000);
  }
}

软件设计(HAL 库版 — 周期+占空比捕获)

项目路径stm32/25_pwm_duty_hal

文件:stm32/25_pwm_duty_hal/Core/Src/tim.c(MX_TIM4_Init 双通道 + 从模式)

TIM_HandleTypeDef htim4;
TIM_HandleTypeDef htim5;

void MX_TIM4_Init(void)
{
  htim4.Instance = TIM4;
  htim4.Init.Prescaler = 71;
  htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim4.Init.Period = 65535;
  htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  HAL_TIM_Base_Init(&htim4);
  HAL_TIM_IC_Init(&htim4);

  TIM_SlaveConfigTypeDef sSlaveConfig = {0};
  sSlaveConfig.SlaveMode = TIM_SLAVEMODE_RESET;
  sSlaveConfig.InputTrigger = TIM_TS_TI1FP1;
  sSlaveConfig.TriggerPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
  sSlaveConfig.TriggerFilter = 0;
  HAL_TIM_SlaveConfigSynchro(&htim4, &sSlaveConfig);

  TIM_IC_InitTypeDef sConfigIC = {0};
  sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
  sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
  sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
  sConfigIC.ICFilter = 0;
  HAL_TIM_IC_ConfigChannel(&htim4, &sConfigIC, TIM_CHANNEL_1);

  sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING;
  sConfigIC.ICSelection = TIM_ICSELECTION_INDIRECTTI;
  HAL_TIM_IC_ConfigChannel(&htim4, &sConfigIC, TIM_CHANNEL_2);
}

double TIM4_GetPWMCycle(void)
{
  return __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1) / 1000.0;
}
double TIM4_GetPWMFreq(void)
{
  return 1000000.0 / __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1);
}
double TIM4_GetPWMDutyCycle(void)
{
  return __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_2) * 1.0 / __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1);
}

文件:stm32/25_pwm_duty_hal/Core/Src/main.c

#include "main.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM4_Init();
  MX_TIM5_Init();
  MX_USART1_UART_Init();

  printf("Hello, world!\n");

  HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
  HAL_TIM_IC_Start(&htim4, TIM_CHANNEL_1);
  HAL_TIM_IC_Start(&htim4, TIM_CHANNEL_2);

  while (1)
  {
    printf("T = %.2f ms, f = %.2f Hz, duty = %.2f %%\n",
        TIM4_GetPWMCycle(), TIM4_GetPWMFreq(), TIM4_GetPWMDutyCycle() * 100);
    HAL_Delay(1000);
  }
}

实验:输入捕获测频率

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

配置流程

1. GPIO 配置为输入(浮空/上拉)
2. TIM 启用输入捕获模式(CCMRx.ICx=1)
3. 配置捕获边沿(CCER.CCxP 上升/下降沿)
4. 使能捕获中断(DIER.CCxIE)
5. 在 ISR 中读取 CCRx,计算差值

输入捕获实现(TIM4 单通道测周期/频率)

项目路径stm32/22_tim_pwm_cycle_register

TIM4_CH1(PB6)捕获 PWM 输入上升沿,在中断中复位 CNT,直接读取 CCR1 获得周期。

文件:stm32/22_tim_pwm_cycle_register/Hardware/TIM/tim4.h

#ifndef __TIM4_H
#define __TIM4_H

#include "stm32f10x.h"

// 初始化
void TIM4_Init(void);

// 控制
void TIM4_Start(void);
void TIM4_Stop(void);

// 获取PWM周期和频率
double TIM4_GetPWMCycle(void);
double TIM4_GetPWMFreq(void);

#endif

文件:stm32/22_tim_pwm_cycle_register/Hardware/TIM/tim4.c

#include "tim4.h"

// 初始化
void TIM4_Init(void)
{
    // 1. 开启时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

    // 2. GPIO配置,浮空输入,CNF = 01,MODE = 00
    GPIOB->CRL &= ~GPIO_CRL_MODE6;
    GPIOB->CRL &= ~GPIO_CRL_CNF6_1;
    GPIOB->CRL |= GPIO_CRL_CNF6_0;

    // 3. 定时器时基单元配置
    // 3.1 预分频值 71,得到 1MHz,计数周期 1us
    TIM4->PSC = 71;

    // 3.2 自动重装值 65535,保证最大范围
    TIM4->ARR = 65535;

    // 3.3 计数方向默认递增
    TIM4->CR1 &= ~TIM_CR1_DIR;

    // 4. 定时器输入捕获部分
    // 4.1 TI1 选择 CH1 通道的直接输入(默认)
    TIM4->CR2 &= ~TIM_CR2_TI1S;

    // 4.2 不使用输入滤波器(默认)
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1F;

    // 4.3 捕获极性,默认高电平(上升沿)
    TIM4->CCER &= ~TIM_CCER_CC1P;

    // 4.4 通道1设置为输入,映射为直通信号(TI1 -> IC1),CC1S = 01
    TIM4->CCMR1 &= ~TIM_CCMR1_CC1S_1;
    TIM4->CCMR1 |= TIM_CCMR1_CC1S_0;

    // 4.5 输入预分频器(默认)
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1PSC;

    // 4.6 使能通道1
    TIM4->CCER |= TIM_CCER_CC1E;

    // 4.7 使能输入捕获中断
    TIM4->DIER |= TIM_DIER_CC1IE;

    // 5. NVIC配置
    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(TIM4_IRQn, 3);
    NVIC_EnableIRQ(TIM4_IRQn);
}

// 控制
void TIM4_Start(void)
{
    TIM4->CR1 |= TIM_CR1_CEN;
}
void TIM4_Stop(void)
{
    TIM4->CR1 &= ~TIM_CR1_CEN;
}

// 改进版中断服务函数:捕获到上升沿直接复位CNT
void TIM4_IRQHandler(void)
{
    // 判断是否是通道1的捕获中断
    if (TIM4->SR & TIM_SR_CC1IF)
    {
        // 清除中断标志位
        TIM4->SR &= ~TIM_SR_CC1IF;

        // 直接复位CNT
        TIM4->CNT = 0;
    }
}

// 获取PWM周期,单位为ms
double TIM4_GetPWMCycle(void)
{
    return TIM4->CCR1 / 1000.0;
}

// 获取PWM频率,单位为Hz
double TIM4_GetPWMFreq(void)
{
    return 1000000.0 / TIM4->CCR1;
}

输入捕获增强版(TIM4 双通道测周期+占空比)

项目路径stm32/24_tim_pwm_duty_register

使用 TIM4 的 CH1(TI1 -> IC1)捕获周期,CH2(TI1 -> IC2)捕获占空比。通过从模式复位(SMS=100, TS=101)自动复位 CNT,无需中断。

文件:stm32/24_tim_pwm_duty_register/Hardware/TIM/tim4.h

#ifndef __TIM4_H
#define __TIM4_H

#include "stm32f10x.h"

// 初始化
void TIM4_Init(void);

// 控制
void TIM4_Start(void);
void TIM4_Stop(void);

// 获取PWM周期和频率
double TIM4_GetPWMCycle(void);
double TIM4_GetPWMFreq(void);

// 获取PWM占空比
double TIM4_GetPWMDutyCycle(void);

#endif

文件:stm32/24_tim_pwm_duty_register/Hardware/TIM/tim4.c(初始化部分差异)

void TIM4_Init(void)
{
    // 1-3. 时钟、GPIO、时基配置同 22 项目 ...

    // 4. 定时器输入捕获部分(双通道)
    // 4.1-4.2 TI1、滤波器配置同 22 项目 ...

    // 4.3 捕获极性:CH1上升沿,CH2下降沿
    TIM4->CCER &= ~TIM_CCER_CC1P;
    TIM4->CCER |= TIM_CCER_CC2P;

    // 4.4 CH1直通(TI1 -> IC1),CH2间接(TI1 -> IC2)
    TIM4->CCMR1 &= ~TIM_CCMR1_CC1S_1;
    TIM4->CCMR1 |= TIM_CCMR1_CC1S_0;         // CC1S = 01
    TIM4->CCMR1 |= TIM_CCMR1_CC2S_1;
    TIM4->CCMR1 &= ~TIM_CCMR1_CC2S_0;        // CC2S = 10

    // 4.5 输入预分频器(默认)
    TIM4->CCMR1 &= ~TIM_CCMR1_IC1PSC;
    TIM4->CCMR1 &= ~TIM_CCMR1_IC2PSC;

    // 4.6 配置从模式触发源:TS = 101(TI1FP1)
    TIM4->SMCR |= TIM_SMCR_TS_2;
    TIM4->SMCR &= ~TIM_SMCR_TS_1;
    TIM4->SMCR |= TIM_SMCR_TS_0;

    // 4.7 从模式:复位模式 SMS = 100
    TIM4->SMCR |= TIM_SMCR_SMS_2;
    TIM4->SMCR &= ~TIM_SMCR_SMS_1;
    TIM4->SMCR &= ~TIM_SMCR_SMS_0;

    // 4.8 使能通道1和通道2
    TIM4->CCER |= TIM_CCER_CC1E;
    TIM4->CCER |= TIM_CCER_CC2E;
}

// 获取PWM占空比
double TIM4_GetPWMDutyCycle(void)
{
    return TIM4->CCR2 * 1.0 / TIM4->CCR1;
}

原理:CH1(TI1 -> IC1)捕获上升沿,此时 CNT 值存入 CCR1(周期);CH2(TI1 -> IC2)捕获下降沿,CNT 值存入 CCR2(高电平宽度)。从模式复位在 TI1FP1 上升沿自动清零 CNT,无需中断干预。占空比 = CCR2 / CCR1。

实验:指定脉冲数 PWM(N 个脉冲后自动停止)

软件设计(寄存器版)

项目路径stm32/26_pwm_n_register

使用 TIM1 高级定时器 + TIM4 输入捕获 + TIM5 PWM 输出 + TIM6 定时。利用 TIM1 的重复计数器(RCR)实现输出 N 个脉冲后停止。

文件:stm32/26_pwm_n_register/Hardware/TIM/tim1.h

#ifndef __TIM1_H
#define __TIM1_H

#include "stm32f10x.h"
#include <stdio.h>

void TIM1_Init(void);
void TIM1_Start(void);
void TIM1_Stop(void);

#endif

文件:stm32/26_pwm_n_register/Hardware/TIM/tim1.c

#include "tim1.h"

void TIM1_Init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;

    GPIOA->CRH |= GPIO_CRH_MODE8;
    GPIOA->CRH |= GPIO_CRH_CNF8_1;
    GPIOA->CRH &= ~GPIO_CRH_CNF8_0;

    TIM1->PSC = 7199;
    TIM1->ARR = 4999;
    TIM1->CR1 &= ~TIM_CR1_DIR;
    TIM1->RCR = 4;

    TIM1->CCMR1 &= ~TIM_CCMR1_CC1S;
    TIM1->CCMR1 |= TIM_CCMR1_OC1M_2;
    TIM1->CCMR1 |= TIM_CCMR1_OC1M_1;
    TIM1->CCMR1 &= ~TIM_CCMR1_OC1M_0;

    TIM1->CCR1 = 2500;
    TIM1->CCER &= ~TIM_CCER_CC1P;
    TIM1->CR1 |= TIM_CR1_URS;
    TIM1->EGR |= TIM_EGR_UG;
    TIM1->CCER |= TIM_CCER_CC1E;
    TIM1->BDTR |= TIM_BDTR_MOE;

    TIM1->DIER |= TIM_DIER_UIE;

    NVIC_SetPriorityGrouping(3);
    NVIC_SetPriority(TIM1_UP_IRQn, 3);
    NVIC_EnableIRQ(TIM1_UP_IRQn);
}

void TIM1_Start(void)
{
    TIM1->CR1 |= TIM_CR1_CEN;
}

void TIM1_Stop(void)
{
    TIM1->CR1 &= ~TIM_CR1_CEN;
}

void TIM1_UP_IRQHandler(void)
{
    printf("into interrupt...\n");
    TIM1->SR &= ~TIM_SR_UIF;
    TIM1_Stop();
}

文件:stm32/26_pwm_n_register/User/main.c

#include "usart.h"
#include "tim1.h"
#include "delay.h"

int main(void)
{
    USART_Init();
    TIM1_Init();

    printf("Hello, world!\n");

    TIM1_Start();

    while (1)
    {
    }
}

软件设计(HAL 库版)

项目路径stm32/27_pwm_n_hal

文件:stm32/27_pwm_n_hal/Core/Src/tim.c(MX_TIM1_Init)

TIM_HandleTypeDef htim1;

void MX_TIM1_Init(void)
{
  htim1.Instance = TIM1;
  htim1.Init.Prescaler = 7199;
  htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim1.Init.Period = 4999;
  htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim1.Init.RepetitionCounter = 4;
  htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  HAL_TIM_Base_Init(&htim1);
  HAL_TIM_PWM_Init(&htim1);

  TIM_OC_InitTypeDef sConfigOC = {0};
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 2500;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_LOW;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1);
}

文件:stm32/27_pwm_n_hal/Core/Src/stm32f1xx_it.c(TIM1 中断)

void TIM1_UP_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim1);
  HAL_TIM_PWM_Stop(&htim1, TIM_CHANNEL_1);
}

文件:stm32/27_pwm_n_hal/Core/Src/main.c

#include "main.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM1_Init();
  MX_USART1_UART_Init();

  __HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_UPDATE);
  __HAL_TIM_ENABLE_IT(&htim1, TIM_IT_UPDATE);
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);

  while (1)
  {
  }
}

HAL 库版 PWM

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

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

void MX_TIM5_Init(void)
{
    htim5.Instance = TIM5;
    htim5.Init.Prescaler = 7199;          // 72MHz / 7200 = 10KHz (0.1ms)
    htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim5.Init.Period = 99;               // ARR → 10KHz / 100 = 100Hz (10ms)
    HAL_TIM_Base_Init(&htim5);
    HAL_TIM_PWM_Init(&htim5);

    TIM_OC_InitTypeDef sConfigOC = {0};
    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 0;                  // 初始占空比 0%
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2);
}

// 启动 PWM 输出
HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);

// 修改占空比
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_2, 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_2, duty);
    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 计数