In embedded system development, as project complexity increases and cross-platform requirements grow, the bare-metal programming approach of “write whatever comes to mind” is becoming inadequate. Adopting a layered architecture is key to ensuring code maintainability, portability, and testability.Among these, the division and collaboration between the hardware abstraction layer and the driver layer is the core of the entire hardware-related code design.
What is the Driver Layer? What is the HAL Layer?
We can understand this with a metaphor of a “company”:
Hardware itself: is like the grassroots employees of a company (for example, a printer).
Driver Layer: is the department manager. He knows every detail and temperament of his department’s employees (hardware) very well (registers, timing, electrical characteristics). He directly manages and commands these employees, but people from other departments cannot directly call his employees.
HAL Layer: is the public interface or project manager. He defines a set of standard, easy-to-understand “work request” specifications (for example, “print this document”). People from the business department only need to make standard requests to the project manager, without worrying about whether the project manager is using the printer from department A or department B to complete the task.
This makes things much clearer, right? Understanding the concepts is the first step in writing code. Now, let’s provide definitions from a technical perspective:
- Driver Layer
The driver layer is the software layer that directly interacts with hardware registers. It is responsible for the lowest-level hardware operations and is the only code that “knows” the specific details of the hardware (such as chip models, peripheral register addresses, interrupt vector numbers, etc.).
Core responsibilities:
- Directly perform register read and write operations
- Interrupt service routines, handling interrupts generated by hardware
- Timing control, meeting hardware timing requirements
- Physical address mapping
- …
It is highly dependent on hardware; if a different MCU model is used, the driver layer code will almost need to be rewritten. This code focuses on making a specific hardware peripheral work.
- Hardware Abstraction Layer
The HAL layer is located above the driver layer and below the application program. It defines a set of unified, standardized API interfaces that provide hardware services to upper-layer applications (or middleware), thereby decoupling application logic from specific hardware implementations.
Core responsibilities:
-
Interface standardization, providing a unified function interface for the same type of hardware functionality.
-
Masking hardware differences, allowing upper-layer applications to call HAL functions without worrying about underlying information.
-
Resource management: managing and arbitrating some shared resources.
-
Providing convenient services, encapsulating common operation processes, and simplifying upper-layer calls.
This is a hardware-independent code that does not change with hardware changes and has portability. This means that when changing hardware platforms, only the lower-level drivers under the HAL layer need to be re-implemented or adapted, while the upper-layer application code does not need to be modified or requires minimal modification.
II. Hierarchical Division Architecture Diagram
A typical layered structure is as follows (taking STM32 sending serial data as an example):
The application layer needs to send the string “Hello”, calling HAL_UART_Transmit(&huart1, (uint8_t*)”Hello”, 5, HAL_MAX_DELAY);.
The HAL layer receives the request, performs parameter checks, and then calls the lower-level driver function to operate the hardware specifically. It first checks the UART status, starts the transmission, and waits for the sending to complete.
The driver layer writes the character ‘H’ into the USART1->TDR register, checks the TXE (transmit buffer empty) flag in the USART1->ISR register, and sends the remaining characters in turn.
The hardware sends the data through the TX pin at a specific baud rate according to the register configuration.
III. Practical Comparison: Without HAL vs. With HAL
Scenario: Light up an LED (connected to pin PC13)
Solution 1: Only the Driver Layer (or bare-metal direct operation)
// Directly operate registers, highly dependent on STM32
#include "stm32f1xx.h" // Header file for specific series
void LED_Init(void) {
// 1. Enable GPIOC clock
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
// 2. Configure PC13 as push-pull output, max speed 50MHz
GPIOC->CRH &= ~(GPIO_CRH_MODE13 | GPIO_CRH_CNF13);
GPIOC->CRH |= GPIO_CRH_MODE13_0;
}
void LED_On(void) {
// Pull low pin to light up LED (assuming LED is common anode)
GPIOC->BSRR = GPIO_BSRR_BR13;
}
void LED_Off(void) {
// Pull high pin to turn off LED
GPIOC->BSRR = GPIO_BSRR_BS13;
}
int main(void) {
LED_Init();
LED_On();
// ... other code
}
The above code, if it needs to be ported to the STM32F4 series, the RCC and GPIO register structures are completely different, so all register operations in LED_Init, LED_On, and LED_Off need to be modified.
Solution 2: Introduce the HAL Layer
// hal_led.h - HAL layer header file, interface is stable
#ifndef __HAL_LED_H
#define __HAL_LED_H
typedef enum {
LED_STATE_OFF = 0,
LED_STATE_ON
} LedState_TypeDef;
void HAL_LED_Init(void);
void HAL_LED_SetState(LedState_TypeDef state);
LedState_TypeDef HAL_LED_GetState(void);
#endif
// hal_led.c - HAL layer implementation, encapsulating lower-level driver calls
#include "hal_led.h"
#include "driver_led.h" // Include specific hardware driver
void HAL_LED_Init(void) {
DRIVER_LED_Init(); // Call driver layer initialization
}
void HAL_LED_SetState(LedState_TypeDef state) {
if (state == LED_STATE_ON) {
DRIVER_LED_On();
} else {
DRIVER_LED_Off();
}
}
// driver_led.c - Driver layer, for STM32F1
#include "stm32f1xx.h"
void DRIVER_LED_Init(void) {
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
GPIOC->CRH &= ~(GPIO_CRH_MODE13 | GPIO_CRH_CNF13);
GPIOC->CRH |= GPIO_CRH_MODE13_0;
}
void DRIVER_LED_On(void) {
GPIOC->BSRR = GPIO_BSRR_BR13;
}
void DRIVER_LED_Off(void) {
GPIOC->BSRR = GPIO_BSRR_BS13;
}
// main.c - Application layer, completely unaware of whether the lower layer is STM32F1 or F4
#include "hal_led.h"
int main(void) {
HAL_LED_Init();
HAL_LED_SetState(LED_STATE_ON); // Application layer only cares about "setting LED state"
// ... other code
}
At the beginning of the project, based on product functional requirements, design the HAL layer header file. The driver engineer implements specific hardware operations according to the HAL interface requirements. The upper layer can call the lower layer, but the lower layer must never call the upper layer. The HAL layer can call the driver layer, but the driver layer must never be aware of the existence of the HAL layer.
In this case, your “application layer” is code written based on the official HAL library, and the business logic of the application layer is perfectly protected.
Although it may increase some design costs and a small amount of performance overhead at the beginning of the project, from a software engineering perspective, the improvement in maintainability, portability, and team collaboration efficiency it brings is undoubtedly a valuable investment.
‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ END ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧