Skip to main content

Arduino Nano 33 Sense Rev2- IMU sensor

What is the OLEDIMU display?sensor?

Library

Ano OLEDuse displaythe IMU (Organicinertial Light-Emittingmeasurement Diodeunit) display)in isNano a33 screenBLE technology where each pixel emits its own light using organic carbon-based materials. Unlike LCDs, OLEDs do not need a backlight, which allows them to produce deeper blacks, higher contrast,Rev2 and thinner,Nano more33 flexibleBLE screens.

Sense

IfRev2, you wantneed to use the displayArduino_BMI270_BMM150 forlibrary animation, please reference this tutorial.

3.3V Logic
This OLED displays have a logic levelinstead of 3.3V.Arduino_LSM9DS1 If you use any classic Arduino (UNO and Leonardo) with 5V logic, the display may misbehave. 3.3V Arduino includes Due, Zero, Nano 33 and MKR series. In this tutorial, we will be using Nano 33 Sense Rev2.

OLED vs LCD

<del class="diffmod">OLED vs LCD Comparison</del> all
FeatureOLEDLCD (TFT / IPS)
Light sourceSelf-emissive: each pixel emits its own light (organic diodes).Requires an external backlight (usually LED) that passes through liquid crystals.
Black levelPerfect blacks — pixels can turn completely off.Blacks are grayish due to backlight bleed; improved in VA panels but not perfect.
Contrast ratioExtremely high (practically infinite for pixel-off blacks).Lower than OLED; good on high-quality IPS/VA but limited by backlight.
Viewing anglesVery wide — colors and brightness remain consistent at off-angles.IPS: wide viewing angles; TN: narrow. Overall varies by LCD type.
Response timeVery fast — excellent for motion and low input lag.Slower than OLED on average; IPSwhich is better than TN for response consistency.
Power consumptionEfficient for darker content (black areas consume no power); bright scenes use more power.Backlight is constant — power more consistent and sometimes better for bright full-screen content.
Thickness & flexibilityVery thin; can be made flexible/curved (used in foldables). Thickerexample duecode.

to

Replace backlight#include and layers; rigid though some thin, curved LCDs exist.

Burn-in & image retentionRisk of permanent burn-in / image retention<Arduino_LSM9DS1.h> with static#include content<Arduino_BMI270_BMM150.h> over long periods.No burn-in (generallyall safeexisting codes if you are using Rev2 boards.

For more information about libraries for static UI elements); temporary image retention very rare.

LifespanOrganic materials can degrade (blue subpixel often ages faster) — lifespan improving with tech.Typically longer stable lifespan for backlight and LCD stack; LED backlight replacement possible.
Brightness in sunlightCan be less visible than the very brightest LCDs; high-end OLEDs mitigate this with strong peak brightness.LCDs can reach higher sustained brightness, giving better legibility in direct sunlight.
Color & HDRVibrant colors and excellent HDR highlights due to per-pixel control; deep blacks improve perceived dynamic range.Good color (especially wide-gamut IPS); HDR performance depends on backlight local dimming (FALD/mini-LED).
Typical costGenerally more expensive (premium devices), though prices are falling.Usually more affordable across many sizes and use-cases.
Common usesSmartphones, premium TVs, smartwatches, VR headsets, foldable devices, high-end laptops.Monitors, budget-to-midrange TVs, laptops, industrial displays, large-size TVs (including mini-LED LCDs).
Best forBest black levels, contrast, thin/flexible designs, and immersive multimedia where perfect blacks matter.Best where cost, high sustained brightness, and static UI longevity are priorities (work monitors, outdoor screens).

Wiring

Wiring up the sensor is simple:

  1. VCC to 3.3V
  2. GND to GND
  3. SCL to SPI CLock Pin (Pin 13 on NanoNANO 33 Sense)
  4. SENSE
  5. SDAREV2, toplease SPI MOSI Pin (Pin 11 on Nano 33 Sense)
  6. RST to Pin 4
  7. DC to Pin 5
  8. CS to Pin 6

Library

To use this code you will need thevisit Adafruit_SH110X Libraryhere. We have a tutorial on how to install a library here.

Getting started

This code is modified from the library example code SH1106_128x64_SPi_QTPY.

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>


#define OLED_MOSI     11
#define OLED_CLK      13
#define OLED_DC       5
#define OLED_CS       6
#define OLED_RST      4

Adafruit_SH1106G display = Adafruit_SH1106G(128, 64,OLED_MOSI, OLED_CLK, OLED_DC, OLED_RST, OLED_CS);

#define NUMFLAKES 10
#define XPOS 0
#define YPOS 1
#define DELTAY 2


#define LOGO16_GLCD_HEIGHT 16
#define LOGO16_GLCD_WIDTH  16
static const unsigned char PROGMEM logo16_glcd_bmp[] =
{ B00000000, B11000000,
  B00000001, B11000000,
  B00000001, B11000000,
  B00000011, B11100000,
  B11110011, B11100000,
  B11111110, B11111000,
  B01111110, B11111111,
  B00110011, B10011111,
  B00011111, B11111100,
  B00001101, B01110000,
  B00011011, B10100000,
  B00111111, B11100000,
  B00111111, B11110000,
  B01111100, B11110000,
  B01110000, B01110000,
  B00000000, B00110000
};


void setup()   {
  Serial.begin(9600);

  //display.setContrast (0); // dim display

  // Start OLED
  display.begin(0, true); // we dont use the i2c address but we will reset!


  // Show image buffer on the display hardware.
  // Since the buffer is intialized with an Adafruit splashscreen
  // internally, this will display the splashscreen.
  display.display();
  delay(2000);

  // Clear the buffer.
  display.clearDisplay();

  // draw a single pixel
  display.drawPixel(10, 10, SH110X_WHITE);
  // Show the display buffer on the hardware.
  // NOTE: You _must_ call display after making any drawing commands
  // to make them visible on the display hardware!
  display.display();
  delay(2000);
  display.clearDisplay();

  // draw many lines
  testdrawline();
  display.display();
  delay(2000);
  display.clearDisplay();

  // draw rectangles
  testdrawrect();
  display.display();
  delay(2000);
  display.clearDisplay();

  // draw multiple rectangles
  testfillrect();
  display.display();
  delay(2000);
  display.clearDisplay();

  // draw mulitple circles
  testdrawcircle();
  display.display();
  delay(2000);
  display.clearDisplay();

  // draw a SH110X_WHITE circle, 10 pixel radius
  display.fillCircle(display.width() / 2, display.height() / 2, 10, SH110X_WHITE);
  display.display();
  delay(2000);
  display.clearDisplay();

  testdrawroundrect();
  delay(2000);
  display.clearDisplay();

  testfillroundrect();
  delay(2000);
  display.clearDisplay();

  testdrawtriangle();
  delay(2000);
  display.clearDisplay();

  testfilltriangle();
  delay(2000);
  display.clearDisplay();

  // draw the first ~12 characters in the font
  testdrawchar();
  display.display();
  delay(2000);
  display.clearDisplay();


  // text display tests
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(0, 0);
  display.println("Failure is always an option");
  display.setTextColor(SH110X_BLACK, SH110X_WHITE); // 'inverted' text
  display.println(3.141592);
  display.setTextSize(2);
  display.setTextColor(SH110X_WHITE);
  display.print("0x"); display.println(0xDEADBEEF, HEX);
  display.display();
  delay(2000);
  display.clearDisplay();

  // miniature bitmap display
  display.drawBitmap(30, 16,  logo16_glcd_bmp, 16, 16, 1);
  display.display();
  delay(1);

  // invert the display
  display.invertDisplay(true);
  delay(1000);
  display.invertDisplay(false);
  delay(1000);
  display.clearDisplay();

  // draw a bitmap icon and 'animate' movement
  testdrawbitmap(logo16_glcd_bmp, LOGO16_GLCD_HEIGHT, LOGO16_GLCD_WIDTH);
}


void loop() {

}


void testdrawbitmap(const uint8_t *bitmap, uint8_t w, uint8_t h) {
  uint8_t icons[NUMFLAKES][3];

  // initialize
  for (uint8_t f = 0; f < NUMFLAKES; f++) {
    icons[f][XPOS] = random(display.width());
    icons[f][YPOS] = 0;
    icons[f][DELTAY] = random(5) + 1;

    Serial.print("x: ");
    Serial.print(icons[f][XPOS], DEC);
    Serial.print(" y: ");
    Serial.print(icons[f][YPOS], DEC);
    Serial.print(" dy: ");
    Serial.println(icons[f][DELTAY], DEC);
  }

  while (1) {
    // draw each icon
    for (uint8_t f = 0; f < NUMFLAKES; f++) {
      display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SH110X_WHITE);
    }
    display.display();
    delay(200);

    // then erase it + move it
    for (uint8_t f = 0; f < NUMFLAKES; f++) {
      display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SH110X_BLACK);
      // move it
      icons[f][YPOS] += icons[f][DELTAY];
      // if its gone, reinit
      if (icons[f][YPOS] > display.height()) {
        icons[f][XPOS] = random(display.width());
        icons[f][YPOS] = 0;
        icons[f][DELTAY] = random(5) + 1;
      }
    }
  }
}


void testdrawchar(void) {
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(0, 0);

  for (uint8_t i = 0; i < 168; i++) {
    if (i == '\n') continue;
    display.write(i);
    if ((i > 0) && (i % 21 == 0))
      display.println();
  }
  display.display();
  delay(1);
}

void testdrawcircle(void) {
  for (int16_t i = 0; i < display.height(); i += 2) {
    display.drawCircle(display.width() / 2, display.height() / 2, i, SH110X_WHITE);
    display.display();
    delay(1);
  }
}

void testfillrect(void) {
  uint8_t color = 1;
  for (int16_t i = 0; i < display.height() / 2; i += 3) {
    // alternate colors
    display.fillRect(i, i, display.width() - i * 2, display.height() - i * 2, color % 2);
    display.display();
    delay(1);
    color++;
  }
}

void testdrawtriangle(void) {
  for (int16_t i = 0; i < min(display.width(), display.height()) / 2; i += 5) {
    display.drawTriangle(display.width() / 2, display.height() / 2 - i,
                         display.width() / 2 - i, display.height() / 2 + i,
                         display.width() / 2 + i, display.height() / 2 + i, SH110X_WHITE);
    display.display();
    delay(1);
  }
}

void testfilltriangle(void) {
  uint8_t color = SH110X_WHITE;
  for (int16_t i = min(display.width(), display.height()) / 2; i > 0; i -= 5) {
    display.fillTriangle(display.width() / 2, display.height() / 2 - i,
                         display.width() / 2 - i, display.height() / 2 + i,
                         display.width() / 2 + i, display.height() / 2 + i, SH110X_WHITE);
    if (color == SH110X_WHITE) color = SH110X_BLACK;
    else color = SH110X_WHITE;
    display.display();
    delay(1);
  }
}

void testdrawroundrect(void) {
  for (int16_t i = 0; i < display.height() / 2 - 2; i += 2) {
    display.drawRoundRect(i, i, display.width() - 2 * i, display.height() - 2 * i, display.height() / 4, SH110X_WHITE);
    display.display();
    delay(1);
  }
}

void testfillroundrect(void) {
  uint8_t color = SH110X_WHITE;
  for (int16_t i = 0; i < display.height() / 2 - 2; i += 2) {
    display.fillRoundRect(i, i, display.width() - 2 * i, display.height() - 2 * i, display.height() / 4, color);
    if (color == SH110X_WHITE) color = SH110X_BLACK;
    else color = SH110X_WHITE;
    display.display();
    delay(1);
  }
}

void testdrawrect(void) {
  for (int16_t i = 0; i < display.height() / 2; i += 2) {
    display.drawRect(i, i, display.width() - 2 * i, display.height() - 2 * i, SH110X_WHITE);
    display.display();
    delay(1);
  }
}

void testdrawline() {
  for (int16_t i = 0; i < display.width(); i += 4) {
    display.drawLine(0, 0, i, display.height() - 1, SH110X_WHITE);
    display.display();
    delay(1);
  }
  for (int16_t i = 0; i < display.height(); i += 4) {
    display.drawLine(0, 0, display.width() - 1, i, SH110X_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();
  for (int16_t i = 0; i < display.width(); i += 4) {
    display.drawLine(0, display.height() - 1, i, 0, SH110X_WHITE);
    display.display();
    delay(1);
  }
  for (int16_t i = display.height() - 1; i >= 0; i -= 4) {
    display.drawLine(0, display.height() - 1, display.width() - 1, i, SH110X_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();
  for (int16_t i = display.width() - 1; i >= 0; i -= 4) {
    display.drawLine(display.width() - 1, display.height() - 1, i, 0, SH110X_WHITE);
    display.display();
    delay(1);
  }
  for (int16_t i = display.height() - 1; i >= 0; i -= 4) {
    display.drawLine(display.width() - 1, display.height() - 1, 0, i, SH110X_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();
  for (int16_t i = 0; i < display.height(); i += 4) {
    display.drawLine(display.width() - 1, 0, 0, i, SH110X_WHITE);
    display.display();
    delay(1);
  }
  for (int16_t i = 0; i < display.width(); i += 4) {
    display.drawLine(display.width() - 1, 0, i, display.height() - 1, SH110X_WHITE);
    display.display();
    delay(1);
  }
  delay(250);
}

Something fun

Nano 33 Sense Rev2 has built-in IMU sensor, temperature and humidity sensor and microphone. The code below will have a ball displayed on the OLED to follow Nano 33's motion.

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <Arduino_BMI270_BMM150.h>
#include <math.h>

// OLED SPI pins
#define OLED_MOSI     11
#define OLED_CLK      13
#define OLED_DC       5
#define OLED_CS       6
#define OLED_RST      4

Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RST, OLED_CS);

// -------- TUNING --------
const int W = 128;
const int H = 64;
const int BALL_RADIUS = 6;

// How strongly linear accel (in g or m/s^2 units) maps to pixels/sec^2.
// Increase to make ball accelerate more for the same hand motion.
const float LIN_ACCEL_TO_PIX = 220.0f;

// Low-pass alpha used to estimate gravity from accel: gravity = LPF(accel).
// Lower alpha -> smoother/slow gravity estimate. Typical 0.01..0.05
const float GRAV_LPF_ALPHA = 0.02f;

// Ignore very small linear accelerations (g) to reduce jitter
const float LINEAR_DEADZONE = 0.02f; // ~0.02 g

// Friction & bounce
const float FRICTION = 0.98f;      // per-frame damping factor (closer to 1 = less friction)
const float BOUNCE_DAMP = 0.6f;    // velocity retained after hitting wall

// Loop timing
const unsigned long LOOP_DELAY_MS = 5;

float gravX = 0.0f, gravY = 0.0f, gravZ = 0.0f; // gravity estimate (same units as IMU accel)
float vx = 0.0f, vy = 0.0f;   // velocity in pixels/sec
float px = W/2.0f, py = H/2.0f; // position in pixels

unsigned long lastMillis = 0;

void setup() {
  Serial.begin(115200);

  display.begin(0, true);
  display.clearDisplay();
  display.display();

  if (!IMU.begin()) {
    Serial.println("Failed to initialize IMU!");
    while (1);
  }

  // Let sensor settle, sample a bit for initial gravity
  delay(200);
  // Initialize gravity estimate with a few samples
  for (int i=0; i<50; ++i) {
    if (IMU.accelerationAvailable()) {
      float ax, ay, az;
      IMU.readAcceleration(ax, ay, az);
      if (i==0) { gravX = ax; gravY = ay; gravZ = az; }
      else {
        gravX = gravX + GRAV_LPF_ALPHA * (ax - gravX);
        gravY = gravY + GRAV_LPF_ALPHA * (ay - gravY);
        gravZ = gravZ + GRAV_LPF_ALPHA * (az - gravZ);
      }
    }
    delay(5);
  }

  lastMillis = millis();

  display.clearDisplay();
  display.fillCircle((int)round(px), (int)round(py), BALL_RADIUS, SH110X_WHITE);
  display.display();
}

void loop() {
  unsigned long now = millis();
  float dt = (now - lastMillis) / 1000.0f;
  if (dt <= 0.0f) dt = 0.001f;
  lastMillis = now;

  float ax = 0.0f, ay = 0.0f, az = 0.0f;
  if (IMU.accelerationAvailable()) {
    IMU.readAcceleration(ax, ay, az);
  }

  // Update gravity estimate with LPF: grav = grav + alpha*(acc - grav)
  gravX = gravX + GRAV_LPF_ALPHA * (ax - gravX);
  gravY = gravY + GRAV_LPF_ALPHA * (ay - gravY);
  gravZ = gravZ + GRAV_LPF_ALPHA * (az - gravZ);

  // Linear acceleration = measured - gravity (in sensor units)
  float linX = ax - gravX;
  float linY = ay - gravY;
  float linZ = az - gravZ; // not used here but available

  // Deadzone to reduce small noise
  if (fabs(linX) < LINEAR_DEADZONE) linX = 0;
  if (fabs(linY) < LINEAR_DEADZONE) linY = 0;

  // Map linear accel to pixels/sec^2
  // Note: choose mapping so motion of device moves the ball in the same direction.
  // You might need to invert signs depending on your mounting/orientation.
  //
  // Common mapping (OLED on top of Nano 33, X axis left-right, Y axis forward-back):
  //   linX -> px (left/right)
  //   linY -> py (up/down)
  //
  // If the ball moves opposite to the board, invert sign(s) below.
  float ax_pixels = linX * LIN_ACCEL_TO_PIX;
  float ay_pixels = linY * LIN_ACCEL_TO_PIX;

  // Integrate velocity (vx, vy) using accel (pixels/sec^2)
  vx += ax_pixels * dt;
  vy += ay_pixels * dt;

  // Apply friction (scaled by dt so behaviour is consistent with loop time)
  float frictionScaled = pow(FRICTION, dt * 60.0f);
  vx *= frictionScaled;
  vy *= frictionScaled;

  // Integrate position
  px += vx * dt;
  py += vy * dt;

  // Keep inside bounds (account for radius)
  if (px < BALL_RADIUS) {
    px = BALL_RADIUS;
    if (vx < 0) vx = -vx * BOUNCE_DAMP;
  } else if (px > W - 1 - BALL_RADIUS) {
    px = W - 1 - BALL_RADIUS;
    if (vx > 0) vx = -vx * BOUNCE_DAMP;
  }

  if (py < BALL_RADIUS) {
    py = BALL_RADIUS;
    if (vy < 0) vy = -vy * BOUNCE_DAMP;
  } else if (py > H - 1 - BALL_RADIUS) {
    py = H - 1 - BALL_RADIUS;
    if (vy > 0) vy = -vy * BOUNCE_DAMP;
  }

  // Render
  display.clearDisplay();
  // border
  display.drawRect(0, 0, W, H, SH110X_WHITE);
  // ball
  display.fillCircle((int)round(px), (int)round(py), BALL_RADIUS, SH110X_WHITE);
  // small velocity vector for visualization
  int vx_end_x = (int)round(px + vx * 0.02f);
  int vy_end_y = (int)round(py + vy * 0.02f);
  display.drawLine((int)round(px), (int)round(py), vx_end_x, vy_end_y, SH110X_WHITE);
  display.display();

  // Optional debug print periodically
  static unsigned long lastPrint = 0;
  if (now - lastPrint > 200) {
    Serial.print("ax: "); Serial.print(ax, 4);
    Serial.print(" ay: "); Serial.print(ay, 4);
    Serial.print(" linX: "); Serial.print(linX, 5);
    Serial.print(" linY: "); Serial.print(linY, 5);
    Serial.print(" px: "); Serial.print(px, 2);
    Serial.print(" py: "); Serial.print(py, 2);
    Serial.print(" vx: "); Serial.print(vx, 3);
    Serial.print(" vy: "); Serial.println(vy, 3);
    lastPrint = now;
  }

  delay(LOOP_DELAY_MS);
}