BBR Digital Expander Documentation

Encoders#

Four extra quadrature encoder inputs, numbered 0 to 3.

Reading counts and velocity#

int counts = exp.getEncoderCount(0);      // signed, accumulates
int rate   = exp.getEncoderVelocity(0);   // signed counts per second

Velocity is computed on the board, over a proper time window. This is worth more than it looks: differentiating counts in your own loop gives you a noisy number that depends on how fast that loop happens to be running this iteration. The board’s answer doesn’t.

Counting also happens on the board, which means your loop rate cannot lose you counts. You can spend 50 ms doing something else and the count is still exact when you come back — the failure mode of polling a quadrature signal in software simply does not exist here.

Zeroing#

exp.resetEncoder(0);       // one channel
exp.resetAllEncoders();    // all four

Resets are idempotent. Every command carries a token, so if the I2C layer retries a transfer, the reset cannot happen twice. You will not get a double-zero from a retry.

Direction#

If a motor is mounted mirrored, fix it on the board rather than negating the number everywhere you use it:

exp.setEncoderDirection(1, BBRDigitalExpander.Direction.REVERSE);

FORWARD means the channel counts up when the mechanism moves the way you call forward; REVERSE flips both the count and the velocity. Ask for the current setting with getEncoderDirection(channel).

This one saves itself. Direction is set once when the encoder is bolted on, and a channel that counts backwards again after every reboot is a bug nobody traces back to a missing save call — so it goes to flash for you. Setting the same direction repeatedly costs nothing: the board compares the payload it would write against what is already stored and skips the write.

Like the other setup helpers, run it once rather than in a loop.

Setting several channels at once, or reading them as one value, is the advanced tier: setEncoderInvertMask(mask) / getEncoderInvertMask(), one bit per channel, set means inverted. Those do not auto-save — call saveConfigToFlash() yourself.

Absolute (pulse-width) encoders#

Any channel can be switched from quadrature to reading a PWM absolute encoder — the kind that reports its position as a pulse width, typically 1 to 1024 µs per revolution. An absolute encoder is correct the instant you power on, with no homing move.

exp.setChannelMode(2, BBRDigitalExpander.ChannelMode.PULSE_WIDTH);
int us = exp.getPulseWidthUs(2);    // measured pulse width in microseconds

For multi-turn tracking, tell the board the pulse-width range of your specific encoder so it can tell a wrap from a jump:

exp.setPwmChannelParams(2, 1, 1024);   // minUs, maxUs
exp.setPwmWrapEnabled(2, true);
exp.saveConfigToFlash();

With wrap tracking on, the channel accumulates across revolutions instead of snapping back to zero at the top of each turn.

A pulse width of 0 means NO SIGNAL, not “the shaft is at zero” — which is exactly what it is not. Test for it before using the value. A disconnected cable reads 0 forever, and treating that as a position is how a mechanism drives itself into a hard stop.

The pulse-width reading only works on a channel in PULSE_WIDTH mode, and the count is the reading you want in QUADRATURE mode. Asking for the wrong one fails — an exception in Java, 0 with lastStatus() == BBRStatus::WrongChannelMode in C++ — rather than returning a plausible-looking wrong number.

Triggering on an encoder position#

The board can watch an encoder for you and drive a digital output when it passes a threshold — no I2C, no loop code:

exp.triggerWhenEncoderPast(2, 0, 1000);   // output 2 high once channel 0 reads >= 1000

That saves to flash for you. The comparison is signed, and on a pulse-width channel the threshold is in microseconds rather than counts. See Digital outputs and triggers.

Diagnosing a channel that reads nothing#

The board exposes the live logic levels on the encoder input pins, which separates “the encoder isn’t turning” from “one wire isn’t connected”:

int pins = exp.readEncoderPinState();   // bit 2n = channel n A, bit 2n+1 = B

Idle inputs read 1, because the inputs are pulled up. Turn the shaft slowly while polling fast: both bits for a healthy channel toggle. A bit that never changes is the dead wire.

Worked example#

BBREncoderExample shows counts, firmware velocities and idempotent resets together. BBRPwmEncoderExample covers the absolute-encoder path. See Example OpModes.