V Visla Hardware
Firmware / IMU Wake-on-Motion

IMU Wake-on-Motion

XIAO nRF54LM20A Sense · accelerometro → sveglia
testato al banco · 2026-07-31

L'IMU LSM6DS3TR-C rileva il movimento e alza INT1 per svegliare l'MCU dal deep sleep. Mostra le accelerazioni live + stato FERMO / MOVIMENTO sull'OLED Expansion, con contatore eventi. È la base del wake-on-motion del Visla Tag: dormire a ~4.76 µA e risvegliarsi solo quando l'oggetto si muove.

Seeed XIAO nRF54LM20A Sense Arduino · core nrf54l15clean
Componenti usati
nRF54LM20A
MCU · Cortex-M33 128 MHz
Nordic Semiconductor
LSM6DS3TR-C
IMU 6 assi (accel + gyro)
STMicroelectronics
nPM1300
PMIC · rail IMU via LDO1 3V3
Nordic Semiconductor
SSD1306
OLED 128×64 (Expansion)
imu_wake_motion.ino
216 righe GitHub
/*
 * VISLA — XIAO nRF54LM20A Sense · IMU wake-on-motion + OLED Expansion
 *
 * Dimostra la sveglia-al-movimento del Tag: l'IMU LSM6DS3TR-C rileva
 * il movimento e alza INT1; l'OLED mostra accelerazioni live, stato
 * FERMO/MOVIMENTO e un contatore di eventi. LED RGB: verde=fermo, rosso=motion.
 *
 * Hardware (dal core nrf54l15clean, variante xiao_nrf54lm20b):
 *   OLED  Expansion  -> Wire  (D4/D5, P1.03/P1.07) SSD1306 0x3C
 *   IMU   LSM6DS3TR-C-> Wire1 (P0.08 SDA / P0.07 SCL) addr 0x6A
 *   IMU   INT1        -> PIN_IMU_INT (P0.06)  [pin wake]
 *   IMU   CS          -> PIN_IMU_CS  (P3.12)  HIGH = modo I2C
 *   Rail IMU&MIC 3V3  -> nPM1300 LDO1 (va acceso via PMIC)
 */
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include "npm1300.h"

U8G2_SSD1306_128X64_NONAME_F_HW_I2C oled(U8G2_R0, U8X8_PIN_NONE);   // Wire (D4/D5)

// ---- LSM6DS3TR-C ----
static uint8_t IMU_ADDR = 0x6A;
#define REG_WHO_AM_I   0x0F
#define REG_CTRL1_XL   0x10
#define REG_CTRL3_C    0x12
#define REG_WAKE_SRC   0x1B
#define REG_OUTX_L_XL  0x28
#define REG_TAP_CFG    0x58
#define REG_WAKE_THS   0x5B
#define REG_WAKE_DUR   0x5C
#define REG_MD1_CFG    0x5E

static void imuW(uint8_t reg, uint8_t val) {
  Wire1.beginTransmission(IMU_ADDR); Wire1.write(reg); Wire1.write(val); Wire1.endTransmission();
}
static uint8_t imuR(uint8_t reg) {
  Wire1.beginTransmission(IMU_ADDR); Wire1.write(reg); Wire1.endTransmission(false);
  Wire1.requestFrom((int)IMU_ADDR, 1); return Wire1.available() ? Wire1.read() : 0;
}
static void imuReadN(uint8_t reg, uint8_t *buf, uint8_t n) {
  Wire1.beginTransmission(IMU_ADDR); Wire1.write(reg); Wire1.endTransmission(false);
  Wire1.requestFrom((int)IMU_ADDR, (int)n);
  for (uint8_t i = 0; i < n && Wire1.available(); i++) buf[i] = Wire1.read();
}

static bool     imuOk = false;
static uint8_t  whoami = 0;
static float    ax = 0, ay = 0, az = 0;
static uint32_t motionCount = 0;
static uint32_t lastMotionMs = 0;
static bool     moving = false;

static int battPct() {
  int32_t mv = npm1300_read_vbat_mv();
  if (mv <= 0) return -1;
  int p = (mv - 3300) * 100 / (4200 - 3300);
  return p < 0 ? 0 : (p > 100 ? 100 : p);
}

static bool imuBegin() {
  // CS HIGH -> forza modo I2C sull'LSM6DS3TR-C
  pinMode(PIN_IMU_CS, OUTPUT);  digitalWrite(PIN_IMU_CS, HIGH);
  pinMode(PIN_IMU_INT, INPUT);
  Wire1.begin();
  Wire1.setClock(100000);
  // scan bus IMU (Wire1) per diagnostica
  Serial.println("[imu] scan Wire1 (P0.08/P0.07):");
  int found = 0;
  for (uint8_t a = 1; a < 127; a++) {
    Wire1.beginTransmission(a);
    if (Wire1.endTransmission() == 0) { Serial.printf("  trovato 0x%02X\n", a); found++; }
  }
  Serial.printf("[imu] device trovati: %d\n", found);
  // prova 0x6A poi 0x6B
  for (uint8_t a = 0x6A; a <= 0x6B; a++) {
    IMU_ADDR = a;
    whoami = imuR(REG_WHO_AM_I);
    Serial.printf("[imu] WHO_AM_I @0x%02X = 0x%02X\n", a, whoami);
    if (whoami == 0x6A) break;   // WHO_AM_I dell'LSM6DS3TR-C = 0x6A
  }
  if (whoami != 0x6A) return false;

  imuW(REG_CTRL1_XL, 0x50);  // accel ODR 208 Hz, ±2 g
  imuW(REG_CTRL3_C,  0x44);  // BDU + IF_INC (auto-increment registri)
  // wake-up (activity) detection -> INT1
  imuW(REG_TAP_CFG,  0x90);  // INTERRUPTS_ENABLE + SLOPE_FDS (filtro HP)
  imuW(REG_WAKE_THS, 0x02);  // soglia (≈ 2 * 2g/64 ≈ 62 mg): sensibile
  imuW(REG_WAKE_DUR, 0x00);  // reazione immediata
  imuW(REG_MD1_CFG,  0x20);  // instrada WU su INT1
  return true;
}

static void imuUpdate() {
  uint8_t b[6];
  imuReadN(REG_OUTX_L_XL, b, 6);
  int16_t rx = (int16_t)(b[0] | (b[1] << 8));
  int16_t ry = (int16_t)(b[2] | (b[3] << 8));
  int16_t rz = (int16_t)(b[4] | (b[5] << 8));
  const float s = 0.000061f;   // ±2g -> 0.061 mg/LSB
  ax = rx * s; ay = ry * s; az = rz * s;

  // evento wake: pin INT1 alto O bit WU_IA nel WAKE_UP_SRC
  bool intPin = digitalRead(PIN_IMU_INT);
  uint8_t src = imuR(REG_WAKE_SRC);   // la lettura pulisce l'evento
  bool wu = intPin || (src & 0x08);   // bit3 = WU_IA
  if (wu) { motionCount++; lastMotionMs = millis(); }
  moving = (millis() - lastMotionMs) < 700;
}

static void setLed(bool motion) {
  pinMode(LED_RED, OUTPUT); pinMode(LED_GREEN, OUTPUT);
  digitalWrite(LED_RED,   motion ? LED_STATE_ON : !LED_STATE_ON);
  digitalWrite(LED_GREEN, motion ? !LED_STATE_ON : LED_STATE_ON);
}

static void draw() {
  oled.clearBuffer();
  oled.setFont(u8g2_font_6x12_tf);
  oled.drawStr(0, 9, "IMU WAKE-ON-MOTION");
  int bp = battPct();
  char t[24];
  if (bp >= 0) { snprintf(t, sizeof t, "%d%%", bp); oled.drawStr(104, 9, t); }
  oled.drawHLine(0, 12, 128);

  if (!imuOk) {
    oled.setFont(u8g2_font_7x14B_tf);
    oled.drawStr(0, 34, "IMU NON TROVATO");
    oled.setFont(u8g2_font_6x12_tf);
    snprintf(t, sizeof t, "WHO=0x%02X (att.0x6A)", whoami);
    oled.drawStr(0, 50, t);
    oled.drawStr(0, 62, "rail LDO1? Wire1?");
    oled.sendBuffer();
    return;
  }

  oled.setFont(u8g2_font_6x12_tf);
  snprintf(t, sizeof t, "ax%+.2f ay%+.2f", ax, ay); oled.drawStr(0, 26, t);
  snprintf(t, sizeof t, "az%+.2f g  WHO %02X", az, whoami); oled.drawStr(0, 38, t);
  snprintf(t, sizeof t, "eventi: %lu", (unsigned long)motionCount); oled.drawStr(0, 62, t);

  // stato grande
  oled.setFont(u8g2_font_9x15B_tf);
  if (moving) oled.drawStr(0, 54, ">> MOVIMENTO <<");
  else        oled.drawStr(0, 54, "FERMO");

  oled.sendBuffer();
}

static bool oledPresent = false;

void setup() {
  // 0) LED SUBITO: verde acceso = "vivo, in setup" (prima di qualsiasi I2C che potrebbe bloccare)
  pinMode(LED_RED, OUTPUT); pinMode(LED_GREEN, OUTPUT); pinMode(LED_BLUE, OUTPUT);
  digitalWrite(LED_RED, !LED_STATE_ON); digitalWrite(LED_BLUE, !LED_STATE_ON);
  digitalWrite(LED_GREEN, LED_STATE_ON);

  Serial.begin(115200);
  delay(300);

  // 1) accendi il rail IMU&MIC (nPM1300 LDO1 = 3.3V)
  npm1300_begin();
  npm1300_imu_mic_power_enable(true);
  npm1300_ldo1_set_mode(NPM1300_LDSW_MODE_LDO);
  npm1300_ldo1_set_voltage(NPM1300_LDO_VOLTAGE_3V3);
  npm1300_ldo1_enable(true);
  delay(150);   // rail settle + boot IMU

  // 2) OLED: verifica presenza su Wire (0x3C) — se assente NON inizializzo (evita hang)
  Wire.begin();
  Wire.setClock(400000);
  Wire.beginTransmission(0x3C);
  oledPresent = (Wire.endTransmission() == 0);
  if (oledPresent) {
    oled.setBusClock(400000);
    oled.begin();
    oled.clearBuffer();
    oled.setFont(u8g2_font_6x12_tf);
    oled.drawStr(0, 20, "VISLA Tag");
    oled.drawStr(0, 34, "IMU wake test...");
    oled.sendBuffer();
  }

  imuOk = imuBegin();
  lastMotionMs = millis();
}

void loop() {
  if (imuOk) imuUpdate();
  setLed(moving);
  if (oledPresent) draw();
  else {
    // niente OLED: stato via LED — rosso se movimento, verde-heartbeat se fermo, blu-blink se IMU KO
    static uint32_t t = 0; static bool on = false;
    if (millis() - t > 250) { t = millis(); on = !on;
      if (!imuOk)      { digitalWrite(LED_BLUE, on ? LED_STATE_ON : !LED_STATE_ON); }
      else if (!moving){ digitalWrite(LED_GREEN, on ? LED_STATE_ON : !LED_STATE_ON); }
    }
  }

  static uint32_t lastDbg = 0;
  if (millis() - lastDbg > 1500) {
    lastDbg = millis();
    uint8_t save = IMU_ADDR;
    IMU_ADDR = 0x6A; uint8_t w6a = imuR(REG_WHO_AM_I);
    IMU_ADDR = 0x6B; uint8_t w6b = imuR(REG_WHO_AM_I);
    IMU_ADDR = save;
    int found = 0;
    for (uint8_t a = 1; a < 127; a++) { Wire1.beginTransmission(a); if (Wire1.endTransmission() == 0) found++; }
    Serial.printf("[dbg] pmic=%d ldo1=%d vbat=%ld who6A=%02X who6B=%02X wire1_dev=%d imuOk=%d cs=%d int=%d\n",
                  npm1300_is_present(), npm1300_ldo1_is_enabled(), (long)npm1300_read_vbat_mv(),
                  w6a, w6b, found, imuOk, digitalRead(PIN_IMU_CS), digitalRead(PIN_IMU_INT));
  }
  delay(60);
}