Graeme Winter

Kernel Modules

One of the great things about the π is the ability to hack at the lowest levels, and break something which doesn’t matter. In addition to exploring bare metal on BCM2835, also started to look at writing kernel modules, since that is the usual MO for interfacing to hardware.

Spoiler: the kernel makes this harder, because its job is to stop you from messing with hardware. This is fine, but meant that the writing of a new kernel module was something of a learning curve. As such, work with something with which I am already familiar, GPCLK as per previous user space investigations.

Kernel Basics

In UNIX everything is a file, so if you want to expose a device, you create a file to do so. The file in this case lives under /dev, and wants to expose a clock divider register - I will assume that the clock has been set up as per the data sheet in advance (I used the user space program from before).

Device file needs to deal with four ideas:

These four functions are essentially what defines the minimal functionality exposed by a character device. The API for these (i.e. stubs) is well documented, but the behaviour expected from these is less well documented. That behaviour matters. You also need two other stub functions, to init and exit the kernel module (i.e. routines which are executed on load and unload).

Memory

Obviously for something like an ARM based SoC everything is accessed by specific memory addresses - in the most basic sense the job of the kernel is to map the machine hardware to a set of predictable locations in a process memory space, and to prevent one program from clobbering another. In our case, we want to access memory which is shared by other devices / programs / whatever so need to think about accessing the right space. In kernel terms, this involves mapping the memory from the bus address that the CPU sees to the right address for access from a kernel module: this is done with ioremap and iounmap - former to perform the mapping, latter to release it. The hardware address of the hardware depends on the specific revision of π - in my case, working on π0 for this where we have 512MB of RAM, the base address is one byte above i.e. #define GPCLK_ADDR 0x20101070. In the data sheet this is listed with prefix 0x7E.

The map process is used to make a chunk of memory available to the kernel address space - takes a void * and number of bytes, returns a void * which can be used to dereference those bytes. In the implementation I chose to do this in the open / release methods:

static void *clk_addr;

static int clk_open(struct inode *i, struct file *f) {
  printk("gpclk open\n");
  clk_addr = ioremap(GPCLK_ADDR, 2 * sizeof(unsigned int));
  return 0;
}

static int clk_release(struct inode *i, struct file *f) {
  printk("gpclk close\n");
  iounmap(clk_addr);
  return 0;
}

N.B. here clk_addr is simply a pointer to the start of the address space: any arithmatic on this will need to be done manually, unless it is cast to a uint32_t (which would seem a totally reasonable option). At this point, the clk_addr pointer can be treated from within the kernel code exactly as if it was just pointing at the registers (which it is).

The other memory-side matter here is that the data being read and written are from user space, while the kernel is obviously working in kernel space, and never the twain shall meet. Accordingly, some care is needed to copy the pertinent data from user to kernel space in the write, and vice versa in the read. This is achieved with:

error = copy_to_user(buf, msg, len);
error = copy_from_user(msg, buf, len);

where the error here is the number of bytes which were not read or written, i.e. the answer should be zero. I did not add a handler for non-zero… should do really.

Device File I/O

This is the “simple bit” except really it isn’t - the function specifications are not really written anywhere, and you effectively need to think of this as being on the other side of the UNIX read() and write() function calls: for example with reading it is up to you to generate the EOF signal.

Effectively we have the open and release calls defined above, then read and write. Read is for output to user space. Write is for input from user space. In this case cat /dev/gpclk (as will be my device name) is reading data out from the device, so we need to provide that data. The function signatures are the same:

static ssize_t clk_read(struct file *f, char __user *buf, size_t len, loff_t *off);
static ssize_t clk_write(struct file *f, const char __user *buf, size_t len, loff_t *off);

In each case we return the number of bytes read / written, as well as advancing the file pointer. EOF is indicated by resetting the file pointer (I don’t know if this was needed?) and returning zero, so every read will be performed by at least two calls to clk_read(), to get the end of file indicator. Beyond this it is a sprintf / sscanf to get the values into the buffer, and the copy command as posed above.

Combination / Registration

To actually build a kernel module from these functions a struct is needed, with members pointing to these functions. This struct is then registered with the kernel, and a device name defined: this is implemented in an init function, executed on module load, with the corresponding deinit executed when the module is unloaded.

// open, close, read, write
static int clk_open(struct inode *i, struct file *f);
static int clk_release(struct inode *i, struct file *f);
static ssize_t clk_read(struct file *f, char __user *buf, size_t len, loff_t *off);
static ssize_t clk_write(struct file *f, const char *buf, size_t len, loff_t *off);

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = clk_read,
    .write = clk_write,
    .open = clk_open,
    .release = clk_release,
};

As the init function performs a sequence of tasks which could be aborted, you are left with the choice of either goto or nested if statements: this was a case where to my mind goto serves the problem well.

static int __init clk_driver_init(void) {

  if ((alloc_chrdev_region(&dev, 0, 1, "gpclk")) < 0) {
    goto r_unreg;
  }

  cdev_init(&clk_cdev, &fops);

  if ((cdev_add(&clk_cdev, dev, 1)) < 0) {
    goto r_del;
  }

  if (IS_ERR(clk_class = class_create(THIS_MODULE, "gpclk"))) {
    goto r_class;
  }

  if (IS_ERR(device_create(clk_class, NULL, dev, NULL, "gpclk"))) {
    goto r_device;
  }

  return 0;

r_device:
  device_destroy(clk_class, dev);
r_class:
  class_destroy(clk_class);
r_del:
  cdev_del(&clk_cdev);
r_unreg:
  unregister_chrdev_region(dev, 1);

  return -1;
}

static void __exit clk_driver_exit(void) {
  device_destroy(clk_class, dev);
  class_destroy(clk_class);
  cdev_del(&clk_cdev);
  unregister_chrdev_region(dev, 1);
}

These are then linked with:

module_init(clk_driver_init);
module_exit(clk_driver_exit);

Making

The Makefile for this could be very complex, finding the right location for the kernel sources etc. - instead most of it is done by a spell in the kernel source code installation:

obj-m += gpclk.o

KDIR = /lib/modules/$(shell uname -r)/build
 
 
all:
	make -C $(KDIR)  M=$(shell pwd) modules
 
clean:
	make -C $(KDIR)  M=$(shell pwd) clean

Full Sources

Available here - though some implicit dependency on the user-space tool here - which could do with a Makefile… I could add this to the kernel module itself trivially, by mapping the GPIO registers to kernel space, but the learning exercise here was already complete.

Usage

sudo -s
cat /dev/gpclk
echo 500 > /dev/gpclk

etc.