选择HAL库还是标准库呢?HAL库是趋势,标准库不再升级了,转HAL库是大势所趋。HAL库有优点,也有自身的不足,建议初学者还是从标准库入手。
标准库是单片机开发的基本库,它把"用寄存器实现的功能"写成一个函数或定义成宏,供用户使用。
HAL库把"有关系的功能"封装成一个函数,供用户调用,其目的是想隔绝用户直接干预寄存器操作,想法很好,但实际是行不通的,大部分是好的,另外提供了两个用户接口函数MspInit和Callback。
1、MspInit函数
HAL库写了很多个Init函数供用户调用,前提是必须要写好MspInit函数。同种功能硬件资源共用一个MspInit函数,所以所以这个MspInit函数必然就有许多分支,降低了程序的执行效率。下面举例说明:
//函数功能:使能TIMx时钟,设置中断优先级,使能TIMx中断
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM1)//初始化TIM1
{
__HAL_RCC_TIM1_CLK_ENABLE(); //使能TIM1时钟
HAL_NVIC_SetPriority(TIM1_BRK_UP_TRG_COM_IRQn, 0, 0); //设置中断优先级
HAL_NVIC_EnableIRQ(TIM1_BRK_UP_TRG_COM_IRQn);
//使能TIM1刹车事件,更新事件,触发事件,COM事件中断
}
if(htim->Instance == TIM3)//初始化TIM3
{
__HAL_RCC_TIM3_CLK_ENABLE(); //使能TIM3时钟
HAL_NVIC_SetPriority(TIM3_IRQn, 0, 0); //设置中断优先级
HAL_NVIC_EnableIRQ(TIM3_IRQn); //使能TIM3中断
}
}
用户通过增删MspInit函数中的内容,好像也很容易。在初始化时,设备用到的时钟,引脚复用,中断优先级管理等都需要写在这里。
2、Callback函数
HAL库写了很多个IRQHandler函数供用户调用,前提是必须要写好Callback函数。同种功能硬件资源共用一个Callback函数,所以所以这个Callback函数必然就有许多分支,降低了程序的执行效率。下面举例说明:
//函数功能:在定时器更新中断时,HAL_TIM_IRQHandler()会调用函数,处理用户程序
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM1)//处理定时器1
{
TIM1_LED_Toggle();
}
if(htim->Instance==TIM3)//处理定时器3
{
TIM3_LED_Toggle();
}
}
//函数功能:TIM1中断服务程序
void TIM1_BRK_UP_TRG_COM_IRQHandler(void)
{
TIM_HandleTypeDef TIM1_HandleStructure;
TIM1_HandleStructure.Instance=TIM1;
HAL_TIM_IRQHandler(&TIM1_HandleStructure);
}
//函数功能:TIM3中断服务程序
void TIM3_IRQHandler(void)
{
TIM_HandleTypeDef TIM3_HandleStructure;
TIM3_HandleStructure.Instance=TIM3;
HAL_TIM_IRQHandler(&TIM3_HandleStructure);
}
通过上面的举例,好像Callback函数也不像想象中的那么难写。但我们发现一个问题,这个IRQHandler函数传递是一个结构指针,所以必须有一个结构变量与之对应,所以在中断服务程序中申请一个局部结构变量,因为申请为全局变量确实有点浪费内存。官方给的例子基本都是全局变量,容易把人带入歧途。
3、HAL库的缺点
HAL库中的某些功能性函数,有可能不能满足需求,需要自己动手写,也是常有的事情。例如串口的函数库,有些函数脱离实际,如:
1)、串口接收函数,必须先知道接收多少个数据,才可以调用。
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
2)、串口发送函数,需要把UART_HandleTypeDef结构类型的变量定义为全局变量,才可以使用。这么设计,很消耗内存。
HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
如果你不按照HAL库的套路干,就需要对某些函数和宏定义进行重构。如果我们不了解底层,就只能按照它的意思干。
还是举例说明
#define _HAL_UART_SendByte(__INSTANCE__, __DATA__) (__INSTANCE__)->DR = ( (__DATA__) & (uint16_t)0x01FF );
//将(__DATA__)写入串口发送缓冲区
#define _HAL_UART_ReceiveByte(__INSTANCE__) ( (uint16_t)( (__INSTANCE__)->DR & (uint16_t)0x01FF) )
//读串口发送缓冲区
#define _HAL_UART_GET_FLAG(__INSTANCE__, __FLAG__) ( ( (__INSTANCE__)->SR & (__FLAG__) ) == (__FLAG__) )
//读串口中断标志位
//(__FLAG__)=UART_IT_RXNE,读"串口接收寄存器为非空时产生的中断标志位"
//(__FLAG__)=UART_IT_PE,读"串口奇偶校验错误产生的中断标志位"
//(__FLAG__)=UART_IT_ERR,读"帧错误、噪音错误和溢出错误时产生的中断标志位"
//(__FLAG__)=UART_IT_TXE时,读"串口发送寄存器为空产生的中断标志位"
//(__FLAG__)=UART_IT_TC时,读"发送完成产生的中断标志位"
#define _HAL_UART_CLEAR_FLAG(__INSTANCE__, __FLAG__) ( (__INSTANCE__)->SR = ~(__FLAG__) )
//清除串口中断标志位
//(__FLAG__)=UART_IT_RXNE,清除"串口接收寄存器为非空时产生的中断标志位"
//(__FLAG__)=UART_IT_PE,清除"串口奇偶校验错误产生的中断标志位"
//(__FLAG__)=UART_IT_ERR,清除"帧错误、噪音错误和溢出错误时产生的中断标志位"
//(__FLAG__)=UART_IT_TXE时,清除"串口发送寄存器为空产生的中断标志位"
//(__FLAG__)=UART_IT_TC时,清除"发送完成产生的中断标志位"
#define _HAL_UART_ENABLE_IT(__INSTANCE__, __INTERRUPT__) ((((__INTERRUPT__) >> 28U) == UART_CR1_REG_INDEX)? ((__INSTANCE__)->CR1 |= ((__INTERRUPT__) & UART_IT_MASK)): \
(((__INTERRUPT__) >> 28U) == UART_CR2_REG_INDEX)? ((__INSTANCE__)->CR2 |= ((__INTERRUPT__) & UART_IT_MASK)): \
((__INSTANCE__)->CR3 |= ((__INTERRUPT__) & UART_IT_MASK)))
//设置串口中断使能位
//(__INTERRUPT__)=UART_IT_RXNE,设置"串口接收寄存器为非空"时,使其产生中断
//(__INTERRUPT__)=UART_IT_PE,设置"串口奇偶校验错误"时,使其产生中断
//(__INTERRUPT__)=UART_IT_ERR,设置"帧错误、噪音错误和溢出错误"时,使其产生中断
//(__INTERRUPT__)=UART_IT_TXE时,设置"串口发送寄存器为空"时,使其产生中断
//(__INTERRUPT__)=UART_IT_TC时,设置"串口发送寄存器发送完成"时,使其产生中断
#define _HAL_UART_DISABLE_IT(__INSTANCE__, __INTERRUPT__) ((((__INTERRUPT__) >> 28U) == UART_CR1_REG_INDEX)? ((__INSTANCE__)->CR1 &= ~((__INTERRUPT__) & UART_IT_MASK)): \
(((__INTERRUPT__) >> 28U) == UART_CR2_REG_INDEX)? ((__INSTANCE__)->CR2 &= ~((__INTERRUPT__) & UART_IT_MASK)): \
((__INSTANCE__)->CR3 &= ~ ((__INTERRUPT__) & UART_IT_MASK)))
//设置串口中断不使能
//(__INTERRUPT__)=UART_IT_RXNE,设置"串口接收寄存器为非空"时,不使其产生中断
//(__INTERRUPT__)=UART_IT_PE,设置"串口奇偶校验错误"时,不使其产生中断
//(__INTERRUPT__)=UART_IT_ERR,设置"帧错误、噪音错误和溢出错误"时,不使其产生中断
//(__INTERRUPT__)=UART_IT_TXE时,设置"串口发送寄存器为空"时,不使其产生中断
//(__INTERRUPT__)=UART_IT_TC时,设置"串口发送寄存器发送完成"时,不使其产生中断
//重定义fputc函数
//函数功能:发送ch的值给USART2串口
int fputc(int ch, FILE *f)
{
_HAL_UART_SendByte(USART2, (unsigned char) ch);
while( _HAL_UART_GET_FLAG(USART2,USART_SR_TC)!= SET);
//等待发送完成标志位被置1
return ch;
}
//函数功能:串口2发送一个字节
void USART2_SendByte( unsigned char ch )
{
_HAL_UART_SendByte(USART2, (unsigned char) ch);
while( _HAL_UART_GET_FLAG(USART2,USART_SR_TC)!= SET);
//等待发送完成标志位被置1
}
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef GPIO_InitStructureure;
if(huart->Instance==USART2)
{
__HAL_RCC_USART2_CLK_ENABLE();//使能USART2外设时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); //使能GPIOA时钟
//串口引脚映射开始/
GPIO_InitStructureure.Pin = GPIO_PIN_0; //选择第0脚,PA0是为USART2_TX
GPIO_InitStructureure.Mode = GPIO_MODE_AF_PP; //复用功能推挽模式
GPIO_InitStructureure.Pull = GPIO_PULLUP; //引脚上拉被激活
GPIO_InitStructureure.Speed = GPIO_SPEED_FREQ_VERY_HIGH; //引脚速度为最高速
GPIO_InitStructureure.Alternate = GPIO_AF9_USART2; //将引脚复用为USART2
HAL_GPIO_Init(GPIOA, &GPIO_InitStructureure);
//根据GPIO_InitStructureure结构变量指定的参数初始化GPIOA的外设寄存器
//将PA0初始化为USART2_TX
GPIO_InitStructureure.Pin = GPIO_PIN_1; //选择第1脚,PA1是USART2_RX
GPIO_InitStructureure.Mode = GPIO_MODE_AF_PP; //复用功能推挽模式
GPIO_InitStructureure.Pull = GPIO_PULLUP; //引脚上拉被激活
GPIO_InitStructureure.Speed = GPIO_SPEED_FREQ_VERY_HIGH; //引脚速度为最高速
GPIO_InitStructureure.Alternate = GPIO_AF9_USART2; //将引脚复用为USART2
HAL_GPIO_Init(GPIOA, &GPIO_InitStructureure);
//根据GPIO_InitStructureure结构变量指定的参数初始化GPIOA的外设寄存器
//将PA1初始化为USART2_RX
//串口引脚映射结束/
HAL_NVIC_SetPriority(USART2_IRQn, 0x01, 0);
//设置串口2中断优先级为0x01,0无意义
}
}
//函数功能:
//PA0是为USART2_TX,PA1是USART2_RX
//中断优先级为0x01
//波特率为115200,数字为8位,停止位为1位,无奇偶校验,允许发送和接收数据,只允许接收中断,并使能串口
void USART2_Init(uint32_t baudrate)
{
UART_HandleTypeDef UART_HandleStructureure;
UART_HandleStructureure.Instance = USART2; //接口为USART2
UART_HandleStructureure.Init.BaudRate = baudrate; //波特率为115200bps
UART_HandleStructureure.Init.WordLength = UART_WORDLENGTH_8B; //串口字长度为8
UART_HandleStructureure.Init.StopBits = UART_STOPBITS_1; //串口停止位为1位
UART_HandleStructureure.Init.Parity = UART_PARITY_NONE; //串口无需奇偶校验
UART_HandleStructureure.Init.HwFlowCtl = UART_HWCONTROL_NONE; //串口无硬件流程控制
UART_HandleStructureure.Init.Mode = UART_MODE_TX_RX; //串口工作模式为发送和接收模式
UART_HandleStructureure.AdvancedInit.AdvFeatureInit=UART_ADVFEATURE_NO_INIT;//不使用自动波特率
// UART_HandleStructureure.AdvancedInit.AdvFeatureInit=UART_ADVFEATURE_AUTOBAUDRATE_INIT;//使用自动波特率配置
// UART_HandleStructureure.AdvancedInit.AutoBaudRateEnable=UART_ADVFEATURE_AUTOBAUDRATE_ENABLE;//自动波特率使能
// UART_HandleStructureure.AdvancedInit.AutoBaudRateMode=UART_ADVFEATURE_AUTOBAUDRATE_ONSTARTBIT;//自动波特率模式
HAL_UART_Init(&UART_HandleStructureure);
//根据UART_HandleStructureure型结构初始化USART2
__HAL_UART_ENABLE_IT(&UART_HandleStructureure, UART_IT_RXNE);
开启串口接收中断
//串口接收数据时,使能"接收数据寄存器不为空"则产生中断(位RXNE=1)
//Enable the UART Data Register not empty Interrupt
__HAL_UART_DISABLE_IT(&UART_HandleStructureure, UART_IT_TXE);
//串口发送数据时,不使能"串口发送数据寄存器为空"产生中断(位TXE=0)
//Disable the UART Transmit Complete Interrupt
__HAL_UART_DISABLE_IT(&UART_HandleStructureure,UART_IT_TC);
//串口发送数据时,不使能"串口发送完成"产生中断(位TC=1)
HAL_NVIC_EnableIRQ(USART2_IRQn);
//使能串口2中断
//USART2_IRQn表示中断源为串口2
}
放弃了HAL_UART_Receive_IT()之后,HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)也就废了。直接用自己的,不用Callback函数,如下:
//函数功能:串口2中断服务程序
void USART2_IRQHandler(void)
{
uint8_t RX_temp;
(void)RX_temp;//防止RX_temp不使用而产生警告
if( _HAL_UART_GET_FLAG(USART2,USART_SR_RXNE) )
{//在串口状态寄存器中,发现RXNE=1,且串口控制寄存器1允许接收数据
RX_temp =_HAL_UART_ReceiveByte(USART2);//读串口数据
_HAL_UART_CLEAR_FLAG(USART2,USART_SR_RXNE);
if(RX_temp=='1' && USART2_RX_Time_Count==0) USART2_RX_Time_Count = 1;
//如果接收到帧头为变频器地址为0x1,则启动USART2接收时间计数器
if(USART2_RX_Time_Count > 0)
{
USART2_RX_Time_Count = 1;//设置USART2接收时间计数器为1;
USART2_RX_Buffer[USART2_RX_Buffer_Load_Index] = RX_temp;//保存接收到的新数据
USART2_RX_Buffer_Load_Index++;
if(USART2_RX_Buffer_Load_Index>=USART2_RX_Buffer_Size) USART2_RX_Buffer_Load_Index=1;//防止USART2_RX_Buffer[]溢出
}
//软件先读"串口状态寄存器(USART_SR)",然后再读"串口数据寄存器USART_DR",就可以将ORE位(Overrun错误标志)清零;
//软件先读"串口状态寄存器(USART_SR)",然后再读"串口数据寄存器USART_DR",就可以将NE位(噪声错误标志)清零;
//软件先读"串口状态寄存器(USART_SR)",然后再读"串口数据寄存器USART_DR",就可以将FE位(帧错误标志)清零;
//软件先读"串口状态寄存器(USART_SR)",然后再读"串口数据寄存器USART_DR",就可以将PE位(奇偶校验值错误)清零;
//软件读"串口数据寄存器USART_DR",就可以将RXNE位清零
}
}
5、学习HAL库的方法:
1)、要了解了HAL库对程序的管理方法,这个只能靠自己去看库中的函数了。
2)、熟悉结构变量中各个成员的意义。
3)、熟悉HAL库的函数。
4)、多测试,多看高手的程序。如果刚入门,可能会被半桶水的人带偏了。例如:官方的程序,他们的思路可能不符合实际使用,但是用来测试还是可以的,我们需要把它修改实际需要的那种代码。
不建议初学者,直接学习HAL库,还是从标准库学习比较好。只要你有兴趣,学习都是一样的。
有时没办法,不能按照HAL库思路干,还是需要涉及到寄存器操作。
6、学习总结
标准库转HAL库,HAL里面的初始化函数,需要重点掌握,虽然申请的结构变量占很多空间,但是初始化完后,就可以释放了,但串口有点特殊,最好自己写。
1)、因为内存容量有限,不建议将初始化结构参数申请为全局变量。由于没有将结构参数设为全局变量,有些函数或宏需要重写,以满足实际需求。如:
读中断标志位
清除中断标志位
使能中断
不使能能中断
串口发送
串口接收
等相关的函数需要重构。
2)、带MspInit的函数,建议写在需要初始化的函数中。HAL采用同一管理,这个MspInit的函数只有一个,所以很庞大,而且还要做判断,效率极低。
3)、带CallBack的函数只有一个,由于没有食使用全局结构变量,所以不能用。而申请为全局结构变量,又很浪费内存。
4)、为了方便移植,中断不再使用同一个CallBack函数了。
7、HAL库有自己的优点:
1)、把所有的硬件驱动都写好了,我们要从中扣出有价值的函数,使编程更加灵活。
2)、初始化函数只有一个。
不管是用什么库,里面的结构参数知道怎么赋值,干啥用的,基本就上路了。
这是我个人的体会,觉得这么做,可以提高程序的执行效率,其次是节省内存,更主要的是功能模块化,易读,易移植,易交流。程序短小。
程序短小精悍,是每个程序员的追求。
芯片内存不是问题,HAL库的程序管理方法,可以值得借鉴,除了那些不切合实际的库函数外。经得起实践检验的方法就是最好的方法,不必死板硬套。