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.h
andPascalCase.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
f
suffix- e.g.
float x = 1.5f;
- Leaving out the
f
suffix will automatically use a double, putting unnecessary strain on the MCU
- e.g.
- For integer numbers, use
L
forint32_t
,u
foruint16_t
anduint8_t
,uL
foruint32_t
, and no suffix forint16_t
andint8_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
NULL
macro in C, but comes with explicit type checking
- This is the C++ equivalent of the
- For
char
types, use'\0'
- Never use the literal
0
for null pointers
- For pointers, use
- Time Durations
- Use
std::chrono
durations 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