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 配置定时器的完整流程,从时钟树到代码生成,每一步的关键节点都在里面了。

CubeMX 定时器配置流程 1. 时钟树配置 2. 选择定时器实例 3. 配置基本参数 4. 配置中断 (NVIC) 5. 生成初始化代码 6. 手动启动定时器 关键检查点 APB1/APB2 分频系数 定时器时钟 = 总线时钟 × 2? PSC 和 ARR 计算是否正确 常见遗漏 忘记调用 Start_IT() 忘记实现回调函数 中断优先级未配置

你看,从时钟树到最终跑起来,一共 6 步。第 5 步是 CubeMX 帮你做的,第 6 步必须自己动手。很多新手栽在第 6 步上,你想想看,是不是挺冤的?

3.5 本章小结

这一章咱们干了三件事:

  • 时钟树设置——搞清楚了定时器时钟从哪来,APB1/APB2 分频系数怎么影响定时器频率
  • 参数配置——PSC、ARR、计数模式,每个参数的含义和计算方式
  • 代码生成与补充——CubeMX 生成初始化,我们自己加启动和回调

说实话,这些配置看起来琐碎,但只要你亲手配过两三次,形成肌肉记忆,后面就快了。我刚开始学的时候,每配一个定时器都要翻一遍参考手册。现在嘛,闭着眼睛都能写出来。你也一样,多练几次就好。