一、关键文件介绍
1、HAL库关键文件
stm32f4xx_hal_ppp.c/.h
基本外设的操作API,ppp代表任意外设。其中stm32f4xx_hal_cortex.c/.h比较特殊,它是一些Cortex内核通用函数声明和定义,例如中断优先级NVIC配置,系统软复位以及Systick配置等。
stm32f4xx_hal_ppp_ex.c/.h
拓展外设特性的API。
sm32f4xx_hal.c
包含HAL通用API(比如HAL_Init,HAL_DeInit,HAL_Delay 等)。
stm32f4xx_hal.h
HAL的头文件,它应被客户代码或包含。
stm32f4xx_hal_conf.h
HAL的配置文件,主要用来选择使能何种外设以及一些时钟相关参数设置。其本身应该被客户代码包含。
stm32f4xx_hal_def.h
包含HAL的通用数据类型定义和宏定义。
stm32f4xx_II_ppp.c/.h
在一些复杂外设中实现底层功能,它们在stm32f4xx_hal_ppp.c 中被调用。
2、stm32f4xx_it.c/stm32f4xx_it.h 文件
stm32f4xx_it.h中主要是一些终端服务函数的声明。stm32f4xx_it.c 中是这些中断服务函数定义,而这些函数定义除了Systick中断服务函数Systick_Handler 外基本都是空函数,没有任何控制逻辑。一般情况下,我们可以去掉这两个文件,然后把中断服务函数写在工程中的任何一个可见文件中。
3、stm32f4xx.h 头文件
stm32f4xx.h 头文件,它是所有stm32f4系列的顶层头文件。使用stm32f4任何型号的芯片,都需要包含这个头文件。同时,因为stm32f4系列芯片型号非常多,ST为每种芯片型号定义了一个特有的片上外设访问层头文件,比如 STM32F407
系列, ST 定义了一个头文件 stm32f407xx.h,然后 stm32f4xx.h 顶层头文件会根据工程芯片型号,来选择包含对应芯片的片上外设访问层头文件。我们可以打开 stm32f4xx.h 头文件可以看到,里面有如下几行代码:
#if defined(STM32F405xx)
#include “stm32f405xx.h”
#elif defined(STM32F415xx)
#include “stm32f415xx.h”
#elif defined(STM32F407xx)
#include “stm32f407xx.h”
#elif defined(STM32F417xx)
#include “stm32f417xx.h”
#elif defined(STM32F427xx)
#include “stm32f427xx.h”
#elif defined(STM32F437xx)
#include “stm32f437xx.h”
#elif defined(STM32F429xx)
#include “stm32f429xx.h”
#elif defined(STM32F439xx)
#include “stm32f439xx.h”
#elif defined(STM32F401xC)
#include “stm32f401xc.h”
#elif defined(STM32F401xE)
#include “stm32f401xe.h”
#elif defined(STM32F410Tx)
#include “stm32f410tx.h”
#elif defined(STM32F410Cx)
#include “stm32f410cx.h”
#elif defined(STM32F410Rx)
#include “stm32f410rx.h”
#elif defined(STM32F411xE)
#include “stm32f411xe.h”
#elif defined(STM32F446xx)
#include “stm32f446xx.h”
#elif defined(STM32F469xx)
#include “stm32f469xx.h”
#elif defined(STM32F479xx)
#include “stm32f479xx.h”
#elif defined(STM32F412Cx)
#include “stm32f412cx.h”
#elif defined(STM32F412Zx)
#include “stm32f412zx.h”
#elif defined(STM32F412Rx)
#include “stm32f412rx.h”
#elif defined(STM32F412Vx)
#include “stm32f412vx.h”
#elif defined(STM32F413xx)
#include “stm32f413xx.h”
#elif defined(STM32F423xx)
#include “stm32f423xx.h”
#else
#error “Please select first the target STM32F4xx device used in your application (in stm32f4xx.h file)”
#endif
如果定义了宏定义标识符 STM32F407xx,那么头文件 stm32f4xx.h 将会包含头文件 stm32f407xx.h。
4、stm32f407xx.h 头文件
stm32f407xx.h 是stm32f407系列芯片通用的片上外设访问层头文件,只要我们进行stm32f407开发,就必须使用到该文件。打开该文件我们可以看到里面主要是一些结构体和宏定义标识符。这个文件的主要作用是寄存器定义声明以及封装内存操作。
5、system_stm32f4xx.c/system_stm32f4xx.h 文件
头文件system_stm32f4xx.h和源文件system_stm32f4xx.c主要是声明和定义了系统初始化函数SystemInit以及系统时钟更新函数SystemCoreClockUpdate。 SystemInit 函数的作用进行时钟系统的一些初始化操作以及中断向量表偏移地址设置,但它并没有设置具体的时钟值,这是与标准库的最大区别,在使用标准库的时候,SystemInit 函数会帮我们配置好系统时钟配置相关的各个寄存器。在启动文件 startup_stm32f407xx.s中会设置系统复位后,直接调用 SystemInit 函数进行系统初始化。SystemCoreClockUpdate函数是在系统时钟配置进行修改后,调用这个函数来更新全局变量SystemCoreClock 的值,变量 SystemCoreClock 是一个全局变量,开放这个变量可以方便我们在用户代码中直接使用这个变量来进行一些时钟运算。
6、stm32f4xx_hal_msp.c 文件
MSP,全称MCU support package。stm32f4xx_hal_msp.c文件中定义了两个函数HAL_MspInit和HAL_MspDeInit。这两个函数分别被文件stm32f4xx_hal.c 中的 HAL_Init 和 HAL_DeInit 所调用。HAL_MspInit 函数的主要作用是进行MCU相关的硬件初始化操作。例如我们要初始化某些硬件,我们可以硬件相关的初始化配置写在HAL_MspDeInit函数中。这样的话,在系统启动后调用了HAL_Init之后,会自动调用硬件初始化函数。实际上,我们在工程模块中直接删除stm32f4xx_hal_msp.c文件也不会对程序运行产生任何影响。
7、startup_stm32f407xx.s 启动文件
STM32系列所以芯片工程都会有一个.s启动文件。对于不同型号的stm32芯片启动文件也是不一样的。我们的开发板是STM32F407系列,所以我们需要使用与之对应的启动文件startup_stm32f407xx.s。启动文件的作用是进行堆栈的初始化,中断向量表以及中断函数定义等。启动文件有一个很重要的作用就是系统复位后引导进行main函数。打开启动文件startup_stm32f407xx.s ,可以看到下面几行代码:
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
Reset handler在我们系统启动的时候会执行,这几行代码的作用是在系统启动之后,首先调用SystemInit函数进行系统初始化,然后引导进入main函数执行用户代码。
接下来我们看看HAL库工程模块中各个文件之间的包含关系:
从上面工程文件包含关系图可以看出,顶层头文件stm32f4xx.h直接或间接包含了其他所有工程必要头文件,所以在我们的用户代码中,我们只需要包含顶层头文件stm32fxx.h即可。
二、HAL库中__weak 修饰符讲解
在HAL库中,很多回调函数前面使用__weak修饰符。weak顾名思义是“弱”的意思,所以如果函数名称前面加上__weak修饰符,我们一般称这个函数为“弱函数”。加上了__weak修饰符的函数,用户可以在用户文件中重新定义一个同名函数,最终编译器编译的时候,会选择用户定义的函数,如果用户没有重新定义这个函数,那么编译器就会执行__weak声明的函数,并且编译器不会报错。
例如:
stm32f4xx_hal.c 文件,里面定义了一个函数 HAL_MspInit,定义如下:
__weak void HAL_MspInit(void)
{
__IO uint32_t tmpreg = 0x00;
UNUSED(tmpreg);
}
可以看出,HAL_MspInit函数前面有加修饰符__weak。同时,在该文件的前面有定义函数HAL_Init,并且HAL_Init函数中调用了函数HAL_MspInit。
HAL_StatusTypeDef HAL_Init(void)
{
…//此处省略部分代码
HAL_MspInit();
return HAL_OK;
}
如果我们没有在工程中其他地方重新定义HAL_MspInit函数,那么HAL_Init初始化函数执行的时候,会默认执行stm32f4xx_hal.c文件中定义的HAL_MspInit函数,而这个函数没有任何控制逻辑。如果用户在工程中重新定义函数HAL_MspInit,那么调用HAL_Init之后,会执行用户自己定义的HAL_MspInit函数而不是执行stm32f4xx_hal.c默认定义的函数。也就是说,表面上我们看到函数HAL_MspInit被定义了两次,但是因为有一次定义是弱函数,使用了__weak修饰符,所以编译器不会报错。
__weak在回调函数的时候经常用到。这样的好处是,系统默认定义了一个空的回调函数,保证编译器不会报错。同时,如果用户自己要定义用户回调函数,那么只需要重新定义即可,不需要考虑函数重复定义的问题,使用非常方便,在HAL库中__weak关键字被广泛使用。
三、Msp 回调函数执行过程解读
打开工程模块SYSTEM分组下面的usart.c文件可以看到,内部我们定义了两个函数uart_init和HAL_UART_MspInit。我们先来大致看看这两个函数的定义。
//初始化IO 串口1
//bound:波特率
void uart_init(u32 bound)
{
//UART 初始化设置
UART1_Handler.Instance=USART1; //USART1
UART1_Handler.Init.BaudRate=bound; //波特率
UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为8位数据格式
UART1_Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位
UART1_Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位
UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控
UART1_Handler.Init.Mode=UART_MODE_TX_RX; //收发模式
HAL_UART_Init(&UART1_Handler); //HAL_UART_Init()会使能UART1
HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);//该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量
}
//UART底层初始化,时钟使能,引脚配置,中断配置
//此函数会被HAL_UART_Init()调用
//huart:串口句柄
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_Initure;
if(huart-》Instance==USART1)//如果是串口1,进行串口1 MSP初始化
{
__HAL_RCC_GPIOA_CLK_ENABLE(); //使能GPIOA时钟
__HAL_RCC_USART1_CLK_ENABLE(); //使能USART1时钟
GPIO_Initure.Pin=GPIO_PIN_9; //PA9
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FAST; //高速
GPIO_Initure.Alternate=GPIO_AF7_USART1; //复用为USART1
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA9
GPIO_Initure.Pin=GPIO_PIN_10; //PA10
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA10
#if EN_USART1_RX
HAL_NVIC_EnableIRQ(USART1_IRQn); //使能USART1中断通道
HAL_NVIC_SetPriority(USART1_IRQn,3,3); //抢占优先级3,子优先级3
#endif
}
}
用户函数uart_init主要作用是设置串口1相关的参数,包括波特率,停止位,奇偶校验位等,并且最终是通过调用HAL_UART_Init函数进行参数设置。而函数HAL_UART_MspInit则主要进行串口GPIO引脚初始化设置。接下来我们打开usart_Init函数内部调用UART初始化函数HAL_UART_Init可以看到代码如下:
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)
{
…//此处省略部分代码
if(huart-》State == HAL_UART_STATE_RESET)//如果串口没有进行过初始化
{
huart-》Lock = HAL_UNLOCKED;
HAL_UART_MspInit(huart);
} …
//此处省略部分代码
return HAL_OK;
}
在函数HAL_UART_Init内部,通过判断逻辑判断如果串口还没有进行初始化,那么会调用函数HAL_UART_MspInit进行相关初始化设置。同时,我们可以看到,在文件stm32f4xx_hal_uart.c 内部,有定义一个弱函数 HAL_UART_MspInit。
__weak void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
/* NOTE: This function should not be modified, when the callback is needed,
the HAL_UART_MspInit could be implemented in the user file
*/
}
这里定义的弱函数HAL_UART_MspInit是一个空函数,没有任何实际的控制逻辑。根据前面的讲解可知,__weak修饰符定义的弱函数如果用户自己重新定义了这个函数,那么会有限执行用户定义函数。所以,实际上在函数 HAL_UART_Init 内部调用的 HAL_UART_MspInit()函数,最终执行的是用户在 usart.c 中自定义的 HAL_UART_MspInit()函数。
那么整个串口初始化的过程为:用户函数usart_init-》HAL_UART_Init-》HAL_UART_MspInit。
为什么串口相关初始化不在HAL_UART_Init函数内部一次初始化而还要调用函数HAL_UART_MspInit呢?实际计时HAL库的一个优点,它通过开放一个回调函数HAL_UART_MspInit,让用户自己去编写与串口想干的MCU级别的硬件初始化,而与MCU无关的串口参数相关的通用配置则放在HAL_UART_Init。
四、程序执行流程图
启动文件startup_stm32f429xx.s 中 Reset_Handler 部分会引导先执行SystemInit函数,然后再进入main函数。再main函数内部,一般情况下,我们会把HAL初始化函数HAL_Init放在最开头部分,然后再进行时钟初始化设置。这些设置完成之后,接下来便是调用外设初始化函数HAL_PPP_Init进行外设参数初始化设置,同时重写回调函数HAL_PPP_MspInit进行外设MCU相关的参数设置。最后编写我们的控制逻辑。