create every day - 福野泰介の一日一創

ハンドアセンブルで大きな文字を256倍速表示!マシン語使いこなしTIPS - IchigoJamではじめるARMマシン語その6

2016/08/15 23:55:00
#IchigoJam #ARM 

Twitterで蘇る懐かしい記憶。
実家の本棚探ってたら小学生の頃書いたZ80の機械語のノート出てきた。」 by @AoVAさん
そう、当時の中高校生が使いこなしていたZ80マシン語は小学生の憧れ、高速ゲームを夢見て、フリーズと戦った!
※フリーズ=暴走、リセットするしかない状態、マシン語を使うとよく起きる

ということで、ARMマシン語入門、その6はちょっと実用的なプログラムをTIPSと共に紹介します。

キャラクターパターンデータを使って、1文字を8x8のテキストキャラクターを使った8倍サイズにするBASICのプログラムはこんな感じ。

12 P="BIG SCRNTEST" 15 CLT:GOSUB 100 30 WAIT 120:?TICK():END 100 FOR I=0 TO 11 110 C=PEEK(S+I) 120 FOR J=0 TO 7 122 N=PEEK(C*8+J) 125 A=#900+I&3*8+(I>>2*8+J)*32 130 FOR K=0 TO 7 140 POKE A+K,N&(1<<(7-K))<>0 150 NEXT 160 NEXT 170 NEXT 180 RETURN

32x24の画面に、8x8の文字は12文字入ります。一文字ずつCで取り出すループ、文字コード*8から8byte分あるキャラクターパターンを取り出すループと、1ビットずつ表示するループの三重ループ構造でできます。描画に242tick(約4秒)もかかってしまうので、ゲームに使うのは厳しそう・・・。

このようにマシン語でいきなり作り始める前に、BASICで考え方(アルゴリズム)が間違ってないか、実際に動かして確認するといいです。(2進数のビット演算には慣れておきましょう

では、早速マシン語で高速化します!
ARMマシン語対応表(PDF版)」を片手に、BASICの変数の代わりにレジスタを使ってつくっていきます。この時、いきなり全部つくろうとせず、ループの内部の細かい部分から順に動作を確かめながらつくるのがコツです。問題は切り分ける!

1. 画面の左上にパラメータ(R0)として渡した数を表示しよう
2. 0ならCHR$(0)そうでなければCHR$(1)を表示しよう
3. 渡したパラメータの下位8bitでパターンを表示しよう(上位bitが左上になるように)
4. キャラクターコードを渡してキャラクターパターンを表示させよう
5. キャラクターコードが12コ連続してかかれたメモリ位置(=アドレス、C言語で言うポインタ)を渡して12コ表示しよう

つくりかたですが、いきなり2進数にしてしまうと読みづらいので、読みやすいプログラム(=アセンブリ言語)で先に書いて、変換(=ハンドアセンブル)するのがおすすめです。

'BIG SCREEN PROGRAM 'R0:param, address of 12 characters 'R1:virtual memory offset(RAM) 'R2:virtual memory offset(ROM) PUSH {LR,R4,R5,R6,R7} R12=R2 'save to R12 R6=0 'offset 0-11 R7=R1+R0 'real memory address of 12 characters R3=9 R3=R3<<8 R1=R1+R3 @LOOP0 R0=[R7+R6] R0=R0<<3 R2=R2+R0 R4=0 @LOOP1 R3=[R2+R4] R5=#80 @LOOP2 R0=R3 R0&=R5 IF 0 GOTO @ELSE1 R0=1 @ELSE1 [R1]=R0 R1+=1 R5=R5>>1 IF !0 GOTO @LOOP2 R1+=24 R4+=1 R4-8 IF !0 GOTO @LOOP1 R1-=24 R0=3 R0&=R6 R0-3 IF 0 GOTO @ELSE2 R1-=224 @ELSE2 R2=R12 R6+=1 R6-12 IF !0 GOTO @LOOP0 @END POP {PC,R4,R5,R6,R7}

レジスタはR0からR3とR12以外を使う場合は、呼び出し元(この場合、IchigoJamの動き)を壊してしまう恐れがあるので、PUSH/POPを使って退避しておきます。この時LR(リンクレジスタ、戻り先のアドレスが入っている)をまとめてPUSHしておき、POPするタイミングでPC(プログラムカウンタ、現在実行中のアドレス)に戻すと、RET(=#4770)の代わりになって便利です。

今回使用したレジスタは破壊してOKなR12を含めて、R7までに収まったのでPUSH/POPは一組で済みましたが、足りなくなった場合は、適宜PUSH/POPをループ前に使いましょう。その都度、レジスタの使いみちを簡単にメモしておくと便利です。


ハンドアセンブルは、IchigoJamの2進数表記とビット演算などを使って少し楽できます。

10 [0]=`1011010 1 11110000 11 [1]=#4600+`1 0010 100 12 [2]=`00100 110<<8+0 14 [3]=`0001100 000 001 111 16 [4]=`00100 011<<8+9 18 [5]=`00000 01000 011 011 20 [6]=`0001100 001 011 001 22 [7]=`0101110 110 111 000 30 [8]=`00000 00011 000 000 40 [9]=`0001100 000 010 010 60 [10]=`00100 100<<8+0 70 [11]=`0101110 100 010 011 80 [12]=`00100 101<<8+#80 90 [13]=`0100011000 011 000 100 [14]=`0100000000 101 000 102 [15]=`11010000<<8+(2-2)&#FF 104 [16]=`00100 000<<8+1 106 [17]=`01110 00000 001 000 110 [18]=`00110 001<<8+1 120 [19]=`00001 00001 101 101 130 [20]=`11010001<<8+(-7-2)&#FF 140 [21]=`00110 001<<8+24 150 [22]=`00110 100<<8+1 160 [23]=`00101 100<<8+8 170 [24]=`11010001<<8+(-13-2)&#FF 180 [25]=`00111 001<<8+24 190 [26]=`00100 000<<8+3 200 [27]=`0100000000 110 000 205 [28]=`00101 000<<8+3 210 [29]=`11010000<<8+(2-2)&#ff 220 [30]=`00111 001<<8+(224) 225 [31]=#4600+`0 1100 010 230 [32]=`00110 110<<8+1 240 [33]=`00101 110<<8+12 250 [34]=`11010001<<8+(-27-2)&#ff 260 [35]=`1011110 1 11110000 RUN A=USR(#800,"IchigoJamASM"):WAIT120

GOTOなどのジャンプ先は、命令の数を相対的に数えます。例えば2つ先なら2、2つ戻るなら-2。その数を-2した数を5桁または11桁の2進数に変換します(例、13コ前に飛ばす場合、(-13-2)&#FF)。命令を追加、変更、削除したときにアドレスを計算し直す必要があるので注意!

PUSHとPOPの数が合わない、ループから抜け出さないなど、どこか間違った場合、保護する機能はないので、高確率でフリーズします。実行前には保存しておきましょう。 フリーズしても、リセットか電源入れ直しですぐに復帰するのがIchigoJamのいいところ!(MSX時代は、暴走時の謎のディスクアクセスに怯えました)

動作が確認できたら、BASICのプログラムに簡単に使えるように、短いコードにしておくのも便利です。(LRUNでつなげてももちろんOKです)

?"1 POKE#700,";:FOR I=0 TO 31:?PEEK(#800+i);",";:NEXT:?CHR$(8) ?"2 POKE#720,";:FOR I=0 TO 36*2-1-32:?PEEK(#820+i);",";:NEXT:?CHR$(8)

アドレスをすべて相対的に使っているため配置変更可能(=リロケータブル)です。このように#800(配列)の領域から#700のPCG領域へ移しておくと、BASICで配列が使えて便利です。

BASICと速度を比べてみましょう。

1 POKE#700,240,181,148,70,0,38,15,24,9,35,27,2,89,24,184,93,192,0,18,24,0,36,19,93,128,37,24,70,40,64,0,208 2 POKE#720,1,32,8,112,1,49,109,8,247,209,24,49,1,52,8,44,241,209,24,57,3,32,48,64,3,40,0,208,224,57,98,70,1,54,12,46,227,209,240,189 10 FOR I=0 TO 12*4-1:POKE#800+I,RND(256):NEXT 12 P="BIG SCRNTEST" 15 FOR S=#800 TO #800+12*3:IF !BTN() GOSUB 100 ELSE A=USR(#700,S) 17 NEXT 20 S=P:IF !BTN() GOSUB 100 ELSE A=USR(#700,S) 22 WAIT 10 30 GOTO 15 100 FOR I=0 TO 11 110 C=PEEK(S+I) 120 FOR J=0 TO 7 122 N=PEEK(C*8+J) 125 A=#900+I&3*8+(I>>2*8+J)*32 130 FOR K=0 TO 7 140 POKE A+K,N&(1<<(7-K))<>0 150 NEXT 160 NEXT 170 NEXT 180 RETURN

ボタンでBASICとマシン語の速度差が体感できます!マシン語だと1tick(1/60秒)かかりません!
1行と2行だけ流用すると、他のプログラムでも大きな文字表示、使えます。

作るのに手間がかかるけど、動作は高速なマシン語。
作るのは簡単・高速だけど、動作は遅いBASIC。
いろいろなプログラミング言語がある理由はここにあります。

面倒なアドレス計算や表からのピックアップを自動化したものをアセンブラといいます。

また、数式など分かりやすいプログラミング言語から、マシン語の生成してくれるソフトのことをコンパイラといって、C言語はその代表。 IchigoJam BASICは、BASIC言語のコマンドに合わせて対応するマシン語を順に呼び出すインタープリタ。 Ruby、JavaScript、Swift、Javaなど最近の言語はバーチャルマシンといって仮想的なマシンのマシン語を使ったり、適宜コンパイルしたりと、両方のいいとこ取りをしています。

マシン語が分かったら、IchigoJam BASICで実際にアセンブラやコンパイラを作って、プログラミング言語の自作にも挑戦してみてください! ARMマシン語は、Android/iPhoneでも使えます。他のマシン語も似たようなものなので、一つ使えるようになっておけば怖いものなし!(実際、小学生の頃のZ80マシン語の知識が活きました)

日本産の世界に広がるプログラミング言語、Rubyに続こう!

- 連載、IchigoJamではじめる、ARMマシン語入門
1. はじめてのマシン語
2. ハンドアセンブルで超速計算!
3. マシン語メモリアクセスで画面超速表示!
4. マシン語でLEDを光らせよう!
5. 楽しさ広がるマルチバイトメモリアクセスとスタック
6. マシン語使いこなしTIPS
7. カジュアルに使うインラインマシン語

- ARMマシン語表(PDF版)

Tweet
クリエイティブ・コモンズ・ライセンス
この作品は「Creative Commons — CC BY 4.0」の下に提供されています。
CC BY 福野泰介 - Taisuke Fukuno / @taisukef / high-res profile image