@felmue Thanks again for your quick and informative reply! Your advice got me on the right track, you're a life saver!
I was able to power down the Paper S3 and have it reboot from IMU movement, which was what I wanted. My remaining challenge is that when powered down I'm still reading ~2.4mA on my USB power meter (with battery disconnected). I'm not sure what could still be drawing, from what I'm reading the BMI270 and PMS150 should be <1mA together? If you have any insight on getting this power draw down please let me know.
For anyone else that's trying to do this, I'm attaching a working example for the Paper S3. It uses the Bosch BMI270 API. To run the below example, copy/paste the example code below into the Arduino IDE and save it. Then copy bmi2.h, bmi2.c, bmi2_defs.h, bmi270.c, and bmi270.h from the Bosch API into the directory with your saved example script. It will look something like this:
ffad1dfe-2359-423f-892c-9a0d7f347750-image.png
Then flash the example code to your Paper S3. You should see the S3 device boot up and then power down, showing this on the screen. If you move the device you'll hear it wake up (if you have USB audio alerts you'll hear it reconnect to your computer to you know it worked!)
b4074a01-042d-47b0-91ab-f93d32bb812b-image.png
Example code:
/**
* PaperS3 + BMI270 "Move-to-Reboot" Demo
*
* Motion-activated power-on using M5Stack PaperS3 with Bosch BMI270 IMU.
* Device powers off and wakes up via motion detection through BMI270 any-motion interrupt.
*
* Hardware: M5Stack PaperS3
* Authors: Lex Kravitz and Cursor AI, 2025
*/
// Include required libraries
#include <M5Unified.h> // M5Stack unified framework for PaperS3
#include <Wire.h> // Arduino I2C communication
#include "bmi2.h" // Bosch BMI2 base driver
#include "bmi270.h" // Bosch BMI270 specific driver
// PaperS3 I2C pin configuration
static const int I2C_SDA = 41; // I2C Data pin
static const int I2C_SCL = 42; // I2C Clock pin
static const uint32_t I2C_FREQ = 400000; // I2C frequency (400kHz)
// BMI270 I2C address configuration
static const uint8_t BMI270_I2C_ADDR = 0x68;
// Any-motion detection sensitivity configuration - control the sensitivity of motion detection
static const uint8_t ANYMOTION_THRESHOLD = 50; // Threshold in Bosch units (~0.1 g)
static const uint8_t ANYMOTION_DURATION = 2; // Duration in samples at current ODR
// Global Bosch driver device object
static bmi2_dev dev;
// Display functions
void initDisplay() {
M5.Display.clear();
M5.Display.setTextColor(BLACK, WHITE);
M5.Display.setFont(&fonts::Font4);
M5.Display.setRotation(1);
M5.Display.setTextDatum(middle_center);
}
void showScreen(const String& title, const String& status) {
M5.Display.drawString("PAPER S3", M5.Display.width()/2, 100);
M5.Display.drawString(title, M5.Display.width()/2, 160);
M5.Display.drawString(status, M5.Display.width()/2, 220);
M5.Display.display();
}
// Bosch BMI270 API glue functions
static int8_t i2c_read(uint8_t reg_addr, uint8_t* data, uint32_t len, void* intf_ptr) {
uint8_t addr = (uint8_t)(uintptr_t)intf_ptr;
Wire.beginTransmission(addr);
Wire.write(reg_addr);
if (Wire.endTransmission(false) != 0) return -1;
Wire.requestFrom(addr, (uint8_t)len);
for (uint32_t i = 0; i < len; ++i) {
if (!Wire.available()) return -1;
data[i] = Wire.read();
}
return 0;
}
static int8_t i2c_write(uint8_t reg_addr, const uint8_t* data, uint32_t len, void* intf_ptr) {
uint8_t addr = (uint8_t)(uintptr_t)intf_ptr;
Wire.beginTransmission(addr);
Wire.write(reg_addr);
for (uint32_t i = 0; i < len; ++i) Wire.write(data[i]);
return (Wire.endTransmission() == 0) ? 0 : -1;
}
static void delay_us(uint32_t period, void* /*intf_ptr*/) {
delayMicroseconds(period);
}
// BMI270 setup
static bool bmi270_setup_anymotion_latched() {
int8_t rslt;
dev.intf = BMI2_I2C_INTF;
dev.read = i2c_read;
dev.write = i2c_write;
dev.delay_us = delay_us;
dev.read_write_len = 32;
dev.config_file_ptr = NULL;
dev.intf_ptr = (void*)(uintptr_t)BMI270_I2C_ADDR;
rslt = bmi270_init(&dev);
if (rslt != BMI2_OK) return false;
uint8_t sensors[2] = { BMI2_ACCEL, BMI2_ANY_MOTION };
rslt = bmi270_sensor_enable(sensors, 2, &dev);
if (rslt != BMI2_OK) return false;
bmi2_sens_config cfg{};
cfg.type = BMI2_ANY_MOTION;
if (bmi270_get_sensor_config(&cfg, 1, &dev) != BMI2_OK) return false;
cfg.cfg.any_motion.threshold = ANYMOTION_THRESHOLD;
cfg.cfg.any_motion.duration = ANYMOTION_DURATION;
if (bmi270_set_sensor_config(&cfg, 1, &dev) != BMI2_OK) return false;
bmi2_int_pin_config int_cfg{};
int_cfg.pin_type = BMI2_INT1;
int_cfg.int_latch = BMI2_INT_LATCH;
int_cfg.pin_cfg[0].lvl = BMI2_INT_ACTIVE_HIGH;
int_cfg.pin_cfg[0].od = BMI2_INT_PUSH_PULL;
int_cfg.pin_cfg[0].output_en = BMI2_INT_OUTPUT_ENABLE;
int_cfg.pin_cfg[0].input_en = BMI2_INT_INPUT_DISABLE;
if (bmi2_set_int_pin_config(&int_cfg, &dev) != BMI2_OK) return false;
bmi2_sens_int_config sens_int{};
sens_int.type = BMI2_ANY_MOTION;
sens_int.hw_int_pin = BMI2_INT1;
if (bmi270_map_feat_int(&sens_int, 1, &dev) != BMI2_OK) return false;
// Enable advanced power save mode for lower power consumption
rslt = bmi2_set_adv_power_save(BMI2_ENABLE, &dev);
if (rslt != BMI2_OK) {
Serial.println("Warning: Failed to enable BMI270 advanced power save mode");
} else {
Serial.println("BMI270 advanced power save mode enabled");
}
return true;
}
// Clear latched interrupt
static void bmi270_clear_latched_if_any() {
uint16_t st = 0;
if (bmi2_get_int_status(&st, &dev) == BMI2_OK && st != 0) {
delay(1);
bmi2_get_int_status(&st, &dev); // re-read to clear
}
}
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
initDisplay();
showScreen("REBOOTING...", "Initializing BMI270...");
delay(2000);
Wire.begin(I2C_SDA, I2C_SCL, I2C_FREQ);
bmi270_setup_anymotion_latched();
bmi270_clear_latched_if_any();
// Configure BMI270 for minimal power consumption
Serial.println("Configuring BMI270 for minimal power...");
// Set accelerometer to lowest power mode (if supported)
bmi2_sens_config accel_cfg{};
accel_cfg.type = BMI2_ACCEL;
if (bmi270_get_sensor_config(&accel_cfg, 1, &dev) == BMI2_OK) {
// Set to lowest ODR (Output Data Rate) for power savings
accel_cfg.cfg.acc.odr = BMI2_ACC_ODR_0_78HZ; // Lowest ODR
accel_cfg.cfg.acc.range = BMI2_ACC_RANGE_2G; // Lower range = less power
accel_cfg.cfg.acc.bwp = BMI2_ACC_NORMAL_AVG4; // Normal bandwidth
if (bmi270_set_sensor_config(&accel_cfg, 1, &dev) == BMI2_OK) {
Serial.println("BMI270 accelerometer configured for low power");
}
}
// Power down the device
showScreen("POWERED DOWN", "Move device to wake");
delay(2000);
M5.Power.powerOff();
}
void loop() {
// The code never gets here
}