65C816を使った実践的なプログラムを集めます。ここでは殆どマリオに関するプログラムではなく、一般的な65C816の記述に関する蘊蓄を述べる場所です。マリオに関するプログラムは以下の場所に書いて下さい。
xkasやTRASMなどのアセンブラによって表記が異なりますので先にそちらの方に目を通して、記載されているアセンブリがどちらで書かれているか認識できるようになる事をお勧めします。
目次
メモリ操作関連
代入
LDA 代入する値 STA 代入するメモリ1 STA 代入するメモリ2 ...
は基本です。
8bitモードと16bitモード
常に意識しておく必要があります。SMWでは8bitモードで演算されることが多いので、必要なときに16bitモードにする必要があります。
REP #$20 ; Aレジスタを16bitモードにする LDA #$1010 SEP #$20 ; Aレジスタを8bitモードにする
#$10 | X, Yレジスタ |
#$20 | Aレジスタ |
#$30 | A, X, Yレジスタ |
です。
当然、PUSH, PULLのときも8/16bitによって取得できる値が異なりますので要注意です。
REP #$20 PLA ; 2byte分PULLされます!! SEP #$20 PLA ; 1byte分PULLされます!!
メモリのクリア
STZ メモリ
レジスタのクリア
Dレジスタは基本的に$00000000なのでこれをコピーすればクリアされると考えられます。
TDC
Xレジスタだけをクリアするには
PHA TDC TAX PLA
などとすればよいでしょう。16bitモードなら
LDA #$0000
とできますが、8bitモードだと出来ません。そこで16bitモードに切り替えてしまえば
REP #$20 ; 16bitモードへ LDA #$0000 SEP #$20 ; 戻す
とクリアできます。X, Yだけを使うなら#$10を指定、A, X, Yを使うなら#$30を指定します。REPしたらちゃんとSEPに戻さないとバグの原因になります。というのも、LDA #$XXは2byte, 16bitで書き込んだ場合 LDA #$XXXXは3byteとなるからずれてしまうのです。
8bitモードでも上位と下位を交換すれば出来なくもないです :
LDA #$00 XBA LDA #$00
×2, ÷2
ASL 2倍するメモリ LSR 1/2倍するメモリ
ビットシフトとは即ち2倍、1/2倍することにほぼ等しくなります。
N bit目のデータを取得
LDA 対象メモリ LSR【N回繰り返す】 AND #$01
とするとAレジスタには$00 or $01が入っています。
分岐だけだったら、
LDA #$01 or #$02 or #$04 or #$08 or #$10 or #$20 or #$40 or #$80 BIT 対象メモリ BNE N bit目が1の場合 BEQ N bit目が0の場合
とも出来ます。
足し算・引き算
CLC ADC 値
で加算、
SEC SBC 値
で減算。というのもADCはキャリーフラグがセットされていると+1だけされて、SBCはキャリーフラグがクリアされていると更に1引いてしまうからです。
内部では符号ありの計算を行っています。40 + 50 = 90は一見普通の計算のようですが、$90は最上位ビットが1なので正の数ではなく負の数です。それでも計算自体に繰り上がりが生じていないのでキャリーフラグは0です。また、$90を正の数と解釈するようなプログラムを書いても問題はありません。
さて、50 - 40 = 10ですが、内部では符号なしで50 + C0 = 01 10と計算しています。このとき桁溢れがおきます。符号なしではキャリーフラグは桁溢れの時に立ちますから結局、50 - 40を計算するとキャリーフラグが立ちます。逆に、40 - 50だと、内部では40 + B0 = F0(= -10)と計算でき、キャリーフラグは立ちません。
掛け算、割り算
実は65C816には掛け算、割り算の命令がありません。それでも必要なのか、SMWには掛け算、割り算を計算してくれるルーチンがあります。
掛け算(8bit × 8bit)
LDA 値1 STA $4202 LDA 値2 STA $4203
とセットすると上位8bitが$4217に、下位8bitが$4216にセットされます。
掛け算(16bit × 8bit)
LDA 値1の上位8bit STA $211B LDA 値1の下位8bit STA $211A LDA 値2 STA $211C
とセットすると上位8bitから順に$2136, $2137, $2138にセットされます。
割り算(16bit ÷ 8bit)
LDA 値1の上位8bit STA $4205 LDA 値1の下位8bit STA $4204 LDA 値2 STA $4206
とセットすると、上位8bitが$4215、下位8bitが$4214、余りが$4216にセットされます。
いずれも何サイクルか要するので、うまくいかないときはセット後にNOP(2サイクル分)を入れて時間を潰して下さい。
X, Yレジスタの待避
X, Yレジスタはそれぞれブロック、スプライト等でかなり使用されます。ところがX, Yレジスタをそれ以外の用途で使いたい場合などがあるとします。そういうときはスタックに一端X, Yレジスタの値を待避させ、使い終わったら元に戻せばいいのです。
PHX Xレジスタを使った処理 PLX
PHK PLB
スプライトのメインルーチンに
PHB PHK PLB 処理 PLB
をよく見掛けます。DBレジスタ, PBレジスタをスタックに入れて、DBレジスタの値をPBレジスタにした後に処理をしてDBレジスタを元に戻しています。DBレジスタは$YYYYのアドレッシングモードでバンクを指すレジスタですから、それが次に実行するアドレスのバンクを指すPBレジスタに変わってしまうのですから特殊な処理なのでしょう。
分岐・ループ
分岐アラカルト
普通の分岐
CMP 比較対象 BEQ 移動先 処理
で等しければ移動先へ行きます。等しくなければ次の処理へ移ります。BEQなどは演算が実行された場合のフラグをチェックして分岐するだけなので、演算がされない間なら何個でも書き連ねることが出来ます。
1つの処理だけのとき
1つの条件の時だけ処理したい、というときは逆に条件の時でなければ飛ばしちゃえばいいのです。
CMP #$XX BNE _NE XXに等しいときの処理 _NE: ; 等しくなければここへジャンプ
どちらかしかない分岐
CMP 比較対象 BEQ 等しいときの移動先 BNE 等しくないとき移動先 処理?
このように書くとBEQ, BNE以外の場合がないので「処理?」は実行されないことになります。
CMP 比較対象 BCC 比較対象の方が大きいときの移動先 BCS 比較対象の方が小さいとき、または等しいときの移動先
も同様に2通り以外あり得ないのでその下の処理は実行されません。
高級言語との対比
一般的な高級言語風の条件で纏めてみました(「比較対象 [演算器号] レジスタの値」です) :
演算記号 | アセンブリでの表現 |
== | BEQ *** |
!= | BNE *** |
> | BCC *** |
<= | BCS *** |
>= |
BCC *** BEQ *** |
< |
BEQ $02 BCS *** |
最後の < はかなり無理矢理ですが、等しいときはBCSを実行しないで飛ばしちゃえばいいという発想です。
AND
if(X1 > Y1 && X2 > Y2) 処理
のような処理を考えてみます。
LDA X1 CMP Y1 BEQ $02 BCS ***
のような処理が2つあるということは、
LDA X1 CMP Y1 BEQ IF_END ; X1 == Y1 BCC IF_END ; X1 < Y1 LDA X2 CMP Y2 BEQ IF_END ; X2 == Y2 BCC IF_END ; X2 < Y2 処理 IF_END
などが考えられます。ところが条件がたくさん増えてくると今度はBEQなどで飛べなくなってしまいます。そこで条件に合わなければJMPしてしまうように書くと、
LDA #$01 LDA Y1 CMP X1 BCC $03 ; X1 > Y1なら次を飛ばす JMP IF_END LDA Y2 CMP X2 BCC $03 ; X2 > Y2なら次を飛ばす JMP IF_END 処理 IF_END:
とすると何個条件があっても大丈夫です。
今度は
if(X1 > Y1 && X2 >= Y2 && X3 == Y3 && X4 <= Y4 && X5 < Y5 && X6 != Y6)
を 考えてみます。
先ほどやったことを一般化すると、「条件に合わなかったらJMPして処理の後に飛ばす」です。条件に合わない場合、そのままJMPして、あった場合はこれを飛ばせばいいですね。JMPは3byteですからその分だけ飛ばすことになります。
LDA Y1 CMP X1 BCC $03 ; X1 > Y1 JMP IF_END LDA Y2 CMP X2 BCC $05 ; X2 > Y2 * 04なのはSTZを飛ばすため BEQ $03 ; X2 == Y2 JMP IF_END LDA Y3 CMP X3 BEQ $03 ; X3 == Y3 JMP IF_END LDA Y4 CMP X4 BCS $03 ; X4 <= Y4 JMP IF_END LDA Y5 CMP X5 BEQ $02 ; X5 == Y5 (のときは次の命令を飛ばす) BCS $03 ; X5 <= Y5 JMP IF_END LDA Y6 CMP X6 BNE $03 ; X6 != Y6 JMP IF_END 処理 JMP IF_END:
OR
if(X1 > Y1 || X2 >= Y2 || X3 == Y3 || X4 <= Y4 || X5 < Y5 || X6 != Y6)
ANDと同じように考えてみますと、今度は逆に条件にあったら処理に飛ばしてあげればいいですね。
LDA Y1 CMP X1 BCC $02 ; X1 > Y1 JMP IF_PROCESSING LDA Y2 CMP X2 BCC $05 ; X2 > Y2 * 04なのはSTZを飛ばすため BEQ $03 ; X2 == Y2 JMP IF_PROCESSING LDA Y3 CMP X3 BEQ $03 ; X3 == Y3 JMP IF_PROCESSING LDA Y4 CMP X4 BCS $03 ; X4 <= Y4 JMP IF_PROCESSING LDA Y5 CMP X5 BEQ $02 ; X5 == Y5 (のときは次の命令を飛ばす) BCS $03 ; X5 <= Y5 JMP IF_PROCESSING LDA Y6 CMP X6 BNE $03 ; X6 != Y6 JMP IF_PROCESSING JMP ID_END JMP IF_PROCESSING: 処理 IF_END:
ド・モルガンの定理よりORをANDに変えることが出来ます。これによって
if(X1 <= Y1 && X2 < Y2 && X3 != Y3 && X4 > Y4 && X5 >= Y5 && X6 == Y6)
となり、ANDの手法でもいいことになります。
N回ループする
LDX #$00 LOOP: 処理 INX CPX 回数 BCC LOOP
Xレジスタをカウンタとして使ったので、必要な場合はPHX ~ PLXの中に書きます。
例えばAレジスタを1から9まで足すには
LDA #$00 LDX #$01 ; 初期X LOOP: PHA TXA STA $00 PLA CLC ADC $00 INX CPX #$0A BCC LOOP
などが考えられます。$00はDレジスタが殆どの場合$0000を表していますから$00:0000を指しています。SMWでは$00 ~ $0Fは汎用RAMとして使え、カウンターなどには適しています。ですから今回の場合は別にXレジスタを使う必要はなく、
LDA #$01 STA $00 LDA #$00 LOOP: ADC $00 PHA INC $00 LDA $00 CMP #$0A PLA BCC LOOP
として良かったわけですね。ただ、比較の時にPHA ~ PLAを書かなければいけない点も考慮すると大して差はないかと……。逆に、Aレジスタをカウンターとして用い、$00に結果を代入するように書くと、
LDA #$00 STA $00 LDA #$01 LOOP: PHA ADC $00 STA $00 PLA INC CMP #$0A BCC LOOP
こちらの方が自然?
1回目で抜ける可能性があるループ
LDX #$00 LOOP: CPX 回数 BCS LOOP_END 処理 INX JMP LOOP LOOP_END:
回数を0にすると処理を実行せずにLOOP_ENDへ行きます。ラベルを2つも使うので書くのが面倒になりますが; ただ、利点としては回数以外の条件でもLOOP_ENDへジャンプすれば抜けることが出来ます。
LDX #$00 LOOP: CPX 回数 BCS LOOP_END 処理 BEQ LOOP_END ; ループ脱出 処理 BCC LOOP_END; ループ脱出 処理…… INX LOOP_END:
処理が長い場合のループ
BEQ系統は符号付き1バイトしか移動できないのでループ内に処理がたくさんあると移動できなくなってしまいます。こういうときはJMPまたはJMLを使えばいいのです。(JMPは4バイト、JMLは6バイトですが殆どJMPで十分でしょう。)
初めのループを書き換える
LDX #$00 LOOP: 処理 INX CPX 回数 BCS $03 JMP LOOP
INXでXレジスタを+1し、CPXで回数と比較してXレジスタの方が大きければキャリーフラグが立つのでBCSでジャンプ。$03というのはBCSの次のアドレスから$03番目と言うこと。JMPは1byte, LOOPは$YYYY型で2byteだから$03が指すのはJMPの次の指令です。
先ほどのアセンブリはBCCでLOOPへジャンプしていましたが、今回はBCSで条件になったら「LOOPへジャンプさせない」形で書きました。BCCはキャリーフラグが0になったらジャンプするもので、BCSは1になったらジャンプします。Xレジスタの値 - 回数が負ならキャリーフラグは0でそれ以外の場合は1であることを考慮するとBCSで比較すると回数以上になったときにジャンプすると考えられます。
最初に判断するバージョン
LDX #$00 LOOP: CPX 回数 BCC $03 JMP LOOP_END 処理 INX JMP LOOP LOOP_END:
こちらも上と同じ理由でBCCになっています。レジスタ - 回数が負ならJMPを飛ばして処理へ、0以上になったらJMPへ、ということです。
サブルーチン
JSR ルーチン JSL ルーチン
でルーチンへ移動し、
RTS RTL
でルーチンから戻って元の命令の次から実行しますが、その戻り先の記録はスタックにされています。即ち、JSRでジャンプしたらそのアドレス(*1)がスタックにPUSHされ、RTSが実行されたらスタックからPULLし、その値 + 1(*2)へジャンプします。
ということはスタックを弄るとおかしな事になってしまいます。逆にこれを利用するアセンブリの書き方もあります。
PUSHしてジャンプ先を指定
JSR MAIN RTS MAIN: LDA #$10 PHA ; [1] REP #$20 LDA #TEST-1 PHA ; [2] SEP #$20 RTS ; [3] TEST: ; [4] PLA ; [5] STA $0F48 ; #$10が代入される! RTS ; [6]
まず、[1]でAレジスタをPULL。#$10が入ります。次に今回は8bitモードでの環境を想定していますからREP #$20で16bitモードに一時切り替えをしています。そしてTESTラベルのアドレス - 1を[2]でPUSHします。ここで-1したのは、後の[3]でRTSするとき+1してしまうからです。[4] RTSするとPULLしてTEST - 1 + 1 = TESTのアドレスにジャンプします。[5]では更にPULLして#$10を引き出します。最後にもう一度RTSするとMAINルーチンから戻ることが出来ます。
ここでTEST自体はJSRでジャンプしていませんが、実質ルーチンと同じ処理が出来ました。
ジャンプテーブル
xkasなどで直接バイナリ書き込みが出来ます。
ROUTINE_LIST dw ROUTINE0, ROUTINE1, ROUTINE2, ROUTINE3, ...
このようにROUTINEn(アドレス群)が連続しているとします。
LDX #$02 JMP ROUTINE_LIST,x ROUTINE0: 処理 ROUTINE1: 処理 ...
とするとROUTINE_LISTのアドレスからX番目を参照して、その場所へジャンプすることが出来ます。但し、ROUTINE_LISTとROUTINEnのバンクが同じでなければなりません。RTLを使ってdlすればいいかもしれません。
引数を受け取る
PHA LDA 引数 JMP func PLA 処理 func: Aレジスタが引数 RTS
のようにレジスタを使えば引数として渡せます。
Sレジスタを弄って引数
LDA #$10 PHA ; [1] JSR MAIN PLA ; [8] RTS MAIN: REP #$20 ; [2] TSC ; [3] INC ; [4] INC TCS ; [5] PLX ; [6] STX $0F48 ; #$10を受ける TSC ; [7] DEC DEC DEC TCS SEP #$20 RTS
まず[1]でPUSHして引数を渡します。次にJSRで移動します。[2]ではAレジスタが8bitモードの時、16bitモードで受けるためのフラグ捜査をしています。[3]ではSレジスタをAレジスタにコピーしています。この時点でアドレスの小さい順に「アドレス上位8bit, アドレス下位8bit, #$10」が入っています。そこで、Sレジスタの値を2足せば#$10の位置へ移動できますので[4]では2回INCして、[5]でSレジスタに書き込みます。続いて[6]でPLXすることにより「#$10」をAレジスタにコピーできます。今度はSレジスタを元に戻してジャンプできるようにさせる必要がありますから2回INCし、1回PULLすることでSがINCされましたから3回DECしTCSします。そうするとRTSで2byte分アドレスがPULLされますから初めの位置より1だけSレジスタが小さいので最後の[8]で適当にPULLして元に戻します。
実用性はないかも知れませんがSレジスタ操作の練習にはなったかも知れません。
Sレジスタを弄らないで引数
PHX LDA #$10 PHA JSR MAIN PLX RTS MAIN: PLA XBA PLA PLX STX $0F48 ; が入る#$10 PHA XBA PHA RTS
MAIN内では
- PLAでアドレス上位8bitを取得し、8bitモードだから下位8bitに代入される
- XBAで上位8bitと下位8bitを交換
- PLAでアドレス下位8bitを取得し、Aレジスタは上位、下位合わせたアドレスが入っている。 という処理を最初に行い、PLXなどで引数を取得した後、PHA, XBA, PHAで初めのアドレスを取得し、ジャンプしています。内部でXレジスタを使ったので外部ではPHX ~ PLXで保護しています。
16bitモードならこんな面倒な事しなくていいんですけどね。
戻り値を指定
func: LDA 戻り値 RTS
とレジスタを使う方法もありますが、2値だけの場合例えばYES, NOの場合はキャリーフラグを使ったりしても良いでしょう。
func: .YES CLS; YES RTS .NO CLC; NO RTS