SD Express驱动:SOC非hot-plug平台的驱动切换方案

SD Express驱动:SOC非hot-plug平台的驱动切换方案

概述

本文介绍一种适用于不支持PCIe热插拔的嵌入式平台的SD Express卡模式切换方案。

方案背景

传统PC平台的PCIe插槽支持硬件热插拔(Hot-Plug),但嵌入式相机平台通常采用贴片式SD卡槽,不具备热插拔能力。当SD Express卡需要从SD模式切换到PCIe/NVMe高速模式时,存在几秒的链路断开窗口期。本文介绍如何通过软件方式实现这一切换。

整体架构(硬件与驱动流水)

image-20260521175347606
image-20260521175846784

整体架构(软件模块)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
┌─────────────────────────────────────────────────────────────────────────┐
│ 嵌入式平台 SD Express 模式切换架构 │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ Linux Kernel Space │
├─────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ NVMe Driver │ │ bht-sd │ │
│ │ (PCIe模式) │◄──►│ (SD模式) │ │
│ └─────────────┘ └──────┬──────┘ │
│ ▲ │ │
│ │ ▼ │
│ ┌──────┴──────────────────────────┐ │
│ │ SD Express Mode Switch Module │ │
│ │ • PCIe Bus Rescan │ │
│ │ • Runtime PM Control │ │
│ │ • Card Mode Detection │ │
│ └─────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌───────────────┴─────────────────┐ │
│ │ SD Host Controller (PCIe EP) │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ SD Express Card │ │
│ │ [SD Mode] ◄──► [PCIe Mode] │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘

工作模式:
• 模式1: SD协议 → bht-sd SCSI驱动
• 模式2: PCIe/NVMe → 内核NVMe驱动

两种工作模式

SD Express卡支持两种工作模式:

模式 协议 驱动 典型速度
SD模式 SD 4.0/UHS-II bht-sd (自定义) ~200 MB/s
PCIe模式 NVMe 内核nvme ~1 GB/s

核心问题:为什么需要软件介入切换?

PC平台 vs 嵌入式平台

特性 PC平台 嵌入式平台
PCIe插槽 标准化热插拔 贴片式,无热插拔
链路断开 硬件中断通知 软件轮询检测
Root Port 标准ACPI电源管理 定制BSP
模式切换 固件/ACPI处理 需软件模拟

问题分析

当SD Express卡执行SD 7.0协议切换到PCIe模式时:

  1. 链路断开阶段:卡停止响应SD命令,PCIe链路Down
  2. 模式切换阶段:卡内部完成协议切换(1-3秒)
  3. 链路恢复阶段:PCIe重新Link Up

在阶段2和阶段3之间,嵌入式平台的Root Port可能因Runtime PM进入D3状态,导致:

  • 无法访问设备配置空间
  • NVMe驱动无法枚举设备
  • 模式切换失败

关键代码实现

1. 模式切换触发机制

1
2
3
4
5
6
7
8
9
10
// cardinterface.c - 卡初始化函数
#if defined(SD_EXPRESS_NO_HOT_PLUG)
// SD Express PCIe模式初始化成功后,触发软件热插拔流程
if(card->pcie_init_flag == TRUE)
{
// 延迟1秒,等待PCIe链路稳定
schedule_delayed_work(&host->detect, msecs_to_jiffies(1000));
return TRUE;
}
#endif

2. Runtime PM Forbid机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// linux_base.c - 软件热插拔工作队列
void mmc_rescan(struct work_struct *work)
{
sd_host_t *host = container_of(work, sd_host_t, detect.work);
struct pci_dev *pdev = host->pci_dev.pci_dev;
struct pci_bus *bus = pdev->bus;
struct pci_dev *bridge = bus->self; // PCIe Root Port

/* 关键:阻止Root Port进入D3
* 否则在disconnect窗口期设备无法被重新发现
*/
pm_runtime_forbid(&bridge->dev);

// 移除bht-sd设备驱动
bht_pci_remove(pdev);

// 重新扫描PCIe总线,让NVMe驱动接管
bht_pci_rescan(bus);

// 恢复Runtime PM
pm_runtime_allow(&bridge->dev);
}

3. PCI总线重新扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// linux_base.c - PCIe设备重新扫描
static int bht_pci_rescan(struct pci_bus *bus)
{
struct pci_dev *bridge = bus->self;
struct pci_bus *parent = bridge->subordinate;
int rescan_cnt = 10;

pci_lock_rescan_remove();

// 检查设备是否已存在
dev = pci_get_slot(parent, PCI_DEVFN(0, 0));
if (dev) {
pci_dev_put(dev);
goto out;
}

rescan:
// 扫描PCIe设备
num = pci_scan_slot(parent, PCI_DEVFN(0, 0));
if (num == 0) {
msleep(1000); // 等待链路稳定
if(!rescan_cnt--)
goto out;
else
goto rescan; // 最多重试10次
}

// 配置并添加设备
for_each_pci_bridge(dev, parent)
pci_hp_add_bridge(dev);
pci_assign_unassigned_bridge_resources(bridge);
pcie_bus_configure_settings(parent);
pci_bus_add_devices(parent);

out:
pci_unlock_rescan_remove();
return ret;
}

SD模式 → PCIe模式完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─────────────────────────────────────────────────────────────────┐
│ SD Express 模式切换流程 │
└─────────────────────────────────────────────────────────────────┘

┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 阶段1 │ │ 阶段2 │ │ 阶段3 │
│ 链路断开 │────►│ 模式切换 │────►│ 链路恢复 │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ bht-sd驱动 │ │ SD 7.0协议 │ │ Runtime PM │
│ 检测链路断开 │ │ 卡内部切换 │ │ forbid D3 │
└──────────────┘ └──────────────┘ └──────────────┘


┌──────────────────────────────────────┐
│ PCIe Bus Rescan │
│ • pci_scan_slot() │
│ • 内核发现NVMe设备 │
│ • nvme驱动加载 │
│ • 设备挂载 /dev/nvmeXn1 │
└──────────────────────────────────────┘


┌──────────────────────────────────────┐
│ pm_runtime_allow() - 恢复电源管理 │
└──────────────────────────────────────┘

PCIe模式 → SD模式(拔卡场景)

当SD Express卡从PCIe模式拔出时,需要切换回SD模式供下次使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// linux_base.c - 设备移除处理
void bht_sd_remove(struct pci_dev *pdev)
{
bht_sd_slot_t *slot = pci_get_drvdata(pdev);
bht_dev_ext_t *pdx = &slot->data;

// 阻止Runtime PM,防止移除过程中进入D3
bht_sd_runtime_pm_forbid(pdx, &pdev->dev);

// 移除SCSI主机适配器
bht_scsi_uinit(pdx);

// 停止工作线程
if(os_stop_thread(&pdx->os, &pdx->os.thread) == FALSE)
{
os_kill_thread(&pdx->os, &pdx->os.thread);
}

// 释放资源
req_global_uninit(pdx);
bht_sd_pci_release(pdx);

os_memset(slot, 0, sizeof(bht_sd_slot_t));
}

重新插入时的处理

当SD Express卡重新插入时,bht-sd驱动会自动检测并重新初始化:

  1. 卡插入检测(轮询GPIO)
  2. SD协议初始化
  3. 检测SD Express能力
  4. 重复上述模式切换流程

NVMe驱动侧修改

虽然模式切换主要由bht-sd驱动控制,但NVMe驱动侧需要适配嵌入式平台的特殊需求:

1. 禁用热插拔检测

嵌入式平台不需要NVMe驱动进行热插拔检测:

1
2
// 内核配置或驱动参数
nvme_core.no_dev_online = true;

2. 适配延迟枚举

在bht_pci_rescan中,NVMe驱动会接收到新设备,需要适配延迟场景:

1
2
3
// nvme驱动需要在设备就绪后再进行初始化
// 在bht_pci_rescan完成后,等待设备稳定
msleep(500);

3. 设备命名稳定性

确保设备路径稳定,避免/dev/nvme0n1变成/dev/nvme1n1:

1
2
// 使用PCIe BDF作为设备标识符
// nvme驱动自动维护设备名映射

关键数据结构

pcie_init_flag标志

1
2
3
4
5
// card.h
typedef struct {
bool card_support_pcie; // 卡是否支持PCIe
bool pcie_init_flag; // PCIe模式初始化成功标志
} sd_card_t;
  • FALSE:卡不支持PCIe或初始化失败,维持SD模式
  • TRUE:PCIe初始化成功,触发模式切换流程

delayed_work结构

1
2
3
4
// host.h
typedef struct {
struct delayed_work detect; // 延迟工作队列,用于触发rescan
} sd_host_t;

Windows驱动参考

Windows端存在类似问题:某些Intel平台的PCIe Root Port在SD Express切换的disconnect窗口期内进入Runtime D3。

问题表现

1
DeviceIoControl读取寄存器返回0xFFFF/0x0000 → 检测到D3模式

解决方案

  1. INF配置:禁用Runtime D3Cold
1
2
[DriverInstallx64.NT.HW]
Needs = PciD3ColdNotSupported.HW
  1. 驱动代码:在切换前阻止D3
1
2
3
4
5
// 增加设备使用计数,阻止进入D3
StorPortAcquireMinidumpMutex(pdx);

// 切换完成后恢复
StorPortReleaseMinidumpMutex(pdx);

总结

本方案通过软件方式在不支持硬件热插拔的嵌入式平台上实现了SD Express卡的模式切换:

  1. Runtime PM干预:确保切换过程中Root Port保持D0状态
  2. PCIe总线重新扫描:让NVMe驱动接管PCIe模式设备
  3. 延迟工作队列:避免在链路不稳定时进行操作
  4. 完整的模式回退:拔卡后可正常切换回SD模式

这种设计对于采用贴片式SD卡槽、不支持传统PCIe热插拔的嵌入式项目具有重要参考价值。