コンソール表示・入力ルーチン前編

戻る



 今回と次回の2回に分けてシリアル端末に文字を表示したり、端末のキーボードから文字を入力するルーチン群を見ていきます。
 シリアル入出力できるマイコンで何かを作る場合、まずデバッグ用に、中で何が起こっているのか表示させるために(いわゆる「printfデバッグ」のアセンブラ版)、この手のルーチン群を作ることが多いと思います。BASICインタプリタ以外の用途にも役に立つと思います。

 今回見ていくのはSH内蔵SCI(シリアルコミュニケーションインターフェース)を直接ハンドリングして1文字単位で表示/入力を行う低レベルルーチンです。

・PUTCH

 1文字表示するルーチンです。これが無いと何も表示できないわけで、すべての表示ルーチンの根幹です。R0に表示したい文字のアスキーコードを入れて呼びだすと、それをSCIから送り出して端末の画面上に表示します。
PUTCH:	STC.L	GBR,@-R15		; GBR保存
	MOV.L	SCIBASE2,R13		; SCIの先頭番地
	MOV.L	R2,@-R15		; レジスタ保存
	MOV.L	R1,@-R15
	LDC	R13,GBR			; GBRはSCI先頭番地を指す
	MOV.L	R0,@-R15		; R0保存(出力すべき文字)

 開始部分です。GBRは通常BASICインタプリタのワークエリアの先頭を指しているのですが、SCIアクセスのために保存します。
 さらにR0〜R2を保存します。この手のルーチンは、使い勝手を良くするために全レジスタを保存するのが普通です。1文字表示するたびにいちいち呼びだし側でレジスタ保存していたら大変ですよね。ただし、R0は「送信したら用済みになるデータ」なので、結局 PUTCH の中の最後の方で破壊されます。また、R13は、「サブルーチン呼びだしをしたらほぼ間違い無く壊されるものと覚悟すべきレジスタ」なので保存しません。

	MOV.L	LATKEY1,R2		; 最後に入力された文字が格納されている番地

 初期バージョンでは本当に文字を送信するだけのルーチンでした。それだと、人間が手でキー入力しているぶんには問題無いのですが、コピー&ペーストで長い文字列を貼りつけると文字落ちしていました。
 そこで、文字出力ルーチンの中でもキー入力がないかどうか監視するようにしています。そのためのワークエリアが LATKEY で、文字送信中に受信を検知したらその文字コードを保存しておくためにあります。何も受信文字が無い場合は0が入っています。

 受信を割り込みで処理すれば文字落ちの心配なんかしなくてもいいのですが、BASICインタプリタが勝手に割り込み許可/不許可をしない、というのもある面ではメリットかと思い、CMBシリーズでは伝統的に割り込みを使っていません。たとえば「一切の割り込みを受け付けたくない、でも端末との文字のやりとりはしたい」というケースもあるかと思います。そういうときにBASICのPRINT文やINPUT文を使うとインタプリタが勝手に割り込み許可したりするようでは困ります。



PUTCHL:	MOV.B	@(SSR_3-SCIBASE,GBR),R0	; SSR(シリアルステータスレジスタ)を読む
	TST	#H'80,R0		; 送信可か?
	BF	PUTCHX			;   送信可なら送信実行
	MOV.B	@R2,R1			; 最後に入力された文字(R2は LATKEY を指している)
	TST	R1,R1			; すでに最後に入力された文字があるなら
	BF	PUTCHL			;   それ以上入力できないので無視
	TST	#H'40,R0		; 受信データあり?
	BF	PUTCH1			;   あれば受信処理へ
	TST	#H'38,R0		; エラーあり?
	BT	PUTCHL			;   無ければループ繰り返し
	AND	#H'C4,R0		; エラークリヤ
	BRA	PUTCHL			; ループ繰り返し
	MOV.B	R0,@(SSR_3-SCIBASE,GBR)	; エラークリヤした値をSSRに書く(遅延スロット)

 ループ部分です。PUTCH の本来の役目は文字を表示(送信)することなのでまず送信可能か調べ、可能なら送信処理に向かいます。

 送信できない場合、つまり前に送った文字の送信が完了していない場合ボケーっと送信待ちだけしていると、その間に向こうから送られてきた文字を取りこぼすかも知れないので、受信もチェックします。受信エラーがあるようならクリヤします。

SHのマニュアル見れば書いてあることですが、参考までにSCIのSSRのビット構成は次のようになっています。

ビット









名称

TDRE
RDRF
ORER
FER
PER
TEND
MPB
MPBT

意味

送信可

受信あり

オーバーラン

フレーミング

パリティ

送信終了

マルチプロセッサ関連

エラー


 エラーをクリヤするには H'C4、エラーと受信フラグをクリヤするには H'84、送信可フラグをクリヤするには H'7F でAND すればよいことがわかります。(マルチプロセッサ関連は使っていないのでどうでもいいです)



PUTCH1:	MOV	R0,R1			; SSRの値をR1に保存
	MOV.B	@(RDR_3-SCIBASE,GBR),R0	; RDR(受信データレジスタ)を読む
	MOV.B	R0,@R2			; 受信データを LATKEY に書きこむ
	MOV	R1,R0			; SSRの値
	AND	#H'84,R0		; 受信フラグとエラーをクリヤ
	BRA	PUTCHL			; ループ先頭へ
	MOV.B	R0,@(SSR_3-SCIBASE,GBR)	; フラグクリヤした値をSSRに書きこむ(遅延スロット)

 送信待ち中受信処理です。受信した文字は LATKEY に格納します。受信フラグとエラーをクリヤし、ループ先頭に戻って再度送信可を待ちます。

PUTCHX:	MOV	R0,R1			; SSRの値をR1に保存
	MOV.L	@R15+,R0		; スタックからR0(送信すべき文字)を復帰
	MOV.B	R0,@(TDR_3-SCIBASE,GBR)	;   をTDR(送信データレジスタ)に書きこむ
	MOV	R1,R0			; SSRの値
	AND	#H'7F,R0		; 送信可フラグをクリヤ
	MOV.B	R0,@(SSR_3-SCIBASE,GBR)	; フラグクリヤした値をSSRに書きこむ
	MOV.L	@R15+,R1		; R1復帰
	MOV.L	@R15+,R2		; R2復帰
	RTS				; リターン
	LDC.L	@R15+,GBR		; GBR復帰(遅延スロット)

 送信処理です。送信すべき文字をスタックから取りだし、TDRに書きこみます。SSRの送信可フラグをクリヤし、保存したレジスタを復帰してリターンします。

・GETCH

 1文字入力ルーチンです。端末から文字が送られてくるのを待ち、受け取った文字コードをR0に返すルーチンです。

GETCH:	MOV.B	@(LATKEY-WRKTOP,GBR),R0	; LATKEY に何か文字が
	CMP/EQ	#0,R0			;  入っているなら
	BF	GETCHE			;   終了(その文字を返す)

 開始部分です。まず LATKEY の内容を見て、それが0でなければすでに CHPUT が受信しているので、SCIを見るまでも無くその値を返します。

	MOV.L	SCIBASE2,R13		; SCIの先頭番地
	STC.L	GBR,@-R15		; GBR保存
	LDC	R13,GBR			; GBRはSCI先頭番地を指す
	MOV.L	R1,@-R15		; レジスタ保存
	MOV.L	R2,@-R15		; 

 LATKEY が空だった場合はSCIを読みに行きます。PUTCH と同様、レジスタの保存とGBRの設定を行います。

GETCHL:	MOV.B	@(SSR_3-SCIBASE,GBR),R0	; SSRを読む
	TST	#H'40,R0		; 受信データあり?
	BF	GETCHX			;   あれば受信処理へ
	TST	#H'38,R0		; エラーあり?
	BT	GETCHL			;   無ければループ繰り返し
	AND	#H'C4,R0		; エラークリヤ
	BRA	GETCHL			; ループ繰り返し
	MOV	R0,@(SSR_3-SCIBASE,GBR)	; エラークリヤした値をSSRに書きこむ(遅延スロット)

 ループ部分です。PUTCH でも同様のことをやっていましたが、H8やSHの内蔵SCIは、エラーが発生するとそれをクリヤしない限り次の文字の受信はできないので、受信待ちループ中でエラーを検出したらクリヤは必須です。

GETCHX:	AND	#H'84,R0		; 受信フラグとエラーをクリヤ
	MOV	R0,R1			; SSRの値をR1に保存
	MOV.B	@(RDR_3-SCIBASE,GBR),R0	; RDRを読む
	MOV	R0,R2			; 受信した文字をR2に保存
	MOV	R1,R0			; フラグクリヤされたSSRの値
	MOV.B	R0,@(SSR_3-SCIBASE,GBR)	; SSRに書きこむ
	EXTU.B	R2,R0			; 受信した文字を符号無し化してR0に移す
	MOV.L	@R15+,R2		; レジスタ復帰
	MOV.L	@R15+,R1		; 
	RTS				; リターン
	LDC.L	@R15+,GBR		; GBR復帰(遅延スロット)

 受信処理です。RDRを読み、SSRの受信フラグとエラーをクリヤし、読んだRDRの値を符号無し化してR0に返してリターンします。もちろん保存したレジスタの復帰も行います。

GETCHE:	SHLL8	R0			; R0の下位8ビットを保存し同時にクリヤ
	MOV.B	R0,@(LATKEY-WRKTOP,GBR)	; LATKEY をクリヤ
	MOV.B	R0,@(LATKTM-WRKTOP,GBR)	; LATKTM をクリヤ
	SHLR8	R0			; 保存した8ビットを復帰
	RTS				; リターン
	EXTU.B	R0,R0			; R0を符号無し化(遅延スロット)

 こちらは LATKEY に何がしかの文字が入っていたときの終了処理です。SCIを全くいじらずに LATKEY の値をR0に返し、 LATKEY と LATKTM(後で説明)をクリヤします。

 ここの SHLL8 と SHLR8 の使い方はトリッキーです。ちょっと説明が必要ですね。

ということで思いついたのがこのコードです。SHLL8 を実行すればR0の下位8ビットを、H8で言うところの「R0H」(SHでは絶対こういう呼び方はしませんが)に退避すると同時に「R0L」を0にすることができます。0を使う用事がすんだら SHLR8 で元に戻せます。
 最後にその値を符号無し化してリターンします。

・CHKBRK

 今回の最後に、もうひとつSCIを直接いじっているルーチン CHKBRK を見ていきます。これは、プログラムの実行中やプログラムリスト表示中に、CTRL−Cキーが押されたら中断するためのルーチンです。RUNコマンドやLISTコマンドは処理の区切れごとにこれを呼びだし、CTRL−Cキーをチェックします。
 このルーチンが呼ばれたとき、CTRL−Cキーが押されていなかったら何事も無かったかのようにリターンします。押されていたらブレーク処理にジャンプし、呼びだし元には戻りません。

CHKBRK:	MOV.B	@(LATKEY-WRKTOP,GBR),R0	; LATKEY を読む
	MOV.L	SCIBASE3,R13		; SCIの先頭番地
	CMP/EQ	#0,R0			; LATKEY が0でなければ(なにか入力があるなら)
	BF	CHKBR1			;   CHKBR1 へ飛ぶ

	STC.L	GBR,@-R15		; GBR保存
	LDC.L	R13,GBR			; SCIの先頭番地をGBRにロード

 開始部分です。GETCH 同様にまず LATKEY を見ます。すでに受信した文字があるなら CHKBR1 へ飛びます。受信した文字がなければSCI先頭番地をGBRにロードします。

	MOV.B	@(SSR_3-SCIBASE,GBR),R0	; SSRを読む
	TST	#H'38,R0		; エラーあり?
	BF	CHKBR2			;   あれば CHKBR2 へ
	TST	#H'40,R0		; 受信文字あり?
	BT	CHKBR3			;   あれば CHKBR3 へ

 SSRを読んでエラーと受信フラグをチェックします。ここでもやはりエラー検出時のクリヤは必須です。

	MOV	R0,R1			; SSRの値をR1に保存
	MOV.B	@(RDR_3-SCIBASE,GBR),R0	; RDRを読む
	MOV	R0,R2			; RDRの値をR2に保存
	MOV	R1,R0			; SSRの値
	AND	#H'84,R0		; 受信フラグとエラーをクリヤ
	MOV.B	R0,@(SSR_3-SCIBASE,GBR)	; クリヤ後の値をSSRに書き戻す
	MOV	R2,R0			; RDRの値

	CMP/EQ	#H'03,R0		; CTRL−Cか?
	BT	JBREAK1			; 

 受信データがあった場合RDRを読み、SSRのエラーと受信フラグをクリヤします。受信した文字がCTRL−Cならブレーク処理に飛びます。

	LDC.L	@R15+,GBR		; GBR復帰
	MOV.B	R0,@(LATKEY-WRKTOP,GBR)	; 受信した文字を LATKEY に入れる
	MOV	#30,R0			; 適当な定数
	RTS				; リターン
	MOV.B	R0,@(LATKTM-WRKTOP,GBR)	; 適当な定数を LATKTM に入れる(遅延スロット)

 CTRL−C以外の文字ならそれを LATKEY に入れ、さらに LATKTM に適当な定数(30)をセットしてリターンします。LATKTM というのは LATKEY の有効期限を表すタイマーです。

 ここで、実際のソースと順番は違うのですが、先にラベル CHKBR2 〜 CHKBR4 の説明をしておきます。CHKBR2 以降の処理は「何事も無かったかのようにリターンする」処理です。
;(エラー検出した場合)
CHKBR2:	AND	#H'C4,R0		; エラーをクリヤ
	MOV.B	R0,@(SSR_3-SCIBASE,GBR)	; SSRに書き戻す

;(受信文字が無かった場合)
CHKBR3:	MOV.B	@(LATKTM-WRKTOP,GBR),R0	; LATKTM を読む
	LDC.L	@R15+,GBR		; GBR復帰
	CMP/EQ	#0,R0			; LATKTM は0か?
	BT	CHKBR4			;   0なら CHKBR4 へ
	ADD	#-1,R0			; LATKTM をデクリメント
	RTS				; リターン
	MOV.B	R0,@(LATKTM-WRKTOP,GBR)	; LATKTM を書き戻す(遅延スロット)

 CHKBR2 はSSRのエラービットが立っていたときに飛んでくるラベルです。エラーをクリヤします。CHKBR3 は受信データが無かったときに飛んでくるラベルです。うーん、どう考えても MOV.B (LATKTM-WRKTOP,GBR),R0 と LDC.L @R15+,GBR の順番、逆になるべきだよな・・・いままでよく動いていたな。一応普通にCTRL−Cでブレークかけられてたけど、運が良かっただけかも。ああ、恥ずかしい(^^;

 受信文字が無かった場合、 LATKTM が0なら CHKBR4 に分岐します。0より大きければデクリメントしてリターンします。

CHKBR4:	MOV	#0,R0			; 0を
	RTS				; 		リターン
	MOV.B	R0,@(LATKEY-WRKTOP,GBR)	; LATKEY に書きこむ(遅延スロット)

 LATKTM が0だった場合は LATKEY もクリヤしています。何のためにこんなことやっているかというと、LISTコマンドなどで大量に表示している最中、うっかりキーボードに触れた際の文字がいつまでも LATKEY に残っているとイヤだな、と思ったからです。
 一定回数 CHKBRK が呼ばれてもその文字が LATKEY に残っている状態、つまり、BASICインタプリタにとってかなり長い期間(人間から見ると一瞬ですが) GETCH による文字入力が全く行われないような状態の場合 LATKEY をクリヤするようにしているわけです。その「かなり長い期間」の間もCTRL−Cのチェックだけは優先的に行っていますから、ブレークはしっかり効きます。

;(最初に LAYKEY に何か入っていた場合)
CHKBR1:	CMP/EQ	#H'03,R0		; CTRL−Cか?
	BF	CHKBR3			;   CTRL−Cでなければ CHKBR3 へ
	BRA	JBREAK1			; CTRL−Cならブレーク処理へ
	NOP				; (遅延スロット)

 最初から LATKEY に何か文字がが入っていたケースの処理です。CTRL−Cが入っていた場合はブレーク処理へジャンプします。CTRL−Cでなければ CHKBR3 へ飛んで何事も無かったかのようにリターンします。・・・って、これも CHKBR3 へ飛んじゃだめじゃん。GBR保存なんてしてないんだから。


 ここからは弁解コーナーです。今回 CHKBRK のいろいろなバグを見つけてしまいましたが、大元のCMBの該当部分にはバグはありません。あれこれ削っているうちに調子に乗って削りすぎてしまったのが原因のようです。
CHKBRK:	MOV.B	@(LATKEY-WRKTOP,GBR),R0	; LATKEY を読む
	STC.L	GBR,@-R15		; GBR保存
	MOV.L	SCIBASE3,R13		; SCIの先頭番地
	CMP/EQ	#0,R0			; LATKEY の値をチェック
	LDC.L	R13,GBR			; SCIの先頭番地をGBRにロード
	BF	CHKBR1			; LATKEY の値により分岐

 これはCMBの CHKBRK の入り口です。 LATKEY の値が何であろうとGBRを保存してSCIの番地を設定していますね。これなら CHKBR1 の部分は正しく動きます。ここを「 LATKEY が0じゃないならSCI見る必要ないじゃん」と「拙速主義」でいいかげんに直してしまったのが敗因。

CHKBR3:	LDC.L	@R15+,GBR		; GBR復帰
	MOV.B	@(LATKTM-WRKTOP,GBR),R0	; LATKTM を読む
	CMP/EQ	#0,R0			; 0かどうかチェック

 CHKBR3 のところも、CMBでは論理的に正しい順番になっています(パイプラインストール起こしまくりですが)。ここはなんで順番変えてしまったのかもう覚えていないのですが、たぶんR0ロード後のパイプラインストールを軽減しようとして、よく確かめもせず安易に順番を入れ替えたのだと思います。

 こんな間違いしたのも、早くCCMBをリリースしたくて焦っていたせいなのですが、何事もチェックは大事、ということですね。思えば最初にCMBをSHに移植したときにはパイプラインとか遅延スロットとか初体験だったので「本当に順番入れ替えても大丈夫か?」と何度も見直したのですが・・・慣れると油断が出るものです。

 今回はここまでにします。