借り初めのひみつきち

仮ブログです。

raspberry pi ベアメタルでマルチコアを利用する方法

入手困難とうわさの raspberry pi を偶然にも入手たので最近触っています。

raspi の CPU は4コアで動作しますが、現在のファームウェアは最初のコア (ID 0) のみ起動します。*1

他のコアがどうしているかというと、ファームウェア物理アドレス 0x0000_00E0, 0x0000_00E8, 0x0000_00F0 の値を監視しているので、ここを書き換えてやれば起動できます。

例えば 0x0000_00E0 に 0x0008_0000 (_start のアドレス) を書き込むと ID 1 のコアが _start から実行開始します。

実際に書き込む場合、マルチコアでメモリ書き込みを保証するために Release のメモリーオーダーで書き込み、 sev 命令でイベントを通知します。

Rust だとこのような感じになります。

    for p in [0xE0, 0xE8, 0xF0] {
        let p = &*(p as *const AtomicUsize);
        p.store(_start as usize, Ordering::Release);
        asm!("sev");
    }

このコードを実行すると各コアが _start から実行開始するので、それぞれ mpidr_el1 レジスタを読み出して下位2ビットからコア ID を取得し、シフトした決め打ちのスタックポインタを設定し、 bl 命令で初期化関数を呼び出します。

_start:
    mrs     x1, mpidr_el1
    and     x1, x1, #3
    cbz     x1, 2f

    lsl     x2, x1, #16
    add     x2, x2, #0x10000
    mov     sp, x2

    bl      _smp_main

1:  wfe
    b       1b
2:
    (省略)

これだけでマルチコアが起動できました。

APIC の設定を弄ったりロングモードの用意が必要な x86-64 に比べると、 Arm64 のマルチコア起動はスッキリしていて簡単にできますね!

と、言いたいところですが、このままでは atomic 変数の read-modify-write や CAS 操作に必要な命令 (stxrなど) が正しく動作しません。 CAS 操作ができないと spinlock を実装できないのでコア間で同期を取る事ができません。 つまり、コアごとに完全に役割分離しているような特殊な用途を除いてマルチコアを活用する事ができません。 CAS 操作を正しく実行するためには、 MMU とキャッシュの設定が必要になります。

この辺り x86 は適当に書いてもなんとなく動きますが、 arm では真面目にちゃんと設定しないと意図した通りに動いてくれないので難しいです。

なお、 Arm 系 CPU は基本的に同様のプロトコルで起動できるようで、各コアの監視アドレス 0xE0, 0xE8, 0xF0 は DeviceTree の各 CPU にある cpu-release-addr で確認できます。

  cpus {
        #address-cells = <0x01>;
        #size-cells = <0x00>;
        enable-method = "brcm,bcm2836-smp";
        phandle = <0xdb>;

        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x00>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xd8>;
            phandle = <0x28>;
        };

        cpu@1 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x01>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xe0>;
            phandle = <0x29>;
        };

        cpu@2 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x02>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xe8>;
            phandle = <0x2a>;
        };

        cpu@3 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x03>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xf0>;
            phandle = <0x2b>;
        };
    };

*1:過去のファームウェアは全てのコアが一斉に起動していた時代があるようで、ネットで raspi ベアメタルのやり方を調べるとスタートアップ時にコアを判別して ID 0 以外のコアで無限ループするやり方がよく紹介されています