SDIO接口基础:从协议到设备枚举

各位同学,今天我们来聊聊SDIO接口。说实话,这个接口在机顶盒WiFi驱动开发中太常见了。我最早接触SDIO是在做一款博通WiFi芯片时,那时候被协议栈折腾得够呛。后来慢慢摸透了,才发现SDIO其实没那么神秘。

SDIO协议简介

SDIO,全称Secure Digital Input Output。说白了,就是在SD卡协议的基础上,扩展出了I/O功能。你想想看,SD卡协议本来只用来读写存储数据,但SDIO允许你挂载WiFi、蓝牙、GPS这些外设。

SDIO协议有几个关键点:

  • 命令-响应模型:主机发命令,设备回响应。就这么简单。
  • 数据线可配置:支持1位、4位两种模式。4位模式更快,但占用的引脚也多。
  • 时钟频率:从0Hz到最大50MHz。我一般初始化时先用低速,枚举成功后再切高速。
  • SPI模式兼容:有些芯片支持SPI模式,但机顶盒里基本都用SDIO原生模式。

这里有个坑,我提醒一下大家。SDIO协议有多个版本,1.0、2.0、3.0。不同版本对电压、时序要求不一样。我遇到过一块芯片,明明支持3.0,但板子设计用了1.8V电平,结果死活枚举不上。后来查了半天,才发现是电平不匹配。

注意:SDIO设备初始化时,一定要先确认电压匹配。1.8V和3.3V不能混用,否则轻则枚举失败,重则烧芯片。

SDIO控制器初始化

控制器初始化,是驱动开发的第一步。我习惯把它分成三个步骤:

  1. 时钟使能:先给SDIO控制器提供时钟。很多SoC的时钟是默认关闭的,需要手动打开。
  2. 引脚配置:配置SDIO相关的GPIO引脚,设置成SDIO功能模式。
  3. 控制器参数设置:设置时钟分频、数据宽度、中断使能等。

来看一段实际的初始化代码,这是我之前在海思平台上调的:

/* SDIO控制器初始化 */
static int sdio_controller_init(struct sdio_host *host)
{
    u32 reg_val;

    /* 1. 使能时钟 */
    writel(0x1, host->base + SDIO_CLK_ENABLE);
    udelay(10);

    /* 2. 配置引脚复用 */
    reg_val = readl(host->base + SDIO_PIN_MUX);
    reg_val |= (0x3 << 16);  /* 设置CLK、CMD、DATA0-3为SDIO功能 */
    writel(reg_val, host->base + SDIO_PIN_MUX);

    /* 3. 设置控制器参数 */
    /* 先设成低速模式,时钟分频设为256 */
    writel(0x100, host->base + SDIO_CLK_DIV);

    /* 数据宽度设为4位 */
    reg_val = readl(host->base + SDIO_CTRL);
    reg_val |= SDIO_CTRL_4BIT_MODE;
    writel(reg_val, host->base + SDIO_CTRL);

    /* 使能中断 */
    writel(0xFFFFFFFF, host->base + SDIO_INT_ENABLE);

    return 0;
}

这段代码看起来简单,但有几个细节要注意。时钟分频的初始值,我建议设大一点。为什么?因为设备刚上电时,可能还没准备好。你一下子给高频,设备反应不过来,就会超时。我曾经吃过这个亏,后来学乖了,先低速枚举,再切高速。

小技巧:初始化完成后,可以读一下控制器的状态寄存器,确认时钟是否稳定。有些SoC的时钟稳定需要几个毫秒,别急着往下走。

SDIO设备枚举流程

枚举,就是主机发现并识别SDIO设备的过程。这个过程由硬件自动完成一部分,但驱动也要参与。我把它总结成四个阶段:

阶段 操作 说明
1. 卡检测 检测SDIO卡是否插入 通过GPIO中断或轮询方式
2. 卡初始化 发送CMD0、CMD8、CMD55、ACMD41 获取卡的工作电压和状态
3. 卡识别 发送CMD2、CMD3 获取CID,分配RCA
4. 卡功能选择 发送CMD7、IO_RW_DIRECT 选中卡,读取FBR,配置功能

嗯,这里我重点说一下卡初始化阶段。很多新手在这里卡住。CMD0是复位命令,CMD8是检查电压范围,CMD55告诉设备接下来是应用相关命令,ACMD41才是真正的初始化命令。

ACMD41的响应里有个关键位——busy位。设备初始化没完成时,busy位是0,主机需要不断重发ACMD41,直到busy位变成1。我见过有人写死循环等busy位,结果设备坏了就永远出不来。正确的做法是加超时,比如循环100次,每次等10毫秒。

来看枚举的核心代码:

/* SDIO设备枚举 */
static int sdio_device_enum(struct sdio_host *host)
{
    int retry = 100;
    u32 resp;

    /* 1. 复位卡 */
    sdio_send_cmd(host, CMD0, 0, 0);

    /* 2. 检查电压 */
    sdio_send_cmd(host, CMD8, 0x1AA, &resp);
    if ((resp & 0xFF) != 0xAA) {
        pr_err("电压检查失败\n");
        return -EIO;
    }

    /* 3. 初始化卡,等待busy位 */
    do {
        sdio_send_cmd(host, CMD55, 0, &resp);
        sdio_send_cmd(host, ACMD41, 0x50FF8000, &resp);
        if (resp & (1 << 31))  /* busy位 */
            break;
        mdelay(10);
    } while (--retry);

    if (!retry) {
        pr_err("卡初始化超时\n");
        return -ETIMEDOUT;
    }

    /* 4. 获取CID */
    sdio_send_cmd(host, CMD2, 0, &resp);

    /* 5. 分配RCA */
    sdio_send_cmd(host, CMD3, 0, &resp);
    host->rca = (resp >> 16) & 0xFFFF;

    /* 6. 选中卡 */
    sdio_send_cmd(host, CMD7, host->rca << 16, &resp);

    return 0;
}

枚举成功后,设备就有了一个RCA(相对卡地址)。这个地址很重要,后续所有操作都要带上它。我见过有人忘记保存RCA,结果后面发命令时用0,设备根本不响应。

核心要点:枚举流程中,ACMD41的循环等待是最容易出问题的地方。一定要加超时,而且超时时间要合理。我一般设1秒,100次循环,每次10毫秒。

避坑指南

做SDIO驱动这些年,我踩过不少坑。分享几个典型的:

  • 时钟问题:初始化时时钟频率不能太高。我建议不超过400KHz。等枚举完成、切换到高速模式后,再提升到25MHz或50MHz。
  • 中断处理:SDIO中断是电平触发,不是边沿触发。如果中断处理函数里没清中断标志,会一直触发,导致CPU满载。我遇到过,当时CPU占用率直接飙到100%。
  • 数据超时:SDIO协议有数据超时机制。如果设备没准备好,会拉低DATA线。主机检测到超时后,要重新发送命令。别傻等,该重试就重试。

最后说一句,SDIO驱动调试时,逻辑分析仪是你的好朋友。我曾经调一个时序问题,看了三天代码没找到原因,最后用逻辑分析仪抓波形,发现是板子布线导致信号反射。嗯,硬件问题有时候比软件更头疼。

好了,SDIO接口基础就讲到这里。下一节我们讲SDIO数据传输,包括块传输和字节传输的区别,以及DMA怎么用。到时候我会带大家看一个完整的WiFi数据收发流程。