1 of 25

Understanding Device Drivers

Through the RTC Driver

​

2 of 25

  • ECE 391 Fall 2026 Assignments
  • mp2 Checkpoint 1 and 2 due Friday, September 18 at 6pm
  • Midterm Exam 1 Tue, Oct 6

GRAINGER ENGINEERING

Announcements

ELECTRICAL & COMPUTER ENGINEERING

3 of 25

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.

4 of 25

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

5 of 25

The Layered Architecture

ELECTRICAL & COMPUTER ENGINEERING

GRAINGER ENGINEERING

Application Code

ioread(rtcio, &timestamp, 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)

6 of 25

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

7 of 25

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

8 of 25

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.

9 of 25

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.

10 of 25

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?

11 of 25

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.

12 of 25

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!

13 of 25

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

14 of 25

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

15 of 25

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

16 of 25

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!

17 of 25

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

18 of 25

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

19 of 25

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

20 of 25

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

21 of 25

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

22 of 25

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

23 of 25

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

24 of 25

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)

25 of 25

Driver Complexity Comparison

ELECTRICAL & COMPUTER ENGINEERING

GRAINGER ENGINEERING

RTC is the foundation: complexity builds incrementally