In this guide, we’ll take a look at the basics of MicroPython asynchronous programming with the Raspberry Pi Pico using the asyncio module. You’ll learn to run multiple tasks concurrently, making the illusion of multitasking and avoiding blocking your code on long-running tasks.
New to the Raspberry Pico? Check out our eBook: Learn Raspberry Pi Pico/Pico W with MicroPython
For example, your program can be waiting for the response of a server and still be able to do other tasks like checking if a button was pressed or blinking an LED at the same time.
Asynchronous programming can be useful in projects that include: interacting with databases, communicating over networks (like when requesting data from a server, or when the Pico acts as a web server), reading sensor data, displaying output to a screen, receiving inputs from users, and much more.
Table of Contents:
In this tutorial, we’ll cover the following topics:
- Introducing Asynchronous Programming
- The asyncio MicroPython Module
- Basic Example of an Asynchronous Program
Prerequisites
Before proceeding, make sure you check the following prerequisites:
MicroPython Firmware
To follow this tutorial you need MicroPython firmware installed in your Raspberry Pi Pico board. You also need an IDE to write and upload the code to your board.
The recommended MicroPython IDE for the Raspberry Pi Pico is Thonny IDE. Follow the next tutorial to learn how to install Thonny IDE, flash MicroPython firmware, and upload code to the board.
Alternatively, if you like programming using VS Code, you can start with the following tutorial:
Introducing Asynchronous Programming
Asynchronous programming is a technique that enables your program to start a potentially long-running task and still be responsive to other events while that task runs, rather than having to wait until that task has finished.
This is achieved by executing tasks in a non-blocking manner and using callback functions to handle the results. This way, the program can continue executing other tasks while waiting for the results of the asynchronous task. On the other hand, in synchronous programming, each task must wait for the previous task to complete before starting.
In summary…
- Asynchronous is a non-blocking architecture, so the execution of one task isn’t dependent on another. Tasks can run simultaneously.
- Synchronous is a blocking architecture, so the execution of each operation depends on completing the one before it.
The asyncio MicroPython Module
MicroPython provides the asyncio module, which is a lightweight asynchronous I/O framework inspired by Python’s asyncio module. You can check all the details about this MicroPython module on the following link:
Let’s take a quick look at some basic concepts you need to know to start using asyncio for asynchronous programming:
- Event Loop: it’s a loop that continually checks for events (tasks or coroutines) and executes them.
- Tasks: individual units of work or coroutines that are scheduled to run concurrently within the event loop
- Asynchronous Functions: also known as coroutines, these are functions that can be paused and resumed without blocking other operations, enabling concurrent execution of multiple tasks.
- await: is a keyword used inside coroutines to pause the execution of the current coroutine until a specific event or operation completes, allowing other coroutines to run in the meantime.
Now, let’s take a more detailed look at each of those concepts and the workflow and methods from the asyncio module to write an asynchronous program.
Event Loop
An event loop is the core of asynchronous programming. It’s a loop that continually checks for events (tasks or coroutines) and executes them. In asyncio, you create an event loop using asyncio.get_event_loop().
The following line creates an instance of the event loop called loop. You can use that loop to manage and schedule asynchronous tasks.
loop = asyncio.get_event_loop()
Creating Tasks
In asynchronous programming, tasks represent units of work. You create tasks to execute coroutines concurrently. Coroutines are functions defined with the async keyword, that can be paused and resumed, allowing asynchronous programming.
For example:
async def blink_led():
#Code to blink an LED
Then, we can use loop.create_task() to schedule this coroutine as a task to be executed by the event loop.
loop.create_task(blink_led())
Running the Event Loop
Once you’ve created tasks, you start the event loop to execute them. The loop will continually check for scheduled tasks and execute them. The following line initiates the event loop, as we’ve seen previously.
loop = asyncio.get_event_loop()
Then, loop.run_forever() makes it run indefinitely, constantly checking for tasks to execute.
loop.run_forever()
Asynchronous Functions – async def
An asynchronous function is defined using the async def syntax. These functions, also known as coroutines, can be paused with the await keyword, allowing other coroutines to run in the meantime. For example:
async def blink_led():
while True:
led.toggle() # Toggle LED state
await asyncio.sleep(1)
Here, blink_led() is an asynchronous function. It toggles an LED and then pauses for 1 second using await asyncio.sleep(1). During this pause, other tasks can run.
await asyncio.sleep(1)
Asynchronous Delay – acyncio.sleep()
asyncio.sleep() is a coroutine provided by the asyncio module and it is used to introduce a delay in the execution of a coroutine for a specified duration without blocking the entire event loop. So, to create an asynchronous function, you must replace all your time.sleep() with asyncio.sleep().
When asyncio.sleep() is called within a coroutine, it temporarily suspends the execution of that coroutine, allowing other coroutines to run in the meantime.
The event loop continues to run while the coroutine is paused, checking for other tasks and events.
After the specified duration (seconds), the paused coroutine resumes execution from the point where asyncio.sleep() was called.
await asyncio.sleep() is a non-blocking way to yield control to other coroutines in the event loop without introducing any actual delay. It effectively allows other coroutines to run immediately.
await
The await keyword is used inside coroutines to indicate a point where the coroutine can be temporarily suspended until a specific event is completed.
In the following example, the coroutine pauses for 1 second without blocking the entire event loop.
await asyncio.sleep(1)
In Summary…
When you run an asynchronous program, the event loop runs continuously, executing the scheduled tasks concurrently. Each coroutine gets a turn to run, and the await statements allow the event loop to switch between tasks, creating the appearance of parallelism.
As a result, you can perform multiple tasks concurrently without having to wait for each one to complete before moving on to the next.
Basic Example of an Asynchronous Program
Now that you know the basic concepts of asynchronous programming and the basic methods of the asyncio module let’s create a simple example to apply the concepts learned.
We’ll create two different coroutines. Each coroutine will blink a different LED at a different rate.
- Green LED: GPIO 20 >> blinks every two seconds;
- Blue LED: GPIO 19 >> blinks every half a second.
To visually check the final result, wire two LEDs to the Raspberry Pi Pico, one to GPIO 20 and other to GPIO 19.
Here’s our example code.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-raspberry-pi-pico-asynchronous-programming/
import asyncio
from machine import Pin
green_led_pin = 20
green_led = Pin(green_led_pin, Pin.OUT)
blue_led_pin = 19
blue_led = Pin(blue_led_pin, Pin.OUT)
# Define coroutine function
async def blink_green_led():
while True:
green_led.toggle()
await asyncio.sleep(2)
# Define coroutine function
async def blink_blue_led():
while True:
blue_led.toggle()
await asyncio.sleep(0.5)
# Define the main function to run the event loop
async def main():
# Create tasks for blinking two LEDs concurrently
asyncio.create_task(blink_green_led())
asyncio.create_task(blink_blue_led())
# Create and run the event loop
loop = asyncio.get_event_loop()
loop.create_task(main()) # Create a task to run the main function
loop.run_forever() # Run the event loop indefinitely
How the Code Works
Let’s take a quick look at the code.
We create two coroutines. One for each LED:
# Define coroutine function
async def blink_green_led():
while True:
green_led.toggle()
await asyncio.sleep(2)
# Define coroutine function
async def blink_blue_led():
while True:
blue_led.toggle()
await asyncio.sleep(0.5)
Inside each coroutine, we toggle the LED state in a loop and await (asynchronous waiting) for a specific interval await asyncio.sleep(0.5).
We create another coroutine called main() that serves as a central point where you can organize and coordinate the execution of those tasks.
In the main() coroutine, we create tasks for both blink_green_led() and blink_blue_led() functions to run concurrently.
# Define the main function to run the event loop
async def main():
# Create tasks for blinking two LEDs concurrently
asyncio.create_task(blink_green_led())
asyncio.create_task(blink_blue_led())
Then, we create an event loop.
loop = asyncio.get_event_loop()
The loop.create_task(main()) creates a task to run the main() coroutine function.
loop.create_task(main()) # Create a task to run the main function
Finally, the run_forever() method will start running the event loop indefinitely. The event loop continually checks for tasks to execute, runs them concurrently, and manages their execution.
loop.run_forever() # Run the event loop indefinitely
Testing the Code
Run the previous code on your Raspberry Pi Pico. The result will be two blinking LEDs at different rates.
Wrapping Up
This is just a simple example to show you how to write an asynchronous program. Now, you should understand that instead of blinking an LED, you can do more complex tasks like reading a file, requesting data from the internet, handle a web server with multiple clients, and more. We have an example of an asynchronous web server that you can check on the following link:
We hope you find this guide useful. To learn more about the Raspberry Pi Pico, make sure to take a look at our resources:
Very interesting!
Is it not an invertion with the delay for leds?
At least I’ve not undertood correctly but in the code the green led is toggled each 0.5 second and for the blue one each second.
Thank you.
Hi.
You’re right.
My mistake. The code is correct now.
Regards,
Sara
Minor error is description of the Green-Blue blink example (Basic Example). The issue is the term “blinks every” as it is not precise, and used with 2 different definitions: For Green “blinks every” refers to the period of the blinking (1 Sec ON, 1 Sec OFF). For the Blue “blinks every” refers to the duration of the ON or OFF state (0.5 Sec each).
Green LED: GPIO 20 >> blinks every two seconds; // Green coded to blink with 2 Sec period
Blue LED: GPIO 19 >> blinks every half a second. // Blue coded to blink with 1 Sec period
The half period of Green is 1.0 sec: (await asyncio.sleep(1.0))
The half period of Blue is 0.5 Sec: (await asyncio.sleep(0.5))
I left a comment and wanted to edit it but I could not. So I tried posting a reply to my comment but could not because it was not yet reviewed. So I posted it as a second comment to the article and my first comment was discarded! So here the text of my second comment:
Note: I did not see the original code before Sara “Fixed” it.
However there are 2 places where the code for the function: “async def blink_blue_led():” is listed on this web-page (in the Basic Example section, and in the How the code works section) and the delay is not identical between these 2 ( with 2 different sleep times (2 Sec and 1 Sec)).
Hi.
I’m sorry. I missed the explanation section.
It should be fixed now.
Regards,
Sara
Ok, I reviewed the code (2 places) and the explanation section and they all agree.
Thanks for corrections.
Hi Sara and Rui, .. I’m a bit confused about the need for this async programming. For a Raspberry Pi, the operating system (Linux) allows for any number of programs to be operating simultaneously and by writing/reading files, to communicate with each other if necessary. So, can you give an example of where these “in a single program” async functions do something that, for example 2 separate python programs operating in LInux wouldn’t? Thanks.
You are correct that the Raspberry Pi runs Linux. And Python (CPython) allows multi-threading applications to be written.
However this article is about MicroPython running on a uController, specifically the Raspberry Pi Pico (although there is an article for the ESP32 (and its little sibling)). These uControllers do not run Linux, and may even run without any RTOS.
Hi.
This article is about the Raspberry Pi Pico, and not the “regular” Raspberry Pi.
Regards,
Sara
… got it. Thanks. … I don’t know where to ask this question, but I’ll try here: What is the advantage of a Pi Pico versus a Pi Zero-W ? … both have small form factor. Both are low cost, but the Zero has so much more it can do with an operating system and a full python suite (again, I don’t know where to otherwise ask this … sorry).
I answered your question in the forum: https://rntlab.com/question/raspberry-pi-pico-versus-raspberry-pi-zero-w/
Hi Rui and Sarah – this is a bit basic but I’m using Thonny and all I get is the ‘module not found’ error for asyncio. However if I use the line ‘import uasyncio as asyncio’ there appear to be no issues and the code works. I don’t know where to find the libraries or if I need to update something. can you help? (Apologies if this is covered elsewhere!)
Thanks for the great articles!
Hi.
Are you using a Raspberry Pi Pico?
Take a look at the firmware you’re loading.
Make sure you’re uploading a recent firmware version.
In recent versions the uasincio was replaced with asincio.
Probably you just need to upload the firmware with the most recent version.
Regards,
Sara