4. 定时器中断:SysTick、通用定时器与PWM中断实战

定时器中断,可以说是嵌入式开发里最常用的外设中断之一。我个人做项目时,几乎每个工程都会用到至少一个定时器——要么做系统心跳,要么做精确延时,要么生成PWM波。CMS32的定时器资源挺丰富的,咱们今天就把SysTick、通用定时器和PWM中断这三个核心应用捋清楚。

4.1 SysTick定时器:系统的“心跳”

SysTick是Cortex-M0内核自带的24位递减计数器。说白了,它就是给操作系统或者裸机程序提供一个固定的时间基准。我习惯把它配置成1ms中断一次,这样整个系统就有了一个“心跳”。

核心要点:SysTick是内核级外设,不依赖芯片厂商的定时器模块。所有Cortex-M系列单片机都有,代码可移植性极强。

4.1.1 SysTick寄存器配置

SysTick只有4个寄存器,操作起来非常简单:

寄存器 地址 功能
SYST_CSR 0xE000E010 控制与状态寄存器(使能、中断使能、标志位)
SYST_RVR 0xE000E014 重装载值寄存器(24位有效)
SYST_CVR 0xE000E018 当前值寄存器(读清零)
SYST_CALIB 0xE000E01C 校准寄存器(一般不用)

配置SysTick的步骤其实就三步:

  1. 设置重装载值(SYST_RVR)
  2. 清除当前值(写SYST_CVR任意值)
  3. 使能SysTick并开启中断(写SYST_CSR)

重装载值的计算公式:

重装载值 = 系统时钟频率 / 目标中断频率 - 1
// 例如:系统时钟48MHz,目标1ms中断
// 重装载值 = 48000000 / 1000 - 1 = 47999

我的习惯:SysTick中断函数里只做“标记”操作,比如置一个全局标志位。千万别在里面做复杂计算或者延时,否则整个系统的实时性就毁了。

4.1.2 代码示例:1ms SysTick中断

/* SysTick初始化函数 */
void SysTick_Init(void)
{
    /* 1. 关闭SysTick,确保配置期间不产生中断 */
    SYST_CSR = 0x00;
    
    /* 2. 设置重装载值:48MHz / 1000Hz = 48000 */
    SYST_RVR = 48000 - 1;
    
    /* 3. 清除当前值 */
    SYST_CVR = 0;
    
    /* 4. 配置并使能:使用处理器时钟,使能中断,使能计数器 */
    SYST_CSR = 0x07;  // Bit0: ENABLE, Bit1: TICKINT, Bit2: CLKSOURCE
}

/* SysTick中断服务函数 */
void SysTick_Handler(void)
{
    static uint32_t ms_counter = 0;
    
    ms_counter++;
    
    /* 每1ms置一次标志 */
    g_systick_flag = 1;
    
    /* 每1000ms(1秒)翻转一次LED */
    if(ms_counter >= 1000)
    {
        ms_counter = 0;
        LED_TOGGLE();
    }
}

嗯,这里要注意:SysTick_Handler这个函数名是固定的,不能自己随便起。CMS32的启动文件里已经帮你把中断向量表写好了,你只需要实现这个函数就行。

4.2 通用定时器中断:更灵活的计时方案

SysTick虽然好用,但毕竟只有一个。如果你需要多个不同频率的定时中断,那就得用通用定时器了。CMS32系列通常有2-4个通用定时器模块,每个都可以独立配置。

我在项目中遇到过这样一个场景:一个传感器需要10ms读取一次,另一个通信协议需要50ms超时判断,还有一个按键消抖需要20ms扫描。三个不同周期,一个SysTick根本不够用。这时候通用定时器就派上用场了。

4.2.1 通用定时器结构

CMS32 通用定时器结构框图 时钟源 预分频器 16位计数器 自动重装载 重装载值 中断输出 说明:计数器从0向上计数到自动重装载值,产生更新事件(中断)。 预分频器可降低计数时钟频率,实现更长的定时周期。

4.2.2 定时时间计算

通用定时器的定时时间由三个参数决定:

定时时间 = (预分频值 + 1) × (自动重装载值 + 1) / 定时器时钟频率

// 例如:定时器时钟48MHz,预分频值47999,自动重装载值999
// 定时时间 = 48000 × 1000 / 48000000 = 1秒

我曾经踩过的坑:预分频器和自动重装载值都是16位的,最大值65535。如果你需要很长的定时时间,记得先算一下会不会溢出。我有个项目就因为这个原因,定时器死活不按预期工作,查了半天才发现是重装载值设超了。

4.2.3 代码示例:通用定时器1秒中断

/* 通用定时器0初始化:1秒中断 */
void TIMER0_Init(void)
{
    /* 1. 使能定时器时钟 */
    SYS->CLKEN |= (1 << 6);  // 假设TIMER0时钟使能位在bit6
    
    /* 2. 配置定时器模式:向上计数,自动重装载 */
    TIMER0->CTRL = 0x00;      // 先停止定时器
    TIMER0->CTRL |= (1 << 4); // 自动重装载模式
    
    /* 3. 设置预分频值:48000000 / 48000 = 1000Hz */
    TIMER0->PSC = 48000 - 1;
    
    /* 4. 设置自动重装载值:1000 / 1000 = 1秒 */
    TIMER0->ARR = 1000 - 1;
    
    /* 5. 使能更新中断 */
    TIMER0->DIER |= (1 << 0);  // UIE位
    
    /* 6. 使能NVIC中断 */
    NVIC_EnableIRQ(TIMER0_IRQn);
    
    /* 7. 启动定时器 */
    TIMER0->CTRL |= (1 << 0);  // CEN位
}

/* 定时器0中断服务函数 */
void TIMER0_IRQHandler(void)
{
    /* 检查更新中断标志 */
    if(TIMER0->SR & (1 << 0))
    {
        /* 清除中断标志 */
        TIMER0->SR = ~(1 << 0);
        
        /* 用户代码:每1秒执行一次 */
        g_timer0_1s_flag = 1;
    }
}

4.3 PWM中断:精准控制与波形生成

PWM(脉冲宽度调制)在电机控制、LED调光、音频输出等场景中非常常见。CMS32的通用定时器通常都支持PWM输出模式,而且可以同时产生中断。

我个人觉得PWM中断最有用的场景是“逐周期控制”——比如电机控制中,每个PWM周期结束时就读取一次电流值,然后调整下一个周期的占空比。这种闭环控制在工业驱动里非常普遍。

4.3.1 PWM中断类型

中断类型 触发时机 典型应用
更新中断(UEV) 计数器溢出/重装载时 周期同步、数据更新
捕获/比较中断(CCx) 计数器值与比较值匹配时 占空比调整、脉宽测量
刹车中断(BRK) 刹车信号输入时 紧急停止、故障保护

4.3.2 代码示例:PWM中断调整占空比

/* PWM初始化:50Hz,初始占空比50% */
void PWM_Init(void)
{
    /* 1. 配置GPIO为PWM输出 */
    GPIO->AFR[0] |= (2 << 4);  // 假设PA2为TIMER0_CH2
    
    /* 2. 配置定时器PWM模式 */
    TIMER0->CTRL = 0x00;
    TIMER0->CTRL |= (1 << 4);  // 自动重装载
    
    /* 3. 设置PWM频率:48MHz / 48000 = 1000Hz */
    TIMER0->PSC = 48 - 1;       // 预分频48
    TIMER0->ARR = 1000 - 1;     // 重装载值1000
    
    /* 4. 配置通道2为PWM模式1 */
    TIMER0->CCMR2 |= (6 << 4); // OC2M = 110 (PWM模式1)
    TIMER0->CCMR2 |= (1 << 3); // OC2PE = 1 (预装载使能)
    
    /* 5. 设置初始占空比50% */
    TIMER0->CCR2 = 500;
    
    /* 6. 使能捕获/比较中断 */
    TIMER0->DIER |= (1 << 4);  // CC2IE
    
    /* 7. 使能输出 */
    TIMER0->CCER |= (1 << 4);  // CC2E
    
    /* 8. 使能NVIC */
    NVIC_EnableIRQ(TIMER0_IRQn);
    
    /* 9. 启动定时器 */
    TIMER0->CTRL |= (1 << 0);
}

/* PWM中断:每个周期调整占空比 */
void TIMER0_IRQHandler(void)
{
    static uint8_t direction = 0;
    
    /* 检查CC2中断标志 */
    if(TIMER0->SR & (1 << 4))
    {
        /* 清除标志 */
        TIMER0->SR = ~(1 << 4);
        
        /* 呼吸灯效果:逐渐改变占空比 */
        if(direction == 0)
        {
            TIMER0->CCR2 += 10;  // 增加占空比
            if(TIMER0->CCR2 >= 990)
                direction = 1;
        }
        else
        {
            TIMER0->CCR2 -= 10;  // 减少占空比
            if(TIMER0->CCR2 <= 10)
                direction = 0;
        }
    }
}

避坑指南:我曾经在PWM中断里直接修改CCR寄存器,结果波形出现了毛刺。后来才发现,必须使能预装载功能(OCxPE位),然后在中断里写CCR,硬件会在下一个周期开始时自动加载新值。这样波形才平滑。

4.4 三种定时器中断对比

咱们把三种定时器中断放在一起对比一下,方便你选型:

特性 SysTick 通用定时器 PWM中断
精度 24位,较高 16位,可调 16位,可调
数量 1个(内核级) 2-4个(外设级) 每个定时器多个通道
主要用途 系统心跳、延时基准 多周期定时、计数 波形生成、闭环控制
中断频率 固定(通常1ms) 可任意配置 与PWM频率相同
资源占用 极少 中等 中等(含GPIO)

选型建议:

  • 只需要一个固定周期中断?用SysTick就够了。
  • 需要多个不同周期的中断?用通用定时器。
  • 需要输出波形并且逐周期控制?用PWM中断。

总结一下:定时器中断是嵌入式系统的“时间管理者”。SysTick负责系统心跳,通用定时器负责多路计时,PWM中断负责波形控制。三者配合使用,基本能覆盖99%的定时需求。我建议你在项目初期就把定时器资源规划好,哪个定时器做什么事,中断优先级怎么分配,这些想清楚了,后面开发会顺畅很多。