《架构特别篇三: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(开源准备中)。

相关推荐
捷配链1 小时前
新能源四层板叠层逻辑-最优架构适配电控高低压混合场景
架构·制造·pcb
AustinXu1 小时前
进化搜索:Harness Engineering 之后,Agent 怎么自己进化自己
架构·claude·敏捷开发
柒和远方1 小时前
Worker Readiness:为什么 `/health` 不能证明后台任务能跑
前端·后端·架构
xiaoshuai10242 小时前
夜里机器替你跑全量测试,挂了还自己修:我的夜间自动巡检系统
架构
星火燎原162 小时前
Android ViewModel 接收 Intent 参数
架构
OceanBase数据库官方博客2 小时前
基于 OceanBase 湖库一体架构,构建 AI 时代的数据基础设施
人工智能·架构·oceanbase
NOVAnet20232 小时前
SaaS 化 MDR 安全运营平台技术架构解析:态势感知、关联研判与自动化响应实践
安全·架构·自动化
heimeiyingwang2 小时前
【架构实战】分布式ID生成:从自增ID到雪花算法
分布式·架构