目录
定时器基本定时功能实现
CubeMX设置
手动书写代码部分
定时器启动
实现溢出回调函数
HAL_Delay介绍
HAL_Delay实现原理
HAL_Delay的优点
HAL_Delay的缺点
利用滴答定时器(SysTick)实现微秒级延时
PWM
PWM介绍
通用定时器中的重要寄存器
PWM中的捕获比较通道
什么是定时器通道?
定时器通道如何工作?
为什么需要定时器通道?
PWM实现呼吸灯
CubeMX配置
关于TIMx_PSC和TIMx_ARR的计算
几种不同方式实现设定PWM的周期
如何选择不同PSC与ARR的组合?
代码编写
定时器基本定时功能实现
CubeMX设置
注意,这里只是对定时器配置实现一个很简单的定时功能。
时钟来源
定时器中断使能
定时器预分频值,计数模式,计数周期
手动书写代码部分
定时器启动
下面主要介绍一下关于定时器的简单运用
关于下面图片中的初始化函数我们可以不用过于在意,因为在我们上面CUBEMX中已经配置过了,而且定时器的初始化相比于外部中断要复杂的多,所以就不推荐手动书写代码配置了,主要掌握CubeMX中的定时器初始化配置方式就行。
在main.c中让定时器开始运行
尽管我们在CubeMX或类似的配置工具中配置了定时器,并生成了代码,但生成代码仅仅是设置了定时器的参数(如时钟源、预分频器、自动重载值、中断使能等)。这些配置通常在 MX_TIMx_Init() 或类似的初始化函数中完成。
然而,这些初始化函数并不会自动启动定时器或启用中断,需要我们调用相应的启动函数来手动对定时器开启。这是一种设计哲学,记住就好,也就是多了一行开启时钟代码。
启动定时器函数需要一个参数,接受一个TIM_HandleTypeDef类型的地址,
这个参数在我们的TIM2_Init函数中有,是在tim.c中定义的,是一个全局变量。
由于我们的main.c文件中包含了tim.h头文件,并且tim.h中对于这个变量进行了extern声明,所以在main中可以直接使用htim2了
htim2 用于表示特定定时器实例(在这里是 TIM2) 的一个句柄 (Handle) 结构体变量,包含了管理特定定时器所需的所有配置信息和状态,我们对定时器进行配置的时候主要就是对这个变量进行配置。
- 它是全局的:通常定义在文件作用域,可以在程序的任何地方(只要包含了定义它的文件或声明它的头文件)访问和修改它。
- 它是访问点:所有针对 TIM2 定时器的 HAL 库函数(如 HAL_TIM_Base_Start_IT(&htim2)、HAL_TIM_PeriodElapsedCallback(&htim) 等)都需要通过这个 htim2 句柄来知道它们正在操作的是哪个定时器。
实现溢出回调函数
由于我们使用了CubeMX配置了定时器,所以关于定时器的TIM6_DAC_IRQHander()不需要我们来声明并实现,这里的逻辑和中断处理那块的逻辑差不多,不过定时器这里有很多回调函数,对于简单的定时功能,我们需要实现的是溢出回调函数——HAL_TIM_PeriodElapsedCallback()。
通过HAL_TIM_PeriodElapsedCallback()可以实现周期性任务:
- LED 闪烁: 每隔 500ms 翻转一次 LED 的状态。
- 数据采样: 每隔 10ms 读取一次传感器数据。
- 任务调度: 以固定的频率触发一个任务的执行。
关于HAL_TIM_PeriodElapsedCallback这个弱定义函数可以放main.c函数中进行实现。
补充一下main.c文件中函数声明和实现的写法
声明在 main() 函数之前: 在 main.c 中,确实经常看到用户编写的函数(在 main() 函数之前进行声明(原型声明)。
实现通常在 main() 函数之后: 而这些函数的实现(函数体)则经常放在 main() 函数的后面,通常是在 /* USER CODE BEGIN 4 */ 和 /* USER CODE END 4 */ 这样的用户代码区域内。
其他模块的文件一般我们的选择是:在.h中进行函数的声明,在.c文件中进行函数的实现。
HAL_Delay介绍
HAL_Delay实现原理
HAL_Delay() 的实现方式主要基于 SysTick 定时器 和一个全局的滴答计数变量 (uwTick)。
如果是使用CubeMX进行的配置,那么默认会在main()中调用HAL_Init(),里面会自动帮我们启动滴答定时器(SysTick)。
SysTick 是 Cortex-M 系列处理器(包括 STM32)内置的一个 24 位倒计时定时器。它直接集成在 CPU 核心内部。
工作方式:
- 我们会配置 SysTick 的重装载值(Reload Value)。
- 一旦启用,SysTick 计数器会从这个重装载值开始递减,SysTick 计数器一般直接使用系统的主时钟频率HCLK。
- 当计数器递减到 0 时,它会产生一个 SysTick 中断,然后自动重新装载并再次开始递减。所以 每过 1ms,SysTick 计数器递减到 0,触发一次 SysTick 中断。
- 执行 ISR: 处理器响应 SysTick 中断,执行相应的 ISR。
- 调用 HAL_IncTick(): 在 SysTick 的 ISR 内部,HAL_IncTick() 被执行。
- 更新 uwTick: HAL_IncTick() 将全局变量 uwTick 加 1。
- HAL_GetTick() 函数的实现非常直接,它只是简单地返回当前 uwTick 变量的值:
HAL_Delay的伪代码如下:
void HAL_Delay(uint32_t Delay) {uint32_t tickstart = HAL_GetTick(); // 获取当前系统滴答值uint32_t wait = tickstart + Delay; // 计算目标滴答值// 等待直到达到目标滴答值,并处理uwTick可能溢出的情况while((HAL_GetTick() < wait) && ((HAL_GetTick() - tickstart) < Delay)) {// 空循环,CPU在此处忙等待}
}
HAL_Delay的优点
1. 使用简单,易于上手
HAL_Delay()
的接口非常直观:你只需要传入一个你想要延时的毫秒数,函数就会阻塞相应的时长。对于嵌入式编程的初学者来说,这是最容易理解和使用的延时方式,能够快速实现一些简单的功能
2. 无需额外配置
一旦 HAL 库和 SysTick 定时器被初始化(这通常在项目启动时自动完成),HAL_Delay()
就可以直接调用,无需进行额外的定时器配置,也不需要编写中断服务程序。这大大简化了开发流程,尤其是在快速原型开发或对延时精度要求不高的场景下。
HAL_Delay的缺点
1. 阻塞式操作 (Blocking):
这是最主要的缺点。当调用 HAL_Delay() 时,CPU 会进入一个忙等待循环,不执行任何其他有用的任务,直到延时结束。
2. 不适用于精确的微秒级延时:
HAL_Delay() 的精度是毫秒级,因为它依赖于 1ms 的 SysTick 中断。对于需要微秒(us)甚至纳秒(ns)级别的精确延时,HAL_Delay() 无法满足要求。
3.不适用于中断服务程序 (ISR) 中使用:
严重问题: 绝对不能在中断服务程序 (ISR) 中直接调用 HAL_Delay()。
HAL_Delay() 依赖于 SysTick 中断来更新 uwTick 变量。如果 SysTick 中断的优先级低于(数值上大于)当前执行的 ISR,那么 SysTick 中断将无法抢占当前 ISR 并执行,导致 uwTick 无法更新。这样一来,HAL_Delay() 就会陷入无限循环,使系统彻底崩溃。
HAL_Delay()中的SysTick定时器默认的优先级是最低的,所以在ISR中调用HAL_Delay不可能调用成功,会持续阻塞在这里,除非我们手动调整SysTick定时器优先级(让其变得更高),但是这也是非常非常非常不推荐的!!!
4. 与 RTOS 的兼容性问题:
如果您的项目使用了实时操作系统 (RTOS),如 FreeRTOS,直接使用 HAL_Delay() 是不推荐的。
RTOS 有自己的任务调度机制,它提供的延时函数(例如 FreeRTOS 的 osDelay() 或 vTaskDelay())会在任务延时期间将当前任务挂起,并允许调度器切换到其他任务执行,从而充分利用 CPU 资源。
利用滴答定时器(SysTick)实现微秒级延时
上面说到了使用滴答定时器(SysTick)实现的延迟函数HAL_Delay,只能实现ms级别延时,对于更精确的微秒级别是不支持的,下面我们自己来使用SysTick实现微秒级延时。
下面实现的delay_us 函数是一个典型的忙等待(busy-waiting) 实现,也就是基于查询方式实现的,没有用到中断。
void delay_us(uint32_t nus){uint32_t ticks;uint32_t told, tnow, tcnt = 0;uint32_t reload = SysTick->LOAD + 1; //计数个数为重装载值加1ticks=nus*(SystemCoreClock/1000000); //nus 微秒总共需要的 SysTick 节拍数told= SysTick->VAL; //初始计数器值while(1){tnow=SysTick->VAL;if(tnow!= told){if(tnow<told) tcnt += told- tnow; //SysTick递减的计数器else tcnt += reload- tnow + told;told= tnow;if(tcnt>=ticks) break; //延时时间已到,退出}}}
这段代码通过不断读取 SysTick 的值,并巧妙地处理了 SysTick 递减计数和溢出(绕回)的特性,来精确地累加流逝的节拍数。当累加的节拍数达到预设的目标值时,就完成了微秒级的延时。
PWM
下面这个视频是对pwm比较专业一点的介绍
【STM32】输出比较模式讲解以及STM32CUBEMX+MDK代码实现_哔哩哔哩_bilibili
下面这篇文章是对pwm比较通俗一点的介绍,更易理解
PWM原理 PWM频率与占空比详解-CSDN博客
PWM介绍
PWM,全称是脉冲宽度调制,它通过数字方式来模拟出模拟信号的效果,简单来说,PWM的原理就是通过快速开关一个数字信号(比如电源),并且控制它在一个周期内“开”的时间长短来达到目的。
脉冲宽度通常就是指在一个完整的 PWM 周期内,信号处于高电平(ON 状态)的持续时间长度。
下面是一些关键点:
-
PWM周期(一个PWM完整波需要的时间)(Period):这是指一个完整的PWM波形所需的时间,也就是说,信号从“开”到“关”再回到“开”的总时间。
-
占空比(Duty Cycle):这是PWM的核心。它表示在一个周期内,信号处于“开”状态的时间所占的比例。占空比越高,信号“开”的时间就越长。
-
PWM频率(Frequency):这是指每秒钟有多少个PWM周期。频率越高,信号切换得越快,看起来就越平滑,越像一个真正的模拟信号。PWM 的频率是由 ARR 和 PSC 共同决定的
PWM是如何工作的?
想象一下你有一个灯泡,你想控制它的亮度。
如果你一直给灯泡供电(100%占空比),它就会全亮。
如果你完全不给灯泡供电(0%占空比),它就会熄灭。
如果你以很快的速度,比如每秒钟开关1000次,每次只让灯泡亮一半的时间(50%占空比),那么因为你的眼睛无法分辨这么快的开关,灯泡看起来就会是半亮的状态。
这就是PWM的工作原理。通过调整“开”的时间比例(占空比),我们就可以控制灯泡的亮度、电机的转速、音频信号的音量等等。
通用定时器中的重要寄存器
预分频器寄存器 (TIMx_PSC)
这个寄存器用于设置定时器时钟的预分频值,从而设置了定时器的时钟频率。
PSC (Prescaler Value):定时器时钟源会通过这个预分频器进行分频,从而得到计数器实际使用的时钟频率。计算方式:计数器时钟频率 = 定时器时钟源频率 / (PSC + 1)。
自动重载寄存器 (TIMx_ARR)
这个寄存器定义了计数器达到多少时会溢出并重新开始计数(或改变计数方向)。
- ARR (Auto-Reload Value):当计数器达到 ARR 的值时,会发生更新事件。
- 计算方式:PWM 的频率是由 ARR 和 PSC 共同决定的。PWM 频率 = 计数器时钟频率 / (ARR + 1) = 定时器时钟源频率 / ((PSC + 1) * (ARR + 1))。
- 作用:设置 PWM 信号的周期和频率。ARR 值越大,PWM周期越长,频率越低。
捕获/比较寄存器 (TIMx_CCRx)
每个 PWM 通道都有一个对应的 CCRx 寄存器(如 TIMx_CCR1, TIMx_CCR2 等)。
CCRx (Capture/Compare Register Value):这个寄存器存储的值与计数器 CNT 的值进行比较,从而决定 PWM 信号的占空比。
- 计算方式:占空比 = CCRx / (ARR + 1)。
- 例如,如果 ARR = 999,CCRx = 500,那么占空比就是 500 / 1000 = 50%。
- 作用:设置 PWM 信号的脉冲宽度,进而控制占空比。
当然通用定时器中还有一些其他寄存器,很多这些寄存器由CubeMX帮我们自动设置好了,所以不需要很关注。
PWM中的捕获比较通道
什么是定时器通道?
你可以把一个微控制器里的定时器想象成一个多功能的厨房定时器总机。这个总机本身可以计时(例如,设定每秒滴答一次)。而通道就是这个总机上独立的定时器插口或功能模块。
每个通道都可以独立地配置来完成特定的任务,例如:
-
捕获输入(Input Capture): 测量外部信号的脉冲宽度、频率或边沿之间的时间间隔。
-
比较输出(Output Compare): 在定时器计数到预设值时,改变输出引脚的状态(高/低电平),用于产生PWM波形、延时输出脉冲等。
-
PWM 生成(PWM Generation): 最常见的用途之一,生成可调占空比的脉冲宽度调制信号,用于电机调速、LED调光等。
-
单脉冲模式(One-Pulse Mode): 在事件发生后产生一个固定宽度的脉冲。
一个定时器通常会有2个、4个或更多个通道,这意味着这个定时器可以同时处理2个、4个或更多个上述的独立任务。
定时器通道如何工作?
每个定时器通道内部都有一组专门的寄存器来配置它的行为,其中最重要的就是比较/捕获寄存器 (Capture/Compare Register, CCR)。
-
作为输出(Output Compare/PWM): 当定时器的内部计数器(通常是TIMx_CNT寄存器)的值与某个通道的CCR寄存器的值相等时,定时器就会触发该通道预设的动作(例如,翻转输出电平、生成PWM脉冲)。你可以为每个通道设置不同的CCR值,从而产生不同的输出波形或在不同时间点触发事件。一旦配置好,通道会根据定时器计数器的值自动在引脚上生成PWM波形,无需CPU干预。
-
作为输入(Input Capture): 当外部引脚上的信号(例如,上升沿或下降沿)发生变化时,定时器会将当前内部计数器(TIMx_CNT)的值“捕获”到对应通道的CCR寄存器中。通过读取不同边沿捕获到的CCR值,就可以计算出脉冲宽度、周期等。
为什么需要定时器通道?
-
多任务并行: 如果一个应用需要同时生成两个不同频率或占空比的PWM波形,或者同时测量两个不同信号的频率,那么使用一个多通道定时器会比使用两个独立的单功能定时器更高效、更节省资源。
-
资源优化: 微控制器内部的硬件定时器是有限的宝贵资源。多通道设计允许单个定时器模块完成多种定时/计数相关的任务,从而节省了片上定时器模块的数量。
-
灵活性: 每个通道都可以独立配置其工作模式,极大地增加了定时器模块的灵活性,使其能够适应各种复杂的应用需求。
-
硬件实现: 定时器通道通常通过硬件逻辑实现,这意味着一旦配置完成,它们就能自动、精确地工作,无需CPU干预,从而减轻了CPU的负担,提高了系统的实时性。
PWM实现呼吸灯
CubeMX配置
PB10 引脚配置成 TIM2_CH3(定时器2的通道3) 的操作,正是属于 STM32 微控制器中的 输出复用功能模式。
复用功能 (Alternate Function, AF): 一个GPIO引脚除了其通用IO功能外,还可以“复用”为某个片内外设(如定时器、SPI、I2C、USART等)的专用功能引脚。
这样配置之后的效果:
引脚功能特化: PB10 不再是简单的 GPIO,它变成了一个由硬件定时器控制的专用引脚。
- 作为 PWM 输出, PB10 引脚将输出一个脉冲宽度调制(PWM)波形。
- 作为 输出比较,当 TIM2 的计数器值与 TIM2_CH3 的比较值(CCR3)相等时,PB10 引脚的电平状态会按照预设的模式发生变化(例如,翻转、置高、置低)。
- 作为 输入捕获,PB10 引脚将作为一个输入引脚。当外部信号在这个引脚上发生预设的边沿(上升沿、下降沿或双边沿)时,TIM2 定时器的当前计数值会被立即“捕获”并存储到 TIM2_CH3 对应的捕获/比较寄存器(CCR3)中。
我想要让LED1变成呼吸灯,在我的电路板上,LED1对应的是PB10,然后右键查看对应的多路复用模式中对应的正好是TIM2_CH3,我们选择定时器2的通道3,然后我们需要去配置 TIM2
具体的参数配置,主要是要计算分频系数(TIMx_PSC)和自动重载寄存器 (TIMx_ARR)
通过设置这两个寄存器,就实现了设置PWM的周期和频率
这里我们的主时钟频率为100Mhz,假如我想要让输入TIM2的时钟频率变为100Khz,让PWM的周期变成20ms,那么此时TIMx_PSC和TIMx_ARR计算方式如下:
TIMx_PSC = 100Mhz/100Khz - 1 = 999;
由于TIM2的时钟频率为100Khz,所以一个节拍对应的为1/100000=0.00001s=0.01ms,
20ms/0.01ms=2000,所以TIMx_ARR=2000-1=1999
关于TIMx_PSC和TIMx_ARR的计算
几种不同方式实现设定PWM的周期
在上面的例子中,要达到 PWM 周期为 20ms 这个目标,TIMx_PSC (分频系数) 和 TIMx_ARR (自动重载寄存器) 的设置并非只有一种固定组合。它们是相互关联的,我们可以通过调整其中一个,来相应地调整另一个,以达到相同的周期。
-
定时器时钟频率:这是输入到特定定时器(例如 TIM2)的时钟频率,通常是主时钟频率经过 APB 分频器后得到的。在我们的例子中,假设主时钟是 100MHz。
-
TIMx_PSC
(Prescaler Value):分频系数。它决定了定时器计数器实际的计数频率。计数器每经过(TIMx_PSC + 1)
个定时器时钟周期,才递增/递减一次。
-
TIMx_ARR
(Auto-Reload Register):自动重载值。它决定了定时器计数器的上限。计数器从 0 数到TIMx_ARR
(或从TIMx_ARR
数到 0),表示一个完整的计数周期,共(TIMx_ARR + 1)
个节拍。
方法一:我们之前的方法 (分频后的时钟频率为 100 KHz)
-
选择分频系数
TIMx_PSC = 999
-
此时
(PSC + 1) = 1000
-
分频后的定时器计数频率 = 100 MHz/1000=100 KHz
-
-
计算
TIMx_ARR
:-
(TIMx_ARR + 1) \times 1000 = 2,000,000
-
(TIMx_ARR + 1) = 2,000,000 / 1000 = 2000
-
TIMx_ARR=1999
-
这种组合是:
PSC = 999
,ARR = 1999
。
-
方法二:让分频后的时钟频率为 1 MHz
-
选择分频系数
TIMx_PSC = 99
-
此时
(PSC + 1) = 100
-
分频后的定时器计数频率 = 100 MHz/100=1 MHz
-
-
计算
TIMx_ARR
:-
(TIMx_ARR + 1) \times 100 = 2,000,000
-
(TIMx_ARR + 1) = 2,000,000 / 100 = 20000
-
TIMx_ARR=19999
-
这种组合是:
PSC = 99
,ARR = 19999
。
-
方法三:让分频后的时钟频率为 50 KHz
-
选择分频系数
TIMx_PSC = 1999
-
此时
(PSC + 1) = 2000
-
分频后的定时器计数频率 = 100 MHz/2000=50 KHz
-
-
计算
TIMx_ARR
:-
(TIMx_ARR + 1) \times 2000 = 2,000,000
-
(TIMx_ARR + 1) = 2,000,000 / 2000 = 1000
-
TIMx_ARR=999
-
这种组合是:
PSC = 1999
,ARR = 999
。
-
如何选择不同PSC与ARR的组合?
虽然有多种组合可以达到相同的 PWM 周期,但在实际应用中,选择哪种组合通常取决于以下因素:
-
占空比精度 (Duty Cycle Resolution):
-
ARR
值越大,表示在一个 PWM 周期内有更多的计数节拍。 -
这意味着您可以更精细地调整占空比。例如,如果
ARR = 19999
,您可以将占空比设置为(0/20000)
到(19999/20000)
之间的任何值,有 20000 个可能的占空比级别。 -
如果
ARR = 999
,您只有 1000 个占空比级别。 -
因此,通常会选择较大的
ARR
值以获得更高的占空比精度,前提是ARR
不超过寄存器的最大值(如 16位定时器ARR
最大为 65535,32位定时器更大)。
-
-
PSC
和ARR
的寄存器大小限制:-
大多数通用定时器的
PSC
和ARR
寄存器是 16 位的,这意味着它们的值不能超过 65535。 -
有些高级定时器或较新的微控制器可能有 32 位的定时器。在选择
PSC
和ARR
时,需要确保它们不超过对应寄存器的最大值。 -
在我们的例子中,
2,000,000
这个乘积超出了 16 位定时器的单个寄存器范围,所以必须进行分频,即PSC
和ARR
都不能为 0(除非定时器频率非常低)。
-
-
计算方便性/可读性:
-
有时会选择整数倍的分频,使得计数频率成为一个“整”的 KHz 或 MHz 值,方便理解和计算。
-
综上所述,我们通常会选择较大的 ARR
值以获得更高的占空比精度,但是同时也要注意PSC 和 ARR 设置的值不能超过寄存器大小限制!
代码编写
由于CubeMX依旧已经帮我们做了很多工作,所以这里我们需要修改的很少。
呼吸灯的效果是通过 PWM (脉冲宽度调制) 来实现的。我们通过周期性地改变 PWM 波形的占空比(即高电平持续时间与整个周期的比值),来控制 LED 的亮度。
- 当占空比从 0% 逐渐增加到 100% 时,LED 会从灭逐渐变亮;
- 当占空比从 100% 逐渐减小到 0% 时,LED 会从亮逐渐变灭。
这个过程循环往复,就形成了“呼吸”的效果。
下面是启动 PWM 输出的关键函数。它告诉定时器 TIM2 的通道3 开始生成 PWM 波形。请务必在进入 while(1) 循环之前调用它。
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
这个函数会启动定时器(如果它尚未运行)并使其 PWM 输出开始在指定的通道引脚上生成波形。
关于调节占空比其实就是在调节比较捕获寄存器的值:
- 计算方式:占空比 = CCRx / (ARR + 1)。
- 例如,如果 ARR = 999,CCRx = 500,那么占空比就是 500 / 1000 = 50%。
设置比较捕获寄存器TIMx_CCRx的值:
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, duty);
这是一个 HAL 库的宏(本质上是直接操作寄存器),用于设置指定定时器通道的比较值(CCR 寄存器)。
duty_cycle 的值直接决定了 PWM 的占空比。当 duty_cycle 接近 0 时,LED 灭;接近 PWM_MAX_DUTY 时,LED 最亮。
/* USER CODE BEGIN 2 */HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); /* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */uint32_t duty;while (1){/* USER CODE END WHILE */for(duty=0; duty<2000; duty += 20){__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, duty);HAL_Delay(20);}for(duty=2000; duty>0; duty-=20){__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, duty);HAL_Delay(20);}/* USER CODE BEGIN 3 */}
推荐好文:
STM32定时器详解:原理、配置与应用实战-CSDN博客
STM32 定时器TIM-CSDN博客