DriveWire
A 2WD mini EV built end to end: ESP32-S3 firmware proven on the bench, load measured from a 2S LiPo, and a custom power PCB designed in Altium.
Now · PCB sent to fab, bring-up next
Working on hardware
- Forward, reverse, coast and active brake on the printed test stand
- 17.5 kHz motor PWM, chosen by sweeping the carrier
- 100 ms FreeRTOS sensor task that survives a failed read
Measured on the bench
- 1.35 A peak stall current from a 2S LiPo
- 0.44 V of battery sag versus 0.95 V lost in the prototype wiring
- Reliable ToF ranging to about 1.3 m indoors
Designed in Altium
- Protected battery input with current sensing and power muxes
- Two TPS62130A bucks, 6 V for the motors and 3.3 V for logic
- ESP32-S3 controller with native USB and per-rail status lines
The schematic, laid out.
The power path, both bucks and the ESP32‑S3 controller, laid out on a 57.5 × 124.1 mm, 4‑layer board with ground and power inner layers. These renders come straight from the Altium STEP and Gerbers, so every pad, trace and label is the real layout.



From the battery to two regulated rails.
- 01
Protected input
A 5 A fuse and two AOD4185 PMOS stages give reverse-polarity protection and let a low-current slide switch control the main rail.
- 02
Board-level sensing
An INA219BIDR on a 20 mΩ shunt measures the whole board's current, with 20 kΩ I2C pull-ups.
- 03
Motor-supply mux
A TPS2121 feeds the motors from the regulated 6 V rail or straight from the battery for a hardware boost mode, with a failsafe pull-down on its select line.
- 04
Two buck rails
VBAT_BUS feeds the 6 V motor buck. A second mux runs the 3.3 V logic buck from USB's 5 V when it's plugged in, and from the battery when it isn't.
Two buck converters, sized by hand.
Both rails come from the measured load: the 1.35 A stall on the bench became a 2 A design point for the motor rail, and the logic rail is budgeted part by part.
6 V motor rail · TPS62130A
- 01
Inductor from the ripple
From an 8.4 V pack at 1.25 MHz, 25 % ripple at 2 A needs 2.74 µH. TI's 2.2 µH gives 0.78 A peak to peak at its −20 % tolerance, accepted knowing light loads won't stay in continuous conduction.
- 02
Smaller part, loss priced in
The smaller Murata inductor has 84 mΩ against the larger Eaton part's 20 mΩ: 13 mW instead of 3 mW at the typical 400 mA, and 336 mW instead of 80 mW at a 2 A stall.
- 03
Capacitors under DC bias
Two 22 µF input capacitors keep about 8.7 µF at 8.4 V of bias, near the datasheet's 10 µF. A 0.4 to 2 A stall step dips the output about 53 mV per 22 µF; the 47 µF output part was checked for ESR and bias derating.
3.3 V logic rail · TPS62130A
- 01
Budget, then margin
750 mA for the ESP32-S3 module (its Wi-Fi maximum is 355 mA), 50 mA for the VL53L0X, 60 mA for future encoders and 1 mA for the INA219 add to 861 mA, so the rail is designed for 1 A.
- 02
One inductor for both
3.2 µH would hold 25 % ripple at 1 A. Reusing the 6 V buck's 2.2 µH gives 729 mA peak to peak, which the 22 µF output handles easily at 1.25 MHz.
- 03
Feedback divider
130 kΩ over 41.7 kΩ puts 0.801 V on the 0.8 V feedback pin and draws 19.2 µA, well above the 2 µA minimum.
The dev board, rebuilt onto the PCB.
- 01
Clean start, two buttons
A 10 kΩ / 1 µF RC on EN holds the module in reset until the 3.3 V rail settles. RESET and BOOT buttons sit on EN and IO0, with a 10 kΩ pull-up keeping IO0 high for a normal boot.
- 02
Native USB, protected
USB D+ and D− run straight to the S3's own USB pins through 22 Ω series resistors, with a TPD2E001 ESD clamp at the connector. A 6-pin UART header is footprinted as a fallback programming path, not populated at first.
- 03
A board that reports on itself
Both bucks' power-good outputs, both power-mux status lines and the boost select reach GPIOs, so firmware can see every rail. Two thermistor inputs, each a 10 kΩ pull-up with a 100 nF filter, watch the motor temperatures.
Measuring the load before choosing the hardware.
Data table · 147 samples
| Time (s) | Current (mA) | Bus voltage (V) | Phase |
|---|---|---|---|
| 0.0 | 3.4 | 7.79 | Idle |
| 0.5 | 3.6 | 7.79 | Idle |
| 1.0 | 3.1 | 7.79 | Idle |
| 1.5 | 3.5 | 7.79 | Idle |
| 2.0 | 3.5 | 7.79 | Idle |
| 2.5 | 3.4 | 7.79 | Idle |
| 3.0 | 3.5 | 7.79 | Idle |
| 3.5 | 3.2 | 7.79 | Idle |
| 4.0 | 3.1 | 7.79 | Idle |
| 4.5 | 3.4 | 7.79 | Idle |
| 5.0 | 3.3 | 7.79 | Idle |
| 5.5 | 3.7 | 7.79 | Idle |
| 6.0 | 3.3 | 7.79 | Idle |
| 6.5 | 3.3 | 7.79 | Idle |
| 7.0 | 3.3 | 7.79 | Idle |
| 7.5 | 3.3 | 7.79 | Idle |
| 8.0 | 3.5 | 7.79 | Idle |
| 8.5 | 3.3 | 7.79 | Idle |
| 9.0 | 3.3 | 7.79 | Idle |
| 9.5 | 3.6 | 7.79 | Idle |
| 10.0 | 3.4 | 7.79 | Idle |
| 10.5 | 3.3 | 7.79 | Idle |
| 11.0 | 3.3 | 7.79 | Idle |
| 11.5 | 3.1 | 7.79 | Idle |
| 12.0 | 3.4 | 7.79 | Idle |
| 12.5 | 3.5 | 7.79 | Idle |
| 13.0 | 3.3 | 7.79 | Idle |
| 13.5 | 3.4 | 7.79 | Idle |
| 14.0 | 3.4 | 7.79 | Idle |
| 14.5 | 3.4 | 7.79 | Idle |
| 15.0 | 3.3 | 7.79 | Idle |
| 15.5 | 3.1 | 7.79 | Idle |
| 16.0 | 3.5 | 7.79 | Idle |
| 16.5 | 3.3 | 7.79 | Idle |
| 17.0 | 3.4 | 7.79 | Idle |
| 17.5 | 3.1 | 7.79 | Idle |
| 18.0 | 3.3 | 7.79 | Idle |
| 18.5 | 435.3 | 7.34 | Running |
| 19.0 | 322.8 | 7.50 | Running |
| 19.5 | 254.4 | 7.50 | Running |
| 20.0 | 267.4 | 7.51 | Running |
| 20.5 | 285.6 | 7.47 | Running |
| 21.0 | 247.1 | 7.50 | Running |
| 21.5 | 260.2 | 7.52 | Running |
| 22.0 | 277.5 | 7.53 | Running |
| 22.5 | 325.2 | 7.50 | Running |
| 23.0 | 262.1 | 7.51 | Running |
| 23.5 | 262.0 | 7.58 | Running |
| 24.0 | 965.5 | 6.79 | Running |
| 24.5 | 1343.4 | 6.28 | Stall |
| 25.0 | 1343.1 | 6.27 | Stall |
| 25.5 | 1345.6 | 6.29 | Stall |
| 26.0 | 1346.1 | 6.28 | Stall |
| 26.5 | 1350.2 | 6.28 | Stall |
| 27.0 | 1351.1 | 6.28 | Stall |
| 27.5 | 1351.8 | 6.28 | Stall |
| 28.0 | 1351.9 | 6.28 | Stall |
| 28.5 | 1352.4 | 6.27 | Stall |
| 29.0 | 1350.4 | 6.28 | Stall |
| 29.5 | 1349.5 | 6.27 | Stall |
| 30.0 | 1349.4 | 6.28 | Stall |
| 30.5 | 1347.4 | 6.28 | Stall |
| 31.0 | 1346.4 | 6.28 | Stall |
| 31.5 | 1345.4 | 6.28 | Stall |
| 32.0 | 1344.5 | 6.28 | Stall |
| 32.5 | 1343.6 | 6.28 | Stall |
| 33.0 | 1341.7 | 6.28 | Stall |
| 33.5 | 1340.9 | 6.28 | Stall |
| 34.0 | 1340.1 | 6.28 | Stall |
| 34.5 | 1339.1 | 6.28 | Stall |
| 35.0 | 1338.2 | 6.28 | Stall |
| 35.5 | 1337.5 | 6.28 | Stall |
| 36.0 | 1336.1 | 6.28 | Stall |
| 36.5 | 1334.0 | 6.28 | Stall |
| 37.0 | 1333.5 | 6.28 | Stall |
| 37.5 | 1333.1 | 6.28 | Stall |
| 38.0 | 1331.9 | 6.27 | Stall |
| 38.5 | 1330.9 | 6.28 | Stall |
| 39.0 | 915.1 | 6.74 | Recovery |
| 39.5 | 321.8 | 7.40 | Recovery |
| 40.0 | 223.9 | 7.52 | Recovery |
| 40.5 | 269.1 | 7.50 | Recovery |
| 41.0 | 271.2 | 7.51 | Recovery |
| 41.5 | 292.2 | 7.43 | Recovery |
| 42.0 | 214.2 | 7.50 | Recovery |
| 42.5 | 241.2 | 7.47 | Recovery |
| 43.0 | 248.6 | 7.48 | Recovery |
| 43.5 | 226.9 | 7.51 | Recovery |
| 44.0 | 267.6 | 7.45 | Recovery |
| 44.5 | 260.7 | 7.52 | Recovery |
| 45.0 | 252.0 | 7.47 | Recovery |
| 45.5 | 248.3 | 7.47 | Recovery |
| 46.0 | 255.6 | 7.53 | Recovery |
| 46.5 | 216.0 | 7.47 | Recovery |
| 47.0 | 212.1 | 7.53 | Recovery |
| 47.5 | 226.8 | 7.57 | Recovery |
| 48.0 | 271.8 | 7.49 | Recovery |
| 48.5 | 3.5 | 7.77 | Idle |
| 49.0 | 3.4 | 7.77 | Idle |
| 49.5 | 3.5 | 7.78 | Idle |
| 50.0 | 3.4 | 7.78 | Idle |
| 50.5 | 3.3 | 7.78 | Idle |
| 51.0 | 3.2 | 7.78 | Idle |
| 51.5 | 3.2 | 7.77 | Idle |
| 52.0 | 3.3 | 7.78 | Idle |
| 52.5 | 3.1 | 7.78 | Idle |
| 53.0 | 3.4 | 7.78 | Idle |
| 53.5 | 3.3 | 7.78 | Idle |
| 54.0 | 3.3 | 7.78 | Idle |
| 54.5 | 3.2 | 7.78 | Idle |
| 55.0 | 3.5 | 7.78 | Idle |
| 55.5 | 3.2 | 7.78 | Idle |
| 56.0 | 3.4 | 7.77 | Idle |
| 56.5 | 3.4 | 7.78 | Idle |
| 57.0 | 3.3 | 7.78 | Idle |
| 57.5 | 3.4 | 7.78 | Idle |
| 58.0 | 3.4 | 7.78 | Idle |
| 58.5 | 3.1 | 7.78 | Idle |
| 59.0 | 3.1 | 7.78 | Idle |
| 59.5 | 3.3 | 7.78 | Idle |
| 60.0 | 3.3 | 7.78 | Idle |
| 60.5 | 3.2 | 7.78 | Idle |
| 61.0 | 3.4 | 7.78 | Idle |
| 61.5 | 3.1 | 7.78 | Idle |
| 62.0 | 3.2 | 7.78 | Idle |
| 62.5 | 3.4 | 7.78 | Idle |
| 63.0 | 3.4 | 7.78 | Idle |
| 63.5 | 3.3 | 7.78 | Idle |
| 64.0 | 3.5 | 7.78 | Idle |
| 64.5 | 3.4 | 7.78 | Idle |
| 65.0 | 3.5 | 7.78 | Idle |
| 65.5 | 3.3 | 7.78 | Idle |
| 66.0 | 3.6 | 7.78 | Idle |
| 66.5 | 3.1 | 7.78 | Idle |
| 67.0 | 3.2 | 7.78 | Idle |
| 67.5 | 3.2 | 7.78 | Idle |
| 68.0 | 3.2 | 7.78 | Idle |
| 68.5 | 3.5 | 7.78 | Idle |
| 69.0 | 3.3 | 7.78 | Idle |
| 69.5 | 3.3 | 7.78 | Idle |
| 70.0 | 3.2 | 7.78 | Idle |
| 70.5 | 3.3 | 7.78 | Idle |
| 71.0 | 3.3 | 7.78 | Idle |
| 71.5 | 3.1 | 7.78 | Idle |
| 72.0 | 3.0 | 7.78 | Idle |
| 72.5 | 3.2 | 7.78 | Idle |
| 73.0 | 3.4 | 7.78 | Idle |
Debugging that actually happened.

The dead motor channel
- Symptom
- During dual-motor bring-up, one wheel ran cleanly while the other stayed dead.
- Test
- Swapped the driver's inputs and outputs so each suspect carried the other's signal, and watched whether the dead behavior followed the wiring or stayed with the driver output.
- Fix
- The fault stayed with one DRV8833 output. That channel was retired, and the second wheel now runs on the working channel of a backup driver.
Swap-based isolation pins a fault to a single stage quickly, without special equipment.

Making the motors effectively silent
- Symptom
- Both motors produced an audible tone during low-duty operation.
- Test
- Held duty constant and swept the PWM carrier across 8 points from 1 kHz through 18 kHz. The tone rose with frequency, barely audible at 17 kHz and effectively unnoticeable at 17.5 kHz.
- Fix
- Replaced implicit analogWrite with explicit ESP32 LEDC channels at 17.5 kHz, keeping the existing 8-bit 0 to 255 command scale.
The fix had to match Arduino-ESP32 core 2.0.17 rather than the newer 3.x API.

A test stand instead of a runaway robot
- Problem
- Bench-testing drive commands on a desk was unsafe and unrepeatable.
- Approach
- Modeled a support stand in SolidWorks that holds the chassis with the wheels suspended, and printed it in black PLA on a Raise3D E2 after a printer error stopped the first attempt.
- Result
- The stand has been the testbench for every motor debugging session since.
Test fixtures are part of the engineering.
Firmware tuned against real hardware.
What the firmware does today
Modular left and right motor control through a shared drive state
Four ESP32 LEDC channels at 17.5 kHz and 8-bit resolution
Forward, reverse, coast and active brake tested on hardware
INA219 voltage, current and power refresh with online status
VL53L0X refresh rejects invalid range without erasing the last good value
Nonblocking millis-based motor-test timing keeps telemetry flowing
Motor task coasts both wheels if no command arrives within 500 ms
const TickType_t sensorPeriod =
pdMS_TO_TICKS(100);
TickType_t lastWakeTime = xTaskGetTickCount();
for (;;) {
refreshAllSensors(*sensorState);
xQueueOverwrite(sensorQueue, sensorState);
xTaskDelayUntil(&lastWakeTime, sensorPeriod);
}Both wheels spinning on the printed stand after the two-driver workaround.
Speed with direction.
Working now
- 17.5 kHz motor PWM
- 100 ms sensor-task scheduling
- 2S LiPo load plots and DMM checks
Designed
- Protected input, sensing and power muxes
- 6 V and 3.3 V bucks and the ESP32-S3 sheet
- 4-layer PCB with test points, sent to fab
Next up
- Bring up each rail on the new board
- Measure the path loss against the prototype
- Validate the motor task on hardware
Later
- Wi-Fi control and a browser dashboard
- Encoder feedback and obstacle braking
- 5 V buck breakout for a Raspberry Pi
The paper trail.
Dated build log32 entries · June 12 to October 23
Mechanical chassis assembled
Built the 2WD chassis with DC motors, wheels, front caster, and wheel encoder discs to create the rolling test platform.
ESP32-S3 communication confirmed
Brought up the microcontroller over serial and confirmed the board could communicate cleanly with the development machine.
Motor driver soldered and direction tested
Soldered the DRV8833 motor driver board, wired an initial powertrain circuit, and confirmed motor forward/reverse behavior from firmware.
Full bench circuit wired and PWM issue debugged
Soldered motor leads, heat-shrunk joints, mounted the battery pack and breadboard, added current and ToF sensor boards, and solved a low-PWM static-friction issue by raising the duty value to 140.
SolidWorks support stand designed for testing
Modeled a custom DriveWire stand, iterated support geometry, sliced the STL, and started a black PLA print on a Raise3D E2 so the wheels can spin safely during bench tests.
Support stand print restarted and adhesion issue noted
A printer error stopped the first stand print early, so the job was restarted. The second attempt reached late-stage progress, with slight corner lift noted for future brim, bed-temperature, or first-layer tuning.
Printed stand became the DriveWire testbench
Picked up the successful black PLA support stand and confirmed the chassis mounts securely and remains stable, creating a safer bench setup for motor debugging and firmware bring-up.
Motor driver fault isolated and both wheels brought online
Swapped motor driver inputs and outputs to isolate the dead channel, confirmed the fault stayed with one driver output, then used the working channel on a backup driver so both wheels spin. The active bug is now turn-command direction mapping.
Custom PCB battery power architecture started in Altium
Designed the first battery-entry schematic for the custom DriveWire PCB, including a planned 5 A fuse, AOD4185 PMOS reverse-polarity protection, and a second PMOS so a low-current slide switch can control the main power rail safely.
VL53L0X ranging tested on the chassis
Wired and mounted the time-of-flight sensor, then wrote a 1 Hz serial-ranging test. Reliable readings reached approximately 1.3 m under the tested indoor conditions, providing a measured starting point for obstacle detection and braking logic.
Motor-control module and braking behavior validated
Completed the motors.cpp and motors.h module, tested forward drive, reverse, coast, and brake at different speeds, and confirmed that active braking stops the wheels much faster than coasting from full speed.
PWM frequency sweep eliminated audible motor noise
Swept the carrier from 1 kHz to 18 kHz at constant duty, linked the changing motor tone to PWM-driven vibration, and selected 17.5 kHz as an effectively unnoticeable operating point. Explicit LEDC control preserves the existing 8-bit command scale and matches Arduino-ESP32 core 2.0.17.
Electrical telemetry module tested with fault injection
Added INA219 voltage, current, and power refresh logic to sensors.cpp, verified healthy serial output, then unplugged the sensor to confirm the module marks it offline and exits on the first failed poll without overwriting the last valid readings.
V1 sensor refresh pipeline completed and tested
Completed distance and combined-sensor refresh functions, then tested valid range, out-of-range data, and sensor disconnection. Invalid measurements preserve the last good distance while hardware status reports the fault. A direct Wire-based INA219 register read also matched the library voltage result.
Firmware prepared for task-based operation
Restructured main.cpp for continued serial bring-up and upcoming FreeRTOS work, while organizing the repository with the early Altium PCB files under Hardware and the existing support-stand model under Mechanical CAD.
First FreeRTOS sensor task verified on hardware
Added one dedicated task that refreshes the INA219 and VL53L0X every 1.5 seconds with xTaskDelayUntil, then overwrites a single-item queue with the latest DriveWireState snapshot. The Arduino loop receives and prints its own copy, and setup stops safely if the queue or task cannot be created. The firmware built successfully and produced live sensor telemetry.
Custom PCB sensing and motor-supply architecture expanded
Added INA219BIDR sensing circuitry to the Altium schematic with a tentative 20 mΩ shunt and 20 kΩ I2C pull-ups. The protected VBAT_BUS is intended to feed a 3.3 V logic supply and a TPS2121 power mux selecting between a planned 6 V motor rail and direct battery power for a hardware boost mode. Component values and the unfinished power circuitry still require validation.
2S LiPo stall-current measurements captured
Tested the prototype with a 2S LiPo measured at approximately 7.8 V unloaded. Two captured system stall-current readings were 1.3414 A and 1.3308 A, with telemetry reporting 6.39 V during both captures. These initial observations inform PCB component selection; they do not establish worst-case peak current. The roughly 1.2 V drop from running readings remains under investigation with direct DMM measurements planned.
DMM follow-up separated battery sag from prototype losses
Repeated the stall test with similar current readings and measured the battery-connected rails directly: 7.78 V in normal operation and 7.34 V during stall. The 0.44 V rail drop was smaller than the drop reported through the INA219: against the 6.39 V stall reading, about 0.95 V was being lost in the prototype power path rather than to battery sag. Reducing those losses with the custom PCB remains a goal to verify during bring-up.
Faster telemetry captured a MATLAB stall-current plot
Changed the sensor task period from 1.5 seconds to 100 ms and replaced blocking motor-test delays with millis-based drive/coast timing. Plotted the recorded voltage and current in MATLAB to show running, stall, and recovery behavior. Completed the tentative power schematic with TPS2121 support circuitry; values still need validation, and an OV2 battery-voltage interlock remains under consideration.
6 V motor buck regulator selected
Selected the TPS62130ARGTR as the regulator for the planned 6 V motor supply and began designing its supporting circuit. The supply has not yet been built or tested.
6 V buck schematic advanced
Chose a soft-start capacitor and a 130 kΩ / 20 kΩ feedback divider for the TPS62130A design. The dated schematic had every pin addressed except the switching-node connection, so the circuit remained unfinished and unvalidated.
Motor-supply inductor and footprint selected
Selected an inductor against saturation current, RMS current, DC resistance, size, and shielding requirements, then drew a custom Altium footprint from its datasheet. PCB integration and electrical validation are still ahead.
Smaller inductor and output capacitor chosen
Moved the 6 V buck to a smaller-footprint inductor that still meets its needs, and chose the output capacitor after checking its ESR and DC-bias derated capacitance to keep motor-rail ripple low.
6 V buck finalised, 3.3 V buck completed
Finalised the provisional 6 V schematic with two 22 µF input capacitors in parallel, about 8.7 µF at 8.4 V of bias against the datasheet's 10 µF. Set the 3.3 V buck's current limit and completed it, so all of the power converter schematics are done. A future Raspberry Pi will get its own 5 V buck breakout fed from a battery connector, rather than a third rail on this board.
ESP32 schematic started
Placed the ESP32-S3 module with its power, ground and input capacitors, plus the RESET and BOOT buttons.
USB, a fallback UART header and the 3.3 V supply mux
Wired the USB connector, and added male UART header footprints for a future adapter option that won't be soldered at first. Created and completed the 3.3 V supply power mux: it prefers 5 V USB power through the 3.3 V buck, and switches to the LiPo when USB is disconnected.
ESP32 schematic complete
Finished the ESP32 sheet: native USB programming with ESD protection, a backup UART programming and debug header, manual BOOT and RESET, all four motor PWM signals, I²C for the INA219 and VL53L0X, power-good, boost-control and mux-status signals, two removable motor thermistors, eight spare GPIOs including three ADC1 channels, an external JTAG header, and a dual I²C expansion interface.
Buck input hot loops laid out
Placed each buck's input capacitors close to the IC with short, wide copper to keep the hot-loop area and parasitic inductance down. Added local ground-return paths and thermal vias in the exposed ground pads to spread heat.
PCB routed, DRC clean
Finished routing the first DriveWire PCB: power converters, motor driver and ESP32 interfaces. Filled the unused bottom-layer space with a GND pour and stitching vias for copper balance and ground-return continuity. DRC reports zero active violations, with 19 waived. The design is complete pending final checks, with fabrication planned shortly.
Dedicated FreeRTOS motor task
Added a motor task with its own single-item command queue and its own DriveWireState, separate from the sensor task. It applies left/right PWM commands and coasts both motors if no new command arrives within 500 ms. A temporary test sends PWM 140/255 to both motors every 100 ms for two seconds, then stops sending to exercise the timeout while sensor telemetry keeps running.
Spin 1 sent to fab
Finished the final checks, exported the Gerbers, drill files, BOM and pick-and-place file, and placed the order with JLCPCB.
V1 bill of materials8 items in use
ESP32-S3 DevKitC-1-N8R8 main controller
Pololu DRV8833 dual motor driver under active test
2WD robot chassis with wired DC gear motors
2S LiPo pack used in September bench tests; AA pack used in earlier bring-up
INA219 current sensor soldered for current and power measurement
VL53L0X distance sensor mounted and tested to approximately 1.3 m indoors
22 AWG automotive wire, heat shrink, zip ties, and mounted breadboard
SolidWorks support stand printed in black PLA for safer debugging
DriveWire is moving.
The drivetrain, telemetry and fault handling work on the bench, and the 4-layer PCB passed DRC and is at fab. Next is bringing up the protected input, the mux and both bucks, and measuring how much of the prototype's 0.95 V path loss the board removes.



