嵌入式C++硬件抽象层设计:一套代码无缝适配STM32、GD32等多系列MCU
换MCU最怕业务层直接调厂商HAL:stm32f1xx_hal.c里写HAL_UART_Transmit,gd32f10x.c里又要改usart_data_transmit,换一次改一次。稳定做法是“应用只依赖自有HAL接口,厂商库关在platform实现层”,STM32/GD32/RISC-V都按同一头文件出实现,构建宏切换目录即可。
一、接口层只定义行为,不碰寄存器
统一头文件放hal/,不包含任何芯片头:
namespace hal {
enum class GpioMode { InFloating, InPullup, OutPushPull, AltPushPull };
struct GpioPin { uint8_t port; uint8_t pin; };
class IGpio {
public:
virtual void config(const GpioPin&, GpioMode) = 0;
virtual void set(const GpioPin&, bool) = 0;
virtual bool get(const GpioPin&) = 0;
virtual ~IGpio() = default;
};
class IUart {
public:
virtual void init(uint32_t baud) = 0;
virtual int send(const uint8_t*, uint16_t) = 0;
virtual void irq_rx_callback(void(*cb)(uint8_t)) = 0;
virtual ~IUart() = default;
};
}
应用、协议栈、传感器驱动只包含hal/igpio.h、hal/iuart.h,不出现STM32的HAL_GPIO_WritePin,也不出现GD32的gpio_bit_set。
二、STM32与GD32分别实现,差异收敛在底层
STM32用Cube注册、GD32用标准库/官方HAL,都包成同一个IUart:
// hal_stm32/uart_impl.cpp
#include "hal/iuart.h"
#include "stm32f4xx_hal.h"
static UART_HandleTypeDef huart1;
static void(*rx_cb)(uint8_t) = nullptr;
class StmUart : public hal::IUart {
public:
void init(uint32_t baud) override {
huart1.Instance=USART1; huart1.Init.BaudRate=baud;
huart1.Init.WordLength=UART_WORDLENGTH_8B;
huart1.Init.StopBits=UART_STOPBITS_1;
huart1.Init.Parity=UART_PARITY_NONE;
huart1.Init.Mode=UART_MODE_TX_RX;
HAL_UART_Init(&huart1);
}
int send(const uint8_t* d,uint16_t n) override {
return HAL_UART_Transmit(&huart1,d,n,100)==HAL_OK?(int)n:-1;
}
void irq_rx_callback(void(*cb)(uint8_t)) override { rx_cb=cb; }
};
extern "C" void USART1_IRQHandler(void){
HAL_UART_IRQHandler(&huart1);
if(rx_cb){uint8_t b; if(HAL_UART_Receive(&huart1,&b,1,0)==HAL_OK) rx_cb(b);}
}
hal::IUart* uart_open(){ static StmUart u; return &u; }
// hal_gd32/uart_impl.cpp
#include "hal/iuart.h"
#include "gd32f10x.h"
static void(*rx_cb)(uint8_t) = nullptr;
class GdUart : public hal::IUart {
public:
void init(uint32_t baud) override {
usart_deinit(USART0);
usart_baudrate_set(USART0, baud);
usart_word_length_set(USART0, USART_WL_8BIT);
usart_stop_bit_set(USART0, USART_STB_1BIT);
usart_parity_config(USART0, USART_PM_NONE);
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
usart_enable(USART0);
}
int send(const uint8_t* d,uint16_t n) override {
for(uint16_t i=0;i<n;i++){
usart_data_transmit(USART0,d[i]);
while(RESET==usart_flag_get(USART0,USART_FLAG_TBE));
}
return (int)n;
}
void irq_rx_callback(void(*cb)(uint8_t)) override { rx_cb=cb; }
};
extern "C" void USART0_IRQHandler(void){
if(usart_interrupt_flag_get(USART0,USART_INT_FLAG_RBNE) && rx_cb){
rx_cb(usart_data_receive(USART0));
}
}
hal::IUart* uart_open(){ static GdUart u; return &u; }
GPIO同理:STM32配MODER/OSPEEDR/PUPDR,GD32配gpio_init,都收敛到config/set/get。应用拿hal::IGpio*,不关心谁在底层。
三、编译期切换,不堆#ifdef
CMake/Make按MCU宏只编一个实现目录:
if(PLATFORM STREQUAL "stm32f4")
add_definitions(-DPLATFORM_STM32)
target_sources(app PRIVATE hal_stm32/gpio_impl.cpp hal_stm32/uart_impl.cpp)
elseif(PLATFORM STREQUAL "gd32f10")
add_definitions(-DPLATFORM_GD32)
target_sources(app PRIVATE hal_gd32/gpio_impl.cpp hal_gd32/uart_impl.cpp)
endif()
引脚映射再单独建board_xxx.cpp,LED、RS485_DE、传感器CS都写成hal::GpioPin{1,5}这类表,换板子只改板级文件,不动车型协议。
四、多系列必须单独校的差异
STM32与GD32同Cortex-M也不等于完全等价:RCC_PLL配置、Flash等待周期、AHB/APB分频要按目标重算;GD32部分系列使能外设时钟后需总线下个周期再写寄存器,STM32无此要求。 USART使能顺序、标志位(TBE/RBNE vs TXE/RXNE)不同; TIM预分频位宽、ARR重装、高级定时器刹车/死区要按手册核对; 中断号差异大,F1系EXTI向量与GD32偏移不同,启动文件别混用; ADC采样时序、RTC预分频、DMA通道映射也要逐外设对照。 抽象层把这些“行为差异”封进platform,应用层只看到统一波特率、统一ms延时、统一接收回调。
五、轻量替代:模板零开销HAL
对主频敏感、不想虚表开销,可用模板把基地址当参数,编译期生成:
template<uint32_t BASE>
class RegUart {
public:
void send(uint8_t c){
volatile uint32_t* sr=(volatile uint32_t*)(BASE+0x00);
volatile uint32_t* dr=(volatile uint32_t*)(BASE+0x04);
while(!(*sr & 0x80)); *dr=c;
}
};
using DebugUart=RegUart<0x40011000>; // STM32 USART1/GD映射按手册改
缺点是多平台基地址、标志位仍要特化,适合驱动库内部;业务层仍建议用IGpio/IUart接口,兼顾可读与可测。
按“接口层—platform实现层—board映射层”三层切,STM32/GD32切换只换实现目录与启动文件,业务代码、Modbus、传感器解析、低功耗策略基本不动;新系列接入只需补hal_xxx实现并跑GPIO/UART/TIM/ADC自测,避免一换料全量返工。





