Hardware Reference
Quick-reference wiring and code for every component used in this course.
- ESP32-S3 Safe Pin Guide
- 1. Built-in NeoPixel (ESP32-S3)
- 2. Push Button (Active-Low)
- 3. IR Proximity Sensor
- 4. HC-SR04 Ultrasonic Distance Sensor
- 5. 9G Micro Servo Motor
- 6. Relay Module (3.3V)
- 7. WS2812B 8×8 NeoPixel Grid
- Quick-Reference: Pin Assignments
Safety first: Always double-check your wiring before powering on. The ESP32-S3 runs at 3.3V logic — do not connect 5V signals directly to GPIO pins without a voltage divider. Never short-circuit power and ground pins.
ESP32-S3 Safe Pin Guide
Not all GPIO pins are equal. Some have special functions or restrictions:
| Pins | Notes |
|---|---|
| GPIO 0 | Good for buttons (has pull-up support). Hold LOW at boot = bootloader mode |
| GPIO 1, 3 | UART TX/RX — avoid using for other things |
| GPIO 6–20 | Generally safe for external components |
| GPIO 45, 46 | Strapping pins — avoid using |
| GPIO 48 | Built-in NeoPixel — already wired on the board |
Recommended pins for components:
- Buttons: GPIO 0, GPIO 14
- IR sensor: GPIO 12
- HC-SR04: TRIG = GPIO 5, ECHO = GPIO 4
- Servo: GPIO 13
- Relay: GPIO 15
- 8×8 NeoPixel grid: GPIO 6
1. Built-in NeoPixel (ESP32-S3)
No external wiring needed — the NeoPixel is soldered onto the board at GPIO 48.
Code:
import machine
import neopixel
pin = machine.Pin(48, machine.Pin.OUT)
np = neopixel.NeoPixel(pin, 1) # 1 LED
np[0] = (255, 0, 0) # Red (R, G, B) — values 0–255
np.write() # Send to LED
np[0] = (0, 0, 0) # Off
np.write()
Colour examples:
| Colour | RGB tuple |
|---|---|
| Red | (255, 0, 0) |
| Green | (0, 255, 0) |
| Blue | (0, 0, 255) |
| Yellow | (255, 255, 0) |
| Cyan | (0, 255, 255) |
| Magenta | (255, 0, 255) |
| White | (255, 255, 255) |
| Off | (0, 0, 0) |
2. Push Button (Active-Low)
Wiring:
| Button Leg | Connect To |
|---|---|
| Leg 1 | GPIO pin (e.g., GPIO 0) |
| Leg 2 | GND |
Use Pin.PULL_UP to enable the internal pull-up resistor. Without it, the pin “floats” and gives unreliable readings.
Logic:
- Button NOT pressed: pin reads
1(HIGH, pulled up to 3.3V) - Button pressed: pin reads
0(LOW, connected to GND)
This is called active-low.
Code:
from machine import Pin
import time
btn = Pin(0, Pin.IN, Pin.PULL_UP)
while True:
if btn.value() == 0: # Button pressed
print("Pressed!")
time.sleep(0.05)
Two buttons:
btn_a = Pin(0, Pin.IN, Pin.PULL_UP)
btn_b = Pin(14, Pin.IN, Pin.PULL_UP)
3. IR Proximity Sensor
Wiring:
| IR Sensor Pin | Connect To |
|---|---|
| VCC | 3.3V |
| GND | GND |
| OUT | GPIO 12 (or any GPIO) |
Logic: Active-low — output goes 0 (LOW) when an object is detected.
Some modules have a sensitivity potentiometer (small screw). Turn it to adjust detection range.
Code:
from machine import Pin
import time
ir = Pin(12, Pin.IN)
while True:
if ir.value() == 0: # Object detected
print("Object detected!")
else:
print("Clear")
time.sleep(0.1)
Did you know? IR sensors work by emitting infrared light and detecting its reflection. They’re used in automatic hand sanitiser dispensers, TV remote controls, and robot obstacle detection.
4. HC-SR04 Ultrasonic Distance Sensor
Wiring:
| HC-SR04 Pin | Connect To |
|---|---|
| VCC | 5V (some modules work on 3.3V — check yours) |
| GND | GND |
| TRIG | GPIO 5 |
| ECHO | GPIO 4 (via voltage divider if module outputs 5V) |
Voltage divider on ECHO: Some HC-SR04 modules output 5V on the ECHO pin, which could damage the ESP32-S3’s 3.3V GPIO. Add a voltage divider: ECHO → 1kΩ → junction → GPIO, and junction → 2kΩ → GND. HC-SR04P variants run natively at 3.3V and don’t need this.
How it works: Send a 10µs pulse on TRIG. The sensor emits 8 ultrasonic pulses and raises ECHO HIGH until they return. Measure ECHO duration → calculate distance.
Distance (cm) = echo_duration_µs / 58.2
Code:
from machine import Pin
import time
trig = Pin(5, Pin.OUT)
echo = Pin(4, Pin.IN)
def get_distance():
"""Measure distance in cm. Returns -1 if no reading."""
trig.off()
time.sleep_us(2)
trig.on()
time.sleep_us(10)
trig.off()
timeout = time.ticks_us() + 30000 # 30ms timeout
while echo.value() == 0:
if time.ticks_us() > timeout:
return -1
start = time.ticks_us()
timeout = start + 30000
while echo.value() == 1:
if time.ticks_us() > timeout:
return -1
duration = time.ticks_diff(time.ticks_us(), start)
return round(duration / 58.2, 1)
while True:
dist = get_distance()
if dist != -1:
print(f"Distance: {dist} cm")
time.sleep(0.5)
Did you know? This sensor uses the same principle as bat echolocation — emit a sound, measure how long it takes to bounce back.
5. 9G Micro Servo Motor
Wiring:
| Servo Wire Colour | Connect To |
|---|---|
| Red | 5V |
| Brown or Black | GND |
| Orange or Yellow (signal) | GPIO 13 |
Servos can draw significant current when moving under load. If your ESP32 resets when the servo moves, power the servo from a separate 5V supply (keep GND connected to the ESP32’s GND).
How PWM works for servos: Send a 50Hz signal. The pulse width (time the signal is HIGH) sets the angle:
- 0.5ms pulse ≈ 0°
- 1.5ms pulse ≈ 90° (centre)
- 2.5ms pulse ≈ 180°
Code:
from machine import Pin, PWM
import time
servo = PWM(Pin(13), freq=50)
def set_angle(angle):
"""Move servo to angle (0–180 degrees)."""
# Map 0–180° to duty cycle 40–115 (0–1023 range, 50Hz)
# Adjust these values to calibrate your specific servo
min_duty = 40
max_duty = 115
duty = int(min_duty + (angle / 180) * (max_duty - min_duty))
servo.duty(duty)
# Move through positions
for angle in [0, 45, 90, 135, 180]:
set_angle(angle)
print(f"Angle: {angle}°")
time.sleep(1)
servo.deinit() # Release PWM when done
Calibrating your servo: If 0° and 180° aren’t quite right, adjust min_duty and max_duty. Try values between 30–50 for min and 110–130 for max.
6. Relay Module (3.3V)
Wiring — control side (ESP32 to relay):
| Relay Pin | Connect To |
|---|---|
| VCC | 3.3V |
| GND | GND |
| IN | GPIO 15 |
Wiring — load side (motor circuit, separate power):
| Relay Terminal | Connection |
|---|---|
| COM | One motor terminal |
| NO (Normally Open) | 5V positive supply |
| Motor other terminal | 5V GND (same GND as ESP32) |
Separate power circuit: The relay switches a separate 5V circuit for the motor. The motor should NOT be powered from the ESP32’s 3.3V pin — motors draw too much current and will damage the board.
Logic:
relay.on()→ relay energised → COM connects to NO → motor runsrelay.off()→ relay de-energised → COM disconnects from NO → motor stops
Code:
from machine import Pin
import time
relay = Pin(15, Pin.OUT)
relay.off() # Start with motor off
relay.on() # Motor ON
time.sleep(3)
relay.off() # Motor OFF
7. WS2812B 8×8 NeoPixel Grid
Wiring:
| Grid Terminal | Connect To |
|---|---|
| 5V or VCC | 5V (external supply recommended for full brightness) |
| GND | GND (same GND as ESP32) |
| DIN (Data In) | GPIO 6 |
Power note: 64 LEDs at full white draw ~3.8A. For testing, keep brightness low (max ~50 per channel). For full brightness, use an external 5V power supply rated at 4A or more. Always connect GND of the external supply to the ESP32’s GND.
Pixel numbering: Pixels are numbered 0–63, left-to-right, top-to-bottom:
0 1 2 3 4 5 6 7 ← Row 0 (top)
8 9 10 11 12 13 14 15 ← Row 1
16 17 18 19 20 21 22 23 ← Row 2
...
56 57 58 59 60 61 62 63 ← Row 7 (bottom)
Formula: index = row * 8 + col
Code:
import machine
import neopixel
pin = machine.Pin(6, machine.Pin.OUT)
np = neopixel.NeoPixel(pin, 64)
def clear():
for i in range(64):
np[i] = (0, 0, 0)
np.write()
def set_pixel(row, col, r, g, b):
np[row * 8 + col] = (r, g, b)
def render():
np.write()
# Example: light up top-left corner
clear()
set_pixel(0, 0, 255, 0, 0) # Red at row 0, col 0
render()
Framebuffer approach (recommended for animations):
def make_framebuffer():
return [[(0, 0, 0)] * 8 for _ in range(8)]
def render_fb(fb):
for row in range(8):
for col in range(8):
np[row * 8 + col] = fb[row][col]
np.write()
# Update framebuffer, then render once
fb = make_framebuffer()
fb[3][4] = (0, 255, 0) # Green pixel at row 3, col 4
render_fb(fb)
Did you know? Each WS2812B LED in your grid contains its own tiny control chip. The data signal is passed from LED to LED in a chain — that’s why you only need one data wire for all 64 LEDs.
Quick-Reference: Pin Assignments
This course uses these default pin assignments. You can change them, but make sure to update your code:
| Component | Pin(s) |
|---|---|
| Built-in NeoPixel | GPIO 48 (fixed) |
| Button A (left) | GPIO 0 |
| Button B (right) | GPIO 14 |
| IR sensor | GPIO 12 |
| HC-SR04 TRIG | GPIO 5 |
| HC-SR04 ECHO | GPIO 4 |
| Servo | GPIO 13 |
| Relay | GPIO 15 |
| 8×8 NeoPixel grid | GPIO 6 |