Skip to content

silvio3105/Template3105

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

65 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ABOUT

This repo contains a template I use for application projects. Template includes:

  • Makefile template with RTOS support for building the application
  • RTX5 files
  • Readme file
  • License file
  • Git ignore file
  • Project folder structure
  • Set of coding rules I follow in embedded software development

APPLICATION LAYERS

Application layers diagram

The diagram shows the application layers.

Hardware

The base for every application. It is only physical layer.

Drivers

Drivers are the gate for other layers of the application to interact with the hardware. They are written with minimal logic inside. Every driver is written without the other driver(s). The only way for the driver to interact with hardware(eg., I2C sensor, SPI flash, GPIO, etc.) is through an external handlers(user provided). Because of that, drivers are not fixed to certain MCU or SDK/framework. Drivers are not written with application logic. Drivers can interact with libraries. Every driver is written as a C++ class within its own namespace.

Libraries

Libraries are pieces of software with basic logic and do not require interactions with the hardware. Every layer can use libraries. Libraries are not written with application logic. Every library is written within its own C++ namespace. Eg., a library with string manipulation functions.

Modules

Modules are pieces of application logic. Each module combines drivers and libraries. Eg., ADC module will interact with ADC driver and provide access to voltage info for other modules. So other modules do not care about how voltage is measured.

Application

Application layer contains all RTOS tasks(or just main "task" in bare metal). If RTOS is used, every module will have its own task or will share task with other module.

PROJECT FOLDER STRUCTURE

Application project folder structure

  • đź“‚ {Project_name}: Root folder.
    • đź“‚ .builds: Folder with per hardware build folders.
      • đź“‚ {Build_name}: Folder for build type.
    • đź“‚ .git: Git folder.
    • đź“‚ .jlink: Folder with J-Link scripts for flash and erase.
    • đź“‚ .releases: Folder with stable releases(one release per folder).
      • đź“‚ RC: Folder with release candidate releases(one release per folder).
    • đź“‚ .vscode: Folder with VS Code config files.
    • đź“‚ Application: Folder with application layer source files.
      • đź“‚ Inc: Folder with application layer header files.
    • đź“‚ CMSIS: Folder with CMSIS-related files.
    • đź“‚ Config: Folder with application configuration files.
      • AppConfig.hpp: Header file with application config(shared between different hardware builds).
      • AppConfig: Make file with application build config(shared between different hardware builds).
    • đź“‚ Documentation: Folder with application documentation generated with Doxygen and files used for documentation.
    • đź“‚ Drivers: Folder with driver source files.
      • đź“‚ Inc: Folder with driver header files.
    • đź“‚ Libraries: Folder with library source files.
      • đź“‚ Inc: Folder with library header files.
    • đź“‚ Linker: Folder with linker scripts.
    • đź“‚ Make: Folder with per hardware Make files(one Make file for every hardware build or application type).
    • đź“‚ MCU: Folder with per MCU-related source files.
      • đź“‚ {MCU type}: Folder with MCU-related source files(eg., STM32F401C8).
        • đź“‚ Inc: Folder with MCU-related header files.
    • đź“‚ Modules: Folder with module source files.
      • đź“‚ Inc: Folder with module header files.
    • đź“‚ RTOS: Folder with RTOS source files.
      • đź“‚ Inc: Folder with RTOS header files.
      • đź“‚ IRQ: Folder with RTOS IRQ files.
    • .gitignore: List of items for Git to ignore.
    • Doxyfile: Doxygen project file.
    • LICENSE: Project license.
    • Main.cpp: Main source file with application entry point.
    • main.h: Legacy main header file.
    • Main.hpp: Main header file.
    • README.md: Project readme file.

Driver/library project folder structure

  • đź“‚ {Project_name}: Root folder.
    • đź“‚ .git: Git folder.
    • đź“‚ .vscode: Folder with VS Code config files.
    • đź“‚ Documentation: Folder with project documentation generated with Doxygen and files used for documentation.
    • đź“‚ Example: Folder with project example files.
    • .gitignore: List of items for Git to ignore.
    • Doxyfile: Doxygen project file.
    • LICENSE: Project license.
    • README.md: Project readme file.
    • {Project_name}.cpp: Driver/library source file.
    • {Project_name}.hpp: Driver/library header file.

VERSIONING & NAMING

Software versioning

  • Library/Driver: vX.Y(rcA)

    • Y: Minor version number. Starts from zero. Cannot go over 99. With leading zero(if Y is not zero). Increased by some amount with new features. Resets to zero when X increases.
    • X: Mayor version number. Can start from zero. Cannot go over 99. Without leading zero. Increased when Y overflows or with big update(eg., reworked project etc...).
    • rc: Stands for release candidate which means test release(version is not production ready).
    • A: Release candidate number. Starts from one. Cannot go over 99. Without leading zero. Increased by one with every new release candidate.

    X = 0 means the software does not contain all planned features for the first full release - beta phase (not the same as the release candidate). Examples:

    • v0.01rc5 Release candidate #5 for version 0.01.
    • v1.13 Stable release, version 1.13.
  • Application: vX.Y.Z(rcA)

    • Z: Build number. Starts from zero. Cannot go over 99. With leading zero(if Z is not zero). Increased by some amount by bug fixes. Resets to zero when Y increases.
    • Y: Minor version number. Starts from zero. Cannot go over 99. With leading zero(if Y is not zero). Increased by one when Z overflows or new features are introduced. Resets to zero when X increases.
    • X: Mayor version number. Can start from zero. Cannot go over 99. Without leading zero. Increased by one when Y overflows or when big changes are introduced.
    • rc: Stands for release candidate which means test release.
    • A: Release candidate number. Starts from one. Cannot go over 99. Without leading zero. Increased by one with every new release candidate.

    X = 0 means the software does not contain all planned features for the first full release - beta phase (not the same as the release candidate). Examples:

    • v0.13.18rc8 Release candidate #8 for version 0.13.18
    • v13.12.0 Stable release, version 13.12.0

Version timeline

v1.10.32 -> v1.11.0rc1 -> v1.11.0rc2 -> v1.11.0rc3 -> v1.11.0 -> v1.11.01

Release naming

Naming rule is: {app_name}_{app_version}(_{HW}) The rule also applies to naming application executables files(.bin and .hex). Application name contains project name and application type tag, eg., 3DCLK-FW is the name of firmware(FW) for 3D Clock. 3DCLK-BL is the name of the bootloader(BL) for 3D Clock. The application name is max 16 chars long. The firmware version is copied from the software versioning rule. _HW is inserted in the case when release is for specific hardware, eg., hardware 22-0091rev1. One release can have multiple executables. Examples:

  • 3DCLK-FW_v0.13.18rc3 is release name for 3D Clock firmware, version 0.13.18, release candidate 3.
  • 3DCLK-FW_v1.0.50rc1_22-0091rev1 is name of executables for hardware version 22-0091rev1, release 3DCLK-FW_v1.0.50rc1 for 3D Clock, firmware version v1.0.50, release candidate 1.

File naming

Every file is named with first uppercase letter(Main.cpp, not main.cpp). Files made for C++ have a .hpp header file, while C files have a .h header file.

TOOLS

List of the tools I use (Windows 10 Pro x64):

CODE RULES

Indents

I prefer to use tabs for indents, size 4.

C++ over C!

I prefer to use C++ over C, but only parts of C++ that do not induce runtime overhead and bloat(except templates), eg., classes, namespaces, and enum classes!

Code layout

This is only a basic layout for source and header files. Layout depends on case-to-case and it is prone to changes.

  • SourceFile.cpp:

    Defines, macro functions, enums, typedefs, structs, and classes in the translation unit means they are intended only and only for that translation unit.

     /**
      * @file SourceFile.cpp
      * @author silvio3105 (www.github.com/silvio3105)
      * @brief This is template source file.
      * 
      * @copyright Copyright (c) 2025, silvio3105
      * 
      */
    
     // ----- INCLUDE FILES
    
    
     // ----- DEFINES
    
    
     // ----- ENUMS
    
    
     // ----- TYPEDEFS
    
    
     // ----- STRUCTS
    
    
     // ----- CLASSES	
    
    
     // ----- VARIABLES
    
    
     // ----- STATIC FUNCTION DECLARATIONS
    
    
     // ----- FUNCTION DEFINITIONS
    
    
     // ----- STATIC FUNCTION DEFINITIONS
    
    
    
     // END WITH NEW LINE
    
  • SourceFile.hpp

     /**
      * @file SourceFile.hpp
      * @author silvio3105 (www.github.com/silvio3105)
      * @brief This is template header file.
      * 
      * @copyright Copyright (c) 2025, silvio3105
      * 
      */
    
     #ifndef _SOURCEFILE_HPP_
     #define _SOURCEFILE_HPP_
    
     // ----- INCLUDE FILES
    
    
     // ----- DEFINES
    
    
     // ----- MACRO FUNCTIONS
    
    
     // ----- NAMESPACES
    
    
     // ----- ENUMS
    
    
     // ----- TYPEDEFS
    
    
     // ----- STRUCTS
    
    
     // ----- CLASSES
    
    
     // ----- EXTERNS
    
    
     // ----- FUNCTION DECLARATIONS
    
    
     #endif // _SOURCEFILE_HPP_
    
    
     // END WITH NEW LINE
    

Code example

Pointers and references are written like this:

uint8_t* ptr8 = nullptr;

void foo(uint16_t& argRef);

not like this:

uint8_t * ptr8 = nullptr;
uint8_t *ptr8 = nullptr;

void foo(uint16_t & argRef);
void foo(uint16_t &argRef);

After comma should be space, so uint8_t foo(void* ptr, uint16_t len); not uint8_t foo(void* ptr,uint16_t len);. Same applies to arrays.

Here's complete code example:

/*
    This is a comment block.
    The comment block is a multiline comment.
*/

// This is an inline comment

/*
    Defines are written in uppercase and space is replaced with underscore.
    Since defines does not have "namespace", every define should start with a module abbreviation, eg., "#define FWCFG_GSM_UART USART1".
    If a macro contains multiple elements(eg., another macro), its value is placed between ().
*/
#define THIS_IS_MACRO					value
#define THIS_IS_SECOND_MACRO			(THIS_IS_MACRO - 5)

/*
    The macro function is written in uppercase, it starts with two underscores, and spaces are replaced with underscores.
    Argument names start with underscore and the first letter is in lowercase.
    The function body is written in a new line.
*/
#define __THIS_IS_MACRO_FUNCTION(_argOne, _argTwo) \
    ((_argOne) - (_argTwo))

/*
    C-style enum type is written in lowercase, spaces are replaced with underscores and the type name ends with "_t".
    Enum values are written like defines.
    Enum definition also contains data size(uint8_t, uint16_t, etc..).
    Every value starts with an abbreviation(eg., "GSM_ERROR") if not placed inside the namespace.
*/
enum enum_type_t : uint8_t
{
    ENUM_ONE = 0,
    ENUM_TWO
};

/*
    Same as classes:
    Enum value names in the enum class are named with uppercase letters for every word.
    Value names do not start with an abbreviation(eg., "ERROR", not "GSM_ERROR").
*/
enum class EnumClass_t : uint16_t
{
    EnumOne = 0,
    EnumTwo
};

// Type alias is written using the same rules as enum classes. 
typedef uint16_t Idx_t;

// Same as typedef above but it ends with "_f".
typedef void (*ExtHandler_f)(void);

/*
    Struct type uses old C style, same as C style enums.
    Struct members are named using rules for global variables.
    Each member should have a default value.
    Structs are used only for data storage(no functions).
*/
struct this_is_struct_t
{
    uint8_t someVar = 1;
    uint32_t* somePtr = nullptr;
};

/*
    Classes hold data(as structs) and methods(not functions!) to manipulate the data.
    The class name is written with the uppercase first letter of every word in the name.
    Only the private part of the class contains variables. To get or set variables from outside, getter and setter methods are used.
    Variables and methods in class use naming rules from global variables and functions 
*/
/**
 * @brief Class brief.
 * 
 * Class description.
 */
class ThisIsClass
{
    public:
    ThisIsClass(void);
    ~ThisIsClass(void);

    uint8_t somePublicFunction(void);

	protected:
	// Here goes protected stuff (between public and private)

    private:
    char someArray[] = "Array"; /**< @brief This is inline doxygen comment. */

    inline void somePrivateFunction(void);
};

// Classic extern
extern volatile uint8_t someVaraible;

/*
    Global variable name starts with module abbreviation if not in namespace.
    Module abbreviation is written in lowercase while every other word starts in uppercase.
    The variable should have a default value.
*/
uint32_t thisIsVariable = 0;

/*
    Global function name starts with module abbreviation if not in namespace.
    Module abbreviation is written in lowercase while every other word starts in uppercase.
*/
/**
 * @brief Function brief.
 * 
 * Function description.
 * 
 * @param argOne Some argument.
 * @param argsList Some argument.
 * @param varRef Some argument.
 * 
 * @return No return value.
 */
void someFunction(const uint8_t argOne, uint16_t* argsList, uint32_t& varRef);

// Namespace uses naming rules from classes. Content in the namespace uses the same global type rule.
namespace SomeNameSpace
{
    // VARIABLES
    uint64_t xVar;

    // FUNCTIONS
    void setFoo(char someChar);
};

Code workflow comments

For some reason, I like to add a bunch of "workflow comments". Workflow comments describe what the lines below (comment) do. I tend to "group" lines of code into little sections.

void foo(void)
{
	float tmp = 0.00f; // Every float value has two decimal digits and f to indicate float, not double
	uint16_t x = 1;
	char str[32] = { '\0' };
	uint32_t* ptr = nullptr;

	// Execute something with value x
	exe(x);

	// Calculate result and convert it
	foo2(ptr, x);
	tmp = 2.00f * (*ptr);

	// Do something with value
	if (tmp > 10.00f)
	{
		tmp = 10.00f;
	}
	else
	{
		tmp -= 0.55f;
	}

	// Check does string exist
	if (str[0])
	{
		// Do something with string		
	}
	else // String does not exist, abort
	{
		return;
	}

	// Rest of the function...

}

Nested if statments?

I prefer less nested code. If I can check requirements before the function does its job, I do it.

Short examples:

void foo(void)
{
	// Check if device is online
	if (isOnline())
	{
		// Check if data is ready
		if (isReady())
		{
			// Send data
			bar();
			bar2();
		}
		else
		{
			print("Not ready\n");
		}
	}
	else
	{
		print("Not online\n");
	}
}
void foo(void)
{
	// Abort if not online
	if (!isOnline())
	{
		print("Not online\n");
		return;
	}

	// Abort if data is not ready
	if (!isReady())
	{
		print("Not ready\n");
		return:	
	}

	// Send data
	bar();
	bar2();
}

Single or multi line if statment

For the sake of easy debugging I prefer multi line if statments.

if (statment)
{
	foo();
}

not

if (statment) foo();

or

if (statment)
	foo();

Curly bracket placment

Every curly bracket is in new line.

function()
{
	someCodeHere;
	foo();
	return 1;
}

Not like this(or any variation where curly bracket is not in new line)

function() {
	someCodeHere;
	foo();
	return 1;
}

Except for simple arrays.

uint8_t arr[] = { 1, 2, 3, 4, 5 };

Declarations & Definitions

Declarations are placed in header files(.hpp).
Definitions and private (static) stuff are placed in translation units(.cpp).
Inline and template stuff are defined in header files.

DEBUG

Debug levels:

  • Verbose: This level is used to print debug stuff only when all information is needed, eg., measured sensor value each second.
  • Info: This level is used to print debug stuff for events/actions, eg., when measured value from the sensor is above or below defined threshold. Will not enable verbose level.
  • Error: This level is used to print debug stuff only when something fails, eg., when sensor init fails. Will not enable info and verbose levels.

Global debug build flags:

  • DEBUG: Main flag for debug build. If not defined during compile/build, content/body of debug print handlers will be empty.
  • DEBUG_SRC: Flag for header file name with declarations of debug handlers.
  • DEBUG_PRINT(_L)=log: Flag with the name of print handler for constant strings. Function is declared as void (const char* string, const uint16_t len = strlen(string));. It is possible to set print handler per debug level.
    • DEBUG_PRINT_VERBOSE: Print handler for verbose debug level.
    • DEBUG_PRINT_INFO: Print handler for info debug level.
    • DEBUG_PRINT_ERROR: Print handler for error debug level.
  • DEBUG_PRINTF(_L)=logf: Flag with the name of print handler for fromatted strings. Function is declared as void (const char* string, ...);. It is possible to set print handler per debug level.
    • DEBUG_PRINTF_VERBOSE: Print handler for verbose debug level.
    • DEBUG_PRINTF_INFO: Print handler for info debug level.
    • DEBUG_PRINTF_ERROR: Print handler for error debug level.

Local debug build flags:

  • DEBUG_X: Enable debug for the driver/libryry/module X. Eg., DEBUG_ILPS22QS will enable debug for ILPS22QS driver.
  • DEBUG_X_L: Flag to enable debug of certian level in certian driver/library/module. X is the name of the drvier/library/module, eg., ILPS22QS and L is debug level(VERBOSE, INFO, ERROR).

Debug-related code have to be removed in non-debug build. Debug implementation example:

#ifdef DEBUG_SRC
#include 			""DEBUG_SRC""
#endif // DEBUG_SRC

#ifdef DEBUG_ILPS22QS

#ifdef DEBUG_ILPS22QS_VERBOSE
	#ifdef DEBUG_PRINT_VERBOSE
	#define DEBUG_ILPS22QS_PRINT(...) \
		DEBUG_PRINT_VERBOSE("[ILPS22QS] ", 11); \
		DEBUG_PRINT_VERBOSE(...)
	#else // DEBUG_PRINT_VERBOSE
	#define DEBUG_ILPS22QS_PRINT(...) \
		DEBUG_PRINT("[ILPS22QS] ", 11); \
		DEBUG_PRINT(...)
	#endif // DEBUG_PRINT_VERBOSE

	#ifdef DEBUG_PRINTF_VERBOSE
	#define DEBUG_ILPS22QS_PRINTF(...) \
		DEBUG_PRINTF_VERBOSE("[ILPS22QS] "); \
		DEBUG_PRINTF_VERBOSE(...)
	#else // DEBUG_PRINTF_VERBOSE
	#define DEBUG_ILPS22QS_PRINTF(...) \
		DEBUG_PRINTF("[ILPS22QS] "); \
		DEBUG_PRINTF(...)
	#endif // DEBUG_PRINTF_VERBOSE	
#else // DEBUG_ILPS22QS_VERBOSE
#define DEBUG_ILPS22QS_PRINT(...)
#define DEBUG_ILPS22QS_PRINTF(...)
#endif // DEBUG_ILPS22QS_VERBOSE

#ifdef DEBUG_ILPS22QS_INFO
	#ifdef DEBUG_PRINT_INFO
	#define DEBUG_ILPS22QS_PRINT_INFO(...) \
		DEBUG_PRINT_INFO("[ILPS22QS] info: ", 17); \
		DEBUG_PRINT_INFO(...)
	#else // DEBUG_PRINT_INFO
	#define DEBUG_ILPS22QS_PRINT_INFO(...) \
		DEBUG_PRINT("[ILPS22QS] info: ", 17); \
		DEBUG_PRINT(...)
	#endif // DEBUG_PRINT_INFO

	#ifdef DEBUG_PRINTF_INFO
	#define DEBUG_ILPS22QS_PRINTF_INFO(...) \
		DEBUG_PRINTF_INFO("[ILPS22QS] info: "); \
		DEBUG_PRINTF_INFO(...)
	#else // DEBUG_PRINTF_INFO
	#define DEBUG_ILPS22QS_PRINTF_INFO(...) \
		DEBUG_PRINTF("[ILPS22QS] info: "); \
		DEBUG_PRINTF(...)
	#endif // DEBUG_PRINTF_INFO	
#else // DEBUG_ILPS22QS_INFO
#define DEBUG_ILPS22QS_PRINT_INFO(...)
#define DEBUG_ILPS22QS_PRINTF_INFO(...)
#endif // DEBUG_ILPS22QS_INFO

#ifdef DEBUG_ILPS22QS_ERROR
	#ifdef DEBUG_PRINT_ERROR
	#define DEBUG_ILPS22QS_PRINT_ERROR(...) \
		DEBUG_PRINT_ERROR("[ILPS22QS] error: ", 18); \
		DEBUG_PRINT_ERROR(...)
	#else // DEBUG_PRINT_ERROR
	#define DEBUG_ILPS22QS_PRINT_ERROR(...) \
		DEBUG_PRINT("[ILPS22QS] error: ", 18); \
		DEBUG_PRINT(...)
	#endif // DEBUG_PRINT_ERROR

	#ifdef DEBUG_PRINTF_ERROR
	#define DEBUG_ILPS22QS_PRINTF_ERROR(...) \
		DEBUG_PRINTF_ERROR("[ILPS22QS] error: "); \
		DEBUG_PRINTF_ERROR(...)
	#else // DEBUG_PRINTF_ERROR
	#define DEBUG_ILPS22QS_PRINTF_ERROR(...) \
		DEBUG_PRINTF("[ILPS22QS] error: "); \
		DEBUG_PRINTF(...)
	#endif // DEBUG_PRINTF_ERROR	
#else // DEBUG_ILPS22QS_ERROR
#define DEBUG_ILPS22QS_PRINT_ERROR(...)
#define DEBUG_ILPS22QS_PRINTF_ERROR(...)
#endif // DEBUG_ILPS22QS_ERROR

#else // DEBUG_ILPS22QS

#define DEBUG_ILPS22QS_PRINT(...)
#define DEBUG_ILPS22QS_PRINTF(...) 
#define DEBUG_ILPS22QS_PRINT_INFO(...)
#define DEBUG_ILPS22QS_PRINTF_INFO(...) 
#define DEBUG_ILPS22QS_PRINT_ERROR(...)
#define DEBUG_ILPS22QS_PRINTF_ERROR(...) 

#endif // DEBUG_ILPS22QS

Releases

No releases published

Packages

No packages published