4. 定时器模块(TMR):基础定时器、PWM生成、输入捕获与输出比较的原理与代码实现

定时器,说白了就是单片机的“心跳”。没有它,很多时序相关的功能都玩不转。CMS32 的定时器模块功能挺全的,基础定时、PWM、输入捕获、输出比较,一个模块全搞定。我刚开始接触 CMS32 时,觉得定时器不就是计个数嘛,后来才发现,用好它,能省掉不少外部硬件。

4.1 定时器模块整体架构

CMS32 的定时器模块,内部有好几个通道。每个通道都可以独立配置。我个人习惯把定时器分成两大类:

  • 基础定时器:就是单纯的计数,产生中断。用来做延时、任务调度。
  • 功能定时器:带比较/捕获功能的通道。用来生成 PWM、测量脉宽、输出精确波形。

你想想看,一个芯片里塞这么多定时器通道,就是为了让你能同时处理多个时序任务。比如,一个通道跑 PWM 驱动电机,另一个通道捕获遥控器信号,互不干扰。

核心知识点:定时器的本质是计数器 + 比较器。计数器不停累加,比较器在计数值达到设定值时触发动作(中断、翻转引脚、更新占空比)。

CMS32 定时器模块结构图 系统时钟源 预分频器 16位计数器 比较/捕获寄存器 输出控制逻辑 中断控制 定时器输出引脚 输入捕获引脚 工作模式 基础定时:计数溢出中断 PWM:比较匹配翻转引脚 输入捕获:捕捉外部事件

4.2 基础定时器:延时与周期性任务

基础定时器,就是让计数器从0跑到65535(16位),溢出时产生中断。我在项目中常用它来做“非阻塞延时”。比如,按键消抖、LED闪烁,用定时器中断比用 delay() 死等靠谱多了。

4.2.1 工作原理

  • 计数器从 0 开始,每个时钟脉冲加1。
  • 当计数值等于 自动重装载值(ARR) 时,计数器清零,并产生更新事件(中断)。
  • 中断频率 = 时钟频率 / (预分频系数 + 1) / (ARR + 1)。

我的经验:预分频系数和ARR的值,最好算成整数。我曾经为了凑一个精确的1ms中断,调了半天分频系数,最后发现直接用系统时钟的1/1000分频,ARR设成1000,简单又准确。

4.2.2 代码实现:1ms定时中断

// 假设系统时钟为 48MHz
// 目标:1ms 中断一次
// 预分频系数:48-1(即 48分频),计数器时钟 = 48MHz / 48 = 1MHz
// ARR = 1000-1,溢出时间 = 1000 * 1us = 1ms

void Timer_Init(void) {
    // 1. 开启定时器时钟(以 TIM2 为例)
    RCC->APB1ENR |= (1 << 0);  // TIM2 时钟使能
    
    // 2. 配置预分频器
    TIM2->PSC = 48 - 1;         // 预分频值
    
    // 3. 配置自动重装载值
    TIM2->ARR = 1000 - 1;       // 自动重装载值
    
    // 4. 清除更新标志位
    TIM2->SR &= ~(1 << 0);     // 清除 UIF 标志
    
    // 5. 使能更新中断
    TIM2->DIER |= (1 << 0);    // 使能更新中断
    
    // 6. 使能定时器
    TIM2->CR1 |= (1 << 0);     // 使能计数器
    
    // 7. 配置 NVIC(中断优先级)
    NVIC_EnableIRQ(TIM2_IRQn);
    NVIC_SetPriority(TIM2_IRQn, 1);
}

// 中断服务函数
void TIM2_IRQHandler(void) {
    if (TIM2->SR & (1 << 0)) {  // 检查更新中断标志
        TIM2->SR &= ~(1 << 0); // 清除标志
        
        // 用户代码:每1ms执行一次
        // 比如:按键扫描、LED翻转
    }
}

注意:中断服务函数里不要做耗时操作。我曾经在中断里调了一个 printf 打印调试信息,结果导致定时器中断响应延迟,整个系统时序乱套了。中断里只做标志位设置,具体处理放到主循环里。

4.3 PWM生成:呼吸灯与电机控制

PWM,就是通过调节高低电平的占空比,来模拟模拟量输出。CMS32 的定时器做 PWM 特别方便,只需要配置好比较值,硬件自动翻转引脚。

4.3.1 原理简述

  • 计数器从0累加到ARR。
  • 当计数值小于 比较值(CCR) 时,输出高电平。
  • 当计数值大于等于 CCR 时,输出低电平。
  • 占空比 = CCR / (ARR + 1)。

说白了,ARR 决定周期,CCR 决定占空比。你想让 LED 呼吸,就不断改变 CCR 的值。

4.3.2 代码实现:PWM输出(1kHz,50%占空比)

// 配置 TIM2 通道1 输出 PWM
// 频率 = 48MHz / (48-1+1) / (1000-1+1) = 1kHz
// 占空比 = 500 / 1000 = 50%

void PWM_Init(void) {
    // 1. 时钟配置(同上)
    RCC->APB1ENR |= (1 << 0);
    
    // 2. 配置 GPIO 为复用功能(PA0 为 TIM2_CH1)
    GPIOA->MODER &= ~(3 << 0);  // 清除模式
    GPIOA->MODER |= (2 << 0);   // 复用功能模式
    GPIOA->AFR[0] |= (1 << 0);  // AF1 对应 TIM2
    
    // 3. 配置定时器
    TIM2->PSC = 48 - 1;          // 预分频
    TIM2->ARR = 1000 - 1;        // 周期
    
    // 4. 配置输出比较模式
    TIM2->CCMR1 |= (6 << 4);    // 模式1:PWM模式1(向上计数时,CNT<CCR时有效)
    TIM2->CCMR1 |= (1 << 3);    // 预装载使能
    
    // 5. 设置比较值(占空比)
    TIM2->CCR1 = 500;            // 50% 占空比
    
    // 6. 使能输出
    TIM2->CCER |= (1 << 0);     // 使能通道1输出
    
    // 7. 使能定时器
    TIM2->CR1 |= (1 << 0);
}

// 动态修改占空比
void PWM_SetDuty(uint16_t duty) {
    TIM2->CCR1 = duty;           // 直接修改比较值
}

避坑指南:修改 CCR 时,如果定时器正在运行,建议先使能预装载功能(CCMRx 的 OCxPE 位),这样 CCR 会在下一个周期开始时才生效,避免产生不完整的脉冲。我曾经没开预装载,结果在修改占空比时,输出了一个极窄的毛刺,把后级的 MOS 管驱动给烧了。

4.4 输入捕获:测量脉宽与频率

输入捕获,就是测量外部信号的脉宽或频率。比如,测量遥控器信号的脉宽,或者测速编码器的脉冲频率。

4.4.1 工作原理

  • 外部信号从定时器的捕获引脚输入。
  • 当检测到指定边沿(上升沿或下降沿)时,硬件自动将当前计数器的值锁存到捕获寄存器。
  • 通过两次捕获值的差值,计算出脉宽或周期。

4.4.2 代码实现:测量高电平脉宽

// 配置 TIM2 通道1 输入捕获
// 测量 PA0 引脚上的高电平脉宽

volatile uint16_t capture_value_1 = 0;
volatile uint16_t capture_value_2 = 0;
volatile uint8_t capture_done = 0;

void Capture_Init(void) {
    // 1. 时钟配置
    RCC->APB1ENR |= (1 << 0);
    
    // 2. GPIO 配置(输入模式)
    GPIOA->MODER &= ~(3 << 0);  // 输入模式
    
    // 3. 定时器基础配置
    TIM2->PSC = 48 - 1;          // 1MHz 计数频率(1us 分辨率)
    TIM2->ARR = 0xFFFF;          // 最大计数值
    
    // 4. 配置输入捕获
    TIM2->CCMR1 |= (1 << 0);    // 通道1 映射到 TI1
    TIM2->CCMR1 |= (0 << 2);    // 每次捕获都更新
    
    // 5. 配置捕获边沿:先捕获上升沿
    TIM2->CCER |= (1 << 0);     // 上升沿捕获
    
    // 6. 使能捕获中断
    TIM2->DIER |= (1 << 1);     // 使能 CC1 中断
    
    // 7. 使能定时器
    TIM2->CR1 |= (1 << 0);
    
    NVIC_EnableIRQ(TIM2_IRQn);
}

void TIM2_IRQHandler(void) {
    if (TIM2->SR & (1 << 1)) {  // CC1 中断
        TIM2->SR &= ~(1 << 1);
        
        if (!capture_done) {
            // 第一次捕获(上升沿)
            capture_value_1 = TIM2->CCR1;
            
            // 切换为下降沿捕获
            TIM2->CCER |= (1 << 1);  // 设置 CC1P 为下降沿
            capture_done = 1;
        } else {
            // 第二次捕获(下降沿)
            capture_value_2 = TIM2->CCR1;
            
            // 计算脉宽(单位:us)
            uint16_t pulse_width;
            if (capture_value_2 >= capture_value_1) {
                pulse_width = capture_value_2 - capture_value_1;
            } else {
                // 处理溢出情况
                pulse_width = (0xFFFF - capture_value_1) + capture_value_2 + 1;
            }
            
            // 用户代码:使用 pulse_width 值
            
            // 切换回上升沿捕获,准备下一次测量
            TIM2->CCER &= ~(1 << 1);
            capture_done = 0;
        }
    }
}

我的经验:输入捕获时,一定要考虑计数器溢出的情况。如果被测信号周期很长,可能两次捕获之间计数器已经溢出好几次了。我一般会在中断里再加一个溢出计数器,或者把预分频调大,降低计数频率,确保不会溢出。

4.5 输出比较:精确波形控制

输出比较,和 PWM 有点像,但更灵活。它可以在计数值达到比较值时,执行多种动作:翻转引脚、输出高电平、输出低电平、触发中断。

4.5.1 应用场景

  • 产生单脉冲:比如,触发一个外部设备,只输出一个特定宽度的脉冲。
  • 产生任意波形:通过多个比较通道,组合出复杂的时序。
  • 软件定时:在指定时间点触发中断,执行特定任务。

4.5.2 代码实现:单脉冲输出

// 配置 TIM2 通道1 输出比较
// 在计数到 500 时,翻转 PA0 引脚

void OC_Init(void) {
    // 1. 时钟配置
    RCC->APB1ENR |= (1 << 0);
    
    // 2. GPIO 配置(复用功能)
    GPIOA->MODER &= ~(3 << 0);
    GPIOA->MODER |= (2 << 0);
    GPIOA->AFR[0] |= (1 << 0);
    
    // 3. 定时器基础配置
    TIM2->PSC = 48 - 1;          // 1MHz
    TIM2->ARR = 1000 - 1;        // 1ms 周期
    
    // 4. 配置输出比较模式
    TIM2->CCMR1 |= (3 << 4);    // 模式3:翻转(Toggle)
    TIM2->CCMR1 |= (1 << 3);    // 预装载使能
    
    // 5. 设置比较值
    TIM2->CCR1 = 500;            // 在 500us 时翻转
    
    // 6. 使能输出
    TIM2->CCER |= (1 << 0);
    
    // 7. 使能定时器
    TIM2->CR1 |= (1 << 0);
}

4.6 四种模式对比

模式 核心功能 典型应用 关键寄存器
基础定时 计数溢出产生中断 延时、任务调度 PSC, ARR, SR
PWM生成 自动输出可调占空比方波 LED调光、电机调速 PSC, ARR, CCR, CCMR
输入捕获 捕获外部信号边沿时的计数值 脉宽测量、频率测量 CCR, CCER, CCMR
输出比较 计数值匹配时执行指定动作 单脉冲、任意波形 CCR, CCMR, CCER

总结一下:定时器模块是 CMS32 的“瑞士军刀”。基础定时是基本功,PWM 是输出利器,输入捕获是测量神器,输出比较是波形艺术家。把这四种模式吃透,大部分时序问题都能搞定。