Interrupts are an invaluable resource in QuickBasic programs. They give potential access
to XMS, EMS, file routines, MSCDEX and many others. While this document wont tell you
how to write an EMS handler, it will show you how you go about tackling the problem from
within QuickBasic. I will first explain what interrupts are and then will proceed to write a small
BMP image loader.
You can think of an interrupt as a SUB or FUNCTION. You Call them with an amount of
parameters, they perform some operation and may return a result. The name "Interrupt" simply comes
from the fact that you are interrupting the CPU's current task and requesting that it do something
else. There are a maximum of 256 (&H0 to &HFF hex)
different interrupts and most aren't actually used. But each interrupt may have several sub-functions.
For example the main DOS Interrupt is &H21. Through this one interrupt you can print characters, open files,
get a directory listing and get system info. You can specify what sub-function you want performed by
passing a sub-function number in the AX register. Which brings me to the next topic ..
Passing Data and the CPU
We need a way to communicate with these interrupt functions as we may want to pass them data
to manipulate. Here's where we use the CPU. The CPU has a number of Registers which are the
same size as an INTEGER in QuickBasic. What we do is, give all the data to the CPU and tell the CPU
to call the Interrupt, and that's it! Actually, it's a lot easier than it sounds as we don't have
to even touch the CPU directly. We use a TYPE structure whose fields represent the CPU's registers
and use a supplied QuickBasic function InterruptX to do all the dirty work.
Here's the TYPE structure:
TYPE RegTypeX
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
ds AS INTEGER
es AS INTEGER
END TYPE
This TYPE structure and declarations for InterruptX can be found in the QB.BI file. So make sure you
'$INCLUDE that at the top of your program.
Let me explain what each of those weird symbols represents.
AX - used mainly for choosing which interrupt sub-function to use. Also used for returning values
BX - general/any use.
CX - general/any use.
DX - general/any use.
BP - not often used for our purposes.
SI - used with DS
DI - used with ES
FLAGS - not often used for our purposes.
DS - Used together with SI for passing a memory address of something
ES - Used together with DI for passing a memory address of something
Another thing that needs mentioning is that some of these registers can be split into two.
When you split an integer in two, you get 2 bytes; a High byte and a Low byte. The registers
capable of this are: AX, BX, CX and DX. When split into two,
AX forms AH and AL
BX forms BH and BL
CX forms CH and CL
DX forms DH and DL
As an example here's how AH and AL combine to make AX:
AX Register
| |
AH AL
00000000 00000000
The 0's just represent the number of binary digits in each register. A byte (or half an integer)
contains eight binary digits (or Bits). AH and AL each have one byte (eight Bits) and AX
takes AH and AL to use 16-Bits (an Integer). In Hexadecimal only 2 Digits are needed to represent a byte.
So if you change AH or AL, you're changing AX too.
Unfortunately, the RegTypeX structure doesn't give us direct access to the smaller High byte
and Low byte registers, so we have to do it the long-way-around and use the bigger register to set the
smaller one. For example, if we wanted to set AH to 5 in our inregs structure we would do the following:
inregs.ax = &H0500
An example
So what are the steps needed to call an interrupt? Well, what we do is, DIM two copies of this RegTypeX structure: one for data going in
and the other for data coming out. These will be called inregs and outregs respectively.
Before we call an interrupt, we fill in the required fields in our inregs structure. If the
interrupt is supposed to return any data, it will enter it into the outregs structure internally.
This can then be checked by you upon exit of the interrupt. As a quick example, here's how
you would get the DOS version number using an interrupt:
'$INCLUDE: 'qb.bi'
DIM inregs AS RegTypeX ' Stuff going in
DIM outregs AS RegTypeX ' Stuff coming out
' To get the DOS version, we use (obviously) the DOS interrupt and sub-function &H30
inregs.ax = &H3000
CALL InterruptX(&H21, inregs, outregs) ' Returns Major version in AL, minor in AH
maj$ = RTRIM$(STR$(outregs.ax AND 255))
min$ = LTRIM$(STR$(outregs.ax \ 256))
PRINT "DOS Version:"; maj$ + "." + min$
If you want to test this program, you will have to start QuickBasic with the /l switch. This loads
in the default QB.QLB library which contains the InterruptX function as well as others.
So hopefully now you have at least some understanding of interrupts. One thing that confused me
when I was learning this is, how do I find out which interrupts and sub-function numbers do what?
Well, there are references on the internet, specifically
Ralf Brown's Interrupt List. Also available are offline references such as
HelpPC which contains many things including detailed interrupt listings.
The BMP loader
Yes, I mentioned I was going to take you through the steps of writing something like this, so here it is =). You will find
this capable of loading a BMP in less than a tenth of a second, even on a 486! (Yes, some people still use them, including me :P).
The reason we're going to use interrupts (specifically the DOS interrupt, &H21) is because QuickBasic doesn't provide
the feature we need to load a BMP quickly enough. What is this feature? It's the ability to copy a series of bytes from a
file to a location in memory. While QuickBasic does do this, it's not as flexible as we'd like. Now because we're using
DOS file access routines, we'll need to use DOS to Open and Close the file too.
Here's the entire source needed to load in a bitmap of any size in 256 colours:
'$INCLUDE: 'qb.bi'
TYPE BMPHeaderType
id AS STRING * 2 'Should be "BM"
size AS LONG 'Size of the data
rr1 AS INTEGER '
rr2 AS INTEGER '
offset AS LONG 'Position of start of pixel data
horz AS LONG '
wid AS LONG 'Image width
hei AS LONG 'Image height
planes AS INTEGER '
bpp AS INTEGER 'Should read 8 for a 256 colour image
pakbyte AS LONG '
imagebytes AS LONG 'Width*Height
xres AS LONG '
yres AS LONG '
colch AS LONG '
ic AS LONG '
pal AS STRING * 1024 'Stored as
END TYPE
DIM inregs AS RegTypeX ' Data IN
DIM outregs AS RegTypeX ' Data OUT
DIM FileHandle AS INTEGER ' Handle used to reference the BMP file
DIM BMPHeader AS BmpHeaderType ' The BMP header
file$ = "demo.bmp" + CHR$(0) ' BMP file. DOS requires us to append a CHR$(0).
inregs.AX = &H3D00 ' Open the File
inregs.DS = VARSEG(file$) ' Segment address of the string
inregs.DX = SADD(file$) ' Offset address of the string (we use SADD
' when working with variable length strings)
CALL InterruptX(&H21, inregs, outregs)
IF outregs.FLAGS AND 1 THEN ' Check it opened ok
PRINT "Error opening >> " + file$
BEEP: END
END IF
FileHandle = outregs.AX ' Save the file handle
inregs.AX = &H3F00 ' Read in the header
inregs.BX = FileHandle
inregs.CX = 1078
inregs.DS = VARSEG(BMPHeader)
inregs.DX = VARPTR(BMPHeader)
CALL InterruptX(&H21, inregs, outregs)
SCREEN 13 ' Set dislay mode
a$ = BMPHeader.pal ' Change the palette
OUT &H3C8, 0
FOR i% = 1 TO 1024 STEP 4
b% = ASC(MID$(a$, i%, 1)) \ 4 'blue
g% = ASC(MID$(a$, i% + 1, 1)) \ 4 'green
r% = ASC(MID$(a$, i% + 2, 1)) \ 4 'red
OUT &H3C9, r%
OUT &H3C9, g%
OUT &H3C9, b%
NEXT
PixelBuffer$ = SPACE$(BMPHeader.wid)
iHeight% = BMPHeader.hei - 1
Segment% = &HA000 ' Address of SCREEN 13 memory
inregs.AX = &H3F00
inregs.BX = FileHandle
inregs.CX = BMPHeader.wid
FOR y% = iHeight% TO 0 STEP -1
Offset% = VAL("&H" + HEX$(320& * y%)) ' Hack to get integers bigger than 32767
inregs.DS = Segment%
inregs.DX = Offset%
CALL InterruptX(&H21, inregs, outregs)
NEXT y%
inregs.DS = 0 ' Close the file
inregs.DX = 0
inregs.AX = &H3E00
inregs.BX = FileHandle
CALL InterruptX(&H21, inregs, outregs)
SLEEP
That code is ready to cut and paste. To compile or run, you'll need to start QuickBasic with the /l switch.
Of course, I don't mind what you do with this code, a good idea would be to encapsulate it into
a SUB so all you'd need to call is something like LoadBMP "demo.bmp". Have fun!