Part IX — Hardware & driver programming
Everything through Part VIII runs on top of an OS that already manages memory, files, and processes for you. This Part is what changes when your C code has to talk directly to a physical chip — a sensor, an LED, a USB device — either from a normal process, or from inside the kernel itself. This is the layer most job postings mean by "systems" or "embedded" experience, and where a senior engineer is expected to be able to go from a chip's datasheet to working code.
A note on testing. Every code sample elsewhere in this guide was compiled and run before being written down. The
/dev/mem-mmap and serial/termios examples below are ordinary POSIX C and were verified the same way. The Linux-kernel-specific examples (kernel modules,ioctlcalls against<linux/i2c-dev.h>/<linux/spi/spidev.h>) cannot be compiled on the machine this guide was written on — a kernel module builds against your running kernel's exact header tree (/lib/modules/$(uname -r)/build), not a generic C toolchain, andlinux/*.hheaders don't exist outside Linux. What's shown is the standard, extremely stable API every Linux driver tutorial and textbook has used for well over a decade; treat it as accurate boilerplate to adapt, and build/test it on a real Linux box, a Raspberry Pi, or a QEMU VM before trusting it on real hardware.
47. Two worlds: user-space hardware I/O vs. writing a kernel driver
The phrase "write a driver" gets used for two very different jobs, and conflating them is the single most common confusion senior candidates run into in interviews for embedded/systems roles.
| User-space hardware I/O | Kernel driver | |
|---|---|---|
| Runs as | A normal process | Code loaded into the kernel itself |
| Talks to hardware via | A /dev node the kernel already provides (/dev/i2c-1, /dev/spidev0.0, /dev/ttyUSB0, /dev/gpiochip0) |
Directly: memory-mapped registers, interrupts, DMA |
| Crash blast radius | That one process dies; the OS is fine | Can panic the entire machine — there's no memory protection between your bug and the kernel |
| When you need it | The overwhelming majority of real "hardware" work: reading a sensor, driving a motor controller, talking to a USB device, flashing firmware | The kernel doesn't already expose the hardware as a /dev node, or you need interrupt-tight integration with a kernel subsystem (a new bus type, a new filesystem, a new network device) |
| Debugging | gdb/lldb, exactly like any other program in this guide | printk+dmesg, kernel oops messages, ftrace, sometimes a JTAG debugger or QEMU |
Real-world example. A team building a robotics product needs to read
an IMU (inertial measurement unit) over I2C and drive motor PWM signals.
Neither needs a kernel driver — the SoC vendor's Linux distribution
already exposes I2C and PWM as /dev nodes and sysfs files; the actual
"driver" work is a user-space C program doing ioctl() calls against
those nodes (Section 48). The team only reaches for actual kernel code
when they add a custom FPGA-based sensor board with no existing driver
and genuinely new interrupt-driven behavior the kernel needs to know
about (Sections 50–53). This 90/10 split — mostly user-space I/O, rarely
a real kernel module — holds for almost every "hardware" job description
you'll see; this Part covers both halves so you can tell, for a given
task, which one you actually need.