Robótica com C — Bloco 1
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.
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 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.
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
...
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.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.
volatileNo 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.
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:
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)
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 |
Use este roteiro para montar o circuito do Blink e testar o controle por Serial no Wokwi agora mesmo:
220L — o LED deve acender. Digite D — deve apagarTestando 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.
Teste o que você aprendeu sobre o Wokwi, comunicação serial, interrupções e bits: