圖文詳解 PCI-PCIe 協議

作者:晏舒

RK3568 總線體系結構:

  1. 速率:APB<AHB<AXI;

  2. AXI 支持讀寫並行操作;

  3. AXI/AHB 支持多主 / 從設備,有總裁機制,APB 不支持;

Soc 廠商會做好 Address Mapping,當 CPU 對不同地址訪問時,由內存控制器將地址或者數據發到對應的總線最終到達最終的設備上。

Example:From Rockchip SPEC

爲什麼要引入 PCI/PCIe 總線:

  1. 提高設備訪問速率:硬件設計 + 協議;

  2. 提高擴展性;

  3. 設備訪問方式的改變;

CPU 內存地址空間和 PCI 總線域地址空間:

CPU 訪問 PCI 設備流程:

  1. CPU 發出 CPU 內存域地址;

  2. HOST 主橋 y 發現該地址屬於自己內部內存域地址和 PCI 地址的轉換範圍;

  3. HOST 主橋 y 將該地址轉換爲 PCI 域地址;

  4. 根據該 PCI 域地址通過 PCI 橋 y1 最終找到 PCI 設備 y12;

PCI 設備之間的訪問流程:

  1. PCI 設備 y11 發出 PCI 域地址;

  2. 該地址通過 PCI 橋 y1 找到掛載在 PCI 總線 y0 上的 PCI 設備 y02;

PCI 設備訪問 DDR 的流程:

  1. PCI 設備 y11 嘗試將數據給到 DDR 地址空間;

  2. HOST 主橋 y 接收到 PCI 設備 y11 嘗試訪問的地址,並將其轉換爲 CPU 域地址;

  3. DMA 實現 PCI 設備數據到 DDR 的數據搬運(可以不使用 DMA,通過 CPU 進行數據搬運,但效率低);

PCI 的總線信號:

如果不是做 PCI 相關電路設計,則只需要關注上述幾個關鍵信號管腳:

  1. AD[31:0]:Address/Data 複用信號線,PCI 總線事務在啓動後的第一個時鐘週期傳送地址,下一個時鐘週期傳送數據;

  2. C/BE[3:0]#:Bus Command/Byte Enables,總線命令或者字節選通複用信號,地址週期表示命令,數據週期時輸出字節選通信號,使用這組信號可以對 PCI 設備進行單個字節、字和雙字訪問;

  1. FRAME#:指示 PCI 總線事務的開始和結束;

  2. IDSEL 信號:PCI 總線在進行配置讀寫總線事務時,使用該信號選擇 PCI 目標設備。

  3. REQ#/GNT#:總線仲裁信號;

PCI 總線的存儲器讀寫總線事務:

PCI 設備的配置空間中,一共有 6 個 BAR 寄存器。BAR 寄存器存放 PCI 設備使用的 PCI 總線域的物理地址,通過 HOST 主橋完成內存域地址到 PCI 域地址的映射關係。

場景 2:PCI 設備 y12 向主存儲器寫數據,PCI 設備進行 DMA 寫操作。

PCI 橋和 PCI 設備的配置空間:

配置空間爲 PCI 橋設備或者 PCI Agent 設備上的一塊 ROM 區間。

PCI 橋:

PCI 橋跨接在兩個 PCI 總線之間,其中距離 HOST 主橋較近的 PCI 總線稱爲該橋片的上游總線(Primary Bus),距離 HOST 主橋較遠的 PCI 總線稱爲該橋片的下游總線(Secondary Bus)。

PCI 橋 y1 的上游總線爲 PCI 總線 y0,下游總線爲 PCI 總線 y1。

通過 PCI 橋組成一個胖樹結構,其中每個橋片都是父節點,而 PCI Agent 設備只能是子節點。

通過 PCI 橋可以擴展一條新的 PCI 總線,但是不能擴展新的 PCI 總線域。在上述 PCI 總線域 y 中,所有設備共享該總線域的地址空間大小。

PCI Agent 設備的配置空間:

以上述 VGS GD 5446 爲例,我們可以通過 mmap 映射 BAR Region0 來讀取 PCI 設備 BAR 1 對應空間的內容,當然對 BAR 1 空間地址的讀寫具體代表什麼含義要參考 GD 5446 的數據手冊了:

示例代碼:(我們可以通過內存訪問的方式去和 PCI 設備打交道了)

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdint.h>

#define MEMORY_REGION_SIZE 4096
#define READ_SIZE 100

int main() {
    int fd;
    off_t pci_addr = 0xfeb90000;
    void *mmap_addr;
    unsigned char *pci_data;

    fd = open("/dev/mem", O_RDONLY);
    if (fd == -1) {
        perror("Error opening /dev/mem");
        return EXIT_FAILURE;
    }

    mmap_addr = mmap(NULL, MEMORY_REGION_SIZE, PROT_READ, MAP_SHARED, fd, pci_addr);
    if (mmap_addr == MAP_FAILED) {
        perror("Error mmapping the device");
        close(fd);
        return EXIT_FAILURE;
    }

    pci_data = (unsigned char *)mmap_addr;

    printf("Reading first 20 bytes of PCI BAR 0 at address 0xfc000000:\n");
    for (int i = 0; i < READ_SIZE; i++) {
        printf("%02hhx ", pci_data[i]);
    }
    printf("\n");

    if (munmap(mmap_addr, MEMORY_REGION_SIZE) == -1) {
        perror("Error unmapping memory");
        close(fd);
        return EXIT_FAILURE;
    }

    close(fd);
    return 0;
}
sudo ./a.out
Reading first 20 bytes of PCI BAR 0 at address 0xfc000000:
20 20 00 00 07 f5 ff 00 00 00 00 00 67 00 06 01 00 00 00 00 1d 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

PCI 橋的配置空間:

PCI 總線樹 BUS/Device 號的初始化:

以 X86 爲例,BIOS 或 UEFI 會負責 PCI 總線樹的遍歷,查找所有 PCI 設備,分配所需資源,比如內存地址空間,IO 空間和中斷等。之前提到 PCI 設備配置空間的初始化是通過 ID 譯碼的,因此這裏要講一下 BUS:Device.Function 是如何被定義的。

示例:

cuixujia.cxj@posix-team:~$ lspci -tv
-[0000:00]-+-00.0  Intel Corporation 440FX - 82441FX PMC [Natoma]
           +-01.0  Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
           +-01.1  Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
           +-01.3  Intel Corporation 82371AB/EB/MB PIIX4 ACPI
           +-02.0  Cirrus Logic GD 5446
           +-03.0  Red Hat, Inc. Virtio network device
           +-04.0  Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #1
           +-04.1  Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #2
           +-04.2  Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #3
           +-04.7  Intel Corporation 82801I (ICH9 Family) USB2 EHCI Controller #1
           +-05.0  Red Hat, Inc. Virtio block device
           \-06.0  Red Hat, Inc. Virtio memory balloon

如上 docker 所示,該系統只有一個 PCI 總線(總線 0),該總線下共有 6 個 Device,其中 Device1/4 有多個 Function。

BUS 號的初始化:

使用深度優先 DFS 算法遍歷 PCI 總線樹。

示例:

Device 號的分配:

Device 號的確定是通過 IDSEL 信號確定,但是這部分就和硬件相關了,而且在將 IDSEL 是如何確定 Device 號之間,必須要先看一下 PCI 的配置讀寫事務是如何工作的:

PCI 總線的配置讀寫事務:

PCI 總線定義了兩類配置請求,一類是 Type 01 配置請求,另一類是 Type 01 配置請求。

CONFIG_ADDRESS 寄存器與 Type 01h 配置請求:

CONFIG_ADDRESS 寄存器與 Type 00h 配置請求:

繼續回到 Device 號的分配,在 Type 00h 配置請求中,Device Number 會給到地址線的 [31:11] 管腳上,如果在 PCI 插槽的硬件設計中,將同一 PCI 總線下的 PCI 設備的 IDSEL 管腳和不同該 PCI 設備的 AD 管腳相連,就可以確定 Device 號了。

PCI Agent 0 的 IDSEL 和 AD16 相連,PCI Agent 1 的 IDSEL 和 AD17 相連,PCI Agent 2 的 IDSEL 和 AD18 相連,這樣在 Type 00h 配置請求中 AD 信號就可以選中不同的 PCI 設備。

示例:

通過 X86 IO Port 操作 CONFIG_ADDRESS 和 CONFIG_DATA 來掃描 PCI 總線樹,獲取所有 PCI 設備及其配置空間的內容:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/io.h>

#define PCI_MAX_BUS 255
#define PCI_MAX_DEV 31
#define PCI_MAX_FUN 7

#define PCI_BASE_ADDR 0x80000000L

#define CONFIG_ADDR 0xcf8
#define CONFIG_DATA 0xcfc

typedef unsigned long DWORD;
typedef unsigned int WORD;

int main()
{
    WORD bus, dev, fun;
    DWORD addr, data;

    printf("\nbus#\tdev#\tfun#\tvendor#\t\tdevice#\n");

    iopl(3);

    for (bus = 0; bus <= PCI_MAX_BUS; bus++) {
        for (dev = 0; dev <= PCI_MAX_DEV; dev++) {
            for (fun = 0; fun <= PCI_MAX_FUN; fun++)
            {
                addr = PCI_BASE_ADDR | (bus << 16) | (dev << 11) | (fun << 8);
                outl(addr, CONFIG_ADDR);
                data = inl(CONFIG_DATA);
                if (((data & 0xFFFF) != 0xFFFF) && (data != 0))
                {
                    printf("%02d  \t%02d  \t%02d  \t", bus, dev, fun);
                    printf("%04x  \t\t%04x", (data & 0xFFFF), (data & 0xFFFF0000) >> 16);
                    printf("\n--------------------------------------------\n");
                }
            }
        }
    }

    iopl(0)return 0;
}

PCI 設備 BAR 空間的初始化:

假設,存儲器域的 0xF000-0000 ~ 0xF7FF-FFFF(128M)物理地址空間與 PCI 總線的地址空間存在映射關係。

當處理器訪問這段儲存器地址空間時,HOST 主橋會認領這個存儲器訪問,通過 outbound 將該存儲器訪問使用的物理地址空間轉換爲 PCI 總線地址空間,並於 0x7000-0000 ~ 0x77FF-FFFF 這段 PCI 總線地址空間對應。

反過來也一樣,但是使用 Inbount 寄存器實現 PCI 總線地址到存儲器域地址的轉換,這裏就不畫了。

PCI 設備 BAR 寄存器和 PCI 橋 Base、Limit 寄存器的初始化:

假定所有 PCI Agent 只使用 BAR0 寄存器,申請的數據空間大小爲 16M(0x1000000B)。

PCIE 總線協議:

硬件接口:

PCIE 使用差分信號進行數據的串行傳輸以提高最大的傳輸速率,因此發送和接收共使用 4 條傳輸線,這 4 條傳輸線共同組成一個 Lane:

可以根據具體的 PCIe 外設,將 PCIe 鏈路配置成 X1、X2、X4...、X32 Lane:

數據傳輸方式:

相比於並行傳輸接口,使用串行數據傳輸方式,就只能使用數據包的方式進行數據傳輸,同時需要定義數據傳輸協議:

硬件拓撲:

PCIE 配置過程:

通過事務層的 TLP Header 決定當前發送的 TLP 的總線事務:

PCIE 總線樹的初始化:

MSI/MSI-X 中斷機制:

在講 MSI 和 MSI-X 中斷機制之前,需要先了解一下 Legacy 的中斷方式,即使用 INTx 的中斷。

PCI INTx 中斷觸發機制:

PCI 設備存在物理中斷線 INTA~INTD,使用邊帶信號將 INTx 和 SOC GPIO 相連,並配置 GIC 中斷向量。

通過對 PCI 設備的配置空間中 Interrupt Pin 配置來決定該 PCI 設備觸發中斷時使用的中斷線。

PCIe INTx 中斷觸發機制:

PCIe 不存在 INTx 中斷線,中斷信號通過 TLP 發送給 PCIe controller。

MSI 中斷機制:

MSI 可以使 PCI/PCIe 設備直接通過 PCI/PCIe 控制器的內存寫事務向 GIC 發起 ITS 中斷請求。

MSI-X 中斷機制:

MSI 中斷機制有一些缺陷:

關於 DeviceID 如何傳送到 ITS:

其實到上面都比較好理解,但是唯獨有一個困惑,就是 GIC ITS 在轉換時還需要一個 DeviceID,但是 MSI Message 裏並沒有傳輸這個 DeivceID,那麼 ITS 如何知道這次 LPI 中斷時的設備呢?

這部分資料很少很少,找了大量資料,從以下文檔找到這這段話:

corelink_gic600_generic_interrupt_controller_technical_reference_manual_100336_0106_00_en.pdf

其實按照我的理解就是我們在配置 ITS 的 DeviceID 時,這個 DeviceID 是由 BDF 以規定的規則組成一個 Requester ID(這也解釋了爲什麼我們在 ITS 中註冊的 DeviceID 也必須是 RID),在 PCIE 觸發 MSI 中斷時,這個 Requester ID 也會跟隨總線傳輸到比如 AXI 總線上,ITS 在硬件設計中可能會使用比如 AWUSER 信號來獲取 DeviceID。上述設計都是 soc 在設計時決定的,Requester ID 的傳遞對用戶是透明的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/5JMtjB9zMZLO-9TC2RXe0g