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.

1 2 3 4 5 6 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

1 2 3 4 5 6 7 8 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:

1 2 3 4 5 6 7 8 9 10 11 12 /** * 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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 char large_buffer[512]; size_t length = 0; // Sample code; ignore any corner cases like buffer overflow (for now) void handle_data(const void * data, size_t byte_count) { // Append the data into our buffer memcpy(large_buffer + length, data, byte_count); length += byte_count; while (length >= sizeof(packet_s)) { packet_s *packet = (packet_s*) large_buffer; process_packet(packet); // Discard the memory we processed memmove(large_buffer, large_buffer + sizeof(packet_s), length - sizeof(packet_s)); length -= sizeof(packet_s); } }

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.

1 2 3 4 5 6 7 8 void handle_data(const void * data, size_t byte_count) { buffer__add(&buffer, data, byte_count); while (buffer__bytes_available(&buffer) >= sizeof(packet_s)) { packet_s packet; buffer__read(&buffer, &packet, sizeof(packet)); process_packet(&packet); } }

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:

1 2 3 4 5 6 7 8 9 10 11 // @file buffer.h // Look, we totally made things private because the user does not know members of the struct typedef struct buffer_s; void buffer_push(buffer_s *module, ...); void buffer_pop(buffer_s *module, ...); // But unfortunately, we have to construct the buffer and have to be a 'factory' for construction buffer_s* buffer_create(void); void buffer_destroy(buffer_s* module);

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:

1 2 3 4 5 6 typedef struct buffer_s; typedef struct { uint8_t dummy[32]; } buffer_memory_s; buffer_s* buffer_create(buffer_memory_s &static_memory_to_create_buffer);

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:

1 2 3 4 5 6 7 8 9 10 11 12 13 // @file buffer.h /** * Module data structure * Do not access the members directly; use the provided API */ typedef struct { void *memory; size_t memory_size_in_bytes; } buffer_s; void buffer_push(buffer_s *module, ...); void buffer_pop(buffer_s *module, ...);

If you have a mix of public and private members in C, you can adopt the following technique:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // @file buffer.h /** * Module data structure * Do not access the members directly; use the provided API */ typedef struct { void *memory; size_t memory_size_in_bytes; struct { int foo; size_t bar; } private_data ; } buffer_s;

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

1 2 3 4 typedef enum { SL_CAN_TP__CHANNEL_UDS, SL_CAN_TP__CHANNEL_COUNT, } sl_can_tp__channel_e;

Header File

The header file is like an ordinary header file, except that instead of the this pointers, it uses the enumeration type.

1 2 3 4 5 #pragma once #include "sl_cantp_component_specific.h" void sl_cantp__do_something(sl_can_tp__channel_e channel, void * data);

Source File

The source file declares the data based on the enumeration count.

1 2 3 4 5 6 7 8 #include "sl_cantp.h" // Static memory allocation of all CAN TP channels static sl_cantp_s cantp_array[SL_CAN_TP__CHANNEL_COUNT]; void sl_cantp__do_something(sl_can_tp__channel_e channel, void * data) { }
  • 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):

1 2 3 4 5 6 7 8 typedef struct { uint8_t * buffer; size_t max_payload_len; } sl_can_tp__channel_config_s; typedef struct { sl_can_tp__channel_config_s * channels; } sl_can_tp__config_s;

In sl_cantp_component_specific.h

1 2 3 4 typedef enum { sl_CAN_TP__CHANNEL_UDS, sl_CAN_TP__CHANNEL_COUNT } sl_can_tp__channel_e;

In sl_can_tp__component_specific.c

1 2 3 4 5 6 7 8 uint8_t buffer[100] = {0}; sl_can_tp__channelConfig_s sl_can_tp__channel_configs[sl_CAN_TP__CHANNEL_COUNT] = { [0] = {.buffer = buffer, .maxPayloadLength = sizeof(buffer) } }; sl_can_tp__config_s sl_can_tp__config = { .channels = sl_can_tp__channel_configs };


----

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:
      https://stackoverflow.com/questions/1644868/c-define-macro-for-debug-printing

    • #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:

1 #define DEBUG__FOO 0

Guard of test code:

1 2 3 4 5 6 7 void foo__loop(void) { //... #if DEBUG__FOO // some test code #endif //... }