1. STM32定时器基础认知
第一次接触STM32的定时器时,我盯着数据手册里那些密密麻麻的寄存器描述发懵。直到把TIM3想象成厨房里的智能定时器才豁然开朗——它能精确计量时间,到点就提醒我们该做什么。STM32F103C8T6的通用定时器就像这个智能设备,只不过它的"提醒"方式是通过中断信号。
这块蓝色小开发板搭载了4个通用定时器(TIM2-TIM5),每个都像瑞士军刀般多功能。以我们要用的TIM3为例,它本质上是个16位向上/向下计数器,配合预分频器和自动重装载寄存器,能实现从微秒到小时的精确计时。实际项目中,我常用它来做:
- 周期性任务调度(比如每100ms采集一次传感器数据)
- PWM波形生成(控制电机转速或LED亮度)
- 输入捕获(测量脉冲宽度或频率)
最让人头疼的是时钟树配置。刚开始我总搞不清APB1总线的72MHz时钟怎么变成定时器的计数频率。后来发现关键在CK_CNT这个隐藏参数——当APB1预分频系数≠1时,定时器时钟会2倍频。所以对于默认配置的72MHz系统时钟,TIM3实际获得的时钟频率是72MHz,这个细节直接影响后续所有时间计算。
2. 定时器初始化五步法
2.1 时钟使能:给定时器通电
就像开电器要先插电源,使用定时器第一步是开启对应的时钟。在标准库中,这个操作只需一行代码,但藏着几个易错点:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);有次调试时死活进不了中断,最后发现是手误写成APB2时钟使能。记住:TIM2-TIM5挂在APB1总线,而高级定时器TIM1和TIM8才用APB2。建议在代码里用宏定义包裹这个操作,避免重复犯错:
#define TIM3_CLK_ENABLE() RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE)2.2 时基结构体:定时器的心跳设置
配置TIM_TimeBaseInitTypeDef结构体就像给手表校时,需要协调好几个关键参数:
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 999; // 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler = 7199; // 预分频系数 TIM_TimeBaseStructure.TIM_ClockDivision = 0; // 时钟分频(滤波用) TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);这里有个实用计算公式:
中断时间 = (TIM_Prescaler + 1) * (TIM_Period + 1) / TIMx_CLK以72MHz时钟为例,上述配置产生的中断间隔是:(7199+1)*(999+1)/72,000,000 = 0.1秒。建议用Excel做个计算工具,避免每次手动计算。
2.3 中断使能:打开提醒功能
光有时基配置还不够,就像设好闹钟后得打开开关:
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);特别注意第三个参数可以是:
- ENABLE:开启更新中断
- DISABLE:关闭中断 新手常犯的错误是忘记检查中断标志位,导致连续进入中断。可以在服务函数里加个标志位检测:
if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { // 中断处理代码 TIM_ClearITPendingBit(TIM3, TIM_IT_Update); }3. 中断控制器深度配置
3.1 NVIC的优先级迷宫
NVIC配置就像给不同中断源分配VIP通道,我习惯用这个模板:
NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);优先级数值越小等级越高,但要注意:
- 抢占优先级高的可以打断正在执行的优先级低的中断
- 响应优先级用于多个中断同时到来时的处理顺序
- STM32F103只有4位优先级分组,具体通过NVIC_PriorityGroupConfig()设置
3.2 中断服务函数编写规范
好的中断服务函数应该像急诊医生——快速诊断,简单处理,复杂任务交给主循环。以LED翻转为例:
void TIM3_IRQHandler(void) { static uint32_t tick = 0; if(TIM_GetITStatus(TIM3, TIM_IT_Update)) { tick++; if(tick >= 5) { // 0.5秒间隔 GPIO_WriteBit(GPIOC, GPIO_Pin_13, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13))); tick = 0; } TIM_ClearITPendingBit(TIM3, TIM_IT_Update); } }这里用了静态变量tick实现分频,避免在中断里做延时。实测发现,如果中断服务超过10us,就可能影响其他实时任务。
4. 实战:精准控制LED闪烁
4.1 硬件连接检查清单
使用PC13驱动LED时,有3个硬件细节要注意:
- 开发板上的用户LED通常已接限流电阻
- STM32的GPIO输出速度要匹配实际需求:
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; // 对LED足够 - 记得先使能GPIOC时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
4.2 完整代码框架
把前面的模块组合起来,完整的工程应该包含:
// 时钟配置(system_stm32f10x.c中) void SystemInit(void) { RCC->CR |= 0x00010000; // HSEON while(!(RCC->CR & 0x00020000)); // HSERDY RCC->CFGR = 0x001D0400; // PLL 9倍频 // ...其他配置 } // 主函数 int main(void) { TIM3_Config(); LED_GPIO_Config(); while(1) { // 主循环可添加其他任务 } } // 定时器3配置 void TIM3_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; TIM3_CLK_ENABLE(); // 时基设置(100ms中断) TIM_TimeBaseStructure.TIM_Period = 999; TIM_TimeBaseStructure.TIM_Prescaler = 7199; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 中断配置 TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE); // 启动定时器 }4.3 调试技巧与常见问题
用逻辑分析仪抓取的波形显示,实际中断间隔有时会有±1us的抖动。通过以下方法优化精度:
- 在中断开始时关闭全局中断,处理完再打开
- 使用TIM_GenerateEvent()手动触发更新事件来同步计数器
- 对于更精确的需求,可以考虑使用TIM1的重复计数器功能
遇到中断不触发时,建议按这个顺序排查:
- 确认RCC时钟配置正确(可用RCC_GetClocksFreq()检查)
- 检查NVIC优先级分组是否冲突
- 在调试模式下查看TIMx_SR寄存器的UIF位是否置1
- 确认没有在其他地方清除中断标志