Writing Linux drivers: when you need the kernel – and when you don't
Before you write a kernel driver: which devices can be driven cleanly from user space, when a kernel driver becomes unavoidable, how a platform driver is structured, and which mistakes cost the most time during bring-up.
In short: The most expensive decision in driver development is made before any code exists – whether a kernel driver is needed at all. A substantial share of the devices in embedded projects can be driven cleanly from user space, with far less effort across the product’s lifetime. When the kernel really is the right place, the reasons are clear: latency, DMA, concurrent access, or a subsystem that provides a standard interface.
First question: can user space do it?
Linux offers generic interfaces for the common buses, letting an application talk to hardware directly:
spidevexposes an SPI chip select as a device node. Transfers go throughioctl, including clock rate and word size.i2c-devdoes the same for I2C buses; reads and writes go throughioctl(I2C_RDWR)or the SMBus helpers.- libgpiod drives GPIOs through the character device interface. The old sysfs interface under
/sys/class/gpiois deprecated and should not appear in new projects. - IIO covers sensors that already have a kernel driver – values arrive through sysfs or a buffered character device channel without you writing anything.
The advantage is not only the smaller effort. User space code can be tested without rebooting the kernel, crashes on its own rather than taking the system with it, and survives a kernel version jump unchanged. For a temperature sensor read once a second, that is the right answer.
When the kernel becomes unavoidable
Four reasons carry the decision for a kernel driver:
Interrupt latency. If an event must be answered within a few microseconds, there is no way around the kernel. The detour through a user space process costs context switches and is subject to the scheduler – even on a system running PREEMPT_RT, a handler in the kernel remains the shorter path.
DMA. As soon as larger amounts of data move without CPU involvement, you need coherent buffers, correct cache handling and an anchor in the DMA framework. That is kernel territory.
Concurrent access. When several processes use the same device, something has to serialise them. A kernel driver does that in exactly one place; in user space it ends in a home-grown locking protocol that every participant has to honour.
Subsystem integration. A device meant to appear as a network interface, input device, video or sensor source has to speak the language of that subsystem. The payoff is considerable: a sensor in the IIO framework works with existing tools, without anyone having to learn your proprietary interface.
Choosing the right subsystem
A bespoke character driver with invented ioctl numbers is almost always the worse choice. Most device classes have a subsystem that does half the work and in return prescribes an interface others already know: IIO for sensors and converters, input for buttons, encoders and touch, V4L2 for image sources, hwmon for temperature and voltage monitoring, PWM, RTC and watchdog for the respective classics.
The price is reading up on the subsystem’s conventions. The return is an interface that fits standard tooling, is documented, and does not need re-explaining when the next engineer takes over.
How a platform driver is structured
Devices soldered to the board that cannot announce themselves are described in the device tree and driven by a platform_driver. The skeleton is compact:
static int my_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct my_priv *priv;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
priv->clk = devm_clk_get_enabled(dev, NULL);
if (IS_ERR(priv->clk))
return dev_err_probe(dev, PTR_ERR(priv->clk), "clock missing\n");
platform_set_drvdata(pdev, priv);
return 0;
}
static const struct of_device_id my_of_match[] = {
{ .compatible = "acme,widget-1" },
{ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
static struct platform_driver my_driver = {
.probe = my_probe,
.driver = {
.name = "acme-widget",
.of_match_table = my_of_match,
},
};
module_platform_driver(my_driver);
Three details matter more than they look. The of_match_table connects the driver to the compatible string in the device tree – without an exact match, nothing happens at all. The devm_ variants tie resources to the device’s lifetime, removing cleanup paths that experience shows are wrong precisely when they are needed. And dev_err_probe handles the common case of a resource not being ready yet: it logs only real errors and keeps the log clean on -EPROBE_DEFER, while the kernel retries later.
Mistakes that cost the most time during bring-up
Blocking calls in interrupt context. A handler must not sleep. I2C transfers, allocations with GFP_KERNEL or mutexes belong in the threaded part – devm_request_threaded_irq with a short hard handler and a thread for the rest is the usual pattern.
Missing resources in the device tree. A driver that does not probe is rarely a driver problem. Usually a clock, regulator or pinctrl entry is missing, or the node is still disabled.
Treating -EPROBE_DEFER as an error. A driver loading before its dependency is not a defect, it is the normal case. Treating that return value as a real failure produces devices that intermittently fail to come up, with behaviour that shifts on every change to boot order.
The maintenance cost out-of-tree. Internal kernel APIs change without regard for external modules. A driver that stops building after a kernel update is expected – the only question is whether that effort was planned for. Keeping the driver versioned properly in your Yocto layer and carrying it forward is a deliberate choice; leaving it as a patch inside the kernel tree means losing it at the next jump.
How we approach it
In projects the work almost always starts with the same inventory: which parts sit on the board, which of them the kernel already handles, and which genuinely need custom code. Often less remains than expected – a sensor with an existing IIO module, and an FPGA interface that cannot do without DMA.
That separation is the actual engineering. It determines how much code you have to carry through kernel versions over the coming years. If you are facing that question, we are happy to look at it with you – as part of our Embedded Linux development or as a training for your team, where exactly this trade-off is practised on your hardware.
Frequently asked questions
- Does every custom device need a kernel driver?
- No, and that is the most important question to settle before the first line of code. An SPI or I2C part your application reads occasionally can be driven straight from user space through `spidev` or `i2c-dev`; GPIOs go through libgpiod. A kernel driver pays off when interrupts must be serviced with hard latency requirements, when DMA is involved, when several processes access the device concurrently, or when it belongs in a subsystem that offers a standard interface upwards.
- What does the devm prefix on functions like devm_kzalloc mean?
- Those variants tie a resource to the device's lifetime: if the driver is unloaded or probe fails, the kernel releases it automatically. That removes the most common source of bugs in drivers – forgotten cleanup paths, or ones unwound in the wrong order. Where a devm variant exists, use it.
- Out-of-tree module or upstream in the kernel?
- An out-of-tree module is quick to build but has to be carried forward across every kernel update, because internal APIs carry no stability guarantee – over a product's lifetime that adds up. Code accepted upstream gets adjusted by the people making those API changes. For parts only you use, out-of-tree is pragmatic; for anything others might use too, going upstream pays for itself across the product's life.
- How do you debug a driver that does not probe?
- Start with `dmesg` and look for the driver's name. If nothing appears at all, probe was never called, so the match is wrong: check the `compatible` string in the device tree against the `of_match_table`, and whether the node is set to `status = "okay"`. If probe aborts with an error, a resource is usually missing: clock, regulator, interrupt or pinctrl entry. `dynamic_debug` makes individual files verbose at runtime without rebuilding the kernel.
More on these topics
Managing Director, bitshift dynamics
Builds hardware-adjacent software for embedded products with his team – C++, Qt/QML, Embedded Linux and the Yocto Project. bitshift dynamics has worked in this field since 2005.