STM32 定时器(一 基本定时器)
大家好,我是杰哥
定时器东西是真的多,也不知道会出多少期,先从最简单的基本定时器
开始写起,由浅入深。
欢迎大家关注我的gitee仓库:
gitee源码仓库链接跳转
TIM简介
顾名思义,定时器就是用来定时的机器
stm32F1中有高级定时器2个 通用定时器4个 基本定时器2个
每个定时器有 8 个外部 IO。
定时器 | 位数 | 计数器类型 | DMA | 捕获/比较通道 | 互补输出 | 补充 | |
---|---|---|---|---|---|---|---|
高级定时器 | TIM1、8 | 16位 | 向上、向下 | 有 | 4 | 有 | 主要用于电机控制 |
通用定时器 | TIM2、3、4、5 | 16位 | 向上、向下 | 有 | 4 | 没有 | 捕获,比较,计时,PWM等 |
基本定时器 | TIM6、7 | 16位 | 向上 | 有 | 0 | 没有 | 主要用于驱动DAC |
高级定时器、通用定时器、基本定时器区别
先说说三者区别:
通用定时器:
通用,16位自动重装载计数器、满足大部分需求
基本定时器:
是一个 16 位的只能向上计数的定时器,不能重载,只能定时,没有外部IO
是一个累加计数器,没有16位自动重装载计数器,可以直接驱动DAC
高级定时器:
用于电机控制较多,有死区、刹车、霍尔传感器等,骚操作一堆
基本定时器
简介
手册中的简介:
基本定时器TIM6和TIM7各包含一个16位自动装载计数器,由各自的可编程预分频器驱动。
它们可以作为通用定时器提供时间基准,特别地可以为数模转换器(DAC)提供时钟。实际上,它们在芯片内部直接连接到DAC并通过触发输出直接驱动DAC。
这2个定时器是互相独立的,不共享任何资源。
手册中的主要功能:
16位自动重装载累加计数器
16位可编程(可实时修改)预分频器,用于对输入的时钟按系数为1~65536之间的任意数值分频
触发DAC的同步电路
在更新事件(计数器溢出)时产生中断/DMA请求
看框图
白话文从头开始讲:
主要看1 2 3 4…几个点的说法,然后在每个点下面可能会有官方的定义或解释进行补充:
- 计数器的时钟TIMxCLK来由系统时钟RCC的内部时钟(CK_INT)提供
定时器时钟TIMxCLK,即内部时钟CK_INT,经APB1预分频器后分频提供,如果APB1预分频系数等于1,则频率不变,否则频率乘以2,库函数中APB1预分频的系数是2,即PCLK1=36M,所以定时器时钟TIMxCLK=36*2=72M。
- 经由控制器输出ck_psc定时器预分频前的时钟
控制器可以预设启动复位、使能、计数等计数器模式功能
- 到PSC预分频器通过写入的16bit的PSC寄存器进行分频,得到并输出计数器时钟频率CK_CNT到CNT计数器
PSC 是一个16 位的预分频器,可以对定时器时钟 TIMxCLK 进行 1~65536 之间的任何一个数进行分频,算方式为:CK_CNT=TIMxCLK/(PSC+1)
- 为什么要这么搞PSC和RCC中APB1中的的预分频系数,因为能改变定时器中的时钟频率,直接决定了定时器能定时的一个时间精度。
- 计数器CNT是哥16位的计数器,只能往上计数,所以最大计数值为65535。从这能看出因为计数器有最大值,所以前面的预分频也算间接改变了能定时的最大时间上限,有失就有得。
- 自动重装载寄存器ARR也是一个16位的寄存器,这里面装着计数器能计数的最大数值,计数达到重装载值以后就会产生计数器溢出事件,并清零从头开始计数。
要让事件变成中断,记得使能中断喔。
- 计数器溢出事件也可以通过软件手动设置TIMx_EGR寄存器的UG位,主动去触发事件,如果使能了中断的话,其实就是主动触发中断。
定时时间计算
定时器的定时时间等于计数器的中断周期乘以中断的次数。
经过预分频PSC后得到计数时钟CK_CNT:
C
k
C
n
t
=
T
I
M
x
C
L
K
(
P
S
C
+
1
)
CkCnt = {TIMxCLK \over (PSC+1)}
CkCnt=(PSC+1)TIMxCLK
每计数一次需要的时间为CK_CNT的倒数:
1
C
k
C
n
t
{1 \over CkCnt}
CkCnt1
所以产生一次中断的时间:
1
C
k
C
n
t
∗
A
R
R
=
(
P
S
C
+
1
)
T
I
M
x
C
L
K
∗
A
R
R
{1 \over CkCnt *ARR} = {(PSC+1) \over TIMxCLK}*ARR
CkCnt∗ARR1=TIMxCLK(PSC+1)∗ARR
如果在中断服务函数里面设置一个变量count,用来记录中断的次数,那么就可以计算出我们需要的定时时间等于:
(
P
S
C
+
1
)
T
I
M
x
C
L
K
∗
A
R
R
∗
c
o
u
n
t
{(PSC+1) \over TIMxCLK}*ARR*count
TIMxCLK(PSC+1)∗ARR∗count
开发
Hal库定时器初始化结构体
typedef struct
{
uint32_t Prescaler; //预分频系数 PSC
uint32_t CounterMode; //计数模式,基本定时器只能向上递增计数
uint32_t Period; //定时器周期 ARR
uint32_t ClockDivision; //外部输入时钟分频,基本定时器用不到
uint32_t RepetitionCounter; //指定重复计数器值,基本定时器用不到
uint32_t AutoReloadPreload; //重复计数,也用不到
} TIM_Base_InitTypeDef;
对于基本定时器只需设置其中TIM_Prescaler、TIM_Period就可以。
- Prescaler:定时器预分频器设置,时钟源经该预分频器才是定时器时钟,它设定TIMx_PSC 寄存器的值。可设置范围为 0 至 65535,实现 1 至 65536 分频。
- Period:定时器周期,实际就是设定自动重载寄存器的值,在事件生成时更新到影子寄存器。可设置范围为 0 至 65535。
寄存器表
有点多 要不算了 不看了。。。看寄存器图
就行了。
前面框图的白话文那已经介绍了几个关键寄存器,
但是没详细讲解寄存器的不同使用、功效、作用。
所以剩下的想深入学习直接去看源码,注释写的非常健全,
(虽然hal已经很健全了但还是有点小bug 真烦!)
既能学习hal库的状态机机制又能锻炼位操作还能锻炼英语。
完美!!!
CubeMx配置
系统主频是72Mhz
这个时候我们的分频系数配置为7199(即7200),也就相当于告诉CPU:
将原来7200个频率脉冲看成一个频率脉冲。
这样的话,分频后定时器6的时钟频率为 72000000/7200=10000hz,
也就是它每1/10000秒会来一个脉冲。
然后我们将定时器周期设置为9999(即10000),
定时器6就会检测到每过10000个脉冲的话就会触发定时器中断。
结合上面定时器每1/10000秒会来一个脉冲就可以得到:
每过10000*(1/10000)=1s就会触发一次定时器中断。
以1s为例:1s = (7199+1)/72000000*(9999+1)
记得把中断也打开喔。
实例
下面所有的代码都来自于hal库
并且被我删减掉非核心部分进行讲解
Hal库定时器句柄结构体
typedef struct
{
TIM_TypeDef *Instance; /*!< Register base address */
TIM_Base_InitTypeDef Init; /*!< TIM Time Base required parameters */
HAL_TIM_ActiveChannel Channel; /*!< Active channel */
DMA_HandleTypeDef *hdma[7]; /*!< DMA Handlers array
This array is accessed by a @ref DMA_Handle_index */
HAL_LockTypeDef Lock; /*!< Locking object */
__IO HAL_TIM_StateTypeDef State; /*!< TIM operation state */
__IO HAL_TIM_ChannelStateTypeDef ChannelState[4]; /*!< TIM channel operation state */
__IO HAL_TIM_ChannelStateTypeDef ChannelNState[4]; /*!< TIM complementary channel operation state */
__IO HAL_TIM_DMABurstStateTypeDef DMABurstState; /*!< DMA burst operation state */
} TIM_HandleTypeDef;
HAL库基本用句柄htim6去做上层的操作封装,
可以看到句柄结构体中还有各种Lock和State用于状态机
库函数比较简单了,直接对TIM6寄存器地址去操作
TIM_TimeBaseInit(TIM6,&TIM_TimeBaseInitStruct);
初始化
void MX_TIM6_Init(void)
{
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim6.Instance = TIM6;
htim6.Init.Prescaler = 7199;
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 9999;
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
}
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
{
if(tim_baseHandle->Instance==TIM6)
{//只有一个中断,优先级默认就行了
/* TIM6 clock enable */
__HAL_RCC_TIM6_CLK_ENABLE();
/* TIM6 interrupt Init */
HAL_NVIC_SetPriority(TIM6_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM6_IRQn);
}
}
中断服务函数
/**
* @brief Period elapsed callback in non-blocking mode
* @param htim TIM handle
* @retval None
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim == &htim6)
{
//前面已经说了我们1秒中断一次,所以可以做一个翻转点灯的示例程序
}
}
主程序
放在所有外设初始化结束和主循环之间
//开启定时器终端,如果未初始化成功,报错。
if(HAL_TIM_Base_Start_IT(&htim6) != HAL_OK){
Error_Handler();
}
编程要点
- 开定时器时钟
- 初始化时基初始化结构体;(只配置TIM_Prescaler、TIM_Period)
- 使能 TIMx中断;
- 打开定时器;
- 编写中断服务程序
总结
目前我了解到的STM32定时器能做下面这些功能
(捕获/比较
编码器
3路霍尔传感器 好像是
红外编码
红外解码
用PWM实现DAC
输入捕获PWM
高级定时器输出指定个数pwm
输出比较
互补输出带死区
OnePulse)
我希望能把这个系列出完吧,
如果出的完 顺带讲讲几个用定时器搞的电机控制算法 这是理想状态
这是第一期 从最简单的基本定时器开始讲
你们的点赞是对我肯定!!
(捕获/比较
编码器
3路霍尔传感器 好像是
红外编码
红外解码
用PWM实现DAC
输入捕获PWM
高级定时器输出指定个数pwm
输出比较
互补输出带死区
OnePulse)
我希望能把这个系列出完吧,
如果出的完 顺带讲讲几个用定时器搞的电机控制算法 这是理想状态
这是第一期 从最简单的基本定时器开始讲
你们的点赞是对我肯定!!