2021-09-05 23:11 — asano
カテゴリー:
前回書いたようにCOSMACにはサブルーチン呼び出しの仕組みがありません。
それではどうするかというとSEP
命令を使います。
COSMACはメモリをポイントできるレジスタを16個 R(0)
~R(F)
と、4ビットのレジスタP
を持っています。R(0)
~R(F)
の内P
で選ばれたR(P)
がプログラムカウンタとして使用されます。
SEP
命令でP
に新たな値を設定することでサブルーチンを呼び出し、P
に元の値を設定することで戻ることができます。
4/ 0 : MAIN:
5/ 0 : ;; Main routine (part 1)
6/ 0 : F8 00 LDI (SUB >> 8)
7/ 2 : B4 PHI 4
8/ 3 : F8 07 LDI (SUB & 0FFH)
9/ 5 : A4 PLO 4
10/ 6 : D4 SEP 4 ; CALL
11/ 7 : ;; Main routine (part 2)
12/ 7 :
13/ 7 : SUB:
14/ 7 : ;; Sub routine
15/ 7 : D3 SEP 3 ; RETURN
メインルーチンはP
=3の状態で実行されていたとします。
6~9行目で呼びたいサブルーチンのアドレスをR(4)
に入れます。
10行目を実行するとそのR(4)
が新たなプログラムカウンタになるので13行目からのサブルーチンに実行が移ります。R(3)
にはSEP
命令の次のアドレスが入ったままとなり、これはもうプログラムカウンタではないのでそのまま更新されなくなります。
サブルーチンの実行が終わって15行目でSEP
命令を実行すると再びR(3)
がプログラムカウンタになるのでメインルーチンの続きを実行することになります。
この方式は簡単ですが、欠点もあります。
- 呼び出しの段数が増えると
R()
レジスタを消費してしまう - サブルーチンは呼び出し元の
P
を知る必要がある - 再帰呼び出しができない
Universal Monitor程度だと1.,3.は問題ありません(F8へUniversal Monitorを移植(その1)参照)が、2.は問題です。
例えばコンソール出力(CONOUT
)はメインルーチンやSTROUT
などいくつかの段から呼ばれる可能性があります。サブルーチンで元のP
の値を知るすべはありませんから、何段目かを固定し場合によってはダミーのルーチンを挟むなどの対策が必要になります。
最初はSC/MPのようにマクロを書いて対処しようかとも思ったのですが...
結構大掛かりになりそうだったのと、RCA標準の方法に合わせたほうが良さそう、とのことから "Standard Call and Return Technique" と呼ばれているものを使うことにしました。
これはプログラムのどこかに以下の3つのルーチンを置きます。
19/ 0 : ;; SCRT initialization
20/ 0 : F8 00 LDI high(CSTART)
21/ 2 : B3 PHI PC
22/ 3 : F8 37 LDI low(CSTART)
23/ 5 : A3 PLO PC
24/ 6 :
25/ 6 : F8 00 LDI high(CALLR)
26/ 8 : B4 PHI CALL
27/ 9 : F8 1B LDI low(CALLR)
28/ B : A4 PLO CALL
29/ C : F8 00 LDI high(RETR)
30/ E : B5 PHI RETN
31/ F : F8 2B LDI low(RETR)
32/ 11 : A5 PLO RETN
33/ 12 : F8 0F LDI high(STACK)
34/ 14 : B2 PHI SP
35/ 15 : F8 EF LDI low(STACK)
36/ 17 : A2 PLO SP
37/ 18 : E2 SEX SP
38/ 19 : D3 SEP PC ; Jump to CSTART
39/ 1A :
40/ 1A : ;; SCRT: Standard CALL
41/ 1A : EXITC:
42/ 1A : D3 SEP PC
43/ 1B : CALLR:
44/ 1B : E2 SEX SP
45/ 1C : 96 GHI LINK ; Save current LINK to STACK
46/ 1D : 73 STXD
47/ 1E : 86 GLO LINK
48/ 1F : 73 STXD
49/ 20 : 93 GHI PC ; Copy PC to LINK
50/ 21 : B6 PHI LINK
51/ 22 : 83 GLO PC
52/ 23 : A6 PLO LINK
53/ 24 : 46 LDA LINK ; PC.h = (LINK)+
54/ 25 : B3 PHI PC
55/ 26 : 46 LDA LINK ; PC.l = (LINK)+
56/ 27 : A3 PLO PC
57/ 28 : 30 1A BR EXITC
58/ 2A :
59/ 2A : ;; SCRT: Standard RETuRn
60/ 2A : EXITR:
61/ 2A : D3 SEP PC ; Return to Main Program
62/ 2B : RETR:
63/ 2B : 96 GHI LINK ; Recover calling program return address
64/ 2C : B3 PHI PC
65/ 2D : 86 GLO LINK
66/ 2E : A3 PLO PC
67/ 2F : E2 SEX SP
68/ 30 : 12 INC SP
69/ 31 : 72 LDXA ; Restore the contents of LINK
70/ 32 : A6 PLO LINK
71/ 33 : F0 LDX
72/ 34 : B6 PHI LINK
73/ 35 : 30 2A BR EXITR
一つ目の19~38行は初期化ルーチンです。COSMACはリセット後はP
=0で走り始めますが、ここでR(2)
~R(5)
を初期化し、以後は原則としてP
=3の状態で実行されます。以後、以下のレジスタは壊してはいけません。
R(2)
=SP
:スタックポインタR(3)
=PC
:プログラムカウンタR(4)
=CALL
:常にCALLR
ルーチンのアドレスを入れておくR(5)
=RETN
:常にRETR
ルーチンのアドレスを入れておくR(6)
=SP
:実行中のサブルーチンからの戻りアドレス
サブルーチンを呼び出したいときは以下のようにします。
77/ 37 : D4 SEP CALL
78/ 38 : 07 00 DW INIT
これで二つ目の43行目からが(R(4)
にアドレスが入っている)実行されます。
44~48行でR(6)
に入っている戻りアドレスをスタックに退避します。
49~52行でR(3)
に入っているSEP CALL
の次のアドレスをR(6)
にコピーします。
53~56行ではR(6)
(DW INIT
のアドレス)から呼び出し先のアドレス(この場合はINIT
)を読み込んでR(3)
に入れます。
57行目から42行に行ってR(3)
に入っている呼び出し先へ飛びます。ここでなぜ57行目にSEP PC
を書かないかというと、SEP
実行後にR(4)
をまた元の値に戻して次のSEP CALL
の準備をさせるためです。
サブルーチンから戻りたいときは以下のようにします。
1088/ 5D9 : D5 SEP RETN
上のSEP CALL
がわかればこれも簡単でしょう。
スタックを使っているのでメモリの許す限り多段呼び出しできますし、再帰呼び出しも問題ありません。またこのCALLR
とRETR
を実行している時以外は常にP
=3なので何段目に呼ばれても大丈夫です。
コメントを追加