Graeme Winter

Times are Hard

The RP2040 has a build in frequency counter, as documented in §2.15.4 of the data sheet. This is a nice, simple device which can be easily accessed from µPython by poking a few registers, and essentially counts the number of pulses on clksrc_gpin0 over a few ms, as clocked against clk_ref. Using this is simple:

# Usage: connect a jumper lead from GPIO0 to GPIO20 i.e.
# pin 1 to pin 26

from machine import mem32, Pin
import rp2


# register bases
CLOCKS_BASE = 0x40008000
IO_BANK0_BASE = 0x40014000


# standard square wave PIO program, counts down from content of
# OSR, with overhead of 2 => set the register as 2 fewer counts
# than wanted for high / low i.e. full period is 2 x (this + 2)
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW)
def square():
    wrap_target()
    mov(x, osr).side(1)
    label("high")
    jmp(x_dec, "high")
    mov(x, osr).side(0)
    label("low")
    jmp(x_dec, "low")
    wrap()


# output 1MHz square wave - to get that need to set high / low to
# 50 counts - 2 => 48 counts and set PIO frequency to 100 MHz
p0 = machine.Pin(0, machine.Pin.OUT)
sm0 = rp2.StateMachine(0, square, freq=125_000_000, sideset_base=p0)
sm0.put(5 - 2)
sm0.exec("pull()")

# square wave on
sm0.active(1)

# set GPIO20 to mode 8 i.e. frequency counter in, zero
mem32[IO_BANK0_BASE | 0xA4] = 0x8

# set up frequency counter - CLK_REG is 6MHz I believe
mem32[CLOCKS_BASE | 0x80] = 6000
mem32[CLOCKS_BASE | 0x8C] = 4

# count for 1ms
mem32[CLOCKS_BASE | 0x90] = 10

# read frequency from IN0
mem32[CLOCKS_BASE | 0x94] = 0x6

while mem32[CLOCKS_BASE | 0x98] & (1 << 8):
    pass

nn = mem32[CLOCKS_BASE | 0x9C]
khz = nn >> 4
frc = nn & 0xF
print(f"{khz + frc / 16:.1f} kHz")

# disable square output
sm0.active(0)

The frustration with this is the integration time is very limited which constrains how well you can use the counter. The benefit is that the counting is not managed by software, so you can go close to or above the core CPU frequency and still get useful results.

If going that fast is not pertinent, but may want to integrate for longer: this is impossible. Two choices then - use the PWM as a counter (limited to 16 bits, unless you want to mess around with interrupts) or write something fun on the PIO. Of course, the PIO wins.

PIO Frequency Counting

Ultimately this is very simple: over a reliably determined interval, count the number of rising edges: the PIO wait instruction is great for this - wait for an input to go high, then low, then increment counter and check if the integration time is exceeded and if it is, return. There are nuances to this, not least how to gate the counter to only run for e.g. one second. Guess what? Just use another PIO state machine for this and gate on a GPIO.

Code

# Frequency counter with example signal generator
#
# Counts pulses in on GPIO20 (used as this is GCLKIN) gated for
# 1s based on second PIO program raising LED. Includes GPIO drive
# test program at 1MHz on GPIO0

from machine import Pin
import rp2
import time


@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW)
def gate():
    pull()
    mov(x, osr).side(1)
    label("high")
    jmp(x_dec, "high")
    nop().side(0)
    label("halt")
    jmp("halt")


@rp2.asm_pio()
def high():
    mov(x, null)
    label("entry")
    jmp(pin, "start")
    jmp("entry")
    label("start")
    wait(1, pin, 0)
    wait(0, pin, 0)
    jmp(x_dec, "next")
    label("next")
    jmp(pin, "start")
    mov(isr, x)
    push()
    label("halt")
    jmp("halt")


@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW)
def square():
    wrap_target()
    mov(x, osr).side(1)
    label("high")
    jmp(x_dec, "high")
    mov(x, osr).side(0)
    label("low")
    jmp(x_dec, "low")
    wrap()


p0 = machine.Pin(0, machine.Pin.OUT)
sm0 = rp2.StateMachine(0, square, freq=10_000_000, sideset_base=p0)
sm0.put(5 - 2)
sm0.exec("pull()")

sm0.active(1)

# Pin(gate) and Pin(count)
pg = Pin(25)
pc = Pin(20, Pin.IN)

sm6 = rp2.StateMachine(6, gate, sideset_base=pg)
sm6.put(125_000_000 - 2)

sm7 = rp2.StateMachine(7, high, jmp_pin=pg, in_base=pc)

sm7.active(1)
sm6.active(1)

count = int((0xFFFFFFFF - sm7.get()))

sm6.active(0)
sm7.active(0)

print(f"{count} Hz")
sm0.active(0)

Discussion

Want the PIO programs to halt at the end of execution, so implement HCF as the last instruction:

    label("halt")
    jmp("halt")

Other than that, we want to start the state machines, switch on the gate, count for 1s (or as long as you like) then switch off gate and read out counts.

Results

Works well - integrating for longer than a second by counting slower on sm6 is easy (100s, say) - connecting GPIO0 to GPIO20 via jumper reports 1000000.000Hz when integrating for 100s and printing out 0.01 * the total count. Connecting to a π0 running a PLL clock to a GPIO via a clock divider, which should also give 1MHz, gives 1000007.813Hz, showing that the clocks are ~ 8ppm difference between these two devices. Telling the time is hard.

Additional

Curious about what the clock speed is on your ROSC, compared with the system clock?

# Frequency counter with example signal generator
#
# Counts pulses in on GPIO20 (used as this is GCLK_GPIN0) gated for
# 1s based on second PIO program raising LED. Connect GPIO 20 to 
# 21 to connect CLK_GPOUT0 to GCLK_GPIN0.

from machine import Pin, mem32
import rp2
import time

# base registers
CLOCKS_BASE = 0x40008000
IO_BANK0_BASE = 0x40014000
ROSC_BASE = 0x40060000

# set up CLK_GPOUT0 to point at ROSC
mem32[CLOCKS_BASE | 0x4] = 1 << 8
mem32[CLOCKS_BASE | 0x0] = (0x1 << 11) | (0x4 << 5)

# set up clock out - ROSC into low mode
# 0xfa5 → MEDIUM
# 0xfa7 → HIGH
mem32[ROSC_BASE | 0] = 0xFA4

# set GPIO21 to mode 8 i.e. clock out
mem32[IO_BANK0_BASE | 0xAC] = 0x8


@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW)
def gate():
    pull()
    mov(x, osr).side(1)
    label("high")
    jmp(x_dec, "high")
    nop().side(0)
    label("halt")
    jmp("halt")


@rp2.asm_pio()
def high():
    mov(x, null)
    label("entry")
    jmp(pin, "start")
    jmp("entry")
    label("start")
    wait(1, pin, 0)
    wait(0, pin, 0)
    jmp(x_dec, "next")
    label("next")
    jmp(pin, "start")
    mov(isr, x)
    push()
    label("halt")
    jmp("halt")


# Pin(gate) and Pin(count)
pg = Pin(25)
pc = Pin(20, Pin.IN)

sm6 = rp2.StateMachine(6, gate, freq=1_000_000, sideset_base=pg)
sm6.put(100_000_000 - 2)

sm7 = rp2.StateMachine(7, high, jmp_pin=pg, in_base=pc)

sm7.active(1)
sm6.active(1)

count = int((0xFFFFFFFF - sm7.get()))

sm6.active(0)
sm7.active(0)

print(f"{0.01 * count:.3f} Hz")

Integrates for 100s before printing out the average frequency: for me, above, 5777972.5 Hz.