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
| Sensor | 6-axis: 3-axis accelerometer + 3-axis gyroscope |
| Interface | SPI (mode 0 or mode 3) |
| SPI clock | up to 24 MHz |
| Supply input | 5 V (regulated on-board) or 3.3 V |
| Logic level | 3.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 rate | 12.5 Hz to 32 kHz |
| Gyro noise | 0.0028 °/s/√Hz |
| Accel noise | 65 µg/√Hz |
| Interrupt output | 1 × programmable (INT1) |
| Mounting | 4 × 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
| Pad | Direction | Function |
|---|---|---|
| +5V | in | Supply input. Accepts 5 V or 3.3 V. |
| GND | — | Ground. Must be shared with your microcontroller. |
Right edge — SPI and interrupt
| Pad | Direction | Function |
|---|---|---|
| SCK | in | SPI clock |
| MOSI | in | Data from your MCU to the board |
| MISO | out | Data from the board to your MCU |
| CS | in | Chip select, active low. Held high on-board, so the sensor is deselected until you drive it. |
| INT1 | out | Data-ready interrupt. Optional — leave unconnected if you prefer to poll. |
3.3 V Logic — Read This First
| Your board | Direct connection? |
|---|---|
| Teensy 4.0 / 4.1 | Yes |
| ESP32 / ESP8266 | Yes |
| Raspberry Pi Pico | Yes |
| Arduino Due / Zero / Nano 33 | Yes |
| 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 pad | Teensy 4.0 pin |
|---|---|
| +5V | 5V (or 3.3V) |
| GND | GND |
| SCK | 13 |
| MOSI | 11 |
| MISO | 12 |
| CS | 10 |
| INT1 | 9 |
Arduino (3.3 V boards, Uno pin layout)
| Board pad | Arduino pin |
|---|---|
| +5V | 3.3V or 5V |
| GND | GND |
| SCK | 13 |
| MOSI | 11 |
| MISO | 12 |
| CS | 10 |
| INT1 | 2 |
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.
#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.
#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
| Range | Divide raw value by |
|---|---|
| ±2 g | 16384 |
| ±4 g | 8192 |
| ±8 g | 4096 |
| ±16 g | 2048 |
A stationary board reads about 1 g on whichever axis points up. That is gravity, not motion — it never goes away.
Gyroscope
| Range | Divide raw value by |
|---|---|
| ±2000 °/s | 16.4 |
| ±1000 °/s | 32.8 |
| ±500 °/s | 65.5 |
| ±250 °/s | 131 |
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
| Symptom | Likely cause |
|---|---|
| ID check reads 0x00 | MISO not connected, or the board has no power |
| ID check reads 0xFF | MOSI or SCK not connected |
| ID check reads garbage that changes each run | SPI clock too fast for your wiring — drop to 1 MHz and lengthen from there |
| Nothing works on an Arduino Uno | 5 V logic. A level shifter is required. |
| All readings stuck at zero | Power mode never enabled — the sensor starts asleep |
| Z axis reads −1 g instead of +1 g | Board is upside down. Normal. |
| Gyro drifts even when still | Bias. Average 1000 samples at startup and subtract. |
| Readings jump wildly near motors | Vibration through the mount, or supply noise. Soft-mount the board and keep its wires away from motor leads. |
| INT1 never fires | Pin does not support interrupts on that board, or INT1 is unconnected |
| Data looks fine but the rate is low | Serial 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.