Device Control


A MIDI Device Driver for XENIX

Danny Lawrence


Danny Lawrence is the systems manager for the Grants Pass Daily Courier in Oregon. He has been programming professionally for seven years. He can be contacted at 1310 Rogue River Hwy #5, Grants Pass, OR 97527, (503) 479-1801 or (503) 474-3809.

Several MS-DOS programs support the Roland MPU-401, a common MIDI interface for MS-DOS machines, but you can't use the MPU if you run UNIX or XENIX. Why? No drivers. In MS-DOS, every program has access to I/O ports and interrupts, but in XENIX, user processes are unable to talk directly to the hardware. They can only communicate with a device through a driver. Since no drivers are available, no programs can use the MPU.

Its multitasking capabilities would seem to make XENIX a natural for MIDI applications. If applications were available, users could run patch editors, librarians, sequencers, and other MIDI applications simultaneously. MIDI connections between processes could be established through the XENIX IPC facilities. Virtual MIDI mergers and patch bays could be in software.

Unfortunately, XENIX is a timeshared system, not a real-time system. Implementing a sequencer in a user process probably wouldn't work too well. The kernel just cannot guarantee anything would be done in time.

The driver presented here doesn't solve this timing problem, but can go a long way toward building other less time- critical applications like patch editors and librarians. This driver implements the full MPU command protocol in intelligent mode, and can toggle the MPU between UART and intelligent mode.

Device Drivers in XENIX

The XENIX kernel itself is device-independent. The kernel communicates with devices through well-defined entry points in device drivers. The drivers hide the low-level details related to specific devices to I/O porting and handling device interrupts.

Through drivers, the kernel makes all devices look like files. Processes open the device, read and write, then close it, just as they would do to files. For example, if you want to print something on the printer, you open /dev/lp and write to it. When /dev/lp is opened, the kernel discovers that /dev/lp is really a device and not a regular file. The kernel then routes the byte stream through the driver. Unless your program takes special action, it can't tell if it is writing to a device or a real file.

Because devices must look like files, they must have entries in the file system. These entries in the file system are called device nodes or device special files. When you do a long listing of files in /dev, two numbers separated by a comma replace the normal file size. These numbers tell users and the kernel that the file is actually a device.

The first number is the major device number. The kernel uses this number to determine which driver to call. The second number is the minor device number. The kernel uses this to differentiate between units using the same driver, such as different serial ports on a multiport board.

A driver is just a function library. The kernel calls the driver functions on behalf of user processes whenever it needs the driver or device to do something. When you configure the kernel for the new driver, you are telling it which functions are included in the driver.

Drivers are linked with the kernel and have all the processor privileges of kernel code, so you must be careful when you're writing and testing. If the driver has a bug, you don't just get a core dump; you usually end up crashing the system.

Programming the MIDI Card

I actually wrote this driver for the Music Quest MIDI card (MPU-401 compatible). The MPU runs in two modes: UART and intelligent or co-processor mode. On power up and after reset, it is in co-processor mode.

In UART mode, the MPU acts almost exactly like a serial port. Bytes sent to the port are passed to the midi port without any processing. Bytes received from the midi port are buffered and made available to applications. No event timing information is available.

In intelligent mode, the MPU becomes a co-processor, relieving the CPU of almost everything having to do with timing and sequencing. It has functions for recording and playing midi events and messages, buffering midi data, filtering and clocking.

In intelligent mode, the CPU no longer must handle timing during record and playback. When recording, the MPU tags each event with a timing indicator as it is received. This timing tag is passed to the CPU along with the event data. All the CPU has to do is store timing and event data in a buffer. On playback, the timing and event information is passed back to the MPU in the same order as it was recorded. The MPU plays back the events at the right time.

Both modes are useful. The MPU has more buffer space in UART mode, so this mode is good for patch editors, sample editors and sysex dumps; things that transfer a lot of data, but aren't so timing critical. Intelligent mode is best for sequencing.

The CPU and the MPU communicate through two I/O ports and an interrupt. Data is transferred a byte at a time by reading or writing to BASE, the data port address. The status and command port is located at BASE+1. Switches on the board allow you to configure several BASE addresses and interrupts.

Two negative-logic bits, the Ready-to-Receive (RR) bit and the Data-Available (DA) bit, in the status byte determine whether the MPU is ready to receive a command or data, or has data ready for the CPU. When these bits are low, the MPU can respond to commands or transfer data.

Communication between the CPU and the MPU follows a simple protocol. To send a byte to the MPU, wait until the status register says the MPU is ready to receive, then output the byte to the data port. The MPU does not generate an interrupt when it is ready to receive; the status port must be polled repeatedly until the MPU is ready. MS-DOS drivers usually employ busy-waiting, but a XENIX driver should go to sleep whenever the MPU is not ready. The next clock tick awakens the driver.

Reading a byte from the MPU is similar, however, the MPU does generate an interrupt whenever it has data to send to the CPU. If the status byte indicates data is available, the driver reads a byte from the data port. If the status byte says data is not available, the driver will go to sleep until the MPU interrupts.

Sending a command is a little more complex since a command may have operands or results, and the MPU may be receiving MIDI at the same time you are trying to give it a command.

The CPU sends a command to the command port when the MPU indicates it is ready to receive. Then the MPU returns a command acknowledge (ACK) and possibly a result code. Any required command operands must be sent out the data port as the very next data bytes sent following the command ACK.

The complication comes while the CPU waits for the ACK. If it reads the data port and finds a byte other than ACK, the byte read must belong to some other data stream. The CPU could poll the data port for several bytes while it awaits the command ACK. The driver queues up these bytes for the read routine to get later.

No commands except Reset can be given to the MPU unless the MPU indicates it is ready to receive. Because it uses a different protocol, Reset can be given almost any time.

On all other commands, the CPU learns if the MPU acknowledged the command by waiting for the ACK byte on the data port. If the MPU is in intelligent mode, it will return the ACK right away. When it is reset but doesn't acknowledge, the MPU is in its other mode, the UART mode. So, this driver repeats the Reset command if it doesn't receive an ACK after a given timeout. If the MPU was in UART mode, it will have switched to intelligent mode and will now be able to acknowledge the second Reset.

The Code

This simple character driver (see Listing 1 and Listing 2) implements no complicated queueing. I assumed the MPU can buffer just fine by itself. Only the interesting routines are highlighted here. For detailed information on driver entry points and kernel support routines, see Egan (1988) in the references.

Two details complicate the driver:

mpuioctl is used to send commands to the MPU and to retrieve response codes. It is also used to get and set certain parameters in the driver, such as debug level and driver version ID.

Applications call ioctl with a command number and a pointer to a struct containing command arguments and a place for the results. The structure looks like this:

   struct mpustuff {
        int opsize;
        int ressize;
        char
        *opbuf;
        char *resbuf;
   };
opsize is the number of command operand bytes contained in opbuf. ressize is the number of result bytes to retrieve from the MPU and place in resbuf.

For a command with no operands and no results, mpustuff would be used like this:

struct mpustuff m;

m.opsize = 0;
m.ressize = 0;
m.opbuf = NULL;
m.resbuf = NULL;
 ret = ioctl(fd, CMD_CODE, &m);
if (ret == -1) { error message }
Whenever the MPU generates an interrupt, it calls the interrupt routine, mprintr. The interrupt routine just wakes up any sleeping readers. A more complex driver implementing sequencer primitives might have the interrupt routine automatically send the MPU the next MIDI event without even telling the application.

Compiling and Installing the Driver

The driver should be compiled with the same compiler switches and preprocessor defines used to compile the kernel. Stack probes must be disabled and structures must be packed as in the rest of the kernel. Reasonable optimizing can be used, but beware, optimizers sometimes really blow your code. See the sidebar Steps to Installing the Driver.

Using the Driver

Using the driver in your applications is straightforward. Open /dev/mpu and begin sending commands to the MPU using ioctl. Read and write data using the normal read and write system calls.

A weakness of this driver is the gaping hole in the ioctl routine. If a bogus pointer or nonsense-size field is sent, the driver could go into never-never land, blow the kernel or lock up the MPU — probably not what you want. So just be careful you pass decent pointers and give correct sizes. If the operand or result sizes are wrong, the protocol will get out of sync.

More Code

If you would like a copy of a MIDI trace utility, in addition to the source code, call me at the numbers listed in my bio. (Source code for the driver is also available on the CUG code disk, Usenet, or BIX.) The trace utility allows you to display the MPU version and revision numbers, get and set driver debug level, and display an interpreted MIDI trace, an unformatted hex trace, or raw bytes to be captured in a binary data file. The trace utility can also be used to send a binary file out the MIDI port, as in a super cheap patch librarian.

Future

I hope this driver will generate some interest in using XENIX for MIDI applications. It is a good starting point for MIDI experimentation under XENIX. It can easily be ported to other flavors of UNIX such as Interactive, or Microport.

I would like to see somebody implement sequencer primitives in the interrupt routine to get around XENIX timing constraints. I would also like to hear of any bugs in the driver and any improvements anyone makes.

References

Egan, Teixeira. 1988. Writing a UNIX Device Driver. John Wiley & Sons, Inc.

XENIX Programmer's Reference Guides.

MIDI Co-Processor Card Technical Reference, 1988.