4. SDK 架构设计:分层模型、API 接口定义、文件组织

做 SDK 这事儿,我踩过不少坑。早年给一个嵌入式平台写打标控制 SDK,上来就把所有功能揉在一个大文件里,结果后面每个新需求都得改核心代码,测试组天天哀嚎。后来我学乖了——架构先行。先定好分层,再定接口,最后规划文件怎么摆。这节课我把这套方法拆给你看。

4.1 分层模型:把职责拆清楚

我的习惯是分三层:用户层、核心层、硬件抽象层。每层只管自己的事,层与层之间通过定义好的接口通信。

层次职责依赖关系
用户层 (User Layer)提供面向业务场景的 API,比如「开始打标」「停止打标」「设置参数」只调用核心层接口
核心层 (Core Layer)管理打标策略、状态机、任务队列,不关心硬件具体怎么操作依赖硬件抽象层接口
硬件抽象层 (HAL)封装硬件寄存器读写、中断处理、DMA 传输,对上层暴露统一结构体无依赖(或被板级支持包替换)

你想想看,如果哪天换了一颗 MCU,只需要重写 HAL 层就行,上面两层几乎不用动。这个设计帮我省了至少两次大规模返工——有一次客户临时要求从 STM32 切到 GD32,我一天就搞定移植。

关键原则:上层不能绕过核心层直接操作 HAL。我曾经为了性能开过后门,结果状态机混乱,bug 查了三天。后来老老实实走分层,反而更稳。

4.2 API 接口定义:名字和参数要一眼懂

接口设计是 SDK 的“脸面”。我给自己定了几条铁律:

  • 命名前缀统一:比如 mark_ 开头,所有函数一眼就知道属于打标模块。
  • 动词 + 名词:mark_start、mark_stop、mark_set_speed,别搞奇奇怪怪的缩写。
  • 参数有方向标记:输入参数用 const,输出参数用指针加 _out 后缀。
  • 错误码枚举清晰:统一返回 mark_err_t,包含 MARK_OK、MARK_ERR_BUSY、MARK_ERR_TIMEOUT 等。

举个头文件片段:

// mark_api.h
typedef enum {
    MARK_OK = 0,
    MARK_ERR_INVALID_PARAM = -1,
    MARK_ERR_HARDWARE_FAIL = -2,
    MARK_ERR_TIMEOUT = -3
} mark_err_t;

typedef struct {
    uint32_t power;        // 激光功率,单位 mW
    uint16_t speed;        // 打标速度,单位 mm/s
    uint8_t  repeat_cnt;   // 重复次数
} mark_config_t;

mark_err_t mark_init(void);
mark_err_t mark_start(const mark_config_t *config);
mark_err_t mark_stop(void);
mark_err_t mark_get_status(uint8_t *out_status);

我见过有人把参数类型混成 int 和 uint32_t 乱写,结果移植到 64 位平台直接崩。我个人习惯是明确宽度,能用 uint32_t 绝不用 int。

小技巧:在结构体末尾加一个 reserved 字段,万一将来要扩展,不用改 ABI。像 uint32_t reserved[4]; 就行。

4.3 文件组织:让新人也能快速找到需要的代码

文件结构搞好了,团队协作效率能翻倍。我常用的目录布局是这样的:

mark_sdk/
├── inc/                     # 公共头文件
│   ├── mark_api.h          # 用户层 API
│   ├── mark_core.h         # 核心层内部接口
│   └── mark_hal.h          # HAL 层定义
├── src/
│   ├── user/               # 用户层实现
│   │   └── mark_api.c
│   ├── core/               # 核心层
│   │   ├── mark_state_machine.c
│   │   └── mark_task_queue.c
│   └── hal/                # 硬件抽象层
│       ├── mark_hal_stm32.c
│       └── mark_hal_gd32.c
├── test/
│   ├── unit/               # 单元测试
│   └── integration/        # 集成测试
└── doc/
    └── api_reference.md

每个 .c 文件对应一个 .h 头文件,尽量不写大而全的“万能头”。为什么?我曾经把 300 行的代码塞进一个文件,后来加功能时发现耦合严重,重构花了三周。所以现在严格规定:一个模块一个文件夹,模块内部尽量不要跨目录包含。

4.4 整体架构图

下面这张图概括了上面讲的所有内容。从用户应用到底层硬件,每层之间的箭头就是 API 调用方向。左侧的“异常处理”和“配置管理”是贯穿各层的辅助模块,我习惯把它们单独拿出来,避免核心逻辑被大量错误处理代码污染。

SDK 分层架构图 用户应用 API 接口层 核心业务层 硬件抽象层 (HAL) 硬件设备 异常处理 配置管理 关注点 • 层间单向依赖 • 接口稳定,实现可换 • 错误码逐层传递
注意:辅助模块(异常处理、配置管理)虽然共用,但不要在它们与各层之间循环引用。我曾经在配置管理中依赖核心层的状态机,结果导致初始化死锁。解决方案是朝一个方向——辅助模块只提供接口,不主动调用上层逻辑。

最后总结几句

分层模型让代码好改、好测;API 定义让调用者不用翻源码;文件组织让协作不乱。这三块做好了,SDK 的生命力就会很长——哪怕过了两三年,新来的同事也能几小时上手。嗯,今天就到这。记住:架构不是一次画出来的,而是在重构中打磨出来的。你自己动手做一个项目,回来再看这些原则,体会会更深。