Logical Expressions and Selection
Logical Expressions and Selection
There are int type variables to work with integers in C / C ++. However, operations on
integers is not the only tool in the developer's arsenal. One of the most important elements
of programming are logical operations, expressions and data types.
An int type variable can store arbitrary integers and operations with them are subject to the
laws of integer arithmetic. Boolean variables can store only one of the two values: true or
false, and operations with them are subject to the laws of Boolean algebra.
Boolean algebra is a clever name for some easily comprehensible things, such as "and",
"or", and "not" operations over statements that are either true or false. For example, let’s
consider the following logical expression: "it is dark in the hallway and there is a man walking
down it." It consists of two Boolean values (statements on the darkness and the man) and
one operator (the "and" conjunction). Its final value will also be a Boolean value, which can
be used, for example, as a signal to turn on the light in the hallway of the hotel.
In programming, variables that store logical values are called Boolean variables, and
operators, which perform actions on them, are called Boolean operators, or simply logical
operators.
Logical operations in C ++
In C ++, Boolean variables are of the bool type. The designation of the true value is done
with the help of the word “true”, while for the false value, the word “false” is used. Thus, if we
use the following to declare an integer variable:
int myValue = 42;
For logical variables we shall use:
bool tooDark = false;
#define MOTION_SENSOR_PIN 2
#define LED_LAMP_PIN 5
#define LIGHT_LEVEL_THRESHOLD 600
void setup()
pinMode(LIGHT_SENSOR_PIN, INPUT);
pinMode(MOTION_SENSOR_PIN, INPUT);
pinMode(LED_LAMP_PIN, OUTPUT);
void loop()
Let’s take a look at the loop and try to see what is happening here. Everything is clear with
the first line: we read the light value from the analog sensor with the help of the inbuilt
analogRead function and assign it to the variable named lightLevel.
Next, we declare the logical variable motionDetected, which is assigned the result of the
inbuilt digitalRead function once we’ve called it. The digitalRead function is similar in nature
to analogRead, but it may return only one of the two values, either true or false. This is a
function that returns a Boolean value. It is suitable for reading a variety of binary digital
sensors. In our example, we just used one of them: the pyroelectric sensor, which outputs 0
volts, when there is no movement within its radius, and 5 volts, when there is movement of a
warm object: a human, a cat or anyone else. The microcontroller assigns the false value to 0
V, and the true value to 5 V.
Thus, following the results of the execution of the second line, the motionDetected variable
will store either the true or the false value depending on whether or not any movement has
been detected in the hallway.
Next, we can see the definition of the tooDark Boolean variable, which is assigned the value
of the expression lightLevel <LIGHT_LEVEL_THRESHOLD. The “<” symbol in C ++, as you
might guess, is the "less than" operator. The operator makes one logical result out of the two
numeric operands. The value of the whole expression is considered true if what is written on
the left of the sign (in our case, it’s lightLevel) is less than what is written on the right of the
sign (LIGHT_LEVEL_THRESHOLD). This is quite logical and easily understandable, isn’t it?
Therefore, the tooDark variable will store the “true” value only if the value of the illumination
level of lightLevel, which was obtained previously, is less than 600. Otherwise, the variable
will store the “false” value.
Specific values, such as 600 in our case, are often obtained experimentally, depending on
the configuration of the sensor and the room where the device is.
Let’s move on. We can see how the Boolean lightningRequired variable is declared, which is
assigned the value of the expression tooDark && motionDetected. The “&&” symbol in C ++,
is the logical "and" operator. As you might guess, the whole expression is considered to be
true if and only if when the expressions that are both on the right and on the left of the
operator are true. Thus, the lightningRequired variable takes the “true” value if there is
human movement observed in the hallway and it’s dark at the same time. If at least one of
the conditions has not been satisfied, the result will be false. This is exactly what we need.
And finally, the last expression is the calling of the digitalWrite function for the Arduino pin, to
which the lamp is connected. We need to turn on the lamp if the lightningRequired variable
contains the “true” value and turn it off if it contains “false”. To do this, we use the ternary
conditional operator ? :, where as a condition we simply use the value of the
lightningRequired variable. Remember? The condition in the ternary operator is considered
to be satisfied if its value is not zero, and not satisfied if its value is zero.
In reality, there is no such concept of logical values for a processor. All that it knows is how
to operate with integers. Therefore, in C ++ there is an automatic type conversion. Where it
expects an “int”, and we use a “bool”, there is an automatic conversion happening: “true” is
converted into an integer 1, and “false” into an integer 0.
Thus Boolean expressions and variables serve as another convenient tool for programmers,
which allows them to write more clear and comprehensible programs.
Brevity of notation
If we recall the automatic conversion of variables of different types, and the fact that HIGH is
nothing but a macrodefinition of number 1, and LOW is a macrodefinition of number 0, the
last line in this example could be reduced to a concise statement:
digitalWrite(LED_LAMP_PIN, lightningRequired);
We have managed to do without the ternary operator, since the digitalWrite function will
anyway receive:
● 1: same as HIGH, if lightningRequired is true
● 0: same as LOW, if lightningRequired is false
Additionally, logical expressions are the most common expressions in C ++, to which general
rules of embedding apply. Therefore, the entire loop code in the example with eco-friendly
lighting could actually be written in one line:
void loop()
Yes, the code is more readable now, but it's just a demonstration of its possibilities. In reality,
it’s better to use a couple of intermediate variables:
void loop()
digitalWrite(LED_LAMP_PIN,
digitalRead(MOTION_SENSOR_PIN)
);
}
What if we actually want the light on: “if” conditional expression
Attentive readers may have noticed that the given sketch can hardly work in the real world. If
we turn on the light, when it is too dark and motion is detected, next time you run the loop,
the sensor will detect the light of our own lamp, so the program will think that there is already
enough light and will turn off the lamp. The process of constant switching on and off will
continue until the pyroelectric sensor detects motion. In the best-case scenario, the lamp will
be glowing, and at worst, it will be irritating us with its flickering. What shall we do?
The program can be put to sleep after the lamp turns on, say, for 30 seconds. It is enough
time for the guest of the hotel to walk down the hallway and it is not so wasteful in terms of
energy savings.
How to create a 30-second delay after the lamp is turned on but not after it’s turned off?
There is the “if” conditional expression in C ++ for this purpose. If we use it, the sketch will
be as follows:
#define LIGHT_SENSOR_PIN A0
#define MOTION_SENSOR_PIN 2
#define LED_LAMP_PIN 5
void setup()
pinMode(LIGHT_SENSOR_PIN, INPUT);
pinMode(MOTION_SENSOR_PIN, INPUT);
pinMode(LED_LAMP_PIN, OUTPUT);
void loop()
digitalWrite(LED_LAMP_PIN, HIGH);
digitalWrite(LED_LAMP_PIN, LOW);
}
The program starts in the same way, but in the loop there is a new compound expression,
denoted by words “if”, “else”, and with braces. Let’s try to understand what this all means.
Immediately after the “if” word, the C ++ compiler expects to see a logical expression in
parentheses, which in this case is called a condition. It has the same meaning as it does for
the ternary operator. If its value is not zero and true, we execute the code block that
immediately follows the condition in braces. In our case, if tooDark and motionDetected are
true, we execute the code block which is made up of two lines:
digitalWrite(LED_LAMP_PIN, HIGH);
If the condition was zero, or false, which is the same thing, the section of the code following
the condition is skipped and not executed at all. However, if this skipped code block is
followed by the “else” word, the code block in curly braces is executed, which comes after
this word. In this case, if either tooDark or motionDetected are false, the code block made up
of a single line is executed:
digitalWrite(LED_LAMP_PIN, LOW);
It should be noted that if the condition is satisfied, the next code block after “else” is not
performed and is skipped. Thus, in fact, the “if“ expression represents a simple and clear
statement in a programming language: "if something is X, you should do Y, otherwise you
should do Z."
Returning to our example, the code can be interpreted as follows: "if you want light, turn on
the lights and let the program go to sleep for 30 seconds, and if the light is not needed - turn
it off." Pretty simple and logical, isn’t it?
Using expressions that change the course of the program depending on some conditions is
called program branching, and blocks of code after the words “if” and “else” are called
branches.
{
bool tooDark = analogRead(LIGHT_SENSOR_PIN) < LIGHT_LEVEL_THRESHOLD;
digitalWrite(LED_LAMP_PIN, lightningRequired);
if (lightningRequired) {
This variation of the code does exactly the same thing as before. Only now the code is
organized differently. We will in any case pass on the instructions to the lamp bout whether it
should turn on or off, calling digitalWrite with the lightningRequired Boolean value. Moreover,
the program will fall asleep for 30 seconds only if we turn on the light, i.e. if lightningRequired
is true. If we have just turned off the light, sleep mode is not necessary: we need to skip the
delay and immediately go at the end of the loop function. Therefore, we did not write the
“else” branch at all.
Moreover, if the section of the code after “if” or “else” contains only one expression, we can
omit the curly brackets:
if (lightningRequired)
For example, let’s consider the sketch of a device that controls the opening of a refrigerator
door, a car door or something like that. We want the backlight activated when we open the
door, and if the door remains open for more than 20 seconds, we want to hear a warning
beep. At the same time, I want to be able to turn off the beep with a switch in case the
load/unload process is too slow – thus, it will not irritate us that much.
Suppose there is a permanent magnet mounted on the door, and there is a binary digital
magnetic field sensor on the frame. If the magnetic field is fixed, the door is closed.
Otherwise, the magnet moves too far away from the sensor, where there is no magnetic
field, so we can understand that the door is open. We shall also connect a backlight, a piezo
beeper (buzzer) and a Rocker switch to Arduino.
In this case, the code of the operating device will look like this:
#define DOOR_SENSOR_PIN 2
#define BUZZER_PIN 3
#define SWITCH_PIN 4
#define LAMP_PIN 5
#define BUZZ_TIMEOUT 20
void setup()
pinMode(DOOR_SENSOR_PIN, INPUT);
pinMode(SWITCH_PIN, INPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LAMP_PIN, OUTPUT);
void loop()
if (doorOpened) {
digitalWrite(LAMP_PIN, HIGH);
delay(BUZZ_TIMEOUT * 1000);
if (buzzEnabled) {
tone(BUZZER_PIN, BUZZ_FREQUENCY);
} else {
digitalWrite(LAMP_PIN, LOW);
noTone(BUZZER_PIN);
Let's look at what is happening in the loop. First, we define the doorOpened variable and
assign it the value of the expression! DigitalRead (DOOR_SENSOR_PIN). It has been
mentioned above that the door is considered open if there is no magnetic field, i.e.
digitalRead returns the “false” value to our sensor. The “!” symbol before the Boolean
expression in C ++ is the the logical "not" operator, or simply complementary operator. It
affects the value recorded after it by turning true into false and vice versa. That’s exactly
what we need in this case.
Then comes the “if” statement that checks whether the door is open. If so, the program starts
the execution of the code block that comes immediately after the condition. First thing first,
we need to turn on the backlight in this code block. Then the program can go to sleep for 20
seconds (20 ms × 1000).
Next, we need to turn on the beeper if it had not been intentionally turned off before. To do
this, we shall check the state of the switch, which is responsible for this. We shall declare the
buzzEnabled variable, which is assigned the logical value read from the switch.
Please, note that the buzzEnabled variable is declared inside the code block which directly
follows the “if” expression. We can and we should do this, because it is good practice to
declare variables closer to the section where they are first used.
Let us recall the concept of the scope of variables: variables are available for use only within
the block where they are declared. In our case, buzzEnabled can be used in expressions
within the block of the code following the “if” espression (doorOpened). However, the attempt
to access it from somewhere else (for example, from the code block of the “else” branch or
directly from the loop outside the ”if” branch) will lead to an error of program compilation.
And this is actually good, since we should not involve a variable that is required at very
moment in some other ongoing processes. This makes the program more clear and
comprehensible.
After the readings of the switch, there comes an embedded conditional expression. Its
essence is exactly the same as before: it depends on the condition whether or not the code
should be executed. The only difference is that it is located directly in the code block of
another “if” branch. It is common practice that occurs quite often in programming. In fact, in a
nested “if”, you can put more “if” expressions, as many as you want: in the C ++ language,
this number is unlimited.
Returning to the example, if the switch is set to "enabled", i.e. buzzEnabled contains the
“true” value, we shall turn on the beep. This is done by using the inbuilt tone function. It
takes two arguments: the number of Arduino’s pin, which has the piezo buzzer connected
and the beep frequency. In our case, we have chosen the frequency of 4000 Hz, i.e. 4 kHz.
Finally, if the door is closed, i.e. doorOpened contains the “false” value, we need to make
sure that the lights and the buzzer are turned off. This is done in the “else” branch of the first
“if” condition. You now know how to turn off the lamp. The noTone function, as you might
guess, disables the beep on the selected pin.
If we take into consideration the rules of embedding expressions and keeping them short, we
can simplify the loop code a bit. It might look like this:
void loop()
if (!digitalRead(DOOR_SENSOR_PIN)) {
digitalWrite(LAMP_PIN, HIGH);
delay(BUZZ_TIMEOUT * 1000);
if (digitalRead(SWITCH_PIN))
tone(BUZZER_PIN, BUZZ_FREQUENCY);
} else {
digitalWrite(LAMP_PIN, LOW);
noTone(BUZZER_PIN);
We have removed curly brackets for the internal “if” expression and embedded digitalRead
callings directly into conditional expressions.
Please, note how the correct use of indenting in code blocks makes it easy to understand to
which “if” expression refers our “else” expression. It also helps us understand what follows
what and under which conditions. Never write without indented lines despite the fact that the
compiler won’t care anyway. This makes the code not readable for other people and for you
as well:
void loop()
if (!digitalRead(DOOR_SENSOR_PIN)) {
digitalWrite(LAMP_PIN, HIGH);
delay(BUZZ_TIMEOUT * 1000);
if (digitalRead(SWITCH_PIN))
tone(BUZZER_PIN, BUZZ_FREQUENCY);
} else {
digitalWrite(LAMP_PIN, LOW);
noTone(BUZZER_PIN);
Button clicks
One of the typical tasks when creating devices on microcontrollers is to determine the time
of the switch click to be able to respond to it somehow. The problem is that the switch, just
like the sensor, can only tell whether it is now pressed or released. It cannot tell us that it
“has just been pressed”.
But this problem is easily solved with a small trick in the software of the project. Let's make a
primitive device with a lamp and a switch that would turn the lamp on and off in turn by
pressing the button: one click – the lamp is on, another click – it’s off:
#define BUTTON_PIN 2
#define LAMP_PIN 5
void setup()
pinMode(BUTTON_PIN, INPUT);
pinMode(LAMP_PIN, OUTPUT);
void loop()
lampState = !lampState;
delay(10);
}
wasButtonDown = isButtonDown;
digitalWrite(LAMP_PIN, lampState);
● lampState contains the “true” value, if the lamp should now be on, and the “false”
value if the lamp should be off.
● wasButtonDown stores the switch status at the time of the last run of the loop: if the
button has been pressed, the value is true.
Now let’s take a look at the loop itself. First, we need to read off the switch status into the
isButtonDown logical variable. If the switch is on, isButtonDown will contain the “true” value.
This is followed by a conditional “if” statement to check the conditions, the essence of which
is to understand whether the button has been pressed just now or a long time ago. To do
this, we shall use the wasButtonDown value.
Thus, the condition should be perceived as "the button is pressed now, but it wasn’t pressed
before”. This is the immediate condition of the button click.
If the condition of the click is satisfied, we need to act. Let’s change the lampState value vice
versa:
lampState = !lampState;
Further on, there is delay (10) in the “if” branch. We call the delay here solely because of the
imperfections of mechanical buttons. When we press the button, in the microseconds that
the plates touch, the button can record the opening and closing dozens of times. By adding
delay (10), we experience a real storm of activity. Ten milliseconds is more than enough to
calm the button down, but is too short for people to notice it.
Next, we assign the wasButtonDown variable the recently read isButtonDown value, which
tells us if the button is pressed right now. If the button has been pressed just now, the
lampState switch status of the lamp will change, and the wasButtonDown variable will
eventually take the “true” value and will store it until the button is released. Thus, next time
you call the loop, the condition will not be changed again.
If we hadn’t resorted to this trick and used only the current state of the button without looking
what was happening before, the status would have changed while the button was pressed
tens of thousands of times per second. From the point of view of a human, it would look like
there were some obvious and rather annoying problem with the device.
“If” chains
Finally, let’s program a device that represents a simple climate controller, say, for a
terrarium. Let’s say that a thermistor, an air conditioner, a heater and a green LED are
connected to Arduino. If the resulting temperature is too high, we have to turn on air
conditioning, if is it too low – the heater, and if it is in the normal range, the LED will turn on.
#define HEATER_PIN 5
#define OK_LED_PIN 6
#define THERMISTOR_PIN A0
void setup()
pinMode(CONDITIONER_PIN, OUTPUT);
pinMode(HEATER_PIN, OUTPUT);
pinMode(OK_LED_PIN, OUTPUT);
void loop()
digitalWrite(CONDITIONER_PIN, LOW);
digitalWrite(HEATER_PIN, HIGH);
digitalWrite(OK_LED_PIN, LOW);
digitalWrite(CONDITIONER_PIN, HIGH);
digitalWrite(HEATER_PIN, LOW);
digitalWrite(OK_LED_PIN, LOW);
} else {
digitalWrite(CONDITIONER_PIN, LOW);
digitalWrite(HEATER_PIN, LOW);
digitalWrite(OK_LED_PIN, HIGH);
In this program, everything should be familiar to you. The only question can be the use of “if”
and “else” next to each other, in the same line. In fact, this is just another way to write a
number of nested conditional statements. We could write the same thing in the following
way:
if (temp < TEMP_MIN) {
digitalWrite(CONDITIONER_PIN, LOW);
digitalWrite(HEATER_PIN, HIGH);
digitalWrite(OK_LED_PIN, LOW);
} else {
digitalWrite(CONDITIONER_PIN, HIGH);
digitalWrite(HEATER_PIN, LOW);
digitalWrite(OK_LED_PIN, LOW);
} else {
digitalWrite(CONDITIONER_PIN, LOW);
digitalWrite(HEATER_PIN, LOW);
digitalWrite(OK_LED_PIN, HIGH);
But you need to remember that a conditional expression is a righteous expression in C ++.
There is only one conditional expression in the external “else” branch, so the curly brackets
can be omitted:
if (temp < TEMP_MIN) {
digitalWrite(CONDITIONER_PIN, LOW);
digitalWrite(HEATER_PIN, HIGH);
digitalWrite(OK_LED_PIN, LOW);
} else
digitalWrite(CONDITIONER_PIN, HIGH);
digitalWrite(HEATER_PIN, LOW);
digitalWrite(OK_LED_PIN, LOW);
} else {
digitalWrite(CONDITIONER_PIN, LOW);
digitalWrite(HEATER_PIN, LOW);
digitalWrite(OK_LED_PIN, HIGH);
And if we remember that an empty space does not mean anything to the compiler, we can
remove some indents and line breaks, and we shall get:
if (temp < TEMP_MIN) {
digitalWrite(CONDITIONER_PIN, LOW);
digitalWrite(HEATER_PIN, HIGH);
digitalWrite(OK_LED_PIN, LOW);
digitalWrite(CONDITIONER_PIN, HIGH);
digitalWrite(HEATER_PIN, LOW);
digitalWrite(OK_LED_PIN, LOW);
} else {
digitalWrite(CONDITIONER_PIN, LOW);
digitalWrite(HEATER_PIN, LOW);
digitalWrite(OK_LED_PIN, HIGH);
Such chains made up from “if” expressions are a fairly common phenomenon. If there were
more than three options, the nested “if” expressions would look awkward and ugly, while
such a chain would still look quite clear and readable.
#define HEATER_PIN 5
#define OK_LED_PIN 6
#define THERMISTOR_PIN A0
void setup()
pinMode(CONDITIONER_PIN, OUTPUT);
pinMode(HEATER_PIN, OUTPUT);
pinMode(OK_LED_PIN, OUTPUT);
void loop()
digitalWrite(CONDITIONER_PIN, tooHot);
digitalWrite(HEATER_PIN, tooCold);
Again, everything is familiar to you except perhaps the value of the ! (TooCold || tooHot)
expression. The “||” symbol in C ++ is the logical "or" operator. The value of the expression
is true if at least one of the values on the left or right of the operator are true.
Logical expressions are nothing more than arithmetic expressions. Here the same rules
apply: in one expression, there can be any number of operators, while the order of their
application can be designated by brackets. In this case, we first calculate the logical value
tooCold || tooHot, and then the logical "not" (!) operator to invert the value obtained.
To sum up, we have got acquainted with logical variables, expressions and related
operators. It already allows us to create devices that look smart. So just use your
imagination, and go for it!