HAL 库架构
STM32 HAL(Hardware Abstraction Layer,硬件抽象层)库由 ST 官方提供,分为三层:HAL 层、LL(Low-Layer)层、CMSIS(Cortex Microcontroller Software Interface Standard)层。
HAL 层
HAL 层提供高度抽象的 API,屏蔽硬件细节,适合快速开发。HAL 函数通常包含参数检查、状态机管理、中断回调等逻辑。
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
HAL_UART_Transmit(&huart1, data, len, timeout);
HAL_TIM_Base_Start(&htim2);
LL 层
LL 层提供轻量级 API,直接操作寄存器,代码更紧凑,性能更高。
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5);
LL_USART_TransmitData8(USART1, data);
LL_TIM_EnableCounter(TIM2);
CMSIS 层
CMSIS 提供内核访问函数和寄存器定义,是所有库的基础。
GPIOA->BSRR = GPIO_BSRR_BS5;
USART1->DR = data;
TIM2->CR1 |= TIM_CR1_CEN;
GPIO 操作对比
HAL 库写法
// 初始化
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 设置输出
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
// 读取输入
GPIO_PinState state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
寄存器写法
// 初始化
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
GPIOA->CRL &= ~(GPIO_CRL_MODE5 | GPIO_CRL_CNF5);
GPIOA->CRL |= (0x02 << GPIO_CRL_MODE5_Pos); // Output mode, max speed 2 MHz
// 设置输出
GPIOA->BSRR = GPIO_BSRR_BS5; // Set
GPIOA->BSRR = GPIO_BSRR_BR5; // Reset
// 读取输入
uint8_t state = (GPIOA->IDR & GPIO_IDR_ID0) ? 1 : 0;
LL 库写法
// 初始化
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_GPIOA);
LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_5, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_5, LL_GPIO_SPEED_FREQ_LOW);
// 设置输出
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5);
LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_5);
// 读取输入
uint32_t state = LL_GPIO_IsInputPinSet(GPIOA, LL_GPIO_PIN_0);
定时器操作对比
HAL 库写法
// 初始化
TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = 7199;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 9999;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
// 启动
HAL_TIM_Base_Start(&htim2);
// 设置比较值
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 5000);
寄存器写法
// 初始化
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
TIM2->PSC = 7199;
TIM2->ARR = 9999;
TIM2->CR1 |= TIM_CR1_CEN; // 启动
// 设置比较值
TIM2->CCR1 = 5000;
LL 库写法
// 初始化
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);
LL_TIM_SetPrescaler(TIM2, 7199);
LL_TIM_SetAutoReload(TIM2, 9999);
LL_TIM_EnableCounter(TIM2);
// 设置比较值
LL_TIM_OC_SetCompareCH1(TIM2, 5000);
UART 操作对比
HAL 库写法
// 发送数据
uint8_t data[] = "Hello";
HAL_UART_Transmit(&huart1, data, 5, 1000);
// 接收数据(阻塞)
uint8_t rx_buf[10];
HAL_UART_Receive(&huart1, rx_buf, 10, 1000);
// 接收数据(中断)
HAL_UART_Receive_IT(&huart1, rx_buf, 10);
寄存器写法
// 发送数据
uint8_t data[] = "Hello";
for (int i = 0; i < 5; i++) {
while (!(USART1->SR & USART_SR_TXE));
USART1->DR = data[i];
}
while (!(USART1->SR & USART_SR_TC));
// 接收数据(阻塞)
uint8_t rx_buf[10];
for (int i = 0; i < 10; i++) {
while (!(USART1->SR & USART_SR_RXNE));
rx_buf[i] = USART1->DR;
}
LL 库写法
// 发送数据
uint8_t data[] = "Hello";
LL_USART_TransmitData8(USART1, data[i]);
// 接收数据
uint8_t rx_data = LL_USART_ReceiveData8(USART1);
代码体积对比
使用 Keil MDK 编译优化等级 -O2,对比不同方式的代码体积。
测试代码
HAL 版本:
void GPIO_Toggle_HAL(void) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
寄存器版本:
void GPIO_Toggle_Register(void) {
GPIOA->ODR ^= GPIO_ODR_OD5;
}
LL 版本:
void GPIO_Toggle_LL(void) {
LL_GPIO_TogglePin(GPIOA, LL_GPIO_PIN_5);
}
编译结果
| 方式 | 代码大小(字节) |
|---|---|
| HAL | 32 |
| LL | 8 |
| 寄存器 | 4 |
HAL 库因包含参数检查和状态管理,代码体积约为寄存器的 8 倍。LL 库约为寄存器的 2 倍。
完整工程对比(包含初始化代码):
- HAL 库工程:约 15KB
- LL 库工程:约 8KB
- 寄存器工程:约 4KB
执行周期对比
使用逻辑分析仪测量 GPIO 翻转时间,对比不同方式的执行效率。
测试方法
while (1) {
GPIOA->BSRR = GPIO_BSRR_BS5; // 测量上升沿
// 被测代码
GPIOA->BSRR = GPIO_BSRR_BR5; // 测量下降沿
}
测试结果(72MHz 系统时钟)
| 操作 | HAL 周期 | LL 周期 | 寄存器周期 | HAL 时间(ns) | 寄存器时间(ns) |
|---|---|---|---|---|---|
| GPIO 翻转 | 12 | 4 | 2 | 167 | 28 |
| 定时器启动 | 45 | 8 | 5 | 625 | 69 |
| UART 发送字节 | 120 | 15 | 10 | 1667 | 139 |
HAL 库在高频操作(如中断服务、PWM 生成)中性能劣势明显,寄存器操作快 5-10 倍。
LL 库折中方案
LL 库在代码体积和性能之间提供平衡,适合需要优化但不想完全使用寄存器的场景。
LL 库优势
- 代码体积比 HAL 小 40-50%
- 执行速度比 HAL 快 2-3 倍
- 保持一定的可读性和可移植性
- 仍支持 HAL 的部分特性(如回调函数)
LL 库使用示例
#include "stm32f1xx_ll_gpio.h"
#include "stm32f1xx_ll_tim.h"
void TIM_PWM_LL_Init(void) {
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);
LL_TIM_SetPrescaler(TIM2, 7199);
LL_TIM_SetAutoReload(TIM2, 9999);
LL_TIM_OC_SetMode(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_OCMODE_PWM1);
LL_TIM_OC_SetCompareCH1(TIM2, 5000);
LL_TIM_EnableCounter(TIM2);
LL_TIM_CC_EnableChannel(TIM2, LL_TIM_CHANNEL_CH1);
}
选型建议
何时使用 HAL 库
快速开发场景:
- 原型验证、概念验证
- 项目周期紧张
- 团队成员对底层不熟悉
可移植性要求高:
- 需要在不同 STM32 系列间移植
- 代码复用于多个项目
低频操作:
- 配置初始化
- 低速通讯(如 I2C、SPI 低速模式)
- 非实时性任务
示例代码:
// 应用层任务,对性能要求不高
void App_Task(void) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(500);
uint8_t data[10];
HAL_UART_Receive(&huart1, data, 10, 1000);
}
何时使用寄存器操作
高频操作:
- 中断服务函数(ISR)
- 高速 PWM 生成
- 实时信号处理
性能关键路径:
- 采样率高的 ADC 处理
- 高速串口接收(>1Mbps)
- 定时器中断中的快速操作
内存受限场景:
- Flash 空间不足
- RAM 紧张
示例代码:
// 高频中断服务
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF; // 清除标志
GPIOA->ODR ^= GPIO_ODR_OD5; // 快速翻转
uint16_t adc_value = ADC1->DR; // 直接读取
Process_ADC_Data(adc_value);
}
}
何时使用 LL 库
需要优化但不想牺牲可读性:
- 中等频率操作(1kHz-100kHz)
- 需要部分 HAL 特性
逐步优化:
- 从 HAL 迁移到高性能代码
- 保持一定抽象层次
混合开发:
- 初始化用 HAL,运行时用 LL
- 非关键路径用 HAL,关键路径用 LL
混合使用最佳实践
HAL + 寄存器混合
初始化使用 HAL,运行时关键路径使用寄存器:
// 初始化用 HAL
void System_Init(void) {
HAL_Init();
MX_GPIO_Init();
MX_TIM2_Init();
HAL_TIM_Base_Start_IT(&htim2);
}
// 中断服务用寄存器
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF;
GPIOA->ODR ^= GPIO_ODR_OD5; // 寄存器操作
}
}
HAL + LL 混合
复杂外设用 HAL,简单外设用 LL:
// UART 用 HAL(配置复杂)
HAL_UART_Init(&huart1);
// GPIO 用 LL(简单操作)
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5);
条件编译切换
通过宏定义切换实现,方便调试和优化:
#define USE_REGISTER_OPTIMIZATION 1
void GPIO_Toggle_Fast(void) {
#if USE_REGISTER_OPTIMIZATION
GPIOA->ODR ^= GPIO_ODR_OD5;
#else
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
#endif
}
性能关键模块优化
识别性能瓶颈,针对性优化:
// 普通操作用 HAL
void Normal_Operation(void) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
HAL_Delay(10);
}
// 高速操作用寄存器
void High_Speed_Operation(void) {
GPIOA->BSRR = GPIO_BSRR_BS5;
// 无延迟,立即执行
}
实际项目案例
案例 1:电机控制项目
需求:20kHz PWM,ADC 采样率 50kHz,PID 控制周期 20kHz。
选型:
- 初始化:HAL
- PWM 输出:寄存器(定时器中断中)
- ADC 读取:寄存器(DMA 完成中断中)
- PID 计算:C 代码(数学运算)
- 通讯:HAL(Modbus RTU,低频)
// PWM 更新(寄存器)
void TIM1_UP_IRQHandler(void) {
TIM1->SR &= ~TIM_SR_UIF;
TIM1->CCR1 = pwm_duty; // 直接写入
}
// ADC 处理(寄存器)
void DMA1_Channel1_IRQHandler(void) {
DMA1->IFCR |= DMA_IFCR_CTCIF1;
uint16_t adc_val = ADC1->DR; // 直接读取
Process_ADC(adc_val);
}
案例 2:数据采集终端
需求:多路 ADC 采集,SD 卡存储,低功耗。
选型:
- ADC 初始化:HAL
- ADC 读取:LL(平衡性能)
- SD 卡:HAL(复杂协议)
- 低功耗模式:寄存器(直接配置 PWR)
// 进入低功耗模式(寄存器)
void Enter_Low_Power(void) {
SCB->SCR |= SCB_SCR_SLEEPONEXIT_Msk;
PWR->CR |= PWR_CR_LPDS; // 深度睡眠
__WFI();
}
性能优化技巧
内联函数
对频繁调用的小函数使用内联:
__attribute__((always_inline))
static inline void GPIO_Fast_Set(GPIO_TypeDef *GPIOx, uint16_t Pin) {
GPIOx->BSRR = Pin;
}
位带操作
使用位带操作实现原子读写:
#define BITBAND(addr, bitnum) ((0x42000000 + ((uint32_t)(addr) - 0x40000000)*32 + (bitnum)*4))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND((uint32_t)&(addr), bitnum))
#define PAout(n) BIT_ADDR(GPIOA->ODR, n)
PAout(5) = 1; // 等价于 GPIOA->ODR |= GPIO_ODR_OD5
DMA 优化
批量数据搬运使用 DMA,避免 CPU 参与:
// 寄存器配置 DMA
DMA1_Channel4->CPAR = (uint32_t)&USART1->DR;
DMA1_Channel4->CMAR = (uint32_t)tx_buffer;
DMA1_Channel4->CNDTR = len;
DMA1_Channel4->CCR |= DMA_CCR_EN;
总结
| 方式 | 代码体积 | 执行速度 | 可读性 | 可移植性 | 适用场景 |
|---|---|---|---|---|---|
| HAL | 大 | 慢 | 高 | 高 | 快速开发、低频操作 |
| LL | 中 | 中 | 中 | 中 | 性能平衡、逐步优化 |
| 寄存器 | 小 | 快 | 低 | 低 | 高频操作、性能关键 |
选型原则:
- 新项目先用 HAL 快速验证
- 识别性能瓶颈,针对性优化
- 初始化用 HAL,运行时关键路径用寄存器/LL
- 通过条件编译保留调试灵活性
混合使用是最佳实践,在开发效率和运行性能间取得平衡。