正点原子STM32(基于HAL库)2

目录

  • 跑马灯实验(IO输出)
    • STM32F1 GPIO 简介
    • GPIO 八种工作模式(输入浮空/输入上拉/输入下拉/模拟功能/开漏输出/推挽输出/开漏复用/推挽复用)
    • GPIO 寄存器介绍
    • 通用外设驱动步骤
    • GPIO外设配置步骤
    • 硬件设计
    • 程序设计
      • GPIO的HAL库驱动函数
      • GPIO 输出配置步骤
      • 程序流程图
      • 课堂源码
      • 跑马灯程序解析
    • 下载验证
  • 按键输入点灯实验(IO输入)
    • 按键与输入数据寄存器简介
      • 独立按键简介
      • GPIO 端口输入数据寄存器(IDR)
    • 硬件设计
    • 课堂源码
    • 程序设计
      • HAL_GPIO_ReadPin 函数
      • GPIO 输入配置步骤
      • 程序流程图
      • 程序解析
    • 下载验证
  • GPIO外部中断实验
    • NVIC 和 EXTI 简介
      • NVIC 简介(中断控制器,控制中断分组和优先级)
      • EXTI 简介(外部中断事件控制)
    • 硬件设计
    • 程序设计
      • EXTI 的HAL库驱动
      • EXTI 外部中断配置步骤
      • 程序流程图
      • 课堂源码
      • 程序解析
    • 下载验证
  • 串口通信实验
    • 串口简介
      • 数据通信的基础概念
      • 串口通信协议简介
      • STM32F1 的串口简介
      • GPIO 引脚复用功能
    • 硬件设计
    • 程序设计
      • 回调机制
      • USART 的HAL 库驱动
      • 课堂源码(发送接收一个字节)
      • 程序流程图
      • 程序解析
    • 下载验证
  • 基本定时器实验
    • 定时器概述
    • 基本定时器简介
      • 基本定时器框图
      • 定时器计数模式及溢出条件
      • TIM6/TIM7 寄存器
      • 定时器溢出时间计算方法
      • 基本定时器中断应用
    • 硬件设计
    • 程序设计
      • 定时器的HAL 库驱动
      • 课堂源码
      • 定时器中断配置步骤
      • 程序流程图
      • 程序解析
    • 下载验证
  • 通用定时器实验
    • 通用定时器简介
    • 通用定时器中断实验
      • TIM2/TIM3/TIM4/TIM5 寄存器
      • 硬件设计
      • 程序设计
      • 下载验证
    • 通用定时器PWM 输出实验(原理)
      • TIM2/TIM3/TIM4/TIM5 寄存器
    • 硬件设计
    • 程序设计
      • 定时器的HAL库驱动
      • 定时器PWM 输出模式配置步骤
      • 课堂源码解析
      • 程序解析
    • 通用定时器输入捕获实验(测量时间)
      • TIM2/TIM3/TIM4/TIM5 寄存器
      • 硬件设计
      • 程序设计
        • 定时器的HAL 库驱动
        • 定时器输入捕获模式配置步骤
        • 课堂源码解析
        • 程序解析
      • 下载验证
    • 通用定时器脉冲计数实验(来一个脉冲计一次数)
      • TIM2/TIM3/TIM4/TIM5 寄存器
      • 硬件设计
      • 程序设计
        • 定时器的HAL 库驱动
        • 定时器从模式脉冲计数配置步骤
      • 课堂源码
        • 程序解析
      • 下载验证

跑马灯实验(IO输出)

本章将通过一个经典的跑马灯程序,带大家开启STM32F103 之旅。通过本章的学习,我们
将了解到STM32F103 的IO 口作为输出使用的方法。我们将通过代码控制开发板上的LED 灯:LED0、LED1 交替闪烁,实现类似跑马灯的效果。

STM32F1 GPIO 简介

正点原子STM32(基于HAL库)2_第1张图片
正点原子STM32(基于HAL库)2_第2张图片
正点原子STM32(基于HAL库)2_第3张图片
正点原子STM32(基于HAL库)2_第4张图片

GPIO 是控制或者采集外部器件的信息的外设,即负责输入输出。它按组分配,每组16 个
IO 口,组数视芯片而定。STM32F103ZET6 芯片是144 脚的芯片,具有GPIOA、GPIOB、GPIOC、GPIOD、GPIOE、GPIOF 和GPIOG 七组GPIO 口,共有112 个IO 口可供我们编程使用。这里重点说一下STM32F103 的IO 电平兼容性问题,STM32F103 的绝大部分IO 口,都兼容5V,至于到底哪些是兼容5V 的,请看STM32F103xE 的数据手册(注意是数据手册,不是中文参考手册),见表5 大容量STM32F103xx 引脚定义,凡是有FT 标志的,都是兼容5V 电平的IO 口,可以直接接5V 的外设(注意:如果引脚设置的是模拟输入模式,则不能接5V!),凡是不带FT标志的,就建议大家不要接5V 了,可能烧坏MCU。

GPIO 八种工作模式(输入浮空/输入上拉/输入下拉/模拟功能/开漏输出/推挽输出/开漏复用/推挽复用)

GPIO 有八种工作模式,分别是:

  • 1、输入浮空
  • 2、输入上拉
  • 3、输入下拉
  • 4、模拟功能
  • 5、开漏输出
  • 6、推挽输出
  • 7、开漏式复用功能
  • 8、推挽式复用功能

通过GPIO 的基本结构图来分别进行详细分析,先看看总的框图。

正点原子STM32(基于HAL库)2_第5张图片

上图中片上外设比如串口;
右边只有I/O 引脚,这个I/O 引脚就是我们可以看到的芯片实物的引脚,其他部分都是GPIO 的内部结构。

①保护二极管

保护二极管共有两个,用于保护引脚外部过高或过低的电压输入。当引脚输入电压高于
VDD 时,上面的二极管导通,当引脚输入电压低于VSS 时,下面的二极管导通,从而使输入芯片内部的电压处于比较稳定的值(二极管压降)。虽然有二极管的保护,但这样的保护却很有限,大电压大电流的接入很容易烧坏芯片。所以在实际的设计中我们要考虑设计引脚的保护电路(引脚外接电阻)。

②上拉、下拉电阻

它们阻值大概在30~50K欧之间,可以通过上、下两个对应的开关控制,这两个开关由寄
存器控制。当引脚外部的器件没有干扰引脚的电压时,即没有外部的上、下拉电压,引脚的电平由引脚内部上、下拉决定。

开启内部上拉电阻工作,引脚电平为高,开启内部下拉电阻工作,则引脚电平为低。同样,如果内部上、下拉电阻都不开启,这种情况就是我们所说的浮空模式。浮空模式下,引脚的电平是不可确定的。引脚的电平可以由外部的上、下拉电平决定。需要注意的是,STM32 的内部上拉是一种“弱上拉”,这样的上拉电流很弱,如果有要求大电流还是得外部上拉。

③施密特(肖特基)触发器

对于标准施密特触发器,当输入电压高于正向阈值电压,输出为高;当输入电压低于负向
阈值电压,输出为低;当输入在正负向阈值电压之间,输出不改变,也就是说输出由高电准位翻转为低电准位,或是由低电准位翻转为高电准位对应的阈值电压是不同的。只有当输入电压发生足够的变化时,输出才会变化,因此将这种元件命名为触发器。这种双阈值动作被称为迟滞现象,表明施密特触发器有记忆性。从本质上来说,施密特触发器是一种双稳态多谐振荡器。

施密特触发器可作为波形整形电路,能将模拟信号波形整形为数字电路能够处理的方波波
形,而且由于施密特触发器具有滞回特性,所以可用于抗干扰,以及在闭回路正回授/负回授配置中用于实现多谐振荡器。

下面看看比较器跟施密特触发器的作用的比较,就清楚的知道施密特触发器对外部输入信
号具有一定抗干扰能力,如图13.1.2.2 所示。
正点原子STM32(基于HAL库)2_第6张图片
比较器的(A)和施密特触发器(B)作用比较

④P-MOS 管和N-MOS 管

正点原子STM32(基于HAL库)2_第7张图片

这个结构控制GPIO 的开漏输出和推挽输出两种模式。
开漏输出:输出端相当于三极管的集电极,要得到高电平状态需要上拉电阻才行。
推挽输出:这两只对称的MOS 管每次只有一只导通,所以导通损耗小、效率高。输出既可以向负载灌电流,也可以从负载拉电流。推拉式输出既能提高电路的负载能力,又能提高开关速度。下面会有详细讲解。


上面我们对GPIO 的基本结构图中的关键器件做了介绍,下面分别介绍GPIO 八种工作模
式对应结构图的工作情况。

正点原子STM32(基于HAL库)2_第8张图片

1、输入浮空

输入浮空模式:上拉/下拉电阻为断开状态,施密特触发器打开,输出被禁止。输入浮空模
式下,IO 口的电平完全是由外部电路决定。如果IO 引脚没有连接其他的设备,那么检测其输入电平是不确定的。该模式可以用于按键检测等场景。

正点原子STM32(基于HAL库)2_第9张图片

正点原子STM32(基于HAL库)2_第10张图片

2、输入上拉
输入上拉模式:上拉电阻导通,施密特触发器打开,输出被禁止。在需要外部上拉电阻的
时候,可以使用内部上拉电阻,这样可以节省一个外部电阻,但是内部上拉电阻的阻值较大,所以只是“弱上拉”,不适合做电流型驱动。

正点原子STM32(基于HAL库)2_第11张图片

正点原子STM32(基于HAL库)2_第12张图片

3、输入下拉
输入下拉模式:下拉电阻导通,施密特触发器打开,输出被禁止。在需要外部下拉电阻
的时候,可以使用内部下拉电阻,这样可以节省一个外部电阻,但是内部下拉电阻的阻值较
大,所以不适合做电流型驱动。

正点原子STM32(基于HAL库)2_第13张图片

正点原子STM32(基于HAL库)2_第14张图片

4、模拟功能
模拟功能:上下拉电阻断开,施密特触发器关闭,双MOS 管也关闭。其他外设可以通过模
拟通道输入输出。该模式下需要用到芯片内部的模拟电路单元单元,用于ADC、DAC、MCO这类操作模拟信号的外设。
正点原子STM32(基于HAL库)2_第15张图片

5、开漏输出

STM32 的开漏输出模式是数字电路输出的一种,从结果上看它只能输出低电平Vss 或者高阻态,常用于IIC 通讯(IIC_SDA)或其它需要进行电平转换的场景。

根据《STM32F10xxx 参考手册_V10(中文版).pdf》第108 页关于“GPIO 输出配置”的描述,我们可以知道开漏模式下,IO 是这样工作的:

⚫ P-MOS 被“输出控制”控制在截止状态,因此IO 的状态取决于N-MOS 的导通状况;
⚫ 只有N-MOS 还受控制于输出寄存器,“输出控制”对输入信号进行了逻辑非的操作;
⚫ 施密特触发器是工作的,即可以输入,且上下拉电阻都断开了,可以看成浮空输入;

下图中写入输出数据寄存器①的值怎么对应到IO 引脚的输出状态②是我们最关心的:

根据参考手册的描述:开漏输出模式下P-MOS 一直在截止状态,即不导通,所以P-MOS
管的栅极相当于一直接VDD

如果输出数据寄存器①的值为0,那么IO 引脚的输出状态②为低电平,这是我们需要的控制逻辑,怎么做到的呢?输出数据寄存器的逻辑0 经过“输出控制”的取反操作后,输出逻辑1 到N-MOS 管的栅极,这时N-MOS 管就会导通,使得IO 引脚连接到VSS,即输出低电平

如果输出数据寄存器的值为1,经过“输出控制”的取反操作后,输出逻辑0 到N-MOS 管的栅极,这时N-MOS 管就会截止。又因为P-MOS管是一直截的,使得IO 引脚呈现高阻态,即不输出低电平,也不输出高电平。因此要IO引脚输出高电平就必须接上拉电阻。又由于F1 系列的开漏输出模式下,内部的上下拉电阻不可用,所以只能通过接芯片外部上拉电阻的方式,实现开漏输出模式下输出高电平

如果芯片外部不接上拉电阻,那么开漏输出模式下,IO 无法输出高电平。

在开漏输出模式下,施密特触发器是工作的,所以IO 口引脚的电平状态会被采集到输入数
据寄存器中,如果对输入数据寄存器进行读访问可以得到IO 口的状态。也就是说开漏输出模式下,我们可以读取IO 引脚状态。

正点原子STM32(基于HAL库)2_第16张图片
在这里插入图片描述

6、推挽输出

推挽输出模式:STM32 的推挽输出模式,从结果上看它会输出低电平VSS或者高电平VDD。推挽输出跟开漏输出不同的是,推挽输出模式P-MOS 管和N-MOS 管都用上。同样地,我们根据参考手册推挽模式的输出描述,可以得到等效原理图,如图13.1.2.8 所示。根据手册描述可以把“输出控制”简单地等效为一个非门。

如果输出数据寄存器①的值为0,经过“输出控制”取反操作后,输出逻辑1 到P- MOS管的栅极,这时P-MOS 管就会截止,同时也会输出逻辑1 到N-MOS 管的栅极,这时N-MOS 管就会导通,使得IO 引脚接到VSS,即输出低电平。

如果输出数据寄存器的值为1 ,经过“输出控制”取反操作后,输出逻辑0 到N-MOS管的栅极,这时N-MOS 管就会截止,同时也会输出逻辑0 到P-MOS 管的栅极,这时P-MOS 管就会导通,使得IO 引脚接到VDD,即输出高电平。

由上述可知,推挽输出模式下,P-MOS 管和N-MOS 管同一时间只能有一个管是导通的。当IO 引脚在做高低电平切换时,两个管子轮流导通,一个负责灌电流,一个负责拉电流,使其负载能力和开关速度都有较大的提高。

另外在推挽输出模式下,施密特触发器也是打开的,我们可以读取IO 口的电平状态。

由于推挽输出模式下输出高电平时,是直接连接VDD,所以驱动能力较强,可以做电流型驱动,驱动电流最大可达25mA,但是芯片的总电流有限,所以并不建议这样用,最好还是使用芯片外部的电源。

正点原子STM32(基于HAL库)2_第17张图片
正点原子STM32(基于HAL库)2_第18张图片

7、开漏式复用功能

开漏式复用功能:一个IO 口可以是通用的IO 口功能,还可以是其他外设(如串口)的特殊功能引脚,这就是IO 口的复用功能。一个IO 口可以是多个外设的功能引脚,我们需要选择作为其中一个外设的功能引脚。当选择复用功能时,引脚的状态是由对应的外设控制,而不是输出数据寄存器。除了复用功能外,其他的结构分析请参考开漏输出模式。

另外在开漏式复用功能模式下,施密特触发器也是打开的,我们可以读取IO 口的电平状
态,同时外设可以读取IO 口的信息。

正点原子STM32(基于HAL库)2_第19张图片

正点原子STM32(基于HAL库)2_第20张图片

8、推挽式复用功能

推挽式复用功能:复用功能介绍请查看开漏式复用功能,结构分析请参考推挽输出模式,
这里不再赘述。

正点原子STM32(基于HAL库)2_第21张图片

正点原子STM32(基于HAL库)2_第22张图片

GPIO 寄存器介绍

正点原子STM32(基于HAL库)2_第23张图片

STM32F1 每组(这里是A~D)通用GPIO 口有7 个32 位寄存器控制,包括:
2 个32 位端口配置寄存器(CRL 和CRH)
2 个32 位端口数据寄存器(IDR 和ODR)
1 个32 位端口置位/复位寄存器(BSRR)
1 个16 位端口复位寄存器(BRR)
1 个32 位端口锁定寄存器(LCKR)

下面我们将带大家理解本章用到的寄存器,没有介绍到的寄存器后面用到会继续介绍。这
里主要是带大家学会怎么理解这些寄存器的方法,其他寄存器理解方法是一样的。因为寄存器太多不可能一个个列出来讲,以后基本就是只会把重要的寄存器拿出来讲述,希望大家尽快培养自己学会看手册的能力。下面先看GPIO 的2 个32 位配置寄存器:

⚫ 端口配置寄存器(GPIOx_CRL 和GPIO_x_CRH)
这两个寄存器都是GPIO 口配置寄存器,不过CRL 控制端口的低八位,CRH 控制端口的
高八位。寄存器的作用是控制GPIO 口的工作模式和工作速度,寄存器描述如图13.1.3.1 和图13.1.3.2 所示。
正点原子STM32(基于HAL库)2_第24张图片
正点原子STM32(基于HAL库)2_第25张图片
每组GPIO 下有16 个IO 口,一个寄存器共32 位,每4 个位控制1 个IO,所以才需要两
个寄存器完成。我们看看这个寄存器的复位值,然后用复位值举例说明一下这样的配置值代表什么意思。比如GPIOA_CRL 的复位值是0x44444444,4 位为一个单位都是0100,以寄存器低四位说明一下,首先位1:0 为00 即是设置为PA0 为输入模式,位3:2 为01 即设置为浮空输入模式。所以假如GPIOA_CRL 的值是0x44444444,那么PA0~PA7 都是设置为输入模式,而且是浮空输入模式。

上面这2 个配置寄存器就是用来配置GPIO 的相关工作模式和工作速度,它们通过不同的
配置组合方法,就决定我们所说的8 种工作模式。下面,我们来列表阐述,如表13.1.3.1 所示。
在这里插入图片描述

因为本章需要GPIO 作为输出口使用,所以我们再来看看端口输出数据寄存器。

⚫ 端口输入数据寄存器(IDR)
正点原子STM32(基于HAL库)2_第26张图片

⚫ 端口输出数据寄存器(ODR)
该寄存器用于控制GPIOx 的输出高电平或者低电平,寄存器描述如图13.1.3.3 所示。

正点原子STM32(基于HAL库)2_第27张图片

该寄存器低16 位有效,分别对应每一组GPIO 的16 个引脚。当CPU 写访问该寄存器,如
果对应的某位写0(ODRy=0),则表示设置该IO 口输出的是低电平,如果写1(ODRy=1),则表示设置该IO 口输出的是高电平,y=0~15。
此外,除了ODR 寄存器,还有一个寄存器也是用于控制GPIO 输出的,它就是BSRR 寄存
器。

⚫ 端口置位/复位寄存器(BSRR),前面图片有讲,是间接控制ODR寄存器的
该寄存器也用于控制GPIOx 的输出高电平或者低电平,寄存器描述如图13.1.3.4 所示。
正点原子STM32(基于HAL库)2_第28张图片
为什么有了ODR 寄存器,还要这个BSRR 寄存器呢

正点原子STM32(基于HAL库)2_第29张图片

我们先看看BSRR 的寄存器描述,
首先BSRR 是只写权限,而ODR 是可读可写权限。BSRR 寄存器32 位有效,对于低16 位(0-15),我们往相应的位写1(BSy=1),那么对应的IO 口会输出高电平,往相应的位写0(BSy=0),对IO 口没有任何影响,高16 位(16-31)作用刚好相反,对相应的位写1(BRy=1)会输出低电平,写0(BRy=0)没有任何影响,y=0~15。

也就是说,对于BSRR 寄存器,你写0 的话,对IO 口电平是没有任何影响的。我们要设置
某个IO 口电平,只需要相关位设置为1 即可。而ODR 寄存器,我们要设置某个IO 口电平,
我们首先需要读出来ODR 寄存器的值,然后对整个ODR 寄存器重新赋值来达到设置某个或者某些IO 口的目的,而BSRR 寄存器直接设置即可,这在多任务实时操作系统中作用很大。BSRR寄存器还有一个好处,就是BSRR 寄存器改变引脚状态的时候,不会被中断打断,而ODR 寄存器有被中断打断的风险。

通用外设驱动步骤

正点原子STM32(基于HAL库)2_第30张图片

GPIO外设配置步骤

正点原子STM32(基于HAL库)2_第31张图片

硬件设计

  1. 例程功能
    LED 灯:DS0 和DS1 每过500ms 一次交替闪烁,实现类似跑马灯的效果。
  2. 硬件资源
    1)LED 灯
    DS0 –PB5
    DS1 –PE5
  3. 原理图
    本章用到的硬件用到LED 灯:DS0 和DS1。电路在开发板上已经连接好了,所以在硬件上不需要动任何东西,直接下载代码就可以测试使用。其连接原理图如图13.2.1 所示:
    正点原子STM32(基于HAL库)2_第32张图片

程序设计

了解了GPIO 的结构原理和寄存器,还有我们的实验功能,下面开始设计程序。

GPIO的HAL库驱动函数

正点原子STM32(基于HAL库)2_第33张图片

HAL 库中关于GPIO 的驱动程序在STM32F1xx_hal_gpio.c 文件以及其对应的头文件。

  1. HAL_GPIO_Init 函数
    要使用一个外设我们首先要对它进行初始化,所以我们先看外设GPIO 的初始化函数。其声明如下:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

⚫ 函数描述:
用于配置GPIO 功能模式,还可以设置EXTI 功能。
⚫ 函数形参:
形参1 是端口号,可以有以下的选择:

#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)

这是库里面的选择项,实际上我们的芯片只能从GPIOA~GPIOE,因为我们只有5 组IO 口。

形参2 是GPIO_InitTypeDef 类型的结构体变量,其定义如下:

typedef struct
{
	uint32_t Pin; /* 引脚号*/
	uint32_t Mode; /* 模式设置*/
	uint32_t Pull; /* 上拉下拉设置*/
	uint32_t Speed; /* 速度设置*/
} GPIO_InitTypeDef;

该结构体很重要,下面对每个成员介绍一下。
成员Pin 表示引脚号,范围:GPIO_PIN_0 到GPIO_PIN_15,另外还有GPIO_PIN_All 和
GPIO_PIN_MASK 可选。

成员Mode 是GPIO 的模式选择,有以下选择项:

#define GPIO_MODE_INPUT 	(0x00000000U) /* 输入模式*/
#define GPIO_MODE_OUTPUT_PP (0x00000001U) /* 推挽输出*/
#define GPIO_MODE_OUTPUT_OD (0x00000011U) /* 开漏输出*/
#define GPIO_MODE_AF_PP 	(0x00000002U) /* 推挽式复用*/
#define GPIO_MODE_AF_OD 	(0x00000012U) /* 开漏式复用*/
#define GPIO_MODE_AF_INPUT 	GPIO_MODE_INPUT
#define GPIO_MODE_ANALOG 	(0x00000003U) /* 模拟模式*/
#define GPIO_MODE_IT_RISING (0x11110000U) /* 外部中断,上升沿触发检测*/
#define GPIO_MODE_IT_FALLING (0x11210000U) /* 外部中断,下降沿触发检测*/
/* 外部中断,上升和下降双沿触发检测*/
#define GPIO_MODE_IT_RISING_FALLING (0x11310000U)
#define GPIO_MODE_EVT_RISING 		(0x11120000U) /* 外部事件,上升沿触发检测*/
#define GPIO_MODE_EVT_FALLING 		(0x11220000U) /* 外部事件,下降沿触发检测*/
/* 外部事件,上升和下降双沿触发检测*/
#define GPIO_MODE_EVT_RISING_FALLING (0x11320000U)

成员Pull 用于配置上下拉电阻,有以下选择项:

#define GPIO_NOPULL 	(0x00000000U) /* 无上下拉,浮空*/
#define GPIO_PULLUP 	(0x00000001U) /* 上拉*/
#define GPIO_PULLDOWN 	(0x00000002U) /* 下拉*/

成员Speed 用于配置GPIO 的速度,有以下选择项:

#define GPIO_SPEED_FREQ_LOW 	(0x00000002U) /* 低速*/
#define GPIO_SPEED_FREQ_MEDIUM (0x00000001U) /* 中速*/
#define GPIO_SPEED_FREQ_HIGH 	(0x00000003U) /* 高速*/

⚫ 函数返回值:

⚫ 注意事项:
HAL 库的EXTI 外部中断的设置功能整合到此函数里面,而不是单独独立一个文件。这个
我们到外部中断实验再细讲。

  1. HAL_GPIO_WritePin 函数
    HAL_GPIO_WritePin 函数是GPIO 口的写引脚函数。其声明如下:
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx,
							uint16_t GPIO_Pin, GPIO_PinState PinState);

⚫ 函数描述:
用于设置引脚输出高电平或者低电平,通过BSRR 寄存器复位或者置位操作。
⚫ 函数形参:
形参1 是端口号,可以选择范围:GPIOA~GPIOG。
形参2 是引脚号,可以选择范围:GPIO_PIN_0 到GPIO_PIN_15。
形参3 是要设置输出的状态,是枚举型有两个选择:GPIO_PIN_SET 表示高电平,
GPIO_PIN_RESET 表示低电平。
⚫ 函数返回值:

  1. HAL_GPIO_TogglePin 函数
    HAL_GPIO_TogglePin 函数是GPIO 口的电平翻转函数。其声明如下:
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

⚫ 函数描述:
用于设置引脚的电平翻转,也是通过BSRR 寄存器复位或者置位操作。
⚫ 函数形参:
形参1 是端口号,可以选择范围:GPIOA~GPIOG。
形参2 是引脚号,可以选择范围:GPIO_PIN_0 到GPIO_PIN_15。
⚫ 函数返回值:

本实验我们用到上面三个函数,其他的API 函数后面用到再进行讲解。

GPIO 输出配置步骤

1)使能对应GPIO 时钟
STM32 在使用任何外设之前,我们都要先使能其时钟(下同)。本实验用到PB5 和PE5 两
个IO 口,因此需要先使能GPIOB 和GPIOE 的时钟,代码如下:

__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();

2)设置对应GPIO 工作模式(推挽输出)
本实验GPIO 使用推挽输出模式,控制LED 亮灭,通过函数HAL_GPIO_Init 设置实现。
3)控制GPIO 引脚输出高低电平
在配置好GPIO 工作模式后,我们就可以通过HAL_GPIO_WritePin 函数控制GPIO 引脚输
出高低电平,从而控制LED 的亮灭了。

程序流程图

程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很
好的主导作用。本实验的程序流程图如下:
正点原子STM32(基于HAL库)2_第34张图片

课堂源码

led.h

#ifndef __LED_H
#define __LED_H

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


void led_init(void);

#endif

led.c

#include "./BSP/LED/led.h"


void led_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;
    
    __HAL_RCC_GPIOB_CLK_ENABLE();//使能GPIOB时钟
    
    gpio_init_struct.Pin = GPIO_PIN_5;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;//推挽输出  开漏输出的话共阳极可以,共阴极不行(不能输出高电平,除非外接上拉电阻)
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOB, &gpio_init_struct);

    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET)//默认灯灭;
}

main.c

/**
 ******************************************************************************
 * @file     main.c
 * @author   正点原子团队(ALIENTEK)
 * @version  V1.0
 * @date     2020-08-20
 * @brief    新建工程实验-HAL库版本 实验
 * @license  Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
 ******************************************************************************
 * @attention
 * 
 * 实验平台:正点原子 STM32F103 开发板
 * 在线视频:www.yuanzige.com
 * 技术论坛:www.openedv.com
 * 公司网址:www.alientek.com
 * 购买地址:openedv.taobao.com
 ******************************************************************************
 */

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"


int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    led_init();                         /* LED初始化 */
    while(1)
    { 
//        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);    /* PB5置1 */ 
//        delay_ms(200);
//        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);  /* PB5置0 */
//        delay_ms(200);
        
        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
        delay_ms(200);
    }
}

跑马灯程序解析

1. led 驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。LED 驱动源码包
括两个文件:led.c 和led.h(正点原子编写的外设驱动基本都是包含一个.c 文件和一个.h 文件,下同)。
下面我们先解析led.h 的程序,我们把它分两部分功能进行讲解。
⚫ LED 灯引脚宏定义
由硬件设计小节,我们知道LED 灯在硬件上分别连接到PB5 和PE5,再结合HAL 库,我
们做了下面的引脚定义。

/* LED0 引脚定义*/
#define LED0_GPIO_PORT GPIOB
#define LED0_GPIO_PIN GPIO_PIN_5
#define LED0_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)
/* LED1引脚定义*/
#define LED1_GPIO_PORT GPIOE
#define LED1_GPIO_PIN GPIO_PIN_5
#define LED1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)

这样的好处是进一步隔离底层函数操作,移植更加方便,函数命名更亲近实际的开发板。比
如:当我们看到LED0_GPIO_PORT 这个宏定义,我们就知道这是灯LED0 的端口号;看到
LED0_GPIO_PIN 这个宏定义,就知道这是灯LED0 的引脚号;看到LED0_GPIO_CLK_ENABLE这个宏定义,就知道这是灯LED0 的时钟使能函数。大家后面学习时间长了就会慢慢熟悉这样的命名方式。

特别注意:这里的时钟使能函数宏定义,使用了do{ }while(0)结构,是为了避免在某些使用
场景出错的问题(下同),详见《嵌入式单片机C 代码规范与风格》第六章第2 点。
__HAL_RCC_GPIOx_CLK_ENABLE 函数是HAL 库的IO 口时钟使能函数,x=A 到G。
⚫ LED 灯操作函数宏定义
为了后续对LED 灯进行便捷的操作,我们为LED 灯操作函数做了下面的定义。

/* LED端口操作定义*/
#define LED0(x) do{ x ? \
					HAL_GPIO_WritePin(LED0_GPIO_PORT,LED0_GPIO_PIN, GPIO_PIN_SET) : \
					HAL_GPIO_WritePin(LED0_GPIO_PORT,LED0_GPIO_PIN, GPIO_PIN_RESET);\
				}while(0) /* LED0翻转*/

#define LED1(x) do{ x ? \
					HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_SET) : \
					HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_RESET);\
			}while(0) /* LED1翻转*/

/* LED电平翻转定义*/
#define LED0_TOGGLE() do{ HAL_GPIO_TogglePin(LED0_GPIO_PORT,
					LED0_GPIO_PIN); }while(0) /* LED0 = !LED0 */

#define LED1_TOGGLE() do{ HAL_GPIO_TogglePin(LED1_GPIO_PORT,
						LED1_GPIO_PIN); }while(0) /* LED1 = !LED1 */

LED0 和LED1 这两个宏定义,分别是控制LED0 和LED1 的亮灭。例如:对于宏定义标识
符LED0(x),它的值是通过条件运算符来确定:

当x=0 时,宏定义的值为HAL_GPIO_WritePin(LED0_GPIO_PORT, LED0_GPIO_PIN, GPIO_PIN_RESET),也就是设置LED0_GPIO_PORT(PB5)输出低电平;

当n!=0 时,宏定义的值为HAL_GPIO_WritePin (LED0_GPIO_PORT, LED0_GPIO_PIN, GPIO_PIN_SET),也就是设置LED0_GPIO_PORT(PB5)输出高电平。

根据前述定义,如果要设置LED0 输出低电平,那么调用宏定义LED0(0)即可,如果要设
置LED0 输出高电平,调用宏定义LED0(1)即可。宏定义LED1(x)同理。

LED0_TOGGLE 和LED1_TOGGLE 这三个宏定义,分别是控制LED0 和LED1 的翻转。这
里利用HAL_GPIO_TogglePin 函数实现IO 口输出电平翻转操作。

下面我们再解析led.c 的程序,这里只有一个函数led_init,这是LED 灯的初始化函数,其
定义如下:

/**
* @brief 初始化LED相关IO口, 并使能时钟
* @param 无
* @retval 无
*/
void led_init(void)
{
		GPIO_InitTypeDef gpio_init_struct;
		LED0_GPIO_CLK_ENABLE(); /* LED0时钟使能*/
		LED1_GPIO_CLK_ENABLE(); /* LED1时钟使能*/
		gpio_init_struct.Pin = LED0_GPIO_PIN; /* LED0引脚*/
		gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出*/
		gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉  F1系列禁止上下拉,这样设置其实是无效的*/
		gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
		HAL_GPIO_Init(LED0_GPIO_PORT, &gpio_init_struct); /* 初始化LED0引脚*/
		
		gpio_init_struct.Pin = LED1_GPIO_PIN; /* LED1引脚*/
		HAL_GPIO_Init(LED1_GPIO_PORT, &gpio_init_struct); /* 初始化LED1引脚*/
		
		LED0(1); /* 关闭LED0 */
		LED1(1); /* 关闭LED1 */
}

对LED 灯的两个引脚都设置为中速上拉的推挽输出。最后关闭LED 灯的输出,防止没有
操作就亮了。

main.c 代码
在main.c 里面编写如下代码:

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
int main(void)
{
		HAL_Init(); /* 初始化HAL库*/
		sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
		delay_init(72); /* 延时初始化*/
		led_init(); /* 初始化LED */
		while (1)
		{
			LED0(0); /* LED0 灭*/
			LED1(1); /* LED1 亮*/
			delay_ms(500);
			LED1(1); /* LED0 灭*/
			LED0(0); /* LED1 亮*/
			delay_ms(500);
		}
}

首先是调用系统级别的初始化:初始化HAL 库、系统时钟和延时函数。接下来,调用led_init来初始化LED 灯。最后在无限循环里面实现LED0 和LED1 间隔500ms 交替闪烁一次。

下载验证

我们先来看看编译结果,如图13.4.1 所示。

正点原子STM32(基于HAL库)2_第35张图片
可以看到没有0 错误,0 警告。从编译信息可以看出,我们的代码占用FLASH 大小为:5804字节(5442+362+28),所用的SRAM 大小为:1928 个字节(28+1900)。这里我们解释一下,编译结果里面的几个数据的意义:

Code:表示程序所占用FLASH 的大小(FLASH)。
RO-data:即Read Only-data,表示程序定义的常量(FLASH)。
RW-data:即Read Write-data,表示已被初始化的变量(FLASH + RAM)
ZI-data:即Zero Init-data,表示未被初始化的变量(RAM)
有了这个就可以知道你当前使用的flash 和ram 大小了,所以,一定要注意的是程序的大小
不是.hex 文件的大小,而是编译后的Code 和RO-data 之和。

接下来,大家就可以下载验证了。这里我们使用DAP 仿真器(也可以使用其他调试器)下载。

下载完之后,运行结果如图13.4.2 所示,可以看到LED 灯的LED0 和LED1 交替亮。

正点原子STM32(基于HAL库)2_第36张图片

按键输入点灯实验(IO输入)

上一章,我们介绍了STM32F1 的IO 口作为输出的使用。本章,我们将向大家介绍如何使
用STM32F1 的IO 口作为输入。我们将利用板载的3 个按键,来控制板载的两个LED 灯亮灭。

通过本章的学习,我们将了解到STM32F1 的IO 口作为输入的使用方法。

按键与输入数据寄存器简介

独立按键简介

几乎每个开发板都会板载有独立按键,因为按键用处很多。常态下,独立按键是断开的,
按下的时候才闭合。每个独立按键会单独占用一个IO 口,通过IO 口的高低电平判断按键的状态。但是按键在闭合和断开的时候,都存在抖动现象,即按键在闭合时不会马上就稳定的连接,断开时也不会马上断开。这是机械触点,无法避免。独立按键抖动波形图如下:

正点原子STM32(基于HAL库)2_第37张图片
图中的按下抖动和释放抖动的时间一般为5~10ms,如果在抖动阶段采样,其不稳定状态可
能出现一次按键动作被认为是多次按下的情况。为了避免抖动可能带来的误操作,我们要做的措施就是给按键消抖(即采样稳定闭合阶段)。消抖方法分为硬件消抖和软件消抖,我们常用软件的方法消抖。

软件消抖:方法很多,我们例程中使用最简单的延时消抖。检测到按键按下后,一般进行
10ms 延时,用于跳过抖动的时间段,如果消抖效果不好可以调整这个10ms 延时,因为不同类型的按键抖动时间可能有偏差。待延时过后再检测按键状态,如果没有按下,那我们就判断这是抖动或者干扰造成的;如果还是按下,那么我们就认为这是按键真的按下了。对按键释放的判断同理。

硬件消抖:利用RC 电路的电容充放电特性来对抖动产生的电压毛刺进行平滑出来,从而
实现消抖,但是成本会更高一点,本着能省则省的原则,我们推荐使用软件消抖即可。

GPIO 端口输入数据寄存器(IDR)

本实验我们将会用到GPIO 端口输入数据寄存器,下面来介绍一下。
该寄存器用于存储GPIOx 的输入状态,它连接到施密特触发器上,IO 口外部的电平信号
经过触发器后,模拟信号就被转化成0 和1 这样的数字信号,并存储到该寄存器中。寄存器描述如图15.1.2.1 所示。
正点原子STM32(基于HAL库)2_第38张图片
该寄存器低16 位有效,分别对应每一组GPIO 的16 个引脚。当CPU 访问该寄存器,如果
对应的某位为0(IDRy=0),则说明该IO 口输入的是低电平,如果是1(IDRy=1),则表示输入的是高电平,y=0~15。

硬件设计

  1. 例程功能
    通过开发板上的三个独立按键控制LED 灯:KEY_UP 控制蜂鸣器翻转,KEY1 控制LED1
    翻转,KEY2 控制LED0 翻转,KEY0 控制LED0/LED1 同时翻转。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    LED1 –PE5
    2)独立按键
    KEY0 –PE4
    KEY1 –PE3
    KEY2 –PE2
    KEY_UP –PA0
  3. 原理图
    独立按键硬件部分的原理图,如图15.2.1 所示:
    正点原子STM32(基于HAL库)2_第39张图片
    这里需要注意的是:KEY0、KEY1 和KEY2 是低电平有效的,而KEY_UP 则是高电平有效的,并且外部都没有上下拉电阻,所以需要在STM32F103 内部设置上下拉,来确定设置空闲电平状态。
  • KEY_UP设置成下拉输入,因为按键没按下是高阻态状态;
  • KEY0、KEY1 和KEY2 设置成上拉输入

课堂源码

key.h

#ifndef __KEY_H
#define __KEY_H

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


void key_init(void);
uint8_t key_scan(void);

#endif

key.c

#include "./BSP/KEY/key.h"
#include "./SYSTEM/delay/delay.h"


/* 按键初始化函数 */
void key_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;
    
    __HAL_RCC_GPIOE_CLK_ENABLE();
    
    gpio_init_struct.Pin = GPIO_PIN_2;
    gpio_init_struct.Mode = GPIO_MODE_INPUT;
    gpio_init_struct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOE, &gpio_init_struct);
}

/* 按键扫描函数 */
uint8_t key_scan(void)
{
    if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_2) == 0)
    {
        delay_ms(10);   /* 消抖 */
        if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_2) == 0)
        {
            while(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_2) == 0);  /* 等待按键松开 */
            return 1;   /* 按键按下了 */
        }
    }
    return 0;   /* 按键没有按下 */
}

main.c

/**
 ******************************************************************************
 * @file     main.c
 * @author   正点原子团队(ALIENTEK)
 * @version  V1.0
 * @date     2020-08-20
 * @brief    新建工程实验-HAL库版本 实验
 * @license  Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
 ******************************************************************************
 * @attention
 * 
 * 实验平台:正点原子 STM32F103 开发板
 * 在线视频:www.yuanzige.com
 * 技术论坛:www.openedv.com
 * 公司网址:www.alientek.com
 * 购买地址:openedv.taobao.com
 ******************************************************************************
 */

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/KEY/key.h"


int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    led_init();                         /* LED初始化 */
    key_init();                         /* KEY初始化 */
    while(1)
    {
        if(key_scan())//按下
        {
            HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
        }
        else
        {
            delay_ms(10);
        }
    }
}

程序设计

HAL_GPIO_ReadPin 函数

HAL_GPIO_ReadPin 函数是GPIO 口的读引脚函数。其声明如下:

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

⚫ 函数描述:
用于读取GPIO 引脚状态,通过IDR 寄存器读取。
⚫ 函数形参:
形参1 是端口号,可以选择范围:GPIOA~GPIOG。
形参2 是引脚号,可以选择范围:GPIO_PIN_0 到GPIO_PIN_15。
⚫ 函数返回值:
引脚状态值0 或者1

GPIO 输入配置步骤

1)使能对应GPIO 时钟
本实验用到PA0 和PE2/3/4 等四个IO 口,因此需要先使能GPIOA 和GPIOE 的时钟,代码
如下:

__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();

2)设置对应GPIO 工作模式(上拉/下拉输入)
本实验GPIO 使用输入模式(带上拉/下拉),从而可以读取IO 口的状态,实现按键检测,
GPIO 模式通过函数HAL_GPIO_Init 设置实现。
3)读取GPIO 引脚高低电平
在配置好GPIO 工作模式后,我们就可以通过HAL_GPIO_ReadPin 函数读取GPIO 引脚的
高低电平,从而实现按键检测了。

程序流程图

正点原子STM32(基于HAL库)2_第40张图片

程序解析

  1. 按键驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。按键(KEY)驱动源码包括两个文件:key.c 和key.h。
    下面我们先解析key.h 的程序,我们把它分两部分功能进行讲解。
    ⚫ 按键引脚定义
    由硬件设计小节,我们知道KEY0、KEY1、KEY2 和KEY_UP 分别来连接到PE4、PE3、PE2 和PA0 上,我们做了下面的引脚定义。
/* 引脚定义*/
#define KEY0_GPIO_PORT GPIOE
#define KEY0_GPIO_PIN GPIO_PIN_4
/* PE口时钟使能*/
#define KEY0_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)
#define KEY1_GPIO_PORT GPIOE
#define KEY1_GPIO_PIN GPIO_PIN_3
/* PE口时钟使能*/
#define KEY1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)
#define KEY2_GPIO_PORT GPIOE
#define KEY2_GPIO_PIN GPIO_PIN_2
/* PE口时钟使能*/
#define KEY2_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)
#define WKUP_GPIO_PORT GPIOA
#define WKUP_GPIO_PIN GPIO_PIN_0
/* PA口时钟使能*/
#define WKUP_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)

⚫ 按键操作函数定义
为了后续对按键进行便捷的操作,我们为按键操作函数做了下面的定义。

#define KEY0 HAL_GPIO_ReadPin(KEY0_GPIO_PORT, KEY0_GPIO_PIN) /* 读取KEY0引脚*/
#define KEY1 HAL_GPIO_ReadPin(KEY1_GPIO_PORT, KEY1_GPIO_PIN) /* 读取KEY1引脚*/
#define KEY2 HAL_GPIO_ReadPin(KEY2_GPIO_PORT, KEY2_GPIO_PIN) /* 读取KEY2引脚*/
#define WK_UP HAL_GPIO_ReadPin(WKUP_GPIO_PORT, WKUP_GPIO_PIN) /* 读取WKUP引脚*/
#define KEY0_PRES 1 /* KEY0按下*/
#define KEY1_PRES 2 /* KEY1按下*/
#define KEY2_PRES 3 /* KEY2按下*/
#define WKUP_PRES 4 /* KEY_UP按下(即WK_UP) */

KEY0、KEY1、KEY2 和WK_UP 分别是读取对应按键状态的宏定义。用HAL_GPIO_ReadPin函数实现,该函数的返回值就是IO 口的状态,返回值是枚举类型,取值0 或者1。
KEY0_PRES、KEY1_PRES、KEY2_PRES 和WKUP_PRES 则是按键对应的四个键值宏定义标识符。

下面我们再解析key.c 的程序,这里有两个函数,先看按键初始化函数,其定义如下:

/**
* @brief 按键初始化函数
* @param 无
* @retval 无
*/
void key_init(void)
{
	GPIO_InitTypeDef gpio_init_struct; /* GPIO配置参数存储变量*/
	KEY0_GPIO_CLK_ENABLE(); /* KEY0时钟使能*/
	KEY1_GPIO_CLK_ENABLE(); /* KEY1时钟使能*/
	KEY2_GPIO_CLK_ENABLE(); /* KEY2时钟使能*/
	WKUP_GPIO_CLK_ENABLE(); /* WKUP时钟使能*/
	
	gpio_init_struct.Pin = KEY0_GPIO_PIN; /* KEY0引脚*/
	gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入*/
	gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉*/
	gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
	HAL_GPIO_Init(KEY0_GPIO_PORT, &gpio_init_struct); /* KEY0引脚模式设置*/
	
	gpio_init_struct.Pin = KEY1_GPIO_PIN; /* KEY1引脚*/
	gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入*/
	gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉*/
	gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
	HAL_GPIO_Init(KEY1_GPIO_PORT, &gpio_init_struct); /* KEY1引脚模式设置*/
	
	gpio_init_struct.Pin = KEY2_GPIO_PIN; /* KEY2引脚*/
	gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入*/
	gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉*/
	gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
	HAL_GPIO_Init(KEY2_GPIO_PORT, &gpio_init_struct); /* KEY2引脚模式设置*/
	
	gpio_init_struct.Pin = WKUP_GPIO_PIN; /* WKUP引脚*/
	gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入*/
	gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉*/
	gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
	HAL_GPIO_Init(WKUP_GPIO_PORT, &gpio_init_struct); /* WKUP引脚模式设置*/
}

这里需要注意的是:KEY0 和KEY1 是低电平有效的(即一端接地),所以我们要设置为内
部上拉,而KEY_UP 是高电平有效的(即一端接电源),所以我们要设置为内部下拉。

另一个函数是按键扫描函数,其定义如下:

/**
* @brief 按键扫描函数
* @note 该函数有响应优先级(同时按下多个按键): WK_UP > KEY2 > KEY1 > KEY0!!
* @param mode:0 / 1, 具体含义如下:
* @arg 0, 不支持连续按(当按键按下不放时, 只有第一次调用会返回键值,
* 必须松开以后, 再次按下才会返回其他键值)
* @arg 1, 支持连续按(当按键按下不放时, 每次调用该函数都会返回键值)
* @retval 键值, 定义如下:
* KEY0_PRES, 1, KEY0按下
* KEY1_PRES, 2, KEY1按下
* KEY2_PRES, 3, KEY2按下
* WKUP_PRES, 4, WKUP按下
*/
uint8_t key_scan(uint8_t mode)
{
	static uint8_t key_up = 1; /* 按键按松开标志*/
	uint8_t keyval = 0;
	if (mode) key_up = 1; /* 支持连按*/
	if (key_up && (KEY0 == 0 || KEY1 == 0 || KEY2 == 0 || WK_UP == 1))
	{ /* 按键松开标志为1, 且有任意一个按键按下了*/
		delay_ms(10); /* 去抖动*/
		key_up = 0;
		if (KEY0 == 0) keyval = KEY0_PRES;
		if (KEY1 == 0) keyval = KEY1_PRES;
		if (KEY2 == 0) keyval = KEY2_PRES;
		if (WK_UP == 1) keyval = WKUP_PRES;
	}
	else if (KEY0 == 1 && KEY1 == 1 && KEY2 == 1 && WK_UP == 0)
	{ /* 没有任何按键按下, 标记按键松开*/
		key_up = 1;
	}
	return keyval; /* 返回键值*/
}

key_scan 函数用于扫描这4 个IO 口是否有按键按下。key_scan 函数,支持两种扫描方式,
通过mode 参数来设置。

当mode 为0 的时候,key_scan 函数将不支持连续按,扫描某个按键,该按键按下之后必
须要松开,才能第二次触发,否则不会再响应这个按键,这样的好处就是可以防止按一次多次触发,而坏处就是在需要长按的时候比较不合适。

当mode 为1 的时候,key_scan 函数将支持连续按,如果某个按键一直按下,则会一直返
回这个按键的键值,这样可以方便的实现长按检测。

有了mode 这个参数,大家就可以根据自己的需要,选择不同的方式。这里要提醒大家,
因为该函数里面有static 变量,所以该函数不是一个可重入函数,在有OS 的情况下,这个大家要留意下。可以看到该函数的消抖延时是10ms。同时还有一点要注意的是,该函数的按键扫描是有优先级的,最优先的是KEY_UP,第二优先的是KEY0,最后是按键KEY2。该函数有返回值,如果有按键按下,则返回非0 值,如果没有或者按键不正确,则返回0。

2. main.c 代码
在main.c 里面编写如下代码:

int main(void)
{
	uint8_t key;
	HAL_Init(); /* 初始化HAL库*/
	sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72); /* 延时初始化*/
	led_init(); /* 初始化LED */
	beep_init(); /* 初始化蜂鸣器*/
	key_init(); /* 初始化按键*/
	LED0(0); /* 先点亮LED0 */
	
	while(1)
	{
		key = key_scan(0); /* 得到键值*/
		if (key)
		{
			switch (key)
			{
				case WKUP_PRES: /* 控制蜂鸣器*/
					BEEP_TOGGLE(); /* BEEP状态取反*/
					break;
				case KEY2_PRES: /* 控制LED0(RED)翻转*/
					LED0_TOGGLE(); /* LED0状态取反*/
					break;
				case KEY1_PRES: /* 控制LED1(GREEN)翻转*/
					LED1_TOGGLE(); /* LED1状态取反*/
					break;
				case KEY0_PRES: /* 同时控制LED0, LED1翻转*/
					LED0_TOGGLE(); /* LED0状态取反*/
					LED1_TOGGLE(); /* LED1状态取反*/
					break;
		    }
		}
		else
		{
			delay_ms(10);
		}
	}
}

首先是调用系统级别的初始化:初始化HAL 库、系统时钟和延时函数。接下来,调用led_i nit来初始化LED 灯,调用key_init 函数初始化按键。最后在无限循环里面扫描获取键值,接着用键值判断哪个按键按下,如果有按键按下则翻转相应的灯,如果没有按键按下则延时10ms。

下载验证

在下载好程序后,我们可以按KEY0、KEY1、KEY2 和KEY_UP 来看看LED 灯的变化,
是否和我们预期的结果一致?

至此,我们的本章的学习就结束了。本章学习了STM32F103 的IO 作为输入的使用方法,
在前面的GPIO 输出的基础上又学习了一种GPIO 使用模式,大家可以回顾前面跑马灯实验介绍的GPIO 的八种模式类型巩固GPIO 的知识。

GPIO外部中断实验

在前面几章的学习中,我们掌握了STM32F1 的IO 口最基本的操作。本章我们将介绍如何把STM32F1 的IO 口作为外部中断输入来使用,在本章中,我们将以中断的方式,实现我们在第十五章所实现的功能。

正点原子STM32(基于HAL库)2_第41张图片
正点原子STM32(基于HAL库)2_第42张图片

NVIC 和 EXTI 简介

NVIC 简介(中断控制器,控制中断分组和优先级)

正点原子STM32(基于HAL库)2_第43张图片

正点原子STM32(基于HAL库)2_第44张图片
正点原子STM32(基于HAL库)2_第45张图片

什么是NVIC?NVIC 即嵌套向量中断控制器,全称Nested vectored interrupt controller。它是内核的器件,所以它的更多描述可以看内核有关的资料《Cortex-M3 权威指南》。M3 内核都是支持256 个中断,其中包含了16 个系统中断和240 个外部中断,并且具有256 级的可编程中断设置。然而芯片厂商一般不会把内核的这些资源全部用完,如STM32F103ZET6 的系统中断有10 个,外部中断有60 个。下面我们看看系统中断部分:

正点原子STM32(基于HAL库)2_第46张图片
关于60 个外部中断部分在《STM32F10xxx 参考手册_V10(中文版).pdf》的9.1.2 小节有详细的列表,这里就不列出来了。STM32F103 的中断向量表在STM32F103xx.h 文件中被定义。

NVIC 寄存器

正点原子STM32(基于HAL库)2_第47张图片
正点原子STM32(基于HAL库)2_第48张图片

NVIC 相关的寄存器定义了可以在core_cm3.h 文件中找到。我们直接通过程序的定义来分析NVIC 相关的寄存器,其定义如下:

typedef struct
{
	__IOM uint32_t ISER[8U]; /* 中断使能寄存器*/
		  uint32_t RESERVED0[24U];
	__IOM uint32_t ICER[8U]; /* 中断清除使能寄存器*/
		  uint32_t RSERVED1[24U];
	__IOM uint32_t ISPR[8U]; /* 中断使能挂起寄存器*/
		  uint32_t RESERVED2[24U];
	__IOM uint32_t ICPR[8U]; /* 中断解挂寄存器*/
		  uint32_t RESERVED3[24U];
	__IOM uint32_t IABR[8U]; /* 中断有效位寄存器*/
		  uint32_t RESERVED4[56U];
	__IOM uint8_t IP[240U]; /* 中断优先级寄存器(8Bit 位宽)*/
		  uint32_t RESERVED5[644U];
	__OM uint32_t STIR; /* 软件触发中断寄存器*/
} NVIC_Type;

STM32F103 的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能方便的使用STM32F103 的中断。下面重点介绍这几个寄存器:
ISER[8]:ISER 全称是:Interrupt Set Enable Registers,这是一个中断使能寄存器组。上面说了CM3 内核支持256 个中断,这里用8 个32 位寄存器来控制,每个位控制一个中断。但是STM32F103 的可屏蔽中断最多只有60 个,所以对我们来说,有用的就是两个(ISER[0]和ISER[1]),总共可以表示64 个中断。而STM32F103 只用了其中的60 个。ISER[0]的bit0~ 31 分别对应中断0~ 31;ISER[1]的bit0~ 27 对应中断32~59,这样总共60 个中断就可以分别对应上了。你要使能某个中断,必须设置相应的ISER 位为1,使该中断被使能(这里仅仅是使能,还要配合中断分组、屏蔽、IO 口映射等设置才算是一个完整的中断设置)。具体每一位对应哪个中断,请参考stm32f103xe.h 里面的第69 行。
ICER[8]:全称是:Interrupt Clear Enable Registers,是一个中断除能寄存器组。该寄存器组与ISER 的作用恰好相反,是用来清除某个中断的使能的。其对应位的功能,也和ICER 一样。这里要专门设置一个ICER 来清除中断位,而不是向ISER 写0 来清除,是因为NVIC 的这些寄存器都是写1 有效的,写0 是无效的。具体为什么是这样子,可以查看《Cortex-M3 权威指南》第125 页,NVIC 章节。
ISPR[8]:全称是:Interrupt Set Pending Registers,是一个中断使能挂起控制寄存器组。每个位对应的中断和ISER 是一样的。通过置1,可以将正在进行的中断挂起,而执行同级或更高级别的中断。写0 是无效的。
ICPR[8]:全称是:Interrupt Clear Pending Registers,是一个中断解挂控制寄存器组。其作用与ISPR 相反,对应位也和ISER 是一样的。通过设置1,可以将挂起的中断解挂。写0 无效。
IABR[8]:全称是:Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。对应位所代表的中断和ISER 一样,如果为1,则表示该位所对应的中断正在被执行。这是一个只读寄存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。
IP [240]:全称是:Interrupt Priority Registers,是一个中断优先级控制的寄存器组。这个寄存器组相当重要!STM32F103 的中断分组与这个寄存器组密切相关。IP 寄存器组由240 个8bit的寄存器组成,每个可屏蔽中断占用8bit,这样总共可以表示240 个可屏蔽中断。而STM32F103只用到了其中的60 个。IP[59]~ IP[0]分别对应中断59~0。而每个可屏蔽中断占用的8bit 并没有全部使用,而是只用了高4 位。这4 位,又分为抢占优先级和子优先级。抢占优先级在前,子优先级在后。而这两个优先级各占几个位又要根据SCB->AIRCR 中的中断分组设置来决定。

关于中断优先级控制的寄存器组我们下面详细讲解。

正点原子STM32(基于HAL库)2_第49张图片
正点原子STM32(基于HAL库)2_第50张图片
正点原子STM32(基于HAL库)2_第51张图片

中断优先级
STM32 中的中断优先级可以分为:抢占式优先级和响应优先级,响应优先级也称子优先级,

每个中断源都需要被指定这两种优先级。抢占式优先级和响应优先级的区别:
抢占优先级:抢占优先级高的中断可以打断正在执行的抢占优先级低的中断。
响应优先级:抢占优先级相同,响应优先级高的中断不能打断响应优先级低的中断。

还有一种情况就是当两个或者多个中断的抢占式优先级和响应优先级相同时,那么就遵循自然优先级,看中断向量表的中断排序,数值越小,优先级越高。

在NVIC 中由寄存器NVIC_IPR0-NVIC_IPR59 共60 个寄存器控制中断优先级,每个寄存器的8 位,所以就有了240 个宽度为8bit 的中断优先级控制寄存器,原则上每个外部中断可配置的优先级为0~255,数值越小,优先级越高。但是实际上M3 芯片为了精简设计,只使用了高四位[7:4],低四位取零,这样以至于最多只有16 级中断嵌套,即2^4=16。

对于NVCI 的中断优先级分组:STM32F103 将中断分为5 个组,组0~ 4。该分组的设置是由SCB->AIRCR 寄存器的bit10~8 来定义的。具体的分配关系如表16.1.1.2.1 所示:
正点原子STM32(基于HAL库)2_第52张图片
通过表16.1.1.2.1,我们就可以清楚的看到组0~ 4 对应的配置关系,例如优先级分组设置为3,那么此时所有的60 个中断,每个中断的中断优先寄存器的高四位中的最高3 位是抢占优先级,低1 位是响应优先级。每个中断,你可以设置抢占优先级为0~7,响应优先级为1 或0。抢占优先级的级别高于响应优先级。而数值越小所代表的优先级就越高。

结合实例说明一下:假定设置中断优先级分组为2,然后设置中断3(RTC_WKUP 中断)的抢占优先级为2,响应优先级为1。中断6(外部中断0)的抢占优先级为3,响应优先级为0。中断7(外部中断1)的抢占优先级为2,响应优先级为0。那么这3 个中断的优先级顺序为:
中断7>中断3>中断6。

上面例子中的中断3 和中断7 都可以打断中断6 的中断。而中断7 和中断3 却不可以相互打断!

NVIC 相关函数

正点原子STM32(基于HAL库)2_第53张图片

ST 公司把core_cm3.h 文件的NVIC 相关函数封装到STM32F1xx_hal_cortex.c 文件中,下面列出我们较为常用的函数进行,想了解更多其他的函数请自行查阅。

  1. HAL_NVIC_SetPriorityGrouping 函数
    HAL_NVIC_SetPriorityGrouping 是设置中断优先级分组函数。其声明如下:
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);

⚫ 函数描述:
用于设置中断优先级分组。
⚫ 函数形参:
形参1 是中断优先级分组号,可以选择范围:NVIC_PRIORITYGROUP_0 到
NVIC_PRIORITYGROUP_4(共5 组)。
⚫ 函数返回值:

⚫ 注意事项:
这个函数在一个工程里基本只调用一次,而且是在程序HAL 库初始化函数里面已经被调用,后续就不会再调用了。因为当后续调用设置成不同的中断优先级分组时,有可能造成前面设置好的抢占优先级和响应优先级不匹配。

  1. HAL_NVIC_SetPriority 函数
    HAL_NVIC_SetPriority 是设置中断优先级函数。其声明如下:
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority,
							uint32_t SubPriority);

⚫ 函数描述:
用于设置中断的抢占优先级和响应优先级(子优先级)。
⚫ 函数形参:
形参1 是中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在stm32f103xe.h(对应中断向量表位置也叫编号那一栏)。

正点原子STM32(基于HAL库)2_第54张图片

形参2 是抢占优先级,可以选择范围:0 到15。
形参3 是响应优先级,可以选择范围:0 到15。
⚫ 函数返回值:

  1. HAL_NVIC_EnableIRQ 函数
    HAL_NVIC_EnableIRQ 是中断使能函数。其声明如下:
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);

⚫ 函数描述:
用于使能中断。
⚫ 函数形参:
形参IRQn 是中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在stm32f103xe.h。
⚫ 函数返回值:

  1. HAL_NVIC_DisableIRQ 函数
    HAL_NVIC_DisableIRQ 是中断失能函数。其声明如下:
void HAL_NVIC_disableIRQ(IRQn_Type IRQn);

⚫ 函数描述:
用于中断失能。
⚫ 函数形参:
形参IRQn 是中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在stm32f103xe.h。
⚫ 函数返回值:

  1. HAL_NVIC_SystemReset 函数
    HAL_NVIC_SystemReset 是系统复位函数。其声明如下:
void HAL_NVIC_SystemReset(void);

⚫ 函数描述:
用于软件复位系统。
⚫ 函数形参:
无形参
⚫ 函数返回值:

其他的NVIC 函数用得较少,我们就不一一列出来了。NVIC 的介绍就到这,下面介绍外部中断。

EXTI 简介(外部中断事件控制)

正点原子STM32(基于HAL库)2_第55张图片
正点原子STM32(基于HAL库)2_第56张图片
正点原子STM32(基于HAL库)2_第57张图片
正点原子STM32(基于HAL库)2_第58张图片

EXTI 即是外部中断和事件控制器,它是由20 个产生事件/中断请求的边沿检测器组成。每一条输入线都可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。挂起寄存器保持着状态线的中断请求。

EXTI 的功能框图是最直接把有关EXTI 的知识点连接起来的图,掌握了该图的来龙去脉,就会对EXTI 有了一个整体熟悉,编程时候可以得心应手。EXTI 的功能框图如图16.1.2.1。
正点原子STM32(基于HAL库)2_第59张图片
正点原子STM32(基于HAL库)2_第60张图片

从EXTI 功能框图可以看到有两条主线,一条是由输入线到NVIC 中断控制器,一条是由输入线到脉冲发生器。这就恰恰是EXTI 的两大部分功能,产生中断与产生事件,两者从硬件上就存在不同。

下面让我们看一下EXTI 功能框图的产生中断的线路,最终信号是流入NVIC 控制器中。输入线是线路的信息输入端,它可以通过配置寄存器设置为任何一个GPIO 口,或者是一些外设的事件。输入线一般都是存在电平变化的信号。

标号①是一个边沿检测电路,包括边沿检测电路,上升沿触发选择寄存器(EXTI_RTSR)和下降沿触发选择寄存器(EXTI_FTSR)。边沿检测电路以输入线作为信号输入端,如果检测到有边沿跳变就输出有效信号‘1’,就输出有效信号‘1’到标号②部分电路,否则输入无效信号‘0’。
边沿跳变的标准在于开始的时候对于上升沿触发选择寄存器或下降沿触发选择寄存器对应位的设置,对应位的设置可以参照一下表16.1.2.1。

标号②是一个或门电路,它的两个信号输入端分别是软件中断事件寄存器(EXTI_SWIER)和边沿检测电路的输入信号。或门电路只要输入端有信号‘1’,就会输出‘1’,所以就会输出‘1’到标号③电路和标号④电路。通过对软件中断事件寄存器的读写操作就可以启动中断/事
件线,即相当于输出有效信号‘1’到或门电路输入端。

标号③是一个与门电路,它的两个信号输入端分别是中断屏蔽寄存器(EXTI_IMR)和标号②电路输出信号。与门电路要求输入都为‘1’才输出‘1’,这样子的情况下,如果中断屏蔽寄存器(EXTI_IMR)设置为0 时,不管从标号②电路输出的信号特性如何,最终标号③电路输出的信
号都是0;假如中断屏蔽寄存器(EXTI_IMR)设置为1 时,最终标号③电路输出的信号才由标号②电路输出信号决定,这样子就可以简单控制EXTI_IMR 来实现中断的目的。标号④电路输出‘1’就会把请求挂起寄存器(EXTI_PR)对应位置1。

最后,请求挂起寄存器(EXTI_PR)的内容就输出到NVIC 内,实现系统中断事件的控制。

接下来我们看看EXTI 功能框图的产生事件的线路。

产生事件线路是从标号2 之后与中断线路有所不用,之前的线路都是共用的。标号④是一个与门,输入端来自标号2 电路以及来自于事件屏蔽寄存器(EXTI_EMR)。如果EXTI_EMR 寄存器设置为0,那不管标号2 电路输出的信号是‘0’还是‘1’,最终标号4 输出的是‘0’;如
果EXTI_EMR 寄存器设置为1,最终标号④电路输出信号就由标号③电路输出的信号决定,这样子就可以简单的控制EXTI_EMR 来实现是否产生事件的目的。

标号④电路输出有效信号1 就会使脉冲发生器电路产生一个脉冲,而无效信号就不会使其产生脉冲信号。脉冲信号产生可以给其他外设电路使用,例如定时器,模拟数字转换器等,这样的脉冲信号一般用来触发TIM 或者ADC 开始转换。

产生中断线路目的使把输入信号输入到NVIC,进一步运行中断服务函数,实现功能。而产生事件线路目的是传输一个脉冲信号给其他外设使用,属于硬件级功能。

EXTI 支持19 个外部中断/事件请求,这些都是信息输入端,也就是上面提及到了输入线,具体如下:

EXTI 线0~15:对应外部IO 口的输入中断
EXTI 线16:连接到PVD 输出
EXTI 线17:连接到RTC 闹钟事件
EXTI 线18:连接到USB 唤醒事件
EXTI 线19:连接到以太网唤醒事件

从上面可以看出,STM32F1 供给IO 口使用的中断线只有16 个,但是STM32F1 的IO 口却远远不止16 个,所以STM32 把GPIO 管脚GPIOx.0~ GPIOx.15(x=A,B,C,D,E,F,G)分别对应中断线0~15。这样子每个中断线对应了最多9 个IO 口,以线0 为例:它对应了GPIOA.0、GPIOB.0、GPIOC.0、GPIOD.0、GPIOE.0、GPIOF.0 和GPIOG.0。而中断线每次只能连接到1 个IO 口上,这样就需要通过配置决定对应的中断线配置到哪个GPIO 上了。

GPIO 和中断线映射关系是在寄存器AFIO_EXTICR1 ~ AFIO_EXTICR4 中配置的。
正点原子STM32(基于HAL库)2_第61张图片
正点原子STM32(基于HAL库)2_第62张图片

正点原子STM32(基于HAL库)2_第63张图片

AFIO_EXTICR1 寄存器配置EXTI0 到EXTI3 线,包含的外部中断的引脚包括PAx 到PGx,x=0 到3。AFIO_EXTICR2 寄存器配置EXTI4 到EXTI7 线,包含的外部中断的引脚包括PAx 到PGx,x=4 到7,AFIO_EXTICR2 寄存器请打开参考手册查看(这里没有截图出来)。AFIO_EX-TICR3 和AFIO_EXTICR4 以此类推。

另外要注意的是,我们配置AFIO 相关寄存器前,还需要打开AFIO 时钟。

正点原子STM32(基于HAL库)2_第64张图片
正点原子STM32(基于HAL库)2_第65张图片

正点原子STM32(基于HAL库)2_第66张图片

硬件设计

  1. 例程功能
    通过外部中断的方式让开发板上的三个独立按键控制LED 灯:KEY_UP 控制LED0 翻转,
    KEY1 控制LED1 翻转,KEY0 控制LED2 翻转。

  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    LED1 –PE5
    2)独立按键
    KEY0 –PE4
    KEY1 –PE3
    KEY2 –PE2
    KEY_UP –PA0

  3. 原理图
    正点原子STM32(基于HAL库)2_第67张图片

正点原子STM32(基于HAL库)2_第68张图片

独立按键硬件部分的连接原理的如图16.2.1。这里需要注意的是:KEY0、KEY1 和KEY2设计为采样到按键另一端的低电平为有效,而KEY_UP 则需要采样到高电平才为按键有效,并且外部都没有上下拉电阻,所以需要在STM32F103 内部设置上下拉以设置空闲电平。

程序设计

EXTI 的HAL库驱动

前面讲解HAL_GPIO_Init 函数的时候有提到过:HAL 库的EXTI 外部中断的设置功能整合到HAL_GPIO_Init 函数里面,而不是单独独立一个文件。所以我们的外部中断的初始化函数也是用HAL_GPIO_Init 函数。

既然是要用到外部中断,所以我们的GPIO 的模式要从下面的三个模式中选中一个:

#define GPIO_MODE_IT_RISING (0x11110000U) /* 外部中断,上升沿触发检测*/
#define GPIO_MODE_IT_FALLING (0x11210000U) /* 外部中断,下降沿触发检测*/
/* 外部中断,上升和下降双沿触发检测*/
#define GPIO_MODE_IT_RISING_FALLING (0x11310000U)

KEY0、KEY1 和KEY2 是低电平有效的,程序设计为按键按下触发中断,所以我们要选择下降沿触发检测,而KEY_UP 是高电平有效的,那么就应该选择上升沿触发检测。

EXTI 外部中断配置步骤

正点原子STM32(基于HAL库)2_第69张图片
正点原子STM32(基于HAL库)2_第70张图片
正点原子STM32(基于HAL库)2_第71张图片

正点原子STM32(基于HAL库)2_第72张图片
正点原子STM32(基于HAL库)2_第73张图片

1)使能对应GPIO 口时钟
本实验用到的GPIO 和按键输入实验是一样的,因此GPIO 时钟使能也是一样的,请参考上一章代码。

2)设置GPIO 工作模式,触发条件,开启AFIO 时钟,设置IO 口与中断线的映射关系这些步骤HAL 库全部封装在HAL_GPIO_Init 函数里面,我们只需要设置好对应的参数,
再调用HAL_GPIO_Init 函数即可完成配置。

3)配置中断优先级(NVIC),并使能中断。
配置好GPIO 模式以后,我们需要设置中断优先级和使能中断,中断优先级我们使用HAL_NVIC_SetPriority 函数设置,中断使能我们使用HAL_NVIC_EnableIRQ 函数设置。

4)编写中断服务函数。
每开启一个中断,就必须编写其对应的中断服务函数,否则将导致死机(CPU 将找不到中断服务函数)。中断服务函数接口厂家已经在startup_stm32f103xe.s 中做好了,STM32F1 的IO口外部中断函数只有7 个,分别为:

void EXTI0_IRQHandler();
void EXTI1_IRQHandler();
void EXTI2_IRQHandler();
void EXTI3_IRQHandler();
void EXTI4_IRQHandler();
void EXTI9_5_IRQHandler();
void EXTI15_10_IRQHandler();

中断线0-4,每个中断线对应一个中断函数,中断线5-9 共用中断函数EXTI9_5_IRQHandler,中断线10-15 共用中断函数XTI15_10_IRQHandler。一般情况下,我们可以把中断控制逻辑直
接编写在中断服务函数中,但是HAL 库把中断处理过程进行了简单封装,请看步骤5 讲解。

5)编写中断处理回调函数HAL_GPIO_EXTI_Callback
HAL 库为了用户使用方便,提供了一个中断通用入口函数HAL_GPIO_EXTI_IRQHandler,在该函数内部直接调用回调函数HAL_GPIO_EXTI_Callback。

我们先看一下HAL_GPIO_EXTI_IRQHandler 函数定义:

void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
	if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00U)
	{
		__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin); /* 清中断标志位*/
		HAL_GPIO_EXTI_Callback(GPIO_Pin); /* 外部中断回调函数*/
	}
}

该函数实现的作用非常简单,通过入口参数GPIO_Pin 判断中断来自哪个IO 口,然后清除相应的中断标志位,最后调用回调函数HAL_GPIO_EXTI_Callback()实现控制逻辑。在所有的外部中断服务函数中直接调用外部中断共用处理函数HAL_GPIO_EXTI_IRQHandler,然后在回调函数HAL_GPIO_EXTI_Callback 中通过判断中断是来自哪个IO 口编写相应的中断服务控制逻辑。

因此我们可以在HAL_GPIO_EXTI_Callback 里面实现控制逻辑编写,详见本实验源码。

程序流程图

下面看看本实验的程序流程图:
正点原子STM32(基于HAL库)2_第74张图片
主程序初始外设,在按键初始化时初始化按键的采样边缘。

课堂源码

通过KEY0控制LED0亮灭

exti.h

#ifndef _EXTI_H
#define _EXTI_H
#include "./SYSTEM/sys/sys.h"


void exti_init(void);

#endif

exti.c

#include "./BSP/EXTI/exti.h"
#include "./SYSTEM/delay/delay.h"


void exti_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;
    __HAL_RCC_GPIOE_CLK_ENABLE();

    gpio_init_struct.Pin = GPIO_PIN_4;
    gpio_init_struct.Mode = GPIO_MODE_IT_FALLING;//下降沿触发
    gpio_init_struct.Pull = GPIO_PULLUP;//上拉
    HAL_GPIO_Init(GPIOE, &gpio_init_struct);//此HAL函数默认设置分组2
    					//中断号
    HAL_NVIC_SetPriority(EXTI4_IRQn, 2, 0);//抢占优先级和响应优先级
    HAL_NVIC_EnableIRQ(EXTI4_IRQn);		   //使能中断
}

//按键0被按下
void EXTI4_IRQHandler(void)//中断服务函数,在.s文件里  命名不能改动
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_4);//公共处理函数  内部调用 HAL_GPIO_EXTI_Callback  内部判断/清除PR寄存器标志位
    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_4);//清除标志位
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)//函数名不能变动
{
    delay_ms(20);//消抖
    
    if(GPIO_Pin == GPIO_PIN_4)
    {
        if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_4)==0)
        {
            HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
        }
    }
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/EXTI/exti.h"


int main(void)
{
    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟,72M */
    delay_init(72);                             /* 初始化延时函数 */
    led_init();                                 /* 初始化LED */
    exti_init();
    
    while(1)
    {
        LED1(1);                                /* LED1 灭 */
        delay_ms(500);
        LED1(0);                                /* LED1 亮 */
        delay_ms(500);
    }
}

程序解析

  1. 外部中断驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。外部中断(EXTI)
    驱动源码包括两个文件:exti.c 和exti.h。
    下面我们先解析exti.h 的程序。

⚫ 外部中断引脚定义
由硬件设计小节,我们知道KEY0、KEY1、KEY2 和KEY_UP 分别来连接到PE4、PE3、PE2 和PA0 上,我们做了下面的引脚定义。

/* 引脚和中断编号& 中断服务函数定义*/
#define KEY0_INT_GPIO_PORT 		GPIOE
#define KEY0_INT_GPIO_PIN 		GPIO_PIN_4
/* PE口时钟使能*/
#define KEY0_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)
#define KEY0_INT_IRQn 			EXTI4_IRQn
#define KEY0_INT_IRQHandler 	EXTI4_IRQHandler
#define KEY1_INT_GPIO_PORT 		GPIOE
#define KEY1_INT_GPIO_PIN 		GPIO_PIN_3
/* PE口时钟使能*/
#define KEY1_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)
#define KEY1_INT_IRQn 			EXTI3_IRQn
#define KEY1_INT_IRQHandler 	EXTI3_IRQHandler
#define KEY2_INT_GPIO_PORT 		GPIOE
#define KEY2_INT_GPIO_PIN 		GPIO_PIN_2
/* PE口时钟使能*/
#define KEY2_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)
#define KEY2_INT_IRQn 			EXTI2_IRQn
#define KEY2_INT_IRQHandler 	EXTI2_IRQHandler
#define WKUP_INT_GPIO_PORT 		GPIOA
#define WKUP_INT_GPIO_PIN 		GPIO_PIN_0
/* PA口时钟使能*/
#define WKUP_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define WKUP_INT_IRQn 			EXTI0_IRQn
#define WKUP_INT_IRQHandler 	EXTI0_IRQHandler

KEY0、KEY1、KEY2 和WK_UP 分别连接PE4、PE3、PE2 和PA0,即对应了EXTI4、EXTI3、EXTI2 和EXTI0 这三条外部中断线。这里需要注意的是EXTI0 到EXTI4 都是有单独的中断向量,EXTI5 到EXTI9 是公用EXTI9_5_IRQn ,EXTI10 到EXTI15 是公用EXTI15_10_IRQn。

下面我们再解析exti.c 的程序,
先看外部中断初始化函数,其定义如下:

/**
* @brief 外部中断初始化程序
* @param 无
* @retval 无
*/
void extix_init(void)
{
		GPIO_InitTypeDef gpio_init_struct;
		key_init();
		gpio_init_struct.Pin = KEY0_INT_GPIO_PIN;
		gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下升沿触发*/
		gpio_init_struct.Pull = GPIO_PULLUP;
		/* KEY0配置为下降沿触发中断*/
		HAL_GPIO_Init(KEY0_INT_GPIO_PORT, &gpio_init_struct);
		gpio_init_struct.Pin = KEY1_INT_GPIO_PIN;
		gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下升沿触发*/
		gpio_init_struct.Pull = GPIO_PULLUP;
		/* KEY1配置为下降沿触发中断*/
		HAL_GPIO_Init(KEY1_INT_GPIO_PORT, &gpio_init_struct);
		gpio_init_struct.Pin = KEY2_INT_GPIO_PIN;
		gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下升沿触发*/
		gpio_init_struct.Pull = GPIO_PULLUP;
		/* KEY2配置为下降沿触发中断*/
		HAL_GPIO_Init(KEY2_INT_GPIO_PORT, &gpio_init_struct);
		gpio_init_struct.Pin = WKUP_INT_GPIO_PIN;
		gpio_init_struct.Mode = GPIO_MODE_IT_RISING; /* 上升沿触发*/
		gpio_init_struct.Pull = GPIO_PULLDOWN;
		/* WKUP配置为下降沿触发中断*/
		HAL_GPIO_Init(WKUP_GPIO_PORT, &gpio_init_struct);
		HAL_NVIC_SetPriority(KEY0_INT_IRQn, 0, 2); /* 抢占0,子优先级2 */
		HAL_NVIC_EnableIRQ(KEY0_INT_IRQn); /* 使能中断线1 */
		HAL_NVIC_SetPriority(KEY1_INT_IRQn, 1, 2); /* 抢占1,子优先级2 */
		HAL_NVIC_EnableIRQ(KEY1_INT_IRQn); /* 使能中断线15 */
		HAL_NVIC_SetPriority(KEY2_INT_IRQn, 2, 2); /* 抢占2,子优先级2 */
		HAL_NVIC_EnableIRQ(KEY2_INT_IRQn); /* 使能中断线15 */
		HAL_NVIC_SetPriority(WKUP_INT_IRQn, 3, 2); /* 抢占3,子优先级2 */
		HAL_NVIC_EnableIRQ(WKUP_INT_IRQn); /* 使能中断线0 */
}

外部中断初始化函数主要做了两件事情,先是调用IO 口初始化函数HAL_GPIO_Init 来初始化IO 口,然后设置中断优先级并使能中断线。
4 个外部中断服务函数,用于产生中断事件时进行处理,其定义如下:

/**
* @brief KEY0 外部中断服务程序
* @param 无
* @retval 无
*/
void KEY0_INT_IRQHandler(void)
{
	/* 调用中断处理公用函数清除KEY0所在中断线的中断标志位*/
	HAL_GPIO_EXTI_IRQHandler(KEY0_INT_GPIO_PIN);
	/* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发*/
	__HAL_GPIO_EXTI_CLEAR_IT(KEY0_INT_GPIO_PIN);
}
/**
* @brief KEY1 外部中断服务程序
* @param 无
* @retval 无
*/
void KEY1_INT_IRQHandler(void)
{
	/* 调用中断处理公用函数清除KEY1所在中断线的中断标志位*/
	HAL_GPIO_EXTI_IRQHandler(KEY1_INT_GPIO_PIN);
	/* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发*/
	__HAL_GPIO_EXTI_CLEAR_IT(KEY1_INT_GPIO_PIN);
}
/**
* @brief KEY2 外部中断服务程序
* @param 无
* @retval 无
*/
void KEY2_INT_IRQHandler(void)
{
	/* 调用中断处理公用函数清除KEY2所在中断线的中断标志位*/
	HAL_GPIO_EXTI_IRQHandler(KEY2_INT_GPIO_PIN);
	/* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发*/
	__HAL_GPIO_EXTI_CLEAR_IT(KEY2_INT_GPIO_PIN);
}
/**
* @brief WK_UP 外部中断服务程序
* @param 无
* @retval 无
*/
void WKUP_INT_IRQHandler(void)
{
	/* 调用中断处理公用函数清除KEY_UP所在中断线的中断标志位*/
	HAL_GPIO_EXTI_IRQHandler(WKUP_INT_GPIO_PIN);
	/* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发*/
	__HAL_GPIO_EXTI_CLEAR_IT(WKUP_INT_GPIO_PIN);
}

所有的外部中断服务函数里都只调用了同样一个函数HAL_GPIO_EXTI_IRQHandler,该函数是外部中断共用入口函数,函数内部会进行中断标志位清零,并且调用中断处理共用回调函数HAL_GPIO_EXTI_Callback。但是它们的形参不同,我们的回调函数也是根据形参去判断是哪个IO 口的外部中断线被触发。
外部中断回调函数,其定义如下:

/**
* @brief 中断服务程序中需要做的事情
在HAL库中所有的外部中断服务函数都会调用此函数
* @param GPIO_Pin:中断引脚号
* @retval 无
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	delay_ms(20); /* 消抖*/
	switch(GPIO_Pin)
	{
		case KEY0_INT_GPIO_PIN:
		if (KEY0 == 0)
			{
				LED0_TOGGLE(); /* LED0 状态取反*/
				LED1_TOGGLE(); /* LED1 状态取反*/
			}
		break;
		case KEY1_INT_GPIO_PIN:
		if (KEY1 == 0)
			{
				LED0_TOGGLE(); /* LED0 状态取反*/
			}
		break;
		case KEY2_INT_GPIO_PIN:
			if (KEY2 == 0)
			{
				LED1_TOGGLE(); /* LED1 状态取反*/
			}
		break;
		case WKUP_INT_GPIO_PIN:
			if (WK_UP == 1)
			{
				BEEP_TOGGLE(); /* 蜂鸣器状态取反*/
			}
		break;
	}
}

我们在前面中断函数的处理过程中都调用了HAL_GPIO_EXTI_IRQHandler()这个接口,它主要帮我们进行了寄存器操作,清除了中断事件,清除完中断源后,调用中断回调函数
HAL_GPIO_EXTI_Callback,这个接口是一个__weak 的接口,我们通过重新实现这个函数来实现真正的外部中断控制逻辑。在该函数内部,通过判断IO 引脚号来确定中断是来自哪个IO 口,也就是哪个中断线,然后编写相应的控制逻辑。所以在该函数内部,我们通过switch 语句判断IO 口来源,例如是来自GPIO_PIN_0,那么一定是来自PA0,因为中断线一次只能连接一个IO口,而三个IO 口中引脚号为0 的IO 口只有PA0,所以中断线0 一定是连接PA0,也就是外部中断由PA0 触发。其他的引脚号的逻辑类似。

main.c 代码
在main.c 里面编写如下代码:

int main(void)
{
	HAL_Init(); /* 初始化HAL库*/
	sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72); /* 延时初始化*/
	usart_init(115200); /* 串口初始化为115200 */
	led_init(); /* 初始化LED */
	beep_init(); /* 初始化蜂鸣器*/
	extix_init(); /* 初始化外部中断输入*/
	LED0(0); /* 先点亮LED0 */
	while (1)
	{
		printf("OK\r\n");
		delay_ms(1000);
	}
}

首先是调用系统级别的初始化:初始化HAL 库、系统时钟和延时函数。接下来,调用串口初始化函数,调用led_init 来初始化LED 灯,调用extix_init 函数初始化外部中断,点亮红灯。最后在无限循环里面执行打印“OK”后1000ms 的重复动作。逻辑控制代码都在中断回调函数中完成。

下载验证

在下载好程序后,我们可以按KEY0、KEY1、KEY2 和KEY_UP 来看看LED 灯以及蜂鸣
器的变化,是否和我们预期的结果一致?

至此,我们的本章的学习就结束了。本章学习了STM32F103 外部中断的使用方法。

串口通信实验

本章我们将学习STM32F1 的串口,教大家如何使用STM32F1 的串口来发送和接收数据。

本章将实现如下功能:STM32F1 通过串口和上位机的对话,STM32F1 在收到上位机发过来的字符串后,原原本本的返回给上位机。

串口简介

学习串口前,我们先来了解一下数据通信的一些基础概念。

数据通信的基础概念

在单片机的应用中,数据通信是必不可少的一部分,比如:单片机和上位机、单片机和外围器件之间,它们都有数据通信的需求。由于设备之间的电气特性、传输速率、可靠性要求各不相同,于是就有了各种通信类型、通信协议,我们最常的有:USART、IIC、SPI、CAN、USB等。下面,我们先来学习数据通信的一些基础概念。

1. 数据通信方式

按数据通信方式分类,可分为串行通信和并行通信两种。串行和并行的对比如下图所示:

正点原子STM32(基于HAL库)2_第75张图片

正点原子STM32(基于HAL库)2_第76张图片

串行通信的基本特征是数据逐位顺序依次传输,优点是传输线少、布线成本低、灵活度高等优点,一般用于近距离人机交互,特殊处理后也可以用于远距离,缺点就是传输速率低。
而并行通信是数据各位可以通过多条线同时传输,优点是传输速率高,缺点就是布线成本高,抗干扰能力差因而适用于短距离、高速率的通信。

2. 数据传输方向

根据数据传输方向,通信又可分为全双工、半双工和单工通信。全双工、半双工和单工通信的比较如下图所示:

正点原子STM32(基于HAL库)2_第77张图片

  • 单工是指数据传输仅能沿一个方向,不能实现反方向传输,如校园广播。
  • 半双工是指数据传输可以沿着两个方向,但是需要分时进行,如对讲机。
  • 全双工是指数据可以同时进行双向传输,日常的打电话属于这种情形。

这里注意全双工和半双工通信的区别:半双工通信是共用一条线路实现双向通信,而全双工是利用两条线路,一条用于发送数据,另一条用于接收数据。

3. 数据同步方式

根据数据同步方式,通信又可分为同步通信和异步通信。同步通信和异步通信比较如下图所示:

正点原子STM32(基于HAL库)2_第78张图片

  • 同步通信要求通信双方共用同一时钟信号,在总线上保持统一的时序和周期完成信息传输。
    优点:可以实现高速率、大容量的数据传输,以及点对多点传输。
    缺点:要求发送时钟和接收时钟保持严格同步,收发双方时钟允许的误差较小,同时硬件复杂。

  • 异步通信不需要时钟信号,而是在数据信号中加入开始位停止位等一些同步信号,以便使接收端能够正确地将每一个字符接收下来,某些通信中还需要双方约定传输速率。优点:没有时钟信号硬件简单,双方时钟可允许一定误差。
    缺点:通信速率较低,只适用点对点传输。

4. 通信速率

在数字通信系统中,通信速率(传输速率)指数据在信道中传输的速度,它分为两种:传信率和传码率。

传信率(比特率):每秒钟传输的信息量,即每秒钟传输的二进制位数,单位为bit/s(即比特每秒),因而又称为比特率。

传码率(波特率):每秒钟传输的码元个数,单位为Baud(即波特每秒),因而又称为波特率。

比特率和波特率常被人混淆。比特率很好理解,波特率被传输的是码元,码元是信号被调制后的概念,每个码元都可以表示一定 bit 的数据信息量。举个例子,在 TTL 电平标准的通信中,用0V 表示逻辑0,5V 表示逻辑1,这时候这个码元就可以表示两种状态。如果电平信号0V、2V、4V 和6V 分别表示二进制数00、01、10、11,这时候每一个码元就可以表示四种状态。

由上述可以看出,码元携带一定的比特信息,所以比特率和波特率也是有一定的关系的。

比特率和波特率的关系可以用以下式子表示:

比特率= 波特率* log2M

其中M 表示码元承载的信息量。我们也可以理解M 为码元的进制数。

举个例子:波特率为100 Baud,即每秒传输100 个码元,如果码元采用十六进制编码(即M=2,代入上述式子),那么这时候的比特率就是400 bit/s。如果码元采用二进制编码(即M=2,代入上述式子),那么这时候的比特率就是100 bit/s。

可以看出采用二进制的时候,波特率和比特率数值上相等。但是这里要注意,它们的相等只是数值相等,其意义上不同,看波特率和波特率单位就知道。由于我们的所用的数字系统都是二进制的,所以有部分人久而久之就直接把波特率和比特率混淆了。

串口通信协议简介

正点原子STM32(基于HAL库)2_第79张图片

串口通信是一种设备间常用的串行通信方式,串口按位(bit)发送和接收字节。尽管比特字节(byte)的串行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。

串口通信协议是指规定了数据包的内容,内容包含了起始位、主体数据、校验位及停止位,双方需要约定一致的数据包格式才能正常收发数据的有关规范。在串口通信中,常用的协议包括RS-232、RS-422 和RS-485 等。

随着科技的发展,RS-232 在工业上还有广泛的使用,但是在商业技术上,已经慢慢的使用USB 转串口取代了RS-232 串口。我们只需要在电路中添加一个USB 转串口芯片,就可以实现USB 通信协议和标准UART 串行通信协议的转换,而我们开发板上的USB 转串口芯片是CH340C 这个芯片。关于USB 转串口芯片的原理图请看17.2 小节。

正点原子STM32(基于HAL库)2_第80张图片

正点原子STM32(基于HAL库)2_第81张图片

正点原子STM32(基于HAL库)2_第82张图片
正点原子STM32(基于HAL库)2_第83张图片

下面我们来学习串口通信协议,这里主要学习串口通信的协议层。

串口通信的数据包由发送设备的TXD 接口传输到接收设备的RXD 接口。在串口通信的协议层中,规定了数据包的内容,它由起始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据,其组成如图17.1.2.1 所示。

正点原子STM32(基于HAL库)2_第84张图片

正点原子STM32(基于HAL库)2_第85张图片

串口通信协议数据包组成可以分为波特率和数据帧格式两部分。

1. 波特率

本章主要讲解的是串口异步通信,异步通信是不需要时钟信号的,但是这里需要我们约定好两个设备的波特率。波特率表示每秒钟传送的码元符号的个数,所以它决定了数据帧里面每一个位的时间长度。两个要通信的设备的波特率一定要设置相同,我们常见的波特率是4800、9600、115200 等。

2. 数据帧格式

数据帧格式需要我们提前约定好,串口通信的数据帧包括起始位、停止位、有效数据位以及校验位。
⚫ 起始位和停止位
串口通信的一个数据帧是从起始位开始,直到停止位。数据帧中的起始位是由一个逻辑0的数据位表示,而数据帧的停止位可以是0.5、1、1.5 或2 个逻辑1 的数据位表示,只要双方约定一致即可。
⚫ 有效数据位
数据帧的起始位之后,就接着是数据位,也称有效数据位,这就是我们真正需要的数据,有效数据位通常会被约定为5、6、7 或者8 个位长。有效数据位是低位(LSB)在前,高位(MSB)在后。
⚫ 校验位
校验位可以认为是一个特殊的数据位。校验位一般用来判断接收的数据位有无错误,检验方法有:奇检验、偶检验、0 检验、1 检验以及无检验。下面分别介绍一下:

奇校验是指有效数据为和校验位中“1”的个数为奇数,比如一个8 位长的有效数据为:10101001,总共有4 个“1”,为达到奇校验效果,校验位设置为“1”,最后传输的数据是8 位的有效数据加上1 位的校验位总共9 位。

偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如数据帧:11001010,此时数据帧“1”的个数为4 个,所以偶校验位为“0”。

0 校验是指不管有效数据中的内容是什么,校验位总为“0”,1 校验是校验位总为“1”。

无校验是指数据帧中不包含校验位。

我们一般是使用无校验的情况。

STM32F1 的串口简介

STM32F103 的串口资源相当丰富,功能也相当强劲。STM32F103ZET6 最多可提供 5 路串口,有分数波特率发生器、支持同步单线通信和半双工单线通讯、支持LIN、支持调制解调器操作、智能卡协议和IrDA SIR ENDEC 规范、具有DMA 等。

STM32F1 的串口分为两种:USART(即通用同步异步收发器)和UART(即通用异步收发器)。UART 是在USART 基础上裁剪掉了同步通信功能,只剩下异步通信功能。简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用串口通信基本都是异步通信。

STM32F1 有3 个USART 和2 个UART,其中USART1 的时钟源来于APB2 时钟,其最大频率为72MHz,其他4 个串口的时钟源可以来于APB1 时钟,其最大频率为36MHz。

STM32 的串口输出的是TTL 电平信号,如果需要RS-232 标准的信号可使用MAX3232 芯片进行转换,而本实验我们是通过USB 转串口芯片CH340C 来与电脑的上位机进行通信。

USART 框图

下面先来学习如图17.1.3.1.1 所示的USART 框图,通过USART 框图引出USART 的相关知识,从而有了一个很好的整体掌握,对之后的编程也会有一个清晰的思路。

正点原子STM32(基于HAL库)2_第86张图片
正点原子STM32(基于HAL库)2_第87张图片

为了方便大家理解,我们把整个框图分成几个部分来介绍。

①USART 信号引脚

  • TX:发送数据输出引脚
  • RX:接收数据输入引脚
  • SCLK:发送器时钟输出,适用于同步传输
  • SW_RX:数据接收引脚,属于内部引脚,用于智能卡模式
  • IrDA_RDI:IrDA 模式下的数据输入
  • IrDA_TDO:IrDA 模式下的数据输出
  • nRTS:发送请求,若是低电平,表示USART 准备好接收数据
  • nCTS:清除发送,若是高电平,在当前数据传输结束时阻断下一次的数据发送

②数据寄存器
USART_DR 包含了已发送或接收到的数据。由于它本身就是两个寄存器组成的,一个专门
给发送用的(TDR),一个专门给接收用的(RDR)。该寄存器具备读和写的功能。
TDR 寄存器提供了内部总线和输出移位寄存器之间的并行接口。
RDR 寄存器提供了输入移位寄存器和内部总线之间的并行接口。
当进行数据发送操作时,往USART_DR 中写入数据会自动存储在TDR内;
当进行读取操作时,向USART_DR 读取数据会自动提取RDR 数据。

USART 数据寄存器(USART_DR)低 9 位数据有效,其他数据位保留。USART_DR 的第
9 位数据是否有效跟USART_CR1 的 M 位设置有关,当 M 位为 0 表示 8 位数据字长;当M 位为 1 时表示 9 位数据字长,一般使用 8 位数据字长。

当使能校验位(USART_CR1 中PCE 位被置位)进行发送时,写到 MSB 的值(根据数据的长度不同,MSB 是第7 位或者第8 位)会被后来的校验位取代。

③控制器
USART 有专门控制发送的发送器,控制接收的接收器,还有唤醒单元、中断控制等等,具
体在后面讲解USART 寄存器的时候细讲。

④时钟与波特率
这部分的主要功能就是为USART 提供时钟以及配置波特率。
波特率,即每秒钟传输的码元个数,在二进制系统中(串口的数据帧就是二进制的形式),
波特率与波特率的数值相等,所以我们今后在把串口波特率理解为每秒钟传输的二进制位数。

波特率通过以下公式得出:

正点原子STM32(基于HAL库)2_第88张图片

在这里插入图片描述

fck 是给串口的时钟(USART2\3\3\4\5 的时钟源为PCLK1,USART1 的时钟源为PCLK2),USARTDIV 是一个无符号的定点数,存放在波特率寄存器(USART_BRR)的低16 位,DIV_Man-tissa[11:0]存放的是USARTDIV 的整数部分,DIV_Fractionp[3:0]存放的是USARTDIV 的小数部分。

下面举个例子说明:
当串口1 设置需要得到115200 的波特率,fck = 72MHz,那么可得:
在这里插入图片描述

得到USARTDIV = 39.0625,分离USARTDIV 的整数部分与小数部分,整数部分为39,即0x27,那么DIV_Mantissa = 0x27;小数部分为0.0625,转化为十六进制即0.0625*16 = 1,所以DIV_Fractionp = 0x1,USART_BRR 寄存器应该赋值为0x271,成功设置波特率为115200。

正点原子STM32(基于HAL库)2_第89张图片
正点原子STM32(基于HAL库)2_第90张图片

HAL库很简单,只需将对应的波特率赋值到结构体成员就可以了

正点原子STM32(基于HAL库)2_第91张图片

值得注意USARTDIV 是允许有余数的,我们用四舍五入进行取整,这样会导致波特率会有
所偏差,而这样的小误差是可以被允许的。

USART 寄存器

正点原子STM32(基于HAL库)2_第92张图片
正点原子STM32(基于HAL库)2_第93张图片
正点原子STM32(基于HAL库)2_第94张图片
正点原子STM32(基于HAL库)2_第95张图片
正点原子STM32(基于HAL库)2_第96张图片
正点原子STM32(基于HAL库)2_第97张图片

使用STM32F103 的USART 的配置步骤在《STM32F10xxx 参考手册_V10(中文版).pdf》中有列出,这里我们引用手册中的配置步骤:

  1. 通过在USART_CR1 寄存器上置位 UE 位来激活USART。
  2. 编程USART_CR1 的 M 位来定义字长。
  3. 在USART_CR2 中编程停止位的位数。
  4. 如果采用多缓冲器通信,配置USART_CR3 中的DMA 使能位(DMAT)。按多缓冲器通
    信中的描述配置 DMA 寄存器。
  5. 利用USART_BRR 寄存器选择要求的波特率。
  6. 设置USART_CR1 中的 TE 位,发送一个空闲帧作为第一次数据发送。
  7. 把要发送的数据写进USART_DR 寄存器(此动作清除TXE 位)。在只有一个缓冲器的情
    况下,对每个待发送的数据重复步骤7。
  8. 在USART_DR 寄存器中写入最后一个数据字后,要等待TC=1,它表示最后一个数据帧的传输结束。当需要关闭USART 或需要进入停机模式之前,需要确认传输结束,避免破坏最后一次传输。

我们按照上面的步骤配置就可以使用STM32F103 的串口了:只要你开启了串口时钟,并设置相应IO 口的模式,然后配置一下波特率,数据位长度,奇偶校验位等信息,就可以使用了。总结地来说,我们要学会配置USART 对应的寄存器就可以使用串口功能了。

下面,我们就简单介绍下这几个与串口基本配置直接相关的寄存器。
(1)串口时钟使能
串口作为STM32F103 的一个外设,其时钟由外设时钟使能寄存器控制,这里我们使用的
串口1 是在APB2ENR 寄存器的第14 位。
注意:除了串口1 的时钟使能在APB2ENR 寄存器,其他串口的时钟使能位都在APB1ENR
寄存器,而APB2(72M)的频率一般是APB1(36M)的一倍。
(2)串口复位
一般系统刚开始配置外设的时候,都会先执行复位该外设的操作,可以使外设的对应寄存
器恢复到默认值,方便我们进行配置。串口1 的复位就是通过配置APB2RSTR 寄存器的第14位来实现的。APB2RSTR 寄存器的描述如图17.1.3.2.1 所示:
正点原子STM32(基于HAL库)2_第98张图片

(3)串口波特率设置
每个串口都有一个自己独立的波特率寄存器USART_BRR,通过设置该寄存器就可以达到配置不同波特率的目的。在前面的USART 框图部分描述过,为了让大家更好了解波特率寄存器,下面截取USART_BRR 寄存器图,如图17.1.3.2.2 所示:

正点原子STM32(基于HAL库)2_第99张图片

(4)串口控制寄存器
STM32F103 每个串口都有3 个控制寄存器USART_CR1~3,串口的很多配置都是通过这3个寄存器来设置的。USART_CR1 寄存器的描述如图17.1.3.2.3 所示:

正点原子STM32(基于HAL库)2_第100张图片

该寄存器的高18 位没有用到,低14 位用于串口的功能设置。我们在这里只介绍需要用到
的一些位,其他位可以参考《STM32F10xxx 参考手册_V10(中文版).pdf》。

  • UE 为串口使能位,通过该位置1,使能串口。
  • M 为字长,当该位为0 的时候设置串口为8 个字长外加n 个停止位,停止位的个数(n)是根据USART_CR2 的[13:12]位设置来决定的,默认为0。
  • PCE 为校验使能位,设置为0,即禁止校验,否则使能校验。
  • PS 为校验位选择,设置为0 为偶校验,否则奇校验。
  • TXIE 为发送缓冲区空中断使能位,设置该位为1,当USART_SR 中的TXE 位为1 时,将产生串口中断。
  • TCIE 为发送完成中断使能位,设置该位为1,当USART_SR 中的TC 位为1时,将产生串口中断。
  • RXNEIE 为接收缓冲区非空中断使能,设置该位为1,当USART_SR 中的ORE 或者RXNE 位为1 时,将产生串口中断。
  • TE 为发送使能位,设置为1,将开启串口的发送功能。
  • RE 为接收使能位,用法同TE。

(5)数据发送与接收
STM32 的发送与接收是通过数据寄存器USART_DR 来实现的,这是一个双寄存器,包含
了TDR 和RDR。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也是存在该寄存器内。在前面的USART 框图已经对USART_DR 有详细的介绍,大家可以自行查阅。下面看一下寄存器的各位描述如图17.1.3.2.4。

正点原子STM32(基于HAL库)2_第101张图片
(6)串口状态
串口状态通过状态寄存器USART_SR 读取。USART_SR 的各位描述如图17.1.3.2.5 所示:
正点原子STM32(基于HAL库)2_第102张图片
这里我们关注一下两个位,第5、6 位RXNE 和TC。

  • RXNE(读数据寄存器非空),当该位被置1 的时候,就是提示已经有数据被接收到了,并且可以读出来了。这时候我们要做的就是尽快去读取USART_DR,通过读USART_DR 可以将该位清零,也可以向该位写0,直接清除。
  • TC(发送完成),当该位被置位的时候,表示USART_DR 内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。该位也有两种清零方式:
    1)读USART_SR,写USART_DR。
    2)直接向该位写0

通过以上一些寄存器的操作再加上IO 口的配置,我们就可以达到串口最基本的配置了,关于串口更详细的介绍,请参考《STM32F10xxx 参考手册_V10(中文版).pdf》关于通用同步异步收发器这一章的相关知识。

GPIO 引脚复用功能

正点原子STM32(基于HAL库)2_第103张图片
正点原子STM32(基于HAL库)2_第104张图片

正点原子STM32(基于HAL库)2_第105张图片

AF7这一路会复用PA9、PA10为串口发送和接收正点原子STM32(基于HAL库)2_第106张图片正点原子STM32(基于HAL库)2_第107张图片
正点原子STM32(基于HAL库)2_第108张图片

我们知道芯片有许多外设,而引脚的资源是很有限的,为了解决这个问题,方法就是引脚
复用,这样使得引脚除了作为普通的IO 口之外,还会与一些外设关联起来,作为第二功能使用,而且一个引脚不单单只有一种复用功能,而是拥有多个第二功能,但是一次只允许一个外设的复用功能,以确保共用同一个IO 引脚的外设之间不会产生冲突。

下面我们把之前没讲解的复用功能寄存器AFIO 讲解一下。

AFIO 寄存器的作用就是复用功能I/O 和调试配置的,STM32F103ZET6 共有6 个AFIO 的
寄存器,事件控制寄存器AFIO_EVCR、复用重映射和调试I/O 配置寄存器AFIO_MAPR、外部中断配置寄存器AFIO_EXTICR1、外部中断配置寄存器AFIO_EXTICR2、外部中断配置寄存器AFIO_EXTICR3 和外部中断配置寄存器AFIO_EXTICR4。

在对这些寄存器进行读写操作前,应先打开AFIO 时钟,该时钟在RCC_APB2ENR 寄存器
上的位0 上配置,在位0 上置0 表示辅助功能IO 时钟关闭;在位0 上置1 表示辅助功能IO 时
钟开启。

事件控制寄存器AFIO_EVCR,用得比较少这里不作过多介绍。

AFIO_EXTICRx 寄存器,在中断章节再来详细讲解,本章节不涉及这些寄存器的配置。
复用重映射和调试I/O 配置寄存器AFIO_MAPR 寄存器描述,如图17.1.4.1 所示。

正点原子STM32(基于HAL库)2_第109张图片

在对AFIO_MAPR 寄存器某些位进行写入实现引脚的重新映射,这时候,复用功能不再映
射到它们原始分配上。例如AFIO_MAPR 寄存器位2 是对USART1 的重映射,置0: 没有重映
像(TX/PA9,RX/PA10);置1: 重映像(TX/PB6,RX/PB7)。默认情况下,PA9 和PA10 是作为串口1 的引脚使用,假如PA9 和PA10 被用作其他地方,但还是需要用到串口1,那么就可以在AFIO_MAPR 的位2 置1,把串口1 的引脚重映射到PB6 和PB7。这个串口初始化的过程,就有点变化,需要初始化AFIO 时钟,和对AFIO_MAPR 的第2 位进行置1 操作,其他与普通串口配置没有区别。

HAL 库关于端口复用相关的代码在STM32F1xx_hal_gpio_ex.h 文件中可以找到,USART1
重映射操作代码如下:

/**
* @brief Enable the remapping of USART1 alternate function TX and RX.
* @note ENABLE: Remap (TX/PB6, RX/PB7)
* @retval None
*/
#define __HAL_AFIO_REMAP_USART1_ENABLE() \
								AFIO_REMAP_ENABLE(AFIO_MAPR_USART1_REMAP)
/**
* @brief Disable the remapping of USART1 alternate function TX and RX.
* @note DISABLE: No remap (TX/PA9, RX/PA10)
* @retval None
*/
#define __HAL_AFIO_REMAP_USART1_DISABLE() \
								AFIO_REMAP_DISABLE(AFIO_MAPR_USART1_REMAP)

以上是使用重映射的串口1 的介绍。

硬件设计

  1. 例程功能
    LED0 闪烁,提示程序在运行。STM32 通过串口1 和上位机对话,STM32 在收到上位机发过来的字符串(以回车换行结束)后,会返回给上位机。同时每隔一定时间,通过串口1 输出一段信息到电脑。
  2. 硬件资源
    1)LED 灯
    LED0 – PB5
    2)串口1 (PA9/PA10 连接在板载USB 转串口芯片CH340C 上面) ,需要跳线帽连接。
  3. 原理图
    USB 转串口硬件部分的原理图,如图17.2.1 所示:
    图17.2.1 USB 转串口原理图

正点原子STM32(基于HAL库)2_第110张图片

这里需要注意的是:上图中的P4 的RXD 和PA9 用跳线帽连接,以及TXD 和PA10 也用
跳线帽连接。如图17.2.2 所示:

正点原子STM32(基于HAL库)2_第111张图片

程序设计

回调机制

正点原子STM32(基于HAL库)2_第112张图片

正点原子STM32(基于HAL库)2_第113张图片
正点原子STM32(基于HAL库)2_第114张图片
正点原子STM32(基于HAL库)2_第115张图片
正点原子STM32(基于HAL库)2_第116张图片

USART 的HAL 库驱动

HAL 库中关于串口的驱动程序比较多,我们主要先来学习本章需要用到的,其余的后续用到再讲解。因为我们现在只是用到异步收发器功能,所以我们现在只需要STM32F1xx_hal_uart.c文件(及其头文件)的驱动代码,STM32F1xx_hal_usart.c 是通用同步异步收发器,暂时没有用到,可以暂时不看。用到一个外设第一个函数就应该是其初始化函数。

  1. HAL_UART_Init 函数
    要使用一个外设首先要对它进行初始化,所以先看串口的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);

⚫ 函数描述:
用于初始化异步模式的收发器。
⚫ 函数形参:
形参1 是串口的句柄,结构体类型是UART_HandleTypeDef,其定义如下:

typedef struct
{
	USART_TypeDef 		*Instance; 		/* UART寄存器基地址*/
	UART_InitTypeDef 	Init; 			/* UART通信参数*/
	uint8_t 			*pTxBuffPtr; 	/* 指向UART 发送缓冲区*/
	uint16_t 			TxXferSize; 	/* UART发送数据的大小*/
	__IO uint16_t 		TxXferCount; 	/* UART发送数据的个数*/
	uint8_t 			*pRxBuffPtr; 	/* 指向UART接收缓冲区*/
	uint16_t 			RxXferSize; 	/* UART接收数据大小*/
	__IO uint16_t 		RxXferCount; 	/* UART接收数据的个数*/
	DMA_HandleTypeDef 	*hdmatx; 		/* UART 发送参数设置(DMA)*/
	DMA_HandleTypeDef 	*hdmarx; 		/* UART 接收参数设置(DMA)*/
	HAL_LockTypeDef 	Lock; 			/* 锁定对象*/
	__IO HAL_UART_StateTypeDef 	gState; /* UART发送状态结构体*/
	__IO HAL_UART_StateTypeDef 	RxState; /* UART接收状态结构体*/
	__IO uint32_t 				ErrorCode; /* UART操作错误信息*/
}UART_HandleTypeDef;
  1. Instance:指向UART 寄存器基地址。实际上这个基地址HAL 库已经定义好了,可以选择范围:USART1~ USART3、UART4、UART5。
  2. Init:UART 初始化结构体,用于配置通讯参数,如波特率、数据位数、停止位等等。下面我们再详细讲解这个结构体。
  3. pTxBuffPtr,TxXferSize,TxXferCount:分别是指向发送数据缓冲区的指针,发送数据的大小,发送数据的个数。
  4. pRxBuffPtr,RxXferSize,RxXferCount:分别是指向接收数据缓冲区的指针,接受数据的大小,接收数据的个数;
  5. hdmatx,hdmarx:配置串口发送接收数据的 DMA 具体参数。
  6. Lock:对资源操作增加操作锁保护功能,可选HAL_UNLOCKED 或者HAL_LOCKED 两个参数。如果gState 的值等于HAL_UART_STATE_RESET,则可认为串口未被初始化,此时,分配锁资源,并且调用HAL_UART_MspInit 函数来对串口的GPIO 和时钟进行初始化。
  7. gState,RxState:分别是UART 的发送状态、工作状态的结构体和UART 接受状态的结构体。HAL_UART_StateTypeDef 是一个枚举类型,列出串口在工作过程中的状态值,有些值只适用于gState,如HAL_UART_STATE_BUSY。
  8. ErrorCode:串口错误操作信息。主要用于存放串口操作的错误信息。

下面,我们来了解UART_InitTypeDef 这个结构体类型,该结构体用于配置UART 的各个通信参数,包括波特率,停止位等,具体说明如下:

typedef struct
{
	uint32_t BaudRate; 		/* 波特率*/
	uint32_t WordLength; 	/* 字长*/
	uint32_t StopBits; 		/* 停止位*/
	uint32_t Parity; 		/* 校验位*/
	uint32_t Mode; 			/* UART模式*/
	uint32_t HwFlowCtl; 	/* 硬件流设置*/
	uint32_t OverSampling; 	/* 过采样设置*/
}UART_InitTypeDef;

1)BaudRate:波特率设置。一般设置为2400、9600、19200、115200。
2)WordLength:数据帧字长,可选8 位或9 位。这里我们设置为8 位字长数据格式。
3)StopBits:停止位设置,可选0.5 个、1 个、1.5 个和2 个停止位,一般我们选择1 个停止位。
4)Parity:奇偶校验控制选择,我们设定为无奇偶校验位。
5)Mode:UART 模式选择,可以设置为只收模式,只发模式,或者收发模式。这里我们设置为全双工收发模式。
6)HwFlowCtl:硬件流控制选择,我们设置为无硬件流控制。
7)OverSampling:过采样选择,选择8 倍过采样或者16 过采样,一般选择16 过采样。

⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值,有4 个,分别是HAL_OK 表示成功,HAL_ERROR 表
示错误,HAL_BUSY 表示忙碌,HAL_TIMEOUT 超时。后续遇到该结构体也是一样的。

  1. HAL_UART_Receive_IT 函数
    HAL_UART_Receive_IT 函数是开启串口接收中断函数。其声明如下:
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart,
										uint8_t *pData, uint16_t Size);

⚫ 函数描述:
用于开启以中断的方式接收指定字节。数据接收在中断处理函数里面实现。
⚫ 函数形参:
形参1 是UART_HandleTypeDef 结构体指针类型的串口句柄。
形参2 是要接收的数据地址。
形参3 是要接收的数据大小,以字节为单位。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。

正点原子STM32(基于HAL库)2_第117张图片

  1. HAL_UART_IRQHandler 函数
    HAL_UART_IRQHandler 函数是HAL 库中断处理公共函数。其声明如下:
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);

⚫ 函数描述:
该函数是HAL 库中断处理公共函数,在串口中断服务函数中被调用。
⚫ 函数形参:
形参1 是UART_HandleTypeDef 结构体指针类型的串口句柄。
⚫ 函数返回值:

⚫ 注意事项:
该函数是HAL 库已经定义好,用户一般不能随意修改。如果用户要在中断中实现自己的逻
辑代码,可以直接在函数HAL_UART_IRQHandler 的前面或者后面添加新代码,也可以直接在HAL_UART_IRQHandler 调用的各种回调函数里面执行,这些回调都是弱定义的,方便用户直接在其它文件里面重定义。串口回调函数主要有下面几个:

__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_AbortCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef *huart)

本实验我们用到的是接收回调函数HAL_UART_RxCpltCallback,就是在接收回调函数里面
编写我们的接收逻辑代码,具体请参考实验源码。

串口通讯配置步骤
正点原子STM32(基于HAL库)2_第118张图片

1)串口参数初始化(波特率、字长、奇偶校验等)
HAL 库通过调用串口初始化函数HAL_UART_Init 完成对串口参数初始化,详见例程源码注意:该函数会调用:HAL_UART_MspInit 函数来完成对串口底层的初始化,包括:串口及GPIO 时钟使能、GPIO 模式设置、中断设置等。
2)使能串口和GPIO 口时钟
本实验用到USART1 串口,使用PA9 和PA10 作为串口的TX 和RX 脚,因此需要先使能
USART1 和GPIOA 时钟。参考代码如下:

__HAL_RCC_USART1_CLK_ENABLE(); /* 使能USART1时钟*/
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能GPIOA时钟*/

3)GPIO 模式设置(速度、上下拉、复用功能等)
GPIO 模式设置通过调用HAL_GPIO_Init 函数实现,详见本例程源码。
4)开启串口相关中断,配置串口中断优先级
本实验我们使用串口中断来接收数据。我们使用HAL_UART_Receive_IT 函数开启串口中
断接收,并设置接收buffer 及其长度。通过HAL_NVIC_EnableIRQ 函数使能串口中断,通过HAL_NVIC_SetPriority 函数设置中断优先级。
5)编写中断服务函数
串口1 中断服务函数为:USART1_IRQHandler,当发生中断的时候,程序就会执行中断服
务函数。HAL 库为了使用方便,提供了一个串口中断通用处理函数HAL_UART_IRQHandler,该函数在串口接收完数据后,又会调用回调函数HAL_UART_RxCpltCallback ,用于给用户处理串口接收到的数据。
因此我们需要在HAL_UART_RxCpltCallback 函数实现数据接收处理,详见本例程源码。
6)串口数据接收和发送
最后我们可以通过读写USART_DR 寄存器,完成串口数据的接收和发送,HAL 库也给我
们提供了:HAL_UART_Receive 和HAL_UART_Transmit 两个函数用于串口数据的接收和发送。

大家可以根据实际情况选择使用那种方式来收发串口数据。

课堂源码(发送接收一个字节)

uart.h

#ifndef __USART_H
#define __USART_H

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


extern UART_HandleTypeDef g_uart1_handle;       /* HAL UART句柄 */
extern uint8_t g_rx_buffer[1];                  /* HAL库使用的串口接收数据缓冲区 */
extern uint8_t g_usart1_rx_flag;                /* 串口接收到数据标志 */

void usart_init(uint32_t bound);                /* 串口初始化函数 */

#endif

uart.c

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


/******************************************************************************************/
/* 加入以下代码, 支持printf函数, 而不需要选择use MicroLIB */

#if 1

#if (__ARMCC_VERSION >= 6010050)            /* 使用AC6编译器时 */
__asm(".global __use_no_semihosting\n\t");  /* 声明不使用半主机模式 */
__asm(".global __ARM_use_no_argv \n\t");    /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */

#else
/* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
#pragma import(__use_no_semihosting)

struct __FILE
{
    int handle;
    /* Whatever you require here. If the only file you are using is */
    /* standard output using printf() for debugging, no file handling */
    /* is required. */
};

#endif

/* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
int _ttywrch(int ch)
{
    ch = ch;
    return ch;
}

/* 定义_sys_exit()以避免使用半主机模式 */
void _sys_exit(int x)
{
    x = x;
}

char *_sys_command_string(char *cmd, int len)
{
    return NULL;
}


/* FILE 在 stdio.h里面定义. */
FILE __stdout;


/* MDK下需要重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
int fputc(int ch, FILE *f)
{
    while ((USART1->SR & 0X40) == 0);     /* 等待上一个字符发送完成 */

    USART1->DR = (uint8_t)ch;             /* 将要发送的字符 ch 写入到DR寄存器 */
    return ch;
}
#endif


/******************************************************************************************/

uint8_t g_rx_buffer[1];          /* HAL库使用的串口接收数据缓冲区 */
uint8_t g_usart1_rx_flag = 0;    /* 串口接收到数据标志 */


UART_HandleTypeDef g_uart1_handle;  /* UART句柄 */
/* 串口1初始化函数 */
void usart_init(uint32_t baudrate)
{
    g_uart1_handle.Instance = 			USART1;
    g_uart1_handle.Init.BaudRate = 		baudrate;
    g_uart1_handle.Init.WordLength = 	UART_WORDLENGTH_8B;
    g_uart1_handle.Init.StopBits = 		UART_STOPBITS_1;
    g_uart1_handle.Init.Parity = 		UART_PARITY_NONE;
    g_uart1_handle.Init.HwFlowCtl = 	UART_HWCONTROL_NONE;
    g_uart1_handle.Init.Mode = 			UART_MODE_TX_RX;
    HAL_UART_Init(&g_uart1_handle);
    
    HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t*)g_rx_buffer, 1);//开启串口接收中断
}

/* 串口MSP回调函数 */
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    GPIO_InitTypeDef gpio_init_struct;
    if(huart->Instance == USART1)                /* 如果是串口1,进行串口1 MSP初始化 */
    {
        __HAL_RCC_USART1_CLK_ENABLE();//使能时钟
        __HAL_RCC_GPIOA_CLK_ENABLE();

        gpio_init_struct.Pin = GPIO_PIN_9;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;            /* 推挽式复用输出 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;      /* 高速 */
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);            /* 初始化串口1的TX引脚 */
        
        gpio_init_struct.Pin = GPIO_PIN_10;
        gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;         /* 输入 */
        gpio_init_struct.Pull = GPIO_PULLUP;                /* 上拉  查看时序,空闲时为高电平*/
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);            /* 初始化串口1的RX引脚 */
        
        HAL_NVIC_SetPriority(USART1_IRQn, 3, 3);//设置优先级
        HAL_NVIC_EnableIRQ(USART1_IRQn);		//使能中断
    }
}

/* 串口1中断服务函数 */
void USART1_IRQHandler(void)
{
    HAL_UART_IRQHandler(&g_uart1_handle);//清除中断,并调用回调函数
    HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t*)g_rx_buffer, 1);//再次开启接收中断
}

/* 串口数据接收完成回调函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART1)
    {
        g_usart1_rx_flag = 1;//串口1接收到数据标志
    }
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"

int main(void)
{
    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟,72M */
    delay_init(72);                             /* 初始化延时函数 */
    led_init();                                 /* 初始化LED */
    usart_init(115200);                         /* 波特率设为115200 */
    
    printf("请输入一个英文字符:\r\n\r\n");
    while(1)
    {
        if(g_usart1_rx_flag == 1)
        {
            printf("您输入的字符为:\r\n");							//1字节  超时
            HAL_UART_Transmit(&g_uart1_handle, (uint8_t*)g_rx_buffer, 1, 1000);
            while(__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_TC) != 1);//状态寄存器SR位6 发送完成标志位
            printf("\r\n");
            g_usart1_rx_flag = 0;
        }
        else
        {
            delay_ms(10);
        }
    }
}

程序流程图

正点原子STM32(基于HAL库)2_第119张图片

程序解析

正点原子STM32(基于HAL库)2_第120张图片
正点原子STM32(基于HAL库)2_第121张图片

  1. 串口1 驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。串口1(USART1)驱动源码包括两个文件:usart.c 和usart.h。
    下面我们先解析usart.h 的程序。
    ⚫ 串口1 引脚定义

由硬件设计小节,我们知道PA9 和PA10 分别被复用为串口1 的发送和接收引脚,我们做了下面的引脚定义。

/* 串口1的GPIO */
#define USART_TX_GPIO_PORT GPIOA
#define USART_TX_GPIO_PIN GPIO_PIN_9
/* 发送引脚时钟使能*/
#define USART_TX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define USART_RX_GPIO_PORT GPIOA
#define USART_RX_GPIO_PIN GPIO_PIN_10
/* 接收引脚时钟使能*/
#define USART_RX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define USART_UX USART1
#define USART_UX_IRQn USART1_IRQn
#define USART_UX_IRQHandler USART1_IRQHandler
/* USART1 时钟使能*/
#define USART_UX_CLK_ENABLE() do{ __HAL_RCC_USART1_CLK_ENABLE(); }while(0)

USART1_IRQn 也就是我们中断向量表的37 号中断,USART1_IRQHandler 是串口1 的中
断服务函数。每个串口都有自己的中断函数,但是我们最终都是通过回调函数去实现逻辑代码,当然我们亦可在中断函数里实现逻辑代码。

另外我们还定义了三个宏,具体如下:

#define USART_REC_LEN 200 /* 定义最大接收字节数200 */
#define USART_EN_RX 1 /* 使能(1)/禁止(0)串口1接收*/
#define RXBUFFERSIZE 1 /* 缓存大小*/

可以看到USART_REC_LEN 表示最大接收字节数,这里定义的是200 个字节,后续如果
有需求要发送更大的数据包,可以改大这个值,这里不改太大,是避免浪费太多内存。
USART_EN_RX 则是用于使能串口1 的接收数据。RXBUFFERSIZE 是缓冲大小。

下面我们再解析usart.c 的程序,先看串口1 的初始化函数,其定义如下:

/**
* @brief 串口X初始化函数
* @param baudrate: 波特率, 根据自己需要设置波特率值
* @note 注意: 必须设置正确的时钟源, 否则串口波特率就会设置异常.
* 这里的USART的时钟源在sys_stm32_clock_init()函数中已经设置过了.
* @retval 无
*/
void usart_init(uint32_t baudrate)
{
	uartx_handle.Instance = USART_UX; /* USART1 */
	uartx_handle.Init.BaudRate = baudrate; /* 波特率*/
	uartx_handle.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式*/
	uartx_handle.Init.StopBits = UART_STOPBITS_1; /* 一个停止位*/
	uartx_handle.Init.Parity = UART_PARITY_NONE; /* 无奇偶校验位*/
	uartx_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控*/
	uartx_handle.Init.Mode = UART_MODE_TX_RX; /* 收发模式*/
	HAL_UART_Init(&uartx_handle); /* HAL_UART_Init()会使能UART1 */
	/*该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量*/
	HAL_UART_Receive_IT(&uartx_handle, (uint8_t *)aRxbuffer, RXBUFFERSIZE);
}

uartx_handle 是结构体UART_HandleTypeDef 类型的全局变量,UART_HandleTypeDef 结
构体成员的含义请回到前面回顾。波特率我们直接赋值给uartx_handle.Init.BaudRate 这个成员,可以看出很方便。需要注意的是,最后一行代码调用函数HAL_UART_Receive_IT,作用是开启接收中断,同时设置接收的缓存区以及接收的数据量。

上面的初始化函数只是串口初始化的其中一部分,我们还有一部分初始化需要
HAL_UART_MspInit 函数去完成。HAL_UART_MspInit 是HAL 库定义的弱定义函数,这里我们做重定义以实现我们的初始化需求。HAL_UART_MspInit 函数在HAL_UART_Init 函数中会被调用,其定义如下:

/**
* @brief UART底层初始化函数
* @param huart: UART句柄类型指针
* @note 此函数会被HAL_UART_Init()调用
* 完成时钟使能,引脚配置,中断配置
* @retval 无
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
	GPIO_InitTypeDef gpio_init_struct;
	if(huart->Instance == USART1) /* 如果是串口1,进行串口1 MSP初始化*/
	{
		USART_UX_CLK_ENABLE(); /* USART1 时钟使能*/
		USART_TX_GPIO_CLK_ENABLE(); /* 发送引脚时钟使能*/
		USART_RX_GPIO_CLK_ENABLE(); /* 接收引脚时钟使能*/
		gpio_init_struct.Pin = USART_TX_GPIO_PIN; /* TX引脚*/
		gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出*/
		gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉*/
		gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
		HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_init_struct); /* 初始化发送引脚*/
		gpio_init_struct.Pin = USART_RX_GPIO_PIN; /* RX引脚*/
		gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;
		HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_init_struct); /* 初始化接收引脚*/
		#if USART_EN_RX
		HAL_NVIC_EnableIRQ(USART_UX_IRQn); /* 使能USART1中断通道*/
		HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3); /* 抢占优先级3,子优先级3 */
		#endif
	}
}

该函数主要实现底层的初始化,事实上这个函数的代码还可以直
接放到usart_init 函数里面,但是HAL 库为了代码的功能分层初始化,定义这个函数方便用户使用。所以我们也按照HAL 库的这个结构来初始化外设。这个函数首先是调用if(huart->Instance == USART1)判断是要初始化那个串口,因为每个串口初始化都会调用HAL_UART_MspInit 这个函数,所以需要判断是哪个串口要初始化才做相应的处理。只能说HAL 库这样的结构机制有好处,自然也有坏处。

首先就是使能串口以及PA9 和PA10 的时钟,PA9 和PA10 需要用做复用功能,复用功能模
式有两个选择:GPIO_MODE_AF_PP 推挽式复用和GPIO_MODE_AF_OD 开漏式复用,我们选择的是推挽式复用,因为PA9 是一个发送管脚,所以模式设置为复用推挽输出,而PA10 是一个接收管脚,所以它的模式设置为浮空输入即可,GPIO_MODE_AF_INPUT 实质就是一个GPIO_MODE_INPUT 浮空输入模式。选择了推挽式复用。然后就是调用HAL_GPIO_Init 函数进行IO 口的初始化。

最后因为我们用到串口中断,所以还需要中断相关的配置。HAL_NVIC_EnableIRQ 函数使
能串口1 复用通道。HAL_NVIC_SetPriority 函数配置串口中断的抢占优先级以及响应优先级。

串口初始化由上述两个函数完成,下面就该讲到串口中断服务函数了,其定义如下:

/**
* @brief 串口X中断服务函数
注意,读取USARTx->SR能避免莫名其妙的错误
* @param 无
* @retval 无
*/
void USART_UX_IRQHandler(void)
{
	#if SYSTEM_SUPPORT_OS /*使用OS*/
	OSIntEnter();
	#endif
	
	HAL_UART_IRQHandler(&uartx_handler); /*调用HAL库中断处理公用函数  清标志,清中断使能*/
	while (HAL_UART_Receive_IT(&uartx_handler, (uint8_t *)aRxBuffer,
		RXBUFFERSIZE) != HAL_OK) /*一次处理完成之后,重新开启中断并设置RxXferCount为1*/
	{
		/* 如果出错会卡死在这里*/
	}
	
	#if SYSTEM_SUPPORT_OS /*使用OS*/
	OSIntExit();
	#endif
}

从代码逻辑可以看出,在中断服务函数内部通过调用HAL_UART_GetState 函数获取串口
状态,计数处理时间是否超时,然后完成一次传输后,调用UART_Receive_IT 函数重新开启中断。UART_Receive_IT 函数的作用就是把每次中断接收到的字符保存在串口句柄的缓存指针pRxBuffPtr 中,同时每次接收一个字符,其计数器RxXferCount 减1,直到接收完成RxXferSize个字符之后RxXferCount 设置为0,同时调用接收回调函数HAL_UART_RxCpltCallback 进行处理。HAL_MAX_DELAY 最大延时时间在STM32F1xx_hal_def.h 中定义。

下面列出串口接收中断的一般流程,如图17.3.3.1 所示:

正点原子STM32(基于HAL库)2_第122张图片

串口接收回调函数定义如下:

/**
* @brief UART数据接收回调接口
数据处理在这里进行
* @param huart:串口句柄
* @retval 无
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
        if(huart->Instance == USART_UX) /* 如果是串口1 */
        {
                if((g_usart_rx_sta & 0x8000) == 0) /* 接收未完成  位15是否为0*/
                {
                        if(g_usart_rx_sta & 0x4000) /* 接收到了0x0d(即回车键)*/
                        {
                                if(aRxBuffer[0] != 0x0a) /* 接收到的不是0x0a(即不是换行键)*/
                                {
                                        g_usart_rx_sta = 0; /*接收错误,重新开始*/
                                }
                                else /* 接收到的是0x0a(即换行键)*/
                                {
                                        g_usart_rx_sta |= 0x8000; /* 接收完成了*/
                                }
                        }
                        else /* 还没收到0X0D(即回车键)*/
                        {
                                if (aRxBuffer[0] == 0x0d)//收到回车
                                        g_usart_rx_sta |= 0x4000;
                                else
                                {
                                        g_usart_rx_buf[g_usart_rx_sta & 0X3FFF] = aRxBuffer[0];
                                        g_usart_rx_sta++;//收到字节数累加
                                        if (g_usart_rx_sta > (USART_REC_LEN - 1))//超过接收缓冲大小
                                        {
                                                g_usart_rx_sta = 0; /* 接收数据错误,重新开始接收*/
                                        }
                                }
                        }
                }
        }
}

因为我们设置了串口句柄成员变量RxXferSize 为1,那么每当串口1 接收到一个字符后触
发接收完成中断,便会在中断服务函数中引导执行该回调函数。当串口接受到一个字符后,它会保存在缓存g_rx_buffer 中,由于我们设置了缓存大小为1,而且RxXferSize=1,所以每次接受一个字符,会直接保存到RxXferSize[0]中,我们直接通过读取RxXferSize[0]的值就是本次接收到的字符。这里我们设计了一个小小的接收协议:通过这个函数,配合一个数组g_usart_rx_buf,一个接收状态寄存器g_usart_rx_sta(此寄存器其实就是一个全局变量,由作者自行添加。由于它起到类似寄存器的功能,这里暂且称之为寄存器)实现对串口数据的接收管理。数组g_usart_rx_buf 的大小由USART_REC_LEN 定义,也就是一次接收的数据最大不能超过USART_REC_LEN 个字节。g_usart_rx_sta 是一个接收状态寄存器其各的定义如表17.3.3.1 所示:

正点原子STM32(基于HAL库)2_第123张图片

设计思路如下:

当接收到从电脑发过来的数据,把接收到的数据保存在数组g_usart_rx_buf 中,同时在接
收状态寄存器(g_usart_rx_sta)中计数接收到的有效数据个数,当收到回车(回车的表示由2 个字节组成:0X0D 和0X0A)的第一个字节0X0D 时,计数器将不再增加,等待0X0A 的到来,而如果0X0A 没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到0X0A,则标记g_usart_rx_sta 的第15 位,这样完成一次接收,并等待该位被其他程序清除,从而开始下一次的接收,而如果迟迟没有收到0X0D,那么在接收数据超过USART_REC_LEN 的时候,则会丢弃前面的数据,重新接收。

学到这里大家会发现,HAL 库定义的串口中断逻辑确实非常复杂,并且因为处理过程繁琐
所以效率不高。这里我们需要说明的是,在中断服务函数中,大家也可以不用调用
HAL_UART_IRQHandler 函数,而是直接编写自己的中断服务函数。串口实验我们之所以遵循HAL 库写法,是为了让大家对HAL 库有一个更清晰的理解。

  1. main.c 代码
    在main.c 里面编写如下代码:
int main(void)
{
        uint8_t len;
        uint16_t times = 0;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟为72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        led_init(); /* 初始化LED */
        while (1)
        {
                if (g_usart_rx_sta & 0x8000) /* 接收到了数据?  位15是否为1*/
                {
                        len = g_usart_rx_sta & 0x3fff; /* 得到此次接收到的数据长度*/
                        printf("\r\n您发送的消息为:\r\n");
                        /*发送接收到的数据*/
                        HAL_UART_Transmit(&uartx_handler,(uint8_t*)g_usart_rx_buf,len,1000);
                        /*等待发送结束 TC标志位为1*/
                        while(__HAL_UART_GET_FLAG(&uartx_handler,UART_FLAG_TC)!=SET);
                        printf("\r\n\r\n"); /* 插入换行*/
                        g_usart_rx_sta = 0;
                }
                else
                {		//没收到数据
                        times++;
                        if (times % 5000 == 0)
                        {
                                printf("\r\n正点原子STM32开发板串口实验\r\n");
                                printf("正点原子@ALIENTEK\r\n\r\n\r\n");
                        }
                        if (times % 200 == 0) printf("请输入数据,以回车键结束\r\n");
                        if (times % 30 == 0) LED0_TOGGLE(); /* 闪烁LED,提示系统正在运行. */
                        delay_ms(10);
                }
        }
}

我们主要看无限循环里面的逻辑:首先判断全局变量g_usart_rx_sta 的最高位是否为1,如
果为1 的话,那么代表前一次数据接收已经完成,接下来就是把我们自定义接收缓冲的数据发送到串口,在上位机显示。这里比较重点的两条语句是:第一条是调用HAL 串口发送函数
HAL_UART_Transmit 来发送一段字符到串口。第二条是我们发送一个字节之后,要检测这个数据是否已经被发送完成了。如果全局变量g_usart_rx_sta 的最高位为0,则执行一段时间往上位机发送提示字符,以及让LED0 每隔一段时间翻转,提示系统正在运行。

下载验证

在下载好程序后,可以看到板子上的LED0 开始闪烁,说明程序已经在跑了。串口调试助
手,我们用XCOM,该软件在光盘有提供,且无需安装,直接可以运行,但是需要你的电脑安装有.NET Framework 4.0(WIN 自带了)或以上版本的环境才可以,该软件的详细介绍请看:http://www.openedv.com/posts/list/22994.htm 这个帖子。

接着我们打开XCOM(正点原子的串口调试助手,位于光资料盘(A 盘)→6,软件资料→1,
软件→串口调试助手→XCOM),设置串口为开发板的USB 转串口(CH340 虚拟串口,得根据你自己的电脑选择,我的电脑是COM15,另外,请注意:波特率是115200)。因为我们在程序上面设置了必须输入回车,串口才认可接收到的数据,所以必须在发送数据后再发送一个回车符,这里XCOM 提供的发送方法是通过勾选发送新行实现,只要勾选了这个选项,每次发送数据后,XCOM 都会自动多发一个回车(0X0D+0X0A)。设置好了发送新行,我们再在发送区输入你想要发送的文字,然后单击发送,可以看到如图15.4.1 所示信息:

正点原子STM32(基于HAL库)2_第124张图片

可以看到,我们发送的消息被发送回来了。大家可以试试,如果不发送回车(取消发送新
行),在输入内容之后,直接按发送是什么结果,大家测试一下吧。

基本定时器实验

定时器概述

正点原子STM32(基于HAL库)2_第125张图片
在这里插入图片描述
正点原子STM32(基于HAL库)2_第126张图片

正点原子STM32(基于HAL库)2_第127张图片

正点原子STM32(基于HAL库)2_第128张图片

基本定时器简介

正点原子STM32(基于HAL库)2_第129张图片

STM32F103 有众多的定时器,其中包括2 个基本定时器(TIM6 和TIM7)、4 个通用定时器(TIM2~TIM5)、2 个高级控制定时器(TIM1 和TIM8),这些定时器彼此完全独立,不共享任何资源。本章我们学习如何使用STM32F103 的基本定时器中断。我们将使用TIM6 的定时器中断来控制LED1 的翻转,在主函数用LED0 的翻转来提示程序正在运行。

STM32F103 有两个基本定时器TIM6 和TIM7,它们的功能完全相同,资源是完全独立的,可以同时使用。其主要特性如下:16 位自动重载递增计数器,16 位可编程预分频器,预分频系数1~65536,用于对计数器时钟频率进行分频,还可以触发DAC 的同步电路,以及生成中断/DMA 请求。

基本定时器框图

下面先来学习基本定时器框图,通过学习基本定时器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。
正点原子STM32(基于HAL库)2_第130张图片

①时钟源
定时器的核心就是计算器,要实现计数功能,首先要给它一个时钟源。基本定时器时钟挂载在APB1 总线(内部时钟),所以它的时钟来自于APB1 总线,但是基本定时器时钟不是直接由APB1 总线直接提供,而是先经过一个倍频器。当APB1 的预分频器系数为1 时,这个倍频器系数为1,即定时器的时钟频率等于APB1 总线时钟频率;当APB1 的预分频器系数≥2 分频时,这个倍频器系数就为2 ,即定时器的时钟频率等于APB1 总线时钟频率的两倍。我们在sys_stm32_clock_init 时钟设置函数已经设置APB1 总线时钟频率为36M,APB1 总线的预分频器分频系数是2,所以挂载在APB1 总线的定时器时钟频率为72Mhz。
②控制器
控制器除了控制定时器复位、使能、计数等功能之外,还可以用于触发DAC 转换。
③时基单元(计数器)
时基单元包括:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR) 。基本定时器的这三个寄存器都是16 位有效数字,即可设置值范围是0~65535。
时基单元中的预分频器PSC,它有一个输入和一个输出。输入CK_PSC 来源于控制器部分,实际上就是来自于内部时钟(CK_INT),即2 倍的APB1 总线时钟频率(72MHz)。输出CK_CNT是分频后的时钟,它是计数器实际的计数时钟,通过设置预分频器寄存器(TIMx_PSC)的值可以得到不同频率CK_CNT,计算公式如下:
在这里插入图片描述
上式中,PSC[15:0]是写入预分频器寄存器(TIMx_PSC)的值。
另外:预分频器寄存器(TIMx_PSC)可以在运行过程中修改它的数值,新的预分频数值将在下一个更新事件时起作用。因为更新事件发生时,会把TIMx_PSC 寄存器值更新到其影子寄存器中,这才会起作用。

什么是影子寄存器?从框图上看,可以看到图20.1.1.1中的预分频器PSC后面有一个影子,
自动重载寄存器也有个影子,这就表示这些寄存器有影子寄存器。影子寄存器是一个实际起作用的寄存器,不可直接访问。举个例子:我们可以把预分频系数写入预分频器寄存器(TIMx_PSC),但是预分频器寄存器只是起到缓存数据的作用,只有等到更新事件发生时,预分频器寄存器的值才会被自动写入其影子寄存器中,这时才真正起作用。
自动重载寄存器及其影子寄存器的作用和上述同理。不同点在于自动重载寄存器是否具有
缓冲作用还受到ARPE 位的控制,当该位置0 时,ARR 寄存器不进行缓冲,我们写入新的ARR值时,该值会马上被写入ARR 影子寄存器中,从而直接生效;当该位置1 时,ARR 寄存器进行缓冲,我们写入新的ARR 值时,该值不会马上被写入ARR 影子寄存器中,而是要等到更新事件发生才会被写入ARR 影子寄存器,这时才生效。预分频器寄存器则没有这样相关的控制位,这就是它们不同点。
值得注意的是,更新事件的产生有两种情况,一是由软件产生,将TIMx_EGR 寄存器的位
UG 置1,产生更新事件后,硬件会自动将UG 位清零。二是由硬件产生,满足以下条件即可:

计数器的值等于自动重装载寄存器影子寄存器的值。下面来讨论一下硬件更新事件。
基本定时器的计数器(CNT)是一个递增的计数器,当寄存器(TIMx_CR1)的CEN 位置
1,即使能定时器,每来一个CK_CNT 脉冲,TIMx_CNT 的值就会递增加1。当TIMx_CNT 值与TIMx_ARR 的设定值相等时,TIMx_CNT 的值就会被自动清零并且会生成更新事件(如果开启相应的功能,就会产生DMA 请求、产生中断信号或者触发DAC 同步电路),然后下一个CK_CNT 脉冲到来,TIMx_CNT 的值就会递增加1,如此循环。在此过程中,TIMx_CNT 等于TIMx_ARR 时,我们称之为定时器溢出,因为是递增计数,故而又称为定时器上溢。定时器溢出就伴随着更新事件的发生。

由上述可知,我们只要设置预分频寄存器和自动重载寄存器的值就可以控制定时器更新事
件发生的时间。自动重载寄存器(TIMx_ARR)是用于存放一个与计数器作比较的值,当计数器的值等于自动重载寄存器的值时就会生成更新事件,硬件自动置位相关更新事件的标志位,如:更新中断标志位。

下面举个例子来学习如何设置预分频寄存器和自动重载寄存器的值来得到我们想要的定时
器上溢事件发生的时间周期。比如我们需要一个500ms 周期的定时器更新中断,一般思路是先设置预分频寄存器,然后才是自动重载寄存器。考虑到我们设置的CK_INT 为72MHz,我们把预分频系数设置为7200,即写入预分频寄存器的值为7199,那么fCK_CNT=72MHz/7200=10KHz。
这样就得到计数器的计数频率为10KHz,即计数器1 秒钟可以计10000 个数。我们需要500ms的中断周期,所以我们让计数器计数5000 个数就能满足要求,即需要设置自动重载寄存器的值为4999,另外还要把定时器更新中断使能位UIE 置1,CEN 位也要置1。

定时器计数模式及溢出条件

正点原子STM32(基于HAL库)2_第131张图片
正点原子STM32(基于HAL库)2_第132张图片
正点原子STM32(基于HAL库)2_第133张图片
正点原子STM32(基于HAL库)2_第134张图片

TIM6/TIM7 寄存器

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

⚫ 控制寄存器1(TIMx_CR1)
TIM6/TIM7 的控制寄存器1 描述如图20.1.2.1 所示。
正点原子STM32(基于HAL库)2_第135张图片
该寄存器,我们需要注意的是:位0(CEN)用于使能或者禁止计数器,该位置1 计数器开始工作,置0 则停止。
位7(APRE)用于控制自动重载寄存器ARR 是否具有缓冲作用,如果ARPE 位置1,ARR 起缓冲作用,即只有在更新事件发生时才会把ARR 的值写入其影子寄存器里;如果ARPE 位置0,那么修改自动重载寄存器的值时,该值会马上被写入其影子寄存器中,从而立即生效(视频教程讲了一个很好例子,忘记了再翻看一下)。

⚫ DMA/中断使能寄存器(TIMx_DIER)
正点原子STM32(基于HAL库)2_第136张图片
该寄存器位0(UIE)用于使能或者禁止更新中断,因为本实验我们用到中断,所以该位需要置1。位8(UDE)用于使能或者禁止更新DMA 请求,我们暂且用不到,置0 即可。

⚫ 状态寄存器(TIMx_SR)
TIM6/TIM7 的状态寄存器描述如图20.1.2.3 所示:

正点原子STM32(基于HAL库)2_第137张图片
该寄存器位0(UIF)是中断更新的标志位,当发生中断时由硬件置1,然后就会执行中断服务函数,需要软件去清零,所以我们必须在中断服务函数里把该位清零。如果中断到来后,不把该位清零,那么系统就会一直进入中断服务函数,这显然不是我们想要的。

⚫ 计数器寄存器(TIMx_CNT)
TIM6/TIM7 的计数器寄存器描述如图20.1.2.4 所示:
在这里插入图片描述
该寄存器位[15:0]就是计数器的实时的计数值,可读可写,范围0~65535。

⚫ 预分频寄存器(TIMx_PSC)
TIM6/TIM7 的预分频寄存器描述如图20.1.2.5 所示:
正点原子STM32(基于HAL库)2_第138张图片
该寄存器是TIM6/TIM7 的预分频寄存器,比如我们要7200 分频,就往该寄存器写入7199。
注意这是16 位的寄存器,写入的数值范围是0~ 65535,分频系数范围(+1):1~65536。

⚫ 自动重载寄存器(TIMx_ARR)
TIM6/TIM7 的自动重载寄存器描述如图20.1.2.6 所示:
正点原子STM32(基于HAL库)2_第139张图片

该寄存器可以由APRE 位设置是否进行缓冲。计数器的值会和ARR 寄存器影子寄存器进行比较,当两者相等,定时器就会溢出,从而发生更新事件,如果打开更新中断,还会发生更新中断。

定时器溢出时间计算方法

正点原子STM32(基于HAL库)2_第140张图片

基本定时器中断应用

本实验,我们主要配置定时器产生周期性溢出,从而在定时器更新中断中做周期性的操作,如周期性翻转LED 灯。假设计数器计数模式为递增计数模式,那么实现周期性更新中断原理示意图如下所示:
正点原子STM32(基于HAL库)2_第141张图片
如图20.1.3.1 所示,CNT 计数器从0 开始计数,当CNT 的值和ARR 相等时(t1),产生一个更新中断,然后CNT 复位(清零),然后继续递增计数,依次循环。图中的t1、t2、t3 就是定时器更新中断产生的时刻。
通过修改ARR 的值,可以改变定时时间。另外,通过修改PSC 的值,使用不同的计数频率(改变图中CNT 的斜率),也可以改变定时时间。

硬件设计

  1. 例程功能
    LED0 用来指示程序运行,每200ms 翻转一次。我们在更新中断中,将LED1 的状态取反。
    LED1 用于指示定时器发生更新事件的频率,500ms 取反一次。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    LED1 –PE5
    2)定时器6
  3. 原理图
    定时器属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们通过LED1 来指示STM32F103 的定时器进入中断的频率。

程序设计

定时器的HAL 库驱动

正点原子STM32(基于HAL库)2_第142张图片

定时器在HAL 库中的驱动代码在STM32F1xx_hal_tim.c 和STM32F1xx_hal_tim_ex.c 文件
(以及它们的头文件)中。

  1. HAL_TIM_Base_Init 函数
    定时器的初始化函数,其声明如下:
    HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);
    ⚫ 函数描述:
    用于初始化定时器。
    ⚫ 函数形参:
    形参1 是TIM_HandleTypeDef 结构体类型指针变量(亦称定时器句柄),结构体定义如下:
typedef struct
{
	TIM_TypeDef *Instance; /* 外设寄存器基地址*/
	TIM_Base_InitTypeDef Init; /* 定时器初始化结构体*/
	HAL_TIM_ActiveChannel Channel; /* 定时器通道*/
	DMA_HandleTypeDef *hdma[7]; /* DMA管理结构体*/
	HAL_LockTypeDef Lock; /* 锁定资源*/
	__IO HAL_TIM_StateTypeDef State; /* 定时器状态*/
}TIM_HandleTypeDef;

1)Instance:指向定时器寄存器基地址。
2)Init:定时器初始化结构体,用于配置定时器的相关参数。
3)Channel:定时器的通道选择,基本定时器没有该功能。
4)hdma[7]:用于配置定时器的DMA 请求。
5)Lock:ADC 锁资源。
6)State:定时器工作状态。
我们主要看TIM_Base_InitTypeDef 这个结构体类型定义:

typedef struct
{
	uint32_t Prescaler; 		/* 预分频系数 操作PSC*/
	uint32_t CounterMode; 		/* 计数模式*/
	uint32_t Period; 			/* 自动重载值ARR */
	uint32_t ClockDivision; 	/* 时钟分频因子*/
	uint32_t RepetitionCounter; /* 重复计数器*/
	uint32_t AutoReloadPreload; /* 自动重载预装载使能*/
} TIM_Base_InitTypeDef;

1)Prescaler:预分频系数,即写入预分频寄存器的值,范围0 到65535。
2)CounterMode:计数器计数模式,这里基本定时器只能向上计数。
3)Period:自动重载值,即写入自动重载寄存器的值,范围0 到65535。
4)ClockDivision:时钟分频因子,也就是定时器时钟频率CK_INT 与数字滤波器所使用的采样时钟之间的分频比,基本定时器没有此功能。
5)RepetitionCounter:设置重复计数器寄存器的值,用在高级定时器中。
6)AutoReloadPreload:自动重载预装载使能,即控制寄存器1 (TIMx_CR1)的ARPE 位,是否具有缓冲。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。

  1. HAL_TIM_Base_Start_IT 函数
    HAL_TIM_Base_Start_IT 函数是更新定时器中断和使能定时器的函数。其声明如下:
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);

⚫ 函数描述:
该函数调用了__HAL_TIM_ENABLE_IT 和__HAL_TIM_ENABLE 两个函数宏定义,分别是更新定时器中断和使能定时器的宏定义。
⚫ 函数形参:
形参1 是TIM_HandleTypeDef 结构体类型指针变量,即定时器句柄。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
⚫ 注意事项:
下面分别列出单独使能/关闭定时器中断和使能/关闭定时器方法:

__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指定的定时器*/

课堂源码

正点原子STM32(基于HAL库)2_第143张图片
btim.h

#ifndef __BTIM_H
#define __BTIM_H

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


void btim_timx_int_init(uint16_t arr, uint16_t psc);

#endif

btim.c

#include "./BSP/LED/led.h"
#include "./BSP/TIMER/btim.h"


TIM_HandleTypeDef g_timx_handle;

/* 定时器中断初始化函数 */
void btim_timx_int_init(uint16_t arr, uint16_t psc)
{
    g_timx_handle.Instance = TIM6;//外设寄存器基地址
    g_timx_handle.Init.Prescaler = psc;
    g_timx_handle.Init.Period = arr;//           结构体成员自动重载预装载使能不用定义,因为定时时间是定值500ms
    HAL_TIM_Base_Init(&g_timx_handle);//HAL开头的都是HAL库里的函数

    HAL_TIM_Base_Start_IT(&g_timx_handle);
}

/* 定时器基础MSP初始化函数 */
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6)
    {
        __HAL_RCC_TIM6_CLK_ENABLE();
        HAL_NVIC_SetPriority(TIM6_IRQn, 1, 3);
        HAL_NVIC_EnableIRQ(TIM6_IRQn);
    }
}

/* 定时器6中断服务函数 */
void TIM6_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&g_timx_handle);//HAL库函数
}

/* 定时器溢出中断中断回调函数 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6)
    {
        LED0_TOGGLE();
    }
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/TIMER/btim.h"

int main(void)
{
    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟,72M */
    delay_init(72);                             /* 初始化延时函数 */
    led_init();                                 /* 初始化LED */
    
    btim_timx_int_init(5000 - 1, 7200 - 1);     /* 前面公式 */
    
    while(1)
    {
        delay_ms(500);
    }
}

定时器中断配置步骤

正点原子STM32(基于HAL库)2_第144张图片

1)开启定时器时钟
HAL 中定时器使能是通过宏定义标识符来实现对相关寄存器操作的,方法如下:

__HAL_RCC_TIMx_CLK_ENABLE(); /* x=1~8 */

2)初始化定时器参数,设置自动重装值,分频系数,计数方式等
定时器的初始化参数是通过定时器初始化函数HAL_TIM_Base_Init 实现的。
注意:该函数会调用:HAL_TIM_Base_MspInit 函数,我们可以通过后者存放定时器时钟
和中断等初始化的代码。
3)使能定时器更新中断,开启定时器计数,配置定时器中断优先级通过HAL_TIM_Base_Start_IT 函数使能定时器更新中断和开启定时器计数。
通过HAL_NVIC_EnableIRQ 函数使能定时器中断,通过HAL_NVIC_SetPriority 函数设置
中断优先级。
4)编写中断服务函数
定时器中断服务函数为:TIMx_IRQHandler 等,当发生中断的时候,程序就会执行中断服务函数。HAL 库提供了一个定时器中断公共处理函数HAL_TIM_IRQHandler,该函数又会调用
HAL_TIM_PeriodElapsedCallback 等一些回调函数,需要用户根据中断类型选择重定义对应的中断回调函数来处理中断程序。

程序流程图

正点原子STM32(基于HAL库)2_第145张图片
程序开始先进行一系列初始化,然后在main 中让LED0 每过200ms 翻转一次,用于指示系统代码正在运行。LED1 的翻转将在定时器更新中断里进行,请看程序解析。

程序解析

  1. 基本定时器中断驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。基本定时器驱动
    代码包括两个文件:btim.c 和btim.h。
    首先看btim.h 头文件的几个宏定义:
/* 基本定时器定义*/
/* TIMX 中断定义
* 默认是针对TIM6/TIM7.
* 注意: 通过修改这4个宏定义,可以支持TIM1~TIM8任意一个定时器.
*/
#define BTIM_TIMX_INT TIM6
#define BTIM_TIMX_INT_IRQn TIM6_DAC_IRQn
#define BTIM_TIMX_INT_IRQHandler TIM6_DAC_IRQHandler
/* TIM6 时钟使能*/
#define BTIM_TIMX_INT_CLK_ENABLE() do{ __HAL_RCC_TIM6_CLK_ENABLE(); }while(0)

通过修改这4 个宏定义,可以支持TIM1~TIM8 任意一个定时器。
下面我们解析btim.c 的程序,先看定时器的初始化函数,其定义如下:

/**
* @brief 基本定时器TIMX定时中断初始化函数
* @note
* 基本定时器的时钟来自APB1,当PPRE1 ≥ 2 分频的时候
* 基本定时器的时钟为APB1时钟的2倍, 而APB1为36M, 所以定时器时钟= 72Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值。
* @param psc: 时钟预分频数
* @retval 无
*/
void btim_timx_int_init(uint16_t arr, uint16_t psc)
{
	g_timx_handle.Instance = BTIM_TIMX_INT; /* 定时器x */
	g_timx_handle.Init.Prescaler = psc; /* 预分频*/
	g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数器*/
	g_timx_handle.Init.Period = arr; /* 自动装载值*/
	HAL_TIM_Base_Init(&g_timx_handle);
	HAL_TIM_Base_Start_IT(&g_timx_handle); /* 使能定时器x和定时器x更新中断*/
}

btim_timx_int_init 函数用来初始化定时器,我们可以通过修改宏定义BTIM_TIMX_INT 来初始化TIM1~TIM8 中的任意一个,本章我们是初始化基本定时器6。该函数的2 个形参:arr设置自动重载寄存器(TIMx_ARR),psc 设置预分频器寄存器(TIMx_PSC)。HAL_TIM_Base_Init函数初始化定时器后,再调用HAL_TIM_Base_Start_IT 函数使能定时器和更新定时器中断。
因为我们在sys_stm32_clock_init 函数里面已经初始化APB1 的时钟为HCLK 的2 分频,所以APB1 的时钟为36M,而从STM32F1 的内部时钟树图得知:当APB1 的时钟分频数为1 的时候,TIM2~ 7 的时钟为APB1 的时钟,而如果APB1 的时钟分频数不为1,那么TIM2~7 的时钟频率将为APB1 时钟的两倍。因此,TIM6 的时钟为72M,再根据我们设计的arr 和psc 的值,就可以计算中断时间了。计算公式如下:

Tout= ((arr+1)*(psc+1))/Tclk

其中:
Tout:定时器溢出时间(单位为s)。
Tclk:定时器的时钟源频率(单位为MHz)。
arr:自动重装寄存器(TIMx_ARR)的值。
psc:预分频器寄存器(TIMx_PSC)的值
定时器底层驱动初始化函数如下:

/**
* @brief 定时器底层驱动,开启时钟,设置中断优先级
此函数会被HAL_TIM_Base_Init()函数调用
* @param 无
* @retval 无
*/
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
	if (htim->Instance == BTIM_TIMX_INT)
	{
		BTIM_TIMX_INT_CLK_ENABLE(); /* 使能TIMx时钟*/
		/* 设置中断优先级,抢占优先级1,子优先级3 */
		HAL_NVIC_SetPriority(BTIM_TIMX_INT_IRQn, 1, 3);
		HAL_NVIC_EnableIRQ(BTIM_TIMX_INT_IRQn); /* 开启ITMx中断*/
	}
}

HAL_TIM_Base_MspInit 函数用于存放GPIO、NVIC 和时钟相关的代码,这里首先判断定时器的寄存器基地址,满足条件后,首先设置使能定时器的时钟,然后设置定时器中断的抢占优先级为1,响应优先级为3,最后开启定时器中断。这里没有用到IO 引脚,所以不用初始化
GPIO。
接着是定时器中断服务函数的定义,这里用的是宏名,其定义如下:

/**
* @brief 定时器中断服务函数
* @param 无
* @retval 无
*/
void BTIM_TIMX_INT_IRQHandler(void)
{
	HAL_TIM_IRQHandler(&timx_handle);
}

这个函数实际上调用HAL 库的定时器中断公共处理函数HAL_TIM_IRQHandler。HAL 库的中断公共处理函数,会根据中断标志位调用各个中断回调函数,中断要处理的逻辑代码就写到这个回调函数中。比如这里我们使用到的是更新中断,定义的更新中断回调函数如下:

/**
* @brief 定时器更新中断回调函数
* @param htim:定时器句柄指针
* @retval 无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if (htim->Instance == BTIM_TIMX_INT)
	{
		LED1_TOGGLE();
	}
}

更新中断回调函数是所有定时器公用的,所以我们就需要在更新中断回调函数中对定时器寄存器基地址进行判断,只有符合对应定时器发生的更新中断,才能进行相应的处理,从而避免多个定时器同时使用到更新中断,导致更新中断代码的逻辑混乱。这里我们使用定时器6 的更新中断,所以进入更新中断回调函数后,先判断是不是定时器6 的寄存器基地址,当然这里使用宏的形式,BTIM_TIMX_INT 的原型就是TIM6,执行的逻辑代码是翻转LED1。

  1. main.c 代码
    在main.c 里面编写如下代码:
int main(void)
{
	HAL_Init(); /* 初始化HAL库*/
	sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72); /* 延时初始化*/
	usart_init(115200); /* 串口初始化为115200 */
	led_init(); /* 初始化LED */
	btim_timx_int_init(5000 - 1, 7200 - 1); /* 10Khz的计数频率,计数5K次为500ms */
	while (1)
	{
		LED0_TOGGLE();
		delay_ms(200);
	}
}

在main 函数里,先初始化系统和用户的外设代码,然后在wilhe(1)里每200ms 翻转一次LED0。由前面的内容知道,定时器6 的时钟频率为72MHZ,而调用btim_timx_int_init 初始化函数之后,就相当于写入预分频寄存器的值为7199,写入自动重载寄存器的值为4999。由公式得:

Tout = ((4999+1)*(7199+1))/72000000 = 0.5s = 500ms

下载验证

下载代码后,可以看到LED0 不停闪烁(每400ms 一个周期),而LED1 也是不停的闪烁,但是闪烁时间较LED0 慢(每1s 一个周期)。

通用定时器实验

正点原子STM32(基于HAL库)2_第146张图片

本章我们主要来学习通用定时器,通过上一章的学习,我们知道STM32F103 有4 个通用定时器(TIM2~TIM5)。这些定时器彼此完全独立,不共享任何资源。本章我们将通过四个实验来学习通用定时器的各个功能,分别是通用定时器中断实验、通用定时器PWM输出实验、通用定时器输入捕获实验和通用定时器脉冲计数实验。

本章分为如下几个小节:
21.1 通用定时器简介
21.2 通用定时器中断实验
21.3 通用定时器PWM 输出实验
21.4 通用定时器输入捕获实验
21.5 通用定时器脉冲计数实验

通用定时器简介

STM32F103 的通用定时器有4 个,为了更好的区别各个定时器的特性,我们列了一个表格,如下所示:
正点原子STM32(基于HAL库)2_第147张图片
注:该表参考数据手册《STM32F103ZET6.pdf》的2.3.17 小节(第19 页)。
由上表知道:该STM32 芯片的计数器都是16 位的。通用定时器和高级定时器其实也就是在基本定时器的基础上,添加了一些其他功能,如:输入捕获、输出比较、输出PWM 和单脉冲模式等。而通用定时器数量较多,其特性也有一些的差异,但是基本原理都一样。
通用定时器框图
下面先来学习通用定时器框图,通过学习通用定时器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。

正点原子STM32(基于HAL库)2_第148张图片
如上图,通用定时器的框图比基本定时器的框图复杂许多,为了方便介绍,我们将其分成六个部分讲解:

①时钟源
通用定时器时钟可以选择下面四类时钟源之一:

  • 1)内部时钟(CK_INT)
  • 2)外部时钟模式1:外部输入引脚(TIMx),x=1,2(即只能来自于通道1 或者通道2,在上图第④部分最左侧,一般此引脚由GPIO复用)
  • 3)外部时钟模式2:外部触发输入(ETR)
  • 4)内部触发输入(ITRx):使用一个定时器作为另一定时器的预分频器通用定时器时钟源的设置方法如下表所示:
    正点原子STM32(基于HAL库)2_第149张图片
    正点原子STM32(基于HAL库)2_第150张图片
    正点原子STM32(基于HAL库)2_第151张图片

下面详细介绍这四种时钟源

1)内部时钟(CK_INT)
STM32F1 系列的定时器TIM2/TIM3/TIM4/TIM5/ TIM6/TIM7 都是挂载在APB1 总线上,这些定时器的内部时钟(CK_INT)实际上来自于APB1 总线提供的时钟。但是这些定时器时钟不是由APB1 总线直接提供,而是要先经过一个倍频器。在HAL 库版本例程源码的sys.c 文件中,系统时钟初始化函数sys_stm32_clock_init 已经设置APB1 总线时钟频率为36MHz,APB1 预分频器的预分频系数为2,所以这些定时器时钟源频率为72MHz。因为当APB1 预分频器的预分频系数≥2 分频时,挂载在APB1 总线上的定时器时钟频率是该总线时钟频率的两倍。这个和基本定时器一样,可回顾基本定时器这部分内容。

另外,高级定时器TIM1 和TIM8 是挂载在APB2 总线上的,也随便给大家说一下它们的情况。由图11.1.1 STM32F1 时钟系统图可以知道,如果APB2 预分频系数为1,挂载在该总线的定时器时钟频率不变,否则频率是该总线时钟频率的2 倍。我们在系统时钟初始化函数
sys_stm32_clock_init 已经设置APB2 总线时钟频率为72MHz,APB2 预分频器的预分频系数为1,所以TIM1 和TIM8 时钟源频率为72MHz。

2)外部时钟模式1(TI1、TI2)外部时钟模式1 这类时钟源,顾名思义时钟信号来自芯片外部。时钟源进入定时器的流程如下:外部时钟源信号→IO→TIMx_CH1(或者TIMx_CH2),这里需要注意的是:外部时钟模式1 下,时钟源信号只能从CH1 或者CH2 输入到定时器,CH3 和CH4 都是不可以的。从IO到TIMx_CH1(或者TIMx_CH2),就需要我们配置IO 的复用功能,才能使IO 和定时器通道相连通。
时钟源信号来到定时器CH1 或CH2 后,需要经过什么“关卡”才能到达计数器作为计数的时钟频率的,下面通过外部时钟模式1 框图给大家解答。

正点原子STM32(基于HAL库)2_第152张图片
图21.1.2 中是以CH2(通道2)为例的,时钟源信号到达CH2 后,那么这里我们把这个时钟源信号用TI2 表示,因为它只是个信号,来到定时器内部,那我们就按定时器内部的信号来命名,所谓入乡随俗。
TI2 首先经过一个滤波器,由ICF[3:0]位来设置滤波方式,也可以设置不使用滤波器。
接着经过边沿检测器,由CC2P 位来设置检测的边沿,可以上升沿或者下降沿检测。
然后经过触发输入选择器,由TS[4:0]位来选择TRGI(触发输入信号)的来源。可以看到图21.1.2 中框出了TI1F_ED、TI1FP1 和TI2FP2 三个触发输入信号(TRGI)。TI1F_ED 表示来自于CH1,并且没有经过边沿检测器过滤的信号,所以它是CH1 的双边沿信号,即上升沿或者下降沿都是有效的。TI1FP1 表示来自CH1 并经过边沿检测器后的信号,可以是上升沿或者下降沿。TI2FP2 表示来自CH2 并经过边沿检测器后的信号,可以是上升沿或者下降沿。这里以CH2 为例,那只能选择TI2FP2。如果是CH1 为例,那就可以选择TI1F_ED 或者TI1FP1。
最后经过从模式选择器,由ECE 位和SMS[2:0]位来选择定时器的时钟源。这里我们介绍的是外部时钟模式1,所以ECE 位置0,SMS[2:0] = 111 即可。CK_PSC 需要经过定时器的预分频器分频后,最终就能到达计数器进行计数了。

3)外部时钟模式2(ETR)外部时钟模式2,顾名思义时钟信号来自芯片外部。时钟源进入定时器的流程如下:外部时钟源信号→IO→TIMx_ETR。从IO 到TIMx_ETR,就需要我们配置IO 的复用功能,才能使IO 和定时器相连通。
时钟源信号来到定时器TIMx_ETR 后,需要经过什么“关卡”才能到达计数器作为计数的时钟频率的,下面通过外部时钟模式2 框图给大家解答。
正点原子STM32(基于HAL库)2_第153张图片
图21.1.3 中,可以看到在外部时钟模式2 下,定时器时钟信号首先从ETR 引脚进来。
接着经过外部触发极性选择器,由ETP 位来设置上升沿有效还是下降沿有效,选择下降沿有效的话,信号会经过反相器。
然后经过外部触发预分频器,由ETPS[1:0]位来设置预分频系数,系数范围:1、2、4、8。
紧接着经过滤波器器,由ETF[3:0]位来设置滤波方式,也可以设置不使用滤波器。fDTS 由TIMx_CR1 寄存器的CKD 位设置。
最后经过从模式选择器,由ECE 位和SMS[2:0]位来选择定时器的时钟源。这里我们介绍的是外部时钟模式2,直接把ECE 位置1 即可。CK_PSC 需要经过定时器的预分频器分频后,最终就能到达计数器进行计数了。

4)内部触发输入(ITRx)
内部触发输入是使用一个定时器作为另一个定时器的预分频器,即实现定时器的级联。下面以TIM1 作为TIM2 的预分频器为例,给大家介绍。

正点原子STM32(基于HAL库)2_第154张图片
上图中,TIM1 作为TIM2 的预分频器,需要完成的配置步骤如下:
1,TIM1_CR2 寄存器的MMS[2:0]位设置为010,即TIM1 的主模式选择为更新(选择更新事件作为触发输出(TRGO))。
2,TIM2_SMCR 寄存器的TS[2:0]位设置为000,即使用ITR1 作为内部触发。TS[2:0]位用于配置触发选择,除了ITR1,还有其他的选择,详细描述如下图所示:

正点原子STM32(基于HAL库)2_第155张图片

上图中的触发选择中,我们在讲解外部时钟模式1 的时候说过TI1F_ED、TI1FP1 和TI2F P2,以及外部时钟模式2 讲的ETRF,它们都是属于外部的,其余的都是内部触发了。那么这内部触发都代表什么意思呢?大家打开《STM32F10xxx 参考手册_V10(中文版).pdf》的285 页,就可以找下面这个表。

正点原子STM32(基于HAL库)2_第156张图片

在步骤2 中,TS[2:0]位设置为000,使用ITR1 作为内部触发,这个ITR1 什么意思?由表21.1.3 可以知道,当从模式定时器为TIM2 时,ITR1 表示主模式定时器就是TIM1。这里只是TIM2~5 的内部触发连接情况,其他定时器请查看参考手册的相应章节。
3,TIM2_SMCR 寄存器的SMS[2:0]位设置为111,即从模式控制器选择外部时钟模式1。
4,TIM1 和TIM2 的CEN 位都要置1,即启动计数器。
定时器的时钟源这部分内容是非常重要的,因为这计数器工作的基础。虽然定时器有四类时钟源之多,但是我们最常用的还是内部时钟。

②控制器
控制器包括:从模式控制器、编码器接口和触发控制器(TRGO)。从模式控制器可以控制计数器复位、启动、递增/递减、计数。编码器接口针对编码器计数。触发控制器用来提供触发信号给别的外设,比如为其它定时器提供时钟或者为DAC/ADC 的触发转换提供信号。

③时基单元(计数器)
时基单元包括:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR)。这部分内容和基本定时器基本一样的,大家可以参考基本定时器的介绍。
不同点是:通用定时器的计数模式有三种:递增计数模式、递减计数模式和中心对齐模式;
TIM2 和TIM5 的计数器是32 位的。递增计数模式在讲解基本定时器的时候已经讲过了,那么对应到递减计数模式就很好理解了。就是来了一个计数脉冲,计数器就减1,直到计数器寄存器的值减到0,减到0 时定时器溢出,由于是递减计数,故而称为定时器下溢,定时器溢出就会伴随着更新事件的发生。然后计数器又从自动重载寄存器影子寄存器的值开始继续递减计数,如此循环。最后是中心对齐模式,字面上不太好理解。该模式下,计数器先从0 开始递增计数,直到计数器的值等于自动重载寄存器影子寄存器的值减1 时,定时器上溢,同时生成更新事件,然后从自动重载寄存器影子寄存器的值开始递减计算,直到计数值等于1 时,定时器下溢,同时生成更新事件,然后又从0 开始递增计数,依此循环。每次定时器上溢或下溢都会生成更新事件。计数器的计数模式的设置请参考TIMx_CR1 寄存器的位CMS 和位DIR。
下面通过一张图给大家展示定时器工作在不同计数模式下,更新事件发生的情况。

正点原子STM32(基于HAL库)2_第157张图片

上图中,纵轴表示计数器的计数值,横轴表示时间,ARR 表示自动重载寄存器的值,小红点就是更新事件发生的时间点。举个例子,递增计数模式下,当计数值等于ARR 时,计数器的值被复位为0,定时器溢出,并伴随着更新事件的发生,后面继续递增计数。递减计数模式和中心对齐模式请参考前面的描述。
上表的描述属于硬件更新事件发生条件,我们还可以通过UG 位产生软件更新事件。
关于影子寄存器和定时器溢出时间计算公式等内容可以参考基本定时器的相关内容。

④输入捕获(达到测量的效果)

图21.1.1.1 中的第④部分是输入捕获,一般应用是要和第⑤部分一起完成测量功能。
TIMx_CH1~ TIMx_CH4 表示定时器的4 个通道,这4 个通道都是可以独立工作的。IO 端口通过复用功能与这些通道相连。配置好IO 端口的复用功能后,将需要测量的信号输入到相应的IO 端口,输入捕获部分可以对输入的信号的上升沿,下降沿或者双边沿进行捕获,常见的测量有:测量输入信号的脉冲宽度、测量PWM 输入信号的频率和占空比等。后续有相应的实验。

下面简单说一下测量高电平脉冲宽度的工作原理,方便大家的理解:一般先要设置输入捕获的边沿检测极性,如:我们设置上升沿检测,那么当检测到上升沿时,定时器会把计数器CNT的值锁存到相应的捕获/比较寄存器TIMx_CCRy 里,y=1~4。然后我们再设置边沿检测为下降沿检测,当检测到下降沿时,定时器会把计数器CNT 的值再次锁存到相应的捕获/比较寄存器TIMx_CCRy 里。最后,我们将前后两次锁存的CNT 的值相减,就可以算出高电平脉冲期间内计数器的计数个数,再根据定时器的计数频率就可以计算出这个高电平脉冲的时间。如果要测量的高电平脉宽时间长度超过定时器的溢出时间周期,就会发生溢出,这时候我们还需要做定时器溢出的额外处理。低电平脉冲捕获同理。

上面的描述是第④部分输入捕获整体上的一个应用情况,下面我们来看第④部分的细节。

当需要测量的信号进入通道后,需要经过哪些“关卡”?我们用图21.1.7 给大家讲解。
正点原子STM32(基于HAL库)2_第158张图片
图21.1.7 是图21.1.1 第④部分通道1 的“放大版”,这里是以通道1 输入捕获为例进行介绍,其他通道同理。
待测量信号到达TIMx_CH1 后,那么这里我们把这个待测量信号用TI1 表示,原因在讲解外部时钟模式1 的时候说过,所谓“入乡随俗”。
TI1 首先经过一个滤波器,由ICF[3:0]位来设置滤波方式,也可以设置不使用滤波器。fDTS由TIMx_CR1 寄存器的CKD 位设置。
接着经过边沿检测器,由CC1P 位来设置检测的边沿,可以上升沿或者下降沿检测。CC1NP是配置互补通道的边沿检测的,在高级定时器才有,通用定时器没有。
然后经过输入捕获映射选择器,由CC1S[1:0]位来选择把IC1 映射到TI1、TI2 还是TRC。
这里我们的待测量信号从通道1 进来,所以选择IC1 映射到TI1 上即可。
紧接着经过输入捕获1 预分频器,由ICPS[1:0]位来设置预分频系数,范围:1、2、4、8。
最后需要把CC1E 位置1,使能输入捕获,IC1PS 就是分频后的捕获信号。这个信号将会到达图21.1.1 的第⑤部分。

下面我们接着看图21.1.1 的第⑤部分的“放大版”,如下图所示:
正点原子STM32(基于HAL库)2_第159张图片

图21.1.8 中,灰色阴影部分是输出比较功能部分,讲到第⑥部分输出比较的时候再介绍。
左边没有阴影部分就是输入捕获功能部分了。
首先看到捕获/比较预装载寄存器,我们以通道1 为例,那么它就是CCR1 寄存器,通道2、通道3、通道4 就分别对应CCR2、CCR3、CCR4。在图21.1.1 中就可以看到CCR1~4 是有影子寄存器的,所以这里就可以看到图21.1.8 中有捕获/比较影子寄存器,该寄存器不可直接访问。
图21.1.8 左下角的CC1G 位可以产生软件捕获事件,那么硬件捕获事件如何产生的?这里我们还是以通道1 输入为例,CC1S[1:0] = 01,即IC1 映射到TI1 上;CC1E 位置1,使能输入捕获;比如不滤波、不分频,ICF[3:0] = 00,ICPS[1:0] = 00;比如检测上升沿,CC1P 位置0;
接着就是等待测量信号的上升沿到来。当上升沿到来时,IC1PS 信号就会触发输入捕获事件发生,计数器当前的值就会被锁存到捕获/比较影子寄存器里。当CCR1 寄存器没有被进行读操作的时候,捕获/比较影子寄存器里的值就会锁存到CCR1 寄存器中,那么程序员就可以读取CCR1 寄存器,得到计数器的计数值。检测下降沿同理。

⑤输入捕获和输出比较公用部分
该部分需要结合第④部分或者第⑥部分共同完成相应功能。

⑥输出比较
图21.1.1.1 中的第⑥部分是输出比较,一般应用是要和第⑤部分一起完成定时器输出功能。
TIMx_CH1~ TIMx_CH4 表示定时器的4 个通道,这4 个通道都是可以独立工作的。IO 端口通过复用功能与这些通道相连。
下面我们按照输出信号产生过程顺序给大家介绍定时器如何实现输出功能的?首先看到第⑤部分的“放大版”图,如下图所示:
正点原子STM32(基于HAL库)2_第160张图片
图21.1.9 中,灰色阴影部分是输入捕获功能部分,前面已经讲过。这里我们看到右边没有阴影部分就是输出比较功能部分了。下面以通道1 输出比较功能为例给大家介绍定时器如何实现输出功能的。
首先程序员写CCR1 寄存器,即写入比较值。这个比较值需要转移到对应的捕获/比较影子寄存器后才会真正生效。什么条件下才能转移?图21.1.9 中可以看到compare_transfer 旁边的与门,需要满足三个条件:CCR1 不在写入操作期间、CC1S[1:0] = 00 配置为输出、OC1PE 位置0(或者OC1PE 位置1,并且需要发生更新事件,这个更新事件可以软件产生或者硬件产生)。
当CCR1 寄存器的值转移到其影子寄存器后,新的值就会和计数器的值进行比较,它们的比较结果将会通过第⑥部分影响定时器的输出。

下面来看看第⑥部分通道1 的“放大版”,如下图所示:
正点原子STM32(基于HAL库)2_第161张图片
上图中,可以看到输出模式控制器,由OC1M[2:0]位配置输出比较模式,该位的描述请参考《STM32F10xxx 参考手册_V10(中文版).pdf》相关定时器章节的TIMx_CCMR1 寄存器。
F1 系列有8 种输出比较模式之多,后面用到再来介绍。
oc1ref 是输出参考信号,高电平有效,为高电平时称之为有效电平,为低电平时称之为无效电平。它的高低电平受到三个方面的影响:OC1M[3:0]位配置的输出比较模式、第⑤部分比较器的比较结果、还有就是OC1CE 位配置的ETRF 信号。ETRF 信号可以将Oc1ref 电平强制清零,该信号来自IO 外部。

一般来说,当计数器的值和捕获/比较寄存器的值相等时,输出参考信号oc1ref 的极性就会根据我们选择的输出比较模式而改变。如果开启了比较中断,还会发生比较中断。
CC1P 位用于选择通道输出极性。
CC1E 位置1 使能通道输出。
OC1 信号就会从TIMx_CH1 输出到IO 端口,再到IO 外部。
下面分别通过四个实验来学习通用定时器的功能。

通用定时器中断实验

本小节我们来学习使用通用定时器中断,以定时器3 中断为例,首先来了解相关的寄存器。

TIM2/TIM3/TIM4/TIM5 寄存器

下面介绍TIM2/TIM3/TIM4/TIM5 的几个与定时器中断相关且重要的寄存器,相关内容可以参考《STM32F10xxx 参考手册_V10(中文版).pdf》定时器的相关章节。
⚫ 控制寄存器1(TIMx_CR1)
TIM2/TIM3/TIM4/TIM5 的控制寄存器1 描述如图21.2.1.1 所示:

正点原子STM32(基于HAL库)2_第162张图片
上图中我们只列出了本实验需要用的一些位,其中:位7(APRE)用于控制自动重载寄存器是否进行缓冲,如果ARPE 位置1,ARR 起缓冲作用,即只有在更新事件发生时才会把ARR的值写入其影子寄存器里;如果ARPE 位置0,那么修改自动重载寄存器的值时,该值会马上被写入其影子寄存器中,从而立即生效。
CMS[1:0]位,用于设置边沿对齐模式还是中心对齐模式,本实验我们使用边沿对齐模式,所以设置为00 即可。
DIR 位,用于控制定时器的计数方向,我们使用递增计数模式,所以设置DIR 位为0。CEN 位,用于使能计数器的工作,必须要设置该位为1,计数器才会开始计数。
⚫ 从模式控制寄存器(TIMx_SMCR)
TIM2/TIM3/TIM4/TIM5 的从模式控制寄存器描述如图21.2.1.2 所示:
正点原子STM32(基于HAL库)2_第163张图片

该寄存器的SMS[2:0]位,用于从模式选择,其实就是选择计数器输入时钟的来源。比如通用定时器中断实验我们设置SMS[2:0]=000,禁止从模式,这样PSC 预分频器的时钟就直接来自内部时钟(CK_INT),按照我们例程sys_stm32_clock_init 函数的配置,频率为72Mhz(APB1总线时钟频率的2 倍)。
⚫ DMA/中断使能寄存器(TIMx_DIER)
TIM2/TIM3/TIM4/TIM5 的DMA/中断使能寄存器描述如图21.2.1.3 所示:
正点原子STM32(基于HAL库)2_第164张图片
该寄存器用于使能/失能触发DMA 请求、捕获/比较中断以及更新中断。本实验只用到更新中断,所以把位0(UIE)置1 即可。
⚫ 状态寄存器(TIMx_SR)
TIM2/TIM3/TIM4/TIM5 的状态寄存器描述如图21.2.1.4 所示:

正点原子STM32(基于HAL库)2_第165张图片
该寄存器都是一些中断标志位,比如更新中断标志位、捕获/比较中断标志位等。在通用定时器中断实验我们用到更新中断标志位,当定时器更新中断到来后,位0(UIF)会由硬件置1,我们需要在中断服务函数里面把该位清零。
⚫ 计数寄存器(TIMx_CNT)
TIM2/TIM3/TIM4/TIM5 的计数器寄存器描述如图21.2.1.5 所示:
在这里插入图片描述
TIM2/TIM3/TIM4/TIM5 的计数寄存器都是16 位有效的,计数模式可以是递增计数模式、递减计数模式和中心对齐计数模式,计数值范围0~65535。可以直接写该寄存器设置计数的初始值,也可以读取该寄存器获取计数器值。
⚫ 预分频寄存器(TIMx_PSC)
TIM2/TIM3/TIM4/TIM5 的预分频器寄存器描述如图21.2.1.6 所示。
正点原子STM32(基于HAL库)2_第166张图片
定时器的预分频寄存器都是16 位的,即写入该寄存器的数值范围是0 到65535,表示1 到65536 分频。比如我们要7200 分频,就往该寄存器写入7199。
⚫ 自动重载寄存器(TIMx_ARR)
TIM2/TIM3/TIM4/TIM5 的自动重载寄存器描述如图21.2.1.7 所示。

正点原子STM32(基于HAL库)2_第167张图片
自动重载寄存器是低16 位有效。该寄存器可以由APRE 位设置是否进行缓冲。计数器的值会和自动重装寄存器影子寄存器进行比较,当两者相等,定时器就会溢出,从而发生更新事件,如果打开了更新中断,还会发生更新中断。

硬件设计

  1. 例程功能
    LED0 用来指示程序正在运行,200ms 翻转一次。LED1 在定时器中断中翻转,500ms 进入中断一次。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    LED1 –PE5
    2)定时器3
  3. 原理图
    定时器属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们通过LED1 来指示STM32F103 的定时器进入中断的频率,LED0 则指示程序的运行状态。

程序设计

本实验的相关HAL 库驱动以及实验配置步骤请参考基本定时器相关内容,基本一样。不同点是基本定时器只能是递增计数模式,通用定时器可以递增计数模式、递减计数模式和中心对齐模式。

21.2.3.1 程序流程图
下面看看本实验的程序流程图,main 函数中并没有对LED1 的操作,我们把对LED1 的操作放到定时器的中断中进行处理:

正点原子STM32(基于HAL库)2_第168张图片

正点原子STM32(基于HAL库)2_第169张图片

程序解析

这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。通用定时器驱动源码包括两个文件:gtim.c 和gtim.h。
首先看gtim.h 头文件的几个宏定义:

/* TIMX 中断定义
* 默认是针对TIM2~TIM5
* 注意: 通过修改这4个宏定义,可以支持TIM1~TIM8任意一个定时器.
*/
#define GTIM_TIMX_INT TIM3
#define GTIM_TIMX_INT_IRQn TIM3_IRQn
#define GTIM_TIMX_INT_IRQHandler TIM3_IRQHandler
/* TIM3 时钟使能*/
#define GTIM_TIMX_INT_CLK_ENABLE() do{ __HAL_RCC_TIM3_CLK_ENABLE(); }while(0)

通过修改这4 个宏定义,可以支持TIM1~TIM8 任意一个定时器。
下面再来看一下gtim.c 文件的代码,主要包括两个函数,先来看看通用定时器的初始化函数,其定义如下:

/**
* @brief 通用定时器TIMX定时中断初始化函数
* @note
* 通用定时器的时钟来自APB1,当PPRE1 ≥ 2 分频的时候
* 通用定时器的时钟为APB1时钟的2倍, 而APB1为36M, 所以定时器时钟= 72Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值。
* @param psc: 时钟预分频数
* @retval 无
*/
void gtim_timx_int_init(uint16_t arr, uint16_t psc)
{
	GTIM_TIMX_INT_CLK_ENABLE(); /* 使能TIMx时钟*/
	g_timx_handle.Instance = GTIM_TIMX_INT; /* 通用定时器x */
	g_timx_handle.Init.Prescaler = psc; /* 预分频系数*/
	g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数模式 基本定时器此成员失效*/
	g_timx_handle.Init.Period = arr; /* 自动装载值*/
	HAL_TIM_Base_Init(&g_timx_handle);
	/* 设置中断优先级,抢占优先级1,子优先级3 */
	HAL_NVIC_SetPriority(GTIM_TIMX_INT_IRQn, 1, 3);
	HAL_NVIC_EnableIRQ(GTIM_TIMX_INT_IRQn); /* 开启ITMx中断*/
	HAL_TIM_Base_Start_IT(&g_timx_handle); /* 使能定时器x和定时器x更新中断*/
}

这里配置的参数和基本定时器中断实验的是一样的,只是这里没有使用到HAL 库的HAL_TIM_Base_MspInit 函数来存放NVIC 和使能时钟的代码,而是全部存放到gtim_timx_int_init 函数里。在一个项目中,用到多个定时器时,建议大家使用这种方式来处理代码,这样方便代码的管理。
下面再来看看定时器中断服务函数,其定义如下:

/**
* @brief 定时器中断服务函数
* @param 无
* @retval 无
*/
void GTIM_TIMX_INT_IRQHandler(void)
{
	/* 以下代码没有使用定时器HAL库共用处理函数来处理,而是直接通过判断中断标志位的方式*/
	if(__HAL_TIM_GET_FLAG(&g_timx_handle, TIM_FLAG_UPDATE) != RESET)
	{
		LED1_TOGGLE();
		/* 清除定时器溢出中断标志位*/
		__HAL_TIM_CLEAR_IT(&g_timx_handle, TIM_IT_UPDATE);
	}
}

可以看到,这里我们没有使用HAL 库的定时器公共处理函数来处理中断部分的代码,而是通过自行判断中断标志位的方式来处理。只不过获取标志位的方式还是使用HAL 库的函数宏__HAL_TIM_GET_FLAG(),大家也可以直接使用寄存器的方式来操作。
通过__HAL_TIM_GET_FLAG()获取中断标志位并判断是否了中断,然后处理中断程序,最后通过__HAL_TIM_CLEAR_IT()将中断标志位清零,这样就完成了一次对中断的处理。这样的方式来处理中断,也是大家学习HAL 库需要掌握的。在一个项目中,用到多个定时器相关中断时,建议大家使用这种方式来处理代码,这样方便代码的管理。

下载验证

下载代码后,可以看到LED0 不停闪烁(每400ms 一个周期),而LED1 也是不停的闪烁,但是闪烁时间较LED0 慢(每1s 一个周期)。

通用定时器PWM 输出实验(原理)

本小节我们来学习使用通用定时器的PWM 输出模式。
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。我们可以让定时器产生PWM,在计数器频率固定时,PWM 频率或者周期由自动重载寄存器(TIMx_ARR)的值决定,其占空比由捕获/比较寄存器(TIMx_CCRx)的值决定。PWM 产生原理示意图如下图所示:

正点原子STM32(基于HAL库)2_第170张图片

上图中,定时器工作在递增计数模式,纵轴是计数器的计数值CNT,横轴表示时。当CNT=CCRx 时,IO 输出高电平(逻辑1);当CNT=ARR 时,定时器溢出,CNT 的值被清零,然后继续递增,依次循环。在这个循环中,改变CCRx 的值,就可以改变PWM 的占空比,改变ARR 的值,就可以改变PWM 的频率,这就是PWM 输出的原理。
定时器产生PWM 的方式有许多种,下面我们以边沿对齐模式(即递增计数模式/递减计数模式)为例,PWM 模式1 或者PWM 模式2 产生PWM 的示意图,如下图所示:
正点原子STM32(基于HAL库)2_第171张图片

STM32F103 的定时器除了TIM6 和TIM7,其他的定时器都可以用来产生PWM 输出。其中高级定时器TIM1 和TIM8 可以同时产生多达7 路的PWM 输出。而通用定时器也能同时产生多达4 路的PWM 输出!本实验我们以使用TIM3 的CH2 产生一路PWM 输出为例进行学习。

TIM2/TIM3/TIM4/TIM5 寄存器

要使STM32F103 的通用定时器TIMx 产生PWM 输出,除了上一小节介绍的寄存器外,我们还会用到3 个寄存器,来控制PWM。这三个寄存器分别是:捕获/比较模式寄存器(TIMx_CCMR1/2)、捕获/比较使能寄存器(TIMx_CCER)、捕获/比较寄存器(TIMx_CCR1~4)。

接下来我们简单介绍一下这三个寄存器。
⚫ 捕获/比较模式寄存器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_CCMR1 寄存器描述如图21.3.1.1 所示:
正点原子STM32(基于HAL库)2_第172张图片
该寄存器的有些位在不同模式下,功能不一样,我们现在只用到输出比较,输入捕获后面的实验再讲解。关于该寄存器的详细说明,请参考《STM32F10xxx 参考手册_V10(中文版).pdf》第288 页,14.4.7 节。比如我们要让TIM3 的CH2 输出PWM 波为例进行介绍,该寄存器的模式设置位OC2M[2:0]就是对应着通道2 的模式设置,此部分由3 位组成。总共可以配置成8 种模式,我们使用的是PWM 模式,所以这3 位必须设置为110 或者111,分别对应PWM 模式1和PWM 模式2。这两种PWM 模式的区别就是输出有效电平的极性相反。位OC2PE 控制输出比较通道2 的预装载使能,实际就是控制CCR2 寄存器是否进行缓冲。因为CCR2 寄存器也是有影子寄存器的,影子寄存器才是真正起作用的寄存器。CC2S[1:0]用于设置通道2 的方向(输入/输出)默认设置为0,就是设置通道作为输出使用。
⚫ 捕获/比较使能寄存器(TIMx_CCER)
TIM2/TIM3/TIM4/TIM5 的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER 寄存器描述如图21.3.1.2 所示:

在这里插入图片描述
该寄存器比较简单,要让TIM3 的CH2 输出PWM 波,这里我们要使能CC2E 位,该位是通道2 输入/输出使能位,要想PWM 从IO 口输出,这个位必须设置为1。CC2P 位是设置通道2 的输出极性,我们默认设置0。
⚫ 捕获/比较寄存器1/2/3/4(TIMx_CCR1/2/3/4)
捕获/比较寄存器(TIMx_CCR1/2/3/4),该寄存器总共有4 个,对应4 个通道CH1~CH4。
我们使用的是通道2,所以来看看TIMx_CCR2 寄存器,描述如图21.3.1.3 所示:
正点原子STM32(基于HAL库)2_第173张图片

在输出模式下,捕获/比较寄存器影子寄存器的值与CNT 的值比较,根据比较结果产生相应动作,利用这点,我们通过修改这个寄存器的值,就可以控制PWM 的占空比了。

硬件设计

  1. 例程功能
    使用TIM3 通道2(由PB5 复用)输出PWM,PB5 引脚连接了LED0,从而实现PWM 输出控制LED0 亮度。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)定时器3 输出通道2(由PB5 复用)
  3. 原理图
    定时器属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们通过LED0 来间接指示定时器的PWM 输出情况。

程序设计

定时器的HAL库驱动

正点原子STM32(基于HAL库)2_第174张图片

定时器在HAL 库中的驱动代码在前面介绍基本定时器已经介绍了部分,这里我们再介绍几个本实验用到的函数。

  1. HAL_TIM_PWM_Init 函数
    定时器的PWM 输出模式初始化函数,其声明如下:
    HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
    ⚫ 函数描述:
    用于初始化定时器的PWM 输出模式。
    ⚫ 函数形参:
    形参1 是TIM_HandleTypeDef 结构体类型指针变量,基本定时器的时候已经介绍。
    ⚫ 函数返回值:
    HAL_StatusTypeDef 枚举类型的值。
    ⚫ 注意事项:
    该函数实现的功能以及使用方法和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; 		/* 输出比较模式选择,寄存器的时候说过了,共8种模式*/
	uint32_t Pulse; 		/* 设置比较值*/
	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。这里我们使用的是定时器3 的通道2,所以取值为TIM_CHANNEL_2 即可。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
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 库也同样提供了单独使能定时器的输出通道函数,函数为:

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 枚举类型的值。
⚫ 注意事项:
该函数主要配置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);

后面的定时器初始化凡是用到内部时钟我们都没有去初始化,系统默认即可。

定时器PWM 输出模式配置步骤

正点原子STM32(基于HAL库)2_第175张图片

1)开启TIMx 和通道输出的GPIO 时钟,配置该IO 口的复用功能输出
首先开启TIMx 的时钟,然后配置GPIO 为复用功能输出。本实验我们默认用到定时器3 通道2,对应IO 是PB5,它们的时钟开启方法如下:

__HAL_RCC_TIM3_CLK_ENABLE(); /* 使能定时器3 */
__HAL_RCC_GPIOB_CLK_ENABLE(); /* 开启GPIOB时钟*/

IO 口复用功能是通过函数HAL_GPIO_Init 来配置的。
2)初始化TIMx,设置TIMx 的ARR 和PSC 等参数使用定时器的PWM 输出功能时,通过HAL_TIM_PWM_Init 函数初始化定时器ARR 和PSC 等参数。
注意:该函数会调用:HAL_TIM_PWM_MspInit函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO 初始化、中断使能以及优先级设置等代码。
3)设置TIMx_CHy 的PWM 模式,输出比较极性,比较值等参数
在HAL 库中,通过HAL_TIM_PWM_ConfigChannel 函数来设置定时器为PWM1 模式或者PWM2 模式,根据需求设置输出比较的极性,设置比较值(控制占空比)等。
4)使能TIMx,使能TIMx 的CHy 输出
在HAL 库中,通过调用HAL_TIM_PWM_Start 函数来使能TIMx 的某个通道输出PWM。
5)修改TIM3_CCR2 来控制占空比
在经过以上设置之后,PWM 其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改比较值来控制PWM 的输出占空比。HAL 库中提供一个修改占空比的宏定义:

__HAL_TIM_SET_COMPARE (__HANDLE__, __CHANNEL__, __COMPARE__)

__HANDLE__是TIM_HandleTypeDef 结构体类型指针变量,__CHANNEL__对应PWM 的输出通道,__COMPARE__则是要写到捕获/比较寄存器(TIMx_CCR1/2/3/4)的值。实际上该宏定义最终还是往对应的捕获/比较寄存器写入比较值来控制PWM 波的占空比,如下解析:
比如我们要修改定时器3 通道2 的输出比较值(控制占空比),寄存器操作方法:

TIM3->CCR2 = ledrpwmval; /* ledrpwmval是比较值,并且动态变化的,
							所以我们要周期性调用这条语句,已到达及时修改PWM的占空比*/

__HAL_TIM_SET_COMPARE 这个宏定义函数最终也是调用这个寄存器操作的,所以说我们使用HAL 库的函数其实就是间接操作寄存器的。

程序流程图

正点原子STM32(基于HAL库)2_第176张图片

课堂源码解析

正点原子STM32(基于HAL库)2_第177张图片

在这里插入图片描述
gtim.h

#ifndef __GTIM_H
#define __GTIM_H

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


void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc);

#endif

gtim.c

#include "./BSP/TIMER/gtim.h"

TIM_HandleTypeDef g_timx_pwm_chy_handle;

/* 通用定时器PWM输出初始化函数 */
void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc)
{
    TIM_OC_InitTypeDef timx_oc_pwm_chy;
    
    g_timx_pwm_chy_handle.Instance = TIM3;//寄存器基地址
    g_timx_pwm_chy_handle.Init.Prescaler = psc;//分频系数
    g_timx_pwm_chy_handle.Init.Period = arr;//自动重装载值
    g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;//计数模式,向上
    HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle);
    
    //配置PWM模式/比较值等
    timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1;//PWM模式1
    timx_oc_pwm_chy.Pulse = arr / 2;		 //比较值,即设置CCR寄存器  这里设置占空比为50%
    timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_LOW;//输出极性,低电平有效
    HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, &timx_oc_pwm_chy, TIM_CHANNEL_2);
    HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, TIM_CHANNEL_2);//使能输出并开启定时器 
}

/* 定时器输出PWM MSP初始化函数 */
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM3)
    {
        GPIO_InitTypeDef gpio_init_struct;
        __HAL_RCC_GPIOB_CLK_ENABLE();//时钟使能
        __HAL_RCC_TIM3_CLK_ENABLE();

        gpio_init_struct.Pin = GPIO_PIN_5;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;            /* 推挽复用 */
        gpio_init_struct.Pull = GPIO_PULLUP;                /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;      /* 高速 */
        HAL_GPIO_Init(GPIOB, &gpio_init_struct);
        
        __HAL_RCC_AFIO_CLK_ENABLE();					//开PB5重映射时钟
        __HAL_AFIO_REMAP_TIM3_PARTIAL();				//开启复用,HAL库底层去操作寄存器
    }
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/TIMER/gtim.h"

extern TIM_HandleTypeDef g_timx_pwm_chy_handle;     /* 定时器x句柄 */

int main(void)
{
    uint16_t ledrpwmval = 0;
    uint8_t dir = 1;
    
    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟,72M */
    delay_init(72);                             /* 初始化延时函数 */
    led_init();                                 /* 初始化LED */
    gtim_timx_pwm_chy_init(500 - 1, 72 - 1);
    
    while(1)
    {
        delay_ms(10);

        /************************呼吸灯部分  正占空比40% - 100% ******************************/
        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后,方向改为递增 */
        

        /* 修改比较值控制占空比 */
        __HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, TIM_CHANNEL_2, ledrpwmval);
    }
}

程序解析

这里我们这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。通用定时器驱动源码包括两个文件:gtim.c 和gtim.h。
首先看gtim.h 头文件的几个宏定义:

/* TIMX PWM输出定义
* 这里输出的PWM控制LED0(RED)的亮度
* 默认是针对TIM2~TIM5
* 注意: 通过修改这几个宏定义,可以支持TIM1~TIM8任意一个定时器,任意一个IO口输出PWM
*/
#define GTIM_TIMX_PWM_CHY_GPIO_PORT GPIOB
#define GTIM_TIMX_PWM_CHY_GPIO_PIN GPIO_PIN_5
#define GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOB_CLK_ENABLE();\
}while(0) /* PB口时钟使能*/

/* TIMX REMAP设置
* 因为我们LED0接在PB5上, 必须通过开启TIM3的部分重映射功能, 才能将TIM3_CH2输出到PB5上
* 因此, 必须实现GTIM_TIMX_PWM_CHY_GPIO_REMAP
* 对那些使用默认设置的定时器PWM输出脚, 不用设置重映射, 是不需要该函数的!
*/
#define GTIM_TIMX_PWM_CHY_GPIO_REMAP() do{__HAL_RCC_AFIO_CLK_ENABLE();\
__HAL_AFIO_REMAP_TIM3_PARTIAL();\
}while(0)

#define GTIM_TIMX_PWM TIM3
#define GTIM_TIMX_PWM_CHY TIM_CHANNEL_2 /* 通道Y, 1<= Y <=4 */
#define GTIM_TIMX_PWM_CHY_CCRX TIM3->CCR2 /* 通道Y的输出比较寄存器*/
#define GTIM_TIMX_PWM_CHY_CLK_ENABLE() do{ __HAL_RCC_TIM3_CLK_ENABLE();\
}while(0) /* TIM3 时钟使能*/

可以把上面的宏定义分成三部分,第一部分是定时器3 输出通道2 对应的IO 口的宏定义。
第二部分是定时器3 的部分重映射功能的宏定义,第三部分则是定时器3 输出通道2 的相应宏定义。
下面看gtim.c 的程序,首先是通用定时器PWM 输出初始化函数。

/**
* @brief 通用定时器TIMX 通道Y PWM输出初始化函数(使用PWM模式1)
* @note
* 通用定时器的时钟来自APB1,当D2PPRE1≥2 分频的时候
* 通用定时器的时钟为APB1时钟的2倍, 而APB1为36M, 所以定时器时钟= 72Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
* @param arr: 自动重装值。
* @param psc: 时钟预分频数
* @retval 无
*/
void gtim_timx_pwm_chy_init(uint16_t arr,uint16_t psc)
{
	g_timx_pwm_chy_handle.Instance = GTIM_TIMX_PWM; /* 定时器x */
	g_timx_pwm_chy_handle.Init.Prescaler = psc; /* 定时器分频*/
	g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 递增计数模式*/
	g_timx_pwm_chy_handle.Init.Period = arr; /* 自动重装载值*/
	HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle); /* 初始化PWM */
	g_timx_oc_pwm_chy_handle.OCMode = TIM_OCMODE_PWM1; /* 模式选择PWM1 */
	/* 设置比较值,此值用来确定占空比,默认比较值为自动重装载值的一半,即占空比为50% */
	g_timx_oc_pwm_chy_handle.Pulse = arr/2;
	g_timx_oc_pwm_chy_handle.OCPolarity = TIM_OCPOLARITY_LOW;/* 输出比较极性为低*/
	HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, &g_timx_oc_pwm_chy_handle,
	GTIM_TIMX_PWM_CHY); /* 配置TIMx通道y */
	HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY);/*开启PWM通道*/
}

HAL_TIM_PWM_Init 初始化TIM3 并设置TIM3 的ARR 和PSC 等参数,其次通过调用函数HAL_TIM_PWM_ConfigChannel 设置TIM3_CH2 的PWM 模式以及比较值等参数,最后通过调用函数HAL_TIM_PWM_Start 来使能TIM3 以及使能PWM 通道TIM3_CH2 输出。
本实验我们使用PWM 的MSP 初始化回调函数HAL_TIM_PWM_MspInit 来存放时钟、GPIO 的初始化代码,其定义如下:

/**
* @brief 定时器底层驱动,时钟使能,引脚配置
此函数会被HAL_TIM_PWM_Init()调用
* @param htim:定时器句柄
* @retval 无
*/
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
	if (htim->Instance == GTIM_TIMX_PWM)
	{
		GPIO_InitTypeDef gpio_init_struct;
		GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE(); /* 开启通道y的CPIO时钟*/
		GTIM_TIMX_PWM_CHY_CLK_ENABLE();
		gpio_init_struct.Pin = GTIM_TIMX_PWM_CHY_GPIO_PIN; /* 通道y的CPIO口*/
		gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推完输出*/
		gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉*/
		gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
		HAL_GPIO_Init(GTIM_TIMX_PWM_CHY_GPIO_PORT, &gpio_init_struct);
		GTIM_TIMX_PWM_CHY_GPIO_REMAP(); /* IO口REMAP设置,设置重映射*/
	}
}

该函数首先判断定时器寄存器基地址,符合条件后,开启对应的GPIO 时钟和定时器时钟,并且初始化GPIO。上面是使用HAL 库标准的做法,我们亦可把HAL_TIM_PWM_MspInit 函数里面的代码直接放到gtim_timx_pwm_chy_init 函数里。这样做的好处是当一个项目中用到多个定时器时,代码的移植性、可读性好,方便管理。
在main.c 里面编写如下代码:

int main(void)
{
	uint16_t ledrpwmval = 0;
	uint8_t dir = 1;
	HAL_Init(); /* 初始化HAL库*/
	sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72); /* 延时初始化*/
	usart_init(115200); /* 串口初始化为115200 */
	led_init(); /* 初始化LED */
	/* 72M/72=1M的计数频率,自动重装载为500,那么PWM频率为1M/500=2kHZ */
	gtim_timx_pwm_chy_init(500 - 1, 72 - 1);
	while (1)
	{
		delay_ms(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后,方向改为递增*/
		/* 修改比较值控制占空比*/
		__HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY,
							ledrpwmval);
	}
}

本小节开头我们就说PWM 波频率由自动重载寄存器(TIMx_ARR)的值决定,其占空比则由捕获/比较寄存器(TIMx_CCRx)的值决定。下面结合实际看看具体怎么计算:
定时器3 的时钟源频率为2 倍APB1 总线时钟频率,即频率为72MHz,而调用gtim_timx_pwm_chy_init 初始化函数之后,就相当于写入预分频寄存器的值为71,写入自动重载寄存器的值为499。基本定时器讲的定时器溢出公式由公式得:

Tout= ((arr+1)*(psc+1))/Tclk= ((499+1)*(71+1))/72000000=0.0005s

再由频率是周期的倒数关系得到PWM 的频率为2000Hz。
占空比怎么计算的呢?结合图21.3.1,我们分两种情况分析,输出比较极性为低和输出比较极性为高,它们的情况正好相反。因为在main 函数中的比较值是动态变化的,不利于我们计算占空比,我们假设比较值固定为200,在本实验中可以调用如下语句得到。
__HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY, 200);
因为LED0 是低电平有效,所以我们在gtim_timx_pwm_chy_init 函数中设置了输出比较极性为低,那么当比较值固定为200 时,占空比= ((arr+1) – CCR1) / (arr+1) = (500-200)/500=60%。
其中arr 是写入自动重载寄存器(TIMx_ARR)的值,CCR2 就是写入捕获/比较寄存器2(TIMx_CCR2)的值。这里我们还需要提醒一下,占空比是指在一个周期内,高电平时间相对于总时间所占的比例。
另外一种情况:设置了输出比较极性为高,那么当比较值固定为200 时,占空比= CCR2/ (arr+1) = 200/500=40%。可以看到输出比较极性为低和输出比较极性为高的占空比正好反过来。
在这里,我们也用了DS100 示波器进行验证,效果图如图21.3.3.3.1 所示:
正点原子STM32(基于HAL库)2_第178张图片
这里把输出比较极性低和输出比较极性高的PWM 波形都显示出来了。本实验默认设置PWM 模式1、输出比较极性低,当CCR2 寄存器的值设置为200 时,对应的PWM 波形如上图黄色的波形图。如果把输出比较极性设置为高,对应的波形图就是绿色的波形图了。
大家感兴趣也可以自行用示波器进行验证。

21.3.4 下载验证
下载代码后,此时定时器3 通道2 输出PWM 信号到PB5 口。可以看到LED0 不停的由暗变到亮,然后又从亮变到暗。

通用定时器输入捕获实验(测量时间)

本小节我们来学习使用通用定时器的输入捕获模式。
输入捕获模式可以用来测量脉冲宽度或者测量频率。我们以测量脉宽为例,用一个简图来
说明输入捕获脉宽测量原理,如图21.4.1 所示:

正点原子STM32(基于HAL库)2_第179张图片

图21.4.1 中,t1 到t2 的时间段,就是我们需要测量的高电平时间。测量方法如下:假如定
时器工作在递增计数模式,首先设置定时器通道x 为上升沿捕获,这样在t1 时刻上升沿到来
时,就会发生捕获事件。这里我们还会打开捕获中断,所以捕获事件发生就意味着捕获中断也会发生。在捕获中断里将计数器值清零,并设置通道x 为下降沿捕获,这样t2 时刻下降沿到来时,就会发生捕获事件和捕获中断。捕获事件发生时,计数器的值会被锁存到捕获/比较寄存器中(比如通道1 对应的是CCR1 寄存器)。那么在捕获中断里,我们读取捕获/比较寄存器就可以获取到高电平脉冲时间内,计数器计数的个数,从而可以算出高电平脉冲的时间。这里是假设定时器没有溢出为前提的。

实际上,t1 到t2 时间段,定时器可能会产生N 次溢出,这就需要我们对定时器溢出做相应
的处理,防止高电平太长,导致测量出错。在t1 到t2 时间段,假设定时器溢出N 次,那么高
电平脉冲时间内,计数器计数的个数计算方法为:N*(ARR+1)+ CCRx2,CCRx2 表示t2 时间点,捕获/比较寄存器的值。经过计算得到高电平脉宽时间内计数器计数个数后,用这个个数乘以计数器的计数周期,就可得到高电平持续的时间。就是输入捕获测量高电平脉宽时间的整个过程。

STM32F103 的定时器除了TIM6 和TIM7,其他定时器都有输入捕获功能。输入捕获,简
单的说就是通过检测TIMx_CHy 上的边沿信号,在边沿信号发生跳变(比如上升沿/下降沿)
时,会发生捕获事件,将当前定时器的值(TIMx_CNT)锁存到对应通道的捕获/比较寄存器
(TIMx_CCRy)里,完成一次捕获。同时还可以配置捕获事件发生时是否触发捕获中断/DMA。

另外还要考虑测量的过程中是否可能发生定时器溢出,如果可能溢出,还要做溢出处理。

TIM2/TIM3/TIM4/TIM5 寄存器

通用定时器输入捕获实验需要用到的寄存器有:TIMx_ARR、TIMx_PSC、TIMx_CCMR1、
TIMx_CCER、TIMx_DIER、TIMx_CR1、TIMx_CCR1 这些寄存器在前面的章节都有提到,在这里只需针对性的介绍。
⚫ 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
该寄存器我们在PWM 输出实验时讲解了他作为输出功能的配置,现在重点学习输入捕获
模式的配置。因为本实验我们用到定时器5 通道1 输入,所以我们要看TIMx_CCMR1 寄存器,其描述如图21.4.1.1 所示:
正点原子STM32(基于HAL库)2_第180张图片
该寄存器在输入模式和输出模式下,功能是不一样,所以需要看准不同模式的描述,请打
开手册查看。TIMx_CCMR1 寄存器对应于通道1 和通道2 的设置,CCMR2 寄存器对应通道3和通道4。如:TIMx_CCMR1 寄存器位[7:0]用于捕获/比较通道1 的控制,而位[15:8]则用于捕获/比较通道2 的控制。我们用到定时器5 通道1 输入,所以需要配置TIMx_CCMR1 的位[7:0]。
其中CC1S[1:0],这两个位用于CCR1 的通道配置,这里我们设置IC1S[1:0]=01,也就是配
置IC1 映射在TI1 上。
输入捕获1 预分频器IC1PSC[1:0],这个比较好理解。我们是1 次边沿就触发1 次捕获,所
以选择00 就行了。
输入捕获1 滤波器IC1F[3:0],这个用来设置输入采样频率和数字滤波器长度。其中,fCK_INT是定时器时钟源频率,按照例程的配置为72Mhz,而fDTS则是根据TIMx_CR1 的CKD[1:0]的设置来确定的,如果CKD[1:0]设置为00,那么fDTS=fCK_INT。N 值采样次数,举个简单的例子:假设IC1F[3:0]=0011,并设置IC1 映射到TI1 上。表示以fCK_INT为采样频率,当连续8 次都是采样到TI1 为高电平或者低电平,滤波器才输出一个有效输出边沿。当8 次采样中有高有低,那就保持原来的输出,这样可以滤除高频干扰信号,从而达到滤波的效果。这里,我们不做滤波处理,所以设置IC1F[3:0]=0000。
⚫ 捕获/比较使能寄存器(TIMx_CCER)
TIM2/TIM3/TIM4/TIM5 的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开
关和极性。TIMx_CCER 寄存器描述如图21.4.1.2 所示:

正点原子STM32(基于HAL库)2_第181张图片
我们要用到这个寄存器的最低2 位,CC1E 和CC1P 位。要使能输入捕获,必须设置CC1E=1,而CC1P 则根据自己的需要来配置。我们这里是保留默认设置值0,即高电平触发捕获。
接下来我们再看看DMA/中断使能寄存器:TIMx_DIER,该寄存器的各位描述见图21.2.1.3
(在21.2.1 小节)。本小节,我们需要用到中断来处理捕获数据,所以必须开启通道1 的捕获比较中断,即CC1IE 设置为1。同时我们还需要在定时器溢出中断中累计定时器溢出的次数,所以还需要使能定时器的更新中断,即UIE 置1。
控制寄存器:TIMx_CR1,我们只用到了它的最低位,也就是用来使能定时器的。
最后再来看看捕获/比较寄存器1:TIMx_CCR1,该寄存器用来存储发生捕获事件时,
TIMx_CNT 的值,我们从TIMx_CCR1 就可以读出通道1 捕获事件发生时刻的TIMx_CNT 值,通过两次捕获(一次上升沿捕获,一次下降沿捕获)的差值,就可以计算出高电平脉冲的宽度(注意,对于高电平脉宽太长的情况,还要计算定时器溢出的次数)。

硬件设计

  1. 例程功能
    1、使用TIM5_CH1 来做输入捕获,捕获PA0 上的高电平脉宽,并将脉宽时间通过串口打印出来,然后通过按WK_UP 按键,模拟输入高电平,例程中能测试的最长高电平脉宽时间为:
    4194303 us。
    2、LED0 闪烁指示程序运行。
  2. 硬件资源
    1)LED 灯:
    LED0 – PB5
    2)独立按键:
    WK_UP - PA0
    3)定时器5,使用TIM5 通道1,将PA0 复用为TIM5_CH1。
  3. 原理图
    定时器属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们借助WK_UP做输入脉冲源并通过串口上位机来监测定时器输入捕获的情况。

程序设计

定时器的HAL 库驱动

正点原子STM32(基于HAL库)2_第182张图片

定时器在HAL 库中的驱动代码在前面已经介绍了部分,这里我们再介绍几个本实验用到
的函数。

  1. HAL_TIM_IC_Init 函数
    定时器的输入捕获模式初始化函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_IC_Init(TIM_HandleTypeDef *htim);

⚫ 函数描述:
用于初始化定时器的输入捕获模式。
⚫ 函数形参:
形参1 是TIM_HandleTypeDef 结构体类型指针变量,介绍基本定时器的时候已经介绍。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
⚫ 注意事项:
与PWM 输出实验一样,当使用定时器做输入捕获功能时,在HAL 库中并不使用定时器初
始化函数HAL_TIM_Base_Init 来实现,而是使用输入捕获特定的定时器初始化函数
HAL_TIM_IC_Init。该函数内部还会调用输入捕获初始化回调函数HAL_TIM_IC_MspInit 来初始化输入通道对应的GPIO(复用),以及输入捕获相关的配置。

  1. 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; 	/* 输入捕获分频系数 比如设置2,两个时钟沿才会触发一次捕获事件*/
	uint32_t ICFilter; 		/* 输入捕获滤波器设置*/
} TIM_IC_InitTypeDef;

该结构体成员我们现在介绍一下。成员变量ICPolarity 用来设置输入信号的有效捕获极性,
取值范围为:TIM_ICPOLARITY_RISING(上升沿捕获),TIM_ICPOLARITY_FALLING(下降沿捕获)和TIM_ICPOLARITY_BOTHEDGE(双边沿捕获)。成员变量ICSelection 用来设置映射关系,我们配置IC1 直接映射在TI1 上,选择TIM_ICSELECTION_DIRECTTI(另外还有两个输入通道TIM_ICSELECTION_INDIRECTTI 和TIM_ICSELECTION_TRC )。成员变量ICPrescaler 用来设置输入捕获分频系数,可以设置为TIM_ICPSC_DIV1 (不分频),TIM_ICPSC_DIV2(2 分频),TIM_ICPSC_DIV4(4 分频)以及TIM_ICPSC_DIV8(8 分频),本实验需要设置为不分频,所以选值为TIM_ICPSC_DIV1。成员变量ICFilter 用来设置滤波器长度,这里我们不使用滤波器,所以设置为0。
形参3 是定时器通道,范围:TIM_CHANNEL_1 到TIM_CHANNEL_4。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。

  1. 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 库函数为:

HAL_StatusTypeDef HAL_TIM_IC_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
定时器输入捕获模式配置步骤

正点原子STM32(基于HAL库)2_第183张图片

1)开启TIMx 和输入通道的GPIO 时钟,配置该IO 口的复用功能输入
首先开启TIMx 的时钟,然后配置GPIO 为复用功能输出。本实验我们默认用到定时器5 通
道1,对应IO 是PA0,它们的时钟开启方法如下:

__HAL_RCC_TIM5_CLK_ENABLE(); /* 使能定时器5 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟*/

IO 口复用功能是通过函数HAL_GPIO_Init 来配置的。
2)初始化TIMx,设置TIMx 的ARR 和PSC 等参数
使用定时器的输入捕获功能时,我们调用的是HAL_TIM_IC_Init 函数来初始化定时器ARR
和PSC 等参数。
注意:该函数会调用:HAL_TIM_IC_MspInit 函数,我们可以通过后者存放定时器和GPIO
时钟使能、GPIO 初始化、中断使能以及优先级设置等代码。
3)设置TIMx_CHy 的输入捕获模式,开启输入捕获在HAL 库中,定时器的输入捕获模式是通过HAL_TIM_IC_ConfigChannel 函数来设置定
时器某个通道为输入捕获通道,包括映射关系,输入滤波和输入分频等。
4)使能定时器更新中断,开启捕获功能以及捕获中断,配置定时器中断优先级
通过__HAL_TIM_ENABLE_IT 函数使能定时器更新中断。
通过HAL_TIM_IC_Start_IT 函数使能定时器并开启捕获功能以及捕获中断。
通过HAL_NVIC_EnableIRQ 函数使能定时器中断。
通过HAL_NVIC_SetPriority 函数设置中断优先级。
因为我们要捕获的是高电平信号的脉宽,所以,第一次捕获是上升沿,第二次捕获时下降
沿,必须在捕获上升沿之后,设置捕获边沿为下降沿,同时,如果脉宽比较长,那么定时器就会溢出,对溢出必须做处理,否则结果就不准了。
5)编写中断服务函数
定时器5 中断服务函数为:TIM5_IRQHandler,当发生中断的时候,程序就会执行中断服
务函数。HAL 库为了使用方便,提供了一个定时器中断通用处理函数HAL_TIM_IRQHandler,该函数会调用一些定时器相关的回调函数,用于给用户处理定时器中断到了之后,需要处理的程序。本实验我们除了用到更新(溢出)中断回调函数HAL_TIM_PeriodElapsedCallback 之外,还要用到捕获中断回调函数HAL_TIM_IC_CaptureCallback。详见本实验例程源码。

程序流程图
正点原子STM32(基于HAL库)2_第184张图片

课堂源码解析

正点原子STM32(基于HAL库)2_第185张图片

正点原子STM32(基于HAL库)2_第186张图片
正点原子STM32(基于HAL库)2_第187张图片
gtim.h

#ifndef __GTIM_H
#define __GTIM_H

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


void gtim_timx_cap_chy_init(uint16_t arr, uint16_t psc);

#endif

gtim.c

#include "./BSP/TIMER/gtim.h"


TIM_HandleTypeDef g_timx_cap_chy_handle;     /* 定时器x句柄 */

/* 通用定时器通道y 输入捕获 初始化函数 */
void gtim_timx_cap_chy_init(uint16_t arr, uint16_t psc)
{
    TIM_IC_InitTypeDef timx_ic_cap_chy = {0};

    g_timx_cap_chy_handle.Instance = TIM5;                              /* 定时器5 */
    g_timx_cap_chy_handle.Init.Prescaler = psc;                         /* 定时器分频 */
    g_timx_cap_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;        /* 递增计数模式 */
    g_timx_cap_chy_handle.Init.Period = arr;                            /* 自动重装载值 */
    HAL_TIM_IC_Init(&g_timx_cap_chy_handle);

    timx_ic_cap_chy.ICPolarity = TIM_ICPOLARITY_RISING;                 /* 上升沿捕获 */
    timx_ic_cap_chy.ICSelection = TIM_ICSELECTION_DIRECTTI;             /* 映射到TI1上 */
    timx_ic_cap_chy.ICPrescaler = TIM_ICPSC_DIV1;                       /* 配置输入分频,不分频 */
    timx_ic_cap_chy.ICFilter = 0;                                       /* 配置输入滤波器,不滤波 */
    HAL_TIM_IC_ConfigChannel(&g_timx_cap_chy_handle, &timx_ic_cap_chy, TIM_CHANNEL_1);  /* 配置TIM5通道1 */

    __HAL_TIM_ENABLE_IT(&g_timx_cap_chy_handle, TIM_IT_UPDATE);         /* 使能更新中断 */
    HAL_TIM_IC_Start_IT(&g_timx_cap_chy_handle, TIM_CHANNEL_1);         /* 开始捕获TIM5的通道1 */
}

/* 定时器 输入捕获 MSP初始化函数 */
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM5)                             /*输入通道捕获*/
    {
        GPIO_InitTypeDef gpio_init_struct;
        __HAL_RCC_TIM5_CLK_ENABLE();                        /* 使能TIM5时钟 */
        __HAL_RCC_GPIOA_CLK_ENABLE();                       /* 开启捕获IO的时钟 */

        gpio_init_struct.Pin = GPIO_PIN_0;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;            /* 复用推挽输出 */
        gpio_init_struct.Pull = GPIO_PULLDOWN;              /* 下拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;      /* 高速 */
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);

        HAL_NVIC_SetPriority(TIM5_IRQn, 1, 3);              /* 抢占1,子优先级3 */
        HAL_NVIC_EnableIRQ(TIM5_IRQn);                      /* 开启ITMx中断 */
    }
}

/* 输入捕获状态(g_timxchy_cap_sta)
 * [7]  :0,没有成功的捕获;1,成功捕获到一次.
 * [6]  :0,还没捕获到高电平;1,已经捕获到高电平了.
 * [5:0]:捕获高电平后溢出的次数,最多溢出63次,所以最长捕获值 = 63*65536 + 65535 = 4194303(前面计数个数公式)
 *       注意:为了通用,我们默认ARR和CCRy都是16位寄存器,对于32位的定时器(如:TIM5),也只按16位使用
 *       按1us的计数频率,最长溢出时间为:4194303 us, 约4.19秒
 *
 *      (说明一下:正常32位定时器来说,1us计数器加1,溢出时间:4294秒)
 */
uint8_t g_timxchy_cap_sta = 0;    /* 输入捕获状态 */
uint16_t g_timxchy_cap_val = 0;   /* 输入捕获值 */


/* 定时器5中断服务函数 */
void TIM5_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&g_timx_cap_chy_handle);  /* 定时器HAL库共用处理函数 */
}

/* 定时器输入捕获中断处理回调函数 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM5)
    {
        if ((g_timxchy_cap_sta & 0X80) == 0)                /* 位7等于0 还没有成功捕获 */
        {
            if (g_timxchy_cap_sta & 0X40)                   /* 位6 捕获到一个下降沿 */
            {
                g_timxchy_cap_sta |= 0X80;                  /* 标记成功捕获到一次高电平脉宽 */
                g_timxchy_cap_val = HAL_TIM_ReadCapturedValue(&g_timx_cap_chy_handle, TIM_CHANNEL_1);  /* 获取当前的捕获值 */
                TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, TIM_CHANNEL_1);                      /* 一定要先清除原来的设置 */
                TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING); /* 配置TIM5通道1上升沿捕获 */
            }
            else /* 还未开始,第一次捕获上升沿 */
            {
                g_timxchy_cap_sta = 0;                              /* 清空 */
                g_timxchy_cap_val = 0;
                g_timxchy_cap_sta |= 0X40;                          /* 位6置1 标记捕获到了上升沿 */
                __HAL_TIM_DISABLE(&g_timx_cap_chy_handle);          /* 关闭定时器5 */
                //前面流程图里 干两件事  清零和改成下降沿
                __HAL_TIM_SET_COUNTER(&g_timx_cap_chy_handle, 0);   /* 定时器5计数器清零 */
                TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, TIM_CHANNEL_1);   /* 一定要先清除原来的设置!! */
                TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, TIM_CHANNEL_1, TIM_ICPOLARITY_FALLING); /* 定时器5通道1设置为下降沿捕获 */
                __HAL_TIM_ENABLE(&g_timx_cap_chy_handle);           /* 使能定时器5 */
            }
        }
    }
}

/* 定时器更新中断回调函数  发生了溢出 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM5)
    {
        if ((g_timxchy_cap_sta & 0X80) == 0)            /* 还未成功捕获 */
        {
            if (g_timxchy_cap_sta & 0X40)               /* 已经捕获到高电平了 */
            {
                if ((g_timxchy_cap_sta & 0X3F) == 0X3F) /* 高电平太长了,溢出次数超过63次 */
                {
                    TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, TIM_CHANNEL_1);                     /* 一定要先清除原来的设置 */
                    TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING);/* 配置TIM5通道1上升沿捕获 */
                    g_timxchy_cap_sta |= 0X80;          /* 标记成功捕获了一次 */
                    g_timxchy_cap_val = 0XFFFF;
                }
                else      /* 累计定时器溢出次数 */
                {
                    g_timxchy_cap_sta++;
                }
            }
        }
    }
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/TIMER/gtim.h"


extern uint8_t  g_timxchy_cap_sta;  /* 输入捕获状态 */
extern uint16_t g_timxchy_cap_val;  /* 输入捕获值 */

int main(void)
{
    uint32_t temp = 0;
    uint8_t t = 0;

    HAL_Init();                             /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);     /* 设置时钟, 72Mhz */
    delay_init(72);                         /* 延时初始化 */
    usart_init(115200);                     /* 串口初始化为115200 */
    led_init();                             /* 初始化LED */
    					  //自动重装载值
    gtim_timx_cap_chy_init(0XFFFF, 72 - 1); /* 以1Mhz的频率计数 捕获 */
								   //预分频系数
    while (1)
    {
        if (g_timxchy_cap_sta & 0X80)       /* 成功捕获到了一次高电平 */
        {
            temp = g_timxchy_cap_sta & 0X3F;
            temp *= 65536;                  /* 溢出时间总和 */
            temp += g_timxchy_cap_val;      /* 得到总的高电平时间 计数一次就是1 us*/
            printf("HIGH:%d us\r\n", temp); /* 打印总的高点平时间 */
            g_timxchy_cap_sta = 0;          /* 开启下一次捕获*/
        }

        t++;

        if (t > 20)         /* 200ms进入一次 */
        {
            t = 0;
            LED0_TOGGLE();  /* LED0闪烁 ,提示程序运行 */
        }
        delay_ms(10);
    }
}

正点原子STM32(基于HAL库)2_第188张图片

程序解析

这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。通用定时器驱动
源码包括两个文件:gtim.c 和gtim.h。
首先看gtim.h 头文件的几个宏定义:

/* TIMX 输入捕获定义
* 这里的输入捕获使用定时器TIM5_CH1,捕获WK_UP按键的输入
* 默认是针对TIM2~TIM5.
* 注意: 通过修改这几个宏定义,可以支持TIM1~TIM8任意一个定时器,任意一个IO口做输入捕获
* 特别要注意:默认用的PA0,设置的是下拉输入!如果改其他IO,对应的上下拉方式也得改!
*/
#define GTIM_TIMX_CAP_CHY_GPIO_PORT 		GPIOA
#define GTIM_TIMX_CAP_CHY_GPIO_PIN 			GPIO_PIN_0
#define GTIM_TIMX_CAP_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE();\
											}while(0) /* PA口时钟使能*/

#define GTIM_TIMX_CAP 						TIM5
#define GTIM_TIMX_CAP_IRQn 					TIM5_IRQn
#define GTIM_TIMX_CAP_IRQHandler 			TIM5_IRQHandler
#define GTIM_TIMX_CAP_CHY 					TIM_CHANNEL_1 /* 通道Y, 1<= Y <=4 */
#define GTIM_TIMX_CAP_CHY_CCRX 				TIM5->CCR1 /* 通道Y的输出比较寄存器*/
#define GTIM_TIMX_CAP_CHY_CLK_ENABLE() 		do{ __HAL_RCC_TIM5_CLK_ENABLE();\
											}while(0) /* TIM5 时钟使能*/

可以把上面的宏定义分成两部分,第一部分是定时器5 输入通道1 对应的IO 口的宏定义,
第二部分则是定时器5 输入通道1 的相应宏定义。
下面看gtim.c 的程序,首先是通用定时器输入捕获初始化函数。

/**
* @brief 通用定时器TIMX 通道Y 输入捕获初始化函数
* @note
* 通用定时器的时钟来自APB1,当PPRE1 ≥ 2 分频的时候
* 通用定时器的时钟为APB1时钟的2倍, 而APB1为36M, 所以定时器时钟= 72Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值
* @param psc: 时钟预分频数
* @retval 无
*/
void gtim_timx_cap_chy_init(uint16_t arr, uint16_t psc)
{
	TIM_IC_InitTypeDef timx_ic_cap_chy = {0};
	
	g_timx_cap_chy_handle.Instance = GTIM_TIMX_CAP; /* 定时器5 */
	g_timx_cap_chy_handle.Init.Prescaler = psc; /* 定时器分频*/
	g_timx_cap_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 递增计数模式*/
	g_timx_cap_chy_handle.Init.Period = arr; /* 自动重装载值*/
	HAL_TIM_IC_Init(&g_timx_cap_chy_handle);
	
	timx_ic_cap_chy.ICPolarity = TIM_ICPOLARITY_RISING; /* 上升沿捕获*/
	timx_ic_cap_chy.ICSelection = TIM_ICSELECTION_DIRECTTI; /* 映射到TI1上*/
	timx_ic_cap_chy.ICPrescaler = TIM_ICPSC_DIV1; /* 配置输入分频,不分频*/
	timx_ic_cap_chy.ICFilter = 0; /* 配置输入滤波器,不滤波*/
	HAL_TIM_IC_ConfigChannel(&g_timx_cap_chy_handle, &timx_ic_cap_chy,
							GTIM_TIMX_CAP_CHY); /* 配置TIM5通道1 */
	__HAL_TIM_ENABLE_IT(&g_timx_cap_chy_handle, TIM_IT_UPDATE); /* 使能更新中断*/
	/* 使能通道输入以及使能捕获中断*/
	HAL_TIM_IC_Start_IT(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);
}

HAL_TIM_IC_Init 初始化定时器的基础工作参数,如:ARR 和PSC 等,第二部分则是调用
HAL_TIM_IC_ConfigChannel 函数配置输入捕获通道映射关系,滤波和分频等。最后是使能更新中断和使能通道输入以及定时器捕获中断。通道对应的IO、时钟开启和NVIC 的初始化都在HAL_TIM_IC_MspInit 函数里编写,其定义如下:

/**
* @brief 通用定时器输入捕获初始化接口
HAL库调用的接口,用于配置不同的输入捕获
* @param htim:定时器句柄
* @note 此函数会被HAL_TIM_IC_Init()调用
* @retval 无
*/
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
	if (htim->Instance == GTIM_TIMX_CAP) /*输入通道捕获*/
	{
		GPIO_InitTypeDef gpio_init_struct;
		GTIM_TIMX_CAP_CHY_CLK_ENABLE(); /* 使能TIMx时钟*/
		GTIM_TIMX_CAP_CHY_GPIO_CLK_ENABLE(); /* 开启捕获IO的时钟*/
		gpio_init_struct.Pin = GTIM_TIMX_CAP_CHY_GPIO_PIN;/* 输入捕获的GPIO口*/
		gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出*/
		gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉*/
		gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
		HAL_GPIO_Init(GTIM_TIMX_CAP_CHY_GPIO_PORT, &gpio_init_struct);
		HAL_NVIC_SetPriority(GTIM_TIMX_CAP_IRQn, 1, 3); /* 抢占1,子优先级3 */
		HAL_NVIC_EnableIRQ(GTIM_TIMX_CAP_IRQn); /* 开启ITMx中断*/
	}
}

该函数调用HAL_GPIO_Init 函数初始化定时器输入通道对应的IO,并且开启GPIO 时钟,
使能定时器。其中要注意IO 口复用功能的选择一定要选对了。最后配置中断抢占优先级和响应优先级,以及打开定时器中断。
通过上面的两个函数输入捕获的初始化就完成了,下面先来介绍两个变量。

/* 输入捕获状态(g_timxchy_cap_sta)
* [7] :0,没有成功的捕获;1,成功捕获到一次.
* [6] :0,还没捕获到高电平;1,已经捕获到高电平了.
* [5:0]:捕获高电平后溢出的次数,最多溢出63次,所以最长捕获值= 63*65536+65535 = 4194303
* 注意:为了通用,我们默认ARR和CCRy都是16位寄存器,对于32位的定时器(如:TIM5),
* 也只按16位使用
* 按1us的计数频率,最长溢出时间为:4194303 us, 约4.19秒
* (说明一下:正常32位定时器来说,1us计数器加1,溢出时间:4294秒)
*/
uint8_t g_timxchy_cap_sta = 0; /* 输入捕获状态*/
uint16_t g_timxchy_cap_val = 0; /* 输入捕获值*/

这两个变量用于辅助实现高电平捕获。其中g_timxchy_cap_sta,是用来记录捕获状态,(这个变量,我们把它当成一个寄存器那样来使用)。对其各位赋予状态含义,描述如下表所示:
在这里插入图片描述
变量g_timxchy_cap_sta 的位[5:0]是用于记录捕获高电平定时器溢出次数,总共6 位,所以
最多可以记录溢出的次数为2 的6 次方减一次,即63 次。
变量g_timxchy_cap_val,则用来记录捕获到下降沿的时候,TIM5_CNT 寄存器的值。
下面开始看中断服务函数的逻辑程序,HAL_TIM_IRQHandler 函数会调用下面两个回调函
数,我们的逻辑代码就是放在回调函数里,函数定义如下:

/**
 * @brief 定时器输入捕获中断处理回调函数
 * @param htim:定时器句柄指针
 * @note 该函数在HAL_TIM_IRQHandler中会被调用
 * @retval 无
 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
        if ((g_timxchy_cap_sta & 0X80) == 0) /* 还没成功捕获*/
        {
                if (g_timxchy_cap_sta & 0X40) /* 捕获到一个下降沿*/
                {
                        g_timxchy_cap_sta |= 0X80; /* 标记成功捕获到一次高电平脉宽*/
                        g_timxchy_cap_val = HAL_TIM_ReadCapturedValue(&g_timx_cap_chy_han-dler,
                                        GTIM_TIMX_CAP_CHY); /* 获取当前的捕获值*/
                        TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handler,
                                        GTIM_TIMX_CAP_CHY);/* 一定要先清除原来的设置*/
                        TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handler, GTIM_TIMX_CAP_CHY,
                                        TIM_ICPOLARITY_RISING);/* 配置TIM5通道1上升沿捕获*/
                }
                else /* 还未开始,第一次捕获上升沿*/
                {
                        g_timxchy_cap_sta = 0; /* 清空*/
                        g_timxchy_cap_val = 0;
                        g_timxchy_cap_sta |= 0X40; /* 标记捕获到了上升沿*/
                        __HAL_TIM_DISABLE(&g_timx_cap_chy_handler); /* 关闭定时器5 */
                        __HAL_TIM_SET_COUNTER(&g_timx_cap_chy_handler,0); /* 计数器清零*/
                        TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handler,
                                        GTIM_TIMX_CAP_CHY); /* 一定要先清除原来的设置!!*/
                        TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handler, GTIM_TIMX_CAP_CHY,
                                        TIM_ICPOLARITY_FALLING); /* 定时器5通道1设置为下降沿捕获*/
                        __HAL_TIM_ENABLE(&g_timx_cap_chy_handler); /* 使能定时器5 */
                }
        }
}

/**
 * @brief 定时器更新中断回调函数
 * @param htim:定时器句柄指针
 * @note 此函数会被定时器中断函数共同调用的
 * @retval 无
 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
        if (htim->Instance == GTIM_TIMX_CAP)
        {
                if ((g_timxchy_cap_sta & 0X80) == 0) /* 还没成功捕获*/
                {
                        if (g_timxchy_cap_sta & 0X40) /* 已经捕获到高电平了*/
                        {
                                if ((g_timxchy_cap_sta & 0X3F) == 0X3F) /* 高电平太长了*/
                                {
                                        TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handle,
                                                        GTIM_TIMX_CAP_CHY); /* 一定要先清除原来的设置*/
                                        /* 配置TIM5通道1上升沿捕获*/
                                        TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handle,
                                                        GTIM_TIMX_CAP_CHY, TIM_ICPOLARITY_RISING);
                                        g_timxchy_cap_sta |= 0X80; /* 标记成功捕获了一次*/
                                        g_timxchy_cap_val = 0XFFFF;
                                }
                                else /* 累计定时器溢出次数*/
                                {
                                        g_timxchy_cap_sta++;
                                }
                        }
                }
        }
}

现在我们来介绍一下,捕获高电平脉宽的思路:首先,设置TIM5_CH1 捕获上升沿,然后
等待上升沿中断到来,当捕获到上升沿中断,此时如果g_timxchy_cap_sta 的第6 位为0,则表示还没有捕获到新的上升沿,就先把g_timxchy_cap_sta、g_timxchy_cap_val 和TIM5_CNT 寄存器等清零,然后再设置g_timxchy_cap_sta 的第6 位为1,标记捕获到高电平,最后设置为下降沿捕获,等待下降沿到来。如果等待下降沿到来期间,定时器发生了溢出,就用g_timxchy_cap_sta变量对溢出次数进行计数,当最大溢出次数来到的时候,就强制标记捕获完成,并配置定时器通道上升沿捕获。当下降沿到来的时候,先设置g_timxchy_cap_sta 的第7 位为1,标记成功捕获一次高电平,然后读取此时的定时器值到g_timxchy_cap_val 里面,最后设置为上升沿捕获,回到初始状态。这样,我们就完成一次高电平捕获了,只要g_timxchy_cap_sta 的第7 位一直为1,那么就不会进行第二次捕获,我们在main 函数处理完捕获数据后,将g_timxchy_cap_sta 置零,就可以开启第二次捕获。
在main.c 里面编写如下代码:

int main(void)
{
	uint32_t temp = 0;
	uint8_t t = 0;
	HAL_Init(); /* 初始化HAL库*/
	sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72); /* 延时初始化*/
	usart_init(115200); /* 串口初始化为115200 */
	led_init(); /* 初始化LED */
	key_init(); /* 初始化按键*/
	gtim_timx_cap_chy_init(0XFFFF, 72 - 1); /* 以1Mhz的频率计数捕获*/
	while (1)
	{
		if (g_timxchy_cap_sta & 0X80) /* 成功捕获到了一次高电平*/
		{
			temp = g_timxchy_cap_sta & 0X3F;
			temp *= 65536; /* 溢出时间总和*/
			temp += g_timxchy_cap_val; /* 得到总的高电平时间*/
			printf("HIGH:%d us\r\n", temp); /* 打印总的高点平时间*/
			g_timxchy_cap_sta = 0; /* 开启下一次捕获*/
		}
		t++;
		if (t > 20) /* 200ms进入一次*/
		{
			t = 0;
			LED0_TOGGLE(); /* LED0闪烁,提示程序运行*/
		}
		delay_ms(10);
	}
}

先看gtim_timx_cap_chy_init(0XFFFF, 72 - 1)这个语句,这两个形参分别设置自动重载寄存
器的值为65535,以及预分频器寄存器的值为71。定时器5 是16 位的计数器,这里设置为最大值65535。预分频系数,我们设置为72 分频,定时器5 的时钟频率是2 倍的APB1 总线时钟频率,即72MHz,可以得到计数器的计数频率是1MHz,即1us 计数一次,所以我们的捕获时间精度是1us。这里可以知道定时器的溢出时间是65536us。
While(1)无限循环通过判断g_timxchy_cap_sta 的第7 位,来获知有没有成功捕获到一次
高电平,如果成功捕获,先计算总的高电平时间,再通过串口传输到电脑。

下载验证

下载代码后,可以看到LED0 在闪烁,说明程序已经正常在跑了,我们再打开串口调试助
手,选择对应的串口端口,我这边是COM15,然后按KEY_UP 按键,可以看到串口打印的高电平持续时间,如图21.4.4.1 所示:
正点原子STM32(基于HAL库)2_第189张图片

通用定时器脉冲计数实验(来一个脉冲计一次数)

前面我们介绍了通用定时器的四类时钟源,本小节我们来学习使用通用定时器的外部时钟
模式1 这类时钟源。

前面的三个通用定时器实验的时钟源都是来自内部时钟(CK_INT),本实验我们将使用外
部时钟模式1:外部输入引脚(TIx)作为定时器的时钟源。关于这个外部输入引脚(TIx),我们使用WK_UP 按键按下产生的高电平脉冲作为定时器的计数器时钟,每按下一次按键产生一次高电平脉冲,计数器加一。

下面通过框图给大家展示本实验用到定时器内部哪些资源,如下图所示:

正点原子STM32(基于HAL库)2_第190张图片

前面介绍过,外部时钟模式1 的外部输入引脚只能是通道1 或者通道2 对应的IO,通道3
或者通道4 是不可以的。以通道1 输入为例,外部时钟源信号通过通道1 输入后,接下来我们用TI1 表示该信号。TI1 分别要经过滤波器、边沿检测器后,来到TI1FP1,被触发输入选择器选择为触发源,接着来到从模式控制器。从模式选择为外部时钟模式1,这时候外部时钟源信号就会到达时基单元的预分频器,后面就是经过分频后就作为计数器的计数时钟了。这个过程的描述,大家亦可参考前面介绍外部时钟模式1 的描述。因为前面已经介绍过,这里只是简单讲一下。

如果大家想时钟源信号的上升沿和下降沿,计数器都计数,可以选择TI1F_ED 作为触发输
入选择器的触发源。

假设计数器工作在递增计数模式,那么每来一个选择的边沿,计数器就加一。最后,外部
时钟源信号的边沿计数个数会保存计数器寄存器中,我们只需要直接读取CNT 的值即可。这里是没有考虑定时器溢出的情况,如果定时器溢出还需要对溢出进行处理。比如开启更新中断,定时器溢出后,在更新中断里,对溢出次数进行记录,然后用溢出次数乘以溢出一次计数的个数,再加上CNT 现在的值,就可以得到总的计数个数了。在例程中,我们也是这样处理的。

下面开始讲解本实验用到的寄存器配置情况。

TIM2/TIM3/TIM4/TIM5 寄存器

通用定时器脉冲计数实验需要用到的寄存器有:TIMx_ARR、TIMx_PSC、TIMx_CCMR1、
TIMx_CCER、TIMx_DIER、TIMx_CR1、TIMx_EGR 这些寄存器在前面的章节都有提到,在这里只需针对性的介绍。
⚫ 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
该寄存器我们在PWM 输出实验时讲解了他作为输出功能的配置,在输入捕获实验学习了输入捕获模式的配置,本小节我们的外部信号也同样要作为输入信号给定时器作为时钟源,所
以我们要看输入捕获模式定时器对应功能。WK_UP 按键(PA0)对应着定时器2 的通道1,这个可以在数据手册《STM32F103ZET6(中文版).pdf》21 页找到。接下来我们开始配置
TIMx_CCMR1 寄存器,其描述如图21.5.1.1 所示:
正点原子STM32(基于HAL库)2_第191张图片

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

正点原子STM32(基于HAL库)2_第192张图片
我们要用到这个寄存器的最低2 位,CC1E 和CC1P 位。要使能输入捕获,必须设置CC1E=1,而CC1P 则根据自己的需要来配置。我们这里是保留默认设置值0,即高电平触发捕获。
⚫ 从模式控制寄存器(TIMx_ SMCR)
TIM2/TIM3/TIM4/TIM5 的从模式控制寄存器,该寄存器用于配置从模式,以及定时器的触
发源相关的设置。TIMx_SMCR 寄存器描述如图21.5.1.3 所示。
正点原子STM32(基于HAL库)2_第193张图片
因为我们要让外部引脚脉冲信号作为定时器的时钟源,所以位[2:0]我们设置的值是111,
即外部时钟模式1。位[6:4]是触发选择设置,TIMx_CH1 对应TI1FP1,TIMx_CH2 则对应TI2FP2,我们是定时器通道1,所以需要配置的值为101。ETF[3:0]和ETPS[1:0]分别是外部触发滤波器和外部触发预分频器,我们没有用到。
接下来我们再看看DMA/中断使能寄存器:TIMx_DIER,该寄存器的各位描述见图21.2.1.3
(在21.2.1 小节)。本实验,我们需要用到定时器更新中断,在中断服务函数中累加定时器溢出的次数,所以需要使能定时器的更新中断,即UIE 置1。
控制寄存器1:TIMx_CR1,我们只用到了它的最低位,也就是用来使能定时器的。

硬件设计

  1. 例程功能
    使用TIM2_CH1 做输入捕获,我们将捕获PA0 上的高电平脉宽,并将脉宽进行计数,通过
    串口打印出来。大家可以通过按WK_UP 按键,输入高电平脉冲,通过按KEY0 重设当前计数。
    LED0 闪烁,提示程序运行。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)独立按键:
    KEY0 –PE4
    WK_UP –PA0
    3)定时器2,使用TIM2 通道1,PA0 复用为TIM2_CH1。
  3. 原理图
    定时器属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们借助WK_UP
    做输入脉冲源,捕获PA0 上的高电平脉宽,然后对脉宽进行计数并通过串口上位机打印出来。还可以通过按KEY0 重设当前计数。

程序设计

定时器的HAL 库驱动

正点原子STM32(基于HAL库)2_第194张图片

定时器在HAL 库中的驱动代码在前面已经介绍了部分,这里我们针对定时器从模式介绍
HAL_TIM_SlaveConfigSynchro 函数。

  1. 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 枚举类型的值。

定时器从模式脉冲计数配置步骤

正点原子STM32(基于HAL库)2_第195张图片

1)开启TIMx 和输入通道的GPIO 时钟,配置该IO 口的复用功能输入
首先开启TIMx 的时钟,然后配置GPIO 为复用功能输出。本实验我们默认用到定时器2 通
道1,对应IO 是PA0,它们的时钟开启方法如下:

__HAL_RCC_TIM2_CLK_ENABLE(); /* 使能定时器2 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟*/

IO 口复用功能是通过函数HAL_GPIO_Init 来配置的。
2)初始化TIMx,设置TIMx 的ARR 和PSC 等参数
使用定时器的输入捕获功能时,我们调用的是HAL_TIM_IC_Init 函数来初始化定时器ARR
和PSC 等参数。
注意:该函数会调用:HAL_TIM_IC_MspInit 函数,我们可以通过后者存放定时器和GPIO
时钟使能、GPIO 初始化、中断使能和优先级设置等代码。
3)选择从模式:外部触发模式1
TIMx_SMCR 寄存器控制着定时器的从模式,包括模式选择,触发源选择、极性和输入预
分频等。这里我们需要设置外部时钟模式1,定时器输入1 (TI1FP1),并且不使用滤波器(提高响应精度),也不使用分频。HAL 库是通过HAL_TIM_SlaveConfigSynchro 函数来初始化定时器从模式配置参数的。
4)设置定时器为输入捕获模式,开启输入捕获
在HAL 库中,定时器的输入捕获模式是通过HAL_TIM_IC_ConfigChannel 函数来设置定
时器某个通道为输入捕获通道,包括映射关系,输入滤波和输入分频等。
5)使能定时器更新中断,开启捕获功能,配置定时器中断优先级
通过__HAL_TIM_ENABLE_IT 函数使能定时器更新中断。
通过HAL_TIM_IC_Start 函数使能定时器并开启捕获功能。
通过HAL_NVIC_EnableIRQ 函数使能定时器中断。
通过HAL_NVIC_SetPriority 函数设置中断优先级。
6)编写中断服务函数
定时器中断服务函数为:TIMx_IRQHandler 等,当发生中断的时候,程序就会执行中断服
务函数。HAL 库提供了一个定时器中断公共处理函数HAL_TIM_IRQHandler,该函数会根据中断类型调用相关的中断回调函数。用户根据自己的需要重定义这些中断回调函数来处理中断程序。本实验我们不使用HAL 库的中断回调机制,而是把中断程序写在定时器中断服务函数里。
详见本实验例程源码。

课堂源码

正点原子STM32(基于HAL库)2_第196张图片

在这里插入图片描述
正点原子STM32(基于HAL库)2_第197张图片
gtim.h

#ifndef __GTIM_H
#define __GTIM_H

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


void gtim_timx_cnt_chy_init(uint16_t psc);

#endif

gtim.c

#include "./BSP/TIMER/gtim.h"


TIM_HandleTypeDef g_timx_cnt_chy_handle;        /* 定时器x句柄 */

 /* 通用定时器通道y 脉冲计数 初始化函数 */
void gtim_timx_cnt_chy_init(uint16_t psc)
{
    TIM_SlaveConfigTypeDef tim_salve_config = {0};
    
    g_timx_cnt_chy_handle.Instance = TIM2;//定时器2
    g_timx_cnt_chy_handle.Init.Prescaler = psc;
    g_timx_cnt_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;
    g_timx_cnt_chy_handle.Init.Period = 65535;
    HAL_TIM_IC_Init(&g_timx_cnt_chy_handle);
    
    tim_salve_config.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;//外部通道1
    tim_salve_config.InputTrigger = TIM_TS_TI1F_ED;		 //通道1单边沿
    tim_salve_config.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;//极性:上升沿
    tim_salve_config.TriggerFilter = 0;//滤波器
    HAL_TIM_SlaveConfigSynchro(&g_timx_cnt_chy_handle, &tim_salve_config);

    HAL_TIM_IC_Start(&g_timx_cnt_chy_handle, TIM_CHANNEL_1);
}

/* 定时器 输入捕获 MSP初始化函数 */
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM2)
    {
        GPIO_InitTypeDef gpio_init_struct;
        __HAL_RCC_GPIOA_CLK_ENABLE();
        __HAL_RCC_TIM2_CLK_ENABLE();

        gpio_init_struct.Pin = GPIO_PIN_0;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;        /* 推挽式复用功能(开漏也可以) */
        gpio_init_struct.Pull = GPIO_PULLDOWN;          /* 下拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;  /* 高速 */
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);
        //没有用到中断,所以就没有NVIC相关初始化代码
    }
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/KEY/key.h"
#include "./BSP/TIMER/gtim.h"


extern TIM_HandleTypeDef g_timx_cnt_chy_handle;        /* 定时器x句柄 */

int main(void)
{
    uint16_t curcnt;
    uint16_t oldcnt;
    uint8_t key;
    uint8_t t = 0;
    
    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟, 72Mhz */
    delay_init(72);                             /* 延时初始化 */
    usart_init(115200);                         /* 串口初始化为115200 */
    led_init();                                 /* 初始化LED */
    key_init();                                 /* 初始化按键 */
    
    gtim_timx_cnt_chy_init(0);

    while (1)
    {
        key = key_scan(0);//不连续扫描
        if(key == KEY0_PRES)//按键0按下
        {
            __HAL_TIM_SET_COUNTER(&g_timx_cnt_chy_handle, 0);//计数器的值清零
        }
        
        
        curcnt = __HAL_TIM_GET_COUNTER(&g_timx_cnt_chy_handle);//获取当前计数值
        if(oldcnt != curcnt)
        {
            oldcnt = curcnt;
            printf("CNT:%d\r\n", oldcnt);
        }
        
        t++;
        
        if(t > 20)//灯显指示程序在跑 
        {
            t = 0;
            LED0_TOGGLE();
        }
        delay_ms(10);
    }
}

正点原子STM32(基于HAL库)2_第198张图片

程序流程图
正点原子STM32(基于HAL库)2_第199张图片

程序解析

这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。通用定时器驱动
源码包括两个文件:gtim.c 和gtim.h。
首先看gtim.h 头文件的几个宏定义:

/* TIMX 输入计数定义
* 这里的输入计数使用定时器TIM2_CH1,捕获WK_UP按键的输入
* 默认是针对TIM2~TIM5, 只有CH1和CH2通道可以用做输入计数, CH3/CH4不支持!
* 注意: 通过修改这几个宏定义,可以支持TIM1~TIM8任意一个定时器,CH1/CH2对应IO口做输入计数
* 特别要注意:默认用的PA0,设置的是下拉输入!如果改其他IO,对应的上下拉方式也得改!
*/
#define GTIM_TIMX_CNT_CHY_GPIO_PORT GPIOA
#define GTIM_TIMX_CNT_CHY_GPIO_PIN GPIO_PIN_0
#define GTIM_TIMX_CNT_CHY_GPIO_CLK_ENABLE() \
					do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能*/

#define GTIM_TIMX_CNT TIM2
#define GTIM_TIMX_CNT_IRQn TIM2_IRQn
#define GTIM_TIMX_CNT_IRQHandler TIM2_IRQHandler
#define GTIM_TIMX_CNT_CHY TIM_CHANNEL_1 /* 通道Y, 1<= Y <=2 */
#define GTIM_TIMX_CNT_CHY_CLK_ENABLE() \
					do{ __HAL_RCC_TIM2_CLK_ENABLE(); }while(0) /* TIM2 时钟使能*/

可以把上面的宏定义分成两部分,第一部分是定时器2 输入通道1 对应的IO 口的宏定义,
第二部分则是定时器2 输入通道1 的相应宏定义。需要注意的点是:只有CH1 和CH2 通道可
以用做输入计数,CH3/CH4 不支持!

下面看gtim.c 的程序,首先是通用定时器脉冲计数初始化函数,其定义如下:

/**
* @brief 通用定时器TIMX 通道Y 脉冲计数初始化函数
* @note
* 本函数选择通用定时器的时钟选择: 外部时钟源模式1(SMS[2:0] = 111)
* CNT的计数时钟源就来自TIMX_CH1/CH2, 可以实现外部脉冲计数(脉冲接入CH1/CH2)
*
* 时钟分频数= psc, 一般设置为0, 表示每一个时钟都会计数一次, 以提高精度.
* 通过读取CNT和溢出次数, 经过简单计算, 可以得到当前的计数值, 从而实现脉冲计数
* @param arr: 自动重装值
* @retval 无
*/
void gtim_timx_cnt_chy_init(uint16_t psc)
{
	GPIO_InitTypeDef gpio_init_struct;
	TIM_SlaveConfigTypeDef tim_slave_config = {0};
	GTIM_TIMX_CNT_CHY_CLK_ENABLE(); /* 使能TIMx时钟*/
	GTIM_TIMX_CNT_CHY_GPIO_CLK_ENABLE(); /* 开启GPIOA时钟*/
	
	g_timx_cnt_chy_handle.Instance = GTIM_TIMX_CNT; /* 定时器x */
	g_timx_cnt_chy_handle.Init.Prescaler = psc; /* 定时器分频*/
	g_timx_cnt_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 递增计数模式*/
	g_timx_cnt_chy_handle.Init.Period = 65535; /* 自动重装载值*/
	HAL_TIM_IC_Init(&g_timx_cnt_chy_handle);
	
	gpio_init_struct.Pin = GTIM_TIMX_CNT_CHY_GPIO_PIN; /* 输入捕获的GPIO口*/
	gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出*/
	gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉*/
	gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
	HAL_GPIO_Init(GTIM_TIMX_CNT_CHY_GPIO_PORT, &gpio_init_struct);
	
	/* 从模式:外部触发模式1 */
	tim_slave_config.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;/*从模式:外部触发模式1*/
	tim_slave_config.InputTrigger = TIM_TS_TI1FP1; /* TI1FP1作为触发输入源*/
	tim_slave_config.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;/* 上升沿*/
	tim_slave_config.TriggerPrescaler = TIM_TRIGGERPRESCALER_DIV1;/* 不分频*/
	tim_slave_config.TriggerFilter = 0x0; /* 滤波:本例中不需要任何滤波*/
	HAL_TIM_SlaveConfigSynchro(&g_timx_cnt_chy_handle, &tim_slave_config);
	
	/* 设置中断优先级,抢占优先级1,子优先级3 */
	HAL_NVIC_SetPriority(GTIM_TIMX_CNT_IRQn, 1, 3);
	HAL_NVIC_EnableIRQ(GTIM_TIMX_CNT_IRQn);
	
	__HAL_TIM_ENABLE_IT(&g_timx_cnt_chy_handle, TIM_IT_UPDATE); /* 使能更新中断*/
	HAL_TIM_IC_Start(&g_timx_cnt_chy_handle, GTIM_TIMX_CNT_CHY);/* 使能通道输入*/
}

gtim_timx_cnt_chy_init 函数包含了输入通道对应IO 的初始代码、NVIC、使能时钟、定时
器基础工作参数和从模式配置的所有代码。下面来看看该函数的代码内容。
第一部分使能定时器和GPIO 的时钟。
第二部分调用HAL_TIM_IC_Init 函数初始化定时器的基础工作参数,如:ARR 和PSC 等。
第三部分是定时器输入通道对应的IO 的初始化。
第四部分调用HAL_TIM_SlaveConfigSynchronization 函数配置从模式选择、输入捕获通道
映射关系、捕获边沿和滤波等。
第五部分是NVIC 的初始化,配置抢占优先级、响应优先级和开启NVIC 定时器中断。
最后是使能更新中断和使能通道输入。
我们在通用定时器输入捕获实验使用了HAL_TIM_IC_MspInit 函数,为了方便代码的管理
和移植性等,这里就不再使用这个函数了。当一个项目使用到多个定时器时,也同样建议大家不要使用HAL 库的回调机制,真的不方便。至于前面为什么要用HAL_TIM_IC_MspInit 函数,只是为了让大家知道HAL 库回调机制的使用方法。大家可以根据自己的情况权衡利弊,决定是否使用HAL 库的回调机制。

下面先看中断服务函数,在基本定时器中断实验中,我们知道中断逻辑程序的逻辑代码是
放在更新中断回调函数里面的,这是HAL 库回调机制标准的做法。因为我们在通用定时器输入捕获实验中使用过HAL_TIM_PeriodElapsedCallback 更新中断回调函数,所以本实验我们不使用HAL 库这套回调机制,而是直接将中断处理写在定时器中断服务函数中,定时器中断服务函数定义如下:

/**
* @brief 通用定时器TIMX 脉冲计数更新中断服务函数
* @param 无
* @retval 无
*/
void GTIM_TIMX_CNT_IRQHandler(void)
{
	/* 以下代码没有使用定时器HAL库共用处理函数来处理,而是直接通过判断中断标志位的方式*/
	if(__HAL_TIM_GET_FLAG(&g_timx_cnt_chy_handle, TIM_FLAG_UPDATE) != RESET)
	{
		g_timxchy_cnt_ofcnt++; /* 累计溢出次数*/
	}
	__HAL_TIM_CLEAR_IT(&g_timx_cnt_chy_handle, TIM_IT_UPDATE);
}

在函数中,使用__HAL_TIM_GET_FLAG 函数宏获取更新更新中断标志位,然后判断是否
发生更新中断,如果发生了更新中断,表示脉冲计数的个数等于ARR 寄存器的值,那么我们让g_timxchy_cnt_ofcnt 变量++,累计定时器溢出次数。最后调用__HAL_TIM_CLEAR_IT 函数宏清除更新中断标志位。这样就完成一次对更新中断的处理。
再来介绍两个自定义的功能函数,第一个是获取当前计数值函数,其定义如下:

/**
* @brief 通用定时器TIMX 通道Y 获取当前计数值
* @param 无
* @retval 当前计数值
*/
uint32_t gtim_timx_cnt_chy_get_count(void)
{
	uint32_t count = 0;
	count = g_timxchy_cnt_ofcnt * 65536;/* 计算溢出次数对应的计数值*/
	count += __HAL_TIM_GET_COUNTER(&g_timx_cnt_chy_handler);/* 加上当前CNT的值*/
	return count;
}

该函数先是计算定时器溢出次数对应的计数个数,因为定时器每溢出一次的计数个数是
65536,所以这里用g_timxchy_cnt_ofcnt 乘以65536,就可以得到溢出计数的个数,前提是ARR寄存器的值得设置为65535(前面课堂源码只能计数到65536,溢出后就清零了)。然后再加上定时器计数器当前的值,计数器当前的值通过调用__HAL_TIM_GET_COUNTER 函数宏可以获取。函数返回值是脉冲计数的总个数。
第二个自定义功能函数是重启计数器函数,其定义如下:

/**
* @brief 通用定时器TIMX 通道Y 重启计数器
* @param 无
* @retval 当前计数值
*/
void gtim_timx_cnt_chy_restart(void)
{
	__HAL_TIM_DISABLE(&g_timx_cnt_chy_handler); /* 关闭定时器TIMX */
	g_timxchy_cnt_ofcnt = 0; /* 累加器清零*/
	__HAL_TIM_SET_COUNTER(&g_timx_cnt_chy_handler, 0); /* 计数器清零*/
	__HAL_TIM_ENABLE(&g_timx_cnt_chy_handler); /* 使能定时器TIMX */
}

该函数先关闭定时器,然后做清零操作,包括:记录溢出次数全局变量g_timxchy_cnt_of cnt和定时器计数器的值,最后使能定时器重新计数。通用定时器脉冲计数实验的整体驱动和逻辑程序还算比较容易理解。
下面看看main.c 里面编写的代码:

int main(void)
{
        uint32_t curcnt = 0;
        uint32_t oldcnt = 0;
        uint8_t key = 0;
        uint8_t t = 0;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        led_init(); /* 初始化LED */
        key_init(); /* 初始化按键*/
        gtim_timx_cnt_chy_init(0); /* 定时器计数初始化, 不分频*/
        gtim_timx_cnt_chy_restart(); /* 重启计数*/
        while (1)
        {
                key = key_scan(0); /* 扫描按键*/
                if (key == KEY0_PRES) /* KEY0按键按下,重启计数*/
                {
                        gtim_timx_cnt_chy_restart(); /* 重新启动计数*/
                }
                curcnt = gtim_timx_cnt_chy_get_count(); /* 获取计数值*/
                if (oldcnt != curcnt)
                {
                        oldcnt = curcnt;
                        printf("CNT:%d\r\n", oldcnt); /* 打印脉冲个数*/
                }
                t++;
                if (t > 20) /* 200ms进入一次*/
                {
                        t = 0;
                        LED0_TOGGLE(); /* LED0闪烁,提示程序运行*/
                }
                delay_ms(10);
        }
}

调用定时器初始化函数gtim_timx_cnt_chy_init(0),形参是0,表示设置预分频器寄存器的
值为0,即表示不分频。如果形参设置为1,就是2 分频,这种情况,要按按键WK_UP 两次才会计数一次,大家不妨试试。该函数内部已经设置自动重载寄存器的值为65535,所以在不分频的情况下,定时器发生一次更新中断,表示脉冲计数了65536 次。

下载验证

下载代码后,可以看到LED0 在闪烁,说明程序已经正常在跑了,我们再打开串口调试助
手,然后每按KEY_UP 按键一次,就可以看到串口打印的高电平脉冲次数。如果按KEY0 按
键,就会重设当前计数,从0 开始计数,如图21.5.4.1 所示:
正点原子STM32(基于HAL库)2_第200张图片

你可能感兴趣的:(STM32,stm32,单片机,嵌入式硬件)