4、定时器与系统时钟:配置系统时钟树,使用定时器产生精确的时间基准,用于数据采样周期控制
各位同学,咱们今天聊点硬核的——时钟树和定时器。说实话,我刚开始做嵌入式那会儿,觉得时钟配置就是抄几个寄存器值,能跑就行。直到有一次做低功耗项目,电池续航死活差30%,最后发现是时钟源选错了,分频系数也不对。嗯,从那以后,我每次画板子第一件事就是先把时钟树理清楚。
4.1 系统时钟树——MCU的“心跳”
时钟树是什么?说白了就是给MCU各个模块分配时钟信号的网络。就像人的心脏泵血到各个器官,时钟源产生的脉冲分配到CPU、外设、内存等模块。每个模块对时钟频率的要求不一样,所以需要一棵树来分级管理。
我个人习惯把时钟树分成三层来看:
- 根节点:时钟源,比如外部晶振(HSE)、内部RC振荡器(HSI)、低速晶振(LSE)等
- 中间节点:PLL锁相环,用来倍频或分频
- 叶子节点:各个外设的时钟使能开关和分频器
你想想看,如果CPU跑在72MHz,但定时器需要1MHz的计数频率,中间就得经过分频。这就是时钟树存在的意义。
核心原则:系统时钟(SYSCLK)决定了CPU的运算速度,而外设时钟(APB1/APB2)决定了外设的工作频率。两者可以不同,但必须保证外设时钟不超过其最大允许频率。
4.2 配置时钟树——以STM32F103为例
我记得第一次用STM32时,被那个复杂的时钟树绕晕了。后来总结了一个套路:先选源,再倍频,最后分频。咱们一步步来。
下面是一个典型的时钟树配置流程:
// 1. 使能外部高速晶振(HSE)
RCC_HSEConfig(RCC_HSE_ON);
while(RCC_WaitForHSEStartUp() != SUCCESS);
// 2. 配置PLL,将8MHz晶振倍频到72MHz
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
RCC_PLLCmd(ENABLE);
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
// 3. 切换系统时钟到PLL输出
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
while(RCC_GetSYSCLKSource() != 0x08);
// 4. 配置APB1和APB2分频
RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB时钟 = SYSCLK
RCC_PCLK2Config(RCC_HCLK_Div1); // APB2时钟 = 72MHz
RCC_PCLK1Config(RCC_HCLK_Div2); // APB1时钟 = 36MHz(最大允许36MHz)
小技巧:配置完时钟后,我习惯用MCO引脚(时钟输出)接示波器看一眼实际频率。曾经有一次代码里写的是9倍频,结果寄存器配置错了变成8倍频,要不是示波器发现频率不对,整个项目就翻车了。
4.3 定时器——精确的时间基准
定时器这东西,说白了就是一个计数器,在时钟脉冲驱动下不断累加。当计数值达到预设的阈值时,产生中断或触发事件。咱们做能耗监测,采样周期控制全靠它。
我常用的定时器配置思路是这样的:
- 确定目标频率:比如采样周期是1kHz,那定时器就要每1ms触发一次
- 计算分频系数:根据定时器时钟频率,算出预分频器(PSC)的值
- 设置自动重装载值:ARR = 目标频率 / (定时器时钟 / (PSC+1)) - 1
举个例子,定时器时钟是72MHz,想要1ms中断一次:
// 定时器2配置,1ms中断一次
TIM_TimeBaseInitTypeDef TIM_InitStruct;
// 预分频:72MHz / (72-1+1) = 1MHz,即每1us计数一次
TIM_InitStruct.TIM_Prescaler = 72 - 1;
// 自动重装载值:1MHz / 1000 = 1000,即1000次计数产生一次中断
TIM_InitStruct.TIM_Period = 1000 - 1;
// 其他配置
TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_InitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &TIM_InitStruct);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);
注意:预分频器和自动重装载值的范围受定时器位宽限制。16位定时器的PSC和ARR最大只能到65535。如果你需要很长的定时周期,可以考虑用32位定时器,或者级联两个定时器。我曾经在项目中需要10秒的采样周期,16位定时器不够用,最后用定时器级联解决的。
4.4 数据采样周期的精确控制
能耗监测系统里,采样周期直接决定了数据的准确性和系统的功耗。周期太短,数据精确但功耗高;周期太长,功耗低但可能漏掉瞬态事件。
我个人建议的做法是:
- 确定采样频率:根据被监测信号的最高频率,按照奈奎斯特定理,采样频率至少是信号最高频率的2倍。实际工程中我一般取5-10倍。
- 使用定时器中断触发采样:在定时器中断服务函数里启动ADC转换或读取传感器数据。
- 考虑中断延迟:如果中断服务函数执行时间过长,会影响下一个周期的定时精度。这时候可以用DMA来搬运数据,减少CPU干预。
下面是一个典型的采样控制代码框架:
// 定时器中断服务函数
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 启动ADC采样
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
// 或者读取外部传感器
// sensor_data = read_sensor();
// 更新采样计数
sample_count++;
}
}
// 主循环中处理采样数据
int main(void)
{
// 初始化...
Timer_Init(); // 配置定时器
ADC_Init(); // 配置ADC
while(1)
{
if(sample_count > 0)
{
// 处理采样数据
process_sample_data();
sample_count--;
}
// 其他低优先级任务
handle_communication();
update_display();
}
}
避坑指南:我曾经在定时器中断里做太多事情,导致中断还没退出下一个中断就来了,造成数据丢失。后来我把中断服务函数精简到只做两件事:清标志位、启动采样。数据处理全部放到主循环里做。这样既保证了采样周期的精确性,又避免了中断嵌套的问题。
4.5 低功耗场景下的时钟与定时器策略
做能耗监测系统,低功耗是绕不开的话题。我常用的策略是:
- 休眠时切换到低速时钟:系统空闲时,从HSE切换到LSI(低速内部振荡器,约40kHz),定时器也切换到低速时钟源
- 使用RTC唤醒:RTC(实时时钟)可以在休眠模式下运行,功耗极低,定时唤醒MCU进行采样
- 按需开启外设时钟:采样前开启ADC时钟,采样完立即关闭
举个例子,用RTC实现每分钟采样一次:
// 配置RTC,1分钟唤醒一次
RTC_SetPrescaler(32768); // 32.768kHz晶振,分频后1Hz
RTC_SetAlarm(RTC_GetCounter() + 60); // 60秒后触发闹钟
// 进入休眠模式
PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);
// 闹钟唤醒后,重新配置系统时钟
SystemClock_Config(); // 恢复到高速时钟
ADC_Enable(); // 开启ADC
Start_Sampling(); // 执行采样
ADC_Disable(); // 采样完关闭ADC
// 再次进入休眠...
经验之谈:低功耗模式下,时钟切换需要时间。HSE起振通常需要几毫秒,PLL锁定也需要时间。所以采样间隔太短(比如小于10ms)就不适合用休眠唤醒的方式,老老实实用定时器中断吧。
4.6 总结与思考
时钟树和定时器,是嵌入式系统的基石。配置对了,系统跑得稳;配置错了,各种奇怪的问题就来了——数据采样不准、通信超时、功耗异常……
我建议你在实际项目中,先花半小时把时钟树图画清楚,再动手写代码。别像我当年那样,上来就抄例程,结果出了问题都不知道从哪查起。
下一章咱们聊聊ADC采样和数据处理,到时候会用到今天配置的定时器来做采样触发。嗯,先把基础打牢,后面就顺了。
公众号:蓝海资料掘金营,微信deep3321