SDIO SDCard热插拔支持

MMC - SDCard热插拔

热插拔的定义

热插拔一般是指某个部件在插入或拔出主体时,能够让主体感知到。在sdcard热插拔这块,也就是指sd card插入和拔出时,linux内核都能检测卡的状态,收到通知并做出响应的反馈。

热插拔的实现方式

热插拔的实现方式一般有两种:

  • 轮询
  • 中断

Linux内核中具体的实现方式

linux内核实现了MMC Core,MMC Core在如下情况下会去扫描mmc硬件总线:

  • Probe host时,调用_mmc_detect_change,唤醒host->detect;
  • cd-gpio中断检测到card状态发送变化,调用mmc_detect_change,唤醒host->detect;
  • host要求轮询sd card插入状态的情况下,所进行的轮询操作;

Probe host时

probe host时就已经会解析设备树中指定的cd-gpios了,

alt text

内核已经实现了mmc_gpio_cd_irqt回调,gpio中断触发时调用mmc_detect_change函数,该函数负责唤醒detect延时任务,detect在初始化时被绑定为:

1
2
3
dw_mci_init_slot(host);
mmc_alloc_host(sizeof(struct dw_mci_slot), host->dev);
INIT_DELAYED_WORK(&host->detect, mmc_rescan);

mmc_rescan中负责检测mmc card

alt text

cd-gpio中断触发时

底层硬件发现card插入状态发生变化而调用mmc_detect_change的时候(sd card插入状态监控)。

当底层硬件发现card插入状态发生变化,例如sd card的插入或者拔出可以触发某个GPIO产生中断。此时,可以在中断处理中调用mmc_detect_change来进行扫描mmc硬件总线,并且根据总线上的card状态变化进行处理。
这种情况的主要目的是为了实现sd card的热插拔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static irqreturn_t mmc_gpio_cd_irqt(int irq, void *dev_id)
{
/* Schedule a card detection after a debounce timeout */
struct mmc_host *host = dev_id;
struct mmc_gpio *ctx = host->slot.handler_priv;

host->trigger_card_event = true;

// 调用mmc_detect_change对card的插入状态变化进行处理
// 注意,这里ctx->cd_debounce_delay_ms=200,延时了200ms是用来进行消抖
mmc_detect_change(host, msecs_to_jiffies(ctx->cd_debounce_delay_ms));

return IRQ_HANDLED;
}

轮询检测

host要求轮询sd card插入状态的情况下,所进行的轮询操作(sd card插入状态监控)。一般来说,在host无法根据硬件来及时获取card插入状态发生变化的情况下,会要求mmc_core每隔一段时间(一般是HZ,一秒)扫描一次mmc硬件总线。

在这种情况下,mmc_host的MMC_CAP_NEEDS_POLL属性会被设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void mmc_rescan(struct work_struct *work)
{
struct mmc_host *host =
container_of(work, struct mmc_host, detect.work);
int i;

......

out:
// 当host设置了MMC_CAP_NEEDS_POLL属性时,需要每隔HZ的时间轮询检测host的卡槽状态,
// 调度了host->detect工作,对应就是mmc_rescan
// INIT_DELAYED_WORK(&host->detect, mmc_rescan)
// 这样,通过mmc_schedule_delayed_work(&host->detect, HZ)就会每隔HZ时间就会执行一次mmc_rescan
if (host->caps & MMC_CAP_NEEDS_POLL)
mmc_schedule_delayed_work(&host->detect, HZ);
}

如何扫描mmc硬件总线

从上述,我们知道了可以通过调用mmc_detect_change和执行host->detect工作来发起mmc硬件总线的扫描。而mmc_detect_change最终也是执行mmc_host->detect工作来发起mmc硬件总线扫描的。

下面描述detect任务的初始化过程和唤醒时机

detect task的创建

1
2
3
4
5
6
7
8
9
struct mmc_host *mmc_alloc_host(int extra, struct device *dev)
{
......
// 初始化detect工作为mmc_rescan,
// 后续调度host->detect来检测是否有card插入时,
// 就会调用到mmc_rescan
INIT_DELAYED_WORK(&host->detect, mmc_rescan);
......
}

detect task的唤醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void mmc_detect_change(struct mmc_host *host, unsigned long delay)
{
_mmc_detect_change(host, delay, true);
}
EXPORT_SYMBOL(mmc_detect_change);

void _mmc_detect_change(struct mmc_host *host, unsigned long delay, bool cd_irq)
{
/*
* If the device is configured as wakeup, we prevent a new sleep for
* 5 s to give provision for user space to consume the event.
*/
if (cd_irq && !(host->caps & MMC_CAP_NEEDS_POLL) &&
device_can_wakeup(mmc_dev(host)))
pm_wakeup_event(mmc_dev(host), 5000);

host->detect_change = 1; // 检测到card状态发生变化的标识
mmc_schedule_delayed_work(&host->detect, delay); // 间隔delay jiffies之后调用host->detect的工作(mmc_rescan)
}

detect task(mmc_rescan)的实现

当mmc core检测到一张card并且初始化该card完成时,会将该mmc_bus于该card绑定,相应的mmc_bus的操作mmc_bus_ops就会被设置成对应card类型的操作集。

例如emmc的话就是mmc_ops,sd card的话就是mmc_sd_ops,sdio card就是mmc_sdio_ops。

mmc_rescan主要是进行以下两种操作:

  • 当mmc_host的mmc_bus_ops没有被设置的时候
      这种情况下,说明mmc_bus并没有和card绑定。也就是之前并没有card插入或者识别初始化card的过程中失败了
      所以只需要去判断当前是否有card插入,如果有的话,进行card的识别和初始化操作(并且会设置mmc_bus_ops)。否则,说明都不做。

  • 当mmc_host的mmc_bus_ops被设置的时候
      这种情况下,说明mmc_bus已经和card绑定了。也就是之前已经有card插入并且初始化成功了
      那么此时,需要调用mmc_bus_ops中的detect方法(对于sd card来说就是mmc_sd_detect)来判断card是否被移除,并且进行相应的动作。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
void mmc_rescan(struct work_struct *work)
{
struct mmc_host *host =
container_of(work, struct mmc_host, detect.work);
int i;

/* 如果rescan_disable被设置,说明host此时还禁止rescan */
if (host->rescan_disable)
return;

/* If there is a non-removable card registered, only scan once */
/* 对于设备不可移除的host来说,只能rescan一次 */
if (!mmc_card_is_removable(host) && host->rescan_entered)
return;
host->rescan_entered = 1;

if (host->trigger_card_event && host->ops->card_event) {
mmc_claim_host(host);
host->ops->card_event(host);
mmc_release_host(host);
host->trigger_card_event = false;
}

mmc_bus_get(host);// 获取host对应的bus

/* Verify a registered card to be functional, else remove it. */
if (host->bus_ops && !host->bus_dead)
host->bus_ops->detect(host);

host->detect_change = 0;

/*
* Let mmc_bus_put() free the bus/bus_ops if we've found that
* the card is no longer present.
*/
mmc_bus_put(host);
// 因为在这个函数的前面已经获取了一次host,
// 可能导致host->bus_ops->detect中检测到card拔出之后,
// 没有真正释放到host的bus,所以这里先put一次
// host bus的计数(bus_refs)为0的时候,会调用__mmc_release_bus清空host bus的信息
mmc_bus_get(host);

/* if there still is a card present, stop here */
if (host->bus_ops != NULL) {
mmc_bus_put(host);
goto out;
}

/*
* Only we can add a new handler, so it's safe to
* release the lock here.
*/
mmc_bus_put(host);

mmc_claim_host(host);
if (mmc_card_is_removable(host) && host->ops->get_cd &&
host->ops->get_cd(host) == 0) {
mmc_power_off(host);
mmc_release_host(host);
goto out;
}

// 调用mmc_rescan_try_freq,以支持的最低频率作为工作频率尝试搜索card
for (i = 0; i < ARRAY_SIZE(freqs); i++) {
if (!mmc_rescan_try_freq(host, max(freqs[i], host->f_min)))
break;
if (freqs[i] <= host->f_min)
break;
}
mmc_release_host(host);

out:
// 当host设置了MMC_CAP_NEEDS_POLL属性时,需要每隔HZ的时间轮询检测host的卡槽状态,
// 调度了host->detect工作,对应就是mmc_rescan
if (host->caps & MMC_CAP_NEEDS_POLL)
mmc_schedule_delayed_work(&host->detect, HZ);
}

sd card热插拔状态的获取

获取方式主要有两种:

  • gpio中断
  • host本身具有检测卡状态的寄存器

gpio中断实现热插拔

mmc core解析gpio中断,在Probe host一节的代码路径里可以看到,这依赖dts中描述cd-gpios节点:
一个配置案例:

alt text

注意,这里必须要引用gpio控制器(带有gpio-controller描述的节点)

host本身具有检测卡状态的寄存器

忽略

一次sd card热插拔适配异常问题的定位和解决经历

内核启动时,sd card初始化成功,但是热插拔时,sd card初始化失败, 所以将drivers\mmc\core\core.c中定义DEBUG宏,对比启动时的初始化log和热插拔时的log

左 - 内核启动时log
右 - 热插拔时的log

alt text

为了理解上面的指令序列:

查询spec sdio-spce-v3:

An SDIO aware host sends CMD5 prior to the CMD55/ACMD41 pair, and thus would receive a valid OCR in
the R4 response to CMD5 and continue to initialize the card. Figure 3-2 shows the operation of an SDIO
aware host operating in the SD modes and Figure 3-3 shows the same operation for a host that operates in
the SPI mode.

在初始化正常的log里,CMD55/CMD41指令对协商工作电压,sd card返回的R4类型的回复,bit24为0,而热插拔的log中CMD41的R3返回的bit24为1

alt text

R4类型的回复中,bit24为S18R,

alt text

看样子是sdhci开始切换1.8v了,所以右边的log里会在CMD41获取到OCR bit24为1后开始执行CMD11。目前我们的原型平台还不支持1.8v工作。

正常的初始化序列是:

alt text

为了屏蔽此处的电压切换,走读代码寻找CMD11的发送位置:

alt text

临时让mmc_set_uhs_voltage在入口处就返回:

alt text

再次启动内核,热插拔sd card,初始化可以成功!