Mon, 20 Nov 2023 23:24:35 -0600Typo-O Asynchronicity

mr's Preposter.us Blog

Spent some time tonight working more on the software side of Type-O.  In particular, refactoring the code to work in a more modular, asynchronous fashion.

I’m using a library called asyncio which among other things grants CircuitPython the ability to run coroutines.  If you’re not familiar, they are a sort of simulated multithreading that allows more than one thing to appear to run at the same time on a single processor core.  Many languages have such a concept but I have to say that this is the first time I’ve encountered this in a microcontroller.

Normally for an embedded project like this I would manually handle sharing a single event loop across the various tasks I’m making the hardware perform, but this particular application has several operations where it’s not completely obvious which will consume the most amount of time.  Since the facility is available, I thought it was worth a shot to see if I could delegate some of the scheduling work to the tooling.

I’ve done parallel/concurrent programming lots of different ways and I generally prefer a more direct approach (working directly with threads and cores and such) but I will admit that for tasks where absolute performance isn’t the primary objective the async/await pattern is probably a lot simpler.  Since everything is in the same thread and shares memory it’s even simpler than something like Go’s “goroutines” with channels and mutates to worry about.

The hardest part is probably deciding where to begin, so I tried not to think too hard about that and instead just dove-in.

The basic idea is to define a function for each process you need to run in parallel, and then define a class to contain the data that will be shared between the processes.  I created a function for each I/O device: keyboard, display, storage, hid and the debugger (serial console).

Next I figured out what data needed to be shared between them.  In my case it’s really only the input from the keyboard (at least for now).  This could be done with a single “value” property on the class, but I want to make sure each device gets a chance to “consume” each character before a new one comes in.  The way I handled this for now was to create a property for each device (display_char, storage_char, etc.).  When they keyboard process reads a new keystroke, it stores the value in all of the device properties, but when each device process reads the value it reads it from its specific property and then zeros it out.

This might get more complex later but it works well enough for now.

All that’s left once the functions and shared data we defined is to start all the functions running with asyncio.gather().

image0.png

With this I have a framework for letting each device work at its own speed and operate in an isolated way with a nice clean interface.  So far I’ve implemented the keyboard and debugger processes fairly completely and started to stub-out the rest.

The next step will be to get back to the display.  I have a short-term solution for that which should work well enough to get the software working and then I can go back into “hardware mode” to make the full-size display work.