4、SDIO/PCIe接口基础:SDIO协议栈(CMD/Data/Block)、PCIe配置空间与BAR空间、接口枚举流程、Linux内核中MMC/SDIO子系统与PCIe子系统

各位同学,今天我们来啃一块硬骨头——WiFi芯片与主控之间的接口。说白了,WiFi芯片再牛,也得通过一根“管道”跟CPU说话。这根管道,最常见的就是SDIO和PCIe。

我个人习惯把接口比作“高速公路”。SDIO是双向两车道,够用但会堵车;PCIe是八车道,跑得快但修路成本高。你想想看,手机里的WiFi芯片大多用SDIO,而PC上的网卡几乎全是PCIe,就是这个道理。

4.1 SDIO协议栈:CMD、Data与Block

SDIO是从SD卡协议演化来的。我刚开始接触时也纳闷:为什么WiFi芯片要用存储卡的协议?后来发现,SDIO的优势在于“简单”——它不需要复杂的配置空间,上电就能发命令。

4.1.1 命令(CMD)与响应

SDIO的通信由Host发起。Host发一个CMD,Device回一个Response。命令格式是固定的:48位,包含6字节的起始位、传输位、命令索引、参数和CRC。

// 一个典型的CMD52(读写单字节寄存器)格式
// 命令索引: 0x34 (CMD52)
// 参数: [31:27] 读写标志 | [26] 功能号 | [25:9] 寄存器地址 | [8] 保留 | [7:0] 写入数据
uint32_t cmd52_arg = (1 << 31) | (func_num << 26) | (reg_addr << 9) | write_data;

我在项目中遇到过一个问题:CMD52的响应超时。查了半天,发现是WiFi芯片的时钟没稳定。嗯,这里要注意——SDIO的时钟必须在发送命令前稳定至少74个周期。

4.1.2 数据传输与Block模式

SDIO的数据传输有两种模式:Byte模式和Block模式。Byte模式适合小数据量,比如读一个寄存器;Block模式适合大数据量,比如下载固件。

Block模式的核心是“块大小”。SDIO规范规定,块大小可以是1到512字节。但实际中,大部分WiFi芯片只支持512字节的块。为什么?因为SD卡就是这么定义的,WiFi芯片直接“抄作业”了。

关键点:SDIO的Block传输必须对齐。如果你要传1025字节,就得拆成2个512字节块加1个1字节块。我曾经因为没处理这个“尾巴”,导致固件下载总是校验失败。

4.1.3 多Function与Function0

SDIO设备可以有多个Function。Function0是控制通道,负责配置和中断管理;Function1~7是数据通道。WiFi芯片通常用Function0做控制,Function1做数据。

Function 用途 典型寄存器
Function0 设备配置、中断控制 CIA(Common I/O Area)寄存器
Function1 WiFi数据收发 FBR(Function Base Register)

个人经验:调试时先用CMD52读Function0的CIA寄存器,确认设备活着。如果读回来全是0xFF,说明设备没响应——检查电源和时钟吧。

4.2 PCIe配置空间与BAR空间

PCIe比SDIO复杂得多。它有一套完整的配置空间,用来描述设备的能力和资源需求。我刚开始看PCIe规范时,被那256字节的配置空间搞得头晕。后来发现,其实只需要关注几个关键字段。

4.2.1 配置空间结构

PCIe配置空间的前64字节是标准头部,包含Vendor ID、Device ID、Class Code、BAR等。后面的192字节是Capability链表,用来描述设备支持的高级特性。

// PCIe配置空间头部(前64字节)
struct pci_config_header {
    uint16_t vendor_id;    // 0x00: 厂商ID,比如Broadcom是0x14E4
    uint16_t device_id;    // 0x02: 设备ID
    uint16_t command;      // 0x04: 命令寄存器
    uint16_t status;       // 0x06: 状态寄存器
    uint8_t  revision_id;  // 0x08: 修订版本
    uint8_t  class_code[3];// 0x09: 类代码(网卡是0x028000)
    uint32_t bar[6];       // 0x10: 基地址寄存器(BAR0~BAR5)
    // ... 省略其他字段
};

我记得第一次调试PCIe网卡时,读到的Vendor ID是0xFFFF。当时以为设备坏了,后来发现是RC(Root Complex)没给设备供电。嗯,PCIe的电源管理比SDIO复杂得多。

4.2.2 BAR空间与地址映射

BAR(Base Address Register)是设备向系统申请的内存或I/O空间。WiFi芯片通常用BAR0做控制寄存器,BAR1做数据缓冲区。

举个例子:如果BAR0的值是0xFE000000,说明设备申请了32MB的内存空间。系统会把这段物理地址映射到CPU的地址空间,驱动通过ioremap()访问它。

避坑指南:我曾经在BAR空间映射时踩过坑——忘记检查BAR的地址对齐。PCIe要求BAR的地址必须按申请大小对齐。比如申请32MB,地址必须是32MB的整数倍。否则系统会报错。

4.3 接口枚举流程

无论是SDIO还是PCIe,设备枚举都是驱动开发的第一步。枚举失败,后面全是白搭。

4.3.1 SDIO枚举流程

SDIO的枚举由Host Controller驱动完成。流程大致如下:

  1. Host发送CMD0(复位),让设备进入空闲状态
  2. 发送CMD5(SDIO Send Op Cond),获取设备支持的电压和功能数
  3. 发送CMD3(设置相对地址),给设备分配一个RCA
  4. 发送CMD7(选择设备),选中该设备
  5. 发送CMD52读取Function0的CIA寄存器,确认设备就绪

我在项目中遇到过一个问题:CMD5返回的电压范围是2.7V~3.6V,但Host只支持3.0V。结果设备初始化失败。后来加了电压协商逻辑才解决。

4.3.2 PCIe枚举流程

PCIe的枚举由BIOS或内核完成。流程更复杂:

  1. RC扫描Bus 0,发现设备后分配Bus号
  2. 读取设备的Vendor ID和Device ID,确认设备存在
  3. 读取Class Code,判断设备类型(网卡、存储等)
  4. 读取BAR,获取设备需要的地址空间大小
  5. 分配物理地址,写入BAR
  6. 使能Memory Space和Bus Master位

关键点:PCIe枚举时,如果设备不支持Memory Space,驱动只能通过I/O Space访问。但现在的WiFi芯片基本都支持Memory Space,I/O Space已经很少用了。

4.4 Linux内核中的MMC/SDIO子系统与PCIe子系统

Linux内核把SDIO和PCIe的驱动框架封装好了。我们写驱动时,不需要从头造轮子。

4.4.1 MMC/SDIO子系统

MMC/SDIO子系统位于drivers/mmc/目录下。核心结构是mmc_host和mmc_card。驱动通过mmc_register_host()注册Host Controller,通过mmc_add_card()添加设备。

// SDIO驱动注册示例
static const struct sdio_device_id wifi_ids[] = {
    { SDIO_DEVICE(0x02D0, 0x4354) },  // Broadcom BCM4354
    { /* end: all zeroes */ }
};

static struct sdio_driver wifi_driver = {
    .name     = "bcm4354_wifi",
    .id_table = wifi_ids,
    .probe    = wifi_probe,
    .remove   = wifi_remove,
};

module_sdio_driver(wifi_driver);

我个人习惯在probe函数里先调用sdio_claim_host(),确保独占访问。否则多个线程同时操作SDIO总线,容易出问题。

4.4.2 PCIe子系统

PCIe子系统在drivers/pci/目录下。核心结构是pci_dev和pci_driver。驱动通过pci_register_driver()注册。

// PCIe驱动注册示例
static const struct pci_device_id wifi_ids[] = {
    { PCI_DEVICE(0x14E4, 0x43A0) },  // Broadcom BCM4360
    { /* end: all zeroes */ }
};

static struct pci_driver wifi_driver = {
    .name     = "bcm4360_wifi",
    .id_table = wifi_ids,
    .probe    = wifi_pci_probe,
    .remove   = wifi_pci_remove,
};

module_pci_driver(wifi_driver);

小技巧:调试PCIe驱动时,可以用lspci -vvv查看设备的配置空间。如果BAR显示为[disabled],说明枚举时地址分配失败。检查dmesg,看是不是地址冲突了。

4.5 本章知识体系

下面这张图总结了SDIO和PCIe的核心知识点。我建议你把它打印出来贴在工位上,调试时对照着看。

WiFi芯片接口知识体系 SDIO接口 PCIe接口 CMD/Response Data/Block Function0~7 CIA寄存器 配置空间 BAR空间 Capability链表 MSI中断 枚举流程 SDIO: CMD0→CMD5→CMD3→CMD7 PCIe: 扫描Bus→读ID→分配BAR Linux内核子系统 drivers/mmc/ (MMC/SDIO) drivers/pci/ (PCIe)

好了,这一章的内容就到这里。SDIO和PCIe是WiFi驱动开发的基础,理解它们的协议栈和枚举流程,后面调试起来会顺手很多。下一章我们聊聊具体的驱动开发实战——怎么从零开始写一个WiFi芯片的SDIO驱动。


公众号:蓝海资料掘金营,微信deep3321