micropython-timers-hardware-machine-timer

Timers in MicroPython for Running Periodic Tasks

  • 5 min

A timer is a mechanism that executes a function when a time interval expires. Depending on the MicroPython port, it may be backed by hardware or managed as a virtual timer.

So far, we have learned two ways to manage time:

  1. time.sleep(): “Take a nap and do nothing else”. (Blocking).
  2. uasyncio: “Work a bit, pause, and let someone else work”. (Cooperative).

What if we need to execute a task at a more regular cadence than what sleep() provides? What if we want an LED to blink every 100 ms while the main program continues working?

That’s what timers are for.

A hardware timer uses an internal counter of the microcontroller. When it reaches the configured value, it generates an event that ends up executing our callback. The exact instant when the code runs depends on the port and the type of interrupt, so we must not confuse a stable cadence with strict real-time.

The machine.Timer Class

In MicroPython, controlling these peripherals is very straightforward thanks to the Timer class.

We need to define three things:

  1. Period (or Frequency): How often the alarm should trigger.
  2. Mode: Should it trigger once (One Shot) or repeat indefinitely (Periodic)?
  3. Callback: What function should execute when the alarm fires?

Example: Metronome with a Periodic Timer

Let’s make an LED blink exactly at 5 Hz (5 times per second) using a Timer, leaving the main loop (while True) completely free.

from machine import Timer, Pin
import time

# 1. Define the hardware
led = Pin(2, Pin.OUT)

# 2. Define the Callback function (what the Timer will do)
# IMPORTANT: The function must accept one argument (the timer itself)
def blink(timer):
    led.toggle()

# 3. Initialize the Timer
# This example uses timer 0 of an ESP32
tim = Timer(0)

# Configure: Frequency 5Hz, Periodic Mode
tim.init(freq=5, mode=Timer.PERIODIC, callback=blink)

print("Timer started. The LED blinks on its own.")

# 4. Main loop
# Notice that we do NOTHING with the LED here.
# We could be doing complex calculations and the LED would keep its rhythm.
counter = 0
while True:
    print(f"The main program continues its business... {counter}")
    counter += 1
    time.sleep(1)
Copied!

If you run this, you will see the LED blinking frantically while the console prints messages slowly every second. They are two independent processes.

Example: Countdown with ONE_SHOT

Sometimes we don’t want something repetitive, but rather something delayed. “Turn on the water pump and turn it off automatically after 10 seconds”.

For this, we use the Timer.ONE_SHOT mode.

from machine import Timer, Pin

relay = Pin(15, Pin.OUT)
tim_shutdown = Timer(1) # We use another timer ID

def shut_down_system(timer):
    relay.value(0)
    print("System shut down for safety.")

# Turn on
relay.value(1)
print("Starting system... it will turn off in 5 seconds.")

# Configure the timer to fire only once after 5000ms
tim_shutdown.init(period=5000, mode=Timer.ONE_SHOT, callback=shut_down_system)
Copied!

Differences Between Boards (ESP32 and Pico)

Here MicroPython has some differences depending on the chip:

  • ESP32: Uses hardware timers with identifiers starting from 0. The available number depends on the model: there might be one, two, or four. The ESP32 port runs its callbacks as soft IRQs and does not support virtual timers.
  • Raspberry Pi Pico (RP2040): The RP2 port only offers virtual timers. They are initialized with Timer(-1) or, in versions that allow omitting the identifier, with Timer().
# Virtual timer for the RP2 port
tim = Timer(-1)
tim.init(period=1000, mode=Timer.PERIODIC, callback=my_function)
Copied!

ISR Rules

The callback of a timer can execute as a hard or soft interrupt, depending on the port. In a hard interrupt, memory cannot be allocated; a soft interrupt allows it, but introduces some extra latency and can be delayed by the garbage collector.

Since the same code may end up on different boards, it is wise to write short callbacks with few dependencies:

  1. Speed: The function must be very brief. Toggle an LED, change a global variable… and exit. No time.sleep inside.
  2. Memory: On many boards, you cannot allocate memory inside the callback.
  • Avoid creating new lists, dictionaries, or strings.
  • Avoid using print() if possible (use a buffer), although it usually works on ESP32 because it is powerful.
  • If there is an error inside the Timer, sometimes the program says nothing and simply stops.

If you need to do something complex (like sending an MQTT message) when the Timer fires, do not do it in the callback. Use the callback to raise a “flag” (global variable) and let the main loop (asyncio or while) process that heavy task.

Disabling a Timer

If you want to stop that frantic blinking, simply call deinit().

tim.deinit()
Copied!

Timer or uasyncio?

That is the big question. Both seem to do things at the same time.

  • Use uasyncio for the general program logic: reading sensors, waiting for web responses, controlling complex flows. It is easier to debug and does not have the memory restrictions of interrupts.
  • Use Timer when you need a strict and guaranteed frequency (audio sampling, signal generation, refreshing multiplexed displays) or for safety tasks like a “Watchdog” that must occur even if the main program hangs.