《架构特别篇三:DEVICES 层》

架构特别篇三:DEVICES 层 --- "协议解析,别管别的"

这是煜坤架构中最"克制"的一层。只有一条规则:解析协议、发布数据。不创建任务、不写业务逻辑。为什么这么设计?因为"单一职责"在嵌入式里比在桌面软件里更重要------你永远不知道这个驱动下个月会接到什么传感器。


1. DEVICES 层只做一件事

c 复制代码
// m10_parser.h --- M10 LiDAR 协议解析器
// 注意: 没有 FreeRTOS, 没有任务创建, 没有全局变量操作

// 输入: 原始字节流
bool M10_ParseFrame(const uint8_t *data, uint16_t len, M10_Frame_t *frame);

// 输出: 结构化的距离数组
void M10_GetRanges(const M10_Frame_t *frame, float *ranges, uint16_t *count,
                   float *angle_min, float *angle_inc);

这一层没有的

  • ❌ FreeRTOS 任务(任务创建是 CONTROL 层的事)
  • ❌ 全局变量(数据发布通过 CONTROL 层提供的指针)
  • ❌ 业务逻辑("这个障碍威胁大吗" → CONTROL 层判断)
  • ❌ HAL 调用(UART 初始化是 SYSTEM 层的事)

这一层有的

  • ✅ 字节流 → 结构化数据的翻译
  • ✅ 数据校验(CRC/帧头帧尾/长度)
  • ✅ 协议常量和数据结构定义

2. 为什么不让 DEVICES 层创建任务?

假设我们把任务创建放在 DEVICES 层:

c 复制代码
// 坏设计:
// m10_parser.c 里创建 FreeRTOS 任务
void M10_StartTask(void) {
    xTaskCreate(m10_task, "M10", 512, NULL, 4, &handle);
}

问题来了:

  1. 这个任务优先级多少?→ DEVICES 层不应该决定优先级(那是 CONTROL 的事)
  2. 栈多大?→ 取决于 CONTROL 层的调用链(路径规划嵌套多深)
  3. 数据推给谁?→ DEVICES 层不知道 CONTROL 层有哪些消费者

正确做法 :CONTROL 层写 lidar_task.c------创建任务、决定频率、调用 M10_ParseFrame、把结果推给 path_planner

复制代码
DEVICES 层: m10_parser.c      (纯解析, 可被任何任务调用)
CONTROL 层: lidar_task.c       (创建任务, 决定频率, 连接消费者)

3. 实际案例:MR72 和 M10 两种完全不同的驱动

MR72 毫米波雷达(33Hz, 14 字节帧/目标, 最多 8 个目标):

c 复制代码
// mr72_parser.h --- 数据结构
typedef struct {
    uint8_t target_id;
    float   distance;     // m
    float   azimuth;      // deg
    float   velocity;     // m/s (多普勒, 接近为负)
    float   threat_level; // 0~1
    bool    is_valid;
} RadarTarget_t;

typedef struct {
    RadarTarget_t targets[8];
    uint8_t       target_count;
    bool          is_valid;
    uint32_t      timestamp;
} RadarFrame_t;

// API --- 只有三个函数
void MR72_InitConfig(RadarConfig_t *config);
bool MR72_ParseFrame(const uint8_t *data, uint16_t len,
                     RadarFrame_t *frame, const RadarConfig_t *config);
void MR72_GetValidTargets(const RadarFrame_t *frame,
                          RadarTarget_t *out, uint8_t *count);

M10 LiDAR(240Hz 帧率, 92 字节/帧, 42 点/15°扇区):

c 复制代码
// m10_parser.h --- 数据结构
typedef struct {
    M10_Point_t points[42];
    uint8_t     valid_count;
    float       start_angle;  // deg
    float       speed_rpm;
    bool        is_valid;
} M10_Frame_t;

// API --- 也是三个函数
void M10_ResetFrame(M10_Frame_t *frame);
bool M10_ParseFrame(const uint8_t *data, uint16_t len, M10_Frame_t *frame);
void M10_GetRanges(const M10_Frame_t *frame, float *ranges, uint16_t *count,
                   float *angle_min, float *angle_inc);

共同模式:Parse → Get → 发布。两个驱动完全不同(一个是目标列表、一个是点云),但 API 风格一致。新人接一个新传感器,照着 MR72 或 M10 的模板写就行。


4. 数据怎么流向 CONTROL 层?

CONTROL 层的任务调用 DEVICES 层解析器,拿到结构体数据,写入全局变量或直接消费:

c 复制代码
// lidar_task.c (CONTROL 层)
static float s_lidar_ranges[42];  // 全局缓冲

while (1) {
    // ① 读 UART (SYSTEM 层)
    while (comGetBuf(COM6, rx_chunk, 512) > 0) {
        // ② 帧状态机 → 拼帧

        // ③ 解析 (DEVICES 层)
        if (M10_ParseFrame(frame_buf, 92, &m10_frame)) {
            // ④ 提取距离 (DEVICES 层)
            M10_GetRanges(&m10_frame, s_lidar_ranges, &count,
                          &angle_min, &angle_inc);

            // ⑤ 推入路径规划 (CONTROL 层)
            path_planner_update_sensors(&g_path_planner,
                NULL, s_lidar_ranges, count, angle_min, angle_inc);
        }
    }
    vTaskDelayUntil(&wake, pdMS_TO_TICKS(10));
}

DEVICES 层不知道"谁是消费者"。它只是把字节变成结构体。CONTROL 层决定"这个数据给谁"。


5. devices_list.h 和 devices_utils.h --- 聚合和工具

devices_list.h:把所有 DEVICES 模块的头文件聚合在一起。

c 复制代码
// devices_list.h --- 一次 include 引入所有设备驱动
#include "radar/mr72_parser.h"
#include "radar/m10_parser.h"
#include "mpu/mpu6050.h"
#include "rtkAntenna/wtrtk.h"
// ...

CONTROL 层只需要 #include "control_list.h"(它依赖 devices_list.h)。加新驱动 → 在 devices_list.h 加一行 → 全局可用。

devices_utils.h:跨设备共享的工具函数。

c 复制代码
// 大端字节序读取 (MR72 和 M10 都用)
static inline uint16_t byte_read_u16_be(const uint8_t *data, uint8_t start) {
    return ((uint16_t)data[start] << 8) | data[start + 1];
}

// CRC8 查表 (MR72 用)
static inline uint8_t crc8_calc(const uint8_t *p, uint8_t len) { ... }

// NMEA 字符串解析 (RTK 用)
static inline int32_t str_to_int_dec(const char *buf, uint8_t *decimals) { ... }

全部 static inline → 零链接开销 → 编译器自动内联到调用处。


6. DEVICES 层的"反模式"

这些是在开发过程中学会的"不该这么做":

反模式 1:在驱动里直接操作全局变量

c 复制代码
// 坏: 驱动写全局变量, 消费者不知道数据什么时候变的
bool M10_ParseFrame(...) {
    g_lidar_data.ranges[0] = ...  // 不行
}

正确做法:返回结构体,调用者决定数据放哪。

反模式 2:驱动里加"一点"业务逻辑

c 复制代码
// 坏: 驱动里判断威胁等级
bool MR72_ParseFrame(...) {
    if (target.distance < 3.0f) threat = HIGH;  // 驱动不该知道"3m是危险"
}

威胁判断属于 CONTROL 层(sensor_fusion/obstacle_avoidance)。

反模式 3:驱动依赖 FreeRTOS

c 复制代码
// 坏: 驱动里调 vTaskDelay
bool MR72_ParseFrame(...) {
    vTaskDelay(10);  // 驱动不知道调用者在哪个任务里
}

驱动应该是"纯函数"------给定输入,产生输出,无副作用。


7. 总结:DEVICES 层的核心价值

makefile 复制代码
其他人: 传感器驱动 = 任务 + 协议解析 + 数据处理 → 揉在一起
煜坤:   传感器驱动 = 协议解析 → 纯函数, 无副作用

好处:
  ① 一个驱动可以被多个任务复用 (同一解析器,M10 和回放模式共用)
  ② 换传感器只需改 DEVICES 层, CONTROL 层不动
  ③ 单元测试极简单: 给字节数组 → 检查输出结构体 → 不需要硬件

本文是《从零搭建无人船控制系统》架构特别篇。项目地址:煜坤 YuKun(开源准备中)。

相关推荐
FW-Linker7 分钟前
多卡聚合的数据包级并行传输原理在广电直播场景中详解
网络·5g·架构·智能路由器
Seoyoneh11 分钟前
呼叫中心系统云原生架构演进:传统自建与云端部署的技术实现对比分析
云原生·架构
天远API14 分钟前
零信任架构实战:基于天远手机空号检测V即时版构建自动化新客入驻网关
人工智能·智能手机·架构·自动化
2601_9622184739 分钟前
万象生鲜系统订单全生命周期追踪实现生鲜企业订单业务数字化可视
大数据·运维·微服务·云原生·架构
七夜zippoe40 分钟前
AI Agent 的三位一体架构:模型(大脑)+ 工具(双手)+ 记忆(海马体)的深度解析
人工智能·ai·架构·agent·三位一体
天空之城--1 小时前
Compose Multiplatform 如何做到跨平台
架构
l1t3 小时前
DeepSeek 4.1总结的Tom Lane 谈塑造 Postgres 三十年历程的架构决策
开发语言·数据库·postgresql·架构
Dawson Zhu10 小时前
工作流与 Agent 的工程选型:从“控制权归属“看 LLM 应用架构
人工智能·语言模型·架构·aigc·agi
nvd1111 小时前
深入现代 Web 鉴权架构:网关统一代理 (Forward-Auth) vs 前端持有 JWT 的终极选型与边缘同域实践
前端·架构·状态模式
天空之城--11 小时前
Android行业一周动态:编码趋势与行业资讯汇总
android·性能优化·架构·kotlin·android jetpack