Skip to main content

Onboard Peripheral Usage

This page collects standalone function-level demos for each onboard peripheral of the 0.96'' IPS Display. Each section is self-contained — you can pick the one that matches your use case without reading through the others.

tip

The demo GIFs on this page are sped up to keep them short.

note

All demos in this page require esp32 Boards by Espressif (3.3.11) as described in Getting Started, plus the Seeed_GFX2 library installed manually as described below.

  • Seeed_GFX2 (Manual Installation) — this library is not available in Library Manager and must be installed manually:

Step 1. Click the button above to download Seeed_GFX2 v1.0.0 as a ZIP file (pinned to a release tag so the tutorial stays reproducible). Alternatively, clone the repository from Seeed-Studio/Seeed_GFX2.

Step 2. In the Arduino IDE, go to Sketch > Include Library > Add .ZIP Library... and select the downloaded ZIP. The IDE reads library.properties and installs it into the correct Seeed_GFX2 folder automatically — you do not need to rename the extracted folder. (To install manually instead, unzip the archive and rename the extracted folder to Seeed_GFX2 before placing it in Documents/Arduino/libraries/.)

Step 3. Restart the Arduino IDE so the new library is detected.

tip
  • Seeed_GFX2 is Seeed Studio's graphics library built on a layered Board + Panel Config architecture. Each demo initializes the display with a single display.begin<Board_..., Config_...>() call — the Board template owns the pin map (CS/DC/SCK/MOSI/RST/BL), and the Panel Config bakes in the 80×160 resolution, BGR color order, and rotation. No driver.h or manual panel construction is needed.
  • On this board the demos use Board_XIAO_0inch96_LCD<13, 12> (RST=13, BL=12) with Config_Seeed_0inch96_LCD_ST7789 (80×160, BGR, rotation 2).
  • The IMU is read directly over I2C (Wire) in these demos — no external IMU library is needed. The PDM microphone and I2S output use the ESP-IDF 5 drivers (driver/i2s_pdm.h, driver/i2s_std.h) and LittleFS, all included with the esp32 board package.
  • The 0.96'' IPS Display has no touch controller, no SD card slot, and no Grove connector — it only has a back-side 4-pin I2C test pad — so no touch, SD, or Grove libraries are needed.

Getting the Demo Code

Every demo on this page lives in the Display-Gadgets repository, under the code_GFX2/Function/ directory. Each demo is a folder containing a single .ino sketch. Always download the complete folder rather than copying the .ino source from the GitHub web view.

Option A — Download the repository as a ZIP (recommended):

  1. Open github.com/Seeed-Projects/Display-Gadgets and click Code > Download ZIP, then extract the archive anywhere convenient.
  2. Navigate into code_GFX2/Function/ and open the folder shown in each demo's Code location line. For example, the GraphicTest demo for this board lives in code_GFX2/Function/096_ESP32/xiao_esp32s3_096_graphictest/.
  3. Double-click the .ino file to open it in the Arduino IDE.

Option B — git clone:

git clone https://github.com/Seeed-Projects/Display-Gadgets.git

Then open the demo's .ino file from the cloned code_GFX2/Function/... folder.

Screen Display — GraphicTest

This demo runs a full graphics benchmark on the 0.96-inch ST7789 IPS panel (80×160), covering color bars, lines, rectangles, circles, triangles, rounded rectangles, text, and a pixel gradient. Use it to verify that the screen is wired correctly and that all draw calls work as expected.

Code location: code_GFX2/Function/096_ESP32/xiao_esp32s3_096_graphictest/


How It Works

The sketch initializes the ST7789 IPS panel via Seeed_GFX2, then runs through ten graphics primitives in sequence, measuring the execution time of each one via micros() and printing the result to the serial monitor.

The display is initialized with a single template call:

display.begin<Board_XIAO_0inch96_LCD<13, 12>,
Config_Seeed_0inch96_LCD_ST7789>();

The Board template owns the pin map — CS=D2, DC=D3, SCK=D8, MOSI=D10 — and its <RST, BL> template parameters take bare GPIO numbers, so <13, 12> sets RST=GPIO13 (D17) and BL=GPIO12 (D18). The Panel Config bakes in the 80×160 resolution, BGR color order, and rotation 2 — no driver.h or manual invertDisplay() call is needed.

Running the Demo

Step 1. Open xiao_esp32s3_096_graphictest.ino in Arduino IDE.

Step 2. Select Tools > Board > esp32 > XIAO_ESP32S3_PLUS and the correct Port.

Step 3. Click Upload.

Step 4. Open Tools > Serial Monitor (115200 baud). You should see the panel size followed by timing output for each test:

=== XIAO ESP32-S3 Plus 0.96 graphic test ===
LCD width: 80
LCD height: 160
Color bars: 56.67 ms
Lines: 1819.10 ms
Fast lines: 85.03 ms
Rectangles: 73.34 ms
Filled rects: 177.53 ms
Circles: 262.63 ms
Triangles: 235.64 ms
Round rects: 100.58 ms
Text: 635.61 ms
Pixel gradient: 1960.36 ms
Graphic test finished.

On the screen, you will see each test pattern displayed for about one second before the next one starts. When all tests complete, a "Done! All tests OK" screen appears.

Expected Result

After the sketch runs through all patterns, the screen shows a "Done!" message. Reset the board to run the test again.


IMU

The 0.96'' IPS Display features an onboard LSM6DS3 6-axis IMU (3-axis accelerometer + 3-axis gyroscope) connected via I2C on D4/D5 at address 0x6A. The motion interrupt line on D14 supports hardware wake-up and gesture detection.

note

The onboard IMU is the LSM6DS3 (I2C address 0x6A). The Electronic Quicksand demo probes for a QMI8658-compatible sensor as a defensive fallback. The Raise to Wake demo targets the onboard LSM6DS3 wake-up registers.

The demos below read the IMU directly over I2C (Wire) — no external IMU library is required.

Demo 1: Electronic Quicksand

This demo turns the screen into an interactive fluid simulation — golden sand particles that flow and settle according to gravity, as measured by the onboard 6-axis IMU. Tilt the board and the sand shifts direction in real time.

Code location: code_GFX2/Function/096_ESP32/xiao_esp32s3_096_electronic_quicksand/


How It Works

The simulation uses a 13×26 occupancy grid overlaid on the 80×160 screen, where each cell is 6×6 pixels. Around 65 particles are placed in the grid, each with a position, velocity, and a golden color gradient.

The IMU is read via I2C (D4/D5). The sketch probes for an IMU at both known addresses — QMI8658 first, then LSM6DS3 — and uses whichever one responds. Raw acceleration values are low-pass filtered and used to derive a gravity vector. When you tilt the board:

  1. Gravity vector updates — accelerometer data is smoothed with an exponential moving average to avoid jitter.
  2. Particle velocity — each particle accelerates in the direction of the gravity vector, with damping and a per-particle mobility factor based on its depth in the flow.
  3. Cell occupancy — particles deeper in the flow (closer to the "bottom" relative to gravity) have reduced mobility, creating a realistic packing effect.
  4. Differential rendering — only cells where particles moved into or out of are redrawn, minimizing SPI traffic and keeping the animation smooth on the small panel.

Particles near the surface flow freely (higher mobility); particles buried deeper pack tightly (lower mobility) — mimicking how real sand behaves.

Running the Demo

Step 1. Open xiao_esp32s3_096_electronic_quicksand.ino in Arduino IDE.

Step 2. Select the board and port, then click Upload.

Step 3. Once uploaded, the screen fills with golden particles at the bottom. Tilt the board in different directions — the sand flows as if pulled by gravity.

Step 4. Open Tools > Serial Monitor (115200 baud) to confirm initialization:

=== Electronic Quicksand 0.96 ===
[IMU] LSM6-compatible at 0x6A, WHO=0x6A

Expected Result

The particles flow toward the lower edge as you tilt the board. When the display lies flat, the demo retains the previous gravity direction.


Demo 2: Raise to Wake

This demo implements a screen sleep/wake system driven by the IMU's built-in wake-up interrupt on D14. The screen automatically turns off (backlight off) after 8 seconds of inactivity, and wakes instantly when you pick up or move the device.

Code location: code_GFX2/Function/096_ESP32/xiao_esp32s3_096_wakeup/


How It Works

The demo uses the LSM6-compatible IMU's embedded wake-up event detector — a hardware feature that monitors accelerometer data internally and asserts the INT1 pin (routed to D14 on this board) when motion exceeds a configurable threshold. This means the MCU does not need to poll the accelerometer continuously.

IMU configuration (LSM6-compatible):

RegisterValuePurpose
CTRL3_C0x44Enable BDU + auto-increment for block reads
CTRL1_XL0x40Accelerometer @ 104 Hz, ±2g
CTRL2_G0x40Gyroscope @ 104 Hz
TAP_CFG0x80Enable embedded interrupts
WAKE_UP_THS0x05Wake-up threshold (medium-low sensitivity)
WAKE_UP_DUR0x00No duration filter (responsive wake)
MD1_CFG0x20Route wake-up to INT1

Sleep/wake flow:

  1. Active state — screen is on with the backlight lit. IMU data and battery voltage (D16) refresh periodically, and a countdown shows seconds remaining until auto-sleep.
  2. Auto-sleep — after 8 seconds of no activity, the sketch turns off the backlight and displays a "Sleep — Move to wake" message. By default the demo uses a display-only sleep: the ESP32-S3 keeps running (so the USB CDC serial port stays connected) and simply turns the panel off. The IMU wake interrupt on D14 stays armed, so motion detection remains active. (Set ENABLE_LIGHT_SLEEP to true in the sketch to use real ESP32 light sleep with GPIO wake-up — note that USB CDC can drop while the chip sleeps.)
  3. Wake-up — when the user picks up the board, the IMU detects motion and asserts D14 HIGH. The sketch turns the backlight back on and redraws the UI — the LCD and IMU are not re-initialized.

Manual test buttons:

ButtonPinAction
USR1D6Force sleep
USR2D7Force wake

Running the Demo

Step 1. Open xiao_esp32s3_096_wakeup.ino in Arduino IDE, select the board and port, and click Upload.

Step 2. The screen shows a compact dashboard with power state, battery voltage/percentage, motion data, interrupt count, and a sleep countdown. Let the board sit still — it will automatically enter sleep after 8 seconds.

Step 3. Pick up the board or shake it gently — the screen wakes immediately.

Step 4. Open Tools > Serial Monitor (115200 baud) to observe the boot and wake transitions:

=== XIAO ESP32-S3 Plus 0.96 IMU Wake Demo ===
[IMU] LSM6-compatible at 0x6A, WHO=0x6A
[READY] awake; USR1=sleep, USR2=manual wake, motion=IMU wake
[READY] auto sleep in 8 seconds
[READY] sleep mode: display only (USB CDC stays connected)
[WAKE] IMU_D14 count=1
[WAKE] IMU_D14 count=2
[SLEEP] USR1
[WAKE] IMU_D14 count=3
[WAKE] IMU_D14 count=4
[SLEEP] USR1
[WAKE] IMU_D14 count=5
[SLEEP] AUTO_TIMEOUT

After boot, three [READY] lines describe the controls: USR1 puts the board to sleep, USR2 wakes it manually, and motion triggers an IMU wake. Each transition is logged — [WAKE] IMU_D14 count=N for motion wake-ups (the count increments each time), [SLEEP] USR1 when you press USR1, and [SLEEP] AUTO_TIMEOUT when it auto-sleeps after 8 seconds of no motion.

Expected Result

The screen displays real-time motion and battery data while awake. After 8 seconds of stillness, the screen goes dark. Pick up the device and the screen restores instantly, with the wake counter incremented.


Microphone & Speaker — Flash Recorder

This demo turns the 0.96'' IPS Display into a small voice recorder. Press USR1 to capture a 5-second clip from the onboard PDM microphone into onboard Flash, then press USR2 to play it back through an external I2S amplifier.

The 0.96'' IPS Display's PDM microphone connects to the same pins as the other XIAO display boards:

PinSignalFunction
D0PDM_CLKPDM clock output to microphone
D1PDM_DATAPDM data input from microphone

Code location: code_GFX2/Function/096_ESP32/xiao_esp32s3_096_flash_record/


Hardware Setup

Playback requires an external I2S audio amplifier and speaker. The demo is written for a MAX98357A breakout connected to the board's I2S output pads:

I2S PadXIAO PinMAX98357A
3V33V3VIN
GNDGNDGND
I2S_SDD11DIN
I2S_SCKD12BCLK
I2S_WSD13LRC

The I2S pads (3V3, GND, D11, D12, D13) are exposed on the bottom expansion pad group of the display board.

How It Works

Recording (USR1) — the onboard PDM (Pulse Density Modulation) digital microphone is sampled through the ESP32-S3's I2S peripheral configured in PDM RX mode. On ESP-IDF v5 (Arduino core 3.3.11), this uses the new driver API (driver/i2s_pdm.h). The microphone is captured at 16 kHz mono with 4 DMA descriptors of 256 frames each. When you press USR1, the sketch samples 5 seconds of audio into a RAM buffer, then writes it to onboard Flash as a WAV file (/REC_RAW.WAV) using LittleFS.

After the PDM microphone starts, the sketch discards the first 300 ms of captured data as warm-up data to reduce the startup transient at the beginning of the recording.

If the sketch cannot capture all samples within 7 seconds, it stops recording and displays "Mic timeout" instead of remaining blocked in the recording loop.

note

On this board the microphone's channel-select pin is tied to GND by R8 (a 0 Ω resistor), while R6 — the alternative 3V3 strap — is not populated. The onboard microphone therefore drives the left PDM slot, which is why the sketch sets slot_cfg.slot_mask = I2S_PDM_SLOT_LEFT. Keep this in mind if you adapt the code for a different microphone wiring.

Playback (USR2) — pressing USR2 reads the WAV back from Flash and streams it out through the I2S peripheral in standard (Philips) stereo mode on D11/D12/D13 (driver/i2s_std.h). The mono samples are duplicated to both channels with a 0.75× gain applied to avoid clipping. The amplifier drives a small speaker so you can hear the recording.

note

The ESP-IDF v5 API (i2s_new_channel() / i2s_channel_read() / i2s_channel_write()) is different from the nRF52840 version of this demo, which uses the nRF52 PDM library and the NRF_I2S peripheral directly. If you are porting the nRF52840 code, you must replace the audio setup entirely.

On-screen states:

StateDescription
Ready"Recorder" title with "USR1: record" and "USR2: play" (or "No recording")
Recording"Capturing voice" with "Please speak" while capturing (no live progress)
Error"Mic timeout" with "Try again" when capture exceeds 7 seconds
Saved"Done — Saved WAV" confirmation, then returns to Ready
Playback"Playing..." while streaming, then "Finished"

Running the Demo

Step 1. Connect a MAX98357A amplifier and speaker to the I2S pads as described above.

Step 2. Open xiao_esp32s3_096_flash_record.ino in Arduino IDE.

Step 3. Select the board: Tools > Board > esp32 > XIAO_ESP32S3_PLUS (using esp32 Boards 3.3.11).

Step 4. Select Tools > Partition Scheme > "Default with spiffs (3MB APP/1.5MB SPIFFS)".

Step 5. Select the correct Port, then click Upload.

caution

The recorder stores the WAV file in LittleFS, which uses the SPIFFS partition. The board's default partition scheme (16M Flash (2MB APP/12.5MB FATFS)) contains no SPIFFS partition, so LittleFS.begin() returns false, the WAV file cannot be written, and the screen shows "Write failed / Check flash". You must select the SPIFFS partition scheme above, or recording will not work.

Step 6. Press USR1 (D6) to record 5 seconds of audio from the onboard microphone. The screen shows "Capturing voice" while recording.

Step 7. Press USR2 (D7) to play the recording back through the speaker.

note

The recording is stored in onboard Flash (LittleFS), so it survives a power cycle — you can record once and play it back later. Recording again overwrites the previous file.

Expected Result

Press USR1 and the screen shows "Capturing voice". After 5 seconds it confirms the WAV was saved. Press USR2 and the audio plays through the connected speaker while the screen shows the playback status.


User Buttons

The 0.96'' IPS Display has two physical push buttons connected to the XIAO ESP32-S3 Plus:

ButtonPinLogicSilkscreen Label
USR1D6Active-low (pressed = LOW)USR1
USR2D7Active-low (pressed = LOW)USR2
note

Unlike the 1.14'' IPS Display, the 0.96'' IPS Display has no third button (no USR3 on D19). It also has no dedicated button breakout pads.

Reading Buttons

The buttons use the XIAO's internal pull-up resistors. A simple read looks like this:

const int USR1 = D6;
const int USR2 = D7;

void setup() {
pinMode(USR1, INPUT_PULLUP);
pinMode(USR2, INPUT_PULLUP);
Serial.begin(115200);
}

void loop() {
if (digitalRead(USR1) == LOW) {
Serial.println("USR1 (D6) pressed");
delay(200); // simple debounce
}
if (digitalRead(USR2) == LOW) {
Serial.println("USR2 (D7) pressed");
delay(200);
}
}

Debounce with Interrupts

For responsive, debounced button handling, you can use GPIO interrupts with a short settling delay:

volatile bool btn1Flag = false;
volatile bool btn2Flag = false;

void btn1Isr() { btn1Flag = true; }
void btn2Isr() { btn2Flag = true; }

void setup() {
pinMode(D6, INPUT_PULLUP);
pinMode(D7, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(D6), btn1Isr, FALLING);
attachInterrupt(digitalPinToInterrupt(D7), btn2Isr, FALLING);
}

void loop() {
if (btn1Flag) {
btn1Flag = false;
delay(30); // debounce settling time
if (digitalRead(D6) == LOW) {
// handle USR1 press
}
}
if (btn2Flag) {
btn2Flag = false;
delay(30);
if (digitalRead(D7) == LOW) {
// handle USR2 press
}
}
}

Default Behavior in the Factory Dashboard

In the preloaded factory firmware, the buttons are mapped as follows (you can override these in your own code):

ButtonPinAction
USR1D6Cycle screen brightness (100% → 75% → 50% → 25% → 100%)
USR2D7Toggle screen backlight ON/OFF

When the screen is off (toggled via USR2), pressing USR2 again restores it to the previous non-zero level.


Battery Voltage Detection

This demo reads the onboard battery voltage divider on D16 and shows two live yellow readings on the 0.96'' IPS Display: the raw D16 divider voltage and the calculated battery voltage. It displays voltage readings only; it does not estimate battery percentage or report charging status.

Code location: code_GFX2/Function/096_ESP32/xiao_esp32s3_096_battery_status/


How It Works

Battery circuit:

The ESP32-S3 Plus reads the LiPo battery voltage through an onboard voltage divider connected to D16:

SignalESP32-S3 PinFunction
BAT_ADCD16Analog input reading the divided battery voltage. Internally connected to a 316 kΩ / 160 kΩ voltage divider. Do not use this pin externally.

Voltage divider ratio: 316 kΩ / 160 kΩ → Divider ratio = (316 + 160) / 160 ≈ 2.975

Reading:

The sketch initializes the display with Board_XIAO_0inch96_LCD<13, 12> and Config_Seeed_0inch96_LCD_ST7789 (80×160, BGR, rotation 2), then samples D16 twelve times (700 µs apart) using analogReadMilliVolts() at 12-bit resolution with 11 dB attenuation. It averages the samples into the raw divider voltage, multiplies by the divider ratio to get the battery voltage (Calc = D16 × 2.975), and draws both as two centered yellow lines. The screen refreshes only when either value changes by a meaningful amount (D16 ≥ 0.02 V or Calc ≥ 0.05 V).

note

No charging-status signal is connected to an ESP32-S3 GPIO. This demo displays voltage readings only; it does not detect battery presence or charging status, or estimate battery percentage.

Running the Demo

Step 1. Open xiao_esp32s3_096_battery_status.ino in Arduino IDE.

Step 2. Select Tools > Board > esp32 > XIAO_ESP32S3_PLUS and the correct Port.

Step 3. Click Upload.

Step 4. Observe the screen — it shows two yellow lines: the raw D16 divider voltage and the calculated battery voltage. Connect or disconnect a LiPo battery (or the USB-C cable) to watch the values update.

Expected Result


Voltage reading (D16 + Calc)

Battery connector (back)

The screen shows the raw D16 divider voltage on the top line and the calculated battery voltage (Calc) on the bottom line. With a LiPo battery connected, Calc approximates the battery terminal voltage. A reading can also appear under USB power alone, so Calc cannot by itself confirm whether a battery is attached.

The demo also prints a diagnostic line to the Serial Monitor every second, for example:

D16 1.39V | Calc 4.14V

Resources

Tech Support & Product Discussion

Thank you for choosing our products! We are here to provide you with different support to ensure that your experience with our products is as smooth as possible. We offer several communication channels to cater to different preferences and needs.

Loading Comments...