Index of Linux Module


1. Introduction

This page contains an example of a linux kernel module. The module converts all data into morse code and controls the LED's from the keyboard.

The example code can be downloaded here.

1.1 Goals of the project

Following section has been copied from [OS]. The goal of this project is to develop a module for the Linux kernel implementing a character oriented device that reads some input from the users and blinks the corresponding morse code (MO) on the keyboard LED's. An user writer process writes data into one end of the device, and the a kernel thread reads reads it from the other end and produces the morse code. The device itself contains a small FIFO buffer used to store temporarily the data being transferred: when the buffer is full, a writer trying to put new data into the device is put asleep; when the buffer is empty, the kernel thread reader is put asleep. To avoid concurrency problems, only one writer at time can send data to the device.

1.2 Requirements

Following requirements were given by the project evaluation team:

Nr. Requirement Met
1 The module must implement a char device for the Linux kernel. yes
2 Only one writer at time can access the device for writing. yes
3 The size of the buffer, and the size of chunk of data read by the reader, can be set upon loading (module parameters). yes (only the buffer size can be set)
4 The length (in milliseconds) of both dot and dashes must be configurable as module parameter. Yes (only one time can be set)
5 When there is no data to convert to morse, the kernel reader is put asleep and the writer is woken up. yes
6 When there is no space left for writing into the buffer, the writer is put asleep and the reader is woken up. yes
7 You must implement a /proc interface to expose the current status of the device (available data, device status, ...) yes
8 You must provide some helper scripts to create the necessary device files in /dev yes
9 You must provide a makefile to compile your module yes
10 you must provide an user-space program (with source code, in whatever language you like) to test and show the functionalities and behaviors of your module (basic functionalities, concurrency management, sleeping, etc.). yes

The requirements met are described in the implementation section.

1.3 Terminology

The kernel module for morse code is called keymorse.
The device driver in /dev directory is called mo0.

2. Design

The module is designed using 2 threads, a writer thread and a reader thread processing the data and controlling the keyboard LED's.

The writer thread is provided by the kernel itself. Each write method is blocked until all data is written to the FIFO queue.

The reader thread is instantiated once loading the module to the kernel. Only one reader thread exists.

Instead of communicating directly with the keyboard hardware the TTY-Keyboard driver is used to control the LED's.

2.1 Lock mechanism

Requirement 2: To meet this requirement a WriterLock is used. Only one writer thread can access to the queue at the time. This ensures that no data gets mixed up.

Requirement 5: The reader turns in an endless loop. If no data is available in the FIFO queue the reader is put asleep and waked up if a writer puts some data into the queue. Once the reader has reawaken it acquires the FIFO-Lock and reads a character out of the FIFO. Now since some space in the FIFO is gained, the possibly waiting writer can be reawaken.

Requirement 6: The writer is trying to write each character separately into the FIFO (loop). If the FIFO is full the writer thread is put asleep (waitfor_EmptySpace) until the reader has processed some data. If there is enough space, the FIFO-Lock is acquired and the data is written to the FIFO. After that the waiting reader is woken up, since the reader is possibly waiting for data.

The sending of the character can be done outside of the FIFO-Lock.

2.2 Morse code

The morse code is provided in a binary table. To get a good performance the character is used as index. The table entry contains a binary string containing the morse code. To send the morse code only one time parameter is used.

  • Each character is separated with 2 time slices.
  • Each sign is separated by one time slice
  • The dash has a length of 3 time slices.
  • The dot has the length of one time slice.

To configure a correct morse code the only one time can be configured within this driver implementation (requirement 4).


3. Implementation

This section talks about the implementation of the keymorse kernel module. The techniques and some code fragments were taken from the Linux Driver book [DRV].

First the project structure is discussed. Then the key implementation of the module is explained.

3.1 Project Structure

The project contains 5 files:

keymorse.h Header for definitions and forward declarations of functions and data structures
keymorse.cpp Implementation of keymorse driver, including processing thread
Make Make file to compile and build the driver
Install.sh Installs the driver and creates the device mo0
Uninstall.sh Uninstalls the driver and removes the device mo0

The keymorse.cpp is parted into three main sections, functions of the reader thread, functions of the writer thread and registration functions.

3.2 Registration functions

The registration functions are needed to have a valid kernel module.

///Initialization of the module
static int initModule(void)
{
  ...
}

///uninitialisation of the module
static void exitModule(void)
{
  ...
}


//Hooks
module_init(initModule);
module_exit(exitModule);

Before going into the two main functions initModule() and exitModule() lets looks at some very important structures and variables needed to register and manage the kernel module. The keymorse module is written as a character device The complete device data is managed with the TypeMorseDevice data structure. It contains following elements:

cdev character device structure (Requirement 1).
pThread Reader thread
lockMutex Mutex to protect the FIFO queue
lockWriter Mutex to ensure that only one writer can send its data to the FIFO queue (Requirement 2).
pcFifoBuffer FIFO Buffer to communicate with the reader thread
pTTYDriver The keyboard device driver used to control the keyboard LED's

struct TypeMorseDevice
{
  struct cdev         cdev;
  struct task_struct* pThread;
  struct kfifo*       pFifo;
  struct semaphore    lockMutex;
  struct semaphore    lockWriter;
  char*               pcFifoBuffer;
  struct tty_driver*  pTTYDriver;
};

///Variable to handle the data
static struct TypeMorseDevice g_MorseDevice;

A very important structure is used to manage the operations the kernel has to call. The variable containing the file operations is called g_Main_fops. Only the functions for opening and releasing the file and for writing data are used.


///Variable to store file operations
static struct file_operations g_Main_fops =
  {
    .write   = main_write,
    .open    = main_open,
    .release = main_release,
    .owner   = THIS_MODULE,
  };

The device ID containing the major and minor device number is called g_devID.


///ID of the device driver
static dev_t g_devID;

All these variables must be initialized when loading the module to the kernel. The first step after initializing some global variables is to register the module as a character device. If the major device number is not given, it will be dynamically allocated by the kernel.

///Name of the device
#define KEYMORSE_NAME     "keymorse"

///Initialisation of the module
static int initModule(void)
{
  int iRetval = 0;
  int iIndex  = 0;
  ...
  //Registers the character device
  if (Mo_Major > 0)
  {
    g_devID = MKDEV(Mo_Major, Mo_Minor);
    iRetval = register_chrdev_region(g_devID, 
                                     Mo_Dev_Numbers, 
                                     KEYMORSE_NAME);
  }
  else
  {
    iRetval = alloc_chrdev_region(&g_devID, Mo_Minor, 
                                  Mo_Dev_Numbers, 
                                  KEYMORSE_NAME);
    Mo_Major = MAJOR(g_devID);
  }

  if (iRetval == 0)
  {
    //Registers the device methods and adds them to the kernel
    iRetval = setup_cdev(&g_MorseDevice, iIndex);
    ...
  }
  ...
  return iRetval;
}

///uninitialisation of the module
static void exitModule(void)
{
  if (Mo_Major > 0)
  {
    unregister_chrdev_region(g_devID, Mo_Dev_Numbers);
    release_cdev(&g_MorseDevice);
  }
  ...
}

Next step is setting up the module functions for open(), release() and write(). This is done by the setup_cdev() function, witch wraps the complete setup procedure. The release_cdev() function is used to unregister the module functions.


/************************************************************/
/*! this function registers the main functions of the device
*/
/************************************************************/
static int setup_cdev(struct TypeMorseDevice* pData, int iIndex)
{
  int iDeviceNo = MKDEV(Mo_Major, Mo_Minor + iIndex);

  cdev_init(&pData->cdev, &g_Main_fops);
  pData->cdev.owner = THIS_MODULE;
  pData->cdev.ops   = &g_Main_fops;

  return cdev_add(&pData->cdev, iDeviceNo, 1);
}

/*************************************************************/
/*! this function releases the device data
*/
/*************************************************************/
static void release_cdev(struct TypeMorseDevice* pData)
{
  cdev_del(&pData->cdev);
}

The last step is to prepare the semaphores, FIFO and the thread for communication between the writer and reader thread. The waiting queues are initialized outside of the initialization function.

The FIFO queue must be allocated with size by power of 2. Therefore the buffer size variable is a number between 5 to 10. This gives a minimal size of 32 bytes and a maximal size of 1024 bytes.

The semaphores lockMutex and lockWriter are both set to 1, meaning that only one instance can acquire the lock.

After the initialization the reader thread can be stared. The thread method will be run_thread().

When uninitializing the module it's important that first the thread is stopped, before freeing the FIFO memory. Otherwise the thread will access to freed memory.

///Queue for reader
DECLARE_WAIT_QUEUE_HEAD(queueReader);
///Queue for writer
DECLARE_WAIT_QUEUE_HEAD(queueWriter);
///Number of bytes not yet processed
static atomic_t g_atomUnprocessedData = ATOMIC_INIT(0);


///Initialisation of the module
static int initModule(void)
{
  int iFifoBufferSize = 0;
  ....
  //create fifo and create thread
  if (iRetval == 0)
  {
    iFifoBufferSize = 0x1 << BufferSize;
    atomic_set(&g_atomUnprocessedData, 0);
    g_MorseDevice.pcFifoBuffer = kmalloc(iFifoBufferSize*sizeof(char), 
                                         GFP_KERNEL);
    g_MorseDevice.pFifo = kfifo_init(g_MorseDevice.pcFifoBuffer,
                                     iFifoBufferSize, GFP_KERNEL, NULL);
    g_MorseDevice.pTTYDriver = vc_cons[fg_console].d->vc_tty->driver;
    init_waitqueue_head(&queueReader);
    init_waitqueue_head(&queueWriter);
    sema_init(&g_MorseDevice.lockMutex, 1);
    sema_init(&g_MorseDevice.lockWriter, 1);
    //start the morse thread
    g_MorseDevice.pThread = kthread_run(run_morse, &g_MorseDevice,
                                        "morse_thread");
    if (IS_ERR(g_MorseDevice.pThread))
    {
      printk(KERN_WARNING "mo 0-2: Error creating the morse_thread_xx\n");
      iRetval = -EAGAIN;
    }
  }
}

///uninitialisation of the module
static void exitModule(void)
{
  ...
  if (g_MorseDevice.pThread != NULL)
  {
    kthread_stop(g_MorseDevice.pThread);
    g_MorseDevice.pThread = NULL;
  }

  if (g_MorseDevice.pcFifoBuffer != NULL)
  {
    kfree(g_MorseDevice.pcFifoBuffer);
    g_MorseDevice.pcFifoBuffer= NULL;
  }

}

At the end lets look at the /proc interface, witch is needed to receive some status information about the data processing. To access the information of the queue the mutex of the FIFO queue must be acquired, because we need several instructions to calculate the available size of the queue.

For unprocessed data an atomic integer is suitable, since only reading is applicated.

///Name of the device
#define KEYMORSE_NAME     "keymorse"

///Initialisation of the module
static int initModule(void)
{
  ...
  //initialize the proc file 
  create_proc_read_entry(KEYMORSE_NAME, 
                         0, 
                         NULL, 
                         read_morsemem, 
                         NULL);
  ...
}

///uninitialisation of the module
static void exitModule(void)
{
  //Remove the proc entry
  remove_proc_entry(KEYMORSE_NAME, NULL);
  ...
}

/********************************************************/
/*! Reads the data for proc mem file
*/
/********************************************************/
/*static*/ int read_morsemem(char* pcBuffer, 
                             char** ppcStart, 
                             off_t iOffset, 
                             int iCount, 
                             int* piEOF, 
                             void* pData)
{
  int iLen = 0;
  if (down_interruptible(&g_MorseDevice.lockMutex) == 0)
  {
    iLen += sprintf(pcBuffer + iLen, "Morse Code Driver [%d.%d]\n", 
                    Mo_Major, Mo_Minor);
    iLen += sprintf(pcBuffer + iLen, "Blinktime: %d [ms]\n", BlinkTime);
    iLen += sprintf(pcBuffer + iLen, "TotalMem : %d [Bytes]\n",
                    g_MorseDevice.pFifo->size);
    iLen += sprintf(pcBuffer + iLen, "FreeMem  : %d [Bytes]\n",
                    g_MorseDevice.pFifo->size - g_MorseDevice.pFifo->in +
                    g_MorseDevice.pFifo->out);
    iLen += sprintf(pcBuffer + iLen, "Data to process: %d [Bytes]\n",
                    atomic_read(&g_atomUnprocessedData));
    up(&g_MorseDevice.lockMutex);
  }
  *piEOF = 1;
  return iLen;
}

3.3 Writer thread

Before any data is written to the kernel module, the connection (file) must be opened. Therefore the function main_open() is called. What is done here is basically setting the device data pointer to the private data of the file pointer.

This is not very important for the general implementation, as long as not different device drivers are supported.

Whenever an main_open() is called there must be a main_release() at the end of the usage.


/*************************************************/
/*! Opens the device
*/
/*************************************************/
/*static*/ int main_open(struct inode* pNode, 
                         struct file* pFile)
{
  struct TypeMorseDevice* pDevice;
  pDevice = container_of(pNode->i_cdev, struct TypeMorseDevice, cdev);
  pFile->private_data = pDevice;
  return 0;
}

/*************************************************/
/*! Closes the device
*/
/*************************************************/
/*static*/ int main_release(struct inode* pNode, 
                            struct file* pFile)
{
  return 0;
}

Writing the data to the FIFO is more involved. First the communication data structure of the module has to be setup. Then the data must be copied from user level memory into the kernel level memory, otherwise it will be not accessible.

The data buffer of kernel module is one character larger than the data buffer of the user level memory. The last item of the kernel level buffer is set to NULL in order to receive a regular NULL terminated string. This is useful for printing and debugging.


/*************************************************/
/*! Used to write data to the device
*/
/*************************************************/
/*static*/ ssize_t main_write(struct file* pFile, 
                              const char __user* pcUserData, 
                              size_t iSize, 
                              loff_t* piPos)
{
  int iRetval = -EFAULT;
  struct TypeMorseDevice* pDevice = 
    (struct TypeMorseDevice*)pFile->private_data;

  if (pDevice != NULL && iSize > 0)
  { 
    char* pcBuffer = kmalloc((iSize+1)*sizeof(char), GFP_KERNEL);
    if (pcBuffer != NULL)
    {
      memset(pcBuffer, 0, iSize*sizeof(char));
      if (copy_from_user(pcBuffer, pcUserData, iSize) == 0)
      {
        pcBuffer[iSize] = 0x00;
        ...
      }
      kfree(pcBuffer);
    }
  }
}

Before entering the writing section the size of unprocessed data is increased. Then the writer mutex is tried to acquire, witch is used to meet the requirement 2 (only one writer at the time can access the FIFO queue). If the acquirement failed the counter of unprocessed data must be reduced by the amount just added before.

If the writer lock is acquired it's able to enter the writeData() function, where the writer thread can write all its data to the FIFO queue.

...
  atomic_add(iSize, &g_atomUnprocessedData);
  if (down_interruptible(&pDevice->lockWriter) == 0)
  {
    writeData(pDevice, pcBuffer, iSize);
    up(&pDevice->lockWriter);
    iRetval = iSize;
  } //down_interruptible(&pDevice->lockWriter) == 0
  else
  {
    //Could not aquire the write lock (interrupted)
    // -> decrement the unprocessed data size
    atomic_sub(iSize, &g_atomUnprocessedData);
  } 
...

The writeData() function writes the data to the FIFO queue in 4 steps.

First step is to check if the FIFO queue is full or if there is still space available to send data. If not the writer thread is getting suspended and put in the waiting queue queueWriter.

The second step can be done if there is enough space available to send data. Therefore the writer tries to acquire the FIFO lock lockMutex. This ensures that only one thread (either writer thread, reader thread or status information reader) can access the FIFO queue.

The third step is to send the data to the queue and to release the FIFO lock afterwards. This step is done as long as there is free space and not all data is written to the FIFO.

The last step is to wake up the reader thread, witch might wait for data arriving from the FIFO queue.

Step 1 to 4 are repeated n times if the string contains n characters.

In case of aborting (interrupting) a writer, the number of unprocessed data must be set back by the amount left to send to the FIFO queue.


/***********************************************/
/*! writes the data to the fifo
*/
/***********************************************/
static void writeData(struct TypeMorseDevice* pDevice, 
                      char* pcBuffer, 
                      int iSize)
{
  char acChar[2] = { 0, 0};
  int i          = 0;
  int iRetval    = 0;

  printk(KERN_NOTICE "mo 0-2: writeData() - ");
  while(i < iSize)
  { 
    //1. Check if there is space available to write
    iRetval = wait_event_interruptible(queueWriter, 
      (pDevice->pFifo->size - pDevice->pFifo->in + pDevice->pFifo->out > 0));

    //2. get fifo lock
    if (iRetval == 0 && down_interruptible(&pDevice->lockMutex) == 0)
    {
      //3. Send data and release fifo lock
      while(i < iSize && 
        pDevice->pFifo->size - pDevice->pFifo->in + pDevice->pFifo->out > 0)
      {
        acChar[0] = pcBuffer[i];
        __kfifo_put(pDevice->pFifo, acChar, 1);
        i++;
      }
      up(&pDevice->lockMutex);

      //4. wake the reader
      wake_up_interruptible(&queueReader);
    }

    //Error
    else
    {
      //Just to be sure the unprocessed data equals the 
      // number of bytes when we entered the function
      atomic_sub(iSize -i, &g_atomUnprocessedData);

      i = iSize;
      printk(KERN_NOTICE "Proceeding interrupted (2)\n");
    }
  }
}

3.4 Reader thread

The reader thread runs in the function run_morse(). As parameter the module data structure is passed containing all mutexes and communication data objects. The thread turns inside a while loop. It's very important to check whether the thread must stop of not. Otherwise it is impossible to uninstall the kernel module and stopping the thread at all. The function kthread_should_stop() takes care of this.


/************************************************/
/*! This runs the morse thread, witch will convert 
    and send all data to the keyboard
*/
/************************************************/
static int run_morse(void* pData)
{
  struct TypeMorseDevice* pDev = 
       (struct TypeMorseDevice*)pData;
  int iRetval = 0;

  if (pDev != NULL)
  {
    printk(KERN_WARNING "mo 0-2: Thread started...\n");

    set_current_state(TASK_INTERRUPTIBLE);
    while(!kthread_should_stop())
    {
      //Reading data from the FIFO queue
      ...
    }
    __set_current_state(TASK_RUNNING);
    printk(KERN_WARNING "mo 0-2: Thread ended.\n");
  } //(pDev != NULL)

  return 0;
}

The reading is done in 5 steps.

The first step is checking whether there is data available to process or not. If no data is available the reader has to be suspended and put in the waiting queue queueReader. There is also the condition kthread_should_stop() witch must be checked, in case the thread must stop.

The second step is done when ever the reader wakes up (not interrupted and not stopped). Here it has to acquire the FIFO lock lockMutex.

The third step is reading data from the FIFO queue and releasing the lockMutex afterwards.

The fourth step is waking up the possibly waiting writer.

The last step is processing the data read before. This can be done outside of any critical section, since the data is copied into a local memory (variable). At the end the number of unprocessed data can be reduced.

Note that only one character is read at the time since sending it as morse code takes very long, it is not necessary to read in blocks to speedup the process.


  //1. check if there is data to write
  set_current_state(TASK_INTERRUPTIBLE);
  iRetval = wait_event_interruptible(queueReader, 
      (pDev->pFifo->in - 
       pDev->pFifo->out > 0 || 
       kthread_should_stop()));

  //2. check error and try to get the fifo lock
  set_current_state(TASK_INTERRUPTIBLE);
  if (iRetval == 0 && 
      !kthread_should_stop() && 
      down_interruptible(&pDev->lockMutex) == 0)
  {
    char acBuffer[1];
    __kfifo_get(pDev->pFifo, acBuffer, 1);

    //3. Release the lock
    up(&pDev->lockMutex);

    //4. wake up all writers
    wake_up_interruptible(&queueWriter);

    //5. send character as morse code to keyboard
    sendCode(pDev, acBuffer[0]);
    atomic_dec(&g_atomUnprocessedData);
  }

Finally the processing code witch is implemented within the sendData() function is explained.

3.4.1 Processing morse code

The morse code is implemented in a table of TypeMorseItem items. A morse code item contains one value for the morse code length (cLength) and one for the morse code itself (cCode).

The morse code is coded in bit format. Each bit set to 0 gives a short sign. Each bit set to 1 gives a long sign. The length is used to know how many signs must be send. For instance the number 0 (Zero coded as 0x00) can represent different morse codes depending on the given length. Length 1 is used to send an "e", where as length 4 represents "h". The whole morse code table is added in the appendix.

struct TypeMorseItem
{
  char cLength;
  char cCode;
};

static struct TypeMorseItem g_acDataMap[59] = { ..}

The table contains 59 items in total, including characters A to Z, a to z and digits 0 to 9 as well as some additional characters for punctuation. In fact the lower case must be mapped to the upper case morse code (see below).

static void sendCode(struct TypeMorseDevice* pDev, char cASCII)
{
  struct TypeMorseItem Item;

  printk(KERN_NOTICE "mo 0-2:   Sending char code: %d\n", cASCII);

  //Make sure the border are met.
  if (cASCII < 32) { cASCII = 32; }
  if (cASCII > 90)
  {
    // Convert lower case to upper case
    if (cASCII >= 97 && cASCII <= 122)
    {
      cASCII -= 32;
    }
    else
    {
      cASCII = 32;
    }
  }
  Item = g_acDataMap[cASCII-32];
...
}

The processing algorithm must start with the LSB (least significant bit). The time intervals are set by the variable BinkTime (see parameters).

Before sending a letter 2x the interval is waited to separate the different letters. Long signs have a duration of 3x the interval as the short signs have only one time interval. Between two signs one time interval is used to turn off the LED's.

//Break from one character to the other one (+BlinkTime from the last send 
// character)
schedule_timeout(2*BlinkTime);
for (i = Item.cLength-1; i >= 0; i--)
{
  iLong = (Item.cCode & (1 << i));
  (pDev->pTTYDriver->ioctl)(vc_cons[fg_console].d->vc_tty, 
                            NULL, 
                            KDSETLED, 
                            LED_SCR | LED_NUM | LED_CAP);  //Capslock

  // Long term blinking
  if (iLong)
  {
    set_current_state(TASK_INTERRUPTIBLE);
    schedule_timeout(3*BlinkTime);
  }

  //Short term blinking
  else
  {
    set_current_state(TASK_INTERRUPTIBLE);
    schedule_timeout(BlinkTime);
  }

  //Turn the light off
  (pDev->pTTYDriver->ioctl)(vc_cons[fg_console].d->vc_tty, 
                            NULL, 
                            KDSETLED, 
                            0xFF); //restore
  set_current_state(TASK_INTERRUPTIBLE);
  schedule_timeout(BlinkTime);
}

4. Manual

This section describes how to build, install and use the keymorse kernel module. First a brief introduction how to build the module is given, including installing and uninstalling. Then some information about implemented interfaces is given such as module information and status information over the /proc interface.

4.1 Building the project

Commands used to build and install the keymorse kernel module:

To build the project just call > make
To clean the project call > make clean

Requirement 9: Installing and uninstalling script files are provided within the project.

To install the the keymorse module (Make sure the root mode is used) # ./Install.sh
To uninstall the keymorse module (Make sure the root mode is used) # ./Uninstall.sh

4.1.1 Driver Parameters

Requirement 3 and 4: Following parameters are provided:

Name Description Min Max
BufferSize Requirement 3: Size of the internal processing FIFO buffer. The buffer size must is a value of power to 2. Therefore the value 5 gives a buffer of 2^5 = 32 Bytes 5 10
BlinkTime Requirement 4: Time in milliseconds of the short blinking morse code 10 --
Mo_Major Major number of the device driver 0 255

Changing the parameters can be done only when the install script runs. For instance to change the buffer size and the blinking time following command can be used:

# ./Install.sh BufferSize=10 BlinkTime=75

The module uses now a FIFO of 1024 Bytes and the short time interval is set at 75 ms.

4.2 Information Interface

To receive general informations about the driver, such as author, version, parameters etc., call following command:


# modinfo ./keymorse.ko

filename:       keymorse.ko
license:        GPL
author:         Benjamin Hadorn
description:    Morse code blink module for key board
vermagic:       2.6.18.8-0.10-default SMP mod_unload 586 REGPARM gcc-4.1
depends:
srcversion:     87CEACA82233F3B33498006
parm:           Mo_Major:Device major number (int)
parm:           BufferSize:Size of the buffer. 
                If BufferSize=5 -> 2^5 = 32 Bytes (int)
parm:           BlinkTime:Time of blinking in [ms] (int)

Requirement 7: To receive live time informations, call following command:


# cd /proc
# more keymorse

MorseCodeDriver [254.0]
Blinktime 10 [ms]
TotalMem  1024 [Bytes]
FreeMem   975 [Bytes]
DataMem   50 [Bytes]

The output will show the status about the module:

  • Blink time in [ms]
  • Total memory of the FIFO queue
  • Available memory (free memory) of the FIFO queue
  • Data waiting to be processed. This includes all unprocessed data of the current writer and all data of writer waiting to send their data.

4.3 Sending data to the driver

To send data to the driver over command line use following command:

# echo "My data" > /dev/mo0

This sends the string "My data" to the device mo0.


Test Application

The test is done within 3 steps. The first step shows and tests the requirement 7. It reads the current driver state and prints it to the screen.


  TAutoPtr<TTextInputStream> ptrStream = 
    new TTextInputStream(TString(L"/proc/keymorse"),
                         TTextInputStream::etISO_8859_1);
  TString strText;
  while (ptrStream->readLine(strText) == RET_NOERROR)
  {
    ...
  }

The 2nd step tests the requirements 1, 5 and 6. It sends a simple string to the device and waits for its completion. After the completion the processed data is read and compared with the data sent. The test includes the correct order of Bytes and the correct number of processed data.

///Sending data
void sendDataToDriver(const TString& rData)
{
  TAutoPtr<TFileOutputStream> ptrStream = 
    new TFileOutputStream(TString(L"/dev/mo0"), false);
  ptrStream->write(rData.c_str(NULL), rData.getSize());
}

//Reading processed data
void readProcessedData(TString& rData)
{
  rData = L"";
  TAutoPtr<TTextInputStream> ptrStream = 
    new TTextInputStream(TString(L"/proc/keymorse_test"),
                         TTextInputStream::etISO_8859_1);
  TString strText;
  while (ptrStream->readLine(strText) == RET_NOERROR)
  {
    rData += strText;
  }
}

The 3rd step tests "the requirement 2" that only one writer can write to the driver. Therefore the data from each writer must be processed separately. The test starts 3 threads each writing 32 Bytes to the device. The first thread is not blocked. The 2nd and 3rd thread are blocked. At the end the processed data must has a format <Data1><Data2><Data3>. This test ensures that the data from each thread is processed separately, each after the other.


6. Conclusion

The project gives a very good idea how Linux drivers are working (in general). It shows how flexible modern operation systems are. As a future part writing a similar driver for Windows would help to state the compare Linux with Windows.

This document might be used as reference even to implement other character devices. It gives a good overview of what is needed to implement a simple kernel module. For more sophisticated drivers the book [DRV] is strongly recommended as reference.


A. Morse code table

The morse code source was taken from [MORSE].

ASCII code Character Morse code Morse code length Binary morse code
32 space   0  
33 ! 00110 5 0x06
34 " 010010 6  
35 #   0  
36 $   0  
37 %   0  
38 &   0  
39 ' 011110 6 0x1E
40 (   0  
41 )   0  
42 *   0  
43 +   0  
44 , 110011 6 0x33
45 -   0  
46 . 010101 6 0x16
47 /   0  
48 0 11111 5 0x1F
49 1 01111 5 0x0F
50 2 00111 5 0x07
51 3 00011 5 0x03
52 4 00001 5 0x01
53 5 00000 5 0x00
54 6 10000 5 0x10
55 7 11000 5 0x18
56 8 11100 5 0x1C
57 9 11110 5 0x1E
58 : 111000 6 0x38
59 ;   0  
60 <   0  
61 = 10001 5 0x11
62 >   0  
63 ? 001100 6 0x0C
64 @   0  
65 A 01 2 0x01
66 B 1000 4 0x08
67 C 1010 4 0x0A
68 D 100 3 0x04
69 E 0 1 0x00
70 F 0010 4 0x02
71 G 110 3 0x06
72 H 0000 4 0x00
73 I 00 2 0x00
74 J 0111 4 0x07
75 K 101 3 0x05
76 L 0100 4 0x04
77 M 11 2 0x03
78 N 10 2 0x02
79 O 111 3 0x07
80 P 0110 4 0x06
81 Q 1101 4 0x0D
82 R 010 3 0x02
83 S 000 3 0x00
84 T 1 1 0x01
85 U 001 3 0x01
86 V 0001 4 0x01
87 W 011 3 0x03
88 X 1001 4 0x09
89 Y 1011 4 0x0B
90 Z 1100 4 0x0C

B. References