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.
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.