<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Firmware Update on Hoa Nguyen — Embedded Software Architect</title><link>https://hoanguyen.de/tags/firmware-update/</link><description>Recent content in Firmware Update on Hoa Nguyen — Embedded Software Architect</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 10 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://hoanguyen.de/tags/firmware-update/index.xml" rel="self" type="application/rss+xml"/><item><title>From Board Bring-Up to Production: 5 Lessons from an i.MX RT Control Unit</title><link>https://hoanguyen.de/posts/bringup-to-production-imx-rt-lessons/</link><pubDate>Tue, 10 Nov 2026 00:00:00 +0000</pubDate><guid>https://hoanguyen.de/posts/bringup-to-production-imx-rt-lessons/</guid><description>Five lessons from taking an i.MX RT control unit from first boot toward production: the bootloader that hides your SDRAM setup, the flash size nobody measured, writing the flash you execute from, firmware updates as a protocol, and surviving the power cut.</description><content:encoded><![CDATA[<p>Getting a board to blink is the easy 10 %. The hard part is everything between &ldquo;it boots on
my desk&rdquo; and &ldquo;it can leave the building&rdquo;: a bootloader in front of the application, a flash
layout that still fits next year, field updates that never leave a device half-updated, and
a power cut at the worst possible moment.</p>
<p>None of the five problems below is exotic. Each one costs days when it goes unnoticed,
because a hidden assumption looks true on the bench until the moment it isn&rsquo;t.</p>
<p>In this article I walk through five lessons from taking an i.MX RT (Cortex-M7) control unit
along that path, in the order a product meets them: the mechanism behind each one, and the
rule that keeps it from coming back.</p>
<figure class="diagram">
  <div class="diagram-svg" role="img" aria-label="Five lessons from bring-up to production, each with the hidden assumption and the rule that replaces it: SDRAM is set up by the boot ROM&#39;s DCD, so the bootloader must take over; the flash size is a measurement, so one header owns the map and CI checks it; writing XIP flash makes the flash vanish, so the write path runs from RAM; an update is a protocol, so only the new version number counts as success; hold-up time is a measurement, so the power-fail path never starts a flash operation"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 610">
<defs><marker id="m_cur" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker></defs>
<text x="66" y="26" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor" fill-opacity="0.7">Phase</text>
<text x="232" y="26" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="#f59e0b">The hidden assumption</text>
<text x="492" y="26" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="#10b981">The rule instead</text>
<rect x="10" y="42" width="740" height="104" rx="10" fill="#94a3b8" fill-opacity="0.06"/>
<circle cx="36" cy="94.0" r="15" fill="#3b82f6"/>
<text x="36" y="99.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#fefefe">1</text>
<text x="62" y="90.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="15" font-weight="bold" text-anchor="start" fill="currentColor">Bring-up</text>
<text x="62" y="110.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.7">behind a bootloader</text>
<rect x="222" y="54" width="236" height="80" rx="8" fill="#f59e0b" fill-opacity="0.12" stroke="#f59e0b" stroke-width="1.5"/>
<text x="234" y="89.5" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">The application sets up</text>
<text x="234" y="108.5" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">its own SDRAM</text>
<line x1="462" y1="94.0" x2="478" y2="94.0" stroke="currentColor" stroke-width="1.6" stroke-opacity="0.6" marker-end="url(#m_cur)"/>
<rect x="482" y="54" width="262" height="80" rx="8" fill="#10b981" fill-opacity="0.12" stroke="#10b981" stroke-width="1.5"/>
<text x="494" y="80.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">The boot ROM did it via the DCD.</text>
<text x="494" y="99.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">The bootloader must take over</text>
<text x="494" y="118.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">SDRAM and clocks</text>
<rect x="10" y="154" width="740" height="104" rx="10" fill="#94a3b8" fill-opacity="0.06"/>
<circle cx="36" cy="206.0" r="15" fill="#8b5cf6"/>
<text x="36" y="211.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#fefefe">2</text>
<text x="62" y="202.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="15" font-weight="bold" text-anchor="start" fill="currentColor">Flash layout</text>
<text x="62" y="222.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.7">partition map</text>
<rect x="222" y="166" width="236" height="80" rx="8" fill="#f59e0b" fill-opacity="0.12" stroke="#f59e0b" stroke-width="1.5"/>
<text x="234" y="192.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">The size in the linker</text>
<text x="234" y="211.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">script and the ticket</text>
<text x="234" y="230.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">is right</text>
<line x1="462" y1="206.0" x2="478" y2="206.0" stroke="currentColor" stroke-width="1.6" stroke-opacity="0.6" marker-end="url(#m_cur)"/>
<rect x="482" y="166" width="262" height="80" rx="8" fill="#10b981" fill-opacity="0.12" stroke="#10b981" stroke-width="1.5"/>
<text x="494" y="192.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">Probe the part. One header owns</text>
<text x="494" y="211.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">the map; an assert and a CI</text>
<text x="494" y="230.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">test keep it true</text>
<rect x="10" y="266" width="740" height="104" rx="10" fill="#94a3b8" fill-opacity="0.06"/>
<circle cx="36" cy="318.0" r="15" fill="#14b8a6"/>
<text x="36" y="323.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#fefefe">3</text>
<text x="62" y="314.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="15" font-weight="bold" text-anchor="start" fill="currentColor">Field update</text>
<text x="62" y="334.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.7">writing the XIP flash</text>
<rect x="222" y="278" width="236" height="80" rx="8" fill="#f59e0b" fill-opacity="0.12" stroke="#f59e0b" stroke-width="1.5"/>
<text x="234" y="313.5" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">Writing the flash we run</text>
<text x="234" y="332.5" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">from is just slow</text>
<line x1="462" y1="318.0" x2="478" y2="318.0" stroke="currentColor" stroke-width="1.6" stroke-opacity="0.6" marker-end="url(#m_cur)"/>
<rect x="482" y="278" width="262" height="80" rx="8" fill="#10b981" fill-opacity="0.12" stroke="#10b981" stroke-width="1.5"/>
<text x="494" y="304.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">While busy, the flash is gone:</text>
<text x="494" y="323.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">write path in RAM, IRQs masked,</text>
<text x="494" y="342.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">I-cache invalidated after</text>
<rect x="10" y="378" width="740" height="104" rx="10" fill="#94a3b8" fill-opacity="0.06"/>
<circle cx="36" cy="430.0" r="15" fill="#f59e0b"/>
<text x="36" y="435.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#fefefe">4</text>
<text x="62" y="426.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="15" font-weight="bold" text-anchor="start" fill="currentColor">Update protocol</text>
<text x="62" y="446.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.7">proof of success</text>
<rect x="222" y="390" width="236" height="80" rx="8" fill="#f59e0b" fill-opacity="0.12" stroke="#f59e0b" stroke-width="1.5"/>
<text x="234" y="425.5" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">"Update done" means</text>
<text x="234" y="444.5" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">the update worked</text>
<line x1="462" y1="430.0" x2="478" y2="430.0" stroke="currentColor" stroke-width="1.6" stroke-opacity="0.6" marker-end="url(#m_cur)"/>
<rect x="482" y="390" width="262" height="80" rx="8" fill="#10b981" fill-opacity="0.12" stroke="#10b981" stroke-width="1.5"/>
<text x="494" y="416.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">One outcome per request; only</text>
<text x="494" y="435.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">the new version number counts</text>
<text x="494" y="454.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">as success</text>
<rect x="10" y="490" width="740" height="104" rx="10" fill="#94a3b8" fill-opacity="0.06"/>
<circle cx="36" cy="542.0" r="15" fill="#ef4444"/>
<text x="36" y="547.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#fefefe">5</text>
<text x="62" y="538.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="15" font-weight="bold" text-anchor="start" fill="currentColor">Power cut</text>
<text x="62" y="558.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.7">hold-up time</text>
<rect x="222" y="502" width="236" height="80" rx="8" fill="#f59e0b" fill-opacity="0.12" stroke="#f59e0b" stroke-width="1.5"/>
<text x="234" y="537.5" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">The capacitors keep the</text>
<text x="234" y="556.5" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">board alive long enough</text>
<line x1="462" y1="542.0" x2="478" y2="542.0" stroke="currentColor" stroke-width="1.6" stroke-opacity="0.6" marker-end="url(#m_cur)"/>
<rect x="482" y="502" width="262" height="80" rx="8" fill="#10b981" fill-opacity="0.12" stroke="#10b981" stroke-width="1.5"/>
<text x="494" y="528.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">Measure hold-up first. If an erase</text>
<text x="494" y="547.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">can outlast it, the power-fail</text>
<text x="494" y="566.0" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">path never starts one</text>
</svg>
</div>
  <figcaption>Five hidden assumptions, and the rule that replaces each one.</figcaption>
</figure>

<h2 id="1-the-board-boots-on-its-own-then-dies-behind-the-bootloader">1. The board boots on its own, then dies behind the bootloader</h2>
<p><strong>The mental model: the boot ROM was quietly doing your setup for you.</strong></p>
<p>A stand-alone application image on the i.MX RT works because the boot ROM reads the image&rsquo;s
boot header and runs its DCD (Device Configuration Data). That is where external SDRAM gets
configured. You never have to think about it.</p>
<p>Then you put a bootloader such as MCUboot in front of the application. A signed application
image behind a bootloader has no boot header of its own, so nobody runs its DCD. The first
stack push or the first <code>.bss</code> clear in SDRAM ends in a bus fault, long before <code>main()</code>.</p>
<p>The fix has two parts:</p>
<ol>
<li>Move the SDRAM setup (the DCD) into the <strong>bootloader</strong>, which the ROM still boots
normally.</li>
<li>Make the application <strong>skip the clock (PLL) re-initialisation</strong>, because the
bootloader has already done it.</li>
</ol>
<p>A related trap: in MCUboot&rsquo;s direct-XIP-with-revert mode, a slot without the image
trailer&rsquo;s magic value is erased at reset. The symptom is <em>the application disappears after
a power cycle</em>. Sign the image with the trailer included.</p>
<div class="callout callout--tip">
  <p class="callout__title"><span class="callout__icon" aria-hidden="true">💡</span>What to check on your own board</p>
  <div class="callout__body">
List everything the boot ROM does for a stand-alone image (DCD, clocks, memory) and ask who
does it once a bootloader sits in front. Anything nobody owns will fault before <code>main()</code>.
  </div>
</div>

<h2 id="2-measure-the-chip-not-the-documents">2. Measure the chip, not the documents</h2>
<p><strong>The mental model: the flash size is a measurement, not a setting.</strong></p>
<p>The flash size in a linker script, a ticket or a code comment is often copied from an
earlier board. Several documents agreeing with each other proves nothing if they all come
from the same assumption. Read the part number on the board, and probe the memory with a
debugger before you plan a partition map around it.</p>
<p>Then make the answer structural, so it cannot drift again:</p>
<ol>
<li><strong>One header</strong> owns the flash partition map.</li>
<li><strong>A start-up assert</strong> checks it against the linker symbols.</li>
<li><strong>A host test</strong> parses the linker scripts, so any drift fails in CI rather than on a
customer&rsquo;s device.</li>
</ol>
<p>The map matters most once you plan field updates. A dual-slot layout needs room for <strong>two
complete images</strong>, next to the bootloader and the data partition, and images grow over a
product&rsquo;s life.</p>
<figure class="diagram">
  <div class="diagram-svg" role="img" aria-label="Flash budget for dual-slot updates: bootloader, slot 1 with the running image, slot 2 for the incoming update, and a data partition must all fit in the part; each slot is sized for the worst-case build plus room to grow; above 16 MiB the flash needs 4-byte addressing; the layout is owned by one header and checked in CI"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 372">
<text x="20" y="72" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="currentColor">At design time</text>
<text x="20" y="90" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.7">everything fits</text>
<rect x="170" y="52" width="50" height="44" rx="4" fill="#94a3b8" fill-opacity="0.3" stroke="#94a3b8" stroke-width="1.5"/>
<text x="195.0" y="79" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="currentColor">boot</text>
<rect x="220" y="52" width="170" height="44" rx="4" fill="#3b82f6" fill-opacity="0.2" stroke="#3b82f6" stroke-width="1.5"/>
<text x="305.0" y="72" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="currentColor">slot 1</text>
<text x="305.0" y="88" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="11.5" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.8">running image</text>
<rect x="390" y="52" width="170" height="44" rx="4" fill="#3b82f6" fill-opacity="0.2" stroke="#3b82f6" stroke-width="1.5"/>
<text x="475.0" y="72" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="currentColor">slot 2</text>
<text x="475.0" y="88" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="11.5" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.8">incoming update</text>
<rect x="560" y="52" width="90" height="44" rx="4" fill="#14b8a6" fill-opacity="0.2" stroke="#14b8a6" stroke-width="1.5"/>
<text x="605.0" y="72" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="currentColor">data</text>
<text x="605.0" y="88" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="11.5" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.8">file system</text>
<text x="20" y="162" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="currentColor">Images grow</text>
<text x="20" y="180" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.7">both slots grow</text>
<rect x="170" y="142" width="50" height="44" rx="4" fill="#94a3b8" fill-opacity="0.3" stroke="#94a3b8" stroke-width="1.5"/>
<text x="195.0" y="169" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="currentColor">boot</text>
<rect x="220" y="142" width="215" height="44" rx="4" fill="#3b82f6" fill-opacity="0.2" stroke="#3b82f6" stroke-width="1.5"/>
<text x="327.5" y="162" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="currentColor">slot 1</text>
<text x="327.5" y="178" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="11.5" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.8">bigger image</text>
<rect x="435" y="142" width="215" height="44" rx="4" fill="#3b82f6" fill-opacity="0.2" stroke="#3b82f6" stroke-width="1.5"/>
<text x="542.5" y="162" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="currentColor">slot 2</text>
<text x="542.5" y="178" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="11.5" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.8">same size again</text>
<rect x="650" y="142" width="90" height="44" rx="4" fill="#ef4444" fill-opacity="0.22" stroke="#ef4444" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="695.0" y="162" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="currentColor">data</text>
<text x="695.0" y="178" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="11.5" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.8">no room</text>
<line x1="650" y1="38" x2="650" y2="200" stroke="currentColor" stroke-width="1.6" stroke-dasharray="5 4" stroke-opacity="0.7"/>
<text x="650" y="30" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.8">end of the part</text>
<text x="170" y="30" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.8">start of flash</text>
<rect x="20" y="222" width="720" height="136" rx="8" fill="#8b5cf6" fill-opacity="0.08" stroke="#8b5cf6" stroke-width="1"/>
<text x="36" y="248" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="currentColor">Rules for the partition map</text>
<text x="36" y="274" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">Size each slot for the worst-case build (debug, with tracing), plus room to grow.</text>
<text x="36" y="295" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">Two slots mean two complete images: an image that grows costs twice its growth.</text>
<text x="36" y="316" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">Above 16 MiB the flash needs 4-byte addressing: boot config, driver commands, size limit.</text>
<text x="36" y="337" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">One header owns the map; a start-up assert and a CI test check it against the linker.</text>
</svg>
</div>
  <figcaption>What a dual-slot layout has to fit, and the rules for sizing it.</figcaption>
</figure>

<table>
  <thead>
      <tr>
          <th>Budget question</th>
          <th>What to do</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Which build do I size against?</td>
          <td>The worst case: the debug build with tracing, not the release build.</td>
      </tr>
      <tr>
          <td>Does reflashing prove the layout?</td>
          <td>No. Debug probes often skip unchanged sectors, so a reflash test can prove nothing.</td>
      </tr>
      <tr>
          <td>What changes above 16 MiB?</td>
          <td>4-byte addressing. On the i.MX RT it has to be set consistently in the FlexSPI configuration block, the flash driver&rsquo;s commands (also used for XIP reads) and the driver&rsquo;s size limit.</td>
      </tr>
  </tbody>
</table>
<h2 id="3-writing-the-flash-you-execute-from">3. Writing the flash you execute from</h2>
<p><strong>The mental model: while the flash is busy, it does not exist.</strong></p>
<p>On the i.MX RT the code usually runs straight out of QSPI NOR (execute in place, XIP). A
firmware update writes to the same chip. From the moment you send a program or erase
command until the flash clears its busy bit (WIP), <strong>nothing</strong> may be fetched from it: not
the interrupt handler, not the vector table, not a helper function the compiler decided
not to inline.</p>
<p>The failure is intermittent, which makes it nasty. The update crashes at a different block
on every run, and every bench session looks like a different bug.</p>
<figure class="diagram">
  <div class="diagram-svg" role="img" aria-label="Timeline of one flash write while executing in place: running from flash, then a busy window where nothing may be fetched from flash and the ISR, vectors and helpers run from RAM with interrupts masked, then invalidate the instruction cache, then resume; a page program is short, a sector erase takes tens of milliseconds and many times longer in the worst case"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 340">
<defs><marker id="m_cur" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker></defs>
<rect x="20" y="70" width="170" height="92" rx="8" fill="#10b981" fill-opacity="0.15" stroke="#10b981" stroke-width="1.5"/>
<text x="34" y="108" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="currentColor">Running from</text>
<text x="34" y="127" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="currentColor">flash (XIP)</text>
<rect x="196" y="70" width="330" height="92" rx="8" fill="#ef4444" fill-opacity="0.15" stroke="#ef4444" stroke-width="2"/>
<text x="210" y="96" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="#ef4444">Flash busy (WIP = 1): it does not exist</text>
<text x="210" y="120" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" text-anchor="start" fill="currentColor">No fetch from flash at all: ISR, vector</text>
<text x="210" y="138" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" text-anchor="start" fill="currentColor">table and helpers must run from RAM;</text>
<text x="210" y="156" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" text-anchor="start" fill="currentColor">interrupts masked</text>
<rect x="532" y="70" width="102" height="92" rx="8" fill="#f59e0b" fill-opacity="0.15" stroke="#f59e0b" stroke-width="1.5"/>
<text x="544" y="108" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="currentColor">Invalidate</text>
<text x="544" y="127" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="currentColor">I-cache</text>
<rect x="640" y="70" width="100" height="92" rx="8" fill="#10b981" fill-opacity="0.15" stroke="#10b981" stroke-width="1.5"/>
<text x="652" y="108" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="currentColor">Resume</text>
<text x="652" y="127" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="currentColor">XIP</text>
<line x1="196" y1="40" x2="196" y2="68" stroke="currentColor" stroke-width="1.6" stroke-opacity="0.7" marker-end="url(#m_cur)"/>
<text x="196" y="34" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="middle" fill="currentColor">program / erase command</text>
<line x1="526" y1="40" x2="526" y2="68" stroke="currentColor" stroke-width="1.6" stroke-opacity="0.7" marker-end="url(#m_cur)"/>
<text x="560" y="34" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="middle" fill="currentColor">busy bit clears</text>
<text x="210" y="192" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">How long is the window? Check your datasheet:</text>
<text x="210" y="212" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">page program</text>
<text x="310" y="212" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" text-anchor="start" fill="currentColor">short</text>
<text x="210" y="230" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">sector erase</text>
<text x="310" y="230" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" text-anchor="start" fill="currentColor">tens of ms, typical</text>
<text x="210" y="248" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">worst case</text>
<text x="310" y="248" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" text-anchor="start" fill="currentColor">many times the typical value</text>
<text x="540" y="192" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.8">Cortex-M7 speculative</text>
<text x="540" y="209" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.8">fetches may have pulled</text>
<text x="540" y="226" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.8">stale lines in the cache</text>
<rect x="20" y="286" width="720" height="38" rx="8" fill="#ef4444" fill-opacity="0.12" stroke="#ef4444" stroke-width="1.5"/>
<text x="36" y="310" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="start" fill="currentColor">Without this: a crash at a random block, different on every run</text>
</svg>
</div>
  <figcaption>One write to the flash you execute from. While the busy bit is set, the flash does not exist for the CPU.</figcaption>
</figure>

<p>What fixes it:</p>
<ol>
<li>Run the erase and program routines, and everything they call, from RAM.</li>
<li>Keep interrupts masked while the flash is busy. Check the datasheet for how long that
is: a page program is short, but a sector erase takes tens of milliseconds, and the
worst case is many times the typical value.</li>
<li><strong>Invalidate the instruction cache</strong> afterwards. The Cortex-M7 fetches speculatively and
can have pulled in stale or garbage lines during the write.</li>
</ol>
<div class="callout callout--warn">
  <p class="callout__title"><span class="callout__icon" aria-hidden="true">⚠️</span>An unknown opcode fails silently</p>
  <div class="callout__body">
A NOR flash ignores a command it does not support. Send the wrong erase opcode and the erase
&ldquo;succeeds&rdquo; instantly. If an erase finishes far faster than the datasheet&rsquo;s typical time,
suspect that the command never ran.
  </div>
</div>

<h2 id="4-treat-the-field-update-as-a-protocol">4. Treat the field update as a protocol</h2>
<p><strong>The mental model: every update request ends in exactly one outcome, and the new version
number is the only proof.</strong></p>
<p>A device that reports &ldquo;update done&rdquo; has proven nothing. It might reboot into the old image,
or not reboot at all. Update logic only gets robust once you treat it as a protocol with
explicit rules:</p>
<ul>
<li>Every request ends in <strong>exactly one</strong> outcome: success, a classified failure, or a
timeout. Never silence, never two answers.</li>
<li>After the restart, poll the device for its version with a fixed interval and a hard
limit. Only the new version string counts as success.</li>
<li>Retries must fit the device&rsquo;s own limits. If the target closes an idle session after a
timeout, all your retries together must finish inside it, or the retries themselves
break the session.</li>
</ul>
<p>The bench catches what unit tests cannot: a bootloader step on the target that takes far
longer than you assumed, or a frame buffer that does not fit on the task stack you gave it.
Measure step times on hardware and check stack high-water marks before you trust the
timeouts.</p>
<h2 id="5-production-ready-means-surviving-the-power-cut">5. Production-ready means surviving the power cut</h2>
<p><strong>The mental model: measure the hold-up time before you design the software around it.</strong></p>
<p>When the supply drops, the board runs a little longer on its capacitors. That hold-up time
is a hardware property: measure it on a real board, in the worst-case load state, before
you design the power-fail path. One wrong part in the hold-up circuit can bring it close
to zero.</p>
<p>That number decides the software design. If the worst-case flash erase (lesson 3) is
longer than the hold-up time, <strong>the emergency path must never start a flash operation</strong>. It
only finishes or abandons what is already running, and closes the file system.</p>
<figure class="diagram">
  <div class="diagram-svg" role="img" aria-label="Power-fail timeline: the supply drops, the power-fail interrupt fires and the measured hold-up window starts; closing the file system and a typical erase fit in the window, a worst-case erase can outlast it; so the emergency path never starts a flash operation"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 372">
<line x1="230" y1="40" x2="230" y2="254" stroke="#f59e0b" stroke-width="2"/>
<text x="230" y="22" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="middle" fill="currentColor">power-fail interrupt</text>
<line x1="590" y1="40" x2="590" y2="254" stroke="#ef4444" stroke-width="2" stroke-dasharray="5 4"/>
<text x="590" y="22" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="middle" fill="currentColor">brown-out: CPU stops</text>
<rect x="230" y="48" width="360" height="34" rx="8" fill="#10b981" fill-opacity="0.18" stroke="#10b981" stroke-width="1.5"/>
<text x="410.0" y="70" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="currentColor">hold-up time: measure it on the real board</text>
<text x="20" y="70" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="start" fill="currentColor">Capacitors</text>
<text x="20" y="124" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">Close the file system</text>
<rect x="230" y="108" width="30" height="24" rx="3" fill="#3b82f6" fill-opacity="0.35" stroke="#3b82f6" stroke-width="1.5"/>
<text x="270" y="125" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="start" fill="#10b981">fits</text>
<text x="20" y="172" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">Typical sector erase</text>
<rect x="230" y="156" width="120" height="24" rx="3" fill="#f59e0b" fill-opacity="0.35" stroke="#f59e0b" stroke-width="1.5"/>
<text x="360" y="173" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="start" fill="#10b981">fits</text>
<text x="20" y="220" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="start" fill="currentColor">Worst-case sector erase</text>
<rect x="230" y="204" width="470" height="24" rx="3" fill="#ef4444" fill-opacity="0.35" stroke="#ef4444" stroke-width="1.5"/>
<text x="690" y="221" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="end" fill="currentColor">outlasts the hold-up</text>
<text x="745" y="254" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="11.5" font-weight="normal" text-anchor="end" fill="currentColor" fill-opacity="0.6" font-style="italic">not to scale</text>
<rect x="20" y="270" width="720" height="88" rx="8" fill="#ef4444" fill-opacity="0.08" stroke="#ef4444" stroke-width="1"/>
<text x="36" y="294" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="currentColor">If a worst-case erase can outlast the hold-up time:</text>
<text x="36" y="318" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">the power-fail path never starts a flash operation. It only finishes or abandons</text>
<text x="36" y="338" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="start" fill="currentColor">what is already running, and closes the file system.</text>
</svg>
</div>
  <figcaption>The hold-up window against flash timing. A worst-case erase may not fit, so the power-fail path never starts one.</figcaption>
</figure>

<p>Then test it properly. Cut power repeatedly while the firmware writes continuously, and
check after each cut that the file system is consistent and the last committed version of
each file survived.</p>
<div class="callout callout--check">
  <p class="callout__title"><span class="callout__icon" aria-hidden="true">✅</span>Test the test</p>
  <div class="callout__body">
A power-cut test that never cuts during a write proves nothing. Count how many cuts actually
landed mid-write before you trust the pass rate.
  </div>
</div>

<h2 id="summary">Summary</h2>
<ol>
<li><strong>Own what the ROM did for you.</strong> Once a bootloader sits in front, it has to set up
memory and clocks.</li>
<li><strong>Measure, then make it structural.</strong> Flash size, step timings and hold-up time are
measurements; a header, an assert and a CI test keep them true.</li>
<li><strong>Design for the worst case.</strong> The flash write path runs from RAM, updates end in exactly
one outcome, and the power-fail path never starts an erase.</li>
</ol>
<div class="callout callout--note">
  <p class="callout__title"><span class="callout__icon" aria-hidden="true">📝</span>Sources</p>
  <div class="callout__body">
<ul>
<li>NXP, <em>i.MX RT1060 Processor Reference Manual</em>: boot ROM, IVT/DCD, FlexSPI.</li>
<li>MCUboot documentation: image trailer and the direct-XIP / revert modes.</li>
<li>The datasheet of your QSPI NOR, for its typical and maximum program and erase times.</li>
</ul>

  </div>
</div>

<p>💬 Questions, corrections, or your own war story? Leave a comment below.</p>
]]></content:encoded></item><item><title>Single Pair Ethernet in Practice: Bringing SPE into an Industrial Control System</title><link>https://hoanguyen.de/posts/single-pair-ethernet-daisy-chain-industrial/</link><pubDate>Tue, 10 Nov 2026 00:00:00 +0000</pubDate><guid>https://hoanguyen.de/posts/single-pair-ethernet-daisy-chain-industrial/</guid><description>What it takes in firmware to run a daisy chain of devices over Single Pair Ethernet: which port decides that the controller is gone, how a node learns its position, when to store the topology, why the receive interrupt lies, and how to update every device on the chain.</description><content:encoded><![CDATA[<p>Single Pair Ethernet (SPE) runs Ethernet over one twisted pair instead of four, and the same
pair can also carry the power. For a machine whose devices sit in a line, that means one
cable from device to device, carrying both data and power.</p>
<p>Most of what you read about SPE is about cables, connectors and PHYs. This post is about the
firmware. As soon as devices are chained one behind the other, the software has to answer
questions a star network never asks: <em>Which neighbour is the controller? Where am I in the
line? What happens to everyone behind me when I reboot?</em></p>
<p>In this article I go through the firmware lessons from bringing a 100BASE-T1 daisy chain into
an industrial control system: one controller, a line of identical nodes, each node an
i.MX RT (Cortex-M7) bridging frames between its two Ethernet ports.</p>
<figure class="diagram">
  <div class="diagram-svg" role="img" aria-label="Daisy chain over 100BASE-T1: a controller sends discovery with hop 0; node 1 and node 2 each have an upstream PHY, an i.MX RT1062 with two Ethernet MACs bridging in software, and a downstream PHY; one pair carries data and power; each node takes position hop plus 1 and forwards with hop plus 1; identity and MAC come from a secure element serial; only the upstream link means controller reachable"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 780 636">
<defs><marker id="m_amber" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#f59e0b"/></marker></defs>
<rect x="150" y="10" width="260" height="56" rx="8" fill="#8b5cf6" fill-opacity="0.2" stroke="#8b5cf6" stroke-width="2"/>
<text x="280" y="36" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="15" font-weight="bold" text-anchor="middle" fill="currentColor">Controller (head)</text>
<text x="280" y="55" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="middle" fill="currentColor">sends discovery with hop = 0</text>
<rect x="120" y="124" width="320" height="196" rx="8" fill="#94a3b8" fill-opacity="0.06" stroke="#94a3b8" stroke-width="1.5"/>
<text x="136" y="148" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="15" font-weight="bold" text-anchor="start" fill="currentColor">Node 1</text>
<text x="424" y="148" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="end" fill="#3b82f6">position 1</text>
<rect x="150" y="160" width="260" height="40" rx="8" fill="#3b82f6" fill-opacity="0.2" stroke="#3b82f6" stroke-width="2.2"/>
<text x="280" y="185" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="middle" fill="currentColor">upstream PHY</text>
<rect x="150" y="210" width="260" height="58" rx="8" fill="#14b8a6" fill-opacity="0.14" stroke="#14b8a6" stroke-width="1.5"/>
<text x="280" y="232" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="currentColor">i.MX RT1062 · 2 Ethernet MACs</text>
<text x="280" y="252" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="middle" fill="currentColor">software bridge (low-priority task)</text>
<rect x="150" y="278" width="260" height="34" rx="8" fill="#94a3b8" fill-opacity="0.2" stroke="#94a3b8" stroke-width="1.5"/>
<text x="280" y="300" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="middle" fill="currentColor">downstream PHY</text>
<text x="468" y="174" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">receives hop = 0</text>
<text x="468" y="192" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" text-anchor="start" fill="currentColor">→ position = hop + 1 = 1</text>
<text x="468" y="228" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.85">identity + MAC address from</text>
<text x="468" y="245" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.85">the secure element's serial</text>
<text x="468" y="284" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" text-anchor="start" fill="currentColor">forwards with hop = 1</text>
<line x1="464" y1="180" x2="414" y2="180" stroke="#3b82f6" stroke-width="1.6" stroke-dasharray="3 3" stroke-opacity="0.5"/>
<rect x="120" y="370" width="320" height="196" rx="8" fill="#94a3b8" fill-opacity="0.06" stroke="#94a3b8" stroke-width="1.5"/>
<text x="136" y="394" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="15" font-weight="bold" text-anchor="start" fill="currentColor">Node 2</text>
<text x="424" y="394" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="end" fill="#3b82f6">position 2</text>
<rect x="150" y="406" width="260" height="40" rx="8" fill="#3b82f6" fill-opacity="0.2" stroke="#3b82f6" stroke-width="2.2"/>
<text x="280" y="431" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="middle" fill="currentColor">upstream PHY</text>
<rect x="150" y="456" width="260" height="58" rx="8" fill="#14b8a6" fill-opacity="0.14" stroke="#14b8a6" stroke-width="1.5"/>
<text x="280" y="478" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="currentColor">i.MX RT1062 · 2 Ethernet MACs</text>
<text x="280" y="498" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="middle" fill="currentColor">software bridge (low-priority task)</text>
<rect x="150" y="524" width="260" height="34" rx="8" fill="#94a3b8" fill-opacity="0.2" stroke="#94a3b8" stroke-width="1.5"/>
<text x="280" y="546" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="middle" fill="currentColor">downstream PHY</text>
<text x="468" y="420" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">receives hop = 1</text>
<text x="468" y="438" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" text-anchor="start" fill="currentColor">→ position = hop + 1 = 2</text>
<text x="468" y="474" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.85">identity + MAC address from</text>
<text x="468" y="491" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.85">the secure element's serial</text>
<text x="468" y="530" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" text-anchor="start" fill="currentColor">forwards with hop = 2</text>
<line x1="464" y1="426" x2="414" y2="426" stroke="#3b82f6" stroke-width="1.6" stroke-dasharray="3 3" stroke-opacity="0.5"/>
<line x1="280" y1="66" x2="280" y2="158" stroke="#f59e0b" stroke-width="3.5" marker-end="url(#m_amber)"/>
<line x1="280" y1="312" x2="280" y2="404" stroke="#f59e0b" stroke-width="3.5" marker-end="url(#m_amber)"/>
<text x="296" y="92" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="start" fill="currentColor">100BASE-T1, one pair:</text>
<text x="296" y="109" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor">data + power (PoDL)</text>
<text x="296" y="346" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="start" fill="currentColor">one pair:</text>
<text x="296" y="363" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor">data + power</text>
<line x1="280" y1="558" x2="280" y2="588" stroke="#f59e0b" stroke-width="3.5" stroke-dasharray="6 5"/>
<text x="280" y="610" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.8">… node N</text>
<rect x="468" y="580" width="302" height="44" rx="8" fill="#3b82f6" fill-opacity="0.12" stroke="#3b82f6" stroke-width="1.5"/>
<text x="480" y="598" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="start" fill="currentColor">Only the upstream link means</text>
<text x="480" y="615" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="start" fill="currentColor">"controller reachable"</text>
</svg>
</div>
  <figcaption>The chain as the firmware sees it: two PHYs per node, position learnt from a hop counter.</figcaption>
</figure>

<h2 id="the-setup">The setup</h2>
<ul>
<li><strong>Physical layer:</strong> 100BASE-T1, point-to-point links chained into a line. Power for the
nodes travels over the same pair (power over data line, PoDL).</li>
<li><strong>Nodes:</strong> identical hardware and <strong>one firmware image</strong> for all of them. Each node has two
Ethernet MACs, one per PHY.</li>
<li><strong>Software:</strong> an RTOS, with a lower-priority forwarding task and a queue that bridge frames
from one port to the other.</li>
</ul>
<h2 id="1-only-the-upstream-port-decides-controller-reachable">1. Only the upstream port decides &ldquo;controller reachable&rdquo;</h2>
<p><strong>The mental model: a node&rsquo;s link to the controller is its upstream port. The downstream port
is someone else&rsquo;s problem.</strong></p>
<p>A tempting first version marks the link as up when <em>either</em> PHY has a link:
<code>up = upstream || downstream</code>. That looks harmless. But every node except the last one
always has a live downstream link, so it never notices that the controller is gone and keeps
running.</p>
<p>The rule that works: the link state the protocol sees is the <strong>upstream PHY&rsquo;s current link
bit, and nothing else</strong>. The downstream bit only controls whether frames are forwarded. Keep
the rule as a tiny pure function with host tests, so it cannot quietly regress.</p>
<figure class="diagram">
  <div class="diagram-svg" role="img" aria-label="A chain of a controller and three nodes with the cable broken between the controller and node 1. With up equals upstream or downstream, all three nodes keep running. With up equals upstream, node 1 stops safely but nodes 2 and 3 keep running because their own upstream links are fine. With an end-to-end heartbeat added, all three nodes stop safely"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 780 380">
<rect x="170" y="14" width="130" height="50" rx="8" fill="#ef4444" fill-opacity="0.08" stroke="#ef4444" stroke-width="1.5" stroke-dasharray="5 4"/>
<text x="235.0" y="36" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="currentColor">Controller</text>
<text x="235.0" y="54" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="middle" fill="#ef4444">gone</text>
<rect x="330" y="14" width="130" height="50" rx="8" fill="#94a3b8" fill-opacity="0.12" stroke="#94a3b8" stroke-width="1.5"/>
<text x="395.0" y="44" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="currentColor">Node 1</text>
<rect x="480" y="14" width="130" height="50" rx="8" fill="#94a3b8" fill-opacity="0.12" stroke="#94a3b8" stroke-width="1.5"/>
<text x="545.0" y="44" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="currentColor">Node 2</text>
<rect x="630" y="14" width="130" height="50" rx="8" fill="#94a3b8" fill-opacity="0.12" stroke="#94a3b8" stroke-width="1.5"/>
<text x="695.0" y="44" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="currentColor">Node 3</text>
<line x1="300" y1="39" x2="330" y2="39" stroke="#ef4444" stroke-width="3" stroke-dasharray="3 3"/>
<text x="315" y="30" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="16" font-weight="bold" text-anchor="middle" fill="#ef4444">✕</text>
<line x1="460" y1="39" x2="480" y2="39" stroke="#f59e0b" stroke-width="3.5"/>
<line x1="610" y1="39" x2="630" y2="39" stroke="#f59e0b" stroke-width="3.5"/>
<text x="20" y="36" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="start" fill="currentColor">Cable broken</text>
<text x="20" y="54" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.75">after the controller</text>
<rect x="10" y="90" width="760" height="80" rx="8" fill="#94a3b8" fill-opacity="0.05"/>
<text x="20" y="112" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.7">Rule:</text>
<text x="20" y="132" font-family="ui-monospace, 'SFMono-Regular', Consolas, monospace" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">up = upstream</text>
<text x="20" y="150" font-family="ui-monospace, 'SFMono-Regular', Consolas, monospace" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">|| downstream</text>
<rect x="330" y="102" width="130" height="56" rx="8" fill="#ef4444" fill-opacity="0.15" stroke="#ef4444" stroke-width="1.5"/>
<text x="395.0" y="126" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="middle" fill="#ef4444">keeps running</text>
<text x="395.0" y="145" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.85">downstream is up</text>
<rect x="480" y="102" width="130" height="56" rx="8" fill="#ef4444" fill-opacity="0.15" stroke="#ef4444" stroke-width="1.5"/>
<text x="545.0" y="126" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="middle" fill="#ef4444">keeps running</text>
<text x="545.0" y="145" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.85">both links up</text>
<rect x="630" y="102" width="130" height="56" rx="8" fill="#ef4444" fill-opacity="0.15" stroke="#ef4444" stroke-width="1.5"/>
<text x="695.0" y="126" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="middle" fill="#ef4444">keeps running</text>
<text x="695.0" y="145" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.85">upstream is up</text>
<rect x="10" y="178" width="760" height="80" rx="8" fill="#94a3b8" fill-opacity="0.05"/>
<text x="20" y="200" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.7">Rule:</text>
<text x="20" y="220" font-family="ui-monospace, 'SFMono-Regular', Consolas, monospace" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">up = upstream</text>
<rect x="330" y="190" width="130" height="56" rx="8" fill="#10b981" fill-opacity="0.15" stroke="#10b981" stroke-width="1.5"/>
<text x="395.0" y="214" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="middle" fill="#10b981">stops safely</text>
<text x="395.0" y="233" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.85">upstream lost</text>
<rect x="480" y="190" width="130" height="56" rx="8" fill="#ef4444" fill-opacity="0.15" stroke="#ef4444" stroke-width="1.5"/>
<text x="545.0" y="214" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="middle" fill="#ef4444">keeps running</text>
<text x="545.0" y="233" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.85">own upstream fine</text>
<rect x="630" y="190" width="130" height="56" rx="8" fill="#ef4444" fill-opacity="0.15" stroke="#ef4444" stroke-width="1.5"/>
<text x="695.0" y="214" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="middle" fill="#ef4444">keeps running</text>
<text x="695.0" y="233" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.85">own upstream fine</text>
<rect x="10" y="266" width="760" height="80" rx="8" fill="#94a3b8" fill-opacity="0.05"/>
<text x="20" y="288" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.7">Rule:</text>
<text x="20" y="308" font-family="ui-monospace, 'SFMono-Regular', Consolas, monospace" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">up = upstream</text>
<text x="20" y="326" font-family="ui-monospace, 'SFMono-Regular', Consolas, monospace" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">+ heartbeat</text>
<rect x="330" y="278" width="130" height="56" rx="8" fill="#10b981" fill-opacity="0.15" stroke="#10b981" stroke-width="1.5"/>
<text x="395.0" y="302" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="middle" fill="#10b981">stops safely</text>
<text x="395.0" y="321" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.85">upstream lost</text>
<rect x="480" y="278" width="130" height="56" rx="8" fill="#10b981" fill-opacity="0.15" stroke="#10b981" stroke-width="1.5"/>
<text x="545.0" y="302" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="middle" fill="#10b981">stops safely</text>
<text x="545.0" y="321" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.85">heartbeat timeout</text>
<rect x="630" y="278" width="130" height="56" rx="8" fill="#10b981" fill-opacity="0.15" stroke="#10b981" stroke-width="1.5"/>
<text x="695.0" y="302" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="middle" fill="#10b981">stops safely</text>
<text x="695.0" y="321" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.85">heartbeat timeout</text>
<text x="390" y="372" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.85">Link state is local: only the first node after the break sees it. The rest needs a heartbeat.</text>
</svg>
</div>
  <figcaption>Which nodes notice a lost controller, under each rule. Link state alone only protects the first node after the break.</figcaption>
</figure>

<div class="callout callout--tip">
  <p class="callout__title"><span class="callout__icon" aria-hidden="true">💡</span>Testing link loss when the cable carries power</p>
  <div class="callout__body">
Pulling the cable is useless as a test here: with power over the data pair, the node simply
switches off. Put the controller&rsquo;s PHY into standby for a few seconds instead, and start it
again.
  </div>
</div>

<p>One limit remains, and the diagram shows it: link state is local. A node only sees its own
neighbour. Detecting a lost controller <em>everywhere</em> in a long chain needs an end-to-end
heartbeat, not link state.</p>
<h2 id="2-position-from-the-hop-count-identity-from-silicon">2. Position from the hop count, identity from silicon</h2>
<p><strong>The mental model: a node&rsquo;s identity is fixed, its position is learnt.</strong></p>
<p>With one firmware image for every node, nothing in flash says &ldquo;you are node 3&rdquo;. So the two
questions are split:</p>
<ul>
<li><strong>Identity</strong> comes from a unique serial number in silicon, for example from a secure
element on each node. The node also derives its MAC address from that serial, setting the
&ldquo;locally administered&rdquo; bit and clearing the &ldquo;multicast&rdquo; bit, before the Ethernet MACs
start.</li>
<li><strong>Position</strong> is discovered at run time. The controller sends a discovery request with a hop
counter of 0, and every node that forwards it downstream adds 1.</li>
</ul>
<p>Two details make it robust:</p>
<ol>
<li><strong>Commands go to a position, never to &ldquo;everyone&rdquo;.</strong> There is no broadcast fallback, because
a broadcast reaches the wrong node when the order in the line is not the one that was
accepted.</li>
<li><strong>Rediscovery waits for the line to settle.</strong> Nodes report a new downstream link a limited
number of times, and the controller collects answers in a short window after the last
link-up instead of reacting to each one.</li>
</ol>
<h2 id="3-store-the-topology-only-once-it-is-stable">3. Store the topology only once it is stable</h2>
<p>The controller remembers which device sat at which position, so it can spot a swapped,
replaced, missing or extra device later. The trap is <em>when</em> to store that map.</p>
<p>If you store the first discovery after power-up, you may store a <strong>half-built chain</strong>: the
nodes further down are still booting. Later, the missing ones come up and get flagged as
&ldquo;extra devices&rdquo;: a false fault.</p>
<p>The fix is a debounce. Every usable discovery restarts one timer, and the map is written only
when the timer fires. A cold start where every node answers the first discovery will not
catch the problem. Test with a <strong>growing chain</strong>, where nodes come up one after another, and
check that the map is written exactly once.</p>
<figure class="diagram">
  <div class="diagram-svg" role="img" aria-label="Two timelines of a growing chain: storing the first discovery saves a map with one node and then flags node 2 as an extra device when it boots; with a debounce, each discovery re-arms a timer and the map is written once, after the chain has stopped changing"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 780 330">
<defs><marker id="m_cur" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker></defs>
<line x1="210" y1="270" x2="760" y2="270" stroke="currentColor" stroke-width="1.6" stroke-opacity="0.6" marker-end="url(#m_cur)"/>
<text x="210" y="292" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.8">controller boots</text>
<text x="760" y="292" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="end" fill="currentColor" fill-opacity="0.8">time</text>
<rect x="10" y="40" width="760" height="90" rx="8" fill="#ef4444" fill-opacity="0.06" stroke="#ef4444" stroke-width="1"/>
<text x="22" y="78" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="start" fill="currentColor">Store the first</text>
<text x="22" y="96" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="start" fill="currentColor">discovery</text>
<circle cx="267.2" cy="85" r="7" fill="#ef4444"/>
<text x="279.2" y="74" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor">saved map:</text>
<text x="279.2" y="90" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor">1 node</text>
<circle cx="388.2" cy="85" r="7" fill="#ef4444"/>
<text x="400.2" y="74" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="start" fill="#ef4444">node 2 boots → flagged</text>
<text x="400.2" y="90" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="#ef4444">"extra device": a false fault</text>
<rect x="10" y="150" width="760" height="96" rx="8" fill="#10b981" fill-opacity="0.06" stroke="#10b981" stroke-width="1"/>
<text x="22" y="190" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="start" fill="currentColor">Debounce: wait</text>
<text x="22" y="208" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13.5" font-weight="bold" text-anchor="start" fill="currentColor">for a stable chain</text>
<rect x="267.2" y="190" width="121.0" height="12" rx="3" fill="#f59e0b" fill-opacity="0.35"/>
<rect x="388.2" y="190" width="330.00000000000006" height="12" rx="3" fill="#f59e0b" fill-opacity="0.35"/>
<text x="553.2" y="182" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor">settle timer, re-armed by every discovery</text>
<circle cx="267.2" cy="196" r="6" fill="#3b82f6"/>
<text x="267.2" y="224" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor">node 1 answers</text>
<circle cx="388.2" cy="196" r="6" fill="#3b82f6"/>
<text x="388.2" y="224" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor">node 2 answers</text>
<circle cx="718.2" cy="196" r="8" fill="#10b981"/>
<text x="712.2" y="222" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="end" fill="#10b981">map written once</text>
<text x="712.2" y="238" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="end" fill="currentColor">chain stopped changing</text>
<line x1="267.2" y1="134" x2="267.2" y2="264" stroke="currentColor" stroke-width="1.6" stroke-dasharray="3 4" stroke-opacity="0.35"/>
<line x1="388.2" y1="134" x2="388.2" y2="264" stroke="currentColor" stroke-width="1.6" stroke-dasharray="3 4" stroke-opacity="0.35"/>
<line x1="718.2" y1="134" x2="718.2" y2="264" stroke="currentColor" stroke-width="1.6" stroke-dasharray="3 4" stroke-opacity="0.35"/>
</svg>
</div>
  <figcaption>Why the first discovery must not be stored.</figcaption>
</figure>

<p>Save the map atomically (write a temporary file, sync, then rename), with a CRC so a damaged
file is detected.</p>
<div class="callout callout--note">
  <p class="callout__title"><span class="callout__icon" aria-hidden="true">📝</span>Enforce at once, notify later</p>
  <div class="callout__body">
A node whose identity does not match the stored map is blocked from the first event: it
receives no normal commands. Only the operator&rsquo;s fault screen waits for the topology to
settle, so the user is not shown flickering faults during start-up. Safe commands and
firmware updates should still reach a blocked node.
  </div>
</div>

<h2 id="4-the-receive-interrupt-is-one-bit-drain-the-ring">4. The receive interrupt is one bit: drain the ring</h2>
<p><strong>The mental model: &ldquo;frames arrived&rdquo; is a flag, not a count.</strong></p>
<p>The i.MX RT Ethernet MAC raises one receive interrupt flag no matter how many frames landed.
If the CPU was busy and three frames arrived, the interrupt still fires once. A handler that
reads one frame per interrupt falls behind and stays behind, and no error counter moves.</p>
<figure class="diagram">
  <div class="diagram-svg" role="img" aria-label="A 4-descriptor receive ring holds 3 frames after a stall while the interrupt flag fires once; reading one frame per interrupt leaves 2 frames behind for good, draining the ring until empty leaves it free"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 780 330">
<text x="20" y="32" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">4-descriptor receive ring after a stall</text>
<rect x="20" y="44" width="56" height="44" rx="4" fill="#3b82f6" fill-opacity="0.3" stroke="#3b82f6" stroke-width="1.5"/>
<text x="48" y="71" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="1">frame</text>
<rect x="82" y="44" width="56" height="44" rx="4" fill="#3b82f6" fill-opacity="0.3" stroke="#3b82f6" stroke-width="1.5"/>
<text x="110" y="71" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="1">frame</text>
<rect x="144" y="44" width="56" height="44" rx="4" fill="#3b82f6" fill-opacity="0.3" stroke="#3b82f6" stroke-width="1.5"/>
<text x="172" y="71" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="1">frame</text>
<rect x="206" y="44" width="56" height="44" rx="4" fill="#94a3b8" fill-opacity="0.08" stroke="#94a3b8" stroke-width="1.5"/>
<text x="234" y="71" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.6">free</text>
<rect x="300" y="44" width="170" height="44" rx="8" fill="#f59e0b" fill-opacity="0.18" stroke="#f59e0b" stroke-width="2"/>
<text x="312" y="62" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="start" fill="currentColor">RX interrupt flag</text>
<text x="312" y="79" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor">one bit, fires once</text>
<text x="490" y="72" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor" fill-opacity="0.85">3 frames arrived, the CPU hears about it once</text>
<rect x="10" y="120" width="375" height="200" rx="8" fill="#ef4444" fill-opacity="0.06" stroke="#ef4444" stroke-width="1"/>
<text x="24" y="146" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="#ef4444">Read one frame per interrupt</text>
<text x="24" y="168" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">after the handler ran</text>
<rect x="24" y="180" width="56" height="44" rx="4" fill="#3b82f6" fill-opacity="0.3" stroke="#3b82f6" stroke-width="1.5"/>
<text x="52" y="207" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="1">frame</text>
<rect x="86" y="180" width="56" height="44" rx="4" fill="#3b82f6" fill-opacity="0.3" stroke="#3b82f6" stroke-width="1.5"/>
<text x="114" y="207" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="1">frame</text>
<rect x="148" y="180" width="56" height="44" rx="4" fill="#94a3b8" fill-opacity="0.08" stroke="#94a3b8" stroke-width="1.5"/>
<text x="176" y="207" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.6">free</text>
<rect x="210" y="180" width="56" height="44" rx="4" fill="#94a3b8" fill-opacity="0.08" stroke="#94a3b8" stroke-width="1.5"/>
<text x="238" y="207" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.6">free</text>
<text x="24" y="254" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor">2 frames stay behind for good:</text>
<text x="24" y="272" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor">every reply is late from now on.</text>
<text x="24" y="290" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="bold" text-anchor="start" fill="currentColor">No error counter moves.</text>
<rect x="395" y="120" width="375" height="200" rx="8" fill="#10b981" fill-opacity="0.06" stroke="#10b981" stroke-width="1"/>
<text x="409" y="146" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="start" fill="#10b981">Drain the ring on every interrupt</text>
<text x="409" y="168" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="start" fill="currentColor">after the handler ran</text>
<rect x="409" y="180" width="56" height="44" rx="4" fill="#94a3b8" fill-opacity="0.08" stroke="#94a3b8" stroke-width="1.5"/>
<text x="437" y="207" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.6">free</text>
<rect x="471" y="180" width="56" height="44" rx="4" fill="#94a3b8" fill-opacity="0.08" stroke="#94a3b8" stroke-width="1.5"/>
<text x="499" y="207" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.6">free</text>
<rect x="533" y="180" width="56" height="44" rx="4" fill="#94a3b8" fill-opacity="0.08" stroke="#94a3b8" stroke-width="1.5"/>
<text x="561" y="207" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.6">free</text>
<rect x="595" y="180" width="56" height="44" rx="4" fill="#94a3b8" fill-opacity="0.08" stroke="#94a3b8" stroke-width="1.5"/>
<text x="623" y="207" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="normal" text-anchor="middle" fill="currentColor" fill-opacity="0.6">free</text>
<text x="409" y="254" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor">loop: read until empty, bounded</text>
<text x="409" y="272" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor">by the ring size; free bad frames;</text>
<text x="409" y="290" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="normal" text-anchor="start" fill="currentColor">queue sized for a full ring per port</text>
</svg>
</div>
  <figcaption>One interrupt, three frames. Only a drain loop catches up.</figcaption>
</figure>

<p>In a daisy chain this hurts more than on a single device: every node behind the slow one
also answers late.</p>
<p>The fix: on each interrupt, keep reading until the receive ring is empty, bounded by the ring
size. Three companion changes go with it:</p>
<ul>
<li>Read and discard bad frames too, so their buffers are freed.</li>
<li>Size the forwarding queue for a full ring from each port.</li>
<li>Keep the frame buffer static, off the interrupt stack.</li>
</ul>
<h2 id="5-keep-slow-hardware-access-off-the-critical-path">5. Keep slow hardware access off the critical path</h2>
<p>The PHYs are managed over MDIO, which is often bit-banged on GPIO pins. Running that inside a
high-priority task blocks the CPU for milliseconds at a time.</p>
<p>Move it to a low-priority worker and match the results back by a token. The high-priority
task then only queues a request and picks up the answer later, and its worst-case runtime
drops from milliseconds to microseconds.</p>
<div class="callout callout--warn">
  <p class="callout__title"><span class="callout__icon" aria-hidden="true">⚠️</span>Bit-bang timing is empirical</p>
  <div class="callout__body">
Shortening the bit-bang delay looks like a free speed-up, and it can break communication with
the PHY entirely. Measure before you optimise a bit-banged bus.
  </div>
</div>

<h2 id="6-updating-every-device-on-the-chain">6. Updating every device on the chain</h2>
<p>Firmware updates over the chain follow a few simple rules:</p>
<ol>
<li>Update the devices <strong>one after another</strong>, in list order, and skip devices that already run
the target version.</li>
<li><strong>Wait for each device to report its new version</strong> before moving on. A restarting device
can cut off every device behind it.</li>
<li>Classify failures: per device (retry, continue, stop) or global (end the batch).</li>
<li>Do not show topology faults while the batch runs. Devices dropping out during their own
restart is expected.</li>
</ol>
<p>Staging every device first and restarting them all together might be faster. Until you have
measured that the chain survives it, keep the update sequential.</p>
<h2 id="summary">Summary</h2>
<ol>
<li><strong>One rule per question:</strong> only the upstream port defines &ldquo;controller reachable&rdquo;, the hop
count defines position, and silicon defines identity.</li>
<li><strong>Wait for the line to settle:</strong> store the topology, and show faults, only once the chain
has stopped changing.</li>
<li><strong>The general rule:</strong> in a daisy chain, every node&rsquo;s behaviour affects everyone behind it,
so design and test for the line, not for the single device.</li>
</ol>
<div class="callout callout--note">
  <p class="callout__title"><span class="callout__icon" aria-hidden="true">📝</span>Sources</p>
  <div class="callout__body">
<ul>
<li>NXP, <em>i.MX RT1060 Processor Reference Manual</em>, Ethernet MAC (ENET) chapter.</li>
<li>IEEE 802.3bw (100BASE-T1) and IEEE 802.3bu (PoDL).</li>
</ul>

  </div>
</div>

<p>💬 Questions, corrections, or your own war story? Leave a comment below.</p>
]]></content:encoded></item></channel></rss>