Building Spark Analyzer: USB-C Powered Stepper Control
How I combined USB-C Power Delivery with silent stepper motor control for a portable, programmable motion platform

Building Spark Analyzer: USB-C Powered Stepper Control
I got tired of the cable mess.
Every stepper motor project needed its own power supply. 12V for this motor, 24V for that one. Meanwhile, I had a 65W laptop charger on my desk that could power all of them - if only I could convince it to output something other than 5V.
That's how Spark Analyzer started: a way to tap into USB-C Power Delivery for motor control. It turned into something more useful than I expected.
The Problem I Was Solving
Traditional stepper motor setups have friction:
- Power supply juggling: Different motors want different voltages. My desk had three power bricks.
- No portability: Wall adapters aren't great for field testing or demos.
- Limited monitoring: Most setups have no real-time feedback on what's actually happening.
- Noise: Cheap stepper drivers sound like angry robots.
I wanted:
- One USB-C cable for everything
- Voltage flexibility (5-20V)
- Silent operation
- Real-time power monitoring
- Wireless control for convenience
The Hardware Stack
Spark Analyzer integrates four key subsystems:
| Component | Purpose | Why This One |
|---|---|---|
| ESP32-C3 | Control + connectivity | WiFi for web interface, low cost |
| FUSB302MPX | USB-C PD negotiation | Industry standard, I2C interface |
| TMC2209 | Stepper driver | Silent operation, UART control |
| INA228 | Power monitoring | 20-bit ADC, high accuracy |
ESP32-C3: The Right Level of Overkill
For this project, I needed:
- WiFi for the web interface
- I2C for sensors (INA228, FUSB302)
- UART for stepper driver (TMC2209)
- PWM for step generation
- Enough GPIO for buttons and LEDs
The ESP32-C3 handles all of this easily, costs about $2, and I was already familiar with the platform from other projects. No need to overthink it.
FUSB302: Making USB-C PD Accessible
The FUSB302MPX is the de facto standard for PD implementation. It handles:
- CC line monitoring
- PD message encoding/decoding
- BMC physical layer
You talk to it over I2C and it handles the gnarly timing requirements internally. I covered the details in my USB-C PD tutorial.
TMC2209: The Quiet Stepper Driver
I've used plenty of A4988 and DRV8825 drivers. They work fine, but they're loud. The distinctive stepper motor whine gets old fast.
The TMC2209 uses Trinamic's StealthChop technology for silent operation. Not "quieter" - actually silent. You hear the motor mechanism, not the electronics.
Other features I actually use:
- StallGuard4: Detects motor stalls without external sensors
- UART interface: Configure everything in software
- Up to 256 microstepping: Smooth motion
- 2A peak current: Handles most NEMA17 motors
INA228: Knowing What's Actually Happening
The INA228 is Texas Instruments' high-precision power monitor. It gives me:
- Voltage measurement (±0.5% accuracy)
- Current measurement (via shunt resistor)
- Power calculation
- Energy accumulation
This isn't just nice-to-have. When you're negotiating USB-C PD power, you need to know if you're staying within your contract. Drawing more current than negotiated can trigger protection shutdowns.
The Power Architecture
Power flows through several stages:
USB-C PD Input (5-20V, up to 60W)
│
▼
INA228 (current sensing via 10mΩ shunt)
│
▼
TMC2209 Motor Power (direct)
│
▼
3.3V Buck Regulator (for ESP32-C3 and logic)
Key design decisions:
No intermediate voltage conversion for motors: The TMC2209 accepts 4.75-28V directly. Whatever voltage I negotiate from USB-C PD goes straight to the motor. This maximizes efficiency and simplifies the design.
Shunt resistor placement: The INA228's shunt is in the high-side path (between USB-C VBUS and the rest of the circuit). This lets me measure total system current, not just motor current.
Separate logic supply: A small buck regulator creates 3.3V for the ESP32-C3 and logic circuits. This isolates the sensitive control electronics from motor noise.
PCB Design Challenges
Motor Driver Isolation
Stepper motors are electrically noisy. PWM chopping creates high-frequency switching that can couple into sensitive circuits.
My approach:
- Separate ground pours: Motor power and logic have distinct ground regions, connected at a single star point near the USB-C input.
- Decoupling everywhere: 100µF bulk capacitor near TMC2209 VM input, plus 100nF ceramic for high-frequency noise.
- Distance: Motor power traces route away from I2C and UART signals.
USB-C Signal Integrity
USB-C PD communication happens at 600kHz over the CC lines. Not crazy fast, but worth respecting:
- Short traces from connector to FUSB302
- Controlled impedance (50Ω isn't critical for CC, but doesn't hurt)
- Ground reference nearby
Thermal Management
At 20V and 2A, the TMC2209 dissipates significant heat. The QFN package has a thermal pad underneath.
My thermal solution:
- Large copper pour connected to thermal pad
- Thermal vias to internal ground plane
- No components directly adjacent (convective clearance)
In testing, the driver reaches about 65°C at maximum load in still air. Well within spec.
Firmware Architecture
The firmware handles several concurrent tasks:
┌─────────────────────────────────────┐
│ Web Server │
│ (HTTP + WebSocket for real-time) │
└───────────────┬─────────────────────┘
│
┌───────────────┼─────────────────────┐
│ │ │
│ ┌────────────▼────────────┐ │
│ │ Motion Controller │ │
│ │ (step generation, accel) │ │
│ └────────────┬────────────┘ │
│ │ │
│ ┌────────────▼────────────┐ │
│ │ TMC2209 Driver │ │
│ │ (UART config, StallGuard) │ │
│ └──────────────────────────┘ │
│ │
│ ┌──────────────────────────┐ │
│ │ Power Management │ │
│ │ (PD negotiation, INA228) │ │
│ └──────────────────────────┘ │
└──────────────────────────────────────┘
Motion Control
Step generation uses a timer interrupt for consistent timing:
hw_timer_t* stepTimer = NULL;
volatile uint32_t stepCount = 0;
volatile uint32_t targetSteps = 0;
void IRAM_ATTR onStepTimer() {
if (stepCount < targetSteps) {
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds(2);
digitalWrite(STEP_PIN, LOW);
stepCount++;
}
}
void moveSteps(uint32_t steps, uint32_t stepDelay_us) {
stepCount = 0;
targetSteps = steps;
timerAlarmWrite(stepTimer, stepDelay_us, true);
timerAlarmEnable(stepTimer);
// Wait for motion to complete
while (stepCount < targetSteps) {
vTaskDelay(1);
}
timerAlarmDisable(stepTimer);
}For smoother motion, I implemented trapezoidal acceleration:
void moveWithAcceleration(uint32_t steps, uint32_t maxSpeed, uint32_t accel) {
// Calculate acceleration profile
uint32_t accelSteps = (maxSpeed * maxSpeed) / (2 * accel);
if (accelSteps > steps / 2) {
// Triangle profile (never reach max speed)
accelSteps = steps / 2;
}
uint32_t coastSteps = steps - (2 * accelSteps);
// Accelerate
for (uint32_t i = 0; i < accelSteps; i++) {
uint32_t speed = sqrt(2 * accel * i);
stepWithDelay(1000000 / max(speed, 100));
}
// Coast
for (uint32_t i = 0; i < coastSteps; i++) {
stepWithDelay(1000000 / maxSpeed);
}
// Decelerate
for (uint32_t i = accelSteps; i > 0; i--) {
uint32_t speed = sqrt(2 * accel * i);
stepWithDelay(1000000 / max(speed, 100));
}
}TMC2209 UART Configuration
The TMC2209 uses a single-wire UART interface. Trinamic's protocol requires careful timing:
void tmc2209_write_register(uint8_t reg, uint32_t value) {
uint8_t buffer[8];
// Sync byte
buffer[0] = 0x05;
// Slave address (0 for single driver)
buffer[1] = 0x00;
// Register + write flag
buffer[2] = reg | 0x80;
// Data (MSB first)
buffer[3] = (value >> 24) & 0xFF;
buffer[4] = (value >> 16) & 0xFF;
buffer[5] = (value >> 8) & 0xFF;
buffer[6] = value & 0xFF;
// CRC
buffer[7] = calculate_crc(buffer, 7);
tmc_serial.write(buffer, 8);
}
void tmc2209_init() {
// Global configuration: enable StealthChop
tmc2209_write_register(0x00, 0x00000041);
// Set motor current (example: 800mA RMS)
tmc2209_write_register(0x10, calculate_irun_ihold(800));
// Enable StallGuard
tmc2209_write_register(0x14, 0x00000000); // TCOOLTHRS
tmc2209_write_register(0x40, 0x00000000); // SGTHRS
}Web Interface
The web interface uses WebSocket for real-time updates:
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
void setupWebServer() {
// Serve static files from SPIFFS
server.serveStatic("/", SPIFFS, "/www/").setDefaultFile("index.html");
// WebSocket for real-time data
ws.onEvent(onWebSocketEvent);
server.addHandler(&ws);
// REST API for commands
server.on("/api/move", HTTP_POST, handleMoveCommand);
server.on("/api/voltage", HTTP_POST, handleVoltageCommand);
server.on("/api/status", HTTP_GET, handleStatusRequest);
server.begin();
}
void broadcastStatus() {
StaticJsonDocument<256> doc;
doc["voltage"] = ina228_read_voltage();
doc["current"] = ina228_read_current();
doc["power"] = ina228_read_power();
doc["position"] = currentPosition;
doc["pdVoltage"] = negotiatedVoltage;
String json;
serializeJson(doc, json);
ws.textAll(json);
}What I Learned
Things That Worked Well
USB-C PD integration: Simpler than I feared. The FUSB302 handles the hard parts. Having 5-20V on demand from any laptop charger is genuinely useful.
TMC2209 StealthChop: Night and day compared to older drivers. Motors are actually silent. Worth the extra cost.
Web-based control: No desktop software to install. Works from any device with a browser. Made demos much easier.
Challenges I Faced
PD timing on first power-up: The ESP32-C3 boots faster than expected. Initial attempts sometimes failed because the charger wasn't ready. Added a small delay before PD initialization.
Thermal throttling at high currents: Extended 2A operation in an enclosure caused thermal slowdowns. Added a small temperature margin in software - if approaching 80°C, reduce current limit automatically.
WiFi and motor noise: Initial prototype had WiFi dropouts during motor motion. Solved by better filtering on motor power and software retry logic.
Future Improvements
- Encoder feedback: Add quadrature encoder input for closed-loop control
- Motion planning: G-code interpreter for complex motion sequences
- Multiple axes: Expand to control 2-4 motors simultaneously
- Battery backup: Small LiPo to maintain position during power transitions
Open Source Release
Everything is on GitHub: github.com/tooyipjee/sparkanalyzer2
Included:
- KiCad schematic and PCB files
- BOM with supplier links
- ESP-IDF firmware
- Web interface source
- Assembly guide
Get Your Own
Spark Analyzer is available in my store for $49.99:
- Assembled and tested board
- USB-C cable included
- Documentation and support
Or build it yourself from the open-source files. Either way, no more cable graveyards.
Questions about USB-C PD, stepper control, or the design? Reach out via the contact form.