-
; Serial Port 16450 IO
; ====================
;base DLAB access Abbreviation Register Nam
; +0 0 Write THR ** Transmitter Holding Buffer
; +0 0 Read RBR ** Receiver Buffer
; +0 1 Read/Write DLL ** Divisor Latch Low Byte
; +1 1 Read/Write DLM ** Divisor Latch High Byte
; +1 0 Read/Write IER Interrupt Enable Registe
; +2 x Read IIR Interrupt Identification Register
; +3 x Read/Write LCR ** Line Control Register
; +4 x Read/Write MCR ** Modem Control Register
; +5 x Read LSR Line Status Register
; +6 x Read MSR Modem Status Register
; baud rate setting
; latch low byte
; 0x00 0x01 = 115200 bps
; 0x00 0x02 = 56700 bps
; 0x00 0x03 = 38400 bps
; 0x00 0x06 = 19200 bps
; 0x00 0x0C = 9600 bps
; 0x00 0x18 = 4800 bps
; 0x00 0x30 = 2400 bps
serial_base equ (0x3f8)
serial_cmd0 equ (serial_base + 0)
serial_cmd1 equ (serial_base + 1)
serial_cmd2 equ (serial_base + 2)
serial_cmd3 equ (serial_base + 3)
serial_cmd4 equ (serial_base + 4)
serial_status equ (serial_base + 5)
console_init:
push dx
push ax
; Settings
mov dx, serial_cmd3
mov al, 0x80 ; DLAB ON
out dx, al ; get it on its way.
mov dx, serial_cmd0 ; 115200 bps
mov al, 0x01 ; baud rate - Divisor Latch low byte
out dx, al ; get it on its way.
mov dx, serial_cmd1
mov al, 0x00 ; baud rate - divisor latch high byte
out dx, al ; get it on its way.
mov dx, serial_cmd3
mov al, 0x03 ; 8 bits, No parity, 1 stop bit
out dx, al ; get it on its way.
mov dx, serial_cmd2
mov al, 0xC7 ; FIFO control register
out dx, al ; get it on its way.
mov dx, serial_cmd4
mov al, 0x0B ; turn on DTR, RTS, and OUT2
out dx, al ; get it on its way.
; clear the Divisor Latch Access Bit
; in order to use the Transmitter Holding Register
mov dx, serial_cmd3 ; Line Control Register
in al, dx
and al, 0x7f ; 0111_1111b, Set DLAB=0, DLAP is bit7
out dx, al
pop ax
pop dx
ret
; reg al: received character
ch_get:
; to be implemented
mov al, 0x00
ret
; reg al: char to transmit
ch_put:
push dx
push ax
push cx
ch_put_wait:
mov dx, serial_status ; Line Status Register
mov cx, ax
in al, dx
test cx, 0x20 ; 0010_0000b, use bit 5 to see if THR is empty
jz ch_put_wait
mov ax, cx
mov dx, serial_cmd0 ; Transmitter Holding Register
out dx, al
pop cx
pop ax
pop dx
ret
(serial.s)
cli ; disable interrupts
mov bp, 0x9000
mov sp, bp
call console_init
main:
mov al, 'h'
call ch_put
mov al, 'a'
call ch_put
mov al, 'l'
call ch_put
mov al, 'l'
call ch_put
mov al, 'o'
call ch_put
jmp main
(main.s)
do you see any error with this simple code?
The serial doesn't correctly show chars :o :o :o
edit:
nasm syntax
-
I'm not familiar with the 16450, however, one common problem is not waiting for the character flag of the transmit register. Once the character is put into the Tx register the tx-full flag will be set, BUT usually this is a state controlled operation that is operating at the speed of the UART clock. Hence it is possible to put a character into tx reg, return from the subroutine, and then re-enter with the next character before the flag is set. The solution, is to use interupts or to wait for the flag to be set after putting the character in the Tx reg. If using the non-interrupt version, you might want to bracket the tx insertion and subsequent wait loop with disabled interupts - so that you don't miss the flag setting completely.
-
ch_put_wait:
mov dx, serial_status ; Line Status Register
mov cx, ax
in al, dx
test cx, 0x20 ; 0010_0000b, use bit 5 to see if THR is empty
jz ch_put_wait
this part? it waits and loops until the status register reports "THR is empty"
-
You still need the empty check.
Consider what happens when you have multiple characters to transmit. Your present code is:
Loop until Tx empty - fill Tx 1 - go away and fetch next character - Loop until Tx empty - fill Tx 2 - etc..
The problem is that the setting of the UART flags is asynchronos and can be much slower than the "go away and fetch next charater" such that when the code returns with the second character it can find the Tx is still being flagged as empty.
The non-interrupt solution is:
Loop until Tx empty - (disable interrupts) - fill Tx 1 - Loop until Tx full - (re-eneable interrupts) - go away to fetch nect character - etc..
You need the fill tx and loop until full to be atomic, hence the disabling/enabling of interrupts - if they are being used.
-
but interrupts are always disabled in that code.
-
There's a couple problems in the loop that waits for THR empty:
ch_put_wait:
mov dx, serial_status ; Line Status Register
mov cx, ax
in al, dx ; <--- al is trashed so you have lost the character you want to transmit, if a jump back to ch_put_wait happens then CX is trashed too
test cx, 0x20 ; <--- cx doesn't have the serial status register's value - al does. // 0010_0000b, use bit 5 to see if THR is empty
jz ch_put_wait
Disclaimer - my x86 assembly is very rusty. I can't remember the last time I had to do anything with it.
Same goes for my 16x50 family UART knowledge.
-
ok, let's also try a different approach, just in case ...
Unfortunately, I don't have a serious x86 machine with a physical 16460 chip handy, my mac-mini/x86 doesn't have it, and my Soekris Net5501's serial line "may be" damaged (its firmware doesn't correctly output chars on the serial line), or it's just that its flash is corrupted, which is why I'd like to reprogram it with u-boot
Can anyone with a BIOS-x86 && physical 16460 uart test/confirm that this code works?
; Serial Port Initialization
; ==========================
;
; ah=0x00 ; serial BIOS service (request, init)
; serial port status is returned in ax
;
; lets set the baud rate, parity modes, stop bits, bits tx/rx
;
; Bits Function
; 5..7 baud rate
; 000 110 bps
; 001 150 bps
; 010 300 bps
; 011 600 bps
; 100 1200 bps
; 101 2400 bps
; 110 4800 bps
; 111 9600 bps
;
; Bits Function
; 3..4 parity
; 00 No parity
; 01 Odd parity
; 10 No parity
; 11 Even parity
;
; Bits Function
; 2 Stop bits
; 0 One stop bit
; 1 Two stop bits
;
; Bits Function
; 0..1 Character Size
; 10 7 bits
; 11 8 bits
;
; Although the standard PC serial port hardware supports 19200 bps
; some BIOSes may not support this speed.
;
; Example: COM1,2400,no parity,8data,1stop
;
; mov ah, 0 ;Initialize opcode
; mov al, 10100111b ;Parameter data.
; mov dx, 0 ;COM1: port.
; int 14h
;
; -----------------------------------------------------
; reg al: character to transmit
ch_put:
pusha ; saves all the registers
mov dx, 0 ; Select COM1:
mov ah, 0x01 ; serial BIOS service (request, putch)
int 0x14 ; serial BIOS service (exec)
; test ah, 0x80h ; Check for error
; jnz SerialError
popa ; restores all the registers
ret
; ----------------------------------------------------------------
;
; Serial Port Status
; ==================
;
; ah=0x03 ; serial BIOS service (request, status)
; This call returns status information about the serial port
; including whether or not an error has occurred
; if a character has been received in the receive buffer
; if the transmit buffer is empty
; other pieces of useful information
; On entry into this routine, the dx register contains the serial port number
; On exit, the ax register contains the following values:
;
; reg ax: Bit Meaning
; =================================================
; bit.15 Time out error
; bit.14 Transmitter shift register empty
; bit.13 Transmitter holding register empty
; bit.12 Break detection error
; bit.11 Framing error
; bit.10 Parity error
; bit.09 Overrun error
; bit.08 Data available
; bit.07 Receive line signal detect
; bit.06 Ring indicator
; bit.05 Data set ready (DSR)
; bit.04 Clear to send (CTS)
; bit.03 Delta receive line signal detect
; bit.02 Trailing edge ring detector
; bit.01 Delta data set ready
; bit.00 Delta clear to send
The UART has been available since the early 1980s, constantly refined rather than reinvented, it has shown little change over the years in its general mode of operation, but modern UARTs like 16450 and 16550 are traceable to early classics like 8250 included on the original IBM PC motherboard to provide communications with modems and serial printers.
The original IBM PC introduced BIOS functions to initialize (max 9600bps) and transfer/receive char.
I am not an expert in BIOS programming (x86/16bit mode), but the above code should be much simpler than the previous uart-chip direct approach :o :o :o
-
There's a couple problems in the loop that waits for THR empty:
mov cx, ax
in al, dx ; <--- al is trashed so you have lost the character you want to transmit
test cx, 0x20 ; <--- cx doesn't have the serial status register's value - al does. //
"mov cx, ax" saves ax, ax is { ah:al } :D
-
"mov cx, ax" saves ax, ax is { ah:al } :D
Yes, but:
ch_put_wait:
mov dx, serial_status ; Line Status Register
mov cx, ax ; /* 3 */
in al, dx ; /* 1 */
test cx, 0x20
jz ch_put_wait ; /* 2 */
- the "in al, dx" at /* 1 */ modifies AL
- if the "jz ch_put_wait" at /* 2 */ results in a jump (which isn't necessarily determined by the read of the status register, since that's in AL not CX), then
- the "mov cx, ax" at /* 3 */ saves into CX an AL register that has been changed by the instruction at /* 1 */ from the previous loop iteration
-
I'd suggest a wait loop like:
ch_put_wait:
mv cx, ax ; save ax
mov dx, serial_status ; Line Status Register
ch_put_wait_loop:
in al, dx
test al, 0x20
jz ch_put_wait_loop
mov ax, cx ; restore ax
-
This code is still not working, but I am now 100% sure it's a hardware problem with the serial of my net5501, and at least I know I'd better try with on my T23 old IBM laptop.
Anyway, see the silly bug? This is what we call "mind vision": you look at the code, but you don't look at the actual code, but rather what your mind has placed on your eyes.
x86-assembly is terrible, especially if you are used to programming m68k or mips, architectures that have much more orthogonality.
With the x86/16bit mode, these only accept al as a target
; in al,imm8 Input byte from imm8 I/O port address into al
; in ax,imm8 Input byte from imm8 I/O port address into ax
; in al,dx Input byte from I/O port in dx into al
; in ax,dx Input word from I/O port in dx into ax
and that's the reason behind that wrong line of code: I wrote it initially as
in cx, dx
test cx, 0x20
but it's illegal x86 code, so I fixed the first line, and forgot the second line.
And behind the bug in the code itself, lurks the most powerful bug of all: the one with how human beings interpret reality, something wrong pre-polarized in the brain that makes the difference between what the brain wants to see rather than what it actually sees.
Thank you guys :-+ :-+ :-+
-
So, yesterday I installed qemu-i386 and I am simulating the serial port on tty/stdio
qemu running ...
CPU in Real Mode
hAllo
confirmed: the put_ch code does work!
This morning I looked at the physical port of the net5511 and found the ground pin of the serial0 was floating, causing bad contact. By re-soldering the pin with fresh lead it's not perfectly working.
Solved :-+ :-+ :-+
-
mov dx, serial_cmd3
Does whatever assembler you're using interpret that at "move immediate value (address of serial_cmd30 into dx", or is it going to load dx with the contents of that address? MOV on x86 could be one of about a dozen different opcodes.
The intel assembler would probably want "serial_cmd3" to be strongly typed (strongly typed assembler. Hmmph.)
I'm not sure what gas does (guessing that you're using gas, based on the .s file extension.)
(and wait - isn't the operand order wrong? Shouldn't it be more like:
mov $serial_cmd3, dx
(this probably means you're using some other assembler...)
-
as=nasm
serial_base equ (0x3f8)
serial_cmd0 equ (serial_base + 0)
serial_cmd1 equ (serial_base + 1)
serial_cmd2 equ (serial_base + 2)
serial_cmd3 equ (serial_base + 3)
serial_cmd4 equ (serial_base + 4)
serial_status equ (serial_base + 5)
ch_put:
pusha ; saves all registers
mov cx, ax ; save al, char to transmit
;------------------------------------------------------------------------
ch_put_wait:
mov dx, serial_status ; Line Status Register
in al, dx
test al, 0x20 ; 0010_0000b, use bit 5 to see if THR is empty
jz ch_put_wait
;------------------------------------------------------------------------
mov ax, cx ; restore al, char to transmit
mov dx, serial_cmd0 ; Transmitter Holding Register
out dx, al ; tramsmit(char)
popa ; restore all registers
ret
this works with nasm
-
dev-lang/nasm-v2.15.05
Homepage: [url]https://www.nasm.us/[/url]
Description: groovy little assembler
License: BSD-2
-
If I may, assuming this is protected mode 32-bit code,
SERIAL_BASE EQU (0x3F8)
SERIAL_THR EQU (SERIAL_BASE+0) ; Transmit Hold Register (w)
SERIAL_RBR EQU (SERIAL_BASE+0) ; Receive Buffer Register (r)
SERIAL_LSR EQU (SERIAL_BASE+5) ; Line Status Register (r)
SERIAL_IS_DATA_READY EQU (0x01)
SERIAL_IS_OVERRUN EQU (0x02)
SERIAL_IS_PARITY_ERROR EQU (0x04)
SERIAL_IS_FRAMING_ERROR EQU (0x08)
SERIAL_IS_BREAK EQU (0x10)
SERIAL_IS_THR_EMPTY EQU (0x20)
SERIAL_IS_EMPTY EQU (0x40)
SERIAL_IS_FIFO_ERROR EQU (0x80)
; Character to be sent in al
; Clobbers ah
serial_putc:
push edx
mov ah, al
movzx edx, SERIAL_LSR
.serial_putc_wait:
in al, dx
test al, SERIAL_IS_THR_EMPTY
jz .serial_putc_wait
movzx edx, SERIAL_THR
mov al, ah
out dx, al
pop edx
ret
; Character to be sent in al
; Carry flag is clear if nothing sent, set if character sent
; Clobbers ah
serial_putc_nowait:
push edx
mov ah, al
movzx edx, SERIAL_LSR
in al, dx
test al, SERIAL_IS_THR_EMPTY
mov al, ah
jnz .serial_putc_nowait_send
pop edx
clc
ret
.serial_putc_nowait_send:
movzx edx, SERIAL_THR
out dx, al
pop edx
stc
ret
The serial_putc_nowait is useful, if you have a control loop and don't want to set up the serial interrupt.
When you have a string to be sent, just load the next character to al, and call serial_putc_nowait.
If carry flag is set, it was sent; otherwise a previous transmission is still in progress and carry flag is clear.
If you loaded al using a pointer, you can simply add-with-carry zero to the pointer register after the call (often esi, i.e: call serial_putc_nowait followed by adc esi, 0), and save the pointer value. Just remember to check if al is zero (end of string) before calling serial_putc_nowait.
As usual, labels starting with . are local (and re-definable), and won't be added to the symbol table.
-
If I may, assuming this is protected mode 32-bit code
real-mode, because it's what the PC_BIOS initializes.
-
If I may, assuming this is protected mode 32-bit code
real-mode, because it's what the PC_BIOS initializes.
Ah, so make that
SERIAL_BASE EQU (0x3F8)
SERIAL_THR EQU (SERIAL_BASE+0) ; Transmit Hold Register (w)
SERIAL_RBR EQU (SERIAL_BASE+0) ; Receive Buffer Register (r)
SERIAL_LSR EQU (SERIAL_BASE+5) ; Line Status Register (r)
SERIAL_IS_DATA_READY EQU (0x01)
SERIAL_IS_OVERRUN EQU (0x02)
SERIAL_IS_PARITY_ERROR EQU (0x04)
SERIAL_IS_FRAMING_ERROR EQU (0x08)
SERIAL_IS_BREAK EQU (0x10)
SERIAL_IS_THR_EMPTY EQU (0x20)
SERIAL_IS_EMPTY EQU (0x40)
SERIAL_IS_FIFO_ERROR EQU (0x80)
; Character to be sent in al
; Clobbers ah
serial_putc:
push dx
mov ah, al
mov dx, SERIAL_LSR
.serial_putc_wait:
in al, dx
test al, SERIAL_IS_THR_EMPTY
jz .serial_putc_wait
mov dx, SERIAL_THR
mov al, ah
out dx, al
pop dx
ret
; Character to be sent in al
; Carry flag is clear if nothing sent, set if character sent
; Clobbers ah
serial_putc_nowait:
push dx
mov ah, al
mov dx, SERIAL_LSR
in al, dx
test al, SERIAL_IS_THR_EMPTY
mov al, ah
jnz .serial_putc_nowait_send
pop dx
clc
ret
.serial_putc_nowait_send:
mov dx, SERIAL_THR
out dx, al
pop dx
stc
ret
In NASM syntax, raw numbers are literals. (A $ prefix does nothing; $0x3F8 is exactly the same thing as 0x3F8 or 1016.)
Memory access is always in square brackets, so for example [0x3F8] refers to the contents of address 0x3F8, and [dx] the contents at address pointed to by the dx register.
-
[size=0px][dx][/size][/size][size=0px] the contents at address pointed to by the dx register.[/size]
But it's not "in al, [dx]" ? :-)
Thanks for the explanations, although I'm not entirely happy that there are apaprently three common ASM syntaxes for x86 these days! (?) (hmm. online says that NASM does use the Intel syntax. But it doesn't look much like the MASM code I used to write; wasn't MASM "real" Intel syntax?)
-
But it's not "in al, [dx]" ? :-)
z80, 8080 and x86 have two spaces: { port_IO, mem }
{ in, out } instructions serve port_IO
"in y, x" can target to reg, read data from IO_port(x), and put it into reg.y
"out x, y" can source data from a reg, read reg.y, and put it into IO_port(x)
three common ASM syntaxes for x86 these days
eh, many more for 68k and 68hc11 ;D
-
[size=0px][dx][/size][/size][size=0px] the contents at address pointed to by the dx register.[/size]
But it's not "in al, [dx]" ? :-)
No, because the I/O port space on x86 is special, like DiTBho said above. The angle bracket notation applies exclusively to memory addresses.
You can only read/write to/from I/O ports from/to AL (8-bit) or AX (16-bit) register (or EAX on 32-bit mode).
You can use an immediate byte to access the first 256 ports, but for ports 256-65535, you have to use the DX register.
See in (https://www.felixcloutier.com/x86/in), out (https://www.felixcloutier.com/x86/out).
This is the same in NASM, MASM, and TASM; i.e., this is the Intel syntax.
In 16-bit mode on 8086, the angle bracket notation is very limited:- [constant] for exact address
- [register] for address specified in register SI, DI, or BX
- [register+constant] for address specified in register SI, DI, BX, or BP plus a fixed offset
- [register1+register2] for address as a sum of registers, register1 being either BX or BP, and register2 either SI or DI
- [register1+register2+constant] for address as a sum of registers plus a fixed constant, register1 being either BX or BP, and register2 either SI or DI
The string instructions (LODSB, LODSW, STOSB, STOSW, MOVSB, MOVSW, CMPSB, CMPSW, SCASB, and SCASW) use intrinsic addressing (load/compare/scan from DS:SI, store to ES:DI), auto-incrementing (if D flag clear) or auto-decrementing (if D flag set) the registers (SI and/or DI) afterwards. For 80186 and later processors in 16-bit mode, add INSB, INSW, OUTSB, and OUTSW for I/O port string instructions; the port number in DX stays unchanged. Some assemblers allow a correct-looking parameter, but don't even check it, leading to confusion since the parameter may not match what the instruction does. Essentially, ignore all parameters for these instructions!
The REP instruction prefix repeats the following instruction CX register value number of times. The prefixes REPZ/REPE, REPNZ/REPNE repeat the following instruction as long as Z flag is set or clear, respectively. They are only reliable across processor models with the string instructions, as the microcode often uses the REP prefix bit for other uses for non-string instructions.
-
I usually use GNU binutils/{ as, ld, * } for { 68k, 68hc11, mips, powerpc }
and the Avocet ProTools line of Compilers, Assemblers, and Simulators for 64-bit { mips64, RISC-V }
So why "nasm" for x86? :o :o :o
well, I don't like x86 asm programming, I need to prepare a special bootloader and nasm offers this
; 16 bit app
; -----------------------------------------------------------------------------
[bits 16]
[org 0x7c00] ; boot sector startup
begin_16bit:
mov bp, stack_base ; base of the stack
mov sp, stack_base ; top of the stack
call console_init
mov bx, msg_16bit_mode
call print16
...
; 32 bit app
; -----------------------------------------------------------------------------
[bits 32]
begin_32bit:
mov ebx, msg_32bit_mode
call print32
call app_start ; Give control to the app
jmp $ ; halt if app returns control
...
; padding
; -----------------------------------------------------------------------------
; The first 512 bytes of a disk are known as the bootsector
; The boot sector is an area of the disk reserved for booting purposes
; If the bootsector of a disk contains a valid boot sector
; the last word of the sector must contain the signature 0xaa55
; then the disk is treated by the BIOS as bootable.
; -----------------------------------------------------------------------------
times 510 - ($-$$) db 0
dw 0xaa55
so "nasm" because for this specific need, these nasm tricks make it simpler than with g/as ;D
-
You can also supply -masm=intel to gcc to use Intel syntax in assembly output (-S) and in extended inline assembly (asm volatile ("instructions" : outputs : inputs : clobbers);).
Similarly, with GNU as (part of binutils), you can use .intel_syntax noprefix at the beginning of the source file, to use Intel syntax.
I don't remember exactly which versions of as and gcc started supporting these, though. They aren't new, but they were not initially supported either.
To get the same machine code from as as my previous listing produces using nasm, you do need slight changes:
.arch i386
.intel_syntax noprefix
.code16
.set SERIAL_BASE, (0x3F8) # Base I/O
.set SERIAL_THR, (SERIAL_BASE+0) # Transmit Hold Register (w)
.set SERIAL_LSR, (SERIAL_BASE+5) # Line Status Register (r)
.set SERIAL_IS_THR_EMPTY, (0x20)
# Character to be sent in al
# Clobbers ah
.global serial_putc
serial_putc:
push dx
mov ah, al
mov dx, SERIAL_LSR
serial_putc_wait:
in al, dx
test al, SERIAL_IS_THR_EMPTY
jz serial_putc_wait
mov dx, SERIAL_THR
mov al, ah
out dx, al
pop dx
ret
# Character to be sent in al
# Carry flag is clear if nothing sent, set if character sent
# Clobbers ah
.global serial_putc_nowait
serial_putc_nowait:
push dx
mov ah, al
mov dx, SERIAL_LSR
in al, dx
test al, SERIAL_IS_THR_EMPTY
mov al, ah
jnz serial_putc_nowait_send
pop dx
clc
ret
serial_putc_nowait_send:
mov dx, SERIAL_THR
out dx, al
pop dx
stc
ret
To switch to 32-bit code generation, use .code32 directive.
You can compile the above using as source.s -o target.o.
The main differences are that dot (.) starts a directive, and # a comment; ; is just a separator like newline. All symbols are local by default, so you need to declare the global ones. Constants are defined using .set NAME, value instead of EQU.
-
NASM is way better than gas if you actually write assembly yourself.
-
NASM is way better than gas if you actually write assembly yourself.
Agreed. If I was writing a bootloader or similar freestanding code in assembly for 16/32-bit x86, I'd use NASM too, and not GNU as.
I just wanted to show that this is possible to do also in (recent enough) GNU as; NASM isn't explicitly required here.
After switching to 32-bit mode, things become much more interesting, since there are a few function attributes (https://gcc.gnu.org/onlinedocs/gcc/x86-Function-Attributes.html) that GCC and Clang support for the calling convention; the default "everything on stack" is pretty annoying in my opinion. Basically, if you have a function that takes its parameters in eax, edx, and ecx registers (in that order), you can just declare the prototype with __attribute__((regparm (3)), and the compiler will pass the parameters in said registers in calls to that function iff the prototype is visible.
-
NASM is way better than gas if you actually write assembly yourself.
I have no doubt. gas is mostly for use by the compiler, with perhaps its only advantage for a programmer being that you maintain a lot of pseudo-ops and cli properties across many different architectures.
Sometimes I think it's been made intentionally ugly to discourage assembly language programming. "@" for a comment character (ARM)? What were they thinking?
-
LOL, crazy, see this trick
; -----------------------------------------------------------------------------
; console_out the EOS terminated string which follows the call
; eg:
; call say
; db "hAllo", EOS
; -----------------------------------------------------------------------------
say:
push bp
mov bp,sp
push ds
push si
push ax
mov si, [bp+2]
push cs
pop ds
say_body:
lodsb ; Load byte at address DS:SI into AL.
cmp al, EOS
je say_done
call ch_put
jmp say_body
say_done:
mov [bp+2], si
pop ax
pop si
pop ds
pop bp
ret
unbelievable that you can easily mix code and data this wild way, but it actually works!!! :o :o :o
ok, seriously it takes too many opcode, let's implement the classic print_string with a string pointer as argument
-
(
you can't do anything like this with a MIPS
unless you force strict align=4 for the string (zero padding string_size mod 4)
and there are some implementations that physically separate data from code
x86 is a very primitive beast :o :o :o
)
-
LOL, crazy, see this trick
At the time when 8086/80186/80286-class machines had less than 640kB of RAM, it was common for games to use self-modifying code.
Starflight (https://en.wikipedia.org/wiki/Starflight) (1986) used this heavily, including storing the modifications on-disk: to restart the game from scratch, you had to reinstall it (create new copies of the game disks)!
ok, seriously it takes too many opcode, let's implement the classic print_string with a string pointer as argument
Well, this takes 33 bytes, noting that now the character to be sent is in AH (and not AL) register,
[bits 16]
EOS EQU (0)
SERIAL_BASE EQU (0x3F8)
SERIAL_THR EQU (SERIAL_BASE+0) ; Transmit Hold Register (w)
SERIAL_RBR EQU (SERIAL_BASE+0) ; Receive Buffer Register (r)
SERIAL_LSR EQU (SERIAL_BASE+5) ; Line Status Register (r)
SERIAL_IS_DATA_READY EQU (0x01)
SERIAL_IS_OVERRUN EQU (0x02)
SERIAL_IS_PARITY_ERROR EQU (0x04)
SERIAL_IS_FRAMING_ERROR EQU (0x08)
SERIAL_IS_BREAK EQU (0x10)
SERIAL_IS_THR_EMPTY EQU (0x20)
SERIAL_IS_EMPTY EQU (0x40)
SERIAL_IS_FIFO_ERROR EQU (0x80)
; Character to be sent in AH
; Clobbers AL
serial_putc:
push dx
mov dx, SERIAL_LSR
.serial_putc_wait:
in al, dx
test al, SERIAL_IS_THR_EMPTY
jz .serial_putc_wait
mov dx, SERIAL_THR
mov al, ah
out dx, al
pop dx
ret
; String to be sent in DS:SI
; SI will point to EOS
serial_puts:
push ax
.serial_puts_next:
mov ah, [si]
cmp ah, EOS
je .serial_puts_done
call serial_putc
inc si
jmp .serial_puts_next
.serial_puts_done:
pop ax
ret
but if you don't need the single-character one, this takes only 28 bytes:
[bits 16]
EOS EQU (0)
SERIAL_BASE EQU (0x3F8)
SERIAL_THR EQU (SERIAL_BASE+0) ; Transmit Hold Register (w)
SERIAL_RBR EQU (SERIAL_BASE+0) ; Receive Buffer Register (r)
SERIAL_LSR EQU (SERIAL_BASE+5) ; Line Status Register (r)
SERIAL_IS_DATA_READY EQU (0x01)
SERIAL_IS_OVERRUN EQU (0x02)
SERIAL_IS_PARITY_ERROR EQU (0x04)
SERIAL_IS_FRAMING_ERROR EQU (0x08)
SERIAL_IS_BREAK EQU (0x10)
SERIAL_IS_THR_EMPTY EQU (0x20)
SERIAL_IS_EMPTY EQU (0x40)
SERIAL_IS_FIFO_ERROR EQU (0x80)
; String to be sent in DS:SI
; SI will point to EOS
serial_puts:
push ax
push dx
.serial_puts_next:
mov ah, [si]
cmp ah, EOS
jnz .serial_puts_one
pop dx
pop ax
ret
.serial_puts_one:
inc si
mov dx, SERIAL_LSR
.serial_puts_wait:
in al, dx
test al, SERIAL_IS_THR_EMPTY
jz .serial_puts_wait
mov dx, SERIAL_THR
mov al, ah
out dx, al
jmp .serial_puts_next
Often, preserving AX isn't useful, so if you let it clobber that too, omit the push/pop AX, and it'll shrink to just 26 bytes.
The reason I didn't use lodsb in either, is because lodsb; mov ah, al and mov ah, [si] ; inc si are both 3 bytes, but the former depends on the direction flag (D), and the latter does not.
If you look at the compare-to-EOS parts, you'll find that adding other characters for true printf-like functionality (in a separate function!) isn't too difficult. I just recommend that you make it caller-cleanup (i.e., you only look at the parameters on stack, not pop them off). For integers, I recommend you use the slow DIV/IDIV-by-radix method (with value in DX*65536+AX), constructing it from right to left in a temporary buffer on stack (16 chars including EOS suffices for 32-bit integers in octal, decimal, and hexadecimal).