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.
To test this, use a Raspberry Pi pico with PWM, set to 50% duty cycle, 1.25MHz:
This is reading back as a steady 1.25002MHz on the ‘scope frequency counter.
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
.
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…
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.
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.
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.
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.