A device driver is a piece of code that your program uses to communicate with a hardware device. Normally device drivers are part of the computer operating system; in fact, most of the code in any operating system (I keep saying there are exceptions to every rule; I think this may be the exception to that one) is the device drivers.
The purpose of a device driver is to raise the level of abstraction of
our communications with the device, which has the following advantage:
we can write our code without knowing what the underlying device looks
like. This doesn't sound like much, until we realize that a floppy
disk's hardware interface has almost nothing in common with a hard
disk interface, and neither one looks anything like a keyboard. But
we can use a read()
call to the operating system to get
data from any of the three. Our program doesn't have to worry about
turning the drive motor on and off and waiting for it to come up to
speed (needed for floppy disks), or doing a data transfer from a
logical block address (needed for hard disks, but floppies need us to
translate our block number into a physical location on the disk) or
understand keyboard scan codes (needed, of course, for keyboard
input. Note that different keyboards supply slightly different scan
codes).
Controlling the motors is a good example of a good application for a device driver. The actual motor interface is very low-level; we have eight bits controlling the four motors, with one bit for on/off and one for direction. It would make a lot more sense if we could write a procedure that would take two arguments (which motor we want to change, and what we want to do with it), and translate that into the actual motor commands. So how can we do that?
Let's start with the API (application programmer's interface). We'll specify the motor API like this (this is going to be pretty sloppy -- you'll be learning more about real program specification in CS371):
signed char motors(unsigned char motorno, signed char speed)
where motorno
specifies which motor we want to control,
and speed
specifies what speed we want it to go. +127 is
full speed forward, 0 is stopped, and -128 is full speed reverse.
Our four motors are numbered 1 through 4. We can also use motor 0 as a ``broadcast'' address; anything we do with motor 0 affects all of the motors. If we ever port the code to a machine with more motors, we have room for future expansion (motor 5, 6, 7, ...).
In Unix, system calls normally return a non-negative number for a successful result, and a -1 for an error. Since most of my experience is with Unix, we'll return 0 on success and -1 on failure.
Why do we specify speed when our motors only do forward, stop, and reverse? Future expansion. If we port to a system that has speed control, we can take advantage of it. For that matter, if we want to implement speed control for our motors by turning them on and off rapidly, we can handle that too.
We'll pass parameters on the stack, since that's the most common way of passing them.
What does a typical call to the driver look like?
ldaa #FULLAHEAD psha ldaa #RIGHT psha jsr motors ins ins
Now, what does the driver look like? Ah, that's going to be a bit trickier. A couple of points going in:
Developing the code, let's start by writing it in C (I'll admit, by the way, that this isn't my first draft of this code. I wrote it, translated to assembler, tried to optimize, found that it didn't want to optimize cleanly, and changed the C code to make the optimization work better):
signed char motors(unsigned char motorno, signed char speed) { static unsigned char enable[5] = {0xf0, 0x10, 0x20, 0x40, 0x80}; static unsigned char forward[5] = {0x0f, 0x01, 0x02, 0x04, 0x08}; if (motorno > NUMMOTORS) return(ERROR); if (speed == 0) motorport &= ~enable[motorno]; else { if (speed > 0) motorport |= forward[motorno]; else // speed < 0 motorport &= ~forward[motorno]; motorport |= enable[motorno]; } return(SUCCESS); }
So now we translate this into HC11 assembler. We need three scalar constants, one symbol defining an address, and two arrays of constants. We'll also be finding some constants we need on the way.
In this code:
0,y
to dereference it.
(note - this code isn't debugged yet! It's gotten quite a bit of optimization, though)
NUMMOTORS equ 4 ERROR equ -1 SUCCESS equ 0 MOTORPORT equ $1004 ENABLE fcb %11110000,%00010000,%00100000,%01000000,%10000000 DIRECTION fcb %00001111,%00000001,%00000010,%00000100,%00001000 motors MOTORNO equ 7 SPEED equ 8 pshb * save old B register pshx * save old X register pshy * save old Y register tsx * point X at activation record ldab MOTORNO,x * if (motorno > nummotors) cmpb #NUMMOTORS bls motgd ldaa #ERROR * return(ERROR) bra motret motgd tst SPEED,x * if (speed == 0) { bne motnz ldy #ENABLE aby ldaa 0,y * a = enable[motorno] coma * a = ~enable[motorno] anda MOTORPORT * a = motorport & ~enable[motorno] bra motgdrt * } else { motnz ldy #DIRECTION aby * ldaa 0,y * a = direction[motorno] tst SPEED,x * if (speed > 0) blt motlt * oraa MOTORPORT * a = motorport | direction[motorno] bra moton * else motlt coma * a = ~direction[motorno] anda MOTORPORT * a = motorport & ~direction[motorno] * * } moton ldy #ENABLE * aby oraa 0,y * a |= enable[motorno] * * } motgdrt staa MOTORPORT * motorport = (calculated new value) clra * return(SUCCESS) motret * get rid of local var puly * restore registers pulx pulb rts
As another note, I was quite pleasantly surprised to find out I didn't
need a temporary variable out in memory. My original version of the
C code had the motorport |= enable[motorno]
line as the
first thing in its else
block; in that version, a
temporary in memory was needed (I found myself needing to do a logical
operation on the values in the A and B accumulators.... and the HC11
doesn't do that). Moving the line fixed it.