What you will learn
- Distinguish relay module power and control inputs from its contacts.
- Connect an NPN transistor between the GPIO command and active-LOW module input.
- Explain why GPIO HIGH energises the relay in this circuit.
- Switch only a 3.3 V LED demonstration circuit through COM and NO.
- Check single-press processing, a held button and startup off behaviour.
Before you start
Courses 03 and 13: a button toggle, debouncing, an LED with its resistor and a transistor output stage.
Reference setup
- Board in Arduino IDE
- ESP32 Dev Module
- Arduino-ESP32
- 3.3.12
- Serial Monitor
- 115200 baud
This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.
What we will build
One button press turns a relay on; the next turns it off. Its contacts switch a 3.3 V LED circuit, showing the difference between a command and an actually closed circuit.
We use channel 1 of a two-channel module. A transistor interfaces the ESP32 command to its 5 V input, extending familiar LED control to an electromechanical switch.
Learning objectives
- Distinguish a relay module's control input from its contacts.
- Explain COM, NO and NC.
- Interface a logic command through an NPN transistor.
- Process one event per button press.
- Check the startup off state and actual contact operation.
Prerequisites
Complete courses 03 and 13 on button toggling and transistor outputs. Recognise LED polarity, common GND and the difference between 3.3 V logic and 5 V power.
Equipment
| Component | Quantity | Specification and notes |
|---|---|---|
| Development board with a classic ESP32-WROOM-32 module | 1 | Match the reference GPIO labels; check physical header positions on the actual board. |
| USB data and power cable | 1 | Use the connector fitted to your board; the pictured kit lists Micro-USB. |
| Solderless breadboard | 1 | The kit lists 830 tie points. Check whether the power rails are split. |
| 5 V two-channel relay module — active LOW | 1 | The reference module has input pull-ups, onboard coil drivers and coil diodes. Verify these properties; only the 5 V/two-channel category is established by the image. One channel is used. |
| Additional PN2222A NPN transistor | 1Required extra — not in kit photo | ADDITIONAL, not listed in the kit. Identify B/C/E for the exact manufacturer; PN2222A and P2N2222A may have different lead orders. ADDITIONAL: the transistor is not listed in the kit photograph. |
| 1 kΩ resistor | 1 | Base resistor; course 14 also uses one across the piezo. The kit quantity at this resistance is unconfirmed. |
| 10 kΩ resistor | 1 | R2 pulls Q1 base down to GND, keeping the base low while the GPIO is not driving. |
| Momentary pushbutton | 1 | Use two terminals connected only when pressed; two legs on the same side may already be joined. |
| Red LED | 1 | Anode A and cathode K; identify polarity on the actual part. |
| 220 Ω resistor | 1 | One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors. |
| Jumper wires | 13 | Use male-to-male or female-to-male leads to suit the board headers. Approximate quantity; depends on the physical layout. |
The PN2222A transistor is additional hardware not shown in the photographed kit. Verify its B/C/E lead order. The reference relay module already provides coil protection; do not add a diode across its input.
The photograph cannot establish the module circuit. Our reference requires a 5 V supply, active-LOW inputs, onboard coil drivers and protection, and an input held HIGH when our transistor is off. Confirm these in your module documentation. Similar appearance does not establish compatibility.
Control circuit and contacts
VCC and GND power the module electronics and coils. IN1 receives the command for channel 1. COM1 is the common contact; NO1 is open when the relay is de-energised, while NC1 is connected to COM1. Energising the relay transfers COM1 to NO1. “Normally open” and “normally closed” describe the de-energised condition.
A contact does not generate voltage. Our LED circuit therefore has its own 3.3 V connection, resistor and return to GND. The contacts switch only this LED demonstration circuit; this package connects no mains voltage, heater, pump or other load.
Why we use a transistor
The reference module activates its relay when IN1 becomes LOW. Directly connecting an unknown 5 V input to a GPIO is unsuitable. NPN transistor Q1 lets the GPIO drive a resistor-fed base while the collector pulls IN1 towards GND. When Q1 switches off, the module's input pull-up restores a high level.
| GPIO25 | Q1 | IN1 | Relay and contact LED |
|---|---|---|---|
LOW | Off. | HIGH. | Off; COM1–NO1 open. |
HIGH | On. | LOW. | On; COM1–NO1 closed. |
These two inversions give the program a simple rule: GPIO25 HIGH means relay on. Q1 switches a module input here, not a bare coil. The module's onboard output stage and diode provide coil driving and protection.
Wiring
Wiring diagram
The reference 5 V module is active LOW, with onboard coil drivers, protective diodes and input pull-ups. Additional Q1 controls IN1, not a bare coil. IN2 is HIGH, keeping channel 2 OFF. COM1/NO1 switch only the 3.3 V LED circuit; NC1 is unconnected.
| From | To | Connection |
|---|---|---|
ESP32.GPIO25 | R1.1 | R1, 1 kΩ, limits base current. |
R1.2 | Q1.B | Base of the additional PN2222A transistor. |
Q1.B | R2.1 | R2, 10 kΩ, pulls the base low while the GPIO is not driving. |
R2.2 | ESP32.GND | Other end of the pull-down resistor. |
Q1.E | ESP32.GND | Emitter to common ground; verify actual B/C/E pin order. |
Q1.C | K1.IN1 | Collector pulls IN1 LOW when GPIO25 is HIGH. |
ESP32.USB5V | K1.VCC | Reference module 5 V supply; retain the documented JD-VCC jumper if present. |
ESP32.GND | K1.GND | Common ground for module and input transistor. |
ESP32.USB5V | K1.IN2 | Hold the unused active-LOW channel input HIGH, meaning OFF. |
ESP32.3V3 | K1.COM1 | Common contact feeds only the 3.3 V LED circuit. |
K1.NO1 | R3.1 | Normally open contact to R3, 220 Ω. |
R3.2 | D1.A | Series resistor to the LED anode. |
D1.K | ESP32.GND | Indicator LED cathode to ground; leave NC1 unconnected. |
ESP32.GPIO27 | SW1.CONTACT_A | Button input configured with INPUT_PULLUP. |
SW1.CONTACT_B | ESP32.GND | Other terminal of the selected contact pair, connected when pressed. |
- Disconnect USB. Confirm module labels and specifications, transistor lead order and adequate current capacity of the USB source and board.
- Connect module GND, ESP32 GND and Q1 emitter to common GND.
- Connect GPIO25 through R1, 1 kΩ, to Q1 base. Add R2, 10 kΩ, from base to GND.
- Connect Q1 collector to IN1. Do not connect IN1 directly to GPIO25.
- Connect module VCC to the ESP32 board's documented USB-derived 5 V connection. An unknown terminal's VIN label alone does not confirm its function.
- Connect unused IN2 to the same 5 V rail to hold channel 2 of the reference active-LOW module off. If JD-VCC exists, use its documented VCC/JD-VCC arrangement with the same 5 V source.
- Connect 3V3 to COM1. Connect NO1 through R3, 220 Ω, to LED D1 anode, and its cathode to GND. Leave NC1 and channel 2 contacts unconnected.
- Connect GPIO27 through the button to GND. Check the button's actual switched contact pair, inspect the whole circuit and then connect USB.
Use USB power only. Do not parallel an independent 5 V supply with it. Do not place a protection diode across IN1 as you did across the buzzer in course 13: IN1 is not a coil terminal.
Arduino setup
In Arduino IDE 2.x use esp32 by Espressif Systems 3.3.12, ESP32 Dev Module and the correct port. No extra libraries are required. Open relay_switch/relay_switch.ino. Check RELAY_DRIVER_PIN is GPIO25 and BUTTON_PIN is GPIO27. Set Serial Monitor to 115200 baud.
Complete Arduino program
#include <Arduino.h>
constexpr uint8_t RELAY_DRIVER_PIN = 25;
constexpr uint8_t BUTTON_PIN = 27;
constexpr uint32_t DEBOUNCE_MS = 30;
// Each event is accepted once after 30 ms of stable input.
// If held during reset, a stable release is required before the first press.
struct DebouncedButton {
bool rawHigh = true;
bool stableHigh = true;
bool armed = true;
bool pressed = false;
bool released = false;
uint32_t rawChangedAt = 0;
uint32_t pressStartedAt = 0;
void begin(uint32_t now) {
rawHigh = stableHigh = digitalRead(BUTTON_PIN) == HIGH;
armed = rawHigh;
rawChangedAt = now;
pressed = released = false;
}
void update(uint32_t now) {
pressed = released = false;
const bool readingHigh = digitalRead(BUTTON_PIN) == HIGH;
if (readingHigh != rawHigh) {
rawHigh = readingHigh;
rawChangedAt = now;
}
if (rawHigh != stableHigh && static_cast<uint32_t>(now - rawChangedAt) >= DEBOUNCE_MS) {
stableHigh = rawHigh;
if (stableHigh) {
armed = true;
released = true;
} else if (armed) {
pressed = true;
pressStartedAt = rawChangedAt;
}
}
}
};
DebouncedButton button;
bool relayOn = false;
void setRelay(bool on) {
relayOn = on;
// HIGH turns the NPN on, pulling the active-LOW module input LOW.
digitalWrite(RELAY_DRIVER_PIN, relayOn ? HIGH : LOW);
Serial.print("Relay="); Serial.println(relayOn ? "ON" : "OFF");
}
void setup() {
Serial.begin(115200);
pinMode(RELAY_DRIVER_PIN, OUTPUT);
digitalWrite(RELAY_DRIVER_PIN, LOW);
pinMode(BUTTON_PIN, INPUT_PULLUP);
button.begin(millis());
setRelay(false);
}
void loop() {
button.update(millis());
if (button.pressed) setRelay(!relayOn);
}
Begin without changing pins or polarity. The stated rule applies to the complete defined GPIO–Q1–IN1–relay chain.
Understanding the program
The logical variable relayOn stores whether the relay should be energised. setRelay() maps this value to the GPIO output. Startup sets both the output and stored state to off; an earlier on state is not preserved through a reset.
The button uses INPUT_PULLUP: released means HIGH, pressed means LOW. The program accepts a new level only after it remains unchanged for at least 30 ms. A newly accepted press toggles the state once. Holding the button does not generate repeated commands.
If the button is already pressed during reset, release it before pressing again. Its initial position therefore does not energise the relay. R2 also holds the transistor off while the GPIO has not yet been configured; a suitable module with an input pull-up is part of this behaviour.
Running and checking the contact
Click Verify, then Upload. The contact LED must be off after reset. Press the button and check the program report, any audible relay operation and the LED powered through COM1–NO1 together.
A module indicator may show the command without proving correct contact wiring. The separate contact LED checks the whole path. Space presses to observe each state clearly.
Experiment and observations
| Procedure | Expected result |
|---|---|
| Reset with the button released. | Relay and contact LED off. |
| Press and release once. | Relay and contact LED on. |
| Hold a press for three seconds. | One change; no repeated toggles while held. |
| Press again after releasing. | The state changes again. |
| Reset while the button is held. | It remains off; release and a new press are required to turn it on. |
Record the results on your own board. This table states expected behaviour rather than reporting a measurement already performed.
Troubleshooting
| Symptom | Check and action |
|---|---|
| The program reports ON but the LED is dark. | Check module supply, actual COM1/NO1 contact wiring, resistor and LED polarity. |
| The LED lights while the relay is off. | It may be connected to NC1; disconnect USB and check the labels. |
| The relay does not energise. | Check active-LOW configuration, common GND, Q1 B/C/E and IN1 wiring. |
| Channel 2 activates unexpectedly. | Check IN2 is tied to 5 V and that the module meets the reference specification. |
| The ESP32 resets during switching. | Disconnect power and check wiring, total load current and USB source capability. |
Challenge and worked solution
Change the behaviour so that the relay operates only while the button is held. Retain debouncing, startup off and the requirement to release a button held during reset.
Keep button.update(millis()) in the loop and replace its toggle condition with this small block:
const bool requestedOn = button.armed && !button.stableHigh;
if (requestedOn != relayOn) setRelay(requestedOn);
A stable LOW means on only after button.armed enables control following release. Stable HIGH means off. The comparison prevents repeating an unchanged command and its report. Raw digitalRead() would bypass both filtering and the startup rule.
Why use NO1? We want the LED circuit open when the coil is de-energised. Does a 5 V IN1 mean the contacts must also switch 5 V? No: the module command and LED circuit supply serve different roles. What does reset change? The program sets the relay to OFF again, without preserving its previous state.
Downloads
Primary sources
- SunFounder: an example 5 V two-channel module with active-LOW inputs.
- SunFounder: relay operation and NO/NC contacts.
- onsemi: PN2222A lead identification and electrical characteristics.
- Espressif: ESP32-DevKitC power sources.
- Espressif: digital inputs, outputs and pull-ups.
- Arduino: button debouncing.
Confirm your relay module's electrical characteristics and terminal labels from its own technical documentation; the kit photograph does not replace that check.
