正点原子 发表于 2022-11-9 09:56:44

《STM32MP1 M4裸机HAL库开发指南》第二十三章 基本定时器实验

本帖最后由 正点原子 于 2022-11-9 09:56 编辑

1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614




第二十三章 基本定时器实验

定时器是单片机中非常重要的元件,定时器,顾名思义,其具有定时/计时功能,例如定时发送和接收数据,定时采集数据,程序延时,对外部时间计数和检测等等。人类最开始使用的计时工具是沙漏、水漏,随着社会的发展,钟表、电子计时产品已经无处不在。
STM32MP1有众多的定时器,其中包括2个基本定时器(TIM6和TIM7)、10个通用定时器(TIM2~TIM5,TIM12~ TIM17)、2个高级控制定时器(TIM1和TIM8)和5 个低功耗定时器(LPTIM1~LPTIM5)。这些定时器彼此完全独立,不共享任何资源。本章节我们来学习STM32MP1的基本定时器,并通过基本定时器中断来控制LED1的翻转。
      本章将分为如下几个小节:
      23.1、基本定时器简介;
      23.2、基本定时器中断应用;
      23.3、硬件设计;
      23.4、软件设计;
      23.5、编译和测试;

23.1 基本定时器简介
STM32MP157有两个基本定时器:TIM6和TIM7。其基本特征如下:
●16位自动重载递增计数器;
●16位可编程预分频器,用于对计数器时钟频率进行分频(可在运行时修改分频值),分频系数1~65535;
●可以用于触发DAC的同步电路;
●发生计数器上溢更新事件(UEV)时会生成中断/DMA 请求。
基本定时器没有通道。
23.1.1 基本定时器框图
下面我们来看看基本定时器的框图,通过框图可以了解基本定时器的工作过程,同时对之后的编程也会有一个清晰的思路。
图23.1.1. 1基本定时器框图
1. ①时钟源
定时器是STM32的一个外设,任何外设要工作的话,都需要时钟的驱动。基本定时器挂在APB1上,其时钟来源于PCLK1,但是基本定时器时钟不是直接由APB1提供,而是经过一个倍频器,当APB1DIV的分频数为1的时候,此倍频器倍频值为1,当APB1DIV的分频数大于1的时候,此倍频器倍频值始终为2。我们前面说过,APB1的时钟频率最大为104.5MHz,所以基本定时器的计数器的时钟频率最大为209MHz,结合图中,从RCC过来的时钟,最后给内部时钟 (CK_INT) 源提供的时钟频率最大为209MHz。
图23.1.1. 2定时器时钟
2. ②控制器
定时器控制器除了负责控制定时器复位、使能、计数等功能外还可以用于控制触发DAC转换的控制信号。相关寄存器我们后面会介绍。
3. ③计数器
定时器中有个计数器,计数器在固定频率下通过计数来达到计时,它是定时器的时基单元,包括计数器寄存器(TIMx_CNT,这里的x指的是6或者7,本章下同)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR) 。基本定时器的这三个寄存器都是16位,即可设置值为0~65535。
图中的预分频器PSC,它有一个输入时钟CK_PSC和一个输出时钟CK_CNT。输入时钟CK_PSC来源于控制器部分,实际上就是内部时钟(CK_INT),即2倍的APB1(209MHz)经过设置预分频器寄存器(TIMx_PSC)后可以得到不同频率的CK_CNT,CK_CNT的频率计算公式如下:
PSC就是写入预分频器寄存器(TIMx_PSC)的值,即预分频数值。
fCK_PSC是输入时钟CK_PSC频率,fCK_CNT是输出时钟CK_CNT频率(即计数器的时钟频率,最大为209MHz)。
计数器如果是向上计数,当计数值CNT和自动重载寄存器的值ARR相等时,计数器溢出,基本定时器的溢出时间计算方法:
另外提示:预分频器寄存器(TIMx_PSC)可以通过软件在定时器运行过程中修改它的数值,修改好后的新的预分频数值不会立马生效,而是在下一个更新事件时起作用。因为更新事件发生时,会把TIMx_PSC寄存器值更新到影子寄存器中,这才会起作用,如下图:
图23.1.1. 3预分频器分频值变化时的计数器时序图
箭头中预分频器控制寄存器(TIMx_PSC)的值还是0,在箭头处写入1(十六进制),表示将预分频值由1变为2,但是此时写入的这个值并不是并不是马上就改变,因为此时定时器时钟频率并没有改变。当更新事件发生的时候,可以发现预分频器控制寄存器(TIMx_PSC)的值变成1了,即定时器的分频系数变成2了,可以看到定时器时钟频率时序图间隔变大了。
●定时器上溢事件
基本定时器的计数器是一个递增的计数器,当控制寄存器(TIMx_CR1)的CEN位置被1时,就使能计数器了,每来一个CK_CNT脉冲,计数器的计数值CNT就会增加1。当TIMx_CNT的值与 TIMx_ARR的设定值相等时,TIMx_CNT就会自动清零并且生成事件(产生 DMA请求、产生中断信号或者触发 DAC 同步电路等),然后计数器再重新开始计数,自动重复以上计数过程。计数器TIMx_CNT递增至与TIMx_ARR值相等,我们把这件事称为定时器上溢事件。定时器下溢事件是当计数器从初始值ARR(TIMx_ARR的值)往下递减为0时发货是能溢出,从而产生计数器溢出事件,不过基本定时器没有向下递减计数的功能,后面我们介绍的通用定时器和高级定时器才有。
●更新事件(UEV)
我们再来说明一下更新事件(UEV),更新事件是由TIMx_CR1 寄存器的UDIS位来决定是否使能/禁止 更新事件生成,如果UDIS位为0,表示使能更新事件,那么,当计数器溢出的时候产生一次更新事件,或者可以通过往事件寄存器TMx_EGR中的UG位软件写入1来产生更新事件。当更新事件产生的时候,我们读写操作的那个寄存器的值会载入影子寄存器中,从而达到所有影子寄存器的更新。
如果使能了中断,当产生更新事件时会发生中断,程序会进入中断服务函数中。
●影子寄存器
什么是影子寄存器?从框图上可以看到预分频器PSC和自动装载寄存器有一个灰色的阴影部分,表示预分频器寄存器(TIMx_PSC)和自动重载寄存器(TIMx_ARR)有个影子,这就表示这两个寄存器有影子寄存器。影子寄存器是一个实际存在的物理寄存器,但是我们是不能直接操作影子寄存器,而是通过操作影子寄存器对应的寄存器。
例如,我们能直接给预分频器寄存器(TIMx_PSC)写入分频系数,此时这寄存器实际上就是起到缓存数据的作用,只有等到发生更新事件时,预分频器寄存器(TIMx_PSC)的值才会自动写入其对应的影子寄存器中(我们称此过程为同步数据),这时设置的数据才会起作用。也就是说,我们操作的寄存器只是用于我们读写用,而影子寄存器才是真正起作用的寄存器。
上面是以预分频器寄存器(TIMx_PSC)为例子说的,那么对于自动重载寄存器(TIMx_ARR)和其对应的影子寄存器又是怎样的呢?这里涉及到TIMx_CR1寄存器,TIMx_CR1寄存器中的APRE位的设置对TIMx_ARR寄存器以及其影子寄存器的值是否同步有影响:
当ARPE位为0时,TIMx_ARR不进行缓冲,即TIMx_ARR和影子寄存器是连通的(两者之间无缓存),同步更新数据。如下图,往TIMx_ARR寄存器写入值36,TIMx_ARR的值马上发生变化,不会再等到来了更新事件时才发生变化。
图23.1.1. 4 ARPE位为0时计数器时序图
当ARPE为1时,TIMx_ARR进行缓冲,即TIMx_ARR和影子寄存器两者之间存在缓存机制,只有在每次产生更新事件(UEV)时,TIMx_ARR的值才会被传到影子寄存器中起作用。如下图,原来TIMx_ARR寄存器的值为F5,此刻往TIMx_ARR中写入36,但是写入的这个值并不会马上发生变化,直到等到发生更新事件以后,TIMx_ARR的值才真正变成36。
图23.1.1. 5 ARPE位为1时计数器时序图
●根据计数器计数频率计算上溢周期
由上述可知,我们只要设置预分频寄存器(TIMx_PSC)和自动重载寄存器(TIMx_ARR)的值就可以控制定时器上溢事件发生的时间。自动重载寄存器(TIMx_ARR)是用于存放一个与计数器的计数值CNT作比较的值,当计数器递增的数值增加到与自动重载寄存器(TIMx_ARR)相等时就会生成更新事件(也可以说是上溢事件),硬件自动置位相关事件的标志位,如中断标志位,此标志位需要通过软件清零。
下面举个例子来学习如何设置预分频寄存器和自动重载寄存器的值来得到我们想要的定时器上溢事件发生的时间周期。比如我们需要一个500ms周期的定时器中断,一般思路是先设置预分频寄存器(TIMx_PSC),然后才是自动重载寄存器(TIMx_ARR)。考虑到我们设置的CK_INT为209MHz,我们把预分频系数设置为20900,即写入预分频寄存器的值为20899,那么:
      这样就得到计数器的计数频率为10KHZ,即计数器1秒钟可以计10000个数。我们需要500ms的中断周期,所以就得让计数器计数5000个数才可以产生上溢事件(中断),那么直接设置自动重载寄存器的值为4999就可以了。
23.1.2 TIM6/TIM7寄存器
下面介绍TIM6/TIM7的几个重要的寄存器,具体如下:
1. 控制寄存器 1(TIMx_CR1)
TIM6/TIM7的控制寄存器1描述如图23.1.2.1所示:
图23.1.2. 1 TIMx_CR1寄存器
      对于TIMx_CR1寄存器,我们重点介绍如下几位:
位0(CEN)用于使能或者禁止计数器,该位置1计数器开始工作,置0则停止。
位1(UDIS)是禁止更新位,该位置1时,定时器溢出后并不会置位UIF位,即不会产生中断;该位置0表示定时器溢出后会把TIMx_SR寄存器的UIF置1,表示未发生更新。
位2(URS)表示更新请求源,该位置1时,只有在计数器溢出的情况下才会产生中断;该位置0时,计数器上溢/下溢、设置UG位为1、通过从模式控制器产生的更新都会产生中断。
位3(OPM)可以设置定时器是工作一次还是重复工作。当OPM=0时,计数器在发生更新事件时不会停止计数;当OPM=1时,计数器在发生下一更新事件时停止计数(将 CEN 位清零),即定时器停止计数。
位7(APRE)是自动重装载预装载使能,当ARPE=0时,TIMx_ARR寄存器没有缓冲,那么修改自动重载寄存器的值马上有效(即不进行缓冲);当ARPE=1时,TIMx_ARR寄存器具有缓冲,即只有在事件更新时才把TIMx_ARR值赋给影子寄存器。
2. DMA/中断使能寄存器(TIMx_DIER)
TIM6/TIM7的DMA/中断使能寄存器描述如下图所示:
图23.1.2. 2 TIMx_DIER寄存器
位0(UIE)用于使能或者禁止更新中断,因为本实验我们用到中断,所以该位需要置1。      位8(UDE)用于使能或者禁止更新DMA请求,本章节实验我们暂且用不到。
3. 状态寄存器(TIMx_SR)
TIM6/TIM7的状态寄存器描述如下图所示:
图23.1.2. 3 TIMx_SR寄存器
该寄存器位0(UIF)是中断更新的标志位,当发生中断时由硬件置1,当执行到中断服务函数的时候,要在中断服务函数里把此位清零,如果中断到来后,不清零该位,那么系统就会一直进入中断服务函数而无法进入主函数,这个不是我们想要的,关于这点我们在前面的外部中断实验中有强调过。在STM32CubeIDE上生成的中断服务函数中已经有清除中断标志位的操作了,如果是自己写中断服务函数的话,一定要记得清除中断标志位。
4. 计数器寄存器(TIMx_CNT)
TIM6/TIM7的计数器寄存器描述如下图所示:
图23.1.2. 4 TIMx_CNT寄存器
该寄存器位就是计数器的实时的递增的值。
5. 预分频寄存器(TIMx_PSC)
TIM6/TIM7的预分频寄存器描述如下图所示:
图23.1.2. 5 TIMx_PSC寄存器
该寄存器是TIM6/TIM7的预分频寄存器,比如我们要20900分频,就往该寄存器写入20899。注意这是16位的寄存器,写入的数值范围是0到65535之间。
6. 自动重载寄存器(TIMx_ARR)
TIM6/TIM7的自动重载寄存器描述如图23.1.2.6所示:
图23.1.2. 6 TIMx_ARR寄存器
该寄存器是用于存放与计数器寄存器比较的值,当两者相等就会产生更新事件,我们在前面对基本定时器框图进行分析的时候有讲解过。如果TIMx_ARR的数值为0,则定时器会停止工作。TIMx_CR1寄存器的第7位ARPE控制TIMx_ARR是否带缓冲,当ARPE=0时,TIMx_ARR寄存器不带缓冲功能,当改变TIMx_ARR寄存器的值时,立马就更新定时器的自动重装载寄存器的值了。

7. 事件产生寄存器 (TIMx_EGR)
图23.1.2. 7 TIMx_EGR寄存器
      位0(UG)是产生更新事件,UG位由软件置1,并由硬件自动清0。
      将UG位清零0时无作用;
      将UG位置1时,若TIMx_CR1寄存器的UDIS位 = 0,则会重新初始化定时器的计数器并产生寄存器更新事件,注意,此时预分频器也被清0,(但预分频系数不变)。
23.1.3 HAL库驱动
      定时器的HAL库驱动在stm32mp1xx_hal_tim.c和stm32mp1xx_hal_tim.h文件中。我们先分析定时器相关的结构体和句柄,再分析API函数,对结构体成员以及以及句柄成员赋值,可以初始化外设,API函数就是通过这些结构体和句柄来初始化外设的。
1. 结构体和句柄
(1)TIM_Base_InitTypeDef
typedef struct
{
uint32_t Prescaler;                                 /* 预分频系数 */
uint32_t CounterMode;                                    /* 计数模式 */
uint32_t Period;                                          /* 自动重载值ARR */
uint32_t ClockDivision;                              /* 时钟分频因子 */   
uint32_t RepetitionCounter;                        /* 重复计数器 */
uint32_t AutoReloadPreload;                        /* 自动重载预装载使能 */
} TIM_Base_InitTypeDef;
①Prescaler:预分频系数,即写入预分频寄存器的值,范围0到65535。
②CounterMode:计数器计数模式,这里基本定时器只能向上计数。
③Period:自动重载值,即写入自动重载寄存器(TIMx_ARR)的值,范围0到65535。
④ClockDivision:时钟分频因子,也就是定时器时钟频率CK_INT与数字滤波器所使用的采样时钟之间的分频比。
⑤RepetitionCounter:设置重复计数器寄存器的值,用在高级定时器中。
⑥AutoReloadPreload:自动重载预装载使能,即控制寄存器 1 (TIMx_CR1)的ARPE位。
(2)TIM_TypeDef
      TIM_TypeDef结构体在stm32mp157dxx_cm4.h文件中有定义,是有关于定时器的寄存器结构体封装,寄存器的偏移地址和封装,在前面的实验中我们也有多次介绍到,这里就不再进行讲解。
(4)HAL_LockTypeDef
typedef enum
{
HAL_UNLOCKED = 0x00U,                                       /* 未上锁 */
HAL_LOCKED   = 0x01U                                           /* 已上锁 */
} HAL_LockTypeDef;
      HAL_LockTypeDef就是一个枚举类型,宏定义HAL_UNLOCKED表示未上锁,HAL_LOCKED表示已上锁。在前面的串口实验中我们已经分析过这两个宏,可以查看第14.3.1小节。
(3)HAL_TIM_StateTypeDef
typedef enum
{
HAL_TIM_STATE_RESET      = 0x00U,                  /* 外围设备尚未初始化或禁用 */
HAL_TIM_STATE_READY      = 0x01U,                  /* 外围设备已初始化并可以使用*/
HAL_TIM_STATE_BUSY       = 0x02U,                  /* 内部流程正在进行中 */
HAL_TIM_STATE_TIMEOUT    = 0x03U,                  /* 超时状态 */
HAL_TIM_STATE_ERROR      = 0x04U                   /* 接收过程正在进行中 */
} HAL_TIM_StateTypeDef;
      枚举类型中定义了定时器的状态,如果定时器处于HAL_TIM_STATE_RESET状态,则可认为定时器未被初始化,HAL库中会执行HAL_UNLOCKED进行解锁(我们会在后面的API函数中看到)。
(4)TIM_HandleTypeDef
#if (USE_HAL_TIM_REGISTER_CALLBACKS == 1)
typedef struct __TIM_HandleTypeDef
#else
typedef struct
#endif /* USE_HAL_TIM_REGISTER_CALLBACKS */
{
TIM_TypeDef               *Instance;                  /* 外设寄存器基地址 */
TIM_Base_InitTypeDef      Init;                     /* 定时器初始化结构体 */
HAL_TIM_ActiveChannel       Channel;    /* 定时器通道,TIM6/TIM7没有通道 */
DMA_HandleTypeDef         *hdma;                   /* DMA管理结构体 */
HAL_LockTypeDef             Lock;                               /* 锁定资源 */
__IO HAL_TIM_StateTypeDef   State;                      /* 定时器状态 */

#if (USE_HAL_TIM_REGISTER_CALLBACKS == 1)
/****** 此处省略部分代码(定时器回调函数)******/            
#endif
} TIM_HandleTypeDef;
      前面分析了几个结构体,TIM_HandleTypeDef句柄就比较好理解了,后面HAL库的API函数会通过句柄来初始化定时器。
①Instance:指向定时器寄存器基地址。
②Init:定时器初始化结构体,用于配置定时器的相关参数。
③Channel:定时器的通道选择,基本定时器没有该功能。
④hdma:用于配置定时器的DMA请求。
⑤Lock:ADC锁资源。
⑥State:定时器工作状态。
2. HAL库中的API函数
      我们先单独列出使能/关闭定时器中断和使能/关闭定时器方法,如下是HAL库中的宏定义:
__HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);/* 使能句柄指定的定时器更新中断 */
__HAL_TIM_DISABLE_IT (htim, TIM_IT_UPDATE); /* 关闭句柄指定的定时器更新中断 */
__HAL_TIM_ENABLE(htim);                        /* 使能句柄htim指定的定时器 */
__HAL_TIM_DISABLE(htim);                     /* 关闭句柄htim指定的定时器 */
      HAL库中几个重要的API函数如下:
(1)HAL_TIM_Base_Init
●函数功能:初始化定时器
●函数参数:htim定时器句柄
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
1   HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim)
2   {
3   /* 检查TIM句柄分配 */
4   if (htim == NULL)
5   {
6       return HAL_ERROR;
7   }
8
9   /* 检查参数 */
10    assert_param(IS_TIM_INSTANCE(htim->Instance));
11    assert_param(IS_TIM_COUNTER_MODE(htim->Init.CounterMode));
12    assert_param(IS_TIM_CLOCKDIVISION_DIV(htim->Init.ClockDivision));
13    assert_param(IS_TIM_AUTORELOAD_PRELOAD(htim->Init.AutoReloadPreload));
14
15    if (htim->State == HAL_TIM_STATE_RESET)
16    {
17      /* 分配锁资源并对其进行初始化 */
18      htim->Lock = HAL_UNLOCKED;
19
20#if (USE_HAL_TIM_REGISTER_CALLBACKS == 1)
21      /* 将中断回调重置为旧的弱回调*/
22      TIM_ResetCallback(htim);
23
24      if (htim->Base_MspInitCallback == NULL)
25      {
26      htim->Base_MspInitCallback = HAL_TIM_Base_MspInit;
27      }
28      /* 初始化底层硬件:GPIO,CLOCK,NVIC */
29      htim->Base_MspInitCallback(htim);
30#else
31      /* 初始化底层硬件:GPIO,CLOCK,NVIC */
32      HAL_TIM_Base_MspInit(htim);
33#endif
34    }
35    /* 设置TIM状态为忙 */
36    htim->State = HAL_TIM_STATE_BUSY;
37    /* 设置时基配置 */
38    TIM_Base_SetConfig(htim->Instance, &htim->Init);
39    /* 设置TIM状态已经准备就绪 */
40    htim->State = HAL_TIM_STATE_READY;
41
42    return HAL_OK;
43}
      第15~18行,如果定时器的工作状态是HAL_TIM_STATE_RESET,则表示定时器未被初始化,HAL库中执行HAL_UNLOCKED将定时器进行解锁。
      第20~29行,如果有定义宏USE_HAL_TIM_REGISTER_CALLBACKS等于1,则将中断回调函数指定为弱定义的回调函数,20到29行的代码,可以用户用户自己定义回调函数,我们在前面窗口看门狗实验中有说过,见第15.1.3小节。这里呢,如果要自定义回调函数,USE_HAL_TIM_REGISTER_CALLBACKS这个宏就需要用户自己去定义了,在HAL库中是找不到定义的,包括stm32mp1xx_hal_conf.h文件中也是没有定义的。
      第32行,调用HAL_TIM_Base_MspInit函数来初始化底层硬件,如开启定时器时钟、设置定时器中断优先级以及开启定时器中断。HAL_TIM_Base_MspInit函数在HAL库中是一个弱函数,函数中没有什么实际内容,需要用户重新定义一个,在后面的实验中,STM32CubeIDE生成的工程中会自动重新定义该函数。
      第36~40行,先设置定时器状态为忙,再调用TIM_Base_SetConfig函数来配置寄存器,然后再设置定时器状态为已准备就绪。
      为什么要这样呢?如果还没配置好定时器,而此刻调用了HAL_TIM_Base_Start_DMA函数的话,可能 会发生一些错误,在HAL_TIM_Base_Start_DMA函数中会通过此宏来判断定时器是否已经就绪,如果定时器处于忙(被占用)的状态,就返回HAL_BUSY,退出HAL_TIM_Base_Start_DMA函数,就不会发生一些错误了。加定时器状态也就是为了判断此时定时器是否可以进行下一步操作。
图23.1.3. 1 HAL_TIM_Base_Start_DMA部分代码
(2)TIM_Base_SetConfig
●函数功能:配置定时器
●函数参数:
TIMx:定时器外设;Structure:时基配置结构定义
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
      HAL_TIM_Base_Init函数是通过调用TIM_Base_SetConfig函数来完成初始化定时器工作的,我们看看其代码。
1   void TIM_Base_SetConfig(TIM_TypeDef *TIMx, TIM_Base_InitTypeDef *Structure)
2   {
3   uint32_t tmpcr1;
4   tmpcr1 = TIMx->CR1;
5
6   /* 设置TIM时基单位参数 */
7   if (IS_TIM_COUNTER_MODE_SELECT_INSTANCE(TIMx))
8   {
9       /*选择计数器模式 */
10      tmpcr1 &= ~(TIM_CR1_DIR | TIM_CR1_CMS);
11      tmpcr1 |= Structure->CounterMode;
12    }
13
14   if (IS_TIM_CLOCK_DIVISION_INSTANCE(TIMx))
15    {
16      /* 设置时钟分频,,TIM6和TIM7没有此功能*/
17      tmpcr1 &= ~TIM_CR1_CKD;
18      tmpcr1 |= (uint32_t)Structure->ClockDivision;
19    }
20
21    /* 设置自动重新加载预加载 */
22    MODIFY_REG(tmpcr1, TIM_CR1_ARPE, Structure->AutoReloadPreload);
23
24    TIMx->CR1 = tmpcr1;
25
26    /* 设置自动重载值 */
27    TIMx->ARR = (uint32_t)Structure->Period ;
28
29    /* 设置预分频器值 */
30    TIMx->PSC = Structure->Prescaler;
31
32    if (IS_TIM_REPETITION_COUNTER_INSTANCE(TIMx))
33    {
34      /* 设置重复计数器值 */
35      TIMx->RCR = Structure->RepetitionCounter;
36    }
37
38    /* 生成更新事件以立即重新加载预分频器和重复计数器(仅适用于高级计时器)值 */
39    TIMx->EGR = TIM_EGR_UG;
40}
      第3第4行,定义一个变量tmpcr1,用于保存TIMx_CR1寄存器当前的值。
      第7行,IS_TIM_COUNTER_MODE_SELECT_INSTANCE是个宏定义,在stm32mp157axx_cm4.h头文件有定义,INSTANCE表示要选择的定时器(TIM1~TIM5和TIM8):
/* 选择计数器 */
#define IS_TIM_COUNTER_MODE_SELECT_INSTANCE(INSTANCE)\
   (((INSTANCE) == TIM1)    || \
   ((INSTANCE) == TIM2)    || \
   ((INSTANCE) == TIM3)    || \
   ((INSTANCE) == TIM4)    || \
   ((INSTANCE) == TIM5)    || \
   ((INSTANCE) == TIM8))
      第10和第11行,TIM_CR1_DIR和TIM_CR1_CMS在stm32mp157axx_cm4.h文件中有定义,分别表示((uint16_t)0x0010)和((uint16_t)0x0060),~(TIM_CR1_DIR | TIM_CR1_CMS)等于二进制的1000 1111,表示设置TIMx_CR1寄存器第0、1、2、3和7位为1,则表示开启计时器、关闭更新事件、计数器上溢/下溢产生中断、计数器在下一个更新事件(清除CEN位)时停止计数、TIMx_ARR寄存器使用缓冲,即设置计数器模式。
      第14~19行,分析方法类似,表示设置计时器分频比,在TIM6和TIM7没有此功能,所以不用设置。
      第22行,宏MODIFY_REG、WRITE_REG和READ_REG 在stm32mp1xx.h文件中有定义:
#define MODIFY_REG(REG, CLEARMASK, SETMASK)\
WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))   
#define READ_REG(REG)         ((REG))
#define WRITE_REG(REG, VAL)   ((REG) = (VAL))   /* 把VAL的值赋值给REG */
      将参数代入宏定义中,其实第22行就是表示设置tmpcr1的ARPE位是否使用缓冲,即TIMx_ARR寄存器不缓冲。
      第24行,将变化后的tmpcr1赋值给TIMx->CR1寄存器,从而实现配置寄存器。
      第27行,通过配置TIMx_ARR寄存器实现设置自动重载值;
      第30行,通过配置TIMx->PSC寄存器实现设置预分频器值;
      第32~36行,设置重复计数器值,TIM6和TIM7没有这个功能;
(3)HAL_TIM_Base_Start_IT      
●函数功能:使能定时器和更新定时器中断
●函数参数:htim定时器句柄
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
1   HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim)
2   {
3   uint32_t tmpsmcr;
4   /* 检查参数*/
5   assert_param(IS_TIM_INSTANCE(htim->Instance));
6   /* 启用TIM更新中断 */
7   __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);
8   /* 启用外围设备,但在触发模式下除外,在触发模式下使用触发自动启用 */
9   tmpsmcr = htim->Instance->SMCR & TIM_SMCR_SMS;
10    if (!IS_TIM_SLAVEMODE_TRIGGER_ENABLED(tmpsmcr))
11    {
12      __HAL_TIM_ENABLE(htim);                                 /* 使能定时器 */
13    }
14    /* 返回功能状态 */
15    return HAL_OK;
16}
      第7行,HAL_TIM_Base_Start_IT函数调用了宏__HAL_TIM_ENABLE_IT来更新定时器中断,其中参数TIM_IT_UPDATE的值为宏TIM_DIER_UIE,,而宏TIM_DIER_UIE在stm32mp157axx_cm4.h文件中有定义为 (uint16_t)0x0001。宏__HAL_TIM_ENABLE_IT定义如下:
#define __HAL_TIM_ENABLE_IT(__HANDLE__, __INTERRUPT__) \   ((__HANDLE__)->Instance->DIER |= (__INTERRUPT__))
      其中__HANDLE__表示要操作的句柄,__INTERRUPT__是操作TIMx_DIER寄存器的某一位,上面分析中,宏TIM_DIER_UIE为 (uint16_t)0x0001,也就是表示对TIMx_DIER寄存器的第0位置1,表示更新中断使能。
      第12行,调用宏__HAL_TIM_ENABLE来开启定时器,宏__HAL_TIM_ENABLE定义如下,其中TIM_CR1_CEN在stm32mp157axx_cm4.h文件中定义为(uint16_t)0x0001,则表示对TIMx_CR1寄存器的第0位置1,即使能定时器。
#define __HAL_TIM_ENABLE(__HANDLE__) \
((__HANDLE__)->Instance->CR1|=(TIM_CR1_CEN))
(4)HAL_TIM_Base_Start
      上面的HAL_TIM_Base_Start_IT      函数使用到了定时器更新中断,有时候并不是中断越多越好,CPU频繁地去响应中断会影响系统的性能,还会滋生出很多问题。如果不使用中断的话,可以使用HAL_TIM_Base_Start。
●函数功能:使能定时器
●函数参数:htim定时器句柄
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
1   HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim)
2   {
3   uint32_t tmpsmcr;
4   /* 检查参数 */
5   assert_param(IS_TIM_INSTANCE(htim->Instance));
6   /* 设置TIM状态忙 */
7   htim->State = HAL_TIM_STATE_BUSY;
8   /* 启用外围设备,但在触发模式下除外,在触发模式下使用触发自动启用 */
9   tmpsmcr = htim->Instance->SMCR & TIM_SMCR_SMS;
10    if (!IS_TIM_SLAVEMODE_TRIGGER_ENABLED(tmpsmcr))
11    {
12      __HAL_TIM_ENABLE(htim);                              /* 使能定时器 */
13    }
14    /* 改变TIM的状态为已就绪*/
15    htim->State = HAL_TIM_STATE_READY;
16    return HAL_OK;
17}
(5)HAL_TIM_Base_Stop_IT
●函数功能:关闭定时器和定时器中断
●函数参数:htim定时器句柄
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
1   HAL_StatusTypeDef HAL_TIM_Base_Stop_IT(TIM_HandleTypeDef *htim)
2   {
3   /* 检查参数 */
4   assert_param(IS_TIM_INSTANCE(htim->Instance));
5   /* 禁用TIM更新中断 */
6   __HAL_TIM_DISABLE_IT(htim, TIM_IT_UPDATE);
7   /* 禁用定时器设备*/
8   __HAL_TIM_DISABLE(htim);
9
10    return HAL_OK;
11}   
(6)HAL_TIM_Base_Stop
      上面的HAL_TIM_Base_Stop_IT除了关闭定时器还关闭了定时器中断,如果只是想 关闭定时器的话,可以直接使用HAL_TIM_Base_Stop函数。
●函数功能:关闭定时器
●函数参数:htim定时器句柄
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
1   HAL_StatusTypeDef HAL_TIM_Base_Stop(TIM_HandleTypeDef *htim)
2   {
3   /* 检查参数 */
4   assert_param(IS_TIM_INSTANCE(htim->Instance));
5   /* 设置TIM的状态 */
6   htim->State = HAL_TIM_STATE_BUSY;
7   /* 停止计数,也就是关闭TIM */
8   __HAL_TIM_DISABLE(htim);
9   /* 改变TIM的状态 */
10    htim->State = HAL_TIM_STATE_READY;
11    return HAL_OK;
12}
23.2 基本定时器中断应用
本章,我们主要使用定时器来做周期性的中断应用,可以利用定时器的溢出中断来实现该功能。实现过程如下图所示,ARR是TIMx_ARR寄存器的值,CNT是TIMx_CNT的值,PSC是TIMx_PSC的值。
图23.2.1通用定时器中断示意图
如图所示,CNT计数器从0开始计数,当CNT的值和ARR相等时(t1),产生一个溢出中断,然后CNT复位(清零),然后继续从0开始递增计数,如此循环。图中的t1、t2、t3就是定时器溢出中断产生的时刻。
我们通过修改ARR的值,可以改变定时时间。另外,通过修改PSC的值,可以使用不同的计数频率(图中CNT的斜率也就会改变),从而也可以改变定时的时间。
23.3 硬件设计
1. 例程功能
LED0用来指示程序运行,500ms为一个周期翻转。LED1用于定时器中断取反,指示定时器中断状态,1000ms为一个周期翻转。
2. 硬件资源
1)
图23.3. 1 LED资源
2)定时器6
3. 原理图
定时器属于STM32MP157的内部资源,只需要软件设置好即可正常工作。我们通过LED1来指示STM32MP157的定时器进入中断情况。
23.4 软件设计
      本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\ MP157-M4 HAL库V1.2\实验12 基本定时器实验。
23.4.1 程序设计
1. 程序设计流程
      基本定时器的配置流程:
1)使能基本定时器时钟,否则无法使用基本定时器;
2)初始化定时器:配置基本定时器的预分频系数和自动重装载寄存器值,设置计数方向为向上计数(基本定时器只有向上计数这个模式);
3)开启定基本定时器全局中断,通过配置NVIC来使能基本定时器中断,并配置基本定时器中断优先级;
4)编写定基本定时器中断回调函数;
5)更新定时器中断和使能定时器(这个不能忘)。
      程序设计过程会在我们后面的实验中有所体现,实验的流程图如下:
图23.4.1. 1基本定时器中断实验程序流程图
2. 新建工程
      在上一章实验工程的Drivers\BSP下新建TIMER文件夹,并在文件夹下新建btim.c、btim.h文件。本实验需要用到HAL库的stm32mp1xx_hal_tim.c、stm32mp1xx_hal_tim_ex.c文件,记得在工程中关联这两个文件。
图23.4.1.2 新建和关联文件
3. 添加用户代码
(1)btim.h文件
      btim.h文件代码如下,主要完成引脚定义、中断服务函数定义和定时器时钟使能定义:
#ifndef __BTIM_H
#define __BTIM_H

#include "./SYSTEM/sys/sys.h"

#define BTIM_TIMX_INT                  TIM6      /* 基本定时器6引脚定义 */
#define BTIM_TIMX_INT_IRQn            TIM6_IRQn/* 基本定时器6中断服务函数定义 */
#define BTIM_TIMX_INT_IRQHandler       TIM6_IRQHandler
/* 开启基本定时器6时钟 */
#define BTIM_TIMX_INT_CLK_ENABLE()   do{__HAL_RCC_TIM6_CLK_ENABLE();}while(0)
   
void btim_timx_int_init(uint16_t arr, uint16_t psc); /* 基本定时器6初始化 */

#endif
(2)btim.c文件
      btim.c文件代码如下:
1   #include "./BSP/LED/led.h"
2   #include "./BSP/TIMER/btim.h"
3
4   TIM_HandleTypeDef g_timx_handle;      /* 定时器x句柄 */
5   /**
6    * @brief      基本定时器TIMX定时中断初始化函数
7    * @note
8    *               基本定时器TIM6和TIM7均为16位计数器,时钟来自PCLK1(APB1),
9    *               当APB1DIV≥2分频的时候,基本定时器的时钟为APB1时钟的2倍,
10   *               而APB1为104.5M, 所以定时器时钟 = 209Mhz。
11   *               定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
12   *               Ft=定时器工作频率,单位:Mhz
13   *
14   * @param       arr: 自动重装值。
15   * @param       psc: 时钟预分频数
16   * @retval      无
17   */
18void btim_timx_int_init(uint16_t arr, uint16_t psc)
19{
20      BTIM_TIMX_INT_CLK_ENABLE();                           /* 使能TIMx时钟 */
21      
22      g_timx_handle.Instance = BTIM_TIMX_INT;            /* 基本定时器x */
23      g_timx_handle.Init.Prescaler = psc;                   /* 分频值为psc */
24      g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 向上计数器 */
25      g_timx_handle.Init.Period = arr;                        /* 自动装载值 */
26      g_timx_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;/* 时钟分频因子 */
27      HAL_TIM_Base_Init(&g_timx_handle);
28      /* 设置中断优先级,抢占优先级1,子优先级3 */
29      HAL_NVIC_SetPriority(BTIM_TIMX_INT_IRQn, 1, 3);
30      HAL_NVIC_EnableIRQ(BTIM_TIMX_INT_IRQn); /* 开启ITMx中断 */
31      
32      HAL_TIM_Base_Start_IT(&g_timx_handle);/* 使能定时器x和定时器x更新中断 */
33}
34/**
35   * @brief       定时器中断服务函数
36   * @param       无
37   * @retval      无
38   */
39void BTIM_TIMX_INT_IRQHandler(void)
40{
41      HAL_TIM_IRQHandler(&g_timx_handle);
42}
43/**
44   * @brief       定时器更新中断回调函数
45   * @param       htim:定时器句柄指针
46   * @retval      无
47   */
48void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
49{
50      if (htim == (&g_timx_handle))
51      {
52          LED1_TOGGLE();             /* LED0翻转 */
53      }
54}
      第18~33行,btim_timx_int_init函数主要用于初始化定时器,其中:
      第20行,使能定时器6时钟;
      第22~26行,配置定时器6的参数,包括:分频值为psc(分频值我们可以在调用函数的时候指定,是比较方便的)、计数模式为向上计数、自动装载值为arr(自动装载值也是可以自己设置的)、时钟分频因为为1,即不分频;
      第27行,根据以上的设置的参数初始化定时器6;
      第29和30行,设置基本定时器的抢占优先级为1,子优先级为3,并使能定时器;
      第32行,更新定时器中断和使能定时器;
      第39~42行,定时器中断服务函数BTIM_TIMX_INT_IRQHandler通过调用定时器中断请求函数HAL_TIM_IRQHandler来完成定时器中断功能;
      第48~54行,在定时器更新中断回调函数中实现LED1翻转,进入中断请求函数以后,会调用HAL_TIM_PeriodElapsedCallback函数。即每当计数器溢出时都调用此回调函数,此函数中实现LED1翻转,每进入一次中断,LED1就翻转一次,所以我们会看到LED1在闪烁。
(3)main.c文件代码
      main.c文件代码代码如下:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/KEY/key.h"
#include "./BSP/TIMER/btim.h"
/**
* @brief       主函数
* @param       无
* @retval      无
*/
int main(void)
{
    HAL_Init();   /* 初始化HAL库*/
    /* 初始化M4内核时钟,209M */
    if(IS_ENGINEERING_BOOT_MODE())
    {
      sys_stm32_clock_init(34, 2, 2, 17, 6826);
    }
    delay_init(209);      /* 延时初始化 */
    led_init();            /* 初始化LED*/
    /* 自动重载寄存器(TIMx_ARR)的值为10000-1,预分频器寄存器(TIMx_PSC)的值为20900-1 */
    btim_timx_int_init(10000 - 1, 20900 - 1);
   
    while(1)
    {
      LED0_TOGGLE();               /* LED0翻转 */
      delay_ms(500);                /* 延时500ms */
    }
}
      以上代码中,基本定时器的时钟频率是209MHz,以上参数预分频器分频值为20900-1,计算出计数器CK_CNT的时钟频率是
      那么计数器计数10000次就会溢出产生中断,所以每次溢出时间是:
      最后,通过delay_ms(500)实现LED0每500ms翻转一次。所以实验后,可以发现LED0每500ms翻转一次,LED1每1s翻转一次。
23.5 编译和测试
      保存修改后,编译无报错,进入Debug模式进行仿真,当运行程序的时候,可以看到LED0和LED1同时点亮并不断闪烁,LED0每500ms闪烁一次,LED1每1s闪烁一次,实验现象和我们的一致。因为LED0和LED1的引脚PI0和PF3都有引出,大家也可以测试看看这两个引脚的高低电平时间,实际上测试出分别为500ms和1s。
页: [1]
查看完整版本: 《STM32MP1 M4裸机HAL库开发指南》第二十三章 基本定时器实验