STM32嵌入式常见分层架构、代码分层思想(裸机开发)
目录
硬件抽象层(HAL - Hardware Abstraction Layer)
2. 板级支持包(BSP - Board Support Package)
为什么代码要分层
我们要明白,所谓的分层并不是什么高级的炫技,而是真正的可以解决实际问题:
当项目越来越复杂时,让代码仍然能够修改、扩展、调试和复用
-
不分层的代码确实也可以跑起来,但是对后续的维护和复用相同的代码时很麻烦
例:你将pid算法程序和应用pid应用程序写到一个.c里面,那么后续你想用这套pid算法程序,你不能直接复制粘贴了(因为里面还有别的代码,一粘贴全乱了,很多报错)
例如:
main.c
├── 读取IMU
├── PID计算
├── 电机控制
├── 串口通信
└── 数据发送
但是随着项目规模增加,会出现:
修改一个功能影响其他模块
代码无法复用
调试困难
多人协作困难
不分层存在的问题
例如:
将PID算法和具体应用写在一起:
void Depth_Control()
{
error = target - depth;
output = PID_Calculate();
Motor_Set(output);
}
看似简单,但是:
这个PID程序里面包含:
深度控制逻辑
电机控制
传感器数据
以后想把PID用于:
速度控制
姿态控制
温度控制
无法直接复制使用。因为算法和具体业务耦合在一起
分层的优势
前期:增加代码量
后期:提高开发效率
因为:不同功能修改互不影响。
例如:
更换传感器:只修改驱动层。
优化控制算法:只修改算法层。
增加机器人功能:只修改应用层。
嵌入式软件常见分层架构
Application 应用层
↓
Algorithm 算法层
↓
Driver 设备驱动层
↓
BSP 板级支持层
↓
HAL 硬件抽象层
↓
MCU Hardware
依赖方向:
上层调用下层,下层不知道上层存在。
层级
-
硬件抽象层(HAL - Hardware Abstraction Layer)
-
职责:HAL是对芯片(MCU)级外设的抽象,它为每个硬件外设(如GPIO、USART、SPI、ADC等)提供统一的接口。它将底层硬件的寄存器级操作进行封装,提供通用的函数来初始化、控制和操作外设。
-
优点:通过HAL层,可以实现对不同型号MCU的代码移植,因为相同外设的底层寄存器操作通过HAL层屏蔽了差异。
-
HAL解决的问题:不同MCU,例如:STM32F4和STM32H7寄存器结构不同。如果直接操作寄存器:移植困难。HAL封装后:
上层调用:HAL_UART_Transmit();不用关心底层寄存器。
实现示例:
GPIO控制:HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);
UART通信:HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);

HAL库自带,无需自己新建
2. 板级支持包(BSP - Board Support Package)
-
职责:BSP是针对具体开发板或硬件平台的硬件抽象层,BSP将MCU和所有板载外设(如LED、按键、屏幕、传感器等)封装成上层应用可以方便调用的接口。
-
特点:BSP的主要功能包括硬件的初始化以及提供对特定板卡的硬件资源的访问函数,例如对LED的控制、按键的读取、传感器的初始化等。
-
实现示例:
-

bsp_tim.c
#include "bsp_tim.h"
#include "tim.h"
#include "main.h"
#include "ROV_Chassis.h"
#include "ROV_Motor.h"
#include "drv_imu.h"
#include "drv_sensor.h"
#include "Rov_Control.h"
#include "Xbox_Rx.h"
#include "Data_Tx.h"
#include "bsp_adc.h"
#include "pid.h"
#include "fuzzy_pid.h"
void TIM_Init(void)
{
HAL_TIM_Base_Start_IT(&htim6); // 开启2ms定时器中断
HAL_TIM_Base_Start_IT(&htim7); // 开启2ms定时器中断
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == htim6.Instance)
{
ROV_Key_Process();
ROV_Motor_Mix(Xbox_Cmd_LY, Xbox_Cmd_LX, Depth_Out, Roll_Out, Pitch_Out+30.0f, Yaw_Out);//遥控器映射更新
ROV_Motor_Output();//电机PWM输出更新
Data_TX_Send();//发送遥测值更新
}
if(htim->Instance == htim7.Instance)
{
ROV_PID_Update();//PID计算更新
}
}
bsp_tim.h
#ifndef BSP_TIM_H
#define BSP_TIM_H
#include "stm32f4xx_hal.h"
void TIM_Init(void);
#endif
由此可见:
TIM_Init函数里面只有定时器中断的启用,后续想要开启其他定时器的步骤:
直接找到bsp文件夹-->找到bsp_tim.c--->在TIM_Init函数里面加就可以
由于TIM_Init已经在main.c里面调用过,所以写一遍就可以,而且一眼就知道是TIM定时器相关的初始化
bsp_uart.c
#include "bsp_uart.h"
#include "drv_imu.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"
#include "pid.h"
#include "tim.h"
#include "main.h"
#include "drv_sensor.h"
#include "Xbox_Rx.h"
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t size)//空闲中断回调
{
if(huart -> Instance == UART4)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart4, (uint8_t *)value_IMU, 1024);
}
if(huart -> Instance == USART2)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, Sensor_RX_buf, Sensor_RX_BUF_LEN);
}
if(huart -> Instance == USART3)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart3,(uint8_t *)Xbox_Rx_Buffer,27);
}
if(huart -> Instance == USART6)
{
}
}
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if(huart -> Instance == UART4)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart4, (uint8_t *)value_IMU, 1024);
}
if(huart -> Instance == USART2)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, Sensor_RX_buf, Sensor_RX_BUF_LEN);
}
if(huart -> Instance == USART3)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart3,(uint8_t *)Xbox_Rx_Buffer,27);
}
if(huart -> Instance == USART6)
{
}
}
bsp_uart.h
#ifndef __BSP_UART_H
#define __BSP_UART_H
#include "stm32f4xx_hal.h"
#endif
大家仔细看就会发现上面串口的代码十分简洁,只有DMA接收HAL_UARTEx_ReceiveToIdle_DMA这一个函数在里面被调用,其他处理代码在驱动层(Driver)
bsp_pwm.c
#include "bsp_pwm.h"
#include "Rov_Motor.h"
#include "math.h"
#include "tim.h"
void Motor_PWM_Init(void)
{
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); //PA5
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); //PB3
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); //PB10
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4); //PB11
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); //PA6
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); //PA7
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3); //PB0
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4); //PB1
// ===== TIM2 → 电机1~4 =====
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1500); // M1
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 1500); // M2
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, 1500); // M3
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, 1500); // M4
// ===== TIM3 → 电机5~8 =====
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 1500); // M5
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 1500); // M6
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, 1500); // M7
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, 1500); // M8
HAL_Delay(5000);
}
void Servo_PWM_Init(void)
{
HAL_TIM_PWM_Start(&htim8, TIM_CHANNEL_3); //PC8
}
bsp_pwm.h
#ifndef BSP_PWM_H
#define BSP_PWM_H
#include "stm32f4xx_hal.h"
void Motor_PWM_Init(void); //推进器PWM初始化
void Servo_PWM_Init(void); //舵机PWM初始化
#endif
HAL和BSP的区别
驱动层(Driver Layer)
-
职责:驱动层的职责是实现对外部硬件设备的具体控制,比如传感器、显示屏、外部存储设备等。驱动层位于HAL和BSP之间,有时与HAL层结合实现,对具体的外设提供初始化和操作接口。
-
特点:通常包含特定外设的操作接口
简单来说:
驱动层就是硬件和上层软件之间的翻译官。
上层只关心“我要实现什么功能”,驱动层负责“怎么操作底层硬件实现这个功能

其实在我看来,驱动层也可以叫设备层(Dvice),因为驱动的就是一个个的设备,所以那些需要通过串口接收数据的设备,例如深度传感器,遥控器接收机,陀螺仪等的数据处理可以写在这个层
-
驱动层作用:
串口发送数据↓设备协议↓解析数据↓提供变量
算法层(Algorithm)
-

-
算法层(Algorithm Layer)负责“计算和决策”,把输入的数据经过数学处理,转换成系统需要的控制结果。
-
驱动层负责“获取数据和执行动作”,算法层负责“如何计算应该怎么动作”。
-
算法层负责:数学方法,例如PID,卡尔曼滤波,低通滤波,斜坡规划等应用层(Application Layer)
应用层是整个系统的“大脑”,负责实现产品最终的功能逻辑。
前面的层级:
HAL:负责操作 MCU 外设
BSP:负责板级硬件资源管理
Driver:负责设备控制和数据获取
Algorithm:负责数学计算
这些层提供的是能力
而应用层负责:
把这些能力组合起来,实现系统想要完成的事情。
完整数据流
用户需求
↓
Application应用层
(决定系统做什么)
↓
Algorithm算法层
(计算应该怎么做)
↓
Driver驱动层
(控制设备)
↓
BSP / HAL
↓
硬件
例如2026年水下机器人想要实现:操控遥控器实现前进,同时保持水平
对应流程:
Application
↓
读取遥控器目标值
↓
调用PID算法
↓
计算补偿量
↓
电机混控
↓
调用电机驱动
↓
推进器工作
一个好的应用层应该是什么样?
优秀的应用层代码读起来像需求文档。
例如:
void Robot_Task(void)
{
Update_State();
if(System_Is_Safe())
{
Control_Attitude();
Control_Depth();
Update_Motor();
}
else
{
Emergency_Stop();
}
}
别人一看代码就知道机器人运行逻辑。
框架参考(第一版仅供参考,后续优化)

这里的ROV_Control,ROV_Motor文件应该属于应用层
碎碎念
最近在整理嵌入式代码架构的时候,突然有一些感触。
以前刚开始写单片机程序的时候,总觉得“能跑起来就是好代码”。一个 main.c 里面塞满各种函数,初始化、传感器读取、PID计算、电机控制、通信协议全部混在一起,虽然当时看起来很直接,但项目一复杂,就会发现代码越来越难维护。
后来接触到软件分层,才慢慢理解为什么工程项目总强调架构。
所谓分层,并不是为了让代码看起来更加高级,也不是为了刻意增加文件数量,而是在面对复杂系统时,给代码建立一种秩序。
HAL负责和芯片沟通,BSP负责管理板级资源,Driver负责让设备能够被软件使用,Algorithm负责解决数学问题,Application负责组织整个系统的行为。
每一层只做好自己的事情。
传感器不需要知道机器人为什么需要这个数据,它只负责准确地采集;
PID算法不需要知道控制的是电机还是陀螺仪,它只负责根据输入计算输出;
电机驱动不需要知道机器人下一步要做什么,它只负责把控制量变成真实动作。
以前觉得这些封装会让代码变复杂,但后来发现,真正复杂的不是代码数量,而是模块之间的联系。
没有架构的时候,修改一个地方可能牵一发动全身;
有架构以后,每个模块像一个独立的零件,坏了可以替换,升级可以迭代。
就像搭建一个机器人,不可能把所有零件焊死在一起。好的设计,是让每个部分通过接口连接,既相互配合,又保持独立。
嵌入式开发也是一样。
从最开始“让灯亮起来”,到后来做机器人控制、传感器融合、多电机协同,真正需要提升的不只是写代码的速度,而是思考代码的方式。
可能优秀的工程师和普通开发者之间的区别,不只是能不能写出功能,而是能不能让几年后的自己,甚至让别人接手的时候,依然看得懂这套系统。
代码最终服务的是项目,而不是一时的实现。
当什么都没有的时候,是最麻烦的时候,也是最可以义无反顾的做自己的时候,当熬过前期的麻烦,才能有后期的从容,人生也是如此,不要觉得自己当前做的无意义,因为你不知道现在的每个选择会把自己带向何方,你要做的就是当未来真的到来时,你会告诉自己:我早就准备好了。
愿大家拥有成品思维,先去做,再去做好,先行动,再完美。

本章结束
更多推荐



所有评论(0)