Post #1.2: Implementing OOP Principles in Embedded Systems
Welcome to Post #1.2 of Mastering Object-Oriented Programming (OOP), C++, and Embedded Systems Series!
This series will take you through the journey of mastering OOP and C++ concepts and applying them in embedded systems development. Each topic is split into two posts: the first part focuses on core C++ concepts, while the second part shows how to apply those concepts to embedded systems using the ESP32-S3 microcontroller.
Note: This series is based on my personal learning path. If you’re completely new to OOP, you may find some parts challenging, but if you have a basic understanding of OOP, you’ll find useful applications of these principles in embedded systems.
Check out the full series here.
In this post, we’ll cover setting up a project in PlatformIO with Visual Studio Code, creating basic classes, and applying OOP concepts to control an RGB LED on an ESP32 board. You can find Post #1.1 here.
Creating a New Project in PlatformIO with Visual Studio Code
- Open PlatformIO in Visual Studio Code:
- Launch Visual Studio Code.
- Click on the PlatformIO icon in the left sidebar.
- Select “Create New Project” under PROJECT TASKS or click the home icon at the bottom left to open “PIO Home.”
- Start a New Project:
- Click the “New Project” button on the PlatformIO Home screen.
- Configure the New Project:
- Name: Enter a name for your project (e.g.,
day1_oop_esp
). - Board: Choose “Espressif ESP32-S3-DevkitC-1-N8”.
- Framework: Select “Arduino”.
- Location: Choose a folder to save the project files or leave it as the default.
- Name: Enter a name for your project (e.g.,
- Create the Project:
- Click “Finish”. PlatformIO will create a project with the required folder structure and configuration files. This may take some time, especially if it’s your first time using PlatformIO.
- Open the Main Project File:
- PlatformIO will open the project folder in the VS Code workspace.
- Navigate to the
src
folder and openmain.cpp
to start writing your code. - The
platformio.ini
file is also essential for including libraries or configuring hardware settings.
- Upload Your Code or Use the Serial Monitor:
- To upload code, click the right arrow button at the bottom left or go to PROJECT TASKS and select Upload under General.
- To open the serial monitor, click the plug icon or select Monitor under General.
Bonus: Install Required Drivers
ESP32 boards usually require CP210x (Silicon Labs) or CH340 (WCH) USB-to-serial drivers:
- Windows: Check “Device Manager” under Ports (COM & LPT) for “Silicon Labs CP210x USB to UART Bridge” or “USB-SERIAL CH340.”
- Linux: Use
dmesg | grep tty
to check for connected devices.
These steps help you set up a new project environment with all the necessary files and dependencies to start developing and experimenting with embedded systems!
RGB LED Control with OOP Concepts
To get started with controlling an RGB LED using Object-Oriented Programming (OOP) principles, we need to set up our environment correctly. Here’s the configuration for the platformio.ini
file:
1
2
3
4
5
6
7
8
9
10
[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
lib_deps =
adafruit/Adafruit NeoPixel
build_flags = -frtti
The -frtti
flag is essential for enabling dynamic casting in C++. We are using the Adafruit NeoPixel library to control the RGB LED. Although the RGB LED on the ESP32 board can function as a regular LED when using LED_BUILTIN
, it can be controlled in more sophisticated ways through the library.
Let’s dive into a basic example where we control the LED using OOP principles.
Step 1: Basic LED Class with Constructors and Destructors
We’ll start by defining a base class to manage basic LED operations:
The ESP32 board has an RGB LED that can be controlled similarly to a regular LED using digitalWrite()
. By encapsulating this functionality within a class, we gain the flexibility to extend its capabilities for more advanced features.
mLED.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <Arduino.h>
// Base class for a basic LED
class mLED {
private:
int pin; // GPIO pin number for the LED
public:
// Constructor: Initializes the LED class
mLED() {
Serial.println("mLED class: mLED() constructor");
}
// Destructor: Cleanup if necessary (not required in this simple example)
~mLED() {
Serial.println("mLED class: mLED() destructor");
}
// Initialize the LED
void begin(int ledPin) {
pin = ledPin;
pinMode(pin, OUTPUT);
Serial.println("mLED class: begin() method");
}
// Method to turn the LED on
virtual void turnOn() {
digitalWrite(pin, HIGH);
Serial.println("mLED class: turnOn() method");
}
// Method to turn the LED off
virtual void turnOff() {
digitalWrite(pin, LOW);
Serial.println("mLED class: turnOff() method");
}
};
- Encapsulation: The
mLED
class encapsulates the LED’s pin number and provides methods to control it (begin
,turnOn
,turnOff
). This hides the implementation details and simplifies the interface. - Constructors and Destructors: The constructor initializes the class, while the destructor is available for any necessary cleanup (though it’s not necessary in this case).
Step 2: Inheritance - Creating an RGB LED Class
Next, we’ll create an RGBLED
class that inherits from mLED
and adds functionality for controlling an RGB LED.
RGBLED.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <Adafruit_NeoPixel.h>
#include "mLED.h"
#define PIN 48
#define NUMPIXELS 1
#define DELAYVAL 10
class RGBLED : public mLED {
private:
Adafruit_NeoPixel pixels;
public:
// Constructor: Initialize base class and Adafruit NeoPixel object
RGBLED() : mLED(), pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800) {
Serial.println("RGBLED class: RGBLED() constructor");
pixels.begin();
}
// Method to fade between colors
void fadeColor(int steps = 100) {
Serial.println("RGBLED class: fadeColor() method");
int colors[3][3] = {
{255, 0, 0}, // Red
{0, 255, 0}, // Green
{0, 0, 255} // Blue
};
for (int colorIndex = 0; colorIndex < 3; colorIndex++) {
int nextColorIndex = (colorIndex + 1) % 3;
int startR = colors[colorIndex][0];
int startG = colors[colorIndex][1];
int startB = colors[colorIndex][2];
int endR = colors[nextColorIndex][0];
int endG = colors[nextColorIndex][1];
int endB = colors[nextColorIndex][2];
for (int i = 0; i <= steps; i++) {
int r = startR + (endR - startR) * i / steps;
int g = startG + (endG - startG) * i / steps;
int b = startB + (endB - startB) * i / steps;
pixels.setPixelColor(0, pixels.Color(r, g, b));
pixels.show();
delay(DELAYVAL);
}
}
}
// Override to turn the LED on with a green light
void turnOn() override {
Serial.println("RGBLED class: turnOn() method");
pixels.setPixelColor(0, pixels.Color(0, 255, 0)); // Green color
pixels.show();
}
// Override to turn the LED off with a red light
void turnOff() override {
Serial.println("RGBLED class: turnOff() method");
pixels.setPixelColor(0, pixels.Color(255, 0, 0)); // Red color
pixels.show();
}
};
- Inheritance allows the
RGBLED
class to reuse methods from themLED
class and add new features, such as color fading. - Polymorphism is achieved through method overriding: the
turnOn
andturnOff
methods provide customized behavior for the RGB LED.
Step 3: Polymorphism Example
Polymorphism is a key concept in OOP that allows us to use a base class pointer to refer to objects of derived classes. This enables us to call methods on these objects through the base class pointer, while the actual method that gets executed is determined at runtime based on the type of the object pointed to.
In this example, we demonstrate polymorphism with the mLED
and RGBLED
classes.
main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include "RGBLED.h"
mLED* myLed;
RGBLED* rgbPtr;
void setup() {
Serial.begin(115200);
// Allocate memory for the base class object
myLed = new mLED();
myLed->begin(LED_BUILTIN);
myLed->turnOn();
delay(2000);
myLed->turnOff();
delay(2000);
// Deallocate memory for the base class object before reassigning
delete myLed;
// Allocate memory for the derived class object
myLed = new RGBLED(); // Upcast to the base class pointer
// Demonstrate polymorphism with upcasting
myLed->turnOn(); // Calls RGBLED's overridden turnOn() method
delay(2000);
myLed->turnOff(); // Calls RGBLED's overridden turnOff() method
delay(2000);
// Downcasting to the derived class pointer
rgbPtr = dynamic_cast<RGBLED*>(myLed); // Use dynamic_cast for safe downcasting
if (!rgbPtr) {
Serial.println("Downcasting failed, program halt");
while (true); // Halt execution if downcasting fails
}
}
void loop() {
rgbPtr->fadeColor(); // Call the derived class-specific method
}
- Polymorphism:
- Concept: Polymorphism allows us to use a base class pointer (
mLED*
) to reference objects of derived classes (RGBLED
). This enables us to call methods defined in the base class (turnOn
,turnOff
), but the actual implementation that gets executed is determined by the object’s actual type (RGBLED
in this case). - Upcasting: When we assign a
RGBLED
object to amLED
pointer (myLed = new RGBLED();
), it’s known as upcasting. Upcasting is safe because aRGBLED
is-amLED
, and it ensures that we can call methods defined in the base class, even though the actual object is of the derived class. - Dynamic Method Binding: When
myLed->turnOn()
is called, it executesRGBLED
’sturnOn
method becauseRGBLED
overrides the base class method. This is due to dynamic binding, which occurs at runtime based on the actual object type.
- Concept: Polymorphism allows us to use a base class pointer (
- Dynamic Casting:
- Concept: Dynamic casting is used for safe downcasting, which means converting a base class pointer back to a derived class pointer. It’s useful when you need to call methods that are specific to the derived class.
- Usage:
rgbPtr = dynamic_cast<RGBLED*>(myLed);
attempts to castmyLed
(which is amLED*
pointing to anRGBLED
object) toRGBLED*
. If the cast fails (i.e.,myLed
doesn’t point to anRGBLED
object),dynamic_cast
returnsnullptr
, which allows us to check for success or failure. - Safety Check: The code checks if
rgbPtr
isnullptr
(i.e., the downcast failed). If it is, it halts execution to prevent undefined behavior, ensuring that further operations onrgbPtr
don’t cause issues.
- Dynamic Memory Allocation:
- Concept: The
new
operator is used to allocate memory for objects dynamically at runtime. This allows flexible and efficient management of resources. In the example, memory is allocated for bothmLED
andRGBLED
objects. - Resource Management: It’s important to deallocate memory using
delete
when the object is no longer needed. This avoids memory leaks. In this example,delete myLed;
is used to clean up the memory before reassigningmyLed
to a new object.
- Concept: The
The output of the program should look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mLED class: mLED() constructor
mLED class: begin() method
mLED class: turnOn() method
mLED class: turnOff() method
mLED class: mLED() destructor
mLED class: mLED() constructor
RGBLED class: RGBLED() constructor
RGBLED class: turnOn() method
RGBLED class: turnOff() method
RGBLED class: fadeColor() method
RGBLED class: fadeColor() method
RGBLED class: fadeColor() method
RGBLED class: fadeColor() method
...