Delta Pico - what I've learned by building a graphing calculator
2022-10-23This is the Delta Pico! It’s a graphing calculator with a 2.8” colour display, running on a Raspberry Pi Pico, with software written in Rust.
It can evaluate expressions with a history stored in flash memory, and plot multiple graphs on an adjustable pair of axes. (You can also play a simple clone of 2048!)
After working on it since July 2021, I’ve reached a point where I’m very satisfied with this project. It has far from all the features you’d find in a commercially-available graphing calculator, but it’s complete enough for me to be happy!
The Delta Pico is probably the most ambitious side-project I’ve ever undertaken, and also the longest I’ve ever stuck with a personal project.
This post will be a short retrospective of the project - how it evolved over time, and what I’ve learned along the way.
Development and Iteration
I really like calculators! Earlier models in history showcase the introduction of computing into our daily lives, and they remain interesting today to watch how they avoid being completely swallowed up by smartphones. I’ve even given a talk about them for my alma-mater’s computer science society.
This project was a follow-on from my Delta M0 - a compact and much more simple calculator, based on one of STM’s Cortex-M0 microcontrollers. I wanted to build the Delta Pico as a calculator more similar to the TI-Nspire, HP Prime, or the brilliant open-source NumWorks calculator.
It all started on a breadboard! While working on the Delta M0, I built a generic button matrix PCB, which you can see connected to the breadboard.
Shortly after getting the first prototype working, I designed a PCB in KiCad and ordered it from JLCPCB.
There have been three hardware revisions in total so far, though the changes have been quite minor.
Instead, most of the changes have been in software. The Delta Pico’s current software stack and architecture is on its third iteration! It began as a PlatformIO Arduino project, since there were existing Arduino libraries for driving this ILI9341-based display, so that helped me to get up-and-running quickly. It then morphed into a Pico SDK-based project, and finally ended up as a pure embedded Rust codebase.
Rust code was introduced gradually before the project became completely Rust - it started out just being for user-level applications, then I rewrote the drivers for each device in Rust too.
The hardware abstraction layer (HAL) and the user software are two separate projects, which means you can run the Delta Pico’s software on other hardware by just implementing a HAL for it. This allowed me to create a simulator to run on my computer which used the exact same software, which came in handy for debugging several times!
What have I learned?
Mathematical editors are tricky
The core of a calculator is the ability to input the expression you’d like to evaluate.
To match the functionality that I enjoyed on other calculators I own, I wanted the Delta Pico to have “natural” input. That means your expression isn’t “flat” like sqrt(2/3)
, it actually looks like how you’d write it on paper:
This turned out to be a much greater challenge than I expected. While there are libraries for this, they’re largely tied to the web, and aren’t suitable for embedded use.
After a lot of experimentation, I built rbop, a framework for implementing mathematical editors on any platform. It’s completely separate to the Delta Pico’s codebase - it’ll provide the input and evaluation logic, and you just need to implement however it will draw the expressions on your platform.
For example, you can even write a crude editor which works in your terminal:
This means that rbop is completely reusable if I decide to work on another calculator project.
rbop works using two different node trees: unstructured, which is looser and suited to user inputs, and structured, which follows a tight structure ready for evaluation. Unstructured node trees can be upgraded to structured ones using a parsing step. Both node trees can be rendered by recursively transforming them into a sequence of glyphs, which are then handed off to platform-provided code to draw them to the screen.
I might write more about how rbop works in the future - I found it fascinating to implement, and wasn’t able to find other projects aiming for the same goals, so hopefully this could be useful going forward.
Embedded Rust isn’t quite mature
Writing Rust for an embedded platform is a rocky experience, especially if you aren’t too familiar with the language itself when starting out, which was the case for me.
For the uninitiated, a key Rust feature is the borrow checker, which essentially enforces that data can only be owned in one place at a time. You can create references to data, but the borrow checker must know how long they’ll exist, which isn’t always easy to specify.
The borrow checker enables Rust to insert memory allocations and deallocations at compile-time, rather than needing a garbage collector which would be impractical on an embedded system.
Rust’s standard model for embedded development treats the peripherals of a microcontroller as data with an owner. This means that it’s very tricky to share peripherals between multiple drivers in a way that the borrow checker is happy with.
The Delta Pico’s keypad, flash storage, and display drivers all need to be able to access the clock peripheral to use delays. How do you model this?
The difficulty of working around the borrow checker has led to me writing what might be the worst Rust function I will ever write, to completely subvert Rust’s referencing restrictions. I’m sorry, Ferris.
The library ecosystem is also currently far from what you have available when writing C or C++. As a Pico SDK project, the Delta Pico implemented USB mass storage using TinyUSB, so it would show us a storage device when connected to a computer. Unfortunately, I wasn’t able to find a suitable Rust replacement for this when migrating the codebase.
At least, thanks to the community, the breadth of embedded libraries available is constantly getting bigger!
Trying new stacks is fun
I had wanted to properly sink my teeth into Rust for a while, and this project was the perfect opportunity to do so. Rewriting parts of a familiar project in a different language was a useful learning experience.
That said, it also encouraged some unhealthy habits. The Delta Pico codebase is littered with patterns which would be considered poor Rust idioms, so that things just work the way they used to.
Now that I know more, I could likely go back and update everything to be more Rust-like, but that’s not exactly my idea of fun! I have refactored the software for this project enough times already.
The Raspberry Pi Pico is a wonderful platform
Time and time again, I was impressed by the Raspberry Pi Pico microcontroller that I chose as the core of this project.
The form factor is great, it has a tonne of RAM (important for a 240x320 colour frame buffer), and the documentation is leagues ahead of other microcontrollers I’ve used.
Having already risen to being a ubiquitous choice among makers, the Rust bindings for the platform are very complete too.
The Pico will absolutely be my default choice of microcontroller for new projects!
In conclusion
I got into electronics about seven years ago now, starting off with little sensor projects using an Arduino Uno kit, and I’ve been dabbling in the occasional project ever since. I also picked up PCB design during the first COVID-19 lockdown, and built a simple function generator.
I have to admit that I’m really proud of the Delta Pico - this is exactly the kind of project that I wanted to be able to create when I bought my first electronics kit those years ago. The fact I’ve finally done it is an incredibly rewarding feeling.
Going forward, I’m excited to continue extending the Delta Pico, and start working on new projects too!
If you’d like to take a look at the Delta Pico’s hardware or software, check out the GitHub repository, where you’ll find the Rust codebase and KiCad project.