ARDUINO Beginner To Advanced
ARDUINO Beginner To Advanced
Beginner to Advanced
ARDUINO
Beginner to Advanced
HappyLogic
Copyrights © All Rights are Reserved.
About the Book
Even though, there are many titles available for learning Arduino from
programming perspective mainly, but it is highly imperative to consider
various hardware, code management, solution architecture and other
programming concepts for being successful at designing medium to large
scale solutions. Arduino itself, has made programming different
microcontrollers, and evaluating designs quite easy; additionally,
understanding the hardware, programming fundamentals and code
architecture are key areas for building stable and extensible solutions
utilizing fundamental building blocks already provided by the software
development kit.
Hence, this book is aimed at acting as a starting point for beginners,
hobbyists, and junior designers – guiding towards designing various
hardware projects utilizing Arduino, while also guiding through to handling
projects with varying difficulties involving various multiple elements.
Afterall, today’s microcontrollers are much more powerful and capable than
most personal computers used during the nineties, though project
complexities in real-world scenarios have also grown in and beyond
equivalent proportions. Still, it requires quite an undertaking to utilize all
the computing and hardware resources in the majority of modern
microcontrollers at full capacity.
Although, selection of microcontrollers must be totally based on
requirements of target utility; mostly, modern applications are not just about
handling one or two tasks with controllers. Often in real-world applications
surplus computing and hardware resources are kept in designs for future
expansions without incurring massive hardware changes. Therefore, in
almost all the modern applications, expansion with system stability remains
a constant concern. Without designing applications with extensibility in
mind, stability concerns are almost always there to be dealt with –
sometimes through a painful process and debugging. Management of
fragility in designs is an ongoing concern that needs consideration early for
all the systems that are expected to grow over time. Due to inherent ease of
prototyping with Arduino, many a times best development practices are
rarely seen especially amongst new designers. These do not cause
difficulties during initial phases but become evident as systems grow in
complexity.
Efforts are made to keep this book concise and to the point; whereby,
initially, general programming concepts from C++ are discussed especially
those which readers are most likely to use and encounter in hardware
programming. Thereafter, Arduino development and hardware concepts are
discussed, along with Arduino general libraries, including design best
practices and further resources.
Figure 1. IC Programmer
IC Burning devices are also available in the form whereby multiple ICs
are burnt in a single go. Their intended uses are in mass production.
Times have changed quite a lot, and much better options are now
available that speed up the overall development cycle and facilitate in
finding problems quite easily. Now, we no longer have to go through long
development cycles anymore, unless we have some specific requirements,
or we have to support some legacy systems. Arduino software development
kit and development boards, along with some other such solutions, are quite
special in this regard. It is one such thing that shortens developing and
prototyping cycle, facilitates quick idea validations, and even deploying
some quick solutions, alongside developing hobbyist-grade projects. Users
readily get pre-patched minimal circuits with quick and easy IC
programming facility to enjoy a head-start.
Developing understanding of underlying technologies plays a major role
in developing effective solutions. In order to develop an understanding in
the embedded world, we will first look at different concepts so as to bring
everyone on the same page before proceeding further with developing
solutions with modern tools.
What are microcontrollers?
Without going to reproduce a customary history lesson on
microcontroller evolution since late 1960s, it is pertinent to discuss their
characteristics and capabilities.
In an IoT system – data is the king. The more relevant data that one has,
the better results can be achieved.
Anyone may look around and quickly find ways in which a lot of things
around them can be connected into a system through different means and
techniques, thereby automating daily routines – rendering lives easy and
efficient. For example, by learning electricity prices during different times
of a day, an IoT enabled washing system can plan jobs for times when
prices are lowest; a smart tagging system can remind you of picking up
required items when you are approaching your car in case, they are not with
you; and a voice command can rearrange smart and movable furniture and
adjust ambient lights as you would prefer while working or while watching
a movie according to preferences.
Now we can install and run Linux environment – just open the Microsoft
Store application and search for Linux distribution. There are many
distributions available through app store to run on WSL, just for the sake
of an example, search for “ubuntu.”
After installation, run the app from the Start Menu. In the first run it will
go through installing base packages, making the environment ready for use.
Figure 9. Ubuntu running in WSL environment
For our simple learning path, the only required tools are gcc for
compiling C code files,
g++ for compiling C++ files and the make utility for introducing the build
automation. After installation, check their versions just to check if the tools
are installed successfully.
gcc --version
g++ --version
make --version
…
user@machine:~$ make --version
gcc --version
g++ --version
make --version
With code editors getting better and better with time, distinction between
them and IDEs has become quite blurry within the recent past. Many
code editors are quite capable and extensible enough to provide all the
features an IDE provides.
Our first program and build script
To evaluate our environment, let us write simple code and run it. For
organization purposes, all the examples are being kept inside the parent
folder “ arduino-ba-examples ,” first let us create it.
mkdir arduino-ba-examples
Then to work inside it, we will change current working directory to it.
cd arduino-ba-examples
mkdir Chapter-02
code Chapter-02
Now we can completely work inside VS Code to create files and even
accessing command shell inside the editor. For accessing the terminal,
select “New Terminal” option inside the “Terminal” menu.
Now write the following few lines in the file and save it.
#include <iostream>
int main() {
C++ programs start execution from main() function. Let us compile and
run it. Open the terminal and type:
./MyProgram
default:
clean:
rm MyProgram
make
As we are using the default name for our Makefile , we do not need to
provide a file name while running the make utility.
Now that we have built and run our first application – we can build up
on simple concepts towards creating much more complex applications.
Final code can be accessed at:
https://github.com/arduino-ba
There are various build systems readily available though many are not
cross-platform, CMake endeavors to provide a uniform cross-platform
build experience.
intentionally left blank
Chapter 3:
Revisiting C++ — Constants, Variables,
Structures, and
Flow Control
I nwritten
coding – no matter which programming language the code is being
in, there are certain building blocks or the basic code elements that
are used in combination accordingly to create desired results or outputs.
Afterall, any software system can be simply viewed as taking some inputs,
doing some processing, and producing some outputs – mostly in repetitions.
Here registers are of varied sizes with some of them representing lower
bytes or lower words of their corresponding larger registers. As a special
register, IP (Instruction Pointer) or its larger variants track execution of
code by holding the address of next instruction that will be executed. Other
registers are there to help with certain other tasks.
Let us say we need to add two numbers. Therefore, first we can load the
first number in one register and the second number in another register, then
both the values can be provided to internal processor circuits that are
capable of performing arithmetic operations, and the result of the operation
can be stored in another register that can later be read or transferred to a
memory location for later use.
There are other registers as well for special operations – for example,
floating-point calculations, running single instruction on multiple data
values, controlling memory operations, tracking operation states, and many
other complicated operations that newer processors offer. It is pertinent to
mention that other architectures like for example ARM names its registers
differently and exact functionality can be learnt through processor
documentation. The beauty of higher-level programming languages comes
in part that we do not have to memorize the names of these registers to
perform common operations, as compilers and execution environments take
care of these elements in the background.
Another especially important feature that processors offer is “timers,”
that enable controlling external hardware, internal code execution and
provide timings. For instance, Operating Systems use timers to allow
multiple threads to keep running one after the other – let us say we have a
processor that has 4 cores; at max it can run 4 threads of instruction streams
in parallel; but if we have more number of threads; then to let all the threads
run in their own turns, OS Kernel uses timers. For example, to let a thread
run for 30 milliseconds the OS Kernel can start a timer for 30 milliseconds
and let the thread execute on the processor; now when the specified time
elapses an interrupt is generated internally that lets the Kernel code run that
in turn switches execution context to let new thread run during next
interval. For real-time applications, timers are specifically important.
ROM
Read-only Memory – as the name indicates is memory that can only be
read, not written. Although it is an oversimplification nowadays, due to this
name having its roots back in the early days of computing. Nowadays,
mostly any memory regarded as read-only, can actually be written
electronically. For example, BIOS (Basic Input Output System) or their
newer version UEFI (Unified Extensible Firmware Interface) can be
regarded as ROM in desktops, yet they can be upgraded. In a general-
purpose computer, ROMs are primarily used for initial system startup and
for microcontrollers these are for storing main program that executes.
Though, there are read-only memories in use that cannot be written at all.
As another important feature, data written is not lost when power is lost
or interrupted – it is particularly important for microcontrollers to store
settings and other user provided data in EEPROM (Electrically Erasable
and Programmable Read Only Memory) or Flash memories.
RAM
Random Access Memory is where most of the transactions are done by
the processor – all the variable data is stored here. Especially, in general-
purpose computers, any program that needs to be executed is first loaded in
RAM and then execution is done from there.
Mostly, RAMs are byte-oriented devices – meaning that each byte has a
unique address, and the minimum width of data that can be read or written
is a byte (8-bits) or multiple bytes depending upon number of data lines
between processor and RAM. An important thing here depends on how
values are stored in the RAM addresses. For example, if a number that
needs 8-bytes to represent has to be stored in consecutive memory
locations, say a hexadecimal number 1122334455667788 has to be stored in
RAM location 0x0000000000001000 on a 64-bit computer. This can be done in
one of two ways:
In little endian, least significant byte is stored first and in big endian,
most significant byte is stored first, with other bytes stored in consecutive
locations. For example, Intel processors use little endian scheme, whereas
some ARM based processors and microcontrollers are capable of handling
both types of storage schemes. This becomes quite important when
managing underlying raw data is required.
Instruction and Data Caches
To state simply, cache (read as “cash”) has only one purpose to perform
– boost execution performance. Caches are mostly internal to processors;
however, they can be installed outside the processor as well. To cut a long
story short, whenever the processor reads instructions or data from RAM,
caches store them and during subsequent reads of same instructions or data
(if they are available in cache), the read times are significantly reduced as
compared with reading from actual location.
Hardware Controllers
Hardware controllers – as the name suggests, control the hardware. A
single controller can control single or multiple hardware devices; but more
importantly, the controllers provide an interface between hardware devices
and the processor. There are many types of controllers we encounter
commonly, rather without paying much attention to them: USB (Universal
Serial Bus) controllers, HID (Human Interface Device) controllers, Ethernet
/ Wi-Fi controllers and SATA (Serial ATA) controllers; just to name a few.
Direct Memory Access
Normally whenever hardware completes transmitting provided data or
receives part of data from outside, it raises interrupt to tell the processor of
the event so that the processor can use the received data or can provide next
data for transmission. For example, if serial communication is being done,
after receiving each byte or transmitting each byte, interrupt can be raised
so that next actions can be taken.
Servicing interrupts is a heavy task – as the processor needs to stop
doing whatever it is doing and service the pending awaiting interrupt. When
communication rates are high, or multiple hardware are generating
interrupts, it can reduce processing efficiency a lot as considerable time
would be required in servicing the interrupts instead of running the
application. This issue is resolved by using Direct Memory Access,
whereby the hardware is granted access to memory locations.
During a data transmission operation, hardware can transmit the data
consecutively written in memory locations; and at the end, upon operation
completion, it finally raises interrupt. Whereby during a data reception
operation, hardware can store the received data in consecutive memory
locations; and at the end of the operation, it can finally raise interrupt for
processor to finally handle the data. This drastically reduces the number of
interrupts raised, and makes the interrupts more meaningful – thus,
allowing better utilization of processing resources.
Comments
Before we begin discussing the coding aspects, it is quite important to
consider commenting as well. We know that C++ code files are basically
plain text files that are parsed by compiler to produce machine code. These
files can contain comments that are skipped by compiler while parsing the
file; hence, comments are useful for documenting the code parts within the
code files.
Two types of comments are supported: Single-line comments start with
two forward slashes // and end at the end of corresponding text lines;
anything before // remains part of code, like:
The second one supported is multi-line comments that can span multiple
lines as the name suggests. A multi-line comment starts with /* and ends
with */ and anything before the starting sequence and after the ending
sequence remains part of code. These comments are
non-hierarchical – that whenever a multi-line comment is started with /*
and ended with */ it cannot contain internal comments with /*…*/ as the
first time */ is encountered after starting the comment will end the multi-
line comment. For example:
…some code… /* Anything that comes after the beginning of comment till
So, we can easily document code pieces just to remember when we see those pieces maybe
after many years or so… */
char a;
char b = 5;
char c = -3;
unsigned char d = 128;
int port_number = 5;
Here SPACE_CHAR gets the value 32 as 32 is the ASCII code for space
character, whereas CAP_A_CHAR gets the value 65 .
6. Since C++14, direct binary number representation has been
supported by prefixing the binary number with 0b :
// Accessible are:
int variable_1 = 1;
int variable_2 = 1;
// Accessible are: variable_1, variable_2
int variable_3 = 1;
Like, our sample application starts from main() and the braces after it
define its scope. Anything that is defined outside its scope is global in scope
and anything inside the scope is in local scope. The outer scope variables
can be accessed from inner scope just by using their names; however, when
the same name is used for an inner-scoped variable, it hides the outer
scoped variable within the inner scope. In such a case, to access a globally
scoped variable with same name, two colons “ :: ” (scope resolution
operator) can be used before its name, like:
::my_global_variable = 50;
Here, (target datatype) is the type casting operation that allows assigning
the value on right hand side having another datatype to the variable on left
hand side having target datatype. When the target datatype is large enough
to hold complete data, it is transferred as its original value, otherwise the
data is truncated from top, hence bits are lost.
Consider variables as buckets – when a value is cast into a larger bucket
it fits in completely, whereas, when larger bucket is cast into a smaller
bucket, it can lose data.
Loss of data during casting operation happens on most significant bytes;
however, the data in least significant bytes is copied to target variable.
Value vs Reference types
C/C++ allows two types of variables when referring to data – ones that
actually hold data values are referred to as value types and the ones that
hold reference to other variables’ data are referred to as reference types.
Essentially, without making copies of same variables, reference types can
refer to same data but with different names. A reference type can be
denoted by an ampersand “ & ” before their name:
int my_variable = 5;
Mathematical operations
Operato
Purpose Example Result
r
Resultant value after adding A
+ Addition A + B
and B
Gives result by subtracting B
- Subtraction A - B
from A
* Multiplication A * B Gives resultant value of
multiplying A with B
Quotient of integers’ division
/ Division A / B or full division result in
floating-point divisions
Remainder of integers’
% Remainder A % B
division
int variable_1 = 5;
int a = variable_1++;
int b = ++variable_1;
value_1 += 10;
Similarly, there are -= , *= , /= and %= operators:
Boolean logic
In C/C++, anything that evaluates to 0 is considered Boolean false, and
anything other than that is considered Boolean true. This often confuses
new developers and can cause many bugs in applications – for example, a =
5 is a Boolean true in C++, no matter what the older value was held in
variable a , it gets updated to 5 and the whole expression evaluates to true,
as it evaluates to a non-zero value. Therefore, to evaluate Boolean logic,
special attention must be given to avoid inadvertent use of equality operator
whereas the actual intention is to evaluate Boolean logic with equality
check ( == ) operator. Available Boolean operators are:
Operato
Stands for Example Returned resultant value
r
<< Left shift A << B Bits in A are B times shifted left
>> Right shift A >> B Bits in A are B times shifted right
&
Bitwise A&B
Bits in A AND with corresponding
AND bits of B
Bitwise Bits in A OR with corresponding
| A|B
OR bits of B
Bitwise Bits in A XOR with corresponding
^ A^B
XOR bits of B
Bitwise Bits in A are inverted ( 1 s to 0 s and
~ ~A
NOT 0 s to 1 s)
Ternary operator
Ternary operator decides between two options depending upon a
Boolean condition:
boolean_condition ? expression_1 : expression_2
int my_data_elements[5];
To access the third element that currently holds 30 , we can use its index
value, which is 2 as the indices start with 0 . Therefore, the following code
updates its value to 333 .
my_data_elements[2] = 333;
This only updates the individual element, nothing else – the array now
looks like:
The first dimension can be regarded as rows and the second as columns
– with this array having 2 rows and 3 columns:
my_2d_array[1][1] = 100;
As the array’s indices are 0-based, this statement updates the second
element in second array – or we can say, the element in second row and
second column is updated, thus making it:
Later, we will see better ways of storing messages in the form of strings.
Standard I/O
As we have established till now that any software or firmware that we
develop can be simply stated to be taking some inputs, processing them and
producing outputs accordingly. Additionally, in this book we are looking at
C++ with the purview of applying the knowledge in developing
applications for Arduino and other microcontrollers in general. In the small
devices’ world, inputs and outputs depend on hardware devices being
connected with microcontroller. Therefore, for keeping our learning focused
on purpose, we will use standard console inputs and outputs provided by
standard C++ library iostream ; Therefore, for outputting text on screen we
shall use std::cout (console output) given in standard library, like:
#include <iostream>
int main() {
return 0;
}
For taking input from user, we shall use std::cin (console input), also
given by standard library. For example:
#include <iostream>
int main() {
int val;
return 0;
In both the examples, the first line “ #include <iostream> ” in each tells the
compiler that we need to use some functionality provided by the standard
library named iostream , and the functionality that we need for outputting text
on console is through using std::cout , and for taking input from console (by
typing from keyboard) is through using std::cin , whereas, std::endl represents
the end of line or the newline character.
In using std::cout , we can send multiple values of variables, constants or
text enclosed between quotation marks to output screen by separating them
with << operator. In the first example, text enclosed between quotation
marks followed by an end of line character is sent to console output;
whereas, in second example, std::cout is used two times – first time it only
prints text on screen without outputting a newline so that user inputs text on
the same line as the message is printed; and in second use it outputs three
items i.e., text, variable value and newline. Example use of second snippet
is:
Just note here that the user has typed 123 at the input, which is stored in
val and later displayed in output on second line.
Structures
Structures are a way to combine different types of related data into
creating new datatypes that can represent an object or an element as a
combination of multiple properties.
struct structure_name {
};
Structure naming rules are the same as for naming the variables. To
elaborate their use, let us say we need to represent many circles; we can
combine all the data that represents a circle in a structure:
struct circle {
float x_position;
float y_position;
float radius;
};
c1.x_position = 120.50f;
c1.y_position = 100.99f;
c1.radius = 10.0f;
c2.x_position = 101.00f;
c2.y_position = 110.00f;
c2.radius = 12.2f;
c3.x_position = 310.10f;
c3.y_position = 100.55f;
c3.radius = 19.50f;
c4.x_position = 180.00f;
c4.y_position = 900.45f;
c4.radius = 11.0f;
c5.x_position = 650.45f;
c5.y_position = 540.25f;
c5.radius = 20.0f;
The main() function itself is executed by the background code that sets
up the execution environment for the program to run and passes in the
command-line arguments into the main() function; though, that is not
part of our discussion here – here we will only assume that main() is the
function where the execution begins.
if (condition)
…a single statement…;
if (condition) {
…statements…
In this way, the whole block of code, i.e., enclosed between the braces
will either execute fully or will not execute at all, depending upon whether
the condition evaluates as Boolean true or false. When the condition is true,
the whole block is executed otherwise the whole block of code is skipped.
Just to reemphasize, when braces are not used, if only applies conditional
execution to its following single statement, so, whenever multiple
statements are relevant in execution context, always use braces to enclose
them into a single block of execution.
…block 1 …
} else {
…block 2 …
}
Here, …block 1… is executed when the condition is true, otherwise …block
2… is executed – not both.
if (condition-1) {
…block 1 …
} else if (condition-2) {
…block 2 …
} else if (condition-3) {
…block 3 …
} else if (condition-4) {
…block 4 …
} else {
…block 5 …
int main() {
int age;
if (age < 0) {
} else if (age == 0) {
} else {
return 0;
We can clearly see in the example output that during each execution,
input is taken from the user, and evaluation is done in a chain of conditional
checks, whichever evaluates to true lets the controlled block of code run
and skips the rest.
Conditional statements – switch
if…else evaluates conditions and runs the corresponding statement or a
block of statements accordingly. However, when a single expression is
compared against a set of constant values to decide which set of statements
to run accordingly, switch provides a cleaner way with following format:
switch (expression) {
case constant-1:
…statements block 1 …
break;
case constant-2:
…statements block 2 …
break;
case constant-3:
…statements block 3 …
break;
case constant-4:
…statements block 4 …
break;
default:
…statements block 5 …
break;
#include <iostream>
int main() {
int option;
switch (option) {
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
default:
break;
return 0;
The example is quite simple, as the input value is compared against a set
of constants that are expected, otherwise the option is expected to be
invalid.
Looping – for loop
Repeating a certain single statement or a block of statements is simply
referred to as a loop. In C++, three types of loops are provided.
We also have goto statement that unconditionally jumps to another part
of code denoted by tag (like, jump_here: ) and program starts executing
from there; in combination with conditional checking e.g., with if…else ,
goto can be used for repetitions – though, usage of goto is considered by
many as a bad practice, as one can easily go offtrack and software bugs
can be quite hard to locate.
Each of the three loops we are going to discuss, either repeats a single
statement or multiple statements enclosed within curly braces {} – just like
we have seen in if…else usage.
The first and probably the most elaborate loop is for – in recent
standards, for loop has been enhanced to work on collections and arrays;
however, we will only be using its original and older form:
…block of code…
…block of code…
#include <iostream>
int main() {
int table;
<< n
<< table * n
<< std::endl;
return 0;
}
A sample execution is:
We are simply taking in input from user and running the for loop to
have values from 1 to 10 – incrementing the variable after each iteration of
the loop; therefore, simple screen output is generated during each loop
iteration.
Looping – while loop
The simplest loop is while – simply speaking, while keeps running a
block of code or a single statement as long as the condition remains
Boolean true:
while (condition) {
…block…
It is important to keep in mind that semicolon is not placed just after the
while condition parentheses – as semicolon itself is a blank statement;
therefore, once the loop starts executing, it may run forever.
while (condition); {
…block…
This is WRONG – semicolon does not let the while loop repeatedly
execute …block… as the while loop executes its following single
instruction unless it has a block of code within a set of braces {} ; since a
semicolon is a blank instruction itself just after the while statement, this
is what is executed by the while loop in this case not the instructions
given inside the block of code.
For understanding purpose, let us implement the same table application
with while loop as well:
#include <iostream>
int main() {
int table;
int n = 1;
<< n
<< std::endl;
n++;
return 0;
The initialization of variable is done outside the loop here, and the
control variable is incremented to control the loop – this ends the loop when
table is complete.
Looping – do-while loop
When we have a requirement to execute a certain block of code at least
once, and its repetition must be decided after first execution, we can use
do…while loop:
do {
…block…
} while (condition);
#include <iostream>
int main() {
int value;
do {
std::cout << " Value?: ";
return 0;
We are simply taking user input and evaluating it to keep the loop
running – as we need to get the first input in any case, we are using the do…
while loop for this scenario.
break;
continue;
Whenever a break statement is executed within the body of a loop, the
loop is exited; whereas, on encountering the continue statement, remaining
instructions inside the loop body are skipped and the next loop iteration is
started.
For example:
if (n == 8) break;
if (n == 3) continue;
#include <iostream>
int main() {
int table;
while (1) {
<< n
<< table * n
<< std::endl;
return 0;
}
Here the outer loop while (1) is an infinite loop because in C/C++ any
non-zero value evaluates to Boolean true, therefore this outer loop never
exists on its own. Within the loop we are getting input from user, and if the
provided number does not meet our valid range of values, we break out of
the main loop, otherwise the table is printed.
The sample execution goes like:
This way of having nested blocks can go any number deep; though,
management of deep level loops or conditional checks can be tricky if code
is not managed cleanly.
Challenge
As a practice exercise, try to produce following output by using nested
loops only:
***
*****
*******
*********
***********
*************
***************
*****************
***************
*************
***********
*********
*******
*****
***
*
H ave you ever heard of terms like Technical Debt and Big Ball of Mud? If
you are familiar with such terms, you can well understand the
importance of the topics we are going to cover in this chapter. For those of
us who are unfamiliar with these: these terms refer to a lot of code that has
been built over an extended period, maybe years, spending a lot of money,
but the code is essentially painful to handle – its management, bug fixing,
reusing in other projects etc., are more painful than pleasure.
You can write a whole application by using simple variables, operators,
conditional blocks, and loops only – however, ease of code management,
bug-fixes and features’ extensions can equally become increasingly
complicated as the size of source code grows; and any additions or
alterations in one part of code can cause side-effects in other non-related
parts. To better manage code, it should be easily readable by humans and
well organized, and to remain easily portable to other hardware or software
stacks, its majority should not be tightly dependent upon underlying
hardware or software stack. Therefore, this chapter can be summarily said
to be related with language provided facilities for better code management,
reusability, and organization.
Functions
Functions are the fundamental tools for a piece of code reusability
within an application.
Wherein, a function in C++ can optionally take inputs, and can optionally
return a value – functions are just like math functions:
This sample math function illustrates the idea that by changing the input
values x and z, the assigned value in y is changed whereas the procedure to
compute the value remains the same.
In C++, function takes following general form:
<return-type> function_name(
input-type-1 input_1,
input-type-2 input_2,
input-type-3 input_3) {
…function-body…
return some_value;
}
Rules for naming a function are just like naming the variables and
constants i.e., it can only start with a letter or an underscore and can only
contain letters, underscores, and numbers.
For invoking or calling a function, it can be done by using its name and
passing in the input parameters as required:
Now, whenever positive value is given as input to the function, the first
return statement is executed to return the value as it is, and when a negative
value is given as parameter the value is sign inverted and returned, thus:
Both the variables get the same value i.e., 5 . On executing the return
statement, further execution inside the function body is terminated and the
control is returned from where the function was invoked.
Since C++11, auto has been allowed to be used as a return datatype – it
instructs the compiler to infer the return datatype from the return statement
inside function’s body. Additionally, since C++14, decltype(auto) has also
been allowed – also for deducing the datatype of returned value but by
preserving the constant or reference nature of value. However, to remain
restricted to our purpose, let us forego these topics for the time being, as
many of our development environments in microcontroller worlds with very
low-powered devices do not fully support these features.
While working with codebases having both C and C++ source code
files, additional linkage specific prefixed extern keyword is commonly used
in the form, like:
extern "C" int absolute_value (int input) {
#include <iostream>
}
int main() {
int value;
while (1) {
}
return 0;
}
The main loop is quite self-explanatory – it keeps taking inputs from
user as long as zero or negative value is not provided, and for each value, its
factorial is computed and displayed. The real magic happens in recursive
call – although, its usability is limited as it can crash the application when
given number is large enough.
Its sample working is as displayed:
In such case, the variable holds its content between multiple invocations
of the function, and its initialization value is assigned only the first-time
function is invoked.
Functions with default parameters
A function can have one or more of its input parameters take default
values when no values are provided against these parameters while
invoking the function. This feature proves powerful in simplifying function
calls without specifying all the parameters.
The rule to remember is to start making parameters default from the last
parameter, as in the following example last two parameters are made to
have default values:
int number2,
int number3 = 5,
int number4 = 6) {
In the first sample use with passing in of two arguments – it sets the first
two parameters to specified values i.e., 1 and 2 respectively, whereas the
third and fourth parameters get their default values given in the function
definition. The second sample uses three arguments thereby setting the third
parameter value to override its default value; as the fourth argument is not
given, this lets the fourth parameter set to its default value. In the last
sample use case, four arguments are provided, therefore, all the parameters
will use the provided values and defaults are completely overridden.
It is clearly a beneficial feature, though the important thing to remember
here is reemphasized: the default values are given to parameters starting
from the last parameter. This can simply be said as, if one parameter is
given a default value then all the parameters coming after it must also be
given default values – if first parameter is given default value then all the
parameters should be given defaults, thereby, the function can be called
without providing any arguments at all, thereby using all the default values
of its parameters, like:
int my_special_function(int number1 = 1,
int number2 = 2,
int number3 = 3,
int number4 = 4) {
void function_name() {
…function-body…
void function_name(void) {
…function-body…
function_name();
Since types are not predefined for variable list of arguments, C++ only
provides type checking for arguments provided against explicitly declared
parameters. Whereas, for variable list, datatypes of arguments are promoted
to larger ones while passing in i.e., arguments of type char are passed in by
converting to int and float by converting to double . Therefore, we can use
such functions in cases when we need greater generalization in terms of
number and types of arguments.
Standard library stdarg.h provides functionality for dealing with variable
list of arguments inside the function – including: va_start , va_end , va_list ,
va_arg and va_copy . Let us see an example to make things clear:
#include <iostream>
#include <stdarg.h>
int sum_all(int total_args, ...) {
va_list args_list;
va_start(args_list, total_args);
int sum = 0;
for(int i = 0; i < total_args; i++) {
va_end(args_list);
return sum;
int main() {
int sum;
sum = sum_all(5, 1, 2, 3, 4, 5);
return 0;
Figure 29. Compiling and running the variable arguments list example
#include <iostream>
int main() {
int value;
while (1) {
return 0;
}
int factorial(int number) {
}
Here, the function prototype is provided before the main() function,
simply by:
This tells the compiler that we have a function named “factorial” that
takes a single integer input and returns an integer as a result. Thereby, we
can simply use it inside the main function. Later, when the compiler reaches
the end of compilation unit, it also finds the definition of the function – the
definition can be inside another compilation unit as well as it is required
during linking process, otherwise the linker can error out saying that it
could not find the definition of something used in code.
As a note: This feature is particularly important in the sense that it helps
in creating libraries
– whereby, declarations are separated in header files so that they can be
included in multiple compilation units (“ .cpp ” or other code files) and
definitions are segregated in other compilation units or prebuilt binaries.
Function overloading
Overloading of functions refers to specifying more than one function
with same name within the same scope – thus, same named functions being
the overloads of each other. Overloaded functions allow using same name
with different semantic meanings that depend on the types and number of
input arguments. To clarify, let us see following example:
#include <iostream>
int main() {
send(longlongNumber);
send(floatNumber);
send(doubleNumber);
return 0;
}
void send(long long number) {
std::cout << "Sending LONG LONG " << number << std::endl;
}
The output shows:
Since we are using the same named functions for sending values,
however, the output clearly indicates that distinct functions are called
behind the scenes depending upon supplied type of argument – selected
based on function parameters.
Inline functions
Another quite common keyword inline can be used with function
definition:
…function-body…
This tells the compiler to copy the code within the function’s body and
paste it at all the places where the function is called – although, some
compilers can opt to not fully copy the code for performance or other
reasons and keep calling the in-lined function in conventional way.
double *double_pointer;
Here we have declared three pointers that can point to integer datatype,
two are declared to hold addresses of floats and one pointer that is intended
to hold address to anything of double datatype.
Getting addresses
& Operator – in addition to providing Boolean bitwise-AND logic
operation, is also used to get memory addresses of variables and objects just
by prefixing their names. Let us say we need to assign address of an integer
to a pointer:
int my_integer = 5;
In the second line of code, & operator gets the address of variable then
it is assigned to int_pointer . This same technique can be used on other types
of data, though the type of addressed variable must match with the type of
pointer.
The same can be applied to access an array element’s address:
Despite pointing different elements, the size of the pointer does not
depend on the size of the pointed data – that is important.
Another way of getting addresses is during object creation using new
keyword, as new creates an object and returns its pointer:
#include <iostream>
struct MyStructure {
int x, y, z;
};
int main() {
int int_value;
float float_value;
#include <iostream>
int main() {
*int_ptr = 20;
std::cout << "Now, pointed value = " << *int_ptr << std::endl;
std::cout << "and, variable value = " << int_value << std::endl;
return 0;
Simply prefixing a pointer with * gives access to pointed value that can
be read and updated, the output makes it further clear:
struct MyStructure {
int x, y, z;
};
int main() {
my_structure.x = 10;
my_structure.y = 20;
my_structure.z = 30;
<< std::endl
<< " x = " << my_structure.x << std::endl
<< std::endl;
<< std::endl
<< std::endl;
struct_ptr->x = 100;
struct_ptr->y = 200;
struct_ptr->z = 300;
std::cout << "Members accessed through pointer: "
<< std::endl
<< std::endl;
<< std::endl
<< std::endl;
return 0;
Its output:
Figure 33. Compiling and executing object elements' dereferencing
Pointers to functions
Anything having an address in the memory can be pointed to by the
pointers, even functions. Till now we have seen pointers pointing to data
elements and that seems quite simple by now; however, when we point to
functions, pointer notation gets quite complicated, and takes the following
general form:
Let us say we need to point to functions that all take two integers as
input parameters and return integer, their pointer takes following form:
#include <iostream>
return a + b;
return a - b;
return a * b;
int main() {
std::cout << "20 + 10 = " << op(20, 10, add) << std::endl;
std::cout << "20 - 10 = " << op(20, 10, subtract) << std::endl;
std::cout << "20 x 10 = " << op(20, 10, multiply) << std::endl;
std::cout << "20 / 10 = " << op(20, 10, divide) << std::endl;
return 0;
As the pointer only stores its starting address, therefore there must be
something that indicates the end of string – and in this case it is null
termination i.e., zero value at the end of the text string. In the background,
this creates an array of char datatype, and each consecutive byte holds the
ASCII value of consecutive bytes:
0 1 2 3 4 5 6 7 8 9
‘a’ ‘ ‘ ‘m’ ‘e’ ‘s’ ‘s’ ‘a’ ‘g’ ‘e’ 0
#include <iostream>
int main() {
int_ptr += 1;
return 0;
#include <iostream>
int main() {
// Double's bytes:
}
std::cout << std::endl;
return 0;
One thing to note here is that the value assigned to integer variable is
printed in reverse order, this is simply because the application is built and
executed on little-endian machine that stores the least significant byte at the
lowest address.
Therefore, we can easily access the byte-level data and additionally can
change each byte. Thus, usage of pointers should always be done
responsibly and with specific purposes.
Classes
In C++, classes are the differentiating factor when compared with C
language – although they can be regarded as an improvement over C
structures, allowing combining data alongside operations within a single
packing as members, while allowing defining accessibility to its members.
You might have heard it before that during earlier days, C++ was
initially named as
“C with Classes,” – denoting the defining feature over C language.
In essence, classes are not simply better structures, they represent a
conceptual improvement that allows a framework of looking at an
application as a collection of objects sitting around in memory, maintaining
their states, and acting on inputs whenever required – thus giving the
famous term: Object Oriented Programming.
A class is simply defined with keyword class , like:
class <class-name> {
};
Rules for naming a class are the same as we have seen for variables and
functions i.e., they can only start with an underscore “_” or an alphabet, and
can only contain underscores, alphabets, and digits. You might find many
conventions used in naming classes, some prefix class names with letter
“C,” and others use simple representative names beginning with capital
letters.
Next comes the accessibility of class members i.e., variables, constants,
and functions. For this, three levels of accessibility are defined, namely:
private , public and protected . Whenever no accessibility is stated, it is private
by default. The accessibilities can be mixed and matched according to
requirements:
class MyClass {
public:
public:
protected:
};
In short, public members are accessible from outside the class, if you
define an integer in the public section, it can be accessed from the outside.
Whereas private members are only accessible from within the class i.e.,
these members are private, and no outside code can access these members.
Lastly, protected members are not directly accessible from outside the class
however when the class is having its child classes, the children can access
the protected members.
Let us create a class and see this in action:
#include <iostream>
class ComplexNumber {
private:
public:
real = r;
complex = c;
void print() {
};
int main() {
cn2.set(5, 8);
cn1.print();
cn2.print();
return 0;
When objects are created, each object gets its own set of variables i.e.,
space is only allocated to hold a separate set of variables not the
functions – functions are only defined once and operate on individual
objects.
Constructors and destructors
A constructor is a special function that has the same name as the class
itself and does not return anything – not even a void . Their aim is to
construct or initialize class members during object creation; therefore, they
are only called during object creation. A class can have many constructors,
each taking different sets of inputs thus allowing distinctive styles of
initialization. In contrast, destructors work opposite to constructors and
work when an object is being destroyed. They also take the same name as
the class but prefixed with a tilde “ ~ ” symbol.
Just to stay focused on our learning purpose, we will not go too deep
into nuances of construction and destruction. To clarify, let us take
following example:
#include <iostream>
class ComplexNumber {
private:
public:
ComplexNumber(int r, int c) {
real = r;
complex = c;
void print() {
~ComplexNumber() {
};
int main() {
cn2.print();
return 0;
}
Its output:
Static functions
Classes support two kinds of functions – member functions and static
functions. When any function is created normally inside a class, it is
considered as its member function that is intended to be invoked through a
class object as we have seen in the previous examples. However, when a
function is marked as static , then it is not intended to be invoked through
class objects:
class MyClass {
private:
int var2;
public:
void func2() {
};
int main() {
MyClass::func1();
MyClass obj;
obj.func2();
return 0;
class MyClass {
private:
MyClass() {
public:
if (instance == nullptr)
};
As the constructor is private, thereby objects of the class cannot be
created from outside code. Thus, we only have get_instance() which is static
member of the class therefore has access to invoking its constructor. On the
first call to it, it creates the class object and stores its pointer internally,
thereafter, any subsequent calls to it return the already created object
pointer and more objects are not created.
class ComplexNumber {
private:
public:
this->real = real;
this->complex = complex;
void print() {
std::cout << this->real << " + "
<< std::endl;
};
As we can see in the constructor, the input parameters are named exactly
as member variables; therefore, to distinguish between input parameters and
the member variables it uses this pointer to refer to the current instance or
object of the class it is being invoked on; same holds true for other member
functions of the class.
Inheritance
Classes can inherit from other classes, the one which inherits is called
child class and the one from which it inherits is called parent class – this in
fact is a powerful thing as many complex things can be modelled by
breaking them into manageable pieces. Let us take following example for
making many concepts clear:
#include <iostream>
class Object {
private:
int x, y;
protected:
x = x_value;
y = y_value;
};
private:
int w, h;
public:
w = width;
h = height;
void draw() {
};
int main() {
b1.draw();
b2.draw();
return 0;
Here we have two classes: Object as parent and Box as its child. The
relationship between classes is given by:
Runtime Polymorphism
Let us consider two features in combination with each other:
1. Any pointer of a base class type can also be used to point to its
children.
2. Any function from base class can be overridden in its children.
#include <iostream>
class Object {
public:
virtual void draw() {
};
public:
};
int main() {
ptr1->draw();
ptr2->draw();
return 0;
namespace <namespace-name> {
}
Rules for naming the namespaces are the same as we have used for
classes, functions, and variables – starting with letter or an underscore and
containing only letters, underscores, and numbers. Though they should
indicate specific purposes. Therefore, any name that comes inside the
namespace is accessed through scope resolution operator i.e., ::
Till now we have used std::cout to print messages on the console, here std
is the namespace in which cout is defined, same goes for std::endl .
Although we can use any name for naming the namespaces, C++
standard libraries use std as their namespace.
A namespace can also have other nested namespaces:
namespace ns1 {
namespace ns2 {
#include <iostream>
int main() {
}
This clearly simplifies our use of anything inside the namespace. Now
for using the namespaces:
#include <iostream>
namespace ns1 {
namespace ns2 {
void print_hello_1() {
void print_hello_2() {
cout << "Hello World 2" << endl;
int main() {
ns1::ns2::print_hello_1();
ns1::print_hello_2();
As we have defined print_hello_1 inside the namespace ns2 and that itself
is inside the namespace ns1 , therefore, to access it we have used scope
resolution with ns1::ns2 . But in the case of print_hello_2 , we have defined it
inside the namespace ns1 itself, therefore it is called by using
ns1::print_hello_2() only.
Type aliasing
For improving code readability and thereby enhancing its quality and
maintainability as a result, C++ provides ways to give datatypes new names
i.e., aliases. Wherein, using aliases can sometimes tremendously improve
code readability. C and C++ support using the typedef keyword and it is still
very commonly used despite new C++ standards allowing using keyword
for the same purpose.
typedef – keyword
Keyword typedef has quite simple format:
return a + b;
return a - b;
return a / b;
int main() {
std::cout << "20 + 10 = " << op(20, 10, add) << std::endl;
std::cout << "20 - 10 = " << op(20, 10, subtract) << std::endl;
std::cout << "20 x 10 = " << op(20, 10, multiply) << std::endl;
std::cout << "20 / 10 = " << op(20, 10, divide) << std::endl;
return 0;
struct circle {
float x_position;
float y_position;
float radius;
};
Wherein, older C-style always uses struct keyword with the name of
structure even while defining the objects. This can be simplified by:
float x_position;
float y_position;
float radius;
} new_name;
Thereafter, the new_name can be used as a type that refers to the
structure.
using – keyword
In addition to using the “ using ” keyword with namespaces, C++ has
extended its use for type aliasing as well, though the purpose is the same. It
takes following general form:
Without going into the details, let us rewrite our previous example with
newer keyword:
#include <iostream>
return a + b;
return a - b;
}
return a * b;
return a / b;
int main() {
std::cout << "20 + 10 = " << op(20, 10, add) << std::endl;
std::cout << "20 - 10 = " << op(20, 10, subtract) << std::endl;
std::cout << "20 x 10 = " << op(20, 10, multiply) << std::endl;
std::cout << "20 / 10 = " << op(20, 10, divide) << std::endl;
return 0;
This simple and quite familiar example by now, utilizes“ using ” to create
type aliases for int as Number , and for the function pointer as FunctionPtr – it
also seems a bit more intuitive than previously discussed “ typedef ”
keyword.
A note on compiler directives and switches
Till this point, we have used the compiler as a Blackbox for just creating
executables from source files. Though compilers themselves are software
applications and provide various facilities that help in writing and
organizing code better, some compilers can even provide additional features
in addition to standards for certain use-cases. Therefore, to understand a
compiler’s nuances its documentation must be consulted. However, in this
section, we will look at the things that compilers do with the code during
distinct phases of compilation.
#include
#include <file-name>
And
#include “file-name”
The difference however is that the first one looks at installed compiler
libraries to search for specified file then in other locations if it does not find
it there; however, the second one looks for the specified file in current path
and then if it is not found the compiler looks for it in standard libraries path.
In both the cases, when the file with specified name is found, its content is
copied and pasted at the location where #include is used.
To demonstrate this, let us say we have comma separated values in a file
that we need to use as an array’s elements. Let us place following values as
example in values.csv file, extension does not matter here as it is just the part
of file’s name:
1, 2, 3, 4, 5, 6, 7, 8, 9
Thereafter, in our “ .cpp ” file:
#include <iostream>
int main() {
int my_array[] = {
#include "values.csv"
};
}
std::cout << std::endl;
return 0;
Here we have not provided the values directly but have used #include to
get the values as content from another file; though for computing the
number of values, we are using sizeof operator – thereby, dividing the total
number of bytes taken by the array with number of bytes taken by each
element gives total number of elements; that’s what we are using to iterate
over each element. Hence, the output shows:
For providing additional search paths to compiler so that it can look for
“included files” in places other than current path or standard libraires’
path, Command-line switches are used. To get exact details on
command-line switches, try using g++ --help or see your compiler’s
documentation for complete details.
Macros
Macros provide a way for in-code replacements i.e., replacing one
symbol with another piece of code before compilation takes place for code
generation. Their generic format is:
REPLACEMENT-LINE-2 \
REPLACEMENT-LINE-3 \
REPLACEMENT-LINE-4
#include <iostream>
#define V1 20
#define V2 10
#define op(oper,value1,value2) function_##oper(value1,value2)
int main() {
cout << V1 << " + " << V2 << " = " << op(add, V1, V2) << endl;
cout << V1 << " - " << V2 << " = " << op(sub, V1, V2) << endl;
cout << V1 << " x " << V2 << " = " << op(mul, V1, V2) << endl;
cout << V1 << " / " << V2 << " = " << op(div, V1, V2) << endl;
return 0;
// code
#else
// alternate code
#endif
#ifdef is shorter form for #if defined(SYMBOL) and #ifndef is shorter form for
#if !defined(SYMBOL) – working opposite to each other. Their most significant
use is in conditional compilation thereby allowing certain pieces of code to
be excluded from compilation when compiling for certain targets or
replacing certain pieces of code for certain targets – for example, GNU
Linux and Windows have different API calls for using sockets, therefore for
making the code cross-platform compatible we can replace API calls in
relevant pieces of code – thereby, making the codebase relevant for multiple
platforms.
Another quite common use is preventing multiple compilations of same
piece of code – especially, we know that header files are simply copied and
pasted during compilation. Therefore, when one header file internally
includes another header file and we use both in our compilation unit, we
can have multiple definition issues. So, to skip already included part, we
can guard using compiler directives:
#ifndef UNIQUE_SYMBOL_MAYBE_FILE_NAME_WITH_PATH
#define UNIQUE_SYMBOL_MAYBE_FILE_NAME_WITH_PATH
#endif
#ifndef MY_LIBRARY_H
#define MY_LIBRARY_H
int get_number_from_user();
void print_table(int);
#endif
#include <iostream>
using namespace std;
int get_number_from_user() {
int value;
return value;
void print_table(int t) {
cout << t << " x " << i << " = " << t * i << endl;
#include "my_library.h"
int main() {
int table;
do {
table = get_number_from_user();
print_table(table);
} while (1);
main:
my_library:
clean:
rm main.o
rm my_library.o
rm tables
Note that for building the whole project, we have the default target “ all ”
that depends on “ main ” and “ my_library ” targets in which we are using -c
switch with g++ to tell it to only produce object files. After building of
these dependencies, “ all ” executes that finally connects the generated
object files together into creating the executable.
O ther than the coding part that we are covering in detail in this book, it
ultimately relates to interacting with the physical outside world outside
the microcontroller. When it comes to interacting with the physical world
through devices of the Internet of Things, understanding basic electronics
becomes more important in the sense that frequent updates at this level are
costly as they normally involve hardware parts replacement, instead of just
writing and shipping software updates. If you are coming from an
electronics background, please skip this chapter, as it would not offer much
for someone with strong background knowledge in this field.
Electronics as well as Feedback and Controls are vast subjects and
numerous books, and other material are available if you are seeking to build
an in-depth understanding; a single chapter of a concise book can never
suffice in providing all the aspects of this knowledge domain. However,
salient recurring and common features are being summarized to start with
using IoT as is the theme here.
Before we proceed further, it would be nicer to build understanding by
abstracting the devices as just providing an interface and functionality.
Abstraction is the key here – by abstracting away implementation details of
underlying hardware while understanding basic input and output
characteristics plays considerable role whenever employment of knowledge
is concerned. Here we will look at basic electronic and control devices that
are commonly required in implementing recurring scenarios – in an abstract
manner.
Power Supplies
Deployed devices always require power sources for working; whether it
be from solar energy, wind turbines, fuel powered generators, batteries, or
main supply lines, the power requires conversion from higher potential (say
110/220-volt AC) to required voltages (e.g., 5-to-9-volt DC). After
conversion to required DC voltage, voltage regulation to keep fluctuations
away from the sensitive equipment is also a requirement. Let us say, if the
equipment is powered from batteries, the battery voltages change depending
upon their held charges and decrease during discharge; therefore, to keep
voltages in certain range, regulation is required.
Just as a reminder from school days: AC is Alternating Current which
keeps changing polarity in cycles i.e., going from negative to positive
potential and reverse; and DC stands for the
Direct Current which keeps polarities of supply wires same i.e., positive
wire will always stay positive and negative will always stay negative.
Transformation of available AC power source requires stepping-it-down
through a transformer or other circuits, to bring high potential to low
voltage levels that makes it manageable for following circuit. Stepped-
down voltages through a transformer are then passed through bridge
rectifier circuit to convert it into DC, followed by conversion to DC, filters
are added to the circuit to reduce ripples in the voltage before supplying
power to final circuit. The power is also regulated through voltage regulator
to keep supplying voltage to driven electronics within specified range.
This is simply said than done, therefore, without trying to reinvent the
wheels, we can simply use readily available power supplies in the market
because they are commonly and readily available in the form of packages;
and can provide amicable results. Just purchase one according to
requirements, otherwise, if very peculiar requirements need to be served
and exact rating is not available, get one with closest required rating and
use a Buck (voltage reducing devices) or
a Boost Converter (voltage increasing devices) – also commonly available.
Unless there are some specific requirements, generally required power
supplies are commonly available. Commonly available power supplies,
generally serve most if not all purposes at cheaper costs when
production volumes of specially built supplies are low.
The main part is coming up with power requirements first, by looking at
maximum power requirements of target device circuitry – including
voltages and current; then finding the closest in upward side of available
power supplies that can provide at least 30 percent extra power (voltages
multiplied with current) as safety factor and more power can be
considered for expansion of circuitry at later stages.
Resistors
Resistors are one of fundamental building blocks in electrical circuits
and draw their name from their intended purpose of providing resistance to
flow of electrons. The measurement of the resistance they provide in the
flow of electric current is mentioned as their rated values represented as
measurement unit of Ohm (Ω) and depicted commonly with color bands on
each resistor. Their common usage in electronics is current limiting, voltage
division, biasing of active elements like transistors, and power dissipation
by acting as pure loads in power circuits – for being just passive devices;
however, resistors are a bit complex in employment. Commonly schematic
diagrams depict resistors with two-legged boxes or with a zig-zag pattern
having terminals at both the ends.
For example, if VR1 and VR2 are required to be the same i.e., dividing
the voltages equally, then both the resistors are selected with equal rated
values. Two resistors having 1k Ω resistance can divide the total voltage
of 9V into two equal values.
Diodes come in different shapes and sizes and are often marked with a
band near one of its terminals; their schematic symbol is denoted as an
arrow that depicts the allowed direction of current flow.
Shown in figure is a typical relay; when the coil is not energized and
current is not flowing through its wire, its terminals C and 2 are connected;
and when the coil is energized by passing current, the current makes the coil
a magnet, therefore, the magnet pulls movable part of the terminal C
towards itself, this connects terminal C with terminal 1. This arrangement is
common in controlling power electric devices.
Their most common use you might find in IoT controls and related
circuits might include them as filters having two or more capacitors
connected in parallel with microcontrollers or other devices to filter out any
noise components at different frequency notches. Other places where
capacitors are found include clock generation circuits and timing devices
which use their charging and discharging properties to generate timing
signals.
Transistors are normally denoted by letter “Q” and their three terminals
are:
C (Collector), B (Base) and E (Emitter); to understand in simple words for
this NPN transistor: whenever base voltages are higher than emitter voltage,
current starts flowing from base to emitter, this phenomena switches on the
transistor when collector voltages are much higher than the base voltages,
therefore, current starts flowing from collector to emitter.
In this circuit, a relay switch is required to be switched on and off with
signals from microcontroller (MC) and signal voltages typically range from
0 to 5 volts with 20 milli-amperes of maximum current. After passing
through a resistor of 220 Ω, the signal is fed into the base of transistor.
When signal is off i.e., 0 volt, it makes transistor stop current flow from
collector to emitter thereby it switches off the relay; and when
microcontroller signals to switch on the relay by providing 3.3 to 5 volts
signal, it makes the transistor switch on, resultantly current starts flowing
through the relay coil and the transistor, this in turn allows the light to
switch on by providing it with 110 volts AC.
The diode we are using in this circuit provides protection to the
transistor. Whenever the relay is switched off from on state, this diode tries
to dissipate backwards generated electromagnetic force i.e., high voltage,
produced due to electromagnetic effects to resist changes in current flow
through the coil. In this configuration the diode is commonly called a
Flywheel diode.
When the devices that are required to be controlled with heavy currents,
and you cannot find transistors to support that much current, do not
bother by keeping dragging your solution in search of more and more
powerful transistors, you probably would not find very powerful
transistors. Instead, look for other solutions like using transistors to
operate FETs or MOSFETs that can handle heavier currents, and then
using them to operate required devices.
You can also use a transistor to operate a small relay and using that relay
you can control a bigger circuit.
This simple voltage divider can help in reading angular changes for
pivot door. As the pivot angle changes, it changes resistance value, which in
turn changes voltages at analog input of A2D converter. Though, more
sophisticated methods with precise sensors are also available.
Actuators
After taking inputs from sensors, analyzing situation, and taking
decisions; execution of decisions is done by utilizing actuators, for
example, running a DC motor using following circuit:
Figure 52. Arduino's industrial grade Nicla board (Source: Arduino site)
There are two major versions i.e., 1 and 2, available for Arduino IDE as
of now. IDE major version 1 is considered legacy one – however, we
will be using it in our examples. Though you might use the newer
version, but it will have certain differences.
In this book we are using the legacy Arduino IDE version 1.* , you can
use the newer
version 2.* of the IDE; however, there might be certain differences.
Although, both the versions are available online.
Initially the IDE gives sample code that only has two blank functions
named setup() and loop() . The code files that the Arduino IDE associates its
code with have extension“ .ino ” and are called “Sketches” in Arduino lingo.
These two functions i.e., setup() and loop() are the only required functions in
a sketch. If you need to see that how these functions get linked with
backend code “File” menu and select “Preferences”:
Here we can see which files are being compiled and where they are
located. Let us open the indicated “ main.cpp ” file, it contains the main()
function in our case and looks somewhat like the following snippet:
int main(void)
setup();
for (;;) {
loop();
return 0;
Using the same method try reading the Arduino libraries – you would
surely come across many new things; probably this is the best way to
understand what Arduino is doing under the covers to make
development experience easy for everyone.
Libraries
Arduino ecosystem is quite huge – many libraries you can find
distributed through Arduino IDE’s Library Manager, and many are
available through other open-source projects. To install libraries, select
“Manage Libraries…” option under “Tools” menu:
Figure 59. Installing Windows Remote Arduino Experience from Microsoft Store
Now, before running it let us load the attached Arduino board with
specific firmware – and it is given with Arduino IDE as examples, just open
the Arduino IDE, open the File menu, and select the example as shown:
In testing the hardware features even before writing code, such tools can
prove quite helpful in developing projects.
Default library functions
Before commencing with the library functions, let us first see an
Arduino board:
Figure 66. Arduino Mega 2550 rev3 Board (Picture from Arduino site)
On the board, we can see that many pins are marked with numbers like
0, 1, 2, …, these are the digital pin numbers that need to be used while
developing applications. Some pins are denoted with numbers prefixed by
“A” like A0, A1, A2, …, these are analog input pins as written above them
as well, any analogue input connected with them is digitized by the internal
Analogue to Digital Converter though the input must be in specified range
and reference voltages should be set in accordance with board
documentation. Additionally, you can find some pins marked as PWM or
their digital pin numbers prefixed with tilde “~” symbol, these are used for
generating Pulse-width Modulated outputs i.e., for controlling motor speeds
or light intensities etc.
When a serial port is being used for communicating with PC’s serial
monitor, its corresponding digital pins should not be used for any other
purpose; otherwise, communication problems can become a headache
while tracing.
Library functions
Arduino library has been growing since its inception; therefore, it is
advisable to visit official Arduino library documentation on its site for full
range of documentation. However, here we will cover certain library
functions to get started with Arduino. Following list covers very typically
used functions:
3. The folder can contain other code files like “ .c ” and “ .cpp ”;
these are automatically compiled as part of project build.
4. The sketch file should contain two functions i.e., “ setup() ” and
“ loop() ” – the first one is only executed once and the second one
is executed inside an infinite loop.
void setup() {
pinMode(13, OUTPUT);
void loop() {
digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
delay(1000);
#ifndef _BLINK_H_
#define _BLINK_H_
void set_pin_mode();
void blink_once();
#endif
#include <Arduino.h>
void set_pin_mode() {
pinMode(13, OUTPUT);
}
void blink_once() {
digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
delay(1000);
#include "Blink.h"
void setup() {
set_pin_mode();
void loop() {
blink_once();
Only including Blink.h is required in the sketch file, rest the build system
will do. At the end of this, the folder now contains our sketch file, Blink.h
and Blink.cpp :
Once new source files are placed in the folder along-with the sketch file,
closing and reopening of sketch makes the newly added files discovered
by the IDE if they are not discovered earlier.
After creating the project, board selection and uploading to the target are
performed just as normally done – there is no special changes in this
case.
Chapter 7:
Example Projects
A rduino projects – or
generally boil down
any other hardware projects for that matter,
to just taking inputs, processing them, and
producing results as outputs. Even though, development environment for
Arduino – in terms of both, the hardware kits and application development
environment is meant to be simple; though by “simple” many people
consider its simplicity only in terms of just creating simple projects –
though by being simple, it primarily relates with the ease with which
projects can be developed, as for the most part it very much allows creating
complex projects. Afterall, you are only limited by the limits of the
microcontroller chip you are using, not the application development
environment.
The majority of modern microcontrollers available these days are quite
capable of executing millions of instructions per second – even the simpler
ones. Once we consider this aspect and compare their capabilities with the
majority of personal computers in use just a few years back, we can easily
appreciate the computer power that these microcontrollers have. However,
power consumption remains a considerable factor in many applications,
whereby the devices are battery powered and are required to operate in
remote locations for longer periods of time – that’s when power
conservation must be taken seriously. Therefore, modern microcontrollers
allow several power saving modes. Whereas such requirements are for
special purposes and the default programming style of Arduino
environment i.e., running in a single infinite loop does not stop from
prototyping the solutions. That is why we will not be covering this aspect as
it varies with different microcontrollers. Therefore, in the next sections we
will focus on the development and interfacing aspects utilizing the default
environment – and that is also relevant for majority of development tasks.
Interfacing a keypad
Taking inputs from keypads is quite a common task in applications
where simple user interfaces are required.
Intel’s initial microcontrollers found remarkable success in being used
inside keyboards.
To save on the number of lines keys in keypads are arranged in rows and
columns. This scheme of connection saves lines by having only rows and
columns connected with the microcontroller instead of reserving single
input for each key. For example, 16 keys can be arranged in four rows and
four columns, thereby we only require eight microcontroller lines to read
the state of sixteen keys. For illustration:
This keypad is quite commonly available, and its rows and columns are
as indicated on the figure with R and C. For us it is enough to clarify the
concept of interfacing it; additionally, the same scheme can be used for
reading the key states in other keypads as well.
namespace cblk {
typedef void(*keypad_callback_t)(char);
class KeypadReader {
private:
char* keysStatus;
public:
KeypadReader(keypad_callback_t on_key_down,
keypad_callback_t on_key_up,
void update();
};
}
First a callback function pointer type is defined – the aim is to give two
function pointers to the constructor of the class which stores the pointers
inside private variables. The functions take a character as an input, and the
aim is to call one function when a key is pressed and the other one when the
key is released – both are passed in the character value of the pressed or
released key.
To keep the code generic enough, the constructor is also given total
number of rows and columns of the keypad and their connected pins, lastly
an array of face values of keys is also given so that the face values are
passed to callback functions when corresponding key is pressed or released.
In the end, the update function is the one which checks the key presses
whenever invoked.
The implementation goes in KeypadReader.cpp :
#include <Arduino.h>
#include "KeypadReader.h"
First for accessing the types in KeypadReader.h and Arduino library, we are
including them.
onKeyDown( \
__r++) { \
digitalWrite(rowPins[__r], HIGH); \
}
This macro helps in setting all the row pins in high state. In our reading
scheme, we are internally pulling up all the columns and setting each row to
low one by one to read keys.
!= val
= val
These are defined to help in checking whether the specified key press
state has changed or not.
cblk::KeypadReader::KeypadReader(
keypad_callback_t on_key_down,
keypad_callback_t on_key_up,
onKeyDown = on_key_down;
onKeyUp = on_key_up;
rows = num_rows;
cols = num_cols;
rowPins = row_pins;
colPins = col_pins;
keyMap = key_map;
pinMode(rowPins[r], OUTPUT);
pinMode(colPins[c], INPUT_PULLUP);
}
The constructor is simple, we are only initializing the values and initial
states of buffer to later track changes.
Now the final update part plays the key role of checking key presses and
releases.
void cblk::KeypadReader::update() {
SET_ALL_ROW_PINS_HIGH;
digitalWrite(rowPins[r], LOW);
delayMicroseconds(timePerRow_us);
if (IS_KEY_STATUS_CHANGED(r, c, val)) {
if (val == KEY_PRESSED_SYMBOL) {
INVOKE_KEY_DOWN_HANDLER(r, c);
} else {
INVOKE_KEY_UP_HANDLER(r, c);
}
SET_KEY_STATUS(r, c, val);
}
Simply, during each iteration only corresponding pin is pulled low and
after giving some settling time, we read state of each column and when any
key is found pressed or released, its corresponding callback function is
invoked.
As a final glue part – the sketch file only uses this functionality:
#include "KeypadReader.h"
// ------------------------
// Keypad Events
// ------------------------
Serial.print(key);
Serial.println('+');
Serial.println('-');
These are our two callback functions – one for being called when a key
is pressed and the other when the key is released. Both are quite simple;
they transmit the key’s face value over the serial port and append “+” or “-”
to indicate pressed or released state respectively.
// ------------------------
// ------------------------
};
KeypadReader keypad(on_key_press,
on_key_release,
rows, cols,
rowPins, colPins,
keyMap);
Now you can see that we have kept the code in class generic enough to
be used with other keypads as well. Here we are defining and giving the
number of rows, number of columns, connection pins, face values of keys
and callback functions to our class constructor.
void setup() {
Serial.begin(115200);
void loop() {
keypad.update();
With all the background logic in place, now leaves us to only setup
serial port for communication in setup function as our callbacks transmit
key presses and depresses over the serial port and the loop function only
invokes our class’s update method which in turn checks key presses and
depresses, and on specified state, calls the callback methods.
Upon uploading the solution to an Arduino device, we can receive the
key presses using any serial monitor or you can use the serial monitor
packaged with Arduino IDE.
While using the Serial Monitor, remember to use the same Serial Port
settings as the ones used in Arduino application – inside the setup()
function.
Cyclic task execution
A lot of microcontroller-based applications can be broken down into
separate tasks with each responsible for performing a certain set of
activities. For example, a simple application that reads two sensor values,
displays on attached display device, and transmits over the serial port – it
can be split into three tasks i.e., reading sensor values, updating the display,
and transmitting over the serial port. The tasks-based approach can make
the application design cleaner and extensible for example, extending the
application to also receive keypad inputs for changing the units in which the
values are being displayed on screen.
Another important improvement that tasks-based approach provides is
running individual tasks at different rates e.g., reading sensor values and
displaying them on screen after every
30 milliseconds can be configured for better real-time visibility, whereas
transmitting over the serial port can be configured to execute after every
second to reduce communication overheads. Although, this is just an
example to illustrate a use-case scenario, for every application use-case
decision of task execution frequency can be decided based on target
requirements.
The master-loop approach i.e., writing code in functions and calling
them in an indefinite loop makes it difficult to run individual tasks at
different rates; it becomes further complicated when task execution
frequencies are required to change dynamically. When code is indefinitely
executed in a master-loop, it simply executes as fast as the microcontroller
can execute instructions based on clock frequency; therefore, to slow down
the execution rates mostly delays are used. Though, within that delay the
microcontroller can execute many instructions.
Let us assume that our microcontroller can execute a million instructions
per second; this implies that a delay of one millisecond is enough time
for the microcontroller to execute
one thousand instructions.
The proper approach for tasks-based execution would be using a Real-
time Operating System that can preempt a running task to execute another
higher priority task which is ready to execute. Though, for our simpler
environment, especially when hard real-time requirements are not required
to be met, we can use Arduino library’s function millis() that returns the
number of milliseconds since starting the microcontroller and use it as our
timescale.
An important thing while breaking the tasks into distinct functions is
that they should not contain indefinitely running loops otherwise the
whole execution can stop as we are not using task preemption in this
case.
Let us start by declaring our class in CyclicTaskExecutor.h that keeps track
of execution of its associated task function:
namespace cblk {
typedef void(*func_ptr_t)();
class CyclicTaskExecutor {
private:
func_ptr_t funcPtr;
public:
CyclicTaskExecutor(func_ptr_t func_ptr,
};
It is a simple declaration of our class that stores the task function, its
interval on which to invoke the function and the last timestamp it was
invoked. In addition, it has a public method to change the interval and an
update method that takes in the value taken from millis() function and
decides whether to run the task or not.
The implementation goes like:
#include <Arduino.h>
#include "CyclicTaskExecutor.h"
cblk::CyclicTaskExecutor::CyclicTaskExecutor(
func_ptr_t func_ptr,
bool call_on_zero) {
funcPtr = func_ptr;
change_interval(calling_interval_ms, call_on_zero);
The constructor simply stores the function pointer internally and passes
the rest of the parameters by treating the construction itself as a change of
interval.
void cblk::CyclicTaskExecutor::change_interval(
callingInterval_ms = calling_interval_ms;
if (call_on_zero) {
}
lastCalledTime_ms = millis();
Here we can make sense of other constructor parameters i.e., the calling
interval is just stored into a private variable, and when the passed in
Boolean is true it means to invoke the function at the time the interval is
updated, otherwise it would be invoked periodically at the specified
interval.
} else {
lastCalledTime_ms = current_time_ms;
#include "CyclicTaskExecutor.h"
CyclicTaskExecutor* exe_1;
CyclicTaskExecutor* exe_2;
void task_1() {
Serial.prisnt(".");
}
void task_2() {
Serial.print("|");
We have defined the tasks and the pointers of executor class against
each task.
The tasks should not block execution with infinite loops otherwise the
whole application would halt.
void setup() {
Serial.begin(115200);
void loop() {
exe_1->update(currentTime_ms);
exe_2->update(currentTime_ms);
}
Thus, we can simply create the executor objects and update them inside
the main loop.
Output on Serial Monitor shows that task_1 is executed five times more
than task_2 :