一、初识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_device.s、system_.h 和 system_.c 这些与初始化和系统相关的函数。
二、HAL库简介
库函数的引入,大大降低 STM 系列主控芯片开发的难度。ST 公司为了方便用户开发 STM32 芯片开发提供了三种库函数,从时间产生顺序是:标准库、HAL 库和 LL 库。目前 ST 已经逐渐暂停对部分标准库的支持,ST 的库函数维护重点对角已经转移到 HAL 库和 LL 库上。
【1】、标准外设库(Standard Peripheral Libraries)
标准外设库(Standard Peripherals Library)是对 STM32 芯片的一个完整的封装,包括所有标准器件外设的器件驱动器,是 ST 最早推出的针对 STM 系列主控的库函数。标准库的设计的初衷是减少用户的程序编写时间,进而降低开发成本。几乎全部使用 C 语言实现并严格按照 Strict ANSI-C、MISRA-C 2004 等多个 C 语言标准编写。但标准外设库仍然接近于寄存器操作,主要就是将一些基本的寄存器操作封装成了 C 函数。开发者仍需要关注所使用的外设是在哪个总线之上,具体寄存器的配置等底层信息。
ST 为各系列提供的标准外设库稍微有些区别。此外,在内部的实现上也稍微有些区别,这个在具体使用(移植)时,需要注意一下!但是,不同系列之间的差别并不是很大,而且在设计上是相同的。
【2】、HAL 库(Hardware Abstraction Layer)
HAL 是 Hardware Abstraction Layer 的缩写,即硬件抽象层。是 ST 为可以更好的确保跨 STM32 产品的最大可移植性而推出的 MCU 操作库。这种程序设计由于抽离应用程序和硬件底层的操作,更加符合跨平台和多人协作开发的需要。HAL 库是基于一个非限制性的 BSD 许可协议(Berkeley Software Distribution)而发布的开源代码。
【3】、LL 库(Low Layer)
LL 库(Low Layer)目前与 HAL 库捆绑发布,它的设计为比 HAL 库更接近于硬件底层的操作,代码更轻量级,代码执行效率更高的库函数组件,可以完全独立于 HAL 库来使用,但 LL 库不匹配复杂的外设,如 USB 等。所以 LL 库并不是每个外设都有对应的完整驱动配置程序。
三、HAL库框架结构
HAL 库头文件和源文件在 STM32Cube 固件包的 STM32F4xx_HAL_Driver 文件夹中。STM32F407xx_HAL_Driver 文件夹下的 Src(Source 的简写)文件夹存放是所有外设的驱动程序源码,Inc(Include 的简写)文件夹存放的是对应源码的头文件。Release_Notes.html 是HAL 库的版本更新信息。
打开 Src 和 Inc 文件夹,大家会发现基本都是 stm32f4xx_hal_和 stm32f4xx_ll_开头的 .c 和 .h文件。stm32f4xx_hal_开头的是 HAL 库,stm32f4xx_ll_开头的文件 LL 库的文件。
stm32f4xx_hal_ppp (c/h)中的函数和变量命名也严格按照命名规则。
对于 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 定义的一些宏操作配合,即可实现一些常用的寄存器位操作。
HAL 库的回调函数,这部分允许用户重定义,并在其中实现用户自定义的功能,也是我们使用 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,做一些简单的修改即可。
stm32f4xx_hal_conf.h 文件 84 行左右,有个 HSE_VALUE 参数,这个参数表示我们的外部高速晶振的频率。这个参数请务必根据我们板子外部焊接的晶振频率来修改,官方默认是 25M。这里,我们使用的开发板,晶振频率为 8MHz。
#if !defined (HSE_VALUE)
/* 外部高速振荡器的值,单位 HZ */
#define HSE_VALUE ((uint32_t)8000000)
#endif /* HSE_VALUE */
源码在 112 行左右,还有一个参数就是外部低速晶振频率,这个官方默认是 32.768KHZ。
#if !defined (LSE_VALUE)
/* 外部低速振荡器的值,单位 HZ */
#define LSE_VALUE ((uint32_t)32768)
#endif /* LSE_VALUE */
用户配置文件可以用来选择使能何种外设,源码配置在 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 文件的 136 行左右,宏定义 TICK_INT_PRIORITY
是滴答定时器的优先级。这个优先级很重要,因为如果其它的外设驱动程序的延时是通过滴答定时器提供的时间基准,来实现延时的话,又由于实现方式是滴答定时器对寄存器进行计数,所以当我们在其它中断服务程序里调用基于此时间基准的延迟函数 HAL_Delay,那么假如该中断的优先级高于滴答定时器的优先级,就会导致滴答定时器中断服务函数一直得不到运行,从而程序卡死在这里。所以滴答定时器的中断优先级一定要比这些中断高。
/*!< tick interrupt priority */
#define TICK_INT_PRIORITY ((uint32_t)0x0F)
这个时间基准可以是滴答定时器提供,也可以是其他的定时器,默认是用滴答定时器。
五、HAL库初始化文件
这个文件内容比较多,包括 HAL 库的初始化、系统滴答、基准电压配置、IO 补偿、低功耗、EXTI 配置等都集合在这个文件里面。
HAL 库初始化函数的源码在 157 行到 183 行,简化函数如下(下面的代码只针对 F4 的 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 函数初始化底层硬件,HAL_MspInit 函数在 stm32f4xx_hal.c 文件里面做了弱定义。
在源码在 253 行到 274 行,有 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(uwTickPrio);
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库中断处理
中断是 STM32 开发的一个很重要的概念,这里我们可以简单地理解为:STM32 暂停了当前手中的事并优先去处理更重要的事务。而这些 “更重要的事务” 是由软件开发人员在软件中定义的。
设置外设的控制句柄结构体 PPP_HandleType
和初始化 PPP_InitType
结构体的参数,然后调用 HAL 库对应这个驱动的初始化 HAL_PPP_Init()
,由于这个 API 中有针对外设初始化细节的接口 HAL_PPP_Msp_Init()
,我们需要重新实现这个函数并完成外设时钟、IO 等细节差异的设置, 完成各细节处理后,使用 HAL_NVIC_SetPriority()
、HAL_NVIC_EnableIRQ()
来使能我们的外设中断;定义中断处理函数 PPP_IRQHandler()
,并在中断函数中调用 HAL_PPP_function_IRQHandler()
来判断和处理中断标记;HAL 库中断处理完成后,根据对应中的调用我们需要自定义的中断回调接口HAL_PPP_ProcessCpltCallback()
,然后,我们在这个函数中实现我们对外设想做的处理;中断响应处理完成后,STM32 芯片继续顺序执行我们定义的主程序功能,按照以上处理的标准流程完成了一次中断响应。