wiki:NewDriver

Adding a driver may be quite easy if the driver class already exists. If not, create a new driver class and come back here.

We'll go through this guide assuming we are adding a new character device, we'll call it pmtt.

This device is an imaginary one with:

  • a transmit register TXR,
  • a receive register RXR,
  • a status register STR, telling whether we can receive,
  • an interrupt mask register ITR, masking send / receive interrupts.

Creating a new directory

We'll go to the correct directory in the source tree and create the directory for the device:

$ cd mutekh/device/driver/char
$ mkdir pmtt
$ cd pmtt

Creating the files

Now we can create the files. There are usually 4 files for a device:

  • a .config for the BuildSystem,
  • a .h for declaring the public functions,
  • a .c implementing the actual driver,
  • a -private.h containing the private definitions for the driver.

The .config file

Let's start with the .config file. It will contain:

%config CONFIG_DRIVER_CHAR_PMTT
desc Enable PMTT imaginary device
default undefined
provide CONFIG_DRIVER_TTY
%config end

Here we declare this driver implements a TTY, i.e. a device with a send/receive interface.

The .h file

Now we may define the external interface of our device.

Interface function prototypes are defined in two headers:

  • device/device.h defines the class-agnostic functions: init, cleanup and irq,
  • device/class.h defines the class-specific functions. for character devices, there is only a request function to implement.

In order to enforce the correct prototype, even in case of evolution, it is mandatory to use the proper macros DEV_INIT, DEV_CLEANUP, DEV_IRQ and DEVCHAR_REQUEST.

#ifndef DRIVER_TTY_PMTT_H
#define DRIVER_TTY_PMTT_H

#include <device/char.h>
#include <device/device.h>

/* The device constructor & destructor */
DEV_INIT(pmtt_init);
DEV_CLEANUP(pmtt_cleanup);

/* The request handler */
DEVCHAR_REQUEST(pmtt_request);

/* The IRQ handler */
DEV_IRQ(pmtt_irq);

#endif

The private header

Here we can define the internal constants, like the header offsets and the register meanings.

All the internally-used structures may also be defined here. Driver instances may have an internal state, we'll define a struct pmtt_state_s containing the current pending request.

#ifndef DRIVER_TTY_PMTT_PRIVATE_H
#define DRIVER_TTY_PMTT_PRIVATE_H

/* Register offsets */
#define PMTT_TXR 0
#define PMTT_RXR 4
#define PMTT_STR 8
#define PMTT_ITR 12

/* Status register bits */
#define PMTT_STR_RXREADY 1
#define PMTT_STR_TXREADY 2

struct pmtt_state_s
{
   struct dev_char_rq_s *current_request;
};

#endif

The implementation file

The drivers should be non-blocking and return as soon as possible. A callback (defined in the request structure) has to be called, either from the IRQ handler or the request function, telling the upper layer the about status of the request. Moreover, this driver relies on the ICU driver class in order to receive interrupts.

First we have common header and declarations:

#include "pmtt.h"
#include "pmtt-private.h"

#include <device/icu.h>
#include <hexo/types.h>
#include <device/device.h>
#include <device/driver.h>
#include <hexo/iospace.h>
#include <hexo/alloc.h>
#include <hexo/interrupt.h>

Let's begin with the device constructor. Here we have to:

  • Fill the device structure with a static and read-only pointer-to-function table,
  • Allocate internal state for the driver and put it in the drv_pv field of the device,
  • Register to the interrupt controller unit,
  • Initialize the peritheral.

The function table is:

static const struct driver_s pmtt_drv =
{
    .class = device_class_char,
    .f_init = pmtt_init,
    .f_cleanup = pmtt_cleanup,
    .f_irq = pmtt_irq,
    .f.chr = {
        .f_request = pmtt_request,
    }
};

When a device is created, the calling code must fill the device structure addr[], icudev and irq fields.

The addr[] field may be used anytime, we'll use it later.

The driver is responsible for registering to the ICU.

DEV_INIT(pmtt_init)
{
   struct pmtt_state_s *state = mem_alloc(sizeof(struct pmtt_state_s), MEM_SCOPE_SYS);

   if ( state == NULL )
     return ENOMEM;

   state->current_request = NULL;

   dev->drv = &pmtt_drv;

   dev->drv_pv = state;

   DEV_ICU_BIND(dev->icudev, dev, dev->irq, pmtt_irq);

   return 0;
}

The destructor is quite straightforward:

DEV_CLEANUP(pmtt_cleanup)
{
   struct pmtt_context_s *pv = dev->drv_pv;

   DEV_ICU_UNBIND(dev->icudev, dev, dev->irq);

   mem_free(pv);
}

For character devices, the upper layer may decide to continue with the request or not, thus the callback has to be called any time the status changes. See trunk/mutekh/drivers/include/device/char.h for complete reference.

For the sake of simplicity, this driver won't be able to handle concurrent requests. Therefore we'll return directly from handler if there is already a pending request.

DEVCHAR_REQUEST(pmtt_request)
{
   struct pmtt_context_s *pv = dev->drv_pv;

   if (rq->size == 0) {
      rq->error = 0;
      rq->callback(dev, rq, 0);
   }

   if (pv->current_request != NULL) {
      rq->error = EBUSY;
      rq->callback(dev, rq, 0);
   }

   pv->current_request = rq;

   switch (rq->type)
   {
   case DEV_CHAR_READ:
      // Enable RX irq
      cpu_mem_write_32(dev->addr[0] + PMTT_ITR, PMTT_RXREADY);
      break;

   case DEV_CHAR_WRITE:
      // Enable TX irq
      cpu_mem_write_32(dev->addr[0] + PMTT_ITR, PMTT_TXREADY);
      break;
    }
}

Then we can define the IRQ handler, where all the device logic takes place:

DEV_IRQ(pmtt_irq)
{
   struct pmtt_context_s *pv = dev->drv_pv;

   struct dev_char_rq_s *rq = pv->current_request;

   size_t len = 0;

   switch (rq->type)
   {
   case DEV_CHAR_READ:
      while ( rq->size && (cpu_mem_read_32(dev->addr[0] + PMTT_STR) & PMTT_RXREADY) ) {
         *(rq->data) = cpu_mem_read_32(dev->addr[0] + PMTT_RXR);
         rq->size--;
         rq->data++;
         len++;
      }
      break;

   case DEV_CHAR_WRITE:
      while ( rq->size && (cpu_mem_read_32(dev->addr[0] + PMTT_STR) & PMTT_TXREADY) ) {
         cpu_mem_read_32(dev->addr[0] + PMTT_TXR, *(rq->data));
         rq->size--;
         rq->data++;
         len++;
      }
      break;
   }

   // At last, we can consider the request as finished if the callback says so,
   // or if the size is 0.
   if ( rq->callback(dev, rq, size) || rq->size == 0 ) {
      pv->current_request = NULL;
      cpu_mem_write_32(dev->addr[0] + PMTT_ITR, 0);
   }

   // Tell the ICU we processed the IRQ.
   return 1;
}

Referencing the directory in the upper level directory

There is a Makefile in mutekh/device/driver/char, we have to edit it and add an reference to the pmtt subdirectory if the driver is enabled.

We can add the following lines:

ifeq ($(CONFIG_DRIVER_CHAR_PMTT), defined)
 subdirs += pmtt
endif

Possible improvements

As an exercise to the reader, this driver could be improved to:

  • buffer the incoming characters even if no request is pending,
  • handle concurrent requests,
  • correctly handle the endianness of the memory-mapped registers,
  • correctly lock the device to avoid race-conditions when multiple processors access the same device,
  • handle memory mapping of registers on MMU-enabled platforms.
Last modified 10 years ago Last modified on Dec 22, 2009, 3:49:39 PM