浮云落笔

浮云落笔

首页
瞬间
反馈
浮云落笔

浮云落笔

首页 瞬间 反馈
  1. 首页
  2. stm32
  3. STM32 定时器实验全解:三阶段完整代码与思路(时基 → 输出比较 → PWM 呼吸灯)

STM32 定时器实验全解:三阶段完整代码与思路(时基 → 输出比较 → PWM 呼吸灯)

  • stm32
  • 发布于 2026-09-24
  • 28 次阅读
flor
flor

平台:STM32F103ZET6 @ 72MHz · LED=PB5(低电平点亮)· 工程名 tim_led 三个阶段是同一条思路的递进:每一步都把上一课软件干的活移交给硬件

阶段一:时基——1ms 定时,LED 1Hz 闪烁(软件计数版)

思路

先解决"HAL_Delay 会阻塞 CPU"的问题:让定时器硬件自己数数,数满 1ms 敲一次门(中断),CPU 平时自由,只在被敲门时干一小点活。LED 要 1Hz 闪(亮 500ms 灭 500ms),但定时器只会每 1ms 响一次铃——"500ms"这个大时间尺度由软件在回调里数数实现:攒 500 个 1ms 就翻转一次引脚。

CubeMX 配置

项 值 算法
TIM2 Clock Source Internal Clock 用内部 72MHz
Prescaler (PSC) 71 72M ÷ (71+1) = 1MHz,每格 1µs
Counter Period (ARR) 999 1000 格 × 1µs = 1ms 溢出一次
NVIC TIM2 global interrupt 打勾 放行中断线

注意 +1 陷阱:PSC/ARR 都从 0 数起,所以 71 是"72 档"、999 是"1000 格"。TIM2 挂 APB1(名义 36M),但定时器走 ×2 后门实际吃 72MHz。

完整代码

/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim2);   // 拧发条:CEN=1 开始数数,UIE=1 数满允许敲门
/* USER CODE END 2 */
/* USER CODE BEGIN 4 */
// TIM2 每 1ms 进来一次(中断上下文:快进快出,只做加减和翻转)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2) {
        static uint16_t ms = 0;              // static:跨中断保留进度
        if (++ms >= 500) {                   // 攒满 500 个 1ms
            ms = 0;
            HAL_GPIO_TogglePin(led1_GPIO_Port, led1_Pin);  // 翻转 → 1Hz 闪烁
        }
    }
}
/* USER CODE END 4 */

逐行讲解

  • HAL_TIM_Base_Start_IT(&htim2):CubeMX 的 MX_TIM2_Init 只负责"装填参数"(PSC、ARR 写进寄存器),此时计数器是静止的(CEN=0)。这一行干两件事:CEN=1(开始数)+ UIE=1(允许 Update 事件触发中断)。不调它,配置再对也是死表。
  • if (htim->Instance == TIM2):HAL_TIM_PeriodElapsedCallback 是全局弱符号回调——将来你开 TIM3、TIM4,它们的 Update 事件也进这同一个函数。不判断是谁,就会串台。
  • static uint16_t ms = 0:本阶段的灵魂。普通局部变量每次函数返回就消失,static 让它活过函数返回——每次中断进来,ms 都是"上次的值"。没有 static,ms 永远从 0 数起,永远到不了 500,灯永远不闪。
  • if (++ms >= 500):前缀自增(先加再比)。500 个 1ms = 500ms。
  • HAL_GPIO_TogglePin:翻转 PB5。亮 500ms、灭 500ms,1Hz。
  • 中断纪律:回调里只做加减和写引脚——中断上下文必须快进快出(拖久了下一次 1ms 到点就堆积)。

现象与本质

LED 1Hz 闪烁。本质:硬件管"1ms 这个小周期",软件数"500 次这个大尺度"。缺点也明显:每秒 1000 次中断,只为数一个数。


阶段二:输出比较——双门槛定点亮灭(零计数版)

思路

阶段一里"500ms"靠软件数——能不能让硬件直接在 500ms 处响铃?输出比较(OC)就是给 CNT 再加一道门槛 CCR:CNT 数到 CCR 的瞬间,Compare 事件爆发。于是周期里有两道硬件门槛:

  • Update 事件(CNT 溢出归零,t=0s)→ 点亮
  • Compare 事件(CNT 数到 CCR1,t=0.5s)→ 熄灭

回调里一个计数器都不需要——两道门槛把时间点定死,软件只负责"听到铃声做动作"。这就是 PWM 的手工版。

CubeMX 配置

项 值 算法
TIM2 Channel1 Output Compare No Output 只比较不占引脚(TIM2_CH1=PA0 是 WKUP 按键脚,不能碰)
Prescaler (PSC) 7199 72M ÷ 7200 = 10kHz,每格 0.1ms
Counter Period (ARR) 9999 10000 格 = 1s 周期
Pulse(CCR1 的界面名) 5000 第 5000 格 = 500ms 处响铃

为什么 PSC 从 71 改成 7199?ARR 是 16 位(最大 65535),1µs 粒度最多数 65.5ms——凑 1 秒周期必须先降频。这就是 (PSC, ARR) 的因式分解自由度:先定粒度,再凑周期。

完整代码

/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim2);              // 数数 + Update中断(周期起点事件)
HAL_TIM_OC_Start_IT(&htim2, TIM_CHANNEL_1);  // 再开CH1比较中断(CCR门槛事件)
/* USER CODE END 2 */
/* USER CODE BEGIN 4 */
// Update事件:CNT溢出归零的瞬间 = 周期起点 → 点亮
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
        HAL_GPIO_WritePin(led1_GPIO_Port, led1_Pin, GPIO_PIN_RESET);  // 低电平=亮
}

// Compare事件:CNT数到CCR1的瞬间 → 熄灭
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
        HAL_GPIO_WritePin(led1_GPIO_Port, led1_Pin, GPIO_PIN_SET);    // 高电平=灭
}
/* USER CODE END 4 */

逐行讲解

  • 两声令:HAL_TIM_Base_Start_IT 开 Update 中断许可(UIE=1);HAL_TIM_OC_Start_IT 开 CH1 的 Compare 中断许可(CC1IE=1)+ 通道使能。两个 Start 各管各的许可灯,缺一个就少一种铃声。
  • 一个入口,两个回调:两种事件走同一个 TIM2_IRQHandler,HAL 在 HAL_TIM_IRQHandler 里查标志分家——UIF 走 PeriodElapsedCallback,CC1IF 走 OC_DelayElapsedCallback。
  • OC 回调多判一个 htim->Channel:4 个通道共用一个 OC 回调,Channel 告诉你是哪个通道在响(这里必须确认是 CH1)。
  • 两个回调名字都是弱符号:名字一字不差才构成覆盖。笔误一个字母 = 函数成孤儿 = 编译零警告、现象纹丝不动(真实踩过:HLA_TIM_PeriodElapsedClallback)。防御:从 HAL 源码抄、用 VSCode 补全敲、写完 grep 核对。
  • 写引脚用 SET/RESET 而不是 Toggle:亮灭动作分别绑定在两道门槛上,语义清晰——这正是 PWM"起点置高、比较处置低"的雏形。

现象与本质

现象和阶段一一样是 1Hz 闪烁,但内核换了引擎:中断从 1000 次/秒降到 2 次/秒(CPU 占用差 500 倍),定时精度从"受中断延迟影响"变成"硬件门槛精准到格"。


阶段三:PWM——硬件方波 + 呼吸灯(最终版)

思路

两个硬件事实决定了方案:

  1. 硬件 PWM 的规则:通道配成 PWM 模式后,硬件自动执行"CNT < CCR → 引脚高,CNT ≥ CCR → 引脚低"——零中断零 CPU。用 TIM3_CH3(PB0)做硬件 PWM 演示(PA6/PA7 被板载 Flash 占用,选脚先查板载外设)。
  2. LED 在 PB5——不属于任何定时器通道,硬件 PWM 够不着它 → 呼吸灯必须软件 PWM。

软件 PWM 的架构是载波与包络分层:

  • 载波层:阶段二的双门槛机器,把周期从 1s 缩到 10ms(100Hz)——这个频率不能慢,慢了眼睛看见闪烁
  • 包络层:Update 回调每个载波周期推一下占空比 duty(±1,在 0↔100 折返),用 __HAL_TIM_SET_COMPARE 动态写进 CCR1
  • 眼睛+LED 是低通滤波器:100Hz 快速亮灭只留平均亮度,正比占空比 → duty 扫 0↔100,灯就呼吸

CubeMX 配置

项 值 算法
TIM2 Prescaler 7199(不变) 每格 0.1ms
TIM2 Counter Period 99 100 格 × 0.1ms = 10ms = 100Hz 载波
TIM2 Pulse 0 代码每周期重写,初值无所谓
TIM3 Clock Source Internal Clock
TIM3 Channel3 PWM Generation CH3 引脚自动分配 PB0
TIM3 PSC / ARR / Pulse 7199 / 9999 / 5000 1Hz、50% 占空比(人眼级慢动作,方便调试器采样验证)
TIM3 NVIC 不勾 硬件 PWM 零中断——和 TIM2 软件版的本质区别

关键刻度对齐:duty 是 0~100 的百分数,直接写进 CCR1,所以 ARR 必须=99——在 ARR=99 的世界里 100/100=100%,在 ARR=9999 的世界里 100/10000 只有 1%(真实踩过:PSC/ARR 改反,呼吸在 1% 亮度里扫,肉眼等于没亮)。占空比是比值,duty 的尺子必须和 ARR 同刻度。

完整代码

/* USER CODE BEGIN PV */
uint8_t duty = 0;     // 当前占空比 0~100(呼吸的"肺活量")
int8_t  dir  = 1;     // 呼吸方向:+1 渐亮,-1 渐暗
/* USER CODE END PV */
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim2);              // TIM2:呼吸灯的软件PWM机器(阶段二原样)
HAL_TIM_OC_Start_IT(&htim2, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);   // TIM3:PB0 硬件PWM,零中断
/* USER CODE END 2 */
/* USER CODE BEGIN 4 */
// Update事件 = 每个PWM周期(10ms)的起点:推进呼吸坡度,装填本周期占空比
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
  if(htim->Instance == TIM2){
    duty += dir;                                        // 坡度每周期 ±1
    if(duty >= 100){ duty = 100; dir = -1; }            // 亮到头,折返渐暗
    if(duty <= 0)  { duty = 0;   dir = +1; }             // 暗到头,折返渐亮
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty); // 门槛=本周期占空比
    if(duty == 0)
      HAL_GPIO_WritePin(led1_GPIO_Port,led1_Pin,GPIO_PIN_SET);   // 0%:直接灭
    else
      HAL_GPIO_WritePin(led1_GPIO_Port,led1_Pin,GPIO_PIN_RESET); // >0%:先亮,等Compare来灭
  }
}

// Compare事件 = CNT数到duty处:熄灭(与阶段二一字不差)
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim){
  if(htim->Instance == TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1){
    HAL_GPIO_WritePin(led1_GPIO_Port,led1_Pin,GPIO_PIN_SET);
  }
}
/* USER CODE END 4 */

逐行讲解

  • PV 区两个变量:duty 是当前占空比(呼吸的"肺活量"),dir 是方向(+1 渐亮 / −1 渐暗)。两个变量合起来就是一个微型呼吸状态机。
  • HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3):硬件 PWM 的启动只要一行,连 NVIC 都不用配——波形完全由硬件按"CNT<CCR3 输出高"的规则自己画,CPU 零参与。
  • duty += dir + 两个折返 if:三角波坡度。duty 到 100 掉头向下、到 0 掉头向上,0↔100 来回扫。呼吸节奏 = 200 步 × 10ms = 2 秒一个呼吸。
  • __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty):呼吸的全部秘密——CCR 不是死的,运行中随时可改。这个宏把新 duty 写进 CCR1,本周期后半段 Compare 事件就按新门槛触发。
  • if (duty == 0) 特判:duty=0 时 CCR1=0,Compare 事件和 Update 在 CNT=0 同一瞬间爆发,而 HAL 先处理 Compare 后处理 Update——不特判,0% 会变成常亮(阶段二调试时那个"常亮 bug"的缩小版)。所以 0% 时 Update 回调里直接写灭,不指望 Compare。
  • else 分支先亮:>0% 时周期起点先点亮,熄灭的活儿留给 Compare 事件在"数到 duty"处干——亮多久完全由 duty 说了算。
  • Compare 回调:与阶段二一字未改——这就是分层的红利,载波机器不用动,只换包络逻辑。

现象与本质

  • PB5 呼吸灯:约 2 秒一个渐亮渐暗循环,亮度平滑变化
  • PB0 硬件 PWM:1Hz、50% 方波(调试器实时采样引脚电平验证:6 次采样 0/1 完美交替,PSC/ARR/CCR 寄存器读数 7199/9999/5000 与计算一致)

三阶段演进总览

阶段一(时基) 阶段二(输出比较) 阶段三(PWM 呼吸灯)
事件源 Update × 1 Update + Compare 同阶段二 + 动态改 CCR
回调里干的事 数 500 个 ms 直接写引脚,零计数 推坡度 + 写引脚
中断频率 1000 次/秒 2 次/秒 200 次/秒(载波)+ 0(硬件 PWM)
定时精度 受中断延迟影响 硬件门槛,精准到格 同左,占空比运行中可调
本质 硬件数周期,软件数精细 硬件记两个时间点 载波/包络分层,CPU 只管节奏

一句话总结整个模块:定时器的学习史就是一部"移交史"——把"等"从 CPU 移交给硬件(阶段一),把"数"移交给门槛(阶段二),把"开关引脚"移交给比较器(阶段三的硬件 PWM);CPU 最后只剩一件体面的活:决定呼吸的节奏。

目录
湘ICP备2025147565号-1
gongan beian 湘公网安备43102602000213号