Graeme Winter

Count Zero Interrupt

Poking around inside the M0 documentation found the system ticks - a simple clock which counts down from some value to zero then restarts, triggering an interrupt on reaching zero. Yes, this is a count zero interrupt; sprawl style. Default clock is set to be 1MHz (boooring) but poking a few registers in the rp2040 we can make this clock at the system frequency which means we have a counter going down at 125MHz.

Accessing this from µPython is effectively impossible interrupt wise, but is simple if a little inline assembly is used:

from machine import mem32

@micropython.asm_thumb
def systick_wait(r0):
    ldr(r1, [r0, 0])
    ldr(r2, [r0, 0])
    sub(r0, r1, r2)

PPB_BASE = 0xe0000000
SYST_CSR = PPB_BASE | 0xe010
SYST_CVR = PPB_BASE | 0xe018

# enable bit 0; count off system clock bit 2
mem32[SYST_CSR] = 0x5
b = systick_wait(SYST_CVR)
mem32[SYST_CSR] = 0x0
print(b)

Tells me that the time delta between ldr 1 and ldr 2 is … 2 cycles.

The Interrupt

This requires a little more work - the pointer for this interrupt is position 0xf in VTOR (i.e. one stop before the usual user interrupts) so need to play the drive-by game to have this accessible from µPython:

#include "py/dynruntime.h"

unsigned int original_handler;
unsigned int original_handler_set;
unsigned int systick;

#define VTOR_ADDR 0xe000ed08
#define SYST_CVR 0xe000e018

void irq(void) {
  // grab the time delta
  systick = *(unsigned int *)SYST_CVR;

  // toggle GPIO 25 as a sign of life
  *(unsigned int *)0xd000001c = 0x1 << 25;
}

STATIC mp_obj_t irq_init(void) {
  if (original_handler_set == 0) {
    unsigned int *VTOR = *(unsigned int **)VTOR_ADDR;
    original_handler = VTOR[0xf];
    VTOR[0xf] = (unsigned int)&irq;
    original_handler_set = 1;
  }
  return mp_obj_new_int(0);
}

STATIC mp_obj_t irq_deinit(void) {
  if (original_handler_set == 1) {
    unsigned int *VTOR = *(unsigned int **)VTOR_ADDR;
    VTOR[0xf] = original_handler;
    original_handler_set = 0;
  }
  return mp_obj_new_int(systick);
}

STATIC MP_DEFINE_CONST_FUN_OBJ_0(irq_init_obj, irq_init);
STATIC MP_DEFINE_CONST_FUN_OBJ_0(irq_deinit_obj, irq_deinit);

mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw,
                  mp_obj_t *args) {
  MP_DYNRUNTIME_INIT_ENTRY

  original_handler_set = 0;

  mp_store_global(MP_QSTR_init, MP_OBJ_FROM_PTR(&irq_init_obj));
  mp_store_global(MP_QSTR_deinit, MP_OBJ_FROM_PTR(&irq_deinit_obj));

  MP_DYNRUNTIME_INIT_EXIT
}

This saves the systick value to a register, then toggles GPIO25 as a sign of life. The last delta is available on deinit; I could add a get() function here. Usage:

from machine import mem32, Pin
import time

import systick

led = Pin(25, Pin.OUT)

# enable interrupt handler
systick.init()

PPB_BASE = 0xE0000000
SYST_CSR = PPB_BASE | 0xE010
SYST_RVR = PPB_BASE | 0xE014
SYST_CVR = PPB_BASE | 0xE018

mem32[SYST_RVR] = 12_499_999

mem32[SYST_CSR] = 0x7
time.sleep(10)
mem32[SYST_CSR] = 0x0

print(12_499_999 - systick.deinit())

Sets up LED GPIO, sets up counting and then arms IRQ. This tells me the call takes about 22 cycles. Additionally: do not mem32[SYST_CSR] = 0x7 without systick.init() because this will go to the default interrupt handler, which is usually HCF loop.