EDS与PUS实践(三):从CCSDS空间包理解PUS服务类型17

  • PUS 不是另一种链路协议,而是建立在 CCSDS 空间包之上的应用层使用规则。先把协议分层、包头字段和几种 ID 拆开,再读 PUS 服务类型 17,很多看似相似的名词就不会混在一起。

前两篇从 EDS XML 一直走到 EdsLib packed bytes。本文暂时不写 XML,而是回答:这些字节应该遵守什么协议?主要依据是 CCSDS 133.0-B-2 Space Packet ProtocolECSS-E-ST-70-41C

本章要解决的问题

上一篇已经能把 native object 转成 packed bytes,但“能转”并不能说明这些字节符合空间通信协议。本章从线包外层向内拆解,并完成三件事:

  1. 按 bit 读懂固定 6 byte 的 CCSDS 主头;
  2. 区分 PUS TC/TM 次头中的公共字段与任务裁剪字段;
  3. 根据 ECSS 41C 的系统要求和接口要求,完整理解服务类型 17 的四种消息及失败路径。

这一章是 XML 建模前的协议设计。先把每个字段来自哪里、由谁决定列清楚,下一篇才能判断它应该放在公共类型、服务基类、具体消息还是业务代码中。

阅读标准时先分“系统要求”和“接口要求”

ECSS-E-ST-70-41C 中与服务类型 17 直接相关的内容分在两处:

  • 6.17 说明服务能力、有效请求、可测试进程列表、成功判据和失败阶段;
  • 8.17 说明消息的 Service Type、Message Subtype 和 application/source data 结构。

只看 8.17 会知道 TC[17,3] 带一个枚举,却不知道目标不在列表时必须拒绝并产生 failed start of execution notification。只看 6.17 又不知道线上消息子类型和字段顺序。实现必须同时满足两组要求。

CCSDS 133.0-B-2 则负责更外层的 Space Packet:主头位宽、Packet Type、APID、序列控制和 Packet Data Length。PUS 使用空间包,但不重新定义这 6 byte 主头。

先分层:CCSDS、PUS 和传输介质不是一回事

1
2
3
4
5
6
7
链路/传输介质(帧、串口、总线、UDP 测试通道……)
└─ CCSDS Space Packet
├─ 6-byte Primary Header
└─ Packet Data Field
├─ PUS Secondary Header
├─ Application Data / Source Data
└─ Packet Error Control(按任务裁剪)

CCSDS Space Packet Protocol 规定主头、标识和分段等通用机制;PUS 在空间包数据域中规定遥控请求、遥测报告、服务类型和执行语义;UDP 只是后续实验中搬运一个完整包的方式,不属于 PUS 服务类型 17 的协议定义。

本文在说明标准时统一使用“PUS 服务类型 17(测试服务)”。源码里的 PUS17st17.xmlTcSt17Base 是当前 EDS 模型的包名、文件名和类型名,只在引用实际代码时保留,不能把这些工程标识当成标准正式名称。

包、帧和数据报不是同义词

一个 CCSDS Space Packet 可以被封装到不同链路的数据单元中。开发测试用 UDP 时,常把“一整个空间包”放进一个 UDP payload;这只是便于保持消息边界。真实链路可能把空间包放入传输帧,也可能需要分段、复用和差错控制。

因此抓取 UDP 时看到的结构是:

1
2
Ethernet/IP/UDP headers | CCSDS primary header | PUS content | PEC
^ UDP payload starts here

IP 地址和 UDP 端口不属于 EDS 中的 PUS 消息字段。反过来,PUS PEC 也不由 UDP checksum 代替。每层只解释自己的范围。

为什么先从外层向内解析

接收方必须先知道数据至少够不够 6 byte 主头,再读取 Packet Data Length 判断整包边界;随后依据 Secondary Header Flag 和任务配置解释 PUS 次头;最后才按 Service/Message Subtype 解释应用数据。直接从某个固定偏移读取进程 ID,会在次头裁剪或消息类型变化后立即失效。

图片预留:绘制 CCSDS 主头、PUS 次头、业务数据和 PEC 的分层图,保存为 eds-pus-03-ccsds-pus-service-type-17/packet-layers.png

CCSDS 主头固定 6 字节

字段 位数 含义
Packet Version Number 3 空间包协议版本
Packet Type 1 0 为 TM/报告,1 为 TC/请求
Secondary Header Flag 1 数据域开头是否存在次头
APID 11 数据路径/应用过程标识
Sequence Flags 2 分段状态
Sequence Count/Name 14 序列计数或任务定义名称
Packet Data Length 16 数据域字节数减 1

Sequence Flags 的标准取值是:00 continuation、01 first、10 last、11 unsegmented。最小实验使用不分段包,所以通常为 11

6 byte 是怎样拼出来的

前 16 bit 由 Version、Packet Type、Secondary Header Flag 和 APID 组成;中间 16 bit 是 Sequence Flags 与 14 bit Sequence Count/Name;最后 16 bit 是 Packet Data Length。网络表示通常按高位在前阅读:

1
2
3
4
5
6
7
bit  0..2   Version
bit 3 Packet Type
bit 4 Secondary Header Flag
bit 5..15 APID
bit 16..17 Sequence Flags
bit 18..31 Sequence Count/Name
bit 32..47 Packet Data Length

这也是为什么不能把 C 位域结构直接当线包:C 位域布局和位序受实现影响,而标准规定的是明确的 bit 位置。EDS packed 编码用于跨编译器保持这些位置。

Packet Version Number 的“版本 1”容易误解

CCSDS 文本把当前结构称为 Version 1 Space Packet,但头中的三位 Packet Version Number 编码为二进制 000。文档版本名称与字段原始值不是同一个概念,程序应按标准字段编码,而不是看到“Version 1”就写整数 1。

Packet Data Length 为什么总是少 1

1
2
Packet Data Length = Packet Data Field 的字节数 - 1
= 整包字节数 - 7

因此 13 字节整包的字段值是 6,15 字节整包的字段值是 8;字段值为 0 表示数据域有 1 字节,而不是 0 字节。这是协议编码,不是 C 数组习惯。用 EDS 的 LengthEntry 自动计算时,也应拿手算结果核对一次。

从字段反推实收长度

接收端读取主头后,可按下面公式得到整包预期字节数:

1
2
3
Packet Data Field octets = Packet Data Length + 1
Total packet octets = 6 + Packet Data Length + 1
= Packet Data Length + 7

若 UDP 数据报长度与此不一致,应先报告长度错误,不要继续把缺失或多余字节交给服务解析。对于流式传输,这个字段还可帮助切分包,但仍需要处理半包和缓存;UDP 已经保留数据报边界,却仍需检查协议长度。

最小数据域仍至少有 1 byte

CCSDS 规定 Packet Data Field 至少 1 octet,所以 Length 的零值不是“空包”。PUS 消息即使没有 application data,仍有 PUS 次头,通常还带任务选择的 PEC,因此不会只有 6 byte 主头。

APID 能说明什么,不能说明什么

APID 位于每个 CCSDS 空间包主头中,用于标识一条 managed data path,任务中常用来区分产生或消费包的应用。它不是:

  • EdsLib 数据库中的类型 ID;
  • PUS 服务号;
  • TC[17,3] 请求正文里的 Application Process ID。

APID 的具体分配属于任务设计。看到十进制 321,只能说当前任务选择了这个 APID,不能推导它是“服务 17 的标准 APID”。

APID 与路由的关系

CCSDS 将 APID 放在 Packet Identification Field 中,用于标识 managed data path。实际系统可以据此选择接收应用、队列或处理器,但映射表属于任务配置。PUS Service Type 进一步说明该应用数据要执行哪类服务。

因此同一 APID 可以承载多个 PUS 服务消息;同一服务类型也可能出现在不同 APID 上。把 APID 固定等同于服务号,会失去这两个维度的独立性。

Idle APID 与任务 APID

空间包协议保留特定 APID 用于 Idle Packets,普通应用不应随意占用。具体任务的 APID 分配应有清单,并同时记录 TC/TM 方向、次头配置和序列策略。示例中的 321 只是普通演示值,不是推荐分配。

PUS-C TC 次头增加了什么

当前实验采用 PUS-C,也就是 PUS 版本字段值 2。TC 次头重点包含:

字段 作用
PUS Version 标识 PUS 版本
ACK Flags 请求哪些 PUS 服务类型 1(请求验证服务)成功确认
Service Type 服务类型,例如 17
Message Subtype 消息子类型,例如 1 或 3
Source ID 请求来源标识,宽度由任务声明

ACK 是 4 bit:bit3 acceptance、bit2 start of execution、bit1 progress、bit0 completion。它们请求的是成功确认;某些标准强制的失败报告不能简单等同于“ACK 位没置位就什么都不发”。这会在交叉验证文章中再次出现。

ACK 位描述的是请求验证,不是 ST17 响应开关

TC[17,1] 的 TM[17,2] 是测试服务自己的功能报告;服务类型 1 的确认则描述该 TC 在接收、开始、进展和完成阶段的验证状态。两类 TM 可能同时存在,目的不同。

例如清除 completion ACK,不表示服务类型 17 可以不返回标准要求的 TM[17,2]。同样,TC[17,3] 指向未登记进程时,标准要求 failed start of execution notification;不能因为 start ACK 位为 0 就静默丢弃请求。实现库的通用 ACK API 若只按成功确认位输出,服务失败路径就要另外满足强制报告要求。

Source ID 为什么不能凭空省略

PUS-C 允许若干任务声明项。当前剖面选择 16 bit Source ID,因此 TC 次头为 5 byte:第一个 byte 由 4 bit PUS Version 与 4 bit ACK 组成,之后是 Service Type、Message Subtype 和 2 byte Source ID。若另一实现配置为不同 Source ID 宽度,应用数据起始偏移也会改变。

双方做交叉验证前必须明确这一点,不能只说“都用 PUS-C”。

PUS-C TM 次头存在任务裁剪

TM 次头通常包含 PUS 版本、时间参考状态、Service/Message Subtype、Destination ID,并可包含消息类型计数器和时间字段。实际线上格式必须显式声明:

  • Destination ID 的位宽;
  • 是否存在 Message Type Counter;
  • 时间编码是 CUC、CDS 还是其他格式;
  • CUC 粗/细时间宽度;
  • 是否带时间前导字节;
  • PEC 使用哪种任务允许的算法。

所以“符合 PUS-C”不代表所有任务的 TM 都逐字节一样。两个实现做交叉测试前,必须先对齐这份任务剖面。

时间字段为什么经常成为第一个错位点

TM 常包含时间。CUC 可以选择不同粗时间和细时间宽度,还可能在线传输前导字段;CDS 又有另一种结构。若 C 端模型是 CUC 4+2 且无 preamble,Python 端默认却带 preamble,后面的 PEC 位置和整包长度都会变化。

测试应固定一个可重复时间值并记录编码参数。先确认长度和字段边界,再比较完整 hex。直接拿不同任务剖面的 TM golden vector 做逐字节比较,只会得到“全部错位”的噪声。

Message Type Counter 是否存在必须写明

某些实现或早期模型会在 TM 次头中加入 Message Type Counter,另一些任务裁剪为不存在。多或少 2 byte 并不一定表示某一方违反 PUS-C,但对同一条通信链路而言必须一致。文章和测试向量都应注明这一选择。

PUS 服务类型 17 到底提供什么

ECSS-E-ST-70-41C 第 6.17 节定义了 PUS 服务类型 17(Test service,测试服务)。一个实现至少提供一个测试子服务;标准化的两组消息是:

请求 响应 语义 应用/源数据
TC[17,1] TM[17,2] are-you-alive connection test 均省略
TC[17,3] TM[17,4] 对指定应用过程做 on-board connection test 都带同一个目标进程 ID

TC[17,1] 不带目标 APID,也不带 Application Process ID。它测试的是承载该测试子服务的服务端本身能否通过最基本的收发与处理链路响应。

“至少一个测试子服务”不等于四种消息全部无条件必选

标准要求每个测试服务至少包含一个测试子服务,并规定每个应用过程最多承载一个测试子服务提供者。are-you-alive 能力属于标准化测试子服务;on-board connection test 能力则需要在定义该子服务时声明。

因此实现声明支持 17.3/17.4 时,还必须声明可测试应用过程列表和每个过程的成功判据。只在 XML 中定义两个消息类型,尚未完成这项能力。

两组测试覆盖的连接范围不同

TC[17,1]/TM[17,2] 检查地面到承载测试子服务的应用过程之间的最小端到端连接。TC[17,3]/TM[17,4] 则由测试子服务所在应用过程去检查另一个星上应用过程。后者不是“带参数版本的 ping”,而是多了一层任务声明的进程映射和成功准则。

17.1/17.2 的严格语义

标准要求 TC[17,1] 只有一条“are you alive”指令且无参数;每个有效请求产生一个 TM[17,2] 报告。收到响应至少说明:

  • 请求上行链路可达;
  • 测试服务能接收和处理请求;
  • 报告下行链路可达;
  • 相关最小功能正在运行。

它不能证明所有应用都健康,也不能替代更深入的功能或性能测试。

有效请求为何只生成一份报告

41C 要求每条有效 are-you-alive 指令产生单个 notification,并由每个有效请求产生单个包含该 notification 的 report。最小实现中一条 TC[17,1] 对应一条 TM[17,2]。若同时启用服务类型 1 确认,还可能看到额外验证 TM,但不能把它们误计为多个 17.2。

“alive” 的结论必须谨慎描述

收到 17.2 说明承载测试子服务的应用过程仍执行最低限度功能,且请求上行和报告下行路径可用。它不证明所有内部线程、外设、载荷或其他应用过程正常,也不提供时延保证。测试结果的语义边界应写进报告。

17.3/17.4 为什么必须带目标进程 ID

TC[17,3] 请求测试某个已声明可测试的应用过程。标准规定该请求包含一个目标应用过程标识,TM[17,4] 成功报告返回同一标识。

这个字段在标准中是任务声明的枚举,标准没有统一规定必须是 8 bit、16 bit 或等同 APID。若任务选择 16 位大端枚举,那么 EDS、服务实现、puslib 策略和测试向量都必须采用同一选择。

“枚举”包含两层含义

线上编码需要一个整数位宽和字节序,任务语义还需要“数值对应哪个应用过程”的枚举表。例如 1=DemoProcess1 只是示例任务选择。未列出的数值可能在线格式上仍可解码为整数,但在服务行为上必须按不可测试目标处理。

另外,可测试列表通常比枚举全集更小或更动态。枚举定义“可能标识哪些进程”,运行时列表定义“当前测试子服务能测试哪些进程”。二者不能合并成一个判断。

为什么服务提供者自己不在列表里

标准明确指出,承载测试子服务的应用过程不包含在 on-board connection test 的可测试列表中。对它自身的最低限度连通性应使用 17.1/17.2;17.3/17.4 用于测试其他星上应用过程。实现登记表时应保留这个约束。

17.3 的失败不一定返回 TM[17,4]

标准要求先声明“可测试应用过程列表”和每个过程的成功判据;承载该测试子服务的应用过程本身不放入这张列表。处理逻辑至少分三种:

1
2
3
4
5
6
7
TC[17,3]
├─ 目标不在可测试列表
│ └─ 拒绝请求,产生 TM[1,4] 开始执行失败报告
├─ 目标存在,但连接测试判据失败
│ └─ 产生 TM[1,8] 完成执行失败报告
└─ 测试成功
└─ 产生 TM[17,4],返回相同目标进程 ID

因此看到“17.3 没有返回 17.4”不能立即判定实现坏了;要先检查是否收到 PUS 服务类型 1 的失败验证报告。PUS 服务类型 17 负责测试业务,PUS 服务类型 1 负责请求执行验证,两者会在失败路径上组合。

为什么一个是开始失败,一个是完成失败

目标不在可测试列表时,服务在开始实际测试前就能判定请求不能执行,所以属于 start of execution failure,对应 TM[1,4]。目标已登记、测试也真正开始,但成功判据未满足,则属于 completion failure,对应 TM[1,8]。

这一区分不是错误码命名偏好,而是执行阶段语义。测试用例应同时检查报告 subtype,不能只检查“返回了某个 ST01 失败包”。具体 failure code 数值通常由任务声明,ECSS 并未给所有任务统一分配 170、171 这类示例值。

成功路径为什么还要返回同一进程 ID

TM[17,4] 的 source data 含 Application Process ID,使接收方能把响应和被测目标对应起来。服务实现应从有效请求中取得 ID,并在成功报告中原样返回;测试不只检查“收到 17.4”,还应检查值一致。

四种容易混淆的 ID

名称 位于哪里 由谁定义 用途
EdsLib_Id_t 本机数据库 生成工具/数据库 找到一种 EDS 类型
CCSDS APID 主头 任务分配 标识 managed data path
PUS Service/Message Subtype PUS 次头 ECSS 标准 标识服务和消息语义
Application Process ID 17.3/17.4 正文 任务声明 指定被测试应用过程

它们可能都以整数形式出现,但绝不能互相代替。尤其不要因为字段都叫“应用标识”,就把主头 APID 自动复制到 17.3 正文。

用“谁读取它”来快速区分

  • EdsLib_Id_t 由本机 EdsLib 查询数据库时读取,不上网;
  • APID 由空间包路由与管理逻辑读取,位于前 2 byte;
  • Service/Message Subtype 由 PUS 分发器读取,位于 PUS 次头;
  • Application Process ID 由服务类型 17 的业务处理器读取,位于 17.3/17.4 正文。

若某个整数不知道属于哪类,先看它在 packet 中的偏移和读取模块,而不是看变量名是否带 id

PEC 与 CRC-16-CCITT

PEC(Packet Error Control)是包尾的错误控制字段。当前 EDS 剖面选择 CRC-16-CCITT:生成多项式为 x^16 + x^12 + x^5 + 1,初始寄存器全 1,计算范围覆盖除 PEC 自身外的整个包,包括主头。

需要区分两件事:PUS/任务剖面决定是否使用 PEC 以及选用什么算法;EDS XML 把这个决定写成 ErrorControlEntry,EdsLib 完整打包/解包负责计算和检查。CRC 能发现许多随机错误,但不提供身份认证或防恶意篡改能力。

CRC 的名字不足以唯一确定算法

工程中“CRC-16-CCITT”有时被用来指不同初值、反射方式或最终异或组合。两端不仅要同意生成多项式,还应确认初始值、输入/输出反射、最终异或和覆盖范围。当前 EDS CRC16_CCITT 路径使用的具体行为应以 EdsLib 源码、标准附录和已知向量共同核对。

PEC 在包中的位置和覆盖范围

当前模型把 2 byte PEC 放在具体消息末尾,计算输入覆盖 PEC 之前的整个包,包括 CCSDS 主头、PUS 次头和应用数据。接收端对任意受保护 bit 的改变都应重新计算并比较;截断 PEC 则首先造成长度不符。

CRC 通过只说明受保护内容与校验值一致。攻击者若能同时修改正文和重新计算 CRC,仍可构造通过校验的包,因此安全认证要由更高层机制提供。

EDS 能表达协议结构,但不能代替服务行为

EDS XML 很适合表达:

  • 主头和次头字段;
  • TC/TM 方向;
  • Service/Message Subtype 固定值;
  • 17.3/17.4 的进程标识;
  • 长度和 PEC;
  • 请求与报告的接口关系。

它不能凭空完成:维护可测试进程列表、调用目标进程测试回调、判断成功条件、决定失败报告、选择传输通道。

1
EDS 定义“包长什么样”;服务代码定义“收到包后做什么”。

把标准要求分配到正确实现位置

标准内容 推荐实现位置
Service Type=17、Subtype=1/2/3/4 EDS ValueConstraint
17.1/17.2 无应用/源数据 具体 EDS 消息不增加 EntryList
17.3/17.4 的进程枚举 ST17 EDS 类型与任务枚举表
可测试进程列表 服务运行时配置/注册表
每个进程的成功判据 回调或服务策略
未登记目标 → 开始执行失败 服务状态机 + ST01 报告生成
测试失败 → 完成执行失败 服务状态机 + ST01 报告生成
UDP 端口 测试传输配置

这张表是下一篇建模的输入。凡是随运行状态变化的判断,都不应伪装成静态 XML 约束。

阅读一条十六进制 TC 的方法

以当前任务的一条 TC[17,1] 为例:

1
2
19 41  c0 01  00 06  20 11 01 00 2a  1b 4c
└主头第1字┘ └序列┘ └长度┘ └──PUS次头──┘ └PEC┘
  • 0x1941:版本 0、TC、存在次头、APID 0x141(321);
  • 0xc001:不分段、序列计数 1;
  • 0x0006:整包 13 字节,因此 13 - 7 = 6
  • 0x20:PUS 版本 2,ACK flags 为 0;
  • 0x11 0x01:Service Type 17、Message Subtype 1;
  • 0x002a:Source ID 42;
  • 0x1b4c:PEC。

这类逐字段解释比只保存“期望 hex”更有价值:任务参数变化后,可以知道哪些字节应该变化,哪些协议字段必须保持不变。

用同样方法拆 TC[17,3]

在相同公共字段下,TC[17,3] 比 TC[17,1] 多 2 byte Application Process ID:

1
2
19 41 c0 01 00 08 20 11 03 00 2a 00 01 9c 72
|---- primary ----| |--- TC secondary ---| |ID| |PEC|

变化不只有 subtype 01→03 和新增 00 01:整包从 13 byte 变为 15 byte,所以 Packet Data Length 从 6 变为 8;PEC 也必须重新计算。若只在 17.1 向量中插入两个字节而不更新长度和 PEC,这不是合法 17.3 包。

十六进制日志应配套字段表

建议日志同时打印 offset、原始 byte 和解释值。纯 hex 适合逐字节比较,字段表适合发现位宽和偏移错误。两者结合才能在“第 11 字节不同”时知道它是 Source ID、进程 ID 还是 PEC。

一份可复用的协议检查表

实现任意 PUS 消息时,至少确认:

  1. 主头 Packet Type 与 TC/TM 方向一致;
  2. Secondary Header Flag 已设置;
  3. APID 来自任务分配,不是服务号;
  4. Sequence Flags 与分段策略一致;
  5. Packet Data Length 使用“数据域字节数减一”;
  6. PUS 版本和任务剖面一致;
  7. Service/Message Subtype 是标准要求的组合;
  8. 可选字段、宽度和时间格式已明确;
  9. 正文严格遵守该消息子类型的数据结构;
  10. PEC 算法和覆盖范围一致;
  11. 正常与失败响应都符合服务语义。

从协议要求推导下一篇 XML

完成本章后,XML 结构已经可以从需求自然推出:

  1. CCSDS 主头独立成公共类型,因为所有服务复用;
  2. PUS TC/TM 次头独立成公共类型,并明确任务裁剪;
  3. ST17 建立 TC/TM 抽象基类,将 Service Type 固定为 17;
  4. 四个具体类型分别固定 subtype;
  5. 17.1/17.2 不增加服务数据;
  6. 17.3/17.4 引用同一个 Application Process ID 枚举;
  7. 每个具体消息末尾声明 PEC;
  8. Interface 表达两组 request/report 关系;
  9. 可测试列表、回调和失败分支保留在运行时服务中。

这个推导顺序比先写一个大 XML 再反复删字段更可靠,因为每个节点都能回到标准条款或任务选择。

本章最容易带走的三个误解

第一,PUS 不等于完整星地链路。它建立在 CCSDS Space Packet 之上,又由更低层传输承载。第二,服务类型 17 不只是四个 subtype 编号,还包含有效性和失败阶段要求。第三,任务剖面不是随意偏离标准,而是在标准允许或要求声明的范围内把可选项具体化。

小结

读 PUS 报文时,应从外向内看:先用 CCSDS 主头判断方向、APID、序列与长度,再用 PUS 次头判断服务语义,最后按消息子类型解释正文和 PEC。PUS 服务类型 17 很小,但已经完整展示了“公共协议字段、任务裁剪字段、业务数据和运行时行为”四个层次。

下一篇将把这些规则落进 EDS XML:公共 CCSDS/PUS 类型怎样复用,17.1/17.2 为什么没有正文,17.3/17.4 的进程 ID 应该放在哪里,以及接口声明和运行时服务实现之间是什么关系。

参考资料

EDS与PUS实践(三):从CCSDS空间包理解PUS服务类型17

https://goko-son626.github.io/post/eds-pus-03-ccsds-pus-service-type-17.html

作者

GoKo Mell

发布于

2026-08-09

更新于

2026-09-20

许可协议

评论

:D 一言句子获取中...