Seeed Studio Round Display for XIAO Christmas Ball

by feiticeir0 in Circuits > Electronics

379 Views, 2 Favorites, 0 Comments

Seeed Studio Round Display for XIAO Christmas Ball

Christmas_ball.gif

Bring the festive spirit to life with a Christmas-themed interactive snow globe! This project uses the Seeed Studio Round Display and XIAO ESP32S3 to create a snow animation, complete with dynamic falling snow, wind effects, and touch interaction.

Supplies

christmas_ball2.jpg

Software Preparation

First Use


To use the Round Display, head to Getting Started with Round Display for XIAO to install the necessary libraries.

Try some of the examples to see if everything is working well.

Libraries


For this project, we're going to use the libraries that come bundled with the Seeed Studio Round Display For XIAO

Install all the libraries like specified in the tutorial Getting Started with Round Display for XIAO. After that, you need the following:

  1. PNGdec library .
  2. Update LVGL library (or not install the one from Seeed Studio github)

Images

background1.png
background3.png
background2.png
screenshot1.jpg
screenshot2.jpg
screenshot3.jpg
screenshot4.jpg
screenshot5.jpg
screenshot6.jpg
screenshot7.jpg
screenshot8.jpg
screenshot9.jpg
screenshot10.jpg
screenshot11.jpg
screenshot12.jpg

Our images are PNG images stored in Flash Arrays. They are displayed using PNGdec library.

All images must be PNG

All the images used are AI generated .

Our background images need to be prepared for that TFT_eSPI can display them and they fit well on the Round Display for XIAO.

Prepare images


Resize Images

Our Round Display for XIAO has a 240x240 resolution. We need to resize the images. I'm going to show how to do it using GIMP

  1. Open the image
  2. Go to Image > Scale Image
  3. Set Width and Height to 240. Because the Keep Ratio is selected (the chain), once you change the width, the height should also change.
  4. Press the Scale button.
  5. Save the image (I'm going to override the old one)


Create the Flash Arrays


NOTE: This instructions are inside the TFT_eSPI Flash_PNG example.

To create the flash array, go to File to C style array converter

The steps are:

  1. Upload the image using Browse .
  2. After uploading the image, we need to set some options
  3. Treat as binary
  4. All the other options gray out.
  5. Let's change the Data type to char
  6. Press convert. This will convert the image to an array.
  7. You can now press the button Save as file to save your image and add it to your Arduino Sketch or press the button Copy to clipboard If you Copy to clipboard, you'll have to press the 3 dots on the right side of the Arduino editor and choose New Tab
  8. Give it a name (generally your image name with .h extension)
  9. You'll end up with all your images as .h files.

Code

Here's the code for the Christmas ball. A little explanation of the principal functions of the code. The code also includes some comments.

Headers and libraries

We start by including some libraries.

#include <PNGdec.h>
#include <TFT_eSPI.h>
#include <Wire.h>

#include "background1.h"
#include "background2.h"
#include "background3.h"

#define USE_TFT_ESPI_LIBRARY
#include "lv_xiao_round_screen.h"

Remember that you need to have the Seeed Studio libraries installed.

Background images

Here are the functions to manage the background images

struct Background {
const uint8_t *data;
size_t size;
};

const Background backgrounds[] = {
{(const uint8_t *)background1, sizeof(background1)},
{(const uint8_t *)background2, sizeof(background2)},
{(const uint8_t *)background3, sizeof(background3)},
};
  1. Struct: Each background image is stored as a Background struct containing:
  2. data: Pointer to the PNG data.
  3. size: Size of the PNG file.
  4. Array: The backgrounds array stores all the background images. The currentBackground variable tracks the currently displayed background.

Snow particles Simulation

  1. Initialization of particles
void initParticles() {
for (int i = 0; i < numParticles; i++) {
particles[i].x = random(0, sprite.width());
particles[i].y = random(0, sprite.height());
particles[i].speed = random(3, 8);
}
}
  1. It initializes numParticles with random positions and speeds.
void updateParticles() {
for (int i = 0; i < numParticles; i++) {
particles[i].speed += random(-1, 2); // Speed variation
particles[i].speed = constrain(particles[i].speed, 3, 8);
particles[i].y += particles[i].speed; // Move down
particles[i].x += random(-1, 2); // Wind effect
// Wrap-around logic
if (particles[i].y > sprite.height()) {
particles[i].y = 0;
particles[i].x = random(0, sprite.width());
particles[i].speed = random(3, 8);
}
if (particles[i].x < 0) particles[i].x = sprite.width();
if (particles[i].x > sprite.width()) particles[i].x = 0;
}
}
  1. Updates particle positions with:
  2. Falling Effect: Each particle moves down.
  3. Wind Effect: Adds a slight horizontal drift.
  4. Wrap Around: Particles reset to the top when they exit the bottom.
  5. Rendering particles:
void renderParticlesToSprite() {
for (int i = 0; i < numParticles; i++) {
sprite.fillCircle(particles[i].x, particles[i].y, 2, TFT_WHITE);
}
}
  1. It renders each particle as a small white circle

PNG Decoding

int16_t rc = png.openFLASH((uint8_t *)backgrounds[currentBackground].data,
backgrounds[currentBackground].size,
pngDrawToSprite);
if (rc != PNG_SUCCESS) {
Serial.println("Failed to open PNG file!");
return;
}
png.decode(NULL, 0);
  1. Loads and decodes the current background PNG using the png.openFLASH() function

Touch interaction

if (chsc6x_is_pressed()) {
currentBackground = (currentBackground + 1) % numBackgrounds; // Cycle backgrounds
delay(300); // Debounce
}
  1. Detects a touch event using the chsc6x_is_pressed() and changes the background image by incrementing currentBackground

Setup and loop

  1. Setup
void setup() {
Serial.begin(115200);
tft.begin();
tft.fillScreen(TFT_BLACK);
sprite.createSprite(240, 240); // Match display size
pinMode(TOUCH_INT, INPUT_PULLUP);
Wire.begin();
initParticles();
}
  1. Initializes the display, touch input and snow particles


  1. Main loop
void loop() {
sprite.fillScreen(TFT_BLACK);
// Render background and snow
int16_t rc = png.openFLASH((uint8_t *)backgrounds[currentBackground].data,
backgrounds[currentBackground].size,
pngDrawToSprite);
if (rc == PNG_SUCCESS) {
png.decode(NULL, 0);
updateParticles();
renderParticlesToSprite();
sprite.pushSprite(0, 0);
}
// Handle touch input
if (chsc6x_is_pressed()) {
currentBackground = (currentBackground + 1) % numBackgrounds;
delay(300);
}
delay(10); // ~100 FPS
}
  1. Clears the sprite, renders the current frame (background + particles), and checks for user input.

The .ino file is attached here, as well the 3 background images that I've used as .h files.

Double Buffering

To reduce the flickering and improving animation smoothness of the snow flakes, we use double buffering.

This allows us to draw in an off-screen buffer before displaying it on the screen.

Double buffering here

In this project, the TFT_eSPI library's TFT_eSprite class implements double buffering.

  1. Sprite creation
  2. The sprite (off-screen buffer) is created in the setup() function:
sprite.createSprite(240, 240); // Match display size
  1. Drawing the buffer
  2. All drawing operations (background rendering and snow particle animation) are done on the sprite:
sprite.fillScreen(TFT_BLACK); // Clear the sprite
renderParticlesToSprite(); // Draw snow particles
  1. Updating the display
  2. After the frame is fully drawn in the sprite, it is pushed to the display in one operation:
sprite.pushSprite(0, 0);
  1. This transfers the buffer's contents to the screen instantly.
  2. Reuse
  3. The sprite is reused for every frame by clearing it at the start of the loop():
sprite.fillScreen(TFT_BLACK);

Advantages of Using Double Buffering

  1. Smooth Snow Animation: The falling snow particles are updated seamlessly without flickering.
  2. Dynamic Background Switching: The touch-triggered background changes happen without visible delays or artifacts.
  3. Efficient Rendering: Drawing in memory (RAM) is faster than directly updating the display line by line

Future and Conclusion

I'm hoping that someone creates a 3D ball to place the Seeed Studio Round display for XIAO in it and then place it on the Christmas Tree.

I'm also hoping to change the code that the images are loaded from the SD Card instead of using FLASH arrays.


Hope you liked my project. Just add a bit of magic to your Christmas