Table of contents
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.
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
| | |
--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)
+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!
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.
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".
#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.
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.
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.
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. :)
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
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
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;
}