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个通用定时器(TIM2TIM5,TIM12 TIM17)、2个高级控制定时器(TIM1和TIM8)和5 个低功耗定时器(LPTIM1~LPTIM5)。这些定时器彼此完全独立,不共享任何资源。本章节我们来学习STM32MP1的基本定时器,并通过基本定时器中断来控制LED1的翻转。
本章将分为如下几个小节:
16.1、基本定时器简介;
16.2、基本定时器中断应用;
16.3、硬件设计;
16.4、软件设计;

16.1 基本定时器简介
STM32MP157有两个基本定时器:TIM6和TIM7。其基本特征如下:
●16位自动重载递增计数器;
●16位可编程预分频器,用于对计数器时钟频率进行分频(可在运行时修改分频值),分频系数1~65535;
●可以用于触发DAC的同步电路;
●发生计数器上溢更新事件(UEV)时会生成中断/DMA 请求。
基本定时器没有通道。
16.1.1 基本定时器框图
下面我们来看看基本定时器的框图,通过框图可以了解基本定时器的工作过程,同时对之后的编程也会有一个清晰的思路。

图16.1.1. 1基本定时器框图

  1. 时钟源
    定时器是STM32的一个外设,任何外设要工作的话,都需要时钟的驱动。基本定时器挂在APB1上,其时钟来源于PCLK1,但是基本定时器时钟不是直接由APB1提供,而是经过一个倍频器,当APB1DIV的分频数为1的时候,此倍频器倍频值为1,当APB1DIV的分频数大于1的时候,此倍频器倍频值始终为2。我们前面说过,APB1的时钟频率最大为104.5MHz,所以基本定时器的计数器的时钟频率最大为209MHz,结合图中,从RCC过来的时钟,最后给内部时钟 (CK_INT) 源提供的时钟频率最大为209MHz。

图16.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[15:0]就是写入预分频器寄存器(TIMx_PSC)的值,即预分频数值。
fCK_PSC是输入时钟CK_PSC频率,fCK_CNT是输出时钟CK_CNT频率(即计数器的时钟频率,最大为209MHz)。
计数器如果是向上计数,当计数值CNT和自动重载寄存器的值ARR相等时,计数器溢出,基本定时器的溢出时间计算方法:

另外提示:预分频器寄存器(TIMx_PSC)可以通过软件在定时器运行过程中修改它的数值,修改好后的新的预分频数值不会立马生效,而是在下一个更新事件时起作用。因为更新事件发生时,会把TIMx_PSC寄存器值更新到影子寄存器中,这才会起作用,如下图:

图16.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的值马上发生变化,不会再等到来了更新事件时才发生变化。

图16.1.1. 4 ARPE位为0时计数器时序图
当ARPE为1时,TIMx_ARR进行缓冲,即TIMx_ARR和影子寄存器两者之间存在缓存机制,只有在每次产生更新事件(UEV)时,TIMx_ARR的值才会被传到影子寄存器中起作用。如下图,原来TIMx_ARR寄存器的值为F5,此刻往TIMx_ARR中写入36,但是写入的这个值并不会马上发生变化,直到等到发生更新事件以后,TIMx_ARR的值才真正变成36。

图16.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就可以了。

16.1.2 TIM6/TIM7寄存器
下面介绍TIM6/TIM7的几个重要的寄存器,具体如下:

  1. 控制寄存器 1(TIMx_CR1)
    TIM6/TIM7的控制寄存器1描述如图16.1.2. 1所示:

图16.1.2. 2 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/中断使能寄存器描述如下图所示:

图16.1.2. 3 TIMx_DIER寄存器
位0(UIE)用于使能或者禁止更新中断,因为本实验我们用到中断,所以该位需要置1。 位8(UDE)用于使能或者禁止更新DMA请求,本章节实验我们暂且用不到。
3. 状态寄存器(TIMx_SR)
TIM6/TIM7的状态寄存器描述如下图所示:

图16.1.2. 4 TIMx_SR寄存器
该寄存器位0(UIF)是中断更新的标志位,当发生中断时由硬件置1,当执行到中断服务函数的时候,要在中断服务函数里把此位清零,如果中断到来后,不清零该位,那么系统就会一直进入中断服务函数而无法进入主函数,这个不是我们想要的,关于这点我们在前面的外部中断实验中有强调过。在STM32CubeIDE上生成的中断服务函数中已经有清除中断标志位的操作了,如果是自己写中断服务函数的话,一定要记得清除中断标志位。
4. 计数器寄存器(TIMx_CNT)
TIM6/TIM7的计数器寄存器描述如下图所示:

图16.1.2. 5 TIMx_CNT寄存器
该寄存器位[15:0]就是计数器的实时的递增的值。
5. 预分频寄存器(TIMx_PSC)
TIM6/TIM7的预分频寄存器描述如下图所示:

图16.1.2. 6 TIMx_PSC寄存器
该寄存器是TIM6/TIM7的预分频寄存器,比如我们要20900分频,就往该寄存器写入20899。注意这是16位的寄存器,写入的数值范围是0到65535之间。
6. 自动重载寄存器(TIMx_ARR)
TIM6/TIM7的自动重载寄存器描述如图16.1.2.6所示:

图16.1.2. 7 TIMx_ARR寄存器
该寄存器是用于存放与计数器寄存器比较的值,当两者相等就会产生更新事件,我们在前面对基本定时器框图进行分析的时候有讲解过。如果TIMx_ARR的数值为0,则定时器会停止工作。TIMx_CR1寄存器的第7位ARPE控制TIMx_ARR是否带缓冲,当ARPE=0时,TIMx_ARR寄存器不带缓冲功能,当改变TIMx_ARR寄存器的值时,立马就更新定时器的自动重装载寄存器的值了。

  1. 事件产生寄存器 (TIMx_EGR)

图16.1.2. 8 TIMx_EGR寄存器
位0(UG)是产生更新事件,UG位由软件置1,并由硬件自动清0。
将UG位清零0时无作用;
将UG位置1时,若TIMx_CR1寄存器的UDIS位 = 0,则会重新初始化定时器的计数器并产生寄存器更新事件,注意,此时预分频器也被清0,(但预分频系数不变)。
16.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[7];        /* 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[7]:用于配置定时器的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函数,就不会发生一些错误了。加定时器状态也就是为了判断此时定时器是否可以进行下一步操作。

图16.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  }

16.2 基本定时器中断应用
本章,我们主要使用定时器来做周期性的中断应用,可以利用定时器的溢出中断来实现该功能。实现过程如下图所示,ARR是TIMx_ARR寄存器的值,CNT是TIMx_CNT的值,PSC是TIMx_PSC的值。

图16.1.4. 1通用定时器中断示意图
如图所示,CNT计数器从0开始计数,当CNT的值和ARR相等时(t1),产生一个溢出中断,然后CNT复位(清零),然后继续从0开始递增计数,如此循环。图中的t1、t2、t3就是定时器溢出中断产生的时刻。
我们通过修改ARR的值,可以改变定时时间。另外,通过修改PSC的值,可以使用不同的计数频率(图中CNT的斜率也就会改变),从而也可以改变定时的时间。
16.3 硬件设计

  1. 例程功能
    LED0用来指示程序运行,500ms为一个周期翻转。LED1用于定时器中断取反,指示定时器中断状态,1000ms为一个周期翻转。
  2. 硬件资源
    1)
    LED0 LED1
    PI0 PF3
    图16.2. 1 LED资源
    2)定时器6
  3. 原理图
    定时器属于STM32MP157的内部资源,只需要软件设置好即可正常工作。我们通过LED1来指示STM32MP157的定时器进入中断情况。
    16.4 软件设计
    本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 9 BTIM。
    16.4.1 程序设计
    基本定时器的配置流程:
    1)使能基本定时器时钟,否则无法使用基本定时器;
    2)配置基本定时器的预分频系数和自动重装载寄存器值,设置计数方向为向上计数(基本定时器只有向上计数这个模式);
    3)开启定基本定时器全局中断,通过配置NVIC来使能基本定时器中断,并配置基本定时器中断优先级;
    4)编写定基本定时器中断回调函数(STM32CubMX生成的中断服务函数会调用用户编写的回调函数);
    5)更新定时器中断和使能定时器(这个不能忘)。
    程序设计过程会在我们后面的实验中有所体现,实验的流程图如下:

图16.3.1. 1基本定时器中断实验程序流程图
16.4.2 GPIO/功能引脚配置

  1. 配置GPIO
    新建一个工程BTIM,进入STM32CubeMX插件配置界面后,在Pinout & Configuration处配置PI0和PF3为GPIO Output,并配置PI0和PF3给CM4内核使用,如下图所示。

图16.3.2. 1配置LED0和LED1
2. 配置时钟
本实验我们采用外部时钟HSE(也可以采用内部时钟),配置时钟树,经过PLL3锁相环以后,APB1的时钟频率为最大209MHz(也可以配置其它频率)。

图16.3.2. 2配置HSE
我们选择HSE,作为锁相环PLL3的时钟源,在MCU子系统时钟里输入209并回车,STM32CubeMX会自动为我们计算参数,然后再手动配置APB1DIV、APB2DIV和APB3DIV的分频值为2。当APB1DIV的分频数大于1的时候,基本定时器的倍频器倍频值始终为2,所以基本定时器的时钟频率为209MHz。

图16.3.2. 3配置系统时钟
3. 配置TIM6
如下图,按照步骤配置TIM6,其中TIM6只能给A7和M4内核两者中的某一个使用,所以,我们要把TIM6分配给M4使用。

图16.3.2. 4配置TIM6参数
注意到上面的参数配置,我们配置预分频器寄存器(TIMx_PSC)的值为20900-1;计数模式是向上递增计数,基本定时器的计数模式只有递增模式;自动重载寄存器 (TIMx_ARR)的值为10000-1;开启自动重装载模式。这里注意的是,自动重载寄存器 (TIMx_ARR)的值不能设置为0,否则计数器不工作(定时器也就不工作了),另外,要开启自动重装载模式,即定时器溢出时会自动重装初值。
Trigger Output (TRGO) Parameters选项中,触发输出事件我们配置为Reset(UG bit from TIMX_EGR),即TIMx_EGR寄存器的UG位用做触发输出,触发事件选择为清零。前面我们分析寄存器的时候说过,每次计数器溢出时可以产生更新事件,在TIMx_EGR寄存器中(通过软件方式或者使用从模式控制器)设置UG位也同样可以产生一个更新事件。另外两个选项我们没有选,其中Enable(CNT_EN)表示使能从定时器,通过一个定时器触发另一个定时器的工作方式称为定时器的同步,而发出触发信号的定时器工作于主模式,接受触发信号而启动的定时器工作于从模式,这里我们没有使用此模式,而且只用了一个定时器TIM6。Update Event表示 更新事件,即选择更新事件用作触发输出(TRGO),在本实验中也可以选择此项。
基本定时器的时钟频率是209MHz,以上参数预分频器分频值为20900-1,计算出计数器CK_CNT的时钟频率是

那么计数器计数10000次就会溢出产生中断,所以每次溢出时间是:
  1. 配置NVIC
    定时器要每秒溢出产生中断,所以我们要使能定时器全局中断,并配置中断优先级。如下图,勾选TIM6定时器全局中断:

图16.3.2. 5配置NVIC
勾选TIM6全局中断以后,开启了定时器中断,此时中断优先级为0,即最高优先级,我们在NVIC处配置中断优先级,如下配置中断优先级分组为2,抢占优先级和子优先级为3:

图16.3.2. 6配置中断优先级
5. 配置生成独立的文件
配置生成独立的.c和.h头文件,如下图:

图16.3.2. 7配置生成独立的.c和.h文件
6. 生成工程
按下“Ctrl+S”保存修改配置,生成工程:

图16.3.2. 8生成工程
16.3.3 添加用户代码

  1. 添加LED驱动
    将上一章节的窗口看门狗的BSP文件夹拷贝到CM4工程的CoreSrc目录下,如下图:

图16.3.2. 9添加BSP文件夹
2. 修改main.c文件
在main.c中添加如下代码:

/* USER CODE BEGIN 2 */
led_init();                         /* 关闭 LED0和LED1 */
HAL_TIM_Base_Start_IT(&htim6);      /* 更新定时器中断和使能定时器 */
/* USER CODE END 2 *//* 在while循环中添加 */
LED0_TOGGLE();                      /* LED0翻转 */
HAL_Delay(500);                     /* 延时500ms */
添加位置如下图所示,也可以直接参考资料里给的工程。

图16.3.3. 1 main.c文件添加的代码位置
3. 重定义回调函数
我们添加回调函数,以实现定时器每次溢出产生中断时,LED1翻转1次。

/*** @brief       定时器更新中断回调函数* @param       htim:定时器句柄指针* @retval      无*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim == (&htim6)){LED1_TOGGLE();                         /* LED1翻转 */}
}

16.3.4 编译测试
保存修改后点击工具栏的小锤子进行编译,编译无报错后,按照第4.1.6小节连接好开发板和ST-Link,进入Debug模式。进入Debug以后,点击继续运行按钮来运行调试。可以看到LED0和LED1同时点亮并不断闪烁,LED0每500ms闪烁一次,LED1每1s闪烁一次。
16.4.5 工程代码分析
工程中,gpio.c是LDE0和LED1的初始化代码,stm32mp1xx_hal_msp.c文件中有HAL_MspInit函数,主要是开启HSEM和设置TIM6的中断优先级分组为2。这些文件我们在前面的工程已经分析过,这里就不再分析了。下面我们来看看TIM6的初始化以及中断过程。

  1. tim.c文件
    tim.c文件用于初始化定时器,我们在STM32CubeMX插件上配置的参数,最后在此文件中完成初始化。代码中已经附上详细的注释,根据注释可以快速理解代码的意思,如下:
1   #include "tim.h"
2
3   TIM_HandleTypeDef htim6;                /* 定时器句柄 */
4
5   /**
6    * @brief   基本定时器TIM6定时中断初始化函数
7    * @note
8    *          基本定时器TIM6和TIM7均为16位计数器,时钟来自PCLK1(APB1),
9    *          当APB1DIV≥2分频的时候基本定时器的时钟为APB1时钟的2倍,
10   *          而APB1为104.5M, 所以定时器时钟 = 209Mhz
11   *          定时器溢出时间计算方法:
12   * Tout=(ARR[15:0]+1)*1/fCK_CNT = ((ARR[15:0]+1)*(PSC[15:0]+1))/fCK_PSC
13   * fCK_PSC=定时器工作频率,单位:Mhz,ARR[15:0]为TIMx_ARR的值,
14   *  PSC[15:0]为TIMx_PSC的值
15   * @param   无
16   * @retval  无
17   */
18  void MX_TIM6_Init(void)
19  {20    TIM_MasterConfigTypeDef sMasterConfig = {0};
21
22    htim6.Instance = TIM6;
23    /* 预分频器寄存器(TIMx_PSC)的值为20900-1 */
24    htim6.Init.Prescaler = 20900-1;
25    /* 计数模式是向上递增计数 */
26    htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
27    /* 自动重载寄存器(TIMx_ARR)的值为10000-1 */
28    htim6.Init.Period = 10000-1;
29    /* 开启自动重装载模式 */
30    htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
31    /* 初始化定时器 */
32    if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
33    {34      Error_Handler();
35    }
36    /* TIMX_EGR的UG位为0 */
37    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
38    /* 没有选择主/从模式 */
39    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
40    if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
41    {42      Error_Handler();
43    }
44
45  }
46  /**
47   * @brief   基本定时器TIM6底层硬件初始化
48   * @note
49   *          开启基本定时器时钟,配置抢占优先级和子优先级,
50              开启NVIC中断
51   * @param   tim_baseHandle定时器句柄
52   * @retval  无
53   */
54  void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
55  {56    if(tim_baseHandle->Instance==TIM6)
57    {58      /* 使能TIM6时钟 */
59      __HAL_RCC_TIM6_CLK_ENABLE();
60
61      /* 初始化TIM6抢占优先级和子优先级均为3 */
62      HAL_NVIC_SetPriority(TIM6_IRQn, 3, 3);  /* TIM6_IRQn是中断号 */
63      /* 使能定时器中断 */
64      HAL_NVIC_EnableIRQ(TIM6_IRQn);              /* TIM6_IRQn是中断号 */
65    }
66  }
67  /**
68   * @brief   基本定时器TIM6反初始化
69   * @note
70   *           关闭基本定时器时钟,关闭NVIC中断
71   * @param   tim_baseHandle定时器句柄
72   * @retval  无
73   */
74  void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle)
75  {76    if(tim_baseHandle->Instance==TIM6)
77    {78      /* 关闭定时器时钟 */
79      __HAL_RCC_TIM6_CLK_DISABLE();
80      /* 关闭定时器中断 */
81      HAL_NVIC_DisableIRQ(TIM6_IRQn);             /* TIM6_IRQn是中断号 */
82    }
83  }
第3行,定义一个定时器句柄htim6;
第18~45行,初始化定时器,其中配置预分频器寄存器(TIMx_PSC)的值为20900-1,自动重载寄存器(TIMx_ARR)的值为10000-1。设置计数模式是向上递增计数,开启自动重装载模式。即计数器从0计数到自动加载值10000-1时(前面我们计算出溢出周期是1s)产生计数器溢出,计数器溢出可以产生更新事件,当发生更新事件时,所有的寄存器都被更新,所以计数器又重新从0开始计数,如此反复。注意第36行,我们设置TIMX_EGR的UG位为0,因为将 UG 位置 1时也会产生更新中断,所以我们直接将其清零了。
  1. stm32mp1xx_it.c文件
    stm32mp1xx_it.c文件中是中断服务函数,我们来看看定时器中断的处理过程。
    /**
  • @brief 定时器中断服务函数
  • @param 无
  • @retval 无
    /
    void TIM6_IRQHandler(void)
    {
    /
    定时器中断请求函数 /
    HAL_TIM_IRQHandler(&htim6);
    }
    TIM6_IRQn是中断号,产生更新中断后,会根据中断号到启动文件中的中断向量表里找中断服务函数的入口地址,然后执行用户定义的中断服务函数(我们在外部中断实验章节有详细讲解过中断的处理过程),也就是STM32CubeMX为我们生成的TIM6_IRQHandler函数。此函数调用了定时器中断请求函数HAL_TIM_IRQHandler,我们来看看函数的大概实现过程。
    如下,HAL_TIM_IRQHandler函数的代码很长,我们这里省略部分代码,贴出和本实验相关的代码部分:
    1 /
    *
    2 * @brief 此函数处理TIM中断请求
    3 * @param htim 定时器句柄
    4 * @retval 无
    5 /
    6 void HAL_TIM_IRQHandler(TIM_HandleTypeDef htim)
    7 {
    8 /
    此处省略 捕获比较1/2/3/4事件 相关代码
    /
    9 /* TIM更新事件 /
    10 if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE) != RESET)
    11 {
    12 if (__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_UPDATE) != RESET)
    13 {
    14 __HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE);
    15 #if (USE_HAL_TIM_REGISTER_CALLBACKS == 1)
    16 htim->PeriodElapsedCallback(htim);
    17 #else
    18 HAL_TIM_PeriodElapsedCallback(htim); /
    定时器更新中断回调函数 /
    19 #endif
    20 }
    21 }
    22 /
    此处省略TIM Break输入事件、TIM触发检测事件等代码 */
    23 }
    本节是基本定时器实验,基本定时器的TIMx_SR寄存器只有第0位有效,第0位为UIF,如果发生更新事件,此位被硬件值1。第10行,通过判断TIMx_SR寄存器的UIF位,当发生更新中断的时候,更新中断标志位UIF被置1,但是需要通过软件将UIF清零,所以第14行将UIF清零了。
    第15~16行,工程中没有定义宏USE_HAL_TIM_REGISTER_CALLBACKS,所以不会编译这行的代码。
    第18行是调用定时器更新回调函数,此函数在stm32mp1xx_hal_tim.h文件中有弱定义,所以需要我们自己定义此函数。回调函数我们可以定义在其它文件中,本章节我们定义在了led.c文件中。
  1. led.c文件
    led.c文件的代码如下,进入中断服务请求函数以后,会调用HAL_TIM_PeriodElapsedCallback函数,此函数是当计数器溢出时要调用的回调函数,此函数中实现LED1翻转,每进入一次中断,LED1就翻转一次,所以我们会看到LED1在闪烁。
#include "./Include/led.h"
#include "tim.h"
void led_init(void)
{LED0(1);    /* 关闭 LED0 */LED1(1);    /* 关闭LED1 */
}
/*** @brief       定时器更新中断回调函数* @param       htim:定时器句柄指针* @retval      无*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim == (&htim6)){LED1_TOGGLE();                         /* LED1翻转 */}
}
  1. main.c文件
    main.c文件的代码大部分我们在前面的实验章节都有分析过了,我们直接看我们前面添加的代码:
1   int main(void)
2   {3     /*初始化HAL库 */
4     HAL_Init();
5     if(IS_ENGINEERING_BOOT_MODE())
6     {7       /* 配置系统时钟 */
8       SystemClock_Config();
9     }
10    /* 初始化已经配置的外设 */
11    MX_GPIO_Init();
12    /* 初始化定时器6 */
13    MX_TIM6_Init();
14    /* USER CODE BEGIN 2 */
15    led_init();/* 关闭 LED0和LED1 */
16    HAL_TIM_Base_Start_IT(&htim6);        /* 更新定时器中断和使能定时器 */
17    /* USER CODE END 2 */
18    while (1)
19    {20      /* USER CODE BEGIN 3 */
21        LED0_TOGGLE();                        /* LED0翻转 */
22        HAL_Delay(500);                       /* 延时500ms */
23    }
24    /* USER CODE END 3 */
25  }
红色部分的代码是我们手动添加的。
第15行,先关闭LED0和LED1,;第16行,调用HAL库的HAL_TIM_Base_Start_IT函数来更新定时器中断和使能定时器,这句代码不能少,如果忘记添加的话,实验会不正常;第21和22行,LED0每隔500ms翻转一次。

【正点原子MP157连载】第十六章 基本定时器实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南相关推荐

  1. 【正点原子MP157连载】第十七章 通用定时器实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  2. 【正点原子MP157连载】第二十六章 DS18B20数字温度传感器实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  3. 【正点原子MP157连载】第十二章 按键输入实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  4. 【正点原子MP157连载】第一章 本书学习方法-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  5. 【正点原子STM32连载】第二十一章 通用定时器实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1

    1)实验平台:正点原子MiniPro H750开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560 3)全套实验源码+手册+视频 ...

  6. 【正点原子MP157连载】第十九章 OLED实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  7. 【正点原子MP157连载】第十五章 窗口门狗(WWDG)实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  8. 【正点原子MP157连载】第二十八章 A7和M4联合调试-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  9. 【正点原子MP157连载】第二十七章 DHT11数字温湿度传感器实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

最新文章

  1. ABAP 程序间的调用
  2. mysql流量控制_UDP流量控制之分析
  3. suse系统_你知道吗,Artifactory还可以管理SUSELinux系统的依赖
  4. Laravel框架与ThinkPHP一些不同点
  5. 定义一个圆类——Circle,在类的内部提供一个属性:半径(r),同时 提供 两个 方 法 : 计算 面积 ( getArea() ) 和 计算 周长(getPerimeter()) 。
  6. poweramp最完美设置_2020年感恩节,你最想感谢的人是谁?
  7. ORA-02291: 违反完整约束条件 …… - 未找到父项关键字
  8. emule学习与分析一 概述
  9. ServiceNow常用角色和分组
  10. linux 权限 代码,linux 管理权限(示例代码)
  11. abaqus6.14 帮助 Abaqus Example Problems Guide翻译
  12. android listview刷新数据库,android – 如何在数据库更改后刷新ListView?
  13. 易班显示不能连接到服务器检查网络,网络思政教育 “易班网”不一般
  14. 安卓手机卡顿怎么解决_苹果七系统内存满了手机卡顿解决方法
  15. DiskGenius 5.4.6.1441 Portable
  16. Android 高德地图(带有定位和点击显示经度纬度)
  17. excel白屏未响应_「excel打开空白」Excel 2016 打开后空白的解决方法 - seo实验室
  18. element ui 中级联选择器,点击完下拉框收回
  19. vue el-form 遇上 v-if,表单校验不生效问题
  20. windows安装golang多版本管理工具gvm/g

热门文章

  1. Open3d图形界面之3D显示控件
  2. Unity 性能优化(力荐)
  3. 带团队,不要轻易放弃任何一个队友
  4. python进阶(十)_mysql数据查询
  5. 苹果4怎么越狱_来看iPhone迁移怎么用 除iOS12.4苹果还为旧款设备推送了更新
  6. window10离线安装net3.5的三种方法
  7. C语言之文件打开模式
  8. Linux_FastDFS分布式文件系统——搭建
  9. elementUI table隐藏行
  10. 三分钟读懂 Chainge(橙子):跨链转账的王者, DeFi 中自由转移的应用聚合平台