原帖 | Jack | 2025-03-11 21:20 | 👍0 | 阅读约1
HAL库与标准库的区别
1. 句柄
标准库中,要初始化一个外设,首先要初始化他们的各个寄存器。在标准库中,这些操作都是利用固件库结构体变量+固件库Init函数实现的:
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
可以看到,要初始化一个串口,需要对六个位置进行赋值,然后引用Init函数,并且USART_InitStructure并不是一个全局结构体变量,而是只在函数内部的局部变量,初始化完成后,这个结构体就失去了作用。HAL库则不然,同样是初始化USART结构体变量,我们要定义为全局变量。
extern UART_HandleTypeDef UART1_Handler;
右键查看结构体成员:
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;
与标准库不同的是,该成员不仅包含了之前标准库就有的六个成员,还包括了采样、数据缓存、数据指针等相关变量。这个UART_HandleTypeDef就称为串口的句柄(对外设资源的封装,通过它可以方便的配置和操作外设),只需配置一次,整个项目任意处都可以传入它的信息,逻辑上确实比标准库合理。
2. MSP函数
MCU Specific Package :MCU 具体方案
标准库中,我们要初始化一个串口,首先要设置和MCU无关的东西,例如波特率、停止位等,这些参数设置和具体的MCU没有任何关系。而一个串口收发功能需要一个MCU来承载,比如具体方案是PA9发送、PA10接收,另有一个引脚是接地的,这些是和MCU牢牢相关的。
HAL库就是上面这个逻辑,先写和MCU无关的那些理论参数,再写和MCU有关的具体方案。
HAL_USART_Init()—>HAL_USART_MspInit()
比如下面这个usart.c中的代码,这个代码是由STM32 IDE生成的,只需要点点点就可以自动生成如下代码:
/* USART1 init function */
void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART1_Init 2 */
/* USER CODE END USART1_Init 2 */
}
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE END USART1_MspInit 0 */
/* USART1 clock enable */
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USER CODE BEGIN USART1_MspInit 1 */
/* USER CODE END USART1_MspInit 1 */
}
}
HAL_USART_MspInit()函数作为回调,被HAL_USART_Init()所调用,当我们需要移植程序到另一款STM32芯片上时,只需要修HAL_USART_MspInit()即可,而不需要修改HAL_USART_Init()函数。可见,HAL库相对于标准库多了MSP函数之后,移植性非常强。
3. Callback(回调)函数
在标准库中,串口中断了以后,我们要先在中断中判断是否接收中断,然后读出数据,顺便清除中断标志位,然后对数据进行处理。
void DEBUG_USART_IRQHandler(void)
{
uint8_t ucTemp;
if (USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE) != RESET)
{
ucTemp = USART_ReceiveData(DEBUG_USARTx); // 读取数据寄存器,自动清除RXNE标志位
USART_SendData(DEBUG_USARTx, ucTemp); // 回发数据
}
}
而HAL库中,进入串口中断后,直接由HAL库中断函数进行托管:
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&UART1_Handler); //调用HAL库中断处理公用函数
/***************省略无关代码****************/
}
HAL_UART_IRQHandler这个函数完成了判断是哪个中断(接收?发送?或其他?),然后读出数据,保存到缓存区,顺便清除中断标志位等等操作。
比如我提前设置了串口每接收5个字节,我就要对这5个字节进行处理。首先,我定义了一个串口接收缓存区:
*HAL库使用的串口接收缓冲,处理逻辑由HAL库控制,接收完这个数组就会调用HAL_UART_RxCpltCallback进行处理这个数组*/
/*RXBUFFERSIZE=5*/
u8 aRxBuffer[RXBUFFERSIZE];
在句柄里设置好缓存区的地址、缓存大小:
/*该代码在HAL_UART_Receive_IT函数中,初始化时会引用*/
huart->pRxBuffPtr = pData;//aRxBuffer
huart->RxXferSize = Size;//RXBUFFERSIZE
huart->RxXferCount = Size;//RXBUFFERSIZE
则在接收数据中,每接收完5个字节,HAL_UART_IRQHandler才会执行一次Callback函数:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
在这个callback回调函数中,我们只需要对这接收到的5个字节(在aRxBuffer[]中)进行处理即可,不用管清除中断标志位的事。所以说Callback函数是一个应用层代码的函数,我们在一开始只设置句柄里面的各个参数,然后就等着HAL库把自己安排好的代码送到手中就可以了~
如此,可见HAL库的可移植性之强大,并且用户可以完全不去理会底层各个寄存器的操作,代码也很有逻辑性。只不过相比于标准库而言,代码量大、编译速度慢,效率略低。但在未来,这些缺点会被弥补,因为STM已经不再维护标准库,all in HAL库。
总的来说,它们的主要区别就在于句柄不同,比如标准库中的句柄会是临时的,HAL中就是永存的,会封装外设所有关键信息。这些因素最终导致两个库再体积上,兼容性,中间件支持,以及代码运行效率上都有区别。
相关笔记