· 12 min read · 0 comments
Embedded Systems Software

How to Write an Embassy Time Driver for a New Microcontroller

My first attempt at writing a time driver for a new microcontroller using the embassy library.

In the process of learning how async Rust works on embedded systems, I’ve been reading about the embassy library. I’ve also recently been playing around with some GD32 based micro-controller development boards and I thought it would be instructive to try to write a time driver for a new chip that doesn’t have one yet.

Embassy is wonderfully convenient to use once your microcontroller is supported, but the documentation for implementing a time driver on a new chip is relatively sparse. The official Embassy guidance on adding support for a new microcontroller describes it as a much harder path than using an already-supported chip and advises beginners to gain experience with a well-supported device before writing drivers from scratch. So I thought I’d try to develop one and document what I learned along the way.

My goal today is to to have a very simple delay loop like the one shown below working on a new microcontroller, and in the process understand some part of how async Rust works on embedded systems. Note that this is a “first-pass” implementation of the time driver and may not be production ready. So use with care!

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_gd32::time_driver;
use embassy_time::Timer;
use panic_probe as _;
use rtt_target::{rprintln, rtt_init_print};

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    rtt_init_print!();
    time_driver::init();

    let mut count: u32 = 0;

    loop {
        count = count.wrapping_add(1);
        rprintln!("embassy-time-smoke tick={}", count);
        Timer::after_millis(500).await;
    }
}

The Problem

In synchronous code, a delay is easy to imagine:

delay_ms(500);

The CPU sits there and waits for 500ms to pass before it continues to the next line. This is fine if your program has exactly one thing to do. But it stops working the moment you want to do two things at once. If one task is waiting for 500 ms, you do not want the whole system to stop.

One way to solve this problem is through asynchronous code. We want one task to say “wake me up later” without blocking everything else and yield control over to the executor so that it can run other tasks.

So Timer::after_millis(500).await is doing something like:

  1. Record a deadline.
  2. Tell the executor not to poll this task until then.
  3. Let other tasks run.
  4. Wake this task when time has passed.
  5. That means Embassy needs a global source of time and a way to wake sleeping tasks at the right moment.

That is the time driver.

The Driver Trait

To wait without blocking, the async executor needs to be able to know what time it is right now and to be able to register an “alarm” for a future time in hwardware. This leads us directly to embassy’s time driver model.

The embassy-time-driver crate defines a Driver trait that looks like this:

use core::task::Waker;

pub trait Driver {
    fn now(&self) -> u64;
    fn schedule_wake(&self, at: u64, waker: &Waker);
}

The function now() tells you “what time is it right now?” by returning a u64 timestamp measured in ticks. The duration of a tick is defined by Embassy’s global TICK_HZ configuration; in this driver, TICK_HZ is 1 MHz, so one tick is one microsecond. The schedule_wake() function allows you to register a wakeup for a future tick. You provide the tick at which you want to be woken up and a reference to the task’s Waker, which the driver arranges to wake when that time arrives. This is enough for Embassy to implement Instant, Duration, Timer, Ticker and deadlines.

How Embassy Wires Everything Together

Embassy defines one global time driver in the final binary that is registered with a macro. Here MyDriver is a struct that implements the Driver trait. The macro generates code that creates a static instance of MyDriver and registers it as the global time driver for embassy.

embassy_time_driver::time_driver_impl!(static DRIVER: MyDriver = MyDriver { ... });

So when the user awaits on a timer, the timer will compute an absolute deadline and calls DRIVER.schedule_wake(deadline, cx.waker), which ends up inside the driver’s schedule_wake() method. The driver logic then arranges for the hardware timer to fire an interrupt at the right moment and call the waker so that the execter then polls the task again. Timer then sees that the deadline has arrived or passed and returns from the await.

At a high level, the application, Embassy’s software timer queue, and TIMER2 form a loop: the task registers a deadline on the way down, and the interrupt wakes the task through the executor on the way back up.

flowchart TB
    subgraph app["Async application"]
        task["Task awaiting Timer::after(...)"]
        executor["Embassy executor"]
    end

    subgraph embassy["Embassy time layer"]
        timer["Timer API"]
        driver["Global Driver singleton<br/>now() and schedule_wake()"]
        queue["Software timer queue<br/>all deadlines and wakers"]
    end

    subgraph gd32["GD32 time-driver implementation"]
        state["Driver state<br/>high_word and pending alarm"]
        counter["TIMER2 16-bit counter<br/>1 MHz free-running clock"]
        compare["Channel 0 compare<br/>one hardware alarm"]
        isr["TIMER2 interrupt handler"]
    end

    task -->|"await creates a deadline"| timer
    timer -->|"read time and register waker"| driver
    driver -->|"store deadline and waker"| queue
    driver -->|"read extended time"| state
    state <-->|"high word + low counter"| counter
    queue -->|"earliest deadline"| driver
    driver -->|"arm now or defer"| compare
    counter -->|"UPIF on overflow"| isr
    compare -->|"CH0IF at deadline"| isr
    isr -->|"extend clock or process alarm"| state
    isr -->|"wake expired wakers"| queue
    queue -->|"make task runnable"| executor
    executor -->|"poll task again"| task

Implementing our Time Driver

Most microcontrollers have hardware timers of varying complexity that lets you time events. They usually have a counter that increments based on a clock (which we can configure to be a certain frequency) and some registers that let you set up interrupts for various events such as when the counter reaches a certain value or when the counter overflows. The details of how to use these timers vary a lot between different microcontrollers but the general idea is the same. To implement the Driver trait, we need to use one of these hardware timers to keep track of time and to schedule wakeups.

For our concrete example I’m going to try and implement a time driver using Timer 2 of the GD32F4xx series of micro-controllers. Timer 2 is general purpose timer and has a 16 bit counter as well as interrupts for overflow and compare. We immediately notice a problem. Our timer counter is only 16 bits, which means it can only count up to 65535 before it overflows. If we want to use a clock frequency of 1 MHz (which gives us microsecond resolution), the timer will overflow every 65.535 ms. This is not ideal for a time driver, which needs to be able to handle much longer durations. Indeed, the Driver trait treats the time stamp ans a u64 so that we can represent time spans long enough that our micro-controller will likely reset before we hit the limit. To solve this problem, we need to handle the overflow and maintain our own extra bits to keep track of how many times the timer has overflowed.

Configuring the Tick Rate

The first step is to configure the timer to tick at a known frequency. I’m implementing for 1MHz since that gives us microsecond resolution. We achieve this by setting the timer’s prescaler register (TIMER1_PSC) to the right value. The timer’s counter clock will be set to the the TIMER_CK frequency divided by (PSC + 1). So if we want a 1 MHz tick rate and our TIMER_CK is 100 MHz, we need to set PSC to 99.

let timer_clock_hz = u64::from(timer2_input_clock_hz(DEFAULT_HXTAL_HZ));
let tick_hz = TICK_HZ;
let divider = timer_clock_hz / tick_hz;
let psc = divider - 1;

// More code


// Set the prescaler
timer2.psc().write(|w| w.psc().bits(psc as u16));

Extending the Hardware Counter

Since most microcontrollers only have 16- or 32-bit timers, we need to extend the hardware counter in software by keeping track of how many times it has overflowed. This first-pass driver uses core::sync::atomic::AtomicU32 for the software high word so that it can be updated from an interrupt handler and read by now(). Combined with TIMER2’s 16-bit counter, this produces a timestamp with 48 effective bits stored in a u64. The implications of that choice are discussed in the limitations section below.

struct EmbassyTimerDriver {
    high_word: AtomicU32,
    // ...
}

However, this is still not enough to make the now() function safe against data races. Without careful thought you might implement now() like so:

  1. Read high_word
  2. Read current timer count
  3. Combine to get time stamp.

However, this results in a data race if the timer overflows between reading the high_word and reading the timer count. To solve this problem, we read the high_word and the timer’s interrupt flag before and after reading the timer count. If the high_word or the interrupt flag has changed after the counter read, then we know that an overflow occurred. In this case, we simply continue to retry the whole process until we get a consistent snapshot of the high_word, the interrupt flag and the timer count.

loop {
    let high_before = self.high_word.load(Ordering::Relaxed);
    let upif_before = timer.intf().read().upif().bit_is_set();
    let cnt = timer.cnt().read().cnt().bits();
    let upif_after = timer.intf().read().upif().bit_is_set();
    let high_after = self.high_word.load(Ordering::Relaxed);

    if high_before != high_after || upif_before != upif_after {
        continue;
    }

    let high = if upif_after {
        high_before.wrapping_add(1)
    } else {
        high_before
    };

    return ((high as u64) << TIMER2_COUNTER_BITS) | u64::from(cnt);
}

You Only Need One Alarm

The schedule_wake() function allows you to schedule a wakeup for a future time. A natural question is: if there are many tasks waiting on a deadline, how can we handle all these deadlines with just a single alarm? The answer is to combine a software timer queue with one hardware alarm. The queue tracks all pending deadlines, while the hardware alarm is programmed only for the earliest one. The queue does not have to be internally sorted; that is an implementation detail. When the alarm fires, the queue wakes all tasks whose deadlines have arrived, finds the earliest remaining deadline, and the driver programs the hardware alarm again.

struct EmbassyTimerDriver {
    high_word: AtomicU32,
    queue: Mutex<RefCell<Queue>>,
    // ...
}

Programming the Hardware Alarm

The queue tells us which task should wake next, but it does not wake anything by itself. We still need to translate the earliest queued deadline into something the hardware timer can interrupt on.

For TIMER2, I use Channel 0 in output-compare mode. The basic idea is simple: write a value into the channel compare register CH0CV, and when the hardware counter CNT reaches that value, TIMER2 raises the channel 0 interrupt flag CH0IF.

That gives us a hardware alarm, but only a small one. CH0CV is also 16 bits wide, so it can only compare against the low 16 bits of the counter. At a 1 MHz tick rate, a 500 ms delay is 500,000 ticks, which is much larger than 65,535. Even though our extended timestamp is represented as a u64, the hardware compare register can only directly represent deadlines within the next counter period.

So the driver classifies every alarm into three cases:

timestamp <= now              -> fire immediately
timestamp - now <= 65_535     -> write CH0CV and enable CH0IE
timestamp - now > 65_535      -> defer until a later overflow

In code, this becomes a small helper enum:

enum AlarmArmDecision {
    FireNow,
    ArmCompare,
    Defer,
}

If the deadline is already in the past, there is no point programming the timer. The driver returns false from set_alarm(), which tells the caller to wake expired tasks immediately. If the deadline is close enough, the driver writes the low 16 bits of the timestamp into CH0CV and enables the channel 0 compare interrupt:

timer
    .ch0cv()
    .write(|w| unsafe { w.bits((timestamp as u32) & TIMER2_COUNTER_MASK) });

timer.dmainten().modify(|_, w| w.ch0ie().set_bit());

If the deadline is too far away, the driver records the full u64 timestamp but leaves the compare interrupt disabled. That leads to the next part of the design.

Handling Long Delays

Long delays are handled by deferring the alarm until it gets close enough for the 16-bit compare register. The driver stores the full target timestamp in AlarmState:

struct AlarmState {
    timestamp: Cell<u64>,
}

u64::MAX is used as the sentinel value meaning “no alarm is pending.” Access to this state is protected by a critical section because it is shared between normal task context and the TIMER2 interrupt handler.

When an alarm is more than 65,535 ticks away, set_alarm() stores the timestamp but disables CH0IE. From there, the overflow interrupt becomes the driver’s way of making progress. Every time the 16-bit counter wraps, the ISR increments high_word and then asks: “Is the pending alarm close enough now?”

fn on_overflow(&self, cs: CriticalSection) {
    self.high_word.fetch_add(1, Ordering::Relaxed);

    let at = self.alarm.borrow(cs).timestamp.get();
    if at != u64::MAX && !self.set_alarm(cs, at) {
        self.trigger_alarm(cs);
    }
}

This is the important split:

A long sleep may pass through many overflow interrupts before it becomes a short sleep. Once it is within the next 65,535 ticks, the driver arms CH0CV, enables CH0IE, and lets the hardware set the compare flag at the low-word match. The interrupt handler and executor run afterward, so interrupt latency and task scheduling mean the task itself resumes at or after its deadline, not necessarily on the exact tick.

Waking Expired Tasks

Now we can connect the hardware alarm back to Embassy’s Wakers.

When Embassy calls schedule_wake(at, waker), the driver inserts that waker into embassy_time_queue_utils::Queue:

fn schedule_wake(&self, at: u64, waker: &core::task::Waker) {
    critical_section::with(|cs| {
        let mut queue = self.queue.borrow(cs).borrow_mut();

        if queue.schedule_wake(at, waker) {
            let mut next = queue.next_expiration(self.now());

            while !self.set_alarm(cs, next) {
                next = queue.next_expiration(self.now());
            }
        }
    });
}

The return value of queue.schedule_wake() tells the driver whether it must recompute the next expiration and re-arm the hardware alarm. A true result does not necessarily mean that the global earliest deadline changed; for example, a queue implementation may return true whenever a task is newly enqueued. The driver therefore calls next_expiration(), which processes expired entries and returns the earliest remaining deadline to program into the hardware.

When the compare interrupt fires, the driver calls trigger_alarm():

fn trigger_alarm(&self, cs: CriticalSection) {
    let mut queue = self.queue.borrow(cs).borrow_mut();
    let mut next = queue.next_expiration(self.now());

    while !self.set_alarm(cs, next) {
        next = queue.next_expiration(self.now());
    }
}

The Interrupt Handler

TIMER2 has one interrupt entry point, and this driver uses it for both overflow and compare events:

UPIF  -> counter overflow; increment high_word; re-check deferred alarm
CH0IF -> compare match; wake expired tasks; arm next alarm

The public interrupt hook is kept small and readable:

pub fn on_interrupt() {
    let timer = unsafe { Timer2::steal() };

    critical_section::with(|cs| {
        let status = timer.intf().read().bits();
        let enabled = timer.dmainten().read().bits();

        EmbassyTimerDriver::clear_interrupt_flags(&timer, status);

        if (status & INTF_UPIF) != 0 {
            DRIVER.on_overflow(cs);
        }

        if (status & INTF_CH0IF) != 0 && (enabled & DMAINTEN_CH0IE) != 0 {
            DRIVER.trigger_alarm(cs);
        }
    });
}

One GD32 detail worth noting: the timer interrupt flag register uses write-0-to-clear semantics. Writing 0 clears a flag, while writing 1 leaves it unchanged. That is the opposite of many simple “write 1 to clear” registers.

fn clear_interrupt_flags(timer: &Timer2, status: u32) {
    let clear_bits = (!status) & INTF_CLEARABLE_MASK;
    timer.intf().write(|w| unsafe { w.bits(clear_bits) });
}

The ISR snapshots the status first, clears the active flags, and then handles the events from that snapshot. If a new hardware event arrives after the clear, the hardware sets the flag again and the interrupt will run another time.

Wiring Everything Together

The application only needs two pieces of glue. First, initialize the time driver before using embassy_time:

time_driver::init();

Second, route the TIMER2 interrupt to the driver:

#[interrupt]
fn TIMER2() {
    time_driver::on_interrupt();
}

After that, the original async loop works:

loop {
    count = count.wrapping_add(1);
    rprintln!("embassy-time-smoke tick={}", count);
    Timer::after_millis(500).await;
}

At this point Timer::after_millis(500) can compute a deadline, register the current task’s waker, and rely on TIMER2 to wake it later. The task is not spinning in a delay loop. It is asleep from the executor’s point of view, and other async tasks can run while the timer counts.

Limitations of This First-Pass Driver

This implementation is intended as a learning exercise and smoke-test driver, not as a production-ready Embassy time driver. So I deliberately leave several robustness issues for a later iteration:

Nevertheless, I hope that this article helps anyone out there trying to implement a time driver for their own micro-controllers currently not supported by embassy.

The Full Driver Implementation

//! # Embassy Time Driver for GD32F425 — TIMER2
//!
//! Implements [`embassy_time_driver::Driver`] to give Embassy's async executor an extended
//! monotonic clock and the ability to wake sleeping tasks at a specific future tick.  Every
//! `Timer::after_*`, `Instant::now()`, and timeout in the firmware ultimately calls into here.
//!
//! ## Hardware resource: TIMER2
//!
//! **TIMER2** is a 16-bit general-purpose timer on the APB1 bus.  A prescaler divides the APB1
//! timer clock down to exactly [`TICK_HZ`] (1 MHz = 1 µs per tick, chosen by the
//! `tick-hz-1_000_000` Cargo feature on `embassy-time-driver`).
//!
//! ## Extended monotonic counter
//!
//! The hardware counter is only 16 bits wide (0 – 65 535).  This first-pass driver extends it
//! with a 32-bit software epoch register `high_word`, incremented every time the hardware
//! counter wraps from 65 535 back to 0 (a counter "update" / overflow event).  The resulting
//! timestamp has 48 effective bits represented in a `u64`:
//!
//! ```text
//! now = (high_word << 16) | CNT
//! ```
//!
//! An overflow interrupt (UPIF) fires every 65 536 ticks (~65.5 ms at 1 MHz); its ISR
//! increments `high_word`.
//!
//! ## Alarm and wakeup
//!
//! `schedule_wake(at, waker)` arranges for the waker to be notified when tick `at` has arrived.
//! Interrupt latency and executor scheduling mean the task itself may resume after that tick.
//! A single hardware alarm is implemented using TIMER2 Channel 0 in output-compare mode: when
//! `CNT == CH0CV`, the CH0IF flag fires a compare-match interrupt.
//!
//! Because the compare register is also 16 bits, it can only express alarms at most 65 535
//! ticks away.  [`AlarmArmDecision`] captures the three cases:
//!
//! | Decision     | Condition                        | Action                                              |
//! |--------------|----------------------------------|-----------------------------------------------------|
//! | `FireNow`    | `timestamp ≤ now`                | Return `false`; caller wakes tasks immediately      |
//! | `ArmCompare` | `now < timestamp ≤ now + 65535`  | Write CH0CV, enable CH0IE                           |
//! | `Defer`      | `timestamp > now + 65535`        | Write CH0CV, keep CH0IE **disabled**; re-arm on each overflow |
//!
//! Deferred alarms are stored in `AlarmState::timestamp` and re-evaluated in `on_overflow`
//! after each 65 536-tick period until they enter the `ArmCompare` window.
//!
//! ## Interrupt handler
//!
//! One TIMER2 ISR services both events:
//! * **UPIF (overflow)** — increments `high_word`, then re-evaluates any deferred alarm.
//! * **CH0IF (compare match)** — fires the alarm: wakes all expired wakers via the timer queue
//!   and arms the next pending deadline.
//!
//! ## INTF write-0-to-clear semantics
//!
//! The GD32F4 INTF register uses **write-0-to-clear** semantics: writing `0` clears a flag;
//! writing `1` has no effect.  `clear_interrupt_flags` computes `(!status) & CLEARABLE_MASK`,
//! placing `0` in every set bit (clearing it) and `1` in every unset bit (no-op).
//!
//! ## Timer queue
//!
//! Multiple futures may register wakeups at different deadlines simultaneously.
//! [`embassy_time_queue_utils::Queue`] manages this list.  `trigger_alarm` loops, calling
//! `queue.next_expiration(now)` to wake all expired entries and retrieve the next pending
//! deadline, which is then re-armed as the next hardware alarm.

use core::cell::{Cell, RefCell};
use core::sync::atomic::{compiler_fence, AtomicU32, Ordering};

use cortex_m::peripheral::NVIC;
use critical_section::{CriticalSection, Mutex};
use embassy_time_driver::{Driver, TICK_HZ};
use embassy_time_queue_utils::Queue;
use gd32f4pac::gd32f425::{Interrupt, Rcu, Timer2};

use crate::system::{timer2_input_clock_hz, DEFAULT_HXTAL_HZ};

// ── TIMER2 INTF register bit positions ────────────────────────────────────────
// The INTF register holds pending interrupt flags.  It uses write-0-to-clear
// semantics: writing 0 to a bit clears that flag; writing 1 has no effect on it.
const INTF_UPIF: u32 = 1 << 0;   // Update (counter overflow) interrupt flag
const INTF_CH0IF: u32 = 1 << 1;  // Channel 0 compare-match interrupt flag
const INTF_CH1IF: u32 = 1 << 2;  // Channel 1 compare-match interrupt flag
const INTF_CH2IF: u32 = 1 << 3;  // Channel 2 compare-match interrupt flag
const INTF_CH3IF: u32 = 1 << 4;  // Channel 3 compare-match interrupt flag
const INTF_TRGIF: u32 = 1 << 6;  // Trigger interrupt flag
const INTF_CH0OF: u32 = 1 << 9;  // Channel 0 over-capture flag
const INTF_CH1OF: u32 = 1 << 10; // Channel 1 over-capture flag
const INTF_CH2OF: u32 = 1 << 11; // Channel 2 over-capture flag
const INTF_CH3OF: u32 = 1 << 12; // Channel 3 over-capture flag
// Union of all writable/clearable INTF bits.  Used when building a word to write
// back to INTF so that reserved fields are never accidentally touched.
const INTF_CLEARABLE_MASK: u32 = INTF_UPIF
    | INTF_CH0IF
    | INTF_CH1IF
    | INTF_CH2IF
    | INTF_CH3IF
    | INTF_TRGIF
    | INTF_CH0OF
    | INTF_CH1OF
    | INTF_CH2OF
    | INTF_CH3OF;
// DMAINTEN bit 1: enables the Channel 0 compare-match interrupt (PAC field: `ch0ie`).
const DMAINTEN_CH0IE: u32 = 1 << 1;
// TIMER2 on GD32F425 is a 16-bit general-purpose timer clocked from APB1.
const TIMER2_COUNTER_BITS: u32 = 16;
const TIMER2_COUNTER_MASK: u32 = (1u32 << TIMER2_COUNTER_BITS) - 1; // 0xFFFF
// Maximum future distance (in ticks) expressible as `timestamp - now` in one 16-bit
// counter period.  Alarms farther away than this must be deferred.
const MAX_COUNTER_HORIZON: u64 = TIMER2_COUNTER_MASK as u64;

/// Determines how a timer alarm should be armed relative to the current 64-bit tick count.
///
/// TIMER2's counter is only 16 bits wide, so alarms more than 65 535 ticks in the future
/// cannot be expressed directly in the compare register and must be deferred.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AlarmArmDecision {
    /// The target timestamp is already in the past (or right now); fire immediately.
    FireNow,
    /// The target falls within the next 65 535 ticks; program the compare register directly.
    ArmCompare,
    /// The target is beyond one full counter period; defer and re-evaluate on each overflow.
    Defer,
}

/// Classifies `timestamp` relative to `now` into one of the three alarm-arm decisions.
fn classify_alarm_arm(now: u64, timestamp: u64) -> AlarmArmDecision {
    if timestamp <= now {
        AlarmArmDecision::FireNow
    } else if timestamp - now <= MAX_COUNTER_HORIZON {
        AlarmArmDecision::ArmCompare
    } else {
        AlarmArmDecision::Defer
    }
}

/// Stores the timestamp of the currently-armed (or pending deferred) alarm.
///
/// `u64::MAX` means no alarm is pending.  [`Cell`] interior mutability is safe here
/// because all accesses are gated behind a [`critical_section`].
struct AlarmState {
    timestamp: Cell<u64>,
}

unsafe impl Send for AlarmState {}

impl AlarmState {
    const fn new() -> Self {
        Self {
            timestamp: Cell::new(u64::MAX),
        }
    }
}

/// The singleton time-driver state.
///
/// * `high_word` — software epoch above the 16-bit hardware counter.  Incremented by the
///   TIMER2 overflow ISR every 65 536 hardware ticks (~65.5 ms at 1 MHz).
/// * `alarm` — the single hardware alarm currently programmed (or deferred) in TIMER2 CH0.
/// * `queue` — all pending waker registrations.  Multiple Embassy futures may be sleeping
///   with different deadlines simultaneously; this queue tracks each one.
struct EmbassyTimerDriver {
    high_word: AtomicU32,
    alarm: Mutex<AlarmState>,
    queue: Mutex<RefCell<Queue>>,
}

embassy_time_driver::time_driver_impl!(static DRIVER: EmbassyTimerDriver = EmbassyTimerDriver {
    high_word: AtomicU32::new(0),
    alarm: Mutex::new(AlarmState::new()),
    queue: Mutex::new(RefCell::new(Queue::new())),
});

impl EmbassyTimerDriver {
    fn init(&'static self, cs: CriticalSection) {
        // Reset the software epoch counter and mark "no alarm pending".
        self.high_word.store(0, Ordering::Relaxed);
        self.alarm.borrow(cs).timestamp.set(u64::MAX);

        // Compute the prescaler value to divide the APB1 timer clock down to exactly
        // TICK_HZ (1 MHz) ticks per second.  The hardware counter clock equals
        // timer_clock / (PSC + 1), so PSC = divider − 1.
        let timer_clock_hz = u64::from(timer2_input_clock_hz(DEFAULT_HXTAL_HZ));
        let tick_hz = TICK_HZ;
        let divider = timer_clock_hz / tick_hz;
        if divider == 0 || timer_clock_hz % tick_hz != 0 {
            panic!(
                "TIMER2 clock {}Hz is not divisible by tick rate {}Hz",
                timer_clock_hz, tick_hz
            );
        }
        let psc = divider - 1;
        if psc > u64::from(u16::MAX) {
            panic!("TIMER2 prescaler overflow: {}", psc);
        }
        let psc = psc as u32;

        let rcu = unsafe { Rcu::steal() };
        // Enable the TIMER2 peripheral clock on the APB1 bus.  Without this, any
        // register access to TIMER2 would be undefined behaviour.
        rcu.apb1en().modify(|_, w| w.timer2en().set_bit());
        // Assert then de-assert the peripheral reset line to return TIMER2 to its
        // power-on default state, clearing any prior configuration.
        rcu.apb1rst().modify(|_, w| w.timer2rst().set_bit());
        rcu.apb1rst().modify(|_, w| w.timer2rst().clear_bit());

        let timer = unsafe { Timer2::steal() };
        // Stop the counter (CEN = 0) before reconfiguring.  PSC and CAR are shadow
        // registers; their new values only latch at the next update event (below).
        timer.ctl0().modify(|_, w| w.cen().clear_bit());
        // Disable slave mode (SMS = 0) so the counter free-runs from the internal APB1
        // clock rather than being gated or triggered by an external signal.
        timer.smcfg().modify(|_, w| w.smc().disabled());
        // Write the prescaler shadow register: counter clock = timer_clock / (psc + 1).
        // This value is latched into the active register by the UPG event below.
        timer.psc().write(|w| unsafe { w.bits(psc) });
        // Set the auto-reload register to the full 16-bit range (0xFFFF).  The counter
        // counts 0 → 65535 then wraps to 0, generating an update (overflow) interrupt.
        timer
            .car()
            .write(|w| unsafe { w.bits(TIMER2_COUNTER_MASK) });
        // Reset the hardware counter to 0 so the epoch starts from a clean state.
        timer.cnt().write(|w| unsafe { w.bits(0) });
        // Clear the Channel 0 compare register.  No alarm is scheduled yet.
        timer.ch0cv().write(|w| unsafe { w.bits(0) });

        // Writing UPG in SWEVG generates an immediate software update event.  This
        // forces the timer to latch the PSC and CAR shadow registers right now, so the
        // prescaler and period are active from the very first tick without waiting for
        // the next natural counter overflow.
        timer.swevg().write(|w| w.upg().set_bit());

        // The UPG event above sets UPIF.  Clear all pending flags now so the ISR does
        // not spuriously fire on this artificial initialization overflow.
        Self::clear_interrupt_flags(&timer, timer.intf().read().bits());
        // Enable the update/overflow interrupt (UPIE) so every counter wrap increments
        // high_word.  Channel 0 compare interrupt (CH0IE) starts disabled; set_alarm
        // will enable it when the first wakeup deadline is scheduled.
        timer
            .dmainten()
            .write(|w| w.upie().set_bit().ch0ie().clear_bit());

        // Remove any stale TIMER2 pending interrupt from the NVIC, then unmask the IRQ
        // so hardware can invoke the ISR.
        NVIC::unpend(Interrupt::TIMER2);
        unsafe { NVIC::unmask(Interrupt::TIMER2) };

        // Start the free-running counter.  The driver is now live.
        timer.ctl0().modify(|_, w| w.cen().set_bit());
    }

    fn clear_interrupt_flags(timer: &Timer2, status: u32) {
        // INTF uses write-0-to-clear semantics.  To clear exactly the flags that fired
        // (bits set in `status`) and leave all others untouched, invert `status` so that
        // set bits become 0 (cleared) and unset bits become 1 (no-op), then mask to the
        // clearable range to avoid writing reserved bit fields.
        let clear_bits = (!status) & INTF_CLEARABLE_MASK;
        timer.intf().write(|w| unsafe { w.bits(clear_bits) });
    }

    fn set_alarm(&self, cs: CriticalSection, timestamp: u64) -> bool {
        let timer = unsafe { Timer2::steal() };
        // Persist the timestamp so that on_overflow can re-evaluate this alarm after each
        // 65 536-tick period until it comes within the 16-bit compare window.
        self.alarm.borrow(cs).timestamp.set(timestamp);

        let now = self.now();
        match classify_alarm_arm(now, timestamp) {
            AlarmArmDecision::FireNow => {
                // The target is already past.  Disable the compare interrupt and signal
                // the caller to wake tasks immediately (return false).
                timer.dmainten().modify(|_, w| w.ch0ie().clear_bit());
                self.alarm.borrow(cs).timestamp.set(u64::MAX);
                return false;
            }
            AlarmArmDecision::ArmCompare => {
                // The target is within the next 65 535 ticks.  Write the low 16 bits of
                // the timestamp into CH0CV and enable the CH0 interrupt so it fires when
                // CNT matches.
                timer
                    .ch0cv()
                    .write(|w| unsafe { w.bits((timestamp as u32) & TIMER2_COUNTER_MASK) });
                timer.dmainten().modify(|_, w| w.ch0ie().set_bit());
            }
            AlarmArmDecision::Defer => {
                // The target is more than one full counter period away (> 65 535 ticks).
                // Store the low 16 bits in CH0CV as a hint, but keep CH0IE disabled.
                // on_overflow will call set_alarm again after each overflow until the
                // alarm transitions into the ArmCompare window.
                timer
                    .ch0cv()
                    .write(|w| unsafe { w.bits((timestamp as u32) & TIMER2_COUNTER_MASK) });
                timer.dmainten().modify(|_, w| w.ch0ie().clear_bit());
            }
        }

        // Race-condition guard: between the classify_alarm_arm snapshot above and the
        // register writes just completed, the timer may have ticked past the target.
        // Re-check now(); if the deadline has already passed, treat it as FireNow.
        if timestamp <= self.now() {
            timer.dmainten().modify(|_, w| w.ch0ie().clear_bit());
            self.alarm.borrow(cs).timestamp.set(u64::MAX);
            return false;
        }

        true
    }

    fn on_overflow(&self, cs: CriticalSection) {
        // The 16-bit counter wrapped from 0xFFFF to 0x0000.  Advance the software
        // high word to keep the 64-bit epoch monotonically increasing.
        self.high_word.fetch_add(1, Ordering::Relaxed);
        // Re-evaluate the pending alarm now that now() has advanced by 65 536 ticks.
        // A previously deferred alarm may now be within the ArmCompare window.
        // If set_alarm returns false, the target has already passed — trigger immediately.
        let at = self.alarm.borrow(cs).timestamp.get();
        if at != u64::MAX && !self.set_alarm(cs, at) {
            self.trigger_alarm(cs);
        }
    }

    fn trigger_alarm(&self, cs: CriticalSection) {
        // Drain the waker queue.  next_expiration wakes every entry whose deadline has
        // passed (calling waker.wake() on each) and returns the soonest remaining deadline,
        // or u64::MAX when the queue is empty.  Loop because the returned next deadline
        // may itself be already past under ISR latency, making set_alarm return false again.
        let mut queue = self.queue.borrow(cs).borrow_mut();
        let mut next = queue.next_expiration(self.now());

        while !self.set_alarm(cs, next) {
            next = queue.next_expiration(self.now());
        }
    }
}

impl Driver for EmbassyTimerDriver {
    fn now(&self) -> u64 {
        let timer = unsafe { Timer2::steal() };

        loop {
            // Sample high_word and UPIF both before and after reading CNT.
            // compiler_fences prevent the compiler from reordering these atomic loads
            // and peripheral reads relative to one another.
            let high_before = self.high_word.load(Ordering::Relaxed);
            compiler_fence(Ordering::Acquire);
            // UPIF is set by hardware the moment the counter wraps to 0, but high_word
            // is only incremented when the ISR subsequently runs.  Reading UPIF before
            // CNT lets us detect an in-flight overflow not yet serviced by the ISR.
            let upif_before = timer.intf().read().upif().bit_is_set();
            let cnt = timer.cnt().read().cnt().bits();
            let upif_after = timer.intf().read().upif().bit_is_set();
            compiler_fence(Ordering::Acquire);
            let high_after = self.high_word.load(Ordering::Relaxed);

            // If high_word changed (ISR ran between our two loads) or UPIF toggled (flag
            // set or cleared mid-read), the CNT we captured may straddle two different
            // epochs.  Discard this sample and retry for a coherent snapshot.
            if high_before != high_after || upif_before != upif_after {
                continue;
            }

            // UPIF is set but the ISR has not yet run to increment high_word.  Adding 1
            // here ensures now() is monotonically increasing even between an overflow and
            // its ISR, e.g. when called from task context right after the counter wraps.
            let high = if upif_after {
                high_before.wrapping_add(1)
            } else {
                high_before
            };

            let cnt = cnt & TIMER2_COUNTER_MASK;
            return ((high as u64) << TIMER2_COUNTER_BITS) | u64::from(cnt);
        }
    }

    fn schedule_wake(&self, at: u64, waker: &core::task::Waker) {
        critical_section::with(|cs| {
            let mut queue = self.queue.borrow(cs).borrow_mut();
            // Enqueue the waker.  A true result means the queue requires us to recompute
            // its next expiration and re-arm the hardware alarm.  It does not necessarily
            // mean that the globally earliest deadline changed.
            if queue.schedule_wake(at, waker) {
                // Find and arm the new earliest deadline.  Loop because set_alarm may
                // return false if that deadline is already past, advancing the queue again.
                let mut next = queue.next_expiration(self.now());
                while !self.set_alarm(cs, next) {
                    next = queue.next_expiration(self.now());
                }
            }
        });
    }
}

/// Initialise the time driver.  Call exactly once at startup, before any Embassy tasks,
/// `Instant`, or `Timer` usage.
pub fn init() {
    critical_section::with(|cs| DRIVER.init(cs));
}

/// TIMER2 interrupt service routine entry point.
///
/// Must be called from the device's `TIMER2` exception handler.  Services both the
/// counter-overflow (UPIF) and compare-match (CH0IF) interrupt events.
pub fn on_interrupt() {
    let timer = unsafe { Timer2::steal() };

    critical_section::with(|cs| {
        // Snapshot the interrupt-status (which flags fired) and interrupt-enable (which
        // channels were armed) registers together for a consistent view of this ISR invocation.
        let status = timer.intf().read().bits();
        let enabled = timer.dmainten().read().bits();

        // Clear all active flags before handling them.  Because INTF is write-0-to-clear,
        // clearing first is safe: any new event that arrives after the clear will re-set its
        // flag for the next ISR call, so no events are silently dropped.
        EmbassyTimerDriver::clear_interrupt_flags(&timer, status);

        // Update (overflow) event: the 16-bit counter wrapped from 0xFFFF to 0x0000.
        if (status & INTF_UPIF) != 0 {
            DRIVER.on_overflow(cs);
        }

        // Compare-match event: CNT reached the value programmed in CH0CV.
        // Guard with CH0IE to ignore a stale CH0IF flag that may linger from when the
        // interrupt was disabled (e.g. during a Defer → ArmCompare transition where the
        // compare value coincidentally matched the running counter).
        if (status & INTF_CH0IF) != 0 && (enabled & DMAINTEN_CH0IE) != 0 {
            DRIVER.trigger_alarm(cs);
        }
    });
}

Prefer my writing delivered to your inbox? Subscribe here.

Correspondence

Reader Comments & Discussion

No comments yet. Be the first to share your thoughts.

Join the Discussion

Kept private
Live Preview Markdown & LaTeX

Start typing to see how your comment will render.

Complete the security check above to enable submission

Comments are moderated and will appear after review.

© 2025 Ashwin Narayan. All rights reserved.