Loose-coupling patterns for embedded systems programming [closed]
We don’t allow questions seeking reco开发者_运维百科mmendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 4 years ago.
Improve this questionWhere can I find some good, proven guidelines or examples on writing extensible, modular, loosely-coupled code in C (if possible)?
Background of our problem is that we are maintaining large plain C, legacy code project for a low-cost microcontroller with limited computing and memory resources. Due to the fact that the system must be extremely reliable and the memory is rather limited, one of the first constraints is not to use dynamic memory allocation at all. All structures are mapped statically.
So we are looking for ways to make this code more maintainable and more modular. We are not interested in coding standards, but rather design suggestions. We have good coding conventions (naming, organizing code, SVN) so this is not a problem.
From what I've seen on the web (I may be wrong), it seems most of the programmers which program exclusively in plain C or assembler, at least in the uC/Embedded community, restrain from using anything more that plain procedural programming.
For example, we could get most of the OOP benefits and decoupling in plain C using callback functions, structs containing function pointers and similar stuff (it wouldn't require dynamic allocation, just passing around pointers to structs), but we would like to see if there are some proven methods already around.
Do you know of such resources, or have similar suggestions besides from "why don't you switch to C++ or other programming language"?
[Edit]
Thanks a lot for all the answers, I haven't had the time to examine them yet. Platform is 16-bit (XC166 or similar) uC, naked hw (no RTOS).
We're in a similar situation. To address these concerns, we've implemented a build system that supports multiple implementations of desired interfaces (which implementation used is a function of the compilation target), and avoid use of API features that aren't included in the portable wrappers. The wrapper definition lives in a .h file that #include
's the implementation-specific header file. The following mock-up demonstrates how we might handle a semaphore interface:
#ifndef __SCHEDULER_H
#define __SCHEDULER_H
/*! \addtogroup semaphore Routines for working with semaphores.
* @{
*/
/* impl/impl_scheduler.h gets copied into place before any file using
* this interface gets compiled. */
#include "impl/impl_scheduler.h"
/* semaphore operation return values */
typedef enum _semaphoreErr_e
{
SEMAPHORE_OK = impl_SEMAPHORE_OK,
SEMAPHORE_TIMEOUT = impl_SEMAPHORE_TIMEOUT
} semaphoreErr_e;
/*! public data type - clients always use the semaphore_t type. */
typedef impl_semaphore_t semaphore_t;
/*! create a semaphore. */
inline semaphore_t *semaphoreCreate(int InitialValue) {
return impl_semaphoreCreate(InitialValue);
}
/*! block on a semaphore. */
inline semaphoreErr_e semaphorePend(semaphore_t *Sem, int Timeout) {
return impl_semaphorePend(Sem, Timeout);
}
/*! Allow another client to take a semaphore. */
inline void semaphorePost(semaphore_t *Sem) {
impl_semaphorePost(Sem);
}
/*! @} */
#endif
The public API is documented for use, and the implementation is hidden until compilation time. Using these wrappers also should not impose any overhead (though it might, depending on your compiler). There is a lot of purely mechanical typing involved, though.
You may want to have a look at the xDAIS algorithm standard. It was designed for DSP applications but the ideas can be adjusted to low resource embedded designs as well.
http://en.wikipedia.org/wiki/XDAIS_algorithms
In a nutshell: xDAIS is an OOP style interface convention not unlike COM for the C language. You have a fixed set of interfaces that a module may implement via a structure of function pointers.
The interfaces are strictly defined, so it is very easy to exchange components, stack them together to build higher level functionality and so on. The interfaces (and a code-checker) also make sure that all components remain separated. If the code-checker is used it is impossible to write a component that directly calls other components private functions.
Memory allocation is usually done at initialization time and under the control of the system designer (it's part of the main interface that all components must implement).
Static and dynamic allocation strategies are possible. You can even go dynamic without the risk of memory fragmentation because all components must be able to relocate themselves to different memory addresses.
The xDAIS standard defines a very lean OOP style mechanism for inheritance. This comes very handy for debugging and logging purposes. Having an algorithm that does funny things? Just add a simple single-file wrapper around an existing algorithm and log all calls to an UART or so. Due to the strictly defined interface there is no guesswork how a module works and how parameters are passed.
I've used xDAIS it in the past, and it works well. It takes a while to get used to it, but the benefits of the plug-and-play architecture and ease of debugging outweigh the initial effort.
I will try to begin an answer here. If anything else comes to mind, I will get back here, because this problem is interesting. I will also monitor this question for other answers.
Separate logic and execution:
Embedded systems can benefit from the same kind of separation of logic and I/O as large business applications.
If, for example you are coding for some embedded device that reads values, interprets these and alter something based on these readings, you might want to separate the "logic" part completely from the part where you actually communicate with the network, the hardware, the user or whatever external entity.
When you can describe the "rules" completely in some kind of memory structure or C code, without linking to anything but the message passing routines or similar, you have what I try to describe. In short, reducing side effects makes the code more modular.
I don't know if you are using threads or not, but either way proto threads offers a similar abstraction, less powerful than threads, but also a lot less likely to confuse the programmer.
Growing up on the Amiga, I have a hard time forgetting it. A ROM-able operating system, yet easily extended in RAM by loadable libraries. Heavy use of pointer passing made for both tight code and fast messages.
Reading another answer from Nils Pipenbrinck, his suggestion to use xDAIS looks to be a good (but far from only) way of implementing this. If most of your code is using some message convention like this, chances are your code is modular and maintainable.
I would also bring up running preprocessor or precompiler passes on the code before compiling for the target, but then we drift into a gray area ... this is almost like switching languages, and the requirement was C.
What you mention about imitating OOP is a good practice. More than knowing a standard I can give you an idea on how I do it. I am actually using it taking care of some details:
- Hiding the internal module structures.
- Exposing the structure sizes to the application to allow static allocation.
- Exposing a callback interface to borrow functionality from other modules.
- Keeping each module compilable by itself.
- Last but not least: keep the code easy to read, easy to use, and easy to modify.
@my_module.c
typedef struct _s_class
{
uint32_t an_attribute;
void (*required_behavior)(uint32_t);
} class_t;
void obj_init(void * obj, void(*req_beh_callback)(uint32_t))
{
((class_t*)obj)->an_attribute = 0;
((class_t*)obj)->required_behavior = req_beh_callback;
}
void obj_method1(void* obj)
{
((class_t*)obj)->an_attribute++;
required_behavior(((class_t*)obj)->an_attribute);
}
size_t get_object_size()
{
return sizeof(class_t);
}
@my_module.h
void obj_init(void * obj, void(*req_beh_callback)(uint32_t));
void obj_method1(void* obj);
size_t get_object_size();
// run get_object_size() once to get the number
// that goes in this macro. may differ between CPU
// architectures.
#define OBJECT_SIZE 4
@my_application.c
#include "my_module.h"
uint8_t my_object[OBJECT_SIZE]; // static allocation :)
void callback_for_obj(uint32_t i)
{
... do stuff ...
}
int main()
{
obj_init(my_object, callback_for_obj);
obj_method1(my_object);
return 0;
}
Let me know if you have either suggestions or questions as they also help me learn more!
We are not using many small devices, but we do have some with memory constraints. We are allocating static buffers, but we found that sometimes dynamic memory allocation actually helps reducing the memory usage. We tightly control the heap size and allocation policy and have to check and handle out of memory conditions not as errors but as normal operation. E.g. we are out of memory, so we send out the data that we have, clear the buffers and resume operations where we left of.
Why do we not switch to C++? I would love to. We don't switch for mainly these reasons:
- Our code monkeys would not grok it, and are reluctant to learn.
- The C++ libraries are significantly larger (although we may be able to work around this one.)
- For those really small RTOS devices it's usually not necessary. For bigger devices, where we are running an embedded Linux, it would be nice.
You'd better be very sure that a fixed layout is what you want! Tearing it down and putting in a dynamic one could get very tricky!
I suggest the problems that any embedded framework are trying to manage are:
Calculating offsets to data
It should be possible to create a single struct for all memory but this *so* doesn't feel like the right way to do it. C compilers are not usually asked to work with multi-megabyte structures and I get the feeling doing this is not very portable between compilers.
If structures are not used, then five sets of defines are needed, based on what is essentially a data schema:
- sizes of simple types
- offsets of fields within group types
- sizes of group types
- offsets of groups in runs of groups
- size of runs of groups
- (possibly also absolute addresses of runs of groups if performance dictates)
These defines have a tree-like dependency tree, that rapidly gets very complicated in raw C because types usually have to be packed/aligned eg in 4-byte clumps for optimal performance. The fully expanded defines can quickly end up more complex than some compilers are happy to process.
The easiest way to manage these issues in raw C projects is to calculate the offsets with a C program that is a "build tool" executable of the project and import them into the project as a .h file that contains explicit opffset numbers. With this approach a single macro taking a base address and relevant indices should be made available during the main compile for accessing each leaf of the data structure.
Avoiding function pointer corruption and maximising debugging productivity
If function pointers are stored in the object they are more vulnerable to data corruption leading to mysterious errors. The better way (for those memory ranges that contain different object types from time to time) is to store a vtable code in the object which is a lookup index into a set of function pointer sets.
The vtables can again be computed and generated as a .h file with #defines by a generator C program that is an executable "build tool".
A special constructor macro is required to write in the appropriate vtable id to initialise usage of an object.
Both of these problems are effectively well-solved already by for example the objective-C preprocessor (which will output raw C), but you can do it from scratch if you want to stay with a very small set of tools.
Allocating memory blocks to resources/tasks in the static memory structure
If you need to support multi-threading, associating short-lived dynamic tasks with particular indexes in the tree structure (nearest thing to allocating an object in the equivalent procedural/OO program) is perhaps best performed by trial-locking an arbitrary index, using for example an atomic increment (from zero with ==1 check) or mutex, then checking to see if the block is available, and if so, marking it as used, then unlocking the block.
If multi-threading support is not required then this is unnecessary; I suggest writing a custom framework for managing such resource allocation processes which can be run in either a multi-threaded or single-threaded mode, to allow the rest of the code base to be unconcerned with this topic, and to allow faster performance on single-threaded systems.
精彩评论