Parallelism (and Much More) on ESP32 and RP2040

by FabriceA6 in Circuits > Microcontrollers

6146 Views, 14 Favorites, 0 Comments

Parallelism (and Much More) on ESP32 and RP2040

Flock2.PNG

After making a recent Instructables about scheduling concurrent tasks on an Arduino, I decided to explore how to run parallel programs. I recently bought a small device called T-Pico C3, equipped with 2 SoC (Systems on Chip) : an RP2040 and an ESP32 C3. The first one has 2 cores, the second only one, so it's the perfect playground for trying multithreading and parallel tasks.

Let's see what we can do.

Supplies

T-picoC3.jpg

All you need is a T-Pico C3 board, less than 20$.

This small board has 2 microcontrollers, a TFT display, one adressable red LED, 2 push buttons, a USB-C port, a battery connector, and a reset button. There are also 2 other LEDs (green and blus) that are not adressable, as we'll see in step 1.


The next 4 steps present the hardware and some useful libraries, and will help you:

  • control the LED and the buttons,
  • use the display,
  • communicate between the 2 microcontrollers,
  • run a small web server.

If you are only interested in multithreading, jump to step 5.

Microcontrollers and MicroPython

Pinout.jpg
T-Pico C3 MCUs_v2.png

Two MCUs, but only one COM port ! How does it know that we want to program one or the other?

It takes benefit of a feature of the USB-C, that can detect the cable's orientation. As you can see on the figure above, if the cable is connected with one side up, it will address the RP2040, and if it is flipped it will address the ESP32. In the first case, the blue LED is lit, in the second case it's the green LED.

You can stick a marker on one side of the USB cable, to identify it more easily.

The board has also some IO that we can use, but only a few are for the ESP32, the others are for the RP2040. The reason is that the designers of the board had in mind that we would only use the ESP32 as a WiFi peripheral for the RP2040. It's a pity, because the ESP32 is so much more powerful than only a simple wireless thing...

When I received this new toy, I also decided to test programming with MicroPython. Both these MCU support this programming language, and it's quite close to regular Python, so why not test it?

Here is a quick summary on how to program in MicroPython from a PC running Windows:

  1. First install Thonny, a Python IDE. Run it on your computer.
  2. The next step is to install the firmwares for both MCUs:
  3. For the ESP32, go to MicroPython.org, in Download select the "ESP32 C3 with USB" link and download the latest release (not the nightly builds)
  4. For the RP2040, you can download the file from here
  5. Then you must install the firmwares.
  6. For the RP2040, connect the board so the blue LED is lit, while pushing the BOOT button on the back side of the board. You can now see your RP2040 as a memory drive. It also appears in the Thonny window. In Thonny, select the uf2 file you just downloaded and move it or download it on this new drive (right click, select 'Download to /'). Or you can do it with the Windows explorer as well.
  • You also need to copy the attached file (tft_config.py) in the RP2040 drive (to correct a mistake in the one already in place)
  1. For the ESP32, it doesn't work like this. You need a wire or a paper clip!
  • First short IO9 to GND using the wire or the paper clip, then connect the board so the green LED is lit (no need to hit the BOOT button).
  • Open a command window on your computer
  • Erase the flash memory with this command:
C:\Path\To\Thonny\python.exe -u -m esptool --chip esp32c3 --port COM13 erase_flash
  • Unplug the board, short IO9 to GND and reconnect the board
  • Now flash the firmware of the ESP32:
C:\Path\To\Thonny\python.exe -u -m esptool --chip esp32c3 --port COM13 --baud 460800 write_flash -z 0x0 C:\Path\To\Firmware\esp32c3-usb-20220618-v1.19.1.bin
  • Of course, use the COM port number associated to your board (you can find it in the peripheral manager) and the correct paths.
  • Unplug the board, plug it again normally.

You're all set!

From now on, when you want to run a MicroPython code on one of the MCUs, you just need to connect the board, run Thonny, open a new tab or select an existing code in the flash memory, modify it and save it. Then hit the green triangle to run the code. Remember to select the correct interpretor in the lower right part of Thonny's window (either "Raspberry Pi Pico" or "ESP32": more information here if needed).

One nice feature of Thonny is that you can execute a code in the IDE: it runs on the microcontroller, but you can change anything then hit the Run button, instead of the compile / upload process of the Arduino IDE, which can be sometimes cumbersome. It's called REPL (Read Evaluate Print Loop).


Micropython comes with a large number of libraries that you can use for programming. We'll quickly see a few of them in the next steps. There is also a dedicated discussion and help forum on GitHub.


MCU: Micro Controller Unit

Downloads

HMI

T-Pico C3_v3.png
pullup_resistor.png

As said above, the board has a TFT display (1.14 inch, 135 x 240 pixels), one LED on its back, and 2 pushbuttons. All these are only adressable by the RP2040 (so, connect with the blue LED on).


The LED and the buttons can be managed using the machine library. It's a library dedicated to the management of your hardware. It has several classes, such as:

  • Pin
  • UART (see next step)

which we will use in this Instructables.

We also need the time library, similar to the one that exists in regular Python, with all time and duration related functions.


LED

Let's start with a simple code to blink the red LED. It's connected to IO25.

from machine import Pin
from time import sleep_ms
# create an output pin on pin #25
led = Pin(25, Pin.OUT)
while True:
    led.value(not led.value())
    sleep_ms(300)
To try this code, just open a new tab in Thonny, copy and paste the code above in the tab (check the tabulations in case of execution error as the copy/paste from here may change the code), and hit the Run button. You may need to hit the Stop button before, if the MCU is busy.

This code is quite straightforward, if you already know Python's syntax and a few about MCU programming. It first loads some methods from the required libraries, set the pin as an output port, and calls it led. Then, it loops forever to blink the led each 300 millisecond.

The value method is here used as read and write: it reads the state of the LED, flips it (True become False and vice versa) and sets the state of the LED.


Buttons

Now, let's use the buttons: they are connected to IO6 and 7. Let's say we want to display a message whenever one button is pushed, but we still want to blink the LED. We must first declare the instances of both buttons:

right_button = Pin (7, Pin.IN, Pin.PULL_UP)
left_button = Pin (6, Pin.IN, Pin.PULL_UP)

As they are declared as PULL_UP, they send False when pushed. The PULL_UP resistor is explained on the schematics above, from here. We can detect if a button is pushed by scanning its value:

stateRight = right_button.value()

To ensure that the message is displayed only when the button is pushed, we need to store the button's state, as follows:

if stateRight == False:      # button is pushed
    if not rightPushed:      # if it was not already down
        print("Right button pushed")
        rightPushed = True
else:                        # button is released
    rightPushed = False


First multithreading attempt

But if we add these lines at the end of the previous code, the buttons will only be read for a very short time every 300 ms, so the code won't be very reactive. We need to improve that: this is the first (very shy) application of multithreading. The idea is to run a loop for 300 ms during which the buttons are read. At the end of that loop, we just change the state of the LED.

To do this, we need to know the time and to compare the current time with the beginning time of the loop. The current time is given by ticks_ms from the time library. ticks_diff enables to compute the difference between 2 instants.

from time import ticks_ms, ticks_diff
start = ticks_ms()

Let's code the loop (pseudo-code but it's the same as above):

while ticks_diff(ticks_ms(), start) < 300:
    # Read the buttons and
    # Display message if pushed

Then flip the LED, as before:

led.value(not led.value())

All this goes inside the first while loop. Our code is now:

from machine import Pin
from time import ticks_ms, ticks_diff

led = Pin(25, Pin.OUT)

right_button = Pin (7, Pin.IN, Pin.PULL_UP)
left_button = Pin (6, Pin.IN, Pin.PULL_UP)

while True:
    start = ticks_ms()
    while ticks_diff(ticks_ms(), start) < 300:
        stateRight = right_button.value()
        stateLeft = left_button.value()
        if stateRight == False:
            if not rightPushed:
                print("Right button pushed")
                rightPushed = True
        else:
            rightPushed = False
        if stateLeft == False:
            if not leftPushed:
                print("Left button pushed")
                leftPushed = True
        else:
            leftPushed = False
    led.value(not led.value())


Display

Using the display is a little more tricky: we need to install another library called st7789_mpy. You can install it using Thonny (Tools / Manage Plug-Ins). In this repository, you can find some examples, and a few fonts that are ready for use. You just need to copy the font files into the RP2040 drive as you already know.

If you look at the Hello.py example (I won't copy it here, I'll just comment it), you first need to import the library and the configuration file (download it from previous step):

import st7789
import tft_config

then declare and initialize the display:

tft = tft_config.config(0)
tft.init()

'0' defines the orientation: 0=Portait, 1=Landscape, R=Reverse portait, 3=Reverse landscape.

Here are some interesting functions and constants to know:

  • Colours: some are already defined (st7789.BLACK, st7789.BLUE, st7789.RED, st7789.GREEN, st7789.CYAN, st7789.MAGENTA, st7789.YELLOW, st7789.WHITE), but you can define your own from RGB values as follows:
colour = st7789.color565(red, green, blue)

with red, green and blue values from 0 to 255.

  • use tft.fill(colour) to fill the screen in the given colour,
  • tft.rect will display a rectangle, tft.fill_rect will fill a rectangle. The same with circle,
  • hline and vline draw horizontal and vertical lines.

To print a text on the display, you need to load a font. It's usually a .py file, load it as follows:

import NotoSans_32 as font

Then use write to display string:

tft.write(font, string, size, x, y, foreground_colour, background_colour)

x and y are the coordinates of the upper left corner of the box that contains the text. write uses bitmap fonts, if you want to use vector fonts, you need the draw method.

All methods are presented here, but these are the most used.

UART Communication

UART.png

Since we want to use all cores from both processors, but only the RP2040 is able to use the display, we need some sort of communication so the ESP32 can send information to the RP2040 for us to see it actually works. One way to do this is to implement the UART communication between both MCUs.

For this we need the UART class from the machine library.

from machine import UART

Wikipedia says that UART "is a computer hardware device for asynchronous serial communication in which the data format and transmission speeds are configurable". From the RP2040's point of view, the GPIO used for the UART are:

  • TX: GPIO8
  • RX: GPIO9

Additional GPIOs are used for flow control:

  • CTS: GPIO10
  • RTS: GPIO11

We declare the UART port with the following instruction, and call it 'ESP32'.

ESP32 = UART(1, baudrate=38400, tx=Pin(8), rx=Pin(9), cts=Pin(10), rts=Pin(11))

'1' is the Id of the communication port, and the clock rate is set to 38400 bits/s (the upper limit as far as I know).

For the ESP32, the GPIOs are different, and the declaration is:

RP2040 = UART(1, baudrate=38400, tx=7, rx=6, cts=5, rts=4)
Do not use the init method, it may change your settings.
Notice the difference: on the RP2040 we use Pin(n) to set GPIOs, and only numbers on the ESP32.

The most useful methods of this class are the following:

  • write: to send a string,
  • any: returns the number of bytes that can be read on the bus, or 0 if it is empty,
  • read: read a given number of incoming bytes,
  • readline: read until a newline (aka \n) character is met.


To test its behaviour, we need 2 codes, one for each microcontroller. In the following test, the ESP32 will act as a transmitter and the RP2040 as a receiver. The RP2040 will display the messages it receives.

First, let us create a code for the ESP32, which sends a new random number each second.

from machine import UART
from random import randint
from time import sleep

RP2040 = UART(1, baudrate=38400, tx=7, rx=6, cts=5, rts=4)
while True:
  n = randint(1, 100)
  message = str(n) + '#'
  RP2040.write(message)
  sleep(1)

The code imports the necessary methods from the libraries, creates the UART, generates a message each second and sends it to the RP2040. Note that each message ends with a specific character (the #) to indicate the end of the message.

Now, for the receiver:

from machine import UART, Pin

ESP32 = UART(1, baudrate=38400, tx=Pin(8), rx=Pin(9), cts=Pin(10), rts=Pin(11))
line = ''

while True:
    while ESP32.any() > 0:
        buf = ESP32.readline()
        line += buf.decode("utf-8")
        if '#' in line:
            print("Received: " + line)
            line = ''

After importing the libraries and declaring the UART, the code scans the input port. When a message arrives, the code assembles it byte per byte (thanks to the decode method, which converts it from raw byte format). When the end character is reached, it displays the message.

If we want to see the received message in Thonny's console, we need to connect the board on the RP2040 side and run this last code. But the ESP32's code must also be running, although we are not connected to it to launch it : it must therefore run at boot. To do that, we need to go back to the ESP32 and create or modify a file called main.py.

When the board boots, each MCU executes a first python script called boot.py which basically does all initialization needed only once (similar to the setup() function of Arduino in C). Then it runs the main.py script which contains the code you want to run at boot.

So there are 2 possibilities:

  1. Copy the ESP32 code into the main.py file
  2. Or copy the code in another file (for example myUART.py) and edit the main.py to put inside:
import myUART

This way, you can save as many python scripts as you want in your ESP32 file drive and run the one you like only by changing the content of main.py.

When this is done, go back to the RP2040 side and do the same, or just run its code from the IDE. You will see something like this in the console:

Received: 6#
Received: 11#
Received: 70#
Received: 79#
Received: 96#
Received: 53#
...

Now we know how to make both MCUs communicate. What else do we need?


UART : Universal Asynchronous Receiver / Transmitter

Web Server

Server.PNG

It would be nice also to be able to interact with the device, and control some parameters that could be easily verified. One way to do this is to take advantage of the WiFi capacities of the ESP32 to have it home a web server.

To do this we need to create a WiFi access point on the ESP32, and create the web server. For the WiFi, the following code creates a WiFi access point, call ESP-AP that you can find using your smartphone or computer WiFi scanner:

import network
# Create access point
ap = network.WLAN(network.AP_IF)
ap.config(essid='ESP-AP') # Change the name as you prefer...
ap.active(True)
# Print the SSID
print("Connect to: " + ap.config('essid'))

No password required to connect. The documentation for the network library is available here.

For the web server, I chose a very light library called microdot. To install it on your device, connect it on the ESP32 side and copy the file microdot.py available here in the ESP32 drive. That's all for now : you can run the first example of the library:

from microdot import Microdot
app = Microdot()

@app.route('/')
def index(request):
    return 'Hello, world!'

app.run(host='192.168.4.1', debug=True, port=80)

If you are connected to the previous access point and open your browser at the URL: http://192.168.4.1 you will see the message 'Hello, world!' (hopefully).


That was the most basic web server in the world, we need something more interesting. Actually, the app.run instruction is an endless loop: whatever comes after this line is never executed. That's not what we want.


Second multithreading attempt

To have a more interacting code, we need another version of the web server, based on the asyncio library.

asyncio is a library to write concurrent code

This will be delt with in the next step, but I can cite Wikipedia for more information about this: "Concurrent computing is a form of computing in which several computations are executed concurrently — during overlapping time periods — instead of sequentially".

Here, we need to copy some additional files into the ESP32 drive: from here, copy the file microdot_asyncio.py. The next code will create an asynchronous server with a web page to choose a colour that we will apply as background colour on the display of the T-Pico C3.

We have already seen everything required to do this:

  • Web server on the ESP32,
  • UART communication from the ESP32 to the RP2040,
  • Display controlled by the RP2040.

We need 2 codes again. First the RP2040 side (the easiest): it creates the UART, scans any incoming message, extracts the RGB component values and sets the background colour of the display. Connect the board on the RP2040 's side and save the following code as bgColour.py

from machine import UART, Pin
import st7789
import tft_config

# Init UART
ESP32 = UART(1, baudrate=38400, tx=Pin(8), rx=Pin(9), cts=Pin(10), rts=Pin(11))

# Init display
tft = tft_config.config(1)
tft.init()
bg = st7789.BLACK
tft.fill(bg)

line = ''
while True:
    while ESP32.any() > 0:
        buf = ESP32.readline()
        line += buf.decode("utf-8")
    if "=" in line:
        # Change background color
        print("Reveived: " + line)
        [r, g, b]=[int(s) for s in line.split() if s.isdigit()]
        bg = st7789.color565(r, g, b)
        tft.fill(bg)
        line = ''

The received message ends with the character '=': when this character is found, the RGB values are extracted and the background is changed.

Now connect the board on the ESP32's side and copy this program.

import uasyncio
from microdot_asyncio import Microdot
from machine import UART
import network
import time

# Create access point
ap = network.WLAN(network.AP_IF)
ap.config(essid='ESP-AP')
ap.active(True)
print("Connect to: "+ap.config('essid'))

# Define server
app = Microdot()
count = 0

# HTML code for the web page
htmlRGB = '''<!DOCTYPE html>
<html>
    <head>
        <title>Enter RGB values</title>
    </head>
    <body>
        <h1>Set RGB values</h1>
        <form action="/rgb?r=red&g=green&b=blue">
        <label for="red">Red:</label><br>
        <input type="range" id="red" name="r"
         min="0" max="255" value="100"><br>
        <label for="green">Green:</label><br>
        <input type="range" id="green" name="g"
         min="0" max="255" value="50"><br>
        <label for="blue">Blue:</label><br>
        <input type="range" id="blue" name="b"
         min="0" max="255" value="25"><br><br>
        <input type="submit" value="Submit">
        </form> 
    </body>
</html>
'''

@app.route('/')
async def hello(request):
    return htmlRGB, 200, {'Content-Type': 'text/html'}

@app.errorhandler(404)
async def not_found(request):
    return {'error': 'resource not found'}, 404

@app.route('/rgb')
async def rgb(request):
    red   = int(request.args['r'])
    green = int(request.args['g'])
    blue  = int(request.args['b'])
    msg   = f'Red: {red} Green: {green} Blue: {blue} =' # '=' at the end of the message
    print(msg)
    RP2040.write(msg)
    return "Sending:\n" + msg

# set UART
RP2040 = UART(1, baudrate=38400, tx=7, rx=6, cts=5, rts=4)
uasyncio.sleep_ms(50)
try:
    app.run(host='192.168.4.1', debug=True, port=80)
except:
    app.shutdown()

Save it in the ESP32 as serverColour.py. Then, edit the main.py file and write inside (remove everything else):

import serverColour

Then go back to the RP2040 side and run the previous code bgColour.py. Connect to your access point (ESP-AP), open your browser and type the URL 192.168.4.1: you should see the web page as shown on the picture above. The HTML code for the web page is defined in the htmlRGB variable: there are 3 sliders, one action button and the associated action is:

action="/rgb?r=red&g=green&b=blue"

It calls the route defined here:

@app.route('/rgb')

which get the 3 values of the colour's components, assembles a message (ending with the character '=') and sends it over the UART to the RP2040.

Just select the 3 values for the RGB components and hit the "Submit" button: you will see the colour of the display change. Additionally, the server and Thonny will display the message that is sent through the UART, such as:

Red: 17 Green: 47 Blue: 215 =


This has set the bases for asynchronous multithreading on the ESP32, which we will use in the next steps...

Multi-whatever

Four.png

Weird title, I know. It means that there are multiple ways of running various tasks at the same moment on a MCU.

We have seen one method in step 2 (also presented in my other Instructables) which essentially consists in watching the clock and acting at well chosen instants.

The second one is illustrated by the uasyncio library shown in step 4: we create tasks (short functions called coroutines) with "sleeping periods" and a scheduler executes each task during the sleeping periods of the other tasks. In our use in step 4, it was transparent because the tasks were created for us by the microdot library.

Both are quite similar, they are examples of "concurrent computing".

The third way of doing this is to run each task in parallel on a different core. As the RP2040 has two cores, it can handle 2 tasks with this method. They are not concurrent because they use different resources, and do not need to share anything, except for some memory parts if required. In Python, this can be done with global variables.


The T-Pico C3 board has 2 microprocessors: the RP2040 has 2 cores, the ESP32 C3 has one. So we can run parallel tasks and concurrent tasks in various ways (hence the image above):

  1. Parallel tasks between 2 MCUs,
  2. Parallel tasks between the cores of the MP2040,
  3. Concurrent tasks in the ESP32,
  4. Concurrent tasks in each core of the RP2040.

We already implemented the first case in steps 3 and 4. We also implemented (sort of) the third case in step 5. To try the second case, we need the _thread library, a low-level threading API. It provides low-level primitives for working with multiple threads. The micropython port is unfortunately not documented, so we must refer to the python documentation, here.

With this library, we can create 2 threads which can run on each core of the MP2040. To create a thread, we just need to write a function and assign it to a new thread with _thread.start_new_thread. For example:

import _thread
import time

# Thread running on core 0
def RP0():
    i = 0
    while True:
        print("Core 0 :" + str(i))
        i = i + 1
        time.sleep_ms(300)
        
# Thread running on core 1
def RP1():
    i = 0
    while True:
        print("Core 1 :" + str(i))
        i = i + 1
        time.sleep_ms(700)
        
# The main program starts here
_thread.start_new_thread(RP1,())
RP0()

This program creates 2 threads, the first one is automatically assigned to the core 0, and the other is assigned to the core 1 with the instruction start_new_thread. You can use the '()' to pass arguments to the function.

When running the code in Thonny, you will get this output:

Core 1 :0
Core 0 :0
Core 0 :1
Core 0 :2
Core 1 :1
Core 0 :3
Core 0 :4
Core 1 :2
Core 0 :5
Core 0 :6
Core 1 :3
etc.

The prints of core 0 are more frequent than those from core 1, which waits a little longer each time.


So running 2 threads on 2 cores is as simple as that. However, it seems that the RP2040 can only run one thread in each core (if you try to create another thread, it will throw an error). On the other hand, the ESP32 C3, even with a single core, can run up to 13 threads concurrently! Yes, the ESP32 is powerful, and should definitively not be acting only as a wireless peripheral...


To sum up:

We have seen how to control the red LED, the buttons, the display, how to make both MCU communicate, how to run an asynchronous webserver and various ways of running several tasks in parallel. Let's assemble everything and see what the T-Pico C3 has in his guts...

The Codes

20220926_183558_copy_1843x1036.jpg
20220926_184347.jpg
20220926_184501.jpg

This demonstration is not really very impressive, but it shows many things:

On the ESP32's side:

  • Create a full duplex UART connexion with the RP2040,
  • Create a WiFi access point,
  • Run an asynchronous web server,
  • With the server, select the background colour for the display and send it to the RP2040,
  • Concurrently and periodically increase a counter and send its value to the RP2040,
  • Receive orders from the RP2040 to reset the counter.

On the RP2040's side:

  • Create a full duplex UART connexion with the ESP32,
  • Create 2 parallel tasks (one on each core) that increment a counter with different periods,
  • Display the values of the 2 counters,
  • Display the number of seconds since boot,
  • Receive and display the value of the ESP32's counter,
  • Flash the red LED when the ESP32's counter changes,
  • Receive the colour value from the ESP32 and change the background accordingly,
  • Concurently monitor both buttons:
  • If the left button is pushed: send a reset order to the ESP32,
  • If the right button is pushed (shortly): reset the counter of core 0,
  • If the right button if (long) pushed: reset the counter of core 1.

Yes, both MCUs are busy, and everything runs smoothly...

To do this, you need to download the 2 codes below (call them RP2040.py and ESP32.py), each one on its side

I mean that RP2040.py will run on the RP2040 and ESP32.py on the ESP32...

Also, change the content of the main.py scripts on each side: on the RP2040's side it must contain:

import RP2040

and on the ESP32's side, it must contain:

import ESP32

Now, both codes will run at boot. You can even supply the board with a power bank, there is no need to connect it to your computer. The display begins to show the values of the 3 counters. The red LED flashes when the ESP32 counter changes. Push the buttons to see what happens.

Then, connect your WiFi to the access point (call ESP-AP) and open the browser on http://192.168.4.1. You can select the RGB values and send them to the board, and see the background colour change.


_______________________________________

Here are the two codes:

ESP32.py:

import uasyncio
from microdot_asyncio import Microdot
from machine import UART
import network
import time

# Create access point
ap = network.WLAN(network.AP_IF)
ap.config(essid='ESP-AP') # essid instead of ssid
ap.active(True)
# Query params one by one
print("Connect to: "+ap.config('essid'))

# Define server
app = Microdot()
count = 0

htmlRGB = '''<!DOCTYPE html>
<html>
    <head>
        <title>Enter RGB values</title>
    </head>
    <body>
        <h1>Set RGB values</h1>
        <form action="/rgb?r=red&g=green&b=blue">
        <label for="red">Red:</label><br>
        <input type="range" id="red" name="r"
         min="0" max="255" value="100"><br>
        <label for="green">Green:</label><br>
        <input type="range" id="green" name="g"
         min="0" max="255" value="50"><br>
        <label for="blue">Blue:</label><br>
        <input type="range" id="blue" name="b"
         min="0" max="255" value="25"><br><br>
        <input type="submit" value="Submit">
        </form>
        <p></p>
        <form action="/shutdown">
        <input type="submit" value="Shutdown server">
        </form>
    </body>
</html>
'''

@app.route('/')
async def hello(request):
    return htmlRGB, 200, {'Content-Type': 'text/html'}

@app.route('/shutdown')
async def shutdown(request):
    request.app.shutdown()
    return 'The server is shutting down...'

@app.errorhandler(404)
async def not_found(request):
    return {'error': 'resource not found'}, 404

@app.route('/rgb')
async def rgb(request):
    red   = int(request.args['r'])
    green = int(request.args['g'])
    blue  = int(request.args['b'])
    msg   = f'Red: {red} Green: {green} Blue: {blue} ='
    print(msg)
    RP2040.write(msg)
    return "Sending: " + msg

async def counter(period_ms):
    global count
    while True:
        start = time.ticks_ms()
        count += 1
        if count == 100:
            count = 0
        while time.ticks_diff(time.ticks_ms(), start) < period_ms:
            await uasyncio.sleep_ms(200)
            # if receive a message, reset the counter
            if RP2040.any() > 0:
                count = 0
                buf = RP2040.read() # Empty the UART buffer
        buf = str(count) + '#'
        RP2040.write(buf)

# set UART
RP2040 = UART(1, baudrate=38400, tx=7, rx=6, cts=5, rts=4)
uasyncio.sleep_ms(50)
try:
    uasyncio.create_task(counter(678))
    app.run(host='192.168.4.1', debug=True, port=80)
except:
    app.shutdown()

RP2040.py:

from machine import UART, Pin
import st7789
import tft_config
import NotoSans_32 as font
import _thread
import time

def initDisplay():
    # Called whenever the background changes
    global fg, bg, width, w
    height = tft.height()
    width = tft.width()
    w = int(tft.write_len(font, "ESP")/2)
    tft.fill(bg)
    tft.rect(0, 0, width, height, fg)
    tft.vline(int(width/3), 0, 90, fg)
    tft.vline(2*int(width/3), 0, 90, fg)
    tft.hline(0, 90, width, fg)
    tft.write(font, "RP0", int(width/6)-w, 10, fg, bg)
    tft.write(font, "RP1", 3*int(width/6)-w, 10, fg, bg)
    tft.write(font, "ESP", 5*int(width/6)-w, 10, fg, bg)

# RP2040 Threads
def RP0():
    global zero, fg, bg, width, w
    line = ''
    n0 = 0
    h = 30
    x = int(width/6)-w+8
    y = 50
    counter = 0
    pushed = False
    start = time.ticks_ms()
    while True:
        # Watch time flowing
        if time.ticks_diff(time.ticks_ms(), start) > 1000:
            start = time.ticks_ms()
            counter += 1
            count = 'time: '+str(counter)+'s'
            tft.fill_rect(55, 100, 160, 30, bg)
            tft.write(font, count, 55, 100, fg, bg)

        # Display core 0
        tft.fill_rect(x, y, 2*w, h, bg)
        tft.write(font, str(n0), x, y, fg, bg)
        n0 += 1
        if n0 == 100: n0 = 0
                
        # Display ESP32
        start32 = time.ticks_ms()
        while time.ticks_diff(time.ticks_ms(), start32) < 234:
            # Process incoming uart message, if any: change bg color, change led status, display message
            while ESP32.any() > 0:
                buf = ESP32.readline()
                line += buf.decode("utf-8")
            if '#' in line:
                # Change value
                tft.fill_rect(5*int(width/6)-w+8, y, 2*w+2, h, bg)
                tft.write(font, line[:-1], 5*int(width/6)-w+8, y, fg, bg)
                led.value(not led.value())
                line = ''
            elif "=" in line:
                # Change background color
                print(line)
                [r,g,b]=[int(s) for s in line.split() if s.isdigit()]
                bg = st7789.color565(r, g, b)
                initDisplay()
                line = ''
                        
            # Manage button
            state = right_button.value()
            if state == False: # button pushed
                if not pushed:
                    startb = time.ticks_ms()
                pushed = True
            elif pushed:
                pushed = False
                if time.ticks_diff(time.ticks_ms(), startb) > 500: # long push
                    zero = True
                else: # short push
                    n0 = 0

def RP1():
    # Change and display RP1 value
    global zero, width, fg, bg
    time.sleep_ms(50)
    n1 = 0
    h = 30
    x = 3*int(width/6)-w+8
    y = 50
    while True:
        start = time.ticks_ms()
        n1 += 1
        if n1 == 100: n1 = 0
        tft.fill_rect(x, y, 2*w, h, bg)
        tft.write(font, str(n1), x, y, fg, bg)
        while time.ticks_diff(time.ticks_ms(), start) < 1524:
            # Check button to send command to ESP32
            state = left_button.value()
            if state == False:
                ESP32.write('z')
            # Check if long press on right button to zero RP1 count
            if zero:
                n1 = -1
                zero = False

# Init buttons
right_button = Pin (7, Pin.IN, Pin.PULL_UP)
left_button  = Pin (6, Pin.IN, Pin.PULL_UP)

# Init red LED
led = Pin(25, Pin.OUT)
led.off()

# Init display
tft = tft_config.config(1)
tft.init()
bg = st7789.BLUE
fg = st7789.WHITE
initDisplay()

# Init thread core 1
zero = False
_thread.start_new_thread(RP1,())

# Init UART
ESP32 = UART(1, baudrate=38400, tx=Pin(8), rx=Pin(9), cts=Pin(10), rts=Pin(11))

RP0()

Downloads

Enjoy!

Screen.png

I hope you enjoyed this trip inside the T-Pico C3 parallel programming in Micropython.