<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mariusz Jurgielewicz</title>
    <description>The latest articles on DEV Community by Mariusz Jurgielewicz (@melastmohican).</description>
    <link>https://dev.to/melastmohican</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1737412%2Fcff5c243-5c73-473a-8228-d993374c420a.jpeg</url>
      <title>DEV Community: Mariusz Jurgielewicz</title>
      <link>https://dev.to/melastmohican</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/melastmohican"/>
    <language>en</language>
    <item>
      <title>Does Rust Support Inheritance? Yes, No, and Maybe, All in the Same File</title>
      <dc:creator>Mariusz Jurgielewicz</dc:creator>
      <pubDate>Sun, 13 Sep 2026 21:17:25 +0000</pubDate>
      <link>https://dev.to/melastmohican/does-rust-support-inheritance-yes-no-and-maybe-all-in-the-same-file-3fi1</link>
      <guid>https://dev.to/melastmohican/does-rust-support-inheritance-yes-no-and-maybe-all-in-the-same-file-3fi1</guid>
      <description>&lt;h2&gt;
  
  
  Does Rust Support Inheritance? Yes, No, and Maybe, All in the Same File
&lt;/h2&gt;

&lt;p&gt;C started with plain structs: bags of fields without attached behavior. C++ added methods and class inheritance on top, letting a &lt;code&gt;Derived&lt;/code&gt; class reuse &lt;code&gt;Base&lt;/code&gt; fields and virtual methods while overriding only what differed. For many developers, that transition from struct to class hierarchy defines what object orientation looks like.&lt;/p&gt;

&lt;p&gt;Rust has structs and traits. Traits look enough like interfaces that developers frequently ask whether Rust supports inheritance the way C++ or Java does. While maintaining &lt;code&gt;epdsi&lt;/code&gt;, a &lt;code&gt;no_std&lt;/code&gt; driver crate for e-paper displays, I ran into concrete examples of yes, no, and maybe all within the same codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  What inheritance actually buys you
&lt;/h3&gt;

&lt;p&gt;In Java, if &lt;code&gt;Ssd1680Controller&lt;/code&gt; and &lt;code&gt;Ssd1681Controller&lt;/code&gt; are two chips in the same family that share almost all of their init sequence and differ only in how they trigger a refresh, you would write them as a hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ssd168xController&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;EpdController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Ssd168xVariant&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;initSequence&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Bus&lt;/span&gt; &lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Delay&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// shared register writes, common to both chips&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;triggerRefresh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Bus&lt;/span&gt; &lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Delay&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// shared power envelope, branches on variant only where the chips disagree&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ssd1680Controller&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Ssd168xController&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ssd1681Controller&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Ssd168xController&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subclasses inherit every method directly. With &lt;code&gt;extends&lt;/code&gt;, fields live once in the base class, and calls on an &lt;code&gt;Ssd1680Controller&lt;/code&gt; instance dispatch through the vtable to the shared implementation unless overridden. You only specify the differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Rust code actually looks like
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;epdsi&lt;/code&gt; has two chip families structured similarly to that Java example: &lt;code&gt;Ssd168xController&lt;/code&gt; covers the SSD1680 and SSD1681, while &lt;code&gt;Jd7966xController&lt;/code&gt; covers the JD79660AA and JD79661AA. Both involve one chip family with two silicon variants sharing register logic. Here is what &lt;code&gt;src/controllers/ssd168x.rs&lt;/code&gt; does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Ssd168xController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Ssd168xVariant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Ssd1680Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Ssd168xController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Ssd1681Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Ssd168xController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;extends&lt;/code&gt;, &lt;code&gt;Ssd1680Controller&lt;/code&gt; wraps &lt;code&gt;Ssd168xController&lt;/code&gt; as an internal field. Each method required by the &lt;code&gt;EpdController&lt;/code&gt; trait must be implemented manually on &lt;code&gt;Ssd1680Controller&lt;/code&gt;, forwarding directly to &lt;code&gt;inner&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;init_sequence&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DELAY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DelayNs&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;SpiBusWrapper&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUSY&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;DELAY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.inner&lt;/span&gt;&lt;span class="nf"&gt;.init_sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EpdController&lt;/code&gt; defines six methods, so this forwarding boilerplate is written for &lt;code&gt;Ssd1680Controller&lt;/code&gt; and repeated identically for &lt;code&gt;Ssd1681Controller&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The variant-specific differences live inside &lt;code&gt;Ssd168xController&lt;/code&gt; itself, handled through a &lt;code&gt;match self.variant&lt;/code&gt;. The wrapper types exist primarily so callers get explicit constructors (&lt;code&gt;Ssd1680Controller::new(...)&lt;/code&gt; and &lt;code&gt;Ssd1681Controller::new(...)&lt;/code&gt;) rather than having to pass a variant argument that could be misconfigured.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/controllers/jd7966x.rs&lt;/code&gt; follows the same structure for a different chip family, noting in its doc comments that it mirrors the relationship modeled in &lt;code&gt;Ssd168xController&lt;/code&gt;. Because the language provides no built-in syntax to abstract this delegation away, the pattern is duplicated by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Rust has instead
&lt;/h3&gt;

&lt;p&gt;Rust provides several mechanisms for code reuse, but none allow one struct to inherit fields and methods from another:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traits with default methods&lt;/strong&gt; resemble default methods in Java interfaces. A trait can provide a default implementation that implementors can keep or override. In &lt;code&gt;epdsi&lt;/code&gt;, all &lt;code&gt;EpdController&lt;/code&gt; methods are mandatory, but default methods are common across the wider ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supertraits&lt;/strong&gt; allow one trait to require another (&lt;code&gt;trait Wait: InputPin&lt;/code&gt;), functioning like interface inheritance to compose contracts rather than state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composition&lt;/strong&gt; means embedding one struct inside another, as &lt;code&gt;Ssd1680Controller&lt;/code&gt; does with &lt;code&gt;Ssd168xController&lt;/code&gt;. While Java developers have long advocated preferring composition over inheritance, Rust leaves no other choice when sharing state.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;epdsi&lt;/code&gt; avoids using &lt;code&gt;Deref&lt;/code&gt; polymorphism here. Implementing &lt;code&gt;Deref&lt;/code&gt; on a wrapper type allows method calls to dereference to an inner struct, avoiding manual forwarding for inherent methods. However, &lt;code&gt;Deref&lt;/code&gt; does not satisfy trait bounds, so the six &lt;code&gt;EpdController&lt;/code&gt; trait methods still require explicit implementations. Explicit delegation keeps trait requirements clear rather than obscuring them behind deref coercion.&lt;/p&gt;

&lt;p&gt;The fundamental constraint is where state lives. An abstract class in Java holds fields and passes them down to subclasses. Rust traits cannot hold state. When multiple types share fields (such as &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, and &lt;code&gt;variant&lt;/code&gt;), that data must live in a concrete struct, which leads directly to composition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Yes, no, maybe
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;No&lt;/strong&gt;, if inheritance means class inheritance via &lt;code&gt;extends&lt;/code&gt;: automatically inheriting fields and methods with selective overrides. Rust structs cannot extend other structs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yes&lt;/strong&gt;, if referring to interface inheritance: traits can require supertraits, provide default method implementations, and allow implementors to selectively override them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maybe&lt;/strong&gt;, if referring to sharing implementation across multiple distinct types. As seen with &lt;code&gt;Ssd1680Controller&lt;/code&gt; and &lt;code&gt;Ssd1681Controller&lt;/code&gt;, Rust requires explicit forwarding delegation. The compiler does not generate forwarding shims automatically, so consistency between wrappers relies on careful manual wiring.&lt;/p&gt;

&lt;p&gt;This design deliberately separates concerns that C++ unified. C++ combined data layout and behavior inheritance into a single mechanism, which works well until it encounters edge cases like the diamond problem. Rust separates them completely: structs manage data, traits define contracts, and they never collapse into a class hierarchy. While &lt;code&gt;ssd168x.rs&lt;/code&gt; and &lt;code&gt;jd7966x.rs&lt;/code&gt; might superficially resemble base and derived classes, they are structs wrapping another struct with manual delegation. That verbosity is the explicit trade-off Rust makes to avoid the pitfalls of traditional class hierarchies.&lt;/p&gt;




&lt;p&gt;&lt;code&gt;epdsi&lt;/code&gt; is available on &lt;a href="https://crates.io/crates/epdsi" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt; and &lt;a href="https://github.com/melastmohican/epdsi" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, a &lt;code&gt;no_std&lt;/code&gt;, &lt;code&gt;embedded-hal&lt;/code&gt; 1.0 driver framework for e-paper displays covering SSD1680/1681/1677, UC8253, JD79660/79661, ED2208, and Pervasive Displays COGs.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Your E-Paper Panel Isn't Broken: How Retained State Makes Drivers Look Buggy</title>
      <dc:creator>Mariusz Jurgielewicz</dc:creator>
      <pubDate>Mon, 24 Aug 2026 04:05:51 +0000</pubDate>
      <link>https://dev.to/melastmohican/your-e-paper-panel-isnt-broken-how-retained-state-makes-drivers-look-buggy-4ifi</link>
      <guid>https://dev.to/melastmohican/your-e-paper-panel-isnt-broken-how-retained-state-makes-drivers-look-buggy-4ifi</guid>
      <description>&lt;p&gt;I spent an afternoon fixing three bugs in an e-paper driver. None of them existed.&lt;/p&gt;

&lt;p&gt;The driver was &lt;a href="https://crates.io/crates/epdsi" rel="noopener noreferrer"&gt;epdsi&lt;/a&gt;, a &lt;code&gt;no_std&lt;/code&gt; Rust framework I maintain for electronic paper displays. I was porting a working example from an RP2350 to an ESP32-C3 — same driver crate, same panel, different board. It should have been a twenty-minute job. It took most of a day, and the code I ended up shipping was, in the essentials, the code I started with.&lt;/p&gt;

&lt;p&gt;What follows is the failure mode that did it, because I don't think it's written down anywhere, and because anyone working with e-paper will eventually hit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Thing Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;E-paper retains its image without power. That's the selling point — the whole reason to use the technology. Every datasheet leads with it.&lt;/p&gt;

&lt;p&gt;What the datasheets don't emphasise is that &lt;strong&gt;the controller retains state too&lt;/strong&gt;, and that state is not always benign.&lt;/p&gt;

&lt;p&gt;Interrupt a refresh mid-flight — Ctrl-C the monitor, reflash the board, unplug it while the charge pump is running — and the controller can be left latched busy. It sits with BUSY asserted, waiting for an operation that will never complete.&lt;/p&gt;

&lt;p&gt;Now run your program again. The driver issues a refresh and waits for BUSY. BUSY is already asserted and never clears, so the wait runs to its timeout. In &lt;code&gt;epdsi&lt;/code&gt; that's 60 seconds. On an SSD1680, a single refresh is three stages — power-on, update, power-off — each with its own wait. That's &lt;strong&gt;three minutes for one refresh&lt;/strong&gt;, and my example did six of them.&lt;/p&gt;

&lt;p&gt;Worse, it is self-perpetuating. A timed-out refresh leaves the panel in the same latched state, so the next run fails identically. You are now debugging a system that fails the same way every time, which feels exactly like a deterministic bug.&lt;/p&gt;

&lt;p&gt;And here is the part that cost me the most: &lt;strong&gt;a hardware reset does not clear it.&lt;/strong&gt; The driver's &lt;code&gt;hard_reset&lt;/code&gt; toggles the RST pin, the panel acknowledges it, BUSY pulses correctly — and the underlying state persists. Only removing power actually resets it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Faces of the Same Problem
&lt;/h2&gt;

&lt;p&gt;Retained state doesn't announce itself. It wears whatever costume fits your current hypothesis. Over one afternoon it produced all of these, and I diagnosed each as a different bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shifted, clipped content.&lt;/strong&gt; A write cut off partway leaves the frame buffer half-transferred. The panel displays the result faithfully.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnblnjjl9n1nurz6zspv3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnblnjjl9n1nurz6zspv3.jpg" alt="2.13 inch panel showing content shifted right and clipped at the edge" width="526" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is a 2.13" monochrome panel. The header at the top is correct; everything below it is displaced about 40 px to the right and running off the edge. It looks precisely like a RAM window or stride miscalculation — and I had recently touched the RAM addressing code, so I believed it immediately.&lt;/p&gt;

&lt;p&gt;Here is the same failure on a 4.26" panel, where an interrupted write produced a frame drawn twice side by side with a band of noise between:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gompu09wxteluyixkty.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gompu09wxteluyixkty.jpg" alt="4.26 inch panel showing the frame duplicated side by side with a noise band" width="561" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A refresh that returns in 10 ms.&lt;/strong&gt; The panel isn't accepting commands, so BUSY never asserts, so the wait sees an idle line and returns at once. I diagnosed this as a race — the driver polling BUSY before the panel had time to raise it. I wrote three successive fixes for that race. The race does not exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A refresh that appears to hang.&lt;/strong&gt; It isn't hanging, it's timing out. But three minutes is indistinguishable from forever when you are watching a terminal, so you interrupt it — which recreates the exact condition that caused it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing at all.&lt;/strong&gt; Commands ignored, display unchanged, program running to completion and reporting success.&lt;/p&gt;

&lt;p&gt;Four symptoms, four plausible driver bugs, one actual cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Made It Worse
&lt;/h2&gt;

&lt;p&gt;Three mistakes, all of which I would have called obvious in someone else's writeup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I kept reasoning from polluted data.&lt;/strong&gt; I knew runs were being interrupted. I still drew conclusions from what happened afterwards. At one point I correctly suspected panel state, told myself to power-cycle, got a result that didn't match my theory — and abandoned the theory instead of concluding the theory was wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I built a diagnostic that lied.&lt;/strong&gt; To get finer timing I hand-rolled the controller's trigger sequence instead of calling the driver's own refresh. It reported nine measurements, all exactly 100 ms, and I read them as data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;---&lt;/span&gt; A: full frame, Full mode &lt;span class="o"&gt;(&lt;/span&gt;0xF7&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;---&lt;/span&gt;
    power-on  &lt;span class="o"&gt;(&lt;/span&gt;0xE0&lt;span class="o"&gt;)&lt;/span&gt;: BUSY released after 100 ms
    update    &lt;span class="o"&gt;(&lt;/span&gt;0xF7&lt;span class="o"&gt;)&lt;/span&gt;: BUSY released after 100 ms
    power-off &lt;span class="o"&gt;(&lt;/span&gt;0x83&lt;span class="o"&gt;)&lt;/span&gt;: BUSY released after 100 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They were noise on an idle line. The code never drove the panel at all. What exposed it was not the log — the log looked entirely plausible — but noticing that the display never changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I anchored on software because software is where I was working.&lt;/strong&gt; Known-good third-party code was sitting right there the whole time. I didn't reach for it until much later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Worked
&lt;/h2&gt;

&lt;p&gt;Three techniques, each of which collapsed hours of speculation into a single experiment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run stock third-party code across several boards.&lt;/strong&gt; I flashed the standard Arduino GxEPD2 demo onto four different microcontrollers with the same adapter and the same panel. Three worked perfectly; one produced noise. That one experiment proved the fault was board-level and not in my driver — a conclusion I had failed to reach through hours of code analysis. I later repeated the trick with a different panel and got an equally clean answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate the instrument against a known measurement.&lt;/strong&gt; Once I had a trustworthy baseline — a full refresh on this panel takes 3891 ms — any diagnostic reporting 100 ms was self-evidently broken. That baseline turns "interesting result" into "my tool is lying" instantly. Establish one before you need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch the display, not the log.&lt;/strong&gt; Firmware happily reports success it did not achieve. The panel cannot. Every diagnostic I wrote after this point drew something unmistakable — solid black, then solid white — so the hardware itself answered the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Protocol
&lt;/h2&gt;

&lt;p&gt;This is now in my repository as required reading:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Power-cycle, run once, don't interrupt, then judge. Connect and disconnect FPCs with the board unpowered.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unglamorous, and it would have saved the entire afternoon. Some corollaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never reason from an interrupted run,&lt;/strong&gt; or from any run following one, until you have removed power. Not reset — power.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know your reference timings.&lt;/strong&gt; Deviation is only recognisable against a baseline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Panel&lt;/th&gt;
&lt;th&gt;Full refresh&lt;/th&gt;
&lt;th&gt;Partial refresh&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4.26" monochrome&lt;/td&gt;
&lt;td&gt;~3.9 s&lt;/td&gt;
&lt;td&gt;~1.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.13" monochrome&lt;/td&gt;
&lt;td&gt;~3.9 s&lt;/td&gt;
&lt;td&gt;~1.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.54" tri-colour&lt;/td&gt;
&lt;td&gt;~14 s&lt;/td&gt;
&lt;td&gt;~14 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Moving a 4,000-byte frame over SPI at 4 MHz takes 8 ms. Data transfer is essentially never your problem, which eliminates a whole family of tempting hypotheses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Colour panels are slow, and that's physics.&lt;/strong&gt; Tri-colour and quad-colour panels have no fast waveform — the coloured pigment is a heavier particle that needs the full waveform to migrate. Every update takes seconds. My 1.54" tri-colour example runs six refreshes at roughly 14 seconds each: ninety seconds of a screen that appears frozen, all of it correct. This is the single most likely thing to make you interrupt a run that was working perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part That Generalises
&lt;/h2&gt;

&lt;p&gt;The reason this took an afternoon rather than twenty minutes is not that e-paper is unusually difficult. It is that I was debugging a system where &lt;strong&gt;both my subject and my instrument could lie to me&lt;/strong&gt;, and I only ever checked one of them.&lt;/p&gt;

&lt;p&gt;The panel lied by retaining state across runs, so every experiment was contaminated by the last. My diagnostics lied by reporting plausible numbers for operations that never happened. Software alone cannot distinguish either case — which is why the fix was physical: pull the power, and watch the screen.&lt;/p&gt;

&lt;p&gt;If you are debugging anything with retained state — displays, EEPROMs, radios with persistent configuration, anything with a charge pump — assume your last experiment is still influencing this one until you have proven otherwise. And when a measurement surprises you, check the ruler before you rewrite the code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fky8jhq2sumjjssdx14cb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fky8jhq2sumjjssdx14cb.jpg" alt="The same XIAO ESP32-C3 and ePaper Driver Board from the failure photographs, now rendering a 2.13 inch quad-colour panel correctly in black, white, red and yellow" width="697" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the same board and the same adapter as the two failure photographs above — a&lt;br&gt;
XIAO ESP32-C3 on a Seeed ePaper Driver Board — rendering all four inks exactly as&lt;br&gt;
intended.&lt;/p&gt;

&lt;p&gt;The driver, for the record, needed no changes at all.&lt;/p&gt;




&lt;p&gt;&lt;code&gt;epdsi&lt;/code&gt; is on &lt;a href="https://crates.io/crates/epdsi" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt; and &lt;a href="https://github.com/melastmohican/epdsi" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — a &lt;code&gt;no_std&lt;/code&gt;, &lt;code&gt;embedded-hal&lt;/code&gt; 1.0 driver framework for e-paper displays, covering SSD1680/1681/1677, UC8253, JD79661, ED2208 and the Pervasive Displays COGs, verified on hardware across Cortex-M, RISC-V and Xtensa.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>embedded</category>
      <category>debugging</category>
      <category>hardware</category>
    </item>
  </channel>
</rss>
