Embedded C Coding Standard
Reference: Embedded C Coding Standard by Barr Group
1. General Rules
1.1 Which C Version to Use
Comply with the C99 version of the ISO C Programming Language Standard. If a C++ compiler is used, set options to restrict the language to ISO C. Avoid using proprietary compiler extensions and don't use #define
to alter language keywords.
1.2 Line Width Limit
Limit the width of all code lines to a maximum of 80 characters. This makes the code easier to read during peer reviews and when printed, and it also facilitates side-by-side code comparison. Enforcement is through automated scans during each build.
1.3 Use of Braces
Always use braces to surround blocks of code following if
, else
, switch
, while
, do
, and for
statements, even for single statements or empty statements. The left brace {
should appear on its own line below the start of the block, and the corresponding right brace }
should appear in the same position lines later. This improves code readability and minimizes bugs when code is modified or commented out. Enforcement is through automated tools at build time.
1.4 Use of Parentheses
Don't rely on C's operator precedence rules; use parentheses to clarify the execution order of operations. For logical AND (&&
) and logical OR (||
), surround each operand with parentheses unless it's a single identifier or constant. This enhances code clarity and reduces misunderstandings about operator precedence.
1.5 Common Abbreviations
Avoid using abbreviations and acronyms unless they are widely understood in the engineering community. Maintain a version-controlled document with a table of project-specific abbreviations and acronyms. This practice ensures better code readability and maintainability by reducing ambiguity.
1.6 Use of Casts
Always accompany casts with comments explaining how the code ensures proper behavior across the possible range of values. Casting can introduce risks such as type mismatches or overflows, so make it clear why a cast is safe. Enforcement of this rule is through code reviews.
1.7 Keywords to Avoid
Avoid using the auto
and register
keywords as they are considered unnecessary or problematic in modern programming. The use of goto
and continue
is discouraged due to the risk of creating spaghetti code, but if used, goto
should only jump to a later label in the same or an enclosing block.
1.8 Keywords to Use Frequently
Use the static
keyword for functions and variables that don't need to be visible outside their module. Utilize the const
keyword to indicate immutability for variables, function parameters, and struct fields. Apply the volatile
keyword when declaring variables accessible by interrupt routines or multiple threads, or when dealing with hardware registers. These practices improve code quality by reducing coupling and ensuring compiler-enforced protections.
2. Comment Rules
2.1 Comment Formatting
Single-line C++ style comments (//
) are acceptable along with traditional C-style comments (/* ... */
). Comments should never contain preprocessor tokens like /*
, //
, or \
. Code should never be commented out; use preprocessor conditional compilation (#if 0 ... #endif
) for temporary disabling. This avoids confusion and ensures cleaner code maintenance.
2.2 Comment Guidelines
Rules:
- Clarity and Language: Comments should be clear, complete, well-spelled, and grammatically correct.
- Placement and Spacing: Useful comments usually precede a code block that's part of a larger algorithm. Leave a blank line after the block. Align the comment at the same indentation level as the code it refers to.
- Relevancy: Don't over-explain obvious code. Comments should add value and clarify non-obvious portions.
- Proportionality: More complex code needs more extensive commenting. The comment's size should match the complexity of the code.
- External References: If your code is based on an external algorithm or technical detail, cite it.
- Diagrams and Flowcharts: If they're needed for clarity, keep them version-controlled and reference them in the comments.
- Assumptions: Clearly state any assumptions in the comments.
- Documentation Tags: Use comments that are compatible with automated documentation tools like Doxygen.
- Special Markers: Use “WARNING:”, “NOTE:”, and “TODO:” to highlight important issues, explanations, or unfinished work. Include the programmer’s initials if applicable.
3. White Space Rules
3.1 Spacing Guidelines
- Keywords: Keywords like
if
,while
,for
,switch
, andreturn
should be followed by a single space if more code is on the same line. - Assignment Operators: Use one space before and after assignment operators like
=
,+=
,-=
, etc. - Binary Operators: Surround binary operators like
+
,-
,*
,/
, etc., with one space on each side. - Unary Operators: Unary operators like
+
,-
,++
,--
,!
, and~
should have no space between the operator and the operand. - Pointer Operators: In declarations, use spaces around
*
and&
. Elsewhere, no space should be on the operand side. - Ternary Operator:
?
and:
should each be surrounded by a space. - Struct Operators: No spaces should surround the
->
and.
operators. - Array Subscript: Brackets
[
and]
should not have surrounding spaces, unless another rule mandates it. - Parentheses: No spaces should be adjacent to the left and right parentheses in expressions.
- Function Calls: No spaces should be around parentheses in function calls. However, one space should follow the function name in its declaration.
- Commas: In function parameters, each comma should be followed by one space unless it's at the line's end.
- Semicolons in for Loops: After each semicolon in a
for
loop, add one space. - Semicolons: Should follow the statement it terminates, with no preceding space.
3.2 Alignment Guidelines
- Variable Names: Within a series of declarations, the first character of variable names should be aligned.
- Struct and Union Members: The first character of members in structs and unions should also be aligned.
- Assignment Operators: In adjacent assignment statements within a block, align the assignment operators.
- Preprocessor Directives: The
#
in preprocessor directives like#ifdef
should always start at the beginning of a line. However, subsequent directives within a#if
or#ifdef
block may be indented.
3.3 Blank Line Usage
Each line should contain only one statement, blank lines should surround natural code blocks like loops and conditions, and every source file should end with a comment followed by a blank line. These practices improve readability by creating visual separation between blocks of code. The end-of-file comment and blank line can also be beneficial for compatibility with some older compilers.
3.4 Code Indentation
Indentation levels should be set at 4-character multiples from the line start. In switch
statements, case labels should align, and the case content should be indented once. For lines that are too long to fit within the maximum width, subsequent lines should be indented in a way that maximizes readability.
3.5 Avoiding Tabs
The tab character should never appear in source code; spaces should be used for indentation instead. The issue with tabs is that their width can vary across different text editors, making it hard to maintain a consistent visual layout.
Visual Studio Code does have an option to automatically convert tabs to spaces. You can configure this by editing the editor.insertSpaces
and editor.tabSize
settings. This ensures that the code layout remains consistent across different platforms and text editors.
3.6 Non-Printing Characters
Source code lines should end with a single 'LF' (Line Feed, ASCII 0x0A) character instead of the 'CR-LF' (Carriage Return-Line Feed, ASCII 0x0D 0x0A) pair. The only other non-printable character allowed is the form feed character 'FF' (ASCII 0x0C).
Using a single 'LF' reduces cross-platform issues, especially when dealing with multi-line preprocessor macros in Unix environments. The 'CR-LF' sequence can lead to problems because different platforms interpret line endings differently.
4. Module Rules
4.1 Module Naming Rules
Use lowercase, numbers, and underscores for module names, ensuring they're unique in their first 8 characters and avoiding C/C++ Standard Library names. Header and source files should end with .h and .c respectively. If a module contains a main()
function, include "main" in its source file name.
4.2 Header File Guidelines
Each source file should have one corresponding header file with the same root name and a preprocessor guard against multiple inclusion. Header files should only expose what's strictly necessary for other modules (procedures, constants, data types) and avoid declaring variables (because they will be global by default). Public header files shouldn't include private header files because this can lead to unnecessary recompilation. Inclusion of private heders should be limited to the corresponding source file.
4.3 Source File Guidelines
Each source file should control one "entity" like a data type or a peripheral driver and follow a specific section order: comment block, include statements, definitions, static data, private prototypes, public function bodies, and private function bodies. It must include its corresponding header file to allow prototype matching. Avoid using absolute paths in includes and ensure no unused or source file includes are present.
4.4 File Templates
Maintain project-level templates for header and source files. Using templates ensures consistency in file headers and the inclusion of relevant copyright notices.
5. Data Type Rules
5.1 Naming Conventions for Data Types
New data types, including structures, unions, and enumerations, should consist only of lowercase characters, internal underscores, and end with '_t'. Use typedefs to name all new structures, unions, and enumerations. Prefix the name of public data types with their module name followed by an underscore.
5.2 Use Fixed-Width Integers
When the bit or byte size of an integer is important, use fixed-width integer types like int16_t
or uint64_t
instead of char
, short
, int
, long
, or long long
. The use of short
and long
keywords should be avoided. The keyword char
should be limited to string-related operations. This practice aims to improve code portability by standardizing integer widths.
5.3 Handle Signed and Unsigned Integers Carefully
Avoid using bit-fields in signed integers and don't apply bitwise operators to signed integer data. Never mix signed and unsigned integers in comparisons or calculations. To emphasize that a constant is unsigned, use a 'u' suffix. These rules aim to eliminate implementation-defined behaviors and unpredictable outcomes when working with signed and unsigned integers.
5.4 Be Cautious with Floating Point
Minimize the use of floating-point constants and variables; consider fixed-point math as an alternative. If you must use them, adhere to C99 type names like float32_t. Append 'f' to single-precision constants and make sure your compiler supports the precision you need. Never test floating-point values for exact equality or inequality. Use isfinite()
to check for INFINITY or NAN. For example, if (isfinite(x)) { ... }
. These practices help avoid floating-point rounding errors and undefined behaviors.
5.5 Handle Structs and Unions Carefully
When using structs or unions for communication with peripherals or over networks, prevent compiler-inserted padding and maintain bit order. Validate the layout with static assertions or compile-time checks. These rules aim to manage the ambiguities and implementation-defined behaviors around structs and unions in the C language, ensuring portability and correct data representation.
5.6 Use Booleans Properly
Declare Boolean variables as bool
type and use relational operators to convert non-Boolean values to Boolean. This modernizes practices by leveraging the bool
type introduced in C99, which also supports older non-zero integer values as true
.
6. Procedure Rules
6.1 Procedure Naming
Follow specific naming conventions for functions and procedures to ensure clarity and avoid conflicts. Keywords from standard versions of C or C++ are not allowed. Don't overlap with C Standard Library names, and avoid names that start with an underscore (because they're reserved for the compiler and standard library) or are longer than 31 characters. Function names should be lowercase, and macro names uppercase. Use underscores to separate words. Names should be descriptive, possibly using a "noun-verb" structure, and public functions should start with the module name followed by an underscore. This promotes readability and encapsulation.
6.2 Function Guidelines
Aim to keep each function within 100 lines and preferably on one printed page to help in code reviews. If possible, start each new function at the top of a page unless they are short and multiple fit on one page. Stick to a single exit point at the bottom via a return
statement. Declare prototypes for public functions in the header file and mark private functions as static
. Parameters should be explicitly declared with meaningful names. These practices improve readability and make the code easier to understand and maintain.
6.3 Function-Like Macros Guidelines
Use functions instead of parameterized macros whenever possible. If macros are unavoidable, follow strict rules like wrapping the entire body and each parameter in parentheses, and avoiding control flow statements. This minimizes risks related to unintended side-effects, type mismatches, and debugging difficulties.
6.4 Naming Conventions for Threads
Functions that act as threads should have names ending with "_thread", "_task", or "_process". This makes it easier to identify these critical functions during code reviews and debugging, especially in a real-time operating system environment where each thread essentially acts as a mini-main() function.
6.5 Handling Interrupt Service Routines
Interrupt Service Routines (ISRs) are unique and must be explicitly marked as such for the compiler, typically using a #pragma
or a special keyword like “__interrupt”. For example:
#pragma interrupt
void timer_isr(void)
{
...
}
Functions that serve as ISRs should end their names with "_isr". These functions should be declared as static and positioned at the end of the associated module to prevent accidental calls that could corrupt the CPU and call stack. Additionally, a default ISR should be provided for unexpected or unhandled interrupt sources, which should disable further such interrupts and possibly trigger an assert. This naming and structuring make it easier to manage the ISRs and prevent potential conflicts or errors, especially considering that ISRs operate asynchronously to the main code and often share global variables or registers.
7. Variable Rules
7.1 Variable Naming Conventions
Variables in C and C++ should adhere to strict naming rules for clarity and to avoid conflicts. Keywords from standard versions of C or C++ should never be used as variable names. Variable names should also not overlap with standard C library names, start with an underscore, be longer than 31 characters, or shorter than 3 characters. Additionally, various prefixes are used to describe the type or scope of a variable, such as 'g' for global variables, 'p' for pointers, and 'b' for Booleans.
These rules aim to make the code more readable and maintainable, while also avoiding potential risks like race conditions or data corruption. They also ensure that code is portable across different compilers, as some compilers only recognize the first 31 characters of variable names and reserve names starting with an underscore for internal usage.
7.2 Variable Initialization
Always initialize variables before using them to prevent undefined behavior. For local variables, it's better to declare them close to where they're first used rather than at the top of a function. If you're using project- or file-global variables, group them together at the top of the source code. Pointers without an initial address should be explicitly set to NULL.
8. Statement Rules
8.1 Variable Declarations
Avoid using the comma operator in variable declarations to prevent ambiguity. For example, writing char *x, y;
could make it unclear whether y
is intended to be a pointer or not.
8.2 Conditional Statements
Prioritize the shortest if or else if clauses by placing them first. Limit nested if...else statements to two levels deep, using function calls or switch statements to simplify. Never make assignments within if or else if tests. Ensure that any if statement with an else if clause also ends with an else clause.
8.3 Switch Statements
Make sure the break
for each case
is indented to align with the case
label itself, not with the block of code within the case
. This makes it easier to spot missing break
statements. Always include a default
block in your switch
statements. Clearly comment any intentional fall-through between cases.
8.4 Loops
Avoid using magic numbers in loop initial values or endpoint tests. Keep loop control expressions simple—other than initializing and updating a loop counter in a for
loop, no assignments should be made there. For infinite loops, use for (;;)
as the controlling expression. If a loop has an empty body, make sure to include braces containing a comment that explains why nothing needs to be done inside the loop.
8.5 Jumps
Minimize the use of goto
statements and avoid using C Standard Library functions like abort()
, exit()
, setjmp()
, and longjmp()
.
8.6 Equivalence Tests
Always place the constant to the left of the equal-to operator (==
) when comparing a variable to a constant. This helps catch potential typos early. If you accidentally use a single equal sign (=
) instead of a double (==
), the compiler will flag this as an error when the constant is on the left. This practice helps in detecting such mistakes at compile-time, making the code more robust and easier to debug.
Appendix A: Table of Abbreviations
Abbreviation | Meaning |
---|---|
adc | Analog-to-Digital Converter |
avg | Average |
b_ | Boolean |
buf | Buffer |
cfg | Configuration |
curr | Current |
dac | Digital-to-Analog Converter |
ee | EEPROM |
err | Error |
g_ | Global |
gpio | General Purpose Input/Output |
h_ | Handle |
init | Initialize |
io | Input/Output |
isr | Interrupt Service Routine |
lcd | Liquid Crystal Display |
led | Light Emitting Diode |
max | Maximum |
mbox | Mailbox |
mgr | Manager |
min | Minimum |
msec | Millisecond |
msg | Message |
next | Next |
nsec | Nanosecond |
num | Number |
p_ | Pointer |
pp_ | Pointer to Pointer |
prev | Previous |
prio | Priority |
pwm | Pulse Width Modulation |
q | Queue |
reg | Register |
rx | Receive |
sem | Semaphore |
str | String |
sync | Synchronize |
temp | Temperature |
tmp | Temporary |
tx | Transmit |
usec | Microsecond |
Appendix B: Header File Template
/** @file module.h
*
* @brief A description of the module’s purpose.
*
* @par
* COPYRIGHT NOTICE: (c) 2018 Barr Group. All rights reserved.
*/
#ifndef MODULE_H
#define MODULE_H
int8_t max8(int8_t num1, int8_t num2);
#endif /* MODULE_H */
/*** end of file ***/
Appendix C: Source File Template
/** @file module.c
*
* @brief A description of the module’s purpose.
*
* @par
* COPYRIGHT NOTICE: (c) 2018 Barr Group. All rights reserved.
*/
#include <stdint.h>
#include <stdbool.h>
#include “module.h”
/*!
* @brief Identify the larger of two 8-bit integers.
*
* @param[in] num1 The first number to be compared.
* @param[in] num2 The second number to be compared.
*
* @return The value of the larger number.
*/
int8_t
max8 (int8_t num1, int8_t num2)
{
return ((num1 > num2) ? num1 : num2);
}
/*** end of file ***/