Code Designs
Static Memory
It is important to design code modules that are based on static memory allocation as many realtime Embedded Systems project demand this. This means, we cannot use:
Dynamic Memory (heap, malloc)
Linked Lists
Code can be designed for static memory allocation by leaving an initialization function or similar by allowing the user to provide the static memory for you.
typedef struct {
void *memory;
size_t memory_size_bytes;
} buffer_s;
void buffer_initialize(buffer_s *module, void * static_memory, size_t static_memory_size_bytes);
This is similar to FreeRTOS static memory API. Have a look at the following two APIs. The first one will internally allocate memory, whereas the second API allows the user to instantiate memory separately and provide the parameter as input to xQueueCreateStatic
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
UBaseType_t uxItemSize );
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer );
Code Modularization
It is important to draw lines between code functionality to encourage the divide and conquer
strategy. For example, imagine that you are given this problem:
/**
* Random amount of data bytes can arrive
* Objective is to figure out when sizeof(packet)
* is available, then call process_packet();
*/
void handle_data(const void * data, size_t byte_count);
typedef struct {
char data[16];
} packet_s;
void process_packet(const packet_s *packet);
A large number of developers may try to approach the problem straight away without thinking about modularizing the code, so this might be the result:
Hmm... not bad! We met the objective, which is to buffer data, and process all data packets. But wait, "buffer data"? Maybe that should have been a different code module. We sort of solved two problems in one and increased the code complexity, and made it more difficult for the reader to understand what is going on. If we took a step back and thought about how we can split this problem, we will end up with much better code.
Doesn't this code look good!? Observe that there are not even any comments to explain what is going on, and with the prior solution, we had to explain things like Append the data into our buffer
. It is easier to read, and we have removed the awkward and rather dangerous memmove()
stuff into another code module. We have divided the problem into different code modules, and the buffer
module can be created, and tested separately.
The conclusion here is that try to draw lines between different portions of the problem. We should not be trying to solve all problems into a single function and we should use dedicated code modules where necessary.
Information Hiding in C
Well, there is no such thing in C. It is really difficult to hide things while avoiding dynamic memory at the same time. We could sort of do it with forward declaration of a struct:
The problem is that when you forward declare a struct
, the user of your API is unable to declare the buffer_s foo
because the compiler does not know how much memory space this struct has, and therefore we need an explicit buffer_create()
function where this can be hidden.
If we really wanted to make this work, we would have to invent something awkward. As much as I like FreeRTOS, the developers invented this kind of awkwardness:
The idea above is to create a second struct whose size is the same as buffer_s
. As you can see, we are going far too deep to do something that the C language does not facilitate and you should wonder if all that effort is really worth it.
So what is our advise? Just leave things public and do not create messy and awkward code that deteriorates code readability and maintenance. If users really want to access internals of buffer_s
or FreeRTOS QueueHandle_t
then they can even with the great effort taken to avoid it, but users should know what they should and should not access and it is up to them to not mis-use the code.
So, our conclusion is just to do this:
If you have a mix of public and private members in C, you can adopt the following technique:
Although code should be designed to prevent mis-use, sometimes it may not be possible in the C language in particular. So we advise others to write easily comprehensible code, and to document things appropriately. There is no point in totally hiding structures in C because a developer can still mis-use the code even with FreeRTOS style data hiding by simply saying ((uint32_t*)freertos_queue_handle) [3] = 123;
.
Component Specific Design
Component Specific Design is a design pattern to allow a module to encapsulate (and statically allocate) all possible instances of objects. This programming pattern potentially includes all of the following files:
sl_queue.h
sl_queue.c
sl_queue_private.h
sl_queue_component_specific.h
Component Specific Header File
This file does not include any other files
This only defines the enums to be later used by
sl_cantp.h
Header File
The header file is like an ordinary header file, except that instead of the this pointers
, it uses the enumeration type.
Source File
The source file declares the data based on the enumeration count.
Channels - these are used to encapsulate all the configurable data from the user and build that into the module at compile time instead of using pointers and run-time loading of configurations.
Example: In the main header file for CANTP (i.e. sl_cantp.h):
In sl_cantp_component_specific.h
In sl_can_tp__component_specific.c
----
Debugging
Suppose a piece of code has been mostly dormant (stable) for a number of years, but now needs to be changed. You re-enable debugging trace - but it is frustrating to have to debug the debugging (tracing) code because it refers to variables that have been renamed or retyped, during the years of stable maintenance. If the compiler (post pre-processor) always sees the print statement, it ensures that any surrounding changes have not invalidated the diagnostics. If the compiler does not see the print statement, it cannot protect you against your own carelessness (or the carelessness of your colleagues or collaborators).
See the answer to this post:
#define macro for debug printing in C?#define debug_print(fmt, ...) \ do { if (DEBUG) fprintf(stderr, fmt, __VA_ARGS__); } while ( 0 )
It assumes you are using C99 (the variable argument list notation is not supported in earlier versions). The do { ... } while (0) idiom ensures that the code acts like a statement (function call). The unconditional use of the code ensures that the compiler always checks that your debug code is valid — but the optimizer will remove the code when DEBUG is 0.
#define debug_print(...) \ do { if (DEBUG) fprintf(stderr, "%s:%d:%s(): " , __FILE__, \ __LINE__, __func__, __VA_ARGS__); } while ( 0 )
This relies on string concatenation to create a bigger format string than the programmer writes.
#define debug_print(fmt, ...) \ do { if (DEBUG) fprintf(stderr, fmt, ##__VA_ARGS__); } while (0)
Some compilers may offer extensions for other ways of handling variable-length argument lists in macros. Specifically, as first noted in the comments by Hugo Ideler , GCC allows you to omit the comma that would normally appear after the last 'fixed' argument to the macro. It also allows you to use VA_ARGS in the macro replacement text, which deletes the comma preceding the notation if, but only if, the previous token is a comma
This solution retains the benefit of requiring the format argument while accepting optional arguments after the format.
This technique is also supported by Clang for GCC compatibility.
Debug Flags
Rules for Test Code:
For test code, it should only be checked in if it's useful for repeat testing in the future. In this case, it should be guarded by a test flag, so the code is not used in production. The test flag should follow naming convention of DEBUG__<MODULE_NAME>
.
Example: Module defined debug flag:
Guard of test code:
SIBROS TECHNOLOGIES, INC. CONFIDENTIAL