stm 社区 关于HAL库中stm32f1xx_hal_msp.c文件的认知(新手贴)
http://www.stmcu.org.cn/module/forum/thread-620752-1-3.html
关于HAL库中stm32f1xx_hal_msp.c文件的认知(新手贴)
要熟练使用STM32CUBEMX这个软件,首先就要熟悉HAL库。俺以前用的片子基本都是51核心的,都是直操寄存器的,后来用STM8的片子,用的是库,感觉用起来很方便。随着项目的不断扩充和客户要求的提高,用到了STM32F103的片子,想想自己关于STM32的认知水平还在6年前3.5版本的库,感觉应该有点落后了,于是上网找找最新版本的库,好做项目设计准备。就在这时候发现了STM32CUBEMX的这个软件以及HAL库,看了相关介绍后,瞬间觉得自己完全落伍了,学习,学习,再学习......
网上好多关于HAL库和标准库优劣的探讨,俺再这就不多说了,俺认为只要能满足项目需要就行,就好像筷子和刀叉的关系,只要能把饭吃到嘴里就行。俺为啥选择学HAL库?因为俺以前啥也不会,要学当然选最新的方向学习了。
言归正传
学习HAL库,首先是对这个库的各个文件仔细研究,每个文件的大概功能、在整个库中所起到的作用、文件与文件之间引用关系、文件与文件之间的层级关系都要做到心中有数,这样在使用库做项目的时候才不至于逻辑混乱,减少调试时间。对整个库的研究需要狠下功夫,对库中文件的作用和关系了解透彻了,也就能明白了HAL的编程思想精髓,结合STM32CUBEMX,可以随心应手操刀自己的设计。
在HAL库中有一个stm32f1xx_hal_msp.c的文件,这个文件的作用就是根据用户所提供的具体的MCU型号以及硬件配置,对HAL库进行初始化设置操作。所以这个文件是就HAL库与MCU结合的纽带(不知这样描述是否恰当)。
以下是个打比方说明方式(欢迎指正):
首先把每个片上外设看成一个功能元件,把有关所有外设的记录合起来就是一个表格(BOM表)。
好比F1系列的HAL库是对F1系列所有型号MCU功能BOM的集合。
现在要使用一个F1系列的单片机,型号就叫作A1。
在CUBEMX中,设置单片机型号为A1
那么CUBEMX就会根据A1这个关键字去配置相应的功能BOM表。并一起把相应具体的用户硬件设置写到stm32f1xx_hal_msp.c中(完成了对HAL库的初始化,注:这个初始化着重的硬件电气上的初始化,就是确定用哪个引脚,什么样高低电平、多大的频率等等)。
那么stm32f1xx_hal_msp.c中的设置是怎么调用和被调用的呢?
1.在stm32f1xx_hal_msp.c内包含了头文件stm32f1xx_hal.h。(可以认为stm32f1xx_hal.h是stm32f1xx_hal_msp.c的头文件,同样也是stm32f1xx_hal.c的头文件)
2.在stm32f1xx_hal.h中声明了HAL_MspInit(void)函数。
3.在stm32f1xx_hal_msp.c内定义了HAL_MspInit(void)函数
(也就是间接的通过stm32f1xx_hal.h文件先声明了HAL_MspInit(void)函数,再接着对其进行具体的定义,为什么要在stm32f1xx_hal.h中先声明,是因为还要在stm32f1xx_hal.c中还进行了弱定义)。
4.stm32f1xx_hal.c内弱定义了 __weak void HAL_MspInit(void)。
5.stm32f1xx_hal_msp.c中的函数定义相对stn32f1xx_hal.c中的同名函数定义具有优先权,如果在tm32f1xx_hal_msp.c没有定义某外设函数,则使用stn32f1xx_hal.c中的定义的那个函数。
6.用户可通过重新定义stm32f1xx_hal_msp.c内的函数,实现对函数的操作。
7.stm32f1xx_hal_msp.c中的函数通过stm32f1xx_hal.h头文件引用。(也就是stm32f1xx_hal_msp.c中具体定义的函数都先在stm32f1xx_hal.h中被预先声明了)。
通过上面可以了解到,用户对MCU与外设配置写在stm32f1xx_hal_msp.c内,作为回调函数被HAL库中的其他文件使用。
下面是根据网上大师们和大咖们的教导结合自己的认知写的总结,仅供参考。
英文翻译,不知道那个对,俺也不敢说:
MSP MCU Specific Package MCU 特征 包、MCU 具体实例化 包
MSP MCU Support package MCU 支持 包
作用和目的:
根据具体的MCU型号,对片上外设进行初始化设置。
这样做的目的是因为从以下几个方面考虑的:
1、所有的同一类型的外设被抽象成通用的封装,即同类型的外设在不同的MCU上的特征属性和API接口都是一样和相同的。
2、相同内核但不同型号的MCU(比如STM32F103C8T6和 STM32F103ZET6都是M3内核)的引脚数量、顺序、资源、内存空间以及片上外设种类数量存在不同。
3、为了使HAL库对具有相同内核但资源不同的MCU有较强兼容性,特增加了此文件,让用户根据每款MCU在硬件上具体区别,初始化和配置外设的IO引脚、外设的工作时钟以及外设的中断与MCU内核寄存器的对应关系。
4、此文件是对MCU硬件上的初始化设置(一些协议、数据格式等等上层的内容一般不涉及),把具体的硬件配置抽象,形成符合HAL库要求的、具有统一格式的和属性种类的结构体。此文件由用户进行编程初始化和配置。该文件内的初始化过程强调的是外设最底层硬件上的初始化。
举例:如果存在某个外设PPP
那么HAL_PPP_MSP_Init() 函数是对外设PPP硬件上的具体设置。
HAL_PPP_MSP_Init()这个函数又进一步被PPP_Init()外设初始化函数调用。
HAL_PPP_MSP_Init()是做为一个回调函数被用户配置,HAL库回调使用,从而使HAL库在整体架构上做到统一和兼容。
就是说MSP的作用是把某个外设的接口资源给具体化了,比如对于串口外设,就是指定串口具体的接口引脚状态(包含引脚的位置、电气属性等等)以及外设与CPU的接口(外设与CPU的接口就是特殊功能寄存器的映射地址,也就是告诉CPU要操作哪个外设只要操作相应地址的寄存器就可以了)。
俺是新手,欢迎大家留言斧正。
很久之前就听说st出了一个新版本的库,用于代替原来的标准库,非常好奇,但是一直没有机会去体验。这次借着做毕设的机会,尝试着切换到新库。
官网介绍说,hal(hardware abstract layer)是一层硬件的抽象,看到这里,我非常激动,看来st终于意识到原来标准库的问题了,原来的标准库非常依赖于具体硬件细节,很难体现出使用库的优势,而且很难移植。同时我也非常好奇,st到底是如何把不同系列mcu的操作给封装起来的,是不是足够抽象,方便移植。
话不多说,直接上官网下下来再说。
![](http://upload-images.jianshu.io/upload_images/833857-a9ef9d1d76630850.png?imageMogr2/auto-orient/strip|imageView2/2/w/570/format/webp)
上图就是hal库的全部内容,其中STM32F1xx_HAL_Driver中属于hal库的内容。
拿到库第一步需要做的就是配置一个简单的hello world,我在配置的时候,出现了非常多的问题。最开始非常自信,只从文件夹里挑选自认为有用的文件加入到工程中,结果出现了各种问题,里面各种库的依赖关系比较复杂,如果不是很熟悉整个架构的话,还是老老实实拷贝整个文件夹吧。
配置之前,首先要了解一下整个库的框架,官方给的框架图如下:
个人认为这幅图和我理解的有些许出入,故重新画了一张:
![](http://upload-images.jianshu.io/upload_images/833857-ecb29c2b1200c12d.png?imageMogr2/auto-orient/strip|imageView2/2/w/766/format/webp)
有几点区别:
- cmsis我放在了驱动层的最底层,因为cmsis库中包含的内容都是和具体cpu内核相关的东西,还有一些地址定义,这些都是非常底层的东西了,而且hal层确实是依赖于cmsis。
- hal底层我增加了一层msp,类似于bsp,全称是mcu support package,这一层相当于hal的驱动层,与硬件相关的部分比如最终的时钟配置,gpio配置等等提取出来,交给用户配置。
了解了架构,下面我们就来配置一个简单的工程吧。
- 首先拷贝整个Driver目录到工程中。
- 新建user文件夹,新建main.c文件。
找到stm32f1xx_hal_conf_template.h,stm32f1xx_hal_msp_template.c,去掉"_template"放入user文件夹。
找到stm32f1xx_it.c和stm32f1xx_it.h放入user文件夹。 - 新建工程
添加源文件:
![](http://upload-images.jianshu.io/upload_images/833857-40a5ec496dfef205.png?imageMogr2/auto-orient/strip|imageView2/2/w/318/format/webp)
配置工程:
- 勾选Use MicroLib,因为hal使用了c标准库。
-
添加全局宏定义:USE_HAL_DRIVER,STM32F103xB。关于芯片选择,有如下表格:
捕获1.PNG - 勾选c99支持,因为hal采用的是c99标准编写,不勾选的话,会出现类似于uint32_t等类型不存在的编译错误。
- 添加包含目录,如下图:
![](http://upload-images.jianshu.io/upload_images/833857-79b6a1ce28b45a9a.png?imageMogr2/auto-orient/strip|imageView2/2/w/310/format/webp)
4.编写代码:
配置stm32f1xx_hal_conf.h:
这里面有许多用于配置的宏,比如用于精准延时的晶振频率,还有各个外设模块的开关等等。
main.c
#include "stm32f1xx_hal.h"
int main()
{
HAL_Init();
__HAL_RCC_GPIOC_CLK_ENABLE();
GPIO_InitTypeDef gpio_initstruct;
gpio_initstruct.Mode=GPIO_MODE_OUTPUT_PP;
gpio_initstruct.Speed=GPIO_SPEED_FREQ_HIGH;
gpio_initstruct.Pull=GPIO_NOPULL;
gpio_initstruct.Pin=GPIO_PIN_13;
HAL_GPIO_Init(GPIOC,&gpio_initstruct);
while(1)
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,0);
HAL_Delay(150);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,1);
HAL_Delay(150);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,0);
HAL_Delay(150);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,1);
HAL_Delay(1000);
}
return 0;
}
stm32f1xx_hal_msp.c
#include "stm32f1xx_hal.h"
void SystemClock_Config(void);
void HAL_MspInit(void)
{
SystemClock_Config();
}
void SystemClock_Config(void)
{
RCC_ClkInitTypeDef clkinitstruct = {0};
RCC_OscInitTypeDef oscinitstruct = {0};
/* Configure PLL ------------------------------------------------------*/
/* PLL configuration: PLLCLK = (HSI / 2) * PLLMUL = (8 / 2) * 16 = 64 MHz */
/* PREDIV1 configuration: PREDIV1CLK = PLLCLK / HSEPredivValue = 64 / 1 = 64 MHz */
/* Enable HSI and activate PLL with HSi_DIV2 as source */
oscinitstruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;//RCC_OSCILLATORTYPE_HSI;
oscinitstruct.HSEState = RCC_HSE_ON;//RCC_HSE_OFF;
oscinitstruct.LSEState = RCC_LSE_OFF;
oscinitstruct.HSIState = RCC_HSI_OFF;//RCC_HSI_ON;
oscinitstruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
oscinitstruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
oscinitstruct.PLL.PLLState = RCC_PLL_ON;
oscinitstruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;//RCC_PLLSOURCE_HSI_DIV2;
oscinitstruct.PLL.PLLMUL = RCC_PLL_MUL9;//RCC_PLL_MUL16;
if (HAL_RCC_OscConfig(&oscinitstruct)!= HAL_OK)
{
/* Initialization Error */
while(1);
}
/* Select PLL as system clock source and configure the HCLK, PCLK1 and PCLK2
clocks dividers */
clkinitstruct.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);
clkinitstruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clkinitstruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
clkinitstruct.APB2CLKDivider = RCC_HCLK_DIV1;
clkinitstruct.APB1CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&clkinitstruct, FLASH_LATENCY_2)!= HAL_OK)
{
/* Initialization Error */
while(1);
}
}
整个编程步骤就是,hal库初始化->开外设时钟->外设初始化->用户程序,然后在msp.c文件里实现其他平台相关的杂七杂八的操作,需要调用的时候会自动调用,我这里只实现了一个点亮led的功能,故只实现了HAL_MspInit()函数。如果我们要使用uart、adc等其他更复杂的外设,我们需要在msp.c文件里重写HAL_UART_MspInit()、HAL_ADC_MspInit()等函数,当我们调用HAL_PPP_Init()时,他们都会自动被调用。
说到这里,我要说一下这里其实使用了一个c语言的技巧,实现了类似于c++的重载功能。比如我们来看UART的源文件:
/**
* @brief USART MSP Init.
* @param husart: Pointer to a USART_HandleTypeDef structure that contains
* the configuration information for the specified USART module.
* @retval None
*/
__weak void HAL_USART_MspInit(USART_HandleTypeDef *husart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(husart);
/* NOTE: This function should not be modified, when the callback is needed,
the HAL_USART_MspInit can be implemented in the user file
*/
}
函数被__weak修饰了,意思就是,如果别处没定义,这个函数就是他,如果别处重定义了,就用新的函数,这样就实现了重载。这有一个很大的好处,就是实现oo思想中的差异化编程,hal实现所有硬件通用的功能,而把不通用的部分通过可重载的函数开放给用户修改。
体现oo的还有个地方,每个函数中,都会接收到一个handle指针,这其实和this指针非常类似,每个函数都不用知道自己到底是在操作某一个具体的对象,只需要根据handle的指向来操作就可以了。
回到上面的重载。在hal库中有一点比较大的改变是,中断都是通过回调函数来开放给用户的,具体使用方式也是重载相关回调函数,不像标准库是直接在stm32fxxx_it.c里填写相关中断处理函数。这样做的好处是,hal帮我们处理了一些中断来临时的杂务,只把我们感兴趣的事件开放给用户。
但是个人觉得这个改变需要再彻底一点,因为这并没有解决代码耦合性的痛点,每一次我们需要写中断函数的时候,总是要去改底层代码,而如果st给我们实现一个注册回调的接口,那么上层和下层之前就完全分离了,应用层各个模块之间也不会产生耦合。
总结:
总体而言,hal相比于标准库,层次架构更加清晰了,对平台更加抽象,但是还远远不够,依然非常依赖于具体的硬件,如果能实现Qt的那种抽象就完美了。用户使用的时候,只用包含hal.h而不用去管是hal_f1还是hal_f2或是什么其他系列的头文件,所有系列的代码打包在一起,通过条件编译来实现真正的跨平台,而如果需要使用某款mcu的特色功能时,就再包含一个hal_f1extend.h。如果这些st都实现了,那么单片机编程将会变得和应用编程一样简单方便!
作者:logic_wei
链接:https://www.jianshu.com/p/c6809c2bcb4f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
新建基于STM32F103ZET6的工程-HAL库版本
1、STM32F103ZET6简介
STM32F103ZET6的FLASH容量为512K,64K的SRAM。按照STM32芯片的容量产品划分,STM32F103ZET6属于大容量的芯片。
2、下载HAL固件库
打开STM32的官方网址:https://www.st.com/。
在官网上通过搜素STM32CubeF1找到如上图所显示的内容,这个就是STM32F1系列的固件包文件,点击“获取软件”下载。
3、安装STM32F1的Pack
到http://www.keil.com/dd2/pack/ 网站下载STM32F1 Pack 安装包,如果不安装STM32F1的Pack,则在使用MDK创建工程选择芯片型号的时候找不到对应的IC型号,当然如果安装MDK的时候已经有对应的Pack了,则不用安装了。如下图:
下载Pack后双击下载的文件进行安装,Pack安装包会自动找到MDK的安装目录进行安装,直接点击下一步进行安装即可,安装完成后可以在MDK的Device中看到STM32F1Series。
4、STM32CubeF1固件包说明
STM32CubeF1固件包的根目录如下图所示。
_htmresc文件夹:
这个文件夹放的是一些图片和网站的资料,个人觉得没什么用。
Documentation文件夹:
里面就一个STM32CubeF1的说明文档,而且是英文的,看得懂的可以琢磨。
Utilities文件夹:
该文件夹下面是一些其他组件,基本没怎么使用。
Middlewares文件夹:
如果做基本的开发,这个文件夹也不怎么使用。Middlewares文件夹中包含了ST和Third_Party两个子文件夹,它们的内容如下:
ST\STemWim文件夹:
存放的是STemWin工具包。
ST\STM32_TouchSensing_Library文件夹:
存放的是STM32电容触摸支持包。
ST\STM32_USB_Device_Library文件夹:
USB从机设备支持包。
ST\STM32_USB_Host_Library文件夹:
USB主机设备支持包。
Third_Party\FatFs文件夹:
FAT文件系统支持包。
Third_Party\FreeRTOS文件夹:
FreeRTOS实时系统支持包。
Drivers文件夹:
Dreivers文件夹下有如图所示几个子文件夹。
BSP文件夹:
该文件夹也叫板级支持包,该包提供的是直接与硬件打交道的API,如触摸屏,LCD,SRAM以及EEPROM等板载硬件资源驱动。该文件夹中还有多种ST官方Discovery开发板,Nucleo开发板以及EVAL板的硬件驱动API文件,如果要编写相关的硬件驱动,可以参考这些文件夹中的程序。
CMSIS文件夹:
该文件夹内包含的是符号CMSIS标志的软件抽象层组件的相关文件。主要包括DSP库(DSP_LIB文件夹),Cortex-M内核及其设备文件(Include文件夹),微控制器专用头文件/启动代码/专用系统文件等(Device文件夹)。
STM32F1xx_HAL_Driver文件夹:
该文件夹包含了所有的STM32F1xx系列HAL库头文件和源文件,也就是所有底层硬件抽象层API声明和定义。它的作用是屏蔽了复杂的硬件寄存器操作,统一了外设的接口函数。该文件夹包含Src和Inc两个子文件夹,其中Src子文件夹存放的是.c源文件,Inc子文件夹存放的是与之对应的.h头文件。每个.c源文件对应一个.h头文件。源文件名称基本遵循stm32f1xx_hal_ppp.c定义格式,头文件名称基本遵循stm32f1xx_hal_ppp.h定义格式。
Projects文件夹:
该文件夹存放的是一些可以直接编译的实例工程。每一个文件夹对应一个ST官方的Demo板。
通过上面的介绍可以看出在STM32CubeF1固件包中用到的就Drivers、Projects文件夹中的文件。
5、创建基于HAL的新工程
新建一个文件夹,用来存放创建的工程,在这个文件夹中创建几个子文件夹,用来存放对应的文件。
CORTEX文件夹:
存放内核和启动文件。
MDK-PRO文件夹:
存放工程文件。
OUTPUT文件夹存:
存放工程编译生成的一些文件
STM32F1xx_HAL_Driver文件夹:
存放STM32F1 HAL库头文件和源文件。
最后在MDK-PRO文件夹下新建Inc和Src两个子文件夹。
打开MDK并点击创建新工程。
输入工程名称,然后选择将工程保存到MDK-PRO文件夹中。
下一步是选择对应的STM32 IC的型号,在这里选择STM32F103ZE。
点击OK就创建好新的工程了,只是这是一个空的工程,什么都没有,如下图。
接下来先将工程的编译输出文件设置到OUTPUT文件夹中。在MDK软件中,选择“Options for Target”,在弹出来的界面中选择Output选项卡,点击“Select Folder for Listings…”,在弹出来的界面中选择OUTPUT文件夹。
然后再选择Listing选项卡,点击“Select Folder for Listings…”,也在弹出来的界面中选择OUTPUT文件夹。
从STM32CubeF1包里复制文件到新工程里。
1、将"STM32Cube_FW_F1_V1.7.0\Drivers\STM32F1xx_HAL_Driver"文件夹中的Inc和Src文件夹复制到新工程的STM32F1xx_HAL_Driver文件夹下。
2、复制相应的启动文件和关键头文件到新工程目录下:
将STM32Cube_FW_F1_V1.7.0\Drivers\CMSIS\Device\ST\STM32F1xx\Source\Templates\arm文件夹中的startup_stm32f103xe.s文件复制到CORTEX文件夹中。
将STM32Cube_FW_F1_V1.7.0\Drivers\CMSIS\Include文件夹中的cmsis_armcc.h、core_cm3.h、core_cmFunc.h、core_cmInstr.h、core_cmSimd.h文件复制到CORTEX文件夹中。
3、复制工程需要用到的一些头文件和源文件。
在MDK-PRO文件夹中创建Inc和Src两个子文件夹,Inc存放头文件,Src存放源文件。
将STM32Cube_FW_F1_V1.7.0\Drivers\CMSIS\Device\ST\STM32F1xx\Include文件夹中的stm32f1xx.h、stm32f103xe.h、system_stm32f1xx.h文件复制到MDK-PRO文件夹的Inc文件夹中。
将STM32Cube_FW_F1_V1.7.0\Projects\STM32F103RB-Nucleo\Templates\Inc文件夹中的main.h、stm32f1xx_hal_conf.h、stm32f1xx_it.h文件复制到MKD-PRO文件夹的Inc文件夹中。
将STM32Cube_FW_F1_V1.7.0\Projects\STM32F103RB-Nucleo\Templates\Src文件夹中的main.c、stm32f1xx_hal_msp.c、stm32f1xx_it.c、system_stm32f1xx.c文件复制到MDK-PRO文件夹的Src文件夹中。
将上面步骤的文件复制完之后。用鼠标右击MDK工程栏中的Group,选择‘Manage Project Items’。然后添加如下图所示的Group。
在MDK中创建完分组之后,往Group里面添加需要的文件。
将STM32F1xx_HAL_Driver\Src文件夹下的.c源文件添加到工程中的HAL-LIBRARY组。
其它组所需要添加的文件如下图所示:
添加完文件到MDK后,点击OK,工程如下图所示:
指定头文件(.h文件)所在的路径:
很多.c源代码都有一个对应的.h文件,但是.h文件是存在不同的目录的,必须告诉编译器.h文件在哪个目录下,编译器才能找到对应的.h文件进行编译,否则就会报错。
如下图所示选择C/C++选项卡,点击Include Paths。将所有.h文件所在的文件夹添加进去。最后点击OK退出。
定义全局宏定义标识符,定义了全局宏定义标识符后,编译器会根据宏定义进行选择编译。打开C/C++选型卡,在define栏输入“USE_HAL_DRIVER, STM32F103xE”,这两个标识符在stm32f1xx.h文件当中有用到。
点击编译工程。提示找不到“stm32f1xx_nucleo.h”,双击跳转到出错点。
将44行的#include "stm32f1xx_nucleo.h"删除,再编译就没发现错误了。
但是当进行全部编译的时候发现有提示又错误。
这里的错误提示是指函数重复定义了。定义的函数在“stm32f1xx_hal_msp.c”和“stm32f1xx_hal_msp_template.c”这两个文件中。在这里将“stm32f1xx_hal_msp_template.c”文件Remove掉。如下图:
再将工程能的模板文件删除掉,找到“stm32f1xx_hal_timebase_rtc_alarm_template.c”和“stm32f1xx_hal_timebase_tim_template.c”并删除,带有template的就是模板文件。
再次编译文件,可以看到没有错误产生了。
6、系统初始化之后的中断优先级分组号和时钟设置
默认情况下调用HAL初始化函数HAL_Init之后,会将中断优先级分组设置为4,可以通过修改HAL_Init函数的内容来更改分组。打开stm32f1xx_hal.c文件,找到HAL_Init函数,如下图:
修改173行的HAL_NVIC_SetPPriorityGrouping函数中的参数就可以了。
时钟设置:
关于时钟的设置,需要注意stm32f1xx_hal_conf.h文件里面HSE_VALUE宏定义的值,这个值是外部晶振大小的值,如果跟实际值对不上,那么跑的时钟可能会跟预计的不同,需要注意。比如外部晶振用的是8M,那么HSE_VALUE宏的值应该设为8000000U,如下图:
7、工程文件说明
任何一个MDK工程,不管有多复杂,无非就是一些.c源文件和.h头文件,还有一些类似.s的启动文件或者lib文件等。在工程中,他们通过各种包含关系组织在一起,被用户代码最终调用或者引用。只要了解了这些文件的作用以及它们之间的包含关系,就能理解这个工程的运行流程。熟悉了文件后,可以自己删减外设功能,减少工程的代码量。
1、文件介绍:
1、HAL库文件介绍:
stm32f1xx_hal_ppp.c/.h文件是一些基本外设的操作API(接口函数),ppp代表任意外设。其中stm32f1xx_hal_cortex.c/.h比较特殊,它是一些Cortex内核通用函数声明和定义,如中断优先级NVIC配置,系统软复位以及Systick配置等。
stm32f1xx_hal_ppp_ex.c/.h文件是拓展外设特性的API(接口函数)。
stm32f1xx_hal.c文件包含了HAL通用API(比如HAL_Init,HAL_DeInit,HAL_Delay等)。
stm32f1xx_hal.h文件是HAL库的头文件,它应被客户代码所包含。
stm32f1xx_hal_conf.h文件是HAL库的配置文件,主要用来选择使能何种外设以及一些时钟相关参数设置。它应被客户代码所包含。
stm32f1xx_hal_def.h文件包含了HAL库的通用数据类型定义和宏定义。
2、stm32f1xx_it.c/ stm32f1xx_it.h文件
stm32f1xx_it.h中主要是一些中断服务函数的申明。stm32f1xx_it.c中是这些中断服务函数的定义,而这些函数定义除了Systick中断服务函数SysTick_Handler外基本都是空函数,没有任何控制逻辑。一般情况下,可以去掉这两个文件,然后把中断服务函数写在工程中的任何一个可见文件中。
3、stm32f1xx.h头文件
stm32f1xx.h文件很重要,它是所有stm32f1系列的顶层头文件,使用STM32F1任何型号的芯片,都需要包含这个头文件。同时因为stm32f1系列芯片型号非常多,ST为每种芯片型号定义了一个特有的片上外设访问层头文件,比如STM32F103xE系列,ST定义了一个头文件stm32f103xe.h,然后stm32F1xx.h顶层头文件会根据工程芯片型号,来选择包含对应芯片的片上外设访问层头文件。打开stm32f1xx.h文件可以看到如下图所示:
以STM32F103ZET6为例,如果定义了宏定义标识符STM32F103xE,那么头文件stm32f1xx.h将会包含头文件stm32f103xe.h。在前面的时候我们已经在C/C++选项卡里面输入全局宏定义标识符STM32F103xE.所以头文件stm32f103xe.h一定会被整个工程所引用。
4、stm32f103xe.h头文件
stm32f103xe.h头文件是STM32F103xE系列芯片通用的片上外设访问层头文件,只要进行STM32F103ZET6开发,就必然要使用到该文件。该文件主要是一些结构体和宏定义标识符。这个文件的主要作用是寄存器定义声明以及封装内存操作。
5、system_stm32f1xx.c/ system_stm32f1xx.h文件
这两个文件主要是声明和定义了系统初始化函数SystemInit以及系统时钟更新函数 SystemCoreClockUpdate。函数SystemInit的作用是进行时钟系统的一些初始化操作以及中断向量表编译地址设置,但它并没有设置具体的时钟值。在启动文件startup_stm32f103xe.s中设置系统复位后,直接调用SystemInit函数进行系统初始化。SystemCoreClockUpdate函数是在系统时钟配置进行修改后,调用这个函数来更新全局变量SystemCoreClock的值,变量SystemCoreClock是一个全局变量,开放这个变量可以方便我们在用户代码中直接使用这个变量来进行一些时钟运算。
6、stm32f1xx_hal_msp.c文件
MSP,全称MCU support package,函数名字找那个带有MSPInit的函数的作用是进行MCU级别硬件初始化设置,并且它们通常会被上一层的初始化函数所调用,这样做的目的是为了把MCU相关的硬件初始化剥夺出来,方便用户代码在不同型号的MCU上移植。stm32lxx_hal_msp.c文件定义了两个函数HAL_MspInit和HAL_MspDeInit。这两个函数分别被文件stm32f1xx_hal.c中的HAL_Init和HAL_DeInit所调用。HAL_MspInit函数的作用主要是进行MCU相关的硬件初始化操作。这样的话,在系统启动后调用了HAL_Init之后,会自动调用硬件初始化函数。实际上,在工程中直接删除掉stm32f1xx_hal_msp.c文件也不会对程序运行产生影响。
7、startup_stm32f103xe.s启动文件
STM32系列所有芯片工程都会有一个.s的启动文件,不同型号的stm32芯片启动文件也是不一样的。启动文件的作用主要是进行堆栈的初始化,中断向量表以及中断函数定义等。启动文件有一个很重要的作用就是系统复位后引导进入C语言的main函数。
如上图所示,Rest_handler是复位函数,在系统启动的时候会执行,系统启动后,先调用SystemInit函数进行系统初始化,然后引导进入main函数执行用户代码。
8、文件包含关系
整个工程的文件包含关系如下图所示:
从上图可以看出各个文件之间的包含关系,顶层头文件stm32f1xx.h直接或间接包含了其他所有工程必要头文件,所以在用户代码中,只需要包含顶层头文件stm32f1xx.h即可。
首先包含stm32f1xx.h,然后包含stm32f1xx_hal.h(.c/.h文件主要实现HAL库的初始化、系统滴答相关函数及CPU的调试模式配置),再包含stm32f1xx_hal_conf.h(该文件是一个用户级别的配置文件,用来实现对HAL库的裁剪)。
2、HAL库源文件说明:
HAL库文件名均以stm32f1xx_hal开头,后面加上_外设或者模块名,比如stm32f1xx_hal_adc.c。
初始化/反初始化函数:HAL_PPP_Init(),HAL_PPP_DeInit()。
IO操作函数:HAL_PPP_Read(),HAL_PPP_Write(),HAL_PPP_Transmit(),HAL_PPP_Receive()。
控制函数:HAL_PPP_Set(),HAL_PPP_Get()。
状态和错误:HAL_PPP_GetState(),HAL_PPP_GetError()。
在STM32Cube中包含了LL库,目前LL库和HAL库捆绑发布,所以在HAL库源码中,还有一些名为stm32f1xx_ll_ppp的源码文件,这些文件就是新增的LL库文件,使用CubeMX生成项目时,也可以选择LL库。
HAL库最大的特点就是对底层进行了抽象。在此结构下,用户代码的处理主要分为三大部分:
处理外设句柄(实现用户功能)。
处理MSP。
处理各种回调函数。
外设句柄:
HAL库在结构上,对每个外设抽象成了一个称为ppp_HandleTypeDef的结构体,其中ppp就是每个外设的名字。所有的函数都是工作的ppp_HandleTyepDef指针之下。
多实例支持:
每个外设/模块实例都有自己的句柄。因此实例资源是独立的。
外围进程相互通信:
该句柄用于管理进程例程之间的共享数据资源。
3、三种编程方式:
HAL库对所有的函数模型也进行了统一。在HAL库中,支持三种编程模式:轮询模式、中断模式、DMA模式(如果外设支持)。其中带有_IT的表示工作在中断模式下;带_DMA的工作在DMA模式下;什么都没带的就是轮询模式。
HAL库架构下统一采用宏的形式对各种中断等进行配置,针对每种外设主要有以下宏:
__HAL_PPP_ENABLE_IT(__HANDLE__,__INTERRUPT__):使能一个指定的外设中断。
__HAL_PPP_DISABLE_IT(__HANDLE__,__INTERRUPT__):失能一个指定的外设中断。
__HAL_PPP_GET_IT(__HANDLE__,__INTERRUPT__):获得一个指定的外设中断状态。
__HAL_PPP_CLEAR_IT(__HANDLE__,__INTERRUPT__):清除一个指定的外设中断状态。
__HAL_PPP_GET_FLAG(__HANDLE__,__FLAG__):获得一个指定的外设的标志状态。
__HAL_PPP_CLEAR_FLAG(__HANDLE__,__FLAG__):清除一个指定的外设的标志状态。
__HAL_PPP_ENABLE(__HANDLE__):使能外设。
__HAL_PPP_DISABLE(__HANDLE__):失能外设。
__HAL_PPP_XXX(__HANDLE__,__PARAM__):指定外设的宏定义。
__HAL_PPP_GET_IT_SOURCE(__HANDLE__,__INTERRUPT__):检查中断源。
8、HAL库函数中__weak修饰符讲解
在HAL库中,很多回调函数前面使用__weak修饰符,weak顾名思义是“弱”的意思,所以一般称加了__weak修饰符的函数为“弱函数”,最终编译器编译的时候,会选择用户自己定义的函数,如果用户没有重新定义这个函数,那么编译器就会执行__weak声明的函数,并且编译不会报错。
__weak在回调函数的时候经常用到。这样的好处是,系统默认定义了一个空的回调函数,保证编译器不会报错。同时,如果用户自己要定义用户回调函数,那么只需要重新定义即可,不需要考虑函数重复定义的问题。
9、Msp回调函数执行过程解读
当我们调用HAL的外设初始化函数,比如HAL_PPP_Init()(PPP代表某某某外设),那么在HAL_PPP_Init()函数内一般就会包含一个HAL_PPP_MspInit()的函数,该函数就是回调函数。该回调函数一般是一个空函数,而且是用__weak修饰符定义的,没有任何实际的控制逻辑,由于它是用__weak修饰符定义的函数,那么就可以重新定义HAL_PPP_MspInit()函数,最终执行的也是用户重新定义的HAL_PPP_MspInit()函数。
10、系统时钟
HAL库的系统时钟是通过SystemClock_Config函数进行初始化的。但是手动移植过来的SystemClock_Config函数并没有配置好。可以通过STM32CubeMX软件进行重新配置。
11、删减工程
HAL库包含了很多的功能,所有外设的功能都抽象出来了。在实际应用中有些外设是用不到的,如果把整个HAL库都包含着工程之中,那就会使得编译后的工程很大,不仅编译时间久,占用的资源也多。所以在实际应用中,只要包含有用到的外设功能就可以了了,其他的可以裁剪掉。
stm32f1xx_hal_conf.h这个头文件里面引用了HAL库源文件的.h文件。stm32f1xx_hal_conf.h中所定义的一些宏,通过定义宏来判断是否引用所对应的头文件,比如HAL_ADC_MODULE_ENABLED宏,用来确定是否引用头文件“stm32f1xx_hal_adc.h”。如下图所示:
如果某些外设用不到,可以屏蔽掉该外设所对应的宏使得对应的头文件不被包含。比如如果只用到GPIO口,那么其他外设可以屏蔽掉,但是需要注意的是,就算只用GPIO外设,有一些文件也是要用到的,比如RCC,如果屏蔽掉这些文件,就会造成编译出错。
如果实在不知道哪一些文件屏蔽删除会出错,可以逐条屏蔽删除后编译,确认是否会出错。
以下是只保留GPIO口功能的工程:
从图中可以看到只保留了HAL_MODULE_ENABLED、HAL_CORTEX_MODULE_ENABLED、HAL_FLASH_MODULE_ENABLED、HAL_GPIO_MODULE_ENABLED、HAL_RCC_MODULE_ENABLED这4个宏,其他的都屏蔽掉,编译并没有报错。
接下来是删减工程中添加的HAL库.c文件。删减后如下图所示:
可以看到HAL-LIBRARY组删除了很多文件,再编译发现也能通过,而且编译的速度比删除前快了很多。