「応用65C816」(2012/03/02 (金) 18:38:44) の最新版変更点
追加された行は緑色になります。
削除された行は赤色になります。
[[65C816>改造マリオ/65C816]]を使った実践的なプログラムを集めます。ここでは殆どマリオに関するプログラムではなく、一般的な65C816の記述に関する蘊蓄を述べる場所です。マリオに関するプログラムは、以下の場所に書いて下さい。
- [[改造マリオ/応用65C816/SMW全般]]
- [[改造マリオ/応用65C816/カスタムブロック]]
- [[改造マリオ/応用65C816/カスタムスプライト]]
- [[改造マリオ/応用65C816/Layer2]]
- [[改造マリオ/応用65C816/Layer3]]
'''xkasやTRASMなどのアセンブラによって表記が異なりますので先にそちらの方に目を通して、記載されているアセンブリがどちらで書かれているか認識できるようになる事をお勧めします。'''
* 目次
#contents
* メモリ操作関連
** 代入
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通り以外あり得ないのでその下の処理は実行されません。
*** 高級言語との対比
一般的な高級言語風の条件で纏めてみました(「比較対象 [演算器号] レジスタの値」です) :
| 演算記号 | アセンブリでの表現 |h
| == | BEQ *** |
| != | BNE *** |
| > | BCC *** |
| <= | BCS *** |
| >= | BCC ***&br;BEQ *** |
| < | BEQ $02&br;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でJSRの次の命令を指します。))へジャンプします。
ということはスタックを弄るとおかしな事になってしまいます。逆にこれを利用するアセンブリの書き方もあります。
*** 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
* 割り込み処理
* 実験
[[65C816>改造マリオ/65C816]]を使った実践的なプログラムを集めます。ここでは殆どマリオに関するプログラムではなく、一般的な65C816の記述に関する蘊蓄を述べる場所です。マリオに関するプログラムは以下の場所に書いて下さい。
- [[応用65C816/SMW全般]]
- [[応用65C816/カスタムブロック]]
- [[応用65C816/カスタムスプライト]]
- [[応用65C816/Layer2]]
- [[応用65C816/Layer3]]
'''xkasやTRASMなどのアセンブラによって表記が異なりますので先にそちらの方に目を通して、記載されているアセンブリがどちらで書かれているか認識できるようになる事をお勧めします。'''
* 目次
#contents
* メモリ操作関連
** 代入
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通り以外あり得ないのでその下の処理は実行されません。
*** 高級言語との対比
一般的な高級言語風の条件で纏めてみました(「比較対象 [演算器号] レジスタの値」です) :
| 演算記号 | アセンブリでの表現 |h
| == | BEQ *** |
| != | BNE *** |
| > | BCC *** |
| <= | BCS *** |
| >= | BCC ***&br;BEQ *** |
| < | BEQ $02&br;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でJSRの次の命令を指します。))へジャンプします。
ということはスタックを弄るとおかしな事になってしまいます。逆にこれを利用するアセンブリの書き方もあります。
*** 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
* 割り込み処理
* 実験
表示オプション
横に並べて表示:
変化行の前後のみ表示: