4. CMS32功能安全特性

定时器这东西,说白了就是单片机的“心跳”。没有它,你连个精准的延时都做不出来。CMS32系列里定时器资源挺丰富的,咱们今天就把SysTick、通用定时器TIM和PWM一口气讲透。

本章核心脉络:从系统节拍(SysTick)到通用定时器(TIM),再到PWM输出,最后用呼吸灯把整个知识串起来。每一步都有我踩过的坑。

4.1 系统滴答定时器 SysTick

SysTick是ARM Cortex-M内核自带的24位递减计数器。它不占用外设定时器资源,专门给操作系统提供心跳,或者做精确延时。我个人习惯把它当作“时间基准”,所有需要毫秒级定时的任务都靠它。

4.1.1 SysTick工作原理

SysTick的核心逻辑很简单:从你设定的重装载值开始,每个系统时钟周期减1,减到0时触发中断,然后自动重装。24位计数器,最大计数值是2^24 - 1 = 16,777,215。

举个例子,CMS32主频48MHz,你要产生1ms中断:

重装载值 = 48,000,000 / 1000 = 48,000

嗯,这里要注意:48,000远小于16,777,215,所以完全没问题。

4.1.2 SysTick寄存器配置

SysTick只有4个寄存器,我列个表,你一看就明白:

寄存器地址功能
SYST_CSR0xE000E010控制与状态寄存器(使能、中断、时钟源选择)
SYST_RVR0xE000E014重装载值寄存器(24位有效)
SYST_CVR0xE000E018当前值寄存器(读它得到当前计数值)
SYST_CALIB0xE000E01C校准寄存器(一般用不到)

我的经验:配置SysTick时,记得先清零SYST_CVR再使能,否则第一次中断时间可能不准。我曾经因为这个bug排查了半天,最后发现是初始化顺序的问题。

4.1.3 SysTick代码实现

// 配置SysTick产生1ms中断
void SysTick_Init(void)
{
    // 1. 关闭SysTick,防止配置过程中产生意外中断
    SYST_CSR = 0x00;
    
    // 2. 设置重装载值(48MHz主频,1ms)
    SYST_RVR = 48000 - 1;  // 注意:实际计数值是RVR+1
    
    // 3. 清零当前值寄存器
    SYST_CVR = 0x00;
    
    // 4. 配置控制寄存器:使能中断、使用处理器时钟、启动定时器
    //    Bit 1: TICKINT = 1 使能中断
    //    Bit 2: CLKSOURCE = 1 使用处理器时钟
    //    Bit 0: ENABLE = 1 启动
    SYST_CSR = 0x07;
}

// SysTick中断服务函数
void SysTick_Handler(void)
{
    static uint32_t tick_ms = 0;
    tick_ms++;
    
    // 每1000ms翻转一次LED
    if(tick_ms >= 1000)
    {
        tick_ms = 0;
        LED_TOGGLE();
    }
}

避坑指南:我曾经在中断服务函数里做复杂运算,结果导致SysTick中断嵌套,系统直接跑飞。记住:中断服务函数要短小精悍,只做标志位设置,具体处理放到主循环里。

4.2 通用定时器 TIM

SysTick虽然好用,但只有一个。你要做多路PWM、输入捕获、输出比较,就得靠通用定时器。CMS32有多个TIM,每个都是16位的,功能比SysTick强大得多。

4.2.1 TIM结构框图

我画了个SVG图,把TIM的内部结构给你拆开看:

CMS32通用定时器TIM内部结构 时钟源 预分频器PSC 16位计数器CNT 自动重装载ARR 输出比较通道1 输出比较通道2 输出比较通道3 ... 中断/事件输出 关键说明: 1. 时钟源经过预分频器分频后驱动16位计数器 2. 计数器从0递增到ARR值,然后清零重新开始 3. 输出比较通道将计数器值与CCR比较,产生PWM或中断 4. 多个通道可以同时工作,互不干扰

4.2.2 TIM工作模式

通用定时器有几种工作模式,我整理了个表格:

模式描述典型应用
向上计数模式计数器从0递增到ARR,然后清零普通定时、PWM
向下计数模式计数器从ARR递减到0,然后重装需要精确计数的场景
中央对齐模式先递增到ARR,再递减到0,循环往复对称PWM
单脉冲模式只计数一个周期就停止单次延时触发

我的建议:做PWM时优先用向上计数模式,逻辑最清晰。中央对齐模式虽然能产生对称波形,但配置起来容易出错,我一般只在电机控制里用。

4.2.3 TIM配置步骤

配置TIM其实就5步,我按顺序列出来:

  1. 使能TIM时钟:在RCC寄存器里打开对应TIM的时钟门控
  2. 配置预分频器PSC:决定计数器的时钟频率
  3. 配置自动重装载ARR:决定定时周期
  4. 配置输出比较CCR:决定PWM占空比(如果做PWM)
  5. 使能TIM:启动计数器开始工作
// TIM2配置示例:产生1kHz的PWM,占空比50%
void TIM2_PWM_Init(void)
{
    // 1. 使能TIM2时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    
    // 2. 配置预分频器:48MHz / 48 = 1MHz
    TIM2->PSC = 48 - 1;
    
    // 3. 配置自动重装载:1MHz / 1000 = 1kHz
    TIM2->ARR = 1000 - 1;
    
    // 4. 配置输出比较通道1:占空比50%
    TIM2->CCR1 = 500;
    
    // 5. 配置输出比较模式:PWM模式1
    TIM2->CCMR1 |= (0x06 << 4);  // OC1M = 110 (PWM模式1)
    TIM2->CCER |= TIM_CCER_CC1E;  // 使能通道1输出
    
    // 6. 使能TIM2
    TIM2->CR1 |= TIM_CR1_CEN;
}

避坑指南:我曾经在配置PSC和ARR时忘了减1,结果定时周期完全不对。记住:PSC和ARR的实际计数值都是寄存器值+1。比如你要分频48倍,PSC要写47。

4.3 PWM输出原理

PWM说白了就是“假装模拟”。单片机输出不了真正的模拟电压,但通过快速切换高低电平,让负载“感觉”到平均电压在变化。你想想看,LED灯快速闪烁时,人眼看到的是亮度变化,这就是PWM的精髓。

4.3.1 PWM关键参数

参数说明计算公式
频率PWM波形的周期F = 时钟频率 / (PSC+1) / (ARR+1)
占空比高电平时间占整个周期的比例D = CCR / (ARR+1) × 100%
分辨率占空比可调节的精细程度R = 1 / (ARR+1) × 100%

举个例子:ARR=999,CCR=500,占空比就是50%。你把CCR从0调到999,占空比就从0%到100%线性变化。

4.3.2 PWM模式选择

CMS32的TIM支持两种PWM模式:

  • PWM模式1:计数器值小于CCR时输出有效电平,大于CCR时输出无效电平
  • PWM模式2:正好反过来,计数器值小于CCR时输出无效电平

我个人习惯用PWM模式1,配合高电平有效,逻辑最直观:CCR越大,占空比越大。

4.4 呼吸灯实现

呼吸灯就是把PWM占空比从0%逐渐增加到100%,再从100%逐渐减小到0%,循环往复。人眼看起来就像灯在“呼吸”。

4.4.1 实现思路

说白了就三步:

  1. 用TIM产生固定频率的PWM(建议1kHz以上,避免闪烁)
  2. 在主循环里定时修改CCR值
  3. CCR从0递增到ARR,再从ARR递减到0

关键点:呼吸效果要平滑,CCR的变化速度要适中。太快了像闪烁,太慢了像没反应。我一般每10ms改变一次CCR,每次步进1或2。

4.4.2 完整代码实现

// 呼吸灯主程序
#include "CMS32.h"

#define LED_PIN      GPIO_PIN_0
#define LED_PORT     GPIOA
#define BREATH_STEP  2      // 每次步进值
#define BREATH_DELAY 10     // 每步延时(ms)

uint8_t breath_dir = 1;     // 方向:1递增,0递减
uint16_t breath_val = 0;    // 当前PWM值

void GPIO_Init(void)
{
    RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
    GPIOA->MODER &= ~(0x03 << (0 * 2));
    GPIOA->MODER |= (0x02 << (0 * 2));  // 复用功能
    GPIOA->AFR[0] |= (0x01 << (0 * 4)); // AF1 = TIM2_CH1
}

void TIM2_PWM_Init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    TIM2->PSC = 48 - 1;          // 1MHz
    TIM2->ARR = 1000 - 1;        // 1kHz
    TIM2->CCR1 = 0;              // 初始占空比0%
    TIM2->CCMR1 |= (0x06 << 4); // PWM模式1
    TIM2->CCER |= TIM_CCER_CC1E; // 使能输出
    TIM2->CR1 |= TIM_CR1_CEN;    // 启动
}

void SysTick_Init(void)
{
    SYST_RVR = 48000 - 1;  // 1ms
    SYST_CVR = 0;
    SYST_CSR = 0x07;       // 使能+中断+处理器时钟
}

volatile uint32_t sys_tick = 0;

void SysTick_Handler(void)
{
    sys_tick++;
}

void delay_ms(uint32_t ms)
{
    uint32_t start = sys_tick;
    while((sys_tick - start) < ms);
}

int main(void)
{
    GPIO_Init();
    SysTick_Init();
    TIM2_PWM_Init();
    
    while(1)
    {
        // 每10ms更新一次PWM值
        delay_ms(BREATH_DELAY);
        
        // 更新PWM值
        if(breath_dir)
        {
            breath_val += BREATH_STEP;
            if(breath_val >= 1000)
            {
                breath_val = 1000;
                breath_dir = 0;
            }
        }
        else
        {
            if(breath_val >= BREATH_STEP)
                breath_val -= BREATH_STEP;
            else
                breath_val = 0;
            
            if(breath_val == 0)
                breath_dir = 1;
        }
        
        // 更新CCR寄存器
        TIM2->CCR1 = breath_val;
    }
}

我的经验:呼吸灯调试时,先用示波器看PWM波形是否正常。我遇到过GPIO复用功能没配置对,PWM死活出不来,最后发现是AFR寄存器写错了位。

4.4.3 效果优化

基本的呼吸灯实现了,但你可能觉得效果不够“丝滑”。这里有几个优化方向:

  • 非线性呼吸:人眼对亮度的感知不是线性的,用指数曲线调整CCR变化速度,效果更自然
  • 多路呼吸:用多个TIM通道控制不同颜色的LED,实现呼吸渐变效果
  • 频率调整:PWM频率低于500Hz时会有闪烁感,建议保持在1kHz以上

避坑指南:我曾经在呼吸灯里用了delay_ms(1000)做延时,结果LED像在“抽搐”而不是“呼吸”。后来改成10ms步进,效果才正常。记住:呼吸灯讲究的是平滑过渡,不是快速跳变。

好了,定时器和PWM的内容就到这里。SysTick是系统的心跳,TIM是外设的利器,PWM是模拟输出的桥梁。呼吸灯这个例子虽然简单,但把定时器、PWM、中断、GPIO都串起来了,你吃透它,后面做电机控制、LED调光、音频输出都不在话下。