Linux Character Device Drivers
Copyright © 1995, Mike Battersby. All rights reserved.
No warranty is associated with any of the information contained,
within this article. The author is not responsible for any damage,
it may cause.
INDEX
Preface
Introduction
Inside the Kernel
Useful Functions
Driver Basics
VFS Functions
Initialization
Using Interupts
Blocking Reads
Probing for Interupts
Command Line Options
Modularizing
Kernel Coding Practices
Compiling
Summary
Preface
Note that a good many of the links within this document reference
parts of the linux kernel source code. In order for them to work
correctly, you will need to be running your browser on a Linux machine
with the kernel source present in /usr/src/linux.
Introduction
- driver provides low level device functions
- devices are treated as files (/dev entries)
- character devices are sequential (read/write one
byte at a time, in linear order)
- examples are console, lp
- need documentation on how to program your device
- pick a MAJOR number, see <linux/major.h>
- a device driver can support multiple similar devices
- all share the same MAJOR number, but are distinguished
by their MINOR numbers
- also need to pick device names to go in /dev
- sources for character drivers are in linux/drivers/char
- look at existing drivers for examples
Inside the Kernel
- the kernel is non-preemtive. Kernel functions do not get
context switched.
- thus, you need not do lots of messy locking on variables
- but, if one of your routines goes into an infinite loop,
the machine will hang. If the routine takes too long,
the machine will appear to pause. Users hate this.
- none of the libc functions are available, but some of them
are duplicated. In particular, the str* and mem* functions.
Useful Functions
#include <asm/io.h>
char inb(int port); char inb(int port);
char inw(int port); char inw(int port);
char outb(char val, int port); char outb(char val, int port);
char outw(char val, int port); char outw(char val, int port);
these are for doing device i/o, to/from the i/o ports
the are actually macros
the _p versions do a short pause after the i/o. This is
sometimes necessary because the CPU can execute the i/o
instructions faster than the device/bus can keep up.
#include <linux/malloc.h>
void *kmalloc(int size, int priority);
void kfree_s(void *obj, int size);
void kfree(void *obj);
these are for allocating small amounts of memory (<4096 bytes)
the priority is either
- GFP_KERNEL - for normal cases
- GFP_ATOMIC - for allocating mem inside interrupts
they can fail, although GFP_KERNEL mostly wont. GFP_ATOMIC
will fail if it can't alloc the mem straight away, so be
prepared for it to return NULL.
the kfree_s function is for when you know the size of the
object you are freeing. You should use it wherever possible
since it is much faster.
#include <linux/mm.h>
void *vmalloc(long size);
void vfree(void *obj);
these are for allocating larger amounts of memory, such as
device buffers
#include <linux/kernel.h>
int printk(char *fmt, ...);
works just like printf, prints to the console, or to the
kernel log (which is like syslog, see the klogd program)
#include <linux/mm.h>
int verify_area(int type, void *area, unsigned long size);
call to verify that user space memory (passed to one of
your kernel functions) is valid.
type is
- VERIFY_READ - if the kernel is to read from the buffer
- VERIFY_WRITE - if the kernel is to write to the buffer
#include <asm/segment.h>
unsigned char get_fs_byte(const char *addr);
unsigned short get_fs_word(const short *addr);
unsigned long get_fs_long(const int *addr);
void put_fs_byte(char val, char *addr);
void put_fs_word(short val, short *addr);
void put_fs_long(long val, int *addr);
void memcpy_fromfs(void *to, void *from, unsigned long len);
void memcpy_tofs(void *to, void *from, unsigned long len);
to copy data to/from user space
#include <asm/system.h>
void sti();
void cli();
void save_flags(unsigned long flags);
void restore_flags(unsigned long flags);
to set, clear, save and restore the interrupt enable flag
use like so
unsigned long flags;
save_flags(flags);
cli(); OR sti();
.
. statements to be executed with interrupts disabled
. or enabled.
.
restore_flags(flags);
Driver Basics
VFS Functions
- our device is called 'foo'.
int foo_open(struct inode *inode, struct file *filp);
int foo_release(struct inode *inode, struct file *filp);
- called when device is open(2)ed or close(2)ed
- inode and filp are big kernel structures with data regarding
the file and the device. See <linux/fs.h>.
- some inportant members
- inode->i_rdev
- device number of this device, use MINOR(inode->i_rdev) and MAJOR(inode->i_rdev);
- filp->private_data
- can be used to store driver specific data (is a void *).
- return 0 on success, -ERRNO on failure, where ERRNO is one of
the error numbers from <linux/errno.h>. Some useful ERRNOs are
- ENODEV - no such device
- EBUSY - device busy
- EINVAL - invalid operation
- see manpages for open(2) and close(2)
int foo_read(struct inode *inode, struct file *filp,
char *buffer, int count);
- called when read(2) is called on opened device
- call verify_area(VERIFY_WRITE, buffer, count);
- return the number of bytes read, or -ERRNO
- if you don't provide a read function, the kernel default
is to return -EINVAL
int foo_write(struct inode *inode, struct file *filp,
char *buffer, int count);
- called when write(2) is called on opened device
- call verify_area(VERIFY_READ, buffer, count);
- return num bytes written or -ERRNO
- default action is -EINVAL
int foo_lseek(struct inode *inode, struct file *filp,
off_t offset, int origin);
- called when lseek(2) is called on opened device, see
manpage for details.
- origin is
- 0 - start of file
- 1 - current position
- 2 - end of file
- unless you have a device which in some sense has a beginning
and an end (remember, character devices are sequential) you
will only implement case 1.
- if you don't provide an lseek function, the default is to
modify filp->f_pos, thus if you disallow lseek you must
write a function which just returns -EINVAL.
int foo_ioctl(struct inode *inode, struct file *filp,
unsigned int cmd, unsigned long arg);
- called when ioctl(2) is called on opened device
- cmd is the ioctl command number, you get to make these
up yourself. You can use the size and in/out designator
bits if you wish (see IOC_IN, IOC_OUT, IOCSIZE_MASK in
<linux/ioctl.h>, but you don't have to.
- make sure your ioctl commands don't clash with any other
devices (use a unique first byte in the cmd).
- arg is the argument to the command, see the ioctl(2) manpage.
It is usually taken as a pointer.
- Now we can fill in the file_operations struct (well, except
for select)
struct file_operations foo_fops = {
foo_lseek,
foo_read,
foo_write,
NULL,
foo_select,
foo_ioctl,
NULL,
foo_open,
foo_release,
};
- use NULL for not-implemented members.
Initialization
long foo_init(long kmem_start);
kmem_start argument is the start of available kernel
memory. You can alloc memory by doing something like
this
char *buffer;
buffer = kmem_start;
kmem_start += SIZE_OF_BUFFER_IN_BYTES;
return kmem_start;
check if device present
register device
int register_chrdev(int major, char *name,
struct file_operations *fops);
register i/o ports
check if already taken with
int check_region(uint from, uint extent);
register with
void request_region(uint from, uint extent, char *name);
do device specific intialization
prink a message to say the device has been installed
also, add a foo_init call in mem.c with the other character
device intialization calls.
Using Interrupts
- need to write an interrupt service routine (ISR)
- keep in mind that interrupts are asynchronous - they can
occur at any time
void foo_interrupt(unsigned int irq, struct pt_reg *reg);
- irq is the number of the irq which triggered this routine
- regs is a copy of the CPU registers
- ISRs MUST BE FAST!
- should service your device appropriately, usually read data
from device and put it into a queue
- you need to register the ISR in foo_open every time the
device is first opened and free it when the device is last
closed (i.e., keep a count of the number of opens)
int request_irq(uint irq, void (*handler)(uint, struct pt_reg *),
ulong irqflags, char *name);
int free_irq(uint irq);
- E.g., in foo_open:
if (num_opens++ == 0)
request_irq(FOO_IRQ, foo_interrupt, 0, "foo");
and in foo_release:
if (--num_opens == 0)
free_irq(FOO_IRQ);
- irqflags is
- 0 - normal interrupt, slow, runs with other
interrupts enabled.
- SA_ONESHOT - fast interrupt, runs with other interrupts
disabled, and the pt_reg struct parameter
is not filled in.
Blocking Reads
- have a queue for all of the processes which are waiting for
i/o on this device
struct wait_queue *foo_wait;
- in foo_read, if device is not ready, block, unless the device
has been opened in nonblocking mode. E.g,
if (!device_ready) {
if (file->f_flags & O_NONBLOCK)
return -EAGAIN;
while (!device_ready) {
if (current->signal & ~current->blocked)
return -EINTR;
interrupible_sleep_on(&foo_wait);
}
}
- note that this is the usual case, where you only block at
the start of a read, but it is easily extented to block until
all of the required data has been read.
- interruptible_sleep_on puts the current process to sleep,
so that it is not given run time by the scheduler. When
the device is ready, it needs to be woken up, so in
foo_interrupt we need to add something like
if (device_ready_for_read)
wake_up_interruptible(&foo_wait);
- this wakes up all of the processes waiting on this device
Probing for Interrupts
Command Line Options
- passed in from the boot loader (e.g., with lilo append=)
- need to write an option parsing routine
void foo_setup(char *str, int *ints);
- the ints paramter is an array containing the comma separated
integers given at the start of the option, with ints[0]
containing the number of integers.
- the str parameter is the rest of the option
- e.g, if we passed the option
37,22,300,footypename,foooption
then the arguments become
ints[0] == 3
ints[1] == 37
ints[2] == 22
ints[3]] == 300
str = "footypename,foooption"
- you also need to add a line to the boot_setups structure in
init/main.c,
like the following
{ "foo=", foo_setup }
- the "foo=" is the option indicator for this device. So to
pass arguments to our foo device, we would put
foo=3,22,300,footypename,foooption
on the command line.
Modularizing
Kernel Coding Practices
- use static wherever possible for functions and global
variables.
- use inline functions instead of macros
- always initialize static variables
Compiling
Summary
- write the file_operations functions
- write the ISR if needed
- write an init routine
- probe for device
- register i/o ports + device
- add init routine to drivers/char/mem.c
- modularize
- modify Makefiles and config.h
- test!