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控制器初始化
控制器初始化,是驱动开发的第一步。我习惯把它分成三个步骤:
- 时钟使能:先给SDIO控制器提供时钟。很多SoC的时钟是默认关闭的,需要手动打开。
- 引脚配置:配置SDIO相关的GPIO引脚,设置成SDIO功能模式。
- 控制器参数设置:设置时钟分频、数据宽度、中断使能等。
来看一段实际的初始化代码,这是我之前在海思平台上调的:
/* 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;
}
这段代码看起来简单,但有几个细节要注意。时钟分频的初始值,我建议设大一点。为什么?因为设备刚上电时,可能还没准备好。你一下子给高频,设备反应不过来,就会超时。我曾经吃过这个亏,后来学乖了,先低速枚举,再切高速。
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,设备根本不响应。
避坑指南
做SDIO驱动这些年,我踩过不少坑。分享几个典型的:
- 时钟问题:初始化时时钟频率不能太高。我建议不超过400KHz。等枚举完成、切换到高速模式后,再提升到25MHz或50MHz。
- 中断处理:SDIO中断是电平触发,不是边沿触发。如果中断处理函数里没清中断标志,会一直触发,导致CPU满载。我遇到过,当时CPU占用率直接飙到100%。
- 数据超时:SDIO协议有数据超时机制。如果设备没准备好,会拉低DATA线。主机检测到超时后,要重新发送命令。别傻等,该重试就重试。
最后说一句,SDIO驱动调试时,逻辑分析仪是你的好朋友。我曾经调一个时序问题,看了三天代码没找到原因,最后用逻辑分析仪抓波形,发现是板子布线导致信号反射。嗯,硬件问题有时候比软件更头疼。
好了,SDIO接口基础就讲到这里。下一节我们讲SDIO数据传输,包括块传输和字节传输的区别,以及DMA怎么用。到时候我会带大家看一个完整的WiFi数据收发流程。