4. GPIO外设LL库驱动
定时器这东西,说白了就是单片机的“心跳节拍器”。我刚开始用STM32的时候,总觉得定时器不就是个计数器嘛,有啥好研究的?结果有一次做电机控制,因为定时器配置不对,电机转起来一顿一顿的,查了整整两天才发现是预分频系数算错了。从那以后,我对定时器就再也不敢马虎了。
这一章,咱们就把STM32的定时器资源捋清楚,然后手把手用LL库配置一个定时中断,实现精准的微秒级和毫秒级延时。
4.1 STM32定时器家族
STM32的定时器种类不少,但别被吓到。我习惯把它们分成三类:
| 类型 | 定时器实例 | 位数 | 主要特点 |
|---|---|---|---|
| 基本定时器 | TIM6、TIM7 | 16位 | 仅定时功能,无外部引脚 |
| 通用定时器 | TIM2~TIM5 | 16/32位 | PWM、输入捕获、输出比较 |
| 高级定时器 | TIM1、TIM8 | 16位 | 带死区插入的互补PWM、刹车功能 |
做精准延时,我一般首选基本定时器或通用定时器。为什么?因为高级定时器功能太复杂,配置起来容易踩坑。你想想看,如果只是要个延时,何必把刹车、死区这些功能都牵扯进来?
4.2 定时器的工作原理
定时器的核心逻辑其实就一句话:时钟源经过预分频器分频后,驱动计数器递增,计数器达到自动重装载值时产生更新事件。
嗯,这句话有点绕。我画个图你就明白了。
看到这个流程,你应该能理解了吧?时钟源进来,先经过预分频器降频,然后计数器开始数数。数到ARR设定的值,就触发更新事件,计数器归零重新开始。这个更新事件,就是我们产生定时中断的源头。
4.3 LL库定时器初始化
LL库的定时器初始化,核心就两个函数:LL_TIM_Init 和 LL_TIM_EnableIT_UPDATE。我习惯把配置步骤总结成四步走:
- 使能定时器时钟 —— 没时钟,啥都干不了
- 配置预分频和重装载值 —— 决定定时周期
- 使能更新中断 —— 让定时器能通知CPU
- 使能定时器 —— 开始计数
来看一个具体的例子。假设系统时钟72MHz,我想实现1ms中断一次:
// 第一步:使能定时器时钟
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM6);
// 第二步:初始化定时器
LL_TIM_InitTypeDef TIM_InitStruct = {0};
TIM_InitStruct.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz
TIM_InitStruct.Autoreload = 1000 - 1; // 1MHz / 1000 = 1kHz
TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
LL_TIM_Init(TIM6, &TIM_InitStruct);
// 第三步:使能更新中断
LL_TIM_EnableIT_UPDATE(TIM6);
// 第四步:使能定时器
LL_TIM_EnableCounter(TIM6);
中断频率 = 时钟频率 / (Prescaler + 1) / (Autoreload + 1)
中断周期 = 1 / 中断频率
这里有个细节要注意:Prescaler和Autoreload都要减1。为什么?因为这两个寄存器的值是从0开始计数的。比如Prescaler设为71,实际分频系数是72。我曾经见过有人直接写72,结果定时时间长了1/72,虽然误差不大,但在高精度场合就出问题了。
4.4 定时中断服务函数
定时器配置好了,中断来了,总得有人处理吧?中断服务函数就是干这个的。
// 中断服务函数
void TIM6_DAC_IRQHandler(void)
{
// 检查更新中断标志
if(LL_TIM_IsActiveFlag_UPDATE(TIM6))
{
// 清除更新中断标志
LL_TIM_ClearFlag_UPDATE(TIM6);
// 用户代码:每1ms执行一次
// 比如:翻转LED、更新系统滴答等
}
}
嗯,这里有个坑。中断服务函数里,一定要先清除标志位,再执行用户代码。我刚开始做的时候顺序搞反了,结果中断标志位一直没清,程序就卡在中断里出不来。你想想看,CPU一直在响应中断,主循环根本跑不动,整个系统就像死机了一样。
4.5 实现精准延时
有了1ms的定时中断,我们就可以在上面搭一个精准延时函数了。我常用的方法是维护一个全局的毫秒计数器:
volatile uint32_t g_tick_ms = 0;
// 中断服务函数中递增
void TIM6_DAC_IRQHandler(void)
{
if(LL_TIM_IsActiveFlag_UPDATE(TIM6))
{
LL_TIM_ClearFlag_UPDATE(TIM6);
g_tick_ms++;
}
}
// 毫秒延时
void delay_ms(uint32_t ms)
{
uint32_t start = g_tick_ms;
while((g_tick_ms - start) < ms);
}
// 微秒延时(忙等待方式)
void delay_us(uint32_t us)
{
// 先停掉定时器,重新配置为微秒级
LL_TIM_DisableCounter(TIM6);
LL_TIM_SetPrescaler(TIM6, 72 - 1); // 1MHz
LL_TIM_SetAutoreload(TIM6, us - 1);
LL_TIM_SetCounter(TIM6, 0);
LL_TIM_EnableCounter(TIM6);
while(!LL_TIM_IsActiveFlag_UPDATE(TIM6));
LL_TIM_ClearFlag_UPDATE(TIM6);
// 恢复为毫秒配置
LL_TIM_SetAutoreload(TIM6, 1000 - 1);
}
这个微秒延时的写法,说白了就是临时改变定时器的重装载值,让它只数us个数就产生一次更新事件。虽然简单粗暴,但在很多场合够用了。
4.6 常见问题与调试技巧
做定时器调试,我总结了几条经验:
- 定时器不工作?先检查时钟是否使能。用调试器看RCC寄存器,确认定时器时钟已经打开。
- 中断不触发?检查NVIC是否使能。LL库只管外设,NVIC配置要自己加:
NVIC_EnableIRQ(TIM6_DAC_IRQn)。 - 时间不准?确认系统时钟频率。很多人用HSE 8MHz外部晶振,但忘了配置PLL,结果系统跑在8MHz而不是72MHz。
- 中断太频繁?检查Prescaler和Autoreload是否算对了。我见过有人把两个值写反了,结果中断频率高得吓人。
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| 定时器不计数 | 时钟未使能 | 检查RCC寄存器 |
| 中断不响应 | NVIC未配置 | 检查NVIC使能位 |
| 时间偏慢/偏快 | 系统时钟配置错误 | 用示波器测PWM输出 |
| 程序卡死 | 中断标志未清除 | 检查清标志顺序 |
最后说一句,调试定时器最好的工具是什么?示波器。把某个GPIO在中断里翻转,用示波器一看,周期对不对一目了然。比你在代码里加断点调试高效多了。