STM32 HAL库函数原理解析

发布于:2025-03-23 ⋅ 阅读:(45) ⋅ 点赞:(0)

从 51 单片机到 STM32:以串口外设为例剖析 HAL 库的层层封装

在 51 单片机的学习过程中,我们就已熟知寄存器是控制单片机外设功能的核心。当过渡到 STM32 单片机时,会发现每个外设内部都存在大量功能各异且相互独立的寄存器。正是这些不同类型寄存器的协同工作,让单片机能够依据开发者的需求实现 “自定义” 功能。

单片机厂商为了高效管理,将模块的处理单元封装成不同类型的寄存器。比如,有专门负责数据管理的数据寄存器、监测外设状态的状态寄存器以及控制功能的功能寄存器。深入探究单个寄存器,尽管 STM32 是 32 位单片机,寄存器为 32 位,但多数寄存器会预留 16 位作为保留位,实际操作主要通过 16 个位来完成。所以,本质上开发者是在和寄存器中的比特位进行交互。

ST 公司在寄存器和比特位的封装上展现出了精妙的设计。它运用宏定义的方式,将比特位与实际地址一一对应。以 USART(通用同步异步收发传输器)状态寄存器(SR)为例,以下是相关的宏定义:

#define  USART_SR_PE                         ((uint16_t)0x0001)            /*!< 奇偶校验错误 */
#define  USART_SR_FE                         ((uint16_t)0x0002)            /*!< 帧错误 */
#define  USART_SR_NE                         ((uint16_t)0x0004)            /*!< 噪声错误标志 */
#define  USART_SR_ORE                        ((uint16_t)0x0008)            /*!< 溢出错误 */
#define  USART_SR_IDLE                       ((uint16_t)0x0010)            /*!< 检测到空闲线路 */
#define  USART_SR_RXNE                       ((uint16_t)0x0020)            /*!< 接收数据寄存器非空 */
#define  USART_SR_TC                         ((uint16_t)0x0040)            /*!< 传输完成 */
#define  USART_SR_TXE                        ((uint16_t)0x0080)            /*!< 发送数据寄存器为空 */
#define  USART_SR_LBD                        ((uint16_t)0x0100)            /*!< LIN 中断检测标志 */
#define  USART_SR_CTS                        ((uint16_t)0x0200)            /*!< CTS 标志 */

为了准确定位具体的外设,采用了外设基地址加上偏移量的方法。这种方式使得开发者能够方便地找到目标外设对应的寄存器地址。

接着,ST 公司使用结构体对每个外设的所有寄存器进行封装。以 USART 为例,其结构体定义如下:   

typedef struct
{
  __IO uint16_t SR;
  uint16_t  RESERVED0;
  __IO uint16_t DR;
  uint16_t  RESERVED1;
  __IO uint16_t BRR;
  uint16_t  RESERVED2;
  __IO uint16_t CR1;
  uint16_t  RESERVED3;
  __IO uint16_t CR2;
  uint16_t  RESERVED4;
  __IO uint16_t CR3;
  uint16_t  RESERVED5;
  __IO uint16_t GTPR;
  uint16_t  RESERVED6;
} USART_TypeDef;

其中,__IO 表示该成员是可读写的,RESERVED 代表保留位。通过这种结构体封装,开发者可以更便捷地访问和操作外设的各个寄存器。

此外,对于 USART 的工作模式,也进行了宏定义:

/** @defgroup USART_Mode 
  * @{
  */ 
  
#define USART_Mode_Rx                        ((uint16_t)0x0004)
#define USART_Mode_Tx                        ((uint16_t)0x0008)
#define IS_USART_MODE(MODE) ((((MODE) & (uint16_t)0xFFF3) == 0x00) && ((MODE) != (uint16_t)0x00))
/**
  * @}

同时,为了方便初始化 USART 外设,定义了一个初始化结构体:

typedef struct
{
  uint32_t USART_BaudRate;            /*!< 该成员用于配置 USART 通信波特率。
                                           波特率通过以下公式计算:
                                            - 整数部分 = ((PCLKx) / (16 * (USART_InitStruct->USART_BaudRate)))
                                            - 小数部分 = ((整数部分 - ((u32) 整数部分)) * 16) + 0.5 */

  uint16_t USART_WordLength;          /*!< 指定一帧中传输或接收的数据位数。
                                           该参数可以是 @ref USART_Word_Length 中的一个值 */

  uint16_t USART_StopBits;            /*!< 指定传输的停止位数量。
                                           该参数可以是 @ref USART_Stop_Bits 中的一个值 */

  uint16_t USART_Parity;              /*!< 指定奇偶校验模式。
                                           该参数可以是 @ref USART_Parity 中的一个值
                                           @note 当启用奇偶校验时,计算得到的奇偶校验位会插入到
                                                 传输数据的最高有效位(当字长设置为 9 位数据时为第 9 位;
                                                 当字长设置为 8 位数据时为第 8 位)。 */
 
  uint16_t USART_Mode;                /*!< 指定接收或发送模式是启用还是禁用。
                                           该参数可以是 @ref USART_Mode 中的一个值 */

  uint16_t USART_HardwareFlowControl; /*!< 指定硬件流控制模式是启用还是禁用。
                                           该参数可以是 @ref USART_Hardware_Flow_Control 中的一个值 */
} USART_InitTypeDef;

最后,ST 公司将这些底层的寄存器操作进一步封装成一个又一个经典的库函数,形成了 HAL(硬件抽象层)库。这个库为开发者提供了一个简单、高效的开发接口,让开发者无需深入了解底层寄存器的操作细节,就能轻松实现各种外设的功能。可以说,HAL 库是无数开发者开启单片机开发之旅的起点,大大提高了开发效率和代码的可维护性。以串口外设为例,这种层层封装的方式使得开发者能够更加专注于应用程序的开发,而不必花费大量时间去研究底层硬件的复杂细节。

USART_Mode :  
#define USART_Mode_Rx                        ((uint16_t)0x0004)   为例子

2. USART_Mode_Rx 宏定义的意义

USART_Mode_Rx 这个宏定义代表着 USART 的接收模式。((uint16_t)0x0004) 实际上是一个掩码,它对应着 CR1 寄存器里特定的某一位或者某几位。当把这个掩码应用到 CR1 寄存器时,就可以开启或者关闭接收功能。

3. 具体的对应关系

一般而言,USART_Mode_Rx 对应的是 CR1 寄存器里的 RE(接收使能)位。假定 RE 位在 CR1 寄存器的第 2 位(从 0 开始计数),那么 0x0004 刚好是 0b0000 0000 0000 0100,这就意味着只有第 2 位被置为 1,其他位都是 0。