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:
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 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 |
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 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.
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.
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.
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.
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.
Typical port names include:
/dev/ttyACM0 or /dev/ttyUSB0/dev/cu.usbmodem*COM portConfirm the exact device and port before flashing. Disconnecting other serial devices can reduce the chance of selecting the wrong target.
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.
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++.
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.
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));
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.
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.
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
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.
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.
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.
idf.py build cannot download a componentCheck access to the ESP Component Registry and retry. The demo declares espressif/esp_codec_dev in main/idf_component.yml.
Confirm all of the following:
esp32s3;sdkconfig is not overriding the supplied defaults.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 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.
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.
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.
Check the exact buffer rules:
ceil(width / 8) × height bytes;The project is more than a screen-only example. Its English UI includes tests for:
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.
| 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 |
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.