はじめに
以前書いた記事 では Raspberry Pi 上で動くベアメタルプログラムとして、 UART を使ってみるコードを試してみました。
このコードは C 言語で書かれていたのですが、最近話題の Zen 言語でも同じことができるといいなと思い、Zen 言語の勉強のためにやってみました。
インスト―ルとプロジェクトの作成
公式のドキュメントにある インストール手順 と Hello World に沿うとできました。
コード
コードは以下の Github のレポジトリ に置きました。
src/main.zen というのが主に今回書いたコードになります。
コード自体は短いですので、この記事の末尾にすべて貼っておきます。
コードの解説
このコード自体は前回の記事同様、最初に Hello World を UART 経由で出力し、その後 UART からの入力をエコーバックするものです。
一つだけ処理内容が変わっている点があり、それは改行コードを CR や LF のみでも必ず CRLF で出力するようにしている点です。
以下では、今回書いたコードにおいて C 言語と違う部分について説明したいと思います。
説明はソースコードの上から順にしていきます。
各章で、コードのどの部分で説明した内容が使われているか書いていますので、適宜コードを見ていただければと思います。
値からポインタへのキャスト
mmio_read()
や mmio_write()
では、もとの C 言語のコードをリスペクトして u32 (unsigned 32 bit) の変数を受け取っています。
C 言語であれば息をするようにこの値をポインタにキャストしてメモリアクセスすればよいのですが、Zen ではこのキャストをかなり明示的に書く必要があります。
しかも、ポインタへのキャストは usize
型からのキャストのみ許されているため、 u32 –> usize –> *u32 という順でキャストする必要があります。
(まぁ、レジスタアドレスの定数を *u32 型で定義すればよいと思います…)
inline fn mmio_read (reg: u32) u32 {
const _reg_addr: usize = @intCast (usize, reg);
const _reg_ptr: *volatile u32 = @intToPtr(*u32, _reg_addr);
return _reg_ptr.*;
}
ポインタの mutability
ポインタが指す先の値を変更するには、ポインタの型に mut
キーワードを付ける必要があります。
mmio_write()
ではそのようなポインタを使っています。
inline fn mmio_write(reg: u32, data: u32) void {
const _reg_addr: usize = @intCast (usize, reg);
// Note: Require 'mut' keyword
const _reg_ptr: *mut volatile u32 = @intToPtr(*mut volatile u32, _reg_addr);
_reg_ptr.* = data;
}
`
定数定義
Zen には C 言語のようなマクロはないので、グローバルな定数として定義しました。
ループにおける条件式
Zen の while ループにおける条件式は ブール型を返す必要があります。
このため、 C でよくある while(reg & 0xff)
みたいな書き方はコンパイルエラーになります。
Zen では while((reg & 0xff) != 0)
のように書く必要がありそうです。
(もしかしたらもっといい書き方があるかもしれませんが…)
なお、Boolean 値の and や or 演算は &&
や ||
ではなく and
と or
です。
uart_getc()
と __uart_putc()
に該当コードがあります。
fn __uart_putc(c: u8) void {
// Note: The condition for while loop must be bool.
while ((mmio_read (UART0_FR) & (1 << 5) != 0)) {}
mmio_write (UART0_DR, c);
}
構造体のスコープ
Zen の構造体では構造体型の変数ごとに作られる変数(フィールド) とは別に、 Java のクラス変数のように構造体の定義に対して一つだけ存在する変数を定義できます。
また、これは構造体定義のあるスコープ内でのグローバルな変数になります。
これを利用して、関数内で構造体を定義しその中にグローバルな変数を定義すると、 C 言語の static 変数みたいなことができます。
詳しくは Zen のドキュメントの構造体#変数/定数 や 識別子のスコープ#グローバル を読んでいただければと思います。
また、このあたりの挙動を理解するのに役立つ(かもしれない?)コードを書いてみたので置いておきます。
https://gist.github.com/fukai-t/50d061b5f7d7da218b13dd1c34891f50
今回のコードでは、 uart_putc()
で改行コードを変換するためにこれを使っています。
変換処理のために、この関数では直前に出力した文字を覚えるような実装になっています。
この文字は uart_putc()
だけがアクセスできるばよいです。
これを実現するために、C 言語における関数内の static 変数のように Zen の構造体を使っています。
fn uart_putc(c: u8) void {
const LastChar = struct {
var c: u8 = 0;
};
if (LastChar.c != '\r' or c != '\n') {
__uart_putc (c);
}
// \r --> \r\n
if (c == '\r') {
__uart_putc ('\n');
}
LastChar.c = c;
}
ビット数が減少するキャスト
Zen では bit 数が減る方向のキャスト (e.g. u32 –> u8) は明示的に書く必要があります。
このため、C 言語でありがちな return mmio_read() & 0xff
で u8 型の値を返すといったコードは Zen では許されません。
代わりに、指定した下位 bit だけ取り出しキャストする @truncate()
という組み込み関数が使えます。
今回のコードでは、 uart_getc()
で @truncate()
を使っています。
fn uart_getc() u8 {
// Note: The condition for while loop must be bool.
while ((mmio_read (UART0_FR) & (1 << 4)) != 0) {}
return @truncate(u8, mmio_read (UART0_DR));
}
スライス
スライス は Zen 特有の型で、配列の先頭ポインタ + 要素数情報 のような内部構造になっています。
内部に要素数の情報を持っているため、後述の for ループなどで簡単に扱えます。
また、配列と違い要素数には実行時の値が使えます。
今回のコードでは、 __uart_puts()
の引数の型がスライス ([]u8
) になっています。
(ちなみに、最初僕はポインタで書こうとしましたが、何か色々ハマったので、スライスを使うことにしました。)
for ループ
for ループですが C 言語のものとは違い、むしろ python や bash の for に近いです。
ちなみに、C 言語のような for ループは Zen では 変化式つきの while でかけます。
今回のコードでは、 __uart_puts()
内で for ループを使っています。
ここでは、スライスから値を一つずつ読み出し処理する for ループを記述しています。
fn __uart_puts(str: []u8) void {
// Note: Zen's for loop is much different from that of C language
for (str) |c| {
uart_putc (c);
}
}
起動コード
以前書いた記事 では起動コードをアセンブラで書いていました。
今回もそれは変わりませんが、Zen を使ってアセンブルするために、Zen のインラインアセンブラに置き換えています。
ソースコードは こんな感じ です。
ビルド方法
Zen にはちゃんとしたビルドシステムがあるのですが、今回は面倒だったので以下のようにコマンドを直接叩いています。
同じ内容のスクリプトもリポジトリに置いています –> build.sh
リンカスクリプトは前回と同じです。
$ ## main のソースをオブジェクトファイルへとコンパイル
$ zen build-obj -fPIC -target aarch64-freestanding-eabi -mcpu cortex-a53 src/main.zen
$ ## アセンブラコードのソースをオブジェクトファイルへとコンパイル
$ zen build-obj -fPIC -target aarch64-freestanding-eabi -mcpu cortex-a53 src/boot_asm.zen
$ ## オブジェクトファイルから ELF ファイルを生成
$ zen build-exe --linker-script src/linker.ld -fPIC -target aarch64-freestanding-eabi -mcpu cortex-a53 --object boot_asm.o --object main.o --name zen-test-uart
これで、 zen-test-uart
という ELF ファイルができます。
動かしてみる
ビルドしてできた ELF ファイルを qemu-system-aarch64
が動くマシンにもっていき、以下のコマンドで実行できます。
aarch64-linux-gnu-objcopy
を使っている理由などは昨日書いた記事を参考にしてください –> qemu-system-aarch64 の raspi3 エミュレートをシングルコアで動かす
$ aarch64-linux-gnu-objcopy -O binary zen-test-uart kernel8.img
$ ~/qemu/aarch64-softmmu/qemu-system-aarch64 -M raspi3 -nographic -kernel kernel8.img
おわりに: 感想など
というわけで、Zen で Raspberry Pi (QEMU ですが) で動くコードが書けました。
今回説明した Zen 言語の機能はほんの一部で、まだまだ色々な機能があります。
しかし、MMIO から文字列の扱いまで今回できたため、低レイヤーのコードはこれでだいたいかけるんじゃないかという気がします(気がするだけかもしれませんが)。
もともと C 言語の置き換え狙っていることもあり、ハードウェアにアクセスする低レイヤの開発も全然大丈夫そうです。
むしろ、クロスコンパイル環境などを別途整える必要もないので環境構築はだいぶ楽かもしれません。
(ちなみに今回は WSL 上でコンパイル、別のサーバーマシンで実行しています。)
Zen で書くと、 C 言語では多少煩雑になるコードも少しすっきりする気がします。
C と違う部分も多いので最初は引っかかる部分もありますが、慣れれば結構書きやすそうだなと思いました。
また C 言語よりもコンパイラのチェックが厳しいため、割と安心して書けます。
個人的には情報の少なさとコンパイラのエラーメッセージが改善するとよりよいかなと思いますが、それは今後に期待という感じです。
付録: main.zen
inline fn mmio_write(reg: u32, data: u32) void {
const _reg_addr: usize = @intCast (usize, reg);
// Note: Require 'mut' keyword
const _reg_ptr: *mut volatile u32 = @intToPtr(*mut volatile u32, _reg_addr);
_reg_ptr.* = data;
}
inline fn mmio_read (reg: u32) u32 {
const _reg_addr: usize = @intCast (usize, reg);
const _reg_ptr: *volatile u32 = @intToPtr(*u32, _reg_addr);
return _reg_ptr.*;
}
// The base address for UART.
const UART0_BASE: u32 = 0x3F201000; // for raspi2 & 3, 0x20201000 for raspi1
// The offsets for reach register for the UART.
const UART0_DR: u32 = (UART0_BASE + 0x00);
const UART0_FR: u32 = (UART0_BASE + 0x18);
fn __uart_putc(c: u8) void {
// Note: The condition for while loop must be bool.
while ((mmio_read (UART0_FR) & (1 << 5) != 0)) {}
mmio_write (UART0_DR, c);
}
fn uart_putc(c: u8) void {
const LastChar = struct {
var c: u8 = 0;
};
if (LastChar.c != '\r' or c != '\n') {
__uart_putc (c);
}
// \r --> \r\n
if (c == '\r') {
__uart_putc ('\n');
}
LastChar.c = c;
}
fn uart_getc() u8 {
// Note: The condition for while loop must be bool.
while ((mmio_read (UART0_FR) & (1 << 4)) != 0) {}
return @truncate(u8, mmio_read (UART0_DR));
}
// Note: Use slice instead of pointer
fn __uart_puts(str: []u8) void {
// Note: Zen's for loop is much different from that of C language
for (str) |c| {
uart_putc (c);
}
}
fn __uart_puts(str: []u8) void {
// Note: Zen's for loop is much different from that of C language
for (str) |c| {
uart_putc (c);
}
}
// Note: export
export fn kernel_main (r0: u32, r1: u32, atags: u32) void {
uart_puts ("Hello, kernel World with Zen!\n");
while(true) {
uart_putc (uart_getc ());
}
}