Skip to content

atmega: add opt-in Timer0 time HAL for ATmega328P - #1087

Open
matpala wants to merge 2 commits into
ZigEmbeddedGroup:mainfrom
matpala:atmega/time
Open

matpala wants to merge 2 commits into
ZigEmbeddedGroup:mainfrom
matpala:atmega/time

Conversation

@matpala

@matpala matpala commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Adds hal.time for the ATmega328P using Timer0. Part of #1069; the timer is opt-in, as discussed there.

  • Provides init, get_time_since_boot, sleep_ms and sleep_us. Time is measured from init.
  • Timer0 runs in normal mode with a /64 prescaler. Its overflow interrupt extends the count to 64 bits.
  • The application registers hal.time.timer0_overflow for TIMER0_OVF, calls init and enables interrupts. init checks the handler registration at compile time. AVR does not use HAL default_interrupts, so registration is explicit. Without initialization, Timer0 remains available for PWM on D5/D6.
  • The CPU frequency comes from the board at compile time. Each tick must span a whole power-of-two number of microseconds: 4 µs at 16 MHz or 8 µs at 8 MHz. Unsupported frequencies produce a compile error. Includes host tests for find_us_per_tick.
  • Reads preserve the interrupt state and account for a pending overflow by rereading TCNT0. Sleeps busy-wait with an extra tick of margin; sleep_ms uses 1 ms chunks to avoid runtime 64-bit multiplication. Sleeps require interrupts enabled. The overflow handler must run within one overflow period: 1.024 ms at 16 MHz or 2.048 ms at 8 MHz.
  • Adds blinky_time for the Nano, Pro Mini 3.3 V and RF-Nano V3.

Hardware-tested on an Emakefun RF-Nano V3.0 (16 MHz) and an Arduino Pro Mini 3.3 V (8 MHz). The example toggles the LED with sleep_ms(500) between toggles. On each board, I counted 60 LED turn-ons over a manually timed 60-second interval.

Inspected the example ELFs: vector 16 points to the Timer0 overflow handler, which returns with reti, and no compiler_rt helpers are linked.

Comment thread port/microchip/atmega/src/hals/atmega328p/time.zig Outdated
Comment thread port/microchip/atmega/src/hals/atmega328p/time.zig Outdated
Comment thread port/microchip/atmega/src/hals/atmega328p/time.zig Outdated
overflows = 0;
TC0.TIFR0.write(.{ .TOV0 = 1, .OCF0A = 0, .OCF0B = 0 }); // writing 1 clears it
TC0.TIMSK0.write(.{ .TOIE0 = 1, .OCIE0A = 0, .OCIE0B = 0 });
TC0.TCCR0B.write(.{ .CS0 = .RUNNING_CLK_64, .WGM02 = 0, .FOC0B = 0, .FOC0A = 0 });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 64 prescaler? At 8MHz that gives a 2uS-ish granularity. With 32 we get a nice 1uS-ish. Have you looked at how many cycles the overflow interrupt takes? Obviously with 32 we'd be paying that cost twice as often.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this isn't connected to prescaler at all which puts us as risk of someone configuring that global while it not actually having any bearing on the tick frequency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timer0 supports /1, /8, /64, /256 and /1024; /32 and /128 are available on Timer2. With /64, ticks are 8 µs at 8 MHz and 4 µs at 16 MHz. /8 gives 1 µs at 8 MHz, but 0.5 µs at 16 MHz, which this HAL's whole-microsecond tick conversion doesn't support.

The ReleaseSmall disassembly gives 86 cycles for the ISR, or 93 including interrupt entry and the vector jmp. That's about 0.57% CPU with /64 versus 4.54% with /8. I'd keep /64.

On the second point: good catch. CS0 now derives from prescaler through a comptime switch, and unsupported Timer0 divisors cause a compile error.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, I assumed there was /32 in timer0. Two uS should be fine.

Comment thread port/microchip/atmega/src/hals/atmega328p/time.zig Outdated

/// Microseconds per tick at `cpu_frequency`, or null if that isn't a whole
/// power of two.
fn find_us_per_tick(cpu_frequency: u32) ?u32 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could make this explicitly comptime

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually shouldn't matter, we can make the one call that isn't a test an explicitly comptime call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, made cpu_frequency explicitly comptime.

@mattnite

mattnite commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Wait so you sat there for an entire minute counting the number of times the LED blinked?

@matpala

matpala commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Guilty. Most suspenseful minute of my week :) It only catches gross errors though (a wrong prescaler or tick size is 2x off), so I measured it properly too.

A test firmware prints get_time_since_boot() over UART every 10 s, and a script fits those values against the host clock as each line arrives (the host clock agrees with Windows' NTP-synced clock to about 4 ppm). To tell lost ticks apart from the board's oscillator, a second firmware counts Timer0 overflows by polling TOV0 with interrupts off, without hal.time. A lost overflow would make hal.time run slower than the polling count.

Board hal.time (10 min) Polling, no interrupts (5 min)
RF-Nano V3.0, 16 MHz −1893 ppm −1889 ppm
Pro Mini 3.3 V, 8 MHz −3844 ppm −3878 ppm

Both methods agree on each board, so hal.time doesn't lose overflows; the error is each board's oscillator (0.19% and 0.38% slow, typical of ceramic resonators). The remaining differences are small and in the opposite direction from lost ticks; most likely the oscillator drifting with temperature between runs.

Happy to share the firmware and the script if they're useful.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants