ESP32-S3 E-Paper: Partial Refresh & 16-Level Grayscale

An e-paper project usually needs two different display behaviors: fast, crisp updates for text and controls, and richer tonal rendering for images or charts. On the ZECTRIX NOTE4 black-and-white hardware, the open-source ESP-IDF reference project provides both paths:

  • full-screen and partial 1-bit black-and-white refresh;
  • full-screen 4bpp / 16-level grayscale refresh.

This guide explains how the framebuffers differ, how to build and flash the reference project, how to prepare image assets, and how to switch refresh modes without creating an invalid partial-refresh state.

Compatibility warning: This guide targets the black-and-white ZECTRIX NOTE4 hardware and NOTE4 DevKit. It does not apply to the four-color NOTE4C. Their display drivers, waveforms, and firmware images are not interchangeable. Flashing the reference demo replaces the firmware currently installed on the device.

View the open-source reference project on GitHub · Watch the real-hardware 16-level grayscale demo

The three refresh paths

The public zectrix_epd component exposes three explicit refresh functions. Choosing the right one is more important than treating every screen as the same kind of bitmap.

Mode Buffer or patch size Update area Best suited to Required state
Full 1bpp 15,000 bytes Entire 400 × 300 panel First frame, clean redraws, high-contrast layouts No previous frame required
Partial 1bpp ceil(width / 8) × height bytes One rectangular region Counters, status values, selections, task rows Requires a successful full 1bpp base frame
Full 4bpp 60,000 bytes Entire 400 × 300 panel Photos, illustrations, shaded icons, grayscale charts Use a white full 1bpp refresh first for a cleaner transition

Full 1-bit black-and-white refresh

A 1bpp frame stores one bit per pixel. The reference format is row-major and MSB first: 0 is black and 1 is white.

For a 400 × 300 panel:

400 × 300 ÷ 8 = 15,000 bytes

Use a full 1bpp refresh when the display first starts, when the page changes substantially, or when accumulated artifacts need to be cleared. It also establishes the old-image state required by the partial-refresh path.

Partial 1-bit refresh

Partial refresh updates a tightly packed rectangular patch instead of sending a new full-screen frame. It is a good fit for changing a time, progress value, selected row, connection state, or other small UI region.

The patch must remain inside the 400 × 300 panel. Its required size is:

ceil(rect.width / 8) × rect.height

A successful full 1bpp refresh must happen before the first partial update. Applications should also perform a full refresh periodically to limit accumulated ghosting. If a partial refresh times out, the reference driver invalidates its shadow state; perform another full 1bpp refresh before sending more partial updates.

Full-screen 4bpp / 16-level grayscale

A 4bpp framebuffer assigns four bits to each pixel, producing 16 values from black to white. The left pixel is stored in the high nibble and the right pixel in the low nibble. In the reference format, 0 is black and 15 is white.

400 × 300 ÷ 2 = 60,000 bytes

This path is full-screen only. Use it for photographs, illustrations, weather scenes, shaded charts, or other content where tonal depth matters more than a small regional update.

For the cleanest transition, the reference demo first displays a white full 1bpp frame and then performs the 4bpp refresh. Before returning to partial 1bpp updates, send a new full 1bpp base frame.

Hardware used by the reference project

The project targets the current black-and-white NOTE4 development platform. The public configuration at the time this guide was prepared is:

Item Reference configuration
Display 4.2-inch black-and-white e-paper
Resolution 400 × 300 pixels
Display controller SSD2683
Processor ESP32-S3, dual-core Xtensa LX7
Memory 16 MB Flash and 8 MB octal PSRAM
Wireless 2.4 GHz Wi-Fi and Bluetooth LE
Framework ESP-IDF 5.4 or later

The default EPD wiring in zectrix_epd_get_default_config() uses the following values:

Signal Default
SPI host SPI3_HOST
Panel power GPIO6
BUSY GPIO8
RESET GPIO9
D/C GPIO10
CS GPIO11
SCLK GPIO12
MOSI GPIO13
SPI clock 20 MHz
BUSY timeout 2,000 ms

These defaults are board-specific. If you are porting the component to another revision or another SSD2683 board, override the configuration instead of assuming the same power rail and GPIO mapping.

Build and flash the reference demo

1. Prepare the toolchain

Install ESP-IDF 5.4 or later and open an ESP-IDF-enabled terminal. Confirm that the tools are active:

idf.py --version

Keep both ESP-IDF and the project in paths without spaces. The official ESP-IDF documentation notes that spaces are not supported in its build paths.

2. Clone and build

git clone https://github.com/itopinion/zectrix-note4-epd-demo.git
cd zectrix-note4-epd-demo

idf.py set-target esp32s3
idf.py build

The supplied defaults select 16 MB Flash, octal PSRAM, and a 3 MB factory application partition. The first configuration or build also downloads the official espressif/esp_codec_dev component.

idf.py set-target esp32s3 creates a local sdkconfig. If a later repository revision changes the supplied defaults, remove the generated sdkconfig before reapplying them.

3. Identify the correct serial port

Typical port names include:

  • Linux: /dev/ttyACM0 or /dev/ttyUSB0
  • macOS: /dev/cu.usbmodem*
  • Windows: a COM port

Confirm the exact device and port before flashing. Disconnecting other serial devices can reduce the chance of selecting the wrong target.

4. Flash and monitor

Replace the example port with the one detected on your computer:

idf.py -p /dev/ttyACM0 flash monitor

Exit ESP-IDF Monitor with Ctrl+].

Before flashing: Record the hardware batch and preserve the recovery files and instructions appropriate to your unit. This command writes the complete project, not just an application update, and replaces the installed firmware.

The repository also provides a v1.0.0 merged release image for the black-and-white NOTE4. That image is written from Flash offset 0x0; follow the release warning and your established recovery procedure before using it. Do not use it on NOTE4C.

Use the display component in another ESP-IDF project

Copy components/zectrix_epd into your ESP-IDF 5.4+ project and add zectrix_epd to the consuming component's REQUIRES list. The public API is a C header and can be called from C or C++.

Driver lifecycle

zectrix_epd_config_t config;
zectrix_epd_get_default_config(&config);

zectrix_epd_handle_t epd = NULL;
ESP_ERROR_CHECK(zectrix_epd_new(&config, &epd));
ESP_ERROR_CHECK(zectrix_epd_power_on(epd));

// Perform one or more valid refresh operations here.

ESP_ERROR_CHECK(zectrix_epd_power_off(epd));
ESP_ERROR_CHECK(zectrix_epd_del(epd));

zectrix_epd_new() configures GPIO and SPI but leaves the external display rail off. The refresh functions are synchronous and return after the SSD2683 BUSY handshake completes or the configured timeout expires.

Full 1bpp example

uint8_t frame[ZECTRIX_EPD_1BPP_FRAME_BYTES];
memset(frame, 0xFF, sizeof(frame));  // White

esp_err_t err = zectrix_epd_refresh_full_1bpp(
    epd, frame, sizeof(frame));

Partial 1bpp example

zectrix_epd_rect_t rect = {
    .x = 80,
    .y = 96,
    .width = 64,
    .height = 32,
};

uint8_t patch[(64 / 8) * 32];
memset(patch, 0x00, sizeof(patch));  // Black patch

esp_err_t err = zectrix_epd_refresh_partial_1bpp(
    epd, &rect, patch, sizeof(patch));

Do not run this partial example until a full 1bpp frame has completed successfully.

Full 4bpp example

uint8_t gray[ZECTRIX_EPD_4BPP_FRAME_BYTES];
memset(gray, 0xFF, sizeof(gray));  // Level 15 / white

esp_err_t err = zectrix_epd_refresh_full_4bpp(
    epd, gray, sizeof(gray));

The functions return standard esp_err_t values. In a long-running interface, handle recoverable display errors instead of automatically rebooting the whole device on every failed refresh.

Convert images to 1bpp or 4bpp

The repository includes two Python tools under tools/. They use Pillow to resize, crop, adjust contrast, create a preview PNG, and write the packed display buffer.

If Pillow is not already available in your Python environment:

python -m pip install Pillow

Prepare a black-and-white image

python tools/prepare_1bpp.py input.png preview-1bpp.png output-1bpp.bin

The default threshold is 160. Pixels below the threshold become black. Override it when the preview loses thin lines or fills too much of the image:

python tools/prepare_1bpp.py \
  input.png preview-1bpp.png output-1bpp.bin \
  --threshold 145

The binary output must be exactly 15,000 bytes.

Prepare a 16-level grayscale image

python tools/prepare_4bpp.py input.png preview-4bpp.png output-4bpp.bin

The tool converts the source to grayscale, fits it to 400 × 300, applies automatic contrast, then quantizes it to 16 levels. Its default contrast multiplier is 1.55:

python tools/prepare_4bpp.py \
  input.png preview-4bpp.png output-4bpp.bin \
  --contrast 1.35

The packed binary must be exactly 60,000 bytes. Inspect the generated preview before embedding it in firmware; a mathematically correct conversion can still produce poor separation on an actual e-paper panel.

A safe refresh sequence

For an interface that mixes fast text updates with occasional grayscale scenes, use an explicit state transition:

Power on panel
  -> Full 1bpp base frame
  -> Zero or more partial 1bpp updates
  -> White full 1bpp refresh
  -> Full 4bpp grayscale frame
  -> Full 1bpp base frame
  -> Resume partial 1bpp updates

This sequence matters because partial refresh relies on a trustworthy black-and-white old-image buffer. The 4bpp path intentionally invalidates partial-refresh use until a new full 1bpp frame establishes that base again.

Troubleshooting

idf.py build cannot download a component

Check access to the ESP Component Registry and retry. The demo declares espressif/esp_codec_dev in main/idf_component.yml.

The board repeatedly resets after flashing

Confirm all of the following:

  • the target is esp32s3;
  • the hardware has 16 MB Flash and octal PSRAM;
  • the complete project was flashed, not only the application binary;
  • a stale sdkconfig is not overriding the supplied defaults.

The display reports a BUSY timeout

Check the board revision, display cable, panel power, and EPD GPIO mapping. Do not start partial refresh before a successful full 1bpp base refresh. A timeout can leave the physical window partly updated, so recover with a full 1bpp refresh rather than continuing with more patches.

Partial updates show increasing ghosting

Partial refresh is not a permanent substitute for full refresh. Schedule a full 1bpp redraw after a suitable number of UI changes or when transitioning between major screens. The correct interval depends on content, temperature, waveform behavior, and the visual quality your application requires.

The grayscale image looks flat or loses detail

Inspect the 16-level preview, then adjust source composition and the conversion tool's contrast value. Avoid expecting LCD-like continuous tones: the output has exactly 16 encoded levels, and the physical result depends on the calibrated waveform and panel behavior.

The device does not fully power off while USB is connected

This is expected in the reference demo. USB keeps the power rail active, so the firmware clears the panel and enters deep sleep. Disconnect USB when validating battery-latch shutdown.

A refresh call returns an argument or size error

Check the exact buffer rules:

  • full 1bpp: 15,000 bytes;
  • full 4bpp: 60,000 bytes;
  • partial 1bpp: ceil(width / 8) × height bytes;
  • every rectangle must stay inside 400 × 300.

What the reference demo can test

The project is more than a screen-only example. Its English UI includes tests for:

  • 2.4 GHz Wi-Fi scanning and optional RSSI qualification;
  • speaker-to-microphone acoustic loopback;
  • PCF8563 RTC behavior;
  • charging state and battery measurement;
  • status LED and three physical buttons;
  • NFC read, temporary write, field detection, and data restoration;
  • Flash, PSRAM, MAC address, peripheral, and power information.

These tests are useful for checking a development unit before you replace the demo with your own application. They do not turn the project into the NOTE4 consumer firmware or cloud service.

Choose the correct ZECTRIX device

Device Intended use Display path Firmware boundary
NOTE4 Ready-to-use consumer AI e-paper product Black-and-white display with the consumer experience Do not flash this reference demo unless you intentionally accept replacing the installed consumer firmware
NOTE4 DevKit Development and custom firmware Full/partial 1bpp and full-screen 4bpp in the open reference project Target device for this guide; first batches use beta demo firmware
NOTE4C DevKit Four-color e-paper development Black, white, red, and yellow static-display path Incompatible with this SSD2683 black-and-white reference image

Next steps and resources

Source and version scope

This guide was prepared against the public reference repository at commit ca285c98ed0641f86780edb1f5ec77b0335fe649 and the v1.0.0 release published on August 7, 2026. Recheck the repository README, component API, hardware map, and release notes before using the instructions with a later hardware or firmware revision.