6.1 Safety Relay Circuit Design
As an added safety measure, our team designed and fabricated a small custom PCB dedicated to separate the logic and power supply, which is essential when controlling high-power components, like motors, using a microcontroller, such as the Xiao RP2040. This separation ensures that the microcontroller is powered on first before the motors, preventing damage from excessive power draw. This board integrates an optocoupler, a transistor, and a relay, acting as an electrically-isolated switch between the microcontroller and high-power components.

Optocoupler (Octocoupler)
An optocoupler is used to isolate the low-power logic side from the high-power motor side. In this configuration, the optocoupler's LED is activated by a signal from the microcontroller. The LED's light triggers the phototransistor on the output side, allowing the signal to pass while maintaining electrical isolation between the two sections.
Mode of Operation:
The optocoupler operates in phototransistor mode. When the LED inside the optocoupler is turned on, it closes the circuit by activating the phototransistor. This allows the control signal to pass through while isolating the high-power components from the microcontroller.
Optocoupler Connections
- Input Side (Pins 1-2): The LED inside the optocoupler
- Pin 1 (Anode) is connected to
GPIO_ON/OFF1
through a 220Ω resistor (R2) - Pin 2 (Cathode) is connected to
PWR_GND
- Output Side (Pins 3-4): The phaototransistor
- Pin 4 (Collector) is connected to
PWR_5V
- Pin 3 (Emitter) is connected to the base of transistor
Q2
through a 1kΩ resistor (R4)
2N2222 NPN Transistor
The 2N2222 NPN transistor acts as a switch to control the relay coil. When the optocoupler’s phototransistor is activated, it provides current to the base of the 2N2222, allowing it to conduct and energize the relay coil.
Mode of Operation:
The transistor is in saturation mode. When the base-emitter junction receives sufficient current from the optocoupler, the transistor allows current to flow from the collector to the emitter, thus activating the relay. The transistor essentially switches the high-power relay without being directly connected to the microcontroller.
Base Resistor (R4):
The resistor limits the current to the base, preventing excessive current flow that could damage the transistor.
Relay (K1)
A relay is used to switch the high-power components, such as motors, in the CNC. When the relay is activated, it closes the contacts, allowing power to flow to the motors.
Relay Connections
- Coil (Pins 1-2):
- Pin 1 is connected to
GND_relay
- Pin 2 is connected to a diode (
D101
) and to the collector of transistorQ2
- Switching Contacts (Pins 3-4-5):
- Pin 3 is connected to
Vmot
(motor voltage) - Pin 4 is connected to
PWR_12V
- Pin 5 is connected to
PWR_5V
through a resistor (R6)
Why a Relay?:
Relays handle higher current than the microcontroller, allowing the microcontroller to control the motor's power without directly supplying the high current, which it can't provide.
Flyback Diode (D1)
The flyback diode is placed in parallel with the relay coil. When the relay is de-energized, the inductive load generates a voltage spike. The diode provides a path for the current to dissipate safely, protecting the transistor from potential damage caused by the back electromotive force (EMF).
Purpose:
The flyback diode absorbs the voltage spike when the relay is turned off, preventing damage to the transistor and other components.
Circuit Operation Summary
When GPIO_ON/OFF1
outputs a HIGH signal, it turns on the LED inside the optocoupler. The phototransistor inside then conducts, sending current to the base of the NPN transistor Q2
. This activates Q2
, which energizes the relay’s coil, switching the relay’s contacts from the default (normally open or closed) to the active position. This mechanism allows safe and controlled power delivery to the stepper motor drivers.
Other additions to the board
Additionally, the board is equipped with an AMS1117 5V voltage regulator. This was necessary because the relay available to us could only be powered with 5V. Therefore, we needed to step down the 12V input from the power supply to 5V to ensure proper operation of the circuit.

The board also includes pull-up resistors for the CNC endstops. Initially, we intended to use endstop modules; however, we were unable to obtain them in the required format and instead acquired individual switches. With the pull-up resistors in place, the Xiao RP2040 is able to interpret signals from the endstops effectively.

Here, you can find both the complete schematic and the PCB design with their respective connections.


For manufacturing, we used the Monofab. The PCB design involved four layers: one for marking true holes for the pins, transistor, and relay; another for etching the traces; a third for engraving; and the final layer for cutting the board.
The components were manually soldered using a soldering iron and tin.

#include <AccelStepper.h> // Library for controlling stepper motors #include <Servo.h> // Library for controlling servo motors // Attribute for interrupt service routine on ESP32 #if defined(ESP32) #define ISR_ATTR IRAM_ATTR #else #define ISR_ATTR #endif // ==== Pin definitions ==== #define X_STEP_PIN D6 // Step pin for X-axis stepper #define X_DIR_PIN D7 // Direction pin for X-axis stepper #define Y_STEP_PIN D8 // Step pin for Y-axis stepper #define Y_DIR_PIN D9 // Direction pin for Y-axis stepper #define SERVO_PIN D10 // PWM pin for servo motor (Z-axis) #define ENABLE_PIN D0 // Motor driver enable pin #define ENDSTOP_X_PIN D2 // Limit switch pin for X-axis #define ENDSTOP_Y_PIN D3 // Limit switch pin for Y-axis // Create stepper motor instances AccelStepper stepperX(AccelStepper::DRIVER, X_STEP_PIN, X_DIR_PIN); AccelStepper stepperY(AccelStepper::DRIVER, Y_STEP_PIN, Y_DIR_PIN); Servo zServo; // Create servo motor object for Z-axis // ==== Position tracking variables ==== long currentX = 0; // Current X-axis position long currentY = 0; // Current Y-axis position // ==== Command buffer ==== const int BUFFER_SIZE = 20; // Max number of buffered positions long bufferX[BUFFER_SIZE]; // Buffer to store X positions long bufferY[BUFFER_SIZE]; // Buffer to store Y positions int bufferCount = 0; // Number of items in the buffer // ==== Movement parameters ==== const float speed = 350; // Base movement speed (steps/sec) float homingSpeed = 100; // Speed for homing sequence int homingDirX = 1; // Homing direction for X-axis int homingDirY = 1; // Homing direction for Y-axis int retractSteps = 1750; // Steps to retract after hitting limit switch // ==== State flags ==== bool isExecuting = false; bool waitingServo = false; bool servoUp = true; // True if the servo is in the 'up' position bool motorsEnabled = false; // True if motors are energized bool homingDone = false; // True if homing has been completed String serialLine = ""; // Buffer to store incoming serial data // ==== Limit switch flags ==== volatile bool endstopX_triggered = false; volatile bool endstopY_triggered = false; // ==== Interrupt Service Routines (ISRs) for limit switches ==== void ISR_ATTR endstopX_ISR() { endstopX_triggered = true; // Mark that X limit switch was triggered } void ISR_ATTR endstopY_ISR() { endstopY_triggered = true; // Mark that Y limit switch was triggered } // ==== Enable or disable stepper motors ==== void enableMotors(bool state) { digitalWrite(ENABLE_PIN, state ? HIGH : LOW); // HIGH to enable, LOW to disable }