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

Posted 行稳方能走远

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了正点原子STM32(基于HAL库)2相关的知识,希望对你有一定的参考价值。

目录

跑马灯实验(IO输出)

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

STM32F1 GPIO 简介




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 有八种工作模式,具体这些模式是怎么实现的?下面我们通过GPIO 的
基本结构图来分别进行详细分析,先看看总的框图,如图13.1.2.1 所示。

上图中片上外设比如串口

如上图所示,可以看到右边只有I/O 引脚,这个I/O 引脚就是我们可以看到的芯片实物的引脚,其他部分都是GPIO 的内部结构。
①保护二极管
保护二极管共有两个,用于保护引脚外部过高或过低的电压输入。当引脚输入电压高于
VDD 时,上面的二极管导通,当引脚输入电压低于VSS 时,下面的二极管导通,从而使输入芯片内部的电压处于比较稳定的值(二极管压降)。虽然有二极管的保护,但这样的保护却很有限,大电压大电流的接入很容易烧坏芯片。所以在实际的设计中我们要考虑设计引脚的保护电路(引脚外接电阻)。
②上拉、下拉电阻
它们阻值大概在30~50K欧之间,可以通过上、下两个对应的开关控制,这两个开关由寄
存器控制。当引脚外部的器件没有干扰引脚的电压时,即没有外部的上、下拉电压,引脚的电平由引脚内部上、下拉决定,开启内部上拉电阻工作,引脚电平为高,开启内部下拉电阻工作,则引脚电平为低。同样,如果内部上、下拉电阻都不开启,这种情况就是我们所说的浮空模式。浮空模式下,引脚的电平是不可确定的。引脚的电平可以由外部的上、下拉电平决定。需要注意的是,STM32 的内部上拉是一种“弱上拉”,这样的上拉电流很弱,如果有要求大电流还是得外部上拉。
③施密特(肖特基)触发器
对于标准施密特触发器,当输入电压高于正向阈值电压,输出为高;当输入电压低于负向
阈值电压,输出为低;当输入在正负向阈值电压之间,输出不改变,也就是说输出由高电准位翻转为低电准位,或是由低电准位翻转为高电准位对应的阈值电压是不同的。只有当输入电压发生足够的变化时,输出才会变化,因此将这种元件命名为触发器。这种双阈值动作被称为迟滞现象,表明施密特触发器有记忆性。从本质上来说,施密特触发器是一种双稳态多谐振荡器。

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

下面看看比较器跟施密特触发器的作用的比较,就清楚的知道施密特触发器对外部输入信
号具有一定抗干扰能力,如图13.1.2.2 所示。

比较器的(A)和施密特触发器(B)作用比较

④P-MOS 管和N-MOS 管

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

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

1、输入浮空

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

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

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

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

5、开漏输出
开漏输出模式:STM32 的开漏输出模式是数字电路输出的一种,从结果上看它只能输出低
电平Vss 或者高阻态,常用于IIC 通讯(IIC_SDA)或其它需要进行电平转换的场景。根据
《STM32F10xxx 参考手册_V10(中文版).pdf》第108 页关于“GPIO 输出配置”的描述,我
们可以知道开漏模式下,IO 是这样工作的:

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

根据参考手册的描述,同时为了方便大家理解,我们在“输出控制”部分做了等效处理,
如图13.1.2.7 所示。图13.1.2.7 中写入输出数据寄存器①的值怎么对应到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 引脚状态。


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,但是芯片的总电流有限,所以并不建议这样用,最好还
是使用芯片外部的电源。


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

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

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

GPIO 寄存器介绍

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 所示。


每组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)

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

该寄存器低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 所示。

为什么有了ODR 寄存器,还要这个BSRR 寄存器呢

我们先看看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 寄存器有被中断打断的风险。

通用外设驱动步骤

GPIO外设配置步骤

硬件设计

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

程序设计

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

GPIO的HAL库驱动函数

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 的亮灭了。

程序流程图

程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很
好的主导作用。本实验的程序流程图如下:

课堂源码

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_F

以上是关于正点原子STM32(基于HAL库)2的主要内容,如果未能解决你的问题,请参考以下文章

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

正点原子STM32(基于HAL库)

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

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

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

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