{{ t.products }}

{{ live_data.products.length }}

{{ t.categories }}

{{ live_data.categories.length }}

{{ t.brands }}

{{ live_data.brands.length }}
Menu
If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!
If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!If you cannot find a specific electronic or automation spare part on our website or in the market, please contact us — quantity does not matter!

How to Build a Mini Reaction Game with XIAO ESP32-C6 and SPI TFT LCD

•
Author: Senanur Yılmaz
•
Publish Time: 14.11.2026
•
Reading Time: 12 Minute
How to Build a Mini Reaction Game with XIAO ESP32-C6 and SPI TFT LCD

In this project, we will build a mini reaction time game using a XIAO ESP32-C6 development board, a 1.8-inch SPI TFT display and a push button. When the display turns green and shows the word “PRESS!”, the player must press the button as quickly as possible. The reaction time will then be displayed in milliseconds.

This project is a fun introduction to SPI communication, TFT display control, push-button input, random timing and measuring elapsed time with the millis() function.

How Does the Reaction Game Work?

  1. The display shows a “Press the button to start” message.
  2. The player presses the button to start a new game.
  3. The display shows a “Get ready...” message.
  4. The system waits for a random period between 2 and 5 seconds.
  5. The display turns green and shows “PRESS!”.
  6. The time taken for the player to press the button is measured.
  7. After five rounds, the best and average reaction times are displayed.

If the player presses the button before the display turns green, a “Too early!” warning appears. The current round is restarted and the early press is not added to the score.

REACTION TIME

247 ms

VERY GOOD!

Required Components

Important: The USB cable used to upload the program must support data transfer. A power-only charging cable may power the board, but the computer will not detect it as a programmable device.

About the TFT Display

The 1.8-inch TFT display used in this project has a resolution of 128×160 pixels and uses the ST7735R display controller. Display data is sent from the XIAO ESP32-C6 to the screen through SPI communication.

This display is not touch-sensitive. A separate physical push button is therefore used as the player's input.

Important TFT Display Pins

  • VCC: Power input for the display.
  • GND: Common ground connection.
  • SCK or CLK: SPI clock signal.
  • SDA or MOSI: Carries display data from the XIAO to the TFT.
  • CS: Selects the TFT on the SPI bus.
  • DC or A0: Distinguishes commands from display data.
  • RST or RESET: Resets the display controller.
  • LED or BL: Powers the display backlight.
The SDA label on some SPI TFT modules can be confusing. In this project, it is not an I2C SDA connection. It is the SPI data input, also known as MOSI.

Connecting the TFT Display to the XIAO ESP32-C6

Disconnect the XIAO ESP32-C6 from the USB cable before making any connections. Recheck every wire before powering the circuit.

TFT display pinXIAO ESP32-C6 pinPurpose
VCC3V3Display power
GNDGNDCommon ground
SCK / CLKD8SPI clock signal
SDA / MOSID10Sends data from the XIAO to the display
CSD3Selects the TFT display
DC / A0D2Distinguishes commands from display data
RST / RESETD1Resets the display
LED / BL3V3Powers the backlight

The TFT display only receives image data in this project, so an MISO connection is not required. The XIAO ESP32-C6 hardware SPI pins are D8 for SCK, D9 for MISO and D10 for MOSI.

Push-Button Connection

Button connectionXIAO ESP32-C6 pin
One side of the buttonD0
Opposite side of the buttonGND

Pin D0 will be configured as INPUT_PULLUP. This activates the internal pull-up resistor of the XIAO ESP32-C6, so an external pull-up resistor is not required. The pin reads HIGH when the button is released and LOW when it is pressed.

If you are using a four-pin tactile button: The two pins on each side are already internally connected. Connect D0 and GND to pins on opposite sides of the button. If both wires are connected to pins on the same side, the button may appear to be permanently pressed.

Installing Arduino IDE

1. Install Arduino IDE

Download and install the current stable version of Arduino IDE. Open Arduino IDE after the installation is complete.

2. Add the ESP32 Board Manager URL

Open File > Preferences in Arduino IDE. Add the following address to the “Additional Boards Manager URLs” field:

https://espressif.github.io/arduino-esp32/package_esp32_index.json

3. Install the ESP32 Board Package

  1. Open Tools > Board > Boards Manager.
  2. Enter esp32 in the search field.
  3. Install the ESP32 package provided by Espressif Systems.
  4. Select XIAO_ESP32C6 as the board.
  5. Select the serial port used by the connected XIAO board.

4. Install the Display Libraries

Open Tools > Manage Libraries and install the following libraries:

  • Adafruit GFX Library
  • Adafruit ST7735 and ST7789 Library

If Arduino IDE asks to install additional dependencies, select Install All.

Mini Reaction Game Arduino Code

Copy the following code into Arduino IDE. Check the selected board and port, and then click the Upload button.

#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <esp_system.h>
#include <limits.h>

// TFT display connections
#define TFT_CS   D3
#define TFT_DC   D2
#define TFT_RST  D1

// Reaction button
#define BUTTON_PIN D0

// Game settings
const uint8_t TOTAL_ROUNDS = 5;
const unsigned long MIN_WAIT_TIME = 2000;
const unsigned long MAX_WAIT_TIME = 5000;

// Create the TFT display object
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

// Possible game states
enum GameState {
  START_SCREEN,
  WAITING,
  GO_SIGNAL,
  ROUND_RESULT,
  EARLY_PRESS,
  GAME_FINISHED
};

GameState gameState = START_SCREEN;

unsigned long stateStartedAt = 0;
unsigned long randomWaitTime = 0;
unsigned long goStartedAt = 0;
unsigned long reactionTime = 0;
unsigned long totalReactionTime = 0;
unsigned long bestReactionTime = ULONG_MAX;

uint8_t currentRound = 1;

// Draw text in the horizontal center of the display
void drawCenteredText(
  const String &text,
  int16_t y,
  uint8_t textSize,
  uint16_t color
) {
  int16_t x1;
  int16_t y1;
  uint16_t textWidth;
  uint16_t textHeight;

  tft.setTextSize(textSize);
  tft.setTextColor(color);
  tft.setTextWrap(false);

  tft.getTextBounds(
    text.c_str(),
    0,
    y,
    &x1,
    &y1,
    &textWidth,
    &textHeight
  );

  int16_t x = (tft.width() - textWidth) / 2;
  tft.setCursor(x, y);
  tft.print(text);
}

// Detect a button press and filter switch bounce
bool buttonPressed() {
  static int lastReading = HIGH;
  static int stableState = HIGH;
  static unsigned long lastChangeTime = 0;

  int currentReading = digitalRead(BUTTON_PIN);

  if (currentReading != lastReading) {
    lastReading = currentReading;
    lastChangeTime = millis();
  }

  if (
    millis() - lastChangeTime > 25 &&
    currentReading != stableState
  ) {
    stableState = currentReading;

    if (stableState == LOW) {
      return true;
    }
  }

  return false;
}

void showStartScreen() {
  tft.fillScreen(ST77XX_BLUE);

  drawCenteredText("MINI REACTION", 22, 2, ST77XX_WHITE);
  drawCenteredText("GAME", 45, 2, ST77XX_WHITE);

  drawCenteredText(
    "Press the button",
    81,
    1,
    ST77XX_YELLOW
  );

  drawCenteredText(
    "to start",
    96,
    1,
    ST77XX_YELLOW
  );
}

void showWaitingScreen() {
  tft.fillScreen(ST77XX_YELLOW);

  drawCenteredText("GET READY...", 42, 2, ST77XX_BLACK);

  drawCenteredText(
    "Round " + String(currentRound) + "/" + String(TOTAL_ROUNDS),
    78,
    1,
    ST77XX_BLACK
  );
}

void showGoScreen() {
  tft.fillScreen(ST77XX_GREEN);
  drawCenteredText("PRESS!", 38, 3, ST77XX_BLACK);
}

void showEarlyPressScreen() {
  tft.fillScreen(ST77XX_RED);

  drawCenteredText("TOO EARLY!", 32, 2, ST77XX_WHITE);
  drawCenteredText("Wait for green", 70, 1, ST77XX_WHITE);
  drawCenteredText("Round will restart", 88, 1, ST77XX_WHITE);
}

void showRoundResult() {
  tft.fillScreen(ST77XX_BLACK);

  drawCenteredText("REACTION TIME", 16, 2, ST77XX_CYAN);

  drawCenteredText(
    String(reactionTime) + " ms",
    50,
    3,
    ST77XX_WHITE
  );

  String message;

  if (reactionTime < 200) {
    message = "INCREDIBLE!";
  } else if (reactionTime < 300) {
    message = "VERY GOOD!";
  } else if (reactionTime < 450) {
    message = "GOOD!";
  } else {
    message = "GO FASTER!";
  }

  drawCenteredText(message, 91, 1, ST77XX_YELLOW);
}

void showFinalScreen() {
  unsigned long averageTime =
    totalReactionTime / TOTAL_ROUNDS;

  tft.fillScreen(ST77XX_MAGENTA);

  drawCenteredText("GAME OVER", 9, 2, ST77XX_WHITE);

  drawCenteredText(
    "Best: " + String(bestReactionTime) + " ms",
    45,
    1,
    ST77XX_WHITE
  );

  drawCenteredText(
    "Average: " + String(averageTime) + " ms",
    63,
    1,
    ST77XX_WHITE
  );

  drawCenteredText(
    "Press the button",
    91,
    1,
    ST77XX_YELLOW
  );

  drawCenteredText(
    "to play again",
    106,
    1,
    ST77XX_YELLOW
  );
}

// Begin the random waiting stage
void beginWaiting() {
  randomWaitTime = random(
    MIN_WAIT_TIME,
    MAX_WAIT_TIME + 1
  );

  showWaitingScreen();

  stateStartedAt = millis();
  gameState = WAITING;
}

// Reset all scores and start a new game
void startNewGame() {
  currentRound = 1;
  totalReactionTime = 0;
  bestReactionTime = ULONG_MAX;

  beginWaiting();
}

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // Use the ESP32 hardware random number generator
  randomSeed(esp_random());

  // XIAO ESP32-C6 hardware SPI pins
  SPI.begin(D8, D9, D10, TFT_CS);

  // Initialize the ST7735R display
  tft.initR(INITR_BLACKTAB);
  tft.setRotation(1);

  showStartScreen();
}

void loop() {
  bool pressed = buttonPressed();

  switch (gameState) {

    case START_SCREEN:
      if (pressed) {
        startNewGame();
      }
      break;

    case WAITING:
      // The player pressed before the green screen
      if (pressed) {
        showEarlyPressScreen();
        stateStartedAt = millis();
        gameState = EARLY_PRESS;
      }
      // The random waiting period has finished
      else if (millis() - stateStartedAt >= randomWaitTime) {
        showGoScreen();

        // Record the moment the green screen appears
        goStartedAt = millis();
        gameState = GO_SIGNAL;
      }
      break;

    case GO_SIGNAL:
      if (pressed) {
        reactionTime = millis() - goStartedAt;
        totalReactionTime += reactionTime;

        if (reactionTime < bestReactionTime) {
          bestReactionTime = reactionTime;
        }

        showRoundResult();
        stateStartedAt = millis();
        gameState = ROUND_RESULT;
      }
      break;

    case ROUND_RESULT:
      // Keep the result on the screen for 2.5 seconds
      if (millis() - stateStartedAt >= 2500) {

        if (currentRound >= TOTAL_ROUNDS) {
          showFinalScreen();
          gameState = GAME_FINISHED;
        } else {
          currentRound++;
          beginWaiting();
        }
      }
      break;

    case EARLY_PRESS:
      // Repeat the same round after an early press
      if (millis() - stateStartedAt >= 2000) {
        beginWaiting();
      }
      break;

    case GAME_FINISHED:
      if (pressed) {
        startNewGame();
      }
      break;
  }
}

How Does the Code Work?

Defining the SPI Display Connections

#define TFT_CS   D3
#define TFT_DC   D2
#define TFT_RST  D1

These lines tell the program which XIAO pins are connected to the CS, DC and RESET pins of the TFT display.

Using the Button with the Internal Pull-up Resistor

pinMode(BUTTON_PIN, INPUT_PULLUP);

This line activates the XIAO's internal pull-up resistor. The D0 pin therefore reads LOW when the button is pressed.

Generating a Random Waiting Time

randomWaitTime = random(2000, 5001);

A different waiting time between 2000 and 5000 milliseconds is created for each round. This prevents the player from predicting when the display will turn green.

Measuring the Reaction Time

goStartedAt = millis();
reactionTime = millis() - goStartedAt;

The starting time is recorded when the green screen appears. When the player presses the button, the saved starting time is subtracted from the current time to calculate the reaction time.

Why Does the Program Not Use delay()?

If a long delay() were used, the XIAO would not be able to check the button while waiting. Using millis() allows the program to continue checking for an early button press during the random waiting period.

Running the Game

  1. Recheck all circuit connections.
  2. Connect the XIAO ESP32-C6 to the computer using a data-capable USB cable.
  3. Select the XIAO ESP32-C6 board and the correct port in Arduino IDE.
  4. Upload the program to the board.
  5. Press the button when the start message appears.
  6. Wait while the display shows “Get ready...”.
  7. Press the button as soon as the display turns green.

Troubleshooting

The Display Remains Completely White

  • Check the CS, DC and RESET connections.
  • Make sure SCK is connected to D8.
  • Make sure SDA/MOSI is connected to D10.
  • Check that the display and XIAO share the same GND connection.

The Display Backlight Does Not Turn On

Check the LED or BL pin on the TFT module. If this pin is not powered, the display may be producing an image even though the backlight remains off.

The Display Colors Are Incorrect

Some ST7735 display versions require a different initialization setting. Replace:

tft.initR(INITR_BLACKTAB);

with one of the following options:

tft.initR(INITR_GREENTAB);
// or
tft.initR(INITR_REDTAB);

The Image Is Rotated or Upside Down

Change the value in the following line to 0, 1, 2 or 3:

tft.setRotation(1);

The Button Appears to Be Permanently Pressed

The two wires may be connected to pins on the same side of a four-pin tactile button. Connect D0 and GND to pins on opposite sides of the button.

Ideas for Improving the Project

  • Add a buzzer that produces a sound when the display turns green.
  • Create easy, normal and difficult game modes.
  • Save the best score permanently in the ESP32 memory.
  • Add a second button to create a two-player reaction game.
  • Display different shapes or colors in each round.
  • Use the wireless features of the XIAO ESP32-C6 to display scores on a web page.
  • Add a LiPo battery and power switch to create a portable game.

Conclusion

In this project, we controlled an ST7735R-based SPI TFT display with the XIAO ESP32-C6, read input from a physical push button and measured the player's reaction time in milliseconds. We also used random number generation, button debouncing and non-blocking timing with millis().

When building the project for the first time, power it through USB. After confirming that the display and game work correctly, you can add a battery, power switch, buzzer and custom enclosure to make the game portable.
Share
T-Soft | Premium