7.3 HAL库框架结构
这一节我们将简要分析一下HAL驱动文件夹下的驱动文件,帮助大家快速认识HAL库驱动的构成,旨在帮大家快速认识HAL库的函数的一些常用形式,帮助大家到遇到HAL库时能根据名字大致推断该函数的用法,本部分不要求大家在学习完本节后完全记住。
7.3.1 HAL库文件夹结构
HAL库头文件和源文件在STM32Cube固件包的STM32F1xx_HAL_Driver文件夹中,打开该文件夹,如图7.3.1.1所示。
图7.3.1.1 STM32F1xx_HAL_Driver文件夹目录结构
STM32F1xx_HAL_Driver文件夹下的Src(Source的简写)文件夹存放是所有外设的驱动程序源码,Inc(Include的简写)文件夹存放的是对应源码的头文件。Release_Notes.html是HAL库的版本更新信息。最后三个是库的用户手册,这个需要可以去熟悉一下,查阅起来很方便。
打开Src和Inc文件夹,大家会发现基本都是stm32f1xx_hal_和stm32f1xx_ll_开头的.c和.h文件。刚学HAL库的朋友可能会说,stm32f1xx_hal_开头的是HAL库,我能理解。那stm32f1xx_ll_开头的文件又是什么?这就告诉大家,stm32f1xx_ll_开头的文件是LL库。
7.3.2 HAL库文件介绍
HAL库关键文件介绍如下表7.3.2.1所示,表中ppp代表任意外设。
以上是HAL库最常见的文件的列表,在Src/Inc下面还有Legacy文件夹,用于特殊外设的补充说明。我们的教程中用到的比较少,这里不展开描述。
不止文件命名有一定规则,stm32f1xx_hal_ppp (c/h)中的函数和变量命名也严格按照命名规则,如表7.3.2.2所示的命名规则在大部分情况下都是正确的:
对于HAL的API函数,常见的有以下几种:
l 初始化/反初始化函数:HAL_PPP_Init(),HAL_PPP_DeInit()
l 外设读写函数:HAL_PPP_Read(),HAL_PPP_Write(),HAL_PPP_Transmit(),HAL_PPP_Receive()
l 控制函数:HAL_PPP_Set(),HAL_PPP_Get ().
l 状态和错误:HAL_PPP_GetState (),HAL_PPP_GetError ().
HAL库封装的很多函数都是通过定义好的结构体将参数一次性传给所需函数,参数也有一定的规律,主要有以下三种:
• 配置和初始化用的结构体:
一般为PPP_InitTypeDef或PPP_ ConfTypeDef的结构体类型,根据外设的寄存器设计成易于理解和记忆的结构体成员。
• 特殊处理的结构体
专为不同外设而设置的,带有“Process”的字样,实现一些特异化的中间处理操作等。
• 外设句柄结构体
HAL驱动的重要参数,可以同时定义多个句柄结构以支持多外设多模式。HAL驱动的操作结果也可以通过这个句柄获得。有些HAL驱动的头文件中还定义了一些跟这个句柄相关的一些外设操作。如用外设结构体句柄与HAL定义的一些宏操作配合,即可实现一些常用的寄存器位操作。比较常见的HAL库寄存器操作如表7.3.2.3所示:
表7. 3.2.3 HAL库驱动部分与外设句柄相关的宏
但是对于SYSTICK/NVIC/RCC/FLASH/GPIO这些内核外设或共享资源来说,不使用PPP_HandleTypedef这类外设句柄进行控制,如:HAL_GPIO_Init() 只需要初始化的GPIO编号和具体的初始化参数。
- <div align="left"><font face="Tahoma"><font size="3">HAL_StatusTypeDef HAL_GPIO_Init(GPIO_TypeDef* GPIOx,GPIO_InitTypeDef *Init)</font></font></div><div align="left"><font face="Tahoma"><font size="3">{</font></font></div><div align="left"><font face="Tahoma"><font size="3">/*GPIO 初始化程序……*/</font></font></div><div align="left"><font face="Tahoma"><font size="3">}</font></font></div>
复制代码最后要分享的是HAL库的回调函数,这部分允许用户重定义,并在其中实现用户自定义的功能,也是我们使用HAL库的最常用的接口之一:
表7. 3.2.4 HAL库驱动中常用的回调函数接口
至此,我们大概对HAL库驱动文件的一些通用格式和命名规则有了初步印象,记住这些规则可以帮助我们快速对HAL库的驱动进行归类和判定这些驱动函数的用法。
ST官方给我们提供了快速查找API函数的帮助文档。我们如果解压了stm32cubef1的固件包后,在路径“STM32Cube_FW_F1_V1.8.3\Drivers\STM32F1xx_HAL_Driver”下可以找到几个chm格式的文档,根据我们开发板主控芯片STMF103ZE我们没有找到直接可用的,但可以查看型号接近的:STM32F103xG_User_Manual.chm(因为G系列比E系列引脚功能更多,只是查看API函数不响应使用的)。双击打开后,可以看到左边目录下有四个主题,我们来查看Modules。以外设GPIO为例,讲一下怎么使用这个文档。点击GPIO外设的主题下的IO operation functions /functions看看里面的API函数接口描述,如图7.3.2.1所示。
这个文档提供的信息很全,不看源码都可以直接使用它来编写代码,还给我们指示源码位置,非常方便。大家多翻一下其他主题了解一下文档的信息结构,很容易使用。
下面举个例子,比如我们要让PB4输出高电平。先看函数功能,HAL_GPIO_WritePin函数就是我们的GPIO口输出设置函数,如图7.3.2.2所示:
函数有三个形参:
第一个形参是GPIO_TypeDef *GPIOx,形参描述说:x可以是A到G之间任何一个,而我们是PB4引脚,所以第一个形参确认是GPIOB。
第二个形参是uint16_t GPIO_Pin,看形参描述:该参数可以是GPIO_PIN_x,x可以1到15,那么我们第二个形参就是GPIO_PIN_4。
第三个形参是GPIO_PinState PinState,看形参描述:该参数可以是枚举里的两个数,一个是GPIO_PIN_RESET:表示该位清零,另一个是GPIO_PIN_SET:表示设置该位,即置1,我们要输出1,所以要置1该位,那么我们第三个形参就是GPIO_PIN_SET。
最后看函数返回值:None,没有返回值。
所以最后得出我们要调用的函数是:
- HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4, GPIO_PIN_SET);
复制代码帮助文档的使用就讲到这。
本节只针对HAL库作了一个简单的介绍,想要了解更多知识,ST提供了关于HAL库LL库的更详细的说明文档,我们已经把它放到光盘资料A盘《8,STM32参考资料》中了,大家可以自行查阅《Description of STM32F1 HAL and low-layer drivers.pdf》获取所需知识。
7.4如何使用HAL库
我们要先知道STM32芯片的某个外设的性能和工作模式,才能借助HAL库来帮助我们编程,甚至修改HAL库来适配我们的开发项目。HAL库的API虽多,但是查找和使用有规律可循,只要学会其中一个,其他的外设就是类似的,只是添加自己的特性的API而已。
7.4.1 学会用HAL库组织开发工具链
需要按照芯片使用手册建议的步骤去配置芯片。HAL库驱动提供了芯片的驱动接口,但我们需要强调一个概念是使用HAL库的开发是对芯片功能的开发,而不是开发这个库,也不是有也这个库能就直接开发。如果我们对芯片的功能不作了解的话,仍然不知道按照怎样的步骤和寻找哪些可用的接口去实现想要实现的功能。ST提供芯片使用手册《STM32F1xx参考手册.pdf》告诉我们使用某一外设功能时如何具体地去操作每一个用到的寄存器的细节,后面我们的例程讲解过程也会结合这个手册来分析配置过程。
嵌入式的软件开发流程总遵循以下步骤:组织工具链、编写代码、生成可执行文件、烧录到芯片、芯片根据内部指令执行我们编程生成的可执行代码。
在HAL库学习前期,建议以模仿和操作体验为基础,通过例程来学习如何配置和驱动外设。下面根据我们后续要学习的工程梳理出来的基于CMSIS的一个HAL库应用程序文件结构,帮助读者学习和体会这些文件的组成意义,如下表7.4.1.1所示。
把这些文件组织起来的方法,我们会在后续章节新建工程中介绍,这只提前告诉一下大家组成我们需要的编译工具链大概需要哪些文件。
7.4.2 HAL库的用户配置文件
stm32f1xx_hal_conf.h用于裁剪HAL库和定义一些变量,官方没有直接提供这个文件,但在STM32Cube_FW_F1_V1.8.3\Drivers\STM32F1xx_HAL_Driver\Inc这个路径下提供了一个模版文件《stm32f1xx_hal_conf_template.h》,我们可以直接复制这个文件重命名为stm32f1xx_hal_conf.h,做一些简单的修改即可,也可以从在官方的例程中直接复制过来。我们开发板使用的芯片是STM32F103的E系列,所以也可以从下面的路径获取这个配置文件:STM32Cube_FW_F1_V1.8.3\Projects\STM3210E_EVAL\Templates\Inc。
(1)配置外部高速晶振的频率。HSE_VALUE这个参数表示我们的外部高速晶振的频率。这个参数请务必根据我们板子外部焊接的晶振频率来修改,源码在78行开始,官方默认是25M。正点原子STM32F103开发板外部高速晶振的频率是8MHZ。我们没有在代码的其它地方定义过HSE_VALUE这个值,所以编译器最终会引用这里的值8MHz作为外部调整晶振的频率值。
注意事项:使用官方的开发板需要定义USE_STM3210C_EVAL这个宏,我们没有在代码的其它位置中或者编译器的预编译选项中定义过这个宏。
- <div align="left"><font face="Tahoma"><font size="3">#if !defined (HSE_VALUE)</font></font></div><div align="left"><font face="Tahoma"><font size="3">#if defined(USE_STM3210C_EVAL)</font></font></div><div align="left"><font face="Tahoma"><font size="3">#define HSE_VALUE 25000000U /*!< Value of the Externaloscillator in Hz */</font></font></div><div align="left"><font face="Tahoma"><font size="3">#else</font></font></div><div align="left"><font face="Tahoma"><font size="3">#define HSE_VALUE 8000000U /*!< Value of the Externaloscillator in Hz */</font></font></div><div align="left"><font face="Tahoma"><font size="3">#endif</font></font></div><div align="left"><font face="Tahoma"><font size="3">#endif /* HSE_VALUE */</font></font></div>
复制代码还可以把上面的代码直接精简为一行,效果是一样的:
- #define HSE_VALUE 8000000U /*!< Value of the Externaloscillator in Hz */
复制代码(2)还有一个参数就是外部低速晶振频率,这个用于RTC时钟,这个官方默认是32.768KHZ,我们开发板的低速晶振也是这个频率,所以不用修改,源码在111行。
- <div align="left"><font face="Tahoma"><font size="3">#if !defined (LSE_VALUE)</font></font></div><div align="left"><font face="Tahoma"><font size="3"> #define LSE_VALUE ((uint32_t)32768) /* 外部低速振荡器的值,单位HZ */</font></font></div><div align="left"><font face="Tahoma"><font size="3">#endif /* LSE_VALUE */</font></font></div>
复制代码(3)用户配置文件可以用来选择使能何种外设,源码配置在31行到71行,代码如下。
- <div align="left"><font face="Tahoma"><font size="3">/* ########################## Module Selection############################# */</font></font></div><div align="left"><font face="Tahoma"><font size="3">/**</font></font></div><div align="left"><font face="Tahoma"><font size="3"> * @brief This isthe list of modules to be used in the HAL driver </font></font></div><div align="left"><font face="Tahoma"><font size="3"> */</font></font></div><div align="left"><font face="Tahoma"><font size="3">#define HAL_MODULE_ENABLED </font></font></div><div align="left"><font face="Tahoma"><font size="3">#define HAL_ADC_MODULE_ENABLED </font></font></div><div align="left"><font face="Tahoma"><font size="3">#define HAL_CEC_MODULE_ENABLED</font></font></div><div align="left"><font face="Tahoma"><font size="3">#define HAL_COMP_MODULE_ENABLED</font></font></div><div align="left"><font face="Tahoma"><font size="3">#define HAL_CORTEX_MODULE_ENABLED</font></font></div>
复制代码...中间省略...
- <div align="left"><font face="Tahoma"><font size="3">#define HAL_USART_MODULE_ENABLED</font></font></div><div align="left"><font face="Tahoma"><font size="3">#define HAL_WWDG_MODULE_ENABLED</font></font></div><div align="left"><font face="Tahoma"><font size="3">#define HAL_MMC_MODULE_ENABLED</font></font></div>
复制代码比如要不使用GPIO的功能,把源码在49行这个宏注释掉即可,具体如下。
- /* #define HAL_GPIO_MODULE_ENABLED */
复制代码结合同样在stm32f1xx_hal_conf.h中的246行的代码:
- <div align="left"><font face="Tahoma"><font size="3">#ifdef HAL_GPIO_MODULE_ENABLED</font></font></div><div align="left"><font face="Tahoma"><font size="3">#include "stm32f1xx_hal_gpio.h"</font></font></div><div align="left"><font face="Tahoma"><font size="3">#endif /*HAL_GPIO_MODULE_ENABLED */</font></font></div>
复制代码这是一个条件编译符,与#endif配合使用。这里的要表达的意思是,只要工程中定义了HAL_GPIO_MODULE_ENABLED这个宏,就会包含stm32f1xx_hal_gpio.h 这个头文件到我们的工程,同时stm32f1xx_hal_gpio.c中的#ifdef到#endif之间的程序(116行到579行)就会参与编译,否则不编译。所以只要我们屏蔽了stm32f1xx_hal_conf.h文件49行的宏,GPIO的驱动代码就不被编译。也就起到选择使能何种外设的功能,其他外设同理。使用时定义,否则不定义。这样就可以在不修改源码的前提下方便地裁剪HAL库代码的体积了。
注意第一个宏定义:
- #define HAL_MODULE_ENABLED
复制代码它决定了stm32f1xx_hal.c中的第47~587行的代码是否能使用,也是根据条件编译来实现的。其中包含HAL_Init()、HAL_Delay()、HAL_GetTick()这些其它驱动函数可能需要引用的函数,所以这个宏也是必须要定义的。
官方的示范例程,就是通过屏蔽外设的宏的方法来选择使能何种外设。表现上就是编译时间会变短,因为屏蔽了不使用的HAL库驱动,编译时间自然就短了。正点原子的例程选择另外一中方法,就是工程中只保留需要的stm32f1xx_hal_ppp.c,不需要的不添加到工程里,由于找不到源文件且没有引用这些文件,同样编译器不会去编译这些代码。
关于配置文件我们暂时只讲这些,具体其它需要修改的地方,我们在例程讲解中再去说明。
(4)大家看到STM32F1xx_hal_conf.h文件的127行。
- #define TICK_INT_PRIORITY ((uint32_t)0x0F) /*!< tickinterrupt priority */
复制代码宏定义TICK_INT_PRIORITY是滴答定时器的优先级。这个优先级很重要,因为如果其它的外设驱动程序的延时是通过滴答定时器提供的时间基准,来实现延时的话,又由于实现方式是滴答定时器对寄存器进行计数,所以当我们在其它中断服务程序里调用基于此时间基准的延迟函数 HAL_Delay,那么假如该中断的优先级高于滴答定时器的优先级,就会导致滴答定时器中断服务函数一直得不到运行,从而程序卡死在这里。所以滴答定时器的中断优先级一定要比这些中断高。
请注意这个时间基准可以是滴答定时器提供,也可以是其他的定时器,默认是用滴答定时器。
(5)断言这个功能我们在使程中不使用。断言这个功能用来判断HAL函数的形参是否有效,并在参数错误时启用这个断言功能,告诉开发者代码错误的位置,断言功能由用户自己决定。这个功能的使能开关代码是一个宏,在源码的160行,默认是关闭的,代码如下。
- /* #define USE_FULL_ASSERT 1 */
复制代码通过宏USE_FULL_ASSERT来选择功能,在源码375行到389,代码如下。
- <div align="left"><font face="Tahoma"><font size="3">#ifdef USE_FULL_ASSERT</font></font></div><div align="left"><font face="Tahoma"><font size="3">/**</font></font></div><div align="left"><font face="Tahoma"><font size="3"> * @brief The assert_param macro is used for function'sparameters check.</font></font></div><div align="left"><font face="Tahoma"><font size="3"> * <a href="home.php?mod=space&uid=271674" target="_blank">@param</a> expr If expr is false, it calls assert_failedfunction</font></font></div><div align="left"><font face="Tahoma"><font size="3"> * which reports the name of the sourcefile and the source</font></font></div><div align="left"><font face="Tahoma"><font size="3"> * line number of the call that failed.</font></font></div><div align="left"><font face="Tahoma"><font size="3"> * If expr is true, it returns no value.</font></font></div><div align="left"><font face="Tahoma"><font size="3"> * @retval None</font></font></div><div align="left"><font face="Tahoma"><font size="3"> */</font></font></div><div align="left"><font face="Tahoma"><font size="3">#define assert_param(expr) ((expr)? (void)0U : \</font></font></div><div align="left"><font face="Tahoma"><font size="3">assert_failed((uint8_t *)__FILE__, __LINE__))</font></font></div><div align="left"><font face="Tahoma"><font size="3">/* Exported functions------------------------------------------------------- */</font></font></div><div align="left"><font face="Tahoma"><font size="3">void assert_failed(uint8_t* file, uint32_t line);</font></font></div><div align="left"><font face="Tahoma"><font size="3">#else</font></font></div><div align="left"><font face="Tahoma"><font size="3">#define assert_param(expr) ((void)0U)</font></font></div><div align="left"><font face="Tahoma"><font size="3">#endif /* USE_FULL_ASSERT*/</font></font></div>
复制代码也是通过条件编译符来选择对应的功能。当用户自己需要使用断言功能,怎么做呢?首先需要定义宏USE_FULL_ASSERT来使能断言功能,即把源码的160行的注释去掉即可。然后看到源码423行的assert_failed()这个函数。根据这个宏定义,我们还需要去定义这个函数的功能,可以按以下格式自定义这个函数,出错后使代码停留在这里:
- <div align="left"><font face="Tahoma"><font size="3">#ifdef USE_FULL_ASSERT</font></font></div><div align="left"><font face="Tahoma"><font size="3"> </font></font></div><div align="left"><font face="Tahoma"><font size="3">/**</font></font></div><div align="left"><font face="Tahoma"><font size="3"> * @brief 当编译提示出错的时候此函数用来报告错误的文件和所在行</font></font></div><div align="left"><font face="Tahoma"><font size="3"> * @param file:指向源文件</font></font></div><div align="left"><font face="Tahoma"><font size="3"> * line:指向在文件中的行数</font></font></div><div align="left"><font face="Tahoma"><font size="3"> * @retval 无</font></font></div><div align="left"><font face="Tahoma"><font size="3"> */</font></font></div><div align="left"><font face="Tahoma"><font size="3">void assert_failed(uint8_t* file, uint32_t line)</font></font></div><div align="left"><font face="Tahoma"><font size="3">{ </font></font></div><div align="left"><font face="Tahoma"><font size="3"> while (1)</font></font></div><div align="left"><font face="Tahoma"><font size="3"> {</font></font></div><div align="left"><font face="Tahoma"><font size="3"> }</font></font></div><div align="left"><font face="Tahoma"><font size="3">}</font></font></div><div align="left"><font face="Tahoma"><font size="3">#endif</font></font></div>
复制代码可以看到这个函数里面没有实现如何功能,就是一个什么不做的死循环,具体功能请根据自己的需求去实现。file是指向源文件的指针,line是指向源文件的行数。__FILE__是表示源文件名,__LINE__是表示在源文件中的行数。比如我们可以实现打印出这个错误的两个信息等等,但前提是你已经学会了如何使芯片输出打印信息这些功能。
总的来说断言功能就是,在HAL库中,如果定义了USE_FULL_ASSERT这个宏,那么所有的HAL库函数将会检查函数的形参是否正确。如果错误将会调用assert_failed()这个函数,程序就会停留在这里,用户可以定位到出错的函数。这个功能实际上是在芯片上运行的时候的增加错误提示信息的功能,属于调试功能的一部分,实际我们的编译器就可以帮助定位到参数错误的问题并提示信息。在F103的工程中我们不使用这个功能。
7.4.3 stm32f1xx_hal.c文件
这个文件内容比较多,包括HAL库的初始化、系统滴答、基准电压配置、IO补偿、低功耗、EXTI配置等都集合在这个文件里面。下面我们对该文件进行讲解。
1. HAL_Init()函数
源码在142行到167行,简化函数如下(下面的代码只针对F1的HAL固件1.8.3版本,其它版本可能有差异):
- HAL_StatusTypeDef HAL_Init(void)
- {
- /* 配置Flash的预取控制器 */
- #if (PREFETCH_ENABLE != 0)
- #if defined(STM32F101x6) || defined(STM32F101xB) ||defined(STM32F101xE) || defined(STM32F101xG) || \ defined(STM32F102x6) || defined(STM32F102xB)|| \
- defined(STM32F103x6) || defined(STM32F103xB) || defined(STM32F103xE) ||defined(STM32F103xG) || \ defined(STM32F105xC) || defined(STM32F107xC)
- /* Prefetch buffer is not available on value line devices*/
- __HAL_FLASH_PREFETCH_BUFFER_ENABLE();
- #endif
- #endif /* 使能Flash的预取控制器 */
- /* 配置中断优先级顺序 */
- HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
- /* 使用滴答定时器作为时钟基准,配置1ms滴答(重置后默认的时钟源为HSI) */
- HAL_InitTick(TICK_INT_PRIORITY);
- /* 初始化其它底层硬件(如果必要) */
- HAL_MspInit();
- /* 返回函数状态 */
- return HAL_OK;
- }
复制代码该函数是HAL库的初始化函数,原则上在程序中必须优先调用,其主要实现如下功能:
1)使能Flash的预取缓冲器(根据闪存编程手册,打开预取指令可以提高对I-Code总线的访问效率,且AHB时钟的预分频系数不为1时,必须打开预取缓冲器)
2)设置NVIC优先级分组为4。
3)配置滴答定时器每1ms产生一个中断。
在这个阶段,系统时钟还没有配置好,因此系统还是默认使用内部高速时钟源HSI在跑程序。对于F1来说,HSI的主频是8MHZ。所以如果用户不配置系统时钟的话,那么系统将会使用HSI作为系统时钟源。
4)调用HAL_MspInit函数初始化底层硬件,HAL_MspInit函数在stm32f1xx_hal.c文件里面做了弱定义。关于弱定义这个概念,后面会有讲解,现在不理解没关系。正点原子的HAL库例程是没有使用到这个函数去初始化底层硬件,而是单独调用需要用到的硬件初始化函数。用户可以根据自己的需求选择是否重新定义该函数来初始化自己的硬件。
注意事项:为了方便和兼容性,正点原子的HAL库例程中的中断优先级分组设置为分组2,即把源码的HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)改为如下代码:
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
中断优先级分组为2,也就是2位抢占优先级,2位响应优先级,抢占优先级和响应优先级的值的范围均为0-3。
2. HAL_DeInit ()函数
源码在175行到194行,简化后函数如下:
- HAL_StatusTypeDef HAL_DeInit(void)
- {
- /* 重置所有外设 */
- __HAL_RCC_APB1_FORCE_RESET();
- __HAL_RCC_APB1_RELEASE_RESET();
- __HAL_RCC_APB2_FORCE_RESET();
- __HAL_RCC_APB2_RELEASE_RESET();
- /* 对底层硬件初始化 */
- HAL_MspDeInit();
- /* 返回函数状态 */
- return HAL_OK;
- }
复制代码该函数取消初始化HAL库的公共部分,并且停止systick,是一个可选的函数。该函数做了一下的事:
1)复位了APB1、APB2的时钟。
2)调用HAL_MspDeInit函数,对底层硬件初始化进行复位。HAL_MspDeInit也在STM32F1xx _hal.c文件里面做了弱定义,并且与HAL_MspInit函数是一对存在。HAL_MspInit函数负责对底层硬件初始化,HAL_MspDeInit函数则是对底层硬件初始化进行复位。这两个函数都是需要用户根据自己的需求去实现功能,也可以不使用。
3. HAL_InitTick ()函数
源码在234行到255行,简化函数如下:
- __weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
- {
- /* uwTickFreq是个枚举类型,如果检测到uwTickFreq为零,则返回 */
- if((uint32_t)uwTickFreq == 0UL)
- {
- return HAL_ERROR;
- }
- /* 配置滴答定时器1ms产生一次中断 */
- if (HAL_SYSTICK_Config(SystemCoreClock /(1000UL / (uint32_t)uwTickFreq))> 0U)
- {
- return HAL_ERROR;
- }
- /* 配置滴答定时器中断优先级 */
- if (TickPriority < (1UL << __NVIC_PRIO_BITS))
- {
- HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
- uwTickPrio = TickPriority;
- }
- else
- {
- return HAL_ERROR;
- }
- /* 返回函数状态 */
- return HAL_OK;
- }
复制代码该函数用于初始化滴答定时器的时钟基准,主要功能如下:
1)配置滴答定时器1ms产生一次中断。
2)配置滴答定时器的中断优先级。
3)该函数是__weak修饰的函数,如果在关联工程中的其它地方没有定义__weak后面的函数,这里是HAL_InitTick(),则使用此处的定义,否则就使用其它地方定义好的函数功能。我们可以通过重定义这个函数来选择其它的时钟源(如定时器)作为HAL库函数的时基或者通过重定义不开启Systick的功能和中断等。
该函数可以通过HAL_Init()或者HAL_RCC_ClockConfig()重置时钟。在默认情况下,滴答定时器是时间基准的来源。如果其他中断服务函数调用了HAL_Delay(),必须小心,滴答定时器中断必须具有比调用了HAL_Delay()函数的其他中断服务函数的优先级高(数值较低),否则会导致滴答定时器中断服务函数一直得不到执行,从而卡死在这里。
4. 滴答定时器相关的函数
源码在293行到416行,相关函数如下:
- /* 该函数在滴答定时器时钟中断服务函数中被调用,一般滴答定时器1ms中断一次,
- 所以函数每1ms让全局变量uwTick计数值加1 */
- __weak void HAL_IncTick(void)
- {
- uwTick += (uint32_t)uwTickFreq;
- }
- /* 获取全局变量uwTick当前计算值 */
- __weak uint32_t HAL_GetTick(void)
- {
- return uwTick;
- }
- /* 获取滴答时钟优先级 */
- uint32_t HAL_GetTickPrio(void)
- {
- return uwTickPrio;
- }
- /* 设置滴答定时器中断频率 */
- HAL_StatusTypeDef HAL_SetTickFreq(HAL_TickFreqTypeDef Freq)
- {
- HAL_StatusTypeDefstatus = HAL_OK;
- HAL_TickFreqTypeDef prevTickFreq;
- assert_param(IS_TICKFREQ(Freq));
- if (uwTickFreq != Freq)
- {
- /* 备份滴答定时器中断频率 */
- prevTickFreq = uwTickFreq;
- /* 更新被HAL_InitTick()调用的全局变量uwTickFreq */
- uwTickFreq = Freq;
- /* 应用新的滴答定时器中断频率 */
- status = HAL_InitTick(uwTickPrio);
- if (status != HAL_OK)
- {
- /* 恢复之前的滴答定时器中断频率 */
- uwTickFreq = prevTickFreq;
- }
- }
- return status;
- }
- /* 获取滴答定时器中断频率 */
- HAL_TickFreqTypeDef HAL_GetTickFreq(void)
- {
- return uwTickFreq;
- }
- /*HAL库的延时函数,默认延时单位ms */
- __weak void HAL_Delay(uint32_t Delay)
- {
- uint32_t tickstart = HAL_GetTick();
- uint32_t wait = Delay;
- /* Add a freq to guarantee minimum wait */
- if (wait < HAL_MAX_DELAY)
- {
- wait += (uint32_t)(uwTickFreq);
- }
- while ((HAL_GetTick() - tickstart) < wait)
- {
- }
- }
- /* 挂起滴答定时器中断,全局变量uwTick计数停止 */
- __weak void HAL_SuspendTick(void)
- {
- /* 禁止滴答定时器中断 */
- CLEAR_BIT(SysTick->CTRL,SysTick_CTRL_TICKINT_Msk);
- }
- /* 恢复滴答定时器中断,恢复全局变量uwTick计数 */
- __weak void HAL_ResumeTick(void)
- {
- /* 使能滴答定时器中断 */
- SET_BIT(SysTick->CTRL,SysTick_CTRL_TICKINT_Msk);
- }
复制代码
这些函数不是很难,请参照注释理解。注意:如果函数被前缀__weak定义,则用户可以重新定义该函数。更多的内容可以参考8.1.5小节。
5. HAL库版本相关的函数
源码在422行到484行,相关函数声明在stm32f1xx_hal.h中,详看如下:
- uint32_t HAL_GetHalVersion(void); /* 获取HAL库驱动程序版本 */
- uint32_t HAL_GetREVID(void); /* 获取设备修订标识符 */
- uint32_t HAL_GetDEVID(void); /* 获取设备标识符 */
- uint32_t HAL_GetUIDw0(void); /* 获取唯一设备标识符的第一个字 */
- uint32_t HAL_GetUIDw1(void); /* 获取唯一设备标识符的第二个字 */
- uint32_t HAL_GetUIDw2(void); /* 获取唯一设备标识符的第三个字 */
复制代码这些函数了解一下就好了,用得不多。
6. 调试功能相关函数
源码在490行到587行,函数声明如下:
- void HAL_DBGMCU_EnableDBGSleepMode(void);
- void HAL_DBGMCU_DisableDBGSleepMode(void);
- void HAL_DBGMCU_EnableDBGStopMode(void);
- void HAL_DBGMCU_DisableDBGStopMode(void);
- void HAL_DBGMCU_EnableDBGStandbyMode(void);
- void HAL_DBGMCU_DisableDBGStandbyMode(void);
复制代码这六个函数用于调试功能,默认调试器在睡眠模式下无法调试代码,开发过程中配合这些函数,可以在不同模式下(睡眠模式、停止模式和待机模式),使能或者失能调试器,当我们使用到时再作详细。
7.4.4 HAL库中断处理
中断是STM32开发的一个很重要的概念,这里我们可以简单地理解为:STM32暂停了当前手中的事并优先去处理更重要的事务。而这些“更重要的事务”是由软件开发人员在软件中定义的。关于STM32中断的概念,我们会在中断例程的讲解再跟大家详细介绍。
由于HAL库中断处理的逻辑比较统一,我们将这个处理过程抽象为图7.4.4.1所表示的业务逻辑:
结合以前的HAL库文件介绍章节,以上的流程大概就是:设置外设的控制句柄结构体PPP_HandleType和初始化PPP_InitType结构体的参数,然后调用HAL库对应这个驱动的初始化HAL_PPP_Init(),由于这个API中有针对外设初始化细节的接口Hal_PPP_Mspinit(),我们需要重新实现这个函数并完成外设时钟、IO等细节差异的设置, 完成各细节处理后,使用HAL_NVIC_SetPriority()、HAL_NVIC_EnableIRQ()来使能我们的外设中断;
定义中断处理函数PPP_IRQHandler,并在中断函数中调用HAL_ppp_function_IRQHandler()来判断和处理中断标记; HAL库中断处理完成后,根据对应中的调用我们需要自定义的中断回调接口HAL_PPP_ProcessCpltCallback();如串口接收函数HAL_UART_RxCpltCallback(),我们在这个函数中实现我们对串口接收数据想做的处理;
中断响应处理完成后,stm32芯片继续顺序执行我们定义的主程序功能,按照以上处理的标准流程完成了一次中断响应。
7.4.5 正点原子对HAL库用法的个性化修改
前面介绍了ST官方建议的HAL库的使用方法,这部分我们结合我们实际例程的编写,列出例程中对HAL库使用上的一些与官方推荐用法的差异,读者们请结合自己的使用习惯,辩证地去看待我们这种修改方式:
1,每个例程的BSP下都有一个HAL库,并且代码中对文件的引用尽量使用相对路径,保证每个复制完整的例程,在其他路径下也能编译通过;这种做法增加了HAL库全部例程的体积;
2,将中断处理函数独立到每个外设中,便于独立驱动;同类型的外设驱动处理函数不使用HAL回调函数接口处理操作而直接在中断函数中处理判断对应中断;
3,我们把原来的中断分组进行了修改,由抢占式无子优先级改为中断分组2;便于管理同类外设的优先级响应;
4,我们编写的初始化函数或者芯片操作时序上的延时,我们使用到了delay_ms()、delay_us()等函数进行初始化,使用的是Systick作的精准延时,而HAL库默认也使用Systick作延时处理,为解决这种冲突和兼容我们大部分的驱动代码,我们在例程中使用delay.c中的延时函数取代Hal_Delay();取消原来HAL库的Systick延时设置。
7.5HAL库使用注意事项
本小节根据经验跟大家讲述一些关于HAL库使用的注意事项,供读者参考。
1,即使我们已经在使用库函数作为开发工具了,我们可以忽略很多芯片的硬件外设使用上的细节,但当发生问题时,我们仍需要回归到芯片使用手册查看当前操作是否违规或缺漏;
2,使用HAL库和其它第三方的库开发类似,把我们需要编写的软件和第三方的库分开成相互独立的文件,开发过程中我们尽量不去修改第三方的软件源码,需要修改的部分尽量在自己的代码中实现;这样一旦我们需要更新第三方库时,我们原来编写的功能也能很快地匹配新的库去执行功能;
3,即使HAL库目前较以前已经相对更完善了,但它仍无法覆盖我们要想实现的所有细节功能,甚至可能存在错误,我们要有怀疑精神,辩证地去使用好这个工具;如我们在PWM一节编码时发现HAL库中有个宏定义TIM_RESET_CAPTUREPOLARITY括号不匹配导致编译报错,这时我们不得不修改一下HAL库的源码了。
4,注意HAL库的执行效率。由于HAL库的驱动对相同外设大多是可重入的,在执行HAL驱动的API函数的效率没有直接寄存器操作来得高,如果在对时序要求比较严苛的代码,建议使用简洁的寄存器操作代替;
5,我们在例程中使用delay.c中的延时函数取代Hal_Delay();取消原来HAL库的Systick延时设置;但这会有一个问题:原来HAL库的超时处理机制不再适用,所以对于设置了超时的函数,可能会导致停留在这个函数的处理中,无法按正常的超时退出;
6,我们建议如无致命BUG出现,尽量使用开发学习时已经测试稳定的HAL库来继续进行开发,不必要频繁更新HAL库,因为更新HAL库后可能会导致原来能正常运行的代码,在更新HAL库之后反面反面无法运行的情况,有些函数操作的结构方式变化了等等,这里只是笔者的建议,大家自己的实际情况权衡。