EDS与PUS实践(一):从EDS XML到C代码与运行时数据库
- 最开始把 EDS 理解成“用 XML 生成结构体”。真正顺着源码跑完一次才发现,结构体只是给 C 程序看的外壳,保存类型、位宽、约束和编解码规则的数据库,才是 EdsLib 能工作的核心。
这是 EDS/PUS 实践系列的第一篇。本文只解决一个问题:一份 EDS XML 怎样变成应用能够使用的 C 类型和运行时数据库。下一篇再基于这些产物讨论 EdsLib 的打包、解包与校验接口。
本章目标、前情和边界
第 0 篇给出的路线是“标准与任务剖面 → EDS 模型 → 生成物 → 运行时 → 服务行为”。本章停在“生成物”这一站,不讨论 TC[17,1] 的服务语义,也不急着调用 Pack/Unpack。需要先弄清楚:
- XML 中的类型、成员、继承和约束怎样被解析;
- 为什么生成结果既有 C 头文件,又有数据库源码;
- DataTypeDB、DisplayDB 和 IntfDB 分别解决什么问题;
- 哪些工具只在构建主机运行,哪些代码需要进入目标系统;
- 怎样从 resolved XML 和生成代码反查模型是否按预期落地。
读完本章,应该能够面对一个陌生 EDS 工程,从 CMake 入口追到 XML 输入、生成目录和最终链接库,并能解释“生成成功”与“模型正确”之间还差哪些验证。
先从一个真实问题出发
假设要定义 TC[17,3]。它至少复用了四层信息:基础无符号整数、CCSDS 主头、PUS TC 次头、服务类型 17 的约束,最后才增加 Application Process ID。若只生成一个平铺结构体,工具还必须在别处记住:
PacketType固定为 TC;ServiceType固定为 17;SubserviceType固定为 3;- Application Process ID 在线上是任务声明的枚举;
- Packet Data Length 依赖最终 packed 长度;
- PEC 依赖此前所有已编码字节。
这说明生成过程的目标不只是让编译器认识成员名,而是把一整套数据契约转化为可执行元数据。C 头文件解决静态类型问题,DataTypeDB 解决运行时解释问题,二者缺一不可。
EDS 解决的不是“少写几个结构体”
EDS(Electronic Data Sheet)可以理解为一份机器可处理的数据契约。它描述的不只是字段名称,还可以包含:
- 整数或浮点类型的位宽、符号和字节序;
- 容器成员、数组、枚举和字符串;
- 类型继承与派生类型约束;
- 长度、校验和等特殊字段;
- 组件、接口、命令与参数之间的关系。
手写 C 结构体最多描述本机内存视角,不能天然表达“线上第 3 位开始的 5 bit 字段”“大端 16 bit 整数”“此派生消息的服务号必须为 17”或“包尾是覆盖前面全部字节的 CRC”。EDS 的意义是让这些规则先进入模型,再由工具和运行库共同执行。
native layout 与 packed layout 从建模阶段就不同
以下两个尺寸经常同时出现:
- native size:目标编译器眼中的本机对象尺寸,受对齐、填充和 ABI 影响;
- packed size:按 EDS 编码规则形成的位流尺寸,受字段位宽和编码顺序影响。
一个包含 1 bit、1 bit、11 bit 和 16 bit 字段的协议头,在 packed 视角可以紧密排列;在 C 语言视角,生成器可能选择便于访问的标量或嵌套结构,并产生填充。EDS 工具链必须同时维护两种视角,运行时才能在两者之间转换。
因此生成头文件后看到 sizeof(Type),不能据此判断线包长度。生成数据库中的尺寸信息才分别描述运行时需要的 native/packed 视图,具体字段应结合 EdsLib_DataTypeDB_TypeInfo_t 定义核对。
先把完整链路画出来
1 | |
建议把图分成两块:左边是“主机生成阶段”,右边是“目标运行阶段”。后续接触交叉编译时,很多疑问都能从这条边界得到答案。
一次配置和一次构建分别发生什么
CMake 项目通常把工作拆成两个时间点:
- 配置阶段:寻找 EdsTool、收集 EDS 源文件、建立目标和依赖关系;
- 构建阶段:在 XML 或生成脚本变化时执行 sedstool,随后编译新生成的 C 源码。
因此不要把“运行 CMake”简单理解为“编译 C 文件”。配置命令可能只生成构建规则,真正的 XML 解析发生在 cmake --build 触发的自定义命令中。排查时应分别看配置日志、sedstool 日志和编译器日志。
一个干净构建至少留下三组可追踪输入输出:
1 | |
若项目把生成目录藏在临时路径中,又没有保存日志,模型问题会被最终的 C 编译错误掩盖。实践中最好能定位导出的 resolved XML 和生成源文件,而不是只看终端最后十行。
图片预留:绘制“XML → DOM → Lua → C/H 与数据库 → 应用”的流程图,保存为
eds-pus-01-xml-to-c-runtime-database/eds-flow.png。
sedstool 实际处理了什么
NASA EdsLib 中,tool/ 提供 sedstool,edslib/eds/ 放 EdsLib 相关生成插件,edslib/fsw/ 则是 C 运行库。生成并不是把 XML 标签机械翻译成 C,而是先建立一份已经“解析完成”的模型。
源码中的处理脚本按编号执行,主要阶段包括:
| 阶段 | 主要工作 | 为什么不能跳过 |
|---|---|---|
05-seds_parse_defines.lua |
处理定义和预处理信息 | 后续脚本需要统一的定义值 |
10-seds_resolve_refs.lua |
解析跨文件类型引用 | 字符串形式的引用要变成实际节点 |
15-seds_create_implicit_scalars.lua |
补充隐式标量类型 | 统一后续尺寸与编码计算 |
20-seds_resolve_sizes.lua |
计算 native/packed 尺寸 | 生成结构和位流布局都依赖它 |
25-seds_resolve_constraints.lua |
解析派生类型约束 | 决定固定字段和类型识别规则 |
30-seds_resolve_components.lua |
解析组件和接口 | 为接口数据库建立完整关系 |
85-seds_write_resolved_xml.lua |
导出已解析模型 | 便于复查最终语义 |
脚本前的数字不是命名习惯,而是依赖顺序。例如尺寸计算前必须知道成员引用的是哪个类型;生成派生类型前必须知道继承链和约束。因此排查问题时,不能只盯最终 C 编译错误,还要判断错误发生在 XML 解析、模型解析还是代码生成阶段。
XML 语法通过以后仍然可能错
XML parser 只确认标签闭合、属性形式和 Schema 约束。下面几类问题可能直到后续阶段才暴露:
- 类型名存在,但引用到了另一个包中同名类型;
- 基类尺寸尚未确定,派生容器无法计算最终布局;
- 两个派生类型使用相同约束,运行时无法唯一识别;
- LengthEntry 的换算方向写反,生成正常但线包长度错误;
- 任务裁剪字段在两个 XML 中重复定义,形成两套不一致公共头;
- Interface 引用了正确类型名,却没有体现实际请求与报告关系。
resolved XML 很重要,因为它位于“原始输入”和“代码生成”之间:引用已经解析,隐式节点已经补齐,尺寸和约束已经计算。若 resolved XML 就错,修改 C 代码不会解决根因;若 resolved XML 正确而生成 C 错,再去检查生成插件。
生成插件并不是编译器
sedstool 建立通用 SEDS 模型,具体输出由插件决定。EdsLib 的插件关注 C 类型和运行时数据库,其他插件完全可以从同一模型生成文档、接口表或其他语言绑定。因此 SEDS XML 是上游模型,EdsLib 生成物是这一模型的一种实现投影。
理解这点后,就不会把某个生成头文件的命名细节误认为 SEDS 标准要求。标准规定模型语义;文件名、宏名和数组组织属于工具实现。
一个最小类型里已经包含哪些信息
1 | |
短短几行已经明确:逻辑值是无符号整数、线上占 16 bit、线上按大端编码。如果主机是小端,C 对象里的 0x1234 可能在内存中表现为 34 12,但打包结果必须是 12 34。native object 和 packed object 从一开始就是两种表示,不能把结构体内存直接当线包发送。
基础类型应该稳定、集中、可复用
若每个服务各自定义 uint16,很容易出现一个服务声明大端、另一个忘记写字节序的情况。更合理的做法是让基础类型成为公共依赖:
1 | |
这条依赖链表示“复用数据契约”,不是 C 头文件的随意 include。修改底层类型会影响所有上层消息,所以基础 XML 应像公共接口一样谨慎评审;服务专属枚举则应留在服务包中,避免污染共享层。
容器成员顺序就是线上顺序的一部分
在 ContainerDataType 的 EntryList 中,成员顺序通常决定 packed bitstream 的排列。把 ServiceType 与 SubserviceType 对调,即使两者都是 8 bit、C 编译也完全通过,线上语义仍然错误。模型评审不能只检查“字段有没有”,还要检查顺序、宽度、编码和约束。
容器、继承和约束比结构体更重要
消息通常由容器类型描述。公共包类型负责布局,派生消息通过约束固定部分字段:
1 | |
这不是简单的 C 继承语法糖。运行时数据库会保存派生关系和约束,EdsLib 因而能够:
- 初始化对象时填写固定字段;
- 打包时选择或确认具体派生类型;
- 解包时检查收到的字段是否满足类型约束;
- 通过基类与派生类关系复用公共包头。
如果业务代码每次手工写 ServiceType = 17,XML 和代码就有两份真相;使用约束后,服务号属于类型本身,应用只填写真正的任务数据。
抽象基类为什么有用
服务类型 17 的四种消息都共享 ServiceType=17,TC 又共享 TC 方向和 TC 次头,TM 共享 TM 方向和 TM 次头。可以建立抽象 TcSt17Base 和 TmSt17Base:它们用于复用约束,却不代表可以直接在线上发送一种“没有消息子类型的 ST17 基类”。
具体类型继续约束 subtype 1、2、3、4。这样运行库从公共基类识别派生类型时,能够沿着约束链逐步收窄。若直接把所有消息平铺复制,不仅重复字段,派生类型识别和公共工具也失去结构信息。
固定值、长度项和错误控制项不是普通成员
三类特殊数据需要在完整对象已知后处理:
- 固定值用于保证具体类型的判别条件;
- LengthEntry 依赖最终编码长度;
- ErrorControlEntry 的 CRC 依赖此前所有受保护字节。
EdsLib 的 Complete Pack 会在普通字段编码完成后处理这些特殊项,Complete Unpack 则在解码后验证它们。若把 PEC 建模为普通 uint16,运行库只会把调用者给出的值编码进去,无法自动证明它等于正确 CRC。
生成物分别负责什么
| 生成物 | 内容 | 典型使用者 |
|---|---|---|
*_eds_typedefs.h、*_eds_datatypes.h |
C 类型、容器结构、packed buffer 声明 | C 业务代码、编译器 |
*_eds_defines.h |
包索引、类型索引、对象 ID 等宏 | 调用 EdsLib 的代码 |
*_eds_datatypedb_impl.c |
类型、成员、尺寸、偏移、继承、约束和编码信息 | DataTypeDB 运行时 |
*_eds_displaydb_impl.c |
类型/成员名称、枚举显示信息 | 命令行工具、调试器 |
*_eds_intfdb_impl.c |
组件、接口、命令、参数映射 | 通用接口查询代码 |
其中 DataTypeDB 是编解码主线。只有头文件而没有数据库,应用虽然能声明结构体,却无法让 EdsLib 按 EDS 语义打包。DisplayDB 和 IntfDB 是否进入目标系统,则可以按资源和功能需要裁剪。
如何从生成目录读懂一个类型
面对 PUS17/Tc17_3,可以按以下顺序追踪:
- 在 datatypes/typedefs 头中找到对应 native C 类型,确认成员嵌套;
- 在 defines 头中找到包索引、类型索引或生成的 ID 宏;
- 在 DataTypeDB 实现中查看 packed bit 数、native byte 数、成员条目和派生关系;
- 在 DisplayDB 中确认完整类型名与字段名;
- 在 IntfDB 中确认它是否作为某个 Command 的输入参数;
- 回到 resolved XML,确认这些信息源自哪条类型和约束。
这样读生成物不是为了手工修改它,而是把“XML 中的抽象声明”与“运行时真正读取的表项”对应起来。一旦发现不一致,回到上游 XML 或生成插件修正。
生成文件为什么不适合版本化手改
生成文件可能很大,也可能因工具版本改变排序或格式。手工补一行虽然能让当前编译通过,但下次干净构建会覆盖;更严重的是,源 XML 仍然没有表达这项语义,其他语言或数据库输出仍旧错误。
若确实需要兼容某个编译器,优先在生成配置、模板或独立兼容层解决,并用干净构建证明可复现。不要把“生成目录里现在看起来正确”当成模型已经修复。
三个数据库不要混在一起
- DataTypeDB 回答“这个类型怎样存、怎样编码、有哪些约束”;
- DisplayDB 回答“这个类型或成员叫什么、枚举值怎样显示”;
- IntfDB 回答“哪个组件暴露了哪个接口、命令参数是什么类型”。
固定类型的嵌入式业务代码往往只需要生成头文件和 DataTypeDB;一个能按字符串输入字段、遍历打印任意消息的通用工具会依赖 DisplayDB;若要按接口名和命令名动态发现,才需要 IntfDB。把职责拆开后,链接哪些生成库就不再是“全都要”。
用三个问题判断依赖哪一个数据库
- 程序是否只对编译期已知类型做 Pack/Unpack?如果是,DataTypeDB 是主线;
- 程序是否要接收字符串类型名、字段路径或输出枚举标签?如果是,需要 DisplayDB;
- 程序是否要从组件和 Command 动态找到输入/输出类型?如果是,才需要 IntfDB。
IntfDB 能告诉程序 AreYouAlive 的 request/report 类型,却不会自动监听端口或调用服务函数。DisplayDB 能按名字定位 PriHdr.AppId,却不会替程序决定 APID 的任务分配。数据库提供的是元数据,不是业务策略。
构建主机与目标系统的边界
1 | |
嵌入式设备不需要在运行时解析 XML,也不需要携带 Lua 或 Python。常见流程是:
- 在构建主机上运行 sedstool;
- 得到平台无关的生成 C/H 与数据库源码;
- 用目标工具链重新编译生成物和 EdsLib Runtime;
- 目标应用链接这些库。
生成的 native C 类型仍受目标 ABI 影响,所以不能把主机编译出来的 .o 直接搬到另一种架构;应该搬生成源码,再由目标编译器编译。packed 线上格式则由 EDS 控制,应当跨架构保持一致。
哪些产物应该进入 SDK
面向外部应用的最小 SDK 通常需要:
- 应用会 include 的生成头文件;
- EdsLib Runtime 的公开头文件;
- 编译后的 EdsLib Runtime 库;
- 编译后的 EDS DataTypeDB 库;
- 必要时再加入 DisplayDB/IntfDB 库。
XML、sedstool 和 Lua 插件可以留在源码构建环境中。这样使用方只面对稳定的 include/lib 边界,不需要了解生成工程内部目录。若使用方也要增加或修改消息模型,则需要完整生成工具链,那是“开发 SDK”而不是“目标运行 SDK”。
交叉编译真正验证什么
交叉编译不是简单换一个 CC。它验证运行时代码和生成数据库是否依赖主机特有头文件、ABI 或工具。一个合理流程是先在主机生成 C/H,再让目标工具链只编译 Runtime、数据库和应用。若目标构建仍尝试执行目标架构的 sedstool,说明 host tool 与 target artifact 没有正确分离。
怎样判断一次生成是否真的正确
“构建成功”只说明工具链没有在当前输入上报错。更可靠的检查应逐层进行:
- 引用层:resolved XML 中跨文件引用是否指向预期类型;
- 尺寸层:native 字节数和 packed bit 数是否符合手算;
- 约束层:派生消息是否包含正确固定值;
- 生成层:头文件和各数据库是否出现预期类型;
- 运行层:初始化、打包和解包能否完成;
- 负向层:篡改固定字段、长度或 CRC 后能否被拒绝。
前四项是在检查模型,后两项是在检查模型有没有被运行库正确执行。两组都通过,才比“看见生成了 struct”更有说服力。
一个可执行的生成审查清单
以 TC[17,3] 为例,可以把审查结果记录为:
| 检查项 | 预期 |
|---|---|
| 完整类型名 | PUS17/Tc17_3 |
| 基类链 | PUS TC 公共包 → ST17 TC 基类 → TC[17,3] |
| Packet Type | TC 固定约束 |
| Service/Message Subtype | 17/3 固定约束 |
| Application Process ID | 任务枚举,示例为 16 bit 大端 |
| PEC | 具体消息尾部的 CRC-16-CCITT ErrorControlEntry |
| packed size | 比 TC[17,1] 多 16 bit |
| Interface | OnBoardConnectionTest 的输入类型 |
这张表把标准、任务选择、XML 和生成结果连接起来。任何一行没有证据,都不应该只凭“构建成功”打勾。
干净构建比增量构建更有说服力
生成系统很容易受旧文件影响。最终验证至少做一次全新构建目录:重新配置、重新生成、重新编译,再检查输出。若删除 XML 中某个类型后,旧生成文件仍留在目录里,增量构建可能制造假成功。干净构建能证明依赖关系与生成规则本身完整。
常见误区与定位方法
手工修改生成文件
生成文件下一次构建就会被覆盖,而且 XML 仍然是错的。应该修改 XML 或生成规则,再重新生成。
把 native sizeof 当作线包长度
C 结构体可能有对齐填充;位字段也未必逐字节对应。线包长度应读取 DataTypeDB 中的 packed bit size,再向上取整为字节。
所有错误都归因于 XML Schema
XML 能通过语法/Schema 检查,仍可能存在未解析引用、矛盾约束或尺寸不符合协议的问题。resolved XML 和生成数据库是更靠近最终语义的检查点。
认为目标系统也必须带生成工具
生成工具运行在主机;目标只需要 Runtime 和生成数据库。除非目标要动态加载新模型,否则没有必要让设备解析 XML。
把工具生成的名字当成稳定协议标识
宏名、包索引和类型索引可能因生成输入顺序或工具版本改变。线上互操作依赖的是字段与编码,不依赖某个 C 宏恰好等于多少。固定业务代码可以使用生成宏,但不要把这个数写进通信协议。
只看最终头文件,不看数据库
两个模型可能生成相似 C 结构,却具有不同 packed 编码或约束。只审查结构体会漏掉大小端、bit 宽度、固定值和 ErrorControl。至少同时检查 TypeInfo、DataTypeDB 表项和实际 packed bytes。
从源码定位问题的推荐顺序
遇到错误时按层收窄:
1 | |
每次只跨一层检查,比从“终端报错”直接回去重写 XML 更高效。
推荐的源码阅读顺序
- 选一个固定长度、字段很少的 XML 类型;
- 对照生成的
*_datatypes.h看 native 对象; - 对照
*_datatypedb_impl.c看成员与 packed 布局; - 查
edslib_datatypedb.h的完整打包/解包接口; - 打印一次 packed bytes;
- 回头看 Lua 尺寸、约束解析脚本。
这样每个抽象概念都有实际产物可以对应,不容易陷入只记标签名的状态。
小结
EDS 的核心不是“XML 生成 C”,而是用一份模型同时定义本机对象、线上位流和运行时元数据。C 类型让应用能写字段,DataTypeDB 让 EdsLib 知道如何编码与校验,DisplayDB/IntfDB 则为通用工具和接口查询提供额外能力。
下一篇将接着这条链路,具体说明 EdsLib_Id_t、InitializeNativeObject()、PackCompleteObject() 和 UnpackCompleteObject() 怎样组成最小闭环,以及最容易写错的 bit/byte 单位。
本章最重要的结论可以压缩为:XML 是源模型,resolved XML 是解析后的证据,生成头文件提供静态 C 视图,DataTypeDB 提供运行时语义,编译后的库才是目标程序依赖。 把这五者分开,才能正确处理主机生成、目标编译、运行时编解码和问题定位。
参考资料
EDS与PUS实践(一):从EDS XML到C代码与运行时数据库
https://goko-son626.github.io/post/eds-pus-01-xml-to-c-runtime-database.html

