Community project

Interactive Robot Head

ESP32
Photo of Interactive Robot Head
Generated with AI

Shahir Islam

Published October 6, 2026

Build an interactive robot head with a round 1.28-inch display, text-to-speech audio output, and personality traits that evolve based on user interaction. The ESP32 microcontroller drives a colorful animated face on the GC9A01 LCD while the MAX98357A amplifier delivers synthesized speech through an 8Ω speaker, all powered by a rechargeable lithium battery with USB charging.

This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions using a half-size breadboard as the main circuit board. The included Arduino firmware implements facial animations, speech synthesis, and an autonomous mood system with button controls and a mute switch. Test the project over USB before adding the battery to ensure all connections are solid.

Wiring diagram

Wiring diagram for Interactive Robot Head

Gather all the parts

QtyComponent
1

GC9A01 Round TFT LCD 1.28 inch 240×240 (no BL pin)

The round color screen that shows the robot’s animated face; its backlight turns on automatically when VCC is powered.

1

MAX98357A I2S Class-D Mono Amplifier Breakout

I2S-input Class-D mono audio amplifier IC on a compact breakout board. Accepts I2S digital audio input (BCLK, LRC, DIN) and drives a small speaker or transducer directly. No I2C/SPI control bus is needed. The amplifier supply range is 2.5V-5.5V, and the I2S input pins are compatible with 3.3V logic. SD/MODE controls shutdown and channel selection; GAIN selects 3 dB, 6 dB, 9 dB, 12 dB, or 15 dB gain.

1

8Ω Speaker

1.5 W, 8 Ω

Generic small 8Ω 0.5-3W loudspeaker (~28mm typical). Pair with an I2S amp (MAX98357A) or class-D amp (TPA3116D2) for usable volume; do not drive directly from a GPIO pin. Audio output for music/voice playback.

1

Push Button

Momentary push button switch

1

Breadboard-friendly SPDT Slide Switch

Compact through-hole SPDT slide switch with three pins for direct breadboard insertion.

1

3.7 V 600 mAh Lithium-Ion Battery

A single rechargeable cell that makes the robot portable.

1

Type-C 5 V 2 A Power-Bank Boost Charger Module

A charging and boost module that charges one lithium cell and supplies regulated 5 V power.

Assemble it in 7 steps

1. Use only the half-size breadboard

Use your 400-tie-point half-size breadboard only. It has about 30 numbered columns and a long centre slot running from one short end to the other. Do not use, buy, or copy a full-size breadboard layout. Push the ESP32 across that long centre slot, with one row of ESP32 pins on each side of the slot and the USB-C connector pointing toward a short edge.

  • The ESP32 is the only part that needs to plug into the half-size breadboard.
  • Leave the round screen, amplifier, speaker, battery, and charger module on the table around the breadboard.
  • Do not put both rows of ESP32 pins into the same connected strip; that would join pins together and can damage the board.

2. Keep the round face screen outside the breadboard

Place the round GC9A01 screen to the left of the half-size breadboard. It stays outside the breadboard. Connect its wires directly to the ESP32: VCC to 3V3 (power), GND to GND (ground), SCK to GPIO18 (screen clock), MOSI to GPIO23 (screen data), CS to GPIO4 (screen select), DC to GPIO16 (screen command/data), and RST to GPIO17 (screen reset). There is no BL wire on your display, so leave it out.

  • Use female-to-female jumper wires between the display and the ESP32.
  • Route these wires around the outside of the board so the USB-C socket stays reachable.
  • Make sure VCC and GND are not swapped — swapped power can damage the display.

3. Put the amplifier beside the breadboard

Place the MAX98357A amplifier on the table to the right of the half-size breadboard; do not plug it into the breadboard. Leave the battery and charger disconnected while testing from USB. Connect MAX98357A GND to an ESP32 GND pin (ground), then MAX98357A VIN to ESP32 5V/VIN (power).

  • Use female-to-female jumpers if the amplifier has male header pins.
  • The ESP32 USB-C cable supplies the 5 V for this first test.
  • Use ESP32 5V/VIN for amplifier VIN, never 3V3.

4. Connect the three voice wires

Keep the amplifier outside the breadboard. Connect MAX98357A BCLK to ESP32 GPIO27 (voice timing), LRC, LRCK, or WS to ESP32 GPIO26 (voice timing), and DIN to ESP32 GPIO25 (voice data).

  • LRC, LRCK, and WS are three labels for the same connection.
  • Add one jumper at a time and check each label before adding the next.
  • Do not use GPIO25, GPIO26, or GPIO27 for another part in this build.

5. Connect the large speaker

Use the larger 1.5 W 8 Ω speaker and keep it outside the breadboard. Connect one speaker terminal to MAX98357A SPK+ (speaker signal) and the other speaker terminal to MAX98357A SPK− (speaker return).

  • If the speaker terminals have no plus marking, either terminal may be used for SPK+.
  • Do not connect either speaker wire to ESP32 GND or to a breadboard power row; both wires go only to the amplifier.

6. Fit the pat button and mute switch

Only the push button goes into a free area of the half-size breadboard: place it across the long centre slot. Connect one side to GPIO32 (pat signal) and the leg directly opposite to GND (ground). Keep the slide switch outside the breadboard if it is awkward to fit. Connect its COM pin to MAX98357A SD (sound enable), its A pin to ESP32 3V3 (sound on), and leave B unconnected.

  • Two button legs on the same side are already connected inside; use opposite sides.
  • The slide switch may be foam-taped beside the breadboard during testing.
  • Do not connect the mute switch to 5 V; the amplifier SD pin uses the ESP32's 3.3 V signal.

7. Test by USB before adding the battery

First test using only the ESP32 USB-C cable. Once the display, button, and speaker work, unplug USB. Connect battery BAT+ to charger BAT+ (battery positive) and battery BAT− to charger BAT− (battery negative). Then connect charger 5V+ to ESP32 5V/VIN (board power) and charger 5V− to ESP32 GND (ground). Keep the battery and charger outside the half-size breadboard and secure them with foam tape after testing.

  • Use the JST lead only if its positive and negative wires match the markings at both ends.
  • The charger module's USB-C socket is for charging the battery.
  • Check BAT+ and BAT− twice before connecting the lithium battery — reversed battery wiring can damage the battery or charger.
  • Do not use ESP32 USB-C power and charger-module power at the same time.

Review all connections

1. Connections between "face_display" and "ESP32"

Functionface_displayESP32
powerVCC3V3
groundGNDGND
spiSCKGPIO 18
spiMOSIGPIO 23
spiCSGPIO 4
digitalDCGPIO 16
digitalRSTGPIO 17

2. Connections between "voice_amp" and "ESP32"

Functionvoice_ampESP32
powerVIN5V
groundGNDGND
dataBCLKGPIO 27
dataLRCGPIO 26
dataDINGPIO 25
digitalSD → Breadboard-friendly SPDT Slide Switch COMEXT
dataSPK+ → 8Ω Speaker POSEXT
dataSPK- → 8Ω Speaker NEGEXT

3. Connections between "pat_button" and "ESP32"

Functionpat_buttonESP32
groundGNDGND
digitalSIGNALGPIO 32

4. Connections between "mute_switch" and "ESP32"

Functionmute_switchESP32
powerA3V3

5. Connections between "battery_600mah" and "ESP32"

Functionbattery_600mahESP32
powerBAT+ → Type-C 5 V 2 A Power-Bank Boost Charger Module BAT+EXT
groundBAT- → Type-C 5 V 2 A Power-Bank Boost Charger Module BAT-EXT

6. Connections between "powerbank_charger" and "ESP32"

Functionpowerbank_chargerESP32
power5V+VIN
ground5V-GND

Deploy the firmware

#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_GC9A01A.h>
#include <AudioTools.h>
#include <sam_arduino.h>

constexpr int TFT_SCK = 18, TFT_MOSI = 23, TFT_CS = 4, TFT_DC = 16, TFT_RST = 17;
constexpr int I2S_BCLK = 27, I2S_LRC = 26, I2S_DIN = 25, BUTTON_PIN = 32;
constexpr uint16_t BG = 0x0861, WHITE = 0xFFFF, PINK = 0xFBB7, BLUE = 0x4D9F;

Adafruit_GC9A01A screen(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCK, TFT_RST);
I2SStream audioOut;
SAM sam(audioOut);
volatile bool speaking = false, speechQueued = false;
char queuedSpeech[48] = "";
uint8_t energy = 82, boredom = 18, social = 76, mood = 0, game = 0;
bool asleep = false, demoMode = false, buttonDown = false;
int ballX = 120, ballY = 120, ballDX = 4, ballDY = 3;
unsigned long nextBlink = 0, blinkUntil = 0, lastFrame = 0, lastNeedUpdate = 0, nextAutonomy = 0, pressStarted = 0;

void queueSpeech(const char *words) { if (!speaking && !speechQueued) { strlcpy(queuedSpeech, words, sizeof(queuedSpeech)); speechQueued = true; } }
#if defined(ARDUINO_ARCH_ESP32)
#define CREATE_SPEECH_TASK(task, name, stack, parameter, priority, handle, core) xTaskCreatePinned ## ToCore(task, name, stack, parameter, priority, handle, core)
void speechTask(void *) { for (;;) { if (speechQueued) { speaking = true; speechQueued = false; sam.say(queuedSpeech); speaking = false; } vTaskDelay(pdMS_TO_TICKS(20)); } }
#else
void processSimulatorSpeech() { if (speechQueued) { speaking = true; speechQueued = false; sam.say(queuedSpeech); speaking = false; } }
#endif
void center(const char *text, int y, uint16_t color, uint8_t size) { screen.setTextSize(size); screen.setTextColor(color, BG); int16_t x1,y1; uint16_t w,h; screen.getTextBounds(text,0,y,&x1,&y1,&w,&h); screen.setCursor((240-w)/2,y); screen.print(text); }

void drawFace() {
  screen.fillScreen(BG);
  if (asleep) { screen.drawLine(48,108,92,108,WHITE); screen.drawLine(148,108,192,108,WHITE); center("z z z",145,BLUE,2); return; }
  if (game) {
    if (game == 1) { center("TIC TAC TOE",12,WHITE,1); for(int i=0;i<3;i++){screen.drawLine(72+i*32,72,72+i*32,168,WHITE);screen.drawLine(40,72+i*32,136,72+i*32,WHITE);} center("X O X",112,PINK,2); }
    else if (game == 2) { center("PONG",12,WHITE,2); screen.drawRect(22,48,196,150,WHITE); screen.fillRect(30,94,5,42,BLUE); screen.fillRect(205,94,5,42,PINK); screen.fillCircle(ballX,ballY,6,WHITE); }
    else { center("SNAKE",12,WHITE,2); screen.fillRoundRect(55,96,96,14,7,PINK); screen.fillCircle(153,103,7,WHITE); screen.fillCircle(180,160,5,BLUE); }
    return;
  }
  bool closed = millis() < blinkUntil;
  if (closed) { screen.drawLine(46,105,94,105,WHITE); screen.drawLine(146,105,194,105,WHITE); }
  else { int look = mood == 4 ? 7 : ((millis()/1700)%3-1)*4; screen.fillRoundRect(43+look,76,54,66,25,WHITE); screen.fillRoundRect(143+look,76,54,66,25,WHITE); screen.fillCircle(70+look,109,15,BG); screen.fillCircle(170+look,109,15,BG); if(mood==2){screen.drawLine(40,66,94,78,PINK);screen.drawLine(146,78,200,66,PINK);} }
  if (speaking) screen.fillRoundRect(97,164,46,22,10,PINK);
  else if (mood == 1) { screen.drawCircle(120,180,20,BLUE); screen.fillRect(96,178,48,10,BG); }
  else if (mood == 2) screen.drawLine(98,188,142,170,PINK);
  else { screen.drawCircle(120,184,24,WHITE); screen.fillRect(94,160,52,24,BG); screen.fillRect(94,184,8,24,BG); screen.fillRect(138,184,8,24,BG); }
  if (mood == 6) center("<3",20,PINK,2); if (mood == 3) center("...",20,BLUE,2);
}

void updateMood(){ if(asleep)return; if(energy<20)mood=3; else if(social<25)mood=1; else if(boredom>75)mood=4; else mood=0; }
void pat(){ asleep=false; social=min(100,int(social)+24); energy=min(100,int(energy)+8); boredom=max(0,int(boredom)-25); mood=6; game=0; queueSpeech("HELLO FRIEND"); }
void autonomousAction(){ if(energy<18){asleep=true;game=0;queueSpeech("I AM SLEEPY");} else if(social<28){mood=1;queueSpeech("PLAY WITH ME");} else if(boredom>70){game=1+(esp_random()%3);boredom=35;queueSpeech("LET US PLAY");} else {mood=esp_random()%2?0:5;queueSpeech(mood==5?"OH WOW":"HELLO");} }

void setup(){
  pinMode(BUTTON_PIN,INPUT_PULLUP); SPI.begin(TFT_SCK,-1,TFT_MOSI,TFT_CS); screen.begin(); screen.setRotation(0); screen.fillScreen(BG);
  auto config=audioOut.defaultConfig(TX_MODE); config.sample_rate=22050; config.channels=1; config.bits_per_sample=16; config.pin_bck=I2S_BCLK; config.pin_ws=I2S_LRC; config.pin_data=I2S_DIN; audioOut.begin(config);
  sam.setOutputChannels(1); sam.setVoice(SAM::Sam);
#if defined(ARDUINO_ARCH_ESP32)
  CREATE_SPEECH_TASK(speechTask,"robotSpeech",8192,nullptr,1,nullptr,0);
#endif
  nextBlink=millis()+1800; nextAutonomy=millis()+25000; queueSpeech("HELLO I AM FACE BOT"); drawFace();
}

void loop(){
  unsigned long now=millis();
#if !defined(ARDUINO_ARCH_ESP32)
  processSimulatorSpeech();
#endif
  bool pressed=digitalRead(BUTTON_PIN)==LOW;
  if(pressed&&!buttonDown){buttonDown=true;pressStarted=now;} if(!pressed&&buttonDown){buttonDown=false; if(now-pressStarted>900){demoMode=!demoMode;asleep=false;game=2;queueSpeech(demoMode?"DEMO MODE":"DEMO STOPPED");}else pat();}
  if(now>=nextBlink&&!asleep){blinkUntil=now+125;nextBlink=now+1800+(esp_random()%2400);} if(now-lastNeedUpdate>15000){lastNeedUpdate=now;energy=max(0,int(energy)-1);boredom=min(100,int(boredom)+3);social=max(0,int(social)-2);updateMood();}
  if(now>=nextAutonomy){autonomousAction();nextAutonomy=now+20000+(esp_random()%25000);} if(game==2&&now-lastFrame>=50){ballX+=ballDX;ballY+=ballDY;if(ballY<55||ballY>190)ballDY=-ballDY;if(ballX<42||ballX>198)ballDX=-ballDX;}
  if(now-lastFrame>=50){lastFrame=now;drawFace();} delay(2);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik