嵌入式C数据存储模块设计

时间:2021-07-25 16:14:00

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

我正在设计一个嵌入式C数据存储模块。它将被希望访问这个“共享”系统范围数据的文件/模块所包含。多个任务聚合数十个输入(GPIO、CAN、I2C/SPI/SSP数据等),并使用API存储这些值。然后,其他任务可以通过API安全地访问数据。该系统是一个带有rto的嵌入式应用程序,因此使用互斥体来保护数据。不管实现如何,这些想法都将被使用

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:

快速概述本模块的需求:

  • Ideally, there would be one interface that can access the variables (one get, one set).
  • 理想情况下,有一个接口可以访问变量(一个get,一个set)。
  • I'd like to return different variable types (floats, ints, etc). This means macros are probably needed.
  • 我想返回不同的变量类型(浮动、int等)。这意味着可能需要宏。
  • I'm not pressed for code space, but it's always a concern
  • 我并不需要代码空间,但这始终是个问题
  • Quick gets/sets are absolutely paramount (which means storing in strings ala xml/json is out)
  • 快速获取/设置绝对是最重要的(这意味着将xml/json存储在字符串ala中)
  • No new variables need to be added during runtime. Everything is statically defined at boot

    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.

    问题是你如何设计这样的东西?枚举、结构、访问器、宏等等?我不是在这里寻找代码,只是讨论总体设计思想。如果互联网上有一个解决这些问题的解决方案,也许仅仅一个链接就足够了。

  • 4 个解决方案

    #1


    5  

    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.

    我自己也经历过好几次这种情况。每次我结束自己的“滚我自己”的时候,我绝对不会因为没有发明(NIH)综合症而受苦。有时,空间、处理周转时间或可靠性/可恢复性需求只是使这条路最不痛苦。

    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.

    c++在讨论中吗?内联函数、模板和一些Boost库在这里可能很有用。但我猜这是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.

    如果您正在使用C99,那么至少可以使用内联函数,在类型安全方面,这比宏更重要。

    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.

    您可能想要考虑使用几个互斥对象来保护数据的不同部分;尽管更新速度很快,但是您可能想要将数据分解成部分(例如,配置数据、init数据、错误日志记录数据、跟踪数据等),并给每个部分提供自己的互斥量,从而减少了漏斗/阻塞点。

    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.

    您还可以考虑通过服务器任务对数据进行所有访问。所有的读写都通过与服务器任务通信的API进行。服务器任务按顺序从队列中提取读写请求,通过写入RAM镜像快速处理请求,在需要时发送响应(至少对于读取请求),然后在必要时将数据缓存在后台的NVM中。与简单的互斥体相比,它听起来是重量级的,但是在某些用例中它有它的优势。不知道你的申请是否有可能。

    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...)

    我要说的一件事是,通过标签来获取/设置(例如,可能有一个枚举列表,比如CONFIG_DATA、ADDRESS_DATA等),这是直接处理数据的一大步。“给我地址ox42000的256字节)。”当整个物理寻址方案最终失败时,我看到许多商店遭受了巨大的痛苦&他们需要重构/重新设计。试着将“什么”与“如何”解耦——客户不应该知道或关心存储在哪里,它有多大,等等(如果是这样,你可能已经知道所有这些了,我只是一直看到它)。

    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...

    最后一件事。你提到的互斥锁。谨防优先级反转……在某些情况下,这种“快速访问”需要很长时间。大多数内核互斥锁实现都允许您对此进行解释,但默认情况下通常不启用它。再说一遍,对不起,如果这是旧消息……

    #2


    2  

    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...

    有一些方法我已经有了经验,并且已经发现每种方法都适合自己的需要。写下我对这个问题的看法,希望这能给你一些建议。

    • For more complex data with lots of dependencies and constraints, I found a higher-level module is usually preferable, even when coming in the expense of speed. It saves you a lot of headache, and its usually the way to go in my experience, unless you have really tough constraints. The thing is to store most of your "static" data, that doesn't change a lot, in this type of module, and benefit from the ease of use and the more "complex" features (e.g. Data Validation, Referential Integrity, Querying, Transactions, Privileges etc). However, if speed is crucial in some areas, you might want to keep it out of this module altogether, or implement some simple "mirroring" scheme (where you could for example periodically synchronize back and forth between your lightweight mirrors and main configuration DB on some idle task). Having some experience with this issue on embedded systems, I have found sqlite to be the easiest to work with. Seeing your comments, I can testify the porting is not that big of a deal if you have the C standard library supported for your platform. I have ported the system to a PowerPC architecture in about a week, and we had pure in-house proprietary code. It was mostly a matter of getting the OS abstraction layer figured out and was smooth sailing from there. I highly recommend sqlite for this.
    • 对于具有大量依赖关系和约束的更复杂的数据,我发现更高级的模块通常更可取,即使是以牺牲速度为代价。这可以省去你很多麻烦,而且通常是按照我的经验去做,除非你有非常严格的限制。在这种类型的模块中,需要存储大多数“静态”数据,这些数据不会有太大的变化,并且可以从易于使用和更“复杂”的特性(例如,数据验证、引用完整性、查询、事务、特权等)中获益。但是,如果速度在某些领域非常重要,您可能希望将它完全排除在这个模块之外,或者实现一些简单的“镜像”方案(例如,您可以在轻量级镜像和一些空闲任务的主配置DB之间定期同步)。我在嵌入式系统上有一些经验,我发现sqlite是最容易使用的。看到您的评论,我可以证明,如果您有支持您的平台的C标准库,那么移植并不是什么大不了的事情。我在大约一周内将系统移植到PowerPC架构中,并且我们有纯内部私有代码。这主要是解决操作系统抽象层的问题,并从那里开始顺利进行。我强烈推荐sqlite。
    • If the above is still too heavy (either in terms of performance, or perhaps it is just overkill), you can get most of the important benefits from using a basic key-value system. The idea is similar to what was mentioned here before: define your keys via enum's, store your data in a table of same-sized (e.g. 64 bits) values, and let each variable reside in one cell. I find this to work for simpler projects, with not THAT many types of configuration entries, while you can keep your flexibility and the system is still easy to develop and use. A few pointers for this:
      • Divide your enum's into groups or categories, each with its own value range. A scheme I like is to define the groups themselves in one enum, and use those values for starting off the first value of each of the groups (EntryInGroupID = (groupid << 16) | serial_entry_id).
      • 将enum划分为组或类别,每个组都有自己的值范围。我喜欢的方案是在一个enum中定义组本身,并使用这些值启动每个组的第一个值(EntryInGroupID = (groupid < 16) | serial_entry_id)。
      • It's easy to implement "tables" using this scheme (where you have more than one entry for each need or configuration, e.g. routing tables). You can generalize the design by making anything a "table", and if you want to define a single value, just make it a table with one entry. In embedded systems I would recommend allocating everything up-front, according to the maximum number of entries possible for that table, and "using" part of it. That way you've covered the worst case and are being deterministic.
      • 使用此方案很容易实现“表”(其中每个需求或配置有多个条目,例如路由表)。您可以通过将任何内容创建为“表”来概括设计,如果您想定义单个值,只需将其创建为具有一个条目的表。在嵌入式系统中,我建议根据该表的最大条目数预先分配所有内容,并“使用”其中的一部分。这样你就能覆盖最坏的情况并且具有确定性。
      • Another thing I found very useful was using Hooks for everything. These hooks can be implemented using Function Pointers (for each behavior / policy / operation), which are given through a "descriptor" struct which is used to define the data entry. Usually, your entries will use the "default" hook functions for read/write (e.g. direct memory access, or file access), and NULL-out the other possible hooks (e.g. "validate", "default value", "notify for change" and such). Once in a while, when you have a more complicated case, you can take benefit of the hooks system you've planted in with little development cost. This hooks system can inherently provides you with a "hardware abstraction layer" for the storage of the values themselves, and can access data from memory, registers, or files with the same ease. A "notify for changes" register/unregister facility can also really help for notifying tasks about configuration changes, and is not that hard to add.
      • 我发现的另一个非常有用的东西是所有东西都使用钩子。这些钩子可以使用函数指针(针对每个行为/策略/操作)实现,这些指针通过一个用来定义数据条目的“描述符”结构来实现。通常,您的条目将使用“默认”钩子函数进行读/写(例如,直接内存访问或文件访问),并取消其他可能的钩子(例如)。“验证”、“默认值”、“通知更改”等)。偶尔,当您遇到更复杂的情况时,您可以利用您所安装的hook系统,而开发成本很低。这个钩子系统可以为值本身的存储提供一个“硬件抽象层”,并且可以同样轻松地从内存、寄存器或文件中访问数据。“通知更改”注册/取消注册功能也可以帮助通知任务有关配置更改,而且添加起来并不难。
    • 如果上面的内容仍然太过沉重(无论是性能方面还是性能方面),您可以从使用基本键值系统中获得大部分重要的好处。这个想法与前面提到的类似:通过enum定义密钥,将数据存储在一个具有相同大小(例如64位)值的表中,并让每个变量驻留在一个计算单元中。我发现这是为了更简单的项目而工作,没有那么多类型的配置条目,而您可以保持灵活性,并且系统仍然很容易开发和使用。这里有一些提示:将枚举划分为组或类别,每个类别都有自己的值范围。我喜欢的方案是在一个enum中定义组本身,并使用这些值启动每个组的第一个值(EntryInGroupID = (groupid < 16) | serial_entry_id)。使用此方案很容易实现“表”(其中每个需求或配置有多个条目,例如路由表)。您可以通过将任何内容创建为“表”来概括设计,如果您想定义单个值,只需将其创建为具有一个条目的表。在嵌入式系统中,我建议根据该表的最大条目数预先分配所有内容,并“使用”其中的一部分。这样你就能覆盖最坏的情况并且具有确定性。我发现的另一个非常有用的东西是所有东西都使用钩子。这些钩子可以使用函数指针(针对每个行为/策略/操作)实现,这些指针通过一个用来定义数据条目的“描述符”结构来实现。通常,您的条目将使用“默认”钩子函数进行读/写(例如,直接内存访问或文件访问),并取消其他可能的钩子(例如)。“验证”、“默认值”、“通知更改”等)。偶尔,当您遇到更复杂的情况时,您可以利用您所安装的hook系统,而开发成本很低。这个钩子系统可以为值本身的存储提供一个“硬件抽象层”,并且可以同样轻松地从内存、寄存器或文件中访问数据。“通知更改”注册/取消注册功能也可以帮助通知任务有关配置更改,而且添加起来并不难。
    • For ultra-real-time needs, I would just recommend you keep it simple and save your data in structs. Its pretty much the most lightweight way you can go, and it usually handles all your needs. However, it is hard to handle concurrency in this system, so you would either have to wrap all access with mutexes like you suggested, or just keep a mirror for every "task". If this is indeed ultra-real-time, I would go for the mirroring approach with periodical sync's throughout the system). Both this system and the one suggested above make it super easy to serialize your data (as it is always stored in binary format), which will ease things down the road and just keep things simple (all you need for the serialization is a memcpy with a cast or two). Also, using unions from time to time might help, and some optimizers handle it well, however it really depends on the architecture and toolchain you're working with.
    • 对于超实时需求,我建议您保持简单,并将数据保存在struct中。这几乎是你能做到的最轻量级的方式,而且它通常能满足你的所有需求。但是,在这个系统中很难处理并发性,所以您必须像您建议的那样使用互斥体包装所有访问,或者为每个“任务”保留一个镜像。如果这确实是超实时的,我将采用全系统同步的镜像方法)。这个系统和上面建议的系统都使序列化数据非常容易(因为数据总是以二进制格式存储),这将使事情变得更容易,并使事情保持简单(序列化所需的所有内容都是带有一两个cast的memcpy)。此外,不时地使用联合可能会有所帮助,一些优化器可以很好地处理它,但是这实际上取决于您使用的架构和工具链。
    • If you're really worried about having to upgrade your data for new schemas, you might want to consider an "upgrader module", which can be configured with several schemas, and will be used to convert old formats to the new when invoked. You can use code generation techniques from XML schemas to generate the database itself (e.g. into structs and enums), and take advantage of XML tools to perform the "conversion code", by writing XSLT or just plain python-like scripts. You might want to perform the conversion off-target (e.g. at the host), with a more high-level script, rather than cramming that logic into the target itself.
    • 如果您真的担心必须为新模式升级数据,您可能需要考虑一个“升级模块”,它可以配置多个模式,并在调用时用于将旧格式转换为新格式。您可以使用来自XML模式的代码生成技术来生成数据库本身(例如,生成结构和枚举),并利用XML工具来执行“转换代码”,编写XSLT或类似于python的普通脚本。您可能希望使用更高级的脚本执行非目标的转换(例如在主机上),而不是将该逻辑硬塞到目标本身中。
    • For Data Type Safety you can use lots of template and meta-programming wizardry. While tough to maintain as the framework coder, it eases things up for the users of the system. If coded right (which is tricky!), it can save you lots of cycles and not impose a lot of code overhead (with inlining, explicit template instantiation and much care). If your compiler isn't all that good (which is usually the case with embedded platform compilers), just use wrapper macros for conversion.
    • 为了数据类型安全,您可以使用许多模板和元编程技巧。虽然作为框架编码器很难维护,但是对于系统的用户来说,它可以简化工作。如果代码是正确的(这很棘手!),它可以节省大量的周期,并且不会带来大量的代码开销(内联、显式模板实例化和非常小心)。如果您的编译器不是很好(通常是嵌入式平台编译器的情况),只需使用包装器宏进行转换。
    • About the concurrency issue, I generally find myself resorting to one of the following methods, depending of the overall design of the system
      • Like you suggested, wrap the whole thing with a mutex and hope for the best. You can get more precision by adding per-table or even per-entry locks, but this has its own set of problems and I would recommend just keeping it simple and stick to a global mutex. If you have replications for the hard real-time stuff, this probably won't cripple your performance since you only do the sync once in a while.
      • 就像你建议的那样,用互斥锁将整个东西包起来,然后期待最好的结果。您可以通过添加每个表甚至每个条目的锁来获得更精确的结果,但是这有它自己的一组问题,我建议保持它的简单性并坚持使用全局互斥锁。如果您有硬实时的副本,这可能不会影响您的性能,因为您只是偶尔执行一次同步。
      • Design your database as a "single task", and wrap all access to it via messages (MQ's, sockets, whatever). This works better for the non-critical-path data (again), and is an easy solution. If there aren't too much simultaneous accesses going on, this might be the way to go. Note that if you're in the same memory range and have shared memory, the performance of reads/writes isn't that expensive (since you aren't copying data), and if you play with your task's priorities right, it can be a simple solution that will solve most of your needs (by avoiding the problem of concurrent access altogether).
      • 将数据库设计为“单个任务”,并通过消息(MQ、套接字等)包装对它的所有访问。这对于非临界路径数据(同样)来说工作得更好,而且是一个简单的解决方案。如果没有太多的同时访问发生,这可能是正确的方法。请注意,如果你在相同的内存范围和共享内存,读/写性能的不是昂贵的(因为你没有复制数据),如果你玩你的任务的优先级,它可以是一个简单的解决方案,将解决你的需要(通过完全避免并发访问的问题)。
    • 关于并发问题,我通常发现自己求助于以下方法之一,根据您所建议的系统的总体设计,使用互斥锁来包装整个系统,并希望得到最好的结果。您可以通过添加每个表甚至每个条目的锁来获得更精确的结果,但是这有它自己的一组问题,我建议保持它的简单性并坚持使用全局互斥锁。如果您有硬实时的副本,这可能不会影响您的性能,因为您只是偶尔执行一次同步。将数据库设计为“单个任务”,并通过消息(MQ、套接字等)包装对它的所有访问。这对于非临界路径数据(同样)来说工作得更好,而且是一个简单的解决方案。如果没有太多的同时访问发生,这可能是正确的方法。请注意,如果你在相同的内存范围和共享内存,读/写性能的不是昂贵的(因为你没有复制数据),如果你玩你的任务的优先级,它可以是一个简单的解决方案,将解决你的需要(通过完全避免并发访问的问题)。

    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.

    因此,根据您提供的信息和上面的分析,我建议您使用键值或直接结构方法。我个人非常喜欢我的系统的“单一任务”方法,但实际上没有一个绝对的答案,这需要更深入的分析。对于这些解决方案,在寻找“通用的”和现成的解决方案之后,我总是发现我自己在1-2周内实现了它,并为自己省去了很多麻烦。

    Hope the answer wasn't overkill :-)

    希望你的回答不过分:

    #3


    1  

    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:

    我通常使用一个简单的类词典的API,使用int作为键和固定大小的值。它执行速度很快,使用非常少的程序RAM,并且具有可预测的数据RAM使用情况。换句话说,最低级API是:

    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.

    和遍历。对我来说,即使在很小(8位)的微控制器上,这个速度也足够快了,不过如果您有很多项,它可能不适合您。

    Remember that your compiler will probably inline or optimise the writes nicely, so cycles per access may be lower than you'd expect.

    请记住,您的编译器可能会内联或优化写操作,因此每次访问的周期可能比您预期的要低。

    #4


    0  

    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:

    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:

    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;
    }
    

    #1


    5  

    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.

    我自己也经历过好几次这种情况。每次我结束自己的“滚我自己”的时候,我绝对不会因为没有发明(NIH)综合症而受苦。有时,空间、处理周转时间或可靠性/可恢复性需求只是使这条路最不痛苦。

    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.

    c++在讨论中吗?内联函数、模板和一些Boost库在这里可能很有用。但我猜这是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.

    如果您正在使用C99,那么至少可以使用内联函数,在类型安全方面,这比宏更重要。

    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.

    您可能想要考虑使用几个互斥对象来保护数据的不同部分;尽管更新速度很快,但是您可能想要将数据分解成部分(例如,配置数据、init数据、错误日志记录数据、跟踪数据等),并给每个部分提供自己的互斥量,从而减少了漏斗/阻塞点。

    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.

    您还可以考虑通过服务器任务对数据进行所有访问。所有的读写都通过与服务器任务通信的API进行。服务器任务按顺序从队列中提取读写请求,通过写入RAM镜像快速处理请求,在需要时发送响应(至少对于读取请求),然后在必要时将数据缓存在后台的NVM中。与简单的互斥体相比,它听起来是重量级的,但是在某些用例中它有它的优势。不知道你的申请是否有可能。

    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...)

    我要说的一件事是,通过标签来获取/设置(例如,可能有一个枚举列表,比如CONFIG_DATA、ADDRESS_DATA等),这是直接处理数据的一大步。“给我地址ox42000的256字节)。”当整个物理寻址方案最终失败时,我看到许多商店遭受了巨大的痛苦&他们需要重构/重新设计。试着将“什么”与“如何”解耦——客户不应该知道或关心存储在哪里,它有多大,等等(如果是这样,你可能已经知道所有这些了,我只是一直看到它)。

    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...

    最后一件事。你提到的互斥锁。谨防优先级反转……在某些情况下,这种“快速访问”需要很长时间。大多数内核互斥锁实现都允许您对此进行解释,但默认情况下通常不启用它。再说一遍,对不起,如果这是旧消息……

    #2


    2  

    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...

    有一些方法我已经有了经验,并且已经发现每种方法都适合自己的需要。写下我对这个问题的看法,希望这能给你一些建议。

    • For more complex data with lots of dependencies and constraints, I found a higher-level module is usually preferable, even when coming in the expense of speed. It saves you a lot of headache, and its usually the way to go in my experience, unless you have really tough constraints. The thing is to store most of your "static" data, that doesn't change a lot, in this type of module, and benefit from the ease of use and the more "complex" features (e.g. Data Validation, Referential Integrity, Querying, Transactions, Privileges etc). However, if speed is crucial in some areas, you might want to keep it out of this module altogether, or implement some simple "mirroring" scheme (where you could for example periodically synchronize back and forth between your lightweight mirrors and main configuration DB on some idle task). Having some experience with this issue on embedded systems, I have found sqlite to be the easiest to work with. Seeing your comments, I can testify the porting is not that big of a deal if you have the C standard library supported for your platform. I have ported the system to a PowerPC architecture in about a week, and we had pure in-house proprietary code. It was mostly a matter of getting the OS abstraction layer figured out and was smooth sailing from there. I highly recommend sqlite for this.
    • 对于具有大量依赖关系和约束的更复杂的数据,我发现更高级的模块通常更可取,即使是以牺牲速度为代价。这可以省去你很多麻烦,而且通常是按照我的经验去做,除非你有非常严格的限制。在这种类型的模块中,需要存储大多数“静态”数据,这些数据不会有太大的变化,并且可以从易于使用和更“复杂”的特性(例如,数据验证、引用完整性、查询、事务、特权等)中获益。但是,如果速度在某些领域非常重要,您可能希望将它完全排除在这个模块之外,或者实现一些简单的“镜像”方案(例如,您可以在轻量级镜像和一些空闲任务的主配置DB之间定期同步)。我在嵌入式系统上有一些经验,我发现sqlite是最容易使用的。看到您的评论,我可以证明,如果您有支持您的平台的C标准库,那么移植并不是什么大不了的事情。我在大约一周内将系统移植到PowerPC架构中,并且我们有纯内部私有代码。这主要是解决操作系统抽象层的问题,并从那里开始顺利进行。我强烈推荐sqlite。
    • If the above is still too heavy (either in terms of performance, or perhaps it is just overkill), you can get most of the important benefits from using a basic key-value system. The idea is similar to what was mentioned here before: define your keys via enum's, store your data in a table of same-sized (e.g. 64 bits) values, and let each variable reside in one cell. I find this to work for simpler projects, with not THAT many types of configuration entries, while you can keep your flexibility and the system is still easy to develop and use. A few pointers for this:
      • Divide your enum's into groups or categories, each with its own value range. A scheme I like is to define the groups themselves in one enum, and use those values for starting off the first value of each of the groups (EntryInGroupID = (groupid << 16) | serial_entry_id).
      • 将enum划分为组或类别,每个组都有自己的值范围。我喜欢的方案是在一个enum中定义组本身,并使用这些值启动每个组的第一个值(EntryInGroupID = (groupid < 16) | serial_entry_id)。
      • It's easy to implement "tables" using this scheme (where you have more than one entry for each need or configuration, e.g. routing tables). You can generalize the design by making anything a "table", and if you want to define a single value, just make it a table with one entry. In embedded systems I would recommend allocating everything up-front, according to the maximum number of entries possible for that table, and "using" part of it. That way you've covered the worst case and are being deterministic.
      • 使用此方案很容易实现“表”(其中每个需求或配置有多个条目,例如路由表)。您可以通过将任何内容创建为“表”来概括设计,如果您想定义单个值,只需将其创建为具有一个条目的表。在嵌入式系统中,我建议根据该表的最大条目数预先分配所有内容,并“使用”其中的一部分。这样你就能覆盖最坏的情况并且具有确定性。
      • Another thing I found very useful was using Hooks for everything. These hooks can be implemented using Function Pointers (for each behavior / policy / operation), which are given through a "descriptor" struct which is used to define the data entry. Usually, your entries will use the "default" hook functions for read/write (e.g. direct memory access, or file access), and NULL-out the other possible hooks (e.g. "validate", "default value", "notify for change" and such). Once in a while, when you have a more complicated case, you can take benefit of the hooks system you've planted in with little development cost. This hooks system can inherently provides you with a "hardware abstraction layer" for the storage of the values themselves, and can access data from memory, registers, or files with the same ease. A "notify for changes" register/unregister facility can also really help for notifying tasks about configuration changes, and is not that hard to add.
      • 我发现的另一个非常有用的东西是所有东西都使用钩子。这些钩子可以使用函数指针(针对每个行为/策略/操作)实现,这些指针通过一个用来定义数据条目的“描述符”结构来实现。通常,您的条目将使用“默认”钩子函数进行读/写(例如,直接内存访问或文件访问),并取消其他可能的钩子(例如)。“验证”、“默认值”、“通知更改”等)。偶尔,当您遇到更复杂的情况时,您可以利用您所安装的hook系统,而开发成本很低。这个钩子系统可以为值本身的存储提供一个“硬件抽象层”,并且可以同样轻松地从内存、寄存器或文件中访问数据。“通知更改”注册/取消注册功能也可以帮助通知任务有关配置更改,而且添加起来并不难。
    • 如果上面的内容仍然太过沉重(无论是性能方面还是性能方面),您可以从使用基本键值系统中获得大部分重要的好处。这个想法与前面提到的类似:通过enum定义密钥,将数据存储在一个具有相同大小(例如64位)值的表中,并让每个变量驻留在一个计算单元中。我发现这是为了更简单的项目而工作,没有那么多类型的配置条目,而您可以保持灵活性,并且系统仍然很容易开发和使用。这里有一些提示:将枚举划分为组或类别,每个类别都有自己的值范围。我喜欢的方案是在一个enum中定义组本身,并使用这些值启动每个组的第一个值(EntryInGroupID = (groupid < 16) | serial_entry_id)。使用此方案很容易实现“表”(其中每个需求或配置有多个条目,例如路由表)。您可以通过将任何内容创建为“表”来概括设计,如果您想定义单个值,只需将其创建为具有一个条目的表。在嵌入式系统中,我建议根据该表的最大条目数预先分配所有内容,并“使用”其中的一部分。这样你就能覆盖最坏的情况并且具有确定性。我发现的另一个非常有用的东西是所有东西都使用钩子。这些钩子可以使用函数指针(针对每个行为/策略/操作)实现,这些指针通过一个用来定义数据条目的“描述符”结构来实现。通常,您的条目将使用“默认”钩子函数进行读/写(例如,直接内存访问或文件访问),并取消其他可能的钩子(例如)。“验证”、“默认值”、“通知更改”等)。偶尔,当您遇到更复杂的情况时,您可以利用您所安装的hook系统,而开发成本很低。这个钩子系统可以为值本身的存储提供一个“硬件抽象层”,并且可以同样轻松地从内存、寄存器或文件中访问数据。“通知更改”注册/取消注册功能也可以帮助通知任务有关配置更改,而且添加起来并不难。
    • For ultra-real-time needs, I would just recommend you keep it simple and save your data in structs. Its pretty much the most lightweight way you can go, and it usually handles all your needs. However, it is hard to handle concurrency in this system, so you would either have to wrap all access with mutexes like you suggested, or just keep a mirror for every "task". If this is indeed ultra-real-time, I would go for the mirroring approach with periodical sync's throughout the system). Both this system and the one suggested above make it super easy to serialize your data (as it is always stored in binary format), which will ease things down the road and just keep things simple (all you need for the serialization is a memcpy with a cast or two). Also, using unions from time to time might help, and some optimizers handle it well, however it really depends on the architecture and toolchain you're working with.
    • 对于超实时需求,我建议您保持简单,并将数据保存在struct中。这几乎是你能做到的最轻量级的方式,而且它通常能满足你的所有需求。但是,在这个系统中很难处理并发性,所以您必须像您建议的那样使用互斥体包装所有访问,或者为每个“任务”保留一个镜像。如果这确实是超实时的,我将采用全系统同步的镜像方法)。这个系统和上面建议的系统都使序列化数据非常容易(因为数据总是以二进制格式存储),这将使事情变得更容易,并使事情保持简单(序列化所需的所有内容都是带有一两个cast的memcpy)。此外,不时地使用联合可能会有所帮助,一些优化器可以很好地处理它,但是这实际上取决于您使用的架构和工具链。
    • If you're really worried about having to upgrade your data for new schemas, you might want to consider an "upgrader module", which can be configured with several schemas, and will be used to convert old formats to the new when invoked. You can use code generation techniques from XML schemas to generate the database itself (e.g. into structs and enums), and take advantage of XML tools to perform the "conversion code", by writing XSLT or just plain python-like scripts. You might want to perform the conversion off-target (e.g. at the host), with a more high-level script, rather than cramming that logic into the target itself.
    • 如果您真的担心必须为新模式升级数据,您可能需要考虑一个“升级模块”,它可以配置多个模式,并在调用时用于将旧格式转换为新格式。您可以使用来自XML模式的代码生成技术来生成数据库本身(例如,生成结构和枚举),并利用XML工具来执行“转换代码”,编写XSLT或类似于python的普通脚本。您可能希望使用更高级的脚本执行非目标的转换(例如在主机上),而不是将该逻辑硬塞到目标本身中。
    • For Data Type Safety you can use lots of template and meta-programming wizardry. While tough to maintain as the framework coder, it eases things up for the users of the system. If coded right (which is tricky!), it can save you lots of cycles and not impose a lot of code overhead (with inlining, explicit template instantiation and much care). If your compiler isn't all that good (which is usually the case with embedded platform compilers), just use wrapper macros for conversion.
    • 为了数据类型安全,您可以使用许多模板和元编程技巧。虽然作为框架编码器很难维护,但是对于系统的用户来说,它可以简化工作。如果代码是正确的(这很棘手!),它可以节省大量的周期,并且不会带来大量的代码开销(内联、显式模板实例化和非常小心)。如果您的编译器不是很好(通常是嵌入式平台编译器的情况),只需使用包装器宏进行转换。
    • About the concurrency issue, I generally find myself resorting to one of the following methods, depending of the overall design of the system
      • Like you suggested, wrap the whole thing with a mutex and hope for the best. You can get more precision by adding per-table or even per-entry locks, but this has its own set of problems and I would recommend just keeping it simple and stick to a global mutex. If you have replications for the hard real-time stuff, this probably won't cripple your performance since you only do the sync once in a while.
      • 就像你建议的那样,用互斥锁将整个东西包起来,然后期待最好的结果。您可以通过添加每个表甚至每个条目的锁来获得更精确的结果,但是这有它自己的一组问题,我建议保持它的简单性并坚持使用全局互斥锁。如果您有硬实时的副本,这可能不会影响您的性能,因为您只是偶尔执行一次同步。
      • Design your database as a "single task", and wrap all access to it via messages (MQ's, sockets, whatever). This works better for the non-critical-path data (again), and is an easy solution. If there aren't too much simultaneous accesses going on, this might be the way to go. Note that if you're in the same memory range and have shared memory, the performance of reads/writes isn't that expensive (since you aren't copying data), and if you play with your task's priorities right, it can be a simple solution that will solve most of your needs (by avoiding the problem of concurrent access altogether).
      • 将数据库设计为“单个任务”,并通过消息(MQ、套接字等)包装对它的所有访问。这对于非临界路径数据(同样)来说工作得更好,而且是一个简单的解决方案。如果没有太多的同时访问发生,这可能是正确的方法。请注意,如果你在相同的内存范围和共享内存,读/写性能的不是昂贵的(因为你没有复制数据),如果你玩你的任务的优先级,它可以是一个简单的解决方案,将解决你的需要(通过完全避免并发访问的问题)。
    • 关于并发问题,我通常发现自己求助于以下方法之一,根据您所建议的系统的总体设计,使用互斥锁来包装整个系统,并希望得到最好的结果。您可以通过添加每个表甚至每个条目的锁来获得更精确的结果,但是这有它自己的一组问题,我建议保持它的简单性并坚持使用全局互斥锁。如果您有硬实时的副本,这可能不会影响您的性能,因为您只是偶尔执行一次同步。将数据库设计为“单个任务”,并通过消息(MQ、套接字等)包装对它的所有访问。这对于非临界路径数据(同样)来说工作得更好,而且是一个简单的解决方案。如果没有太多的同时访问发生,这可能是正确的方法。请注意,如果你在相同的内存范围和共享内存,读/写性能的不是昂贵的(因为你没有复制数据),如果你玩你的任务的优先级,它可以是一个简单的解决方案,将解决你的需要(通过完全避免并发访问的问题)。

    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.

    因此,根据您提供的信息和上面的分析,我建议您使用键值或直接结构方法。我个人非常喜欢我的系统的“单一任务”方法,但实际上没有一个绝对的答案,这需要更深入的分析。对于这些解决方案,在寻找“通用的”和现成的解决方案之后,我总是发现我自己在1-2周内实现了它,并为自己省去了很多麻烦。

    Hope the answer wasn't overkill :-)

    希望你的回答不过分:

    #3


    1  

    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:

    我通常使用一个简单的类词典的API,使用int作为键和固定大小的值。它执行速度很快,使用非常少的程序RAM,并且具有可预测的数据RAM使用情况。换句话说,最低级API是:

    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.

    和遍历。对我来说,即使在很小(8位)的微控制器上,这个速度也足够快了,不过如果您有很多项,它可能不适合您。

    Remember that your compiler will probably inline or optimise the writes nicely, so cycles per access may be lower than you'd expect.

    请记住,您的编译器可能会内联或优化写操作,因此每次访问的周期可能比您预期的要低。

    #4


    0  

    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:

    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:

    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;
    }