Tutorials

CIRCUITPYTHON FOR C HEADS: PARDON THE INTERRUPTS

by David "ishotjr" Groom
When Dennis Ritchie created C in the 1970s, he was targeting UNIX systems the size of several large appliances – it’s hard to believe he could have ever imagined that the language he was cobbling together could end up as a critical underpinning for the countless invisible microcontrollers that surround us every day. And he certainly wasn’t thinking about approachability for the would-be microcontroller audience. Fast-forward fifty or so years, however, and Adafruit have attempted to simplify learning to code on microcontrollers by developing the CircuitPython language. CircuitPython is a fork of MicroPython, itself a (mostly) Python 3 compatible compiler and runtime for microcontrollers, and aims to make things easy by simplifying a few key aspects of programming:


  • It presents the board as a regular mass storage device, so that development is as simple as using a thumb drive and a text editor. No intermediary programmer board or interface other than USB is needed.
  • It handles variable types for you, so there's no need to declare a uint8_t or an int or a float or a double. Removing the need to consider the overall range of the number you're storing before you start handling it is a bit easier for beginners, hopefully leading to fewer overflow problems.
  • The syntax is different - no more curly braces or semicolons. (We feel this is a debatable "pro" - some beginners do better with the structure that semi-colons and curly braces provide.)
  • It handles strings & text parsing a heck of a lot better, without pointers. Pointers are a pretty abstract concept that takes a while to wrap your head around, and an even longer while to become second-nature. A downfall of C is how early they're needed to handle pretty mundane tasks that any beginning programmer wants to do - like display words on LCDs, or save and recall data.


Accessibility and approachability are important to us at Alpenglow, so for our next big project (currently codenamed Minimum Viable Cat…!) we took a look at approaching it from a beginner-friendly CircuitPython perspective. But we’re mostly oldhead C folks, so some concepts have taken us a hot minute to grasp or adjust to. And one such momentary mindfuck was something we’ve long taken for granted in C-world: Interrupts.


As you do, we started by searching for “circuitpython interrupts”, expecting helpful Stack Overflow pages or maybe some random yet magical forum threads. Instead we found a bunch of lengthy discussions in GitHub issues with titles like “no interrupts?” and “Asynchronous events: what are your use cases?” – not explaining how to use interrupts, but rather: confirming their absence. The feeling of confusion about the lack of this MCU mainstay was echoed in other discussions, such as “Anyone have any guidance on working without interrupts?” on Reddit. Numerous references to interrupts in MicroPython were forthcoming, and CircuitPython is just a fork of that, right, so … ? But the CP docs confirmed:


Concurrency within Python is not well supported. Interrupts and threading are disabled. async/await keywords are available on some boards for cooperative multitasking.


under “Differences from MicroPython.” Reading all the way to the bottom of issue 4542, however, we found a link to Cooperative Multitasking in CircuitPython with asyncio! asyncio brings cooperative multitasking with the async and await keywords paradigm found in other languages such as JavaScript and C#. While we don’t claim to be Python experts by any stretch of the imagination, the concept of event-style tasks was familiar from front end frameworks and other paradigms. Here’s how we used async/await to effectively run a servo and vibrator motor concurrently.

note that the adafruit_motor, asyncio, and adafruit_ticks libraries need to be added to the lib folder in order to run the above cody.py


So, what’s going on here? First in twitch() we have an infinite loop of random servo movements that result in our servo moving around randomly, forever. Then in purr() we have another infinite loop that vibrates our motor at various intensities then rests, again, forever. So how does that even work – wouldn’t one infinite loop preempt the other from running? That’s where asyncio comes in! By replacing traditional time.sleep() calls with asyncio.sleep() calls, other tasks (defined with asyncio.create_task() and orchestrated via asyncio.gather()) are welcome to jump in and do their thing!


And it turns out, it is possible to use interrupts directly in some situations. CP’s countio module uses actual interrupts to count rising/falling-edge pin transitions, the result of which you could use to e.g. see if a button has been pressed. Its use is fairly limited, but it’s at least good to know our friend the interrupt hasn’t been completely forgotten. One other caveat: asyncio is only supported in CP 7.1.0 and above, and only on certain boards. Our frequent go-to SAMD21, for example, is not supported. 😔


We’re having a blast exploring CircuitPython, but continue to be surprised by how different some aspects of it are from other microcontroller development we’ve experienced in the past. What surprises have you encountered in CircuitPython? Our hope is to share our findings as we explore from a C-based perspective, and we’d love to hear what you’ve learned or what you’re struggling with so we can help guide our blog post series! :)