Originally Published: Monday, 30 April 2001 Author: Cherry George Mathew
Published to: featured_articles/Featured Articles Page: 3/5 - [Printable]

Creating a Kernel Driver for the PC Speaker

Take a jaunty look at the basics of driver development as Cherry George Mathew guides us through the process of creating a driver for the PC speaker under Linux.

Linux Here We Come  << Page 3 of 5  >>

Linux here we Come.

The Linux kernel is an amazing piece of programming in that it has been organized so well that a person with little or no knowledge of assembly language can write a lot of kernel code (in fact 99% of the kernel is written in "c"). It is also designed in such a way that device driver writers are given a preset environment and an elegantly exhaustive programming interface to write code.

The kernel code is very portable, i.e., it can be compiled on a variety of machines (processors like the i86, alpha, sparc). I think it makes good logic to write code templates, which can be elaborated and tailor made for individual hardware. In English, I could best illustrate this principle with an example. Suppose that you want to publish a Phd thesis on how to wash clothes using your brand of washing machine. You'd write a sequence of steps starting from:

1) Insert the power cord into the wall socket and switch on the power


n) Finally, retrieve your garments from the soaking mess and dump them on the clothesline.

The sequence from 1 to n would take minor variations depending on whether your washing machine was semi or fully automatic, whether it was top or side loading (try step 'n' from the side loading washing machine, and send me an e-mail about it) and other variables. The instructions in your thesis would be fine for one washing machine, but how about if you were a freelance user manual writer, and needed to write manuals for a thousand brands?

Take the case of the /dev/dsp device interface, the default interface for PCM (pulse code modulated) and coded PCM sound: Hannu Solvelein with Alan Cox making significant contributions designed much of the interface. But these designers, quite rightly, didn't make room for one teeny-weeny little device called the PC-speaker, in favor of the AWE 64 and cards of the kind. They assumed that all DSP devices would at least have DMA support, or on board buffers, if not coprocessors (ie on-board processors). So they put the DMA registration code as a mandatory part of the OSS API. That's where we begin the real hacking. We have to avoid the standard OSS interface, and rebuild it from scratch. Which means it's time for another technical discussion - character devices in Linux.

Character Devices in Linux

In Linux, there are mainly two kinds of devices: Block and Character. (Ignoring network devices as they're not really "devices", more like interfaces.)

Block devices are assumed to have certain characteristics like reading and writing in blocks, buffering, partitioning etc. The hard disk drive is the perfect example of a block device. An application normally accesses a hard drive through a file system driver. That's why in Unix you mount disk drives and do not access them sector-by-sector.

Character devices are meant to be read and written one byte at a time (e.g.: Serial port), and are not buffered. An application accesses them by doing ordinary file control operations on the corresponding device nodes. Device nodes are special "files" which can be accessed through the ordinary path tree. So if you want to write to the sound device, by convention, /dev/dsp is the published device node to use for that. Note that any device node that points to the corresponding device number registered by the driver can be used to access that driver. For example, the /dev/dsp node is attached to device number 14/3. (try: file /dev/dsp; on your system). You could equally well access it via /dev/mynode if /dev/mynode points to 14/3. Check the mknod man pages for exact semantics.

Now if you have a .wav file which is of a specific format, say 16-bit, Stereo, raw pcm, to make it play on the system sound device, you might open the /dev/dsp node using the open system call, and open your .wav file, read a block of data from the .wav file, and write it to the /dev/dsp node using read and write system calls respectively. AHA! And guess what client is readily available for this? Our very own cp. So next time, try cp -f fart.wav /dev/dsp. And tell me how it sounded. I'll bet that unless you're very lucky, you wouldn't get the right sound even if you play Celine Dione. That's because the sound driver needs to be told what format the raw data it gets is in. More often than not, you'd be trying to play a 16-bit stereo file at 44khz on a 8-bit mono 22khz driver. That's like trying to play an LP disc at the wrong turntable speed.

The ioctl (short for input/output control) system call is used on /dev/dsp, to talk to the device driver. Unfortunately, the exact semantics of the ioctl call is left to the device driver writer's discretion. That's sort of like the chaos one gets in the DOS software market. Thankfully, we have a few recognized conventions in Linux, the most popular of which is the OSS or Open Sound System. This is the interface implemented in Linux by Sovelein and co. So we have XMMS plug-ins for OSS on the application side, and scores of device drivers on the kernel side.

The Kernel

When an application makes the open "call" it's obviously calling something. That something is a kernel routine (remember open is a system call). The kernel is designed to pass on the call to the corresponding device driver. The amazingly nice thing about the Linux kernel is that you can tell the kernel to call your routine for a particular device number. This is called device callback registration, and is a kernel mode call, i.e., you cannot write applications that do these calls and can be run from the terminal. You use insmod for that, and write a special program called a kernel module, which insmod can load into kernel space and link with the kernel system calls. The main difference between system calls and kernel mode calls is that system calls have to conform to general conventions if they are ever to be recognized as a part of Unix. The kernel, on the other hand, is Linux. So it's just Linux conventions one has to follow in kernel programming, and mind you, Linux kernel conventions change nine to the dozen per kernel release. That's why you have a "pre-x.xx.xx version compile only "warning with many releases of module binaries. At a minimum, we need to have a read and write callback routine each, besides open and close. The Linux kernel specifies a routine called init_module, and another called cleanup_module, which are called by the kernel at the insertion and removal of our module. (Somewhat like main() in user space.) In other words, when we write a init_module routine, we assume that we have full control over the system ports, memory, etc. and that we can call all available kernel functions.

Linux Here We Come  << Page 3 of 5  >>