Edited at

蟹さんをBare Metalにしてmrubyを動かしてみた。

RTL8196でFreeBSDを試して、手軽にバイナリを焼いて実行できる事に気がついたのでBare Metalでmrubyを動かしてみたくなりました。

写真(2018-01-24 10.38).jpg

Bare MetalとはOSが入っていない、素の状態で使う事です。

RTLBM-STACK.png

RTL8196CはLexraという会社が大昔に開発したmips1なCPUを使ったSOCです。

自分の開発環境はFreeBSDです。もちろんLinuxでもだいたい同じ流れでできると思います。


コンパイラーの準備

コンパイラーはREALTEKが提供しているLinux用のGCCを使う事にします。

ここにあるrtl819x-SDK-v3.2.3のrtl819x/toolchain/rsdk-1.5.5-5281-EB-2.6.30-0.9.30.3-110714を使いました。この中には二つ別のgccが入っていて、mips-linux/binの方を使います。これはbinの下のgccはターゲットの4181で使えないためです。cc1も必要なのでlibexec/gcc/mips-linux/4.4.5-1.5.5p4にもパスを通しておきます。

いつものようにFreeBSDのlinuxエミュレーションを使ってlinuxのelfを実行します。

% sudo kldload linux

% sudo pkg install linux_base-c6
% ./gcc -v
Using built-in specs.
Target: mips-linux
Configured with: RSDK Builder release 1.5
Thread model: posix
gcc version 4.4.5-1.5.5p4 (GCC)


libc

これでコンパイラーが用意できたので、次はlibcを用意します。SDKにもuClibcのlibcは入っているのですが、使い方がよく分からなかったので組み込み系でよく使われるnewlibをコンパイルしました。使ったファイルはnewlib-2.5.0.20171222のもので、二つのファイルでエラーがありましたが、それを修正したらすんなり、libcができました。newlibは複雑なconfigureになっていて、最初なかなかビルドが通らず困りました。またエラーがおきても何がエラーなのか分かりづらいです。

修正は

newlib/libc/include/stdlib.h の __deprecated__を2行コメントアウト

newlib/libc/include/sys/_intsup.h の Unable to determineを2行コメントアウト

% ./configure --target=mips --host=amd64

% gmake

ビルドする時はmips-linux/binの下でmips-cc,mips-as,mips-arをシンボリックリンクで作っておきます。

コンパイルが通るとmipsディレクトリが出来てそこに、コンパイラの対応アーキテクチャ(march)毎にライブラリを作ってくれるようです。

libcの提供する関数には、strlenなどのポインターや変数のだけを処理するものと、mallocの様なheapを使うもの(下にあるsbrkが必要です)、stackの特殊操作をおこなうsetjmpのようなものがあります。これらが正しく使えるかもテストコードを書いて確認しました。


mruby

次はmrubyのライブラリを作ります。libcだけでビルドするためにfloat関係のサポートを止めてlibmを使わない設定でクロスビルドしました。ソースはgithubのmasterをcloneしました。

MRuby::CrossBuild.new('realtek') do |conf|

toolchain :gcc
conf.cc.command = 'mips-cc'
conf.archiver.command = 'mips-ar'

cc.defines << %w(MRB_DISABLE_STDIO)
cc.defines << %w(MRB_WITHOUT_FLOAT)
conf.cc.flags << "-march=4181"
conf.cc.flags << "-Os -g -fno-pic -mno-abicalls"
conf.cc.flags << "-pipe -mlong-calls"
conf.cc.flags << "-mips16"
conf.cc.include_paths = ["#{root}/include", "../newlib-2.5.0.20171222/newlib/libc/include"]

# conf.build_mrbtest_lib_only

end

これでビルドするとbuild/realtek/lib/の下にlibmruby.aが出来上がります。include/mrbconf.hのMRB_METHOD_TABLE_INLINEはコメントアウトする必要がありました。 デフォルトを外してもらって修正必要なくなりました。またconf.big_endianを追加してもったので設定すると使用メモリが少なくなるとの事です。


実行ELFをビルドする

ELFをビルドしてbootが読み込んでくれるイメージを作る流れはこのようになります。

RTL-IMG.png

ELFのメモリ配置は以下のようにします。このチップは物理メモリが0x8000 0000からあり先頭の8Mを使うようにしてみました。

RTLBM-MEM.png

処理の流れはこのようになります。

RTLBM-LANG.png

startポイントは以下のようにアセンブラで作りました。


start.S

#define INITIAL_SP      0x80800000

.globl main
.globl _init

.text
.globl start
.ent start
.set reorder
start:
li $sp, INITIAL_SP
nop
/* allocate space for result */
addiu $sp, $sp, -8
jal main
/* store the result */
sw $2, 4($sp)
addiu $sp, $sp, 8
/* watchdog reset */
li $3, 0xb800311c
li $4, 0
sb $4,0($3)
loop:
j loop
.end start


cの関数のmainをコールして、戻ってきたらwatchdogでリセットするようにしてあります。

libcで不足している関数はダミーとsbrkはheapのアドレスを返すようにつくりました。


syscalls.c

#include <_ansi.h>

#include <sys/types.h>
#include <sys/stat.h>

extern char _end[];

int kill()
{
return 0;
}

int getpid()
{
return 0;
}

int _exit()
{
return 0;
}

void *
_sbrk (incr)
int incr;
{
static char * heap_end = _end;
char * prev_heap_end;

prev_heap_end = heap_end;
heap_end += incr;

return (void *) prev_heap_end;
}
char * sbrk (int) __attribute__((weak, alias ("_sbrk")));


mainはこんな感じです。


main.c

#include <mruby.h>

#include <mruby/string.h>
#include <mruby/irep.h>

#include "hoge.c"

extern char _end[];
extern char _fbss[];

void put(char ch)
{
unsigned char *prt;
unsigned char reg;
int i;

i = 0;
prt = (unsigned char *)0xb8002014;
while (1) {
++i;
if (i >=0x6000)
break;
reg = *prt;
if (reg & 0x20)
break;
}
prt = (unsigned char *)0xb8002000;
*prt = ch;
}

mrb_value myputs(mrb_state *mrb, mrb_value self){
char *ptr;
mrb_value val;
mrb_get_args(mrb, "S", &val);
for (ptr = RSTRING_PTR(val); *ptr != 0'; ++ptr) {
put(*ptr);
}
put(r');
put(n');
return mrb_nil_value();
}

int
main(int argc, char *argv[])
{
int i;
unsigned char *ptr;
long *lptr;

/* uart intr disable */
ptr = (unsigned char *)0xb8002004;
*ptr = 0;

/* bss clear */
for (lptr = (long *)_fbss; lptr < (long *)_end; ++lptr) {
*lptr = 0;
}

mrb_state *mrb;
mrb = mrb_open();
mrb_define_method(mrb, mrb->object_class,"myputs", myputs,
MRB_ARGS_REQ(1));
mrb_load_irep( mrb, bytecode);
mrb_close(mrb);

return 1;
}


本来bssのクリアはstartupでやるべきなのですが、うまく動くアセンブラが書けなかったので、mainの中でやってます。bssの開始位置(_fbss)と終端(_end)はldscript(main.ld)で定義されています。

put()はシリアル(uart)に文字を出力する関数です。これが無いとデバッグが出来ません。

なぜかuartのLSRのTHREレジスタ(送信レジスタが空のフラグ)が立たないので、適当にループを抜けるコードになっています。SDKのコードやOpenWRTでもそうなっていました。なんかおかしいです。

newlibのlibcはlibgccが必要で、これはSDKの物を利用しました。

mrubyのスクリプトはネットで見つけた物を使わせてもらいました。


hoge.rb

o = Object.new

100.times do |i|
o.myputs "index:" + i.to_s
end

Flashの本来linuxが書き込まれるところに上記のビルドしたバイナリを書き込み実行させます。

RTLBM-SPI.jpg

REALTEK社製のbootのシリアルは38400ボーで電源投入後にESCキーで止まります。bootのプロンプトが出た状態でサーバ側からtftpで192.168.1.6にバイナリをputします。

ELFのbinaryをREALTEK形式にしてtftpで流し込んでFlashを焼くスクリプトはこんな感じです。Flashを焼くと元々の機能がなくなるので、注意してください。ビルドしているホストを192.168.1.1などにして実行します。


flash.sh

#!/bin/sh

TERGET=main
IMG=${TERGET}.rtl

make
ENTRY=`readelf -h ${TERGET}.elf | awk '/Entry point address/{print $4}'`
echo ${ENTRY}
./rtktools/cvimg linux ${TERGET}.bin ${IMG} ${ENTRY} 30000
echo "bin
put
${IMG}
quit"
| tftp 192.168.1.6


実行するとこうなります。

========== SPI =============

SDRAM CLOCK:156MHZ
------------------------- Force into Single IO Mode ------------------------
|No chipID Sft chipSize blkSize secSize pageSize sdCk opCk chipName |
| 0 c22017h 0h 800000h 10000h 1000h 100h 86 39 MX6405D/05E/45E|
----------------------------------------------------------------------------

---RealTek(RTL8196C)at 2011.11.13-11:40+0900 version v1.1f [16bit](390MHz)
no sys signature at 0009E000!
no sys signature at 00020000!
Jump to image start=0x80010000...
index:0
index:1
index:2
index:3
index:4
index:5
index:6
index:7
index:8
index:9
index:10
index:11

ファイルはここに置いてあります。ここに書いた事は最初のリリースタグの中身になります。

https://github.com/yamori813/rtlbm-mruby

ここに書いてある事は試行錯誤の結果なので、すっきりしていますが、結構骨が折れました。はまったところは、コンパイラのオプションがよく分からないことや、mrb_load_irepなどでヘッダーをincludeせずに関数定義無しにコンパイルすると引数の型がずれて、ちゃんと動かないなどがありました。あとSDKとnewlibの関係も微妙で、SDKを他のバージョンで試したら未定義関数が大量に出たりしていたので、たまたま組み合わせでうまくいっているだけかもしれません。

SDKのgccのmarchのデフォルトは4180のようで、これでコンパイルすると変な動きをしていました。4181と1しか違わないのになんだかな。。。

蟹さんは国内ではあまり評判が良くありませんが、RTL8196なターゲットはHardOffに108円とか324円でころがっているので、気軽に試せるのが良いです。またmipsのBare MetalでBig Endianと人とはちょっと違った環境も楽しい物です。

FreeBSDは動かせなかったのですが、瓢箪から駒といったところでしょうか。

蟹さんのbootのコードからコピーした物にGPLがはいっていますが、それ以外はGPLは無いです。GPLなコードはLinux由来のようですが、そもそものオリジナルはmipsが提供したコードではないかと思われます。

IO関係のgemはぼちぼち作ってみたいと思います。

RTL8196にはUART,GPIO,Timer,SPI,Ethernet,USB,PCM,Ethernet Switch,(WIFIはPICeの外付け)などのIOが入っているが、WIFIやEthernetやUSBのサポートは結構大変だと思う。Ethernetはbootにtftpのコードがあるので、それを参考にすると良いかもしれません。

Bare Metaで動かしてみて、動くまでのデバッグが結構大変です。問題があるとbootのエクセプションハンドラがUndefined Exception happen.だけ出してだんまりになります。

蟹さんが嫌われるのは、いまさらmips1なチップを作っている事が象徴しているような気がします。mipsは2000年代前半に4Kでアーキテクチャが完成していたにも関わらずLexra側について、そのままというのは、bootやOSやコンパイラをやってる人間からするととっても嫌な感じがします。FreeBSDを動かすのを断念したのはFreeBSDのmipsは4K以上をターゲットにしていて、今さら古いアーキテクチャをサポートしても先が無いと思ったからです。また蟹さんのデータシートは何故か断片的だったりして不可解です。RTL8196CのデーターシートにはGPIOの事しか書いてなくて、他のIOがどうなってるのか全く分かりません。他のチップのデーターシートにはTimerなどがのっているものもありますが、それでも抜けている部分があります。やはり嫌われるには訳があるような気がします。

後日追記:lwIPを組み込んでEthernetも使えるようにしてみました。

LexraなSOCはこんな感じでリリースされていたようです。

RTL-AGE.png

I am looking for job.