0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

(自作CUIOS)キーボード入力バッファの不具合

Posted at

「手探りでCUI OS作成に挑む」連載

この記事は「手探りでCUI OS作成に挑む」連載の一部です。
全体の目次は「手探りでCUI OS作成に挑む」連載目次を御覧下さい。

現象

キーボードから入力したコマンドはinput_bufferに一時保存し、エンターが押されると予め登録していたコマンド(help等)等と比較し、一致すればそれらの処理を開始する仕組みとなっている。
どんなコマンドを入力しても、登録しているコマンドと一致しないため、エンターを押す度にinput_bufferの中身を画面へ出力するようにした結果、初めの1文字しか保存されていないことが分かった。

图片.png

検証用の為、最小限に絞ったコード

testcode.asm
testcode.asm
[bits 16]
[org 0x7E00]

start:
    ; セグメント初期化
    xor ax, ax
    mov ds, ax
    mov ss, ax
    mov sp, 0x7C00
    
    ; スタック設定
    mov ss, ax
    mov sp, 0x7C00
    
    mov si, message
    call print_string

command_loop:
    
    ; c:\>を表示
    mov si, prompt
    call print_string
    
    ;キーボードから入力されるのを待つ(入力が終わるまで返ってこない) int 0x16未使用
    call read_input
    
    mov si, input_buffer; ; ; ; ; 
    call print_string    ; ; ; ; ; 
    mov si, newline
    call print_string    ; ; ; ; ; 
    
    ; command_loopへ戻りc:\>を表示して次の命令を待つ
    jmp command_loop
    jmp $   ; 無限ループ(停止)

; 起動画面に表示する文字列
message db "HEAD", 0x0D, 0x0A,0
        
prompt db  'C:\>', 0
        
%include "keyboard.asm"
%include "vga.asm"

times 5120-($-$$) db 0
dw 0xAA55
keyboard.asm
keyboard.asm

%define MAX_INPUT 64     ; 命令の最大入力文字数
input_buffer times MAX_INPUT+1 db 0
newline db 0x0D, 0x0A,0

; キーボード入力関数
; 入力された結果はinput_bufferへ格納
read_input:
    pusha
    ;BIOS依存(INT10)では動いていたがES=DSと明記と失敗してハマった
    ;(stosb は 必ず ES:DI を使う 命令)
    push ds
    pop es
    mov di, input_buffer ;入力バッファ
    mov cx, 0            ; 文字数を数える
    
.read_char:
    ; 一文字読み込み(結果はASCIIコードでALへ格納される)
    ; mov ah, 0x00
    ; int 0x16
    call get_key
    
    ; 無効キーは無視
    cmp al, 0
    je .read_char         ; 無効キーは無視
    
    ; エンターが押された場合の処理
    cmp al, 0x0D
    je .done_input
    
    ; 長さが超えていないか確認
    cmp cx, MAX_INPUT ; 入力文字数が上限に達していないか確認
    jae .read_char    ; 上限超えたら無視して次のキー入力へ
    
    ; 入力に問題が無い場合は入力バッファへ格納
    stosb    ; AL の文字を [ES:DI] に格納(バッファに保存)、DI++(次の位置へ)
    inc cx   ; 入力文字数+1
    
    ; 入力された文字を画面へ表示
    ; mov ah, 0x0E
    ; int 0x10
    call putchar_direct ; 表示する文字はALに入っている
    jmp .read_char
    
.done_input:
    ; 文字列を終了(終端文字を追加)
    mov al, 0
    stosb
    
    ; 改行
    ; mov ah, 0x0E
    ; mov al, 0x0D
    ; int 0x10
    ; mov al, 0x0A
    ; int 0x10
    mov si, newline
    call print_string
    
    popa
    ret
    
; キーボードからの入力を受け取り対応するASCIIコードをAL経由で返す
get_key:
    in al, 0x64       ; 0x64ポート経由で入力されているかを確認
    test al, 1        ; 0ビット目 1:有り 0:無し
    jz get_key        ; 押されていなければget_keyへ戻って初めから
    
    in al, 0x60       ; 押されたキーの番号を取得

    ; キーの番号最高位が 0:押された 1:離された
    test al, 0x80
    jnz get_key       ; 離された場合は入力と見做さない
    
    ; キーの番号からASCIIコードへ変換していく
    cmp al, 0x1E      ; 'a'
    je .a
    cmp al, 0x30      ; 'b'
    je .b
    cmp al, 0x2E      ; 'c'
    je .c
    cmp al, 0x20      ; 'd'
    je .d
    cmp al, 0x12      ; 'e'
    je .e
    cmp al, 0x21      ; 'f'
    je .f
    cmp al, 0x22      ; 'g'
    je .g
    cmp al, 0x23      ; 'h'
    je .h
    cmp al, 0x17      ; 'i'
    je .i
    cmp al, 0x24      ; 'j'
    je .j
    cmp al, 0x25      ; 'k'
    je .k
    cmp al, 0x26      ; 'l'
    je .l
    cmp al, 0x32      ; 'm'
    je .m
    cmp al, 0x31      ; 'n'
    je .n
    cmp al, 0x18      ; 'o'
    je .o
    cmp al, 0x19      ; 'p'
    je .p
    cmp al, 0x10      ; 'q'
    je .q
    cmp al, 0x13      ; 'r'
    je .r
    cmp al, 0x1F      ; 's'
    je .s
    cmp al, 0x14      ; 't'
    je .t
    cmp al, 0x16      ; 'u'
    je .u
    cmp al, 0x2F      ; 'v'
    je .v
    cmp al, 0x11      ; 'w'
    je .w
    cmp al, 0x2D      ; 'x'
    je .x
    cmp al, 0x15      ; 'y'
    je .y
    cmp al, 0x2C      ; 'z'
    je .z
    cmp al, 0x1C      ; Enter
    je .enter
    
    xor al, al        ; 上記以外のキーは無効(0を返す)
    ret
    
.a:
    mov al, 'a'
    ret
.b:
    mov al, 'b'
    ret
.c:
    mov al, 'c'
    ret
.d:
    mov al, 'd'
    ret
.e:
    mov al, 'e'
    ret
.f:
    mov al, 'f'
    ret
.g:
    mov al, 'g'
    ret
.h:
    mov al, 'h'
    ret
.i:
    mov al, 'i'
    ret
.j:
    mov al, 'j'
    ret
.k:
    mov al, 'k'
    ret
.l:
    mov al, 'l'
    ret
.m:
    mov al, 'm'
    ret
.n:
    mov al, 'n'
    ret
.o:
    mov al, 'o'
    ret
.p:
    mov al, 'p'
    ret
.q:
    mov al, 'q'
    ret
.r:
    mov al, 'r'
    ret
.s:
    mov al, 's'
    ret
.t:
    mov al, 't'
    ret
.u:
    mov al, 'u'
    ret
.v:
    mov al, 'v'
    ret
.w:
    mov al, 'w'
    ret
.x:
    mov al, 'x'
    ret
.y:
    mov al, 'y'
    ret
.z:
    mov al, 'z'
    ret
.enter:
    mov al, 0x0D      ; EnterのASCIIコード(CR)
    ret
vga.asm
vga.asm
;------------------------------------------------------------
; VGAカーソル位置をI/Oポートから取得
; 結果は文字単位で cursor_pos に格納
; カーソル位置を取得しないとBIOS呼び出しを使って表示した文字が上にあった場合に
; 既に表示された文字の上から上書きしてしまいます
;------------------------------------------------------------
get_cursor_position:
    ; 下位バイト(カーソル位置のLSB)
    mov dx, 0x3D4
    mov al, 0x0F
    out dx, al
    inc dx        ; DX = 0x3D5
    in al, dx
    mov bl, al    ; BL = LSB

    ; 上位バイト(カーソル位置のMSB)
    mov dx, 0x3D4
    mov al, 0x0E
    out dx, al
    inc dx
    in al, dx
    mov bh, al    ; BH = MSB

    ; BX = カーソル位置(文字単位)
    mov [cursor_pos], bx
    ret
    
;------------------------------------------------------------
; ハードウェアカーソルを更新(VGAポートに書き込み)
; 入力:BX = 新しいカーソル位置(文字単位)
;------------------------------------------------------------
update_hardware_cursor:
    push ax
    push dx

    ; 下位バイト(0x0F)を設定
    mov dx, 0x3D4
    mov al, 0x0F
    out dx, al
    inc dx              ; DX = 0x3D5
    mov al, bl          ; 下位バイト(BXのLSB)
    out dx, al

    ; 上位バイト(0x0E)を設定
    dec dx              ; DX = 0x3D4
    mov al, 0x0E
    out dx, al
    inc dx              ; DX = 0x3D5
    mov al, bh          ; 上位バイト(BXのMSB)
    out dx, al

    pop dx
    pop ax
    ret

;------------------------------------------------------------
; VRAMへ文字列表示(改行対応、BIOSなし)
;------------------------------------------------------------
print_string:
    pusha
    push si
    
    ; カーソル位置を取得(BIOSなし)
    call get_cursor_position
    
    ; VRAMのセグメント B800h をESに設定
    mov ax, 0xB800
    mov es, ax

    ; カーソル位置を読み込んで、バイト単位に変換
    mov di, [cursor_pos]
    shl di, 1

.next_char:
    lodsb ; AL ← [DS:SI]
    or al, al ; 終端文字0なら終了
    jz .done

    cmp al, 0x0D        ; CR(行頭に戻る)
    je .carriage_return
    cmp al, 0x0A        ; LF(次の行)
    je .line_feed

    ; 通常文字出力
    mov [es:di], al
    mov byte [es:di+1], 0x0A  ; 緑文字
    add di, 2
    
    ; スクロール判定(80×25×2 = 4000バイト)
    cmp di, 4000
    jl .next_char
    call scroll_screen
    ; scroll後は最下行に戻す
    ; mov di, 80 * 24 * 2
    mov di, 80 * 24
    shl di, 1
    jmp .next_char

.carriage_return:
    mov ax, di
    shr ax, 1
    xor dx, dx
    mov bx, 80
    div bx        ; AX = 行番号, DX = 列番号
    mul bx        ; 行頭へ
    shl ax, 1
    mov di, ax
    jmp .next_char

.line_feed:
    add di, 160         ; 次の行へ(80文字×2バイト)
    
    ; スクロール判定
    cmp di, 4000
    jl .next_char
    call scroll_screen
    mov di, 80 * 24 * 2
    
    jmp .next_char

.done:
    shr di, 1
    mov [cursor_pos], di
    mov bx, di            ; BXにカーソル位置(文字単位)を設定
    call update_hardware_cursor  ; ハードウェアカーソル移動
    pop si
    popa
    ret

;------------------------------------------------------------
; putchar_direct: ALの文字をVRAMに直接書き込む
;------------------------------------------------------------
putchar_direct:
    pusha

    ; カーソル位置(文字単位)をDIに
    mov di, [cursor_pos]
    shl di, 1          ; DI *= 2(バイト単位に)

    ; セグメント ES に VRAM の 0xB800 を設定(ここでAXを退避せずAL破壊してどハマりした)
    push ax 
    mov ax, 0xB800
    mov es, ax
    pop ax

    ; 文字と属性を書き込む
    mov [es:di], al          ; 文字
    mov byte [es:di+1], 0x0A ; 属性(緑)

    ; カーソルを1文字進める(これがないと入力する度前の文字を上書きする)
    add di, 2
    
    shr di, 1
    mov [cursor_pos], di

    ; カーソル表示も更新
    mov bx, di
    call update_hardware_cursor

    popa
    ret
    
;------------------------------------------------------------
; 画面を1行スクロール(上に1行詰めて最下行を空白に)
;------------------------------------------------------------
scroll_screen:
    pusha
    push es
    push ds

    ; ES = VRAM (0xB800)
    mov ax, 0xB800
    mov es, ax
    mov ds, ax          ; コピー元にも同じセグメント使う(安全のため)

    ; SI = 行1の先頭(2行目) = 1行160バイト(80文字×2byte 文字1byte 色1bite)
    mov si, 160

    ; DI = 行0の先頭(1行目)
    mov di, 0

    ; CX = 24行 × 80文字 = 1920文字(3840バイト)
    mov cx, 80 * 24 ; (ここは25にしてはいけない)

.copy_loop:
    mov ax, [ds:si]
    mov [es:di], ax
    add si, 2
    add di, 2
    loop .copy_loop

    ; 最下行(25行目 = 行24)の初期化
    ; DI は現在 3840バイト目(最下行の開始位置)
    mov cx, 80
    mov ax, 0x0720         ; 空白 + 属性(黒背景+灰色文字)

.clear_last_line:
    mov [es:di], ax
    add di, 2
    loop .clear_last_line

    ; カーソル位置を1行上に(80文字 = 1行)
    ; sub word [cursor_pos], 80とすると画面の1行目に戻ってしまったので減算ではなくべた書き
    mov word [cursor_pos], 80 * 24  ; 行番号24× 80列 = 1920文字
    mov bx, 80 * 24
    call update_hardware_cursor

    pop ds
    pop es
    popa
    ret

cursor_pos: dw 0

原因解明

keyboardの中でget_keyを呼び出し、[ES:DI]経由でinput_bufferへ1文字ずつ保存している。
元々はES=0x0000としていたはずが、putchar_directの内部でESをVRAMの0xB800へ設定し、その後復元していなかった為最初の一回しか正常に保存ができていなかった。

keyboard.asm
.read_char:
    ; 一文字読み込み(結果はASCIIコードでALへ格納される)
    ; mov ah, 0x00
    ; int 0x16
    call get_key
    
... 省略 ...
    
    ; 入力に問題が無い場合は入力バッファへ格納
    stosb    ; AL の文字を [ES:DI] に格納(バッファに保存)、DI++(次の位置へ)
    inc cx   ; 入力文字数+1
    
    call putchar_direct ; 表示する文字はALに入っている
    jmp .read_char
vga.asm
putchar_direct:
    pusha
    push es ; 今回追加

    ; カーソル位置(文字単位)をDIに
    mov di, [cursor_pos]
    shl di, 1          ; DI *= 2(バイト単位に)

    ; セグメント ES に VRAM の 0xB800 を設定(ここでAXを退避せずAL破壊してどハマりした)
    push ax 
    mov ax, 0xB800
    mov es, ax
    pop ax

... 省略 ...
    pop es ; 今回追加
    popa
    ret

图片.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?