# PCI/PCIe(4): Transaction Layer ## PCI Express Layering PCIe 是一種序列匯流排(serial bus)。在 PCIe 協議上,所有操作都以封包(packet)的形式完成。舉例來說,假設 CPU 想要將一些資料寫入裝置。它將命令轉送到 PCIe Bridge,然後 PCIe bridge 會建立一個封包、之中寫入位址和資料、然後以序列方式轉送到目標。目標裝置收到封包後,再進行解析並執行之。 ![image](https://hackmd.io/_uploads/rJ_bW1EtC.png) > [PCI Express® Base Specification Revision 5.0](https://picture.iczhiku.com/resource/eetop/SYkDTqhOLhpUTnMx.pdf) PCIe 所連接兩端以封包的形式來溝通,以將資訊從傳輸端傳送到接收端。而 PCIe 是一個多層協定,由事務層(Transaction)、資料交換層(Data Link)和實體層(Physical)構成。傳輸時,每當封包經過下一層級後,會將原本的封包加上此層的封包所需的附加資訊。接收時則相反,封包由實體層步步拆解至事務層的封包, ![image](https://hackmd.io/_uploads/SycwgN7Eye.png) > [PCI Express® Base Specification Revision 5.0](https://picture.iczhiku.com/resource/eetop/SYkDTqhOLhpUTnMx.pdf) 在本章節中,我們接針對事務層(Transaction Layer)進行更仔細的探討。 ## 事務層(Transaction Layer) 事務層是協定的最上層,主要職責是 TLP(Transaction Layer Packet) 的組裝和拆卸。 TLP 用於例如讀取和寫入以及特定類型的事件。交易層還負責管理 TLP 的 [credit-based flow control](https://oneflow2020.medium.com/the-history-of-credit-based-flow-control-part-1-342ec6efe23c)。 每個封包都具有一個唯一的 id,使回應的封包能夠定位到正確的傳輸者。封包格式還支援不同形式的定址,根據事務類型可以分為 Memory、I/O、Configuration 和 Message 四種。其中 Message 方式主要目的是消除目前在實作中使用的各種 sideband 訊號,其支援把所有舊有(PCI)的 sideband 訊號轉變為 inband 訊號,例如中斷、電源管理的請求等。 ## 封包格式 ### Overview 從系統軟體的角度,事務層(Transaction layer)的運作原理是我們理解 PCIe 重要的一環。在事務層,封包被接收。而封包可以有許多類型:比如讀寫記憶體、讀寫 I/O、表示完成訊息等。這一類封包在標準中稱為「事務層封包(Transaction Layer Packet, TLP)」。 PCIe 是 32 bits 的 bus,因此每 32 bits 的資料在 PCIe 上可稱為 double word(以下簡稱 DW)。每個 TLP 一般由 header (3-4 個 DW)和 data payload (0-1023 個 DW)組成。 :::info 為求容易理解,這裡暫時忽略可選的 TLP Prefix 和 TLP Digest。詳情請翻閱 PCIe Specification 的第二章 Transaction Layer Specification。 ::: Header 的格式有許多種,但第一個 DW 會是相同的。`Fmt` 一欄位標示 header 的格式,比如 header 大小以及是否有 data payload 等。結合 `Type` 則描述了 TLP 對應的操作。詳情請見 [PCI Express® Base Specification Revision 5.0](https://picture.iczhiku.com/resource/eetop/SYkDTqhOLhpUTnMx.pdf) 的 `Table 2-3 Fmt[2:0] and Type[4:0] Field Encodings`。 ![image](https://hackmd.io/_uploads/r1OjJk4Y0.png) ![image](https://hackmd.io/_uploads/Syx0ZGyVK0.png) 由於 PCIe 事務的類型複雜,要仔細闡述 PCIe 所支援的所有封包格式並不容易。這裡我們僅以舉例方式說明,過程會省略部分的特例與細節,只是嘗試讓讀者對 PCIe 傳輸的流程有基本的了解。如果需要完整的認識,請務必閱讀規格書。 ### Example: Write 32 bits data 假設 CPU 嘗試以 32 位元尋址方式,對 PCIe 周邊的實體位址 `fdaff040` 寫入 32 bits 的資料 `0x12345678`。 在此案例中,現代平台常見的設計是 CPU 首先在自己的匯流排上執行寫入操作,而由於連接到 CPU 匯流排的記憶體控制器(memory controller) 直接連接到 PCIe 匯流排。因此,記憶體控制器能再藉由 Root Complex 產生一個記憶體寫入的封包,表明將資料寫入特定位址。封包經過 PCIe port 到達周邊裝置(中間可能有額外的網路,比如 switch 等)後,裝置對封包做解析並執行要求的寫入操作。 在封包部分。可由 4 個 DW 組成,具體內容如下所示。 ![image](https://hackmd.io/_uploads/ry-g3HXEke.png) 這裡我們可以再觀察封包的更多細節: * `Fmt` 和 `Type` 結合在一起,表示這是一個記憶體寫入請求 * `TD` 為零,表示 TLP 資料上沒有 TLP Digest(額外的 CRC 資料) * `Length` 為 0x001,表示該 TLP 具有 1 個 DW payload * `Request ID` 欄位表示該封包的傳送者之 ID 為零,也就是 Root Complex * `Tag` 在此案例下是未使用的欄位。發送者可以在此處放置任何內容,而其他人都應忽略它。後面會再探討這有甚麼用途 * `1st BE` 欄位(1st Double-Word Byte Enable)可以用來選擇第一個 data DW 中的四個 bytes 之有效性。以本例來說,設定為 0xf 表示四個 bytes 都有效,應被寫入 * `Last BE` 在 Length 為 1 時必須為零,因為第一個 DW 和最後一個 DW 是相同的 * `Address` 表示資料寫入的起始位址 * `0x3f6bfc10 << 2 == 0xfdaff040` * `Data DW0` 表示所要寫入的資料 `0x12345678` 附帶一提,由於 PCIe 是使用 Big Endian,如果 CPU 是使用 Little Endian,則在其在軟體會需要寫入 `0x78563412`。 ### Example: Read 32 bits data 假設 CPU 嘗試對 PCIe 周邊的實體位址 `fdaff040` 讀出 32 bits 的資料時又是如何呢? 在讀取操作上,這會比寫入稍微複雜一下,因為會涉及兩個封包: 1. 從 Host 到周邊的 TLP,要求後者執行讀取操作 2. 從周邊返回到 host 的 TLP,攜帶讀取到的資料內容 在 PCIe 術語中,我們有一個 Requester 和一個 Completer,在此案例中分別是 Host 和 PCIe 周邊。和上一個例子類似,CPU 在與記憶體控制器共享的匯流排上執行寫入操作,讓記憶體控制器藉 Root Complex 產生透過 PCIe bus 發送的 TLP。 在封包部分。這個讀取請求的封包由 3 個 DW 組成,內容如下。 ![image](https://hackmd.io/_uploads/S1L6fAK4kx.png) 大體上欄位和前述的寫操作相似。 * `Fmt` 和 `Type` 接合表示一個讀取請求 * `Request ID` 欄位表示該封包的傳送者之 ID 為零,在讀取上此欄位至關重要,因為它告訴 Completer 應該將回應發送到哪裡 * `Tag` 在讀取請求中很重要。當 Completer 回應時,它會將此值複製到 Completion TLP 中。這讓 Request 知道完成的封包是對應之前送出的哪個請求 * `Length` 為 1 表示要讀取一個 DW * `Address` 表示從哪個位址讀取 關於 `Tag` 再額外說明。在 PCIe 上,單一裝置被允許發送多個請求,Requester 可以根據自身管理方式設定之,只要確保所有未完成請求的 `Tag` 都是唯一的即可。 當週邊收到 Read TLP 時,無論它是否能夠確實完成該請求,它都必須以某種 Completion TLP 做回應。若以成功讀取的結果為例,裝置從其內部讀取資料,然後將結果傳回給 Requester,封包會如下所示: ![image](https://hackmd.io/_uploads/rkSR0AKEkx.png) 這個封包表示「告訴 Requester 0x0000,對於 0x0100 的請求中,其中 tag 標記為 0x0c 的一個之答案是 0x12345678。 * `Fmt` 與 `Type` 分別是 `0x2` 和 `0xa`,結合在一起表示這是一個帶有資料的 Complete TLP * `Length` 值為 0x001,表示該 TLP 具有 1 個 DW 的資料 * 由於 TLP 的長度有限制,這個 Length 長度可能小於一開始 Reqeust 要求的 DW 數量,改以拆分為多個 Complete TLP 完成 * `Byte Count` 所代表是包含目前的 Completion TLP 所剩餘需要傳輸的 byte 數量。在本例中,單個 TLP 就完成請求的情況中,其值為 4(bytes) * `Lower Address` 是 TLP 所讀取的起始位址之最低 7 個 bits。以本例來說 0x40(`0xfdaff040 & 0x7f`)。這個欄位在多個 Completion TLP 時非常有用 * `Completer ID` 代表此封包的傳送者是 `0x0100` * `Requester ID` 表示封包的接收者 ID 為 0,也就是 Root Complex * `Status` 為 0 表示是一個成功的 Completion,其他值就用來不同類型的拒絕/失敗 * `BCM` 欄位除了封包源自具有 PCI-X 的 bridge 的情況,始終是零 * `Data DW0` 就是所得到的資料 ### Posted and Non-Posted operations 比較上述的讀寫案例,我們會發現一個明顯的差異: 讀操作無需等待額外的 Completion TLP,而寫操作需等待直到收到 Completion TLP。對於後者,這表示在收到 Completion TLP 之前,Requester 必須保留與 request 有關的資訊。甚至 CPU 開始讀取動作時,CPU 需持續等待直到成功讀取來自 PCI 裝置的資料。 PCI/PCIe 的術語上,這種發出請求即結束的操作稱為 Posted transaction,而由請求和完成組成的操作則是 non-Posted transaction。顯而易見,non-Posted transaction 容易造成更大的延遲。請參閱 PCIe 的規範手冊以得知關於 Posted/Non-Posted 的更多細節。 ### Snoop and No-Snoop Operation 當進行 PCIe transaction 時,對於 CPU 記憶體的讀寫可能需要檢查 Cache 以保證 [Cache Coherence](https://en.wikipedia.org/wiki/Cache_coherence),否則可能導致同步的錯誤。 舉例來說,CPU A 嘗試要將記憶體上的資料寫入 PCIe 裝置時,記憶體最近一次是由 CPU B 嘗試對其更新。但由於內容還保存在 CPU B 的 Cache 上,尚未同步到記憶體中,此時 CPU A 的寫入操作就會發生錯誤。 確保 Cache Coherence 的相關過程就稱為 **Snoop**。 在某些狀況下,某些區段的記憶體是不需要特地確保 Cache Coherence 的,省下 Snoop 的過程可以對效能帶來幫助。因此,PCIe 的讀寫操作可以分為 Snoop 和 No-Snoop 兩種。 ## MPS Max Payload size ECRC ## Rerfenece * [Down to the TLP: How PCI express devices talk (Part I)](https://xillybus.com/tutorials/pci-express-tlp-pcie-primer-tutorial-guide-1)