STM32嵌入式常见分层架构、代码分层思想(裸机开发)

发布时间:2026/7/31 11:13:03
STM32嵌入式常见分层架构、代码分层思想(裸机开发) 目录为什么代码要分层不分层存在的问题分层的优势嵌入式软件常见分层架构层级硬件抽象层HAL - Hardware Abstraction Layer2. 板级支持包BSP - Board Support Packagebsp_tim.cbsp_tim.hbsp_uart.cbsp_uart.hbsp_pwm.cbsp_pwm.hHAL和BSP的区别驱动层Driver Layer算法层Algorithm完整数据流一个好的应用层应该是什么样框架参考第一版仅供参考后续优化碎碎念本章结束为什么代码要分层我们要明白所谓的分层并不是什么高级的炫技而是真正的可以解决实际问题当项目越来越复杂时让代码仍然能够修改、扩展、调试和复用不分层的代码确实也可以跑起来但是对后续的维护和复用相同的代码时很麻烦例你将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_Out30.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这一个函数在里面被调用其他处理代码在驱动层Driverbsp_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初始化 #endifHAL和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_ControlROV_Motor文件应该属于应用层碎碎念最近在整理嵌入式代码架构的时候突然有一些感触。以前刚开始写单片机程序的时候总觉得“能跑起来就是好代码”。一个 main.c 里面塞满各种函数初始化、传感器读取、PID计算、电机控制、通信协议全部混在一起虽然当时看起来很直接但项目一复杂就会发现代码越来越难维护。后来接触到软件分层才慢慢理解为什么工程项目总强调架构。所谓分层并不是为了让代码看起来更加高级也不是为了刻意增加文件数量而是在面对复杂系统时给代码建立一种秩序。HAL负责和芯片沟通BSP负责管理板级资源Driver负责让设备能够被软件使用Algorithm负责解决数学问题Application负责组织整个系统的行为。每一层只做好自己的事情。传感器不需要知道机器人为什么需要这个数据它只负责准确地采集PID算法不需要知道控制的是电机还是陀螺仪它只负责根据输入计算输出电机驱动不需要知道机器人下一步要做什么它只负责把控制量变成真实动作。以前觉得这些封装会让代码变复杂但后来发现真正复杂的不是代码数量而是模块之间的联系。没有架构的时候修改一个地方可能牵一发动全身有架构以后每个模块像一个独立的零件坏了可以替换升级可以迭代。就像搭建一个机器人不可能把所有零件焊死在一起。好的设计是让每个部分通过接口连接既相互配合又保持独立。嵌入式开发也是一样。从最开始“让灯亮起来”到后来做机器人控制、传感器融合、多电机协同真正需要提升的不只是写代码的速度而是思考代码的方式。可能优秀的工程师和普通开发者之间的区别不只是能不能写出功能而是能不能让几年后的自己甚至让别人接手的时候依然看得懂这套系统。代码最终服务的是项目而不是一时的实现。当什么都没有的时候是最麻烦的时候也是最可以义无反顾的做自己的时候当熬过前期的麻烦才能有后期的从容人生也是如此不要觉得自己当前做的无意义因为你不知道现在的每个选择会把自己带向何方你要做的就是当未来真的到来时你会告诉自己我早就准备好了。愿大家拥有成品思维先去做再去做好先行动再完美。本章结束