80x86 Assembler, Part 5

Atrevida Game Programming Tutorial #16
Copyright 1998, Kevin Matz, All Rights Reserved.

Prerequisites:

  • Chapter 15, 80x86 Assembler, Part 4
  • Chapter 6, Hardware Ports

       
Let's expand our assembler vocabularies by learning some more instructions. In this chapter, we'll learn about the multiplication and division instructions, the shift instructions, and the hardware port instructions.

Multiplication and division

There are two multiplication instructions: MUL, for multiplying unsigned values, and IMUL, for multiplying signed values. MUL and IMUL both permit either the multiplication of a byte-sized value with another byte-sized value, or the multiplication of a word-sized value with another word-sized value.

To multiply a byte by a byte, we put one of the values to be multiplied in the AL register. (Mathematics fanatics call this value the multiplicand.) Then we use either the MUL or IMUL instruction. We supply one operand -- the value (the multiplier) to multiply the first value by. This operand can use virtually any addressing mode except for the immediate mode (that means you can say "MUL AL" or "MUL [x]" or "MUL [BYTE DS:BX + SI + 3]" but not "MUL 5" or "MUL 03Dh"). The result (the product) of the multiplication is returned in AX. (Note that the largest possible value obtained by multiplying two bytes is 255 dec * 255 dec = 65025 dec, which is very close to the range of a word; 65535 - 255 - 255 = 65025 dec, right?)

Let's multiply 3 and 8.

MOV AL, 3                              ; Multiply 3...
MOV BL, 8                              ; ...by 8...
MUL BL                                 ; ...storing the result in AX.
; The result, 24 dec, is now in AX.

Let's multiply two signed, byte-sized variables called x and y.

MOV AL, [x]                            ; Multiply [x]...
IMUL [y]                               ; ...by [y]; signed result is in AX.

Personally, when I think of multiplication, I automatically think of the MUL instruction. But using a MUL instruction when an IMUL instruction should be used instead can be a hard-to-find bug. Remember, use MUL for unsigned (all positive) values, and use IMUL for signed (positive and negative) values.

To multiply a word by a word, place the multiplicand operand in the AX register. Then use either the MUL or IMUL instruction with a word-sized operand. Here's the fun part. The result of multiplying two sixteen-bit values together can produce a value with up to thirty-two bits. So the processor splits the product into two word-sized halves. The least-significant half is placed in the AX register, and the most-significant half is placed in the DX register.

Let's multiply -2000 dec by 30000 dec:

MOV AX, -2000
MOV BX, 30000
IMUL BX                                ; Most-siginificant word of the
                                       ;  result is now in DX; least-
                                       ;  significant word of the result
                                       ;  is in AX.

Division works similar to multiplication. DIV is used for dividing one unsigned value by another unsigned value. IDIV is used for dividing one signed value by another signed value. DIV and IDIV permit two "modes": you can divide a word by a byte, or you can divide a doubleword by a word.

To divide a word by a byte, place the word-sized dividend (the value that gets divided) into AX. Then use either DIV or IDIV with the byte-sized divisor (the value the dividend is divided by) as an operand. When the division is finished, the result (the quotient) is available in AL. Also, the remainder or modulo of the dividend and divisor is calculated and is available in AH. (So if you want to do a "mod" operation, use DIV or IDIV and read the remainder out of AH.)

To divide a doubleword by a word, break the doubleword-sized dividend into least-significant and most-significant halves. Place the most-significant word into DX and the least-significant word into AX. Then you can use either DIV or IDIV as appropriate; supply a word-sized divisor as the operand. The quotient will be available in AX, and the remainder or modulo will be available in DX.

Here's a quick assembler program that demonstrates the use of the multiplication and division instructions. It doesn't do anything noticeable, but of course, if you have a debugger, you can watch the values in the registers change as you step through the program:

------- TEST11.ASM begins -------

%TITLE "Assembler Test Program 11 -- Multiplying and Dividing"

    IDEAL

    MODEL small
    STACK 256
    LOCALS

    DATASEG

ecks                         DB   11
why                          DW   1000h

    CODESEG
Start:
    ; Let DS point to the start of the data segment:
    MOV AX, @data
    MOV DS, AX

    ; Multiply the byte-sized [ecks] by 9, giving a word-sized product:
    MOV AL, 9
    MUL [ecks]
    ; AX should now contain 99 dec.  (Note: IMUL could be used here
    ;  instead.)

    ; Multiply the contents of AX (99 dec) by the word-sized [why],
    ;  giving a doubleword-sized product:
    MUL [why]
    ; The product of 99 dec by 1000 hex (1000 hex = 4096 dec) is
    ;  405504 dec, or 63000 hex.  The most-significant word is 6,
    ;  so DX should contain 6.  The least-significant word is 3000 hex,
    ;  so AX should contain that value.

    ; Divide a word, 80 dec, by a byte, 10 dec:
    MOV AX, 80
    MOV BL, 10
    DIV BL
    ; The quotient, 8, should be in AL.  The remainder, 0, should be
    ;  in AH.
    
    ; Divide a doubleword, -8000 dec, by the word -999 dec:
    MOV AX, -8000                      ; Least-significant word
    MOV DX, 0FFFFh                     ; Most-significant word...  All
                                       ;  bits are set; this "extends"
                                       ;  the negative value to 32 bits.
                                       ;  More on this later.
    MOV BX, -999
    IDIV BX
    ; The quotient, 7, should be in AX.  The remainder, -8, should be
    ;  in DX.
    
EndProgram:
    ; Terminate the program:
    MOV AX, 04C00h
    INT 21h

END Start

------- TEST11.ASM ends -------

We'll see example programs later that actually do things with the values calculated by MUL/IMUL and DIV/IDIV.

Keep in mind that the multiplication and division instructions have a reputation for being slow. After all, if you do addition and multiplication on paper, the multiplication generally takes much longer, because there are more steps to do. On the 8088, multiplication or division instructions often took more than a hundred clock cycles to execute. On newer processors these are much faster -- on a 486, for instance, using MUL or IMUL to multiply two word-sized values can take between 13 and 26 clock cycles. Surely Pentium-class processors do much better still.

Extending negative values

It is often necessary for whatever reason to "convert" a byte-sized value to a word-sized value. For example, let's suppose we have the number 3 in AL. If we needed to convert this number to a word-sized value, we would simply need to make sure that AH was empty. After we set AH to zero, then AX would contain 3.

MOV AL, 3                              ; Now we have 3 in AL.
MOV AH, 0                              ; Zero out AH.
; Now, AX contains 3.

That's easy enough. But what about signed numbers? What if we were to have -3 in AL, and we wanted to convert this value to a word-sized value (ie. we want AX to contain -3)?

Let's try the above method...

MOV AL, -3                             ; AL contains -3.
MOV AH, 0                              ; Zero out AH.

But did this work? No. Recall two's complement notation for negative numbers. When we put -3 into AL, we were actually storing 1111 1101 bin in AL. 1111 1101 bin represents -3 in a signed byte context. (How did we get 1111 1101 bin? First, we take a positive number, such as 3, in binary form, and we apply a NOT operation to it. That means we flip all the bits; ones become zeroes and zeroes become ones. Then we add one. Look back to chapter two, "Binary Operations", for the full story on two's complement.)

Then we clear AH. So AX contains 0000 0000 1111 1101 bin. And what does that represent in a signed word context? Well, the sign bit (the leftmost bit) is zero, so we interpret the number as an unsigned value. 0000 0000 1111 1101 bin equals 128 + 64 + 32 + 16 + 8 + 4 + 1, which is 253 dec. 253 dec certainly isn't equal to -3 dec. So the above method doesn't work with negative values.

Fortunately, there are instructions that will handle this task for us correctly.

The CBW (Convert Byte to Word) instruction takes the byte in the AL register and extends it to a 16-bit value. This 16-bit value is then placed in AX.

Let's convert -3 (in a byte context) to its equivalent word-sized respresentation:

MOV AL, -3                             ; AL = -3
CBW                                    ; Convert byte in AL to word in AX
; Now AX contains -3.

Note that it's not possible to use registers other than AL and AX.

If you need to convert a word to a doubleword, you can use the CWD instruction. It will take the word in AX, and convert it to a 32-bit doubleword value, with the most-significant word in DX, and the least-significant word in AX. For example:

MOV AX, 0ABCDh                         ; AX = 0ABCDh (a negative number)
CWD                                    ; Convert word in AX to doubleword
                                       ;  with MSW in DX and LSW in AX

The CBW and CWD instructions simply copy the sign bit to all of the leftmost positions in the new value. If the sign bit is 1, all of the bits to the left become ones. If the sign bit is 0, all of the bits to the left becomes zeroes.

Optimization tip

Here's a distracting sidenote: there are several ways to set a register to zero. Let's use AH as an example. "MOV AH, 0" is the most obvious one. But "SUB AH, AH" would work too, wouldn't it? Or, you could use "XOR AH, AH". The XOR method happens to be slightly faster than the others. So that's why you often see something like "XOR DX, DX" in programs -- it's just a faster method of setting a register to zero.

The SHL and SHR bit shift instructions

Do you recall the "<<" and ">>" bit-shift operators in C? Here's how you shift values in assembler:

The SHL instruction is the shift-left instruction. It can left-shift either bytes or words. There are two ways in which SHL can be used:

If you want to shift a register or memory location left by one bit, you can specify the register or memory location as the first operand, and 1 for the second operand. For example:

SHL AX, 1                              ; Shift AX left by 1
SHL CH, 1                              ; Shift CL left by 1
SHL [Count], 1                         ; Shift variable Count left by 1
SHL [BYTE ES:BX + SI + 3], 1           ; Shift the byte at location
                                       ;  "ES:BX + SI + 3" left by 1

If you want to shift left by some constant other than 1, you have to put that constant into the CL register. Then you use CL as the second operand:

MOV CL, 3
SHL BX, CL                             ; Shift BX left by 3

MOV CL, 5
SHL [x], CL                            ; Shift the variable x left by 5

Sadly, you can't use any other register than CL, and you can't specify an immediate value other than 1. (Intel at its best.)

SHR is the shift-right instruction. It works the same way as SHL, but, of course, it performs right shifts. For example:

SHR DL, 1                              ; Shift DL right by 1
SHR [WORD SS:BP], 1                    ; Shift the word at SS:BP right by 1

MOV CL, 4
SHR AX, CL                             ; Shift AX right by 4

MOV CL, [x]
SHR [Amount], CL                       ; Shift variable Amount right by
                                       ;  the value specified in variable x

Remember that bit-shifting causes bits to be lost when they are shifted out of the byte or word's range. Also recall that left-shifts can be used to multiply numbers by powers of two, while right-shifts can be used to divide numbers by powers of two. If you want to review bit shifts, read the first section of Chapter 3, "Binary Manipulations".

The ROL and ROR rotation instructions

Bitwise rotations are similar to shifts, except that when a value is rotated, bits that "fall off the edge" are copied back to the other side of the value.

Let's look at left rotations. If we were to rotate a byte to the left by one bit, we would move all of the bits to the left once. The last bit, in bit 7, would be copied back to bit 0.

          7   6   5   4   3   2   1   0
        +---+---+---+---+---+---+---+---+
   /----|   |   |   |   |   |   |   |   |<----\       (Left rotation)
   |    +---+---+---+---+---+---+---+---+     |
   |       <-  <-  <-  <-  <-  <-  <-         |
   |                                          |
   \------------------------------------------/

For example, if we had the following byte...

 10010110 bin

...rotating it left once would give us:

 00101101 bin

And rotating it left once again:

 01011010 bin

And again:

 10110100 bin

If we were to rotate it eight times, we would get the original byte.

Right rotations are similar, but of course the rotation occurs towards the right. The bits that fall out of position 0 are copied back to bit 7:

            7   6   5   4   3   2   1   0
          +---+---+---+---+---+---+---+---+
    /---->|   |   |   |   |   |   |   |   |----\      (Rotate Right)
    |     +---+---+---+---+---+---+---+---+    |
    |         ->  ->  ->  ->  ->  ->  ->       |
    |                                          |
    \------------------------------------------/

If we had the following byte...

 00100101 bin

...rotating it to the right once would give us:

 10010010 bin

And again:

 01001001 bin

And again:

 10100100 bin

Again, if there were a total of eight rotations, we would get the original byte again.

Rotations work in a similar fashion for word-sized values -- for left rotations, bits that leave position 15 are transferred to bit 0, and for right rotations, bits that leave position 0 are transferred to position 15.

ROL is the rotate-left instruction. ROR is the rotate-right instruction. The operands work exactly the same way as they do for SHL and SHR -- if you want to rotate by a value larger than 1, you must put that value in CL:

MOV AL, 23h                            ; AL = 23h
ROL AL, 1                              ; Rotate AL left once, so AL = 46h

MOV DX, 0FADEh                         ; DX = FADEh
ROR DX, 1                              ; Rot. DX right once, so DX = 7D6Fh
                                       ;  (Before: 1111 1010 1101 1110
                                       ;   After:  0111 1101 0110 1111)

MOV BX, 1                              ; BX = 1
MOV CL, 3                              ; CL = 3
ROL BX, CL                             ; Rotate BX left by CL, so BX = 8

MOV CL, 5                              ; CL = 5
ROR BYTE [Array + 25], CL              ; Find the 26th byte of the "Array"
                                       ;  array, and rotate that byte right
                                       ;  by CL

One last thing: the carry flag, CF, gets modified whenever you use ROL or ROR. For ROL, the diagram actually looks like this:

     CF            7   6   5   4   3   2   1   0
   +---+         +---+---+---+---+---+---+---+---+
   |   |<---+----|   |   |   |   |   |   |   |   |<----\    (ROL)
   +---+    |    +---+---+---+---+---+---+---+---+     |
            |       <-  <-  <-  <-  <-  <-  <-         |
            |                                          |
            \------------------------------------------/

The bit that is copied from the leftmost position (position 7 for bytes, position 15 for words) is also copied to the carry flag, CF. I normally ignore this, but you should just be aware that CF gets modified.

For ROR, the diagram should actually look like this:

         7   6   5   4   3   2   1   0             CF
       +---+---+---+---+---+---+---+---+         +---+
 /---->|   |   |   |   |   |   |   |   |----+--->|   |      (ROR)
 |     +---+---+---+---+---+---+---+---+    |    +---+
 |         ->  ->  ->  ->  ->  ->  ->       |
 |                                          |
 \------------------------------------------/

Notice again that the bit that is copied from the rightmost position, bit 0, is also copied to CF. Again, you can ignore this, unless you have a clever plan for using this bit.

Other shift and rotation instructions

SHL, SHR, ROL, and ROR are the most commonly used shift and rotate instructions. However, there are several others. Let's look at the instructions SAL and SAR.

SAR stands for "Shift Arithmetic Right". SAR is similar to SHR, but SAR preserves the most significant bit. How is this useful?

In Chapter 3, "Binary Manipulations", I very briefly indicated that caution should be used when shifting signed numbers in order to do multiplication or division. Actually, using left-shifts (SHL) to multiply signed numbers by powers of two produces the correct results (as long as there is no overflow or "underflow"). But using right-shifts (SHR) to divide signed numbers by powers of two doesn't work, because the sign bit gets dragged out of the most-significant position. To preserve the sign bit, we can use SAR instead of SHR.

SAR works this way for a single right shift: it saves a copy of the number's sign bit. Then it shifts the number to the right, the same way SHR does. Then it copies the saved sign bit back into the most-significant position. For right shifts by two or more, this procedure is repeated an appropriate number of times.

Let's divide the byte-sized number -20 dec by two, using SAR:

MOV AL, -20                            ; AL = -20 dec
SAR AL, 1                              ; Signed-right-shift AL by 1

Now let's work out what happens. The two's complement representation of -20 is:

20 dec =     00010100 bin
         NOT
         -----------------
             11101011 bin
           +        1
         -----------------
             11101100 bin  =  -20 dec

Now, let's signed-right-shift 11101100 bin:

 11101100 bin     becomes     11110110 bin

And what does 11110110 bin represent?

     11110110 bin
 NOT
 -----------------
     00001001 bin
   +        1
 -----------------
     00001010 bin   = 8 + 2 = 10 dec, so 11110110 bin = -10 dec.

It works: -20 dec divided by two is indeed -10 dec. Note that I have used a byte in this example, but word-sized values work as well.

Now, what about SAL? SAL is actually exactly the same as SHL, because SHL works correctly for multiplying signed numbers by powers of two. Because SAL and SHL are the same, each can be used in place of the other. But it's best to use the SHL instruction for unsigned numbers and the SAL instruction for signed numbers, because it helps to make your assembler code more understandable.

Now, are there any other rotation or shift instructions? I personally manage to get by using only SHL, SHR, SAL, SAR, ROL, and ROR. But you might find the other instructions useful, so I'll very briefly list them here. Check your instruction set listing if you need more information.

The RCL (Rotate Left through Carry Flag) instruction rotates a value and takes each bit that falls out of the most-significant position and puts it into the carry flag, CF. Those bits then fall out of CF and are transferred back to the least-signficant position:

       CF        7   6   5   4   3   2   1   0
     +---+     +---+---+---+---+---+---+---+---+
 /---|   |<----|   |   |   |   |   |   |   |   |<----\      (RCL)
 |   +---+     +---+---+---+---+---+---+---+---+     |
 |                                                   |
 \---------------------------------------------------/

The RCR (Rotate Right through Carry Flag) instruction is similar:

         7   6   5   4   3   2   1   0         CF
       +---+---+---+---+---+---+---+---+     +---+
 /---->|   |   |   |   |   |   |   |   |---->|   |---\      (RCR)
 |     +---+---+---+---+---+---+---+---+     +---+   |
 |                                                   |
 \---------------------------------------------------/

There are also a number of shift and rotate instructions that only work on the 386 and higher processors.

Hardware port instructions

Do you recall hardware ports? It might be helpful to skim parts of Chapter 6, "Hardware Ports", if you want a refresher.

The hardware port output instruction is OUT. OUT takes two operands: the first specifies the hardware port number, and the second specifies the value to send to that port.

If the hardware port number is between 0 and 255 dec, you can specify the port number in "immediate mode", like this:

OUT 20h, AL                           ; Send the contents of AL to port 20h

(Please note that executing some of these examples could cause strange results!)

But if the port number is above 255 dec, you must put the port number in DX:

MOV DX, 2000h                         ; DX = 2000h
OUT DX, AL                            ; Send the contents of AL to port
                                      ;  2000h

(Of course, values between 0 and 255 dec can be put in DX too.)

OUT is equally picky about its second operand: you can specify either AL or AX. Recall that hardware ports are only 8 bits wide. If you specify AL, then the contents of AL are sent to the hardware port that you specify. If you instead use AX, then the least-significant byte, AL, is sent to the specified hardware port, and the most-significant byte, AH, is sent to the next hardware port (the specified hardware port plus one). For example:

MOV AL, 123
MOV DX, 1234
OUT DX, AL                            ; Send 123 dec to port 1234 dec.

MOV AX, 0ABCDh
OUT 50, AX                            ; Send CDh to port 50 dec.  Then
                                      ;  send ABh to port 51 dec.

To input values from hardware ports, use the IN instruction. It has two operands, which are the same as OUT's but are switched around: the first operand is the place to store the value read in, and the second operand is the hardware port number.

The first operand must be either AL or AX. If it's AL, a single byte is read from the hardware port and stored in AL. If it's AX, one byte is read from the hardware port and stored in AL, and then one byte is read from the next hardware port (the hardware port plus one), and that byte is stored in AH.

The hardware port number must be given in the same way as with the OUT instruction -- the port number must be in DX unless it is between 0 and 255 dec.

For example:

MOV DX, 0A3B7h                         ; DX = 0A3B7h
IN AL, DX                              ; Input one byte from port 0A3B7h
                                       ;  and store it in AL.

MOV DX, 4000h                          ; DX = 4000h
IN AX, DX                              ; Input one byte from port 4000h
                                       ;  and store it in AL, then input
                                       ;  a byte from port 4001h and store
                                       ;  it in AH.

IN AL, 0FFh                            ; AL = byte read from port 0FFh

Note that not all ports are readable. For that matter, not all ports can be written to. There are read-only and write-only ports, as well as read-and-write ports... and, of course, there are many unused ports.

That's basically all there is to using hardware ports in assembler. Let's try using these instructions in an actual program.

Sample program using IN and OUT

We can control the printer using hardware ports. Before we begin, it should be noted that there are easier ways to program the printer -- the BIOS provides services under INT 17h to initialize the printer, to send characters to the printer, and to get the printer's status. But since we're using assembler, let's do it the painful way, so we get some practice using the IN and OUT instructions.

There are three hardware ports related to the printer:

OUTPUT DATA       378 hex     Write only
INPUT STATUS      379 hex     Read only
OUTPUT CONTROL    37A hex     Read and write

Note: Some very old, early 80's PC's had a video card called the MDPA (Monochrome Display and Printer Adapter) -- it was for users who couldn't afford the CGA graphics adapter. On this card there are different port assignments: Output Data is at 3BC hex, Input Status is at 3BD hex, and Output Control is at 3BE hex.

    7     6     5     4     3     2     1     0
 +-----+-----+-----+-----+-----+-----+-----+-----+   OUTPUT CONTROL
 | XXX | XXX | XXX | INT | SEL | INI | AUF | STR |   Port 37Ah
 +-----+-----+-----+-----+-----+-----+-----+-----+

 Bit 0:     Strobe:     0 = Normal setting
                        Set to 1 and then 0 to output data to the printer
 Bit 1:     Auto Feed:  0 = Does not send a line-feed character (ASCII
                            10 dec) after carriage returns (ASCII 13
                            dec); this is the default
                        1 = Sends a line-feed character after each CR
                            (double-spacing)
 Bit 2:     Init:       1 = Normal setting
                        Set to 0 and then 1 to initialize printer
 Bit 3:     Select:     1 = Printer accepts characters sent to it
                        0 = Printer ignores data sent to it?
 Bit 4:     Enable Int: 0 = Printer interrupts disabled (default)
                        1 = IRQ7 interrupt enabled on printer ack. pulse
 Bits 5..7: Unused


    7     6     5     4     3     2     1     0
 +-----+-----+-----+-----+-----+-----+-----+-----+   INPUT STATUS
 | BZY | ACK | PAP | ONL | ERR | XXX | XXX | XXX |   Port 379h
 +-----+-----+-----+-----+-----+-----+-----+-----+

 Bits 0..2: Unused
 Bit 3:     Error:       0 = Printer error
                         1 = Printer normal
 Bit 4:     On-Line:     0 = Printer is not on-line
                         1 = Printer is on-line
 Bit 5:     Paper:       0 = Printer has paper
                         1 = Printer is out of paper
 Bit 6:     Acknowledge: 0 = Acknowledge pulse
                         1 = Normal input
 Bit 7:     Busy:        0 = Printer is busy (stop sending data)
                         1 = Printer is ready to accept more data

First, we must initialize the printer. We do this by "strobing" bit 2 of the Output Control port. Bit 2 is normally set to 1. To strobe this bit, we set the bit to 0 and then back to 1. We can send one byte, containing a zero in that position, and then we can send a second byte, containing a one in that position.

We can read the Input Status port to get information on the status of the printer. If there is any error at all, bit 3 is set to 0. If we want to find the cause of the printer error, we can can test each bit corresponding to the different possible conditions. For example, if we wanted to see if the printer is out of paper, we can test bit 5.

To get the printer to print characters, we send each character, one at a time, to the Output Data port. After each character is written to this port, we must strobe bit 0 in of the Output Control port. Bit 0 is normally set to 0, so to strobe this bit, we write a 1 and then a 0.

Here's a short example program demonstrating the use of the IN and OUT instructions with the printer's hardware ports:

------- TEST12.ASM begins -------

%TITLE "Assembler Test Program 12 -- Printer control using IN and OUT"

    IDEAL

    MODEL small
    STACK 256
    LOCALS

    DATASEG

PrinterPort_OutputData            DW   0378h
PrinterPort_InputStatus           DW   0379h
PrinterPort_OutputControl         DW   037Ah

CR                                DB   13    ; Carriage return: ASCII 13 dec
FormFeed                          DB   12    ; Form feed: ASCII 12 dec

PrinterStatus                     DB   ?

    CODESEG

Start:
    ; Make variables in the data segment addressable:
    MOV AX, @data
    MOV DS, AX

    CALL InitializePrinter

    ; Get the printer status:
    MOV DX, [PrinterPort_InputStatus]
    IN AL, DX
    MOV [PrinterStatus], AL

    ; If you wish, you could test the bits in the PrinterStatus variable
    ;  here, to check for errors...
    ; (put your code here)


    XOR AX, AX                         ; AX = 0

    ; Write a character to the printer:
    MOV AL, 'S'
    PUSH AX
    CALL SendCharacterToPrinter

    MOV AL, 'a'
    PUSH AX
    CALL SendCharacterToPrinter

    MOV AL, 'l'
    PUSH AX
    CALL SendCharacterToPrinter

    MOV AL, 'u'
    PUSH AX
    CALL SendCharacterToPrinter

    MOV AL, 't'
    PUSH AX
    CALL SendCharacterToPrinter

    MOV AL, 'o'
    PUSH AX
    CALL SendCharacterToPrinter

    MOV AL, 'n'
    PUSH AX
    CALL SendCharacterToPrinter

    MOV AL, '!'
    PUSH AX
    CALL SendCharacterToPrinter

    MOV AL, [CR]
    PUSH AX
    CALL SendCharacterToPrinter

    ; Comment out the following if you don't want to eject the page.  (If
    ;  you have a laser printer, it may not start printing until the form
    ;  feed character is received).
    MOV AL, [FormFeed]
    PUSH AX                            ; Formfeed char. will eject the page
    CALL SendCharacterToPrinter


    ; Terminate the program:
EndProgram:
    MOV AX, 04C00h
    INT 21h


; ------------------------------------------------------------------------
; InitializePrinter
; ------------------------------------------------------------------------
; Desc: Initializes the printer.
;  Pre: None.
; Post: None.  Registers and flags are not affected.
; ------------------------------------------------------------------------
PROC InitializePrinter
    PUSH AX                            ; Save affected registers and flags
    PUSH DX
    PUSHF

    ; First, get the current output control byte:
    MOV DX, [PrinterPort_OutputControl]
    IN AL, DX

    ; Now, turn off bit 2, as a "pulse" to initialize the printer, and
    ;  ensure that bit 3, "Select", is turned on so that the printer
    ;  will accept data:
    AND AL, 0FBh                       ; AL = AL AND 11111011 hex
    OR AL, 08h                         ; AL = AL OR  00001000 hex

    ; Send back this altered byte:
    OUT DX, AL

    ; Complete the pulse or strobe by sending back the same byte, but with
    ;  bit 2 set to on again:
    OR AL, 04h                         ; AL = AL OR 00000100 hex
    OUT DX, AL

    POPF                               ; Restore registers and flags
    POP DX
    POP AX
    RET
ENDP InitializePrinter
; ------------------------------------------------------------------------


; ------------------------------------------------------------------------
; SendCharacterToPrinter
; ------------------------------------------------------------------------
; Desc: Sends a character to the printer using hardware ports.
;  Pre: Ensure the printer has been initialized.
; Post: The character is printed.  No registers or flags are affected.
; ------------------------------------------------------------------------
PROC SendCharacterToPrinter
    ARG @@CharToPrint:BYTE = @@ArgBytesUsed

    PUSH BP                            ; Save BP
    MOV BP, SP                         ; Let arguments, locals be reached

    PUSH AX                            ; Save affected flags and registers
    PUSH DX
    PUSHF

    ; Send the character to the Output Data port:
    MOV DX, [PrinterPort_OutputData]
    MOV AL, [@@CharToPrint]
    OUT DX, AL

    ; Now, perform the "strobing" in the output control byte, to send the
    ;  character to the printer:

    ; First, get the current output control byte:
    MOV DX, [PrinterPort_OutputControl]
    IN AL, DX

    ; Set bit 1:
    OR AL, 01h                         ; AL = AL OR 00000001 hex

    ; Send back this altered byte:
    OUT DX, AL

    ; Complete the pulse or strobe by sending back the same byte, but with
    ;  bit 1 cleared again:
    AND AL, 0FEh                       ; AL = AL AND 11111110 hex
    OUT DX, AL

    POPF                             ; Restore registers and flags
    POP DX
    POP AX
    POP BP
    RET @@ArgBytesUsed
ENDP
; ------------------------------------------------------------------------
END Start

------- TEST12.ASM ends -------

If you're wondering, saluton means hello in Esperanto, the international language.

Do you want more practice with loops and procedures? You might want to add a procedure to the above program to print a string to the printer. You would need to pass the address of the string to the procedure, and you would print out characters until you reached a terminating character, such as "$" (just like the string print routine INT 21 Service 9) or ASCII 0 (the null-terminator in C/C++).

Summary

In this chapter, we've learned about the multiplication instructions MUL, IMUL, DIV, and IDIV. We've learned about extending values using CBW and CWD. And we saw shift and rotation instructions: SHL, SHR, SAL, SAR, ROL, ROR, and briefly, RCL and RCR. Finally, we've seen how IN and OUT can be used to access hardware ports.

In the following assembler chapters, we'll look at string instructions, macros, equates, and how to interface assembler code with C and C++. And have you found these example programs rather boring? Starting in the next assembler chapter, we'll start seeing some actual game-programming-related example programs -- we'll find out how to draw Mode 13h graphics using assembler, for example. (If you're adventurous, you might even want to give this problem some thought and give it a try. Here's some important hints... Remember how to switch to Mode 13h? Yes, INT 10h Service 0 certainly works in assembler! And the video memory where the Mode 13h pixels are stored begins at A000:0000. You can set up some of the registers to point to this area, and you can read and write bytes using MOV and certain addressing modes. To calculate addresses for pixels, you can either use MUL/IMUL to do the multiplication by 320 dec, or you can use the bit shifting tricks.)

Acknowledgements

The information about the printer's hardware ports comes from some messy handwritten notes I made a few years ago. I believe the source I used was:

Willen, David C., and Jeffrey I. Krantz. "8088 Assembler Language Programming: The IBM PC, Second Edition". USA: Howard W. Sams & Co., 1983-84. ISBN: 0-672-22400-3.

(Page numbers unknown.)

  

Copyright 1998, Kevin Matz, All Rights Reserved. Last revision date: Sun, Feb. 01, 1997.

[Go back]

A project