Projects

Ports Aplenty - ESP8266 Port Expansion Techniques

Gamal Labib

Issue 71, June 2023

This article includes additional downloadable resources.
Please log in to access.

Log in

The ESP8266 MCU is provided with a limited number of usable digital I/O ports and with only one analog input port. Such limitation needs to be confronted whenever the MCU has to drive several digital and analog sensors or actuators.

In this article, I present a couple of techniques for resolving ESP8266 modules' port limitations.

I have built two projects using prototyping boards and Arduino's IDE to illustrate the hardware and software issues pertinent to each technique.

How capable is the ESP8266 module?

The Good, the Bad and the Ugly

The ESP8266 is a system-on-chip (SOC) module1 with a 32-bit CPU running at clock speed up to 160MHz.

It has 17 general-purpose input/output ports (GPIO) supporting digital and pulse width modulation (PWM) modes of operation.

It also integrates an analog-to-digital converter (ADC) but unfortunately is limited to a single input port.

The ESP8266 supports several connectivity alternatives, namely the Inter-Integrated Circuit (I2C), the Serial Peripheral Interface (SPI), the universal asynchronous receiver/transmitter (UART), and the wireless fidelity (WiFi).

With such an abundance of features and capabilities, it is no wonder we find different variations of the module at the heart of sensing and control devices all over the Internet of Things (IoT).

Another limitation of the module lays in the usable bidirectional GPIO ports which count to only 5, while the remaining 12 are either involved in flashing or booting the module. So, in order to make an ultimate use of the module's computing resources, we need to extend its I/O connectivity to accommodate as much sensors and controllers as possible. Luckily, there are discrete electronic components as well as breakout boards that would come to the rescue.

In this article, I shall explore some techniques for extending the analog and the digital ports, and I shall dive into the hardware and software aspects involved in their implementations.

Project 1: Using the CD4051 8-channel multiplexer chip

The Dummy Extension

Simply add multiplexer chips

Parts Required:JaycarAltronics
1 x WeMos D1 Mini Standard (built-in antenna)XC3802-
1 x 4051 Analog 8-Channel Multiplexer ICZC4051Z4051
4 x TEMT6000 Light Sensor Modules-Phipps: PHI1001838

Parts Required:

*A breadboard, 5V power supply, and prototyping hardware is also required.

There are two directions for expanding MCU ports, whether they are analog or digital. The first adopts channel multiplexers from which a single input/output channel out of a set of 4/8/16 channels is selected using its binary address formed of a 2/3/4 bit combination. The selected channel is connected to the target port of the MCU.

Figure 1 below illustrates how to extend an MCU port (in red) to four ports (in brown) using selection address of two bits driven by two digital ports (in blue).

In the first project, I follow this direction using the CD4051 8-channel multiplexer chip2 to extend the analog port of a WeMos D1 Mini board with 4 analog ports that may be accessed one at a time by setting 2 digital ports of the module as channel address. The WeMos D1 Mini board3,4 is a development board built around the ESP8266 module and equipped with serial to TTL USB interface that facilitates flashing and debugging the module using Arduino IDE5.

Note: It is always possible to complicate things a little bit by adopting breakout boards such as SparkFun's analog/digital multiplexer built around the CD74HC4067 16-Channel chip6. However, 8-channel option will just do as I shall connect only 4 analog light sensors to the ESP8266 module.

Figure 1 Channel multiplexer as port extender, an illustration of how to extend an MCU port (in red) to four ports (in brown) using selection address of two bits driven by two digital ports (in blue).

In this project, a set of 4 analog breakout boards built around TEMT6000 light sensor are connected to the A0 analog port of the WeMos board using the CD4051 chip.

The GPIO12 and 13 pins are used for selecting either sensor's channel to connect to A0.

The sketch in Arduino IDE addresses each sensor in turn and displays its recorded Lux value in the serial monitor of the IDE. Listing 1 depicts code sections of the sketch and shows only sensor #1 manipulation statements in (c).

Figure 2 shows parts of recorded Lux readings of the four sensors under different lighting conditions.

The WeMos board settings in IDE are shown in Figure 3.

Light sensors readings in normal room conditions (A), with hands on (B), and smartphone flash (B) are as follows:

A. Light sensors readings in normal room conditions

LUX from sensor #1: 100.55
LUX from sensor #2: 116.02
LUX from sensor #3: 116.02
LUX from sensor #4: 119.88

Figure 2

B. with hands on

LUX from sensor #4: 29.00
LUX from sensor #1: 25.14
LUX from sensor #2: 27.07
LUX from sensor #3: 29.00

C. with smartphone flash

LUX from sensor #3: 1077.01
LUX from sensor #4: 1980.00

The Board settings for an WeMos D1 Mini is as follows:

The Board settings for Arduino Pro Mini is as follows:

The code:

Listing 1

The main parts of Project 1's Sketch are shown here:

A. MCU pin declarations & global variables

B. Setup of MCU pin modes

C. Loop section depicting only the first sensor's selection and reading

D. Sensor's reading manipulation function.

Part a

#define LIGHTSENSORPIN A0  // Ambient light sensor reading port
#define SEL1 12          // sensor selection address (LSB) D6 
#define SEL2 13          // sensor selection address (MSB) D7
float const AREF = 3.3;    // set to 5 or 3.3 depending on sensor's Vcc

Part b

pinMode(SEL1, OUTPUT);
pinMode(SEL2, OUTPUT);

Part c

digitalWrite(SEL1, LOW);  // switch to light sensor #1 (LSB)
digitalWrite(SEL2, LOW);  // switch to light sensor #1 (MSB)
delay(100);
float lt = readLux();      // call sensor manipulation function in (d)
Serial.print("Lux from sensor #1: ");
Serial.println(lt);      // send Lux reading to IDE's serial monitor
delay(1000);

Part d

float readLux(void) {
  float sensor_value = analogRead(LIGHTSENSORPIN) 
    // Get raw sensor reading
  float volts = sensor_value * AREF / 1024.0; 
    // Convert reading to voltage
  float amps = volts / 10000.0;       
    // Convert to amps across 10K resistor
  float microamps = amps * 1000000.0;   
    // Convert amps to microamps
  float Lux = microamps * 2.0;       
    // 2 microamps = 1 Lux, @ 3.3V max.
  return Lux;
}

Project 2: WeMos D1 Mini to Arduino Pro Mini

The Intelligent Extension Hitting two birds with one stone: extra I/O ports and multiplied processing power

Additional Parts Required:JaycarAltronicsPakronics
1 x Arduino Pro Mini-Z6222ADF-DFR0159
1 x Logic Level Converter--ADA757
2 x Dual LCD 16x2 with PCF8574 I2C modules-Phipps: PHI1001894-

Additional Parts Required:

This is the second direction to follow in order to extend the analog port of the ESP8266 in which I made use of a co-processor board rich in analog ports, a typical Arduino board. In this project, I connected the WeMos D1 Mini to Arduino Pro Mini8,9 using the supported UART communication facility in both boards. The Arduino board not only provided an extra 6 input analog and 14 digital I/O ports to join the WeMos ports, but also relieved the WeMos from polling and processing sensors' readings.

Note: The WeMos D1 Mini works at signal level of 3.3V on its I/O ports, while the Arduino Pro Mini works at 5.0V level, so interfacing both devices requires signal level conversion to avoid browning the WeMos. Luckily, there are breakout boards that do just that, and I am using one here.

When setting up the testbed for the project, I needed an LCD panel connected to each MCU, since I was using the serial communication for linking them, and the IDE serial monitor will no longer be available for debugging.

I used I2C driver breakout boards for connecting the LCDs to the MCUs in order to save their digital I/O ports. The I2C reserved 2 analog ports out of the Arduino board, e.g. A4 and A5, and also 2 digital ports out of the WeMos, e.g. GPIO4 and GPIO5. So the net available ports on both boards would come to 3 digital ports plus the analog one on the WeMos and all digital ports plus 4 analog ports on the Arduino. This explains the restriction imposed by the testbed of using only 4 analog light sensor breakout boards for this project.

The build photo and Fritzing shows how I implemented the testbed in which the left-hand side section was dedicated to the WeMos connections, and the right-hand side was left for the Arduino.

The light sensor boards were wired to the available 4 analog ports of the Arduino, and I chose to power them using the 3.3V power output from the WeMos even though they can take 5.0V for Vcc. The reason for this was because of the uncertainty I had towards the Arduino's model, 3.3V or 5.0V.

Later on, I figured out the model to be the 5.0V one when I measured the voltage on its Tx pin and it was fluctuating between 0V and 5.0V. Thus, I introduced the signal level converter (shown below) which lies between the two peers and had its 5.0V connections wired to the Arduino's Tx and Rx, and the 3.3V connections wired to the WeMos Rx and Tx respectively.

Figure 4

Figure 4 above shows the data structure I used to pass the sensors' readings from the Arduino to the WeMos. A light sensor typically produces a maximum of 4-digit reading so I thought of packing all sensors' readings into a 16-character string assembled in Arduino and disassembled in WeMos. The LCD display would normally fit the 16 digits into one line as it supports 16x2 rows.

Figures 5 shows a sample of extracted reading for sensor #4 on WeMos which matches the string of readings on the Arduino (Figure 6). I tuned the speed by which the Arduino polls the light sensors in such a way to give the WeMos a comfortable time window to show me the light intensity measurements. This counted for 12 seconds due to human eye intervention in the process.

The table shown here is the Packet structure sent from Arduino Pro Mini to WeMos D1 Mini, as a string having the size of 16 characters, each 4 characters depict a sensor's reading, assembled in Arduino (integer-> string) and split in WeMos (string -> integer).

Manipulating each sensor data on WeMos (Figure 5) upon capture by Arduino (Figure 6)

As for the sketches deployed on the Arduino and WeMos peers, I included Listing 2 and Listing 3 that depict the key functionality characterising each of them. It is worthwhile to note some coding tricks I used to seamlessly finish the job, as follows.

Note: I used the dtostrf() function to convert a sensor's reading from float to string and set it up to generate 4 digits with no fractions, and pointed the location of the digits within the readings' structure.

Note: The light sensor manipulation function readLux() was extracted from the manufacturer recommendation, with slight variation.

The code:

Listing 1: WeMos D1 Mini

The main parts of Project 2's Sketch on the WeMos D1 Mini are shown here:

A. Declare global variable to accommodate the received data from Arduino

B. WeMos waits for serial data from Arduino

C. When data is buffered in WeMos, it extracts sensors readings without trailer characters - important

D. WeMos calls the data splitting function to isolate each sensor reading

E. Function code that splits sensors data.

Code a

String input;      // string variable that collects received sensors readings

Code b

lcd.print("Waiting for Data");   
while(Serial.available() == 0){   // wait for data from Arduino 
    }

Code c

input = Serial.readStringUntil('n'); 
  // read serial data coming from Arduino
  // and eliminate any control characters

Code d

readLT();  // segregate sensors readings from the 16 digit string

Code e

void readLT() {
      String lux;
      for(int i = 0; i < 4; ++i) {
          lux = input.substring(i*4, (i*4)+4);
          // extract 4-digit sensor reading from received string 
          lcd.clear(); 
          lcd.setCursor(0,0);
          lcd.print("Sensor #");
          lcd.setCursor(8,0);
          lcd.print(i+1);
          lcd.setCursor(0,1);
          lcd.print(lux); 
          delay(2000);
          // 2 second delay to view the sensor reading on LCD
      }

Listing 2: Arduino Pro Mini

The main parts of Project 2's Sketch on the Arduino Pro Mini are shown here:

A. Declarations of global variables

B. Reading sensors data in succession into collection string then sending it to WeMos

C. LCD display code of collated readings

D. Tuned delay of looping through (B) & (C) to allow WeMos manipulation of sent readings

E. A variation of the light intensity reading function from Listing 1.

Code a

float sensor_value;
char readings[16];     // string accommodating the 4 sensors readings,
                  // each has 4 digits
float const AREF = 3.3;   // TEMT6000 is powered by 3.3V from WeMos D1 Mini 

Code b

sensor_value = analogRead(A0);     // Get raw sensor reading @ A0
dtostrf(readLux(),4,0,readings);    // Double converted to string
sensor_value = analogRead(A1);       // Get raw sensor reading @ A1
dtostrf(readLux(),4,0,readings+4);   // Double converted to string
sensor_value = analogRead(A2);      // Get raw sensor reading @ A2
dtostrf(readLux(),4,0,readings+8);   // Double converted to string
sensor_value = analogRead(A3);      // Get raw sensor reading @ A3
dtostrf(readLux(),4,0,readings+12);   // Double converted to string
Serial.print(readings);           // sending collated readings to WeMos

Code c

lcd.setCursor(0, 0);  
// print message  
lcd.print("Sensors Reading: ");
lcd.setCursor(0,1);
lcd.print(readings); // the 4 sensors readings are displayed in one line on LCD

Code d

delay(12000);   // 12 sec. delay between sensors readings to sync with WeMos
          // manipulation of current readings

Code e

float readLux(void) {
  float volts = sensor_value * AREF / 1024.0;  // Convert to voltage
  float amps = volts / 10000.0;   // Convert to amps across 10K resistor
  float microamps = amps * 1000000.0;  // Convert amps to microamps
  float lux = microamps * 2.0;      // 2 microamps = 1 lux, @ 3.3V max
  return lux;
}

Conclusion

In this article, I demonstrated two techniques that vary in complexity and payoff for extending the analog port of the infamous ESP8266 module.

Simple multiplexer IC did the job but consumed few digital ports in return.

Linking the module to another MCU board that is rich in analog ports resolved the dilemma while the complementary MCU added digital ports and processing power at the same time.

Glossary of Terms

Gamal has kindly provided a glossary of terms for those relevant to the above text.

CPU: Central Processing Unit

GPIO: General-Purpose Input/Output

PWM: Pulse Width Modulation

ADC: Analog to Digital Converter

I2C: Inter-Integrated Circuit

SPI: Serial Peripheral Interface

UART: Universal Asynchronous Receiver/Transmitter

WiFi: Wireless Fidelity

IoT: Internet of Things

Lux: A measure of luminance, the total amount of light that falls on a surface

ABOUT THE AUTHOR

Gamal Labib has been an enthusiast of embedded systems for two decades and currently a mentor (at codementor.io). He holds a MEng and a PhD in IT. Besides writing for technical magazines, he is a visiting associate professor at Egyptian universities, and a certified IT consultant.