Software Development Guidelines#

The software development guidelines for the UltraZohm consists of:

  1. Guidelines on how to develop software

  2. Coding style describes how to format the code visually

  3. Example Implementations for common code modules

  4. Coding rules to follow when writing code

The guidelines are based on concepts described by:

  • Working through the sources is recommended

Guidelines#

  • Write clean code [1] (p. 2 ff):

    • Elegant & efficient

    • Logic should be straightforward

    • Minimal dependencies

    • Ease of maintenance

    • Clean code does one thing well

    • Simple and direct

    • Reads like well-written prose

    • Can be read and enhanced by a developer other than its original author

    • Has meaningful names

    • Clear and minimal API

    • Looks like it was written by someone who cares

    • Contains no duplication

    • You know you are working on clean code when each routine you read turns out to be pretty much what you expect (principle of least surprises)

  • Do not make a mess

  • Encapsulate modules [2] (p. 16)

    • Only expose relevant information through the interface (API)

    • Interface hides implementation details!

    • Objects are self-contained

  • Object oriented programming in C

    • Object orientation is a property of code, not of the language

    • Use object orientated programming

    • Critical idea: data hiding

    • Hide the data in private variables

    • Use interfaces

    • Use structures / pointers to structures to pass it around as an object

    • Abstract the hardware

  • No premature optimization!

    • If you think about optimization of the framework code of the UltraZohm, it is probably premature optimization

    • The compiler is better at optimization than a developer

Names#

  • Write code to be readable by other humans

  • Use intention revealing names, e.g., int pwm_frequency_kHz

  • Use pronounceable, searchable names [1] (p.21) (e.g., not tmrctr for timer_counter)

  • Encode physical units into variables and functions (int time_in_seconds, uz_systemTime_get_uptime_us)

  • Favor longer names to prevent misunderstandings (e.g., _min could be interpreted as minutes or minimum)

  • Append units with _unit (float id_A, float pwm_frequency_kHz)

  • No encoding of information that the compiler knows (e.g., Hungarian notation) (prefixing the variable name by its data type) [1] (p. 23)

    • Exception are AXI-Ports in Simulink for HDL-Generation! (prefix these with axi_ to prevent name conflicts)

    • Structs that are used with a typedef end with _t

    • AXI read/write functions (the C compiler can not know what data type the PL write to a register)

  • Classes (objects) have noun or noun phrase names (uz_pwmModule) [1] (p. 25)

  • Method (functions) have verb or verb phrases (they do things, e.g., uz_pwmModule_set_duty_cycle(uint32_t duty_cycle))

  • Naming convention:

    • Group composites of the object name with lower case camel case (pwmModule)

    • Use snake_case for everything else

    • Encode relationships with underscore (e.g., a method of an object)

    • Everything is lower case except the capital latter in camel case and #defines which are in capital letters

Interface function names

  • Prefix interface functions with uz_ to prevent name conflicts (lower case)

  • Name of the module in lower camel case (uz_moduleName)

  • Name of the function with underscores (uz_moduleName_set_duty_cycle)

  • Group multiple, similar functions with additional underscore

    • Example: uz_systemTime_get_uptime_seconds, uz_systemTime_get_uptime_us, uz_systemTime_get_uptime_minutes

Functions#

  • Functions should be small

  • Do one thing

  • One thing means one cannot extract any meaningful function from the existing function

  • One level of abstraction per function

  • Descriptive names, the function name tells you what it does

  • Do not be afraid to make a name long

  • Function arguments: less is better

  • Use structs for more than two function arguments (e.g., a config struct)

Error handling#

  • Error handling is one thing

  • Fail loudly with Assertions

Comments#

  • Comments lie because code changes and comments get outdated

  • Comment only why code does things (intend), not how

  • Do not comment bad code, rewrite it

  • Explain yourself in code with small functions with meaningful names!

  • Do not comment out code, delete it!

  • But I want to have it for future reference - that is what git and the docs are for

  • Use Doxygen to document the interface of a module

Coding style#

  • Coding style is K&R except:

    • Opening braces of functions are in the same line (int myFunction(int x) {)

    • All control statements have braces (if, else, ..) [5]

  • Indentation is a tab with size 8 [7]

  • Use Vitis autoformat function (ctrl + shift + f) to conform with coding style

  • Optional: Change theme (Light/Dark)

    1. Window

    2. Preferences

    3. Additional -> General -> Appearance

    4. Choose a Theme to adjust color palette

Static code analysis#

Static code analysis checks the source code for potential errors and problems. We use cppcheck , which is also run in the bitbucket pipeline (see Static code check) Usage with the VS Code Remote Container in a terminal to check all files in src folder (recursive):

cppcheck vitis/software/Baremetal/src/
cppcheck --addon=misra vitis/software/Baremetal/src/
cppcheck --addon=cert vitis/software/Baremetal/src/

You can specify a path to only check your currently developed files. Adding --addon=misra checks for violations of [5] coding rules. The output only gives the number of the violated rule, you have to obtain an copy to get readable information. Adding --addon=cert checks for violations of [6] coding rules.

Additional static code analyser that are not implemented for the UltraZohm project:

Example Implementations#

Single-instance module#

Encapsulates an object if only one instance of the module can be present in the system. This only applies to software modules that are hard-coupled to specific hardware and does not apply to IP-Core drivers! This means all initialization is done inside the module function, there is no initialization in code and nothing is passed to init except for configuration if necessary. All required data of the module is declared in the implementation and no data is leaked outside of the module. Functions that are only required internally are declared static. The module offers a public interface in its header.

See the implementation of System Time R5 for a reference implementation of a single-instance module.

Example interface for a LED [3] (p. 194):

Listing 45 Single-instance module#
1 void uz_led_init(void);
2 void uz_led_turn_on(void);
3 void uz_led_turn_off(void);
4 void uz_led_set_toggle_frequency_Hz(float blink_frequency_in_Hz);
5 float uz_led_get_toggle_frequency_Hz(void);

Multiple-instance module#

Encapsulates a module of which multiple instances can be used. This is the default for IP-core drivers. A full example implementation is located at ultrazohm_sw/vitis/software/Baremetal/src/IP_Cores/uz_myIP2 (see How to create a IP-core driver).

  • The implementation scheme uses opaque data types to hide the data of the object

  • The _init function is used to initialize and configure the object

  • The _init function returns a handle to the object, which has to be passed to the functions of the module

  • A public interface in the header is used to use the module

  • A pointer to the object is passed as the first argument of all functions in the public interface (except initialization)

Static memory allocation#

Modules of which multiple instances can exist in the code require a specific way to allocate memory. This allocation must be facilitated in the implementation (.c) to enable the usage of opage data types to hide the data of the object. The default implementation scheme would be to use malloc for dynamic memory allocation at run time, which must not be done due to coding rule 35 (forbidden by MISRA rule 21.3 [5]). This is solved by using a static allocation scheme. A local memory pool (file scope) is allocated in the implementation and pointers to these instances are returned by an allocation function.

  • The header uz_global_configuration.h holds a define for every multi-instance module that configures how many instances will be used.

  • A counter at file scope (static variable instance_counter)

  • A memory pool instances with file scope

  • The function uz_wavegen_allocation which has to be called from the _init function without arguments and returns a pointer to an unused instance of the object

Listing 46 Static allocation of memory with opaque data type#
 1#include "../uz_global_configuration.h"
 2#if myIP_MAX_INSTANCES > 0U
 3#include <stdbool.h>
 4#include "myIP.h"
 5
 6struct myIP_t {
 7 bool is_ready;
 8};
 9
10static uint32_t instance_counter = 0U;
11static myIP_t instances[myIP_MAX_INSTANCES] = { 0 };
12
13static myIP_t* uz_wavegen_allocation(void);
14
15static myIP_t* uz_wavegen_allocation(void){
16 uz_assert(instance_counter < UZ_WAVEGEN_CHIRP_MAX_INSTANCES);
17 myIP_t* self = &instances[instance_counter];
18 uz_assert_false(self->is_ready);
19 instance_counter++;
20 self->is_ready = true;
21 return (self);
22}
23
24myIP_t* uz_wavegen_chirp_init() {
25  myIP_t* self = uz_wavegen_allocation();
26  // more initialization code, configure the object
27  return (self);
28}

Coding rules#

Table 82 table#

Nr

Rule

Example

Comment

1

Write boring code that works instead of clever buggy code that can not be maintained

Maintainability of the codebase is more important than performance - especially since performance gains based on manuall optimization is probably not existent

2

Compile at least with warnings -Wall and -Wextra (see gcc warnings )

3

The number of acceptable warnings is zero

4

Do not comment out code and check it in

Forbidden by MISRA rule 4.4 [5]

5

A MACRO is always all captial letters

#define ADAPTER_CARD_D1_OPTICAL

6

Avoid function like macros. Use static inline instead

inline functions are as fast as macros while macros can lead to a lot of problems - see gcc inline

7

Avoid excessive use of inline keyword

inline is just a hintwhich can be ignored by the compiler. The compiler inlines functions without the inline keyword if appropriate see gcc inline

8

Add a suffix to typed constants

10U; 1.5f;

Clearly communicates intend to other programmers

9

Always initialize everything at declaration

int myVar=0;

11

Initialize variables when they are first used

Complaint: for(int i=0;i<LIM;i++)

12

Switch statments have a default case

13

Declare all functions with a function prototype

14

Function prototype for functions without arguments need void as parameter

void foo(void)

The function declaration void foo() allows to pass a variable number of arguments without a compiler warning

15

Use typedef only for struct and enum

typedef struct uz_myIp uz_myIp;

16

Use typedef for struct and enum wich are used in an interface

17

Use defined width types from <stdint.h>

int32_t; uint32_t

Only use fixed width integers if they are required; for example for hardware mapped registers. Data width below 32-bit is not useful in most cases

18

Use static for all functions that are only used in their translation unit (the .c file)

static float private_function_foo(float x);

Behaves as “private function” (only usable in the translation unit) and gcc inlines these functions if they are only called once with -O1

19

Use bool from <stdbool.h>

Useful for hardware/ip-cores that are off or on, enable signals, valid/reay signals

20

Use float versions of math functions when using float

sinf()

21

Use NULL to check for null pointers

Complaint: uz_assert_not_null(ptr)

22

Check function arguments for validity

Use assertions to communicate intend to the user of what the limits of function arguments are

23

Only one exit at the end of a function

no multiple return

Requirement of MISRA [5]

24

No unused code, variables, typedef, macros

25

No use of the comma operator

Non-complaint: int a, b, c;

Forbidden by MISRA [5]

26

Do not compare for equality with float

Modern C [4] takeaway 1.5.7.18

27

No goto

Forbidden by MISRA rule 15.1 [5]

28

No recursion

Forbidden by MISRA rule 17.2 [5]

29

No union

Forbidden by MISRA rule 19.2 [5]

30

No octal constants

Non-complaint: int i = 0042;

31

No typecast

Leads to bugs since the compailer can not help with types after the cast [4]

32

No pointer arithmetic

Non-complaint: int *ptr; ptr++;

Forbidden by MISRA rule 18.4 [5]

33

No pointer pointer

Non-complaint: int **ptrptr;

Can be used in special cases - only use if really necessary

34

No pointer pointer pointer

Non-complaint: int ***ptr

Forbidden by MISRA rule 18.5 [5]

35

No pointer to automatic storage objects

Non-Complaint (link to godbolt): int* foo(void){int x=0; return (&x);}

Automatic storage objects such as local variables of functions are allocated on the stack and not persistent and should not be leaked outside of their local scope by pointers

36

No dynamic memory allocation

Non-complaint: malloc and friends

Forbidden by MISRA rule 21.3 [5]

37

Define loop variable in the initial part of the for loop

Modern C [4] takeaway 0.2.42

38

Do not hide pointers in a typedef

Modern C [4] takeaway 2.11.2.1

39

Favor pure functions for small tasks if possible

The return value of a pure functions only depend on the input arguments without any side effects of dependencies (e.g. sinf(x))

Modern C [4] takeaway 2.10.2.7

Sources#