sanguisorbaのアイコン画像
sanguisorba 2021年12月06日作成 (2022年01月22日更新) © MIT
セットアップや使用方法 セットアップや使用方法 Lチカ Lチカ 閲覧数 3738
sanguisorba 2021年12月06日作成 (2022年01月22日更新) © MIT セットアップや使用方法 セットアップや使用方法 Lチカ Lチカ 閲覧数 3738

アセンブラでLチカ (H8/3048, 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/

アセンブラ —

※秋月キット付属のVer 1.0の評価版アセンブラでも同じバイナリデータを吐きます

逆アセンブラ —

アセンブラにもいろいろ程度があります。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番地に書き込むという宣言です。

今回のコードを実際にバイナリに吐き出すと次のような配置となります。

キャプションを入力できます

先頭コードのバイナリは以下のような記述がされています。

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.L0000 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/

アセンブラ —

逆アセンブラ — あるにはあるがアドレス・バイナリと対照して出力してくれないので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言語は最高だな!

sanguisorbaのアイコン画像
マイコンを使わない低レベルな電子工作とかPCBパターン製作など
ログインしてコメントを投稿する