A motor driver can receive the same average command and still stop very differently. Build a small two-channel driver module, distinguish its electrical states, and use a controller that pauses before reversing direction. The experiment keeps commanded duty, winding-current regulation and observed shaft motion as three separate quantities.
This is a design candidate for a low-voltage bench prototype. The supplied software has host checks; the board has not been assembled, thermally qualified or run with a motor, and no target firmware binary is claimed. Start with a regulated 5 V supply and a 0.5 A supply-current limit. The prototype targets a nominal 0.2 A current trip per bridge; it does not have a qualified continuous-current rating.
The Texas Instruments DRV8833 is an integrated circuit (IC): a semiconductor device containing both control circuits and power switches. Its two H-bridges each connect a motor winding to the supply in either polarity. Reversing that polarity can reverse a brushed direct-current motor. A direct-current (DC) supply maintains one polarity; the bridge changes which motor terminal receives its positive side.
Here we use the exact DRV8833PWPT, in the PWP 16-pin thermally enhanced small-outline package. The final T specifies tape packaging. PWP has an exposed underside thermal connection, unlike the similarly named PW package. Package choice changes both assembly and the conditions behind the data-sheet current figures. A library symbol labelled only “DRV8833” is insufficient unless its pins and footprint match this exact package.
Pulse-width modulation (PWM) controls how much of each repeated interval an input remains HIGH. Duty is that HIGH fraction, not a speed measurement. Motor resistance, friction, load, supply voltage and internal current regulation all affect the resulting speed. The complete example runs on an original non-wireless Raspberry Pi Pico with the RP2040 controller, using MicroPython, a Python implementation intended for microcontrollers.

A net is a set of terminals joined by conductive wiring. The repeated labels in this drawing keep the two bridges and their support parts readable without overlapping wire bundles. They do not imply isolation between repeated labels. Ground (GND) is the common reference conductor, including the Pico, motor supply and driver module.
| Part | Exact choice or value | Job |
|---|---|---|
| U1 | TI DRV8833PWPT, PWP0016C | Two bridges and winding-current regulation |
| R1, R2 | Vishay CRCW12061R00FKEA; 1 Ω, ±1%, 0.25 W, 1206 | Convert each bridge current into a sense voltage |
| C1 | TDK C3225X7R1E226M250AB; 22 µF, ±20%, 25 V, 1210 | Local motor-supply ceramic bypass; qualify at least 10 µF effective |
| C2 | TDK C2012X7R1C225K125AB; 2.2 µF, 16 V, 0805 | Bypass the internal regulator; do not power another circuit from VINT |
| C3 | TDK C1608X7R1H103K080AA; 10 nF, 50 V, 0603 | Connect the charge-pump node VCP to VM |
| D1 | Vishay SMBJ6.0A-E3/52, unidirectional transient suppressor | Banded cathode to VM, anode to GND; finite-pulse rail clamp, not a continuous shunt supply |
| R3, R4 | 10 kΩ, ±1%, 0805 | Pull nFAULT to host 3.3 V and hold nSLEEP LOW by default |
| J1–J3, J4 | Samtec TSW-102-07-G-S and TSW-108-07-G-S | Power, two motors and eight-pin logic interface |
The leading n in nSLEEP and nFAULT identifies signals that are active LOW: LOW requests sleep or reports a fault. nFAULT is an open-drain output, a switch that can pull its signal toward ground but requires an external resistor for HIGH. R3 uses the Pico’s 3.3 V supply reference, not the motor supply. HIGH on that wire alone does not prove that the motor rail is present, correctly regulated or healthy.
| U1 physical pin | Connection |
|---|---|
| 1; 16; 15 | nSLEEP to SLEEP; AIN1 and AIN2 to the A command nets |
| 2; 4; 3 | AOUT1 and AOUT2 to J2; AISEN through R1 to GND |
| 7; 5; 6 | BOUT1 and BOUT2 to J3; BISEN through R2 to GND |
| 9; 10; 8 | BIN1, BIN2 and nFAULT to their named logic nets |
| 12; 11; 14 | VM supply, VCP with C3 to VM, VINT with C2 to GND |
| 13; exposed pad 17 | Both to GND copper; the thermal pad is not optional floating metal |
For the supplied firmware, connect J4 pins 1–8 to Pico GND, 3V3 OUT, GP2, GP3, GP4, GP5, GP6 and GP7 respectively. On the original Pico, GP2/3/4/5 are physical pins 4/5/6/7, GP6/7 are pins 9/10, 3V3 OUT is pin 36 and GND can be pin 38. The Pico’s 3V3 OUT connection only supplies the logic pull-up reference; it must not supply the motor. Check the exact board label before wiring another Pico variant.
The charge pump is an internal switching circuit that creates the gate-drive voltage for the bridge switches. C3 is its specified support capacitor; it must return to VM exactly as shown, rather than to a convenient ground pad.
The nominal sense threshold is 0.2 V. With 1 Ω resistance, the nominal trip is Itrip = Vtrip / Rsense = 0.2 V / 1 Ω = 0.2 A. Using the data-sheet 160–240 mV threshold range and ±1% resistor tolerance gives a static threshold calculation from 0.16/1.01 = 0.1584 A to 0.24/0.99 = 0.2424 A under the stated data-sheet conditions. This is not a bound on every instantaneous peak: blanking time, winding inductance and switching transients matter.
At the nominal trip, resistor dissipation is I²R = 0.04 W. Even the high static threshold calculation gives approximately 0.0582 W, below the selected resistor’s nominal power rating. Check its temperature derating and actual layout. The chip’s illustrative conduction loss at 0.2 A and its typical 0.36 Ω combined switch resistance is 0.0144 W per bridge; this excludes hot resistance, switching and other losses. It does not establish the temperature of this board.
The DRV8833 current regulator can interrupt a bridge even while the external command remains HIGH. Its short-circuit protection is a separate mechanism with different thresholds. Do not deliberately short a motor connector to “test the 0.2 A limit.” A stall can also heat a motor at a current the driver can tolerate. Choose a motor whose documented voltage, winding current, starting demand and mechanical limits suit this deliberately modest experiment.
| IN1 | IN2 | Bridge condition |
|---|---|---|
| LOW | LOW | Coast: both outputs high impedance |
| HIGH | LOW | Forward polarity |
| LOW | HIGH | Reverse polarity |
| HIGH | HIGH | Brake: both outputs LOW |
High impedance means the switches are not actively fixing the terminal voltage. An inductive winding can still carry recirculating current through available semiconductor paths. In brake mode the winding is shorted through the bridge’s LOW-side paths; it is not disconnected. Neither state gives an unconditional mechanical stopping distance.
The delivered hardware demonstration defaults to forward and coast only. Hard braking and reversal are explicit function options, to be enabled only after load-energy and supply-transient qualification. The supplied controller implements forward PWM by toggling IN1 while holding IN2 LOW, giving the fast-decay command pattern described in the data sheet. Reversal requests pass through a chosen 100 ms coast interval. This pause makes the experiment repeatable but does not prove that a loaded rotor has stopped. Actual motor reversal and rail excursions still require measurement.

The pure policy classes return desired PWM values and a sleep state. The hardware entry point writes those values to the four actual Pico outputs. It latches a LOW fault report, clears requests and requires explicit rearming; a later HIGH does not restart the motor automatically. Cleanup drives sleep LOW and releases PWM resources even if an exception interrupts the demonstration. A stalled Python program is a separate failure: this is not a safety-rated motor controller or a complete watchdog system.
"""Pico MicroPython two-bridge bench controller; host-testable policy.
No motor speed, motor-current or power-rail feedback is inferred from commands.
"""
MODULUS = 1 << 30
def elapsed(now, then):
"""Signed ticks difference, valid for intervals shorter than half a wrap."""
import time
if hasattr(time, 'ticks_diff'):
return time.ticks_diff(now, then)
return ((now - then + MODULUS // 2) % MODULUS) - MODULUS // 2
def drive_pair(mode, duty):
if mode not in ('coast', 'brake', 'forward', 'reverse'):
raise ValueError('Unknown bridge mode')
if not isinstance(duty, int) or isinstance(duty, bool) or not 0 <= duty <= 65535:
raise ValueError('duty must be an integer from 0 through 65535')
if mode == 'coast':
return (0, 0)
if mode == 'brake':
return (65535, 65535)
return (duty, 0) if mode == 'forward' else (0, duty)
def trip_current(resistance_ohm, trip_v=0.2):
if not 0 < resistance_ohm or not 0 < trip_v:
raise ValueError('Positive sense resistance and threshold required')
return trip_v / resistance_ohm
class Bridge:
"""Direction reversal passes through a 100ms COAST interval.
This is a chosen bench pause, not a proof that the shaft has stopped.
"""
def __init__(self):
self.requested = ('coast', 0)
self.applied = ('coast', 0)
self.last_direction = None
self.coast_since = None
def request(self, mode, duty=0):
drive_pair(mode, duty) # validate before changing the previous request
self.requested = (mode, duty)
def clear(self):
self.__init__()
def step(self, now):
mode, duty = self.requested
direction = mode if mode in ('forward', 'reverse') and duty else None
if direction and self.last_direction and direction != self.last_direction:
if self.coast_since is None:
self.coast_since = now
if elapsed(now, self.coast_since) < 100:
self.applied = ('coast', 0)
return (0, 0)
self.last_direction = direction
self.coast_since = None
elif direction:
self.last_direction = direction
self.coast_since = None
else:
# Explicit COAST or BRAKE cancels the reversal wait. Retain the
# previous direction so a later opposite command still waits.
self.coast_since = None
self.applied = (mode, duty)
return drive_pair(mode, duty)
class Controller:
"""Caller writes the returned input pairs and nSLEEP state every cycle."""
def __init__(self):
self.bridges = [Bridge(), Bridge()]
self.enabled_at = None
self.fault_latched = False
def arm(self, now, fault_ok):
if not fault_ok:
self.stop(True)
return False
self.stop(False)
self.enabled_at = now
return True
def stop(self, fault=False):
for bridge in self.bridges:
bridge.clear()
self.enabled_at = None
self.fault_latched = bool(fault)
def request(self, bridge, mode, duty=0):
if bridge not in (0, 1):
raise ValueError('Bridge must be 0 or 1')
if self.enabled_at is None or self.fault_latched:
raise RuntimeError('Explicit arm required')
self.bridges[bridge].request(mode, duty)
def step(self, now, fault_ok):
if not fault_ok:
self.stop(True)
if self.enabled_at is None or self.fault_latched:
return False, ((0, 0), (0, 0))
# Datasheet lists 1ms wake time; a 2ms software guard is chosen here.
# This loop is not a verified timing instrument or a rail monitor.
if elapsed(now, self.enabled_at) < 2:
return True, ((0, 0), (0, 0))
return True, tuple(b.step(now) for b in self.bridges)
def demo_sequence(allow_brake=False, allow_reverse=False):
return [(0, 'forward', 16000), (3000, 'coast', 0),
(5000, 'forward', 16000),
(8000, 'brake' if allow_brake else 'coast', 0),
(10000, 'coast', 0),
(12000, 'reverse' if allow_reverse else 'coast',
16000 if allow_reverse else 0), (15000, 'coast', 0)]
def run_demo(allow_brake=False, allow_reverse=False):
from machine import Pin, PWM
import time
sleep = Pin(6, Pin.OUT, value=0)
outputs = [PWM(Pin(n)) for n in (2, 3, 4, 5)]
for out in outputs:
out.freq(1000)
out.duty_u16(0)
fault = Pin(7, Pin.IN) # module R3 pulls to the host's 3.3V reference
controller = Controller()
started = time.ticks_ms()
# Hard braking/reversal require a separately qualified energy setup.
sequence = demo_sequence(allow_brake, allow_reverse)
index = 0
try:
if not controller.arm(started, bool(fault.value())):
raise RuntimeError('nFAULT LOW before arm')
while time.ticks_diff(time.ticks_ms(), started) < 17000:
now = time.ticks_ms()
age = time.ticks_diff(now, started)
if index < len(sequence) and age >= sequence[index][0]:
_, mode, duty = sequence[index]
controller.request(0, mode, duty)
print(age, mode, duty, 'command only; no RPM measurement')
index += 1
enabled, pairs = controller.step(now, bool(fault.value()))
# Sleep LOW takes precedence over the input commands on any fault.
if not enabled:
sleep.value(0)
for out, value in zip(outputs, pairs[0] + pairs[1]):
out.duty_u16(value)
if enabled:
sleep.value(1)
if controller.fault_latched:
print('FAULT latched; no automatic restart')
break
time.sleep_ms(5)
finally:
sleep.value(0)
for out in outputs:
out.duty_u16(0)
out.deinit()
if __name__ == '__main__':
run_demo()
Run python3 test_drv8833_control.py on a computer to exercise the supplied command classes, invalid requests, independent bridges, fault latching, reversal timing, wrap arithmetic and sense-threshold calculations. These tests do not import the target’s machine module. They are not a MicroPython board run, an electrical driver simulation or a firmware-binary build. Copy the full file to the selected Pico only after the wiring review and bench checks below.
Local bypass is only part of the power design. A transient-voltage suppressor (TVS) is a diode intended to divert a specified short overvoltage pulse. The module adds Vishay SMBJ6.0A-E3/52 across VM and GND, with its banded cathode at VM. Its 6 V stand-off is above the prototype input range; the data sheet gives a 10.3 V maximum clamp at a specified 58.3 A, 10/1000 µs pulse and temperature condition. This is not a guarantee for every front edge or layout. The 600 W pulse figure is not a continuous power rating or an unlimited motor-energy allowance. Motor braking can return energy to the supply; many bench supplies do not absorb it. At least 100 µF of external bulk capacitance rated 16 V or higher must be fitted very close to J1, and the complete energy-handling arrangement must be qualified for the actual motor, lead inductance and braking method. The IC’s 10.8 V operating maximum and 11.8 V absolute maximum are different limits, and neither grants permission for uncontrolled rail spikes on a nominal 5 V board.
For the printed circuit board (PCB), use short output loops, keep both sense resistors near the IC and provide their low-impedance ground returns. Solder the exposed pad to appropriate copper. Inspect actual thermal-via and paste geometry with the chosen assembly process. A clean design-rule report only checks the configured geometry; it does not prove electrical current capability, thermal performance or motor compatibility.
The serial log says “brake,” but the measured stopping time changes when a heavier wheel is fitted. Has the truth table failed?
No. The table specifies an electrical connection, not a mechanical stop time. The larger inertia changes stored energy and the current/torque history. Check actual input states, winding current, supply behavior and the mechanical setup; do not redefine a command as an RPM sensor.
Download the complete source and experiment files · Blank observations CSV. CSV means comma-separated values, a plain-text table format. For another example separating a timer command from a physical load, read the published ATtiny85 passive-piezo experiment.