タイマー
この章では,Raspberry Piに内蔵されているシステムタイマを使って正確に時間を計るプログラムを作る.
システムタイマはBCM2835 ARM Peripheralsの12章で説明されている.
Raspberry Piのシステムタイマは,64ビットのカウンタレジスタを持っている. このカウンタレジスタの値は,電源を投入すると自動的に1MHzでカウントされる(1マイクロ秒毎にカウンタの値が1ずつ増える). カウントを止めたりリセットしたりできないので,フリーランニングカウンタ (free running counter) と呼ばれている. プログラムからはカウンタレジスタの現在の値を読み出すことができる.
本章及び以降では,カウンタレジスタの値を定期的に読み出すことで時間を計り,目標時刻になれば処理を行うようなプログラムを作成する. このようにカウンタレジスタの値を定期的に読み出すことを,「カウンタレジスタの値をポーリング (polling) する」という.
(参考: 実用的なシステムでは,ポーリングよりも割り込み (interrupt) を使うことが多い. 割り込みとは,周辺装置からCPUに信号(割り込み信号)が届いたときに,実行中のプログラムを中断して割り込み処理ルーチンに制御を移す機能である. システムタイマを始め,いくつかの周辺装置は割り込みを発生させる(割り込み信号をCPUに送る)機能を持つ. システムタイマの場合は,カウンタレジスタが特定の値になった時に割り込みを発生させるよう設定できる. ポーリングだとCPUの処理時間の多くをカウンタレジスタの値の調査に費やしてしまうが,割り込みであれば,割り込みが発生するまでCPUは別の処理に専念でき,無駄が少ない. しかし,割り込み処理ルーチンの作成や登録に関して格段に多くの知識が必要になるので,本科目では簡単のため,割り込みは使わずポーリングで時間を計ることにする.)
システムタイマの制御
システムタイマはGPIO経由で接続された装置ではなく,SoCに組み込まれた装置である. そのため,専用の制御レジスタを持っている.
システムタイマの制御レジスタがマップされた番地のベースアドレス (TIMER_BASE
) は 0x3f003000 である.
BCM2835 ARM Peripherals
の172ページ12.1節にあるシステムタイマを制御するレジスタ一覧表に掲載されたAddress Offsetは,
このTIMER_BASE
の番地からのオフセットである.つまり
TIMER_BASE + (Address Offset)
が実際のレジスタの番地となる(GPFSEL0, GPSET0, GPCLR0などがGPIO_BASE
番地からのオフセットであったのと同じ).
このオフセットを足す加算演算を add
命令を使って計算することもできる(下記)が,少し冗長である.
@ 少し冗長な書き方
ldr r0, =TIMER_BASE
add r0, r0, #(オフセット)
ldr r1, [r0] @ (TIMER_BASE + オフセット)番地から読み出し
GPFSEL0等のGPIO制御レジスタにアクセスしたときのように, レジスタに定数オフセットを加えるアドレッシングモードを使ってロード/ストアする方が,簡潔で良い.
@ 簡潔な書き方
ldr r0, =TIMER_BASE
ldr r1, [r0, #(オフセット)] @ (TIMER_BASE + オフセット)番地から読み出し
システムタイマを制御するレジスタのオフセットも,GPFSEL0やGPSET0等と同じく名前付き定数としておくと良い(ソースコードが読みやすくなる.誤りを防止できる).
この実験で必要な処理は,カウンタレジスタの値を読み出すことである.どのレジスタにどのようにアクセスすれば読み出せるかを BCM2835 ARM Peripheralsを読んで調べよう.
タイマを使ったLEDの点滅
カウンタレジスタは1マイクロ秒で1増えるので,bit19は219マイクロ秒(=512×1024マイクロ秒)毎に変化する. したがって,
- カウンタレジスタの下から20ビット目(bit 19)が1であればLEDを点灯させる
- カウンタレジスタの下から20ビット目(bit 19)が0であればLEDを消灯させる
という処理を行えば,LEDはほぼ1秒周期で点滅(約0.5秒点灯,約0.5秒消灯)するはずである.そのプログラムは次のようになる. ただし,一部の処理は日本語で記述している.
(ここで定数を定義する)
.section .init
.global _start
_start:
(ここで LED に接続された GPIO のポートを出力用に設定する)
@ ループ前の準備 (r0, r1を設定する)
ldr r0, =GPIO_BASE @ r0: GPIO_BASE に固定
mov r1, #(1 << LED_PORT) @ r1: LED のポートを制御するビット
loop:
(ここで r2 にカウンタレジスタの下位 32 ビットを読み出す.ただし r0 と r1 を壊してはいけない.)
@ カウンタレジスタのbit 19に応じて点灯または消灯
tst r2, #(1 << 19) @ bit 19 をテスト
strne r1, [r0, #GPSET0] @ 1 であれば LED 点灯
streq r1, [r0, #GPCLR0] @ 0 であれば LED 消灯
b loop
演習3.3-1 chap3 ディレクトリを作り,その中に次の処理を繰り返すプログラム bit19.s を作りなさい.
- カウンタレジスタの下から20ビット目(bit 19)が1であればLEDを点灯させる
- カウンタレジスタの下から20ビット目(bit 19)が0であればLEDを消灯させる
bit19.img
を作るMakefileとREADME.mdもpushすること.
(TIMER_BASE
についての補足) 仕様書 P.172 には The Physical (hardware) base address for the system timers is 0x7E003000. とあるが,これはハードウェアが接続されたバス上の番地であって,その番地はCPU側から見ると 0x3f003000 番地に見える.
タイマを使ったLEDの点滅(任意周期)
1.5秒は1,500,000マイクロ秒である.これは2のべき乗ではない. そのため,例えば1.5秒待ちたい場合,カウンタレジスタの特定のビットによって時刻を調べる方法ではうまくいかない.
カウンタジレスタの値が,計測開始時刻の1.5秒後(以下,これを目標時刻と呼ぶことにする)の値になるまで,カウンタレジスタの値をポーリングしながら待てば,1.5秒を待つことができる. これには,まず計測開始時のカウンタレジスタの値を読み取り,それに1,500,000を加えた値(以降,これを目標値と呼ぶ)を計算する. そして,カウンタレジスタの値をポーリングしながら,その値が目標値になるまで待つ. カウンタレジスタの値が目標値以上になったら,目的の処理を行う.
周期的なタスク(例えば1.5秒ごとに同じことを実行する)の場合は,目標時刻が来てタスクを実行した後,次の目標値を計算して,またポーリングに戻る.
注意1: ポーリングとポーリングの間にかかる時間
読み出したカウンタレジスタの値と目標値の比較は,等しいかどうかではなく,目標値以上かどうかを調べないと一般にうまくいかない. 2回カウンタレジスタの値を読み出したとき,1回目は目標値より小さかったが2回目は目標値を超えている(つまり1回目と2回目の間に2マイクロ秒以上経過した)可能性があるからである.
(等しいかどうか検査するようにしていた場合,もし上記のような見逃しが起こると,64ビットカウンタが一周するまで目標値と一致しなくなる.64ビットカウンタが一周するのにどれだけ時間が掛かるか計算してみなさい.)
注意2: 誤差の蓄積
周期的なタスクを実装する場合,タスクを行った後の「次の目標時刻」は,「現在時刻 + 周期」ではなく「前回の目標時刻 + 周期」でなければならない. 「現在時刻」はすでに本来タスクを実行すべきだった時刻(前回の目標時刻)からずれているからである. 次の目標時刻を「現在時刻 + 周期」にしてしまうと,次のタスクは本来実行すべき時刻より遅れてしまう. さらにこれを繰り返すと誤差が累積してしまう.
例えば0.1秒間隔で数字をカウントアップするプログラム(1/10秒ストップウォッチ.演習3.4-4)を作ったとする. カウントアップ1回につき100マイクロ秒の誤差が発生すると,10000回実行したときには1秒の誤差になる.
次の目標時刻を「前回の目標時刻 + 周期」にすれば,目標時刻は常に「本来タスクを実行すべき時刻」を表すことになり,誤差は累積しない.
(参考: 上記のようにしても,もしタスクの処理時間が1周期よりも長ければ,1回目の処理が終わった時点で2回目の目標時刻を過ぎていることになる. この場合,すぐに2回目の処理が開始されることになるが,2回目の処理時間も1周期より長ければ,3回目はさらに本来実行すべき時刻より遅れる. このように遅れが累積すると,遅れが人間の目にもわかるようになる. ビデオゲームなどで動きが「もたつく」と感じる時は,このようなことが起こっている.)
演習3.3-2
システムタイマを使い,LEDを3秒周期で点滅(1.5秒点灯のあと1.5秒消灯)させるプログラム1500m.s を作りなさい. 上述の注意をふまえて,誤差の蓄積が起こらないようにしなさい.
1500m.img
を作るMakefileとREADME.mdもpushすること.
演習3.3-3
演習3.2-5で作ったディスプレイに渦の絵を表示するプログラムframe.sをもとにして, 0.125秒毎に表示する行を下に進める(次の行を表示する)ようにしたプログラムuzu1sec.sを作りなさい. 空ループによる時間調整ではなく,タイマをポーリングして時間を計ること. 上述の注意をふまえて,誤差の蓄積が起こらないようにしなさい. ディスプレイは8行あるので,0.125秒毎に表示する行を進めると,丁度1秒で1周するはずである. (1行ずつ表示されていることが目でもわかる速さ.誤差が蓄積しないよう作られていれば,時計と見比べたとき,表示される行が1周するのと時計が1秒進むのがぴったり同期するはず.)
uzu1sec.img
を作るMakefileとREADME.mdもpushすること.
(補足) 「次の行を表示 → 少し待つ → 次の行を表示 → …」の「少し待つ」をシステムタイマを使って実現せよ,という問題. 演習3.2-5のプログラムが以下のようになっていれば,「少し待つ」を「次の目標時刻まで待つ」に換えるだけでよい.
r = 1;
while (true) {
r行目を表示;
少し待つ; ←「次の目標時刻まで待つ」に換える
r行目を消灯;
r++;
if (r > 8) r = 1;
}
以下のようになっていると,変更箇所が多くなる.
while (true) {
1行目を表示;
少し待つ; ←「次の目標時刻まで待つ」に換える
1行目を消灯;
2行目を表示;
少し待つ; ←「次の目標時刻まで待つ」に換える
2行目を消灯;
...
}
また,次章の演習3.4-3以降を実施する際,前者のプログラム(「r行目を表示,r行目を消灯」)を基にすれば比較的容易だが,後者のプログラムを基にすると非常に複雑になり,完成させられない可能性が高い. (複雑なプログラムを誤りなく作成できる技術力があれば完成させられるだろうが,そういう人は前者のようなプログラムを書くであろう.) 従って,いずれは前者のようにプログラムしなければならないであろう.