Ultrassomico e o projeto da metade do trimestre — Arduino e IoT — semana 11 do 1o trimestre
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
TRIGdoHC-SR04e medir o tempo do eco compulseIn. - Converter a duracao do eco em distancia em centimetros, dividindo por dois, e explicar por que.
- Recusar a leitura quando o
pulseIndevolve 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-SR04e 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:
| Situacao | O que aconteceu |
|---|---|
| Nada a frente em 5 m | o pulso saiu e não voltou |
| Sensor desconectado | nunca houve pulso de saída |
TRIG e ECHO trocados | o sensor espera o pulso que não vem |
| Obstaculo longe demais | o 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
| Limite | Valor | Por que existe |
|---|---|---|
| Minimo | cerca de 2 cm | perto demais, o sensor mede a propria capsula |
| Maximo | cerca de 4 m | o eco chega fraco demais e o sensor erra |
| Zona cega | conica | o 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:
VCCdoHC-SR04na trilha+da protoboard.GNDdoHC-SR04na trilha-.TRIGdoHC-SR04noGPIO5.ECHOdoHC-SR04noGPIO18.
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.
- Monte o circuito do enunciado e grave o sketch da resolucao.
- 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?
- Troque
/ 2.0por 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. - 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.
- 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.
- Troque
TEMPO_MAXIMO_USde 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ério | Pontos |
|---|---|
Circuito montado, com TRIG no GPIO5 e ECHO no GPIO18 | 2 pontos |
| A divisão por 2 presente, e a razão do caminho de ida e volta explicada | 3 pontos |
pulseIn com tempo limite, e o zero tratado como "sem eco" e não como distancia | 3 pontos |
| Medida a 30 cm confere com a regua dentro da tolerancia da aula | 1 ponto |
| Item 6 respondido: o timeout controla o alcance, não a precisao | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
| 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, oHC-SR04, um botao e o displayOLED, 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
loopsemdelaylongo. - Declarar a
structque 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
OLEDI2C de 0,96 polegada (128x64, controladorSSD1306) por aluno - 1
LDRe 1 resistor de 10 k ohm por aluno - 1 sensor
HC-SR04e 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:
| Bloco | Como le | Intervalo | Por que esse |
|---|---|---|---|
| Botao | digitalRead | a cada volta | a mao e mais rapida que qualquer intervalo |
LDR | analogRead | 300 ms | leitura analogica e cara em relacao a digital |
HC-SR04 | pulseIn | 200 ms | o sensor exige mais de 60 ms entre disparos |
| Display | 1024 bytes no I2C | 500 ms | o 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:
| Linha | Campo | Fonte |
|---|---|---|
| 0 | titulo e versão | constante |
| 16 | luz | LDR |
| 32 | distancia | HC-SR04, ou "sem eco" |
| 48 | contadores | botao 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:
VCCna trilha+,GNDna trilha-,SDAnoGPIO21,SCLnoGPIO22. LDR: uma perna na trilha+, outra noGPIO34, com resistor de 10 k doGPIO34para a trilha-.HC-SR04:VCCna trilha+,GNDna trilha-,TRIGnoGPIO5,ECHOnoGPIO18.- 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.
- Monte o circuito do enunciado e grave o sketch da resolucao. Confira os quatro campos na tela.
- 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.
- Troque
INTERVALO_LUZ_MSde 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. - 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.
- Adicione um campo novo na tela: a umidade, calculada por uma função que você vai escrever para o
DHT11do 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. - 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ério | Pontos |
|---|---|
| As quatro pecas montadas e os quatro campos aparecendo na tela | 2 pontos |
struct Estado declarada, com os quatro campos, e estado global | 2 pontos |
Os três relogios com millis, sem delay longo no loop | 3 pontos |
Botao lido fora dos if de intervalo, e o motivo anotado | 2 pontos |
| Item 6 respondido: a saída não decide o que o programa faz | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
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.
