3. CubeMX 定时器基础配置:时钟树、参数界面与代码生成
好,咱们进入实战环节。这一章我带你走一遍 CubeMX 的配置流程。很多初学者觉得 CubeMX 是“傻瓜工具”,点几下就完事。其实不然——你如果不懂背后的时钟树逻辑,生成的代码跑起来很可能跟你预期的不一样。我在项目里见过太多次“定时器不准”的故障,最后查下来,都是时钟源配错了。
3.1 时钟树设置——定时器的“心跳”源头
定时器要工作,首先得有时钟。STM32 的时钟树比较复杂,但咱们只需要关注跟定时器相关的几条路径。
打开 CubeMX,切换到 Clock Configuration 标签页。你会看到一张时钟树图。别被它吓到,咱们只抓重点。
核心原则:定时器的时钟源,要么来自 APB1,要么来自 APB2。具体哪个,取决于定时器挂在哪个总线上。
我习惯先确认系统主频(HCLK)。比如我用 STM32F103,外部晶振 8MHz,通过 PLL 倍频到 72MHz。然后看 APB1 和 APB2 的分频系数。
| 总线 | 最大频率 | 挂载的定时器 | 注意点 |
|---|---|---|---|
| APB1 | 36MHz (F1系列) | TIM2~TIM7 | 如果 APB1 预分频系数 ≠1,定时器时钟 = APB1×2 |
| APB2 | 72MHz (F1系列) | TIM1、TIM8 | 如果 APB2 预分频系数 ≠1,定时器时钟 = APB2×2 |
嗯,这里有个坑。很多人以为 APB1 是 36MHz,那 TIM2 的时钟就是 36MHz。不对!当 APB1 的预分频系数大于 1 时(比如 /2),定时器会得到一个倍频后的时钟。说白了,就是为了补偿总线降频带来的损失。
我曾经踩过的坑:有一次配置 TIM3,算出来的溢出时间总是实际值的一半。查了半天,发现 APB1 设成了 /2,但我在代码里直接用了 APB1 的频率去算预分频系数。正确的做法是:定时器时钟 = APB1 频率 × 2(当 APB1 预分频 ≠1 时)。
所以,你在 Clock Configuration 里设置完 HCLK 后,务必看一眼 APB1 和 APB2 的分频系数。我个人的习惯是:把 APB1 和 APB2 都设成跟 HCLK 一样(即不分频),这样定时器时钟就等于 HCLK,省得后面算来算去。当然,如果外设太多需要降频,那就另当别论。
3.2 定时器参数配置界面详解
时钟树搞定后,切换到 Pinout & Configuration 标签页。在左侧找到 Timers,展开,选择你要用的定时器。比如我选 TIM2。
点击 TIM2,右侧会出现配置界面。咱们一项一项看。
3.2.1 定时器基本参数
| 参数项 | 含义 | 我的建议 |
|---|---|---|
| Clock Source | 时钟源选择(内部时钟、外部触发等) | 一般选 Internal Clock |
| Prescaler (PSC) | 预分频系数,16位,范围 0~65535 | 实际分频值 = PSC + 1 |
| Counter Mode | 计数模式(向上、向下、中心对齐) | 最常用 Up |
| Counter Period (ARR) | 自动重装载值,16位或32位 | 决定定时周期 |
| Auto-reload Preload | 自动重装载预装载使能 | 建议 Enable |
这里有个公式,你最好记在心里:
定时器溢出频率 = 定时器时钟 / ((PSC + 1) × (ARR + 1))
定时周期 = 1 / 溢出频率
举个例子。假设定时器时钟是 72MHz,我想要 1ms 的定时中断。那么:
目标频率 = 1 / 0.001 = 1000 Hz
设 PSC = 71(即 72 分频),则计数频率 = 72MHz / 72 = 1MHz
ARR = 1MHz / 1000Hz - 1 = 999
这样,每计数 1000 次(从 0 到 999),定时器溢出一次,周期正好 1ms。
小技巧:我习惯先定 PSC,让计数频率变成一个整数(比如 1MHz),然后再算 ARR。这样调试的时候,用示波器看波形,心里有数。
3.2.2 中断与 DMA 配置
在定时器配置界面的上方,有个 NVIC Settings 标签页。这里要勾选 TIM2 global interrupt,才能启用定时器中断。
另外,DMA Settings 标签页可以配置 DMA 传输。比如用定时器触发 ADC 采样,或者用定时器更新事件触发 DMA 搬运数据。这个后面章节会细讲,这里先知道有这么个入口就行。
3.3 生成初始化代码
参数配完后,点击 Project → Generate Code。CubeMX 会帮你生成 HAL 库的初始化代码。
打开生成的工程,找到 tim.c 文件。你会看到类似这样的代码:
/* TIM2 init function */
void MX_TIM2_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
{
Error_Handler();
}
// ... 时钟源配置和主配置代码
}
嗯,这里要注意。生成的代码只是初始化了定时器,但并没有启动定时器,也没有开启中断。你需要自己在 main.c 里添加:
HAL_TIM_Base_Start_IT(&htim2); // 启动定时器并开启中断
同时,还要实现中断回调函数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
// 你的定时任务代码
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
我曾经犯过的错:刚用 HAL 库时,我以为生成代码后定时器就会自己跑。结果下载程序后,LED 死活不闪。查了半天,发现忘了调用 HAL_TIM_Base_Start_IT()。记住,CubeMX 只生成初始化,启动要自己写。
3.4 本章知识结构图
下面这张图总结了 CubeMX 配置定时器的完整流程,从时钟树到代码生成,每一步的关键节点都在里面了。
你看,从时钟树到最终跑起来,一共 6 步。第 5 步是 CubeMX 帮你做的,第 6 步必须自己动手。很多新手栽在第 6 步上,你想想看,是不是挺冤的?
3.5 本章小结
这一章咱们干了三件事:
- 时钟树设置——搞清楚了定时器时钟从哪来,APB1/APB2 分频系数怎么影响定时器频率
- 参数配置——PSC、ARR、计数模式,每个参数的含义和计算方式
- 代码生成与补充——CubeMX 生成初始化,我们自己加启动和回调
说实话,这些配置看起来琐碎,但只要你亲手配过两三次,形成肌肉记忆,后面就快了。我刚开始学的时候,每配一个定时器都要翻一遍参考手册。现在嘛,闭着眼睛都能写出来。你也一样,多练几次就好。