「Hello world」の次はコンソール電卓に挑戦してみます。
下記のような仕様を考えています。
- 16bit符号付き整数(-32,768〜32,767)
- 加減乗除、剰余、括弧
- 演算子の優先順位付き
- オーバーフロー判定は無し
目標は92%の人が間違えるという「7+7÷7+7×7-7」を正しく解くことです。
まずは数値の入力と出力をしてみます
HD6301/3はDレジスタで16bitの加減算が可能です。そこで今回の電卓は16bitの整数を扱うこととします。Tiny BASICと同じですね。
数値の取得(get_int_from_decimal)
一行入力「read_line
」は実装済みですので、入力された数字を16bit数値に変換してDレジスタに代入するルーチンを考えます。
; -----------------------------------------------------------------------
; テキストバッファの10進文字列から数値を取得する
; Get a integer from a decimal string in a text buffer
; [Args] X:Buffer address
; [Return] TRUE:C=1 / D:Integer X:Next buffer address
; FALSE:C=0 / B:Character X:Current buffer address
; -----------------------------------------------------------------------
get_int_from_decimal:
.RetValue .eq UR0 ; Return Value
.TempValue .eq UR1 ; Temporary Value
clra
clrb
std <:RetValue
staa <:TempValue
staa <SignFlag
ldab 0,x ; Get a one character.
cmpb #'-' ; Is it a minus sign?
bne :1 ; No.
inc <SignFlag ; Yes. Set a sign flag.
bra :next
.1 cmpb #'+' ; Is it a plus sign?
beq :next
jsr is_decimal_char ; No. Is it a decimal character?
bcc :false ; No. It is not a decimal string.
bra :first
.next inx ; Get a next character.
ldab 0,x
jsr is_decimal_char ; Is it a decimal character?
bcc :err00 ; No. Syntax error.
bra :first ; Yes. This is a first digit.
.loop std <:RetValue
ldab 0,x
jsr is_decimal_char ; Is it a decimal character?
bcc :end ; No. Go to end process.
.first subb #$30 ; Convert ASCII code to binary.
stab <:TempValue+1
ldd <:RetValue ; RetValue * 10 + TempValue
asld ; * 2
asld ; * 4
addd <:RetValue ; * 5
asld ; * 10
addd <:TempValue ; RetValue + TempValue
inx
bcs :err02 ; Out of range
bmi :overflow ; If MSB is set, check the overflow.
bra :loop
.end ldd <:RetValue ; Now, integer is in D-register.
tst <SignFlag ; Is it plus sign?
beq .true ; Yes. Set carry and return.
coma ; No. Make 2's complement.
comb
addd #1
.true sec
.false rts
.overflow
xgdx
cpx #$8000 ; Is it |-32768|?
xgdx
bne :err02 ; No. So, it is out of range.
tst <SignFlag ; Is sign plus?
beq :err02 ; Yes. So, it is out of range.
bra :loop ; No. So, it isn't out of range. return.
.err00 clrb
jmp write_err_msg
.err02 ldab #2
jmp write_err_msg
9,10行目でこのルーチンで使用するローカルラベルを定義しています。
11〜15行目でローカル変数を初期化します。
16〜25行目は符号のチェックをしています。マイナスであればゼロページにある符号フラグ(SignFlag)を立てておきます。符号がなければ数値への変換処理に進みます。
26〜30行目は符号があった場合の追加処理です。符号の次の文字が数値でなければエラー処理を行います。
31〜46行目がメインの処理ルーチンです。
やっていることは結構単純で、取得した文字のアスキーコードから$30
を引いて数値に変換、元の数値を10倍にしたものに足すことを文字数分繰り返しているだけです。
ただしこのままだと何桁の数字でも受け入れてしまうので、入力時のみオーバーフロー判定することにします。
44行目ではキャリーが発生したら桁あふれなのでエラー処理に進みます。
45行目ではMSBが1(N=1
)かどうか判定しています。32,767を超えているのでオーバーフローなのですが、$8000
のみ-32,768で範囲内です。そのため、56行目からのルーチンで、$8000
かつ符号がマイナスの時のみエラーではないとしてルーチンに復帰させています。
47行目からは終了処理です。結果をDレジスタに転送して、符号がマイナスであれば2の補数に変換します。
65行目からはエラーメッセージ表示のための処理です。これは後ほど説明します。
数値の出力(write_integer)
お次はDレジスタの内容を文字列に変換してSCIに送信するルーチンです。
; -----------------------------------------------------------------------
; Dレジスタの数値をコンソールに出力する
; Write Decimal Character converted from Integer
; [ Args ] D:Integer
; -----------------------------------------------------------------------
write_integer:
.ZeroSuppress .eq UR0H ; Zero suppress flag
.Counter .eq UR0L ; Digit counter
bpl :plus ; Determine the sign.
pshb
ldab #'-'
jsr write_char
pulb
coma ; Make 2's complement.
comb
addd #1
.plus clr <:ZeroSuppress
ldx #:CONST
.loop clr <:Counter ; Clear the digit counter.
.digit subd 0,x ; Subtract the digit constant from a integer.
bcs :write
inc <:Counter ; Count the number of times it's subtracted.
bra :digit
.write addd 0,x ; Restore the Integer.
pshb
ldab <:Counter
beq :1 ; Is this digit zero?
inc <:ZeroSuppress ; No. Set the zero suppress flag.
.1 tst <:ZeroSuppress ; Is the zero suppress flag set?
beq :2 ; No. Don't write this digit.
addb #$30
jsr write_char
.2 pulb
inx ; Next constant
inx
cpx #:CONST+8
bne :loop
addb #$30 ; Write last digit.
jmp write_char
; Each digit up to 10,000 for conversion
.CONST .dw $2710 ; 10,000
.dw $03e8 ; 1,000
.dw $0064 ; 100
.dw $000a ; 10
考え方は各桁を1ずつ引いていき、引けた数を表示していくだけです。
7,8行目でこのルーチンで使用するローカルラベルを定義しています。
9〜16行目は符号チェックです。N=1
であればマイナス符号を出力した後に2の補数に変換します。
17行目はゼロサプレスを行なうためのフラグです。
18行目で定数(まずは10,000)のアドレスを設定してメインルーチンに入ります。
19〜23行目でDレジスタから定数を引き、その回数を変数Counterに記録します。
25行目では引きすぎた分を戻しています。Counter
がゼロ以外であればゼロサプレスのフラグを立てます。Counter
値をアスキーコードに変換し、SCIに送信します。ただしゼロサプレスのフラグが立っていない場合はなにもしません。
以後は定数を示すアドレスを更新し、繰り返します。1の位はそのままアスキーコードに変換して表示します。Dレジスタの値がゼロで最後までゼロサプレスでもここでゼロが表示されるわけです。
関連ルーチン
空白を読み飛ばす(skip_space)
; -----------------------------------------------------------------------
; 空白を読み飛ばす
; Skip Space
; [Args] X:Buffer address
; [Return] B:Ascii code ( if $00 then Z=1)
; X:Current buffer address
; -----------------------------------------------------------------------
skip_space:
ldab 0,x
beq :end
cmpb #SPACE
bhi :end
inx
bra skip_space
.end rts
空白($20
)以外の文字を取得するまでテキストバッファを読み進めます。
Bレジスタには空白直後のアスキーコードが代入されます。
末端記号($00
)も含みますので、ルーチン復帰後ゼロフラグを確認すればすぐに分岐が可能です。
エラーメッセージを表示する(write_err_msg)
; -----------------------------------------------------------------------
; エラーメッセージを表示する
; Write Error Messege
; [ Args ] B: Error code
; -----------------------------------------------------------------------
write_err_msg:
ldx #ERRMSG
abx
ldx 0,x
jsr write_line
jsr write_crlf
ldx <StackPointer
txs
jmp main
ERRMSG .dw .0
.dw .2
.0 .az "Syntax error"
.2 .az "Out of range value"
Bレジスタにエラーコードを入れてこのルーチンを呼び出すと、対応するエラーメッセージを表示します。その後スタックポインタを初期化してプログラム先頭にジャンプします。
エラーコードは偶数のみ割り振ってあり、テーブルの基準アドレスにエラーコードを足すだけでメッセージのアドレスを取得できるようにしてあります。
メインルーチン
; ***********************************************************************
; * Service Routine Jump Table *
; ***********************************************************************
init_sbc6303 .eq $ffa0
mon_main .eq $ffa3
read_char .eq $ffa6
read_line .eq $ffa9
write_char .eq $ffac
write_line .eq $ffaf
write_crlf .eq $ffb2
write_space .eq $ffb5
write_byte .eq $ffb8
write_word .eq $ffbb
is_alphabetic_char .eq $ffbe
is_decimal_char .eq $ffc1
is_hexadecimal_char .eq $ffc4
; ***********************************************************************
; * Constants *
; ***********************************************************************
SPACE .eq $20 ; Space
CR .eq $0d ; Carriage Return
LF .eq $0a ; Line Feed
PROGRAM_START .eq $1000
Rx_BUFFER .eq $0100 ; SCI Rx Buffer
Rx_BUFFER_END .eq $0148 ; 73byte(72character)
; ***********************************************************************
; * Variables *
; ***********************************************************************
.sm RAM ; Select Memory Directive
.or $80
StackPointer .bs 2
SignFlag .bs 1
; General-Purpose Registers
UR0 *
UR0H .bs 1
UR0L .bs 1
UR1 *
UR1H .bs 1
UR1L .bs 1
UR2 *
UR2H .bs 1
UR2L .bs 1
UR3 *
UR3H .bs 1
UR3L .bs 1
; ***********************************************************************
; * Program Start *
; ***********************************************************************
.sm CODE
.or PROGRAM_START
init: tsx
stx <StackPointer
main:
ldab #'>'
jsr write_char
jsr read_line
ldx #Rx_BUFFER
jsr skip_space
jsr get_int_from_decimal
bcs :1
bra :err
.1 jsr write_integer
jsr write_crlf
bra main
.err ldx #MSG
jsr write_line
bra main
MSG .az "It isn't a decimal number.",#CR,#LF
メインルーチンは結構単純です。
58行目ではプログラムスタート時のスタックポインタを保存しています。write_err_msg
を実行するとそのままmain
に戻ってくるので、スタックポインタを元に戻さなければいけません。そのための準備です。
61行目からがメインルーチンです。一行入力ルーチンread_line
で読み込んだテキストからget_int_from_decimal
で数値を取得、そのままwrite_integer
で表示します。get_int_from_decimal
の返り値がC=0
だった場合はエラーメッセージを表示してからmain
に戻ります。
実行結果

大丈夫そうですね!-32,768はOKで32,768はきちんと範囲外と認識されています。
コメント