Vengineerの妄想(準備期間)

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

Apple M1 Mac mini の PCIe 関連を調べてみた

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

はじめに

昨日のブログでは、Apple M1 Mac mini 内の アドレスマップを調べてみました。その中で、PCIe 関連が気になったので、今日は PCIe について調べていきます。

Apple M1 Mac mini の PCIe 関連

dts ファイル では、次の部分が PCIe 部分になります。
デバイスドライバは、apple,pcie-m1 で、ここ にあります。

        pcie: pcie@690000000 {
            compatible = "apple,pcie-m1";
            reg = <0x6 0x90000000 0x0 0x1000000                                                                 /* config */
                   0x6 0x80000000 0x0 0x100000  0x6 0x8c000000 0x0 0x100000                                     /* core and AXI bridge */
                   0x6 0x81000000 0x0 0x20000   0x6 0x82000000 0x0 0x20000   0x6 0x83000000 0x0 0x20000>;       /* ports */
            interrupt-parent = <&aic>;
            interrupts = <0 695 4   0 698 4   0 701 4                                                           /* state */
                          0 704 1   0 705 1   0 706 1   0 707 1   0 708 1   0 709 1   0 710 1   0 711 1         /* MSI */
                          0 712 1   0 713 1   0 714 1   0 715 1   0 716 1   0 717 1   0 718 1   0 719 1
                          0 720 1   0 721 1   0 722 1   0 723 1   0 724 1   0 725 1   0 726 1   0 727 1
                          0 728 1   0 729 1   0 730 1   0 731 1   0 732 1   0 733 1   0 734 1   0 735 1>;
            clocks = <&pcie_gp_clk &pcie_clk &pcie_refclk>;
            clock-names = "core", "aux", "ref";
            pinctrl-0 = <&pcie_clkreq_pins>;
            pinctrl-names = "default";
            perst-gpios = <&gpio 152 0   &gpio 153 0   &gpio 33 0>;
            clkreq-gpios = <&gpio 150 0   &gpio 151 0   &gpio 32 0>;
            devpwr-gpios = <&smc 13 0>;
            devpwr-on-0 = <0 1>;
            devpwr-on-1 = <>;
            devpwr-on-2 = <>;
            #address-cells = <3>;
            #size-cells = <2>;
            #interrupt-cells = <1>;
            device_type = "pci";
            msi-controller;
            msi-parent = <&pcie>;
            msi-doorbell = <0x0 0xfffff000>;
            ranges = <0x43000000   0x6 0xa0000000   0x6 0xa0000000 0x0 0x20000000
                      0x02000000   0x0 0xc0000000   0x6 0xc0000000 0x0 0x40000000>;
            bus-range = <0x00 0x0f>;
            iommu-map = <0x0000 &pcie_dart0 0x8000 0x0100>, /* fake, and should never be used as RC bridges don't DMA */
                        <0x0100 &pcie_dart0 0x0000 0x0100>,
                        <0x0200 &pcie_dart1 0x0000 0x0100>,
                        <0x0300 &pcie_dart2 0x0000 0x0100>;
            refclk-always-on-2;
            max-speed-2 = <1>; /* 2.5 GT/s */
        };

アドレス空間は、この部分で、全部で6個です。最初の 0x6_9000_0000 からの 16MB は PCI Configuration 空間です。2番目の 0x6_8000_0000 からの 1MB と 0x6_8c00_0000 からの1MBが PCIe RootComplexのコアと AXI bridge 回路用です(もしこれが本当なら 内部バスは、AXI なんですね)。最後の3つはポートで、0x6_8100_0000, 0x6_8200_0000, 0x6_8300_0000 でそれぞれ128KBの空間です。

            reg = <0x6 0x90000000 0x0 0x1000000                                                                 /* config */
                   0x6 0x80000000 0x0 0x100000  0x6 0x8c000000 0x0 0x100000                                     /* core and AXI bridge */
                   0x6 0x81000000 0x0 0x20000   0x6 0x82000000 0x0 0x20000   0x6 0x83000000 0x0 0x20000>;       /* ports */

次の2つは、割り込み関連です。interrupt-parent は、PCIe から出る割り込み線がどこに繋がっているかを示しています。aic に繋がっています。
割り込み線は、全部で35本出ています。最初の3個(0 695 4 0 698 4 0 701 4)は、この回路の割り込み用です。残りの32個は、MSI用の割り込み線です。

            interrupt-parent = <&aic>;
            interrupts = <0 695 4   0 698 4   0 701 4                                                           /* state */
                          0 704 1   0 705 1   0 706 1   0 707 1   0 708 1   0 709 1   0 710 1   0 711 1         /* MSI */
                          0 712 1   0 713 1   0 714 1   0 715 1   0 716 1   0 717 1   0 718 1   0 719 1
                          0 720 1   0 721 1   0 722 1   0 723 1   0 724 1   0 725 1   0 726 1   0 727 1
                          0 728 1   0 729 1   0 730 1   0 731 1   0 732 1   0 733 1   0 734 1   0 735 1>;

aic は、apple,aicアドレス空間 (0x2_3b10_0000 から32KB)で、fast-ipi オプションが設定されています。

        aic: interrupt-controller@23b100000 {
            compatible = "apple,aic";
            #interrupt-cells = <3>;
            interrupt-controller;
            reg = <0x2 0x3b100000 0x0 0x8000>;
            fast-ipi;
        };

MSIは、下記の部分です。msi-doorbel は、ドアベル用の64ビットアドレスでここでは、0x_ffff_f000 を設定しています。

            msi-controller;
            msi-parent = <&pcie>;
            msi-doorbell = <0x0 0xfffff000>;

下記の ranges の部分は、アドレス変換です。

  • 1番目のエントリは、non-prefetchable 32-bit アドレス(0x4300_0000)、64-bitアドレス(0x6_0xa000_0000)は、0x6_a000_0000 から512MBに
  • 2番目のエントリは、non-prefetchable 32-bit アドレス(0x0200_0000)、64-bitアドレス(0x0_0xc000_0000)は、0x6_c000_0000 から1GBに

マッピングされます。

            ranges = <0x43000000   0x6 0xa0000000   0x6 0xa0000000 0x0 0x20000000
                      0x02000000   0x0 0xc0000000   0x6 0xc0000000 0x0 0x40000000>;

次は、クロック部分です。3つのクロックが接続していて、core, aux, ref という名前が付いています。

            clocks = <&pcie_gp_clk &pcie_clk &pcie_refclk>;
            clock-names = "core", "aux", "ref";

3つのクロック、pcie_gp_clk, pcie_clk, pcie_refclk は、次のような接続になっています。refclk100mhz (100MHzのクロックから)、pcie_refclk と imx_clk に接続しています。この pcie_reflk と imx_clk は、apple,pmgr-clk-gate が示すように、Gated Clock です。ここでクロックを止めることができます。pcie_refclk は、pcie に接続されています。imx_clk は、pcie_clk に繋がっています。ここでも apple,pmgr-clk-gate なので Gated Clock になります。この pcie_clk も pcie に接続しています。そして、pcie_gp_clk を経由して、pcie と pcie_dart0, 1, 2 に接続しています。pcie_gp_clk も Gated Clockになります。Gated Clockにすることで使っていない場合は、この部分を Off にすればいいだけです。Apple M1 Mac mini は PCIe を使っていますが、Apple M1 Macbook AirApple M1 Macbook Pro では PCIe を使っていないのでこの大本となる pcie_refclk と pcie_clk を Off にしていると思います。

  • refclk100mhz
    • pcie_refclk (apple,pmgr-clk-gate)
      • pcie
    • imx_clk (apple,pmgr-clk-gate)
      • pcie_clk (apple,pmgr-clk-gate)
        • pcie
        • pcie_gp_clk (apple,pmgr-clk-gate)
          • pcie
          • pcie_dart0
          • pcie_dart1
          • pcie_dart2

次の perst-gpios は、PCIe の PERST の制御に使うものだと思います。3つありますので、ポートはやっぱり、3ポートなんでしょうね。

            perst-gpios = <&gpio 152 0   &gpio 153 0   &gpio 33 0>;

次の clkreq-gpios は、クロックに関係する何かなんではないでしょうか?

            clkreq-gpios = <&gpio 150 0   &gpio 151 0   &gpio 32 0>;

gpio の 150, 151, 32 は、gpio:pinctrl にも、pcie_clkreq_pins: pcie_clkreq_pins とあります。

        gpio: pinctrl@23c100000 {
            compatible = "apple,gpio-v0";
            reg = <0x2 0x3c100000 0x0 0x100000>;
            pin-count = <212>;

            interrupts = <0 190 4   0 191 4   0 192 4   0 193 4   0 194 4   0 195 4   0 196 4>;

            clocks = <&gpio_clk>;

            gpio-controller;
            #gpio-cells = <2>;

            interrupt-controller;
            #interrupt-cells = <2>;

            i2c0_pins: i2c0_pins {
                pins = "gpio192", "gpio188";
                function = "periph";
            };

            pcie_clkreq_pins: pcie_clkreq_pins {
                pins = "gpio150", "gpio151", "gpio32";
                function = "periph";
            };
        };

次の devpwr-gpios と devpwr-on-X は電源関連だと思います。

            devpwr-gpios = <&smc 13 0>;
            devpwr-on-0 = <0 1>;
            devpwr-on-1 = <>;
            devpwr-on-2 = <>;

下記のコードにあるように、ポート番号(0, 1, 2) の内、0のみ、<0 1> を設定しています。ポート番号0のみ電源をONにするのでしょうか?

		sprintf(name, "devpwr-on-%d", i);
		ret = of_property_count_elems_of_size(node, name, 8);
		pcie->devpwron[i].num = ret;
		if(ret <= 0)
			continue;
		pcie->devpwron[i].seq = devm_kzalloc(dev, ret * 8, GFP_KERNEL);
		if(!pcie->devpwron[i].seq)
			return -ENOMEM;
		ret = of_property_read_variable_u32_array(node, name, pcie->devpwron[i].seq, ret * 2, ret * 2);
		if(ret < 0)
			return ret;

bus-range は、PCIe の バス番号は 0x00から0x0f、iommu-map は、pcie_dart0,1,2 に繋がっています。

            bus-range = <0x00 0x0f>;
            iommu-map = <0x0000 &pcie_dart0 0x8000 0x0100>, /* fake, and should never be used as RC bridges don't DMA */
                        <0x0100 &pcie_dart0 0x0000 0x0100>,
                        <0x0200 &pcie_dart1 0x0000 0x0100>,
                        <0x0300 &pcie_dart2 0x0000 0x0100>;
            refclk-always-on-2;
            max-speed-2 = <1>; /* 2.5 GT/s */

refclk-always-on-2 と max-speed-2 は、PCIe のデバイスドライバ のこの部分でパースしています。refclk-always-on-2 は、2の部分をパースしています。

		sprintf(name, "refclk-always-on-%d", i);
		pcie->refclk_always_on[i] = of_property_read_bool(node, name);

パースした値がポート番号なので、2番目のポートの REFCLK を設定しています。

	if(!pcie->refclk_always_on[idx])
		rmwl(PORT_REFCLK_CGDIS, 0, pcie->base_port[idx] + PORT_REFCLK);
	rmwl(PORT_APPCLK_CGDIS, 0, pcie->base_port[idx] + PORT_APPCLK);

また、max-speed-2 も 2 の部分をパースしています。この部分はポート番号はポート2に 1 (2.5GT/s、Gen1)を設定しています。設定していない場合はデフォルト値の 2 (5.0GT/s、Gen2)のようです。

		sprintf(name, "max-speed-%d", i);
		if(of_property_read_u32(node, name, &pcie->max_speed[i]) < 0)
			pcie->max_speed[i] = 2;

結果的に、ポート0には電源を供給し、ポート0はGen2、ポート1はGen2、ポート2はGen1に設定しているようです。ポート2をGen1にしているのは接続しているデバイスがGen1対応のみだからでしょうか?

終わりに

Apple M1 Mac mini の dts ファイルから PCIe 関連を調べてみました。
dts ファイルについてもっと知りたいのなら、下記の平田豊さんの「私はどのようにしてLinuxカーネルを学んだか Device Tree編」がいいと思います。Kindle版なら550円とお得です。
www.amazon.co.jp

2021年1月の映画鑑賞

映画好きの戯言

1月:26本、アマゾン100円(5本)

ロスト・イン・マンハッタン 人生をもう一度 (2014) / TIME OUT OF MIND
リチャード・ギアがホームレス役になっているのって、珍しいっす。

スノー・ロワイヤル (2019) / COLD PURSUIT、アマゾン100円
リーアム・ニーソン主演。雪かきって、民間の会社がやるのね。。。

エスケープ・ルーム (2019) / ESCAPE ROOM、アマゾン100円
脱出ゲーム。富豪のお遊び。。。

今日も嫌がらせ弁当 (2018)
お母さん役、篠原涼子だけど、奇麗だよね。妹役は芳根京子(TV版の海月姫)なんだけど、中学生の役は無理あるね。既に20歳過ぎだったので)

ゴーン・ガール (2014) / GONE GIRL
ゴーン・ガール、DVDで一度観たけど、これ凄いわ。。。怖いわ。。。

マネー・ショート 華麗なる大逆転 (2015) / THE BIG SHORT
リーマンショックCDS/CDOの件、CDOをショート(売り)して、26億ドルも儲けたってすごいね。原題の「THE BIG SHORT」だと日本ではさっぱりなんだろうね。

きみはいい子 (2014)
高良健吾が地味な教師役。尾野真千子もいやらしい母親役。。ある意味しょうがないがお友達のお母さん役が池脇千鶴が明るくふるまっていたのが印象だった。

バードマン あるいは(無知がもたらす予期せぬ奇跡) 2014
BIRDMAN OR (THE UNEXPECTED VIRTUE OF IGNORANCE)
マイケル・キートンが残念な役やっている。。。エマ・ストーンが娘役。。

ジュマンジ/ネクスト・レベル (2019) / JUMANJI: THE NEXT LEVEL、アマゾン、100円
前作、DVDで観た記憶あるんだけど、全く覚えていない。なんでだろうか?

ビリーブ 未来への大逆転 (2018) / ON THE BASIS OF SEX、アマゾン、100円
原題のままじゃ、なんのストーリーかわからないのかな
昨年亡くなった合衆国最高裁判所女性判事ルース・ベイダー・ギンズバーグの若き日の物語を描いた伝記ドラマ

イマジネーションゲーム 2018
マチャミがまじめな役をやると、いつもこんな感じ。

チア男子!! 2019
横浜流星だよ。わが青春って感じ。

ダーティ・グランパ (2016) / DIRTY GRANDPA
ロバード・デ・ニーロがやんちゃな、おじいちゃん役だよ。

デアデビル (2003) / DAREDEVIL
子供の頃、事故で薬品が目にかかり失明した子供が親父を殺した犯人に復讐するお話。バットマンにかなり似ているがこちらは人権派の貧乏弁護士。

デッド or キル (2012) WOULD YOU RATHER
簡単にはお金は入らない。結局、お金を入手したが、、、

ゾンビーバー (2014) /ZOMBEAVERS
ビーバーがゾンビになって、湖に遊びに来た若者を襲うというお話。ビーバーのちゃっちさに笑った。

ビジネス・ウォーズ (2015) / UNFINISHED BUSINESS
グランド・イリュージョン」に出ていたデイヴ・フランコがおバカなお兄ちゃん役で出演

喝 風太郎!! 2019
本宮ひろ志の同名コミックス
鶴田真由NPOの代表で裏の顔がホームレスからの搾取だったのが意外だった

(2015)
マ・ドンソク主演、結構、怖かった。

SP 野望篇 (2010)
SP 革命篇 (2011)
堤真一、珍しく、悪役

アンダードッグ 二人の男 (2016) / DERAILED

アサシン クリード (2016) / ASSASSIN'S CREED
原題から過去に戻り、エデンの果実を見つけ出すというお話
博士、マリオン・コティヤールだったんだ。

監視者たち (2013) / COLD EYES
これ、結構面白かった。

映画 暗殺教室 2015
暗殺教室~卒業編~ 2016
ちゃんと観た。殺せんせいの声、二宮だったんだ。

ザ・ピーナッツバター・ファルコン (2019) / THE PEANUT BUTTER FALCON (アマゾン、100円)
エレノア役のダコタ・ジョンソンがお母さんのメラニー・グリフィスにそっくり。

Apple M1 Mac mini内のアドレスマップを確認してみた

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

はじめに

Apple M1 で Linux 動くようになったのは、先週のブログにまとめました。

vengineer.hatenablog.com

Linux では、Kernel (Imageなど)の他に、dts ファイルをコンパイル(バイナリ化)した dtb ファイルを読み込んでブートします。
この dts ファイルには、どのモジュールがどのアドレスにあって、どのくらいの空間(サイズ)を使っているのか?割り込みやその他の情報を設定するファイルであります。dts ファイルで設定された情報から Linux がブート中に各デバイスドライバを呼び出すのに使います。

ARM64用の dts ファイルは、arch/arm64/boot/dts ディレクトリにあります。各ベンダ毎にディレクトリに分かれています。Apple M1 Mac mini 用の dts ファイルは、apple ディレクトリの apple-m1-j274.dts です。j274 が Mac mini の型番か何かになっているんだと思います。

Apple M1 Mac mini のアドレスマップ

apple-m1-j274.dtsから各モジュールのアドレスとサイズを抽出し、どのようになっているかを見てみます。

まとめたのが、こんな感じになりました。

  • memory 0x8 0x00000000 0x2 0x00000000
    • fw_area 0x8 0x00000000 0x0 0x01000000
    • fw_area 0x9 0xd0000000 0x0 0x30000000
    • smpentry 0x8 0x03400000 0x0 0x4000
  • I/O-1 0x2 0x00000000 0x2 0x00000000
    • USB
  • I/O-2 0x5 0x00000000 0x0 0x80000000
    • USB
  • I/O-3 0x6 0x80000000 0x0 0x20000000
    • PCIe Port-0 0x6 0x81000000 0x0 0x20000
    • PCIe Port-1 0x6 0x82000000 0x0 0x20000
    • PCIe Port-2 0x6 0x83000000 0x0 0x20000

メモリは、0x8_0000_0000 からの 8GB です。この dts ファイルでは、8GBモデルになっています。16GBモデルになった場合、このエントリのサイズ部分の 0x2_0000_0000 が 0x4_0000_0000 になるのか、他のアドレス空間に 8GB のメモリがマッピングされるかはこのファイルではわかりません。
メモリの中に、fw_area が 2か所と smpentry が 1か所あります。この3つの領域は、Linuxが使用するメモリから除外されます。

I/O空間は、3つ。0x2_0000_0000 から 8GBの I/O-1、0x5_0000_0000 からの 2GBの I/O-2、0x6_8000_0000 からの 512MBのI/O-3 です。
I/O-1はUSB、I/O-2はUSB、I/O-3はPCIe の空間のようです。I/O-3のPCIe の空間では、3つのポートに対して、128KBを割り当てています。

ここまでは、CPUコアから見たアドレス空間です。下図は、先週のブログにも載せた図です。DMAを行う各モジュールには図のように I/O MMU が付いています。この I/O MMU を使って、論理アドレスから物理アドレス(上記の 0x8_0000_0000 からの8GB) に変換します。

f:id:Vengineer:20210130120045p:plain

dts ファイルでは、USBに関しては usb_dart、PCIeに関しては pcie_dart が I/O MMU になります。

下記は、I/O-1のUSB用のI/O MMU 部です。アドレス空間は2つで、0x3_82f0_0000 から 512KB と 0x3_2f80_0000 からの16KB です。page-bits というものが Page size を意味していて、14とありますので、14ビット、16KBが Page size になります。一般的なLinux (x86-64やarm32, arm64)では、4KB ですが、AppleiOS/iPadOS/最新macOS では Page size が 16KB になっているので、同じになります。

        usb_dart0: usb_dart0@382f00000 {
            compatible = "apple,dart-m1";
            clocks = <&atc0_usb>;
            reg = <0x3 0x82f00000 0x0 0x80000   0x3 0x82f80000 0x0 0x4000>;
            interrupts = <0 781 4>;
            page-bits = <14>;
            sid-mask = <11>;
            sid-remap = <0 1>;
            #iommu-cells = <1>;
        };

下記は、PCIe の I/O MMU 部です。こちらのアドレス空間は、ひとつで、0x6_8100_8000 から 16KB です。page-bits が 14 なので、こちらも Page size は 16KB になります。

        pcie_dart0: pcie_dart0@681008000 {
            compatible = "apple,dart-m1";
            clocks = <&pcie_gp_clk>;
            reg = <0x6 0x81008000 0x0 0x4000>;
            interrupts = <0 696 4>;
            pcie-dart;
            page-bits = <14>;
            sid-mask = <65535>;
            #iommu-cells = <1>;
        };

PCIe の I/O MMU は、3つ、pcie_dart0、pcie_dart1、pcie_dart2、あります。この I/O MMU は、下記の pcie の ports と空いてあるところに接続しているんだと思っています。

        pcie: pcie@690000000 {
            compatible = "apple,pcie-m1";
            reg = <0x6 0x90000000 0x0 0x1000000                                                                 /* config */
                   0x6 0x80000000 0x0 0x100000  0x6 0x8c000000 0x0 0x100000                                     /* core and AXI bridge */
                   0x6 0x81000000 0x0 0x20000   0x6 0x82000000 0x0 0x20000   0x6 0x83000000 0x0 0x20000>;       /* ports */

       途中、省略

            iommu-map = <0x0000 &pcie_dart0 0x8000 0x0100>, /* fake, and should never be used as RC bridges don't DMA */
                        <0x0100 &pcie_dart0 0x0000 0x0100>,
                        <0x0200 &pcie_dart1 0x0000 0x0100>,
                        <0x0300 &pcie_dart2 0x0000 0x0100>;
            refclk-always-on-2;
            max-speed-2 = <1>; /* 2.5 GT/s */
        };

終わりに

Linuxでは、dts ファイルを見ることで、各デバイス(SoC) 内のアドレスだけでなく、割り込みや内部構成がなんとなくわかります。
また、dts ファイルからアドレスマップを作ることで、どんな感じに I/O が割り当てられているのが分かります。

クラッチからシステムのアドレスマップを作る機会はそうそうないです。
昔、アドレスマップを決められる機会がありました。その時はいろいろ悩みました。特に、PCIのメモリ空間とPCI Config空間をどのようにマップすればいいかでした。

Apple M1で Linux をロードするまでのステップはどうなっているのかを調べてみた

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

はじめに

昨日のブログ、「Apple M1でLinuxがnativeに動くようになってきた眺めてみた」では、Apple M1 で Linux が native で動くようになったのをソースコードを眺めてみました。ポイントとなる2点、1)、CPU の enable_methodと2)、割り込みコントローラ(AIC)、について深堀しました。P.S として、macOS (iOS/iPadOS)では、Page sizeが16KBだったので、Linuxではどのようになっているかを調べてみたら、iommu では Page size が 16KB になっているのを確認できました。このことにより、Apple M1 で Linux がまともに動くようになったら、Intel CPU よりも速くなると思います(GPUも対応しないといけないので、Intel CPUのLinuxを超えるにはかなり先になるとは思います)

vengineer.hatenablog.com

今日のブログでは、Linuxが動く前について調べていきます。

ARM64 で Linux が立ち上がるまでのステップ

一般的なARM64なCPUコア(2コア-8コア)を搭載したSoCでは、ARM64なCPUコアは Cortex-AシリーズのCPUです。また、SoCの中には Cortex-AシリーズのCPUだけでなく、Cortex-Mシリーズのマイコンを搭載しています。このCortex-Mシリーズのマイコンは、Cortex-AシリーズのCPUを起動する前の初期設定や LinuxのPSCI(省エネ)の処理などをしています。

このようなSoCで Linux が立ち上がるまでのステップは、次のようになっています。

  • Cortex Mシリーズのマイコンが立ち上がり、いろいろな初期設定をする。この時、Cortex-AシリーズのCPUのリセット後のアクセスするアドレスを設定し、Cortex Mシリーズのマイコンが Cortex-AシリーズのCPU(1コアだけ)をリセットから解除する
  • Cortex AシリーズのCPUコア(1コアだけ)がリセット解除後、設定されたアドレスからプログラムをロードし、次のようなステップで処理を行います。
    • ARM Trauted Firmware (EL1/EL2/EL3) を実行する (EL3が u-Boot をメモリにロードし、u-Boot の先頭アドレスにジャンプする)
    • u-Boot を実行する (u-Boot が Linux:Image と dtb をメモリにロードし、Linux:Imageの先頭アドレスにジャンプする)
    • Linux を実行する

Apple M1 で Linux が立ち上がるまでのステップ

Apple M1では、一般的なARM64なCPUコアを搭載のSoCと違って、

などは利用していません。その代わりに、corellium は、preloader-m1 というプログラムを使って、Linux (Imageとdtb)をメモリにロードして、Linuxを起動しています。

では、preloader-m1 というプログラムは、どのようにして起動されるのでしょうか? preloader-m1 の README.txt には kmutilというコマンドで preloader-m1 (test.machoというファイル)をつぎのようにして、HDに登録するようです。

  kmutil configure-boot -c test.macho -v /Volumes/Macintosh HD/

kmutil コマンドは、Installing a Custom Kernel Extensionに書いてあるようなので、こちらを読んでください。

kmutil コマンドでHDに登録される test.macho は、preloader-m1 の Makefile の中を見てみると、

linux.macho: machopack preboot.bin Image apple-m1-j274.dtb
	./machopack $@ preboot.bin@0x803040000 Image@0x803080000 apple-m1-j274.dtb@0x803060000

のように、macopack コマンドで、preboot.bin、Image、apple-m1-j274.dtb から linux.macho (上の test.macho になるのかな)を生成しています。
macopack コマンドは、このディレクトリの machopack.c から生成するっぽいです。

preloader-m1 の中身

ここでは、preloader-m1 の中身を追っていきます。ただし、preloader-m1 のポイントを見ていきます。

上記の macopack コマンドで linux.macho ファイルにパックした preboot.bin、Image、apple-m1-j274.dtb の中の preboot.bin が Linux (Imageとdtbファイル)を起動するためのプログラムです。preboot.bin は、

preboot.bin: preboot.elf
	$(AA64)objcopy -Obinary $^ $@

preboot.elf: preboot.o preboot-c.o tunable.o dtree-dict.o dtree.o adtree.o libc.o printf.o unscii-16.o preboot.ld
	$(AA64)gcc -Wl,-T,preboot.ld -Wl,--build-id=none -nostdlib -o $@ preboot.o preboot-c.o tunable.o dtree-dict.o dtree.o adtree.o printf.o libc.o unscii-16.o

のように、生成されています。

preboot.bin プログラムは、以下のように、preboot.o (preboot.S) の entry から始まります。

    .align 8
.global entry
entry:
    b start

entry から直ぐに、start にジャンプしています。

start:
    mov x24, x0				<= x0 には、bootargsのアドレス(loader_mainの第3引数)

になっていて、x0 レジスタの値を x24 に設定しています。x0 レジスタの値は、preboot.bin プログラムを起動した時の第一引数の値になります。この第一引数は、下記の iphone_boot_args構造体へのポインタになっています。なんでこのような構造体になっているかはここでは説明しません。

struct iphone_boot_args {
    uint16_t revision;
    uint16_t version;
    uint64_t virt_base;
    uint64_t phys_base;
    uint64_t mem_size;
    uint64_t top_of_kernel;
    struct {
        uint64_t phys, display, stride;
        uint64_t width, height, depth;
    } video;
    uint32_t machine_type;
    uint64_t dtree_virt; 
    uint32_t dtree_size;
    char cmdline[BOOT_LINE_LENGTH];
};

次に、フレームバッファになんかを書き込んでいます。x24 が上記の構造体のポインタなので、x24 + 0x048 の値 [x24, #0x40] が width, x24 + 0x48 の値 [x24, #0x48]が height になり、fill_rect にジャンプしています。

    mov x0, #0
    mov x1, #0
    ldr x2, [x24, #0x40]		<= 上記の width
    ldr x3, [x24, #0x48]		<= 上記の height
    mov w4, #0x0000001f
    bl fill_rect				<= Frame buffer に描画

fill_rect は、次のようになっています。x0 (0), x1 (0), x2 (width), x3(height), w4(0x1f) になっています。x8 に phys, x9 に stride になり、x8 (phys) から始まるメモリに x2(width)*x3(height) の範囲に w4(x12, 0x1f)を書き込んでいます。フレームバッファに、0x1f を書き込んでいるんだと思います。

fill_rect:
    ldr x8, [x24, #0x28]
    ldr x9, [x24, #0x38]
    mov w12, w4
    mul x10, x1, x9
    add x8, x8, x0, lsl #2
    add x8, x8, x10
    mov x10, x3
1:  mov x11, x2
2:  str w12, [x8], #4
    sub x11, x11, #1
    cbnz x11, 2b
    add x8, x8, x9
    sub x8, x8, x2, lsl #2
    sub x10, x10, #1
    cbnz x10, 1b
    ret

fill_rect の後は、UART の初期化 です。

    mov x1, #0x35200000
    orr x1, x1, #0x200000000	<= x1 : 0x2_3520_0000

x1 に設定している 0x2_3520_0000 は、apple-m1-j274.dtsのchosenで指定されている値と同じで、UARTのレジスタのアドレスになります。

   chosen {
        bootargs = "earlycon=apple_uart,0x235200000 console=tty0";
    };

apple-m1-j274.dts の uart0 のアドレスを見ると、0x235200000 になってるので確認できます。

       uart0: serial@235200000 {
            compatible = "apple,uart";
            reg = <0x2 0x35200000 0x0 0x4000>;
            interrupts = <0 605 4>;
            clocks = <&refclk24mhz>;
            clock-names = "refclk";
            index = <0>;
        };

その後に、uartのレジスタの初期設定を行っています。

    mov x0, #3
    str w0, [x1]				<= 0x2_3520_0000に、0x3 (REG_ULCON_WORD_8)を write
    mov x0, #5
    str w0, [x1, #4]			<= 0x2_3520_0004に、0x5 (REG_UCON_TX_MODE_PIO|REG_UCON_RX_MODE_PIO)を write
    str wzr, [x1, #0xc]		<= 0x2_3520_000cに、0x0 を write
    mov x0, #0xc
    str w0, [x1, #28]			<= 0x2_3520_001cに、0xc を write
    mov x0, #0x3
    str w0, [x1, #8]			<= 0x2_3520_0008に、0x3 (REG_UFCON_TXF_RESET|REG_UFCON_RXF_RESET)を write
    mov x0, #0x1
    str w0, [x1, #0xc]		<= 0x2_3520_000cに、0x1 (REG_UMCON_RTS)を write

コメントに追記した () 内のマクロ値は、Linuxのuartドライバ(apple-uart.c)の中で次のように定義しています。

#define REG_ULCON                       0x000
#define REG_UCON                        0x004
#define REG_UFCON                       0x008
#define REG_UMCON                       0x00c
#define REG_UTRSTAT                     0x010
#define REG_UERSTAT                     0x014
#define REG_UFSTAT                      0x018
#define REG_UMSTAT                      0x01c

UARTの初期化の後では、0x2_3d2b_0000 (0x2_3d2b_001c)へに 0x0 を書いているのですが、このアドレスが何かまではこの時点ではわかっていません。

    mov x0, #0x3d2b0000
    orr x0, x0, #0x200000000	<= x0 : 0x2_3d2b_0000
    str wzr, [x0, #0x1c]		<= 0x2_3d2b_001cに、0x0 を write

ここまでにUARTの初期化が終わったので、Imageとdtbをメモリに読み、load_mainにて dtb内の情報から rvbar の値を設定します。

    mov x0, #0x800000000
    orr x0, x0, #0x80000000		<= x0 : 0x8_8000_0000  (Memory)
    sub x0, x0, #16
    mov sp, x0				<= sp : 0x8_7fff_ffe0	 (Stack)
    adr x0, smpentry			<= x0smpentry のアドレスを設定
    add x0, x0, #0x20000		<= x0 : smpentry + 0x2_0000 (dtbが入っている)
    mov x1, x24				<= x1 : x0 (struct iphone_boot_args *bootargs)
    adr x2, smpentry			<= x2 : smpentry
    adr x3, rvbar			<= x3 : rvbar
    bl loader_main			load_main(x0, x1, x2, x3)

loader_main は、preloader-m1/preboot-c.c at main · corellium/preloader-m1 · GitHub で定義されています。

void loader_main(
void *linux_dtb, 
struct iphone_boot_args *bootargs, 
uint64_t smpentry, 
uint64_t rvbar)

loader_mainの最後に次のような感じで Image と linux_dtb をメモリに書き込みます。

    printf("Loader complete, relocating kernel...\n");
    dt_write_dtb(linux_dt, linux_dtb, 0x20000);
    dt_free(linux_dt);

loader_main に戻ってきたら、途中に、ちょこっと何かやっていて、cpuinit にて、CPUコアを初期化します。

    bl cpuinit

CPUコアの初期化が終わったら、いよいよ、Linux にジャンプします。Linux (Image)の先頭アドレスは、x18(0x8_8008_0000)です。Linuxへの第一引数(x0)は、dtbが書き込まれている先頭アドレス(0x8_8006_0000)です。最後に、br x18 で、Linuxにジャンプします。

    mov x0, #0x800000000	<= ここから Linux にジャンプするコード
    orr x0, x0, #0x80000000
    add x0, x0, #0x60000		<= x0 0x8_8006_0000 (dtbのアドレス)
    mov x1, #0				<= x1, x2, x3 は、引数だけど、0を設定
    mov x2, #0
    mov x3, #0
    add x18, x0, #0x20000	<= x18 : 0x8_8008_0000 (Imageにjump)
    br x18					(だたし、Makefileでは、0x803080000 になっている)

おわりに

上記のように、preboot.bin の中で、初期化し、引数として渡された iphone_boot_args の情報から、Linux (Imageとdtb) をメモリにロードし、dtb を読み込んだアドレスを x0 (第一引数)として、Linux(Image)を起動していました。

感想

よくここまで、リバースしたものです。

Apple M1でLinuxがnativeに動くようになったのでソースコードを眺めてみた

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

はじめに、

今日のブログは、今週、Apple M1 (Mac mini)で native な Linux (Ubuntu) が動くようになったという下記のサイトの情報からgithubに公開されたLinuxソースコードを追っていくという作業やっていきます。
www.omgubuntu.co.uk

Apple M1 で native な Linux (Ubuntu)は、どんな感じなのか?

このツイートによると、USB経由で full Ubuntu desktop で立ち上がり、USB dongle でネットワークが動きます。USB、I2C、DART (DARTってなんだろう)が動くようです。

github に公開されている linuxソースコードは、ここにあります。最初に commit されてのは、2021年1月20日っぽいです。
github.com

linuxソースコードを眺めてみて、変更している部分は、

  • CPU の enable_method
  • 割り込みコントローラ (AIC)

がメインです。Apple M1 の中を知るには、dts ファイル:apple-m1-j274.dtsを見ればいいです。

CPU の enable_method

Linux では、CPU の enable_method として、spin_table と psci があります。一般的には sleep をするしないによって分けていて、spin_table が sleep なし、psci が sleep ありです。Android用のKernelでは、sleep ありの psci が基本です。 spin_table を使っているのは、サーバーようです。PSCI に関しては、Slideshareにアップしていますので、興味がある方は見てください、

Apple M1 の場合は、sleep ありですが、psci をサポートしていないので独自の enable_method を 次のように”apple,startcpu" として定義しています。

        cpu@0 {
            device_type = "cpu";
            compatible = "apple,v1";
            reg = <0>;
            enable-method = "apple,startcpu";
        };


"apple,startcpu"は、arch/arm64/kernel/apple_cpustart.c で定義されています。
この中で boot の部分 (cpu_apple_start_boot) が定義されています。

cpu_apple_start_bootのこの部分が Apple M1独自のコードだと思います。この部分の後に、dsb(sy); sev(); で CPU を起こしています。

    writel(1 << cpu, info->pmgr_start);
    if(info->pmgr_start_size >= 12) {
        if(cpu < 4) {
            writel(1 << cpu, info->pmgr_start + 4);
            writel(0, info->pmgr_start + 8);
        } else {
            writel(0, info->pmgr_start + 4);
            writel(1 << (cpu - 4), info->pmgr_start + 8);
        }
    } else
        writel(1 << cpu, info->pmgr_start + 4);

    dsb(sy);
    sev();

ここで使われている info->pmgr_start は、どこに設定しているかというと、dts ファイル:apple-m1-j274.dtsapplestart の部分です。この reg の一部を info->pmgr_start に設定しています。

        applestart: applestart@23b754004 {
            compatible = "apple,startcpu";
            reg = <0x2 0x3b754004 0x0 0xc   0x2 0x10050000 0x0 0x8   0x2 0x10030fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x10150000 0x0 0x8   0x2 0x10130fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x10250000 0x0 0x8   0x2 0x10230fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x10350000 0x0 0x8   0x2 0x10330fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x11050000 0x0 0x8   0x2 0x11030fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x11150000 0x0 0x8   0x2 0x11130fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x11250000 0x0 0x8   0x2 0x11230fb0 0x0 0x4
                   0x2 0x3b754004 0x0 0xc   0x2 0x11350000 0x0 0x8   0x2 0x11330fb0 0x0 0x4>;
        };

info->pmgr_start の他に、info->cputrc_rvbar と info->dbg_unlock も apple_cpustart.c のここで、reg の値から設定しています。

reg は、3個のレジスタ x 8組になっていて、3個のレジスタの内、最初の値が info->pmgr_start、2番目の値が info->cputrc_rvbar、最後の値が info->dbg_unlock の値になります。
info->pmgr_start の設定値は、すべて同じで 0x2 0x3b7540000 0x0 0xc になっています。最初の2個が64ビットのアドレスで 0x2_3b754_0000 になり、残りの2個がサイズになり、0xcなので12バイトです。info->pmgr_start に 0x2_3b74_0000 が、info->pmgr_start_size に 0xc が設定されることになります。of_get_address(node,cpu*3, &info->pmgr_start_size, NULL)の戻り値が0の時は、info->pmgr_start_size に 8 を再設定してます。

    if(!info->pmgr_start) {
        info->pmgr_start = of_iomap(node, cpu * 3);
        if(!info->pmgr_start) {
            pr_err("%s: failed to map start register for CPU %d.\n", __func__, cpu);
            return -EINVAL;
        }
        if(!of_get_address(node, cpu * 3, &info->pmgr_start_size, NULL))
            info->pmgr_start_size = 8;
    }

    if(!info->cputrc_rvbar) {
        info->cputrc_rvbar = of_iomap(node, cpu * 3 + 1);
        if(!info->cputrc_rvbar) {
            pr_err("%s: failed to map reset address register for CPU %d.\n", __func__, cpu);
            return -EINVAL;
        }
    }

    if(!info->dbg_unlock) {
        info->dbg_unlock = of_iomap(node, cpu * 3 + 2);
        if(!info->dbg_unlock) {
            pr_err("%s: failed to map unlock register for CPU %d.\n", __func__, cpu);
            return -EINVAL;
        }
    }

再度、cpu_apple_start_boot に戻ってみます。info->pmgr_start_size が 12(0xc)以上の時は、cpu の値によって、info->pmgr_start +4 と +8 の値に 0 or 1 を書いています。
info->pmgr_start_size が 12未満の時は、info->pmgr_start + 4 に 1を書いています。

    writel(1 << cpu, info->pmgr_start);
    if(info->pmgr_start_size >= 12) {
        if(cpu < 4) {
            writel(1 << cpu, info->pmgr_start + 4);
            writel(0, info->pmgr_start + 8);
        } else {
            writel(0, info->pmgr_start + 4);
            writel(1 << (cpu - 4), info->pmgr_start + 8);
        }
    } else
        writel(1 << cpu, info->pmgr_start + 4);

    dsb(sy);
    sev();

上記の最後で、sev() にて、イベントを発生しています。では、このイベントを待っているところはどこでしょうか?

linux-m1/process.c at master · corellium/linux-m1 · GitHub の _cpu_do_idleです。ops->cpu_wfi が設定されている場合は、ops->cpu_wfi() が呼び出されています。ops->cpu_wfi が設定されていない場合は、wfi() で待っています。

static void noinstr __cpu_do_idle(void)
{
	const struct cpu_operations *ops = get_cpu_ops(task_cpu(current));

	if (ops->cpu_wfi) {
		ops->cpu_wfi();
	} else {
		dsb(sy);
		wfi();
	}
}

ops->cpu_wfi には、linux-m1/apple_cpustart.c at master · corellium/linux-m1 · GitHubにある cpu_apple_wfi が設定されています。cpu_apple_wfi では、wfi() ではなく、wfe() になっています。

static void cpu_apple_wfi(void)
{
    /* can't do a proper WFI, because the CPU tends to lose state; will need
       a proper wrapper sequence */
    dsb(sy);
    wfe();
}

割り込みコントローラ(AIC)

Linux の arm64での割り込みコントローラはARM社のGICを使っていますが、Apple M1では独自の割り込みコントローラ(AIC)を使っているようです。
割り込みコントローラ(AIC)は、デバイスドライバ(irqchip)のディレクトリの irq-apple-aic.c になります。
この中の apple_aic_handle_irq が割り込み処理を行っています。割り込みの種類は、REG_IRQ_ACK_TYPE_NONE, REG_IRQ_ACK_TYPE_IRQ, REG_IRQ_ACK_TYPE_IPIです。REG_IRQ_ACK_TYPE_NONEは特に何もやっていません。REG_IRQ_ACK_TYPE_IRQは普通の割り込み処理として handle_domain_irq にて処理しています。最後の REG_IRQ_ACK_TYPE_IPI が Apple M1 特有の処理になっています。IPIは、Inter-process Interrupt の略だと思います。この割り込みの処理では、割り込みを発生元を if(ack == REG_IRQ_ACK_IPI_SELF) で判断し、1 のときは aic.base + REG_IPI_CLEARレジスタへの書く値(REG_IPI_FLAG_SELF or REG_IPI_FLAGE_OTHER)を変えています。

ここに出てくる REG_IPI_FLAG_SELF と REG_IPI_FLAG_OTHER というのがどうやらポイントだと思います。

static void __exception_irq_entry apple_aic_handle_irq(struct pt_regs *regs)
{
    atomic_t *maskptr;
    uint32_t ack;
    unsigned done = 0, irqnr;
    unsigned long mask;

    while(1) {
        ack = readl(aic.base + REG_IRQ_ACK);
        switch(ack & REG_IRQ_ACK_TYPE_MASK) {
        case REG_IRQ_ACK_TYPE_NONE:
            done = 1;
            break;
        case REG_IRQ_ACK_TYPE_IRQ:
            handle_domain_irq(aic.domain, ack & REG_IRQ_ACK_NUM_MASK, regs);
            break;
        case REG_IRQ_ACK_TYPE_IPI:
#ifdef CONFIG_SMP
            if(ack == REG_IRQ_ACK_IPI_SELF)
                writel(REG_IPI_FLAG_SELF, aic.base + REG_IPI_CLEAR);
            else
                writel(REG_IPI_FLAG_OTHER, aic.base + REG_IPI_CLEAR);
            maskptr = get_cpu_ptr(&aic_ipi_mask);
            smp_mb__before_atomic();
            mask = atomic_xchg(maskptr, 0);
            smp_mb__after_atomic();
            put_cpu_ptr(&aic_ipi_mask);
            for_each_set_bit(irqnr, &mask, NUM_IPI) {
                handle_domain_irq(aic.domain, aic.num_irqs + 2 + irqnr, regs);
            }
            if(ack == REG_IRQ_ACK_IPI_SELF)
                writel(REG_IPI_FLAG_SELF, aic.base + REG_PERCPU(REG_IPI_ENABLE, __smp_processor_id()));
            else
                writel(REG_IPI_FLAG_OTHER, aic.base + REG_PERCPU(REG_IPI_ENABLE, __smp_processor_id()));
#endif
            break;
        }
        if(done)
            break;
    }
}

irq-apple-aic.c の最初の方では、apple_aic_ipi_send_mask では、aic.fast_ipi が設定されていないと、apple_aic_handle_irqと同じように aic.base + REG_IPI_SET に REG_IPI_FLAG_SELF or REG_IPI_FLAG_OTHER を書き込んでいます。

aic.fast_ipi が設定されている場合は、(lcpu >> 2) と (cpu >> 2) の値が同じ時は SR_APPLE_IPI_LOCAL に、違う時は SR_APPLE_IPI_REMOTE に設定しています。
(lcpu >> 2) および (cpu >> 2) で、各値を2ビット、右にシフトしています。これは cpuの番号が 0-3 と 4-7 のものを比較していて、cpuがbigコアのクラスタに属するCPUコア、LITTLEコアのクラスタに属するCPUコアのどちらに属しているのかを調べているんだと思います。同じクラスタに属しているときはSR_APPLE_IPI_LOCALに、違うクラスタに属しているSR_APPLE_IPI_REMOTEに設定しています。

#ifdef CONFIG_SMP
static void apple_aic_ipi_send_mask(struct irq_data *d, const struct cpumask *mask)
{
    int cpu, lcpu;
    int irqnr = d->hwirq - (aic.num_irqs + 2);

    if (WARN_ON(irqnr < 0  || irqnr >= NUM_IPI))
        return;

    /*
     * Ensure that stores to Normal memory are visible to the
     * other CPUs before issuing the IPI.
     */
    wmb();

    for_each_cpu(cpu, mask) {
        smp_mb__before_atomic();
        atomic_or(1u << irqnr, per_cpu_ptr(&aic_ipi_mask, cpu));
        smp_mb__after_atomic();
        lcpu = get_cpu();
        if(aic.fast_ipi) {
            if((lcpu >> 2) == (cpu >> 2))
                write_sysreg(cpu & 3, SR_APPLE_IPI_LOCAL);
            else
                write_sysreg((cpu & 3) | ((cpu >> 2) << 16), SR_APPLE_IPI_REMOTE);
        } else
            writel(lcpu == cpu ? REG_IPI_FLAG_SELF : (REG_IPI_FLAG_OTHER << cpu), aic.base + REG_IPI_SET);
        put_cpu();
    }

    /* Force the above writes to be executed */
    if(aic.fast_ipi)
        isb();
}
#else
#define apple_aic_ipi_send_mask NULL
#endi

aic.fast_ipi は、linux-m1/apple-m1-j274.dts at master · corellium/linux-m1 · GitHubで次のようにせってされています。
dtsファイル内の aic に、fast-ipi があるときに 設定されます。

        aic: interrupt-controller@23b100000 {
            compatible = "apple,aic";
            #interrupt-cells = <3>;
            interrupt-controller;
            reg = <0x2 0x3b100000 0x0 0x8000>;
            fast-ipi;
        };

apple_aic_handle_irqの割り込み要因として、FIQがありませんでしたが、linux-m1/irq-apple-aic.c at master · corellium/linux-m1 · GitHub にて、apple_aic_handle_fiq を設定しています。FIQに関しては、下記のようにapple_aic_handle_fiq にて処理されています。

static void __exception_irq_entry apple_aic_handle_fiq(struct pt_regs *regs)
{
#ifdef CONFIG_SMP
    atomic_t *maskptr;
    unsigned long mask;
    unsigned irqnr;

    if(aic.fast_ipi) {
        if(read_sysreg(SR_APPLE_IPI_STAT)) {
            write_sysreg(1, SR_APPLE_IPI_STAT);

            maskptr = get_cpu_ptr(&aic_ipi_mask);
            smp_mb__before_atomic();
            mask = atomic_xchg(maskptr, 0);
            smp_mb__after_atomic();
            put_cpu_ptr(&aic_ipi_mask);
            for_each_set_bit(irqnr, &mask, NUM_IPI)
                handle_domain_irq(aic.domain, aic.num_irqs + 2 + irqnr, regs);
        }
    }
#endif
    handle_domain_irq(aic.domain, aic.num_irqs, regs);
}

おわりに、

Apple M1で native な Linux が動くようになったということで、github に公開された Linux コードの中からポイントとなる次の2点について見てみました。

  • CPU の enable_method
  • 割り込みコントローラ (AIC)

Linux の Page size は通常4KBです。一方、iOS/iPadOS の Page size は16KBです。Apple M1 の Linux の Page size はどうなっているかは、まだ未調査です。

P.S
Page Size、16KBになっているのを確認できました。
https://github.com/corellium/linux-m1/blob/master/drivers/iommu/apple-dart-iommu.c (DART IOMMU on Apple SoCs)のところにありました。
IOMMUの page_bit を ここ で設定してます。

USB、I2C、DART (DARTってなんだろう)が動くようです。<= DART って、このことのようですね。

	if(of_property_read_u32(pdev->dev.of_node, "page-bits", &im->page_bits) < 0)
		im->page_bits = 12;

dts ファイルの"apple,dart-m1"のところ、以下の2か所の page-bits に <14> が設定されています。

        usb_dart0: usb_dart0@382f00000 {
            compatible = "apple,dart-m1";
            clocks = <&atc0_usb>;
            reg = <0x3 0x82f00000 0x0 0x80000   0x3 0x82f80000 0x0 0x4000>;
            interrupts = <0 781 4>;
            page-bits = <14>;
            sid-mask = <11>;
            sid-remap = <0 1>;
            #iommu-cells = <1>;
        };
        usb_dart1: usb_dart1@502f00000 {
            compatible = "apple,dart-m1";
            clocks = <&atc1_usb>;
            reg = <0x5 0x02f00000 0x0 0x80000   0x5 0x02f80000 0x0 0x4000>;
            interrupts = <0 861 4>;
            page-bits = <14>;
            sid-mask = <11>;
            sid-remap = <0 1>;
            #iommu-cells = <1>;
        };

14ビットということは、16KB です。IOMMU の Page size が 16KB ということは、当然、CPU側も 16KB になると思います。
kernel config の時に、ARM64_16K_PAGES を設定することで、Page size を 16KB にすることができると思います。
 

config ARM64_16K_PAGES
	bool "16KB"
	help
	  The system will use 16KB pages support. AArch32 emulation
	  requires applications compiled with 16K (or a multiple of 16K)
	  aligned segments.

System memory と IOMMU の接続は、Intel (左) と Apple Silicon (Apple M1、右)のように違うようです。
f:id:Vengineer:20210124130453p:plain

Apple M1 の Linux の dts ファイルから次のようになっていることが分かりました。

f:id:Vengineer:20210124130351p:plain
IOMMUについて

MIT の Vivienne Sze さんの Energy-Efficient Deep Learning を眺めてみた

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

はじめに

この記事では、MITの Vivienne Sze さんの講演ビデオを観て、そこから得られたものをまとめたものです。

Vivienne Sze さんの研究室は、こちら。ENERGY-EFFICIENT MULTIMEDIA SYSTEMS GROUP
www.rle.mit.edu

Vivienne Sze さんの経歴は、

学士の時に、IC Design Engineerとして働き、博士後にTIで3年近く半導体エンジニアとして働いていたようです。その時にビデオコーデックとかをやっていたので、現在の研究室のタイトルに MULTIMEDIA が入っているのかもしれません。その後、過去の発表資料を眺めたら、最初が動画関連が多かったので、最初からMULTIMEDIAの研究をやっていて、今はDeep Learningを道具として使っているって感じなんだと思いました。

このビデオ「Energy-Efficient AI」を観れば、現在どんな研究をやっているかが分かるようだ。

  • Energy-Efficient AI with Cross-Layer Design (Algorithms)
  • Specialized Compute Hardware (Compute Architectures & Circuits : Eyeriss
  • Robot Localization Under a tenth of a Watt (コラボ with Sertac Karaman), Navion chip
  • Low Energy Robotics (Lighter than Air Vehicles), Miniature Satellites, Origami Robots
  • Neuropsychological Testing (コラボ with Thomas Heldt), eye movement

EEMS Groupに出てくるYoutubeのビデオ

講演ビデオにも出てきますが、現在の研究対象は

  • Energy-Efficient Deep Learning, Computer Vision, and Autonomous Navigation
  • Mobile Health Monitoring
  • Accelerating Super Resolution
  • Next-Generation Video Coding

です。

Energy-Efficient Deep Learning に関係するビデオ

この記事では、Energy-Efficient Deep Learning に関するものを追っていきます。
Energy-Efficient Deep Learningに関しては、Efficient Processing of Deep Neural Networksという書籍が出ていて、このブログでも紹介しました。
vengineer.hatenablog.com

Youtubeのサイトは、
www.youtube.com
この他に、Youtubeで「Vivienne Sze」で検索すると、20以上の講演ビデオが出てきます。

Vivienne Sze さんの研究成果で特に有名なのは、Eyeriss Projects ですね。推論用アクセラレータです。
最初の論文が2016年3月なので、あたしが TensorFlow XLAに興味を持つ1年も前に出ています。
Eyeriss は名前は知っていましたが、詳しく調べることは今までありませんでした。この機会の一番最初の論文から読み返すことにしました。

最初の論文は、ISCA 2016。「Eyeriss: A Spatial Architecture for Energy-Efficient Dataflow for Convolutional Neural Networks
この時の著者は、Yu-Hsin Chen、Joel Emer、Vivienne Szeの3人。
Yu-Hsin Chen さんは MITで博士を取ってから、NVIDIA => Facebook です。
Joel Emer さんは、2005年からMITと関わっていますね。Compaq => Intel => NVIDIA です。なんと、Intelの時は、このブログでも紹介したLEAP FPGAにかかわっていたようです。Bluespec:MIT & Intelの事例(LEAP)

65nmで実際のInference用チップとして開発して Eyeriss 。12*14個のPE + 108KBのSRAM(バッファ) + DRAM I/F。
各PEは200MHzで動作し、0.5KBのレジスタファイルを持っている。
各PEの中に、input feature/weight/output feature のどれを保持すれば、電力効率が良くて、Latencyが短いのかをベンチマークして、Row Stationary (RS) が一番いいと。
この RS が一番いいというのは、最初に観た何本かのビデオに出てくるんです。どうして、RSが一番いいのかと。。。Eyeriss の論文とスライドを観てやっとわかりました。
ISCA 2016のスライド。に、その RS が一番いいという説明が丁寧に載っています。
Eyeriss (この論文)では、DRAMから・へのデータ移動時には圧縮・伸長を行っています。Run Lengthなのでリアルタイムに処理ができるし、回路規模もそんなにないです。これにより、DRAMへのアクセスを減らせることができます。

f:id:Vengineer:20210102132203p:plain
Eyeriss system architecture

EyerissはCNN/FCの電力効率とLatencyをよくするためのものであったが、その後のMobile Device用のモデル(MobileNetなど)ではCNNのサイズが変わったため、Eyeriss の特徴を生かししきれなくなり、
Eyeriss v2: A Flexible Accelerator for Emerging Deep Neural Networks on Mobile Devices を発表。
著者は、Eyerissの3名 + 1名 Tien-Ju Yang さん です。(Yu-Hsin Chen さん の後輩っぽい)

下図が Eyeriss と Eyeriss v2 の違いです。GLB(Global Buffer)が1つからGLB cluster として分散。2D Mesh Network にて、(GLB cluster + PE cluster + Router cluster) との接続を変えられるのが大きな違いです。また、各PEは SIMD (x2) になっています。
PE を cluster にして、4x3 の PE clusterに分散することで、MobileNetのようなモデルでも電力効率とLatencyをよくなるようです。

f:id:Vengineer:20210102123500p:plain
Eyeriss と Eysriss v2 の違い

2D Mesh Networkにて、PE cluster 間のデータ移動がスムーズになっています。

f:id:Vengineer:20210102125502p:plain
2D Mesh Network

下図では、CONV、FC、だけでなく、DW CONV の対応できるようになっているのが分かります。

f:id:Vengineer:20210102125636p:plain
CONV、FC、DW CONV

GLB cluster からRouter cluster 経由で PE cluster へのデータ移動の図がこちら
f:id:Vengineer:20210102125825p:plain

psums の PE cluster 間の接続図がこちら
f:id:Vengineer:20210102125901p:plain

Eyerissの時はDRAMからGLBへの圧縮・伸長をしていますが、Eyeriss v2 では 各PEへの Weight も圧縮・伸長しています。つまり、Sparse 対応

f:id:Vengineer:20210102132504p:plain
Example of compressing sparse weights with compressed sparse column (CSC) coding

入力データに関しては、PEの中で伸長するようになっています。PEはSIMD (x2)なので、赤字 (x2) があります。

f:id:Vengineer:20210102132901p:plain
Eyeriss v2 PE Architecture

まとめとしての比較 (Eyeriss v1が最初のASIC、v1.5 は NoC を 2D Mesh Network にしたもので PE は v1 と同じ、v2 が PEを SIMD & Weight Sparse対応)

f:id:Vengineer:20210102133522p:plain
Eyeriss と Eyeriss v2 の比較

他のチップとの比較 (PEをSIMD x2にしたのがだ、演算精度が16bitから8bitになっているのを発見)
どのチップも200MHzで動作する。Eyeriss v1 に対して、Eyeriss v2 は、ALEXNETで10倍の性能で6倍の電力性能を達成。おまけに、MobileNetも対応している。

f:id:Vengineer:20210102134201p:plain
他のチップとの比較

何故、16bit から 8bit になったのは、2016年から2018年の間に、Inference に関しては 8bit の精度があれば OK になったからであろうか?

ここまでで、Inferenceに関しては、RS、8bit、データおよびWeightは圧縮(伸長)、SIMD (x2)、2D Mesh Network で Mobile用ネットワークにも対応できるようになった。

2020年は、Timeloop & Accelergyについての2本のビデオ

スライドは、こちら

Timeloop の github は、こちら (NVLabs内にあるので、基本的にはNVIDIA)
Accelergy は、こちらは、MITで、基本的には、Eyeriss の DRAM <=> GLB <=> PE という構成で ハードウェアを決めていく感じですね。

左側のMAPPER部分が Timeloop 、右側のMODEL部分が Accelegry という感じ。
MAPPERである Timeloop で問題(Problem)とArchitecture、Constraintsにて、Mapspace を構築し、Mapspace に対して、Mapper parameters を入れて、Mapping したものを
Accelergy にて micro Architecture を Energy, Performace, Area を評価尺度にてて探索する感じ。

f:id:Vengineer:20210102143910p:plain
Timeloop と Accelergy

これって、1月2日にアップした「ハードウェアを意識したNASからハードウェアの構成も一緒に決めるNASに!」に書いたHASの部分なのかな?MAPPER の Problem が Network Model で Architecture と Constrains を合わせると、NAS なんでしょうね。

最後に

ディープラーニングのハードウェアを研究している大学の研究室の多くは、FPGAに実装して評価しているところがおおいですが、
MITのVivienne Szeさんの研究室では、実際に半導体に実装し、評価を行っています。実際に実装して、評価することでしか得られない知見が次の研究につながっているんだなーと思いました。

Google Edge TPUのエコシステムをまとめてみた

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

はじめに、

この記事は、Google (Coral)が販売している Edge TPU のエコシステムについてまとめたものです。

このブログでも何度も取り上げた Google Edge TPU 。USB ドングルだけでなく、mini PCIe, M.2, M.2 dual, SOM, Moduleで提供されています。また、デバイスを搭載して、Coral Dev Board および Coral Dev Boad Mini もあります。
その Google Edge TPU を使う上でのソフトウエアの多くはオープンソースとして、github にて公開されています。

今年の正月にこんなツイートをしたので、見た方もいると思います。

このツイートで書いた

  • pycoral
  • libcoral
  • libedgetpu

までは、Coral の github にて公開されています。

  • linux gasket driver
  • apex drvier

については、Linux (githubで公開されています)の中にあります。

これらソフトウェアは、Runtimeと呼ばれるソフトウェアで、TensorFlow Liteもモデルを Google Edge TPU用に変換する Edge TPU Compiler に関してはソースコードは公開されていません。
しかしながら、Linux の中にある apex driver を解析することでいろいろなことがわかります。

処理の流れ

Google Edge TPUで処理をさせるためには次の2つのステップが必要です。

1. TensorFlow Liteモデルを Google Edge TPU用の TensorFlow Liteモデルに変換する
既に変換済みのモデル(ここ)を使う場合はこのステップは必要ありません。
TensorFlow LiteモデルをGoogle Edge TPU に読ませる TensorFlow Liteモデルに変換するには、専用の(Offline) Compiler(Edge TPU Compiler) を使います。

2. Pycoral あるいは、libcoral を使って、アプリケーションを作成する。

それでは、順番にそれぞれのソフトウェアを見ていきましょう。

ソフトウェア

coral からのソフトウェアは、ここ、説明等があります。

Edge TPU Compiler

Edge TPU Compiler | Coral

TensorFlow Liteモデルの中のOpの内、Google Edge TPU 内で連続して実行できるものを切り出し、大きなOp (edge-tpu-custom) にラップしたモデルに変換するのが Edge TPU Compiler の働きです。Edge TPU Compilerにて、入力となるTensorFlow Liteモデルの中で連続して実行できるOpの中には複数のOp を1つのOpに融合(Fusion)したものに置き換えたりもします。現時点では、1つのモデルの内、1つの塊しか Google Edge TPU 内で実行できませんが、複数の Google Edge TPU を使うことはできます。

Pycoral : Python API

https://github.com/google-coral/pycoral

Pycoral は、Google Edge TPU に TensorFlow Liteモデルを読み込ませて、推論をするための Python APIです。

Pycoral の使い方については、Run inference on the Edge TPU with Pythonというドキュメントがあります。
また、各 Python API のドキュメントは、ここ にあります。
コンソールからちょおっと使ってみたいときにはとっても便利なものだと思います。

例題には、

  • 物体検出:detect_image.py
  • 物体識別:classify_image.py

などがあります。

ここで使っている学習済みモデルは、ここ にあります。

libcoral : C++ API

ibcore

Google Edge TPU に TensorFlow Liteモデルを読み込ませて、推論をするための C++ APIです。C++ のアプリケーションなどに組み込むことができます。
libcoral の使い方については、Run inference on the Edge TPU with C++というドキュメントがあります。
また、各 C++ API のドキュメントは、ここ にあります。

x86-64, ARM-v7(RasPi/2/3/4など), ARM-v8(Google Coral Dev Board) などで ビルドできます。ビルド時には、Docker も利用できます。

この2種類のAPIを使うことで、いろいろなところで Google Edge TPU が利用できると思います。

また、Colab tutorials for Coral では、Google Cloud上の Colab にて、Tutorial もあります。

libedgetpu : Edge TPU runtime library

https://github.com/google-coral/libedgetpu

このlibedgetpuは、Runtimeに相当するものです。libedgetpuが最初に公開されたときはバイナリバージョンでソースコードは公開されていませんでした。
その後、2020年11月のFrogfishにて、ソースコードgithub にて公開されました。

libedgatpu は、libcoral から呼び出され、次に説明する Linux 内にある gasket driver と apex driver の橋渡しをします。
driver ディレクトリにあるdriver.[h,c]がLinux内にある gasket driver と apex driver に対していろいろなことを行い、ハードウェアである Edge TPU を制御しています。

このようなハードウェアを制御する部分のRuntimeのソースコードが公開されるのは非常にまれなことです。ソースコードを解析することでどのようにEdge TPUを制御しているかがわかるかもしれません。

libcoral同様に、x86-64, ARM-v7(RasPi/2/3/4など), ARM-v8(Google Coral Dev Board) などで ビルドできます。ビルド時には、Docker も利用できます。

Linux 内にある gasket driver と apex drvier

この2つの driver は、linuxdrivers/staging/gasket ディレクトリに存在します。
gasket driver を経由して、apex ドライバが呼び出されます。この gasket driver は、PCIe 用のデバイスドライバです。USBタイプの Google Edge TPU のものではありません。

apex_driver.c がデバイスドライバの本体で、PCIのBAR[2]にいろいろなレジスタがあるのがわかります。

関連ブログ:Google Edge TPUのlibedgetpuの中とApexデバイスドライバの関係
関連ブログ:Google Edge TPU のデバイスドライバを見つけました
関連スライド:Pixel Visual Core device driver source code analysis

ハードウェア

ここでは、ハードウェア側の Coral Dev Board/Coral Dev Board Mini と Edge TPU 本体について、見ていきます。

Coral Dev Board/Coral Dev Board Mini

Google Edge TPU がボード上に実装された Coral Dev Board および Coral Dev Board Mini があります。
これらのボードの Schematics and layoutgithub にて、公開されています。

Dev Board Mini については、SoC(MediaTek 8167s)は PCIe Gen2 を持っていないので、USB 2.0 で Edge TPU と接続しています (上記の回路図でも USB 2.0で接続されています)。また、Data Sheet にも載っていました。

ASUS Tinker T

ASUS Tinker Edge T は、CoralのSOM(1GBメモリ版)を搭載したボード。

AI-pITX-100-GC

Al-plTX-100-GC は、i.MX8搭載だけどメモリが8GBもあるボード。

Google Edge TPU の中身

クラウド用学習チップである Google TPU v2とv3に関しては、ざっくりでありますが、内部がどんな感じになっているかまでは公開されています。
一方、Google Edge TPU ついては、そのような情報は公開されていません。

しかしながら、Googleで検索すると、いろいろと出てきます。
その一つが、Google Edge TPU の中には、Corex-A53x2が入っているようです。
また、Linux の apex デバイスドライバの中を解析してみると、DRAMのon/off という記述があります。Google Edge TPU は DRAM は off (付いていない)ですが、
Pixel 4に搭載されている Pixel Neural Core では、Google Edge TPU のコアと同じようなものが入っていて、こちらはチップにDRAM(LPDDR4)が付いています。

関連ブログ:Google Edge TPUには、Cortex A53x2が入っていた
関連ブログ:Pixel Visual CoreのASIC内では、Headless Androidが走っている!
関連ブログ:Pixel Visual Coreのデバイスドライバのソースコード解析のスライド
関連ブログ:やっぱり入っていた Pixel 4に、Edge TPUが

最後に、

簡単ではありますが、Google Edge TPUのエコシステムについてまとめました。
あとは、Edge TPU Compilerとハードウェアである Edge TPU 本体がNVIDIAのNVDLAにようにgithubにて公開されると、学習用途や研究の土台になって面白いと思うのだが、それは贅沢という話でしょうか。

このブログでの Google Edge TPU に関するもの
vengineer.hatenablog.com