How to write layered code in C for embedded devices
03:55 26 May 2026

I'm struggling to implement data persistence in non-volatile memory in a modular, reusable and layered way.

In my application I have (I think I have) 3 types of data to persist:

  • system configuration (stored "once", changed rarely);

  • runtime settings - e.g. sound level, brightness level, whatever (changed occasionally);

  • runtime data - previous calculation to compare (in case of power off), logs (changed periodically, quite often);

To address XY problem

I'm using ESP32 platform with FreeRTOS. Writing C code.

What I want: I want to use abstractions for non-volatile memory storage so that my modules don't know anything about persistence, they just know that they can call "save" to save and "load" to get earlier saved data. Each module could use different memory target.

Why I want: I want to simulate my firmware code on my host machine, to test and develop without uploading the firmware on my target device. Also I believe that each data type could require different memory type/chip because of write frequency and volume of data.

My current thoughts

There are several options for the memory: Flash (internal, external), EEPROM, SD, etc. If a memory is external I suppose it'll use some serial interface to read/write data.
ESP32 SDK provides NVS library to work with internal Flash. Also there is LittleFS port for esp.

I think we could divide data into 3 categories:

  • CurrentValueType - like a variable, holds only the last specified value;

  • OrderedDataType - like an array, holding values, has a notion of order/index (5 previous values);

  • StackDataType - like an array, holding values, but no order/index (logs, append each line);

Every module should be able to store data of arbitrary size/structure.
Pseudo-code interface could look like this:

persist(char * data_tag, uint8_t * data, size_t size);

So we have data name/label which is meaningful for that specific module and data (which could be a struct defined in the module or a primitive value).

To avoid data name collisions between different modules there should be something like namespace. Namespace could be assigned at a compile time for each "storage" which is passed to a module as constructor argument (DI) so that the module doesn't know anything about partitions/namespaces/memory chips.

As I see it, for each data value I need a physical region in a memory to store it. The size of the region should be large enough to hold the value. There is a trade-off: to allocate just enough (or a little bit more) space and every time write data to this space/address — but if using Flash it could reach write cycles limit; therefore it's better to allocate large block and use some wear-leveling mechanism (write in different parts of the region) whether it's a self-made algorithm or some kind of a file-system implementation (3d party library).

Also I can't wrap my head around the fact that the firmware should be aware of the hardware in use. Therefore for every platform (target or host simulation) I need to properly initialize "hardware" in a specific main file than call application code.

Questions I don't know the answer yet

#1 Is it ok to mix settings and log data in one abstraction?
Even if I have separate system_config_settings module which acts upon CurrentValueType, there should also be a persistent mechanism for logs and ordered data somewhere. Seems like data-stream storing abstraction would be the same for all types? Should it be like an append flag for the data-stream abstraction?

#2 What abstractions should be in the system?
I see it like:

--NAMED DATA LAYER INTERFACE--
persist(char * data_tag, uint8_t * data, size_t size); // implicitly holds the namespace assigned at the system bootstrap 
|
V
--DATA STREAM LAYER INTERFACE--
write(void *address?, uint8_t * data, size_t len); // how do we know data_tag <=> address (whatever it is)?
read(void *address?, uint8_t * data, size_t max_len);
|
V
[
-- SERIAL COMM LAYER INTERFACE (for external memory chips)-- // not all implementations would use a serial interface (e.g. Windows simulation doesn't need it), so it's more an implementation detail
write(...)
read(...)
]

#3 I don't understand how and where (in the layers) to convert data name and namespace to the "physical" address where to write/read data. For example, on Windows simulation it could be a filename and a directory name. So what abstraction should handle this?

#4 I believe that for each memory chip (or virtual memory for Windows) I'll have a *.c implementation which knows the interface (command set) of the relevant chip and communicates to it to actually store/read the data. Where on the layer diagram does it sit? Does each *.c implementation implements DATA STREAM LAYER or is it an additional layer/interface?

#5 If I use a wear-leveling mechanism (self-made, or 3d party filesystem lib) should it be like a middleware after DATA STREAM LAYER?
So it gets data stream and decides where to write it "physically"? Or the filesystem implementation will replace a memory chip implementation?

#6 How to put it on the C syntax?
I believe there should be structs for each abstraction which hold internal/meta data (and methods) to execute logic, and this structs should be passed to every method? If I want static memory allocation this structs couldn't be opaque, I mean that struct structure would be visible publicly? So it's just a convention not to touch this fields?
I would like to use DI "constructor" injection (I believe using xxx_init method for each module), so that I could wire up everything in my main.c and pass it to modules as dependencies. Something like:

// esp32-platform.c
int specific_platform_main(void) {
  struct i2c_config i2c_config = {
    .port_num = I2C_NUM_0,
    .some_implementation_specific_data = SOME_VALUE,
  };
  i2c_init(&i2c_config); // platform hardware implementation
  struct serial_api {
    void *serial_handle;
    int (*read)(...); // public method for clients
    int (*write)(...); // public method for clients
  } serial_api = {
    .serial_handle = &i2c_config,
    .read = i2c_read, // is called with i2c_config: read(&i2c_config, ...) 
    .write = i2c_write, // is called with i2c_config: write(&i2c_config, ...)
  };
  
  struct mem_chip_imp_config mem_chip_config = {
    // ...
    .serial = &serial_api,
  };
  mem_chip_init(&mem_chip_config);

  // it should somehow pass structs to the application function?
}

// application.c
int run_app(void) {
  specific_platform_main(); // initializes specific storage hardware which will be used in the storage abstraction in business logic modules.

  my_algo_module_init({
    .persist = {
      .save = WHAT?,
      .load = WHAT?,
    },
  });
}

#7 Do I need a separate FreeRTOS task for handling persistent requests?
Communicating with external chip could take some time blocked? Is it supposed ok for a module to call save data method and block while the called method communicates with a memory chip by i2c (if there is no a dedicated persistent task)?

---

In spite of the fact that the question is asked about persistence storage I'd be glad to hear what is the correct model of thinking to approach such problems? How one should think defining abstractions and layers regarding the embedded development (I think it's not very far from the general software development approach, is it?) and mostly important how one should think to implement it in C language. Can't seamlessly transfer my OOP knowledge to C unfortunately.

c design-patterns architecture embedded esp32