LEDをつけよう
<プログラム編>

とりあえずの目標

 今日はAKI-80、AKI-ROMライタともに完成したということで、TAIKIさんに私の部屋に来ていただきました(遠いところをご苦労様です)。
 とりあえずマイコンを使って工作をする環境は整ったわけですから、とりあえず動作確認もかねて簡単なものを作っていただき感触をつかんでいただこうととになりました。
 最初の課題はずばり、「LED(発光ダイオード)を点滅させる」というものです。



先にプログラミング?

本来ならハードの設計が先にあることが多いのですが、いきなりということもあるので私の方でプログラムを用意して(といってもKOKEさんに手伝っていただきながらその場で適当に作りました)、それを説明しながらTAIKI氏にハードを組み立てて頂くという手順を選びました。今回はLSIC-80のアセンブラとリンカを利用してROM化用のHEXファイルを生成します。
 細かい説明は今後行うとして、今回は大雑把に流れだけ理解して下さい。



プログラム

; ---------------------------------------------------------------------
;  TAIKI氏に捧げるLED点灯プログラム
; ---------------------------------------------------------------------


; ----- ポート定義 (1)
PIOA		equ	01ch
PIOAC		equ	01dh

; ----- コードセグメント開始 (2)
		cseg
start:		
	; ----- スタックポインタ初期化 (3)
		ld	sp,0
		
	; ----- PIO初期化 (4)
		ld	a,0cfh
		out	(PIOAC),a	; PIOAをビットモードに
		ld	a,000h
		out	(PIOAC),a	; すべて出力
		ld	a,007h
		out	(PIOAC),a	; 割り込みは使用しない
		
	; ----- 点滅 (5)
loop:		
		ld	a,0aah
		out	(PIOA),a	; 交互に点灯
		call	Wait		; ウェイトルーチンを呼び出し (6)
		ld	a,055h
		out	(PIOA),a	; さっきの反転で点灯
		call	Wait		; 1秒程度待つ
		jp	loop		; 無限に繰り返す


	; ----- ウェイト (7)
Wait:
		ld	de,1000		; 1ms待ちを1000回行う
Wait_loop:	
		call	Wait1ms		; 1ms待つ
		dec	de		; デクリメント
		ld	a,d
		or	e
		jr	nz,Wait_loop	; ループ終了条件比較
		ret			; リターン

	; ----- 1msec待ち
Wait1ms:	
		ld	bc,375		; 1msec待つためのループ数 (8)
Wait1msLoop:	dec	bc
		ld	a,c
		or	b
		jr	nz,Wait1msLoop
		ret

		end


プログラム説明

 大変大雑把にですが、プログラムの説明をします


(1) ポート定義

 ここでは equ という擬似命令を使って定数を宣言しています。C言語の #define と同じと考えていただければ分かりやすいでしょう。で、何を定義しているかです。
 今回はLEDを点滅させるためにZ80-PIOというパラレル制御のためのLSIを操作します。CPUにはI/Oポートという外部機器を制御するためのポートが備わっていて、それぞれのポートにいろいろな機能のLSIを繋ぐことが出来ます。AKI-80の場合はTMPZ84C015BFというワンチップマイコンが載っていますのでこのZ80-PIOが内蔵されており 0x1c からのポートがこのPIOの制御ポートとして割り当てられています。


(2) コードセグメント開始

 マイコン用のプログラムではコード(プログラム)、初期化されるデータ、初期化されないデータなどに応じてROMやRAMに配置してやらなければなりません。実際のアドレスはリンク時に指定するのですが、プログラムの中ではとりあえず何処がどういう部分か指定してやります。 cseg 擬似命令は「これ以降はコードセグメントというコードの入ったブロックだよ」とアセンブラに指示してやるためのものです。


(3) スタックポインタ初期化

 アセンブラをやる上で常に無視できないのがスタックです。スタックとはどういう物かというと結局データの格納場所で格納したデータを格納した順とは逆順で取り出せる機構です。データの一時待避やサブルーチンコール時の戻り場所の保存、C言語などでは自動変数の割り当てにも利用されます。そしてその機構を一人で背負って立つのが sp(スタックポインタ)レジスタなのです。
 spはデータの格納の必要が生じるとそのサイズ分値を減算し、そのアドレスにデータを書き込みます(push)。逆にデータを取り出す時は、データを取り出した後、そのザイズ分spを加算します(pop)。つまりスタックはメモリの高位アドレスから低位アドレスに向かってデータを格納していきます。そのため通常はRAM領域の最後にspを設定します。AKI-80の場合は 0000〜7fff がROM、8000〜ffff までがROMですから、spはffffの次のアドレスである 0000 に初期設定します。


(4) PIO初期化

 PIOとはパラレルI/Oのことでデータを8bitの入出力端子を持ったデバイスです。今回はPIOのAポートの端子にLEDを取り付けて、各ビット毎に1出力で点灯させ、0出力で消灯させます。そのため、PIOのAポートをそれに適したモードに初期設定せねばなりません。PIOのAポートを制御するIOポートは2つあり、上記プログラムではPIOAとPIOACで定義しています。ここでPIOAは8本の端子と直接データを入出力するものであり、PIOAC がモード設定などを行うものです。
 とりあえず詳しい説明は今後ということにしますが、ここで行っている処理は、PIOをビットモードという単純な入出力を行うモードにしてすべてのピンを出力に設定しています。よって今後 PIOA に書き込んだ値はそのまま2進数として各端子に0のときはLレベル(0Vに近い電圧)、1のときはHレベル(5Vに近い電圧)が出力されます。


(5) 点滅

いよいよ、点滅させるルーチンです。今回は各ビットを交互に点灯させようということで 10101010 (AA) と 01010101 (55)を間にウェイトを入れながら繰り返し出力することにしました。PIOのモード設定は終わっていますので後は PIOA に AA と 55 を交互に出力するだけです。


(6) ウェイトルーチンを呼び出し

 Z80と言えども馬鹿にしてはいけません。4MHzでもPC-8801やMSXなどでガンガンゲームが楽しめるわけで、AKI-80の場合 10MHzで動いてるわけですからその速度はたいした物です。大雑把に1命令に数μ秒と見積もっても単純ループさせると1秒間に10万回程度点滅を繰り返します。秒間30コマのTVにしっかり騙されてる人間がこんなものを見たって点滅には見えません(これを利用してダイナミック点灯という技がありますが)。というわけでウェイトを入れます。C言語などですと関数呼び出しに当たるのがこの call 命令です。どういう動作をするかと言うと、現在実行中の番地(PC)をスタックに積み、オペランドで指定されたアドレスにジャンプします。サブルーチンから帰るときは ret というC言語の return に相当する命令を利用して帰ってきます。 ret はスタックからアドレスを取り出し、PC(プログラムカウンタ)にセットする働きがあります。


call nn の動作
   PC  <- PC + 3  (自分の命令サイズ分PCを進める)
   SP  <- SP - 2  (PCのサイズ分スタックポインタを移動)
  (SP) <- PC       (スタックにPCを保存)
   PC  <- nn      (指定されたアドレスにジャンプ)

ret の動作
  PC  <- (SP)     (スタックから戻るアドレスを取り出す)
  SP  <- SP + 2   (取り出したサイズ分スタックポインタを戻す)

(7) ウェイト

 今回は私のPCの中を捜すと 1ms 待ちのルーチンが出てきましたのでこれを1000回呼び出すことでおおよそ1秒のウェイトルーチンを作ることにしました。ここで1000回というのが問題で Z80 は 8bit CPUですので 256 以上の数値を扱うにはちょっとした工夫が必要です。幸い Z80 には 8bitレジスタを2つペアで利用して16bitレジスタとして扱える機能があるのでこれを利用します。ここでは de レジスタペアをカウンタとしてループ毎にデクリメントしていき、0になったら終了します。デクリメントは dec という命令が 16 bit 対応なので簡単です。問題は 0 判定です。一度に 16bit 全部が0かどうか判定する命令はないので、8bitづつ行う必要があります。ここではちょっとしたテクニックとして、「上位8bitと下位8bitのORを取り、結果が0なら全部0である」という考え方で判別しています。命令の機能を見ながら考えて見てください。
 なお、条件判定ですが、CPUはフラグレジスタと言う暗黙(でもないんですが)の レジスタを持っており演算命令を実行するたびにこのレジスタを変更していきます。演算結果が0だった場合にはゼロフラグというものが1になります。ここにある jr と 第一オペランドの nz を合わせて、「もしゼロフラグがセットされていなければ指定番地にジャンプしなさい」という意味になります。


(9) 1msec待つためのループ数

 「1msecってどうやって決めるの?」TAIKI氏から鋭い突っ込みを受けました。
 この辺がマイコンのマイコンたるところで割り込みや他のタスクが走っている普通のPCではあまり見かけないテクニックですね。
 当たり前の話ですが、CPUが命令を実行する時間というのは初めからわかっていて、どの命令にステート利用するかはわかっていますからクロックから計算可能なわけです。


Wait1msLoop:
		dec	bc		--- 6ステート
		ld	a,c		--- 4ステート
		or	b		--- 4ステート
		jr	nz,Wait1msLoop	--- 12ステート(ジャンプ時)

 上記のようにループ1回につき26ステートです。AKI-80を 9.8304 MHzで動かした場合1ステートは1クロックですので 9,830,400 ÷ 26 ≒ 378回ループすればいい計算になります。上記プログラムではこれからさらに関数の呼び出しとリターンの時間を差し引いて375回のループを行っています。



終わりに

いやー、相変わらず実に大雑把ですね。今回の説明だけでは何が何やらという人も多いと思います。ぼちぼち補完していきますんでご容赦下さい。m(_ _)m