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 上觀察到的行為一致。