Community project

Morphing Haunted House Diorama

Photo of Morphing Haunted House Diorama
Generated with AI

darrell hixon

Published November 9, 2026

This haunted house diorama combines animated graphics, servo-driven mechanics, and spooky sound effects to create an interactive display. Two 1.8-inch TFT screens show morphing portraits and flickering fire animations while seven servos animate physical scenery elements, all synchronized with audio playback through a speaker.

Builders will receive a complete wiring diagram, parts list, and firmware that handles screen rendering, servo control via the PCA9685 driver, and MP3 audio playback. The guide covers power distribution from a single 5V 10A supply, calibration of servo travel ranges, and step-by-step assembly to bring the diorama to life.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

ST7735 1.8 inch TFT SPI Display

1.8 inch SPI TFT colour display with ST7735 controller, 128x160 resolution, 3.3V logic. Communicates over hardware SPI with MOSI, SCK, CS, DC, and RST control lines. Modules typically include an onboard regulator and level shifting accepting 3.3V or 5V VCC; logic is 3.3V. MISO is optional for write-only display use.

1

ST7735 1.8 inch TFT SPI Display

1.8 inch SPI TFT colour display with ST7735 controller, 128x160 resolution, 3.3V logic. Communicates over hardware SPI with MOSI, SCK, CS, DC, and RST control lines. Modules typically include an onboard regulator and level shifting accepting 3.3V or 5V VCC; logic is 3.3V. MISO is optional for write-only display use.

1

PCA9685 PWM Servo Driver

NXP PCA9685-based 16-channel, 12-bit PWM/servo driver module. Controlled over I2C at default address 0x40, with address pins for additional addresses. Outputs 16 independent PWM channels at one shared configurable frequency; servos normally use about 50-60 Hz. VCC is the logic supply and sets the I2C/PWM logic level. V+ is the separate servo/load rail, commonly 5V-6V for servos. OE is active high disable and is pulled low by default on Adafruit-style breakouts.

1

DFPlayer Mini MP3 Audio Module

Small UART-controlled MP3 playback module with a microSD card slot and onboard audio DAC/amplifier interface. The MCU controls playback over serial while speaker/DAC pins connect to the audio output path.

1

8Ω Speaker

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

SG90 Servo

Micro servo motor (SG90)

1

SG90 Servo

Micro servo motor (SG90)

1

SG90 Servo

Micro servo motor (SG90)

1

SG90 Servo

Micro servo motor (SG90)

1

SG90 Servo

Micro servo motor (SG90)

1

SG90 Servo

Micro servo motor (SG90)

1

SG90 Servo

Micro servo motor (SG90)

1

Resistor

1 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Electrolytic Capacitor

2200 µF, 10 V

Radial aluminium electrolytic capacitor bought by value and voltage rating, such as 10 µF, 100 µF or 1000 µF 10 V. It is polarised: the + lead goes to the higher voltage and the - lead, marked by the stripe on the sleeve, to the lower voltage or ground. It is mostly used for bulk filtering across a power rail, with a voltage rating comfortably above the rail.

1

Regulated 5 V 10 A external supply

5 V DC, 10 A

User-owned regulated 5 V DC supply, rated 10 A; use its low-voltage positive and negative output terminals only.

1

Resistor

1 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

2 kΩ

Through-hole resistor (current-limiting in series with an LED)

Assemble it in 6 steps

1. Prepare the two screens

Use two 1.8-inch 128×160 ST7735R non-IPS TFT modules with backlight circuitry onboard. Fire goes behind the fireplace; portrait behind a picture frame. Both share GPIO11 (image data) and GPIO12 (clock). Fire CS → GPIO10, DC → GPIO4, RST → GPIO5. Portrait CS → GPIO14, DC → GPIO6, RST → GPIO7. VCC and BLK → 3V3; GND → GND. Leave MISO disconnected.

  • The included portrait is stylized drawn artwork, not a photographic painting. Check viewing angle before mounting non-IPS panels.
  • Firmware uses INITR_BLACKTAB. A module with shifted or incorrect colors may need its matching ST7735 initialization option.

2. Connect the servo controller

PCA9685 VCC → 3V3 (logic power), SDA → GPIO8 (data), SCL → GPIO9 (clock), GND and OE → GND. Leave address jumpers open for address 0x40. Connect servo signals to channels 0–6 in order.

  • Do not connect PCA9685 VCC to 5 V: that would pull the ESP32 data wires above their safe voltage.

3. Connect the single 5 V 10 A supply

With all power unplugged, connect supply1 +5V to a covered distribution point using wire and connectors rated for at least 10 A. Run separate branches to the seven servo red wires, PCA9685 V+, and DFPlayer VCC. Return all servo grounds and DFPlayer GND directly to the supply-negative distribution point; also connect PCA9685 GND and ESP32 GND there for the signal reference. Do not route combined servo current through the PCA9685 board or a solderless breadboard. Servo signal wires still go to PCA9685 channels 0–6. Put the 2200 µF, 10 V capacitor close to the driver: + to the 5 V distribution branch and striped minus to GND. Keep the DFPlayer power branch separate from the servo branches back to the supply to reduce motor noise.

  • Use regulated 5 V DC only; check voltage and polarity before connecting electronics. The 10 A rating is available capacity, not current forced into each device.
  • Never connect external +5 V to ESP32 5V, USB, or 3V3. USB powers only the board and displays; the grounds must still join.
  • A 10 A supply can overheat thin wires during a short. Use insulated, properly rated distribution wiring and a current-limited or short-circuit-protected supply. Never work on exposed mains terminals; an open-frame supply must be safely enclosed by a qualified person.
  • Reverse capacitor polarity can make it burst. The capacitor reduces brief voltage dips but cannot fix an overloaded supply or jammed servo.

4. Add sound

GPIO17 → 1 kΩ resistor → DFPlayer RX (sound commands). DFPlayer TX → 1 kΩ tx_top resistor → GPIO18. Connect the 2 kΩ tx_bottom resistor from GPIO18 to GND; this divider protects the ESP32 input. Connect an 8Ω 3W speaker across SPK1 and SPK2, not ground. Insert a FAT32 microSD card, 32 GB or smaller, into DFPlayer with a combined haunted music and fire crackle recording named /mp3/0001.mp3. It repeats continuously. Audio is supplied by you; Deploy cannot write this card.

  • A single DFPlayer cannot mix two recordings. Combine the sounds into one file before copying it to the card. Start at modest volume.

5. Fit and test moving scenery

First test all seven servos without horns or linkages. Channels 0–3 operate windows, channel 4 the door, and channels 5–6 other props. Firmware moves one channel at a time through a small 1400–1600 microsecond range. Mount horns only after noting the closed position, then adjust each closedUs/openUs value for your mechanism. Use light scenery and flexible links; provide mechanical clearance. Servos release after each movement, so props must rest safely without motor holding force.

  • Keep fingers away from linkages. Stop power immediately if a servo buzzes, jams, or gets hot. Never increase travel against a physical stop.

6. Double-check the separate power rails

The checker flags the driver's 5 V servo rail, but its logic VCC must remain 3.3 V as wired. V+ supplies motors only. GPIO14 is a normal digital chip-select output. The external 5 V supply does not feed the ESP32 board; USB powers the board and screens. Confirm these connections before switching on.

Review all connections

1. Connections between "fire" and "ESP32"

FunctionfireESP32
powerVCC3V3
groundGNDGND
powerBLK3V3
spiSCKGPIO 12
spiMOSIGPIO 11
spiCSGPIO 10
digitalDCGPIO 4
digitalRSTGPIO 5

2. Connections between "portrait" and "ESP32"

FunctionportraitESP32
powerVCC3V3
groundGNDGND
powerBLK3V3
spiSCKGPIO 12
spiMOSIGPIO 11
spiCSGPIO 14
digitalDCGPIO 6
digitalRSTGPIO 7

3. Connections between "driver" and "ESP32"

FunctiondriverESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 8
i2cSCLGPIO 9
powerV+5V
groundOEGND
digitalPWM0-PWM15[0] → SG90 Servo SIGNALEXT
digitalPWM0-PWM15[1] → SG90 Servo SIGNALEXT
digitalPWM0-PWM15[2] → SG90 Servo SIGNALEXT
digitalPWM0-PWM15[3] → SG90 Servo SIGNALEXT
digitalPWM0-PWM15[4] → SG90 Servo SIGNALEXT
digitalPWM0-PWM15[5] → SG90 Servo SIGNALEXT
digitalPWM0-PWM15[6] → SG90 Servo SIGNALEXT

4. Connections between "audio" and "ESP32"

FunctionaudioESP32
powerVCC5V
groundGNDGND
dataSPK1 → 8Ω Speaker POSEXT
dataSPK2 → 8Ω Speaker NEGEXT
uartTX → Resistor P1EXT

5. Connections between "rx_resistor" and "ESP32"

Functionrx_resistorESP32
digitalP1GPIO 17
digitalP2 → DFPlayer Mini MP3 Audio Module RXEXT

6. Connections between "servo0" and "ESP32"

Functionservo0ESP32
powerVCC5V
groundGNDGND

7. Connections between "servo1" and "ESP32"

Functionservo1ESP32
powerVCC5V
groundGNDGND

8. Connections between "servo2" and "ESP32"

Functionservo2ESP32
powerVCC5V
groundGNDGND

9. Connections between "servo3" and "ESP32"

Functionservo3ESP32
powerVCC5V
groundGNDGND

10. Connections between "servo4" and "ESP32"

Functionservo4ESP32
powerVCC5V
groundGNDGND

11. Connections between "servo5" and "ESP32"

Functionservo5ESP32
powerVCC5V
groundGNDGND

12. Connections between "servo6" and "ESP32"

Functionservo6ESP32
powerVCC5V
groundGNDGND

13. Connections between "supply1" and "ESP32"

Functionsupply1ESP32
power+5V5V
groundGNDGND

14. Connections between "bulk" and "ESP32"

FunctionbulkESP32
power+5V
ground-GND

15. Connections between "tx_top" and "ESP32"

Functiontx_topESP32
digitalP2GPIO 18

16. Connections between "tx_bottom" and "ESP32"

Functiontx_bottomESP32
digitalP1 → Resistor P2EXT
groundP2GND

Deploy the firmware

#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <Adafruit_PWMServoDriver.h>
#include <DFRobotDFPlayerMini.h>
#include <math.h>
Adafruit_ST7735 fire(10,4,5), portrait(14,6,7);
Adafruit_PWMServoDriver pwm(0x40);
DFRobotDFPlayerMini audio;
HardwareSerial music(1);
GFXcanvas16 canvas(128,160), lady(128,160), bones(128,160);
bool servoReady=false, moving=false;
// Conservative starting travel. Calibrate with linkages disconnected.
const uint16_t closedUs[7]={1400,1400,1400,1400,1400,1400,1400};
const uint16_t openUs[7]={1600,1600,1600,1600,1600,1600,1600};
int activeServo=0, lastMorph=-1;
uint32_t lastFire=0,lastPortrait=0,lastServo=0;
uint16_t rgb(int r,int g,int b){return ((r&248)<<8)|((g&252)<<3)|(b>>3);}
void makePortrait(GFXcanvas16 &c,bool skull){
 c.fillScreen(rgb(21,12,18));
 for(int i=0;i<5;i++) c.drawRect(i+3,i+3,122-2*i,154-2*i,rgb(145-i*17,100-i*12,32));
 c.fillTriangle(20,146,64,87,108,146,rgb(37,22,48));
 c.fillCircle(64,95,14,rgb(46,28,55));
 c.fillCircle(64,53,30,rgb(39,22,17));
 c.fillCircle(39,66,12,rgb(39,22,17));c.fillCircle(89,66,12,rgb(39,22,17));
 c.fillRoundRect(44,40,40,48,18,skull?rgb(209,203,167):rgb(211,163,129));
 if(!skull){
  c.fillCircle(54,59,2,0);c.fillCircle(74,59,2,0);
  c.drawLine(64,61,61,70,rgb(145,95,78));
  c.drawLine(57,77,71,77,rgb(130,39,49));
  c.fillTriangle(47,91,64,105,81,91,rgb(210,201,180));
 }else{
  c.fillCircle(54,59,7,0);c.fillCircle(74,59,7,0);
  c.fillTriangle(64,67,60,74,68,74,0);
  c.fillRect(52,78,25,8,rgb(220,213,177));
  for(int x=54;x<78;x+=5)c.drawFastVLine(x,78,8,0);
  c.drawFastVLine(64,94,44,rgb(214,208,176));
  for(int y=105;y<134;y+=7){c.drawLine(64,y,45,y-4,rgb(214,208,176));c.drawLine(64,y,83,y-4,rgb(214,208,176));}
 }
 c.fillCircle(64,104,3,rgb(160,25,35));
}
void drawFire(){
 canvas.fillScreen(0);
 // Vary each flame independently; buffer prevents visible clearing flicker.
 for(int x=4;x<124;x+=6){
  int h=random(25,100), tip=x+random(-8,9);
  canvas.fillTriangle(x-9,145,x+9,145,tip,145-h,rgb(205+random(50),random(30,95),0));
  canvas.fillTriangle(x-4,145,x+5,145,tip,150-h/2,rgb(255,160+random(80),15));
 }
 canvas.fillRoundRect(12,140,80,10,4,rgb(72,28,10));
 canvas.fillRoundRect(40,149,76,8,4,rgb(83,33,12));
 fire.drawRGBBitmap(0,0,canvas.getBuffer(),128,160);
}
void drawPortrait(int mix){
 uint16_t *out=canvas.getBuffer(),*a=lady.getBuffer(),*b=bones.getBuffer();
 for(int i=0;i<128*160;i++){
  int r=(((a[i]>>11)&31)*(32-mix)+((b[i]>>11)&31)*mix)/32;
  int g=(((a[i]>>5)&63)*(32-mix)+((b[i]>>5)&63)*mix)/32;
  int bl=((a[i]&31)*(32-mix)+(b[i]&31)*mix)/32;
  out[i]=(r<<11)|(g<<5)|bl;
 }
 portrait.drawRGBBitmap(0,0,out,128,160);
}
void setup(){
 Serial.begin(115200);
 pinMode(10,OUTPUT);digitalWrite(10,HIGH);pinMode(14,OUTPUT);digitalWrite(14,HIGH);
 SPI.begin(12,-1,11);
 fire.initR(INITR_BLACKTAB);portrait.initR(INITR_BLACKTAB);
 makePortrait(lady,false);makePortrait(bones,true);
 drawPortrait(0);lastMorph=0;
 Wire.begin(8,9);
 servoReady=pwm.begin();
 if(servoReady){pwm.setPWMFreq(50);for(int i=0;i<16;i++)pwm.setPWM(i,0,4096);}
 else Serial.println("PCA9685 missing: check power and I2C wiring.");
 // Module TX returns through a 1k/2k voltage divider.
 music.begin(9600,SERIAL_8N1,18,17);
 audio.begin(music,false,false);
 delay(1500);audio.volume(16);audio.playMp3Folder(1);delay(150);audio.enableLoop();
 Serial.println("Haunted house running. Audio commands sent; check card and speaker for playback.");
}
void loop(){
 uint32_t now=millis();
 if(now-lastFire>=80){lastFire=now;drawFire();}
 if(now-lastPortrait>=100){
  lastPortrait=now;uint32_t t=now%24000;int m;
  if(t<8000)m=0;else if(t<12000)m=(t-8000)*32/4000;
  else if(t<20000)m=32;else m=32-(t-20000)*32/4000;
  if(m!=lastMorph){drawPortrait(m);lastMorph=m;}
 }
 // One prop moves at a time. Channels 0..3 windows, 4 door, 5..6 objects.
 if(servoReady && now-lastServo>=20){
  lastServo=now;uint32_t slot=now%21000;activeServo=slot/3000;
  uint32_t t=slot%3000;
  if(t<2200){
   float f=(1.0f-cosf(6.2831853f*t/2200.0f))/2;
   pwm.writeMicroseconds(activeServo,closedUs[activeServo]+int((openUs[activeServo]-closedUs[activeServo])*f));
  }else pwm.setPWM(activeServo,0,4096); // release prop, reduce heating
 }
}
// Wiring reviewed: GPIO18 is the junction of tx_top.P2 and tx_bottom.P1.
// GPIO14 is software-controlled CS, not a fixed SPI peripheral signal.
// PCA9685 VCC is 3.3V; its separate 5V V+ rail never sets I2C logic voltage.

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