Toby Toby - 12 days ago 5x
C Question

Common interface pattern for writing struct members to memory?

I am working with an embedded device (no OS) that has some external non-volatile memory.

I am storing multiple copies of a single struct's data in the NV memory. I read and write to the data in memory as whole structs and as individual members.

Currently I have a working implementation. When the module that deals with the memory operations gets initialised it is given the location of the struct that is in RAM, which it stores as part of its context. This means that when it comes to read and write single members it can calculate the offset of the member by subtracting the previously mentioned location from the member's location. Then it uses this offset to know where to find the copy in the NV memory.

e.g. in psuedo-code:

wm_ram = pointer to working memory struct start in RAM;

write to a member from the member in RAM:
offset = working memory member location in RAM - wm_ram;
sector address = address of selected copy of struct in NV to write to;
write data from working memory member location to NV sector @ start +
offset + sector address;

(The struct type has several nested structs, so I think offsetof() is out here)

This results in a pretty simple and familiar feeling function signature for the common operations:

int write_or_read(*handle, copyNumber, *data, size);

This is fine until I want to also store a second struct from RAM. This means there are now two addresses in RAM that the memory module needs to keep track of to correctly calculate offsets, plus some added decision work to decide which to use

Thus I reasoned that the struct RAM start location should be passed in at the time of the memory operations rather than being held as part of the memory module's context.

This starts to get a bit ungainly though:

int write_or_read(*handle, copyNumber, *data, *data_struct_start, size);

And so my question is: is there any convention or pattern that is often or commonly used for similar or analogous cases, hopefully such that would also result in simple and familiar operation function signatures?

EDIT: for further explanation
The NV memory is a simple external device, similar to an EEPROM, with 2KB of memory. My structs contain multiple data types including other structs and total around 600B in size each. The device does not have buffering; it's actually FRAM, so it's fairly fast, but I have little overhead so I'd like not to have to write the whole struct for just one member.

The picture shows that I currently have a struct in RAM with multiple copies in NV memory. If I want to read or write to a specific member of a specific copy then I have the address of the member in RAM by simply taking the address of the member (
) but what address should I use in NV? the RAM address don't line up with the addresses in NV (e.g. address 0x300 in RAM != 0x300 in NV) so I need some other way of determining that.

enter image description here.

If a read or write function is told to write a copy of
to struct_1 in NV then what byte address does it need to write to?

Maybe I'm incorrect in trying to keep the interface this simple?


Don't use structures for serialization.

Instead, define your setting variables in clear text data files (CSV, JSON, XML, ...):

Foo, int32,      120
Bar, float,     40.0 

Then convert that data file to source files using the scripting language of your choice. You can determine needed size from the types, and calculate NV memory offsets automatically to an table.

One of the files that your script spits out is a list of ID's:

typedef enum {
} ID_T;

And API you offer is simple:

int32_t ReadInt32(ID_T id);
float ReadFloat(ID_T id);
void WriteInt32(ID_T id, int32_t value);
void WriteFloat(ID_T id, float value);

There are several advantages to this:

  • All dirty details of offsets and addresses are hidden from other code.
  • Now you can easily extend this, add for example min/max/description fields.
  • You can easily change to format of the data tables by changing the script, while your API stays the same.
  • If you later need additional features, like ability to read/write settings through serial port, settings are listed in one place.
  • If you add additional field for pointer to data, you can update the actual RAM value automatically, when you call write functions.
  • You can create other script to generate documentation (pdf/doc/etc.) of the settings