1. Quick-Start Rules for Embedded C Systems
1.1. Compiler Warnings
We like to have the compiler give us as much help and catch as many unintended bugs as possible, and therefore we like to have the “Strict ANSI Warnings” project setting on for all C projects.
1.2. Line Widths
Try to keep line widths within the viewable editing area of the screen. Given that different people will view the source code at different times, a right-margin setting of around column 80 is the norm. This is not a strict rule, but helps make code more readable.
1.3. Braces
Braces ({...}) shall always surround blocks of code (also known as compound
statements) following if
, else
, switch
, while
, do
, and
for
keywords. Single statements and empty statements following these
keywords shall also always be surrounded by braces. Rationale: this makes the
code more easily understandable and avoids slip-ups (due to code ambiguity) that
often happen when making changes to code without them.
/* Don't do this ... */
if (timer.done)
/* A single statement needs braces! */
timer.control = TIMER_RESTART;
/* Do this ... */
if (timer.done) {
/* A single statement needs braces! */
timer.control = TIMER_RESTART;
}
while (!timer.done) {
/* Even an empty statement should be surrounded by braces. */
}
/* This can be okay as well since it is clear what is happening, though
* debuggers have difficulty stepping through these. */
while (!timer.done);
The Kernighan and Ritchie standard brace format above is preferable to the Allman (flat) format if only because it reduces the number of lines occupied by code, and thereby makes the code more readable.
/* Don't do this ... (Allman format) */
if (timer.done)
{
timer.control = TIMER_RESTART;
}
else
{
/* Something else here. */
}
/* Do this ... (Kernighan & Ritchie format) */
if (timer.done) {
timer.control = TIMER_RESTART;
} else {
/* Something else here. */
}
It’s just more readable and thus faster to comprehend.
The following is a classic example of code ambiguity that I came across and nearly stumbled on on 10-Apr-2024:
if (EUSART1_Read() == LIN_getChecksum(LIN_packet.length, LIN_packet.PID, LIN_packet.data))
;
return true;
Yes, this is how it was indented.
The above equates to:
if(EUSART1_Read() == LIN_getChecksum(LIN_packet.length, LIN_packet.PID, LIN_packet.data)) {
; /* No action taken. */
}
return true;
I was porting code from a polling execution environment on a PIC18F to a multi-threaded event-driven environment on a PIC32MZ, and ALMOST interpreted that TRUE was only returned when the checksum matched! If I had made that interpretation, other surrounding code (receiving data from a UART) would have allowed data to be overwritten in a buffer and been VERY sloppy data management, inviting a re-write. But when the ambiguity was clarified, then the other data management was clear and okay.
Do you see how dangerous ambiguous code is???
1.4. Parentheses
Parentheses shall be used around all subsets within a complex mathematical or Boolean expression. Reason: not everyone remembers the details of the C-language’s 12-level operator-precedence hierarchy, and adding parentheses around expressions that should be evaluated first removes all ambiguity. Rationale: it increases understandability, avoiding ambiguity.
1.5. const
Keywoard
The const
keyword shall be used whenever possible, including:
To declare variables that should not be changed after initialization.
To define call-by-reference function parameters that should not be modified.
Example:
const char * acpString /* Makes the data pointed to read-only, but the contained address can be changed. * The compiler will complain if this read-only restriction is violated. */ char * const acpString /* Makes the contained address read-only, but the object it points to can be changed. * The compiler will complain if this read-only restriction is violated. */ const char * const acpString /* Makes both the contained address AND the data pointed to, read only. * The compiler will complain if this read-only restriction is violated. */
This is very useful in functions as it explicitly conveys a guarantee to the caller of what can or cannot be changed by the call. This is often important when passing pointers to buffers. A classic example is:
extern void * memcpy(void *, const void *, size_t);
where the presence of the
const
modifier unambiguously indicates which argument points to the source buffer vs the target buffer.To define fields in structs and unions that cannot be modified (such as in a struct overlay for memory-mapped I/O peripheral registers, for reserved bit fields that cannot be changed).
As a strongly typed alternative to #define for numerical constants.
Rationale: The upside of using const as much as possible is compiler-enforced protection from unintended writes to data that should be read-only.
1.6. static
Keyword
The static
keyword shall be used to declare all functions and variables that do
not need to be visible outside of (i.e. are not part of the public API of) the
module in which they are declared.
static
function names are prefixed with an underscore (_) to make it very clear
that they are accessible internally within the module only.
Rationale: C’s static keyword has several meanings. At the module-level, global variables and functions declared static are protected from inadvertent access from other modules. Heavy-handed use of static in this way thus decreases coupling and furthers encapsulation (these are good things that reduce bugs).
1.7. volatile
Keyword
The volatile
keyword shall be used whenever appropriate, including:
To declare a global variable accessible (by current use or scope) by any interrupt service routine,
To declare a global variable accessible (by current use or scope) by two or more tasks (threads), and
To declare a pointer or other reference to a memory-mapped I/O peripheral register set. (In the Microchip PIC microcontroller world, the microcontroller’s include file [e.g. p33EP512MU810.h] takes care of this for peripheral register references such as
LATGbits
.) Example:T1CONBITS volatile * const lpTimer = &T1CONbits;
Complex variable declarations like this can be difficult to comprehend. However, the practice shown here of making the declaration read “right-to-left” simplifies the translation into English. Here,
lpTimer
is a constant (read-only) pointer (contained address may not be changed) to a volatile T1CONBITS type register set. That is, the address of the timer registers is fixed while the contents of those registers may change at any time.
Rationale: Proper use of volatile
eliminates a whole class of difficult-to-detect
bugs by preventing the compiler from making optimizations that would eliminate
requested reads or writes to variables or registers that may be changed at any time
by a parallel-running entity (such as a hardware peripheral or ISR or another thread).
Anecdotal evidence suggests that programmers unfamiliar with the volatile
keyword
think their compiler’s optimization feature is more broken than helpful and disable
optimization. Michael Barr’s experience consulting with numerous companies
suggests that the vast majority of embedded systems contain bugs waiting to happen
due to a shortage of volatile keywords. These kinds of bugs often exhibit
themselves as “glitches” or only after changes are made to a “proven” code base.
1.9. Portability
Whenever the width (in bits or bytes) of an integer value matters in the program, a fixed-width data type shall be used in place of char, short, int, long, or long long. The signed and unsigned fixed-width integer types shall be as shown below.
These are the standard signed and unsigned fixed-width integer types:
Integer Width Signed Type Unsigned Type
----------------- --------------- ---------------
8 bits / 1 byte int8_t uint8_t
16 bits / 2 bytes int16_t uint16_t
32 bits / 4 bytes int32_t uint32_t
64 bits / 8 bytes int64_t uint64_t
Rationale: The ISO C standard allows implementation-defined widths for char, short,
int, long, and long long types, which leads to portability problems. Though the 1999
standard did not change this underlying issue, it did introduce the uniform type
names shown in the table, which are defined in the new header file <stdint.h>
.
These are the names to use even if you have to create the typedefs by hand.
1.10. Bit-Wise Operations on Signed Integers
None of the bit-wise operators (in other words, &
, |
, ~
, ^
, <<
,
and >>
) shall be used to manipulate signed integer data.
/* Don't do this ... */
int8_t li8Byte = -4;
li8Byte >>= 1; /* not necessarily -2 */
Rationale: This policy keeps C code portable across different C compilers. Reason: The C standard does not specify the underlying format of signed data (for example, 2’s complement) and leaves the effect of some bit-wise operators to be defined by the compiler author.
1.11. Integer Type Consistency
Signed integers shall not be combined with unsigned integers in comparisons or expressions without specific casting indicating the intentions for the operation. In support of this, decimal constants meant to be unsigned should be declared with a ‘u’ or ‘ul’ suffix so the compiler interprets them as unsigned values. Use lower-case ‘ul’ instead of ‘UL’ because it makes it easier to see where the number ends and where the suffix begins. A classic example of why this policy is needed: some compilers will generate two very different sets of underlying assembly language code for these two lines of code:
if (aHwnd->iui32Style == 0x10800000) { ... }
vs
if (aHwnd->iui32Style == 0x10800000ul) { ... }
and this can and does result in the evaluation of the condition to return TRUE when it should return FALSE, and vice versa. More obvious examples are the use of <, >, <=, and >= comparison operators.
Rationale: Several details of the manipulation of binary data within signed integer containers are implementation-defined behaviors of the C standard. Additionally, the results of mixing signed and unsigned data can lead to data-dependent bugs.
1.12. Ambiguous Meaning of double
with XC Compilers
Never use the double
data type with Microchip compilers. Reason: the term is
ambiguous and depending on compiler options (which can be set in the project as well
as with individual files), it can mean EITHER an IEEE 754 32-bit single-precision
floating-point value, or an IEEE 754 64-bit double-precision floating point value.
Instead always use only float
or long double
, as these always mean IEEE 754
32-bit single-precision floating-point value, and an IEEE 754 64-bit double-precision
floating point values, respectively.
Note, however, that we are FORCED to deal with double
type with the printf
library, since the %f
format specifier requires an argument of type double
.
The preferred way of dealing with this to explicitly deal with double-precision
values by using format specifier “%Lf” (note “L” must be capitalized) and passing
type long double
, and single-precision by explicitly casting to double
type
in the argument list. Correct examples:
sprintf(lcpBuf, "%Lf", lldLongitude);
sprintf(lcpBuf, "%f", (double)lfAltitude);
Also note that as of late 2013, HM Quickshifter UK, Ltd. has its own full-featured, cross-platform, exhaustively tested “printf” library to bypass the unreliable Microchip “printf” libraries (which have historically have had “odd” bugs in them, including causing CPU exceptions!).
1.13. Comma Operator
Avoid using the comma operator when declaring variables (in a list), especially when mixing pointer and value types. Doing so opens the door to code ambiguities. Defining one variable per line eliminates this potential ambiguity and universally increases readability.
1.14. char
Keyword
Use of the keyword char
shall be restricted to the declaration of and operations
concerning strings.
Rationale: Among the implementation-defined behaviors of C is the signed-ness of a
char data type. One compiler may treat your char variables as unsigned, another as
signed—and yet both are technically ISO C compliant! This introduces subtle and
potentially hidden risks related to using bit-wise operations on signed integers
or mixing signed and unsigned integers in operations. The risk of bugs derived
from this subtlety of C are entirely eliminated by choosing int8_t
or uint8_t
explicitly whenever the data is other than part or all of a string.
1.15. Data Type Names
Names of all new data types, including structs, unions, and enumerations, shall consist camel-case letters, internal underscores, and end with “_t”. Enumeration types and values are both prefixed with the letter ‘e’ to help readability in code (since the underlying data types are actually
int
, (a.k.a.signed int
) and it is important for this to be visible in code). See Enumeration Prefix for more information as to how this policy came about and why it is important.All new structs, unions, and enumerations shall be defined via a
typedef
.The name of all public data types shall be prefixed with the module name and underscore.
1.16. Structured Code
For clarity and readability, functions should only have one exit point at the
end of the function. (This comes from very real gains implemented by something
called “structured design” and “structured coding” from the late 1990s.) Functions
that return values exit with a return
statement, and functions that return
void
exit by “executing at the end” of the function.
ISR functions never have return statements since the compiler generates special return instructions for ISRs.
1.17. Subsystems
There are two common approaches to building a new reliable subsystem in firmware. One is a pure Object-Oriented approach wherein a .C/.H module represents potential FUTURE instances of an object, and the logic that relates to it (i.e. it is the client’s job to allocate memory for the object(s) created and used, and it is the client’s job to properly call functions related to that logic, where the norm is that the first function argument is always the address of the object to which it applies). The client side of that equation looks like this (say the subsystem represents objects of type xxxxx_t:
Xxxxx_t gMyRamXXXXX; /* Which can be 1, or several or an array. */
or
Xxxxx_t * gpMyRamXXXXX; /* Pointer to an object of that type, which is then
* dynamically allocated in the heap, with pointer
* stored here until it is disposed of. */
Then the client uses that subsystem like this:
XXXXX_Initialize(&gMyRamXXXXX, ...);
XXXXX_DoThis(&gMyRamXXXXX, ...);
XXXXX_DoThat(&gMyRamXXXXX, ...);
etc.
or
XXXXX_Initialize(gpMyRamXXXXX, ...);
XXXXX_DoThis(gpMyRamXXXXX, ...);
XXXXX_DoThat(gpMyRamXXXXX, ...);
etc.
Common examples are containers we learned about in computer-science (e.g. arrayed lists, linked lists, circular queues, etc.), or they might be something specific to the problem domain the firmware is being created for (e.g. odometer, mixture control, feed storage, logging_state, gps_engine, etc.). Either way, the module merely contains the type definitions for those types of objects (in the .H file), and the logic that deals with those type of objects (in the .C file), but does not allocate storage for those objects, instead leaving that up to client software.
The second common approach is when the .C/.H module provides the storage required, and it will always be the case that there are only ever ONE or a KNOWN UNDERSTOOD NUMBER of those type of objects in the system. For a dashboard, for example, where the .C/.H module provides a single encapsulation for something like “global vehicle information”, that gets displayed on the dashboard, such as SPEED, RPM, Water Temperature, Lifetime Distance Travelled (odometer), Session Distance Traveled, Trip Distance Traveled, etc. etc.. In this case, the module provides both the object storage AND its management/service logic.
In both of these cases, the software development principles that boost reliability all apply. For example, data encapsulation, where when it is used in pure form, only logic contained in THAT MODULE is able to directly change the data. On the other side of that coin, when modification of that data is spread in various places across the system (different source-code files, in different subsystems), then to that degree new bugs are likely when it comes to managing that data. Data encapsulation attempts to significantly narrow the how far and wide the logic that deals with that data is spread across the system. And doing so almost always has excellent reliability improvement as a result.
As much as possible, we try to adhere to the above when constructing new subsystems for firmware.
1.19. Indenting Compiler Directives
Compiler directives USUALLY start in column 1, even in indented source code. However, when there are nested conditional compiler directives, when it makes it more readable/understandable to indent the directives it is desirable to do so for the sake of readability.
1.20. Code Location
We try to eliminate static function declarations (i.e. prototypes, not to be confused with static function definitions) where possible. Instead, wherever possible, we want the single static function definition to also be the declaration for code that uses it. This makes maintenance of these functions simpler and less error prone.
To put it another way....
We prefer not to have to provide any static function prototypes wherever possible because having them means that when the function’s signature changes (which, in practice, happens frequently as a module is being developed), that we have to remember to change it in two places.
To put it another way....
We don’t like redudancy in code.
This helps apply some of the principles of clean code:
DRY: Don’t Repeat Yourself (eliminate code duplication);
KISS: Keep it Simple Stupid (embracing simplicity); and
YAGNI: You Ain’t Gonna Need It (focus on essential functionality, i.e. don’t implement features you don’t need).
We further prefer to only include in the .H file of the .C/.H pair, only those macros, typedefs, storage declarations, and function prototypes actually needed by external client software that uses it (in other words, just the API and nothing else, wherever possible).
The above desirable rules require there be a certain ordering to the functions in a C file, roughly from top to bottom:
heavily-used utility functions that are used throughout the module come at the top of the file (whether they are public [API] or private [
static
]);static
(private) functions;static
functions that call otherstatic
functions come below thestatic
functions they call;public (API) functions come at the bottom of a C file;
the first public (API) functions used by client software (such as initializing a subsystem) are at the very bottom.
And to make API functions easy to find, they are separated into labeled grouped that often resemble the following (though the group names vary with the type of subsystem). The following groups (common for “container” type subsystems) are shown for illustration purposes only:
- Access \ Queries (do not change the state of the container or its elements)
- Status Report /
- Cursor Movement \
- Element Change \ Commands (do change the state of the container or its elements)
- Removal /
- Initialization /
For smaller, less-complex subsystems, this commonly breaks down to just these three groups:
And finally, the list of prototypes in the .H file are in the same order, but of
course do not include prototypes for the static
functions — only the public
(API) functions.
During development, this makes the list of prototypes easy to create without re-typing: just copy the entire contents of the C file into the .H file prototypes section (at the bottom) and delete the code and keep just the function signatures, placing semicolons at the end instead of an opening curly brace.
Further: static (private to a module) variables and functions are never declared in a .H file.
This pattern serves 2 purposes:
reduction of bugs by removal of redundant declarations, and
merely studying the list of exported (API) functions in a .H file can be all one needs to understand how to use the module.
A classic example of the educational value of the list of prototypes in a .H file is this list that comes from a real-world heavily-used library subsystem: Crystal-Clear Research’s Ordered Circular Two-Way Linked List, whose prototype list is shown below. Take note of the facts that:
anyone already familiar linked lists can pretty much figure out how to use this tool merely by studying the list of prototypes; and
the visual alignment of return types, function names, and argument lists, shows a pattern in the API that is very orienting for unfamiliar readers.
Note
The Doxygen code group commands are left in the comments that mark each group so you can see how this is done.
wgOrderedCircularTwoWayLinkedList.h:
/**========================================================================
* \name Access
* @{
*//*======================================================================*/
OCTWLLItem_t * OCTWLL_qpItem (const OCTWLList_t * const apList);
OCTWLLItem_t * OCTWLL_qpFirst (const OCTWLList_t * const apList);
OCTWLLItem_t * OCTWLL_qpLast (const OCTWLList_t * const apList);
OCTWLLItem_t * OCTWLL_qpIth (const OCTWLList_t * const apList, int aiIndex);
/**========================================================================
* @}
* \name Status Report
* @{
*//*======================================================================*/
bool OCTWLL_qboolEmpty (const OCTWLList_t * const apList);
bool OCTWLL_qboolIsFirst (const OCTWLList_t * const apList);
bool OCTWLL_qboolIsLast (const OCTWLList_t * const apList);
bool OCTWLL_qboolHas (const OCTWLList_t * const apList, const OCTWLLItem_t * const apItem);
bool OCTWLL_qboolValidIndex(const OCTWLList_t * const apList, int aiIndex);
int OCTWLL_qiIndex (const OCTWLList_t * const apList);
int OCTWLL_qiIndexOf (const OCTWLList_t * const apList, const OCTWLLItem_t * const apItem);
/**========================================================================
* @}
* \name Cursor Movement
* @{
*//*======================================================================*/
void OCTWLL_Start (OCTWLList_t * const apList);
void OCTWLL_Finish (OCTWLList_t * const apList);
void OCTWLL_Forth (OCTWLList_t * const apList);
void OCTWLL_Back (OCTWLList_t * const apList);
bool OCTWLL_qboolAfter (const OCTWLList_t * const apList);
bool OCTWLL_qboolBefore (const OCTWLList_t * const apList);
bool OCTWLL_qboolOff (const OCTWLList_t * const apList);
void OCTWLL_CircularForth (OCTWLList_t * const apList);
void OCTWLL_CircularBack (OCTWLList_t * const apList);
void OCTWLL_GoTo (const OCTWLLItem_t * const apItem);
void OCTWLL_GoIth (OCTWLList_t * const apList, int aiIndex);
/**========================================================================
* @}
* \name Element Change
* @{
*//*======================================================================*/
void OCTWLL_Put (OCTWLList_t * const apList, OCTWLLItem_t * const apItem);
void OCTWLL_PutLeft (OCTWLList_t * const apList, OCTWLLItem_t * const apItem);
void OCTWLL_PutRight (OCTWLList_t * const apList, OCTWLLItem_t * const apItem);
void OCTWLL_PutBeforeItem (OCTWLLItem_t * const apAnchorItem, OCTWLLItem_t * const apItem);
void OCTWLL_PutAfterItem (OCTWLLItem_t * const apAnchorItem , OCTWLLItem_t * const apItem);
void OCTWLL_PutFront (OCTWLList_t * const apList, OCTWLLItem_t * const apItem);
void OCTWLL_Extend (OCTWLList_t * const apList, OCTWLLItem_t * const apItem);
void OCTWLL_Replace (OCTWLList_t * const apList, OCTWLLItem_t * const apItem);
void OCTWLL_MergeLeft (OCTWLList_t * const apList, OCTWLList_t * const apOtherList);
void OCTWLL_MergeRight (OCTWLList_t * const apList, OCTWLList_t * const apOtherList);
void OCTWLL_Append (OCTWLList_t * const apList, OCTWLList_t * const apOtherList);
void OCTWLL_Prepend (OCTWLList_t * const apList, OCTWLList_t * const apOtherList);
/**========================================================================
* @}
* \name Removal
* @{
*//*======================================================================*/
void OCTWLL_RemoveItem (OCTWLLItem_t * const apItem);
void OCTWLL_Remove (OCTWLList_t * const apList);
void OCTWLL_RemoveLeft (OCTWLList_t * const apList);
void OCTWLL_RemoveRight (OCTWLList_t * const apList);
void OCTWLL_WipeOut (OCTWLList_t * const apList);
/*=========================================================================
* @}
* \name Initialization
* @{
*//*======================================================================*/
void OCTWLL_InitList (OCTWLList_t * const apList);
void OCTWLL_InitItem (OCTWLLItem_t * const apItem, void * apOwner, int aiValue);
/** @} */
Note that these functions follow a pattern for any (generic) LIST-type data structure, but the implementation is of course specific to the particular data structure implemented in the wgOrderedCircularTwoWayLinkedList module.
As you can see, studying this list all by itself gives the client programmer ALMOST all he needs to correctly use the module. Once the programmer has studied the documentation on how to use the module at the top of the .C file, in most cases thereafter he will only need to refer to this prototype list in the .H file to use it for years to come.
1.21. Module Documentation
Documentation about the module and how to use it is located at the top of the module’s .C file. Documentation about how to use each function is located above the function DEFINITION (in the .C file), leaving the .H file’s prototype list VERY much more readable and therefore more able to accomplish the objective of quickly giving the client programmer an overview of the API, as illustrated above.
1.22. Use Doxygen Documentation
Documentation of module and functions use Doxygen documentation, enabling the generation of any external documentation (including documentation as complex as a book or website) to be generated from the code itself. This is specifically aimed at adherence to the SELF-DOCUMENTATION PRINCIPLE from the bible of Object-Oriented software: OOSC2, for all the reasons it states.
1.23. Things That Happen Once
For “things” that you only want to happen ONCE when they change, there is a “previous” value that is typically a local-static variable so that it is clear for one and all that management of that variable is guaranteed to be managed within the scope of the static variable.
When detecting a change in that value that you want to respond to just ONCE when it changes, prefer updating the ...Prev... value IMMEDIATELY after the conditional logic that detects the change, UNLESS the old (previous) value is needed for something within that block of code.
Example:
Normally:
if (lsui8PrevGear != gui8LastKnownGear) {
gui8LastKnownGear = lsui8PrevGear;
// Do "the thing" just ONCE when it changes here.
}
And this only when the old value is needed within the block:
if (lsui8PrevGear != gui8LastKnownGear) {
// Do "the thing" just ONCE when it changes here
// when `lsui8PrevGear` is needed in this logic.
gui8LastKnownGear = lsui8PrevGear;
}
That preference makes it more clear in the code how that variable is being managed by keeping the lines of code that manage it close together, except when absolutely necessary that they are spread apart.
1.8. Comments
Comments should be clear and precise, and for purposes of clarity, contain complete sentences and use proper grammar and punctuation.
Assume the reader knows the C programming language. But do not assume reader knows:
the over-arching design of the subsystem involved, especially if complex; and
what is in your mind that is not visible in the code!
Comments should give orientation to the problem being solved, as well as the rationale behind doing certain things the way they are being done, including (and especially) assumptions being made by the coder. Remember: the purpose of these comments is to allow another programmer later on (which may be you) to quickly understand your code and the rationale behind it, because he will need this in order to FULLY understand it. And FULL understanding is a MUST before making changes to any code!
When code implements a complex alogirthm, the algorithm should be thoroghly documented somewhere, typically in the header comment over the function definition, or in the header of the module if the algorithm is central to the module itself. When code is implementing such an algorithm, some of the most useful comments label the major steps of the algorithm without re-explaining it, but instead, referring back to the algorithm’s documentation.
The other side of that coin is that comments should NOT be used when the code and its rationale are already clear. Avoid explaining the obvious, and specifically avoid writing unhelpful and/or redundant comments.
Whenever a flow chart, truth table, sequence diagram, Warnier-Orr diagram, or other diagram is needed to sufficiently document code, the drawing shall be maintained, whenever possible, directly in the source code above the applicable function or group of functions it applies to, or in the module header comments if it is applicable to the entire module. If it must be contained in an external file, then it should reside with the applicable source code, under version control, and the comments should reference the diagram by file name or title.
Function-header comments are always included when a function is going to be used by others, e.g. when it is part of an API. But even when a function is only used internally, if the function’s use, arguments and/or calling constraints are not already clear from the code, it should still be documented. Sometimes why the function is there is more important that the function’s signature or return value, since that can often be gleaned directly from the code.
Calling constraints are best documented by actual preconditions (actual assertions in the code testing that the calling conditions have been met), but if and when they cannot be in preconditions, they should be well documented in the function header comments, even when the function is only used internally.
Function-header comments become even more important in library source code. When present, function-header comments should not only provide the specification for that function (i.e. clear description of what it does [or returns], specifications of arguments, return values, side effects [if any]), but should also contain the design rationale for that specific function and under what circumstances it should be called, i.e. what problem it solves. This helps readers know when to use it, and just as importantly, when not to. If the design rationale is already clearly covered in the header comments, then minimally a “See _____ section in file header comments for more details” statement should be present. Note that such comments can be as short as one line, or as complex as necessary to ensure the client of such functions fully understands HOW and WHEN to call that function.
Overall design rationale (related to the subsystem contained within that module) should be in the file header comments. When there are a matching pair of .C and .H files, such comments are written into the .C file, not the .H file. For libraries where the end user of that library will not have access to the .C file, such comments are moved to the .H file, and function-documentation is provided above the exported (public API) prototype declarations.
Comment formats should support documentation tools such as Doxygen. See example function header comment block below in the “Documentation Policies” section below.
Use the following capitalized comment markers to highlight important issues:
“WARNING:” alerts a maintainer there is risk in changing this code. For example, that a delay loop counter’s terminal value was determined empirically and may need to change when the code is ported, optimization level changed, or SYSCLK frequency changed.
“NOTE:” calls attention to the “why” of a chunk of code, as distinguished from the “how” usually placed in comments. For example, that a chunk of driver code deviates from the data sheet because there was an errata in the chip. Or that an assumption is being made by the original programmer.
“TODO:” indicates an area of the code is still under construction and explains what is to be done. When appropriate, the programmer initials may be placed immediately after the colon so that when sorted alphabetically, the TODO’s for a programmer will be grouped together. This also can be done with development topics or tasks so that related TODO’s in a list can be grouped together. Example: TODO:GPS_EFFICIENCY ...description... might apply to a part of a task to increase CPU efficiency in code that manages GPS data flow.
End-of-line comments should only be used when the meaning of that one line of code may be unclear from the variable and function names and operations alone, but where a short comment makes it clear. Note, however, that since we like to compile with the ANSI C restriction ON, and since end-of-line comments (//) are not allowed in ANSI C, we use end-of-line comments only when absolutely necessary, and when we do, we make them block comments like this:
End-of-line comments start at column 50 when feasible. Exception: if doing otherwise significantly improves readability and therefore understandability of the code. Once again, Purpose is sensior to Policy.
If a group of end-of-line comments are used on lines of code that are close together, the beginnings of these comments are left-aligned, and always start at least 2 spaces after the longest code line of the group. Example:
Doxygen uses back-quotes to switch the text temporarily into a monospaced font. For this reason, use back-quotes instead of single- or double-quotes to surround things that are intended to be interpreted as source code, such as variable or function names. To be consistent, use this in all types of comments, not just Doxygen comments.
See also
Documentation