I want to apply my CircuitPython lessons to a project that will require importing the busio library to utilize asynchronous serial communication hardware on board a RP2040 chip. This is a pretty common microcontroller scenario, but example projects usually only deal with one direction, either just sending or just receiving. Adafruit's CircuitPython UART serial introduction only received data and did not send. Things will get more complex as I intend to implement bidirectional communication, and even more so because there's interaction between the two directions: I need to send data and listen for an acknowledgement.

If that's the only thing being communicated, it would be easy: one line of code to send data, followed by a line of blocking code to wait for response data. But it's more complicated in this project because the K13988 chip on the control panel sends key scan matrix reports on a regular basis. (Every ~9ms.) If I wrote my code in the naive way, that wait for response may pick up a key scan matrix report byte instead of the acknowledgement byte. What I need is something that is constantly receiving and processing data, mostly key scan report bytes. After the data sender code runs, it will have to wait on the receiver loop to pick out any acknowledgement bytes as they come through in between key scan reports.

This is doable within an Arduino-style loop(), and I've done it before, but I end up with code that's hard to follow as it rapidly switches between multiple contexts. That's why I was happy to learn CircuitPython implements the async/await pattern with its asyncio library, which should theoretically allow cleaner code structure. My prior experiences with this pattern mostly centered around JavaScript client/server networking code and not microcontrollers, but never fear, as per usual for Adafruit there's an excellent guide Cooperative Multitasking in CircuitPython with asyncio putting the pattern in context for microcontroller tasks.

It also highlights an important simplifying characteristic of CircuitPython: it has only a single thread of execution running a single event loop, so there is no concern of race conditions between multiple threads/loops and we don't have to worry about protecting critical sections. Eliminating concurrency eliminates all the huge headaches of writing concurrent code, at the performance cost of running only a single core. A RP2040 has two cores, like an ESP32, but apparently one core sits idle while the other runs CircuitPython. For this project (and most of my potential CircuitPython projects) a single CPU core will suffice. I'm not going to worry about the lack of dual-core operation until I run into an actual problem that needs it. Right now my problems are simple, like checking to see if there's any activity on a pin.


Installation note: for most CircuitPython libraries in the bundle, we install it by copying its single corresponding *.mpy file into the \lib\ subdirectory on our microcontroller's CIRCUITPY storage volume. But asyncio is actually a directory containing multiple *.mpy files and we copy the entire directory under \lib\ so all its components are under \lib\asyncio\*.mpy.