By Petter Holmberg
This month: Hardware I/O
Welcome to the fifth part of my assembly tutorial series! During the last months, we've gone through some rough material, but now you should know almost all the basics of assembly programming. We've looked at calculations, memory transfers and flow control. This is enough to make some really cool assembly routines. But there are other aspects of programming in assembler. In this part we will look at the possibilities to do a little more with the assembly language. You can certainly do more than manipulate numbers and the memory in asm, so that's what we're going to look at this time. And maybe we'll also pick up some extra info that we lost on the way here...
IN source, destination
These instructions have equivalents in QBASIC. There you use them in a very similar way. In QBASIC they're called INP and OUT, and the syntax is the same too!
The source for IN and destination for OUT are values specifying the port number. There are many I/O ports, so the number is 16 bits long. You can use a direct number as destination, but it's also common to MOV the number to DX first and use it as source/destination. The values used as destination for IN and source for OUT can be either bytes or words. You should use AX or AL for these tasks. As an example: Suppose you want to write a 1 to port 0487h and then read a value from the same port. Then you could use this program:
MOV DX, 0487 ; Port number.
Don't test this program!!! It was only an example and it won't work. It's a bad idea to experiment with the I/O ports at all if you don't know what you're doing. If you're unlucky you may damage your hard drive or some other sensitive piece of hardware in your computer.
Another important thing to know is that you can't read and write to all ports. Some only works one way, so you may be able to read values from them but not write, or write to them but not read. This depends on the purpose of the port. Some devices, such as the VGA card and many soundboards uses only a few I/O ports for dozens of system functions. This is made possible through a clever technique called indexing: One port is used as an index port and another as a data port. First you write a value to the index port telling the hardware what system function you want to use, and then the data port can work in different ways depending on the value sent to the index port. Exactly how this works varies with the device you're accessing.
Ok, now I've rambled on enough. What can we do with IN and OUT then? Well, you can do really much, but the problem is that it's REALLY hard to control most I/O devices in this way. But let's look at an example anyway:
In VGA mode 13h (SCREEN 13), color 0 is usually black. But it's possible to set color 0 to white, red, yellow or any other color by changing the palette registers. The palette tells the VGA card how much red, green and blue to put on the screen for a certain color index. As default, the intensities are set to 0 for red, green and blue. Using two I/O ports assigned to the VGA card we can change the palette setting of color 0. It's not difficult at all. First, we write a 0 to port 03C8h, telling the VGA card both that we want to change the palette and that we want to change color 0. Then we write three successive values to port 03C9h, telling the VGA card the new intensities for the red, green and blue components of the color. The maximum value possible is 63, so if we set all three components to 63 we'll make color 0 look white:
MOV DX, 03C8 ; Set the port number to 03C8h
If you happened to be in screen mode 13h looking at a blank, black screen, it would become bright white after these six lines of asm instructions.
All right, now you know that you can program I/O devices through the I/O ports. Some I/O devices are fairly simple to program this way, but most I/O devices are a nightmare to access this way. The mouse is a great example of such a device. You must access it in different ways depending on what type of mouse you have, what port it's connected to your computer through and so on. Is there a solution to this problem? Luckily, the answer is yes!
Basically, interrupts are small machine language routines stored in the conventional memory. Most interrupts controls different I/O devices in your computer, but there are interrupts with other functions too. Some interrupts are initialized by the BIOS of your computer when you start it up, some are initialized by the operating system and others are initialized by different device drivers.
Interrupts can be called by a program like we will do, but they may also be called automatially by the computer when something special happens, such as the pressing of a key on the keyboard. Interrupts called by programs are called software interrupts and interrupts called by the hardware are called hardware interrupts. You can have use of both types.
When an interrupt is called, the computer immediately interrupts what it's doing and runs the interrupt code. When the interrupt code has been executed, the control is returned to the code that was interrupted. Guess why they call it interrupts ;-)
Calling interrupts in assembler is easy. You can do it through an instruction called INT. The syntax is:
The number you pass with the INT instruction is the number of the interrupt you want to call. The number should be a one byte immediate number. Many times you can access many different subfunctions through one interrupt. By setting some of the registers before the INT call, you can tell the interrupt code what subfunction you want to access and what parameters you want to pass to that subfunctions. Like with the I/O ports, some interrupt routines returns data to you, some wants you to send data to them and some works both ways. You can do many different things with interrupts, but let's begin by using them to access an I/O device that you usually cannot access in QB: The mouse!
The mouse can be controlled through interrupt 33h. There are lots of subfunctions assigned to that interrupt. You tell the interrupt what subfunction you want to use by putting a number in the AX register. Additional registers can be used to pass parameters to the subfunction. The subfunction may also return data to you in the registers.
First of all, it would be nice to know if we have a mouse installed and a working mouse driver. The very first subfunction of int 33h can do this test for you. This subfunction has the number 0, and you don't have to send any parameters with it. This asm code would make the call:
MOV AX, 0 ; Access subfunction 0 of int 33h (detect mouse)
When the interrupt code has been executed, the line after the INT call would be called. The interrupt has now returned the current status of the mouse in the AX register, and additional information in the BX register. The AX registers tells you if there's a mouse installed. If it's set to -1, a working mouse was detected. If it's 0, you're apparently without a working mouse. If the subfunction found a mouse, you can also see how many buttons the mouse has by looking at the number in the BX register.
When you have detected a working mouse, you might want to display the mouse cursor on the screen. This can be done with subfunction 1. All you have to do is to set AX to 1 and call the interrupt:
MOV AX, 1 ; Access subfunction 1 of int 33h (display mouse cursor)
If you want to hide the mouse cursor, you can call subfunction 2 in the same way.
Earlier parts of Petter Holmberg's assembly series can be found in the Archive. They are in issues 4 thru 7.
Now you may want to get some information about the current position of the mouse cursor and the state of the mouse buttons. Subfunction 3 takes care of this for you:
MOV AX, 3 ; Access subfunction 3 of int 33h (get mouse status)
After this call, BX should tell you the current state of the mouse buttons. If bit 0 of BX is set, the left button is currently being pressed, bit 1 returns the state of the right button and bit 2 returns the state of the middle button if you have a three-button mouse.
The current coordinates of the mouse cursor can be found in CX and DX. CX contains the horizontal coordinate and DX contains the vertical coordinate.
Another interesting interrupt is interrupt 21h. This interrupt has tons of subfunctions installed by DOS. And don't worry Win95/98/NT users, you have them too!
Int 21h has many DOS-assigned subfunctions. For example, you can change the default directory, open and read/write data in files and so on. But there are also a couple of functions that does some other neat things for you, such as printing a string of text on the screen. Let's try it out!
Int 21h, subfunction 09h can be used to print a string on the screen. Here, you'll need to put the number 09h in the AH register, the segment address of the string in DS and the offset address of the string in DX. The string must be terminated with a $ sign, so if you want to print "Hello World!" on the screen, the string should be: Hello World!$. This example routine could print such a string on the screen for you.
MOV DX, [BP+08] ; Set DX to offset address of string
Another interesting interrupt is interrupt 10h. It contains subfunctions initialized by your BIOS. This is a great way of creating compability between different PC:s Even if two PC users have different BIOS manufacturers with hardware working in different ways, they can both access the same functions in the same ways, using the same interrupts. The difference lies in the actual machine language routines assigned to those interrupts, but the user doesn't need to worry about them.
Int 10h contains many interesting subfunctions for accessing video services, such as the ones you use to change screen modes. Through int 10h you can access many more screen modes than you can do using the SCREEN keyword in QB, such as hi-res VESA modes!
The ultimate resource when you need to look up information about interrupts is Ralph Brown's interrupt list. No assembly programmer should be without it! It tells you what the different interrupts and subfunctions do, what you should set the registers to when calling them and what data they return. You can download it from the resources section at the Enhanced Creations website, or from many other sites. Just search for it and you'll get thousands of hits! It's a big download, but it's worth it!
I remember when I first discovered interrupts. It was so much fun, because I instantly got access to functions and I/O devices that aren't possible to access in QB. Interrupts give you a lot of power as a programmer, and they make life a whole lot easier for you if you program in assembler. I'll leave you to explore the world of interrupt functions yourselves now. It can be really fun! Int 10h, 21h and 33h are good starting points.
I said before that it was possible to do much more with interrupts than this. Actually, you can do a lot more. It's possible to tamper with hardware interrupts as well, and if you want to, you can make your own interrupt functions and override existing interrupts with your own routines! The example program this month is a good example of advanced interrupt handling. I though we should write our own keyboard handler. As you may have noticed when working in QBASIC, the keyboard handling is great, unless you want to make an action game. It's impossible to detect multiple keypresses with INKEY$, something you often need in games.
We can solve this problem with a little assembly code. The keyboard is controlled through int 09h, and it's a hardware interrupt that is called whenever a key is pressed or released. If we can override this interrupt routine with a keyboard routine that we write ourselves, we should be able to handle the keyboard input in a more game-oriented way.
The first thing we need to know is how we can make int 09h run our assembly code instead of the one it's running right now. Well, believe it or not, there is an interrupt subfunction assigned to make just this change for you! This subfunction lies under interrupt 21h. It can change the memory address of the code that should be executed when you make a certain interrupt call. However, it would be nice to know the address of the old keyboard routine so we can give it the control back when we exit our program. There's a subfunction under int 21h for that too. But let's begin by writing the actual keyboard handler:
PUSH DS ; Push everything we're going to modify on the stack
This is how I've thought the keyboard handler should work: We'll create a QB integer array with 128 elements. Each one will work as a keyboard "flag". So if a certain element is 0, the key with that scancode is not being pressed at the moment, but if it turns to 1, that key is being pressed. All QB arrays has a default offset address of 0, so all the asm routine needs to know is the segment address of the array. But how can we pass this value to the keyboard handler from BASIC? This is not a routine we call with CALL ABSOLUTE. Well, there's a really smart solution to this, but we'll wait with this problem. All we do is to set BX to an immediate number for the moment:
MOV BX, 1234 ; DS = SEG keyboard flag array (currently not implemented)
Now comes the reading of the keyboard status. How do we do this? Through I/O ports of course! Exactly how this works is beyond my knowledge, I've only looked at other keyboard handlers to see how they do it. First we should read a byte from port 60h. We will also set AH to 0 for later use. If the value read from the port is bigger than 127, a key has been released. But first, we'll take care of keypresses:
IN AL, 60 ; Read a value from port 60h
Now it's time to take care of the keyboard array. If the jump to the Release label wasn't done, it means a key has been pressed and we should set an element of the keyboard array to 1. The array consists of two-byte integers, and the number currently in AL is between 0 and 127. So if we multiply AL by two, we should get the correct offset address in the array. We want to write a 1 to this offset address since a key was pressed, so that's what we'll do:
All right, that should take care of keypresses. Now it's time for releases. If a key is released, AL will contain a number between 128 and 255. If we subtract 128 from this, we get a number between 0 and 127 which is what we want. But we don't need to use a SUB here. All we need to do is to mask out the highest bit of AL, and we'll get the same result. We can do this by ANDing the number with 127. Then we'll do the same as we did with keypresses, except that we write a 0 to the array this time:
Now we're done with the array handling. All we need to do is to exit our interrupt handler and return to the code that was interrupted by it. Well, not really. The keyboard needs some more attention before we can leave it. We need to tell the keyboard that we recieved the information it sent us and that we want it to tell us of later keystrokes too. This is also a matter of I/O port programming that I don't really understand. So let's just do it without asking any questions :-)
All right, NOW we can exit the keyboard handler. All we need to do is to POP back the registers we used and then we use the Interrupt Return instruction, IRET, to end the routine:
POP BX ; Pop back all the registers we used
All right! Now we have a complete keyboard handler except for that problem with the array segment. For some strange reason, this keyboard handler won't run through Absolute Assembly, even though I've made it work before. Since I didn't have the time to fix this problem, I'm going to give you the BASIC code as it would have looked if it had been working. We'll also add the keyflag array to the program. Save this in a file called KEYDEMO.BAS:
' A demonstration of advanved interrupt handling in QB:
Now, take a look at the line of asm code that was supposed to set BX to the segment address of keydata%() but only set it to 1234h:
newkey$ = newkey$ + CHR$(&HBB) + + CHR$(&H34) + CHR$(&H12) ' MOV BX, SEG keydata%(0)
Look at the hexadecimal numbers: First comes a BBh, which is the machine language equivalent to MOV BX, immediate number. Then comes the numbers 34h and 12h. Didn't we just set BX to 1234h? There's an obvious pattern here! Here comes the trick: What if we changed the numbers a little here. If we put other numbers there, it wouldn't make any difference to the routine. What if we put the actual segment of the keyflag array there. It would work as good as any other number, but we would also get a working routine then. Of course we need to convert that number into a string, but that's really easy here. Just change the line to this:
newkey$ = newkey$ + CHR$(&HBB) + MKI$(VARSEG(keydata%(0))) ' MOV BX, SEG keydata%(0)
And that's it! Everything will work fine now.
Ok, that was the actual keyboard handler. But we haven't installed it yet. We need one routine that can install it, and antother one that can remove it. Let's begin with the first one:
The routine that initializes the keyboard handler will use two subfunctions of int 21h. The first one, subfunction 35h tells you the address of the current interrupt handler. If we set AH to 35h and AL to 9h (telling the subfunction that we want to get the address of int 9h, the keyboard handler) and call int 21h, it will return the current address of int 9h in ES:BX. All we need to do is to fetch these values and return them to two QB variables for storing. The other subfunction is subfunction 25h, which changes the address of the interrupt. (Actually, it's usually called the interrupt vector.) We only need to set AH to 25h, AL to 9h, DS:DX to the address of the newkey$ string and call int 21h. When this is done, our keyboard handler has taken over the control. I'm not going to describe every step in this routine like I've done before. There's nothing new in this routine, so I'll leave you to figure out how it works. It can be a good exercise for you to figure out exactly how this routine works. Regardless if you care or not, just add this code to the BAS file:
The three lines above the CALL absolute code may seem confusing, but here's the explanation: Before installing the new keyboard handler it's a good idea to save the state of the keyboard flags. Don't mix this up with the flag array that we created for our own keyboard handler. The keyboard flags I'm talking about right now are a couple of bits stored at a fixed position in the memory by the default keyboard handler. They tell you the state of some of the keys on your computer, such as the Num Lock, Caps Lock, Scroll Lock, Alt, Ctrl and Shift keys. If we don't save the state of these keys before we install our keyboard handler and we hold down, say, a shift key before the new keyboard handler is installed and release it while this keyboard handler is in control, the old keyboard handler will think it's still being held down when we remove the new keyboard handler. Wow, that DID sound confusing, didn't it? Well, don't mind about it. We just save the state of these flags before installing our keyboard handler so we can set it back to that state afterwards.
Now all we have left is the routine that removes our keyboard handler and returns the control to the old one, and some additional BASIC code to test our keyboard handler with.
We'll just make it a simple demo that constantly prints the state of every element in the array on the screen so we can see if our keyboard handler reacts on the pressing and releasing of different keys. It's also important that we end the loop when we see that the ESC key is being held down, or otherwise we'll get stuck in the demo program. With our new keyboard handler it's not possible to use Ctrl-Break to abort the program. Our keyboard handler has the full control over the keyboard, remember? The state of the ESC key is stored in keydata%(1), so all we need to do is to wait for it to become 1. The routine that removes the handler only needs to use in 21h, subfunction 25h to put the old interrupt address back. And finally, we must set those keyboard flags to the state that they were in before we installed our keyboard handler:
And that's it! This was an example of advanced assembly handling, but it wasn't too hard to understand, was it? There's much you can do with custom interrupt handlers. You can also make interrupt handlers that doesn't take over the control of the old one, but merely runs first and then passes over the control to the old interrupt handler. Some old computer viruses worked like this, but you should use them for nicer things :-)
I think this is enough for now. I'm sure you already have ideas for what you want to use this newly aquired knowledge for. If you want to look more at I/O ports I suggest that you go and find some document about advanced soundcard- or video programming. Just don't blame me if you fry your monitor ;-) If you want to play with interrupts more, download Ralph Brown's interrupt list and start with something easy, like making mouse routines or using basic DOS interrupts. If you want to do something more advanced, try writing interrupt handlers for other things than the keyboard. One thing you can do is to make interrupt routines that are called automatically 18.2 times per second. This can be useful for many things.
If you run into problems, (it's easy to do when programming these things) try hard to find them on your own. I've discovered that it's easy to start making the same, small mistake over and over again when you're writing asm code. If you find these annoying habits of yours by your own, you will usually never make them again. It's easy to forget simple things, such as that a certain register won't work in certain conditions, how the stack looks at the moment and that you accidently deletes some important number by overwriting it with another. The only thing you can do is to practise and practise until you get so used to assembly programming that you know where the potential traps are.
I'm planning to write a part 6 of this tutorial series, but I still haven't figured out what it's going to be about. Probably a lot of things that I haven't discussed yet. So see you next time, and good luck with your coding!
Back to Top