Arduino C++

date
May 22, 2024
type
KnowledgeBase
year
slug
arduino-cpp
status
Published
tags
Arduino
C++
Microcontroller
summary
How to get started with C++ for Arduino
🎓
Sections marked with 🎓 are advanced knowledge, ignore these for now and unfold them once you’re ready to go deeper!
Arduino C++ Basics (PDF) ← Here’s an even simpler single page introduction for you to 🖨️ print
Arduino C++ Basics (PDF) ← Here’s an even simpler single page introduction for you to 🖨️ print

Prelude - Getting things set up

  • Open Arduino IDE
  • File > Examples > 01.Basics > Blink
    • Arduino IDE with Blink example loadedArduino IDE with Blink example loaded
      Arduino IDE with Blink example loaded
  • Connect your Microcontroller
  • Click the Board Dropdown and select the type of your Board and the COM Port it showed up under
  • Click ➡️ (Upload) - this will compile the Blink example, upload it to your board and run it!
    • Compilation in progressCompilation in progress
      Compilation in progress
      This is what the output will look like when it successfully completed!This is what the output will look like when it successfully completed!
      This is what the output will look like when it successfully completed!
  • Watch the internal LED on your microcontroller blink! Done!

Troubleshooting

  • Program uploaded correctly, but the microcontroller doesn’t blink: It’s possible that LED_BUILTIN doesn’t point at the correct Pin. Do a web search for your microcontroller model, find out which pin the onboard LED is connected to and replace all occurances of LED_BUILTIN with that pin number in the code.

Programming - The basics

Comments

// This is a comment - it's ignored when the program runs

Basic Structure

The very basic structure of a C++ program in Arduino:
// the setup function runs at the very start of the program void setup() { // in here you can initialize all the things that need initializing } // the loop function runs again and again and again for as long as your program runs void loop() { }

🎓Lifecycle

These are the steps involved in creating and running a program:
These are the steps involved in creating and running a program:
  1. We write are instructions for the Microcontroller to follow.
    1. We do this using a programming language called C++
    2. We use an Integraded Development Environment (IDE) - essentially a fancy text-editor with extra functionality to make coding easy.
  1. Our 📄 source code is then ⚙️ compiled (translated) into 🤖 machine code (a low-level representation of instructions that the microcontroller can execute directly)
  1. We ⬆️ upload the compiled program to the microcontroller’s flash memory
  1. The microcontroller’s program counter points to the first instruction and starts executing the code sequentially. Variables and other data are allocated to RAM (Random Access Memory)

What we need to know to be able to program

These are the very basic things we have to understand to be able to program:
These are the very basic things we have to understand to be able to program:
notion imagenotion image
  • 📜 Instructions = individual commands
  • 🆎 Data Types = the types of data we can use
  • 📦 Variables = containers that hold bits of information that we can change and work with
  • 🔣 Operators = how to assign values to variables, etc.
  • 🔂 Control structures = how to compare stuff and branch out
  • ↗️ Functions = sections of code that can be called from elsewhere in the program
These things all play together, so it might be confusing at first when you learn about one while you don’t yet know about another. But bear with me, it will all make sense after an hour or two!
These things all play together, so it might be confusing at first when you learn about one while you don’t yet know about another. But bear with me, it will all make sense after an hour or two!

1. Instructions

📜
An instruction is a single command that ends with a semi-colon ;
  • When programming, we’re writing lists of instructions for the computer to execute.
  • Instructions can be function calls, variable asignments, control instructions, etc.
Here’s an example with 3 instructions inside the setup function:
void setup() { int a = 0; // make a variable of type integer, call it "a" and assign 0 to it a = a + 5; // add 5 to a a = a - 2; // subtract 2 from a }
Note how every instruction in C++ has to end with a semi-colon ;!
Note how every instruction in C++ has to end with a semi-colon ;!

2. Data Types

🆎
Data Types define the kind of data that a 📦variable can hold.
bool - true or false int - used to store an integer number (like 438 or -12) float - used to store floating point numbers (like 37.83752 or -1.2) char - used to store a single character (like 'C' or 'f') String - used to store text (like "Hello" or "Who came up with this nonsense?")

🎓Complete List of Data Types

  • bool - true or false, so 1 bit of information, but still uses 8 bits (1 byte) for efficient memory access reasons…
  • int - used to store an integer number. 16 bits (2 bytes), Range -32,768 to 32,767.
    • uint unsigned int - same, but only positive numbers. 16 bits (2 bytes), Range 0 to 65,535.
  • long - used for large integers. 32 bits (4 bytes), Range -2,147,483,648 to 2,147,483,647.
    • ulong unsigned long - same, but only positive numebrs. 32 bits (4 bytes), Range 0 to 4,294,967,295.
  • float - used to store floating point numbers. 32 bits (4 bytes), Precision 6-7 decimals, Range -3.4028235E+38 to 3.4028235E+38.
    • ⚠️
      If doing math with floats, you need to add a decimal point, otherwise it will be treated as an int!
  • double - higher range and precision, 64 bits (8 bytes), BUT on many Arduino boards (including boards with the ATmega32u4 chip) double is implemented as float, so absolutely no difference.
  • char - used to store a single character or small integers. 8 bits (1 byte), Range -128 to 127
    • unsigned char - same, except only positive values. 8 bits (1 byte), Range 0 to 255
  • byte - same as unsigned char
  • String - used for text, size depends on content.

strings are weird in C++

char myChar = 'A'; - a single character char* myString = "Hello"; - A pointer to a string of characters String str = "Hello"; - A string.
 
How to convert a string to a char*:
String str = "Hello"; char* chArr = str.c_str();
 
🤔 Now what do we do with these data types? We make variables!

3. Variables

📦
A variable represents a value that can change.
  • Think of it as a box where you can put different things (numbers, words, or other data)
  • Pick a data type - what kind of thing do you want to store?
  • Give your variable a name
For example: You can make a variable called score that you can put the player’s current score into (like 25, 52 or 100)
int score = 100; float num = 32.19076; String name = "Bob";
Basic signature of a variable: data-type variable-name = initial value;

🎓 Scope and Lifetime:

  • Global variables exist throughout the program’s execution and can be used from anywhere.
    • int loopCount; void setup() { loopCount = 0; } void loop() { loopCount++; }
  • Local variables exist only within a specific function or block
    • void blah() { int a = 5; } void loop() { blah(); // at this point in the code we know nothing about the variable a }
  • Static variables retain their value between function calls. So even though this variable is declared inside a function, it’s lifetime extends beyond the function call. But it’s scope is limited to that function (meaning it can’t be accessed from outside)
    • void loop() { static int count = 0; count++; }
 
🤔 So what do we do with variables? We assign and re-assign values and we compare them with operators!

4. Operators

Assign a value
  • = (simple assignment). Example: score = 12;
Change values
  • + (addition). Example: score = score + 2;
    • += (addition assignment). Example: score += 2;
    • ++ (increment by 1). Example: score++;
  • - (subtraction). Example: score = score - 10;
    • -= (subtraction assignment). Example: score -= 10;
    • -- (decrement). Example: score--;
  • * (multiplication). Example: score = score * 2;
  • / (division). Example: score = score / 4;
Compare values:
  • == (equal to). Example: if(name == "Bob") { Serial.println("Hello"); }
  • != (not equal to). Example: if(name != "Bob") { Serial.println("Hi"); }
  • < (less than). Example: if(health < 1) { Serial.println("Dead"); }
  • > (greater than). Example: if(score > 50) { Serial.println("Yay"); }
  • <= (less than or equal to). Example: if(health <= 0) { Serial.println("Dead"); }
  • >= (greater than or equal to). Example: if(score >= 100) { Serial.println("You Win!"); }

Complete List of Operators

Arithmetic Operators

  • + (addition)
  • - (subtraction)
  • * (multiplication)
  • / (division)
  • % (remainder)

Assignment Operators

  • = (simple assignment)
  • += (addition assignment)
  • -= (subtraction assignment)
  • *= (multiplication assignment)
  • /= (division assignment)
  • %= (remainder assignment)

Comparison Operators

These compare values and return a Boolean result (true or false).
  • == (equal to)
  • != (not equal to)
  • < (less than)
  • > (greater than)
  • <= (less than or equal to)
  • >= (greater than or equal to)

Logical Operators

These combine Boolean expressions.
  • && (logical AND)
  • || (logical OR)
  • ! (logical NOT)

Bitwise Operators

These operate on individual bits of integers.
  • & (bitwise AND)
  • | (bitwise OR)
  • ^ (bitwise XOR)
  • ~ (bitwise NOT)
  • << (left shift)
  • >> (right shift)

Increment and Decrement Operators:

These modify the value of a variable.
  • ++ (increment)
  • -- (decrement)

Ternary Operator

This is a shorthand for an if-else statement.
  • condition ? value_if_true : value_if_false

5. Control Structures

Manage the flow of execution in your program!
Manage the flow of execution in your program!

If

If this then that.
if(a > b) { // if a > b do this }
if(a > b) { // if a > b do this } else { // otherwise do this }
if(a > b) { // if a > b do this } else if(a < b) { // otherwise, if a < b do this } else { // in all other cases, do this }

🎓 Switch

Choose between multiple cases. (This example uses Serial Input, explained further down)
void loop() { char userChoice; Serial.begin(9600); Serial.println("Enter something:"); while (!Serial.available()) {} // Wait for user input userChoice = Serial.read(); switch (userChoice) { case '1': Serial.println("One"); break; case '2': Serial.println("Two"); break; case '3': case '4': case '5': Serial.println("Yep."); break; case 'blah': Serial.println("Blah indeed!"); break; default: Serial.println("Whatever..."); } }
You need to break; after each case or it will continue (but this can also be used to our advantage - in the example above this is used to make 3, 4 and 5 all give the same result)

For

Repeat a block of code a fixed number of times.
In the example below we make a LED blink 5 times at the start of the program
const int ledPin = 13; void setup() { pinMode(ledPin, OUTPUT); for (int i = 0; i < 5; i++) { digitalWrite(ledPin, HIGH); // turn LED on delay(500); digitalWrite(ledPin, LOW); // turn LED off delay(500); } } void loop() { }

While

Repeat a block of code while a condition remains true
Here’s an exciting example where we do nothing while we wait for a button to be pressed:
const int buttonPin = 2; void setup() { pinMode(buttonPin, INPUT_PULLUP); Serial.begin(9600); } void loop() { // Wait for button press while (digitalRead(buttonPin) == HIGH) { // Do nothing until the button is pressed } // Button pressed! Serial.println("Button pressed."); delay(1000); // Debounce }

🎓 Do-While

Do-while allows you to do it the other way around and do nothing first and then check. 😉 Ok, TODO: find better examples
do { // Wait for button press } while (digitalRead(buttonPin) == HIGH);

6. Functions / Methods

A function is a section of code that can be called from elsewhere in your code. 
Ideally your function has a descriptive name and does one specific thing.
A function is a section of code that can be called from elsewhere in your code. Ideally your function has a descriptive name and does one specific thing.
Functions and Methods are the same thing. A Function that’s part of a Class or Object is called a Method
Functions and Methods are the same thing. A Function that’s part of a Class or Object is called a Method

Calling functions

The Arduino library comes with many built-in functions to call:
delay(1000) - pauses the execution of the program for 1000ms (1 sec)
millis() - returns the time elapsed since the program started (in ms)
random(0, 10) - generates a random number between 0 and 10
abs(a), min(a, b), max(a, b) - basic math stuff (absolute value, minimum value, maximum value)
🎓 You can find a complete list here: https://www.arduino.cc/reference/en/#functions

Making your own functions

The signature of a function looks like this: return type function name ( arguments ) { content }
Here’s an example:
int add(int a, int b) { return a + b; }
int is the return type - if you call this function you’ll get an integer number back from it.
Use void as the return type if your function returns nothing.
add is the name of the function - this is how we call it from elsewhere
int a and int b are the two arguments we have to send along when we call this function.
return is a keyword that sends back a value to wherever the function was called from. This also ends the execution of the function. nothing below this in the function would execute.
Here is the same function in action:
// Define a function called "add" that takes two integer numbers as arguments int add(int a, int b) { return a + b; // return the result of a + b } void setup() { int c = add(5, 8); // call the function and save the result in a new variable Serial.println(c); // output the result over Serial int d = add(12876, 3908234); // call the same function again with different arguments Serial.println(d); }

🎓 Passing Values

  • Passing by Value
    • If you pass by value a copy of the variable is created within the function.
      void function(int x) { x = 10; // This will not affect the original variable } int main() { int a = 5; function(a); // a is still 5 here }
  • Passing by Reference &
    • You are passing the original variable, not a copy. Every change inside the function will affect the original variable
      void function(int &x) { x = 10; // This will affect the original variable } int main() { int a = 5; function(a); // a is now 10 here }
      using const makes sure that the original value can’t be changed inside the function:
      void drawPlatform(const Platform& platform) { // the value of platform can not be changed in here }
  • Passing via Pointer *
    • A pointer is a variable that stores the memory address of another variable. Pointers can be used to indirectly access and modify the variable they point to. This can be useful for passing large data structures (like arrays) without having to copy them, or for dynamic memory allocation.
      void function(int *x) { *x = 10; // This will affect the original variable } int main() { int a = 5; int *ptr = &a; function(ptr); // a is now 10 here }
      Why would I ever use a pointer instead of a reference?
      ⚠️
      Use pointers when necessary, but prefer references when you can.
      Pointers can be reassigned, so we could have the same pointer point at different things at different times. Pointers are nullable and can thus point at nothing.
      They’re somehow necessary when working with Dynamic Memory Allocation (TODO: investigate)
      int *arr = new int[10]; // dynamically allocated array
      Pointers support arithmetic operations (increment, decrement, difference), which can be useful when dealing with arrays and other sequential memory structures. References do not support arithmetic operations.
      int arr[10]; int *ptr = arr; ptr++; // move to the next element

🎓 Static Variables inside Functions

In this example, callCount retains its value across loop iterations:
int myFunction() { static int callCount = 0; // Static variable callCount++; // Increment on each call return callCount; } void setup() { Serial.begin(9600); } void loop() { int result = myFunction(); Serial.print("Function called "); Serial.print(result); Serial.println(" times."); delay(1000); }
Why would we do this?
  • static variable retains its value across multiple invocations of the same function.
    • When the function exits, the variable’s value persists until the next time the function is called.
  • By declaring a variable as static inside a function, you limit its scope to that function.
  • It doesn’t interfere with other functions or global variables.
  • Encapsulation helps organize your code and prevents accidental name clashes.
Considerations:
  • Thread Safety: Be cautious when using static variables in multithreaded environments. They are not thread-safe by default.
  • Initialization Order: Static variables are initialized in the order they appear in the code. Ensure proper initialization sequence.
  • Lifetime: Static variables exist throughout the program’s lifetime, which may not be suitable for all scenarios.

Serial Output/Input

Serial Output

How do we know if our microcontroller actually does what we want? The simples way is through a serial connection!
int count = 0; void setup() { Serial.begin(9600); // start a serial connection at the given baud rate } void loop() { count++; // increment count Serial.println(count); // print a line to serial containing the current count delay(1000); // pause for 1 second }
Now if we run this program we can open the Serial Monitor and watch what our microcontroller says!
notion imagenotion image

Serial Input

We can also send instructions to our microcontroller via the serial connection!
In the following example we can turn an LED on and off via the Serial connection:
const int ledPin = 13; // LED connected to digital pin 13 void setup() { pinMode(ledPin, OUTPUT); Serial.begin(9600); } void loop() { char userChoice; Serial.println("Enter '1' to turn on the LED, '0' to turn it off:"); while (!Serial.available()) {} // Wait for user input to complete userChoice = Serial.read(); // Read the user input into a variable switch (userChoice) { case '1': digitalWrite(ledPin, HIGH); break; case '0': digitalWrite(ledPin, LOW); break; default: Serial.println("Invalid choice. Enter '1' or '0'."); } }

Libraries

There is a lot of ready-made code out there that can make our lives easier! Here’s how to include it in a project.
In the Arduino IDE there’s a special sidebar tab for libraries where you can find and install the ones you want to use.
For example: The Arduboy2 library is one that provides many things that help us make an Arduboy game.
Once a library is ⬇️ installed, we can include it in our program like this:
#include <Arduboy2.h>
notion imagenotion image
Now our program can call all the things in the Arduboy2 library!

🎵 Intermission 🎵

And that’s all we need to get started - the rest is easy!

Working with Pins

pinMode(pin, mode);
  • Parameters:
    • pin: The Arduino pin number to set the mode of.
    • mode: Can be one of the following:
      • INPUT: Configures the pin as an input (read data from external devices).
      • OUTPUT: Configures the pin as an output (control external devices).
      • INPUT_PULLUP: Enables the internal pull-up resistor for the pin (useful for buttons and switches). ⚠️ Be aware however that turning on a pull-up will affect the values reported by analogRead().
Reading from a Digital Pin (Digital Read)
pinMode(7, INPUT); int buttonState = digitalRead(7); // Read input from digital pin 7 (1 = HIGH, 0 = LOW)
  • For input (reading):
    • HIGH (1): Indicates a voltage above a certain threshold (e.g., 2.5V).
    • LOW (0): Indicates a voltage below the threshold.
Writing to a Digitial Pin (Digital Write)
pinMode(13, OUTPUT); digitalWrite(13, HIGH); // Set pin 13 to HIGH
  • For output (writing):
    • HIGH (1): Represents a logic high voltage (usually 3.3V or 5V).
    • LOW (0): Represents a logic low voltage (usually 0V or ground).
Analog Pins
int sensorValue = analogRead(A0); // Read analog value from pin A0 (10-bit resolution = 0-1023) // NOTE: On ESP32 it's 12-bit, so 0-4095
PWM (Pulse Width Modulation):
  • Certain pins support hardware PWM (e.g., GPIOs 2, 4, 5, etc.).
  • Use analogWrite(pin, value) to generate PWM signals (vary the intensity of an output).
pinMode(9, OUTPUT); analogWrite(9, 128); // Generate PWM signal on digital pin 9 (50% duty cycle)
⚠️ On some microcontrollers PWM is only available on selected pins.

Object Oriented Programming

OOP is a way of writing code that organizes it around objects. Instead of writing one long script full of functions and logic we’re grouping data and functions together and create smaller, reusable parts.
OOP is a way of writing code that organizes it around objects. Instead of writing one long script full of functions and logic we’re grouping data and functions together and create smaller, reusable parts.

Key Concepts / Building Blocks

🔡 Classes: abstract blueprints that define the properties and behaviours of objects (for example we could make an Animal class with properties for name and species)
📦 Objects: Specific instances created from class templates. Each object can have unique values for its properties (for example we could create an instance of Animal with species ”Cat” and name ”Bonko” as well as a second instance with species “Giraffe” and name “Sue”)
Here’s example code for this:
#include <Arduino.h> // Animal class definition class Animal { public: // Properties (attributes) String species; String name; // Constructor (initialize properties) Animal(const String& animalSpecies, const String& animalName) { species = animalSpecies; name = animalName; } // Method to introduce the animal void introduce() { Serial.print("Hi, I'm "); Serial.print(name); Serial.print(", the "); Serial.print(species); Serial.println("!"); } }; // Create animal objects Animal bonko("Cat", "Bonko"); Animal sue("Giraffe", "Sue"); void setup() { Serial.begin(9600); delay(1000); // Call the introduce method on the animals bonko.introduce(); sue.introduce(); } void loop() {}

Classes

Members are private by default.
class MyClass { int x; // private by default public: int y; // public };
Usually you have a header file that contains declarations for data structures, classes, functions and other program elements and serves as an interface between different parts of your program.
Separating declarations from implementations promotes modularity, reusability and maintainability, they say. You can just as well throw everything directly into the header file.
// Guy.h (Header File) #include <Arduino.h> class Guy { private: // Current Velocity float x; float y; public: Guy(int16_t x, int16_t y); bool reset(int16_t x, int16_t y); void processInput(uint8_t up, uint8_t right, uint8_t down, uint8_t left, uint8_t a, uint8_t b); };
// Guy.cpp (Implementation) #include "Guy.h" // !include the header file! #include <Arduino.h> Guy::Guy(int16_t xPos, int16_t yPos) { x = xPos; y = yPos; } bool Guy::reset(int16_t xPos, int16_t yPos) { x = xPos; y = yPos; return true; } void Guy::processInput(uint8_t up, uint8_t right, uint8_t down, uint8_t left, uint8_t a, uint8_t b) { // ... }
⚠️
Header / Include Guards - if you need to include the same header file in multiple other files, use include guards or compilation will fail with an error saying you’re trying to redefine the same thing.
// INCLUDE/HEADER GUARD - to prevent redefinition of the same reusable class! #ifndef PLATFORM_H // only if not yet defined #define PLATFORM_H // define it struct Platform { int_fast16_t x; int_fast16_t y; int_fast16_t w; int_fast16_t h; }; #endif // PLATFORM_H

There’s different ways to access stuff inside a class.

The Dot Operator . is used to access class members through an object instance.
The Arrow Operator -> is used to access class members through a pointer to an object
The Scope Resolution Operator :: is used to access static members of a class or members from a base class in a derived class.

Structs

They are nearly identical to classes in C++, except here members (variables) are public by default.
You can have methods, constructors, destructors and even inheritance, just like with classes.
struct MyStruct { int x; // public by default private: int y; // private };
// Define a new struct // If you want to ever use this as an argument in a function, then it has to be defined // BEFORE the FIRST function in the file, or there will be an error (Arduino bug) struct Platform { uint_fast8_t x; uint_fast8_t y; uint_fast8_t w; uint_fast8_t h; }; // Now I make an array with this struct Platform platforms[8]; // C++ is not good at resizing arrays, so I just make an array and keep track of its size myself uint_fast8_t platformLength = 0; // A function to add to the array void addPlatform(uint_fast8_t px, uint_fast8_t py, uint_fast8_t pw, uint_fast8_t ph) { platforms[platformLength] = {px, py, pw, ph}; platformLength++; } // And a function that uses the array void drawPlatform(const Platform& platform) { tft.fillRect(platform.x, platform.y, platform.w, platform.h, ST77XX_WHITE); } void setup() { addPlatform(20, 20, 100, 20); drawPlatform(platforms[0]); }

Types of Memory

Flash memory is where your program is stored when you upload it. The data in flash memory survives turning the power off. It’s not ideal for storing data that changes frequently as it has a limited number of write cycles (usually 10,000-100,000 writes). The ATmega32u4 has 32KB Flash memory.
SRAM (Random Access Memory) is where your program’s variables are stored while you run the program. It’s volatile and all content is lost when you turn off the power. It can be written to and read from an almost unlimited number of times. The ATmega32u4 has 2.5KB RAM
PSRAM (Pseudo-static RAM) - additional RAM, only available on some modules.
ROM (Read-Only Memory) contains fixed boot code and system level functions. Not directly accessible for user applications.
EEPROM (Electrically Erasable Programmable Read-Only Memory) - data that survives reboots
⁉️
You can use the PROGMEM is a special keyword used to store data in the microcontroller’s program memory (flash memory) instead of RAM.

💽 EEPROM

(Electrically Erasable Programmable Read-Only Memory)
Separate memory where we can store data that survives reboots. The ATmega32u4 has 1KB of internal EEPROM memory available.
Most Arduinos have EEPROM, the ESP32s don’t have any, but the same functionality is emulated using Flash memory (the EEPROM library handles this for us)
An EEPROM write takes 🕐 3.3 ms to complete. The EEPROM memory has a specified life of 💀 100,000 write/erase cycles, so you may need to be careful about how often you write to it.
An EEPROM write takes 🕐 3.3 ms to complete. The EEPROM memory has a specified life of 💀 100,000 write/erase cycles, so you may need to be careful about how often you write to it.
An example where it saves values from an analogRead into EEPROM:
#include <EEPROM.h> int address = 0; // the current address in the EEPROM (i.e. which byte we're going to write to next) void setup() {} void loop() { int val = analogRead(0) / 4; // read input, divide by 4 so result fits into 1 byte if(EEPROM.read(address) != val) EEPROM.write(address, val); //save val to current eeprom address // advance to the next address address = address + 1; if (address == EEPROM.length()) { address = 0; } delay(100); }

Leave a comment