This is only a preview of the May 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:
|
Teach-In 2024
Learn electronics with
the ESP32 by Mike Tooley
Part 3 – Analogue input and output
I
n the last month’s part of our Teach-In series, we
introduced digital I/O and showed you how to interface
buttons and switches, and how to drive loads such as
LED, relays and sounders. We also showed you how to use
the Serial Monitor to test and debug your code. In this third
part we will be introducing the ESP32’s analogue-to-digital
converter (ADC) and moving into the analogue world. Our
Teach-In Practical Project is a tester for 1.5V alkaline batteries.
The learning objectives for this third part of our series are
to know how to:
n Configure and use simple analogue I/O
n Interface analogue sensors
n Use binary, octal, hexadecimal and ASCII.
ESP32 analogue input
The ESP32 has two 12-bit analogue-to-digital converters
(ADC). Each of these supports up to 18 analogue channels,
but not all may be available in a particular development
board implementation. For example, the 30-pin board that
we’re using for most of our practical projects has just six
ADC1 channels and nine ADC2 channels available. A further
limitation is that ADC2 is unavailable when Wi-Fi is being
used. Obviously, this isn’t an issue if Wi-Fi isn’t required, but
it might be a problem if you need many analogue channels.
Fig.3.1 shows the ADC pins available on the most common
30- and 36-pin ESP32 boards.
The ESP32 ADC uses a technique known as ‘successive
approximation’. This uses a comparator
that compares an analogue input with the
output of a digital-to-analogue converter
(DAC), as shown in Fig.3.2. The digital
input to the DAC (a 12-bit value) is held
in a dedicated successive-approximation
register (SAR). This holds a series of
values that rapidly approximate to
an SAR value that’s equivalent to the
analogue input. The process stops when
the data held in the SAR is converted (by
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
Earch month, there’ll be projects and challenges to help you
check and develop your understanding of the topics covered.
the DAC) to an analogue value perceived by the comparator
as being the same as the analogue input.
Reading analogue inputs
Last month, we used digitalRead() to sense the state
of the ESP32’s GPIO pins, the result being either HIGH or
LOW. The corresponding function for analogue inputs is
analogRead().
Gotcha!
The ESP32 supports 18 different
analogue channels but not all of
them may be present on a particular
development board.
40
Fig.3.1. Analogue pins on two common ESP32 development boards (30-pin left/36-pin right).
Practical Electronics | May | 2024
Fig.3.3.
Using a
potentiometer
to test the
ADC.
Gotcha!
ADC2 can’t be used if your application is using WiFi. So, if you do need Wi-Fi it’s important to use
ADC1 instead.
Fig.3.2. An ADC based on successive approximation.
As you might expect, analogRead() requires a GPIO pin as
an argument. Note that we would normally want to assign the
pin before the main body of code using a statement like this:
int analogPin = 15;
// Sensor voltage on pin-15
Here, GPIO15 (often marked ‘D15’ on development boards)
corresponds to ADC2 Channel 3. Later in the code we will
need a variable in which to store the returned analogue value
from pin-15. We can do this using:
int rawReading
Sensor value
=
analogRead(analogPin);
//
The result stored in rawReading will be a 12-bit value
from the ADC. This can range from 0 to 4095 (where 4095
corresponds to the 3.3V DAC reference voltage).
In some applications you might need to convert the 12-bit
value from the ADC into a corresponding voltage value. To
do this, you will need to convert the value returned by the
ADC and store it in a float variable, as in:
Float volts = rawReading * (3.3/4095);
Convert to voltage
Fig.3.4. Wiring arrangement for the circuit shown in Fig.3.3.
Practical Electronics | May | 2024
There are two things to note from this. First, we need to
use a float because we are no longer dealing with integer
values for voltage.
Second, a scaling factor (3.3/4095) is needed to convert
our raw reading from the ADC into a corresponding voltage.
It’s worth checking this out by connecting an ordinary
potentiometer (10kΩ to 20kΩ would be ideal) across the
3.3V supply with the slider taken to the pin in question, as
shown in Fig.3.3. A suggested wiring arrangement is shown
in Fig.3.4.
Enter or download the code shown in Listing 3.1 – all this
month’s code is available for download from the May 2024
page of the PE website: https://bit.ly/pe-downloads
When you execute the code and start the Serial Monitor
you will see the voltage present at pin-15 updated every
second. If you rotate the shaft of the potentiometer over its
full range, you will see the voltage changing smoothly from
0V at one extreme to 3.3V at the other. Typical analogue
readings are shown in Fig.3.5.
//
Fig.3.5. Typical analogue readings obtained from the Serial Monitor.
41
Gotcha!
The ESP32’s ADC are non-linear at both
extremes. It’s important to be aware that input
voltages below 0.1V will be read as 0V, while
inputs above 3.2V will be read as 3.2V. If you
can live with these restrictions, then it’s worth
noting that the device is reasonably linear
from 0.5V to 2.5V.
Gotcha!
Voltages outside the range 0 to 3.3V must
never be applied to the ESP32’s analogue
input pins. If you need to measure larger
voltages it will be necessary to use a potential
divider at the input. Furthermore, in some
applications it’s important to avoid reverse
polarity at the input.
Listing 3.1 Testing the ADC with a potentiometer
/* Analogue input using a potentiometer */
int analogPin = 15; // Analogue input via ADC2 Channel 3
void setup() {
Serial.begin(9600);
}
void loop() {
int rawReading = analogRead(analogPin);
// The raw reading from the potentiometer needs to be
// converted to volts and stored as floating point
float volts = rawReading * (3.3/4095);
// Send the value and print it using the serial monitor
Serial.println(volts);
delay(1000); // Delay for 1s before repeating the loop
}
Listing 3.2 Using the serial plotter
// Using the serial plotter
void setup() {
// Start serial communication at 9600 bps
Serial.begin(9600);
}
Fig.3.6. The ESP32’s ADC step size.
ESP32 ADC performance
Before we move on to some practical ESP32
ADC applications it’s worth explaining some
of the specifications and potential limitations
of the device.
Resolution
The resolution of an ADC is defined by its step size.
In the ESP32, a 3.3V supply reference is used with a
12-bit ADC. This achieves a comfortably small step
size of approximately 0.8mV (3.3/4096). This step
void loop() {
// Select ADC2 Channel 3 (GPIO pin-15)
int gpioPin = 15;
// Read the voltage at D15
int analogVolts = analogReadMilliVolts(gpioPin);
// Display the current voltage in mV
Serial.printf(“ADC input = %d mV\n”,analogVolts);
// Wait for a while
delay(1000);
}
size is illustrated in Fig.3.6. If
you don’t need the full default
12-bit resolution you can
select a different value using
analogReadResolution().
For example, 10-bit resolution
(210 = 1024 different values)
can be selected using
analogReadResolution(10).
Accuracy
The accuracy of an
ADC depends on
the accuracy of its
reference voltage
source. The reference
voltage for the ESP32
is derived from the
3.3V supply and there’s
no provision for an
external (and more
accurate) reference
voltage source.
Fig.3.7. ESP32 ADC non-linearity (the range from
1V to 2.75V can be considered reasonably linear).
42
ESP32’s ADC is that it does exhibit
some non-linearity, as shown in Fig.3.7.
Range
The ESP32’s analogue input range extends
from 0V to 3.3V. The voltage applied must
not be allowed to fall outside this range.
Introducing the Serial Plotter
Thus far in our series we’ve made
extensive use of the IDE’s Serial Monitor,
Linearity
Ideally, an ADC
should be perfectly
linear. Unfortunately,
a n u n c o m f o r t a b l e Fig.3.8. Circuit to demonstrate the use of the
p e c u l i a r i t y o f t h e Serial Plotter.
Practical Electronics | May | 2024
Listing 3.3 LDR test code
/* Simple LDR analogue interface */
int analogPin = 15; // Use ADC2 Channel 3
void setup() {
Serial.begin(9600);
}
void loop() {
// Get the current input from the LDR
int ldrValue = analogRead(analogPin);
Serial.println(ldrValue);
delay(1000);
// Repeat forever
}
but there’s another useful tool that’s worth knowing about.
This is the Serial Plotter, which will provide you with a neat
way of visualising data that changes over time. We’ll now use
the Serial Plotter to show how the voltage across a capacitor
increases as it charges. The code is shown in Listing 3.2, the
circuit in Fig.3.8, and a suggested wiring diagram is illustrated
in Fig.3.9. When running Listing 3.2 you will need to start
execution with the shorting link in place see Fig.3.8.)
After the code in Listing 3.2 has been compiled and uploaded,
select Tools from the IDE’s menu bar and then Serial Plotter.
Next, remove the shorting link so that the capacitor begins to
charge. You should observe a Serial Plotter display like that
shown in Fig.3.10. After the capacitor has fully charged (ie,
when the voltage at D15 has reached and flattened off at 3.3V)
replace the link and repeat the measurement with different
values for C1 and R1 (try 10µF, 47µF and 220µF). Note how
this affects the charging rate. C-R circuits are widely used in
Fig.3.11. Simple LDR interface.
electronics, as we will see a little later when we need to average
a voltage over a period of time.
Interfacing analogue sensors
The ESP32’s analogue inputs provide you with a means of
interfacing a variety of simple, low-cost analogue sensors.
As an example, we will show you how to sense ambient light
level using a light-dependent resistor (LDR).
Interfacing analogue sensors with the ESP32 is usually
very straightforward, as we will now show using an LDR.
Most LDRs exhibit resistances of several megohms in total
darkness falling progressively to a few hundred ohms in
bright sunlight. Since the ESP32 can’t sense resistance
directly, this resistance change needs to be converted to
a corresponding change in voltage. This is easily done by
connecting a series resistor to supply current to the LDR.
The voltage dropped across the LDR will then be inversely
proportional to incident light. This voltage can then be
passed to an analogue input for sensing.
A simple arrangement for light sensing is shown in Fig.3.11
with R1 and LDR1 forming a potential divider across the 3.3V
supply. The level of ambient light can be displayed using
the Serial Monitor using the code shown in Listing 3.3. Once
again, we’ve used ADC2 Channel 3 and GPIO pin D15. A
suggested wiring diagram is shown in Fig.3.12.
If you execute the code shown in Listing 3.3 you will
be rewarded with values that decrease as the intensity of
incident light increases. Our readings varied from as low
as 200 in strong sunlight to 4095 in full darkness. Average
room lighting produced a value of around 2200.
Fig.3.9. Suggested wiring layout for Fig.3.8.
Fig.3.10. Serial Plotter display for Listing 3.2.
Practical Electronics | May | 2024
Fig.3.12. Suggested wiring diagram for Fig.3.11.
43
Listing 3.4 Code for the automatic light controller
/* Simple analogue LDR lighting controller using a relay.
NB: The relay interface is active ‘low’ so a LOW output will
turn the load ‘on’ while a HIGH output will turn it ‘off’.
*/
int ldrPin = 15;
int relayPin = 4;
int thresholdValue = 2000;
// Analogue input via ADC2 Channel 3
// Digital output via GPIO 4
// Set darkness threshold
void setup() {
pinMode(relayPin, OUTPUT); // Set the relay as an output
digitalWrite(relayPin, HIGH);
// Start with light ‘off’
}
void loop() {
// Get the current analogue input from the LDR
int ldrValue = analogRead(ldrPin);
if (ldrValue < thresholdValue)
digitalWrite(relayPin, HIGH);
// Enough light so switch ‘off’
else
digitalWrite(relayPin, LOW); // Not enough light so switch ‘on’
delay(1000);
// 1s delay
// Repeat forever
}
interface (see last month). This relay
is activated by a LOW state output
from D4. A suggested wiring diagram
is shown in Fig.3.14. Note that the
relay derives it positive supply from
the development board’s +5V/VIN
pin rather than from the +3.3V pin.
The code for the automatic light
controller is shown in Listing 3.4.
This has been liberally commented
and should be reasonably selfexplanatory. Note that we have set
the switching threshold to 2000.
This is an arbitrary value and can be
changed to suit your own situation.
ESP32 analogue output
Having dealt with analogue input,
it’s now time to introduce analogue
output. Unfortunately, the ESP32
does not incorporate a true digital-toanalogue converter (DAC) so analogue
output is based on a technique known
as pulse-width modulation (PWM).
Because the ESP32 uses PWM the
analogWrite() function produces
a rectangular pulse waveform rather
than a continuous analogue voltage.
This can be a tricky concept to grasp,
so we will take some time to explain
how it works.
Fig.3.15 shows three different
pulse waveforms. At any time,
the voltage described by these
waveforms can only be either HIGH
or LOW. The waveform in Fig.3.15(a)
is a perfect square wave and is HIGH
for 50% of the time and low for the
other 50%. This is equivalent to a
mark-space ratio (HIGH-LOW ratio)
of 1:1 or a duty cycle of 50%.
Fig.3.13. Circuit of the automatic light controller.
Fig.3.15.
PWM
principle.
Fig.3.14. A suggested wiring layout for Fig.3.13.
Check it out!
Having demonstrated how easy it is to sense light level using
a low-cost LDR it is worth showing how this inexpensive
component can form the basis of a simple automatic lighting
controller. Fig.3.13 shows the circuit of our automatic light
controller. The output of our ESP32 at D4 is fed to a relay
44
Practical Electronics | May | 2024
analogWrite(outPin, 200);
// Output 2.64V
Fig.3.16 shows the output waveform produced when this
function is executed. Note that the pulsed output is not
constant (as with a true analogue signal).
You can also define a custom range of duty cycle values.
If we need 10-bit instead of 8-bit resolution we can use:
analogWrite(outPin, 511, 1023); // Output 1.65V
The default PWM frequency for the analogue channels is 5kHz
but this can be changed if required. To set the frequency to
10kHz you could use:
analogWriteFrequency(10000); //
Generating waveforms
Let’s now move on to generating a waveform rather than a
steady average. This is a little bit trickier because we will
need to code the waveform into a repetitive loop in which
successive values of duty cycle are output via the DAC. Listing
3.5 shows an example of generating a stepped waveform:
If you enter and execute the code in Listing 3.5 and connect
a DC voltmeter between pin-15 and ground you should be
rewarded with series of voltage values that steadily increment
from zero to about 2.9V in increments of about 0.32V. This
is a simple low-speed stepped waveform. You might now be
wondering if it’s possible to generate a sinewave so let’s examine
Fig.3.16. PWM output waveform.
Listing 3.5 Code for generating a stepped output voltage
/* Simple application to produce a stepped output
Using the default ult PWM setting (5kHz) */
Fig.3.17. Improving the output waveform using a low-pass filter.
The waveform in Fig.3.15(b) is HIGH for 25% of the time
and low for the remaining 75%. It has a mark-space-ratio of
1:3. The HIGH time is one quarter of the total time for the
cycle and so the duty cycle is 25%.
The waveform in Fig.3.15(c) is HIGH for 75% of the time
and low for the remaining 25%. It has a mark-space-ratio of
3:1. The HIGH time is three quarters of the total time for the
cycle and so the duty cycle is thus 75%.
Now look again at the three waveforms in Fig.3.15 and note
how the average value of voltage differs according to the duty
cycle. In Fig.3.15(a) the average value is 0.5V where V is the
maximum value. The corresponding averages for Figs.3.15(b)
and 3.15(c) are 0.25V and 0.75V. Thus, as we change the duty
cycle of the wave we also change its average value.
The first of the two parameters used in the analogWrite()
function is the GPIO pin number, while the second relates to
the duty cycle of the waveform. By default, this parameter
can range from 0 to 255, corresponding to duty cycles from
zero to 100%. So, for example, to generate a 50% duty cycle
waveform at D15 we could use lines of the form:
int outPin = 15;
// PWM output pin
// Output 1.65V
Note that 1.65V is 50% of the 3.3V reference voltage. As a
further example, let’s assume that we need to produce an
output of 2.64V. We can determine the value to use for the
duty cycle parameter from (2.64/3.3) × 255 = 200 so the code
required is:
Practical Electronics | May | 2024
// Use GPIO D15
void setup() {
}
void loop() {
for (int step = 0; step < 10; step++) {
analogWrite(outPin, step * 25);
delay(2000); // 2 sec. delay between levels
}
}
Listing 3.6 Code for sinewave generation using a look-up table
/* Low frequency sine wave generator.
This code uses the default PWM settings. */
int outPin = 15; // Use GPIO D15
// This is the sinewave lookup table:
const uint8_t sineLUT[] = {
128, 152, 176, 198, 218, 234, 245, 253,
255, 253, 245, 234, 218, 198, 176, 152,
128, 103, 79, 57, 37, 21, 10, 2,
0, 2, 10, 21, 37, 57, 79, 103
};
void setup() {
}
and
analogWrite(outPin, 128);
int outPin = 15;
void loop() {
for (int step = 0; step < 32; step++) {
analogWrite(outPin, sineLUT[step]);
delay(3); // Delay sets frequency
}
}
45
+
Fig.3.18. Sinewave output produced by Listing 3.6.
two ways of doing this; first, using a look-up table (LUT) and
second, by calculating values using the maths library.
The code in Listing 3.6 produces a sinewave of about
2Vpk-pk with a frequency of 10Hz. The output waveform
benefits from the addition of the simple C-R low-pass filter
(R1 and C1) shown in Fig.3.17. This helps to smooth the
output waveform and improve its shape. The frequency can
be changed by altering delay(). Varying the delay parameter
from 1 to 20 will change the frequency from 31Hz to 1.6Hz
respectively. Fig.3.18 shows the waveform produced.
Fig.3.19. Sinewave output produced by Listing 3.7.
Gotcha!
When using the noTone() function to turn off your tone
you must ensure that the ‘T’ is in upper case, notone()
just won’t work!
There’s another way of generating a sinewave that uses the
built-in maths library. Here we repeatedly calculate values as
we need them rather than extract them from a look-up table.
Listing 3.7 shows how this is done. Once again, the delay
parameter sets the frequency of the sinewave output.
Fig.3.19 shows the waveform produced.
Using tone() and noTone()
The ESP32 can generate a simple square wave using
the tone() function. This is simple and expedient in
many applications. For example, the code in Listing
3.8 generates an alarm signal consisting of repeated
one-second bursts of 1kHz tone. All that you need is an
audible transducer connected between pin-15 and ground.
Generating TTL levels
You may sometimes find that you need to generate a goodquality square wave at TTL (5V logic) levels. This can be
easily done by adding an external transistor buffer stage,
as shown in Fig.3.20. The TTL-compatible square wave
generated by Listing 3.9 is shown in Fig.3.21.
Fig.3.20. Using an external transistor buffer to provide a 5Vpk-pk
TTL-compatible output.
Listing 3.7 Code for generating a sinewave using maths library
/* Low frequency sine wave generator.
This code uses the math library. */
int outPin = 15;
int level;
// Use GPIO D15
void setup() {
}
void loop() {
for (int step = 0; step < 36; step++) {
level = 128 + (127 * sin(step * 10 * (6.28 / 360)));
analogWrite(outPin, level);
delay(5); // Delay sets frequency
}
}
46
LED brightness control
Another important application of PWM is controlling the
brightness of an LED display. This can be easily done by
varying the duty cycle of the pulses supplied to an LED,
Listing 3.8 Testing tone() and noTone() functions
/* Tone test */
int outPin = 15;
// Use GPIO D15
void setup() {
}
void loop() {
tone(outPin, 1000);
delay(1000);
noTone(1000);
delay(1000);
}
Practical Electronics | May | 2024
is the ones digit, the next
digit to the left is the eights
digit, next is the 64s digit,
and so on. The valid digits
are 0 to 7. For example, the
value of the digits present
in the octal number 127
can be calculated from:
1278 = (1 × 64) + (2 × 8) +
(7 × 1) = 64 + 16 + 7 = 8710
Hexadecim al (base 16)
number system
Although microcontrollers
are quite comfortable working
with binary numbers of 8,
16, or even 32 binary digits,
Fig.3.22. Denary, binary, octal
humans find it inconvenient
and hexadecimal numbers.
Fig.3.21. A TTL-compatible square wave generated by Listing
to work with so many digits
3.8 using the transistor buffer shown in Fig.3.20.
at a time. The hexadecimal (base 16) numbering system
offers a practical compromise. One hexadecimal digit
and the ESP32 provides several functions for doing this, including can exactly represent four binary digits so an 8-bit binary
ledcSetup(), ledcAttachPin(), and ledcWrite(). Listing number can be expressed using just two hexadecimal
digits. Hexadecimal notation is thus much more compact
3.9 shows how this is done.
than binary notation and often easier to work with than
decimal notation.
Coding Workshop
In the hexadecimal (base 16) number system, the
In this month’s Coding Workshop we introduce ways of
representing numbers and characters in our code. This can be weight of each digit is sixteen times as great as the
particularly useful when we need to send or receive data to/ digit immediately to its right. The rightmost digit of a
from external devices. We will start with the base 10 (denary or hexadecimal integer is the ones digit, the next digit to
decimal) number system that we’re all familiar with and move the left is the sixteens digit, next is the 256 digit and so
on to binary (base 2), octal (base 8) and hexadecimal (base 16) on. The valid digits are 0 to F. Note that, because we have
run out of numerical characters beyond 9, we introduce
number systems.
the letters A to F to represent numbers from 10 to 15.
Fig.3.22 shows the equivalence between the first sixteen
Denary (base 10) number system
The denary (or decimal) system of numbers is something decimal, binary, and hexadecimal numbers.
As an example of hexadecimal notation, the value
that we are all familiar with because we use it every day of
our lives. The valid digits in a decimal number are 0 to 9 of the digits present in the hexadecimal number 7F
and the weight of each digit is 10-times greater than the digit can be accumulated from the following weightings:
immediately to its right. The rightmost digit of a denary integer 7F16 = (7 × 16) + (15 × 1) = 112 + 15 = 12710.
(ie, a whole number with no fractional part) is the units place,
the digit to its left is the tens digit, the next is the 100s digit,
Conversion from binary to hexadecimal (and vice
and so on. For example, the value of the digits present in the versa) is very easily performed by simply arranging the
denary number 123 can be accumulated from the following: binary number into groups of four digits from right to
12310 = (1 × 100) + (2 × 10) + (3 × 1)
Note that we have used the suffix subscript 10 to
indicate the number base. This can help avoid
confusion, particularly when different number bases
are being used concurrently.
Binary (base 2) number system
In the binary system (base 2), the weight of each digit
is twice as great as the digit immediately to its right.
The rightmost digit of the binary integer is the ones
digit, the next digit to the left is the twos digit, next
is the fours digit, then the eights digit, and so on. The
only valid digits in the binary system are 0 and 1. For
example, the value of the digits present in the binary
number 1011 can be accumulated from the following:
10112 = (1 × 8) + (0 × 4) + (1 × 2) + (1 × 1) = 8 + 2 + 1 = 1110.
Octal (base 8) number system
In the octal (base 8) number system, the weight of each
digit is eight times as great as the digit immediately
to its right. The rightmost digit of an octal integer
Practical Electronics | May | 2024
Listing 3.9 Using PWM to control LED brightness
/* Simple PWM application to slowly increase the brightness
of an LED indicator */
int ledPin = 15;
int ledChan = 0;
int freq = 1000;
int bits = 8;
int delayTime = 50;
// Use GPIO D15
// LED channel
// PWM frequency
// 8-bit resolution
// Delay between levels
void setup() {
ledcSetup(ledChan, freq, bits);
ledcAttachPin(ledPin, ledChan);
ledcWrite(ledChan, 0);
}
// Configure the PWM
// and attach the LED
// Start with LED off
void loop() {
for (int dutyCycle = 0; dutyCycle <= 255; dutyCycle++) {
ledcWrite(ledChan, dutyCycle); // Output to the LED
delay(delayTime);
}
}
47
Listing 3.10 Decimal, binary, octal, and hexadecimal table
/*
Decimal, binary, octal, and hexadecimal table
*/
void setup() {
//Initialize serial and wait for port to open:
Serial.begin(9600);
while (!Serial) {
; // Wait for the serial port
}
delay(1000);
// Print the table heading and
// use tabs to separate columns
Serial.println(“”);
Serial.print(“Dec.”);
Serial.print(“\t”);
Serial.print(“Binary”);
Serial.print(“\t”);
Serial.print(“Octal”);
Serial.print(“\t”);
Serial.print(“Hex.”);
Serial.print(“\t”);
Serial.println(“ASCII”);
}
int lineValue = 64;
int endValue = 127;
Fig.3.23. Conversion of binary to hexadecimal (and vice versa).
// Start value
// End value
void loop() {
delay(200);
Serial.print(lineValue);
Serial.print(“\t”);
Serial.print(lineValue, BIN);
Serial.print(“\t”);
Serial.print(lineValue, OCT);
Serial.print(“\t”);
Serial.print(lineValue, HEX);
Serial.print(“\t”);
Serial.println(char(lineValue));
if (lineValue == endValue) {
while (true) {
continue; // Got to the end!
}
}
lineValue++; // Go round again ...
}
Table 3.1 Number base prefix
Name
Base
Prefix
Valid digits
Example
Denary
10
none
0 to 9
109
Binary
2
0b
0 and 1
0b11001000
Octal
8
0
0 to 7
0102
Hexadecimal
16
0x
0 to F
0x2C
Gotcha!
It’s important to avoid inserting
an unnecessary 0 before a number
because it will be interpreted as an
octal value. So, only use the zero
prefix if you really do want to use
octal numbers!
Fig.3.24. Serial Monitor display from Listing 3.10.
is the default way of showing numbers. Binary, octal and
hexadecimal numbers are denoted by using a prefix, as shown
in Table 3.1.
As an example of using hexadecimal values, the following
code fragment show how our sinewave look-up table (LUT)
appears when coded in hexadecimal rather than in denary:
Fig.3.25. Circuit for the
1.5V battery tester.
left and then converting each digit to
its hexadecimal equivalent, as shown
in Fig.3.23.
Number base prefix
When writing code, denary (base 10)
48
Practical Electronics | May | 2024
Listing 3.11 Code for the 1.5V battery tester
const uint8_t sineLUT[] = {
0x80, 0x98, 0xb0, 0xc6, 0xda, 0xea, 0xf5, 0xfd,
0xff, 0xfd, 0xf5, 0xea, 0xda, 0xc6, 0xb0, 0x98,
0x80, 0x67, 0x4f, 0x39, 0x25, 0x15, 0x0a, 0x02,
0x00, 0x02, 0x0a, 0x15, 0x25, 0x39, 0x4f, 0x67
};
When sending data via the Serial Monitor you can have
your numerical values printed in decimal (default),
binary, octal and hexadecimal. You can also print the
corresponding American Standard Code for Information
Interchange (ASCII) character. Listing 3.10 shows how
this is done. If you enter and execute the code you
will be rewarded with a handy reference table like that
shown in Fig.3.24. Note, for example, that the ASCII
character ‘A’ can be variously represented by 63 (in
decimal), 0b1000001 (in binary), 0101 (in octal), and
0x41 (in hexadecimal).
Practical project
Our Practical Project involves the design, construction,
and coding of a simple tester for 1.5V alkaline batteries.
The tester is to provide three different indications using
a ‘traffic light’ display. A green LED will indicate that
a battery is fresh and ready for service. An amber LED
will indicate that the battery is partially drained but
still serviceable. A red LED will show that the battery
is exhausted and should be rejected.
The circuit of the 1.5V battery tester is shown in
Fig.3.25. The analogue input of the ESP32 (via D15) is
protected by means of silicon diode D1. This component
will only conduct when the battery is inserted correctly.
If the battery is fitted incorrectly in its holder then the
diode will be reverse biased and will not conduct. The
15Ω resistor (R1) provides a test load for the battery
which demands a test current of about 50mA (note that
0.7V will be dropped across D1 when it’s conducting).
The three series current-limiting resistors (R2 to R4) are
located on the small ‘traffic light’ circuit board (see last
month for further details). The wiring layout for the 1.5V
alkaline battery tester is shown in Fig.3.26.
Fig.3.26. Wiring layout for the
1.5V battery tester.
/* Simple ESP32 1.5V battery tester */
// Assign LEDs to digital I/O lines
int redLED = 23;
// End-of life LED is red
int amberLED = 22;
// Mid-life LED is amber
int greenLED = 21;
// Beginning of life LED is green
int analogPin = 15; // Analogue input via ADC2 Channel 3
float end = 1.1;
// End-of-life threshold = 1.1V
float mid = 1.4;
// Mid-life threshold = 1.4V
float diodeFwd = 0.75;
void setup() {
// Initialize digital I/O pins outputs
pinMode(redLED, OUTPUT);
pinMode(amberLED, OUTPUT);
pinMode(greenLED, OUTPUT);
// We need to adjust the thresholds to take into
// account the forward voltage drop of the diode
end = end - diodeFwd;
mid = mid - diodeFwd;
}
void loop() {
// Get the terminal voltage of the battery on-load
int rawReading = analogRead(analogPin);
// Convert the raw reading from the analogue input
// to volts and store it as floating point
float volts = rawReading * (3.3 / 4095);
if (volts > mid) {
// It’s above the mid threshold, put green on
digitalWrite(greenLED, HIGH);
digitalWrite(amberLED, LOW);
digitalWrite(redLED, LOW);
}
if ((volts > end) && (volts < mid)) {
// It’s between the two thresholds, put amber on
digitalWrite(greenLED, LOW);
digitalWrite(amberLED, HIGH);
digitalWrite(redLED, LOW);
}
if (volts < end) {
// It’a below the end threshold, put red on
digitalWrite(greenLED, LOW);
digitalWrite(amberLED, LOW);
digitalWrite(redLED, HIGH);
}
delay(100); // Short delay
}
Teach-In Challenge
This month’s Teach-In Challenge involves a
modification to the 1.5V alkaline battery tester.
Connect a piezoelectric buzzer between D18
(GPIO18) and ground. Then modify the code so
that the tester beeps continuously whenever a
fresh battery is inserted into the test holder. This
will provide users with an audible indication
of the state of a good battery without having to
refer to the LED display. Here are the lines that
you need to add to the code – but, we will leave
you to decide where to add them!
int outPin = 18; // Tone ouput for buzzer
tone(outPin, 1000); // Output a beep
delay(500);
noTone(1000);
Next month
In Part 4, we will introduce seven-segment LED
and matrix displays. Coding workshop will
deal with random number generation and our
Practical Project will feature a dice roller.
Practical Electronics | May | 2024
49
|