IchigoJam P (RP2040) 内蔵のRTCを使ってみる

[IchigoJam](https://ichigojam.net/) P で用いられる Raspbery Pi Pico (RP2040) には、RTC (リアルタイムクロック) が内蔵されており、現在時刻の情報を扱うことができる。

今回は、実際にこの RTC にアクセスし、時刻を読み書きしてみた。
そのため、[Raspberry Pi Pico の商品ページ](https://www.raspberrypi.com/products/raspberry-pi-pico/#specification)に掲載されている RP2040 のデータシートを参照し、プログラミングを行った。

この記事に掲載しているマシン語のアセンブリコードは、[改造版 asm15](https://mikecat.github.io/asm15/) でアセンブルすることを想定している。

※IchigoJamはjig.jpの登録商標です。

# RTC の操作を支える技術

## RTC に供給されるクロックの確認

RP2040 の RTC が時刻を進める基準となるクロックは、`clk_rtc` である。
データシートの 2.15. Clocks をもとに、このクロックが IchigoJam P においてどのような設定になっているかを確認した。

```text:clk_rtc の設定を読み取るプログラム
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)
```

```text:clk_rtc の設定を読み取るプログラム (マシン語部分)
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
```

実行結果は、以下のようになった。

![実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/1faf4575-9974-4d97-a52f-5528d05b049b.png)

```text
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で割った余りを求めるには？](https://www.perplexity.ai/search/ge-risuan-ming-ling-woshi-wasu-D5LUqq92RnGCrI3WdKPx6w)

すると、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 を引く処理を省略できる。
実際に、以下のコードで試してみた。

```c
#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;
}
```

[実行結果](https://wandbox.org/permlink/h8ECQ3S4fF2ymBxX) を見ると、`wa_cnt = 0` が 0 となっており、全ての入力において正しい判定ができたことがわかる。
すなわち、入力が 0～4095 の範囲のとき、400 で割ったときの商を「5 を掛けて 11 ビット右シフトする」処理で近似し、この商を用いて「余り」を求めた結果が 100, 200, 300 のいずれかであるかを判定することで、「入力が 100 の倍数であり、かつ 400 の倍数ではない」かどうかと同じ判定結果を得ることができる。
よって、この判定方法で「4 の倍数であってもうるう年でない年かどうか」を判定することができる。

## ビットシフトを用いたビットマスク処理

複数の情報が格納されたワードから欲しい情報を取り出すには、ビットマスクとの AND 演算を行う方法がよく用いられる。
しかし、この方法では、まずビットマスクの値を用意する必要があり、面倒である。
たとえば、レジスタ `R0` の下から 4 ビット目から 7 ビット目を取り出す場合、素直に実装すると

```text:ビットマスクにより情報を取り出す例
R1 = R0 >> 4
R2 = #F
R1 &= R2
```

のように、3 命令を使うことになる。

しかし、以下のようにすることで、ビットシフトを用いて欲しい情報を取り出すことができる。

1. 左シフトを行い、欲しい情報より上位の余計なビットを範囲外に押し出す
2. 論理右シフトを行い、欲しい情報より下位の余計なビットを範囲外に押し出す

論理右シフトの幅は、「行った左シフトの幅 ＋ 最初の状態での欲しい情報より下位の余計なビットの数」である。
この方法を用いると、

```text:ビットシフトにより情報を取り出す例
R1 = R0 << 24
R1 = R1 >> 28
```

のように、2 命令で欲しい情報だけを取り出すことができ、より短いコードで実現できる。

## 書き込みによるビットセット・ビットクリア

RP2040 の周辺機器を制御するレジスタでは、普通の読み書きアクセスに加え、

* 本来のアドレスに `0x2000` を足したアドレスに書き込むことで、書き込んだ値が「1」のビットを「1」にし、「0」のビットは変えない (ビットセット)
* 本来のアドレスに `0x3000` を足したアドレスに書き込むことで、書き込んだ値が「1」のビットを「0」にし、「0」のビットは変えない (ビットクリア)

ことができる。
(データシート 2.1.2. Atomic Register Access)

この機能を用いることで、レジスタ内の一部のビットのみを変更したい際、

1. レジスタの値を読み出す
2. 変更したいビットを変更する
3. 変更後の値を書き込む

という手順を踏まず、変更するビットの位置を表す値を書き込むだけで変更できるので、効率よく処理を行うことができる。

# RTC の操作で用いるデータ形式

## RTC のレジスタ (RP2040 の仕様)

RP2040 の RTC では、32ビットのレジスタを2個用いて時刻を表現する。
2個のレジスタに、それぞれ「日付データ」および「曜日・時刻データ」を格納する。

### 日付データ

![RP2040 の日付レジスタ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/3abdf298-5dfa-437c-87d6-556e2b7e8f44.png)

|データ名|最下位ビット|ビット数|意味|
|:-:|:-:|:-:|---|
|`YEAR`|12|12|年 (0～4095)|
|`MONTH`|8|4|月 (1～12)|
|`DAY`|0|5|日 (1～最大31)|

日の有効な最大値は、年と月による。

### 曜日・時刻データ

![RP2040 の曜日・時刻レジスタ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/75142baa-cc4f-4dfa-9d45-c1020c5845bc.png)

|データ名|最下位ビット|ビット数|意味|
|:-:|:-:|:-:|---|
|`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 が管理している時刻を設定できる。

1. 書き込む時刻を設定する
2. 設定した時刻を書き込む指示を出す

具体的には、以下のように行う。

1. `SETUP_0` レジスタ (オフセット `0x04`) に日付データを書き込む
2. `SETUP_1` レジスタ (オフセット `0x08`) に曜日・時刻データを書き込む
3. `CTRL` レジスタ (オフセット `0x0c`) の `LOAD` (ビット 4) に `1` を書き込む

## RTC を起動する

まず、RTC に供給しているクロック `clk_rtc` の周波数の情報を設定する。
具体的には、`CLKDIV_M1` レジスタ (オフセット `0x00`) に「周波数 [Hz] － 1」の値を設定する。
このレジスタは、下位 16 ビットが有効である。

今回は `clk_rtc` の周波数を 46,875 Hz と仮定しているので、`46874` を設定する。

:::note info
これは、`clk_rtc` を分周して 1 Hz にするための分周比の設定である。
本来より小さい値を設定すると、時刻が本来より速く進むようになり、実験などに役立つ可能性がある。
:::

次に、RTC を有効にする。
具体的には、`CTRL` レジスタ (オフセット `0x0c`) の `RTC_ENABLE` (ビット 0) に `1` を書き込む。

RTC が動作しているかは、`CTRL` レジスタ (オフセット `0x0c`) の `RTC_ACTIVE` (ビット 1) で確認できる。
このビットが 1 であれば動作しており、0 であれば動作していない。

## RTC から時刻を読み込む

以下の手順で、RTC が管理している時刻を取得できる。

1. `RTC_0` レジスタ (オフセット `0x1c`) から曜日・時刻データを読み込む
2. `RTC_1` レジスタ (オフセット `0x18`) から日付データを読み込む

:::note info
必ず `RTC_1` レジスタを読み込む**前に** `RTC_0` レジスタを読み込む。
こうすることで、`RTC_0` レジスタから読み込む時に `RTC_1` レジスタから読み込む値が決定され、正しい時刻を取得できるようになる。
:::

:::note warn
`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 のレジスタの表現の間の変換や、うるう年判定の補正も行う。

```text: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埋め指定桁数表示するテクニック](https://qiita.com/mikecat_mixc/items/fbbbedeb7b7262ea52a8)を用いる。

```text:RTC を操作するプログラム (BASIC)
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 が動作しているので、最初から現在日時の表示を行う。

![時刻の表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/f84ab0f2-7c5e-4556-9bd5-62b4276f65c2.png)

# おわりに

IchigoJam P (RP2040) に内蔵されている RTC の使い方を確認し、実際に時刻の設定や取得を行うことができた。

今回は、マシン語のプログラムで BASIC の容量の大部分が埋まってしまい、それを用いるプログラムとしては大した処理ができていない。
これは単純なエンコードを用いているためであり、より効率の良いエンコードを用いたり、マシン語をメモリに格納するプログラムとそれを用いた処理を行うプログラムを分けたりすることで改善できることが期待できる。

とはいえ、この RTC は IchigoJam P をリセットしたり電源を切ったりするとリセットされてしまい、次に使う際には再設定しなければならない。
そのため、バッテリーバックアップにより連続動作が可能な外部の RTC モジュールの方が使い勝手が良いかもしれない。
もしくは、[MixJuice](http://mixjuice.shizentai.jp/) などを使用して自動で再設定を行う、という選択肢もあるだろう。

この記事に掲載したソースコードは、[CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/deed.ja) でライセンスする。