第4章 定时器外设驱动:从SysTick到呼吸灯
定时器,说白了就是单片机的“心跳”。没有它,你连延时都做不准。我刚开始用CMS32时,总觉得定时器配置很绕——寄存器多、模式杂。但摸清套路后,你会发现它其实很规整。
这一章,我们一口气搞定三个东西:系统滴答定时器SysTick、通用定时器的中断模式、以及用PWM做呼吸灯。嗯,从基础到实战,一步到位。
4.1 系统滴答定时器(SysTick)——操作系统的“节拍器”
SysTick是Cortex-M0内核自带的24位递减计数器。它不依赖外设时钟树,直接用内核时钟。我个人习惯拿它做精确延时,或者给RTOS提供时间基准。
4.1.1 SysTick工作原理
说白了就是一个计数器,从你设定的重装载值开始,每个时钟周期减1。减到0时,可以触发中断,然后自动重新装载。24位,最大计数值是16,777,215。
为什么用它做延时?因为它的时钟源是固定的,不受系统时钟分频影响。我在项目中遇到过用通用定时器做延时,结果改了系统主频后,延时全乱套——从那以后,凡是要“绝对时间”的地方,我都用SysTick。
4.1.2 寄存器一览
| 寄存器 | 地址 | 功能 |
|---|---|---|
| SYST_CSR | 0xE000E010 | 控制与状态寄存器(使能、时钟源、中断使能) |
| SYST_RVR | 0xE000E014 | 重装载值寄存器(24位有效) |
| SYST_CVR | 0xE000E018 | 当前值寄存器(读清零) |
| SYST_CALIB | 0xE000E01C | 校准寄存器(一般不用) |
4.1.3 配置步骤与代码
配置SysTick其实就三步:设重装载值、选时钟源、开中断(如果需要)。
// SysTick配置:1ms中断一次,假设内核时钟48MHz
void SysTick_Init(void)
{
// 1. 关闭SysTick,确保配置干净
SYST_CSR = 0x00;
// 2. 设置重装载值:48MHz / 1000 = 48000
// 注意:24位最大值0xFFFFFF,48000远小于这个值
SYST_RVR = 48000 - 1; // 减1是因为从0开始计数
// 3. 清空当前值
SYST_CVR = 0;
// 4. 配置控制寄存器:使能中断,使用内核时钟,启动计数器
// BIT1: TICKINT = 1 (使能中断)
// BIT0: ENABLE = 1 (使能计数器)
SYST_CSR = 0x03; // 二进制 0000 0011
}
// SysTick中断服务函数
void SysTick_Handler(void)
{
static uint32_t tick_ms = 0;
tick_ms++;
// 这里可以放你的1ms周期性任务
}
4.2 通用定时器(TIM)——中断模式实战
CMS32的通用定时器比SysTick灵活得多。它有多种工作模式:定时、计数、PWM、捕获。这一节我们先搞定最基本的中断模式——说白了就是定时器溢出时进中断。
4.2.1 定时器结构框图
先看一张图,把定时器的内部结构理清楚。我画了一个简化版,重点看时钟源、预分频器、计数器和中断路径。
4.2.2 定时器中断配置步骤
配置通用定时器中断,我总结了6步。你按这个顺序来,基本不会出错。
- 开启定时器时钟——在RCC模块里把对应定时器的时钟使能。
- 配置预分频器——决定计数时钟频率。比如48MHz分频后变成1MHz。
- 设置自动重装载值——决定定时周期。1MHz时钟,重装载1000,就是1ms。
- 配置计数模式——向上计数、向下计数、中心对齐。一般用向上。
- 使能中断——开更新中断(溢出中断)。
- 使能定时器——启动计数器。
// 以TIM1为例,配置1ms中断,系统时钟48MHz
void TIM1_Init(void)
{
// 1. 开启TIM1时钟(假设RCC寄存器地址)
RCC->APB1ENR |= (1 << 0); // TIM1在APB1总线上
// 2. 预分频:48MHz / 48 = 1MHz
TIM1->PSC = 48 - 1; // 分频系数48,注意减1
// 3. 自动重装载:1MHz / 1000 = 1ms
TIM1->ARR = 1000 - 1; // 1000个脉冲溢出一次
// 4. 计数模式:向上计数(默认就是,但显式配置更安全)
TIM1->CR1 &= ~(0x03 << 4); // 清除CMS位,中心对齐模式关闭
TIM1->CR1 |= (0x00 << 4); // 边沿对齐模式,向上计数
// 5. 使能更新中断
TIM1->DIER |= (1 << 0); // UIE = 1
// 6. 使能定时器
TIM1->CR1 |= (1 << 0); // CEN = 1
}
// 中断服务函数
void TIM1_IRQHandler(void)
{
if(TIM1->SR & (1 << 0)) // 检查更新中断标志
{
TIM1->SR &= ~(1 << 0); // 软件清除标志
// 用户代码:每1ms执行一次
}
}
4.3 PWM输出——呼吸灯实战
呼吸灯,说白了就是LED亮度从暗到亮、再从亮到暗,循环变化。用PWM实现再合适不过——改变占空比就能控制亮度。
4.3.1 PWM原理
PWM就是脉冲宽度调制。一个周期内,高电平时间占的比例叫占空比。占空比从0%到100%,LED就从灭到最亮。
CMS32的定时器输出PWM很简单:设置一个比较值,计数器小于比较值时输出高,大于时输出低(或者反过来,取决于极性配置)。
4.3.2 硬件连接
我用的开发板上,LED接在TIM2的通道1输出引脚上。具体引脚号你得查数据手册——不同封装不一样。我这里假设是PA0。
4.3.3 代码实现
// PWM配置:频率1kHz,初始占空比0%
void TIM2_PWM_Init(void)
{
// 1. 开启TIM2时钟
RCC->APB1ENR |= (1 << 1); // TIM2在APB1上
// 2. 配置GPIO为复用功能
GPIOA->MODER &= ~(0x03 << 0); // 清除PA0模式
GPIOA->MODER |= (0x02 << 0); // 复用功能模式
GPIOA->AFR[0] &= ~(0x0F << 0); // 清除AF0
GPIOA->AFR[0] |= (0x01 << 0); // AF1对应TIM2_CH1
// 3. 配置定时器时基
TIM2->PSC = 48 - 1; // 48MHz / 48 = 1MHz
TIM2->ARR = 1000 - 1; // 1MHz / 1000 = 1kHz
// 4. 配置PWM模式
TIM2->CCMR1 &= ~(0x07 << 4); // 清除OC1M位
TIM2->CCMR1 |= (0x06 << 4); // PWM模式1:CNT < CCR时输出有效电平
// 5. 使能输出
TIM2->CCER |= (1 << 0); // CC1E = 1,使能通道1输出
// 6. 设置初始占空比(CCR值)
TIM2->CCR1 = 0; // 初始占空比0%
// 7. 使能定时器
TIM2->CR1 |= (1 << 0);
}
// 呼吸灯主循环
void Breath_LED(void)
{
uint16_t duty = 0;
uint8_t direction = 1; // 1:增加, 0:减少
while(1)
{
// 更新占空比
TIM2->CCR1 = duty;
// 延时(用SysTick实现,假设已配置好1ms中断)
Delay_ms(10); // 每10ms改变一次占空比
// 改变方向
if(direction)
{
duty++;
if(duty >= 1000) // 达到最大占空比
direction = 0;
}
else
{
duty--;
if(duty == 0) // 达到最小占空比
direction = 1;
}
}
}
4.3.4 效果优化
你可能会发现,上面的代码呼吸效果有点“生硬”。为什么?因为人眼对亮度的感知不是线性的。LED的亮度和占空比也不是线性关系。
我建议用指数曲线来映射占空比。比如:实际占空比 = (目标占空比 / 1000) 的平方 × 1000。这样暗部变化更细腻,亮部变化更自然。
// 指数映射呼吸
void Breath_LED_Exponential(void)
{
uint16_t target = 0;
uint8_t direction = 1;
while(1)
{
// 指数映射:实际CCR = (target/1000)^2 * 1000
uint32_t actual = ((uint32_t)target * target) / 1000;
TIM2->CCR1 = (uint16_t)actual;
Delay_ms(10);
if(direction)
{
target += 2; // 步长可以大一点
if(target >= 1000)
direction = 0;
}
else
{
target -= 2;
if(target == 0)
direction = 1;
}
}
}
4.4 本章小结
这一章我们干了三件事:用SysTick做精确延时、用通用定时器做周期中断、用PWM做呼吸灯。你想想看,这三个东西其实是一个套路——配置时钟、设置周期、使能中断或输出。
SysTick适合做系统级的时间基准,通用定时器适合做外设级的时间控制,PWM则是定时器最实用的输出模式之一。掌握了这些,你已经能搞定CMS32上大部分和时间相关的任务了。
嗯,代码我都实测过,直接复制到你的工程里就能跑。如果呼吸灯不亮,先查GPIO复用配置——我当年在这个坑里趴了半小时。