Software Development Guidelines#
The software development guidelines for the UltraZohm consists of:
Guidelines on how to develop software
Coding style describes how to format the code visually
Example Implementations for common code modules
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
fortimer_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
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 styleOptional: Change theme (Light/Dark)
Window
Preferences
Additional
->General
->Appearance
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):
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 objectThe
_init
function returns a handle to the object, which has to be passed to the functions of the moduleA 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 scopeThe 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
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#
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 |
||
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 |
|
|
6 |
Avoid function like macros. Use |
inline functions are as fast as macros while macros can lead to a lot of problems - see gcc inline |
|
7 |
Avoid excessive use of |
|
|
8 |
Add a suffix to typed constants |
|
Clearly communicates intend to other programmers |
9 |
Always initialize everything at declaration |
|
|
11 |
Initialize variables when they are first used |
Complaint: |
|
12 |
Switch statments have a |
||
13 |
Declare all functions with a function prototype |
||
14 |
Function prototype for functions without arguments need |
|
The function declaration |
15 |
Use typedef only for |
|
|
16 |
Use typedef for |
||
17 |
Use defined width types from |
|
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 |
|
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 |
Useful for hardware/ip-cores that are off or on, enable signals, valid/reay signals |
|
20 |
Use float versions of math functions when using |
|
|
21 |
Use |
Complaint: |
|
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 |
Requirement of MISRA [5] |
24 |
No unused code, variables, |
||
25 |
No use of the comma operator |
Non-complaint: |
Forbidden by MISRA [5] |
26 |
Do not compare for equality with |
Modern C [4] takeaway 1.5.7.18 |
|
27 |
No |
Forbidden by MISRA rule 15.1 [5] |
|
28 |
No recursion |
Forbidden by MISRA rule 17.2 [5] |
|
29 |
No |
Forbidden by MISRA rule 19.2 [5] |
|
30 |
No octal constants |
Non-complaint: |
|
31 |
No typecast |
Leads to bugs since the compailer can not help with types after the cast [4] |
|
32 |
No pointer arithmetic |
Non-complaint: |
Forbidden by MISRA rule 18.4 [5] |
33 |
No |
Non-complaint: |
Can be used in special cases - only use if really necessary |
34 |
No |
Non-complaint: |
Forbidden by MISRA rule 18.5 [5] |
35 |
No pointer to automatic storage objects |
Non-Complaint (link to godbolt): |
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: |
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. |
Modern C [4] takeaway 2.10.2.7 |
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