AVRly - AVR Development Resources
Anatomy of Embedded Firmware

The style and layout guidelines shown here are based on my personal preference; of course you are free to develop your code in whichever way you choose - this is just a guide for users who are unsure how to correctly structure their projects, or for developers contributing to this site.

A Word on Doxygen

Throughout this repository, the code has been documented using Doxygen. It's an easy way to generate documentation which stays closely coupled with the code so it's not too difficult to keep the two in line. The html for this site was also generated using Doxygen. It relies on special comments like this:

/**
* Comment goes here
*/

However, normal C style comments like:

// Comment goes here

or

/*
* Comment goes here
*/

are ignored by Doxygen.

Comment lines starting with an @ symbol are special Doxygen commands.

Ordinarily it's best to only use Doxygen documentation for the public function declarations in the header file rather than the source file definitions, as we are usually documenting the API. If you were documenting source code for other developers you might want to document the implementation instead, but never both. If you provide a brief for both, then the one from the declaration will be used, the other will be ignored. If you provide a detailed description for both, the one for the definition is used and the other ignored.

Formatting

Leave a comfortable amount of whitespace between logical sections of code, to make things easy on the eye. Header, source, and Makefiles should all be limited to 80 characters wide.

Licence

At the top of each file, a Copyright notice and licence terms are added. This let's other users know whether they can use and distribute this software, and the terms under which they may do so. The software in this repository is released under the MIT licence.

/******************************************************************************
@copyright Copyright © YYYY by Your Name Here.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
******************************************************************************/

The licence and copyright notice are enclosed in a Javadoc style banner, which Doxygen will recognise as a banner comment. To use this stye of banner, JAVADOC_BANNER = YES must be set in the Doxyfile. Add your name and the year the work was produced in the copyright notice.

C Header Files

The purpose of a C header file is to provide public data which can be shared with other files. Users wanting to get a feel for how to use an API will first look at the header file, as the function signatures will provide information on how to use them.

A C header file should be named after the function of the module or the peripheral that it serves. The filename should be entirely in lowercase, and the file extension is .h. The aim is to expose as little information as possible in the header file - just the public function declarations and any macros or typedefs etc required by other files, without them having to know too much about how it actually works.

An example file can be found in the repo here, and the Doxygen output from these special comments and commands can be found here.

After the license, place a Javadoc style comment block with some special Doxygen commands: @file, @group, @author, @date, @brief, @bug and @see.

/**
* @file filename.h
* @author Your Name Here.
* @date 15th March 2022
* @brief The brief description goes here. Add an explanation of the purpose and
* limitations of the module, along with any other notes that may be useful to
* others using it.
* @bug Known bugs are declared and described here.
* @see "See also" links go here https://www.doxygen.nl/manual/docblocks.html
*/

The doxygen output from these special comments looks like this:

Next come the include guards - a preprocessor mechanism used to prevent the header file contents being included in other files multiple times.

#ifndef FILENAME_DOT_H
#define FILENAME_DOT_H
// Contents of header file goes here.
#endif // FILENAME_DOT_H

#include directives come next, with standard library headers listed first, then the local, project specific headers. You can think of these #include directives as inserting the whole contents of the header file referenced into that section of the file.

// Header file needed for fixed width integer types.
#include <stdint.h>

Public typedefs, structs, enums etc come next, along with any macro definitions.

Function prototypes/declarations are now listed, with the initialisation function first. In the Doxyfile, ensure JAVADOC_AUTOBRIEF = YES to use this style of commenting.

/**
* This is a Javadoc autobrief style comment. After the first full stop the text
* becomes a detailed description. Explain how to use the function here.
* Initialisation function names should start with 'init' and appear first.
* @param List the parameters here.
* @return Declare the return type here (not needed for void return type).
*/
void init_object(uint16_t value);
/**
* Other public function declarations come afterwards.
* @return Returns a 16 bit unsigned integer. Returns are documented like this.
*/
uint16_t get_value(void);
uint16_t get_value(void)
Other public function declarations come afterwards.
Definition: filename.c:87
void init_object(uint16_t value)
This is a Javadoc autobrief style comment.
Definition: filename.c:74

The file ends with closure of the header guards, two blank lines and an /*** end of file ***/ comment followed by a blank line.

// End of include guard.
#endif // FILENAME_DOT_H
/*** end of file ***/

C Source Files

C source files should have the same name as their respective header file (if they have one) all in lowercase, with the filename extension .c

The source file provides the implementation and definitions of the public functions listed in it's header file, usually along with some helper functions, structs, macros and variables private to the file.

An example file can be found in the repo here, and the Doxygen output from these special comments and commands can be found here.

Begin with a licence and copyright notice as before, and then the Doxygen commands to provide some information about the file.

/**
* @file filename.c
* @ingroup anatomy
* @author Your Name Here.
* @date 15th March 2022
* @brief The brief description goes here, keep it relatively short and to the
* point.
*
* After the brief, this text becomes the detailed description. Add an
* explanation of the purpose and limitations of the module, along with any
* other notes that may be useful to others using it.
* @bug Known bugs are declared and described here.
* @see "See also" links go here https://www.doxygen.nl/manual/docblocks.html
*/

#include directives come next, with standard library headers listed first, then the local, project specific headers. These serve as an instruction for the preprocessor to insert the contents of the named header file in that section of the source file.

// Standard library header files are declared first, with angular brackets.
#include <stdint.h>
// System header files are declared next, also with angular brackets.
// Define the path to search for these files in the Makefile.
#include <util/delay.h>
// Project specific includes come next, in quotation marks.
// These files are located in the project directory.
#include "pin_defines.h"
// Include the header file for this module, with the same name but .h
#include "filename.h"
The brief description goes here, keep it relatively short and to the point.

Next we have macro definitions.

/**
* Next come #define statements/macros. Append integer values with a 'U' to
* make them unsigned, as the default type is signed integer.
*/
#define DECIMAL_MACRO 855U

Then we have structs, enums, typedefs, and variable declarations and initialisations.

/**
* Try to keep the scope of variables limited to the file at least, though
* function level scope is usually better where possible.
*/
static uint16_t file_scope_variable = 0;

Next in line are function prototypes/declarations for private helper functions defined at the bottom of the file. They are declared ahead of any definitions to ensure they can be referenced throughout the file.

// Private helper function declarations go here.
void do_some_helpful_stuff(void)
Helper function definitions come last, though their declarations are at the top of the file.
Definition: filename.c:97

Now we have the definitions for the public functions, listed in the same order as they appear in the header file, with the initialisation routine first.

/*
* Note that in the C source files, I've switched back to regular C style comment blocks.
* This is so that Doxygen ignores them.
*/
void init_object(uint16_t value)
{
OBJECT_DDR |= (1 << OBJECT_GPIO); // Set object gpio as output.
OBJECT_PORT |= (1 << OBJECT_GPIO); // Set level of OBJECT_GPIO high.
// Do some other stuff with the parameters
}
/*
* Get value and return it without exposing a private variable.
*/
uint16_t get_value(void)
{
return file_scope_variable;
}
#define OBJECT_PORT
Defines the AVR port we have wired our peripheral to.
Definition: pin_defines.h:43
#define OBJECT_GPIO
Defines the GPIO number the peripheral is wired to.
Definition: pin_defines.h:53
#define OBJECT_DDR
Defines the Data Direction Register for the GPIO connected to our peripheral.
Definition: pin_defines.h:48

Lastly, we have the definitions of private helper functions. Since they are less likely to be needed by people using the software, they are located at the bottom of the file.

/**
* Helper function definitions come last, though their declarations are at the
* top of the file.
*/
{
// Body of function definition goes here.
}

The file is ended with two blank lines and an /*** end of file ***/ comment followed by a blank line.

/*** end of file ***/

pin_defines.h

Macro definitions for ports, pins, data direction registers and GPIOs should be listed in the pin_defines.h file. When you have a larger project, it's much clearer to be able to see the pinout all together in one place, rather than strewn across multiple files.

An example file can be found in the repo here, and the Doxygen output from these special comments and commands can be found here.

Again we start with the Javadoc style Doxygen comment block to provide some info about the file.

/**
* @file pin_defines.h
* @author Your Name Here
* @date 15th March 2022
* @brief Pin and port definitions for the project.
* @see Datasheets or other links can go here.
*/

Then we have our include guards, as with all .h files.

#ifndef PIN_DEFINES_DOT_H
#define PIN_DEFINES_DOT_H
// Contents of header file goes here.
#endif // PIN_DEFINES_DOT_H

This header is required to access pin and port names.

// Definitions for pins and ports are in this header file.
#include <avr/io.h>

Then we make our macro definitions for pins, ports and GPIOs - see datasheet for more on this.

/**
* Defines the AVR port we have wired our peripheral to.
*/
#define OBJECT_PORT PORTB
/**
* Defines the Data Direction Register for the GPIO connected to our peripheral.
*/
#define OBJECT_DDR DDRB
/**
* Defines the GPIO number the peripheral is wired to
*/
#define OBJECT_GPIO PB0

The file ends with closure of the header guards, two blank lines and an /*** end of file ***/ comment followed by a blank line.

// End of include guard.
#endif // PIN_DEFINES_DOT_H
/*** end of file ***/

main.c

main.c is where the int main() routine lives, and is the heart of the application.

An example file can be found in the repo here, and the Doxygen output from these special comments and commands can be found here.

It begins, as usual with the Javadoc style Doxygen comment block to provide some info about the file.

/**
* @file main.c
* @author Your Name Here
* @date 15th March 2022
* @brief Main source file where main() routine is called.
* @bug Known bugs are declared and described here.
* @see links to other files or docs go here.
*/

Followed by #include directives, (standard library headers first).

// Include our driver header file here.
#include "filename.h"

And then macro definitions.

/**
* Avoid using magic bumbers, use defines or typedef enums instead.
*/
#define INITIALISATION_VALUE 3000

Then we have structs, enums, typedefs, and variable declarations and initialisations.

uint16_t returned_value = 0;

Lastly, we have the main() routine. Initialisation routines are called on entry of the main routine, this is the Setup stage. Then we enter the while loop, and this runs continuously until the device is powered off or a reset condition is met. The return statement is needed for valid syntax, as the special function main() expects to return an integer value.

/**
* Main routine to be executed on MCU.
*/
int main()
{
// Initialisation routines go here.
while (1) // Loop forever
{
returned_value = get_value();
}
return 0; // This line is never reached.
}
#define INITIALISATION_VALUE
Avoid using magic bumbers, use defines or typedef enums instead.
Definition: main.c:39
int main()
Main routine to be executed on the MCU.
Definition: main.c:45

This application that will be run on the target MCU as a superloop.

The file is ended with two blank lines and an /*** end of file ***/ comment followed by a blank line.

/*** end of file ***/

Makefile

The Makefile contains "recipes" intended to simplify the process of building your application. With this, you only need to type make to compile and link your code, rather than manually listing the many options, flags, dependancies etc in the terminal each time.

An example file can be found in the repo here. They cannot be dosumented using Doxygen, so I prefer to use a help recipe instead. Type make help to see a list of commands available to the user.

The Makefile must always be named "Makefile". Keep basic user defined variables at the top of the file.

##########-----------------------------------------------------------##########
########## Project-specific Details ##########
########## Check these every time you start a new project ##########
##########-----------------------------------------------------------##########
MCU = atmega328p
F_CPU = 8000000UL
BAUD = 9600UL
## Also try BAUD = 19200 or 38400 if you're feeling lucky.
## A directory for common include files.
LIBDIR = /usr/local/
##########-----------------------------------------------------------##########
########## Programmer Defaults ##########
########## Set up once, then forget about it ##########
########## (Can override. See bottom of file.) ##########
##########-----------------------------------------------------------##########
PROGRAMMER_TYPE = avrisp
# extra arguments to avrdude: baud rate, chip type, -F flag, etc.
PROGRAMMER_ARGS = -b 19200 -P /dev/tty.usbmodem141201

If you add a new recipe for users to invoke from terminal, document it by adding it to the help recipe at the bottom of the Makefile.

Templates

The repository contains templates for each file type mentioned in this guide.