【STM32】HAL库中的实现(一)GPIO/SysTick/EXTI

STM32标准外设库(StdPeriph)vs HAL库

自2017 年前后,是STM32F1/F4 系列 StdPeriph 最后一次更新。2018 年起,ST 官网不再推荐使用 StdPeriph,并推荐使用 STM32Cube HAL 和 LL 库。目前,StdPeriph 被视为"遗留库",仅用于维护旧项目,官方不再维护。虽然,我们之前写的很多文章是基于标准库的控制硬件的实现方式,但那也只是因为通过标准库的一行一行的底层代码实现来帮助大家理解代码的底层原理。然而,由于STM32 的开发库经历了从 标准外设库(Standard Peripheral Library) 到 HAL 库(Hardware Abstraction Layer) 的演进 ,我们在实际开发中用的都是基于 HAL库 或者 LL库 进行开发的。

总而言之,HAL 是 ST 在 StdPeriph 基础上,为了适配更多芯片系列、支持自动代码生成而发展出的新一代库。 不过,这二者的本质上都是为了简化对底层寄存器的操作,提供更高层次的驱动接口。 所以,其实我们在开发中也经常会直接操作寄存器的方式进行开发,这是因为,HAL 库底层仍然访问 MCU 的寄存器,某些驱动底层实现与 StdPeriph 类似。

二者的区别
对比项 StdPeriph(标准外设库) HAL(STM32Cube HAL)
官方支持 已停止更新(仅维护旧芯片) 官方主推,支持所有新芯片
抽象层次 贴近寄存器,面向硬件 更高层抽象,面向应用
接口复杂度 接口简洁、灵活 接口冗长,但易于上手
调试难度 更透明、利于调试 封装较深,不易定位问题
移植性 移植性差,针对具体芯片 移植性好,跨芯片一致性强
学习曲线 需要对寄存器有较深理解 上手容易,适合初学者
自动生成支持 不支持自动生成 配合 STM32CubeMX 支持自动生成代码
代码体积 小,适合资源受限设备 较大,适合中高端MCU

在实际的新项目 / 商用产品开发之中,当然推荐使用 HAL库(官方长期支持)(优化了代码、开发效率高),不过,从教学 / 学术研究 / 控制精度要求高的某些特定场景下 ,还是推荐使用 StdPeriph 或裸机 。如果是 资源极限 / 启动程序 / Bootloader / 安全代码 的情况,建议使用 裸机编程(直接操作寄存器的方式),更轻量可控 且适合满足精细化控制的要求。

例如说,Bootloader 开发中需要极小代码体积,快速启动;芯片启动初始化时候,系统刚上电,HAL 还未运行;某些资源受限的 MCU 单片机,无法承受 HAL 库的代码尺寸和 RAM 占用;或 需要高实时性控制 ,比如 PWM、DMA 精确控制,HAL 可能不够灵活;如果代码出现问题,需要差错和调试代码,调试底层BUG时,常常需要直接操作寄存器来定位问题;还有,驱动某些自定义外设时,HAL 可能不支持全部外设或第三方模块。综上所述,HAL库依旧存在一些方面的缺失,我们在开发中依然要学会使用裸机开发和使用标准库(虽然可能不常用)。要理解学习嵌入式的编程原理,推荐还是走 裸机(寄存器)+ StdPeriph标准库的路线,辅助我们在这个过程中理解嵌入式开发的本质原理。

不过,HAL的出现很好的解决了标准库和裸机开发效率低,维护成本高的问题,而且HAL库有利于团队协作开发,HAL 的接口统一、文档丰富、合适团队协作编程;而且,HAL 更好地封装了多种模式和外设资源,能够简单的实现MCU复杂的功能。

接下来通过 使用 HAL库实现来实现 GPIO、SysTick、EXTI 三大基础模块, 辅助你理解 HAL 库背后的设计逻辑及其与底层硬件的关系。不过,

虽然 HAL 库通过统一封装 GPIO、SysTick、EXTI 等模块,大大简化了 STM32 的开发流程,但也增加了代码体积和抽象层级。 理解 HAL 的实现原理,有利于更高效地调试、优化和移植项目。


HAL库中的实现 GPIO

在CubeMX中使能GPIO:

生成后,代码要写在规定的范围内:

main.c 文件中 写上按键后,亮灯和状态翻转的代码实现:

c 复制代码
while (1)
 {
	if( HAL_GPIO_ReadPin(Key_UP_GPIO_Port, Key_UP_Pin) == GPIO_PIN_RESET)  //gpio输入
	{
		HAL_GPIO_TogglePin(LED_G_GPIO_Port, LED_G_Pin);		//状态翻转
	}
}
HAL 库使用方式

配置:

c 复制代码
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_SET);
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_6);
GPIO_PinState state = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13);
HAL 内部实现原理

HAL_GPIO_WritePin() 为例:

c 复制代码
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
    if(PinState != GPIO_PIN_RESET)
    {
        GPIOx->BSRR = GPIO_Pin;       // 设置:BSRR 低 16 位
    }
    else
    {
        GPIOx->BRR = GPIO_Pin;        // 复位:BRR 低 16 位
    }
}
  • GPIOx->BSRR:位操作原子寄存器(置位)
  • GPIOx->BRR:复位寄存器(部分芯片上也通过 BSRR 高 16 位)

因此我们通过对GPIO的配置可以非常直观的感受到,虽然HAL库操作简单,封装良好,支持所有 GPIO 端口,但是封装层级深,调试不直观,速度也不如裸机快。

其余的代码在CubeMX的图形化中配置好后系统会帮忙自动生成。不需要我们手动配置,这便是CubeMX的强大之处,和为什么能够取代并淘汰标准库的缘由。


HAL库中的实现 SysTick

配置:

(中断法)源代码:

c 复制代码
/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "string.h"

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
	uint32_t LED_ts = 0;
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
		//中断延时法
		if((HAL_GetTick() - LED_ts) > 100)
		{
			LED_ts = HAL_GetTick();
			HAL_GPIO_TogglePin(LED_B_GPIO_Port, LED_B_Pin);  //LED灯高低电平的翻转
		}
		
		
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

其实没必要贴上来这么多的代码,我们只需要知道实现SYSTICK时钟的代码思路即可。后续的代码中我即将不再贴源代码。

中断法:
c 复制代码
 /* USER CODE BEGIN 1 */
uint32_t LED_ts = 0;
 /* USER CODE END 1 */
c 复制代码
 while (1)
 {
	//中断延时法
	if((HAL_GetTick() - LED_ts) > 100)
	{
		LED_ts = HAL_GetTick();
		HAL_GPIO_TogglePin(LED_B_GPIO_Port, LED_B_Pin);  //LED灯高低电平的翻转
	}
	
	
   /* USER CODE END WHILE */

   /* USER CODE BEGIN 3 */
	
 }
 /* USER CODE END 3 */
}
HAL 延时法:
c 复制代码
HAL_Delay(1000);  // 延时 1000ms

背后实现:

HAL 使用 SysTick(系统定时器)来实现 HAL_Delay() 功能。SysTick 是 Cortex-M 内核自带的 24 位递减计数器,通常配置为 1ms 一次中断。

c 复制代码
void SysTick_Handler(void)
{
    HAL_IncTick();               // 每毫秒调用一次
    osSystickHandler();          // 如果使用了RTOS
}

全局变量:

c 复制代码
volatile uint32_t uwTick;       // HAL 延时核心变量

延时函数实现:

c 复制代码
void HAL_Delay(uint32_t Delay)
{
    uint32_t tickstart = HAL_GetTick();
    while((HAL_GetTick() - tickstart) < Delay)
    {
        // 等待
    }
}

HAL库中的实现 EXTI 模块(外部中断)

CubeMX配置:

根据硬件原理图:

配置好后,生成代码即可:
stm32f1xx_it.c

c 复制代码
/* USER CODE BEGIN 1 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == PA0_Key_Pin)
	{
		if( HAL_GPIO_ReadPin(PA0_Key_GPIO_Port, PA0_Key_Pin) == GPIO_PIN_RESET)
		{
			HAL_GPIO_TogglePin(LED_R_GPIO_Port, LED_R_Pin);
		}
	}
}
/* USER CODE END 1 */

EXTI详细的配置流程思路:

1. HAL 中断配置方式(GPIO + EXTI)
c 复制代码
HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);  // 用户中断回调

HAL 中断处理机制:

handlebars 复制代码
EXTI IRQ Handler
     ↓
HAL_GPIO_EXTI_IRQHandler()
     ↓
清除中断标志位
     ↓
调用 HAL_GPIO_EXTI_Callback()
2. 外部中断的 HAL 配置流程

2.1. GPIO 配置为中断输入模式:

c 复制代码
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 上升/下降/双沿
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

2.2. 中断优先级配置:

c 复制代码
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);

2.3. 中断服务函数:

c 复制代码
void EXTI15_10_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);  // 统一入口
}

2.4. 用户回调处理函数:

c 复制代码
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13)
    {
        // 处理按键中断或其他逻辑
    }
}

综上。HAL 库通过统一封装 GPIO、SysTick、EXTI 等模块,大大简化了 STM32 的开发流程,但也增加了代码体积和抽象层级。理解 HAL 的实现原理,有利于更加高效地调试、优化和移植项目。

以上,欢迎有从事同行业的电子信息工程、互联网通信、嵌入式开发的朋友共同探讨与提问,我可以提供实战演示或模板库。希望内容能够对你产生帮助!