xt-sdk 文档 v1.0.3
xt-sdk 嵌入式 SDK API 参考
载入中...
搜索中...
未找到
文档准确度排序与组织原则

本文档是 xt-sdk 文档系统的元规范(meta-spec)。所有后续文档工作必须遵循本规范。

版本:v1.0 适用范围:xt-sdk 全仓库文档


第 1 章:文档准确度三档排序(核心)

xt-sdk 的文档按准确度分为三档: T1(最高) 、 T2(次准) 、 T3(最低) 。文档撰写者必须在动笔前明确自己写的文档属于哪一档,并严格遵守该档的内容边界。

1.1 T1(最高准确度)

属性 说明
形式 头文件 / 源文件的 doxygen 注释
准确度 最准(随代码一起改,编译时强制校验)
例子 components/xt_task/xt_timer2.h
写什么 API 签名、参数、返回值、错误码、线程安全、可重入性、简单示例

为什么 T1 最准?

T1 文档直接写在代码旁边,是开发者修改代码时必须同时维护的注释。如果 API 签名变了而注释没改,doxygen 会生成错误或不一致的 HTML,开发者一眼就能发现。T1 与代码的物理距离为零,失真的概率最低。

T1 必须写的内容

  • API 签名 :函数名、参数类型、参数名、返回值类型。
  • 参数说明 :每个参数的含义、取值范围、单位、是否可为 NULL。
  • 返回值说明 :正常返回值、错误返回值、错误码枚举。
  • 线程安全 :是否线程安全、是否需要外部加锁、是否可重入。
  • 简单示例 :一段 5~10 行的最小可用示例,展示函数怎么调用。
  • 前置条件 / 后置条件 :调用前必须满足什么、调用后保证什么。

T1 禁止写的内容

  • 设计动机 :不要在头文件里写"为什么选这个方案",这会导致头文件膨胀,且设计动机变更时不需要改头文件。
  • 跨组件关系 :不要在头文件里写"这个组件依赖那个组件",这种关系应由构建系统或架构图描述。
  • 反模式 :不要在头文件里写"不要这样做",这属于 T3 范畴。
  • 大段示例程序 :超过 20 行的示例应放在 T2(测试代码)或 T3(独立文档)中。

1.2 T2(次准)

属性 说明
形式 测试代码
准确度 次准(编译时强制更新,但可能覆盖不全)
例子 tests/xt_timer2/xt_main.c
写什么 完整使用范例、边界条件、行为契约验证

为什么 T2 次准?

测试代码必须编译通过,因此 API 签名变更时测试代码会报错,迫使开发者同步更新。但测试代码的覆盖率有限:它只验证被测到的行为,没写测试的边界条件仍然可能失真。此外,测试代码的意图需要人类阅读才能理解,不像 T1 那样直接对应 API。

T2 必须写的内容

  • 完整使用范例 :从初始化到销毁的完整生命周期示例。
  • 边界条件 :NULL 指针、0 长度、最大值、最小值、并发调用等极端场景。
  • 行为契约验证 :断言返回值、断言状态变化、断言副作用。
  • 错误路径 :展示 API 在错误输入下的行为(返回什么错误码、是否触发回调)。

T2 禁止写的内容

  • API 签名重复 :不要在测试代码里再写一遍函数签名文档(这是 T1 的事)。
  • 设计动机 :测试代码里不要写"为什么要这样设计"。
  • 跨组件架构说明 :测试代码只测本组件,不要描述组件间关系。

1.3 T3(最低准确度)

属性 说明
形式 独立 .md 文档
准确度 最易失真(没有编译器强制校验,全靠人工维护)
例子 components/xt_task/xt_timer2.md
写什么 只写"why" :设计动机、ADR、跨组件关系、性能特性、反模式

为什么 T3 最易失真?

独立 .md 文档与代码没有物理绑定。开发者改代码时,编辑器不会自动提示"你忘了改对应的 .md"。历史经验表明,独立文档在代码迭代 3~5 轮后几乎必然与代码脱节。因此 T3 的边界必须严格限定: 只写代码里找不到的信息 。

T3 必须写的内容

  • 设计动机 :为什么选这个方案? rejected 的替代方案是什么?
  • ADR(架构决策记录) :关键决策的上下文、决策、后果。
  • 跨组件关系 :这个组件与哪些组件交互?数据流怎么走?
  • 性能特性 :时间复杂度、内存占用、延迟要求、吞吐瓶颈。
  • 反模式(anti-pattern) :常见误用、已知陷阱、必须避免的模式。
  • 版本演进 :重大变更的历史记录(不是变更日志,而是"为什么变更")。

T3 禁止写的内容

  • 参数说明 :不要解释每个参数的含义。读者应直接看 T1。
  • 返回值说明 :不要列出返回值的正常值和错误码。读者应直接看 T1。
  • 基础函数调用示例 :不要写 5~10 行的基础函数调用示例(如"如何调用 init")。读者应直接看 T1 的 @code 块。完整使用场景示例(从初始化到清理的完整流程)和关键场景说明(特定条件下的行为)可以且应该在 T3 中
  • 线程安全说明 :不要重复头文件里的线程安全注释。读者应直接看 T1。
  • 逐函数讲解 :不要写"函数 A 用来做什么、函数 B 用来做什么"的流水账。这是 T1 的 doxygen HTML 自动生成内容。

第 2 章:推导规则

基于三档排序,推导出以下七条硬性规则。

规则 1:T3 禁止重复 T1 的 API 细节

什么是"API 细节"?

以下信息属于 API 细节,T3 禁止重复(函数签名除外,T3 可列大纲):

  • 参数说明(含义、取值范围、单位、是否可为 NULL)
  • 返回值说明(正常返回值、错误码、特殊返回值)
  • 错误码枚举值及其字面含义
  • 线程安全 / 可重入性标注

什么是"非 API 细节"?

以下信息 不属于 API 细节 ,可以且应该出现在 T3:

  • 为什么参数设计成这个类型(设计动机)
  • 为什么返回值用这种错误码风格(ADR)
  • 线程安全策略的取舍(例如:"为了性能选择无锁设计,调用者需自行同步")
  • 某个参数在特定场景下的陷阱(反模式)

示例

# 坏: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)`,或标记待停止由主循环处理。

规则 2:T3 聚焦"why",不聚焦"what"

"what" 与 "why" 的区别

类型 示例 归属
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 的作者在落笔前必须问自己:"读者在头文件里找不到这个信息吗?"如果找得到,就不要写。

规则 3:T3 在 doxygen @defgroup 块内用 @subpage / \includedoc 引用

为了让用户在阅读 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 内容。

规则 4:修改代码时,更新顺序必须是 T1 → T2 → T3

当 API 发生变更时,必须按以下顺序更新文档:

  1. T1 :先改头文件 doxygen 注释。这是物理距离最近的文档,失真风险最低。
  2. T2 :再改测试代码。编译器会强制校验测试代码是否与新的 API 签名一致。
  3. T3 :最后改独立 .md 文档。检查是否有"why"层面的内容需要更新(例如设计动机是否仍然成立、反模式是否新增)。

反例

# 错误顺序:先改 T3,再改 T1
1. 开发者觉得 .md 里的描述不对,先改了 .md
2. 然后改头文件
3. 结果 .md 里引用的函数名和头文件不一致(因为 .md 是基于旧代码写的)

规则 5:当 T3 与 T1 冲突时,以 T1 为准

如果读者发现 T3(.md 文档)中描述的 API 行为与 T1(头文件 doxygen 注释)不一致, 无条件信任 T1 。

原因:T1 随代码编译,最可信;T3 没有编译器校验,必然比 T1 更容易滞后。

T3 的作者在发现冲突时,应立即修正 T3,而不是质疑 T1。


第 3 章:文档阅读场景兼容

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

场景说明

场景 A:文档站(MkDocs)

MkDocs 是长期目标,用于提供美观的导航式文档站。目前暂不上线,但所有 .md 文档的目录结构必须兼容 MkDocs 的约定(例如 docs/ 为根目录、支持 mkdocs.yml 导航配置)。

场景 B:doxygen HTML(首版目标)

doxygen 是近期目标,用于从源码注释自动生成 API 参考。doxygen 配置已在仓库中,首版要求:

  • 所有公共头文件必须有 doxygen 注释(T1)。
  • doxygen 生成命令应能一键执行(例如 doxygen Doxyfile)。
  • 生成的 HTML 应包含指向 T3 文档的链接(通过 @subpage / \includedoc)。

场景 C:开发者直读源码

这是最常见的开发场景:开发者直接在 IDE 或编辑器中打开头文件和 .md 文件阅读。要求:

  • 头文件注释完整、格式统一(T1)。
  • 组件目录下有对应的 .md 文件(T3),文件名与组件名一致或明确对应。
  • 路径不要太深,尽量在 3 级以内。

场景 D:GitHub 在线阅读

GitHub 会自动渲染 README.md,因此以下约定必须遵守:

  • markdown 文件命名优先用 README.md :如果某个目录需要入口文档,优先命名为 README.md,这样 GitHub 进入该目录时会自动渲染。
  • 如果某个章节用 README.md 不合适(例如与已有 README.md 冲突、或该文件不是目录入口),就用普通 .md 名。
  • 兼容不了就算了,不要为了让 GitHub 渲染而破坏文档结构的语义。

第 4 章:文档维护方式分类

不同文档类型有不同的生命周期和变更规则。下表规定各类文档的维护方式:

文档类别 维护方式 触发原因
grill 问题清单 只能附加 时间线、防 agent 忘记
任务列表 单独维护 状态频繁变更(pending → in_progress → done)
决策记录 只能附加 决策不能撤回,追加新的决策
调研报告 单次写完 一次性产出
方案文档 单次写完 + 修订追加章节 终态,但可追加新版本章节
子代理报告 单次写完 一次性产出
校验报告 单次写完 一次性产出
会话笔记 / 工作过程文档 只能附加或小改 时间线,记录当时发生了什么
handoff 交接文档 单次写完(每次会话一份) 快照,供下一个会话接手

判断标准

在撰写或修改文档前,用以下标准判断维护方式:

  • 文档有明显时间线(对话推进) → 只能附加 。例如 grill 记录、会话笔记。旧内容不可修改,新内容追加在尾部。
  • 文档需要状态变更(任务 pending → done) → 单独维护 。例如任务列表、TODO 文件。允许修改状态字段,但不允许删除已完成的条目。
  • 文档是一次性快照(交接、调研) → 单次写完 。写完后原则上不再修改,如有新发现应另起新文档。

违规示例

```
# 坏:修改了 grill 历史记录
## 2024-01-01 的疑问
为什么定时器用链表而不是数组?
(此处原本有回答,被后续修改删掉了)

# 好:grill 只能追加
## 2024-01-01 的疑问
为什么定时器用链表而不是数组?
> 答:因为动态增删频繁,链表 O(1) 插入删除优于数组 O(n)。

## 2024-01-02 的新疑问
那为什么不直接用数组 + free list?
> 答:free list 需要额外维护位图,代码复杂度上升,当前场景不需要。
```

第 5 章:路径规范

5.1 用户对话中的路径

在与用户(或子代理之间)的对话中, 使用绝对路径 ,例如:

D:\work\pt\xt-sdk-family\xt-sdk\components\xt_task\xt_timer2.h

绝对路径消除歧义,避免"这个 components 目录是哪个 components"的困惑。

5.2 文档记录中的路径

在正式文档(包括 .md 文件、doxygen 注释)中, 使用相对路径 ,base 为仓库根目录 D:\work\pt\xt-sdk-family\xt-sdk:

```
头文件路径:`components/xt_task/xt_timer2.h`
测试代码路径:`tests/xt_timer2/xt_main.c`
```

相对路径让文档在克隆到不同目录后仍然可读。

5.3 正式文档中的路径过滤

正式文档(Doxyfile、README、教程)在引用文件时, 忽略 .gitignore 忽略的文件和目录 :

  • .vscode
  • .backup
  • .temp
  • .xmake
  • .build

例外 : .opencode 目录虽然也被 .gitignore 忽略,但其中的配置文件(例如技能定义)可以作为参考。引用时需注明"非权威参考"。


第 6 章:组件 .md 与 doxygen API 的去重策略

这是 xt-sdk 文档系统的核心痛点:组件目录下往往同时存在 .md 文档和头文件 doxygen 注释,内容大量重复,维护困难。

解决策略

策略 1:严格 T1/T2/T3 分层

  • T1(头文件注释)= API 细节。
  • T2(测试代码)= 完整示例 + 边界验证。
  • T3(组件 .md)= why + 反模式 + 跨组件关系。

三者内容不重叠。T3 的作者在动笔前必须阅读 T1,确保不重复。

策略 2:关键片段用 @snippet

如果 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 不会失真。

策略 3:T3 重写范围限定

如果现有组件 .md 已经存在大量 API 细节重复,重写时遵循以下范围:

  • 仅删除 API 签名重复部分 :删除函数原型列表、参数说明表格、返回值说明。
  • 其他章节保留 :设计动机、使用流程图、性能分析等非 API 细节内容保留。
  • 新增"反模式"章节 :用"反模式"替代原有的"常见误用"标题,语气更专业。

策略 4:T3 在 doxygen 中的引用

T3 文档必须在对应组件的 doxygen @defgroup 块中被引用,确保用户在阅读 doxygen HTML 时能发现 T3 的存在:

```
/**
 * @defgroup xt_task_timer2 Timer2 高精度定时器
 * @brief ...
 * 
 * 设计动机与反模式见 \subpage xt_timer2_design。
 */
```

第 7 章:实施检查清单

后续子代理在撰写 xt-sdk 的 .md 文档时,必须逐条对照以下检查清单:

内容定位检查

  • 本文档的内容是否属于 T3(设计动机、ADR、跨组件关系、性能特性、反模式)?
  • 是否重复了 T1 的 API 细节(签名、参数、返回值)?如是, 删除 。
  • 是否包含"why"(为什么这样设计)?如果通篇只有"what",需补充"why"或降级为 T1。

路径与引用检查

  • 是否用相对路径引用其他文件(base = 仓库根目录)?
  • 是否避免引用 .gitignore 忽略的文件( .vscode 、 .backup 、 .temp 、 .xmake 、 .build )?
  • 如引用 .opencode 内容,是否注明"非权威参考"?

维护方式检查

  • 如果是 grill 类文档 ,是否只追加不修改旧内容?
  • 如果是 任务列表 ,是否用单独维护方式(允许修改状态,但不删除条目)?
  • 如果是 决策记录 ,是否只追加新决策,不修改旧决策?
  • 如果是 一次性文档 (调研、交接、校验),是否写完后不再修改?

命名与格式检查

  • 文件命名是否优先使用 README.md(用于 GitHub 自动渲染)?
  • 是否使用 @page / @subpage / \includedoc 与 doxygen 集成?
  • 反模式标题是否使用"反模式"而不是"常见误用"?

最终验收标准

  • 本文档如果与 T1 冲突,以 T1 为准。是否已检查 T1 的最新版本?
  • 本文档是否可由下一个子代理或用户直接接手,无需额外上下文?

附录:快速查询表

你想写什么 应该写在哪 禁止写在哪
函数签名(大纲) 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(头文件)