6-Axis IMU Breakout — SPI


A high-performance motion sensing board on a 7-pad SPI interface. Feed it 5 V or 3.3 V, connect four SPI lines, and read calibrated acceleration and rotation at up to 32 kHz.

Built by Navi3D for flight controllers, stabilised gimbals, balancing robots and motion capture rigs.

What This Board Does

Specifications

Sensor6-axis: 3-axis accelerometer + 3-axis gyroscope
InterfaceSPI (mode 0 or mode 3)
SPI clockup to 24 MHz
Supply input5 V (regulated on-board) or 3.3 V
Logic level3.3 V only — see the warning below
Accelerometer range±2 g / ±4 g / ±8 g / ±16 g, selectable
Gyroscope range±15.6 to ±2000 °/s, selectable
Output data rate12.5 Hz to 32 kHz
Gyro noise0.0028 °/s/√Hz
Accel noise65 µg/√Hz
Interrupt output1 × programmable (INT1)
Mounting4 × 2 mm holes, one at each corner

Pinout

Seven pads, all labelled on the silkscreen. Power on the left edge, signals on the right.

Left edge — power

PadDirectionFunction
+5VinSupply input. Accepts 5 V or 3.3 V.
GNDGround. Must be shared with your microcontroller.

Right edge — SPI and interrupt

PadDirectionFunction
SCKinSPI clock
MOSIinData from your MCU to the board
MISOoutData from the board to your MCU
CSinChip select, active low. Held high on-board, so the sensor is deselected until you drive it.
INT1outData-ready interrupt. Optional — leave unconnected if you prefer to poll.

3.3 V Logic — Read This First

Your boardDirect connection?
Teensy 4.0 / 4.1Yes
ESP32 / ESP8266Yes
Raspberry Pi PicoYes
Arduino Due / Zero / Nano 33Yes
STM32 "Blue Pill"Yes
Arduino Uno / Mega / Nano (classic)No — use a level shifter

Classic 5 V Arduinos drive their SPI pins at 5 V. Put a 4-channel bidirectional level shifter between the MCU and this board, or switch to a 3.3 V board. Driving the signal pads at 5 V will damage the sensor.

Wiring

Teensy 4.0

Board padTeensy 4.0 pin
+5V5V (or 3.3V)
GNDGND
SCK13
MOSI11
MISO12
CS10
INT19

Arduino (3.3 V boards, Uno pin layout)

Board padArduino pin
+5V3.3V or 5V
GNDGND
SCK13
MOSI11
MISO12
CS10
INT12

On boards where SPI lives on the ICSP header rather than D11–D13, use the ICSP pins and keep CS and INT1 on any free digital pins.

Example: Arduino

Polls the sensor and prints acceleration in g and rotation in degrees per second. Written against the raw SPI interface, so it needs no external library.

cpp
#include <SPI.h>

// ---- Wiring ----
const int CS_PIN   = 10;
const uint32_t SPI_HZ = 8000000;   // 8 MHz; the board handles up to 24 MHz

// ---- Registers ----
#define REG_DEVICE_CONFIG 0x11
#define REG_ACCEL_DATA_X1 0x1F
#define REG_PWR_MGMT0     0x4E
#define REG_GYRO_CONFIG0  0x4F
#define REG_ACCEL_CONFIG0 0x50
#define REG_WHO_AM_I      0x75
#define REG_BANK_SEL      0x76

// ---- Scale factors for +/-16 g and +/-2000 dps ----
const float ACCEL_LSB_PER_G   = 2048.0f;
const float GYRO_LSB_PER_DPS  = 16.4f;

SPISettings spiCfg(SPI_HZ, MSBFIRST, SPI_MODE0);

void writeReg(uint8_t reg, uint8_t value) {
  SPI.beginTransaction(spiCfg);
  digitalWrite(CS_PIN, LOW);
  SPI.transfer(reg & 0x7F);        // MSB low = write
  SPI.transfer(value);
  digitalWrite(CS_PIN, HIGH);
  SPI.endTransaction();
}

uint8_t readReg(uint8_t reg) {
  SPI.beginTransaction(spiCfg);
  digitalWrite(CS_PIN, LOW);
  SPI.transfer(reg | 0x80);        // MSB high = read
  uint8_t value = SPI.transfer(0x00);
  digitalWrite(CS_PIN, HIGH);
  SPI.endTransaction();
  return value;
}

void readBurst(uint8_t reg, uint8_t *buf, uint8_t len) {
  SPI.beginTransaction(spiCfg);
  digitalWrite(CS_PIN, LOW);
  SPI.transfer(reg | 0x80);
  for (uint8_t i = 0; i < len; i++) buf[i] = SPI.transfer(0x00);
  digitalWrite(CS_PIN, HIGH);
  SPI.endTransaction();
}

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 3000) { }

  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH);
  SPI.begin();
  delay(100);

  writeReg(REG_BANK_SEL, 0x00);       // make sure we are in bank 0
  writeReg(REG_DEVICE_CONFIG, 0x01);  // soft reset
  delay(50);

  uint8_t id = readReg(REG_WHO_AM_I);
  Serial.print("WHO_AM_I: 0x");
  Serial.println(id, HEX);
  if (id != 0x47) {
    Serial.println("Sensor not found. Check wiring, CS pin and 3.3 V logic.");
    while (1) { }
  }

  writeReg(REG_GYRO_CONFIG0,  0x06);  // +/-2000 dps, 1 kHz output rate
  writeReg(REG_ACCEL_CONFIG0, 0x06);  // +/-16 g,     1 kHz output rate
  writeReg(REG_PWR_MGMT0,     0x0F);  // accel + gyro, low-noise mode
  delay(50);                          // gyro needs ~45 ms to stabilise

  Serial.println("ax(g)\tay(g)\taz(g)\tgx(dps)\tgy(dps)\tgz(dps)");
}

void loop() {
  uint8_t raw[12];
  readBurst(REG_ACCEL_DATA_X1, raw, 12);

  int16_t ax = (int16_t)((raw[0]  << 8) | raw[1]);
  int16_t ay = (int16_t)((raw[2]  << 8) | raw[3]);
  int16_t az = (int16_t)((raw[4]  << 8) | raw[5]);
  int16_t gx = (int16_t)((raw[6]  << 8) | raw[7]);
  int16_t gy = (int16_t)((raw[8]  << 8) | raw[9]);
  int16_t gz = (int16_t)((raw[10] << 8) | raw[11]);

  Serial.print(ax / ACCEL_LSB_PER_G, 3);  Serial.print('\t');
  Serial.print(ay / ACCEL_LSB_PER_G, 3);  Serial.print('\t');
  Serial.print(az / ACCEL_LSB_PER_G, 3);  Serial.print('\t');
  Serial.print(gx / GYRO_LSB_PER_DPS, 2); Serial.print('\t');
  Serial.print(gy / GYRO_LSB_PER_DPS, 2); Serial.print('\t');
  Serial.println(gz / GYRO_LSB_PER_DPS, 2);

  delay(50);
}

Hold the board flat and still. The Z acceleration should read close to +1.000 g and all three gyro axes close to 0.00 °/s.

Example: Teensy 4.0 with Data-Ready Interrupt

This is where the INT1 pin earns its place. Instead of guessing when to read, the board tells you. The loop stays free and every sample you get is fresh and complete.

Runs at 1 kHz and reports the true sample rate once a second so you can confirm nothing is being dropped.

cpp
#include <SPI.h>

// ---- Wiring ----
const int CS_PIN   = 10;
const int INT1_PIN = 9;
const uint32_t SPI_HZ = 16000000;   // Teensy 4.0 handles 16 MHz comfortably

// ---- Registers ----
#define REG_DEVICE_CONFIG 0x11
#define REG_INT_CONFIG    0x14
#define REG_TEMP_DATA1    0x1D
#define REG_ACCEL_DATA_X1 0x1F
#define REG_PWR_MGMT0     0x4E
#define REG_GYRO_CONFIG0  0x4F
#define REG_ACCEL_CONFIG0 0x50
#define REG_INT_CONFIG1   0x64
#define REG_INT_SOURCE0   0x65
#define REG_WHO_AM_I      0x75
#define REG_BANK_SEL      0x76

const float ACCEL_LSB_PER_G  = 2048.0f;
const float GYRO_LSB_PER_DPS = 16.4f;

SPISettings spiCfg(SPI_HZ, MSBFIRST, SPI_MODE0);

volatile bool dataReady = false;
volatile uint32_t sampleCount = 0;

void onDataReady() {
  dataReady = true;
  sampleCount++;
}

void writeReg(uint8_t reg, uint8_t value) {
  SPI.beginTransaction(spiCfg);
  digitalWriteFast(CS_PIN, LOW);
  SPI.transfer(reg & 0x7F);
  SPI.transfer(value);
  digitalWriteFast(CS_PIN, HIGH);
  SPI.endTransaction();
}

uint8_t readReg(uint8_t reg) {
  SPI.beginTransaction(spiCfg);
  digitalWriteFast(CS_PIN, LOW);
  SPI.transfer(reg | 0x80);
  uint8_t value = SPI.transfer(0x00);
  digitalWriteFast(CS_PIN, HIGH);
  SPI.endTransaction();
  return value;
}

void readBurst(uint8_t reg, uint8_t *buf, uint8_t len) {
  SPI.beginTransaction(spiCfg);
  digitalWriteFast(CS_PIN, LOW);
  SPI.transfer(reg | 0x80);
  for (uint8_t i = 0; i < len; i++) buf[i] = SPI.transfer(0x00);
  digitalWriteFast(CS_PIN, HIGH);
  SPI.endTransaction();
}

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 3000) { }

  pinMode(CS_PIN, OUTPUT);
  digitalWriteFast(CS_PIN, HIGH);
  pinMode(INT1_PIN, INPUT);
  SPI.begin();
  delay(100);

  writeReg(REG_BANK_SEL, 0x00);
  writeReg(REG_DEVICE_CONFIG, 0x01);   // soft reset
  delay(50);

  uint8_t id = readReg(REG_WHO_AM_I);
  Serial.printf("WHO_AM_I: 0x%02X\n", id);
  if (id != 0x47) {
    Serial.println("Sensor not found. Check wiring and the CS pin.");
    while (1) { }
  }

  writeReg(REG_GYRO_CONFIG0,  0x06);   // +/-2000 dps, 1 kHz
  writeReg(REG_ACCEL_CONFIG0, 0x06);   // +/-16 g,     1 kHz

  writeReg(REG_INT_CONFIG,  0x03);     // INT1 push-pull, active high, pulsed
  writeReg(REG_INT_CONFIG1, 0x00);     // required for output rates below 4 kHz
  writeReg(REG_INT_SOURCE0, 0x08);     // route data-ready to INT1

  writeReg(REG_PWR_MGMT0, 0x0F);       // accel + gyro, low-noise mode
  delay(50);

  attachInterrupt(digitalPinToInterrupt(INT1_PIN), onDataReady, RISING);
  Serial.println("Streaming.");
}

void loop() {
  static uint32_t lastReport = 0;
  static float ax, ay, az, gx, gy, gz, tempC;

  if (dataReady) {
    dataReady = false;

    uint8_t raw[14];
    readBurst(REG_TEMP_DATA1, raw, 14);   // temperature first, then accel, then gyro

    int16_t t  = (int16_t)((raw[0]  << 8) | raw[1]);
    int16_t rx = (int16_t)((raw[2]  << 8) | raw[3]);
    int16_t ry = (int16_t)((raw[4]  << 8) | raw[5]);
    int16_t rz = (int16_t)((raw[6]  << 8) | raw[7]);
    int16_t px = (int16_t)((raw[8]  << 8) | raw[9]);
    int16_t py = (int16_t)((raw[10] << 8) | raw[11]);
    int16_t pz = (int16_t)((raw[12] << 8) | raw[13]);

    tempC = (t / 132.48f) + 25.0f;
    ax = rx / ACCEL_LSB_PER_G;
    ay = ry / ACCEL_LSB_PER_G;
    az = rz / ACCEL_LSB_PER_G;
    gx = px / GYRO_LSB_PER_DPS;
    gy = py / GYRO_LSB_PER_DPS;
    gz = pz / GYRO_LSB_PER_DPS;
  }

  if (millis() - lastReport >= 1000) {
    lastReport = millis();
    noInterrupts();
    uint32_t rate = sampleCount;
    sampleCount = 0;
    interrupts();

    Serial.printf("%6.3f %6.3f %6.3f g | %8.2f %8.2f %8.2f dps | %5.1f C | %lu Hz\n",
                  ax, ay, az, gx, gy, gz, tempC, rate);
  }
}

The reported rate should sit at roughly 1000 Hz. A much lower number means interrupts are being missed — check that INT1 is on a pin your board supports for interrupts.

Understanding the Readings

The sensor returns 16-bit signed integers. Divide by the scale factor for your chosen range.

Accelerometer

RangeDivide raw value by
±2 g16384
±4 g8192
±8 g4096
±16 g2048

A stationary board reads about 1 g on whichever axis points up. That is gravity, not motion — it never goes away.

Gyroscope

RangeDivide raw value by
±2000 °/s16.4
±1000 °/s32.8
±500 °/s65.5
±250 °/s131

A stationary board reads close to zero, with a small constant offset called bias. Measure that offset once at startup while the board is still, then subtract it from every reading.

Picking a range

Smaller range means finer resolution but earlier clipping. A balancing robot rarely exceeds 500 °/s, so ±500 gives four times the resolution of ±2000. A quadcopter doing flips will saturate anything below ±2000.

Mounting

Four 2 mm holes, one at each corner, for M2 hardware.

Troubleshooting

SymptomLikely cause
ID check reads 0x00MISO not connected, or the board has no power
ID check reads 0xFFMOSI or SCK not connected
ID check reads garbage that changes each runSPI clock too fast for your wiring — drop to 1 MHz and lengthen from there
Nothing works on an Arduino Uno5 V logic. A level shifter is required.
All readings stuck at zeroPower mode never enabled — the sensor starts asleep
Z axis reads −1 g instead of +1 gBoard is upside down. Normal.
Gyro drifts even when stillBias. Average 1000 samples at startup and subtract.
Readings jump wildly near motorsVibration through the mount, or supply noise. Soft-mount the board and keep its wires away from motor leads.
INT1 never firesPin does not support interrupts on that board, or INT1 is unconnected
Data looks fine but the rate is lowSerial printing is the bottleneck, not the sensor. Print less often.

Libraries and Further Reading

The examples above talk to the sensor directly and need no library. If you prefer a higher-level driver, this board uses the ICM-42688-P sensor — any Arduino or Teensy library for that part will work, configured for SPI mode.

Support

Questions about this board, or something not behaving as described here — get in touch through navi3d.in.