前言:
ST官方从2017年下半年开始就不再维护升级标准库,转而推广HAL库。到2019年,HAL库仍不够成熟,其原因有以下:
1. HAL库的配套指导文档,特别是中文的使用手册文档欠缺得很厉害,除了野火在2017年上半年出了一套STM32F4的HAL库教程,就几乎没有任何由ST官方协助完成的教程了。
2. HAL库全称是Hardware Abstraction Layer(抽象印象层),ST初衷是减少开发人员前期在底层驱动上所消耗的时间,这样就能很快的转到应用层代码的编写上,以达到敏捷开发的要求。
而HAL库的第一个不成熟就在于,MCU芯片需要的是芯片级的驱动库,不是板卡级的驱动库,这个大方向就错了。特别是单片机产品的一个根本性质就是随意性大,可任意配置硬件和软件,底层的驱动和上层的应用是没有被封装剥离开的;且产品一旦成熟,就很难再有大改动。
ST的意愿是好的,但脱离了实际。他们的意愿是搞这个HAL库来协助加快单片机研发速度,设备商的产品交付得就越快,这样他们的出货量就越大。ST公司这种希望把MCU的开发,变成像手机APP那样的敏捷开发,是脱离了单片机产品的实际,这是只从自己的角度看问题。
HAL库的第二个不成熟是它的函数相对于标准库缺少得太多,现有的这些函数的接口对于做C51这类单片机出身的单片机工程师很不友好,仅用HAL库无法把底层驱动写得具有更高的自由度。
例如下面我要补充的单独设置某一通道的PWM占空比函数,在标准库中有,但是在HAL库中就没有。另外获取指定ADC通道的AD数值的函数也没有。
更让人吐槽的是,有的函数甚至返回值只有HAL_OK,而没有HAL_ERROR,HAL_BUSY,HAL_TIMEOUT这些状态值,那么这些返回值只有唯一状态的函数,要这样的返回值有何意义呢?
特别是有的HAL库函数其实是一个宏定义的计算,其返回值是0或非0(并非FALSE或TRUE),而这个非0就是有很多个值了,那你到底是哪一个值呢,当前的HAL库没有对这个非0进行判断,那么就会出错,搞出歧义来。
下面来看这段代码:
- /** @brief Checks whether the specified UART interrupt has occurred or not.
- * @param __HANDLE__ specifies the UART Handle.
- * UART Handle selects the USARTx or UARTy peripheral
- * (USART,UART availability and x,y values depending on device).
- * @param __IT__ specifies the UART interrupt source to check.
- * This parameter can be one of the following values:
- * @arg UART_IT_CTS: CTS change interrupt (not available for UART4 and UART5)
- * @arg UART_IT_LBD: LIN Break detection interrupt
- * @arg UART_IT_TXE: Transmit Data Register empty interrupt
- * @arg UART_IT_TC: Transmission complete interrupt
- * @arg UART_IT_RXNE: Receive Data register not empty interrupt
- * @arg UART_IT_IDLE: Idle line detection interrupt
- * @arg UART_IT_ERR: Error interrupt
- * @retval The new state of __IT__ (TRUE or FALSE).
- */
- #define __HAL_UART_GET_IT_SOURCE(__HANDLE__, __IT__) (((((__IT__) >> 28U) == UART_CR1_REG_INDEX)? (__HANDLE__)->Instance->CR1:(((((uint32_t)(__IT__)) >> 28U) == UART_CR2_REG_INDEX)? \
- (__HANDLE__)->Instance->CR2 : (__HANDLE__)->Instance->CR3)) & (((uint32_t)(__IT__)) & UART_IT_MASK))
这个宏定义的函数:__HAL_UART_GET_IT_SOURCE 是用于获取串口发生中断时的中断源的,由函数名就可看出来。这个很好理解。再看它的描述:@retval The new state of __IT__ (TRUE or FALSE). 它的返回值是 __IT__ 最新状态,这个状态的解释是TRUE或FALSE。于是,我们就会这样去做判断:
- if(__HAL_UART_GET_FLAG(&huart5, UART_FLAG_TXE) == TRUE && __HAL_UART_GET_IT_SOURCE(&huart5, UART_IT_TXE) == TRUE) //TXE模式的发送中断
- {
- __HAL_UART_CLEAR_FLAG(&huart5, UART_FLAG_TXE);
- }
代码一编译,就有报错说没有定义TRUE,然后心里不免会冒出一句:shirt。然后几经折腾,发现可以自己定义一个TRUE和FALSE,或者这一句可以改成:
- if(__HAL_UART_GET_FLAG(&huart5, UART_FLAG_TXE) == SET && __HAL_UART_GET_IT_SOURCE(&huart5, UART_IT_TXE) == SET) //TXE模式的发送中断
- {
- __HAL_UART_CLEAR_FLAG(&huart5, UART_FLAG_TXE);
- }
然后编译能通过了,但是程序一跑,你会发现明明有了发送中断,但是这里的判断就是进不去,于是又是一阵的懵逼,心里把shirt变成了fuck。再几经检查,发现 __HAL_UART_GET_IT_SOURCE(&huart5, UART_IT_TXE) 这一句的返回值实际是0x00000080,而并非SET(SET的定义是1,实质是0x01)。
其实这还不算什么,如果串口同时有 TC 中断 和 LBD中断,你会发现用 __HAL_UART_GET_IT_SOURCE 去计算出来的值都是0x00000040,这就无法简单用if去判断是哪里在中断了,相信很多人都会爆粗口了。其实在标准库中也有类似的情况,但是标准库已经被大家熟悉了,一打听、一百度就能找到解决办法。而HAL库的使用文档少之又少,反倒是耽误了大家的时间。
我相信大多搞单片机的程序员对这个搞了三四年的HAL库都不屑,依据是看看各个论坛上有多少关于HAL库的帖子就能知道用HAL库的人有多少,特别是2018年后新出的HAL库,其相关帖子更是几乎没有。
而相对稳定得多的、但仍有一个重大缺陷的标准库却仍是绝大多数人的选择。我这里说的缺陷就是标准库对硬件IIC的支持很不好,有人用它解决了EEROM的读写,但是像更多的器件,比如SHT2x传感器,iAQ-core C型传感器(本人亲自用过的器件)用标准库来驱动硬件IIC就不能实现,而HAL库却可以(2019年11月最新版),这一点还是要给HAL库点赞。
既然HAL库能解决,我相信升级的标准库也能解决,但是为何ST就不愿意去维护升级呢?
总结一下吧:
ST公司搞这个HAL库,初衷是减少开发人员前期在底层驱动上所消耗的时间,让大家能敏捷开发,增加产品的出货速度,相应的他们也会受益。但是我个人认为这是一个事与愿违的案例,单片机程序员该做什么关你ST公司什么事,你去试图替代别人的工作干什么呢,是想要我们这些做底层驱动的程序员都失业么?单片机的应用五花八门、千变万化地,是弄一个HAL库来抽象与封装就能包办的么?难道不知道写C代码的程序员本就是做这样琐碎的事情的吗?
早在2017年10月参加的在成都举办的那次ST公司的开发者大会时,我在会上就质疑和批评:为何标准库没有弄得尽善尽美,而又着急弄什么HAL库。当年(2013年时吧)标准库也是一大堆问题,如今好不容易完善了很多,又放弃它了,不愿百尺竿头,更进一步。说好的工匠精神呢?
当年,诺基亚公司不愿作出改变,死认着他们的塞班搞,结果他们被淘汰了,而如今,另一个想作出改变的,一心想进入移动端市场的微软,其 windows10 mobile却在前不久宣布被放弃了。所以,变与不变是需要智慧的,套用陈道明那句广告词:简约而不简单,取舍之间,彰显智慧。
吐槽完毕,上代码。
补充代码
第1个:按通道,调整PWM的占空比
- HAL_StatusTypeDef HAL_TIMx_SetCompare(TIM_HandleTypeDef *htim, uint32_t Channel, uint16_t Compare);
- HAL_StatusTypeDef HAL_TIMx_SetCompare(TIM_HandleTypeDef *htim, uint32_t Channel, uint16_t Compare)
- {
- assert_param(IS_TIM_CCXN_INSTANCE(htim->Instance, Channel));
- switch (Channel)
- {
- case TIM_CHANNEL_1:
- {
- htim->Instance->CCR1 = Compare;
- break;
- }
- case TIM_CHANNEL_2:
- {
- htim->Instance->CCR2 = Compare;
- break;
- }
- case TIM_CHANNEL_3:
- {
- htim->Instance->CCR3 = Compare;
- break;
- }
- case TIM_CHANNEL_4:
- {
- htim->Instance->CCR4 = Compare;
- break;
- }
- default:
- break;
- }
- return HAL_OK;
- }
函数的声明是放在文件stm32f1xx_hal_tim_ex.h中的,
函数的定义是放在文件stm32f1xx_hal_tim_ex.c中的。
入口参数:TIM_HandleTypeDef *htim, 定时器,HAL库自带,其具体描述请见HAL库中stm32f1xx_hal_tim.h的描述。
uint32_t Channel, PWM通道,HAL库自带的宏定义中有,其具体描述请见HAL库中stm32f1xx_hal_tim.h的描述。
uint16_t Compare,占空比值,存入定时器的寄存器ccr1的值
返回值: HAL_OK;
具体用法(代码示例):
- TIM_HandleTypeDef htim3;
- HAL_TIMx_SetCompare(&htim3,TIM_CHANNEL_4,200);
这个函数本人写得简单,并没有对占空比的范围做判断,返回值也只有 HAL_OK,有完善的小伙伴可自行修改。
第2个:按通道,获取ADC的采样值
- uint16_t Get_AdcValue(ADC_HandleTypeDef* hadc,ADC_ChannelConfTypeDef* sConfig, uint32_t ADC_Channel);
- uint16_t Get_AdcValue(ADC_HandleTypeDef* hadc,ADC_ChannelConfTypeDef* sConfig, uint32_t ADC_Channel)
- {
- uint16_t val;
- sConfig->Channel = ADC_Channel;
- if(HAL_ADC_ConfigChannel(hadc,sConfig) != HAL_OK)
- {
- Error_Handler();
- }
- HAL_ADC_Start(hadc);
- HAL_ADC_PollForConversion(hadc, 10); //等待转换完成,第二个参数表示超时时间,单位ms
- if(HAL_IS_BIT_SET(HAL_ADC_GetState(hadc), HAL_ADC_STATE_REG_EOC))
- {
- val=HAL_ADC_GetValue(hadc);
- }
- HAL_ADC_Stop(&hadc1);
- return val;
- }
函数的声明可以放在stm32f1xx_hal_adc_ex.h中,也可以放在自己建立的接口层的头文件中,函数自然是放在对应的C文件中。
入口参数:TIM_HandleTypeDef *adc, 模数转换器,HAL库自带,其具体描述请见HAL库中stm32f1xx_hal_adc.h的描述。
ADC_ChannelConfTypeDef* sConfig, 指定要配置为ADC常规组的通道,HAL库自带,其具体定义方式和描述请见HAL库中stm32f1xx_hal_adc.h的描述。
uint32_t ADC_Channel,ADC的通道,HAL自带,其取值和具体描述请见HAL库中stm32f1xx_hal_adc.h的描述。
返回值: ADC采样值;
具体用法:
- ADC_HandleTypeDef hadc1;
- ADC_ChannelConfTypeDef sConfigADC1 = {0};
- INT16U adcVal=0; //ADC采样值
- adcVal = Get_AdcValue(&hadc1, &sConfigADC1, ADC_CHANNEL_10);
第3个:串口中断,仿标准库那种对串口中断的处理,以串口5举例
- void UART5_IRQHandler(void)
- {
- uint8_t UART5Res;
- HAL_UART_IRQHandler(&huart5);
- if( __HAL_UART_GET_FLAG(&huart5, UART_FLAG_ORE) != RESET && __HAL_UART_GET_IT_SOURCE(&huart5, UART_IT_ERR) != RESET ) //注意!不能使用 ( __HAL_UART_GET_FLAG(&huart5, UART_FLAG_ORE) == SET)来判断,也不能使用__HAL_UART_GET_IT_SOURCE(&huart5, UART_IT_ERR) == SET 来判断,只能是!= RESET
- {
- __HAL_UART_CLEAR_FLAG(&huart5, UART_FLAG_ORE); //清除溢出中断
- UART5Res = (uint8_t)(huart5.Instance->DR & (uint16_t)0x01FF); //读取出来扔掉
- //User code begin
- //自定义代码
- //User code end
- }
- if(__HAL_UART_GET_FLAG(&huart5, UART_FLAG_RXNE) != RESET && __HAL_UART_GET_IT_SOURCE(&huart5, UART_IT_RXNE) != RESET) //接收中断
- {
- __HAL_UART_CLEAR_FLAG(&huart5,UART_FLAG_RXNE);
- UART5Res = (uint8_t)(huart5.Instance->DR & (uint16_t)0x01FF);
- //User code begin
- //自定义代码
- //User code end
- }
- if(__HAL_UART_GET_FLAG(&huart5, UART_FLAG_TXE) != RESET && __HAL_UART_GET_IT_SOURCE(&huart5, UART_IT_TXE) != RESET) //TXE模式的发送中断
- {
- __HAL_UART_CLEAR_FLAG(&huart5, UART_FLAG_TXE);
- //User code begin
- //自定义代码
- //User code end
- }
- else if( __HAL_UART_GET_FLAG(&huart5, UART_FLAG_TC) != RESET && __HAL_UART_GET_IT_SOURCE(&huart5, UART_IT_TC) != RESET ) //判断是否是TC发送中断
- {
- __HAL_UART_CLEAR_FLAG(&huart5, UART_FLAG_TC);
- //User code begin
- //自定义代码
- //User code end
- }
- }
函数void UART5_IRQHandler(void)是HAL库自带的,直接调用就OK了。这个函数是改写,不是新增的,它仿照的是正点原子和野火库标准库教程中的那种用法,学过的人一看就明白。
这里有有一个特别、特别、特别要注意的地方:不能使用 __HAL_UART_GET_IT_SOURCE(&huart5, UART_IT_RXNE) == SET 来判断,只能是: != RESET
用SET和RESET这个梗,是连正点原子和野火的标准库教程中都没有提到的,过去标准库的USART_GetITStatus(UART5, USART_IT_RXNE) != RESET,也是用!= RESET 来判断,现在HAL库也只能这样 __HAL_UART_GET_IT_SOURCE(&huart5, UART_IT_TXE) != RESET 来判断。
因为 __HAL_UART_GET_IT_SOURCE 和 __HAL_UART_GET_FLAG 的返回值是 0 或 非0(0x00000080,0x00000040 之类的),用SET去判断会出错(SET在内存中实际的值是0x01)。
第4个:片内Flash的读保护。
什么是片内Flash,什么是读保护,请自行上网搜索或者看看正点原子、野火他们出的教程。这里只在代码的注释中讲一下下(有三行那段注释)。
代码只有两个函数:
- void STMFlash_EN_ReadProtect(void);//启动读保护
- void STMFlash_DIS_ReadProtect(void);//关闭读保护
- /**************************************************************************
- STM32可以对存储在flash上的程序进行读保护.
- 启动读保护后,用户就不能再读写程序了.
- 所以,在烧写程序之前,需要程序调用关闭读保护.关闭读保护后,会自动清空flash上的程序
- **************************************************************************/
- void STMFlash_EN_ReadProtect(void) //启动读flash保护
- {
- FLASH_OBProgramInitTypeDef OBInit;
- __HAL_FLASH_PREFETCH_BUFFER_DISABLE();
- HAL_FLASHEx_OBGetConfig(&OBInit);
- if(OBInit.RDPLevel == OB_RDP_LEVEL_0)
- {
- OBInit.OptionType = OPTIONBYTE_RDP;
- OBInit.RDPLevel = OB_RDP_LEVEL_1;
- HAL_FLASH_Unlock();
- HAL_FLASH_OB_Unlock();
- HAL_FLASHEx_OBProgram(&OBInit);
- HAL_FLASH_OB_Lock();
- HAL_FLASH_Lock();
- }
- __HAL_FLASH_PREFETCH_BUFFER_ENABLE();
- }
- void STMFlash_DIS_ReadProtect(void) //关闭读flash保护
- {
- FLASH_OBProgramInitTypeDef OBInit;
- __HAL_FLASH_PREFETCH_BUFFER_DISABLE();
- HAL_FLASHEx_OBGetConfig(&OBInit);
- if(OBInit.RDPLevel == OB_RDP_LEVEL_0)
- {
- OBInit.OptionType = OPTIONBYTE_RDP;
- OBInit.RDPLevel = OB_RDP_LEVEL_1;
- HAL_FLASH_Unlock();
- HAL_FLASH_OB_Unlock();
- HAL_FLASHEx_OBProgram(&OBInit);
- HAL_FLASH_OB_Lock();
- HAL_FLASH_Lock();
- }
- __HAL_FLASH_PREFETCH_BUFFER_ENABLE();
- }
这两个函数都是基于HAL库编写的,其中的结构体、宏定义函数都是HAL库自带的,请与你当前所使用的HAL库比对与修改。我这两个函数也是参考网友提供的源代码进行了一丁点儿的修改完成的,所以不要怕,不难的。
函数的声明请放在一个头文件中,这样通过包含这个头文件,就能在所有的C文件中使用。函数的定义(即具体代码)请放在你对片内Flash操作的中间层的C文件中。
第5个:关闭、开启总中断。
这个操作算是较为高档的应用了,主要用于RTOS,或者不能被任何中断打断的操作。比如有一种Clock Stretching 形式的IIC总线,它在使用中,Master会有一小段时间把SCLK的线的控制权交给Slaver,如果此时发生中断,且在中断中处理的时间较长,退出中断后,就会导致Clock Stretching IIC通讯的失败。另外,在很多产品中,有时要对片内Flash进行操作,那么读写Flash的过程就最好关闭总中断。在我做过的批量产品中,这样做过,设备的工作很稳定。
所以开关总中断就是一个很重要的操作,过去标准库的代码不能直接使用,需要进行修改。现在附上HAL库的操作代码。
请为开关总中断单独写一个C文件和一个H文件(头文件)。
⑴ C文件的代码如下:
- #define _XYZ_CRITICALSEG_C_
- #include "xyz_CriticalSeg.h"
- void STM32_EnableIRQ( void )
- {
- if(IRQIndex > 0)
- {
- IRQIndex --;
- }
- if(0 == IRQIndex)
- {
- __set_PRIMASK(IRQStatus[IRQIndex]); //注意,此处是修改过的,这是HAL库的
- }
- }
- void STM32_DisableIRQ( void )
- {
- if(IRQIndex < CRITICAL_DEEP)
- {
- IRQIndex ++;
- //可以写入以下代码进行测试关键区域深度,避免出现溢出情况,从而调整CRITICAL_DEEP大小
- if(maxIndex < IRQIndex)
- {
- maxIndex = IRQIndex;
- }
- __asm("CPSID I");
- }
- else//中断关键区域溢出
- {
- while(1);
- }
- }
- void CriticalSeg_init(void)
- {
- IRQIndex = 0;
- }
__set_PRIMASK 这个宏定义函数是在cmsis_armcc.h中,所以我们这个开关总中断的C文件中一定要包含这个头文件,或者如我下面的操作,把它放在开关总中断的H文件中。
⑵ H文件的代码如下:
- #ifndef _XYZ_CRITICALSEG_H_
- #define _XYZ_CRITICALSEG_H_
- #include "cmsis_armcc.h" //HAL库工程的一定要包含这个头文件
- #ifdef _XYZ_CRITICALSEG_C_
- #define CRITICALSEG_EXT
- #else
- #define CRITICALSEG_EXT extern
- #endif
- #ifdef _XYZ_CRITICALSEG_C_
- INT16U maxIndex = 0;
- #endif
- #define OS_ENTER_CRITICAL STM32_DisableIRQ()
- #define OS_EXIT_CRITICAL STM32_EnableIRQ()
- //关键区域嵌套深度最大值
- #define CRITICAL_DEEP 25
- CRITICALSEG_EXT INT32U IRQStatus[CRITICAL_DEEP];
- CRITICALSEG_EXT INT16U IRQIndex;
- CRITICALSEG_EXT void STM32_EnableIRQ( void );
- CRITICALSEG_EXT void STM32_DisableIRQ( void );
- CRITICALSEG_EXT void CriticalSeg_init(void);
- #endif
注意上面第4行代码,在HAL库的工程中,一定要有#include "cmsis_armcc.h",否者不能使用__set_PRIMASK
⑶ 修改cmsis_armcc.h文件
在这个文件的开头添加 #include "stdint.h" ,如图:
包含这个stdint.h是因为:cmsis_armcc.h文件中没有对uint32_t的定义。
(看不懂什么是对uint32_t 的定义?不好意思,我也只能帮这么多,请再好好学一遍C语言的基础内容吧)