Ultrassomico e o projeto da metade do trimestre — Arduino e IoT — semana 11 do 1o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 11 · Ultrassomico e o projeto da metade do trimestre — Material de Apoio Arduino

Semana 11 de 13· 1o trimestre · 01/02 a 30/04

Ultrassomico e o projeto da metade do trimestre

HC-SR04 mede distancia com som, e fecha a parte de sensores com um projeto curto.

Aula 1 — HC-SR04: medir distancia com som

Objetivos

  • Disparar um pulso no TRIG do HC-SR04 e medir o tempo do eco com pulseIn.
  • Converter a duracao do eco em distancia em centimetros, dividindo por dois, e explicar por que.
  • Recusar a leitura quando o pulseIn devolve zero, em vez de calcular um número falso.
  • Declarar o alcance minimo e maximo do sensor e dizer por que eles existem.
  • Medir três vezes antes de aceitar um valor, e explicar o que a media faz com o ruido.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 sensor HC-SR04 e 1 módulo de 4 pinos por aluno
  • 1 protoboard de 830 pontos e 4 jumpers por aluno
  • 1 módulo MB102 por dupla
  • 1 objeto rigido para medir, como uma caixa de papel ou um copo

Conceitos

O sensor não mede distancia

O HC-SR04 tem um transdutor que emite um pulso ultrasonico e mede quanto tempo o eco leva para voltar. Isso e tudo. Não existe sensor de distancia dentro dele.

O código faz a conversao. E por isso que um fator errado no código produz um erro constante e proporcional: todos os números ficam errados pelo mesmo fator, e o defeito se disfarca de "sensor impreciso".

pino em HIGH  ___|      |_______
pino em LOW   _____________|     |_________________________
              10 us       |  20 ms - pulso  |
                           |_____| 500 a 2500 us

O pulso de TRIG e de 10 microssegundos. O pulso de ECHO e o que dura a medida, e e entre 500 e 2500 microssegundos.

A conta

distancia_cm = (duracao_us * 0,034) / 2

O fator 0,034 vem da velocidade do som. Um centimetro por microsegundo e 343 metros por segundo, que e o valor da tabela em condições normais. Ele sobe e desce com a temperatura, e o professor menciona isso sem developsar, porque e um detalhe que o aluno encontra mais tarde.

A divisão por 2 existe porque o som fez o caminho de ida e o de volta. Isso e o erro mais comum da aula, e ele e silencioso: o programa funciona, o sensor funciona, e o número sai exatamente o dobro.

pulseIn e o zero que não e zero

pulseIn fica contando microssegundos até encontrar o padrao pedido, ou até estourar o tempo limite. Quando ela devolve 0, significa que não houve eco:

SituacaoO que aconteceu
Nada a frente em 5 mo pulso saiu e não voltou
Sensor desconectadonunca houve pulso de saída
TRIG e ECHO trocadoso sensor espera o pulso que não vem
Obstaculo longe demaiso eco volta depois do tempo limite

Nos quatro casos, pulseIn devolve 0. O código tem que recusar, e não tratar 0 como "0 centimetros". Multiplicar zero por 0,034 da zero, e o programa anunciaria "distancia: 0,0 cm" com a placa funcionando perfeitamente.

Da mesma forma, pulseIn sem tempo limite e uma bomba: se o sensor falhar, o programa trava ali e nem o Serial responde. O terceiro argumento existe por isso.

O alcance, e os limites

LimiteValorPor que existe
Minimocerca de 2 cmperto demais, o sensor mede a propria capsula
Maximocerca de 4 mo eco chega fraco demais e o sensor erra
Zona cegaconicao sensor não ve o que esta fora do eixo

O alcance maximo do HC-SR04 e de 4 m, e o minimo e de 2 cm. Entre 25 cm e 2 m a precisao e melhor. O professor escreve a faixa util no caderno do aluno, porque e o que vai aparecer no projeto do trimestre.

A zona cega explica um resultado que o aluno sempre estranha: a medida muda quando a mao muda de lugar, mesmo com a distancia igual. O sensor responde ao que esta mais perto do eixo. Um objeto largo, como a mao, tem partes em distancias diferentes — e o sensor mede a mais proxima.

Medir três vezes

Uma medida so oscila: muda de centimetro entre uma leitura e a seguinte, mesmo com o objeto parado. Isso vem do ruido eletrico e da temperatura.

A media de três leituras suaviza. O objetivo não e precisao — e o mesmo objetivo do dia 8 com o LDR, e o professor faz a ligacao de propósito: suavizar o sinal para poder decidir sobre ele.

O custo de medir três vezes e que o intervalo total triplica. O HC-SR04 precisa de mais de 60 ms entre disparos, para não misturar o eco anterior com o pulso novo. Três medidas com 60 ms de intervalo ocupam quase 200 ms.

Atividade

Montagem:

  • VCC do HC-SR04 na trilha + da protoboard.
  • GND do HC-SR04 na trilha -.
  • TRIG do HC-SR04 no GPIO5.
  • ECHO do HC-SR04 no GPIO18.

Nota: 10 pontos. Critério de fim: o serial mostra distancia em centimetros para um objeto a cerca de 30 cm, e mostra "sem eco" quando o objeto e removido.

  1. Monte o circuito do enunciado e grave o sketch da resolucao.
  2. Coloque um objeto a uns 30 cm, meca com a regua e anote o número do serial. Repita a medida cinco vezes. Qual e a maior diferença entre as suas leituras?
  3. Troque / 2.0 por nada, isto e, apague a divisão por 2, e grave. Meça de novo a 30 cm. Em uma frase: o que o número faz, e o que isso prova sobre a causa do erro.
  4. Com o objeto a 3 metros, meca e anote. Depois tire o objeto e espere três leituras. O que o serial mostra quando não ha eco? Em uma frase: por que isso não pode ser tratado como zero centimetros.
  5. Meça com a mao aberta a uns 10 cm, e depois com os dois dedos em forma de V. As medidas batem? Explique em uma frase, usando o termo de zona cega.
  6. Troque TEMPO_MAXIMO_US de 30000 para 2000 e meca a 1 metro. O que aconteceu com a leitura? Em uma frase, o que esse número controla: a distancia ou o tempo que o código espera?

Resolucao

#include <Arduino.h>

const int PINO_TRIG = 5;
const int PINO_ECHO = 18;
const int DURACAO_TRIGGER = 10;
const int TEMPO_MAXIMO_US = 30000;
const float VELOCIDADE_CM_US = 0.034;
const float DISTANCIA_MINIMA = 2.0;
const float DISTANCIA_MAXIMA = 400.0;
const int MEDIDAS_POR_RODADA = 5;

unsigned long duracaoEco = 0;
float distancia = 0;

void setup() {
  pinMode(PINO_TRIG, OUTPUT);
  pinMode(PINO_ECHO, INPUT);

  Serial.begin(115200);
  Serial.println();
  Serial.println("HC-SR04 — distancia em centimetros");
  Serial.println("us do eco | cm | situacao");
}

void loop() {
  digitalWrite(PINO_TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(PINO_TRIG, HIGH);
  delayMicroseconds(DURACAO_TRIGGER);
  digitalWrite(PINO_TRIG, LOW);

  duracaoEco = pulseIn(PINO_ECHO, HIGH, TEMPO_MAXIMO_US);

  if (duracaoEco == 0) {
    distancia = -1.0;
  } else {
    distancia = (duracaoEco * VELOCIDADE_CM_US) / 2.0;
  }

  Serial.print(duracaoEco);
  Serial.print("   | ");
  if (distancia < 0) {
    Serial.print("---  | sem eco");
  } else {
    Serial.print(distancia, 1);
    Serial.print(" | ");
    if (distancia < DISTANCIA_MINIMA) {
      Serial.print("abaixo do minimo");
    } else if (distancia > DISTANCIA_MAXIMA) {
      Serial.print("acima do maximo");
    } else {
      Serial.print("ok");
    }
  }
  Serial.println();

  delay(120);
}

Por que assim e não de outro jeito. O if (duracaoEco == 0) vem antes da conta, e não depois. E o unico jeito de a recusa ser correta: um zero dividido por dois, multiplicado por 0,034 e arredondado para uma casa, daria "0,0 cm" — um número plausivel que ninguem questionaria. Guardando o zero e testando antes, o programa distingue "o sensor mediu zero" de "o sensor não mediu", e são coisas que precisam de tratamento diferente.

O distancia = -1.0 e o "sem eco" codificado num número. Um valor negativo não pode ser uma distancia, então ele serve de marca. A alternativa seria uma variável booleana a parte; o professor prefere o número porque um unico campo carrega o valor e o aviso, e e assim que o dia 11 faz com quatro sensores ao mesmo tempo.

O TEMPO_MAXIMO_US de 30000 microssegundos e o alcance maximo do sensor escrito em tempo. 30 ms de ida e volta, a 343 m/s, dao 5,1 metros — acima dos 4 metros do HC-SR04. O timeout e folgado de propósito: se fosse justo em 4 metros, o sensor marcaria sem eco em uma medida valida. O timeout define o alcance, não a precisao. O aluno que reduz o timeout para 2000 esta reduzindo o alcance para 34 cm, e não aumentando a precisao.

O delay(120) no fim tem duas razões que o professor explica: respeitar os 60 ms minimos do sensor, e dar tempo para o serial entregar a linha antes da proxima medicao. O MEDIDAS_POR_RODADA declarado no topo e o que prepara o item do desafio extra — ele passa a ser usado no dia 11.

Os limites DISTANCIA_MINIMA e DISTANCIA_MAXIMA não servem para recusar a leitura: servem para rotular a leitura. O código imprime o número de qualquer jeito. Isso e uma escolha: esconder o número quando ele esta fora da faixa impede o aluno de ver o grafico completo e descobrir que o sensor erra na borda.

Criterios de correcao

CritérioPontos
Circuito montado, com TRIG no GPIO5 e ECHO no GPIO182 pontos
A divisão por 2 presente, e a razão do caminho de ida e volta explicada3 pontos
pulseIn com tempo limite, e o zero tratado como "sem eco" e não como distancia3 pontos
Medida a 30 cm confere com a regua dentro da tolerancia da aula1 ponto
Item 6 respondido: o timeout controla o alcance, não a precisao1 ponto

Erros comuns

ErroComo apareceCorrecao
Esquecer de dividir por 2"Todos os números são o dobro""O som foi e voltou. Divida a distancia por 2."
pulseIn sem tempo limite"A placa travou e o serial parou""Passe um terceiro argumento. Sem ele, se o sensor falhar, o código espera para sempre."
Tratar o zero como distancia"O monitor mostra 0,0 cm""Zero significa que não houve eco. Guarde o zero e mostre "sem eco"."
TRIG e ECHO trocados"Nunca aparece nada""TRIG no 5 e o que dispara. ECHO no 18 e o que recebe."
Valor de MEDIDAS sem espaco entre disparos"As medidas cruzam: uma interfere na outra""O sensor precisa de mais de 60 ms. Respeite o intervalo entre disparos."
TEMPO_MAXIMO_US curto demais"Perto funciona e longe marca sem eco""O timeout define o alcance. 30000 us cobrem os 4 metros do sensor."
Objeto em angulo em vez de de frente"A medida muda quando mexo o objeto""A zona cega e conica. Meça de frente, com o objeto plano."

Desafio extra

Escreva a parte do código que tira MEDIDAS_POR_RODADA medidas e devolve a media, descartando qualquer leitura que veio com "sem eco". Depois escreva uma frase sobre o risco: e melhor perder o valor de uma medida e fazer a media do resto, ou devolver a media de todas incluindo as que falharam? Defenda a sua escolha.

>

A resolucao, compilada

// Aula 1 do dia 11: medir distancia com som.
//
// O HC-SR04 nao entrega distancia. Ele entrega quanto tempo o eco levou para
// voltar. O codigo transforma tempo em distancia, e essa transformacao e toda
// a aula: se o aluno errar o fator, todos os numeros ficam errados por um
// fator constante, e o erro passa como "o sensor esta imprecise".

#include <Arduino.h>

// Pino que dispara o pulso sonoro.
const int PINO_TRIG = 5;

// Pino que recebe o eco. Precisa ser um pino que aceite interrupt, e por isso
// nao pode ser um dos de saida so.
const int PINO_ECHO = 18;

// Duracao do pulso de disparo, em microssegundos. O manual pede 10.
const int DURACAO_TRIGGER = 10;

// Tempo maximo que o codigo espera pelo eco, em microssegundos. Passando
// deste tempo, pulseIn devolve 0 e o codigo sabe que o sensor esta longe,
// apontado para nada ou desconectado.
const int TEMPO_MAXIMO_US = 30000;

// Velocidade do som no ar, em centimetros por microssegundo. Um centimetro
// por microsegundo e 343 metros por segundo, que e o valor da tabela.
const float VELOCIDADE_CM_US = 0.034;

// Menor distancia confiavel, em centimetros. Perto disso o sensor mede a
// propria capsula do transdutor, nao o objeto.
const float DISTANCIA_MINIMA = 2.0;

// Maior distancia confiavel. Coincide com o timeout de 30 ms acima.
const float DISTANCIA_MAXIMA = 400.0;

// Quantas amostras o programa tira antes de decidir. Uma medida so oscila
// demais para servir de criterio de fim.
const int MEDIDAS_POR_RODADA = 5;

unsigned long duracaoEco = 0;
float distancia = 0;

void setup() {
  pinMode(PINO_TRIG, OUTPUT);
  pinMode(PINO_ECHO, INPUT);

  Serial.begin(115200);
  Serial.println();
  Serial.println("HC-SR04 — distancia em centimetros");
  Serial.println("us do eco | cm | situacao");
}

void loop() {
  // Dispara o pulso. A sequencia e: trigger em LOW para o sensor zerar o
  // receptor, 2 microsegundos, pulso de 10 em HIGH, e de volta para LOW.
  digitalWrite(PINO_TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(PINO_TRIG, HIGH);
  delayMicroseconds(DURACAO_TRIGGER);
  digitalWrite(PINO_TRIG, LOW);

  // pulseIn fica parado contando microssegundos ate o pino ir para HIGH e
  // depois ate voltar para LOW, ou ate estourar o tempo maximo.
  duracaoEco = pulseIn(PINO_ECHO, HIGH, TEMPO_MAXIMO_US);

  if (duracaoEco == 0) {
    distancia = -1.0;
  } else {
    // O som foi e voltou: a distancia e metade do caminho, e a velocidade e
    // 0,034 cm por microsegundo. Sem dividir por 2, todo numero sai dobrado.
    distancia = (duracaoEco * VELOCIDADE_CM_US) / 2.0;
  }

  Serial.print(duracaoEco);
  Serial.print("   | ");
  if (distancia < 0) {
    Serial.print("---  | sem eco");
  } else {
    Serial.print(distancia, 1);
    Serial.print(" | ");
    if (distancia < DISTANCIA_MINIMA) {
      Serial.print("abaixo do minimo");
    } else if (distancia > DISTANCIA_MAXIMA) {
      Serial.print("acima do maximo");
    } else {
      Serial.print("ok");
    }
  }
  Serial.println();

  // Tempo entre disparos. O manual do HC-SR04 pede mais de 60 ms para nao
  // misturar o eco anterior com o pulso novo. O intervalo e maior que isso de
  // proposito: as 5 medidas da rodada precisam de espaco entre elas.
  delay(120);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 1 dia11 aula1.

Aula 2 — Projeto: central de monitoramento com sensor, botao e display

Objetivos

  • Juntar em um unico sketch o LDR, o HC-SR04, um botao e o display OLED, cada um com seu ritmo.
  • Guardar as leituras em um unico bloco de estado, para que tela e serial nunca divirjam.
  • Distribuir quatro rotinas com três relogios diferentes dentro de um loop sem delay longo.
  • Declarar a struct que descreve o estado e explicar por que ela existe.
  • Dividir um programa grande em funções, com os protótipos declarados antes do uso.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 display OLED I2C de 0,96 polegada (128x64, controlador SSD1306) por aluno
  • 1 LDR e 1 resistor de 10 k ohm por aluno
  • 1 sensor HC-SR04 e 1 módulo de 4 pinos por aluno
  • 1 push button por aluno
  • 1 protoboard de 830 pontos e 8 jumpers por aluno
  • 1 módulo MB102 por dupla

Conceitos

O problema: três relogios num so loop

Até aqui cada aula tinha um sensor e um intervalo. O projeto junta quatro, e cada peca tem seu ritmo proprio:

BlocoComo leIntervaloPor que esse
BotaodigitalReada cada voltaa mao e mais rapida que qualquer intervalo
LDRanalogRead300 msleitura analogica e cara em relacao a digital
HC-SR04pulseIn200 mso sensor exige mais de 60 ms entre disparos
Display1024 bytes no I2C500 mso olho não distingue mais que isso

O jeito obvio de escrever isso e uma parede de delay, e esse e o erro que trava o projeto:

delay(200);              // espera a luz
delay(60);               // espera a distancia
delay(500);              // espera a tela

Nesse código, o botao so e lido a cada 760 ms. O aluno aperta, e o programa não ve.

O padrao: perguntar "já deu tempo?"

Cada bloco ganha o seu relogio, e o loop pergunta para cada um:

unsigned long agora = millis();

if (agora - ultimaLuz >= INTERVALO_LUZ_MS) {
  ultimaLuz = agora;
  medirLuz();
}

O loop inteiro roda sem parar, e cada bloco age so quando cabe no seu tempo. O botao e lido fora dos três if, porque não tem intervalo proprio: ele e lido sempre, e quem filtra o que importa e o olho do aluno.

O delay(20) no fim e o que sobra: ele impede que o processador gire em vao e mantem o programa responsivo. Não e um intervalo de negocio; e uma folga.

O estado: uma struct, um instante

Quatro informações que descrevem o mesmo instante não deveriam ficar em quatro variables soltas. O código as junta:

struct Estado {
  int luz;
  float distanciaCm;
  bool semEco;
  int apertos;
  int atualizacoes;
};

Estado estado = {0, -1.0, true, 0, 0};

O ganho não e organizacao estetica. E consistencia: quando a tela desenha, ela desenha este bloco inteiro; quando o serial imprime, imprime este bloco inteiro. Os dois instrumentos mostram o mesmo instante por construção, e não por sorte.

estado e global — declarado fora das funções — porque e o unico lugar onde os quatro blocos se encontram. Uma variável local de cada função seria quatro copias, e a tela mostraria um número que o serial nunca viu.

O -1.0 no valor inicial repete o -1.0 do dia 11 aula 1: sem eco e codificado como um valor que não pode ser distancia.

Prototipos: a ordem das linhas

O desenharTela precisa do valor lido por medirLuz. Se desenharTela estiver escrito antes de medirLuz, o compilador reclama: ele so conhece o que já viu.

void medirLuz();        // prototipo: "esta funcao existe"
void desenharTela();

void medirLuz() { estado.luz = analogRead(PINO_LDR); }
void desenharTela() { /* ... */ }

O prototipo e uma promessa escrita antes da hora. A ordem em que as funções são escritas não importa mais, e e exatamente isso que permite reorganizar o programa sem quebrar.

Quatro linhas de tela

A tela tem 64 pixels de altura e cada linha de texto ocupa 16. Cabem quatro linhas. O programa usa exatamente as quatro:

LinhaCampoFonte
0titulo e versãoconstante
16luzLDR
32distanciaHC-SR04, ou "sem eco"
48contadoresbotao e telas

Quando o projeto final, no dia 13, precisar de um quinto campo, a resposta não e diminuir a fonte: e escolher o que sai da tela. E uma decisao de projeto, e o professor trata como tal.

Atividade

Montagem:

  • Display: VCC na trilha +, GND na trilha -, SDA no GPIO21, SCL no GPIO22.
  • LDR: uma perna na trilha +, outra no GPIO34, com resistor de 10 k do GPIO34 para a trilha -.
  • HC-SR04: VCC na trilha +, GND na trilha -, TRIG no GPIO5, ECHO no GPIO18.
  • Botao: uma perna no GPIO27, a outra na trilha -.

Nota: 10 pontos. Critério de fim: a tela mostra os quatro campos ao mesmo tempo, o serial imprime o mesmo instante, e apertar o botao aumenta o contador mesmo em toques rapidos.

  1. Monte o circuito do enunciado e grave o sketch da resolucao. Confira os quatro campos na tela.
  2. Em 10 segundos, conte as linhas do serial. Quantas atualizacoes de tela foram? Agora conte quantas vezes a coluna da luz mudou de valor. Explique em uma frase por que o segundo número e maior.
  3. Troque INTERVALO_LUZ_MS de 300 para 1000. Repita a contagem da atividade 2. Em uma frase, o que isso demonstra sobre a leitura do sensor e a atualização da tela.
  4. Com o objeto a 1 metro, toque o botao cinco vezes rapido, sem olhar a tela. So depois olhe. O contador de apertos bateu? Em uma frase, qual dos dois instrumentos teria detectado os cinco toques se a tela estivesse desligada.
  5. Adicione um campo novo na tela: a umidade, calculada por uma função que você vai escrever para o DHT11 do dia 8. Como são quatro linhas e a tela não aceita uma quinta, descreva em uma frase o que você tirou da tela para dar lugar a ela.
  6. Desconecte o display e repita a atividade 2. O serial continua imprimindo as quatro informações? Em uma frase: o que isso mostra sobre o papel de uma saída no programa.

Resolucao

#include <Arduino.h>
#include <Wire.h>
#include <string.h>

const int PINO_SDA = 21;
const int PINO_SCL = 22;
const int ENDERECO_I2C = 0x3C;
const int LARGURA = 128;
const int ALTURA = 64;
const int TAMANHO_BUFFER = (LARGURA * ALTURA) / 8;
const int COMANDO = 0x00;
const int DADO = 0x40;

uint8_t buffer[TAMANHO_BUFFER];

void enviarComandos(const uint8_t *comandos, int quantidade) {
  Wire.beginTransmission(ENDERECO_I2C);
  Wire.write(COMANDO);
  for (int i = 0; i < quantidade; i++) {
    Wire.write(comandos[i]);
  }
  Wire.endTransmission();
}

void atualizarDisplay() {
  for (int pagina = 0; pagina < 8; pagina++) {
    const uint8_t posicao[3] = {
      (uint8_t)(0xB0 + pagina), 0x00, 0x10
    };
    enviarComandos(posicao, 3);
    Wire.beginTransmission(ENDERECO_I2C);
    Wire.write(DADO);
    for (int coluna = 0; coluna < LARGURA; coluna++) {
      Wire.write(buffer[(pagina * LARGURA) + coluna]);
    }
    Wire.endTransmission();
  }
}

void limparTela() {
  memset(buffer, 0, TAMANHO_BUFFER);
}

void escreverTexto(const char *texto, int coluna, int linha) {
  int posicao = (linha / 8) * LARGURA + coluna;
  while (*texto != 0 && posicao < TAMANHO_BUFFER) {
    buffer[posicao] = (uint8_t)(*texto);
    texto++;
    posicao++;
  }
}

void iniciarDisplay() {
  Wire.begin(PINO_SDA, PINO_SCL);
  delay(100);
  const uint8_t inicializacao[] = {
    0xAE, 0x20, 0x00, 0xB0, 0xC8, 0x00, 0x10, 0x40,
    0x81, 0x7F, 0xA1, 0xA6, 0xA8, 0x3F, 0xA4, 0xD3,
    0x00, 0xD5, 0x80, 0xD9, 0xF1, 0xDA, 0x12, 0xDB,
    0x40, 0x8D, 0x14, 0xAF,
  };
  enviarComandos(inicializacao, sizeof(inicializacao));
  delay(100);
  limparTela();
  atualizarDisplay();
}

const int PINO_LDR = 34;
const int PINO_TRIG = 5;
const int PINO_ECHO = 18;
const int PINO_BOTAO = 27;

const int DURACAO_TRIGGER = 10;
const int TEMPO_MAXIMO_US = 30000;
const float VELOCIDADE_CM_US = 0.034;

const int LIMIAR_ESCURO = 1800;
const int INTERVALO_LUZ_MS = 300;
const int INTERVALO_DISTANCIA_MS = 200;
const int INTERVALO_TELA_MS = 500;

unsigned long ultimaLuz = 0;
unsigned long ultimaDistancia = 0;
unsigned long ultimaTela = 0;

struct Estado {
  int luz;
  float distanciaCm;
  bool semEco;
  int apertos;
  int atualizacoes;
};

Estado estado = {0, -1.0, true, 0, 0};

void medirLuz();
void medirDistancia();
void desenharTela();
void mostrarCabecalho();

void medirLuz() {
  estado.luz = analogRead(PINO_LDR);
}

void medirDistancia() {
  digitalWrite(PINO_TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(PINO_TRIG, HIGH);
  delayMicroseconds(DURACAO_TRIGGER);
  digitalWrite(PINO_TRIG, LOW);

  unsigned long eco = pulseIn(PINO_ECHO, HIGH, TEMPO_MAXIMO_US);
  if (eco == 0) {
    estado.semEco = true;
    estado.distanciaCm = -1.0;
  } else {
    estado.semEco = false;
    estado.distanciaCm = (eco * VELOCIDADE_CM_US) / 2.0;
  }
}

void mostrarCabecalho() {
  Serial.println();
  Serial.println("CENTRAL DE MONITORAMENTO — dia 11, aula 2");
  Serial.println("luz | distancia | botao | telas");
  Serial.println();
}

void desenharTela() {
  char linha[24];

  snprintf(linha, sizeof(linha), "CENTRAL v1");
  escreverTexto(linha, 0, 0);

  snprintf(linha, sizeof(linha), "LUZ %4d", estado.luz);
  escreverTexto(linha, 0, 16);

  if (estado.semEco) {
    snprintf(linha, sizeof(linha), "DIST  sem eco");
  } else {
    snprintf(linha, sizeof(linha), "DIST %4d cm", (int)estado.distanciaCm);
  }
  escreverTexto(linha, 0, 32);

  snprintf(linha, sizeof(linha), "BT %d TELA %d",
           estado.apertos, estado.atualizacoes);
  escreverTexto(linha, 0, 48);

  atualizarDisplay();
}

void setup() {
  pinMode(PINO_LDR, INPUT);
  pinMode(PINO_TRIG, OUTPUT);
  pinMode(PINO_ECHO, INPUT);
  pinMode(PINO_BOTAO, INPUT_PULLUP);

  analogReadResolution(12);
  analogSetPinAttenuation(PINO_LDR, ADC_11db);

  iniciarDisplay();

  Serial.begin(115200);
  mostrarCabecalho();
}

void loop() {
  if (digitalRead(PINO_BOTAO) == LOW) {
    estado.apertos++;
    Serial.print(">> botao apertado: ");
    Serial.println(estado.apertos);
  }

  unsigned long agora = millis();

  if (agora - ultimaLuz >= INTERVALO_LUZ_MS) {
    ultimaLuz = agora;
    medirLuz();
  }

  if (agora - ultimaDistancia >= INTERVALO_DISTANCIA_MS) {
    ultimaDistancia = agora;
    medirDistancia();
  }

  if (agora - ultimaTela >= INTERVALO_TELA_MS) {
    ultimaTela = agora;
    estado.atualizacoes++;

    limparTela();
    desenharTela();

    Serial.print(estado.luz);
    Serial.print("   | ");
    if (estado.semEco) {
      Serial.print("sem eco ");
    } else {
      Serial.print(estado.distanciaCm, 1);
      Serial.print(" cm ");
    }
    Serial.print("| ");
    Serial.print(estado.apertos);
    Serial.print("   |   ");
    Serial.println(estado.atualizacoes);
  }

  delay(20);
}

Por que assim e não de outro jeito. O estado e global, e essa e a decisao de projeto mais importante do sketch. Cada bloco escreve em um campo e cada bloco le os campos — o display, o serial e a proxima medicao. Se estado fosse local, cada função teria uma copia e a tela mostraria um instante que o serial nunca viu. O professor pede para o aluno apontar, no código, onde cada campo e escrito. São quatro lugares, e nenhum deles e na tela.

Os três if de millis estão em ordem de custo: o mais barato primeiro. Isso não muda o resultado, e o professor admite em voz alta. Mas o botao fica fora dos três, e esse e o ponto: ele não tem intervalo proprio porque a mao humana e o intervalo mais rapido do sistema.

O Serial.print do botao esta dentro do if do botao, e o Serial.print do bloco de tela esta dentro do if da tela. Isso produz dois formatos de linha no mesmo serial, e o professor justifica: um evento e um estado são informações diferentes. A contagem de apertos e um evento — aconteceu ou não aconteceu. A leitura de luz e um estado — e o que o sistema era naquele instante.

A struct foi declarada entre as constantes e as funções, e não no topo absoluto do arquivo. Código em C++ não obriga a posicao, e a colocacao e estetica: constantes, depois estado, depois prototipo, depois implementacao. O aluno que growing um código de 300 linhas sem seccionar vai perder o proprio programa até o fim do trimestre.

O -1.0 em distanciaCm continua significando "sem eco" mesmo com o campo semEco existindo ao lado. Isso e redundancia, e o professor diz que e. A razão: o snprintf da tela precisa de um número para imprimir, e uma função com um unico parametro e mais facil de manter do que uma com três.

Criterios de correcao

CritérioPontos
As quatro pecas montadas e os quatro campos aparecendo na tela2 pontos
struct Estado declarada, com os quatro campos, e estado global2 pontos
Os três relogios com millis, sem delay longo no loop3 pontos
Botao lido fora dos if de intervalo, e o motivo anotado2 pontos
Item 6 respondido: a saída não decide o que o programa faz1 ponto

Erros comuns

ErroComo apareceCorrecao
Quatro delay empilhados"O botao so funciona as vezes""Troque tudo por millis. Cada bloco pergunta se já deu tempo."
delay(500) para a tela"Contador de apertos atrasa""O delay longo para a tela atrasa tudo. So o delay(20) fica."
Campo como variável local"A tela mostra um número e o serial mostra outro""estado e global. E o unico lugar onde os blocos se encontram."
if do botao dentro do if da tela"Toque rapido não conta""O botao e lido a cada volta. Quem filtra e o intervalo da tela."
Escrever estado.luz no lugar errado"A tela mostra o número, mas o serial não""Confira qual função escreve o campo e qual so le."
pulseIn dentro do if da tela"A distancia so muda a cada 500 ms""O sensor tem o seu intervalo, 200 ms. Não espere a tela para medir."
Recompilar o código inteiro a cada campo novo"Gravou, mas a tela mostra a versão antiga""Cheque se você gravou o sketch certo, com o nome da pasta igual ao do arquivo."

Desafio extra

Reescreva o sketch trocando a tela de quatro linhas por duas telas alternadas, que trocam a cada 5 segundos: uma com os sensores, outra com os contadores. Ela precisa de uma função que desenhe a tela atual e outra que troque. Depois escreva, em três linhas, o que aconteceria no projeto final do dia 13 se o WiFi demorasse três segundos para responder — e em que ponto do loop essa espera teria que entrar.

>

A resolucao, compilada

// Aula 2 do dia 11: la central de monitoramento.
//
// Este sketch junta o que o trimestre ensina ate aqui: um sensor analogico, um
// sensor de tempo, um botao e um display, cada um com seu ritmo proprio. A
// dificuldade nao e ligar as pecas: e fazer quatro coisas que rodam em
// frequencias diferentes caberem em um unico loop.

#include <Arduino.h>
#include <Wire.h>
#include <string.h>

// ===== Barramento do display: as mesmas rotinas do dia 9 =====

const int PINO_SDA = 21;
const int PINO_SCL = 22;
const int ENDERECO_I2C = 0x3C;
const int LARGURA = 128;
const int ALTURA = 64;
const int TAMANHO_BUFFER = (LARGURA * ALTURA) / 8;
const int COMANDO = 0x00;
const int DADO = 0x40;

uint8_t buffer[TAMANHO_BUFFER];

void enviarComandos(const uint8_t *comandos, int quantidade) {
  Wire.beginTransmission(ENDERECO_I2C);
  Wire.write(COMANDO);
  for (int i = 0; i < quantidade; i++) {
    Wire.write(comandos[i]);
  }
  Wire.endTransmission();
}

void atualizarDisplay() {
  for (int pagina = 0; pagina < 8; pagina++) {
    const uint8_t posicao[3] = {
      (uint8_t)(0xB0 + pagina), 0x00, 0x10
    };
    enviarComandos(posicao, 3);
    Wire.beginTransmission(ENDERECO_I2C);
    Wire.write(DADO);
    for (int coluna = 0; coluna < LARGURA; coluna++) {
      Wire.write(buffer[(pagina * LARGURA) + coluna]);
    }
    Wire.endTransmission();
  }
}

void limparTela() {
  memset(buffer, 0, TAMANHO_BUFFER);
}

void escreverTexto(const char *texto, int coluna, int linha) {
  int posicao = (linha / 8) * LARGURA + coluna;
  while (*texto != 0 && posicao < TAMANHO_BUFFER) {
    buffer[posicao] = (uint8_t)(*texto);
    texto++;
    posicao++;
  }
}

void iniciarDisplay() {
  Wire.begin(PINO_SDA, PINO_SCL);
  delay(100);
  const uint8_t inicializacao[] = {
    0xAE, 0x20, 0x00, 0xB0, 0xC8, 0x00, 0x10, 0x40,
    0x81, 0x7F, 0xA1, 0xA6, 0xA8, 0x3F, 0xA4, 0xD3,
    0x00, 0xD5, 0x80, 0xD9, 0xF1, 0xDA, 0x12, 0xDB,
    0x40, 0x8D, 0x14, 0xAF,
  };
  enviarComandos(inicializacao, sizeof(inicializacao));
  delay(100);
  limparTela();
  atualizarDisplay();
}

// ===== Pinos e limites =====

const int PINO_LDR = 34;
const int PINO_TRIG = 5;
const int PINO_ECHO = 18;
const int PINO_BOTAO = 27;

const int DURACAO_TRIGGER = 10;
const int TEMPO_MAXIMO_US = 30000;
const float VELOCIDADE_CM_US = 0.034;

const int LIMIAR_ESCURO = 1800;
const int INTERVALO_LUZ_MS = 300;
const int INTERVALO_DISTANCIA_MS = 200;
const int INTERVALO_TELA_MS = 500;

// Cada rotina do projeto tem o seu proprio relogio. Declarar os tres juntos, no
// topo, e o que permite ver de relance que nada esta atrasando nada.
unsigned long ultimaLuz = 0;
unsigned long ultimaDistancia = 0;
unsigned long ultimaTela = 0;

// O estado compartilhado da central. Uma struct porque sao varias informacoes
// que andam sempre juntas: quando a tela mostra, ela mostra este bloco inteiro,
// e quando o serial imprime, ele imprime este bloco inteiro. Sao quatro
// numeros que descrevem um unico instante.
struct Estado {
  int luz;
  float distanciaCm;
  bool semEco;
  int apertos;
  int atualizacoes;
};

Estado estado = {0, -1.0, true, 0, 0};

// Prototipos das rotinas. Sem estas linhas o compilador reclama, porque a
// tela precisa da leitura da luz e essa leitura ainda nao foi declarada.
void medirLuz();
void medirDistancia();
void desenharTela();
void mostrarCabecalho();

// Uma leitura analogica, com a faixa do ADC ajustada para 3,3 V.
void medirLuz() {
  estado.luz = analogRead(PINO_LDR);
}

// Dispara o pulso do ultrassomico e converte o tempo de eco em centimetros.
void medirDistancia() {
  digitalWrite(PINO_TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(PINO_TRIG, HIGH);
  delayMicroseconds(DURACAO_TRIGGER);
  digitalWrite(PINO_TRIG, LOW);

  unsigned long eco = pulseIn(PINO_ECHO, HIGH, TEMPO_MAXIMO_US);
  if (eco == 0) {
    estado.semEco = true;
    estado.distanciaCm = -1.0;
  } else {
    estado.semEco = false;
    estado.distanciaCm = (eco * VELOCIDADE_CM_US) / 2.0;
  }
}

// Cabecalho impresso uma vez, para o serial nao virar centenas de linhas por
// minuto e ficar impossivel de ler.
void mostrarCabecalho() {
  Serial.println();
  Serial.println("CENTRAL DE MONITORAMENTO — dia 11, aula 2");
  Serial.println("luz | distancia | botao | telas");
  Serial.println();
}

// A tela tem quatro linhas de 16 pixels de altura. Quatro linhas e o limite do
// display de 64 pixels, e e por isso que o aluno escolhe o que deixar de fora
// quando precisa de um quinto campo.
void desenharTela() {
  char linha[24];

  snprintf(linha, sizeof(linha), "CENTRAL v1");
  escreverTexto(linha, 0, 0);

  snprintf(linha, sizeof(linha), "LUZ %4d", estado.luz);
  escreverTexto(linha, 0, 16);

  if (estado.semEco) {
    snprintf(linha, sizeof(linha), "DIST  sem eco");
  } else {
    snprintf(linha, sizeof(linha), "DIST %4d cm", (int)estado.distanciaCm);
  }
  escreverTexto(linha, 0, 32);

  snprintf(linha, sizeof(linha), "BT %d TELA %d",
           estado.apertos, estado.atualizacoes);
  escreverTexto(linha, 0, 48);

  atualizarDisplay();
}

void setup() {
  pinMode(PINO_LDR, INPUT);
  pinMode(PINO_TRIG, OUTPUT);
  pinMode(PINO_ECHO, INPUT);
  pinMode(PINO_BOTAO, INPUT_PULLUP);

  analogReadResolution(12);
  analogSetPinAttenuation(PINO_LDR, ADC_11db);

  iniciarDisplay();

  Serial.begin(115200);
  mostrarCabecalho();
}

void loop() {
  // O botao e lido a cada volta, porque o ser humano e mais rapido que o
  // intervalo de 500 ms da tela: se a contagem so subisse junto com o
  // redesenho, o aluno perderia os toques rapidos.
  if (digitalRead(PINO_BOTAO) == LOW) {
    estado.apertos++;
    Serial.print(">> botao apertado: ");
    Serial.println(estado.apertos);
  }

  // Cada rotina pergunta "ja deu tempo?" e o loop segue em frente. Este e o
  // padrao que separa o projeto do trimestre de um programa com quatro delays
  // empilhados, em que so a ultima coisa lida acontece.
  unsigned long agora = millis();

  if (agora - ultimaLuz >= INTERVALO_LUZ_MS) {
    ultimaLuz = agora;
    medirLuz();
  }

  if (agora - ultimaDistancia >= INTERVALO_DISTANCIA_MS) {
    ultimaDistancia = agora;
    medirDistancia();
  }

  if (agora - ultimaTela >= INTERVALO_TELA_MS) {
    ultimaTela = agora;
    estado.atualizacoes++;

    // Ordem fixa: limpar, escrever, atualizar. Sem o limpar, o numero antigo
    // continua embaixo do novo quando o novo tem menos digitos.
    limparTela();
    desenharTela();

    Serial.print(estado.luz);
    Serial.print("   | ");
    if (estado.semEco) {
      Serial.print("sem eco ");
    } else {
      Serial.print(estado.distanciaCm, 1);
      Serial.print(" cm ");
    }
    Serial.print("| ");
    Serial.print(estado.apertos);
    Serial.print("   |   ");
    Serial.println(estado.atualizacoes);
  }

  // Delay pequeno: da tempo ao processador sem travar o programa. E o que o
  // aluno aprende com a aula 3: esperar e uma decisao, nao um lugar fixo.
  delay(20);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 1 dia11 aula2.