4. CMS32 定时器:从 SysTick 到输入捕获的实战之路

定时器,说白了就是单片机的“心跳”。没有它,你连个精准的延时都做不了。CMS32 系列在这方面做得挺实在——从最简单的 SysTick 到高级的输入捕获,一套下来基本覆盖了嵌入式开发的常见需求。

我个人习惯把定时器分成三类:系统节拍器(SysTick)、通用定时器(TIM)、以及高级定时器。今天咱们重点聊前两个,PWM 和输入捕获也会手把手带大家过一遍。

4.1 SysTick:系统的心跳

SysTick 是 Cortex-M 内核自带的 24 位递减计数器。它不属于芯片厂商的外设,而是 ARM 内核的标配。这意味着什么?你写的 SysTick 代码,换到任何 Cortex-M 芯片上都能跑。

我在项目中经常用它做两件事:

  • 操作系统的心跳:FreeRTOS 的滴答定时器就是靠它
  • 精准延时:比软件循环靠谱得多

核心要点:SysTick 是 24 位计数器,从你设定的重装载值向下数到 0,然后触发中断,再自动重装。

配置代码其实很简单:

// SysTick 配置示例:1ms 中断一次
// 假设系统时钟 48MHz
#define SYSTICK_LOAD  (48000000 / 1000 - 1)  // 1ms

void SysTick_Init(void)
{
    // 1. 设置重装载值
    SysTick->LOAD = SYSTICK_LOAD;
    
    // 2. 清空当前计数值
    SysTick->VAL = 0;
    
    // 3. 配置时钟源和使能中断
    //    CLKSOURCE=1 使用系统时钟
    //    TICKINT=1   使能中断
    //    ENABLE=1    启动定时器
    SysTick->CTRL = (1 << 2) | (1 << 1) | (1 << 0);
}

// 中断服务函数
void SysTick_Handler(void)
{
    // 每 1ms 进入一次
    // 可以在这里做系统节拍计数
    g_sys_tick++;
}

我的经验:SysTick 的重装载值不要设得太大。24 位最大值是 16,777,215,如果时钟 48MHz,最大定时约 349ms。超过这个数,计数器就溢出了。我曾经见过有人设了 1 秒,结果定时完全不准——排查了半天才发现是这个问题。

4.2 通用定时器 TIM:真正的多面手

SysTick 虽然好用,但只有一个。真正干活还得靠通用定时器。CMS32 的 TIM 模块,我总结下来有这几个核心能力:

功能 说明 典型应用
基本定时 向上/向下计数,可产生中断 周期性任务调度
PWM 输出 多通道独立占空比控制 电机调速、LED 调光
输入捕获 捕获外部信号边沿 频率测量、脉冲宽度测量
编码器接口 正交解码 电机转速/位置检测

嗯,这里要注意:CMS32 的 TIM 有多个实例,每个的寄存器基地址不同,但结构体定义是一样的。写驱动时可以用指针数组来管理,代码会清爽很多。

4.3 定时器中断实战:精准定时

先来个最基础的——定时器中断。说白了就是让定时器数到某个值,然后跳进中断函数。

// TIM 定时中断配置
// 目标:100ms 中断一次,时钟 48MHz,预分频 48000-1

void TIM_Init(void)
{
    // 1. 使能 TIM 时钟(以 TIM0 为例)
    RCC->APB1ENR |= (1 << 0);  // TIM0 时钟使能
    
    // 2. 配置预分频器
    //    计数器时钟 = 48MHz / 48000 = 1kHz
    TIM0->PSC = 48000 - 1;
    
    // 3. 设置自动重装载值
    //    100ms = 100 次计数 @ 1kHz
    TIM0->ARR = 100 - 1;
    
    // 4. 清空计数器
    TIM0->CNT = 0;
    
    // 5. 使能更新中断
    TIM0->DIER |= (1 << 0);  // UIE 位
    
    // 6. 使能 NVIC 中断
    NVIC_EnableIRQ(TIM0_IRQn);
    
    // 7. 启动定时器
    TIM0->CR1 |= (1 << 0);  // CEN 位
}

// 中断服务函数
void TIM0_IRQHandler(void)
{
    // 检查更新中断标志
    if(TIM0->SR & (1 << 0))
    {
        // 清除标志位
        TIM0->SR &= ~(1 << 0);
        
        // 用户代码:每 100ms 执行一次
        g_tim_flag = 1;
    }
}

避坑指南:我曾经在中断服务函数里忘了清标志位,结果程序一直卡在中断里出不来。CMS32 的中断标志位是写 0 清除的,不是写 1。这点和 STM32 不一样,刚转过来的朋友要特别注意。

4.4 PWM 输出:让 LED 呼吸起来

PWM 输出,说白了就是让定时器的输出引脚按照你设定的占空比翻转。CMS32 的 TIM 支持边沿对齐和中心对齐两种模式。

我个人习惯用边沿对齐模式,配置起来更直观:

// PWM 输出配置
// 目标:1kHz PWM,50% 占空比
// 使用 TIM0 的通道 1(CH1)

void PWM_Init(void)
{
    // 1. 时钟和 GPIO 配置(略)
    //    注意:PWM 输出引脚要配置为复用推挽输出
    
    // 2. 配置 TIM 时基
    TIM0->PSC = 48 - 1;       // 48MHz / 48 = 1MHz
    TIM0->ARR = 1000 - 1;     // 1MHz / 1000 = 1kHz
    
    // 3. 配置通道 1 为 PWM 模式 1
    //    OC1M = 110 (PWM 模式 1)
    //    OC1PE = 1 (预装载使能)
    TIM0->CCMR1 |= (6 << 4) | (1 << 3);
    
    // 4. 设置占空比:50%
    TIM0->CCR1 = 500;
    
    // 5. 使能通道 1 输出
    TIM0->CCER |= (1 << 0);  // CC1E 位
    
    // 6. 使能预装载寄存器
    TIM0->CR1 |= (1 << 4);   // ARPE 位
    
    // 7. 启动定时器
    TIM0->CR1 |= (1 << 0);   // CEN 位
}

// 动态修改占空比
void PWM_SetDuty(uint16_t duty)
{
    TIM0->CCR1 = duty;
}

你想想看,如果让占空比从 0 慢慢增加到 1000,再从 1000 减到 0,LED 就会像呼吸一样亮暗变化。这就是呼吸灯的原理。

4.5 输入捕获:测量外部信号的频率

输入捕获,说白了就是定时器“偷看”外部引脚的电平变化。当检测到上升沿或下降沿时,它会立刻把当前计数器的值锁存到捕获寄存器里。

我在项目中用它来测量 PWM 遥控信号的脉宽:

// 输入捕获配置
// 测量外部信号的频率
// 使用 TIM0 的通道 1(CH1)

void InputCapture_Init(void)
{
    // 1. GPIO 配置为输入模式(略)
    
    // 2. 配置 TIM 时基
    TIM0->PSC = 48 - 1;       // 1MHz 计数频率
    TIM0->ARR = 0xFFFF;       // 最大计数值
    
    // 3. 配置通道 1 为输入捕获
    //    CC1S = 01 (映射到 TI1)
    //    IC1PSC = 00 (无预分频)
    //    IC1F = 0000 (无滤波)
    TIM0->CCMR1 |= (1 << 0);
    
    // 4. 配置捕获边沿:上升沿
    TIM0->CCER |= (1 << 0);  // CC1E = 1, CC1P = 0
    
    // 5. 使能捕获中断
    TIM0->DIER |= (1 << 1);  // CC1IE 位
    
    // 6. 使能 NVIC
    NVIC_EnableIRQ(TIM0_IRQn);
    
    // 7. 启动定时器
    TIM0->CR1 |= (1 << 0);
}

// 捕获中断处理
uint32_t g_capture_val;
uint8_t  g_capture_flag;

void TIM0_IRQHandler(void)
{
    if(TIM0->SR & (1 << 1))  // CC1IF 标志
    {
        TIM0->SR &= ~(1 << 1);
        
        // 读取捕获值
        g_capture_val = TIM0->CCR1;
        g_capture_flag = 1;
    }
}

我的经验:测量频率时,最好连续捕获两次上升沿,用两次的差值计算周期。如果信号频率很低,要注意计数器溢出问题。我一般会在溢出中断里加一个变量做溢出计数,这样就能测量很宽的频率范围了。

4.6 知识结构总览

下面这张图把 CMS32 定时器的知识体系串起来了。你可以把它当作学习路线图:

CMS32 定时器知识体系 CMS32 定时器知识体系 CMS32 定时器 SysTick 定时器 通用定时器 TIM 高级定时器 系统心跳 精准延时 定时中断 PWM 输出 输入捕获 编码器接口 刹车功能 本章重点:SysTick 基础 + 通用定时器的定时中断、PWM、输入捕获 高级定时器将在后续章节深入讲解

4.7 实战要点总结

最后,我把这章的核心要点整理成清单,方便你复习:

  1. SysTick:24 位递减计数器,内核自带,适合做系统心跳和延时
  2. 定时器中断:配置 PSC 和 ARR 决定中断频率,别忘了清标志位
  3. PWM 输出:CCR 控制占空比,ARR 控制频率,注意引脚要配成复用功能
  4. 输入捕获:捕获边沿事件,测量脉宽或频率,注意溢出处理
  5. 避坑:时钟使能、中断优先级、标志位清除顺序——这三样出问题最多

一句话总结:定时器是嵌入式开发的基石。SysTick 让你“动起来”,通用定时器让你“精准控制”,PWM 和输入捕获则是和外部世界打交道的桥梁。把这章吃透了,后面的 Bootloader 开发会顺畅很多。