My RP2040 Pomodoro Timer
I've finally completed a project I've wanted to make with microcontrollers for a long time!
You might know the Pomodoro Technique;
in short, you use a timer to improve your productivity.
A 25-minute timer is dedicated to getting some work done, and a 5-minute timer is used to take a break. The idea is that taking short breaks often improves focus and productivity.
Why I Decided to Make My Very Own Timer
From time to time, I've used this technique, but I've always struggled to find a timer that doesn't get in the way of what I'm doing.
I've experimented with different timers, but none of them worked for me:
- I've used this web app, but I always end up losing the tab. Using it in fullscreen mode on a second screen distracts me and makes me anxious.
- I've used Clockwork Tomato Android application, which has many functions (maybe too many), but I'm not a big fan of keeping my phone unlocked on my desk.
- You can use whatever timer you have available, but I don't like the noise they make. I would prefer visual feedback.
My Requirements
So, I started imagining what the perfect Pomodoro timer would be for me, and I've come up with the following requirements:
- No sound alarm.
- Visual, minimalist, and not distracting indication of the timer status.
- Simple interface to start the timer that moves automatically from work to break timer.
- Standalone device that can be kept on my desk.
A simple board with a microcontroller, a few LEDs, and one or more buttons to control the device would be the perfect candidate for the job.
The Hardware
I accidentally stumbled across the YD-RP2040 board.
This board has all I need:
- An RGB LED that can be used to create many animations to indicate the timer status.
- A button that can be used by the user.
- A little blue LED.
My Experience Developing the Firmware
In the RP2040-pomodoro GitHub repository, you will find all the details on how to operate the device and on how to flash your own.
Here, I just want to discuss the things I've learned writing this firmware.
How to Achieve Multitasking?
The first challenge I immediately faced was to figure out how to achieve some resemblance of multitasking.
At the same time, a few things need to happen in parallel:
- You need to monitor the button to detect user interaction.
- You need to display the desired animation by continuously updating the LED state.
- You need to keep track of time to generate events, for instance, the timer reaching the end of the cycle.
A trivial sequential program can usually do only one thing at a time. If, for instance, you wait for a button to be pressed using a loop, you will not be able to do anything else. Unless...
The Cooperation Model
You can make it look like many things are happening at the same time if you do small pieces of each thing for a short while and then move to the next.
As long as you do it right and every task runs for a very short amount of time, it will look like many things are happening at the same time, even if, in reality, they happen sequentially.
This is how I've implemented my first version. I've structured my code so that a main loop would take care of running sequentially all the currently registered tasks.
Of course, each task you run needs to cooperate and run for a very short time; otherwise, the resemblance of multitasking will be lost.
Some care needs to be given in order to make sure that this delicate dance happens smoothly. To help with this, I've used a state machine to help with managing the transition from one state to another, removing and adding tasks to the runner.
Using Asyncio
I've developed this firmware in MicroPython, which offers a subset of the Python Asyncio functionality.
Asyncio is a framework that helps in writing asynchronous code (many other languages have similar frameworks). It helps in achieving multitasking by providing the basic infrastructure to run purposely made pieces of code, called tasks.
I've intentionally avoided using it in my first version just because I wanted to see how difficult it would have been to write my rudimentary event loop. But as soon as I got it working, I decided to refactor everything to use the Asyncio framework instead.
That's how the version 2 has come to life. It is functionally identical to the first version, but it leverages the Asyncio framework.
This coroutine is the core of the program.
- It creates a task that always runs in the background, checking for user input and generating events when an interaction is detected.
- It waits for events to come.
- It passes the events to the current running task and takes care of changing tasks if necessary.
Conclusion
The Asyncio implementation is a little more elegant, but practically, you would not be able to tell the difference as a user.
It's worth to mention that the MicroPython implementation of Asyncio is not complete so you might have to improvise and create workarounds to achieve your desired behaviour.
Time accuracy in both the versions I've developed is not perfect.
A different approach should have been taken if the exact timing had been crucial for a given application, probably involving the use of timers and interrupts.
For my application, accuracy wasn't a big concern, and using ticks and time allowed me to achieve all the precision I wished for. So, I didn't bother to go down the timer and interrupt rabbit hole, for which I would recommend reading the documentation.