Difference between revisions of "Output"

From SizeCoding
Jump to: navigation, search
m (PC Speaker)
 
(72 intermediate revisions by 6 users not shown)
Line 1: Line 1:
 
== Outputting to the screen ==
 
== Outputting to the screen ==
  
First, be aware of the [https://http://img.tfd.com/cde/MEMMAP.GIF MSDOS memory layout]
+
First, be aware of the [http://img.tfd.com/cde/MEMMAP.GIF MSDOS memory layout]
  
 
=== Outputting in Textmode (80x25) ===
 
=== Outputting in Textmode (80x25) ===
  
Right after the start of your program you are in mode 3, that is 80x25 in 16 colors.
+
==== Hello World / High Level function ====
  
See the [http://www.columbia.edu/~em36/wpdos/videomodes.txt Video Modes List]
+
Here's an obligatory "Hello World" program in text mode, using a [http://www.ctyme.com/intr/rb-2562.htm "high level" MS-DOS function]. With a small optimization already included (using <code>XCHG BP,AX</code> instead of <code>MOV AH,09h</code>), this snippet is 20 bytes in size.
  
[[File:Drawchar example.png|thumb|draw char example]]
+
[[File:Hello world.png|thumb|Hello World!]]
  
So, to show something on the screen, you would need to set a segment register to 0xB800, then write values into this segment.
+
<syntaxhighlight lang="nasm">
 +
org 100h ; we start at CS:100h
 +
xchg bp,ax ; already a trick, puts 09h into AH
 +
mov dx,text ; DX expects the adress of a $ terminated string
 +
int 21h ; call the DOS function (AH = 09h)
 +
ret ; quit
 +
text:
 +
db 'Hello World!$'
 +
</syntaxhighlight>
  
  
 +
Of course, this gets shorter with each byte you remove from the text itself. Now let's look into arbitrary screen access. Right after the start of your program you are in mode 3, that is 80x25 in 16 colors. See the [http://www.columbia.edu/~em36/wpdos/videomodes.txt Video Modes List] [[File:Drawchar example.png|thumb|draw char example]] So, to show something on the screen, you would need to set a segment register to 0xB800, then write values into this segment.
  
 +
==== Low level access ====
  
 
The following three snippets showcase how to draw a red smiley in three different ways. All example snippets are meant to be standalone programs, starting with the first instruction and nothing before it. The target coordinate (40,12) is about the middle of the screen. We need a multiplier 2 since one char needs two bytes in memory (char and color is a byte each). The high byte 0x04 means red (4) on black (0) while the 0x01 is the first ASCII char - a smiley.
 
The following three snippets showcase how to draw a red smiley in three different ways. All example snippets are meant to be standalone programs, starting with the first instruction and nothing before it. The target coordinate (40,12) is about the middle of the screen. We need a multiplier 2 since one char needs two bytes in memory (char and color is a byte each). The high byte 0x04 means red (4) on black (0) while the 0x01 is the first ASCII char - a smiley.
  
 
+
<syntaxhighlight lang="nasm">push 0xb800
 
 
 
 
 
 
 
 
<syntaxhighlight lang="nasm">  
 
push 0xb800
 
 
pop ds
 
pop ds
 
mov bx,(80*12+40)*2
 
mov bx,(80*12+40)*2
 
mov ax, 0x0401
 
mov ax, 0x0401
 
mov [bx],ax
 
mov [bx],ax
ret
+
ret</syntaxhighlight>  
</syntaxhighlight>  
 
  
<syntaxhighlight lang="nasm">  
+
<syntaxhighlight lang="nasm">push 0xb800
push 0xb800
 
 
pop es
 
pop es
 
mov di,(80*12+40)*2
 
mov di,(80*12+40)*2
 
mov ax, 0x0401
 
mov ax, 0x0401
 
stosw
 
stosw
ret
+
ret</syntaxhighlight>  
</syntaxhighlight>  
 
  
<syntaxhighlight lang="nasm">  
+
<syntaxhighlight lang="nasm">push ss
push ss
 
 
push 0xb800
 
push 0xb800
 
pop ss
 
pop ss
Line 49: Line 49:
 
push ax
 
push ax
 
pop ss
 
pop ss
int 0x20
+
int 0x20</syntaxhighlight>
</syntaxhighlight>
 
  
 
You might notice that the ''push <word>'' + ''pop seg_reg'' combination is always the same and occupies four bytes alltogether. If correct alignment is not important to you and you really just want ''any'' pointer to the screen, there is another way to get a valid one:
 
You might notice that the ''push <word>'' + ''pop seg_reg'' combination is always the same and occupies four bytes alltogether. If correct alignment is not important to you and you really just want ''any'' pointer to the screen, there is another way to get a valid one:
Line 60: Line 59:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
That's also four bytes, but it already has the ''stosb'' opcode (for putting something onto the screen) integrated and even one slot free for another one-byte-instruction. It works because SI initially points to the start of our code, and ''stosb'' has the hexadecimal representation of 0xAA. After the first command, the segment register ES contains the value 0xAB90. If you repeatedly write something to the screen with ''stosb'' you will eventually reach the 0xB800 segment and chars will appear on the screen. With a careful selection of the free one-byte-opcode you can also reintroduce some alignment. This works also with the ''stosw'' opcode (0xAB).
+
That's also four bytes, but it already has the <code>stosb</code> opcode (for putting something onto the screen) integrated and even one slot free for another one-byte-instruction. It works because <code>SI</code> initially points to the start of our code, and <code>stosb</code> has the hexadecimal representation of <code>0AAh</code>. After the first command, the segment register <code>ES</code> contains the value <code>0AA90h</code>. If you repeatedly write something to the screen with <code>stosb</code> you will eventually reach the <code>0B800h</code> segment and chars will appear on the screen. With a careful selection of the free one-byte-opcode you can also reintroduce some alignment. This works also with the <code>stosw</code> opcode <code>0ABh</code>.
 
 
=== Outputting in mode 13h (320x200) ===
 
 
 
The videomemory for mode 13h is located at segment 0xA000, so you need to assign this value to a segment register. Also, after the start of your program you are normally still in textmode, so you need to switch to the videomode. The following snippet does both:
 
 
 
<syntaxhighlight lang="nasm">
 
mov al,0x13
 
int 0x10    ; AH = 0 means : set video mode to AL = 0x13 (320 x 200 pixels in 256 colors)
 
push 0xA000  ; put value on the stack
 
pop es      ; pop the stack into segment register ES
 
</syntaxhighlight>
 
 
 
You're free to use any of the segment register / opcode combinations to write to the screen
 
* ''ES / stosb''
 
* ''DS / mov''
 
* ''SS / push''
 
 
 
Let's add some code that actually draws something on the screen, the following program occupies 23 bytes and draws a fullscreen XOR texture
 
[[File:Mode13h-example-xor.png|left|bottom|thumb|mode13h-example-xor]]
 
 
 
<syntaxhighlight lang="nasm">  
 
mov al,0x13
 
int 0x10
 
push 0xa000
 
pop es
 
X: cwd ; "clear" DX (if AH < 0x7F)
 
mov ax,di ; get screen position into AX
 
mov bx,320 ; get screen width into BX
 
div bx ; divide, to get row and column
 
xor ax,dx ; the famous XOR pattern
 
and al,32+8 ; a more interesting variation of it
 
stosb ; finally, draw to the screen
 
jmp short X ; rinse and repeat
 
</syntaxhighlight>
 
 
 
 
 
Note that there is a different way of preparing the segment register, instead of :
 
<syntaxhighlight lang="nasm">
 
push 0xa000
 
pop es
 
</syntaxhighlight>
 
you can also do :
 
<syntaxhighlight lang="nasm">
 
mov ah,0xA0
 
mov es,ax
 
</syntaxhighlight>
 
both variations occupy 4 bytes, but the latter is executable on processor architectures where ''push <word>'' is not available.
 
 
 
Now let's optimize on the snippet. First, we can adapt the "LES" trick from the textmode section. We just exchange
 
<syntaxhighlight lang="nasm">
 
push 0xa000
 
pop es
 
</syntaxhighlight>
 
with:
 
<syntaxhighlight lang="nasm">
 
les bx,[bx]
 
</syntaxhighlight>
 
to save two bytes. This works because BX is 0x0000 at start and thus, accesses the region ''before'' our code, which is called [https://en.wikipedia.org/wiki/Program_Segment_Prefix Program Segment Prefix]. The two bytes that are put into the segment register ES are bytes 2 and 3  = ''"Segment of the first byte beyond the memory allocated to the program"'' which is usually 0x9FFF. That is just off by one to our desired 0xA000. Unfortunately that means a 16 pixel offset, so if screen alignment means something to you, you can't use this optimization. Also, said two bytes are not always 0x9FFF, for example if resident programs are above the ''"memory allocated to the program"'' (FreeDos) their content is overwritten if we take their base as our video memory base.
 
 
 
Second, we can use an alternative way of putting pixels to the screen, subfunction AH = 0x0C of int 0x10. Also, instead of constructing row and column from the screen pointer, we can use some interesting properties of the screenwidth regarding logical operations. This results in the following 16 byte program:
 
 
 
<syntaxhighlight lang="nasm">  
 
cwd            ; "clear" DX for perfect alignment
 
mov al,0x13
 
X: int 0x10 ; set video mode AND draw pixel
 
inc cx ; increment column
 
mov ax,cx ; get column in AH
 
xor al,ah ; the famous XOR pattern
 
mov ah,0x0C ; set subfunction "set pixel" for int 0x10
 
and al,32+8 ; a more interesting variation of it
 
jmp short X ; rinse and repeat
 
</syntaxhighlight>
 
 
 
The first optimization is the double usage of the same "int 0x10" as setting the videomode and drawing the pixel. The subfunction AH = 0x0C expects row and column in DX and CX. Since the screenwidth is 320, which is 5 * 64, we can ignore the row and just works with the column, if we use logical operations and just use bit 0-6 of the result. The subfunction AH = 0x0C allows for unbounded column values in CX (up to 65535) and correctly "wraps" it internally without an error.
 
 
 
The major drawback of the "subfunction AH = 0x0C" approach is performance loss. While DosBox and many emulators perform just fine, real hardware will draw much much slower based on the Video BIOS.
 
 
 
== Producing sound ==
 
 
 
=== MIDI notes ===
 
 
 
=== PC Speaker ===
 
 
 
Producing sound with PC speakers is incredibly easy. Basically, you set a system timer to a desired frequency, then connect this timer to the speaker. [http://wiki.osdev.org/PC_Speaker The PC Speaker Article] from OSDEV Wiki has the details about it. A very optimized and dirty variant of producing sound with the speaker is this 12 byte snippet :
 
 
 
<syntaxhighlight lang="nasm">
 
hlt ; sync to timer1
 
inc bx ; increment our counter
 
mov ax,bx ; work with a copy
 
or al,0x4B      ; melody pattern + 2 LSB for speaker link
 
out 0x42,al ; set new countdown for timer2 (two passes)
 
out 0x61,al ; link timer2 to PC speaker (2 LSBs are 1)
 
jmp si ; rinse and repeat
 
</syntaxhighlight>
 
 
 
Instead of sending low and high byte of our divisor directly in succession, we do it the "two path" way. That reduces the amount of possible frequencies to 255, which is still good enough for some rough sounds. Linking the timer to the PC speaker might not be obvious : Normally you would read the value of port 0x61, set the two least significant bits to TRUE and write the value again. You can save on all of this, if you just send the "two path" value which you just used for the timer if that value has the two least significant bits already set (''or al,0x4B'' does this). Be aware that port 0x61 does many things apart from just connecting the timer to the speaker. A useful resource for ports in general is the [http://bochs.sourceforge.net/techspec/PORTS.LST Bochs Ports List], for port 0x61 it displays:
 
 
 
 
 
<code>
 
''0061 w KB controller port B (ISA, EISA)  (PS/2 port A is at 0092)
 
 
 
system control port for compatibility with 8255
 
 
 
bit 7 (1= IRQ 0 reset )
 
 
 
bit 6-4    reserved
 
 
 
bit 3 = 1  channel check enable
 
 
 
bit 2 = 1  parity check enable
 
 
 
'''bit 1 = 1  speaker data enable'''
 
 
 
'''bit 0 = 1  timer 2 gate to speaker enable''' ''
 
 
 
</code>
 
 
 
 
 
So if you experience strange things with highly optimized pc speaker output, revert to the safe way. The described way works with real hardware and DosBox. Unfortunately, both Orcacle Virtual Box with MsDos 6.22 and Windows XP NTVDM seem not to properly emulate PC speakers (Investigation and citation needed here!)
 
  
 +
==== Alternative high level functions  ====
  
An example for a tiny intro that uses PC speaker sound is [http://www.pouet.net/prod.php?which=67833 SpeaCore]
+
Besides the direct way of accessing memory there are also other ways of bringing char to the screen (f.e)
 +
* [http://www.ctyme.com/intr/rb-4124.htm INT 29h]
 +
* [http://www.ctyme.com/intr/rb-2558.htm INT 21h AH=6]
 +
* [http://www.ctyme.com/intr/rb-2562.htm INT 21h AH=9]

Latest revision as of 14:19, 8 April 2024

Outputting to the screen

First, be aware of the MSDOS memory layout

Outputting in Textmode (80x25)

Hello World / High Level function

Here's an obligatory "Hello World" program in text mode, using a "high level" MS-DOS function. With a small optimization already included (using XCHG BP,AX instead of MOV AH,09h), this snippet is 20 bytes in size.

Hello World!
 
org 100h			; we start at CS:100h
xchg 	bp,ax		; already a trick, puts 09h into AH
mov		dx,text		; DX expects the adress of a $ terminated string
int 	21h			; call the DOS function (AH = 09h)
ret					; quit
text:
db 'Hello World!$'


Of course, this gets shorter with each byte you remove from the text itself. Now let's look into arbitrary screen access. Right after the start of your program you are in mode 3, that is 80x25 in 16 colors. See the Video Modes List
draw char example
So, to show something on the screen, you would need to set a segment register to 0xB800, then write values into this segment.

Low level access

The following three snippets showcase how to draw a red smiley in three different ways. All example snippets are meant to be standalone programs, starting with the first instruction and nothing before it. The target coordinate (40,12) is about the middle of the screen. We need a multiplier 2 since one char needs two bytes in memory (char and color is a byte each). The high byte 0x04 means red (4) on black (0) while the 0x01 is the first ASCII char - a smiley.

push 0xb800
pop ds
mov bx,(80*12+40)*2
mov ax, 0x0401
mov [bx],ax
ret
push 0xb800
pop es
mov di,(80*12+40)*2
mov ax, 0x0401
stosw
ret
push ss
push 0xb800
pop ss
mov sp,(80*12+40)*2
mov ax, 0x0401
push ax
pop ss
int 0x20

You might notice that the push <word> + pop seg_reg combination is always the same and occupies four bytes alltogether. If correct alignment is not important to you and you really just want any pointer to the screen, there is another way to get a valid one:

 
les bx,[si]
nop
stosb

That's also four bytes, but it already has the stosb opcode (for putting something onto the screen) integrated and even one slot free for another one-byte-instruction. It works because SI initially points to the start of our code, and stosb has the hexadecimal representation of 0AAh. After the first command, the segment register ES contains the value 0AA90h. If you repeatedly write something to the screen with stosb you will eventually reach the 0B800h segment and chars will appear on the screen. With a careful selection of the free one-byte-opcode you can also reintroduce some alignment. This works also with the stosw opcode 0ABh.

Alternative high level functions

Besides the direct way of accessing memory there are also other ways of bringing char to the screen (f.e)