Skip to content

Latest commit

 

History

History
2057 lines (1330 loc) · 88.5 KB

coding_standard.md

File metadata and controls

2057 lines (1330 loc) · 88.5 KB

1 Introduction

1.1 Purpose of this document

The purpose of this document is to describe the coding standard used by Silicon Labs firmware. A unified coding standard is necessary to provide a unified look and feel and reduces cognitive load when learning a new project. Coding style and preferences differ from person to person. However, please adhere to these rules for any code project or pull request submitted to repos in the Silicon Labs GitHub organizations. Thank you!

1.2 Latest version of this document

This document is version 1.0.

Note: Use pull-requests for changes to make it easier to track changes to the standard.

1.3 Changing this document

Any change made to this document must comply with following procedure. Additionally, please make sure that the commit has a good and descriptive commit message.

1.3.1 Versioning

While a new version of the coding standard is in development, the pull requests should be merged in the coding_standard_next branch. Whenever a new coding standard will be released, it will be merged back in the main branch and the version number of the standard will be incremented.

Therefore, each commit on the main branch will be a new, stable version of this coding standard. For small additions to the standard, only the minor number version should be incremented. For versions not compatible with previous ones, the major version number should be incremented.

1.3.2 Review committee

The pull request to merge in to coding_standard_next needs to be approved by one of the people on the review commit, managed by the Review Committee Team in the SiliconLabsSoftware organization.

1.4 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 the code. This part is split into a general and a C-specific part.
  • The coding style guide details how the 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.4.1 Source Code

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

1.4.2 Required

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

1.4.3 Recommended

Recommended rules should generally be followed. However, recommended rules can be broken in special circumstances. All recommended coding standards are tagged Recommended.

1.4.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.5 Application of this standard

1.5.1 New code

This standard applies unconditionally to new code and 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.5.2 Existing code

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

Planning for updating existing components to the new coding standard will be done independently for each stack/module. The timing for these updates should be strategically chosen for each module to reduce the impact to dependencies.

When updating to the new coding standard, especially from a naming convention perspective, it will be required to provide a compatibility layer. Exceptions will only be made for stack/modules with a valid, documented justification as well as agreement from the various stakeholders. An exception could be made either to have a partial compatibility layer or none at all.

Some modules left in 'maintenance mode' or currently deprecated might never be updated, and that is acceptable. An examples of this would be the SLEEP Driver, RTCDRV.


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 colleague should review all code. In practice, pull requests are required 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.2 Fix what you break

If your changes break anything, then you are responsible for correcting the problem. This means that if you break an automated or manual testing job by committing something, you should strive to fix it as soon as possible. Other people depend on being able to build!

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. 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. Whenever 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's important!
  • 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, with radio protocols being a prime example.


3 C-Specific Guidelines

3.1 Compilers

3.1.1 C/C++ standard version (Required)

For 32-bit MCUs (Cortex-Mx) we use C17 (also known as C18) and C++17.

For 8-bit MCUs (8051) we use C90. Since the Keil PK51 development tool is the default build tool for our 8051 devices, we have to make sure that we comply with the C90 exceptions Keil implemented in their PK51. These exceptions can be found here:

[http://www.keil.com/support/man/docs/c51/c51_xa.htm] keil

Even though we are restricted to C90 code for 8051 MCUs, we still use C99 standard integer types (uint8_t, etc). We provide our own stdint.h and stdbool.h in our SDK.

If a project is intended to run on both 8-bit and 32-bit platforms, use the lowest common denominator (i.e. C90 with compatible extensions).

3.1.2 Support mixing C++ and C (Required)

C++ is increasing in popularity. To make sure that our libraries and code are compatible with C++ code, functions in header files should be defined inside an extern "C" block. This makes it possible to call C functions from C++.

Example

#ifdef __cplusplus
extern "C" {
#endif

void foo(void);

// other functions ...

#ifdef __cplusplus
}
#endif

Comment: Do not add an extra layer of indentation between the opening and closing braces in this case. This is also an exception to the rule that requires braces to be on line by themselves.

3.1.3 Multiple build tool support (Recommended)

Most of our 8051 and Cortex-M source code can be built with more than one build tool chain. To make the porting job easier between toolchains, we will use header files that define macros/functions for how to access registers, intrinsic functions, and other parts of the device that differs between the different tool chains.

For Cortex-M, this is defined by CMSIS and located in cmsis_compiler.h. For 8051 MCUs we use the si_toolchain.h file.

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 warnings are 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 other projects.

Exceptions to Werror have to be approved by the module owner and have their rationale documented. Exceptions should disable errors, but leave warnings 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.2.2 Preprocessor conditional compilation (Recommended)

Limit the use of preprocessor conditional compile statements like #if and #ifdef. If used, avoid using preprocessor conditions inside functions.

Example

#if !defined(LINUX_DBG)
#define sl_enable_interrupts()   __restore_interrupt()
#define sl_disable_interrupts()  __disable_interrupt()
#else
#define sl_enable_interrupts()   (void)0
#define sl_disable_interrupts()  (void)0
#endif

void sl_do_something(void)
{
  sl_enable_interrupts();
  sl_do_atomic_operation();
  sl_disable_interrupts();
}

Comment: The above is a cleaner way to write code that in this example has more than one way to enable/disable interrupts. Below is the original, not so clean, code from the example above.

void sl_do_something(void)
{
#if !defined(LINUX_DBG)
  __disable_interrupt();
#endif
  sl_do_atomic_operation();
#ifndef LINUX_DBG
  __restore_interrupt();
#endif
}

3.2.3 Use of preprocessor directive #error (Recommended)

Use #error when #defines can only be set to certain values, or other configuration problems.

Example

#if (DEFAULT_SAMPLE_RATE8 == 44)
#define DEFAULT_SAMPLE_FREQ 44100
#elif (DEFAULT_SAMPLE_RATE8 == 48)
#define DEFAULT_SAMPLE_FREQ 48000
#else
#error Invalid value: DEFAULT_SAMPLE_RATE8
#endif

3.2.4 Use of preprocessor directive #include (Recommended)

Header files should generally list their dependencies explicitly. The preferred way is to #include prerequisite header files. Alternatively, the header file may validate that an appropriate macro is defined.

Preferred

#include "sl_foo.h"

void sl_bar(sl_foo_t baz);     // sl_foo_t is defined in sl_foo.h

Alternative

// sl_foo_t is defined in sl_foo.h
#ifndef FOO_T_DEFINED
#error This source file needs to #include sl_foo.h
#endif

void sl_bar(sl_foo_t baz);     // sl_foo_t is defined in sl_foo.h

You should strive to not include more header files than you really require in a given C file. Having more than you need increases compilation time and increases the chance of unintended side effects. <> and "" are used by the compiler to change the search path for included files. In general, <> should only be used for C standard libraries. #include should generally not be used for C files.

Example

#include <stdlib.h>
#include "SI_C8051F850_Register_Enums.h"
#include "SI_Bluetooth_Init.h"

3.2.5 Use of preprocessor directive #undef (Recommended)

Using #undef should be avoided as it can lead to weird behaviors that are often fairly difficult to detect and solve. For example, errors can occur only when compiling files in a certain order.

3.2.6 Use of preprocessor 'stringify' and concatenation (Recommended)

Use of the preprocessor 'stringify' or string concatenation operations should be avoided or kept to a minimum. Manipulations such as these can quickly become quite complex to understand, maintain or debug and should be avoided whenever possible.

3.2.7 General use of preprocessor (Recommended)

The preprocessor should be used reasonably, mostly for usual operations such as doing conditional compilation including that described in section 3.2.2, including files, reporting an error, etc. Using the preprocessor to do complex or unusual operations should normally be avoided.

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.2 Use of stdlib (Recommended)

You can use the stdlib functions memset, memcpy, memmove, memcmp, strcmp, etc. instead of writing your own for-loops. In code bases where these functions have been abstracted (e.g., by MEMSET, MEMCMP, and friends in the base repo), it is recommended to use those abstractions. Otherwise, for Cortex-M class devices it is recommended to use the stdlib versions as they are often heavily optimized.

3.3.3 Use of dynamic memory / the heap (Recommended)

Dynamic memory allocation with malloc() and free() should generally not be used. Some of our external developers have strict rules in their QA procedures that dictate that malloc() cannot be used in embedded software. If we use it, then our libraries become unusable for them. The problem with malloc()/free() is fragmentation. Because our micros are memory constrained and have no virtual memory, malloc() can over time fragment memory, which in turn can cause a malloc() to fail down the road.

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 below (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.1 The "volatile" keyword (Recommended)

Be careful using the volatile keyword. Access to a volatile variable/constant can never be optimized away. Only variable/constants that can have side effects and/or that can change value from outside the embedded application should be declared volatile. Typically Special Function Registers (SFRs) are volatile.

Delay loops using volatile are usually a bad idea since they consume both CPU time and energy. It is better to implement delays with for example timer/counter interrupts. But there are exceptions when using a volatile variable may be favorable, such as:

  • Very short delays when timer setup and interrupt handling is time consuming, especially when the latency is longer than the delay period itself.
  • Applications that are constrained on timer resources.

As an example, the Simplicity SDK's "udelay.c" file in kits/common/drivers is a good example of a "busy wait" delay loop, and is used successfully in the Sharp Memory LCD device driver where we need delays of down to 2 microseconds.

3.4.2 The "inline" keyword (Recommended)

The inline keyword is supported in C99, which is the language standard use for our 32-bit MCUs, but behaves differently compared to inline in C++. Inline was also added/used by some C89/C90 compilers and those implementations might also differ compared to the C99 standard.

So adding inline to your code is usually not a problem. Moving code that uses inline and was built with a C++ or C89/C90 compiler might change the behavior of your application.

Only very small (few lines of code) global functions that can be called from many different files can be inlined.

For Cortex-Mx devices, use the CMSIS __INLINE macro since this will expand to whatever keyword is used by the supported set of build tools.

Generally speaking, inline is just a hint to the compiler to inline the function body at the call site. It still can decide not to do so. In the case of a global function where the body is defined in a header file, you could end up with multiple definitions of the same symbol. A solution is to use static inlining, but that can duplicate the implementation of the function in each compilation unit that invokes it. A better recommended approach is to not use static inlining but add an external reference to the function in a single (related) c file. That will tell the linker to only keep one definition of the function and put it in this compilation unit.

Example

main.c

#include "lib_temp.h"

int main (void)
{
  int temp;

  temp = temp_get_temp();

  return 0;
}

lib_temp.h

__INLINE int temp_get_temp(void)
{
  return the_temp;
}

lib_temp.c

// In case temp_get_temp() is not inlined, only one instance will be kept here.
extern __INLINE int temp_get_temp(void);

In case there is absolutely no obvious related .c file where to put the external reference for an inline function, it is recommended to keep using __STATIC_INLINE.

3.4.3 The "static" keyword (Recommended)

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)

This is especially important for aggregate types shared between pre-built libraries (e.g. RAIL) and externally-compiled code (e.g. emlib). The problem is that the size of enums is not standardized, but is compiler-defined. Even within the same compiler, there could be options to select one style vs. another. Libraries built with one compiler or options will not interface properly to non-library code built with a different compiler or options -- field offsets and structure / array sizes could mismatch.

Example

// sl_lib.h include file:

typedef enum {
  SL_LIB_OPTION_A, // 0
  SL_LIB_OPTION_B, // 1
  SL_LIB_OPTION_C, // 2
} sl_lib_option_t;

typedef struct {
  sl_lib_option_t default_option;
  uint8_t some_other_field;
} sl_lib_config_t;

extern void sl_lib_init(sl_lib_config_t *config);
extern void sl_lib_set_options(sl_lib_option_t *option_list, uint32_t option_count);

// sl_lib.c implementation:
//
// Built with compiler or options where enums are the smallest type
// able to represent all of its defined values, so:
// sizeof(sl_lib_option_t) == 1 (int8_t can represent 0..2)
// sizeof(sl_lib_config_t) == 2, offsetof(some_other_field) == 1

// Application code:
//
// This code might be built with compiler or options where enums are
// of type int (i.e. int32_t on an I32 platform).

#include "sl_lib.h"

const sl_lib_config_t SL_LIB_CONFIG = {
  .default_option = SL_LIB_OPTION_A,
  .some_other_field = 10,
}

void sl_lib_use_lib(void)
{
  sl_lib_option_t lib_options[] = { SL_LIB_OPTION_B, SL_LIB_OPTION_C };

  // sizeof(SL_LIB_CONFIG) == 8, offsetof(some_other_field) == 4
  sl_lib_init(&SL_LIB_CONFIG);

  // sizeof(lib_options) == 8, sizeof(*lib_options) == 4
  sl_lib_set_options(lib_options, sizeof(lib_options)/sizeof(*lib_options));
}

3.5.2 Avoid bitfields in structures/unions (Required)

This is also especially important for aggregate types shared between libraries and externally-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.6.1 Using global variables (Recommended)

Minimize use of global variables. It is hard for a compiler to optimize code using them. The compiler (usually) only sees one C file at the time and does not know if the global has been changed between accesses.

A way around this might be to copy the global to a static/auto variable and use the copy in your code.

Global variables can also easily become a source for confusion and errors in the application code as time goes on. As different developers work on the code they might use the global in slightly different ways and accidentally break something in parts of the code they are not working on. This recommendation should be evaluated at an architecture/project level to determine if it applies.

8051 Note: The 8051 compiler will often produce more efficient code when using a project global instead of sharing global data through a handle pointer or other mechanism. This trade-off should be taken into account when writing code targeted for 8051. As much as possible, steps should still be taken to minimize the use of global 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 functions that can fail should return sl_status_t. The idea is to be as consistent and predictable throughout all of our code base to make it easier for others to know what to expect from our functions.

There will be functions that will not return sl_status_t. 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 the Review Committee team 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 preferably done through returning sl_status, 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.

Example

// Don't do:
bool sl_do_something(sl_type_t *var)
{
  sl_status_t my_status;
  // [...]
  my_status = sli_do_anything(var);
  // [...]
  if (my_status == SL_STATUS_OK) {
    return true;
  } else {
    return false;
  }
}

sl_status_t sl_net_is_link_up(bool *is_up)
{
  // [...]
  *is_up = true;
}

// Instead do:
sl_status_t sl_do_something(sl_type_t *var)
{
  sl_status_t my_status;
  // [...]
  my_status = sli_do_anything(var);
  // [...]
  return my_status;
}

bool sl_net_is_link_up(void)
{
  // [...]
  return true;
}

sl_usbh_action_t sl_usbh_on_device_connection(void)
{
  // [...]
  return SL_USBH_ACTION_ACCEPT;
}

sl_usbd_handle_t sl_usbd_get_handle(uint8_t id)
{
  // [...]
  if (id < max_id) {
    return sli_usbd_handle_table[id];
  } else {
    return SL_USBD_HANDLE_INVALID; //defined to something invalid and properly documented
  }
}

3.7.3 Return values should always be checked and propagated (Recommended)

Values returned by any function should always be checked to see if an error occurred. Such return values should normally be propagated up the callers tree up to the application or to a function that can react to it.

3.7.4 Functions replicating a standard API can follow that API (Recommended)

If a function aims to replicate a standard API (like strcpy or printf), our version of the function can replicate that standard function's API. These functions do not need to follow our coding standards' directives relating to data types, return values or status. It should still follow our naming convention.

3.8 Macros

3.8.1 Macros with side-effects (Recommended)

Macros that may have have side-effects should be function-like and named appropriately.

Example

#define sl_lock()     do {__acquire_mutex(&sl_top_level_mutex);} while(0)
#define sl_unlock()   do {__release_mutex(&sl_top_level_mutex);} while(0)

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 someone uses it without braces in a selection (if (...) FOO();) or iteration (while (...) FOO();) statement.

Example

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

#define sli_do_nothing()   (void)0

void sl_bar(int baz)
{
  if (baz) {
    SLI_FOO(123, 321);
  } else {
    sli_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. E.g. the printf and sprintf functions are better avoided within a library as they require a large footprint

Exception: They can be used for generating debug information as long as they are conditionally compiled in with a flag that defaults to false.

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 and ask for permission before introducing new open source software into our distributions. Consult the Review Committee before including any third party software code intended to be released.

3.10.3 Configuring libraries in source form (Required)

A developer 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 application project. Normally, this can be done with macros that allow a developer 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.10.4 Configuring libraries in binary form (Recommended)

If a library is provided in binary form, then macros cannot be used for configuration settings.

A good alternative is to use callback functions or static variables. Callbacks might not be appropriate for 8-bit code, so use your judgment.

Example

// We call sl_custom_configuration_callback() from our library to get the
// user defined configuration.
//
int configuration = sl_custom_configuration_callback();

// The user have to define sl_custom_configuration_callback() and return a
// valid configuration for the application.
//
int sl_custom_configuration_callback(void)
{
  return OPT_SIZE | MAX_BUF_SIZE;
}

Comment: sl_custom_configuration_callback is defined in user application and returns different values depending on the user implementation.

Example

// We initialize configuration with a sensible default that is suitable
// for the largest number of applications
//
int configuration = SL_DEFAULT_CONFIGURATION;

// The user application can customize the behavior by calling
// sl_set_configuration
//
void sl_set_configuration(int user_config)
{
  configuration = user_config;
}

3.11 Misc

3.11.1 Avoid embedding assignments in expressions (Recommended)

Embedding assignments in expressions makes for all kinds of wacky bugs. When scanning some code it is easy to miss that a complicated expression contains an assignment deep inside.

Example

++ceiling;
*handle = ceiling;

Comment: The above is easy to understand, hard to get wrong and most compilers today will generate the same optimized code that that example as it will for the below code:

*handle = ++ceiling;

3.11.2 Use designated initializers in structure literals (Required)

Structure literals used in initializations shall use the C99 designated initializer syntax. The only exception is defined literals exposed via header file. Because C++ does not fully support C99 style designated initializers, and we want to make our headers usable from C++, initializers in headers must use the structure order syntax. Structures allocated automatically (on the stack) shall be initialized when declared.

Example

foo.h: typedef struct { int bar; int baz; bool quux; } foo_t;

#define FOO_DEFAULT {3,5,false}

foo.c: foo_t tuna = { .bar = 3, .baz = 5, .quux = false }; foo_t salmon = FOO_DEFAULT;

Comment: Literals without field identifiers create backward compatibility traps whenever structures are updated. Structures that are not initialized when declared can contain random values on the stack, leading to subtle bugs.


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.2.1 Preprocessor indentation (Recommended)

Preprocessor directives historically have not been indented, but they may be indented to make them less distracting to the module's code flow (though the technique described in the section 3.2.2 offers an even cleaner alternative where it makes sense). When indented, the # should remain attached to the directive and not remain in the first column -- no modern preprocessor still requires the # be in the first column. When an #if or #ifdef is indented, its #else, #elif, and #endif shall also be identically indented. The code between the preprocessor directives may also be indented.

Example

void sl_set_xyz_option(xyz_option_t xyz_option)
{
  #if defined( _XYZ_LFCCLKEN0_MASK ) // XYZ supports LFC clock
    if (xyz_option == xyz_clock_lfc) {
      xyz_clock_set_lfc();
      return;
    }
  #endif
  #if defined( _XYZ_LFECLKSEL_MASK ) // XYZ supports LFE clock
    if (xyz_option == xyz_clock_lfe) {
      xyz_clock_set_lfe();
      return;
    }
  #endif
}

4.1.3 Lines should not be longer than 80 characters (Recommended)

We enforce an 80 characters limit per line of source code. This lets people set up their editors such that they can have multiple editors side-by-side. Although 80 characters are little by modern standards, it mixes well with existing code.

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 developers are using Windows operating system which expects a CRLF line ending. Therefore, with rare exception, all source code 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/Indentation_style#Variant:_1TBS\_(OTBS)) 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.

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.6 Identify do-nothing code (Recommended)

Avoid loops that do not make it obvious that they do nothing. Add a comment to explain the reason for the do-nothing loop. This applies to any code that does not do anything.

Example

// Don't do:
while (waiting_for_something);

// Instead do:
// Useful comment explaining why you are waiting
while (waiting_for_something);

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.

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.

The prefix will be the same for all constructs, it will only change based on whether that construct is public (can be used by end users), internal (can only be used from within Silicon Labs code), static (local to a file) or experimental (may change without further notice during prototyping).

If a construct is public, it should be prefixed by sl_.

If a construct is internal, it should be prefixed by sli_.

If a construct is experimental, it should be prefixed by slx_. For code which is not yet following this coding standard to the full extend the prefix EXPERIMENTAL should be used.

For example, this means that no public function would refer to either a private or internal type in one of their parameters.

Those constructs' names should then be followed by a <module>_ prefix, that can consists of either one or two words, separated by an underscore. For example, this could be uart_ or usbd_msc_. The <module>_ prefix can be omitted if the code is intended to be common code used by any other module and is placed in platform/common or a similar common location.

Constants and macros will use capitalized version of these prefixes (SL_, SLI_, SLX_ and <MODULE>_).

As previously stated, function parameters, variables local to a function and structure fields will be prefixed by neither the sl[i;x]/SL[I;X] nor the _/ prefix.

If a construct is static or in any way local to a single file, it is not required to have any prefix at all. If it has some kind of prefix, it should not use anything starting with sl.

Example

// File-local
#define SLI_READY 0x0020

static uint8_t next_transmit;

static urb_t transmit_buffer(uint8_t *buffer);

typedef struct {
    uint8_t  urb_size; // Struct fields do not have any prefix.
    uint8_t  *urb_list_next;
    uint16_t array[10];
} urb_t;

// Internal-only
#define SLI_UNUSED_PARAMETER(param)   (void)(param)

const unsigned int SLI_MAX_UART_CONNECTIONS = 3;

sl_usbd_endpoint sli_usbd_msc_endpoint; // Internal variable using public type.

typedef struct {
  uint8_t time_ms;
  uint8_t *longer_blah_ptr;
} sli_usbd_msc_cnt_t;

void sli_usbd_msc_tx_endpoint(sli_usbd_msc_cnt_t *buffer,
                              uint8_t            count);

void sli_usbd_msc_tx_endpoint(sli_usbd_msc_cnt_t *buffer,
                              uint8_t            count)
{
  uint8_t flag; // Local variable does not have any prefix.
  urb_t urb; // Referring to static type from within public function is ok.
}

// Public

#define SL_CREATE_HANDLE(node, id) (node).flag = ((id) | 0xFF00)

const unsigned int SL_USBD_VERSION = 20200;

sl_uart_channel_t sl_uart_channels_list[10];

sl_time_t sl_get_time(void);

typedef struct {
    uint32_t hour;
    uint32_t minute;
    uint32_t second;
} sl_time_t;

// Experimental

#define SLX_CREATE_MYHANDLE(node, id) (node).flag = ((id) | 0xAA00)

const unsigned int SLX_USBD_VERSION = 20200;

sl_uart_channel_t slx_proto_uart_channels_list[10];

slx_mytime_t slx_get_time(void);

typedef struct {
     uint32_t hour;
     uint32_t minute;
     // uint32_t second;
} slx_mytime_t;

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 happening, 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);

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.

All #defines that are intended to be used by applications and part of the API need to be prefixed by SL_. This includes #defines in configuration files.

All #defines that are local to a C file or private to our products (not intended to be used by applications; present in a private header file) need to be prefixed by SLI_. This is to prevent name clashes with compiler defines (with -D) that could be specified by the user when building our source files.

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) must include the namespace prefix of the module as well as the sl_ or sli_ prefix (see section 9.2). Source files must always be prefixed by sl_ while header files (including configuration files) must be prefixed by sl_ if they only contain public APIs (functions, defines, data types, etc.) and by sli_ if they only contain private things.

Example

sl_simple_mpu.c
sl_status.h
sl_sleeptimer_config.h
sli_iostream_swo_itm_8.h

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.

4.6.9 UC-Instantiable components

The following section (4.6.9.*) defines naming format for UC-Instantiable component related information (configuration files, macros and handles)

4.6.9.1 Configuration file names (Required)

The configuration files for every instance of an instantiable component must have the following format: sl_<module>_<type>_<instance_name>_config.h

Example

// instance_name - vcom
sl_iostream_usart_vcom_config.h
// instance_name - btn0
sl_simple_button_btn0_config.h

4.6.9.2 Configuration macros (Required)

The configuration macros for the instantiable components must have the following format: SL_<module>_<type>_<instance_name>_CFG_VAL

Example

// instance_name - vcom
#define SL_IOSTREAM_USART_VCOM_BAUDRATE 115200
// instance_name - btn0
#define SL_SIMPLE_BUTTON_BTN0_POLARITY 0

4.6.9.3 Instance handles (Required)

The handles for the instantiable components must have the following format: sl_<module>_<instance_name>

Example

sl_iostream_vcom
sl_button_btn0

5 Documentation

5.1 General

Write documentation so that others can easily pick up the code. It makes life easier for everyone.

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 externally, 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 Javadoc style is used:

/**************************************************************************//**
 * doc comments ...
 *****************************************************************************/

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

Note: The reason we are using C style doxygen for this is that Eclipse (which Simplicity Studio is based on) can do correct code folding of this type of blocks.

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.2 Grouping modules (@addtogroup) (Recommended)

Use the Doxygen command @addtogroup to group together source code that belongs to the same module, for example UART and SPI and so on. This will make it easier for developers to find the API documentation for the modules they are interested in.

It is recommended to add experimental source code making use of the SLXor EXPERIMENTAL prefixes to a group called experimental as indicated in the example below.

It is also possible to create groups hierarchies, for example to have an ADC group in the EM_Library group.

Example

/**************************************************************************//**
 * @addtogroup EM_Library
 * @{
 *****************************************************************************/

/// variable, constants and code belonging to EM_Library
/// (but not in the ADC module).
uint32_t sl_some_global_variable;

 /**************************************************************************//**
 * @addtogroup experimental
 * @{
 *****************************************************************************/

/// experimental variable, constants and code belonging to EM_Library
/// (but not in the ADC module).
uint32_t slx_some_experimental_global_variable;

/** @} (end addtogroup experimental) */

/**************************************************************************//**
 * @addtogroup ADC
 * @brief Analog to Digital Converter (ADC) Peripheral API
 * @{
 *****************************************************************************/

/// variable, constants and code belonging to EM_Library and ADC module
bool sl_is_adc_configured;

/** @} (end addtogroup ADC) */

/** @} (end addtogroup EM_Library) */

/// @} (end groupName)

6.3 File/module level documentation (Recommended)

If a file is part of a module, it may be desirable to include file-level documentation. For example, the file level documentation may provide an overview of how to use an API. To provide file-level documentation, a Javadoc style Doxygen comment block should follow the file include statements.

Example

/**************************************************************************//**
 *
 * @addtogroup adc_group ADC API
 *
 * @brief Brief one-sentence description of ADC module.
 *
 * The rest of this section can be multi paragraph explanation of the
 * module. It can include tables and code examples. End the description
 * as shown below and this will collect the remaining API documentation
 * into this group.
 *
 * @{
 *
 *****************************************************************************/

If a group is created as shown above, it needs to be closed at the end of the file using one of the two methods shown below:

// The following is used if there is only one group in a file or
// groups are not nested.

/// @} end adc_group

// The following is used when groups are nested. This is needed so
// that doxygen does not pick up the "end adc_group" as part of a doc
// comment for a following or enclosing group.

/** @} end adc_group */

Comment: The method used is optional. If in doubt about whether the second method is needed, run Doxygen on your file and see if the "end" comment is being picked up in the documentation.

6.4 Function documentation (Required)

This is required for any library or module code.

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 Javadoc 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;

6.6 Header file vs. implementation file (Recommended)

To keep a simple distinction between public documentation and internal documentation, all public documentation comments should be placed into the library or module header (.h) file. This allows us to just pull in header files when generating public-facing documentation and not need to worry about separation of public or private content.

The implementation file (.c) should also be documented if there additional non-public functions or variables that are not part of the public documentation. The .c doc comments can be used for generating internal documentation.

If a function in a .c file is already documented in the header file, then it is not necessary to repeat the documentation. However, a comment block should be used with the brief description to visually mark the function in the source file.

Example

/**************************************************************************//**
 * Brief description of function.
 *****************************************************************************/
 uint8_t sl_do_something(uint8_t my_param1, uint16_t my_param2)

If the function is a private (static) function then it should use a normal comment block (same as public function in a header file). Private variables should also be documented.

6.7 Do not document sections (Recommended)

The Doxygen configuration file for the project should be configured to exclude undocumented objects. Therefore it is not necessary to add extra statements to exclude Doxygen processing. Simply do not add Doxygen comments to the code you do not want documented.

Example

/// The following variable will be picked as part of the
/// documentation.
uint8_t sl_my_public_variable;

// This variable should also have a comment to explain it,
// but by using only double-slash, it will not get picked
// up by Doxygen.
static uint8_t my_private_variable;

/******************************************************************************
 * This is a function that will not be documented.
 *
 * Note the lack of the double slash at the end of the top line. That
 * means this is not a true Javadoc comment block and Doxygen will not
 * see it.
 *****************************************************************************/
void sli_do_something_secret(void)
{
  // ...
}

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 Open-Source/zlib license text is used for select software such as our CMSIS-Device header files and emlib, but can also be applied to other parts of the code. Only use this license text if the module already uses it, or with permission from the Review Committee.
  • The Apache 2.0 license text is used for select software, but can also be applied to other parts of the code. Only use this license text if the module already uses it, or with permission from Review Committee.
  • 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. Using and releasing third-party code has more constraints, see section 7.2 for the details.
  • The Micrium license text is used for any code that is an additional cost (i.e. the Micrium software stacks, with the exception of the kernel).

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 Simplicity SDK's 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 Review Committee must give 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 Review Committee 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 Universal Configurator (UC) Metadata Specific Guidelines

8.1 Motivations

Now that we are starting to write more and more UC metadata files, we see a need to define a standard for some of the information (component id, provides, description, label, category, etc). This offers many benefits:

  • Simplifies writing metadata: Lots of inter-connections exist in the metadata (for instance when comes time to define the requirements of a module). If the naming (id/provides) of the components follow a standard, it becomes much easier to guess the provides of another component and thus we don't need to dig into the metadata to find the information.
  • Improves look and feel/UX: If there is a standard way of describing/categorizing/etc. the components, the UI tools will look much more consistent and thus improve the UX.

This section, for now, only describes the standard for software module metadata. It does not cover hardware/chip description.

8.2 Metadata good practices

8.2.1 Super components

Description

Super components are components of components. They are normally used to represent concepts or libraries. For example, a component "emlib" can be created and composed of all the emlib drivers. While they are allowed, the super components main usage should be to be included from example application (to showcase some functionalities, for instance).

Standard (encouraged)

  • Should only contain a list of other components (list of requirements) and not define any file list
  • In normal situation, they should not be used as a requirement for another component. As a matter of fact, they should ideally have no "provides"
  • They should only be include-able from a .slcp file and selectable from the UC GUI.

The reasons why this standard recommends against using these super components as requirements from other components are the following:

  • The categorization of components is always evolving. As a matter of fact, the library "emlib" will disappear in a near future. Making these super component's name and content unpredictable.
  • Adding a requirement on a super component when only one (or a few) sub-components are really needed will result in a project that may contain more stuff than really needed. Not everything can be optimized out by the linker, this is likely to end up having a cost in footprints.

8.2.2 Style guideline

The YAML format is quite flexible, so in order to have the same style across the SDK it will help to have some guidelines for how we write the content of the .slcc YAML files.

  • Use 2 space indentation
  • Prefer not to use quotes around elements. This will make the files look clean and nice, and will match what python generation tools produce.
  • When writing multi-line comments, use the : > syntax. There are nine+ different ways of writing multiline comments in YAML, so we should default to : > to be consistent.

8.3 Components metadata (slcc)

8.3.1 File name

Standard

<id>.slcc

Description

From a technical point of view, it doesn't matter. However, having the files always named this way allows to quickly see the id of the components by browsing the component folders without having to open the files.

Example(s)

power_manager.slcc

8.3.2 Component Id / Standard provide names

Standard

<module_name>

<group_name>_<module_name>

<module_name>_<group_name>_<submodule_name>

<module_name>_<group_name>_<submodule_name>_<subgroup_name>_<subsubmodule_name>

All lower case and snake_case

Description

Use module names without taking into consideration any categorizations as much as possible. EMLIB, EMDRV and Micrium OS, for example, should not be seen as components or group.

We should try to make our components as small as possible (basically being atomic pieces of code).

This may be complicated in some cases, for instance some modules have a "common" component such as "micrium os" and "emdrv". Of course we cannot have two components with id = "common". In order to avoid having "micriumos_common" and "emdrv_common" as components, those could be split up in multiple components.

Note that most of these "common to a module" components will have a tendency to disappear with UP.

Groups are added for clarity as generic provides may have to be added. More on that in the following section.

The only components that are allowed to have their id starting with a group are the ones that are hardware specific and have no common parent module (for instance; BSPs).

Example(s)

Component Id
SL_STATUS (part of platform/common) slstatus
Micrium OS - kernel kernel
Micrium OS - Common - Auth auth
Micrium OS - USB Device usbd
Gecko USB device core geckousbd
Micrium OS - USBD MSC class usbd_class_msc
BSP for stk3701 bsp_stk3701

8.3.3 Generic provide names

Standard

<group_name>

<module_name>_<group_name>

<module_name>_<group_name>_<submodule_name>_<subgroup_name>

All lower case and snake_case

Description

Is considered a generic provide something that can be provided by multiple software components, hence having multiple software components having the exact same provide name in their list.

There are typically 2 use cases for those:

Single functionality that can be provided by different modules

Only a single component that has the provide must be added to a project.

Most typical cases are generic hardware drivers. For cases where the driver files are not generic and are tied to a given module (for example: sleeptimer_hal), or if they do not require a choice from the user, those should just be conditionally included to the main module itself.

Necessary add-ons to a given module

At least one component that has the provide must be added to the project

Those are normally add-ons or plugins to a module.

Example(s)

Case Generic provide
BSP. BSP are board specific. Once a board has been selected, only one software module will fulfill the needs. bsp
Kernel port. For a given architecture, different ports with different functionalities may be provided such as FPU support or not. May end up being a user's choice. kernel_port
USB Device classes. At least one usb device class must be added once you add USB device to your project. However, you can add as many as you want. usbd_class

8.3.4 Label

Standard

Module name, in Capital Case and human readable format. For improved readability, parent module name, if any, should be added in the label.

Example(s)

Component Label
SL_STATUS (part of platform/common) Status Code
Micrium OS - kernel Micrium OS Kernel
Micrium OS - USB Device Micrium OS USB Device
Gecko USB device core Gecko USB Device
Micrium OS - USBD MSC class Micrium OS USB Device: MSC Class
BSP for stk3701 STK3701 BSP

8.3.5 Description

Standard

1-3 succinct complete sentences, in simple English, describing the contents and/or purpose of the component or project.

8.4 Validation Libraries

As the components get more and more complex, there will be a need to write an increasing number of validation scripts to ensure that the correct configurations and required dependencies are selected. In order to avoid code duplication, writing more generic functions inside validation libraries and importing those libraries into the validation helpers will greatly improve the readability and maintainability of these scripts.

Validation libraries that are generic enough to be used by components in multiple repositories should be placed in platform/common. The validation libraries target more specifically at a single repo should remain inside that repo. It would be strongly recommended to avoid using libraries from one repository in another, try to see instead if the required library can be made generic enough to be placed inside of platform/common.

Avoid grouping multiple validation functions used for very different tasks in the same library, this will make finding the use case of the library easier.


9 Content organization

9.1 General

This section describes how our data (source code, template, metadata, test code, etc) must be organized.

9.2 Folders name and content

All of our modules must have their content organized as described below. Note that it is not mandatory for a given module to contain all of the folders described here.

9.2.1 inc

Contains all header files that can be included by other modules/application. Only these folders can be added to the include paths. They can contain header files that expose SDK internal features that should not be visible to the application. However, those files must be prefixed with "sli_". SDK internal functions and other symbols must not be exposed from public header files (prefixed with "sl_"). SDK internal header files can be included from other module source files but they must not be included from any public header files.

9.2.2 src

Contains source code (.c, assembly) that is released externally and represent the actual product. No test code should be in this folder. Also contains header files (.h) that are local/internal to the module. These header files can only be included from source files located in the same src folder. These header files must be prefixed by "sli_". The source files must be prefixed by "sl_".