Introduction
Earlier this week, I spotted that Arduino had updated their ArduinoCore-mbed Boards Manager files for Arduino Mbed OS RP2040 Boards to version 2.1.0.
The exciting part of this update is that it includes the pico-sdk multicore library, which I have been eager to try out for some time now.
Unfortunately for Arduino fans, there is no ready-made Arduino multicore examples available on the Arduino IDE. Instead you have to go to the Raspberry Pi Github page and grab the multicore C examples from their pico-examples repository.
At the time of writing this blog, there were 4 multicore examples listed on Github.
Unfortunately, I found that when inserting these examples into an Arduino sketch, not all compiled as-is via the Arduino IDE.
I also found that none of these code examples were designed to run continuously on the RPI Pico, as in, there was no repeated multicore data exchange within a while(true) function or the sketch's loop() function. This made it more difficult for me to work out how the runtime interaction between the two cores actually worked as the examples were all one off events.
Thankfully I've made progress and solved a few issues myself, which I now want to share with others.
Examples
FIFO Blocking routines
To understand the fundamentals I'll start with two FIFO blocking examples, namely the hello_multicore example and then the multicore_runner example, which uses the same core functions as hello_multicore but in a slightly different way. This latter example uses function pointers, which I hadn't used in Arduino before.
The core functions highlighted in both examples are:
- multicore_launch_core1( --NameOfCore1RunTimeEntryFunction--); // This launches the entry function for core1.
- This "entry function" can only be a void function. In Arduino terms, think of it as setup() and loop() rolled into one function.
- You would typically place this multicore_launch_core1 function in your Arduino sketch setup() routine, as it only needs to be called once. You also have the option inside your main code to reset Core1 (using the multicore_reset_core1 (void) function). You cannot "unlaunch" a Core1 entry function during runtime.
- There are also two other launch functions available:
- multicore_launch_core1_raw ( void(*entry)(void), uint32_t *sp, uint32_t vector_table )
- multicore_launch_core1_with_stack ( void(*entry)(void), uint32_t *stack_bottom, size_t stack_size_bytes )
- multicore_fifo_push_blocking(--Data in int32 format--); // This pushes data on to the FIFO.
- This function will block until there is space for the data to be sent.
- You can use the multicore_fifo_wready() function to check if the FIFO is ready to write to.
- multicore_fifo_pop_blocking(void); // This "pops" data from the FIFO. The data is returned as int32 format.
- Similarly, this function will block until there is data ready to be read.
- You can use the multicore_fifo_rvalid() function to check if there is data on the FIFO to be read. This is handy if you do not want to block and wait for data.
Now as these two examples don't really do much in terms of bells and whistles, I decided to beef things up a bit and created my own versions of the "hello world" sketch and an alternative "multicore_runner" sketch. Please note that I am using a bit of mbedOS code syntax for my LEDs and I am using Serial1 instead of Serial for my serial output which means the serial stream is transmitted via GP0.
Here is the code for my new "hello multicore world" sketch:
#include <stdio.h> #include <mbed.h> #include "pico/multicore.h" mbed::DigitalOut led1(LED1, 0); mbed::DigitalOut led2(p2, 0); void core1_entry() { bool Flop = false; uint32_t cntr = 0; while(1) { Flop = multicore_fifo_pop_blocking(); if (Flop) { led2 = !led2; Serial1.println("...One!"); } cntr += 10; multicore_fifo_push_blocking(cntr); } } bool Flip = false;; void setup() { Serial1.begin(115200); sleep_ms(500); Serial1.print("\r\nHello..."); sleep_ms(1000); Serial1.println("let's hear from our multicores!"); multicore_launch_core1(core1_entry); sleep_ms(1400); } void loop() { led1 = !led1; multicore_fifo_push_blocking(Flip); uint32_t SleepCntr = multicore_fifo_pop_blocking(); sleep_ms(100 + SleepCntr); if (Flip) { Serial1.println("...Zero!"); } Flip = !Flip; }
And here is a short video demonstrating the output. You will notice that the onboard LED (led1) blink interval is controlled by Core1 and the 2nd added LED (led2) blink interval is controlled by the multicore_fifo_pop_blocking function inside the Core1 entry function.
And here is my new "multicore_runner" sketch:
#include <stdio.h> #include <mbed.h> #include "pico/multicore.h" mbed::DigitalOut led1(LED1, 0); mbed::DigitalOut led2(p2, 0); void core1_entry() { while (1) { // Function pointer is passed to us via the FIFO // We have one incoming int32_t as a parameter, and will provide an // int32_t return value by simply pushing it back on the FIFO // which also indicates the result is ready. int32_t (*func)(int32_t) = (int32_t(*)(int32_t)) multicore_fifo_pop_blocking(); int32_t p = (int32_t)multicore_fifo_pop_blocking(); led2 = !led2; int32_t result = (*func)(p); multicore_fifo_push_blocking(result); } } int32_t factorial(int32_t n) { int32_t f = 1; for (int i = 2; i <= n; i++) { f *= i; } return f; } int32_t fibonacci(int32_t n) { if (n == 0) return 0; if (n == 1) return 1; int n1 = 0, n2 = 1, n3; for (int i = 2; i <= n; i++) { n3 = n1 + n2; n1 = n2; n2 = n3; } return n3; } int32_t rNum = 0; void setup() { Serial1.begin(115200); sleep_ms(100); Serial1.println("\r\nHello, multicore_runner!"); randomSeed(analogRead(A0)); // This example dispatches arbitrary functions to run on the second core // To do this we run a dispatcher on the second core that accepts a function // pointer and runs it multicore_launch_core1(core1_entry); } void loop() { int32_t result; led1 = !led1; sleep_ms(500); rNum = random(0, 16); multicore_fifo_push_blocking((int32_t) &factorial); multicore_fifo_push_blocking(rNum); // We could now do a load of stuff on core 0 and get our result later result = multicore_fifo_pop_blocking(); Serial1.println("Factorial " + String(rNum,DEC) + " is " + String(result, DEC)); // Now try a different function led1 = !led1; sleep_ms(500); multicore_fifo_push_blocking((int32_t) &fibonacci); multicore_fifo_push_blocking(rNum); result = multicore_fifo_pop_blocking(); Serial1.println("Fibonacci " + String(rNum,DEC) + " is " + String(result, DEC)); }
And another short video demonstrating the output. Here you will notice that the onboard LED (led1) blink interval is simply controlled by the sleep duration while the 2nd added LED (led2) blink interval is controlled by the multicore_fifo_pop_blocking function inside the Core1 entry function.
Non-Blocking (Polling) for FIFO
If you want both cores to run independently without the "pop" or "push" functions holding things up by waiting to receive and send data you can use two other functions. These essentially provide a means to peek inside the pop FIFO buffer to see if any data is available or peeks inside the push FIFO to see if data can be written into this buffer and then lets you know.
- bool multicore_fifo_rvalid (void); This checks the read FIFO buffer to see if there is data waiting and returns true if the FIFO has data.
- bool multicore_fifo_wready (void); This checks the write FIFO buffer to see if it is available or otherwise and returns true if ready (note SDK documentation states that it returns true if full but it does not appear to do so).
For my first home-grown non-blocking example I included just the multicore_fifo_rvalid check in the Core 1. In the video you will see that the onboard LED (led1) eventually stops blinking when the write buffer gets full.
Then in the second non-blocking example I included both the multicore_fifo_rvalid check in the Core 1 and a multicore_fifo_wready check in Core 0. In this video you will notice that both LED's (led1 and led2) continue to behave independtly and there is no blocking with the onboard LED (led1) when the write buffer gets full.
In terms of code, the only difference between these two non-blocking examples is this line of code, which was added to the 2nd non-blocking example. In the 1st non-blocking example we simply use multicore_fifo_push_blocking(t_int2) without the if statement.
if (multicore_fifo_wready()) multicore_fifo_push_blocking(t_int2);
The complete code is as follows:
#include <stdio.h> #include <mbed.h> #include "pico/multicore.h" #include "hardware/irq.h" mbed::DigitalOut led1(LED1,0); mbed::DigitalOut led2(p2,0); void core1_entry() { uint32_t t_int = millis(); uint8_t cntr = 0; int32_t LEDint = 500; while(1) { if ((millis() - t_int) > LEDint) { t_int = millis(); led2 = !led2; cntr++; } if (multicore_fifo_rvalid()) { if (cntr > 10) { LEDint = multicore_fifo_pop_blocking(); cntr = 0; } } } } uint8_t cntr2 = 0; int32_t t_int2 = 1000; void setup() { pinMode(LED1, OUTPUT); pinMode(p2, OUTPUT); Serial1.begin(115200); sleep_ms(100); Serial1.print("\r\nHello this is fancy FIFO IRQ's"); //analogWrite(p2, value); multicore_launch_core1(core1_entry); } void loop() { led1 = !led1; sleep_ms(250); if (cntr2 > 3) { if (multicore_fifo_wready()) multicore_fifo_push_blocking(t_int2); t_int2 += 1000; cntr2=0; } else cntr2++; }
Summary
Hopefully these four two examples have helped you understand how multicore works using both the blocking and non-blocking functionality during runtime. I have still to test the use of non-blocking service interrupt routines, as found in the multicore_fifo_irqs example. I did try to use the queues library functions but at the time of writing I had problems with getting the pico queue library working via the Arduino IDE.
Top Comments