Drive the BKA30D-R5 automotive instrument motor (ShenZhen Bactrianus) from CircuitPython. The BKA30D packs two independent 2-coil steppers behind a single 180:1 gear train, driving two concentric pointers — ideal for dual-pointer gauges and clocks.
Driven through TB6612FNG dual H-bridges, it provides:
- Sine/cosine microstepping (1/12° and finer) on the TB6612
- Absolute positioning in output-shaft degrees
- Smooth trapezoidal start/stop ramps with datasheet-derived speed limits
- Stall homing against the mechanical end-stop for absolute zero
- Idle release for power saving (gearbox holds position;
STBYsleeps the chip) - Synchronized dual-pointer moves (both pointers arrive together)
- Non-blocking
update()+ anasynciowrapper
Copy onto the CIRCUITPY drive:
/lib/bka30d.py # this file (no external driver dependency)
asyncio (adafruit_asyncio) is only needed for the async examples.
The coils want ~5 V / ~18 mA. A 3.3 V GPIO can't supply that (you'd sit below
the datasheet's 3.5 V minimum and torque suffers), so the coils are driven by a
TB6612FNG: 3.3 V logic on its control pins, coils powered from a separate
VM supply at ~5 V (the datasheet's Ub=5V operating point).
The TB6612 uses magnitude + direction control — one PWM pin per coil
(PWMA/PWMB) plus two digital direction pins (xIN1/xIN2) — so the library
microsteps by setting PWM duty = |sin/cos| and the direction pins = its sign.
That needs only two PWM channels per motor (four total).
One TB6612 = two H-bridges = one BKA30D motor (its two coils). Use two TB6612s for both pointers.
Pico TB6612 #1 (motor A)
GP0 (PWM) --> PWMA AO1/AO2 --> motor A coil 1
GP1 --> AIN1
GP2 --> AIN2
GP3 (PWM) --> PWMB BO1/BO2 --> motor A coil 2
GP4 --> BIN1
GP5 --> BIN2
GP6 --> STBY (chip enable; optional GPIO — see below)
5V --> VM (motor supply ~5 V)
3V3 --> VCC (logic supply)
GND -- GND (common ground — required)
Motor B uses a second TB6612 (PWMC/CIN1/CIN2/PWMD/DIN1/DIN2, its own STBY,
shared VM/VCC/GND). Pass each chip's STBY pin via a_stby=/b_stby= and
release() will sleep the chip between moves; if STBY is hard-tied high on
your board, just omit it.
The BKA30D has 8 pins = two motors, each a bipolar 2-coil stepper (no
center tap): motor A = pins A1–A2 (coil 1) + A3–A4 (coil 2); motor B =
B1–B2 + B3–B4. One TB6612's two H-bridge outputs drive one motor's two
coils — four wires:
TB6612 #1 (motor A) BKA30D
AO1 -----------------> A1 ┐ coil 1 (TB6612 channel A = library "coil A")
AO2 -----------------> A2 ┘
BO1 -----------------> A3 ┐ coil 2 (TB6612 channel B = library "coil B")
BO2 -----------------> A4 ┘
(TB6612 #2 → B1/B2 on AO1/AO2, B3/B4 on BO1/BO2)
⚠️ Keep each coil's two wires on the same H-bridge. The only way to wire this wrong is to cross coils (e.g.AO1→A1,AO2→A3) — then no bridge drives a full coil and the motor just buzzes. If unsure which pins pair into a coil, measure with a multimeter: the two ends of one coil read ~260–300 Ω; across two different coils reads open/much higher. No external resistors or flyback diodes needed — the TB6612 has them built in.
Direction: which coil wire goes to O1 vs O2 only sets rotation
direction. If a pointer sweeps the wrong way, don't rewire — set invert=True
on that pointer (a_kwargs={"invert": True}). Confirm the physical pin numbers
against datasheet Fig. 12 / your part's silkscreen.
import board, pwmio, digitalio
from bka30d import BKA30D
def pwm(p): return pwmio.PWMOut(p, frequency=20000, duty_cycle=0)
def out(p):
d = digitalio.DigitalInOut(p); d.direction = digitalio.Direction.OUTPUT
return d
bka = BKA30D.from_tb6612(
# (PWMx, xIN1, xIN2, PWMy, yIN1, yIN2): coil A first triple, coil B second
(pwm(board.GP0), out(board.GP1), out(board.GP2),
pwm(board.GP3), out(board.GP4), out(board.GP5)), # motor A
(pwm(board.GP7), out(board.GP8), out(board.GP9),
pwm(board.GP10), out(board.GP11), out(board.GP12)), # motor B
microsteps=16,
a_stby=out(board.GP6), b_stby=out(board.GP13),
a_kwargs={"max_angle": 315.0}, # outer shaft travel
b_kwargs={"max_angle": 270.0}, # inner shaft travel
)
bka.home(toward="min") # find absolute zero on the end-stop
bka.move_to(a=180.0, b=90.0, sync=True) # both arrive together
bka.wait() # block until stopped
bka.release() # cut idle current; gearbox holdsNon-blocking from your own loop:
bka.move_to(a=270.0, b=45.0)
while True:
bka.update() # advance ramps; call often
... # your other workWith asyncio (recommended for two pointers at once):
import asyncio
async def main():
asyncio.create_task(bka.run()) # drives both ramps
await bka.move_to_async(a=315, b=270) # or just set targets elsewhere
asyncio.run(main())See examples/: bka30d_simpletest.py, bka30d_clock.py.
Coil driver for one motor on one TB6612: pwma/pwmb are PWMOut (magnitude),
the xIN1/xIN2 pairs are DigitalInOut (direction), stby is the optional
chip-enable. step(sign), enable(), release().
Pointer(driver, *, max_speed=600, start_speed=200, accel=3000, min_angle=0, max_angle=315, invert=False, auto_release=False, release_delay=0.5)
One pointer / motor wrapping a coil driver.
move_to(angle),move(delta),stop()— degrees, clamped to limitsangle,target_angle,is_moving— stateupdate(now_ns=None)→ bool — advance toward target; call frequentlyhome(toward="min"|"max", speed=None, set_to=None, overtravel=0.1)— blocking stall-homeset_angle(angle)— declare current position;release()— de-energize
BKA30D(a, b) / BKA30D.from_tb6612(a_pins, b_pins, *, microsteps=16, a_stby, b_stby, a_kwargs, b_kwargs, **common)
Both pointers as .a / .b. Each *_pins is (PWMx, xIN1, xIN2, PWMy, yIN1, yIN2).
move_to(a=None, b=None, sync=False)—sync=Truemakes both finish togetherupdate(),is_moving,wait(poll=0.001),stop(),release(),home(**kw)async run(poll=0.001),async move_to_async(a, b, sync=False)
| Quantity | Value |
|---|---|
| Gear ratio | 180:1 |
| Full step | 1/3° output (3 steps/°) |
| Micro step | 1/12° nominal |
| Start/stop speed (instant) | ≤ 200 °/s |
| Max driving speed | ≤ 600 °/s |
| Travel range | ~320° (outer) / ~270° (inner) |
| Backlash | 0.7–1.2° |
| Holding torque (gearbox) | 100–110 mN·m |
Speeds above 200 °/s are reached by ramping (accel); the velocity floor is
kept at the start/stop speed so moves can stop instantly without losing steps.
The datasheet's "Hz" axes are output °/s (Fig. 3).
microsteps:4matches the datasheet's native 1/12°;16is smoother and quieter. Higher values raise the step rate your loop must sustain — for fast slews keep it modest or callupdate()more often.- Backlash: uncompensated. For repeatable placement, always approach a target from the same direction in your application code.
- Homing drives the pointer into a hard stop on purpose; keep
speedat or below the start/stop speed (the default) so the motor slips harmlessly.