Robótica com C — Bloco 2
Esta aula não introduz nenhum componente ou função nova. Ela consolida tudo que o Bloco 2 ensinou num único projeto funcional — o mesmo tipo de sistema que os projetos do Bloco 3 vão usar, mas sem Bluetooth ainda.
O sistema simula um painel de controle doméstico com três modos de operação, dois dispositivos (LED de iluminação e LED de alarme), um sensor de luminosidade (LDR) e um display LCD que mostra o estado em tempo real:
| Modo | Comportamento | Como ativar |
|---|---|---|
| AUTO | A iluminação acende e apaga automaticamente conforme a luminosidade do LDR | Comando "A" via Serial |
| MANUAL | Iluminação controlada diretamente por comandos Serial ("L" liga, "D" desliga) |
Comando "M" via Serial |
| EMERGENCIA | Iluminação pisca rapidamente, alarme ligado, ignora todos os outros comandos exceto "R" para resetar |
Comando "E" via Serial ou LDR abaixo do limiar crítico |
Por que este projeto é o divisor de águas do curso: os projetos do Bloco 3 têm exatamente esta estrutura — modos de operação + LCD + controle remoto. A diferença será apenas o canal (Bluetooth em vez de USB) e os dispositivos (relay, servo, motor em vez de LEDs). Dominar este projeto significa estar pronto para o Bloco 3.
| Componente | Pino | Função no projeto |
|---|---|---|
| LED amarelo + resistor 220Ω | 9 (PWM) | Iluminação — controlada pelo modo |
| LED vermelho + resistor 220Ω | 8 | Alarme — ativo no modo EMERGENCIA |
| LDR + resistor 10kΩ | A0 | Sensor de luminosidade para o modo AUTO |
| LCD I2C 16×2 | A4 (SDA), A5 (SCL) | Exibe modo atual, status e luminosidade |
// Montagem no Wokwi:
// Pino 9 → resistor 220Ω → LED amarelo (ânodo) → GND
// Pino 8 → resistor 220Ω → LED vermelho (ânodo) → GND
// 5V → resistor 10kΩ → A0 → LDR → GND (divisor de tensão)
// LCD I2C: GND→GND, VCC→5V, SDA→A4, SCL→A5
Projetos com múltiplos subsistemas (estado + sensor + display + Serial) ficam difíceis de depurar quando tudo está misturado no loop(). A solução é a mesma que você usou em C desde a Aula 13: dividir em funções com responsabilidade única.
O projeto é dividido em quatro camadas:
| Camada | Funções | Responsabilidade |
|---|---|---|
| Sensores | lerLuminosidade() |
Leitura de hardware — retorna dados brutos |
| Estado | mudarModo(), processarAuto(), processarEmergencia() |
Lógica de transição e comportamento de cada modo |
| Saída física | atualizarLEDs() |
Traduz o estado em sinais elétricos |
| Interface | atualizarLCD(), processarSerial() |
Comunicação com o usuário |
O loop() apenas orquestra essas camadas — não contém lógica de negócio:
// Como o loop() vai ficar — lê-se como um roteiro: void loop() { processarSerial(); // 1. há comandos do usuário? int lum = lerLuminosidade(); // 2. qual a luminosidade agora? switch (modoAtual) { case AUTO: processarAuto(lum); break; case EMERGENCIA: processarEmergencia(); break; case MANUAL: /* aguarda comandos */ break; } atualizarLEDs(); // 3. atualiza os pinos físicos atualizarLCD(lum); // 4. atualiza o display }
Escreva o loop() primeiro, em pseudocódigo. Quando o roteiro de alto nível está claro, as funções individuais ficam triviais de implementar. Esse é o mesmo princípio top-down que você usou no Projeto Integrador da Aula 40.
O código está organizado em blocos comentados, seguindo exatamente a arquitetura planejada acima:
/* * ===================================================== * Painel de Controle Doméstico — Aula 47 * Bloco 2: Projeto Integrador * ===================================================== * Hardware: LED iluminação (p9), LED alarme (p8), * LDR (A0), LCD I2C 16×2 (A4/A5) * Comandos: A=Auto | M=Manual | E=Emergência * L=Ligar | D=Desligar | R=Reset * ===================================================== */ #include <Wire.h> #include <LiquidCrystal_I2C.h> // ── Pinos ────────────────────────────────────────── const int PINO_LUZ = 9; // LED iluminação (PWM) const int PINO_ALARM = 8; // LED alarme const int PINO_LDR = A0; // sensor de luminosidade // ── LCD ──────────────────────────────────────────── LiquidCrystal_I2C lcd(0x27, 16, 2); // ── Máquina de estados ───────────────────────────── typedef enum { AUTO, MANUAL, EMERGENCIA } Modo; Modo modoAtual = AUTO; // inicia em AUTO // ── Estado dos dispositivos ──────────────────────── bool luzLigada = false; bool alarmeLigado = false; // ── Limiares ─────────────────────────────────────── const int LIMIAR_AUTO = 400; // abaixo: liga luz no AUTO const int LIMIAR_CRITICO = 100; // abaixo: ativa EMERGENCIA // ── Temporização ─────────────────────────────────── unsigned long ultimaTelaAtual = 0; unsigned long ultimoPiscada = 0; bool estadoPiscada = false; // ════════════════════════════════════════════════════ // CAMADA 1 — SENSORES // ════════════════════════════════════════════════════ int lerLuminosidade() { return analogRead(PINO_LDR); // 0–1023 } // ════════════════════════════════════════════════════ // CAMADA 2 — ESTADO // ════════════════════════════════════════════════════ void mudarModo(Modo novoModo) { if (modoAtual == novoModo) return; // já está neste modo modoAtual = novoModo; luzLigada = false; alarmeLigado = false; Serial.print(">> Modo: "); switch (novoModo) { case AUTO: Serial.println("AUTO"); break; case MANUAL: Serial.println("MANUAL"); break; case EMERGENCIA: Serial.println("EMERGENCIA"); break; } } // Comportamento do modo AUTO void processarAuto(int luminosidade) { // Luminosidade crítica → aciona EMERGENCIA automaticamente if (luminosidade < LIMIAR_CRITICO) { mudarModo(EMERGENCIA); return; } // Escuro → liga luz; claro → apaga luzLigada = (luminosidade < LIMIAR_AUTO); } // Comportamento do modo EMERGENCIA — pisca a iluminação void processarEmergencia() { alarmeLigado = true; if (millis() - ultimoPiscada >= 300) { // pisca a cada 300ms ultimoPiscada = millis(); estadoPiscada = !estadoPiscada; luzLigada = estadoPiscada; } } // ════════════════════════════════════════════════════ // CAMADA 3 — SAÍDA FÍSICA // ════════════════════════════════════════════════════ void atualizarLEDs() { digitalWrite(PINO_LUZ, luzLigada ? HIGH : LOW); digitalWrite(PINO_ALARM, alarmeLigado ? HIGH : LOW); } // ════════════════════════════════════════════════════ // CAMADA 4 — INTERFACE // ════════════════════════════════════════════════════ void atualizarLCD(int lum) { // Atualiza no máximo a cada 500ms para evitar pisca if (millis() - ultimaTelaAtual < 500) return; ultimaTelaAtual = millis(); // Linha 0: modo atual lcd.setCursor(0, 0); switch (modoAtual) { case AUTO: lcd.print("MODO: AUTO "); break; case MANUAL: lcd.print("MODO: MANUAL "); break; case EMERGENCIA: lcd.print("!! EMERGENCIA !!"); break; } // Linha 1: status dos dispositivos e luminosidade lcd.setCursor(0, 1); lcd.print("Luz:"); lcd.print(luzLigada ? "ON " : "OFF"); lcd.print(" Lum:"); lcd.print(map(lum, 0, 1023, 0, 100)); lcd.print("% "); } void processarSerial() { if (!Serial.available()) return; String msg = Serial.readStringUntil('\n'); msg.trim(); if (msg.length() == 0) return; char cmd = msg[0]; // No modo EMERGENCIA só "R" funciona if (modoAtual == EMERGENCIA && cmd != 'R' && cmd != 'r') { Serial.println("EMERGENCIA ativa — apenas R para resetar"); return; } switch (cmd) { case 'A': case 'a': mudarModo(AUTO); break; case 'M': case 'm': mudarModo(MANUAL); break; case 'E': case 'e': mudarModo(EMERGENCIA); break; case 'L': case 'l': if (modoAtual == MANUAL) { luzLigada = true; Serial.println("Luz LIGADA"); } else { Serial.println("Comando L só funciona no modo MANUAL"); } break; case 'D': case 'd': if (modoAtual == MANUAL) { luzLigada = false; Serial.println("Luz DESLIGADA"); } else { Serial.println("Comando D só funciona no modo MANUAL"); } break; case 'R': case 'r': mudarModo(AUTO); Serial.println("Sistema resetado → AUTO"); break; case '?': Serial.println("Comandos: A M E L D R"); break; case '\n': case '\r': break; default: Serial.print("Desconhecido: "); Serial.println(cmd); } } // ════════════════════════════════════════════════════ // SETUP e LOOP // ════════════════════════════════════════════════════ void setup() { pinMode(PINO_LUZ, OUTPUT); pinMode(PINO_ALARM, OUTPUT); lcd.init(); lcd.backlight(); Serial.begin(9600); Serial.println("=== Painel de Controle ==="); Serial.println("Comandos: A=Auto M=Manual E=Emerg L=Luz D=Deslig R=Reset ?=Ajuda"); lcd.setCursor(0, 0); lcd.print("Iniciando..."); delay(1000); } void loop() { processarSerial(); int lum = lerLuminosidade(); switch (modoAtual) { case AUTO: processarAuto(lum); break; case EMERGENCIA: processarEmergencia(); break; case MANUAL: /* aguarda comandos */ break; } atualizarLEDs(); atualizarLCD(lum); }
=== Painel de Controle ===
Comandos: A=Auto M=Manual E=Emerg L=Luz D=Deslig R=Reset ?=Ajuda
[digitou M] >> Modo: MANUAL
[digitou L] Luz LIGADA
[digitou D] Luz DESLIGADA
[digitou E] >> Modo: EMERGENCIA
[digitou L] EMERGENCIA ativa — apenas R para resetar
[digitou R] Sistema resetado → AUTO
loop() — por que ele é tão curtoCom a arquitetura em camadas, o loop() ficou com apenas 7 linhas de conteúdo. Cada linha é uma pergunta:
| Linha | Pergunta respondida | Camada |
|---|---|---|
processarSerial() |
O usuário enviou algum comando? | Interface |
int lum = lerLuminosidade() |
Qual o estado do ambiente agora? | Sensores |
switch(modoAtual) |
O que o modo atual deve fazer com esses dados? | Estado |
atualizarLEDs() |
Os pinos físicos refletem o estado interno? | Saída física |
atualizarLCD(lum) |
O display mostra informação atualizada? | Interface |
Separação entre estado lógico e estado físico: note que processarAuto() e processarEmergencia() só modificam as variáveis luzLigada e alarmeLigado — elas não chamam digitalWrite() diretamente. Quem faz isso é atualizarLEDs(), sempre no final do loop. Essa separação evita que o estado físico fique inconsistente quando múltiplas funções tentam controlar o mesmo pino.
Este projeto tem quatro substituições diretas que o transformam nos projetos reais do Bloco 3:
| Nesta aula | Nos projetos do Bloco 3 | Mudança no código |
|---|---|---|
| Serial USB (Monitor Serial) | Bluetooth HC-05 (app no celular) | Troca Serial por bluetooth em processarSerial() |
| LED de iluminação | Módulo relay → lâmpada real | Pino do relay no lugar do pino do LED — digitalWrite() idêntico |
| LED de alarme | Buzzer ou segundo relay | Mesmo pino, mesmo digitalWrite() |
| LDR como sensor | PIR, DHT11, ultrassônico | Troca a leitura em lerLuminosidade() |
A arquitetura em camadas isola as mudanças. Quando você trocar o LED pelo relay, só muda o número do pino em uma linha. Quando trocar o Serial pelo Bluetooth, só muda o objeto em processarSerial(). O restante do código — máquina de estados, LCD, lógica de cada modo — não toca. Isso é exatamente o que torna o código escalável.
O mapa completo de estados e transições — cada seta é uma condição que muda o modoAtual:
┌─────────────────────────────────┐
│ │
cmd "A"│ │cmd "A"
▼ │
┌──────────────┐ cmd "M" ┌──────────────┐
│ │──────────────▶│ │
│ AUTO │ │ MANUAL │
│ │◀──────────────│ │
└──────────────┘ cmd "A" └──────────────┘
│ │
lum < 100 │ cmd "E" cmd "E" │
(crítico) │◀─────────────────────────────┘
│
▼
┌──────────────┐
│ │
│ EMERGENCIA │
│ (pisca+ │
│ alarme) │
└──────────────┘
│
cmd "R"│
▼
volta para AUTO
Regra de ouro das transições: no modo EMERGENCIA, apenas "R" funciona — todos os outros comandos são bloqueados em processarSerial(). Isso representa um princípio de segurança: estados críticos devem ser explicitamente resetados, não silenciosamente sobrescritos por um comando mal enviado.
Antes de avançar para o Bloco 3, vale mapear como cada aula do bloco contribuiu para este projeto integrador:
| Aula | Conceito introduzido | Onde aparece neste projeto |
|---|---|---|
| 44 | enum de estados, switch, millis() para transições |
typedef enum { AUTO, MANUAL, EMERGENCIA }, switch(modoAtual), pisca em EMERGENCIA |
| 45 | LCD I2C, atualização com setCursor(), frequência controlada |
atualizarLCD() com controle de 500ms e sobrescrita com espaços |
| 46 | Protocolo Serial, padrão available()/read(), feedback estruturado |
processarSerial() com readStringUntil(), bloqueio no modo EMERGENCIA |
| 41b–43 | LDR analógico, map(), arquitetura não-bloqueante |
lerLuminosidade(), conversão para %, nenhum delay() no código final |
O Bloco 3 começa aqui. Você agora tem todos os padrões necessários:
enum + switch + millis()O Projeto 01 do Bloco 3 vai pegar este projeto, trocar o LED pelo relay e o Monitor Serial pelo app do celular. Você vai reconhecer cada linha.
Teste sua compreensão do projeto integrador e da arquitetura em camadas: