Surface 串行 Hub 协议

Surface 串行 Hub (SSH) 是较新 Surface 代中嵌入式 Surface 聚合器模块控制器(SAM 或 EC)的中央通信接口。我们将此协议和接口称为 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 相同,但不需要被 ACK。

NAKACK 类型帧都用于控制消息的流,因此不携带有效负载。DATA_SEQDATA_NSQ 类型帧则必须携带有效负载。下一节将更深入地描述不同帧类型的流序列和交互。

SSH 数据包协议:流序列

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

序列 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 帧唯一已知的负载类型,其负载类型值为 0x80SSH_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)进行唯一标识。目标类别指定命令的通用类别(例如,一般系统、电池和 AC、温度等等),而命令 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 似乎可以在特定时间并行可靠地处理最多四个请求。当并行处理五个或更多请求时,已经观察到一致地丢弃命令(ACKed 帧但没有命令响应)。对于五个并发命令,这可重复地导致一个命令被丢弃,而四个命令被处理。

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

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

总之,这表明最多只能有一个挂起的未 ACK 帧(每个发送方,实际上导致有关帧的同步通信)和最多三个挂起的命令。同步帧传输的限制似乎与 Windows 上观察到的行为一致。