「7+7÷7+7×7-7」に挑戦(1/3)

HD6301/HD6303

「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行目でこのルーチンで使用するローカルラベルを定義しています。

ローカル変数もどきです。実際にはゼロページ領域にある変数UR0,UR1の別名を定義しているだけなのでどこからでもアクセスできてしまいますが、少しだけ分かりやすくなります

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はきちんと範囲外と認識されています。

コメント

タイトルとURLをコピーしました