SSブログ

beagleboard を触ろう - タイマーとコンテキストスイッチ [組み込みソフト]


OMAP3530 には、11 個の汎用タイマー (General Purpose Timer) が載っています。
タイマーデバイスとしては、他に 32kHz 同期タイマーが 1 個、ウォッチドッグタイマーが 2 個ついています。
ここでは、汎用タイマーを見てみます。

汎用タイマーは、入力クロックとして、SYS_CLK と 32K_CLK の 2 つから選択できます。
x-loader は、汎用タイマー 0 - 11 のすべてに対して、入力クロックが SYS_CLK となるように設定しています。
また、SYS_CLK の値は、13MHz に設定しています。

入力クロックが入ると、タイマーカウンターがインクリメントされます。
x-loader の場合は、13MHz = 1/13M 秒毎に、タイマーカウンターがインクリメントされることになります。
つまり、13M 回クロックが入ると、1 秒です。
タイマーカウンターの値は、TCRR レジスタから読み出すことができます。

汎用タイマーの使い方は、

1. タイマーカウンターの初期値を設定する
2. 周期タイマーか、ワンショットタイマーかの設定をする
3. 指定時間経ったら割り込みを上げるように、割り込み設定する
4. タイマーをスタートする
5. 割り込みを待つ
6. 指定時間経ったら、割り込みにより通知される
7. 周期タイマーならば、タイマーをストップしない限り、5 - 6 が繰り返される

というような感じです。


タイマーカウンターの初期値の設定方法は、2 通りあります。

a) TCRR レジスタに直接初期値を書き込む
b) TLDR レジスタに初期値を書込み、TTGR に任意の値を書き込む(こうすると TCRR に TLDR の値がコピーされる)

a) の方が簡単でいいじゃないかと思えますが、b) の方がいい時もあります。
汎用タイマーを周期タイマーとして使う場合は、タイマーが expire した時に、TLDR の値が自動的に TCRR にコピーされます。
なので、周期タイマーの場合は、b) の方が便利です。

割り込みを発生させるイベントとしては、オーバーフロー、コンペアマッチ、キャプチャーがあります。
オーバーフロー割り込みは、タイマーカウンターがオーバーフローした場合、つまり 0xFFFF_FFFF からインクリメントして 0x0000_0000 になった時に発生します。
コンペアマッチ割り込みは、タイマーカウンターの値が、TMAR レジスタの値と一致した場合に発生します。
キャプチャー割り込みは、使ったことがないので省略です (^^;

オーバーフロー割り込みを使用する場合、タイマーカウンターの初期値は、0xFFFF_FFFF - (expire するまでのカウント) + 1 にします。
例えば、1 ms タイマーにする場合、expire するまでのカウントは 13000 になるので、タイマーカウンターの初期値は 0xFFFF_CD37 にします。

コンペアマッチ割り込みを使用する場合は、タイマーカウンターの初期値を 0, TMAR レジスタに expire するまでのカウントを設定します。
1 ms タイマーなら、タイマーカウンターの初期値 0, TMAR レジスタの値を 13000 にすればよいです。
というよりは、(TMAR の値) - (タイマーカウンターの初期値) が 13000 になればよいです。


割り込みを発生させるには、TIER レジスタで、対応する割り込みイベントをイネーブルしてやる必要があります。
オーバーフロー割り込みを発生させるには、TIER のビット 1, コンペアマッチ割り込みを発生させるには TIER のビット 0 を立てる必要があります。

割り込みが発生すると、TISR レジスタで、対応する割り込みイベントのビットが立ちます。
TIER と同じように、オーバーフロー割り込みが発生した場合はビット 1, コンペアマッチ割り込みが発生した場合はビット 0 が立ちます。
割り込みハンドラーでは、割り込み要因をクリアするために、1 になったビットに 1 を書き込んでやる必要があります。


・・・


こんな感じで、タイマーを使うのは結構簡単です。

例えば、↓のようなコードで指定 micro sec 分だけビジーウェイトするような関数を作れます。
割り込みを使いつつもビジーウェイトするなんて、普通はなかなかないと思いますが・・・。

 94: static volatile int oneshot_timer_expired[NUM_TIMER];
     	    :
	    :
120: static void oneshot_timer_handler(int irq)
121: {
122:   int num = irq - 36;
123:
124:   __raw_writel(__raw_readl(gpt_tisr[num-1]), gpt_tisr[num-1]);
125:   oneshot_timer_expired[num-1] = 1;
126: }
127: 
128: void wait_us_interrupt(int num, int usec)
129: {
130:   /* disable prescalar */
131:   sr32(gpt_tclr[num-1], 5, 1, 0);
132:
133:   /* one shot timer */
134:   sr32(gpt_tclr[num-1], 1, 1, 0);
135:
136:   /* set load value */
137:   __raw_writel(0xFFFFFFFF - 13 * usec + 1, gpt_tldr[num-1]);
138:
139:   /* load TLDR */
140:   __raw_writel(1, gpt_ttgr[num-1]);
141:
142:   oneshot_timer_expired[num-1] = 0;
143:
144:   /* interrupt settings */
145:   __raw_writel(OVF_IT_BIT, gpt_tisr[num-1]);
146:   __raw_writel(OVF_IT_BIT, gpt_tier[num-1]);
147:   register_handler(oneshot_timer_handler, GPT_IRQ(num));
148:   intc_enable_irq(GPT_IRQ(num));
149:
150:   /* start timer */
151:   sr32(gpt_tclr[num-1], 0, 1, 1);
152:
153:   while (!oneshot_timer_expired[num-1])
154:     ;
155:
156:   /* stop timer */
157:   sr32(gpt_tclr[num-1], 0, 1, 0);
158: }


145 行目で TISR に OVF_IT_BIT を書き込んでいるのは、念のため割り込み要因をクリアしておくためです。


・・・



タイマー割り込みを使って、コンテキストスイッチを実現してみましょう。
まず、どんな風にしたらコンテキストスイッチができるか、考えてみます。

割り込みが発生した時、S/W によってレジスタ一式(コンテキスト)がスタックに保存されます。
元の処理に戻る場合、保存したレジスタ一式を復帰させることによって、元に戻ります。

これだと、元の処理に戻るだけですが、スタック上に保存してあるレジスタ値を書き換えてあげると、コンテキストスイッチが可能です。
保存してあるレジスタ値の中には、pc (= program counter, 命令カウンタ) も含まれています。
pc を含め、スタック上に保存してあるレジスタ値をうまく書き換えることができれば、別の処理にジャンプさせることができます。

copy_context.png

コンテキストを格納する領域をタスク数分用意しておき、タイマーハンドラーが定期的にコンテキストを交換してやれば、うまくいきそうです。


割り込み発生時には、レジスタ値を全て保存しなければなりません。

270: _do_irq:
271: #if 0
272:	sub lr,lr,#4
273:	str lr,[sp,#-4]!
274:	mrs r14,spsr
275:	stmfd sp!,{r0-r3,r12,r14}
276:	bl do_irq
277:	ldmfd sp!,{r0-r3,r12,r14}
278:	msr spsr_csxf,r14
279:	ldmfd sp!,{pc}^
280: #else
281:	sub lr,lr,#4
282:	str lr,[sp,#-4]!
283:	mrs r14,spsr
284:	stmfd sp!,{r0-r12,r14}
285:	mov r0, sp
286:	bl save_context
287:	bl do_irq
288:	bl restore_context
289:	ldmfd sp!,{r0-r12,r14}
290:	msr spsr_csxf,r14
291:	ldmfd sp!,{pc}^
292: #endif


272 行目から 279 行目までが、今まで使っていた割り込み処理コードでしたが、これを 281 行目から 291 行目までのように変更しました。
282 行目で保存している lr は、実質 pc です。
_do_irq にジャンプしてきた時点で、Cortex-A8 は、SVC モードから IRQ モードに移行しています。
IRQ モードでの lr には、SVC モードの pc の値がコピーされています。
つまり、282 行目で保存している lr には、SVC モードの pc の値が入っています。

284 行目で保存しているのは、r0 - r12 と、SPSR の値です。
IRQ モードでの SPSR には、SVC モードの CPSR の値がコピーされています。

284 行目までに、r0 - r12, CPSR, pc が保存できましたが、sp と lr だけが保存できていません。
sp と lr は、286 行目で呼び出している save_context 関数の中で保存しています。


 4: struct context {
 5: 	unsigned long r0;
 6: 	unsigned long r1;
 7: 	unsigned long r2;
 8:	unsigned long r3;
 9:	unsigned long r4;
10:	unsigned long r5;
11:	unsigned long r6;
12:	unsigned long r7;
13:	unsigned long r8;
14:	unsigned long r9;
15:	unsigned long r10;
16:	unsigned long r11;
17:	unsigned long r12;
18:	unsigned long spsr;
19:	unsigned long pc;
20:	unsigned long sp;
21:	unsigned long lr;
22: };


 3: static struct context *stack_pointer;
 4: static struct context task_context[5];
 5: static struct context *current_context = &task_context[0];
 	  :
	  :
32: void save_context(struct context *sp)
33: {
34:	unsigned long r13, r14;
35:	unsigned long mode;
36:
37:	stack_pointer = sp;
38:
39:	current_context->r0 = sp->r0;
40:	current_context->r1 = sp->r1;
41:	current_context->r2 = sp->r2;
42:	current_context->r3 = sp->r3;
43:	current_context->r4 = sp->r4;
44:	current_context->r5 = sp->r5;
45:	current_context->r6 = sp->r6;
46:	current_context->r7 = sp->r7;
47:	current_context->r8 = sp->r8;
48:	current_context->r9 = sp->r9;
49:	current_context->r10 = sp->r10;
50:	current_context->r11 = sp->r11;
51:	current_context->r12 = sp->r12;
52:	current_context->spsr = sp->spsr;
53:	current_context->pc = sp->pc;
54:	
55:	mode = sp->spsr & 0x1f;
56:	__asm__ volatile("mrs r0,cpsr\n\t"
57:			 "and r1,r0,#0x1f\n\t"
58:			 "bic r0,#0x1f\n\t"
59:			 "orr r0,r0,%2\n\t"
60:			 "msr cpsr,r0\n\t"
61:			 "mov %0,r13\n\t"
62:			 "mov %1,r14\n\t"
63:			 "bic r0,#0x1f\n\t"
64:			 "orr r0,r0,r1\n\t"
65:			 "msr cpsr,r0\n\t":"=r"(r13),"=r"(r14):"r"(mode):"r0","r1");
66:	current_context->sp = r13;
67:	current_context->lr = r14;
68: }


55 行目から 67 行目が、sp, lr を保存している箇所です。
SVC モードの sp, lr は、SVC モードに移行すれば取得できるので、それをやっています。
取得した sp, lr も含め、すべてのレジスタ値を current_context が指す struct context に保存しています。

save_context.png

逆に、割り込み処理が終わるときは、restore_context を呼び出します。

 70: void restore_context(void)
 71: {
 72:   unsigned long mode;
 73:
 74:   stack_pointer->r0 = current_context->r0;
 75:   stack_pointer->r1 = current_context->r1;
 76:   stack_pointer->r2 = current_context->r2;
 77:   stack_pointer->r3 = current_context->r3;
 78:   stack_pointer->r4 = current_context->r4;
 79:   stack_pointer->r5 = current_context->r5;
 80:   stack_pointer->r6 = current_context->r6;
 81:   stack_pointer->r7 = current_context->r7;
 82:   stack_pointer->r8 = current_context->r8;
 83:   stack_pointer->r9 = current_context->r9;
 84:   stack_pointer->r10 = current_context->r10;
 85:   stack_pointer->r11 = current_context->r11;
 86:   stack_pointer->r12 = current_context->r12;
 87:   stack_pointer->spsr = current_context->spsr;
 88:   stack_pointer->pc = current_context->pc;
 89:
 90:   mode = current_context->spsr & 0x1f;
 91:   __asm__ volatile("mrs r0,cpsr\n\t"
 92:   		 "and r1,r0,#0x1f\n\t"
 93:             	 "bic r0,#0x1f\n\t"
 94:		 "orr r0,r0,%2\n\t"
 95:		 "msr cpsr,r0\n\t"
 96:		 "mov r13,%0\n\t"
 97:		 "mov r14,%1\n\t"
 98:		 "bic r0,#0x1f\n\t"
 99:		 "orr r0,r0,r1\n\t"
100:		 "msr cpsr,r0\n\t"::"r"(current_context->sp),"r"(current_context->lr),"r"(mode):"r0","r1");
101: }


74 行目から 88 行目までの stack_pointer は、レジスタ値が保存されているスタックのアドレスを保持しています。
スタックに保存されているレジスタ値および SVC モードの sp, lr を current_context に保持されている値で上書きしています。

restore_context.png

save_context で保存対象となっている current_context の実体が、restore_context で復帰対象となっている current_context と同じならば、まったく同じ値を上書きしていることになります。
しかし、save_context と restore_context の間で、current_context を別の struct context に切り替えてやれば、restore_context で、切り替えた struct context の値がスタックに上書きされることになります。

そうすると、_do_irq の最後の方、289 行目から 291 行目でスタック上のレジスタ値を復帰させるので、struct context に設定していた値が各レジスタにコピーされることになるので、新しい処理に移る、というような流れです。

288:	bl restore_context
289:	ldmfd sp!,{r0-r12,r14}
290:	msr spsr_csxf,r14
291:	ldmfd sp!,{pc}^


コンテキストの切り替えは、タイマーハンドラーが行います。

198: static void cyclic_timer_handler(int irq)
199: {
200:   int num = irq - 36;
201:
202:   __raw_writel(OVF_IT_BIT, gpt_tisr[num-1]);
203:   serial_printf("timer_handler!\n");
204:
205:   context_switch();
206: }


context_switch 関数は、これです。

 4: static struct context task_context[5];
    	   :
22: void context_switch(void)
23: {
24: 	static int tid = 0;
25:
26:	tid++;
27:	if (tid == task_num)
28:		tid = 0;
29:	current_context = &task_context[tid];
30: }


何のことはなく、単にぐるぐる task_context を回しているだけです (^^;
まあ、ラウンドロビンですね。

context_switch でアクセスしている task_context[] には、下の関数で登録します。

 7: static int task_num = 1;
 8:
 9: void add_task(unsigned long pc, unsigned long sp)
10: {
11:	task_context[task_num].pc = pc;
12:	task_context[task_num].sp = sp;
13:	task_context[task_num].spsr = 0x153;
14:	task_num++;
15: }


それではテストプログラムです。

179: static void task_test1(void)
180: {
181:   while (1) {
182:     wait_us_interrupt(3, 1000000);
183:     serial_printf("%s\n", __FUNCTION__);
184:   }
185: }
186:
187: static void task_test2(void)
188: {
189:   while (1) {
190:     wait_us_interrupt(4, 1000000);
191:     serial_printf("%s\n", __FUNCTION__);
192:   }
193: }
194:
195: static int timer_test(void)
196: {
197:   serial_ch = 0;
198:
199:   add_task((unsigned long)task_test1, (unsigned long)0x80000400);
200:   add_task((unsigned long)task_test2, (unsigned long)0x80000800);
201:
202:   intc_enable_irq(UART3_IRQ);
203:   start_timer(1, 3000000);
204:   while (1) {
205:     if (serial_ch == 'q')
206:       break;
207:     wait_us_interrupt(2, 1000000);
208:     serial_printf("%s\n", __FUNCTION__);
209:   }
210:   stop_timer(1);
211:   intc_disable_irq(UART3_IRQ);
212:
213:   delete_all_tasks();
214:
215:   return 0;
216: }


199 行目、200 行目で task_context に登録します。
199 行目が task_context[1], 200 行目が task_context[2] への登録になります。
add_task の第 2 引数はスタックのアドレスですが、199 行目、200 行目では、SDRAM の先頭付近を指定しています。
x-loader は内臓 SRAM だけで動作するので、SDRAM はすべて空いている状態ですので、どこを使っても大丈夫です。

203 行目で、3 秒周期の周期タイマーをスタートしています。
これにより、3 秒ごとに、先ほどの cyclic_timer_handler 関数が呼び出されます。

timer_test 関数は、タイマーをスタートした後、1 秒おきに "timer_test" をシリアルコンソールに出し続けます。
そして、3 秒後にタイマー割り込みが発生します。
この時に何が起こるかというと・・・

cyclic_timer_handler の中で context_switch が呼び出され、current_context のポイント先が 199 行目で登録した task_context[1] に切り替わります。
cyclic_timer_handler の処理が終了すると、_do_irq の 288 行目が実行され、スタック上のレジスタ値および SVC モードの sp, lr を上書きしていきます。

288:	bl restore_context
289:	ldmfd sp!,{r0-r12,r14}
290:	msr spsr_csxf,r14
291:	ldmfd sp!,{pc}^


288 行目で、スタック上のレジスタ値および SVC モードの sp, lr は、↓のようになります。

switch_to_task_test1.png

pc は task_test1 のアドレスを指し、sp は 0x8000_0400 を指しています。
spsr の値 0x153 は、SVC モードを表します。

291 行目が実行された時に、SVC モードに移行し、命令ポインタ (pc) は task_test1 のアドレスを指すようになります。
つまり、task_test1 がこれから SVC モードで実行される状態になったわけです。

タイマー割り込みが発生する前は timer_test が実行されていたのに、割り込み処理が終わった後は、task_test1 に処理が移ったわけですね。
つまり、タイマー割り込みでコンテキストスイッチが発生、なわけです。

次の 3 秒後にも同じことが行われます。
次は、task_test2 の番ですね。

その次の 3 秒後は、timer_test に戻ります。
戻る場所は、前回 timer_test が中断したところです。
前回 task_test1 に実行権が移った時、task_context[0] に、その情報がすべて保存されたわけですから。

その次の 3 秒後は、task_test1 に戻ります。
戻る場所は、前回 task_test1 が中断したところです。
task_context[1] に、その情報がすべて保存されているので・・・。

という感じで続きます。

実行結果は、こんな感じです。

timer_test.png

・・・

例としては相変わらずしょうもないですが、タスクってこんなものなのか、って実感が湧きませんか?
実際の OS のコンテキストスイッチも、こんな感じです (^-^)
nice!(0)  コメント(0)  トラックバック(0) 
共通テーマ:パソコン・インターネット

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。