【STM32】定时器
定时器就像Qt的QTimer,还是硬件级的,超好用。不过有一说一,基本定时器更符合定时器的定义,通用定时器和高级定时器的作用已经不是“定时器”三个字可以概括的了。
大部分图片来源:正点原子HAL库课程
专栏目录:记录自己的嵌入式学习之路-CSDN博客
目录
1 定时器类型
2 计时器的几种溢出模式
3 芯片定时器参数及性质查询
4 关于计数器计数值的计算
5 常用公共函数
5.1 定时器计数器清零
5.2 获取计数器当前值
5.3 开启中断
5.4 启用xx功能及其对应中断
5.5 启用xx功能但不启用其对应中断
5.6 清除标志位
6 基本定时器
6.1 特性
6.2 原理
6.3 溢出时间计算方法
6.4 配置步骤
6.5 用到的函数
6.6 注意事项⚠️
7 输出比较模式
7.1 PWM1/PWM2模式
7.2 翻转模式
8 通用定时器
8.1 特性
8.2 原理
8.3 配置PWM输出模式
8.4 配置PWM输出模式相关函数
8.5 输入捕获
8.6 输入捕获配置步骤
8.7 脉冲计数
8.8 脉冲计数配置步骤
9 高级定时器
9.1 特性
9.2 重复计数器
9.3 输出指定个数的PWM
9.4 输出比较模式配置步骤(输出翻转模式实验)
9.5 带死区控制的互补输出
9.6 带死区控制的互补输出配置步骤
9.7 刹车(断路)功能
9.8 PWM输入模式
10 公共注意事项
10.1 BASE_Init和功能_Init之间的MsInit矛盾
10.2 HAL_TIM_IC_Start和HAL_TIM_IC_Start_IT的区别
10.3 while等待状态变化和中断回调不要同时使用
10.4 上升沿和下降沿的监测和切换
1 定时器类型
2 计时器的几种溢出模式
3 芯片定时器参数及性质查询
芯片资料文档,如:《STM32F103ZET6.pdf》
4 关于计数器计数值的计算
- 计数次数实际上是计数器值+1,因为计数器是从0开始计数的。我的理解是它计的第一次是0,后面是1,所以实际计数次数才是要加一的;
- 关于PWM占空比的计算,由于占空比是由ARR和CCR共同决定的,而计算式子中ARR总是会被+1,同理计算时CCR应当也要加一,因此设置占空比计算一般都是这样的:
- 占空比 = (CRR+1) / (ARR+1)
5 常用公共函数
5.1 定时器计数器清零
__HAL_TIM_SET_COUNTER(定时器句柄地址, 0);
5.2 获取计数器当前值
__HAL_TIM_GET_COUNTER(定时器句柄地址);
5.3 开启中断
__HAL_TIM_ENABLE_IT(定时器句柄地址, 中断类型);
5.4 启用xx功能及其对应中断
HAL_TIM_xx_Start_IT (定时器句柄, 通道);
5.5 启用xx功能但不启用其对应中断
HAL_TIM_xx_Start (定时器句柄, 通道);
5.6 清除标志位
__HAL_TIM_CLEAR_FLAG(定时器句柄, 标志位类型);
常见用于清除比较捕获的标志位:
__HAL_TIM_CLEAR_FLAG(&tim_ic_handle, TIM_FLAG_CC2);
6 基本定时器
TIM6/TIM7
6.1 特性
- 16位递增计数器(0~65535)
- 16位预分频器(1~65536)
- 可用于触发DAC
- 在更新事件(计数器溢出)时,会产生中断/DMA请求
- 并不与具体的IO绑定,只能用作普通的计时器(时基),就像Qt里面的QTimer
6.2 原理
- 16位递增计数器
- 时钟源:内部RCC时钟的TIMx_CLK
- 当CNT计数器的值等于重载影子寄存器的值时,发生溢出。溢出后可产生:①更新事件(默认产生);②中断和DMA输出(默认不产生)。
其具有一个影子寄存器的概念,即图上的影子,自动重装载寄存器和PSC寄存器都有其对应的影子寄存器。它实际上是真正起作用的寄存器,需要有更新事件后才会将其原生寄存器的值加载至影子寄存器中(除非将ARPE位设置为无缓冲,那就设置了原生就直接加载到ARR寄存器)。我个人觉得可以理解为:新的配置需要在原有定时器结束后才会生效。
影子寄存器开启缓冲的作用:假设需要LED灯先亮1秒再灭2秒,那要是等一秒后再让它灭,再修改ARR寄存器,那就会存在一定的误差。而如果开启ARR的缓冲,就可以在等待1秒期间就设置后面的2秒,等1秒完毕后它自己就自动配置了。
6.3 溢出时间计算方法
6.4 配置步骤
6.5 用到的函数
6.6 注意事项⚠️
- TIM句柄中需要关注的结构体成员:TIM_TypeDef、TIM_Base_InitTypeDef
- 对于基本定时器来说TIM_Base_InitTypeDef结构体中的计数模式CounterMode、时钟分频因子ClockDivision、重复计数器寄存器RepetitionCounter都是无效的
- 其中AutoReloadPreload就是是否启用预装载,即是否使用影子寄存器的意思,启用了就有一个对ARR寄存器的缓冲作用,不启用就一修改就直接改变其影子寄存器;
- 使用TIM时外设需要导入的除了stm32f1xx_hal_tim.c外,还有stm32f1xx_hal_tim_ex.c,否则会编译失败。
7 输出比较模式
PWM有八种模式
- TIM_OCMODE_TIMING:冻结,输出比较不起作用
- TIM_OCMODE_ACTIVE:当计数值为比较/捕获寄存器值相同时,强制输出为高电平
- TIM_OCMODE_INACTIVE:当计数值为比较/捕获寄存器值相同时,强制输出为低电平
- TIM_OCMODE_TOGGLE:当计数值与比较/捕获寄存器值相同时,翻转输出引脚的电平
- TIM_OCMODE_PWM1:向上计数时,当TIMx_CNT < TIMx_CCR*时,输出电平有效,否则为无效;向下计数时,当TIMx_CNT > TIMx_CCR*时,输出电平无效,否则为有效
- TIM_OCMODE_PWM2:与PWM1相反
- TIM_OCMODE_FORCED_ACTIVE:强制为有效电平
- TIM_OCMODE_FORCED_INACTIVE:强制为无效电平
7.1 PWM1/PWM2模式
- PWM1和PWM2,其实就是什么时候输出有/无效电平的差别;
- PWM的占空比由CCRx决定。
7.2 翻转模式
- 关于周期
- 从上图可看出一个周期计数为2*(ARR+1)次,而计数一次的时间为(PSC+1)/Ft,因此周期为Ft/(2×(ARR+1)×(PSC+1) ),也就是说PWM的周期是溢出周期的二分之一。
- 关于占空比
- 占空比是50%,只要是反转模式就是50%
- 关于相位
- 相位由CCR决定
8 通用定时器
TIM2/TIM3/TIM4/TIM5
8.1 特性
- 16位递增、递减、中心对齐计数器(0~65535)
- 16位预分频器(1~65536)
- 可用于触发DAC和ADC
- 在更新事件、触发事件、输入捕获、输出比较时,会产生中断/DMA请求
- 4个独立通道,可用于:输入捕获、输出比较、输出PWM、单脉冲模式
- 使用外部信号控制定时器且可实现多个定时器互连的同步电路
- 支持编码器和霍尔传感器电路等
8.2 原理
16位递增计数器
- 时钟源(4个):
(1) 内部时钟:RCC时钟的TIMx_CLK(APB)
(2) 外部时钟模式1:TIx(通道1和通道2的引脚信号)【注意⚠️!外部时钟模式1仅CH1和CH2可用】
(3) 外部时钟模式2:外部IO口复用触发输入TIMx_ETR,每个定时器只有一个ETR引脚功能
(4) 内部触发输入ITRx:与内部或外部其他定时器进行级联
内部时钟模式意味着使用内部时钟频率做计数,就像基本定时器一样;外部时钟模式即使用外部的上升沿、下降沿信号来触发计数器的改变
- 滤波器原理:
要采样到N次相同的信号才输出一个信号
8.3 配置PWM输出模式
- 时钟来源:内部时钟;
- PWM的占空比由CCRx决定。
- 其中使能通道预装载即启用CCR影子寄存器的缓冲功能,与ARR的预装载功能类似。
8.4 配置PWM输出模式相关函数
- 需要关注的结构体:TIM_OC_InitTypeDef
- 仅需要关注前三个结构体成员;
- OCPolarity是用来定义有效电平是高电平还是低电平的;
- 注意⚠️:赋值OCPolarity的时候千万别写成OCNPolarity
8.5 输入捕获
- 作用:
- 测量脉宽
- 原理:
- 四个通道皆可用于输入捕获
- 时钟来源:内部时钟
- 在检测到上升沿和下降沿时读到捕获寄存器的值,通过两次读取间捕获寄存器的差值(实际上是计数器的差值)以及记一次数所需时间,就可以知道脉宽。
- 计一个数所花时间:(PSC+1) / Ft 【(分频系数+1)/定时器时钟源频率】
8.6 输入捕获配置步骤
- 需要关注的结构体:TIM_IC_InitTypeDef
分频系数:就是设置多少个上升沿/下降沿才产生一次计数;
- 重点:与基本定时器及PWM输出不同,输入捕获并不是很在意计数器溢出的时间(6.3),其更关注计数频率,计数频率越高得到的脉宽精度才会越高,6.3这条式子就不是用来倒着用来求移出时间了,而是:
- 先决定计数频率,再决定溢出条件ARR。其中我觉得ARR可以设置为最大值65535,因为其越大,计算脉宽是需要存储的溢出次数就越小;
- 如果要捕获脉宽,就需要在捕获回调里面写切换的代码,在捕获到上升沿后,将捕获模式改成下降沿检测。
- 注意⚠️!修改配置前为了保险可先关闭定时器,修改完毕后再打开
- 注意⚠️!修改配置时必须先清除上一配置!
- 获取捕获值的函数为:HAL_TIM_ReadCapturedValue
8.7 脉冲计数
- 原理:
- 时钟来源:外部时钟模式1,即通道1或通道2的输入。或TIMx_ETR引脚的输入,每个定时器只有一个ETR引脚功能;
- 特性(外部1):来源于通道1或通道2的输入,有经边缘检测和不经边缘检测两种可选的信号作为时钟源,分别为TIxFPy和TI1F_ED。若为不经检测,则上升沿和下降沿都会分别触发一次计数,若经,就只会触发一个。
- 特性(外部2):一定会经过边缘检测器,所以无需考虑上面的情况。
8.8 脉冲计数配置步骤
- 需要关注的函数和结构体
- HAL_TIM_SlaveConfigSynchro函数,用于配置从模式控制器,使得计数器使用外部时钟;
- TIM_SlaveConfigTypeDef结构体,用于配置从模式
- 从模式选择:用于选择外部时钟模式1;
- 输入触发源选择:可选择TIxFP1(单边)、TIxFP2(单边)、TI1F_ED(双边);
- 输入触发极性:上升、下降或者双边沿;
- 预分频:外1模式没有,外2模式才有;
- 滤波器:选择滤波;
9 高级定时器
TIM1/TIM8
9.1 特性
- 16位递增、递减、中心对齐计数器(0~65535)
- 16位预分频器(1~65536)
- 可用于触发DAC和ADC
- 在更新事件、触发事件、输入捕获、输出比较时,会产生中断/DMA请求
- 4个独立通道,可用于:输入捕获、输出比较、输出PWM、单脉冲模式
- 使用外部信号控制定时器且可实现多个定时器互连的同步电路
- 支持编码器和霍尔传感器电路等
- 重复计数器(比通用定时器多的功能)
- 死区时间带可编程的互补输出,但仅通道1到通道3有互补输出通道,通道4没有(比通用定时器多的功能)
- 断路(刹车)输入,用于将定时器的输出信号置于用户可选的安全配置中(比通用定时器多的功能)
- 使用高级定时器的输出功能时,必须将TIMx_BDTR寄存器的MOE位置1,否则无法输出
9.2 重复计数器
高级定时器与普通定时器不同,其可设置为单纯产生溢出时并不产生定时器更新事件,而是几次溢出后才产生一次更新事件。具体的更新事件发出条件由TIMx的RCR寄存器进行设置。如果设置RCR为N,则更新事件将在N+1次溢出时发生。
9.3 输出指定个数的PWM
- 原理:
- 因为在边沿对齐模式下,定时器溢出周期对应着PWM周期,我们只要在更新事件发生时,停止输出PWM就行。
- 关键结构体
- 需要注意的点
- 高级定时器需要对其句柄.Init.RepetitinCounter(即重复计数器的初始值)进行设定
9.4 输出比较模式配置步骤(输出翻转模式实验)
- 需要关注的结构体
- 依然是只关注前三个就足够了
9.5 带死区控制的互补输出
- 概念
- 死区
- 在死区中,输出通道和互补输出通道的电平都是无效电平;
- 死区控制的存在是为了解决元器件传输电平过程中造成的延时。以电机控制为例,就是使用互补输出使得下图交叉导通分别实现正传和反转,但是正反转切换过程中由于元器件的延时特性,有可能会造成在某一时刻双边的三极管同时导通导致VCC直接连接到了GND,因此需要人为在电机切换正反转方向时创造一个死区,使得电机是先关闭再切换方向,从而避免了短路烧毁。
- 注意⚠️
- 任何时候输出和互补输出都不能同时处于有效电平,这个硬件上就决定了,若软件上同时输出有效电平,则都会变成无效电平;
- 死区
- 死区时间计算
9.6 带死区控制的互补输出配置步骤
- 关键结构体1
- 这次除了OCFastMode以外全都用上了;
- 其中最后两个成员就是上文所说的刹车完毕后空闲状态的电平设置;
- 注意⚠️:上述最后两个不能都是其自身的有效电平,否则会被硬件强制赋值为两个无效电平,就像上文说的一样:“任何时候输出和互补输出都不能同时处于有效电平,这个硬件上就决定了,若软件上同时输出有效电平,则都会变成无效电平”
- 关键结构体2
- 寄存器锁定:一般用不到
- 刹车输入极性:指定高电平是刹车还是低电平是刹车
- 自动恢复输出使能:允许刹车结束后自动恢复输出
- 注意⚠️!死区时间DeadTime是DTG寄存器的值,并不是真正的死区时间,真正的死区时间需要通过死区时间计算公式算出来。
- 值得注意的是,按照例程跑出来的死区时间是在输出信号的下降沿后上升沿前的,如下图,要考虑一下如果要实现上升沿后下降沿前,可能就不是这样了。按我的理解来说,是将输出和互补输出的极性都设置为低电平有效应该就可以实现了。但是也不是那么确定,毕竟没有示波器做实验。⚠️⚠️⚠️因此,后续要使用到死区时间的时候必须弄清楚这个问题才行!!!
9.7 刹车(断路)功能
- 刹车发生后:
- 关于刹车互补输出的设置可以看STM32F1XX参考手册的13.4.9,重点是表75,其中关于空闲状态的描述挺有意思,就是刹车后会进入空闲状态,而空闲状态的输出和互补输出电平由OISx和OISxN寄存器来决定。
9.8 PWM输入模式
- 作用:测量输入PWM波的参数;
- 测量精度:由计数器工作频率决定,要是计数器选择内部时钟源,而APB对计数器时钟又没有分频,那么测量精度就是1/72000000s(计一次数的时间数值是这样,但是单纯这样表示精度我不知道对不对,我瞎写的)
- 原理:记录PWM信号的上升沿、下降沿、下一个上升沿的对应计数值,再用它们和计一次数的时间相乘,即可计算其占空比和周期了;
- 例程中选用的方法:
- 将IC1和IC2同时映射到TIMx_CH1上;
- IC1使用上升沿触发,IC2使用下降沿触发;
- 从模式控制器设置为复位模式,利用TI1FP1的上升沿信号触发。当从模式控制器为复位模式时,TI1FP1的信号会使计数器CNT重新初始化为0(递增计数)或者ARR(递减计数,一般用递增,因为好算);
- 注意⚠️:
- 只有通道1和通道2可以用于测量,因为通道3和通道4没有响应的TI1FP1信号用于触发CNT复位;
- 配置方法:
- 关键结构体
- 仅需使用前两个成员;
- 从模式选择:选复位模式
- 触发源:TI1FP1或TI1FP2
- 触发极性:就是用于配置TI1FP1或TI1FP2产生前的上升沿、下降沿检测器
- 剩下的两个用不到;
- 注意⚠️:
- 需要使用定时器的捕获比较中断:TIMx_CC_IRQHandler
- TIM_IC_InitTypeDef中的ICSelection,意思为选择ICx的映射关系,如IC1映射到TI1上,就选TIM_ICSELECTION_DIRECTTI,如IC2也映射到TI1上,就选TIM_ICSELECTION_INDIRECTTI,索引到其注释可以看到下图所述的内容,即映射关系,根据映射关系选就行了,也是一个分组选取的东西。
- 在中断处理回调函数中,可通过htim->Channel来判断中断处理函数响应的通道,来区分TI1和TI2,如下图:
- 获取捕获值:HAL_TIM_ReadCapturedValue
10 公共注意事项
10.1 BASE_Init和功能_Init之间的MsInit矛盾
当HAL_TIM_PWM_Init和HAL_TIM_Base_Init函数先后调用时,后调用的函数的MsInit函数将不被执行,因为外设Init函数中有对外设进行状态判断的代码,只要执行过一次Init,那么其State就不是Reset状态的了,因此就不会执行另一个Init函数的MsInit函数。
注意⚠️:
对同一个外设执行多次不同层级的Init函数的话,只能在第一个Init函数的MsInit函数中写相关的初始化函数。
10.2 HAL_TIM_IC_Start和HAL_TIM_IC_Start_IT的区别
HAL_TIM_IC_Start是只启动输入捕获,不使能其捕获中断;而HAL_TIM_IC_Start_IT是在启动输入捕获的同时也启动输入捕获的中断,相当于在HAL_TIM_IC_Start后同时调用__HAL_TIM_ENABLE_IT(htim, TIM_IT_CCx)而已。
如果是使用HAL_TIM_IC_Start_IT,那就必须定义其中断处理函数以及回调处理函数了。不然中断产生后,程序会一直中断,无法执行下去,这俩任意少写一个都会这样。
10.3 while等待状态变化和中断回调不要同时使用
标题说可能说不太清楚,这里举一个例子:
假设需要输入捕获一个上升沿,如果你开启了输入捕获的中断,并编写了中断服务函数和输入捕获回调函数(哪怕没有内容,当然公共中断处理函数还是要调用的),而同时你又有一个while循环用来计算输入捕获的产生时间,那么这个while就是不准的了。如下图这样的while,在启用了IC中断后这个函数返回的就不是TPAD_TIMX_CAP_CHY_CCRX值了,而是中间那个CNT。
- 原因分析:
在中断产生后,中断服务中的外设公共中断服务函数会将中断的标志位清除,从而使得while循环多次直至满足超时条件。
- 更严重的后果:
若在所述的while循环体中,加入了一点耗时的函数,哪怕就是单纯的读取CNT,就有可能造成连超时条件都是难以达成。因为稍微耗时一点点的函数都有可能导致CNT溢出,从而重新计数,极大可能最终导致一直卡在循环中,一直无法满足超时条件而退出。
因此,假设真的需要添加耗时函数在其中,最好是使能一下TIM的Update事件,从而记录下溢出的次数,再从而记录下真实的累积CNT来进行超时判断。
10.4 上升沿和下降沿的监测和切换
- 监测:
- 方法一:使用标志位,自己有变量存储上升沿和下降沿的检测到与否的状态,那就知道现在来的是上升沿还是下降沿;
- 方法二:在回调处理函数中,检测GPIO的值,要是为高电平,那肯定就是上升沿;要是为低电平,那就肯定为下降沿;
- 切换:
- 使用TIM_RESET_CAPTUREPOLARITY()函数先清除,再使用TIM_SET_CAPTUREPOLARITY()函数重新设定为另一个边沿检测;