The code looked correct.
The Flash erase and program routines were marked with a .ramfunc attribute. The linker script placed the section in SRAM. Startup code copied it from the load image before main(). On paper, the firmware could keep executing while embedded Flash was busy.
Then I inspected the final ELF.
One SRAM-resident routine still called a compiler division helper located in Flash.
That single call was enough to break the safety argument.
Why Flash-IAP Code Moves to SRAM
Many microcontrollers cannot reliably fetch instructions from Flash while the same Flash bank is being erased or programmed. Depending on the device, a read may stall, return invalid data, or fault until the controller becomes ready again.
A common design is therefore:
- store the Flash-IAP routine in the firmware image;
- copy it into SRAM during startup;
- execute the SRAM copy during erase or program operations.
The source often looks reassuring:
__attribute__((section(".ramfunc")))
int flash_program(uint32_t address, const uint8_t *data, uint32_t length)
{
/* unlock, program, wait, verify */
}
But the attribute answers only one question:
Where was this function’s own body linked?
It says nothing about its callees, constants, compiler-generated helpers, interrupt handlers, or recovery path.
VMA, LMA, and the Startup Copy
An SRAM function normally has two addresses:
- LMA — Load Memory Address: where its bytes live in the Flash image;
- VMA — Virtual Memory Address: where the CPU executes those bytes in SRAM.
A simplified GNU linker layout might be:
.ramfunc :
{
_sramfunc = .;
KEEP(*(.ramfunc))
KEEP(*(.ramfunc.*))
_eramfunc = .;
} > SRAM AT > FLASH
_siramfunc = LOADADDR(.ramfunc);
Startup must then copy the complete range:
uint8_t *src = &_siramfunc;
uint8_t *dst = &_sramfunc;
while (dst < &_eramfunc) {
*dst++ = *src++;
}
Three checks are necessary:
- the input section name matches the source attribute;
- the output section has an SRAM VMA and Flash LMA;
- startup copies the exact linker-defined range before any IAP call.
If any one of these is wrong, the source annotation is decorative.
.ramfunc load bytes"] START["Startup copy
_siramfunc to _sramfunc"] SRAM["Executable copy in SRAM
.ramfunc VMA"] IAP["Flash erase/program
Flash temporarily unavailable"] IMG --> START --> SRAM --> IAP
The Real Requirement Is a Call Closure
During the interval when Flash cannot be read, every instruction and every required datum on the active execution path must remain accessible.
For a Flash-IAP routine, the closure may include:
- unlock and status helpers;
- address or length validation;
- checksum or verification functions;
memcpy,memset, or comparison routines;- division and modulo helpers emitted by the compiler;
- timeout functions and their timer reads;
- error cleanup;
- any interrupt handler that remains enabled;
- lookup tables and
constdata.
In the case I reviewed, integer division in a .ramfunc routine became a call to a runtime helper similar to __aeabi_uidiv. The compiler was correct to emit it. The linker was correct to place the helper with normal code in Flash. The source still looked harmless.
Only the linked binary exposed the problem.
Why Source Review Is Not Enough
Optimization level, target architecture, compiler version, and operand types can all change code generation. A division may be optimized into shifts in one build and become a library call in another.
That is why I inspect the final artifact:
arm-none-eabi-objdump -d firmware.elf > firmware.dis
arm-none-eabi-nm -n firmware.elf > firmware.sym
arm-none-eabi-readelf -S firmware.elf
The exact tool prefix depends on the architecture, but the questions stay the same:
- What address range contains
.ramfunc? - Does every direct branch remain inside SRAM or a safe ROM region?
- Are there indirect calls?
- Where do compiler helpers resolve?
- Does the routine load constants from Flash?
- Can an enabled interrupt vector into Flash during the busy window?
A map file is helpful for section placement. Disassembly is decisive for actual calls.
Interrupts Are Part of the Execution Path
Many IAP routines disable interrupts before modifying Flash:
uint32_t state = save_and_disable_interrupts();
/* modify Flash */
restore_interrupts(state);
That can be valid, but it creates two additional obligations.
First, the routine must restore the original interrupt state on every exit path. Returning early after a controller error while leaving interrupts disabled can turn a recoverable programming failure into a frozen system.
Second, the busy wait must be bounded.
This is dangerous:
while (flash_is_busy()) {
}
If the controller never clears its busy flag, the CPU waits forever with interrupts disabled. A robust implementation needs:
- a timeout source that remains readable;
- timeout code that is also safe while Flash is unavailable;
- controller reset or cleanup where supported;
- restoration of interrupt and lock state;
- an error that reaches a diagnosable layer.
Moving an infinite loop into SRAM does not make it safe. It merely makes the infinite loop executable.
Reads and Verification Need the Same Scrutiny
Erase and program functions receive most of the attention, but verification can be equally subtle.
Ask:
- Is Flash readable immediately after the busy flag clears?
- Does verification call a normal library comparison in Flash?
- Is cache invalidation required?
- Are alignment and width constraints respected?
- Can power loss leave metadata claiming that an incomplete image is valid?
For bootloaders, the problem expands from one function to an update protocol. Image validity markers, rollback behavior, reset timing, and power-failure recovery matter as much as the low-level write loop.
My Verification Checklist
Before calling a Flash-IAP path SRAM-safe, I now check:
Source and linker
- all intended entry points use the correct section attribute;
- the linker keeps every required
.ramfunc*input section; - VMA lies in executable SRAM;
- LMA lies in the load image;
- section boundaries are exported.
Startup
- the complete range is copied before use;
- byte/word copy alignment is valid;
- startup does not skip the copy in an alternate boot path.
Final ELF
- section addresses match the intended memory map;
- every erase/program call path is disassembled;
- callees and compiler helpers are in safe memory;
- required constants are not stranded in unavailable Flash.
Runtime safety
- interrupts are intentionally handled;
- busy loops have bounded timeouts;
- all failure paths restore controller and CPU state;
- invalid address and length inputs fail before Flash is unlocked;
- hardware tests cover success, controller error, timeout, and reset.
The Attribute Is a Claim, Not Evidence
.ramfunc is useful. It expresses intent and gives the linker a place to collect special code.
But it does not prove that the code can execute while Flash is unavailable.
The proof lives in the complete chain:
source attribute → linker placement → startup copy → final ELF call graph → bounded runtime behavior → hardware evidence
Skip any link in that chain and a perfectly annotated function can still jump back into the memory it is busy erasing.
