You are not Logged In!

Public:Onboarding Fall 2022 - Basics of C++

From Illini Solar Car Wiki
Jump to navigation Jump to search

Some of you might be familiar with C++ already, but many of you probably aren’t. Having basic knowledge of an object-oriented programming (OOP) language like Java or Python (C++ is also an OOP) will help in learning C++, but don't worry if you have no programming experience.

The W3Schools website has very good online C++ tutorials. I'll link to those if you want to become more familiar with these C++ concepts - a lot of this page will focus on examples from our code rather than going over C++ concepts in detail.

NOTE: Some of the linked tutorials reference the "cout" object, which is used to print things to the terminal. You can't actually use "cout" in your firmware because there's no support for printing stuff (there is some special hardware/software you can set up to print things, but we don't have that available for the onboarding projects).

Intro

W3Schools Tutorial - What is C++?

W3Schools Tutorial - C++ Syntax (Basic structure of a C++ program)

Variables

W3Schools Tutorial - C++ Variables

W3Schools Tutorial - C++ Data Types

This is an example variable declaration that you'll see in pretty much every ISC firmware project.

bool shutdown = false;

This particular variable is a boolean used to indicate whether the MCU should shut down - it can either be true or false. It's initially set to false because we don't want the MCU to shut down, but changing its value is as easy as doing this:

shutdown = true; // This sets the variable to true (a double backslash denotes a comment in C++, comments are ignored by the C++ compiler)
shutdown = !shutdown; // This sets the variable to the opposite of its current value

Operators

This tutorial has a few sub-tutorials in the left drop-down menu regarding operators. Look through all of them, as you'll see examples of all of them throughout our firmware.

W3Schools Tutorial - C++ Operators

Operators are pretty easy to understand. The most important thing to know is that operators are an important tool for changing the value (stored data) of variables.

One operator that the W3Schools tutorial overlooked is the bit shifting operator, which are used to shift the binary representation of values left or right. This one is a bit hard to explain until you understand how values are stored in binary (ECE`120), but I'll leave this GeeksForGeeks tutorial if you'd like to check it out. We use bit shifting operators a lot in our firmware (often for bitmaps, a data structure that we use a lot) so it'll be good to know eventually.

Macros

There's no W3Schools tutorial for macros, but you'll see them a lot in our firmware. Here's an example of one that most of our firmware projects use to help us determine the rate at which we want to check the CAN controller, in setup.h.

#define CHECK_CAN_RATE_US 500000

This macro is declared using the #define statement. All it does is tie a name (CHECK_CAN_RATE_US) to a value (500000) so that you can use the macro name in place of the value in your code.

We use this macro in main.cpp like so:

timing.addCallback(CHECK_CAN_RATE_US, checkCANController);

You'll learn more about what this code means later, but basically, we can use the macro name CHECK_CAN_RATE_US instead of 500000. If we want to change this rate later, we can change the macro value in setup.h rather than having to hunt through the main.cpp code and change it there.

Whenever you see an item in ALL_CAPS like this, it's probably a macro. Macro names don't need to be all caps, but we do that to distinguish macros from variables. The difference between macros and variables has to do with how they are treated when the code is compiled/built - in general, use macros for values that won't change while your code is running.

Conditionals (If + Else, Switch)

This tutorial has a few sub-tutorials in the left drop-down menu regarding if/else statements. Look through all of them, as you'll see examples of all of them throughout our firmware.

W3Schools Tutorial - C++ Conditionals

W3Schools Tutorial - C++ Switch Statements

This is an example of an if/else statement used in the wheel firmware to switch between the different displays on the wheel screen based on driver button presses.

if (buttons.getActiveTime(menu_up) > 0) {
	wheelScreen.setDisplay(WheelScreen::powerStatus);
} else if (buttons.getActiveTime(menu_down) > 0) {
	wheelScreen.setDisplay(WheelScreen::boardStatus);
} else {
	driverLeds.setMode(WheelIndicators::displaySpeed);
	wheelScreen.setDisplay(WheelScreen::primary);
}

These if/else statements call some functions as part of their conditions. You'll learn about functions in a little bit if you aren't familiar with them. But based on the function names and the if/else structure, you can kind of tell that depending on the wheel menu button that the driver presses, the screen will switch between displaying the power statuses, board statuses, and the primary display.


Once you've read the C++ switch statements tutorial, hopefully you understand that switch statements are basically fancy if/else statements. This code...

if (day == 1) {
         ...
} else if (day == 2) {
         ...
} else if (day == 3) {
         ...
} ... AND SO ON

is the same as one of the switch statements in the W3Schools tutorial.

We use a lot of switch statements in the code. One common switch statement in almost all firmware projects is this one:

switch(msg.id) {
         case BMS_PACK_VOLTAGE.ID:
                  ...
         case MCCR_RPM.ID:
                  ...
         // AND SO ON...
}

which we use to take different actions based on the CAN message IDs that the code reads in from the message buffer. We use CAN messages to send data/commands between boards on the car.

Looping (For + While)

W3Schools Tutorial - C++ For Loops

W3Schools Tutorial - C++ While Loops

This is a for loop written in the MCC firmware that voltage across its thermistors. Thermistors are special resistors that we can use to determine if the MCC is overheating - there are 4 of them on the MCC, so we'd prefer to check them all using a for loop rather than checking them individually.

for (uint8_t i = 0; i < 4; i++) {
         *tempsArr[i] = checkTemperature(i, therm_temps[i]); 
}

Don't worry about the for loop body for now, but you can tell from the for loop header that it loops from 0 to 3, representing the 4 thermistors on the MCC.


One while loop used on almost every MCU is this one written in main.cpp, which is used to check the CAN message buffer.

while(!common.readCANMessage(msg) && canCount < CAN_READ_LIMIT) {
         // DO STUFF HERE (Again, the double backslashes denote a comment in C++)
}

Again, this while loop calls some functions as a part of its condition. You'll learn more about functions in the next section. Notice that the while loop is made up of 2 conditions, joined by the && statement. Essentially, while the buffer that we use to read CAN messages is not empty (first condition) and while the number of messages we've read has not hit a predetermined limit (second condition), we will continue to process CAN messages in the buffer.

Functions

W3Schools Tutorial - C++ Functions

The next tutorial has a bunch of sub-tutorials regarding parameters/arguments in the left drop-down menu, all of which would be good to look at. Examples below will follow some of these sub-tutorials.

W3Schools Tutorial - C++ Function Parameters

The following example is a function declaration from the US2066.h file.

uint8_t init(uint8_t contrast = 127);

It's function body is not here, but actually written in a separate ".cpp" file (you'll learn more about .cpp and .h files in a little bit). The function has 1 parameter called contrast, of data type unsigned 8-bit integer (uint8_t), which has a default value of 127. In this function, this parameter is a value that gets sent (via some dedicated MCU pins) to the OLED screen driver chip to control the OLED screen brightness. Having a default value of 127 means that the screen should have a default brightness of 127 (out of 255, since 8-bit integers can range from 0 to 255).

Thus, calling init() without inputting an argument will automatically set "contrast" to 127, turning on the OLED screen with that brightness.

Notice that the return data type is specified as a uint8_t. Although not shown, this function returns an 8-bit error code if OLED screen initialization fails, which is why the return type is a uint8_t.


The next example is an MCC firmware function that takes multiple arguments (the function has multiple parameters). This is a function sends acceleration requests to the motor controller.

bool MotorController::requestAccel(uint8_t accel, uint32_t now) {
	_lastAccelUpdate = now;
	if (accel == 0) {
		_requestedAccel = accel;
		return _setAccel(0);
	}
	if (_state != BRIZO_CAN::mccMCState_neutral) {
		if (_setRegen(0)) {
			_requestedAccel = accel;
			return true;
		}
	}
	return false;
} 

The function takes an accel argument and a now argument. Don't worry about the function body for now, but basically it returns true if the acceleration request was successful and false if not.

If you tried to call the function without inputting both arguments, you'd get an error.

bool x = motorControllerObject.requestAccel(100); // This DOES NOT WORK
bool y = motorControllerObject.requestAccel(100, 100000); // This WORKS

Let's quickly go over functions that pass by reference. The W3Schools tutorial doesn't do a great job of explaining this, but you are unable to modify the value of arguments that you pass into a function unless they are references.

This hopefully illustrates the difference between regular arguments and arguments that are references.

void changeValue(int x) {
         x = 40;
}

void changeReference(int & x) {
         x = 40;
}

int a = 100;
changeValue(a);
cout << a; // This will print 100, not 40, because "a" is still 100 even though the function attempted to modify its value.

changeReference(a);
cout << a; // This will print 40 because "a" was passed in as a reference to changeReference(...) and modified to 40.

This example is a function that utilizes passing by reference in a useful way. It was written in EMS22A.cpp in the wheel firmware.

uint8_t EMS22A::read(uint16_t& data){
	data = 0;//_read();
	uint8_t error = _checkForErrors(data);
	data = (data >> 6) & 0x03FF ;
	return error;
}

This function reads the rotational position of the thumb encoder that the car driver uses to accelerate and indicates whether there were any errors in reading. By passing "data" as a reference to the function, the function can modify it to whatever value it reads from the encoder. The function then returns any error codes in a uint8_t.

Thus, by calling the function like so...

uint16_t rotPosition = 0;
uint8_t errors = ems22aObject.read(rotPosition);

...the firmware now knows both the rotational position of the encoder (saved in the rotPosition variable) and any error codes (saved in the "errors" variable). Without passing rotPosition as a reference to the function, it would be trickier to get both the rotational position and the errors all at once.

C++ Files (.h and .cpp) - The Basics

In C++, we often split code between .h (header) and .cpp (C++) files. You can very clearly see this in our firmware - in every firmware project, the "inc" folder contains the .h files while the "src" folder contains the corresponding .cpp files.

There's no W3Schools tutorial for .h and .cpp files, but basically, the .h files contain declarations of classes/objects/variables/functions while .cpp files contain any necessary code to implement those things. For example, in the pins.h file in most firmware projects, the file declares a few DigitalOut objects for several board LEDs like so:

extern DigitalOut led1;
extern DigitalOut led2;
extern DigitalOut led3;
extern DigitalOut led4;

while the code to actually instantiate those objects by passing in the necessary arguments is in the pins.cpp file. Note that the pins.cpp file #includes the pins.h file at the top. A .cpp file must #include its corresponding .h file in order to implement any items declared in that .h file.

DigitalOut led1(P_LED1);
DigitalOut led2(P_LED2);
DigitalOut led3(P_LED3);
DigitalOut led4(P_LED4);

As you go through our firmware more, you'll have a better idea of what should go in the .h files and what should go in the .cpp files.

Objects

W3Schools Tutorial - What Are Objects?

W3Schools Tutorial - C++ Classes/Objects

W3Schools Tutorial - C++ Class Methods

W3Schools Tutorial - C++ Constructors

W3Schools Tutorial - C++ Access Specifiers

W3Schools Tutorial - C++ Encapsulation

Objects are one of the most important, yet often misunderstood, concepts in C++. As their name is not very descriptive, what exactly are they? At their core, objects are just special variables that combine variables, functions, and other objects. They can store data (variables) and can also do stuff with that data via their built-in functions. It's a simple concept, but a very powerful one - object-oriented programming is the most popular form of programming for software development and is the one most commonly taught in CS curriculums.

As you should have learned from first few tutorials, "classes" are blueprints that define variables/functions/internal objects while "objects" are actual instances of those classes that you use in your code.

EX: if you're creating a school records app, you can define a "Student" class that defines variables for things like number of classes or GPA, as well as functions for things like dropping a class or switching a major. You'd instantiate a "Student" object for each student at the school.

Although a simple concept, there are many ways to structure objects to get the most out of them (hence why I linked so many tutorials above, and why people sometimes misunderstand what objects can actually do). You use objects to group variables and functions that you think should belong together - in our firmware, we often write classes to group variables/functions for specific chips and components that the MCU talks to. The MCC firmware has a class that contains all the functionality to communicate and store data from the motor controller. The wheel firmware has such classes for the thumb wheel, the 7-segment display, and the OLED screen. The BMS firmware has a class for its contactor drivers.


Let's take the wheel OLED screen class (.h file) as an example, a class used to write items like battery voltages, current draw, and acceleration level to the screen. The class is written in the "WheelScreen.h" and "WheelScreen.cpp" files - let's look at the .h file first. In the linked ".h" file on Github, you can see the public and private sections, which puts public methods (class functions) in the public section and internal variables/methods in the private section. Methods that the object uses to set actual statuses and levels on the screen are public. Internal methods/variables/objects that implement some of the details like the location of numbers/statuses on the screen and checking data recency are private - other C++ files that use the WheelScreen object don't need to access those things.

In the linked WheelScreen.cpp file, you can see the actual method bodies for the methods defined in WheelScreen.h, the code that actually gets run when the methods are called. Notice that you must #include the WheelScreen.h file at the top of this file, and that each method starts with "WheelScreen::" before the method name, indicating that these methods are being written for the WheelScreen class defined in the WheelScreen.h file.

In the wheel firmware, a WheelScreen object is instantiated in the peripherals.cpp file,like so:

WheelScreen wheelScreen(spi, OLEDCS, OLEDReset);

(You also have to #include WheelScreen.h and declare the WheelScreen object in the peripherals.h file, which contains all the declarations for the things that are instantiated in peripherals.cpp)

Once you have this WheelScreen object (called wheelScreen), you can call any of the public methods in any file, as long as they #include the peripherals.h file that contains the WheelScreen object declaration. For example, in main.cpp, which does #include peripherals.h, we can call methods "wheelScreen.init()" to or "wheelScreen.setAccelPercentage(...)" to work with the wheel screen.

C++ Files (.h and .cpp) - Classes

Very often, we use .h and .cpp files to separate code for classes (blueprints for objects). Class methods and variables are declared in the .h file to provide a high-level overview of the class, while the actual implementation of class methods are in the .cpp files. You've seen an example of this with the WheelScreen.h and WheelScreen.cpp files.

For classes, the .h file should serve as a high-level overview (interface) while the .cpp file has the actual code to implement methods in that class. No method bodies should be written in the .h files.

Enums ("Enumerated Values")

No W3Schools tutorial for enums, so here's a GeeksForGeeks tutorial.

GeeksforGeeks Tutorial - C++ Enums

Enums are another feature in C++ that we use in our firmware. These are just special data structures that "enumerate" a set of variables, or tie them to numbers. Look at the "Gender" example from the GeeksForGeeks tutorial. If you create a variable of type Gender like they do, you can only set its value to "Male" or "Female" because those are the only values listed in the enum declaration. In a way, enums let you create your own variable types with complete control over the possible values.

The use for enums might not be immediately obvious, but they can be extremely useful in certain situations. In our firmware, we have a lot of enums declared in can_data.h. We use these enums to help decode data that we get from CAN messages.

Take the DashDriveDirection enum in can_data.h for example.

enum DashDriveDirection : uint8_t {
	dashDriveDirection_forward = 0, // The "= 0" is not actually necessary. C++ assigns the first value in the enum to 0 by default, the second value to 1, etc.
	dashDriveDirection_neutral = 1,
	dashDriveDirection_reverse = 2
};

Our car has a set list of possible drive directions, namely forward, neutral, and reverse. DashDriveDirection encodes these values in an enum - whenever we receive a DashDriveDirection variable from a dash CAN message, we know that it must be one of these 3 values. This prevents us from messing up and accidentally sending some drive direction value that isn't in this specified list of values.

DashDriveDirection d = dashDriveDirection_forward; // This WORKS
DashDriveDirection d = 0; // This WORKS, it sets the variable to dashDriveDirection_forward
DashDriveDirection d = 4; // This DOES NOT WORK because 4 is neither a value or number allowed by the enum declaration

The fact that enums tie values to numbers makes enums a useful tool for interpreting arrays and bitmaps, other data structures that we commonly send in CAN messages. We won't go over that in too much detail here, but you can examine this for yourself later.

Structs

No W3Schools tutorial for structs, so here's a GeeksForGeeks tutorial.

GeeksforGeeks Tutorial - C++ Structs

Structs are simply data structures that combine multiple variables into a single entity. Like enums, structs are another "user-defined type" - you write your own structs and can create variables from them later. Also like enums, we have a lot of structs declared in can_data.h which we use to help decode data that we get from CAN messages.

Take the DashBrakeAndDirection enum in can_data.h for example.

struct DashBrakeAndDirection {
	DashDriveDirection drive_dir;
	uint8_t mechBrakePressed;
};

This struct contains a DashDriveDirection variable described in the enum section, and a uint8_t indicating whether the brake pedal is pressed or not. By combining these values into one entity, we can send this entire struct in one CAN message rather than sending separate CAN messages for the drive direction and brake pedal. We need to send this information multiple times per second, so it saves us a decent amount of bandwidth in the long run.

We can instantiate this struct in multiple ways:

DashDriveDirection driveDirection = dashDriveDirection_forward;
uint8_t brakePressed = 0;

DashBrakeAndDirection x = {driveDirection, brakePressed}; // We can fill all the values at instantiation like this

// Or we can access individual elements in the struct by name and fill them like this:
DashBrakeAndDirection y;
y.drive_dir = driveDirection;
y.mechBrakePressed = brakePressed; 

Arrays

W3Schools Tutorial - C++ Arrays

An array combines multiple variables/values/objects into a list-like structure. Unlike structs, you don't access items in the array by name, but rather by an index (positional number).

An array can only contain 1 variable/object type. For example, you can have an array of integers or an array of DigitalOut objects, but not an array that has both integers and DigitalOut objects. This is because of how C++ allocates memory for arrays.

In the BMS firmware fans class, there is an array to keep track of the last time that each of the fans' RPMs were read. There are multiple fans on the battery box, so an array is an easy way to keep track of all of those values. We declare the array in the fans.h file like so:

uint32_t _lastTime[NUM_FANS] = {0};

The array is initialized with a fixed number of elements NUM_FANS, a macro that tells the code how many fans there are. It is initialized to {0}, meaning that each element in the array has a starting value of 0.

We can loop through all the items in this array like so:

for (int i = 0; i < NUM_FANS; i++) {
         uint32_t time = _lastTime[i];
         // Do something with this value
}