🏆 Winner Winner... Microamp Dinner!™ 1.54" E-Paper & Xiao

Hi there,

SO the E-Paper is getting better every year, more support for the partial refresh and the speed of the display updates is very reasonable and fast.

This video demonstrates the code that is included and this thread will be expanded to include the nRF54L15A and the New nRF54LM20A Xiao family MCU’s the Newest unit should get us even lower Sleep current numbers with it’s PMIC new design.
9-12 uA. Sleep current baseline for the nRF52840 parts.. The code has a few of the obvious power down items and is compliable for both BSP’s 1.1.3 and 2.9.3.

note: I choose the non-embed for For PPK II low-power testing, use Seeed nRF52 Boards 1.1.13 as your primary benchmark.

It is the better choice here because it has less framework overhead than the mbed core and gives more direct, predictable control over the nRF52840 hardware. The mbed 2.9.3 package brings a larger runtime, USB serial infrastructure, RTOS/ticker activity, and more background initialization, which makes it harder to determine whether a high sleep current comes from your sketch or the core.

  1. Hibernate the SSD1681.
  2. Perform the RGB signoff
  3. Check the PPKII for values.

HTH
GL :slight_smile: PJ :v:

/***************************************************************************
 *
 *  WINNER WINNER... MICROAMP DINNER!
 *
 *  XIAO nRF52840 Sense
 *  1.54-inch SSD1681 / D67 E-Paper Demonstration
 *
 *  GOLD STANDARD / MASTER ARCHIVE BUILD
 *
 *  Primary BSP:
 *      Seeed nRF52 Boards 1.1.13
 *
 *  Also compatible with:
 *      Seeeduino mbed-enabled Boards 2.9.3
 *
 *  Tested final System OFF current:
 *      Approximately 9.99 µA
 *
 *  Test equipment:
 *      Nordic Power Profiler Kit II
 *
 *  Test configuration:
 *      XIAO powered only from the PPK II
 *      No USB cable connected during the power measurement
 *      Complete XIAO Sense + e-paper breakout assembly measured
 *
 ***************************************************************************
 *
 *  HARDWARE
 *
 *      MCU:
 *          Seeed Studio XIAO nRF52840 Sense
 *
 *      Display:
 *          1.54-inch monochrome e-paper
 *
 *      Resolution:
 *          200 Ă— 200 pixels
 *
 *      Controller:
 *          SSD1681
 *
 *      GxEPD2 panel class:
 *          GxEPD2_154_D67
 *
 ***************************************************************************
 *
 *  E-PAPER CONNECTIONS
 *
 *      E-paper RST   -> XIAO D0
 *      E-paper CS    -> XIAO D1
 *      E-paper DC    -> XIAO D3
 *      E-paper BUSY  -> XIAO D5
 *      E-paper SCK   -> XIAO D8
 *      E-paper MOSI  -> XIAO D10
 *      E-paper VCC   -> XIAO 3V3
 *      E-paper GND   -> XIAO GND
 *
 *  The display is write-only for this demonstration.
 *  MISO is not required.
 *
 ***************************************************************************
 *
 *  DEMONSTRATION SEQUENCE
 *
 *      1. Initialize the SSD1681 e-paper display.
 *
 *      2. Clear the panel to white.
 *
 *      3. Show the system-information screen:
 *
 *             SYSTEM
 *             MCU INSTALLED
 *             XIAO nRF52840
 *             SENSE
 *             EPD 1.54 inch
 *             SSD1681 / D67
 *             200 x 200
 *
 *      4. Show the HELLO SEEED WORLD screen.
 *
 *      5. Perform a 30-count partial-refresh countdown.
 *
 *      6. Show the GOING TO SLEEP screen.
 *
 *      7. Perform a flashing 10-count partial-refresh countdown.
 *
 *      8. Show the final retained SLEEP screen.
 *
 *      9. Put the SSD1681 controller into hibernate.
 *
 *     10. Perform the visible RGB signoff:
 *
 *             RED
 *             WHITE
 *             BLUE
 *
 *     11. Turn off the onboard IMU supply.
 *
 *     12. Stop the e-paper SPI interface.
 *
 *     13. Force the active-low RGB LED fully off.
 *
 *     14. Enter nRF52840 System OFF.
 *
 *  The final e-paper image remains visible without continuous display power.
 *
 ***************************************************************************
 *
 *  LOW-POWER DESIGN NOTES
 *
 *  This master build intentionally uses a minimal shutdown path.
 *
 *  The nRF52840 automatically shuts down its internal peripherals when
 *  System OFF is entered successfully. Manually disabling every timer,
 *  interrupt, UART, USB block, clock and peripheral was found to introduce
 *  additional failure modes and was not required.
 *
 *  The final shutdown therefore handles only the external hardware states
 *  that matter:
 *
 *      - SSD1681 hibernate
 *      - e-paper interface pins
 *      - onboard IMU power
 *      - microphone-related pins
 *      - battery-measurement control pin
 *      - onboard RGB LED
 *      - MCU System OFF
 *
 ***************************************************************************
 *
 *  ONBOARD QSPI FLASH NOTE
 *
 *  The XIAO nRF52840 Sense includes onboard QSPI flash.
 *
 *  A direct custom-instruction experiment was previously used to send the
 *  flash command:
 *
 *      0xB9 = Deep Power-Down
 *
 *  Although the command appeared to execute, manipulating the QSPI peripheral
 *  during final shutdown caused the tested assembly to remain near 9–18 mA
 *  instead of entering the expected microamp System OFF state.
 *
 *  The QSPI flash deep-power-down routine is therefore deliberately omitted
 *  from this gold-standard build.
 *
 *  Leaving the onboard flash untouched produced the proven approximately
 *  9.99 µA final measurement.
 *
 ***************************************************************************
 *
 *  WAKE BEHAVIOR
 *
 *  This sketch does not configure a GPIO wake source.
 *
 *  After entering System OFF, restart the board using:
 *
 *      - the XIAO RESET button, or
 *      - a complete power cycle
 *
 ***************************************************************************
 *
 *  REQUIRED ARDUINO LIBRARY
 *
 *      GxEPD2
 *
 *  Fonts used from the Adafruit GFX font collection included with GxEPD2:
 *
 *      FreeMonoBold9pt7b
 *      FreeMonoBold12pt7b
 *      FreeMonoBold18pt7b
 *      FreeMonoBold24pt7b
 *
 ***************************************************************************/

#include <Arduino.h>
#include <SPI.h>

#include <GxEPD2_BW.h>

#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#include <Fonts/FreeMonoBold18pt7b.h>
#include <Fonts/FreeMonoBold24pt7b.h>

#include <nrf.h>


/***************************************************************************
 *
 *  BSP SUPPORT
 *
 *  Primary:
 *      Seeed nRF52 Boards 1.1.13
 *
 *  Secondary compatibility:
 *      Seeeduino mbed-enabled Boards 2.9.3
 *
 ***************************************************************************/

#if defined(ARDUINO_ARCH_NRF52) && !defined(ARDUINO_ARCH_MBED)

#include <nrf_sdm.h>
#include <nrf_soc.h>

#define XIAO_BSP_SEEED_NRF52  1
#define XIAO_BSP_MBED         0

#elif defined(ARDUINO_ARCH_MBED)

#define XIAO_BSP_SEEED_NRF52  0
#define XIAO_BSP_MBED         1

#else

#error "Unsupported BSP. Select a Seeed XIAO nRF52840 Sense board."

#endif


/***************************************************************************
 *
 *  E-PAPER PIN ASSIGNMENTS
 *
 ***************************************************************************/

#define EPD_RST   D0
#define EPD_CS    D1
#define EPD_DC    D3
#define EPD_BUSY  D5

#define EPD_SCK   D8
#define EPD_MOSI  D10


/***************************************************************************
 *
 *  XIAO nRF52840 SENSE HARDWARE
 *
 *  IMU power:
 *      P1.08
 *
 *  PDM microphone:
 *      DATA = P0.16
 *      CLK  = P1.00
 *
 *  Battery measurement control:
 *      P0.14
 *
 ***************************************************************************/

constexpr uint32_t IMU_POWER_PORT = 1;
constexpr uint32_t IMU_POWER_PIN  = 8;

constexpr uint32_t MIC_DATA_PORT = 0;
constexpr uint32_t MIC_DATA_PIN  = 16;

constexpr uint32_t MIC_CLOCK_PORT = 1;
constexpr uint32_t MIC_CLOCK_PIN  = 0;

constexpr uint32_t BATTERY_CONTROL_PORT = 0;
constexpr uint32_t BATTERY_CONTROL_PIN  = 14;


/***************************************************************************
 *
 *  DISPLAY INSTANCE
 *
 ***************************************************************************/

GxEPD2_BW<GxEPD2_154_D67, GxEPD2_154_D67::HEIGHT> display(
  GxEPD2_154_D67(
    EPD_CS,
    EPD_DC,
    EPD_RST,
    EPD_BUSY
  )
);


/***************************************************************************
 *
 *  DEMONSTRATION TIMING
 *
 ***************************************************************************/

/*
 * Extra viewing time after each full refresh.
 *
 * GxEPD2 already waits for the controller BUSY signal internally.
 * These delays are included to make the video demonstration easy to follow.
 */
constexpr uint32_t FULL_SCREEN_VIEW_TIME_MS = 2000;

/*
 * Time between partial-refresh countdown updates.
 */
constexpr uint32_t PARTIAL_UPDATE_PAUSE_MS = 900;

/*
 * RGB signoff timing.
 */
constexpr uint32_t RGB_ON_TIME_MS  = 600;
constexpr uint32_t RGB_OFF_TIME_MS = 200;


/***************************************************************************
 *
 *  COUNTDOWN SETTINGS
 *
 ***************************************************************************/

constexpr uint8_t HELLO_COUNT_START = 30;
constexpr uint8_t SLEEP_COUNT_START = 10;


/***************************************************************************
 *
 *  PARTIAL UPDATE WINDOWS
 *
 *  Coordinates are based on display rotation 1.
 *
 ***************************************************************************/

constexpr int16_t HELLO_COUNTER_X = 48;
constexpr int16_t HELLO_COUNTER_Y = 70;
constexpr int16_t HELLO_COUNTER_W = 104;
constexpr int16_t HELLO_COUNTER_H = 70;

constexpr int16_t SLEEP_COUNTER_X = 48;
constexpr int16_t SLEEP_COUNTER_Y = 82;
constexpr int16_t SLEEP_COUNTER_W = 104;
constexpr int16_t SLEEP_COUNTER_H = 72;


/***************************************************************************
 *
 *  CENTERED TEXT HELPERS
 *
 ***************************************************************************/

/*
 * Print text centered horizontally using a specified text baseline.
 */
void printCentered(
  const char* text,
  int16_t baselineY
)
{
  int16_t boundsX;
  int16_t boundsY;

  uint16_t boundsWidth;
  uint16_t boundsHeight;

  display.getTextBounds(
    text,
    0,
    baselineY,
    &boundsX,
    &boundsY,
    &boundsWidth,
    &boundsHeight
  );

  const int16_t cursorX =
    ((int16_t)display.width() - (int16_t)boundsWidth) / 2 -
    boundsX;

  display.setCursor(
    cursorX,
    baselineY
  );

  display.print(text);
}


/*
 * Print text centered both horizontally and vertically inside a rectangular
 * partial-update window.
 */
void printCenteredInWindow(
  const char* text,
  int16_t windowX,
  int16_t windowY,
  int16_t windowWidth,
  int16_t windowHeight
)
{
  int16_t boundsX;
  int16_t boundsY;

  uint16_t boundsWidth;
  uint16_t boundsHeight;

  display.getTextBounds(
    text,
    0,
    0,
    &boundsX,
    &boundsY,
    &boundsWidth,
    &boundsHeight
  );

  const int16_t cursorX =
    windowX +
    ((windowWidth - (int16_t)boundsWidth) / 2) -
    boundsX;

  const int16_t cursorY =
    windowY +
    ((windowHeight - (int16_t)boundsHeight) / 2) -
    boundsY;

  display.setCursor(
    cursorX,
    cursorY
  );

  display.print(text);
}


/***************************************************************************
 *
 *  ONBOARD RGB LED
 *
 *  The XIAO onboard RGB LED is active-low:
 *
 *      LOW  = LED on
 *      HIGH = LED off
 *
 ***************************************************************************/

void initializeRgbLed()
{
  pinMode(
    LED_RED,
    OUTPUT
  );

  pinMode(
    LED_GREEN,
    OUTPUT
  );

  pinMode(
    LED_BLUE,
    OUTPUT
  );

  digitalWrite(
    LED_RED,
    HIGH
  );

  digitalWrite(
    LED_GREEN,
    HIGH
  );

  digitalWrite(
    LED_BLUE,
    HIGH
  );
}


void rgbOff()
{
  digitalWrite(
    LED_RED,
    HIGH
  );

  digitalWrite(
    LED_GREEN,
    HIGH
  );

  digitalWrite(
    LED_BLUE,
    HIGH
  );
}


void showRgbColor(
  bool redOn,
  bool greenOn,
  bool blueOn
)
{
  digitalWrite(
    LED_RED,
    redOn ? LOW : HIGH
  );

  digitalWrite(
    LED_GREEN,
    greenOn ? LOW : HIGH
  );

  digitalWrite(
    LED_BLUE,
    blueOn ? LOW : HIGH
  );

  delay(
    RGB_ON_TIME_MS
  );

  rgbOff();

  delay(
    RGB_OFF_TIME_MS
  );
}


/*
 * Visible signoff before the board enters System OFF.
 *
 * Sequence:
 *
 *      RED
 *      WHITE
 *      BLUE
 */
void playFinalRgbSequence()
{
  showRgbColor(
    true,
    false,
    false
  );

  showRgbColor(
    true,
    true,
    true
  );

  showRgbColor(
    false,
    false,
    true
  );

  rgbOff();
}


/***************************************************************************
 *
 *  SCREEN 1
 *
 *  SYSTEM INFORMATION
 *
 ***************************************************************************/

void showStartupScreen()
{
  display.setRotation(1);
  display.setFullWindow();

  display.firstPage();

  do
  {
    display.fillScreen(
      GxEPD_WHITE
    );

    display.setTextColor(
      GxEPD_BLACK
    );

    display.setTextSize(1);

    display.setFont(
      &FreeMonoBold12pt7b
    );

    printCentered(
      "SYSTEM",
      28
    );

    display.setFont(
      &FreeMonoBold9pt7b
    );

    printCentered(
      "MCU INSTALLED",
      58
    );

    printCentered(
      "XIAO nRF52840",
      82
    );

    printCentered(
      "SENSE",
      104
    );

    printCentered(
      "EPD 1.54 inch",
      137
    );

    printCentered(
      "SSD1681 / D67",
      160
    );

    printCentered(
      "200 x 200",
      184
    );
  }
  while (
    display.nextPage()
  );
}


/***************************************************************************
 *
 *  SCREEN 2
 *
 *  HELLO SEEED WORLD
 *
 ***************************************************************************/

void showHelloScreen()
{
  display.setRotation(1);
  display.setFullWindow();

  display.firstPage();

  do
  {
    display.fillScreen(
      GxEPD_WHITE
    );

    display.setTextColor(
      GxEPD_BLACK
    );

    display.setTextSize(1);

    display.setFont(
      &FreeMonoBold9pt7b
    );

    printCentered(
      "HELLO SEEED WORLD",
      30
    );

    printCentered(
      "COUNTDOWN",
      62
    );

    display.setFont(
      &FreeMonoBold12pt7b
    );

    printCentered(
      "GxEPD2",
      164
    );

    display.setFont(
      &FreeMonoBold9pt7b
    );

    printCentered(
      "PjG",
      190
    );
  }
  while (
    display.nextPage()
  );
}


/***************************************************************************
 *
 *  HELLO SCREEN PARTIAL UPDATE
 *
 ***************************************************************************/

void updateHelloCounter(
  uint8_t counterValue
)
{
  char counterText[8];

  snprintf(
    counterText,
    sizeof(counterText),
    "%u",
    counterValue
  );

  display.setRotation(1);

  display.setPartialWindow(
    HELLO_COUNTER_X,
    HELLO_COUNTER_Y,
    HELLO_COUNTER_W,
    HELLO_COUNTER_H
  );

  display.firstPage();

  do
  {
    display.fillScreen(
      GxEPD_WHITE
    );

    display.setTextColor(
      GxEPD_BLACK
    );

    display.setTextSize(1);

    display.setFont(
      &FreeMonoBold24pt7b
    );

    printCenteredInWindow(
      counterText,
      HELLO_COUNTER_X,
      HELLO_COUNTER_Y,
      HELLO_COUNTER_W,
      HELLO_COUNTER_H
    );
  }
  while (
    display.nextPage()
  );

  delay(
    PARTIAL_UPDATE_PAUSE_MS
  );
}


void runHelloCountdown()
{
  for (
    int16_t counterValue = HELLO_COUNT_START;
    counterValue >= 1;
    counterValue--
  )
  {
    updateHelloCounter(
      (uint8_t)counterValue
    );
  }
}


/***************************************************************************
 *
 *  SCREEN 3
 *
 *  GOING TO SLEEP
 *
 ***************************************************************************/

void showGoingToSleepScreen()
{
  display.setRotation(1);
  display.setFullWindow();

  display.firstPage();

  do
  {
    display.fillScreen(
      GxEPD_WHITE
    );

    display.setTextColor(
      GxEPD_BLACK
    );

    display.setTextSize(1);

    display.setFont(
      &FreeMonoBold12pt7b
    );

    printCentered(
      "GOING TO",
      32
    );

    printCentered(
      "SLEEP",
      60
    );

    display.setFont(
      &FreeMonoBold9pt7b
    );

    printCentered(
      "HIBERNATING IN",
      174
    );
  }
  while (
    display.nextPage()
  );
}


/***************************************************************************
 *
 *  SLEEP SCREEN FLASHING PARTIAL UPDATE
 *
 ***************************************************************************/

void updateFlashingSleepCounter(
  uint8_t counterValue,
  bool inverted
)
{
  char counterText[8];

  snprintf(
    counterText,
    sizeof(counterText),
    "%u",
    counterValue
  );

  display.setRotation(1);

  display.setPartialWindow(
    SLEEP_COUNTER_X,
    SLEEP_COUNTER_Y,
    SLEEP_COUNTER_W,
    SLEEP_COUNTER_H
  );

  display.firstPage();

  do
  {
    if (inverted)
    {
      display.fillScreen(
        GxEPD_BLACK
      );

      display.setTextColor(
        GxEPD_WHITE
      );
    }
    else
    {
      display.fillScreen(
        GxEPD_WHITE
      );

      display.setTextColor(
        GxEPD_BLACK
      );
    }

    display.setTextSize(1);

    display.setFont(
      &FreeMonoBold24pt7b
    );

    printCenteredInWindow(
      counterText,
      SLEEP_COUNTER_X,
      SLEEP_COUNTER_Y,
      SLEEP_COUNTER_W,
      SLEEP_COUNTER_H
    );
  }
  while (
    display.nextPage()
  );

  delay(
    PARTIAL_UPDATE_PAUSE_MS
  );
}


void runFlashingSleepCountdown()
{
  bool inverted = true;

  for (
    int16_t counterValue = SLEEP_COUNT_START;
    counterValue >= 1;
    counterValue--
  )
  {
    updateFlashingSleepCounter(
      (uint8_t)counterValue,
      inverted
    );

    inverted = !inverted;
  }
}


/***************************************************************************
 *
 *  FINAL RETAINED SCREEN
 *
 *  This image remains visible after:
 *
 *      - SSD1681 hibernate
 *      - MCU System OFF
 *      - display power consumption falls to essentially zero
 *
 ***************************************************************************/

void showFinalSleepScreen()
{
  display.setRotation(1);
  display.setFullWindow();

  display.firstPage();

  do
  {
    display.fillScreen(
      GxEPD_WHITE
    );

    display.setTextColor(
      GxEPD_BLACK
    );

    display.setTextSize(1);

    display.setFont(
      &FreeMonoBold18pt7b
    );

    printCentered(
      "SLEEP",
      90
    );

    display.setFont(
      &FreeMonoBold9pt7b
    );

    printCentered(
      "But Seeen ;-)",
      158
    );

    printCentered(
      "Zero display power",
      188
    );
  }
  while (
    display.nextPage()
  );
}


/***************************************************************************
 *
 *  GPIO LOW-POWER HELPER
 *
 *  Configures a physical nRF52840 pin as:
 *
 *      - input
 *      - input buffer disconnected
 *      - no pull resistor
 *      - GPIO sense disabled
 *
 ***************************************************************************/

void disconnectPhysicalPin(
  uint32_t port,
  uint32_t pin
)
{
  const uint32_t pinConfiguration =
    (
      GPIO_PIN_CNF_DIR_Input
      << GPIO_PIN_CNF_DIR_Pos
    ) |
    (
      GPIO_PIN_CNF_INPUT_Disconnect
      << GPIO_PIN_CNF_INPUT_Pos
    ) |
    (
      GPIO_PIN_CNF_PULL_Disabled
      << GPIO_PIN_CNF_PULL_Pos
    ) |
    (
      GPIO_PIN_CNF_DRIVE_S0S1
      << GPIO_PIN_CNF_DRIVE_Pos
    ) |
    (
      GPIO_PIN_CNF_SENSE_Disabled
      << GPIO_PIN_CNF_SENSE_Pos
    );

  if (
    port == 0 &&
    pin < 32
  )
  {
    NRF_P0->PIN_CNF[pin] =
      pinConfiguration;
  }
  else if (
    port == 1 &&
    pin < 16
  )
  {
    NRF_P1->PIN_CNF[pin] =
      pinConfiguration;
  }
}


/***************************************************************************
 *
 *  XIAO SENSE HARDWARE SHUTDOWN
 *
 ***************************************************************************/

void shutDownSenseHardware()
{
  /*
   * Turn off the onboard IMU supply.
   *
   * P1.08 is driven low.
   */
  NRF_P1->DIRSET =
    1UL << IMU_POWER_PIN;

  NRF_P1->OUTCLR =
    1UL << IMU_POWER_PIN;

  /*
   * Disconnect microphone pins.
   */
  disconnectPhysicalPin(
    MIC_DATA_PORT,
    MIC_DATA_PIN
  );

  disconnectPhysicalPin(
    MIC_CLOCK_PORT,
    MIC_CLOCK_PIN
  );

  /*
   * Disconnect the battery-measurement control pin.
   */
  disconnectPhysicalPin(
    BATTERY_CONTROL_PORT,
    BATTERY_CONTROL_PIN
  );
}


/***************************************************************************
 *
 *  E-PAPER INTERFACE SHUTDOWN
 *
 *  The SSD1681 must already be in hibernate before this function is called.
 *
 ***************************************************************************/

void shutDownEpaperInterface()
{
  /*
   * Stop the Arduino SPI interface.
   */
  SPI.end();

  /*
   * Keep e-paper chip select inactive.
   */
  pinMode(
    EPD_CS,
    OUTPUT
  );

  digitalWrite(
    EPD_CS,
    HIGH
  );

  /*
   * Leave the control lines in quiet, deterministic states.
   */
  pinMode(
    EPD_DC,
    OUTPUT
  );

  digitalWrite(
    EPD_DC,
    LOW
  );

  pinMode(
    EPD_RST,
    OUTPUT
  );

  digitalWrite(
    EPD_RST,
    HIGH
  );

  /*
   * Release the SPI clock, data and BUSY lines.
   */
  pinMode(
    EPD_SCK,
    INPUT
  );

  pinMode(
    EPD_MOSI,
    INPUT
  );

  pinMode(
    EPD_BUSY,
    INPUT
  );
}


/***************************************************************************
 *
 *  FINAL RGB OFF STATE
 *
 ***************************************************************************/

void forceRgbOff()
{
  /*
   * The onboard RGB LED is active-low.
   *
   * All three pins must remain HIGH to guarantee that no color remains on.
   */
  pinMode(
    LED_RED,
    OUTPUT
  );

  pinMode(
    LED_GREEN,
    OUTPUT
  );

  pinMode(
    LED_BLUE,
    OUTPUT
  );

  digitalWrite(
    LED_RED,
    HIGH
  );

  digitalWrite(
    LED_GREEN,
    HIGH
  );

  digitalWrite(
    LED_BLUE,
    HIGH
  );
}


/***************************************************************************
 *
 *  ENTER NRF52840 SYSTEM OFF
 *
 *  Primary BSP:
 *      Seeed nRF52 Boards 1.1.13
 *
 *  If a SoftDevice is active, the SoftDevice-safe API is used.
 *
 *  If no SoftDevice is active, or when compiling with the mbed BSP,
 *  System OFF is entered directly through NRF_POWER.
 *
 ***************************************************************************/

[[noreturn]] void enterSystemOff()
{
  /*
   * No Arduino framework calls should be made after the System OFF request.
   */

#if XIAO_BSP_SEEED_NRF52

  uint8_t softDeviceEnabled = 0;

  const uint32_t softDeviceStatus =
    sd_softdevice_is_enabled(
      &softDeviceEnabled
    );

  if (
    softDeviceStatus == NRF_SUCCESS &&
    softDeviceEnabled != 0
  )
  {
    /*
     * This function should not return after entering System OFF.
     */
    sd_power_system_off();
  }

#endif

  /*
   * Direct System OFF path:
   *
   *      - Seeed nRF52 BSP with no active SoftDevice
   *      - Seeeduino mbed-enabled BSP
   */
  NRF_POWER->SYSTEMOFF = 1;

  __DSB();
  __ISB();

  /*
   * SYSTEMOFF should never return.
   *
   * The loop is included as a defensive fallback.
   */
  while (true)
  {
    __WFE();
  }
}


/***************************************************************************
 *
 *  FINAL LOW-POWER SEQUENCE
 *
 ***************************************************************************/

[[noreturn]] void finishDemonstrationAndSleep()
{
  /*
   * Stop the external display interface.
   */
  shutDownEpaperInterface();

  /*
   * Turn off the XIAO Sense peripherals that have an externally controlled
   * power or signal path.
   */
  shutDownSenseHardware();

  /*
   * Guarantee that the active-low RGB LED remains fully off.
   */
  forceRgbOff();

  /*
   * Allow GPIO states to settle before entering System OFF.
   */
  delay(100);

  /*
   * Enter the nRF52840's lowest-power System OFF state.
   */
  enterSystemOff();
}


/***************************************************************************
 *
 *  ARDUINO SETUP
 *
 ***************************************************************************/

void setup()
{
  /*
   * Initialize the onboard RGB LED in its fully off state.
   */
  initializeRgbLed();

  /*
   * Configure the e-paper BUSY input.
   */
  pinMode(
    EPD_BUSY,
    INPUT
  );

  /*
   * Start hardware SPI.
   */
  SPI.begin();

  /*
   * Initialize GxEPD2.
   *
   * A diagnostic baud rate of zero disables GxEPD2 serial status output.
   * This master build contains no runtime debug logging.
   */
  display.init(0);

  /*
   * Begin with a clean white panel.
   */
  display.clearScreen();

  delay(
    FULL_SCREEN_VIEW_TIME_MS
  );

  /*
   * Show the system-information screen.
   */
  showStartupScreen();

  delay(
    FULL_SCREEN_VIEW_TIME_MS
  );

  /*
   * Show the Hello Seeed World screen.
   */
  showHelloScreen();

  delay(
    FULL_SCREEN_VIEW_TIME_MS
  );

  /*
   * Run the 30-count partial-update demonstration.
   */
  runHelloCountdown();

  /*
   * Show the Going to Sleep screen.
   */
  showGoingToSleepScreen();

  delay(
    FULL_SCREEN_VIEW_TIME_MS
  );

  /*
   * Run the flashing 10-count partial-update demonstration.
   */
  runFlashingSleepCountdown();

  /*
   * Draw the final retained image.
   */
  showFinalSleepScreen();

  delay(
    FULL_SCREEN_VIEW_TIME_MS
  );

  /*
   * Put the SSD1681 controller into hibernate.
   *
   * The image remains visible after the controller and MCU stop consuming
   * normal operating power.
   */
  display.hibernate();

  /*
   * Visible red, white and blue completion sequence.
   */
  playFinalRgbSequence();

  /*
   * Enter the proven low-power shutdown path.
   */
  finishDemonstrationAndSleep();
}


/***************************************************************************
 *
 *  ARDUINO LOOP
 *
 ***************************************************************************/

void loop()
{
  /*
   * setup() enters System OFF and never returns.
   */
}




and here is a bad Flash shutdown fail or missed command, You can see the end that consumption go’s ma. high. TEST everything.. :grin:

So here is a gallery of the displays, boards and a competitor below.

I’m calling it the “STAMP-STICKY” :wink: (it’s not much bigger than a stamp), now that’s a refrigerator magnet.
I’ll mash_up a 3D case and battery… why not :face_with_tongue:


Way cheaper and Last Long Time. :money_mouth_face:

I like the Breakout board better than the ePaper_Driver_board
more testing on the Driver board will be performed to determine if the power switch and on board battery charger interferes with the Xiao Deep sleep and over-all sleep current , I’m certain it will.
The Breakout board on the other hand with this 1.54." display is the Gold Standard for me in this application. It’s thinner, can be as an SPI peripheral or direct Xiao mounted.
add a PLUS variant Xiao and more sensors are possible too. :+1:

one project I ditched the OLED , estimate went from 8 months battery , to 4 years…
450Mah, @.07ma. sleep vs. 450Mah. @.012 , client wants displays and just when I got the RGB LED back… SOB. :grin:

GL :slight_smile: PJ :v:

Hi there,

So Awesome news , a few tweaks and BAM … Bob is Your Uncle
:grin:

nRF54LM20A Xiao Sense is a GO! for ePaper :+1:
PMIC reporting for duty. :saluting_face:

SLEEP numbers cooking right now and code to follow. :face_with_tongue:

HTH
GL :slight_smile: PJ :v:

Thank you for sharing those interesting results!

A couple of questions

  • How long does the partial refresh take, for the whole cycle: turning the screen on, sending the data, performing the refresh, turning the screen back off?
  • And how does partial refresh compare with the standard refresh, including the size of the modified area vs. 200x200?

Thank you!

Hi there, and M,y pleasure…
Two very astute questions right there, I’ll open with this one, to Say it doesn’t work that way but I have a feeling you know that and it’s over simplified in the ask.

This code example specifically is at the end. Represents with both nRf52 & 54 Xiao MCU’s)

Partial vs. Standard (Full) Refresh Comparison

Highlight the key trade-offs in time, power, and buffer handling:

  • Cycle Duration:

    • Full Refresh: Takes ~1.2s – 2.0s (requires multiple full black/white inversion passes to prevent burn-in/ghosting).

    • Partial Refresh: Takes ~0.3s – 0.5s (roughly 3Ă— to 4Ă— faster with significantly lower total active energy draw).

  • Windowed Area vs. Full 200Ă—200 Buffer:

    • Even if you target a partial bounding box (e.g., changing only clock digits or a small icon), the partial LUT execution time on the controller (e.g., SSD1681) remains relatively fixed (~300–400 ms).

    • Sub-window updates primarily optimize SPI transfer time (transferring a few hundred bytes instead of 5 KB), though full-frame partial updates are so fast that SPI transfer time is rarely the bottleneck compared to the panel waveform execution.

  • Maintenance Note: Mention that running a full refresh every 5 to 10 partial updates is recommended to clear accumulated ghosting on the e-ink capsules.

A couple things at this point should be Painfully obvious , Certain Manufactures, STRETCH :lying_face: the Truth (by shortening or bypassing) the Panel manufacture proper performance guideline to extend the life or performance marketing info of the panel.
Buyer beware…

In this demo code the bounding box saves more SPI speed by smaller payload, and you can see down the line, that intern pays dividends on the over all response impresion. :wink: looks fast, because it is :grin:

I got in this for the power vs. function. having the last runs data still present or status at no power cost compared to an RGB LED even. forget the WOW factor. :fishing_pole:

BTW the timing is sent out the serial port for the refreshes, I did that so ONE, :index_pointing_at_the_viewer: could verify these numbers.

So ,there’s definitely a bit of “marketing specs” floating around regarding partial refresh speeds. A lot of fast refresh claims rely on severely truncated waveform LUTs that leave residual charge on the capsules, which wrecks the panel’s lifespan and causes severe ghosting over time.

Here is how the timing breaks down when doing it right:

1. Full Execution Cycle Timing (~350 ms – 500 ms Total)

  • Power On / Init: ~10–20 ms (PMIC enable, SPI setup, controller wake).

  • Data Transfer (SPI): ~5–10 ms (For 200x200 1-bit, 5 KB of frame data moves near-instantly over standard SPI speeds).

  • Partial Refresh (LUT Execution): ~300–400 ms (This is the physical particle transition—shortcutting this to get sub-200ms refreshes is what damages panels long-term).

  • Deep Sleep Command: < 5 ms (0x10 deep sleep command and power rail cutoff).

2. Partial vs. Standard Refresh & Modified Area Size

  • Full Refresh: Takes ~1.2s to 2.0s due to multiple full inversion cycles (black/white clearing).

  • Partial Refresh: Takes ~0.3s to 0.5s (roughly 3x to 4x faster and significantly lower overall active energy consumption).

  • Sub-Windowing: Restricting updates to a small bounding box saves SPI transfer time and host RAM headroom, but since SPI transfer is already under 10 ms for the whole screen, the panel waveform execution time (~300–400 ms) dominates the overall cycle regardless of update size.

Doing proper partial waveforms lets you run around 10–20 partial updates before needing a full refresh pass to clear ghosting, keeping power low and panel longevity intact!

Demo code

Break down the execution sequence step-by-step:

  • Power-on & Initialization: ~10–30 ms (PMIC/LDO enable, SPI init, panel wake commands).

  • Data Transmission (SPI): ~5–10 ms (For 200x200 resolution at 1-bit per pixel, the frame buffer is only 5 KB; at standard SPI clock speeds of 4–8 MHz, transfer is near-instantaneous).

  • Display Refresh (Partial LUT execution): ~300 ms – 500 ms (The panel waveform cycle itself does the heavy lifting here).

  • Power Down / Deep Sleep: < 5 ms (Sending deep sleep command 0x10 and dropping the power rail).

  • You can see the power the Full refresh requires on the Graph below (complete cycle to reset) in amplitude and the Partials minuscule in comparison, The LUT is a fixed timing and therefore the speed can be “Jacked” by bypassing it, but it comes a a cost in lifespan.

You only need to look on discord and other places to see the types of failures and issue’s even the Seeedineers :crossed_fingers:are having with this area of tech.

One panel self destructs half of the whole screen, they are working on it and others , the controllers and the panels have to match to get the Unbelievable nice looks the get for the larger ones. I like the E_paper Breakout best, works well as small panel driver. both SPI, and XIAO direct.

It’s newish technology so more testing and discover is ongoing. :+1:

HTH
GL :slight_smile: PJ :v:

A lot more to come , we are just dipping the quill in the E-ink :grin: here if you will
it’s good stuff and the some of the big color ones are STUPID nice. :zany_face:
I hope they last long.

Hi there,

So more fun than normal… BANANAS :see_no_evil_monkey: :hear_no_evil_monkey: :speak_no_evil_monkey:

Ultra-Low-Power ePaper Technology Demonstration

What you’ll see

This demonstration showcases the capabilities of the Seeed Studio XIAO nRF54LM20A combined with a 1.54-inch SSD1681 (D67) e-paper display and the Nordic nPM1300 Power Management IC.

The demo performs a complete power-up sequence including:

  • MCU initialization
  • Hardware diagnostics
  • Live battery and PMIC status
  • RGB LED signature sequence
  • High-quality Floyd–Steinberg dithered 200Ă—200 e-paper graphics
  • Animated 30-second countdown
  • Complete peripheral shutdown
  • Permanent System OFF

The last image remains visible indefinitely while the complete system draws approximately:

8.56 µA

This represents a reduction of well over 99.9% from the board’s active operating current while still retaining useful information on the display.

During shutdown the software:

  • Places the SSD1681 display into Hibernate mode.
  • Stops the SPI interface.
  • Powers down the RGB LED.
  • Disables the IMU and microphone power rail.
  • Configures the nPM1300 for minimum power operation.
  • Parks unused GPIOs.
  • Places the Nordic nRF54LM20A into permanent SYSTEMOFF.

HTH
AMA
GL :slight_smile: PJ :v:

I’ll probably not use anything else EVER :grin:
wouldn’t be a lie…
:lying_face:
“While evaluating the board, I noticed two capabilities that could make a future revision even more flexible.” it is 99.8% perfect though IMO. -.2% for leaving money on the table with the PMIC
VOUT1 buck unused
VOUT2 buck → LM20A / VSYS_3V3
LDO1 / Load Switch 1 → IMU + microphone
LDO2 / Load Switch 2 unused
(with a couple added traces to the pcb…) Expected current would move from roughly 8.9 µA toward the bare-board 4.9 µA, while retaining the e-paper image.

Could one of the PMIC’s GPIOs control the enable of the external 3.3 V regulator?"

Those are actually two different enhancement ideas.

Idea 1 — Use VOUT1

Use the unused PMIC output to power something like:

  • 1.8 V sensor
  • secure element
  • RTC

Advantages:

  • Fully programmable voltage.
  • PMIC-controlled.
  • Could be independently switched.

Idea 2 — PMIC GPIO controls 3V3_OUT EN

Use a PMIC GPIO to enable/disable the TPS628438.

Advantages:

  • Existing shields and peripherals continue using the 3.3 V pin.
  • No hardware changes required on add-on boards.
  • External peripherals can be completely powered down.

The existing design already offers excellent low-power performance, so I see this as an opportunity to make the platform even more flexible for battery-powered designs. :v:

Hi there,

Here is the code,
:wink: :v:

What this project demonstrates

  • XIAO nRF54LM20A
  • Nordic nPM1300 PMIC
  • SSD1681 / D67 e-paper display
  • Partial and full display refresh
  • Battery voltage monitoring
  • Low-power peripheral management
  • RGB PWM control
  • Optimized deep-sleep firmware
  • Permanent image retention
  • Ultra-low-power embedded system design

Overview & Baseline

  • Active Base Current (~2 mA): Between major processing events, the microcontroller operates at an active baseline around 2 mA during setup, peripheral communication, and execution intervals.

  • Deep Sleep / System OFF (~8.56 µA): At the far right of the timeline, after all routines complete, the current drops off the scale completely down to the ultra-low microamp static floor.

The Graph represents the following;

  1. System Power-Up & Initialization (First Cluster of Spikes)
  2. RGB LED Sequence & Diagnostic Checks
  3. e-Paper Display Refresh Engine (Highest Spikes, ~28–30 mA)
  4. 30-Second Countdown & Animation Loop
  5. Shutdown Sequence & System OFF

:sleeping_face:

Here is the Serial Port output;


======================================
CAMEL / CHIMP DEMO
XIAO nRF54LM20A
SSD1681 / D67 + nPM1300
Press user button anytime for CHIMP
======================================
SPI.setPins(): ACCEPTED
_Update_Full : 1290441
SCREEN 1: System information
_Update_Full : 1290440

=== nPM1300 Power Status ===
VBAT: 29 mV
Estimated battery: 0 %
VSYS: 4979 mV
VBUS: 4985 mV
IBAT: 0 mA
Temperature: 30.8 C
USB present: YES
Charging: NO
VBUS status: 0x21
Charger status: 0x0
SCREEN 2: Power status
_Update_Full : 1290440
SCREEN 3: Hello Seeed World
_Update_Full : 1290439
Hello countdown: 30
_Update_Part : 323146
Hello countdown: 29
_Update_Part : 323146
Hello countdown: 28
_Update_Part : 323146
Hello countdown: 27
_Update_Part : 323146
Hello countdown: 26
_Update_Part : 323146
Hello countdown: 25
_Update_Part : 323146
Hello countdown: 24
_Update_Part : 323146
Hello countdown: 23
_Update_Part : 323146
Hello countdown: 22
_Update_Part : 323146
Hello countdown: 21
_Update_Part : 323146
Hello countdown: 20
_Update_Part : 323146
Hello countdown: 19
_Update_Part : 323146
Hello countdown: 18
_Update_Part : 323146
Hello countdown: 17
_Update_Part : 323146
Hello countdown: 16
_Update_Part : 323146
Hello countdown: 15
_Update_Part : 323146
Hello countdown: 14
_Update_Part : 323146
Hello countdown: 13
_Update_Part : 323146
Hello countdown: 12
_Update_Part : 323146
Hello countdown: 11
_Update_Part : 323146
Hello countdown: 10
_Update_Part : 323146
Hello countdown: 9
_Update_Part : 323146
Hello countdown: 8
_Update_Part : 323146
Hello countdown: 7
_Update_Part : 323146
Hello countdown: 6
_Update_Part : 323146
Hello countdown: 5
_Update_Part : 323146
Hello countdown: 4
_Update_Part : 323146
Hello countdown: 3
_Update_Part : 323146
Hello countdown: 2
_Update_Part : 323146
Hello countdown: 1
_Update_Part : 323146
SCREEN 4: Going to Sleep
_Update_Full : 1290440
Sleep countdown: 10
_Update_Part : 323146
Sleep countdown: 9
_Update_Part : 323146
Sleep countdown: 8
_Update_Part : 323146
Sleep countdown: 7
_Update_Part : 323146
Sleep countdown: 6
_Update_Part : 323146
Sleep countdown: 5
_Update_Part : 323146
Sleep countdown: 4
_Update_Part : 323146
Sleep countdown: 3
_Update_Part : 323146
Sleep countdown: 2
_Update_Part : 323146
Sleep countdown: 1
_Update_Part : 323146

FINAL IMAGE WINDOW
Press button now for CHIMP.
Final retained image: CHIMP
FINAL SCREEN: Chimp selected
_Update_Full : 1290440

Playing RGB signoff...
RGB signoff complete.

======================================
Final image retained
SSD1681 hibernating
SPI stopping
IMU/MIC rail off
nPM1300 prepared for sleep
Entering permanent SYSTEMOFF
======================================

Zip file also includes the recorded PPK2 log, if you have one you can replay it. The Graph and Chimp.
(Floyd) after Floyd Steinberg Dithering.

Gives you the old News paper vibe.

Xiao_nRF54LM20A_E-Paper_Sleep_v1.zip (496.4 KB)

Hi PJ,

I tried running it right away.
I can’t display the SleepingChimp image.
chimp_200x200.h is in the same folder as the ino file — is that correct?.

Hi there,

Yes, and Be sure to press the USER button (b) once anytime while it 's running otherwise you get the text sleeping screen :grin:

:v:
HTH
PJ :slight_smile:

It seems I wasn’t pressing the userButton correctly. Now I’m able to display Sleeping Chimp.

What a wonderful piece of work!

Hi there,

Glad you enjoy it, I think this Xiao is the Best YeT!
:grin:

Just getting started, I’m curious though How the Clean Core handles the Battery Temp TZ, TC .. Just the fact it works is Awesome. If they add one more trace, maybe two. We could Control of the 3.3v(out) pin12 allowing to shut-OFF power to peripherals or reset a stuck I2C buss. Open LDO and V_out would seal the deal. Could get 3-4uA. Hibernate current. AND meet the advertised spec. :+1:

GL :slight_smile: PJ :v:

Hi there,

So Floyd is getting a lot of likes :grin:

The E-paper is getting crazy good, albeit a technology that has been around for 6 or so years.
I used Floyd Steinburg Dithering for the Sleeping final picture but others look good too.

HTH
GL :slight_smile: PJ :v: