Getting started with Embedded Linux: Part Seven

March 25, 2014

Eager-March 25, 2014

This series on how to get started using Embedded Linux is on my Open Mike blog. This installment discusses creating a character device driver.

In the previous installment, we talked about Linux Kernel Modules (LKM) in general and wrote a minimal LKM. The most common use of LKM is to create a driver to support new hardware or to create a virtual device. The simplest virtual device is /dev/null, which discards all data written to it, and returns an immediate EOF when read. You can find the source for the /dev/null driver in linux/drivers/char/mem.c.

We're going to write a driver which creates a virtual device which will return a pseudo-random number when read. This is a similar to the /dev/random virtual device, but simpler, and not to be used in real programs.

Here's the basic framework for our driver, named myrandom.c:

/*  Random number virtual device. */
#include <linux/modules.h>

#define DRIVER_AUTHOR "Michael J. Eager <eager@eagercon.com>"
#define DRIVER_DESC   "random number generator device"

/* Declare license and provide authorship and description. */
MODULE_LICENSE("GPL");
MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);

/* Declare all functions. */
static int init_myrandom(void);
static void cleanup_myrandom(void);

static int init_myrandom(void)
{
  printk(KERN_ALERT "myrandom init\n");

  return 0;
}

static void cleanup_myrandom(void)
{
  printk(KERN_ALERT "myrandom stopped\n");
}

module_init(init_myrandom);
module_exit(cleanup_myrandom); 

This looks a lot like the simple LKM we created in the previous installment. All we need to change in the Makefile is replace lkm.o with myrandom.o.

We've added a MODULE_LICENSE macro, saying that this driver is licensed under the GPL, as well as a macro listing the author and a description. These will be displayed when you run modinfo on the myrandom.ko file. We also added two macros at the end, module_init and module_exit, which allow us to specify the name of the init and cleanup routines, rather than using the defaults. When you run insmod myrandom.ko (as root) and tail /var/log/messages, you will see the messages issued by the driver when it is loaded. Running rmmod myrandom will remove the driver, and add the stopped message to the system log.

Creating the device
Next, let's tell the kernel that we have support for our myrandom device:

Toward the top of the file, add:

#include <linux/fs.h>

And add some data structures:

static int Major;
static struct file_operations fops = {
};

Update the init and cleanup routines as shown below:

static int init_myrandom(void)
{
  printk(KERN_ALERT "myrandom init\n");


  Major = register_chrdev(0, "myrandom", &fops);
  if (Major < 0) {
    printk(KERN_ALERT "Registering myrandom device failed: %d\n",
	      Major);
    return Major;
  }

  printk("Myrandom assigned major number %d\n", Major);

  return 0;
}

static void cleanup_myrandom(void)
{
  /* Unregister myrandom device. */
  unregister_chrdev(Major, "myrandom");
  printk(KERN_ALERT "myrandom stopped\n");
}

When we load this driver using insmod, it will register a character device driver. Devices are represented as special files under /dev which have a major device number indicating a device class, and a minor device number indicating the specific device in that class. If you run cat /proc/devices you will see the list of device classes defined in your system, including the device number for myrandom. When we call register_chrdev, with the first argument of zero, we ask the kernel to assign an available major device number. Alternately, you can specify one which is not otherwise used.

Registering a device driver does not create the device file under /dev. There are a few ways to do this. We are going to use the simplest method and run the mknod command as root, and chmod to make it accessible by all users:

# mknod /dev/myrandom c 249 0
# chmod 0666 /dev/myrandom
# ls -l /dev/myrandom
crw-rw-rw- 1 root root 249, 0 Mar 25 08:00 /dev/myrandom

The kernel assigned the my driver the major device number of 249; your number might be different.

Other ways to create the device file is to call the kernel's mknod() function when we register the driver, which will do the same as the mknod command. A more elegant and flexible method is to use udev. Udev is a user-space daemon which is notified by the kernel when a new device is registered. It then uses rules in /etc/udev/rules.d to create device nodes.

Adding device functions
We want our device driver to support the open, close, and read operations, and we want a write to our device to return an error. Let's define the routines which will do these operations:

static int open_myrandom(struct inode *, struct file *);
static int close_myrandom(struct inode *, struct file *);
static ssize_t read_myrandom(struct file *, char *, size_t, loff_t *);
static ssize_t write_myrandom(struct file *, const char *, size_t, loff_t *);

static int Major;
static struct file_operations fops = {
  .read = read_myrandom,
  .release = close_myrandom,
  .open = open_myrandom,
  .write = write_myrandom
};

static int myrandom_in_use = 0;

static int open_myrandom(struct inode *inode, struct file *file)
{
  if (myrandom_in_use)
    return -EBUSY;
  myrandom_in_use++;
  return 0;
}

static int close_myrandom(struct inode *inode, struct file *file)
{
  if (myrandom_in_use)
    myrandom_in_use--;
  return 0;
}

static ssize_t read_myrandom(struct file *filp, char *buf, size_t len, loff_t *ofs)
{
  return 0;
}

static ssize_t write_myrandom(struct file *filp, const char *buf, size_t len, loff_t *ofs)
{
  return 0;
}

The open routine increments an internal flag so that only one program can open the device at a time. A second program trying to open the device will receive a busy error. The close routine resets this flag. For the moment, the read and write routines are dummies which do nothing.

If you insmod the driver, you can use the dd command to open the device and read from it:

$ dd if=/dev/myrandom
0+0 records in
0+0 records out
0 bytes (0 B) copied, 4.5739e-05 s, 0.0 kB/s

The dd command opened the device and immediately received an EOF.

Transferring data
Let's complete this example driver by returning some data when we read the device:

Near the top of the file, add:

#include <asm/uaccess.h>
static unsigned char myrandom(void);

Let's create a simple random character generator which return a random letter or number each time it is called:

/* Generate random letters and numbers.  Algorithm from Wikipedia. */
static unsigned char myrandom(void)
{
  static char letters[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  static unsigned int m_w = 0x12345678;
  static unsigned int m_z = 0x87654321;
  int myrand;

  m_z = 36969 * (m_z & 65535) + (m_z >> 16);
  m_w = 18000 * (m_w & 65535) + (m_w >> 16);
  
  myrand = (m_z << 16) + m_w;
  myrand = (myrand >> 16) % sizeof(letters);
  
  return letters[myrand];
}

Let's flesh out the read routine:

static ssize_t read_myrandom(struct file *filp, char *buf, size_t len, loff_t *ofs)
{
  unsigned char rand_val;
  int count = 0;

  /* Return EOF when all bytes read. */
  if (bytes_read == 100) 
    return 0;

  while (len-- > 0 && bytes_read++ < 100) {
    rand_val = myrandom();
    put_user(rand_val, buf++);
    count++;
  }
  /* Return number of bytes transferred. */
  return count;
}

Finally, in the open routine, let's zero out the count whenever the device is opened:

static int open_myrandom(struct inode *inode, struct file *file)
{
  if (myrandom_in_use)
    return -EBUSY;
  myrandom_in_use++;
  bytes_read = 0;
  return 0;
}


When we read /dev/myrandom, we will receive the number of random characters we requested, up to a limit of 100. When we write to the device, we get an immediate device full message.

$ dd if=/dev/myrandom bs=10 count=1
QFF5tmwf3i1+0 records in
1+0 records out
10 bytes (10 B) copied, 0.000252246 s, 39.6 kB/s
$ dd if=/dev/myrandom
e9LDb6se6jJZnrS6prxpbkyTwIaaTlU1YDaz7buUtbvDXw1hxSgImzTc84zF28SZqUtS6tfRO8kl1iQCXEXSGjOTftygRqzV0+1 records in
0+1 records out
100 bytes (100 B) copied, 0.000145579 s, 687 kB/s
$ dd if=/dev/zero of=/dev/myrandom
dd: writing to /dev/myrandom: No space left on device
1+0 records in
0+0 records out
0 bytes (0 B) copied, 0.000461547 s, 0.0 kB/s

Note that since the string returned from /dev/myrandom is not zero terminated, the message from dd is appended to the output.

Click on myrandom.c to see the full code, available for download.

Last comments about drivers
One topic we didn't discuss is passing parameters to a driver. This can be done using modprobe, either directly or by using /etc/modprobe.conf. In our example, we might have specified the random number seed. A real driver would perhaps have device addresses or options specified as parameters.

We've only touched on the basics of writing a device driver and none of the hardware related issues. I recommend Linux Device Drivers by Alessandro Rubini, Jon Corbet and Greg Kroah-Hartman, published by O'Reilly. The 4th edition should be out in July, adding Jessica McKeller to the list of authors.

Michael Eager is principal consultant at Eager Consulting in Palo Alto, Calif. He has over four decades experience developing compilers, debuggers, and simulators for a wide range of processor architectures used in embedded systems. His current and former clients include major semiconductor companies and systems developers. Michael has been a member of the ISO C++ Standard Committee and ABI Committees for several processor architectures. He is chair of the Debugging Standards Committee for DWARF, a widely used debug data format. He is active in the open-source and Linux communities.

Related links:

Loading comments...

Parts Search Datasheets.com

KNOWLEDGE CENTER