06 HAL映射:工业库也是同一套封装套路

C 语言 OOP 系列导航

  1. C语言你真的会封装吗?
  2. 三个LED写了三份代码?
  3. 同事改一行,LED全乱了
  4. 手搓class:前缀、init/deinit和生命周期
  5. 你的全局变量,该死了
  6. 工业库也是同一套封装套路 ⇐ 当前位置
  7. struct嵌套消灭复制粘贴
  8. 写死的函数怎么换
  9. 把一组函数指针装进对象
  10. 一个指针管所有LED
  11. 虚函数不实现会怎样
  12. 换硬件不改应用
  13. 从自动注册到Linux内核OOP全貌

前面讲的 struct + me + static + init/deinit,你在 STM32 HAL 这类工业库里看到的 GPIO、UART、SPI、TIM,用的也是同一套设计——只是它们操作的不是一颗 LED 的软件状态,而是一组硬件寄存器。

先看问题:HAL 函数一堆,看完就忘

很多人第一次看 HAL,会被一堆函数名劝退:

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_TypeDef struct 封装一组寄存器
GPIOA/GPIOB 同一类型的不同实例
GPIO_TypeDef *GPIOx C 版 me 指针
HAL_GPIO_ 模块前缀 / 类名
HAL_GPIO_Init() 初始化 / 构造
.c 里的 helper static 私有实现

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:同一类型的多个实例

可以把 GPIO 端口理解成多个实例:

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 通常是映射到固定硬件地址的指针。但理解上,它和我们写的:

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 代码里的 me 完全同构:

教学 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() 的工业版——检查参数、配置硬件、填充默认状态,步骤完全一样。

.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);

这和我们前面讲的 .h = 菜单,.c = 后厨 一样——公共接口曝光,内部细节用 static 隐藏。

换个视角看工业库

学会这套映射后,再看 HAL 就不会只看到一堆函数名:

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

下次再打开 HAL 的代码,试着用这个视角读:UART_HandleTypeDef huart1 就是在创建对象实例,HAL_UART_Init(&huart1) 就是构造函数,HAL_UART_Transmit(&huart1, buf, 10, 100) 就是给 huart1 发一条 Transmit 消息。不再是”调了一个库函数”,而是”向某个对象发了一条消息”。

HAL 库几千个函数看起来复杂,骨架从来不复杂:struct 封装数据、指针指定对象、前缀组织接口、Init/DeInit 管生命周期。和我们从头手搓的 LED 驱动,同一套设计。