|
xt-sdk 文档 v1.0.3
xt-sdk 嵌入式 SDK API 参考
|
本文档是 xt-sdk 文档系统的元规范(meta-spec)。所有后续文档工作必须遵循本规范。
版本:v1.0 适用范围:xt-sdk 全仓库文档
xt-sdk 的文档按准确度分为三档: T1(最高) 、 T2(次准) 、 T3(最低) 。文档撰写者必须在动笔前明确自己写的文档属于哪一档,并严格遵守该档的内容边界。
| 属性 | 说明 |
|---|---|
| 形式 | 头文件 / 源文件的 doxygen 注释 |
| 准确度 | 最准(随代码一起改,编译时强制校验) |
| 例子 | components/xt_task/xt_timer2.h |
| 写什么 | API 签名、参数、返回值、错误码、线程安全、可重入性、简单示例 |
T1 文档直接写在代码旁边,是开发者修改代码时必须同时维护的注释。如果 API 签名变了而注释没改,doxygen 会生成错误或不一致的 HTML,开发者一眼就能发现。T1 与代码的物理距离为零,失真的概率最低。
| 属性 | 说明 |
|---|---|
| 形式 | 测试代码 |
| 准确度 | 次准(编译时强制更新,但可能覆盖不全) |
| 例子 | tests/xt_timer2/xt_main.c |
| 写什么 | 完整使用范例、边界条件、行为契约验证 |
测试代码必须编译通过,因此 API 签名变更时测试代码会报错,迫使开发者同步更新。但测试代码的覆盖率有限:它只验证被测到的行为,没写测试的边界条件仍然可能失真。此外,测试代码的意图需要人类阅读才能理解,不像 T1 那样直接对应 API。
| 属性 | 说明 |
|---|---|
| 形式 | 独立 .md 文档 |
| 准确度 | 最易失真(没有编译器强制校验,全靠人工维护) |
| 例子 | components/xt_task/xt_timer2.md |
| 写什么 | 只写"why" :设计动机、ADR、跨组件关系、性能特性、反模式 |
独立 .md 文档与代码没有物理绑定。开发者改代码时,编辑器不会自动提示"你忘了改对应的 .md"。历史经验表明,独立文档在代码迭代 3~5 轮后几乎必然与代码脱节。因此 T3 的边界必须严格限定: 只写代码里找不到的信息 。
基于三档排序,推导出以下七条硬性规则。
以下信息属于 API 细节,T3 禁止重复(函数签名除外,T3 可列大纲):
以下信息 不属于 API 细节 ,可以且应该出现在 T3:
# 坏:T3 重复了 T1 的 API 细节 ## xt_timer2_start 函数 ```c int xt_timer2_start(xt_timer2_t *timer, uint32_t timeout_ms, xt_timer2_callback_t cb, void *arg); ``` - `timer`:定时器句柄 - `timeout_ms`:超时时间,单位毫秒 - `cb`:回调函数 - `arg`:回调参数 - 返回值:0 表示成功,负数表示错误 # 好:T3 只写"why"和反模式 ## 为什么超时参数用 uint32_t 而不是 uint64_t? 在 1ms 分辨率的定时器上,uint32_t 可覆盖约 49.7 天的超时。 嵌入式场景极少需要更长的单次定时,因此选择 uint32_t 以节省内存。 ## 反模式:在回调中调用 xt_timer2_stop(timer, true) 回调参数 `timer` 是当前定时器句柄。在回调内部调用 `xt_timer2_stop(timer, true)` 会立即释放 timer 结构体,导致回调返回后访问已释放内存。 正确做法:在回调中调用 `xt_timer2_stop(timer, false)`,或标记待停止由主循环处理。
| 类型 | 示例 | 归属 |
|---|---|---|
| what | xt_timer2_start() 启动一个定时器 | T1(doxygen 自动生成) |
| what | 参数 timeout_ms 表示超时毫秒数 | T1(doxygen 注释) |
| why | 为什么选择单链表而不是数组管理定时器?因为定时器动态增删频繁,链表 O(1) 插入删除优于数组 O(n) 移动 | T3 |
| why | 为什么回调签名是 void (*)(void *arg) 而不是带事件类型的 void (*)(xt_timer2_event_t evt, void *arg)?因为 timer2 只有一种事件(超时),减少事件类型枚举可以简化 API 面 | T3 |
| why | 为什么 xt_timer2_stop() 的 sync 参数存在?因为有些场景需要确保定时器完全停止后才能释放资源,而另一些场景允许延迟释放以提高并发性能 | T3 |
T3 的作者在落笔前必须问自己:"读者在头文件里找不到这个信息吗?"如果找得到,就不要写。
为了让用户在阅读 doxygen HTML 时能跳转到 T3 内容,T3 文档需要在头文件的 doxygen 模块定义中被引用。
在头文件的 @defgroup 块中,使用 @subpage 或 \includedoc 指向对应的 .md 文件:
``` /** * @defgroup xt_task_timer2 Timer2 高精度定时器 * @brief 提供毫秒级定时器管理,支持单次和周期模式。 * * @subpage xt_timer2_design "设计文档与反模式指南" */ ```
对应的 T3 文档开头需要声明 @page:
``` \@page xt_timer2_design Timer2 设计文档与反模式指南 (注意删掉`\`) # Timer2 设计文档与反模式指南 ... ```
这样 doxygen 生成的 HTML 会在 API 文档页面中插入一个链接,引导用户跳转到 T3 内容。
当 API 发生变更时,必须按以下顺序更新文档:
如果读者发现 T3(.md 文档)中描述的 API 行为与 T1(头文件 doxygen 注释)不一致, 无条件信任 T1 。
原因:T1 随代码编译,最可信;T3 没有编译器校验,必然比 T1 更容易滞后。
T3 的作者在发现冲突时,应立即修正 T3,而不是质疑 T1。
xt-sdk 的文档组织需要兼容多种阅读场景。以下表格列出各场景的现状与目标:
| 场景 | 主要形式 | 现状 | 目标 |
|---|---|---|---|
| 场景 A:文档站 | MkDocs(优先候选)+ doxygen HTML | 未做 | 暂不做,但目录结构兼容 |
| 场景 B:doxygen HTML | doxygen + awesome-css | 配置中 | 首版推出 |
| 场景 C:开发者直读头文件 + markdown | 源码 doxygen + 组件 .md | 部分有 | 完善 |
| 场景 D:GitHub 在线阅读 markdown | README.md 自动渲染 | 有(仅最小入口) | 可能以 README.md 作为 MkDocs 的 index.md |
MkDocs 是长期目标,用于提供美观的导航式文档站。目前暂不上线,但所有 .md 文档的目录结构必须兼容 MkDocs 的约定(例如 docs/ 为根目录、支持 mkdocs.yml 导航配置)。
doxygen 是近期目标,用于从源码注释自动生成 API 参考。doxygen 配置已在仓库中,首版要求:
这是最常见的开发场景:开发者直接在 IDE 或编辑器中打开头文件和 .md 文件阅读。要求:
GitHub 会自动渲染 README.md,因此以下约定必须遵守:
不同文档类型有不同的生命周期和变更规则。下表规定各类文档的维护方式:
| 文档类别 | 维护方式 | 触发原因 |
|---|---|---|
| grill 问题清单 | 只能附加 | 时间线、防 agent 忘记 |
| 任务列表 | 单独维护 | 状态频繁变更(pending → in_progress → done) |
| 决策记录 | 只能附加 | 决策不能撤回,追加新的决策 |
| 调研报告 | 单次写完 | 一次性产出 |
| 方案文档 | 单次写完 + 修订追加章节 | 终态,但可追加新版本章节 |
| 子代理报告 | 单次写完 | 一次性产出 |
| 校验报告 | 单次写完 | 一次性产出 |
| 会话笔记 / 工作过程文档 | 只能附加或小改 | 时间线,记录当时发生了什么 |
| handoff 交接文档 | 单次写完(每次会话一份) | 快照,供下一个会话接手 |
在撰写或修改文档前,用以下标准判断维护方式:
``` # 坏:修改了 grill 历史记录 ## 2024-01-01 的疑问 为什么定时器用链表而不是数组? (此处原本有回答,被后续修改删掉了) # 好:grill 只能追加 ## 2024-01-01 的疑问 为什么定时器用链表而不是数组? > 答:因为动态增删频繁,链表 O(1) 插入删除优于数组 O(n)。 ## 2024-01-02 的新疑问 那为什么不直接用数组 + free list? > 答:free list 需要额外维护位图,代码复杂度上升,当前场景不需要。 ```
在与用户(或子代理之间)的对话中, 使用绝对路径 ,例如:
绝对路径消除歧义,避免"这个 components 目录是哪个 components"的困惑。
在正式文档(包括 .md 文件、doxygen 注释)中, 使用相对路径 ,base 为仓库根目录 D:\work\pt\xt-sdk-family\xt-sdk:
``` 头文件路径:`components/xt_task/xt_timer2.h` 测试代码路径:`tests/xt_timer2/xt_main.c` ```
相对路径让文档在克隆到不同目录后仍然可读。
正式文档(Doxyfile、README、教程)在引用文件时, 忽略 .gitignore 忽略的文件和目录 :
例外 : .opencode 目录虽然也被 .gitignore 忽略,但其中的配置文件(例如技能定义)可以作为参考。引用时需注明"非权威参考"。
这是 xt-sdk 文档系统的核心痛点:组件目录下往往同时存在 .md 文档和头文件 doxygen 注释,内容大量重复,维护困难。
三者内容不重叠。T3 的作者在动笔前必须阅读 T1,确保不重复。
如果 T3 确实需要引用代码来说明问题, 不要复制粘贴 ,而是用 doxygen 的 @snippet 或 \snippet 指令引用测试代码中的标记段:
```c
// tests/xt_timer2/xt_main.c
//! [anti_pattern_callback_stop]
void bad_callback(void *arg) {
xt_timer2_t *timer = (xt_timer2_t *)arg;
xt_timer2_stop(timer, true); // 错误:在回调中同步释放
}
//! [anti_pattern_callback_stop]
```
在 T3 文档中引用:
``` ## 反模式:在回调中同步停止并释放 \snippet tests/xt_timer2/xt_main.c anti_pattern_callback_stop ```
这样代码片段随测试代码一起维护,T3 不会失真。
如果现有组件 .md 已经存在大量 API 细节重复,重写时遵循以下范围:
T3 文档必须在对应组件的 doxygen @defgroup 块中被引用,确保用户在阅读 doxygen HTML 时能发现 T3 的存在:
``` /** * @defgroup xt_task_timer2 Timer2 高精度定时器 * @brief ... * * 设计动机与反模式见 \subpage xt_timer2_design。 */ ```
后续子代理在撰写 xt-sdk 的 .md 文档时,必须逐条对照以下检查清单:
| 你想写什么 | 应该写在哪 | 禁止写在哪 |
|---|---|---|
| 函数签名(大纲) | T3(.md API 参考)+ T1(头文件 doxygen) | — |
| 参数说明、返回值 | T1(头文件 doxygen) | T3(.md) |
| 线程安全、可重入性 | T1(头文件 doxygen) | T3(.md) |
| 5~10 行简单示例 | T1(头文件 @code) | T3(.md) |
| 完整生命周期示例 | T2(测试代码) | T3(.md) |
| 边界条件、错误路径 | T2(测试代码) | T3(.md) |
| 设计动机、ADR | T3(.md) | T1(头文件) |
| 跨组件关系、数据流 | T3(.md) | T1(头文件) |
| 性能特性、复杂度分析 | T3(.md) | T1(头文件) |
| 反模式、已知陷阱 | T3(.md) | T1(头文件) |
| 版本演进(为什么变更) | T3(.md) | T1(头文件) |