一、什么是定时器?
定时器,顾名思义就是用来定时的内部外设。不同的芯片型号上搭载了不同的定时器,定时器的类型也分为高级定时器,中级定时器,基本定时器。
高级定时器 (TIM1, TIM8等)
高级定时器功能最为强大,通常包含以下特性:
- 多通道 PWM 输出: 可以产生多个独立的 PWM 信号,用于电机控制、逆变器等应用。
- 互补输出: 支持互补的 PWM 输出,常用于控制三相电机。
- 死区时间插入: 在互补输出之间插入可编程的死区时间,防止功率器件同时导通。
- 编码器接口: 可以直接连接增量式编码器,用于测量旋转速度和方向。
- 触发输入和输出: 可以与其他定时器或外设进行同步触发。
高级定时器通常用于需要精确控制和复杂波形生成的场合。
中级定时器 (TIM2, TIM3, TIM4, TIM5等)
中级定时器也称为通用定时器,功能相对平衡,包含以下特性:
- 基本计数功能: 提供向上、向下或中央对齐的计数模式。
- 可编程预分频器: 可以对时钟源进行分频,以获得不同的计数频率。
- 自动重装载功能: 计数器溢出后可以自动重装载预设值。
- 输出比较功能: 可以产生单脉冲或用于 PWM 生成。
- 输入捕获功能: 可以捕获外部信号的上升沿、下降沿或双边沿。
- 触发输入和输出: 可以与其他定时器或外设进行同步触发。
中级定时器应用广泛,可以用于测量时间间隔、生成 PWM 信号、实现简单的定时中断等,例如您提供的链接中的实验就是使用了通用定时器 TIM3 来实现中断。
基本定时器 (TIM6, TIM7等)
基本定时器功能最为简单,主要用于产生基本的时间基准,包含以下特性:
- 简单的向上计数功能: 只能进行向上计数。
- 可编程预分频器: 可以对时钟源进行分频。
- 自动重装载功能: 计数器溢出后可以自动重装载预设值。
基本定时器通常用于驱动 DAC 转换器或者作为简单的延时功能。
网上有一个说法,定时器本质上就是计数器,何以见得,我觉得通过下面关于时基单元的介绍,也许能解释这个说法。
二、时基单元是什么,有什么用?
首先,我们引入一个东西,晶振,
晶振
晶体振荡器(Crystal Oscillator)是一种利用石英晶体的压电效应制成的谐振器件。当石英晶体受到外加电场的作用时会发生形变,反之,当石英晶体发生形变时也会产生电场。如果在石英晶体的两个电极之间施加一个交变电压,当电压的频率接近石英晶片的固有频率时,就会发生谐振现象,产生一个非常稳定的振荡信号。
在嵌入式系统中,晶振通常作为系统时钟的来源,为 CPU、外设等提供稳定的时序基准。晶振的频率非常精确且稳定,受温度和电压变化的影响较小,因此是实现精确定时的关键。常见的晶振频率有 8MHz、12MHz、25MHz 等。
总而言之,晶振会以一个一定的频率发出一个信号,我们使用一个计数器接受到信号,然后计数器得到的数,就反映了时间。时基单元的配置,就是配置信号的频率,以及我们需要定时的时间,这个定时的时间,其实也是通过对比计数器得到的数来体现的。
时基单元
时基单元(Time Base Unit)是定时器的核心组成部分,它负责产生定时器的时间基准。一个典型的时基单元包含以下几个关键组件:
- 时钟源 (Clock Source): 定时器的计数时钟来源,通常是来自系统时钟(如 APB 总线时钟)经过选择和可能的预分频后的时钟信号。在 STM32 中,可以通过 RCC 寄存器配置时钟源和分频系数。
- 预分频器 (Prescaler, PSC): 预分频器用于对输入的时钟源进行分频。通过设置预分频器的值,可以将时钟频率降低到适合计数器计数的范围。预分频器的作用是延长计数器的计数周期,从而实现更长时间的定时。例如,如果预分频器的值为 N,则计数器的时钟频率为输入时钟频率的 1/(N+1)。
- 计数器 (Counter, CNT): 计数器是一个寄存器,它在每个时钟脉冲到来时进行递增(或递减,取决于计数模式)。计数器的位数决定了其最大计数值,例如一个 16 位的计数器可以计数到 65535。
- 自动重装载寄存器 (Auto-Reload Register, ARR): 自动重装载寄存器存储着计数器需要达到的目标值。当计数器计数到 ARR 的值时,会发生以下动作之一(取决于配置):
- 溢出 (Overflow): 如果是向上计数模式,计数器会从 0 重新开始计数,并可能产生一个更新事件(Update Event),该事件可以触发中断。
- 比较匹配 (Compare Match): 如果配置了输出比较功能,当计数器值等于某个比较寄存器的值时,也会产生相应的事件。
通过配置预分频器和自动重装载寄存器的值,可以精确地控制定时器的计数频率和计数周期,从而实现所需的定时时间。例如,假设 APB1 时钟频率为 36MHz,我们希望实现 1ms 的定时周期。可以将预分频器设置为 35,这样计数器的时钟频率就为 1MHz。然后将自动重装载寄存器设置为 999,这样计数器每计数 1000 次(即 1ms)就会溢出一次,产生更新事件。
时基单元原理图

时基单元的原理可以简单描述为:
一个时钟源信号输入到预分频器 (PSC),预分频器根据配置的分频系数对时钟信号进行分频,得到计数器 (CNT) 的时钟信号。计数器在该时钟信号的驱动下进行计数,其计数值不断与自动重装载寄存器 (ARR) 中的值进行比较。当计数器的值达到或超过 ARR 的值时,会产生一个更新事件(Update Event)。这个更新事件可以触发中断标志位,从而引发定时中断。
逻辑连接示意:
Clock Source ---> Prescaler (PSC) ---> Counter (CNT) ---> Comparator (vs. Auto-Reload Register ARR) ---> Update Event Flag
三、定时中断是什么
理解了时基单元,自然就知道定时中断是什么了。当定时器的计数器计数到自动重装载寄存器设定的值,并产生更新事件时,如果使能了相应的中断,那么就会触发一个中断请求信号,CPU 响应这个请求后,会跳转到预先定义好的中断服务函数中执行相应的代码。在 STM32 中,需要配置定时器的中断使能位以及 NVIC(嵌套向量中断控制器),才能使能定时中断。您提供的链接中的实验就展示了如何配置 TIM3 的中断,并在中断服务函数中翻转 LED 的电平。关于中断,可以看这篇文章中断:嵌入式系统的高效事件处理机制_嵌入式单片机中断-CSDN博客文章浏览阅读315次,点赞3次,收藏5次。中断是嵌入式系统中一种重要的事件处理机制。它允许单片机在执行主程序的过程中,当有特定的外部事件发生时,能够暂时中断当前的任务,转而去执行一段专门处理该事件的程序,处理完毕后再回到原来的位置继续执行主程序。这就好比你在专注地写代码,突然手机铃声响起,你暂停编码,接听电话处理重要事务,通话结束后再回到代码编辑状态,继续未完成的工作。总的来说,中断是一种节约资源的手段,也是提升程序灵活度的工具,与日常生活也很贴近,很好理解。_嵌入式单片机中断https://blog.csdn.net/m0_62710548/article/details/146465808?spm=1001.2014.3001.5501
四、用定时中断写一个 delay()
补充一个常用定时中断做的 delay()
#include "stm32f10x.h" // 根据你的STM32型号选择头文件
// 定义一个全局变量作为延时标志位
volatile uint8_t delay_flag = 0;
// 配置 TIM2 定时器
void TIM2_Configuration(uint16_t period_ms) {
// 1. 使能 TIM2 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 2. 配置 TIM2 的时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
// 假设系统时钟为 72MHz,APB1 时钟为 36MHz
// 设置预分频系数,将时钟频率降低到 1MHz (36MHz / 36)
TIM_TimeBaseStructure.TIM_Prescaler = 35;
// 设置自动重装载值,以实现 period_ms 毫秒的定时
// 例如,period_ms = 1 时,计数 1000 次溢出 (1MHz / 1000 = 1kHz, 周期 1ms)
TIM_TimeBaseStructure.TIM_Period = period_ms * 1000 - 1;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 3. 使能 TIM2 中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 4. 配置 NVIC 中断控制器
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 5. 使能 TIM2 定时器
TIM_Cmd(TIM2, ENABLE);
}
// TIM2 中断服务函数
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
// 清除中断标志位
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 设置延时标志位
delay_flag = 1;
}
}
// 延时函数 (毫秒级)
void delay_ms(uint16_t ms) {
delay_flag = 0;
TIM2_Configuration(ms); // 重新配置定时器以实现所需的延时
// 等待延时标志位被设置
while (delay_flag == 0);
// 停止定时器 (可选,如果只需要单次延时)
TIM_Cmd(TIM2, DISABLE);
TIM_ITConfig(TIM2, TIM_IT_Update, DISABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, DISABLE);
}
// 示例:主函数 (包含 GPIO 配置作为演示)
void GPIO_Configuration(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能 GPIOA 时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 选择 GPIOA 的引脚 0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置为推挽输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置传输速度
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_0); // 初始状态设置为高电平
}
int main(void) {
GPIO_Configuration(); // 配置 GPIO
while (1) {
GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 拉低电平,例如点亮 LED
delay_ms(500);
GPIO_SetBits(GPIOA, GPIO_Pin_0); // 拉高电平,例如熄灭 LED
delay_ms(1000);
}
}
代码补充说明:
- GPIO 配置: 在
main()
函数中,我添加了一个GPIO_Configuration()
函数作为演示,配置了 GPIOA 的引脚 0 为推挽输出,可以用于连接 LED 等外设,更贴合您提供的链接中实验的做法。 - 主函数示例:
main()
函数中展示了如何使用delay_ms()
函数来控制 GPIO 引脚的输出状态,实现简单的闪烁效果。 - 总的来说,当使用
delay_ms()
时,程序会配置一个硬件定时器来计时,并在计时结束后通过中断的方式通知程序,程序会一直等待这个通知的到来,从而实现延时的效果。