This is only a preview of the April 2024 issue of Practical Electronics. You can view 0 of the 72 pages in the full issue. Articles in this series:
Articles in this series:
Articles in this series:
Articles in this series:
Articles in this series:
Articles in this series:
Articles in this series:
|
Teach-In 2024
Learn electronics with
the ESP32 by Mike Tooley
Part 2 – Digital input and output
I
n last month’s Part 1 of our Teach-In series we
provided an overview of the ESP32 and introduced the
development environment. We showed you how to use
your PC to monitor the ESP32’s built-in capacitive touch sensors and our practical project involved using the ESP32 in a
portable emergency beacon. This month, we will be taking
a more detailed look at digital I/O and showing you how to
interface buttons and switches as well as how to drive loads
such as LEDs, relays and sounders. We will also introduce
the Serial Monitor. As part of the Arduino IDE, this handy
tool provides a great way of testing your code. The learning
objectives for this part are:
n Know how to use the Serial Monitor
n Know how to configure and use simple digital I/O
n Know how to increase the output capability of the ESP32.
Using the Serial Monitor
When testing even the most basic code you will often find that
you need some means of knowing what’s going on. Without
having the benefit of a full user interface (such as a local keypad
and display) this can be tricky, but the Arduino IDE can come
to your rescue with its in-built Serial Monitor. However, before
you can begin to use this useful tool you will need to initialise
the Serial Monitor within your setup() code like this:
void setup() {
Serial.begin(9600); // Initialize the Serial
Monitor
}
About Teach-In
Our latest Teach-In series is about using the popular ESP32
module as a basis for learning electronics and coding. We
will be making no assumptions about your coding ability
or your previous experience of electronics. If you know one
but not the other, you have come to the right place. On
the other hand, if you happen to be a complete newbie
there’s no need to worry because the series will take a
progressive hands-on approach. There will be plenty of
time to build up your knowledge and plenty of opportunity
to test things out along the way.
We’ve not included too much basic theory because this
can be easily found elsewhere, including several of our
previous Teach-In series, see:
https://bit.ly/pe-ti
https://bit.ly/pe-ti-bundle
There will be projects and challenges to help you check
and develop your understanding of the topics covered
each month.
To send a message to the Serial Monitor you can use the Serial.
print and Serial.println() functions. The only difference
between these functions is that the latter adds a newline character
at the end of the printed string. Here’s an example:
Serial.print(“Pin selected = “);
You probably noticed the parameter that we’ve used in the
Serial.begin() function. This sets the speed or data
rate in bits per second (baud) that will be used for serial
communication. It is typically set to either 9600 or 115200, but
other baud rates can be selected if required. An optional second
argument configures the data, parity and stop bits. The default
is eight data bits, no parity and one stop bit. This should work
fine with the Arduino IDE and many other applications, but
you may need to change it for different external hardware. For
example, the following code sets the serial UART to 9600 bits
per second with eight data bits, even parity and two stop bits:
void setup() {
Serial.begin(9600, SERIAL_8E2); // Initialize
the Serial Monitor
}
40
Fig.2.1. Code for testing the ESP32’s built-in Hall effect sensor.
Practical Electronics | April | 2024
the variable selectedPin and then move
to the next line we would use:
Serial.println(selectedPin);
Fig.2.2. Enabling the Serial Monitor from the IDE’s Tools menu.
This will print the character string enclosed in the quotes and
the next item to be printed will appear immediately after the
trailing space. So, if we wanted to add a pin number stored in
Listings 2.1 and 2.2 show how the serial
monitor can be used both to receive data
from, and send data to, the ESP32. The
first example (Listing 2.1) makes use of
the ESP32’s built-in Hall effect magnetic
sensor. Note that having entered or
downloaded the code (Fig.2.1) you will
need to enable the serial monitor from the
Tools menu, as shown in Fig.2.2 and select
the baud rate, as shown in Fig.2.3. When
the code is executed, you will be rewarded
with a list of returned data values, updated
every 500ms (see Fig.2.4). The data will
vary, although residual values of magnetic
field should all be fairly low, but the Hall
effect sensor can be tested by placing a
small magnet close to the upper surface of
the ESP32 module, as shown in Fig.2.5. As
the permanent magnet is moved towards
the ESP32 there will be a marked increase
in detected field strength accompanied by
a change in polarity if the direction of the
field is reversed, as also shown in Fig.2.5.
The second example (Listing 2.2) shows how individual
characters can be sent from the PC via the serial link. The data
Fig.2.5. Using a small magnet to test the Hall effect magnetic
sensor. Reversing the field, as shown in (a) and (b) will result in a
reversal of the polarity of the returned data.
Fig.2.3. Setting the Serial Monitor’s baud rate.
Listing 2.1 Using the serial monitor to display data from
the ESP32’s in-built magnetic Hall effect sensor
/* Using the serial monitor to display data received from
ESP32’s in-built Hall effect magnetic field sensor */
int magField = 0;
// Initialise data from sensor
void setup() {
Serial.begin(115200); // Initialise serial monitor
}
Fig.2.4. Returned data from ESP32’s built-in Hall effect
magnetic sensor.
Practical Electronics | April | 2024
void loop() {
// The main loop repeats forever
magField = hallRead(); // Update sensor data
Serial.println(magField);
// Display using
serial monitor
delay(500);
// Short delay between readings
}
41
Gotcha!
During compilation you may receive
an error message informing you that
‘hallRead was not declared in this
scope’. If that’s the case, it is worth
checking the currently installed
version of the Espressif System
esp32 board library. The failure to
compile seems to arise in the current
‘3.0.0 alpha’ version. To put things
right you can use the IDE’s Library
Manager (see Part 1 last month) to
remove the current version before
installing a previous version (we
used version 2.0.11 without this
problem occurring).
to be sent (a single character followed
by New Line) is entered into the Serial
Monitor’s message field, as shown in
Fig.2.6. The code will allow you to toggle
the ESP32’s in-built LED (on GPIO2) ‘on’
and ‘off’ using a lower or upper case ‘X’
entered from the keyboard.
Listing 2.2 Using the serial monitor to control the ESP32’s in-built LED
/* Using the serial monitor to toggle the ESP32’s in-built LED
on GPIO2 using the ‘x’ keys on the keyboard */
int ledPort = 2;
boolean LEDstatus = HIGH;
void setup() {
pinMode(ledPort, OUTPUT);
Serial.begin(115200);
}
// GPIO2 is connected to the LED
// Initialise the LED
// Initialise the serial monitor
void loop() {
char ch = Serial.read();
// Check for a character
// Check for a valid keyboard entry
if ((ch == ‘X’) || (ch == ‘x’)) {
digitalWrite(ledPort, LEDstatus);
// Set on or off
LEDstatus = !LEDstatus;
// Toggle the status
}
delay(100);
// Short delay
}
Configuring GPIO digital outputs
The output state from a GPIO line can be HIGH (approx. 3.3V)
or LOW (close to 0V). The GPIO pins can source current when
in the HIGH state or sink current when in the LOW state. In
both cases (and to keep well within the maximum ratings for
the ESP32) we recommend the current that’s sourced or sunk
should always be less than 10mA (equivalent to a load of no
less than about 330Ω). An output device (or ‘load’) can be
connected so that it can be activated by either a HIGH state
output or by a LOW output condition (see Fig.2.7).
A current-limiting series resistor is required when driving an
LED. The value of LED series resistor should normally be within
the range 220Ω to 470Ω. For most of our examples we have used
330Ω, which results in a typical LED current of around 4mA. If more
brightness is required, the value of series resistor can be reduced to
220Ω. This brings us once again to the important question of just
how much current we can safely extract (source or sink) from an
Fig.2.8. Circuit of the ESP32 traffic lights controller.
Fig.2.6. Entering character data into the Serial Monitor’s
message field.
Fig.2.7. Output loads activated by (a) HIGH and (b) LOW output
states.
42
Fig.2.9. Wiring layout for the ESP32 traffic lights controller.
Practical Electronics | April | 2024
ESP32’s GPIO pins. Although the Espressif
datasheet states a maximum output drive
current of 12mA, we recommend limiting
the load current on any individual GPIO
line to no more than 10mA, while at the
same time ensuring that the total load on the
ESP32’s digital outputs is less than 100mA.
This should give plenty of scope for most
applications, but where additional current
drive is required an external transistor can
be used, as described later in our Practical
Project.
Listing 2.3 Code for the ESP32 twoway traffic light controller
/* ESP32 traffic lights. Outputs
red, amber and
green control signals for a 2-way
road junction
*/
// Define GPIO pins
const int Red1 = 12;
const int Amber1 = 13;
const int Green1 = 14;
const int Red2 = 18;
const int Amber2 = 19;
const int Green2 = 21;
const int delaytime = 1000;
digitalWrite(Amber1, LOW);
digitalWrite(Green1, LOW);
digitalWrite(Red2, LOW);
digitalWrite(Amber2, LOW);
digitalWrite(Green2, HIGH);
delay(5 * delaytime);
// State 3
digitalWrite(Red1, HIGH);
digitalWrite(Amber1, LOW);
digitalWrite(Green1, LOW);
digitalWrite(Red2, LOW);
digitalWrite(Amber2, HIGH);
digitalWrite(Green2, LOW);
delay(delaytime);
// State 4
digitalWrite(Red1, HIGH);
digitalWrite(Amber1, LOW);
digitalWrite(Green1, LOW);
digitalWrite(Red2, HIGH);
digitalWrite(Amber2, LOW);
digitalWrite(Green2, LOW);
delay(delaytime);
// State 5
digitalWrite(Red1, HIGH);
digitalWrite(Amber1, HIGH);
digitalWrite(Green1, LOW);
digitalWrite(Red2, HIGH);
digitalWrite(Amber2, LOW);
digitalWrite(Green2, LOW);
delay(delaytime);
// State 6
digitalWrite(Red1, LOW);
digitalWrite(Amber1, LOW);
digitalWrite(Green1, HIGH);
digitalWrite(Red2, HIGH);
digitalWrite(Amber2, LOW);
digitalWrite(Green2, LOW);
delay(5 * delaytime);
// State 7
digitalWrite(Red1, LOW);
digitalWrite(Amber1, HIGH);
digitalWrite(Green1, LOW);
digitalWrite(Red2, HIGH);
digitalWrite(Amber2, LOW);
digitalWrite(Green2, LOW);
delay(delaytime);
Check it out!
Our Check it out! feature involves building
a simple two-way traffic lights controller
shown in Fig.2.8. Each of the two traffic
void setup() {
lights (Light 1 and Light 2) will comprise
// All pins configured as outputs
a set of three LEDs: red, amber and green.
pinMode(Red1, OUTPUT);
Rather than use individual LEDs (together
pinMode(Amber1, OUTPUT);
with series resistors) we’ve taken the easy
pinMode(Green1, OUTPUT);
option and used a ready-built mini traffic
pinMode(Red2, OUTPUT);
lights module fitted with red, amber and
pinMode(Amber2, OUTPUT);
green LEDs, together with series 330Ω
pinMode(Green2, OUTPUT);
resistors. The module is available from
}
several online suppliers, and it uses a
common ground (GND) connection so
void loop() {
// State 0
that the individual LEDs will become
digitalWrite(Red1, HIGH);
illuminated when their associated input
digitalWrite(Amber1, LOW);
pins are taken HIGH. (Just search for ‘5V
digitalWrite(Green1, LOW);
Mini Traffic Light LED Display’; eBay and
digitalWrite(Red2, HIGH);
others offer plenty of choice.)
digitalWrite(Amber2, LOW);
The wiring layout for the ESP32
digitalWrite(Green2, LOW);
traffic lights controller is shown in
delay(delaytime);
Fig.2.9. The code shown in Listing 2.3
// State 1
should be entered or downloaded from
digitalWrite(Red1, HIGH);
the April 2024 page of the PE website:
digitalWrite(Amber1, LOW);
digitalWrite(Green1, LOW);
https://bit.ly/pe-downloads
digitalWrite(Red2, HIGH);
When the code is executed the two
digitalWrite(Amber2, HIGH);
traffic lights should follow the sequence
digitalWrite(Green2, LOW);
shown in the state table (Fig.2.10).
delay(delaytime);
Notice how States 2 and 6 are each to
// State 2
be displayed for five seconds while the
}
digitalWrite(Red1, HIGH);
remaining states are displayed for 1
second. These display times are achieved
by introducing delays of appropriate length in the code. Note
applications include sensing the state of buttons and switches
that it is only necessary to modify one line of code to change
as well as a host of sensors with digital outputs, such as float
the timing of the lights. The code is very straightforward and
switches, pressure switches, touch switches, microswitches,
should need no further explanation.
proximity switches and magnetic reed switches. These devices
can all make use of a straightforward connection to a GPIO
Configuring digital inputs
There are numerous applications in which you might need to
make use of one or more of the ESP32’s digital inputs. Such
Fig.2.10. Sequence table for the traffic lights controller.
Practical Electronics | April | 2024
Fig.2.11. A selection of commonly available buttons and
switches ideal for use with the ESP32.
43
Gotcha!
GPIO pins 34, 35, 36 and 39 are only available for digital
input and do not have internal pull-up and pull-down
resistors. So, if you need to use these pins for digital inputs,
remember to fit external pull-up or pull-down resistors.
input pin. A selection of commonly available buttons and
switches is shown in Fig.2.11.
Pull-up or pull-down?
We need our digital inputs to be in one of two states, either HIGH
or LOW with nothing in between. The former is usually defined
as a voltage level that’s greater than 75% of the nominal 3.3V
supply, while the latter is taken as less than 25% of the same
value (these two states correspond to more than 2.5V or less
than 0.8V, respectively). Note that, by default, the GPIO inputs
adopt a high-impedance state, and this invariably results in an
unpredictable and indeterminate input Because of this you will
often find that the GPIO pins are either pulled HIGH (pulled up)
or pulled LOW (pulled down) by means of external resistors.
Values are generally uncritical and can often be in the range
4.7kΩ to 100kΩ. Fig.2.12 illustrates the use of this technique.
Gotcha!
A problem arises with some types of mechanically
actuated switch in which the switching action is not
clean, and the output state fluctuates rapidly before
settling to a steady value. This condition is referred to as
‘switch bounce’ (see Fig.2.14) and it can be undesirable
in many applications. The effects of switch bounce can
be reduced or eliminated by various means including
the introduction of software delays or by slowing the
switching action using a simple capacitor-resistor (C-R)
network between the switch contacts and a GPIO input
pin, as shown in Fig.2.15.
Fig.2.14. Waveform showing switch bounce.
It’s important to be aware that the ESP32 has a rather neat
feature that allows inputs to be pulled HIGH or LOW without
the need for external resistors. This can be achieved during
GPIO initialisation using the pinMode() command, as shown
in the following fragment of code:
pinMode(18, INPUT); D18 is a default digital input
pinMode(19, INPUT_PULLUP); D19 is a digital input
pulled high
pinMode(21, INPUT_PULLDOWN); D21 is a digital
input pulled low
The effect of INPUT_PULLDOWN and INPUT_PULLUP is
illustrated in Fig.2.13. Note that the ESP32’s internal pull-up/
pull-down resistors have a nominal value of 45kΩ.
Fig.2.12. External pull-up and pull-down resistors.
Fig.2.13. Equivalent internal pull-up and pull-down resistors.
44
Practical project (Part 1)
Our Practical Project involves the design, construction and
coding for a simple motor controller. The minimal circuit and
wiring layout for our prototype motor controller are shown
in Fig.2.16 to Fig.2.18 (the pin connections for the LED were
shown in Fig.1.21 last month). We’ve used 4.7kΩ external
pull-down resistors in conjunction with the RUN and STOP
switches and a 330Ω series resistor for the LED (later, we will
be replacing this with a small motor and driver).
Having connected the circuit, made the USB connection
to your PC and started the IDE, you can enter (or download)
the code shown in Listing 2.4. When you execute the code,
you should see that the LED becomes illuminated when
the RUN button is pressed. The LED should then remain
illuminated until the STOP button is operated. A subsequent
press of the RUN button
should restart the LED.
Now let’s walk through
the code.
The first few lines of
code define the constants
used to allocate the
GPIO pin numbers to
the two buttons and
LED. Next, we define
the variables that will
represent the states of Fig.2.15. Using a C-R network to
the two buttons and the reduce the effects of switch bounce.
Practical Electronics | April | 2024
Fig.2.16. Circuit for the prototype motor controller.
Fig.2.17. Circuit layout for the prototype motor controller
(based on a 30-pin ESP32 board).
LED output. Finally, the GPIO pins are then configured in
the setup() code.
The main loop of the code repeats forever and starts
by reading the state of the two buttons. Having read the
button states we decide what to do depending upon
the current state of each button. If the RUN button
has been operated, we activate the LED but if the
STOP button has been operated, we turn the LED
off. Later, in Coding workshop we will take a close
look at how these decisions are made.
Having verified that the prototype works we can
add a small motor to be controlled by the RUN
and STOP buttons. Bearing in mind the current
limitation mentioned earlier we will need to add
some extra circuitry to supply the greater current
required. We will return to this in Part 2 a little
later, but first it’s worth considering how to drive
loads that demand more current or voltage than the
ESP32 can provide.
Driving high-current/high-voltage loads
The ESP32’s current drive capability is sufficient for a lot of
purposes (including illuminating an LED) but not enough to operate
actuators, motors, lamps and many other ‘real-world’ output
devices. Fortunately, the problem of driving the vast majority of
high-voltage and high-current loads can be easily solved using
one or more miniature relays. These electromechanical devices
comprise a coil wound on a high-permeability core and a moving
armature mechanically linked to a set of contacts that make and
break when the device is actuated. When sufficient current is
applied to the coil of the relay the resulting magnetic field will
cause the soft iron armature to pull-in and this in turn will open
or close the relay’s electrical contacts. A typical miniature PCB
mounted relay will operate from a 5V supply, and its contacts will
pull-in at around 75% of this value. The specifications of such a
relay are listed in Table 2.1. Solid-state relays (like those shown in
Listing 2.4 Simple motor control
/* ESP32 button control example. One button will be
for Start and the other for Stop. The output will be
indicated on an LED*/
// Define constants for the GPIO pins
const int runButton = 19; // GPIO19
const int stopButton = 21; // GPIO21
const int ledOutput = 18; // GPIO18
// Define variable states
int runButtonState = LOW;
int stopButtonState = LOW;
int outputState = LOW;
// No user input yet so both
// buttons will be low
// Initially this must be low
void setup() {
// Initialize the LED pin as an output
pinMode(ledOutput, OUTPUT);
// Initialize the two push button pins as an inputs
pinMode(runButton, INPUT);
pinMode(stopButton, INPUT);
}
Fig.2.18. Wiring for the prototype motor controller (based on
a 30-pin ESP32 board).
Fig.2.19. Solid-state relays designed for controlling AC mains
with maximum load currents ranging from 10A to 45A.
Practical Electronics | April | 2024
void loop() {
// Read the current state of the two buttons
runButtonState = digitalRead(runButton);
stopButtonState = digitalRead(stopButton);
// Check if the buttons are pressed
if (runButtonState == HIGH) {
outputState = HIGH;
digitalWrite(ledOutput, HIGH);
}
if (stopButtonState == HIGH) {
outputState = LOW;
digitalWrite(ledOutput, LOW);
}
}
45
Parameter
+
Table 2.1 Electrical specifications of a typical miniature relay
Value
Nominal operating voltage
5V DC
Nominal operating current
73mA
Maximum load rating
AC 250V 10A, DC 30V 10A
Pull-in voltage (typical)
3.8V
DC coil resistance
70Ω
Power consumption (typ)
0.36W
Operating time (max)
10ms
Release time (max)
5ms
Contact resistance (max)
0.11Ω
Operating life
100,000 operations at rated load
Maximum switching rate
30 operations per second
Fig.2.19) can be used to control much greater AC mains loads
of up to 45A, but can still operate from a low voltage input.
It is important to note from Table 2.1 that the relay coil requires
an operating current that’s well beyond the output drive capability
of the ESP32. We therefore need an interface that will provide the
extra current required. Fortunately, this can be little more than a
low-power transistor and a handful of other components, as we
shall see later in Part 2 of our Practical Project.
A neater, and often more cost-effective alternative is that
of making use of a ready built relay interface board. Luckily,
there are quite a few to choose from and the most common
types are fitted with two, four or eight relays with each relay
having its own driver circuit.
Fig.2.20 shows a typical four-channel relay board. The board
has a transistor driver and an opto-isolator for each channel.
Similar boards can be purchased very cheaply (often less than
£5) and so it is invariably more cost effective to purchase one
of these boards rather than attempt to build one yourself.
Coding relay outputs
It’s very easy to control one or more relays using just a few
lines of simple code. First, you will need to make sure that
you define the digital output pins to which the relays are
connected using a line of the form:
int pump = 18;
int heater = 21;
// Pump connected via a relay
on GPIO pin-18
// Heater connected via a
relay on GPIO pin-21
Next you will need to add a couple of lines into the setup()
code block, as follows:
Fig.2.21. Circuit of the four-channel relay interface board.
pinMode(pump, OUTPUT);
// Pump is configured
as an output
pinMode(heater, OUTPUT); // Heater is
configured as an output
The relays and their respective loads can be turned on and off
incorporating the following lines of code at appropriate points
in the main program loop:
Fig.2.20. A four-channel relay interface board.
46
digitalWrite(pump, LOW);
// Turn the pump off
digitalWrite(pump, HIGH); // Turn the pump on
digitalWrite(heater, LOW); // Turn the heater off
digitalWrite(heater, HIGH); // Turn the pump on
Practical Electronics | April | 2024
An example might be counting items into
batches of 10 on a conveyor. Let’s assume
that we need to operate an LED when the
count reaches (or exceeds) ten items. The
following fragment of code would do this:
if (count >=10) {
digitalWrite(fullLED, HIGH);
}
If the value of count is less than 10 then
the condition evaluates false and the
statement following the condition is
simply ignored. However, if the value of
Fig.2.22. Adding a transistor driver and motor to the output of the motor controller.
count is 10 or greater then the condition
evaluates true and the statement following the condition is
Practical project (Part 2)
executed. In some applications it can be appropriate to use
Now let’s return to the Practical project. We can add a motor to
a series of if statements to detect various conditions and to
the output of the ESP32 (GPIO pin-2) using a transistor driver
act on them accordingly.
similar to those used in the four-channel relay interface shown
in Fig.2.20. The modified circuit and wiring diagrams for the
The if ... else construct
motor controller are shown in Figs.2.22 and 2.23 respectively.
The if ... else construct is very straightforward. The
The motor is a low-cost miniature 5V component fitted with
basic syntax is just:
an integral gearbox. TR1 provides the larger current drive
for the motor (typically 50 to 100mA) and acts as a switch
if (conditional expression) {
which is open when the GPIO pin-2 goes LOW and is closed
// code to be executed if true,
when GPIO pin-2 goes HIGH (see Fig.2.24). Diode D1 is used
// each statement ending with ;
to protect TR1 from the back EMF generated when the motor
} else {
current flow ceases. Similar diodes are used whenever inductive
// code to be executed if false
loads (such as relays, solenoids and motors) are used as loads.
// each statement ending with ;
}
Coding workshop
Being able to make decisions based on what’s happening and
Here’s a simple example. Let’s assume that we are monitoring
then acting on this information in different ways is an essential
an analogue voltage and wish to set a threshold of 512 as the
pre-requisite of any programming language. The C-code used
value at which a green LED should become illuminated and,
in the Arduino IDE provides you with a variety of different
below this value, we would like a red LED to be turned on. Our
conditional constructs that allow you to do this. Simple
if … else construct would then look something like this:
decisions can be made using nothing more than if and else,
while loops can be controlled using while, do while, for and
if (inVoltage >=128) {
loop. We will look at all of these, starting with if and else.
digitalWrite(greenLED, HIGH);
} else {
The if construct
digitalWrite(redLED, HIGH);
The if construct is the simplest of all the condition constructs.
}
It is used when a statement (or a series of statements) should
be executed when a particular condition prevails. The basic
Unfortunately, this isn’t quite the whole story. The red and
syntax is as follows:
green status LEDs should be mutually exclusive and so we
might need to ensure that when one LED is turned on the other
if (conditional expression) {
LED is turned off. There are various ways that we could do
// code to be executed if true,
this. We could either set them both off before we arrive at the
// each statement ending with ;
if else construct or we could turn one on and the other off
}
Fig.2.23. Wiring of the modified motor controller.
Practical Electronics | April | 2024
Fig.2.24. Switching action of the transistor in Fig.2.22.
47
within a construct containing more than one statement (ie, a
compound construct).
Using the first method we might have:
while (limitSwitchStatus == LOW) {
// Limit not reached so run the motor
digitalWrite(motorRun, HIGH);
// Check to see if anything has changed?
limitSwitchStatus = digitalRead(limitSwitch);
}
// start with both LEDs off
digitalWrite(redLED, LOW);
digitalWrite(greenLED, LOW);
// now decide which LED to put on
if (inVoltage >=512) {
digitalWrite(greenLED, HIGH);
} else {
digitalWrite(redLED, HIGH);
}
By making the conditional expression dependent on the value
of a counter modified inside the loop we have a simple means
of performing one or more statements a predetermined number
of times, as follows:
The other possibility is:
count = 0;
while (count < 50) {
// code to be executed 50 times
// each statement ending with ;
count = count+1;
}
// Read the input voltage and
// put the red or green LED on
if (inVoltage >=512) {
digitalWrite(greenLED, HIGH);
digitalWrite(redLED, LOW);
} else {
digitalWrite(redLED, HIGH);
digitalWrite(greenLED, LOW);
}
Notice how in this example we’ve compounded several
statements after the if and else. The curly braces, { and
}, help make the logic clear and unambiguous. Of course, at
some point earlier in the code we would have to define the
pins that we are using to control the two LEDs and initialise
the variables (inVoltage, redLED and greenLED).
Conditions
There are several logical conditions that we can test for, and
they are summarised in Table 2.2. Notice how an exclamation
mark (!) is used to denote ‘not’.
In the previous example you should have noticed the >=
condition that we used to find out whether the input voltage
has exceeded the threshold value of 512. The ‘greater than
or equal to’ condition isn’t the only one that we have to play
with, as Table 2.2 shows.
The while construct
The while construct provides you with a means of continuously
executing one or more statements until a condition evaluates false.
The loop containing the statement (or statements) will continue
to be executed as long as the condition remains true but, as soon
as it becomes false the loop will terminate and execution will
continue with the next subsequent statement. The basic syntax is:
while (conditional expression) {
// statements to be executed if true,
// each ending with ;
}
Here’s an example that shows how a belt motor could be
controlled using a while loop. The belt motor will run for
Table 2.2
Conditions
48
as long as it takes for an item placed on the belt to reach a
limit switch. Note that we must check the status of the limit
switch inside the loop. If we forget to do this the motor will
run forever!
In this wait loop we increment the counter on every pass
through the loop until it reaches 50, at which point the
conditional expression evaluates to false and execution
continues with the next statement in the code. Note that there’s
a neater way of incrementing the count value, as follows:
count = 0;
while (count < 50) {
// code to be executed 50 times
// each statement ending with ;
count++;
}
In this case, count++ is used to ‘post-increment’ the value
of count. In other words, it takes the current value of
count, adds one to it and places the new value back into
the count variable.
Finally, here’s an example showing how a simple wait loop
could be used to flash an alarm LED ten times:
flashCount = 0;
while(flashCount < 10) {
digitalWrite(alarmLED, HIGH);
delay(500); // wait half a second
digitalWrite(alarmLED, LOW);
delay(500); // wait half a second
flashCount++;
}
The do ... while construct
The do ... while loop works in a similar fashion to the
while loop but with the exception that the condition is tested
at the end of the loop, not the beginning. This means that the
statements within the loop will always be executed at least
once. The syntax is as follows:
Code
a == b
Meaning
Notes
a is equal to b
True if a and b have the same value
a != b
a is not equal to b
True if a and b have the different values
a > b
a is greater than b
True if a is larger than b (but not true if they have the same value)
a < b
a is less than b
True if a is smaller than b (but not true if they have the same value)
a >= b
a is greater than or equal to b
True if a is larger than b (and also true if they have the same value)
a <= b
a is less than or equal to b
True if a is smaller than b (and also true if they have the same value)
Practical Electronics | April | 2024
do {
// code to be executed at least once,
// each statement ending with ;
} while (conditional expression);
Here’s an example of reading a pressure sensor and waiting a
short time for the output to reach a steady value:
do {
delay(100);
// wait for the value to settle
cp = readPressure();
// read the pressure sensor
} while (x < 10);
In this example we are calling the readPressure() function 10
times before arriving at the final value returned from the sensor.
The for loop construct
The for loop is widely used in almost every computer
language, and C is no exception. The construct is used to repeat
a statement (or series of statements) whenever a condition
evaluates true. If the condition evaluates false, then the loop
is exited, and execution continues with the statement that
immediately follows the loop. The for loop must be initialised
at the outset and thus is a little more complex than the while
loop. The basic syntax is:
for (loop initialization, conditional expression,
increment) {
// statements to be executed if true,
// each ending with ;
}
As with the while loop, a counter is often used to control the
loop, and this is incremented or decremented each time round
the loop. This makes the construct ideal for use in any repetitive
application, for example, checking the status of several I/O
lines. It is important to remember that loop initialisation occurs
only once and before the loop is executed for the first time.
The next code fragment shows how the ASCII character set
can be sent to the serial monitor. Note that, for this to run, we
would first need to initialise the serial port interface using a line
such as Serial.begin(9600). Note also that we have declared
the count variable (i) within the loop initialisation itself.
for (int i = 0; i <= 63; i++)
{
testValue = 64 + i;
Serial.print(testValue);
Serial.print(“\n”);
delay(100);
}
Program structure and layout
By now you should have gained some idea of what code looks
like and how it is structured, but before we go any further it is
well worth explaining the layout in a little more detail. You
may have noticed that the first few lines of code in a program
usually take the form of a heading enclosed between pairs
of characters, /* and */, which constitute a comment block.
Everything between these two characters is taken as plain text
and, since this has no effect on program execution you can
use as many lines of text here as you want.
The code’s title comment block is usually followed by several
variable declarations. The reason behind this is simply that, in
the C language, variables must always be declared before they are
used. In fact, declarations don’t have to be placed at the beginning
of the program code but the point at which they are declared
Practical Electronics | April | 2024
Fig.2.25. Motor controller with toggle action RUN/STOP button.
(ie, their position in the program) can impose restrictions on
the scope over which they can be used. However, since we
often need to use variables on a global basis (ie, anywhere in
our program code) we will often place them before any of the
other code. Declarations involve assigning a variable type (see
last month), a name and (optionally) an initial value.
Next, follows code that’s used for setting up. This code is
placed in a function called setup() and it is executed at the
beginning and only once. The setup() function is often used
to specify the pin modes (ie, input or output) and to configure
the IDE’s Serial Monitor, but if they are not being used then
the setup() function can simply be left empty.
The main program code is written inside a loop that executes
forever (or until the reset button is pressed or the power is
removed). This loop() function contains the functions that
will execute when the program is being run. Each function
takes the form of a block of code that is executed whenever the
function is called. Functions can be the ones that are built into
the language, or they can be user-defined. This feature allows us
to extend the basic language for our own needs with our userdefined functions calling other functions (both user-defined
and in-built) as and when required. Function declarations take
the form of one or more statements enclosed between curly
braces, { and }. Note that each individual statement must end
with a semicolon, ;.
As well as the block comments that we mentioned earlier,
comments can be placed in-line. These consist of plain text
appearing after two // characters and added at the end of the
line to which they apply. As previously mentioned, comments
provide us with a useful reminder of what’s going on in the code
and they can be invaluable when maintaining and debugging
a program. Indenting (often by three or four spaces) is used
to assist with program readability. This becomes particularly
important when functions are enclosed within other functions.
Top tip – do note that the IDE can tidy up your code very nicely
if you select Tools and then Auto Format.
Teach-In challenge
Our second Teach-In challenge is one that will test your coding
skills. Fig.2.25 shows a simplified version of the motor controller
in which only one button is provided. This button is to have a
toggle function; press once for RUN and then again for STOP.
See if you can modify the code from Listing 2.4 to achieve this.
Next month
Next month, we will move into the analogue world and investigate
the ESP32’s analogue input and output capability. We will be
introducing techniques for generating analogue waveforms, and
our Practical Project will feature a simple battery checker. Coding
workshop will provide an explanation of the binary, octal and
hexadecimal number systems.
49
|