Unify Coding Standard

1 Introduction

1.1 Purpose of this document

The purpose of this document is to describe the coding standard used by the components of the Unify Host SDK. It is a modified version of the standard for Silicon Labs MCU customer facing firmware.

A unified coding standard is necessary to provide a professional look & feel in the source code that we provide to our customers. Furthermore, it reduces cognitive load, when you need to work with another group’s code. Coding style and preferences differ from person to person, which means that there will be rules in this document that you will disagree with. However, please adhere to the rules described here, as having a coding standard that you (somewhat) disagree with is better than having no coding standard.

1.2 Structure and wording of this document

This document contains three separate parts: Coding guidelines, coding style and documentation. The coding guidelines deal with the structure of your code. This part is split into a general and a C specific part. The coding style guide details how your code should look, e.g. brace style, comment style etc. Documentation deals with how you should document your code to make it easy for other people to use and modify the code.

1.2.1 Source Code

In this document, Source code is used as a common name for any C, C++ or header file from Silicon Labs that is being provided to customers. It applies to both manually created as well as autogenerated code.

1.2.2 Required

Some of the coding standards are required and some are recommendations. All required coding standards are tagged with Required.

1.2.4 Should

The word “should” is used throughout this document. In this context, “should” is used to indicate best practice. You should strive to meet these guidelines. However, it is possible to break these rules, if that results in a better end result.

1.3 Application of this standard

1.3.1 New modules

This standard applies unconditionally to new modules.

Please note that a new software module does not need to be a new full stack. This could be only a new part in an existing stack.

1.3.2 Existing modules

Existing components will continue to use the coding standard they are already using (there is still some differences between the various stacks).


2 General Guidelines

2.1 General Guidelines

We can’t put everything into these guidelines, for things not covered in the guidelines, try to make your code blend in with the code you are editing. Use your best judgment!

2.1.1 Use peer review

A coworker should review all code. In practice, pull requests are encouraged as it allows people to comment on your work easily and it makes it possible to have good discussions about specific lines of code.

2.1.3 Be a Boy Scout

Leave the source code in a better condition than you found it!

2.1.4 Avoid reinventing the Wheel

If somebody has implemented some functionality before, you should use that rather than developing it again from scratch. This could be a function in a standard library, or a module that was internally developed. Silicon Labs is a large company with many different groups developing software for various use. What you need might already exist! If the existing code doesn’t suit your needs exactly but is close, consider whether the required changes would make it more generally useful. If so, change it.

2.1.5 Refactor code

Try to refactor code instead of duplicating it. Extract common functionality into functions, parameterize or abstract differences, etc.

2.2 Testing

2.2.1 Testability

Design the code in a modular fashion that allows functional testing. If possible, create unit tests for your software.

2.2.2 Regression Test

It is encouraged to add a decent level of automated regression tests.

2.3 Embedded Coding

2.3.1 Write energy friendly code

The code should be energy friendly. This means taking advantage of hardware features available and always going down to the lowest energy mode possible. Try to always keep this in mind when writing energy aware code:

  • Make sure all hardware modules or functions that are not needed by the application are disabled or shut down as often as possible

  • Use DMA whenever possible and put the device in the lowest possible sleep mode

  • Faster code = more time to sleep

  • Avoid busy waiting unless absolutely necessary!

  • Remember: everything matters!

  • Energy aware code can get complicated and difficult to debug. That is why it adds more value!

  • Use Simplicity Studio or similar tool to profile the energy usage of your code. The results are often surprising.

2.3.2 Interrupts

Interrupts shall have as minimal processing as possible. If possible it should just send an event to a task for later processing. Having long interrupt processing times might cause problems when other time-critical pieces of code are blocked from executing (radio protocols being a prime example).


3 C Specific Guidelines

3.1 Compilers

3.1.1 C standards version (Required)

Unify uses GNU99 (C99 with GNU Extensions).

3.1.1.1 C++ standards version (Required)

Unify uses C++17 with GNU extensions.

3.1.4 Compile with warnings (Required)

Compiler warnings often indicate bugs that will only be seen at runtime and that may be very difficult to find during testing. For example, the compiler may warn you about using an uninitialized variable. This can be difficult to find during testing.

Use the compiler option -Wall to enable all warnings.

3.1.5 Treat warnings as errors (Required)

Warnings should be treated as errors so that the build does not complete until the warning if fixed. Having a zero tolerance for warnings will help keep our code cleaner and contain fewer bugs.

Use the compiler option -Werror to treat warnings as errors. This applies to builds that are part of “make test”. It does not apply to build mechanisms passed through to customer projects.

Exceptions to Werror have to be approved in code review and have their rationale documented. Exceptions should disable errors, but leave warning intact (-wno-error=some-kind-of-warning) unless the warning is so noisy as flood build output. In that case it should be disabled entirely. (-wno-some-kind-of-warning)

3.2 Preprocessor

3.2.1 Use header file guards (Required)

A header file guard must cover all header files unless they are intended to be included multiple times from the same source file (which should be rare and justified). The name of the guard is of the form:

upper case file name

where characters in the file name other than alphanumerics are replaced with underscores and leading underscores are stripped. Guards should guard all other code in the file except comment headers. In particular, they should guard extern declarations and #include statements.

Example

#ifndef MY_FILE_H
#define MY_FILE_H

#include <stdint.h>

extern volatile uint64_t jiffies;

// ...

#endif  // MY_FILE_H

3.3 Standard Libraries

3.3.1 Use of stdint.h and stdbool.h (Required)

Use stdint.h and stdbool.h types whenever possible. This makes types unambiguous, more portable and opens up compiler optimizations.

This means that you should always use C99 types like uint8_t. Do not use other types such as U8 or UINT8 unless needed to maintain compatibility with older code or third party library.

3.3.4 Do not use the standard C assert() (Required)

It is strongly recommended to not use the assert() function that comes with the standard C/C++ library. If the Silicon Labs libraries used in your project contains assert function(s), then use them. If no Silicon Labs specific assert function is available, then use the code from the example below. The rationale for this rule is that a number of toolchains will bring in their standard library versions of printf and friends in order to implement assert. This can bring in unwanted side effects, such as code size increases.

Example

// void user_assert (int file, int line);                   /* declaration */
// #define USER_ASSERT(file, line) user_assert(file, line)  /* definition  */

void sl_assert(const char *file, int line)
{
  (void)file;  // Unused parameter
  (void)line;  // Unused parameter
  // Wait forever until the watchdog fires
  while (1);
}

#if defined(NDEBUG)
#define SL_ASSERT(expr)
#else
#if defined(USER_ASSERT)
#define SL_ASSERT(expr)   ((expr) ? ((void)0) : USER_ASSERT(__FILE__, __LINE__))
#else
#define SL_ASSERT(expr)   ((expr) ? ((void)0) : sl_assert(__FILE__, __LINE__))
#endif
#endif

Comment: Below are the steps to overwrite Silicon Labs assert function with a user defined assert.

  1. define a void user_assert(const char *, int) function

  2. Uncomment the two lines in the above example (marked “declaration” and “definition”).

assert() is a void function and never return any value. If you like a function to return a value depending on the evaluation of an expression, then feel free to create a new function for that purpose.

3.4 Keywords

3.4.3 The “static” keyword (Required)

For file global variables (variables that are global to a file but not visible outside the file) should always use the static keyword. This should also apply to any functions that are used only within a single file.

Keep in mind the difference between a static variable at the file scope level, and a static variable declared within a function.

Example

// A variable that is only visible within a file
static uint32_t my_local_variable = 0xFF;

// A function that is used only within a file
static void do_something(void)
{
  // ...
}

For Cortex-Mx devices, use the CMSIS __STATIC_INLINE macro if you need something to be both static and inline.

3.5 Data Types

3.5.1 Avoid enums in arrays, structures, or unions (Required)

Not used in Unify. The full source is shared with customers, so there is no toolchain mismatch. Serialized format must be decoupled from the in-memory representation anyway, so this will not affect the persistent data formats.

3.5.2 Avoid bitfields in structures/unions (Required)

Also relevant for Unify, for two reasons: 1) Bitfields cannot be used for wire formats, because the bit-layout is compiler-implemnentation defined; by disallowing it we prevent this type of mistakes. And 2) for performance reasons: Bitfields are typically slower to manipulate, and we are not trying to optimize the Unify Host SDK for memory size.

This is also especially important for aggregate types shared between libraries and customer-compiled code. As with enums, the size and bit layout of bitfields is not standardized, but is compiler-defined or subject to compiler options. Libraries built with one compiler or options will not interface properly to non-library code built with a different compiler or options.

Example

// sl_lib.h include file:

typedef struct {
  bool    ena          : 1;
  uint8_t some_setting : 5;
} sl_lib_struct_t;

// Library could be built such that this structure is laid out in a byte:
//   7   6   5   4   3   2   1   0
// [ x | x |     some_setting  |ena]
// while application could be built such that this structure is laid out
// across two bytes, or in different endian order:
//   7   6   5   4   3   2   1   0
// [ena| x | x | x | x | x | x | x ]
// [   some_setting    | x | x | x ]

3.6 Variables

3.7 Functions

3.7.1 Prototype functions (Required)

Make sure either the full implementation or a prototype precedes any call to a function. For external functions, this should be done by with a #include of the appropriate header.

3.7.2 Functions return data types (Required)

All public and private functions that can fail should return either:

  1. In case the possible outcomes is binary SUCCESS or FAILURE use sl_status_t. The idea is to be as consistent and predictable throughout all of our code base to make it easier for customers to know what to expect from our functions.

  2. In case of multiple error outcomes, define an enum with the with the possible error and success values and return that. The enum should have its own Doxygen and be linked from the function Doxygen.

Exceptions

There will be functions that will not return sl_status_tor a function specific error enum. For example, functions returning void; simple “getter” functions that cannot fail or that we don’t need to differentiate between error cases; a function checking if a condition is true or false could return a bool; a function adding data to a string or buffer could return the number of bytes added to the string or buffer; or a callback function could return an indicator to let the stack know how to act. Other examples may exist but all of these exceptions should be used sparingly and with good reason. Please check with your team and/or manager before doing so.

In any case, the following requirements must be complied with:

  • if a function can fail, there must be a clear indication as to how to detect a failure. This is preferrably done through returning sl_statusor a function specific error enum, but having a special ‘invalid’ value returned in case of failure is also allowed. No matter how this is achieved, it must be documented in the doxygen function header.

  • bool must not be returned to indicate success or failure. bool should only be used to indicate if a given condition is true or false. Even then, using an enum should be considered for future-proofing the function, should it need to return more than a true/false value in the future.

3.8 Macros

3.8.2 Macros with statement(s) (Required)

If a macro expands into one or more full statements, make sure it consumes a subsequent semicolon. Furthermore, multiple-statement macros must be wrapped in a block. These rules ensure that the expanded macro will have the same syntax as a non-compound statement. Otherwise, it may cause undesirable parsing if a customer uses it without braces in a selection (if (...) FOO();) or iteration (while (...) FOO();) statement.

Example

#define SL_FOO(x, y)          \
          do {                \
            ga = (x);         \
            gb = (y);         \
            gc = ga + gb;     \
          } while (0)

#define do_nothing()   (void)0

void sl_bar(int baz)
{
  if (baz) {
    SL_FOO(123, 321);
  } else {
    do_nothing();
  }
}

3.8.3 Functional macros with argument(s) (Required)

Uses of arguments within macros, and the macro body itself, if it is an expression, should be wrapped in parentheses. This avoids problems stemming from unintended precedence groupings. Arguments should be used only once if possible to avoid problems when a statement or expression with side effects is passed.

Note: In general, static inline functions are preferred to macros as they have less weird side-effects and are easier to read.

Example

#define sl_bar(x, y, z) (0xFFFF | ((x) & ((y) | (z))))

3.9 goto statements

3.9.1 goto statements should only be used for cleanup purposes or early exit in case of an error, when there is no simple workaround (Required)

gotos should never be used, except when needing to cleanup (free resources, release a lock, exit a critical section, etc.) in the case an error occurred in the function. If a simple workaround can be used instead of goto, the workaround should be used instead.

Example

// Using goto, if no simple workaround available
void sli_usb_function(void)
{
  // Acquire lock
  // [...]
  if (!ok) {
    goto release;
  }
  // [...]
release:
  // Release lock
}

// Workaround, whenever possible
void sli_usb_function(void)
{
  // Acquire lock
  // [...]
  if (ok) {
    // [...]
  }
  // Release lock
}

3.9.2 gotos should only refer to a label declared after them (Required)

A goto statement should only refer to a label declared after (below) them, in the code.

No goto shall ever cause the code to go back “up”, it should always jump “down”, towards the end of the function.

Example

// Don't do:
void sli_usb_function(void)
{
  // [...]
loop_start:
  // [...]
  if (loop) {
    goto loop_start;
  }
}

// Instead do:
void sli_usb_function(void)
{
  // Acquire lock
  // [...]
  if (!ok) {
    goto release;
  }
  // [...]
release:
  // Release lock
}

3.9.3 gotos should only refer to a static label located in the same function (Required)

No computed goto statement (as available in some GCC extensions) shall be used. setjmp and longjmp should never be used. The label referred to by a goto statement needs to be in the same function as the goto statement itself.

3.9.4 Any label referenced by a goto need to be declared in the same block or a block enclosing the goto (Required)

goto statements and labels should not be used to jump between blocks, as it can easily lead to unstructured code. goto should not be used either to jump between cases of a switch.

Example

// Don't do:
void sli_usb_function(uint8_t bar)
{
  if (bar > 0) {
    goto label;
  }
  // [...]
  goto label;
  // [...]
  if (foo > 0) {
label:
    // [...]
  }
}

// Instead do:
void sli_usb_function(uint8_t bar)
{
  // [...]
  goto label;
  // [...]
  if (bar > 0) {
    goto label;
  }
  // [...]
label:
  // [...]
}

// Don't do:
void sli_usb_function(uint8_t bar)
{
  switch(bar) {
    case 1:
      if (x == y) {
        goto label;
      }
      break;

    case 2:
      doThat();
label:
      doTheOtherThing();
      break;

    default:
      break;
  }
}

3.10 Libraries

3.10.1 Dependencies (Required)

Be conscious about what code the library depends on. To avoid excessive code footprint, we must be aware of what external functions each library pulls in.

3.10.2 Including third party code (Required)

For all open source or third party software that we include in our software releases, we need to have an acceptable license that allows us to do so. That means we need to send an email to our legal department (DL.Legal) and ask for permission before introducing new open source software into our distributions. Consult the Software Manager before including any third party software code intended to be released.

3.10.3 Configuring libraries in source form (Required)

The customer should never have to change the original source files to configure the library (this creates problems when upgrading the library to a newer version, and also makes it impossible to have two projects with different configuration settings using the same instance of the library). Instead, it should be possible to set all configurations settings from the customer’s application project. Normally this can be done with macros that allow a customer to configure library settings from within the application.

Example

#if !defined(EM_SETTING)                   // If EM_SETTING is not defined by user,
#define EM_SETTING default_value_for_em    // then we set our default value/function.
#endif

3.11 Misc


4 Coding style and formatting

4.1 General formatting

4.1.1 Use spaces, not tabs (Required)

For indenting files, use spaces and never tabs. A mix of tabs and spaces is never acceptable.

4.1.2 Indents are 2 spaces per level (Required)

Indent each nested level with 2 spaces of indent.

4.1.4 Line endings (Required)

We use line ending normalization in our repositories. This means that all text files are converted to ‘\n’ line endings when they are stored in git. However most customers are using Windows operating system which expects a CRLF line ending. Therefore, with rare exception, all source code delivered to customers should have CRLF (DOS) line endings. There are two ways to accomplish this. First, if you are using a Windows host operating system, set your git autocrlf setting as follows:

core.autocrlf true

This will ensure all text files have DOS line endings when checked out from the repository.

The second method is to use a release script that forces all text file line endings to CRLF when a source code release package is built by the release script.

Exceptions: if the source code is intended for a system that >use normal line endings, for example a OS X or Linux system, then >the source line endings can be left as ‘\n’.

Note: All repositories should include a .gitattributes file to explicitly specify file types for line endings.

4.1.5 Use only plain ASCII or UTF-8 (Required)

Text files should almost always contain only plain ASCII. Specifically, avoid characters outside the usual whitespace and printable ones (0x9-0xD and 0x20-0x7E, inclusive). Internationalized strings, when used, are best placed in a resource file.

In the rare case that other characters are needed in a text file and cannot be escaped, the file should be UTF-8 encoded with no byte-order mark.

4.1.6 Use ISO8601 formatting for dates (Required)

If you use a date in the comments or elsewhere, specify it using the unambiguous ISO8601 format, i.e. 2013-11-26.

4.1.7 Inserting empty and blank lines (Required)

There should never be consecutive blank rows.

Use two slashes (C++ comment) and 77 dashes to separate logical parts of the code. Use 2 slashes and 32 dashes for minor sections. Use of section separators as shown here is optional.

Example

// -----------------------------------------------------------------------------
// Here we start a new logical part in this source/header file...

// -------------------------------
// And here comes a new minor section...

4.1.8 Use parentheses liberally (Required)

Add parentheses if you have any doubt at all about precedence, not doing so has led to some very obscure bugs. This is especially true of the bitwise binary operators {&, |, ^} and Boolean operators {&&, ||}.

4.1.9 Break up long expressions (Required)

Whenever there are long expressions, it should be broken into multiple lines. When a line is broken into multiple lines, each line should start with the operator that operates on that full line (single element or a group of elements within parentheses). The operator must be the first thing on the line, and it should be indented appropriately.

Example

int bitmask = (OPTION_1
               | OPTION_2
               | (IS_THIS_SET
                  ? OPTION_3
                  : OPTION_4));

int bitmask = (OPTION_1
               | OPTION_2
               | (IS_THIS_SET ? OPTION_3 : OPTION_4));

int bitmask = (OPTION_1
               | OPTION_2
               | (OPTION_3 & OPTION_4));

Comment: All the above are examples of nicely formatted long expressions.

Below is an example how you should not format long and complex expressions.

// Avoid this...
int no_good_formatting = (OPTION_1
                         | OPTION_2
                         | OPTION_3 & OPTION_4);

4.1.10 goto labels should be on column 1 (Required)

Labels referred to by gotos need to be located at column 1, disregarding any indentation.

Example

// Don't do:
void sli_usb_function(void)
{
  // Acquire lock
  // [...]
  if (!ok) {
    goto release;
  }
  // [...]
  release:
  // Release lock
}

// Instead do:
void sli_usb_function(void)
{
  // Acquire lock
  // [...]
  if (!ok) {
    goto release;
  }
  // [...]
release:
  // Release lock
}

4.2 Commenting code (Required)

4.2.1 Use C++ style comments (“//”)

All inline code comments should use the C++ style //. However there are two exceptions to this rule. First, function and file documentation blocks use a different style (see documentation section below).

Second, for multi-line macros using #define, C style comments /* ... */ should be used for embedded comments (see example below).

For both kinds of comments, there should be a space following the opening comment marker. For example // My comment.

Make sure constant values in the code are explained. Function calls with raw constants should be labeled based on the parameter that is being passed in.

Example

// Example for the exception
// This type is needed because using // would swallow the line continuation marker.
#define MY_CLI_COMMANDS \
  /* This command takes 3 arguments: */           \
  /*  Node ID - a 2-byte node ID destination */   \
  /*  Endpoint - a 1-byte node ID */              \
  /*  Cluster  - a 2-byte cluster ID */           \
  { "my_command", my_command_function, "vuv" },

// Example of how to comment constant values.
// For the function declaration below
void function(int seconds, boolean print_output);

// we add comments after each parameter (OPTIONAL)
function(0,        // seconds
         FALSE);   // print_output

4.3 Bracing style (Required)

Use the so called “One True Brace Style” (see [https://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS]) Indent increases one level after an opening brace, and decreases one level before a closing brace. Opening braces do not go on their own separate line, except for free standing blocks and function definitions. Closing braces are on their own separate line with nothing else on these lines.

Clarification: The paragraph above takes precedence over the linked “One True Brace Style” definition. In particular, opening braces must go on their own line for function definitions even though this is a deviation from the linked “One True Brace Style”.

All if/else/while/for/do-while blocks must be enclosed by braces, even if there is only one statement in the block.

Exceptions for the above rule are;

  1. The typedef alias for a composite (struct/union) or enum type is on the same line as the closing brace.

  2. In a do-while loop the condition (while (...);) is on the same line as the closing brace.

  3. else and else if are on the same line as the closing brace.

Example

void sl_do_something(uint8_t bar)
{
  if (foo > bar) { // The preceding brace is *required*
    do_this();
  } else if (for < bar) {
    do_that();
  } else {
    do_another_thing();
  }

  if (foo > bar) {
    do_this();
  }

  while (1) {
  }

  do {
    sli_do_work();
  } while (foo);
}

typedef enum {
  SL_CARD_SPADE,
  SL_CARD_HEART,
  SL_CARD_CLUB,
  SL_CARD_DIAMOND
} sl_card_suit_t;

4.4 Switch statements and labels

4.4.1 Using labels (Required)

Switch-case labels should be indented as any other line of code would be.

Example

if (foo) {
  sl_bsp_set_leds(0xff00);

  testing:

  sl_bsp_set_leds(0x00ff);
}

4.4.2 Labels with block (Required)

If a block is desired after a label, then the opening brace should be by itself on the line following the label, at the same indentation.

The closing brace should be on a line by itself after the last statement in the block, at the same indent level as the opening brace.

Example

if (foo) {
  sl_bsp_set_leds(0xff00);

  testing:
  {
    sl_bsp_set_leds(0x00ff);
    sl_bsp_set_leds(0x0000);
  }
}

4.4.3 Switch statements (Required)

The cases in switch statements should be indented one level from the enclosing braces. Separate case blocks with a blank line after each break;. All switch statements should include a default block unless there is a good reason not to. The default block can collapse into one of the other cases but it should clearly show what happens when there is no matching case. Finally, if a case block does not end with an unconditional jump, there should be a comment clearly stating that the code is intentionally meant to fall through.

Example

switch(baz) {
  case 1:
    sli_do_this();
    break;

  case 2:
    sli_do_that();
    // This case is meant to fall through to the next

  case 3: {
    sli_do_the_other_thing();
    break;
  }

  case 0:
  default:
    sli_do_what();
    break;
}

4.5 Functions, operators and C keywords

4.5.1 Listing function parameters (Required)

Whenever there is a list of function parameters, they should all fit on a single line or be listed one parameter per line. If listed on separate lines, each parameter has the same indent level as the first parameter.

Example

void sl_do_something(int a,
                     int b,
                     int c,
                     const char *string1,
                     const char *string2)
{
  // ...
}

void sl_do_something2(int a, int b, int c)
{
  // ...
}

4.5.2 Using function parentheses (Required)

For function declarations, definitions and calls, there shall be no spaces before or after the opening parentheses, or before the closing parentheses.

Example

int sl_foo(int days, int seconds);
// ...
ret = sl_foo(days, seconds);

4.5.3 Binary and ternary operators (Required)

Use spaces around binary & ternary operators in expressions. C expressions are hard enough to parse without running them altogether.

Example

for (j = 0; j < parameter_sizes[i]; j++) {

4.5.4 Use a single space after C keywords (Required)

C keywords (ex. for, switch, while, if, do) – should have a single space after the keyword.

Example

while (counter < UART_MAX_IO_BUFFER) {

4.5.5 Additional space within expressions

Use of additional whitespace within expressions to improve readability is permitted as long as it doesn’t interfere with appropriate multi-line expression indentation. Such style should be consistent within the module.

4.5.7 Pointer asterisk position (Required)

When declaring a pointer to a variable, the pointer asterisk should be placed together with the variable name, not together with the type.

Example

// Don't do:
char* c = (char*)a;
void sl_foo(uint32_t* bar);

// Instead do:
char *c = (char *)a;
void sl_foo(uint32_t *bar);

4.5.8 Don’t mix pointer and value type declarations (Required)

Don’t mix declarations of pointer type variables and value type variables on the same line.

Example

// Don't do:
uint32_t *a, b;

// Instead do:
uint32_t *a;
uint32_t b;

4.6 Naming

4.6.1 General Considerations

4.6.1.1 Use meaningful names (Required)

Special care should be taken when naming anything. The name should convey the purpose of the construct as clearly as possible.

4.6.1.2 Avoid abbreviations (Required)

In general, use long names and avoid unclear abbreviations and other cryptic notations.

Prefixes such as sl_ and sli_ are exceptions to this rule and therefore can and need to be used where appropriate.

Exception: Well-known abbreviations in the Unify project can be used. But they must be documented in the Unify repo in the file doc/standards/known-abbreviations.md.

4.6.2 Namespaces (Required)

All constructs will have a prefix, with the exception of function parameters, variables local to a function and structure fields.

The purpose of using namespace prefixes is to prevent namespace collisions and not necessarily for branding.

All constructs will start with the module/component prefix. The Unify Host SDK does not use the sl or sli_prefix because it is a complete application and does not need namespace separation from customer code. The reason for this exception is to avoid overly long function names. Code intended for re-use in other protocol controllers shall use the sl_ prefix to indicate that both customers and Silicon Labs developers are expected to develop protocol controllers.

4.6.3 Naming (Required)

The following section (4.6.3.*) contains information on how to name anything. This acts as a default, if nothing more specific exists for a particular construct. In general, construct-specific standards should be avoided.

4.6.3.1 Casing (Required)

Every construct’s name needs to be all lower-case, with each word or abbreviation separated by an underscore. This is also known as snake case.

Example

// Global variable
uint8_t sl_uart_char; // Publicly available
uint8_t sli_usbd_endpoint_buffer[32]; // Internal use only

// File-local variable
static sl_usbd_urb_t *urb_list_head;

// Functions
void sl_led_turn_on(void);
void sli_nvic_set_priority(ADC_IRQ);

// Data types
typedef uint32_t sli_kernel_flags_t;
typedef struct {
  uint8_t *data_start;
} sl_usbd_urb_t;
4.6.3.2 Casing and acronyms (Required)

Acronyms and abbreviations are treated like any other word. Do not put in lower-case or upper-case some or all letters of an abbreviation, even if it is normally written that way. Instead, comply with the standard about that particular construct and treat acronyms as a regular word.

Example

static sl_usbd_urb_t *urb_list_head;

sl_led_turn_on();

sl_irq_priority_t irq_priority = sl_nvic_get_priority(ADC_IRQ);

4.6.4 Naming functions and variables

4.6.4.1 Functions and variables names have the form verb_noun (Required)

When a verb and a noun the verb acts on are in a function name, the verb should come first (xxxx_get_power, not xxxx_power_get). It should feel natural to read the name. Most functions should contain both a verb and a noun and therefore need to follow this rule. Notable exceptions are functions that are callbacks, interrupt handlers and tasks, which typically do not have a verb. These functions follow their own naming convention, as stated in 4.6.4.5, 4.6.4.6 and 4.6.4.7, respectively.

Variables and functions of boolean type should use a naming style indicating the sense of the boolean. For example sl_usbd_device_is_connected.

Example

// This is for legacy EM code and does not apply to 8051
// code base.

sl_get_power();    // Call to a new-style API function.

// Below is an example of how to redefine old style API calls to the new syntax.
#if (SLAB_OLD_API == SLAB_REPLACE_OLD_API_CALLS)
#define power_get sl_get_power
#define power_set sl_set_power
#endif
4.6.4.2 Function parameters should not have any prefix (Required)

Do not prefix any function parameter with any sl_, sli_ or <module>_ prefix.

4.6.4.3 Function-local variables should not have any prefix (Required)

Do not prefix any function-local variable with any sl_, sli_ or <module>_ prefix.

4.6.4.4 Variable and Function Names Indicate Units for Time (Required)

When a variable is used for time, it must include the units in the name. When a function returns a value that is used for time it must include units in the name. This is important to prevent problems with accidentally utilizing one unit of time in a function or variable that takes a different unit of time.

Variables and function names may either use abbreviations or spell out the units. If abbreviations are used, the following shall be the abbreviations:

Full Name Abbreviation (if applicable)
Years Years
Days Days
Hours Hours
Minutes Minutes
Seconds Sec
Milliseconds Ms
Microseconds Us
Nanoseconds Ns

Example

#define SLI_JITTER_DELAY_MS 100

static void restart_discovery_after_delay(uint8_t delay_ms);

uint8_t sli_get_discovery_time_remaining_ms(void);
4.6.4.5 Functions/stubs called on specific events/callbacks should start ‘on’ in their name (Required)

Whenever a function is called to indicate an event occurred, or is called in ‘reaction’ to an event happenning, this function should have on in its name, directly after the <module>_ prefix. This also applies to callbacks or function stubs shipped to the user.

Example void sl_usb_on_device_connection(void);

void sl_kernel_on_task_deletion(void);

static void on_transmit_completed(void);
4.6.4.6 Interrupt handlers should be suffixed by ‘IRQHandler’ or ‘irq_handler’ (Required)

If a function is an interrupt handler, it should either be suffixed by IRQHandler if it needs to follow CMSIS’ format or by irq_handler if it doesn’t (for example, if an interrupt source is shared and multiplexed) between several handlers.

Example void RTCC_IRQHandler(void);

void sl_gpio_irq_handler(void);
4.6.4.7 Non-blocking functions executed periodically in a main loop should be suffixed by ‘step’ (Required)

If a non-blocking function (a function that doesn’t pend or delay before returning) needs to be called periodically in order to check if it has something to process and then process what it can, this function needs to be suffixed with step, to indicate it executes a single round of processing. It should not be called tick, since tick can lead to confusion with timer or OS ticks.

Example void sl_cli_step(void);

void sli_usb_msc_step(void);
4.6.4.8 Functions that are tasks should be suffixed by ‘task’ (Required)

If a function is a task (in an OS environment), this function needs to be suffixed with task, to indicate it is a task and needs to loop indefinitely and never return.

Example void sl_cli_task(void *task_argument);

void sli_usb_task(void *task_argument);

Unify comment: Contiki processes are exempted from this rule and follow the Contiki convention of a _process suffix.

4.6.5 Naming constants

4.6.5.1 Constants should use upper case (Required)

All constants should be named and use upper case letters. Avoid raw numbers in code. This includes #defines constants, const variables and enum values.

Example

#define SLI_NET_ARP_FLAG 0x0040
const unsigned int SL_MAX_UART_CONNECTIONS = 3;

typedef enum {
    SL_USBH_HC_TYPE_LIST,
    SL_USBH_HC_TYPE_PIPE
} sl_usbh_hc_type_t;

4.6.6 Naming function-like macros

4.6.6.1 Follow the naming convention for regular functions (Required)

Functional macros that can be used in the same way as functions follow the same naming conventions as regular functions.

4.6.6.2 Use all caps for macros that can’t be functions (Required)

All caps with underscores are used for macros that cannot be made into semantically equivalent functions.

Example

// This is a macro function that can be used as a regular function.
#define sl_uart_init_default_uart(x) init_uart(UART0, (x))

// This is a macro function that cannot be used as a function.
#define SL_SOME_NUMBERS(x) {(x), ((x)+(x)), ((x)*(x))}

4.6.7 Naming types

4.6.7.1 Public typedefs (Required)

Each typedef must end with a ‘_t’ suffix and cannot start with ‘int’, ‘uint’ or ‘unicode’.

4.6.7.2 Structure fields should be snake_case, without any prefixes (Required)

There should not be any prefix (no sl_ and no <module>_) in the name of any structure field.

Example

// Don't do
typedef struct
{
  uint32_t sl_nvm_page_header_offset;
  uint32_t sl_nvm_page_header_size;
} sl_nvm_page_header_t;

// Instead do:
typedef struct
{
  uint32_t offset;
  uint32_t size;
} sl_nvm_page_header_t;
4.6.7.3 Type from typedef (Optional)

If the type is a typedef, you can optionally add a type name if there is a reason the anonymous type does not work. In this case use the same name as the typedef name, without the ‘_t’.

Example

// Anonymous structure name ...
// Use this style in most cases.
typedef struct
{
  // ...
} sl_nvm_page_header_t;

// You can use this style if the struct needs a name.
typedef struct sl_nvm_page_header
{
  // ...
} sl_nvm_page_header_t;

4.6.8 Files and directory structure

4.6.8.1 Filenames and directories use lower case (Required)

All file names are lower case and multiple words are separated with one underscore ‘_’.

Example

sl_packet_buffer.c
4.6.8.2 Avoid duplicate filenames

Avoid using the same file name for source files.

Note: Among our entire source code there will be files with the same name. This cannot be avoided. But files with the same name should never be used in the same project/build.

4.6.8.3 File names (Required)

File names (both for library and source) should include the namespace prefix of the module.

Example

foobar_controller.c
mymod_status.h

The sl_ prefix is not used in the Unify Host SDK, because it produces an integrated application without clear distinction between customer and company modules.

4.6.8.4 Directory names (Required)

Directories use lower case names and underscores as word separators. Subfolder names do not need to be unique.


5 Documentation

5.1 General

Write documentation so that others can easily pick up the code. It makes life easier for everyone. In particular, it makes supports job a lot easier as it will generate less support requests.

5.2 Comments should answer the question “Why?” (Required)

Write comments that say why the code is the way it is. What the code does, and how it does it, can usually be figured out from the code itself. Why it does it, and why it does it the way that it does, cannot. If you write it the obvious way, and it doesn’t work, explain in a comment why it didn’t. Otherwise the next person to come along is going to convert it back to the obvious method and have to learn the same painful lesson (or even worse, force you to relearn it).

There needs to be enough documentation that a peer can read and understand the code without having to ask the author for explanations. If a code reviewer feels the need to ask for explanations about how stuff works, then the code author should add this additional information as comments in the code.

Comments should use correct grammar and full sentences.


6 Doxygen Coding Style Guide

Doxygen is used to document all code that is released to customers, with a focus on functions that are part of an API. For code that is only used internally it is still strongly recommended to use Doxygen commenting as internal users will find it useful.

For file and function comment blocks, the following style is used:

/**
 * doc comments ...
 */

For all other doc comments, the C++ style /// is used.

6.1 File comment header (Required)

All source files should have a file comment header that looks similar to the following. The actual license text will depend on the project. See section 7 for details about licenses.

6.4 Function documentation (Required)

This is required for any library or module code that will be customer facing. It is also highly recommended to document any other code in the same way, even if it is not expected to be customer-facing. Often times non-customer-facing code ends up being delivered to a customer anyway.

Normally, functions that are part of an example (like main) or other simple functions that are part of the application and not library or module functions are not documented with Doxygen. It may still be useful to use Doxygen to provide documentation for complex examples.

Each function definition contains at least a function brief statement and list of parameters and return value if any. Doxygen will pick up a single sentence at the start of the comment block as the brief description, so the @brief tag is not necessary.

If the function contains parameters and a return value, then these parameters and return value must also be documented in Doxygen.

Optionally the @note command can be used to highlight anything that the user should pay extra attention to when using this function.

It is useful to read the Doxygen documentation about how you can write good documentation for your functions. Besides @note, there are other tags that can also be used. For example @sa (see also) can be used to create a link to another documented section. If you put a full function name or variable name that Doxygen knows about, it will automatically create a link to the documentation for that other function or variable. For macros you can add the @ref tag before the item name and a link will be generated.

Example

/**
 * Brief description of function.
 *
 * @param myParam1 is a parameter that is most excellent
 * @param myParam2 is not such a good parameter
 * @returns Returns true on Tuesdays or whenever it is raining.
 *
 * Detailed description here. Can be multiple sentences. Can
 * contain markdown (tables, code examples, bulleted lists)
 *
 * Can add as many additional paragraphs as needed.
 *
 * @note A note if the API needs a note.
 */
 uint8_t sl_my_public_function(uint8_t my_param1, uint16_t my_param2)
 {
   // ...
 }

6.5 Variable documentation (Required)

All public variables that are part of an API must be documented. It is also recommended to document all file-level global variables even if they are not public.

Note: It is not necessary to document local (automatic) variables.

Many variable will only require a single line or brief comment. In this case you should use a C++ style doxygen comment ///. You can use this same style even for several lines of a documenting comment. However, if the variable requires a large documentation block or needs to be visually separated from other sections of the source code file, then use the same style blocks as used for functions.

Example

/// A brief description of this variable.
uint8_t sl_meaningful_variable_name;

/// This variable has a brief line of documentation.
/// Then it also has some additional lines of documentation.
uint32_t sl_another_variable;

/**
 * Brief description of complicated variable.
 * Additional extra documentation for this complicated variable that
 * needs a bigger explanation. Or perhaps I just want this variable to
 * stand out in my source file so I use the large comment blocks.
 */

For fields of a structure, or a list of constants or anything that requires just a brief comment, there is another style (post) that can be used. In this case you put the doc comment on the same line as the item (instead of before it). To do this the comment needs to start like this: ///< Brief comment.

Example

/// A brief comment about this data type.
/// I can follow with additional explanation as needed, perhaps how
/// it is used or allowed values or constraints.
typedef struct
{
    uint8_t r;    ///< Brief comment about this field.
    uint8_t g;    ///< Green pixel value.
    uint8_t b;    ///< Blue pixel value.
} sl_pixel_t;

7 Licensing

7.1 Silicon Labs Licenses

All of our released source files (.c, .h, others; generated and manually written) should fall under one of the few licenses approved by our legal team. All source files are therefore required to have a file comment header containing the correct license type.

  • The default license text must be used unless otherwise specified.

  • The third-party license text is added on top of any third-party code. It does not remove the existing text. If we include any third party software, we need a way to clearly identify it as such. Yes, this is extra work, but is required by our legal team. Using and releasing third-party code has more constraints, see section 7.2 for the details.

Please note that the version field in these headers is optional. If used, the version field should be used to denote the component version, not the Gecko SDK version.

7.2 Third-Party Code

We generally favor writing and maintaining our own code when the differences (in effort, quality, market acceptance, etc.) with an open-source one are small.

The VP of Software must give his consent before any kind of third-party code is allowed to be used, as special care must be taken when dealing with this type of code. Specifically, content licensed to us under a “copyleft” license (GPL and other viral open-source licenses) must not be released, including in compiled form (such as a library or binary).

New copyleft content should not be added. Existing copyleft released content must be audited against this rule, documented and flagged to the VP of Software for quick resolution. Whenever feasible and regardless of whether it is being released or is a documented exception, existing copyleft content should be replaced.

Content that is “multi-licensed” (offered to us under our choice of more than one license) is not considered copyleft if at least one of the offered licenses is not copyleft.


8 C++ Specific Guidelines

C is our primary language because we assume that a larger customer base will be familiar with C. We are gradually introducing C++ where it makes sense. Essentially, where the benefits of a particular C++ language feature outweighs the maintenance cost associated with customer education, tooling maintenance and developer cognitive overhead.

Automatic mocking with CMock is an integral part of our effort to have comprehensive unit test coverage. Automatic mocks of C++ code present unique challenges. Particularly, there is no C++ equivalent of the automatic mock creation feature of CMock.

  1. Components must expose a C API even when using C++ internally for the implementation. This can be a thin C wrapper around a C++ component. This requirement may however be omitted for some cases, which are described below.

  2. Component may expose C++ APIs that are mockable using CMock. E.g. by also exposing a C API wrapping the C++ API and mocking that with CMock.

  3. Trivial components that do not require unit testing may expose C++ APIs that are not mockable with CMock. E.g. simple converters.