Heading (IMU)#
Every call on this page fails if the gyro cannot be trusted — an exception in Java, false or NAN in C++. That is deliberate, and better than a heading of 0.00 that never changes. See Troubleshooting.
Needs the odometry variant of the board. On a base board these calls say so specifically, rather than reporting a plausible heading forever.
Reading heading#
double deg = exp.getHeading(); // degrees, CCW positive
float deg = expander.heading(); // degrees, CCW positive; NAN if untrustworthy
if (!expander.ok()) {
Serial.println(expander.lastErrorText());
}
deg = expander.heading() # degrees, CCW positive; raises if untrustworthy
There is no sentinel here: a board with no IMU raises NoImuError, and one whose IMU has stopped answering raises ImuFaultError.
The board fuses gyro and accelerometer data continuously at high rate, so the heading you read is already filtered. You are not integrating anything yourself.
Zero it at the start#
exp.resetHeading();
expander.resetHeading();
expander.reset_heading()
Heading is measured from wherever the robot was when you last called this. Call it once the robot is in its starting position.
Resetting the heading is per-run and is not saved to flash — it will not disturb any mounting calibration.
Calibrate the gyro#
exp.calibrateGyro(); // ~1 second, robot must be COMPLETELY still
expander.calibrateGyro(); // ~1 second, robot must be COMPLETELY still
expander.calibrate_gyro() # ~1 second, robot must be COMPLETELY still
This measures the gyro’s bias — the small non-zero reading it produces while sitting still, which would otherwise accumulate into heading drift.
The robot must be genuinely still. Not “mostly still”, not “someone is holding it”. If the board detects movement it refuses rather than storing a bad bias, because a wrong bias is worse than an old one: the call returns false and the previous bias stays in use. Nothing stops your program, so a bump during init costs you a little drift, never the match.
Doing it while the robot sits waiting to start is the natural place for it.
Checking the state#
BBRDigitalExpander.ImuState imu = exp.readImu();
imu.yawDeg; // heading
imu.pitchDeg;
imu.rollDeg;
imu.gyroDps; // rates, robot frame
imu.linearAccel; // m/s^2, gravity removed
imu.isFusionValid(); // is the attitude output usable right now
imu.isBiasValid(); // has it been still long enough to trust the bias
imu.isGyroSaturated(); // did a rate exceed what the gyro can measure
BBRImuState imu;
if (expander.readImu(imu)) {
imu.yawDeg; // heading
imu.pitchDeg;
imu.rollDeg;
imu.gyroDps[2]; // rates, robot frame, x y z
imu.accelMps2[2]; // m/s^2, gravity removed
imu.fusionValid(); // is the attitude output usable right now
imu.biasValid(); // has it been still long enough to trust the bias
imu.gyroSaturated(); // did a rate exceed what the gyro can measure
}
imu = expander.read_imu()
imu.yaw_deg # heading
imu.pitch_deg
imu.roll_deg
imu.gyro_dps[2] # rates, robot frame, x y z
imu.accel_mps2[2] # m/s^2, gravity removed
imu.fusion_valid # is the attitude output usable right now
imu.bias_valid # has it been still long enough to trust the bias
imu.gyro_saturated # did a rate exceed what the gyro can measure
fusionValid is the one to check before trusting a heading. It drops briefly after a mounting change or a reset while the filter re-converges — around 100 ms — and comes back on its own.
gyroSaturated matters if your robot spins hard. Past the gyro’s maximum rate the heading estimate is no longer trustworthy, and this flag is how you find out that happened rather than wondering why the heading is off afterwards.
Mounting#
The board knows its own IMU orientation — that is handled in firmware and is not your problem.
What it cannot know is how you bolted the board to your robot.
Mount the board square to the robot’s forward direction. The firmware supports a mounting yaw offset (IMU_YAW_OFFSET), and it applies live without a reboot — but neither driver currently exposes a method to set it. Until one does, a board mounted at an angle in the horizontal plane will report headings rotated by that angle, with nothing to correct it from your code.
Square and flat is the default and needs no configuration at all.
If you had to mount the board on its edge rather than flat — bolted to a side rail, say — tell it which way is up, so it integrates yaw about the true vertical instead of about whatever axis happens to be pointing sideways:
exp.setImuAxisUp(BBRRegMap.AXIS_POS_Z); // AXIS_POS_Z is flat, the default
exp.saveConfigToFlash();
exp.resetLocalizerAndCalibrateImu(); // re-calibrate on the new axis
expander.setImuAxisUp(BBR_AXIS_POS_Z); // BBR_AXIS_POS_Z is flat, the default
expander.saveConfigToFlash();
expander.resetLocalizerAndCalibrateImu(); // re-calibrate on the new axis
expander.set_imu_axis_up(AxisUp.POS_Z) # POS_Z is flat, the default
expander.save_config_to_flash()
expander.reset_localizer_and_calibrate_imu() # re-calibrate on the new axis
The six axis values cover ±X, ±Y and ±Z. Re-calibrate after changing it, or the gyro bias will still belong to the old axis.
Heading triggers#
The board can drive a digital output from heading, the same way it does for colours and distances. A “robot is pointing roughly downfield” pin costs you no loop time and no I2C traffic:
exp.triggerWhenFacing(0, 90, 15); // output 0 high while heading is 90° ±15°
expander.triggerWhenFacing(0, 90.0f, 15.0f); // output 0 high while heading is 90° ±15°
expander.trigger_when_facing(0, 90.0, 15.0) # output 0 high while heading is 90° ±15°
Degrees, not centidegrees, and the same headings the heading getter reports. Tolerance is either side of the target, so that example is a 30° window. Like the other everyday helpers it saves to flash automatically.
The window is allowed to straddle the ±180 seam — a target of 180 with a tolerance of 20 covers 160° to −160°. The board does that arithmetic; you do not.
The pin stays low until the gyro has settled after power-on, and goes low again if the IMU faults. A heading trigger never reports a confident answer the board doesn’t have.
If you need an asymmetric window rather than a target-and-tolerance, the advanced tier takes the raw edges in degrees × 100:
BBRDigitalExpander.OutputConfig out = new BBRDigitalExpander.OutputConfig();
out.source = BBRDigitalExpander.OutputSource.IMU_HEADING;
out.threshMin = 4500; // 45.00°
out.threshMax = 13500; // 135.00°
exp.configureOutput(0, out);
exp.saveConfigToFlash();
BBROutputConfig out;
out.source = BBROutputSource::ImuHeading;
out.threshMin = 4500; // 45.00°
out.threshMax = 13500; // 135.00°
expander.configureOutput(0, out);
expander.saveConfigToFlash();
expander.configure_output(0, OutputConfig(
source=OutputSource.IMU_HEADING,
thresh_min=4500, # 45.00°
thresh_max=13500, # 135.00°
))
expander.save_config_to_flash()
Setting threshMin above threshMax is not an error there either: it is how you ask for a window that wraps through ±180.
Worked example#
BBRImuHeadingExample covers heading, calibration, and failing loudly when the gyro cannot be trusted. BBRHeadingTriggerExample arms a heading trigger and shows the pin following it. See Example OpModes.
HeadingIMU covers heading, calibration and the saturation flags, and TriggerSetup arms a heading trigger. See Example sketches.
heading_imu.py covers heading, calibration and the saturation flags, and trigger_setup.py arms a heading trigger. See Example scripts.