Multi-Core Programming with MicroPython
The Raspberry Pi Pico and the Pico W both come with two "Cores". In this section we will review why microcontrollers have multiple cores and how they are used.
Introduction
Having multiple processor cores is critical when you need to do multiple tasks simultaneously. Having two cores means we can run two different parts of our program at the same time - just like having two brains working on different tasks! This tutorial will help you understand how to use both cores effectively.
Why Use a Second Core?
Imagine you're trying to pat your head and rub your stomach at the same time. It's tricky because you're trying to do two different tasks simultaneously. This is similar to what your Pico faces when it needs to:
- Read sensor data while updating a display
- Control a motor while monitoring buttons
- Play music while running LED animations
- Process incoming data while sending responses
Using the second core lets you handle these tasks properly without one task slowing down the other. Having a second core matches the job of physical computing. One core is used to gather data and one core is used to analyze the data and log or display the data.
Real-World Example: Display Updates and Sensor Reading
Let's look at a common problem: updating an OLED display while reading from a sensor. If we do both on one core, we might miss important sensor readings while the display is updating.
Here's a practical example showing how to use both cores. In this example we are trying to gather sensor data on our Analog-to-digital (ADC) input and update our OLED display at the same time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
In the example above, the key line is the following:
1 2 |
|
By default, the main() function is always started on the main core number 0. This line starts the new task on the second core - core number 1
Let me break this down in a clear step-by-step way.
The line _thread.start_new_thread(update_display_task, ())
is like hitting a "start" button for a second brain in your Pico. Let's understand it piece by piece:
_thread
is a special MicroPython module that lets us work with multiple cores. The underscore at the start just means it's a more technical, behind-the-scenes module.start_new_thread
is a function that does exactly what its name suggests - it starts running code on a new thread. On the Pico, this new thread automatically runs on Core 1 (while your main program runs on Core 0).update_display_task
is the name of the function we want to run on the second core. Notice there are no parentheses after it - we're just telling the system which function to run, not actually running it yet.- The empty parentheses
()
at the end are where you'd put any arguments that your function needs. In our case,update_display_task
doesn't need any arguments, so we leave them empty.
Core-to-Core Communication
Here's how the cores communicate in our example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Think of it like two people (cores) sharing a notepad (sensor_value):
- Core 0 writes new sensor readings to the notepad
- Core 1 reads from the notepad to update the display
- The
lock
is like a "do not disturb" sign - when one core is using the notepad, the other core has to wait
We know it's running on the second core because:
- The main program runs on Core 0 by default
- When we call
_thread.start_new_thread()
, the Pico automatically runs that function on Core 1 - Both functions keep running continuously (the
while True
loops) - this would be impossible if they were running on the same core
LED Blink Rates on Multiple Cores
How do we know that the functions are running on different cores? Let's create a simple test program that clearly demonstrates both cores running independently.
This program proves the cores are running independently in several ways:
- Different Blink Rates:
- Core 0's LED blinks fast (every 0.25 seconds)
- Core 1's LED blinks slow (every 1 second)
-
If they were running on the same core, you couldn't have different timing like this
-
Print Statements:
- You'll see "Core 0 blink" printing four times for every one "Core 1 blink"
-
The prints will interleave, showing both cores are running simultaneously
-
Visual Proof:
- Connect two LEDs (with appropriate resistors) to pins 14 and 15
- You'll see them blinking at different rates independently
- If this was running on a single core, the blink rates would interfere with each other
To try this out: 1. Connect an LED + resistor (220Ω) to GPIO 15 (Core 0 LED) 2. Connect another LED + resistor to GPIO 14 (Core 1 LED) 3. Run the program 4. Watch the LEDs blink at different rates 5. Look at the print output in your console
Here's a diagram showing how to wire it up:
1 2 |
|
You can also modify the sleep times to see how changing one core's timing doesn't affect the other core.
Try changing time.sleep(1)
to time.sleep(0.5)
in the blink_core1
function - you'll see that LED change speed while the other LED keeps its original timing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
Understanding IRQs and PIOs on Multiple Cores
Interrupts (IRQs)
Interrupts are special signals that can pause your program to handle important events. When using two cores, each core can handle its own interrupts independently. This means:
- Core 0 can handle button presses without affecting display updates on Core 1
- Each core can respond to time-critical events without waiting for the other core
- You can prioritize which interrupts go to which core
Here's an example showing how to handle interrupts on different cores:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
PIO (Programmable Input/Output)
The Pico has special hardware called PIO (Programmable Input/Output) that can handle tasks like generating precise signals or reading sensor data. The great thing about PIO is that it works independently of both cores. This means:
- You can set up a PIO program to handle precise timing tasks
- Both cores can interact with the PIO programs
- PIO can keep running even if both cores are busy
Here's a simple example using PIO with two cores:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
Tips for Using Two Cores Effectively
- Use Locks for Shared Data: Always use locks when both cores need to access the same variable
- Keep Tasks Independent: Try to split your program so each core has separate responsibilities
- Avoid Printing from Both Cores: The USB serial connection works best when only one core prints
- Mind Your Resources: Remember both cores share the same memory and pins
- Start Simple: Begin with basic two-core programs before trying complex applications
Common Issues and Solutions
- Program Crashes: Usually happens when both cores try to access the same resource. Use locks to prevent this.
- One Core Stops: Check if your while loops have proper sleep() calls to prevent core lockups.
- Data Gets Mixed Up: Always use locks when sharing data between cores.
Conclusion
Using both cores of your Raspberry Pi Pico can make your projects much more capable. You can handle multiple tasks simultaneously without one task interfering with another. Remember to start simple and gradually build up to more complex applications as you get comfortable with dual-core programming.
Remember that practice makes perfect - try modifying the example programs to handle different tasks on each core and experiment with different ways of sharing data between them!
Would you like me to explain any part of this tutorial in more detail?