cocoaの隠れ処

更新ページ

取得中です。

人気ページ

TOTAL : -

応用65C816


※上記の広告は60日以上更新のないWIKIに表示されています。更新することで広告が下部へ移動します。

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内では

  1. PLAでアドレス上位8bitを取得し、8bitモードだから下位8bitに代入される
  2. XBAで上位8bitと下位8bitを交換
  3. 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

割り込み処理

実験