-
Notifications
You must be signed in to change notification settings - Fork 6
Embedded C Coding Guidelines
Tim Brewis edited this page Jun 14, 2022
·
6 revisions
- Match ThreadX / STM32 naming conventions (most of the time) for consistency.
- Variables named in
snake_case
. - Pointers end with
_ptr
. - Functions named in
snake_case
. - Struct typedefs end with
_t
. - Functions which operate on structs prefixed with the name of the struct. For example:
foo_create(foo_t* foo_ptr)
. - Macros and defines in
ALL_CAPS
.
Use trunk fmt
to format code pre-commit (see README).
- Include the file header below at the start of every file.
/*************************************************************************** * @file my_file.c * @author An Author (aa1g18@soton.ac.uk) * @author Different Author (da1g19@soton.ac.uk) * @date 2021-11-30 * @brief Brief description of the file / module ***************************************************************************/
- Use Doxygen comment tags for functions.
/** * @brief A brief description of the function * * @details If relevant, extended details about the implementation of the * function and its usage. * * @param[in] x A description of an input parameter * @param[out] f A description of an output parameter * * @return A description of the return value */ int my_function(int x, int f) { // ... }
- Put the documentation in the source files and not the headers (this encourages updating comments when the implementation changes).
Header and source files:
- Create a header and corresponding source file for each new module in the system.
- Put include guards in all headers.
- Try to only put what is absolutely essential for the interface of a module in the header files. Imagine that functions with prototypes in the header are analogous to
public
functions in a C++/Java/etc class and functions with prototypes in the source file areprivate
. - Only
#include
other headers inside a header if it will be required for the interface. If the header is only required for internal implementation, just include it in the source file. - If functions are only required for the internal implementation of a module, put them only in the source file and declare them as
static
. - Don't declare instances of global variables in headers, prefer to extern them and create them in the source file.
Macros:
- Restrict the scope of macros as much as possible.
- Use function-like macros instead of actual functions where the result can be computed entirely at compile time (e.g. squaring a compile time constant). Prefer to use functions when the result cannot be computed at compile time (e.g. squaring a runtime variable).
- Don't abuse the pre-processor!
Dynamic memory allocation:
- Never use
malloc()
/calloc()
/free()
. Use of these functions is widely discouraged in embedded systems, especially in safety-critical applications. - Instead, prefer to use fixed sized buffers which have a known size at compile time.
- If you really need dynamic memory allocation, you can create a fixed size memory pool in ThreadX and use
tx_byte_allocate()
and related functions to allocate / free memory from that pool.
Structs:
- It can be helpful to think of structs as "classes with only public members and no (built-in) methods".
- Even though C is not an object-oriented language, it is common to see similar principles to classes applied to structs by creating functions that operate on struct pointers. Consider the following example which shows how you might create a data class, initialise it, operate on it and retrieve its properties using structs.
/** * @brief A rectangle */ typedef struct { int x, y, w, h; } rect_t; /** * @brief Initialise a rectangle * * @param[in] rect_ptr Pointer to a rectangle * @param[in] x x coordinate of the top left corner * @param[in] y y coordinate of the top left corner * @param[in] w Width * @param[in] h Height */ void rect_init(rect_t* rect_ptr, int x, int y, int w, int h) { rect_ptr->x = x; rect_ptr->y = y; // ... } /** * @brief Calculates the area of a rectangle * * @param[in] rect_ptr Pointer to a rectangle * * @return The area of the rectangle */ int rect_area(rect_t* rect_ptr) { return (rect_ptr->w * rect_ptr->h); } /** * @brief Shifts a rectangle's position in the Cartesian plane * * @param[in] rect_ptr Pointer to a rectangle * @param[in] x_shift Shift in the x-direction * @param[in] y_shift Shift in the y-direction */ void rect_shift(rect_t* rect_ptr, int x_shift, int y_shift) { rect_ptr->x += x_shift; rect_ptr->y += y_shift; }
Workspace structure:
- The IDE generates the folder
Core
which has a bunch of auto-generated config code in it. Generally we don't want to touch anything in there. - Convention is to have a folder named after your organization (we have the
SUFST
folder). -
SUFST/Inc
for header files. -
SUFST/Src
for source files.
IOC files:
- IOC files are used by the STM32Cube toolchain to automatically configure the microcontroller based on the specifications of the file.
- This allows a lot of complex microcontroller configuration to be handled by automatic code generation.
- In files generated by the IDE, there will be comment blocks named something line
/* USER CODE BEGIN PTD */
followed by/* USER CODE END PTD */
. Code within these blocks will not be overwritten by automatic code generation but ALL other code outside them will be. - These files are notoriously bad for causing git conflicts and unfortunately there isn't a quick solution to resolve them.