September 24, 2023

Raspberry Pi Pico - launching the second core

As we all know, the RP2040 has two ARM cores. The game here it to learn how to get some of our code running on the second core.

Some chips keep the second core powered down until you enable clocks for it and bring it out of reset. On the RP2040, the second core runs right away. A register exists that the bootrom code can test to find out if the code running is core 0 or core 1. When it detects core 1 starting up, it diverts it into special code that comments in the boot code calls a "holding pen". Take a look at "pico-bootrom/bootrom/bootrom_rt0.S" to see all of this.

Many other multi-core processors do something like this, and usually the protocol the bring a core out of the "holding pen" is quite simple. Typically you write the address you want it to run at into a register dedicated to that purpose, and the job is done. Code at that address will need to set up a stack and perform whatever other processor initialization is necessary or desired.

The RP2040 has the most complicated scheme that I have seen to date. The RP2040 has a pair of FIFO devices between the two cores, one for each direction (core 0 to 1 and core 1 to 0). A sequence of values is written to the "0 to 1" fifo, a proper response is monitored on the "1 to 0" fifo and that is how the game is played. The data sheet explains all of this in section 2.8.2 (page 133). The sequence that is written to the 32 bit fifo register is:

0
0
1
vector table
stack pointer
entry address

A note is in order about the entry address. The way the ARM processor works, when you perform a jump in this way (and this is also true of entries in the vector table), you must set the low bit in the address in the table. The ARM processor takes note of this bit and clears it, so it does not jump to an odd address as would seem to be the case. What the bit does is the tell the processor to run code at that address in thumb mode, hence this bit is called the thumb bit.

If you are writing assembly language, you will designate functions with ".thumb_func" and the assembler will take care of this for you. If you are building a table by hand, you will probably need to do something explicit to set this bit yourself.

The ARM Cortex-M series processors only execute thumb instructions. You would think this would make this entirely unnecessary, but it works the other way -- you must set the "thumb bit" to reassure them that they should keep doing the only thing they know how to do. If you don't set the bit, things "just don't work" -- presumably the processor takes a hard fault exception or something (and ends up spinning given the handler for that I have provided), but I have never been curious enough to investigate the details.

The C compiler knows all about this and does the right thing, setting the thumb bit for function pointers. Note that the actual address itself is not odd (that would be badly aligned and cause other troubles.

Here is a nice discussion of how you fire up core 1 on the RP2040:

Most people using the RP2040 will use the SDK which takes care of (and hides) all of these details.

SEV and WFE instructions

The WFE instruction is "wait for exception" and the bootrom code will have core 1 sitting on this instruction which is just the right thing to do. It reduces power consumption. As I understand it, the instruction itself powers down parts of the chip, but it also avoids having the processor endlessly spinning and testing some register.

The SEV instruction is a partner to this. On a single core machine it will be a NOP. On a multicore machine it "causes an event to be signaled to all cores within a multiprocessor system." It should be easy to see how all of this works.

The vector table

You need to provide a pointer to one of these. You could provide a pointer to the one contained in the bootrom image. I found it fairly easy to set up one of my own in my assembly language startup file.

It must be 256 byte (64 word) aligned for the RP2040. The first 16 entries are exceptions. The next 32 entries are IRQ vectors. (This leaves 16 entries, i.e. 64 bytes unused in the 256 byte section set aside for this). The RP2040 only defines 26 of the 32, and most of the 16 exception entries are "reserved"

Also, as near as I can tell, each core has its own VTOR (vector table offset register). This means each core can have its own vector table, but stand by for more on this when I start working with interrupts and the NVIC.

It was all fairly straightforward to work out and I got the second core out of the "holding pen" and running my code with little trouble as per this:

I continued from there to explore CPU clocks. I found that coming out of the bootrom both cores are running at the crystal frequency (12 Mhz). I figured out how to configure the PLL and clock control to get the frequency up to 125 Mhz. Note that there is one clock for both cores, so they both will and must run at the same rate.


Have any comments? Questions? Drop me a line!

Tom's electronics pages / tom@mmto.org