One PCB, Many Uses: The Power of Solder Bridges
By adding or removing a few key components and shorting specific solder jumpers, the same board can function in six different modes:
(💡 Short = connect with solder)
1️⃣ Standard Relay Mode (The Original Design)
✔ Install: J1 (screw terminal) and K1 (relay)
❌ Remove: J2 (fan header) and R3 (10K pull-up resistor)
⚙ Behavior: The board switches a relay via USB commands.
2️⃣ Solid-State Relay (SSR) Mode
✔ Short: B6
✔ Install: J1 (screw terminal, now outputting 5V for SSR)
❌ Remove: J2 and R3
⚙ Behavior: Similar to option 1, but the relay provides a switched 5V between Pin 2 and GND.
3️⃣ Simple Fan Control (2- or 3-Pin Fan)
✔ Install: J2 (fan header)
✔ Short: B4
❌ Remove: J1, K1, and R3
⚙ Behavior: The board powers a 2- or 3-pin fan with PWM speed control applied to the power pin.
4️⃣ PWM Fan Control (4-Pin Fan)
✔ Short: B5
✔ Install: J2 (fan header) and R3 (10K resistor in relay coil holes)
❌ Remove: J1 and K1
⚙ Behavior: Speed control by PWM on Pin 4 (PWM input).
5️⃣ Servo Motor Control
✔ Short: B5 & B1
✔ Install: J2 (wired as a servo header)
❌ Remove: J1, K1, and R3
⚙ Behavior: TIM2 generates servo control PWM (50Hz, 1-2ms pulse width).
6️⃣ UART Communication Interface
✔ Short: B2 & B3
✔ Install: J1 (now a UART terminal block)
❌ Remove: J2 & K1
⚙ Behavior: J1 now provides TX/RX pins for serial communication.
Ditching the Crystal: How USB Syncs Itself
Most USB-enabled microcontrollers require a crystal (typically 12MHz) for accurate timing.
But here’s the cool part: The STM32F042K6U6 doesn’t need one. It can self-synchronize using its internal RC oscillator.
How USB Self-Synchronization Works
🔹 USB Full-Speed mode (12Mbps): The host sends a Start Of Frame (SOF) pulse every 1ms.
🔹 The STM32 uses these pulses to fine-tune its internal oscillator.
🔹 This eliminates the need for an external crystal, reducing component count without losing accuracy.
That means fewer parts, lower cost, and easier assembly.
The Magic Button: One Switch, Two Tricks
One of my favorite design choices was a dual-purpose tactile switch connected to the BOOT0 pin.
Feature 1: Enter Bootloader Mode (Firmware Update)
Hold the button while plugging in USB → The STM32 enters bootloader mode.
Now you can flash firmware over USB, no programmer needed!
Feature 2: Manual Override (Toggle Outputs)
When the STM32 is already running firmware, pressing the button toggles the relay or fan manually—without USB commands.
💡 BOOT0 is repurposed as a general-purpose input once firmware is running.
USB Command Interface
The firmware uses USB CDC (Virtual COM Port) to communicate via serial commands; accepts structured ASCII commands (easy to use in Bash, Shell and Serial Terminal).
Example Commands (Send via Terminal/Script)
Command | Function |
---|---|
#S1!
| Switch ON relay |
#P3!
| Pulse relay for 300ms |
#F90!
| Fan at 90% PWM |
#W40!
| Load controlled with a PWM signal at 40% (B4 or B5 Closed) |
#A75!
| Servo motor positioned at 0° |
#Z15000!
| Set the PWM frequency at 15 KHz |
#I!
| Transmit on the USB the input pin state |
#T!
| Transmit on the USB the fan RPM |
#O!
| Transmit on the USB the PWM percentage of the output |
How to use on Linux Systems in Bash
// To send a command
echo '#S1!' > /dev/serial/by-id/usb-IT_Logic_USB_Relay-if00
// To read a result and store in a variable FAN_TACH:
echo '#T!' > /dev/serial/by-id/usb-IT_Logic_USB_Relay-if00
read -d'~' -t1 FAN_TACH < /dev/serial/by-id/usb-IT_Logic_USB_Relay-if00
echo $FAN_TACH
How to use on Mac Os in Bash
// To send a command
echo '#S1!' > /dev/cu.usbmodem8301
// To read a result and store in a variable FAN_TACH:
echo '#T!' > /dev/cu.usbmodem8301
read -d'~' -t1 FAN_TACH < /dev/cu.usbmodem8301
echo $FAN_TACH
Generating PWM for Fans & Servos with TIM2
With fan and servo control becoming a feature, I needed precise PWM signals on the fly.
The solution? The STM32's TIM2 hardware timer.
PWM Outputs
🔹 25 kHz PWM → Controls 4-pin fans (standard frequency for PC cooling fans)
🔹 50 Hz PWM → Controls servo motors (1ms = 0°, 2ms = 180°)
The microcontroller dynamically reconfigures TIM2 depending on the mode.
One Input, Two Uses: Fan RPM & External Sensors
To track fan speed (RPM) and possibly other sensor data, I added a single input pin.
Features
🔹 Default 5V pull-up resistor (10KΩ, R1)
🔹 Schottky diode protection (lower voltage drop = more reliable LOW signals)
💡 If you need a higher voltage pull-up, simply remove R1—the diode still prevents overvoltage damage.
Full Firmware Overview
The software is written in C using the STM32Cube IDE. Most of the code is automatically generated by the IDE through the configuration of the CPU features. I have concentrated all the software in two files: cmd_parser.c
and cmd_parser.h
. Some customizations to the generated USB code are explained in the comments section.
The firmware follows a real-time event-driven model:
🔹 Main Loop: Continuously listens for USB commands & button presses.
🔹 Interrupt Handlers: Detect tachometer pulses for accurate RPM tracking.
🔹 Non-Blocking Execution: Avoids unnecessary delays while handling PWM & I/O control.
Interrupt-Driven RPM Counting (Tachometer Callback)
The RPM measurement system uses a GPIO interrupt to increment a pulse counter each time a tachometer signal from the fan is detected. The callback interrupt function:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
TachIncrement();
}
Whenever a fan tach pulse is received, this function TachIncrement()
is called to increase the counter asynchronously. Later, every one second, the pulse count is used to calculate and update the fan’s RPM. Using an interrupt ensures accurate RPM measurement without requiring CPU-intensive polling.
Non-Blocking Timing & Event Handling
All timing is handled via CheckDelays()
using a millisecond timestamps from HAL_GetTick()
, making execution event-driven and efficient.
Action | Condition | Handling Function |
---|---|---|
Debounce Manual Button | HAL_GetTick() >= debounceStop
| CheckButton() |
Relay Pulse Duration | HAL_GetTick() >= pulseStop
| Auto-Turn OFF |
RPM Calculation | HAL_GetTick() >= rpmNextCalc
| Updates rpmMinute |
Delayed USB Transmission | HAL_GetTick() >= sendTxBufStart
| CDC_Transmit_FS() |
Final Thoughts: Minimal Parts, Maximum Versatility
This small USB relay board turned into a multi-purpose control interface, thanks to:
🔹 Adaptive configuration via solder bridges
🔹 Cost-saving USB synchronization (no crystal!)
🔹 One button, two functions: manual mode + firmware flashing
The best part? It’s dead simple to build and customize.
What Would You Add?
This project opens up many possibilities for hardware hackers.
What mods or improvements would you make? Share your thoughts in the comments!