Writing

The Quest for Smooth Animations: Optimising the ESP32-C3 for Performance

3 min read

I’ve always loved working with hardware, pushing the limits of what small microcontrollers can do.

I’ve always loved working with hardware, pushing the limits of what small microcontrollers can do. Recently, I found myself knee-deep in a project involving the ESP32-C3, Squareline Studio, and LVGL—a combination that promised a sleek UI with smooth animations for my embedded device. But, as with all things in development, the road to perfection was anything but smooth.

I’ll walk you through my journey of tackling one of the trickiest challenges: getting animations to run fast and smoothly without overloading the microcontroller.

The Challenge: Choppy Animations on Limited Hardware

At first, everything seemed straightforward. I set up my custom UI with Squareline Studio, integrated it with LVGL, and deployed it onto my ESP32-C3. The animations worked, but something was off—they were choppy, sluggish, and inconsistent. Given the limited resources of the ESP32-C3, I knew this would require some deep digging to optimize things without sacrificing the user experience.

I began to break down the problem:

  • Frame updates felt sluggish.

  • Animations were inconsistent, lagging behind user interactions.

  • The overall responsiveness of the system was slow.

It was clear: I needed to make every millisecond count.

The Journey: Fine-Tuning the Timer and Buffer

My first instinct was to check the loop. I had added a delay() in the loop to handle some timing, thinking it would help keep things organized. Big mistake. This turned out to be the culprit—blocking the entire microcontroller, which in turn blocked LVGL’s animation updates. The result? Jittery animations and a UI that felt far from smooth.

I replaced the blocking delay() with a non-blocking approach using millis(). This simple change allowed the loop to continue running while still performing tasks at intervals. It was a quick win, but the animations still weren’t as fluid as I wanted.

That’s when I turned to the LVGL timer settings. It turns out LVGL relies on timers to update the display and handle animations. By default, the refresh rate was too slow, causing the UI to struggle with keeping up. I reduced the refresh period to 16 milliseconds, targeting a smooth 60 FPS, and almost immediately, the animations became much more fluid. The UI now felt responsive, and the stutter was almost entirely gone.

But there was still more to do.

The Breakthrough: Buffer Size and SPI Tweaks

Next, I turned my attention to the frame buffer. The ESP32-C3 doesn’t have a ton of RAM to work with, so I had to carefully tune the buffer size. A larger buffer meant faster rendering, but too large a buffer would eat up precious memory. I found that setting the buffer size to cover around 40 rows of my display was the sweet spot—enough to keep things smooth but still leave room for other tasks.

I also realized that the SPI communication between the ESP32-C3 and my display needed some love. By increasing the SPI clock frequency to 40 MHz, I was able to speed up the transfer of data to the display, further reducing any lag between animations.

The Result: Smooth Sailing (and Animations)

After tuning the timer, optimizing the buffer, and tweaking the SPI communication, the ESP32-C3 began to shine. Animations were now fluid and responsive, matching the user’s input with no noticeable lag. The entire UI felt snappy and polished, and it was all running on a microcontroller with limited resources.

The key takeaway from this journey? When working with resource-constrained devices, every little detail matters. A simple delay() can throw everything off, while careful tuning of timers and buffer sizes can breathe life into your UI.

What I Learned: A Balanced Approach

  • Avoid blocking operations like delay() in your main loop. Instead, use non-blocking timing techniques like millis() FreeRTOS delays.

  • Tune the LVGL refresh timer to a period that suits your target FPS—16 ms for 60 FPS or 30 ms for 30 FPS.

  • Optimize the frame buffer size for your display and available memory. It’s a balance between performance and memory constraints.

  • Maximize your SPI communication speed to reduce latency when sending data to the display.

Working with embedded systems often feels like juggling limited resources, but with the right optimizations, you can achieve performance that feels far beyond the hardware’s limitations. If you’re working on a similar project, I hope this story helps you find your balance, just like I did.

First published on Substack.