06 HAL 映射:工业库的封装结构

C 语言 OOP 系列导航

本系列面向会 C 和单片机基础、刚进入企业项目的同学,从 struct 封装一路讲到 ops、多态和类 Linux 驱动框架。

  1. C 语言面向对象设计路线
  2. 封装:struct 数据与 me 指针
  3. 信息隐藏:static 与头文件边界
  4. 类的组织:前缀、init/deinit 与生命周期
  5. 数据归位:struct 成员与 static 变量
  6. HAL 映射:工业库的封装结构 ⇐ 当前位置
  7. 继承:struct 嵌套复用公共字段
  8. 函数指针:把行为作为参数
  9. ops/vtable:行为表与对象绑定
  10. 多态与转型:base 指针与 dispatch
  11. 接口设计:ops 合同与默认实现
  12. 完整框架:抽象层与硬件解耦
  13. 终章:自动注册与 Linux 内核 OOP 结构

学完 struct + me + static + init/deinit 后,再看 STM32 HAL,会发现它并不是一堆随便命名的函数。它只是把同样的设计放到了更大的硬件库里。

HAL GPIO 常见元素有这些:

1
2
3
4
5
HAL_GPIO_Init();
HAL_GPIO_DeInit();
HAL_GPIO_WritePin();
HAL_GPIO_ReadPin();
HAL_GPIO_TogglePin();

以及这些类型和宏:

1
2
3
4
5
GPIO_TypeDef
GPIO_InitTypeDef
GPIOA
GPIOB
GPIO_PIN_5

一句话先懂:HAL 的 GPIO 模块,本质也是 struct 表示对象、指针指定对象、前缀组织接口、Init/DeInit 管生命周期。

先看映射关系

HAL 元素 本系列里的对应物
GPIO_TypeDef struct 封装一组寄存器
GPIOA/GPIOB 同一类型的不同实例
GPIO_TypeDef *GPIOx C 版 me 指针
HAL_GPIO_ 模块前缀 / 类名
HAL_GPIO_Init() 初始化 / 构造
.c 里的 helper static 私有实现

看懂这张表后,HAL 源码会从“一堆库函数”变成“一个个模块对象”。

GPIO_TypeDef:硬件寄存器对象

教学版可以这样理解:

1
2
3
4
5
6
7
8
9
10
typedef struct {
uint32_t MODER;
uint32_t OTYPER;
uint32_t OSPEEDR;
uint32_t PUPDR;
uint32_t IDR;
uint32_t ODR;
uint32_t BSRR;
uint32_t LCKR;
} GPIO_TypeDef;

它和 LED 对象的形式很像:

1
2
3
4
5
typedef struct {
uint8_t pin;
uint8_t brightness;
bool is_on;
} Led_t;

区别在于封装的数据不同:

对象 封装的数据
Led_t LED 的软件状态
GPIO_TypeDef GPIO 端口的一组寄存器

GPIOA/GPIOB:同一类型的多个实例

教学模型可以写成:

1
2
3
4
5
6
7
GPIO_TypeDef gpioa_regs;
GPIO_TypeDef gpiob_regs;
GPIO_TypeDef gpioc_regs;

#define GPIOA (&gpioa_regs)
#define GPIOB (&gpiob_regs)
#define GPIOC (&gpioc_regs)

真实芯片里,GPIOA 通常映射到固定外设地址。设计模型仍然一样:GPIOAGPIOBGPIOC 是同一类型的不同实例。

这和下面的 LED 多实例没有本质差别:

1
2
3
Led_t red_led;
Led_t green_led;
Led_t blue_led;

GPIOx 就是 me 指针

HAL 函数签名通常把对象指针放在第一个参数:

1
2
3
4
5
6
7
8
9
10
11
12
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t pin, bool value)
{
if (GPIOx == NULL) {
return;
}

if (value) {
GPIOx->ODR |= pin;
} else {
GPIOx->ODR &= ~pin;
}
}

这里的 GPIOx 决定本次操作哪个 GPIO 端口。它和 Led_t *me 是同一类东西:

1
2
3
4
5
6
7
8
9
10
11
int led_on(Led_t *me)
{
if (me == NULL) {
return -1;
}

me->is_on = true;
gpio_write(me->pin, true);

return 0;
}
教学 LED HAL GPIO
Led_t *me GPIO_TypeDef *GPIOx
me->pin GPIOx->ODR
led_on(&red) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, true)
对象指针决定操作谁 端口指针决定操作哪个 GPIO

HAL_GPIO_Init 是工业版构造函数

GPIO 初始化需要配置结构体:

1
2
3
4
5
6
7
typedef struct {
uint32_t Pin;
uint32_t Mode;
uint32_t Pull;
uint32_t Speed;
uint32_t OType;
} GPIO_InitTypeDef;

初始化函数会根据配置写寄存器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *init)
{
if (GPIOx == NULL || init == NULL) {
return;
}

for (int pin = 0; pin < 16; pin++) {
if ((init->Pin & (1U << pin)) == 0) {
continue;
}

set_2bit_field(&GPIOx->MODER, pin, init->Mode);
set_1bit_field(&GPIOx->OTYPER, pin, init->OType);
set_2bit_field(&GPIOx->OSPEEDR, pin, init->Speed);
set_2bit_field(&GPIOx->PUPDR, pin, init->Pull);
}
}

它与 led_init() 是同一类函数:

初始化步骤 LED 模块 HAL GPIO
检查参数 me == NULL `GPIOx == NULL
配置硬件 gpio_init(pin) 写 MODER/OTYPER 等寄存器
设置默认状态 is_on = false 配置模式、速度、上下拉
指定对象 Led_t *me GPIO_TypeDef *GPIOx

.h 与 .c 的分工也一样

HAL 头文件暴露公共接口:

1
2
3
4
5
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *init);
void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t pin);
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t pin, bool value);
bool HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t pin);
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t pin);

源文件内部保留私有辅助函数:

1
2
3
static void set_2bit_field(uint32_t *reg, int pin, uint32_t value);
static void set_1bit_field(uint32_t *reg, int pin, uint32_t value);
static bool is_pin_valid(uint32_t pin);

公共接口与私有实现分离,这一点和我们自己写 LED 模块没有区别。

新人常见误区

误区 更准确的理解
HAL 是纯过程式库,和 OOP 没关系 它大量使用对象指针和模块前缀
GPIOA 是普通变量 它通常是映射到固定寄存器地址的对象指针
GPIOx 只是随便起的名字 它就是这次操作的 GPIO 对象
只会调 HAL 就够了 看懂结构后,才能更稳地封装自己的驱动

C/C++ 对照

C / HAL 写法 OOP 理解
GPIO_TypeDef GPIO 类的数据结构
GPIOA GPIO A 实例
GPIO_TypeDef *GPIOx this 指针
HAL_GPIO_ 前缀 类名 / 命名空间
HAL_GPIO_Init() 构造 / 初始化
HAL_GPIO_DeInit() 析构 / 释放
static helper private 方法

嵌入式项目意义

看工业库源码时,可以按这些问题拆:

看源码时的问题 应该找什么
这个模块的数据在哪里? 找核心 struct
这次操作哪个对象? 看第一个指针参数
公共接口有哪些? 看头文件函数声明
内部细节在哪里? .cstatic 函数
初始化做了什么? Init 函数
释放做了什么? DeInit 函数

这就是从“会调用库”到“看懂库设计”的分界线。

总结

HAL 库几千个函数看起来复杂,本质仍然是 struct 封装数据、指针指定对象、前缀组织接口、Init/DeInit 管生命周期。