Aula 41b — Wokwi, Serial e Conceitos Avançados

Robótica com C — Bloco 1

O que é o Wokwi?

Wokwi (wokwi.com) é um simulador de Arduino online e gratuito. Você escreve o código, monta o circuito e vê tudo funcionando — sem precisar de placa, LED, resistor ou fio físico nenhum.

Por que usar o Wokwi antes do hardware físico?

A estratégia deste curso é: simule no Wokwi primeiro, entenda o comportamento, e só depois monte no hardware físico. Você vai errar menos e não vai desperdiçar componentes.

Primeiros passos: criando um projeto

O fluxo para criar um projeto no Wokwi é direto:

Passo O que fazer Analogia com C
1 Acesse wokwi.com e clique em New Project → Arduino Uno Criar um novo arquivo programa.c
2 Na aba diagram.json clique no "+" para adicionar componentes (LED, resistor, sensor…) Declarar as variáveis e structs que seu programa usará
3 Clique num componente e arraste os fios até os pinos da placa Conectar as partes — como encadear nodos em uma lista
4 Na aba sketch.ino escreva o código Escrever o código dentro do .c
5 Clique em ▶ Play para iniciar a simulação Compilar e executar: gcc programa.c && ./a.out
6 Abra o Monitor Serial para ver mensagens e enviar comandos ao Arduino Ver saída do printf() e digitar entrada para o scanf()

O circuito do Blink com LED externo

O LED soldado na placa (pino 13) é ótimo para testar, mas na prática você vai sempre usar LEDs externos. Isso exige entender três componentes:

Componente O que é Por que é necessário
LED Diodo Emissor de Luz — acende quando corrente elétrica passa no sentido correto Tem polaridade: perna longa = ânodo (+), perna curta = cátodo (−)
Resistor 220Ω Limita a corrente elétrica que passa pelo LED Sem ele, o LED queima em segundos. O Arduino fornece 5V; o LED aguenta ~2V com ~20mA
GND Pino de referência 0V da placa Todo circuito precisa de um caminho de retorno para a corrente
Pino 8 do Arduino
    │
    ├─── Resistor 220Ω ─── ânodo (+, perna longa) ─── cátodo (−, perna curta)
                                                                │
                                                               GND

Regra do resistor: Sempre que conectar um LED a um pino do Arduino, use um resistor de 220Ω a 470Ω em série — tanto no Wokwi quanto no hardware físico.

Código completo: Blink com LED externo e Serial

Este é o primeiro sketch completo do curso, já seguindo as boas práticas da Aula 41a (const int em vez de #define para pinos):

/*
 * Projeto 01 — Blink com LED externo
 * Pino 8: LED + resistor 220Ω → GND
 */

const int PINO_LED  = 8;    // const int preferível a #define para pinos
const int TEMPO_ON  = 1000;
const int TEMPO_OFF = 500;  // pisca assimétrico

int contadorPiscadas = 0;

void setup() {
    pinMode(PINO_LED, OUTPUT);
    Serial.begin(9600);
    Serial.println("=== Blink iniciado ===");
}

void loop() {
    digitalWrite(PINO_LED, HIGH);
    contadorPiscadas++;
    Serial.print("Piscada #");
    Serial.println(contadorPiscadas);
    delay(TEMPO_ON);

    digitalWrite(PINO_LED, LOW);
    delay(TEMPO_OFF);
}

=== Blink iniciado ===

Piscada #1

Piscada #2

Piscada #3

...

O problema do delay() e a solução com millis()

delay() é simples e funciona bem para exemplos básicos, mas tem um problema sério: ele bloqueia tudo. Enquanto o programa espera, ele não lê sensores, não verifica botões, não responde a nada.

A solução é millis(), que retorna o número de milissegundos desde que a placa ligou — como um relógio. Com ele você verifica se já passou tempo suficiente sem parar o programa:

// Blink SEM delay() — o programa continua livre para outras tarefas

const int PINO_LED  = 8;
const int INTERVALO = 1000;

int estadoLED = LOW;
unsigned long ultimoTempo = 0;

void setup() {
    pinMode(PINO_LED, OUTPUT);
    Serial.begin(9600);
}

void loop() {
    unsigned long agora = millis();

    if (agora - ultimoTempo >= INTERVALO) {
        ultimoTempo = agora;
        estadoLED   = (estadoLED == LOW) ? HIGH : LOW;
        digitalWrite(PINO_LED, estadoLED);
        Serial.println(estadoLED == HIGH ? "LED ON" : "LED OFF");
    }

    // Aqui há espaço para ler sensores, verificar botões, etc.
}
Aspecto delay(ms) millis()
Bloqueia o programa? Sim — congela pelo tempo inteiro Não — programa continua rodando
Facilidade de uso Muito simples Requer variável de controle de tempo
Quando usar Exemplos simples, um único componente Sempre que o projeto tiver 2+ componentes

Serial bidirecional: Serial.read() e Serial.write()

Até aqui você usou o Serial só para enviar mensagens ao computador com Serial.print(). Mas a comunicação serial é bidirecional — o Arduino também pode receber dados digitados no Monitor Serial. Isso é fundamental para controlar o robô por comandos de texto.

Função Direção O que faz Analogia em C
Serial.print(x) Arduino → PC Envia x como texto legível printf("%s", x)
Serial.println(x) Arduino → PC Igual ao print, mas adiciona \n no final printf("%s\n", x)
Serial.write(byte) Arduino → PC Envia um byte bruto (não converte para texto) fwrite(&byte, 1, 1, stdout)
Serial.available() — Retorna quantos bytes chegaram e ainda não foram lidos Verificar se há dados no buffer de entrada
Serial.read() PC → Arduino Lê e retira um byte do buffer de entrada (retorna -1 se vazio) getchar()
Serial.readString() PC → Arduino Lê toda a string disponível no buffer fgets(buf, n, stdin)

Sempre verifique Serial.available() antes de Serial.read(). Se o buffer estiver vazio e você chamar Serial.read(), ele retorna -1. É o mesmo princípio de verificar se um arquivo tem dados antes de ler — você já fez isso com fscanf() em C.

Exemplo prático: Arduino que responde a comandos digitados no Monitor Serial:

/*
 * Controle por Serial — Liga/desliga LED por comando de texto
 * Digite "L" no Monitor Serial para ligar, "D" para desligar
 */

const int PINO_LED = 8;

void setup() {
    pinMode(PINO_LED, OUTPUT);
    Serial.begin(9600);
    Serial.println("Comandos: L = ligar, D = desligar");
}

void loop() {
    if (Serial.available() > 0) {        // há bytes no buffer?
        char cmd = Serial.read();        // lê um caractere

        if (cmd == 'L' || cmd == 'l') {
            digitalWrite(PINO_LED, HIGH);
            Serial.println("LED LIGADO");
        } else if (cmd == 'D' || cmd == 'd') {
            digitalWrite(PINO_LED, LOW);
            Serial.println("LED DESLIGADO");
        } else if (cmd != '\n' && cmd != '\r') {  // ignora quebras de linha
            Serial.print("Comando desconhecido: ");
            Serial.println(cmd);
        }
    }
}

Comandos: L = ligar, D = desligar

[usuário digita "L" e pressiona Enter]

LED LIGADO

[usuário digita "D"]

LED DESLIGADO

Esse padrão — verificar Serial.available(), ler com Serial.read(), processar com if/switch — é o mesmo que você usará para receber comandos de um módulo Bluetooth no robô. Aprenda bem aqui, pois ele retorna nas aulas de robótica com um canal diferente, mas lógica idêntica.

Interrupções externas e variáveis volatile

No modelo normal do Arduino, o programa lê o estado de um botão dentro do loop() — e se o loop estiver ocupado, pode perder um evento rápido. Interrupções resolvem isso: elas permitem que o microcontrolador pause imediatamente o que está fazendo para atender a um evento externo, e depois volte exatamente de onde parou.

Analogia com C: pense na interrupção como um sinal Unix (SIGINT, SIGALRM) — o SO interrompe o processo principal para chamar um handler, e depois o processo continua. A função chamada pela interrupção no Arduino se chama ISR (Interrupt Service Routine).

No Arduino Uno, apenas os pinos 2 e 3 suportam interrupções externas. A função que configura a interrupção é attachInterrupt():

// Sintaxe:
// attachInterrupt(digitalPinToInterrupt(pino), funcaoISR, modo);
//
// Modos disponíveis:
//   RISING  — dispara na borda de subida (LOW → HIGH)
//   FALLING — dispara na borda de descida (HIGH → LOW)
//   CHANGE  — dispara em qualquer mudança de estado
//   LOW     — dispara enquanto o pino estiver LOW

Variáveis compartilhadas com ISR devem ser volatile. Quando uma variável é modificada tanto no loop() quanto dentro de uma ISR, o compilador pode otimizá-la incorretamente — assumindo que ela não muda em determinado momento. O qualificador volatile instrui o compilador a sempre ler o valor da memória, nunca de um registrador em cache. É o mesmo conceito de variável volátil em sistemas concorrentes em C.

/*
 * Contador de pulsos com interrupção
 * Botão no pino 2 (com pull-up interno)
 * Cada clique incrementa o contador — sem perder nenhum evento
 */

const int PINO_BTN = 2;   // pino 2 suporta interrupção no Uno
const int PINO_LED = 8;

// volatile: avisa ao compilador que esta variável pode mudar
// a qualquer momento (pela ISR), nunca a otimize em cache
volatile int contadorPulsos = 0;

// ISR — deve ser CURTA e RÁPIDA
// Não use delay(), Serial ou operações lentas aqui
void IRAM_ATTR contarPulso() {
    contadorPulsos++;
}

void setup() {
    pinMode(PINO_BTN, INPUT_PULLUP);
    pinMode(PINO_LED, OUTPUT);
    Serial.begin(9600);

    // Registra a ISR: chama contarPulso() na borda de descida do pino 2
    attachInterrupt(digitalPinToInterrupt(PINO_BTN), contarPulso, FALLING);

    Serial.println("Pressione o botão — cada clique conta!");
}

void loop() {
    // loop pode fazer outras coisas — a contagem acontece na interrupção
    static int ultimoContador = 0;

    if (contadorPulsos != ultimoContador) {
        ultimoContador = contadorPulsos;
        Serial.print("Pulsos: ");
        Serial.println(contadorPulsos);
        digitalWrite(PINO_LED, !digitalRead(PINO_LED));  // toggle
    }
}

Pressione o botão — cada clique conta!

Pulsos: 1

Pulsos: 2

Pulsos: 3

...

Aspecto Leitura no loop() Interrupção (attachInterrupt)
Pode perder eventos rápidos? Sim — se o loop estiver ocupado Não — o chip para tudo para atender
Pinos disponíveis no Uno Todos (D0–D13) Apenas pinos 2 e 3
Pode usar delay() dentro? Sim Não — ISR deve ser curta e rápida
Variáveis compartilhadas Normal Devem ser volatile
Uso típico Leituras periódicas, controle de fluxo Encoder de motor, sensor de fluxo, botão de emergência

Regra de ouro da ISR: faça o mínimo possível dentro da rotina de interrupção — geralmente só incrementar um contador ou setar um flag volatile bool. Toda a lógica de processamento vai no loop(), que lê esses valores e age sobre eles.

Manipulação de bits no hardware

Você já conhece os operadores bit a bit do C — &, |, ^, ~, <<, >> — das aulas de operadores. No Arduino, esses operadores são usados o tempo todo para interagir com o hardware diretamente, especialmente em dois contextos:

1. Verificar, setar e limpar bits de status

Sensores e módulos frequentemente retornam um byte onde cada bit representa um estado diferente — como um campo de flags em C. Para ler ou modificar um bit específico sem afetar os outros, usam-se as mesmas técnicas que você já conhece:

// Suponha que um sensor retorna um byte de status:
// bit 0 = temperatura OK
// bit 1 = umidade OK
// bit 2 = alarme ativo
// bit 3 = bateria baixa

byte status = lerSensor();  // ex: 0b00000101 = temp OK + alarme ativo

// Verificar se bit N está ligado: AND com máscara
if (status & (1 << 0)) Serial.println("Temperatura OK");
if (status & (1 << 2)) Serial.println("ALARME ATIVO!");

// Setar bit N (ligar): OR com máscara
status = status | (1 << 1);   // liga bit 1 (umidade OK)

// Limpar bit N (desligar): AND com NOT da máscara
status = status & ~(1 << 2);  // desliga bit 2 (limpa alarme)

// Alternar bit N (toggle): XOR com máscara
status = status ^ (1 << 3);   // inverte bit 3 (bateria baixa)

2. Port Manipulation — acesso direto aos registradores

As funções digitalWrite() e digitalRead() são convenientes, mas executam dezenas de instruções internamente (verificações de validade, conversões). Para situações onde a velocidade é crítica — como gerar pulsos em alta frequência para controlar um motor de passo — você pode escrever diretamente nos registradores do chip:

// Port Manipulation — equivalente a digitalWrite() mas ~50x mais rápido
//
// No Arduino Uno, os pinos digitais são agrupados em "portas":
//   Porta B = pinos 8 a 13   (registrador PORTB)
//   Porta C = pinos A0 a A5  (registrador PORTC)
//   Porta D = pinos 0 a 7    (registrador PORTD)
//
// DDRB  = Data Direction Register B (1 = OUTPUT, 0 = INPUT)
// PORTB = escreve nos pinos da porta B
// PINB  = lê os pinos da porta B

void setup() {
    DDRB |= (1 << PB0);    // pino 8 como OUTPUT (bit 0 da porta B)
                            // equivale a: pinMode(8, OUTPUT)
}

void loop() {
    PORTB |=  (1 << PB0);  // liga pino 8  — digitalWrite(8, HIGH)
    PORTB &= ~(1 << PB0);  // desliga pino 8 — digitalWrite(8, LOW)
}

Quando usar Port Manipulation? Para o robô e projetos das próximas aulas, digitalWrite() é mais do que suficiente — o overhead é de microssegundos e não afeta motores ou LEDs. Port manipulation fica reservado para aplicações de alta frequência como geração de sinal de áudio, comunicação SPI manual ou motores de passo em velocidades altas. Entender a existência é importante; usar em produção é situacional.

Operação digitalWrite() / digitalRead() Port Manipulation
Velocidade ~6 µs por chamada ~0,125 µs — ~50x mais rápido
Legibilidade Alta — nome do pino claro Baixa — requer conhecer a porta do chip
Portabilidade Funciona em qualquer placa Arduino Específico do chip AVR — não funciona em ESP32, Mega etc.
Erro humano Difícil de danificar hardware Um bit errado pode configurar pinos incorretamente
Quando usar Quase sempre Apenas quando velocidade é crítica

Montando no Wokwi passo a passo

Use este roteiro para montar o circuito do Blink e testar o controle por Serial no Wokwi agora mesmo:

  1. Acesse wokwi.com → New Project → Arduino Uno
  2. Clique no "+" e adicione um LED
  3. Clique no "+" e adicione um Resistor — na propriedade value coloque 220
  4. Conecte: pino 8 → Resistor → ânodo LED (+) → cátodo LED (−) → GND
  5. Cole o código de controle por Serial da Seção 6 na aba sketch.ino
  6. Clique em ▶ Play
  7. Abra o Monitor Serial e digite L — o LED deve acender. Digite D — deve apagar

Testando interrupções no Wokwi: adicione um segundo componente — um botão (Button) — conectado ao pino 2 e ao GND. Cole o código da Seção 7 e observe: cada clique no botão incrementa o contador no Monitor Serial, mesmo que o loop() esteja ocupado com outra tarefa.

Salve o link do projeto! O Wokwi gera uma URL única para cada projeto. Copie e guarde — você vai retornar a estes projetos nas aulas seguintes para adicionar novos componentes. Pense no projeto como um repositório que cresce junto com o curso.

Quiz — Aula 41b

Teste o que você aprendeu sobre o Wokwi, comunicação serial, interrupções e bits:

1. Qual é a principal vantagem de usar o Wokwi antes do hardware físico?

2. O que Serial.available() retorna?

3. Por que variáveis compartilhadas entre o loop() e uma ISR devem ser declaradas como volatile?

4. Quais pinos do Arduino Uno suportam interrupções externas com attachInterrupt()?

5. O que faz a operação status & (1 << 2)?

6. Qual é a regra de ouro sobre o que colocar dentro de uma ISR?

7. Qual expressão limpa (zera) o bit 3 de uma variável byte flags sem afetar os outros bits?

8. Quando Port Manipulation (acesso direto a PORTB, DDRB) é preferível a digitalWrite()?