Wednesday, 18 April 2018

Developing a Custom Device Driver


After twiddling parallel port bits using port I/O and ppdev, the Project Trailblazer engineers decided that the potential failures due to drawbacks were unacceptable. Port I/O and ppdev approaches were error prone and risky. They needed a custom device driver for their interface circuit.
Armed with their interface circuit, port I/O, and ppdev knowledge, the Project Trailblazer engineers developed a few requirements for their first device driver:
  • The driver should load at startup.
  • When loaded, the driver should enable the latch output to drive the output modules.
  • The driver should shield software developers from interface logic specifics.
  • The driver should offer control of individual pieces of snow-making equipment without requiring the developer to perform bit manipulation.
  • The driver should offer monitoring of individual lift signals without requiring the developer to perform bit manipulation.
The engineers started learning about device driver development by reading. Rubini and Corbet's Linux Device Drivers (published by O'Reilly) offers a complete discussion of driver development. It covers kernel version 2.4 and new kernel additions. Unfortunately, after reading this book, the engineers still didn't know where to start. Then they found the "Linux Kernel Module Programming Guide,"3 which steps through kernel module development, character device files, the /proc filesystem, communications with device files, I/O controls, blocking processes, scheduling tasks, and interrupt handlers. The engineers learned the following about device drivers:
  • Device drivers are accessed through file operations such as read and write.
  • Device drivers run in the kernel space.
  • Device drivers can be written as loadable modules.
  • Device drivers running in the kernel do not have access to glibc.
  • Device drivers that have memory problems (for example, array out of bounds) could crash the kernel.
  • Traditionally, accessing device drivers is performed through file operations in the /dev directory.
  • Device files in the /dev directory require static or dynamic assignment of major numbers, the general class of the device.
  • Static assignment of major numbers creates the potential for device drivers to clash with each other.
  • Dynamic assignment of major numbers introduces a small inconvenience in locating the device driver.
  • Device drivers can exist in the /proc directory.
  • Many Linux processes make available information via files in the /proc directory.
  • /proc directory device files are created on-the-fly and do not clash with other device files and can be found easily by filename.
The "Linux Kernel Module Programming Guide"3 is out of date, and its examples are difficult to compile. The engineers found the "Linux Kernel Procfs Guide,"4 which applies to kernel 2.4 and is up-to-date. They decided to create the lift monitoring and snow-making control device driver, which would be a loadable kernel module that would use the /proc file system. The engineers planned to be extra careful when writing the device driver, to make sure that array-out-of-bounds conditions never occur. They knew they could not rely on glibc functions. They planned to develop their kernel module device driver around the "Linux Kernel Procfs Guide" program procfs_example.c. They began by writing a helloworld device driver module for the /proc directory. After they were confident in their skills at creating device drivers, they would modify helloworld and create the lift monitoring and snow-making control device driver.

Understanding helloworld_proc_module

Most helloworld programs print "Hello World" and terminate. Kernel module versions of helloworld print "Hello World" to the console or to the system log file when loaded and may print "Goodbye World" when they are unloaded. The helloworld_proc_module will print loading and unloading messages, and it will also store a character string that can be read and written with file operations. This character string offers data persistence across the life of the module. helloworld_proc_module will demonstrate the process of creating the /proc directory file, transferring data from userland to the module, transferring data from the module back to userland, and removing the /proc directory file. Here are the basic steps involved in the execution of helloworld_proc_module:

  1. The module is loaded from a script or by hand, with insmod helloworld_proc_module.o.
  2. The kernel calls the module's init function. The source code uses a macro define called module_init that declares which function is init. In this case, the statement is module_init(init_helloworld).
  3. The module's init function executes and creates a /proc directory file entry called helloworld_file, fills its fields, and then prints a message to the system log. The helloworld_file /proc entry structure contains four fields that the kernel requires for read and write file operations:
    • read_proc� The read_proc field should contain a pointer to the file's callback function that executes for a read file operation. For example, if a user ran the cat /proc/helloworld_proc_module command at the bash prompt, the kernel would ultimately call the file's read_proc function to handle this file read operation.
    • write_proc� The write_proc field should contain a pointer to the file's callback function that executes for a write file operation. If a user ran the echo test > /proc/helloworld_proc_module command at the bash prompt, the kernel would call the file's write_proc function to handle this file write operation.
    • data� The data field contains a pointer to the file's data. During module initialization, a data structure is created and populated. When a read or write callback occurs, using read_proc or write_proc, this data pointer is passed as a parameter. This way, the file's read_proc and write_proc functions can find their associated data.
    • owner� Because the /proc file entry is used in a module, the owner field should be set to THIS_MODULE.
  4. The module is unloaded by a script or by hand, with rmmod helloworld_proc_module.
  5. The kernel calls the module's exit function. The source code uses a macro definition called module_exit that declares which function is exit. In this case, the statement is module_exit(cleanup_helloworld);.
  6. The exit routine removes the /proc file entry and prints a message to the system log file.

This is quite a bit different from the standard helloworld.c program that you're used to seeing. This helloworld_proc_module program seems complicated, but after you get it running and you understand how to set the helloworld_file fields and how the data pointer is passed to the read and write callback functions, it's pretty simple. Listing 7.4 shows the complete helloworld_proc_module.c program.
Listing 7.4 The helloworld_proc_module.c Program
/*
 * helloworld_proc_module v1.0 9/25/01
 * www.embeddedlinuxinterfacing.com
 *
 * The original location of this code is
 * http://www.embeddedlinuxinterfacing.com/chapters/07/
 * helloworld_proc_module.c
 *
 * Copyright (C) 2001 by Craig Hollabaugh
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Library General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

/*
 * helloworld_proc_module.c is based on procfs_example.c by Erik Mouw.
 * For more information, please see The Linux Kernel Procfs Guide,
 * http://kernelnewbies.org/documents/kdoc/procfs-guide/lkprocfsguide.html
 */

/* helloworld_proc_module
 * helloworld_proc_module demonstrates the use of a /proc directory entry.
 * The init function, init_helloworld, creates /proc/helloworld and
 * populates its data, read_proc, write_proc and owner fields. The exit
 * function, cleanup_helloworld, removes the /proc/helloworld entry.
 * The proc_read function, proc_read_helloworld, is called whenever
 * a file read operation occurs on /proc/helloworld. The
 * proc_write function, proc_write_helloworld, is called whenever a file
 * file write operation occurs on /proc/helloworld.
 *
 * To demonstrate read and write operations, this module uses data
 * structure called helloworld_data containing a char field called value.
 * Read and write operations on /proc/helloworld manipulate
 * helloworld_data->value. The init function sets value = 'Default'.
 */
/*
gcc -O2 -D__KERNEL__ -DMODULE -I/usr/src/linux/include \
    -c helloworld_proc_module.c -o helloworld_proc_module.o

arm-linux-gcc -O2 -D__KERNEL__ -DMODULE -I/usr/src/arm-linux/include \
    -c helloworld_proc_module.c \
    -o /tftpboot/arm-rootfs/helloworld_proc_module.o
*/


#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <asm/uaccess.h>

#define MODULE_VERSION "1.0"
#define MODULE_NAME "helloworld proc module"

/* this is how long our data->value char array can be */
#define HW_LEN 8

struct helloworld_data_t {
  char value[HW_LEN + 1];
};

static struct proc_dir_entry *helloworld_file;

struct helloworld_data_t helloworld_data;

/* proc_read - proc_read_helloworld
 * proc_read_helloworld is the callback function that the kernel calls when
 * there's a read file operation on the /proc file (for example,
 * cat /proc/helloworld). The file's data pointer (&helloworld_data) is
 * passed in the data parameter. You first cast it to the helloworld_data_t
 * structure. This proc_read function then uses the sprintf function to
 * create a string that is pointed to by the page pointer. The function then
 * returns the length of page. Because helloworld_data->value is set to
 * "Default", the command cat /proc/helloworld should return
 * helloworld Default
 */
static int proc_read_helloworld(char *page, char **start, off_t off,
                                int count, int *eof, void *data)
{
  int len;

/* cast the void pointer of data to helloworld_data_t*/
  struct helloworld_data_t *helloworld_data=(struct helloworld_data_t *)data;

/* use sprintf to fill the page array with a string */
  len = sprintf(page, "helloworld %s\n", helloworld_data->value);

  return len;
}


/* proc_write - proc_write_helloworld
 * proc_write_helloworld is the callback function that the kernel calls
 * when there's a write file operation on the /proc file, (for example,
 * echo test > /proc/helloworld). The file's data pointer
 * (&helloworld_data) is passed in the data parameter. You first cast it to
 * the helloworld_data_t structure. The buffer parameter points to the
 * incoming data. You use the copy_from_user function to copy the buffer
 * contents to the data->value field. Before you do that, though, you check
 * the buffer length, which is stored in count to ensure that you don't
 * overrun the length of data->value. This function then returns the length
 * of the data copied.
 */
static int proc_write_helloworld(struct file *file, const char *buffer,
                                 unsigned long count, void *data)
{
  int len;

/* cast the void pointer of data to helloworld_data_t*/
  struct helloworld_data_t *helloworld_data=(struct helloworld_data_t *)data;

/* do a range checking, don't overflow buffers in kernel modules */
  if(count > HW_LEN)
    len = HW_LEN;
  else
    len = count;
/* use the copy_from_user function to copy buffer data to
 * to our helloworld_data->value */
  if(copy_from_user(helloworld_data->value, buffer, len)) {
    return -EFAULT;
  }

/* zero terminate helloworld_data->value */
  helloworld_data->value[len] = '\0';

  return len;
}

/* init - init_helloworld
 * init_helloworld creates the /proc/helloworld entry file and obtains its
 * pointer called helloworld_file. The helloworld_file fields, data,
 * read_proc, write_proc and owner, are filled.  init_helloworld completes
 * by writing an entry to the system log using printk.
 */
static int __init init_helloworld(void)
{
  int rv = 0;

/* Create the proc entry and make it readable and writable by all - 0666 */
  helloworld_file = create_proc_entry("helloworld", 0666, NULL);
  if(helloworld_file == NULL) {
    return -ENOMEM;
  }

/* set the default value of our data to Sam. This way a read operation on
 * /proc/helloworld will return something. */
  strcpy(helloworld_data.value, "Default");

/* Set helloworld_file fields */
  helloworld_file->data = &helloworld_data;
  helloworld_file->read_proc = &proc_read_helloworld;
  helloworld_file->write_proc = &proc_write_helloworld;
  helloworld_file->owner = THIS_MODULE;

/* everything initialize */
  printk(KERN_INFO "%s %s initialized\n",MODULE_NAME, MODULE_VERSION);
  return 0;
}

/* exit - cleanup_helloworld
 * cleanup_helloworld removes the /proc file entry helloworld and
 * prints a message to the system log file.
 */
static void __exit cleanup_helloworld(void)
{
  remove_proc_entry("helloworld", NULL);
  printk(KERN_INFO "%s %s removed\n", MODULE_NAME, MODULE_VERSION);
}

/* here are the compiler macros for module operation */
module_init(init_helloworld);
module_exit(cleanup_helloworld);

MODULE_AUTHOR("Craig Hollabaugh");
MODULE_DESCRIPTION("helloworld proc module");

EXPORT_NO_SYMBOLS;

TIP
You should use the helloworld_proc_module.c source file as a skeleton for interfacing projects. It compiles, loads, and executes correctly on the x86, ARM, and PowerPC target boards. You can simply add hardware initialization code to the init function, interfacing code to the proc_read and proc_write functions, change the names of the /proc entries, and recompile.


Compiling, Inserting, and Testing helloworld_proc_module on the MediaEngine

When it is inserted into the kernel, the helloworld_proc_module creates a /proc directory entry that bash scripts can read from and write to. Here are the steps to compile the module using tbdev1, and then insert and test the operation of the helloworld_proc_module using tbdevarm, the MediaEngine:

  1. Compile helloworld_proc_module.c by using this command:
    root@tbdev1[526]: arm-linux-gcc -O2 -D__KERNEL__ -DMODULE -I/usr/src/arm-linux/include 
    graphics/ccc.gif-c helloworld_proc_module.c -o /tftpboot/arm-rootfs/helloworld_proc_module.o
    root@tbdev1[527]: ls -sh /tftpboot/arm-rootfs/helloworld_proc_module.o
    4.0k /tftpboot/arm-rootfs/helloworld_proc_module.o
    
  2. Boot tbdevarm to gain access using minicom:
    root@tbdev1[528]: minicom
    
    Here's the tbdevarm prompt from minicom:
    bash-2.04#
    
  3. Check for the helloworld_proc_module object file and to see what modules are loaded:
    bash-2.04# ls /
    bin  dev  etc  helloworld_proc_module.o  lib  proc  sbin  tmp  usr
    
    bash-2.04# cat /proc/modules
    bash-2.04#
    
  4. Insert helloworld_proc_module.o, list the current modules and check the /proc directory:
    bash-2.04# insmod helloworld_proc_module.o
    helloworld proc module 1.0 initialized
    
    bash-2.04# cat /proc/modules
    helloworld_proc_module    1056   0 (unused)
    
    bash-2.04# ls /proc
    1   bus          helloworld  locks       stat
    15  cmdline      ide         meminfo     swaps
    2   cpuinfo      interrupts  misc        sys
    28  devices      iomem       modules     sysvipc
    3   dma          ioports     mounts      tty
    4   driver       kcore       net         uptime
    5   execdomains  kmsg        partitions  version
    6   filesystems  ksyms       self
    7   fs           loadavg     slabinfo
    
    The module initialized, /proc/modules contains an entry for it, and the /proc/helloworld file exists.
  5. Check the read_proc function, proc_read_helloworld, by performing a read file operation on the /proc file entry, helloworld:
    bash-2.04# cat /proc/helloworld
    helloworld Default
    
  6. Check the write_proc function, proc_write_helloworld, by performing a write file operation on the /proc file entry, helloworld:
    bash-2.04# echo 1234 > /proc/helloworld
    bash-2.04# cat /proc/helloworld
    helloworld 1234
    
    bash-2.04#
    
    The string 1234 was successfully copied from the proc_write_helloworld function to the data->value field. (You verified that with a read file operation using cat /proc/helloworld.)
  7. Remove the module, check the module list in /proc/modules and /proc directory:
    bash-2.04# rmmod helloworld_proc_module
    helloworld proc module 1.0 removed
    bash-2.04# cat /proc/modules
    bash-2.04# ls /proc/
    1   bus          ide         meminfo     swaps
    15  cmdline      interrupts  misc        sys
    2   cpuinfo      iomem       modules     sysvipc
    3   devices      ioports     mounts      tty
    36  dma          kcore       net         uptime
    4   driver       kmsg        partitions  version
    5   execdomains  ksyms       self
    6   filesystems  loadavg     slabinfo
    7   fs           locks       stat
    bash-2.04#
    

You have just compiled, inserted, and tested helloworld_proc_module. This module dynamically creates a /proc directory entry called helloworld. You can read and write to helloworld, and it stores an eight-character value. This seems simple, but this powerful module is the skeleton that the Project Trailblazer engineers needed to create their lift monitoring and snow-making control device driver.

No comments: