Vengineerの戯言

人生は短いけど、長いです。人生を楽しみましょう!

Apple M1 で SSD(NVMe)から Linux がブートできるようになったので、NVMe関連を眺めてみた

@Vengineerの戯言 : Twitter
SystemVerilogの世界へようこそすべては、SystemC v0.9公開から始まった 

はじめに

Apple M1 Mac miniLinuxUSBメモリ経由でブートしていましたが、下記のツイートのように、先週、SSD(NVMe)からブートできるようになったようです。

Corelliumのブログが更新されていました。Apple M1 Mac mini だけでなく、Macbook Air/Macbook Pro でも USBメモリおよびSSD(NVMe)でブートできるようになったようです。
corellium.com

今日のブログでは、SSD(NVMe)関連について見ていきます。

NVMe とは

NVMe の仕様書は、ここ で公開されている。最新版は 1.4b です。

NVMe とは、ウィキペディア によると、

PCI Express (PCIe) を通じて、不揮発性ストレージメディアを接続するための論理デバイスインターフェースの規格であり、AHCIに代わる次世代の接続プロトコル規格である。

とあります。Apple M1 と SDD の接続は、PCIe で行うようです。
物理的な形状としては、

の3種類がありますが、一般的には M.2 コネクタのものが多いですね。

ウィキペディアには、iOSでは、
>>iPhone 6Sと6S Plusの発売でAppleスマートフォンにNVMe over PCIeを採用した初のモバイル展開を発表した。Appleはこれらの発売に続いて、同じくNVMe over PCIeを使用するiPad ProとiPhone SE を発売した<< ともあります。Apple M1は、iPhoneiPad用のAxxシリーズと基本的には互換性があるので、NVMe over PCIe を使っているのだと思います。

Apple M1 Mac mini/Macbook Air/Macbook Pro では、M.2 コネクタではなく、銀色のパッケージが2つ、基板上に載っています。
下の写真は、Apple M1 MacBook teardowns reveal surprises からの引用で、Apple M1 Macbook Pro の基板です。M1の左側に銀色のパッケージが2つ載っています。

https://zdnet1.cbsistatic.com/hub/i/r/2020/11/20/4c59a1c0-d558-462f-a3a3-9a3d2008e6b3/resize/1200xauto/0c45e4c9aa534195daac2fc7cfb49b6a/2020-11-20-08-11-01-2.jpg

下の写真は、FIXITのiPhone 12 and 12 Pro Teardownからの引用で、オレンジの四角で囲まれている部分がSSD(NVMe)です。この基板は Apple A14 Bionic が載っていて、SSD(NVMe)は裏側に載っています。

https://d3nevzfk7ii3be.cloudfront.net/igi/VC2EIMTYPUdYIacu.medium

Apple M1 の NVMe関連

NVMe関連のデバイスドライバは次の3つです。

それぞれ、見ていきましょう。

NVMe over PCIe デバイスドライバ (apple,nvme-m1)

dts ファイルの中での NVMe 関連は以下のところです。
ans の compatible のところが、apple,nvme-m1 になっています。
apple,nvme-m1 の device_type は、"pci" ですので、NVMe over PCIe ということになるのだと思います。

        ans: ans@27bcc0000 {
            compatible = "apple,nvme-m1";
            reg = <0x2 0x7bcc0000 0x0 0x40000  /* NVMe + Apple regs */
                   0x2 0x7bc50000 0x0 0x4000>; /* SART regs */
            interrupts = <0 590 4>;
            clocks = <&pcie_st_clk>;
            mboxes = <&ans_mbox 32>;

            #address-cells = <3>;
            #size-cells = <2>;
            #interrupt-cells = <1>;
            device_type = "pci";
            msi-controller;
            msi-parent = <&ans>;
            ranges = <0x02000000   0x0 0x7bc00000   0x2 0x7bc00000 0x0 0x00100000>;
            bus-range = <0x00 0x01>;
        };
    };

apple,nvme-m1 の デバイスドライバは、drivers/pci/controller の下の pcie-apple-m1-nvme.c になります。
このファイルの最初に以下のようなコメントがあります。Apple M1 には、ANS という coprocessor が居るようです。だから、apple,nvme-m1 のインスタンスの名前が ans になっています。

/*
 * Quick explanation of this driver:
 *
 * M1 contains a coprocessor, called ANS, that fronts the actual PCIe
 * bus to the NVMe device.  This coprocessor does not, unfortunately,
 * expose a PCIe like interface.  But it does have a MMIO range that is
 * a mostly normal NVMe BAR, with a few quirks (handled in NVMe code).
 *
 * So, to reduce code duplication in NVMe code (to add a non-PCI backend)
 * we add a synthetic PCI bus for this device.
 */

ans のレジスタ空間は、下記のように2個です。1番目のレジスタ空間は NVMe と Apple 専用のレジスタ、2番目のレジスタ空間は SART レジスタです。割り込みは1つ、クロックは pcie_st_clk、メールボックスは ans_mbox が 32 個付いています。

            reg = <0x2 0x7bcc0000 0x0 0x40000  /* NVMe + Apple regs */
                   0x2 0x7bc50000 0x0 0x4000>; /* SART regs */
            interrupts = <0 590 4>;
            clocks = <&pcie_st_clk>;
            mboxes = <&ans_mbox 32>;

クロックの pcie_st_clk は、下記のように、pcie_clk => ans_clk => pcie_st_clk になっていますので、大本は PCIe のクロックです。

        ans_clk: ans_clk@23b7003f0 {
            compatible = "apple,pmgr-clk-gate";
            #clock-cells = <0>;
            reg = <0x2 0x3b7003f0 0x0 0x8>;
            clocks = <&pcie_clk>;
            clock-output-names = "ans_clk";
        };

        pcie_st_clk: pcie_st_clk@23b700418 {
            compatible = "apple,pmgr-clk-gate";
            #clock-cells = <0>;
            reg = <0x2 0x3b700418 0x0 0x8>;
            clocks = <&ans_clk>;
            clock-output-names = "pcie_st_clk";
        };

メールボックスは、apple,iop-mailbox-m1 です。先ほどの ANS のところにあった coprocessor を意味するのか、iop というキーワードが付いています。
レジスタ空間が1つ、割り込みは2本、クロックは apple,nvme-m1 と同じ pcie_st_clk です。endpoints というものが 32 になっています。

        ans_mbox: ans_mbox@277400000 {
            compatible = "apple,iop-mailbox-m1";
            reg = <0x2 0x77400000 0x0 0x20000>;
            interrupts = <0 583 4   0 586 4>;
            clocks = <&pcie_st_clk>;

            #mbox-cells = <1>;
            endpoints = <32>;
            wait-init;
        };

apple,nvme-m1 に戻ります。初期化の apple_m1_ans_probe の下記の部分では、PCIe の ホストブリッジを生成しています。

	bridge = devm_pci_alloc_host_bridge(&pdev->dev, sizeof(*ans));
	if(!bridge)
		return -ENOMEM;
	ans = pci_host_bridge_priv(bridge);
	ans->bridge = bridge;
	ans->dev = &pdev->dev;
	platform_set_drvdata(pdev, ans);

下記の部分では、レジスタ空間の割り当てを行っています。レジスタ空間の 0 番では ans->nvme に、1番ではans->sart に ベースアドレスを設定してます。

	for(i=0; i<2; i++) {
		res = platform_get_resource(pdev, IORESOURCE_MEM, i);
		if(!res) {
			dev_err(&pdev->dev, "missing MMIO range %d.\n", i);
			return -EINVAL;
		}
		base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
		if(IS_ERR(base)) {
			err = PTR_ERR(base);
			dev_err(&pdev->dev, "failed to map MMIO %d: %d.\n", i, err);
			return err;
		}
		if(i == 0) {
			ans->nvme = base;
			bus_base = res->start;
		} else
			ans->sart = base;
	}

下記の部分は、メールボックスの設定部分です。rx_callback には、受信した時のコールバック(apple_m1_ans_mbox_msg)を設定しています。

	ans->mbox.dev = ans->dev;
	ans->mbox.rx_callback = apple_m1_ans_mbox_msg;
	ans->mbox.tx_block = true;
	ans->mbox.tx_tout = 500;
	ans->chan = mbox_request_channel(&ans->mbox, 0);
	if(IS_ERR(ans->chan)) {
		err = PTR_ERR(ans->chan);
		if(err != -EPROBE_DEFER)
			dev_err(ans->dev, "failed to attach to mailbox: %d.\n", err);
		return err;
	}

apple_m1_ans_mbox_msgを覗いてみましたが、何もしていませんでした。

static void apple_m1_ans_mbox_msg(struct mbox_client *cl, void *msg)
{
}

下記の部分で、ans の初期化を apple_m1_ans_prepare で行っています。

	err = apple_m1_ans_prepare(ans);
	if(err)
		return err;

apple_m1_ans_prepare では、ANSのメールボックスの起動を mbox_send_messege で行っています。その後は NVMeとApple レジスタの初期化です。

static int apple_m1_ans_prepare(struct apple_m1_ans *ans)
{
	int ret;
	u32 val;
	u64 msg[2] = { -1ull, -1ull };

	ret = mbox_send_message(ans->chan, msg);
	if(ret < 0) {
		dev_err(ans->dev, "ANS mailbox startup failed: %d.\n", ret);
		return ret;
	}

	ret = readl_poll_timeout(ans->nvme + APPLE_BOOT_STATUS, val, (val == APPLE_BOOT_STATUS_OK), 100, 500000);
	if(ret < 0) {
		dev_err(ans->dev, "ANS NVMe startup timed out (0x%x).\n", val);
		return ret;
	}

	ans->basecmd = readl(ans->nvme + APPLE_BASE_CMD_ID) & APPLE_BASE_CMD_ID_MASK;

	writel(APPLE_MAX_PEND_CMDS_VAL, ans->nvme + APPLE_MAX_PEND_CMDS);

	writel(readl(ans->nvme + 0x24004) | 0x1000, ans->nvme + 0x24004);
	writel(APPLE_LINEAR_SQ_CTRL_EN, ans->nvme + APPLE_LINEAR_SQ_CTRL);
	writel(readl(ans->nvme + 0x24008) & ~0x800, ans->nvme + 0x24008);
	/* set command permissions */
	writel(0x102, ans->nvme + 0x24118);
	writel(0x102, ans->nvme + 0x24108);
	writel(0x102, ans->nvme + 0x24420);
	writel(0x102, ans->nvme + 0x24414);
	writel(0x10002, ans->nvme + 0x2441c);
	writel(0x10002, ans->nvme + 0x24418);
	writel(0x10002, ans->nvme + 0x24144);
	writel(0x10002, ans->nvme + 0x24524);
	writel(0x102, ans->nvme + 0x24508);
	writel(0x10002, ans->nvme + 0x24504);

	dev_info(ans->dev, "ANS NVMe startup done, base command %d.\n", ans->basecmd);

	return 0;
}

apple_m1_ans_probeの最後に次のようなコードがあります。bridge->probe_only に true を設定しているので、PCIe デバイスをプローブのみ対応しているようです。このプローブの時に使う PCIe Configuration Spaceへのアクセスは通常のアクセスではなく、bridge->ops に登録したものを使います。

	bridge->probe_only = true;
	bridge->sysdata = ans;
	bridge->ops = &apple_m1_ans_pci_ops;

	err = pci_host_probe(bridge);
	if(err < 0)
		return err;

bridge->ops に登録するのは、以下のapple_m1_ans_pci_ops です。read 時には apple_m1_ans_config_read、write 時には apple_m1_ans_config_write が呼ばれます。

static struct pci_ops apple_m1_ans_pci_ops = {
	.read = apple_m1_ans_config_read,
	.write = apple_m1_ans_config_write,
};

apple_m1_ans_config_read と apple_m1_ans_config_write は次のように定義されています。物理的なレジスタにアクセスするのではなく、デバイスドライバの ans->config というメモリにアクセスしています。

int apple_m1_ans_config_read(struct pci_bus *bus, unsigned int devfn, int where, int size, u32 *val)
{
	unsigned vpci = bus->number;
	struct apple_m1_ans *ans = bus->sysdata;
	unsigned long flags;
	u64 data;


	if(devfn != 0 || vpci >= NUM_VPCI) {
		*val = 0xFFFFFFFF;
		return PCIBIOS_DEVICE_NOT_FOUND;
	}
	*val = 0;
	if((where & (size - 1)) || size > 4)
		return PCIBIOS_BAD_REGISTER_NUMBER;
	if(where >= CFGREG_SIZE || size >= CFGREG_SIZE - size)
		return PCIBIOS_SUCCESSFUL;

	spin_lock_irqsave(&ans->lock, flags);

	data = ans->config[vpci][where >> 3];
	data >>= (where & 7) * 8;
	*val = data & (BIT_MASK(size * 8) - 1);

	spin_unlock_irqrestore(&ans->lock, flags);

	return 0;
}

int apple_m1_ans_config_write(struct pci_bus *bus, unsigned int devfn, int where, int size, u32 val)
{
	unsigned vpci = bus->number;
	struct apple_m1_ans *ans = bus->sysdata;
	unsigned long flags;
	u64 data, mask;

	if(devfn != 0 || vpci >= NUM_VPCI)
		return PCIBIOS_DEVICE_NOT_FOUND;
	if((where & (size - 1)) || size > 4)
		return PCIBIOS_BAD_REGISTER_NUMBER;
	if(where >= CFGREG_SIZE || size >= CFGREG_SIZE - size)
		return PCIBIOS_SUCCESSFUL;

	spin_lock_irqsave(&ans->lock, flags);

	data = val;
	mask = BIT_MASK(size * 8) - 1;
	data <<= (where & 7) * 8;
	mask <<= (where & 7) * 8;
	mask &= apple_m1_ans_config[vpci][(where >> 2) | 1];
	ans->config[vpci][where >> 3] = (ans->config[vpci][where >> 3] & ~mask) | (data & mask);

	spin_unlock_irqrestore(&ans->lock, flags);

	return 0;
}

ans->config は、下記のように apple_m1_ans_config の値が初期値になっています。そして、レジスタ空間の 0番の bridge memory base & limitに、レジスタ空間の 1 番の BAR[0] に bus_base を設定ます。

	for(i=0; i<NUM_VPCI; i++)
		for(j=0; j<CFGREG_SIZE/8; j++)
			ans->config[i][j] = apple_m1_ans_config[i][j * 2];
	/* build bridge memory base & limit */
	ans->config[0][4] |= ((bus_base >> 16) & 0xfff0) | (bus_base & 0xfff00000);
	/* build device BAR */
	ans->config[1][2] |= bus_base;

メールボックスデバイスドライバ(apple,iop-mailbox-m1)

メールボックスデバイスドライバ apple,iop-mailbox-m1 にあります。
マクロ定義の部分で、AP to IOP, IOP to AP というのがあります。IOP が Processor で、APがユーザーアプリケーション側で、今回は、NVMe になる。

/* A2I -> AP to IOP, I2A -> IOP to AP */
#define CPU_CTRL		0x0044
#define   CPU_CTRL_RUN		BIT(4)
#define A2I_STAT		0x8110
#define   A2I_STAT_EMPTY	BIT(17)
#define   A2I_STAT_FULL		BIT(16)
#define I2A_STAT		0x8114
#define   I2A_STAT_EMPTY	BIT(17)
#define   I2A_STAT_FULL		BIT(16)
#define   I2A_STAT_ENABLE	BIT(0)
#define A2I_MSG0		0x8800
#define A2I_MSG1		0x8808
#define I2A_MSG0		0x8830
#define I2A_MSG1		0x8838

このメールボックスに関しては、奥が深そうなので、別途、ブログに書こうと思います。

NVMe デバイスドライバ

NVMe デバイスドライバはもともとあった nvme.h pcie.c を修正しています。なんで修正しているのかな?と思ってました。コースコードを眺めてみたら、どうやら pcie.c の中でいろいろなベンダーのNVMeをサポートするような感じになっているので、修正して使うのは普通のようです。

nvme.h の変更(追加)部分は、下記の部分だけです。

	/*
	 * Apple's SoC ANS2 controller frontend uses a different
	 * scheme for submission queues, where instead of rings they
	 * are arrays and instead of ringing a doorbell one writes
	 * an index into that array into a FIFO-type register.
	 */
	NVME_QUIRK_LINEAR_SQ			= (1 << 16),

pcie.c の変更部分は、こちら です。
nvme.h で追加した部分は、下記の pci_device_id nvme_id_table の .driver_data に設定する値(ビット)の定義のようです。追加した NVME_QUIRK_LINEAR_SQ に対応するように変更しているっぽいです。

	{ PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0xff81),
		.driver_data = NVME_QUIRK_SINGLE_VECTOR |
				NVME_QUIRK_SHARED_TAGS |
				NVME_QUIRK_LINEAR_SQ },

おわりに