选择HAL库还是标准库

选择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库的程序管理方法,可以值得借鉴,除了那些不切合实际的库函数外。经得起实践检验的方法就是最好的方法,不必死板硬套。

 

你可能感兴趣的:(产品研发,单片机,stm32,嵌入式硬件,PY32F003F18P,HAL库,经验分享)