Surface Serial Hub 协议

Surface Serial Hub (SSH) 是嵌入式 Surface 聚合器模块控制器(SAM 或 EC)的中央通信接口,可在较新的 Surface 代中找到。我们将把此协议和接口称为 SAM-over-SSH,而不是旧代的 SAM-over-HID。

在具有 SAM-over-SSH 的 Surface 设备上,SAM 通过 UART 连接到主机,并在 ACPI 中定义为 ID 为 MSHW0084 的设备。在这些设备上,通过 SAM 提供了重要的功能,包括访问电池和电源信息和事件、热读数和事件等等。对于 Surface 笔记本电脑,键盘输入通过 SAM 指向的 HID 处理,在 Surface Laptop 3 和 Surface Book 3 上,这也包括触摸板输入。

请注意,此子系统的标准免责声明也适用于本文档:所有这些都是经过逆向工程的,因此可能是错误的和/或不完整的。

以下使用的所有 CRC 均为双字节 crc_itu_t(0xffff, ...)。所有多字节值都是小端字节序,值之间没有隐式填充。

SSH 数据包协议:定义

SSH 协议的基本通信单元是一个帧 (struct ssh_frame)。一个帧由以下字段组成,这些字段打包在一起并按顺序排列

SSH 帧

字段

类型

描述

TYPE

u8

帧的类型标识符。

LEN

u16

与帧关联的有效负载的长度。

SEQ

u8

序列 ID(请参见下面的说明)。

每个帧结构之后都有一个针对此结构的 CRC。 帧结构(TYPELENSEQ 字段)的 CRC 直接放置在帧结构之后,有效负载之前。 有效负载之后是它自己的 CRC(覆盖所有有效负载字节)。 如果有效负载不存在(即,帧具有 LEN=0),则有效负载的 CRC 仍然存在,并将计算为 0xffffLEN 字段不包括任何 CRC,它等于帧的 CRC 和有效负载的 CRC 之间的字节数。

此外,使用以下固定的双字节序列

SSH 字节序列

名称

描述

SYN

[0xAA, 0x55]

同步字节。

消息由 SYN 组成,后跟帧(TYPELENSEQ 和 CRC),如果在帧中指定(即,LEN > 0),则后跟有效负载字节,最后,无论是否存在有效负载,后跟有效负载 CRC。 与交换相对应的消息部分通过具有相同的序列 ID (SEQ)(存储在帧内)来标识(有关此内容的更多信息,请参见下一部分)。 序列 ID 是一个环绕计数器。

帧可以具有以下类型 (enum ssh_frame_type)

SSH 帧类型

名称

简短描述

NAK

0x04

在先前接收的消息中发生错误时发送。

ACK

0x40

发送以确认接收到 DATA 帧。

DATA_SEQ

0x80

发送以传输数据。 已排序。

DATA_NSQ

0x00

DATA_SEQ 相同,但不需要确认。

NAKACK 类型的帧用于控制消息的流动,因此不携带有效负载。 另一方面,DATA_SEQDATA_NSQ 类型的帧必须携带有效负载。 不同帧类型的流动顺序和交互将在下一节中更详细地描述。

SSH 数据包协议:流动顺序

每个交换都以 SYN 开头,后跟 DATA_SEQDATA_NSQ 类型的帧,然后是其 CRC、有效负载和有效负载 CRC。 如果是 DATA_NSQ 类型的帧,则交换完成。 如果是 DATA_SEQ 类型的帧,则接收方必须通过响应包含 ACK 类型帧且序列 ID 与 DATA 帧相同的消息来确认接收到该帧。 换句话说,ACK 帧的序列 ID 指定要确认的 DATA 帧。 如果发生错误,例如 CRC 无效,则接收方将响应包含 NAK 类型帧的消息。 由于无法依赖先前数据帧(通过 NAK 帧指示错误)的序列 ID,因此不应使用 NAK 帧的序列 ID,并且将其设置为零。 在接收到 NAK 帧后,发送方应重新发送所有未完成(未确认)的消息。

序列 ID 在双方之间不同步,这意味着它们由每一方独立管理。 因此,识别与单个交换相对应的消息依赖于序列 ID 以及消息的类型和上下文。 具体来说,序列 ID 用于将 ACK 与其 DATA_SEQ 类型帧关联,但不将 DATA_SEQDATA_NSQ 类型帧与其他 DATA 类型帧关联。

一个示例交换可能如下所示

tx: -- SYN FRAME(D) CRC(F) PAYLOAD CRC(P) -----------------------------
rx: ------------------------------------- SYN FRAME(A) CRC(F) CRC(P) --

其中两个帧都具有相同的序列 ID (SEQ)。 在这里,FRAME(D) 指示 DATA_SEQ 类型帧,FRAME(A) 指示 ACK 类型帧,CRC(F) 是先前帧的 CRC,CRC(P) 是先前有效负载的 CRC。 如果发生错误,则交换将如下所示

tx: -- SYN FRAME(D) CRC(F) PAYLOAD CRC(P) -----------------------------
rx: ------------------------------------- SYN FRAME(N) CRC(F) CRC(P) --

发送方应重新发送该消息。 FRAME(N) 指示 NAK 类型帧。 请注意,NAK 类型帧的序列 ID 固定为零。 对于 DATA_NSQ 类型帧,两个交换是相同的

tx: -- SYN FRAME(DATA_NSQ) CRC(F) PAYLOAD CRC(P) ----------------------
rx: -------------------------------------------------------------------

在这里,可以检测到错误,但无法纠正或指示给发送方。 这些交换是对称的,即切换 rxtx 再次导致有效的交换。 目前,没有更长的交换是已知的。

命令:请求、响应和事件

命令作为数据帧内的有效负载发送。 目前,这是 DATA 帧的唯一已知有效负载类型,有效负载类型值为 0x80 (SSH_PLD_TYPE_CMD)。

命令类型有效负载 (struct ssh_command) 由一个八字节命令结构组成,后跟可选的可变长度命令数据。 此可选数据的长度从相应帧中给出的帧有效负载长度得出,即 frame.len - sizeof(struct ssh_command)。 命令结构包含以下字段,这些字段打包在一起并按顺序排列

SSH 命令

字段

类型

描述

TYPE

u8

有效负载的类型。 对于命令,始终为 0x80

TC

u8

目标类别。

TID

u8

命令/消息的目标 ID。

SID

u8

命令/消息的源 ID。

IID

u8

实例 ID。

RQID

u16

请求 ID。

CID

u8

命令 ID。

通常,命令结构和数据不包含任何故障检测机制(例如 CRC),这仅在帧级别完成。

主机使用命令类型有效负载将命令和请求发送到 EC,EC 也使用命令类型有效负载将响应和事件发送回主机。 我们区分请求(由主机发送)、响应(由 EC 发送以响应请求)和事件(由 EC 发送而没有先前的请求)。

命令和事件通过其目标类别 (TC) 和命令 ID (CID) 唯一标识。 目标类别指定命令的一般类别(例如,一般系统,与电池和交流电相比,与温度等相比),而命令 ID 指定该类别中的命令。 只有 TC + CID 的组合是唯一的。 此外,命令还有一个实例 ID (IID),用于区分不同的子设备。 例如,TC=3 CID=1 是请求获取热传感器上的温度,其中 IID 指定相应的传感器。 如果未使用实例 ID,则应将其设置为零。 如果使用了实例 ID,则通常从值 1 开始,而零可以用于实例无关的查询(如果适用)。 对请求的响应应具有与相应请求相同的目标类别、命令 ID 和实例 ID。

响应通过请求 ID (RQID) 字段与其相应的请求匹配。 这是一个类似于帧上的序列 ID 的 16 位环绕计数器。 请注意,请求-响应对的帧的序列 ID 不匹配。 只有请求 ID 必须匹配。 从帧协议方面来说,这是两个单独的交换,甚至可以分开,例如,在请求之后但在响应之前发送一个事件。 并非所有命令都会产生响应,并且这是无法通过 TC + CID 检测到的。 发布方有责任等待响应(或将此信号发送给通信框架,如 SAN/ACPI 中通过 SNC 标志所做的那样)。

事件通过唯一且保留的请求 ID 来标识。 发送新请求时,主机不应使用这些 ID。 它们在主机上用于首先检测事件,然后将它们与注册的事件处理程序匹配。 事件的请求 ID 由主机选择,并在设置和启用事件源时(通过启用事件源请求)定向到 EC。 然后,EC 将指定的请求 ID 用于从相应源发送的事件。 请注意,仍然应通过其目标类别、命令 ID 以及(如果适用)实例 ID 来识别事件,因为单个事件源可以发送多个不同的事件类型。 但是,通常,单个目标类别应映射到单个保留的事件请求 ID。

此外,请求、响应和事件都具有关联的目标 ID (TID) 和源 ID (SID)。 这两个字段指示消息的来源 (SID) 以及消息的预期目标 (TID)。 请注意,因此,与原始请求相比,对特定请求的响应具有交换的源 ID 和目标 ID(即,请求目标是响应源,请求源是响应目标)。 有关两者的可能值,请参见 (enum ssh_request_id) 。

请注意,即使请求和事件应该仅通过目标类别和命令 ID 唯一地可识别,但 EC 可能需要特定的目标 ID 和实例 ID 值才能接受命令。 例如,为 TID=1 接受的命令可能不为 TID=2 接受,反之亦然。 虽然这可能并不总是适用于现实,但您可以将不同的目标/源 ID 视为指示具有潜在不同功能集的不同物理 EC。

限制和观察

从理论上讲,该协议可以并行处理多达 U8_MAX 个帧,最多可处理 U16_MAX 个挂起的请求(忽略为事件保留的请求 ID)。 但是,实际上,这更加有限。 从我们的测试(尽管是通过 python 以及用户空间程序),EC 似乎可以在特定时间并行处理最多四个请求(主要是)可靠的请求。 并行执行五个或更多请求时,已观察到一致地丢弃命令(已确认的帧但没有命令响应)。 对于五个并发命令,这可重复地导致丢弃一个命令并处理四个命令。

但是,也已经注意到,即使并行执行三个请求,也会发生偶尔的帧丢弃。 除此之外,在三个挂起的请求的限制下,没有观察到丢弃的命令(即丢弃命令,但已确认携带命令的帧)。 在任何情况下,如果超过某个超时,主机应重新发送帧(并且可能还包括命令)。 EC 对超时为一秒的帧执行此操作,最多可以重试两次(即总共三次传输)。 重试的限制也适用于收到的 NAK,并且在最坏的情况下,可能导致丢弃整个消息。

虽然这对于挂起的数据帧也似乎可以正常工作,只要没有发生传输故障,但这些帧的实现和处理似乎依赖于只有 一个未确认的数据帧 的假设。 特别是,重复帧的检测依赖于最后一个序列号。 这意味着,如果再次发送 EC 已成功接收的帧,例如,由于主机未收到 ACK,则 EC 仅当它具有 EC 接收的最后一个帧的序列 ID 时才会检测到此情况。 例如:发送两个序列号分别为 SEQ=0SEQ=1 的帧,然后重复 SEQ=0 不会将第二个 SEQ=0 帧检测为这样的帧,因此每次收到它时都会执行此帧中的命令,即在本例中两次。 发送 SEQ=0SEQ=1,然后重复 SEQ=1 会将第二个 SEQ=1 检测为第一个 SEQ=1 的重复,并忽略它,因此仅执行一次包含的命令。

总之,这表明每个参与方最多有一个挂起的未确认的帧(实际上导致关于帧的同步通信),以及最多三个挂起的命令。 同步帧传输的限制似乎与在 Windows 上观察到的行为一致。