Originally Published: Monday, 30 April 2001 Author: Cherry George Mathew
Published to: featured_articles/Featured Articles Page: 5/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.

  << Page 5 of 5  

One Last Point

I guess, that's it, but before I run away satisfied that I've shoved Latin up a thousand throats, I want to clear a few things about the sample that I did in myaudio.x . Linux has a formal method to claim interrupts for device drivers. Trouble is, by the time a module is loaded the scheduler has already claimed the timer interrupt. So we have to hack a bit and steal it from the scheduler. That's why we had to discuss IDTs and stuff. I haven't implemented the OSS interface even though I elaborated it on a TODO list earlier. Nevertheless it's possible to listen to MP3 music with the following:

As root user, chdir to the directory where you've copied the source files myaudio.c, myaudio.h, and myaudio.mak. make -f myaudio.mak do a mknod c 254 /dev/pcspeaker cat /proc/devices

If you see an "Internal Speaker" entry, we're ready for some fun. Else forget about the whole thing.

insmod myaudio.o

mpg123 -m -r 22000 --8bit -w /dev/pcspeaker x.mp3 #this should playx.mp3

ENJOY!!!!

My name is Cherry George Mathew. I'm a third year Electronics Engineering Under graduate student at College of Engineering, Adoor, Kerala, India. Any questions about this article may be sent to berry.plum@mailcity.com, which time permitting, I will try to answer.

Note: All these operations need to be done as user root. So I'm assuming that you're using your own machine, and are ready to trash any part of it, maybe permanently. I cannot take any responsibility for what happened to your system because of my code, so I'll insist that you try it only at your own risk.

myaudio.h

#include <linux/module.h>
#include <linux/version.h>
#include <linux/linkage.h>
#include <linux/malloc.h>
#include <linux/soundcard.h>
#include <asm/io.h>
#include <asm/uaccess.h>
/* #include linux/kernel.h> */
/* #include include/vmalloc.h> */

/* Among many hazy assumptions, we decide that 
- gcc compiler specific operand passing assumption for type 
  'long', calling proc cleans up stack
- Stack is push down, ie, grows to lower addresses
- 32bit far return does popl eip, popl cs(msw neglected) in that order
- 'leave' cleans up the stack...........................???
for a baby of an interrupt handler routine.......               */


/*****************helper functions in use***************/
asmlinkage void myhandler(void);
static int fops_sync_output(struct inode *inode, struct file *file);
void sync_output();
long getvect(long vector_number);
long setvect(long vector_number, long new_handler_address);
int helper_get_pit_count(void);
void helper_set_pit_count(int count);
static void speaker_close(struct inode *inode, struct file *file);
static int speaker_open(struct inode *inode, struct file *file);


static ssize_t speaker_read(struct file *file, char *buf,
                size_t count, loff_t * ppos);

static ssize_t speaker_write(struct file *file, const char *buf,
                 size_t count, loff_t * ppos);
static int speaker_ioctl(struct inode *inode, struct file *file,
             unsigned int cmd, unsigned long arg);



/********************GLOBAL STRUCTURES ********************/
struct file_operations speaker_fops = {
    NULL,
    speaker_read,
    speaker_write,
    NULL,
    NULL,
    NULL,           /* we'll support speaker_ioctl later, 
                 */
    NULL,
    speaker_open,
    speaker_close,
    fops_sync_output,
};

static char berio;
static long timer_idt_ptr, data_buffer, data_ptr;
static int sampling_factor, canplay, pit_counter, compensation_count,
    temp_pit_counter, old_pit_counter, buffer_length, virgin,
    device_major, no_sleep_just_try_again;

struct __stack_pad_tag {
    long __stack_padding1, eax, __stack_padding2;
};


long setvect(long vector, long hook)
{

    struct __stack_pad_tag stack_pad __attribute__ ((packed));
    asm("sidt %3                    \n      
    leal % 3, %%eax \ n
    movl 2(%%eax), %%eax \ n leal(%%eax, %1, 8), %%ecx \ n  /* ecx 
                                   is 
                                   the 
                                   'pointer 
                                   register' 
                                   for 
                                   int 
                                   vector 
                                 */
    movl 4(%%ecx), %%eax \ n
    xorw %% ax, %%ax \ n movw(%%ecx), %%ax \ n
    /* Remember , pointer to int handler is in eax, we've not
       pushed it */
    cli \ n
    movw %% bx, (%%ecx) \ n
    shrl $16, %%ebx \ n movw %% bx, 6(%%ecx) \ n
    /* I'm under the assumption that eax can safely be returned
       to pseudo register 'stackpad.eax' */
    sti \ n " : " =
    a " (stack_pad.eax) : " c " (vector)," m
    " (stack_pad.__stack_padding1), " m " (stack_pad.eax), " m
    " (stack_pad.__stack_padding2)," b " (hook));

    return stack_pad.eax;}

    long getvect(long vector) {
    long eax, __stack_padding1, __stack_padding2;
    asm("sidt %4                    \n
        leal % 4, %%eax \ n
        movl 2(%%eax), %%eax \ n
        leal(%%eax, %1, 8), %%ecx \ n
        movl 4(%%ecx), %%eax \ n
        xorw %% ax, %%ax \ n
        movw(%%ecx), %%ax \ n " : " =
        a " (eax) : " c " (vector)," m " (__stack_padding1), " m
        " (eax), " m " (__stack_padding2) ); return eax;}

/* A little glitch in the stack padding-not to worry though, 
 * it works just fine...... (%4 vs %3 in setvect */

        asmlinkage void myhandler(void) {
/* pushl %%ebp; movl %%esp,%%ebp */
        asm volatile ("
              leave
              pushl timer_idt_ptr
              pushal
              pushf
              cli
              movw canplay, %cx
              jcxz skip_isr
              inb $0x61, %al orb $3, %al outb % al, $0x61
/* fetch data */
              movl data_ptr, %ebx movw(%ebx), %dx
/* load mode register of pit */
              movb $0xb0, %al
              outb % al, $0x43
              movw pit_counter, %ax
              xorb % dh, %dh
              mulw % dx movb $8, %cl shrw % cl, %ax
/* load counter 2 */
              orw $1, %ax outb % al, $0x42
/* dump MSB into counter 2 assuming it to be zero */
              xorb % al, %al outb % al, $0x42
/* increment data pointer */
/* movb berio, %cl
xorb %ch, %ch
jcxz data_ptrplus
add $1, data_ptr
*/
              data_ptrplus:incl data_ptr
              movl data_ptr, %eax
              subl data_buffer, %eax
              cmpw % ax, buffer_length jnc skip_isr /* We 
                                   don't 
                                   want 
                                   to 
                                   reset 
                                   count 
                                   everytime 
                                 */
              movw $0, canplay  /* So just switch off 
                           playback to
                           indicate end of
                           buffer */
              skip_isr:decw temp_pit_counter
              jz hop_to_kernel movb $0x20, %al  /* EOI 
                                   to 
                                   PIC 
                                 */
              outb % al, $0x20 popf popal
/*subl $4, %esp        /* readjust stack for iret */
              pop timer_idt_ptr
              iret
              hop_to_kernel:movw compensation_count, %ax
              movw % ax, temp_pit_counter; popf ");
/* return stack has been set up -> oldhandler. End with popa; ret */
              asm("popal ret ");}

myaudio.c

#include "myaudio.h"

static int
speaker_ioctl(struct inode *inode, struct file *file,
          unsigned int cmd, unsigned long arg)
{
    int val;            /* , *arg; */
    struct audio_buf_info _infobuffer = { 1, 1, 32000, 32000 };
    /* we're assuming that this stuff is on a local stack. In that
       case the above initializations assume the same values on each
       call to ioctl....... ie, 1 buffer fragment available, 1 also
       max no: of frags, size in bytes of fragment, amount of
       available mem. */


    switch (cmd) {
    case SNDCTL_DSP_SYNC:   /* reset buffers */
    call sync_output();
    goto default;

    case SNDCTL_DSP_POST:   /* do launch output ->dmap whatever
                   that means */
    case SNDCTL_DSP_RESET:  /* reset dsp */
    call sync_output();
    goto default;

    case SNDCTL_DSP_GETFMTS:    /* return in val,dsp format mask */
    val = AFMT_U8 | AFMT_U16_LE;
    break;

    case SNDCTL_DSP_SETFMT: /* set dsp format mask and return val 
                 */
    val = get_user((int *) arg);
    if (val & AFMT_U8)
        berio = 0;
    if (val & AFMT_U16_LE) {
        berio = 8;
        break;
    } else
        return -EINVAL;
    break;

    case SNDCTL_DSP_GETISPACE:  /* output in audio_buf_info
                   (include/soundcard.h) amount of
                   unused internal buffer space -
                   invalid for o/p only device */
    return -EINVAL;

    case SNDCTL_DSP_GETOSPACE:  /* same as above but valid */
    if (canplay) {
        _infobuffer.fragments = 0;
        _infobuffer.bytes = 0;
    }

    else {
        _infobuffer.fragments = 1;
        _infobuffer.bytes = 32000;
    }

    case SNDCTL_DSP_NONBLOCK:   /* set non-blocking flag in the dev
                   structs */
    no_sleep_just_try_again = 1;
    goto default;


    case SNDCTL_DSP_GETCAPS:    /* DON'T SUPPORT THIS SHIT */
    return -EINVAL;
    case SOUND_PCM_WRITE_RATE:  /* returns speed in val in Hz */
    val = case SOUND_PCM_READ_RATE: /* returns read speed in val. 
                       Want to return -EINVAL */
    return -EINVAL;

    case SNDCTL_DSP_STEREO: /* returns and sets the number of
                   channels aske d for (1,2) */
    case SOUND_PCM_WRITE_CHANNELS:  /* returns and sets the
                       number of channels
                       requested for (0,1) */
    case SOUND_PCM_READ_CHANNELS:   /* We return -EINVAL here */
    case SOUND_PCM_READ_BITS:   /* sets and returns bits per sample */
    case SNDCTL_DSP_SETDUPLEX:  /* no need to support */
    case SNDCTL_DSP_PROFILE:    /* no need to support */
    case SNDCTL_DSP_GETODELAY:  /* I think that this is simply the
                   time it take for the damned thing
                   to stop playing when told to stop */

      /********** from dma_ioctl ****************/
    case SNDCTL_DSP_GETOPTR:    /* we need to support this URGENTLY
                   something called a cinfo struct is 
                   set */

    default:

    return 0;
    }
    return put_user(val, (int *) arg);
}

static ssize_t
speaker_write(struct file *file, const char *buf,
          size_t count, loff_t * ppos)
{

    if (count < 0)
    return -EINVAL;

    if (!count) {       /* Flush output */
    sync_output();
    return 0;
    }

    if (!canplay) {
    count = (count <= 32000 ? count : 32000);
    if (copy_from_user((void *) data_buffer, buf, count))
        return -EFAULT;
    data_ptr = data_buffer;
    buffer_length = count;
    canplay = 1;

    return count;
    }

    return 0;           /* The idea is to return 0 bytes and
                   leave the problem to the client */
}

static ssize_t
speaker_read(struct file *file, char *buf, size_t count,
         loff_t * ppos)
{
    return -EINVAL;
}

static int speaker_open(struct inode *inode, struct file *file)
{
    /* Check for re-entrancy */
    if (virgin >= 1)
    return -EBUSY;
    virgin = 1;
    MOD_INC_USE_COUNT;
    return 0;

}

static void speaker_close(struct inode *inode, struct file *file)
{
    virgin = 0;
    MOD_DEC_USE_COUNT;
}

static int fops_sync_output(struct inode *inode, struct file *file)
{
    sync_output();
    return 0;
}


void sync_output()
{
    canplay = 0;
    data_ptr = data_buffer;
    buffer_length = 0;

}

int helper_get_pit_count(void)
{
    long ntsc = 1193180;
    return ntsc / HZ;
}

void helper_set_pit_count(int counter)
{
    cli();
    outb(0x36, 0x43);

    outb((char) (counter & 0xff), 0x40);

    outb((char) (counter >> 8), 0x40);
    sti();
}


int init_module(void)
{

    /* Initailize all global variables */
    if (
    (device_major =
     register_chrdev(0, "Internal Speaker", &speaker_fops)) < 0) {
    printk(KERN_WARNING "Cannot get device majornumber");
    return device_major;
    }

    no_sleep_just_try_again = 0;    /* we're into the big leagues 
                       now, ioctls, putting
                       procceses to sleep ......
                               mmmmmmmmmm......
                          */

    virgin = 0;         /* Just in case you didn't
                   know....... ;)  */
    berio = 0;          /* set to 8 for 16 bit mono */
    canplay = 0;
    sampling_factor = 1;    /* ie; counter = 22Khz /
                   sampling_factor; */
    pit_counter = 54;

    /* allocate room for our 32KB buffer */
    data_buffer = (long) kmalloc(32000, GFP_KERNEL);    /* this is
                               assuming
                               that
                               kmalloc
                               NEVER
                               FAILS */


    timer_idt_ptr = setvect(0x20, (long) myhandler);
    old_pit_counter = helper_get_pit_count();
    temp_pit_counter = compensation_count =
    old_pit_counter / pit_counter;

    helper_set_pit_count(pit_counter);

    printk
    ("Hopefully, irq0 has been hooked from vector 20 to %p  \n",
     myhandler);
    return 0;

}

void cleanup_module(void)
{
    helper_set_pit_count(old_pit_counter);
    setvect(0x20, timer_idt_ptr);

    unregister_chrdev(device_major, "Internal Speaker");

    kfree((void *) data_buffer);

    printk("Phew! Graceful exit!!!!! \n irq0 reset to %p \n",
       (void *) timer_idt_ptr);
}

Makefile

INCLUDEDIR = /usr/src/linux/include

CFLAGS = -c  -D__KERNEL__ -DMODULE -O -Wall -I$(INCLUDEDIR)

VER = $(shell awk -F\" '/REL/ {print $$2}' $(INCLUDEDIR)/linux/version.h)

OBJS = myaudio.o

all:    $(OBJS)

myaudio.o:
    cc $(CFLAGS) myaudio.c  

install:
    install -c myaudio.o 

clean:
    rm -f *~ core




  << Page 5 of 5