PICO2/AFPOC

February saw another momentous step in the ambitious John Cage Organ Art Project at Halberstadt when the playing chord changed for the first time in two years. The resultant chord will now continue to play for another two years before the next change.

The project is a performance of John Cage’s ORGAN2/ASLSP, generally known by the title “As slow as possible“. Written in 1987, performances generally last between a few hours up to a day. The longest completed performance lasted 24 hours. The Halberstadt performance started in 2001 and is planned to finish in 2640, taking a staggering 639 years. The duration was chosen as this was the age of the church when the performance started. The website includes a virtual walk-through of the church, extracts of the sounds, full rationale for the project and details of all the note-changes.

Having performed a Lo-Fi version of John Cage’s 4’33” I was wondering what to do for a follow up so when I saw the recent note change, the idea started to form for a microcontroller take on ORGAN2/ASLSP – could I get a microcontroller to run as slow as possible and still play the music?

Spoilers: I’ve used a Raspberry Pi Pico, running at 20MHz, which admittedly isn’t all that slow! But it will do for today as a proof of concept to show that there might be some merit in my thinking. It is all really just an excuse to think about how I might go about it in the future. All that thinking so far has been written up in this post and some ideas for future directions provided at the end.

So here is my Lo-Fi Performance of a short extract of the opening of John Cage’s ORGAN2/ASLSP which I’ve called PICO2/AFPOC.

Warning! I strongly recommend using old or second hand equipment for your experiments.  I am not responsible for any damage to expensive instruments!

If you are new to microcontrollers, see the Getting Started pages.

Introduction and Initial Thoughts

There was obviously no point in attempting something new on the longevity front, and as for the performances that last several hours, well I could just power up a microcontroller and have it playing until any resulting video was simply too long to upload.

No, I needed a different take, so I decided to interpret “as slow as possible” in a different way. What if the timings for the notes could come from a microcontroller running as slow as possible, yet still able to produce sound?

In these days of GHz and multi-MHz processor frequencies, the idea of turning a microprocessor down to slower, yet still usable speeds really appealed.

So that is the direction I’ve decided to go.

Underclocking a MCU

The practice of winding up the clock on a processor of any sort is called overclocking and is a well used technique. I used it myself with my PicoDexed to run the Pi Pico at 250MHz to support more polyphony.

Sometimes it requires additional cooling, as the processor is now working harder than initially designed for. As a consequence it will draw additional power too.

Underclocking is less used (I believe) but not unusual in microcontrollers, as generally speaking winding down the processor system clock can allow it to consume less power. If the processing power isn’t needed for your application this can give significant improvements in power consumption. Useful if you want to run it off a battery for example.

So, how (s)low can we go? Well as with all things, it depends.

One thing to look out for in a microcontroller for underclocking is “fully static operation”. These magic words might be listed in the specification and essentially means that the MCU has what is known as a static core. This means that the processor will retain all its state between successive clock “ticks”. If perfectly static, one should be able, in principle, to manually trigger each tick and have the MCU work through all operations – fetching instructions, reading/writing to memory or IO, calculations, etc – on each clock tick quite happily.

Again, in principle, it could even be paused in this low-power “static” state until the clock is started up again. Note – that is “low power” not “no power” – the state requires a trickle of power to be maintained, but it is a lot less than a running processor.

The reality is a little more complex. I was initially quite intrigued to note that the ATtiny85 (a microcontroller I’ve played with, but not really blogged about here) claims fully static operation, and can be used with an external clock. However that comes with a caveat:

When applying an external clock, it is required to avoid sudden changes in the applied clock frequency to ensure stable operation of the MCU. A variation in frequency of more than 2% from one clock cycle to the next can lead to unpredictable behavior.

A typical modern microcontroller is a complex beast, especially when it comes to timers and clocks. There are typically many options: external or internal (e.g. PLL) clocks; pre-scalar settings to “scale down” the clocks used; different clocks for different subsystems – e.g. IO, comms, counter/timers, USB, etc.

Another issue is that sometimes the MCU has to be configured in a specific mode, sometimes with a specific frequency of clock, via “FUSES”. These are hardware-related configuration settings sometimes configurable with the IDE, but often requiring a specific programming option via the toolchain.

But as the usual way to set these fuses is through the debug interface, which itself is usually some kind of serial protocol (SWD or similar, possibly even over USB). Serial protocols will be running at a known frequency (baud), so their operation is highly dependent on the system clock speed. Changing the operating frequency of the device is a sure way to mess up your debug communications and leave you with an inaccessible device!

But generally speaking, keeping to a “sensible” clock frequency, with commonly available crystals, and perhaps a useful multiple of common serial protocol or USB baud rates is usually quite doable.

But is that “as slow as possible”?

The Music and Audio

Whilst I could underclock a processor and then have it somehow trigger sound generation elsewhere, what I really want is for it to still be able to generate the audio itself. So a little analysis of what kind of audio will be required is probably worth a brief diversion at this point.

There are a number of snapshots of part of the score for ORGAN2/ASLSP online, so we can be a feel for the kind of thing quite easily.

In the interests of practical application, I’m not going to attempt an entire performance, but instead thought I’d look at what would be required to get the same point as the Halberstadt performance.

The full piece is in eight parts. Helpfully both wikipedia and the aslsp project have lists of the changes so far, so I popped them into a spreadsheet and came up with the following as my “score” for this short extract of the first section of the piece. The full piece itself has eight sections in total.

The dates are the dates the note changes happened in Halberstadt. The last two columns are the number of days from the start of the project and the number of days between steps. I’m going to use the last column as the basis of the timings for each step.

Taking the full range of notes in this extract means I need to be able to play up to eight notes at a time from the following set:

The middle column is the frequency in Hz of the note, and the last column is the equivalent MIDI note number.

We can see from this that I need the microcontroller to be able to generate sounds at frequencies between ~160Hz through to just over 830Hz. Taking the simplest audio output on a microcontroller to be a basic oscillating square wave (i.e. just turning a pin on and off at a known frequency), then in principle audio might still be achievable even down to a basic processor clock frequency in the small number of kHz…?

Clocks, Pre-scalars, Crystals and Timers

There is no simple way to get a microcontroller down to these speeds that I’ve found so far. It will almost certainly need a custom external crystal oscillator. I’ll need to do some more reading and experimentation if I wanted to drive a microcontroller down to low speeds with external circuitry and that is probably going to take a bit of figuring out and might be beyond my basic electronics knowledge anyway. So that might be a follow-on project at some point in the future.

For now, I already know that the Raspberry Pi Pico has a software API call that can set the processor clock. It is what I used for my PicoDexed. It is called set_sys_clock_khz() which sounds promising. If this works, it means the system can boot at normal speed and then from software switch over to a slower speed at run-time.

The clock subsystem for the RP2040 is pretty complicated however. There are several clocks that need setting up properly. This is fully described in the RP2040 datasheet (sections 2.15, 2.16, 2.17 and 2.18) and in the appropriate SDK documentation. The main options are:

  • External crystal (XOSC) typically in the range 1-15 MHz.
  • Internal ring oscillator (ROSC) typically in the range 1.8-12MHz.
  • Two on-chip phase-locked loops (PLL) which can scale up from the external oscillator to higher frequencies. One is used for the system clock and one for USB and the ADCs.

Interestingly there is also an option to synchronise the clocks via specific GPIO inputs too.

The Raspberry Pi Pico uses an external 12MHz crystal to drive the RP2040 and the PLLs are configured for a 125MHz system clock and 48MHz USB/ADC.

As far as I can see set_sys_clock_khz() will work out a valid set of configuration registers and then set the PLL clock of the Pico. But only certain values are possible corresponding to the various combinations of the hardware registers. There are only specific frequencies that are supported.

Regardless of the frequency of a clock source, the user-available end clocks can usually be scaled back more if required. A typical method of scaling clock sources is to use a pre-scalar or divider value. These would often be a power of two, but not always.

The timers on an ATmega328, for example, can be linked to the system clock and scaled by factors of two by 1, 2, 4, 8, 16 and so on. This way, the 16MHz clock on an Arduino could become a 8MHz, 4MHz, 2MHz, 1MHz, etc “tick” when driving one of the timers.

The Pico incudes dividers too. The ROSC can be scaled down a factor of between 1 and 32 for example (the DIV register). The PLL has two dividers (POSTDIV1, POSTDIV2). Section 2.18.12 in the datasheet has the full set of criteria for setting the various configuration registers for the PLL.

It is complicated, which is why there is a python script provided that can be used to calculate the values for your requested frequency: https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/hardware_clocks/scripts/vcocalc.py.

There is also an API function that can be used to check if the requested values might work prior to their use: check_sys_clock_khz().

There is a Micropython function to set the system clock too: machine.freq(freq) which is pretty handy.

Putting various values into the python script, it appears to only go down to 8MHz without causing an exception, but it seems that the Micropython function itself will only accept down to 20,000,000 without returning “cannot change frequency”. I’m not sure why that is, as the code appears to just call set_sys_clock_khz() from the SDK which I think is following the same logic that is encoded in the python script.

If the minimum VCO frequency used by default in the calculations can be changed (I believe it defaults to 750MHz, but there have been suggestions that in previous versions of the SDK it was 400MHz) then lower frequencies might be possible. It seems to affect the following lines in check_sys_clock_khz() which is the function used in set_sys_clock_khz() to determine if a frequency is valid or not:

uint vco_khz = fbdiv * reference_freq_khz;
if (vco_khz < PICO_PLL_VCO_MIN_FREQ_KHZ ||
vco_khz > PICO_PLL_VCO_MAX_FREQ_KHZ) continue;

This is iterating through fbdiv values from 320 down to 16. With a MIN_FREQ_KHZ of 750,000 only fbdiv values above 62 are valid. With a MIN_FREQ_KHZ of 400,000 that drops to around 33 opening up more possibilities.

The rest of the code then effectively “brute forces” various values of postdiv2 and postdiv1 to see if any of them yield the requested frequency. If not, then it will return FALSE, but if a combination is found it returns values for the VCO and the two dividers.

The upshot of all this is that unfortunately, despite the names of the API functions, I’ve not been able to successfully configure a clock below 20MHz. There might be other options, so further investigation will be required, but I suspect this is as (s)low as possible for now.

Which in microcontroller terms, is still actually pretty fast!

Here is the resultant initialisation code:

machine.freq(125000000) # Reset to normal for comparison
print ("Frequency: ", machine.freq())
for n in range (0, 10):
timepin.toggle()
machine.freq(20000000) # Slow down
print ("Frequency: ", machine.freq())
for n in range (0, 10):
timepin.toggle()

And this is the result of monitoring the timepin:

So that looks promising at least in that I know the frequency has appeared to change.

In order to continue with the proof of concept, I’ve cheated for now. I’ve added a waiting loop delay in my main code! This slows the execution down by several factors, yielding toggle timings as follows:

  • Full speed (125MHz): toggle period 20uS.
  • Slowed (20MHz): toggle period 160uS.
  • With waiting loop: toggle period 2mS.

I was tempted to add in another factor of 10 waiting around, but that would make the performance too long for a short demonstration, and as this isn’t really doing what I wanted fully – i.e. the timings are already somewhat artificial due to code waiting loops, and not wholly down to microcontroller speed, I didn’t see it necessary to prove the concept.

The PICO2/AFPOC Performance

Putting all this together, on a Raspberry Pi Pico, means that the first 518 unit wait before any notes sound lasts around 16 seconds and the longest sounding chord, at 2527 units, lasts around 1 minute, 20 seconds.

The entire extract at this speed takes just under 5 minutes which is long (and slow) enough to show that the idea might have some merit.

The sound itself is produced using Ben Everard’s PIOBeep library, which configures the Raspberry Pi Pico’s PIO state machines for a square wave output. This is the approach I used with my Pi Pico PIO Poly Tone Keyboard. For this demonstration I’ve used my Pi Pico PIO Poly Tone MIDI “Pack”.

The PIO runs at some divisor of the Pico’s system clock. The Micropython PIO interface allows the frequency to be set, and one of the examples in the Raspberry Pi Pico Python SDK guide shows a frequency of 2000Hz. The Micropython documentation for the RP2040 StateMachine class states the following about the “freq” parameter to StateMachine.init():

freq is the frequency in Hz to run the state machine at. Defaults to the system clock frequency.

The clock divider is computed as system clock frequency / freq, so there can be slight rounding errors.

The minimum possible clock divider is one 65536th of the system clock: so at the default system clock frequency of 125MHz, the minimum value of freq is 1908. To run state machines at slower frequencies, you’ll need to reduce the system clock speed with machine.freq().

This can be seen in the source implementation. The following code calculates the PIO clock divisor for a given frequency. We can see how this will pick up any system frequency changes automatically via the call to clock_get_hz(clk_sys).

From rp2_pio.c:

// Frequency given in Hz, compute clkdiv from it.
uint64_t div = (uint64_t)clock_get_hz(clk_sys) * 256ULL /
(uint64_t)args[ARG_freq].u_int;
if (!(div >= 1 * 256 && div <= 65536 * 256)) {
mp_raise_ValueError(MP_ERROR_TEXT("freq out of range"));
}
clkdiv_int = div / 256;
clkdiv_frac = div & 0xff;

Note the clk_sys is configured from set_sys_clock_pll() which is used by set_sys_clock_khz() which is used by machine.freq().

Ideally, the system clock would be running at such low frequencies that no divisor need be used, but for now I’ve just stuck with the default frequency specified in the PIOBeep library, of 1MHz rather than mess around with changing it.

Ok, so none of this is very slow, and it certainly isn’t “as slow as possible” at the moment, but it gives me plenty to work with moving forward.

Closing Thoughts

I’m happy I’ve proven the concept. There is plenty to do if I want to genuinely get this to be “as slow as possible”. Future directions for investigation are:

  • See if the C/C++ SDK gives me access to raw registers on the Pico which might allow a system clock of less than 20MHz.
  • See if there is any possibility of replacing the external crystal on a Pico with something slower.
  • Learn how to drive a different microcontroller from an external clock. Ideally this would be one that doesn’t need a specific frequency setting in fuses. There might be something in the ATtiny range that is suitable, but more research is needed.
  • Go back in time and get a “vintage” CPU on the case. My ZX Spectrum had a 3.5MHz Z80 back in the day so there must be several possibilities there.

Ultimately, if I’m simply triggering and generating tones then there might even be potential here for some discrete logic driven oscillators.

At one end, I guess I could have a bank of 555s for the oscillators and some (slow) logic to work out which to play. But is that really in the spirit of getting a microcontroller to go as slow as possible whilst making music? I’m not sure.

What would be really neat is a discrete logic CPU that could essentially be single-stepped to show itself working and then wound up to audio frequencies (say 2kHz) and have its outputs eventually become sound.

But considering today’s date, the ease of use of winding down the Pico this far, and the fact that the results are indeed audible, I’m very happy with my PICO2/AFPOC.

Kevin

Leave a comment