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
)。一个帧由以下字段组成,它们打包在一起并按顺序排列
字段 |
类型 |
描述 |
---|---|---|
|
|
帧的类型标识符。 |
|
|
与该帧关联的有效负载的长度。 |
|
|
序列 ID(请参见下文解释)。 |
每个帧结构后都紧跟一个该结构的 CRC。帧结构(TYPE
、LEN
和 SEQ
字段)的 CRC 紧随帧结构之后,在有效负载之前。有效负载后紧跟其自身的 CRC(对所有有效负载字节进行计算)。如果不存在有效负载(即,帧的 LEN=0
),则有效负载的 CRC 仍然存在,并且计算结果为 0xffff
。LEN
字段不包括任何 CRC,它等于帧的 CRC 和有效负载的 CRC 之间的字节数。
此外,还使用以下固定的双字节序列
名称 |
值 |
描述 |
---|---|---|
|
|
同步字节。 |
一个消息由 SYN
组成,后跟帧(TYPE
、LEN
、SEQ
和 CRC),如果在帧中指定(即 LEN > 0
),则后跟有效负载字节,最后,无论是否存在有效负载,都后跟有效负载 CRC。部分地,通过在帧内存储相同的序列 ID (SEQ
) 来标识与交换对应的消息(下一节将对此进行详细介绍)。序列 ID 是一个循环计数器。
一个帧可以具有以下类型 (enum ssh_frame_type
)
名称 |
值 |
简短描述 |
---|---|---|
|
|
在先前接收的消息中发生错误时发送。 |
|
|
发送以确认接收到 |
|
|
发送以传输数据。已排序。 |
|
|
与 |
NAK
和 ACK
类型帧都用于控制消息的流,因此不携带有效负载。DATA_SEQ
和 DATA_NSQ
类型帧则必须携带有效负载。下一节将更深入地描述不同帧类型的流序列和交互。
SSH 数据包协议:流序列¶
每个交换都以 SYN
开头,后跟 DATA_SEQ
或 DATA_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_SEQ
或 DATA_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: -------------------------------------------------------------------
这里可以检测到错误,但无法纠正或指示给发送方。这些交换是对称的,即切换 rx
和 tx
仍然会得到有效的交换。目前,没有发现更长的交换。
命令:请求、响应和事件¶
命令作为数据帧内的负载发送。目前,这是 DATA
帧唯一已知的负载类型,其负载类型值为 0x80
(SSH_PLD_TYPE_CMD
)。
命令类型负载(struct ssh_command
)由一个八字节的命令结构组成,后跟可选和可变长度的命令数据。此可选数据的长度从相应帧中给出的帧负载长度得出,即 frame.len - sizeof(struct ssh_command)
。命令结构包含以下字段,它们被打包在一起并按顺序排列
字段 |
类型 |
描述 |
---|---|---|
|
|
负载的类型。对于命令,始终为 |
|
|
目标类别。 |
|
|
命令/消息的目标 ID。 |
|
|
命令/消息的源 ID。 |
|
|
实例 ID。 |
|
|
请求 ID。 |
|
|
命令 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=0
和 SEQ=1
的帧,然后重复 SEQ=0
不会将第二个 SEQ=0
帧检测为重复,因此每次接收到该帧时都会执行该帧中的命令,即在本例中执行两次。发送 SEQ=0
、SEQ=1
,然后重复 SEQ=1
将检测到第二个 SEQ=1
是第一个的重复,并忽略它,因此只执行一次包含的命令。
总之,这表明最多只能有一个挂起的未 ACK 帧(每个发送方,实际上导致有关帧的同步通信)和最多三个挂起的命令。同步帧传输的限制似乎与 Windows 上观察到的行为一致。