Table of Content

Experiment and Programming Environment

Refer to the bootstrap tutorial for the experiment and programming environment, and for how to compile and run the programs here.

Accessing I/O Device Controllers

There are three methods to access memory and registers in I/O device controllers, i.e.,

For examples using the I/O port method, please check out the examples in Section I/O Schemes. For the example using memory-mapped I/O, see the display example.

Example 1 Displaying Text via Accessing VGA Memory

;
; disp0.asm
;
; VGA
;   https://en.wikipedia.org/wiki/VGA-compatible_text_mode
;   https://en.wikipedia.org/wiki/Video_Graphics_Array#Color_palette

[org 0x7c00]
mov ax, 0xb800
mov es, ax

mov byte [es:0], 'H'
mov byte [es:1], 0x01

mov byte [es:2], 'e'
mov byte [es:3], 0x02

mov byte [es:4], 'l'
mov byte [es:5], 0x03

mov byte [es:6], 'l'
mov byte [es:7], 0x04

mov byte [es:8], 'o'
mov byte [es:9], 0x05

mov byte [es:10], ','
mov byte [es:11], 0x06

mov byte [es:12], 'W'
mov byte [es:13], 0x17

mov byte [es:14], 'o'
mov byte [es:15], 0x28

mov byte [es:16], 'r'
mov byte [es:17], 0x39

mov byte [es:18], 'l'
mov byte [es:19], 0x4a

mov byte [es:20], 'd'
mov byte [es:21], 0x5b

mov byte [es:22], '!'
mov byte [es:23], 0x6c

jmp $

times 510-($-$$) db 0
dw 0xaa55

I/O Schemes

We divide the pattern to access the device controller into 3 I/O schemes,

Here we present examples for Programmed I/O. These examples use the subroutines defined in three other .asm files, print_line.asm, print_space.asm, and print_byte.asm whose source code we list below examples.

Example 2 Polled I/O for PS/2 Keyboard

;
; kbdpoll0.asm
;
; http://www-ug.eecg.toronto.edu/msl/nios_devices/datasheets/PS2%20Keyboard%20Protocol.htm
; http://bochs.sourceforge.net/techspec/PORTS.LST

; disable keyboard interrupt
mov al, 0x02
out 21h, al

mov cx, 0
KBD_READ:
    push cx 
LOOP:
    ; query keybard status
    in al, 0x64
    mov cl, al
    and al, 0x01
    jz LOOP

    ; read scan code
    in al, 0x60

    ; print scan code
    ; for PS/2 scan code table, see
    ;   http://www.vetra.com/scancodes.html
    mov cl, al
    call print_byte
    call print_space

    pop cx
    inc cx
    and cx, 0x01
    jnz KBD_READ
    call print_line

    jmp KBD_READ


jmp $

%include "print_byte.asm"
%include "print_space.asm"
%include "print_line.asm"
times 510-($-$$) db 0
dw 0xaa55

Example 3 Interrupted I/O for PS/2 Keyboard

;
; kbdint0.asm
;
[org 0x7c00]

; Keybaord interrupt is maskable. To Intel 8259 Programmable Interrupt
; Controller, the IRQ number of the keyboard interrupt is 1, which
; means if we were to mask the interrupt, we set the 1st bit of the
; mask (a byte) as 1, i.e, 0x02 (0000 0010 in binary). 
;
; For interrupt request number assignment on PC, see 
; https://en.wikibooks.org/wiki/X86_Assembly/Programmable_Interrupt_Controller
;
; Uncomment the following two statements, and run the code. Does
; the keyboard interrupt till work? why? 
; 
; mov al, 0x02
; out 21h, al

jmp START

KBD_ISR:
    push ax
    push cx
    ; read scan code
    in al, 0x60
    mov cl, al
    call print_byte
    call print_space

    mov byte ah, [line_counter]
    inc ah
    mov byte [line_counter], ah
    and ah, 0x01
    jz LINE_BREAK
    jmp EXIT_ISR

LINE_BREAK:
    call print_line

EXIT_ISR:
    pop cx
    pop ax
    iret
    
%include "print_byte.asm"
%include "print_space.asm"
%include "print_line.asm"

line_counter:
    db 0


START:
    ; Set interrupt number 0x16 (or 21 in decimal)'s interrupt
    ; service routine as ours. For interrupt 0x16, see
    ;   https://en.wikipedia.org/wiki/INT_16H
    cli
    cld
    mov ax, 0
    mov es, ax
    mov ax, KBD_ISR
    mov [es:21*4], ax
    mov [es:21*4+2], cs
    sti

    jmp $


POS:
    db 0, 0, 0, 0

times 510-($-$$) db 0
dw 0xaa55

Example 4 DMA I/O

; ============================================================
; dma0.asm
;
; Boot from hard disk.
; Read floppy LBA 0 using DMA channel 2.
; DMA destination: 0000:8000
;
; Display first 16 bytes in hexadecimal.
; Display first 32 bytes as ASCII.
;
; Uses the course routines:
;   print_byte.asm
;   print_space.asm
;   print_line.asm
;
; Assemble:
;   nasm -f bin dma0.asm -o dma0.bin
;
; ============================================================

[org 0x7c00]


; ============================================================
; Hardware ports
; ============================================================

FDC_DOR     equ 0x3f2
FDC_MSR     equ 0x3f4
FDC_FIFO    equ 0x3f5

DMA_STAT    equ 0x08
DMA_MASK    equ 0x0a
DMA_MODE    equ 0x0b
DMA_FF      equ 0x0c
DMA_ADDR    equ 0x04
DMA_COUNT   equ 0x05
DMA_PAGE    equ 0x81

BUFFER      equ 0x8000


; ============================================================
; Start
; ============================================================

START:

    cli

    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7c00

    sti


; ============================================================
; Print startup message
; ============================================================

    mov si, MSG
    call print_string


; ============================================================
; Turn on floppy drive 0
;
; DOR = 3F2h
;
; 1Ch:
;   bit 4 = motor 0 ON
;   bit 3 = DMA/IRQ enable
;   bit 2 = controller enable
;   bit 1-0 = drive 0
; ============================================================

    mov dx, FDC_DOR
    mov al, 0x1c
    out dx, al

    call DELAY


; ============================================================
; Program DMA channel 2
;
; Destination = 0000:8000
; Count       = 511 = 512 bytes
; Direction   = device -> memory
; ============================================================

    ; Mask channel 2

    mov dx, DMA_MASK
    mov al, 0x06
    out dx, al


    ; Reset DMA flip-flop

    mov dx, DMA_FF
    xor al, al
    out dx, al


    ; Address = 8000h

    mov dx, DMA_ADDR

    mov al, 0x00
    out dx, al

    mov al, 0x80
    out dx, al


    ; Page = 00h

    mov dx, DMA_PAGE
    xor al, al
    out dx, al


    ; Reset DMA flip-flop

    mov dx, DMA_FF
    xor al, al
    out dx, al


    ; Count = 01FFh
    ; 01FFh + 1 = 512 bytes

    mov dx, DMA_COUNT

    mov al, 0xff
    out dx, al

    mov al, 0x01
    out dx, al


    ; DMA mode = 46h
    ;
    ; single transfer
    ; address increment
    ; device -> memory
    ; channel 2

    mov dx, DMA_MODE
    mov al, 0x46
    out dx, al


    ; Unmask channel 2

    mov dx, DMA_MASK
    mov al, 0x02
    out dx, al


; ============================================================
; FDC READ DATA
;
; Read floppy CHS 0/0/1.
; This is floppy LBA 0.
; ============================================================

    call FDC_WRITE

    mov dx, FDC_FIFO


    ; READ DATA + MFM

    mov al, 0x46
    out dx, al


    ; Drive 0, head 0

    xor al, al
    out dx, al


    ; Cylinder 0

    xor al, al
    out dx, al


    ; Head 0

    xor al, al
    out dx, al


    ; Sector 1

    mov al, 0x01
    out dx, al


    ; N = 2 -> 512 bytes

    mov al, 0x02
    out dx, al


    ; End of track = 18

    mov al, 0x12
    out dx, al


    ; GAP3

    mov al, 0x1b
    out dx, al


    ; DTL

    mov al, 0xff
    out dx, al


; ============================================================
; Wait for DMA channel 2 terminal count
;
; DMA status port = 08h
; Channel 2 TC = bit 2
; ============================================================

WAIT_DMA:

    mov dx, DMA_STAT
    in al, dx

    test al, 0x04
    jz WAIT_DMA


; ============================================================
; Read and discard FDC result bytes
;
; ST0 ST1 ST2 C H R N
; ============================================================

    call FDC_READ

    mov cx, 7

READ_RESULT:

    in al, dx
    loop READ_RESULT


; ============================================================
; Print first 16 bytes as hexadecimal
; Same technique as kbdpoll0.asm:
; save CX because CL is used by print_byte.
; ============================================================

    mov si, BUFFER
    mov cx, 16

DISP_HEX:

    push cx

    mov cl, [si]
    call print_byte
    call print_space

    inc si

    pop cx
    loop DISP_HEX

    call print_line


; ============================================================
; Print first 32 bytes as ASCII
; ============================================================

    mov si, MSG_TEXT
    call print_string

    mov si, BUFFER
    mov cx, 32

DISP_ASCII:

    mov al, [si]

    cmp al, 0x20
    jb NOT_PRINTABLE

    cmp al, 0x7e
    ja NOT_PRINTABLE

    jmp short DISP_CHAR_ASCII

NOT_PRINTABLE:

    mov al, '.'

DISP_CHAR_ASCII:

    mov ah, 0x0e
    int 0x10

    inc si
    loop DISP_ASCII

    call print_line

; ============================================================
; Stop
; ============================================================

DONE:

    cli
    hlt
    jmp DONE


; ============================================================
; FDC command phase
;
; Wait for:
;   RQM = 1
;   DIO = 0
; ============================================================

FDC_WRITE:

    mov dx, FDC_MSR

FDC_WRITE_WAIT:

    in al, dx

    test al, 0x80
    jz FDC_WRITE_WAIT

    test al, 0x40
    jnz FDC_WRITE_WAIT

    mov dx, FDC_FIFO
    ret


; ============================================================
; FDC result phase
;
; Wait for:
;   RQM = 1
;   DIO = 1
; ============================================================

FDC_READ:

    mov dx, FDC_MSR

FDC_READ_WAIT:

    in al, dx

    test al, 0x80
    jz FDC_READ_WAIT

    test al, 0x40
    jz FDC_READ_WAIT

    mov dx, FDC_FIFO
    ret


; ============================================================
; Print zero-terminated string DS:SI
;
; We keep this small local routine because the course routines
; supplied here are specifically byte/space/line routines.
; ============================================================

print_string:

    lodsb

    test al, al
    jz DISP_STRING_DONE

    mov ah, 0x0e
    int 0x10

    jmp print_string


DISP_STRING_DONE:

    ret


; ============================================================
; Floppy motor startup delay
; ============================================================

DELAY:

    mov cx, 0xffff

DELAY_LOOP:

    loop DELAY_LOOP

    ret


; ============================================================
; Messages
; ============================================================

MSG:

    db 13,10
    db 'Booted from hard disk.',13,10
    db 'Reading floppy LBA 0 using DMA...',13,10
    db 0


MSG_HEX:

    db 'Hex: ',0


MSG_TEXT:

    db 13,10
    db 'Text: ',0


; ============================================================
; Course routines
;
; IMPORTANT:
; These must come AFTER the boot code because BIOS begins
; execution at START (offset 0x7c00).
; ============================================================

%include "print_byte.asm"
%include "print_space.asm"
%include "print_line.asm"


; ============================================================
; Boot-sector padding and signature
; ============================================================

times 510-($-$$) db 0

dw 0xaa55

To run the program, follow the steps illustrated in the shell script below

#!/bin/bash

# ============================================================
# run-dma.sh
#
# DMA demonstration:
#
#   Boot from hard disk
#   Read first sector of floppy using DMA
#
# Files:
#
#   dma1.asm        Boot-sector source
#   dma1.bin        Assembled boot sector
#   dma-hd.img      Hard-disk image
#   dma-floppy.img  Floppy image
#
# ============================================================

set -e

ASM="dma0.asm"
BIN="dma0.bin"
HDIMG="dma-hd.img"
FDIMG="dma-floppy.img"


# ============================================================
# Check dependencies
# ============================================================

if ! command -v nasm >/dev/null 2>&1; then
    echo "ERROR: nasm is not installed."
    exit 1
fi

if ! command -v qemu-system-i386 >/dev/null 2>&1; then
    echo "ERROR: qemu-system-i386 is not installed."
    exit 1
fi


# ============================================================
# Assemble boot sector
# ============================================================

echo "[1/5] Assembling $ASM..."

nasm -f bin "$ASM" -o "$BIN"


# ============================================================
# Check boot-sector size
# ============================================================

SIZE=$(stat -c%s "$BIN" 2>/dev/null || stat -f%z "$BIN")

if [ "$SIZE" -ne 512 ]; then
    echo "ERROR: $BIN is $SIZE bytes."
    echo "       A boot sector must be exactly 512 bytes."
    exit 1
fi

echo "      $BIN = 512 bytes"


# ============================================================
# Create hard-disk image
#
# For this simple example we create a 1.44 MB raw image.
#
# The BIOS will boot from sector 0.
# ============================================================

echo "[2/5] Creating hard-disk image..."

rm -f "$HDIMG"

dd if=/dev/zero \
   of="$HDIMG" \
   bs=512 \
   count=2880 \
   status=none


# ============================================================
# Install boot sector into hard disk
# ============================================================

echo "[3/5] Installing boot sector..."

dd if="$BIN" \
   of="$HDIMG" \
   bs=512 \
   count=1 \
   conv=notrunc \
   status=none


# ============================================================
# Create floppy image
#
# 1.44 MB floppy:
#
#   80 cylinders
#   2 heads
#   18 sectors/track
#   512 bytes/sector
#
# Total:
#
#   80 * 2 * 18 * 512 = 1,474,560 bytes
# ============================================================

echo "[4/5] Creating floppy image..."

rm -f "$FDIMG"

dd if=/dev/zero \
   of="$FDIMG" \
   bs=512 \
   count=2880 \
   status=none


# ============================================================
# Put recognizable data in FIRST floppy sector.
#
# The assembly program reads:
#
#   cylinder = 0
#   head     = 0
#   sector   = 1
#
# This is LBA 0.
# ============================================================

printf 'DMA FLOPPY SECTOR 0 - Hello from the floppy!\n' \
    | dd of="$FDIMG" \
       bs=512 \
       count=1 \
       conv=notrunc \
       status=none


# ============================================================
# Show image information
# ============================================================

echo
echo "Images created:"
echo

ls -lh "$HDIMG" "$FDIMG"

echo


# ============================================================
# Run QEMU
#
# dma-hd.img:
#       hard disk
#
# dma-floppy.img:
#       floppy drive A:
#
# -boot c:
#       boot from hard disk
# ============================================================

echo "[5/5] Starting QEMU..."
echo
echo "      Boot device : hard disk"
echo "      DMA source  : floppy"
echo "      DMA buffer  : 0000:8000"
echo

qemu-system-i386 \
    -drive file="$HDIMG",format=raw,if=ide \
    -drive file="$FDIMG",format=raw,if=floppy \
    -boot c \
    -display curses \
    -monitor telnet:127.0.0.1:54321,server,nowait

Required Subroutines

;
; print_byte.asm
;
; print a byte in register cl to hex assuming ASCII encoding
;    void * print_byte(char c) 
print_byte:
    pusha ; Push AX, CX, DX, BX, original SP, BP, SI, and DI.

    mov ch, cl
    shr ch, 4
    and ch, 0x0f

    push cx
    mov cl, ch
    call print_low_4bits
    pop cx

    mov ch, cl
    and ch, 0x0f

    push cx
    mov  cl, ch
    call print_low_4bits
    pop cx

    popa
    ret


; print lower 4 bits in register cl to hex assuming ASCII encoding
;    void * print_byte(char c) 
print_low_4bits:
    pusha
    mov ah, 0x0e

    mov ch, cl
    and ch, 0x0f
    cmp ch, 0x09
    jg A_TO_F

    add ch, '0'
    jmp PRINT_CHAR

A_TO_F:
    sub ch, 0x0a
    add ch, 'A'
    jmp PRINT_CHAR

PRINT_CHAR:
    mov al, ch
    int 0x10

    popa
    ret
;
; print_space.asm
;
print_space:
    pusha

    mov ah, 0x0e
    mov al, ' '
    int 0x10

    popa
    ret
;
; print_line.asm
;
print_line:
    pusha

    ; line feed    
    mov ah, 0x0e
    mov al, 0x0a
    int 0x10

    ; carriage return
    mov ah, 0x0e
    mov al, 0x0d
    int 0x10

    popa
    ret