IOT-OPEN.EU Reloaded Consortium partners proudly present the 2nd edition of the Introduction to the IoT book. The complete list of contributors is juxtaposed below.
ITT Group
TalTech
Riga Technical University
Silesian University of Technology
Tallinn University of Technology
IT Silesia
Technical Correction
This book and its offshoots were prepared to provide a guide on how to use VREL NextGen Remote Access laboratories.
It contains a technical description of the hardware, a software guide, and hands-on labs with detailed scenarios for various levels of IoT students.
We (Authors) assume that a person willing to use VREL NextGen labs is already familiar with IoT on the engineering level: understands IoT concepts, knows IoT MCUs, sensors and actuators, understands communication principles and, most of all, has programming skills in C++ and Python.
This book is instantly updated online to catch up with the latest developments in software libraries and tools, so its printed edition is considered a collection edition only. Please always refer to the latest online version on Dokuwiki.
Enjoy the power of experiencing real hardware touch, even if done remotely, from your favourite study place.
This Book was implemented under the following projects:
Erasmus+ Disclaimer
This project has been funded with support from the European Commission.
This publication reflects the views only of the author, and the Commission cannot be held responsible for any use which may be made of the information contained therein.
Copyright Notice
This content was created by the IOT-OPEN.EU Reloaded Consortium 2022-2025.
The content is Copyrighted and distributed under CC BY-NC Creative Commons Licence, free for Non-Commercial use.
In case of commercial use, please get in touch with IOT-OPEN.EU Reloaded Consortium representative.
The VREL NextGen distant laboratory with remote access allows one to work with real hardware (not a simulator) and experience real engineering problems. The web browser is all that is needed to interface the lab. Programming the IoT devices is done in the browser, and firmware uploads, restarts, etc., are all handled in the web browser as well. The device's state can be observed directly via video streaming, almost in real-time, and indirectly via the network (figure 2). One or more devices can be booked exclusively for a user. While booked, it is unavailable for other users, but several devices exist. Booking time is limited. Users can choose which laboratory and device to access, then book and use it. Technical documentation and hands-on laboratory scenarios are integrated with the user interface.
The VREL NextGen solution comprises one or more hosting servers that provide a user interface via www and several laboratories with hardware located across Europe, currently in Poland, Estonia and Latvia. Public instances are announced via the https://iot-open.eu website. Besides public instances, there are private ones for consortium HE partner's students only. Those servers and related hardware (lab nodes) are not publicly accessible. Public instances sometimes also share resources with consortium HE partner's activities.
It is a rule of thumb that a single laboratory usually shares common space and services. One should refer to the technical description to understand capabilities and physical limitations. Scenarios requiring more than one device can be implemented locally within the limits of the single laboratory composed of many programmable IoT nodes or across space, transferring data through the internet. This enables virtually unlimited integration capabilities using physically separated devices worldwide and integrates other devices and solutions, such as cloud providers' services.
The following chapters provide a manual for software and hardware. Laboratory scenarios are provided as per laboratory. Integration and local services such as access points, gateways and related technical information (network configuration, credentials, etc) are described per laboratory in the hardware section.
VREL NextGen software is a web-based, integrated solution for both IoT software developers (Users/Students) and system administrators (Administrators, Super Administrators). It can be used in one of the three aforementioned roles.
There are many public and private instances for internal purposes of the Consortium HE and SME Members. Use of the system requires user registration with a valid email address. A front-page view is present in figure 3.
In the following chapters, there is a manual on how to use the system:
[pczekalski]Do a user's guide
[pczekalski]Do an admin's guide The system has two kinds of administrators: the Super Admin, a built-in account, and any number of Laboratory Admins. To understand activities and relations, it is essential to recognise system-building components (figure {ref>vrelsoftware2}).
The Super Admin can create new Laboratory Admins by promoting the regular user's account or explicitly creating a new one.
The Super Admin can also create a laboratory (a group of administrators) and assign Administrator's (Administrators') accounts to it, remove admins and manage individual Users.
The Admin's role is to configure the system per laboratory and manage cohorts of students (Users) and individual devices. There can be many Admins in the system, and each can manage their own Users, Laboratories (called Group of Devices) and individual Devices.
Regular Admin has several scenarios:
Below there are some hints and important information:
Device configuration is the most complex part of the administration process. It requires the correct configuration of the end node proxy service regarding the specification of the target IoT device it manages. It is a compilation service (figure 4) that executes compilation commands and execution.
The device configuration supports several configuration parameters, many of which may be redundant or unnecessary, to ensure flexibility among different IoT hardware.
Commands to compile and upload firmware (or configuration) are Admin-defined. Besides common configuration parts such as name, location, and description, there are sections for:
Each laboratory node is equipped with an ESP32-S3 double-core chip. Several peripherals, networks and network services are available for the user. The UI is necessary to observe results in the camera when programming remotely. Thus, a proper understanding of UI programming is essential to successfully using the devices.
The table 1 lists all hardware components of the SUT's ESP32-S3 node and hardware details such as connectivity, protocols, GPIOs, etc. Please note that some pins overlap because buses such as SPI and I2C are shared among multiple components.
The node is present in the figure 5 and reference numbers reflecting components in the table 1.
The MCU standing behind the laboratory node is a genuine ESP32-S3-DevKitM-1-N8 from Espressif [1], present in figure 6:
A suitable platformio.ini file for the correct code compilation is presented below. It does not contain libraries that need to be added regarding specific tasks and hardware used in particular scenarios. The code below presents only the typical section. Refer to the scenario description for details regarding case-specific libraries needed for the implementation:
[env:esp32] platform = espressif32 board = esp32-s3-devkitc-1 board_build.mcu = esp32s3 board_build.f_cpu = 240000000L framework = arduino platform_packages = toolchain-riscv32-esp @ 8.4.0+2021r2-patch5 lib_ldf_mode = deep+
Figure 7 represents SUT's VREL Next Gen IoT remote lab networking infrastructure and services. Details are described below.
If you're a SUT student or can access the campus network, you can also use 157.158.56.0/24 addresses.
The WiFi network, separated (no routing to and from the Internet) for IoT experimentation is available for all nodes:
A public, wired (157.158.56.0/24) network is available only for on-site students and from the SUT's campus network.
It is important to distinguish the network context and use the correct address. Integration services usually have two interfaces: one is available from the IoT WiFi network so nodes can access it, and the other IP address (from the public campus network) is available only for students directly connected to it.
There are currently two application layer services available:
coap://<ipaddress>/
that brings you a secret code in the message's payload,
coap://<ipaddress>/hello
that brings you a hello world welcome message in the payload.
Know the hardware
The following scenarios explain the use of hardware components and services that constitute the laboratory node. It is intended to seamlessly introduce users to IoT scenarios where using sensors and actuators is an intermediate step, and the main goal is to use networking and communication. Besides IoT, those scenarios can be utilised as a part of the Embedded Systems Modules.
Advanced techniques
In the following scenarios, we will focus on advanced programming techniques, such as asynchronous programming and timers.
IoT programming
In the following scenarios, you will write programs interacting with other devices, services, and networks, which are pure IoT applications.
Alphanumerical LCD is one of the most popular output devices in the Embedded and IoT. Using LCD with predefined line organisation (here, 2 lines, 16 characters each) is as simple as sending a character's ASCII code to the device. This is so much simpler than in the case of the use of dot-matrix displays, where it is necessary to use fonts. The fixed organisation LCD has limits; here, only 32 characters can be presented to the user simultaneously. ASCII presents a limited set of characters, but many LCDs can redefine character maps (how each letter, digit or symbol looks). This way, it is possible to introduce graphics elements (i.e. frames), special symbols and letters.
In this scenario, you will learn how to handle easily LCD to present information and retrieve it visually with a webcam.
Familiarise yourself with a hardware reference: this LCD is controlled with 6 GPIOs as presented in the “Table 1: ESP32-S3 SUT Node Hardware Details” on the hardware reference page.
You are going to use a library to handle the LCD. It means you need to add it to your platformio.ini
file. Use the template provided in the hardware reference section and extend it with the library definition:
lib_deps = adafruit/Adafruit LiquidCrystal@^2.0.2
Draw “Hello World” in the upper line of the LCD and “Hello IoT” in the lower one.
Check if you can see a full LCD in your video stream. Book a device and create a dummy Arduino file with void setup()…
and void loop()…
.
= Step 1 = Include the library in your source code:
#include <Adafruit_LiquidCrystal.h>
= Step 2 = Declare GPIOs controlling the LCD, according to the hardware reference:
#define LCD_RS 2 #define LCD_ENABLE 1 #define LCD_D4 39 #define LCD_D5 40 #define LCD_D6 41 #define LCD_D7 42
= Step 3 = Declare a static instance of the LCD controller class and preconfigure it with appropriate control GPIOs:
static Adafruit_LiquidCrystal lcd(LCD_RS, LCD_ENABLE, LCD_D4, LCD_D5, LCD_D6, LCD_D7);
= Step 4 = Initialise class with display area configuration (number of columns, here 16 and rows, here 2):
lcd.begin(16,2);
= Step 5 = Implement your algorithm. The most common class methods that will help you are listed below:
.clear()
- clears all content;.setCursor(x,y)
- set cursor, writing will start there;.print(contents)
- prints text in the cursor location; note there are many overloaded functions, accepting various arguments, including numerical.You should be able to see “Hello World” and “Hello IoT” on the LCD now.
VREL NExtGen laboratory node is equipped with b/w, ePaper module. It is a dot matrix display with a native resolution of 250×122 pixels. It has 64kB display memory and is controlled via SPI. The ePaper display presents data even if powered off, so don't be surprised that finishing your application does not automatically clean up the display, even if you use some other code later. To clean up the display, one has to clear the screen explicitly.
Familiarise yourself with a hardware reference: this ePaper is controlled with 6 GPIOs as presented in the “Table 1: ESP32-S3 SUT Node Hardware Details” on the hardware reference page.
You are going to use a library to handle the ePaper drawing. It means you need to add it to your platformio.ini
file. Use the template provided in the hardware reference section and extend it with the library definition:
lib_deps = zinggjm/GxEPD2@^1.5.0
To generate an array of bytes representing an image, it is easiest to use an online tool, e.g.:
Present an image on the screen and overlay the text “Hello World” over it.
Check if you can see a full ePaper Display in your video stream. Book a device and create a dummy Arduino file with void setup()…
and void loop()…
.
Prepare a small bitmap (e.g. 60×60 pixels) and convert it to the byte array with b/w settings.
Sample project favicon you can use is present in Figure 9:
Remember to include the source array in the code when drawing an image.
The corresponding generated C array for the logo in Figure 9 (horizontal 1 bit per pixel, as suitable for ePaper Display) is present below:
// 'logo 60', 60x60px const unsigned char epd_bitmap_logo_60 [] PROGMEM = { 0xff, 0xff, 0xff, 0x80, 0x1f, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xf8, 0x00, 0x01, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0xff, 0xff, 0x01, 0xff, 0xf8, 0x0f, 0xff, 0xf0, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0x03, 0xff, 0xf0, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xc1, 0xff, 0xf0, 0xff, 0xe0, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xf0, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xf0, 0xff, 0x87, 0xff, 0xf0, 0xff, 0xfe, 0x1f, 0xf0, 0xff, 0x0f, 0xfe, 0x00, 0x07, 0xff, 0x0f, 0xf0, 0xfe, 0x1f, 0xf8, 0x7f, 0xe1, 0xff, 0x87, 0xf0, 0xfc, 0x3f, 0xe3, 0xff, 0xfc, 0x7f, 0xc3, 0xf0, 0xfc, 0x7f, 0x8f, 0xff, 0xff, 0x1f, 0xe3, 0xf0, 0xf8, 0xff, 0x3f, 0xff, 0xff, 0xcf, 0xf1, 0xf0, 0xf1, 0xfe, 0x7f, 0xff, 0xff, 0xe7, 0xf8, 0xf0, 0xf1, 0xfc, 0xff, 0xff, 0xff, 0xf3, 0xf8, 0xf0, 0xe3, 0xf9, 0xff, 0xfc, 0x7f, 0xf9, 0xfc, 0x70, 0xe3, 0xf3, 0xff, 0xfc, 0x0f, 0xfc, 0xfc, 0x70, 0xc7, 0xf7, 0xff, 0xff, 0xc3, 0xfe, 0xfe, 0x30, 0xc7, 0xe7, 0xff, 0xff, 0xf1, 0xfe, 0x7e, 0x30, 0xcf, 0xef, 0xff, 0xff, 0xfc, 0xff, 0x7f, 0x30, 0x8f, 0xcf, 0xff, 0xff, 0xfe, 0x7f, 0x3f, 0x10, 0x8f, 0xdf, 0xff, 0xff, 0xff, 0x3f, 0xbf, 0x10, 0x9f, 0x9f, 0xff, 0xff, 0xff, 0x3f, 0x9f, 0x90, 0x9f, 0x9f, 0xff, 0xff, 0xff, 0x9f, 0x9f, 0x90, 0x1f, 0xbf, 0xff, 0xff, 0xff, 0x9f, 0xdf, 0x80, 0x1f, 0xbf, 0xff, 0xf9, 0xff, 0xdf, 0xdf, 0x80, 0x1f, 0xbf, 0xff, 0xe0, 0x7f, 0xcf, 0xdf, 0x80, 0x1f, 0x3f, 0xff, 0xe0, 0x7f, 0xcf, 0xcf, 0x80, 0x1f, 0x3f, 0xff, 0xc0, 0x3f, 0xcf, 0xcf, 0x80, 0x1f, 0xff, 0xff, 0xc0, 0x3f, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xf0, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xf0, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xf0, 0x8f, 0xff, 0xff, 0xf9, 0xfc, 0x03, 0xf0, 0x10, 0x8f, 0xff, 0xff, 0xf9, 0xf8, 0x01, 0xe0, 0x10, 0xcf, 0xff, 0xff, 0xf9, 0xf0, 0xf0, 0xf9, 0xf0, 0xc7, 0xff, 0xff, 0xf9, 0xf3, 0xfc, 0xf9, 0xf0, 0xc7, 0xff, 0xff, 0xf9, 0xe3, 0xfc, 0x79, 0xf0, 0xe3, 0xff, 0xff, 0xf9, 0xe3, 0xfc, 0x79, 0xf0, 0xe3, 0xff, 0xff, 0xf9, 0xe3, 0xfc, 0x79, 0xf0, 0xf1, 0xff, 0xff, 0xf9, 0xe3, 0xfc, 0x79, 0xf0, 0xf1, 0xff, 0xff, 0xf9, 0xf3, 0xfc, 0x79, 0xf0, 0xf8, 0xff, 0xff, 0xf9, 0xf1, 0xf8, 0xf9, 0xf0, 0xfc, 0x7f, 0xff, 0xf9, 0xf8, 0x61, 0xf8, 0xc0, 0xfc, 0x3f, 0xff, 0xf9, 0xfc, 0x03, 0xf8, 0x00, 0xfe, 0x1f, 0xff, 0xf9, 0xff, 0x0f, 0xfe, 0x10, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0x01, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00 }; // Total bytes used to store images in PROGMEM = 496 const int epd_bitmap_allArray_LEN = 1; const unsigned char* epd_bitmap_allArray[1] = { epd_bitmap_logo_60 };
= Step 1 = Include necessary libraries.
#include <SPI.h> #include <GxEPD2.h> #include <GxEPD2_BW.h> //Fonts #include <Fonts/FreeMonoBold12pt7b.h>
The code above also includes a font to draw text on the ePaper Display. There are many fonts one can use, and a non-exhaustive list is present below (files are located in the Adafruit GFX Library
, subfolder Fonts
):
FreeMono12pt7b.h FreeMono18pt7b.h FreeMono24pt7b.h FreeMono9pt7b.h FreeMonoBold12pt7b.h FreeMonoBold18pt7b.h FreeMonoBold24pt7b.h FreeMonoBold9pt7b.h FreeMonoBoldOblique12pt7b.h FreeMonoBoldOblique18pt7b.h FreeMonoBoldOblique24pt7b.h FreeMonoBoldOblique9pt7b.h FreeMonoOblique12pt7b.h FreeMonoOblique18pt7b.h FreeMonoOblique24pt7b.h FreeMonoOblique9pt7b.h FreeSans12pt7b.h FreeSans18pt7b.h FreeSans24pt7b.h FreeSans9pt7b.h FreeSansBold12pt7b.h FreeSansBold18pt7b.h FreeSansBold24pt7b.h FreeSansBold9pt7b.h FreeSansBoldOblique12pt7b.h FreeSansBoldOblique18pt7b.h FreeSansBoldOblique24pt7b.h FreeSansBoldOblique9pt7b.h FreeSansOblique12pt7b.h FreeSansOblique18pt7b.h FreeSansOblique24pt7b.h FreeSansOblique9pt7b.h FreeSerif12pt7b.h FreeSerif18pt7b.h FreeSerif24pt7b.h FreeSerif9pt7b.h FreeSerifBold12pt7b.h FreeSerifBold18pt7b.h FreeSerifBold24pt7b.h FreeSerifBold9pt7b.h FreeSerifBoldItalic12pt7b.h FreeSerifBoldItalic18pt7b.h FreeSerifBoldItalic24pt7b.h FreeSerifBoldItalic9pt7b.h FreeSerifItalic12pt7b.h FreeSerifItalic18pt7b.h FreeSerifItalic24pt7b.h FreeSerifItalic9pt7b.h
= Step 2 = Declare GPIOs and some configurations needed to handle the ePaper display properly:
#define GxEPD2_DRIVER_CLASS GxEPD2_213_BN #define GxEPD2_DISPLAY_CLASS GxEPD2_BW #define USE_HSPI_FOR_EPD #define ENABLE_GxEPD2_GFX 0 #define SPI_SCLK_PIN 18 #define SPI_MOSI_PIN 15 #define EPAPER_SPI_DC_PIN 13 #define EPAPER_SPI_CS_PIN 10 #define EPAPER_SPI_RST_PIN 9 #define EPAPER_BUSY_PIN 8 #define SCREEN_WIDTH 250 #define SCREEN_HEIGHT 122 #define MAX_DISPLAY_BUFFER_SIZE 65536ul #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
= Step 3 = Declare hardware SPI controller and ePaper display controller:
static SPIClass hspi(HSPI); static GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ EPAPER_SPI_CS_PIN, /*DC=*/ EPAPER_SPI_DC_PIN, /*RST=*/ EPAPER_SPI_RST_PIN, /*BUSY=*/ EPAPER_BUSY_PIN));
You can also declare a message to display as an array of characters:
static const char HelloWorld[] = "Hello IoT!";
= Step 4 = Initialise SPI and, on top of that, the ePaper controller class:
hspi.begin(SPI_SCLK_PIN, -1, SPI_MOSI_PIN, -1); delay(100); pinMode(EPAPER_SPI_CS_PIN, OUTPUT); pinMode(EPAPER_SPI_RST_PIN, OUTPUT); pinMode(EPAPER_SPI_DC_PIN, OUTPUT); pinMode(EPAPER_BUSY_PIN,INPUT_PULLUP); delay(100); digitalWrite(EPAPER_SPI_CS_PIN,LOW); display.epd2.selectSPI(hspi, SPISettings(4000000, MSBFIRST, SPI_MODE0)); delay(100); display.init(115200); digitalWrite(EPAPER_SPI_CS_PIN,HIGH);
= Step 5 = Set display rotation, font and text colour:
digitalWrite(EPAPER_SPI_CS_PIN,LOW); display.setRotation(1); display.setFont(&FreeMonoBold12pt7b); display.setTextColor(GxEPD_BLACK);
then get the external dimensions of the string to be printed:
int16_t tbx, tby; uint16_t tbw, tbh; display.getTextBounds(HelloWorld, 0, 0, &tbx, &tby, &tbw, &tbh); uint16_t x = ((display.width() - tbw) / 2) - tbx; uint16_t y = ((display.height() - tbh) / 2) - tby;
= Step 6 = Then display contents of the image and the text in the ePaper display:
display.setFullWindow(); display.firstPage(); do { display.drawImage((uint8_t*)epd_bitmap_logo_60,0,0,60,60); display.setCursor(x, y); display.print(HelloWorld); } while (display.nextPage()); digitalWrite(EPAPER_SPI_CS_PIN,HIGH);
You should be able to see an image and a text on the ePaper Display.
This scenario presents how to use the OLED display. Our OLED display is an RGB (16bit colour, 64k colours) 1.5in, 128×128 pixels. The OLED chip is SSD1351, and it is controlled over the SPI interface using the following pin configuration:
As usual, there is no need to program SPI directly; instead, it should be handled by a dedicated library. In addition to the protocol communication library and display library, we will use a graphic abstraction layer for drawing primitives such as lines, images, text, circles, and so on:
lib_deps = adafruit/Adafruit SSD1351 library@^1.2.8
Note that the graphics abstraction library (Adafruit GFX) is loaded automatically because of the
lib_ldf_mode = deep+
declaration in the platformio.ini
. You can also add it explicitly, as below:
lib_deps = adafruit/Adafruit SSD1351 library@^1.3.2 adafruit/Adafruit GFX Library@^1.11.9
To generate an array of bytes representing an image in 565 format, it is easiest to use an online tool, e.g.:
Draw a text on the OLED display and an image of your choice (small, to fit both text and image).
Perhaps you will need to use an external tool to preprocess an image to the desired size (we suggest something no bigger than 100×100 pixels) and another tool (see hint above) to convert an image to an array of bytes.
Check if you can see a full OLED Display in your video stream. Book a device and create a dummy Arduino file with void setup()…
and void loop()…
.
Prepare a small bitmap and convert it to the byte array for 16-bit colour settings.
Sample project favicon you can use is present in Figure 9:
Remember to include the source array in the code when drawing an image. The corresponding generated C array for the logo in Figure 11 is too extensive to present here in the textual form, so below it is just the first couple of pixels represented in the array, and full contents you can download here: ZIPed archive with a C file containing all pixel data of the image .
const uint16_t epd_bitmap_logo_60 [] PROGMEM = { 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xf7be, 0xbdd7, 0x8430, 0x5aeb, 0x39c7, 0x2104, 0x1082, 0x0020, 0x0020, 0x1082, 0x2104, 0x39c7, 0x5aeb, 0x8430, 0xbdd7, 0xf7be, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, .... .... 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 }; // Array of all bitmaps for convenience. (Total bytes used to store images in PROGMEM = 3616) const int epd_bitmap_allArray_LEN = 1; const uint16_t* epd_bitmap_allArray[1] = { epd_bitmap_logo_60 };
= Step 1 = Include necessary libraries:
#include <Arduino.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1351.h> #include <SPI.h> //Fonts #include <Fonts/FreeMono9pt7b.h>
The code above also includes a font to draw text on the OLED Display. There are many fonts one can use, and a non-exhaustive list is present below (files are located in the Adafruit GFX Library
, subfolder Fonts
):
FreeMono12pt7b.h FreeMono18pt7b.h FreeMono24pt7b.h FreeMono9pt7b.h FreeMonoBold12pt7b.h FreeMonoBold18pt7b.h FreeMonoBold24pt7b.h FreeMonoBold9pt7b.h FreeMonoBoldOblique12pt7b.h FreeMonoBoldOblique18pt7b.h FreeMonoBoldOblique24pt7b.h FreeMonoBoldOblique9pt7b.h FreeMonoOblique12pt7b.h FreeMonoOblique18pt7b.h FreeMonoOblique24pt7b.h FreeMonoOblique9pt7b.h FreeSans12pt7b.h FreeSans18pt7b.h FreeSans24pt7b.h FreeSans9pt7b.h FreeSansBold12pt7b.h FreeSansBold18pt7b.h FreeSansBold24pt7b.h FreeSansBold9pt7b.h FreeSansBoldOblique12pt7b.h FreeSansBoldOblique18pt7b.h FreeSansBoldOblique24pt7b.h FreeSansBoldOblique9pt7b.h FreeSansOblique12pt7b.h FreeSansOblique18pt7b.h FreeSansOblique24pt7b.h FreeSansOblique9pt7b.h FreeSerif12pt7b.h FreeSerif18pt7b.h FreeSerif24pt7b.h FreeSerif9pt7b.h FreeSerifBold12pt7b.h FreeSerifBold18pt7b.h FreeSerifBold24pt7b.h FreeSerifBold9pt7b.h FreeSerifBoldItalic12pt7b.h FreeSerifBoldItalic18pt7b.h FreeSerifBoldItalic24pt7b.h FreeSerifBoldItalic9pt7b.h FreeSerifItalic12pt7b.h FreeSerifItalic18pt7b.h FreeSerifItalic24pt7b.h FreeSerifItalic9pt7b.h
= Step 2 = Add declarations for GPIOs, colours (to ease programming and use names instead of hexadecimal values) and screen height and width. To recall, the OLED display in our lab is square: 128×128 pixels, 16k colours (16-bit 565: RRRRRGGGGGGBBBBB colour model):
//Test configuration of the SPI #define OLED_SPI_MOSI_PIN 15 //DIN #define OLED_SPI_SCLK_PIN 18 //CLK #define OLED_SPI_CS_PIN 11 #define OLED_SPI_DC_PIN 13 #define OLED_SPI_RST_PIN 12 #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 128 // Color definitions #define BLACK 0x0000 #define BLUE 0x001F #define RED 0xF800 #define GREEN 0x07E0 #define CYAN 0x07FF #define MAGENTA 0xF81F #define YELLOW 0xFFE0 #define WHITE 0xFFFF
= Step 3 = Declare an SPI communication and OLED controller objects:
static SPIClass hspi(HSPI); static Adafruit_SSD1351 tft = Adafruit_SSD1351(SCREEN_WIDTH, SCREEN_HEIGHT, &hspi, OLED_SPI_CS_PIN, OLED_SPI_DC_PIN, OLED_SPI_RST_PIN);
= Step 4 = Initialise the SPI communication object and the OLED controller object. Then clear the screen (write all black):
pinMode(OLED_SPI_CS_PIN, OUTPUT); hspi.begin(OLED_SPI_SCLK_PIN, -1, OLED_SPI_MOSI_PIN, -1); delay(50); digitalWrite(OLED_SPI_CS_PIN,LOW); tft.begin(); delay(100); tft.fillScreen(BLACK);
= Step 5 =
Draw a bitmap around the centre part of the screen (screen is 128x128px); please mind that OLED_SPI_CS_PIN
must be LOW
(OLED SPI device controller selected) before executing the following code:
tft.drawRGBBitmap(48,48, epd_bitmap_logo_60, 60, 60);
= Step 6 = Drop some additional text on the screen:
tft.setFont(&FreeMono9pt7b); tft.setTextSize(1); tft.setTextColor(WHITE); tft.setCursor(0,10); tft.println("Hello IoT");
Some remarks regarding coordinates:
setFont
sets the base font later used for printing. The font size is given in the font name, so in the case of the FreeMono9pt7b
, the base font size is 9 pixels vertically,setTextSize
sets a relative font scaling; assuming the base font is 9 pixels, setTextSize(2)
will scale it up to 200% (18 pixels); there is no fractal calling here :(,setTextColor
controls the colour of the text: as we have a black screen (fillScreen(BLACK)
), we will use white here, but any other colour is valid,setCursor(X,Y)
sets the text location; note the upper-left corner is 0.0, but that relates to the lower-left corner of the first letter. So, to write in the first line, you need to offset it down (Y-coordinate) by at least font size (relative, also regarding text size calling, if any).println(…)
to print the text is very handy as once executed, setCursor
is automatically called to set the cursor in the next line so you can continue printing in a new line without a need to set the cursor's position explicitly. Use print(…)
to continue printing in the current line.
Besides the functions presented above, the controller class has several other handy functions (among others):
drawPixel(x,y, colour)
draws a pixel in x,y
coordinates of the colour
colour,drawCircle(x,y, radius, colour)
draws a circle in x,y
coordinates with colour colour
and specified radius
(in pixels),drawLine(x1,y1, x2,y2, colour)
draws a line starting from x1,y1
and finished in x2,y2
with given colour
- to draw straight (horizontal or vertical) lines there is a faster option:drawFastHLine(x,y, w, colour)
draws horizontal line that starts from x,y
and of the length w
with given colour
,drawFastVLine(x,y, h, colour)
draws vertical line that starts from x,y
and of the length h
with given colour
,drawRect(x,y, w,h, colour)
draws a rectange starting in x,y
of the width and height w
and h
and with given colour
(no fill),drawTriangle(x1,y1, x2,y2, x3,y3, colour)
draws a triangle using 3 vertexes and of given colour (no fill),You should see the image and the text in the video stream.
The screen is black even if I write to it. What to do?: Check if you have initialised an SPI communication object and pulled the “chip select” GPIO down to LOW before drawing. Follow the code example in this manual: it does work!
A Smart LED stripe (also referenced as Digital LED or NEOPIXEL) is a chain of connected LEDs, commonly RGB, but other combinations such as RGBWW (Red+Green+Blue+Warm White+Cold White) or WWA (Warm White+Cold White+Amber) exist. They are controlled with just one pin/GPIO. GPIO drives a first LED in a chain and the LED relays configuration to the next one, and so on.
The most common type of LED stripes is WS2812B (RGB). Initially LED Stripes were powered with 5V, but that limits the length of the chain up to some 1-2m (because of the voltage drop), so nowadays LED stripes powered with 12V and even 24V are getting more and more popular.
In this scenario you will learn how to control a small LED RGB Stripe, composed of 8 Smart (Digital) LEDs.
Familiarise yourself with a hardware reference: this LED Stripe is controlled with a single GPIO (GPIO 34), as presented in the “Table 1: ESP32-S3 SUT Node Hardware Details” on the hardware reference page. To control a WS2812B LED stipe, we will use a library:
lib_deps = freenove/Freenove WS2812 Lib for ESP32@^1.0.5
There are at least two ways (algorithms) to implement this task:
delay(…);
) calls in the void loop();
and,Implement a rainbow of colours flowing through the LED Stripe.
When booking a device, ensure the LED stripe is visible in the camera.
Below, we provide a sample colour configuration that does not reflect the desired effect that should result from the exercise. It is just for instructional purposes. You need to invent how to implement a rainbow effect yourself. = Step 1 = Include necessary library:
#include "Freenove_WS2812_Lib_for_ESP32.h"
= Step 2 = Declare configuration and a controller class:
#define WLEDS_COUNT 8 #define WLEDS_PIN 34 #define WLEDS_CHANNEL 0 static Freenove_ESP32_WS2812 stripe = Freenove_ESP32_WS2812(WLEDS_COUNT, WLEDS_PIN, WLEDS_CHANNEL, TYPE_GRB);
= Step 3 = To switch a particular LED, use the following function:
stripe.setLedColorData(1,60,0,0); //light Red of the 2nd LED stripe.show(); //Writes colours to the LED stripe
Parameters are: setLedColorData(int index, u8 r, u8 g, u8 b);
.
If you want to set all LEDs in the stripe to the same colour, there is a handy function: setAllLedsColorData(u8 r, u8 g, u8 b);
.
Remember to use the show();
function afterwards.
Observe the flow of the colours via the stripe.
I cannot see the colour via the video camera - everything looks white…: Try to lower the LED's brightness.
— MISSING PAGE — — MISSING PAGE —
You will learn how to control a standard miniature servo in this scenario. Standard miniature, so-called “analogue” servo is controlled with a PWM signal of 50Hz with a duty cycle between 1 ms (rotate to 0) and 2 ms (rotate to 180 degrees), where 1.5 ms corresponds to 90 degrees.
A servo has a red arrow presenting the gauge's current position.
The servo is an actuator. It requires a time to operate. So, you should give it time to operate between consecutive changes of the control PWM signal (requests to change its position). Moreover, because of the observation via camera, too quick rotation may not be observable at all depending on the video stream fps. A gap of 2s between consecutive rotations is usually a reasonable choice.
To ease servo control, instead of use of ledc
we will use a dedicated library:
ib_deps = dlloydev/ESP32 ESP32S2 AnalogWrite@^5.0.2
This library requires minimum setup but, on the other hand, supports, i.e. fine-tuning of the minimum and maximum duty cycle as some servos tend to go beyond 1ms and above 2ms to achieve a full 180-degree rotation range. It is usually provided in the technical documentation accompanying the servo.
Rotate the servo to the following angles: 0, 90, 180, 135, 45 and back to 0 degrees.
Check if the servo is in the camera view. The servo is controlled with GPIO 37.
Write your application all in the setup()
function, leaving loop()
empty.
= Step 1 = Include servo control library, specific for ESP32 and declare GPIO, minimum and maximum duty cycle values:
#include <Servo.h> #define SRV_PIN 37
MG 90 servos that we use in our lab are specific. As mentioned above, to achieve a full 180-degree rotation range, their minimum and maximum duty cycle timings go far beyond standards. Here, we declare minimum and maximum values for the duty cycle (in microseconds) and a PWM control channel (2):
#define PWMSRV_Ch 2 #define srv_min_us 550 #define srv_max_us 2400
= Step 2 = Define a servo controller object:
static Servo srv;
= Step 3 = Initialise parameters (duty cycle, channel, GPIO):
srv.attach(SRV_PIN, PWMSRV_Ch,srv_min_us, srv_max_us);
50Hz frequency is standard, so we do not need to configure it.
= Step 4 = Rotating a servo is as easy as writing the desired angle to the controller class, e.g.:
srv.write(SRV_PIN,180); delay(2000);
How do I know minimum and maximum values for the timings for servo operation?: Those parameters are provided along with servo technical documentation, so you should refer to them. Our configuration reflects the servos we use (MG90/SG90), and as you can see, it goes far beyond the standard servo rotation control that is a minimum of 1000us and a maximum of 2000us. Using standard configuration, your servo won't rotate at full 180 degrees but at a shorter rotation range.
Observe the red arrow to rotate accordingly. Remember to give the servo some time to operate.
— MISSING PAGE — — MISSING PAGE —
Digital potentiometer DS1803 is an I2C-controlled device that can digitally control the potentiometer.
Opposite to the physical potentiometers, there are no movable parts.
DS1803 has two digital potentiometers controlled independently. We use just one with the lower cardinal number (index 0). In our example, it is a 100k spread between GND and VCC, and its output is connected to the ADC (analogue to digital converter) input of the ESP32 MCU. This way, the potentiometer's wiper is controlled remotely via the I2C bus.
The device's I2C address is 0x28, and the ADC input GPIO pin is 7.
The digital potentiometer in our laboratory node forms then a loopback device: it can be set (also read) via I2C, and the resulting voltage can be measured on the separate PIN (ADC) 15. This way, it is possible, e.g. to draw a relation between the potentiometer setting and ADC readings to check whether it is linear or forms some other curve.
Reading of the ADC is possible using the regular analogRead(pin)
function.
To implement this scenario, it is advised to get familiar with at least one of the following scenarios first:
They enable you to present the data on the display (i.e. readings).
To handle communication with the DS1803 digital potentiometer, we use bare I2C programming. For this reason, we need to include only the I2C protocol library:
#include <Wire.h>
Below, we present a sample control library that you need to include in your code:
enum POT_LIST {POT_1 = 0xA9, POT_2=0xAA, POT_ALL=0xAF}; //We have only POT_1 connected typedef enum POT_LIST POT_ID; //Prototypes void setPotentiometer(TwoWire& I2CPipe, byte potValue, POT_ID potNumber); byte readPotentiometer(TwoWire& I2CPipe, POT_ID potNumber); //Implementation void setPotentiometer(TwoWire& I2CPipe, byte potValue, POT_ID potNumber) { I2CPipe.beginTransmission(DS1803_ADDRESS); I2CPipe.write(potNumber); I2CPipe.write(potValue); I2CPipe.endTransmission(true); }; byte readPotentiometer(TwoWire& I2CPipe, POT_ID potNumber) //reads selected potentiometer { byte buffer[2]; I2CPipe.requestFrom(DS1803_ADDRESS,2); buffer[0]=I2CPipe.read(); buffer[1]=I2CPipe.read(); return (potNumber==POT_1?buffer[0]:buffer[1]); };
readPotentiometer(…)
function returns a value previously set to the digital potentiometer, not an actual ADC voltage reading! It returns a set value by setPotentiometer(…)
, which is on the “digital” side of the DS1803 device. Actual ADC reading can be obtained using analogRead(pin)
.
Iterate over the potentiometer settings, read related voltage readings via ADC, and present them in graphical form (as a plot). As the maximum resolution is 256, you can use a plot of 256 points or any other lower value covering all ranges. Present graph (plot) on either ePaper or OLED display, and while doing the readings, you should present data in the LCD (upper row for a set value, lower for a reading of the ADC).
Check if you can see all the displays. Remember to use potentiometer 1 (index 0) because only this one is connected to the ADC input of the ESP32 MCU. In these steps, we present only how to handle communication with a digital potentiometer and how to read the ADC input of the MCU. Methods for displaying the measurements and plotting the graph are present in other scenarios. Remember to include the functions above in your code unless you want to integrate them with your solution.
Below, we assume that you have embedded functions handling operations on the digital potentiometer as defined above in your source file. Remember to add Wire.h
include!
= Step 1 = Define I2C bus GPIOs: clock (SCL) uses GPIO 4 and data (SDA) GPIO 5. ADC uses GPIO 7. Digital potentiometer chip DS1803 uses 0x28 I2C address. All definitions are present in the following code:
#define SCL 4 #define SDA 5 #define POT_ADC 7 #define DS1803_ADDRESS 0x28
= Step 2 = Declare an array of readings that fits an OLED display. Adjust for ePaper resolution (horizontal) if using it. OLED is 128×128 pixels:
static int16_t aGraphArray[128];
= Step 3 = Include functions present in the PREREQUISITES section. = Step 4 = Initialise the I2C bus and configure ADC's GPIO as input:
Wire.begin(SDA,SCL); delay(100); pinMode(POT_ADC, INPUT);
= Step 4 = Read the loopback characteristics of the digital potentiometer to ADC loop and store it in the array:
for(byte i=0; i<128; i++) { setPotentiometer(I2CPipe, 2*i, POT_1); aGraphArray[i]=analogRead(POT_ADC); }
= Step 5 = Display on the OLED. Assume the following handler to the pointer to the display controller class:
SSD1306Wire& display
More information in the scenario EMB7: Using OLED display.
Note, ADC measures in the 12-bit mode (we assume such configuration, adapt factor
if using other sampling resolution), so values stored in an aGraphArray
array are between 0 and 4095.
float factor = 63./4095.; for(byte x=0;x<128;x++) { int16_t y=63-round(((float)aGraphArray[x])*factor); display.setPixel(x,y); } display.display();
A relation between the potentiometer set value and ADC reading should be almost linear from 0V up to about 3V. It becomes horizontal because the ESP32 chip limits the ADC range to 3V, so going beyond 3V (and due to the electronic construction as in figure 15 it may go to about 3.3V) gives no further increase but rather a reading of the 4096 value (which means the input voltage is over the limit). For this reason, your plot may be finished suddenly with a horizontal instead of linearity decreasing function. It is by design. ADC input of the ESP32 can tolerate values between 3V and 3.3V. The linear correlation mentioned above is never perfect, either because of the devices' implementation imperfection (ESP32's ADC input and digital potentiometer output) or because of the electromagnetic noise. There are many devices in our lab room.
The ADC readings are changing slightly, but I have not changed the potentiometer value. What is going on?: The ADC in ESP32 is quite noisy, mainly when using WiFi parallelly. Refer to the Coursebook and ESP32 documentation on how to increase measurement time that will make internally many readings and return to you an average. Use the analogSetCycles(cycles)
function to increase the number of readings for the averaging algorithm. The default is 8, but you can increase it up to 255. Note that the higher the cycles
parameter value, the longer the reading takes, so tune your main loop accordingly, particularly when using an asynchronous approach (timer-based). Eventually, you can implement low-pass filters yourself (in the software).
In this scenario, we will introduce a popular DHT11 sensor. The DHT series covers DHT11, DHT22, and AM2302. Those sensors differ in accuracy and physical dimensions but can all read environmental temperature and humidity. This scenario can be run stand-alone to read weather data in the laboratory nodes' room. The DHT11 sensor is controlled with one GPIO (in all our laboratory nodes, it is GPIO 47) and uses a proprietary protocol.
Air temperature and humidity can be easily read using a dedicated library. Actually, you need to include two of them, as presented below:
lib_deps = adafruit/DHT sensor library@^1.4.6 adafruit/Adafruit Unified Sensor@^1.1.9
Sensor readings can be sent over the network or presented on one of the node's displays (e.g. LCD), so understanding how to handle at least one of the displays is essential:
It is also possible to present the temperature as the LED colour changes with a PWM-controlled LED or LED stripe. Their usage is described here:
A good understanding of the hardware timers is essential if you plan to use asynchronous programming (see note below). Consider getting familiar with the following:
In this scenario, we only focus on reading the sensor (temperature and humidity). Information on how to display measurements is part of other scenarios that you should refer to to create a fully functional solution (see links above).
Present the current temperature, and humidity on any display (e.g. LCD). Remember to add units (C, %Rh).
A general check to see if you can see the chosen display in the camera field of view is necessary. No other actions are required before starting development.
The steps below present only interaction with the sensor. Those steps should be supplied to present the data (or send it over the network) using other scenarios accordingly. = Step 1 = Include the DHT library and related sensor library.
#include <Adafruit_Sensor.h> #include <DHT.h>
= Step 2 = Declare type of the sensor and GPIO pin:
#define DHTTYPE DHT11 // DHT 11 #define DHTPIN 47
= Step 3 = Declare controller class and variables to store data:
static DHT dht(DHTPIN, DHTTYPE,50); static float hum = 0; static float temp = 0; static boolean isDHTOk = false;
= Step 4 =
Initialise sensor (mind the delay(100);
after initialisation as the DHT11 sensor requires some time to initialise before one can read the temperature and humidity:
dht.begin(); delay(100);
= Step 5 =
Reat the data and check whether the sensor works OK. In the case of the DHT sensor and its controller class, we check the correctness of the readings once the reading is finished. The sensor does not return any status but checks if the reading is okay. This can be done by comparing the readings with the NaN
(not a number) value, each separately:
hum = dht.readHumidity(); temp = dht.readTemperature(); (isnan(hum) || isnan(temp))?isDHTOk = false:isDHTOk = true;
NaN
numbers. Please give it some 250ms, at least, between consecutive readings, whether you do it asynchronously or using a blocking call of delay(250);
in the loop.
The observed temperature is usually between 19 and 24C, with humidity about 40-70%, depending on the weather. On rainy days, it can even go higher.
I've got NaN (Not a Number) readings. What to do?: Check if GPIO is OK (should be D22), check if you initialised controller class and most of all, give the sensor some recovery time (at least 250ms) between consecutive readings.
The temperature-only sensor DS18B20 uses a 1-wire protocol. “1-wire” applies only to the bidirectional bus; power and GND are on separate pins. The sensor is connected to the MCU using GPIO 6 only. Many devices can be connected on a single 1-wire bus, each with a unique ID. DS18B20 also has a water-proof metal enclosure version (but here, in our lab, we use a plastic one) that enables easy monitoring of the liquid's temperature.
To handle operations with DS18B20, we will use a dedicated library that uses a 1-wire library on a low level:
lib_deps = milesburton/DallasTemperature@^3.11.0
Sensor readings can be sent over the network or presented on one of the node's displays (e.g. LCD), so understanding how to handle at least one of the displays is essential:
A good understanding of the hardware timers is essential if you plan to use asynchronous programming (see note below). Consider getting familiar with the following:
In this scenario, we present how to interface the 1-wire sensor, DS18B20 (temperature sensor).
Read the temperature from the sensor and present it on the display of your choice. Show the reading in C. Note that the scenario below presents only how to use the DS18B20 sensor. How to display the data is present in other scenarios, as listed above. We suggest using an LCD (scenario EMB5: Using LCD Display).
Update reading every 10s. Too frequent readings may cause incorrect readings or faulty communication with the sensor. Remember, the remote video channel has its limits, even if the sensor can be read much more frequently.
Check if your display of choice is visible in the FOV of the camera once the device is booked.
The steps below present only interaction with the sensor. Those steps should be supplied to present the data (or send it over the network) using other scenarios accordingly. = Step 1 = Include Dallas sensor library and 1-Wire protocol implementation library:
#include <OneWire.h> #include <DallasTemperature.h>
= Step 2 = Declare the 1-Wire GPIO bus pin, 1-Wire communication handling object, sensor proxy and a variable to store readings:
#define ONE_WIRE_BUS 6 static OneWire oneWire(ONE_WIRE_BUS); static DallasTemperature sensors(&oneWire); static float tempDS;
sensors
class represents a list of all sensors available on the 1-Wire bus. Obviously, there is just a single one in each of our laboratory nodes.
= Step 3 = Initialise sensors' proxy class:
sensors.begin();
= Step 4 =
[pczekalski][✓ ktokarz, 2024-04-22] check if you don't need to call sensors.requestTemperatures(); before the code below
Read the data:
if (sensors.getDeviceCount()>0) { tempDS = sensors.getTempCByIndex(0); } else { // Sensors not present (broken?) or 1-wire bus error }
Remember not to read the sensor too frequently. 10s between consecutive readings is just fine.
Devices in the 1-Wire bus are addressable either by index (as in the example above) or by their address.
Some useful functions are present below:
DallasTemperature::toFahrenheit(tempDS)
- converts temperature in C to F,sensors.getAddress(sensorAddress, index)
- reads device address given by uint8_t index
and stores it in DeviceAddress sensorAddress
.The library can make non-blocking calls, which can also be implemented using timers, as presented in the scenario ADV1: Using timers to execute code asynchronously.
The observable temperature is usually within the range of 19-24C. If you find the temperature much higher, check your code, and if that is okay, please contact our administrator to inform us about the faulty AC.
It is advised to use timers that periodically execute a function to handle repeating tasks. Hardware timers work parallel to the CPU and consume few CPU resources. ESP32-S3 has 4 hardware timers, but each timer can execute multiple handlers. You can think about these handlers as they are interrupted handling functions, but instead of externally triggered interrupts, those are initiated internally by the hardware timer.
The idea of using the timer is to encapsulate a piece of compact code that can be run virtually asynchronously and executed is a precisely-defined time manner. In this scenario, we use a timer to update the LCD screen periodically. We choose a dummy example where Timer 1 is used to increment a value of the byte
type, and Timer 2 reads this value and writes it on the LCD. Naturally, a collision may occur whenever two processes are to access a single memory block (variable). It is critical when both processes (tasks, asynchronous functions) are writing to it. However, in our example, the handler executed by Timer 1 only writes to the variable, while Timer 2 only reads from it. In this scenario, there is no need to use mutexes (semaphores). A scenario with a detailed description of when to use mutexes is beyond the scope of this example.
To implement this scenario, it is necessary to get familiar with at least one of the following scenarios first:
A standard LCD handling library is attached to the platformio.ini
, and it is the only library needed. Timers are integrated into the Arduino framework for ESP32.
lib_deps = adafruit/Adafruit LiquidCrystal@^2.0.2
Present on the LCD current value of the byte
variable; update the LCD every 3 seconds using a timer. Use another timer to increase this variable every 1 second.
Check LCD visibility in the camera FOV. You may also disable LED if it interferes with the camera video stream, as described in the scenario EMB9A: Use of RGB LEDs.
We used to separate tasks, but for this case, complete code is provided in chunks, including LCD handling. It presents relations on where particular parts of the code should be located when using timers and how timing relates between components.
= Step 1 = Include libraries:
#include <Arduino.h> #include <Adafruit_LiquidCrystal.h>
= Step 2 = Define LCD configuration pins (see details and explanation in the scenario: EMB5: Using LCD Display):
#define LCD_RS 2 #define LCD_ENABLE 1 #define LCD_D4 39 #define LCD_D5 40 #define LCD_D6 41 #define LCD_D7 42
= Step 3 = Declare variables and classes (timers):
volatile byte i = 0; hw_timer_t *Timer1 = NULL; hw_timer_t *Timer2 = NULL; static Adafruit_LiquidCrystal lcd(LCD_RS, LCD_ENABLE, LCD_D4, LCD_D5, LCD_D6, LCD_D7);
Above, we declare two separate timers: Timer1
and Timer2
as pointers. They are initialised later in the setup code.
= Step 4 = Declare constants that define how frequently timers will be calling handlers:
const int baseFrequency = 80; //MHz const int interval1 = 1000000; // Interval in microseconds = 1s const int interval2 = 3000000; // Interval in microseconds = 3s
The base frequency for the timers in ESP32 is 80 MHz. Each timer counts in microseconds, so we define 2 intervals: 1s (1000000us) for the counter increase and 3s (3000000us) for the LCD update routine.
= Step 5 = Declare and implement functions that are timer handles: timer calls the bound function every execution period:
void IRAM_ATTR onTimer1() //handler for Timer1 { i++; } void IRAM_ATTR onTimer2() //handler for Timer2 { lcd.clear(); lcd.setCursor(0,0); lcd.print(i); }
float
variables when using asynchronous calls such as timer routines (handlers) and ISR (interrupt functions)! This is because of the limitation of the ESP32 where type float
is hardware implemented in FPU: FPU cannot share its resources among cores, and timer routines may switch from one core to another during consecutive executions, so in that case, your application can hang and throw an exception. An exception won't be seen on the screen as you're remote, so you may notice only the laboratory node becoming unresponsive. This kind of error is usually tough to trace.
double
as this type is not hardware accelerated in ESP32s and is software implemented. Note, however, that there is a significant performance drop between float
(faster) and double
(slower) calculations.
IRAM_ATTR
to be kept in RAM rather than flash. It is because of the performance. Interrupt and Timer functions should be kept as simple as possible. A watchdog exception can be thrown if more complex tasks are to be done and handler execution takes too long, particularly when the previous execution is not finished before the next one starts. This sort of problem may require fine-tuning the timer frequency and eventually changing the algorithm to set up only a flag in the handler that is later detected and handled in the loop()
function.
= Step 6 =
Configure timers, start LCD and enable timers. Note the correct order: LCD have to be ready when the timer calls LCD.write(…);
:
void setup(){ //Timer1 config Timer1 = timerBegin(0, baseFrequency, true); timerAttachInterrupt(Timer1, &onTimer1, true); timerAlarmWrite(Timer1, interval1, true); //Timer2 config Timer2 = timerBegin(1, baseFrequency, true); timerAttachInterrupt(Timer2, &onTimer2, true); timerAlarmWrite(Timer2, interval2, true); //start LCD lcd.begin(16,2); lcd.clear(); //start both timers timerAlarmEnable(Timer1); timerAlarmEnable(Timer2); }
In the code above, Timer1 = timerBegin(0, baseFrequency, true);
creates a new timer bound to the hardware timer 0 (first, timers are zero-indexed). The last parameter causes the timer to count up (false counts down) internally, as every timer has its counter (here 64-bit).
Following, timerAttachInterrupt(Timer1, &onTimer1, true);
attaches function onTimer1
(by its address, so we use &
operator) to the Timer1
to be executed periodically.
Then we define how frequently the execution of the function above will occur: timerAlarmWrite(Timer1, interval1, true);
. The last parameter causes the timer to reload after each execution automatically. Timers can also trigger an action only once (you need to set up the last parameter to false
). In our example, we wish continuous work, so we use true
.
Note that at this moment, timers are not executing the handlers yet; the last step is required: timerAlarmEnable(Timer1);
. This step is separated from the other configuration steps because the LCD has to be initialised before timers can use the LCD.
= Step 7 = This way, a main loop is empty: everything runs asynchronously, thanks to the timers. As suggested above, when timer handlers require long or unpredictable execution time (e.g. external communication, waiting for the reply), handlers should set a flag that is read in the main loop to execute the appropriate task and then clear the flag.
void loop() { }
On the LCD screen, you should see values starting from number 3, then 6, 9, 12 and so on (=3 every 3 seconds). Note, as byte
has a capacity of 256 (0…255), the sequence changes in the following increments once it overflows.
How many timers can I use?: ESP32-S3 has 4 hardware timers. You may trick this limitation by using smart handlers that have, e.g., an internal counter and internally execute every N-th cycle. This helps to simulate more timers in a software way.
— MISSING PAGE — — MISSING PAGE — — MISSING PAGE — — MISSING PAGE — — MISSING PAGE — — MISSING PAGE — — MISSING PAGE — — MISSING PAGE — — MISSING PAGE —
Each laboratory node is equipped with an STM32WB55 chip. Several peripherals, networks and network services are available for the user. The UI is necessary to observe results in the camera when programming remotely. Thus, a proper understanding of UI programming is essential to successfully using the devices.
The table 2 lists all hardware components of the SUT's STM32WB55 node and hardware details such as connectivity, protocols, GPIOs, etc. Please note that some pins overlap because buses such as SPI and I2C are shared among multiple components.
The node is present in the figure 5 and reference numbers reflecting components in the table 2.
A suitable platformio.ini file for the correct code compilation is presented below. It does not contain libraries that need to be added regarding specific tasks and hardware used in particular scenarios. The code below presents only the typical section. Refer to the scenario description for details regarding case-specific libraries needed for the implementation:
[env:nucleo_wb55rg_p] platform = ststm32 framework = arduino board = nucleo_wb55rg_p lib_ldf_mode = deep+
[ktokarz][✓ ktokarz, 2024-04-21]Rozważ dodanie do platformio.ini sekcji lib_ldf_mode = deep+
Know the hardware
The following scenarios explain the use of hardware components and services that constitute the laboratory node. It is intended to seamlessly introduce users to IoT scenarios where using sensors and actuators is an intermediate step, and the main goal is to use networking and communication. Besides IoT, those scenarios can be utilised as a part of the Embedded Systems Modules.
IoT programming
In the following scenarios, you will write programs interacting with other devices, services, and networks, which are pure IoT applications.
[ktokarz][✓ ktokarz, 2024-03-29]Prośba - zamień URLe do EMB9A i EMB9B żeby URL odpowiadał treści/tematowi zadania
Advanced techniques
In the following scenarios, we will focus on advanced programming techniques, such as asynchronous programing and timers.
[ktokarz]Add IoT scenarios
Alphanumerical LCD is one of the most popular output devices in the Embedded and IoT. Using LCD with predefined line organisation (here, 2 lines, 16 characters each) is as simple as sending a character's ASCII code to the device. This is so much simpler than in the case of the use of dot-matrix displays, where it is necessary to use fonts. The fixed organisation LCD has limits; here, only 32 characters can be presented to the user simultaneously. ASCII presents a limited set of characters, but many LCDs can redefine character maps (how each letter, digit or symbol looks). This way, it is possible to introduce graphics elements (i.e. frames), special symbols and letters.
In this scenario, you will learn how to handle easily LCD to present information and retrieve it visually with a webcam.
Familiarise yourself with a hardware reference. LCD of this type can be connected to the microcontroller with 8 or 4 lines of data, RS - register select, R/#W - read/write, and EN - synchronisation line. In our lab equipment, the LCD is controlled with 6 GPIOs. We use 4 lines for data and because We don't read anything from the LCD the R/#W is connected to the ground. Details can be found in Table 1: STM32WB55 Node Hardware Details on the STM32 laboratory hardware reference page.
You are going to use a library to handle the LCD. It means you need to add it to your platformio.ini
file. Use the template provided in the hardware reference section and extend it with the library definition:
lib_deps = arduino-libraries/LiquidCrystal@^1.0.7
Draw “Hello World” in the upper line of the LCD and “Hello IoT” in the lower one.
Check if you can see a full LCD in your video stream. Book a device and create a dummy Arduino file with void setup()…
and void loop()…
.
= Step 1 = Include the library in your source code:
#include <LiquidCrystal.h>
= Step 2 = Declare GPIOs controlling the LCD, according to the hardware reference:
const int rs = PC5, en = PB11, d4 = PB12, d5 = PB13, d6 = PB14, d7 = PB15;
= Step 3 = Declare an instance of the LCD controller class and preconfigure it with appropriate control GPIOs:
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
= Step 4 = Initialise class with display area configuration (number of columns, here 16 and rows, here 2):
lcd.begin(16, 2);
= Step 5 = Implement your algorithm. The most common class methods that will help you are listed below:
.clear()
- clears all content;.setCursor(x,y)
- set cursor, writing will start there;.print(contents)
- prints text in the cursor location; note there are many overloaded functions, accepting various arguments, including numerical.A simple example can be the Hello World:
lcd.print("hello, world!");
You should be able to see “Hello World” and “Hello IoT” on the LCD now.
VREL NExtGen laboratory node is equipped with b/w, ePaper module. It is a dot matrix display with a native resolution of 250×122 pixels. It has 64kB display memory and is controlled via SPI. The ePaper display presents data even if powered off, so don't be surprised that finishing your application does not automatically clean up the display, even if you use some other code later. To clean up the display, one has to clear the screen explicitly.
Familiarise yourself with a hardware reference: this ePaper is controlled with 6 GPIOs as presented in the “Table 1: STM32WB55 Node Hardware Details” on the hardware reference page.
You are going to use a library to handle the ePaper drawing. It means you need to add it to your platformio.ini
file. Use the template provided in the hardware reference section and extend it with the library definition:
lib_deps = zinggjm/GxEPD2@^1.5.0
To generate an array of bytes representing an image, it is easiest to use an online tool, e.g.:
Present an image on the screen and overlay the text “Hello World” over it.
Check if you can see a full ePaper Display in your video stream. Book a device and create a dummy Arduino file with void setup()…
and void loop()…
.
Prepare a small bitmap (e.g. 64×64 pixels) and convert it to the byte array with b/w settings.
Sample project favicon you can use is present in Figure 11:
Remember to include the source array in the code when drawing an image.
The corresponding generated C array for the logo in Figure 22 (horizontal 1 bit per pixel, as suitable for ePaper Display) is present below:
// 'logo64', 64x64px const unsigned char epd_bitmap_logo_64 [] PROGMEM = { 0xff, 0xff, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0x80, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x07, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xf8, 0x7f, 0xff, 0xef, 0xff, 0xe1, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xe8, 0xff, 0xf0, 0xff, 0xff, 0xe1, 0xff, 0xff, 0xfe, 0x5f, 0xf8, 0x7f, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xf7, 0xfc, 0x3f, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xf3, 0xfe, 0x3f, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xfe, 0xff, 0x1f, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xfe, 0xff, 0x8f, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x8f, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xbf, 0xc7, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xc7, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xe3, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xf7, 0xe3, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xf1, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xf1, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xf9, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xf9, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xf8, 0xf1, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xfd, 0xf8, 0xf1, 0xff, 0xff, 0xfe, 0x07, 0xff, 0xfd, 0xf8, 0xf1, 0xff, 0xff, 0xfe, 0x07, 0xff, 0xfd, 0xf8, 0xf1, 0xfc, 0x00, 0x1c, 0x03, 0xff, 0xfc, 0xf8, 0xf1, 0xfc, 0x00, 0x1c, 0x03, 0xfe, 0xfe, 0xf8, 0xf1, 0xff, 0xff, 0xfe, 0x07, 0xfe, 0xff, 0xf8, 0xf1, 0xff, 0xff, 0xfe, 0x07, 0xfd, 0xfd, 0xf8, 0xf1, 0xff, 0xff, 0xff, 0x9f, 0xfd, 0xfd, 0xf8, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xfd, 0xf8, 0xf1, 0xff, 0xe1, 0xff, 0xff, 0xfb, 0xfd, 0xf9, 0xf1, 0xff, 0x80, 0x7f, 0xff, 0xfb, 0xf9, 0xf9, 0xf1, 0xff, 0x00, 0x3f, 0xff, 0xf7, 0xfb, 0xf1, 0xf1, 0xfe, 0x3f, 0x3f, 0xff, 0xef, 0xf3, 0xf1, 0xf1, 0xfe, 0x7f, 0x1f, 0xff, 0xcf, 0xff, 0xf3, 0xf1, 0xfc, 0x7f, 0x9f, 0xff, 0x5f, 0xef, 0xe3, 0xf1, 0xfc, 0x7f, 0x9f, 0xfe, 0xbf, 0xcf, 0xe3, 0xf1, 0xfc, 0xff, 0x9f, 0xe9, 0xff, 0xef, 0xc7, 0xf1, 0xfc, 0x7f, 0x9f, 0xef, 0xff, 0xbf, 0xc7, 0xf1, 0xfe, 0x7f, 0x9f, 0xff, 0xff, 0x3f, 0x8f, 0xf1, 0xfe, 0x3f, 0x1f, 0xff, 0xfe, 0xff, 0x8f, 0xf1, 0xff, 0x00, 0x3f, 0xff, 0xfe, 0xff, 0x1f, 0xf1, 0xff, 0x80, 0x7f, 0xff, 0xf9, 0xfe, 0x3f, 0xf1, 0xff, 0xc1, 0xff, 0xff, 0xe7, 0xfc, 0x7f, 0xf1, 0xff, 0xff, 0xff, 0xfe, 0x9f, 0xf8, 0x7f, 0xf1, 0xff, 0xff, 0xff, 0xe8, 0xff, 0xf0, 0xff, 0xf1, 0xff, 0xff, 0xbf, 0xef, 0xff, 0xe1, 0xff, 0xf1, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xc3, 0xff, 0xf1, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0x07, 0xff, 0xf1, 0xfe, 0x00, 0x03, 0xff, 0xfc, 0x1f, 0xff, 0xf1, 0xfc, 0x7f, 0x9f, 0xff, 0xf0, 0x3f, 0xff, 0xf1, 0xfc, 0xff, 0x9f, 0xff, 0x80, 0xff, 0xff, 0xf1, 0xfc, 0xff, 0x9f, 0xc0, 0x03, 0xff, 0xff, 0xf1, 0xfc, 0x7f, 0x9f, 0xc0, 0x1f, 0xff, 0xff, 0xf1, 0xfe, 0x7f, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
= Step 1 = Include necessary libraries and the definition of the font.
// Epaper Display libraries #include <GxEPD2.h> #include <GxEPD2_BW.h> // Fonts #include <Fonts/FreeMonoBold12pt7b.h>
The code above includes a font to draw text on the ePaper Display. There are many fonts one can use, and a non-exhaustive list is present below (files are located in the Adafruit GFX Library
, subfolder Fonts
):
FreeMono12pt7b.h FreeMono18pt7b.h FreeMono24pt7b.h FreeMono9pt7b.h FreeMonoBold12pt7b.h FreeMonoBold18pt7b.h FreeMonoBold24pt7b.h FreeMonoBold9pt7b.h FreeMonoBoldOblique12pt7b.h FreeMonoBoldOblique18pt7b.h FreeMonoBoldOblique24pt7b.h FreeMonoBoldOblique9pt7b.h FreeMonoOblique12pt7b.h FreeMonoOblique18pt7b.h FreeMonoOblique24pt7b.h FreeMonoOblique9pt7b.h FreeSans12pt7b.h FreeSans18pt7b.h FreeSans24pt7b.h FreeSans9pt7b.h FreeSansBold12pt7b.h FreeSansBold18pt7b.h FreeSansBold24pt7b.h FreeSansBold9pt7b.h FreeSansBoldOblique12pt7b.h FreeSansBoldOblique18pt7b.h FreeSansBoldOblique24pt7b.h FreeSansBoldOblique9pt7b.h FreeSansOblique12pt7b.h FreeSansOblique18pt7b.h FreeSansOblique24pt7b.h FreeSansOblique9pt7b.h FreeSerif12pt7b.h FreeSerif18pt7b.h FreeSerif24pt7b.h FreeSerif9pt7b.h FreeSerifBold12pt7b.h FreeSerifBold18pt7b.h FreeSerifBold24pt7b.h FreeSerifBold9pt7b.h FreeSerifBoldItalic12pt7b.h FreeSerifBoldItalic18pt7b.h FreeSerifBoldItalic24pt7b.h FreeSerifBoldItalic9pt7b.h FreeSerifItalic12pt7b.h FreeSerifItalic18pt7b.h FreeSerifItalic24pt7b.h FreeSerifItalic9pt7b.h
= Step 2 = Declare GPIOs and some configurations needed to handle the ePaper display properly:
// The epaper display's model is selected #define GxEPD2_DRIVER_CLASS GxEPD2_213_BN #define GxEPD2_DISPLAY_CLASS GxEPD2_BW // Definition of GPIOs except SPI pins #define EPAPER_DC_PIN D4 #define EPAPER_CS_PIN D1 #define EPAPER_RST_PIN D5 #define EPAPER_BUSY_PIN D7 // Definition of the size of the buffer #define MAX_DISPLAY_BUFFER_SIZE 65536ul #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
= Step 3 = Declare ePaper display controller:
static GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ EPAPER_CS_PIN, /*DC=*/ EPAPER_DC_PIN, /*RST=*/ EPAPER_RST_PIN, /*BUSY=*/ EPAPER_BUSY_PIN));
You can also declare a message to display as an array of characters:
static const char HelloWorldMsg[] = "Hello IoT World!";
= Step 4 = Configure GPIOs and initialise the ePaper controller class. It uses the standard SPI connection.
pinMode(EPAPER_CS_PIN, OUTPUT); pinMode(EPAPER_RST_PIN, OUTPUT); pinMode(EPAPER_DC_PIN, OUTPUT); pinMode(EPAPER_BUSY_PIN,INPUT_PULLUP); digitalWrite(EPAPER_CS_PIN,LOW); display.epd2.selectSPI(SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); \\ Delay here is to ensure a stable CS signal before SPI data sending delay(100); display.init(115200); digitalWrite(EPAPER_CS_PIN,HIGH);
= Step 5 = Set display rotation, font and text colour:
digitalWrite(EPAPER_SPI_CS_PIN,LOW); display.setRotation(1); display.setFont(&FreeMonoBold12pt7b); display.setTextColor(GxEPD_BLACK);
then get the external dimensions of the string to be printed and calculate the starting point (x, y)
for the centred text:
int16_t tbx, tby; uint16_t tbw, tbh; display.getTextBounds(HelloWorldMsg, 0, 0, &tbx, &tby, &tbw, &tbh); uint16_t x = ((display.width() - tbw) / 2) - tbx; //center in x arrow uint16_t y = ((display.height() - tbh) / 4) - tby; //one fourth from the top
= Step 6 = Then display the contents of the image and the text in the ePaper display:
display.setFullWindow(); display.fillScreen(GxEPD_WHITE); display.setCursor(x, y); display.print(HelloWorldMsg); display.display(true); display.drawImage((uint8_t*)epd_bitmap_logo_64,0,0,64,64); digitalWrite(EPAPER_SPI_CS_PIN,HIGH);
You should be able to see an image and a text on the ePaper Display.
This scenario presents how to use the OLED display connected to the STM32WB55 SoC. Our OLED display is an RGB (16bit colour, 64k colours) 1.5in, 128×128 pixels. The OLED chip is SSD1351, and it is controlled over the SPI interface using the pin configuration as described in STM32 node Hardware Reference in Table 1 STM32WB55 Node Hardware Details.
There is no need to program SPI because the display is connected to the hardware SPI directly, which is handled by a library built in the STMDuino. We will use an SSD1351 display library, and a graphic abstraction layer for drawing primitives such as lines, images, text, circles, and so on:
lib_deps = adafruit/Adafruit SSD1351 library@^1.3.2 adafruit/Adafruit GFX Library@^1.11.9
Note that the graphics abstraction library (Adafruit GFX) can be loaded automatically if the
lib_ldf_mode = deep+
declaration in the platformio.ini
is set.
To generate an array of bytes representing an image in 565 format, it is easiest to use an online tool, e.g.:
Draw a text on the OLED display and an image of your choice (small, to fit both text and image).
Perhaps you will need to use an external tool to preprocess an image to the desired size (we suggest something no bigger than 100×100 pixels) and another tool (see hint above) to convert an image to an array of bytes.
Check if you can see a full OLED Display in your video stream. Book a device and create a dummy Arduino file with void setup()…
and void loop()…
.
Prepare a small bitmap and convert it to the byte array for 16-bit colour settings.
Sample project favicon you can use is present in Figure 22:
Remember to include the source array in the code when drawing an image. The corresponding generated C array for the logo in Figure 24 is too extensive to present here in the textual form, so below it is just the first couple of pixels represented in the array, and full contents you can download here: ZIPed archive with a file containing all pixel data of the image .
const uint16_t epd_bitmap_logo64 [] PROGMEM = { 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xf7be, 0xbdd7, 0x8430, 0x5aeb, 0x39c7, 0x2104, 0x1082, 0x0020, 0x0020, 0x1082, 0x2104, 0x39c7, 0x5aeb, 0x8430, 0xbdd7, 0xf7be, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, .... .... 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 };
= Step 1 = Include necessary libraries:
// Libraries #include <Arduino.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1351.h> // Fonts #include <Fonts/FreeMono9pt7b.h> // Here you can also include the file with the picture #include "epd_bitmap_logo64.cpp"
The code above also includes a font to draw text on the OLED Display. There are many fonts one can use, and a non-exhaustive list is present below (files are located in the Adafruit GFX Library
, subfolder Fonts
):
FreeMono12pt7b.h FreeMono18pt7b.h FreeMono24pt7b.h FreeMono9pt7b.h FreeMonoBold12pt7b.h FreeMonoBold18pt7b.h FreeMonoBold24pt7b.h FreeMonoBold9pt7b.h FreeMonoBoldOblique12pt7b.h FreeMonoBoldOblique18pt7b.h FreeMonoBoldOblique24pt7b.h FreeMonoBoldOblique9pt7b.h FreeMonoOblique12pt7b.h FreeMonoOblique18pt7b.h FreeMonoOblique24pt7b.h FreeMonoOblique9pt7b.h FreeSans12pt7b.h FreeSans18pt7b.h FreeSans24pt7b.h FreeSans9pt7b.h FreeSansBold12pt7b.h FreeSansBold18pt7b.h FreeSansBold24pt7b.h FreeSansBold9pt7b.h FreeSansBoldOblique12pt7b.h FreeSansBoldOblique18pt7b.h FreeSansBoldOblique24pt7b.h FreeSansBoldOblique9pt7b.h FreeSansOblique12pt7b.h FreeSansOblique18pt7b.h FreeSansOblique24pt7b.h FreeSansOblique9pt7b.h FreeSerif12pt7b.h FreeSerif18pt7b.h FreeSerif24pt7b.h FreeSerif9pt7b.h FreeSerifBold12pt7b.h FreeSerifBold18pt7b.h FreeSerifBold24pt7b.h FreeSerifBold9pt7b.h FreeSerifBoldItalic12pt7b.h FreeSerifBoldItalic18pt7b.h FreeSerifBoldItalic24pt7b.h FreeSerifBoldItalic9pt7b.h FreeSerifItalic12pt7b.h FreeSerifItalic18pt7b.h FreeSerifItalic24pt7b.h FreeSerifItalic9pt7b.h
= Step 2 = Add declarations for GPIOs, colours (to ease programming and use names instead of hexadecimal values) and screen height and width. To recall, the OLED display in our lab is square: 128×128 pixels, 16k colours (16-bit 565: RRRRRGGGGGGBBBBB colour model):
// Pins definition for OLED #define SCLK_PIN D13 // It also works with STM numbering style PB_13 #define MOSI_PIN D11 // It also works with STM numbering style PB_15 #define MISO_PIN D12 // It also works with STM numbering style PB_14 #define OLED_DC_PIN D4 #define OLED_CS_PIN D2 // Doesn't work with STM numbering style #define OLED_RST_PIN D10 // // Colours definitions #define BLACK 0x0000 #define BLUE 0x001F #define RED 0xF800 #define GREEN 0x07E0 #define CYAN 0x07FF #define MAGENTA 0xF81F #define YELLOW 0xFFE0 #define WHITE 0xFFFF // Screen dimensions #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 128
= Step 3 = Declare an OLED controller objects:
Adafruit_SSD1351 oled = Adafruit_SSD1351(SCREEN_WIDTH, SCREEN_HEIGHT, &SPI, OLED_CS_PIN, OLED_DC_PIN, OLED_RST_PIN);
= Step 4 = Initialise the OLED controller object. Then clear the screen (write all black):
pinMode(OLED_CS_PIN, OUTPUT); pinMode(OLED_RST_PIN, OUTPUT); oled.begin(); oled.fillRect(0, 0, 128, 128, BLACK);
= Step 5 =
Draw a bitmap in the left top corner of the screen (screen is 128x128px). The OLED library handles the OLED_CS_PIN
automatically.
oled.drawRGBBitmap(0,0, epd_bitmap_logo64, 64, 64);
= Step 6 = Write some additional text in the middle of the screen:
oled.setFont(&FreeMono9pt7b); oled.setTextSize(1); oled.setTextColor(WHITE); oled.setCursor(10,80); oled.println("Hello IoT");
Some remarks regarding coordinates:
setFont
sets the base font later used for printing. The font size is given in the font name, so in the case of the FreeMono9pt7b
, the base font size is 9 pixels vertically,setTextSize
sets a relative font scaling; assuming the base font is 9 pixels, setTextSize(2)
will scale it up to 200% (18 pixels); there is no fractal calling here :(,setTextColor
controls the colour of the text: as we have a black screen (fillScreen(BLACK)
), we will use white here, but any other colour is valid,setCursor(X,Y)
sets the text location; note the upper-left corner is 0.0, but that relates to the lower-left corner of the first letter. So, to write in the first line, you need to offset it down (Y-coordinate) by at least font size (relative, also regarding text size calling, if any).println(…)
to print the text is very handy as once executed, setCursor
is automatically called to set the cursor in the next line so you can continue printing in a new line without a need to set the cursor's position explicitly. Use print(…)
to continue printing in the current line.
Besides the functions presented above, the controller class has several other handy functions (among others):
drawPixel(x,y, colour)
draws a pixel in x,y
coordinates of the colour
colour,drawCircle(x,y, radius, colour)
draws a circle in x,y
coordinates with colour colour
and specified radius
(in pixels),drawLine(x1,y1, x2,y2, colour)
draws a line starting from x1,y1
and finished in x2,y2
with given colour
- to draw straight (horizontal or vertical) lines there is a faster option:drawFastHLine(x,y, w, colour)
draws horizontal line that starts from x,y
and of the length w
with given colour
,drawFastVLine(x,y, h, colour)
draws vertical line that starts from x,y
and of the length h
with given colour
,drawRect(x,y, w,h, colour)
draws a rectange starting in x,y
of the width and height w
and h
and with given colour
(no fill),drawTriangle(x1,y1, x2,y2, x3,y3, colour)
draws a triangle using 3 vertexes and of given colour (no fill),You should see the image and the text in the video stream.
The screen is black even if I write to it. What to do?: Check if you have done all the steps shown in the example. Check if you used proper GPIOs to control the OLED display. Follow carefully the code example in this manual: it does work!
This Intellectual Output was implemented under the Erasmus+ KA2.
Project IOT-OPEN.EU Reloaded – Education-based strengthening of the European universities, companies and labour force in the global IoT market.
Project number: 2022-1-PL01-KA220-HED-000085090.
Erasmus+ Disclaimer
This project has been funded with support from the European Commission.
This publication reflects the views of only the author, and the Commission cannot be held responsible for any use that may be made of the information contained therein.
Copyright Notice
This content was created by the IOT-OPEN.EU Reloaded consortium, 2022,2024.
The content is Copyrighted and distributed under CC BY-NC Creative Commons Licence, free for Non-Commercial use.
A Smart LED stripe is a chain of connected digital LEDs (also referenced as NEOPIXEL) which can be individually controlled. The stripe in our lab equipment consists of eight RGB LEDs. There exist also other colour configurations such as RGBWW (Red+Green+Blue+Warm White+Cold White) or WWA (Warm White+Cold White+Amber). They are controlled with just one pin/GPIO. GPIO sends the digital signal to the first LED in a chain and the LED passes data to the next one, and so on.
The most common type of LED stripes is WS2812B (RGB). Initially LED Stripes were powered with 5V, but that limits the length of the chain up to some 1-2m (because of the voltage drop), so nowadays LED stripes powered with 12V and even 24V are getting more and more popular.
In this scenario, you will learn how to control a small LED RGB Stripe, composed of 8 Smart (Digital) LEDs with STM32 SoC.
Familiarise yourself with a hardware reference on the LED Stripe. It is controlled with a single GPIO (D8 in Arduino style naming, or PC_12 in Nucleo style naming), as presented in the “Table 1: STM32WB55 Node Hardware Details” on the hardware reference page. To control a WS2812B LED stipe, we will use a library:
adafruit/Adafruit NeoPixel@^1.12.0
There are at least two ways (algorithms) to implement this task:
delay(…);
) function calls in the void loop();
or,Implement a rainbow of colours flowing through the LED Stripe.
When booking a device, ensure the LED stripe is visible in the camera.
Below, we provide a sample colour configuration that does not reflect the desired effect that should result from the exercise. It is just for instructional purposes. You need to invent how to implement a rainbow effect yourself. = Step 1 = Include necessary library:
#include "Adafruit_NeoPixel.h"
= Step 2 = Declare configuration and a controller class:
#define NEOPIXEL_PIN D8 //Arduino numbering D8, STM numbering PC_12 #define NUMPIXELS 8 //How many LEDs are attached Adafruit_NeoPixel strip(NUMPIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
The strip class definition contains the number of pixels, the pin for transmission, and the type of LEDs. In our laboratory LEDs use Green Red Blue format of data and 800kHz data transmission frequency.
= Step 3 =
To switch a particular LED, use the function setPixelColor();
. It accepts parameters of different types.
void setPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b); void setPixelColor(uint16_t n, uint32_t c);
If you use the first version you give the LED number as the first parameter and then three colour values for red, green and blue colour intensity as the number in the 0-255 range.
If you use the second version you give the LED number as the first parameter and a 32-bit version of the colour information. To convert three RGB bytes to 32-bit value you can use the function Color();
:
static uint32_t Color(uint8_t r, uint8_t g, uint8_t b);
The setPixelColor();
sets the appropriate data in the internal buffer of the class object. Sending it to the LED stripe requires the usage of the show();
function afterwards.
The different versions of the setPixelColor();
function can look like in the following code:
strip.setPixelColor(0, strip.Color(10, 0, 0)); // Red strip.setPixelColor(1, 0, 10, 0); // Green strip.setPixelColor(2, 0, 0, 10); // Blue strip.setPixelColor(3, 0x000F0F0F); // White strip.show(); // Update strip
Observe the flow of the colours via the stripe.
I cannot see the colour via the video camera - everything looks white…: Try to lower the LED's brightness.
— MISSING PAGE — — MISSING PAGE —
You will learn how to control a standard miniature servo with the STM32 System on Chip. Standard miniature analogue servo is controlled with a PWM signal of frequency 50Hz with a duty cycle period between 1 ms (rotate to 0) and 2 ms (rotate to 180 degrees), where 1.5 ms corresponds to 90 degrees. Some servos have other duty cycle minimum and maximum values, always refer to the documentation.
A servo has a red arrow presenting the gauge's current position.
The servo is an example of a physical actuator. It requires some time to operate and change the position. Please give it time to set a new position between consecutive changes of the control PWM signal. Moreover, because of the observation via camera, too quick rotation may not be observable at all depending on the video stream fps. A gap of 2s between consecutive rotations is usually a reasonable choice.
A good understanding of the PWM signal and duty cycle is necessary. In this scenario, we use built-in timers to control the PWM hardware channels of the STM32WB55 chip. In this case, we do not use any external library; instead, we use the hardware timer library built in the Arduino Core STM32 framework for STM32WB55 (stm32duino)[2] so that no additional external libraries will be included in the project.
Some servos tend to go below 1 ms and above 2 ms to achieve a full 180-degree rotation range. For example, the servo in our laboratory (MG90/SG90) accepts values starting from 500 ms, and ending at 2500 ms. If there is a need for a high accuracy of rotation, it is possible to fine-tune the minimum and maximum duty cycle values.
Rotate the servo to the following angles: 0, 90, 180, 135, 45 and back to 0 degrees.
In the laboratory equipment, the servo and the fan are connected to the same hardware timer instance. It means that both elements use the same base frequency of the PWM signal. If you use a fan and servo in the same project the frequency of the PMW signal needs to match the servo requirements.
The hardware timer library implements functions which allow us to control the duty cycle of the PWM signal and express it in different formats, including percentages. In the case of setting the PWM duty cycle expressed in percentages, the minimum value is 0, and the maximum is 100. We can also express the duty cycle in microseconds which is easier to calculate for the servo.
Write your application all in the setup()
function, leaving loop()
empty.
= Step 1 = Include Arduino and timer libraries, specific to STM32. The servo is controlled with GPIO PA_10 (the PA_10 name is the STM Nucleo naming convention). Define the pin name constant.
#include "Arduino.h" #include <HardwareTimer.h> #define SERVO_PWM_PIN PA0
We will also need some variables:
// PWM variables definitions TIM_TypeDef *SRVInstance; //General hardware instance uint32_t channelSRV; //2 channel for the servo HardwareTimer *MyTimServo; //Hardware Timer class for Servo PWM
= Step 2 = The hardware timer library uses internal timer modules and allows us to define channels attached to the timer. Channels represent the output pins connected to the hardware elements. Our laboratory board uses the same timer to control the servo and fan. In this example, we will use one channel for the servo only setting the proper PWM frequency of the timer. The channel connected to the servo controls the PWM duty cycle.
Create the timer instance type based on the servo pin:
TIM_TypeDef *SRVInstance = (TIM_TypeDef *)pinmap_peripheral(digitalPinToPinName(SERVO_PWM_PIN), PinMap_PWM);
Define the channel for servo:
channelSRV = STM_PIN_CHANNEL(pinmap_function(digitalPinToPinName(SERVO_PWM_PIN), PinMap_PWM));
Instantiate HardwareTimer object. Thanks to 'new' instantiation, HardwareTimer is not destructed when setup() function is finished.
MyTimServo = new HardwareTimer(SRVInstance);
Configure and start PWM
MyTimServo->setPWM(channelSRV, SERVO_PWM_PIN, 50, 5); // 50 Hertz, 5% dutycycle.
= Step 3 = MG 90 servos that we use in our lab are specific. As mentioned above, to achieve a full 180-degree rotation range, their minimum and maximum duty cycle timings go far beyond standards. We will create a function for calculating the duty cycle for the servo with the angle as the input parameter.
void fSrvSet(int pAngle) { if (pAngle<0) pAngle=0; //check boudaries of angle if (pAngle>180) pAngle=180; int i_srv=500+(200*pAngle)/18; //minimal duty cycle is 0,5ms, maximal 2,5ms MyTimServo->setCaptureCompare(channelSRV, i_srv, MICROSEC_COMPARE_FORMAT); //modify duty cycle };
= Step 4 = Rotating a servo is as easy as calling our function with the desired angle as the parameter, e.g.:
fSrvSet(90); delay(2000);
How do I know minimum and maximum values for the timings for servo operation?: Those parameters are provided along with servo technical documentation, so you should refer to them. Our configuration reflects the servos we use (MG90/SG90), and as you can see, it goes far beyond the standard servo rotation control that is a minimum of 1000us and a maximum of 2000us. Using standard configuration, your servo won't rotate at full 180 degrees but at a shorter rotation range. Would it be possible to control the servo and fan with the same program?: Yes, but you have to remember that servo has very strict timing requirements. Because both elements share the same timer, they also share the same frequency which must be set according to the servo requirements.
Observe the red arrow to rotate accordingly. Remember to give the servo some time to operate.
— MISSING PAGE — — MISSING PAGE —
Digital potentiometer DS1803 is an I2C-controlled device that digitally controls the resistance between the outputs as in a real turning potentiometer.
While in the turning potentiometers, there are wipers which are moving from minimal to a maximal value, in digital potentiometers there are no movable parts. Everything is implemented in a silicon.
DS1803 has two digital potentiometers controlled independently. We use just one with the lower cardinal number (index 0). In our example, it is a 100k spread between GND and VCC, and its output is connected to the ADC (analogue to digital converter) input of the STM32 SoC. This way, the potentiometer's electronic wiper is controlled remotely via the I2C bus.
The device's I2C address is 0x28, and the ADC input GPIO pin is 7.
The digital potentiometer in our laboratory node forms then a loopback device: it can be set (also read) via I2C, and the resulting voltage can be measured on the separate PIN (ADC) 15. This way, it is possible, e.g. to draw a relation between the potentiometer setting and ADC readings to check whether it is linear or forms some other curve.
Reading of the ADC is possible using the regular analogRead(pin)
function. In STM32WB55 there are 16 analogue inputs, and the potentiometer is connected to the pin A4 (PC_3 in Nucleo numbering).
To implement this scenario, it is advised to get familiar with at least one of the following scenarios first:
They enable you to present the data on the display (i.e. readings).
To handle communication with the DS1803 digital potentiometer, we use bare I2C programming. For this reason, we need to include only the I2C protocol library:
#include <Wire.h>
Below, we present a sample control library that you need to include in your code:
enum POT_LIST {POT_1 = 0xA9, POT_2=0xAA, POT_ALL=0xAF}; //We have only POT_1 connected typedef enum POT_LIST POT_ID; //Prototypes void setPotentiometer(TwoWire& I2CDev, byte potValue, POT_ID potNumber); byte readPotentiometer(TwoWire& I2CDev, POT_ID potNumber); //Implementation void setPotentiometer(TwoWire& I2CDev, byte potValue, POT_ID potNumber) { I2CDev.beginTransmission(DS1803_ADDRESS); I2CDev.write(potNumber); I2CDev.write(potValue); I2CDev.endTransmission(true); }; byte readPotentiometer(TwoWire& I2CDev, POT_ID potNumber) //reads selected potentiometer { byte buffer[2]; I2CDev.requestFrom(DS1803_ADDRESS,2); buffer[0]=I2CDev.read(); buffer[1]=I2CDev.read(); return (potNumber==POT_1?buffer[0]:buffer[1]); };
readPotentiometer(…)
function returns a value previously set to the digital potentiometer, not an actual ADC voltage reading! It returns a set value by setPotentiometer(…)
, which is on the “digital” side of the DS1803 device. Actual ADC reading can be obtained using analogRead(pin)
.
Iterate over the potentiometer settings, read related voltage readings via ADC, and present them in graphical form (as a plot). As the maximum resolution of the potentiometer is 256, you can use a plot of 256 points or any other lower value covering all ranges. Present graph (plot) on either ePaper or OLED display, and while doing the readings, you should present data in the LCD (upper row for a set value, lower for a reading of the ADC).
Check if you can see all the displays. Remember to use potentiometer 1 (index 0) because only this one is connected to the ADC input of the ESP32 MCU. In steps 1-3, we present how to handle communication with a digital potentiometer and how to read the ADC input of the MCU. Methods for displaying the measurements and plotting the graph are presented in steps 4 and 5. Remember to include the functions above in your code unless you want to integrate them with your solution.
Below, we assume that you have embedded functions handling operations on the digital potentiometer as defined above in your source file. Remember to include the Wire.h
library.
= Step 1 = Define ADC pin and Digital potentiometer chip DS1803 I2C address. All definitions are present in the following code:
#define POT_ADC A4 #define DS1803_ADDRESS 0x28
= Step 2 = Declare an array of readings that fits an OLED display. Adjust for ePaper resolution (horizontal) if using it. OLED is 128×128 pixels:
static int16_t aGraphArray[128];
= Step 3 = Include functions present in the PREREQUISITES section.
= Step 4 = Initialise the I2C bus and configure ADC's GPIO as input. Change the ADC resolution to 12-bits.
Wire.begin(); pinMode(POT_ADC, INPUT); analogReadResolution(12);
= Step 4 = Perform the loop that sets 128 values (scaled to the range 0 to 256) on the potentiometer's output and read the value back from the digital potentiometer via ADC input. Store readings in the array:
for(byte i=0; i<128; i++) { setPotentiometer(Wire, 2*i, POT_1); aGraphArray[i]=analogRead(POT_ADC); }
= Step 5 = Display on the OLED. Assume the following handler to the pointer to the display controller class:
Adafruit_SSD1351 oled
More information on OLED display is in the scenario STM_7: Using OLED display.
Note, ADC measures in the 12-bit mode (we assume such configuration, adapt factor
if using other sampling resolution), so values stored in an aGraphArray
array are between 0 and 4095.
float factor = 128./4095.; for(byte x=0;x<128;x++) { int16_t y=128-round(((float)aGraphArray[x])*factor); display.setPixel(x,y); } display.display();
A relation between the potentiometer set value and ADC reading should be almost linear from 0V up to the maximum. The linear correlation is never perfect, either because of the devices' implementation imperfection (STM32's ADC input and digital potentiometer output) or because of the electromagnetic noise. There are many devices in our lab room.
The ADC readings are changing slightly, but I have not changed the potentiometer value. What is going on?: The ADC in ESP32 is quite noisy, mainly when using WiFi parallelly. Refer to the Coursebook and ESP32 documentation on how to increase measurement time that will make internally many readings and return to you an average. Use the analogSetCycles(cycles)
function to increase the number of readings for the averaging algorithm. The default is 8, but you can increase it up to 255. Note that the higher the cycles
parameter value, the longer the reading takes, so tune your main loop accordingly, particularly when using an asynchronous approach (timer-based). Eventually, you can implement low-pass filters yourself (in the software).
In this scenario, we will introduce a popular DHT11 sensor. The DHT series covers DHT11, DHT22, and AM2302. Those sensors differ in accuracy and physical dimensions but can all read environmental temperature and humidity. This scenario can be run stand-alone to read weather data in the laboratory nodes' room. The DHT11 sensor is controlled with one GPIO (in all our laboratory nodes, it is GPIO D22 or PB_2 in Nucleo-style numbering) and uses a proprietary protocol.
Air temperature and humidity can be easily read using a dedicated library. You need to include two of them, as presented below:
lib_deps = adafruit/DHT sensor library@^1.4.6
Sensor readings can be sent over the network or presented on one of the node's displays (e.g. LCD), so understanding how to handle at least one of the displays is essential:
It is also possible to present the temperature as the LED colour change with a PWM-controlled LED or LED stripe. Their usage is described here:
In this scenario, we only focus on reading the sensor (temperature and humidity). Information on how to display measurements is part of other scenarios that you should refer to to create a fully functional solution (see links above).
Present the current temperature, and humidity on any display (e.g. LCD). Remember to add units (C, %Rh).
A general check to see if you can see the chosen display in the camera field of view is necessary. No other actions are required before starting development.
The steps below present only interaction with the sensor. Those steps should be supplied to present the data (or send it over the network) using other scenarios accordingly.
= Step 1 = Include the DHT library and related sensor library.
#include <DHT.h>
= Step 2 = Declare type of the sensor and GPIO pin:
#define DHTTYPE DHT11 // DHT 11 #define DHTPIN 47
= Step 3 = Declare controller class and variables to store data:
static DHT dht(DHTPIN, DHTTYPE, 50); static float hum = 0; static float temp = 0; static boolean isDHTOk = false;
= Step 4 =
Initialise sensor (mind the delay(100);
after initialisation as the DHT11 sensor requires some time to initialise before one can read the temperature and humidity:
dht.begin(); delay(100);
= Step 5 =
Reat the data and check whether the sensor works OK. In the case of the DHT sensor and its controller class, we check the correctness of the readings once the reading is finished. If something is wrong the sensor library returns the reading as NaN
. To check if the value read is OK, compare the readings with the NaN
(not a number) text, each separately:
hum = dht.readHumidity(); temp = dht.readTemperature(); (isnan(hum) || isnan(temp))?isDHTOk = false:isDHTOk = true;
NaN
numbers. Please give it some 250ms, at least, between consecutive readings, whether you do it asynchronously or using a blocking call of delay(250);
in the loop.
The observed temperature is usually between 19 and 24C, with humidity about 40-70%, depending on the weather. On rainy days, it can even go higher.
I've got NaN (Not a Number) readings. What to do?: Check if GPIO is OK (should be D22), check if you initialised controller class and most of all, give the sensor some recovery time (at least 250ms) between consecutive readings.
The temperature-only sensor DS18B20 uses a 1-wire protocol. “1-wire” applies only to the bidirectional bus; power and GND are on separate pins. The sensor is connected to the MCU using GPIO D0 only (PA_3 in Nucleo numbering). Many devices can be connected on a single 1-wire bus, each with a unique ID. Except plastic version, which we have in our laboratory (enclosure TO-92) DS18B20 also comes in a water-proof metal enclosure version that enables easy monitoring of the liquid's temperature.
To handle operations with DS18B20, we will use a dedicated library that uses a 1-wire library on a low level:
lib_deps = milesburton/DallasTemperature@^3.11.0
Sensor readings can be sent over the network or presented on one of the node's displays (e.g. LCD), so understanding how to handle at least one of the displays is essential:
In this scenario, we present how to interface the 1-wire sensor, DS18B20 (temperature sensor).
Read the temperature from the sensor and present it on the display of your choice. Show the reading in Celsius degrees. Note that the scenario below presents only how to use the DS18B20 sensor. How to display the data is present in other scenarios, as listed above. We suggest using an LCD (scenario STM_5: Using LCD Display).
Update reading every 10s. Too frequent readings may cause incorrect readings or faulty communication with the sensor. Remember, the remote video channel has its limits, even if the sensor can be read much more frequently.
Check if your display of choice is visible in the FOV of the camera once the device is booked.
The steps below present only interaction with the sensor. Those steps should be supplied to present the data (or send it over the network) using other scenarios accordingly.
= Step 1 = Include Dallas sensor library and 1-Wire protocol implementation library:
#include <OneWire.h> #include <DallasTemperature.h>
= Step 2 = Declare the 1-Wire GPIO bus pin, 1-Wire communication handling object, sensor proxy and a variable to store readings:
#define ONE_WIRE_BUS D0 static OneWire oneWire(ONE_WIRE_BUS); static DallasTemperature sensors(&oneWire); static float tempDS;
sensors
class represents a list of all sensors available on the 1-Wire bus. There is just a single one in each of our laboratory nodes.
= Step 3 = Initialise sensors' proxy class:
sensors.begin();
= Step 4 = Read the data:
sensors.requestTemperatures(); if (sensors.getDeviceCount()>0) { tempDS = sensors.getTempCByIndex(0); }
Remember not to read the sensor too frequently. 10s between consecutive readings is just fine.
Devices in the 1-Wire bus are addressable either by index (as in the example above) or by their address.
Some useful functions are present below:
DallasTemperature::toFahrenheit(tempDS)
- converts temperature in C to F,sensors.getAddress(sensorAddress, index)
- reads device address given by uint8_t index
and stores it in DeviceAddress sensorAddress
.The observable temperature is usually within the range of 19-24C. If you find the temperature much higher, check your code, and if that is okay, please contact our administrator to inform us about the faulty AC.
I've got constant readings of value 85.0. What to do?: Check if GPIO is OK (should be D0), check if you initialised controller class and call the function sensors.requestTemperatures();
. Give the OneWire bus and the sensor some recovery time (at least 250ms) between consecutive readings.
— MISSING PAGE — — MISSING PAGE — — MISSING PAGE — — MISSING PAGE — — MISSING PAGE — — MISSING PAGE — — MISSING PAGE — — MISSING PAGE —
The main module is a controller development board (controller board) equipped with the AVR ATmega2561 microcontroller. In addition to the microcontroller, the board consists of several peripherals, voltage stabilizer, connectors, JTAG programmer, Ethernet, SD memory card slot. The controller board has the following features:
The module is equipped with a AC/DC rectifier circuit and a LDO voltage stabilizer (with low dropout) -an external feeder with voltage stabilization is not needed. The module can be powered with a step down transformer with an output voltage which is greater than 6 volts and lower than 15 volts. In order to reduce power losses it is recommended to use power supply between 6-9V. The POWER LED signalizes a connected feed (“POWER” description on the board). All ATmega2561 signals are available on three connectors on the edge of the board. Connectors pin assignment is described in the next part of this instruction. It includes full descriptions of ATmega2561 pins and their alternative functions. The module is equipped with a microprocessor reset circuit (when power on) and a reset button for a microprocessor restart. A microprocessor can be programmed with an on-board JTAG programmer over USB or with an ISP interface. To the seventh pin of port B (named as PB7) the status LED (described as PB7 on the board) is connected. This LED can be used as a status indicator of application software. Low state on PB7 pin causes the status LED to be lit. The module is equipped with SD memory card slot, where it can be used as a standard microSD memory card. The memory card is connected to the microcontroller via the ISP interface and can be used to store data where data must be maintained even if the power supply is removed.
Nr | Pin | Alternative function / Description | |
---|---|---|---|
1 | VCC | - | +5 V |
2 | GND | - | GND |
3 | REF | AREF | Analog Reference Voltage For ADC |
4 | GND | - | GND |
5 | PF0 | ADC0 | ADC Input Channel 0 |
6 | GND | - | GND |
7 | PF1 | ADC1 | ADC Input Channel 1 |
8 | GND | - | GND |
9 | PF2 | ADC2 | ADC Input Channel 2 |
10 | GND | - | GND |
11 | PF3 | ADC3 | ADC Input Channel 3 |
12 | GND | - | GND |
Nr | Pin | Alternative function / Description | |
---|---|---|---|
1 | PD7 | T2 | Timer/Counter2 Clock Input |
2 | PD6 | T1 | Timer/Counter1 Clock Input |
3 | PD5 | XCK1 | USART1 External Clock Input/Output |
4 | PD4 | IC1 | Timer/Counter1 Input Capture Trigger |
5 | PD3 | INT3/TXD1 | External Interrupt3 Input or UART1 Transmit Pin |
6 | PD2 | INT2/RXD1 | External Interrupt2 Input or UART1 Receive Pin |
7 | PD1 | INT1/SDA | External Interrupt1 Input or TWI Serial Data |
8 | PD0 | INT0/SCL | External Interrupt0 Input or TWI Serial Clock |
9 | VCC | - | +5V |
10 | GND | - | GND |
11 | PB7 | OC0A/OC1C/PCINT7 | Output Compare and PWM Output A for Timer/Counter0, Output Compare and PWM Output C for Timer/Counter1 or Pin Change Interrupt 7 |
12 | PB6 | OC1B/PCINT6 | Output Compare and PWM Output B for Timer/Counter1 or Pin Change Interrupt 6 |
13 | PB5 | OC1A/PCINT5 | Output Compare and PWM Output A for Timer/Counter1 or Pin Change Interrupt 5 |
14 | PB4 | OC2A/PCINT4 | Output Compare and PWM Output A for Timer/Counter2 or Pin Change Interrupt 4 |
15 | PB3 | MISO/PCINT3 | SPI Bus Master Input/Slave Output or Pin Change Interrupt 3 |
16 | PB2 | MOSI/PCINT2 | SPI Bus Master Output/Slave Input or Pin Change Interrupt 2 |
17 | PB1 | SCK/PCINT1 | SPI Bus Serial Clock or Pin Change Interrupt 1 |
18 | PB0 | SS/PCINT0 | SPI Slave Select input or Pin Change Interrupt 0 |
19 | PE7 | INT7/IC3/CLK0 | External Interrupt 7 Input, Timer/Counter3 Input Capture Trigger or Divided System Clock |
20 | PE6 | INT6/T3 | External Interrupt 6 Input or Timer/Counter3 Clock Input |
21 | PE5 | INT5/OC3C | External Interrupt 5 Input or Output Compare and PWM Output C for Timer/Counter3 |
22 | PE4 | INT4/OC3B | External Interrupt4 Input or Output Compare and PWM Output B for Timer/Counter3 |
23 | PE3 | AIN1/OC3A | Analog Comparator Negative Input or Output Compare and PWM Output A for Timer/Counter3 |
24 | PE2 | AIN0/XCK0 | Analog Comparator Positive Input or USART0 external clock input/output |
25 | PE1 | PDO/TXD0 | ISP Programming Interface Data Output or USART0 Transmit Pin |
26 | PE0 | PDI/RXD0/INT8 | ISP Programming Interface Data Input, USART0 Receive Pin or Pin Change Interrupt 8 |
Nr | Pin | Alternative function / Description | |
---|---|---|---|
1 | GND | - | Gnd |
2 | VCC | - | +5 V |
3 | PA0 | AD0 | External memory interface address and data bit 0 |
4 | PA1 | AD1 | External memory interface address and data bit 1 |
5 | PA2 | AD2 | External memory interface address and data bit 2 |
6 | PA3 | AD3 | External memory interface address and data bit 3 |
7 | PA4 | AD4 | External memory interface address and data bit 4 |
8 | PA5 | AD5 | External memory interface address and data bit 5 |
9 | PA6 | AD6 | External memory interface address and data bit 6 |
10 | PA7 | AD7 | External memory interface address and data bit 7 |
11 | PG4 | TOSC1 | RTC Oscillator Timer/Counter2 |
12 | PG5 | OC0B | Output Compare and PWM Output B for Timer/Counter0 |
13 | PG2 | ALE | Address Latch Enable to external memory |
14 | PG3 | TOSC2 | RTC Oscillator Timer/Counter2 |
15 | PC6 | A14 | External Memory interface address bit 14 |
16 | PC7 | A15 | External Memory interface address bit 15 |
17 | PC4 | A12 | External Memory interface address bit 12 |
18 | PC5 | A13 | External Memory interface address bit 13 |
19 | PC2 | A10 | External Memory interface address bit 10 |
20 | PC3 | A11 | External Memory interface address bit 11 |
21 | PC0 | A8 | External Memory interface address bit 8 |
22 | PC1 | A9 | External Memory interface address bit 9 |
23 | PG0 | WR | Write strobe to external memory |
24 | PG1 | RD | Read strobe to external memory |
25 | GND | - | GND |
26 | 3V3 | - | +3,3 V |
The User Interface module is designed for simple tasks and basic process control. Module has three push-buttons and three LEDs, which can be used as digital inputs and outputs of microcontroller. Additionally to simple LEDs the module is equipped with 7-segment indicator, graphical LCD display, alphanumeric LCD outputs and a buzzer. User Interface module is handy to use along with other modules enabling to control the output device behavior, like motors and display the sensor readings.
Module features:
User Interface module is connected to Controller module to ports PA/PC/PG, which includes the 8-pin ports PA and PC and 6-pin port PG.
User Interface module is equipped with three buttons S1, S2, S3 which are connected to ports PC0, PC1, PC2 accordingly. The other end of buttons are connected through the resistors to ground (logical 0). LED1, LED2 and LED3 on the module are connected to the ports PC3, PC4, PC5 accordingly. The anodes of LEDs are connected to the supply (logical 1).
User Interface module is equipped with 7-segment indicator, which is connected to the microcontroller ports through the driver A6275. Driver data bus (S-IN) is connected to pin PC6, clock signal (CLK) to the pin PC7 and latch signal (LATCH) to pin PG2.
The graphical LCD display on the module is connected to port PA. In parallel the same port has external connector where all pins are aligned according to the standard 2 x 16 alphanumeric LCD 4 bit control. The user can choose whether to use the graphical or alphanumeric LCD. It is not possible to use both at the same time. The graphical LCD's background lighting can be logically controlled by the software. The back-light intensity of the alphanumeric LCD is regulated with on-board resistor and graphical LCD back-light can be controlled by the software. Both LCD displays are connected to port PA but only one at a time can be used. When selecting the alphanumeric LCD the jumper should be removed to avoid random control of graphical LCD background light.
The buzzer is connected with controller board's pin PG5 and with Vcc. It's possible to generate sound with Robotic Homelab library or with your own software.
Combo module is used in this remote Laboratory to power the motors and connect external sensors.
Combo module is able to drive following motors:
Connect following sensors:
Use the following communication interfaces:
Combo board is connected with controller using PA-PB-PC-PD-PE-PF ports. DC and stepper motor feeds come from external power cord, servo motors feed comes trough 5V voltage regulator also from external power cord. Motors power circuit is separated from controller's power circuit. If external power feed is correctly connected to the Combo board the green PWR led will lit.
DC motors is connected to the DC group of pins. Every pair of pins can control 1 dc motor, thus it's possible to manipulate 4 dc motors. Combo board uses bd6226fp H-bridge to control dc motors. It's possible to manipulate some other device, what can be controlled digitally and it's current is smaller than 1 A and voltage does not exceed 18 V, beside dc motor with DC pins (Piezoelectric generator, relay etc).
AVR pin | Signal | AVR pin | Signal |
---|---|---|---|
PB4 | Motor 1 A | PD6 | Motor 3 A |
PB7 | Motor 1 B | PD7 | Motor 3 B |
PD0 | Motor 2 A | PD4 | Motor 4 A |
PD1 | Motor 2 B | PD5 | Motor 4 B |
Servo is connected to Servo group of pins. Ground wire is connected to the GND pin which is the pin nearest to the board edge. It's possible to use 2 servomotors at once. Signal pins on the Combo module is directly connected to the controller's timer's output pins.
AVR pin | Signal | Socket |
---|---|---|
PB5(OC1A) | PWM1 | Top |
PB6(OC1B) | PWM2 | Bottom |
Combo module has 4 ADC input pins, every ADC input pin forms a group with Vcc (+5 V) and ground which is marked as GND, that makes them ideal to use to connect analog sensors.
AVR pin | Signal | Socket |
---|---|---|
PF0(ADC0) | ADC0 | Top |
PF1(ADC1) | ADC1 | Bottom |
PF2(ADC2) | ADC2 | Top |
PF3(ADC3) | ADC3 | Bottom |
Combo module has 4 groups of pins where to connect digital sensors. Every group consist of +5 V also called Vcc, ground also called GND and signal pin.
AVR pin | Signal | Socket |
---|---|---|
PE2(XCK0/AIN0) | PE2 | Top |
PE3(OC3A/AIN1) | PE3 | Bottom |
PE4(OC3B/INT4) | PE4 | Top |
PE5(OC3C/INT5) | PE5 | Bottom |
Combo module has with 8 input pins parallel in serial out shift register 74HC/HCT165, what is able to read in 8 digital signals and convert it to 8 bit digital number. Those inputs are suitable for line follower applications.
AVR pin | Signal |
---|---|
PA7(AD7) | Q7 |
PC7(A15) | CP |
PA6(AD6) | PL |
GND | CE |
Necessary knowledge: [HW]User Interface Module, [ELC]LED Resistor Calculation, [AVR]Registers, [AVR] Digital Inputs/Outputs, [LIB]Bitwise Operations, [LIB]Pins
A light-emitting diode is a semiconductor which emits light when forward voltage is applied. The acronym for light-emitting diode is LED. There are different color combination of diodes and the diodes, which can also emit white light. Like a normal diode, the LED has two contacts – anode and cathode. On drawings the anode is marked as “+” and cathode as “-“.
When forward voltage is applied, an LED’s anode is connected to the positive voltage and the cathode to the negative voltage. The voltage of the LED depends on the LED’s color: longer wavelength (red) ~2 V, shorter wavelength (blue) ~3 V. Usually the power of a LED is no more than a couple of dozen milliwatts, which means electrical current must be in the same range. When applying greater voltage or current a LED may burn out.
If the LEDs are used specially to illuminate, it is wise to use special electronic circuits which would regulate current and voltage suited for LEDs. However LEDs are quite often used as indicators and they are supplied directly from microcontroller’s pins. Since the supply voltage for microcontrollers is usually higher than the voltage for LEDs, there must be a resistor connected into series with the LED, which limits current and creates the necessary voltage drop. Instructions to calculate proper resistor can be found in the electronics chapter.
LEDs are produced in a variety of casings. Most common LEDs with feet have 3 mm or 5 mm diameter round shell and two long metal connector pins. Longer pin is the anode, the shorter one is the cathode. Surface mounted casing LEDs (SMD – Surface Mounted Device) have a T-shaped symbol on the bottom to indicate the polarity, where the roof of T stands for the location of the anode and the pole marks the cathode.
The HomeLab controller control module has one single indicator LED, whose anode is connected through resistor to a power supply and the cathode is connected to the controllers pin. In order to switch on and off this LED, LED pin should be defined as the output and set low or high accordingly. Which means if the pin is set high, the LED is turned off and if the pin is set low, the LED is turned on. Basically it would be possible to connect the LED also so that the anode is connected to the pin of microcontroller, and the cathode is connected to the earth (somewhere there has to be a resistor too) – in that case when the pin is set as high, the LED shines and when the pin is set as low the LED is switched off.
All practical examples for the HomeLab kit, LED switching included, use HomeLab’s pin library. Pin library includes data type pin, which contains addresses of pin related registers and pin bitmask. If to create a pin type variable in the program and then initialize it by using macro function PIN, the pin can be used freely with this variable (pin) through whole program without being able to use registers. Here are 2 example programs, which are doing exactly the same thing, but one is created on the basis of HomeLab’s library, the other is not. The debug LED, led_debug in HomeLab library, has been described as PB7 (HomeLab I & II) and PQ2 (HomeLab III). The Debug LED is physically located in the Controller module.
// HomeLab Controller module LED test program, which // is based on HomeLab library #include <homelab/pin.h> // LED pin configuration. pin led_debug = PIN(Q,2); // Main program int main(void) { // Configuring LED pin as an output pin_setup_output(led_debug); // Lighting up LED pin_clear(led_debug); }
// HomeLab II Controller module LED test program, which // accesses registers directly #include <avr/io.h> // Main program int main(void) { // Configuring LED pin as an output DDRB |= (1 << 7); // Lighting up LED PORTB &= ~(1 << 7); }
First example uses pins’ library (pin.h file). First a pin-type variable named debug led is created in the program, which holds information about LED pin. In the main program this pin will be set as output by using pin_setup_output function. After that the pin is set as low by function pin_clear. As the result LED will glow. In the second example variables are not used, setting LED output and lighting it will be done by changing port B data direction and output registers values. The reader who knows more about AVR notices, that in both examples there is no need to give command to light LED, because the default output value of the AVR is 0 anyway, but here it is done by the means of correctness.
What is the difference between the use of the library and the registers? The difference is in the comfort – library is easier, because you do not need to know the registers’ names and their effects. Most important benefit of library is adaptability. Using registers, you must change registers’ names and bitmasks through entire program in order to change pin. When using library, it must be done only in the beginning of the program where pin variable is initialized. Using registers has one deceptive advantage – usage of pin is direct and it is not done through program memory and time consuming functions. However, newer AVR-GCC compiler versions are so smart that they transform library’s functions to exactly same direct commands for manipulating registers like it would have been done directly in program. Must be said that compilers can optimize the code only when it deals with constant single variables not with volatile variables that are changing during work and with arrays.
The next program code is partial pin operations library. Its purpose is to explain the procedures with pin variables. It might not be understandable for the beginners as it uses C language pointers which are not covered in this book, but a lot of materials about pointers can be found from books and internet.
// Defining the Pins inside the pin struct // pin name = PIN(PORT LETTER, PIN NUMBER IN PORT); pin led_green = PIN(H,5); // Configuring pin as output inline void pin_setup_output(pin pin){ bitmask_set(*pin.ddr, pin.mask); } // Setting pin high inline void pin_set(pin pin){ bitmask_set(*pin.port, pin.mask); } // Setting pin low inline void pin_clear(pin pin){ bitmask_clear(*pin.port, pin.mask); }
In addition to the Controller module, LEDs are also located on the User interface module board. They are connected electrically in the same way as Controller module’s LED, which means cathode is connected to the AVR pin. For more information see the modules hardware guide. In addition to pin_set and pin_clear commands one can use led_on and led_off commands to control LED pins. The following table shows LEDs constants which are described in the library and the corresponding Controller module pins. Green, yellow and red LEDs are located in the user interface module.
Constant name | Alternative name | HomeLab I & II pin | HomeLab III pin | Description |
---|---|---|---|---|
led_debug | LED0 | PB7 | PQ2 | Blue LED on the Controller module |
led_green | LED1 | PC3 | PH5 | Green LED |
led_yellow | LED2 | PC4 | PH4 | Yellow LED |
led_red | LED3 | PC5 | PH3 | Red LED |
HomeLab library based example program which uses LEDs constants looks as follows:
// LED test program for HomeLab User interface module #include <homelab/pin.h> // Main program int main(void) { // Configuring LED pins as an output pin_setup_output(led_red); pin_setup_output(led_yellow); pin_setup_output(led_green); // Lighting up red and green LED led_on(led_red); led_on(led_green); // Turn off yellow LED led_off(led_yellow); }
Necessary knowledge:
[HW] User Interface Module,
[LIB] 7-segment LED Display,
[LIB] Delay
[PRT] Light-emitting Diode
7-segmented LED number-indicator is a display which consists of 7 LEDs positioned in the shape of number 8. By lighting or switching off the corresponding LEDs (segments), it is possible to display numbers from 0 to nine as well as some letters.
Electrically all anodes of the LEDs are connected to one anode pin ca. LEDs are lit by switching their cathodes (a, b, c…). Exists also reversed connections, where the indicators have a common cathode cc. Generally several number-indicators are used for displaying multi digit numbers - for this purpose the indicators are equipped with coma (point) segment dp. All in all one indicator has 8 segments, but they are still called 7-segmented according to the number of number-segments.
LED number-indicators are easy to use, they can be controlled directly from the pins of the microcontroller, but there are also special drivers, which able to control number-indicators using fewer pins of the microcontroller. There are different colors of LED number indicators, which can be very bright and very large. For displaying the entire Latin alphabet exist indicators with extra segments. There are different drivers, but common drivers using a serial interface, which is similar to the SPI, where both clock signal and data signal are used. Different from SPI the chip-select is not used there, and is replaced with latch function. The above mentioned three lines are connected to the controller pins.
There is one 7-segment LED number-indicator on the Digital i/o module. It is controlled through a driver with serial interface. For displaying the numbers on the HomeLabs Digital i/o module indicator, the following functionality is written to the library of the HomeLab.
// Marking card // The bits are marking the segments. Lower ranking is A, higher ranking is DP const unsigned char __attribute__ ((weak)) segment_char_map[11] = { 0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110, 0b01101101, 0b01111100, 0b00000111, 0b01111111, 0b01100111, 0b01111001 // E like Error }; // Start-up of the 7-segment indicator void segment_display_init(void) { // Set latch, data out and clock pins as output pin_setup_output(segment_display_latch); pin_setup_output(segment_display_data_out); pin_setup_output(segment_display_clock); } // Displaying number on the 7-segment indicator void segment_display_write(unsigned char digit) { unsigned char map; signed char i; // Check-up of the number if (digit > 9) { digit = 10; } // Number as the card of the segments map = segment_char_map[digit]; // Latch-signal off pin_clear(segment_display_latch); // Sending he bits. Higher ranking goes first for (i = 7; i >= 0; i--) { // Setting the pin according to the value of the bit of the card pin_set_to(segment_display_data_out, bit_is_set(map, i)); // The clock-signal as high for a moment pin_set(segment_display_clock); _delay_us(1); // The clock-signal as low pin_clear(segment_display_clock); _delay_us(1); } // Latch-signal on pin_set(segment_display_latch); }
For displaying numbers and the letter “E”, is created a “weak” constant array segment_char_map, where lighting of all 8 segments is marked with bit 1 and switch off is market with bit 0. The bits form lower to higher (from right to left in binary form) are marking segments A, B, C, D, E, F, G ja DP. The control interface of the driver is realized through software SPI, i.e. by using a software for controlling the data communication pins in the program. All three pins are set as output with segment_display_init function. segment_display_write is for displaying the function, which finds the segment-card of the mark from the array and transmits bit by bit all values of the segments to the driver. The frequency of the clock signal with the software delays is now approximately 500 kHz. When a user defines a variable segment_char_map its own code, it is possible to create other characters on the screen (eg, text, etc.)
The following is a more concrete example of a program for using the number-indicator. Previously described function of the library is described in the program. The program counts numbers from 0 to 9 with approximate interval of 1 second and then displays letter E, because two-digit numbers is not possible to show on the one digit indicator.
// The example program of the 7-segment LED indicator of the HomeLab's #include <homelab/module/segment_display.h> #include <homelab/delay.h> // Main program int main(void) { int counter = 0; // Set-up of the 7-segment indicator segment_display_init(); // Endless loop while (true) { // Displaying the values of the counter segment_display_write(counter % 10); // Counting up to 10 counter++; if (counter>19) counter=0; // Delay for 1 second sw_delay_ms(1000); } }
Necessary knowledge:
[HW] User Interface Module,
[LIB] Alphanumeric LCD, [LIB] Graphic LCD,
[LIB] Delay
LCD screens, which are used with microcontrollers, can be divided mainly into a two parts: a text or alphanumeric LCD and a graphic LCD. Alphanumeric LCD is liquid crystal display, with the purpose of displaying letters and numbers. In basic LCD is used liquid crystal which is placed between transparent electrodes, and which changes the polarization of the passing light in electrical field. The electrodes are covered by polarization filters, which assure that only one way polarized light can pass the screen. If the liquid crystal changes its polarity due to an electrical field, the light can not pass the screen or part (segment) of it and it looks dark.
Main characteristic of alphanumerical LCD is the placing of its segments. The screen is divided into many indicators. Each indicator has either enough segments for displaying letters and numbers or it is formed from matrix of little square segments (pixels). For example, a matrix of 5×7 pixels is enough to display all numbers, and letters of Latin alphabet. There are usually 1 – 4 rows of indicators and 8 – 32 columns. Each indicator has a small difference similar to the differences of the letters in text.
Besides the screen Alphanumerical LCD has also controller which controls the segments of the screen according to the commands from the communication interface. A controller has a preprogrammed card of letters, where each letter, number or symbol has its own index. Displaying the text on the screen is basically done by sending the indexes to the controller. In reality there must be more control orders sent to the controller before anything can be displayed. It is important to get familiarize with each LCD data-sheet, because there are many different types of LCDs and they all are controlled differently.
Graphical LCD liquid crystal display is a display which allows displaying pictures and text. Its construction is similar to the alphanumerical LCD, with a difference that on the graphic display all pixels are divided as one large matrix. If we are dealing with a monochrome LCD, then a pixel is one square segment. Color displays’ one pixel is formed of three subpixels. Each of the three subpixels lets only one colored light pass (red, green or blue). Since the subpixels are positioned very close to each other, they seem like one pixel.
Monochrome graphic displays have usually passive matrix, large color displays including computer screens have active matrix. All information concerning the color of the background and the pixels of the graphic LCDs are similar to alphanumerical LCDs. Similar to the alphanumerical displays, graphic displays are also equipped with separate controller, which takes care of receiving information through the communication interface and generates the electrical field for the segments.
In the HomeLab III set is a 128×160 pixels and 1,8 inch full color TFT LCD screen. Sitronixi ST7735 controller is attached to the display which can be communicated through SPI serial interface. The background lighting of the display module is separately controlled, but without the background light, it is not possible to use the screen. Communicating with the display is not very difficult, but due to the large amount of the functions it is not explained here. Home-Labs library has functions for using it.
Main difference compared to from HomeLab III to HomeLab II is a 84×48 pixels monochrome graphic LCD. It is the same display as used in Nokia 3310 mobile phones. Philips PCD8544 controller is attached to the display which can be communicated through SPI-like serial interface. The background lighting of the display module is separately controlled.
First, the graphical LCD screen must be started with lcd_gfx_init function. There is a letter map in side of the library with full Latin alphabet, numbers and with most common signs written. To display a letter or text, first its position must be determined by using function lcd_gfx_goto_char_xy. For displaying s letter is lcd_gfx_write_char function and for displaying text lcd_gfx_write_string function.
The following is an example of time counter. The program counts seconds (approximately), minutes and hours. For converting time to text sprintf function is used.
// Example of using the graphic LCD of the HomeLab // Time of day is displayed on LCD since the beginning of the program #include <stdio.h> #include <homelab/module/lcd_gfx.h> #include <homelab/delay.h> // Main program int main(void) { int seconds = 0; char text[16]; // Set-up of the LCD lcd_gfx_init(); // Cleaning the screen lcd_gfx_clear(); // Switching on the background light lcd_gfx_backlight(true); // Displaying the name of the program lcd_gfx_goto_char_xy(1, 1); lcd_gfx_write_string("Time counter"); // Endless loop while (true) { // Converting the seconds to the form of clock // hh:mm:ss sprintf(text, "%02d:%02d:%02d", (seconds / 3600) % 24, (seconds / 60) % 60, seconds % 60); // Displaying the clock text lcd_gfx_goto_char_xy(3, 3); lcd_gfx_write_string(text); // Adding one second seconds++; // Hardware delay for 1000 ms hw_delay_ms(1000); } }
Necessary knowledge: [HW] User Interface Module, [ELC] Voltage Divider, [AVR] Analog-to-digital Converter, [LIB] Analog to Digital Converter, [LIB] Graphic LCD, [LIB] Sensors
A thermistor is a type of resistor which resistance varies with temperature. There are two types of thermistors: positive temperature coefficient of resistance and negative temperature coefficient of resistance. The resistance of thermistors with positive temperature coefficient of resistance is increasing when the temperature grows and with negative the resistance decreases. The respective abbreviations are PTC (positive temperature coefficient) and NTC (negative temperature coefficient).
The thermistors resistances' dependence of the temperature is not linear and this complicates the usage of it. For accurate temperature measurements in wider temperature flotation the Steinhart-Hart third-order exponential equation is used as the thermistors resistance is linear only in small temperature range. The following simplified Steinhart-Hart equation with B-parameter exists for NTC thermistors:
where:
Parameter B is a coefficient, which is usually given in the datasheet of the thermistor. But it is stable enough constant only in a certain ranges of temperature, for example at ranges 25–50 °C or 25–85 °C. If the temperature range measured is wider , the data sheet should be used for retrieving the equation.
Usually a voltage-divider is used for measuring the resistance of a thermistor, where one resistor is replaced with a thermistor and the input voltage is constant. The output voltage of the voltage-divider is measured, which changes according to the change of the resistance of the thermistor. If the voltage is applied, current goes through the thermistor which heats up the thermistor due to thermistors resistance and therefore alters again the resistance. The fault caused by heating up of the thermistor can be compensated with calculations, but it is easier to use a thermistor that has higher resistance and therefore heats up less.
With restricted resources and with less demands on accuracy, previously calculated charts and tables for temperatures are used. Generally the tables have ranges of temperatures and respective values of resistance, voltage or analogue-digital converters. All exponential calculations are already done and the user needs to only find the correct row and read the temperature given.
The Sensor module of the HomeLab is equipped with a NTC type thermistor which has 10 kΩ nominal resistance. At temperatures 25-50 °C the parameter B of the thermistor is 3900. One pin of the thermistor is connected to supply and the other one is connected to the analogue-digital converter (HomeLab II channel 2 and HomeLab III channel 14). A typical 10 kΩ resistor is also connected with the same pin of the microcontroller and earth and together with the thermistor forms a voltage divider. Since we are dealing with a NTC thermistor, which resistance decreases as the temperature grows; the output voltage of the voltage divider is increasing repectively with growing temperature.
While using the AVR it is practical to use a conversion table of values of temperature and analogue-digital converter to find the correct temperature. It is wise to find corresponding value of analogue-digital converter for each temperature degree of desired range of temperature because reverse table will be too large due to the amount of 10 bit ADC values. It is recommended to use any kind of spreadsheet program (MS Excel, LibreOffice Calc, etc.) to make the table. Steinhart-Hart formula which is customized for the mentioned NTC thermistors able's to find the resistance of the thermistor which corresponds to the temperature. Derived from the resistance, is possible to calculate the output voltage of the voltage divider and using this output voltage to calculate the value of the ADC. Calculated values can be inserted to the program as follows:
// Table for converting temperature values to ADC values // Every element of the array marks one Celsius degree // Elements begin from -20 degree and end at 100 degree // There are 121 elements in the array const signed short min_temp = -20; const signed short max_temp = 100; const unsigned short conversion_table[] = { 91,96,102,107,113,119,125,132,139,146,153, 160,168,176,184,192,201,210,219,228,238,247, 257,267,277,288,298,309,319,330,341,352,364, 375,386,398,409,421,432,444,455,467,478,489, 501,512,523,534,545,556,567,578,588,599,609, 619,629,639,649,658,667,677,685,694,703,711, 720,728,736,743,751,758,766,773,780,786,793, 799,805,811,817,823,829,834,839,844,849,854, 859,863,868,872,876,880,884,888,892,896,899, 903,906,909,912,915,918,921,924,927,929,932, 934,937,939,941,943,945,947,949,951,953,955 };
Following algorithm may be used to find the temperature which corresponds to the parameters of the ADC:
// Converting the ADC values to Celsius degrees: signed short thermistor_calculate_celsius(unsigned short adc_value) { signed short celsius; // Covering the table backwards: for (celsius = max_temp - min_temp; celsius >= 0; celsius--) { // If the value in the table is the same or higher than measured // value, then the temperature is at least as high as the // temperature corresponding to the element if (adc_value >= conversion_table[celsius])) { // Since the table begins with 0 but values of the elements // from -20, the value must be shifted return celsius + min_temp; } } // If the value was not found the minimal temperature is returned return min_temp; }
The algorithm searches range from the table where the ADC value is and acquires the lower ranking number of this range. The ranking number marks degrees, adding the primary temperature to this a temperature with accuracy of 1 degree is reached.
This conversion table and function are already in the library of the HomeLab, therefore there is no need to write them for this exercise. In the library the conversion function is named thermistor_calculate_celsius. Must be considered, that the conversion is valid only when used on the thermistor on the Sensors module of the HomeLab. For using other thermistors, a conversion table needs to be created and more complex function described in the manual of the library must be used. Example program of this exercise is a thermometer, which measures temperature in Celsius scale and displays it on an alphabetical LCD.
// Example program of the thermistor of Sensors module // The temperature is displayed on the LCD #include <stdio.h> #include <homelab/adc.h> #include <homelab/module/sensors.h> #include <homelab/module/lcd_gfx.h> #include <homelab/delay.h> // Robotic Homelab II //#define ADC_CHANNEL 2 // Robotic Homelab III #define ADC_CHANNEL 14 // Main program int main(void) { unsigned short value; signed short temperature; char text[16]; // Initialization of LCD lcd_gfx_init(); // Clearing the LCD and setting backlight lcd_gfx_clear(); lcd_gfx_backlight(true); // Name of the program lcd_gfx_goto_char_xy(1, 1); lcd_gfx_write_string("Thermometer"); // Setting the ADC adc_init(ADC_REF_AVCC, ADC_PRESCALE_8); // Endless loop while (true) { // Reading the 4 times rounded values of the voltage of the // thermistor value = adc_get_average_value(ADC_CHANNEL, 4); // Converting the values of ADC into celsius scale temperature = thermistor_calculate_celsius(value); // Converting the temperature in to text // To display the degree sign, the octal variable is 56 sprintf(text, "%d\56C ", temperature); // Displaying the text in the beginning of the third row of the LCD lcd_gfx_goto_char_xy(5, 3); lcd_gfx_write_string(text); hw_delay_ms(1000); } return 0; }
Necessary knowledge: [HW] User Interface Module, [ELC] Voltage Divider, [AVR] Analog-to-digital Converter, [LIB] Analog to Digital Converter, [LIB] Graphic LCD, [LIB] Sensors
A photoresistor is a sensor which electrical resistance is altered depending on the light intensity falling on it. The more intense is the light the more free carriers are formed and therefore the lower gets the resistance of the element. Two exterior metal contacts of the photoresistor are reaching through the ceramic base material to the light sensitive membrane, which determines the electrical resistance properties with its geometry and material properties. Since photo sensitive material itself has high resistance, with narrow, curvy track between the electrodes, low total resistance at average light intensity is gained. Similarly to the human eye, the photoresistor is sensitive at certain range of wavelengths and needs to be considered when selecting a photo element, otherwise it may not react to the light source used in the application. Following is simplified list of wavelengths of visible light segmented by colours:
Colour | Range of wavelength (nm) |
---|---|
Purple | 400 – 450 |
Blue | 450 – 500 |
Green | 500 – 570 |
Yellow | 570 – 590 |
Orange | 590 – 610 |
Red | 610 – 700 |
A range of working temperature is set for photoresistor. Wishing the sensor to work at different temperatures, precising conversions must be executed, because the resisting properties of the sensors are depending on the temperature of the ambient.
For characterizing light intensiveness physical concept called light intensity (E) is used, this shows the quantity of light reaching any given surface. Measuring unit is lux (lx), where 1 lux represents, the even flow of light 1 lumen, falling on a surface of 1 m2. Hardly ever in reality falls light (living area) on a surface evenly and therefore light intensity is reached generally as a average number. Below are few examples of light intensity for comparison:
Environment | Intensity of light (lx) |
---|---|
Full moon | 0,1 |
Dusk | 1 |
Auditorium | 10 |
Class room | 30 |
Sunset or sunrise | 400 |
Operating room (hospital) | 500 - 1000 |
Direct sun light | 10000 |
The HomeLab is equipped with VT935G photoresistor. One pin of the photoresistor is connected to power supply and second pin to the analogue-digital converter (HomeLab II channel 1, HomeLab III channel 13). Between this pin and the ground resistor is also connected, which forms a voltage divider with the photoresistor. Since the electrical resistance of the photoresistor is decreasing as the light intensity falling on it grows, the measured voltage on the pin of the microcontroller grows as light intensity grows. It is worth to take into account that the photoresistor used in the HomeLab reacts most on orange and yellow light.
The sensor VT935G is not meant to be a specific measuring device. It is meant to be more a device to specify overall lighting conditions – is there a lighted lamp in the room or not. In this case one has to just measure the resistance of the sensor in the half dark room, note it in the program and compare measured values – is it lighter or darker.
The exercise here is a little bit more complex as the light intensity is measured also in lux. For doing this, exists an approximate formula and floating-point variables. In the C-language are floating-point variables float- and double-type variables, which can be used to present fractions. Their flaw is high demand of resources. Computers have special hardware to calculate floating-point variables, in the 8-bit AVR microcontroller calculations are executed in software which demands a lot of memory and time. If the flaws are not critical, the floating-point variables are worth using.
There is an approximate formula showing the relationship between the intensity of light and electrical resistance in the sensor datasheet. As seen on the graph (on the right), with using logarithm scale, the resistance and intensity of light are almost in linear relationship and form a in-line formula, because following conversion applies:
log(a/b) = log(a) - log(b)
The relation is characterised by the ascent of the factor γ (ascend of the line), which is 0,9 on VT935G sensor. We have also data on one of the points on that line: resistance 18.5 kΩ (RA) at 10 lx intensity of light (EA). Hence we have the coordinates of one point as well as the ascent of the line and for calculating any other point, we only need one coordinate. Meaning, if sensors' resistance (RB) is measured, it is possible to calculate from the equation of line, the intensity of light EB) that falls on the sensor. Finding EB from the equation of line:
log(EB) = log(RA/RB) / γ + log(EA)
EB = 10log(RA/RB) / γ + log(EA)
This gives the formula for calculating the intensity of light when the resistance is known. The resistance can not be measured directly with microcontroller. For this the photoresistor is in the voltage divider. The output voltage of this voltage divider is converted to a specific variable by the analogue-digital converter (ADC). To find the resistance, the output voltage (U2) of the voltage divider must be calculated first, using the ADC value, also comparison voltage (Uref) of the converter must be taken into account: The formula is following:
U2 = Uref * (ADC / 1024)
From the formula for voltage divider(check the chapter on voltage divider) the resistance of the upper photoresistor (R1) can be found:
R1 = (R2 * U1) / U2 - R2
In the following calculation of voltage and resistance, the known factors are replaced with numbers and indexes have been removed:
U = 5 * (ADC / 1024)
R = (10 * 5) / U - 10
For finding the intensity of light, simplifying conversions can be done:
E = 10log(18.5/R) / 0.9 + 1 = 10log(18.5/R) * 10/9 * 101 =
= 10log18.5*10/9 - logR*10/9 * 10 = (10log18.5*10/9 / 10logR*10/9) * 10 =
= (18.510/9 / R10/9) * 10 = 18.510/9 * 10 * R-10/9
By calculating the constant in front of the variable of the field R, the expression remains follows:
E = 255,84 * R-10/9
These formulas help only if the photoresistor on the module of the HomeLab is used. If circuit is used equipped with different components, respective variables need to be changed. Next, source code of the example program is presented, which measures and calculates using ADC and displays the intensity of light on the LCD.
In the example program variables of voltage, resistance and intensity are defined using type double of floating-point variables. The variables which should be used as floating-point variables must always contain a decimal point (it can be also just 0, because then the compiler understands it correctly).
// HomeLab photoresistor demonstration // LCD screen displays the approximate illuminance in lux #include <stdio.h> #include <math.h> #include <homelab/module/lcd_gfx.h> #include <homelab/adc.h> #include <homelab/delay.h> // Main program int main(void) { char text[16]; unsigned short adc_value; double voltage, resistance, illuminance; // Initializing the LCD lcd_gfx_init(); // Setting LCD backlight to work lcd_gfx_backlight(true); // Clearing the LCD. lcd_gfx_clear(); //Cursor on the position lcd_gfx_goto_char_xy(3, 2); // Name of the program lcd_gfx_write_string("Luxmeter"); // Setting the ADC adc_init(ADC_REF_AVCC, ADC_PRESCALE_8); // Endless loop. while (1) { // Reading the average value of the photoresistor adc_value = adc_get_average_value(13, 10); // HomeLab II //adc_value = adc_get_average_value(1, 10); // Calculating the voltage in the input of the ADC // HomeLab II //voltage = 5.0 * ((double)adc_value / 1024.0); // HomeLab III voltage = 2.0625 * ((double)adc_value / 2048.0); // Calculating the resistance of the photoresistor // in the voltage divider // HomeLab II //resistance = (10.0 * 5.0) / voltage - 10.0; // HomeLab III resistance = (33.0) / voltage - 10.0; // Calculating the intensity of light in lux illuminance = 255.84 * pow(resistance, -10/9); // Dividing variable into two integer variable // to display it on the screen int8_t illu = illuminance; int16_t illudp = trunc((illuminance - illu) * 1000); // Converting the intensity of light to text sprintf(text, "%3u.%3u lux ", illu,illudp); // Displaying it on the LCD lcd_gfx_goto_char_xy(3, 3); lcd_gfx_write_string(text); // Delay 500 ms sw_delay_ms(500); } }
Necessary knowledge:
[HW] User Interface Module,
[AVR] Analog-to-digital Converter,
[LIB] Analog to Digital Converter, [LIB] Graphic LCD, [LIB] Sensors
For measuring the distance to an object there are optical sensors using triangulation measuring method. Company “Sharp” produces most common infra-red (IR) wavelength using distance sensors which have analogue voltage output. The sensors made by “Sharp” have IR LED equipped with lens, which emits narrow light beam. After reflecting from the object, the beam will be directed through the second lens on a position-sensible photo detector (PSD). The conductivity of this PSD depends on the position where the beam falls. The conductivity is converted to voltage and if the voltage is digitalized by using analogue-digital converter, the distance can be calculated. The route of beams reflecting from different distance is presented on the drawing next to the text
The output of distance sensors by “Sharp” is inversely proportional, this means that when the distance is growing the output is decreasing (decreasing is gradually slowing). Exact graph of the relation between distance and output is usually on the data-sheet of the sensor. All sensors have their specific measuring range where the measured results are creditable and this range depends on the type of the sensor. Maximum distance measured is restricted by two aspects: the amount of reflected light is decreasing and inability of the PSD registering the small changes of the location of the reflected ray. When measuring objects which are too far, the output remains approximately the same as it is when measuring the objects at the maximum distance. Minimum distance is restricted due to peculiarity of Sharp sensors, meaning the output starts to decrease (again) sharply as the distance is at certain point (depending on the model 4-20 cm). This means that to one value of the output corresponds two values of distance. This problem can be avoided by noticing that the object is not too close to the sensor.
The HomeLab set of sensors includes IR distance sensor SHARP GP2Y0A21YK. Measuring range of the sensor is 10 cm – 80 cm. The output voltage of this sensor is, depending on the distance measured, up to 3 V. The distance sensor can be connected to any ADC (the analogue-digital converter) channel of the HomeLab module. On the basis of previous exercises of sensors, it is easy to write a program which measures the output voltage of the distance sensors, but in addition, this exercise includes converting this output voltage to distance.
On the datasheet of the GP2Y0A21YK is graph of relation between its output voltage and measured distance. This graph is not a linear one, however the graph of inverse values of output voltage and distance almost is, and from that is quite easy to find the formula for converting voltage to distance. To find the formula, the points of the same graph must be inserted to any kind of spreadsheet application and then generate a new graph. In spreadsheet programs is possible to calculate automatically the trend-line. Next, the graph of GP2Y0A21YK corrected output voltage inverse value’s relation to the corrected inverse value of measured distance with linear trend-line is presented. To simplify, the output voltage is already converted to 10 bit +5 V values of analogue-digital converter with comparison voltage.
As seen on the graph, the trend-line (blue) overlaps quite precisely with the points of the graph. Such overlapping is achieved by using the help of the corrective constant. This corrective constant is discovered by using the trial-and-error method – many variables were tested until such was found which made the graph overlap the trend-line the most. This corrective constant of present graph is +2; this means that to all real distances +2 is added. This way the graph is very similar to the linear trend line and a generalization can be made and say that the relation between the distance and the voltage is following:
1 / (d + k) = a * ADC + b
where
Distance d can be expressed from the formula:
d = (1 / (a * ADC + B)) - k
Now it is basically possible to calculate the distance by using this formula, but this requires floating-point calculations, since while dividing fractions will occur. Because the microcontroller operates using integers, the formula must be simplified and converted to larger ratios. Then when dividing the quotient with a linear-member it will look as follows:
d = (1 / a) / (ADC + B / a) - k
When introducing the corrective constant to the formula and also the linear-member and the free-member from the trend-line equation, the formula for calculating the distance will be:
d = 5461 / (ADC - 17) - 2
This formula is computable with 16-bit integers and completely suitable to AVR. Before calculating, must be ensured that the value of the ADC is over 17, otherwise dividing with 0 or negative distance may occur.
Following is the function for converting the values of ADC to centimeters, it is written in the library of the HomeLab. Linear- and free-member and corrective constant are not stiffly written into the function, they are fed with the structure object parameters of the IR distance sensor. By holding the parameters in separate constant, it is easy to add new IR distance sensors to the program.
// The structure of the parameters of the IR distance sensors typedef const struct { const signed short a; const signed short b; const signed short k; } ir_distance_sensor; // The object of the parameters of GP2Y0A21YK sensor const ir_distance_sensor GP2Y0A21YK = { 5461, -17, 2 }; // Converting the values of the IR distance sensor to centimeters // Returns -1, if the conversion did not succeed signed short ir_distance_calculate_cm(ir_distance_sensor sensor, unsigned short adc_value) { if (adc_value + sensor.b <= 0) { return -1; } return sensor.a / (adc_value + sensor.b) - sensor.k; }
To make the conversion the function ir_distance_calculate_cm must be engaged. The first parameter of this function is the object of the parameters of the IR distance sensor, second is the value of the ADC. The function returns the calculated distance in centimeters. If the operation is wrong (unnatural value of the ADC) the returned value is -1. Following program demonstrates the use of IR distance sensor and conversion function. Graphical LCD is used, where measured results are displayed. If the distance is unnatural “?” is displayed.
// The example program of the IR distance sensor of the HomeLab // Measured results in centimeters is displayed on the LCD #include <stdio.h> #include <homelab/adc.h> #include <homelab/delay.h> #include <homelab/module/sensors.h> #include <homelab/module/lcd_gfx.h> #define ADC_CHANNEL 0 // Main program int main(void) { signed short value, distance; char text[16]; // Robotic HomeLab II external sensors pin of Sensor module //pin ex_sensors = PIN(G, 0); //pin_setup_output(ex_sensors); //pin_set(ex_sensors); // Initialization of LCD lcd_gfx_init(); lcd_gfx_clear(); lcd_gfx_goto_char_xy(1,2); lcd_gfx_write_string("Distance sensor"); // Setup of the ADC adc_init(ADC_REF_AVCC, ADC_PRESCALE_8); // Endless loop while (1) { // Reading the 4 times rounded value of output voltage value = adc_get_average_value(ADC_CHANNEL, 4); // Conversing ADC value to distance distance = ir_distance_calculate_cm(GP2Y0A21YK, value); lcd_gfx_goto_char_xy(1,3); // Was the calculation successful? if (distance >= 0) { // Conversing distance to text sprintf(text, "%3d cm ", distance); } else { // Creating the text for unknown distance sprintf(text, "Error "); } lcd_gfx_goto_char_xy(1,3); lcd_gfx_write_string(text); sw_delay_ms(500); } }
Necessary knowledge: [HW] User Interface Module, [HW] Combo module, [AVR] Counters/Timers, [AVR] Analog-to-digital Converter, [LIB] Motors, [LIB] Analog to Digital Converter
Permanent magnet DC motors are very common in different applications, where small dimensions, high power and low price are essential. Due to their fairly high speed, they are used together with transmission (to output lower speed and higher torque).
Permanent magnet DC motors have quite simple construction and their controlling is quite elementary. Although controlling is easy, their speed is not precisely determined by the control signal because it depends on several factors, primarily of the torque applied on the shaft and feeding current. The relationship between torque and speed of a ideal DC motor is linear, which means: the higher is the load on the shaft the lower is the speed of the shaft and the higher is the current through the coil.
Brushed DC motors are using DC voltage and basically do not need special control electronics because all necessary communication is done inside the motor. When the motor is operating, two static brushes are sliding on the revolving commutator and holding the voltage on the coils. The direction of revolving of the motor is determined by the polarity of the current. If the motor must revolve in only one direction, then the current may come through relay or some other simple connection. If the motor has to revolve in both directions, then an electronic circuit called H-bridge is used.
In the H-bridge are four transistors (or four groups) directing the current for driving the motor. The electrical scheme of the H-bridge is similar to the letter H and that is where it gets its name. The peculiarity of the H-bridge is the possibility to apply both directional polarities to the motor. Picture on the side shows the principal scheme of the H-bridge based on the example of the switches. If two diagonal switches are closed, the motor starts operating. The direction of the revolving of the motor depends on in which diagonal the switches are closed. In the real H-bridge the switches are replaced with transistors which are selected according to the current of the motor and voltage.
H-bridge can also change the direction of rotation than the rotation speed of the motor. There exist also integrated H-bridges, for conducting smaller currents. For higher currents special power MOSFET-s are used. The H-bridge with other electronics is called motor controller or driver.
While the speed of the DC motor is easy to control, there is no guarantee that the desired speed is reached after all. The actual velocity depends on many factors, primarily torque on the output shaft of the motor, current and other motor characteristics. The speed and the output torque of the ideal motor is linearly dependent, i.e. the larger is the output torque, the lower is the speed of the motor, and it consumes more current. This depends on the exact type of motor in case of real motor.
A direct current (DC) motor can be controlled with analog as well as digital signals.
Normally, the motor speed is dependent on the applied voltage at the terminals of the motor. If the motor feed a nominal voltage, it rotates a nominal speed. If the voltage given to the motor is reduced, the motor speed and torque are reduced as well. This type of speed control is also called as analog control. This can be implemented, for example, using a transistor or a rheostat.
DC motors are controlled by microcontrollers, and because microcontrollers are digital devices, it is also reasonable to control the motors digitally. This is achieved by using pulse width modulation (PWM), by switching transistors quickly on - off. The total motor power is something in between standing and full speed. The time of the entire PWM period when transistor is opened, called duty cycle, which is denoted by percent. 0% means that transistor is constantly closed and not conduct, 100% means that transistor is opened and conducts. The PWM frequency should be high enough to prevent vibration of the motor shaft. At low frequencies the motor produces a noise and is therefore used modulating frequency above 20 kHz mostly. However, transistors efficiency is suffering from very high frequencies.
Compared to the analog control a digital control has a number of advantages. The main advantage of microcontroller-controlled systems is that it requires only a single digital output and there is no need for complicated digital-to-analog converter. The digital controlling is also more efficient because less energy is converted into heat.
A simplified control scheme is shown in the next drawing. The control voltage Vc is coming to the microcontroller output pin and switch the transistor Q on-off at a frequency of approximately 20 kHz. When the transistor Q is switched on, then the total current I is going through the motor M. In this case, the transistor behaves as a closed switch and a voltage drop Vq is near 0, and the entire input voltage Vdd remains the engine.
The total power which is passing the transistor can be calculated by the formula:
P = I * V
P = I * Vq, and when Vq ~ 0, then P ~ 0 W
This means that the transistor spend almost no energy in the open state. Similar situation is also the case when the transistor is in the closed state. In this case, there is no current flow through the transistor or the motor. Now the power which is going through the transistor, is calculated as follows:
P = I * Vq, and when I = 0, then P = 0 W
In conclusion, we can say that if the transistor is a switch element on the scheme, then the system efficiency is high and the power used by transistors is low. Compared with a linear (analog) system, where the transistor consumes of the half-open state the same amount of power than the motor, it is a very big energy savings. In practice, there is no lossless system and in fact, the losses occur when the transistor switch one state to other. Therefore, higher losses are occurring when the transistors are switched at higher frequencies.
The HomLab uses a combined ships to drive DC motors, which includes 2 integrated H-bridges and circuit breaking diodes. The motor is controlled with three digital signals, one of them is operation enabling signal enable and the other two are determining the state of the transistors in the H-bridge. Never can occur that two vertical transistors are opened, because this would short-circuit the power source. This means that the driver is designed as foolproof and only option that can be chosen is which transistor (upper or bottom) of one side of the H-bridge (of “semi-bridge”) is opened. In other words the polarity is selected using two driving signals which is applied to the two ends of the coil of the motor.
The Combo Board of the HomeLab allows connecting up to four DC motors. Basically, for every motor there is a H-bridge which is controlled with two digital output pins of the microcontroller, because the enable pin is constantly high. If both controlling pins have same value, then the motor is stopped if different then it revolves in the corresponding direction. The state of the H-bridge is described in the following table:
Input A | Input B | Output A | Output B | Result |
---|---|---|---|---|
0 | 0 | - | - | The motor is stopped |
1 | 1 | + | + | The motor is stopped |
1 | 0 | + | - | The motor revolves in direction 1 |
0 | 1 | - | + | The motor revolves in direction 2 |
For each motor that is connected to the H-bridge is operated by two of the digital output of the microcontroller. The motor speed is is controlled by timers that generate a continuous PWM signals to the H-bridge, the direction of rotation of the motor is controlled to the second terminal. Motor speed is controlled a relative values from 0 to 255, where 0 means that the motor is standing and 255 is the maximum moving speed of the motor. The following code describes a function’s, which are described in the HomeLab II (ATmega2561) library to control DC motors.
// The setup of the pins driving pins static pin dcmotor_pins[4][2] = { { PIN(B, 7), PIN(B, 4) }, { PIN(D, 1), PIN(D, 0) }, { PIN(D, 7), PIN(D, 6) }, { PIN(D, 5), PIN(D, 4) } }; static int motorindex[4][2] = { { 0, 1 }, { 2, 3 }, { 4, 5 }, { 6, 7 } }; // Initializing a PWM to chosen motor void dcmotor_drive_pwm_init(unsigned char index, timer2_prescale prescaler) { unsigned char i, pwm; pin_setup_output(dcmotor_pins[index][0]); pin_setup_output(dcmotor_pins[index][1]); motor[index] = 1; pwm = PWMDEFAULT; // Starting all channels for(i=0 ; i<CHMAX ; i++) { // PWM state variable initialization compare[i] = pwm; compbuff[i] = pwm; } // Starting Timer 2 to normal mode timer2_init_normal(prescaler); // Allow Timer 2 interrupt timer2_overflow_interrupt_enable(true); // Enable global interrupts sei(); } // Generating a PWM for chosen motor void dcmotor_drive_pwm(unsigned char index, signed char direction, unsigned char speed) { if(direction == -1) { compbuff[motorindex[index][0]] = 0x00; compbuff[motorindex[index][1]] = speed; } if(direction == 1) { compbuff[motorindex[index][0]] = speed; compbuff[motorindex[index][1]] = 0x00; } }
The controlling pins of four motor-controllers are determined with the array dcmotor_pins in the library. Before controlling the motors, function dcmotor_drive_pwm_init with the number of the motor-controller (0 – 3) must be called out. It sets the pins as output. It should also set the timer prescaler, for HomeLab II timer2_prescale and for HomeLab III timer_prescale, which determines the frequency of the PWM signal. In case of HomeLab II, as the program does not have functions which are using timer, it is appropriate for the value TIMER2_NO_PRESCALE. When for example an ultrasound sensor are used, then should be chosen TIMER2_PRESCALE 8, otherwise the controller performance may not be sufficient and the sensor readings may be corrupted. This is not applying in the HomeLab III. Higher values of the prescaler are not recommended, because it makes the motor rotation intermittent, and generates vibration.
Function dcmotor_drive_pwm is for control motor speed. This function need three input values: motor number, direction (-1, 0, +1), where -1 is the rotation in one direction, +1 other direction and 0 for stop and thirdly, the speed range of 0-255. The speed value is not linked to a specific rotational speed, it is the relative value between minimal and maximal motor speed. Motor actual speed depends on the motor type, load and the supply voltage. Motor speed accuracy is 8-bits, which means that the minimum control accuracy is 1/255 of the maximum engine speed.
The following is an example program which controls first and second DC motor so that first motor rotates half of the speed and the second motor speed is controlled by a potentiometer.
// Robotic HomeLab DC motor driving example program #include <homelab/module/motors.h> #include <homelab/adc.h> // Main program int main(void) { // Variable of speed int speed; // Start of ADC adc_init(ADC_REF_AVCC, ADC_PRESCALE_8); // DC1 & DC2 motor initialization (without timer prescaler) // HomeLab II //dcmotor_drive_pwm_init(1, TIMER2_NO_PRESCALE); //dcmotor_drive_pwm_init(2, TIMER2_NO_PRESCALE); // HomeLab III dcmotor_drive_pwm_init(1, TIMER_NO_PRESCALE); dcmotor_drive_pwm_init(2, TIMER_NO_PRESCALE); // Endless loop while (true) { // Reading potentiometer value (average of 4) speed = adc_get_average_value(15, 4); // ADC value is 12-bit but DC motor input is 8-bit // conversion can be ether dividing the value with 8 or // make bit shifting to right 3 times (>>3) dcmotor_drive_pwm(1, 1, speed/8); dcmotor_drive_pwm(2, 1, 128); } }
Necessary knowledge: [HW] User Interface Module, [HW] Combo module, [AVR] Counters/Timers, [AVR] Analog-to-digital Converter, [LIB] Motors, [LIB] Analog to Digital Converter
RC (radio-controlled) servo-motors are very common actuator devices in robotics and model building. RC servo motors are consisting of small DC motor, reduction gear and control logic device. Usually the rotor of the servo motor moves to a certain position and tries to maintain that position. The position of the rotor depends of the control signal received by the servo motor. Depending on the type of the motor, the maximum revolving angle of the motor may differ. Servo motors that revolve constantly are rare. In this case, the control signal determines not the revolving angle but the speed of revolving. Servo motor “hack” is also quite common. This makes the position determining servo motor to constantly revolving servo. In this case the feedback potentiometer is replaced by two fixed resistors and the mechanical resistor that prevents full rotations is removed from the gear. A very important characteristic of servo motors is its power-weight ratio.
The controlling signal of servo motor is specific pulse with modulated signal (PWM), where width of the pulse determines the position of the rotor. The period of the signal is 20 ms (50 Hz) and the width of the high period is 1 ms – 2 ms. 1 ms marks one extreme position and 2 ms marks the second one. 1,5 ms marks the middle position of the servo motor’s rotor.
Traditional RC servo motor is also known as analogue-servo motor. It is because in the last decade so called digital servo motors were becoming common. The difference between those two is that in analogue servo motor the motor is controlled by the same 50 Hz PWM input signal. In digital servo motor the motor is controlled by a microcontroller with much higher frequency signal. The input signal is the same in the digital servo motor but higher modulation frequency of the motor enables much more precise and faster position determining.
On the board of module of motors of the HomeLab are two or four plugs for connecting RC servo motors. The PWM ends of the plugs are connected to the pins of the microcontroller, which alternative functions are outputs of comparing units of the timer. Timer is capable of producing PWM signal and due to that the control of motors is very simple in the program. Only difficulty is set-up of the timer.
The timer must be set up in PWM production mode, where the maximum value of the timer is determined with ICR register. With the maximum value changed in the program and in the pace divider of the timer, the precise PWM frequency for controlling the servo motor can be determined. With the comparison register of the timer, lengths of both high semi periods of PWM signal can be determined. The timers have special comparing units which are monitoring the value of the counter and in case it remains equal with the value of the comparison register they change the output value of comparing units. The following is the program code of the servo motor control library of the HomeLab. For the purpose of functionality, it uses parameters for timers which are determined with macro functions. For example, the period is found using F_CPU constant, which marks the clock rate of the microcontroller. When using macros, there is no need to calculate the parameters of timer for different clock rates and the compiler converts the operations with macros to constants anyway, so the program memory is not growing and does not demand more time. The following example of library is for HomeLab II (ATmega2561).
// The value of the timer (20 ms)for achieving the full period of PWM // F_CPU is the clock rate of the microcontroller which is divided with // 50 Hz and 8 #define PWM_PERIOD (F_CPU / 8 / 50) // Middle position of PWM servo (5 ms / 20 ms) // Middle position is 15/200 of full period #define PWM_MIDDLE_POS (PWM_PERIOD * 15 / 200) // Factor for converting the percents (-100% to 100%)to periods // +1 is added to ensure that semi periods would reach to the boundaries // of 1 ms and 2 ms or // a little over #define PWM_RATIO (PWM_PERIOD / 20 / 2 / 100 + 1) // Set-up of the pins static pin servo_pins[2] = { PIN(B, 5), PIN(B, 6) }; // Preparing the servo motor for working void servomotor_init(unsigned char index) { // The pin of PWM signal for output pin_setup_output(servo_pins[index]); // Setup of timer 1 // Prescaler = 8 // Fast PWM mode, where TOP = ICR // OUTA and OUTB to low in comparisson timer1_init_fast_pwm( TIMER1_PRESCALE_8, TIMER1_FAST_PWM_TOP_ICR, TIMER1_FAST_PWM_OUTPUT_CLEAR_ON_MATCH, TIMER1_FAST_PWM_OUTPUT_CLEAR_ON_MATCH, TIMER1_FAST_PWM_OUTPUT_DISABLE); // Determining the period by maximum value timer1_set_input_capture_value(PWM_PERIOD); } // Determining the position of the servo motor // The parameter of the position is from -100% to +100%. void servomotor_position(unsigned char index, signed short position) { switch (index) { case 0: timer1_set_compare_match_unitA_value( PWM_MIDDLE_POS + position * PWM_RATIO); break; case 1: timer1_set_compare_match_unitB_value( PWM_MIDDLE_POS + position * PWM_RATIO); break; } }
The example program uses described functions of the library of the HomeLab. In the beginning of the program the first servo motor’s PWM signal generator is started with the servomotor_init function. The value of the position of the servo motor is obtained from the channel of the analogue-digital converter, where a potentiometer on the board of sensors is connected. To get the range -100 % - +100 % necessary for controlling the servo motor, half of the maximum (512) is subtracted of the ADC value and the result is divided with 5. The result is +/- 102, but small inaccuracy does not count because servo motors also differ by the relation of the PWM signal and revolving angle. Final PWM‘s semi period’s width in applications has to be determined using test-and-error method. Also the remote controls of RC models have corresponding opportunities for precise setup (trim function). When the program is started the rotors position of the servomotor is changed according to the position of the potentiometer.
// Testing program of the motors module of the HomeLab kit #include <homelab/adc.h> #include <homelab/module/motors.h> // Main program int main(void) { short position; // Set-up of the ADC adc_init(ADC_REF_AVCC, ADC_PRESCALE_8); // Set-up of the motor servomotor_init(1); // Endless loop while (1) { // Reading the position of the potentiometer and // converting the range of // the servo motor // For HomeLab II ADC must be read for the corresponding channel, // and use the following formula: // position = ((short)adc_get_value(3) - (short)512) / (short)5; position = ((short)adc_get_value(15) / 10) - 102 ; // Determining the position of the servo motor servomotor_position(1, position); } }
Necessary knowledge:
[HW] Combo module,
[AVR] Digital Inputs/Outputs,
[LIB] Motors,
[LIB] Delay
Stepper-motors are widely used in applications which demand accuracy. Unlike DC motors, stepper motors do not have brushes nor commutator – they have several independent coils, which are commutated with exterior electronics (drivers). Rotating the rotor is done by commutating coils step by step, without feedback. This is one of the faults in stepper motors – in case of mechanical overloading, when the rotor is not rotating, the steps will be mixed up and movement becomes inaccurate. Two types of stepper motors are distinguished by coils: unipolar and bipolar stepper motors. By construction three additional segments are considered:
Variable reluctance stepper motors have toothed windings and toothed iron rotor. The largest pulling force is when the teeth of both sides are covering each other. In Permanent magnet stepper motor,just like the name hints, are permanent magnets which orientate according to the polarity of the windings. In hybrid synchronous steppers both technologies are used.
Depending on the model of stepper motor, performing one full rotation (360 degrees) of the rotor, demands hundredths of steps of commutations. For stable and smooth movement, appropriate control electronics are used which control the motor according to its parameters (inertia of the rotor, torque, resonance etc.). In addition to control electronics different commutating methods may be applied. Commutating one winding in a row is called Full Step Drive and if the drive is alternated between one and two windings it is called Half Stepping. Cosine micro stepping is also used, allowing specially accurate and smooth controlling.
Unipolar stepper-motor
Unipolar-stepper motor has 5 or 6 leads. According to the scheme of the motor only ¼ of the windings is activated. Vcc lines are usually connected to the positive power supply. During commutation the ends of windings 1a, 1b, 2a and 2b are connected through transistors only to the ground and that makes their control electronics fairly simple.
Bipolar stepper-motor
Bipolar stepper motor differs from unipolar stepper motor by having the polarity of the windings altered during the commutation. Half of the windings are activated together, this allows to gain higher efficiency than unipolar stepper motors. Bipolar stepper motors have four leads, each connected to a different half-bridge. During commutation half-bridges are applying either positive or negative voltage to the ends of the windings. Unipolar motors can be started using bipolar driver: just connect lines 1a, 1b, 2a and 2b of the windings (Vcc will be not connected).
The commutation necessary for controlling stepper-motors with windings at full step mode and half step mode is displayed in the table below. Since in drivers for uni-polar stepper motors only opening of the transistors takes place, the steps are marked by 0 and 1. Controlling of bipolar stepper motors may need more signals and therefore the steps are marked using the polarity of the driver outputs:
Unipolar | Bipolar | |||||||
---|---|---|---|---|---|---|---|---|
Step | 1A | 2A | 1B | 2B | 1A | 2A | 1B | 2B |
Full step | ||||||||
1 | 1 | 0 | 0 | 0 | + | - | - | - |
2 | 0 | 1 | 0 | 0 | - | + | - | - |
3 | 0 | 0 | 1 | 0 | - | - | + | - |
4 | 0 | 0 | 0 | 1 | - | - | - | + |
Half step | ||||||||
1 | 1 | 0 | 0 | 0 | + | - | - | - |
2 | 1 | 1 | 0 | 0 | + | + | - | - |
3 | 0 | 1 | 0 | 0 | - | + | - | - |
4 | 0 | 1 | 1 | 0 | - | + | + | - |
5 | 0 | 0 | 1 | 0 | - | - | + | - |
6 | 0 | 0 | 1 | 1 | - | - | + | + |
7 | 0 | 0 | 0 | 1 | - | - | - | + |
8 | 1 | 0 | 0 | 1 | + | - | - | + |
The Combo Module has a H-bridges to control bipolar stepper motors and the transistor matrix for unipolar stepper motor.
There are functions bipolar_init and unipolar_init in the library of the HomeLab which sets the pins as output and functions bipolar_halfstep and unipolar_halfstep executes revolving by determined half steps. The commutation is done by the table of half steps, but more complex bit operations are used. Unipolar stepper motor is connected to a separate connector Unipolar Stepper, bipolar stepper motor is connected to a DC motor connector, where one of the bipolar motor occupies driver pins of two DC motor. The following code section is HomeLab II (ATmega2561) library functions.
// Preparing for controlling the bipolar stepper motor void bipolar_init(void) { DDRB |= 0x0F; PORTB &= 0xF0; } // Moving the bipolar stepper motor by half steps void bipolar_halfstep(signed char dir, unsigned short num_steps, unsigned char speed) { unsigned short i; unsigned char pattern, state1 = 0, state2 = 1; // Insuring the direction +- 1 dir = ((dir < 0) ? -1 : +1); // Execution of half-steps. for (i = 0; i < num_steps; i++) { state1 += dir; state2 += dir; // Creating the pattern pattern = (1 << ( (state1 % 8) >> 1) ) | (1 << ( (state2 % 8) >> 1) ); // Setting the output. PORTB = (PORTB & 0xF0) | (pattern & 0x0F); // Taking a break to wait for executing the step sw_delay_ms(speed); } // Stopping the motor PORTB &= 0xF0; }
Usage of the functions is demonstrated by the example program which rotates the motor alternately to one direction and then to the other direction 200 half steps. The speed of rotating the motor is determined by the length of the brakes made between the steps. If the break is set to be too short, the motor can not accomplish the turn due to the inertia of the rotor and the shaft does not move.
// The test program for the stepper motor of the HomeLab #include <homelab/module/motors.h> // Main program int main(void) { // Set up of the motor unipolar_init(0); // Endless loop while (true) { // Turning the rotor 200 half steps to one direction // at speed of 30 ms/step. unipolar_halfstep(0,+1, 2000, 30); // Turning 200 half steps to the other direction // at speed 30 ms/step. unipolar_halfstep(0,-1, 2000, 30); } }