tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(进阶篇)V1.0.1 — 第1章:PWM + 配套代码20~27" author: "尚硅谷研究院" date: 2026-07-15
用生活理解:PWM 就像快速开关灯——如果你的手足够快(每秒 1000 次),眼睛看见的就不是闪烁而是灯光变暗了。开的时间比例(占空比)越大就越亮。呼吸灯就是占空比从 0% 逐渐增加到 100% 再回来,像呼吸一样平滑。输入捕获就像用第二个秒表测量第一个秒表的读数——测量外部信号的频率或脉宽。
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 ——————→
| 寄存器 | 功能 |
|---|---|
| CCMR1 | 捕获/比较模式寄存器1(CH1/CH2 模式配置、预装载使能) |
| CCMR2 | 捕获/比较模式寄存器2(CH3/CH4 模式配置) |
| CCER | 捕获/比较使能寄存器(CCxE 输出使能、CCxP 输出极性) |
| CCR1~4 | 捕获/比较值寄存器(PWM 时存占空比,捕获时存事件发生时的 CNT) |
参考:参考手册 §14(TIM 寄存器描述)
TIM 的 ARR 和 CCRx 都有影子寄存器机制:
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);
}
}
项目路径: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);
}
}
输入捕获可测量外部信号的频率或脉宽。原理:记录两次边沿事件发生时的 CNT 值,差值 = 脉冲宽度或周期。
1. GPIO 配置为输入(浮空/上拉)
2. TIM 启用输入捕获模式(CCMRx.ICx=1)
3. 配置捕获边沿(CCER.CCxP 上升/下降沿)
4. 使能捕获中断(DIER.CCxIE)
5. 在 ISR 中读取 CCRx,计算差值
项目路径: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 + 1) / 1000.0;
}
// 获取PWM频率,单位为Hz
double TIM4_GetPWMFreq(void)
{
return 1000000.0 / (TIM4->CCR1 + 1);
}
项目路径: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_GetPWMDuty(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_GetPWMDuty(void)
{
return (TIM4->CCR2 + 1) * 1.0 / (TIM4->CCR1 + 1);
}
原理:CH1(TI1 -> IC1)捕获上升沿,此时 CNT 值存入 CCR1(周期);CH2(TI1 -> IC2)捕获下降沿,CNT 值存入 CCR2(高电平宽度)。从模式复位在 TI1FP1 上升沿自动清零 CNT,无需中断干预。占空比 = (CCR2 + 1) / (CCR1 + 1)。
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 | — | — |