Skip to content

rp2/machine_timer: Support hard IRQ timer callbacks. #17363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 16, 2025

Conversation

arachsys
Copy link
Contributor

@arachsys arachsys commented May 26, 2025

Summary

When using Timer on the pyboard (and stm32 more generally), the callback is run in hard IRQ context and is reliably punctual.

On a Pi Pico 2 board, I was surprised to find that the rp2 port instead queues timer callbacks via mp_sched_schedule() which means they are subject to GC delays and potentially unbounded jitter.

This probably makes sense as an 'easy mode' default, allowing allocation and less disciplined code in a timer callback, but having it as the only option makes tight timing more awkward on rp2 than it is on stm32. Searching discussions, some have worked around the unpunctual timers by repurposing a PWM output with a hard trigger IRQ. A PIO state machine in an irq() loop with a hard IRQ handler would also do the job, but in each case we're wasting limited resources and adding complexity.

Rather than hacking around the problem once again, it feels sensible to add a (non-default) option to make the rp2 timers dispatch callbacks in hard IRQ context in the same way stm32 timers do.

Add a hard= parameter to the Timer() constructor and init, matching the hard= parameter on Pin.irq() and rp2.DMA.irq() and rp2.PIO.irq(). When set true, this dispatches the callback in hard IRQ context in the same way as the other hard IRQ handlers for rp2 and the timer callbacks for stm32, taking the same precautions to lock the scheduler and GC, catch unhandled exceptions, and disable the callback on unhandled exceptions.

Does this make sense? I've tested this reasonably heavily in my own application without uncovering any problems (see below for a demo) but would be grateful for any thoughts. For example, should this patch make the behaviour configurable in the other ports at the same time? Obviously, I'll update the patch to update the documentation to match wherever it ends up as well.

Testing

To demonstrate the GC jitter with the default soft callbacks on a Pi Pico or Pico 2, try:

from machine import Pin, Timer

pin = machine.Pin(0, machine.Pin.OUT)
timer = Timer(freq = 1000, mode = Timer.PERIODIC, callback = lambda t: pin.toggle())

x = 0.3
while True:
    x = 4 * x * (1 - x)

with a scope on pin GP0. The fp loop allocates and gcs heavily, so you'll see the 500Hz square wave often lengthen out from 1 ms to 2-4 ms when the callback is delayed.

With this patch and the new hard callbacks, this effect disappears and the jitter is down in the single digit microseconds:

from machine import Pin, Timer

pin = machine.Pin(0, machine.Pin.OUT)
timer = Timer(freq = 1000, mode = Timer.PERIODIC, callback = lambda t: pin.toggle(), hard = True)

x = 0.3
while True:
    x = 4 * x * (1 - x)

I've tested with multiple concurrent timers of disjoint frequencies up to 20kHz (pushing towards 100% utilisation in interrupt handling) on a variety of different RP2350 boards.

Copy link

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
     mimxrt:    +0 +0.000% TEENSY40
        rp2:  +136 +0.015% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@peterhinch
Copy link
Contributor

An enthusiastic +1 here.

@Josverl
Copy link
Contributor

Josverl commented May 26, 2025

Looks like a good addition for a limited increase in size.
Is this already covered by the existing tests, or is a new test needed?

@arachsys
Copy link
Contributor Author

arachsys commented May 27, 2025

Thanks @peterhinch and @Josverl.

I confess I don't know much about MicroPython's automatic testing infrastructure, but it looks like generic 'soft timers' (without a hardware ID) are tested in https://github.com/micropython/micropython/blob/master/tests/extmod/machine_soft_timer.py - so there is at least coverage that the rp2 hard= parameter doesn't break the existing behaviour.

Apart from the generic timer skip for esp32 and esp8266 at the top of this, it looks like there's no port-specific timer testing at the moment, e.g. for the tick_hz argument available only on some ports. I'm not sure what @dpgeorge would think about adding a port-specific tests/ports/rp2/rp2_machine_timer.py as part of this patch vs creating that as a separate change - very happy to fit in with whatever the preferred option is.

What we do with generic vs port-specific testing might also depend on whether you want the hard= option for any other ports that are currently soft-irq only.

@Josverl
Copy link
Contributor

Josverl commented May 27, 2025

adding a port-specific tests/ports/rp2/rp2_machine_timer.py as part of this patch

In order to ensure coverage, and make sure that the feature remains operational in the future, an additional test would be needed.
I tend to use separate commits for code, tests and documentation.

I would start with a port specific test, but possibly that could be lifted to a port-agnostic test if/when other ports opt-in to this new keyword.

Copy link

codecov bot commented May 28, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.55%. Comparing base (cfcc53d) to head (8162468).
Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #17363   +/-   ##
=======================================
  Coverage   98.55%   98.55%           
=======================================
  Files         169      169           
  Lines       21946    21946           
=======================================
  Hits        21628    21628           
  Misses        318      318           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@arachsys arachsys force-pushed the rp2/hardtimer branch 2 times, most recently from 9bcbd1c to f8fe705 Compare May 28, 2025 10:44
@arachsys
Copy link
Contributor Author

arachsys commented May 28, 2025

I've added tests for both this hard= option and the existing rp2-specific tick_hz= option. This means all the existing rp2-specific timer functionality is now tested as well. I've also documented the ability to choose between soft- vs hard-IRQ context and rebased against the current HEAD of the master branch.

@arachsys arachsys changed the title [RFC] rp2/machine_timer: Support hard IRQ timer callbacks. rp2/machine_timer: Support hard IRQ timer callbacks. May 30, 2025
@arachsys
Copy link
Contributor Author

Rebased against the current HEAD of the master branch again.

@dpgeorge
Copy link
Member

Thanks for the contribution, this is a very nice enhancement!

The Timer class is a bit inconsistent across ports, as you have noticed. We can't fix that here, but at least we can try not to make things any worse.

One question is whether Timer should have an irq() method to match other classes like Pin. Then the hard argument would be easy to add. Well, the fact that the Timer constructor already takes a callback argument, and usually the only thing you want to do (or can do) with a timer is to have it periodically call a callback, I guess adding hard argument alongside the callback argument makes the most sense.

Overall, I think this PR has got it right in terms of API decision, ie simply add the hard argument to the constructor/init method. Eventually that can be extended to other ports pretty easily. And then one day we can make things fully consistent across ports and have a common test. But for now this PR is a good step forward.

@dpgeorge dpgeorge added this to the release-1.26.0 milestone Jun 16, 2025
@robert-hh
Copy link
Contributor

And then one day we can make things fully consistent across ports

Quite a few ports use Softimer. At a quick glance it seems to be alif, mimxrt, renesas, samd and stm32.

@dpgeorge
Copy link
Member

Quite a few ports use Softimer.

We can add hard to those ports (in a separate PR). But it needs to remain backwards compatible, eg on stm32 it would need to be hard=True by default.

@arachsys
Copy link
Contributor Author

I've pushed a corrected version with all the fixes you spotted - thanks!

If you're happy with the API choice, I'm very happy to have a go at a follow-up PR generalising this to the other ports, preserving the current default for hard vs soft in each case?

arachsys added 3 commits June 16, 2025 12:35
Unlike some boards like stm32, timer callbacks on the rp2 port are
unconditionally dispatched via mp_sched_schedule(), behaving like
soft IRQs with consequent GC jitter and delays.

Add a 'hard' keyword argument to the rp2 Timer constructor and init.
This defaults to False but if it is set True, the timer callback will
be dispatched in hard IRQ context rather than queued.

Signed-off-by: Chris Webb <[email protected]>
Add tests for both one-shot and periodic timers using the rp2-specific
tick_hz= and hard= parameters.

Signed-off-by: Chris Webb <[email protected]>
Copy link
Member

@dpgeorge dpgeorge left a comment

Choose a reason for hiding this comment

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

Thanks for updating, this looks good now.

And thanks for making clean commits with good git commit messages! Makes it easier to just merge.

@dpgeorge dpgeorge merged commit 8162468 into micropython:master Jun 16, 2025
32 checks passed
@dpgeorge
Copy link
Member

If you're happy with the API choice, I'm very happy to have a go at a follow-up PR generalising this to the other ports, preserving the current default for hard vs soft in each case?

Yes, feel free to do that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants