Skip to content

Embedded C Coding Guidelines

Tim Brewis edited this page Jun 14, 2022 · 6 revisions

Naming conventions

  • 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.

Formatting

Use trunk fmt to format code pre-commit (see README).

Documentation

  • 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).

Embedded C Tips

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 are private.
  • 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;
    }

STM32Cube Projects

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.