🫯 Quad Color 2.9" ePaper Display w/ ePaper-Breakout Board & Xiao C3

Hi there,

So, I wanted a Basic Color Forum Activity & Weather or Rain Display for the desk. These E-paper displays are made for Low Power apps, So cool to have the device off but a display is still vizable (including a last check time is key too) at a glance you can see the status of things as of X minutes ago.
It’s a Nice Display… SO I put this together and it is a WIP! :grin:

I started with the Basic color cycle test… Seeed and Good_Display have these displays.

Hardware Used

  • Seeed Studio XIAO ESP32-C3
  • 2.9" Quad-Color ePaper (128×296)
  • Seeed ePaper Breakout Board

This is the professional embedded approach, especially for e-ink.
Use a Full framebuffer with Correct timing.
Single refresh (no flashing)
Quad-color mapping
How text works here;
You render text into a buffer, then refresh once.

// 1/21/26
// Quad Color 2.9" ePaper (JD79667) Full-Screen Color + Image Test
// Minimal-flash sequencing: init once, run sequence, sleep once.
// Rev 1.1 - 2026-01-24
// Pj Glasso & EAILLM
// Tested with Breakout Board AOK with Xiao ESP32C3, C5, S3, BSP 3.3.5
// be sure to place demo.h & IMAGE.h files in the directory with skecth
// Use the image2hex tool to create bitmaps.
// https://www.seeedstudio.com/2-9-Quadruple-Color-ePaper-Display-with-128x296-Pixels-p-5783.html
// https://www.seeedstudio.com/ePaper-Breakout-Board-p-5804.html
//
#include <SPI.h>
#include"demo.h" //Seeed Studio Graphic
//#include"AP_29demo.h" //Good displays Graphic
#include"IMAGE.h" // Graphic basic 2.9 one.

//SCLK--GPIO23
//MOSI---GPIO18
int BUSY_Pin = 7;  //D5; //23 //works with S3 too...change the GPIO's
int RES_Pin = 2;   //D0; //0
int DC_Pin = 5;    //D3; //21
int CS_Pin = 3;    //D1; //01

#define EPD_W21_CS_0 digitalWrite(CS_Pin,LOW)
#define EPD_W21_CS_1 digitalWrite(CS_Pin,HIGH)
#define EPD_W21_DC_0  digitalWrite(DC_Pin,LOW)
#define EPD_W21_DC_1  digitalWrite(DC_Pin,HIGH)
#define EPD_W21_RST_0 digitalWrite(RES_Pin,LOW)
#define EPD_W21_RST_1 digitalWrite(RES_Pin,HIGH)
#define isEPD_W21_BUSY digitalRead(BUSY_Pin)

//2bit
#define black   0x00  /// 00
#define white   0x01  /// 01
#define yellow  0x02  /// 10
#define red     0x03  /// 11


#define Source_BITS 128
#define Gate_BITS   296
#define ALLSCREEN_BYTES Source_BITS*Gate_BITS/4

////////FUNCTION//////   
void SPI_Write(unsigned char value);
void EPD_W21_WriteCMD(unsigned char command);
void EPD_W21_WriteDATA(unsigned char datas);
//EPD
//EPD
void EPD_init(void);
void Acep_color(unsigned char color);
void PIC_display (const unsigned char* picData);
void Display_All_White(void);
void EPD_sleep(void);
void EPD_refresh(void);
void lcd_chkstatus(void);


void setup() {
  Serial.begin(115200);
  delay(600);
  Serial.println("\n=== PJ Forum Basic E-Paper test ===");

   pinMode(BUSY_Pin, INPUT); 
   pinMode(RES_Pin, OUTPUT);  
   pinMode(DC_Pin, OUTPUT);    
   pinMode(CS_Pin, OUTPUT);    
   //SPI
   SPI.begin();
   SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));

}

//Tips//
/*When the electronic paper is refreshed in full screen, the picture flicker is a normal phenomenon, and the main function is to clear the display afterimage in the previous picture.
  When the local refresh is performed, the screen does not flash.*/
/*When you need to transplant the driver, you only need to change the corresponding IO. The BUSY pin is the input mode and the others are the output mode. */
  
void loop() {

  ///////////////////////////Normal picture display/////////////////////////////////////////////////////////////////
    /************Normal picture display*******************/
    //EPD_init(); //EPD init
    //PIC_display(gImage_1);//EPD_picture1
    //EPD_sleep();//EPD_sleep,Sleep instruction is necessary, please do not delete!!!
    //delay(1000); //5s  
  EPD_init();
  PIC_display(gImage_2);   // one refresh
  EPD_sleep();
  while(1);                // stop (no repeated refreshes)
}

///////////////////EXTERNAL FUNCTION////////////////////////////////////////////////////////////////////////
/////////////////////delay//////////////////////////////////////

//////////////////////SPI///////////////////////////////////

void SPI_Write(unsigned char value)                                    
{                                                           
  SPI.transfer(value);
}

void EPD_W21_WriteCMD(unsigned char command)
{
  EPD_W21_CS_0;                   
  EPD_W21_DC_0;   // command write
  SPI_Write(command);
  EPD_W21_CS_1;
}
void EPD_W21_WriteDATA(unsigned char datas)
{
  EPD_W21_CS_0;                   
  EPD_W21_DC_1;   // command write
  SPI_Write(datas);
  EPD_W21_CS_1;
}

/////////////////EPD settings Functions/////////////////////

//////////////////////////////////////////////////////////////////////////////////////////////////
//JD
void EPD_init(void)
{
  delay(20);//At least 20ms delay  
  EPD_W21_RST_0;    // Module reset
  delay(40);//At least 40ms delay 
  EPD_W21_RST_1;
  delay(50);//At least 50ms delay 
  
  lcd_chkstatus();
  EPD_W21_WriteCMD(0x4D);
  EPD_W21_WriteDATA(0x78);

  EPD_W21_WriteCMD(0x00); //PSR
  EPD_W21_WriteDATA(0x0F);
  EPD_W21_WriteDATA(0x29);

  EPD_W21_WriteCMD(0x01); //PWRR
  EPD_W21_WriteDATA(0x07);
  EPD_W21_WriteDATA(0x00);
  
  EPD_W21_WriteCMD(0x03); //POFS
  EPD_W21_WriteDATA(0x10);
  EPD_W21_WriteDATA(0x54);
  EPD_W21_WriteDATA(0x44);
  
  EPD_W21_WriteCMD(0x06); //BTST_P
  EPD_W21_WriteDATA(0x05);
  EPD_W21_WriteDATA(0x00);
  EPD_W21_WriteDATA(0x3F);
  EPD_W21_WriteDATA(0x0A);
  EPD_W21_WriteDATA(0x25);
  EPD_W21_WriteDATA(0x12);
  EPD_W21_WriteDATA(0x1A); 

  EPD_W21_WriteCMD(0x50); //CDI
  EPD_W21_WriteDATA(0x37);
  
  EPD_W21_WriteCMD(0x60); //TCON
  EPD_W21_WriteDATA(0x02);
  EPD_W21_WriteDATA(0x02);
  
  EPD_W21_WriteCMD(0x61); //TRES
  EPD_W21_WriteDATA(Source_BITS/256);   // Source_BITS_H
  EPD_W21_WriteDATA(Source_BITS%256);   // Source_BITS_L
  EPD_W21_WriteDATA(Gate_BITS/256);     // Gate_BITS_H
  EPD_W21_WriteDATA(Gate_BITS%256);     // Gate_BITS_L  
  
  EPD_W21_WriteCMD(0xE7);
  EPD_W21_WriteDATA(0x1C);
  
  EPD_W21_WriteCMD(0xE3); 
  EPD_W21_WriteDATA(0x22);
  
  EPD_W21_WriteCMD(0xB4);
  EPD_W21_WriteDATA(0xD0);
  EPD_W21_WriteCMD(0xB5);
  EPD_W21_WriteDATA(0x03);
  
  EPD_W21_WriteCMD(0xE9);
  EPD_W21_WriteDATA(0x01); 

  EPD_W21_WriteCMD(0x30);
  EPD_W21_WriteDATA(0x08);  
  
  EPD_W21_WriteCMD(0x04);
  lcd_chkstatus(); 
  
}
//////////////////////////////All screen update////////////////////////////////////////////



/////////////////////////////////////////////////////////////////////////////////////////
void  EPD_refresh(void)
{
  EPD_W21_WriteCMD(0x12); //Display Update Control
  EPD_W21_WriteDATA(0x00);
  lcd_chkstatus();   
}

void EPD_sleep(void)
{  
  EPD_W21_WriteCMD(0X02);   //power off
  EPD_W21_WriteDATA(0x00);
  lcd_chkstatus();          //waiting for the electronic paper IC to release the idle sign   
 
  EPD_W21_WriteCMD(0X07);   //deep sleep
  EPD_W21_WriteDATA(0xA5);
}
void lcd_chkstatus(void)
{
  while (isEPD_W21_BUSY == 0) {
    delay(3);
  }
}



unsigned char Color_get(unsigned char color)
{
  unsigned datas;
  switch(color)
  {
    case 0x00:
      datas=white;  
      break;    
    case 0x01:
      datas=yellow;
      break;
    case 0x02:
      datas=red;
      break;    
    case 0x03:
      datas=black;
      break;      
    default:
      break;      
  }
   return datas;
}



void PIC_display(const unsigned char* picData)
{
  unsigned int i,j;
  unsigned char temp1;
  unsigned char data_H1,data_H2,data_L1,data_L2,datas;
   
  EPD_W21_WriteCMD(0x10);        
  for(i=0;i<Gate_BITS;i++)  //Source_BITS*Gate_BITS/4
  { 
    for(j=0;j<Source_BITS/4;j++)
    {   
      temp1=picData[i*Source_BITS/4+j]; 

      data_H1=Color_get(temp1>>6&0x03)<<6;      
      data_H2=Color_get(temp1>>4&0x03)<<4;
      data_L1=Color_get(temp1>>2&0x03)<<2;
      data_L2=Color_get(temp1&0x03);
      
      datas=data_H1|data_H2|data_L1|data_L2;
      EPD_W21_WriteDATA(datas);
    }
  } 
  
   //Refresh
    EPD_refresh();  
}
void Display_All_White(void)
{
  unsigned long i;
 
  EPD_W21_WriteCMD(0x10);
  {
    for(i=0;i<ALLSCREEN_BYTES;i++)
    {
      EPD_W21_WriteDATA(0x55);
    }
  }  
   EPD_refresh(); 
}
//////////////////////////////////END//////////////////////////////////////////////////

This code shows ways to do that:
Pre-render text → bitmap (easy, reliable)
Minimal bitmap font renderer (lightweight)
Hybrid (icons static + small dynamic text)
No GFX stack required.
No library dependency hell.
No surprise flicker.

This is how: ,Kindle, Shelf labels and Industrial HMI do it.

Here is the Serial Output for the basic test.

EPD Seq Rev 1.1
[EPD] WHITE
[EPD] BLACK
[EPD] YELLOW
[EPD] RED
[EPD] gImage_1
[EPD] CLEAR
[EPD] gImage_2
[EPD] IDLE BLANK
[EPD] WHITE
[EPD] BLACK
[EPD] YELLOW
[EPD] RED
[EPD] gImage_1
[EPD] CLEAR
[EPD] gImage_2
[EPD] IDLE BLANK

Basic_demo.zip (15.2 KB)

I ended up with this :grin:

Forum Activity & Rain Monitor

Next…

GL :slight_smile: PJ :v:

This is a WIP…

but it looks really GOOD :grin:

This demo shows a low-power, always-on information display built with the Seeed Studio XIAO ESP32-C3 and the 2.9" quad-color (Black / White / Red / Yellow) ePaper display.

It’s designed to be something you can hang on a wall or leave on a desk and glance at a few times a day, not a constantly refreshing screen.

What the demo displays

:pushpin: Forum Activity (Seeed Studio Discourse)

  • Large number (top-right)
    Daily total of new or updated forum topics (UTC-based, resets daily)
  • Small number (bottom-right)
    → Number of new or updated topics since the last check
  • Activity is detected using the forum’s
    latest.json endpoint (bumped_at, last_posted_at, created_at)

:sun_behind_rain_cloud: Weather Status (top-left)

  • Sun icon → No rain today
  • Cloud + rain icon → Rain expected today

This comes from Open-Meteo’s API using daily precipitation totals.
Only one simple value is used (precipitation_sum[0]) to keep the logic reliable and lightweight.


:high_voltage: Connectivity / Heartbeat Indicator (bottom-left)

  • A thin lightning bolt icon
  • Indicates successful forum connectivity on the last cycle
  • Drawn in solid black for maximum contrast on quad-color ePaper

:stopwatch: Timestamp

  • Shows the HH:MM (UTC) time of the most recent forum activity detected
  • Rendered using a custom bitmap font (no external font libraries)

How it works (high level)

:one: Wake → Connect → Fetch

On each cycle:

  • The ESP32-C3 wakes (light sleep or deep sleep selectable)
  • Connects to Wi-Fi
  • Fetches:
    • forum.seeedstudio.com/latest.json
    • api.open-meteo.com daily precipitation data

:two: Activity Detection (not polling spam)

Instead of redrawing every time:

  • The code stores the last activity timestamp in NVS (flash)
  • It compares new forum timestamps against the saved value
  • Only new or updated topics are counted

This avoids false positives and keeps the numbers meaningful.


:three: Daily Reset Logic

  • The code extracts the UTC date from forum timestamps
  • When the date changes:
    • Daily total resets to 0
    • Accumulation starts again automatically

No RTC or NTP sync is required.


:four: Change-Detection Display Updates (ePaper-friendly)

ePaper displays don’t like unnecessary refreshes, so this demo:

  • Stores the last rendered display state
  • Compares it with the newly computed state
  • Only refreshes the panel if something actually changed

This:

  • Saves power
  • Reduces ghosting
  • Extends panel life

:five: Custom Graphics Pipeline (no display libraries)

Instead of using a heavyweight display library:

  • A raw framebuffer is built in RAM
  • All text, numbers, and icons are drawn manually
  • Color mapping is done per-pixel for quad-color ePaper
  • The framebuffer is pushed to the panel in one clean refresh

This keeps memory use predictable and avoids timing issues.


Power behavior

  • Light Sleep mode (default)
    • USB stays connected
    • No Windows “disconnect/reconnect” sound
    • Ideal for development and desk use
  • Deep Sleep mode (optional)
    • Lowest power draw
    • Suitable for battery operation
    • Wakes on timer only

User interaction

  • BOOT button
    • Immediately shows a Hello / demo splash screen
    • Forces a full refresh
    • Then returns to normal operation

If there is an interest in the final code I can post it.

HTH
GL :slight_smile: PJ :v:

Go build something COOL today…

Seeed Studio!