/*
============================================================================
CRONÔMETRO DE LARGADA ESTILO F1
============================================================================
Hardware: Arduino Uno R3
COMO FUNCIONA:
1. Cinco LEDs vermelhos acendem em sequência, um a cada 0,6 segundo.
2. Com os 5 acesos, o sistema espera um tempo aleatório (1,2s a 6,4s).
3. Ao fim desse tempo, todos os LEDs apagam ao mesmo tempo = LARGADA.
4. Nesse instante, dois cronômetros começam a contar (um por piloto),
mostrados em tempo real em dois displays LCD.
5. Cada piloto tem um botão. Ao apertar, seu tempo é congelado na tela.
6. Se algum piloto apertar o botão ANTES da largada, é "falsa largada":
a corrida é cancelada e o infrator aparece identificado nas telas.
7. O botão Start/Restart reinicia tudo e limpa os resultados.
COMO O CÓDIGO É ORGANIZADO:
- Uma máquina de estados (enum EstadoCorrida) controla em que fase da
corrida o sistema está a cada momento.
- Em vez de delay(), o código usa millis() para medir o tempo. Isso
permite verificar LEDs, botões e displays ao mesmo tempo, sem travar
o programa esperando um tempo passar.
============================================================================
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// ============================================================================
// PINOS UTILIZADOS
// ============================================================================
const uint8_t PINOS_LEDS[5] = {2, 3, 4, 5, 6}; // ordem de acionamento
const uint8_t QUANTIDADE_LEDS = 5;
const uint8_t PINO_BOTAO_PILOTO_1 = 7;
const uint8_t PINO_BOTAO_PILOTO_2 = 8;
const uint8_t PINO_BOTAO_START = 9;
// Endereços I2C dos displays (ajustar conforme os jumpers de cada módulo)
const uint8_t ENDERECO_LCD_PILOTO_1 = 0x27;
const uint8_t ENDERECO_LCD_PILOTO_2 = 0x3F;
LiquidCrystal_I2C lcdPiloto1(ENDERECO_LCD_PILOTO_1, 16, 2);
LiquidCrystal_I2C lcdPiloto2(ENDERECO_LCD_PILOTO_2, 16, 2);
// ============================================================================
// TEMPOS DO SISTEMA (em milissegundos)
// ============================================================================
const unsigned long INTERVALO_ENTRE_LEDS = 800; // 0,6s entre cada LED
const unsigned long ESPERA_ALEATORIA_MIN = 1200; // 1,2s mínimo após o 5º LED
const unsigned long ESPERA_ALEATORIA_MAX = 6400; // 6,4s máximo após o 5º LED
const unsigned long TEMPO_DEBOUNCE = 30; // anti-ruído dos botões (ms)
const unsigned long INTERVALO_ATUALIZA_LCD = 50; // a cada quanto tempo redesenha o cronômetro
// ============================================================================
// MÁQUINA DE ESTADOS
// ============================================================================
enum EstadoCorrida {
AGUARDANDO_INICIO, // Sistema ligado, esperando o botão Start (LEDs apagados)
ACENDENDO_LUZES, // Acendendo os 5 LEDs em sequência
ESPERANDO_TEMPO_ALEATORIO, // Todos os LEDs acesos, contando o tempo aleatório
CORRIDA_EM_ANDAMENTO, // Luzes apagadas, cronômetros dos pilotos correndo
MOSTRANDO_RESULTADO // Resultado final na tela, esperando novo Start
};
EstadoCorrida estadoAtual = AGUARDANDO_INICIO;
// ============================================================================
// VARIÁVEIS DE TEMPO (controlam tudo sem usar delay())
// ============================================================================
unsigned long momentoEntradaNoEstado = 0; // quando o estado atual começou
unsigned long momentoDaLargada = 0; // instante exato em que os LEDs apagaram
unsigned long duracaoEsperaAleatoria = 0; // duração sorteada para a espera
uint8_t quantidadeLedsAcesos = 0; // quantos LEDs já estão acesos na sequência atual
// ============================================================================
// RESULTADOS DOS PILOTOS
// ============================================================================
bool tempoPiloto1Congelado = false;
bool tempoPiloto2Congelado = false;
unsigned long tempoReacaoPiloto1 = 0;
unsigned long tempoReacaoPiloto2 = 0;
bool piloto1FezFalsaLargada = false;
bool piloto2FezFalsaLargada = false;
// ============================================================================
// CONTROLE DE DEBOUNCE DOS BOTÕES
// ============================================================================
unsigned long ultimaMudancaBotaoPiloto1 = 0;
unsigned long ultimaMudancaBotaoPiloto2 = 0;
unsigned long ultimaMudancaBotaoStart = 0;
// Marca se o clique atual do botão Start (já estabilizado pelo debounce)
// já foi tratado. Evita reiniciar a corrida repetidamente enquanto o
// operador mantém o botão pressionado.
bool cliqueStartJaProcessado = false;
bool estadoAnteriorBotaoPiloto1 = HIGH;
bool estadoAnteriorBotaoPiloto2 = HIGH;
bool estadoAnteriorBotaoStart = HIGH;
unsigned long ultimaAtualizacaoDisplay = 0;
// ============================================================================
// DECLARAÇÃO DAS FUNÇÕES
// (permite organizar o código com as funções na ordem em que são chamadas,
// sem depender da ordem física em que aparecem mais abaixo no arquivo)
// ============================================================================
void mostrarTelaInicial();
void iniciarNovaCorrida();
void atualizarSequenciaDeLuzes();
void atualizarEsperaAleatoria();
void atualizarCronometrosNaTela();
void mostrarTempoNoDisplay(LiquidCrystal_I2C &display, unsigned long tempoEmMs);
void verificarBotaoPiloto1();
void verificarBotaoPiloto2();
void verificarSeCorridaTerminou();
void verificarFalsaLargada();
void mostrarResultadoFinal();
void verificarBotaoStart();
void apagarTodosOsLeds();
// ============================================================================
// SETUP — roda uma única vez, ao ligar o sistema
// ============================================================================
void setup() {
for (uint8_t i = 0; i < QUANTIDADE_LEDS; i++) {
pinMode(PINOS_LEDS[i], OUTPUT);
digitalWrite(PINOS_LEDS[i], LOW);
}
// INPUT_PULLUP: botão solto = HIGH, botão pressionado = LOW.
// Dispensa o uso de resistor externo nos botões.
pinMode(PINO_BOTAO_PILOTO_1, INPUT_PULLUP);
pinMode(PINO_BOTAO_PILOTO_2, INPUT_PULLUP);
pinMode(PINO_BOTAO_START, INPUT_PULLUP);
pinMode(BUZZER, OUTPUT)
lcdPiloto1.init();
lcdPiloto1.backlight();
lcdPiloto2.init();
lcdPiloto2.backlight();
// Usa a leitura de um pino analógico flutuante (sem nada ligado) como
// semente aleatória, garantindo que o sorteio do tempo de espera mude
// a cada vez que o sistema é ligado.
randomSeed(analogRead(A0));
mostrarTelaInicial();
}
// ============================================================================
// LOOP PRINCIPAL — roda continuamente, sem travar em nenhum delay()
// ============================================================================
void loop() {
verificarBotaoStart(); // verificado sempre, em qualquer estado
switch (estadoAtual) {
case AGUARDANDO_INICIO:
// Nada a fazer aqui: só esperando o botão Start ser pressionado.
break;
case ACENDENDO_LUZES:
atualizarSequenciaDeLuzes();
verificarFalsaLargada(); // já pode haver falsa largada nesta fase
break;
case ESPERANDO_TEMPO_ALEATORIO:
atualizarEsperaAleatoria();
verificarFalsaLargada(); // e também nesta fase
break;
case CORRIDA_EM_ANDAMENTO:
atualizarCronometrosNaTela();
verificarBotaoPiloto1();
verificarBotaoPiloto2();
verificarSeCorridaTerminou();
break;
case MOSTRANDO_RESULTADO:
// Resultado já está na tela; só aguarda o botão Start.
break;
}
}
// ============================================================================
// TROCA DE ESTADO
// Centraliza a mudança de estado e marca o instante em que ela ocorreu —
// esse instante é a referência usada por millis() em cada novo estado.
// ============================================================================
void mudarEstado(EstadoCorrida novoEstado) {
estadoAtual = novoEstado;
momentoEntradaNoEstado = millis();
}
// ============================================================================
// INÍCIO DE UMA NOVA CORRIDA (chamado pelo botão Start)
// ============================================================================
void iniciarNovaCorrida() {
tempoPiloto1Congelado = false;
tempoPiloto2Congelado = false;
tempoReacaoPiloto1 = 0;
tempoReacaoPiloto2 = 0;
piloto1FezFalsaLargada = false;
piloto2FezFalsaLargada = false;
quantidadeLedsAcesos = 0;
apagarTodosOsLeds();
lcdPiloto1.clear();
lcdPiloto1.setCursor(0, 0);
lcdPiloto1.print("PILOTO 1");
lcdPiloto1.setCursor(0, 1);
lcdPiloto1.print("Preparar...");
lcdPiloto2.clear();
lcdPiloto2.setCursor(0, 0);
lcdPiloto2.print("PILOTO 2");
lcdPiloto2.setCursor(0, 1);
lcdPiloto2.print("Preparar...");
mudarEstado(ACENDENDO_LUZES);
}
// ============================================================================
// SEQUÊNCIA DE ACENDIMENTO DOS 5 LEDS (sem usar delay)
// A cada 0,6s (INTERVALO_ENTRE_LEDS), mais um LED acende, com base no
// tempo decorrido desde a entrada neste estado.
// ============================================================================
void atualizarSequenciaDeLuzes() {
unsigned long tempoDecorrido = millis() - momentoEntradaNoEstado;
uint8_t ledsQueDeveriamEstarAcesos = tempoDecorrido / INTERVALO_ENTRE_LEDS;
if (ledsQueDeveriamEstarAcesos > QUANTIDADE_LEDS) {
ledsQueDeveriamEstarAcesos = QUANTIDADE_LEDS;
}
// Acende, um por um, os LEDs que ainda faltam
while (quantidadeLedsAcesos < ledsQueDeveriamEstarAcesos) {
digitalWrite(PINOS_LEDS[quantidadeLedsAcesos], HIGH);
quantidadeLedsAcesos++;
}
// Com os 5 LEDs acesos, sorteia o tempo de espera e avança de estado
if (quantidadeLedsAcesos >= QUANTIDADE_LEDS) {
duracaoEsperaAleatoria = random(ESPERA_ALEATORIA_MIN, ESPERA_ALEATORIA_MAX + 1);
mudarEstado(ESPERANDO_TEMPO_ALEATORIO);
}
}
// ============================================================================
// ESPERA ALEATÓRIA COM TODOS OS LEDS ACESOS
// Ao expirar o tempo sorteado, apaga todos os LEDs ao mesmo tempo —
// esse é o momento oficial da largada.
// ============================================================================
void atualizarEsperaAleatoria() {
unsigned long tempoDecorrido = millis() - momentoEntradaNoEstado;
if (tempoDecorrido >= duracaoEsperaAleatoria) {
apagarTodosOsLeds();
momentoDaLargada = millis(); // referência para os dois cronômetros
mudarEstado(CORRIDA_EM_ANDAMENTO);
lcdPiloto1.clear();
lcdPiloto1.setCursor(0, 0);
lcdPiloto1.print("PILOTO 1");
lcdPiloto2.clear();
lcdPiloto2.setCursor(0, 0);
lcdPiloto2.print("PILOTO 2");
}
}
// ============================================================================
// ATUALIZA OS CRONÔMETROS NA TELA (só do piloto que ainda não congelou)
// ============================================================================
void atualizarCronometrosNaTela() {
unsigned long agora = millis();
// Evita escrever no LCD a cada ciclo do loop (causaria flicker e
// sobrecarregaria o barramento I2C sem necessidade)
if (agora - ultimaAtualizacaoDisplay < INTERVALO_ATUALIZA_LCD) {
return;
}
ultimaAtualizacaoDisplay = agora;
unsigned long tempoAtualDeCorrida = agora - momentoDaLargada;
if (!tempoPiloto1Congelado) {
mostrarTempoNoDisplay(lcdPiloto1, tempoAtualDeCorrida);
}
if (!tempoPiloto2Congelado) {
mostrarTempoNoDisplay(lcdPiloto2, tempoAtualDeCorrida);
}
}
// Formata e mostra um tempo em milissegundos como "S.mmm s" na segunda linha do display
void mostrarTempoNoDisplay(LiquidCrystal_I2C &display, unsigned long tempoEmMs) {
unsigned long segundos = tempoEmMs / 1000;
unsigned long milissegundos = tempoEmMs % 1000;
char textoFormatado[17];
snprintf(textoFormatado, sizeof(textoFormatado), "%lu.%03lu s ", segundos, milissegundos);
display.setCursor(0, 1);
display.print(textoFormatado);
}
// ============================================================================
// LEITURA DO BOTÃO DO PILOTO 1 (com debounce)
// Relevante apenas durante CORRIDA_EM_ANDAMENTO, para registrar a reação.
// ============================================================================
void verificarBotaoPiloto1() {
bool leituraAtual = digitalRead(PINO_BOTAO_PILOTO_1);
if (leituraAtual != estadoAnteriorBotaoPiloto1) {
ultimaMudancaBotaoPiloto1 = millis();
estadoAnteriorBotaoPiloto1 = leituraAtual;
}
if ((millis() - ultimaMudancaBotaoPiloto1) > TEMPO_DEBOUNCE) {
if (leituraAtual == LOW && !tempoPiloto1Congelado) {
tempoReacaoPiloto1 = millis() - momentoDaLargada;
tempoPiloto1Congelado = true;
mostrarTempoNoDisplay(lcdPiloto1, tempoReacaoPiloto1);
lcdPiloto1.setCursor(0, 0);
lcdPiloto1.print("TEMPO REGISTRADO");
}
}
}
// ============================================================================
// LEITURA DO BOTÃO DO PILOTO 2 (com debounce)
// ============================================================================
void verificarBotaoPiloto2() {
bool leituraAtual = digitalRead(PINO_BOTAO_PILOTO_2);
if (leituraAtual != estadoAnteriorBotaoPiloto2) {
ultimaMudancaBotaoPiloto2 = millis();
estadoAnteriorBotaoPiloto2 = leituraAtual;
}
if ((millis() - ultimaMudancaBotaoPiloto2) > TEMPO_DEBOUNCE) {
if (leituraAtual == LOW && !tempoPiloto2Congelado) {
tempoReacaoPiloto2 = millis() - momentoDaLargada;
tempoPiloto2Congelado = true;
mostrarTempoNoDisplay(lcdPiloto2, tempoReacaoPiloto2);
lcdPiloto2.setCursor(0, 0);
lcdPiloto2.print("TEMPO REGISTRADO");
}
}
}
// ============================================================================
// VERIFICA SE OS DOIS PILOTOS JÁ REGISTRARAM O TEMPO DE REAÇÃO
// Se sim, a corrida terminou: mostra o resultado final e muda de estado.
// ============================================================================
void verificarSeCorridaTerminou() {
if (tempoPiloto1Congelado && tempoPiloto2Congelado) {
mostrarResultadoFinal();
mudarEstado(MOSTRANDO_RESULTADO);
}
}
// ============================================================================
// DETECÇÃO DE FALSA LARGADA
// ----------------------------------------------------------------------------
// Chamada a cada ciclo do loop enquanto o sistema está em ACENDENDO_LUZES
// ou ESPERANDO_TEMPO_ALEATORIO — ou seja, em qualquer momento ANTES do
// apagamento oficial das luzes.
//
// Funciona checando diretamente se algum botão de piloto está pressionado
// (LOW) nesse intervalo. Se estiver, o piloto "se moveu" antes da hora —
// igual ao que acontece na F1, em que sensores na grid detectam o avanço
// do carro antes do sinal verde.
//
// Ao detectar a infração: apaga os LEDs na hora, marca qual piloto errou,
// mostra o resultado já invalidado e pula direto para MOSTRANDO_RESULTADO.
//
// Não há debounce aqui de propósito: qualquer toque, mesmo rápido, já
// caracteriza a infração e precisa ser detectado imediatamente.
// ============================================================================
void verificarFalsaLargada() {
bool botaoPiloto1Pressionado = (digitalRead(PINO_BOTAO_PILOTO_1) == LOW);
bool botaoPiloto2Pressionado = (digitalRead(PINO_BOTAO_PILOTO_2) == LOW);
if (botaoPiloto1Pressionado || botaoPiloto2Pressionado) {
apagarTodosOsLeds();
if (botaoPiloto1Pressionado) piloto1FezFalsaLargada = true;
if (botaoPiloto2Pressionado) piloto2FezFalsaLargada = true;
mostrarResultadoFinal();
mudarEstado(MOSTRANDO_RESULTADO);
}
}
// ============================================================================
// MOSTRA O RESULTADO FINAL NOS DOIS DISPLAYS
// Cobre tanto o cenário normal (tempos de reação) quanto o de falsa largada.
// ============================================================================
void mostrarResultadoFinal() {
// --- Cenário 1: houve falsa largada de um ou dos dois pilotos ---
if (piloto1FezFalsaLargada || piloto2FezFalsaLargada) {
if (piloto1FezFalsaLargada) {
lcdPiloto1.clear();
lcdPiloto1.setCursor(0, 0);
lcdPiloto1.print("FALSA LARGADA!");
lcdPiloto1.setCursor(0, 1);
lcdPiloto1.print("Piloto 1 - PENAL");
} else {
lcdPiloto1.clear();
lcdPiloto1.setCursor(0, 0);
lcdPiloto1.print("CORRIDA ANULADA");
lcdPiloto1.setCursor(0, 1);
lcdPiloto1.print("Rival queimou");
}
if (piloto2FezFalsaLargada) {
lcdPiloto2.clear();
lcdPiloto2.setCursor(0, 0);
lcdPiloto2.print("FALSA LARGADA!");
lcdPiloto2.setCursor(0, 1);
lcdPiloto2.print("Piloto 2 - PENAL");
} else {
lcdPiloto2.clear();
lcdPiloto2.setCursor(0, 0);
lcdPiloto2.print("CORRIDA ANULADA");
lcdPiloto2.setCursor(0, 1);
lcdPiloto2.print("Rival queimou");
}
return;
}
// --- Cenário 2: corrida válida, mostra os tempos e quem venceu ---
lcdPiloto1.clear();
lcdPiloto1.setCursor(0, 0);
lcdPiloto1.print(tempoReacaoPiloto1 < tempoReacaoPiloto2 ? "VENCEDOR!" : "PILOTO 1");
mostrarTempoNoDisplay(lcdPiloto1, tempoReacaoPiloto1);
lcdPiloto2.clear();
lcdPiloto2.setCursor(0, 0);
lcdPiloto2.print(tempoReacaoPiloto2 < tempoReacaoPiloto1 ? "VENCEDOR!" : "PILOTO 2");
mostrarTempoNoDisplay(lcdPiloto2, tempoReacaoPiloto2);
}
// ============================================================================
// LEITURA DO BOTÃO START/RESTART (com debounce)
// Funciona em qualquer estado, permitindo reiniciar a corrida a qualquer
// momento — inclusive para abortar uma sequência em andamento.
// ============================================================================
void verificarBotaoStart() {
bool leituraAtual = digitalRead(PINO_BOTAO_START);
// Mudou de nível: reinicia a contagem de debounce e libera o
// processamento de um novo clique.
if (leituraAtual != estadoAnteriorBotaoStart) {
ultimaMudancaBotaoStart = millis();
cliqueStartJaProcessado = false;
estadoAnteriorBotaoStart = leituraAtual;
}
// O nível ficou estável por tempo suficiente: se for LOW (pressionado)
// e esse clique ainda não foi tratado, processa agora, uma única vez.
if ((millis() - ultimaMudancaBotaoStart) > TEMPO_DEBOUNCE) {
if (leituraAtual == LOW && !cliqueStartJaProcessado) {
cliqueStartJaProcessado = true;
iniciarNovaCorrida();
}
}
}
// ============================================================================
// APAGA TODOS OS LEDS DE UMA VEZ
// ============================================================================
void apagarTodosOsLeds() {
for (uint8_t i = 0; i < QUANTIDADE_LEDS; i++) {
digitalWrite(PINOS_LEDS[i], LOW);
}
}
// ============================================================================
// TELA INICIAL, MOSTRADA AO LIGAR O SISTEMA (CHAVE GERAL)
// ============================================================================
void mostrarTelaInicial() {
lcdPiloto1.clear();
lcdPiloto1.setCursor(0, 0);
lcdPiloto1.print("CRONOMETRO F1");
lcdPiloto1.setCursor(0, 1);
lcdPiloto1.print("Pressione START");
lcdPiloto2.clear();
lcdPiloto2.setCursor(0, 0);
lcdPiloto2.print("CRONOMETRO F1");
lcdPiloto2.setCursor(0, 1);
lcdPiloto2.print("Pressione START");
}