STM32HAL 快速入门(三):从 HAL 函数到寄存器操作 —— 理解 HAL 库的本质

前言

大家好,这里是 Hello_Embed。上一篇我们用 HAL 库函数实现了 LED 闪烁,而函数的底层其实是对寄存器的操作。本篇将带你跳出 HAL 库的 "封装",亲手通过寄存器控制 LED,理解 HAL 库的本质 ------ 它其实是寄存器操作的 "简化版"。同时,我们会对比不同寄存器的操作方式,体会 HAL 库带来的便利。

一、HAL 函数的底层:本质是操作寄存器

HAL 库的函数(如HAL_GPIO_WritePin)看似复杂,但其核心功能是通过代码操作单片机的寄存器。我们可以通过 "跳转定义"(选中函数按 F12)查看源码,但不必深究细节 ------ 现在,我们试着自己实现类似功能,直接操作寄存器控制 LED。

二、直接操作 ODR 寄存器:控制引脚电平

要通过寄存器控制 GPIO,需先明确目标寄存器的地址,再通过指针操作该地址的内存(即寄存器)。

步骤 1:确定 GPIOC 的 ODR 寄存器地址

寄存器的地址由 "基地址" 和 "偏移地址" 组成:

  • 基地址 :GPIOC 模块的起始地址,查手册可知为0x40011000
  • 偏移地址 :ODR 寄存器(输出数据寄存器)相对于基地址的偏移量,查手册得0x0C
  • 完整地址 :基地址 + 偏移地址 = 0x40011000 + 0x0C = 0x4001100C
步骤 2:用指针操作 ODR 寄存器实现 LED 闪烁

ODR 寄存器的第 13 位对应 PC13 引脚(1 为高电平,0 为低电平)。通过逻辑运算修改该位(不影响其他位),代码如下:

c 复制代码
{
  /* USER CODE BEGIN 1 */
  unsigned int *p;  // 定义指针,指向ODR寄存器
  p = (unsigned int *)0x4001100C;  // 赋值ODR地址(强制类型转换为指针)
  /* USER CODE END 1 */

  while (1)
  {
    /* 熄灭LED:将ODR第13位置1(高电平) */
    unsigned val = *p;  // 读取当前ODR值
    val = val | (1 << 13);  // 第13位置1(其他位不变)
    *p = val;  // 写回ODR寄存器
    HAL_Delay(500);  // 延时500ms

    /* 点亮LED:将ODR第13位清0(低电平) */
    val = *p;  // 读取当前ODR值
    val = val & ~(1 << 13);  // 第13位清0(其他位不变)
    *p = val;  // 写回ODR寄存器
    HAL_Delay(500);
    /* USER CODE BEGIN 3 */
  }
}

代码解析:

  • unsigned int *p定义指针,指向0x4001100C(ODR 寄存器);
  • 通过| (1 << 13)& ~(1 << 13)操作,只修改第 13 位,保护其他引脚的电平状态(这是操作 ODR 的关键,需要用到前面笔记中讲的逻辑运算知识);
  • 最终实现的效果与HAL_GPIO_WritePin完全一致,LED 间隔 500ms 闪烁。
三、更简洁的方式:操作 BSRR 寄存器

ODR 寄存器需要通过逻辑运算保护其他位,操作较繁琐。而 BSRR 寄存器(位设置 / 清除寄存器)可以直接操作目标位,无需考虑其他位,更方便。

BSRR 寄存器的特性

查手册可知:

  • BSRR 寄存器共 32 位,低 16 位(0~15)用于 "置位"(写 1 时对应引脚输出高电平,写 0 无影响);
  • 高 16 位(16~31)用于 "清零"(写 1 时对应引脚输出低电平,写 0 无影响)。
步骤 1:确定 GPIOC 的 BSRR 寄存器地址
  • 基地址仍为0x40011000
  • BSRR 的偏移地址为0x10
  • 完整地址:0x40011000 + 0x10 = 0x40011010
步骤 2:用 BSRR 寄存器实现 LED 闪烁

直接向 BSRR 的对应位写 1 即可,无需逻辑运算,代码更简洁:

c 复制代码
int main(void)
{
  /* USER CODE BEGIN 1 */
  unsigned int *p;  // 定义指针,指向BSRR寄存器
  p = (unsigned int *)0x40011010;  // 赋值BSRR地址
  /* USER CODE END 1 */

  while (1)
  {
    /* 熄灭LED:低13位写1(置位PC13) */
    *p = (1 << 13);  // 等价于设置PC13为高电平
    HAL_Delay(500);

    /* 点亮LED:高13位(即第29位)写1(清零PC13) */
    *p = (1 << 29);  // 等价于设置PC13为低电平(13 + 16 = 29)
    HAL_Delay(500);
    /* USER CODE BEGIN 3 */
  }
}

代码解析:

  • 无需读取寄存器当前值,直接写(1 << 13)即可置位 PC13(高电平),写(1 << 29)即可清零 PC13(低电平);
  • 其他位写 0 无影响,省去了 ODR 操作中的逻辑运算,更高效。
四、HAL 库的本质:封装寄存器操作

亲手操作寄存器后会发现:直接编写底层代码需要记忆地址、计算位运算,非常繁琐。而 HAL 库的核心作用,就是将这些复杂的寄存器操作封装成简单的函数(如HAL_GPIO_WritePin),通过填写参数(端口、引脚、电平)即可完成操作,大大降低了开发难度。

补充说明

韦东山老师视频合集的 5-5~5-8 节补充了相关 C 语言基础知识,这些内容在我前面的笔记中都有记录,大家可以结合笔记辅助学习,加深对指针、位运算等概念的理解。

结尾

本文通过直接操作 ODR 和 BSRR 寄存器,实现了与 HAL 库函数相同的功能,让我们看清了 HAL 库的本质 ------ 它是寄存器操作的 "封装层"。理解这一点后,使用 HAL 库时会更清楚其底层逻辑。

下一篇笔记,我们将彻底分析 GPIO 的工作模式(如推挽输出、开漏输出等),看看不同模式下寄存器的配置有何差异。Hello_Embed 继续带你深入 STM32 的硬件细节,敬请期待~

相关推荐
charlie11451419111 小时前
嵌入式现代C++教程: 构造函数优化:初始化列表 vs 成员赋值
开发语言·c++·笔记·学习·嵌入式·现代c++
IT=>小脑虎12 小时前
C++零基础衔接进阶知识点【详解版】
开发语言·c++·学习
Felven12 小时前
A. Helmets in Night Light
c语言
#眼镜&12 小时前
嵌入式学习之路2
学习
码农小韩12 小时前
基于Linux的C++学习——指针
linux·开发语言·c++·学习·算法
微露清风12 小时前
系统性学习C++-第十九讲-unordered_map 和 unordered_set 的使用
开发语言·c++·学习
早日退休!!!12 小时前
ARM Cortex-M核 【保存上下文&恢复上下文】
arm开发·单片机·嵌入式硬件
wdfk_prog12 小时前
[Linux]学习笔记系列 -- [fs]seq_file
linux·笔记·学习
来自晴朗的明天13 小时前
差分控多少Ω阻抗
单片机·嵌入式硬件·硬件工程
行业探路者13 小时前
二维码标签是什么?主要有线上生成二维码和文件生成二维码功能吗?
学习·音视频·语音识别·二维码·设备巡检