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

第十七章 通用定时器实验

本章我们主要来学习通用定时器,STM32MP157有10个通用定时器(TIM2TIM5,TIM12 TIM17)。我们将通过四个实验来学习通用定时器的几个功能,分别是通用定时器中断实验、通用定时器PWM输出实验、通用定时器输入捕获实验和通用定时器脉冲计数实验。
本章分为如下几个小节:
17.1、通用定时器简介;
17.2、通用定时器中断实验;
17.3、通用定时器PWM输出实验;
17.4、通用定时器输入捕获实验;
17.5、通用定时器脉冲计数实验(外部时钟模式1);
17.6、通用定时器脉冲计数实验(外部时钟模式2);

17.1 通用定时器简介
17.1.1 STM32MP157的通用定时器

  1. 定时器资源
    STM32MP157的通用定时器有10个之多,其基本特性也是不尽相同,为了更好的区别各个定时器的特性,我们列了一个表格,如下所示:

表17.1.1. 1定时器基本特性表
由上表知道:除了TIM2和TIM5是32位的计数器,其他定时器是16位的。通用定时器和高级定时器是在基本定时器的基础上,添加了一些额外功能,基本定时器有的功能通用定时器都有,而且还增加了递减计数、PWM生成、输入捕获、输出比较等功能。高级定时器又包含了通用定时器的所有功能,此外还增加带可编程死区的互补输出、重复计数器、断路输入等功能。以上定时器中,通用定时器数量较多,并且其特性也有一定的差异,但是基本原理一样。
2. 通用定时器框图
下面我们以TIM2/TIM3/TIM4/TIM5的框图为例来学习通用定时器框图,其他通用定时器的框图会有差异,因为内容比较多,大家学习了这里的框图再看ST官方的手册其他的定时器框图就会比较容易理解。通过学习通用定时器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。

图17.1.1. 1通用定时器框图
如上图,通用定时器的框图比基本定时器的框图复杂许多,为了方便介绍,我们将其分成六个部分讲解:
●①时钟源
通用定时器时钟可由下列的时钟源提供:
1)内部时钟 (CK_INT)
2)外部时钟模式 1:外部输入引脚 (TIx),x=1,2,3,4
3)外部时钟模式 2:外部触发输入 (ETR)
4)内部触发输入 (ITRx):使用一个定时器作为另一定时器的预分频器
内部时钟 (CK_INT)
这里的内部时钟 (CK_INT)实际来自APB1,定时器TIM2TIM7和定时器TIM12TIM14挂在APB1总线上,定时器TIM1、TIM8和TIM15~TIM17挂在APB2上。TIM2/TIM3/TIM4/TIM5定时器的时钟源是APB1经过一个倍频器才接到这些定时器的(即时钟不是直接来自APB1),当APB1的预分频系数为1时,此倍频器倍频值为1,定时器的时钟频率等于APB1的频率;当 APB1的预分频系数为其它数值时,此倍频器倍频值为2,定时器的时钟频率等于APB1的频率2倍。这个情况跟基本定时器的一样,请回顾基本定时器的这部分内容,最后得到TIM2/TIM3/TIM4/TIM5定时器的时钟频率为2倍的APB1,即209MHZ。如下图,可以在STM32CubeMX上动态配置时钟:

图17.1.1. 2 定时器时钟
外部时钟模式 1

图17.1.1. 3 TI2外部时钟连接示例
根据上图,我们做简单的描述:
时钟信号
如果时钟源选择的是外部时钟模式 1,即时钟信号来自外部输入引脚TI1TI4中的某一个(即定时器的通道TIMx_CH1 TIMx_CH4),属于触发输入TRGI。
滤波器和边沿检测
外部输入引脚输入的信号经过滤波器处理以后,再经过边沿检测器输出上升沿或者下降沿有效信号。滤波器的功能,简单来说就是多次检测视为一次有效,也就是连续进行N次采样检测,如果采样检测的结果都是高电平,则说明这是一个有效的电平信号,这样便可以过滤掉那干扰信号。
触发源选择
触发输入源有很多,可以来自内部触发ITRx(x等于0~4)、边沿检测器TI1F_ED、滤波后的定时器输入1(TI1FP1)、滤波后的定时器输入2(TI2FP2)、外部触发输入(ETRF)中的某一个。其中ITRx可由内部其他定时器产生信号,即使用一个定时器作为另一个定时器的预分频器,提供触发信号的定时器工作于主模式,接受触发信号的定时器工作于从模式。
上图中,采用外部时钟模1时,如果时钟信号来自外部输入引脚TI2,通过配置TIMx_SMCR 寄存器的TS[4:0]= 00101可以配置触发信号来自滤波后的定时器输入 1 (TI1FP1),也可以配置TIMx_SMCR寄存器的TS[4:0]= 00100选择TI1边沿检测器(TI1F_ED)为触发源。
从模式选择
对于外部时钟模式1,触发信号接到TRGI引脚给外部时钟模式1以后,还需要配置TIMx_SMCR寄存器的SMS[2:0]位= 0111来配置从模式为外部时钟模式1。
外部时钟模式 2

图17.1.1. 4外部时钟模式 2
根据上图,我们做简单的描述:
时钟信号输入
使用外部是种模式2时,时钟信号来自ETR引脚,ETR引脚可以为定时器提供外部时钟信号,例如PA0可以复用为TIM2_ETR/TIM2_CH1,如果配置PA0复用为TIM2_ETR的话,那么PA0引脚作为外部时钟输入引脚,例如可以让别的引脚模拟输出脉冲或者PWM波形,然后用杜邦线将此模拟输出脉冲引脚连接到PA0,给PA0提供时钟脉冲,或者将外部要采集的脉冲接入到PA0(注意IO口耐压范围)也是可以的。

图17.1.1. 5 PA0引脚可以复用为ETR
如果要选择ETR作为时钟源,需要配置TIMx_AF1 寄存器中的 ETRSEL[3:0] 位来选择正确的 ETR 源(如上面的TIM2_ETR),并通过配置TIMx_SMCR寄存器来设置预分频器、上升沿或者下降沿检测以及使能外部时钟模式 2,还需要配置TIMx_CR1 寄存器的CEN=1 来使能计数器。具体配置步骤可以查阅参考手册。
根据以上外部时钟模式1和外部时钟模式2的框图,对比两者:
外部时钟模式1的时钟信号来自定时器通道TIMx_CH1~ TIMx_CH4,经过滤波、边沿检测和极性选择后,以触发信号TRGI的形式进入到从模式选择器,作为定时器的时钟源,如下图中的1路线。外部时钟模式2的时钟信号来自特定的ETR引脚,此信号经过极性选择、分频和滤波后(分频和滤波不是必需的,可以根据外来信号频率的高低以及信号干扰信号程度来决定),不经过从模式选择器,像内部时钟(CK_INT)一样直接进入到了计数器,为计数器提供时钟,如图中的路线2。内部时钟(CK_INT)是下图中的路线3。

图17.1.1. 6通用定时器框图(部分)
关于两种模式的时钟输入引脚:
外部时钟模式1的是来自定时器通道TIMx_CH1~ TIMx_CH4,而外部时钟模式2则来自特定的ETR引脚;外部时钟模式1的时钟信号具有触发的特点,定时器工作于外部时钟模式1从模式,触发信号可以产生触发事件,从而产生中断或者DMA请求;外部时钟模式2来自ETR引脚,只是一个时钟信号,不具备触发的功能,定时器可以工作在主模式,也可以工作在从模式(复位、发、门控等)。
例如对TIM2的通道1可以配置这两种模式:

图17.1.1. 7 STM32CubeMX配置
内部触发输入(ITRx)
定时器连接来自其它定时器的触发输出,即使用一个定时器作为另一定时器的预分频器。发送触发输出信号的定时器工作于主模式,接收触发信号的定时器工作于从模式。主模式定时器可以对从模式定时器的计数器执行复位、启动、停止操作或为其提供时钟。这种模式也就是定时器的级联,如下图:

图17.1.1. 8 主/从模式示意图
接下来我们继续分析通用定时器框图部分的内容。
●②控制器
控制器包括:从模式控制器、编码器接口和触发控制器(TRGO)。从模式控制器可以控制计数器复位、启动、递增/递减、计数。编码器接口针对编码器计数,我们没用到。触发控制器用来提供触发信号给别的外设,比如为其它定时器提供时钟或者为DAC/ADC的触发转换提供信号。
●③时基单元
时基单元包括:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR)。这里注意的一点是这里的计数器模式有三种:递增、递减和中心对齐,并且TIM2 和TIM5是32位的。
递增计数模式在基本定时器已经讲过,递减计算模式就很好理解,就是来一个脉冲计数器减1,直到计数器值减到0,然后计数器又从自动重载寄存器ARR的值开始继续递减计数,并生成定时器下益事件。
而中心对齐计数模式字面上不好理解,该模式下,计数器从0开始递增计数,直到计数值等于自动重载寄存器ARR的值减1后,生成定时器上溢事件,然后从自动重载寄存器ARR的值开始递减计算,直到计数值等于1,并生成定时器下益事件。然后又从0开始计数,一直循环。每次发生计数器上溢和下溢事件都会生成更新事件。
●④输入捕获
输入捕获包括:4个输入捕获通道(TIMx_CH1~ TIMx_CH4)、输入滤波和边沿检测和预分频器等部分,用于输入捕获功能,如:测量输入信号的脉冲宽度、测量 PWM 输入信号的频率和占空比等。
下面简单说一下输入捕获的工作原理:一般先设置输入捕获为上升沿检测,并记录发生上升沿时计数器寄存器(TIMx_CNT)的值。然后设置输入捕获为下降沿检测,当检测到下降沿到来时,记录此时计数器寄存器(TIMx_CNT)的值。最后,用后面记录的值减去前面记录的值,就得到此次高电平的脉冲宽度,再根据定时器的计数频率就可以计算出这个高电平脉冲的时间。低电平脉冲捕获同理。
●⑤输入捕获和输出比较公用部分
这部分包括:4个捕获比较寄存器,后面寄存器内容再详细分析。
●⑥输出比较
输出比较包括:4个输出比较通道和相应的输出控制器组成,用于输出比较模式或PWM输出模式。
通用定时器定时器比较多,有10个,我们这里就主要以TIM2/TIM3/TIM4/TIM5为例子进行实验讲解,其它没讲到的定时器也是类似的,具体可以查询参考手册来了解。下面分别通过四个实验来详细学习通用定时器的功能。
17.2 通用定时器中断实验
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 10-1 GTIM。
17.2.1 TIM2/TIM3/TIM4/TIM5寄存器
下面介绍TIM2/TIM3/TIM4/TIM5的几个与定时器中断相关且重要的寄存器,其他的通用定时器的寄存器会有一些差异,请大家自行对比《STM32H7xx参考手册》第39~41章,具体如下:

  1. 控制寄存器 1(TIMx_CR1)
    TIM2/TIM3/TIM4/TIM5的控制寄存器1描述如下图所示:

图17.2.1. 1 TIMx_CR1寄存器
上图中我们只列出了本章需要用的一些位,其中:
位0(CEN),用于计数器使能,将该位置1表示禁止计数器,将改位置0表示使能计数器;
位4(DIR),用于控制定时器的计数方向,我们需要向上计数模式,所以设置DIR=0;
位6和位5(CMS[1:0]位),用于控制中心对齐模式,本章我们使用边沿对齐模式,所以设置为00即可;
位7(ARPE)用于控制自动重载预装载使能,0表示TIMx_ARR 寄存器不进行缓冲,1表示TIMx_ARR 寄存器进行缓冲,在基本定时器章节介绍影子寄存器时我们有讲解过。
2. 从模式控制寄存器(TIMx_SMCR)
TIM2/TIM3/TIM4/TIM5的从模式控制寄存器描述如下图所示:

图17.2.1. 2 TIMx_SMCR寄存器
该寄存器的SMS[3:0]位,用于从模式选择,其实就是选择计数器输入时钟的来源。比如通用定时器中断实验我们设置SMS[3:0]=0000,禁止从模式,这样PSC预分频器的时钟就直接来自内部时钟(CK_INT),频率一般为209Mhz(APB1频率的2倍)。而通用定时器中断实验我们设置SMS[3:0]=0111,外部时钟模式1,这样就可以检测按键的脉冲当做计数器时钟。
3. TIMx DMA/中断使能寄存器 (TIMx_DIER)
TIM2/TIM3/TIM4/TIM5的DMA/中断使能寄存器描述如图21.2.1.3所示:

图17.2.1. 3 TIMx_DIER寄存器
该寄存器涉及触发DMA请求、捕获/比较中断以及更新中断使能,本章实验只用到后面两个。位0(UIE)是更新中断允许位,通用定时器中断实验需要用到定时器的更新中断,所以该位要设置为1来允许由于更新事件所产生的中断。而位1到位4是捕获/比较中断使能位,分别对应四个输入输出通道,将其置1表示使能中断,清0表示禁止中断。
4. 状态寄存器(TIMx_SR)

图17.2.1. 4 TIMx_SR寄存器
该寄存器都是一些中断标志位,CC1OF~CC4OF对应捕获/比较重复捕获标志,以第9位为例,如果CC1IF位为1,表示TIMx_CCR1寄存器中已捕获到计数器值且CC1IF 标志已置1;
位6属于触发中断标志,0表示未发生触发事件,1表示触发中断挂起;
位0(UIF)表示更新中断标志,当定时器中断来到该位会由硬件置1,标志中断到来,我们需要在中断服务函数里面把该位清零。
5. 计数器寄存器(TIMx_CNT)

图17.2.1. 5 TIMx_CNT寄存器
因为定时器2和定时器5的计数器是32位的,所以当用到这两个定时器的时候,TIMx_CNT寄存器的32位都是用做计数器寄存器,其他定时器的就跟基本定时器一样,只用到低16位。
6. 预分频寄存器(TIMx_PSC)

图17.2.1. 6 TIMx_PSC寄存器
所有定时器的预分频寄存器都是16位的,即写入该寄存器的数值范围是0到65535,表示1到65536分频。比如我们要20900分频,就往该寄存器写入20899。
7. 自动重载寄存器(TIMx_ARR)

图17.2.1. 7 TIMx_ARR寄存器
该寄存器是用于存放与计数器寄存器比较的值,ARR[15:0]为自动重载值的低 16 位,ARR[31:16]为自动重载值的高 16 位,定时器2和定时器5的计数器是32位的,那该寄存器也是32位,其他定时器的就跟基本定时器一样,只用到低16位。
17.2.2 HAL库的API函数
本章节实验重要的API函数和上一章基本定时器介绍的内容差不多,这里就不再重复介绍此部分内容。
17.2.3 硬件设计

  1. 例程功能
    LED0用来指示程序运行,500ms为一个周期。LED1用于定时器中断取反,指示定时器中断状态,1000ms为一个周期。
  2. 硬件资源

表17.2.3. 1 LED硬件资源
2)定时器3
3. 原理图
定时器属于STM32MP157的内部资源,只需要软件设置好即可正常工作。我们通过LED1来指示STM32MP157的定时器进入中断情况。
17.2.4 软件设计

  1. 程序流程图
    下面看看本实验的程序流程图:

图17.2.4. 1通用定时器中断实验程序流程图

  1. STM32CubeMX配置
    (1)配置GPIO
    新建一个工程GTIM(或者直接在上一章节实验的基础上操作),进入STM32CubeMX插件配置界面后,在Pinout & Configuration处配置PI0和PF3为GPIO Output,并配置PI0和PF3给CM4内核使用,如下图所示。

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

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

图17.2.4. 4配置系统时钟
(3)配置TIM3
本实验我们使用的是通用定时器的TIM3,如下图,按照步骤配置TIM3,其中TIM3只能给A7和M4内核两者中的某一个使用,所以,我们要把TIM3分配给M4使用。

图17.2.4. 5配置TIM3参数
如上图,按照步骤配置基本定时器。Clock Source我们选择Internal Clock,表示使用内部时钟,即来自APB1的时钟。然后配置预分频器寄存器(TIMx_PSC)的值为20900-1。计数模式是向上递增计数,前面我们提到基本定时器的计数模式只有递增(向上)计数模式,而通用定时器具有向上或者向下计数模式。对于向上计数模式,复位后计数器从0重新开始计数对于向下计数模式,复位后计数器从ARR开始向下递减计数。自动重载寄存器 (TIMx_ARR)的值为10000-1。开启自动重装载模式。
定时器3的时钟频率是209MHz,计算出计数器CK_CNT的时钟频率是

那么计数器计数10000次就会溢出产生中断,所以每次溢出时间是:

其它选项就保持默认状态,不需要配置了。例如,Slave Mode用于配置从模式(复位、触发、门控);Trigger Source用于配置选择触发源,触发源可以是ITR0~ITR3和TI1_ED中的某一个,我们前面介绍过,这几个是外部时钟模式 1的时钟源,也可以选择外部时钟源,本章节我们直接使用Channel1~Channel4表示通道,本章节实验用不到所以就不配置。
(4)配置NVIC
定时器要每秒溢出产生中断,所以我们要使能定时器全局中断,并配置中断优先级。如下图,勾选TIM3定时器全局中断:

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

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

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

图17.2.4. 9生成工程
2. 添加用户代码
(1)将上一章节基本定时器的BSP文件夹拷贝到工程中:

图17.2.4. 10拷贝上一个工程的BSP文件夹
上一章节的led.c中我们是自定义了回调函数HAL_TIM_PeriodElapsedCallback,实现每次中断LED1翻转:

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

(2)修改main.c文件
在min.c中添加如下代码:

/* USER CODE BEGIN 2 */led_init();/* 关闭 LED0和LED1 */HAL_TIM_Base_Start_IT(&htim3);           /* 更新定时器中断和使能定时器 *//* USER CODE END 2 */LED0_TOGGLE();                  /* LED0翻转  */
HAL_Delay(500);                 /* 延时500ms */

图17.2.4. 11添加的代码在main.c文件中的位置
3. 编译和测试
保存修改,再进行编译,编译不报错进入Debug模式验证,实验现象和上一章节的基本定时器的一样,即运行后,LED0和LED1同时点亮,LED0以500ms周期闪烁,LED1以1s周期闪烁。
17.3 通用定时器PWM输出实验
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 10-2 GTIM_PWM。
17.3.1 定时器的PWM输出模式

  1. 脉冲宽度调制
    本小节我们来学习如何使用通用定时器的PWM输出模式。
    脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。
    PWM通过对一系列脉冲的宽度进行调制后,等效输出一系列幅值相等的波形,它把模拟信号转化为数字电路所需要的编码。PWM 常用来做电机控制、LCD 背光亮度调节、开关电源等,在测量、通信、及功率控制等许多领域中广泛应用。如下图,一模拟信号(正弦波)经过PWM后输出一系列幅值相等的脉冲(我们称为PWM波),使用这些脉冲来代替正弦波或所需要的波形,可以通过调节这些脉冲的占空比等效控制输出电压的大小,例如,在PWM频率一定的条件下,比如占空比为100%时,输出电压为5V,占空比为0时,输出电压为0V,当我们需要输出2.5V的电压时,只要将占空比调节为50%就能实现。

图17.3.1. 1 PWM输出
注:
PWM占空比就是一个脉冲周期内高电平在整个周期占的比例
如果芯片内部自带PWM输出功能模块,可以直接配置PWM实现PWM输出。如果芯片内部没有PWM功能模块,我们也可以通过定时器来控制一个IO口按照一定的时间间隔输出一高一低的电平以模拟PWM波形,例如模拟上图的波形b)。本章节我们就是使用定时器的一个通道输出PWM波形。
2. PWM输出模式
PWM有两种输出模式,分别为PWM模式1和PWM模式2。可以理解PWM模式1是与PWM模式2互补的波,PWM模式1为高电平时,PWM模式2为低电平,反之亦然。PWM模式和计数器计数模式对通道输出的PWM波电平有影响:在PWM模式1下,当计数器向上计数时,如果CNT< CCRx时,通道为有效电平,否则为无效电平;当计数器向下计数时,如果CNT>CCRx时,通道为无效电平,否则为有效电平。

表17.3.1. 1 PWM输出模式
例如一个外设是低电平有效才会运行,采用递增计数时:当CNT< CCRx时,此外设运行;当CNT> CCRx时,此外设不运行。
3. PWM 生成(边沿对齐模式)
PWM波频率由自动重载寄存器(TIMx_ARR)的值决定,其占空比则由捕获/比较寄存器(TIMx_CCRx)的值决定。它们生成PWM的原理如图下图所示:

图17.3.1. 2 PWM原理示意图
上图就是一个简单的PWM原理示意图。图中,我们假定定时器工作在边沿对齐,向上计数PWM模式,且当CNT<CCRx时,输出0,当CNT>=CCRx时输出1。那么就可以得到如上的PWM示意图:当CNT值小于CCRx的时候,IO输出低电平(0),当CNT值大于等于CCRx的时候,IO输出高电平(1),当CNT达到ARR值的时候,重新归零,然后重新向上计数,依次循环。改变CCRx的值,就可以改变PWM输出的占空比,改变ARR的值,就可以改变PWM输出的频率,这就是PWM输出的原理。
4. PWM 生成(中心对齐模式)
通用定时器(包括后面我们后面会学到的高级定时器)除了支持单向的向上或向下计数模式外,还支持中心对齐计数模式,即一个计数周期内分别由向上计数和向下计数两个过程组成,中心对齐模式用来输出对称波形,比如正弦波,可以基于中心对齐模式来实现PWM输出比较功能。中心对齐模式由TIMx_CR1寄存器的CMS[1:0]位来配置,当CMS[1:0]配置:
00:边沿对齐模式。计数器根据方向位 (DIR) 递增计数或递减计数;
01:中心对齐模式 1。计数器交替进行递增计数和递减计数,输出比较中断标志位,只在计数器递减计数时被置1;
10:中心对齐模式 2。计数器交替进行递增计数和递减计数,输出比较中断标志位,只在计数器增计数时被置1;
11:中心对齐模式 3。计数器交替进行递增计数和递减计数,输出比较中断标志位,只在计数器增计数时和递减计数时均被置1。
以上列出有3种中心对齐模式,我们以PWM模式1和中心对齐模式 3为例,PWM波形图如下:

图17.3.1. 3中心对齐模式 3
如上,对于中心对齐模式3,计数器在向上计数或者向下计数时,输出比较中断标志位被置1,当CNT<CCRx时,PWM是高电平1,当CNT>CCRx时,PWM是低电平。
STM32MP157的定时器除了基本定时器TIM6和TIM7,其他的定时器都可以用来产生PWM输出。其中高级定时器TIM1和TIM8可以同时产生多达7路的PWM输出。而通用定时器也能同时产生多达4路的PWM输出!本实验我们以使用TIM5的CH4通道产生一路PWM输出来控制LED0为例进行学习,如下图是STM32MP157数据手册的部分截图,我们的开发板上PI0接的LED0,PI0可以复用为TIM5_CH4,通过配置TIM5_CH4产生PWM波形可以控制LED0工作。

图17.3.1. 4 PI0可以复用为TIM5_CH4
17.3.2 TIM2/TIM3/TIM4/TIM5寄存器
要使STM32H750的通用定时器TIMx产生PWM输出,除了上一小节介绍的寄存器外,我们还会用到3个寄存器,来控制PWM。这三个寄存器分别是:捕获/比较模式寄存器(TIMx_CCMR1/2)、捕获/比较使能寄存器(TIMx_CCER)、捕获/比较寄存器(TIMx_CCR1~4)。接下来我们简单介绍一下这三个寄存器。

  1. 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
    TIM2/TIM3/TIM4/TIM5的捕获/比较模式寄存器(TIMx_CCMR1/2,指输出比较模式),该寄存器一般有2个:TIMx _CCMR1和TIMx _CCMR2。TIMx_CCMR1控制CH1和CH2,而TIMx_CCMR2控制CH3和CH4。TIMx_CCMR2寄存器描述如下图所示:

图17.3.2. 1 TIMx_CCMR2寄存器
该寄存器的有些位在不同模式下,功能不一样,我们现在只用到输出比较,输入捕获后面的实验再讲解。关于该寄存器的详细说明,请参考《STM32MP157参考手册》第41.4.10小节。 比如我们要让TIM5的CH4输出PWM波为例进行介绍:
OC4M[3:0] 是输出比较4模式设置位,对应着通道4的输出比较4模式设置,此部分由4位组成。总共可以配置成14种模式,我们使用的是PWM模式,所以这4位必须设置为0110或者0111,分别对应PWM模式1和PWM模式2。这两种PWM模式的区别就是输出有效电平的极性相反;
OC4PE是输出比较通道4的预装使能,该位需要置1;
CC4S[1:0]用于设置通道1的方向(输入/输出)默认设置为0,就是设置通道作为输出使用。
2. 捕获/比较使能寄存器(TIMx_ CCER)
TIM2/TIM3/TIM4/TIM5的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如下图所示:

图17.3.2. 2 TIMx_CCER寄存器
该寄存器比较简单,要让TIM5的CH4输出PWM波,这里我们要使能CC4E位,该位是输入/捕获通道4(ch4)的使能位,要想PWM从IO口输出,这个位必须设置为1。CC4P位是设置通道4的输出极性,我们默认设置0,即OC4 高电平有效。
3. 捕获/比较寄存器1/2/3/4(TIMx_ CCR1/2/3/4)
捕获/比较寄存器(TIMx_ CCR1/2/3/4),该寄存器总共有4个,对应4个通道CH1~CH4。我们使用的是通道4,所以来看看TIMx_ CCR4寄存器描述如下图所示:

图17.3.2. 3 TIMx_ CCR1寄存器
此寄存器是捕获/比较寄存器 4 的预装载值,在输出模式下,该寄存器的值与CNT的值比较,根据比较结果产生相应动作,利用这点,我们通过修改这个寄存器的值,就可以控制PWM的输出脉宽了。注意,对于TIM2和TIM5来说,该寄存器是32位有效的,对其他定时器来说,则是16位有效位。
4.TIM1/TIM8断路和死区寄存器(TIMx_ BDTR)
如果是通用定时器,则配置以上说的寄存器就够了,但是如果是高级定时器,则还需要配置:断路和死区寄存器(TIMx_BDTR),该寄存器各位描述如图21.3.1.4所示:

图17.3.2. 4 TIMx_ BDTR寄存器
该寄存器,我们只需要关注位15(MOE),主输出使能位,要想高级定时器的PWM正常输出,则必须设置MOE位为1,否则不会有输出。注意:通用定时器不需要配置这个。该寄存器的其他位我们这里就不详细介绍了,讲到高级定时器的时候再介绍其它位。
17.3.3 定时器的HAL库驱动
定时器在HAL库中的驱动代码在前面介绍基本定时器已经介绍了部分,这里我们再介绍几个和本实验用到的函数。

  1. HAL_TIM_PWM_Init函数
    定时器的PWM输出模式初始化函数,其声明如下:
    HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
    函数描述:
    用于初始化定时器的PWM输出模式。
    函数形参:
    形参1是TIM_HandleTypeDef结构体类型指针变量,基本定时器的时候已经介绍。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
    注意事项:
    该函数实现的功能以及使用方法和HAL_TIM_Base_Init都是类似的,作用都是初始化定时器的ARR和PSC等参数。为什么HAL库要提供这个函数而不直接让我们使用HAL_TIM_Base
    _Init函数呢?这是因为HAL库为定时器的针对PWM输出定义了单独的MSP回调函数HAL_TIM_PWM_MspInit,所以当我们调用HAL_TIM_PWM_Init进行PWM初始化之后,该函数内部会调用MSP回调函数HAL_TIM_PWM_MspInit。而当我们使用HAL_TIM_Base_Init初始化定时器参数的时候,它内部调用的回调函数为HAL_TIM_Base_MspInit,这里大家注意区分。
  2. HAL_TIM_PWM_ConfigChannel函数
    定时器的PWM通道设置初始化函数。其声明如下:
    HAL_StatusTypeDef HAL_TIM_PWM_ConfigChannel(TIM_HandleTypeDef *htim,
    TIM_OC_InitTypeDef sConfig,
    uint32_t Channel)
    函数描述:
    该函数用于设置定时器的PWM通道。
    函数形参:
    形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。
    形参2是TIM_OC_InitTypeDef结构体类型指针变量,用于配置定时器的输出比较参数。
    重点了解一下TIM_OC_InitTypeDef结构体指针类型,其定义如下:
    typedef struct
    {
    uint32_t OCMode; /
    输出比较模式选择,寄存器的时候说过了,共7种模式 /
    uint32_t Pulse; /
    设置比较值,默认比较值为自动重装载值的一半,即占空比为50% /
    uint32_t OCPolarity; /
    设置输出比较极性 /
    uint32_t OCNPolarity; /
    设置互补输出比较极性 /
    uint32_t OCFastMode; /
    使能或失能输出比较快速模式 /
    uint32_t OCIdleState; /
    选择空闲状态下的非工作状态(OC1 输出) /
    uint32_t OCNIdleState; /
    设置空闲状态下的非工作状态(OC1N 输出) */
    } TIM_OC_InitTypeDef;
    该结构体成员我们重点关注前三个。成员变量OCMode用来设置模式,这里我们设置为PWM模式1。成员变量Pulse用来设置捕获比较值。成员变量TIM_OCPolarity用来设置输出极性是高还是低。其他的参数TIM_OutputNState,TIM_OCNPolarity,TIM_OCIdleState和TIM_OCNIdleState是高级定时器才用到的。
    形参3是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。这里我们使用的是定时器5的通道4,所以取值为TIM_CHANNEL_4即可。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
  3. HAL_TIM_PWM_Start函数
    定时器的PWM输出启动函数,其声明如下:
    HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim,uint32_t Channel)
    函数描述:
    用于启动定时器的PWM输出模式。
    函数形参:
    形参1是TIM_HandleTypeDef结构体类型指针变量。
    形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
    注意事项:
    对于单独使能定时器的方法,在上一章定时器实验我们已经讲解。实际上,HAL库也同样提供了单独使能定时器的输出通道函数,函数为:
    void TIM_CCxChannelCmd(TIM_TypeDef *TIMx, uint32_t Channel, uint32_t ChannelState);
    HAL_TIM_PWM_Start函数内部也调用了该函数。
  4. HAL_TIM_ConfigClockSource函数
    配置定时器时钟源函数,其声明如下:
    HAL_StatusTypeDef HAL_TIM_ConfigClockSource(TIM_HandleTypeDef *htim, \ TIM_ClockConfigTypeDef sClockSourceConfig)
    函数描述:
    用于配置定时器时钟源。
    函数形参:
    形参1是TIM_HandleTypeDef结构体类型指针变量。
    形参2是TIM_ClockConfigTypeDef结构体类型指针变量,用于配置定时器时钟源参数。
    TIM_ClockConfigTypeDef定义如下:
    typedef struct
    {
    uint32_t ClockSource; /
    时钟源 /
    uint32_t ClockPolarity; /
    时钟极性 /
    uint32_t ClockPrescaler; /
    定时器预分频器 /
    uint32_t ClockFilter; /
    时钟过滤器 /
    } TIM_ClockConfigTypeDef;
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
    注意事项:
    该函数主要操作TIMx_SMCR寄存器,系统默认定时器的时钟源就是内部时钟,所以一般定时器要使用内部时钟,我们就不对定时器的时钟源就行初始化,默认即可。这里只是让大家知道有这个函数可以设定时器的时钟源。比如用HAL_TIM_ConfigClockSource初始化选择内部时钟,方法如下:
    TIM_HandleTypeDef timx_handle; /
    定时器x句柄 /
    TIM_ClockConfigTypeDef sClockSourceConfig = {0};
    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; /
    选择内部时钟 */
    HAL_TIM_ConfigClockSource(&timx_handle, &sClockSourceConfig);
    其他的时钟源请大家参考TIMx_SMCR寄存器和HAL库。后面的定时器初始化凡是用到内部时钟我们都没有去初始化,系统默认即可。
  5. 修改占空比
    前面我们说过,通过修改比较值TIMx_CCRx则可以控制通道的输出占空比。继而控制LED0的亮度(本实验是使用TIM5,则可以通过控制TIM5_CCR2来控制CH4的输出占空比)。HAL库中提供一个修改占空比的宏定义:
#define __HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \(((__CHANNEL__) == TIM_CHANNEL_1) ? ((__HANDLE__)->Instance->CCR1 =    (__COMPARE__)) :\((__CHANNEL__) == TIM_CHANNEL_2) ? ((__HANDLE__)->Instance->CCR2 =    (__COMPARE__)) :\((__CHANNEL__) == TIM_CHANNEL_3) ? ((__HANDLE__)->Instance->CCR3 =    (__COMPARE__)) :\((__CHANNEL__) == TIM_CHANNEL_4) ? ((__HANDLE__)->Instance->CCR4 =    (__COMPARE__)) :\((__CHANNEL__) == TIM_CHANNEL_5) ? ((__HANDLE__)->Instance->CCR5 =    (__COMPARE__)) :\((__HANDLE__)->Instance->CCR6 = (__COMPARE__)))

__HANDLE__是TIM_HandleTypeDef结构体类型指针变量,__CHANNEL__对应PWM的输出通道,_COMPARE__则是要写到捕获/比较寄存器(TIMx CCR1/2/3/4)的值。实际上该宏定义最终还是往对应的捕获/比较寄存器写入比较值来控制PWM波的占空比。如下解析:
比如我们要修改定时器5通道4的输出比较值(控制占空比),寄存器操作方法:
TIM5->CCR2 = ledrpwmval; /* ledrpwmval是比较值,并且动态变化的,
所以我们要周期性调用这条语句,已达到及时修改PWM的占空比 */
__HAL_TIM_SET_COMPARE (HANDLE, CHANNEL, COMPARE)这个宏定义函数最终也是调用这个寄存器操作的,所以说我们使用HAL库的函数其实就是间接操作寄存器的。
17.3.4 硬件设计

  1. 例程功能
    通过PWM控制LED0由暗变到亮,然后又从亮变到暗,每个过程大概持续时间大概为3秒钟左右。
  2. 硬件资源
    1)LED灯
    LED0 总线
    PI0 AHB4
    表17.3.4. 1 LED硬件资源
    2)定时器5输出通道4(TIM5_CH4)
    从核心板原理图看出,PI0和TIM5_CH4存在复用关系,我们在STM32CubeMX上把PI0配置为TIM5_CH4即可,然后程序控制TIM5_CH4输出PWM波形,从而控制LED0。

图17.3.4. 1 PI0引脚部分原理图
从《STM32MP157A&D数据手册》中也可以查阅PI0的复用关系:

图17.3.4. 2数据手册部分截图
3. 原理图
定时器属于STM32MP157的内部资源,只需要软件设置好即可正常工作。
17.3.5 软件设计
PI0引脚上接的是LED0,如果PI0上输出低电平,则LED0点亮,我们称LED0电平为低电平有效,如果PI0输出的是高电平,则LED0熄灭。PI0引脚可以复用为TIM5_CH4,即定时器5的PWM通道4,通过通道输出PWM高低电平来控制LED0点亮和熄灭,通过控制PWM的占空比来控制LED0的有效电平。本节实验配置步骤为:
1)通道选择:配置PI0引脚复用为TIM5_CH4;
2)时基配置:配置TIM5的预分频器PSC、自动重载TIMx_ARR值、计数模式、PWM模式、TIM5时钟源选择以及比较值等参数;
3)时钟树配置;
4)生成初始化代码;
5)添加用户代码,实现动态改变占空比,从而控制LED0灯的亮灭,可以实现类似呼吸灯的效果。

  1. 程序流程图

图17.3.5. 1通用定时器中断实验程序流程图
2. STM32CubeMX配置
(1)配置PI0复用为TIM5_CH4
新建一个工程GTIM_PWM(或者直接在上一章节实验的基础上操作),进入STM32CubeMX插件配置界面后,在Pinout & Configuration处配置PI0复用为TIM5_CH4,如下图所示:

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

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

图17.3.5. 4配置系统时钟
(3)配置TIM5
在TimersTIM5中配置如下,Clock Source选择Internal Clock,表示选择内部时钟,Channel4选择PWM Generation CH4,表示使用TIM5的通道4产生PWM波形,其它选项保持默认配置。

图17.3.5. 5配置TIM5为内部时钟
在Parameter Settings处配置参数如下:

图17.3.5. 6配置计数器参数以及PWM通道参数
Counter Settings用于配置计数器的参数:
Prescaler:配置定时器的分频系数,这里配置209-1;
Counter Mode:定时器的计数模式,我们选择UP,即向上计数模式;
Counter Period:计数周期,即自动重装载值,也就是装在TIM5_ARR的值,这里配置为500-1;
Internal Clock Division (CKD):内部时钟分频因子,这里就不设置分频了;
auto-reload preload:这里设置为Enable,定时器自动重装载使能,即使能TIMx_ARR寄存器进行缓冲;
上述参数可以计算定时器的时钟频率为:

TIM5的溢出时间:

Trigger Output(TRGO) Parameters 用于配置触发输出(TRGO)参数,我们这里不配置。
PWM Generation Channel4用于配置通道4的参数,其中:
Mode:用于配置PWM的模式,这里选择PWM mode 1,即PWM模式1。另外还有PWM模式2,可以理解PWM mode l是与PWM mode 2模式互补的波,PWM模式1为高电平时PWM模式2为低电平,反之亦然。
Pulse (32 bits value):是占空比值,即TIM5_CCR4的值,也就是有效电平的值,可以配置在0-500之间,例如配置0。这里配置250,即占空比为50%。在后面的实验中,我们会对TIMx_CCR4寄存器写入新的值来改变占空比,从而控制LED逐渐点亮和熄灭。
Output compare preload:输出比较预加载项选择Enable,即在定时器工作时是否能修改Pulse的值,如果禁用此项,表示定时器工作时不能进行修改,只能等到更新事件到来的时候才能进行修改,所以这里选择使能。
CH Polarity:输出极性,这里我们选择Low(LED0是低电平有效)。
以上配置中要注意的是输出极性和PWM模式,其中TIM5_CNT为TIM5的计数寄存器,用于计数器计数,TIM5_CCR4为TIM5的比较寄存器,其值由用户设置,如上面设置为250。
板子上的LED0是低电平有效,如果输出极性选择为Low:
在PWM模式1下当向上计数时,如果TIM5_CNT<TIM5_CCR4时,通道4为有效电平,否则为无效电平;在向下计数时,如果TIM5_CNT>TIM5_CCR4时通道4为无效电平,否则为有效电平。
如果在PWM模式2下,在向上计数时,如果TIM5_CNT<TIM5_CCR4时通道4为无效电平,否则为有效电平;在向下计数时,如果TIM5_CNT>TIM5_CCR4时通道4为有效电平,否则为无效电平。
例如配置为PWM模式2、向上计数、比较值始终为600、自动重装载值为500、输出极性为低,如果这么配置的话,计数器最大值只能为500,永远小于600,即TIM5_CNT<TIM5_CCR4,通道4电平为无效值,所以LED0灯永远不会亮。
(4)配置IO为上拉
我们配置定时器复用的IO口为上拉模式,即开启内部上拉,我们也在第12.4小节分析过为什么要开启上拉。

图17.3.5. 7配置GPIO为上拉
(5)配置生成独立的文件
配置生成独立的.c和.h头文件,如下图:

图17.3.5. 8配置生成独立的.c和.h文件
2. 生成工程
本章节这里我们不使用中断的方式,配置好后,按下“Ctrl+S”保存修改配置,生成工程:

图17.3.5. 9生成工程
3. 添加用户代码
在main.c文件中添加如下代码:

/* USER CODE BEGIN 0 */
uint16_t ledrpwmval = 0;
uint8_t dir = 1;
/* USER CODE END 0 *//* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_4);/* 开启PWM通道 */
/* USER CODE END 2 */
/*在while循环中添加 */
HAL_Delay(10);
if (dir)ledrpwmval++;                 /* dir==1 ledrpwmval递增  */
else ledrpwmval--;                      /* dir==0 ledrpwmval递减  */if (ledrpwmval > 300)dir = 0;       /* ledrpwmval达到300以后,方向为递减 */
if (ledrpwmval == 0)dir = 1;         /* ledrpwmval递减 到0后,方向改为递增  */(TIM5->CCR4)=ledrpwmval;               /* 修改比较值,修改占空 比 */

/* 调用HAL库API函数来修改占空 比 */
//__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_4, ledrpwmval);

图17.3.5. 10 main.c代码位置
以上代码中,第95行,调用HAL_TIM_PWM_Start函数来开启PWM通道,如果不添加这行代码,则PWM无法正常输出,实际上HAL_TIM_PWM_Start函数也是通过调用TIM_CcxChannelCmd函数来使能定时器的输出通道的。
第106~110行,当dir=1的时候,ledrpwmval递增,当dir=0的时候,ledrpwmval递减。ledrpwmval一开始配置为0,当其从0递增到300的时候,dir等于0,然后ledrpwmval就又递减。这几行代码就是控制ledrpwmval从0递增到300以后,再从300递减到0。
第112行,将不断变化的ledrpwmval值写入TIM5的TIMx_CCR4寄存器中实现动态修改占空比,占空比变化以后,LED0灯就会先逐渐变亮,再逐渐变暗,如此反复。
第112行的代码也可以用114行的来代替,两行代码本质是一样的,114行的是使用HAL库里封装好的API函数来实现,112行的则直接操作寄存器实现。
本小节开头我们就说过PWM波频率由自动重载寄存器(TIMx_ARR)的值决定,其占空比则由捕获/比较寄存器(TIMx_CCRx)的值决定,实验中LED0是低电平有效的,且设置为向上计数模式。下面我们取比较值ledrpwmval为100(也就是TIM5_CCR4的值)时的情况,计算看看占空比。
如果输出极性(也叫比较极性)为低,当计数器TIM5_CNT的值小于100时,通道4为有效电平,LED0亮;当计数器的值大于100时,通道4为无效电平,LED0灭:

如果输出极性为高,当计数器的值小于100时,通道4为无效电平,LED0灭;当计数器的值大于100时,通道4为有效电平,LED0亮:

可以看到输出比较极性为低和输出比较极性为高的占空比正好反过来。
感兴趣大家可以用示波器进行验证。
4. 编译测试
下载代码后,我们将看LED0不停的由暗变到亮,然后又从亮变到暗。每个过程持续时间大概为3秒钟左右(0.005s3002=3s)。
本实验也可以使用中断的方式来做,直接参考上一章节17.2小节的来做,只需要在回调函数中编写配置TIM5_CCR4变化的值即可实现,大家可以尝试,我们已经把工程放到开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程目录下了,文件夹名字为GTIM_PWM_INT。
17.3.6 工程代码分析

  1. tim.c文件
    tim.c文件是用于初始化定时器的,代码如下,已经附上详细的注释:
1   #include "tim.h"
2
3   TIM_HandleTypeDef htim5;                            /* 定时器5句柄 */
4   /**
5    * @brief  通用定时器TIM5通道4 PWM输出 初始化函数(使用PWM模式1)
6    * @note
7    * 通用定时器的时钟来自APB1,当APB1DIV≥2分频的时候
8    * 通用定时器的时钟为APB1时钟的2倍, 而APB1为104.5M, 所以定时器时钟 = 209Mhz
9    * 定时器溢出时间计算方法: Tout = ((ARR + 1) * (psc + 1)) / Ft us.
10   * Ft=定时器工作频率,单位:Mhz
11   * @retval   无
12   */
13  void MX_TIM5_Init(void)
14  {15    /* 定义时钟配置结构体变量sClockSourceConfig */
16    TIM_ClockConfigTypeDef sClockSourceConfig = {0};
17    /* 定义TIM主站配置结构体变量sMasterConfig */
18    TIM_MasterConfigTypeDef sMasterConfig = {0};
19    /* 定义TIM输出比较配置结构体变量sConfigOC */
20    TIM_OC_InitTypeDef sConfigOC = {0};
21
22    htim5.Instance = TIM5;                               /* 定时器5 */
23    htim5.Init.Prescaler = 209-1;                        /* 定时器分频 */
24    /* 向上计数模式 */
25    htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
26    htim5.Init.Period = 500-1;                           /* 自动重装载值 */
27    /* 时钟分频因子 */
28    htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
29    /* TIM5自动重载使能 */
30    htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
31    /* 初始化定时器时基 */
32    if (HAL_TIM_Base_Init(&htim5) != HAL_OK)
33    {34      Error_Handler();
35    }
36    /* TIM5时钟源选择,使用内部时钟 */
37    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
38    /* 初始化时钟 */
39    if (HAL_TIM_ConfigClockSource(&htim5, &sClockSourceConfig) != HAL_OK)
40    {41      Error_Handler();
42    }
43    /* 初始化PWM */
44    if (HAL_TIM_PWM_Init(&htim5) != HAL_OK)
45    {46      Error_Handler();
47    }
48    /* 主模式触发输出(TRGO)选择选择为复位  */
49    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
50    /* 主/从模式选择关闭 */
51    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
52    /* 主模式配置 */
53    if (HAL_TIMEx_MasterConfigSynchronization(&htim5, \                                                               &sMasterConfig) != HAL_OK)
54    {55      Error_Handler();
56    }
57    sConfigOC.OCMode = TIM_OCMODE_PWM1;/* PWM模式1 */
58    sConfigOC.Pulse = 250;/* 占空比值 */
59    /* 输出极性,这里选择低 */
60    sConfigOC.OCPolarity = TIM_OCPOLARITY_LOW;
61    /*指定快速模式状态不开启 */
62    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
63    /* 初始化PWM通道 */
64    if (HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, \                                                                   TIM_CHANNEL_4) != HAL_OK)
65    {66      Error_Handler();
67    }
68    /* 调用PWM输出通道引脚初始化函数 */
69    HAL_TIM_MspPostInit(&htim5);
70  }
71
72  /* 使能TIM5时基 */
73  void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
74  {75    if(tim_baseHandle->Instance==TIM5)
76    {77      /* TIM5时钟使能 */
78      __HAL_RCC_TIM5_CLK_ENABLE();
79    }
80  }
81
82  /**
83   * @brief       通用定时器PWM输出通道引脚初始化函数
84   * @param       timHandle:定时器句柄
85   * @note        此函数会被MX_TIM5_Init调用
86   * @retval      无
87   */
88  void HAL_TIM_MspPostInit(TIM_HandleTypeDef* timHandle)
89  {90    GPIO_InitTypeDef GPIO_InitStruct = {0};
91    if(timHandle->Instance==TIM5)
92    {93      __HAL_RCC_GPIOI_CLK_ENABLE();
94      /**TIM5 GPIO Configuration
95      PI0     ------> TIM5_CH4
96      */
97      /* 配置通道4的GPIO口 */
98      GPIO_InitStruct.Pin = GPIO_PIN_0;
99      /* 复用推挽输出 */
100     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
101     /* 上拉 */
102     GPIO_InitStruct.Pull = GPIO_PULLUP;
103     /* 高速 */
104     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
105     /* TIM5通道4的GPIO口复用 */
106     GPIO_InitStruct.Alternate = GPIO_AF2_TIM5;
107     /* 初始化GPIO */
108     HAL_GPIO_Init(GPIOI, &GPIO_InitStruct);
109   }
110 }
111
112 /**
113  * @brief       通用定时器反初始化
114  * @param       tim_baseHandle:定时器句柄
115  * @retval      无
116  */
117 void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle)
118 {119   if(tim_baseHandle->Instance==TIM5)
120   {121     /* 关闭TIM5的时钟 */
122     __HAL_RCC_TIM5_CLK_DISABLE();
123   }
124 }
第13~80行是定时器初始化函数,其中第16~20行定义和TIM5相关的结构体变量,并初始化为0。
第22行,通过TIM_TypeDef结构体成员Instance赋值,指定配置TIM5,定时器的寄存器结构体在stm32mp157dxx_cm4.h文件中有定义。
第23行,配置TIM5分频为209-1;
第25行,配置TIM5位向上计数模式;
第26行,配置TIM5的自动重装载值为500-1;
第28行,配置TIM5的时钟分频因子,选择不分频;
第30行,使能TIM5自动重装载,即定时器溢出时会自动重装初值;
第32行,根据前面第23~30行的参数,调用HAL_TIM_Base_Init函数完成定TIM5的时基初始化,使用计时器生成简单的时基;
第37行,我们前面说过定时器时钟源可以选择内部时钟(CK_INT)、外部时钟模式 1、外部时钟模式 2和内部触发输入 (ITRx),这里配置定时器选择内部时钟;
第39行,根据第37行内部时钟来初始化TIM5时钟;
第44行,初始化TIM5的PWM;
第49~56行,定时器主模式配置,触发输出(TRGO)选择选择为复位,主/从模式选择关闭,因为这里只是使用一个定时器,所以没有从定时器;
第57行,配置TIM5的通道4为PWM模式1;
第58行,指定要加载到捕获比较寄存器的脉冲值,即配置PWM的占空比,这里配置为250,在本实验中此参数可以随意配置为其它值,只是用于初始化定时器,后面会在main.c文件中通过程序动态修改此值(通过ledrpwmval变量);
第60行,配置PWM的输出极性,这里配置为低,关于极性以及计数模式,我们前面有分析过;
第64行,根据前面第57~62行的参数初始化PWM通道;
第69行,调用HAL_TIM_MspPostInit函数来初始化PWM通道所对应的引脚,我们来看看此函数。
第88~110行,HAL_TIM_MspPostInit函数用于初始化PWM输出通道的引脚,即配置通道4对应的IO口为复用推挽输出、上拉、高速、模式。
第73~80行,调用__HAL_RCC_TIM5_CLK_ENABLE宏来使能TIM5的时钟。我们在始终系统章节有分析过,STM32默认将所有外设的时钟关闭,如果要使用某个外设,则先开启其时钟才可以使用。
第117~124行,TIM5的反初始化函数,前面是使能TIM5,这里是关闭TIM5,如果有需要,程序可以调用此函数来禁用TIM5。
  1. gpio.c文件
    gpio.c文件内容很简单,MX_GPIO_Init函数会在main.c文件中调用,用于开启GPIO时钟,如下分别开启GPIOI和GPIOH的时钟,因为实验中使用的定时器5通道4和PI0是复用的,因为HSE的两个引脚PH0-OSC_IN和PH1-OSC_OUT挂在GPIOH上,所以也要开启GPIOH的时钟。
#include "gpio.h"void MX_GPIO_Init(void)
{/* GPIO时钟使能 */__HAL_RCC_GPIOI_CLK_ENABLE();__HAL_RCC_GPIOH_CLK_ENABLE();
}
本实验的相关代码就分析到这里了,main.c文件的代码我们就不分析了,因为大部分的代码我们在前面的实验章节都已经分析了。

17.4 通用定时器输入捕获实验
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 10-4 GTIM_CAP。
17.4.1 输入捕获原理

  1. 输入捕获简介
    本小节我们来学习如何使用通用定时器的输入捕获模式。输入捕获就是指对TIMx_CHy通道上的输入信号的上升沿、下降沿或者双边沿进行捕获/检测,在边沿信号发生跳变(比如上升沿/下降沿)的时候捕获到这些信号,将计数器的计数值TIMx_CNT保存到对应通道的捕获/比较寄存器中(TIMx_CCRy)完成一次捕获。在捕获模式中还可以配置捕获时是否触发中断/DMA等以完成捕获的一些响应。下面我们以一个周期的脉冲为例说明:

图17.4.1. 1输入捕获示意图
计数器以一定频率工作,根据计数器的工作频率可以计算出计数器每计数一次(一个节拍)所使用的时间t。如上图,当捕获到一个上升沿,记录计数器的计数值为CNT1,接着捕获到一个下降沿,记录计数器的计数值为CNT2。两次计数器的差值(CNT2-CNT1)就是两次捕获之间计数器计数了多少个节拍,t*(CNT2-CNT1)就是此高电平脉冲的宽度。同理,要测量低电平脉冲的宽度,可以是t*(CNT3-CNT2)。此脉冲的周期是,t*(CNT3-CNT1),频率是1/(t*(CNT3-CNT1))。如果捕获的脉宽的宽度超过了捕获定时器的周期,那就会发生溢出,必须对溢出做处理,否则测试的数据不准确。我们用一个简图来详细说明输入捕获的原理,如下图所示:

图17.4.1. 2输入捕获脉宽测量原理
图中ARR是自动重载寄存器(TIMx_ARR)的值,CCRx是捕获时计数器(TIMx_CNT)的值。
上图就是输入捕获测量高电平脉宽的原理,假定定时器工作在向上计数模式,图中t1t2的时间,就是我们需要测量的高电平时间。测量方法如下:首先设置定时器通道x为上升沿捕获,这样,t1时刻,就会捕获到当前的CNT值,然后立即清零CNT,并设置通道x为下降沿捕获,这样到t2时刻,又会发生捕获事件,得到此时的CNT值,记为CCRx2。这样,根据定时器的计数频率,我们就可以算出t1t2的时间,从而得到高电平脉宽。
在t1t2之间,可能产生N次定时器溢出,这就要求我们对定时器溢出做处理,防止高电平太长,导致数据不准确。t1t2之间,CNT计数的次数等于:N*ARR+CCRx2,有了这个计数次数,再乘以CNT的计数周期,即可得到t2-t1的时间长度,即高电平持续时间。输入捕获的原理,我们就介绍到这。
2. 输入捕获框图

图17.4.1. 3输入捕获框图
输入通道
如上图,需要测试的信号从定时器通道TIMx_CH1~TIMx_CH4中输入,例如从TIMx_CH1通道输入的信号T1x。
捕获通道
输入的信号经过滤波采样去除掉干扰信号后,生成一个信号TIxF,TIxF再经过带有极性选择功能的边沿检测器生成一个信号 (TIxFPx),该信号可用作从模式控制器的触发输入,然后从从模式控制器输出ICx,我们称ICx为捕获通道。
捕获比较模块
从捕获通道ICx出来的信号先进行预分频后输出 ICxPS信号,此信号最终进入到捕获比较模块中,捕获比较模块由一个预装载寄存器和一个影子寄存器组成,可通过读写操作访问预装载寄存器实现捕获/比较处理。在捕获模式下,捕获实际发生在影子寄存器中,然后将影子寄存器的内容复制到预装载寄存器中。在比较模式下,预装载寄存器的内容将复制到影子寄存器中,然后将影子寄存器的内容与计数器进行比较。
在输入捕获模式下,当相应的ICx信号第一次检测到跳变沿后,将使用捕获/比较寄存器 (TIMx_CCRx)来锁存计数器的值,发生捕获事件时,硬件会将TIMx_SR 寄存器相应的捕获/比较中断标志CCXIF标志位置1,从而触发中断(如果已经使能了中断),可以通过软件将CCxIF清零或者通过读取TIMx_CCRx中的值也可以将CCxIF清零。前面是第一次捕获,如果发生第二次或者多次捕获(重复捕获),如果CCxIF 标志未被清零,这样 CCxOF 重复捕获标志会被置 1,CCxOF只能通过软件清零。
要处理重复捕获,建议在读出重复捕获标志之前读取数据,这样可避免在读取重复捕获标志之后与读取数据之前可能出现的重复捕获信息丢失。
输出
输出通道也是TIMx_CH1~TIMx_CH4中的某个,通过将TIMx_CCER寄存器CCxE位置1,使在相应输出引脚上输出OCx信号。
17.4.2 TIM2/TIM3/TIM4/TIM5寄存器
本章实验会使用通用定时器5的通道1,通用定时器输入捕获实验需要用到的寄存器有:TIMx_ARR、TIMx_PSC、TIMx_CCMR1、TIMx_CCER、TIMx_DIER、TIMx_CR1、TIMx_CCR1这些寄存器在前面的章节都有提到,在这里只需针对性的介绍。

  1. 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
    该寄存器我们在PWM输出实验时讲解了他作为输出功能的配置,现在重点学习输入捕获模式的配置,因为本实验我们用到定时器5通道1输入,所以我们要看TIMx_CCMR1寄存器。相同的寄存器可用于输入捕获模式或输出比较模式。

图17.4.2. 1 《STM32MP157参考手册》部分截图
其描述如下图所示:

图17.4.2. 2 TIMx_CCMR1寄存器
TIMx_CCMR1的低16位用于配置通道1和通道2,低八位[7:0]用于捕获/比较通道1的控制,而高八位[15:8]则用于捕获/比较通道2的控制,因为TIMx还有CCMR2这个寄存器,所以可以知道CCMR2是用来控制通道3和通道4。我们用到定时器5通道1输入,重点关注TIMx_CCMR1的[7:0]位(其[8:15]位配置类似)。
CC1S[1:0]
这两个位用于捕获/比较1选择(CC1S),该位字段定义通道的方向(输入/输出)以及使用的输入,将该位配置:
00:将CC1通道配置为输出(CC1表示捕获比较通道1);
01:将CC1通道配置为输入,IC1映射到TI1;
10:将CC1通道配置为输入,将IC1映射到TI2;
11:将CC1通道配置为输入,将IC1映射到TRC。仅当通过TIMx_SMCR寄存器的TS位选择了内部触发输入时,该模式才有效。
注:仅当通道为OFF(TIMx_CCER中的CC1E = 0)时,CC1S位才可写。
本章节实验是输入捕获实验,所以将设置IC1S[1:0]=01,也就是配置IC1映射在TI1上,即CCR1对应TIMx_CH1
IC1PSC[1:0]
配置输入捕获1预分频器,用于CC1输入(IC1)的预分频器的比率,将该位配置:
00:没有预分频器,每次在捕获输入上检测到边沿时都进行捕获
01:每2个事件捕获一次
10:每4个事件捕获一次
11:每8个事件捕获一次
本章节实验,我们是来1次边沿就触发1次捕获,所以选择00。
IC1F [3:0]
输入捕获 1 滤波器,可定义 TI1 输入的采样频率和适用于 TI1 的数字滤波器带宽。数字滤波器由事件计数器组成,每N个连续事件才视为一个有效输出边沿:
0000:无滤波器,按 f DTS 频率进行采样;
0001:f SAMPLING =f CK_INT ,N=2;
0010:f SAMPLING =f CK_INT ,N=4;

1110:f SAMPLING =f DTS /32,N=6
1111:f SAMPLING =f DTS /32,N=8
其中,f SAMPLING 是采样频率,fCK_INT是定时器的输入频率,这里为209Mhz,而fDTS则是根据TIMx_CR1的CKD[1:0]的设置来确定的,如果CKD[1:0]设置为00,那么fDTS=fCK_INT。N值就是滤波长度。
举个简单的例子:假设IC1F[3:0]=0011,并设置IC1映射到通道1上,且为上升沿触发,那么在捕获到上升沿的时候,再以fCK_INT的频率,连续采样到8次通道1的电平,如果都是高电平,则说明却是一个有效的触发,就会触发输入捕获中断(如果开启了的话)。这样可以滤除那些高电平脉宽低于8个采样周期的脉冲信号,从而达到滤波的效果。
本章节,我们不做滤波处理,所以设置IC1F[3:0]=0000,只要采集到上升沿,就触发捕获。
2. 捕获/比较使能寄存器(TIMx_ CCER)
TIM2/TIM3/TIM4/TIM5的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述下图所示:

图17.4.2. 3 TIMx_CCER寄存器
此寄存器每3位控制一个通道,我们使用的是定时器1,只介绍通道1相关部分。
CC1E:是捕获/比较1输出使能,当CC1通道配置为输入时,此位为0表示禁止捕获,此位为1表示使能捕获;
CC1P:用于配置捕获/比较1输出极性,当CC1通道配置为输入时:
00:未反相/上升沿触发;
01:反相/下降沿触发;
10:保留,不使用此配置;
11:未反相/上升沿和下降沿均触发。
CC1NP:用于配置捕获/比较1互补输出极性,当CC1通道配置为输入时,该位与 CC1P 配合使用可定义TI1FP1/TI2FP1极性。
我们要用到这个寄存器的最低2位,CC1E和CC1P位。要使能输入捕获,必须设置CC1E=1,而CC1P则根据自己的需要来配置。我们这里是保留默认设置值0,即高电平触发捕获。
3. DMA/中断使能寄存器(TIMx_DIER)
接下来我们再看看DMA/中断使能寄存器:TIMx_DIER,该寄存器的各位描述如下图:

图17.4.2. 4 TIMx_DIER寄存器
UIE位表示更新中断使能。0:禁止更新中断;1:使能更新中断。
CC1IE位用于捕获/比较1中断使能。0:禁止CC1中断;1:使能CC1中断。
CC2IE位用于捕获/比较2中断使能。0:禁止CC2中断;1:使能CC2中断。
本小节,我们需要用到中断来处理捕获数据,所以必须开启通道1的捕获比较中断,即CC1IE设置为1。同时我们还需要在定时器溢出中断中累计定时器溢出的次数,所以还需要使能定时器的更新中断,即UIE置1。
4. 控制寄存器(TIMx_CR1)

图17.4.2. 5 TIMx_CR1寄存器
TIMx_CR1我们只用到了它的最低位,也就是用来使能定时器。将第0位置1,表示使能计数器,清零表示关闭计时器。
5. 捕获/比较寄存器1(TIMx_CCR1)

图17.4.2. 6 TIMx_CCR1寄存器
最后再来看看捕获/比较寄存器1:TIMx_CCR1,该寄存器用来存储通道1捕获发生时,TIMx_CNT的值,我们从TIMx_CCR1就可以读出通道1捕获发生时刻的TIMx_CNT值,通过两次捕获(一次上升沿捕获,一次下降沿捕获)的差值,就可以计算出高电平脉冲的宽度(注意,对于脉宽太长的情况,还要计算定时器溢出的次数)。
17.4.3 定时器的HAL库驱动

  1. HAL_TIM_IC_Init函数
    定时器的输入捕获模式初始化函数,其声明如下:
    HAL_StatusTypeDef HAL_TIM_IC_Init(TIM_HandleTypeDef *htim)
    函数描述:
    用于初始化定时器的输入捕获模式。
    函数形参:
    形参1是TIM_HandleTypeDef结构体类型指针变量,基本定时器的时候已经介绍此结构体。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)。
    注意事项:
    与PWM输出实验一样,当使用定时器做输入捕获功能时,在HAL库中并不使用定时器初始化函数HAL_TIM_Base_Init来实现,而是使用输入捕获特定的定时器初始化函数HAL_TIM_IC_Init。该函数内部还会调用输入捕获初始化回调函数HAL_TIM_IC_MspInit来初始化输入通道对应的GPIO(复用),以及输入捕获相关的配置。
    2.HAL_TIM_IC_ConfigChannel函数
    定时器的输入捕获通道设置初始化函数。其声明如下:
    HAL_StatusTypeDef HAL_TIM_IC_ConfigChannel(TIM_HandleTypeDef *htim,
    TIM_IC_InitTypeDef *sConfig, uint32_t Channel);
    函数描述:
    该函数用于设置定时器的输入捕获通道。
    函数形参:
    形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数,我们前面也介绍过此结构体,这里就不再重复介绍了。
    形参2是TIM_IC_InitTypeDef结构体类型指针变量,用于配置定时器的输入捕获参数。
    重点了解一下TIM_IC_InitTypeDef结构体指针类型,其定义如下:
typedef struct
{uint32_t ICPolarity;       /* 输入捕获触发方式选择,比如上升、下降和双边沿捕获 */uint32_t ICSelection;  /* 输入捕获选择,用于设置映射关系 */uint32_t ICPrescaler;   /* 输入捕获分频系数 */uint32_t ICFilter;        /* 输入捕获滤波器设置 */
} TIM_IC_InitTypeDef;

该结构体成员我们主要设置前三个成员变量。
成员变量ICSelection用来设置映射关系,我们配置IC1直接映射在TI1上,所以此成员就选择TIM_ICSELECTION_DIRECTTI。其它映射关系还可以选择以下这些宏:
/* 选择TIM输入1、2、3或4分别连接到IC1,IC2,IC3或IC4 /
#define TIM_ICSELECTION_DIRECTTI TIM_CCMR1_CC1S_0
/
选择TIM输入1、2、3或4分别连接到IC2,IC1,IC4或IC3 /
#define TIM_ICSELECTION_INDIRECTTI TIM_CCMR1_CC1S_1
/
选择TIM输入1、2、3或4连接到TRC */
#define TIM_ICSELECTION_TRC TIM_CCMR1_CC1S
成员变量ICPrescaler用来设置输入捕获分频系数,可以设置为:

#define TIM_ICPSC_DIV1  0x00000000U              /* 不分频 */
#define TIM_ICPSC_DIV2  TIM_CCMR1_IC1PSC_0          /* 2分频 */
#define TIM_ICPSC_DIV4  TIM_CCMR1_IC1PSC_1          /* 4分频 */
#define TIM_ICPSC_DIV8  TIM_CCMR1_IC1PSC            /* 8分频 */

本实验需要设置为不分频,所以选值为TIM_ICPSC_DIV1。
成员变量ICFilter用来设置滤波器长度,这里我们不使用滤波器,所以设置为0。
形参3是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_6,比如定时器5只有4个通道,那就选择范围只有TIM_CHANNEL_1到TIM_CHANNEL_4,就具体情况选择。
函数返回值:
HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)。
3. HAL_TIM_IC_Start_IT函数
启动定时器输入捕获模式函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_IC_Start_IT(TIM_HandleTypeDef *htim,uint32_t Channel);
函数描述:
用于启动定时器的输入捕获模式,且开启输入捕获中断。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
函数返回值:
HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)。
注意事项:
如果我们不需要开启输入捕获中断,只是开启输入捕获功能,可以使用HAL库函数:
HAL_StatusTypeDef HAL_TIM_IC_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
4. HAL_TIM_IC_Start函数
使能输入捕获功能,使能以后,输入捕获的功能才可以打开。
HAL_StatusTypeDef HAL_TIM_IC_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
函数参数和返回值上面的HAL_TIM_IC_Start_IT函数一样。
函数描述:
用于启动定时器的输入捕获模式,此函数没有开启中断。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_6。
函数返回值:
HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)。
5. HAL_TIM_ReadCapturedValue函数
uint32_t HAL_TIM_ReadCapturedValue(TIM_HandleTypeDef *htim, uint32_t Channel)
函数描述:
用于获取定时器对应通道当前的捕获值,也就是TIMx_CCRx的值。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
函数返回值:
HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)。
17.4.4 硬件设计

  1. 例程功能
    使用TIM5_CH1来做输入捕获,捕获PA0上的高电平脉宽,并将脉宽时间通过串口打印出来,然后通过按WKUP按键,模拟输入高电平,这里能测试的最长时间为:4194303 us。同时LED0闪烁指示程序在运行。
  2. 硬件资源
    1)LED0灯UART4和WKUP按键(通过按键给PA0输入高脉冲)
    LED0 WKUP UART4_TX UART4_RX
    PI0 PA0 PG11 PB2
    表17.4.4. 1硬件资源
    2)定时器5输出通道1(TIM5_CH1)
    定时器属于STM32MP157的内部资源,只需要软件设置好即可正常工作。
  3. 原理图
    从核心板原理图看出,PA0上接了WKUP按键,此按键高电平有效(按键按下,对应的IO口即为高电平)。PA0还可以复用为TIM5_CH1。程序上通过配置PA0复用为TIM5_CH1,然后按下WKUP按键后,PA0就输入一个高电平脉冲,调用HAL库的函数,实现串口上位机监测定时器输入捕获的情况。

图17.4.4. 1引脚部分原理图
从《STM32MP157A&D数据手册》中也可以查阅PI0的复用关系:

图17.4.4. 2《STM32MP157A&D数据手册》部分截图
17.4.5 软件设计

  1. 程序流程图

图17.4.5. 1通用定时器中断实验程序流程图
2. STM32CubeMX配置
(1)配置PI0复用为TIM5_CH1
新建一个工程GTIM_CAP(或者直接在第十四章节串口通信实验的基础上操作,因为本实验中会用到UART4),进入STM32CubeMX插件配置界面后,在Pinout & Configuration处配置PA0复用为TIM5_CH1,如下图所示:

图17.4.5. 2 配置PA0复用为TIM5的通道1
本实验还会用到LED0、UART4,前面的实验我们有配置过,如下:

图17.4.5. 3 配置PI0为输出

图17.4.5. 4配置UART4两个引脚
(2)配置TIM5时基和捕获参数
在TimersTIM5中配置如下,Clock Source选择Internal Clock,表示选择内部时钟,Channel1选择Input Capture direct mode,表示使用TIM5的通道1输入捕获模式:

图17.4.5. 5 配置TIM5通道1
上图的参数介绍如下:
Counter Settings(计数器)配置如下:
Prescaler用于配置定时器预分频值,这里配置为209-1;
Counter Mode用于配置计数模式,我们选择向上计数Up;
Counter Period用于配置定时器自动重装载值,我们设置为65536-1(或者写为0Xffff-1),即只是用了低16位。也可以设置最大为0Xffffffff-1,因为TIM5是32位的。
Internal Clock Division (CKD)配置为No Division即内部时钟不分频;
auto-reload preload用于配置自动重载是否使能,我们选择 Enable使能自动重载
上述参数可以计算定时器的时钟频率为:

即计数器每计数一下(一个节拍),时间为1us。
Trigger Output (TRGO)Parameters参数配置如下:

Master/Slave Mode(MSM bit)用于配置主/从模式,这里不使用主从模式,所以配置为Disable(Trigger input effect not delayed);
Trigger Event Selection TRGO 用于配置触发事件选择,这里选Reset(UG bit from TIMX_EGR)
Input Capture Channel1参数配置如下:
Polarity Selection用于配置极性选择,这里配置上升沿;
IC Selection 配置为Direct,即配置IC1直接映射在TI1上;
Prescaler Division Ratio用于配置配置输入分频,这里选择为No division,即不分频;
Input Filter (4 bits value) 配置输入滤波器,这里配置为0,即不滤波。
(3)配置UART4参数
UART4配置如下:

图17.4.5. 6配置UART4
(4)配置GPIO
因为按键WKUP是高电平有效的,PA0配置为下拉:

图17.4.5. 7配置按键的引脚
同时,也要配置UART4和LED0,配置UART参数:

图17.4.5. 8配置UART4的两个引脚

图17.4.5. 9配置LED0引脚
(5)配置NVIC
本节实验我们用到串口接收数据,会用到串口接收中断,同时也要用到定时器输入捕获中断来完成捕获功能。
先开启TIM5的全局中断,开启以后才可以设置中断:

图17.4.5. 10开启TIM5的全局中断
接下来开启UART4的全局中断:

图17.4.5. 11开启UART4的全局中断
然后设置中断优先级,如下,我们设置中断优先级分组为2,TIM5的抢占优先级和子优先级分别为2和0,UART4的抢占优先级和子优先级分别为3和3:

图17.4.5. 12配置TIM5和UART4的抢占优先级和子优先级
(6)配置时钟
本实验我们采用外部24MHz的时钟HSE(也可以采用内部时钟),配置时钟树,经过PLL3锁相环以后,APB1的时钟频率为最大209MHz(也可以配置其它频率):

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

图17.4.5. 14配置系统时钟
UART4的时钟频率为104.5MHz。

图17.4.5. 15 UART4的时钟为104.5MHz
(5)配置生成独立的文件
配置生成独立的.c和.h头文件,如下图:

图17.4.5. 16配置生成独立的.c和.h文件
2.生成工程
配置好后,按下“Ctrl+S”保存修改配置,生成工程,如下:

图17.4.5. 17生成工程
3. 添加LED0驱动代码
将跑马灯实验的BSP文件夹拷贝到工程中:

图17.4.5. 18添
加BSP文件夹
本实验中我们只用到LED0,用不到LED1,所以将LED1相关的代码删除掉,如下:

led.c文件代码
#include "./Include/led.h"
#include "tim.h"
void led_init(void)
{LED0(1);       /* 关闭 LED0 */
}led.h代码
#ifndef __LED_H
#define __LED_H#include"gpio.h"
/* LED端口定义 */
#define LED0(x)   do{ x ? \HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin,                              GPIO_PIN_SET) : \HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin,                                 GPIO_PIN_RESET); \}while(0)
/* LED取反定义 */
#define LED0_TOGGLE()    do{ HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin); }while(0)     /* LED0 = !LED0 */void led_init(void);     /* 初始化 */#endif
  1. 定义UART4接收回调函数
    我们前面的实验有操作过UART4,可以直接将串口通信实验的usart.c文件的代码拷贝过来,如果是在串口通信实验章节的工程中操作本章实验的话,此步骤可以忽略。本工程中,在usart.c文件中的“USER CODE BEGIN 1”和中“USER CODE END 1”位置处添加如下代码,也就是回调函数:
/* USER CODE BEGIN 1 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle)
{HAL_UART_Transmit(&huart4,&RxBuffer,1,0);      /* 发送字符 */HAL_UART_Receive_IT(&huart4,&RxBuffer,1);     /* 打开对应的串口中断 */
}
/* USER CODE END 1 */
  1. tim.c初始化函数
    tim.c文件中的初始化代码如下,代码中已经附上详细的注释,可以方便我们快速浏览。第3和第4行的代码是我们手动添加的,其它代码是根据我们在STM32CubeMX中配置的参数自动生成的。
1   #include "tim.h"
2
3   uint8_t capture_state = 0;     /* 输入捕获状态 */
4   uint16_t capture_val=0 ;       /* 输入捕获值 */
5
6   TIM_HandleTypeDef htim5;            /* 定时器5句柄 */
7
8    /**
9    * @brief  通用定时器TIM5通道1输入捕获初始化接口
10   * @note   这里配置定时器1MHz的计数频率,即计数器计数一个节拍为1us
11   * 通用定时器的时钟来自APB1,当APB1DIV≥2分频的时候
12   * 通用定时器的时钟为APB1时钟的2倍, 而APB1为104.5M, 所以定时器时钟 = 209Mhz
13   * 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
14   * Ft=定时器工作频率=209MHz/209=1MHz;
15   * @retval      无
16   */
17  void MX_TIM5_Init(void)
18  {19    /* 声明TIM_ClockConfigTypeDef结构体变量,并初始化为0 */
20    TIM_ClockConfigTypeDef sClockSourceConfig = {0};
21    /* 声明TIM_MasterConfigTypeDef结构体变量,并初始化为0 */
22    TIM_MasterConfigTypeDef sMasterConfig = {0};
23    /* 声明TIM_IC_InitTypeDef结构体变量,并初始化为0 */
24    TIM_IC_InitTypeDef sConfigIC = {0};
25
26    htim5.Instance = TIM5;/* 定时器5 */
27    htim5.Init.Prescaler = 209-1;            /* 定时器分频 */
28    /* 向上计数模式 */
29    htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
30    htim5.Init.Period = 65536-1;         /* 自动重装载值 */
31    /* 时钟分频因子,tDTS=tCK_INT */
32    htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
33    /* 自动重载预装载使能,开启TIMx_ARR缓冲 */
34    htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
35    /* 初始化TIM5时基单元 */
36    if (HAL_TIM_Base_Init(&htim5) != HAL_OK)
37    {38      Error_Handler();
39    }
40    /* 时钟源参数配置:采用内部时钟 */
41    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
42    /* 完成时钟源配置  */
43    if (HAL_TIM_ConfigClockSource(&htim5, &sClockSourceConfig)!= HAL_OK)
44    {45      Error_Handler();
46    }
47    /* 完成TIM5初始化 */
48    if (HAL_TIM_IC_Init(&htim5) != HAL_OK)
49    {50      Error_Handler();
51    }
52    /* TIM5主模式触发输出配置为复位  */
53    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
54    /* TIM5主从模式选择不使能  */
55    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
56    /* TIM5主从模式配置  */
57    if (HAL_TIMEx_MasterConfigSynchronization(&htim5, &sMasterConfig)\!= HAL_OK)
58    {59      Error_Handler();
60    }
61    /* TIM5极性配置为上升沿捕获 */
62    sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
63    /* 配置IC1直接映射在TI1上 */
64    sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
65    /* 每次在捕获输入上检测到边缘时执行捕获 */
66    sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
67    /* 配置输入滤波器,这里配置为0,即不滤波 */
68    sConfigIC.ICFilter = 0;
69    /* 完成TIM5 通道极性配置 */
70    if (HAL_TIM_IC_ConfigChannel(&htim5, &sConfigIC, TIM_CHANNEL_1)\!= HAL_OK)
71    {72      Error_Handler();
73    }
74
75  }
76
77  /**
78   * @brief    通用定时器输入捕获引脚初始化函数
79   * @param    tim_baseHandle:定时器句柄
80   * @note     此函数会被MX_TIM5_Init调用
81   * @retval   无
82   */
83  void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
84  {85    /* 定义GPIO_InitTypeDef结构体变量GPIO_InitStruct */
86    GPIO_InitTypeDef GPIO_InitStruct = {0};
87    /* 配置TIM5 */
88    if(tim_baseHandle->Instance==TIM5)
89    {90      /* 使能TIM5时钟 */
91      __HAL_RCC_TIM5_CLK_ENABLE();
92      /* 使能GPIOA时钟 */
93      __HAL_RCC_GPIOA_CLK_ENABLE();
94      /**TIM5 GPIO Configuration
95      PA0     ------> TIM5_CH1
96      */
97      /* 配置通道1的GPIO口 */
98      GPIO_InitStruct.Pin = GPIO_PIN_0;
99      /* 配置引脚为复用推挽输出 */
100     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
101     /* 配置引脚为下拉 */
102     GPIO_InitStruct.Pull = GPIO_PULLDOWN;
103     /* 配置引脚为高速模式 */
104     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
105     /* 配置TIM5的复用功能 */
106     GPIO_InitStruct.Alternate = GPIO_AF2_TIM5;
107     /* 初始化GPIOA */
108     HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
109
110     /* 配置TIM5中断优先级分组为2 */
111     HAL_NVIC_SetPriority(TIM5_IRQn, 2, 0);
112     /* 使能TIM5中断 */
113     HAL_NVIC_EnableIRQ(TIM5_IRQn);
114   }
115 }
116
117 /**
118  * @brief    通用定时器输入捕获引脚反(去)初始化函数
119  * @param    tim_baseHandle:定时器句柄
120  * @note     如果有必要去初始化TIM5,可以调用此函数
121  * @retval   无
122  */
123 void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle)
124 {125   if(tim_baseHandle->Instance==TIM5)
126   {127     /* 关闭TIM5的时钟 */
128     __HAL_RCC_TIM5_CLK_DISABLE();
129
130     /**TIM5 GPIO Configuration
131     PA0     ------> TIM5_CH1
132     */
133     /* 反初始化PA0引脚 */
134     HAL_GPIO_DeInit(GPIOA, GPIO_PIN_0);
135     /* TIM5中断反初始化 */
136     HAL_NVIC_DisableIRQ(TIM5_IRQn);
137   }
138 }
在上一章节PWM实验中,我们已经基本介绍过初始化函数,这里就不重复讲解了。下面我们完成回调函数,回调函数需要我们手动添加,回调函数我们选择添加在tim.c文件中,当然也可以选择添加在其它文件中。
  1. 定义TIM5相关回调函数
    在tim.h文件中添加如下代码:
/* USER CODE BEGIN Private defines */
extern uint8_t capture_state;           /* 输入捕获状态 */
extern uint16_t capture_val;            /* 输入捕获值 */
/* USER CODE END Private defines */
上述代码定义的这两个变量用于辅助实现高电平捕获,注意使用extern来申明变量,表示在其它文件中也可以使用此变量。变量capture_state用于表示输入捕获的状态,这里将其定位为8位,我们把它当成一个8位的寄存器来用,对其各位赋予状态含义,描述如下表所示:

表17.4.5. 1 capture_state各位描述
变量capture_state的位[5:0]是用于记录捕获高电平定时器溢出次数,总共6位,所以最多可以记录溢出的次数为(26-1)次,即63次,所以最长捕获值= 63*65536+65535 = 4194303。前面配置定时器1MHz的计数频率,所以每计数一个节拍就用时1us,所以最长溢出时间为4194303us,约4.19秒。
注意:为了通用,我们默认ARR和CCRy都是16位寄存器,对于32位的定时器(如:TIM5),也只按16位使用。
第6位用于标志是否有捕获到高/低电平。第7位用于表示是否已经捕获完成,0表示未捕获完成,1表示捕获完成。一个高电平脉冲是有一个上升沿和一个下降沿组成。
capture_val用于记录捕获到下降沿的时候,TIM5_CNT寄存器的值,脉冲捕获成功以后,需要通过capture_val的值计算出脉冲的宽度。
TIM5的中断服务函数已经在stm32mp1xx_it.c文件中有生成了,如下:

/* 定时器中断服务函数 */
void TIM5_IRQHandler(void)
{/* 定时器中断请求函数 */HAL_TIM_IRQHandler(&htim5);
}
如果计数器计数时,当TIMx_CNT 的值和TIMx_ARR相等时计数器发生溢出,产生一个溢出中断(更新中断)。如果定时器配置为输入捕获,极性为上升沿,当来一个上升沿脉冲时产生一个捕获中断。这两个中断对应的中断服务函数是stm32mp1xx_it.c文件中的TIM5_IRQHandler函数,而TIM5_IRQHandler函数调用定时器中断请求函数HAL_TIM_IRQHandler。
在中断请求函数HAL_TIM_IRQHandler中,通过调用TIM5更新中断回调函数HAL_TIM_PeriodElapsedCallback来处理溢出中断,通过调用TIM5输入捕获中断处理回调函数HAL_TIM_IC_CaptureCallback来处理捕获中断。在HAL库中,这两个回调函数都是弱定义、无实际内容的函数,需要用户自定义以实现中断处理功能。
下面我们在tim.c文件中添加TIM5更新中断回调函数和TIM5输入捕获中断处理回调函数,这两个函数会被定时器中断请求函数HAL_TIM_IRQHandler调用。
在tim.c文件的“USER CODE BEGIN 1”和“USER CODE END 1”之间添加如下代码:
1   /* USER CODE BEGIN 1 */
2   /**
3    * @brief       定时器输入捕获中断处理回调函数
4    * @param       htim:定时器句柄指针
5    * @note        该函数在HAL_TIM_IRQHandler中会被调用
6    * @retval      无
7    */
8   void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
9   {10      if ((capture_state & 0X80) == 0)     /* 还没成功捕获 */
11      {12          if (capture_state & 0X40)         /* 捕获到一个下降沿 */
13          {14              capture_state |= 0X80;        /* 标记成功捕获到一次高电平脉宽 */
15              /* 获取当前的捕获值 */
16       capture_val = HAL_TIM_ReadCapturedValue(&htim5, TIM_CHANNEL_1);
17              /* 一定要先清除原来的设置 */
18      TIM_RESET_CAPTUREPOLARITY(&htim5, TIM_CHANNEL_1);/*将计数寄存器的值变为0*/
19              /* 配置TIM5通道1上升沿捕获 */
20 TIM_SET_CAPTUREPOLARITY(&htim5, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING);
21          }
22          else                                  /* 还未开始,第一次捕获上升沿 */
23          {24              capture_state = 0;              /* 清空 */
25              capture_val = 0;
26              capture_state |= 0X40;         /* 标记捕获到了上升沿 */
27              __HAL_TIM_DISABLE(&htim5);     /* 关闭定时器5 */
28              __HAL_TIM_SET_COUNTER(&htim5,0);/* 设置计数寄存器的值变为0 */
29              /* 一定要先清除原来的设置!! */
30              TIM_RESET_CAPTUREPOLARITY(&htim5, TIM_CHANNEL_1);
31              /* 定时器5通道1设置为下降沿捕获 */
32TIM_SET_CAPTUREPOLARITY(&htim5, TIM_CHANNEL_1,TIM_ICPOLARITY_FALLING);
33              __HAL_TIM_ENABLE(&htim5);        /* 使能定时器5 */
34          }
35      }
36  }
37
第10行,如果8位的capture_state和0X80位与等于0的话,表示此时还没有捕获成功(位与后等于0则表示第7位为0,表示捕获未完成);
第12行,在没有捕获成功时,如果这时候capture_state和0X40位相与等于1的话,表示此时第6位为1,即捕获到了下降沿;
第14行,捕获到低电平以后,则将capture_state的第7位置1,标记此时完成一次捕获;
第16行,捕获成功后,先获取当前的捕获值,也就是获取此时TIMx_CCR1寄存器的值,因为发生捕获时,计数器TIMx_CNT的值会被锁存到捕获寄存器TIMx_CCR1中。这里,HAL_TIM_ReadCapturedValue函数返回的是此时TIMx_CCR1的值,把此值赋值给我们前面定义的变量capture_val;
第18行,调用TIM_RESET_CAPTUREPOLARITY宏清除清除极性,即清除原来设置的下降沿捕获,下一行代码会重新配置为上升沿捕获,即捕获完下降沿,下次要捕获上升沿;
第20行,配置TIM5的通道1为上升沿捕获;
以上第12~21行是捕获下降沿的逻辑代码,下面第22~34行的是捕获上升沿的逻辑代码,因为一个脉冲是由一个上升沿和一个下降沿组成的。一开始capture_state和capture_val的值都是0,而且程序初始化时配置极性为上升沿,所以程序运行以后首先执行第22行之后的代码。
第22行,如果capture_state和0X40位与后不等于1的话,即第6位为0,表示捕获到上升沿;
第24~26行,捕获到上升沿时,先清空capture_state和capture_val,并将capture_state的第6位置1,表示已经捕获到高电平了,不过此时并没有完成捕获,因为一次完整的捕获是先捕获到到高电平再捕获到低电平才组成一个脉冲,所以capture_state的第7位被清零,表示未完成捕获。
第27~28行,调用__HAL_TIM_DISABLE来关闭TIM5,即关闭计数器,并调用__HAL_TIM_SET_COUNTER来设置计数器值为0。这里为什么要关闭定时器还要设置计数器计数值为0呢?我们以一个图来说明:
一个高电平脉冲的宽度是上升沿到下降沿之间这段时间的值,通过捕获寄存器的值可以知道捕获到这次高电平脉冲时计数器计数了多少个节拍,则可以计算此高电平脉冲的宽度。如下图,捕获到上升沿时计数器的值为0,捕获到下降沿时计数器的值为capture_val,假设计数器的频率为f,则这段高电平脉冲的时间为capture_val/f。

图17.4.5. 19高电平脉冲捕获示意图(没有溢出的情况下)
第30和32行,清除原来的设置,再设置TIM5为下降沿捕获,为下一个捕获做准备。
第33行,再次开启定时器。
以上就是输入捕获中断处理回调函数,通过来回切换捕获边沿的极性以及关闭和开启定时器来实现。在捕获的过程中,如果脉冲的时间比较长,计数器可能会发生溢出,我们来看看溢出中断回调函数,如下:

1  /**
2   * @brief       定时器更新(溢出)中断回调函数
3   * @param        htim:定时器句柄指针
4   * @note        此函数会被定时器中断函数共同调用的
5   * @retval      无
6   */
7  void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
8  {9      if (htim == (&htim5))
10      {11          if ((capture_state & 0X80) == 0)        /* 还没成功捕获 */
12          {13              if (capture_state & 0X40)                /* 已经捕获到低电平了 */
14              {15                 if ((capture_state & 0X3F) == 0X3F) /* 脉冲太长了 */
16                  {17                      /*获取当前的捕获值*/
18        capture_val = HAL_TIM_ReadCapturedValue (&htim5, TIM_CHANNEL_1);
19                      /* 一定要先清除原来的设置 */
20                      TIM_RESET_CAPTUREPOLARITY(&htim5, TIM_CHANNEL_1);
21                      /* 配置TIM5通道1上升沿捕获 */
22 TIM_SET_CAPTUREPOLARITY(&htim5, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING);
23                      capture_state |= 0X80;         /* 标记成功捕获了一次 */
24                      capture_val = 0XFFFF;
25                  }
26                  else  /* 累加高电平长度 */
27                  {28                      capture_state++;
29                  }
30              }
31          }
32     }
33  }
3  /* USER CODE END 1 */
第9行,如果判断是TIM5,则执行第9行以后的语句;
第11行,如果capture_state的第7位是0,表示还未成功捕获到高电平脉冲;
第13行,如果capture_state的第6位为1,表示捕获到了下降沿;
第15行,capture_state的第0~5位表示捕获到低电平后定时器的溢出次数,如果capture_state的第0~5位都是1,表示已经达到capture_state最大记录的溢出次数了;
第18行,HAL_TIM_ReadCapturedValue返回的是当前的捕获值,先将此时的捕获值保存在capture_val中;
第20~22行,清除原来的设置,并设置TIM5的通道1为上升沿捕获,为下一个捕获做准备;
第23行,将capture_state的第7位置1,捕获到低电平脉冲时,表示已经完成了一次捕获;
第24行,16位的capture_val用于保存捕获值,如果脉冲太长了,将capture_val设置为最大值0XFFFF。
第28行,如果capture_state的第0~5位还没达到最大值,表示capture_state还能继续记录溢出次数,每次捕获到一次低电平,则capture_state自加1;
  1. main.c文件
    main.c已经生成了时钟初始化代码,这里我们就在讲解了,我们在第九章节的时候已经有讲解过。如下,标红的字体之间的代码是我们手动添加的:
1   #include "main.h"
2   #include "tim.h"
3   #include "usart.h"
4   #include "gpio.h"
5   /* USER CODE BEGIN Includes */
6   #include "./BSP/Include/led.h"
7   /* USER CODE END Includes */
8   uint8_t t = 0;         /* 定义变量t,用于指示LED0灯什么时候亮 */
9   uint32_t temp = 0;     /* 定义变量temp,用于计算溢出时间 */
10
11  /* 系统时钟配置 */
12  void SystemClock_Config(void);
13
14  int main(void)
15  {16    /* 初始化HAL库 */
17    HAL_Init();
18    /* 判断引脚BOOT2 是否为1,为1的话表示从MCU启动(ST官方叫做工程启动模式)*/
19    if(IS_ENGINEERING_BOOT_MODE())
20    {21      /* 配置系统时钟 */
22      SystemClock_Config();
23    }
24
25    /* 初始化所有已经配置的外设 */
26    MX_GPIO_Init();           /* 初始化GPIO */
27    MX_TIM5_Init();           /* 初始化TIM5 */
28    MX_UART4_Init();      /* 初始化UART4 */
29
30    /* USER CODE BEGIN 2 */
31    /* printf("请输入字符,并按下回车键结束\r\n"); */
32    /* 以中断方式接收数据 */
33     HAL_UART_Receive_IT(&huart4,&RxBuffer,1);
34    /* 启动TIM5通道1的输入捕获 */
35     HAL_TIM_IC_Start_IT(&htim5, TIM_CHANNEL_1);
36    /* 使能TIM5通道1的中断  */
37     __HAL_TIM_ENABLE_IT(&htim5, TIM_CHANNEL_1);
38    /* USER CODE END 2 */
39
40    while (1)
41    {42      /* USER CODE BEGIN 3 */
43      if (capture_state & 0X80)               /* 成功捕获到高电平脉冲 */
44      {45         temp = capture_state&0X3F;
46         temp *= 65536;                      /* 溢出时间总和 */
47         temp += capture_val;               /* 得到总的高电平时间 */
48         printf("HIGH:%d us\r\n", temp);    /* 打印总的高电平时间 */
49         capture_state = 0;                  /* 开启下一次捕获 */
50      }
51      t++;
52       if (t > 20)                             /* 200ms进入一次 */
53      {54          t = 0;
55          LED0_TOGGLE();                  /* LED0闪烁 ,提示程序运行 */
56      }
57       HAL_Delay(10);                         /* 延时10ms */
58    }
59    /* USER CODE END 3 */
60  }
61
62  /* 将printf函数重映射到STM32串口,实现UART4打印信息 */
63  /* USER CODE BEGIN 4 */
64  #ifdef __GNUC__
65  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
66  #else
67  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
68  #endif
69  PUTCHAR_PROTOTYPE
70  {71  /* 本实验使用的是UART4,如果使用的是其它串口,则将UART4改为对应的串口即可 */
72      while ((UART4->ISR & 0X40) == 0);
73      UART4->TDR = (uint8_t) ch;
74      return ch;
75  }
76  /* USER CODE END 4 */
第33行,以中断方式接收数据,我们在串口实验章节有讲解过;
第35行,启动TIM5通道1的输入捕获功能,前面我们是配置了TIM5的通道1,但还需要添加此行代码来启动,否则无法捕获输入。这行代码也可以在初始化代码里添加,考虑到有可能改动了STM32CUbeMX以后,添加的代码可能会被生成的初始化代码覆盖掉,这里直接在main.c文件中添加;
第37行,使能TIM5通道1的中断。我们用到TIM5的中断,使用前要使能中断,如果不使能的话也是看不到现象的;
第43行,表示如果capture_state的第7位为1,即成功捕获到高电平脉冲;
第45行,使用变量temp获取此时溢出的次数;
第46行,计算溢出时间总和,计数器每次计数65536个节拍后溢出一次;
第47行,temp += capture_val表示计数器溢出节拍数+捕获值等于计数器总的节拍数。前面TIM5初始化部分,配置计数器每个节拍计时1us,所以总共计时temp us;
第48行,打印总的高电平时间;
第49行,将capture_state清0,因为如果capture_state的第7位一直是1的话,就不会进行二次捕获,所以在main.c文件中处理完捕获数据后,要将capture_state清0,从而开启第二次捕获;
第51~56行,当t自加到20时,LED0翻转;
  1. 编译测试
    以上代码添加完毕以后,保存修改,编译工程无报错以后,用Type-C线接在开发板的USB_TTL接口上,线的一端接在电脑的USB口上,按照前面的步骤连接好ST-Link,同时注意开发板上的JP11处的跳线帽是否已经接好,如果跳线帽没接,那么UART4则无法正常通信,拨码开关拨成001,即MCU启动模式,进入Debug模式。

图17.4.5. 20开发板连接方式
双击开发板光盘A-基础资料\3、软件下的串口软件XCOM V2.0.exe将其打开:

图17.4.5. 21打开XCOM V2.0
打开XCOM V2.0以后,选择Type-C接口对应的串口(笔者的是com64),设置波特率为115200,停止位为1,数据位为8,无奇偶校验位,即保持和前面配置工程的时候一样的参数配置,在串口操作处选择打开串口(打开串口以后显示的字眼是关闭串口):

图17.4.5. 22设置打开XCOM V2.0的参数
进入Debug以后,点击运行按钮,可以看到底板的LED0灯在闪烁,说明程序已经在跑了。按下底板的WK_UP按键后,可以看到XCOM V2.0显示捕获的高电平脉冲的宽度数据,也可以使用串口来回显发送的字符。

图17.4.5. 23串口输出printf的信息
上面的数据中可以看到出现一个288us,这种就是按键按下时发生的抖动引起的,这就是为什么在按键实验中进行了消抖处理,防止因为按键抖动对数据产生干扰。定时器5是32位的计数器,本实验我们为了通用性只配置了16位,如果想使用定时器的32位,可以把自动重载寄存器的值设置为(0XFFFFFFFF-1),main.c文件中temp = 65536改为temp=0XFFFFFFFF即可。
17.5 通用定时器脉冲计数实验(外部时钟模式1)
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 10-5 GTIM_CNT。
在17.1小节介绍定时器框图时我们有对外部时钟模式1和外部时钟模式2进行过讲解,本小节实验我们使用定时器的外部时钟模式1的方式来对外部输入的脉冲进行计数,当然,在外部时钟模式1下,定时器也是工作在从模式,也可以说为使用从模式进行脉冲计数。
在17.1小节我们知道定时器的时钟来源有四种, 前面的三个通用定时器实验使用的时钟源都是来自内部时钟 (CK_INT),本小节的实验我们将使用外部时钟模式 1:外部输入引脚 (TIx)作为定时器的时钟源。
本章节使用TIM2的通道1作为外部时钟输入引脚,此引脚上接的是按键WK_UP,此按键是高电平有效,按下以后,会给PA0提供一个高电平脉冲作为定时器的计数器时钟,每按下一次按键产生一次高电平脉冲,计数器加一,这时定时器就工作在从模式。
关于定时器的主从模式该怎么理解?定时器都可以通过外部信号触发而启动计数,还可以通过另外一个定时器的某种TRGO信号(包括复位,使能,更新,比较脉冲等TRGO信号)触发启动计数。像这样通过一个定时器触发另一个定时器,发出触发信号的定时器工作于主模式,接受触发信号而启动的定时器工作于从模式。
下面开始讲解本实验用到的寄存器配置情况。
17.5.1 TIM2/TIM3/TIM4/TIM5寄存器
通用定时器脉冲计数实验需要用到的寄存器有:TIMx_ARR、TIMx_PSC、TIMx_CCMR1、TIMx_CCER、TIMx_DIER、TIMx_CR1、TIMx_EGR这些寄存器在前面的章节都有介绍到了,在本小节我们只需针对性的介绍。

  1. 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
    该寄存器我们在PWM输出实验时讲解了他作为输出功能的配置,在输入捕获实验学习了输入捕获模式的配置,本小节我们的外部信号也同样要作为输入信号给定时器作为时钟源,所以我们要看输入捕获模式定时器对应功能。WK_UP按键(PA0)对应着定时器2的通道1。接下来我们开始配置TIMx_CCMR1寄存器,其描述如下图所示:

图17.5.1. 1 TIMx_CCMR1寄存器
因为我们要让PA0引脚输入的脉冲到定时器2的通道1,所以应该配置TIM2_CCMR1寄存器,低八位[7:0]用于捕获/比较通道1的控制,其中
CC1S[1:0],这两个位用于CCR1的通道配置,这里我们设置IC1S[1:0]=01,也就是配置IC1映射在TI1上,即CCR1对应TIMx_CH1。
IC1PSC[1:0]输入捕获1预分频器,我们是1次高电平脉冲就触发1次计数,所以不用分频选择00即可。
IC1F[3:0]输入捕获1滤波器,这个用来设置输入采样频率和数字滤波器长度,关于滤波长度的介绍请看上一个实验。这里,我们不做滤波处理,所以设置IC1F[3:0]=0000,只要采集到上升沿,就触发捕获。
2. 捕获/比较使能寄存器(TIMx_ CCER)
TIM2/TIM3/TIM4/TIM5的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如下图所示:

图17.5.1. 2 TIMx_CCER寄存器
我们要用到这个寄存器的最低2位,CC1E和CC1P位。要使能输入捕获,必须设置CC1E=1,而CC1P则根据自己的需要来配置。我们这里是保留默认设置值0,即高电平触发捕获。
可以看到上面两个寄存器配置和输入捕获实验的配置是一样的。两个实验配置最大的区别就是本实验是在从模式下工作的,下面主要看看从模式控制寄存器的配置。
3. 从模式控制寄存器(TIMx_ SMCR)
TIM2/TIM3/TIM4/TIM5的从模式控制寄存器,该寄存器用于配置从模式,以及定时器的触发源相关的设置。TIMx_SMCR寄存器描述如下图所示:

图17.5.1. 3 TIMx_SMCR寄存器
因为我们要让外部引脚脉冲信号作为定时器的时钟源,所以位[2:0]和位16组合的SMS[0:3],我们设置的值是0111,即外部时钟模式1。位[6:4] 和位[21:20]组合的TS[0:5]是触发选择设置,TIMx_CH1对应TI1FP1,TIMx_CH2则对应TI2FP2,我们是定时器通道1,所以需要配置的值为00101。ETF[3:0]和ETPS[1:0]分别是外部触发滤波器和外部触发预分频器,我们没有用到。
4. DMA/中断使能寄存器(TIMx_DIER)
接下来我们再看看DMA/中断使能寄存器:TIMx_DIER,该寄存器的各位描述如下图。本小节,我们需要在定时器溢出中断中累计定时器溢出的次数,所以需要使能定时器的更新中断,即UIE置1。

图17.5.1. 4 TIMx_DIER寄存器
控制寄存器:TIMx_CR1,我们只用到了它的最低位CEN,将该位置1则使能定时器。
17.5.2 定时器的HAL库驱动
定时器在HAL库中的驱动代码在前面已经介绍了部分,这里我们针对定时器从模式介绍HAL_TIM_SlaveConfigSynchronization函数。

  1. HAL_TIM_SlaveConfigSynchronization函数
    该函数是HAL_TIM_SlaveConfigSynchro函数的宏定义,真正的函数定义是后者,其定义如下:
    HAL_StatusTypeDef HAL_TIM_SlaveConfigSynchro(TIM_HandleTypeDef *htim,
    TIM_SlaveConfigTypeDef *sSlaveConfig);
    函数描述:
    该函数用于配置定时器的从模式。
    函数形参:
    形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。
    形参2是TIM_SlaveConfigTypeDef结构体类型指针变量,用于配置定时器的从模式。
    重点了解一下TIM_SlaveConfigTypeDef结构体指针类型,其定义如下:
typedef struct
{uint32_t SlaveMode;                /* 从模式选择 */uint32_t InputTrigger;               /* 输入触发源选择 */uint32_t TriggerPolarity;          /* 输入触发极性 */uint32_t TriggerPrescaler;          /* 输入触发预分频 */uint32_t TriggerFilter;            /* 输入滤波器设置 */
} TIM_SlaveConfigTypeDef;

函数返回值:
HAL_StatusTypeDef枚举类型的值。HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
2. 几个重要的宏
实验中我们会用到一下几个宏定义:
__HAL_TIM_ENABLE_IT /* 使能句柄指定的定时器更新中断 /
__HAL_TIM_SET_COUNTER /
在运行时设置定时器计数器寄存器的值 /
__HAL_TIM_ENABLE /
启用定时器 /
__HAL_TIM_DISABLE /
关闭定时器,同时也将通道输出或者输入关闭 /
__HAL_TIM_GET_COUNTER /
在运行时获取定时器计数器寄存器值 */
3. HAL_TIM_IC_Start_IT函数
HAL_TIM_IC_Start_IT函数我们在前面的17.4小节也讲解过,主要用于启动定时器的输入捕获模式,且开启输入捕获中断。
4. 回调函数HAL_TIM_PeriodElapsedCallback
更新中断回调函数HAL_TIM_PeriodElapsedCallback函数我们在上一章节实验使用过,实验中我们需要手动编写此回调函数来处理溢出中断。
17.5.3 硬件设计

  1. 例程功能
    用TIM2_CH1做输入捕获,捕获PA0上的高电平脉冲,可以通过按WK_UP按键,输入高电平脉冲,调用HAL库的API将脉冲进行计数,然后通过串口UART4打印出来,通过按KEY0重设当前计数,LED0闪烁,提示程序运行。
  2. 硬件资源
    1)LED0灯UART4和WKUP按键(通过按键给PA0输入高脉冲)
    LED0 WKUP UART4_TX UART4_RX KEY0
    PI0 PA0 PG11 PB2 PG3
    表17.5.3. 1硬件资源
    2)定时器2输出通道1(TIM2_CH1)
    定时器属于STM32MP157的内部资源,只需要软件设置好即可正常工作。
  3. 原理图
    从核心板原理图看出,PA0上接了WKUP按键,此按键高电平有效(按键按下,对应的IO口即为高电平)。PA0还可以复用为TIM2_CH1,程序上通过配置PA0复用为TIM2_CH1,然后按下WKUP按键后,PA0就输入一个高电平脉冲给定时器。

图17.5.3. 1引脚部分原理图
从《STM32MP157A&D数据手册》中也可以查阅PA0的复用关系:

图17.5.3. 2《STM32MP157A&D数据手册》部分截图
17.5.4 软件设计

  1. 程序流程图

图17.5.4. 1通用定时器中断实验程序流程图
2. STM32CubeMX配置
(1)配置PI0复用为TIM2_CH1
新建一个工程GTIM_CNT(或者直接在第十四章节串口通信实验的基础上操作,因为本实验中会用到UART4),进入STM32CubeMX插件配置界面后,在Pinout & Configuration处配置PA0复用为TIM2_CH1,如下图所示:

图17.5.4. 2配置PA0复用为TIM2的通道1
(2)配置TIM2时基等参数
在TimersTIM2中配置如下:

图17.5.4. 3配置TIM2通道1
上图的参数介绍如下:
定时器模式配置如下:
Slave Mode选择External Clock Mode 1,即选择外部时钟模式1;
Trigger Source表示触发源,我们选择TI1FP1,表示触发信号来自滤波后的定时器输入1(TI1FP1);
Counter Settings配置如下:
Prescaler用于配置定时器预分频值,这里配置为0,表示每一个时钟都会计数一次, 以提高精度;
Counter Mode用于配置计数模式,我们选择向上计数Up;
Counter Period用于配置定时器自动重装载值,我们设置为65536-1(或者写为0XFFFF-1);
Internal Clock Division (CKD)配置为No Division即内部时钟不分频;
auto-reload preload用于配置自动重载是否使能,我们选择 Enable使能自动重载;
Slave Mode Controller默认选择ETR mode1
Trigger Output (TRGO)Parameters参数配置如下:
Master/Slave Mode(MSM bit)用于配置主/从模式,这里是指是用一个定时器来触发另一个定时器,我们这里不用此模式。
Trigger Event Selection TRGO 用于配置触发事件选择,这里选Reset(UG bit from TIMX_EGR)
Trigger参数配置如下:
Trigger Polarity用于配置触发极性选择,这里配置上升沿Rising Edge;
Trigger Fiter (4 bits value)用于配置滤波,这里配置为0,即不滤波。
(3)配置UART4参数
实验中会用到UART4,UART4配置如下:

图17.5.4. 4配置UART4
(4)配置GPIO
本实验还会用到LED0、和按键WK_UP,前面的实验我们有配置过,为了方便,本节实验会拷贝按键输入实验的BSP文件夹到工程中使用,所以这里将三个按键(KEY0、KEY1、WK_UP)以及LDE0和LDE1的都配置了。
LED0、LED1、KEY0和KEY1配置如下:

图17.5.4. 5配置LED0、LED1、KEY0和KEY2
由于WK_UP接在PA0上,而PA0已经配置为TIM2_CH1,所以直接在GPIOTIM处配置WK_UP按键:

图17.5.4. 6配置WK_UP
同时,也要配置UART4的两个引脚,User Label可以写也可以不写:

图17.5.4. 7配置UART4的两个引脚
(5)配置NVIC
本节实验我们用到串口接收数据,会用到串口接收中断,同时定时器也要累计溢出次数,所以要用到定时器的更新中断。
先开启TIM2的全局中断,开启以后才可以设置中断:

图17.5.4. 8开启TIM2的全局中断
接下来开启UART4的全局中断:

图17.5.4. 9开启UART4的全局中断
然后设置中断优先级,如下,我们设置中断优先级分组为2,TIM2的抢占优先级和子优先级分别为2和0,UART4的抢占优先级和子优先级分别为3和3:

图17.5.4. 10配置TIM2和UART4的抢占优先级和子优先级
(6)配置时钟
本实验我们采用外部24MHz的时钟HSE(也可以采用内部时钟),配置时钟树,经过PLL3锁相环以后,APB1的时钟频率为最大209MHz(也可以配置其它频率):

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

图17.5.4. 12配置系统时钟
UART4的时钟频率为104.5MHz。

图17.5.4. 13 UART4的时钟为104.5MHz
(5)配置生成独立的文件
配置生成独立的.c和.h头文件,如下图:

图17.5.4. 14配置生成独立的.c和.h文件
2.生成工程
配置好后,按下“Ctrl+S”保存修改配置,生成工程,如下:

图17.5.4. 15生成工程
3. 添加LED和按键驱动代码
将按键输入实验的BSP文件夹拷贝到工程中,我们后面直接用此文件夹里的LED灯驱动和按键驱动:

图17.5.4. 16添加BSP文件夹
4. 定义UART4接收回调函数
我们前面的实验有操作过UART4,可以直接将串口通信实验的usart.c文件的代码拷贝过来,如果是在串口通信实验章节的工程中操作本章实验的话,此步骤可以忽略。本工程中,在usart.c文件中的“USER CODE BEGIN 1”和中“USER CODE END 1”位置处添加如下代码,也就是回调函数:

/* USER CODE BEGIN 1 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle)
{HAL_UART_Transmit(&huart4,&RxBuffer,1,0);HAL_UART_Receive_IT(&huart4,&RxBuffer,1);
}
/* USER CODE END 1 */
  1. 修改tim.c文件
    (1)tim.h文件添加如下代码,即声明两个函数:
/* USER CODE BEGIN Private defines */
void gtim_restart(void);                /* 通用定时器 重启计数器 */
uint32_t gtim_get_count(void);          /* 通用定时器 获取脉冲计数 */
/* USER CODE END Private defines */
(2)在tim.c文件中定义gtim_restart和gtim_get_count和更新中断函数。
1   /* USER CODE BEGIN 0 */
2   uint32_t ofcnt = 0 ;
3   /* USER CODE END 0 */
4
5   /**
6    * @brief    通用定时器2通道1重启计数器
7    * @param    无
8    * @retval   无
9    */
10  void gtim_restart(void)
11  {12      __HAL_TIM_DISABLE(&htim2);                  /* 关闭定时器TIM2 */
13      ofcnt = 0;                                 /* 累加器清零 */
14      __HAL_TIM_SET_COUNTER(&htim2, 0);       /* 计数器清零 */
15      __HAL_TIM_ENABLE(&htim2);                   /* 使能定时器TIMX */
16  }
17
18  /**
19   * @brief    通用定时器2通道1获取当前计数值
20   * @param    无
21   * @retval   count:当前计数值
22   */
23  uint32_t gtim_get_count(void)
24  {25      uint32_t count = 0;
26      count = ofcnt * 65536;                     /* 计算溢出次数对应的计数值 */
27      count += __HAL_TIM_GET_COUNTER(&htim2);/* 加上当前CNT的值 */
28      return count;
29  }
以上两个函数比较简单,我们简单分析一下这两个函数。
gtim_restart函数用于重启计数器,按下KEY0按键后就会调用此函数实现计数器清零:
第12行,先使用宏__HAL_TIM_DISABLE将TIMx_CR1的第0位CEN清零,即关闭计数器,计数器关闭后,定时器就不工作了;
第13行,将ofcnt先清零,只要关闭计数器,该变量就应该清零;
第14行,将TIM2的计数器清零;
第15行,重新使能TIM2;
gtim_get_count用于计算计数器的值,通过此值可以知道计数器记录的高脉冲次数。程序运行后,先按下KEY0清零计数器,然后再按下WK_UP按键给PA0提供高电平脉冲,每按下一次WK_UP就给PA0一次高电平脉冲:
第25行,定义32位的局部变量count为0;
第26行,count用于获取总溢出的计数器值;
第27行,count用于获取总的计数器计数值,也就是溢出的值加上没有溢出的捕获值;
第28行,函数返回值是计数器的计数值。
这里的变量ofcnt是计数器溢出次数,其值可以在定时器更新中断函数HAL_TIM_PeriodElapsedCallback中得出。计数器每次溢出都会触发更新中断,中断服务函数TIM2_IRQHandler在stm32mp1xx_it.c文件中有定义,发生中断后会执行中断服务函数:
/* TIM2的中断服务函数 */
void TIM2_IRQHandler(void)
{/* 中断请求函数 */HAL_TIM_IRQHandler(&htim2);
}
进入中断请求函数HAL_TIM_IRQHandler后,会根据中断类型执行对应的中断回调函数,这里是溢出中断,所以会执行更新(溢出)中断回调函数,我们可以在更新中断中实现溢出次数。HAL_TIM_PeriodElapsedCallback的代码如下:
函数中,每次溢出,ofcnt的值就自加1。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim == (&htim2)){ofcnt++;                         /* 累计溢出次数 */}
}
6. 修改main.c文件main.c文件的部分代码如下,时钟初始化相关代码我们就不列出了,我们在始终系统章节就已经介绍了。
1   #include "main.h"
2   #include "tim.h"
3   #include "usart.h"
4   #include "gpio.h"
5
6   /* USER CODE BEGIN Includes */
7   #include "./BSP/Include/led.h"
8   #include "./BSP/Include/key.h"
9   /* USER CODE END Includes */
10
11  void SystemClock_Config(void);
12
13  int main(void)
14  {15    /* USER CODE BEGIN 1 */
16    uint32_t curcnt = 0;
17    uint32_t oldcnt = 0;
18    uint8_t key = 0;
19    uint8_t t = 0;
20    /* USER CODE END 1 */
21
22    HAL_Init();
23
24    if(IS_ENGINEERING_BOOT_MODE())
25    {26      /* 配置系统时钟 */
27      SystemClock_Config();
28    }
29
30    /* 初始化已经配置的外设 */
31    MX_GPIO_Init();
32    MX_TIM2_Init();
33    MX_UART4_Init();
34    /* USER CODE BEGIN 2 */
35    gtim_restart();                           /* 通用定时器2通道1重启计数器 */
36    HAL_UART_Receive_IT(&huart4,&RxBuffer,1);     /* 以中断方式接收数据 */
37   __HAL_TIM_CLEAR_FLAG(&htim2, TIM_IT_UPDATE);   /* 清除更新事件标志 */
38    /* 开启更新中断*/
39    __HAL_TIM_ENABLE_IT(&htim2,TIM_IT_UPDATE);
40    /* 开始捕获TIM2的通道1 */
41    HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
42    /* USER CODE END 2 */
43
44    while (1)
45    {46      /* USER CODE BEGIN 3 */
47        key = key_scan(0);                       /* 扫描按键 */
48        if (key == KEY0_PRES)                   /* KEY0按键按下,重启计数 */
49        {50            gtim_restart();                       /* 重新启动计数 */
51        }
52        curcnt = gtim_get_count();           /* 获取计数值 */
53        if (oldcnt != curcnt)
54        {55            oldcnt = curcnt;
56            printf("CNT:%ld\r\n", oldcnt);  /* 打印脉冲个数 */
57        }
58        t++;
59        if (t > 20)                                /* 约200ms进入一次 */
60        {61            t = 0;
62            LED0_TOGGLE();                        /* LED0闪烁 ,提示程序运行 */
63        }
64        HAL_Delay(10);
65    }
66    /* USER CODE END 3 */
67  }
68
69  /* USER CODE BEGIN 4 */
70  #ifdef __GNUC__
71  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
72  #else
73  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
74  #endif
75  PUTCHAR_PROTOTYPE
76  {77  // 本实验使用的是UART4,如果使用的是其它串口,则将UART4改为对应的串口即可
78      while ((UART4->ISR & 0X40) == 0);
79      UART4->TDR = (uint8_t) ch;
80      return ch;
81  }
82  /* USER CODE END 4 */
如上图,标红的字体之间的代码是我们手动添加的,部分代码我们已经介绍过了。
第15~20行,定义几个变量,用于while循环中计算脉冲;
第35行,进入主函数之前先清除计数器;
第36行,以中断方式接收数据,后面会将脉冲数目通过UART4打印出来;
第37行,清除更新事件标志,一般建议 在定时器初始化后在开启定时器之前先清除中断标志,如果不清除,程序运行以后串口UART4上打印的第一个数是65536,也就是1个溢出的计数器的计数值。为什么会这样呢?因为在HAL库初始化并开启定时器以后,是默认开启中断的,只是等使能定时器以后,才会进入中断里,如不需要默认就开启中断可以用这行代码将其关闭。第37行的代码实际上是将TIMx_SR寄存器的第0位UIF给清零了,所以这行代码也可以使用如下的来代替:

TIM2->SR = (uint16_t)~TIM_FLAG_UPDATE;
第39行,使能htim2指定的定时器更新中断;
第41行,启动定时器的输入捕获模式,且开启输入捕获中断;
第47~51行,执行按键扫描,如果是KEY0按下,则将计数器清零;
第52行,gtim_get_count返回的是计数器的计数值,curcnt记录此值;
第53~57行,打印脉冲个数;
第57~64,LED0以每隔200ms的时间闪烁,指示程序正在运行;
第69~82行,将printf函数重映射到STM32串口的寄存器,使用UART4打印脉冲个数。
7. tim.c初始化代码
tim.c初始化代码在前面的实验中我们已经介绍过了,基本差不多,这里就不再重复介绍。不同的就是预分频值为0,输入触发源是TI1FP1。

图17.5.4. 17 tim.c初始化代码
8. 编译运行
以上代码添加完毕以后,保存修改,编译工程无报错以后,用Type-C线接在开发板的USB_TTL接口上,线的一端接在电脑的USB口上,按照前面的步骤连接好ST-Link,同时注意开发板上的JP11处的跳线帽是否已经接好,如果跳线帽没接,那么UART4则无法正常通信,拨码开关拨成001,即MCU启动模式,进入Debug模式。

图17.5.4. 18开发板连接方式
双击开发板光盘A-基础资料\3、软件下的串口软件XCOM V2.0.exe将其打开:

图17.5.4. 19打开XCOM V2.0
打开XCOM V2.0以后,选择Type-C接口对应的串口(笔者的是com66),设置波特率为115200,停止位为1,数据位为8,无奇偶校验位,即保持和前面配置工程的时候一样的参数配置,在串口操作处选择打开串口(打开串口以后显示的字眼是关闭串口):

图17.5.4. 20设置打开XCOM V2.0的参数
进入Debug以后,点击运行按钮,可以看到底板的LED0灯在闪烁,说明程序已经在跑了。
如果上面的main.c文件中不添加清除中断标志位的代码的话:
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_IT_UPDATE);
运行程序后,串口UART4第一次打印的字符是“CNT:65536”,也就是1个溢出的计数值。按下底板的WK_UP按键后,可以看到串口打印“CNT:0”,按下WK_UP按键后定时器得到一个高电平脉冲,串口会打印“CNT:1”,继续按下WK_UP按键,串口打印的数值逐渐加1:

图17.5.4. 21串口输出printf的信息
如果在main.c文件中添加了清除中断标志位代码的话,程序运行后串口无打印信息,按下WK_UP按键后,串口打印“CNT:1”(当然可能按下按键后因为按键抖动会偶尔打印其它数值),按下KEY0后,串口打印“CNT:0”。

图17.5.4. 22串口输出printf的信息
17.6 通用定时器脉冲计数实验(外部时钟模式2)
17.6.1 使用按键WK_UP给TIM2_ETR脉冲
前面的实验我们使用的是外部时钟模式1,如果在前面外部时钟模式1的实验的工程中,在STM32CubeMX引脚配置中,将PA0选择复用为TIM2_ETR,即选择外部时钟模式2:

图17.6.1. 1配置PA0复用为TIM2_ETR
然后,TIM2配置如下::

图17.6.1. 2配置TIM2
按键WK_UP配置为如下:

图17.6.1. 3配置WK_UP
其它地方都不需要再修改了,重新编译工程,实验结果和上面的是一样的,这种方式是外部时钟模式2。
外部时钟模式2的话,时钟信号输入引脚是特定的引脚,即ETR引脚,因为PA0引脚刚好可以复用为TIM2_CH1和TIM2_ETR,这个TIM2_ETR就是外部时钟模式2的时钟输入引脚,也就是说,PA0作为外部时钟输入的引脚,只要把脉冲传输到这个引脚就可以了(注意IO口耐压范围,输入过大的电压会烧坏IO口),而按键WK_UP刚好接在PA0上,如果按下按键WK_UP后,PA0输入高电平。

图17.6.1. 4《STM32MP157A&D数据手册》部分截图
如下图是《STM32MP157A&D数据手册》的部分截图,可以看到IO口的耐压范围在1.71 V < V DD < 3.6 V之间,所以如果从外部接入的脉冲信号电压范围应该在此范围之间。

图17.6.1. 5《STM32MP157A&D数据手册》部分截图
17.6.2 使用PZ6给TIM2_ETR提供脉冲
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 10-6 GTIM_PULSE。

  1. 例程功能
    使用TIM2的TIM2_ETR引脚接收脉冲信号,接收到的高电平脉冲数通过串口UART4打印出来。按下KEY1按键,实现PZ6输出脉冲,使用杜邦线将PZ6接在PA0上,PZ6输出的脉冲给PA0。LED0用于指示程序在运行中。
  2. 硬件资源
    1)LED0灯、UART4、WKUP按键(可以通过按键给PA0输入高脉冲)、KEY0以及KEY1按键

表17.6.2. 1硬件资源
2)定时器2的 TIM2_ETR引脚
定时器属于STM32MP157的内部资源,只需要软件设置好即可正常工作。
3. 原理图
下面,我们直接使用开发板上某个空闲的IO口给PA0(TIM2_ETR)提供脉冲。开发板底板原理图中,JJP1排针引出了PA0和PZ6,且PZ6没有被占用,程序可以控制此IO口输出高低电平,模拟输出一个脉冲,然后使用两头是母头的杜邦线将PZ6和PA0连接起来,这样就将PZ6输出的脉冲给PA0了。

图17.6.2. 1底板原理图部分

图17.6.2. 2将PZ6和PA0连接起来
4. STM32CubeMX配置
这里为了方便讲解,重新新建一个工程GTIM_PULSE来做此实验,大家也可以直接在上一个实验的工程(第17.5小节的实验)中直接修改。
(1)配置PI0复用为TIM2_CH1
新建一个工程GTIM_PULSE(或者直接在第17.5节实验的基础上操作,因为大部分的配置是一样的),进入STM32CubeMX插件配置界面后,在Pinout & Configuration处配置PA0复用为TIM2_ETR,如下图所示:

图17.6.2. 3配置PA0复用为TIM2_ETR
(2)配置TIM2时基等参数
在TimersTIM2中配置如下:

图17.6.2. 4 TIM2模式和参数配置
上图的参数介绍如下:
定时器模式配置如下:
Clock Source选择ETR2,即选择外部时钟模式2;
Counter Settings配置如下:
Prescaler用于配置定时器预分频值,这里配置为0,表示每一个时钟都会计数一次, 以提高精度;
Counter Mode用于配置计数模式,我们选择向上计数Up;
Counter Period用于配置定时器自动重装载值,我们设置为65536-1(或者写为0XFFFF-1);
Internal Clock Division (CKD)配置为No Division即内部时钟不分频;
auto-reload preload用于配置自动重载是否使能,我们选择 Enable使能自动重载;
Trigger Output (TRGO)Parameters参数配置如下:
Master/Slave Mode(MSM bit)用于配置主/从模式,这里是指是用一个定时器来触发另一个定时器,我们这里不用此模式。
Trigger Event Selection TRGO 用于配置触发事件选择,这里选Reset(UG bit from TIMX_EGR)
Clock参数配置如下:
Trigger Fiter (4 bits value)用于配置滤波,这里配置为0,即不滤波;
Clock Polarity时钟极性默认选择non inveted,即无极性;
Clock Prescaler选择Prescaler not used,即不使用分频。
(3)配置UART4参数
实验中会用到UART4,UART4配置如下:

图17.6.2. 5配置UART4
(4)配置GPIO
本实验还会用到LED0、按键KEY0和KEY0以及按键WK_UP(WK_UP按键只是用于给PA0提供脉冲,和PZ6提供脉冲做对比),前面的实验我们有配置过,为了方便,本节实验会拷贝按键输入实验的BSP文件夹到工程中使用,所以这里将三个按键(KEY0、KEY1、WK_UP)以及LDE0和LDE1的都配置了。
PZ6、LED0、LED1、KEY0和KEY1配置如下:

图17.6.2. 6配置PZ6、LED0、LED1、KEY0和KEY1
由于WK_UP接在PA0上,而PA0已经配置为TIM2_ETR,所以直接在GPIOTIM处配置WK_UP按键:

图17.6.2. 7配置WK_UP
同时,也要配置UART4的两个引脚,User Label可以写也可以不写:

图17.6.2. 8配置UART4的两个引脚
(5)配置NVIC
本节实验我们用到串口接收数据,会用到串口接收中断,接下来开启UART4的全局中断:

图17.6.2. 9开启UART4的全局中断
然后设置中断优先级,如下,我们设置中断优先级分组为2, UART4的抢占优先级和子优先级分别为3和3:

图17.6.2. 10配置TIM2和UART4的抢占优先级和子优先级
(6)配置时钟
本实验我们采用外部24MHz的时钟HSE(也可以采用内部时钟),配置时钟树,经过PLL3锁相环以后,APB1的时钟频率为最大209MHz(也可以配置其它频率):

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

图17.6.2. 12配置系统时钟
UART4的时钟频率为104.5MHz。

图17.6.2. 13 UART4的时钟为104.5MHz
(5)配置生成独立的文件
配置生成独立的.c和.h头文件,如下图:

图17.6.2. 14配置生成独立的.c和.h文件
2.生成工程
配置好后,按下“Ctrl+S”保存修改配置,生成工程,如下:

图17.6.2. 15生成工程
3.添加LED和按键驱动代码
将按键输入实验的BSP文件夹拷贝到工程中,我们后面直接用此文件夹里的LED灯驱动和按键驱动:

图17.6.2. 16添加BSP文件夹
5. 修改usart.c
关于UART4相关的代码,我们就不再做详细的讲解了,前面部分都有分析过,这里直接贴出代码以及添加的位置。
在usart.h中添加如下代码:

/* USER CODE BEGIN Private defines */
uint8_t RxBuffer;
/* USER CODE END Private defines */在usart.c下添加如下代码:
/* USER CODE BEGIN 1 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle)
{HAL_UART_Transmit(&huart4,&RxBuffer,1,0);HAL_UART_Receive_IT(&huart4,&RxBuffer,1);
}
/* USER CODE END 1 */
  1. 修改main.c文件
    tim.c文件是初始化TIM2的代码,我们在前面有介绍到此文件的代码,大多数都差不多的,不同的就是本小节实验使用的是外部时钟模式2:

图17.6.2. 17 tim.c的初始化代码
本小节实验,tim.c文件不需要添加什么代码,main.c文件的代码如下,其中标红的字体之间的代码是我们手动添加的:

1   #include "main.h"
2   #include "tim.h"
3   #include "usart.h"
4   #include "gpio.h"
5
6   /* USER CODE BEGIN Includes */
7   #include "./BSP/Include/led.h"
8   #include "./BSP/Include/key.h"
9   /* USER CODE END Includes */
10
11  void SystemClock_Config(void);
12
13  int main(void)
14  {15    /* USER CODE BEGIN 1 */
16      uint8_t key = 0;
17      uint8_t t = 0;
18      uint8_t Count = 0;
19    /* USER CODE END 1 */
20
21    HAL_Init();
22
23    if(IS_ENGINEERING_BOOT_MODE())
24    {25      /* Configure the system clock */
26      SystemClock_Config();
27    }
28
29    /* Initialize all configured peripherals */
30    MX_GPIO_Init();
31    MX_TIM2_Init();
32    MX_UART4_Init();
33    /* USER CODE BEGIN 2 */
34      HAL_UART_Receive_IT(&huart4,&RxBuffer,1);/* 以中断方式接收数据 */
35      HAL_TIM_Base_Start(&htim2);             /* 启动定时器2进行外部脉冲计数 */
36    /* USER CODE END 2 */
37
38    while (1)
39    {40       /* USER CODE BEGIN 3 */
41      key = key_scan(0);                                 /* 扫描按键 */
42
43         if (key == KEY1_PRES)
44          {45              /* 配置PZ6输出高电平 */
46              HAL_GPIO_WritePin(PULSE_GPIO_Port,PULSE_Pin,GPIO_PIN_SET);
47              HAL_Delay(1);                               /* 延时1ms */
48              /* 配置PZ6输出低电平 */
49           HAL_GPIO_WritePin(PULSE_GPIO_Port,PULSE_Pin,GPIO_PIN_RESET);
50              HAL_Delay(1);                               /* 延时1ms */
51              /* 获取计数器的计数值,即高电平脉冲个数 */
52              Count = __HAL_TIM_GET_COUNTER(&htim2);
53              printf("Count %d\r\n",Count);         /* 打印脉冲个数 */
54          }
55          if (key == WKUP_PRES)/* WK_UP按下 */
56           {57               /* 获取计数器的计数值,即高电平脉冲个数 */
58               Count = __HAL_TIM_GET_COUNTER(&htim2);
59               printf("Count %d\r\n",Count);            /* 打印脉冲个数 */
60           }
61          if (key == KEY0_PRES)/* KEY0按下 */
62          {63              __HAL_TIM_DISABLE(&htim2);              /* 关闭TIM2 */
64              Count = 0;/* 将计数值清零 */
65              __HAL_TIM_SET_COUNTER(&htim2, 0);/* 设置计数器计数值为0 */
66              __HAL_TIM_ENABLE(&htim2);               /* 使能TIM2 */
67              printf("Count %d\r\n",Count);         /* 打印脉冲个数 */
68          }
69          /* LED0闪烁代码 */
70          t++;
71          if (t > 20)
72          {73              t = 0;
74              LED0_TOGGLE();
75          }
76          HAL_Delay(10);
77    }
78    /* USER CODE END 3 */
79  }
80
81  /* USER CODE BEGIN 4 */
82  #ifdef __GNUC__
83  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
84  #else
85  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
86  #endif
87  PUTCHAR_PROTOTYPE
88  {89  // 本实验使用的是UART4,如果使用的是其它串口,则将UART4改为对应的串口即可 */
90      while ((UART4->ISR & 0X40) == 0);
91      UART4->TDR = (uint8_t) ch;
92      return ch;
93  }
94  /* USER CODE END 4 */
第35行,启动定时器2进行外部脉冲计数,来一个脉冲就会计数一次。HAL_TIM_Base_Start函数我们在基本定时器的第16.1.3小节有介绍过,另外一个函数HAL_TIM_Base_Start_IT是除了关闭定时器,还将更新中断也关闭了,本节实验我们没有用到中断。
第43~54行,如果是KEY1按下,PZ6先输出高电平,再延时1ms,然后PZ6再输出低电平,再延时1ms,这样就是周期为2ms的PWM波形。第52和53行就是获取高电平脉冲个数并通过UART4打印出来;
第55~60行,如果是WK_UP按键按下,则获取高电平脉冲个数,并通过UART4打印出来;
第61~68行,此段代码主要是清空计数器计数值,如果是KEY0按下,则先关闭TIM2,然后将Count值清零,设置计数器计数值为0,再打开计数器,然后打印出0。
第70~75行,LED0闪烁,如果程序运行后看到LED0闪烁,可以判断程序在运行;
  1. 编译测试
    以上代码添加完毕以后,保存修改,编译工程无报错以后,用Type-C线接在开发板的USB_TTL接口上,线的一端接在电脑的USB口上,按照前面的步骤连接好ST-Link,同时注意开发板上的JP11处的跳线帽是否已经接好,如果跳线帽没接,那么UART4则无法正常通信,拨码开关拨成001,即MCU启动模式。找一根两头是母头的杜邦线将PZ6和PA0连接起来。
    打开XCOM V2.0以后,选择Type-C接口对应的串口(笔者的是com66),设置波特率为115200,停止位为1,数据位为8,无奇偶校验位,即保持和前面配置工程的时候一样的参数配置,在串口操作处选择打开串口(打开串口以后显示的字眼是关闭串口):

图17.6.2. 18设置打开XCOM V2.0的参数
STM32CubeIDE进入Debug模式,进入Debug以后,点击运行按钮,可以看到底板的LED0灯在闪烁,说明程序已经在跑了。按下按键KEY1以后,串口打印出接收到的脉冲,按下KEY0以后脉冲数值清零。按下WK_UP按键以后,串口打印出接收到的脉冲。可以发现按下WK_UP以后,串口打印的数值不一定每次都是加1,这是因为按键有抖动。

图17.6.2. 19测试结果

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

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

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

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

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

  3. 【正点原子MP157连载】第二十七章 DHT11数字温湿度传感器实验-摘自【正点原子】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. 【正点原子MP157连载】第二十八章 A7和M4联合调试-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

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

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

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

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

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

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

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

  9. 【正点原子MP157连载】第十章 跑马灯实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

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

最新文章

  1. java shiro实例_Apache Shiro入门实例
  2. 关于“INS-40922 Invalid Scan Name – Unresolvable to IP address”
  3. BZOJ 4732 UOJ #268 [清华集训2016]数据交互 (树链剖分、线段树)
  4. FTP开启虚拟用户访问
  5. 新版本阿里云网站的云服务器添加安全组规则
  6. @Transactional 实现原理
  7. php6 配置,thinkphp6下载安装与配置图文详细讲解教程(composer下载安装)
  8. 使用EasyRecovery轻松修复损坏的照片
  9. 同时启动两个android模拟器
  10. JavaScript 盖尔-沙普利算法
  11. 14.各种所需内存计算
  12. 西门子 HTML控件 上不了网,Win10系统在西门子PLC调试中以太网连接异常处理办法...
  13. Java常用关键字查询
  14. 怎样批量将图片转成PDF格式?图片转换PDF操作方法
  15. 根据ID3算法给出游玩的决策树的实战案例
  16. java计算机毕业设计重庆旅游景点源码+数据库+系统+lw文档
  17. 表单验证工具类ValidationUtils
  18. java解决包依赖冲突
  19. MXNet对DenseNet(稠密连接网络)的实现
  20. android 最好的gtd软件,Windows 上的高颜值 GTD 应用,这可能是最棒的一款了:MyerList...

热门文章

  1. 批量设置excel条件格式改变整行的填充色
  2. bean和bean获取
  3. 2022-2027年中国手机饰品市场规模现状及投资规划建议报告
  4. python多个领域140个常用库 (标准库/第三方库)
  5. 怎么html文字下划线,HTML怎么设置下划线?html文字加下划线方法
  6. Java实现 LeetCode 699 掉落的方块(线段树?)
  7. 公交智能化是解决城市公共交通的问题的利器
  8. 动力电池编码追溯系统_学习编码时如何保持动力(10条可行的技巧!)
  9. xshell连接linux的命令,Xshell远程连接Linux服务器xshelllinux命令大全
  10. Monolog使用教程【入门案例版】