SD 卡在上电后的完整加载流程:从中断信号到文件系统挂载

SD 卡在上电后的完整加载流程:从中断信号到文件系统挂载

一篇讲清 SD 卡驱动在上电阶段的完整工作链路——从 CPU 如何访问硬件寄存器,到 CD 引脚电平检测、中断向量跳转、识别线程发送的每一条命令,最终到文件系统挂载。全文面向具备 RT-Thread 基础、但尚未深入外设中断与 MMIO 机制的嵌入式开发者。

引言

很多人调试 SD 卡时,只会在 rt_device_find("sd0") 成功后开始读写,却说不清"卡到底是怎么被挂载上去的"。这篇文章不讨论读写性能,聚焦上电时刻驱动从无到有、最终让 /sd/ 可用的完整链路。

整条链路可以拆成四段:

  1. 地基:软件如何"够到"硬件寄存器(MMIO 与寄存器写入)
  2. 搭建:上电时驱动如何被内核自动调用,如何建立 host 与事件对象
  3. 检测:SD 卡插入状态如何被判定(CD 引脚电平、去抖、判位)
  4. 挂载:中断/事件如何驱动识别线程逐条发命令、最终把卡挂载为文件系统

先交代一个容易忽略的前提:卡插入状态与"有没有发生中断"是两回事,这决定了上电与热插拔走的是两条不同的触发路径,后面会专门对比。

开篇先用一段判定流程回答读者最关心的两个问题——“这个中断是什么"以及"已插卡的上电是否会触发中断”:

上电已在位
电平稳定无跳变

插拔边沿跳变

SD 卡插入

上电 or 热插拔?

主动查询 CDETECT

硬件中断 SDIO_INT_CARD_DET

软发事件 rt_event_send

CLIC 向量跳转 → ISR

检测线程 → 加载

核心结论:"中断"指的是 SDIO 控制器的卡检测中断(SDIO_INT_CARD_DET已插卡的上电不走中断,走主动查询,中断留给真正产生边沿的热插拔。


一、地基:软件如何访问硬件寄存器

1.1 内存映射输入输出(MMIO)

CPU 执行读写指令时,它只负责把目标地址放到地址总线上,并通过控制信号声明本次是读还是写。CPU 自身并不区分地址背后是内存、是 CLIC 中断控制器、还是 UART——这个裁决由总线上的一段译码电路完成。

这段电路是地址译码器(Address Decoder):它比较地址总线的高位,判断本次地址落入哪台设备的地址区间,并只对该设备发出有效的片选信号。片选无效的设备,其数据输出处于高阻态,不参与总线响应。

于是就有了内存映射输入/输出(MMIO)的定义:外设寄存器以内存地址的形式暴露给 CPU,软件用普通的读地址、写地址指令即可操作外设。 例如某设备的中断控制器寄存器组被映射在地址空间的某一段,软件通过指针访问它:

#define XXX_BASE  0x????0000UL          /* 中断控制器基地址——具体值由芯片决定 */
#define XXX       ((XXX_Type *)XXX_BASE)

此后 XXX->REG = value 就等价于向那片寄存器写入一个值。

1.2 寄存器与"写寄存器"

寄存器是一组能保存 0/1 的存储单元,硬件上由多个 D 触发器(Flip-Flop) 构成——每个 D 触发器保存 1 bit,8 个即组成一个 8 位寄存器。D 触发器的行为是:在时钟有效沿到来时,把数据输入端的 0/1 抄入并保持;时钟不来则维持原值。

由此,"写寄存器"的定义清晰起来:

写寄存器 = 触发目标 D 触发器的时钟,把数据总线上的值锁存进该存储单元,并让其持续保持。

它是一次状态设定而非一次性的数据传递——写入的 0/1 会一直保持,直到下次被改写。这也解释了为何软件写一次中断使能位后,那个中断会持续可用。

一次完整的"写寄存器"在硬件上按以下顺序发生:

  1. CPU 指令给出地址、数据与写使能标记
  2. 地址译码器按地址选中目标外设并置其片选有效
  3. 目标寄存器的 D 触发器在(片选 ∧ 写使能)共同产生的时钟沿上,锁存数据总线上的值
  4. 锁存值经内部组合逻辑持续控制对应功能(如接通/断开某条中断请求通道)

理解这一点,后面所有"打开中断""清除标志"的代码才不是黑盒。


二、搭建:驱动如何被内核自动加载

2.1 自动初始化机制

RT-Thread 的驱动不是在上电后手动调用的,而是靠自动初始化。内核按固定等级顺序执行 INIT_*_EXPORT 宏注册的所有函数:

初始化等级就绪内容
板级硬件INIT_BOARD_EXPORTGPIO、时钟
组件INIT_COMPONENT_EXPORT内核线程工具
环境INIT_ENV_EXPORT文件系统等组件
应用INIT_APP_EXPORTSD 卡驱动在此执行

SD 卡驱动通过 INIT_APP_EXPORT(rt_hw_sdio_init) 挂入链表,上电到应用阶段时内核自动调用它。

2.2 host 与事件对象

rt_hw_sdio_init 对每个 SDIO 控制器执行 sdio_host_create,其核心工作有两项,分别对应两个关键数据结构:

  • host(主机控制器):即 rt_mmcsd_host 结构体,描述该 SDIO 控制器与 SD 卡的关联。创建过程会分配内存、绑定一组操作回调(发命令、配总线、读卡检测、开关中断),并把 host 与底层硬件句柄 hw_sdio 关联。
  • 事件对象(event):由 rt_event_init(&hw_sdio->event, ...) 创建的内核 IPC 对象,用于跨线程通知。检测线程阻塞等待该事件,ISR 或初始化代码向其发送标志。

它们在源码中的作用可以对应到如下关键行:

/* 创建并启动检测线程,它随后阻塞等待卡检测事件 */
hw_sdio->detect_thread =
    rt_thread_create(name, rthw_mmcsd_detect, host,
                     RT_MMCSD_DETECT_STACK_SIZE, 25, 10);
rt_thread_startup(hw_sdio->detect_thread);

/* 初始化事件对象 */
rt_event_init(&hw_sdio->event, name, RT_IPC_FLAG_PRIO);

/* host 绑定能力:操作回调表 + 基础配置 */
host->ops = &ops;           /* {发命令, 配总线, 读卡检测, 开关中断} */
host->freq_min = 400000;    /* SD 协议规定的启动时钟 */
host->valid_ocr = ...;      /* 支持的电压范围 */

需要强调的是:本阶段只完成资源与能力登记,尚未读写卡片。 真正的检测发生在下一段的 rthw_sd_detect()

上电后从 INIT_APP_EXPORT 到挂载前的执行步骤可概括为下列流程:

上电复位

INIT_APP_EXPORT
rt_hw_sdio_init

创建 host + 事件 + 检测线程

CD 引脚在位?

发送 CARD_DET 事件

使能中断
待热插拔

检测线程 → 加载


三、检测:SD 卡插入状态如何被判定

3.1 CD 引脚与机械开关

SD 卡插座上有一根专门的引脚 CD(Card Detect),它连接插座内的机械行程开关,随插卡动作改变通断:

  • 未插卡:开关断开(开路),由上拉电阻把 CD 引脚保持在高电平
  • 完全插入:卡片边缘压合开关(短路到地),CD 引脚被拉到低电平
  • 半插入:触点反复接通/断开,引脚电平在高、低之间抖动——这正是需要"去抖"的原因。

因此,"卡是否插好"的本质判定,是检测 CD 引脚的电平是否为低

CD 引脚的电平状态由机械开关与上拉电阻共同决定,其判定关系如下:

未插卡
开关断开

上拉
引脚=高

完全插入
压合到地

引脚=低

半插入
触点抖动

高/低抖动

CDETECT=1
无卡

CDETECT=0
有卡

需去抖后判定

3.2 去抖

机械开关的抖动会导致电平在插入瞬间不稳定。驱动通过写 SDIO 控制器的去抖寄存器来过滤:

HAL_SDIO_DEBNCE_SET(&hsdio, Hal_Sys_AhbClkGet() / 50u);  /* 约 20ms */

DEBNCE 寄存器设置了一个约 20ms 的窗口:只有当 CD 引脚电平连续稳定 20ms,才认定该电平有效。 半插入状态下触点持续抖动,无法通过这一判决,从而避免误判。

3.3 判位:读 CDETECT 寄存器

驱动层的 rthw_sd_detect 通过 HAL 函数读取控制器内部的卡检测状态:

/* rthw_sd_detect */
if (Hal_Sdio_CardIsPresent(&hsdio)) {
    ret = 0x01;   /* 卡在位 */
} else {
    ret = 0x00;   /* 无卡 */
}

HAL 层的实现读取的是 CDETECT 寄存器(而非直接读某个通用 GPIO):

rs_uint8_t Hal_Sdio_CardIsPresent(SDIO_HandleTypeDef *hsdio)
{
    rs_uint8_t isPresent = FALSE;
    if ((hsdio != NULL) && (hsdio->Index < HAL_SDIO_INDEX_MAX)) {
        if (0 == Hal_RegRead(SDIO_REG(hsdio->Index, CDETECT), REG_MASK_ALL, 0u)) {
            isPresent = TRUE;   /* CDETECT bit0 = 低 → 卡在位 */
        }
    }
    return isPresent;
}

因此判位逻辑非常简单:CDETECT.bit0 == 0(引脚被拉低)即判定卡在位。 整个判断收敛在 HAL 那一个条件里。

关于"是哪个引脚"的澄清:在该 SoC 上,卡检测走的是 SDIO 控制器自带的 CD 检测通道(CDETECT 寄存器),而非普通 GPIO 的可编程引脚。SD 的 CLK/CMD/DAT 引脚复用交由板级/FPGA 实现,软件层并不配置它们的 PIN。

从插卡到中断的完整信号链——五个环节依次传导,如图:

插卡
开关压合

CD 引脚拉低

去抖 20ms

判位 CDETECT=0

置中断 CARD_DET


四、中断机制:从信号到 ISR

4.1 谁负责中断:RISC-V 的 CLIC

该 SoC 的 CPU 是 RISC-V 内核(T-Head E906),自带 CLIC(Core-Local Interrupt Controller)。所有外设(SDIO、UART、GPIO、DMA、ISP…)的中断请求汇入 CLIC 仲裁后进入 CPU。

CLIC 初始化时,为所有中断源启用向量中断模式

for (i = 0; i < MAX_IRQn; i++) {
    CLIC->CLICINT[i].IP = 0;         /* 清除挂起位 */
    CLIC->CLICINT[i].ATTR |= 1;      /* SHV=1 → 向量跳转 */
}

SHV(Selectable Hardware Vector)位是关键:它让 CLIC 在中断被判定有效后,依据中断源编号直接向 CPU 提供处理函数地址,CPU 据此跳转——不经过软件查表

4.2 中断请求信号的本质

"外设向 CLIC 发送中断请求"在物理上不是发送数据,而是驱动一根专用中断请求线的电平。通常有两种形式:

  • 电平触发:线被持续驱动到有效电平,直到被清除才恢复。
  • 边沿触发:线上产生一次电平跳变(上升沿或下降沿)。

SD 卡检测属于电平锁存型——SD 卡中断标志由软件主动清除(__HAL_SDIO_CLEAR_INT),中断请求线在清除前持续有效。这印证了该信号是电平状态,而非一次性脉冲。

中断信号只承载"有事件发生"这一事实,不携带事件内容。 具体是哪类事件(卡插入、数据传输完成、超时…),CPU 在进入 ISR 后读取中断状态寄存器,逐位判断得出。

4.3 向量跳转:谁"调用"了 ISR

以 SDIO0 为例,SDIO0_IRQHandler 通过 ATTRIBUTE_ISR(即 __attribute__((interrupt("machine"))))声明,让编译器自动生成机器模式中断的现场保存/恢复与 mret 返回:

ATTRIBUTE_ISR void SDIO0_IRQHandler(void)
{
    rt_interrupt_enter();
    if (host[HAL_SDIO_INDEX_0] != NULL)
        rthw_sdio_irq_process(host[HAL_SDIO_INDEX_0]);
    rt_interrupt_leave();
}

SDIO0_IRQHandler 并非由软件函数调用,而是由 CLIC 硬件向量跳转进入。 中断到来时,CLIC 依据 SDIO0 的中断源编号直接让 CPU 跳到该函数地址,CPU 硬件完成现场切换。ISR 内部 rt_interrupt_enter() / rt_interrupt_leave() 只是通知内核当前处于中断上下文。

ISR 内 rthw_sdio_irq_process 的处理极短:读中断状态寄存器 → 清除标志 → 逐位判断事件 → 若是卡检测位则通过 rt_event_send 发送事件标志。ISR 不执行耗时操作,仅负责发信号。

4.4 中断源的规模与函数对应关系

该 SoC 的中断源枚举覆盖了从内核定时器中断到十余类外设中断,数量上百。要澄清的一点是:每个中断源是否拥有专属处理函数,取决于驱动是否实现了同名的强符号。

  • 启动代码(vectors.S)用宏为每个中断源预置 .weak 弱符号,默认指向兜底的 Default_Handler
  • 有驱动实现的(如 SDIO0_IRQHandlerUART0_IRQHandler)以强符号覆盖 weak,成为真正的处理函数。
  • 未实现的中断停在 Default_Handler(检查是否非屏蔽中断,否则进入异常流程)。
  • GPIO 采用分组中断:每 16 根引脚共用一个中断源编号,共 8 个中断处理函数管理全部 GPIO 引脚。进 ISR 后仍需读取引脚状态做进一步细分。

因此"每个中断都有专属函数"并不绝对——架构上每个中断源占一个向量位,真正有专属函数的只有驱动写过的那些

SDIO 外设中断从发生到进入 ISR 的消息时序如下(注意全程由 CLIC 硬件仲裁与向量跳转,非软件分发):

ISRCPU 向量跳转CLIC 中断SDIO 控制器ISRCPU 向量跳转CLIC 中断SDIO 控制器校验 IE + 优先级仲裁(SHV 向量模式)rt_event_send(CARD_DET) 唤醒检测线程驱动中断请求线 置 IP 位提供处理函数地址硬件向量跳转

五、关键区分:上电插入 与 热插拔

这是整条链路最容易混淆的地方。两者最终都触发加载,但触发路径完全不同

维度上电时已插卡使用中热插拔
触发源软件主动查询硬件中断
是否走中断
触发机制创建 host 时主动读 CDETECTCD 引脚边沿 → CLIC 向量跳转
时序前提发生在中断使能之前发生在中断使能之后
后续路径殊途同归:均触发检测线程 → 识别 → 挂载同左

为什么上电时已插卡不触发中断? 因为卡早在上电前就在位,CD 引脚电平是稳定态,没有边沿跳变,CLIC 不会据此产生中断。此时驱动选择在创建 host 的末尾主动调用一次检测

if (rthw_sd_detect(host) == 0x01) {
    rt_event_send(&hw_sdio->event, SDIO_INT_CARD_DET);  /* 卡在位 → 直接发事件 */
}

于是上电走了"主动查询 + 直接发事件"的路径;而 SDIO_INT_CARD_DET 向量中断留给"使用中突然插入/拔出"这类真正的边沿事件。两者最终汇合于同一条加载流程。


六、挂载:从事件到文件系统

加载过程实际由多条线程协作完成——启动线程建立资源、检测线程等待并分发事件、识别线程下发命令。三者通过事件与邮箱传递信号,时序如下:

识别线程检测线程启动线程识别线程检测线程启动线程收到事件,开始加载识别 CMD0/ACMD41 建档dfs_mount("/sd/") 挂载rt_event_send(CARD_DET)mmcsd_change 发邮箱消息mmcsd_hotpluge_mb 通知完成

6.1 检测线程

事件标志 SDIO_INT_CARD_DET 唤醒的就是检测线程 rthw_mmcsd_detect。它一直阻塞在 rt_event_recv 上:

while (1) {
    /* 阻塞等待卡检测事件,无事件时不占用 CPU */
    rt_event_recv(&hw_sdio->event, SDIO_INT_CARD_DET,
                  RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
                  RT_WAITING_FOREVER, NULL);

    hw_sdio->sdio_dev->use_hold_reg = 1;
    mmcsd_change(host);                    /* 通知识别线程 */
    ret = mmcsd_wait_cd_changed(300);      /* 等待识别结果 */

    if ((ret == MMCSD_HOST_PLUGED) && (rthw_sd_detect(host) == 0x01)) {
        dev = rt_device_find("sd0");
        if (dev && (dfs_mount("sd0", "/sd/", "elm", 0, 0) == 0)) {
            LOG_I("sd0 mount to /sd/ !");
            mmcsd_mount_over();
        }
    } else if ((ret == MMCSD_HOST_UNPLUGED) && (rthw_sd_detect(host) == 0x00)) {
        Hal_Sdio_DeInit(&hsdio);           /* 拔卡 → 反初始化 */
    }
}

这里 mmcsd_change 本身不处理识别,它只是向接收邮箱 mmcsd_detect_mb 投递一条消息,真正的识别在识别线程。

6.2 识别线程与命令序列

识别线程 mmcsd_detect 收到邮箱消息后,若当前 host->card == NULL(尚未初始化卡片),则依次下发识别命令。一套识别按 SDIO → SD → MMC 的顺序尝试:

while (1) {
    rt_mb_recv(&mmcsd_detect_mb, &host, RT_WAITING_FOREVER);

    if (host->card == NULL) {
        mmcsd_power_up(host);                    /* 上电 + 延时稳定 */
        mmcsd_go_idle(host);                     /* CMD0 复位,至少 74 个时钟 */

        err = sdio_io_send_op_cond(host, 0, &ocr);  /* 先试 SDIO 卡 */
        if (!err) { if (init_sdio(host, ocr)) ...; continue; }

        mmcsd_go_idle(host);
        err = mmcsd_send_app_op_cond(host, 0, &ocr); /* 再试 SD 卡:CMD55 + ACMD41 */
        if (!err) {
            if (init_sd(host, ocr)) ...          /* 是 SD 卡 → 读 CID/CSD 建档 */
            rt_mb_send(&mmcsd_hotpluge_mb, &host);  /* 通知"卡已插好" */
            continue;
        }

        err = mmc_send_op_cond(host, 0, &ocr);   /* 最后试 MMC 卡 */
        ...
    }
}

识别原理:一张卡无法凭外观判断类型,只能靠下发命令"试探"。SD 卡的关键命令是 ACMD41,其响应携带该卡支持的工作电压范围,据此确认是 SD 卡。init_sd 读取 CID/CSD 并建立块设备 sd0

6.3 挂载

识别完成后,检测线程等待在 mmcsd_wait_cd_changed(300),确认卡在位后执行 dfs_mount("sd0", "/sd/", "elm", 0, 0),把块设备 sd0 以 FAT(elm = Elm-Chan FatFs)文件系统挂载到路径 /sd/。至此,上层代码可通过 open("sd0/...") 读写 SD 卡文件。


七、完整链路与分层设计

将以上四段串起来,整条链路为:

上电复位
  → INIT_APP_EXPORT 自动初始化
  → sdio_host_create:host + 事件对象 + 检测线程
  → 主动读 CD 引脚(CDETECT):卡在位 → 直接发事件
  → 检测线程收到事件 → 发邮箱
  → 识别线程:上电 → CMD0 → 试探 SDIO/SD/MMC → 建档 sd0
  → dfs_mount("sd0", "/sd/", "elm")
  → /sd/ 可读写

热插拔分支(另一张卡):
  → CD 引脚边沿 → CLIC 向量跳转 SDIO0_IRQHandler → 发事件
  → 汇入同上加载流程

值得强调的分层原则是:中断上下文只负责最小操作(读状态、清标志、发事件),真正的耗时工作(上电、逐条命令、读卡识别、文件系统挂载)都在普通线程上下文完成。 这是嵌入式设计中普遍遵循的"中断做最小、耗时进线程"原则,它保证了中断响应及时、系统不被长 ISR 阻塞。

完整流程的"一图流"汇总如下(涵盖上电与热插拔两条路径):

是 上电路径

否 等待插拔

上电复位

自动初始化
创建 host/事件/检测线程

CD 引脚在位?

主动发 CARD_DET 事件

CD 引脚边沿 → 中断

CLIC 向量跳转 → ISR

检测线程唤醒
向识别线程发邮箱

识别 CMD0/ACMD41
建档 sd0

dfs_mount 挂载 /sd/

总结

从上电到 SD 卡可读写,驱动实际经历了四层递进:

  1. 寄存器访问地基——MMIO 与 D 触发器,是一切外设操作的前提;
  2. 资源搭建——host、事件对象、检测线程的建立,只登记能力不动卡片;
  3. 插入检测——CD 引脚电平经去抖后形成 CDETECT 位,是判定卡在位的依据;
  4. 中断与加载——上电走主动查询,热插拔走 CLIC 向量中断,最终由检测线程与识别线程逐条发命令、建档、挂载文件系统。

理解这条链路的关键,在于分清**“电平状态"与"边沿事件”“中断只发信号"与"线程做工作”**这两组对照。搞清它们,再回头阅读 drv_sdio 驱动代码时,就只剩直接对应了。


本文依据实际嵌入式工程中的 SD 卡驱动源码整理,去除了项目代号与平台实现细节,保留通用原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘色的喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值