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_CSR | 0xE000E010 | 控制与状态寄存器(使能、中断、时钟源选择) |
| SYST_RVR | 0xE000E014 | 重装载值寄存器(24位有效) |
| SYST_CVR | 0xE000E018 | 当前值寄存器(读它得到当前计数值) |
| SYST_CALIB | 0xE000E01C | 校准寄存器(一般用不到) |
我的经验:配置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的内部结构给你拆开看:
4.2.2 TIM工作模式
通用定时器有几种工作模式,我整理了个表格:
| 模式 | 描述 | 典型应用 |
|---|---|---|
| 向上计数模式 | 计数器从0递增到ARR,然后清零 | 普通定时、PWM |
| 向下计数模式 | 计数器从ARR递减到0,然后重装 | 需要精确计数的场景 |
| 中央对齐模式 | 先递增到ARR,再递减到0,循环往复 | 对称PWM |
| 单脉冲模式 | 只计数一个周期就停止 | 单次延时触发 |
我的建议:做PWM时优先用向上计数模式,逻辑最清晰。中央对齐模式虽然能产生对称波形,但配置起来容易出错,我一般只在电机控制里用。
4.2.3 TIM配置步骤
配置TIM其实就5步,我按顺序列出来:
- 使能TIM时钟:在RCC寄存器里打开对应TIM的时钟门控
- 配置预分频器PSC:决定计数器的时钟频率
- 配置自动重装载ARR:决定定时周期
- 配置输出比较CCR:决定PWM占空比(如果做PWM)
- 使能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 实现思路
说白了就三步:
- 用TIM产生固定频率的PWM(建议1kHz以上,避免闪烁)
- 在主循环里定时修改CCR值
- 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调光、音频输出都不在话下。