mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 12:12:48 +00:00
[epaper_spi] Add the UC8279 controller and the Xteink X4 Pro (#20545)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
co-authored by
clydebarrow
parent
325c747dcd
commit
ae62960193
@@ -30,6 +30,7 @@ from esphome.const import (
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODEL,
|
||||
CONF_OFFSET_HEIGHT,
|
||||
CONF_PAGES,
|
||||
CONF_RESET_DURATION,
|
||||
CONF_RESET_PIN,
|
||||
@@ -73,12 +74,17 @@ for module_info in pkgutil.iter_modules(models.__path__):
|
||||
|
||||
MODELS = models.EpaperModel.models
|
||||
|
||||
DIMENSION_SCHEMA = cv.Schema(
|
||||
{
|
||||
|
||||
def dimension_schema(model: models.EpaperModel) -> cv.Schema:
|
||||
schema = {
|
||||
cv.Required(CONF_WIDTH): cv.int_,
|
||||
cv.Required(CONF_HEIGHT): cv.int_,
|
||||
}
|
||||
)
|
||||
# Only models with an offset_height default use it, and the default must survive explicit dimensions
|
||||
if (offset_height := model.get_default(CONF_OFFSET_HEIGHT, None)) is not None:
|
||||
schema[cv.Optional(CONF_OFFSET_HEIGHT, default=offset_height)] = cv.positive_int
|
||||
return cv.Schema(schema)
|
||||
|
||||
|
||||
TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
|
||||
|
||||
@@ -135,7 +141,7 @@ def model_schema(config):
|
||||
model.option(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
cv.GenerateID(): cv.declare_id(class_name),
|
||||
cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8),
|
||||
cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA,
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(model),
|
||||
model.option(CONF_ENABLE_PIN): cv.ensure_list(pins.gpio_output_pin_schema),
|
||||
model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list(
|
||||
map_sequence
|
||||
|
||||
@@ -227,10 +227,12 @@ void EPaperBase::process_state_() {
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
return;
|
||||
}
|
||||
// A full update resets update_count_ to zero, so a non-zero count marks a partial one
|
||||
this->partial_ = this->update_count_ != 0;
|
||||
this->set_state_(EPaperState::RESET);
|
||||
break;
|
||||
case EPaperState::INITIALISE:
|
||||
if (!this->initialise(this->update_count_ != 0)) {
|
||||
if (!this->initialise(this->partial_)) {
|
||||
return; // Not done yet, come back next loop
|
||||
}
|
||||
this->set_state_(EPaperState::TRANSFER_DATA);
|
||||
@@ -250,11 +252,14 @@ void EPaperBase::process_state_() {
|
||||
this->set_state_(EPaperState::REFRESH_SCREEN);
|
||||
break;
|
||||
case EPaperState::REFRESH_SCREEN:
|
||||
this->refresh_screen(this->update_count_ != 0);
|
||||
this->refresh_screen(this->partial_);
|
||||
this->update_count_ = (this->update_count_ + 1) % this->full_update_every_;
|
||||
this->set_state_(EPaperState::POWER_OFF);
|
||||
break;
|
||||
case EPaperState::POWER_OFF:
|
||||
if (!this->after_refresh()) {
|
||||
return; // Not done yet, come back next loop
|
||||
}
|
||||
this->power_off();
|
||||
this->set_state_(EPaperState::DEEP_SLEEP);
|
||||
break;
|
||||
|
||||
@@ -167,6 +167,13 @@ class EPaperBase : public Display,
|
||||
*/
|
||||
virtual void power_off() = 0;
|
||||
|
||||
/**
|
||||
* Called after the refresh, before power off, repeatedly until it returns true.
|
||||
* Lets a driver do extra work spread over several loops, such as rewriting the previous image.
|
||||
* @return true when done, false to be called again next loop
|
||||
*/
|
||||
virtual bool after_refresh() { return true; }
|
||||
|
||||
/**
|
||||
* Place the display into deep sleep
|
||||
*/
|
||||
@@ -201,6 +208,7 @@ class EPaperBase : public Display,
|
||||
uint8_t effective_transform_{};
|
||||
uint8_t update_count_{};
|
||||
bool full_update_requested_{};
|
||||
bool partial_{}; // the update in progress is a partial one; set when UPDATE moves to RESET
|
||||
// these values represent the bounds of the updated buffer. Note that x_high and y_high
|
||||
// point to the pixel past the last one updated, i.e. may range up to width/height.
|
||||
uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{};
|
||||
|
||||
@@ -10,12 +10,11 @@ ESPHOME_LOG_TAG(TAG, "epaper_spi.uc8179");
|
||||
|
||||
bool EPaperUC8179::initialise(bool partial) {
|
||||
EPaperBase::initialise(partial); // send the model init sequence
|
||||
this->partial_ = partial;
|
||||
ESP_LOGV(TAG, "Power on");
|
||||
// POWER ON must precede the waveform/mode registers and the data transfer
|
||||
// (the original driver powers on and busy-waits before writing them).
|
||||
// The state machine busy-waits before entering TRANSFER_DATA.
|
||||
this->command(0x04);
|
||||
this->command(CMD_POWER_ON);
|
||||
// Give the busy line time to assert before the state machine polls it
|
||||
this->next_delay_ = 100;
|
||||
return true;
|
||||
@@ -27,21 +26,17 @@ void EPaperUC8179::set_refresh_mode_() {
|
||||
return; // plain full refresh uses the mode set by the init sequence
|
||||
}
|
||||
// Fast and partial refresh use flipped data polarity and a floating border
|
||||
this->cmd_data(0x50, {0xA9, 0x07});
|
||||
this->cmd_data(CMD_CDI, {0xA9, 0x07});
|
||||
// Force the waveform via the temperature registers: 0x5A selects the fast
|
||||
// full-refresh waveform, 0x6E the partial-refresh waveform
|
||||
this->cmd_data(0xE0, {0x02});
|
||||
this->cmd_data(CMD_CCSET, {0x02});
|
||||
if (this->partial_) {
|
||||
this->cmd_data(0xE5, {0x6E});
|
||||
this->command(0x91); // enter partial mode
|
||||
// Set the partial window to the full screen
|
||||
const uint16_t x_end = this->width_ - 1;
|
||||
const uint16_t y_end = this->height_ - 1;
|
||||
this->cmd_data(0x90, {0x00, 0x00, static_cast<uint8_t>(x_end >> 8), static_cast<uint8_t>(x_end & 0xFF), 0x00, 0x00,
|
||||
static_cast<uint8_t>(y_end >> 8), static_cast<uint8_t>(y_end & 0xFF), 0x01});
|
||||
this->cmd_data(CMD_TSSET, {0x6E});
|
||||
// Partial mode, with the window set to the full screen
|
||||
this->set_partial_window_(0, this->width_ - 1, 0, this->height_ - 1);
|
||||
} else {
|
||||
this->cmd_data(0xE5, {0x5A});
|
||||
this->command(0x92); // exit partial mode
|
||||
this->cmd_data(CMD_TSSET, {0x5A});
|
||||
this->command(CMD_PARTIAL_OUT); // exit partial mode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +58,7 @@ bool HOT EPaperUC8179::transfer_data() {
|
||||
// Phase 1 (fast full refresh only): previous image via 0x10 (DTM1), inverse of the new image
|
||||
if (two_pass && this->current_data_index_ < buffer_length) {
|
||||
if (this->current_data_index_ == 0) {
|
||||
this->command(0x10); // DATA START TRANSMISSION 1 (previous image)
|
||||
this->command(CMD_DTM1); // DATA START TRANSMISSION 1 (previous image)
|
||||
}
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ < buffer_length) {
|
||||
@@ -86,7 +81,7 @@ bool HOT EPaperUC8179::transfer_data() {
|
||||
const size_t total = offset + buffer_length;
|
||||
if (this->current_data_index_ < total) {
|
||||
if (this->current_data_index_ == offset) {
|
||||
this->command(0x13); // DATA START TRANSMISSION 2 (new image)
|
||||
this->command(CMD_DTM2); // DATA START TRANSMISSION 2 (new image)
|
||||
}
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ < total) {
|
||||
@@ -117,7 +112,7 @@ void EPaperUC8179::power_on() {
|
||||
|
||||
void EPaperUC8179::refresh_screen(bool /*partial*/) {
|
||||
ESP_LOGV(TAG, "Refresh");
|
||||
this->command(0x12); // DISPLAY REFRESH
|
||||
this->command(CMD_REFRESH); // DISPLAY REFRESH
|
||||
// Delay the next busy poll: the busy line takes a short time to assert after
|
||||
// the refresh command, and polling too early would read it as already idle
|
||||
this->next_delay_ = 100;
|
||||
@@ -125,15 +120,7 @@ void EPaperUC8179::refresh_screen(bool /*partial*/) {
|
||||
|
||||
void EPaperUC8179::power_off() {
|
||||
ESP_LOGV(TAG, "Power off");
|
||||
this->command(0x02); // POWER OFF
|
||||
}
|
||||
|
||||
void EPaperUC8179::deep_sleep() {
|
||||
// Deep sleep loses the previous-image RAM that partial refresh compares against
|
||||
if (!this->is_using_partial_update_()) {
|
||||
ESP_LOGV(TAG, "Deep sleep");
|
||||
this->cmd_data(0x07, {0xA5}); // DEEP SLEEP with check code
|
||||
}
|
||||
this->command(CMD_POWER_OFF); // POWER OFF
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi.h"
|
||||
#include "epaper_spi_ultrachip.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
@@ -28,11 +28,11 @@ namespace esphome::epaper_spi {
|
||||
* waveform is forced, partial mode is entered with a full-screen window and
|
||||
* only DTM2 is sent; the controller compares against its previous-image RAM.
|
||||
*/
|
||||
class EPaperUC8179 final : public EPaperBase {
|
||||
class EPaperUC8179 final : public EPaperUltraChip {
|
||||
public:
|
||||
EPaperUC8179(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length)
|
||||
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {}
|
||||
: EPaperUltraChip(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {}
|
||||
|
||||
protected:
|
||||
bool initialise(bool partial) override;
|
||||
@@ -40,11 +40,7 @@ class EPaperUC8179 final : public EPaperBase {
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_on() override;
|
||||
void power_off() override;
|
||||
void deep_sleep() override;
|
||||
void set_refresh_mode_();
|
||||
|
||||
// Set by initialise() so transfer_data() knows which planes to send
|
||||
bool partial_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
#include "epaper_spi_uc8279.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
ESPHOME_LOG_TAG(TAG, "epaper_spi.uc8279");
|
||||
|
||||
static constexpr uint8_t CMD_PSR = 0x00;
|
||||
static constexpr uint8_t CMD_PFS = 0x03;
|
||||
static constexpr uint8_t CMD_GATE_SCAN = 0xE1;
|
||||
|
||||
static constexpr uint8_t PSR0_OTP_LUT = 0x17;
|
||||
static constexpr uint8_t PSR1 = 0x4D;
|
||||
static constexpr uint8_t PFS = 0x20;
|
||||
static constexpr uint8_t GATE_SCAN = 0x02;
|
||||
static constexpr uint8_t CCSET = 0x02;
|
||||
static constexpr uint8_t TSSET_FULL = 0x1E;
|
||||
static constexpr uint8_t TSSET_PARTIAL = 0x5A;
|
||||
static constexpr uint8_t CDI_FULL = 0x97;
|
||||
static constexpr uint8_t CDI_PARTIAL = 0xD7;
|
||||
|
||||
static constexpr uint16_t RESET_SETTLE_MS = 50;
|
||||
static constexpr uint16_t BUSY_ASSERT_MS = 10;
|
||||
static constexpr uint16_t REFRESH_START_MS = 50;
|
||||
|
||||
bool EPaperUC8279::reset() {
|
||||
// A partial update follows one that left the controller configured, in standby, with DTM1 holding the image
|
||||
if (this->partial_)
|
||||
return true;
|
||||
if (!EPaperBase::reset())
|
||||
return false;
|
||||
this->next_delay_ = RESET_SETTLE_MS;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EPaperUC8279::initialise(bool partial) {
|
||||
if (partial)
|
||||
return true;
|
||||
return EPaperBase::initialise(partial);
|
||||
}
|
||||
|
||||
void EPaperUC8279::write_gate_(size_t gate, bool white) {
|
||||
const size_t row_width = this->row_width_;
|
||||
if (white || gate < this->offset_height_) {
|
||||
uint8_t white_bytes[MAX_TRANSFER_SIZE];
|
||||
memset(white_bytes, 0xFF, sizeof(white_bytes));
|
||||
for (size_t written = 0; written != row_width;) {
|
||||
const size_t length = std::min(row_width - written, MAX_TRANSFER_SIZE);
|
||||
this->write_array(white_bytes, length);
|
||||
written += length;
|
||||
}
|
||||
return;
|
||||
}
|
||||
size_t index = (gate - this->offset_height_) * row_width;
|
||||
for (size_t remaining = row_width; remaining != 0;) {
|
||||
size_t length;
|
||||
const uint8_t *data = this->buffer_.get_span(index, length);
|
||||
length = std::min(length, remaining);
|
||||
this->write_array(data, length);
|
||||
index += length;
|
||||
remaining -= length;
|
||||
}
|
||||
}
|
||||
|
||||
// One plane of one gate's row per step, time-sliced; `base` is the value of current_data_index_ at its first gate
|
||||
bool HOT EPaperUC8279::write_plane_(uint8_t command, bool white, size_t base) {
|
||||
const uint32_t start_time = millis();
|
||||
const size_t gates = this->gate_count_();
|
||||
if (this->current_data_index_ == base)
|
||||
this->command(command);
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ - base != gates) {
|
||||
this->write_gate_(this->current_data_index_ - base, white);
|
||||
++this->current_data_index_;
|
||||
if (this->current_data_index_ - base != gates && millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->disable();
|
||||
return true;
|
||||
}
|
||||
|
||||
// DTM2 gets the new image; a full refresh also writes a white DTM1, so every pixel transitions
|
||||
bool HOT EPaperUC8279::transfer_data() {
|
||||
const size_t gates = this->gate_count_();
|
||||
if (this->current_data_index_ < gates) {
|
||||
if (!this->write_plane_(CMD_DTM2, false, 0))
|
||||
return false;
|
||||
// Start DTM1 on the next pass, so this one stays within its time slice
|
||||
if (!this->partial_)
|
||||
return false;
|
||||
}
|
||||
if (!this->partial_ && !this->write_plane_(CMD_DTM1, true, gates))
|
||||
return false;
|
||||
this->current_data_index_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperUC8279::power_on() {
|
||||
ESP_LOGV(TAG, "Power on");
|
||||
this->cmd_data(CMD_CDI, {this->partial_ ? CDI_PARTIAL : CDI_FULL});
|
||||
this->cmd_data(CMD_CCSET, {CCSET});
|
||||
this->cmd_data(CMD_TSSET, {this->partial_ ? TSSET_PARTIAL : TSSET_FULL});
|
||||
if (this->partial_) {
|
||||
this->cmd_data(CMD_PFS, {PFS});
|
||||
this->cmd_data(CMD_GATE_SCAN, {GATE_SCAN});
|
||||
}
|
||||
this->command(CMD_POWER_ON);
|
||||
this->next_delay_ = BUSY_ASSERT_MS;
|
||||
}
|
||||
|
||||
void EPaperUC8279::refresh_screen(bool partial) {
|
||||
ESP_LOGV(TAG, "Refresh screen (%s)", partial ? LOG_STR_LITERAL("partial") : LOG_STR_LITERAL("full"));
|
||||
if (partial)
|
||||
this->set_partial_window_(0, this->width_ - 1, this->offset_height_, this->gate_count_() - 1);
|
||||
// Power-on reloads the panel's stored settings, so the panel setting only takes when written after it
|
||||
this->cmd_data(CMD_PSR, {PSR0_OTP_LUT, PSR1});
|
||||
this->command(CMD_REFRESH);
|
||||
this->next_delay_ = REFRESH_START_MS;
|
||||
}
|
||||
|
||||
bool HOT EPaperUC8279::after_refresh() {
|
||||
// While partial updates are in use, DTM1 holds the image on the panel for the next partial comparison
|
||||
if (!this->is_using_partial_update_())
|
||||
return true;
|
||||
if (this->current_data_index_ == 0 && this->partial_)
|
||||
this->command(CMD_PARTIAL_OUT);
|
||||
if (!this->write_plane_(CMD_DTM1, false, 0))
|
||||
return false;
|
||||
this->current_data_index_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperUC8279::power_off() {
|
||||
this->command(CMD_POWER_OFF);
|
||||
this->next_delay_ = BUSY_ASSERT_MS;
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi_ultrachip.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
/**
|
||||
* Monochrome e-paper displays using the UltraChip UC8279 controller, such as the Xteink X4 Pro.
|
||||
*
|
||||
* Buffer layout: 1 bit per pixel, 1=white, 0=black (the base class default).
|
||||
*
|
||||
* The controller scans offset_height + height gates; the panel is bonded to the last height of them,
|
||||
* so every image plane is written for all gates, white outside the panel.
|
||||
*
|
||||
* A full refresh writes the new image to DTM2 and white to DTM1. A partial refresh writes only DTM2
|
||||
* and drives the pixels that differ from DTM1, which therefore has to hold the image on the panel:
|
||||
* while partial updates are in use it is rewritten after every refresh, in time slices like the
|
||||
* image transfer. The panel loses both planes in its own deep sleep, so it then waits in standby
|
||||
* between updates instead.
|
||||
*/
|
||||
class EPaperUC8279 : public EPaperUltraChip {
|
||||
public:
|
||||
EPaperUC8279(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length, uint16_t offset_height)
|
||||
: EPaperUltraChip(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY),
|
||||
offset_height_(offset_height) {}
|
||||
|
||||
protected:
|
||||
bool reset() override;
|
||||
bool initialise(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
void power_on() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_off() override;
|
||||
bool after_refresh() override;
|
||||
|
||||
uint16_t gate_count_() const { return this->offset_height_ + this->height_; }
|
||||
// Writes one gate's row of the image, or white for a gate outside the panel or when white is true
|
||||
void write_gate_(size_t gate, bool white);
|
||||
// Writes one plane in time slices, sending `command` at its first gate; true once all gates are written
|
||||
bool write_plane_(uint8_t command, bool white, size_t base);
|
||||
|
||||
uint16_t offset_height_;
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "epaper_spi_ultrachip.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "epaper_spi.ultrachip");
|
||||
|
||||
static constexpr uint8_t hi(uint16_t v) { return static_cast<uint8_t>(v >> 8); }
|
||||
static constexpr uint8_t lo(uint16_t v) { return static_cast<uint8_t>(v & 0xFF); }
|
||||
|
||||
void EPaperUltraChip::deep_sleep() {
|
||||
// Partial updates compare against the previous image, which deep sleep would lose; standby keeps it
|
||||
if (this->is_using_partial_update_())
|
||||
return;
|
||||
this->send_deep_sleep_();
|
||||
}
|
||||
|
||||
void EPaperUltraChip::send_deep_sleep_() {
|
||||
ESP_LOGV(TAG, "Deep sleep");
|
||||
this->cmd_data(CMD_DEEP_SLEEP, {DEEP_SLEEP_CHECK});
|
||||
}
|
||||
|
||||
void EPaperUltraChip::on_safe_shutdown() { this->send_deep_sleep_(); }
|
||||
|
||||
void EPaperUltraChip::set_partial_window_(uint16_t x_start, uint16_t x_end, uint16_t y_start, uint16_t y_end) {
|
||||
this->command(CMD_PARTIAL_IN);
|
||||
this->cmd_data(CMD_PARTIAL_WINDOW,
|
||||
{hi(x_start), static_cast<uint8_t>(lo(x_start) & 0xF8), hi(x_end),
|
||||
static_cast<uint8_t>(lo(x_end) | 0x07), hi(y_start), lo(y_start), hi(y_end), lo(y_end), 0x01});
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
/**
|
||||
* Shared base for the UltraChip UC81xx mono controllers (UC8179, UC8279).
|
||||
*
|
||||
* They use the same command set and the same deep-sleep and partial-window handling; the
|
||||
* concrete drivers differ only in how each refresh is configured and which planes are sent.
|
||||
*/
|
||||
class EPaperUltraChip : public EPaperBase {
|
||||
public:
|
||||
using EPaperBase::EPaperBase;
|
||||
|
||||
// Nothing carries the image across a restart or the controller's deep sleep, so sleep the panel rather than
|
||||
// leaving it waiting in standby
|
||||
void on_safe_shutdown() override;
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t CMD_POWER_OFF = 0x02;
|
||||
static constexpr uint8_t CMD_POWER_ON = 0x04;
|
||||
static constexpr uint8_t CMD_DEEP_SLEEP = 0x07;
|
||||
static constexpr uint8_t CMD_DTM1 = 0x10;
|
||||
static constexpr uint8_t CMD_REFRESH = 0x12;
|
||||
static constexpr uint8_t CMD_DTM2 = 0x13;
|
||||
static constexpr uint8_t CMD_CDI = 0x50;
|
||||
static constexpr uint8_t CMD_PARTIAL_WINDOW = 0x90;
|
||||
static constexpr uint8_t CMD_PARTIAL_IN = 0x91;
|
||||
static constexpr uint8_t CMD_PARTIAL_OUT = 0x92;
|
||||
static constexpr uint8_t CMD_CCSET = 0xE0;
|
||||
static constexpr uint8_t CMD_TSSET = 0xE5;
|
||||
static constexpr uint8_t DEEP_SLEEP_CHECK = 0xA5;
|
||||
|
||||
void deep_sleep() override;
|
||||
// Sends the deep-sleep command unconditionally; deep_sleep() skips it while partial updates are in use
|
||||
void send_deep_sleep_();
|
||||
// Enters partial mode and sets the refresh window; x is rounded out to whole bytes
|
||||
void set_partial_window_(uint16_t x_start, uint16_t x_end, uint16_t y_start, uint16_t y_end);
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -1,12 +1,17 @@
|
||||
from typing import Any, Self
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH
|
||||
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_OFFSET_HEIGHT, CONF_WIDTH
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
def ultrachip_resolution(width: int, gates: int) -> tuple:
|
||||
"""The UC81xx resolution command (0x61): source (width) and gate counts, each big-endian."""
|
||||
return (0x61, width >> 8, width & 0xFF, gates >> 8, gates & 0xFF)
|
||||
|
||||
|
||||
class EpaperModel:
|
||||
models: dict[str, Self] = {}
|
||||
|
||||
@@ -86,6 +91,13 @@ class EpaperModel:
|
||||
height = self.get_default(CONF_HEIGHT)
|
||||
return width, height
|
||||
|
||||
def get_offset_height(self, config) -> int:
|
||||
# The panel's first row within the controller's addressable area; from explicit dimensions or the model default
|
||||
dimensions = config.get(CONF_DIMENSIONS, {})
|
||||
return dimensions.get(
|
||||
CONF_OFFSET_HEIGHT, self.get_default(CONF_OFFSET_HEIGHT, 0)
|
||||
)
|
||||
|
||||
def extend(self, name, **kwargs) -> "EpaperModel":
|
||||
"""
|
||||
Extend the current model with additional parameters or a modified init sequence.
|
||||
|
||||
@@ -18,7 +18,7 @@ from typing import Any
|
||||
|
||||
from esphome.const import CONF_DATA_RATE
|
||||
|
||||
from . import EpaperModel
|
||||
from . import EpaperModel, ultrachip_resolution
|
||||
|
||||
|
||||
class UC8179(EpaperModel):
|
||||
@@ -51,13 +51,7 @@ class UC8179(EpaperModel):
|
||||
# PANEL SETTING (black/white mode, LUT from OTP)
|
||||
(0x00, 0x1F),
|
||||
# RESOLUTION SETTING (width x height)
|
||||
(
|
||||
0x61,
|
||||
(width >> 8) & 0xFF,
|
||||
width & 0xFF,
|
||||
(height >> 8) & 0xFF,
|
||||
height & 0xFF,
|
||||
),
|
||||
ultrachip_resolution(width, height),
|
||||
# DUAL SPI MODE (disabled)
|
||||
(0x15, 0x00),
|
||||
# VCOM AND DATA INTERVAL SETTING
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Monochrome e-paper displays using the UltraChip UC8279 controller.
|
||||
|
||||
Supported models:
|
||||
- uc8279: the controller on its own; dimensions and pins must be configured
|
||||
- xteink-x4pro-uc8279: the Xteink X4 Pro e-reader (ESP32-S3) with its 800x480 UC8279 panel;
|
||||
the X4 Pro also ships with other panel controllers
|
||||
|
||||
The controller scans offset_height + height gates, and the panel is bonded to the last
|
||||
height of them; offset_height (under dimensions) is a property of the panel.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA_RATE, CONF_DIMENSIONS, CONF_OFFSET_HEIGHT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import EpaperModel, ultrachip_resolution
|
||||
|
||||
|
||||
class UC8279(EpaperModel):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
class_name: str = "EPaperUC8279",
|
||||
data_rate: str = "10MHz",
|
||||
**defaults: Any,
|
||||
) -> None:
|
||||
defaults.setdefault(CONF_DATA_RATE, data_rate)
|
||||
defaults.setdefault("partial_update", True)
|
||||
defaults.setdefault(CONF_OFFSET_HEIGHT, 0)
|
||||
defaults.setdefault("width_multiple", 8)
|
||||
super().__init__(name, class_name=class_name, **defaults)
|
||||
|
||||
def validate_config(self, config: ConfigType) -> ConfigType:
|
||||
_width, height = self.get_dimensions(config)
|
||||
if self.get_offset_height(config) + height > 0xFFFF:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_OFFSET_HEIGHT} plus the panel height must not exceed {0xFFFF} gates",
|
||||
path=[CONF_DIMENSIONS, CONF_OFFSET_HEIGHT],
|
||||
)
|
||||
return config
|
||||
|
||||
def get_constructor_args(self, config) -> tuple:
|
||||
return (self.get_offset_height(config),)
|
||||
|
||||
# fmt: off
|
||||
def get_init_sequence(self, config: dict) -> tuple:
|
||||
width, height = self.get_dimensions(config)
|
||||
gates = self.get_offset_height(config) + height
|
||||
return (
|
||||
(0x00, 0x37, 0x4D), # panel setting; the driver switches it to the OTP LUT before each refresh
|
||||
ultrachip_resolution(width, gates), # resolution: sources, gates
|
||||
(0x65, 0x00, 0x00, 0x00, 0x00), # gate and source start
|
||||
(0x03, 0x20), # power off sequence
|
||||
(0x30, 0x0E), # PLL
|
||||
(0xE1, 0x02), # gate scan
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
|
||||
uc8279 = UC8279("uc8279")
|
||||
|
||||
uc8279.extend(
|
||||
"xteink-x4pro-uc8279",
|
||||
width=800,
|
||||
height=480,
|
||||
offset_height=120,
|
||||
cs_pin=13,
|
||||
dc_pin=18,
|
||||
reset_pin=14,
|
||||
busy_pin={"number": 6, "inverted": True},
|
||||
reset_duration="50ms",
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
variant: esp32s3
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO12
|
||||
mosi_pin: GPIO11
|
||||
|
||||
display:
|
||||
- platform: epaper_spi
|
||||
id: epaper_display
|
||||
model: xteink-x4pro-uc8279
|
||||
@@ -13,7 +13,12 @@ from esphome.components.epaper_spi.display import (
|
||||
FINAL_VALIDATE_SCHEMA,
|
||||
MODELS,
|
||||
)
|
||||
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
|
||||
from esphome.components.esp32 import (
|
||||
KEY_BOARD,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.const import (
|
||||
CONF_BUSY_PIN,
|
||||
CONF_CS_PIN,
|
||||
@@ -516,6 +521,140 @@ def test_uc8179_e1001_code_generation(
|
||||
assert "97, 4, 3, 32, 1, 224" in main_cpp
|
||||
|
||||
|
||||
def test_uc8279_width_must_be_a_multiple_of_8(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""The UC8279 takes whole bytes of sources, so the width is a multiple of 8."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
|
||||
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
|
||||
|
||||
with pytest.raises(cv.Invalid, match="multiple of 8"):
|
||||
run_schema_validation(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "uc8279",
|
||||
"dc_pin": 21,
|
||||
"dimensions": {"width": 804, "height": 480},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_uc8279_x4pro_code_generation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The Xteink X4 Pro generates the UC8279 driver with the panel's gate offset and gate count."""
|
||||
main_cpp = generate_main(component_config_path("uc8279_x4pro_test.yaml"))
|
||||
|
||||
assert "epaper_spi::EPaperUC8279" in main_cpp
|
||||
# 800 sources, 480 rows, and the panel on the last 480 of 600 gates
|
||||
assert re.search(
|
||||
r'"XTEINK-X4PRO-UC8279",\s*800,\s*480,.*?,\s*120\)', main_cpp, re.DOTALL
|
||||
)
|
||||
resolution = next(
|
||||
cmd
|
||||
for cmd in MODELS["XTEINK-X4PRO-UC8279"].get_init_sequence({})
|
||||
if cmd[0] == 0x61
|
||||
)
|
||||
assert resolution == (0x61, 0x03, 0x20, 0x02, 0x58)
|
||||
|
||||
|
||||
def test_uc8279_offset_height_configurable(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""The generic uc8279 model takes offset_height under dimensions, reaching the constructor and gate count."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
|
||||
|
||||
config = run_schema_validation(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "uc8279",
|
||||
"dc_pin": 21,
|
||||
"dimensions": {"width": 16, "height": 2, "offset_height": 8},
|
||||
}
|
||||
)
|
||||
model = MODELS["UC8279"]
|
||||
assert model.get_constructor_args(config) == (8,)
|
||||
resolution = next(cmd for cmd in model.get_init_sequence(config) if cmd[0] == 0x61)
|
||||
# 16 sources, and 8 + 2 = 10 gates
|
||||
assert resolution == (0x61, 0x00, 0x10, 0x00, 0x0A)
|
||||
|
||||
|
||||
def test_uc8279_offset_height_plus_height_must_fit_16_bits(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""offset_height plus the panel height is the gate count, which the controller holds in 16 bits."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
|
||||
|
||||
with pytest.raises(cv.Invalid, match="must not exceed"):
|
||||
run_schema_validation(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "uc8279",
|
||||
"dc_pin": 21,
|
||||
"dimensions": {"width": 16, "height": 2, "offset_height": 0xFFFF},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_uc8279_x4pro_explicit_dimensions_keep_the_model_offset(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""Writing out the X4 Pro's dimensions without offset_height keeps the panel's offset of 120."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
set_component_config("spi", {"id": "spi_bus", "clk_pin": 12, "mosi_pin": 11})
|
||||
|
||||
config = run_schema_validation(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "xteink-x4pro-uc8279",
|
||||
"dimensions": {"width": 800, "height": 480},
|
||||
}
|
||||
)
|
||||
assert MODELS["XTEINK-X4PRO-UC8279"].get_constructor_args(config) == (120,)
|
||||
|
||||
|
||||
def test_offset_height_rejected_by_models_without_it(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""A model that does not use offset_height rejects it rather than ignoring it."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
|
||||
|
||||
with pytest.raises(cv.Invalid, match="offset_height"):
|
||||
run_schema_validation(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "ssd1677",
|
||||
"dc_pin": 21,
|
||||
"dimensions": {"width": 200, "height": 200, "offset_height": 8},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_enable_pin_code_generation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
|
||||
#include "../common.h"
|
||||
#include "esphome/components/epaper_spi/epaper_spi_uc8279.h"
|
||||
|
||||
namespace esphome::epaper_spi::testing {
|
||||
|
||||
/// A reset pin that counts how often it is driven low.
|
||||
class CountingPin : public RecordingPin {
|
||||
public:
|
||||
void digital_write(bool value) override {
|
||||
if (!value)
|
||||
this->pulses++;
|
||||
RecordingPin::digital_write(value);
|
||||
}
|
||||
int pulses{0};
|
||||
};
|
||||
|
||||
/// A 16x2 panel bonded to gates 1 and 2 of three, so gate 0 is outside the panel.
|
||||
class TestableUC8279 : public EPaperUC8279 {
|
||||
public:
|
||||
TestableUC8279() : EPaperUC8279("test", 16, 2, nullptr, 0, 1) {}
|
||||
|
||||
void install(spi::SPIDelegate *delegate, uint8_t full_update_every) {
|
||||
this->delegate_ = delegate;
|
||||
this->set_dc_pin(&this->dc);
|
||||
this->set_reset_pin(&this->reset_pin);
|
||||
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
|
||||
this->set_full_update_every(full_update_every);
|
||||
}
|
||||
|
||||
void set_frame(std::initializer_list<uint8_t> bytes) {
|
||||
size_t i = 0;
|
||||
for (const uint8_t byte : bytes)
|
||||
this->buffer_[i++] = byte;
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
/// A whole update with the frame already drawn, run through loop() as the main loop would.
|
||||
void run_update() {
|
||||
this->set_auto_clear(false);
|
||||
this->state_ = EPaperState::UPDATE;
|
||||
const uint32_t deadline = millis() + 2000;
|
||||
while (this->state_ != EPaperState::IDLE) {
|
||||
ASSERT_LT(millis(), deadline) << "update did not finish";
|
||||
this->loop();
|
||||
}
|
||||
}
|
||||
|
||||
bool transfer() { return this->transfer_data(); }
|
||||
|
||||
using EPaperUC8279::on_safe_shutdown;
|
||||
|
||||
RecordingPin dc;
|
||||
CountingPin reset_pin;
|
||||
};
|
||||
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
|
||||
static size_t count(const Bytes &commands, uint8_t command) {
|
||||
return std::count(commands.begin(), commands.end(), command);
|
||||
}
|
||||
|
||||
/// A full refresh writes the new image to DTM2 and white to DTM1, for every gate the controller scans:
|
||||
/// white on the gate outside the panel, then the panel's rows.
|
||||
TEST(EPaperUC8279, FullRefreshWritesTheImageAndAWhitePreviousImage) {
|
||||
TestableUC8279 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.run_update();
|
||||
|
||||
EXPECT_TRUE(display.reset_pin.level) << "reset pin left low";
|
||||
EXPECT_EQ(bus.data[0x13], (Bytes{0xFF, 0xFF, 0x0F, 0xF0, 0x3C, 0xC3}));
|
||||
EXPECT_EQ(bus.data[0x10].size(), 12u) << "DTM1 is not the white previous image plus the image written after";
|
||||
EXPECT_EQ(Bytes(bus.data[0x10].begin(), bus.data[0x10].begin() + 6), (Bytes(6, 0xFF)));
|
||||
EXPECT_EQ(count(bus.commands, 0x91), 0u) << "a full refresh entered partial mode";
|
||||
EXPECT_EQ(bus.data[0x00], (Bytes{0x17, 0x4D})) << "the OTP LUT was not selected before the refresh";
|
||||
}
|
||||
|
||||
/// While partial updates are in use, DTM1 is rewritten with the image after every refresh, so the next
|
||||
/// partial one compares against the image on the panel. The panel then waits in standby, since its own
|
||||
/// deep sleep loses both planes.
|
||||
TEST(EPaperUC8279, PreviousImageIsRewrittenAfterEveryRefresh) {
|
||||
TestableUC8279 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.run_update();
|
||||
|
||||
EXPECT_EQ(Bytes(bus.data[0x10].begin() + 6, bus.data[0x10].end()), (Bytes{0xFF, 0xFF, 0x0F, 0xF0, 0x3C, 0xC3}));
|
||||
EXPECT_EQ(bus.commands.back(), 0x02) << "the panel was not powered off after DTM1 was written";
|
||||
EXPECT_EQ(count(bus.commands, 0x07), 0u) << "the panel was put into deep sleep";
|
||||
}
|
||||
|
||||
/// A partial refresh writes only DTM2, without a reset or the configuration, inside a partial window
|
||||
/// covering the panel's gates.
|
||||
TEST(EPaperUC8279, PartialRefreshWritesOnlyTheNewImage) {
|
||||
TestableUC8279 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.run_update();
|
||||
bus.clear();
|
||||
const int pulses = display.reset_pin.pulses;
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
display.run_update();
|
||||
|
||||
EXPECT_EQ(display.reset_pin.pulses, pulses) << "the partial refresh reset the panel";
|
||||
EXPECT_EQ(bus.data[0x13], (Bytes{0xFF, 0xFF, 0x0F, 0xF0, 0x3C, 0x00}));
|
||||
EXPECT_EQ(Bytes(bus.data[0x10].begin(), bus.data[0x10].end()), (Bytes{0xFF, 0xFF, 0x0F, 0xF0, 0x3C, 0x00}))
|
||||
<< "DTM1 was written before the refresh, or not rewritten after it";
|
||||
EXPECT_EQ(bus.data[0x90], (Bytes{0x00, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x00, 0x02, 0x01}))
|
||||
<< "the partial window does not cover the panel's gates";
|
||||
const auto refresh = std::find(bus.commands.begin(), bus.commands.end(), 0x12);
|
||||
const auto dtm1 = std::find(bus.commands.begin(), bus.commands.end(), 0x10);
|
||||
EXPECT_LT(refresh, dtm1) << "DTM1 was written before the refresh";
|
||||
}
|
||||
|
||||
/// Without partial updates the panel goes into its deep sleep after every update, and the next update
|
||||
/// resets it and writes both planes again.
|
||||
TEST(EPaperUC8279, EveryUpdateFullSleepsAndResetsEachTime) {
|
||||
TestableUC8279 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 1);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.run_update();
|
||||
EXPECT_EQ(bus.data[0x07], (Bytes{0xA5}));
|
||||
EXPECT_EQ(bus.data[0x10], (Bytes(6, 0xFF))) << "DTM1 was rewritten without partial updates";
|
||||
bus.clear();
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
display.run_update();
|
||||
EXPECT_EQ(bus.data[0x13], (Bytes{0xFF, 0xFF, 0x0F, 0xF0, 0x3C, 0x00}));
|
||||
EXPECT_EQ(bus.data[0x10], (Bytes(6, 0xFF)));
|
||||
EXPECT_EQ(count(bus.commands, 0x91), 0u);
|
||||
}
|
||||
|
||||
/// The rewrite of DTM1 is spread over loop iterations like the image transfer, and the state machine
|
||||
/// waits for it: the panel is powered off and the update finishes only once all of it is written.
|
||||
TEST(EPaperUC8279, PreviousImageRewriteIsTimeSliced) {
|
||||
TestableUC8279 display;
|
||||
RecordingDelegate bus(&display.dc, MAX_TRANSFER_TIME + 1); // every row overruns the time slice
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.run_update();
|
||||
|
||||
EXPECT_EQ(Bytes(bus.data[0x10].begin() + 6, bus.data[0x10].end()), (Bytes{0xFF, 0xFF, 0x0F, 0xF0, 0x3C, 0xC3}));
|
||||
EXPECT_EQ(bus.commands.back(), 0x02);
|
||||
}
|
||||
|
||||
/// In a full update the white DTM1 plane starts on the pass after DTM2 ends, so one pass never writes both planes.
|
||||
TEST(EPaperUC8279, FullTransferWritesOnePlanePerPass) {
|
||||
TestableUC8279 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 1);
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
|
||||
EXPECT_FALSE(display.transfer());
|
||||
EXPECT_EQ(count(bus.commands, 0x13), 1u);
|
||||
EXPECT_EQ(count(bus.commands, 0x10), 0u) << "DTM1 started in the same pass as DTM2";
|
||||
|
||||
EXPECT_TRUE(display.transfer());
|
||||
EXPECT_EQ(count(bus.commands, 0x13), 1u);
|
||||
EXPECT_EQ(bus.data[0x10], (Bytes(6, 0xFF)));
|
||||
}
|
||||
|
||||
/// The full refresh that comes round after partial ones resets the controller, configures it again and writes a white
|
||||
/// DTM1 over the image it held, as the first full refresh does.
|
||||
TEST(EPaperUC8279, FullRefreshAfterPartialOnesStartsAfresh) {
|
||||
TestableUC8279 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 2);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.run_update();
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
display.run_update();
|
||||
bus.clear();
|
||||
const int pulses = display.reset_pin.pulses;
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x00, 0x00});
|
||||
display.run_update();
|
||||
EXPECT_EQ(display.reset_pin.pulses, pulses + 1) << "the full refresh did not reset the controller";
|
||||
EXPECT_EQ(count(bus.commands, 0x91), 0u) << "the full refresh entered partial mode";
|
||||
ASSERT_EQ(bus.data[0x10].size(), 12u);
|
||||
EXPECT_EQ(Bytes(bus.data[0x10].begin(), bus.data[0x10].begin() + 6), (Bytes(6, 0xFF)));
|
||||
EXPECT_EQ(Bytes(bus.data[0x10].begin() + 6, bus.data[0x10].end()), (Bytes{0xFF, 0xFF, 0x0F, 0xF0, 0x00, 0x00}));
|
||||
}
|
||||
|
||||
/// A requested full update, between partial ones, is a full refresh from scratch too.
|
||||
TEST(EPaperUC8279, RequestedFullUpdateStartsAfresh) {
|
||||
TestableUC8279 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.run_update();
|
||||
bus.clear();
|
||||
const int pulses = display.reset_pin.pulses;
|
||||
|
||||
display.request_full_update();
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0x00});
|
||||
display.run_update();
|
||||
EXPECT_EQ(display.reset_pin.pulses, pulses + 1);
|
||||
EXPECT_EQ(count(bus.commands, 0x91), 0u);
|
||||
EXPECT_EQ(Bytes(bus.data[0x10].begin(), bus.data[0x10].begin() + 6), (Bytes(6, 0xFF)));
|
||||
}
|
||||
|
||||
/// Before a restart or the controller's deep sleep the panel goes into its own deep sleep, even while partial updates
|
||||
/// would otherwise leave it in standby.
|
||||
TEST(EPaperUC8279, ShutdownPutsThePanelToSleep) {
|
||||
TestableUC8279 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus, 5);
|
||||
|
||||
display.set_frame({0x0F, 0xF0, 0x3C, 0xC3});
|
||||
display.run_update();
|
||||
bus.clear();
|
||||
display.on_safe_shutdown();
|
||||
EXPECT_EQ(bus.data[0x07], (Bytes{0xA5}));
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi::testing
|
||||
@@ -435,3 +435,27 @@ display:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
|
||||
# Xteink X4 Pro (800x480, UC8279)
|
||||
# Pins overridden to avoid conflicts with the other display configs
|
||||
# full_update_every > 1 exercises the partial refresh path
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
model: xteink-x4pro-uc8279
|
||||
full_update_every: 10
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO17
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
lambda: |-
|
||||
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 100, Color::BLACK);
|
||||
|
||||
Reference in New Issue
Block a user