Laboratory guide for IoT students on ESP32 programming using Espressif Development Kit: ESP32 WROVER development board and external sensors.
ESP32-WROVER-KIT
This board is double-sided, on one side you will find an LCD screen (so-called back) on the other side (front) there are GPIOs connectors you connect sensors and actuators to, TF (microSD) card slot with SPI reader and an RGB LED diode (among others).
The board is powered via USB cable from the PC computer (micro USB).
When you connect the board to the PC you will find two COM/Serial ports present in your system:
| Problem | Jumper/Header | Description |
|---|---|---|
| Development board won't power on even when the master power switch is on. | JP7 3) | This header enables you to power development board from the external, barrel connector. It is useful when you need to deliver more power to the board than USB can handle but not a case in our laboratory, so keep it short with USB enabled on, as on the image. When your board won't wake up, check this first. |
| Debugger is not working via 2nd COM port. | JP2 4) | This header redirects debugger into the COM-to-USB bridge. Keep those 4 jumpers short, unless you use external, JTAG, hardware debugger. Otherwise, you won't be able to use the debug feature. |
| Cannot program nor monitor the board via USB. | JP2 5) | This header enables COM-to-USB communication to the PC. Keep those 2 jumpers short any time, otherwise, you won't be able to program it nor use the serial monitor to observe results. |
This development board does not contain any environmental sensors so there is a 37 pcs sensor set that can be used when connected to the GPIOs of the development board, using F-F 2.54 cables. ESP32 exposes a number of GPIOs. Not all of them are “general purpose” - some are dedicated to handling analogue inputs while some others are dedicated to implementing communication protocols like I2C, SPI, software serial and so on. As this development board has some features like i.e. RGB LED and LCD display, many of the GPIOs are already used and even if exposes on the header, should not be used to connect sensors, i.e., if you connect a sensor to the LED dedicated GPIOs, LED, may disturb communication protocol and at least will flash when communication occurs. Opposite, your LCD display may not work correctly, if you connect a sensor to the GPIOs operating the LCD. A good practice is to use those GPIO lines that are not in use with the hardware integrated into the development board.
Following list present GPIOs used with the integrated hardware:
| No | Feature | ESP32 GPIO Pin | Notes |
|---|---|---|---|
| 1 | JTAG | GPIO12-15 | |
| 2 | Camera | GPIO0, GPIO4-5, GPIO18-19, GPIO21-23, GPIO25-27, GPIO34-36, GPIO39 | If you do not use the camera, feel free to use those GPIOs unless they're not shared with another feature. |
| 3 | RGB LED | GPIO0, GPIO2, GPIO4 | |
| 4 | MicroSD card reader | GPIO2, GPIO4, GPIO12-15, GPIO21 | |
| 5 | LCD | GPIO5, GPIO18-19, GPIO21-23, GPIO25 |
This is the temperature and humidity sensor, delivering calibrated (yet not very accurate) data in Celsius and % of the relative humidity of the air. This sensor uses a single data line, so by the power (GND, VCC 5V/3.3V), it requires single GPIO to operate.
This sensor uses a switch button so it simply shorts connection of the GPIO line, binary way. By the power, it requires single GPIO to operate. Note pinout on the button board. Power it from 3.3V (not from 5V!). Observe board for correct pinout. - (minus) stands for GND, S for signal from the button, remaining is +, note 3.3V, not 5V!
The built-in display is ILI9341 TFT display, 320×240 pixels (240×320 actually), RGB, 262k colours. Although it is possible to program it raw, we will use a ready software library to control it.
RGB led is composed of the three independent LEDs: red, green and blue, controlled via separate GPIOs. You can control the brightness of each LED (RGB component) via PWM.
Note it is different from the popular WS2812 LED chains, where each led is controlled digitally, using single GPIO and complex protocol and each LED has dedicated microcontroller.
PWM is frequently used to control analogue-style, efficient voltage on the GPIO pin. Instead of using resistance driver, PWM uses pulses to change effective power, delivered to the actuator. It applies to motors, but also to LEDs, bulbs, heaters and indirectly to the servos (but that works actually another way, see lectures).
Classical analogWrite method as known from Arduino (Uno, Mega) and ESP8266 does not work here.
ESP32 has sixteen (0 to 15) PWM channels (controllers) that can be freely bound to any of the regular GPIOs.
ESP32 can use various “resolutions” of the PWM: from 8 bit (0..255) to 15bit (0..32767) while regular Arduino uses only 8 bit one.
To use PWM in ESP32 one must perform the following steps:
OUTPUTFor the laboratory works, a good choice is to use Visual Studio Code with PlatformIO extension.
Under PlatformIO one need to install ESP32 platform (development framework) that comes with the ability to create and implement projects using Arduino-style and IDF-style. Following scenarios assume one uses Arduino-style source code design and platform, even, if we do develop for ESP32, not Arduino.
There is a need to install a number of libraries that will simplify work with external devices (sensors, actuators) but this varies among laboratory scenarios, so refer to the particular scenario for a list of required libraries.
The purpose of this laboratory is to configure your development environment, connect hardware and compile the first “Hello World” application.
You will use a serial port to trace and send your first Hello World message to the computer, then write it on the LCD screen.
Serial port tools are embedded into the ESP32 development framework, so SPI protocol implementation is. But to use an integrated LCD display, you need to install the following extra libraries:
It is also pretty common to include Arduino.h to let you use GPIO ids as in case of Arduino Uno in your main.cpp source code file:
...
#include <Arduino.h>
...
Task 1 - using the serial port to send Hello World periodically to your PC via COM port
Serial port communication is handy to trace (not DEBUG!) actions taken in your implementation. Here is, how to start:
setup() section of the code, start your serial communication providing speed (usually 9600 or 115200 but virtually any common serial port communication speed is OK here, but please note, you need o change it accordingly in VS Code Serial Monitor tool: default is 9600, so we suggest to use it).print or println functions.delay(x) where x is a time in milliseconds to temporary stop execution of your code. It is useful in case of the loop() function to let you be able to observe results.
Use the following code (excerpt) in your main.cpp to start serial communication on 9600 bps:
... void setup() { ... Serial.begin(9600); ... } ...
and then in the loop section:
... void loop() { ... Serial.println("Hello world"); ... delay(2000); ... }
Task 2 - counter
Add a counter to your loop, to present a number of times the loop has been executed, modulo 255.
Print this number to the serial port.
The desired result is: numbers from 0 to 255 should appear one number per line.
Adapt delay to speed it up!
Task 3 - controlling LEDs
Using a counter implemented as above, you will control RED (GREEN or BLUE) LED with PWM signal to change its brightness. Within the loop, use a counter to change the duty cycle from 0 to 100% (from 0 to 255 on 8-bit resolution). Use a 5kHz PWM signal.
This algorithm will increase the brightness of the LED from dark towards brighter and then back starts from 0.
Consider using the following configuration in your setup function:
... #define REDLED 0 ... const int freq = 5000; const int ledChannel = 0; const int resolution = 8; ... void setup() { ... pinMode(REDLED, OUTPUT); ... ledcSetup(ledChannel, freq, resolution); ledcAttachPin(REDLED,ledChannel); ...
and then to set PWM duty cycle, you may use:
... ledcWrite(ledChannel, dutycyclevalue); delay(15); ...
Task 4 - controlling LEDs #2
Now modify code above to use three controllers (one per LED) and provide a solution to pulse LEDs, one colour at a time:
RED from 0 to 255 and back to 0 then GREEN (0..255..0) and then BLUE and loop back to RED and so on.
Task 5 - write Hello World on the LCD screen.
Now, type textual “Hello World” on the screen. You're welcome to decorate it, change colours, size, orientation, etc.
To use LCD you must include libraries. Beginning of your main.cpp should then look more-less following way:
... #include "SPI.h" #include "Adafruit_GFX.h" #include "WROVER_KIT_LCD.h" ...
Then define a controller class that manages your LCD:
... WROVER_KIT_LCD tft; ...
Some useful functions to handle LCD operations are presented below (but not limited to):
... tft.fillScreen(WROVER_BLACK); //Clear screen tft.setCursor(0, 0); //Set upper left corner tft.setTextColor(WROVER_WHITE); //Set text colour tft.setTextSize(1); //Set font size tft.println("Hello World!"); // Print the text ...
** Task 6 ** - using hardware, GPIO interrupts
Handling external sensors that interface physics, like, i.e. button, requires asynchronous approach and hardware interrupts. One can implement easily a button handler that executes some code when the button is pressed. Interrupt handler routine should be kept as simple and quick, as possible, so here the approach is to set a “do action” flag, then handle it in the regular loop() code.
The task is to implement a code that handles asynchronously and implements bistable switch: the first click causes red led to switch on, next to switch it off and so on. Parallel, you should count the number of clicks and present them over the serial port (but only when button has been pressed, not continuously.
You can use virtually any switch, but for this lab, we will use a switch button module. Please note correct pinout. Connect - to board's GND, + to 3.3V (NOT 5V!) and S to the chosen GPIO port. Do not use GPIOs that are used by LED, LCD and JTAG even, if they're exposed on the connectors.
In the beginning section of your code, define constants, structures and singletons for interrupt handling.
... #define buttonGPIOPin 39 //sensor board is connected to the GPIO 39 #define REDLED 0 ...
Here the approach is to keep all data related to the handler and interrupt as a separate structure, but you may choose to use plain variables:
... struct ButtonPressHandler { int numberOfClicks; bool handleInterrupt; //A flag, showing there is a need to do the action bool bLEDIsOn; //Digital twin variable for the LED state. }; ButtonPressHandler bph1 = {0, false, false}; ...
Then declare a function that will be called, when an interrupt has been triggered. Keep this function shortest possible, not to block code execution in the main loop thus not to interfere and starve other tasks:
... void IRAM_ATTR InterruptHandler(){ bph1.handleInterrupt = true; bph1.numberOfClicks++; } ...
To bind the physical action and virtual one need to bind a handler to the interrupt, in the context of the GPIO (usually in the setup() section of the code).
... pinMode(buttonGPIOPin, INPUT_PULLUP); attachInterrupt(buttonGPIOPin, InterruptHandler, FALLING); ...
Please note the last argument of the binding function: FALLING - this one means that InterruptHandler will be executed when buttonGPIOPin changes its state from 1 to 0.
You will observe, however, that action is triggered actually when you push the button, not release it. The reason is the button board is constructed the way that pushing the button shorts it with GND, not Vcc thus works in opposite logic than expected.
There are 5 modes that define the moment the interrupt routine is executed:
Finally, in the loop() section of your source code, you need to handle all actions that should be taken, when the interrupt was triggered, testing interrupt flag that is set in the interrupt handling function, previously:
... if (bph1.handleInterrupt) { ... } ...
You may observe that for single button press an interrupt is triggered not once but many times. That is natural because of the physical switch connection oscillations. To avoid it, introduce the minimum time that is necessary between subsequent interrupt triggers. a function micros() will help you here.
micros() not millis() as ADC conversion is pretty quick and usually takes less than a ms.
** Task 7 ** - reading analog data
In this task, you will read analogue data via integrated ADCs.
Hardware part includes use of the “Joystick” sensor, composed of two potentiometers (one per axis) and a button (we do not use it here).
Please note correct pinout. Connect GND to board's GND, 5V to 3.3V (NOT 5V!) even if on-board print states 5V.!. Analogue resistance (voltage divider) goes through VRx and VRy.
ESP32 has 15 channels exposed (18 total) of the up to 12-bit resolution ADCs. Reading of the raw data (12-bit resolution is the default, 8 samples per measure as default) is easy using analogRead() function.
Technically, under the hood on the hardware level, there are two ADCs (ADC1 and ADC2). ADC 1 uses GPIOs 32 through 39. ADC2 GPIOs 0,2,4, 12-15 and 25-27. Note, ADC2 is used for WiFi purposes so you cannot use it when WiFi communication is enabled (so cannot you use GPIOs). Other limitations include use of GPIOs 0, 2, 4 an 15 in ESP WROVER KIT as mentioned above (those are already bound to other hardware).
Just execute analogRead(GPIO).
A number of useful functions is here (not limited to):
analogReadResolution(res) - where res is a value between 9 and 12 (default 12). For 9-bit resolution, you get 0..511 values, for 12-bit resolution, it is 0..4095 respectively. analogSetCycles(ccl) - where ccl is number of cycles per ADC sample. Default is 8, valid number is between 1 and 255.analogSetClockDiv(divider) - sets base clock divider for the ADC. That has an impact on the speed of conversion.analogSetAttenuation(a) and analogSetPinAttenuation(GPIO,a) - sets input attenuation (for all channels or for selected channel). Default is ADC_11db. This paremeter reflects dynamic scalling of the input value:ADC_0db - no attenuation (1V on input = 1088 reading on ADC), so full scale is 0..1.1VADC_2_5db - 1.34 (1V on input = 2086 reading on ADC), so full scale is 0..1.5VADC_6db - 1.5 (1V on input = 2975 reading on ADC), so full scale is 0..2.2VADC_11db - 3.6 (1V on input = 3959 reading on ADC), so full scale is 0..3.9VanalogRead(). As technically all channels use same two registers (ADC1 and ADC2) you need to give it some time to sample (i.e. delay(100) between consecutive reads on different channels).
During your exercise, you may start from informing ESP to start sampling on particular GPIOs, usually in the setup() function:
... #define VRxPIN 25 #define VRyPIN 26 ... void setup() { ... adcAttachPin(VRxPIN); adcAttachPin(VRyPIN); // here you can also change clock divider, number of cycles per sample and so on. ... }
Join this course on-line (you need to register a free account to participate in the course)
IoT course for Bachelor students