IchigoJam P で用いられる Raspbery Pi Pico (RP2040) には、RTC (リアルタイムクロック) が内蔵されており、現在時刻の情報を扱うことができる。
今回は、実際にこの RTC にアクセスし、時刻を読み書きしてみた。
そのため、Raspberry Pi Pico の商品ページに掲載されている RP2040 のデータシートを参照し、プログラミングを行った。
この記事に掲載しているマシン語のアセンブリコードは、改造版 asm15 でアセンブルすることを想定している。
※IchigoJamはjig.jpの登録商標です。
RTC の操作を支える技術
RTC に供給されるクロックの確認
RP2040 の RTC が時刻を進める基準となるクロックは、clk_rtc
である。
データシートの 2.15. Clocks をもとに、このクロックが IchigoJam P においてどのような設定になっているかを確認した。
10 ' IchigoJam P clk_rtc
20 POKE#700,#03,#4B,#3F,#A2
30 POKE#704,#18,#68,#10,#60
40 POKE#708,#58,#68,#50,#60
50 POKE#70C,#70,#47,#00,#00
60 POKE#710,#6C,#80,#00,#40
70 X=USR(#700,0)
80 ?"CLK_RTC_CTRL = #";
90 ?HEX$([1],4);HEX$([0],4)
100 ?"CLK_RTC_DIV = #";
110 ?HEX$([3],4);HEX$([2],4)
ORG #700
R3 = [@CLK_RTC_ADDRESS]L
R2 = @ARRAY_ADDRESS
R0 = [R3]L
[R2]L = R0
R0 = [R3 + 1]L
[R2 + 1]L = R0
RET
ALIGN 4,0,0
@CLK_RTC_ADDRESS
UDATAL #4000806C
ORG #800
@ARRAY_ADDRESS
実行結果は、以下のようになった。
CLK_RTC_CTRL = #00000800
CLK_RTC_DIV = #00040000
となっていることから、以下のことがわかる。
-
clk_rtc
には、クロックの供給が行われている (ENABLE = 1) -
clk_rtc
に供給されるクロックは、PLL_USB の出力がもとになっている (AUXSRC = 0) -
clk_rtc
に供給されるクロックの周波数は、もとのクロックの 1 / 1024 である
よって、PLL_USB の出力が 48 MHz であると仮定すると、clk_rtc
にはデフォルトで 46,875 Hz のクロックが供給されていることがわかる。
今回は、これを仮定した実装を行う。
400の倍数でない100の倍数の年の判定
RP2040 の RTC は、0~4095 年を扱うことができる。
うるう年の処理において、4の倍数の年かの判定はハードウェアで行ってくれるが、100の倍数の年かの判定は行わず、デフォルトでは4の倍数の年は必ずうるう年として処理される。
これを防ぐには、ソフトウェアで判定を行い、レジスタの対策用フラグを設定しなければならない。
ところが、RP2040 には割り算を行う命令が無く、100の倍数の年かの判定には工夫が求められる。
そこで、AI にやり方を聞いてみた。
割り算命令を使わずに、効率よく16ビットの符号なし整数を400で割った余りを求めるには?
すると、6554 を掛けて 16 ビット右シフトすることで 400 の逆数を掛けた結果の近似を求めるという方法が示された。
残念ながらこの方法は間違っており、400 ではなく 10 で割った値が得られた。
しかし、「掛け算とビットシフトで割り算を近似する」というアイデア自体は有効そうである。
そこで、いくつかのビットシフト幅について、掛けるべき値を求めた。
掛けるべき値は、(1 << シフト幅) / 400
で求めることができる。
なお、続く処理の都合で、求める近似値は真の値より少し小さいのがよい。
シフト幅 | 掛けるべき値 |
---|---|
16 | 163.84 |
15 | 81.92 |
14 | 40.96 |
13 | 20.48 |
12 | 10.24 |
11 | 5.12 |
10 | 2.56 |
9 | 1.28 |
掛ける値をこの「掛けるべき値」から小数点以下を切り捨てた (すなわち、少し小さくした) 値にすることで、真の値より少し小さい近似値を得ることができる。
この中では、シフト幅 11 のとき切り捨てる量が 0.12 と小さいので、誤差が小さくなることが期待できるとして採用した。
年の値を 400 で割った商が求まったら、それに 400 を掛けて年の値から引くことで、年の値を 400 で割った余りを求めることができる。
今回は商の近似値を用いており、これは正しい商より小さいことがあるため、求まる「余り」は 400 以上になることがある。
「余り」が 400 以上になった際は 400 を引くことで正しい余りを求めることができる。
ところで、今回は余りを求めたいのではなく、余りが 100 の倍数になるかだけわかればよい。
そのため、この判定に影響を与えなければ、400 を引く処理を省略できる。
実際に、以下のコードで試してみた。
#include <stdio.h>
int main(void) {
int i;
int wa_cnt = 0;
for (i = 0; i < 4096; i++) {
int q = (i * 5) >> 11;
int r = i - 400 * q;
int expected = i % 100 == 0 && i % 400 != 0;
int actual = r == 100 || r == 200 || r == 300;
int ac = expected == actual;
printf("%4d %4d %d %d %d\n", i, r, expected, actual, ac);
wa_cnt += !ac;
}
printf("wa_cnt = %d\n", wa_cnt);
return 0;
}
実行結果 を見ると、wa_cnt = 0
が 0 となっており、全ての入力において正しい判定ができたことがわかる。
すなわち、入力が 0~4095 の範囲のとき、400 で割ったときの商を「5 を掛けて 11 ビット右シフトする」処理で近似し、この商を用いて「余り」を求めた結果が 100, 200, 300 のいずれかであるかを判定することで、「入力が 100 の倍数であり、かつ 400 の倍数ではない」かどうかと同じ判定結果を得ることができる。
よって、この判定方法で「4 の倍数であってもうるう年でない年かどうか」を判定することができる。
ビットシフトを用いたビットマスク処理
複数の情報が格納されたワードから欲しい情報を取り出すには、ビットマスクとの AND 演算を行う方法がよく用いられる。
しかし、この方法では、まずビットマスクの値を用意する必要があり、面倒である。
たとえば、レジスタ R0
の下から 4 ビット目から 7 ビット目を取り出す場合、素直に実装すると
R1 = R0 >> 4
R2 = #F
R1 &= R2
のように、3 命令を使うことになる。
しかし、以下のようにすることで、ビットシフトを用いて欲しい情報を取り出すことができる。
- 左シフトを行い、欲しい情報より上位の余計なビットを範囲外に押し出す
- 論理右シフトを行い、欲しい情報より下位の余計なビットを範囲外に押し出す
論理右シフトの幅は、「行った左シフトの幅 + 最初の状態での欲しい情報より下位の余計なビットの数」である。
この方法を用いると、
R1 = R0 << 24
R1 = R1 >> 28
のように、2 命令で欲しい情報だけを取り出すことができ、より短いコードで実現できる。
書き込みによるビットセット・ビットクリア
RP2040 の周辺機器を制御するレジスタでは、普通の読み書きアクセスに加え、
- 本来のアドレスに
0x2000
を足したアドレスに書き込むことで、書き込んだ値が「1」のビットを「1」にし、「0」のビットは変えない (ビットセット) - 本来のアドレスに
0x3000
を足したアドレスに書き込むことで、書き込んだ値が「1」のビットを「0」にし、「0」のビットは変えない (ビットクリア)
ことができる。
(データシート 2.1.2. Atomic Register Access)
この機能を用いることで、レジスタ内の一部のビットのみを変更したい際、
- レジスタの値を読み出す
- 変更したいビットを変更する
- 変更後の値を書き込む
という手順を踏まず、変更するビットの位置を表す値を書き込むだけで変更できるので、効率よく処理を行うことができる。
RTC の操作で用いるデータ形式
RTC のレジスタ (RP2040 の仕様)
RP2040 の RTC では、32ビットのレジスタを2個用いて時刻を表現する。
2個のレジスタに、それぞれ「日付データ」および「曜日・時刻データ」を格納する。
日付データ
データ名 | 最下位ビット | ビット数 | 意味 |
---|---|---|---|
YEAR |
12 | 12 | 年 (0~4095) |
MONTH |
8 | 4 | 月 (1~12) |
DAY |
0 | 5 | 日 (1~最大31) |
日の有効な最大値は、年と月による。
曜日・時刻データ
データ名 | 最下位ビット | ビット数 | 意味 |
---|---|---|---|
DOTW |
24 | 3 | 曜日 (日:0、月:1、…、土:6) |
HOUR |
16 | 5 | 時 (0~23) |
MIN |
8 | 6 | 分 (0~59) |
SEC |
0 | 6 | 秒 (0~59) |
配列を用いた表現 (今回の独自仕様)
今回は、IchigoJam BASIC の配列の要素を以下のように日時の要素に割り当てる。
曜日は扱わない。
添字 | 日時の要素 |
---|---|
0 | 年 |
1 | 月 |
2 | 日 |
3 | 時 |
4 | 分 |
5 | 秒 |
RTC の読み出し処理は、これらの要素に結果を格納する。
逆に、RTC への書き込み処理は、これらの要素から書き込む日時を取得する。
RTC の操作を行う方法
RP2040 の RTC は、以下の手順で操作を行うことができる。
ここで、「オフセット」とは、各レジスタのアドレスから 0x4005c000
(RTC 関係のレジスタのベースアドレス) を引いた値を示す。
すなわち、各レジスタのアドレスは、「オフセット」に 0x4005c000
を足したものである。
また、ビットの位置は LSB (最下位ビット) を 0 とする。(0-origin)
RTC に時刻を書き込む
以下の手順で、RTC が管理している時刻を設定できる。
- 書き込む時刻を設定する
- 設定した時刻を書き込む指示を出す
具体的には、以下のように行う。
-
SETUP_0
レジスタ (オフセット0x04
) に日付データを書き込む -
SETUP_1
レジスタ (オフセット0x08
) に曜日・時刻データを書き込む -
CTRL
レジスタ (オフセット0x0c
) のLOAD
(ビット 4) に1
を書き込む
RTC を起動する
まず、RTC に供給しているクロック clk_rtc
の周波数の情報を設定する。
具体的には、CLKDIV_M1
レジスタ (オフセット 0x00
) に「周波数 [Hz] - 1」の値を設定する。
このレジスタは、下位 16 ビットが有効である。
今回は clk_rtc
の周波数を 46,875 Hz と仮定しているので、46874
を設定する。
これは、clk_rtc
を分周して 1 Hz にするための分周比の設定である。
本来より小さい値を設定すると、時刻が本来より速く進むようになり、実験などに役立つ可能性がある。
次に、RTC を有効にする。
具体的には、CTRL
レジスタ (オフセット 0x0c
) の RTC_ENABLE
(ビット 0) に 1
を書き込む。
RTC が動作しているかは、CTRL
レジスタ (オフセット 0x0c
) の RTC_ACTIVE
(ビット 1) で確認できる。
このビットが 1 であれば動作しており、0 であれば動作していない。
RTC から時刻を読み込む
以下の手順で、RTC が管理している時刻を取得できる。
-
RTC_0
レジスタ (オフセット0x1c
) から曜日・時刻データを読み込む -
RTC_1
レジスタ (オフセット0x18
) から日付データを読み込む
必ず RTC_1
レジスタを読み込む前に RTC_0
レジスタを読み込む。
こうすることで、RTC_0
レジスタから読み込む時に RTC_1
レジスタから読み込む値が決定され、正しい時刻を取得できるようになる。
SETUP_0
および SETUP_1
レジスタとは、 0/1 とデータの対応関係が逆である。
うるう年判定の補正を行う
以下の条件を全て満たす場合、2月28日の次の日付が2月29日になる。(うるう年)
- 年の値が 4 で割り切れる
-
FORCE_NOTLEAPYEAR
ビットが 0 である
そうでない場合、2月28日の次の日付は3月1日になる。(平年)
FORCE_NOTLEAPYEAR
ビットの判定は2月28日が終わる時に行われ、その前の任意のタイミングで設定してよい。
たとえば、十分短い間隔で時刻をポーリングしながら現在時刻の年だけを見て、それが 100 で割り切れて 400 で割り切れないなら 1、そうでないなら 0 を設定すればよい。
FORCE_NOTLEAPYEAR
ビットは、CTRL
レジスタ (オフセット 0x0c
) の 8 ビット目である。
RTC を操作するプログラムの実装
マシン語
USR
関数の第2引数が 0 のときは、RTC の状態を取得し、RTC が起動していれば時刻を配列に格納する。
0 以外のときは、配列に格納されている時刻を RTC に設定し、RTC を起動する。
RTC が起動していれば 0 を、起動していなければ 1 を返す。
配列の表現と RTC のレジスタの表現の間の変換や、うるう年判定の補正も行う。
ORG #700
' ----- コマンドを確認する -----
R0 & R0
IF !0 GOTO @SET_RTC_VALUE
' ----- 0:RTCの状態を読み取る -----
R3 = [@RTC_ADDRESS]L
' RTCが動作中かを確認する
R0 = [R3 + 3]L
R0 = R0 >> 2
IF CS GOTO @RTC_RUNNING_IN_GET
' RTCは停止しているので、1を返す
R0 = 1
RET
@RTC_RUNNING_IN_GET
' RTCの時刻を読み取る
R1 = [R3 + 7]L ' RTC_0
R2 = [R3 + 6]L ' RTC_1
R3 = @ARRAY_ADDRESS
R0 = R2 << 20
R0 = R0 >> 28
[R3 + 1]W = R0 ' MONTH
R0 = R2 << 27
R0 = R0 >> 27
[R3 + 2]W = R0 ' DAY
R0 = R1 << 11
R0 = R0 >> 27
[R3 + 3]W = R0 ' HOUR
R0 = R1 << 18
R0 = R0 >> 26
[R3 + 4]W = R0 ' MIN
R0 = R1 << 26
R0 = R0 >> 26
[R3 + 5]W = R0 ' SEC
' うるう年補正の判定に用いるため、年を最後に取得し、R0に置く
R0 = R2 << 8
R0 = R0 >> 20
[R3]W = R0 ' YEAR
GOTO @ADJUST_LEAP_YEAR
@SET_RTC_VALUE
' ----- 非0:RTCの時刻を設定し、有効化する -----
' 配列から時刻を読み取り、 RTC用の形式にする
R3 = @ARRAY_ADDRESS
R0 = [R3]W ' YEAR
R0 = R0 << 20
R1 = R0 >> 8
R0 = [R3 + 1]W ' MONTH
R0 = R0 << 28
R0 = R0 >> 20
R1 |= R0
R0 = [R3 + 2]W ' DAY
R0 = R0 << 27
R0 = R0 >> 27
R1 |= R0
R0 = [R3 + 3]W ' HOUR
R0 = R0 << 27
R2 = R0 >> 11
R0 = [R3 + 4]W ' MIN
R0 = R0 << 26
R0 = R0 >> 18
R2 |= R0
R0 = [R3 + 5]W ' SEC
R0 = R0 << 26
R0 = R0 >> 26
R2 |= R0
' 時刻をRTCのレジスタに書き込む
R3 = [@RTC_ADDRESS]L
[R3 + 1]L = R1 ' SETUP_0
[R3 + 2]L = R2 ' SETUP_1
' RTCが停止中であれば、クロックの分周比を設定する
R0 = [R3 + 3]L
R0 = R0 >> 2
IF CS GOTO @RTC_RUNNING_IN_SET
' 46874 を書き込む (分周比 = 1 / 46875)
R0 = #B7
R0 = R0 << 8
R0 += #1A
[R3]L = R0
@RTC_RUNNING_IN_SET
R3 = [@RTC_SET_ADDRESS]L
' 時刻をRTCに反映させる
R0 = `10000
[R3 + 3]L = R0
' RTCを動作させる
R0 = 1
[R3 + 3]L = R0
' うるう年補正の判定用に、年をR0に置く
R3 = @ARRAY_ADDRESS
R0 = [R3]W ' YEAR
R0 = R0 << 20
R0 = R0 >> 8
@ADJUST_LEAP_YEAR
' ----- 共通:うるう年の補正処理を行う -----
' うるう年の可能性がないと仮置きする
R3 = [@RTC_SET_ADDRESS]L
' R1 = (R0 * 5) >> 11
R1 = 5
R1 *= R0
R1 = R1 >> 11
' R1 = 400 * R1
R2 = 200
R2 = R2 << 1
R1 *= R2
R0 = R0 - R1
' 年を400で割った余りが 100, 200, 300 のいずれかかをチェックする
R0 -= 100
IF 0 GOTO @NOT_LEAP_YEAR
R0 -= 100
IF 0 GOTO @NOT_LEAP_YEAR
R0 -= 100
IF 0 GOTO @NOT_LEAP_YEAR
' うるう年の可能性がある
R3 = [@RTC_CLEAR_ADDRESS]L
@NOT_LEAP_YEAR
' 判定結果をレジスタに書き込む
R0 = 1
R0 = R0 << 8
[R3 + 3]L = R0
' 0を返す (RTCが動作中であることを示す)
R0 = 0
RET
ALIGN 4,0,0
@RTC_ADDRESS
UDATAL #4005C000
@RTC_SET_ADDRESS
UDATAL #4005E000
@RTC_CLEAR_ADDRESS
UDATAL #4005F000
ORG #800
@ARRAY_ADDRESS
BASIC
最初に、マシン語のプログラムをキャラクターパターン領域に格納する。
次に、1回状態を取得して RTC が動作しているかを判定し、動作していなければ時刻の設定と RTC の起動を行う。
その後は、時刻のポーリングと表示を行う。
変数 S
を用いて表示した秒を管理し、秒が変わっていたら表示の更新を行う。
この際、上位が 0 でない状態にして切ることで、0埋め指定桁数表示するテクニックを用いる。
10 ' IchigoJam P RTC
20 POKE#700,0,66,27,209,44,75,216,104,128,8,1,210,1,32,112,71,217,105,154,105,58,163,16,5,0,15,88,128,208,6,192,14,152,128,200,2,192,14,216,128,136,4,128,14,24,129,136,6,128,14,88,129,16,2,0,13
30 POKE#738,24,128,41,224,48,163,24,136,0,5,1,10,88,136,0,7,0,13,1,67,152,136,192,6,192,14,1,67,216,136,192,6,194,10,24,137,128,6,128,12,2,67,88,137,128,6,128,14,2,67,19,75,89,96,154,96,216,104
40 POKE#772,128,8,3,210,183,32,0,2,26,48,24,96,15,75,16,32,216,96,1,32,216,96,29,163,24,136,0,5,0,10,10,75,5,33,65,67,201,10,200,34,82,0,81,67,64,26,100,56,4,208,100,56,2,208,100,56,0,208,4,75,1,32
50 POKE#7B0,0,2,216,96,0,32,112,71,0,192,5,64,0,224,5,64,0,240,5,64
60 S=-1:IF USR(#700,0) INPUT"YEAR =",[0]:INPUT"MONTH=",[1]:INPUT"DAY =",[2]:INPUT"HOUR =",[3]:INPUT"MIN =",[4]:INPUT"SEC =",[5]:X=USR(#700,1)
70 CLS
80 X=USR(#700,0):IF [5]=S WAIT1 ELSE LOCATE6,11:?DEC$([0]+10000,4);"/";DEC$([1]+100,2);"/";DEC$([2]+100,2);" ";DEC$([3]+100,2);":";DEC$([4]+100,2);":";DEC$([5]+100,2):S=[5]
90 GOTO80
実行結果例
IchigoJam P をリセットして最初にこのプログラムを起動すると、RTC が動作していないので、設定する時刻の入力を求める。
時刻を入力すると、その時刻を RTC に設定して RTC を起動し、現在日時の表示に移る。
2 回目以降に起動すると、RTC が動作しているので、最初から現在日時の表示を行う。
おわりに
IchigoJam P (RP2040) に内蔵されている RTC の使い方を確認し、実際に時刻の設定や取得を行うことができた。
今回は、マシン語のプログラムで BASIC の容量の大部分が埋まってしまい、それを用いるプログラムとしては大した処理ができていない。
これは単純なエンコードを用いているためであり、より効率の良いエンコードを用いたり、マシン語をメモリに格納するプログラムとそれを用いた処理を行うプログラムを分けたりすることで改善できることが期待できる。
とはいえ、この RTC は IchigoJam P をリセットしたり電源を切ったりするとリセットされてしまい、次に使う際には再設定しなければならない。
そのため、バッテリーバックアップにより連続動作が可能な外部の RTC モジュールの方が使い勝手が良いかもしれない。
もしくは、MixJuice などを使用して自動で再設定を行う、という選択肢もあるだろう。
この記事に掲載したソースコードは、CC0 1.0 でライセンスする。