Parallels 上に MintLinux 17.2 を使用
-
必要なもの
- binutils
- gcc
- make
-
セルフコンパイラ
クロス開発するため、クロスコンパイラが必要になる。 しかしそもそもビルドする環境がないため、まずセルフコンパイラが必要になる。 クロスコンパイラ作成後は基本的には必要なくなる。(make は使ってるかな?)
- クロスコンパイラ
動作環境(実行用ボード)で動作するバイナリにビルドするための開発モジュール。 一回作成してしまえばこれをずっと使用するが、 クロスコンパイラ作成時には開発環境がないのでセルフコンパイラにて作成する。
H8 はベクタ割り込み形式である。 ベクタ割り込み形式は、割り込み発生時にどのアドレスを処理するかを 特定のアドレスに設定しておくことができる。 H8 の場合は、"0x000000〜0x0000ff" となる。 ここにアドレスを記述することで色々な割り込みに対して指定したものを実行させることができる。
起動時の割り込みは "リセット" になる。したがってリセットベクタ で 指定されているアドレスが処理開始のアドレスとなる。 H8 ではリセットベクタは先頭(0x000000)である。 (H8/3069Fマニュアル 4.1.3 参照) 割り込みベクタは "vector.c" で定義されており、 開始は start という関数となる。
※ C での関数や変数名などのシンボルはアセンブラ上ではアンダースコアが付けられて管理されている。
start という関数は c のソース上にはないが、 startup.s で指定されている _start が該当のシンボルとなる。 startup.s での _start ラベルは、スタックポインタを設定し main 関数を実行している。後は通常の流れ。
起動(リセット割り込み発生) => "0x000000" で指定されているもの実行 = "_start" ラベルの実行 => スタックポインタ設定し "main" 関数の実行
割り込みベクタを指定の位置に設置する必要があるが、 それは "ld.scr" リンカスクリプトで指定している。 リンカスクリプトはその他にも色々なオブジェクトのメモリの配置を指定している。
オブジェクトファイルフォーマットの一種。 MS-DOS の "EXE形式" や "COFF形式" などがあるがそのうちの1つでよく使用される。
ソース(xxx.c)がコンパイルされる際に、 オブジェクトファイル(xxx.o)が生成され、リンクされて実行形式のファイルができる。 オブジェクトファイルはELF形式となっており、リンカによってリンクされる際に 同じセクション(.text, .data, .bss etc...)のものがまとめられ セグメントとなる。
これは、プログラム実行時に ローダ と呼ばれる プログラムをメモリ上に展開するプログラムに読み込まれて、 セグメント としてメモリに展開される。
セクション => オブジェクトファイル セグメント => リンカ
※ readelf によって表示される情報では Section Header => セクション情報 Program Header => セグメント情報
(H8/3069Fマニュアル 3.6.1 参照 ※ AKI-H83069LANのマニュアルにモード5で動作してると記述あり)
ROM: 0x000000 - 0x07ffff(512KB) RAM: 0xffbf20 - 0xffff1f(16KB)
リンカスクリプトで指定されているアドレス ramall: 0xffbf20 - 0xffff20(16KB) buffer: 0xffdf20 - 0xfffc20( 7KB) data : 0xfffc20 - 0xffff20(768B) stack : 0xffff00 - 0x000000(上に拡張していくので)
リンカスクリプト(ld.scr)内で、 静的変数(関数内部などのように自動的にメモリに割り当てられない変数)は .data セクション(初期値あり)か .bss セクション(初期値なし)に割り当てられる。 step1〜2 くらいでは、上記セクションはROMに割り当たっているため書き込みができない。 (変数に値を代入できない)
そこでリンカスクリプトで、.data セクションや .bss セクションを RAMに割り当てるように指定する。 しかし、以下の理由により問題がある
- h8write はフラッシュ ROM に書き込むようのツールである(RAM には直接書き込めない)
- RAM は電源 OFF でクリアされてしまうので、初期値がなくなってしまう。
そこで以下の処理が必要になる。
- 初期値等はROMに書き込んでおく
- 電源ON時に、プログラムの早い段階でROM上の初期値をRAMに書き込む。
- プログラム上では、変数はRAM上の値を参照する。
この際に以下のように呼ばれる。 ROM上に書き込まれたアドレス:物理アドレス(Physical Address)・ロードアドレス RAM上に書き込まれたアドレス:仮想アドレス(Virtual Address)・論理アドレス(Logical Address)・リンクアドレス
上記の PA ≠ VA 対応をするためには、リンカスクリプトで指定する。 セクション指定で以下のようにする。
SECTIONS
{
:
:
.data : {
_data_start = . ;
*(.data)
_edata = . ;
} > data AT> rom
:
:
}
プログラムは .data セクションを参照するような機械語コードにコンパイルされるので RAM を参照する。 しかしプログラムのロード時には ROM に格納する。
step03 まではプログラムをROMに直接書き込み、それを起動していた。 しかし、ROM は書き込み回数の制限があるので、頻繁に書き込みすべきではない。 そこで ROM には一定の動作をするプログラムを書き込んでおき 通常は書き込みをしないようにする。 動作はさまざまあるが、OSの実行ファイルをシリアル経由でダウンロードし RAMに展開してそれを起動するプログラムにしておく。 このプログラムを ブートローダー という。
-
SOH: 0x01
-
STX: 0x02
-
EOT: 0x04
-
ACK: 0x06
-
NAK: 0x15
-
CAN: 0x18
-
ブロック
内容 | サイズ | 備考 |
---|---|---|
ヘッダ | 1byte | SOH |
データブロック番号 | 1byte | ブロック番号。1からの連番。255以上で0から |
データブロック番号のビット反転(1の補数) | 1byte | チェック用 |
データ | 128byte | 121byte未満は EOF で埋める |
チェックサム | 1byte | チェックサム |
[送信側]
- NAK を受信したらデータを送信
- データはブロック単位にする。
- 1ブロック終了したら ACK または NAK を待つ
- ACK の場合は次のブロックを送信する
- NAK の場合は再送する
- データ終了時には EOT を送信する。ACK を受信したら終了。
- CAN を受信したら中断する。中断したいときは CAN を送信
[受信側]
- 受信準備できたら NAK を定期的に送信する。
- SOH (ブロックの先頭)を受信したら、それ以降をブロックとして132byte受信する。
- 受信OKなら ACK を返す
- 受信NGなら NAK を返して再送
- EOT を受信したら ACK を送信して終了。
- CAN を受信したら中断する。中断したいときは CAN を送信
あるメインの処理がされているときに、この処理を一時的に中断して(割り込んで)別の処理をすること。 この割り込んだときにしている処理を 割り込みハンドラ(またはコールバック) という。
CPUの割り込み検知用のピンを IRQ(Interrupt ReQuest) という。 このピンがどの状態の時変わるかで以下のようになる。
-
正論理: Low => High
-
不論理: High => Low
-
外部割り込み(ハードウェア割り込み):周辺I/Oに割り込み要因が発生して割り込み線がアサートされる
-
内部割り込み(ソフトウェア割り込み):CPU内部で発生したエラーなどで発生する割り込み
割り込みの動作は、
- 現在の動作に必要な情報を退避
- 割り込み処理(割り込みハンドラ実行)
- 割り込み復帰(退避した情報を元に戻す) となるので、割り込みハンドラの最後に 割り込み復帰命令 を置く必要がある。
割り込み復帰命令(Return From Interrupt instruction:RFI instruction) => H8では rte という命令をもつ。
情報の退避場所としては主に2つ。
- スタック
- 退避先専用のレジスタ
何を対比するかは主に2つ。 この2つの保存処理は アトミック でなければならない。 (=>CPUに命令を持っていることが多い)
- プログラムカウンタ(CPUが現在実行中のアドレス)
- モードレジスタ(コントロールレジスタ)
- ※特殊なレジスタ:割り込み禁止モードなどのCPUのモードを保持する。 割り込み発生時には異なるモードで動作する可能性があるため、 この状態も保持しておかなければならない。 また、多重割り込みを防ぐため(プログラムカウンタで戻り先がわからなくなるので) 割り込み発生時は自動的に割り込み無効モードとなる。
※この2つはCPUが持っている機能のため、割り込み発生時には自動的に退避される。 それ以外に必要な情報は自分で退避する必要がある。(汎用レジスタ・プログラムで利用するレジスタなど) => H8については、モードレジスタ=コンディションコードレジスタ(CCR)であり、 マニュアル(4.1.2)からPCおよびCCRはスタックに保存される。
- ER0〜ER2が揮発性レジスタ
- ER3〜ER6が不揮発性レジスタ
- 関数の引数は、ER0,ER1,ER2 を利用する
- 16ビットの値を渡すときには、16ビットレジスタの R0〜R2 を使用する。E0〜E2 は使用されない。
- 引数の4個目からはスタック渡しとなる。
- 0x000000: リセット
- 0X00001C: NMI(Non-Maskable Interrupt)無効化できない割り込み(無効化したくない最重要の割り込み)
- 0x000020〜0x00002c: トラップ命令(ソフトウェア割り込み。H8はシステムコール割り込みがないので代わりに使用する。)
- 0x0000d0〜0x0000dc: SCI0
- 0x0000e0〜0x0000ec: SCI1
- 0x0000f0〜0x0000fc: SCI2
H8 では割り込みベクタは ROM に配置されている。 これでは割り込みハンドラが固定されてしまう。(ROMは通常書き換えないため) 以下のように2段階にする。
- 割り込みハンドラはブートローダ(ROM)に固定する
- 割り込みハンドラは RAM の先頭をみてそのアドレスのハンドラを実行する
- RAM の先頭に OS 側の割り込みハンドラのアドレスを記述する => これで OS 側で自由に設定できるようになる。これを「ソフトウェア割り込みベクタ」と名づけておく
# intr.S
move.l er6,@-er7
move.l er5,@-er7
move.l er4,@-er7
move.l er3,@-er7
move.l er2,@-er7
move.l er1,@-er7
move.l er0,@-er7
[実行前]
メモリ |
---|
ER7(SP) |
[実行後]
メモリ |
---|
ER0 |
ER1 |
ER2 |
ER3 |
ER4 |
ER5 |
ER6 |
ER7(SP) |
intr.S で記述している関数 "intr_softerr", "intr_syscall", "intr_serintr" は、 汎用レジスタを退避して、"interrupt" をコールし、終わったら汎用レジスタを元に戻す。
[step07実装]
- vector.c: intr_syscall, intr_softerr, intr_serintrがコールされる(割り込みハンドラ)
- intr.S : intr_syscall, intr_softerr, intr_serintr の実装
- 汎用レジスタ退避し、interrupt()をコール。引数としてSOFTVEC_TYPE_XXXを渡す
- interrupt.c: SOFTVEC_TYPE_XXX の種類ごとに登録された関数を実行する。登録はsoftvec_setintr()
- ld.scr, interrupt.h: 0xffbf20 => softvec => SOFTVEC_ADDR => SOFTVECS SOFTVECSにソフトウェア割り込みベクタが格納されるが、RAMの先頭に指定された関数が実行される。
[step08実装]
-
kozos.c: setintr()で**softvec_setintr()**がコールされ登録する。
- intr_syscall, intr_softerr, intr_serintr はすべてthread_intr()で処理する
- kz_start()にて、SOFTVEC_TYPE_SYSCALLとSOFTVEC_TYPE_SOFTERRを登録。
- thread_intr() :登録した関数を実行してからスレッドを実行する。
- スレッドは割り込み発生を契機に実行される
-
システムコールの呼び出し
- thread_intr()によりsyscall_intr()を実行
- レディキューからcurrentを切り出して、current->syscall を参照して実行
- kz_syscall()がシステムコールを実行する。実行前にcurrent->syscallを設定する
- kz_run()とkz_exit()で実行している
- thread_intr()によりsyscall_intr()を実行
複数の処理をビジーループ(ポーリング)によって実装しようとすると、 いろいろと面倒な処理をしなければならない。
- 処理が終わらない場合に、処理の中断処理をしなければならい。
- 処理の個数が増えるとどんどん大変になる。
- 長い処理があると遅くなる
- 無駄な処理が多い。大半はどの処理を実行するかのチェックだけど常にチェックしている。
割り込みを使えばいいのでは? レジスタとスタックの状態を保持しておけば、処理を切り替えることが可能になり、 各々の処理が共通で考えることができる。 各々の処理をあるサービスの単位で考える必要が出てくるが、これをタスクという。
このタスクごとに独立して動作できるようにするものがスレッドという。
システムコール : OS に対するサービス要求。コールされたら OS はタスク切り替えをする。 スケジューリング: 次に動作すべきスレッドを選択する ディスパッチ : 選択されたスレッドの処理再開をする コンテキスト : スレッド処理中断時に保存が必要なCPUの状態 (一般的には、スタックポインタ・プログラムカウンタ・汎用レジスタなど)
OS は、スレッドの切り替えを以下のようにする。
- 割り込みハンドラで、現在のスレッドのコンテキストを保存する
- 現在のスレッドをスケジューリングする
- 次に実行するスレッドのコンテキストを復旧する
- 次に実行するスレッドをディスパッチする
また、OS は様々なリソースを管理する必要があるので、 ライブラリ関数的な呼び出され方をすると、 呼び出し側の影響がOSに影響するため割り込みの延長として実装される。 => OS は割り込みドリブンなプログラムということ。 => OSの機能を利用するということは割り込みを発生させるということ。 => システムコールを実行すると内部的にはシステムコール割り込みが発生する。
- ブートスタック :起動処理で使用する
- 割り込みスタック:割り込み処理で使用する
- ユーザスタック :スレッドごとに確保され使用する(※ブートローダには不要)
# intr.S(boot loader)
#現在実行中のプログラムの汎用レジスタをユーザスタックに保存
mov.l er6,@-er7
:
mov.l er0,@-er7
mov.l er7,er1 # interruptのarg2(現在使用していたスタックのアドレス)
mov.l #_intrstack,sp # スタックを割り込みスタックに変更(sp=er7である)
mov.l er1,@-er7 # 使用していたスタックのアドレスを割り込みスタックに保存
mov.w #SOFTVEC_TYPE_SOFTERR,r0 # interruptのarg1
jsr @_interrupt
mov.l @er7+,er1 # 割り込みスタックから割り込み実行前に使用していたスタックアドレスを戻す
mov.l er1,er7 # スタックを割り込み前のスタックに変更(sp=er7である)
# startup.s(OS)
# ディスパッチ処理
_dispatch: # dispatch関数
mov.l @er0,er7 # dispatch関数に渡された引数をスタックポインタにする(sp=er7)
mov.l @er7+,er0 # 渡された引数以降は汎用レジスタの値なので戻す
mov.l @er7+,er1
mov.l @er7+,er2
mov.l @er7+,er3
mov.l @er7+,er4
mov.l @er7+,er5
mov.l @er7+,er6
rte # PCとCCRを復旧する
TCB(タスクコントロールブロック)はリンクトリスト構造。 次のTCBのアドレスをnextにもつ。 readyque(レディキュー)は先頭のTCBと最後のTCBのアドレスを管理する。
TCB1(current) => TCB2 => TCB3 => NULL
readyque
head:TCB1
tail:TCB3
-
kz_start: 初期スレッドの起動
- ソフトウェア割り込みの登録: setintr()
- システムコールにはsyscall_intr
- ソフトエラーにはsofterr_intr
- thread_run(): start_threads() をキューに登録
- このときのputcurrent()処理はキューが空なので何もしないのと同じ
- dispatch(): キューの実行(startタスク)
- ソフトウェア割り込みの登録: setintr()
-
start_threads: startタスク。test08_1_main()(commandタスク)の登録
- kz_syscall(): トラップ命令による割り込み => thread_intr()実行
- thread_intr():
- syscall_intr():
- syscall_proc(): startタスクをキューから切り離して、call_functions()=thread_run()実行
- thread_run(): startタスクをキューに再登録。test08_1_main()をキューに登録。
- syscall_proc(): startタスクをキューから切り離して、call_functions()=thread_run()実行
- schedule(): startタスクをスケジューリング
- dispatch(): startタスクを継続実行
- syscall_intr():
-
thread_end(): kz_exit()実行
-
kz_exit():
- kz_syscall(): トラップ命令による割り込み => thread_intr()実行
- thread_intr():
- syscall_intr():
- syscall_proc(): startタスクをキューから切り離して、call_functions()=thread_exit()実行
- thread_exit(): startタスク終了
- syscall_proc(): startタスクをキューから切り離して、call_functions()=thread_exit()実行
- schedule(): startタスクはキューに戻されていないのでcommandタスクをスケジューリング
- dispatch(): commandタスクを継続実行
- syscall_intr():
ビットシフトはビットをズラして0を埋める。
- 左シフトは1ビットズラすと
$2^1$ になる - 右シフトは1ビットズラすと
$1/2$ になる
# 1 を 0ビット左シフトなので結局 "1" = 1ビット目がフラグとなる
# 2ビット目を指定したい場合は1ビット左シフトすればいい。 TWO_BIT (1 << 1)
# 3ビット目を指定したい場合は2ビット左シフトすればいい。 THREE_BIT (1 << 2)
#define KZ_THREAD_FLAG_READY (1 << 0)
# current->flags = current->flags | KZ_THREAD_FLAG_READY
# となりcurrent->flagsの1ビット目と "1" の OR を取るので必ず "1" になる。
# すなわち1ビット目に "1" を立てることになる。
current->flags |= KZ_THREAD_FLAG_READY;
# ~KZ_THREAD_FLAG_READY => NOT "1" => 1ビット目が "0"
# current->flagsの1ビット目と "0" の AND を取るので必ず "0" になる。
# すなわち1ビット目に "0" を立てることになる。
current->flags &= ~KZ_THREAD_FLAG_READY;