How to write a simple Linux device driver?

CLinuxLinux KernelLinux Device-DriverEmbedded Linux

C Problem Overview


I need to write an SPI Linux character device driver for omap4 from scratch. I know some basics of writing device drivers. But, I don't know how to start writing platform specific device driver from scratch.

I've written some basic char drivers, and I thought writing SPI device driver would be similar to it. Char drivers have a structure file_operations which contains the functions implemented in the driver.

struct file_operations Fops = {
	.read = device_read,
	.write = device_write,
	.ioctl = device_ioctl,
	.open = device_open,
	.release = device_release,	/* a.k.a. close */
};

Now, I am going through spi-omap2-mcspi.c code as a reference to get an idea to start developing SPI driver from scratch.

But, I don't see functions such as open, read, write etc. Don't know from where the program starts.

C Solutions


Solution 1 - C

First, start by writing a generic kernel module. There are multiple places to look up for information but I found this link to be very useful. After you have gone through all examples specified there you can start writing your own Linux Driver Module.

Please note, that you will not get away with just copy-pasting the example code and hope it will work, no. Kernel API can sometimes change and examples will not work. Examples provided there should be looked at as a guide on how to do something. Depending on the kernel version you are using you have to modify the example in order to work.

Consider using TI platform-provided functions as much as you can, because that can really do a lot of work for you, like requesting and enabling needed clocks, buses, and power supplies. If I recall correctly you can use the functions to acquire memory-mapped address ranges for direct access to registers. I have to mention that I have a bad experience with TI-provided functions because they do not properly release/clean up all acquired resources, so for some resources, I had to call other kernel services to release them during module unload.

Edit 1:

I'm not entirely familiar with Linux SPI implementation but I would start by looking at omap2_mcspi_probe() function in drivers/spi/spi-omap2-mcspi.c file. As you can see there, it registers it's methods to Linux master SPI driver using this API: Linux/include/linux/spi/spi.h. In contrast to char driver, the main functions here are *_transfer() functions. Look up the struct descriptions in spi.h file for further details. Also, have a look at this alternative device driver API, too.

Solution 2 - C

I assume your OMAP4 linux uses one of arch/arm/boot/dts/{omap4.dtsi,am33xx.dtsi} device-tree, thus it compiles drivers/spi/spi-omap2-mcspi.c (if you don't know about device-tree, read this). Then:

  • the SPI master driver is done,
  • it (most probably) registers with Linux SPI core framework drivers/spi/spi.c,
  • it (probably) works fine on your OMAP4.

You actually don't need to care about the master driver to write your slave device driver. How do I know spi-omap2-mcspi.c is a master driver? It calls spi_register_master().

SPI master, SPI slave ?

Please refer to Documentation/spi/spi_summary. The doc refers to Controller driver (master) and Protocol drivers (slave). From your description, I understand you want to write a Protocol/Device driver.

SPI protocol ?

To understand that, you need your slave device datasheet, it shall tell you:

  • the SPI mode understood by your device,
  • the protocol it expects on the bus.

Contrary to i2c, SPI does not define a protocol or handshake, SPI chips manufacturers have to define their own. So check the datasheet.

SPI mode

From include/linux/spi/spi.h:

  • @mode: The spi mode defines how data is clocked out and in.
  • This may be changed by the device's driver.
  • The "active low" default for chipselect mode can be overridden
  • (by specifying SPI_CS_HIGH) as can the "MSB first" default for
  • each word in a transfer (by specifying SPI_LSB_FIRST).
Again, check your SPI device datasheet.

An example SPI device driver?

To give you a relevant example, I need to know your SPI device type. You would understand that a SPI flash device driver is different from a SPI FPGA device driver. Unfortunately there are not so many SPI device drivers out there. To find them:

$ cd linux 
$ git grep "spi_new_device\|spi_add_device"

Solution 3 - C

I don't know if I understood your question correctly. As m-ric pointed out, there are master drivers and slave drivers.

Usually master drivers are more hardware bound, I mean, they usually manipulate IO registers or do some memory mapped IO.

For some architectures already supported by linux kernel (like omap3 and omap4) master drivers are already implemented (McSPI).

So I assume you want to USE those SPI facilities of omap4 to implement a slave device driver (your protocol, to communicate with your external device through SPI).

I've written the following example for BeagleBoard-xM (omap3). The full code is at https://github.com/rslemos/itrigue/blob/master/alsadriver/itrigue.c (worth a view, but have more initialisation code, for ALSA, GPIO, module parameters). I've tried to set apart code that deals with SPI (maybe I forgot something, but anyway you should get the idea):

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/spi/spi.h>

/* MODULE PARAMETERS */
static uint spi_bus = 4;
static uint spi_cs = 0;
static uint spi_speed_hz = 1500000;
static uint spi_bits_per_word = 16;

/* THIS IS WHERE YOUR DEVICE IS CREATED; THROUGH THIS YOU INTERACT WITH YOUR EXTERNAL DEVICE */
static struct spi_device *spi_device;


/* SETUP SPI */

static inline __init int spi_init(void) {
	struct spi_board_info spi_device_info = {
		.modalias = "module name",
		.max_speed_hz = spi_speed_hz,
		.bus_num = spi_bus,
		.chip_select = spi_cs,
		.mode = 0,
	};

	struct spi_master *master;

	int ret;

	// get the master device, given SPI the bus number
	master = spi_busnum_to_master( spi_device_info.bus_num );
	if( !master )
		return -ENODEV;

	// create a new slave device, given the master and device info
	spi_device = spi_new_device( master, &spi_device_info );
	if( !spi_device )
		return -ENODEV;

	spi_device->bits_per_word = spi_bits_per_word;

	ret = spi_setup( spi_device );
	if( ret )
		spi_unregister_device( spi_device );

	return ret;
}

static inline void spi_exit(void) {
	spi_unregister_device( spi_device );
}

To write data to your device:

spi_write( spi_device, &write_data, sizeof write_data );

The above code is independent of implementation, that is, it could use McSPI, bit-banged GPIO, or any other implementation of an SPI master device. This interface is described in linux/spi/spi.h

To make it work in BeagleBoard-XM I had to add the following to the kernel command line:

omap_mux=mcbsp1_clkr.mcspi4_clk=0x0000,mcbsp1_dx.mcspi4_simo=0x0000,mcbsp1_dr.mcspi4_somi=0x0118,mcbsp1_fsx.mcspi4_cs0=0x0000

So that an McSPI master device is created for omap3 McSPI4 hardware facility.

Hope that helps.

Solution 4 - C

file_operations minimal runnable example

This example does not interact with any hardware, but it illustrates the simpler file_operations kernel API with debugfs.

Kernel module fops.c:

#include <asm/uaccess.h> /* copy_from_user, copy_to_user */
#include <linux/debugfs.h>
#include <linux/errno.h> /* EFAULT */
#include <linux/fs.h> /* file_operations */
#include <linux/kernel.h> /* min */
#include <linux/module.h>
#include <linux/printk.h> /* printk */
#include <uapi/linux/stat.h> /* S_IRUSR */

static struct dentry *debugfs_file;
static char data[] = {'a', 'b', 'c', 'd'};

static int open(struct inode *inode, struct file *filp)
{
	pr_info("open\n");
	return 0;
}

/* @param[in,out] off: gives the initial position into the buffer.
 *      We must increment this by the ammount of bytes read.
 *      Then when userland reads the same file descriptor again,
 *      we start from that point instead.
 * */
static ssize_t read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
	ssize_t ret;

	pr_info("read\n");
	pr_info("len = %zu\n", len);
	pr_info("off = %lld\n", (long long)*off);
	if (sizeof(data) <= *off) {
		ret = 0;
	} else {
		ret = min(len, sizeof(data) - (size_t)*off);
		if (copy_to_user(buf, data + *off, ret)) {
			ret = -EFAULT;
		} else {
			*off += ret;
		}
	}
	pr_info("buf = %.*s\n", (int)len, buf);
	pr_info("ret = %lld\n", (long long)ret);
	return ret;
}

/* Similar to read, but with one notable difference:
 * we must return ENOSPC if the user tries to write more
 * than the size of our buffer. Otherwise, Bash > just
 * keeps trying to write to it infinitely. */
static ssize_t write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
	ssize_t ret;

	pr_info("write\n");
	pr_info("len = %zu\n", len);
	pr_info("off = %lld\n", (long long)*off);
	if (sizeof(data) <= *off) {
		ret = 0;
	} else {
		if (sizeof(data) - (size_t)*off < len) {
			ret = -ENOSPC;
		} else {
			if (copy_from_user(data + *off, buf, len)) {
				ret = -EFAULT;
			} else {
				ret = len;
				pr_info("buf = %.*s\n", (int)len, data + *off);
				*off += ret;
			}
		}
	}
	pr_info("ret = %lld\n", (long long)ret);
	return ret;
}

/*
Called on the last close:
http://stackoverflow.com/questions/11393674/why-is-the-close-function-is-called-release-in-struct-file-operations-in-the-l
*/
static int release(struct inode *inode, struct file *filp)
{
	pr_info("release\n");
	return 0;
}

static loff_t llseek(struct file *filp, loff_t off, int whence)
{
	loff_t newpos;

	pr_info("llseek\n");
	pr_info("off = %lld\n", (long long)off);
	pr_info("whence = %lld\n", (long long)whence);
	switch(whence) {
		case SEEK_SET:
			newpos = off;
			break;
		case SEEK_CUR:
			newpos = filp->f_pos + off;
			break;
		case SEEK_END:
			newpos = sizeof(data) + off;
			break;
		default:
			return -EINVAL;
	}
	if (newpos < 0) return -EINVAL;
	filp->f_pos = newpos;
	pr_info("newpos = %lld\n", (long long)newpos);
	return newpos;
}

static const struct file_operations fops = {
	/* Prevents rmmod while fops are running.
	 * Try removing this for poll, which waits a lot. */
	.owner = THIS_MODULE,
	.llseek = llseek,
	.open = open,
	.read = read,
	.release = release,
	.write = write,
};

static int myinit(void)
{
	debugfs_file = debugfs_create_file("lkmc_fops", S_IRUSR | S_IWUSR, NULL, NULL, &fops);
	return 0;
}

static void myexit(void)
{
	debugfs_remove_recursive(debugfs_file);
}

module_init(myinit)
module_exit(myexit)
MODULE_LICENSE("GPL");

Userland shell test program:

#!/bin/sh

mount -t debugfs none /sys/kernel/debug

insmod /fops.ko
cd /sys/kernel/debug/lkmc_fops

## Basic read.
cat f
# => abcd
# dmesg => open
# dmesg => read
# dmesg => len = [0-9]+
# dmesg => close

## Basic write

printf '01' >f
# dmesg => open
# dmesg => write
# dmesg => len = 1
# dmesg => buf = a
# dmesg => close

cat f
# => 01cd
# dmesg => open
# dmesg => read
# dmesg => len = [0-9]+
# dmesg => close

## ENOSPC
printf '1234' >f
printf '12345' >f
echo "$?"
# => 8
cat f
# => 1234

## seek
printf '1234' >f
printf 'z' | dd bs=1 of=f seek=2
cat f
# => 12z4

You should also write a C program that runs those tests if it is not clear to you what system calls are being called for each of those commands. (or you could also use strace and find out :-)).

The other file_operations are a bit more involved, here are some further examples:

Start with software models of simplified hardware in emulators

Actual device hardware development is "hard" because:

  • you can't always get your hand on a given hardware easily
  • hardware APIs may be complicated
  • it is hard to see what is the internal state of the hardware

Emulators like QEMU allow us to overcome all those difficulties, by simulating simplified hardware simulation in software.

QEMU for example, has a built-in educational PCI device called edu, which I explained further at: https://stackoverflow.com/questions/28315265/how-to-add-a-new-device-in-qemu-source-code/44612957#44612957 and is a good way to get started with device drivers. I've made a simple driver for it available here.

You can then put printf's or use GDB on QEMU just as for any other program, and see exactly what is going on.

There is also an OPAM SPI model for you specific use case: https://github.com/qemu/qemu/blob/v2.7.0/hw/ssi/omap_spi.c

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSagar JainView Question on Stackoverflow
Solution 1 - CNenad RadulovicView Answer on Stackoverflow
Solution 2 - Cm-ricView Answer on Stackoverflow
Solution 3 - CrslemosView Answer on Stackoverflow
Solution 4 - CCiro Santilli Путлер Капут 六四事View Answer on Stackoverflow