Getting a board to blink is the easy 10 %. The hard part is everything between “it boots on my desk” and “it can leave the building”: 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.
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’t.
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.
1. The board boots on its own, then dies behind the bootloader
The mental model: the boot ROM was quietly doing your setup for you.
A stand-alone application image on the i.MX RT works because the boot ROM reads the image’s boot header and runs its DCD (Device Configuration Data). That is where external SDRAM gets configured. You never have to think about it.
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 .bss clear in SDRAM ends in a bus fault, long before main().
The fix has two parts:
- Move the SDRAM setup (the DCD) into the bootloader, which the ROM still boots normally.
- Make the application skip the clock (PLL) re-initialisation, because the bootloader has already done it.
A related trap: in MCUboot’s direct-XIP-with-revert mode, a slot without the image trailer’s magic value is erased at reset. The symptom is the application disappears after a power cycle. Sign the image with the trailer included.
What to check on your own board
main().2. Measure the chip, not the documents
The mental model: the flash size is a measurement, not a setting.
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.
Then make the answer structural, so it cannot drift again:
- One header owns the flash partition map.
- A start-up assert checks it against the linker symbols.
- A host test parses the linker scripts, so any drift fails in CI rather than on a customer’s device.
The map matters most once you plan field updates. A dual-slot layout needs room for two complete images, next to the bootloader and the data partition, and images grow over a product’s life.
| Budget question | What to do |
|---|---|
| Which build do I size against? | The worst case: the debug build with tracing, not the release build. |
| Does reflashing prove the layout? | No. Debug probes often skip unchanged sectors, so a reflash test can prove nothing. |
| What changes above 16 MiB? | 4-byte addressing. On the i.MX RT it has to be set consistently in the FlexSPI configuration block, the flash driver’s commands (also used for XIP reads) and the driver’s size limit. |
3. Writing the flash you execute from
The mental model: while the flash is busy, it does not exist.
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), nothing may be fetched from it: not the interrupt handler, not the vector table, not a helper function the compiler decided not to inline.
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.
What fixes it:
- Run the erase and program routines, and everything they call, from RAM.
- 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.
- Invalidate the instruction cache afterwards. The Cortex-M7 fetches speculatively and can have pulled in stale or garbage lines during the write.
An unknown opcode fails silently
4. Treat the field update as a protocol
The mental model: every update request ends in exactly one outcome, and the new version number is the only proof.
A device that reports “update done” 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:
- Every request ends in exactly one outcome: success, a classified failure, or a timeout. Never silence, never two answers.
- 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.
- Retries must fit the device’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.
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.
5. Production-ready means surviving the power cut
The mental model: measure the hold-up time before you design the software around it.
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.
That number decides the software design. If the worst-case flash erase (lesson 3) is longer than the hold-up time, the emergency path must never start a flash operation. It only finishes or abandons what is already running, and closes the file system.
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.
Test the test
Summary
- Own what the ROM did for you. Once a bootloader sits in front, it has to set up memory and clocks.
- Measure, then make it structural. Flash size, step timings and hold-up time are measurements; a header, an assert and a CI test keep them true.
- Design for the worst case. The flash write path runs from RAM, updates end in exactly one outcome, and the power-fail path never starts an erase.
Sources
- NXP, i.MX RT1060 Processor Reference Manual: boot ROM, IVT/DCD, FlexSPI.
- MCUboot documentation: image trailer and the direct-XIP / revert modes.
- The datasheet of your QSPI NOR, for its typical and maximum program and erase times.
💬 Questions, corrections, or your own war story? Leave a comment below.
💬 Discussion
Questions, corrections, or war stories from your own board? Leave a comment below — I read and answer all of them. (Sign-in with GitHub.)