Skip to content

04. System Timer - Precise Timing for Your OS

Now that we can communicate via UART, let's add timing capabilities to our OS using the Raspberry Pi's system timer.

Why Do We Need a Timer?

Timers are essential for: - Delays: sleep() functions - Scheduling: Task switching in multitasking - Timeouts: Network or hardware communication - Performance measurement: Benchmarking code

Understanding Raspberry Pi Timers

The Raspberry Pi 4B has multiple timing mechanisms:

1. System Timer (Legacy)

  • Base Address: 0xFE003000 (RPi 4)
  • Frequency: 1 MHz (fixed)
  • Use: Simple delays, legacy compatibility

2. ARM Generic Timer (Preferred)

  • Built into CPU: Part of ARM architecture
  • Frequency: Typically 54 MHz on RPi 4
  • Advantages: More precise, accessible via special registers

We'll use the ARM Generic Timer as it's more accurate and follows ARM standards.

ARM Generic Timer Registers

The ARM architecture provides special system registers for timing:

Register Description
CNTPCT_EL0 Physical Count register (current tick count)
CNTFRQ_EL0 Counter Frequency register (ticks per second)

Reading Registers in Assembly

mrs x0, cntpct_el0  ; Read current counter
mrs x1, cntfrq_el0  ; Read frequency

Understanding Inline Assembly

To read these special hardware registers, C code isn't enough. We need to "inject" assembly instructions directly into our C code.

asm volatile("mrs %0, cntpct_el0" : "=r"(count));
  • asm volatile: "Compiler, do not touch or optimize this!"
  • mrs: The assembly instruction (Move from System Register).
  • %0: A placeholder for a variable.
  • "=r"(count): "Put the result into the C variable count."

It's like opening a secret door in C to talk directly to the CPU.

Implementing the Timer Driver

Header File: include/timer.h

#ifndef TIMER_H
#define TIMER_H

#include "types.h"

void timer_init(void);
uint64_t timer_get_ticks(void);
void timer_sleep(uint32_t ms);

#endif

Driver Implementation: drivers/timer.c

1. Reading Timer Registers

static inline uint64_t timer_read_cntpct(void) {
    uint64_t count;
    asm volatile("mrs %0, cntpct_el0" : "=r"(count));
    return count;
}

static inline uint64_t timer_read_freq(void) {
    uint64_t freq;
    asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));
    return freq;
}

2. Initialization

1
2
3
4
5
static uint64_t timer_freq = 0;

void timer_init(void) {
    timer_freq = timer_read_freq();
}

On the Raspberry Pi 4, timer_freq is typically 54,000,000 (54 MHz).

3. Sleep Function

void timer_sleep(uint32_t ms) {
    if (timer_freq == 0) {
        timer_init();
    }

    uint64_t start = timer_get_ticks();
    uint64_t ticks_per_ms = timer_freq / 1000;
    uint64_t end = start + (ticks_per_ms * ms);

    while (timer_get_ticks() < end) {
        asm volatile("nop");
    }
}

How it works (Polling):

The CPU constantly checks the clock, asking "Are we there yet?" over and over.

graph TD
    Start["Start Sleep"] --> GetStart["Read Start Time"]
    GetStart --> CalcEnd["Calculate End Time"]
    CalcEnd --> Check{"Current Time >= End Time?"}
    Check -->|No| Check
    Check -->|Yes| Done["Return"]
  1. Calculate how many ticks = 1 millisecond
  2. Compute target tick count
  3. Busy-wait until we reach that count

Busy-Wait Limitation

This is a busy-wait implementation. The CPU actively waits, wasting power. In a real OS, we'd use interrupts to sleep efficiently.

Using the Timer in Your Kernel

Update kernel/main.c:

#include "uart.h"
#include "timer.h"

void kernel_main(void) {
    uart_init();
    timer_init();

    uart_puts("System Timer initialized.\n");

    int count = 0;
    while(1) {
        uart_puts("Tick ");
        // Print count...
        uart_puts("\n");

        timer_sleep(1000); // 1 second delay
        count++;
    }
}

Building and Testing

1. Build the Kernel

cd simpian-os/build
make

Verify that drivers/timer.c is compiled.

2. Deploy to Raspberry Pi

Copy kernel8.img to your SD card (along with firmware files).

3. Connect Serial and Boot

Open terminal:

screen /dev/ttyUSB0 115200

Expected output:

1
2
3
4
5
6
Hello World from Bare Metal RPi OS!
System Timer initialized.
Tick 0
Tick 1
Tick 2
...

You should see a message every second!

How Accurate Is It?

The ARM Generic Timer is quite precise: - Jitter: < 1 microsecond - Drift: Negligible for short durations

For comparison: - System Timer (1 MHz): 1 µs resolution - ARM Generic (54 MHz): ~18.5 ns resolution

Complete Source Code

The full implementation is available: - drivers/timer.c - include/timer.h - kernel/main.c

Troubleshooting

Problem Solution
Messages print too fast/slow Check timer frequency with uart_puts
No output at all Ensure timer_init() is called
Incorrect delays Verify 64-bit arithmetic (not 32-bit overflow)

What's Next?

With timing capabilities in place, we can now: - Implement interrupts (timer-based interrupts for preemptive multitasking) - Add watchdog timer (system recovery) - Create performance profiling tools

Stay tuned for the next article on Exception Handling and Interrupts!