This tutorial intends to be (or at least become) a structured introduction how to use arrays, structures and classes to handle multiple actuators or sensors in one sketch.
The links in the following content list refer to the corresponding Wokwi projects.
Content
- Without Struct I
Without Struct I - Wokwi Arduino and ESP32 Simulator
Basic solution to switch a LED on and off in given time periods
Sketch ''Without Struct I"
uint8_t pinNo = 13;
uint8_t state = LOW;
unsigned long onTime = 1000; // 1000 msec = 1 sec
unsigned long offTime = 500; // 500 msec = 0.5 sec
unsigned long lastTimeSwitched = 0;
void switchLED() {
unsigned long waitTime;
if (state)
{
waitTime = onTime;
}
else
{
waitTime = offTime;
}
if (millis() - lastTimeSwitched >= waitTime) {
lastTimeSwitched = millis();
state = !state;
digitalWrite(pinNo, state);
}
}
void setup() {
Serial.begin(115200);
Serial.println("Start ");
pinMode(pinNo, OUTPUT);
}
void loop() {
switchLED();
}
Detailed Explanations "Without Struct I"
/*
=============================================================
Background:
Introductory examples often make use of the delay function as it
is the most simple approach to wait for a certain time. This
is okay for basic demonstration purposes. However the delay()
function blocks the microcontroller for the complete time interval.
This means the controller cannot perform any other function
in loop() until the delay interval is over. This hinders
e.g. to react to status changes of buttons, read data from
Serial input, measure distances or perform other time critical
functions.
Objective of this Sketch:
This first sketch shows a usual solution to switch a LED on and off
for given intervals without using the delay() function in loop().
It is possible now to add further functionality as loop() is
not blocked by a delay().
Hardware:
This sketch uses pin 13 of an ESP32 as used in the corresponding
Wokwi project. However it is neither limited nor oprimized for use
with an ESP32. The sketch can be changed easily to work with other
controllers such as Arduino UNO, MEGA or the like.
For Arduino Uno pin 13 (D13) is connected to the board internal
LED. So the sketch can be used without any change to control this
LED. An external LED (with additional resistor to limit the current)
can also be connected to this pin. Or any other pin that allows
digitial output can be used. In that case pinNo has to changed to
the appropiate value.
=============================================================
The Sketch in Detail:
*/
uint8_t pinNo = 13;
/*
defines the number of the output pin where the LED is connectet to.
As this number will never be less than zero (we do not expect a negative
number for the pin assignment in the Arduino world) it is declared as uint8_t.
It could also be "byte" as this is also an 8 bit unsigned number but uint8_t
clearly states "u" for unsigned, "int" for integer and 8 for eight bit.
We are just introducing the habit of using "uintxx_t" as this becomes
more important when it comes to "integer" types where it may make a big
difference whether one uses e.g. "long" or "unsigned long".
*/
uint8_t state = LOW;
/*
declares a variable, again of type uint8_t, which will hold the state of
the pin pinNo. "state" will contain either the value LOW or HIGH which
control the voltage level at pinNo (LOW -> 0V, HIGH -> 3.3V or 5V depending
on the board used).
(The names LOW and HIGH are usually defined in the library Arduino.h which is - in most cases -
included by the Arduino IDE or other libraries without the need to explicitely
call it in a sketch. A lot of #defines and basic functions will be missing
if the IDE fails to include this library.)
*/
unsigned long onTime = 1000; // 1000 msec = 1 sec
/*
declares a variable of data type "unsigned long" with a range from 0 to 4,294,967,295.
Although we use only a small part of this range, it is wise to use this data type:
The millis() function which counts the milliseconds since the last (re)start of the
controller returns an "unsigned long" value and we want compare time differences.
By using the same type we avoid possible conversion/calculation problems between
data of different types. The value of onTime specifies the interval in msecs we
want the LED to be switched on.
*/
unsigned long offTime = 500; // 500 msec = 0.5 sec
/*
The value of offTime specifies the interval in msecs we want the LED to be switched off.
The reason for the data type is exactly the same as with onTime.
*/
unsigned long lastTimeSwitched = 0;
/*
This variable will be used to store the value of millis() at the time we switch
the LED on and off. It is initialized with zero so that the first activity will
be performed after the sketch has been (re)started.
*/
void switchLED() {
/*
This function is declared and defined before loop() and will be used in loop()
to check whether and when the LED status has to change and if the LED will be
switched on or off.
*/
unsigned long waitTime;
/*
is a local variable only used inside switchLED() to store the actual time to wait
depending on the state (LOW or HIGH).
*/
if (state)
/*
if the state is HIGH the variable waitTime shall have the same value as onTime
*/
{
waitTime = onTime;
}
else
/*
else the variable waitTime shall have the same value as offTime
*/
{
waitTime = offTime;
}
if (millis() - lastTimeSwitched >= waitTime)
/*
Here we subtract the time when the LED was switched before from
the actual time (millis()) and check whether this is equal or greater than
the waitTime as set in the lines above.
After restart or start the sketch will also not switch the LED before the
programmed first(!) waitTime is reached! As we defined state = LOW, it will
take in minimum "offTime". If you have setup() procedures that delay before
loop() is started or "offTime" is very short, millis() may already have
reached or exceeded "offTime" of course.
If this is the case the sketch enters the next software block otherwise
this block is ignored:
*/
{
lastTimeSwitched = millis();
/*
As the time interval is over we copy the actual time to lastTimeSwitched so
that the next time interval starts "now"...
*/
state = !state;
/*
We use the "logical not" operator (the exclamation mark !) to invert the value
of state as !LOW equals HIGH and !HIGH equals LOW.
*/
digitalWrite(pinNo, state);
/*
Here we switch the pin pinNo to either HIGH or LOW depending on the value of state.
*/
}
}
void setup() {
Serial.begin(115200);
Serial.println("Start ");
/*
In setup() we start Serial so that we get the message "Start" in the Serial Terminal
when the sketch has started, just to please our nerves ...
*/
pinMode(pinNo, OUTPUT);
/*
With pinMode() we configure the hardware pin pinNo as a digital output.
*/
}
void loop() {
switchLED();
/*
Here our function switchLED() is called more than 100,000 times per second!
And everything loops in loop() ...
*/
}
- Without Struct II
Without Struct II - Wokwi Arduino and ESP32 Simulator
Brute Force method to control three LEDs by copying variables and functions three times
as often done by beginners
Sketch "Without Struct II"
uint8_t greenPinNo = 13;
uint8_t greenState = LOW;
unsigned long greenOnTime = 1000; // 1000 msec = 1 sec
unsigned long greenOffTime = 500; // 500 msec = 0.5 sec
unsigned long greenLastTimeSwitched = 0;
uint8_t yellowPinNo = 12;
uint8_t yellowState = LOW;
unsigned long yellowOnTime = 500; //
unsigned long yellowOffTime = 500; //
unsigned long yellowLastTimeSwitched = 0;
uint8_t redPinNo = 14;
uint8_t redState = LOW;
unsigned long redOnTime = 500; //
unsigned long redOffTime = 1000; //
unsigned long redLastTimeSwitched = 0;
void switchLEDGreen() {
unsigned long waitTime;
if (greenState)
{
waitTime = greenOnTime;
}
else
{
waitTime = greenOffTime;
}
if (millis() - greenLastTimeSwitched >= waitTime) {
greenLastTimeSwitched = millis();
greenState = !greenState;
digitalWrite(greenPinNo, greenState);
}
}
void switchLEDYellow() {
unsigned long waitTime;
if (yellowState)
{
waitTime = yellowOnTime;
}
else
{
waitTime = yellowOffTime;
}
if (millis() - yellowLastTimeSwitched >= waitTime) {
yellowLastTimeSwitched = millis();
yellowState = !yellowState;
digitalWrite(yellowPinNo, yellowState);
}
}
void switchLEDRed() {
unsigned long waitTime;
if (redState)
{
waitTime = redOnTime;
}
else
{
waitTime = redOffTime;
}
if (millis() - redLastTimeSwitched >= waitTime) {
redLastTimeSwitched = millis();
redState = !redState;
digitalWrite(redPinNo, redState);
}
}
void setup() {
Serial.begin(115200);
Serial.println("Start ");
pinMode(greenPinNo, OUTPUT);
pinMode(yellowPinNo, OUTPUT);
pinMode(redPinNo, OUTPUT);
}
void loop() {
switchLEDGreen();
switchLEDYellow();
switchLEDRed();
}
- Without Struct III
Without Struct III - Wokwi Arduino and ESP32 Simulator
Solution to control three LEDs using one dimensional arrays for the variables
Much easier to maintain and add further LEDs.
Sketch "Without Struct III"
const int noOfLeds = 3;
uint8_t pinNo[noOfLeds] = {13, 12, 14};
uint8_t state[noOfLeds] = {LOW, LOW, LOW };
unsigned long onTime[noOfLeds] = {1000, 500, 500};
unsigned long offTime[noOfLeds] = { 500, 500, 1000};
unsigned long lastTimeSwitched[noOfLeds] = { 0, 0, 0};
void switchLED(int no) {
unsigned long waitTime;
if (state[no])
{
waitTime = onTime[no];
}
else
{
waitTime = offTime[no];
}
if (millis() - lastTimeSwitched[no] >= waitTime) {
lastTimeSwitched[no] = millis();
state[no] = !state[no];
digitalWrite(pinNo[no], state[no]);
}
}
void setup() {
Serial.begin(115200);
Serial.println("Start ");
for (int i = 0; i < noOfLeds; i++) {
pinMode(pinNo[i], OUTPUT);
}
}
void loop() {
for (int i = 0; i < noOfLeds; i++) {
switchLED(i);
}
}
- With Struct I
With Struct I - Wokwi Arduino and ESP32 Simulator
Solution to control three LEDs with a struct for the LED specific variables and a function to switch
the LEDs that uses the address operator & to handle the struct and not a copy of its data. There is a separate struct defined per LED.
Sketch "With Struct I"
struct ledType {
uint8_t pinNo;
uint8_t state;
unsigned long onTime;
unsigned long offTime;
unsigned long lastTimeSwitched;
};
ledType greenLED = {13, LOW, 1000, 500, 0};
ledType yellowLED = {12, LOW, 500, 500, 0};
ledType redLED = {14, LOW, 500, 1000, 0};
void switchLED(ledType &led) { // & tells the compiler to use the address of the variable and NOT a copy!!
unsigned long waitTime;
if (led.state)
{
waitTime = led.onTime;
}
else
{
waitTime = led.offTime;
}
if (millis() - led.lastTimeSwitched >= waitTime) {
led.lastTimeSwitched = millis();
led.state = !led.state;
digitalWrite(led.pinNo, led.state);
}
}
void setup() {
Serial.begin(115200);
Serial.println("Start ");
pinMode(greenLED.pinNo, OUTPUT);
pinMode(yellowLED.pinNo, OUTPUT);
pinMode(redLED.pinNo, OUTPUT);
}
void loop() {
switchLED(greenLED);
switchLED(yellowLED);
switchLED(redLED);
}
- With Struct II
With Struct II - Wokwi Arduino and ESP32 Simulator
The same solution as in 4. but this sketch defines an array of structs. This way it combines structs with the advantage of arrays as in 3.
Sketch "With Struct II"
struct ledType {
uint8_t pinNo;
uint8_t state;
unsigned long onTime;
unsigned long offTime;
unsigned long lastTimeSwitched;
};
const int noOfLeds = 3;
ledType LEDs[noOfLeds] =
{
{13, LOW, 1000, 500, 0},
{12, LOW, 500, 500, 0},
{14, LOW, 500, 1000, 0}
};
void switchLED(ledType &led) { // & tells the compiler to use the address of the variable and NOT a copy!!
unsigned long waitTime;
if (led.state)
{
waitTime = led.onTime;
}
else
{
waitTime = led.offTime;
}
if (millis() - led.lastTimeSwitched >= waitTime) {
led.lastTimeSwitched = millis();
led.state = !led.state;
digitalWrite(led.pinNo, led.state);
}
}
void setup() {
Serial.begin(115200);
Serial.println("Start ");
for (int i = 0; i < noOfLeds; i++) {
pinMode(LEDs[i].pinNo, OUTPUT);
}
}
void loop() {
for (int i = 0; i < noOfLeds; i++) {
switchLED(LEDs[i]);
}
}
- With Struct III
With Struct III - Wokwi Arduino and ESP32 Simulator
This sketch is based on solution 4. but the struct includes now functions to initialize data and
pins as well as the function to switch the LED on/off. The variables inside the struct are declared
as "private" so that they can only be accessed and changed via the public functions.
Sketch "With Struct III"
struct ledType {
public:
void init(uint8_t pin, unsigned long on, unsigned long off);
void switchLED();
private:
uint8_t pinNo;
uint8_t state = LOW;
unsigned long onTime;
unsigned long offTime;
unsigned long lastTimeSwitched = 0;
};
void ledType::init(uint8_t pin, unsigned long on, unsigned long off) {
ledType::pinNo = pin;
ledType::onTime = on;
ledType::offTime = off;
pinMode(pin, OUTPUT);
}
void ledType::switchLED() {
unsigned long waitTime;
if (ledType::state)
{
waitTime = ledType::onTime;
}
else
{
waitTime = ledType::offTime;
}
if (millis() - ledType::lastTimeSwitched >= waitTime)
{
ledType::lastTimeSwitched = millis();
ledType::state = !ledType::state;
digitalWrite(ledType::pinNo, ledType::state);
}
}
ledType greenLED;
ledType yellowLED;
ledType redLED;
void setup() {
Serial.begin(115200);
Serial.println("Start ");
greenLED.init( 13, 1000, 500);
yellowLED.init(12, 500, 500);
redLED.init( 14, 500, 1000);
}
void loop() {
greenLED.switchLED();
yellowLED.switchLED();
redLED.switchLED();
}
- With Class I
With Class I - Wokwi Arduino and ESP32 Simulator
This sketch is identical to solution 6. except that the definition of "struct" has now been named
"class". It shows that both are almost the same in C++. Just that every entry in a struct is public
if not declared as private while in a class every entry is private if not explicitly declared as
public.
Sketch "With Class I"
class ledType {
public:
void init(byte pin, unsigned long on, unsigned long off);
void switchLED();
private:
byte pinNo;
byte state = LOW;
unsigned long onTime;
unsigned long offTime;
unsigned long lastTimeSwitched = 0;
};
void ledType::init(byte pin, unsigned long on, unsigned long off) {
ledType::pinNo = pin;
ledType::onTime = on;
ledType::offTime = off;
pinMode(pin, OUTPUT);
}
void ledType::switchLED() {
unsigned long waitTime;
if (ledType::state)
{
waitTime = ledType::onTime;
}
else
{
waitTime = ledType::offTime;
}
if (millis() - ledType::lastTimeSwitched >= waitTime) {
ledType::lastTimeSwitched = millis();
ledType::state = !ledType::state;
digitalWrite(ledType::pinNo, ledType::state);
}
}
ledType greenLED;
ledType yellowLED;
ledType redLED;
void setup() {
Serial.begin(115200);
Serial.println("Start ");
greenLED.init( 13, 1000, 500);
yellowLED.init(12, 500, 500);
redLED.init( 14, 500, 1000);
}
void loop() {
greenLED.switchLED();
yellowLED.switchLED();
redLED.switchLED();
}
- With Class II
With Class II - Wokwi Arduino and ESP32 Simulator
This sketch shows how to declare and use the constructor of a class to set variables and to perform required initialisation processes (like pinMode()).
Sketch "With Class II"
class ledType {
public:
ledType(uint8_t pin, unsigned long on, unsigned long off);
void switchLED();
void begin();
private:
uint8_t pinNo;
uint8_t state = LOW;
unsigned long onTime;
unsigned long offTime;
unsigned long lastTimeSwitched = 0;
};
ledType::ledType(uint8_t pin, unsigned long on, unsigned long off) {
pinNo = pin;
onTime = on;
offTime = off;
}
void ledType::begin() {
pinMode(pinNo, OUTPUT);
}
void ledType::switchLED() {
unsigned long waitTime;
if (state == HIGH) {
waitTime = onTime;
} else {
waitTime = offTime;
}
if (millis() - lastTimeSwitched >= waitTime) {
lastTimeSwitched = millis();
state = !state;
digitalWrite(pinNo, state);
}
}
ledType greenLED( 13, 1000, 500);
ledType yellowLED(12, 500, 500);
ledType redLED( 14, 500, 1000);
void setup() {
Serial.begin(115200);
Serial.println("Start ");
greenLED.begin();
yellowLED.begin();
redLED.begin();
}
void loop() {
greenLED.switchLED();
yellowLED.switchLED();
redLED.switchLED();
}
- With Class III
With Class III - Wokwi Arduino and ESP32 Simulator
This sketch is identical to 8. but the class declaration has been moved to a separate file that is
included in the main file by the #include instruction.
Sketch "With Class III"
Main sketch:
#include "LedType.h"
ledType greenLED( 13, 1000, 500);
ledType yellowLED(12, 500, 500);
ledType redLED( 14, 500, 1000);
void setup() {
Serial.begin(115200);
Serial.println("Start ");
greenLED.begin();
yellowLED.begin();
redLED.begin();
}
void loop() {
greenLED.switchLED();
yellowLED.switchLED();
redLED.switchLED();
}
The separate file "LedType.h" to be stored in the same directory as the main file:
#ifndef LEDTYPE_H
#define LEDTTYPE_H
#include <Arduino.h>
class ledType {
public:
ledType(uint8_t pin, unsigned long on, unsigned long off);
void switchLED();
void begin();
private:
uint8_t pinNo;
uint8_t state = LOW;
unsigned long onTime;
unsigned long offTime;
unsigned long lastTimeSwitched = 0;
};
#endif
The separate file "LedType.cpp" to be stored in the same directory as the main file and the header file:
#include "LedType.h"
ledType::ledType(uint8_t pin, unsigned long on, unsigned long off) {
pinNo = pin;
onTime = on;
offTime = off;
}
void ledType::begin() {
pinMode(pinNo, OUTPUT);
}
void ledType::switchLED() {
unsigned long waitTime;
if (state == HIGH) {
waitTime = onTime;
} else {
waitTime = offTime;
}
if (millis() - lastTimeSwitched >= waitTime) {
lastTimeSwitched = millis();
state = !state;
digitalWrite(pinNo, state);
}
}
Explanations to "With Class III"
Header file with explanations
/*
This an example of a Header File as e.g. used for libraries.
It contains
- a header guard (the #ifndef part ...) which ensures that this file is only
used once when included
- the inclusion of the file Arduino.h where "defines" are performed that
are used in the following code, like e.g. "uint8_t"
- the interface to the class "ledType"
If this header file is included in a sketch the .cpp file with the same name
(in this case LedType.cpp) will be loaded by the compiler automatically.
*/
#ifndef LEDTYPE_H // If LEDTYPE_H is already defined, the rest of the sketch
// until the final "#endif" is ignored
#define LEDTTYPE_H // Otherwise LEDTYPE_H will be defined here and is now
// available for a possible but unwanted additional inclusion
// This type of "header guard" is widely spread in Arduino libraries.
// A newer possibility is just to write
// #pragma once
// at the beginning of the file which has the same effect
#include <Arduino.h> // This include is required just in case that this lib has not
// been included before, it contains the declaration of uint8_t
// used in the following code, here in .h and in LedType.cpp
class ledType { // This line defines the name of the class ledType
public: // The following elements are available for use in sketch.ino
ledType(uint8_t pin, unsigned long on, unsigned long off); // The constructor function that
// sets the LED pin and the on- and off
// intervals
void switchLED(); // The function to be called in loop() to switch the LED
void begin(); // This function has to be called once in setup() to
// prepare the pinMode for the LED pin.
private: // The following elements cannot be changed or read in sketch.ino
// They are for internal use within the class functions only
// This concept "encapsulates" data and functions in a class
// which shall not be controlled or used from outside.
// This way the developer can avoid interference by third party
// to the coded functionality.
uint8_t pinNo; // The pin where the LED is connected to
uint8_t state = LOW; // The actual state of the LED LOW = off, HIGH = on
unsigned long onTime; // The time the LED shall remain switched on
unsigned long offTime; // The time the LED shall remain switched off
unsigned long lastTimeSwitched = 0; // The last time the LED changed its state
};
#endif // The closing "#endif" for the "#ifndef" of the "header guard" at the top
The .cpp-File with explanations:
/*
This an example of a cpp File as e.g. used for libraries.
It contains
- no header guard as this is taken care of in the header file "LedType.h".
- the inclusion of the file LedType.h the interface to the class "ledType"
is defined
- the definitions of the functions which are a part of the class ledType
which are named in the header file but are not defined yet
*/
#include "LedType.h" // This line includes the definition of the class members/elements
// The following line defines how to call the constructor when using the class
// Example:
// ledType blueLED(12, 500, 1000);
// - creates an object of type ledType named blueLED
// - sets the internal variables
// pinNo to 12
// onTime to 500 [msec]
// offTime to 1000 [msec]
ledType::ledType(uint8_t pin, unsigned long on, unsigned long off) {
pinNo = pin;
onTime = on;
offTime = off;
}
// As one cannot be sure that the required hardware access is already available when the
// constructor is called, it is recommended to use a separate function e.g. .begin()
// to perform hardware specific functions
// The function begin() has to be called once in setup() to set the pinMode correctly
void ledType::begin() {
pinMode(pinNo, OUTPUT);
}
// This is the main functionality of the class. It requires a non-blocking loop()
// for correct performance as it is based on the evaluation of the actual time in
// millis(). If other functions in loop() block longer than the programmed intervals
// it cannot switch in due time.
void ledType::switchLED() {
unsigned long waitTime; // A temporary storage for the actual waitTime (interval to wait)
if (state == HIGH) { // If the LED is on (the pin is set HIGH)
waitTime = onTime; // waitTime shall be equal to onTime
} else { // if not (which means LED is off, the pin set to LOW)
waitTime = offTime; // waitTime shall become equal to offTime
}
if (millis() - lastTimeSwitched >= waitTime) // The time between now (millis()) and the last change
// (lastTimeSwitched) is calculated and compared
// to waitTime
// if waitTime is neither reached nor exceed
// the following lines will be ignored
{ // But if the time is over
lastTimeSwitched = millis(); // lastTimeSwitched stores the actual time
state = !state; // The state is inverted (LOW -> HIGH, HIGH -> LOW)
digitalWrite(pinNo, state); // and the LED pin is set to the new state
}
}