Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bka30d — CircuitPython driver for the BKA30D-R5 dual-shaft gauge stepper

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; STBY sleeps the chip)
  • Synchronized dual-pointer moves (both pointers arrive together)
  • Non-blocking update() + an asyncio wrapper

Install

Copy onto the CIRCUITPY drive:

/lib/bka30d.py            # this file (no external driver dependency)

asyncio (adafruit_asyncio) is only needed for the async examples.

Why a TB6612

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).

Wiring

One TB6612 = two H-bridges = one BKA30D motor (its two coils). Use two TB6612s for both pointers.

MCU → TB6612 (control side)

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.

TB6612 → BKA30D (motor side)

The BKA30D has 8 pins = two motors, each a bipolar 2-coil stepper (no center tap): motor A = pins A1A2 (coil 1) + A3A4 (coil 2); motor B = B1B2 + B3B4. 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.

Quick start

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 holds

Non-blocking from your own loop:

bka.move_to(a=270.0, b=45.0)
while True:
    bka.update()       # advance ramps; call often
    ...                # your other work

With 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.

API summary

TB6612Motor(pwma, ain1, ain2, pwmb, bin1, bin2, *, microsteps=16, stby=None)

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 limits
  • angle, target_angle, is_moving — state
  • update(now_ns=None) → bool — advance toward target; call frequently
  • home(toward="min"|"max", speed=None, set_to=None, overtravel=0.1) — blocking stall-home
  • set_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=True makes both finish together
  • update(), is_moving, wait(poll=0.001), stop(), release(), home(**kw)
  • async run(poll=0.001), async move_to_async(a, b, sync=False)

Datasheet limits baked in (BKA30D-R5.pdf)

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).

Notes & tuning

  • microsteps: 4 matches the datasheet's native 1/12°; 16 is smoother and quieter. Higher values raise the step rate your loop must sustain — for fast slews keep it modest or call update() 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 speed at or below the start/stop speed (the default) so the motor slips harmlessly.

About

CircuitPython driver + hardware for a dual-pointer clock using the BKA30D-R5 dual-shaft gauge stepper (TB6612FNG-driven)

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages