Device Drivers

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:

(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.


Last modified: Wed Jan 28 13:06:46 MST 2004