Coding Style
Naming Conventions
- Class names should use
PascalCase- File names for classes should use the exact same name as the class itself, e.g.
PascalCase.handPascalCase.cpp
- File names for classes should use the exact same name as the class itself, e.g.
- Function and variable names should use
snake_case
Software Development Principles
- DRY (Don’t repeat yourself)
- Minimize the amount of repeated code
- Rule of Three
- Refactor once code is repeated 3 or more times
- KISS (Keep it simple, stupid)
- Limit the amount of code in each function (40-50 lines max)
- YAGNI (You Aren’t Gonna Need It)
- Only implement what you need now
Import Rules
- Angle brackets
<>for external libraries (C/C++ standard libraries, Mbed, Unity, FakeIt)- e.g.
#include <mbed.h>
- e.g.
- Double quotes
""for user/project libraries and headers (files we write)- e.g.
#include "CANInterface.h"
- e.g.
Variable Types
- Numeric Types
- Avoid using 64-bit types, as this will put unnecessary strain on our MCU
- This includes
double,int64_t, anduint64_t
- This includes
- Avoid sending floating-point types over CAN
- For integers, don’t use the ambiguous types
int,unsigned int,short,long, orlong long- Instead use
int8_t,uint8_t,int16_t,int32_t
- Instead use
- Literals
- For floating-point numbers, use the
fsuffix- e.g.
float x = 1.5f; - Leaving out the
fsuffix will automatically use a double, putting unnecessary strain on the MCU
- e.g.
- For integer numbers, use
Lforint32_t,uforuint16_tanduint8_t,uLforuint32_t, and no suffix forint16_tandint8_t- e.g.
uint16_t x = 123u;
- e.g.
- For floating-point numbers, use the
- Avoid using 64-bit types, as this will put unnecessary strain on our MCU
- Null Type
- For pointers, use
nullptr- This is the C++ equivalent of the
NULLmacro in C, but comes with explicit type checking
- This is the C++ equivalent of the
- For
chartypes, use'\0' - Never use the literal
0for null pointers
- For pointers, use
- Time Durations
- Use
std::chronodurations for all time durations (as Mbed has deprecated the use of integer or float types for all time durations) - e.g.
std::chrono::milliseconds
- Use
- References vs Pointers
- Whenever possible, use references
- Otherwise, use pointers
- Reasons why you would need to use pointers include:
- The variable may be null
- The memory location of the variable may need to change (i.e. the pointer changes)
- The initial memory location of the variable is unknown during initialization
Formatting
- Use four spaces for indentation instead of the tab character
'\t'(this can be changed via a setting in your IDE or text editor)
Dependency Injection
Dependency injection is a technique used to ensure our code can be unit tested with mocks of all dependencies used by our code. It fundamentally relies on all our code receiving all of its dependencies externally rather than instantiating any dependencies internally.
What is a Dependency?
Anything that can be described as a “uses a” relationship between classes. For example, all Mbed classes we use are dependencies of the classes we write, so Mbed’s CAN class is a dependency of our CANInterface class, because CANInterface “uses a” CAN object.
How are Dependencies Mocked?
FakeIt is used to create an instance of a mock of our Mbed dependencies. Then that mock instance is passed to the unit of code (usually a class) we are testing. This tricks our code to think all Mbed dependencies are real Mbed implementations when they are actually all mocks with little or no actual functionality implemented.
How this Affects how we Write our Code
All dependencies (specifically Mbed objects) we use in any of our libraries must be instantiated outside of our libraries and simply passed in on initialization. For classes we write, this means the constructor must take in all Mbed dependencies as inputs and store references or pointers to these inputs instead of simply initializing these dependencies in the constructor.
The actual initialization of all Mbed dependencies must be done either in main.cpp (for release builds) or in testing code via the mocking API (for testing). In either case, the initialized dependencies will be passed in to our initialization or constructors to fill the pointers/references.
This works because of polymorphism: the mock instances are effectively concrete subclasses of the abstract Mbed mock classes we write.
Example
Below is the definition of our CANInterface class.
class CANInterface
{
public:
CANInterface(CAN &c, CANParser &cp, Thread &tx_thread, Thread &rx_thread, DigitalOut *stby=nullptr, std::chrono::milliseconds can_period = 1s);
void startCANTransmission(void);
private:
void rx_handler(void);
void tx_handler(void);
CAN &can;
CANParser &can_parser;
DigitalOut *standby;
Thread &tx_thread;
Thread &rx_thread;
std::chrono::milliseconds tx_period;
};
Notice how the constructor accepts all Mbed dependencies used by the CANInterface class as reference or pointer inputs. For example the CAN object is taken as a reference, while the DigitalOut object is taken as a pointer. The distinction between whether a dependency should be a reference and a pointer is that optional dependencies should be pointers (which can be null), while required dependencies should be references (which cannot be null). So in our example, the DigitalOut object is optional, while all other Mbed dependencies are required.
Note that when using references for class data fields, they must be instantiated before the start of the constructor. This can be done as shown in the implementation of our CANInterface class constructor, using C++ constructor initialization lists.
CANInterface::CANInterface(CAN &c, CANParser &cp, Thread &tx_thrd, Thread &rx_thrd, DigitalOut *stby, std::chrono::milliseconds tx_prd) : can(c), can_parser(cp), tx_thread(rx_thrd), rx_thread(tx_thrd), tx_period(tx_prd)
{
if(stby)
{
standby = stby;
*standby = 0; // active low
}
}
Coding Standard
TODO