STM32 HAL 库基础

深入理解 STM32 HAL 库的设计思想

点灯之前……

STM32 相比与 8051 系列太复杂了,不是我说,在 STC89C52RC 中,你可以清楚地看到每个寄存器是干啥的,直接操控寄存器来完成每个控制动作,同时执行逻辑也很清晰,一条由 RAM 到 ALU 的流水线,但是到了 STM32 来说,两者同样是「改良型哈弗架构」,后者却是 32 位的微控制器,在单寄存器能实现 32 位数据的同时,即使是最低端的 F1 系列仍然具有数以百计的寄存器,高端型号寄存器数目甚至突破千位,寄存器的记忆本身就是一个麻烦的地方,而正是基于寄存器设计的官方库文件,标准库,也在数年前被弃用。在官方指导下,人们正式转向了经过封装抽象过的 HAL 库,HAL 本身即为 “Hardware Abstraction Layer(硬件抽象层)” 的缩写,通过将对寄存器的操作封装位函数,实现了以调用函数为基础的程序编写,此时,对于寄存器的高阶需求则全面转向与 HAL 相配套的 LL 库。

[!Note] 改良哈弗架构是什么? 众所周知,在图灵机理想模型具象化之后,冯依诺曼机作为一种工程架构被提出,即计算机需要由运算器、控制器、存储器、输入设备、输出设备五个部件组成。

由这种架构思维,发展出了多种实际架构,如程序指令和数据存放在同一个存储空间中,并且共用同一条总线(连接CPU和存储器的通道)的冯依诺曼架构,不难想到其灵活性很好高,程序可以像数据一样被修改,但是缺点也很明显,因为共用一条总线,CPU无法同时读取指令和读写数据,会产生所谓的“冯·诺依曼瓶颈”,限制了性能。

程序指令和数据分别存放在两个独立的存储空间中,并且各自拥有独立的总线,拥有两类不同的指令,此时CPU可以同时读取指令和读写数据,速度非常快,突破了冯·诺依曼瓶颈。缺点也很明显,结构相对复杂,并且由于指令和数据严格分离,程序在运行时不能轻易地修改自身。

于是想要兼具两者特点的「改良型哈弗架构」出现了,在CPU内核层面,它是哈佛架构:CPU内核拥有两条独立的总线:一条叫I-Bus (Instruction Bus),专门用于去获取程序指令;另一条叫D-Bus (Data Bus),专门用于读写数据。而在内存管理层面,它又是统一的冯·诺依曼架构:虽然有两条总线,但这两条总线最终都连接到了一个统一的、4GB的地址空间上。无论是存放程序的Flash,还是存放数据的SRAM,都被映射到了这个统一的地址空间里。CPU在一个时钟周期内,可以一边执行上一条指令,一边预取下一条指令,极大地提高了流水线效率和执行速度。

HAL 库的设计自有其逻辑,核心主要是「对象」,将外设视为多个独立的对象,通过对象组织寄存器,由于 C 中并没有对象的概念,因此 ST 大量使用了结构体和函数对该外设对象的进行封装,称作句柄(Handle)

句柄的命名是由固定格式的,即 name_HandleTypeDef,如 UART_HandleTypeDef 即是串口的句柄、I2C_HandleTypeDef 就是 I2C的句柄,其既包含了该外设的所有配置参数、也记录了其实时状态。

HAL 库的外设初始化总是分为四步:

  • 使能时钟:STM32 在初始情况下总是默认关闭所有外设,为此需要配置其属于开启状态
    • 外设本身运作需要时钟:此时即需要调用 __HAL_RCC_name_CLK_ENABLE()name 为外设名称,如 LPUART1
    • 外设用到的 GPIO 引脚需要时钟:此时需要调用 __HAL_RCC_GPIOnumber_CLK_ENABLE()number 为 GPIO 引脚编号,如 GPIOA
  • 配置 GPIO 输出模式:为了配置 GPIO 口,我们需要定义并一张 GPIO_InitTypeDef 类型的表单,当定义结束后,通过 HAL_GPIO_Init() 函数来提交申请。 对于该结构体如何定义,在代码里保持按下 Ctrl并使用鼠标点击即可得到其定义
    • uint32_t Pin:当前端口内(如前所述 GPIOA)的针脚编号,可以是 GPIO_PIN_numbernumber 取引脚编号,也可以是 GPIO_PIN_ALL 来指该端口的所有针脚,也可以使用“按位或 |”来指定多个引脚
    • uint32_t Mode输出/输出模式,主要有 GPIO_MODE_INPUT 输入模式、GPIO_MODE_OUTPUT_PP 推挽输出模式、GPIO_MODE_OUTPUT_OD 开漏输出模式、GPIO_MODE_AF_PP / GPIO_MODE_AF_OD 复用功能模式、GPIO_MODE_ANALOG 模拟模式
    • uint32_t Pull默认倾向GPIO_NOPULL 针脚浮空,无上拉或下拉、GPIO_PULLUP 上拉、GPIO_PULLDOWN 下拉
    • uint32_t Speed反应速度,即电平从高到低或从低到高变化的快慢,由“压摆率”实现。参数为 GPIO_SPEED_FREQ_speed ,其中 speed 可选 LOWMEDIUMHIGHVERY_HIGH,速度越高功耗越大,EMI 更强,会产生更尖锐的信号边缘,一般选满足需求的最低速度
    • uint32_t Alternate复用指定,当 Mode 为复用模式时,该参数有效,其被用来指定该引脚要被复用为何种功能,功能的 AF 编号由 Datasheet 给出,也可使用 HAL 库中的别名
  • 配置外设本身:为了配置外设本身,我们需要进行以下步骤
    • 创建外设句柄:使用 device_HandTypeDef name 创建一个命名为 name 关于外设 device 的句柄
    • 填写参数:根据 HAL 定制的要求,将句柄所需参数填写入结构体表单,即定义结构体中的变量值
    • 初始化外设:调用 HAL_device_Init 函数,传入所需变量,如 HAL_GPIO_Init( GPIOA, &name) 传入 GPIO 端口编号与结构体表单地址
  • (可选)配置中断:若要实现非阻塞式数据处理,则可将外设绑定至中断进行,一般有两步:
    • 设定中断优先级HAL_NVIC_SetPriority(),三个参数
      • IRQn:外设中断编号
      • PreemptPriority:抢占优先级,在 G4 系列中,值为 0-15,数字越小优先级越高,级别高低直接决定了目前执行的中断是否能被突发的另一个中断打断,高能打断低
      • SubPriority:响应优先级 / 亚优先级,当两个中断优先级一致且需要一个打断另一个时候,又或者是两个中断同时发生时候,依靠该值比较谁先发生,值越小优先级越高
    • 使能中断HAL_NVIC_EnableIRQ(),参数为 IRQn_Type IRQn 其中 IQRn 是枚举类型,是每个外设唯一的中断编号,在头文件中预置,如 USART1_IQRn 为串口 1 中断、TIM2_IQRn 为定时器 2 中断
  • 使用函数操作外设:外设操作求公约数的的话……大概就是以下几个
    • 启动/发送/接收(轮询模式,会阻塞):一般以 HAL_device_...() 命名,如 HAL_UART_Transmit()
    • 启动/发送/接收(中断模式,不阻塞):一般以 HAL_device_..._IT() 命名,如 HAL_UART_Receive_IT()
    • 启动/发送/接收(DMA模式,不阻塞):一般以 HAL_device_..._DMA() 命名,HAL_I2C_Master_Transmit_DMA()
    • 中断处理函数:device_IRQHandler() ,例如 LPUART1_IRQHandler()

[!Note] 到这里这一部分算是结束了 hhh,其实这部分介绍主要是理清思路的,除了操作函数外,所有的代码都不用手写的 hhh,而我们的 STM32CubeMX 就是干这个的,图形化生成出来的就是时钟设定、以及各个外设与端口的设定代码,实际上也就解答了大部分 STM32CubeMX 怎么配置的问题(除了时钟树)

一、点灯

[!Note] 前期只使用 PIO 自带配置创建项目

PIO 的项目代码内部不需要 SystemClock_Config() 函数来初始化时钟,起码自带配置创建时是不需要的,加上能过编译烧录……但是会 HardFault

点灯代码如下: