Programming is simply the process of writing code, a specific "language" that computers can "understand", in order to make them function (if you're joining firmware, you should know this much already). Writing firmware is different than other types of software because firmware engineers have to care much more about the physical nature of devices. You may commonly hear to firmware (aka embedded) referred to as "low-level" programming. This is in contrast with "high-level" programming, which is generally things like creating applications or websites for computers and phones. Note that the level in low-level and high-level is not level of difficulty, but level of abstraction. If you were writing a website, for example, you don't care what CPU, GPU, or (to an extent) screen type your users' computer has; there are prewritten libraries that handle those differences (thus, it is a "high level of abstraction"). When we write firmware, we do care about the physical details of a device. We need to know what pins are connected to other devices, and how large our end program will be, and other concerns that are not common in the majority of high-level programming.
Programming languages have a relatively standardized structure to them, and learning one will make learning any others immensely easier. If you are completely new to programming, that's okay, but don't expect to understand everything the first time you read it. Learning programming is one part learning and nine parts trying (and failing). For people that are completely new, investigate this webpage. Don't focus on specific languages/details (although if you do look at them, the C++ examples will be most useful!), that will come later, but try to understand the 5 core concepts.
NOTE: We don't use dynamic memory management in our code due to hardware constraints, so you don't need to spend time learning that right away.
Read through the following articles. Note that reading through the W3 Schools or Geeks for Geeks pages on C++ will get you most of this, but some stuff will simply not be relevant. Most significantly, all these tutorials use "cout" to print to the console. In embedded development, there is no console, so UART is used instead. For more information on the print method we use for UART instead of cout, see the printf C++ documentation (especially the examples section). While this printf method prints to the console (which again, we do not have in embedded), a functionally identical printf is used for the UART terminal.
If you have programming experience, some of these articles (basic syntax, datatypes, and functions) will be less important, but make sure to read all the articles with a [ ! ] next to them, as they are features that are relatively unique to C++ (or in some cases, C).
Articles/Written Resources
In EVT, we almost never use the float or double datatypes- floating point numbers have precision issues that can lead to inaccuracy when performing mathematical operations on them. Additionally, instead of using the simple int type, we use the C <stdint.h> integer types; these alternate integer types allow us to specify the size of each integer with a high degree of accuracy.
For instance, when writing
int a = 5;
The compiler will most likely allocate 32 bits (4 bytes) to the variable a. This means that a can store numbers from -2,147,483,648 to 2,147,483,647. Notice, however, that this is most likely; depending on the architecture of your cpu and the compiler, the int datatype might be allocated a different number of bytes. This variability is normally not really a big deal, but for embedded applications, knowing the size of a datatype can be very significant. Certain IO protocols assume that all sent values are of specific length.
So, in EVT-core, instead of the above line, you would most likely see something like:
uint8_t a = 5;
In this situation, a is of type uint8_t. Each part of this name is significant:
u - stands for unsigned. This means that a cannot have a negative value.
int - a is still an integer type.
8 - a is 8 bits (1 byte) long. This means that a can represent the numbers 0 to 255.
_t - In every datatype of this variety, _t simply stands for type. This simply represents that uint8 is a type, and not a name of a variable. Datatypes in the <stdint.h> header have this datatype to signify that they are platform independent- a uint8_t is always 8 bits long.
uint8_t, uint16_t, uint32_t, and uint64_t are the primary datatypes used in EVT. When negative numbers are needed, the corresponding int8_t, int16_t, int32_t, and int64_t datatypes are used.
When choosing what type to use, it is generally a safe bet to use uint32_t, since it can represent any number from 0 to 4.2 billion, which is typically more than enough. When you choose smaller datatypes, like uint8_t, you must be sure that the maximum value that your variable could be expected to hold is lower than the maximum representable value of the datatype. This is because attempting to increase a variable past it's maximum value will result in "overflow", where the value will "wrap around" to the lowest possible value.
For instance, if we executed the following lines:
uint8_t a = 255; //255 is the maximum value of a uint8_t
a += 1; //overflow wraps around
printf("The value of a is %u.", a); //prints the value of a. %u indicates that we are printing an unsigned number.
The output of our print statement would be "The value of a is 0.". This is because a is a uint8_t, which has a maximum value of 255. When a is increased past 255, it wraps around to start back at 0. Note that if we had incremented a by 2 instead of 1, the value of a would then be 1.
Any EVT-core namespace have the structure core::area::specific. For example, if you wanted to initialize UART, you would refer to UART as core::io::UART, because the UART class is inside of the core::io namespace (IO is an abbreviation for Input/Output). To avoid having to continually write core::io, you will often see at the beginning of files the line
namespace io = core::io.
This tells the compiler that when you write io, you really mean core::io. In a file with this line, you would be able to refer to UART simply via io::UART, instead of core::io::UART.
In EVT-core, there are 3 primary namespaces that you will need to concern yourself with:
core::io - Any input/output protocols exist within this. GPIO, UART, I2C, SPI, CAN, and CANOpen all are within this namespace.
core::dev - Any "developer" classes exist within this namespace. This includes stuff like LEDs, LCDs, and Encoders.
core::utils - The time and log classes exist within this namespace.
There are two other namespaces in EVT-Core, core::platform and core::rtos, but they will not be largely relevant for rampup (in every project main method you will call the function core::platform::init(), but otherwise these namespaces are not commonly used).
Any non-core namespace should be a namespace that describes the set of devices. In the rampup project, for instance, all of your objects are inside of the rampup namespace. When working on a specific board, the objects you create will be inside of that board's namespace (for instance, if you were working on the BMS (battery management system), you would be working inside the bms namespace.
C++ Header Files [ ! ]
References and pointers are by far the most difficult part of working with C++, so don't feel discouraged if you feel confused by them. Feel free to read over this C pointers guide if you want more information. Pointers in C and C++ work identically, so this resource is useful for C++ as well as C.
Pointers are important in C++ and C because they make the difference between copying and referencing objects explicit, unlike in languages like Python and Java where copying and referencing differ implicitly. Additionally, in C++ and C, arrays are referenced via pointers. It is incredibly common to see a method header like:
doSomethingWithArray(uint8_t* arr, size_t length);
In this instance, arr is not a pointer to an integer, but is actually a pointer to the beginning of an array of integers. The length variable holds the length of the array arr. This works because an integer array is really a series of consecutive integers held in memory. In C and C++, the [] operator (as in arr[2]) is actually functionally equivalent to *(arr + s*i), where s is the size of the datatype the array is holding. This means that you may see something that looks like a pointer, but is actually an array. This is why documentation is very important: good documentation (and hopefully good variable names) will always tell you if something is a pointer or an array.
C++ Templates (don't spend too much time on this, you should just be able to recognize what a template is and how to use it, not necessarily how to create one) [ ! ]
C++ Macros [ ! ]
Macros are very important, and are used primarily for avoiding "magic numbers". These are instances where you need a specific, somewhat arbitrary, number for something (say the size of an array). It is very poor practice to simply leave those numbers in your code with no explanation. For example:
int arr[5]; //this is a magic number- why is the array of length 5?
In most programming languages, you would simply create a variable with a value of 5 and use that. However, creating a variable in this situation is largely unnecessary. Instead, a macro would work excellently:
Somewhere at the top of the file
/// The maximum number of elements in the array
#define MAX_ARRAY_ELEMENTS 5
...later in the file...
int arr[MAX_ARRAY_ELEMENTS];
Of course, as a toy example, our macro name isn't that descriptive. In real circumstances, macros are very useful and make your code far more readable. Additionally, they can be created in ways that allow overriding:
#ifndef MAX_ARRAY_ELEMENTS
#define MAX_ARRAY_ELEMENTS 5
#endif //MAX_ARRAY_ELEMENTS
In the above example, the define statement (also called a pound define), is wrapped in a #ifndef statement. This statement checks to see if a macro with the name MAX_ARRAY_ELEMENTS has already been defined, and executes the code it is wrapped around only if that macro has not been defined (for the opposite effect, use a #ifdef!). Macro names should always be in MACRO_CASE (all caps with underscores).
Additionally, there are other uses for macros- they can even be used to create function-like sequences, with the advantage of compile-time evaluation. Generally, however, it is encouraged to use macros sparingly, as too many can become confusing and difficult to handle.
Fun fact! Include statements (#include) are in-and-of themselves essentially macros; when compiling your program, the compiler will simply paste the entire contents of any files pointed to by an #include line, and will recursively paste that file's #includes as well.
C++ Include Guards [ ! ]
If you have experience with Python, check out this C++ for Python Developers article.
Videos
Object-Oriented Programming (OOP) is one of the major programming paradigms, and the one we use to structure our code. It is based on the idea of "objects" interacting with each other to execute the program. This conceptual models provides a number of benefits for working with the code. It can make the code more organized, more readable, more easily testable, etc. Our whole codebase is written in this way, so it is important to have a good grasp on this idea before you get started. Below is a list of videos and resources for learning about OOP. Use these as a starting point to begin researching the topic and as a reference to look back on as you learn more. Also, if you find any videos you find particularly helpful that are not already on this list, be sure to share them with the firmware lead.
If you're already familiar with object-oriented programming, still make sure to read through the C++ Classes link below, which has important syntax details about how C++ classes function that are slightly different than other OOP languages like Java.
Articles/Written Resources
Videos
JAVA OOP Playlist