Multicore Forth execution on the RP2040 (cont'd)

Travis BemannTravis Bemann wrote 02/05/2022 at 02:51 • 4 min read • Like

Recent versions of zeptoforth (release 0.25.0 or newer) now support straightforward symmetric multiprocessing in that they support interaction between tasks executing on different cores of the RP2040 interacting with one another with task communication and synchronization constructs, which was not supported by older versions of zeptoforth which only supported multicore execution where different cores did not interact with each other aside from through shared RAM, hardware spinlocks, and hardware FIFO's. Additionally any task on any core can now start a task on any given core, where previously one had to explicitly boot the second core of the RP2040 from the first core and aside from that tasks could only spawn tasks on their own core.

For an example of multicore operation combined with multitasking with shared task communication and synchronization constructs, take the following example:

continue-module forth

  task import
  systick import
  chan import

  \ The data size
  1 constant element-size
  \ The intermediate stream byte count
  256 constant inter-count

  \ The endpoint stream byte count
  256 constant end-count

  \ The endpoint counter interval
  1000 constant end-interval

  \ The tasks
  variable producer-task
  variable inter-task
  variable consumer-task

  \ The streams
  element-size inter-count chan-size buffer: inter-chan
  element-size end-count chan-size buffer: end-chan

  \ The data buffers
  element-size buffer: source-send-buf
  element-size buffer: inter-recv-buf
  element-size buffer: end-recv-buf

  \ The receive count
  variable recv-count

  \ The starting systick
  variable start-systick

  \ Our producer
  : producer ( -- )
    source-send-buf element-size 0 fill
    begin source-send-buf element-size inter-chan send-chan again

  \ Our intermediate
  : inter ( -- )
      inter-recv-buf element-size inter-chan recv-chan
      inter-recv-buf swap end-chan send-chan

  \ Our consumer
  : consumer ( -- )
      1 recv-count +!
      end-recv-buf element-size end-chan recv-chan drop
      recv-count @ end-interval > if
        0 recv-count !
        systick-counter dup start-systick @ -
        cr ." Receives per second: " 0 swap 0 end-interval f/
        10000,0 f/ 1,0 2swap f/ f.
        start-systick !

  \ Initialize our test
  : init-test ( -- )
    0 recv-count !
    systick-counter start-systick !
    element-size inter-count inter-chan init-chan
    element-size end-count end-chan init-chan
    0 ['] consumer 320 128 512 0 spawn-on-core consumer-task !
    0 ['] inter 320 128 512 1 spawn-on-core inter-task !
    0 ['] producer 320 128 512 0 spawn-on-core producer-task !
    consumer-task @ run
    inter-task @ run
    producer-task @ run

In this example, three tasks are spawned, a consumer task on core 0, an intermediate task on core 1, and a producer task on core 0. Two queue channels (a task synchronization and communication construct that consists of a queue which one or more tasks send messages to and which one or more task receive messages from) are created, one for communication from the producer task to the intermediate task, and one for communication from the intermediate task to the consumer task.

The producer task continually sends messages to the intermediate task with the only delays being when the channel for communication from the producer task to the intermediate task is full. The intermediate task continually receives single messages from the producer task and sends the same messages to the consumer task. The consumer task continually receives messages from the intermediate task, and times how long it takes for a given number of messages to be received, with which it calculates how many messages it has received per second and displays it to the user..

By this inter-task communication is demonstrated to function across multiple cores using task communication and synchronization constructs. Likewise, one task can use RUN to initiate a task on another core, as also shown here. (Unlike previously, spawning a task on another core no longer starts it immediately; rather the user must use RUN to start its execution.)