06 HAL 实例:STM32 外设接口中的封装设计
06 HAL 实例:STM32 外设接口中的封装设计
C 语言面向对象系列
前面用 LED 示例介绍了结构体、对象指针、函数前缀和初始化接口。本节对照 STM32 HAL 的 GPIO 接口,看看这些写法在外设库中如何使用。
下文的结构体和函数是教学简化版,用于说明数据与接口的关系。实际项目应使用对应芯片的头文件和 HAL 实现。
先辨认接口中的类型和参数
GPIO 模块有初始化、反初始化、读写和翻转等接口。下面只列出函数名,省略调用参数:
1 | HAL_GPIO_Init(); |
阅读这些函数时,还会遇到结构体类型和宏:
1 | GPIO_TypeDef |
可以先按用途区分:
| 名称 | 用途 |
|---|---|
GPIO_TypeDef |
描述 GPIO 端口的寄存器布局 |
GPIO_InitTypeDef |
保存初始化配置 |
GPIOA/GPIOB |
指定具体 GPIO 端口 |
GPIO_PIN_5 |
指定端口中的引脚 |
GPIO_TypeDef *GPIOx |
向函数传入要操作的端口 |
HAL_GPIO_ |
标明函数属于 HAL 的 GPIO 模块 |
先找到类型定义和函数参数,比逐个记住函数名更容易理解接口之间的关系。
用结构体描述寄存器布局
GPIO 的寄存器可以用结构体成员表示。简化后如下:
1 | typedef struct { |
前面的 LED 对象也使用结构体:
1 | typedef struct { |
两者都能通过成员名访问数据,但数据的含义不同。Led_t 保存程序维护的软件状态,GPIO_TypeDef 描述硬件寄存器的布局。
实际芯片头文件还规定了成员偏移和访问限定。例如 STM32F407 的定义使用 __IO 标注寄存器成员。上面的简化定义省略了这些内容,不能直接替换芯片定义。具体可对照 ST 的 STM32F407 头文件。
通过指针区分 GPIO 端口
先用普通变量模拟三个端口,可以直观看到它们共用同一类型:
1 | GPIO_TypeDef gpioa_regs; |
这与创建多个 LED 对象的写法有相似之处:
1 | Led_t red_led; |
相同类型可以对应不同实例,函数通过指针区分当前操作的实例。区别在于,实际芯片上的 GPIOA、GPIOB 等指向固定的外设地址,而不是上面声明的普通 RAM 变量。
GPIOx 参数指定操作端口
下面用简化的写引脚函数说明参数如何参与操作:
1 | void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t pin, bool value) |
GPIOx 指定端口,pin 指定端口内的引脚或引脚组合,value 表示要写入的电平。函数通过 GPIOx->ODR 访问选定端口的输出寄存器。
这与 Led_t *me 的共同点是通过指针选择操作对象:
| LED 示例 | GPIO 示例 |
|---|---|
Led_t *me 指向 LED 数据 |
GPIO_TypeDef *GPIOx 指向端口寄存器 |
从 me->pin 读取引脚配置 |
通过 GPIOx->ODR 访问输出寄存器 |
led_on(&red) 操作红灯 |
示例中的 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, true) 操作端口 A 的引脚 5 |
实际 STM32F4 HAL 的第三个参数是 GPIO_PinState,使用 GPIO_PIN_SET 或 GPIO_PIN_RESET,实现通过 BSRR 写入。上面的 bool 参数和 ODR 读改写只用于简化讲解。参见 ST 的 GPIO 驱动实现。
用配置结构体完成初始化
初始化时,可以将引脚、模式、上下拉和速度等参数放在配置结构体中:
1 | typedef struct { |
初始化函数接收端口指针和配置指针,逐项设置对应寄存器:
1 | void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *init) |
这里的两个参数分工明确:GPIOx 指定配置哪个端口,init 指定如何配置。它与前面的 led_init() 都承担使用前的准备工作,但寄存器配置和软件对象初始化的具体步骤需要分别理解。
将公共声明与内部实现分开
按前面介绍的模块组织方式,简化接口可以声明在头文件中:
1 | void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *init); |
实现所需的辅助函数可以放在源文件内:
1 | static void set_2bit_field(uint32_t *reg, int pin, uint32_t value); |
调用方使用公开接口,具体的寄存器位操作由实现处理。这里的辅助函数同样用于教学示意,阅读实际 HAL 源码时,应以对应版本的实现为准。
按数据和调用关系阅读外设库
阅读其他外设模块时,可以沿着下面的顺序查找:
| 阅读内容 | 查找位置 |
|---|---|
| 寄存器布局或对象状态 | 核心结构体定义 |
| 本次操作的设备 | 函数中的端口或句柄参数 |
| 对外支持的操作 | 头文件中的函数声明 |
| 内部处理步骤 | 源文件中的实现和辅助函数 |
| 使用前的准备 | Init 接口 |
| 结束使用时的处理 | DeInit 接口 |
UART、SPI、TIM 等接口也可以从类型和参数入手。以 UART 为例,UART_HandleTypeDef huart1 声明句柄,HAL_UART_Init(&huart1) 初始化,HAL_UART_Transmit(&huart1, buf, 10, 100) 通过句柄指定本次发送使用的外设。GPIO 的寄存器指针与 UART 的句柄各有职责,阅读时需要结合各自的类型定义。










