LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

Linuxカーネルのファイルアクセスの処理を追いかける (21) MMC: mmc_start_host

関連記事

概要

QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。

本章では、MMCサブシステムの初期化処理について確認した。

はじめに

ユーザプロセスはファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。

調査対象や環境などはPart 1: 環境セットアップを参照。

注意

一部の仕様書は非公開となっているため、公開情報からの推測が含まれています。そのため、内容に誤りが含まれている恐れがります。

host の起動

MMCホストコントローラドライバ PL180/1 の probe処理の後半で ホストコントローラの初期化処理のために mmc_add_host関数が呼ばれる。 初期化処理の最後に ホストコントローラを起動させるために mmc_start_host関数を呼び出す。

// 2252:
void mmc_start_host(struct mmc_host *host)
{
    host->f_init = max(min(freqs[0], host->f_max), host->f_min);
    host->rescan_disable = 0;

    if (!(host->caps2 & MMC_CAP2_NO_PRESCAN_POWERUP)) {
        mmc_claim_host(host);
        mmc_power_up(host, host->ocr_avail);
        mmc_release_host(host);
    }

    mmc_gpiod_request_cd_irq(host);
    _mmc_detect_change(host, 0, false);
}

mmc_start_host関数では、ホストコントローラのパラメータを持つmmc_host構造体を用いて、電源シーケンスの制御やカード検出シーケンスを(遅延)実行する。

この時、特定のコントローラ (rtsx_usb_sdmmcなど) では、初期化時間が長くブート時間に影響を与えるものもある。 そういったコントローラには、 MMC_CAP2_NO_PRESCAN_POWERUP フラグを付けて、起動を遅延させることがある。

git.kernel.org

今回の PL180/1ドライバではそういった考慮が必要ないため、ifブロック文の mmc_power_up関数によって起動シーケンスに入る。

コントローラの占有・解放

起動シーケンスでは、mmcホストコントローラの資産を利用する(コマンド発行など) ために排他制御する必要がある。 mmc_claim_host関数は、mmcホストコントローラが占有されているかどうかを検出する。

// 130:
static inline void mmc_claim_host(struct mmc_host *host)
{
    __mmc_claim_host(host, NULL, NULL);
}

git.kernel.org

mmc_claim_host関数は互換性維持のために用意されたラッパ関数であり、実態は __mmc_claim_host関数となっている。

// 780:
int __mmc_claim_host(struct mmc_host *host, struct mmc_ctx *ctx,
             atomic_t *abort)
{
    struct task_struct *task = ctx ? NULL : current;
    DECLARE_WAITQUEUE(wait, current);
    unsigned long flags;
    int stop;
    bool pm = false;

    might_sleep();

    add_wait_queue(&host->wq, &wait);
    spin_lock_irqsave(&host->lock, flags);
    while (1) {
        set_current_state(TASK_UNINTERRUPTIBLE);
        stop = abort ? atomic_read(abort) : 0;
        if (stop || !host->claimed || mmc_ctx_matches(host, ctx, task))
            break;
        spin_unlock_irqrestore(&host->lock, flags);
        schedule();
        spin_lock_irqsave(&host->lock, flags);
    }
    set_current_state(TASK_RUNNING);
    if (!stop) {
        host->claimed = 1;
        mmc_ctx_set_claimer(host, ctx, task);
        host->claim_cnt += 1;
        if (host->claim_cnt == 1)
            pm = true;
    } else
        wake_up(&host->wq);
    spin_unlock_irqrestore(&host->lock, flags);
    remove_wait_queue(&host->wq, &wait);

    if (pm)
        pm_runtime_get_sync(mmc_dev(host));

    return stop;
}
EXPORT_SYMBOL(__mmc_claim_host);

__mmc_claim_host関数はメインコントローラが占有できるまで Wait Queue の仕組みを使って休止状態で待ち続ける関数となっている。

git.kernel.org

また、占有されたホストコントローラは、mmc_release_host関数によって解除し、他のプログラムに ホストコントローラの占有権を譲渡することができる。

// 828:
void mmc_release_host(struct mmc_host *host)
{
    unsigned long flags;

    WARN_ON(!host->claimed);

    spin_lock_irqsave(&host->lock, flags);
    if (--host->claim_cnt) {
        /* Release for nested claim */
        spin_unlock_irqrestore(&host->lock, flags);
    } else {
        host->claimed = 0;
        host->claimer->task = NULL;
        host->claimer = NULL;
        spin_unlock_irqrestore(&host->lock, flags);
        wake_up(&host->wq);
        pm_runtime_mark_last_busy(mmc_dev(host));
        if (host->caps & MMC_CAP_SYNC_RUNTIME_PM)
            pm_runtime_put_sync_suspend(mmc_dev(host));
        else
            pm_runtime_put_autosuspend(mmc_dev(host));
    }
}

コントローラの電源 On

ホストコントローラが占有でき次第、 mmc_power_up関数によって電源 On のシーケンスに移る。
この時、電源が安定する前にクロックを有効にすべきではないので、いくつかのステップに分けて実施する。

MMCによる電力供給のステップ

  1. クロックが動作していない状態でカードに電力を供給する。
  2. 電源が安定するまで待つ。
  3. バスドライバとクロックを有効にする

mmc_power_up関数の定義は次のようになっている。

// 1320:
void mmc_power_up(struct mmc_host *host, u32 ocr)
{
    if (host->ios.power_mode == MMC_POWER_ON)
        return;

    mmc_pwrseq_pre_power_on(host);

    host->ios.vdd = fls(ocr) - 1;
    host->ios.power_mode = MMC_POWER_UP;
    /* Set initial state and call mmc_set_ios */
    mmc_set_initial_state(host);

    mmc_set_initial_signal_voltage(host);

    /*
    * This delay should be sufficient to allow the power supply
    * to reach the minimum voltage.
    */
    mmc_delay(host->ios.power_delay_ms);

    mmc_pwrseq_post_power_on(host);

    host->ios.clock = host->f_init;

    host->ios.power_mode = MMC_POWER_ON;
    mmc_set_ios(host);

    /*
    * This delay must be at least 74 clock sizes, or 1 ms, or the
    * time required to reach a stable voltage.
    */
    mmc_delay(host->ios.power_delay_ms);
}

mmc_pwrseq_pre_power_on関数やmmc_pwrseq_post_power_on関数は、特定のコントローラのために MMC の power sequence の一部にコールバック処理を挟む仕組みの一つである。

git.kernel.org

ホストコントローラによっては、電源投入時のシーケンス動作に特別な処理が必要なモジュール (Marvell SD8787など) がある。 PL180/1 ドライバでは、そういった処理が不要であるため、ここでは割愛する。

その後、mmc_set_initial_state関数とmmc_set_initial_signal_voltage関数によって、ホストコントローラの初期設定を実施する。

まず、mmc_set_initial_state関数の定義を確認する。

// 974:
void mmc_set_initial_state(struct mmc_host *host)
{
    if (host->cqe_on)
        host->cqe_ops->cqe_off(host);

    mmc_retune_disable(host);

    if (mmc_host_is_spi(host))
        host->ios.chip_select = MMC_CS_HIGH;
    else
        host->ios.chip_select = MMC_CS_DONTCARE;
    host->ios.bus_mode = MMC_BUSMODE_PUSHPULL;
    host->ios.bus_width = MMC_BUS_WIDTH_1;
    host->ios.timing = MMC_TIMING_LEGACY;
    host->ios.drv_type = 0;
    host->ios.enhanced_strobe = false;

    /*
    * Make sure we are in non-enhanced strobe mode before we
    * actually enable it in ext_csd.
    */
    if ((host->caps2 & MMC_CAP2_HS400_ES) &&
         host->ops->hs400_enhanced_strobe)
        host->ops->hs400_enhanced_strobe(host, &host->ios);

    mmc_set_ios(host);

    mmc_crypto_set_initial_state(host);
}

e-MMC は JEDECによって規格化されており、2024年現在では バージョン5.1までアップデートされている。 ここでは、HS400モードCommand Queue Engine (CQE) に着目する。

  • HS400モード: e-MMC 5.0 からサポートされた 400MB/secの転送速度に対応した仕様 (mmc-spi-slot)
  • CQE: e-MMC 5.1からサポートされた コマンドにキューに入れ適切な順番で実行するための機構 (mmc-hs400-enhanced-strobe)

その後、mmc_set_ios関数から MMCホストコントローラに依存したデータ速度、mmc フェーズ、電力モード、データ バス幅などを設定する。 mmc_set_ios関数の定義は次のようになっている。

// 884:
static inline void mmc_set_ios(struct mmc_host *host)
{
    struct mmc_ios *ios = &host->ios;

    pr_debug("%s: clock %uHz busmode %u powermode %u cs %u Vdd %u "
        "width %u timing %u\n",
         mmc_hostname(host), ios->clock, ios->bus_mode,
         ios->power_mode, ios->chip_select, ios->vdd,
         1 << ios->bus_width, ios->timing);

    host->ops->set_ios(host, ios);
}

今回の環境では、mmci_set_ios関数によって設定する。 本稿では、この関数まで追うことは割愛するが、設定後に mmc_set_initial_signal_voltage関数によって電源への供給を試みる。

mmc_set_initial_signal_voltage関数の定義は次のようになっている。

// 1159:
void mmc_set_initial_signal_voltage(struct mmc_host *host)
{
    /* Try to set signal voltage to 3.3V but fall back to 1.8v or 1.2v */
    if (!mmc_set_signal_voltage(host, MMC_SIGNAL_VOLTAGE_330))
        dev_dbg(mmc_dev(host), "Initial signal voltage of 3.3v\n");
    else if (!mmc_set_signal_voltage(host, MMC_SIGNAL_VOLTAGE_180))
        dev_dbg(mmc_dev(host), "Initial signal voltage of 1.8v\n");
    else if (!mmc_set_signal_voltage(host, MMC_SIGNAL_VOLTAGE_120))
        dev_dbg(mmc_dev(host), "Initial signal voltage of 1.2v\n");
}

mmc_set_initial_signal_voltage関数では、3.3V, 1.8V, 1.2V を初期値として設定を試みる。 設定のためにmmc_set_signal_voltage関数を呼び出す。

mmc_set_signal_voltage関数の定義は次のようになっている。

// 1143:
int mmc_set_signal_voltage(struct mmc_host *host, int signal_voltage)
{
    int err = 0;
    int old_signal_voltage = host->ios.signal_voltage;

    host->ios.signal_voltage = signal_voltage;
    if (host->ops->start_signal_voltage_switch)
        err = host->ops->start_signal_voltage_switch(host, &host->ios);

    if (err)
        host->ios.signal_voltage = old_signal_voltage;

    return err;

}

mmc_set_signal_voltage関数では、ホストコントローラ特有の start_signal_voltage_switchを呼びだす。 PL180/1ドライバの場合には、mmci_sig_volt_switch関数を呼び出す。

ここではその詳細を割愛するが、start_signal_voltage_switch関数によって Vqmmc のレギュレータを有効化する。

Card Detect の GPIO 検出

mmc_start_host関数では、Card Detect用のGPIOのために mmc_gpiod_request_cd_irq関数を呼び出す。 mmc_gpiod_request_cd_irq関数の定義は次のようになっている。

// 88:
void mmc_gpiod_request_cd_irq(struct mmc_host *host)
{
    struct mmc_gpio *ctx = host->slot.handler_priv;
    int irq = -EINVAL;
    int ret;

    if (host->slot.cd_irq >= 0 || !ctx || !ctx->cd_gpio)
        return;

    /*
    * Do not use IRQ if the platform prefers to poll, e.g., because that
    * IRQ number is already used by another unit and cannot be shared.
    */
    if (!(host->caps & MMC_CAP_NEEDS_POLL))
        irq = gpiod_to_irq(ctx->cd_gpio);

    if (irq >= 0) {
        if (!ctx->cd_gpio_isr)
            ctx->cd_gpio_isr = mmc_gpio_cd_irqt;
        ret = devm_request_threaded_irq(host->parent, irq,
            NULL, ctx->cd_gpio_isr,
            IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
            ctx->cd_label, host);
        if (ret < 0)
            irq = ret;
    }

    host->slot.cd_irq = irq;

    if (irq < 0)
        host->caps |= MMC_CAP_NEEDS_POLL;
}

PL180/1 ドライバでは、 mmci_probe関数がすでに実施しているため処理は不要である。 そこで host->slot.cd.irq には、"デバイスまたはアドレスが存在しない" ENXIOを設定する。

mmc のスキャン

その後、_mmc_detect_change関数でカードの検出を試みる。
_mmc_detect_change関数の定義は次のようになっている。

// 1401:
void _mmc_detect_change(struct mmc_host *host, unsigned long delay, bool cd_irq)
{
    /*
    * Prevent system sleep for 5s to allow user space to consume the
    * corresponding uevent. This is especially useful, when CD irq is used
    * as a system wakeup, but doesn't hurt in other cases.
    */
    if (cd_irq && !(host->caps & MMC_CAP_NEEDS_POLL))
        __pm_wakeup_event(host->ws, 5000);

    host->detect_change = 1;
    mmc_schedule_delayed_work(&host->detect, delay);
}

_mmc_detect_change関数では、カードが取り外されているかどうかのフラグ detect_change と、mmc_schedule_delayed_work関数によって検出ロジックに移る。

mmc_schedule_delayed_work関数の定義は次のようになっている。

// 64:
static int mmc_schedule_delayed_work(struct delayed_work *work,
                     unsigned long delay)
{
    /*
    * We use the system_freezable_wq, because of two reasons.
    * First, it allows several works (not the same work item) to be
    * executed simultaneously. Second, the queue becomes frozen when
    * userspace becomes frozen during system PM.
    */
    return queue_delayed_work(system_freezable_wq, work, delay);
}

mmc_schedule_delayed_work関数では、Work Queue でmmc_rescan関数を遅延実行させることで、カードの検出を開始する。

おわりに

本記事では、カーネル起動時に呼び出される mmc_start_host関数について確認した。

変更履歴

  • 2024/02/23: 記事公開

参考文献