projects | about

Table of contents

keypad



uClinux / Linux - Keypad kernel module

keypad keypad keypad


In order to learn about more about linux, the linux kernel and to keep my hobby programming skills fresh I decided to write a small kernel module. This module is written for both the intel x86 platform and the ucsimm MC68EZ328 integrated processor. If you are interested in learning about the internals of the linux kernel and interested in an embedded programming I highly suggest picking one of these up from acturus.

Getting Started

The first things you will need if you are interested in linux driver programming are the appropriate reference books.

The C programming Language by Kernighan and Ritchie
Linux Device Drivers by Alessandro Rubini

The latter is available for free online but I strongly suggest that you either print it out for reference or buy it from oreilly.

The next step is to obtain the necessary hardware. Alessandro Rubini has many examples in his device driver book if you are interested in writing a kernel module without buying any hardware to control. Because this was my first time writing a linux driver I decided on something extremely simple, a small matrix keypad. I must admit there is nothing like a chrome payphone pad to bring one back to highschool days of making free calls in the courtyard of north hunterdon high school and reading the 2600 phreaker stories on the bulletin boards. Regardless, a payphone keypad is the one I decided on and (instead of trying to take one from a payphone in service) I decided to buy one from the good people at payphone.com. The keypad comes apart fairly easy and the chrome keys can be switched if you prefer the computer keypad orientation. The bell keypad is wired as a simple matrix with 7 wires for the rows/columns and an additional wire for ground. Here are the pinouts for each column/row, pin 1 being the furthest pin from ground.
 
       |    |    |
     --9----8----7----  2
       |    |    | 
     --6----5----4----  7
       |    |    | 
     --3----2----1----  6
       |    |    | 
     --*----0----#----  4
       |    |    | 
 
       3    1    5
 

PC Parallel Port

 
       |    |    |
     --9----8----7----<|-D0  O DATA Port  
       |    |    |           U (base_addr+0)
     --6----5----4----<|-D1  T 
       |    |    |           P
     --3----2----1----<|-D2  U 
       |    |    |           T
     --*----0----#----<|-D3  
       |    |    | 
      S6   S5   S4
         
          INPUT
   Status Port (base_addr+1) 
 
Borrowing a parallel port cable from my friend Brent (who will probably not want it back, now that it is mangled) I stripped one end and broke out the wires on a breadboard using some duct-tape to keep the cable in place (see picture, top).

The parallel port offers plenty of pins for input and output, I decided to use the Status Port (base_addr + 1) for my input pins and the Data Ports (base_addr + 0) for output.
If two keys are pressed current will drain into the output pin. This can be prevented by putting germanium diodes inline with the connections. (avoid using diodes with high voltage drops or LEDs as it will not drop low enough to set the input low)

uCsimm/uC68EZ328

 
          +3.3V
       |    |    |
          1kOhm
       |    |    |
     --9----8----7----<|-PD0  O 
       |    |    |            U 
     --6----5----4----<|-PD1  T 
       |    |    |            P
     --3----2----1----<|-PD2  U 
       |    |    |            T
     --*----0----#----<|-PD3  
       |    |    | 
      PD6  PD5  PD4
          
          INPUT
 
The UCSIMM has a similar configuration except I used a pull up resister instead of connecting the input pins directly to the output pins. I connected the keypad to the ucsimm with 7 wires using its existing connector and the the ucsimm gardner board. (picture, top)

Getting uClinux on the uCsimm is extremely easy, grab the latest cvs snapshot from uclinux.org.
The one available at the time of writing was uClinux-dist-20030305.tar.gz
There is small bug in this release causing logins not to work, you can either hack login.c so that it accepts all login/passwords or if you missed it on the uclinux-dev mailing list apply this patch from David McCullough. First untar it, and run makeconfig to choose your platform and enable loadable module support.
Next you will need to create the keypad device, to do this edit the file vendors/Arcturus/uCsimm/Makefile and add the following line to the end of "DEVICES":
keypad,c,253,0

Once you do that you can compile the uClinux kernel and flash your module.
screenshot.jpg - screenshot of the driver in action, exciting!

Source Code

keypad.c
keypad_load_pcparport.sh
keypad_unload_pcparport.sh
Makefile.pcparport
Makefile.ucsimm

Or the entire source tree:
keypad.tar.bz

I won't go into much detail here about how a kernel module is structured as this is covered in great detail in Linux Device Drivers. Instead, I will go over important sections of the module code.

keypad_init

 
static int my_init(void)
{
    int result;
    result = register_chrdev(keypad_major, "keypad", &keypad_fops);
 
    if (result < 0)  {
        printk(KERN_WARNING "keypad: unable to get major %d\n", keypad_major);
        return result;
    }
 
    if (keypad_major == 0)
        keypad_major = result;
 
 
#ifdef UCSIMM
      PDSEL = 0x7f;  //select PD0-PD6 for I/O
      PDDIR = 0x0f;  //select PD0-PD3 for output
#endif
 
 
 
 
#ifdef PCPARPORT
    /* attempt to allocate the I/O region for the PC parport  */
    result = check_region(base,1);
    if (result) {
        printk(KERN_INFO "keypad: can't get I/O address 0x%ld\n",base);
        return result;
    }
    request_region(base,1,"keypad");
 
    /* init the port to zero */
    outb(0,base);
    wmb();  /*write memory barrier*/
#endif
 
 
    /* set up kernel timer */
    poll_timer.expires = jiffies + 1;
    poll_timer.function = keypad_poll;
    add_timer(&poll_timer);
 
    printk("<1>keypad Module Loaded\n");
    return 0;
}
 
 
This code gets executed when the module is inserted into the linux kernel. The important thing to do here is to set up our I/O ports. In the case of the PC parallel port we attempt to allocate the I/O region and initialize it at our base address. The UCSIMM has I/O ports that need to be set to either input or output so we do that here as well. Finally after all I/O regions are ready for input and output we initialize the kernel timer. I set the timer up to fire every jiffy (every 10ms). Every time this timer fires we will execute the code in "keypad_poll".

keypad_poll

#ifdef PCPARPORT
#   define KP_PORT_READ         (inb(base+1))
#   define KP_PORT_WRITE(A)     (outb((A),base))
#endif
 
#ifdef UCSIMM
#   define KP_PORT_READ         (PDDATA)
#   define KP_PORT_WRITE(A)     ((PDDATA) = (A))
#endif
Because I am programming for two architectures there are two different ways to read and write to the output ports connected to the keypad columns and rows.
 
void keypad_poll(void)
{
 
    del_timer(&poll_timer);
 
    KP_PORT_WRITE(0xE);
    wmb();
 
 
        if (!(KP_PORT_READ & 0x10))  {
            if (!(kpkeystate & 0x200)) addkpQueue(57); //printk("keypress 9 - %ld\n",jiffies);
            kpkeystate = kpkeystate | 0x200;
        }
 
        else kpkeystate = kpkeystate & 0xDFF;
 
 
        if (!(KP_PORT_READ  & 0x20))  {
            if (!(kpkeystate & 0x100)) addkpQueue(56); //printk("keypress 8 - %ld\n",jiffies);
            kpkeystate = kpkeystate | 0x100;
        }
 
        else kpkeystate = kpkeystate & 0xEFF;
 
 
 
        if (!(KP_PORT_READ & 0x40 ))  {
            if (!(kpkeystate & 0x080)) addkpQueue(55); //printk("keypress 7 - %ld\n",jiffies);
            kpkeystate = kpkeystate | 0x080;
        }
 
        else kpkeystate = kpkeystate & 0xF7F;
 
    rmb();
 
 
.... (( check ROWS 2 & 3 ))
 
    poll_timer.expires = jiffies + 1;
    add_timer(&poll_timer);
 
}
 
This looks ugly but the concept is quite simple and applies to both architectures. The above code is for the first row, if you would like to examine the rest check out the keypad.c source. After setting one row low and the rest high we check to see which columns are driven low or high to determine what key is pressed. Once a key is pressed the ascii code of it is added to our FIFO buffer to be read later in our next function.

keypad_read

 
size_t keypad_read(struct file *filp, char *buf, size_t count, loff_t *f_pos)
{
    int popd_key,cnt;
    int retval = count;
    //char key;
    char userbuf[KPBUFSIZE] = {0};
 
    if (kpbuf.isempty)    //if our circular buffer is empty we block until a keypress
           interruptible_sleep_on(&kp_queue);
 
 
/*
 * This loop pops keys off our circular FIFO buffer
 *      and adds them to a local buffer which later gets copied to userspace.
 * This will not grab more than the user requests (count).
 * If the user requests more than what is in our FIFO buffer it will break out
 *      early and return what we have.
 *
 */
    for (cnt = 0; cnt < count; cnt++) {
        popd_key = pop();
        if (popd_key == -1)
             break;
        //key = (char)(popd_key);  /*m68k doesn't like this :( */
        sprintf(userbuf, "%s%c",userbuf,popd_key);
 
    }
#ifdef DEBUG
    printk("before copy_to_user\n");
#endif
    copy_to_user(buf, userbuf, cnt);
#ifdef DEBUG
    printk("after copy_to_user\n");
#endif
 
    retval = cnt;
 
 
    return retval;
}
 
This code will be executed when the keypad is opened for reading. If the FIFO buffer is empty the function interruptible_sleep_on will put the calling process to sleep. The process will wake back up when new data is available and we call wake_up_interruptible in our addkpQueue() function. If the there is data in the FIFO buffer to read we will empty count bytes that the user requested and copy the data from kernelspace to userspace.

keypad_cleanup

 
static void my_cleanup(void)
{
 
#ifdef PCPARPORT
    /* release I/O region for parport */
    release_region(base,1);
#endif
 
    /* remove our kernel timer */
    del_timer(&poll_timer);
    unregister_chrdev(keypad_major, "keypad");
    printk("<1>keypad Module Unloaded\n");
 
}
 
This will be called when the module is removed from the kernel. All we need to do here is release the I/O region for the parallel port and delete our kernel timer.

ucsimm troubleshooting

kernel exceptions

By default m68k-elf-gcc will generate -m68020 code and although things may seem to work using 68020 instructions and addressing modes bad things can happen like:
*** Exception 107 ***   FORMAT=2
Current process id is 0
BAD KERNEL TRAP: 00000000
PC: [<001215e5>]
SR: 21a5  SP: 000456e0  a2: 0012181c
d0: 00000001    d1: 00042000    d2: 00000042    d3: 00000000
d4: fffffffe    d5: 00000001    a0: 001215e4    a1: 001215d8
Process swapper (pid: 0, stackpage=00044810)
Frame format=2 instr addr=20000012
Stack from 00045718:
        14080004 57260012 118e0000 00380004 580810c1 c9240000 00000000 20040000
        0000ffff fffe0000 00010000 0002ffff ffff0000 00000003 0f140003 0f40007f
        ff980004 58080004 575e0004 575e10c1 ca8610c1 b2460000 20040000 000010c1
        b16a0000 00000000 0001ffff fffe0004 afac0000 27040004 afac0003 0f1c0003
        0f1410c1 af3c0004 afac0000 0001ffff ff810000 00000000 000010c1 7ec80000
        4514007f ff1810c1 3e120000 00000000 0001ffff ff810000 00000000 00000004

The lesson here is to make sure you include the -m68000 compile flag. Thanks to Greg Ungerer on the uclinux-dev list for pointing this out in my Makefile. :)

accessing registers from userspace

Of course this isn't a problem in kernel land although it is a common one with the ucsimm and user applications.
Starting in kernel 2.4 access to the registers from userland was disabled. There is an option to enable this in the configuration or if that doesn't work try setting the system control register manually. This can be done by adding
"moveb #0,0xfffff000"
in front of "jump start_kernel" in
/linux-2.4.x/arch/m68knommu/platform/68EZ328/ucsimm/crt0_ram.S

nfs mount timeouts transferring

Another common problem with the ucsimm is nfs mount timeouts when transfering large files. Make sure that when you nfs mount a network drive that you use 1024 as your blocksize. For example:
mount -t nfs -o rsize=1024,wsize=1024 192.168.1.10:/home/jarv/kit /mnt

circular queue

Bob Ray on the uclinux-dev mailing list was very helpful and suggested a better implementation of my circular queue that is now being used in the driver.
static int addkpQueue(KP_QUEUE_TYPE val)
{
    int i;

    i = (kp_queue_tail + 1) % KPQUEUESIZE;
    if (i != kp_queue_head)  {
        kpQueue[i] = val;
        kp_queue_tail = i;
        wake_up_interruptible(&kp_queue);  // a button was pushed! wake up!
        return 0;
    }
    return -1;

}

static int deletekpQueue(KP_QUEUE_TYPE *pval)
{

    if (kp_queue_tail != kp_queue_head)  {
        kp_queue_head = (kp_queue_head + 1) % KPQUEUESIZE;
        *pval = kpQueue[kp_queue_head];
        return 0;
    }
    return -1;
}



Copyright (c) jarv.org Verbatim copying and redistribution of this entire page are permitted provided this notice is preserved.
Validate (nerd)