Multicore Forth execution with multitasking on the RP2040

Travis BemannTravis Bemann wrote 11/23/2021 at 03:10 • 4 min read • Like

RP2040 (e.g. Raspberry Pi Pico) support in zeptoforth has advanced to the point that both multicore execution and PIO are supported, with multicore support being in a beta release, 0.22.0b0. Additionally, multicore execution on the RP2040 supports multitasking on both cores, so multiple tasks can be executed on each core simultaneously. Multicore functionality is mostly symmetric, once core 1 has been booted, aside from that the RP2040 must be rebooted, programmed, or erased from core 0, and the main task running the REPL is always on core 0. Multitasking is largely independent of multicore functionality in that two separate multitasking environments exist, one on each core, even though the API functionality for spawning a main task on core 1 is located in task-module because the main task on core 1 is in many ways an ordinary task unto itself.

Here is the source of a test program for demonstrating multicore execution combined with multitasking on the RP2040, repeated here as well:

begin-module forth-module

  import task-module
  import led-module
  import int-io-module

  \ LED test loop task
  : led-loop ( ms -- ) begin led-toggle dup ms again ;

  \ Star test loop task
  : star-loop ( ms -- ) begin ." *" dup ms again ;

  \ Second core main task
  : core-1-main ( star-ms led-ms -- )
    1 ['] led-loop 256 128 512 spawn run
    1 ['] star-loop 256 128 512 spawn run
    current-task kill

  \ Initialize the test
  : init-test ( -- )
    1000 500 2 ['] core-1-main 256 128 512 1 spawn-aux-main
    250 ms 750 1 ['] led-loop 256 128 512 spawn run

What this code does is, once compiled to RAM and init-test is invoked, first it turns off interrupt-driven serial IO with disable-int-io, then it spawns a main task on core 1 with spawn-aux-main (note that on the RP2040 spawn-aux-main can only spawn a main task on core 1) with the specified dictionary, data stack, and return stack sizes in bytes, with an execution token for core-1-main, which will be executed as the main task of core 1, and two arguments, 1000 and 500, which will be pushed onto the data stack of the main task of core 1 as well as their count.

The main task of core 1 then spawns two tasks on core 1 which each take 1 argument off its data stack and have the specified stack configuration, which execute led-loop and star-loop, and then terminates itself. led-loop takes a number of milliseconds and toggles the LED at that interval forever, and star-loop does the same except it outputs asterisks to serial IO.

Back in the main task of core 0 from which init-test was invoked, it waits 250 ms and then spawns a task on core 0 which executes led-loop with an argument of 750, to toggle the same LED toggled by led-loop on core 1 but at an interval of 750 ms. After this control is returned to the REPL on core 0.

Note that despite sharing RAM and peripherals, multitasking operates largely separately on the two cores of the RP2040. For starters, critical sections only block multitasking on the core on which they are entered; multitasking continues on the other core. Attempting to modify the state of the multitasker of one core from the other core has undefined (and almost certainly undesirable) results. Consequently, one cannot share semaphores, locks, queue channels, rendezvous channels, or byte streams between cores. If one does wish to communicate between the two cores, the hardware FIFO's (mailboxes) and spinlocks provided by the RP2040 combined with the shared memory (e.g. using hardware FIFO's to raise processor exceptions on the other core and using hardware spinlocks to protect shared RAM) are what one should use.

Also note that currently there are some hitches - interrupt-driven serial IO must be disabled prior to booting the second core of the RP2040 or serial IO will break, and after the second core of the RP2040 flash must not be written to until the RP2040 is power-cycled (and not merely rebooted) or else the RP2040 may hang altogether unpredictably. It is not projected when these will be solved, especially since there are fundamental design issues posed by extending interrupt-driven serial IO across both cores, because of issues with synchronizing interrupt-driven serial IO on both cores simultaneously. Also, there does not appear to be any good way of forcibly stopping—but not resetting—a core, something which will be needed for writing to flash after both cores have been booted.