Virtually every embedded system needs storage for critical non-volatile data. There are many devices to choose from (TF/SD cards, NAND/NOR flash, …); automotive electronics most commonly use EEPROM and flash.
This article first covers the upper-layer NvM service (configuration and API), then the implementation of its flash backend Fee (Flash EEPROM Emulation).
Using an EEPROM is straightforward. In general both flash and EEPROM require an erase before writing (some modern EEPROMs allow byte-wise writes without erase). The smallest erasable unit of an EEPROM is typically 8-32 bytes, while a flash sector is much larger (512 bytes or more) and is always erased as a whole, which makes EEPROM-based software considerably simpler.
Take the common automotive mileage example: the total odometer takes 4 bytes, the trip meter 2 bytes, plus a 2-byte checksum - 8 bytes in total. On an EEPROM you might statically allocate 8 minimum erasable units starting at address 0 and rotate writes over them: on every cold start the software picks the maximum odometer value as the current one and therefore knows where the next write goes. In other words, EEPROM usage is typically “one or more fixed-address slots per data item”.
Flash is different. Its erase granularity is too large; some MCUs have only a handful of flash blocks and erasing a block wipes the whole block, so the EEPROM-style slot scheme is impractical. Instead, flash is used to emulate EEPROM, which in AUTOSAR is the job of the Fee (Flash EEPROM Emulation) module. (Some MCUs advertise an on-chip EEPROM and note that it is flash-emulated - usually the vendor simply implemented an algorithm like the one described here.)
NvM (NVRAM Manager) sits between the application and Fee/Ea and manages non-volatile data in units of “blocks”: it maps logical blocks onto the underlying Fee (flash emulation) or Ea (real EEPROM abstraction) target and takes care of defaults, CRC and read/write job scheduling.
An NvM configuration example for DTC storage (complete file: NvM.json):
{
"class": "NvM",
"target": "Fee",
"blocks": [
{
"name": "Dem_NvmEventStatusRecord{}",
"repeat": 8,
"NumberOfWriteCycles": 100000,
"data": [
{ "name": "status", "type": "uint8", "default": "0x50" },
{ "name": "testFailedCounter", "type": "uint8", "default": 0 }
]
}
]
}
Top-level attributes:
| Attribute | Description |
|---|---|
class |
Fixed value "NvM" |
target |
Storage target: "Fee" (flash emulation) or "Ea" (EEPROM abstraction) |
blocks |
List of logical blocks |
Block-level attributes:
| Attribute | Description |
|---|---|
name |
Block name; when used with repeat it must end with {} (generates Record0 … Record7) |
repeat |
Optional, number of block instances |
NumberOfWriteCycles |
Optional, maximum writes over the block’s lifetime, default 10,000,000; used for the life-cycle calculation |
data |
List of data elements inside the block |
Data element attributes:
| Attribute | Description |
|---|---|
name |
Element name; arrays end with {} |
repeat |
Optional, number of copies of the element |
type |
Scalars: int8/int16/int32/uint8/uint16/uint32/uint64; arrays: <type>_n together with size, e.g. uint8_n |
size |
Number of array elements |
default |
Default value, evaluated as a Python expression ("0x50", "[0xFF]*13", …); used to initialize the ROM defaults |
The NvM.py generator reads the JSON and emits the block configuration C code (per-block size, write-cycle limit, ROM defaults), expanding repeat into concrete instance names.
NvM jobs are asynchronous: an API call only queues the request, NvM_MainFunction drives the processing, and the outcome is polled with NvM_GetErrorStatus (see NvM.h):
void NvM_Init(const NvM_ConfigType *ConfigPtr);
Std_ReturnType NvM_ReadBlock(NvM_BlockIdType BlockId, void *NvM_DstPtr); /* returns ROM default if invalid */
Std_ReturnType NvM_WriteBlock(NvM_BlockIdType BlockId, const void *NvM_SrcPtr);
Std_ReturnType NvM_RestoreBlockDefaults(NvM_BlockIdType BlockId, void *NvM_DstPtr);
Std_ReturnType NvM_EraseNvBlock(NvM_BlockIdType BlockId);
Std_ReturnType NvM_InvalidateNvBlock(NvM_BlockIdType BlockId);
Std_ReturnType NvM_SetRamBlockStatus(NvM_BlockIdType BlockId, boolean BlockChanged);
Std_ReturnType NvM_GetErrorStatus(NvM_BlockIdType BlockId, NvM_RequestResultType *RequestResultPtr);
void NvM_ReadAll(void); /* bulk read of all blocks at startup */
void NvM_WriteAll(void); /* bulk write-back of all blocks before shutdown */
void NvM_FirstInitAll(void);
void NvM_MainFunction(void); /* must be called periodically */
The implementation lives in infras/memory/Fee. The basic principle is shown below:

The figure shows EEPROM emulation with 2 flash blocks; at any time one block is idle (the scheme works the same with 3 or more blocks). When the system is fresh, both blocks are empty and the software starts with BANK0. Because the block is empty, it is trivial to locate the bottom of the next ID field and the top of the next DATA field:
Group 1 in Fig. 1 shows the state after writing block 0, 1, 2 and then block 0 again: storage is allocated dynamically in write order, and on a cold start the latest valid copy of every block is found through its ID field. The ID area grows upward and the DATA area grows downward toward the middle; since allocation is dynamic, no space is wasted even though the data sizes differ.
Group 2: when the two areas meet and BANK0 runs out of space, the software compacts (backs up) the newest copy of every block into BANK1; as shown in Group 3, BANK0 is then erased and becomes the spare for the next swap when BANK1 fills up.
This FEE implementation uses the in-house factory library to split the complex flow into explicit state machines; the steps of each state are defined in factory.json. FEE has 4 working states (4 state machines):
Initialization traverses the admin area of every FEE bank to determine the active bank (the bank currently used for reading and writing). With sudden power loss in mind, it must also check whether the active bank has enough free space for new data; if not, it enters the backup flow.

The Fee_BankAdminType layout in Fee_Priv.h consists of three parts:
High: | Full Magic | ~ Full Magic | <- Status -\
| Number | ~ Number | <- Info + <- Bank Admin
Low: | FEE Magic | ~ FEE Magic | <- Header -/
"FEEF" in code);FEE_MAX_ERASED_NUMBER, default 1,000,000) the bank is end-of-life;0xFFFFFFFF); when the bank runs out of space and a backup starts, the full marker (ASCII "DEAD", see FEE_BANK_FULL_MAGIC) is written.The three parts live in three different pages so each can be written independently without corrupting the others on power loss.
Initialization steps (the Init machine in factory.json):
Fls_BlankCheck on the admin Info to determine its state; if blank, treat it as FLS_ERASED_VALUE;BlankCheckBlock: run Fls_BlankCheck on the first block (page) to determine whether the bank is empty; if blank, treat the block as FLS_ERASED_VALUE.
Fls_BlankCheckis optional. It exists for flashes whose erased state is not reliablyFLS_ERASED_VALUE(0xFF), e.g. TC387, so that valid data can be distinguished from a freshly erased state.
FEE_MIN_FREE_SPACE), start a backup; otherwise initialization is done.The Read machine has just two nodes, ReadData and SearchNext:
FLS_DIRECT_ACCESS it is a direct memory access). Each record carries a CRC16 and its bitwise inverse at the tail; the inverse is checked first and the CRC16 is then recomputed. Only when both pass is the data copied to the caller;default) is returned and the job completes successfully.The Write machine has three nodes, WriteCheckDataChanged / WriteAdmin / WriteData:
FEE_BLOCK_ADMIN_AND_DATA_SIZE). If yes, write the block admin (BlockNumber, …) from the ID side. If not, trigger the Backup machine to compact/swap banks and then continue;
Backup machine nodes (14 nodes in factory.json):
FLS_ERASED_VALUE;FULL_MAGIC (“DEAD”) to mark the current bank as full;SetNextBankAdmin: write a valid admin into the next bank.
Steps 7-8 cover power loss during backup: after a reset there is no way to tell how far the copy progressed, and since power loss is rare the implementation keeps things simple by erasing and starting over.
Vehicle applications usually require 10+ years of data retention, so the bank erase count under a given FEE configuration must stay below the flash endurance limit (typically 100k to 1M cycles).
FeeLifeCycle.py computes the worst-case backup (erase) rounds from the block sizes and NumberOfWriteCycles in NvM.json:
# basic usage
python tools/utils/memory/FeeLifeCycle.py app/app/config/NvM/NvM.json
# verbose output
python tools/utils/memory/FeeLifeCycle.py app/app/config/NvM/NvM.json -v
# custom bank parameters
python tools/utils/memory/FeeLifeCycle.py app/app/config/NvM/NvM.json --block_size "32*1024" --num_of_banks 4 -v
Parameters:
| Parameter | Description | Default |
|---|---|---|
config |
Path to NvM.json | required |
-v/--verbose |
Print the detailed calculation | off |
--block_size |
Bank size (expressions allowed) | 32*1024 (32 KB) |
--page_size |
Flash page size in bytes | 8 |
--num_of_banks |
Number of banks | 2 |
The tool estimates the worst-case backup rounds from two angles and takes the maximum:
Practical advice:
NumberOfWriteCycles to the real requirement (expected writes per day x 365 x lifetime in years x a safety factor of 2-10); do not blindly keep the 10-million default;