(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

ちょっと別件の作業をしていたので滞っていたけど,昨日ようやくそっちがなんとか片付いたので,H8移植をまた進めよう.

今回は,割り込み処理の実装だ.とりあえずタイマ割り込みを実装したい.

で,いろいろちょっと調べたのだけど,今回利用しているH8ボードでは,割り込みベクタがROM上にあるのでソフトウエアから起動後に書き換えるのは無理っぽい.なので,割り込みベクタの管理はブートローダーにまかせるしかない.ていうかブートローダーのROMへの書き込み時に,割り込みベクタもいっしょに書き込まれてしまい,OS起動後に設定しなおすようなことができない.

で,とりあえず割り込みの入口はブートローダーにやってもらって,ソフトウエア的に独自に割り込みベクタみたいなのをRAM上に作っておいて,ブートローダー側ではそのソフトウエア・割り込みベクタを見て適切なアドレスに飛ぶ,というよくありがちな2段構成にしてみよう.これならソフトウエア・割り込みベクタをOS側で書き換えることで,割り込みハンドラを自由に登録できるようになる.

ソフトウエア・割り込みベクタの場所なのだけど,RAMの先頭領域である0xffbf20 から256バイトを割り当てよう.現状でRAMの先頭付近からOSをロードして使うようになっているので,OSのロード先は256バイト後ろにずらして調整する.

ということで実装したのが以下のような感じ.まずブートローダー側の修正なのだけど,割り込みの入口として intr.S を新規追加してある.ちょっとベタな書き方になってしまっていて,ほんとはマクロとか使って短くしたいところなんだけど,なんかうまくアセンブルできなかったので面倒なのでベタで書いてしまった.

ちなみにファイル名が *.s だとプリプロセッサを通さないが,*.S だとプリプロセッサを通して前処理が行われるようだ(man gcc にそう書いてあった).アセンブラのソースで #define とか使いたいときは要注意.なお intr.S は内部ではひとまず #define とかは使ってないので現状ではプリプロセッサを通す必要はないのだけど,将来的に #define でまとめたいので,intr.S というファイル名にしている.

vector.c では割り込みハンドラとしてとりあえずスタートアップを全部に登録していたが,intr.Sで定義してある各割り込みに応じたハンドラを登録するように修正した.

割り込みの入口では,スタックにレジスタを退避して,割り込みベクタ番号を引数にして interrupt() という関数を呼んでいる.interrupt() の本体はinterrupt.c で定義してあって,ソフトウエア・割り込みベクタ(VECTORS[])を参照して,登録してあるハンドラを呼び出すような作りになっている.OSが起動したらここにハンドラを登録しておけば,割り込みが入ったときに適切にハンドラを呼び出してくれるわけだ.割り込み関連の定義は interrupt.h に書いておいた.

あと main.c からソフトウエア・割り込みベクタの初期化用関数であるinterrupt_init()を呼んで,最初はNULLクリアするようにしている.

ブートローダーの修正はこんなもんかな.次にOS側の修正.

まず,RAMの先頭である 0xffbf20 番地はソフトウエア・割り込みベクタを配置するので,プログラムのロード先を256バイト後ろにずらす.このためにリンカスクリプトに以下の修正を入れている.

--- hello/ld.scr Wed Sep 2 23:32:57 2009
+++ timer/ld.scr Mon Sep 14 22:13:00 2009
@@ -4,8 +4,11 @@

MEMORY
{
- /* reserve 256bytes space for ELF header */
- ram(rwx) : o = 0xffbf20 + 0x100, l = 0x004000 - 0x100 /* 16kb */
+ /*
+ * reserve 256bytes space for software interrupt vector
+ * and 256bytes space for ELF header.
+ */
+ ram(rwx) : o = 0xffbf20 + 0x200, l = 0x004000 - 0x200 /* 16kb */
stack(rw) : o = 0xffff00, l = 0x000010 /* end of RAM */
}

まあ,単にずらしただけ.

あとタイマ関連の処理のために timer.[ch] を追加.これらはH8の持つタイマの各種レジスタを操作して,タイマ起動とあと割り込み時の処理を行うもの.ボード付属のCD-ROMについてたH8のマニュアルとか,あと本とか読んでてきとうに書いてみた.とりあえず,1/50秒周期でタイマをかけてカウントして,1秒おきに「I」という文字を出力するようにしてみた.

あと割り込み処理用にブートローダーの interrupt.[ch] を流用して,ソフトウエア・割り込みベクタに割り込みハンドラを登録できるようにした.これは実際には timer.c のタイマ初期化関数である timer_init() で,

int timer_init()
{
...
interrupt_sethandler(VECTYPE_IMIA0, timer_interrupt);
...

のようにして,timer_interrupt() をベクタ番号24(IMIA0)に登録している.

main.c では timer_init() を呼んでタイマの初期化をして,あと ENABLE_INTR を呼んで割り込み有効にしている.

では,実際に実行してみよう.今回はブートローダーに割り込み処理の機能追加がされているので,まずはブートローダーをビルドしてフラッシュROMに上書きする.

teapot# make write
../../h8write/h8write -3069 -f20 kzload.mot
H8/3069F is ready! 2002/5/20 Yukio Mituiwa.
writing
WARNING:This Line dosen't start with"S".
Address Size seems wrong
WARNING:This Line dosen't start with"S".
Address Size seems wrong
.........................................................................
EEPROM Writing is successed.
teapot#

気のせいか,書き込み時の「.」の数が増えたような...intr.S がベタで書いてあるのが効いているのかしら.

次にブートローダーを起動して,OSをダウンロードして実行する.

teapot# cu -l /dev/cuad0
Connected
Hello World!
> load
~CLocal command? lsx hello
Sending hello, 20 blocks: Give your local XMODEM receive command now.
Bytes Sent: 2688 BPS:665

Transfer complete
eceive succeeded.
> run
starting from entry point.boot succeed!
os> IIItest
test
os> IIIsampleI
sample
os> III

なんと驚くことに一発で動いてしまって,作った本人がビックリ.

いやホントに一発で動いたのよ.今回は割り込み周りのコーディングだし,H8のアセンブラってろくに書いたことないし,もっとハマることを覚悟していたのだけど,コーディングしただけであっさり動いてホント,びっくり.

なんつーか,H8は使うのが簡単でとても楽.H8だとシリアルもタイマもちょろっと設定しただけであっさり動くのですげー楽.PowerPCだとこうはいかないよなあ.

とりあえず割り込みも受けられるようになって,なんか随分前進した感じ.次はシリアルの受信割り込みかなあ.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回作成したブートローダーだけど,前回はちょろっと動作させただけでろくに解説していなかったので,ちょっと解説しよう.

まずブートローダーについてなのだけど,リンカスクリプトに以下の行が追加されている.

buffer(rwx) : o = 0xffdf20, l = 0x002000 /* 8kb */

さらにデータ領域とBSS領域は,上記 buffer 領域に割り当てられるように変更してある.

これはどーいうことかというと,まあ本当はシリアルからプログラムをダウンロードしながらロード先のメモリ上に直接展開できればいいのだけど,面倒なので,いったんRAM上のバッファにぜんぶダウンロードして,で,ELFヘッダを解析してロード先にコピーするという構成にしてある.

で,プログラムをロードする先はRAMの先頭付近にしておきたいので,RAMの終端付近にデータ領域とBSSを置いてブートローダーはそこで動作する,というメモリ配置にしてある.バッファはブートローダー内で

#define BUFSIZE 4096
static unsigned char loadbuf[BUFSIZE];

のようにして文字配列として定義してあるので,BSSに配置される.これが上記の buffer 領域に置かれることになる.loadbufのサイズが4KBなので,buffer 領域は余裕をみて8KBとして定義している.

いったんバッファ上に置いておいて,さらにロード先に展開することを考えると,loadbuf[]の最大サイズは,RAMの16KBの半分の8KBとなる.(loadbuf[]を12KBにすれば12KBまでのプログラムをダウンロードできるが,残りRAMは4KBしかないので展開先が無くて意味がない.よってRAMサイズの半分が,バッファサイズの最大となる.

このためこのブートローダーは現状,8KBまでのプログラムしかロードできない.あとロードされるプログラムは,RAMの先頭付近を使うようにリンカスクリプトでメモリ配置する必要がある.

あと serial.c には,バイナリデータの送受信用に serial_getb(), serial_putb() を新規作成した.これらは改行コードの変換を行わずに生データを送受信する.前回はここで従来の serial_getc(), serial_putc() を使っていたために改行コードが変換されてしまってチェックサムとかが壊れてしまう,というのにハマッたわけだ.

あと uuencode 形式のデコードのために,uudecode.c を追加してある.xmodem.c からは USE_UUENCODE の定義の有無で,uudecode を呼び出すか,生データとして扱うかが切り替わるようになっている.シリアルからデータをダウンロードしながらデコードを行えるように,uudecode.c はストリームデータを処理できるような書き方をしてある.(同様に書けば,ELF形式をダウンロードしながらヘッダ解析してロード先に直接展開,ということもできると思う)

あとELF形式の解析のために elf.c が追加されている.ELF形式はそれなりに複雑なフォーマットだけど,ここでやってるのはELFヘッダのチェックをしてからプログラムヘッダを見て展開することだけなので,超簡単に書けている.ELF形式について詳しくはこの記事の第2回を参照してほしい.

main.c は,putval(), putxval() に実は負の値を渡すと無限ループに陥ってしまうという問題があったので,引数をunsignedとして受け取るように修正.あと run コマンドを追加して,ダウンロードしたプログラムをロード先に展開して実行するように機能追加してある.

次に,ダウンロードされるプログラムのほうについて.まあ現状では入力された単語をエコーするだけの機能しかなくて,実はブートローダーを流用して書いてある.

シリアルまわりとかスタートアップはブートローダーから持ってきたそのまま.スタートアップ(startup.s)ではスタックポインタを設定しているので,サンプルプログラムを起動するとスタックは設定されなおすことになる(ブートローダーが使用していたスタックを引続き利用するわけではない).ライブラリ(lib.c)もそのまま.異なるのはリンカスクリプトとmain.cなのだけど,main.cは使わない関数に蓋をして,単語をエコーするだけに書き換えただけなのでたいした変更は無い.あ,あとデータ領域の展開とBSSのクリアはブートローダー側でやっているので,main.c の init() からは削除してある.

大きな変更があるのはリンカスクリプトだ.以下,ブートローダーとロードされるサンプルプログラムでの,リンカスクリプトの変更点.

--- kzload/ld.scr Fri Sep 4 00:16:17 2009
+++ os/ld.scr Fri Sep 4 00:16:35 2009
@@ -4,44 +4,38 @@

MEMORY
{
- vectors(r) : o = 0x000000, l = 0x000100 /* top of ROM */
- rom(rx) : o = 0x000100, l = 0x07ff00 /* 512kb */
- ram(rwx) : o = 0xffbf20, l = 0x004000 /* 16kb */
- buffer(rwx) : o = 0xffdf20, l = 0x002000 /* 8kb */
+ /* reserve 256bytes space for ELF header */
+ ram(rwx) : o = 0xffbf20 + 0x100, l = 0x004000 - 0x100 /* 16kb */
stack(rw) : o = 0xffff00, l = 0x000010 /* end of RAM */
}

SECTIONS
{
- .vectors : {
- vector.o(.data)
- } > vectors
-
.text : {
_text_start = . ;
*(.text)
_etext = . ;
- } > rom
+ } > ram

.rodata : {
_rodata_start = . ;
*(.strings)
*(.rodata)
_erodata = . ;
- } > rom
+ } > ram

- .data : AT(_erodata) {
+ .data : {
_data_start = . ;
*(.data)
_edata = . ;
- } > buffer
+ } > ram

.bss : {
_bss_start = . ;
*(.bss)
*(COMMON)
_ebss = . ;
- } > buffer
+ } > ram

_end = . ;



まずロード時ていうかリンク時の注意なのだけど,ELF形式はリンカによって作成される
際に,先頭にELFヘッダが付加される.で,このELFヘッダがテキスト領域の直前に
配置&ロードされるようにプログラムヘッダが作成される.




これはロードされたプログラム側で,自身のELFヘッダを参照できるようにするためだと
思われる(あくまで想像だけど,もしかしたら共有ライブラリがらみで参照が必要
なのかもしれない)のだが,RAMの先頭(0xffbf20)から利用するようにふつうに書くと,
RAM領域の先頭として指定された 0xffbf20 というアドレスよりも少し前にELFヘッダが
はみ出して配置されてしまう.いろいろやってみたのだけどこれをうまく削ったり
調整したりする方法が見つからなくて,しかたがないので対策として,RAM領域の
先頭256バイトをELFヘッダ用に空けるようにしてある.
(今思ったのだけど,ブートローダがプログラムヘッダを見て展開するときに,
RAM領域内でないデータは展開しないようにすればいいだけだね.まあでもいいや)




ということで,サンプルプログラムのほうではRAMは先頭 0xffbf20 付近から利用する.
RAMの終端のほうはブートローダーが利用しているので,ブートローダーからロードした
プログラムに実行が移るまでは,上書きすることはできない.つまり,RAM終端付近に
なにかを配置するようにリンカスクリプトを書いてはいけない.




割り込みベクタはいまのところとくに上書きしていないので,リンカスクリプトから
削除.あとテキスト,リードオンリーデータ,データ,BSSをすべてRAM上に配置する
ように変更してある.




まあ解説はこんなとこかな.ざざっと説明しただけだけど.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

今回は,いよいよブートローダーでプログラムをロードして,実行してみよう.最終的にやりたいのは,ブートローダーでの起動後にOSをロードして実行することでOSに処理を渡すことなのだけど,とりあえずはブートローダーで起動して,簡単なコマンド受け付けのサンプルプログラムをロードして実行してみる.

まずロードするプログラムのオブジェクトフォーマットなのだけど,いろいろ考えたのだけどELF形式で行くことにした.というのは,まあぼくがELFの記事を書いたことがあって馴染み深くてよく知っているのと,ELFでできないことはそうそう無いので融通が効くこと,フォーマット解析がそんなに難しくないこと(ローダの実装がラクそう),標準的なフォーマットとして普通に使われていること,などが理由だ.ELFフォーマットについて詳細は,この記事の第2回をぜひ読んでほしい.かなり詳細に書いてある.(追記:この記事を書いている現段階では,モトローラSフォーマットを知った今となってはそっちにすればよかったかなと思います.展開もラクそうだし,あとロード先に直接展開ができるのでRAMを最大限まで使えるし.まあ対応は簡単そうなので,そのうち対応しよう)

で,ELFの実行形式をいったんバッファにロードしてから実行コマンドでロード(メモリ上の動作すべきアドレスに展開)し,実行を渡すようにブートローダーを書いてみたのだけど,いまいちうまく動かない.

まあこのへんでいろいろハマってしまいちょっと時間をくってしまった.最初のうちはロードしたプログラムの挙動がなんかおかしくて,スタックポインタの設定とかシリアルの設定とかXMODEMのチェックサム計算とかをいろいろ疑っていたのだけど,じつはそもそもシリアルの受信関数(serial_getc())の内部で改行コードの変換をしているので,バイナリデータが壊れてしまうという問題だった.あー間抜け.(たとえテキストデータだとしても,XMODEMのブロックナンバとかチェックサム部分が壊れてしまい,エラーになってしまう)

ということで実装したのが以下のような感じ.

上のリンクでは,ブートローダーと,ロードして実行させるサンプルプログラム(テキスト入力を受け付けて,それを表示するだけのもの)を配布している.kzload というフォルダがブートローダーで,osというフォルダがサンプルプログラムだ.(将来的にOSをロードするようにするつもりなので,osというフォルダ名にしている)

なので手順としては,以下のようになる.
  1. ブートローダーとサンプルプログラムを別々にビルドする.
  2. ブートローダーを h8write でフラッシュに焼き込む.
  3. ブートローダーを起動する.
  4. loadコマンドでXMODEMでのファイル受信に入り,サンプルプログラムのELFファイルをシリアル転送する.
  5. 転送完了したらrunコマンド(これは今回新設)でサンプルプログラムを実行する,
  6. サンプルプログラムが起動する.
なお上のプログラムでは,実はuuencode形式でのファイル転送もサポートしている.というのは,うまく転送できないのはバイナリファイルで転送しているからでは?と考えて,uuencode形式でファイル転送するようにして試したから,その名残り.まあ実は serial_getc() の改行コード変換が問題だったのでバイナリファイルを転送することに問題は無く,その後 uuencode 形式は使用しないように修正したが,もしも uuencode 形式を利用したいならば以下のようにすればよい.
  • xmodem.c の USE_UUENCODE という定義を有効にしてブートローダーをビルド.
  • サンプルプログラムのビルド時に ./make.sh image を行うことで hello.uu という uuencode 形式のファイルが生成されるので,それを転送する.
デフォルトでは USE_UUENCODE は無効になっているので,ELF形式をそのままシリアル転送すればよいのだけど,なんかうまくシリアル転送できない場合にはuuencode 形式での転送を試してみるといいかもしれない.

ちなみに今回この uuencode 形式にすることを試したときに,モトローラSフォーマットでもいいのでは? と思ってモトローラSフォーマットについて調べてみたのだけど,実は以下のような利点がある.(uuencode形式を選択したのは,単に実装がラクそうだったから)
  1. テキスト形式なので,回線が7bitだったらどうとかフロー制御のコードがあったらどうとか余計なことを考えなくていい.
  2. データを受信しながらそのままメモリ上に展開する,という処理に向いている.
とくに,今回ブートローダーを作っていて,「データをバッファ上に一度読み込んでからロードアドレスに展開するのではなく,データを読み込みながら展開先を調べて直接ロードできないか?」という疑問があった.というのは,いったんどこかのバッファに置いてからロード先にコピーするような動作だと,処理自体は楽なのだけど,まあワーク領域として倍のメモリを使うことになるので,今回のマイコンボードのようにRAMが少ない場合には,問題となるからだ.(結局,今はいったんバッファに置く実装になっているので,これは将来課題ではある)

で,上のような問題意識があったうえでフォーマットを調べていたからこそ今回気がついたことなのだが,モトローラSフォーマットというのは,データを読み込みながらロード先のメモリ上に直接展開するのに非常に向いている.というのは,ロード先のアドレスが先頭にあるので,まずアドレスを読んで,後続のデータをそのアドレスの指す先に配置していけばいいからだ.うーん,きっとこういうことを考えた上で作られたフォーマットなんだろうなあ.

ついでにいうと,これもそーいう問題意識で考えていたからこそ気がついたことなのだけど,ELF形式というのも,まあモトローラSフォーマットほどやりやすくは無いが,プログラムヘッダが先頭付近にあるため,読み込みながら直接展開,ということは原理的にはできる(プログラムヘッダがファイルの終端にあったりすると,最後まで読まないとロード先がわからないので,直接展開は原理的に不可能).

この記事からもわかるのだけど,ELF形式は先頭付近にプログラムヘッダ,末尾にセクションヘッダが配置されており,プログラムのロードはプログラムヘッダを参照して行われるが,リンク作業はセクションヘッダを参照して行われる.なぜこのような配置になっているのか今までとっても疑問だったのだけど,おそらくプログラムヘッダが先頭付近にあるのは上記のようにプログラムを読み込みしながら展開先に直接展開するためだ.あとセクションヘッダが終端にあるのは,これもおそらくだけど,プログラムのロード&実行にはセクションヘッダは必要無いので,サイズ節約のために実行形式から取り除きたい場合がある.この場合,ファイルの先頭付近にあると,そこを削除すると後続のデータのオフセットが変わってしまうため,様々なオフセット計算をやり直さなければならなくなってこれはそうとう面倒臭い.しかし終端にあれば,単にそれを取り除くだけでいいからだ,と思う.

で,難しい話はあとに回して,とりあえず実行してみよう.今までのおさらいも兼ねて,ビルドからひととおり説明する.

まず,ブートローダーとサンプルプログラムをビルドする.ブートローダーは今まで通り,以下でビルドできる.

% ./make.sh clean ; ./make.sh ; ./make.sh image

これで kzload.mot というファイルが生成される.ここまでは(ファイル名が kzload* に変わっているけど)前回通り.

次に,サンプルプログラムをビルドする.

% ./make.sh clean ; ./make.sh

上に書いたように,ファイル転送に uuencode 形式を使うなら,以下も実行しておく.

% ./make.sh image

ここまでで準備は完了.

次に,h8write を使ってブートローダーをフラッシュROMに転送する.実際には Makefile に書いてあるので,make write するだけでいい.

teapot# make write
../../h8write/h8write -3069 -f20 kzload.mot
H8/3069F is ready! 2002/5/20 Yukio Mituiwa.
writing
WARNING:This Line dosen't start with"S".
Address Size seems wrong
WARNING:This Line dosen't start with"S".
Address Size seems wrong
.......................................
EEPROM Writing is successed.
teapot#

これで完了.

cuでシリアル接続し,ディップスイッチを起動用に切替えてリセットボタンを押してブートローダーを起動する.あとでサンプルプログラムを転送するときにファイル指定しやすくするために,cuはサンプルプログラムをビルドしたディレクトリで起動するとよい.

teapot# cu -l /dev/cuad0
Connected
Hello World!
>

ブートローダーが起動して,コマンド入力待ちになる.コマンド待ちするので,ブートローダーっつうよりモニタっつったほうが適切かもしんない.

loadコマンドを実行して,XMODEMによるファイル転送待ちに入る.

teapot# cu -l /dev/cuad0
Connected
Hello World!
> load
(待ち状態)

前回説明したように,「~」「C」でコマンド指定に入り,XMODEM送信アプリとしてlsxを指定する.引数にはサンプルプログラムのELF形式である「hello」ファイルを指定する.(uuencode形式を利用する場合は,ここで「hello.uu」を指定する)

teapot# cu -l /dev/cuad0
Connected
Hello World!
> load
~CLocal command? lsx hello

Enterを押すと転送が始まる.

> load
~CLocal command? lsx hello
Sending hello, 15 blocks: Give your local XMODEM receive command now.
Bytes Sent: 2048 BPS:586

Transfer complete
eceive succeeded.
>



うまく転送できた.実はこの「うまく転送できるまで」でハマッていろいろな試行錯誤があったのだけど,まあうまくいったのでいいや.

今回,ブートローダーにrunコマンドというのを新規追加してある.runコマンドを実行すると,loadによってバッファ上に読み込んだファイルをELF形式とみなして解析し,ロード先のアドレスを調べてメモリ上に展開し,エントリポイントに処理を渡す(要するに,ロードして起動する).

runコマンドで実行してみよう.

> run
starting from entry point.boot succeed!
os>

おー,起動した.かなり感動.ここまで来るのにどれだけ苦労したことか...(いやH8への移植だけに関して言えばここ数日のことなのだけど,OS作りたいなあとむかし漠然と思っていたときから考えると,ホント,しみじみ思ってしまう)

てきとうにコマンドを打ってみる.

> run
starting from entry point.boot succeed!
os> run
run
os> test
test
os> dump
dump
os> load
load
os> aaa
aaa
os>

入力した文字列をそのまま返している.ひとまずちゃんと動いているようだ.runやloadを実行しても,ブートローダーでの動作が行われずにただ文字列をそのまま返しているので,サンプルプログラムに完全に処理が移っていることがわかる.

うーんちょっと感動.結局,ブートローダーが作れてしまった.まあものすごく簡単なものではあるが,きちんと動作して,他のプログラムをブートできている.

OS作りたいなあと漠然と思ってからいろいろ勉強したりしてきたけれど,実際作ろうとなったときにこーいうのがパパッと作れるというのは,やっぱしいままで勉強してきた甲斐があったなあというものだ.しみじみ.やっぱりなんでも基礎力が大事だね!

ちなみに今回のソースコードだけど,ブートローダーには前回に対して以下のファイルが新規追加されている.
  • ELF形式のファイルの解析と展開 (elf.c)
  • uudecode の復号 (uudecode.c)
ここで注目したいのはこれらのファイルサイズなのだけど,elf.cは92行,uudecode.cは48行だ.ちなみにXMODEMの処理である xmodem.c は96行.ELFとかXMODEMとかUUENCODE実装とかってなんか難しそうなイメージあるけど,特定の処理に限定すればこんなもんで済むもんです.これくらいならちょっと読めば誰でも十分に理解できるし,じゃあモトローラSフォーマットに対応してみようとかBinHexにも対応しようとか思ったときに,なんか簡単にできそうな気がするよね.

いま調べたら,ブートローダーぜんぶ含めても634行だった.まあエラー処理とかがかなりザルではあるが,ファイルを読んで展開するだけなら,こんなんで書けるということだ.ブートローダーってなんか難しいイメージあるけど,600行ちょいなら自分でも十分に読めるかな?作れるかな?って感じがするでしょ?OS作るとかブートローダー自作とかってなんか難しいイメージあるけど,あんま難しいこと考えずに作れば,これくらいでできちゃうもんなのよ.

あと今回思ったことだけど,ELF形式についてなのだけど,実際に自分で作ってみないと気が付かない,逆に言えば自分で作ってみれば気がつくこと,というのはあるもんだなあ,と.ELF形式のプログラムヘッダがなぜ先頭付近にあるのか?セクションヘッダがなぜ終端にあるのか?は,今回実際にいろいろ作ってみて(試行錯誤してみて)はじめて気がついたことだ.

よく,通り一辺倒の知識を得ただけで「この分野に関しては制覇したぜ!」みたいなこという人がいるよね.「C言語は完璧だぜ!」とか言っちゃう感じ.でもそーいうのって全然不十分で,そもそも本に載っている知識を得ただけで満足するだけじゃだめで,なぜそのような仕様になっているのか?なぜそーいう実装になっているのか?という製作者の考えとか気配りとか苦肉の策とかに気がつくようにならないと,その分野をマスターしたとはいえないなあ,と思う.こうこういう経緯で,歴史の流れでそうなってしまった,とかね.(まあ別に自分がELF形式をマスターしたぜ!という意味ではなく,自分に対する戒めです).

C言語なら,文法事項をマスターしただけでなく,なぜそのような言語仕様になっているのか?とかね.こーいうのは習って知るだけではなく,作られた経緯とか歴史とか実際の使われかたとかを勉強していく過程で,自分で自然と気がつけるようになりたいものだ.1を知ることで10を知る,って感じでね.

今日はとりあえず動いたので満足.サンプルプログラムの構成とか詳しい解説は次回にしよう.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回でシリアル送受信ができるようになったのだけど,次はシリアル経由でのファイル転送ができるようにしたい.で,ファイル転送用の独自プロトコルを作るか適当な既存のプロトコルを実装するかだが,まあ独自プロトコルにしてしまうと送信アプリを書く必要が出てきてしまい,さらにその送信用アプリをLinuxでコンパイルして使うにはとかWindowsから使うにはどうすればいいかとかいった話になってしまって,それはそれでとっつきづらいと思うので,適当な既存のプロトコルを実装することにする.

で,プロトコルなのだけど,XMODEMというのが簡単そうなのでそれにしよう.まあXMODEMならLinuxとかWindowsとかでもいっぱい送信用アプリがあるだろうし.(WindowsならたぶんTeraTermでファイル転送できるだろうと思う)

ちなみにXMODEMにはファイルの終端が Ctrl-Z で終ってしまっている場合にそれを検出できないという問題があるようだが,実行形式ファイルのお尻がわからないだけで,実行形式ファイルの展開という意味ではとくに問題は無い.(実行形式は内部に独自にヘッダ情報を持っていて,それを見てメモリ上に別途展開するので,終端に余計なものが付いていても別に問題ない)

XMODEMについては,以下が参考になる.他にも改良版のYMODEMとかZMODEMとかあるらしいが,まあ転送するファイルのサイズもそれほど大きくはならないだろうから転送効率とかあんまし考えなくていいし,エラー時再送とかきちんとしなくても(信頼性が低くても)もう一度やりなおすだけだし,とりあえず実装が一番楽そうなXMODEMを実装する.

で,上のホームページを見て実装したのがこんなかんじ.

xmodem.c というファイルを追加してあってその中でファイル受信しているのだけど,xmodem.c はなんとたったの89行.うーん,簡単なプロトコルなので楽でいいのう.ちなみにファイルの転送先なのだけど,とりあえず loadbuf[] というバッファを適当なサイズで定義して,そこにそのままコピーするようにしてある.まあ対して難しいプログラムではないので,詳しくはソースを見てちょうだい.(現状,Cファイルとヘッダファイルだけで400行程度なので,十分に読み切れると思う)

あと細かいところをいろいろ修正してある.
  • H8の書籍とかちゃんと読んで,シリアルの送受信処理を見直し.とくに受信時にSSRのRDRFを落とす処理を追加.(これをやっていなかったので,連続して受信したときに読み出しデータがおかしなことになっていた)
  • リンカスクリプトを,もうちょっと修正.(MEMORYコマンドにより,ROM領域とRAM領域を明示的に定義するようにした.これにより,ROMやRAMのサイズ不足で溢れたときにはリンカがエラーにしてくれるようになる)
  • XMODEMでファイル受信するコマンドとして,loadコマンドを追加.あと受信したファイルの内容チェックのために dump コマンドを追加.
  • ライブラリ関数とかをこまごまと修正.
で,ビルドしてフラッシュROMに焼いて実行してみる.フラッシュROMへの転送のしかたは第1回を参照.

ちなみにフラッシュROMへの転送なのだけど,以下の手順で電源のOFF/ON無しでできるようだ.
  • フラッシュROMへの書き込み
    1. ディップスイッチを左からON,ON,OFF,ONにする.
    2. cuで接続している場合は,抜ける.(これをやらないとh8writeが動作できない)
    3. リセットボタンを押す.(電源OFF/ONは不要)
    4. h8writeで書き込む.
  • 書き込んだプログラムの起動
    1. ディップスイッチを左からON,OFF,ON,OFFにする.
    2. cuで接続する.
    3. リセットボタンを押す.(電源OFF/ONは不要)
    4. 実行が開始される.
上のような感じで,ディップスイッチを切替えてリセットボタンを押すだけで,電源のOFF/ON無しでフラッシュへの書き込みとプログラムの実行を繰り返すことができる.

で,以下,実行結果.

teapot# cu -l /dev/cuad0
Connected
Hello World!
>

cuでつないでからリセットボタンを押して起動すると,まず Hello World が表示される.

ここでXMODEMでのファイル受信用の「load」コマンドを実行する.

> load

コマンド実行すると,XMODEMでの受信待ちに入る.

で,FreeBSD側からのXMODEMでのファイル転送なのだけど,PowerPCのときも使った「lrzsz」を使う.インストールされていない場合はまずインストールする.(FreeBSDでは packages になっているので,パッケージインストールできます)

で,ファイル転送なのだけど,cuは「~C」を入力することで別コマンドを fork してその入出力を繋げることができるので,その機能を利用して lrzsz にXMODEMプロトコルをしゃべってもらってファイル転送する.まあ手っ取り早く言うと,loadコマンド実行後に,まず「~」「C」のようにキーボードから入力する.

> load
~CLocal command?

「Local command?」のように実行するコマンドを聞いてくる.lrzszでは,「lsx」というコマンドがXMODEMのためのコマンドなので,lsxを指定する.さらに,送信するファイルを引数として指定する.ここでは a.txt という適当なテキストファイルを作成して指定してみる.

> load
~CLocal command? lsx a.txt

ちなみにa.txtの内容は以下の通り.ほんとてきとう.

abcdefg
hijklmn

これで Enter を押すと,ファイル転送が開始される.

> load
~CLocal command? lsx a.txt
Sending a, 0 blocks: Give your local XMODEM receive command now.
Bytes Sent: 128 BPS:142

Transfer complete
eceive succeeded.

ちなみにWindowsとかだとXMODEMでファイル転送するためのアプリがきっといっぱいあるだろうから,それを使えばいいだろう.その場合,転送プロトコルは「XMODEM」で,転送ブロックサイズは128バイト,チェックサムは1バイト(8ビット)の単なる加算のもの(CRCチェックサムではない)を指定する.正式には「XMODEM/SUM」というらしい.(「XMODEM/CRC」とか「XMODEM/1k」だとダメなので注意)

ファイルが転送できたら,dumpコマンドでファイルの内容を確認してみる.

> dump
size: 128
61 62 63 64 65 66 67 0a 68 69 6a 6b 6c 6d 6e 0a
1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a
1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a
1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a
1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a
1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a
1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a
1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a

>

a.txtの内容がばっちり書き込まれている.(128バイト単位で,お尻はCtrl-Zで埋められる)

今回は実は最初はぜんぜんうまく動作しなかったのだけど,シリアルの受信処理に問題があっておかしなデータが受信されていたのが原因で,そこを直したらXMODEMの処理自体はあっさりと動いた.うーん楽でいいねえ.

これでシリアルからのコマンドを受け付けて,XMODEMでファイル転送できるようになった.なんかモニタっぽくなってきたね.次は実際にプログラムを転送して,動作できるようにしたい.オブジェクトファイルフォーマットを何にしようかなあ...やっぱしELFかなあ...
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回はシリアル送信で hello world が出せたので,今回はブートローダーを自作する準備として,シリアルの送受信をやってみる.というのは,今回自作するブートローダーは,シリアル経由でOSの実行形式ファイルをダウンロードしてRAM上に展開して実行,という動作にするつもりなので,シリアル経由での操作が必須になるからだ.

まあ前回作成したソースコードで,シリアル受信の関数 serial_getc() がすでに作ってあるので,main.c をちょいちょいと適当に修正して,起動したらプロンプトを出して文字列が入力されたらそれをそのまま返すようなプログラムにしてみた.以下のような感じ.

で,ビルドしてフラッシュROMに焼いて実行してみる.フラッシュROMへの転送のしかたは前回を参照.

teapot# cu -l /dev/cuad0
Connected
Hello World!
> test
test
> sample
sample
> test
test
>

うーん,あっさり動いた.

とりあえずこれでシリアルの送受信はできるようになったので,いよいよブートローダー作成なのだけど,何点か決めなきゃならんことがある.

まず,ブートローダーは起動したらプロンプトを出して,入力されたコマンドに応じて動作するというような動作にするつもり.で,コマンドには「シリアル転送でプログラムの実行形式をダウンロード」とか,「ほげほげ番地にダウンロードしたプログラムを実行」みたいなのを作る.つまりコマンドベースで動作して,プログラムのダウンロードや実行を行えるようにする.ちなみにこーいうのを一般的に「モニタ」とか言ったりする.

で,決めなきゃならんのは,シリアル経由でのプログラムのダウンロード方法だ.世の中にはシリアル経由でファイル転送するためのXMODEMとかYMODEMとかいう方式がいくつかあるのだけど,
  • (案1)XMODEMあたりをてきとうに実装する.
    • 長所...世の中になるXMODEMに対応したファイル転送アプリを使って転送できる.
    • 短所...とくになし.
  • (案2)てきとうな独自プロトコルを作って実装する.
    • 長所...好きにプロトコルを作れるので,どうにでもなる.
    • 短所...ファイル転送アプリを自作する必要あり.
うーんどうしようかなあ.

あと,転送するプログラムはブートローダーがRAM上に展開するのだけど,これも実行形式ファイルのフォーマットを何にするか決めておかないといけない.実行形式ファイルはELF形式とかモトローラのSレコード形式とかCOFF形式とかあるけど,そーいう一般的な形式にするか,それともオリジナル形式にしてしまうか.

うーん悩みどころだ.