(H8移植編第14回)DRAM上で動かそう

2009/10/05

あなたは 人目のお客様です.

今まで内蔵RAM上で動いていたのだけど,LANコントローラ対応でいよいよ内蔵RAMでは サイズ的にきつくなってきて,これ以上の機能追加は厳しいので,OSをDRAM上で 動かすように修正しよう.

で,DRAMへの以降なのだけど,OSで行っているDRAMコントローラの初期化を ブートローダーで行うようにして,あとOSのリンカスクリプト書き換えて DRAM上に持っていくだけで超簡単にできると思っていたのだけど, バグがあってちょっとひと苦労.

まずDRAMの初期化なのだけど,OSのデバッグ中に気がついたことなのだけど, 従来のままだとしばらく値を保持させるとビット化けしてしまうアドレスが存在する ことが判明.というのは,ブートローダー対応してからOSをDRAMに移したのだけど, DRAM上で動かすようにすると固まってしまう(内蔵RAM上だとなぜか正常に動作する) という問題が出て,まあ実際の原因は後述するint型の問題だったのだけど, どうもDRAMがちゃんと動いていないように思えたのでチェックプログラムを作って 試してみたらやはりビット化けするアドレスが存在する.

従来のメモリチェックは値を書き込んだ直後に読む,ということをやっていたので 値をしばらくのあいだ保持できているかということはチェックできていなかった. なので最初に全領域に値を書き込んで,その後で全領域を読んでチェック,さらに 特定のビットが0に化けても1に化けても検出できるように,ビットを立てた パターンとビットを落としたパターンの両方でチェックする,というようなチェックを したらビット化けが検出された.

ということでdram_init()での設定パラメータがあやしく思い,ウエイトと リフレッシュレートをいろいろ変えてみたのだけど,リフレッシュレートを上げたら ビット化けしないようになった.どうもリフレッシュレートが低すぎたらしい. まあほんとはデータシート見たりして適切な値を調べるべきなのだけど, とりあえず全メモリ全パターンチェックでエラーにならなくなったのでよしとする. (なんかDRAM搭載のボートで買うと,DRAMコントローラの設定例が載った ドキュメントがついてくるみたいね.今回利用しているボードだとDRAMはおまけ扱い なのでドキュメントがついてないってことかもしれん)

ただそれだけ直しただけではまだダメ(っていうか,ビット化けするのは使っていない アドレスだったので,これが固まる原因ではなかった)で,OSのテキストとデータ領域は DRAMに移しても正常動作したのだけど,BSS領域を移すとやはりうまく動かず固まって しまう.

いろいろ見直したのだけど原因がよくわからなくて,しかたがないので いろんなところに puts() を入れてシリアル出力させてどこまで処理が進んだか 見るという原始的ないわゆる printf デバッグで追いかけたら, kz_free()でのメモリ解放でおかしな領域を解放しようとして kz_sysdown() が 呼ばれて while(1) の無限ループに入っていることが判明.この時点では うわーメモリ破壊かなーめんどくさそーと思ったのだけど,もっと追いかけたら kz_send()でのメッセージ送信でなんか送信先のスレッドIDがおかしくなっていて, そのせいでおかしなポインタの先を参照して kz_free() におかしなアドレスを 渡していることが判明.

ということで原因はおかしなスレッドIDでkz_send()が呼ばれていることなのだけど, printfデバッグで調べたら,送信元は command スレッドになっている.ということは 送り先は extintr なのだけど,extintr_id は uint32 としてきちんと定義されて いる.

で,extintr_id はどこで得られているかというと,kozos.c のスレッド起動部分で kz_run()の戻り値を与えている.このへんでようやく気がついたのだけど, 実は kz_run() の戻り値が int 型として定義されていたので,16ビット整数に なってしまい,スレッドIDの上位ビットが削られた状態で extintr_id に格納されて いたのだ.このため kz_send()でのメッセージ送信時に,おかしなスレッドID宛に 送信していたというのが結局の原因.

この原因を考えると,うーんいままでよく動いていたなーと思うのだけど, 実は従来は内蔵RAM上にBSSがあったのだけど,内蔵RAMのアドレスが 0xffbf20 〜 0xffff00 なので,上位ビットが削られて16ビット値となっても, その後に extintr_id に格納されるときに32ビットに拡張される際に, 上位ビットが符号拡張されて,0xffffXXXX のようなアドレスになって格納されると 思うのだな.で,実際にメモリアクセスされる際には上位8ビットが無視されて 0x00ffXXXX のアドレスがアクセスされることで,正常動作していたようだ. なのでBSSをDRAMに移したことでアドレスが 0x400000 〜 0x5fffff になってしまい, 正常動作しなくなったということのようだ.

いやー,実は今までもBSSを動かすとなんか固まるときがあるなーとは思っていたの だけど,これが原因だったわけだ.ちょっと調べるのたいへんだったけど,まあ 原因判明してよかったよかった.なおしたらあっさりと動いた.

で,今回の修正なのだけど,いい機会なのでリンカスクリプトを修正して,メモリ上の アドレスは基本的にリンカスクリプトで指定して,そこを見るように変更した. というのは,たとえばスレッドのスタックのアドレスを指定するのに今までは configure.h で

#define THREAD_STACK_START 0xffe820
のようなことをやっていたわけなのだけど,こーいうアドレス情報は基本的には リンカスクリプトで指定して,その値を見るようにすべきだと思うのだな. というのは,これだとまずいろんなアドレス設定がいろんなヘッダファイルに点在 してしまい,メモリ調整が面倒になったり調整し忘れでバグの原因になったりする 可能性が高い.さらに,たとえばある領域の直後の領域を使いたい場合などに, #define でのベタ書きではなく,リンカスクリプトのリンク情報に応じて 自動調整されるようにしたほうが効率的,といった理由がある.

ということで,今回の大きな修正は以下.

以下,修正したソース. まずブートローダーの修正点について.

main.cなのだけど,DRAMコントローラの初期化用に dram_init() の呼び出しを 追加した.実際の DRAM 初期化処理は dram.c をOS側から持ってきて, リフレッシュレートの対応とメモリチェックの追加を行った. 具体的には dram.c に以下のような対応が入っている.

--- os/ether/dram.c	Sat Oct  3 13:22:25 2009
+++ bootload/dram/dram.c	Sun Oct  4 23:35:28 2009
@@ -27,10 +27,29 @@
 
 int dram_init()
 {
+  /*
+   * dram_check2()でチェックしたら,値をうまく保持できないビットが存在した
+   * ので,リフレッシュレートを上げたら解消された.(ウエイトを多めにしても
+   * 効果は無かった)
+   * とりあえず,リフレッシュレート高めの設定にしておく.
+   */
+
   *H8_3069F_ABWCR  = 0xff;
+#if 0
   *H8_3069F_RTCOR  = 0x07;
+#else
+  *H8_3069F_RTCOR  = 0x03; /* リフレッシュ周期を短めに設定 */
+#endif
+#if 1
   *H8_3069F_RTMCSR = 0x37;
+#else
+  *H8_3069F_RTMCSR = 0x2f; /* リフレッシュ周波数UP */
+#endif
+#if 1
   *H8_3069F_DRCRB  = 0x98;
+#else
+  *H8_3069F_DRCRB  = 0x9f; /* ウエイト挿入 */
+#endif
   *H8_3069F_DRCRA  = 0x30;
 
   *H8_3069F_P1DDR  = 0xff;
@@ -39,9 +58,17 @@
   /* *H8_3069F_PBDDR = ...; */
 
   /* H8_3069F_WCRH = ...; */
+#if 1
   *H8_3069F_WCRL = 0xcf;
-
-  *H8_3069F_ASTCR = 0xfb;
+#else
+  *H8_3069F_WCRL = 0xff; /* ウエイト挿入 */
+#endif
+
+#if 1
+  *H8_3069F_ASTCR = 0xfb; /* 2ステートアクセス */
+#else
+  *H8_3069F_ASTCR = 0xff; /* 3ステートアクセス */
+#endif
 
   return 0;
 }
dram.c には他にもメモリチェックとメモリクリアの関数が追加されている. これは以下.
@@ -107,4 +134,82 @@
   putxval((unsigned long)*p, 8);
   puts("\n");
   return -1;
+}
+
+static uint32 dram_check2_val0(uint32 *addr) {  return  0x55555555UL; }
+static uint32 dram_check2_val1(uint32 *addr) {  return  0xaaaaaaaaUL; }
+static uint32 dram_check2_val2(uint32 *addr) {  return  0x00000000UL; }
+static uint32 dram_check2_val3(uint32 *addr) {  return  0xffffffffUL; }
+static uint32 dram_check2_val4(uint32 *addr) {  return  (uint32)addr; }
+static uint32 dram_check2_val5(uint32 *addr) {  return ~(uint32)addr; }
+
+int dram_check2()
+{
+  uint32 *p;
+  int ret = 0, i;
+  uint32 (*getval[])(uint32 *) = {
+    dram_check2_val0,
+    dram_check2_val1,
+    dram_check2_val2,
+    dram_check2_val3,
+    dram_check2_val4,
+    dram_check2_val5,
+    NULL
+  };
+
+  for (i = 0; getval[i]; i++) {
+
+    puts("DRAM check pattern: ");
+    putxval(i, 0);
+
+    puts("\nDRAM setting...\n");
+
+    for (p = (uint32 *)DRAM_START; p < (uint32 *)DRAM_END; p++) {
+      *p = getval[i](p);
+      if (!((uint32)p & 0xfff)) {
+	putxval((unsigned long)p, 8);
+	puts("\x08\x08\x08\x08\x08\x08\x08\x08");
+      }
+    }
+
+    puts("\nDRAM checking...\n");
+
+    for (p = (uint32 *)DRAM_START; p < (uint32 *)DRAM_END; p++) {
+      if (*p != getval[i](p)) {
+	puts("\nERROR! :");
+	putxval((unsigned long)p, 8);
+	puts(" ");
+	putxval((unsigned long)*p, 8);
+	puts("\n");
+	ret = -1;
+      }
+      if (!((uint32)p & 0xfff)) {
+	putxval((unsigned long)p, 8);
+	puts("\x08\x08\x08\x08\x08\x08\x08\x08");
+      }
+    }
+
+    puts("\n");
+
+  }
+
+  if (ret == 0) {
+    puts("\nall check OK.\n");
+  }
+
+  return ret;
+}
+
+int dram_clear()
+{
+  uint32 *p;
+
+  puts("DRAM clearing...\n");
+
+  for (p = (uint32 *)DRAM_START; p < (uint32 *)DRAM_END; p++)
+    *p = 0;
+
+  puts("DRAM cleared.\n");
+
+  return 0;
 }
あとmemset()とかのライブラリ関数を集めた lib.c と,あとシリアルドライバの serial.c が,OS側のやつとちょっとなんか食い違っていたので,OSのものに 実装を合わせた.

さらに main.c の init() に dram_init() の呼び出しを加えて, コマンド解釈部分には メモリチェック用に ramchk, ramchk2 と, あとメモリクリア用に ramclr を追加した.以下のような感じ.

@@ -159,6 +76,12 @@
 	f = (void (*)())entry_point;
 	f();
       }
+    } else if (!strcmp(buf, "ramchk")) {
+      dram_check();
+    } else if (!strcmp(buf, "ramchk2")) {
+      dram_check2();
+    } else if (!strcmp(buf, "ramclr")) {
+      dram_clear();
     } else {
       puts("unknown command.\n");
     }
あと serial.c をOS側に合わせて修正したことでSCIの番号指定が必要になったので, シリアルを使っている部分全般(main.cとlib.cとxmodem.c)にインデックス指定の 対応が各所に入っている.

次に,OS側の修正.

まず,kz_run()の戻り値がint型になっていて,スレッドIDの上位ビットを渡せていない という大問題の修正.

diff -u os/ether/syscall.h os/ondram/syscall.h
--- os/ether/syscall.h	Sat Oct  3 11:01:06 2009
+++ os/ondram/syscall.h	Mon Oct  5 01:02:01 2009
@@ -31,7 +31,7 @@
       int stacksize;
       int argc;
       char **argv;
-      int ret;
+      uint32 ret;
     } run;
     struct {
       int dummy;
修正はこれだけ.kz_run()の処理の本体である thread.c:thread_run() では戻り値は きちんとuint32になっているので,ホントに修正忘れ.これでずいぶんハマッて しまった.

次に,リンカスクリプトの修正.

diff -u os/ether/ld.scr os/ondram/ld.scr
--- os/ether/ld.scr	Sat Oct  3 21:11:47 2009
+++ os/ondram/ld.scr	Sun Oct  4 23:38:45 2009
@@ -5,11 +5,19 @@
 MEMORY
 {
 	/*
-	 * reserve 256bytes space for software interrupt vector
-	 * and 256bytes space for ELF header.
+	 * DRAM (2MB)
+	 * reserve 256bytes space for ELF header.
 	 */
+	dram(rwx)	: o = 0x400000 + 0x100, l = 0x200000 - 0x100
+
+	/*
+	 * internal RAM (16KB)
+	 * reserve 256bytes space for software interrupt vector.
+	 */
+	uservec(rw)	: o = 0xffbf20, l = 0x000100 /* start of RAM */
 	ram(rwx)	: o = 0xffbf20 + 0x200, l = 0x004000 - 0x200 /* 16kb */
-	bootstack(rw)	: o = 0xffff00, l = 0x000010 /* end of RAM */
+	userstack(rw)	: o = 0xffc120, l = 0x000010
+	bootstack(rw)	: o = 0xffff00, l = 0x000010
 	intrstack(rw)	: o = 0xffff00, l = 0x000010 /* end of RAM */
 }
 
@@ -19,29 +27,43 @@
 		_text_start = . ;
 		*(.text)
 		_etext = . ;
-	} > ram
+	} > dram
 
 	.rodata : {
 		_rodata_start = . ;
 		*(.strings)
 		*(.rodata)
+		*(.rodata.*)
 		_erodata = . ;
-	} > ram
+	} > dram
 
 	.data : {
 		_data_start = . ;
 		*(.data)
 		_edata = . ;
-	} > ram
+	} > dram
 
 	.bss : {
 		_bss_start = . ;
 		*(.bss)
 		*(COMMON)
 		_ebss = . ;
-	} > ram
+	} > dram /* DRAMにするとなぜか固まる.要調査 */
 
+	. = ALIGN(4);
 	_end = . ;
+
+	.freearea : {
+		_freearea = .;
+	} > dram
+
+	.uservec : {
+		_uservec = .;
+	} > uservec
+
+	.userstack : {
+		_userstack = .;
+	} > userstack
 
 	.bootstack : {
 		_bootstack = .;
dram空間を定義して,テキスト,データ,BSSをdram空間に持っていく. dram空間を+256バイトして定義しているのは,以前にELF形式をロードできるように したときと同じ理由で,ELFヘッダがうまく入る領域を空けているため (ELFヘッダがはみ出してしまうので).

さらにソフトウエア割り込みベクタの登録域として uservec 領域, スレッドのスタック領域として userstack 領域というのを新設した. これらは従来は configure.h で THREAD_STACK_START とか, interrupt.h で VECTOR_ADDR とかで直の値で定義してあったのだけど, リンカスクリプトで集中定義することでメモリ調整時の修正ミスや修正洩れを 無くすため.

さらにさらに,従来は kz_alloc() で獲得できる動的メモリ領域の2048バイトの 領域をDRAMの先頭からとしてこれも直接指定していたのだけど,DRAM先頭付近は OSの本体がロードされるので,それに合わせて後ろの空いた部分を使えるように, freeareaという領域をBSSの直後に作成している.(実際にはその先頭アドレスが わかればいいだけなので,空領域として定義している)

で,リンカスクリプトの修正に合わせて,configure.h と interrupt.h の修正.

diff -u os/ether/configure.h os/ondram/configure.h
--- os/ether/configure.h	Sat Oct  3 16:11:48 2009
+++ os/ondram/configure.h	Mon Oct  5 01:11:41 2009
@@ -4,13 +4,10 @@
 #include "types.h"
 #include "lib.h"
 
-extern uint32 intrstack;
+extern char intrstack;
+extern char userstack;
 #define INTR_STACK_START   (&intrstack)
-#if 0
-#define THREAD_STACK_START 0xffe820
-#else
-#define THREAD_STACK_START 0x5f0000
-#endif
+#define THREAD_STACK_START (&userstack)
 
 #define SIGHUP   1
 #define SIGILL   4
アドレス直指定でなく,リンカスクリプトで定義した userstack シンボルの位置を 見るようにしている.userstack はリンカスクリプト内で作成したシンボルなので 変数としての実体は無いが,C言語から参照するために extern 宣言して参照して いる.

次に,interrupt.h の修正.

diff -u os/ether/interrupt.h os/ondram/interrupt.h
--- os/ether/interrupt.h	Sat Oct  3 11:01:06 2009
+++ os/ondram/interrupt.h	Sun Oct  4 17:32:21 2009
@@ -2,7 +2,10 @@
 #define _INTERRUPT_H_INCLUDED_
 
 #define VECTOR_NUM 64
-#define VECTOR_ADDR ((void *)0xffbf20)
+
+extern char uservec;
+#define VECTOR_ADDR (&uservec)
+
 typedef void (*interrupt_handler_t)(int vec);
 
 #define VECTORS ((interrupt_handler_t *)VECTOR_ADDR)
これも configure.h の修正と同じで,アドレス直指定でなくリンカスクリプトで 定義した uservec シンボルの位置を見ることで,ソフトウエア割り込みベクタの 位置を求めるようにしている.どう?スマートになったでしょ?

次に,main.c の修正.

diff -u os/ether/main.c os/ondram/main.c
--- os/ether/main.c	Sat Oct  3 19:45:04 2009
+++ os/ondram/main.c	Mon Oct  5 01:08:59 2009
@@ -14,11 +14,25 @@
   extern int ebss;
 
   /* clear BSS */
+#if 0
   memset(&bss_start, 0, (uint32)&ebss - (uint32)&bss_start);
+#else
+  /*
+   * memset()はサイズをintで渡すので,BSSが32KBを越えたときに
+   * 正常にクリアできないので,memset()を利用せずに自前でクリアする.
+   */
+  {
+    char *p;
+    for (p = (char *)&bss_start; p < (char *)&ebss; p++)
+      *p = 0;
+  }
+#endif
 
   serial_initialize(0, 0);
 
+#if 0 /* ブートローダーで初期化しているので,初期化不要 */
   dram_init();
+#endif
 
   return 0;
 }
まあコメントの通りなのだけど,BSSのクリアに従来は memset() を呼んでいたが, memset() のサイズは16ビットint型として渡しているので,BSSが大きくなったときに クリアしきれないことが考えられる.これについてはmemset()側を直すべきかとも 思ったのだけど,BSSの初期化みたいな基本的な部分がライブラリ関数に左右される のもよくないかと思ったので,自前で(そしてサイズに依存しないように)クリアする ように修正した.

dram_init()によるDRAMの初期化はブートローダー側で行うので呼ばないように修正. ていうかこの段階ではすでにOSがDRAM上で動いているので,下手に設定値をいじったら いきなりDRAMの内容が消えたりしてOSが動かなくなる可能性があるので,dram_init() を呼んではいけない.

次に memory.c での動的メモリ管理の修正.

diff -u os/ether/memory.c os/ondram/memory.c
--- os/ether/memory.c	Sat Oct  3 21:10:10 2009
+++ os/ondram/memory.c	Mon Oct  5 01:15:24 2009
@@ -4,11 +4,11 @@
 
 #include "lib.h"
 
-#define MEMORY_AREA1_SIZE 32
+#define MEMORY_AREA1_SIZE (uint32)32
 #define MEMORY_AREA1_NUM  16
-#define MEMORY_AREA2_SIZE 64
-#define MEMORY_AREA2_NUM  8
-#define MEMORY_AREA3_SIZE 2048
+#define MEMORY_AREA2_SIZE (uint32)256
+#define MEMORY_AREA2_NUM  16
+#define MEMORY_AREA3_SIZE (uint32)2048
 #define MEMORY_AREA3_NUM  64
 
 #if 0
@@ -16,7 +16,8 @@
 static char memory_area_2[MEMORY_AREA2_SIZE * MEMORY_AREA2_NUM];
 static char memory_area_3[MEMORY_AREA3_SIZE * MEMORY_AREA3_NUM];
 #else
-#define memory_area_1 ((char *)DRAM_START)
+extern char freearea;
+#define memory_area_1 (&freearea)
 #define memory_area_2 (memory_area_1 + MEMORY_AREA1_SIZE * MEMORY_AREA1_NUM)
 #define memory_area_3 (memory_area_2 + MEMORY_AREA2_SIZE * MEMORY_AREA2_NUM)
 #endif
まずメモリサイズを uint32 として定義するように修正.というのは, サイズ×領域数の掛け算を行う際に,(int)×(int)になるとintの上限あふれしたときに またおかしなことになってしまうかな〜と思ったので.実際にどうなるかは未確認 なのだけど.

あと,DRAM_STARTでなくリンカスクリプトで定義した freearea を見ることで, OSのBSSの直後から無駄無くメモリを利用するように修正.こーいうのができるのが, リンカスクリプトにメモリ定義をすべて移した利点ではある.(自動調整してくれる)

最後に,これはおまけだけど,extintr の修正.

diff -u os/ether/extintr.c os/ondram/extintr.c
--- os/ether/extintr.c	Sat Oct  3 16:13:04 2009
+++ os/ondram/extintr.c	Sun Oct  4 23:55:35 2009
@@ -8,7 +8,8 @@
 
 #define USE_MESSAGE
 
-#define BUFFER_SIZE 1800
+#define CONS_BUFFER_SIZE   16
+#define ETH_BUFFER_SIZE  1800
 
 uint32 extintr_id;
 
@@ -125,9 +126,9 @@
   char *buffer;
 
 #ifdef USE_MESSAGE
-  buffer = kz_kmalloc(BUFFER_SIZE);
+  buffer = kz_kmalloc(CONS_BUFFER_SIZE);
 #else
-  buffer = kx_kmalloc(BUFFER_SIZE);
+  buffer = kx_kmalloc(CONS_BUFFER_SIZE);
 #endif
   size = 1;
 
@@ -220,9 +221,9 @@
   char *buffer;
 
 #ifdef USE_MESSAGE
-  buffer = kz_kmalloc(BUFFER_SIZE);
+  buffer = kz_kmalloc(ETH_BUFFER_SIZE);
 #else
-  buffer = kx_kmalloc(BUFFER_SIZE);
+  buffer = kx_kmalloc(ETH_BUFFER_SIZE);
 #endif
 
   size = ether_recv(intp - interrupts, buffer);
従来はシリアル入力もLANからのパケットも1800バイトの領域を獲得することで memory.c の2048バイト領域を利用していたのだけど,シリアル入力は1文字入力で あり,さすがにこれは無駄なので,シリアル入力では少ない領域が利用されるように 修正.

あと dram.c は dram_init() を呼ばなくなったのでOS側では必要は無くなったの だけど,いちおうブートローダー側の修正に合わせた修正をしておいた.

修正は以上.で,これでようやくDRAM上で実際に動作するようになった. 面倒なので今回はお試し結果は省略.各自で試してみてね. シリアルの速度はちょっと前に 19200bps に変更されているので, 接続時には注意してちょうだい.


メールは kozos(アットマーク)kozos.jp まで