Graeme Winter

What’s the Frequency?

SAMD51 includes a frequency counter (FREQM) which I wanted to have a look at. Turns out it is a wonderland-like rabbit hole full of clocks. Let’s enter.

Input signal

To test this, use a Raspberry Pi pico with PWM, set to 50% duty cycle, 1.25MHz:

Oscilloscope trace

This is reading back as a steady 1.25002MHz on the ‘scope frequency counter.

Prelude: CMSIS

CMSIS is a standard way of expressing register configurations etc. based on an XML document: this is used under the hood in Arduino to define the header structures which allow access to the registers as defined in the data sheet, without manually calculating addresses and offsets. Reading the same XML documents to transform to a simple ucstruct representation is pretty simple:

import sys
import os
from xml.etree import ElementTree

tree = ElementTree.parse(sys.argv[1])

device = tree.getroot()

known = {}

os.mkdir(device.find("name").text)
os.chdir(device.find("name").text)

for thing in device.findall("./peripherals/peripheral"):
    tname = thing.find("name")

    base = thing.find("baseAddress").text

    f = open(f"{tname.text}.py", "w")
    f.write("import uctypes\n\n")

    for cluster in thing.findall("./registers/cluster"):
        cname = cluster.find("name").text
        cdim = cluster.find("dim")

        if cdim is None:
            continue

        assert "[%s]" in cname
        cname = cname.replace("[%s]", "")
        f.write("%s = {\n" % cname)

        for register in cluster.findall("register"):
            name = register.find("name")
            dim = register.find("dim")
            size = register.find("size").text
            offset = register.find("addressOffset").text
            if dim is None:
                assert not "%s" in name.text
                f.write(f'    "{name.text}": {offset} | uctypes.UINT{size},\n')
            else:
                assert "%s" in name.text
                text = name.text.replace("[%s]", "")
                f.write(
                    f'    "{text}": ({offset} | uctypes.ARRAY, {dim.text} | uctypes.UINT{size}),\n'
                )

        f.write("}\n\n")

    f.write("%s = {\n" % tname.text)
    known[tname.text] = thing

    if thing.get("derivedFrom"):
        assert thing.get("derivedFrom") in known
        thing = known[thing.get("derivedFrom")]

    for register in thing.findall("./registers/register"):
        name = register.find("name")
        dim = register.find("dim")
        size = register.find("size").text
        offset = register.find("addressOffset").text
        if dim is None:
            assert not "%s" in name.text
            f.write(f'    "{name.text}": {offset} | uctypes.UINT{size},\n')
        else:
            assert "%s" in name.text
            text = name.text.replace("[%s]", "")
            f.write(
                f'    "{text}": ({offset} | uctypes.ARRAY, {dim.text} | uctypes.UINT{size}),\n'
            )

    for cluster in thing.findall("./registers/cluster"):
        cname = cluster.find("name").text
        cdim = cluster.find("dim")

        if not cdim is None:
            offset = cluster.find("addressOffset").text
            text = cname.replace("[%s]", "")
            f.write(f'    "{text}": ({offset} | uctypes.ARRAY, {cdim.text}, {text}),\n')
            continue

        for register in cluster.findall("register"):
            name = register.find("name")
            dim = register.find("dim")
            size = register.find("size").text
            offset = register.find("addressOffset").text
            if dim is None:
                assert not "%s" in name.text
                f.write(f'    "{name.text}": {offset} | uctypes.UINT{size},\n')
            else:
                assert "%s" in name.text
                text = name.text.replace("[%s]", "")
                f.write(
                    f'    "{text}": ({offset} | uctypes.ARRAY, {dim.text} | uctypes.UINT{size}),\n'
                )
    f.write("}\n\n")
    f.write(
        f"{tname.text.lower()} = uctypes.struct({base}, {tname.text})\n"
    )
    f.close()

This will generate one Python file for every peripheral, which embeds the addresses of registers and presents a simpler API, for example for the true random number generator:

import uctypes

TRNG = {
    "CTRLA": 0x0 | uctypes.UINT8,
    "EVCTRL": 0x4 | uctypes.UINT8,
    "INTENCLR": 0x8 | uctypes.UINT8,
    "INTENSET": 0x9 | uctypes.UINT8,
    "INTFLAG": 0xA | uctypes.UINT8,
    "DATA": 0x20 | uctypes.UINT32,
}

trng = uctypes.struct(0x42002800, TRNG)

This does no reduce the need for datasheet reading, but does mean you can write the interfaces without too much manual messing around with mem32. from TRNG import trng will then give you access to the registers. These can be compiled to .mpy format and used natively using mpy-cross.

FREQM

The frequency measuring peripheral (FREQM) takes an reference clock signal and a measure clock signal, and records the number of pulses of the latter in a given number of the former (up to 255). Since the counts are recorded in a 24 bit register there is some chance of overflow if the reference clock is slow. Obviously the reliability of the measurement will depend on the trustworthiness of the reference clock…

Clocks on SAMD51 with CircuitPython (Adafruit Grand Central)

Though the SAMD51 on the Grand Central is clocked at 120MHz, this clock has to come from somewhere. When I looked a while back at µPython clocking on the same board, this was rock solid if slightly off from 120MHz. With CircuitPython it seems pretty “drifty” - on the ‘scope the frequency rises and drops over fairly short timescales, suggesting that the base time is not tied to a crystal despite a 32.768kHz crystal bring included on the board and activated.

The crystal can however be used to clock FREQM just fine. So, we can use this to measure both an internal frequency and also external frequency sources.

Counting Time (Internal)

Though it may feel a little like marking your own homework, using FREQM to actually measure the CPU frequency is instructive - not only is it not 120MHz, it is not constant either… this code makes extensive use of the Python CMSIS model mentioned above though that does not break down the content of every register, since this is used at run time not compile time.

from PORT import port
from GCLK import gclk
from MCLK import mclk
from OSCCTRL import oscctrl
from FREQM import freqm

# input / output clocks to pins - PA16/GCLK[2]/D37 PA17/GPCLK[3]/D36
# mode M/c is GPCLK IO - 0x3 for input, 0x1 for output N.B. need
# OE (0x1 << 11) on GENCTRL also for outputs
port.GROUP[0].PINCFG[16] = 0x1
port.GROUP[0].PINCFG[17] = 0x1
port.GROUP[0].PMUX[8] = 0xcc

# sources - reference on XOSC32K, measure on CPU clock
gclk.GENCTRL[2] = (1 << 16) | (0x1 << 11) | (0x1 << 8) | 0x5
gclk.GENCTRL[3] = (1 << 16) | (0x1 << 11) | (0x1 << 8) | 0x7

# main clock enable to peripheral
mclk.APBAMASK |= 0x1 << 11

# connect clock sources to inputs
gclk.PCHCTRL[5] = (0x1 << 6) | 0x3
gclk.PCHCTRL[6] = (0x1 << 6) | 0x2

# reset FREQM
freqm.CTRLA = 0x10
while freqm.SYNCBUSY:
    continue

# configure REFNUM counts
freqm.CFGA = 0x80

# enable, wait for enabled
freqm.CTRLA = 0x2
while freqm.SYNCBUSY:
    continue

# trigger, wait for measurement to be complete
freqm.CTRLB = 0x1
while freqm.STATUS == 1:
    continue

if freqm.STATUS:
    print(f"Error code: {freqm.STATUS}")
else:
    print(f"{freqm.VALUE * 32.768 / freqm.CFGA:.3f}")

For convenience this program also sends the signals out through the GPIO connectors (hence the PORT configurations) so that they can be hooked up to an oscilloscope to verify the results. Doing so (not shown) indicates that the 32.768kHz frequency os pretty steady, if sligtly off from exactly that (by 0.2Hz or so, which may be temperature dependent). Running this program however, and also hooking up a ‘scope indicates that the actual mainCPU frequency is all over the place:

120117.469
120104.656
120115.906
120120.281

Obviously this is too high precision for real life! As a note, when running with µPython the clocks were pretty steady, so this is, I think, an indicator that the main clocks are not phase locked to the crystal. Trying to figure out how to fix that was a headache for another day.

Measuring External

To test with an external source, I hooked up a Raspberry Pi Pico (which has aa crystal) using PWM with 50% duty cycle, 1.25MHz frequency: this was solid on the scope as stated above. The only difference here is redefining the input for GCLK[3] to depend on GCLK_IO not a system clock source:

# sources - reference on XOSC32K, measure on input pin
gclk.GENCTRL[2] = (1 << 16) | (0x1 << 11) | (0x1 << 8) | 0x5
gclk.GENCTRL[3] = (1 << 16) | (0x1 << 8) | 0x2

This nicely illustrated that the clock coming out of the RP2040 is very stable compared with the crystal in the Grand Central:

1250.047
1250.047
1250.047
1250.047
1250.047

Even if this is not accurate, it is precise which is nice for a frequency counter. The ‘scope says 1.25002MHz, this says 1.250047 so a difference of opinion of around 20ppm - probably as good as it will get without sending stuff off to NIST for calibration.

A Word of Warning

There are a lot of pitfalls here. If you don’t have the clocks correctly configured the FREQM will just lock. The clocks, certainly in CircuitPython, are all over the place (will report later). The clocking documentation is quite opaque. But, once you have a handle on it, there are some nice tools in here.