news 2026/10/4 9:33:59

SD卡初始化流程全解析:从协议命令到uboot源码与实测波形

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SD卡初始化流程全解析:从协议命令到uboot源码与实测波形

第一次在自制板卡上调 SD 卡启动的时候,我拿着逻辑分析仪盯着 CMD 线上的波形看了整整一下午。CMD0 发出去了,卡没有回应;CMD8 发出去,依旧只有主机侧的帧。当时我甚至怀疑是 uboot 的代码没编对,后来把 SD 卡规范、uboot 的 mmc 框架源码和实际波形三者对照着看,才发现问题几乎都出在对协议握手阶段的理解上——不是代码不会跑,而是你根本不知道卡在哪个环节等你。

这篇文章就把 SDIO 接口下 SD 卡初始化的完整流程摊开讲:常用命令(CMD0、CMD8、ACMD41、CMD2、CMD3、CMD7、CMD9、CMD17)的格式与参数意图、uboot 里从 mmc_init 到 mmc_startup 的代码执行链路,以及逻辑分析仪实测波形的判读方法。适合正在调 uboot 启动、移植 bootloader 到新板卡,或者单纯想把 SD 协议栈吃透的嵌入式开发者参考。

1. 为什么初始化是调 SD 卡的第一道坎:先看懂 SDIO 总线与会话模型

1.1 SDIO 总线上到底有哪些信号,驱动关系是什么

SDIO 这个缩写在项目标题里通常指的是 SD 卡赖以工作的总线接口。它最少需要三根线:CLK 时钟、CMD 命令/响应线和一根 DAT0 数据线,完整模式下扩展为 DAT0~DAT3 四根数据线。

CLK 永远是主机输出,SD 卡只负责在时钟沿上采样和输出数据。CMD 和 DAT 线都是半双工双向的,主机发命令时驱动总线,卡返回响应时接管驱动。空闲状态下,CMD 和所有 DAT 线都要被上拉电阻拉到高电平——这个细节非常关键,很多初始化失败就是上拉电阻漏焊或者根本没接。

GPIO 上拉电阻的取值有讲究。常见的是 10kΩ~47kΩ 对 VDD,SD 规范要求的是通信线上的上拉,同时还要注意卡侧的内部上拉有时会和外部的分压。我通常先在原理图上确认 CMD 和 DAT0~DAT3 的 pull-up 都接了,再考虑软件问题。曾经有一块板子 CMD 线只留了 Pad 没贴上拉电阻,uboot 里反复 mmc rescan 都识别不到卡,焊上 10k 电阻立刻正常,这个坑记忆犹新。

SDIO 的时钟速度也不是一开始就能跑很高。卡上电后,主机必须先把时钟设置在 100kHz~400kHz 的识别频率区间,完成整套初始化之后才能升到 25MHz 默认速度或 50MHz 高速。uboot 的主机控制器驱动在初始化时做的第一件事,就是把 SDHCI 时钟分频器设置成 400kHz 左右。如果你看到 CLK 一上来就是几十兆,那大概率是驱动配置有问题而不是协议问题。

1.2 SD 卡的六种关键状态:命令顺序不能乱的根因

SD 卡内部是一个严格的状态机,绝大多数命令只在特定状态下才被接受。我把和初始化最相关的几个状态拎出来:

  • inactive:卡进入永久休眠,只能重新上电才能唤醒。电压不匹配会给到这个状态。
  • idle:CMD0 复位后所处的状态,等待主机发送 ACMD41。
  • ready:ACMD41 初始化完成,卡已就绪,等待主机读取 CID。
  • ident:CID 被主机读取后进入,等待分配 RCA 地址。
  • stby:CMD3 分配完相对地址,卡处于待机状态,未被选中。
  • tran:CMD7 选中后进入传输状态,可以正常读写数据块。

看清楚这张状态流转图就能理解,为什么 CMD2 必须在 ACMD41 成功之后发,CMD7 又必须等 CMD3 拿到 RCA 之后才能发。命令顺序错了,卡会直接返回错误或者干脆不做任何响应。

我在 uboot 里最常见的误操作是:给一张已经被初始化过一次但没断电的卡直接重新发 CMD0 序列。有些卡在非 idle 状态下对 CMD0 的响应并不干净,最稳妥的做法是整卡掉电重新上电,再走一遍完整流程。规范要求的 74 个空时钟周期,本质上就是给卡一个从上电到稳定工作的缓冲时间。

1.3 一张表看懂初始化全过程的命令序列

把整个初始化过程压缩成一张表,后面所有的细节都是围绕这张表展开的:

阶段命令参数要点期望响应作用
复位CMD0 (GO_IDLE_STATE)0x00000000无响应卡回到 idle 状态
电压握手CMD8 (SEND_IF_COND)0x000001AAR7 (echo 0x1AA)确认卡支持 2.7~3.6V
容量/电压协商CMD55+ACMD41HCS + 电压窗口R3 (OCR)初始化卡,声明主机容量支持
读取 CIDCMD2 (ALL_SEND_CID)0x00000000R2 (136 bit)获取卡唯一标识
分配地址CMD3 (SEND_RELATIVE_ADDR)0x00000000R6 (含 RCA)卡给自己分配相对地址
选中卡CMD7 (SELECT_CARD)RCA << 16R1让卡进入 tran 状态
读 CSDCMD9 (SEND_CSD)RCA << 16R2 (CSD)解析容量、速度参数
切位宽CMD55+ACMD60x2R11-bit 切换 4-bit
读 SCRCMD55+ACMD510x0R1 + 数据块确认 SD spec 版本

关注一个容易忽略的细节:ACMD 命令并不是独立的命令编号,而是先发 CMD55(APP_CMD),把下一帧命令标记为"应用专用命令",然后紧接着发对应的命令号。所以 ACMD41 在协议栈里实际是 CMD55 后跟 CMD41,ACMD6 是 CMD55 后跟 CMD6。uboot 代码里发 ACMD41 的地方你一定能看到相邻两个 mmc_send_cmd 调用。

2. 常用命令拆解:命令帧格式、参数设计意图和响应判读

2.1 48 位命令帧与 CRC7:bit-bang 时必须自己算

SD 模式下的命令帧是 48 位,按位序排列如下:

位域长度内容
[47]1 bit起始位,恒为 0
[46]1 bit传输方向,主机->卡为 1
[45:40]6 bit命令索引(CMD0~CMD63)
[39:8]32 bit命令参数
[7:1]7 bitCRC7 校验
[0]1 bit结束位,恒为 1

如果你用的是 SDHCI 控制器(uboot、Linux 里绝大多数场景),CRC7 和结束位由控制器硬件自动生成,软件只需要把命令索引和参数填到 SDHCI 的 COMMAND 寄存器和 ARGUMENT 寄存器。但如果你在做 51 单片机模拟 SDIO 这类位操作实现,CRC7 就必须自己算。

CRC7 的生成多项式是 x^7 + x^3 + 1,覆盖范围为 40 位(命令索引 + 参数)。分享一段实测可用的 CRC7 计算函数,它返回的是 CRC7 左移一位再或上结束位后的最后一字节:

static uint8_t sd_crc7(const uint8_t *data, uint8_t n) { uint8_t crc = 0; for (uint8_t i = 0; i < n; i++) { uint8_t d = data[i]; for (uint8_t j = 0; j < 8; j++) { crc <<= 1; if ((d & 0x80) ^ (crc & 0x80)) crc ^= 0x09; d <<= 1; } } return (crc << 1) | 1; }

拿 CMD0 验证:data 为 {0x40, 0x00, 0x00, 0x00, 0x00},函数返回 0x95,和 SD 规范里 CMD0 的标准帧 0x40 00 00 00 00 95 完全一致。CMD8 参数 0x1AA 时,标准帧是 0x48 00 00 01 AA 87。用这两个帧做自测,如果你的实现结果不同,先检查字节序。

2.2 CMD0/CMD8/CMD55+ACMD41:复位与电压/容量握手的三个动作

CMD0(GO_IDLE_STATE)参数恒为 0,SD 模式下无响应。它的目的是无论卡当前处于什么状态(除非 inactive),都把卡拉回 idle。uboot 的 mmc_go_idle 实现很朴素,发完 CMD0 后 udelay(2000) 等一下让卡完成内部复位:

static int mmc_go_idle(struct mmc *mmc) { struct mmc_cmd cmd; int err; udelay(1000); cmd.cmdidx = MMC_CMD_GO_IDLE_STATE; cmd.cmdarg = 0; cmd.resp_type = MMC_RSP_NONE; err = mmc_send_cmd(mmc, &cmd, NULL); if (err) return err; udelay(2000); return 0; }

CMD8(SEND_IF_COND)是 SD 2.0 协议新增的电压握手。参数低 12 位的设计很巧妙:bit[11:8] 声明主机支持的电压范围(0b0001 表示 2.7~3.6V),bit[7:0] 是一个 0xAA 校验模式。卡如果支持该电压,会把 0x1AA 原样回显在 R7 响应里,主机据此判断双方电压兼容。

uboot 里参数是这样构造的:

cmd.cmdidx = SD_CMD_SEND_IF_COND; cmd.cmdarg = ((mmc->voltages & 0xff8000) != 0) << 8 | 0xaa; cmd.resp_type = MMC_RSP_R7;

mmc->voltages 来自板级配置,默认覆盖 2.7~3.6V。注意响应判读:uboot 只检查响应低 8 位是否等于 0xAA,这已经足够确认"卡活着且电压范围匹配"。如果卡不响应 CMD8,说明这是只支持 SD 1.x 的老卡,后续要跳过 SD 2.0 的逻辑,这个分支代码里也有。

CMD55 + ACMD41是整个初始化过程中唯一的轮询环节。ACMD41 的参数是主机向卡声明两件事:支持哪些电压(bit[23:0]),以及是否支持高容量卡(bit[30] HCS)。卡的 R3 响应里会返回 OCR 寄存器内容,其中 bit[31] 是 busy 位,表示卡是否完成上电初始化;bit[30] CCS 表示卡是不是 SDHC/SDXC。

uboot 的循环逻辑如下:

int sd_send_op_cond(struct mmc *mmc) { int timeout = 1000; int err; struct mmc_cmd cmd; do { /* CMD55 先告诉卡下一个命令是应用命令 */ cmd.cmdidx = MMC_CMD_APP_CMD; cmd.resp_type = MMC_RSP_R1; cmd.cmdarg = 0; err = mmc_send_cmd(mmc, &cmd, NULL); if (err) return err; /* ACMD41:HCS + 电压窗口 */ cmd.cmdidx = SD_CMD_APP_SEND_OP_COND; cmd.resp_type = MMC_RSP_R3; cmd.cmdarg = mmc->cfg->host_caps & MMC_CAP_HC ? OCR_HCS | OCR_VOLTAGE : OCR_VOLTAGE; err = mmc_send_cmd(mmc, &cmd, NULL); if (err) return err; udelay(1000); } while (!(cmd.response[0] & OCR_BUSY) && timeout--); if (timeout <= 0) return -ETIMEDOUT; if (mmc->version != SD_VERSION_2) mmc->version = SD_VERSION_1_0; mmc->high_capacity = ((cmd.response[0] & OCR_CCS) == OCR_CCS); mmc->ocr = cmd.response[0]; return 0; }

OCR_VOLTAGE 在 include/mmc.h 里定义是 0x00FF8000,覆盖 2.7~3.6V。如果你看到 ACMD41 反复发出但 busy 位始终不置 1,优先怀疑卡供电不足、电压窗口参数错误,或者卡已经因为之前的操作进入 inactive 状态。

有个小经验:轮询时不要在 CMD55 和 ACMD41 之间插其他命令。我见过有代码在循环里顺带读卡状态,结果把 ACMD 语义打断,卡迟迟不置 busy,最后超时。CMD55 只对紧随其后的一帧命令生效,这个语义必须严格执行。

2.3 CMD2/CMD3/CMD7:识别卡、分配地址、选中卡

ACMD41 成功之后,卡从 idle 进入 ready 状态。紧接着的 CMD2(ALL_SEND_CID)让所有卡把自己的 CID 寄存器放到 CMD 线上,响应类型是 R2,共 136 位(起始位 + 传输位 + 6 位保留 + 120 位 CID + CRC7 + 结束位)。

uboot 把 R2 响应的 128 位有效数据复制到 mmc->cid 数组:

int mmc_all_send_cid(struct mmc *mmc, u32 *cid) { struct mmc_cmd cmd; cmd.cmdidx = MMC_CMD_ALL_SEND_CID; cmd.resp_type = MMC_RSP_R2; cmd.cmdarg = 0; int err = mmc_send_cmd(mmc, &cmd, NULL); if (err) return err; memcpy(cid, cmd.response, sizeof(u32) * 4); return 0; }

CID 里包含厂商 ID、产品名、序列号和制造日期。调试时通过 mmc info 看到这些信息,能确认卡有没有被正确识别。

CMD3(SEND_RELATIVE_ADDR)是 SD 卡初始化里很有意思的一步:主机不知道卡的地址,卡的 RCA 是它自己生成并返回给主机的。R6 响应的高 16 位就是 RCA。uboot 里取出来存到 mmc->rca:

int mmc_send_relative_addr(struct mmc *mmc) { struct mmc_cmd cmd; cmd.cmdidx = SD_CMD_SEND_RELATIVE_ADDR; cmd.resp_type = MMC_RSP_R6; cmd.cmdarg = 0; int err = mmc_send_cmd(mmc, &cmd, NULL); if (err) return err; mmc->rca = cmd.response[0] >> 16; return 0; }

拿到 RCA 之后,CMD7(SELECT_CARD)把指定地址的卡从 stby 状态拉进 tran 状态。参数是 RCA 左移 16 位,所以 u-boot 里这句 cmd.cmdarg = mmc->rca << 16;。从这一步开始,主机和卡才真正建立"会话"。

需要提一下:CMD3 之后总线上的所有带 RCA 参数的普通命令都要携带正确的 RCA,否则卡不认。调试时看到 CMD7 之后卡返回 error 状态,多半是前面 RCA 解析错了或者某个结构体没更新。

2.4 CMD9/ACMD6/ACMD51:读 CSD、切位宽、读 SCR

进入 tran 状态后,主机需要读 CSD 寄存器来解析容量和速度参数。CMD9(SEND_CSD)参数同样是 RCA << 16,响应 R2。CSD 的解析是容量计算的核心,这里给出两种典型情况:

SDSC 卡(CSD 1.0 版本)容量计算公式:

BLOCK_LEN = 2^READ_BL_LEN MULT = 2^(C_SIZE_MULT + 2) BLOCKNR = (C_SIZE + 1) * MULT 容量 = BLOCK_LEN * BLOCKNR

SDHC/SDXC 卡(CSD 2.0 版本)公式简单很多:

容量 = (C_SIZE + 1) * 512KB

举例:一张 32GB SDHC 卡的 C_SIZE 字段为 0xFFFF,容量 = (65535 + 1) × 512KB = 32 GiB。uboot 里 mmc_read_capacity 就是按这个逻辑解析的,判断依据是之前 ACMD41 响应里的 CCS 位。

ACMD6(SET_BUS_WIDTH)用于把数据线从 1-bit 切换到 4-bit。参数 0b00 表示 1-bit,0b10 表示 4-bit。uboot 的 sd_set_bus_width 先发 CMD55,再发 CMD6(索引 6,参数 2),成功后再调 mmc_set_bus_width 让主机控制器也切到 4-bit 模式。注意 ACMD6 的 CMD55 参数要带 RCA:

cmd.cmdidx = MMC_CMD_APP_CMD; cmd.cmdarg = mmc->rca << 16; cmd.resp_type = MMC_RSP_R1;

ACMD51(SEND_SCR)读取 8 字节的 SCR 寄存器,通过它判断卡支持的 SD spec 版本、是否支持 4-bit、是否支持安全命令。uboot 里 sd_send_scr 在 mmc_startup 中被调用。这块信息对正常启动影响不大,但如果卡是 SD 4.x 以上的新卡,涉及 UHS 模式切换,读 SCR 是判断能力集的前提。

3. uboot 源码走读:从 mmc_init 到 mmc_startup 的执行链路

3.1 入口:mmc_init 的主机控制器准备与 400kHz 时钟

以 uboot 2021.x~2023.x 的 drivers/mmc/mmc.c 为例。系统启动过程中,mmc_initialize() 会遍历注册过的 MMC 主机控制器,逐个调用 mmc_probe,最终进入 mmc_init。这个函数是整个初始化流程的总入口。

mmc_init 的第一件事是 mmc_set_ios,主机控制器驱动会在这里把时钟设置到 400kHz 识别频率。这一步如果没做对,后面所有命令都会失败。我看过一份 i.MX6ULL 的板级代码,时钟树配置错误导致 SD 卡时钟根本没有输出,现象就是逻辑分析仪上 CLK 纹丝不动,uboot 卡在 mmc_init 超时。

3.2 识别阶段三个关键函数在 uboot 里的实现

mmc_init 里真正和协议相关的调用顺序是:

/* Reset the card */ err = mmc_go_idle(mmc); /* Test for SD version 2 */ err = mmc_send_if_cond(mmc); if (IS_SD(mmc)) { err = sd_send_op_cond(mmc, mmc->ocr, &mmc->ocr); ... } err = mmc_startup(mmc);

三个函数分别对应 CMD0、CMD8、ACMD41,前面已经贴了代码。这里补充一个阅读源码的技巧:mmc_init 里对 mmc->version 的判断要理清楚。如果 CMD8 成功,version 被设为 SD_VERSION_2;如果 CMD8 失败(老卡),sd_send_op_cond 会把 version 设为 SD_VERSION_1_0。后面对卡能力的判断(是否支持 4-bit、是否高容量)都会走不同的分支。

很多移植 uboot 的人喜欢直接改 mmc.c,我建议不要动协议层代码,板级差异应该通过 mmc->cfg 的 voltages、host_caps、f_max 等配置项表达。协议层代码经过大量平台验证,改坏的风险远大于收益。

3.3 mmc_startup 里做了哪些收尾动作

mmc_startup 承担 ACMD41 之后的所有识别动作。核心流程如下:

static int mmc_startup(struct mmc *mmc) { int err; /* 1. 读取 CID */ err = mmc_all_send_cid(mmc, mmc->cid); if (err) return err; /* 2. 获取 RCA(仅 SD 卡需要,MMC 卡 RCA 固定为 0) */ err = mmc_send_relative_addr(mmc, &mmc->rca); if (err) return err; /* 3. 选中卡,进入 tran 状态 */ err = mmc_select_card(mmc); if (err) return err; /* 4. 读取 CSD,解析容量和速度 */ err = mmc_send_csd(mmc, &mmc->csd); if (err) return err; if (IS_SD(mmc)) { /* 5. 读 SCR 确认能力集 */ err = sd_send_scr(mmc, &mmc->scr); if (err) return err; /* 6. 切换 4-bit 总线 */ err = sd_set_bus_width(mmc, mmc->card_caps); if (err) return err; } /* 7. 提高时钟到卡支持的传输速度 */ err = mmc_set_clock(mmc, mmc->tran_speed); ... }

注意到一个容易踩的细节:mmc_select_card 发送 CMD7 之后,卡的状态才真正能被后续的读写命令接受。如果 mmc_startup 中途失败,比如 CMD9 响应 CRC 错误,整个 mmc_init 返回错误,uboot 会认为卡不存在。这时候别一上来怀疑 uboot 代码,先用逻辑分析仪确认 CMD7 之后 CMD9 的响应内容里 CSD 数据有没有被正确传输。

3.4 数据读命令 CMD17 到块设备的调用路径

初始化完成后,读写数据就走另一套路径。uboot 命令行执行 mmc read 0x82000000 0x100 1,最终会走到 mmc_bread,再进入 mmc_read_blocks。数据读的核心命令是 CMD17(READ_SINGLE_BLOCK)和 CMD18(READ_MULTIPLE_BLOCK)。

static ulong mmc_read_blocks(struct mmc *mmc, void *dst, lbaint_t start, lbaint_t blkcnt) { struct mmc_cmd cmd; struct mmc_data data; int err; /* CMD17 读单块,CMD18 读多块 */ cmd.cmdidx = blkcnt > 1 ? MMC_CMD_READ_MULTIPLE_BLOCK : MMC_CMD_READ_SINGLE_BLOCK; cmd.resp_type = MMC_RSP_R1; /* 注意:SDSC 用字节地址,SDHC/SDXC 用块地址 */ cmd.cmdarg = mmc->high_capacity ? start : start * mmc->read_bl_len; data.dest = dst; data.blocks = blkcnt; data.blocksize = mmc->read_bl_len; data.flags = MMC_DATA_READ; err = mmc_send_cmd(mmc, &cmd, &data); ... }

这里最关键的是 cmdarg 的计算。SDSC 卡 CMD17 参数是字节地址(block number × 512),而 SDHC/SDXC 卡直接用块地址。很多自行实现 SD 驱动的朋友在 SDHC 卡上把参数乘了 512,导致读到完全错误的位置——uboot 通过 high_capacity 标志区分,这个标志正是来自 ACMD41 响应里的 CCS 位。

数据块本身不在 CMD 线上传输,而是在 DAT0 上(1-bit 模式)或 DAT0~DAT3 上(4-bit 模式)传输。每块数据后有 16 位 CRC 校验。所以当你看到 CMD17 有响应但数据读不出来时,问题往往出在 DAT 线上而不是命令线上。

4. 实测波形:把一次完整初始化过程按命令逐段还原

4.1 抓波形要准备什么,探针和采样率怎么选

逻辑分析仪是调 SD 卡初始化最趁手的工具。我用的方案是 Saleae Logic 16,采样率设 50MS/s,同时抓 CLK、CMD、DAT0 三根线。50MS/s 对 400kHz 识别频率绰绰有余,等初始化完成后时钟升到 25MHz,50MS/s 也还能看清关键沿变化。

探针连接注意几点:夹头要稳,避免在 CMD/DAT 上引入过长裸露导线;所有探针共地,接在板子的 GND 测试点;如果板子空间允许,CLK 和 CMD 分开走线,尽量避免两个探针的线缆互相缠绕,否则高频下串扰会干扰波形判读。

抓触发条件上,推荐把触发设在 CMD 线的下降沿。CMD 空闲是高电平,命令帧一出现就会拉低,这个下降沿是最干净的触发点。不需要等上电瞬间的触发,uboot 反复 mmc rescan 的时候,你随时可以手动触发抓一帧完整序列。

4.2 从上电到进入传输模式的波形特征逐段解读

一次完整初始化的波形在逻辑分析仪上可以分成几个肉眼可辨的段落:

第一段是空白时钟段。CLK 以 400kHz 连续翻转,CMD 线维持高电平,持续至少 74 个时钟周期。这是规范要求的"卡上电稳定时间"。如果这段时钟缺失或者频率远高于 400kHz,卡后续不会响应任何命令。

第二段是 CMD0 单独出现。帧内容是 0x40 00 00 00 00 95,发送后 CMD 线回到高电平,没有响应帧。有的卡需要重复两三次 CMD0 才能稳定进入 idle,这是正常的,不必紧张。

第三段是 CMD8 加上 R7 响应。CMD8 帧 0x48 00 00 01 AA 87 发送后,大约 8 个时钟周期,CMD 线上出现一个方向反转的帧——起始位 0、传输位 0、回显的命令索引 8、参数 0x1AA,最后是 CRC7 和结束位。这个方向反转在波形上表现为:主机发送时 CMD 线先低后高走完 48 位;响应帧同样 48 位,但内容由卡驱动。

第四段是 CMD55/ACMD41 的重复循环。波形上你会看到成对的帧:CMD55(0x77 开头)带 R1 响应,紧接着 ACMD41(0x69 开头)带 R3 响应。R3 的响应内容是 OCR,一开始可能是 0x00FF8000,busy 位(bit31)为 0;循环若干次后变成 0xC0FF8000(SDHC 卡,busy 置位 + CCS 置位)。看到 busy 从 0 变 1,初始化就完成了。

第五段是 CMD2 的 R2 长响应。R2 有 136 位,在波形上是所有响应里最长的帧。Saleae 的 SD 协议解析器能直接把 CID 内容解出来,包括厂商 ID、产品名、序列号。

第六段是 CMD3 和 CMD7。CMD3 的 R6 响应里能看到 RCA 值(比如 0x0001 对应响应 0x00010000)。CMD7 参数是 RCA << 16,响应 R1。到这里,卡进入 tran 状态。

最后一段是位宽和时钟切换。ACMD6 切换 4-bit 模式后,主机把 CLK 从 400kHz 升到 25MHz,同时 DAT0~DAT3 上的数据开始出现。在逻辑分析仪屏幕上,CLK 频率的跳变非常显眼。

4.3 三种异常波形的识别方法

异常波形基本可以通过"有没有命令帧、有没有响应帧、响应内容对不对"来归类。

第一种:有 CLK 和 CMD0,但没有响应帧出现。CMD8 之后一直看不到 R7,ACMD41 也看不到 R3。这种情况往往是卡没进入工作状态,优先查电源、上拉和卡座接触。

第二种:命令帧发出来了,响应帧也有,但响应内容错误。比如 R7 返回的不是 0x1AA,或者 R1 的卡状态位带着 error 标志。多数是 CRC 计算错或者信号质量差导致响应位被采样错。我会把采样率调到最大重新抓,看响应帧的边沿是否存在振铃或过冲,如果有,就在 CMD 线上串联 22Ω~33Ω 电阻改善信号质量。

第三种:ACMD41 的 R3 响应一直在 0x00FF8000 打转,busy 永远不置位。这种我见过最典型的场景是卡电源电压偏低,卡内部 LDO 还没起来;其次是主机把电压窗口参数写错,只声明了某个窄区间,卡认为电压不兼容。前者用示波器看 VDD 波形,后者检查 OCR_VOLTAGE 的定义。

再强调一次:波形上的命令帧内容可以对照 2.1 节的帧格式手工解析。0x40 00 00 00 00 95 这个序列值得背下来,它是判断"uboot 有没有真的把命令发出去"的黄金标准。

5. 初始化失败排查手册:按现象反推根因

5.1 卡对 CMD0 完全无响应时的排查顺序

如果逻辑分析仪上看到 CMD0 已经发出去,但卡没有后续反应,按这个顺序排查:

第一,确认卡供电。用示波器量卡座的 VDD 引脚,3.3V 是否稳定,有没有在初始化瞬间跌落。卡在 ACMD41 上电过程中电流可能突然拉高,如果电源带载能力不足,电压跌落超过几百毫伏就会让初始化失败。我遇到过一次,问题出在卡座旁的 10μF 电容没焊,补上就好了。

第二,确认 CMD 线上拉。规范要求 CMD 线在主机不驱动时为高电平。如果你看到 CMD 线在帧与帧之间出现长时间低电平或者高阻态,极可能是上拉电阻缺失。可以用万用表量 CMD 对地电阻,正常应该在 10k~100k 范围。

第三,确认时钟频率。初始化阶段 CLK 必须在 100kHz~400kHz,超过这个范围很多卡直接忽略。uboot 里如果 f_max 配置错误导致 400kHz 分频计算异常,CLK 会跑到几兆甚至十几兆,卡当然没反应。

第四,换一张卡。听起来像废话,但卡损坏的概率比想象中高,特别是二手拆机卡。uboot 的协议实现经过全世界上亿设备验证,大概率不是 uboot 的问题。

5.2 ACMD41 一直拉不高的常见原因

ACMD41 循环超时基本上集中在三种原因。

电压窗口不匹配最常见。记得检查 sd_send_op_cond 里 cmdarg 的电压位。如果你自定义板子时覆盖了 mmc->cfg->voltages,只声明了 3.0~3.1V(bit[18]),而卡需要 2.7~3.6V 全范围,ACMD41 就会一直不置 busy。uboot 默认的 0x00FF8000 恰好覆盖 2.7~3.6V,不要轻易改小。

卡进了 inactive 状态也常见。如果之前一次初始化因为电压不匹配失败,卡可能已经进入 inactive,CMD0 都叫不醒,必须断电重新上电。这解释了为什么"第一次失败后不管怎么重试都不行,断电再试就好了"。

第三种是 HCS 位和卡实际容量不匹配。主机声明支持高容量(HCS=1),卡响应没有 CCS 位(说明是 SDSC 卡),或者反过来主机没置 HCS,卡却是 SDHC——后一种情况下 SDHC 卡虽然能完成初始化,但后续容量解析会出问题。uboot 的 mmc->high_capacity 是由响应里的 CCS 位决定的,注意确认最终赋值是在 ACMD41 响应之后。

5.3 能识别、读写失败时的 DAT 线与位宽问题

初始化能过、读写失败,问题基本都在数据通道上,和命令通道无关。这时候把逻辑分析仪的探针从 CMD 挪到 DAT0~DAT3 上。

最常见的问题是位宽不一致:初始化阶段永远是 1-bit 模式(只用 DAT0),ACMD6 切换成 4-bit 后,如果硬件上 DAT1~DAT3 没接、焊错、或者虚焊,数据就整个乱掉。表现是 mmc read 返回成功但读出来的数据全是 0xFF 或者 CRC 错误。排查方法是先在设备树或板级配置里强制只走 1-bit 模式(host_caps 去掉 MMC_CAP_4BIT),如果读写恢复正常,问题就是 4-bit 通路硬件。

第二个常见问题是 DAT1 被复用作卡检测。某些板子的 CD 信号和 DAT1 共用了一个引脚,4-bit 模式下会因为信号冲突导致读写失败。解决办法是换一个空闲 GPIO 做卡检测,或者直接用 DAT3 做卡检测(很多卡规范推荐的做法)。

第三个是信号完整性问题。面包板或者飞线调试时,DAT 线太长、CLK 串扰过强,都会导致数据 CRC 错。表现是偶发读写失败,重试可能成功。这个时候给 CLK 和 DAT 线加串联电阻(22Ω~47Ω),把线缩短,问题基本能解决。

5.4 uboot 命令行下快速定位问题的几条命令

uboot 命令行本身提供了很好的调试手段。卡初始化失败时,先在提示符下跑这几条:

mmc list mmc rescan mmc info

mmc list 能看到当前注册的 MMC 主机控制器;mmc rescan 强制重新执行 mmc_init 流程;mmc info 打印卡信息。如果 mmc info 能输出 CID、CSD、容量这些,说明初始化已经通了,问题在后面的 boot 流程。

读数据测试用:

mmc dev 0 mmc read 0x82000000 0 1 md 0x82000000 0x10

mmc read 把 block 0 读到内存,md 查看内存内容。如果读到的是 0xFF 或者一堆无规律数据,对照 5.3 节查数据通道。

还有个容易被忽略的点:uboot 编译时如果开了 CONFIG_MMC_DEBUG,mmc 驱动会打印大量调试信息,包括每条命令的参数和响应值。这个开关在 drivers/mmc/Kconfig 里,遇到难啃的初始化问题可以打开重编,配合逻辑分析仪双管齐下。

我自己的习惯是:先在 uboot 命令行确认 mmc info 正常,再回到 Linux 内核验证,这样能把问题边界划得很清楚。如果 uboot 正常而 Linux 不正常,优先怀疑设备树里的 SD 卡节点配置——pinctrl、bus-width、max-frequency 这几个属性是重灾区。

最后再说一个比较隐蔽的坑:SD 卡对命令时序有一定容忍度,但不同的卡宽容度差异很大。有些杂牌卡对 CMD8 之后的等待时间很敏感,uboot 默认的延时不够时,个别卡就会初始化失败。如果你在 uboot 下反复失败、换一张大牌卡立刻通过,不要急着改代码,先确认是不是卡兼容性问题——这类问题通常通过调整 mmc_go_idle 里的 udelay 参数就能缓解。嵌入式开发里"换卡试试"从来不是偷懒,而是最便宜的二分定位法。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 9:33:36

产品经理的 Claude Code 免费教程——TaoToken 统一 Key 接入概述

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 9:32:38

开源模块化支架openrig:从3D打印到桌面工作站的完整DIY指南

上个月我把桌面上乱七八糟的支架全扔了&#xff0c;换成了一个自己动手组装的 openrig。这个开源支架系统的核心思路很简单&#xff1a;用标准接口把夹持件、延长臂和底座自由组合&#xff0c;手机、微单、平板甚至GoPro都能共用一套设备&#xff0c;再也不用桌面摆三个架子。如…

作者头像 李华
网站建设 2026/10/4 9:31:21

锁相环PLL学习笔记:从架构原理到环路设计的关键要点

如果你在射频、时钟、通信或者数字系统领域工作&#xff0c;基本都会撞上锁相环&#xff08;PLL&#xff09;。我第一次翻开《PLL Performance, Simulation and Design》第四版时&#xff0c;并没有马上觉得它好懂&#xff0c;但越往后看越发现&#xff0c;这本书确实是能把一个…

作者头像 李华
网站建设 2026/10/4 9:29:09

西门子S7-200与显控触摸屏在RO反渗透纯水处理系统中的应用

1. 系统硬件选型与整体架构1.1 为什么中小型RO设备绕不开西门子224XP先交代一下背景。我刚交付的一套RO反渗透纯水处理控制柜&#xff0c;用的就是西门子S7-200 224XP加显控触摸屏这个组合。很多做水处理设备的同行一看这套配置就明白了——这是中小型纯水项目里性价比非常高的…

作者头像 李华
网站建设 2026/10/4 9:28:56

Claude 安装及部署指南:用 TaoToken 统一 Key 打通本地与云端调用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 9:28:07

COSCon‘25青少年开源论坛:从第一次Pull Request到开源成长

1. 青少年开源论坛&#xff1a;为什么说这是今年最值得关注的议程看到 COSCon‘25 青少年开源论坛的议程正式发布&#xff0c;我第一反应是&#xff1a;等了这么多年&#xff0c;终于有人把“青少年参与开源”这件事从口号落到了具体的议程上。作为一个在开源社区混了十来年、也…

作者头像 李华