RaspberryPi
dlang
Baremetal
D言語Day 18

D言語くんにもできるRaspberry Piベアメタルプログラミング

今回のソース

https://github.com/outlandkarasu-sandbox/pidos

はじめに

D言語のver. 2.076から、-betterCオプションが追加されました。

このオプションを使用すると、druntimeやPhobosはコンパイル時に参照されなくなり、ランタイムライブラリを使用するような機能は使えなくなります。
その代わり、コンパイル時のみで解決できるような機能は制限されません。

つまり、PhobosやC言語の標準ライブラリに依存していないD言語のサブセットを、コンパイラのチェックのもと使うことができます。

これを嬉しく思える人はたぶんごく一部ですが、以下のような場合に該当すると思われます。

  • OSやドライバなど、ランタイムライブラリを気軽に使用できない部分の実装
  • マイコンなどの組み込み機器向けのプログラミング
  • リアルタイム性の高い箇所で、GCなどのオーバーヘッドのある言語機能を排除したい場合
  • C言語などから呼び出されるライブラリを作成したい場合
    • GCなどD言語のランタイムの初期化が行えなかったり避けたい場合

関数や構造体に付ける属性としては、nogcnothrowpureなど使用機能を制限できるものがあります。またDMDの-vgcオプションを使用すれば、GC使用箇所の洗い出しを行うことも可能です。
しかし、標準ライブラリの参照を外したり、言語組み込みの機能を制限することは、betterCオプションなしでは難しかったり面倒だったりしました。

今回はこのbetterCオプションを使用して、Raspberry Piベアメタルプログラミングをやってみます。

つまり、Raspberry PiをOS無しで動かします。

OSが無いので、OSが提供するメモリ管理やIOなどを使用した標準ライブラリは使用できません。
ふつうこういう環境ではC/C++が使用されますが、そこをD言語で頑張ってゆきます。

環境準備

さて、まずは開発環境を用意します。
Raspberry Piの中身はARMのため、ARMの組み込み開発に使用するツールが必要になります。
基本的には下記が必要です。

  1. ARM向けのD言語クロスコンパイラ
  2. ARM向けのツールチェイン
    • アセンブラやリンカなどもろもろ
  3. 動作確認用のエミュレータ

1行目のD言語クロスコパイラだけ、そんなのあるのか……という感じですが、最近のD言語は結構環境が揃っています。本家のDMDは基本的にx86・amd64向けですが、GDCやLDCといったコンパイラはより多くのCPUに対応しています。

今回は、LLVMをバックエンドとしたLDCを使用します。

ARMむけのツールチェイン(Cコンパイラも含む)は、こちらからダウンロードできるそうです。

Docker

さて、色々ツールが必要になるわけですが、そんな、今後いつ使うことになるのかもよくわからないツール群を愛機の中に散らかしておきたくないと思うのが人情です。

そんな時に活用したいのがDockerです。これで開発環境をまるごとコンテナ化しておけば、以下の点で割と捗ります。

  • Dockerさえ入っていれば、開発環境がそのまま再現できる
  • ホストのシステムをそれほど汚さない
  • 環境の復元にはテキストのDockerfileだけあれば良いので、不要になった環境(Dockerイメージなど)を気軽に消せる

というわけで、DockerとDockerfileで開発環境を用意します。

# なんとなくプレーンそうなdebianを使用
FROM library/debian

# 作業用ユーザーIDを指定可能
ARG USER_ID

# apt-getでインストールできる必要ツールをインストール。
# 不要なキャッシュファイルは消しておく
RUN apt-get update && apt-get -y install apt-utils && apt-get -y install wget vim xz-utils bzip2 && apt-get clean

# 最新版LDCのバイナリをダウンロードして/optに展開
RUN wget https://github.com/ldc-developers/ldc/releases/download/v1.7.0-beta1/ldc2-1.7.0-beta1-linux-x86_64.tar.xz \
    -O /tmp/ldc2.tar.xz && \
  tar xvJf /tmp/ldc2.tar.xz -C /opt && \
  ln -s /opt/ldc2-1.7.0-beta1-linux-x86_64 /opt/ldc2 && \
  rm -f /tmp/ldc2.tar.xz

# ARMツールチェクをダウンロードして/optに展開
RUN wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/7-2017q4/gcc-arm-none-eabi-7-2017-q4-major-linux.tar.bz2 \
    -O /tmp/gcc-arm-none-eabi.tar.bz2 && \
  tar xvjf /tmp/gcc-arm-none-eabi.tar.bz2 -C /opt && \
  ln -s /opt/gcc-arm-none-eabi-7-2017-q4-major /opt/gcc-arm-none-eabi && \
  rm -f /tmp/gcc-arm-none-eabi.tar.bz2

# 作業用ユーザーを作成する。
RUN useradd -u ${USER_ID} -m pidos && \
  echo 'export PATH=${PATH}:/opt/ldc2/bin:/opt/gcc-arm-none-eabi/bin' >> /etc/profile

# raspi2をエミュレーション可能な最新QEMUをソースからビルドする。
RUN apt-get -y install git libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev build-essential && apt-get clean
RUN cd /usr/local/src && \
  git clone git://git.qemu-project.org/qemu.git && \
  cd ./qemu && \
  ./configure --target-list=aarch64-softmmu,arm-softmmu && \
  make && make install && make clean && \
  cd && rm -rf /usr/local/src/qemu

ビルド・実行は下記のようになります。

docker_build.sh
# ホストマシンの作業ユーザーと同じUIDでユーザーを作るよう指定してDockerイメージをビルドする
docker build --build-arg USER_ID=${UID} -t pidos/build:latest .
docker_run.sh
docker run --rm -i ¥ # インタラクティブモードで起動。終了時にはコンテナを消す。
    -u pidos ¥ # 作業用ユーザーを使用
    -v $(pwd)/pidos:/home/pidos/pidos ¥ # ホストのカレントディレクトリ/pidosをボリュームとしてマウント
    -w /home/pidos/pidos ¥ # 作業ディレクトリをマウントしたボリュームに指定
    -t pidos/build ¥ # さっきビルドしたイメージを指定
    bash -l # bashを実行

これで、各種ツールを備えたDebian/GNU Linuxにログインして作業を行えます。

C to D

さて、準備ができたらARMでのプログラミングを行っていくのですが、当然私は初心者なので、何かしら元になる情報が必要です。

C言語の情報源

色々探したところ、OSDev.orgというところに各種アーキテクチャのOS実装向けの情報がたくさんまとまっています。
なんとD言語のBare Bones(x86向けですが……)も用意されています。

今回はこちらで公開されているC言語のRaspberry Pi Bare Bonesパクり(ライセンスはCC0らしいので……)、D言語で書き直していきます。

D言語に変換

C言語版のRaspberry Pi Bare Bonesのソースは下記の通りです。

  • boot.S
    • ブート直後に実行されるコード。アセンブラ
  • kernel.c
    • boot.Sから呼び出されるコード。OSで言えばカーネルに当たる部分。
  • linker.ld
    • リンカスクリプト。各種コードなどのバイナリ上での配置を指定する。

この中で書き換える必要があるのは、kernel.cだけです。これをkernel.dにすれば完成です。

インラインアセンブラ

C言語をD言語にするのはごく簡単ですが、インラインアセンブラの部分はあまり馴染みがなくて大変でした。
まず、C言語版ではGCCのインラインアセンブラで記載されています。今回はD言語版ではLDCを使用するので、LLVMのインラインアセンブラに直す必要があります。

今回コーディングで一番難しかったのが、このdelay関数です。delay(何もしないループ)が一番難しいってどういうことよ……。

/* C言語版 */
static inline void delay(int32_t count)
{
    asm volatile("__delay_%=: subs %[count], %[count], #1; bne __delay_%=\n"
        : "=r"(count): [count]"0"(count) : "cc");
}

私のような高級言語育ちのひ弱なプログラマからすると、なんだこの呪文は、という感じです。
これを少し整理します。

static inline void delay(int32_t count)
{
    asm volatile(
        "__delay_%=:" /* ラベル */
        "    subs %[count], %[count], #1;" /* countから1を引く。ステータスレジスタ変更付き */
        "    bne __delay_%=\n" /* 減算結果が0でなければ__delayに分岐 */
        : "=r"(count) /* 引数のcountへレジスタ越しに出力 */
        : [count]"0"(count)/* 引数のcountを最初(0)の出力レジスタ越しに入力 */
        : "cc"); /* ステータスレジスタの値が変わってしまうので、復元するよう指定 */
}

色々ぐぐった結果、上記のような雰囲気のようです。
これを、LDC(LLVM)のインラインアセンブラの形に書き直します。

// LDCでインラインアセンブラを使用するために必要
import ldc.llvmasm;

void delay(int count) {
    __asm(`
        1: subs $0, $0, #1;
        bne 1b
        `, "r,~{cpsr}", count); // cpsrはステータスレジスタ。GCC版のccに該当する。
}

volatile

インラインアセンブラ以外の部分は、基本的にメモリマップドIOで単にアドレスを叩いているだけなので、型などを修正するだけで普通にD言語のコードとして動きます。
ただ、C言語でのvolatileの部分が気になります。D言語ではすでにvolatileはdeprecatedです。型修飾子としてもありません。

その代わり、core.bitopvolatileLoadvolatileStoreが用意されているので、こちらを使用してみます。

kernel.d
import core.bitop: volatileLoad, volatileStore;

void mmio_write(uint reg, uint data) {
    volatileStore(cast(uint*)reg, data);
}

uint mmio_read(uint reg) {
    return volatileLoad(cast(uint*)reg);
}

betterCだとランタイムライブラリが使用できないはずなので心配ですが、coreパッケージは大丈夫なのでしょうか……。とりあえず動いたのでよしとします。

ビルド

ビルドについて、GCCでkernel.cをコンパイルしている部分をLDCに置き換えることになります。
また、例外情報(?)のセクションが作られてしまい、そこがリンクエラーになるので、リンカスクリプトで破棄するようにします。

linker.ld
SECTIONS
{
    /* 破棄するセクションを指定 */
    /DISCARD/ :
    {
        *(.ARM.extab*)
        *(.ARM.exidx*)
    }

    /* 後略 */
}

このあたりの情報は、D WikiのMinimal semihosted ARM Cortex-M "Hello World"を参考にしました。

最終的なビルドコマンドは下記の通りです。

build.sh
#!/bin/sh

# boot.Sをアセンブリ
arm-none-eabi-gcc -mcpu=arm1176jzf-s -fpic -ffreestanding -c boot.S -o boot.o

# C言語版のコンパイル
#arm-none-eabi-gcc -mcpu=arm1176jzf-s -fpic -ffreestanding -std=gnu99 -c kernel.c -o kernel.o -O2 -Wall -Wextra

# D言語版ではこちら
ldc2 -mtriple=arm-none-linux-eabi -mcpu=arm1176jzf-s -betterC -c kernel.d

# オブジェクトファイルをリンクする 
arm-none-eabi-gcc -T linker.ld -o kernel.elf -ffreestanding -O2 -nostdlib boot.o kernel.o

# ELFフォーマットをバイナリに変換
arm-none-eabi-objcopy kernel.elf -O binary kernel.img

優秀な人はぜひMakefileを書きましょう。

QEMUで実行

ここまででビルド等が行えるようになったら、とりあえずエミュレータ(QEMU)で動かしてみます。
実機とエミュレータで微妙に修正しなければならない点があるので、注意が必要です。

QEMUで変えなければならない点

QEMUでは、ブート直後の開始アドレスが異なります。実機は0x8000から開始しますが、QEMUでは0x10000から開始されます。そこで、linker.ldでコードの配置アドレスを修正する必要があります。

linker.ld
    /* Starts at LOADER_ADDR. */
    /*. = 0x8000; 実機の場合はこちら */
    . = 0x10000;
    __start = .;
    __text_start = .;
    .text :
    {
        KEEP(*(.text.boot))
        *(.text)
    }
    . = ALIGN(4096); /* align to page size */
    __text_end = .;

実行

ビルドが行えたら、QEMUに適切なオプションを与えて実行します。

qemu.sh
#!/bin/sh

qemu-system-arm \
  -M raspi2 \ # Raspberry Pi 2 のエミュレーション
  -cpu arm1176 \ # CPUもRaspberry Pi 2 のものを指定
  -m 256 \ # メモリ(MB)
  -nographic \ # コンソールから実行するのでディスプレイ等なし
  -kernel kernel.elf # 実行するカーネルを指定。QEMUはELF形式を直接実行可能
pidos@db017d33992f:~/pidos$ ./qemu.sh 
  _   _
 (_) (_)
/______ \
\\(O(O \/
 | | | |
 | |_| |
/______/
 <   >
(_) (_)
  _   _
 (_) (_)
/______ \
\\(O(O \/
 | | | |
 | |_| |
/______/
 <   >
(_) (_)
  _   _
 (_) (_)
/______ \
\\(O(O \/
 | | | |
 | |_| |
/______/
 <   >
(_) (_)

今回はカーネルに少し手を加え、UARTに'd'が入ってきた時にD言語くんAAが出力されるようにしました。
QEMUのシリアルポートはコンソールと繋がるので、キーボードのdを押すたびにD言語くんが表示されることになります。ちゃんと動きましたね。

実機確認

エミュレータで動作確認ができたら、いよいよ実機で動かします。以下の準備が必要です。

  • Raspberry Pi 2 または 3 実機
    • Raspberry Pi 1の場合は少し修正が必要です。私が持っているのは実は1の方でした……。
  • OS書き込み済みSDカード
  • USB-Serialケーブル

実機で変えなければならない点

前述の通りブート後の実行アドレスが異なるので、修正が必要です。おそらくプリプロセッサなどでもっとちゃんとやれます……。

linker.ld
    /* Starts at LOADER_ADDR. */
    . = 0x8000;
    /* . = 0x10000; QEMUの場合はこちら */
    __start = .;
    __text_start = .;
    .text :
    {
        KEEP(*(.text.boot))
        *(.text)
    }
    . = ALIGN(4096); /* align to page size */
    __text_end = .;

持っていたRaspberry Piが実は1だった場合

私です……。2014年頃までのRaspberry Piは2ではなく1なので、ソースを微妙に修正する必要があります。

kernel.d
// 前略

enum
{
    // The GPIO registers base address.
    //GPIO_BASE = 0x3F200000, // for raspi2 & 3
    GPIO_BASE = 0x20200000, // for raspi

    // 後略

SDカード焼き込み

SDカードは、通常のRaspbianを入れたものを再利用できます。

Raspbianの入っているSDカードを参照すると、下記のようなファイルが見えると思います。

bootcode.bin  fixup.dat     kernel.img            start.elf
cmdline.txt   fixup_cd.dat  kernel_cutdown.img    start_cd.elf
config.txt    issue.txt     kernel_emergency.img

Raspberry Piは、ブートシーケンスでまずGPUが起動して、その後色々あって、そしてkernel.imgが起動されるようになっています。
そこで、kernel.imgを差し替えれば、自作のカーネルが動くことになります。

書き込みが完了したら、SDカードをRaspberry Piに挿して起動させます。

USB-Serialケーブル接続

この辺りなどを参考にすれば大丈夫です。

結果

rpi_baremetal_d.jpg

これから

以前にSHOOさんが苦労されていたようなことが、最近のD言語の周辺では割と解消されているようです。mbedなどで試してみないとまだ分かりませんが……。

今回のソースを出発点に、以下の公式リファレンスなどを参考にどんどん機能を追加すれば、小さなOSは作成できそうです。

GPUを直接叩いてゲーム機にしたり、いま流行りのビットコイン採掘をやらせても面白いかもしれません。高級GPUと旧式Raspberry Piでは、価格性能比だとどちらが勝つのでしょうか?

今回すぐ試せたように、D言語は生成されるオブジェクトファイルもC言語中心のツールチェインと親和性が高いので、組み込み開発でC言語では面倒になるような部分にD言語を採用して使用することも今後は可能かもしれません。

また、せっかくだからmbedなどのもっとマイコンらしいマイコンでも動かしてみたい気がします。実行コードサイズの制約をどこまで回避できるか……。