Wiki:知识&笔记 Wiki:知识&笔记
首页
  • 学习笔记

    • 《JavaScript教程》笔记
  • 嵌入式

    • STM32
  • 技术文档
归档
GitHub (opens new window)
首页
  • 学习笔记

    • 《JavaScript教程》笔记
  • 嵌入式

    • STM32
  • 技术文档
归档
GitHub (opens new window)
  • 基础

  • 底层&寄存器

  • HAL库外设

    • HAL库函数
    • HAL结构
      • 区别于标准库
        • 句柄
        • MSP函数
        • 回调函数
      • HAL库结构
        • 文件结构
        • 命名规则
      • 三种编程方式
      • 三大回调函数
        • 总结
    • GPIO各种点亮LED
    • 定时器
    • PWM控制LED颜色
    • 按键外部中断
    • ADC使用
    • 串口与DMA
    • FLASH 读写
    • IIC通信
    • SPI 通信
    • CAN
  • STM32
  • HAL库外设
2020-09-23
区别于标准库
句柄
MSP函数
回调函数
HAL库结构
文件结构
命名规则
三种编程方式
三大回调函数
总结

HAL结构

本文部分转载自STM32 之 HAL库 (opens new window)。

如有侵权请联系博主删除

# 区别于标准库

# 句柄

相较于标准库,HAL 库中的外设可以全程挂载在一个结构体上而不是通过 init 函数初始化后就还是基于寄存器的操作了。这个句柄指向设定这个外设的结构体

以 USART 为例,首先要初始化它们的各个寄存器:

typedef struct
{
	  USART_TypeDef                 *Instance;        /*!< UART registers base address        */
	  UART_InitTypeDef              Init;             /*!< UART communication parameters      */
	  uint8_t                       *pTxBuffPtr;      /*!< Pointer to UART Tx transfer Buffer */
	  uint16_t                      TxXferSize;       /*!< UART Tx Transfer size              */
	  uint16_t                      TxXferCount;      /*!< UART Tx Transfer Counter           */
	  uint8_t                       *pRxBuffPtr;      /*!< Pointer to UART Rx transfer Buffer */
	  uint16_t                      RxXferSize;       /*!< UART Rx Transfer size              */
	  uint16_t                      RxXferCount;      /*!< UART Rx Transfer Counter           */  
	  DMA_HandleTypeDef             *hdmatx;          /*!< UART Tx DMA Handle parameters      */ 
	  DMA_HandleTypeDef             *hdmarx;          /*!< UART Rx DMA Handle parameters      */
	  HAL_LockTypeDef               Lock;             /*!< Locking object                     */
	  __IO HAL_UART_StateTypeDef    State;            /*!< UART communication state           */
	  __IO uint32_t                 ErrorCode;        /*!< UART Error code                    */
}UART_HandleTypeDef;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

作为比较,如果使用标准库操作,需要的过程是这样:

USART_InitTypeDef USART_InitStructure;

USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式

USART_Init(USART3, &USART_InitStructure); //初始化串口1
1
2
3
4
5
6
7
8
9
10

我们发现,与标准库不同的是,该成员不仅 :

  • 包含了之前标准库就有的六个成员(波特率,数据格式等),
  • 还包含过采样、(发送或接收的)数据缓存、数据指针、串口 DMA 相关的变量、各种标志位等等要在整个项目流程中都要设置的各个成员。 该 UART1_Handler 就被称为串口的句柄,它被贯穿整个 USART 收发的流程,比如开启中断:
HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);
1

比如后面要讲到的 MSP 与 Callback 回调函数:

void HAL_UART_MspInit(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
1
2

在这些函数中,只需要调用初始化时定义的句柄 UART1_Handler 就好

# MSP函数

MSP函数

MCU Specific Package 单片机的具体方案

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 函数又可以配合句柄,达到非常强的移植性:

void HAL_UART_MspInit(UART_HandleTypeDef *huart);
1

入口参数仅仅需要一个串口句柄,这样有能看出句柄的方便。

# 回调函数

还是以 USART 为例,在标准库中,串口中断了以后,我们要先在中断中判断是否是接收中断,然后读出数据,顺便清除中断标志位,然后再是对数据的处理,这样如果我们在一个中断函数中写这么多代码,就会显得很混乱:

void USART3_IRQHandler(void)                	//串口1中断服务程序
{
	u8 Res;
	if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
	{
		Res =USART_ReceiveData(USART3);	//读取接收到的数据
		/*数据处理区*/
		}   		 
     } 
} 
1
2
3
4
5
6
7
8
9
10

而在 HAL 库中,进入串口中断后,直接由 HAL 库中断函数进行托管:

void USART1_IRQHandler(void)                	
{ 
    /*By the way,这个函数就是中断发生后的入口函数*/
	HAL_UART_IRQHandler(&UART1_Handler);	//调用HAL库串口中断处理公用函数
	/***************省略无关代码****************/	
}
1
2
3
4
5
6

HAL_UART_IRQHandler这个函数完成了判断是哪个中断(接收?发送?或者其他),然后读出数据,保存至缓存区,顺便清除中断标志位等等操作。 比如我提前设置了,串口每接收五个字节,我就要对这五个字节进行处理。 在一开始我定义了一个串口接收缓存区:

/*HAL库使用的串口接收缓冲,处理逻辑由HAL库控制,接收完这个数组就会调用HAL_UART_RxCpltCallback进行处理这个数组*/
/*RXBUFFERSIZE=5*/
u8 aRxBuffer[RXBUFFERSIZE];
1
2
3

在初始化中,我在句柄里设置好了缓存区的地址,缓存大小(五个字节)

/*该代码在HAL_UART_Receive_IT函数中,初始化时会引用*/
	huart->pRxBuffPtr = pData;//aRxBuffer
    huart->RxXferSize = Size;//RXBUFFERSIZE
    huart->RxXferCount = Size;//RXBUFFERSIZE
1
2
3
4

则在接收数据中,每接收完五个字节,HAL_UART_IRQHandler 才会执行一次 Callback 函数:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
1

在这个 Callback 回调函数中,我们只需要对这接收到的五个字节(保存在 aRxBuffer[] 中)进行处理就好了,完全不用再去手动清除标志位等操作。 所以说 Callback 函数是一个应用层代码的函数,我们在一开始只设置句柄里面的各个参数,然后就等着 HAL 库把自己安排好的代码送到手中就可以了~

综上,从这三个小点就可以看出 HAL 库的可移植性之强大,并且用户可以完全不去理会底层各个寄存器的操作,代码也更有逻辑性。但与此带来的是复杂的代码量,极慢的编译速度,略微低下的效率。看怎么取舍了

# HAL库结构

# 文件结构

  • stm32f4xx.h 主要包含 STM32 同系列芯片的不同具体型号的定义,是否使用 HAL 库等的定义,接着,其会根据定义的芯片信号包含具体的芯片型号的头文件:

    #if defined(STM32F405xx)
    #include "stm32f405xx.h"
    #elif defined(STM32F415xx)
    #include "stm32f415xx.h"
    #elif defined(STM32F407xx)
    #include "stm32f407xx.h"
    #elif defined(STM32F417xx)
    #include "stm32f417xx.h"
    #else
    #error "Please select first the target STM32F4xx device used in your application (in stm32f2xx.h file)"
    #endif
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    紧接着,其会包含 stm32f4xx_hal.h:

  • stm32f4xx_hal.h:stm32f4xx_hal.c/h 主要实现 HAL 库的初始化、系统滴答相关函数、及 CPU 的调试模式配置

  • stm32f4xx_hal_conf.h:该文件是一个用户级别的配置文件,用来实现对 HAL 库的裁剪,其位于用户文件目录,不要放在库目录中

接下来对于 HAL 库的源码文件进行一下说明,HAL 库文件名均以 stm32f4xx_hal 开头,后面加上 _ 外设或者模块名(如:stm32f4xx_hal_adc.c):

  • 库文件:
    • stm32f4xx_hal_ppp.c/.h :主要的外设或者模块的驱动源文件,包含了该外设的通用 API stm32f4xx_hal_ppp_ex.c/.h // 外围设备或模块驱动程序的扩展文件。这组文件中包含特定型号或者系列的芯片的特殊 API。以及如果该特定的芯片内部有不同的实现方式,则该文件中的特殊 API 将覆盖 _ppp 中的通用 API
    • stm32f4xx_hal.c/.h:此文件用于 HAL 初始化,并且包含 DBGMCU、重映射和基于 systick 的时间延迟等相关的 API
  • 其他库文件:
    • 用户级别文件:
      • stm32f4xx_hal_msp_template.c: 只有 .c没有 .h。它包含用户应用程序中使用的外设的 MSP 初始化和反初始化(主程序和回调函数)。使用者复制到自己目录下使用模板
      • stm32f4xx_hal_conf_template.h: 用户级别的库配置文件模板。使用者复制到自己目录下使用
      • system_stm32f4xx.c:此文件主要包含 SystemInit() 函数,该函数在刚复位及跳到 main 之前的启动过程中被调用。它不在启动时配置系统时钟(与标准库相反)。时钟的配置在用户文件中使用 HAL API 来完成
      • startup_stm32f4xx.s:芯片启动文件,主要包含堆栈定义,终端向量表等
      • stm32f4xx_it.c/.h:中断处理函数的相关实现
  • main.c/.h

# 命名规则

根据 HAL 库的命名规则,其 API 可以分为以下三大类:

  • 初始化 / 反初始化函数:HAL_PPP_Init(), HAL_PPP_DeInit()
  • IO 操作函数: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 库对所有的函数模型也进行了统一。在 HAL 库中,支持三种编程模式:轮询模式、中断模式、DMA 模式(如果外设支持)。其分别对应如下三种类型的函数(以 ADC 为例):

HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADC_Stop(ADC_HandleTypeDef* hadc);

HAL_StatusTypeDef HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADC_Stop_IT(ADC_HandleTypeDef* hadc);

HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length);
HAL_StatusTypeDef HAL_ADC_Stop_DMA(ADC_HandleTypeDef* hadc);
1
2
3
4
5
6
7
8

其中,带 _IT 的表示工作在中断模式下;带 _DMA 的工作在 DMA 模式下(注意:DMA 模式下也是开中断的):什么都没带的就是轮询模式(没有开启中断的)。至于使用者使用何种方式,就看自己的选择了

此外,新的 HAL 库架构下统一采用宏的形式对各种中断等进行配置(原来标准外设库一般都是各种函数)。针对每种外设主要由以下宏:

  • __HAL_PPP_ENABLE_IT(HANDLE, INTERRUPT):使能一个指定的外设中断
  • __HAL_PPP_DISABLE_IT(HANDLE, INTERRUPT):失能一个指定的外设中断
  • __HAL_PPP_GET_IT (HANDLE, __ INTERRUPT __):获得一个指定的外设中断状态
  • __HAL_PPP_CLEAR_IT (HANDLE, __ INTERRUPT __):清除一个指定的外设的中断状态
  • __HAL_PPP_GET_FLAG (HANDLE, FLAG):获取一个指定的外设的标志状态
  • __HAL_PPP_CLEAR_FLAG (HANDLE, FLAG):清除一个指定的外设的标志状态
  • __HAL_PPP_ENABLE(HANDLE):使能外设
  • __HAL_PPP_DISABLE(HANDLE):失能外设
  • __HAL_PPP_XXXX (HANDLE, PARAM):指定外设的宏定义
  • __HAL_PPP_GET_IT_SOURCE (HANDLE, __ INTERRUPT __):检查中断源

# 三大回调函数

在 HAL 库的源码中,到处可见一些以 __weak开头的函数,而且这些函数,有些已经被实现了,比如:

__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
	/*Configure the SysTick to have interrupt in 1ms time basis*/
	HAL_SYSTICK_Config(SystemCoreClock/1000U);
	/*Configure the SysTick IRQ priority */
	HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority ,0U);
	/* Return function status */
	return HAL_OK;
}
1
2
3
4
5
6
7
8
9

有些则没有被实现,例如:

__weak void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(hspi);
  /* NOTE : This function should not be modified, when the callback is needed,the HAL_SPI_TxCpltCallback should be implemented in the user file
  */
}
1
2
3
4
5
6
7

所有带有 __weak 关键字的函数表示,就可以由用户自己来实现。如果出现了同名函数,且不带 __weak 关键字,那么连接器就会采用外部实现的同名函数。 通常来说,HAL 库负责整个处理和 MCU 外设的处理逻辑,并将必要部分以回调函数的形式给出到用户,用户只需要在对应的回调函数中做修改即可。HAL 库包含如下三种用户级别回调函数(PPP 为外设名):

  • 外设系统级初始化 / 解除初始化回调函数(用户代码的第二大部分:对于 MSP 的处理):HAL_PPP_MspInit() 和 HAL_PPP_MspDeInit

    例如:__weak void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)

    在 HAL_PPP_Init() 函数中被调用,用来初始化底层相关的设备(GPIOs, clock, DMA, interrupt)

  • 处理完成回调函数:HAL_PPP_ProcessCpltCallback*(Process 指具体某种处理,如 UART 的 Tx)

    例如:__weak void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)

    当外设或者 DMA 工作完成后时,触发中断,该回调函数会在外设中断处理函数或者 DMA 的中断处理函数中被调用

  • 错误处理回调函数:HAL_PPP_ErrorCallback

    例如:__weak void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)

    当外设或者 DMA 出现错误时,触发终端,该回调函数会在外设中断处理函数或者 DMA 的中断处理函数中被调用

绝大多数用户代码均在以上三大回调函数中实现。

# 总结

HAL 库结构中,在每次初始化前(尤其是在多次调用初始化前),先调用对应的反初始化(DeInit)函数是非常有必要的。 某些外设多次初始化时不调用返回会导致初始化失败。完成回调函数有多中,例如串口的完成回调函数有HAL_UART_TxCpltCallback 和 HAL_UART_TxHalfCpltCallback 等 (用户代码的第三大部分:对于上面第二点和第三点的各种回调函数的处理) 在实际使用中,发现 HAL 仍有不少问题,例如在使用 USB 时,其库配置存在问题

编辑 (opens new window)
上次更新: 2020/11/18, 13:11:00
HAL库函数
GPIO各种点亮LED

← HAL库函数 GPIO各种点亮LED→

Theme by Vdoing | Copyright © 2020-2025 Jack :) | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式