Embedded Coding Qualities
Creating source code (code implementation) is an inevitable task for developing embedded software. Success or failure of this task greatly affects the quality of the resulting software.
C language, the most commonly used programming language for embedded software development, is said to give the programmers a relatively extensive writing flexibility.
The quality of programs written in C thus tends to reflect quite clearly the difference in coding skill level between seasoned and less experienced programmers. It is undesirable to have source code varying largely in quality, depending on the programmers’ individual coding skills and experience.
To prevent this risk from leading into serious quality issues, standardization of source codes by establishing coding standards or conventions to be followed organization-wide or group-wide is necessary.
ISO/IEC 25010 defines the quality of software product by breaking it down into eight characteristics (quality characteristics): “reliability”, “maintainability”, “portability”, “efficiency”, “security”, “functionality”, “usability” and “compatibility”.
Among them, “functionality”, “usability” and “compatibility” are considered to be the three quality characteristics that should be addressed at an early stage, preferably before moving on to the design phases in the upstream process.
Whereas, “reliability”, “maintainability”, “portability”, and “efficiency” are considered to be the quality characteristics that have close relevance with the development of high-quality source code and should therefore be examined in depth during the coding phase.
“Security”, which has been defined as the quality sub-characteristic of “functionality” in the previous standard, ISO/IEC 9126-1, is considered basically as a quality characteristic that is relevant in the design phase, but coding such as for avoiding stack overflow can also affect security.
For more information on coding practices related to security, please refer to “CERT C Secure Coding Standard”.
Embedded Coding Quality Concepts
This blog shall address 4 coding quality characteristics as below and corresponding rules should be apply to achieve them.
1. Reliability:
Degree to software performs specified functions under specified conditions for a specified period of time
- Maturity: low occurrence of bugs through continued use
Availability: N/A (system level characteristic: operational and accessible when required for use)- Fault Tolerance: software tolerate against bugs and interface violations, etc
Recoverability: N/A (system level characteristic: in the event of an interruption or a failure, system can recover the data directly affected and re-establish the desired state of the system)
2. Maintainability:
Degree of effectiveness and efficiency with which software can be modified by the intended maintainers
- Modularity: components are composed such that a change to one component of the code has minimal impact(loose coupling) on other components.
- Reusability: degree to which a code can be used in other programs.
- Analysability: easiness of understanding the code.
- Modifiability: easiness of modifying the code, and lowness of impact from modifications.
- Testability: easiness of testing and debugging the modified code.
3. Portability:
Degree of effectiveness and efficiency with which software can be transferred from one hardware, other operational or usage environment to another.
- Adaptability: easiness of adapting to different environments.
Installability: N/A (system level characteristic: degree of effectiveness and efficiency with which system can be successfully installed and/or uninstalled in a specified environment).Replaceability: N/A (system level characteristic: degree to which a product can be replaced by another specified software product for the same purpose in the same environment).
4. Efficiency:
Performance relative to the amount of resources used under stated conditions
- Time Behaviour: efficiency with regard to processing time.
- Resource Utilization: efficiency with regard to resources.
Capacity: N/A (system level characteristic: degree to which the maximum limits of a product or system parameter meet requirements)
5. Security:
Degree to which software protects information and data so that other software have the degree of data access appropriate to their types and levels of authorization.
- Confidentiality: degree of certainty that data are accessible only to those authorized to have access.
- Integrity: degree of prevention of unauthorized access to, or modification of, computer programs or data.
Nonrepudiation: N/A (system level characteristic: degree to which actions or events can be proven to have taken place, so that the events or actions cannot be repudiated later)Accountability: N/A (system level characteristic: degree to which the actions of an entity can be traced uniquely to the entity)Authenticity: N/A (system level characteristic: degree to which the identity of a subject or resource can be proved to be the one claimed)
Detail Rules
Term “areas”: used to specify the usage of memory in general such as variables(local, global), arguments, arrays, ptr,…
Side-effect: processing that cause changes to a state of execution environment. The following process apply: reference and change to volatile data, change to data, change to files, and function-calls that perform these operations.
I. Reliability
A large number of embedded software is incorporated into products and used to support our daily lives in various situations. Consequently, the level of reliability demanded to quite a number of embedded software is extremely high.
Software reliability requires the software to be capable of not behaving wrongly (not causing failure), not affecting the functionality of the entire software and system in case of malfunction, and promptly restoring its normal behavior after a malfunction occurs.
At the source code level, the point to be noted in regard to software reliability is the need of contriving methods to avoid coding that may cause such malfunctions as much as possible
Practices to improve the reliability of software that has been developed fall under this category. Main points taken into consideration include:
- Minimizing problems arising while using the software.
- Increasing tolerability against bugs and interface violation.
1. Initialize areas and use them by taking their sizes into consideration
- Variables shall be initialized at the time of declaration, or the initial values shall be assigned just before using them.
- Arrays with specified number of elements shall be initialized with values that match the number of the elements.
- Integer addition to or subtraction from (including
++
and--
) pointers shall be made only when the pointer points to the array and the result must be pointing within the range of the array. Otherwise, array format with[]
shall be used for references and assignments to the allocated area.
- Comparison or subtraction between pointers shall be used only when the two pointers are both pointing at either the elements in the same array or the members of the same structure.
2. Use data by taking their ranges, sizes and internal representations into consideration.
- Floating-point type, values written in the source code do not exactly match with those actually represented by the hardware ⟹ don’t use it for equal comparison or counter.
- Memcmp should not be used to compare structures and unions (structures and unions may contain unused areas because of default memory padding for memory alignment. Since the values in the areas are unknown, memcmp should not be used). However, memcmp could still be used when struct packing is explicitly specified (e.g. in GCC compiler by adding
__attribute__((__packed__))
to struct keyword).
- In C language, true is represented by any non-zero value, not necessarily 1 ⟹ don’t use logical compare with TRUE(not 0), use comparison with FALSE(0) instead.
- Use same type in comparison or arithmetic operations, use explicit cast(cast the involved operant to target operant’s type) and beware of overflow.
- Use explicit cast to target type when operation involve one’s complement (
~
) or left shift (<<
) (since these operation could cause signed bit turn on)
- The right-hand side of a shift operator shall be zero or more, and less than the bit width of the left-hand side.
- Pointer type shall not be converted to other pointer type or integer type with less data width than that of the pointer type, with the exception of mutual conversion between “pointer to data” type and “pointer to other data” type, and between “pointer to data” type and “pointer to void*” type.
- A cast shall not be performed that removes any const or volatile qualification from the type addressed by a pointer.
3. Write in a way that ensures intended behavior.
- Write in a way that is conscious of area size (i.e. no function with variable number of arguments, size of array shall always be specified, iteration conditions for a loop to sequentially access array elements shall include the decision to whether the access is within the range of the array or not.).
- Prevent operations that may cause runtime error from falling into error cases (e.g. always check for zero for division operation and
nullptr
with dereference operation).
- Check the interface restrictions when a function is called (e.g. always check function input parameters are in constrain range, check return value when called function could return error information).
- Do not perform recursive calls (functions should not call them self directly or indirectly because recursive’s stack size can not be predicted at runtime).
- All branch condition should be write explicitly(i.e.
if-else
if-else if
,switch-case-default
), in case of else, switch-default branch that is not reached normally then a comment shall be added to explain.
- Equality operators (
==
) or inequality operators (!=
) shall not be used for comparisons of loop counters. (<=
,>=
,<
, or>
shall be used.) (if the amount of change for the loop counter is not 1 then==
,!=
could cause infinite loop).
- Pay attention to the order of evaluation, be explicit, split the operation that could cause possible side-effect (ex:
f (x, x++);
« this could cause side-effect since the compiler does not guarantee the order of execution from left or right of arguments).
II. Maintainability
Many embedded software developments require maintenance tasks, including the modification of the software that has already been developed. There are various reasons for maintenance. For example, maintenance becomes necessary:
- When a bug is found in one part of the released software and must be modified.
- When a new function is added to existing software.
When any kind of additional work is carried out on the already developed software as in the above examples, it is important to perform such work as accurately and efficiently as possible to maintain the quality of the software.
Practices to create source code that is easy to modify and maintain fall under this category. Main points taken into consideration include:
- Making the code easy to understand and modify (keep in mind that others will read and make modify to the program).
- Minimizing the impact of modifications on the entire code (keep it simple and modular).
- Making the modified code easy to check.
1. Keep in mind that others will read the program.
- Do not leave unused descriptions (remove unused things: vars, args, typedefs, code, comment).
- Do not mix declared variables with init value and without init value.
- Use suffix in upper case (
L
,U
) for constant number.
- Use
( )
to clearly specify the operator precedence.
- Always use preceding & operator to get function address.
-
One variable used for one purpose (e.g. if a variable is declared to be used as counter in a loop, don’t use it for other purpose in different part of the program).
-
Do not reuse name in different scope (C language will not prevent you to have the same identifier in different namespace/scope but you should not do it).
- The right-hand operand of a logical
&&
or||
operator shall not contain side effects (The right-hand side of&&
or||
operators may not be executed, depending on the result of the condition of their left-hand side)
- Do not embed magic numbers, meaningful constant shall be defined as macro.
-
Read-only areas shall be declared as const type (a variable is only referenced and not modified, declaring it as const-qualified variable makes it clear that it is not modified).
-
Areas that may be updated by other execution units shall be declared as volatile (volatile prohibit the compiler from optimizing them).
2. Write in a style that can prevent modification errors.
- If arrays and structures are initialized with values other than 0, their structural form shall be indicated by using braces
{ }
. Data shall be described without any omission, except when all values are 0.
- The body of
if
,else if
,else
,while
,do
,for
, andswitch
statements shall be enclosed into blocks.
-
Variables used only in one function shall be declared within the function.
-
Variables accessed by several functions defined in the same file shall be declared with static in the file scope (the fewer the global variables, the higher the readability of the entire program becomes).
-
Functions that are called only by functions defined in the same file shall be static.
-
Enum
shall be used rather than#define
when defining related constants. By defining related constants asenum
type, and using this type, mistakes caused by the use of incorrect values can be prevented. While macro names defined by#define
are expanded at the preprocessing stage and the compiler does not process those names,enum
constants defined byenum
declaration will be the names processed by the compiler. The names processed by the compiler are easier to debug, because they can be referenced. during symbolic debugging.
3. Write programs simply.
- For any iteration statement, there shall be at most one break statement used for loop termination (keep only one return point for code block so it easier to trace)
-
The goto statement shall not be used (to prevent the program logic from becoming complex).
-
A function shall end with one return statement (except for the case of recovery from abnormality).
-
Multiple assignments shall not be written in one statement, except when the same value is assigned to multiple variables.
- Write expressions that differ in purpose separately.
- Numeric variables being used within a for loop for iteration counting shall not be modified in the body of the loop.
- Do not use complicated pointer operations.
4. Write in a unified style.
-
Unify the coding styles. (brace position
{}
, space, tab usage) -
Unify the style of writing comments. (comment format)
-
Unify the naming conventions (variable, function, file, function should be verb to describe operation, variable should be noun to describe data content).
-
Unify the contents to be described in a file and the order of describing them, example order content in header file as below:
-
Only declarations or type definitions should be described in a header file.
-
Header files shall has header guard macro to prevent redundant inclusions.
-
In a function prototype declaration, all the parameters shall be named. (C allow to omit parameter name in prototype declaration but should not be used).
-
Unify the style of writing declarations, example order content in source file as below:
-
Unify the style of writing null pointers.
NULL
shall be used for the null pointer.NULL
shall not be used for anything other than the null pointer. -
Unify the style of writing preprocessor directives. The body and parameters of a macro that includes operators shall be enclosed with parentheses
( )
. -
#if defined(macro_name)
or#if defined macro_name
shall be used to check whether the macro name has already been defined by#if
or#elif
-
#else
,#elif
or#endif
that correspond to#ifdef
,#ifndef
or#if
shall be described in the same file, and《their correspondence relationship shall be clearly stated with a comment defined in the project》. -
Controlling expression of
#if
or#elif
preprocessing directive shall be evaluated as 0 or 1.
5. Write in a style that makes testing easy.
- Write in a style that makes it easy to investigate the causes of problems when they occur.
- by isolating the debug descriptions using macro definitions
- by using assert macros for debugging purpose.
- Outputting logs after release
- When: Logs should be output not only when an abnormal condition is detected, but also at the timing of, such as, data communication with an external system (when key events occur)
- What: data values processed at that time, and information for tracing memory usage
- Localize the log information output as a macro or a function
- Dynamic memory shall not be used. Issues with dynamic memory:
- Buffer overflow: result of writing past the end of the buffer. If this overwrites adjacent data or executable code, this may result in erratic program behavior, including memory access errors, incorrect results, and crashes.
- Forgetting to initialize: acquired memory fill with trash value.
- Memory leak: cause memory depletion and system malfunction.
- Use after return: reference to memory that has been deleted.
III. Portability
One of the distinctive aspects of embedded software is that there are diverse options in the platform used for software operation. This also means that there are many possible combinations of MCU options and OS options to select the hardware and software platforms from. As the number of functionalities realized by the embedded software increases, opportunities to port the existing software to other platforms by modifying or remodeling it to make it compatible with multiple platforms are also on the rise.
Due to this trend, software portability is becoming an extremely important element also at the source code level. In particular, writing in a style that is implementation-dependent is one of the most common mistakes made on a regular basis.
Practices to port the software program that has been created on the assumption of being used to operate under a certain environment to another environment as efficiently as possible without error.
- Abstract the source code implementation with layer of independent(e.g. independent in term of HW, compiler, operating system,…).
- Loose coupling between components in source code.
1. Write in a style that is not dependent on the compiler.
-
Do not use functionalities that are advanced features(not specified in the language standard) or implementation-defined (e.g. when specific implementation-defined features which behavior varies depending on the compiler is used, they should be clearly documented).
-
Use only the characters and escape sequences defined in the language standard.
-
Confirm and document data type representations, behavioral specifications of advanced functionalities and implementation-dependent parts.
-
For source file inclusion, confirm the implementation dependent parts and write in a style that is not implementation-dependent.
-
Write in a style that does not depend on the environment used for compiling. (ex: no absolute path)
2. Localize the code that has a problem with portability.
- When assembly language programs are called from C language or keywords extended by the compiler expressing them as functions or inline functions of C language that contain only inline assembly language code or describing them using macros.
- The basic types (
char
,int
,long
,long long
,float
,double
andlong double
) shall not be used. Instead, the types defined by typedef shall be used (i.e.int8_t int16_t int32_t int64_t uint8_t uint16_t uint32_t uint64_t
)
IV. Efficiency
Embedded software is characteristic for being embedded in a product and operating together with hardware to serve its purposes in the real world. The increasing demand for further product cost reduction has imposed various restrictions, not only on, such as, MCU or memory, but also on software.
In addition, requirements, such as, on real-time property have placed stricter time constraints that need to be met. Embedded software must therefore be coded with particular attention on resource efficiency like efficient use of memory and time efficiency that takes account of time performance.
Practices to effectively utilize the performance and resources of the software that has been developed fall under this category. Main points taken into consideration include:
- Coding that is processing time-conscious.
- Coding that takes account of memory size.
Write in a style that takes account of resource and time efficiencies.
-
Macro functions shall be used only in parts related to speed performance. (Function is safer than macro function. So, use function as much as possible, inline function can be one way of preventing the processing speed from slowing down. But since inlining is implementation-dependent, use macro function instead)
-
Operations that remain unchanged shall not be performed within an iterated process.
- Instead of structures, pointers to structures shall be used as function parameters (if a structure is passed as a function argument, all the structure data are copied into the area for storing arguments when the function is called. If the size of the structure is large, it will become the cause of speed performance degradation).
- The policy of selecting either switch or if statement shall be determined and defined by taking readability and efficiency into consideration.
- switch statements often provide higher readability than if statements
- recent compilers tend to output optimized code using, such as, table jump or binary search when they process switch statements
Typical Coding Errors in Embedded Software
1. Meaningless expressions and statements
Leaving statements or expressions that are not executed in the source code is likely to create misunderstanding that often leads to problems as a result. It is said that confusion tends to be caused especially when the source code is modified by engineers who are not the originator of that particular code.
- Writing statements that are not executed.
- Writing statements whose execution result is not used
- Writing expressions whose execution result is not used (e.g.
return cnt++;
) - Values passed as arguments are not used
2. Wrong expressions and statements
To write proper source code, it must be written according to the grammar of the programming language being used. But even programmers who are familiar with the programming language being used can make careless mistakes. Presented below are some examples of wrong expressions and statements that are often seen.
- Incorrect range specification (e.g.
if (0 < x < 10)
) - Comparing outside the range (e.g.
unsigned char uc; if (uc == 256)
) - String comparisons cannot be performed with
==
operation (no operator overloading in C) - Inconsistency between a function type and return statement of the function
3. Wrong memory usage
One of the characteristics of C language is that memory can be handled directly. While this is a very useful feature when creating embedded software, it also often causes incorrect operations and must therefore be used carefully.
- Reference and update outside the array bounds (e.g.
char var1[N]; var1[-1] = 0; var1[N] = 0; /* error */
) - Passing the address of an automatic variable to the caller mistakenly
- Referencing memory after being freed as dynamic memory
- Writing into string literals mistakenly
- Specifying copy sizes mistakenly
4. Errors due to misunderstanding in logical expressions
The use of logical operators is relatively error-prone. In situations where they are used, close attention must be given especially to the operation results, since in many cases, they lead to different subsequent processes.
- Using a logical product mistakenly instead of a logical sum or other way around (
&&
,||
)
- Using a bitwise operation mistakenly instead of a logical operation
5. Mistakes due to typos
Some operators in C language like = and == have completely different meaning even though they do not differ that much. When writing these operators, sufficient attention must be given to prevent careless mistakes or typos.
- Writing
=
operator instead of==
operator (e.g.if (x = 0)
to avoid this case always let constant to be 1st operantif (0 = x)
then compiler shall detect this error)
6. Wrong descriptions that do not cause errors in some compilers
Each compiler has various characteristics of its own. Note that some compilers do not cause compile errors during compilation even if the program contains inappropriate descriptions.
- Macro with the same name that has multiple definitions
- Writing into the const area mistakenly
Ref:
- MISRA C guidelines
- Embedded System development Coding Reference guide