Skip to content
Digital Rhyme

Serious Linux Driver Example

A detailed platform-driver walkthrough for seasoned programmers: lifecycle, matching, module loading, probe/remove, misc-device file operations, ioctl ABI design, userspace access, and the build/deploy mechanics around modprobe.

Embedded Linux board with oscilloscope probe and driver architecture flow blocks

Driver Shape

The example is a platform driver named drhyme-sensor. It binds through the Linux driver model, creates a per-device miscdevice, exposes a character-device node such as /dev/drhyme_sensor0, and implements a small ioctl ABI. It is deliberately more realistic than a pure module_init toy while still being compact enough to read in one sitting.

In real hardware, the platform device usually comes from Device Tree, ACPI, or board setup code. For lab testing, this module has create_demo_device=1 by default, so the module registers a synthetic platform_device and then binds its own platform_driver.

This is example kernel code. Build and load it only on a disposable VM or development machine. A bug in kernel space is a system bug.

Example Files

The split is intentional: the driver includes a small UAPI header shared with the userspace test program. That mirrors the way real drivers keep ioctl command numbers and userspace-visible structs stable and explicit.

Lifecycle

The important call chain in the demo path is:

insmod/modprobe drhyme_sensor
  -> kernel loads drhyme_sensor.ko
  -> module_init(drhyme_init)
  -> platform_driver_register(&drhyme_driver)
  -> platform_device_register_simple("drhyme-sensor", ...)
  -> platform bus matches device name/id table
  -> drhyme_probe(pdev)
  -> misc_register(&sensor->miscdev)
  -> userspace sees /dev/drhyme_sensor0

The teardown path reverses ownership:

rmmod/modprobe -r drhyme_sensor
  -> module_exit(drhyme_exit)
  -> platform_device_unregister(demo_pdev)
  -> drhyme_remove(pdev)
  -> misc_deregister(&sensor->miscdev)
  -> platform_driver_unregister(&drhyme_driver)

In a real Device Tree boot, the device would often exist before the module is loaded. Then platform_driver_register is enough to cause the platform bus to walk unbound devices and call probe for a match.

Probe

probe is the point where the abstract driver meets one concrete device. The callback should allocate per-device state, map resources, request IRQs, initialize locks and runtime state, read firmware properties, register userspace interfaces, and attach state with platform_set_drvdata.

static int drhyme_probe(struct platform_device *pdev)
{
	struct drhyme_sensor *sensor;
	u32 default_config = 0;
	int ret;

	sensor = devm_kzalloc(&pdev->dev, sizeof(*sensor), GFP_KERNEL);
	if (!sensor)
		return -ENOMEM;

	sensor->dev = &pdev->dev;
	mutex_init(&sensor->lock);
	device_property_read_u32(&pdev->dev,
				 "digital-rhyme,default-config",
				 &default_config);

	sensor->miscdev.minor = MISC_DYNAMIC_MINOR;
	sensor->miscdev.name = sensor->misc_name;
	sensor->miscdev.fops = &drhyme_fops;
	sensor->miscdev.parent = &pdev->dev;

	ret = misc_register(&sensor->miscdev);
	if (ret)
		return dev_err_probe(&pdev->dev, ret,
				     "failed to register misc device\n");

	platform_set_drvdata(pdev, sensor);
	return 0;
}

The example uses devm_kzalloc, so the memory lifetime is tied to the device. It still explicitly deregisters the misc device in remove because that registration creates a userspace-visible object that must disappear before the driver state goes away.

Ioctls

Ioctls are an ABI, not just an internal command dispatch table. Once shipped, command numbers and struct layouts become compatibility promises. This example uses the standard ioctl macros:

CommandDirectionPurpose
DRHYME_IOCTL_RESET_IONo pointer argument; reset driver state.
DRHYME_IOCTL_GET_STATUS_IORCopy struct drhyme_sensor_status to userspace.
DRHYME_IOCTL_SET_CONFIG_IOWCopy struct drhyme_sensor_config from userspace.
static long drhyme_ioctl(struct file *file, unsigned int cmd,
			 unsigned long arg)
{
	if (_IOC_TYPE(cmd) != DRHYME_IOCTL_MAGIC)
		return -ENOTTY;

	switch (cmd) {
	case DRHYME_IOCTL_GET_STATUS:
		memset(&status, 0, sizeof(status));
		/* fill status under lock */
		if (copy_to_user((void __user *)arg, &status, sizeof(status)))
			return -EFAULT;
		return 0;
	default:
		return -ENOTTY;
	}
}
  • Return -ENOTTY for unknown commands.
  • Use fixed-width UAPI types such as __u32 and __u64.
  • Initialize structs before copying them to userspace to avoid leaking padding.
  • Use copy_from_user and copy_to_user; never trust userspace pointers.
  • For 32-bit userspace on a 64-bit kernel, add .compat_ioctl where pointer layout permits it.

modprobe And The Load Mechanism

modprobe is a userspace loader from kmod. It does not directly call your driver's probe. It resolves module names and aliases, loads dependencies, asks the kernel to load the .ko, and then the kernel runs the module's init function.

Manual load by module name:

sudo modprobe drhyme_sensor

Automatic load by modalias is the more interesting path:

kernel discovers device
  -> emits modalias through sysfs/uevent
  -> udev asks kmod/modprobe to satisfy that alias
  -> depmod-generated modules.alias maps alias to drhyme_sensor.ko
  -> module is loaded
  -> module_init registers platform_driver
  -> bus matching calls drhyme_probe

The example provides both MODULE_DEVICE_TABLE(of, ...) and MODULE_DEVICE_TABLE(platform, ...). Those macros place alias metadata in the module. After installation, depmod reads that metadata and updates files such as modules.alias.

sudo make modules_install
sudo depmod -a
modinfo drhyme_sensor
modprobe drhyme_sensor

If a matching Device Tree node exists, such as compatible = "digital-rhyme,drhyme-sensor", the kernel's device model can cause the module to be requested by alias. If you load by hand with insmod, dependency and alias handling are bypassed.

Build System

External modules should build through kbuild. The kernel build system injects the correct include paths, compiler flags, generated headers, symbol versioning, and module post-processing.

ifneq ($(KERNELRELEASE),)
obj-m += drhyme_sensor.o
else
KDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all: modules user

modules:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

user: drhyme_ioctl_test
	$(CC) -Wall -Wextra -O2 -o drhyme_ioctl_test drhyme_ioctl_test.c
endif

The wrapper Makefile is evaluated twice: first by normal make, then by kbuild with KERNELRELEASE set. The obj-m line is what kbuild consumes to produce drhyme_sensor.ko.

Build And Run

sudo apt install build-essential linux-headers-$(uname -r)
cd examples/serious_platform_driver
make
sudo insmod drhyme_sensor.ko
dmesg | tail
cat /dev/drhyme_sensor0
./drhyme_ioctl_test /dev/drhyme_sensor0
sudo rmmod drhyme_sensor
make clean

To test the installed/modprobe path:

sudo make modules_install
sudo depmod -a
sudo modprobe drhyme_sensor
sudo modprobe -r drhyme_sensor
For real hardware, disable the synthetic device with create_demo_device=0 and let firmware/device enumeration create the platform device that triggers matching and probe.