|
|
@@ -28,9 +28,12 @@ using EpdSPIDevice = spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARIT
|
|
|
// need a watchdog subscription.
|
|
|
//
|
|
|
// request_power_off()/request_hibernate() are a separate, standalone entry
|
|
|
-// point (no reset pulse, no init/window/plane-write) replaying just
|
|
|
+// point (no init/window/plane-write) replaying just
|
|
|
// power_off_sequence/hibernate_sequence - for a user's own lambda, not run
|
|
|
-// automatically by request_full_update(). See epd_command.h.
|
|
|
+// automatically by request_full_update(). Called while a refresh is still
|
|
|
+// in flight (e.g. from on_powerdown()'s teardown path), they force a reset
|
|
|
+// pulse first to abort it rather than writing over a busy controller - see
|
|
|
+// begin_power_down_(). See epd_command.h.
|
|
|
class CycleEngine {
|
|
|
public:
|
|
|
void begin(const EpdPanelDescriptor *desc, GPIOPin *cs, GPIOPin *dc, GPIOPin *rst, GPIOPin *busy,
|
|
|
@@ -72,30 +75,21 @@ class CycleEngine {
|
|
|
}
|
|
|
#endif
|
|
|
|
|
|
- // Arms a standalone powerOff() replay - no reset pulse, no init/window/
|
|
|
- // plane write, just the captured power_off_sequence (if any) then DONE.
|
|
|
- // Undefined to call while !idle(). Safe to call repeatedly: GxEPD2's own
|
|
|
- // powerOff() is itself guarded on "already off," which the capture already
|
|
|
- // baked in (see make_power_sequence_resolvers in emit_descriptor.h).
|
|
|
- void request_power_off() {
|
|
|
- this->cursor_ = 0;
|
|
|
- this->cmd_phase_ = CmdPhase::BEFORE_DELAY;
|
|
|
- this->hibernating_ = false;
|
|
|
- this->set_state_(State::POWER_OFF_CMD);
|
|
|
- }
|
|
|
+ // Arms a standalone powerOff() replay - no init/window/plane write, just
|
|
|
+ // the captured power_off_sequence (if any) then DONE. Safe to call any
|
|
|
+ // time, including mid-refresh: see begin_power_down_() below for why that
|
|
|
+ // matters. Safe to call repeatedly: GxEPD2's own powerOff() is itself
|
|
|
+ // guarded on "already off," which the capture already baked in (see
|
|
|
+ // make_power_sequence_resolvers in emit_descriptor.h).
|
|
|
+ void request_power_off() { this->begin_power_down_(false); }
|
|
|
|
|
|
// Arms a standalone hibernate() replay: power_off_sequence then
|
|
|
// hibernate_sequence (the deep-sleep trigger only - see epd_command.h).
|
|
|
- // Undefined to call while !idle(). No paired "wake" call exists or is
|
|
|
- // needed: request_full_update()'s RESET_LOW is an unconditional hardware
|
|
|
- // reset pulse every cycle, which is exactly how GxEPD2 wakes a hibernating
|
|
|
- // (RST-wakeable) panel.
|
|
|
- void request_hibernate() {
|
|
|
- this->cursor_ = 0;
|
|
|
- this->cmd_phase_ = CmdPhase::BEFORE_DELAY;
|
|
|
- this->hibernating_ = true;
|
|
|
- this->set_state_(State::POWER_OFF_CMD);
|
|
|
- }
|
|
|
+ // See begin_power_down_() below for the mid-refresh case. No paired "wake"
|
|
|
+ // call exists or is needed: request_full_update()'s RESET_LOW is an
|
|
|
+ // unconditional hardware reset pulse every cycle, which is exactly how
|
|
|
+ // GxEPD2 wakes a hibernating (RST-wakeable) panel.
|
|
|
+ void request_hibernate() { this->begin_power_down_(true); }
|
|
|
|
|
|
bool idle() const { return this->state_ == State::IDLE; }
|
|
|
|
|
|
@@ -136,7 +130,13 @@ class CycleEngine {
|
|
|
// silently dropped, so the panel never refreshes. Panels that don't
|
|
|
// assert BUSY post-reset read "not busy" on the first poll and fall
|
|
|
// straight through to INIT_CMD, so this is safe for all of them.
|
|
|
- this->start_busy_wait_(State::INIT_CMD);
|
|
|
+ // begin_power_down_()'s abort path sets reset_then_wait_busy_ to
|
|
|
+ // false instead - see there for why.
|
|
|
+ if (this->reset_then_wait_busy_) {
|
|
|
+ this->start_busy_wait_(this->reset_done_next_);
|
|
|
+ } else {
|
|
|
+ this->set_state_(this->reset_done_next_);
|
|
|
+ }
|
|
|
return;
|
|
|
|
|
|
case State::INIT_CMD:
|
|
|
@@ -252,9 +252,43 @@ class CycleEngine {
|
|
|
this->cursor_ = 0;
|
|
|
this->plane_ = 0;
|
|
|
this->cmd_phase_ = CmdPhase::BEFORE_DELAY;
|
|
|
+ // Normal reset path: post-reset busy wait (see RESET_SETTLE below), then
|
|
|
+ // the real init sequence - this->reset_done_next_/reset_then_wait_busy_
|
|
|
+ // are only ever overridden by begin_power_down_()'s abort path.
|
|
|
+ this->reset_done_next_ = State::INIT_CMD;
|
|
|
+ this->reset_then_wait_busy_ = true;
|
|
|
this->set_state_(start_state);
|
|
|
}
|
|
|
|
|
|
+ // Shared by request_power_off()/request_hibernate(). Most controllers
|
|
|
+ // ignore new commands outright while BUSY is asserted, so writing
|
|
|
+ // power_off_sequence/hibernate_sequence straight over a cycle that's still
|
|
|
+ // mid-refresh would silently do nothing - the panel never actually powers
|
|
|
+ // down, it just keeps running the update it was already doing. If a reset
|
|
|
+ // pin is wired, force a hardware reset pulse first (the same one
|
|
|
+ // request_full_update() issues every normal cycle) to abort whatever's in
|
|
|
+ // flight and bring the controller back to a known, command-accepting
|
|
|
+ // state before sending the power-down sequence - skipping the post-reset
|
|
|
+ // busy wait that a real update takes, since this is an emergency path
|
|
|
+ // (e.g. on_powerdown()'s tight teardown budget) that can't afford to
|
|
|
+ // stall on busy_timeout_ms. Without a reset pin there's no way to
|
|
|
+ // interrupt a busy controller, so this is best-effort in that case:
|
|
|
+ // jamming the state machine straight to POWER_OFF_CMD, same as calling
|
|
|
+ // this while idle.
|
|
|
+ void begin_power_down_(bool hibernating) {
|
|
|
+ this->cursor_ = 0;
|
|
|
+ this->plane_ = 0;
|
|
|
+ this->cmd_phase_ = CmdPhase::BEFORE_DELAY;
|
|
|
+ this->hibernating_ = hibernating;
|
|
|
+ if (this->state_ != State::IDLE && this->rst_ != nullptr) {
|
|
|
+ this->reset_done_next_ = State::POWER_OFF_CMD;
|
|
|
+ this->reset_then_wait_busy_ = false;
|
|
|
+ this->set_state_(State::RESET_LOW);
|
|
|
+ } else {
|
|
|
+ this->set_state_(State::POWER_OFF_CMD);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// Every command byte and every data byte is its own SPI transaction, CS
|
|
|
// toggled around each one - this looks wasteful, but it's mechanically
|
|
|
// what GxEPD2_EPD::_writeCommand()/_writeData(uint8_t) do (see
|
|
|
@@ -507,6 +541,8 @@ class CycleEngine {
|
|
|
uint32_t wire_bytes_{0}; // bytes actually sent per plane (>= plane_bytes_)
|
|
|
uint32_t bytes_per_row_{0};
|
|
|
uint32_t deadline_{0};
|
|
|
+ State reset_done_next_{State::INIT_CMD}; // RESET_SETTLE's continuation - see arm_full_update_()/begin_power_down_()
|
|
|
+ bool reset_then_wait_busy_{true}; // false only for begin_power_down_()'s abort path
|
|
|
State delay_return_state_{State::DONE};
|
|
|
uint32_t busy_wait_deadline_{0};
|
|
|
State busy_wait_next_{State::DONE};
|