入手困難とうわさの 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>; }; };