org 0x7C00 ; The boot code on a drive always gets loaded at address 0x7C00 bits 16 ; .bin defaults to 16 bits WIDTH equ 0x4F ; 79 HEIGHT equ 0x18 ; 24 VIDEO_MODE equ 0x3 ; 80x25 screen, text mode PIT_MODECOMMAND_PORT equ 0x43 ; programmable interval timer mode/command port (used for speaker) PIT_CH2_DATA_PORT equ 0x42 ; PIT channel 2 data port VIRTUAL_KBD_CHAR equ 0x09 ; character to detect if a command is comming from the virtual keyboard (htab) ;; see http://www.ctyme.com/intr/int-10.htm for interrupts start: mov sp, 0x2000 ; initialise stack pointer ;; Interrupt 0x14, ah=0 ;; bit 7 - 5 baud rate ;; 0 0 0 110 ;; 0 0 1 150 ;; 0 1 0 300 ;; 0 1 1 600 ;; 1 0 0 1200 ;; 1 0 1 2400 ;; 1 1 0 4800 ;; 1 1 1 9600 (used) ;; bit 4 - 3 parity ;; 0 0 none (used) ;; 0 1 odd ;; 1 1 even ;; bit 2 stop bits ;; 0 1 ;; 1 2 ;; bit 1 - 0 data bits ;; 1 0 7 ;; 1 1 8 ;; initialise serial port mov ah, 0x0 ; initialise UART mov al, 0b111_00_0_11 ; baud 9600, no parity, 1 stop bit, 8 data bits mov dx, 0x0 ; COM1 int 0x14 ;; Use color mode mov ax, VIDEO_MODE int 0x10 ;; Clear screen mov ah, 0x06 ; scroll screen up mov al, 0x00 ; lines to scroll (0 = clear) mov bh, 0x0F ; bg/fg colour mov cx, 0x0000 ; upper row number, left col number (both 0) mov dh, HEIGHT ; lower row number (24) mov dl, WIDTH ; right col number (80) int 0x10 ;; Set cursor to bottom of screen ;; dh already contains HEIGHT mov ah, 0x02 ; set cursor position mov bh, 0x00 ; page number mov dl, 0x00 ; col (leftmost col) int 0x10 .loop: ;; read a character, ASCII in al mov ah, 0x00 ; Read character int 0x16 ;; a special character will be sent before every command from the virtual keyboard ;; so if that character is detected go to the virtual_kbd_loop instead cmp al, VIRTUAL_KBD_CHAR je .virtual_kbd_loop_init cmp al, 0x08 ; backspace je change_color_mode cmp al, 0x1B ; esc je beep ;; print character to serial port mov ah, 0x01 ; transmit char mov dx, 0x00 ; COM1 int 0x14 cmp al, 0x0D ; enter (carriage return) je .nl_lf jmp nonewline ; all other characters .nl_reset_state: mov byte[is_from_virtual_kbd], 0x0 ; reset state variable jmp .nl ;; BIOS keyboard routines read as CR which is usually not recognized ;; as actually beginning a new line, so anytime a CR is sent a LF is sent with ;; it .nl_lf: mov al, 0x0A ; line feed mov ah, 0x01 ; transmit char int 0x14 .nl: call newline jmp .loop .virtual_kbd_loop_init: mov byte[is_from_virtual_kbd], 0x1 ; update state variable ;; same as regular .loop except nothing is sent to serial .virtual_kbd_loop: ;; read a character, ASCII in al mov ah, 0x00 int 0x16 cmp al, 0x08 ; backspace je change_color_mode cmp al, 0x1B ; esc je beep ;; an enter character signifies the end of a command ;; so a newline should get printed and then the program should resume ;; from the main .loop again cmp al, 0x0D ; enter je .nl_reset_state jmp nonewline ; all other characters ;; Update the color_mode variable with a byte from the keyboard ;; ;; CLOBBERS ;; - ax ;; - bl change_color_mode: call readbyte ; get byte from keyboard (in al) mov [color_mode], al ; store byte in color_mode variable jmp compare_virtual_and_jump ;; Simulates going to a new line ;; ;; CLOBBERS ;; - ax ;; - bh ;; - cx ;; - dx newline: ;; Scroll up window mov ah, 0x06 ; scroll screen up mov al, 0x01 ; lines to scroll (1) mov bh, 0x0F ; bg/fg colour mov cx, 0x0000 ; upper row num/left col num (both 0) mov dh, HEIGHT ; lower row num (24) mov dl, WIDTH ; right col num (80) int 0x10 ;; Move cursor to beginning of screen ;; dh already contains HEIGHT mov ah, 0x02 ; set cursors position mov bh, 0x00 ; page number mov dl, 0x00 ; col int 0x10 ret ;; Plays a note on the speaker for a given duration ;; ;; CLOBBERS ;; - ax ;; - bx ;; - cx beep: ;; Read duration (0-16 supported) mov ah, 0x00 ; read keypress int 0x16 ;; Read ASCII character is in al ;; the character must be normalised to a=1, b=2, ... ;; and not a=0, b=1, ... because during the waiting loop ;; see #NOTE_LOOP ;; the duration counter gets decremented BEFORE ;; it gets compared to zero ;; ('a' is 0x61) sub al, 0x60 ; normalise (a=1, b=2, c=...) ;; interrupt 0x16, ah=0 returns the read scancode in ah ;; so ah must be reset mov ah, 0 mov cx, ax ; save ax into cx temporarily ;; move 182 (0b10 11 011 0) into al ;; bit 7-6 = channel select (2, PC speaker) ;; bit 5-4 = access mode (lobyte/hibyte) ;; bit 3-1 = operating mode (square wave generator) ;; bit 0 = BCD/binary mode (16-bit binary) mov al, 0b10_11_011_0 out PIT_MODECOMMAND_PORT, al ; write to PIT control port ;; Read byte and convert to a usable frequency call readbyte ; read byte from keyboard (in al) mov ah, 0 ; readbyte clobbers ax sal ax, 5 ; multiply by 32 add ax, 1140 out PIT_CH2_DATA_PORT, al ; Output low byte. mov al, ah out PIT_CH2_DATA_PORT, al ; Output high byte ;; Port 0x61 does a whole lot but only the bottom 2 bits are relevant here ;; (speaker enable and speaker input select) in al, 0x61 ; Preserve previous value or al, 00000011b ; Set bits 1 and 0 (enable speaker, input = channel 2) out 0x61, al ; Send new value ;; mov bx, 50000 ; Pause for duration of note. ;; multiply duration by 3125 ;; (duration is in cx) mov ax, 3125 mul cx ; multiply ax <- ax * cx ;; play note for a certain duration .pause1: mov ecx, 65535 .pause2: dec cx ; count down jne .pause2 ; if not zero keep counting down ;; #NOTE_LOOP ;; the duration gets decremented before being checked ;; hence why it was normalised to '`' and not 'a' dec ax ; decrement duration counter jne .pause1 ; keep doing that until it is 0 in al, 0x61 ; Preserve previous value and al, 11111100b ; Clear bits 1 and 0 (disable speaker, clear channel select) out 0x61, al ; Send new value jmp compare_virtual_and_jump ;; Handles all other characters by writing them to the screen ;; ;; CLOBBERS ;; - ah ;; - bx ;; - cx ;; - dx nonewline: ;; Write character mov ah, 0x09 ; write character with attribute mov bh, 0x00 ; page number mov bl, [color_mode] ; BIOS colour code mov cx, 0x01 ; amount of times to print character int 0x10 ;; Get current cursor position ;; bh already contains page number ;; returns dh = row, dl = col mov ah, 0x03 ; get cursor position and shape int 0x10 ;; if the cursor is at the edge of the screen go to the next line ;; else move it forward once cmp dl, WIDTH jge .eol jmp .noeol .eol: call newline jmp compare_virtual_and_jump .noeol: ;; Move cursor forward ;; bh already contains page number ;; dh already contains row inc dl ; go to next col mov ah, 0x02 ; set cursor position, dh (row) already set, dl (col) updated on prev line int 0x10 jmp compare_virtual_and_jump ;; Compares the is_from_virtual_kbd state variable to 1 to check if the command ;; came from the virtual keyboard ;; if it did, jump to the virtual keyboard loop ;; if it didn't, jump to the main loop compare_virtual_and_jump: cmp byte[is_from_virtual_kbd], 0x01 je start.virtual_kbd_loop jmp start.loop ;; Reads two characters from the keyboard buffer and converts them into a single byte ;; ;; The conversion happens as follows: ;; ASCII 'a' is subtracted from each character (so 'a' = 0, 'b' = 1, ...) ;; the first character is shifted left by 4 ;; the shifted and non-shifted values are then or'ed together to form a byte ;; ;; RETURNS ;; A byte in al ;; ;; CLOBBERS ;; - ax ;; - bl readbyte: mov ah, 0x00 ; Read character int 0x16 ;; Read character is in al sub al, 'a' ; Normalise mov bl, al shl bl, 0x04 ; First character is upper 4 bits mov ah, 0x00 ; Read character int 0x16 ;; Read character is in al sub al, 'a' ; Normalise or al, bl ; Combine both characters ret ;; Reads a duration from the keyboard ;; ;; RETURNS ;; A duration in al ;; ;; CLOBBERS ;; - ax ;read_duration: ; ;; Read duration (0-16 supported) ; mov ah, 0x00 ; read keypress ; int 0x16 ; ; ;; Read character is in al ; sub al, 'a' ; normalise ; ; ret ;; Variable to store color mode information for writing to the screen color_mode: db 0x02 ;; State variable for detecting whether keystrokes are virtual or real is_from_virtual_kbd: db 0x00 ;; Magic number must be in the last 2 bytes of the sector so ;; fill the binary with zeroes until it is 510 bytes in size ;; (magic number will use up the remaining 2) times 510 - ($ - $$) db 0 dw 0xAA55 ;; Magic number to mark sector as bootable