I was interested in what modern, C-based, Linux device driver development looked like, and so I took a good gander at this excellent article.

I didn't read every single line, but perused the majority of it, and my take away is this: writing device drivers for Linux seems like an incredibly painful process. Now that's my opinion, and I'm sure many Linux/C gurus out there would disagree, but there's one thing that I just can't wrap my head around here.

Every CPU/MCU exposes a C library that defines I/O registers, ports, etc. for accessing (both reads and writes) every single pin on the chip. On AVR controllers, for instance, there is <avr/io.h>. For ARM chips there is CMSIS.

So my question is this:

Why would I go to all the 'trouble' (more work) of following the guidelines in that article and building a proper device driver in Linux, when I could just write a C library that uses one of these processor-provided I/O registry accessors (again, avr/io.h, CMSIS, etc.) to access all the pins on the chip (which seems like less work)?

I'm sure there's a reason: I'm just not seeing it. What is gained by using a proper device driver? What is lost by not using one and just using one of these libs directly?

  • 1
    If that ~100 lines of mostly boring C code scare you, don't look at the "real" part of any real device driver (i.e. the part that actually talks to the device and "just" reads/writes to the IO ports or whatever). The articles shows you just the scaffolding which should take you a couple hours max to get right. A full device driver is probably in the man-years of dev for interesting devices.
    – Mat
    May 13, 2015 at 14:03
  • Thanks @Mat (+1) - I appreciate the input there, but it doesn't really help me understand anything better. I'm not scared by that article, I might have been exaggerating to make a point. My point is: of what utility is a device driver when I can access IO ports & registers directly in C code. Any ideas?
    – smeeb
    May 13, 2015 at 14:06
  • 2
    Do you want to program for a single device you know? Or do you want to write a generic application that will have to interact with hardware devices of thousands of different vendors? May 13, 2015 at 14:39
  • Thanks @herzmeister (+1) - I don't have a specific use case in mind, and so I would be interested in your answer to both cases (single device and generic app). Thanks again!
    – smeeb
    May 13, 2015 at 15:05
  • Read the wikipage on operating systems May 13, 2015 at 20:59

2 Answers 2


Asking why microcontrollers like the AVR family let you twiddle the pins directly when an operating system like Linux requires device drivers is an apples-to-suspension-bridges comparison.

Microcontrollers are typically single-threaded free-for-alls with no supervisor (OS) and all code has full access to memory, I/O and anything else the chip offers. This means that if one part of the application sets GPIO pin 3 high and needs to see that it stays that way, there's nothing that prevents some other part of the code from setting it low again. The developer is responsible for putting those kinds of interlocks into place. There's also no notion of devices: the MCU has a set of registers that control I/O that's the same on every copy of the chip and that's all there is. If you want to run your program on a different chip in the same family with different I/O, you're likely going to have to rewrite parts of it or, at the very least, recompile it with chip-specific headers or libraries.

General-purpose processors like the one in your desktop PC don't have the I/O features built in, opting instead to communicate with external peripherals whose registers are mapped somewhere in memory. The model is the same, though. If you plop an x86 CPU into a board where you know the I/O mappings, it's entirely possible to write AVR-style programs that do their own I/O, but you're going to be doing it on bare metal without any of the facilities an operating system provides.

A lot of software was done this way, but at some point in history, it became apparent that the model wasn't going to scale. People wanted to write programs to run on a given processor without having to fuss with building a separate version for each combination of peripherals. This spawned operating systems, which let a program say things like "send the character C across serial port 2" using a standard software interface. The code behind that standard interface was built up for the specific set of hardware on that system and knew that serial port 2 was a Brand X UART mapped at address 1234. In microcomputing, this kind of thing became most evident with the appearance of the CP/M operating system in the 1970s. Vendors selling computers would provide a version of the OS with custom drivers for their specific hardware. Do-it-your-selfers had to patch up generic versions by hand to work with what they had. The result was that they all ended up with systems that could run off-the-shelf versions of software without having to modify it.

Modern operating systems do pretty much the same thing but with a much higher level of sophistication. Systems (PC-style and even many embedded ones) are built around I/O architectures that are enumerable and dynamic, meaning that the software can figure out what's present, assign it a memory mapping and use the right functions to communicate with it. Along with that comes driver infrastructure that lets hardware developers leverage all of that sophistication without having to re-invent it each time and without risk that driver A was going to conflict with something driver B is doing.

If you decide to try going the poke-the-device-directly route on a full-blown operating system like Linux, the first question you'll ask is how a userland process gains access to the memory area where the device's registers are mapped. The answer ("you can't") is what brings up the other reason: multi-user, multi-tasking operating systems don't trust user programs to be well-behaved. They're designed to protect the system from harm and the programs running on the system from each other. Making this happen brings with it a complex model of interlocks and permissions, all of which contribute to whether or not a program can ask a device driver to do something on its behalf. The device driver code put into the kernel is given a much greater amount of trust because, presumably, those who wrote it did so carefully.

The functions you put into a device driver for most flavors of Unix are just the essentials (load, unload, open, close, read, write, ioctl) and are called at the end of a very long chain of decision making. If a process wants to write to a serial port, the driver code isn't called until the OS is sure there's a driver present and functioning for that device, the process has permission to use the device and that the port has been opened by that process.

This, at long last, is why you write a device driver: The amount of code you have to develop for a device driver is minuscule compared to the amount of sophistication the rest of the OS provides to go with it.

Really, it isn't painful. It's just unfamiliar. Once you've done a couple, you'll wonder why you were making such a fuss about it. (And trust me, it's a lot less work now than it was 25 years ago.)

  • "The answer ("you can't")" if you are running as root you can. Just mmap the relavent portion of /dev/mem. Feb 12, 2018 at 19:27
  • @PeterGreen True to a point, although the existence of a device that maps physical memory isn't required for a system to function, behavior of certain X servers and virtual machine subsystems excepted.
    – Blrfl
    Feb 12, 2018 at 22:40

The main answer is uniformity - if your device is a webcam, you can expose a C library and tell every application that to receive images from your cam, they can easily write to your custom API. I guarantee you will not have a working webcam in any application other than the ones you write!

That's typically why you abstract hardware behind a device driver - you say it yourself, you can access AVR chips via io.h, and ARM via CMSIS. If you write an application that works for AVR, it'll never work for ARM unless you have a big switch statement and an abstracting code anyway, and if someone uses your application with a different CPU, it just won't work at all.

  • Thanks @gbjbaanb (+1) - things are starting to become clearer to me. So are you saying that the device driver is just an API? If so, I would imagine it would need "bindings" for different CPUs. In your example, a custom webcam, it sounds like I would write a device driver (API), and then 1 binding for each type of CPU I would want to support, yes? Thanks again!
    – smeeb
    May 13, 2015 at 15:04
  • @smeeb: It'd be more correct to say that the device driver provides an abstraction. Basically, on one side you have the hardware's (device specific) interface, in the middle is the driver that provides the abstraction, and on the other side is the software that uses the abstraction (file systems, networking stack, whatever).
    – Brendan
    May 13, 2015 at 16:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.