In embedded project development, STM32CubeMX is widely used for its visual configuration and one-click generation of initialization code. However, as project complexity increases, we gradually find that the structure of the code generated by CubeMX, while convenient for beginners, is not conducive to code reuse and cross-project migration. To address this issue, extracting and encapsulating reusable driver layers has become a key step in engineering architecture design.
Based on the STM32 HAL/LL interface, this article explains how to separate and encapsulate the hardware-related driver parts from the code generated by CubeMX, and demonstrates the design ideas and encapsulation methods of a general driver module with practical examples.
1. Why Encapsulate the Driver Layer?
The code generated by STM32CubeMX is essentially “project-specific,” meaning that the generated initialization code is directly bound to specific hardware configurations (such as GPIO numbers, clock sources, UART instances, etc.), resulting in a relatively flat code structure. If migrating to another project, even if the hardware is similar, a significant amount of rewriting is often required.
Problem Manifestations:
- GPIO operations are hard-coded in main.c or user functions
- Peripheral initialization is severely coupled with application logic
- HAL functions are directly scattered throughout the application logic, making modular reuse impossible
- The project structure lacks hierarchy
By encapsulating HAL/LL operations as abstract interfaces, we can build driver modules that are “platform-independent + hardware-related and separable.”
2. Basic Strategies for Encapsulation
The goal of extracting the driver layer from the CubeMX project is:
- To abstract the interface between the application layer and hardware;
- To hide specific hardware information in the underlying implementation;
- To access all peripherals through intermediate interface calls, avoiding direct use of HAL functions.
When using the HAL library, we can uniformly use HAL functions in the driver layer while masking specific device numbers. When using the LL library, the code efficiency is higher, suitable for performance-critical scenarios, but the interface granularity is finer, resulting in higher encapsulation costs.
3. Example 1: GPIO Control Encapsulation (Using LED Control as an Example)
1. Traditional CubeMX Project Code (in main.c):
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
HAL_Delay(500);
The issues with this approach are:
- GPIOC and PIN13 are directly exposed at the application layer
- Logic is hard-coded, making it non-reusable
2. Code After Driver Encapsulation:
(1) Interface Definition bsp_led.h
#ifndef __BSP_LED_H__
#define __BSP_LED_H__
typedef enum {
LED1 = 0,
LED2,
LEDn
} Led_TypeDef;
void BSP_LED_Init(Led_TypeDef led);
void BSP_LED_On(Led_TypeDef led);
void BSP_LED_Off(Led_TypeDef led);
void BSP_LED_Toggle(Led_TypeDef led);
#endif
(2) Implementation bsp_led.c
#include "bsp_led.h"
#include "stm32f4xx_hal.h"
typedef struct {
GPIO_TypeDef* port;
uint16_t pin;
} Led_GPIO_Map;
static const Led_GPIO_Map led_map[] = {
[LED1] = {GPIOC, GPIO_PIN_13},
[LED2] = {GPIOB, GPIO_PIN_0}
};
void BSP_LED_Init(Led_TypeDef led)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOC_CLK_ENABLE(); // Modify according to actual pin
GPIO_InitStruct.Pin = led_map[led].pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(led_map[led].port, &GPIO_InitStruct);
}
void BSP_LED_On(Led_TypeDef led)
{
HAL_GPIO_WritePin(led_map[led].port, led_map[led].pin, GPIO_PIN_RESET);
}
void BSP_LED_Off(Led_TypeDef led)
{
HAL_GPIO_WritePin(led_map[led].port, led_map[led].pin, GPIO_PIN_SET);
}
void BSP_LED_Toggle(Led_TypeDef led)
{
HAL_GPIO_TogglePin(led_map[led].port, led_map[led].pin);
}
(3) Application Layer Call
BSP_LED_Init(LED1);
while (1)
{
BSP_LED_On(LED1);
HAL_Delay(500);
BSP_LED_Off(LED1);
HAL_Delay(500);
}
With this approach, the application layer is completely independent of GPIO numbers, achieving driver abstraction and code reuse.
4. Example 2: USART Communication Encapsulation
Interface Definition bsp_usart.h
#ifndef __BSP_USART_H__
#define __BSP_USART_H__
void USART_Debug_Init(void);
void USART_Debug_SendChar(char c);
void USART_Debug_SendString(const char* str);
#endif
Implementation bsp_usart.c
#include "bsp_usart.h"
#include "stm32f4xx_hal.h"
extern UART_HandleTypeDef huart2; // Generated by CubeMX
void USART_Debug_Init(void)
{
// Optional: Additional parameter initialization
}
void USART_Debug_SendChar(char c)
{
HAL_UART_Transmit(&huart2, (uint8_t *)&c, 1, HAL_MAX_DELAY);
}
void USART_Debug_SendString(const char* str)
{
while (*str)
{
USART_Debug_SendChar(*str++);
}
}
Application Layer Usage
USART_Debug_SendString("System start\r\n");
To use this serial port module in other projects, simply replace the definition and initialization of huart2 without changing the encapsulation logic.
5. Enhancing Portability and Engineering Structure Recommendations
It is recommended to divide the entire driver encapsulation layer into independent bsp/ folders, for example:
/bsp
├── bsp_led.h / .c
├── bsp_usart.h / .c
├── bsp_key.h / .c
└── ...
Additionally, keep the CubeMX generated initialization code centralized in functions with the MX_ prefix, while delegating actual logic calls to bsp_ modules, forming a clear structural hierarchy:
- HAL Layer: Generated by STM32CubeMX, strongly hardware-dependent
- BSP Layer: Provides a unified interface, reusable, and customizable
- APP Layer: Only uses the BSP layer, does not directly call HAL functions
6. Conclusion
Extracting reusable driver layers from STM32CubeMX generated code not only enhances the modularity of the project but also lays a solid foundation for future cross-platform and cross-model migrations. By encapsulating the HAL/LL interfaces, we can build a clear layered architecture, significantly improving code maintenance and development efficiency.
This encapsulation approach will be particularly important in scenarios involving parallel projects, team collaboration, or later maintenance.