When you first hold the Waveshare ESP32-S3-Touch-LCD-1.47 in your hand, the expectation is often: “I just copy a few PNGs onto it and then the display will show them.” Unfortunately, embedded systems don’t quite work that way. The board can render really nice interfaces – Waveshare even explicitly states that it runs LVGL “smoothly” – but you need to prepare your graphics so that a microcontroller can efficiently read them from memory or filesystem. And that’s exactly where it becomes either frustrating or where you can have menus, icons, and touch buttons running on the display within an hour.
The good news first, since you asked: You don’t need an SD card to display "graphics." An SD/TF card is only an option if you want to swap images easily later or have many/large assets. Although the board has a TF card slot, you can natively show graphics completely without a card – either as drawn primitives (text, lines, filled areas) or as embedded image data in flash.
What you’re actually dealing with here (and why it matters)
The 1.47" touch display is based on the ESP32-S3R8, has PSRAM and a good amount of flash, a 172×320 IPS touch display, and communicates with display/touch via SPI and I2C. That might sound like datasheet jargon but is practical: PSRAM is what makes LVGL really comfortable to use, and 172×320 is small enough that with efficient buffers and compact image formats you can build very smooth UIs.
Why you should start with the Waveshare demos in the Arduino IDE
You could theoretically wire up everything "manually": display initialization, backlight, touch controller, LVGL display driver, flush callbacks, tick timers, touch read functions… that works, but costs time and often leads to the classic black screen trap.
Waveshare provides a wiki page for exactly your board including an Arduino guide and ready-made examples – including GFX HelloWorld, LVGL v8, LVGL Image, and SD/TF tests. This is invaluable because it contains not only example code but also the correct combination of board settings, driver libraries, and LVGL versions.
One detail many underestimate: Waveshare specifically requires something like "esp32 by Espressif Systems >= 3.0.0" and Arduino info mentions LVGL v8.4.0. Mixing versions wildly often causes issues appearing like magic ("compiles but display stays black" or "lvgl.h missing").
"Graphics" means three different things on the ESP32
Before you commit, it helps to be clear about terms:
Drawing graphics (primitives): Text, lines, rectangles, circles, fills – this is “native” in the sense that you calculate pixels and send them to the display. You don’t need image files for that.
Displaying images (assets): Icons, logos, backgrounds. They have to be stored somewhere: in flash as a C array or as a file (LittleFS/SD).
Building the UI (widgets): Buttons, lists, sliders, animations, touch events. That’s LVGL territory.
On your board, all three methods make sense – depending on how UI-heavy your project is.
Method 1: Without LVGL – immediate visible results with the GFX library
If you just want to quickly put something on the display, use the approach from the Waveshare demo "01_gfx_helloworld": The example uses the Arduino GFX library to control the display and output text. You can also immediately draw your own things: status displays, measurement values, bars, simple menus. This is the fastest way to get "it’s alive."
This is not a “modern UI framework.” As soon as you want buttons, layouts, screens, themes, or clean touch interaction, you will practically automatically end up with LVGL.
Method 2: LVGL in the Arduino IDE – images without SD (in flash)
This is the practical sweet spot: You build your UI with LVGL and embed your icons/logos directly into the firmware. Then your project has no filesystem dependencies, starts fast, and is robust.
The typical workflow is:
You take a PNG/JPG/BMP on your PC and convert it to an LVGL-compatible format with the LVGL Image Converter. The tool can produce either C files (arrays) or binary data that LVGL can understand as an image source.
Then you add the generated file to your Arduino project, and in LVGL, displaying it looks approximately like this:
LV_IMG_DECLARE(my_logo); // from the converted .c file
lv_obj_t *img = lv_img_create(lv_scr_act());
lv_img_set_src(img, &my_logo);
lv_center(img);
Less important is the code than the conversion choice: For small displays, you often pick a format that fits well with RGB565 displays (less RAM/flash than “full” 32-bit variants). This is exactly the kind of decision that later determines whether your UI runs silky smooth or stutters.
The downside is clear: If you want to change the logo, you need to re-flash. For a finished device, that’s usually perfectly fine.
Method 3: Images as files – without SD but with flash filesystem (LittleFS)
If you want to swap images later but don’t want to use an SD card, you can use a filesystem in internal flash. In the Arduino world, LittleFS is the common way for this (SPIFFS has been treated as “deprecated” for years; LittleFS is the recommended path). ([Arduino documentation][3])
The principle is simple: You put files (e.g., logo.bin or depending on setup also logo.png) in a "data" folder, upload them into the flash partition, and let your software load assets from there. This involves more setup than "everything in code," but you get a kind of middle ground: firmware stays the same, assets can change.
Whether you want to load PNG/JPG directly from LittleFS depends again on decoders – many also prefer LVGL binary assets here because they demand less from the microcontroller.
Method 4: SD/TF – optional, practical, but more moving parts
The board has a TF card slot, and Waveshare even provides dedicated SD/TF demos for it. SD is worth it if you have many assets (e.g., multiple screens with large backgrounds) or want to swap images/files “like a USB stick.”
The price is complexity: FAT32, card detection, file paths, decoders, timing – all doable but with more points where errors can occur.
Arduino IDE: the most common stumbling block is not the code but library installation
A classic when reproducing the Waveshare examples: You open a demo, hit compile, and get a crash because lvgl.h is "not found." In an Arduino forum discussion about exactly these 1.47" boards, a pragmatic solution is described: take the libraries from the Waveshare demo ZIP and install them as ZIP libraries in the Arduino IDE (instead of grabbing some LVGL version from the library manager). It’s not "pretty," but often the fastest way to get a working build.
Also easily overlooked in board settings: USB CDC “on boot,” appropriate flash/partition/PSRAM settings – these are exactly the things that come up repeatedly in community instructions around these boards because otherwise upload/serial or memory get tight.
Once you have one working demo, the rest suddenly becomes relaxed: You start building screens, placing buttons, wiring touch events, and integrating your graphics step by step.
A small mental model that saves you time
When you say "graphics natively," you usually mean one of two things:
- "I want to draw an interface" → then GFX primitives or LVGL without images (labels, buttons, styles) often suffice.
- "I want to display real image files" → then you have to decide where they live (embedded flash / LittleFS / SD) and in which format you prefer to handle them (converted vs. PNG/JPG with decoder).
On the Waveshare ESP32-S3 1.47" Touch, the pragmatic entry path is almost always: Get the Waveshare LVGL demo running → first own screen layout → first icon as LVGL asset in flash. You keep the SD card for when you really have a reason to use it.