Reputation: 5865
I'm in the process of designing a embedded C data storage module. It will be included by files/modules who want access to this "shared" system-wide data. Multiple tasks aggregate dozens of inputs (GPIO, CAN, I2C/SPI/SSP data, etc) and stores those values off using the API. Then, other tasks can access the data safely through the API. The system is an embedded app with an RTOS, so mutexes are used to protect the data. These ideas will be used regardless of the implementation
I've designed something like this in the past, and I'm trying to improve upon it. I'm currently halfway through a new implementation and I'm running into a few hiccups and would really benefit from a fresh perspective.
Quick rundown of the requirements of this module:
The question is how would you go about designing something like this? Enumerations, structures, accessors, macros, etc? I'm not looking for code here, just discussing general overall design ideas. If there's a solution on the internet that addresses these sorts of things, perhaps even just a link is sufficient.
Upvotes: 7
Views: 5228
Reputation: 1496
I could think of some mixture concepts from AUTOSAR modules, like NVM or even Com. For most of them, the actual type is irrelevant for the operation of this module, its just a number of bytes.
NvM has a statically configured configuration, generated by a tool according to a input definition. Each block is given an ID which is also configurable (e.g. typedef uint16 NvM_BlockIdType). The config also contains the data size, a possible CRC (8,16,32), some infos like used with NvM_ReadAll or NvM_WriteAll (or not), possible init callbacks or init blocks for initializing the RamBlock, NvM Block Type as native, redundant or DataSet...
The Block is handled by NVM usually with one or two queues (Standard Job Queue and Immediate Job Queue). From application side, its just a call of:
Std_ReturnType NvM_ReadBlock(NvM_BlockIdType BlockId, uint8* data);
Std_ReturnType NvM_WriteBlock(NvM_BlockIdType BlockId, uint8* data);
Std_ReturnType NvM_ReadAll(void);
Std_ReturnType NvM_WriteAll(void);
Std_ReturnType NvM_GetErrorStatus(NvM_BlockIdType BlockId, NvM_RequestResultType* res);
Usually Std_ReturnType will return E_OK on an accepted request, E_NOT_OK on some failure, e.g. ID not found, NvM not up...
Regarding some signal handling, like flags or like signals which are not primitive types like uint8, uint16 .. but maybe like uint11, uint4 .. Com actually receives IPDUs, and stores them to buffers. For transmission, it is also copies an IPDU for transmission. On the higher layers, you have Com_SendSignal(uint16 ID, uint8* data) or Com_ReceiveSignal(uint16 ID, uint8* data).
Some implementation just create an big buffer by the size of all IPDUs. Then the have a SignalGroup and Signal configuration by Signal-ID and SignalGroup-ID, which stores the offset into the array as index, plus startbit and bitsize to finally pack/unpack the signal data from/to the pointer to data passed into the functions.
Similar to Com regarding pack/unpack can be seen in SomeIP transformer.
Upvotes: 0
Reputation: 5865
Figured I'd update one of my only unaccepted questions. Here's my final implementation. Been using this for over a year and it works fantastic. Very easy to add variables to and very little overhead for the benefit it gives us.
lib_data.h:
#ifndef __LIB_DATA_H
#define __LIB_DATA_H
#include <type.h>
/****************************************************************************************
* Constant Definitions
***************************************************************************************/
/* Varname, default value (uint32_t) */
#define DATA_LIST \
DM(D_VAR1, 0) \
DM(D_VAR2, 1) \
DM(D_VAR3, 43)
#define DM(y, z) y,
/* create data structure from the macro */
typedef enum {
DATA_LIST
NUM_DATA_VARIABLES
} dataNames_t;
typedef struct {
dataNames_t name;
uint32_t value;
} dataPair_t;
/* the macro has to be undefined to allow the fault list to be reused without being
* defined multiple times
*
* this requires:
* a file specific lint option to suppress the rule for use of #undef
*/
#undef DM
/****************************************************************************************
* Data Prototypes
***************************************************************************************/
/****************************************************************************************
* External Function Prototypes
***************************************************************************************/
/**
* Fetch a machine parameter
*
* \param dataName The variable from DATA_LIST that you want to fetch
*
* \return The value of the requested parameter
*
*/
uint32_t lib_data_Get(dataNames_t dataName);
/**
* Set a machine parameter
*
* \param dataName The variable from DATA_LIST that you want to set
* \param dataVal The value you want to set the variable to
*
* \return void
*
*/
void lib_data_Set(dataNames_t dataName, uint32_t dataVal);
#endif /* __LIB_DATA_H */
lib_data.c:
#include <type.h>
#include "lib_data.h"
/****************************************************************************************
* Variable Declarations
***************************************************************************************/
/* Used to initialize the data array with defaults ##U appends a 'U' to the bare
* integer specified in the DM macro */
#define DM(y, z) \
dataArray[y].name = y; \
dataArray[y].value = z##U;
static bool_t dataInitialized = FALSE;
static dataPair_t dataArray[NUM_DATA_VARIABLES];
/****************************************************************************************
* Private Function Prototypes
***************************************************************************************/
static void lib_data_Init(void);
/****************************************************************************************
* Public Functions
***************************************************************************************/
uint32_t lib_data_Get(dataNames_t dataName) {
if(!dataInitialized) {
lib_data_Init();
}
/* Should only be used on systems that do word-sized asm reads/writes.
* If the lib gets extended to multi-word storage capabilities, a mutex
* is necessary to protect against multi-threaded access */
return dataArray[dataName].value;
}
void lib_data_Set(dataNames_t dataName, uint32_t dataVal) {
if(!dataInitialized) {
lib_data_Init();
}
/* Should only be used on systems that do word-sized asm reads/writes.
* If the lib gets extended to multi-word storage capabilities, a mutex
* is necessary to protect against multi-threaded access */
dataArray[dataName].value = dataVal;
}
/****************************************************************************************
* Private Functions
***************************************************************************************/
/**
* initialize the machine data tables
*
* \param none
*
* \return none
*
*/
static void lib_data_Init(void) {
/* Invoke the macro to initialize dataArray */
DATA_LIST
dataInitialized = TRUE;
}
Upvotes: 0
Reputation: 828
A few approaches I've had experience with and have found each good for its own needs. Just writing down my thoughts on this issue, hope this gives you some ideas you can go with...
(EntryInGroupID = (groupid << 16) | serial_entry_id)
.Most systems I've worked with required some type of configuration module, usually over both volatile and non-volatile memory. It's very easy (and even tempting) to over-design this, or bring an overkill solution where its not needed. I have found that trying to be "future-proof" on these issues will in a lot of cases just be a waste of time, usually the simplest way to go is the best (in real-time and embedded systems anyway). Also, performance issues tend to creep up when you scale the system, and its sometimes to stick with the simpler and faster solution up ahead.
Therefore, given the information you've supplied and the above analysis, I'd recommend you go with the key-value or direct structs approach. I'm a personal fan of the "single task" approach for my systems, but there is really no one absolute answer for this, and it requires deeper analysis. For these solutions, having looked for a "generic" and off-the-shelf solution, I always find myself implementing it myself in 1-2 weeks and saving myself a lot of headaches.
Hope the answer wasn't overkill :-)
Upvotes: 2
Reputation: 683
I usually go for a simple dictionary-like API using an int as the key and a fixed-size value. This executes quickly, uses a very small amount of program RAM and has predictable data RAM usage. In other words, the lowest-level API looks like:
void data_set(uint16 key, uint32 value);
uint32 data_get(uint16 key);
Keys become a list of constants:
#define KEY_BOGOMIPS 1
#define KEY_NERDS_PER_HOUR 2
You handle different data types by casting. Sucks, but you can write macros to make the code a little cleaner:
#define data_get_float(key) (float)data_get(key)
Achieving type safety is difficult to do without writing a separate macro or accessor function for each item. On one project, I needed validation of input data, and this became the type-safety mechanism.
The way you structure the physical storage of data depends how much data memory, program memory, cycles and separate keys you have. If you've got lots of program space, hash the key to get a smaller key that you can look up directly in an array. Usually, I make the underlying storage look like:
struct data_item_t {
uint16 key;
uint32 value;
}
struct data_item_t items[NUM_ITEMS];
and iterate through. For me, this has been fast enough even on very small (8-bit) microcontrollers, though it might not fit for you if you've got a lot of items.
Remember that your compiler will probably inline or optimise the writes nicely, so cycles per access may be lower than you'd expect.
Upvotes: 1
Reputation: 10393
I've been in this situation a couple times myself. Every time I've ended "rolling my own", and I definitely don't suffer from Not Invented Here (NIH) syndrome. Sometimes the space, processing turnaround time or reliability/recoverability requirements just make that the least painful path.
So, rather than writing the great American novel on the topic, I'll just throw out some thoughts here, as your question is pretty broad (but thank you for at least forming a question & providing background).
Is C++ on the table? Inline functions, templates, and some of the Boost libraries might be useful here. But I'm guessing this is straight-up C.
If you're using C99, you can at least use inline functions, which are a step above macros when it comes to type safety.
You might want to think about using several mutexes to protect different parts of the data; even though the updates are quick, you might want to break up the data into sections (e.g. configuration data, init data, error logging data, trace data, etc.) and give each its own mutex, reducing the funnel/choke points.
You could also consider making all access to the data go through a server task. All reads & writes go through an API which communicates with the server task. The server tasks pulls reads & write requests in order from its queue, handles them quickly by writing to a RAM mirror, sending responses if needed (at least for read requests), and then buffers data to NVM in the background, if necessary. Sounds heavyweight compared to simple mutexes but it has its advantages in certain use cases. Don't know enough about your application to know if this is a possibility.
One thing I will say, is the idea of getting/setting by a tag (e.g. maybe a list of enums like CONFIG_DATA, ADDRESS_DATA, etc.) is a huge step forward from directly addressing data (e.g. "give me the 256 bytes at address ox42000). I've seen many many shops suffer great pain when the whole physical-addressing scheme finally breaks down & they need to re-factor / re-design. Try to keep the "what" decoupled from the "how" - clients shouldn't have to know or care where stuff is stored, how big it is, etc. (You might already know all this, sorry if so, I just see it all the time...)
One last thing. You mentioned mutexes. Beware of priority inversion... that can make those "quick accesses" take a very long time in some cases. Most kernel mutex implementations allow you to account for this, but often it's not enabled by default. Again, sorry if this is old news...
Upvotes: 5