1. 引言
在 ARM 嵌入式开发中,定时器(Timer)与时钟(Clock)是绕不开的基础模块。无论是实现延时、产生 PWM 波形、捕获输入信号,还是驱动操作系统的心跳节拍,背后都离不开定时器与时钟系统的支撑。本文结合定时器与时钟的学习过程,梳理 ARM 嵌入式开发中的关键知识点与实践心得。
2. 时钟系统基础
2.1 为什么需要时钟
时钟是嵌入式系统的"心跳"。CPU、外设、总线都需要时钟信号来同步工作。时钟频率的高低直接影响系统的运行速度与功耗。
2.2 时钟树结构
ARM 芯片内部通常包含一个复杂的时钟树,常见结构如下:
#mermaid-svg-J3KcPKhnFytoguPQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-J3KcPKhnFytoguPQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-J3KcPKhnFytoguPQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-J3KcPKhnFytoguPQ .error-icon{fill:#552222;}#mermaid-svg-J3KcPKhnFytoguPQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-J3KcPKhnFytoguPQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-J3KcPKhnFytoguPQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-J3KcPKhnFytoguPQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-J3KcPKhnFytoguPQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-J3KcPKhnFytoguPQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-J3KcPKhnFytoguPQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-J3KcPKhnFytoguPQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-J3KcPKhnFytoguPQ .marker.cross{stroke:#333333;}#mermaid-svg-J3KcPKhnFytoguPQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-J3KcPKhnFytoguPQ p{margin:0;}#mermaid-svg-J3KcPKhnFytoguPQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-J3KcPKhnFytoguPQ .cluster-label text{fill:#333;}#mermaid-svg-J3KcPKhnFytoguPQ .cluster-label span{color:#333;}#mermaid-svg-J3KcPKhnFytoguPQ .cluster-label span p{background-color:transparent;}#mermaid-svg-J3KcPKhnFytoguPQ .label text,#mermaid-svg-J3KcPKhnFytoguPQ span{fill:#333;color:#333;}#mermaid-svg-J3KcPKhnFytoguPQ .node rect,#mermaid-svg-J3KcPKhnFytoguPQ .node circle,#mermaid-svg-J3KcPKhnFytoguPQ .node ellipse,#mermaid-svg-J3KcPKhnFytoguPQ .node polygon,#mermaid-svg-J3KcPKhnFytoguPQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-J3KcPKhnFytoguPQ .rough-node .label text,#mermaid-svg-J3KcPKhnFytoguPQ .node .label text,#mermaid-svg-J3KcPKhnFytoguPQ .image-shape .label,#mermaid-svg-J3KcPKhnFytoguPQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-J3KcPKhnFytoguPQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-J3KcPKhnFytoguPQ .rough-node .label,#mermaid-svg-J3KcPKhnFytoguPQ .node .label,#mermaid-svg-J3KcPKhnFytoguPQ .image-shape .label,#mermaid-svg-J3KcPKhnFytoguPQ .icon-shape .label{text-align:center;}#mermaid-svg-J3KcPKhnFytoguPQ .node.clickable{cursor:pointer;}#mermaid-svg-J3KcPKhnFytoguPQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-J3KcPKhnFytoguPQ .arrowheadPath{fill:#333333;}#mermaid-svg-J3KcPKhnFytoguPQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-J3KcPKhnFytoguPQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-J3KcPKhnFytoguPQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J3KcPKhnFytoguPQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-J3KcPKhnFytoguPQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J3KcPKhnFytoguPQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-J3KcPKhnFytoguPQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-J3KcPKhnFytoguPQ .cluster text{fill:#333;}#mermaid-svg-J3KcPKhnFytoguPQ .cluster span{color:#333;}#mermaid-svg-J3KcPKhnFytoguPQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-J3KcPKhnFytoguPQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-J3KcPKhnFytoguPQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-J3KcPKhnFytoguPQ .icon-shape,#mermaid-svg-J3KcPKhnFytoguPQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J3KcPKhnFytoguPQ .icon-shape p,#mermaid-svg-J3KcPKhnFytoguPQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-J3KcPKhnFytoguPQ .icon-shape .label rect,#mermaid-svg-J3KcPKhnFytoguPQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J3KcPKhnFytoguPQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-J3KcPKhnFytoguPQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-J3KcPKhnFytoguPQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 外部晶振 HSE
PLL 锁相环
系统时钟 SYSCLK
AHB 总线时钟
APB1 外设时钟
APB2 外设时钟
定时器 TIM2-TIM5
定时器 TIM1/TIM8
2.3 时钟配置要点
- HSE(高速外部时钟):通常由外部晶振提供,精度高。
- PLL(锁相环):用于倍频,将低频晶振倍频到系统所需的高频。
- 分频器:通过 AHB、APB1、APB2 预分频器为不同外设提供合适的时钟频率。
3. 定时器的工作原理
3.1 定时器的基本组成
定时器本质上是一个计数器,在时钟脉冲的驱动下不断累加,当计数值达到预设值时触发中断或事件。
3.2 定时器的分类
| 类型 | 特点 | 典型应用 |
|---|---|---|
| 基本定时器 | 功能简单,仅支持定时 | 定时中断、延时 |
| 通用定时器 | 支持输入捕获、输出比较、PWM | 测频、PWM 输出 |
| 高级定时器 | 功能最全,支持互补输出、死区控制 | 电机控制、三相逆变 |
3.3 定时时间的计算
定时器的定时时间由以下公式决定:
定时时间 = (预分频值 + 1) × (自动重装载值 + 1) / 定时器时钟频率
例如,定时器时钟为 72 MHz,预分频值为 7199,自动重装载值为 999,则定时时间为:
(7199 + 1) × (999 + 1) / 72_000_000 = 0.1 秒
4. 定时器编程实践
4.1 定时器初始化流程
以 STM32 通用定时器为例,初始化步骤如下:
- 使能定时器时钟;
- 配置预分频器与自动重装载值;
- 配置计数模式(向上/向下/中心对齐);
- 使能更新中断;
- 配置 NVIC 中断优先级;
- 启动定时器。
4.2 代码示例:定时器定时中断
c
#include "stm32f10x.h"
void TIM2_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
// 1. 使能定时器时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 2. 配置定时器基础参数
TIM_InitStruct.TIM_Prescaler = 7199; // 预分频值
TIM_InitStruct.TIM_Period = 999; // 自动重装载值
TIM_InitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_InitStruct);
// 3. 使能更新中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 4. 配置中断优先级
NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
// 5. 启动定时器
TIM_Cmd(TIM2, ENABLE);
}
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 在此处添加定时中断处理逻辑
}
}
4.3 代码示例:PWM 输出
c
void PWM_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
TIM_OCInitTypeDef TIM_OCInitStruct;
// 使能 GPIO 和定时器时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 配置 PA6 为复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置定时器
TIM_TimeBaseInitTypeDef TIM_InitStruct;
TIM_InitStruct.TIM_Prescaler = 71;
TIM_InitStruct.TIM_Period = 999;
TIM_InitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_InitStruct);
// 配置 PWM 输出通道
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStruct.TIM_Pulse = 500; // 占空比 50%
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM3, &TIM_OCInitStruct);
// 启动定时器
TIM_Cmd(TIM3, ENABLE);
}
5. 学习心得与常见问题
5.1 学习心得
- 先理解时钟树,再学定时器:定时器的频率来源于时钟树,不理解时钟配置就无法准确计算定时时间。
- 动手验证公式:通过修改预分频值和重装载值,观察 LED 闪烁频率的变化,能加深对定时器原理的理解。
- 善用调试工具:使用逻辑分析仪或示波器观察 PWM 波形,验证配置是否正确。
5.2 常见问题
- 定时时间不准确:多为时钟源配置错误或分频计算有误。
- 中断不触发:检查中断使能、NVIC 配置以及中断标志位是否清除。
- PWM 无输出:检查 GPIO 复用配置、输出通道使能以及定时器是否启动。
6. 总结
定时器与时钟是 ARM 嵌入式开发的核心基础。掌握时钟树的配置方法,理解定时器的工作原理与计算公式,并通过实际编程验证,是学好嵌入式开发的关键路径。希望本文的总结能对正在学习 ARM 嵌入式的你有所帮助。