081220

|


スレッドを実装します。まずは
  • 多重割り込みなし、
  • プライオリティなし。
の簡単な仕様からはじめました。スケジューリングポリシーは基本的にはITRON に従います。
  • 実行中のスレッドは割り込み以外にプリエンプトされることはない。
  • 割り込みの終了時にスケジューリングが行なわれる。この際に実行中スレッ ドより高いプライオリティのスレッドがレディキューに入っていない限り、 実行権を奪われることはない。(rotate ready queueしない)
細かい仕様についてはITRONの仕様に拘らず、作りたいように作ります。
まずは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が入っている ので、それぞれを設定してジャンプする。
JSRをした時は
  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
このオーバーヘッドの評価はまだだけど、オーバーヘッドがきつければディス パッチしなくてもいいし、したければ、タイマ割りこみをここで仕掛ければい い(そこでディスパッチ)。