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_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
}
}
项目路径: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 库使用 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 | — | — |