1368 lines
30 KiB
NASM
1368 lines
30 KiB
NASM
;
|
|
; BIOS Extension ROM for XTMax SD Card support.
|
|
; Copyright (c) 2025 Matthieu Bucchianeri, All rights reserved.
|
|
;
|
|
|
|
bits 16
|
|
cpu 8086 ; ensure we remain compatible with 8086
|
|
|
|
|
|
;%define DEBUG
|
|
;%define DEBUG_IO
|
|
;%define EXTRA_DEBUG
|
|
|
|
;
|
|
; The base I/O port for the XTMAX SD Card.
|
|
;
|
|
%define XTMAX_IO_BASE (0x280)
|
|
%define REG_DATA (XTMAX_IO_BASE+0)
|
|
%define REG_CS (XTMAX_IO_BASE+1) ; write-only
|
|
%define REG_CONFIG (XTMAX_IO_BASE+1) ; read-only
|
|
%define REG_SCRATCH_0 (XTMAX_IO_BASE+2) ; fixed disk id
|
|
%define REG_SCRATCH_1 (XTMAX_IO_BASE+3) ; BIOS INT13h segment
|
|
%define REG_SCRATCH_2 (XTMAX_IO_BASE+4) ; BIOS INT13h segment
|
|
%define REG_SCRATCH_3 (XTMAX_IO_BASE+5) ; BIOS INT13h offset
|
|
%define REG_SCRATCH_4 (XTMAX_IO_BASE+6) ; BIOS INT13h offset
|
|
%define REG_TIMEOUT (XTMAX_IO_BASE+7)
|
|
|
|
;
|
|
; Whether we will try to use our own bootstrap code.
|
|
;
|
|
%define USE_BOOTSTRAP
|
|
|
|
;
|
|
; Whether we are going to rename the BIOS's 1st disk to be the second disk.
|
|
; This is useful to allow booting from the second disk.
|
|
;
|
|
;%define TAKE_OVER_FIXED_DISK_0
|
|
|
|
;
|
|
; The properties of our emulated disk.
|
|
;
|
|
%define NUM_CYLINDERS (1024)
|
|
%define NUM_HEADS (255)
|
|
%define SECTORS_PER_TRACK (63)
|
|
; the last cylinder is reserved on fixed disks
|
|
%define NUM_SECTORS (NUM_HEADS * (NUM_CYLINDERS - 1) * SECTORS_PER_TRACK)
|
|
|
|
%if NUM_HEADS > 255
|
|
%error NUM_HEADS_ is too large
|
|
%endif
|
|
%if NUM_HEADS > 16
|
|
%warning NUM_HEADS_ above 16 can cause compatibility issues
|
|
%endif
|
|
%if SECTORS_PER_TRACK > 63
|
|
%error SECTORS_PER_TRACK_ is too large
|
|
%endif
|
|
%if NUM_CYLINDERS > 1024
|
|
%error NUM_CYLINDERS_ is too large
|
|
%endif
|
|
|
|
beginning_of_rom:
|
|
%ifndef AS_COM_PROGRAM
|
|
;
|
|
; BIOS will look for the AA55 signature between C8000-DFFFF in 2KB increments.
|
|
; We choose an address in that range.
|
|
;
|
|
%define ROM_SEGMENT (0xce00)
|
|
org 0
|
|
dw 0AA55h ; signature
|
|
db 4 ; size in blocks of 512 bytes
|
|
|
|
%else
|
|
;
|
|
; Building as a COM file for testing.
|
|
;
|
|
org 0x100
|
|
|
|
%endif
|
|
|
|
entry:
|
|
%ifndef AS_COM_PROGRAM
|
|
push es
|
|
push ax
|
|
push cx
|
|
push dx
|
|
cli
|
|
%endif
|
|
|
|
mov ax, welcome_msg
|
|
call print_string
|
|
|
|
mov ax, rom_base_msg
|
|
call print_string
|
|
mov ax, cs
|
|
call print_hex
|
|
mov ax, colon
|
|
call print_string
|
|
mov ax, beginning_of_rom
|
|
call print_hex
|
|
mov ax, newline
|
|
call print_string
|
|
|
|
;
|
|
; Initialize the SD Card.
|
|
;
|
|
call init_sd
|
|
%ifndef AS_COM_PROGRAM
|
|
jc .skip
|
|
%endif
|
|
|
|
%ifndef AS_COM_PROGRAM
|
|
;
|
|
; Install our BIOS INT13h hook into the interrupt vector table.
|
|
;
|
|
.install_13h_vector:
|
|
xor ax, ax ; INT vector segment
|
|
mov es, ax
|
|
mov ax, es:[0x13*4+2]
|
|
mov dx, REG_SCRATCH_1
|
|
out dx, ax ; save segment
|
|
mov ax, es:[0x13*4]
|
|
mov dx, REG_SCRATCH_3
|
|
out dx, ax ; save offset
|
|
mov ax, cs
|
|
mov es:[0x13*4+2], ax ; store segment
|
|
mov ax, int13h_entry
|
|
mov es:[0x13*4], ax ; store offset
|
|
|
|
;
|
|
; Move fixed disk 0 to fixed disk 1.
|
|
;
|
|
%ifdef TAKE_OVER_FIXED_DISK_0
|
|
call swap_fixed_disk_parameters_tables
|
|
%endif
|
|
|
|
;
|
|
; Determine our fixed disk ID.
|
|
;
|
|
.identify_fixed_disk:
|
|
mov ax, disk_id_msg
|
|
call print_string
|
|
mov ax, 0x40 ; BIOS data area
|
|
mov es, ax
|
|
%ifndef TAKE_OVER_FIXED_DISK_0
|
|
mov al, es:[0x75] ; HDNUM
|
|
add al, 0x80
|
|
%else
|
|
mov al, 0x80
|
|
%endif
|
|
mov dx, REG_SCRATCH_0 ; save fixed disk id
|
|
out dx, al
|
|
push ax
|
|
call print_hex
|
|
mov ax, newline
|
|
call print_string
|
|
.update_bda:
|
|
;
|
|
; Increment the number of fixed disks in the BIOS Data Area.
|
|
;
|
|
mov ax, num_drives_msg
|
|
call print_string
|
|
inc byte es:[0x75] ; HDNUM
|
|
mov al, es:[0x75]
|
|
xor ah, ah
|
|
call print_hex
|
|
mov ax, newline
|
|
call print_string
|
|
pop ax
|
|
|
|
;
|
|
; Install our fixed disk parameter table.
|
|
; For the 1st disk, it is stored in the interrupt vector table, at vector 41h.
|
|
; For the 2nd disk, it is stored in the interrupt vector table, at vector 46h.
|
|
;
|
|
.install_fixed_disk_parameters_table:
|
|
xor cx, cx ; INT vector segment
|
|
mov es, cx
|
|
cmp al, 0x80
|
|
jne .second_disk
|
|
mov ax, cs
|
|
mov es:[0x41*4+2], ax ; store segment
|
|
mov ax, fixed_disk_parameters_table
|
|
mov es:[0x41*4+0], ax ; store offset
|
|
jmp .end_fdpt
|
|
.second_disk:
|
|
mov ax, cs
|
|
mov es:[0x46*4+2], ax ; store segment
|
|
mov ax, fixed_disk_parameters_table
|
|
mov es:[0x46*4+0], ax ; store offset
|
|
.end_fdpt:
|
|
|
|
%ifdef USE_BOOTSTRAP
|
|
;
|
|
; Install our BIOS INT18h and 19h hook into the interrupt vector table.
|
|
; Their utility depends on the BIOS, some BIOS will not invoke INT19h and only invoke INT18h in case of fallback.
|
|
;
|
|
.install_18h_vector:
|
|
mov ax, cs
|
|
mov es:[0x18*4+2], ax ; store segment
|
|
mov es:[0x19*4+2], ax ; store segment
|
|
mov ax, int18h_entry
|
|
mov es:[0x18*4], ax ; store offset
|
|
mov ax, int19h_entry
|
|
mov es:[0x19*4], ax ; store offset
|
|
%endif
|
|
|
|
.skip:
|
|
sti
|
|
pop dx
|
|
pop cx
|
|
pop ax
|
|
pop es
|
|
retf
|
|
|
|
%else
|
|
|
|
;
|
|
; Execute COM program testing.
|
|
;
|
|
%include "tests.inc"
|
|
|
|
;
|
|
; DOS exit program.
|
|
;
|
|
mov ah, 0x4c
|
|
xor al, al
|
|
int 0x21
|
|
|
|
%endif
|
|
|
|
;
|
|
; The handler will preserve BP and SI across calls and therefore they can be used as temp.
|
|
;
|
|
%define TEMP0 bp
|
|
%define TEMP1 si
|
|
%define TEMP_LO TEMP0
|
|
%define TEMP_HI TEMP1
|
|
|
|
;
|
|
; INT 13h entry point.
|
|
;
|
|
int13h_entry:
|
|
%ifdef EXTRA_DEBUG
|
|
push TEMP0
|
|
mov TEMP0, sp
|
|
mov TEMP0, [TEMP0+6] ; grab the flags for iret
|
|
push TEMP0
|
|
popf
|
|
pop TEMP0
|
|
call dump_regs
|
|
%endif
|
|
sti
|
|
push TEMP0
|
|
push TEMP1
|
|
push ax
|
|
push dx
|
|
mov dx, REG_SCRATCH_0
|
|
in al, dx
|
|
pop dx
|
|
cmp dl, al ; is this our drive?
|
|
pop ax
|
|
je .check_function
|
|
|
|
;
|
|
; This is not an operation for the SD Card. Forward to the BIOS INT 13h handler.
|
|
;
|
|
.forward_to_bios:
|
|
%ifdef TAKE_OVER_FIXED_DISK_0
|
|
cmp dl, 0x81 ; is this the other fixed drive?
|
|
jne .no_fixed_disk_take_over
|
|
mov dl, 0x80
|
|
call swap_fixed_disk_parameters_tables
|
|
.no_fixed_disk_take_over:
|
|
%endif
|
|
mov TEMP0, ax ; save ax
|
|
mov TEMP1, dx ; save dx
|
|
pushf ; setup for iret from INT 13h handler
|
|
push cs ; setup for iret from INT 13h handler
|
|
mov ax, .return_from_int13h
|
|
push ax ; setup for iret from INT 13h handler
|
|
.simulate_int13h:
|
|
;
|
|
; Simulate INT 13h with the original vector.
|
|
;
|
|
mov dx, REG_SCRATCH_1
|
|
%ifndef AS_COM_PROGRAM
|
|
in ax, dx
|
|
%else
|
|
mov ax, cs
|
|
%endif
|
|
push ax ; setup for retf below
|
|
mov dx, REG_SCRATCH_3
|
|
%ifndef AS_COM_PROGRAM
|
|
in ax, dx
|
|
%else
|
|
mov ax, fake_int13h_entry
|
|
%endif
|
|
push ax ; setup for retf below
|
|
mov ax, TEMP0 ; restore ax
|
|
mov dx, TEMP1 ; restore dx
|
|
cli ; INT inhibits interrupts
|
|
retf ; call the INT 13h handler
|
|
.return_from_int13h:
|
|
sti ; just in case
|
|
pushf
|
|
push ax
|
|
mov ax, TEMP1 ; original dx
|
|
cmp al, 0x80 ; is fixed fixed?
|
|
jb .skip_update_hdnum
|
|
%ifdef TAKE_OVER_FIXED_DISK_0
|
|
mov dl, 0x81
|
|
call swap_fixed_disk_parameters_tables
|
|
%endif
|
|
mov ax, TEMP0 ; original ax
|
|
cmp ah, 0x08 ; is read parameters?
|
|
jne .skip_update_hdnum
|
|
push es
|
|
mov ax, 0x40 ; BIOS data area
|
|
mov es, ax
|
|
mov dl, es:[0x75] ; HDNUM
|
|
pop es
|
|
.skip_update_hdnum:
|
|
pop ax
|
|
popf
|
|
jmp .return_common
|
|
|
|
;
|
|
; This is an operation for the SD Card. Use our own INT 13h logic.
|
|
;
|
|
.check_function:
|
|
cmp ah, 0x19 ; is valid function?
|
|
jle .prepare_call
|
|
call func_unsupported
|
|
jmp .update_bda
|
|
.prepare_call:
|
|
.check_function_01_and_15:
|
|
cmp ah, 0x1 ; is read status?
|
|
je .call_no_update_bda
|
|
cmp ah, 0x15 ; is read size?
|
|
je .call_no_update_bda
|
|
mov TEMP1, .update_bda
|
|
jmp .call
|
|
.call_no_update_bda:
|
|
mov TEMP1, .return
|
|
.call:
|
|
push TEMP1 ; setup for ret from the handler
|
|
mov TEMP1, bx ; save bx
|
|
mov bl, ah
|
|
xor bh, bh
|
|
shl bl, 1
|
|
xchg TEMP1, bx ; restore bx
|
|
jmp [cs:TEMP1+func_table]
|
|
.update_bda:
|
|
mov TEMP0, es ; save es
|
|
mov TEMP1, 0x40 ; BIOS data area
|
|
mov es, TEMP1
|
|
mov es:[0x74], ah ; store HDSTAT
|
|
mov es, TEMP0 ; restore es
|
|
.return:
|
|
%ifdef DEBUG
|
|
push ax
|
|
mov ax, status_msg
|
|
call print_string
|
|
pop ax
|
|
push ax
|
|
xchg ah, al
|
|
pushf
|
|
xor ah, ah
|
|
popf
|
|
call print_hex
|
|
mov ax, newline
|
|
call print_string
|
|
pop ax
|
|
%endif
|
|
|
|
;
|
|
; Propagate the Carry Flag to the caller.
|
|
; MS-DOS hooks INT13h and will not perform a proper INT call, but instead a far call.
|
|
; Therefore, we handle the flags like we would for a far call.
|
|
;
|
|
.return_common:
|
|
mov TEMP0, sp
|
|
mov TEMP1, [TEMP0+8] ; grab the flags for iret
|
|
push TEMP1
|
|
jnc .return_success
|
|
popf
|
|
stc
|
|
jmp .return_with_flags
|
|
.return_success:
|
|
popf
|
|
clc
|
|
.return_with_flags:
|
|
pop TEMP1
|
|
pop TEMP0
|
|
sti ; workaround - MS-DOS does not properly propagate IF
|
|
%ifdef EXTRA_DEBUG
|
|
call dump_regs
|
|
%endif
|
|
retf 2 ; return and discard flags on the stack
|
|
|
|
;
|
|
; INT 13h function table
|
|
;
|
|
func_table:
|
|
dw func_10_is_ready ; reset
|
|
dw func_01_read_status
|
|
dw func_02_read_sector
|
|
dw func_03_write_sector
|
|
dw func_04_verify_sector
|
|
dw func_unsupported ; format_cyl
|
|
dw func_unsupported ; format_bad_trk
|
|
dw func_unsupported ; format_drv
|
|
dw func_08_read_params
|
|
dw func_10_is_ready ; init_params
|
|
dw func_unsupported ; read_sector_long
|
|
dw func_unsupported ; write_sector_long
|
|
dw func_0c_seek_cyl
|
|
dw func_10_is_ready ; reset_alt
|
|
dw func_unsupported ; read_sector_buf
|
|
dw func_unsupported ; write_sector_buf
|
|
dw func_10_is_ready
|
|
dw func_10_is_ready ; recalibrate
|
|
dw func_unsupported ; diagnostics
|
|
dw func_unsupported ; diagnostics
|
|
dw func_10_is_ready ; diagnostics
|
|
dw func_15_read_size
|
|
dw func_unsupported ; detect_change
|
|
dw func_unsupported ; set_media_type
|
|
dw func_unsupported ; set_media_type
|
|
dw func_10_is_ready ; park_heads
|
|
|
|
func_unsupported:
|
|
%ifdef DEBUG
|
|
call debug_handler
|
|
%endif
|
|
push ax
|
|
mov ax, unsupported_msg
|
|
call print_string
|
|
pop ax
|
|
push ax
|
|
mov al, ah
|
|
xor ah, ah
|
|
call print_hex
|
|
mov ax, newline
|
|
call print_string
|
|
pop ax
|
|
jmp error_invalid_parameter
|
|
|
|
;
|
|
; Fixed Disk Parameters Table
|
|
;
|
|
fixed_disk_parameters_table:
|
|
dw NUM_CYLINDERS ; cylinders
|
|
db NUM_HEADS ; heads
|
|
db 0, 0 ; reserved
|
|
dw 0xffff ; starting cylinder for write compensation
|
|
db 0 ; reserved
|
|
db 0xc0 | ((NUM_HEADS > 8) * 0x8) ; control byte (disable retries, >8 heads)
|
|
db 0, 0, 0 ; reserved
|
|
dw 0 ; landing zone
|
|
db SECTORS_PER_TRACK ; sectors per track
|
|
db 0 ; reserved
|
|
|
|
;
|
|
; Function 01h: Read Disk Status
|
|
; in: AH = 01h
|
|
; DL = drive number (80h or 81h)
|
|
; out: AH = status
|
|
; CF = 0 (success), 1 (error)
|
|
;
|
|
func_01_read_status:
|
|
%ifdef DEBUG
|
|
call debug_handler
|
|
%endif
|
|
push es
|
|
mov TEMP0, 0x40 ; BIOS data area
|
|
mov es, TEMP0
|
|
xor ah, ah
|
|
xchg ah, es:[0x74] ; load then clear HDSTAT
|
|
test ah, ah
|
|
jz .end
|
|
stc
|
|
.end:
|
|
pop es
|
|
ret
|
|
|
|
;
|
|
; Function 02h: Read Disk Sectors
|
|
; in: AH = 02h
|
|
; AL = number of sectors to read
|
|
; CH = cylinder number (low 8 bits)
|
|
; CL = bits 7-6: cylinder number (high 2 bits)
|
|
; bits 5-0: sector number
|
|
; DH = head number
|
|
; DL = drive number (80h or 81h)
|
|
; ES:BX = destination buffer
|
|
; out: AH = status
|
|
; CF = 0 (success), 1 (error)
|
|
;
|
|
func_02_read_sector:
|
|
%ifdef DEBUG
|
|
call debug_handler
|
|
%endif
|
|
test al, al
|
|
jz error_invalid_parameter
|
|
push ax
|
|
xor ah, ah
|
|
mov TEMP_HI, ax
|
|
push bx
|
|
call compute_lba
|
|
mov TEMP_LO, ax ; save lba
|
|
add ax, TEMP_HI ; check the upper boundary
|
|
mov TEMP_HI, bx ; save lba
|
|
adc bx, 0 ; carry
|
|
call is_lba_valid
|
|
pop bx
|
|
pop ax
|
|
jc error_sector_not_found
|
|
; TODO: (robustness) check buffer boundaries
|
|
.setup:
|
|
push ds
|
|
push bx
|
|
push cx
|
|
push dx
|
|
push di
|
|
push ax
|
|
mov cx, ax ; number of sectors to read
|
|
xor ch, ch
|
|
mov ax, cs
|
|
mov ds, ax
|
|
mov di, bx ; setup use of movsw
|
|
.assert_cs:
|
|
mov dx, REG_CS
|
|
mov al, 0 ; assert chip select
|
|
out dx, al
|
|
.cmd17:
|
|
push cx
|
|
; TODO: (opt) use CMD18 for multiple blocks
|
|
%ifdef DEBUG_IO
|
|
mov ax, send_cmd17_msg
|
|
call print_string
|
|
%endif
|
|
mov ax, TEMP_LO ; restore lba
|
|
mov bx, TEMP_HI ; restore lba
|
|
mov cl, 0x51 ; CMD17
|
|
call send_sd_read_write_cmd
|
|
jc .error
|
|
%ifdef DEBUG_IO
|
|
mov ax, wait_msg
|
|
call print_string
|
|
%endif
|
|
mov dx, REG_TIMEOUT
|
|
mov al, 10 ; 100 ms
|
|
out dx, al
|
|
.receive_token:
|
|
mov dx, REG_DATA
|
|
in al, dx
|
|
cmp al, 0xfe
|
|
je .got_token
|
|
mov dx, REG_TIMEOUT
|
|
in al, dx
|
|
test al, al
|
|
jnz .error
|
|
jmp .receive_token
|
|
.got_token:
|
|
%ifdef DEBUG_IO
|
|
mov ax, sd_token_msg
|
|
call print_string
|
|
%endif
|
|
mov cx, 256 ; block size (in words)
|
|
push si
|
|
mov si, end_of_rom ; virtual buffer
|
|
cld
|
|
.receive_block:
|
|
rep movsw
|
|
.receive_crc:
|
|
lodsw ; discard CRC
|
|
pop si
|
|
add TEMP_LO, 1 ; next block
|
|
adc TEMP_HI, 0 ; carry
|
|
pop cx ; number of sectors left to read
|
|
loop .cmd17
|
|
.success:
|
|
.deassert_cs1:
|
|
mov dx, REG_CS
|
|
mov al, 1 ; deassert chip select
|
|
out dx, al
|
|
.return1:
|
|
pop ax
|
|
pop di
|
|
pop dx
|
|
pop cx
|
|
pop bx
|
|
pop ds
|
|
jmp succeeded
|
|
.error:
|
|
.deassert_cs2:
|
|
mov dx, REG_CS
|
|
mov al, 1 ; deassert chip select
|
|
out dx, al
|
|
.return2:
|
|
pop cx ; number of sectors not read successfully
|
|
pop ax
|
|
sub al, cl ; number of sectors read successfully
|
|
pop di
|
|
pop dx
|
|
pop cx
|
|
pop bx
|
|
pop ds
|
|
jmp error_drive_not_ready
|
|
|
|
;
|
|
; Function 03h: Write Disk Sectors
|
|
; in: AH = 03h
|
|
; AL = number of sectors to write
|
|
; CH = cylinder number (low 8 bits)
|
|
; CL = bits 7-6: cylinder number (high 2 bits)
|
|
; bits 5-0: sector number
|
|
; DH = head number
|
|
; DL = drive number (80h or 81h)
|
|
; ES:BX = source buffer
|
|
; out: AH = status
|
|
; CF = 0 (success), 1 (error)
|
|
;
|
|
func_03_write_sector:
|
|
%ifdef DEBUG
|
|
call debug_handler
|
|
%endif
|
|
test al, al
|
|
jz error_invalid_parameter
|
|
push ax
|
|
xor ah, ah
|
|
mov TEMP_HI, ax
|
|
push bx
|
|
call compute_lba
|
|
mov TEMP_LO, ax ; save lba
|
|
add ax, TEMP_HI ; check the upper boundary
|
|
mov TEMP_HI, bx ; save lba
|
|
adc bx, 0 ; carry
|
|
call is_lba_valid
|
|
pop bx
|
|
pop ax
|
|
jc error_sector_not_found
|
|
; TODO: (robustness) check buffer boundaries
|
|
.setup:
|
|
push ds
|
|
push bx
|
|
push cx
|
|
push dx
|
|
push di
|
|
push ax
|
|
mov cx, ax ; number of sectors to write
|
|
xor ch, ch
|
|
mov di, bx ; destination address
|
|
mov ax, es
|
|
mov ds, ax ; save es and setup for movsw
|
|
mov ax, cs
|
|
mov es, ax
|
|
.assert_cs:
|
|
mov dx, REG_CS
|
|
mov al, 0 ; assert chip select
|
|
out dx, al
|
|
.cmd24:
|
|
push cx
|
|
; TODO: (opt) use CMD25 for multiple blocks
|
|
%ifdef DEBUG_IO
|
|
mov ax, send_cmd24_msg
|
|
call print_string
|
|
%endif
|
|
mov ax, TEMP_LO ; restore lba
|
|
mov bx, TEMP_HI ; restore lba
|
|
mov cl, 0x58 ; CMD24
|
|
call send_sd_read_write_cmd
|
|
jc .error
|
|
mov dx, REG_DATA
|
|
mov al, 0xfe ; send token
|
|
out dx, al
|
|
mov cx, 256 ; block size (in words)
|
|
push si ; save si (aka TEMP1)
|
|
mov si, di
|
|
mov di, end_of_rom ; virtual buffer
|
|
cld
|
|
.send_block:
|
|
rep movsw
|
|
mov di, si
|
|
pop si ; restore si (aka TEMP1)
|
|
%ifdef DEBUG_IO
|
|
mov ax, wait_msg
|
|
call print_string
|
|
%endif
|
|
mov dx, REG_TIMEOUT
|
|
mov al, 25 ; 250 ms
|
|
out dx, al
|
|
.receive_status:
|
|
mov dx, REG_DATA
|
|
in al, dx
|
|
cmp al, 0xff
|
|
jne .got_status
|
|
mov dx, REG_TIMEOUT
|
|
in al, dx
|
|
test al, al
|
|
jnz .error
|
|
jmp .receive_status
|
|
.got_status:
|
|
%ifdef DEBUG_IO
|
|
push ax
|
|
mov ax, sd_status_msg
|
|
call print_string
|
|
pop ax
|
|
push ax
|
|
xor ah, ah
|
|
call print_hex
|
|
mov ax, newline
|
|
call print_string
|
|
pop ax
|
|
%endif
|
|
and al, 0x1F
|
|
cmp al, 0x5
|
|
jne .error
|
|
%ifdef DEBUG_IO
|
|
mov ax, wait_msg
|
|
call print_string
|
|
%endif
|
|
mov dx, REG_TIMEOUT
|
|
mov al, 25 ; 250 ms
|
|
out dx, al
|
|
.receive_finish:
|
|
mov dx, REG_DATA
|
|
in al, dx
|
|
test al, al
|
|
jnz .got_finish
|
|
mov dx, REG_TIMEOUT
|
|
in al, dx
|
|
test al, al
|
|
jnz .error
|
|
jmp .receive_finish
|
|
.got_finish:
|
|
%ifdef DEBUG_IO
|
|
mov ax, sd_idle_msg
|
|
call print_string
|
|
%endif
|
|
add TEMP_LO, 1 ; next block
|
|
adc TEMP_HI, 0 ; carry
|
|
pop cx ; number of sectors left to write
|
|
%ifndef DEBUG_IO
|
|
loop .cmd24
|
|
%else
|
|
dec cx
|
|
jnz .cmd24
|
|
%endif
|
|
.success:
|
|
.deassert_cs1:
|
|
mov dx, REG_CS
|
|
mov al, 1 ; deassert chip select
|
|
out dx, al
|
|
.return1:
|
|
mov ax, ds
|
|
mov es, ax ; restore es
|
|
pop ax
|
|
pop di
|
|
pop dx
|
|
pop cx
|
|
pop bx
|
|
pop ds
|
|
jmp succeeded
|
|
.error:
|
|
.deassert_cs2:
|
|
mov dx, REG_CS
|
|
mov al, 1 ; deassert chip select
|
|
out dx, al
|
|
.return2:
|
|
pop cx ; number of sectors not written successfully
|
|
mov ax, ds
|
|
mov es, ax ; restore es
|
|
pop ax
|
|
sub al, cl ; number of sectors written successfully
|
|
pop di
|
|
pop dx
|
|
pop cx
|
|
pop bx
|
|
pop ds
|
|
jmp error_drive_not_ready
|
|
|
|
;
|
|
; Function 04h: Verify Disk Sectors
|
|
; in: AH = 04h
|
|
; AL = number of sectors to verify
|
|
; CH = cylinder number (low 8 bits)
|
|
; CL = bits 7-6: cylinder number (high 2 bits)
|
|
; bits 5-0: sector number
|
|
; DH = head number
|
|
; DL = drive number (80h or 81h)
|
|
; out: AH = status
|
|
; CF = 0 (success), 1 (error)
|
|
;
|
|
func_04_verify_sector:
|
|
%ifdef DEBUG
|
|
call debug_handler
|
|
%endif
|
|
test al, al
|
|
jz error_invalid_parameter
|
|
push ax
|
|
xor ah, ah
|
|
mov TEMP0, ax
|
|
push bx
|
|
call compute_lba
|
|
add ax, TEMP0 ; check the upper boundary
|
|
adc bx, 0 ; carry
|
|
call is_lba_valid
|
|
pop bx
|
|
pop ax
|
|
jc error_sector_not_found
|
|
jmp succeeded
|
|
|
|
;
|
|
; Function 08h: Read Drive Parameters
|
|
; in: AH = 08h
|
|
; DL = drive number (80h or 81h)
|
|
; out: AH = status
|
|
; AL = 0
|
|
; CH = maximum usable cylinder number (low 8 bits)
|
|
; CL = bits 7-6: maximum usable cylinder number (high 2 bits)
|
|
; bits 5-0: maximum usable sector number
|
|
; DH = maximum usable head number
|
|
; DL = number of drives
|
|
; ES:DI = address of disk parameters table (floppies only)
|
|
; CF = 0 (success), 1 (error)
|
|
;
|
|
func_08_read_params:
|
|
%ifdef DEBUG
|
|
call debug_handler
|
|
%endif
|
|
mov dh, (NUM_HEADS - 1)
|
|
|
|
push es
|
|
mov ax, 0x40 ; BIOS data area
|
|
mov es, ax
|
|
mov dl, es:[0x75] ; HDNUM
|
|
pop es
|
|
|
|
; the last cylinder is reserved on fixed disks
|
|
mov ch, ((NUM_CYLINDERS - 2) & 0xff)
|
|
mov cl, (((NUM_CYLINDERS - 2) & 0x300) >> 2) | SECTORS_PER_TRACK
|
|
xor ax, ax
|
|
clc
|
|
.exit:
|
|
ret
|
|
|
|
;
|
|
; Function 0Ch: Seek to Cylinder
|
|
; in: AH = 0Ch
|
|
; CH = cylinder number (low 8 bits)
|
|
; CL = bits 7-6: cylinder number (high 2 bits)
|
|
; bits 5-0: sector number
|
|
; DH = head number
|
|
; DL = drive number (80h or 81h)
|
|
; out: AH = status
|
|
; CF = 0 (success), 1 (error)
|
|
;
|
|
func_0c_seek_cyl:
|
|
%ifdef DEBUG
|
|
call debug_handler
|
|
%endif
|
|
push ax
|
|
push bx
|
|
call compute_lba
|
|
call is_lba_valid
|
|
pop bx
|
|
pop ax
|
|
jc error_sector_not_found
|
|
jmp succeeded
|
|
|
|
;
|
|
; Function 10h: Test for Drive Ready
|
|
; in: AH = 10h
|
|
; DL = drive number (80h or 81h)
|
|
; out: AH = error code
|
|
; CF = 0 (success), 1 (error)
|
|
;
|
|
func_10_is_ready:
|
|
%ifdef DEBUG
|
|
call debug_handler
|
|
%endif
|
|
jmp succeeded
|
|
|
|
;
|
|
; Function 15h: Read Disk Size
|
|
; in: AH = 15h
|
|
; DL = drive number (80h or 81h)
|
|
; out: AH = 00h (no drive present)
|
|
; 03h (drive present)
|
|
; CX:DX = number of 512-byte sectors
|
|
; CF = 0 (success), 1 (error)
|
|
;
|
|
func_15_read_size:
|
|
%ifdef DEBUG
|
|
call debug_handler
|
|
%endif
|
|
mov ah, 0x3 ; drive present
|
|
call get_max_lba
|
|
xchg cx, dx
|
|
clc
|
|
ret
|
|
|
|
;
|
|
; Common error handling
|
|
;
|
|
error_drive_not_ready:
|
|
mov ah, 0xaa
|
|
stc
|
|
ret
|
|
|
|
error_invalid_parameter:
|
|
mov ah, 0x1
|
|
stc
|
|
ret
|
|
|
|
error_sector_not_found:
|
|
mov ah, 0x4
|
|
stc
|
|
ret
|
|
|
|
succeeded:
|
|
xor ah, ah
|
|
clc
|
|
ret
|
|
|
|
%ifdef USE_BOOTSTRAP
|
|
;
|
|
; INT 19h entry point.
|
|
; Attempt to boot from floppy (single try for expediency).
|
|
;
|
|
int19h_entry:
|
|
mov ax, boot_floppy_msg
|
|
call print_string
|
|
xor dx, dx ; 1st floppy drive
|
|
call read_sector
|
|
.test_signature: ; taken from IBM BIOS for XT 286
|
|
cmp byte [0x7c00], 0x06 ; check for first instruction invalid
|
|
jb int18h_entry
|
|
mov di, 0x7c00 ; check data pattern
|
|
mov cx, 8 ; check the next 8 words
|
|
mov ax, word [0x7c00]
|
|
.test_byte:
|
|
add di, 2 ; point to next location
|
|
cmp ax, [di] ; check data pattern for a fill pattern
|
|
loopz .test_byte
|
|
jz int18h_entry ; boot not valid print message halt
|
|
jmp jump_to_boot
|
|
|
|
;
|
|
; INT 18h entry point.
|
|
; Attempt to boot from SD Card.
|
|
;
|
|
int18h_entry:
|
|
mov bp, sp
|
|
mov ax, ss:[bp+2]
|
|
test ax, ax ; caller is boot sector, must be no active partition
|
|
mov ax, no_part_msg
|
|
je no_boot
|
|
mov ax, boot_sd_msg
|
|
call print_string
|
|
mov dx, 0x80 ; MBR can only boot from 1st fixed disk
|
|
call read_sector
|
|
.test_signature:
|
|
cmp word [0x7c00+510], 0xaa55
|
|
mov ax, no_boot_msg
|
|
jne no_boot
|
|
|
|
;
|
|
; Common code between INT18h and INT19h.
|
|
;
|
|
jump_to_boot:
|
|
jmp 0:0x7c00
|
|
no_boot:
|
|
call print_string
|
|
sti
|
|
.loop:
|
|
hlt
|
|
jmp .loop
|
|
read_sector:
|
|
xor ax, ax
|
|
mov ds, ax
|
|
mov es, ax
|
|
.clear_memory:
|
|
mov cx, 256
|
|
mov di, 0x7c00
|
|
rep stosw
|
|
.read_boot_sector:
|
|
mov ax, 0x201 ; read 1 sector
|
|
mov cx, 1 ; sector 1
|
|
mov bx, 0x7c00
|
|
int 0x13
|
|
ret
|
|
%endif
|
|
|
|
;
|
|
; Disk utilities
|
|
;
|
|
|
|
%ifdef TAKE_OVER_FIXED_DISK_0
|
|
swap_fixed_disk_parameters_tables:
|
|
push es
|
|
push ax
|
|
push bx
|
|
xor ax, ax ; INT vector segment
|
|
mov es, ax
|
|
mov ax, es:[0x41*4+2]
|
|
xchg es:[0x46*4+2], ax
|
|
mov es:[0x41*4+2], ax
|
|
mov ax, es:[0x41*4+0]
|
|
xchg es:[0x46*4+0], ax
|
|
mov es:[0x41*4+0], ax
|
|
pop bx
|
|
pop ax
|
|
pop es
|
|
ret
|
|
%endif
|
|
|
|
;
|
|
; Compute LBA address based on CHS address
|
|
; in: CH = cylinder number (low 8 bits)
|
|
; CL = bits 7-6: cylinder number (high 2 bits)
|
|
; bits 5-0: sector number
|
|
; DH = head number
|
|
; out: BX:AX = LBA address
|
|
; FL = <TRASH>
|
|
;
|
|
compute_lba:
|
|
xor ax, ax
|
|
xor bx, bx
|
|
push dx
|
|
push cx
|
|
push dx
|
|
mov al, cl
|
|
and al, 0xc0
|
|
shl ax, 1
|
|
shl ax, 1
|
|
mov al, ch ; cylinder
|
|
mov cx, NUM_HEADS
|
|
mul cx ; cylinder * hpc
|
|
pop dx
|
|
mov cl, dh
|
|
xor ch, ch
|
|
add ax, cx ; cylinder * hpc + head
|
|
mov cl, SECTORS_PER_TRACK
|
|
mul cx ; (cylinder * hpc + head) * spt
|
|
pop cx
|
|
push cx
|
|
xor ch, ch
|
|
and cl, 0x3f
|
|
dec cx ; sector - 1
|
|
add ax, cx ; (cylinder * hpc + head) * spt + (sector - 1)
|
|
adc dx, 0 ; carry
|
|
mov bx, dx
|
|
pop cx
|
|
pop dx
|
|
ret
|
|
|
|
;
|
|
; Check if an LBA address is within the drive
|
|
; in: BX:AX = LBA address
|
|
; out: CF = 0 (success), 1 (error)
|
|
; FL = <TRASH>
|
|
;
|
|
is_lba_valid:
|
|
push cx
|
|
push dx
|
|
call get_max_lba
|
|
cmp bx, dx ; check highest word
|
|
jb .success
|
|
ja .error
|
|
cmp ax, cx ; check lowest word
|
|
jb .success
|
|
.error:
|
|
stc
|
|
pop dx
|
|
pop cx
|
|
ret
|
|
.success:
|
|
clc
|
|
pop dx
|
|
pop cx
|
|
ret
|
|
|
|
;
|
|
; Retrieve maximum LBA address for the drive
|
|
; out: DX:CX = maximum LBA address
|
|
;
|
|
get_max_lba:
|
|
mov dx, (NUM_SECTORS >> 16)
|
|
mov cx, (NUM_SECTORS & 0xFFFF)
|
|
ret
|
|
|
|
;
|
|
; I/O utilities
|
|
;
|
|
|
|
;
|
|
; Initialize the SD Card.
|
|
; out: CF = 0 (success), 1 (error)
|
|
; FL = <TRASH>
|
|
;
|
|
init_sd:
|
|
push ax
|
|
push ds
|
|
push bx
|
|
push cx
|
|
push dx
|
|
push si
|
|
mov ax, cs
|
|
mov ds, ax
|
|
mov dx, REG_CS
|
|
mov al, 1 ; deassert chip select
|
|
.power_up_delay:
|
|
out dx, al
|
|
mov cx, 0x0001
|
|
mov dx, 0x86a0 ; 0x186a0 = 100 ms
|
|
mov ah, 0x86 ; wait
|
|
int 0x15
|
|
.dummy_cycles:
|
|
mov dx, REG_DATA
|
|
mov al, 0xff
|
|
mov cx, 10 ; send 80 clock cycles
|
|
.synchronize:
|
|
out dx, al
|
|
loop .synchronize
|
|
.assert_cs:
|
|
mov dx, REG_CS
|
|
mov al, 0 ; assert chip select
|
|
out dx, al
|
|
.cmd0:
|
|
mov bx, 10 ; retries
|
|
.retry_cmd0:
|
|
%ifdef DEBUG_IO
|
|
mov ax, send_cmd0_msg
|
|
call print_string
|
|
%endif
|
|
mov si, cmd0
|
|
mov cx, 1 ; response is 1 byte
|
|
mov ah, 1 ; expect idle state
|
|
call send_sd_init_cmd
|
|
jnc .cmd8
|
|
.delay_retry_cmd0:
|
|
xor cx, cx
|
|
mov dx, 20000 ; microseconds
|
|
mov ah, 0x86 ; wait
|
|
int 0x15
|
|
dec bx
|
|
jnz .retry_cmd0
|
|
stc
|
|
jmp .exit
|
|
.cmd8:
|
|
%ifdef DEBUG_IO
|
|
mov ax, send_cmd8_msg
|
|
call print_string
|
|
%endif
|
|
mov si, cmd8
|
|
mov cx, 5 ; response is 5 bytes
|
|
mov ah, 1 ; expect idle state
|
|
call send_sd_init_cmd
|
|
jc .exit
|
|
.acmd41:
|
|
mov dx, REG_TIMEOUT
|
|
mov al, 250 ; 2.5 s
|
|
out dx, al
|
|
.retry_acmd41:
|
|
%ifdef DEBUG_IO
|
|
mov ax, send_acmd41_msg
|
|
call print_string
|
|
%endif
|
|
mov si, cmd55
|
|
mov cx, 1 ; response is 1 byte
|
|
mov ah, 1 ; expect idle state
|
|
call send_sd_init_cmd
|
|
; TODO: (older cards): handle v1 vs v2
|
|
mov si, acmd41
|
|
mov cx, 1 ; response is 1 byte
|
|
mov ah, 0 ; expect ready state
|
|
call send_sd_init_cmd
|
|
jnc .exit
|
|
mov dx, REG_TIMEOUT
|
|
in al, dx
|
|
test al, al
|
|
jz .retry_acmd41
|
|
stc
|
|
.exit:
|
|
; TODO: (older cards): retrieve SDHC flag
|
|
pop si
|
|
pop dx
|
|
pop cx
|
|
pop bx
|
|
pop ds
|
|
jc .error
|
|
.success:
|
|
mov ax, init_ok_msg
|
|
call print_string
|
|
pop ax
|
|
ret
|
|
.error:
|
|
mov ax, init_error_msg
|
|
call print_string
|
|
pop ax
|
|
ret
|
|
|
|
;
|
|
; Send an initialization command to the SD card
|
|
; in: DS:SI = command buffer
|
|
; CX = response size (in bytes)
|
|
; AH = expected highest status
|
|
; out: AX = <TRASH>
|
|
; CX = <TRASH>
|
|
; DX = <TRASH>
|
|
; SI = <TRASH>
|
|
; CF = 0 (success), 1 (error)
|
|
; FL = <TRASH>
|
|
;
|
|
send_sd_init_cmd:
|
|
mov dx, REG_DATA
|
|
.settle_before:
|
|
mov al, 0xff
|
|
out dx, al
|
|
.send_cmd:
|
|
push cx
|
|
mov cx, 6 ; command size
|
|
cld
|
|
.send_byte:
|
|
lodsb
|
|
out dx, al
|
|
loop .send_byte
|
|
mov cx, 8 ; retries
|
|
.receive_r1:
|
|
in al, dx
|
|
cmp al, 0xff
|
|
loope .receive_r1
|
|
pop cx
|
|
cmp al, ah
|
|
jbe .receive_payload
|
|
stc
|
|
jmp .settle_after
|
|
.receive_payload:
|
|
clc
|
|
.receive_byte:
|
|
in al, dx
|
|
loop .receive_byte
|
|
.settle_after:
|
|
mov al, 0xff
|
|
out dx, al
|
|
ret
|
|
|
|
cmd0 db 0x40, 0x00, 0x00, 0x00, 0x00, 0x95
|
|
cmd8 db 0x48, 0x00, 0x00, 0x01, 0xAA, 0x87
|
|
cmd55 db 0x77, 0x00, 0x00, 0x00, 0x00, 0x01
|
|
acmd41 db 0x69, 0x40, 0x00, 0x00, 0x00, 0x01
|
|
|
|
;
|
|
; Send a read or write command to the SD card
|
|
; in: CL = command
|
|
; BX:AX = LBA address
|
|
; out: AX = <TRASH>
|
|
; BX = <TRASH>
|
|
; CX = <TRASH>
|
|
; DX = <TRASH>
|
|
; CF = 0 (success), 1 (error)
|
|
; FL = <TRASH>
|
|
;
|
|
send_sd_read_write_cmd:
|
|
mov dx, XTMAX_IO_BASE+0 ; data port
|
|
push ax
|
|
.settle_before:
|
|
mov al, 0xff
|
|
out dx, al
|
|
.send_cmd:
|
|
mov al, cl ; command byte
|
|
out dx, al
|
|
mov al, bh ; address byte 1
|
|
out dx, al
|
|
mov al, bl ; address byte 2
|
|
out dx, al
|
|
pop ax
|
|
xchg al, ah ; address byte 3
|
|
out dx, al
|
|
xchg al, ah ; address byte 4
|
|
out dx, al
|
|
mov al, 0x1 ; crc (dummy)
|
|
out dx, al
|
|
mov cx, 8 ; retries
|
|
.receive_r1:
|
|
in al, dx
|
|
cmp al, 0xff
|
|
loope .receive_r1
|
|
test al, al
|
|
jz .exit
|
|
stc
|
|
.exit:
|
|
ret
|
|
|
|
;
|
|
; General utilities
|
|
;
|
|
|
|
%include "utils.inc"
|
|
|
|
%ifdef DEBUG
|
|
debug_handler:
|
|
push ax
|
|
mov ax, handler_msg
|
|
call print_string
|
|
pop ax
|
|
push ax
|
|
mov al, ah
|
|
xor ah, ah
|
|
call print_hex
|
|
mov ax, newline
|
|
call print_string
|
|
pop ax
|
|
ret
|
|
%endif
|
|
|
|
;
|
|
; Strings
|
|
;
|
|
|
|
welcome_msg db 'BootROM for XTMax v1.0', 0xD, 0xA
|
|
db 'Copyright (c) 2025 Matthieu Bucchianeri', 0xD, 0xA, 0
|
|
rom_base_msg db 'ROM Base Address = ', 0
|
|
init_ok_msg db 'SD Card initialized successfully', 0xD, 0xA, 0
|
|
init_error_msg db 'SD Card failed to initialize', 0xD, 0xA, 0
|
|
disk_id_msg db 'Fixed Disk ID = ', 0
|
|
num_drives_msg db 'Total Fixed Disk Drives = ', 0
|
|
unsupported_msg db 'Unsupported INT13h Function ', 0
|
|
%ifdef USE_BOOTSTRAP
|
|
boot_floppy_msg db 'Attempting boot from floppy...', 0xD, 0xA, 0
|
|
boot_sd_msg db 'Attempting boot from fixed disk...', 0xD, 0xA, 0
|
|
no_boot_msg db 'No bootable media found', 0xD, 0xA, 0
|
|
no_part_msg db 'No active partition found', 0xD, 0xA, 0
|
|
%endif
|
|
|
|
%ifdef DEBUG
|
|
handler_msg db 'INT13h Function ', 0
|
|
status_msg db 'Return with ', 0
|
|
|
|
%ifdef DEBUG_IO
|
|
send_cmd0_msg db 'Sending CMD0', 0xD, 0xA, 0
|
|
send_cmd8_msg db 'Sending CMD8', 0xD, 0xA, 0
|
|
send_cmd17_msg db 'Sending CMD17', 0xD, 0xA, 0
|
|
send_cmd24_msg db 'Sending CMD24', 0xD, 0xA, 0
|
|
send_acmd41_msg db 'Sending ACMD41', 0xD, 0xA, 0
|
|
wait_msg db 'Waiting for SD Card', 0xD, 0xA, 0
|
|
sd_token_msg db 'Received token', 0xD, 0xA, 0
|
|
sd_status_msg db 'Received status ', 0
|
|
sd_idle_msg db 'Received idle', 0xD, 0xA, 0
|
|
%endif
|
|
%endif
|
|
|
|
%ifndef AS_COM_PROGRAM
|
|
;
|
|
; Pad to 2KB. We will try to keep our ROM under that size.
|
|
;
|
|
times 2047-($-$$) db 0
|
|
db 0 ; will be used to complete the checksum.
|
|
%endif
|
|
|
|
;
|
|
; The virtual buffer for I/O follows the ROM immediately.
|
|
;
|
|
end_of_rom:
|