--- 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 **参考**:参考手册 §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`** ```c #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`** ```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`** ```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) ```c 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`** ```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`** ```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`** ```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 + 回调) ```c 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`**(输入捕获回调) ```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`** ```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 双通道 + 从模式) ```c 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`** ```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`** ```c #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`** ```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`** ```c #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`**(初始化部分差异) ```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`** ```c #ifndef __TIM1_H #define __TIM1_H #include "stm32f10x.h" #include void TIM1_Init(void); void TIM1_Start(void); void TIM1_Stop(void); #endif ``` **文件:`stm32/26_pwm_n_register/Hardware/TIM/tim1.c`** ```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`** ```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) ```c 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 中断) ```c 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`** ```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()` 修改占空比。 ```c // 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` | CubeMX 图形化配置 | | PWM 输出使能 | `CCER |= CC1E` | `HAL_TIM_PWM_Start(htim, CH)` | | 修改占空比 | `CCR1 = value` | `__HAL_TIM_SET_COMPARE(htim, CH, val)` | | 修改频率 | `ARR = new_value` | 改 `htim.Init.Period` 重新 Init | | 输入捕获 | `CCMR1 |= CC1S_0` | `HAL_TIM_IC_Start_IT(htim, CH)` | | 读取捕获值 | `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 计数 ```