Environmental Monitoring with STM32 Nucleo L476RG and DHT22: A Practical Guide

by Müller | Nov 15, 2025 | Hardware & IoT | 0 comments

Building environmental monitoring projects is fundamental for many applications, from agriculture to smart homes. In this article, we explore building a prototype system for visualizing temperature and humidity using the STM32 Nucleo L476RG microcontroller, a DHT22 sensor, and an MCUFRIEND_kbv TFT display, programmed in C++ with the Arduino framework.

Project Goal

The proposed code continuously monitors ambient temperature and humidity, displaying the results in real time on a graphic display. Additionally, the system records the minimum and maximum values of the last 24 hours and presents a simple chart that helps visualize trends over time.

Required Hardware

  • STM32 Nucleo L476RG – prototyping platform with a good balance of performance and cost.
  • DHT22 – temperature and humidity sensor, known for accuracy and durability.
  • MCUFRIEND_kbv TFT display – compatible with various graphic controllers, used for visualization.

Pinout

20240428 204647 1024x551.jpg

DHT22 Sensor

  • VCC: connected to the 3.3V pin of the STM32 Nucleo L476RG board.
  • GND: connected to the GND pin of the STM32 board.
  • DHTPIN (PB7): data pin between the DHT22 and the STM32.

MCUFRIEND_kbv Display Connection

  • LCD_CS (A3): Chip Select, enables/disables the display on the bus.
  • LCD_CD (A2): Command/Data.
  • LCD_WR (A1): write pin.
  • LCD_RD (A0): read pin.
  • LCD_RESET (A4): display reset pin.

Project Setup in VSCode with PlatformIO

The platformio.ini file defines the environment and the required libraries:

[env:nucleo_l476rg]
platform = ststm32
board = nucleo_l476rg
framework = arduino
lib_deps = 
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit GFX Library@^1.11.9
    prenticedavid/MCUFRIEND_kbv@^3.1.0-Beta
    adafruit/Adafruit ST7735 and ST7789 Library@^1.10.3

Source Code Analysis

Storing the Last 24 Hours of Data

To monitor environmental conditions over a day, the code uses arrays to store temperature and humidity readings:

const int NUM_READINGS = 1440; // Total readings for 24 hours at one-minute intervals
float tempReadings[NUM_READINGS];      // Temperature readings
float humidityReadings[NUM_READINGS];  // Humidity readings
int readingIndex = 0;                  // Current index for new reading
int readingsCount = 0;                 // Effective number of recorded readings

Every minute a new reading is inserted, and the index is updated in a circular scheme, always keeping the last 24h of data.

Data Visualization with Progress Bars

To make it easy to visualize current readings against minimum and maximum values, the code uses dynamic progress bars:

void drawProgressBar(int x, int y, int width, int height,
                     int value, int max, uint16_t barColor) {
  int filledWidth = (int)((width * value) / max);
  tft.fillRect(x, y, filledWidth, height, barColor);         // Filled area
  tft.fillRect(x + filledWidth, y, width - filledWidth, height, BLACK); // Empty area
}

Color Mapping for Temperature and Humidity

The bar colors change according to the values read, allowing a quick visual read:

uint16_t getTemperatureColor(int temp) {
  if (temp >= 30) return RED;
  if (temp < 10) return BLUE;
  int red = map(temp, 10, 30, 0, 255);
  int blue = map(temp, 10, 30, 255, 0);
  return tft.color565(red, 0, blue);
}

uint16_t getHumidityColor(int humidity) {
  if (humidity <= 30) return RED;
  if (humidity > 60) return BLUE;
  int blue = map(humidity, 20, 90, 0, 255);
  int red = map(humidity, 20, 90, 255, 0);
  return tft.color565(red, 0, blue);
}

Using a Composite Indicator

The drawIndicador function groups common elements to display the value, minimum, maximum, and corresponding bar in a compact form:

void drawIndicador(int x, int y,
                   float value, float min, float max,
                   String indicador, String unidade,
                   uint16_t cor) {
  tft.fillRect(x, y, 320, 70, BLACK);
  tft.setTextSize(2);
  tft.setCursor(x, y + 5);
  tft.setTextColor(cor);
  tft.print(indicador + ": " + String(value) + " " + unidade);

  tft.setTextColor(WHITE);
  tft.setCursor(x, y + 30);
  tft.setTextSize(1);
  tft.print("Min: " + String(min) + " | Max: " + String(max));

  if (indicador == "Temperatura") {
    drawProgressBar(x, y + 45, 300, 20, value, 40, getTemperatureColor(value));
  } else if (indicador == "Umidade") {
    drawProgressBar(x, y + 45, 300, 20, value, 100, getHumidityColor(value));
  }
}

Graphical Visualization of the Last 24h

Graphical visualization is essential to understand temperature and humidity trends over the last 24 hours:

void drawTemperatureChart(int x, int y, int width, int height,
                          float vector[], int tamanho, int iterator,
                          uint16_t cor) {
  int y_ponto, x_ponto = 0;
  for (int i = 0; i < iterator; i++) {
    y_ponto = static_cast<int>(round((static_cast<float>(vector[i] - 15)
              / (40 - 15)) * height));
    x_ponto = static_cast<int>(round((static_cast<float>(i)
              / static_cast<float>(tamanho)) * static_cast<float>(width)));
    tft.drawPixel(x + x_ponto, y + height - y_ponto, cor);
  }
}

void drawHumidityChart(int x, int y, int width, int height,
                       float vector[], int tamanho, int iterator,
                       uint16_t cor) {
  int y_ponto, x_ponto = 0;
  for (int i = 0; i < iterator; i++) {
    y_ponto = static_cast<int>(round((static_cast<float>(vector[i])
              / 100.0) * height));
    x_ponto = static_cast<int>(round((static_cast<float>(i)
              / static_cast<float>(tamanho)) * static_cast<float>(width)));
    tft.drawPixel(x + x_ponto, y + height - y_ponto, cor);
  }
}

These functions map the reading vectors to pixel coordinates, allowing a compact chart within the display’s physical limits.

Complete Code

Below is the complete example code used in the project. It integrates DHT22 reading, TFT display rendering, and 24-hour history storage:

#include <Arduino.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>
#include <Adafruit_GFX.h>    // Base graphics library
#include <MCUFRIEND_kbv.h>

#define DHTPIN PB7
#define DHTTYPE DHT22

// Display pins
#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
#define LCD_RESET A4

// 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

#define RGB(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3))

#define GREY      RGB(127, 127, 127)
#define DARKGREY  RGB(64, 64, 64)
#define TURQUOISE RGB(0, 128, 128)
#define PINK      RGB(255, 128, 192)
#define OLIVE     RGB(128, 128, 0)
#define PURPLE    RGB(128, 0, 128)
#define AZURE     RGB(0, 128, 255)
#define ORANGE    RGB(255, 128, 64)

// DHT_Unified to get sensor metadata
DHT_Unified dht(DHTPIN, DHTTYPE);
MCUFRIEND_kbv tft;

uint32_t delayMS;

const int NUM_READINGS = 1440;
float tempReadings[NUM_READINGS];
float humidityReadings[NUM_READINGS];
int readingIndex = 0;
int readingsCount = 0;

float tempMin = 500;
float tempMax = -100;
float humidityMin = 100;
float humidityMax = 0;

// Function to get color based on temperature
uint16_t getTemperatureColor(int temp) {
  if (temp >= 30) return RED;
  if (temp < 10) return BLUE;
  int red = map(temp, 10, 30, 0, 255);
  int blue = map(temp, 10, 30, 255, 0);
  return tft.color565(red, 0, blue);
}

// Function to get color based on humidity
uint16_t getHumidityColor(int humidity) {
  if (humidity <= 30) return RED;
  if (humidity > 60) return BLUE;
  int blue = map(humidity, 20, 90, 0, 255);
  int red = map(humidity, 20, 90, 255, 0);
  return tft.color565(red, 0, blue);
}

// Progress bar
void drawProgressBar(int x, int y, int width, int height,
                     int value, int max, uint16_t barColor) {
  int filledWidth = (int)((width * value) / max);
  tft.fillRect(x, y, filledWidth, height, barColor);
  tft.fillRect(x + filledWidth, y, width - filledWidth, height, BLACK);
}

// Full indicator (value + min/max + bar)
void drawIndicador(int x, int y, float value, float min, float max,
                   String indicador, String unidade, uint16_t cor) {
  tft.fillRect(x, y, 320, 70, BLACK);
  tft.setTextSize(2);
  tft.setCursor(x, y + 5);
  tft.setTextColor(cor);
  tft.print(indicador + ": " + String(value) + " " + unidade);

  tft.setTextColor(WHITE);
  tft.setCursor(x, y + 30);
  tft.setTextSize(1);
  tft.print("Min: " + String(min) + " | Max: " + String(max));

  if (indicador == "Temperatura") {
    drawProgressBar(x, y + 45, 300, 20, value, 40, getTemperatureColor(value));
  } else if (indicador == "Umidade") {
    drawProgressBar(x, y + 45, 300, 20, value, 100, getHumidityColor(value));
  }
}

// Temperature chart
void drawTemperatureChart(int x, int y, int width, int height,
                          float vector[], int tamanho, int iterator,
                          uint16_t cor) {
  int y_ponto, x_ponto = 0;
  for (int i = 0; i < iterator; i++) {
    y_ponto = static_cast<int>(round((static_cast<float>(vector[i] - 15)
              / (40 - 15)) * height));
    x_ponto = static_cast<int>(round((static_cast<float>(i)
              / static_cast<float>(tamanho)) * static_cast<float>(width)));
    tft.drawPixel(x + x_ponto, y + height - y_ponto, cor);
  }
}

// Humidity chart
void drawHumidityChart(int x, int y, int width, int height,
                       float vector[], int tamanho, int iterator,
                       uint16_t cor) {
  int y_ponto, x_ponto = 0;
  for (int i = 0; i < iterator; i++) {
    y_ponto = static_cast<int>(round((static_cast<float>(vector[i])
              / 100) * height));
    x_ponto = static_cast<int>(round((static_cast<float>(i)
              / static_cast<float>(tamanho)) * static_cast<float>(width)));
    tft.drawPixel(x + x_ponto, y + height - y_ponto, cor);
  }
}

void setup() {
  tft.reset();
  uint16_t identifier = tft.readID();
  tft.begin(identifier);
  tft.setRotation(1);
  tft.fillScreen(BLACK);
  tft.setTextColor(WHITE);
  tft.setTextSize(2);

  Serial.begin(9600);
  while (!Serial);
  delay(1000);

  dht.begin();
  Serial.println(F("Starting DHT22"));
  sensor_t sensor;

  dht.temperature().getSensor(&sensor);
  Serial.println(F("------------------------------------"));
  Serial.println(F("Temperature Sensor"));
  Serial.print  (F("Sensor Type: ")); Serial.println(sensor.name);
  Serial.print  (F("Driver Ver:  ")); Serial.println(sensor.version);
  Serial.print  (F("Unique ID:   ")); Serial.println(sensor.sensor_id);
  Serial.print  (F("Max Value:   ")); Serial.print(sensor.max_value); Serial.println(F("°C"));
  Serial.print  (F("Min Value:   ")); Serial.print(sensor.min_value); Serial.println(F("°C"));
  Serial.print  (F("Resolution:  ")); Serial.print(sensor.resolution); Serial.println(F("°C"));
  Serial.println(F("------------------------------------"));

  dht.humidity().getSensor(&sensor);
  Serial.println(F("Humidity Sensor"));
  Serial.print  (F("Sensor Type: ")); Serial.println(sensor.name);
  Serial.print  (F("Driver Ver:  ")); Serial.println(sensor.version);
  Serial.print  (F("Unique ID:   ")); Serial.println(sensor.sensor_id);
  Serial.print  (F("Max Value:   ")); Serial.print(sensor.max_value); Serial.println(F("%"));
  Serial.print  (F("Min Value:   ")); Serial.print(sensor.min_value); Serial.println(F("%"));
  Serial.print  (F("Resolution:  ")); Serial.print(sensor.resolution); Serial.println(F("%"));
  Serial.println(F("------------------------------------"));
  Serial.print  (F("Min Delay:  ")); Serial.print(sensor.min_delay / 1000 / 1000); Serial.println(F("s"));
  Serial.println(F("------------------------------------"));
  Serial.print  (F("Display Height:  ")); Serial.println(tft.height());
  Serial.print  (F("Display Width:   ")); Serial.println(tft.width());

  delayMS = 60 * 1000;
}

void loop() {
  sensors_event_t event;

  // Temperature
  dht.temperature().getEvent(&event);
  float tempValue = isnan(event.temperature) ? 0 : event.temperature;

  // Humidity
  dht.humidity().getEvent(&event);
  float humidityValue = isnan(event.relative_humidity) ? 0 : event.relative_humidity;

  // Updates circular buffers
  tempReadings[readingIndex] = tempValue;
  humidityReadings[readingIndex] = humidityValue;

  readingIndex = (readingIndex + 1) % NUM_READINGS;
  readingsCount = min(readingsCount + 1, NUM_READINGS);

  // Recalculates min and max
  tempMin = tempMax = tempReadings[0];
  humidityMin = humidityMax = humidityReadings[0];
  for (int i = 0; i < readingsCount; i++) {
    tempMin = min(tempMin, tempReadings[i]);
    tempMax = max(tempMax, tempReadings[i]);
    humidityMin = min(humidityMin, humidityReadings[i]);
    humidityMax = max(humidityMax, humidityReadings[i]);
  }

  // Drawing on the display
  if (!isnan(tempValue)) {
    drawIndicador(10, 5, tempValue, tempMin, tempMax, "Temperatura", "C", BLUE);
    drawTemperatureChart(10, 160, 300, 60, tempReadings, NUM_READINGS, readingsCount, BLUE);
  }

  if (!isnan(humidityValue)) {
    drawIndicador(10, 80, humidityValue, humidityMin, humidityMax, "Umidade", "%", GREEN);
    drawHumidityChart(10, 160, 300, 60, humidityReadings, NUM_READINGS, readingsCount, GREEN);
  }

  delay(delayMS);
}

Conclusion

This project shows how to combine the STM32 Nucleo L476RG, the DHT22 sensor, and a TFT display to create a complete environmental monitoring system with 24-hour history and graphical visualization. The same approach can be extended to other sensors and communication methods, such as Wi-Fi, MQTT, and cloud dashboards.

Table of Contents