How to Write Device Drivers for Linux


How does this document differ from the old one?

Not much really... Well, it is updated to use the new build system and also includes changes for the Linux 2.4 kernel running on the ETRAX 100LX.

Description

This document describes how-to write a simple device driver for a LCD connected to parallel port A on the ETRAX. It does not describe how to connect the LCD to the developer board. Instead this is left as an exercise for the reader.

Requirements

In this tutorial we assume that you have already installed your elinux tree and are somewhat familiar with various tools and concepts. Otherwise have a look at the documents How to Install the Developer Board Software and How to Write Applications for ETRAX 100LX.

Information about rebuilding the linux kernel can be found in How to use the Top-Level Makefile under the Process segment.

We also assume that you have installed the devboard software in a directory called axis/devboard_lx which resides in your home directory.

Theory

This document tries to describe how one can write a simple device driver for a piece of hardware connected to the parallel port of the ETRAX. A LCD is chosen since it might be generally use-full and is rather easy to interface.

The particular display we choose to use came from Powertip, it is described at Powertips official site. The displays documentation and timing charts is highly recommended reading. It shall be noted though that this code should work on any display with a HD44780 controller.

Process

The LCD driver will be a character driver, which means that it operates on sequential data, there are also block drivers but these are mainly for storage devices. We need to create a node in the file system, /dev/lcd where it can be accessed. To create this node we must add some stuff to ~/axis/devboard_lx/packages/devices/. Create a directory called mydevs in which you put a Makefile i.e. ~/axis/devboard_lx/packages/devices/mydevs/Makefile. The makefile should look like this:

include $(APPS)/Rules.elinux

MKNOD     = mknod_elinux
DEV       = $(prefix)/dev

install:
        @$(MKNOD) -m 0666        $(DEV)/lcd   c 126 0
Now you need to edit your ~/axis/devboard_lx/makespec by adding devices/mydevs in the <subdirs> section. Next time you run make install, this will create a device node in the file system of character type at /dev/lcd with permissions 0666 and a major number of 126 and a minor of 0.

The major and minor numbers are used by the kernel to keep track of which device is being addressed. The major number indicates which driver is to be addressed and the minor number which device the driver controls. E.g. the serial driver controls several devices and they have different minor numbers so that the driver can tell them apart. The major range 120 - 127 is reserved for experimental and local use.

The driver source file is put in the ~/axis/devboard_lx/os/linux/arch/cris/drivers/ directory.

In this directory we also edit some other files to set up the build environment. Add the following line to ~/axis/devboard_lx/os/linux/arch/cris/drivers/Config.in

bool 'LCD (HD44780) Support' CONFIG_ETRAX_LCD_HD44780
this will make 'make menuconfig' aware of our driver so that we can select it or deselect when we configure our kernel. It is also a good idea to add some text to ~/axis/devboard_lx/os/linux/Documentation/Configure.help so that the help sections on this option will be available. Something like this will do (note: this entry may already exist):

ETRAX parallel LCD (HD44780) Driver
CONFIG_ETRAX_LCD_HD44780
  Adds support for a HD44780 controlled LCD connected to the parallel
  port par0 of the ETRAX 100LX.

To make the code compile when you build the kernel, with CONFIG_ETRAX_LCD_HD44780 enabled, add the following line to ~/axis/devboard_lx/os/linux/arch/cris/drivers/Makefile:
obj-$(CONFIG_ETRAX_LCD_HD44780) += etrax100parlcd.o
Driver initialization in Linux 2.4 differs quite a bit from the old eLinux way of doing it (and that is probably a good thing too). In the older kernels, there was a function named chr_dev_init(), which took care of calling all the init functions for the character devices. Obviously this function became very heavily polluted with #ifdef's for all the different drivers. Luckily this is no longer used, instead each driver adds itself to the init chain by registering its init function with the module_init() macro e.g. module_init(my_init_function);, my_init_function has to be declared like this int __init my_init_function(void) the __init macro is used to tell the compiler that this function shall be placed in a special section that can be freed after system boot-up (thus saving some valuable system memory).

Before we can start using the driver we must edit the file ~/axis/devboard_lx/os/linux/arch/cris/kernel/head.S. This file contains assembly code that initializes various registers, one of those registers is R_GEN_CONFIG, which controls how the ports are set up, e.g. SCSI port 0 and parallel port 0 can not be used simultaneously and this register controls which one is used, if any.

Locate the following lines:

#if defined(CONFIG_ETRAX_PARALLEL_PORT0) || \ 
    defined(CONFIG_ETRAX_ETHERNET_LPSLAVE)
	or.w	0x4,r0	; parport 0 enabled using DMA 2/3
#endif
and change them to this:
#if defined(CONFIG_ETRAX_PARALLEL_PORT0) || \
    defined(CONFIG_ETRAX_ETHERNET_LPSLAVE) || \
    defined(CONFIG_ETRAX_LCD_HD44780)
	or.w	0x4,r0	; parport 0 enabled using DMA 2/3
#endif
This way the parallel port 0 will be enabled if our driver is used. Of course when using the lcd driver you must disable the parallel port 0 driver (in 'make menuconfig' by deselecting Drivers for ETRAX 100LX built-in interfaces ---> Parallel port 0 enabled).

Source Code

/* etrax100parlcd.c */

#include <linux/config.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/ioctl.h>
#include <linux/fs.h>
#include <linux/init.h>

#include <asm/segment.h>
#include <asm/uaccess.h>
#include <asm/system.h>
#include <asm/irq.h>
#include <asm/svinto.h>

#include "etrax100parlcd.h"

/*   DDRAM (Display Data RAM) addresses
 *   at different display locations.
 *   0                 19 
 *  64                 83
 *  20                 39
 *  84                103
 *
 */

#define LCD_HD44780_MAJOR 126

/*
 * RegisterSelect (RS) = autofd (bit 18 in R_PAR0_CTRL_DATA)
 * Read/Write (R/W_) = strb (bit 17 in R_PAR0_CTRL_DATA)
 * Enable (E) = init (bit 16 in R_PAR0_CTRL_DATA)
 */

#define LCD_E  0x00010000  /* LCD enable */
#define LCD_RW 0x00020000  /* LCD read/write */
#define LCD_RS 0x00040000  /* LCD register select */
#define PAR_OE 0x00100000  /* Output Enable on par port */

/* Flags used for the flag field in the etrax100parlcd_struct */
#define LCD_BUSY 0x00000001
#define LCD_NL   0x00000002

#define LCD_MAX_ROWS 4
#define LCD_MAX_COLS 20

typedef struct etrax100parlcd_struct {
  unsigned int flags;
  unsigned char row;
  unsigned char col;
} etrax100parlcd_struct;

static etrax100parlcd_struct lcd_status;

static ssize_t lcd_write(struct file *file, const char *buf,
                         size_t count, loff_t *off);
static int lcd_ioctl(struct inode *inode, struct file *file,
                     unsigned int op, unsigned long arg);
static int lcd_open(struct inode *inode, struct file *file);
static int lcd_close(struct inode *inode, struct file *file);

static struct file_operations lcd_fops = {
  write:   lcd_write,  /* write   */
  ioctl:   lcd_ioctl,  /* ioctl   */
  open:    lcd_open,   /* open    */
  release: lcd_close,  /* release */
};

static int row_col_to_addr(int row, int col)
{
  if (row == 0)
    return 0 + col;
  else if (row == 1)
    return 64 + col;
  else if (row == 2)
    return 20 + col;
  else if (row == 3)
    return 84 + col;
}

static void display_write_instr(__u8 instr)
{
  /* We have dual writes to R_PAR0_CTRL_DATA to make sure that
     the timing requirements are met */

  *R_PAR0_CTRL_DATA = PAR_OE | LCD_E;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_E | instr;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_E | instr;
  *R_PAR0_CTRL_DATA = PAR_OE | instr;
  *R_PAR0_CTRL_DATA = 0;
}

static void display_write_text(__u8 text)
{
  /* We have dual writes to R_PAR0_CTRL_DATA to make sure that
     the timing requirements are met */

  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RS;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RS | LCD_E;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RS | LCD_E | text;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RS | LCD_E | text;
  *R_PAR0_CTRL_DATA = PAR_OE | LCD_RS | text;
  *R_PAR0_CTRL_DATA = 0;
}

static void lcd_init_int(void)
{
  /* Initialization by instruction */
  display_write_instr(0x30);
  udelay(4500);
  display_write_instr(0x30);
  udelay(150);
  display_write_instr(0x30);
  udelay(150);
  display_write_instr(0x38);  /* Function set: 8bit, 2 rows and 5x8 dots */
  udelay(50);
  display_write_instr(0x04);  /* Display on/off control: all off */
  udelay(50);
  display_write_instr(0x01);  /* Clear display */
  udelay(1700);
  display_write_instr(0x06);  /* Entry mode set: inc cursor */
  udelay(50);
  display_write_instr(0x0e);  /* Display on/off control: disp on, cursor on */
  udelay(50);    
}

static ssize_t lcd_write(struct file *file, const char *buf, size_t count, loff_t *off)
{
  unsigned int i;
  int retval;

  retval = verify_area(VERIFY_READ, (void *)buf, count);
  if (retval != 0) {
    printk("LCD_HD44780: VERIFY_READ error: %i\n", retval);
    return retval;
  }
  
  for (i = 0; i < count; i++) {
    
    if (lcd_status.flags & LCD_NL) {
      if (buf[i] == '\n') {
        if (++lcd_status.row < LCD_MAX_ROWS) {
          lcd_status.col = 0;
        }
        else {
          lcd_status.row = 0;
          lcd_status.col = 0;
          lcd_ioctl(0, 0, LCD_HD44780_CLEAR, 0);
        }
        continue;
      }
    }
    
    if (lcd_status.col < LCD_MAX_COLS) {

      display_write_instr(0x80 | (unsigned char)
                          row_col_to_addr(lcd_status.row, lcd_status.col));
      udelay(50);
      lcd_status.col++;
    }
    else {
      if (lcd_status.row < LCD_MAX_ROWS) {
        lcd_status.row++;
        lcd_status.col = 0;
      }
      else {
        lcd_status.row = 0;
        lcd_status.col = 0;
        lcd_ioctl(0, 0, LCD_HD44780_CLEAR, 0);
      }
      display_write_instr(0x80 | (unsigned char)
                          row_col_to_addr(lcd_status.row, lcd_status.col++));
      udelay(50);
    }
    display_write_text(buf[i]);
    udelay(50);
  }

  return count;
}

static int lcd_ioctl(struct inode *inode, struct file *file,
              unsigned int op, unsigned long arg)
{
  switch (op) {
   case LCD_HD44780_CAH:  /* Cursor at home */
    display_write_instr(0x02);
    lcd_status.row = 0;
    lcd_status.col = 0;
    udelay(1700);
    break;
    
   case LCD_HD44780_CLEAR:  /* Clear display */
    display_write_instr(0x01);
    lcd_status.row = 0;
    lcd_status.col = 0;
    udelay(1700);
    break;
    
   case LCD_HD44780_SET_DDRAM:  /* Set DDRAM address */
    display_write_instr(0x80 | (unsigned char)arg);
    udelay(50);
    break;
    
   case LCD_HD44780_DISP:  /* Display on/off control */
    display_write_instr(0x08 | (unsigned char)arg);
    udelay(50);
    break;

   case LCD_HD44780_LCD_SHIFT:  /* Cursor/display shift */
    display_write_instr(0x10 | (unsigned char)arg);
    udelay(50);
    break;

   case LCD_HD44780_NEWLINE:  /* Set new line mode */
    if (arg == 1)
      lcd_status.flags |= LCD_NL;
    else if (arg == 0)
      lcd_status.flags &= ~LCD_NL;
    break;

   case LCD_HD44780_GOTO_XY:  /* Move cursor to x, y */
    lcd_status.row = (arg & 0x000000ff);
    lcd_status.col = (arg & 0x0000ff00) >> 8;
    break;

   default:
    return -EINVAL;
    break;
  }  

  return 0;
}

static int lcd_open(struct inode *inode, struct file *file)
{
  if (lcd_status.flags & LCD_BUSY) {
    return -EBUSY;
  }
  
  lcd_status.flags |= LCD_BUSY;
  lcd_status.flags |= LCD_NL;
  
  lcd_status.row = 0;
  lcd_status.col = 0;

  *R_PAR0_CONFIG = 0x00000060;  /* Manual-mode, enable port,
                                   immediate mode change */
  
  lcd_init_int();
  
  return 0;
}

static int lcd_close(struct inode *inode, struct file *file)
{
  *R_PAR0_CONFIG = 0x00000000;  /* Reset port */

  lcd_status.flags = 0;

  return 0;
}

static int __init etrax_par_lcd_hd44780_init(void)
{
  printk("ETRAX 100LX parallel LCD HD44780 driver v0.2 (c) 2000-2001 Axis Communications AB\r\n");
  
  if (register_chrdev(LCD_HD44780_MAJOR, "LCD_HD44780", &lcd_fops)) {
    printk("unable to get major %d for LCD_HD44780\n", LCD_HD44780_MAJOR);
  }

  return 0;
}

module_init(etrax_par_lcd_hd44780_init);
/* etrax100parlcd.h */

/*
 *
 * ioctls for etrax parallel lcd driver
 *
 */

#define LCD_HD44780_CAH        _IO('l', 0x10)
#define LCD_HD44780_CLEAR      _IO('l', 0x11)
#define LCD_HD44780_DISP       _IOW('l', 0x12, char)
#define LCD_HD44780_SET_DDRAM  _IOW('l', 0x13, char)
#define LCD_HD44780_LCD_SHIFT  _IOW('l', 0x14, char)
#define LCD_HD44780_NEWLINE    _IOW('l', 0x15, char)
#define LCD_HD44780_GOTO_XY    _IOW('l', 0x16, short) /* 0xff00 = x and 0x00ff = y */

Example Code/Usage

The LCD driver will be accessed through the /dev file system just as any other device under UNIX. This means that we should be able to write to it using standard shell utilities i.e.
echo "Hello World!" > /dev/lcd

Notes

The header file asm/svinto.h contains the register definitions e.g. R_PAR0_CTRL_DATA and R_PAR0_CONFIG

register_chrdev tries to register the driver at major 126. Take note that it passes a reference to the file_operations struct lcd_fops, if all goes well all file operations on /dev/lcd will map to those being referenced in the lcd_fops structure

There are a number of calls to a function named udelay, this puts the kernel in a busy wait loop so that we can synchronize with the display. The display normally has a 40Ás delay due to internal execution. However some instructions like the 'clear' and 'cursor at home' have the somewhat longer delay of 1.64ms.

If you for some reason want to use par1 (controlled by the R_PAR1_* registers) instead of par0, it is important to enable it in head.S. This can be done by locating the following code (in head.S):

#if defined(CONFIG_ETRAX_PARALLEL_PORT1) || \
    defined(CONFIG_ETRAX_ETHERNET_LPSLAVE)
        or.w    0x80,r0 ; parport 1 enabled using DMA 4/5
#endif
And changing it to:
#if defined(CONFIG_ETRAX_PARALLEL_PORT1) || \
    defined(CONFIG_ETRAX_ETHERNET_LPSLAVE) || \
    defined(CONFIG_I_WANT_TO_USE_PAR1)
        or.w    0x80,r0 ; parport 1 enabled using DMA 4/5
#endif



$Date: 2001/05/11 07:44:40 $
$Revision: 1.2 $
Please send feedback on how to improve this document to technology@axis.com.
Axis Communications, Technology Division