RISC-V ベアメタルプログラミング

2019/06/25

ベアメタルプログラミング

単純な一つのメモリ空間だけがある環境で動作する実行ファイルを生成するには、Linux等のOSが動作している場合の仮定がほとんどなりたたない。一番異なるのは、メモリの管理とその初期化について。簡単なアプリケーションのテストをするときに、本当に簡単な時にはscanf/printfなどの標準ライブラリの関数をつかって数値の入出力をする。ベアメタル環境ではキーボードもディスプレイもディスクもネットワークもないので、それが出来ない。メモリにある命令を実行し、メモリからデータを読み、結果をメモリに書き戻す、しかできない。これはGPU kernelのプログラミングや、Cell Broadband EngineのSPU用プログラミングと同じである。よって、CUDA/OpenCLのコンパイラがしていることを手動で行う必要がある。

アドレスの計算などを全て手動でやるのは難しすぎるので、リンカと呼ばれる便利なツールを利用する。リンカを使うことで、C言語のソース内のポインタを適切に処置してくれる。リンカの詳細な使い方は 『リンカ・ローダ実践開発テクニック』 https://www.amazon.co.jp/dp/4789838072 という書籍があるほどなので、買うなり調べるとわかる。

ここでのアプリケーションは、重力多体問題の計算。Plummer球と呼ばれる粒子分布のデータがテキストファイルに用意してある。fscanfは使えないので、事前にこのデータをメモリ上に書き込んで、その粒子データから重力加速度とポテンシャルエネルギーを計算する。

このためのシンプルなリンカスクリプトは以下になる。

SECTIONS
{
   . = 0x80;
   .text :  {
         start.o (.text);
         main.o (.text);
   }
}

今の環境では、最初の命令は0x80のアドレスから読み出されるので、最初にその指定をしている。そのあとは"start.o"と"main.o"の2つのオブジェクトファイルをtextセクションに配置するという指定。“main.o"はstartup routineと呼ばれる、main関数を実行するための前処理と後処理をするプログラムである。ここでは、以下のアセンブラファイルを使った。

	.text
	.align	2
	call main
	nop
	nop
	nop
	nop
	nop
DUMMY:
	nop
	j DUMMY

	.section .data
	.global base0
base0:
	.incbin "model0.bin"

前半はtextセクション。今回は前処理はない。main関数を呼びだして、後処理としてはnop命令をいくつか。これは都合によりmain関数の終了を検知するため。dataセクションには"base0"という名前のグローバルなシンボルを定義している。メモリ上でこの場所に"model0.bin"というファイルの内容を配置する。“model0.bin"は、粒子分布のデータをbinaryに変換したファイル。元のファイルは、粒子あたりでは質量、位置ベクトル、速度ベクトルの7ワードのAoS形式でテキストとして保存されている。以下の形式。

7.8125000000000000e-03
1.0563020562847498e+00  8.8754524753811037e-01  6.2388603500053069e-02
-5.8789402544138647e-01  2.8313370516718223e-01 -2.3444415855454401e-01
7.8125000000000000e-03
-3.0753395617152618e-01 -9.1157976455007672e-01  1.9906852225340521e-01
2.9809718024328968e-01  9.5743848924208952e-01 -3.3635174478177260e-01
...

この粒子データを読み込み、単精度変数に変換しSoA形式に変換して、x座標、y座標、z座標と質量mの4個の配列を並べたファイルとして"model0.bin"を作成した。粒子数が128だとすると、読み込まれたファイルはメモリ上で

const n = 128
static float x[n], y[n], z[n], m[n];

のように配置されるはず。このデータ部分の先頭のアドレスが"base0"となる。textセクションのサイズは、main関数とそこから呼び出される命令等のサイズによるため、コンパイルするまでわからない。このリンカスクリプトの定義により、リンカは"base0"のアドレスを適切に計算をしてくれる。この準備をした上で実行するコードを以下のように作成した。

重力多体問題の加速度とポテンシャルエネルギーを計算するコード。

実際の処理(アプリケーション)はgrav_kernel関数に記述した。OpenCLでいうところのカーネル関数に相当する。main関数では、カーネル関数を呼び出すための準備をしている。この例では"base0"のアドレスを元にして、粒子データのアドレスを計算しているだけ。位置ベクトルと質量のあとに、加速度ベクトル(ax, ay, az)とポテンシャルエネルギーptを配置することにした。さらに、resという変数をその後に置くこととして、grav_kernelの最後でresに計算結果(ポテンシャルエネルギーの総和)を保存している。

このPlummer球の粒子データは、ポテンシャルエネルギーの総和が-0.5になるようスケーリングされている。これをもって計算が正しいかどうかをひとまずはチェックできる。この計算には単精度浮動小数点回路の加算や乗算だけでなく、除算と平方根が正しく計算できなければならない。

このアプリケーションをコンパイルするための手順について。現状、以下のまどろっこしいスクリプトで実行ファイルのメモリイメージを作成している。

#!/bin/zsh
DIR=~/x-tools/riscv32-unknown-elf/bin
STARTUP=startup

# CのコードをコンパイルしてRISC-V assembly main.s として保存
$DIR/riscv32-unknown-elf-gcc -march=rv32imf -mabi=ilp32f -S -O3 main_grav2.c -o main.s

# main.s をアセンブルする
$DIR/riscv32-unknown-elf-as -march=rv32imf main.s -o main.o

# startup routineをアセンブルする
$DIR/riscv32-unknown-elf-as -march=rv32imf $STARTUP/start.s -o start.o

# リンカスクリプトを指定して start.o と main.o をリンクする。実行ファイル"a.out"が生成される。
$DIR/riscv32-unknown-elf-ld -T$STARTUP/test.ld start.o main.o 

# 実行ファイルから必要な部分だけを切り出してメモリイメージを作成
$DIR/riscv32-unknown-elf-objcopy -I elf32-littleriscv \
-j .rodata -j .srodata.cst4 -j .text -j .text.startup -j .data \
a.out -O binary a.out.binary

メモリイメージには実行するコードと粒子データだけでなく、定数値(.srodata.cst4の部分)が含まれている。生成された"a.out.binary"を使って、RTLシミュレーションや論理合成用のメモリ初期化ファイル等を作成することができる。

FPGAで実行した結果

ここに示したスクリプトでコンパイル、リンクをした結果、base0のアドレスは0x24cとなった。そこを先頭に、合計8 x 128ワードの配列データを配置しているため、res[0]のアドレスは0x24c + 1024x4 = 0x124cとなる。

計算結果は”-4.997978e-01"となった。その前の整数値はサイクル数を表す。