一、HAL库与STD库对比
之前使用STM32系列芯片都是基于STD(Standard Peripheral Libraries)标准外设库,标准外设库将相关寄存器以结构体的形式组织起来,使用库函数操作外设有点轻度面向对象编程的感觉,比寄存器操作方便不少。但在不同芯片上使用标准外设库开发的程序可移植性比较差,换个芯片常需要重新做大量的开发工作,为了解决这个问题,ST推出了全新的HAL(Hardware Abstraction Layer)库。
HAL库即硬件抽象层库,相比标准外设库提供了更高层次的抽象,更类似于面向对象编程的风格,将一个外设抽象组织为一个句柄,对该句柄的操作也就相当于对该外设的操作,在不同芯片上使用HAL库开发的程序更方便移植,能大幅减少重复的开发任务。更高层的抽象意味着更低的效率,对于实时性和效率要求更高的场合,ST也提供了LL(Low Layer)库可以达到标准外设库的效率并兼容HAL API,HAL与LL将称为ST主推的库。
HAL库与STD库两者相互独立,互不兼容,几种库的对比如下(参考自博客:STM32 之一 HAL库、标准外设库、LL库):
ST为新的标准库HAL和LL注册了一个新商标STMCube™,ST专门为其开发了配套的桌面软件STMCubeMX,开发者可以直接使用该软件进行可视化配置,大大节省开发时间。STMCubeMX的层级框架如下图所示:
从上图不难看出,LL库和HAL库两者相互独立,只不过LL库更底层。而且,部分HAL库会调用LL库(例如:USB驱动)。同样,LL库也会调用HAL库。
HAL库可以更好的确保跨STM32产品的最大可移植性,同时提供了一整套一致的中间件组件,如RTOS,USB,TCP / IP和图形等。LL库与HAL捆绑发布,文档也是和HAL文档在一起的,但可完全抛开HAL库独立使用,也可与HAL库结合使用,在使用STM32CubeMX生成项目时,可以选择LL库(默认选择HAL库)。
二、HAL库详解
2.1 HAL库结构
说到STM32的HAL库,就不得不提STM32CubeMX,其作为一个可视化的配置工具,对于开发者来说,确实大大节省了开发时间。STM32CubeMX就是以HAL库为基础的,且目前仅支持HAL库及LL库!
下面以STM32Cube_FW_L4_V1.14.0(下载地址:https://www.st.com/content/st_com/en/products/embedded-software/mcu-mpu-embedded-software/stm32-embedded-software/stm32cube-mcu-mpu-packages/stm32cubel4.html)为例,官方给出的STM32CubeL4固件包组件体系如下图所示:
STM32CubeL4固件包的文件结构如下图所示:
STM32CubeL4固件包中HAL库的文件包含结构如下图所示(借用了正点原子的图,可以忽略下图中的sys.h文件):
下面以STM32CubeL4库和STM32L475VET6芯片(基于正点原子的STM32L4潘多拉开发板)为例简单介绍下各个头文件作用,首先是HAL库的主要源码文件如下表所示:
- stm32l4xx.h:是所有stm32l4系列的顶层头文件,主要包含STM32同系列芯片的不同具体型号的定义,是否使用HAL库等的宏定义,其会根据定义的芯片型号包含具体芯片型号的头文件,比如stm32l475xx.h(主要提供STM32L475芯片的寄存器定义声明以及封装内存操作);
- system_stm32l4xx.c/h:主要声明和定义了系统初始化函数SystemInit以及时钟更新函数SystemCoreClockUpdate:SystemInit函数的作用是进行时钟系统的一些初始化操作以及中断向量表偏移地址设置,并没有设置具体的时钟值,这是与标准库的最大区别,使用标准库时该函数会帮我们设置好系统时钟配置相关的寄存器,使用HAL库时需要我们自己配置具体的时钟值;SystemCoreClockUpdate函数可以在系统时钟配置修改后被调用,以更新全局变量SystemCoreClock;
- stm32l4xx_it.c/h :主要是一些中断服务函数的定义和声明;
- stm32l4xx_hal_msp.c:msp全称为MCU Support Package,主要实现了HAL_MspInit和HAL_MspDeInit函数的定义,HAL_MspInit实现MCU级别的硬件初始化配置,其会被上一层的初始化函数HAL_Init和HAL_DeInit所调用,这种机制可以把MCU相关的硬件初始化剥离出来,方便用户代码在不同型号的MCU上移植;
- startup_stm32l475xx.s:STM32L475芯片的启动文件,主要作用是进行堆栈的初始化、中断向量表以及中断函数的定义等,启动过程可以参考我的博客:实时操作系统RTOS(六)—系统启动与固件移植。
从上面的文件包含关系图可以看出,顶层头文件stm32l4xx.h直接或间接包含了其他所有HAL库必要的头文件,所以在我们的用户代码中,只需要包含顶层头文件stm32l4xx.h即可。
在HAL库中很多回调函数前使用了__weak修饰符,可以称之为“弱函数”,加上该修饰符用户可以在自己的代码文件中重定义一个同名函数,最终编译的时候会选择用户定义的函数,如果用户没有重定义该函数则编译器会执行__weak声明的函数而不会报错,可以类比下C++中虚函数的概念。
2.2 HAL库API
从上面HAL库的结构看,供用户调用的API函数定义与声明主要在stm32l4xx_hal_ppp.c/h文件中,其中stm32l4xx_hal.c/h主要定义和声明了通用API函数,stm32l4xx_hal_ppp.c/h则定义和声明了基本外设ppp的操作API,stm32l4xx_hal_ppp_ex.c/h则定义和声明了扩展外设ppp的操作API。打开几个头文件看一下里面的API函数声明,大致可以分为下面几类:
- 初始化/反初始化函数: HAL_PPP_Init() / DeInit(), HAL_PPP_MspInit() / MspDeInit();
- I / O 操作函数: HAL_PPP_Read() / Write(), HAL_PPP_Transmit() / Receive();
- 控制函数: HAL_PPP_SetConfig(), HAL_PPP_GetConfig();
- 回调函数:HAL_PPP_ProcessCpltCallback(), HAL_PPP_ErrorCallback;
- 状态和错误: HAL_PPP_GetState (), HAL_PPP_GetError ()。
HAL库最大的特点就是对底层进行了抽象。在此结构下,用户代码的处理主要分为三部分:
- 处理外设句柄:HAL库对每个外设抽象成了一个ppp_HandleTypeDef的结构体,ppp外设的所有API函数都是对ppp_HandleTypeDef结构体实例化变量的操作,每个外设/模块实例资源相互独立且都有自己的句柄,用户根据自己需求对相关外设句柄进行处理;
- 处理MSP:用来初始化底层MCU级相关的设备,如GPIOx, Clock, DMA, Interrupt等,该部分也是在不同MCU间移植时需要重新实现的部分;
- 处理各种回调函数:当外设或者DMA工作完成后触发中断,该回调函数会在外设中断处理函数或者DMA的中断处理函数中被调用,可以在相应的回调函数中实现用户的控制逻辑。
2.3 HAL库移植使用
HAL库移植使用时主要需以下步骤(参考自博客:STM32 之二 HAL库详解 及 手动移植):
- 复制stm32f2xx_hal_msp_template.c,参照该模板,依次实现用到的外设的HAL_PPP_MspInit()和 HAL_PPP_MspDeInit;
- 复制stm32f2xx_hal_conf_template.h,用户可以在此文件中自由裁剪,配置HAL库;
- 在使用HAL库时,必须先调用函数HAL_StatusTypeDef HAL_Init(void),该函数在stm32f2xx_hal.c中定义,也就意味着第一点中,必须首先实现HAL_MspInit(void)和HAL_MspDeInit(void);
- HAL库与STD库不同,HAL库使用RCC中的函数来配置系统时钟,用户需要单独编写并调用时钟配置函数SystemClock_Config(void)(STD库默认在system_stm32f2xx.c中);
- 关于中断,HAL提供了中断处理函数,只需要调用HAL提供的中断处理函数,用户自己的代码,不建议先写到中断中,而应该写到HAL提供的回调函数中;
- 对于每一个外设,HAL都提供了回调函数,回调函数用来实现用户自己的代码,整个调用结构由HAL库自己完成。例如:UART中HAL提供了void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)函数,用户只需要触发中断后调用该函数即可,自己的代码写在对应的回调函数中,这些回调函数的声明如下(回调函数均实现为__weak弱函数,用户直接按声明重写即可):
// Drivers\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_uart.h
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);
三、CubeMX使用
下面以LED灯的控制为例为例,展示下CubeMX可视化配置工具的使用,以及基于HAL库的基本开发流程,案例中使用的软硬件资源如下:
工具的安装就略去了,需要提醒一点的是STM32CubeMX的运行需要JRE(Java Runtime Environment)的支持。
3.1 GPIO / AFIO配置
CubeMX提供了可视化配置操作,可以通过图形菜单配置GPIO引脚、外设、时钟、中断等;HAL库提供更高层次的抽象可以让我们专注实现用户业务逻辑,减少对寄存器相关的操作。但我们仍需要对各个外设的原理特性有较深的了解,这样才能清楚在CubeMX中如何配置,也才能更有效的利用外设为自己服务。
下面先看看GPIO / AFIO端口位的基本结构(参考自《SMT32L475VE Reference manual.pdf》):
从上图来看,每个I / O端口可以自由编程,通过寄存器配置可实现多种输入、输出、复用模式,比如浮空输入、上拉输入、下拉输入、模拟输入、复用功能输入、开漏输出、推挽输出、复用功能输出等,具体的GPIO / AFIO端口位配置表如下所示:
下面简单介绍下各种模式的特点:
- GP(General Purpose):作为普通的输入 / 输出引脚使用,比如可以驱动LED、产生PWM、驱动蜂鸣器等,也可以接收来自外部的中断信号或事件;
- PP(Push Pull)Output:推挽结构一般是指两个三极管分别受两互补信号的控制,总是在一个三极管导通的时候另一个截止,可以由 IC控制输出高、低电平;
- OD(Open Drain) Output:输出端相当于三极管的集电极,要得到高电平状态需要上拉电阻才行,适合于做电流型的驱动,其吸收电流的能力相对强(一般 20ma 以内);
- AF(Alternate Function):作为内置外设的输入 / 输出引脚使用,比如AF_PP复用推挽输出常用于USART / SPI外设,AF_OU复用开漏输出常用于IIC外设;
- PU(Pull Up):内部接通上拉电阻,将不确定的信号通过一个电阻嵌位在高电平VDD;
- PD(Pull Down):内部接通下拉电阻,将不确定的信号通过一个电阻嵌位在低电平VSS;
- Floating Input:浮空输入(内部上拉、下拉电阻均不接通),比如用作外部按键输入;
- Analog Input:应用ADC模拟输入,或者低功耗下省电;
下面是推挽输出与开漏输出的原理对比,正如前面介绍的:推挽输出可以由芯片IC控制端口输出高、低电平,既可以向负载灌电流又可以从负载抽取电流;开漏输出若没有外接上拉电阻只能输出低电平,若外接上拉电阻则可以通过改变外接上拉电源的电压来改变输出电平大小,适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内):
每个端口的配置除了输入/输出模式、普通/复用模式外,还有一个重要参数即传输速度,从源码中看到GPIO结构体定义如下:
// Drivers\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_gpio.h
/**
* @brief GPIO Init structure definition
*/
typedef struct
{
uint32_t Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins */
uint32_t Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIO_mode */
uint32_t Pull; /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.
This parameter can be a value of @ref GPIO_pull */
uint32_t Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIO_speed */
uint32_t Alternate; /*!< Peripheral to be connected to the selected pins
This parameter can be a value of @ref GPIOEx_Alternate_function_selection */
}GPIO_InitTypeDef;
/** @defgroup GPIO_speed GPIO speed
* @brief GPIO Output Maximum frequency
*/
#define GPIO_SPEED_FREQ_LOW (0x00000000u) /*!< range up to 5 MHz, please refer to the product datasheet */
#define GPIO_SPEED_FREQ_MEDIUM (0x00000001u) /*!< range 5 MHz to 25 MHz, please refer to the product datasheet */
#define GPIO_SPEED_FREQ_HIGH (0x00000002u) /*!< range 25 MHz to 50 MHz, please refer to the product datasheet */
#define GPIO_SPEED_FREQ_VERY_HIGH (0x00000003u) /*!< range 50 MHz to 80 MHz, please refer to the product datasheet */
/** @defgroup GPIO_pull GPIO pull
* @brief GPIO Pull-Up or Pull-Down Activation
*/
#define GPIO_NOPULL (0x00000000u) /*!< No Pull-up or Pull-down activation */
#define GPIO_PULLUP (0x00000001u) /*!< Pull-up activation */
#define GPIO_PULLDOWN (0x00000002u) /*!< Pull-down activation */
/** @defgroup GPIO_mode GPIO mode
* @brief GPIO Configuration Mode
* Elements values convention: 0xX0yz00YZ
* - X : GPIO mode or EXTI Mode
* - y : External IT or Event trigger detection
* - z : IO configuration on External IT or Event
* - Y : Output type (Push Pull or Open Drain)
* - Z : IO Direction mode (Input, Output, Alternate or Analog)
*/
#define GPIO_MODE_INPUT (0x00000000u) /*!< Input Floating Mode */
#define GPIO_MODE_OUTPUT_PP (0x00000001u) /*!< Output Push Pull Mode */
#define GPIO_MODE_OUTPUT_OD (0x00000011u) /*!< Output Open Drain Mode */
#define GPIO_MODE_AF_PP (0x00000002u) /*!< Alternate Function Push Pull Mode */
#define GPIO_MODE_AF_OD (0x00000012u) /*!< Alternate Function Open Drain Mode */
#define GPIO_MODE_ANALOG (0x00000003u) /*!< Analog Mode */
#define GPIO_MODE_ANALOG_ADC_CONTROL (0x0000000Bu) /*!< Analog Mode for ADC conversion */
#define GPIO_MODE_IT_RISING (0x10110000u) /*!< External Interrupt Mode with Rising edge trigger detection */
#define GPIO_MODE_IT_FALLING (0x10210000u) /*!< External Interrupt Mode with Falling edge trigger detection */
#define GPIO_MODE_IT_RISING_FALLING (0x10310000u) /*!< External Interrupt Mode with Rising/Falling edge trigger detection */
#define GPIO_MODE_EVT_RISING (0x10120000u) /*!< External Event Mode with Rising edge trigger detection */
#define GPIO_MODE_EVT_FALLING (0x10220000u) /*!< External Event Mode with Falling edge trigger detection */
#define GPIO_MODE_EVT_RISING_FALLING (0x10320000u) /*!< External Event Mode with Rising/Falling edge trigger detection */
/** @defgroup GPIO_pins GPIO pins
*/
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */
......
#define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */
#define GPIO_PIN_All ((uint16_t)0xFFFF) /* All pins selected */
#define GPIO_PIN_MASK (0x0000FFFFu) /* PIN mask for assert test */
/* Initialization and de-initialization functions *****************************/
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin);
/* IO operation functions *****************************************************/
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin);
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);
上面的代码展示了对某端口位的配置结构体GPIO_InitTypeDef的定义和操作函数,结合前面介绍的 I / O 端口位的结构图及配置表,我们可以比较清楚对GPIO结构体GPIO_InitTypeDef各成员该如何设置。GPIO_InitTypeDef各成员与具体寄存器间的映射关系这里就不赘述了,之前的博客:《中断管理与定时器》和《存储管理与虚拟内存》
中都有介绍。
下面再看看我们要操作的外设RGB LED灯与STM32L475连接的原理图:
RGB LED灯负极通过3个GPIO引脚PE7 / PE8 / PE9与STM32L475芯片相连,因LED正极已与高电平VCC相连,故需要STM32L475控制GPIO引脚PE7 / PE8 / PE9输出低电平亮起,高电平熄灭。因引脚PE7 / PE8 / PE9需要能输出高、低电平,故三个引脚需要配置为普通推挽输出模式GPIO_MODE_OUTPUT_PP;如果想默认LED灯处于熄灭状态可以配置为上拉模式GPIO_PULLUP;速率根据需求配置即可,如果只是控制LED灯亮灭配置低速率GPIO_SPEED_FREQ_LOW即可,如果想实现类似LED屏幕的PWM调光效果可以配置为高速率GPIO_SPEED_FREQ_HIGH。
3.2 CubeMX配置
STM32 CubeMX是ST意法半导体近几年大力推荐的STM32芯片图形化配置工具,允许用户使用图形化向导生成C初始化代码,可以大大减轻开发工作、时间和费用。大多数情况下,我们只使用STM32CubeMX来生成工程的时钟系统初始化代码以及外设的初始化代码,因为用户控制逻辑代码是无法在STM32CubeMX中完成的,需要用户自己根据需求来实现。使用STM32CubeMX配置工程的一般步骤为:
- 工程初步建立和保存,包括选择MCU型号,可指定系列、封装、外设数量等条件;
- RCC时钟源选择和时钟系统(时钟树)配置;
- GPIO功能引脚配置,包括外设和中间件配置;
- NVIC中断系统配置;
- C代码工程生成器配置,生成工程代码;
- 编写用户控制逻辑代码,包括回调函数实现;
下面以控制RGB LED的工程为例,详细图解MCU型号选择–>配置GPIO外设–>配置RCC时钟–>设置工程选项–>生成工程代码的全过程。
首先打开CubeMX–>File–>New Project 弹出如下的界面,搜索并选择目标芯片STM32L475VET6:
GPIO外设要工作离不开时钟的支持,下面先介绍RCC时钟的配置,我们选择的STM32L4潘多拉开发板使用外部高速时钟源HSE的晶振频率为8MHZ,外部高速时钟源HSE选择陶瓷晶振:
配置好了RCC时钟源及时钟引脚,接下来要配置时钟树了,首先是时钟源HSE的时钟频率配置为8MHZ,接下来就是时钟信号选择和时钟的分频、倍频系数配置了,我们配置为STM32L475支持的最高频率80MHZ,具体的参数配置如下图所示:
时钟配置完了,生成工程代码时会按上面的配置生成一个时钟配置函数SystemClock_Config(void)。
下面开始介绍GPIO外设的图形化配置过程,先搜索并配置目标引脚PE7 / PE8 / PE9为输出模式GPIO_Output:
选择左侧的System Core下的GPIO,配置各GPIO的具体参数,这里PE7 / PE8 / PE9三个引脚的配置参数相同:
GPIO外设也配置完了,生成工程代码时会按上面的配置生成一个GPIO初始化函数MX_GPIO_Init(void),因为GPIO更靠底层,没有抽象为句柄,所以这里不需要MspInit函数。只是控制RGB LED,也不涉及中断,所以也不需要配置NVIC。
到这里RGB LED工程需要的时钟配置和GPIO配置已经完成了,接下来配置工程管理器后就可以生成工程代码了,生成代码需要调用HAL库,所以这里需要选择之前下载的HAL库文件目录,工程管理器的配置如下图所示:
代码生成器还有一些配置选项,比如如何复制HAL库文件到工程目录,如何生成工程代码文件,设置工程模板等,这里选择的配置如下:
还可以选择使用HAL库还是LL库,如果MCU资源有限可以选择使用LL库以节约资源,我们的开发板资源足够,为了后续使用更高级的协议就选择HAL库了:
到这里项目工程配置完毕,如果想要使用调试功能,可以按下图配置调试模式:
到这里STM32CubeMX配置完毕,直接点击右上角的GENERATE CODE即可生成工程代码,生成完成后打开项目,即可直接调用配置的KEIL V5 IDE打开生成的工程,接下来在KEIL MDK中完成用户逻辑代码和回调函数的编写:
3.3 完成用户控制逻辑
STM32CubeMX除了调用HAL库函数外,还为我们生成了几个函数,主要是时钟配置函数SystemClock_Config(void)和GPIO外设初始化函数MX_GPIO_Init(void)与HAL_MspInit(void),因为本实验只是控制RGB LED灯颜色,并没有涉及到中断,也就不需要编写回调函数,只需要完成用户的控制逻辑代码即可。
RGB LED的颜色可由三种颜色的LED灯的亮灭搭配出8种颜色(由于红、绿、蓝三色灯等只有亮灭两种状态,如果想实现更精细的亮度搭配出更多样的颜色,可以每个LED灯都使用PWM或DAC控制其亮度),我们把这8种颜色定义为枚举类型,要点亮某种颜色需要打开或熄灭哪几个LED的控制逻辑可以在一个函数内实现,下面给出其中一种实现代码:
// Core\Inc\gpio.h文件新增如下代码
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
typedef enum{
RED,
GREEN,
BLUE,
YELLOW,
PURPLE,
CYAN,
WHITE,
BLACK,
MAX_NUM
}RGB_Color;
/* USER CODE END PV */
/* USER CODE BEGIN Prototypes */
void RGB_ON(RGB_Color RGB_LED);
/* USER CODE END Prototypes */
// Core\Src\gpio.c文件新增如下代码
/* USER CODE BEGIN 2 */
void RGB_ON(RGB_Color RGB_LED)
{
switch(RGB_LED % MAX_NUM)
{
case RED:
HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_SET);
break;
case GREEN:
HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_SET);
break;
case BLUE:
HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_RESET);
break;
case YELLOW:
HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_SET);
break;
case PURPLE:
HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_RESET);
break;
case CYAN:
HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_RESET);
break;
case WHITE:
HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_RESET);
break;
default:
HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_SET);
break;
}
}
/* USER CODE END 2 */
调用函数RGB_ON,并传入枚举RGB_Color类型的参数,即可控制RGB LED亮起指定的颜色,其中LED_R_Pin / LED_G_Pin / LED_B_Pin在main.h中有CubeMX生成为宏定义,红、绿、蓝三色灯哪个被设置为GPIO_PIN_RESET低电平即亮起。
下面在主函数main中实现用户控制逻辑,这里简单起见,就控制RGB LED每个一段时间(比如1秒)变换一种颜色,轮流在8种颜色中转换,下面是其中一种实现代码:
// Core\Src\main.c
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
uint16_t color = 0;
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
......
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
RGB_ON((RGB_Color)color);
color++;
HAL_Delay(1000);
}
/* USER CODE END 3 */
}
编译工程成功,通过我们的潘多拉开发板自带的ST-LINK V2.1烧录到开发板主芯片STM32L475中,RGB LED并没有亮起,按下RESET按键RGB LED按照我们预期的方式开始变换颜色,如果想实现下载后立即执行,需要在下面的对话框中勾选Reset and Run选项:
我们使用KEIL MDK V5编译HAL库生成的程序时发现编译速度非常慢,生成的工程默认是使用ARM Compiler V5(也即ARM GCC)编译的,如果取消Browse Infomation选项可以明显加快编译速度,但取消该选项后就无法实现跳转到某函数或变量的定义与声明了,不便于调试。新版的KEIL MDK V5(可能是V5.21之后的版本吧)支持ARM Compiler V6(也即ARM CLANG),该编译器得益于LLVM和Clang技术可以大大提升编译速度,下面我们选择ARM Compiler V6重新编译,配置界面如下(可参考:Migrate ARM Compiler 5 to ARM Compiler 6):
如果使用ARM Compiler V6编译器,C语言需要选择gnu99(或gnu11),否则会出现很多编译错误(可能HAL库中使用了一些GNU C语法),编译成功,且编译速度明显加快了很多,生成的文件所占用存储空间也小了一些,AC5与AC6编译耗时与所占空间对比如下:
到这里控制RGB LED的整个工程就完成了,工程文件下载地址:https://github.com/StreamAI/STM32L4/tree/master/RGB_LED
四、新增外部中断
前面介绍RGB LED控制工程并没有涉及到中断,所以也没涉及到HAL库的另一大特色—NVIC中断配置与回调函数的编写。下面以按键KEY作为外部中断输入信号,分别用KEY0、KEY1、KEY2来控制LED_R、LED_G、LED_B三个灯的亮灭,三个按键组合使用可以控制RGB LED亮起指定的色彩。
4.1 EXIT外部中断/事件控制器配置
前面介绍GPIO配置时,谈到GPIO输入模式可以接收外部的中断 / 事件,且在GPIO结构体成员变量GPIO_InitTypeDef->mode可以配置为中断或事件触发模式,相关宏定义摘取如下:
// Drivers\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_gpio.h
#define GPIO_MODE_IT_RISING (0x10110000u) /*!< External Interrupt Mode with Rising edge trigger detection */
#define GPIO_MODE_IT_FALLING (0x10210000u) /*!< External Interrupt Mode with Falling edge trigger detection */
#define GPIO_MODE_IT_RISING_FALLING (0x10310000u) /*!< External Interrupt Mode with Rising/Falling edge trigger detection */
#define GPIO_MODE_EVT_RISING (0x10120000u) /*!< External Event Mode with Rising edge trigger detection */
#define GPIO_MODE_EVT_FALLING (0x10220000u) /*!< External Event Mode with Falling edge trigger detection */
#define GPIO_MODE_EVT_RISING_FALLING (0x10320000u) /*!< External Event Mode with Rising/Falling edge trigger detection */
我有一篇博客:中断管理与定时器也详细介绍过中断原理与过程,下面再给出外部中断 / 事件控制器的配置框图如下(参考自《SMT32L475VE Reference manual.pdf》):
从上面的配置框图与宏定义可以看出,接收到输入信号有三种触发方式:上升沿触发、下降沿触发、上升下降沿均触发;对于输入信号又可分为两大类:中断模式、事件模式,二者的区别主要是中断模式会触发NVIC的中断服务,事件模式则不会触发NVIC的中断服务,但事件模式可以触发其他的外设响应比如DMA操作、AD转换等。
事件模式提供了一个完全由硬件自动完成的触发到产生结果的通道,不要软件的参与,降低了CPU的负荷,节省了中断资源,提高了响应速度(硬件总快于软件),是利用硬件来提升CPU芯片处理事件能力的一个有效方法。本例中为了了解NVIC配置与回调函数编写,将相关GPIO配置为中断模式。
下面看看EXIT的相关数据结构定义与API操作函数:
// Drivers\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_exti.h
/**
* @brief EXTI Configuration structure definition
*/
typedef struct
{
uint32_t Line; /*!< The Exti line to be configured. This parameter
can be a value of @ref EXTI_Line */
uint32_t Mode; /*!< The Exit Mode to be configured for a core.
This parameter can be a combination of @ref EXTI_Mode */
uint32_t Trigger; /*!< The Exti Trigger to be configured. This parameter
can be a value of @ref EXTI_Trigger */
uint32_t GPIOSel; /*!< The Exti GPIO multiplexer selection to be configured.
This parameter is only possible for line 0 to 15. It
can be a value of @ref EXTI_GPIOSel */
} EXTI_ConfigTypeDef;
/** @defgroup EXTI_Mode EXTI Mode
*/
#define EXTI_MODE_NONE 0x00000000u
#define EXTI_MODE_INTERRUPT 0x00000001u
#define EXTI_MODE_EVENT 0x00000002u
/** @defgroup EXTI_Trigger EXTI Trigger
*/
#define EXTI_TRIGGER_NONE 0x00000000u
#define EXTI_TRIGGER_RISING 0x00000001u
#define EXTI_TRIGGER_FALLING 0x00000002u
#define EXTI_TRIGGER_RISING_FALLING (EXTI_TRIGGER_RISING | EXTI_TRIGGER_FALLING)
/** @defgroup EXTI_GPIOSel EXTI GPIOSel
*/
#define EXTI_GPIOA 0x00000000u
......
#define EXTI_GPIOI 0x00000008u
/**
* @brief EXTI Handle structure definition
*/
typedef struct
{
uint32_t Line; /*!< Exti line number */
void (* PendingCallback)(void); /*!< Exti pending callback */
} EXTI_HandleTypeDef;
/** @defgroup EXTI_Exported_Functions_Group1 Configuration functions
* @brief Configuration functions
*/
/* Configuration functions ****************************************************/
HAL_StatusTypeDef HAL_EXTI_SetConfigLine(EXTI_HandleTypeDef *hexti, EXTI_ConfigTypeDef *pExtiConfig);
HAL_StatusTypeDef HAL_EXTI_GetConfigLine(EXTI_HandleTypeDef *hexti, EXTI_ConfigTypeDef *pExtiConfig);
HAL_StatusTypeDef HAL_EXTI_ClearConfigLine(EXTI_HandleTypeDef *hexti);
HAL_StatusTypeDef HAL_EXTI_RegisterCallback(EXTI_HandleTypeDef *hexti, EXTI_CallbackIDTypeDef CallbackID, void (*pPendingCbfn)(void));
HAL_StatusTypeDef HAL_EXTI_GetHandle(EXTI_HandleTypeDef *hexti, uint32_t ExtiLine);
/** @defgroup EXTI_Exported_Functions_Group2 IO operation functions
* @brief IO operation functions
*/
/* IO operation functions *****************************************************/
void HAL_EXTI_IRQHandler(EXTI_HandleTypeDef *hexti);
uint32_t HAL_EXTI_GetPending(EXTI_HandleTypeDef *hexti, uint32_t Edge);
void HAL_EXTI_ClearPending(EXTI_HandleTypeDef *hexti, uint32_t Edge);
void HAL_EXTI_GenerateSWI(EXTI_HandleTypeDef *hexti);
从上面EXTI_ConfigTypeDef的定义,结合前面GPIO_InitTypeDef的定义,我们对于EXIT外部中断如何配置应该已经比较了解了,跟寄存器的映射这里同样不再赘述,下面看看我们的开发板按键KEY与STM32L475芯片的连接原理图:
由上图可以看出,KEY0 / KEY1 / KEY2三个按键接上拉电阻,当按键按下时对应的引脚PD10 / PD9 / PD8电平被拉低也即低电平有效,所以配置模式为GPIO_MODE_IT_FALLING下降沿触发中断模式即可(WK_UP正相反,接下拉电阻,按下时引脚电平被拉高也即高电平有效,可配置为GPIO_MODE_IT_RISING,但我们在本例中不使用该按键)。
4.2 CubeMX新增配置
本例在前面RGB LED工程的基础上新增配置,RCC与时钟树的配置跟前面一样,下面介绍外部中断线PD10 / PD9 / PD8的配置如下图所示:
前面已经介绍过三个按键均配置为下降沿触发中断模式,接上拉电阻,为更直观编程依然使用了宏定义别名。
下面看NVIC中断控制器配置外部中断线的使能、优先级分组、抢占优先级、子优先级等:
工程管理和代码生成器配置跟前面也一致,工程目录和工程名最好跟前面的区分开来,生成工程后使用KEIL MDK V5打开。
4.3 完成回调函数编写
CubeMX生成的时钟配置函数和GPIO外设初始化函数跟前面的工程类似,但增加了按键部分的初始化配置。下面主要介绍下中断处理与回调函数的调用过程:
// Core\Src\stm32l4xx_it.c
/**
* @brief This function handles EXTI line[9:5] interrupts.
*/
void EXTI9_5_IRQHandler(void)
{
/* USER CODE BEGIN EXTI9_5_IRQn 0 */
/* USER CODE END EXTI9_5_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_8);
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_9);
/* USER CODE BEGIN EXTI9_5_IRQn 1 */
/* USER CODE END EXTI9_5_IRQn 1 */
}
/**
* @brief This function handles EXTI line[15:10] interrupts.
*/
void EXTI15_10_IRQHandler(void)
{
/* USER CODE BEGIN EXTI15_10_IRQn 0 */
/* USER CODE END EXTI15_10_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_10);
/* USER CODE BEGIN EXTI15_10_IRQn 1 */
/* USER CODE END EXTI15_10_IRQn 1 */
}
// stm32l4xx_hal_gpio.c
/**
* @brief Handle EXTI interrupt request.
* @param GPIO_Pin Specifies the port pin connected to corresponding EXTI line.
* @retval None
*/
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
/**
* @brief EXTI line detection callback.
* @param GPIO_Pin: Specifies the port pin connected to corresponding EXTI line.
* @retval None
*/
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(GPIO_Pin);
/* NOTE: This function should not be modified, when the callback is needed,
the HAL_GPIO_EXTI_Callback could be implemented in the user file
*/
}
由上面的代码可知,中断触发后执行HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)中断服务函数,该函数在HAL库中已经给出,我们只需要调用即可,不用关心中断标志位的读取和清除,简化了我们的开发工作量。在中断服务函数中会调用回调函数HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin),该函数被__weak修饰,我们只需要重定义该函数(已在HAL库中声明过,不需用户再次声明),将我们跟中断服务相关的控制逻辑写到该回调函数中即可,下面给出该回调函数的一种实现代码:
// Core\Src\gpio.c
/* USER CODE BEGIN 2 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
HAL_Delay(10); //消除抖动
switch(GPIO_Pin)
{
case KEY0_Pin: //控制LED_R灯状态翻转
if(HAL_GPIO_ReadPin(GPIOD, KEY0_Pin) == RESET)
HAL_GPIO_TogglePin(LED_R_GPIO_Port, LED_R_Pin);
break;
case KEY1_Pin: //控制LED_G灯状态翻转
if(HAL_GPIO_ReadPin(GPIOD, KEY1_Pin) == RESET)
HAL_GPIO_TogglePin(LED_G_GPIO_Port, LED_G_Pin);
break;
case KEY2_Pin: //控制LED_B灯状态翻转
if(HAL_GPIO_ReadPin(GPIOD, KEY2_Pin) == RESET)
HAL_GPIO_TogglePin(LED_B_GPIO_Port, LED_B_Pin);
break;
default:
break;
}
}
/* USER CODE END 2 */
主程序main函数也不需要实现什么控制逻辑了,保持默认即可,通过按键KEY控制RGB LED的工程到这里就完成了。依然选择ARM Compiler V6编译器编译该工程无错误,下载到我们的潘多拉开发板中按照预期的结果运行,工程文件下载地址:https://github.com/StreamAI/STM32L4/tree/master/RGB_LED_KEY