A construção de projetos de monitoramento ambiental é fundamental para diversas aplicações, desde a agricultura até residências inteligentes. Neste artigo, exploramos a construção de um protótipo de sistema de visualização de temperatura e umidade usando o microcontrolador STM32 Nucleo L476RG, um sensor DHT22 e um display TFT MCUFRIEND_kbv, programados em C++ com o framework Arduino.

Objetivo do Projeto

O código proposto permite monitorar continuamente a temperatura e a umidade do ambiente, exibindo os resultados em tempo real em um display gráfico. Além disso, o sistema registra os valores máximos e mínimos das últimas 24h e apresenta um gráfico simples que ajuda a visualizar as tendências ao longo do tempo.

Hardware Necessário

  • STM32 Nucleo L476RG – plataforma de prototipagem com bom equilíbrio entre desempenho e custo.
  • DHT22 – sensor de temperatura e umidade, conhecido por precisão e durabilidade.
  • Display TFT MCUFRIEND_kbv – compatível com diversos controladores gráficos, usado para visualização.

Pinagem

20240428 204647 1024x551.jpg

Sensor DHT22

  • VCC: conectado ao pino 3.3V da placa STM32 Nucleo L476RG.
  • GND: conectado ao pino GND da placa STM32.
  • DHTPIN (PB7): pino de dados entre o DHT22 e a STM32.

Conexão do Display MCUFRIEND_kbv

  • LCD_CS (A3): Chip Select, ativa/desativa o display no barramento.
  • LCD_CD (A2): Command/Data.
  • LCD_WR (A1): pino de escrita.
  • LCD_RD (A0): pino de leitura.
  • LCD_RESET (A4): pino de reset do display.

Configuração do Projeto no VSCode com PlatformIO

O arquivo platformio.ini define o ambiente e as bibliotecas necessárias:

[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

Análise do Código-Fonte

Armazenamento de Dados das Últimas 24 Horas

Para monitorar as condições ambientais ao longo de um dia, o código utiliza arrays para armazenar as leituras de temperatura e umidade:

const int NUM_READINGS = 1440; // Total de leituras para 24 horas com intervalos de um minuto
float tempReadings[NUM_READINGS];      // Leituras de temperatura
float humidityReadings[NUM_READINGS];  // Leituras de umidade
int readingIndex = 0;                  // Índice atual para nova leitura
int readingsCount = 0;                 // Qtde efetiva de leituras registradas

Cada minuto uma nova leitura é inserida, e o índice é atualizado em esquema circular, mantendo sempre as últimas 24h de dados.

Visualização de Dados com Barras de Progresso

Para facilitar a visualização das leituras atuais em relação aos valores mínimos e máximos, o código utiliza barras de progresso dinâmicas:

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);         // Área preenchida
  tft.fillRect(x + filledWidth, y, width - filledWidth, height, BLACK); // Área vazia
}

Mapeamento de Cores para Temperatura e Umidade

As cores das barras mudam de acordo com os valores lidos, permitindo uma leitura visual rápida:

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);
}

Uso de um Indicador Composto

A função drawIndicador agrupa elementos comuns para exibir o valor, mínimo, máximo e barra correspondente de forma compacta:

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));
  }
}

Visualização Gráfica das Últimas 24h

A visualização gráfica é fundamental para entender tendências de temperatura e umidade nas últimas 24 horas:

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);
  }
}

Essas funções mapeiam os vetores de leituras para coordenadas de pixels, permitindo um gráfico compacto dentro dos limites físicos do display.

Código Completo

Abaixo está o código completo de exemplo usado no projeto. Ele integra leitura do DHT22, renderização no display TFT e armazenamento de um histórico de 24 horas:

#include <Arduino.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>
#include <Adafruit_GFX.h>    // Biblioteca gráfica base
#include <MCUFRIEND_kbv.h>

#define DHTPIN PB7
#define DHTTYPE DHT22

// Pinos do display
#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
#define LCD_RESET A4

// Definição de cores
#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 para obter metadados do sensor
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;

// Função para obter a cor baseada na temperatura
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);
}

// Função para obter a cor baseada na umidade
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);
}

// Barra de progresso
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);
}

// Indicador completo (valor + min/max + barra)
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));
  }
}

// Gráfico de temperatura
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);
  }
}

// Gráfico de umidade
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("Iniciando 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;

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

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

  // Atualiza buffers circulares
  tempReadings[readingIndex] = tempValue;
  humidityReadings[readingIndex] = humidityValue;

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

  // Recalcula min e 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]);
  }

  // Desenho no 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);
}

Conclusão

Este projeto mostra como combinar o STM32 Nucleo L476RG, o sensor DHT22 e um display TFT para criar um sistema completo de monitoramento ambiental com histórico de 24h e visualização gráfica. A mesma abordagem pode ser estendida para outros sensores e formas de comunicação, como Wi-Fi, MQTT e dashboards em nuvem.

Monitoramento Ambiental com STM32 Nucleo L476RG e DHT22: Um Guia Prático