3. CXL Type2设备初始化流程:设备发现与配置空间、LD枚举、MMIO与寄存器映射

好,咱们今天聊聊Type2设备的初始化流程。说实话,这块内容在CXL协议里属于「看着简单,做起来坑多」的部分。我当年第一次调Type2设备时,光设备发现就卡了两天——后来发现是配置空间里一个保留位没处理好。嗯,咱们一步步来。

3.1 设备发现与配置空间

Type2设备在PCIe总线上,本质上就是个PCIe设备。所以第一步,还是走PCIe的枚举流程。

系统软件(通常是BIOS或OS的PCI子系统)会扫描总线,读取每个设备的Vendor ID和Device ID。Type2设备有自己独特的Class Code——我记得是0x05(Memory Controller)下的某个子类。但更靠谱的做法,是看CXL Capability结构。

关键点:Type2设备必须实现CXL Capability结构,位于配置空间的某个扩展Capability链中。系统软件通过这个结构确认设备是CXL设备,以及它的Type类型。

配置空间里还有个重要的东西——DVSEC(Designated Vendor-Specific Extended Capability)。CXL协议用DVSEC来存放一些特定信息,比如设备所属的CXL版本、端口信息等。我个人习惯在初始化时先读DVSEC,确认设备支持的特性集。

// 伪代码:读取CXL Capability
uint16_t cap_offset = pci_find_capability(dev, PCI_CAP_ID_CXL);
if (cap_offset) {
    uint16_t cxl_cap = pci_read_word(dev, cap_offset + 0x02);
    uint8_t cxl_type = (cxl_cap >> 4) & 0x0F;
    if (cxl_type == 0x02) {
        // 确认是Type2设备
        print("Found CXL Type2 device");
    }
}

个人经验:有些早期Type2原型设备,CXL Capability的位置可能不标准。我建议在枚举时多留个心眼——如果找不到标准Capability,可以尝试扫描整个Capability链。我曾经遇到过某款FPGA原型,它的CXL Capability藏在第7个扩展Capability里,差点以为是普通PCIe设备。

3.2 LD(Logical Device)枚举

Type2设备跟Type3最大的区别,就是它支持多个Logical Device。每个LD可以独立访问HPA(Host Physical Address)空间,也可以有自己的MMIO区域。

LD的枚举,说白了就是搞清楚设备内部到底有几个「子设备」。这些信息存在哪里?嗯,在CXL的Device Capability结构里。

具体流程是这样的:

  1. 读取CXL Device Capability结构,获取LD的个数
  2. 遍历每个LD,读取它的LD Info结构
  3. 根据LD Info中的信息,确定每个LD的类型(是加速器、内存还是其他)
  4. 为每个LD分配LD ID,并建立LD与HPA的映射关系
LD Info字段 含义 我踩过的坑
LD ID 逻辑设备编号 注意ID不一定连续,可能有空洞
LD Type 设备类型(加速器/内存/其他) 有些设备会复用Type值,需要查表
MMIO Base 该LD的MMIO基地址 基地址可能对齐到64KB,别算错了
HPA Range 该LD可访问的HPA范围 范围可能重叠,需要做冲突检测

注意:LD枚举时,系统软件需要为每个LD分配独立的MMIO空间。如果LD个数很多(比如超过16个),MMIO空间可能会不够用。我建议在BIOS阶段就预留足够的MMIO窗口,否则到了OS里再调整,那可就麻烦了。

3.3 MMIO与寄存器映射

MMIO映射,是Type2设备初始化的重头戏。每个LD都有自己的MMIO区域,里面放着控制寄存器、状态寄存器、Doorbell寄存器等等。

映射流程大致如下:

  • 第一步:读取每个LD的MMIO Base和Size
  • 第二步:将这些MMIO区域映射到系统物理地址空间
  • 第三步:建立页表映射,让CPU可以访问这些寄存器
  • 第四步:初始化每个LD的关键寄存器(比如Doorbell、Interrupt控制)

这里有个细节——Type2设备的MMIO区域,通常包含两部分:

  1. Device MMIO:每个LD私有的寄存器空间
  2. Global MMIO:所有LD共享的寄存器空间(比如全局中断控制)

我个人习惯把Global MMIO映射在单独的页面上,方便做权限控制。你想想看,如果所有LD都能改全局中断配置,那不乱套了?

// 伪代码:映射LD的MMIO
for (int i = 0; i < num_lds; i++) {
    uint64_t mmio_base = ld_info[i].mmio_base;
    uint64_t mmio_size = ld_info[i].mmio_size;
    
    // 映射到系统物理地址
    void *virt_addr = ioremap(mmio_base, mmio_size);
    
    // 初始化Doorbell寄存器
    writel(0, virt_addr + DOORBELL_OFFSET);
    
    // 配置中断向量
    writel(irq_vector, virt_addr + INT_VECTOR_OFFSET);
    
    print("LD %d: MMIO mapped at 0x%llx", i, mmio_base);
}

避坑指南:我曾经遇到过一个问题——某个LD的MMIO Size是4MB,但系统只分配了2MB的窗口。结果一访问寄存器就触发Page Fault。后来我学乖了,在映射前先检查MMIO Size是否超过了系统预留的窗口大小。如果超了,要么扩大窗口,要么把LD拆成多个子区域映射。

寄存器映射还有个容易忽略的点——Endianness。CXL协议规定寄存器默认是Little Endian,但有些加速器内部用的是Big Endian。如果你不做转换,读出来的值就是错的。我建议在初始化时统一做一次Endianness检测,写一个已知值再读回来,确认字节序正确。

3.4 初始化流程图

下面这张图,是我根据实际项目经验画的Type2设备初始化流程。你看一眼,基本就能明白整个链路了。

CXL Type2设备初始化流程 1. PCIe设备发现 2. 读取CXL Capability 3. 确认Type2类型 4. LD枚举(读取LD Info) 5. MMIO映射与寄存器初始化 6. 设备就绪,等待Host请求 注意:步骤4和5之间,需要做MMIO Size检查,防止映射越界 每个LD的MMIO区域独立映射,Global MMIO单独处理

这张图里,我把关键步骤都标出来了。你注意看步骤4和5之间的那个说明——MMIO Size检查,这个真不能省。我当年就是跳过了这步,结果在某个LD上踩了坑,排查了整整一个下午。

总结一下:Type2设备初始化,核心就是三件事——发现设备、枚举LD、映射MMIO。每一步都有细节,但只要你按流程走,别跳步骤,基本不会出大问题。嗯,今天就聊到这儿,下节课咱们聊聊Type2设备的数据通路。


专注资料整理