sanguisorba が 2021年12月06日00時05分10秒 に編集
初版
タイトルの変更
アセンブラでLチカ (H8/3048, MCS-48)
タグの変更
H8
MCS-48
アセンブラ
本文の変更
本記事は慶應理工アドベントカレンダー2021の6日目の記事です https://adventar.org/calendars/6279 Lチカの記事は主にプログラムの転送の仕方や動作環境を構築するという部分が肝だと思っていてアセンブラコードのほうには深く触れてこなかったのですが、今回はアセンブラコードのほうについて、どんな感じの動作が記述されているのか解説します。 ## アセンブラは機種依存が激しい アセンブラでは一つ一つのコードをニーモニックと呼び、ニーモニックはROMに書き込まれるバイナリと1対1で対応しています。コンパイラ無しでアセンブラを直接バイナリに変換する事も可能で、ハンドアセンブルなんて呼ばれたりします。 アセンブラはCPU毎の機種依存が激しく、Arduino-Cのようにdigitalwriteを書けばLEDが光るんでしょ みたいな考え方は捨てるべきです。CPUのデータシートを深く読み、そのCPUがどのような構造をしているのかをしっかり理解することが重要です。 今回は過去に紹介したH8/3048とMCS-48でアセンブラを使ったLチカの書き方を紹介します。 普段からプログラミングをやっている人は、以下の単語を知っておけばこの記事はよめると思います * レジスタ - 1つの値しか記憶できないメモリ。レジスタの数によってプログラムの書きやすさが変わってくる。 * MOV - レジスタの値を別のレジスタへ**転送**したり、RAMへ**転送**したり、ROMやRAMの値をレジスタへ**転送**したり、要は値をコピペする処理 ## H8 / 3048の場合 環境構築についてはこの記事をご覧ください https://elchika.com/article/1ab5a4a9-b14d-476f-b835-b1a01120d7b8/ アセンブラ — * RENESAS H8ファミリ用C/C++コンパイラ Ver. 6.02 Release 02 https://www.renesas.com/jp/ja/software-tool/cc-compiler-package-h8sx-h8s-h8-family ※秋月キット付属のVer 1.0の評価版アセンブラでも同じバイナリデータを吐きます 逆アセンブラ — * DASMH83 https://www.vector.co.jp/soft/dos/prog/se084906.html アセンブラにもいろいろ程度があります。H8のアセンブラは高級な部類に入ります。アセンブラが高級ということはCPUの命令セットが豊富である事を意味するので、アセンブラが低級なマイコンと比べればいろいろできます。 ※高級なほど人間に理解しやすくマシンは難解、低級なほどマシンに理解しやすく人間は難解な記述となる ``` ;---------------------------------- ; AE-H8MB用Lチカプログラム ;---------------------------------- ; ;-----CPUの指定----- .CPU 300HA ;-----シンボル----- P5DR .EQU H'FFFFCA ;ポート5データレジスタ LED1 .BEQU 0,P5DR ;LED1(P5-0) LED2 .BEQU 1,P5DR ;LED2(P5-1) ;-----アドレス指定/リセットベクトル----- .SECTION RESET0,DATA,LOCATE=H'000000 .DATA.L INIT ;リセットベクトル ;-----I/Oの初期設定----- .SECTION ROM,CODE,LOCATE=H'000100 INIT: MOV.L #H'FFF10,ER7 ;スタックポインタ設定 MOV.B #H'FF,R0L ;出力設定... MOV.B R0L,@H'FFFFC8 ;...ポート5に ;-----MAIN----- BSET LED2 ;LED2のビットをセット(ON) MAIN: BNOT LED1 ;LED1のビットを反転 BNOT LED2 ;LED2のビットを反転 JSR @WAIT ;ウェイトサブルーチンの呼び出し JMP @MAIN ;無限ループ ;-----MAIN終了----- ;-----ウェイト----- WAIT: MOV.L #D'2000000,ER6 ;約1秒のウェイトサブルーチン AAA: DEC.L #1,ER6 ;ER6を1デクリメント.いわゆるFORループです NOP ;なにもしない BNE AAA ;FORループ RTS ;メインルーチンへ復帰 ;-----ウェイト終了----- .END ``` どのようなコードになっているか、軽く解説します ### CPUの指定 ``` ;-----CPUの指定----- .CPU 300HA ``` これはコンパイラのための記述です。H8アセンブラはH8/300HA以外のCPUにも対応しているため、どのCPUで動作させるかをここで指定します。 ### コード内シンボルの指定 ``` ;-----シンボル----- P5DR .EQU H'FFFFCA ;ポート5データレジスタ LED1 .BEQU 0,P5DR ;LED1(P5-0) LED2 .BEQU 1,P5DR ;LED2(P5-1) ``` ここではアセンブラコード内で使っている変数が実際は何であるかを宣言しています。動作に影響はありません。 ### アドレス指定 ``` ;-----アドレス指定----- .SECTION RESET0,DATA,LOCATE=H'000000 ;ROM 000000H番地に書き込む宣言 ``` データ用の”RESET0”というセクションをROM 000000H番地に書き込みますという宣言です。どの番地からバイナリを書き込むかはライタープログラムではなく、アセンブラコード側で指定します。例えば、0FF030番地のRAMへコードを書き込みたい場合は次のようになります。(どの番地がROMでどの番地がRAMとなっているかはCPUのユーザーズマニュアルを参照する必要があります) ``` ;-----アドレス指定----- .SECTION P,CODE,LOCATE=H'0FF030 ``` ちなみに、セクション名RESET0は秋月のサンプルプログラムがそうなっているから、PはH8のアセンブラマニュアルの例がそうなっているからであり、深い意味はありません。 ``` .SECTION ROM,CODE,LOCATE=H'000100 ;ROM 000100H番地に書き込む宣言 ``` この記述も、”ROM”というコードセクションをROM 000100H番地に書き込むという宣言です。 今回のコードを実際にバイナリに吐き出すと次のような配置となります。 ![キャプションを入力できます](https://camo.elchika.com/08f4c3d7a1f33d883db7512c6357baa987468858/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38303034383535362d663062322d343030302d616334642d3266373330643761393339392f39303065306664642d306430342d346366312d383436312d376438653463653766313235/) 先頭コードのバイナリは以下のような記述がされています。 ``` 000000 0000 0100 .DATA.L H'0100 000004 0000 NOP (000100まで空) ``` ``` ;-----リセットベクトル----- .DATA.L INIT ;リセットベクトル 000000 0000 0100 .DATA.L H'0100 ``` .DATA.Lは4バイト長(.L)データの宣言で、リセットベクタのアドレス番地を入れます。リセットベクタとは嚙み砕いていうとリセット処理の記述で、今回の場合は```INIT:```以降の記述がそうなのでINITとなっています。もちろん、自分が好き勝手に他の名前にしてもいいわけです。 電源を入れるとプログラムカウンタがまず0000 0100というアドレスを読み込み、リセットベクタ(リセット処理の記述がされているアドレス番地)が00000100Hである事を理解させます。 ### スタックポインタの設定 実際のバイナリデータとしてCPUに命令を与える記述はINIT以降となります。 ``` ;-----I/Oの初期設定----- .SECTION ROM,CODE,LOCATE=H'000100 ;ROM 000100H番地に書き込む宣言 INIT: MOV.L #H'FFF10,ER7 ;スタックポインタ設定 000100 7A07 000F FF10 MOV.L #H'000FFF10,ER7 ``` コードは000100H番地以降に書き込むと宣言しており、コードの先頭に```INIT:```が来ているので、先ほどの```.DATA.L```は``` 0000 0100```であることが確定します。 ER7とは8本ある32ビット長汎用レジスタ(ER0 - ER7)のうちの1本で、唯一スタックポインタとして設定ができます。レジスタはかみ砕いて言えば一時的に値を記憶しておくメモリで、原則1つの値だけを記憶できます。もし汎用レジスタの本数を超えて値を保持しておきたい場合はレジスタからメモリへ転送するわけですが、どのアドレスへ転送すればよいかのアドレスの番地を保持しているところがスタックポインタです。 H8のRAMアドレスはモード1,2,5,7ではH'FEF10からH'FFF0F、モード3,4,6ではH'FFEF10からH'FFFF0Fと決まっているので、ER7のスタックポインタにはRAMのアドレス内の任意の番地を渡す必要があります。H8の場合はモニタデバッガで動かす事もあり、FEF10をいきなり指定するとモニタデバッガが使用しているRAM領域やRAMに転送したプログラムが破壊されるなどが起きるので秋月のサンプルプログラムが指定しているFFF10というのは割と妥当です。H'0FFF10を渡しているので、モード1,2,5,7のどれかで動かす事を前提としています。 ### I/Oの初期設定 ``` ;-----I/Oの初期設定----- MOV.B #H'FF,R0L ;出力設定... MOV.B R0L,@H'FFFFC8 ;...ポート5に 000106 F8FF MOV.B #H'FF,R0L 000108 38C8 MOV.B R0L,@H'FFFFC8 ``` 秋月のH8マザーボードにはLEDが2個ついており、そのLEDはポート5ー0とポート5-1にそれぞれ繋がっています。これは秋月H8マザーボードの回路図を見ればわかります。 ArduinoでもpinMode関数でピンを入力、プルアップ入力、出力のどれかに設定するように、殆どのICではポートを入力として扱うのか、出力として扱うのかを設定する必要があります。 ポートが入力であるか出力であるかの設定を保持しておくレジスタ、DDR (Data Direction Resister)というものがH8/300Hアーキテクチャにはあり、ポート5のDDRのアドレスはH'C8です。H'C8にあるポート5DDRに出力を意味する「FF」を記憶させているのがここの記述です。 データディレクションレジスタでは一本毎に入出力を設定できます。ポート5は4本しかないため、8ビット長のデータディレクションレジスタのうち、下位4ビットを設定します。上位4ビットは1固定です。 ``` 全て出力にする場合 = H'FF 7 6 5 4 3 2 1 0 1 1 1 1 1 1 1 1 全て入力にする場合 = H'F0 7 6 5 4 3 2 1 0 1 1 1 1 0 0 0 0 0,1を出力、2,3を入力にする場合 = H'F3 7 6 5 4 3 2 1 0 1 1 1 1 0 0 1 1 ``` 今回の場合、P5-2, P5-3は使用しませんが、出力となっています。入力でもいいです。(個人の好みで) 記述としてはまず、R0LレジスタというCPU内部にあるレジスタにFFという値を書き込んでいます。 その後、R0Lの値をH'FFFFC8番地にあるポート5のDDRレジスタに書き込む。 といった記述となっています。 ### LEDを点灯させる ``` ;-----シンボル----- P5DR .EQU H'FFFFCA ;ポート5データレジスタ LED1 .BEQU 0,P5DR ;LED1(P5-0) LED2 .BEQU 1,P5DR ;LED2(P5-1) ;-----MAIN----- BSET LED2 ;LED2のビットをセット(ON) 00010A 7FCA 7010 BSET #1,@H'FFFFCA ``` データディレクションレジスタはポートの入出力を設定し、データレジスタは出力のH/Lを設定します。 データレジスタは8ビット長で、1つのポートにつき1ビット使用しています。ただしポート5は4本しかないので、 ``` 7 6 5 4 3 2 1 0 x x x x L L L L ``` これがデフォルトです。```BSET #1,@H'FFFFCA``` というのは、H'FFFFCAにあるレジスタの1ビット目をセット、つまりHIGHにしなさいと書いているので、この記述により、 ``` 7 6 5 4 3 2 1 0 x x x x L L H L ``` となり、P5-1の出力がHIGHになります。 ``` ;-----シンボル----- P5DR .EQU H'FFFFCA ;ポート5データレジスタ LED1 .BEQU 0,P5DR ;LED1(P5-0) LED2 .BEQU 1,P5DR ;LED2(P5-1) ;-----MAIN----- MAIN: BNOT LED1 ;LED1のビットを反転 BNOT LED2 ;LED2のビットを反転 00010E 7FCA 7100 BNOT #0,@H'FFFFCA 000112 7FCA 7110 BNOT #1,@H'FFFFCA ``` こちらも先ほどと同様ですが、BNOTは現在の状態を反転させなさいという命令です。0ビットはLなのでH、1ビットはHなのでLとなります。つまり、 ``` (初期状態) 7 6 5 4 3 2 1 0 x x x x L L L L ↓ 00010A 7FCA 7010 BSET #1,@H'FFFFCA ↓ 7 6 5 4 3 2 1 0 x x x x L L H L ↓ 00010E 7FCA 7100 BNOT #0,@H'FFFFCA ↓ 7 6 5 4 3 2 1 0 x x x x L L H H ↓ 000112 7FCA 7110 BNOT #1,@H'FFFFCA ↓ 7 6 5 4 3 2 1 0 x x x x L L L H ↓ (ここにウェイト処理が入ります) 00010E 7FCA 7100 BNOT #0,@H'FFFFCA ↓ 7 6 5 4 3 2 1 0 x x x x L L L L ↓ 000112 7FCA 7110 BNOT #1,@H'FFFFCA ↓ 7 6 5 4 3 2 1 0 x x x x L L H L ↓ (ここにウェイト処理が入ります) (以降繰り返し) ``` とデータレジスタを操作する事により、ポート出力の設定がされ、「Lチカ」となるわけです。 ### サブルーチンの実行とループ処理 ``` ;-----MAIN----- MAIN: BNOT LED1 ;LED1のビットを反転 BNOT LED2 ;LED2のビットを反転 JSR @WAIT ;ウェイトサブルーチンの呼び出し JMP @MAIN ;無限ループ 00010E 7FCA 7100 BNOT #0,@H'FFFFCA 000112 7FCA 7110 BNOT #1,@H'FFFFCA 000116 5E00 011E JSR @H'00011E 00011A 5A00 010E JMP @H'00010E 00011E 7A06 001E ... ``` ビットを反転させた後、アセンブラ上では```WAIT:```という名前がついたサブルーチンへジャンプします。JSRはJump-to-SubRoutineの事です。JSRはサブルーチンの処理が終わった後、元のメインルーチンに戻ってきます。よって、次に実行されるコードは```JMP @MAIN``` です。JMPはみたまんま「ジャンプ」で、```MAIN:```のアドレスへジャンプします。 ```MAIN:```ではBNOTでデータレジスタのビットを反転させる処理から記述しているので、ウェイト処理が終わってメインルーチンに復帰し、ジャンプをすると、ついているLEDが交代します。これは無限ループなので「Lチカ」の無限ループがちゃんと記述できていることが確認できました。 ### ウェイト処理 ``` ;-----ウェイト----- WAIT: MOV.L #D'2000000,ER6 ;約1秒のウェイトサブルーチン AAA: DEC.L #1,ER6 ;ER6を1デクリメント.いわゆるFORループです NOP ;なにもしない BNE AAA ;FORループ RTS ;メインルーチンへ復帰 ;-----ウェイト終了----- .END 00011E 7A06 001E 8480 MOV.L #H'001E8480, ER6 000124 1B76 DEC.L #1,ER6 000126 0000 NOP 000128 46FA BNE H'000124 00012A 5470 RTS ``` 非常に多くの回数(200万回)、「何もしない」ループを行っています。 クロックの遅さを利用してウェイトを行っています。 ``` ;-----MAIN----- JSR @WAIT ;ウェイトサブルーチンの呼び出し JMP @MAIN ;無限ループ ;-----ウェイト----- WAIT: MOV.L #D'2000000,ER6 ;約1秒のウェイトサブルーチン 000116 5E00 011E JSR @H'00011E 00011A 5A00 010E JMP @H'00010E 00011E 7A06 001E 8480 MOV.L #H'001E8480, ER6 ``` JSR H'00011Eによって、00011E番地に飛んできます。 ER6は8本ある汎用レジスタのうちの1本で、200万という10進数(Dec)を16進数(Hex)にした1E8480を代入しています。 ``` ;-----ウェイト----- AAA: DEC.L #1,ER6 ;ER6を1デクリメント.いわゆるFORループです NOP ;なにもしない BNE AAA ;FORループ RTS ;メインルーチンへ復帰 000124 1B76 DEC.L #1,ER6 000126 0000 NOP 000128 46FA BNE H'000124 00012A 5470 RTS ``` DEC.L #1,ER6 ではER6の汎用レジスタから1引き算しています。 NOP は何もしません BNE はBranch Not Equal のことで、比較対象がnot equal 0の時に分岐します。H8のアセンブラでDEC命令を用いている場合、CMP命令は不要でER6の汎用レジスタを参照してくれています。つまり、ER6が0でないならAAAにジャンプしろという記述です。これにより、200万回の何もしないループが構成されています。 ER6が0になるとBNEの分岐は発生せず、次のRTSが実行されます。RTSはメインルーチンへ復帰する命令ReTurn-from-Subroutineなのでメインルーチン内JSR @WAITの次の命令であるJMPが実行されます。 ### 重要な点 * プログラムの開始番地はアセンブラ側で指定する * ウェイト処理にしても入出力にしても、アセンブラでLチカを記述するにはレジスタの操作とジャンプが重要 * データディレクションレジスタを操作して各ポート各ピンの入出力の設定をする * データレジスタを操作して各ポート各ピンの出力を設定する 大体こんな感じでしょうか。一度わかれば「そんなもんなんだ」ぐらいの印象を受けると思います。 ただし冒頭の通り、H8のアセンブラは高級です。 ## MCS-48の場合 (FLASH.ASM) 環境構築についてはこの記事をご覧ください https://elchika.com/article/1ab5a4a9-b14d-476f-b835-b1a01120d7b8/ アセンブラ — * ASM48 (Vector) MS-DOS専用 https://www.vector.co.jp/soft/dos/prog/se007626.html * ASM48 (Github) Windows 11で動作可能 https://daveho.github.io/asm48/ 逆アセンブラ — あるにはあるがアドレス・バイナリと対照して出力してくれないのでROMイメージから手動 MCS-48はH8と比べるとだいぶ低級なマイコンであるため、人間の我々からすると複雑なプログラムを書く必要があります。 Lチカのプログラムは面白味が欠ける(後述)ので、前回の記事で使用したASM48内のFLASH.ASMがどのような記述になっているのかを見ていきます。 ``` ;****************************************** ;* ;* 8049 LED FLUSHING PROGRAM ;* ;* 1992-MAY-18 BY S.FUKUDA ;* ;****************************************** ;-----アドレス指定----- .ORG 000H ;アドレス000Hに書き込む ;-----レジスタ初期化----- MOV R0, #0 ;汎用レジスタR0にD'00を代入 ;-----メイン----- START: MOV A, R0 ;アキュムレータへR0に格納された値を代入 MOVP3 A,@A ;第3ページ、アキュムレータに格納されたアドレスの値をアキュムレータに代入 CPL A ;アキュムレータ内の値を反転 OUTL P1,A ;ポート1をアキュムレータのビットに出力する INC R0 ;汎用レジスタR0をインクリメント(内部の値に1足す) ; ;-----ここからループ1----- MOV R1,#0 ;汎用レジスタR1にD'00を代入 ;-----ここからループ2----- LOOP: MOV R2,#77 ;汎用レジスタR2にD'77を代入 LOOP2: DEC R2 ;汎用レジスタR2をデクリメント(内部の値から1引く) MOV A,R2 ;アキュムレータへR2に格納された値を代入 JNZ LOOP2 ;アキュムレータが0ではないならLOOP2にジャンプ ;-----ループ2ここまで----- DEC R1 ;汎用レジスタR1をデクリメント(内部の値から1引く) MOV A, R1 ;アキュムレータへR1に格納された値を代入 JNZ LOOP ;アキュムレータが0ではないならLOOPにジャンプ ;-----ループ1ここまで----- ; ;-----ページ3に書き込まれたパターンが最後まで到達したか?----- MOV A, R0 ;アキュムレータへR0に格納された値を代入 ADD A, #-LEN ;アキュムレータへ(#-LEN)を足し算 JNZ START ;アキュムレータが0ではないならSTARTにジャンプ ; ;-----パターンが最後まで到達してプログラムが一通り終了した----- ;-----再度レジスタを初期化してループ----- MOV R0, #0 ;汎用レジスタR0にD'00を代入 JMP START ;STARTへジャンプ ; ;-----アドレス指定----- .ORG 300H ;アドレス300Hに書き込む ;-----出力パターンの書き込み----- TABLE: .DB 001H ;ポート1の出力パターンが書き込まれている .DB 002H (中略) .DB 0FFH .DB 000H ;-----シンボル----- LEN: .EQU $-TABLE ;シンボル"LEN"は現在のアドレス($)からTABLEのアドレスを引いた値 .END ``` MCS-48ではアキュムレータ・レジスタ共に8ビットまでしか使えないため、ページ内で扱えるアドレス空間は00からFFまでとなっています。この256バイトを1単位としてページと呼び、000 - 0FFを0ページ、100 - 1FF を1ページみたいにカウントします。このようなアドレス管理をセグメント方式と呼んだりします。とりあえずこれだけ知っておけば基本的にH8と同じ要領で読めます。 ※アキュムレータは演算用に用意されたレジスタです ### アドレス指定 ``` ;-----アドレス指定----- .ORG 000H ;アドレス000Hに書き込む ``` アセンブラでROMのアドレスを指定する場合は```.ORG```で指定するのが一般的です。H8でも```.ORG```で書き込みアドレスを指定する事が可能です。(.SECTIONはRENESASマイコンオリジナルな部分がある) ### レジスタ初期化 ``` ;-----レジスタ初期化----- MOV R0, #0 ;汎用レジスタR0にD'00を代入 0000 B8 00 MOV R0, H'00 ``` レジスタの中がundefinedなので00と代入して初期化します。 ### ページ3にあるデータを持ってくる ``` ;-----メイン----- START: MOV A, R0 ;アキュムレータへR0に格納された値を代入 MOVP3 A, @A ;第3ページ、アキュムレータに格納されたアドレスの値をアキュムレータに代入 0002 F8 MOV A, R0 0003 E3 MOVP3 A, @A ``` まず最初のコードではR0に格納されたアドレスを読み取りアキュムレータに格納させています。 次に、第3ページ内で、アキュムレータに格納されたアドレスに記載された値をアキュムレータに記憶させています。 少々ややこしいので初動を考えると、 レジスタR0にはH'00が記載されているはずなのでアキュムレータはH'00が記憶されます。 第3ページ内のアキュムレータに記憶されたアドレス「H'00」を読み取れと記載されているのでROMのアドレスH'300内の値を読み取り、アキュムレータへ記憶させています。 ### ビット反転 ``` ;-----メイン----- CPL A ;アキュムレータ内の値を反転 0004 37 CPL A ``` CASL2だとCPLはComPare Logicalで全く違う動作を実行するのですが、MCS-48でのCPLは ComPLementです。 これはMCS-48のマニュアルに記載されており、アキュムレータ内のビットを反転させる動作です。 例えば、0000 0010 という値がアキュムレータ内にある場合、 反転させるとアキュムレータ内は1111 1101 となります。 MCS-48での```OUTL```ニーモニックでは、1がLowで0がHighです。 ### ポートを出力する & レジスタインクリメント ``` ;-----メイン----- OUTL P1,A ;ポート1をアキュムレータのビットに出力する INC R0 ;汎用レジスタR0をインクリメント(内部の値に1足す) 0005 39 OUTL P1, A 0006 18 INC R0 ``` H8ではデータディレクションレジスタを用いて入出力設定、データレジスタで出力設定を行っていましたが、MCS-48ではIN命令とOUTL命令があるのでバイナリで直接入出力を指定できます。 ポート1には8本のI/Oピンがあります。```.ORG 300H```以降のテーブルには8ビット(=1バイト Data Byte)で記述されたLEDの光るパターンが収録されており、どのように光るかが2進数変換を行う事でわかると思います。 初動ではアドレスH'300から読み取られた"H'01 (=0000 0001)"という値がアキュムレータに格納されているため、P1-0のLEDが光ります。 その後、レジスタR0の値をインクリメントしているため、次回はH'301のアドレスに記載された値(=H'02)を読み取る事になります。 ### ウェイト処理 ``` ;-----ここからループ1----- MOV R1,#0 ;汎用レジスタR1にD'00を代入 ;-----ここからループ2----- LOOP: MOV R2,#77 ;汎用レジスタR2にD'77を代入 LOOP2: DEC R2 ;汎用レジスタR2をデクリメント(内部の値から1引く) MOV A,R2 ;アキュムレータへR2に格納された値を代入 JNZ LOOP2 ;アキュムレータが0ではないならLOOP2にジャンプ ;-----ループ2ここまで----- DEC R1 ;汎用レジスタR1をデクリメント(内部の値から1引く) MOV A, R1 ;アキュムレータへR1に格納された値を代入 JNZ LOOP ;アキュムレータが0ではないならLOOPにジャンプ ;-----ループ1ここまで----- 0007 B9 00 MOV R1, H'00 0009 BA 4D MOV R2, H'4D (=10進数で77) 000B CA DEC R2 000C FA MOV A, R2 000D 96 0B JNZ @H'0B 000F C9 DEC R1 0010 F9 MOV A, R1 0011 96 09 JNZ @H'09 ``` ここではH8でのLチカコードと全く同じ構造の何もしないループを77回1セットとして256回実行しています。最初から19712回ループさせればいいのですが、アキュムレータはFF, 255以上の値を記憶できないためこのような方法を取っています。 尚、R1の初期値は0が宣言された後にデクリメントされてFFとなっている点に注意してください。 ### ページ3に書き込まれたパターンが最後まで到達したかの判定 ``` ;-----ページ3に書き込まれたパターンが最後まで到達したか?----- MOV A, R0 ;アキュムレータへR0に格納された値を代入 0013 F8 MOV A, R0 ``` 再び、R0に記憶されているページ3のアドレス値をアキュムレータへ格納します。 ``` ;-----ページ3に書き込まれたパターンが最後まで到達したか?----- ADD A, #-LEN ;アキュムレータへ(#-LEN)を足し算 JNZ START ;アキュムレータが0ではないならSTARTにジャンプ ;-----シンボル----- LEN: .EQU $-TABLE ;シンボル"LEN"は現在のアドレス($)からTABLEのアドレスを引いた値 0014 03 CF ADD H'CF 0016 96 02 JNZ @H'02 ``` LENの位置はページ3にあるパターンデータの後にあります。パターンデータは49個(H'30個)あるので、アドレスは1つ次のH'31となります。従って、LEN=H'31です。 もう一つ、```#-LEN```がいくつかという話ですが、これはLENの値に-がついており、0が省略されています。オーバーフローに注意しながら計算するとH'00 - H'31 = H'CF となります。よって、#-LEN = D'207 = H'CFとなります。最後のアドレスH'30を読み込んだ後、R0はインクリメントされH'31となります。ここにH'CFが足し算されるとオーバーフローしてH'00となります。アキュムレータは0なのでSTARTにジャンプせず、次の命令へ飛びます。 ### 再初期化&ループ ``` ;-----再度レジスタを初期化してループ----- MOV R0, #0 ;汎用レジスタR0にD'00を代入 JMP START ;STARTへジャンプ 0018 B8 00 MOV R0, H'00 001A 04 02 JMP(0) @H'002 ``` 足し算してH'00にオーバーフローしたのはあくまでアキュムレータ内での話であり、R0の値はまだH'31のままです。 そこで再びR0にH'00を代入して初期化し、```START:```へループさせる記述です。 ### パターンデータの格納 ``` ;-----アドレス指定----- .ORG 300H ;アドレス300Hに書き込む ;-----出力パターンの書き込み----- TABLE: .DB 001H ;ポート1の出力パターンが書き込まれている .DB 002H (中略) .DB 0FFH .DB 000H ``` ページ3 (H'330)以降にはData Byte (1バイト長)の出力パターン が49個収録されています。動作コードはありません。 ## MCS-48の Lチカコード 先程の複数ページの参照をしているコードと比べるとだいぶ面白味はありませんが、こんな感じになります。ループ処理は同じものを使用しました。 下にハンドアセンブルしたものを載せました。これをバイナリエディタで手打ちすれば動きます。 ``` ;-----アドレス指定----- .ORG 000H ;アドレス000Hに書き込む ;-----メイン----- START: MOV R0, #1 ;汎用レジスタR0にD'1を代入 (=0000 0001) MOV A, R0 ;アキュムレータにR0の値を代入 OUTL P1,A ;ポート1をアキュムレータのビットに出力する ; ;-----ここからループ1----- INIT: MOV R1,#0 ;汎用レジスタR1にD'00を代入 ;-----ここからループ2----- LOOP: MOV R2,#77 ;汎用レジスタR2にD'77を代入 LOOP2: DEC R2 ;汎用レジスタR2をデクリメント(内部の値から1引く) MOV A,R2 ;アキュムレータへR2に格納された値を代入 JNZ LOOP2 ;アキュムレータが0ではないならLOOP2へジャンプ ;-----ループ2ここまで----- DEC R1 ;汎用レジスタR1をデクリメント(内部の値から1引く) MOV A, R1 ;アキュムレータへR1に格納された値を代入 JNZ LOOP ;アキュムレータが0ではないならLOOPへジャンプ ;-----ループ1ここまで----- ; ;-----R0が1ならパス、2ならSTARTへジャンプ----- MOV A, R0 ;アキュムレータにR0の値を代入 ADD A, #255 ;アキュムレータへH'FFを加算 JNZ START ;アキュムレータが0ではないならSTARTへジャンプ ;-----R0が1だった----- MOV R0, #2 ;汎用レジスタR0にD'2を代入 (=0000 0010) MOV A, R0 ;アキュムレータにR0の値を代入 OUTL P1,A ;ポート1をアキュムレータのビットに出力する JMP INIT ;INITへジャンプ .END @0000 .ORG 000H =@0000 START: 0000 B8 01 MOV R0, D'1 = H'01 0002 F8 MOV A, R0 0003 39 OUTL P1,A =@0004 INIT: 0004 B9 00 MOV R1, D'0 = H'00 =@0006 LOOP: 0006 BA 4D MOV R2, D'77 = H'4D =@0008 LOOP2: 0008 CA DEC R2 0009 FA MOV A, R2 000A 96 08 JNZ LOOP2 = @H'08 000C C9 DEC R1 000D F9 MOV A, R1 000E 96 06 JNZ LOOP = @H'06 0010 F8 MOV A, R0 0011 03 FF ADD A, D'255 = H'FF 0013 96 00 JNZ START = @H'00 0015 B8 02 MOV R0, D'2 = H'02 0017 F8 MOV A, R0 0018 39 OUTL P1,A 0019 04 04 JMP INIT = @H'04 .END ``` 反転させていないので、他のポート1のI/Oは全てHighです。 H8のような高級アセンブラと比べて、 * サブルーチン関連の処理がないので、メインルーチン内に記述する必要がある * ビット反転の処理がないので、ポートの出力パターンを指定する必要がある * アキュムレータが00 - FFまでしか使えないので、FF回以上のループはループの中にループを作る必要がある * レジスタが00-FFまでしか使えないので、ページと呼ばれるROM番地の切り替えが必要(今回のLチカでは不使用) という制約があるため同じ構造にはならず、上記のようなプログラムとなりました。 ## 総括 ++こんなコードを書かなくても良いC言語は最高だな!++