@Vengineerの戯言 : Twitter
SystemVerilogの世界へようこそ、すべては、SystemC v0.9公開から始まった
はじめに
Apple M1 Mac mini でLinuxでUSBメモリ経由でブートしていましたが、下記のツイートのように、先週、SSD(NVMe)からブートできるようになったようです。
Look ma, no dongle! Linux on the Apple M1 now supports booting from NVMe. The instructions on the Corellium blog have been updated. Stellar effort by the whole @CorelliumHQ team. pic.twitter.com/ghYxmhBSaI
— Chris Wade (@cmwdotme) 2021年1月28日
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 で行うようです。
物理的な形状としては、
- 標準サイズのPCI Expressの拡張カード型
- 2.5インチのカード型で、U.2コネクター(SFF-8639)を通じて4レーンのPCI Expressインターフェースを内部に持つ、2.5インチフォームファクタのもの
- M.2コネクタのもの
の3種類がありますが、一般的には M.2 コネクタのものが多いですね。
ウィキペディアには、iOSでは、
>>iPhone 6Sと6S Plusの発売でAppleはスマートフォンにNVMe over PCIeを採用した初のモバイル展開を発表した。Appleはこれらの発売に続いて、同じくNVMe over PCIeを使用するiPad ProとiPhone SE を発売した<< ともあります。Apple M1は、iPhoneやiPad用の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つ載っています。
下の写真は、FIXITのiPhone 12 and 12 Pro Teardownからの引用で、オレンジの四角で囲まれている部分がSSD(NVMe)です。この基板は Apple A14 Bionic が載っていて、SSD(NVMe)は裏側に載っています。
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 },