STM32使用HAL库的初始化过程
HAL库驱动包
首先我们来介绍一下HAL库驱动包,HAL库是ST推出的STM32Cube软件生态下的一个分支,STM32Cube是ST公司提供的一套免费开发工具和STM32Cube固件包,包含两个关键部分
- 允许用户通过图形化向导来生成C语言工程图形配置工具
STM32CubeMX
可以通过CubeMX实现方便地下载各种软件或开发固件包 - 包括由
STM32Cube
硬件抽象层(HAL),还有一组一致的中间件组件(RTOS,USB,FAT文件系统,图形,TCP/IP和以太网),以及一系列完整的例程组成的STM32Cube固件包
新建HAL库版本MDK工程
新建HAL库版本MDK工程分为两个步骤:
- 新建工程文件夹
- 拷贝工程相关文件
新建工程文件夹
为了使工程的文件目录结构更加清晰,我们全在工程的根目录文件夹下建立以下几个文件夹,文件夹名称及其作用如下表
名称 | 作用 |
---|---|
Drivers | 存放于硬件相关的驱动层文件 |
Middlewares | 存放中间层组件文件和第三方中间层文件 |
Output | 存放工程编译输出文件 |
Projects | 存放MDK工程文件 |
User | 存放HAL库用户配置文件、main.c、stm32f1xx_it.c、以及我们自己编写的其他应用程序 |
拷贝工程相关文件
Drivers文件夹
该文件夹用于存放与硬件相关的驱动层文件,一般包括如下表所示的四个文件夹
文件夹名称 | 作用 |
---|---|
BSP | 存放开发板板级驱动代码 |
CMSIS | 存放CMSIS底层代码,如启动文件(.s文件)、stm32f1xx.h等 |
STM32F1xx_HAL_Driver | 存放ST提供的F1XX HAL库驱动代码 |
SYSTEM | 存放系统级驱动核心代码 |
CMSIS文件夹
CMSIS 文件夹,用于存放 CMSIS 底层代码(ARM 和 ST 提供,如:启动文件(.s 文件)、stm32f1xx.h 等各种头文件。该文件夹我们可以直接从 STM32CubeF1固件包里面拷贝,CMSIS 的文件夹路径在“STM32CubeF1 固件包→Drivers”。由于这个文件夹原来设计是用于匹配全部 F1 系列的芯片的,导致非常大, 部分文件对我们的例程来说不会使用到,而且浪费磁盘的存储空间,所以我们会对这个文件夹 进行精简:打开目录“CMSIS\Device\ST\STM32F1xx”,其中的Include文件夹里都是芯片的头文件我们只留下如图1这三个头文件,其他删除
Source文件夹下的Templates 文件夹留下如图2的内容
arm 文件夹存放的是启动文件,我们只需要 startup_stm32f103xe.s,其他全部删除。如图3所示
最后就是 CMSIS 文件夹下的 Include 文件夹,里面都是内核的头文件,我们只需要如图4的内容
SYSTEM 文件夹
SYSTEM 文件夹里面的代码由正点原子提供,是 STM32F1xx 系列的底层核心驱动函数, 可以用在 STM32F1xx系列的各个型号上面,方便大家快速构建自己的工程。
User文件夹
User文件夹用于存放HAL库用户配置文件stm32f1xx_it.c
和stm32f1xx_it.h
、main.c
、中断处理以及配置文件stm32f1xx_hal_conf.h
STM32F1时钟配置
时钟源
对于STM32F1
输入时钟源主要包括HSI
,HSE
,LSI
,LSE
其中,从时钟频率来分可以分为高速时钟源和低速时钟源,HSI
和HSE
是高速时钟,LSI
和LSE
是低速时钟
从来源可分为外部时钟源和内部时钟源
外部时钟源就是从外部接晶振的方式获取时钟源包括HSE
和LSE
,HSI
和LSI
是内部时钟源,芯片上电即可产生
我们的配置过程本质就是将时钟源输出的时钟信号经过各种倍频和分频操作得到我们想要的频率
使用HAL库配置STM32F1时钟系统
编写自己的时钟配置函数在main函数里调用
在系统启动之后,程序会先执行SystemInit
函数,进行系统一些初始化配置,主要做了如下两个方面工作
- 外部存储器配置
- 中断向量表地址配置
HAL库的SystemInit
函数并没有任何时钟相关设置,所以后续的初始化步骤我们必须编写自己的时钟配置函数,看看正点原子定义的 sys.c 文件中的时钟设置函数sys_stm32_clock_init
的内容:
/**
* @brief 系统时钟初始化函数
* @param plln: PLL倍频系数(PLL倍频), 取值范围: 2~16
中断向量表位置在启动时已经在SystemInit()中初始化
* @retval 无
*/
void sys_stm32_clock_init(uint32_t plln)
{
HAL_StatusTypeDef ret = HAL_ERROR;
RCC_OscInitTypeDef rcc_osc_init = {0};
RCC_ClkInitTypeDef rcc_clk_init = {0};
rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; /* 选择要配置HSE */
rcc_osc_init.HSEState = RCC_HSE_ON; /* 打开HSE */
rcc_osc_init.HSEPredivValue = RCC_HSE_PREDIV_DIV1; /* HSE预分频系数 */
rcc_osc_init.PLL.PLLState = RCC_PLL_ON; /* 打开PLL */
rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; /* PLL时钟源选择HSE */
rcc_osc_init.PLL.PLLMUL = plln; /* PLL倍频系数 */
ret = HAL_RCC_OscConfig(&rcc_osc_init); /* 初始化 */
if (ret != HAL_OK)
{
while (1); /* 时钟初始化失败,之后的程序将可能无法正常执行,可以在这里加入自己的处理 */
}
/* 选中PLL作为系统时钟源并且配置HCLK,PCLK1和PCLK2*/
rcc_clk_init.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);
rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; /* 设置系统时钟来自PLL */
rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; /* AHB分频系数为1 */
rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV2; /* APB1分频系数为2 */
rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1; /* APB2分频系数为1 */
ret = HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_2); /* 同时设置FLASH延时周期为2WS,也就是3个CPU周期。 */
if (ret != HAL_OK)
{
while (1); /* 时钟初始化失败,之后的程序将可能无法正常执行,可以在这里加入自己的处理 */
}
sys.c
文件中的时钟设置函数sys_stm32_clock_init
,除了配置PLL
相关参数确定SYSCLK
值之外,还配置了AHB
,AHB1
和AHB2
的分频系数,也就是确定了HCLK
,PCLK1
和PCLK2
的时钟值。主要分为两个步骤
- 配置时钟源相关参数:调用函数 HAL_RCC_OscConfig()
- 配置系统时钟源SYSCLK以及AHB、APB1和APB2的分频系数:调用函数 HAL_RCC_ClockConfig()
STM32F1时钟使能和配置
在配置好时钟系统之后,如果我们要使用某些外设,例如GPIO,ADC等,我们还要使能这些外设时钟。STM32的外设时钟使能是在RCC相关寄存器中配置的。在STM32F1的HAL库中,外设时钟使能操作都是在RCC相关固件库文件头文件STM32F1xx_hal_rcc.h
定义的。外设时钟使能在HAL库中都是通过宏定义标识符来实现的,首先,我们来看看 GPIOA 的外设时钟使能宏定义标识符:
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { __IO uint32_t tmpreg; SET_BIT(RCC->APB2ENR, \ RCC_APB2ENR_IOPAEN); tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN); UNUSED(tmpreg); } \ while(0U)
这段代码主要是定义了一个宏定义标识符__HAL_RCC_GPIOA_CLK_ENABLE()
,它的核 心操作是通过下面这行代码实现的:
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
这行代码的作用是设置寄存器RCC->APB2ENR
的相关位为1,至于是哪个位,是由宏定义标识符RCC_APB2ENR_IOPAEN的值决定的, 而它的值为:
#define RCC_APB2ENR_IOPAEN_Pos (0U)
#define RCC_APB2ENR_IOPAEN_Msk (0x1UL << RCC_APB2ENR_IOPAEN_Pos)
#define RCC_APB2ENR_IOPAEN RCC_APB2ENR_IOPAEN_Msk
上面三行代码很容易计算出来RCC_APB2ENR_IOPAEN= (0x00000001<<2)
,因此上面代码的作用是设置寄存器RCC->APB2ENR
的位2为1,我们可以从STM32F1的参考手册中搜索APB2ENR寄存器定义,位2的作用是用来使用GPIOA时钟。APB2ENR寄存器的位2描述如下:
位 0 IOPAEN:IO 端 A 时钟使能(I/O port A clock enable)
由软件置‘1’或清‘0’
0:IO 端口 A 时钟关闭
1:IO 端口 A 时钟开启
那么我们只需要在我们的用户程序中调用宏定义标识符就可以实现GPIOA时钟使能。使用方法为:
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能 GPIOA 时钟 */
对于其他外设,同样都是在STM32F1xx_hal_rcc.h
头文件中定义,大家只需要找到相关宏定义标识符即可,这里我们列出几个常用使能外设时钟的宏定义标识符使用方法:
__HAL_RCC_DMA1_CLK_ENABLE(); /* 使能 DMA1 时钟 */
__HAL_RCC_USART2_CLK_ENABLE(); /* 使能串口 2 时钟 */
__HAL_RCC_TIM1_CLK_ENABLE(); /* 使能 TIM1 时钟 */
我们使用外设的时候需要使能外设时钟,如果我们不需要使用某个外设,同样我们可以禁止某个外设时钟。禁止外设时钟使用方法和使能外设时钟非常类似,同样是头文件中定义的宏 定义标识符。我们同样以 GPIOA 为例,宏定义标识符为:
#define __HAL_RCC_GPIOA_CLK_DISABLE() (RCC->APB2ENR) &= ~ (RCC_APB2ENR_GPIOAEN)
同样,宏定义标识符__HAL_RCC_GPIOA_CLK_DISABLE()
的作用是设置RCC->APB2ENR
寄存器的位2为0,也就是禁止GPIOA时钟。具体使用方法我们这里就不做过多讲解,我们这里同样列出几个常用的禁止外设时钟的宏定义标识符使用方法:
__HAL_RCC_DMA1_CLK_DISABLE(); /* 禁止 DMA1 时钟 */
__HAL_RCC_USART2_CLK_DISABLE(); /* 禁止串口 2 时钟 */
__HAL_RCC_TIM1_CLK_DISABLE(); /* 禁止 TIM1 时钟 */