架构特别篇一五层架构 — 为什么分层是煜坤的核心竞争力PX4 用 uORB 消息总线APM 用 C 纯虚 HAL。煜坤的答案是什么五层架构APP → CONTROL → DEVICES → SYSTEM → BSP。纯 C静态 inline零虚函数开销。这一章讲清每一层该干什么不该干什么。1. 不分层的代价刚开始写嵌入式代码的人大概率这样组织// main.c — 所有东西揉在一起intmain(void){MX_USART1_Init();MX_I2C_Init();MX_CAN_Init();// ...while(1){// GPS 解析if(uart3_byte_available()){nmea_parse(...);ekf_update(...);pid_control(...);motor_output(...);}}}能跑但三个月后你回头看——“这段 GPS 解析代码在哪个文件里来着”不分层的代码只有作者自己能维护。开源项目的第一要务是让陌生人能在 10 分钟内理解项目结构。2. 煜坤的五层架构┌───────────────────────────────────────────────────┐ │ APP 应用聚合层 │ │ 我要用哪些模块按什么顺序启动 │ │ 只做一件事创建 FreeRTOS 任务 │ ├───────────────────────────────────────────────────┤ │ CONTROL 核心逻辑层 │ │ 船怎么控制障碍怎么绕数据怎么融合 │ │ 所有 FreeRTOS 任务 算法 业务逻辑 │ │ 依赖 DEVICES 提供的全局数据不直接调 HAL │ ├───────────────────────────────────────────────────┤ │ DEVICES 设备驱动层 │ │ 这个传感器输出什么协议怎么解析 │ │ 纯协议解析不含 FreeRTOS 任务 │ │ 输出全局数据结构体 (g_xxx_data) │ ├───────────────────────────────────────────────────┤ │ SYSTEM 硬件抽象层 │ │ STM32 的 UART 怎么配DMA 怎么分引脚在哪 │ │ HAL 封装 中断 DMA 环形缓冲 │ │ 不包含任何业务逻辑 │ ├───────────────────────────────────────────────────┤ │ BSP 板级支持包 │ │ 这块板子上焊了什么外设芯片 │ │ SPI Flash / I2C GPIO 扩展器 / 以太网 PHY │ ├───────────────────────────────────────────────────┤ │ STM32H7 HAL CMSIS Middlewares │ │ FreeRTOS / lwIP / cJSON / MAVLink │ └───────────────────────────────────────────────────┘3. 核心规则上层依赖下层下层绝不依赖上层依赖方向 (只能向下): APP ──→ CONTROL ──→ DEVICES ──→ SYSTEM ──→ BSP ──→ HAL ↑ ↑ ↑ ↑ ↑ 可以 可以 可以 可以 可以 调用 调用 调用 调用 调用 反方向 (禁止): SYSTEM ──✕──→ DEVICES (硬件层不知道雷达是什么) DEVICES ──✕──→ CONTROL (协议解析器不创建任务) CONTROL ──✕──→ APP (算法模块不决定启动顺序)这个规则说起来简单做起来需要纪律。举几个差点违反的例子违规示例 1雷达解析器里直接调path_planner_update_sensors()这违反了DEVICES 层不依赖 CONTROL 层。正确做法雷达任务CONTROL 层调协议解析器DEVICES 层拿到数据然后 CONTROL 层内部调 path_planner。违规示例 2在代价地图代码里直接读写 STM32 寄存器代价地图是 CONTROL 层不应该知道硬件。它只调 SYSTEM 层提供的抽象接口。4. 每一层该干什么、不该干什么APP 层 - 只管编排该做: ✅ 创建 FreeRTOS 任务 (xTaskCreateStatic) ✅ 决定任务启动顺序 ✅ 调用各层初始化函数 不该做: ❌ 写任何算法逻辑 ❌ 直接操作硬件寄存器 ❌ 定义全局数据结构体为什么单独抽一层换一个应用场景比如从无人船改成无人车只需要改这一层——把船的推进器任务换成车的电机任务。CONTROL 层里 90% 的代码PID/EKF/规划不变。CONTROL 层 - 核心大脑该做: ✅ 所有 FreeRTOS 任务 ✅ 控制算法 (PID/L1/EKF/DWA/D* Lite) ✅ 传感器融合 数据消费 ✅ 通信协议的业务逻辑 不该做: ❌ 直接操作硬件寄存器 → 调 SYSTEM 层 ❌ 解析传感器原始协议 → 调 DEVICES 层 ❌ 决定 SPI Flash 的擦除粒度 → 那是 BSP 层的事DEVICES 层 - 纯协议解析该做: ✅ 解析传感器数据帧 (NMEA/HEX/iBUS) ✅ 发布到全局数据结构体 ✅ 数据校验 (CRC/帧尾/长度) 不该做: ❌ 创建 FreeRTOS 任务 → 那是 CONTROL 层的事 ❌ 写业务逻辑 (这个目标威胁大吗) → CONTROL 层判断 ❌ 调 HAL 初始化 (UART 配好了,SYSTEM 层已经做了)SYSTEM 层 - 硬件薄封装该做: ✅ 封装 HAL (comGetBuf 替代 HAL_UART_Receive) ✅ 引脚定义集中到一个文件 (cc_h7_def.h) ✅ DMA 流分配 环形缓冲 ✅ 中断服务函数 不该做: ❌ 解析任何传感器协议 ❌ 写任何业务判断 ❌ 依赖 FreeRTOS (可以用在 ISR 里,但不创建任务)5. 分层后怎么跨层——依赖注入理论上 CONTROL 不直接调 SYSTEM。但实际上 CONTROL 里的串口任务需要comGetBuf。怎么解不跨层用聚合头文件链// 依赖链:sys_config.h(SYSTEM 层全部头文件)↓ devices_list.h(DEVICES 层全部头文件,include 了 sys_config.h)↓ control_list.h(CONTROL 层全部头文件,include 了 devices_list.h)↓ zscode_app.h(APP 层,include 了 control_list.h)CONTROL 层代码只 includecontrol_list.h不直接 include SYSTEM 层的头文件。如果需要 SYSTEM 层的函数——通过devices_list.h间接获得。这样做的好处CONTROL 层不知道自己用的是 STM32 还是 GD32。comGetBuf声明在devices_list.h的依赖链里。换芯片只需改 SYSTEM 层的实现CONTROL 层代码零改动。6. 和 PX4/APM 的架构对比维度PX4ArduPilot煜坤语言CC纯 C核心抽象uORB 消息总线AP_HAL 纯虚基类五层静态分层IPC 机制发布-订阅 (异步)虚函数调用 (同步)全局结构体 函数调用跨平台NuttX POSIXChibiOS/Linux/SITL/ESP32STM32H7 (cc_h7_def.h 换芯片)虚函数开销有 (uORB 回调)有 (HAL 虚表)零 (全 static inline)新人 10 分钟看懂困难 (C 继承链)困难 (C 模板)容易 (C 结构体 扁平调用)煜坤的取舍放弃了跨 OS 可移植性只跑 FreeRTOS→ 换来了零虚函数开销放弃了异步消息总线 → 换来了调用栈可追踪放弃了 C 多态 → 换来了纯 C 的极简依赖这个取舍在当前阶段是正确的一个人开发、一个硬件平台、先跑稳。未来如果真要跨平台再加 HAL 层——但我们知道什么时候该加不是一开始就加。7. 实际案例加一个新传感器要改几层以我们加的 M10 LiDAR 为例① SYSTEM 层: cc_h7_def.h 加 COM6 引脚定义 (如果引脚没配) → 改 1 个文件, 5 行 ② DEVICES 层: 写 m10_parser.h/.c (纯协议解析) → 新增 2 个文件, ~200 行 ③ CONTROL 层: 写 lidar_task.h/.c (FreeRTOS 任务, 调解析器path_planner) → 新增 2 个文件, ~250 行 ④ APP 层: zscode_app.c 加 lidar_task_init() → 改 1 个文件, 1 行CONTROL 层已有的代码DWA/DLite/代价地图/传感器融合全部不动。* 因为 LiDAR 接入点就是path_planner_update_sensors()一个函数——代价地图和规划器本来就在那等着传感器数据进来。这就是分层的好处新功能只影响 2-3 层核心算法零改动。本文是《从零搭建无人船控制系统》架构特别篇。项目地址[煜坤 YuKun]开源准备中。