如何基于HAL库进行STM32开发
一、初识HAL库
-
STM32 开发中常说的 HAL 库开发,指的是利用 HAL 库固件包里封装好的 C 语言编写的驱动文件,来实现对 STM32 内部和外围设备的控制。但只有 HAL 库还不能直接驱动一个 STM32 的芯片,其它的组件已经由 ARM 与众多芯片硬件、软件厂商制定的通用的软件开发标准 CMSIS 实现了。
-
因为基于 Cortex 系列芯片采用的内核都是相同的,区别主要为核外的片上外设的差异,但也正是因为这些差异导致了我们编写的软件在“同内核、不同外设”的芯片之间移植相对困难。为了解决不同的芯片厂商生产的 Cortex 微控制器软件的兼容性问题,ARM 与芯片厂商建立了 CMSIS 标准 (Cortex MicroController Software Interface Standard)。所谓 CMSIS 标准,实际是新建了一个软件抽象层。
- 从图中可以看出,从用户程序到内核底层实现做了分层。按照这个分级,HAL 库属于 CMSIS-Pack 中的 “Peripheral HAL” 层。CMSIS 规定的最主要的 3 个部分为:核内外设访问层(由 ARM 负责实现)、片上外设访问层和外设访问函数(由芯片厂商负责实现)。
- ARM 整合并提供了大量的模版,各厂商根据自己的芯片差异修改模版,这其中包括汇编文件
startup_xxx.s
、system_xxx.h
和system_xxx.c
,其中xxx表示某系列的芯片型号。这些与初始化和系统相关的函数。 - 下图所示为基于HAL库的适配ST公司基于ARM公司cortex M4内核设计的STM32F4系列芯片的文件结构:
二、HAL库简介
- 为了方便用户开发 STM32 芯片,ST 公司先后提供了标准库、HAL 库和 LL 库共三种库函数。目前 ST 已经逐渐暂停对部分标准库的支持,ST 的库函数维护重点已经转移到 HAL 库和 LL 库上。
2.1 标准外设库(Standard Peripheral Libraries)
- 标准外设库(Standard Peripherals Library)是对 STM32 芯片的一个完整封装,包括所有标准外设器件的驱动,是 ST 最早推出的针对 STM 系列主控的库函数。标准库设计的初衷是减少用户程序编写时间,降低开发难度。标准库全部使用 C 语言实现,并严格按照 Strict ANSI-C、MISRA-C 2004 等多个 C 语言标准编写。但标准外设库仍然接近于寄存器操作,主要就是将一些基本的寄存器操作封装成了 C 函数。开发者仍需要关注所使用的外设是在哪个总线之上,具体寄存器的配置等底层信息。
- ST 为各系列芯片提供的标准外设库在内部的实现上稍微有些区别,在具体使用(移植)时,需要注意!但是,不同系列之间的差别并不是很大,而且在设计框架上大体是相同的。
2.2 HAL 库(Hardware Abstraction Layer)
- HAL 是 Hardware Abstraction Layer 的缩写,即硬件抽象层。是 ST 为可以更好的确保跨 STM32 产品的最大可移植性而推出的 MCU 操作库。这种程序设计由于抽离了应用程序和硬件底层的操作,更加符合跨平台和多人协作开发的需要。HAL 库是基于一个非限制性的 BSD 许可协议(Berkeley Software Distribution)而发布的开源代码。
2.3 LL 库(Low Layer)
- LL 库(Low Layer)目前与 HAL 库捆绑发布,它是一种在设计上为比 HAL 库更接近于硬件底层的操作,代码更轻量级,代码执行效率更高,可以完全独立于 HAL 库来使用,但 LL 库不匹配复杂的外设,如 USB 等。所以 LL 库并不是每个外设都有对应的完整驱动配置程序。
三、HAL库框架结构
-
STM32Fxx HAL函数库被压缩在一个zip文件中。解压该文件会产生一个类似STM32Cube_FW_F1_V1.0.0的文件夹,包含如下所示的子文件夹:
-
HAL库文件调用结构
-
HAL初始化全局结构
-
HAL 库头文件(Inc)和源文件(Src)存放在 STM32Cube 固件包的 STM32F4xx_HAL_Driver 文件夹中。其中,文件夹 Src(Source 的简写)存放是所有外设的驱动程序源码,文件夹Inc(Include 的简写)存放的是对应源码的头文件。Release_Notes.html 是HAL 库的版本更新信息。
-
打开 Src 和 Inc 文件夹,基本都是
stm32fxxx_hal_
和stm32fxxx_ll_
开头的 .c 和 .h文件。前者是 HAL 库,后者是 LL 库。 -
下表是以STM32F4系列芯片为例,阐述HAL库关键文件及其作用
API 函数和变量命名规则
-
以下“PPP”表示任一外设缩写,例如:ADC。系统、源程序文件和头文件命名都以
stm32f40x_hal_
作为开头。 -
所有常量都由英文字母大写书写,仅在某文件内部使用的,定义于该文件中;在多个文件中使用的,在对应头文件中定义。
-
寄存器作为常量处理,他们的命名都由英文字母大写表示。
-
外设函数的命名以HAL加该外设的缩写加下划线为开头,每个单词的第一个字母都由英文字母大写书写,例如:
HAL_I2C_Master_Transmit()
。 -
下表为常见命名示例
-
对于 HAL 的 API 函数,常见的有以下几种:
- 初始化/反初始化函数:
HAL_PPP_Init()
,HAL_PPP_DeInit()
- 外设读写函数:
HAL_PPP_Read()
,HAL_PPP_Write()
,HAL_PPP_Transmit()
,HAL_PPP_Receive()
- 控制函数:
HAL_PPP_Set()
,HAL_PPP_Get()
- 状态和错误:
HAL_PPP_GetState()
,HAL_PPP_GetError()
- 初始化/反初始化函数:
-
HAL 库封装的很多函数都是通过定义好的结构体将参数一次性传给所需函数,参数也有一定的规律,主要有以下三种:
-
配置和初始化用的结构体:一般为
PPP_InitTypeDef
或PPP_ConfTypeDef
的结构体类型,根据外设的寄存器地址结构,设计成易于理解和记忆的结构体成员。 -
特殊处理的结构体:专为不同外设而设置的,带有
Process
的字样,实现一些特异化的中间处理操作等。 -
外设句柄结构体:HAL 驱动的重要参数,可以同时定义多个句柄结构以支持多外设多模式。HAL 驱动的操作结果也可以通过这个句柄获得。有些 HAL 驱动的头文件中还定义了一些跟这个句柄相关的一些外设操作。如用外设结构体句柄与 HAL 定义的一些宏操作配合,即可实现一些常用的寄存器位操作。如下表所示:
-
-
MSP函数
- MSP是指和MCU相关的初始化。例如,我们要初始化一个串口,首先要设置和 MCU 无关的东西,例如波特率,奇偶校验,停止位等,这些参数设置和 MCU 没有任何关系,可以使用 STM32F1,也可以是 STM32F2/F3/F4/F7
上的串口。而一个串口设备它需要一个 MCU 来承载,例如用 STM32F4 来做承载,PA9 做为发送,PA10 做为接收,MSP 就是要初始化 STM32F4 的 PA9,PA10,配置这两个引脚。所以 HAL驱动方式的初始化流程就是:HAL_USART_Init()—>HAL_USART_MspInit() ,先初始化与 MCU无关的串口协议,再初始化与 MCU 相关的串口引脚。 - 在 STM32 的 HAL 驱动中,
HAL_PPP_MspInit()
作为回调,被HAL_PPP_Init()
函数所调用。当我们需要移植程序到 STM32F1平台的时候,我们只需要修改HAL_PPP_MspInit()
函数内容而不需要修改HAL_PPP_Init()
入口参数内容。 - 在HAL库中,几乎每初始化一个外设就需要设置该外设与单片机之间的联系,比如IO口,是否复用等等,可见,HAL库相对于标准库多了MSP函数之后,移植性非常强,但与此同时却增加了代码量和代码的嵌套层级。可以说各有利弊。
- MSP是指和MCU相关的初始化。例如,我们要初始化一个串口,首先要设置和 MCU 无关的东西,例如波特率,奇偶校验,停止位等,这些参数设置和 MCU 没有任何关系,可以使用 STM32F1,也可以是 STM32F2/F3/F4/F7
-
回调函数
-
类似于MSP函数,Callback函数主要帮助用户应用层的代码编写。还是以USART为例,在标准库中,串口中断了以后,我们要先在中断中判断是否是接收中断,然后读出数据,顺便清除中断标志位,然后再是对数据的处理,这样如果我们在一个中断函数中写这么多代码就会显得很混乱。
void USART3_IRQHandler(void) //串口1中断服务程序 {u8 Res;//接收中断(接收到的数据必须是0x0d 0x0a结尾)if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) {Res =USART_ReceiveData(USART3); //读取接收到的数据/*数据处理区*/} } }
-
而在HAL库中,进入串口中断后,直接由HAL库中断函数进行托管,
HAL_UART_IRQHandler()
帮我们完成了判断是哪个中断(接收?发送?或者其他?),然后读出数据,保存至缓存区,顺便清除中断标志位等等操作。void USART1_IRQHandler(void) { //调用HAL库中断处理公用函数HAL_UART_IRQHandler(&UART1_Handler); }
-
用户需要做的仅仅是根据不同情况,编写相应的回调函数(如
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
)并在其中实现用户自定义的功能,这也是我们使用 HAL 库的最常用的接口之一。所以说Callback函数是一个应用层代码的函数,我们在一开始只设置句柄里面的各个参数,然后就等着HAL库把自己安排好的代码送到手中就可以了~
-
四、HAL库的用户配置文件
-
stm32f4xx_hal_conf.h
:用于裁剪 HAL 库和定义一些变量,官方没有直接提供这个文件,但在STM32Cube_FW_F4_V1.26.0/Drivers/STM32F4xx_HAL_Driver/Inc
这个路径下提供了一个模版文件stm32f4xx_hal_conf_template.h
,我们可以直接复制这个文件,并重命名为stm32f4xx_hal_conf.h
,根据自己项目实际做一些简单的修改即可。 -
用户配置文件可以用来选择使能何种外设,源码配置在 35 行到 74 行。我们只要屏蔽某个外设的宏,则这个外设的驱动代码机会被屏蔽,从而不可用。
#define HAL_MODULE_ENABLED #define HAL_ADC_MODULE_ENABLED #define HAL_CAN_MODULE_ENABLED /* #define HAL_CAN_LEGACY_MODULE_ENABLED */ #define HAL_CRC_MODULE_ENABLED /* ...中间省略... */ #define HAL_WWDG_MODULE_ENABLED #define HAL_CORTEX_MODULE_ENABLED #define HAL_PCD_MODULE_ENABLED #define HAL_HCD_MODULE_ENABLED
-
在
stm32f4xx_hal_conf.h
文件中(约在第84 行左右),有个 HSE_VALUE 参数,表示外部高速晶振的频率。这个参数务必根据板子外部焊接的晶振频率来修改,官方默认是 25M,而我们使用的开发板,一般晶振频率为 8MHz。#if !defined (HSE_VALUE) /* 外部高速振荡器的值,单位 HZ */#define HSE_VALUE ((uint32_t)8000000) #endif /* HSE_VALUE */
-
在
stm32f4xx_hal_conf.h
文件中(约在第112 行左右),有个 LSE_VALUE 参数,表示外部低速晶振频率,官方默认是 32.768KHZ。#if !defined (LSE_VALUE)/* 外部低速振荡器的值,单位 HZ */#define LSE_VALUE ((uint32_t)32768) #endif /* LSE_VALUE */
-
在
stm32f4xx_hal_conf.h
文件中(约在第136 行左右),有个宏定义 TICK_INT_PRIORITY,表示滴答定时器的优先级。这个优先级很重要,因为如果外设驱动程序的延时是通过滴答定时器提供的时间基准的话,当我们在中断优先级高于滴答定时器的某中断服务程序里调用了基于此时间基准的延迟函数HAL_Delay()
的话,就会导致滴答定时器中断服务函数一直得不到运行,从而程序卡死在这里。所以,滴答定时器的中断优先级一定要比这些中断高。当然,这个时间基准可以是滴答定时器提供,也可以是其他片内外设定时器,默认是用滴答定时器。/*!< tick interrupt priority */ #define TICK_INT_PRIORITY ((uint32_t)0x0F)
五、HAL库初始化文件stm32f4xx_hal.c
-
HAL库初始化文件内容比较多,主要包括了 HAL 库的初始化、系统滴答、基准电压配置、IO 补偿、低功耗、EXTI 配置等。
-
HAL 库初始化函数的源码在 157 行到 183 行(以STM32F4 的 HAL 固件 1.26.0 版本为例,其它版本可能有差异),其简化函数如下:
HAL_StatusTypeDef HAL_Init(void) {/* 设置中断优先级分组 */HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);/* 使用滴答定时器作为时钟基准,配置 1ms 滴答(重置后默认的时钟源为 HSI) */if(HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK){return HAL_ERROR;}/* 初始化硬件 */HAL_MspInit();/* 返回函数状态 */return HAL_OK; }
- 该函数是 HAL 库的初始化函数,在程序中必须首先调用,其主要实现如下功能:
- 设置 NVIC 优先级分组为 4
- 配置滴答定时器每 1ms 产生一个中断。在这个阶段,系统时钟还没有配置好,因此系统还是默认使用内部高速时钟源 HSI 在跑程序。对于 F4 来说,HSI 的时钟频率是 16MHZ。所以如果用户不配置系统时钟的话,那么系统将会使用 HSI 作为系统时钟源。
- 调用
HAL_MspInit()
函数初始化底层硬件,该函数在stm32f4xx_hal.c
文件里面做了弱定义。
- 该函数是 HAL 库的初始化函数,在程序中必须首先调用,其主要实现如下功能:
-
HAL_InitTick()
函数的简化函数如下:__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) {/* 配置滴答定时器 1ms 产生一次中断 */if (HAL_SYSTICK_Config(SystemCoreClock / (1000UL / (uint32_t)uwTickFreq)) > 0U){return HAL_ERROR;}/* 配置滴答定时器中断优先级 */if (TickPriority < (1UL << __NVIC_PRIO_BITS)){HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);uwTickPrio = TickPriority;}else{return HAL_ERROR;}/* 返回函数状态 */return HAL_OK; }
- 该函数用于初始化滴答定时器的时钟基准,主要功能如下:
- 配置滴答定时器 1ms 产生一次中断
- 配置滴答定时器的中断优先级
- 该函数是__weak 定义的 “弱函数”,用户可以重新定义该函数。
- 该函数可以通过
HAL_Init()
或者HAL_RCC_ClockConfig()
重置时钟。在默认情况下,滴答定时器是时间基准的来源。如果其他中断服务函数调用了HAL_Delay()
,必须小心,滴答定时器中断必须比调用了HAL_Delay()
函数的其他中断服务函数的优先级高(数值较低),否则会导致滴答定时器中断服务函数一直得不到执行,从而卡死在这里。
- 该函数用于初始化滴答定时器的时钟基准,主要功能如下:
-
滴答定时器相关的函数(源码在 303 行到 435 行)如下:
/* 该函数在滴答定时器时钟中断服务函数中被调用,一般滴答定时器 1ms 中断一次,所以函数每 1ms 让全局变量 uwTick 计数值加 1 */
__weak void HAL_IncTick(void)
{uwTick += (uint32_t)uwTickFreq;
}/* 获取全局变量 uwTick 当前计算值 */
__weak uint32_t HAL_GetTick(void)
{return uwTick;
}/* 获取滴答时钟优先级 */
uint32_t HAL_GetTickPrio(void)
{return uwTickPrio;
}/* 设置滴答定时器中断频率 */
HAL_StatusTypeDef HAL_SetTickFreq(HAL_TickFreqTypeDef Freq)
{HAL_StatusTypeDef status = HAL_OK;HAL_TickFreqTypeDef prevTickFreq;assert_param(IS_TICKFREQ(Freq));if (uwTickFreq != Freq){/* 备份滴答定时器中断频率 */prevTickFreq = uwTickFreq;/* 更新被 HAL_InitTick()调用的全局变量 uwTickFreq */uwTickFreq = Freq;/* 应用新的滴答定时器中断频率 */status = HAL_InitTick(uwTickFreq);if (status != HAL_OK){/* 恢复之前的滴答定时器中断频率 */uwTickFreq = prevTickFreq;}}return status;
}/* 获取滴答定时器中断频率 */
HAL_TickFreqTypeDef HAL_GetTickFreq(void)
{return uwTickFreq;
}/*HAL 库的延时函数,默认延时单位 ms */
__weak void HAL_Delay(uint32_t Delay)
{uint32_t tickstart = HAL_GetTick();uint32_t wait = Delay;/* Add a freq to guarantee minimum wait */if (wait < HAL_MAX_DELAY){wait += (uint32_t)(uwTickFreq);}while ((HAL_GetTick() - tickstart) < wait){}
}/* 挂起滴答定时器中断,全局变量 uwTick 计数停止 */
__weak void HAL_SuspendTick(void)
{/* 禁止滴答定时器中断 */SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
}/* 恢复滴答定时器中断,恢复全局变量 uwTick 计数 */
__weak void HAL_ResumeTick(void)
{/* 使能滴答定时器中断 */SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;
}
六、HAL库编程方式
-
在 HAL 库中对外设模型进行了统一,支持三种编程方式:
- 轮询模式/阻塞模式
- 中断方式
- DMA模式
-
以IIC为例,三种编程模式对应的函数如下:
-
轮询模式/阻塞模式
HAL_I2C_Master_Transmit(); HAL_I2C_Master_Receive(); HAL_I2C_Slave_Transmit(); HAL_I2C_Slave_Receive() HAL_I2C_Mem_Write(); HAL_I2C_Mem_Read(); HAL_I2C_IsDeviceReady()
-
中断模式
HAL_I2C_Master_Transmit_IT(); HAL_I2C_Master_Receive_IT(); HAL_I2C_Slave_Transmit_IT() HAL_I2C_Slave_Receive_IT(); HAL_I2C_Mem_Write_IT(); HAL_I2C_Mem_Read_IT()
-
DMA模式
HAL_I2C_Master_Transmit_DMA(); HAL_I2C_Master_Receive_DMA(); HAL_I2C_Slave_Transmit_DMA(); HAL_I2C_Slave_Receive_DMA(); HAL_I2C_Mem_Write_DMA(); HAL_I2C_Mem_Read_DMA()
-
七、HAL库中断处理
- 中断是 STM32 开发的一个很重要的概念,这里我们可以简单地理解为:STM32 暂停了当前手中的事并优先去处理更重要的事务。而这些 “更重要的事务” 是由软件开发人员在软件中定义的。
- 使用某外设中断的流程如下:
- 定义外设的控制句柄结构体
PPP_HandleType
- 初始化该结构体成员,特别是其中的
PPP_InitType
结构体的参数 - 调用 HAL 库对应这个驱动的初始化
HAL_PPP_Init()
函数, - 由于
HAL_PPP_Init()
函数调用了针对外设底层硬件的初始化函数HAL_PPP_Msp_Init()
,而HAL库并没有定义该接口,所以需要我们自己实现这个函数并完成外设时钟、IO 等硬件相关底层设置 - 使用
HAL_NVIC_SetPriority()
、HAL_NVIC_EnableIRQ()
来使能我们的外设中断; - 定义中断处理函数
PPP_IRQHandler()
,并在中断函数中调用HAL_PPP_function_IRQHandler()
来判断和处理中断标记; - HAL 库中断处理完成后,我们需要自定义中断回调函数
HAL_PPP_ProcessCpltCallback()
,我们在这个函数中实现对外设想做的处理; - 中断响应处理完成后,STM32 芯片继续顺序执行主程序功能,按照以上处理的标准流程完成了一次中断响应。
- 定义外设的控制句柄结构体
八、创建HAL库工程
-
HAL 库文件【STM32Cube_FW_F1_V1.8.6\Drivers\STM32F4xx_HAL_Driver】导入到自己新建的工程中,删除其子文件夹Src中的
xxx_template.c
文件(stm32f4xx_hal_msp_template.c
、stm32f4xx_hal_timebase_rtc_alarm_template.c
、stm32f4xx_hal_timebase_rtc_wakeup_template.c
、stm32f4xx_hal_timebase_tim_template.c
)。 -
将其子文件夹Inc中的
xxx_template.h
文件重命名(去掉 _template后缀),即将stm32_assert_template.h
、stm32f4xx_hal_conf_template.h
文件重命名为stm32_assert.h
、stm32f4xx_hal_conf.h
文件。 -
将【STM32Cube_FW_F1_V1.8.6\Drivers\CMSIS\Device\ST\STM32F4xx\Include】文件夹中的3个头文件(
stm32f4xx.h
、stm32f407xx.h
、system_stm32f4xx.h
)拷贝到我们新建的工程。 -
将【STM32Cube_FW_F1_V1.8.6\Drivers\CMSIS\Device\ST\STM32F4xx\Source\Templates】文件夹中的
system_stm32f4xx.c
的拷贝到我们新建的工程。并从对应的编译器文件夹(arm、gcc、iar)下,找到开发板对应的启动文件startup_stm32f407xc.s
并拷贝到工程。 -
将【STM32Cube_FW_F1_V1.8.6\Drivers\CMSIS\Include】文件夹中需要的头文件(
cmsis_compiler.h
、cmsis_armcc.h
、cmsis_version.h
、core_cm4.h
、mpu_armv7.h
)拷贝到新建的工程。 -
将【STM32Cube_FW_F1_V1.8.6\Projects\STM32F407ZGT6\Templates\Inc】文件中的
stm32f4xx_it.h
文件拷贝到新建的工程。 -
将【STM32Cube_FW_F1_V1.8.6\Projects\STM32F407ZGT6\Templates\Src】文件中的
stm32f4xx_hal_msp.c
文件和stm32f4xx_it.c
文件拷贝到新建的工程。 -
在工程中添加 HAL 库所需要的各种头文件路径:
Application\Inc
、SDK\CMSIS\Include
、SDK\CMSIS\Device\ST\STM32F4xx\Include
、SDK\STM32F4xx_HAL_Driver\Inc
和SDK\STM32F4xx_HAL_Driver\Inc\Legacy
-
在新建的工程中,点击魔术棒,在C/C++选项卡中添加如下宏定义:
STM32F407xx,USE_HAL_DRIVER,USE_FULL_LL_DRIVER
。 -
在配置文件
stm32f4xx_hal_conf.h
中开启STM32 的相关外设功能、定义 外部晶振HSE的频率。#if !defined (HSE_VALUE) #if defined(USE_STM3210C_EVAL)#define HSE_VALUE 25000000U /*!< Value of the External oscillator in Hz */ #else#define HSE_VALUE 8000000U /*!< Value of the External oscillator in Hz */ #endif #endif /* HSE_VALUE */
【参考资料】
STM32HAL库使用详解-CSDN博客
02. 初识HAL库 - 星光映梦 - 博客园