週末!プログラミング部

ソフトウェア開発ネタを中心に自分でいろいろ調べた内容を自分の勝手な解釈で思うがままに書いくためのブログ。サンプルソースコード、API、プラットフォーム、プログラミング言語、開発環境などを調査、分析して追求いく予定です。

RPiでAMP環境を構築してみる

今回は、Crosstool-NGで作ったRPiのベアメタル開発環境を使用して、Raspberry Pi(以降、RPi)でAMPシステムを作ってみたいと思います(・`ω´・)b

AMPシステムとは

AMPとはAsymmetrical Multi Processingの略で、マルチコアプロセッサにおいてそれぞれの演算コアで別々のプログラムを動作させることを言います。
最近のRPi(RPi2、RPi3、RPi4のModel B、およびB+など)には、ARM Cortex-AシリーズというMPUが搭載されています。
さらに、これらのMPUクアッドコアになっているので、これらの演算コアを使用して別々のプログラムを動かしてみたいと思います。
https://ja.wikipedia.org/wiki/Raspberry_Pi


これが今回構築するAMPシステムのイメージです。 f:id:NATSU_NO_OMOIDE:20201220184754p:plain
今回は、4つの演算コアの内、3コアを使用してRaspberry Pi OSを動作させ、その上でユーザプログラムを動作させます。
※ちなみに複数の演算コアを使用して同じプログラム動作させる方式はSMP(Symmetric Multi Processing)と呼ばれているようです。
残りの1コアを使用してベアメタルアプリケーションを実行します。
さらにRaspberry Pi OSで動作しているユーザプログラムと別コアで動作しているベアメタルアプリケーション間で共有メモリを使用してIPC(Inter Process Communication)させることを目指します。
ちなみに今回はRPi3を使って、32bit環境を対象にシステム構築したいと思います。

なおAMPとSMPの説明は以下がとても分かりやすかったです。 https://news.mynavi.jp/article/20071205-mp/3

Raspberry Pi OSをインストールしてみる

まずは普通にRaspberry Pi OSをインストールします。インストール自体は以下を参考にしてやりました。
最近はより簡単にインストールできるRaspberry Pi Imagerというツールが公式から登場しました。
とても簡単にインストールすることができますヽ(*´∀`)ノ
※そのほか、SSHを使用できるようにしたり、vimをインストールしたりしましたがここでは説明を割愛します。
https://www.indoorcorgielec.com/resources/raspberry-pi/raspberry-pi-osのインストール

準備ができたらRPiにRaspberry Pi OSをインストールしたSDカード、HDMIケーブル、LANケーブル、USBキーボードを接続してUSBケーブルをPCにつないで電源ONします。
f:id:NATSU_NO_OMOIDE:20201220191824p:plain

電源が入ると起動画面が表示されます。
このときベリーの絵が4つ表示されました。
これはRaspberry Pi OSが演算コアを4つ使用して動作していることを示しているようです。(・・・と、どこかに書いてあったような気がします。)
f:id:NATSU_NO_OMOIDE:20201220191819p:plain

一応、以下のコマンドを使用してメモリ使用量とCPUコア使用数を確認しておきます。

$ free
             total       used         free    shared buffer/cache    available
Mem:        948304      50288       819920      6308        78096       840372
Swap:       102396          0       102396
$ cat /proc/cpuinfo
processor       : 0
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

processor       : 1
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

processor       : 2
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

processor       : 3
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

これによるとメモリを1GBフル活用(RPi3の実装RAMは1GB)しており、CPUも4コア使用していることがわかります。
これでRaspberry Pi OSのインストールは完了です(・`ω´・)b

Raspberry Pi OSが使用するメモリとCPUコア数を制限してみる

さて、Raspberry Pi OSにリソースを制限させて動作させるためには、起動時に使用するメモリの量や範囲、演算コアの個数を伝えてやる必要があります。
そのために今回はu-Bootを使用することにしました。
U-Bootは、多種のプラットフォームに対応したブートローダです。当然RPiもサポートされています。
https://ja.wikipedia.org/wiki/Das_U-Boot
https://elinux.org/RPi_U-Boot

u-bootをビルドする

まずはu-bootをビルドします。
コンパイラCrosstool-NGで作ったRPiのベアメタル開発環境を使います。
ソースはgitから落とします。本家のgit://git.denx.de/u-boot.gitもありますが、ダウンロードが遅いです(・´ω`・)

$ git clone git://github.com/swarren/u-boot.git
$ make clean
$ make distclean
$ make rpi_3_32b_defconfig
$ make all


これでRPi3のu-boot(boot.bin)ができます。
なおmakeするとき、使用したいRPiのモデルを指定します。
指定できるdefconfigには以下のものがあります。

  • rpi_defconfig : 初代Rpi用
  • rpi_2_defconfig : RPi2用
  • rpi_3_32b_defconfig : 32bit RPi3用
  • rpi_3_defconfig : 64bit RPi3用
  • rpi_4_32b_defconfig : 32bit RPi4用
  • rpi_4_defconfig : 64bit RPi4用

bootスクリプトを書く

続いてu-bootが使用するbootスクリプトを書きます。
このu-bootはこのスクリプトを使ってRaspberry Pi OSをbootします。

スクリプトは以下のような感じになります。

mmc dev 0
fatload mmc 0:1 ${kernel_addr_r} kernel7.img
fatload mmc 0:1 0x19000000 bcm2710-rpi-3-b.dtb
setenv bootargs earlyprintk console=tty0 console=ttyAMA0 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait noinitrd mem=512M maxcpus=3
bootz ${kernel_addr_r} - 0x19000000

2行目でロードする.imgファイル(Raspberry Pi OSカーネル)を指定しています。
.imgファイルはRaspberry Pi OSをインストールしたときSDカードの/bootに作られます。
カーネルには以下の種類があります。今回は32bit RPi3用のkernel7.imgを指定します。

  • kernel8.img :64bit RPi3、RPi4用
  • kernel7l.img :32bit RPi4用(LPAE付)
  • kernel7.img :32bit RPi3、RPi4用(LPAEなし)
  • kernel.img :上記以外のRPi

3行目はデバイスツリーを配置するアドレスを指定しています。
バイスツリーもRaspberry Pi OSをインストールしたときSDカードに作られるものを流用します。
今回は1GBあるメモリの内、lowerの512MBをRaspberry Pi OS用、upperの512MBをベアメタルアプリケーション用にしたいと考えています。
そこでデバイスツリーの配置アドレスは、あえて0x19000000(400MB付近)にしました。

4行目では、Raspberry Pi OSの起動オプションを指定しています。
mem=512M maxcpus=3を指定することで、メモリサイズを512MB、使用するCPUのコア数を3個にしてRaspberry Pi OSを起動できるようにはずです。

5行目では、bootzコマンドでカーネルとデバイスツリーを配置させます。
${kernel_addr_r}はたしか0x80000からだったと思います。
したがってここから4行目で指定したmem(=512MB)がカーネルが使用できるメモリ範囲になると思います。
(めんどくさいのでデバイスツリーの開始アドレスを指定していますが(;^_^A

上記スクリプトが書けたらboot.txtとして保存し、以下のコマンドを使用してboot.scrを作成します。

mkimage -A arm -O linux -T script -C none -n boot.script -d boot.txt boot.scr


最後に作成したboot.binとboot.scrをSDカードの/boot直下にコピーします。
そして/boot/config.txtに以下の行を追加して準備完了です。

kernel=u-boot.bin


再びRaspberry Pi OSを起動してみる

これでSDカードをRPiに入れて電源ONすると同じようにRaspberry Pi OSが立ち上がるはずです。
ただし起動プロセスとしてはu-bootが先に起動してRaspberry Pi OSをキックするような流れになっていると思います。
(本当にu-bootが起動しているかはUARTを有効にするなどして確かめるとよいかと思います。)

起動後は再びSSHで接続し、メモリとCPUコア使用数を確認しておきます。

$ free
             total       used         free    shared buffer/cache    available
Mem:        506152      40940       397744      3500        67468       411052
Swap:       102396          0       102396
$ cat /proc/cpuinfo
processor       : 0
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

processor       : 1
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

processor       : 2
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

これによるとメモリ使用量が512MB(起動時のメッセージを見ると0x80000から配置されていると思います)で 使用CPUコア数が3個になっていることがわかります。
これでRaspberry Pi OSの準備は完了です。

ベアメタルアプリケーションを作成する

ここまででRaspberry Pi OSが3コアで動作するようになったので、残りの1コアでベアメタルアプリケーションを動作させるようにしていきます。
が、まずはベアメタルアプリケーションがちゃんと動くか確認します。

Lチカコードを作成する

コンパイラは同じくCrosstool-NGで作ったRPiのベアメタル開発環境を使います。
ベアメタルアプリケーションの動作確認は単純なLEDチカチカプログラムを使って行います。 以下、ソースコードです。

#define GPFSEL1 0x3F200004 /* GPIO のピン設定をするためのレジスタ */
#define GPSET0  0x3F20001C /* GPIO を HIGH にするためのレジスタ */
#define GPCLR0  0x3F200028 /* GPIO を LOW  にするためのレジスタ */

typedef unsigned char bool;

#define TRUE  1
#define FALSE 0

#define WAIT_COUNT 3000000

/*
 * @fn wait_count だけビジーウェイトする
 */
void busy_wait(int wait_count);

/*
 * @fn Lチカする
 */
int main(void)
{
    // GPIO 出力に設定。
    *(volatile unsigned int*)GPFSEL1 = (1 << (18));

    // セットして待つ、クリアして待つ、を繰り返す。
    while (1) 

        *(volatile unsigned int*)GPSET0 = (1 << 16);
        busy_wait(WAIT_COUNT);
        *(volatile unsigned int*)GPCLR0 = (1 << 16);
        busy_wait(WAIT_COUNT);
    }

    return 0;
}

/*
 * @fn delay関数
 */
void busy_wait(int wait_count)
{
    volatile unsigned int i;

    for (i = 0; i < wait_count; i++);
}


このプログラムではGPIO16(pin36)を制御してH/Lを繰り返します。
したがってpin36に抵抗を挟んでLEDを接続しておきます。
f:id:NATSU_NO_OMOIDE:20201220221858p:plain

スタートアップコードを作成する

続いてスタートアップコードをアセンブラで書きます。
スタックポインタは、ARM Coreの実行開始アドレスが0x0008 0000で、spはそこより若いアドレスの領域を指定します。

mov sp, #0x80000
bl  main

ビルドして動かしてみる

コードができたら以下のコマンドでビルドします。
今回はリンカスクリプトを作成しないのでldの-Tオプションを使用してtextアドレスを決めまています。

$ aarch64-rpi3-elf-as -o start.o start.S
$ aarch64-rpi3-elf-gcc -c -o main.o main.c
$ aarch64-rpi3-elf-ld -Ttext 0x80000 -o metal.elf start.o main.o
$ aarch64-rpi3-elf-objcopy -O binary metal.elf metal.img


ビルドが成功したらmetal.imgをSDカードの/bootにコピーし、config.txtのkernelパラメータを以下のように変更します。

kernel=metal.img


あとは再びSDカードをRPiに入れて電源をONします。
LEDがチカチカすれば成功です。
f:id:NATSU_NO_OMOIDE:20201220221902p:plain

ベアメタルアプリケーションをAMPで動かす

ここまでで、SMP動作するRaspberry Pi OSとベアメタルアプリケーションができました。 次はベアメタルアプリケーションをAMPで動作させるようにしていきます。

ベアメタル アプリケーションを改造する

まずはベアメタルアプリケーションを改造していきます。
start.Sを以下のようにしてスタックポインタの初期位置を変更します。
1GBあるメモリの内、upper512MBをベアメタルアプリケーションの領域としたので適当に0x30000000(768MB付近)としました。

mov sp, #0x30000000
bl  main

変更できたらビルドします。
前回同様、リンカスクリプトは作っていないので-Ttextを使用してupper512MBの先頭アドレス0x20000000にプログラムを配置するようにしています。

$ aarch64-rpi3-elf-as -o start.o start.S
$ aarch64-rpi3-elf-gcc -c -o main.o main.c
$ aarch64-rpi3-elf-ld -Ttext 0x20000000 -o metal.elf start.o main.o
$ aarch64-rpi3-elf-objcopy -O binary metal.elf metal.img

metal.imgができたらSDカードの/bootにコピーしておきます。
なおconfig.txtのkernelパラメータはu-bootが起動するようにしておきます。
これでベアメタルアプリーケーションの準備完了です。

ベアメタルアプリケーションをメモリに展開するプログラムを作成する

最初はRaspberry Pi OSからベアメタルアプリケーションをメモリに展開するプログラムを作成します。
このプログラムは0x20000000番地にベアメタルアプリケーションの実行ファイルを展開します。
ただ0x20000000番地はRaspberry Pi OSの管理対象外です。
なのでmmapを使用してファイルを展開するようにしています。

#include <stdio.h>    
#include <stdlib.h>  
#include <fcntl.h>   
#include <sys/mman.h>    
    
int main (int argc, char * argv [])  
{   
    int fd_mem;
    void *load_address;
    unsigned long fileLen;
    FILE *file;
    printf ("Opening %s\n",argv[1]);
    file=fopen(argv[1],"rb");
    
    fseek(file, 0, SEEK_END);
    fileLen=ftell(file);
    fseek(file, 0, SEEK_SET);
    printf ("File lenght %d\n",fileLen);
    
    printf ("Opening Mem %x\n",0x20000000);
    fd_mem = open("/dev/mem", O_RDWR);
    load_address = mmap(NULL, fileLen,PROT_READ|PROT_WRITE, MAP_SHARED, fd_mem, 0x20000000);
    
    fread(load_address, fileLen, 1, file);
    fclose(file);
}   

ソースが書けたらビルドして実行してみます。 以下のようになれば成功です。

$ gcc loadmetal.c -o loadmetal
$ sudo ./loadmetal metal.img
Opening metal.img
File lenght 160
Opening Mem 20000000

特定のメモリアドレスを読み書きするプログラムを作成する

お次は特定のメモリアドレスを読み書きするためにdevmemを用意します。
ソースコードは以下を流用しました。
https://bootlin.com/pub/mirror/devmem2.c
以下のコマンドでソースをダウンロードしてビルドします。

$ wegt https://bootlin.com/pub/mirror/devmem2.c
$ gcc devmem2.c -o devmem
$ sudo ./loadmetal metal.img

Raspberry Pi OSからベアメタルアプリケーションをキックしてみる

ここまでできたら、ようやくRaspberry Pi OSからベアメタルアプリケーションをキックさせます。
Raspberry Pi OSからCore 3をkickするためにメールボックスを使用します。
Core3_MBOX3_SETレジスタ= 0x400000BCに0x20000000を書き込むと、 Core 3はその位置にロードした実行可能ファイルにジャンプするようです。
メールボックスの仕様は以下が参考になりました。
https://qiita.com/toshinaga/items/e65004cc207a46d17b7b

devmemを使用して0x400000BCに0x20000000を書き込みます。
これでRaspberry Pi OSからベアメタルアプリケーションがキックされ、ベアメタルアプリケーションがGPIOを制御し始めます。
以下のコマンドを実行した直後にLEDがチカチカすれば成功です!

$ sudo ./devmem 0x400000bc w 0x20000000
/dev/mem opened.
Memory mapped at address 0x76f080000.
Value at address 0x400000BC (0x76f080bc): 0xACA0000
Written 0x20000000; readback 0xACA0000

Raspberry Pi OSとベアメタルアプリケーションで共有メモリを試す

最後に共有メモリを使用して、Raspberry Pi OSとベアメタルアプリケーション間のIPCをテストしてみます。

再びベアメタル アプリケーションを改造する

まずはベアメタルアプリケーションを改造します。

#define GPFSEL1 0x3F200004 /* GPIO のピン設定をするためのレジスタ */        
#define GPSET0  0x3F20001C /* GPIO を HIGH にするためのレジスタ */     
#define GPCLR0  0x3F200028 /* GPIO を LOW  にするためのレジスタ */     
        
typedef unsigned char bool;     
        
#define TRUE  1       
#define FALSE 0       
        
#define WAIT_COUNT 3000000        
        
volatile char delay = 0x54;      
volatile char count = 0x00;      
        
/*     
 * @fn wait_count だけビジーウェイト
 */     
void busy_wait(int wait_count);       
        
/*     
 * @fn Lチカする       
 */     
int main(void)        
{       
        
    // GPIO 出力に設定。     
    *(volatile unsigned int*)GPFSEL1 = (1 << (18));

    // セットして待つ、クリアして待つ、を繰り返す。
    while (1) {      
        *(volatile unsigned int*)GPSET0 = (1 << 16);
        busy_wait(WAIT_COUNT);      
        *(volatile unsigned int*)GPCLR0 = (1 << 16);
        busy_wait(WAIT_COUNT);      
            
            if( count == 0xFF )
            {
                count = 0x00;
            }
            else
            {
                count++;
            }
    }       
        
    return 0;        
}       
        
/*     
 * @fn delay関数     
 */     
void busy_wait(int wait_count)
{       
    volatile unsigned int i;
        
    for (i = 0; i < delay * 10000; i++);
}

このプログラムでは、delayとcountという変数を追加しました。
delay変数の値でLチカの速度が変化し、Lチカされる度にcount変数が加算されていきます。
ビルドが成功してバイナリができたら、以下のコマンドを実行して変数シンボルのアドレスを確認しておきます。

$nm metal.elf
20000000 t $a
20000008 t $a
2000009c t $a
20010119 b $d
20000088 t $d
20000114 t $d
20000118 t $d
2001011c B __bss_end__
20010119 B __bss_start
20010119 B __bss_start__
20010118 D __data_start
2001011c B __end__
2001011c B _bss_end__
20010119 D _edata__
2001011c B _end
00080000 N _stack
2000009c T busy_wait
20010119 B count
20010118 D delay
20000008 T main

ここではdelay変数は0x20010118に配置されました。

共有メモリで通信できるか試してみる

バイナリができたらこれまでと同様にSDカードにコピーし、loadmetalを使用してメモリに展開しておきます。
そしてdevmemでdelay変数のアドレスの値を読み込んでみます。
プログラムで定義した値(delay = 0x54)になっていれば成功です。

$ sudo ./devmem 0x20010118 b
/dev/mem opened.
Memory mapped at address 0x76fb1000
Value at address 0x20010118 (0x76fb1118): 0x54

続いてdelay変数の値をdevmemを使用して書き換えてみます。

sudo ./devmem 0x20010118 b 0x30
sudo ./devmem 0x20010118 b 0x20
sudo ./devmem 0x20010118 b 0x10

0x30→0x20→0x10に下げていくと高速点滅し、 逆に0x10→0x20→0x30に上げていくと低速点滅すれば成功です。
これでRaspberry Pi OSからメモリを介してベアメタルアプリケーションが制御できたことがわかります。


まだ共有メモリのキャッシュやコヒーレントなどの問題が残っているかもですが、とりあえず思っていたAMPシステムが構築できました。
かなり長くなりましたが、ここまで読んでくださりありがとうございますm(_ _)m
なにかの参考になれば幸いです。