スレッドを実装します。まずは
まずはH8/300Hの命令から。
僕はアドレスイメージが下から上なのでデータシートとは逆です。H8のデータシートは ひっくり返して見てます...。
SP(ER7)をアドレスレジスタとしてスタックをアクセスする時はバイトアクセス はできない。これは気をつけないといけない。(見事にはまりました。)
RTSとRTEの違いはCCRの設定をするかしないかだけ。 プログラム的にはPCとCCRを合わせて32bitとして扱うことにしました。
規模的にはthread.h
まずはsetjmp,longjumpの延長系から、プログラムから明示的にコンテキストを スイッチするプログラム。ITRON的にはrotate ready queue.
テストにIRQ4に実装してみたのがこれ。
- 多重割り込みなし、
- プライオリティなし。
- 実行中のスレッドは割り込み以外にプリエンプトされることはない。
- 割り込みの終了時にスケジューリングが行なわれる。この際に実行中スレッ ドより高いプライオリティのスレッドがレディキューに入っていない限り、 実行権を奪われることはない。(rotate ready queueしない)
まずはH8/300Hの命令から。
- JSR Jump to SubRoutine (PC → @-SP, リターンアドレス→PC) calleeのSPに戻るべきアドレス(JSR命令の次のアドレス)が入る。
- RTS ReTurn from Subroutine (@SP+ →PC) SPからPCをとってそこにジャンプ。
- RTE ReTurn form Exception (@SP+ →PC, @SP+ →CCR) 例外からのリターンは 例外発生時にSPの位置に上位8bitにCCRが、下位24bitに例外発生時のPCが入っている ので、それぞれを設定してジャンプする。
before after | SP | | | | | | PC | 3 | | | PC | 2 | | | PC | 1 | | |reserved| 0← SP MSB 1stなので|res|PC |PC |PC |例外に入った時は
before after | SP | | | | | | PC | 3 | | | PC | 2 | | | PC | 1 | | | CCR | 0← SP MSB 1stなので|ccr|PC |PC |PC |
僕はアドレスイメージが下から上なのでデータシートとは逆です。H8のデータシートは ひっくり返して見てます...。
SP(ER7)をアドレスレジスタとしてスタックをアクセスする時はバイトアクセス はできない。これは気をつけないといけない。(見事にはまりました。)
RTSとRTEの違いはCCRの設定をするかしないかだけ。 プログラム的にはPCとCCRを合わせて32bitとして扱うことにしました。
規模的にはthread.h
#define THREAD_MAX 4
ターゲットのRAMの量が8Kということもあり、4つ、せいぜい8つまでを考えます。
#define THREAD_STACK_SIZE 256
このスタック量は少な過ぎかも。とりあえずこの程度で。
enum thread_state
{
RUN, READY, WAIT, DORMANT, NONEXISTENT
};
enum error_code
{
E_OK,
E_OBJ,
E_NOMEM,
};
void thread_init (void);
int thread_create (void (*)(void), int *);
int thread_start (int);
int thread_switch (void);
int thread_sleep (int);
int thread_wakeup (int);
int thread_id (int *);
このくらいで。
まずはsetjmp,longjumpの延長系から、プログラムから明示的にコンテキストを スイッチするプログラム。ITRON的にはrotate ready queue.
_thread_switch: ; disable interrupt stc.b ccr, r2l 現在のCCRの状況をとっておきます。ここで欲しいのは割り込みだけ。フラグは いらない(明示的にサブルーチンコールなので)。 orc #0x80, ccr このルーチンの途中で割り込みがかかって、そこでスレッドの状態をいじられると 困るので割り込みを禁止します。 ; save context. mov.l @_current_thread, er0 現在走っているスレッドのコンテキストをとってきます。 mov.l @sp, er1 ; return address mov.l er1, @(0x20, er0) ; pc このルーチンに入った時点でJSRによってリターンアドレスがSPに入っているので それをcurrent_thread->reg.ccr_pcに入れます。 mov.b r2l, @(0x20, er0) ; ccr |ccr(8)| pc(24) | current_thread->reg.ccr_pcの上位バイトにCCRを入れます。これでRTEを使って CCR,PCともに復帰させる目論見。 mov.l sp, @(0x1c, er0) ; sp 復帰すべきSPをとっておきます。 mov.l er6, @(0x18, er0) ; callee saved mov.l er5, @(0x14, er0) ; callee saved mov.l er4, @(0x10, er0) ; callee saved callee saved(呼び出された側がリターン時に保証しないといけないレジスタ) をとっておきます。caller saved(呼び出し側があらかじめサブルーチンコール の前に保存しておかないといけないレジスタ)は、既にスタックの中に入ってい るのでとっておく必要はありません。 この時点で呼び出し元に戻る情報は全て保存したので、この先は全部のレジスタ、 スタックを使い放題です。 jsr @_thread_context_switch thread_context_switchで、current_threadが変更されたかもしれないし、元のまま かもしれません。次のスレッドに向けてレジスタを設定します。 mov.l @_current_thread, er0 mov.l @(0x1c, er0), sp ; sp スタックをまず復帰。 mov.l @(0x20, er0), er2 ; pc mov.l er2, @sp ; set return address and ccr RTE命令が読み出すアドレス(SP)にPCとCCRを設定。サブーチンコールの場合、 割込みビットだけが必要。あとのビットはなんでもいい。 mov.l @(0x18, er0), er6 ; callee saved mov.l @(0x14, er0), er5 ; callee saved mov.l @(0x10, er0), er4 ; callee saved mov.l @(0x0c, er0), er3 ; callar saved mov.l @(0x08, er0), er2 ; callar saved mov.l @(0x04, er0), er1 ; callar saved mov.l @(0x00, er0), er0 ; callar saved このサブルーチンだけでスイッチする分にはcallee savedだけ復帰させればい いのだけれど、割り込みでスイッチされたコンテキストを復帰させるには全て のレジスタの復帰が必要なので、caller savedも復帰させます。 ; resume interrupt. and return rte ここでCCRが設定されるので、中断した状態の割り込み状態に戻り、リターンします。
struct reg
{
uint32_t er0; // 0x00
uint32_t er1; // 0x04
uint32_t er2; // 0x08
uint32_t er3; // 0x0c
uint32_t er4; // 0x10
uint32_t er5; // 0x14
uint32_t er6; // 0x18
uint32_t sp; // 0x1c (er7)
uint32_t ccr_pc; // 0x20
} __attribute__((packed));
実行状態そのものです。JSR,RTEの仕様を使ってCCRとPCは一緒になっています。
リオーダリングされないように(アセンブラの中でこの順番になっていることを
前提としているので)、packedに。この場合、4byteアラインなのでパディング
は不用。中途半端だった場合、これを内包したstructをpackedした時にとんで
もないことになるので、4byteアラインになるようにパディングしないとだめ。
struct thread_control
{
struct reg regs;
実行状態です。このストラクチャもpackedなのでこのstructの先頭は必ずレジスタです。
enum thread_state state;
int id;
int priority;
addr_t stack_bottom;
addr_t stack_top;
メモリの少なさとスタックの溢れがせめぎ合うのでチェック用にとっておきます。
int wakeup_request;
RUNあるいはREADY状態でthread_wakeupを受けた時のカウント。
SIMPLEQ_ENTRY(thread_control) tc_link;
レディキュー用のエントリ
} __attribute__((packed, aligned(4)));
レジスタのストラクチャを一番先頭にもっておきたいのと、アラインの保証の
ために。(まずaligned(4)しなくても大丈夫だとは思うけど。まだあまり調べて
ないので安牌に振った。)
struct thread_control thread_control[THREAD_MAX];
uint8_t stack[THREAD_MAX][THREAD_STACK_SIZE];
mallocは実装しないので、全部静的に割り当てです。状況によってはアロケー
トできなかった...なんてことは絶対に避けます。
struct thread_control *current_thread;
今動いているRUN状態のスレッドです。
SIMPLEQ_HEAD (tc_queue, thread_control);
struct thread_manager
{
struct tc_queue ready_queue;
} thread_manager;
レディキューです。ここに、つなげられれば実行される可能性があります。
void
thread_init ()
{
int i, s;
struct tc_queue *ready_queue = &thread_manager.ready_queue;
s = intr_suspend ();
SIMPLEQ_INIT (ready_queue);
for (i = 0; i < THREAD_MAX; i++)
{
struct thread_control *tc = thread_control + i;
memset (tc, 0, sizeof (struct thread_control));
.bssに置いてるので無駄な作業です。スタートアップで一括してゼロクリアしてます。
tc->id = i;
tc->state = NONEXISTENT;
tc->priority = 0;
tc->stack_bottom = (addr_t)stack[i] + (THREAD_STACK_SIZE - 4);
ここの位置にuint32_tでPCが入るので-4。
tc->stack_top = (addr_t)stack[i];
*(uint32_t *)tc->stack_top = 0xac1dcafe; // canary
スタックオーバーフローをチェックするためにカナリヤを入れておきます。
SCI_PRINTF ("init %d: %x-%x\n", i, tc->stack_bottom, tc->stack_top);
}
current_thread = &thread_control[0];
今の状態をスレッド0としておきます。スタックは違うけれど。ここはいずれ。
current_thread->state = RUN;
SIMPLEQ_INSERT_HEAD (ready_queue, current_thread, tc_link);
整合性のために設定します。
intr_resume (s);
}
int
thread_id (int *id) 呼び出し元のスレッドIDを返します。
{
int i;
addr_t sp = 0;
__asm volatile ("mov.l sp, %0" :: "r"(sp));
スタック位置によってどのスレッドかを見分けます。
for (i = 0; i < THREAD_MAX; i++)
{
struct thread_control *tc = thread_control + i;
if (sp < tc->stack_bottom && sp > tc->stack_top)
break;
}
if (i == THREAD_MAX)
return E_OBJ;
*id = i;
return E_OK;
}
int
thread_create (void (*start)(void), int *tid)
{
struct thread_control *tc;
int id;
int s;
s = intr_suspend ();
空いてるスレッドスロットを探します。
for (id = 0; id < THREAD_MAX; id++)
{
tc = thread_control + id;
if (tc->state == NONEXISTENT)
break;
}
if (id == THREAD_MAX)
return E_NOMEM;
tc->regs.ccr_pc = (uint32_t)start;
コンテキストスイッチされる時にジャンプする場所を設定。CCRはどうでもいい。
割り込み許可でディスパッチされます。
tc->regs.sp = (uint32_t)tc->stack_bottom;
// install return address for 'rts'
*(uint32_t *)tc->regs.sp = tc->regs.ccr_pc;
RTE命令用にスタックに埋めこんでおきます。
tc->state = DORMANT;
thread_start()まで動きません。
*tid = id;
intr_resume (s);
SCI_PRINTF ("thread create %d: pc=%x sp=%x\n", id, tc->regs.ccr_pc,
tc->regs.sp);
return E_OK;
}
int
thread_start (int id)
{
struct thread_control *tc = thread_control + id;
int s;
enum error_code err = E_OBJ;
s = intr_suspend ();
if (tc->state == DORMANT)
{
tc->state = READY;
SIMPLEQ_INSERT_TAIL (&thread_manager.ready_queue, tc, tc_link);
レディキューに登録します。機会があれば実行されます。
err = E_OK;
}
intr_resume (s);
return err;
}
int
thread_sleep (int id)
{
int s = intr_suspend ();
struct thread_control *tc = thread_control + id;
if (--tc->wakeup_request < 0)
{
wakeup_requestはWAIT状態じゃないのにwakeupされた時のカウント。それがあ
ればWAIT状態に入ってもすぐに起きる。ITRONの仕様です。どこまでキューでき
るかは実装依存。
SIMPLEQ_REMOVE (&thread_manager.ready_queue, tc, thread_control, tc_link);
tc->state = WAIT;
レディキューから外してWAIT状態にします。
SCI_PRINTF ("sleep: %d\n", id);
thread_switch ();
次のスレッドに明け渡します。
SCI_PRINTF ("wakeup: %d\n", id);
どこか他のスレッドや、割り込みからwakeupされればここまで来ます。二度とこないかも
しれません。
}
intr_resume (s);
return E_OK;
}
int
thread_wakeup (int id)
{
int s = intr_suspend ();
struct thread_control *tc = thread_control + id;
if (tc->state == READY)
{
tc->wakeup_request++;
ターゲットのスレッドがレディーキューに入っているなら、起床回数を増やします。
シングルプロセッサなので自分がRUNなら相手がRUNであることはありえません。
}
else
{
tc->state = READY;
SIMPLEQ_INSERT_TAIL (&thread_manager.ready_queue, tc, tc_link);
WAIT状態からREADY状態にしてレディキューに追加します。WAIT状態なのを確認しないとだめだ。DORMANT, NONEXISTENTの場合がある。XXX
}
intr_resume (s);
return E_OK;
}
int
thread_context_switch ()
{
struct tc_queue *ready_queue = &thread_manager.ready_queue;
// already interrupt disabled.
アセンブラのthread_switchから呼ばれるルーチンです。
if (current_thread->state == RUN)
{
current_thread->state = READY;
SIMPLEQ_REMOVE_HEAD (ready_queue, tc_link);
SIMPLEQ_INSERT_TAIL (ready_queue, current_thread, tc_link);
}
thread_sleepしてから呼ばれる場合、current_threadは既にレディキューから
外されてWAITになっているので、RUNの確認が必要。
そうでない場合、今のスレッドはレディーキューの最後につながれる。
current_thread = SIMPLEQ_FIRST (ready_queue);
current_thread->state = RUN;
次のスレッドをcurren_threadにしてリターンすれば、後のthread_switchで
コンテキストを切り替えてくれる。
return 0;
}
void
thread_stack_check ()
{
int i;
スタックオーバーフローしていないかチェックします。
for (i = 0; i < THREAD_MAX; i++)
{
struct thread_control *tc = thread_control + i;
if (*(uint32_t *)tc->stack_top != 0xac1dcafe)
{
SCI_PRINTF ("thread %d stack overflow. %x\n", i,
*(uint32_t *)tc->stack_top);
}
}
}
ここまでで明示的にサブルーチンコール(thread_switch)で切り替えるのはでき
た。割り込みから切り替えるには、すべてのレジスタを退避しないといけない。
テストにIRQ4に実装してみたのがこれ。
.globl _irq4 _irq4: mov.l er0, @-sp mov.l @_current_thread, er0 mov.l er1, @(0x04, er0) mov.l er2, @(0x08, er0) mov.l er3, @(0x0c, er0) mov.l er4, @(0x10, er0) mov.l er5, @(0x14, er0) mov.l er6, @(0x18, er0) mov.l @sp+,er1 mov.l er1, @(0x00, er0) ; er0 mov.l @sp, er1 mov.l er1, @(0x20, er0) ; ccr_pc mov.l sp, @(0x1c, er0) ; sp jsr @_thread_context_switch これを呼ぶとrotate ready queueなのでITRONの仕様に合わないのだけれど、 いろいろコンテキストをスイッチするテストのため。 mov.l @_current_thread, er0 mov.l @(0x1c, er0), sp ; sp mov.l @(0x20, er0), er2 ; pc mov.l er2, @sp ; set return address and ccr mov.l @(0x18, er0), er6 mov.l @(0x14, er0), er5 mov.l @(0x10, er0), er4 mov.l @(0x0c, er0), er3 mov.l @(0x08, er0), er2 mov.l @(0x04, er0), er1 mov.l @(0x00, er0), er0 ; resume ccr and return. rteこのオーバーヘッドの評価はまだだけど、オーバーヘッドがきつければディス パッチしなくてもいいし、したければ、タイマ割りこみをここで仕掛ければい い(そこでディスパッチ)。
