Custom Update Module on bare flash: where to store "which bank is live"?

I have a custom Update Module working on mender-mcu, and I want to check if my approach is right.

Setup

NXP MIMXRT1170-EVK, Zephyr 4.2.1, mender-mcu. Two modules:

  • zephyr-image for firmware (not modified)
  • signage-content (mine) for the images on the display

The device is a small SPI display. Firmware update by MCUboot is fine. But content is different. Rewriting 1.5 MB of firmware just to change a price tag is too much. And reboot makes the screen blank while MCUboot checks the RSA signature.

So content has its own artifact type, requires_reboot = false, and its own two flash partitions (5.5 MB each). The update writes the bank that is not displayed. Then it switches. This works well.

The problem on MCU

I read Edge AI Models: Custom Update Modules in Practice. In that article, the module does the switch by itself — cp to backup, then promote staging/ to current/. Mender does not store any state for this.

This works on Linux because:

  • current/ is a fixed name. Nothing needs to remember which copy is live.
  • mv is atomic. So overwriting the live location is safe.

On bare flash, both are missing. There is no rename. To overwrite the live location, I must erase the partition the display is reading. If power fails there, the screen is blank and there is no fallback.

So I need A/B banks. And then I need to remember which bank is live. Linux never has this problem.

What I did

I write a footer in the last sector of each bank: magic, generation counter, frame count. At boot, the bank with the higher generation wins. If the magic does not match, the bank is empty.

Before writing, the download erases the target bank’s footer. So if the download fails, the old bank still has the higher generation and stays on the screen. Writing the footer is the switch. Rollback is free, because the live bank was never touched.

Questions

  1. Is this the expected way on MCU? Or is there a pattern I missed?
  2. For fleet visibility, should I use mender_inventory_add_callback() to report signage_bank=1? Composable Updates explains provides/depends well, but the bank number is decided at runtime on the device. So I cannot put it in the artifact like --software-version. Inventory looks correct, but the server can only see the value. It cannot gate a deployment on it like --depends can.
  3. Is there a plan to expose the provides store to Update Modules on MCU? mender_storage_get_provides() exists in src/include/storage.h, but the public headers are only the seven in include/mender/. And include/mender/update-module.h is the same in main and in the release I use. I am not sure if this is intentional.

If the footer approach is correct, I am happy to write a short note about it. It would have saved me a day.

Thanks.

Hi @hkenken,

Thanks for reaching out! Yes indeed that’s a perfectly valid and very nice use case of Mender MCU.:+1:

On the question of A/B marking: it depends a bit on the actual hardware and peripherals, especially NVS which you have at your disposal. I’m not super familiar with the IMXRT1170, and of course not with your board at all, so I can just provide a few thoughts.

  1. a header/footer in each block as you mentioned, which is erased first and written last should work. Might require careful handling of write syncing, but it’s a good approach.
  2. If you have something like NVRAM or an EEPROM, you could use that as A/B marker. It is not required for the Update Module to use only one piece of storage, so you can put the payload in flash and markers somewhere else.

On visibility, I would add it to inventory, yes. What would you need provides/depends for? It could be used to abstract the API or data format (depending on the direction), but I don’t exactly see how it would relate to the specific current payload bank.

On mender_storage_get_provides(), I’ll ping the developers, don’t know right off the cuff.

Greetz,
Josef

Hi @hkenken,

To follow up on the provides-depends topic, there are two conflated aspects here.

  1. the actual metadata of what is provided (already installed on the device) and required (by an artifact to be installed). This is only defined by the chain or artefacts, meaning the the provides on the device are essentially accumulated over lifetime minus the ones which get cleared by an artifact upon installation. A new artefact can then depend on them to ensure compatibility.
  2. the implementation of how the Mender Client obtains said information. On the Linux incarnation, this usually uses an LMDB, but technically could use whatever other format. It is not considered a public API, so any modification to this state is unsupported. The mender_storage_get_provides() function is on the same level. It’s an internal function to obtain the provides which are persisted on the device, and it is not considered a public API.

Greetz,
Josef

Continuing the discussion from Custom Update Module on bare flash: where to store "which bank is live"?:

Hi Josef-san,

Thanks — your question about provides/depends showed me I had two things mixed together.

The bank number does not belong in provides. I was conflating “the server should be able to see this” with “the server should be able to gate a deployment on it”. The bank number only needs the first, and it is an implementation detail of my module. The data format you mentioned is what actually belongs there — something like signage-content.format=rgb565-v1, so a later artifact can depend on the firmware being able to decode it. That is a real compatibility constraint; the bank number is not.

So:

  • provides / depends — the content format version
  • the module’s own storage — which bank is live
  • inventory — generation and frame count

Your caution about write syncing was well placed. The footer write is the switch, so the whole rollback story rests on that one write landing. I have not checked whether flash_area_write() returning means the bytes are on the FlexSPI NOR. I will read the footer back right after writing it, and separately try cutting power just after the write to see which bank comes up.

Markers elsewhere had not occurred to me. For this board I will stay with the footer: a marker outside the bank means two writes to two places that can disagree, whereas a footer inside the bank cannot get out of step with it. That is my reasoning for this hardware, not a general claim — as you say, it depends on the peripherals.

On mender_storage_get_provides(), no rush. Your second reply told me what I needed: knowing it is deliberately internal is enough, and I will leave it alone.

One more thing — I am writing this up as a section in a Zephyr feature for a Japanese embedded magazine (Interface, CQ Publishing). The contrast with the Linux layout turned out to be a good way to explain why the embedded case needs a different shape: current/ as a fixed name and mv being atomic, neither of which exists on raw flash. I will point readers at this thread.

Thanks for taking the time