Skip to content

Quickstart

Read orientation from a BNO08x over I2C: wire seven signals, write a small port class, call four driver functions.

1. Wire it

MCU BNO08x Notes
I2C SCL H_SCL (pin 19) 100 or 400 kHz; 2 kΩ to 4 kΩ pull-up to VDDIO
I2C SDA H_SDA (pin 20) Pull-up as for SCL
GPIO in H_INTN (pin 14) Active low: the hub has data. Use a pin that can wake the MCU
GPIO out NRST (pin 11) Active low reset; the driver pulses it in HardwareReset()
GND PS0 (pin 6), PS1 (pin 5) Both low selects I2C; sampled at reset
GND or VDDIO SA0 (pin 17) Low: address 0x4A, high: 0x4B
3.3 V, GND VDD, VDDIO, GND VDD must be up no later than VDDIO

Pull BOOTN (pin 4) high through 10 kΩ, or wire it to a GPIO if you want firmware updates. Breakout boards usually strap PS0, PS1, SA0 and BOOTN already; check yours. The Hardware page has the full pin list, SPI and UART wiring and the timing.

2. Add the driver

Add the repository as a submodule with its SH-2 library and link the hf_bno08x CMake target. Installation covers CMake and ESP-IDF.

git submodule add https://github.com/hardfoc/hf-bno08x-driver.git external/hf-bno08x-driver
git submodule update --init external/hf-bno08x-driver/src/sh2

3. Write the port

The port is the only platform code. It moves bytes over I2C, reads H_INTN, drives NRST and BOOTN, waits and keeps time. Replace the platform_* calls with your SDK's.

my_i2c.hpp
class MyI2c : public bno08x::CommInterface<MyI2c> {
public:
  static constexpr int kIntnGpio = 17;      // H_INTN, active low
  static constexpr int kRstnGpio = 16;      // NRST, active low
  static constexpr int kBootnGpio = 18;     // BOOTN, active low (optional, for firmware updates)

  BNO085Interface GetInterfaceType() noexcept {
    return BNO085Interface::I2C;
  }

  // Called by Begin() and the bootloader helpers, possibly more than once: initialise once.
  bool Open() noexcept {
    if (!open_) {
      open_ = platform_i2c_open(400000); // standard 100 kHz or fast 400 kHz
    }
    return open_;
  }

  void Close() noexcept {
    platform_i2c_close();
    open_ = false;
  }

  // One SHTP transfer from the SH-2 library. Return the bytes accepted, or a negative value.
  int Write(const uint8_t* data, uint32_t length) noexcept {
    return platform_i2c_write(address_, data, length) ? static_cast<int>(length) : -1;
  }

  // Return one complete SHTP transfer (header included), 0 when the hub has nothing to send.
  int Read(uint8_t* data, uint32_t length) noexcept {
    if (!DataAvailable()) {
      return 0;
    }
    uint8_t header[4];
    if (!platform_i2c_read(address_, header, sizeof(header))) {
      return -1;
    }
    uint32_t transfer = (header[0] | (header[1] << 8)) & 0x7FFFU; // bit 15: continuation flag
    if (transfer == 0) {
      return 0; // empty transfer
    }
    if (transfer > length) {
      transfer = length;
    }
    // A new read starts again at the header, so read the whole transfer.
    return platform_i2c_read(address_, data, transfer) ? static_cast<int>(transfer) : -1;
  }

  // H_INTN low: the hub has a transfer waiting.
  bool DataAvailable() noexcept {
    return !platform_gpio_read(kIntnGpio);
  }

  void Delay(uint32_t ms) noexcept {
    platform_delay_ms(ms);
  }
  uint32_t GetTimeUs() noexcept {
    return platform_micros();
  }

  // ACTIVE means "function asserted": low on NRST and BOOTN. PS0/PS1 are strapped to GND for I2C.
  // Return false on a GPIO error; a pin the board does not wire is not an error.
  bool GpioSet(bno08x::CtrlPin pin, bno08x::GpioSignal signal) noexcept {
    const bool active = signal == bno08x::GpioSignal::ACTIVE;
    switch (pin) {
    case bno08x::CtrlPin::RSTN:
      return platform_gpio_write(kRstnGpio, !active);
    case bno08x::CtrlPin::BOOTN:
      return platform_gpio_write(kBootnGpio, !active);
    case bno08x::CtrlPin::WAKE:
    case bno08x::CtrlPin::PS0:
    case bno08x::CtrlPin::PS1:
      break; // not wired on this board
    }
    return true;
  }

  // Optional, for firmware updates: the driver moves the port to the bootloader address (0x28 /
  // 0x29) for the transfer and back to the application address afterwards.
  uint8_t GetI2cAddress() noexcept {
    return address_;
  }
  bool SetI2cAddress(uint8_t addr7) noexcept {
    address_ = addr7;
    return true;
  }

private:
  bool open_ = false;
  uint8_t address_ = 0x4A; // SA0 low; 0x4B with SA0 high
};

Read() returns one complete SHTP transfer: it reads the 4-byte header for the length, then reads the whole transfer again from the start. Porting explains each method and shows the UART-RVC port.

4. Read orientation

main.cpp
#include "bno08x.hpp"

#include <cstdio>

int print_orientation(MyI2c& bus) {
  BNO085<MyI2c> imu(bus);

  imu.HardwareReset();         // 1. pulse NRST
  if (!imu.Begin()) {          // 2. open the bus, start SH-2
    return imu.GetLastError(); //    SH2_ERR_* from sh2_err.h
  }
  if (!imu.EnableSensor(BNO085Sensor::RotationVector, 10)) { // 3. orientation every 10 ms
    return imu.GetLastError();
  }
  for (;;) {
    imu.Update(); // 4. service the hub
    if (imu.HasNewData(BNO085Sensor::RotationVector)) {
      const Quaternion q = imu.GetLatest(BNO085Sensor::RotationVector).rotation;
      std::printf("w=%.3f x=%.3f y=%.3f z=%.3f accuracy=%u\n", q.w, q.x, q.y, q.z,
                  static_cast<unsigned>(q.accuracy));
    }
    bus.Delay(2);
  }
}

What each step does:

  1. HardwareReset() holds NRST low for 2 ms and waits 94 ms for the hub to start (datasheet figure 6-8). PS0, PS1, SA0 and BOOTN are sampled now.
  2. Begin() opens the port, starts the CEVA SH-2 library, waits for the hub's reset message and sends a reinitialise command. Nothing else works until it returns true.
  3. EnableSensor() asks the hub for a rotation vector every 10 ms (100 Hz).
  4. Update() services the SH-2 library: it reads pending transfers, decodes reports and runs your callbacks. Call it at least as often as your fastest report.

accuracy goes from 0 (unreliable) to 3 (high). Move the board through a few orientations to calibrate the magnetometer. This example is built by the host CI, so it compiles against the current headers.

If it does not work

Symptom First thing to check
Begin() returns false, GetLastError() is SH2_ERR (-1) Open() failed: bus pins, pull-ups, the address (0x4A or 0x4B)
Begin() returns false with SH2_ERR_TIMEOUT (-6) H_INTN not wired or read inverted; PS0/PS1 not both low at reset
No reports after EnableSensor() Update() not called often enough; HasNewData() checked for another sensor
accuracy stays at 0 Magnetic interference near the board; move it in a figure eight

Troubleshooting has more.

Next

  • API guide: callbacks, polling, several reports, errors.
  • Configuration: report rates and the 40 report types.
  • UART-RVC mode: orientation over a single UART line, no protocol.