Understanding Device Drivers
Through the RTC Driver
GRAINGER ENGINEERING
Announcements
ELECTRICAL & COMPUTER ENGINEERING
What Is the RTC?
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
A Real-Time Clock: wall-clock time, kept in hardware
What it is
• Keeps wall-clock time, independent of the CPU
• Reports nanoseconds since the Unix epoch
• Epoch = 00:00:00 UTC, 1 January 1970
• Goldfish RTC, mapped at 0x00101000
• Always ready, nothing to wait for
One 64-bit Number
63
32
31
0
time_high
time_low
one 64-bit nanosecond count, two 32-bit registers
DS1287: real-time clock manufactured in 1988.
64 bits of nanoseconds runs to the year 2554, so there is no Y2038 problem here.
Why Start with the RTC?
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Simplest Driver
No interrupts, queues, or concurrency
Foundation Pattern
Every concept you need appears here
Scales Up Naturally
More complex drivers add to this pattern
Real Implementation
Working code in production kernel
Try it in any Unix terminal:
$ cat /sys/class/rtc/rtc0/time
Core concepts: I/O objects, device manager, MMIO, driver lifecycle
The Layered Architecture
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Application Code
ioread(rtcio, ×tamp, 8)
I/O Object Layer (struct io)
Function pointer dispatch
Device Manager (device.c)
"rtc" → rtc_open()
Driver (rtc.c)
Hardware specifics
Hardware (MMIO @ 0x00101000)
Device Data Structure Relationships
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
How All the Pieces Connect
device_record
name: "rtc"
openfn: &rtc_open
ofaux: rtc
next: <next>
struct rtc_device
volatile rtc_regs *regs
struct io {
intf: &rtc_intf
blksz: 8
refcnt: 1
}
struct iointf rtc_intf
implname: "rtc"
reclaim: NULL
read: &rtc_read
write: NULL
fetch: NULL
store: NULL
ioctl: NULL
MMIO @ 0x00101000
struct rtc_regs {
time_low;
time_high;
}
Application View
open_device("rtc0", &rngio); ioread(rngio, buf, 8);
Key Relationships
• device_record.ofaux → rtc_device • rtc_device.io.intf → rtc_intf • rtc_device.regs → MMIO
Boot Initialization Sequence
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
1
board_init()
Memory, PLIC, timer setup
2
intrmgr_init()
Interrupt manager initialization
3
devmgr_init()
Device manager (empty registry)
4
thrmgr_init()
Thread manager setup
5
attach_devices()
Probe hardware, attach_rtc()
6
enable_interrupts()
Enable global interrupts
7
run_games()
Open devices, start programs
Memory-Mapped I/O (MMIO)
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
How Software Talks to Hardware
• Special Memory Regions
• Reads/writes go to device, not RAM
• RTC at physical address 0x00101000
• Each device has assigned region
Under the Hood: One Address Space
lw t0, 0(a0)
a0 = 0x00101000
CPU
a plain load:
no special I/O instruction
SYSTEM BUS · physical address + data
ADDRESS DECODER · which target owns this address?
RAM
0x8000_0000 +
actual storage
RTC
0x0010_1000
device registers
UART
0x1000_0000
device registers
Same load/store instructions, same address space. The decoder decides whether the bytes come from RAM or a device register.
The volatile Keyword
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Why the compiler must not optimize MMIO accesses away
What it does
volatile struct rtc_regs *regs;
• Forces actual memory access
• Prevents compiler optimization
• Essential for MMIO correctness
Without it, this breaks
// nothing in the program writes these,
// so the compiler assumes they cannot
// change between reads
lo = regs->time_low;
hi = regs->time_high;
// free to reorder, hoist, or delete
The RTC latch only works if both reads actually happen, in this order. volatile is what guarantees that.
Does Read Order Matter?
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Two versions of read_real_time(): both compile, both read both registers
Version A
uint32_t lo, hi;
lo = regs->time_low;
hi = regs->time_high;
return ((uint64_t)hi << 32) | lo;
Version B
uint32_t lo, hi;
hi = regs->time_high;
lo = regs->time_low;
return ((uint64_t)hi << 32) | lo;
Both compile. Both read both registers. Both combine them the same way, so one of them is broken. Which one, and what actually goes wrong?
Why High-Then-Low Breaks
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Same instant, two different answers
Version B: high, then low
true time = 0x00000002_FFFFFFF0
hi = regs->time_high -> 0x00000002
... counter rolls over ...
lo = regs->time_low -> 0x00000005
result = 0x00000002_00000005
4.295 s in the past. The two halves were sampled at different instants.
Version A: low, then high
true time = 0x00000002_FFFFFFF0
lo = regs->time_low -> 0xFFFFFFF0
hardware latches hi = 0x00000002
... counter rolls over ...
hi = regs->time_high -> 0x00000002
result = 0x00000002_FFFFFFF0
Exactly the time at the low read. The latch froze the high half to match.
The low half wraps every 2³² ns ≈ 4.3 s. Only a read pair that straddles a wrap goes wrong: rare, non-deterministic, and painful to debug.
RTC Hardware Registers
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Simplest Possible Device: Two 32-bit Registers
struct rtc_regs {
uint32_t time_low;
offset 0x00 - read FIRST
uint32_t time_high;
offset 0x04 - latched
};
Hardware Contract: Reading time_low latches time_high. Wrong order = garbage data!
The Device Struct Pattern
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Every Driver Follows This Structure
struct rtc_device {
volatile struct rtc_regs *regs;
struct io io;
};
Component | RTC | UART | vioblk |
MMIO pointer | rtc_regs* | uart_regs* | virtio_mmio_regs* |
struct io | ✓ | ✓ | ✓ |
IRQ number | none | irqno | irqno |
Sync primitives | none | cond vars, ring buf | cond var, rwlock |
RTC is the minimal case: MMIO + I/O object only
The I/O Object: struct io
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Definition (ioimpl.h)
struct io {
const struct iointf *intf;
unsigned int blksz;
unsigned int refcnt;
};
Field Purposes
intf
Function pointer table (vtable)
blksz
Block size (RTC: 8 bytes)
refcnt
Reference count (lifecycle)
Embedded pattern: struct io lives inside the device struct
The I/O Interface: iointf
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Polymorphism in C: Function Pointer Table
struct iointf {
const char *implname;
void (*reclaim)(...);
long (*read)(...);
long (*write)(...);
long (*store)(...);
long (*fetch)(...);
int (*ioctl)(...);
};
RTC's iointf (Minimal)
static const struct iointf
rtc_intf = {
.implname = "rtc",
.read = &rtc_read
};
• Only read is set
• All other ops return -ENOTSUP
Drivers fill in only what they support
I/O Dispatch Mechanism
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
1
Validate arguments
assert(io != NULL)
2
Check support
if (io->intf->read == NULL) return -ENOTSUP
3
Validate buffer size
if (bufsz < io->blksz) return -EINVAL
4
Dispatch to driver
return io->intf->read(io, buf, bufsz)
One interface, many implementations. Caller doesn't know which driver is behind the pointer!
attach_rtc(): Birth of a Driver
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Called once at boot to set up the driver
1
Allocate
rtc = kcalloc(1, sizeof(*rtc));
Zero-initialized from heap
2
Store MMIO
rtc->regs = mmio_base;
0x00101000 physical address
3
Register
register_device("rtc", -1, &rtc_open, rtc);
Publishes as "rtc"
4
Init I/O
ioinit(&rtc->io, &rtc_intf, 8, 0);
Setup intf, blksz=8, refcnt=0
No interrupt registration, no virtqueue setup: RTC is always ready
Opening: From Name to I/O Object
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Application Call
open_device("rtc0", &rngio);
Device Manager
Searches registry for "rtc0"
rtc_open() Driver Function
int rtc_open(struct io **ioptr,
void *aux) {
struct rtc_device *rtc = aux;
*ioptr = ioaddref(&rtc->io);
return 0;
}
Result
• rngio now points to &rtc->io
• refcnt incremented: 0 → 1
• Caller has I/O handle
rtc_read(): The Actual Work
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
long rtc_read(struct io *io, void *buf, long bufsz) {
struct rtc_device *rtc =
(void*)io - offsetof(struct rtc_device, io);
if (bufsz == 0) return 0;
uint64_t time_now = read_real_time(rtc->regs);
memcpy(buf, &time_now, sizeof(uint64_t));
return sizeof(uint64_t);
}
Container Recovery
Given struct io*, recover the enclosing rtc_device* using pointer arithmetic and offsetof()
Hardware Interaction
read_real_time() performs two MMIO reads, combines into 64-bit timestamp
Container Recovery Pattern
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Every Driver Uses This Technique
Memory Layout
regs (8 bytes)
io { intf, blksz, refcnt }
rtc →
io →
Universal Pattern
RTC:
(void*)io - offsetof(struct rtc_device, io)
UART:
(void*)io - offsetof(struct uart_device, io)
vioblk:
(void*)io - offsetof(struct vioblk_device, io)
Similar to Linux's container_of macro
Complete Journey: Boot to ioread()
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
1
board_init() → heap, PLIC, timer
2
devmgr_init() → empty registry
3
attach_rtc() → allocate, register, init I/O
4
open_device("rtc0") → rtc_open()
5
ioread(rngio) → rtc_read() → MMIO
From power-on reset to reading nanoseconds from hardware
What the RTC Doesn't Need
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Understanding Omissions Reveals Complexity
No Interrupts
Polled device, data immediately available
UART/vioblk: async operations need ISRs
No reclaim Function
No cleanup needed on close
Drivers with ISRs must disable interrupts
No Synchronization
No ISR to race with, always ready
UART: condition vars, interrupt disable/restore
No Feature Negotiation
Simple Goldfish device, fixed registers
VirtIO: multi-step state machine, feature bits
Universal Driver Checklist
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Every Driver Follows This Recipe
1
Define register struct
Map hardware MMIO to C struct
2
Define device struct
volatile regs* + embedded struct io
3
Define iointf
Function pointers for supported ops
4
Write attach_xxx()
Allocate, register, init I/O object
5
Write xxx_open()
Increment refcnt, enable hardware
6
Write I/O operations
read, write, fetch, store
7
Write ISR + reclaim
(If needed) Handle interrupts, cleanup
Key Patterns Summary
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
Embedded I/O Object
struct io lives inside device struct
struct rtc_device { ...; struct io io; };
Container Recovery
Recover device* from io* using offsetof
(void*)io - offsetof(struct rtc_device, io)
Function Dispatch
iointf provides polymorphic operations
io->intf->read(io, buf, bufsz)
Device Registry
Map names to open callbacks
register_device("rtc", -1, &rtc_open, rtc)
Driver Complexity Comparison
ELECTRICAL & COMPUTER ENGINEERING
GRAINGER ENGINEERING
RTC is the foundation: complexity builds incrementally