Efetuar a programação de microcontroladores (UC00670)
- Introdução
- 1. O microcontrolador: fundamentos e plataformas
- 2. Arquitetura interna e memória
- 3. Conjunto de instruções e modos de endereçamento
- 4. Ambiente de desenvolvimento e configuração
- 5. Fluxogramas: simbologia e técnicas de elaboração
- 6. Estrutura de um programa: funções, variáveis, tipos e operadores
- 7. Entradas e saídas digitais
- 8. Entradas analógicas, ADC e PWM
- 9. Interligação de sensores
- 10. Atuadores e ligação de potência
- 11. Interrupções e temporização
- 12. Comunicação série e protocolos
- 13. Ensaio e depuração
- 14. Projeto integrado: ler, decidir, atuar
- Erros comuns
- Glossário
- Síntese
- Exercícios resolvidos
- Apêndices
Há um pacote premium desta unidade: um PowerPoint editável de 100 ou mais diapositivos, um manual em PDF e dois testes com correção. Esta sebenta continua gratuita.
- Introdução
- 1. O microcontrolador: fundamentos e plataformas
- 2. Arquitetura interna e memória
- 3. Conjunto de instruções e modos de endereçamento
- 4. Ambiente de desenvolvimento e configuração
- 5. Fluxogramas: simbologia e técnicas de elaboração
- 6. Estrutura de um programa: funções, variáveis, tipos e operadores
- 7. Entradas e saídas digitais
- 8. Entradas analógicas, ADC e PWM
- 9. Interligação de sensores
- 10. Atuadores e ligação de potência
- 11. Interrupções e temporização
- 12. Comunicação série e protocolos
- 13. Ensaio e depuração
- 14. Projeto integrado: ler, decidir, atuar
- Erros comuns
- Glossário
- Síntese
- Exercícios resolvidos
- Apêndices
Introdução
Esta sebenta ensina a programar microcontroladores e a ligar o código ao mundo físico: ler um botão, medir uma temperatura, acender um LED, mover um servo, ligar uma bomba de rega. O microcontrolador está dentro de quase tudo o que tem eletrónica: máquinas de lavar, comandos de garagem, contadores de energia, impressoras, automóveis, quadros de automação industrial. Quem instala, repara ou programa estes equipamentos precisa de saber ler um esquema, ler um programa e perceber onde o sinal elétrico se transforma em decisão.
Usamos o Arduino UNO (microcontrolador ATmega328P) como plataforma de referência, porque tem documentação abundante, simuladores gratuitos e um ambiente de desenvolvimento simples. Tudo o que aprendes (entradas, saídas, temporização, interrupções, protocolos, depuração) transfere-se para ESP32, STM32, Raspberry Pi Pico e outros: muda a folha de características, não a lógica.
O percurso segue as três realizações do referencial desta unidade:
- Escrever as instruções em código de programação de microcontroladores (capítulos 1 a 6).
- Interligar dispositivos externos com o microcontrolador: botões, sensores, motores, relés (capítulos 7 a 12).
- Ensaiar e depurar o programa e o funcionamento do circuito (capítulos 13 e 14).
Como estudar. Em cada capítulo há código que podes copiar para o simulador Wokwi ou Tinkercad Circuits e correr sem hardware. Lê o código, prevê o que vai acontecer, só depois corre. Quando a previsão falhar, aprendeste alguma coisa.
Segurança. Nesta unidade trabalhas com baixa tensão (5 V e 3,3 V). Qualquer ligação à rede de 230 V, por exemplo comandar uma lâmpada através de um relé, só se faz com o circuito sem tensão, com módulos preparados para essa tensão e, na prática profissional, por quem tem habilitação para instalações elétricas. Nas aulas, ensaia a carga com uma lâmpada de baixa tensão ou um LED.
1. O microcontrolador: fundamentos e plataformas
1.1 Microcontrolador e microprocessador
Um microprocessador (µP) é só a unidade central de processamento: precisa de memória RAM, de armazenamento e de circuitos de entrada e saída montados à sua volta. É o que existe num computador pessoal. Um microcontrolador (µC) é um computador completo num único circuito integrado: CPU, memória de programa, memória de dados, temporizadores, conversor analógico-digital, portas de comunicação e pinos de entrada e saída.
| Microcontrolador (µC) | Microprocessador (µP) |
|---|---|
| Integra CPU, RAM, Flash e periféricos num único chip | Apenas CPU; RAM, armazenamento e I/O são externos |
| Tarefa dedicada (controlar um sistema) | Uso geral (PC, servidor, telemóvel) |
| Baixo consumo, baixo custo, arranque imediato | Alto desempenho, mais consumo, arranque lento |
| Corre um programa (firmware), muitas vezes sem sistema operativo | Corre um sistema operativo com muitos programas |
| Arduino, ESP32, PIC, STM32, RP2040 | Intel, AMD e os ARM dos computadores |
O programa de um microcontrolador chama-se firmware: fica gravado na memória Flash, sobrevive a cortes de energia e começa a correr assim que o circuito é alimentado. Não há ecrã, teclado nem ficheiros por omissão; o programador tem de decidir como o sistema comunica com o exterior.
1.2 Famílias e plataformas
| Plataforma | Núcleo | Lógica | Pontos fortes |
|---|---|---|---|
| Arduino UNO R3 (ATmega328P) | AVR 8 bits, 16 MHz | 5 V | Didática, ecossistema enorme, robusta |
| Arduino Mega 2560 (ATmega2560) | AVR 8 bits, 16 MHz | 5 V | Muitos pinos e mais memória (256 KB de Flash, 8 KB de SRAM) |
| ESP32 | Xtensa de 32 bits, até 240 MHz | 3,3 V | Wi-Fi e Bluetooth integrados: IoT |
| STM32 | ARM Cortex-M de 32 bits | 3,3 V | Uso profissional, muitos periféricos |
| Raspberry Pi Pico (RP2040) | 2 núcleos ARM Cortex-M0+ | 3,3 V | Barata, programável em C/C++ e MicroPython |
| PIC (Microchip) | 8 a 32 bits | 3,3 V ou 5 V | Muito usada na indústria |
Os valores exatos (frequência, memória, corrente por pino) estão na folha de características (datasheet) de cada componente e variam de versão para versão. Antes de ligar qualquer coisa, confirma-os no documento do fabricante.
1.3 Como escolher um microcontrolador
A escolha parte dos requisitos, não da moda. Perguntas a fazer, por ordem:
- Quantas entradas e saídas são precisas, e de que tipo (digitais, analógicas, PWM)?
- Que comunicação é necessária (série, I²C, SPI, Wi-Fi, Bluetooth)?
- Qual a tensão dos sensores e atuadores? Misturar 5 V com 3,3 V exige adaptação de níveis.
- Quanta memória de programa e de dados? Processar imagens ou áudio não cabe em 2 KB de SRAM.
- Quanto consome e como é alimentado (rede, pilha, bateria solar)?
- Quanto custa e quanto tempo se demora a desenvolver (bibliotecas, apoio, ferramentas)?
Exemplo resolvido · Escolher a placa para três projetos
| Projeto | Requisitos | Escolha | Justificação |
|---|---|---|---|
| Termóstato de sala com ecrã | 1 sensor, 1 relé, 1 ecrã I²C | Arduino UNO | Poucos pinos, tudo a 5 V, simples |
| Sonda de humidade do solo que envia dados para a nuvem | 1 sensor, Wi-Fi, bateria | ESP32 | Wi-Fi integrado, modos de baixo consumo |
| Painel com 40 LED e 12 botões | 52 pinos | Arduino Mega 2560 | Número de pinos que o UNO não tem |
1.4 Especificar as funções e os requisitos do sistema
Antes de escrever uma linha de código, escreve o que o sistema tem de fazer. Uma especificação útil tem três partes: entradas (o que o sistema lê), saídas (o que comanda) e regras (a ligação entre elas, com números).
Exemplo para uma rega automática:
- Entrada: sensor de humidade do solo (analógico, A0).
- Saída: relé que liga a bomba (pino 7); LED de estado (pino 13).
- Regras: se a humidade estiver abaixo de 30 %, liga a bomba durante 10 s; nunca mais de uma rega por hora; se o sensor der leitura fora da gama, desliga a bomba e sinaliza erro.
Uma regra sem número ("regar quando estiver seco") não se programa. "Abaixo de 30 %" já se programa e já se testa.
2. Arquitetura interna e memória
2.1 Diagrama de blocos do ATmega328P
O diagrama de blocos mostra o que existe dentro do chip e como se liga. Simplificado:
+-----------------------------------------------+
Flash | Contador de programa --> Descodificador |
(programa)-->| | |
32 KB | 32 registos +-----v-----+ |
| R0 a R31 <-------> | ALU | |
SRAM <---->| | +-----------+ |
2 KB | Registo de estado SREG |
EEPROM +------------+----------------------------------+
1 KB | barramento de dados (8 bits)
+---------+--------+---------+----------+-----------+
| Portas | Timers | ADC | UART | I2C / SPI |
| B, C, D | 0,1,2 | 10 bits | USART0 | TWI / SPI |
+---------+--------+---------+----------+-----------+
| | | | |
pinos PWM entradas TX/RX SDA/SCL
Pontos a reter:
- Arquitetura Harvard. O programa (Flash) e os dados (SRAM) têm barramentos separados, o que permite buscar uma instrução enquanto se acede a um dado. Nos AVR a maior parte das instruções executa-se num ciclo de relógio.
- CPU de 8 bits. Os registos e o barramento de dados têm 8 bits: uma variável
int(16 bits) obriga a duas operações. - 32 registos de uso geral (R0 a R31), ligados diretamente à ALU. Trabalhar com registos é muito mais rápido do que com a memória.
- Periféricos (portas, temporizadores, ADC, UART, I²C, SPI) são circuitos independentes da CPU, configurados por registos de funções especiais.
2.2 Unidade lógica e aritmética (ALU)
A ALU realiza as operações aritméticas (soma, subtração) e lógicas (E, OU, OU exclusivo, negação, deslocamentos) sobre os registos. Cada operação atualiza o registo de estado SREG, cujos bits (flags) permitem à CPU tomar decisões.
| Bit | Nome | Significado |
|---|---|---|
| I | Interrupt enable | Interrupções globais ligadas |
| T | Bit copy storage | Bit auxiliar |
| H | Half carry | Transporte do bit 3 para o bit 4 |
| S | Sign | N exclusivo-OU V |
| V | Overflow | Excesso em complemento para dois |
| N | Negative | Resultado negativo |
| Z | Zero | Resultado igual a zero |
| C | Carry | Transporte (ou borrow) |
Exemplo resolvido · Somar dois valores de 8 bits e ver as flags
Somar 200 com 100 num registo de 8 bits (0 a 255):
- 200 em binário:
1100 1000. 100 em binário:0110 0100. - Soma bit a bit:
1 0010 1100, ou seja, 9 bits. - O registo guarda só os 8 bits menos significativos:
0010 1100= 44. - O nono bit foi para a flag C (carry = 1).
Conclusão: o resultado guardado é 44, não 300, e a flag C avisa que houve excesso. Num programa em C, um uint8_t com 200 + 100 também dá 44. É por isso que se escolhe o tipo de dados com cuidado (capítulo 6).
2.3 Memória de programa e memória de dados
O ATmega328P tem três memórias com funções diferentes:
| Memória | Tamanho | Volátil? | Para que serve |
|---|---|---|---|
| Flash | 32 KB (cerca de 0,5 KB ocupados pelo carregador de arranque) | Não | Guarda o programa (e constantes marcadas com PROGMEM) |
| SRAM | 2 KB | Sim | Variáveis, pilha e memória dinâmica durante a execução |
| EEPROM | 1 KB | Não | Dados que têm de sobreviver a desligar (calibrações, contadores) |
A EEPROM tem um número limitado de escritas (a folha de características indica cerca de 100 000 ciclos por célula), por isso nunca se escreve nela dentro de um ciclo que repete milhares de vezes por segundo.
A SRAM é a memória mais escassa e a que mais engana. Divide-se em três zonas: variáveis globais e estáticas (tamanho fixo), heap (memória dinâmica, cresce para cima) e pilha (stack, guarda chamadas de funções e variáveis locais, cresce para baixo). Se a pilha e o heap se encontram, o programa comporta-se de forma imprevisível, sem mensagem de erro.
Mapa do espaço de dados (ATmega328P):
| Endereço | Conteúdo |
|---|---|
| 0x0000 a 0x001F | 32 registos de uso geral |
| 0x0020 a 0x005F | 64 registos de I/O (PORTx, DDRx, PINx, SREG...) |
| 0x0060 a 0x00FF | 160 registos de I/O alargados (timers, ADC, UART...) |
| 0x0100 a 0x08FF | 2048 bytes de SRAM |
Exemplo resolvido · Ler o relatório de compilação
Depois de compilar, o IDE mostra algo como:
O sketch usa 4120 bytes (12%) do espaço de armazenamento de programa. O máximo é 32256 bytes.
As variáveis globais usam 410 bytes (20%) de memória dinâmica, deixando 1638 bytes para variáveis locais. O máximo é 2048 bytes.
Leitura: 4120 de 32256 são 12,8 %, por isso o IDE mostra 12 %. Há muita Flash livre. Das variáveis globais, 410 de 2048 são 20 %, restam 1638 bytes para a pilha e o heap. Se a segunda percentagem passar de cerca de 75 %, o risco de falta de SRAM é real: reduzir String, usar F() para textos e arrays com o menor tipo possível.
(Os números variam com o programa e a versão do IDE; o que importa é saber interpretar as duas linhas.)
2.4 Pinagem do Arduino UNO
O UNO tem 14 pinos digitais (D0 a D13) e 6 entradas analógicas (A0 a A5, que também funcionam como digitais). Cada pino tem várias funções:
| Pino | Função(ões) especial(is) |
|---|---|
| D0 (RX), D1 (TX) | Porta série (partilhada com a ligação USB: evitar nestes pinos durante o carregamento) |
| D2, D3 | Interrupções externas INT0 e INT1 |
| D3, D5, D6, D9, D10, D11 | Saídas PWM |
| D10 a D13 | SPI: SS, MOSI, MISO, SCK |
| A4, A5 | I²C: SDA e SCL |
| D13 | LED integrado na placa |
Cada pino pertence a uma porta do microcontrolador: D0 a D7 formam a porta D, D8 a D13 a porta B, A0 a A5 a porta C. Esta correspondência é essencial para a manipulação direta de registos (capítulo 3).
Limites elétricos (valores típicos; confirmar na folha de características do ATmega328P e na documentação da placa):
- Tensão de funcionamento 5 V; um nível alto é reconhecido a partir de cerca de 0,6 × Vcc (3 V) e um nível baixo até cerca de 0,3 × Vcc (1,5 V).
- Corrente por pino: recomendada até cerca de 20 mA, máximo absoluto 40 mA.
- Há também um limite para a corrente total do chip: somar o consumo de todos os pinos e confirmar que não o ultrapassa.
ESP32 e restantes placas de 3,3 V. Os pinos não toleram 5 V. Ligar a saída de um módulo de 5 V diretamente a um pino de 3,3 V pode destruir o pino. Usa um divisor resistivo (por exemplo 1 kΩ em série e 2 kΩ para a massa: 5 V × 2/3 ≈ 3,3 V) ou um conversor de níveis.
3. Conjunto de instruções e modos de endereçamento
3.1 Linguagens: máquina, assembly e C
O processador só executa código máquina. O assembly dá nomes legíveis (mnemónicas) a essas instruções; o compilador avr-gcc traduz o C/C++ do sketch para código máquina. Conhecer o assembly serve para perceber o que o processador faz e porque é que certas linhas de C são lentas ou ocupam muita memória.
3.2 Tipos de instruções
O conjunto de instruções dos AVR é do tipo RISC (conjunto reduzido, cerca de 130 instruções, quase todas num ou dois ciclos). Agrupam-se em:
| Tipo | Exemplos | O que fazem |
|---|---|---|
| Transferência de dados | LDI, MOV, LD, ST, IN, OUT, PUSH, POP |
Movem dados entre registos, memória e I/O |
| Aritméticas | ADD, ADC, SUB, INC, DEC, MUL |
Somam, subtraem, incrementam |
| Lógicas | AND, OR, EOR, COM, LSL, LSR |
Operações bit a bit e deslocamentos |
| Operações sobre bits | SBI, CBI, SBRS, SBRC |
Põem a 1/0 ou testam um bit |
| Comparação e salto | CP, CPI, BREQ, BRNE, RJMP, RCALL, RET |
Comparam e alteram o fluxo do programa |
| Controlo | NOP, SEI, CLI, SLEEP |
Ativam interrupções, adormecem a CPU |
3.3 Modos de endereçamento
O modo de endereçamento diz onde a instrução vai buscar ou guardar o operando.
| Modo | Descrição | Exemplo |
|---|---|---|
| Registo direto | O operando está num registo | MOV R16, R17 |
| Imediato | O valor vem na própria instrução | LDI R16, 0x05 |
| Direto à memória de dados | O endereço vem na instrução | LDS R16, 0x0100 |
| Indireto | O endereço está num par de registos (X, Y, Z) | LD R16, X |
| Indireto com pré-decremento ou pós-incremento | O ponteiro atualiza-se sozinho | LD R16, X+ |
| Indireto com deslocamento | Ponteiro mais um deslocamento fixo | LDD R16, Y+5 |
| Memória de programa | Lê constantes da Flash | LPM R16, Z |
| Relativo | Salto em relação ao contador de programa | RJMP etiqueta |
Exemplo resolvido · Somar 5 e 3 em assembly AVR
LDI R16, 5 ; R16 = 5 (imediato)
LDI R17, 3 ; R17 = 3
ADD R16, R17 ; R16 = R16 + R17 = 8 (registo direto)
Resultado: R16 = 8, flag Z = 0 (não deu zero), flag C = 0 (não houve excesso).
3.4 Registos de entrada e saída: DDR, PORT e PIN
Cada porta tem três registos de 8 bits, um bit por pino:
| Registo | Função |
|---|---|
| DDRx | Direção: 1 = saída, 0 = entrada |
| PORTx | Saída: nível do pino (se saída). Se entrada: 1 liga a resistência de pull-up |
| PINx | Leitura do nível atual do pino |
O que pinMode, digitalWrite e digitalRead fazem por baixo é escrever e ler estes registos. O pino 13 do UNO é o bit 5 da porta B (PB5).
// Equivalente a pinMode(13, OUTPUT); digitalWrite(13, HIGH);
void setup() {
DDRB |= (1 << DDB5); // PB5 como saída
PORTB |= (1 << PORTB5); // PB5 a 1 (HIGH)
}
void loop() {
PORTB ^= (1 << PORTB5); // inverte PB5
delay(500);
}
3.5 Operações bit a bit
Para mexer num bit sem alterar os outros usam-se máscaras:
| Objetivo | Expressão | Exemplo (bit 5) |
|---|---|---|
| Pôr a 1 | reg \|= (1 << n) |
PORTB \|= (1 << 5) |
| Pôr a 0 | reg &= ~(1 << n) |
PORTB &= ~(1 << 5) |
| Inverter | reg ^= (1 << n) |
PORTB ^= (1 << 5) |
| Testar | reg & (1 << n) |
PINB & (1 << 5) |
Exemplo resolvido · Pôr a 1 os bits 2 e 5 de uma porta
Estado inicial de PORTB = 0b0100 0001 (bits 0 e 6 a 1).
- Máscara:
(1 << 2) | (1 << 5)=0b0000 0100 | 0b0010 0000=0b0010 0100. PORTB |= máscara:0100 0001OU0010 0100=0110 0101.
Resultado 0b0110 0101 = 0x65: os bits 0 e 6 mantiveram-se, os bits 2 e 5 ficaram a 1.
Para limpar o bit 6 depois: PORTB &= ~(1 << 6): ~(0100 0000) = 1011 1111; 0110 0101 E 1011 1111 = 0010 0101.
Em hexadecimal, cada algarismo representa 4 bits: 0b1011 0100 = 0xB4 = 180 (128 + 32 + 16 + 4).
4. Ambiente de desenvolvimento e configuração
4.1 O que é um IDE
Um ambiente de desenvolvimento integrado (IDE) reúne num só programa o editor de código, o compilador, o carregador (uploader) e as ferramentas de monitorização. Para Arduino:
| Ferramenta | Características |
|---|---|
| Arduino IDE 2 | Oficial, simples, editor com realce, gestor de placas e bibliotecas, monitor série |
| PlatformIO (extensão do VS Code) | Profissional, vários microcontroladores, controlo de versões, depuração, ficheiro platformio.ini |
| Simuladores online (Wokwi, Tinkercad Circuits) | Corre o código num microcontrolador virtual, sem hardware |
4.2 Instalar e configurar a plataforma, passo a passo
- Instalar o IDE a partir do site oficial e, se for outra placa (ESP32, RP2040), o suporte da placa. O UNO já vem com o IDE; as restantes instalam-se no Gestor de Placas.
- Ligar a placa por USB. Se o sistema não a reconhecer, instalar o driver do conversor USB-série (nos clones costuma ser o CH340).
- Escolher a placa (
Ferramentas > Placa) e a porta (Ferramentas > Porta). Estas duas escolhas erradas explicam a maioria dos "não carrega". - Testar com o exemplo Blink (
Ficheiro > Exemplos > 01.Basics > Blink): compilar, carregar, ver o LED a piscar. - Abrir o Monitor Série e definir a velocidade (baud rate) igual à do
Serial.begin.
Procedimento de configuração é um procedimento. Em contexto profissional, regista a versão do IDE, a versão do pacote da placa e as versões das bibliotecas usadas. Um projeto que compila hoje pode deixar de compilar daqui a um ano se as versões mudarem. No PlatformIO isto fica escrito no platformio.ini.
4.3 O ciclo de desenvolvimento
escrever o código
|
v
compilar (verificar) --erro--> corrigir erros de sintaxe
|
v
carregar (upload via USB)
|
v
observar (LED, Monitor Série, multímetro)
|
+--comportamento errado--> depurar --> voltar a escrever
Compilar traduz o código C/C++ em código máquina e deteta erros de sintaxe. Carregar envia o código máquina para a Flash através do carregador de arranque (bootloader) que já está no chip. Cada carregamento apaga o programa anterior.
4.4 Instalar e usar bibliotecas
Uma biblioteca é código já escrito por outros para uma tarefa (ler um DHT22, comandar um servo, falar com um ecrã). Poupa tempo e erros, mas cada biblioteca ocupa memória e tem versões.
Formas de instalar no Arduino IDE:
- Gestor de Bibliotecas (
Sketch > Incluir Biblioteca > Gerir Bibliotecas): pesquisar, escolher a versão, instalar. Recomendado. - Ficheiro .zip (
Sketch > Incluir Biblioteca > Adicionar biblioteca .ZIP). - No PlatformIO: acrescentar a linha
lib_deps = ...aoplatformio.ini.
Depois usa-se com #include <Nome.h> no início do sketch. Bibliotecas que vêm com o IDE: Servo, Wire (I²C), SPI, EEPROM, SoftwareSerial, LiquidCrystal, Stepper.
#include <Servo.h> // biblioteca instalada com o IDE
Servo braco; // objeto da biblioteca
Verifica sempre a compatibilidade. Uma biblioteca escrita para AVR pode não funcionar em ESP32 por usar registos específicos do ATmega328P. Lê a documentação da biblioteca e as placas suportadas antes de a instalar.
4.5 Selecionar o ambiente
Para os primeiros passos e sem hardware, o Wokwi ou o Tinkercad Circuits chegam. Numa aula com Arduino UNO, o Arduino IDE 2. Num projeto com várias placas, repositório Git e depuração, o PlatformIO. A linguagem é determinada pelo microcontrolador: C/C++ é a escolha comum para Arduino; MicroPython é possível em RP2040 e ESP32 (mais fácil de escrever, mais lento e mais pesado em memória).
5. Fluxogramas: simbologia e técnicas de elaboração
5.1 Para que servem
Um fluxograma representa a lógica de um programa por símbolos ligados por setas. Desenha-se antes de programar: obriga a pensar em todos os caminhos possíveis, e é a melhor forma de explicar a um colega (ou a um cliente) o que o sistema faz.
5.2 Simbologia
Os símbolos seguem a norma internacional para fluxogramas (ISO 5807).
| Símbolo | Forma | Uso |
|---|---|---|
| Terminal | Oval | Início e fim |
| Processo | Retângulo | Uma ação: atribuição, cálculo, comando de um atuador |
| Decisão | Losango | Pergunta com resposta sim/não (duas saídas) |
| Entrada/saída | Paralelogramo | Ler um sensor, mostrar um valor |
| Subrotina | Retângulo com barras laterais | Chamada de uma função |
| Conector | Círculo pequeno | Continuação noutro ponto ou página |
| Seta | Linha com ponta | Sentido do fluxo |
5.3 Regras de elaboração
- Um só ponto de início; o fluxo corre de cima para baixo e da esquerda para a direita.
- Cada decisão tem duas saídas rotuladas (Sim/Não).
- Os ciclos fecham-se: a seta volta a um ponto anterior.
- Um passo por símbolo; texto curto, no infinitivo ou como pergunta.
- Em programas de microcontrolador o
loop()é um ciclo infinito: o fluxograma do programa principal não tem "Fim", tem um retorno ao início.
Exemplo resolvido · Fluxograma de um termóstato com histerese
Requisito: ventoinha liga acima de 26 °C e só desliga abaixo de 24 °C.
( Início )
|
[ Configurar pinos e Serial ]
|
v
+------->[ Ler temperatura t ]
| |
| v
| < t > 26 ? >--Sim--> [ Ligar ventoinha ]--+
| | |
| Não |
| v |
| < t < 24 ? >--Sim--> [ Desligar ventoinha ]
| | | |
| Não | |
| v v v
| [ Mostrar t no Monitor Série ]<-----------+
| |
| v
+----[ Esperar 2 s (sem bloquear) ]
A zona entre 24 °C e 26 °C não altera nada: a ventoinha mantém o estado anterior. Isto é a histerese e evita que o relé comute dezenas de vezes por minuto quando a temperatura oscila à volta do limiar.
5.4 Do fluxograma ao código
Cada decisão do fluxograma é um if, cada ciclo um while ou for, cada subrotina uma função void nome() { ... } e cada símbolo de entrada/saída uma chamada como analogRead ou Serial.println.
6. Estrutura de um programa: funções, variáveis, tipos e operadores
6.1 Anatomia de um sketch
Um programa Arduino tem duas funções obrigatórias, chamadas pelo sistema, e zero ou mais funções do programador.
#include <Servo.h> // 1. bibliotecas
const int PINO_LED = 13; // 2. constantes e variáveis globais
int contador = 0;
void piscar(int vezes); // 3. protótipos (opcional)
void setup() { // 4. corre UMA vez
pinMode(PINO_LED, OUTPUT);
Serial.begin(9600);
}
void loop() { // 5. repete para sempre
piscar(3);
delay(2000);
}
void piscar(int vezes) { // 6. funções do programador
for (int i = 0; i < vezes; i++) {
digitalWrite(PINO_LED, HIGH);
delay(200);
digitalWrite(PINO_LED, LOW);
delay(200);
}
}
setup()corre uma vez ao ligar a alimentação ou ao carregar o botão de reset.loop()corre em ciclo infinito. Entre duas voltas não há nada: o próprio programa decide quanto tempo cada volta demora.- Os comentários
//e/* ... */não vão para o microcontrolador. Comentar o porquê, não o óbvio.
6.2 Tipos de dados
Em AVR (Arduino UNO) os tamanhos são:
| Tipo | Bytes | Gama | Uso típico |
|---|---|---|---|
bool |
1 | true ou false |
Estados |
byte / uint8_t |
1 | 0 a 255 | Valores de PWM, registos |
char / int8_t |
1 | -128 a 127 | Carateres, pequenos valores |
int / int16_t |
2 | -32 768 a 32 767 | Contadores, leituras do ADC |
unsigned int / uint16_t |
2 | 0 a 65 535 | Valores positivos |
long / int32_t |
4 | cerca de -2,1 mil milhões a +2,1 mil milhões | Cálculos intermédios |
unsigned long / uint32_t |
4 | 0 a 4 294 967 295 | millis(), micros() |
float |
4 | cerca de 7 algarismos significativos | Valores com vírgula |
No ATmega328P double tem o mesmo tamanho que float (4 bytes). Noutras placas (ESP32) é diferente: o tamanho de int também muda (4 bytes em ARM e ESP32). Para código portável usa os tipos de tamanho fixo (uint8_t, int16_t, uint32_t).
Exemplo resolvido · Excesso (overflow) de um int
Cálculo: converter uma leitura do LM35 (10 mV/°C, referência de 5 V, ADC de 10 bits) para graus Celsius (parte inteira). Fórmula: T(°C) = leitura × 5 / 1024 × 100 = leitura × 500 / 1024.
Versão errada:
int leitura = 66;
int graus = leitura * 500 / 1024; // ERRADO
leitura * 500= 66 × 500 = 33 000.- 33 000 passa o máximo de um
int(32 767) e o resultado "dá a volta": 33 000 - 65 536 = -32 536. - -32 536 / 1024 = -31 (divisão inteira). A temperatura real seria 32 °C.
Versão certa, com um tipo maior:
int leitura = 66;
long graus = (long)leitura * 500 / 1024; // 33000 / 1024 = 32
Conversão explícita para long antes da multiplicação. O erro mais frequente é converter o resultado e não o operando. Para obter décimas de grau (322, ou seja 32,2 °C) a fórmula passa a (long)leitura * 5000 / 1024.
Exemplo resolvido · Divisão inteira
int a = 5, b = 2;
float r1 = a / b; // 2.0 (divisão inteira feita ANTES da conversão)
float r2 = (float)a / b; // 2.5
float r3 = 5 / 2.0; // 2.5 (um dos operandos já é decimal)
Em float volts = leitura * 5 / 1023; com leitura inteiro, o cálculo faz-se em inteiros e perde a parte decimal. Escrever leitura * 5.0 / 1023.0.
6.3 Variáveis: âmbito e duração
| Tipo de variável | Onde se declara | Vive | Notas |
|---|---|---|---|
| Global | Fora de qualquer função | Todo o programa | Ocupa SRAM sempre; partilhada |
| Local | Dentro de uma função | Só durante a função | Perde o valor ao terminar |
static local |
Dentro de uma função, com static |
Todo o programa | Mantém o valor entre chamadas |
const |
Qualquer | Constante | Não ocupa SRAM se o compilador a substituir |
volatile |
Qualquer | Variável que muda "por fora" (interrupção) | Impede o compilador de a guardar em registo (capítulo 11) |
Convenção útil: constantes em MAIÚSCULAS (PINO_LED) e const int em vez de #define.
6.4 Operadores
| Família | Operadores |
|---|---|
| Aritméticos | + - * / % |
| Comparação | == != < > <= >= |
| Lógicos | && \|\| ! |
| Bit a bit | & \| ^ ~ << >> |
| Composição | += -= *= /= \|= &= ^= e ++ -- |
Armadilha clássica: = atribui, == compara. if (x = 5) atribui 5 a x e a condição é sempre verdadeira. Os compiladores avisam; lê os avisos.
6.5 Estruturas de controlo
// Decisão
if (t > 26.0) {
ligar();
} else if (t < 24.0) {
desligar();
}
// Seleção múltipla
switch (estado) {
case 0: /* repouso */ break;
case 1: /* armado */ break;
default: /* inválido */ break;
}
// Ciclos
for (int i = 0; i < 5; i++) { /* 5 vezes */ }
while (digitalRead(BTN) == HIGH) { /* enquanto não premido */ }
Atenção ao while e ao delay() dentro do loop(): um while que espera por um botão bloqueia o programa inteiro. Outras tarefas (ler outro sensor, atualizar um ecrã) ficam paradas. Voltamos a isto nos capítulos 7 e 11.
6.6 Funções e modularidade
Uma função agrupa instruções com um nome, recebe parâmetros e pode devolver um valor.
const int LED_ALERTA = 12; // LED de aviso de calor
float lerTemperatura() { // devolve °C
int leitura = analogRead(A0);
float tensao = leitura * 5.0 / 1024.0;
return tensao * 100.0; // LM35: 10 mV/°C
}
void aplicarControlo(float t) { // não devolve nada
digitalWrite(LED_ALERTA, t > 30.0 ? HIGH : LOW); // LED de aviso de calor
}
Regra de ouro: uma função, uma responsabilidade. lerTemperatura só lê; aplicarControlo só decide; loop só orquestra. Código assim lê-se, testa-se por partes e reutiliza-se.
6.7 Bibliotecas e funções do sistema
Funções que usarás todos os dias, sem #include:
| Função | Para que serve |
|---|---|
pinMode(p, modo) |
Configura o pino como INPUT, OUTPUT ou INPUT_PULLUP |
digitalRead(p) / digitalWrite(p, v) |
Lê e escreve um pino digital |
analogRead(p) / analogWrite(p, v) |
Lê o ADC e escreve PWM |
delay(ms) / delayMicroseconds(us) |
Bloqueia o programa |
millis() / micros() |
Tempo desde o arranque |
map(v, a, b, c, d) |
Converte um valor de um intervalo para outro |
constrain(v, min, max) |
Limita um valor |
Serial.print, Serial.println |
Escrever no Monitor Série |
7. Entradas e saídas digitais
7.1 Saída digital e dimensionamento do LED
pinMode(9, OUTPUT);
digitalWrite(9, HIGH); // pino a 5 V
digitalWrite(9, LOW); // pino a 0 V
Um LED precisa de uma resistência em série que limite a corrente. Com Vcc = 5 V, tensão direta do LED (Vf) e corrente pretendida (I):
R = (Vcc - Vf) / I
Exemplo resolvido · Resistência para um LED vermelho
Dados: Vcc = 5 V, Vf ≈ 2,0 V (valor típico; confirmar na folha de características), I = 15 mA.
- R = (5 - 2,0) / 0,015 = 200 Ω.
- Valor normalizado imediatamente acima: 220 Ω.
- Corrente real: I = 3 / 220 = 13,6 mA.
- Potência dissipada na resistência: P = R × I² = 220 × 0,0136² = 0,041 W, muito abaixo de 0,25 W.
Sem resistência, o LED tenta puxar uma corrente muito acima do limite do pino e a vida útil de ambos fica em risco.
7.2 Entrada digital, pull-up e pull-down
Um pino de entrada sem nada ligado flutua: lê 0 ou 1 ao acaso, captando ruído. É preciso definir o estado de repouso.
| Configuração | Esquema | Repouso | Premido |
|---|---|---|---|
| Pull-up | Botão entre o pino e GND; resistência para Vcc | HIGH | LOW |
| Pull-down | Botão entre o pino e Vcc; resistência para GND | LOW | HIGH |
No Arduino, INPUT_PULLUP liga uma resistência interna (da ordem de dezenas de kΩ, ver folha de características) entre o pino e Vcc. Não é preciso componente externo:
pinMode(2, INPUT_PULLUP);
bool premido = (digitalRead(2) == LOW); // lógica invertida
A lógica fica invertida: premido = LOW. Habitua-te a escrever == LOW para "premido" quando usas pull-up.
7.3 Ressalto (debounce)
Os contactos mecânicos "saltam" durante alguns milissegundos ao fechar. O microcontrolador, muito mais rápido, lê vários toques onde houve um.
Sinal real do botão: HIGH ____ __ _ ________ LOW
\__/ \_| |_/
ressalto (poucos ms)
Debounce por software, bloqueante (simples, só para exemplos):
if (digitalRead(BTN) == LOW) {
delay(20); // espera que estabilize
if (digitalRead(BTN) == LOW) { /* toque válido */ }
}
Debounce não bloqueante, com millis():
const int BTN = 2;
const unsigned long ATRASO = 30; // ms
int ultimoLido = HIGH;
int estavel = HIGH;
unsigned long tMudanca = 0;
bool premiu() { // true uma vez por toque
int lido = digitalRead(BTN);
if (lido != ultimoLido) { // mudou: reinicia a contagem
tMudanca = millis();
ultimoLido = lido;
}
if (millis() - tMudanca > ATRASO && lido != estavel) {
estavel = lido;
if (estavel == LOW) return true; // flanco descendente estável
}
return false;
}
Também se resolve por hardware com um condensador (filtro RC) em paralelo com o botão.
7.4 Detetar o flanco e fazer um interruptor de toque
Um botão que alterna o LED a cada toque tem de reagir à mudança, não ao nível.
Exemplo resolvido · Botão que liga e desliga um LED
Requisito: cada toque inverte o estado do LED (pino 9); botão no pino 2 com pull-up.
const int LED = 9;
const int BTN = 2;
bool ledLigado = false;
int anterior = HIGH;
void setup() {
pinMode(LED, OUTPUT);
pinMode(BTN, INPUT_PULLUP);
}
void loop() {
int atual = digitalRead(BTN);
if (anterior == HIGH && atual == LOW) { // flanco descendente
ledLigado = !ledLigado;
digitalWrite(LED, ledLigado);
delay(30); // debounce simples
}
anterior = atual;
}
Passo a passo: com o botão em repouso, anterior e atual são HIGH, nada acontece. No instante em que atual passa a LOW com anterior ainda HIGH, há um flanco: inverte-se o estado uma só vez. Enquanto o botão se mantém premido, anterior passa a LOW e a condição deixa de se verificar. Sem a variável anterior, o LED piscaria centenas de vezes por segundo enquanto o dedo estivesse no botão.
8. Entradas analógicas, ADC e PWM
8.1 O conversor analógico-digital
O mundo físico é analógico (temperatura, luz, posição). O ADC converte uma tensão numa contagem. O ADC do ATmega328P tem 10 bits: 2¹⁰ = 1024 níveis (0 a 1023) entre 0 V e a tensão de referência.
leitura = tensão / Vref × 1024 (arredondado para baixo)
tensão = leitura × Vref / 1024
Por convenção simples usa-se frequentemente 1023; a diferença (menos de 0,1 %) é desprezável na maioria dos casos.
Referência (analogReference) |
Valor | Uso |
|---|---|---|
DEFAULT |
5 V (tensão de alimentação) | Predefinição |
INTERNAL |
1,1 V interna (valor típico; a folha de características dá 1,0 a 1,2 V) | Sensores de sinal pequeno (LM35): mais resolução |
EXTERNAL |
Tensão no pino AREF | Referência precisa externa |
Resolução é a menor variação distinguível: Vref / 1024. Com 5 V: 4,88 mV. Com 1,1 V: 1,07 mV.
Exemplo resolvido · Resolução de um LM35 com duas referências
O LM35 dá 10 mV por °C.
- Com Vref = 5 V: cada nível do ADC vale 5 / 1024 = 4,88 mV, ou seja 0,49 °C por nível. A 25 °C a tensão é 250 mV e
analogReaddevolve 250 / 4,88 = 51,2, logo 51 (leitura de 24,9 °C). - Com
analogReference(INTERNAL), Vref = 1,1 V: cada nível vale 1,1 / 1024 = 1,07 mV, ou seja 0,107 °C por nível. A 25 °C: 250 / 1,074 = 232,7, logo 232 (leitura de 24,9 °C), mas agora com mais de quatro vezes mais passos por grau.
Conclusão: a mesma medida ganha cerca de 4,5 vezes mais resolução usando a referência de 1,1 V, à custa de só medir até 1,1 V (110 °C no cálculo; confirma na folha de características a gama da variante do LM35 usada).
Os 1,1 V são um valor típico: a folha de características do ATmega328P dá 1,0 a 1,2 V. Por isso os 232 níveis e os 0,107 °C são valores nominais. Para medidas rigorosas mede-se o valor real da referência e calibra-se.
O ADC demora cerca de 104 µs por conversão na configuração predefinida (13 ciclos do relógio do ADC de 125 kHz), cerca de 9600 leituras por segundo. É rápido para a maioria das aplicações, mas não para áudio.
8.2 Ruído e média
O valor do ADC "treme" um ou dois níveis por ruído elétrico. Tratamento comum: média de várias leituras (somar num long para evitar overflow e dividir por n). Com 8 leituras o ruído aleatório reduz-se aproximadamente na raiz quadrada de 8.
8.3 A função map e os seus limites
map(valor, deMin, deMax, paraMin, paraMax) faz uma proporção linear com números inteiros:
map(512, 0, 1023, 0, 255) = (512 - 0) × (255 - 0) / (1023 - 0) + 0 = 127 (divisão inteira).
map não limita o resultado. Se o valor sair do intervalo de origem, sai também do intervalo de destino. Junta-lhe constrain(v, 0, 255) quando a entrada pode exceder o esperado.
8.4 PWM: saída "analógica"
O microcontrolador não tem saídas verdadeiramente analógicas. O PWM (modulação por largura de impulso) liga e desliga o pino muito depressa; a fração do tempo em que fica ligado chama-se duty cycle (ciclo de trabalho).
Duty de 25 % dá uma média de 1,25 V; de 50 %, 2,5 V; de 75 %, 3,75 V.
analogWrite(pino, valor) aceita 0 a 255: duty = valor / 255.
- Pinos PWM do UNO: 3, 5, 6, 9, 10 e 11 (marcados com ~ na placa).
- Frequência: cerca de 490 Hz nos pinos 3, 9, 10 e 11 e cerca de 980 Hz nos pinos 5 e 6.
- Cada pino PWM está associado a um dos três temporizadores (Timer0, Timer1, Timer2). Mudar a configuração de um temporizador afeta os pinos dele; o
Servousa o Timer1 e inutiliza o PWM nos pinos 9 e 10.
Exemplo resolvido · Potenciómetro a regular o brilho de um LED
Potenciómetro em A0, LED com resistência de 220 Ω no pino 6.
const int POT = A0;
const int LED = 6; // pino PWM
void setup() { pinMode(LED, OUTPUT); }
void loop() {
int leitura = analogRead(POT); // 0 a 1023
int brilho = map(leitura, 0, 1023, 0, 255); // 0 a 255
analogWrite(LED, brilho);
}
Verificação com três posições do potenciómetro:
| Posição | leitura |
brilho |
Duty |
|---|---|---|---|
| Mínimo | 0 | 0 | 0 % |
| Meio | 512 | 127 | 49,8 % |
| Máximo | 1023 | 255 | 100 % |
O olho humano não vê linear: a 50 % de duty o LED parece bastante mais brilhante do que "metade". Para comandos de iluminação convém uma curva (por exemplo, quadrática) em vez de uma reta.
PWM serve para brilho de LED, velocidade de motores DC (através de driver), posição de servos e sinais de áudio simples. Não alimenta cargas diretamente: o pino continua limitado a poucos miliamperes (capítulo 10).
9. Interligação de sensores
Um sensor converte uma grandeza física num sinal elétrico. O microcontrolador lê-o de uma de três formas: digital (HIGH/LOW), analógica (via ADC) ou por protocolo (I²C, SPI, 1-Wire, dados temporizados).
9.1 Divisor de tensão: a base dos sensores resistivos
Sensores resistivos (LDR, NTC, potenciómetro) não dão tensão: variam a resistência. O ADC lê tensão, por isso monta-se um divisor de tensão com uma resistência fixa.
Vcc (5 V)
|
[ LDR ] resistência variável
|
+-----------> A0 (ADC)
|
[ 10 kΩ ] resistência fixa
|
GND
Vout = Vcc × Rfixa / (Rfixa + Rsensor)
Exemplo resolvido · LDR em divisor com 10 kΩ
Valores típicos de um LDR: cerca de 1 kΩ com muita luz e 100 kΩ às escuras (confirmar no componente usado).
| Condição | R do LDR | Vout | analogRead |
|---|---|---|---|
| Muita luz | 1 kΩ | 5 × 10 / (10 + 1) = 4,55 V | cerca de 931 |
| Luz normal | 10 kΩ | 5 × 10 / 20 = 2,50 V | 512 |
| Escuro | 100 kΩ | 5 × 10 / 110 = 0,45 V | cerca de 93 |
Nesta montagem mais luz = valor mais alto. Se trocares a posição do LDR e da resistência fixa, inverte-se: mais luz = valor mais baixo. Define o limiar de acender uma luz exterior pelo valor lido, por exemplo if (analogRead(A0) < 200) para "escuro".
9.2 Temperatura: LM35, NTC e DHT22
LM35 (analógico, linear): 10 mV por °C. Cálculo simples e com boa precisão.
float lerLM35() {
int leitura = analogRead(A0);
return leitura * 5.0 / 1024.0 * 100.0; // °C
}
NTC (resistência que diminui com a temperatura, não linear). Monta-se num divisor e converte-se com a equação beta:
1/T = 1/T0 + (1/B) × ln(R/R0) (T em kelvin)
Exemplo resolvido · NTC de 10 kΩ (B = 3950) num divisor com 10 kΩ
NTC ligado a Vcc, resistência fixa de 10 kΩ a GND, saída em A0. Leitura obtida: 754.
- Resistência do NTC: R = Rfixa × (1024 / leitura - 1) = 10 000 × (1024 / 754 - 1) = 10 000 × 0,358 = 3580 Ω.
- Equação beta, com T0 = 298,15 K (25 °C) e R0 = 10 000 Ω:
1/T = 1/298,15 + ln(3580 / 10 000) / 3950ln(0,358) = -1,0275;-1,0275 / 3950 = -0,000260;1/298,15 = 0,0033541/T = 0,003094, logoT = 323,2 K. - T em °C: 323,2 - 273,15 = 50,0 °C.
Em código:
float lerNTC() {
const float R0 = 10000.0, B = 3950.0, T0 = 298.15, RF = 10000.0;
int leitura = analogRead(A0);
if (leitura <= 0 || leitura >= 1023) return NAN; // 0 daria divisão por zero; 0 e 1023 indicam NTC solto ou em curto
float r = RF * (1024.0 / leitura - 1.0);
float tK = 1.0 / (1.0 / T0 + log(r / R0) / B);
return tK - 273.15;
}
O chamador testa o resultado antes de decidir:
float t = lerNTC();
if (isnan(t)) {
Serial.println("NTC com falha"); // e a carga fica no estado seguro
} else {
controlar(t);
}
Os valores R0 e B vêm da folha de características do NTC usado; tolerâncias de 1 a 5 % mudam o resultado.
DHT11 e DHT22 (digitais, temperatura e humidade): protocolo temporizado de um só fio, tratado por uma biblioteca (por exemplo a DHT sensor library). Lê-se no máximo cada poucos segundos (consultar a folha de características: o DHT22 tem cadência de leitura lenta).
#include <DHT.h>
DHT dht(4, DHT22); // pino 4
void setup() { Serial.begin(9600); dht.begin(); }
void loop() {
float h = dht.readHumidity();
float t = dht.readTemperature();
if (isnan(h) || isnan(t)) { // leitura inválida
Serial.println("Falha de leitura");
} else {
Serial.print(t); Serial.print(" C "); Serial.print(h); Serial.println(" %");
}
delay(2000);
}
Testar sempre isnan: um sensor desligado devolve "não é número" e, se o código não o trata, o relé comanda-se com lixo.
9.3 Distância: HC-SR04
O módulo ultrassónico envia um pulso de som e mede o tempo até ao eco.
TRIG: pulso de 10 µs
ECHO: fica HIGH durante o tempo de ida e volta do som
Distância = tempo × velocidade do som / 2 (o som vai e volta). A cerca de 20 °C o som viaja a 343 m/s = 0,0343 cm/µs.
Exemplo resolvido · Calcular a distância a partir do eco
Medimos duracao = 5830 µs.
- Distância total percorrida: 5830 × 0,0343 = 199,97 cm.
- Dividir por dois (ida e volta): 199,97 / 2 = 100 cm.
const int TRIG = 9, ECHO = 10;
void setup() {
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
Serial.begin(9600);
}
float lerDistanciaCm() {
digitalWrite(TRIG, LOW); delayMicroseconds(2);
digitalWrite(TRIG, HIGH); delayMicroseconds(10);
digitalWrite(TRIG, LOW);
unsigned long duracao = pulseIn(ECHO, HIGH, 30000UL); // timeout 30 ms
if (duracao == 0) return -1.0; // sem eco
return duracao * 0.0343 / 2.0;
}
void loop() {
Serial.println(lerDistanciaCm());
delay(100);
}
O argumento de timeout de pulseIn (30 000 µs) evita que o programa fique preso a esperar um eco que não chega. O módulo tem alcance limitado (consultar a folha de características do módulo); superfícies muito inclinadas ou macias devolvem leituras erradas.
9.4 Movimento, gás e outros
O PIR dá HIGH ao detetar movimento e precisa de tempo de estabilização depois de ligado. Os fins de curso e as barreiras óticas são digitais (usar pull-up e debounce). O MQ-x (gás) é analógico, requer aquecimento e calibração e não substitui um detetor certificado. Os sensores de pressão e de humidade do solo são analógicos: ver a gama e a tensão na folha de características e calibrar com dois pontos.
9.5 Calibrar um sensor
Um valor lido só é útil se for calibrado: relacionado com a grandeza real.
- Recolhe dois pontos de referência conhecidos (por exemplo, sensor de humidade do solo seco e dentro de água).
- Regista o valor lido em cada um (
seco = 820,agua = 350). - Converte a leitura em percentagem:
perc = map(leitura, seco, agua, 0, 100)e limita comconstrain(perc, 0, 100). - Compara com um instrumento fiável e anota o desvio.
Mostra primeiro o valor bruto do ADC no Monitor Série e só depois converte: se o bruto já está errado, o problema é de ligação, não de fórmula.
10. Atuadores e ligação de potência
10.1 O limite do pino
Um pino do ATmega328P aguenta, de forma recomendada, cerca de 20 mA (máximo absoluto 40 mA, ver folha de características). Isso chega para um LED com resistência; não chega para um motor, um relé nu, uma fita de LED ou um altifalante. A regra é simples: o pino comanda, não alimenta. A potência vem de uma fonte própria, interrompida por um componente que o pino sabe acionar.
Ordens de grandeza: um LED com resistência pede 10 a 20 mA (ligação direta); uma bobina de relé, dezenas de mA (transístor ou módulo de relé); um motor DC pequeno, centenas de mA (transístor ou ponte H); um motor de passo, de centenas de mA a vários A (driver dedicado); fitas de LED, bombas e ventoinhas de potência, amperes (MOSFET ou relé com fonte própria).
10.2 Transístor como interruptor
Um transístor NPN (de pequeno sinal) comanda uma carga maior: o pino alimenta a base através de uma resistência, o transístor deixa passar a corrente da carga pelo coletor para o emissor (ligado à massa).
+V (fonte da carga)
|
[CARGA] --- díodo em antiparalelo se for indutiva
|
C
pino --[Rb]-- B NPN
E
|
GND (comum com o do Arduino)
Exemplo resolvido · Resistência de base para comandar um relé de 5 V
Dados: bobina do relé com 70 mA (valor de exemplo: confirmar na folha de características do relé), saída do pino a 5 V, queda base-emissor Vbe ≈ 0,7 V, ganho do transístor hFE mínimo dado pela folha de características do transístor usado (confirmar; aqui assumimos 100).
- Para garantir saturação usa-se um ganho "forçado" de cerca de 10: Ib = Ic / 10 = 70 / 10 = 7 mA.
- Rb = (Vpino - Vbe) / Ib = (5 - 0,7) / 0,007 = 614 Ω.
- Valor normalizado: 560 Ω. Ib real = 4,3 / 560 = 7,7 mA, abaixo dos 20 mA recomendados para o pino.
Se Rb fosse demasiado alta (por exemplo 10 kΩ, Ib = 0,43 mA), o transístor não saturava, aquecia e o relé não fechava bem.
10.3 Cargas indutivas e díodo de roda livre
Motores, relés e eletroímãs são indutivos. Ao cortar a corrente, a bobina gera um pico de tensão muito superior à alimentação, que pode destruir o transístor e o microcontrolador. A solução é um díodo de roda livre (flyback) em antiparalelo com a bobina (cátodo para o positivo), por exemplo um 1N4007 ou um 1N4148 consoante a corrente. Os módulos de relé já trazem este díodo; ao montar a mão, é obrigatório.
10.4 Alimentação e massa comum
- A carga tem fonte própria dimensionada para a sua corrente máxima (motores com arranque exigem bem mais do que a corrente nominal).
- As massas (GND) são ligadas entre si: a do Arduino e a da fonte da carga. Sem massa comum o sinal de comando não tem referência e o circuito não funciona.
- Não alimentar motores pelo pino de 5 V da placa USB: o porto USB e o regulador têm pouca margem.
- Quedas de tensão por motores a arrancar podem reiniciar o microcontrolador: separar as alimentações ou acrescentar um condensador de desacoplamento junto à carga.
10.5 LED, buzzer e relé
LED: digitalWrite para ligar/desligar, analogWrite para o brilho.
Buzzer: o ativo tem oscilador interno e liga-se com digitalWrite; o passivo recebe uma frequência com tone().
tone(8, 440); // nota Lá (440 Hz) no pino 8
delay(500);
noTone(8);
Relé: é um interruptor comandado por eletroíman, isolado galvanicamente do circuito de comando. Os módulos habituais têm um transístor e um díodo integrados e, muitas vezes, lógica de ativação invertida (LOW liga). Lê a documentação do módulo.
const int RELE = 7;
void setup() {
digitalWrite(RELE, HIGH); // nível seguro primeiro (módulo invertido: HIGH = desligado)
pinMode(RELE, OUTPUT); // só depois passa a saída
}
Escreve o nível seguro antes de declarar o pino como saída, para o relé não dar um clique de arranque.
Corrente alternada. Os contactos do relé têm tensão e corrente máximas escritas no próprio componente; nunca as ultrapasses. Montagens a 230 V exigem caixa isolada, ligações protegidas e habilitação adequada. Nas aulas, usa a carga de baixa tensão.
10.6 Servomotor
Um servo posiciona-se num ângulo (0 a 180° nos modelos comuns). Recebe um sinal PWM de 50 Hz (período de 20 ms) em que a largura do impulso codifica a posição: cerca de 1 ms numa extremidade e 2 ms na outra. A biblioteca Servo esconde isto.
#include <Servo.h>
Servo s;
void setup() { s.attach(9); }
void loop() {
s.write(0); delay(1000);
s.write(90); delay(1000);
s.write(180); delay(1000);
}
Por omissão, a biblioteca mapeia 0° a 544 µs e 180° a 2400 µs. Para 90° o impulso é 544 + (2400 - 544) / 2 = 1472 µs. Um servo pode consumir centenas de mA ao mover-se: dá-lhe fonte própria, com massa comum.
10.7 Motor DC e ponte H
O motor DC roda com velocidade proporcional à tensão média e inverte o sentido se a polaridade inverter. A ponte H (L298N, L293D, TB6612, entre outros) permite as duas coisas: dois sinais de direção (IN1, IN2) e um sinal PWM de velocidade (ENA).
| IN1 | IN2 | Motor |
|---|---|---|
| LOW | LOW | Parado |
| HIGH | LOW | Sentido 1 |
| LOW | HIGH | Sentido 2 |
| HIGH | HIGH | Travagem (em muitos drivers) |
const int IN1 = 4, IN2 = 5, ENA = 6; // ENA é pino PWM
void motor(int velocidade) { // -255 a +255
digitalWrite(IN1, velocidade > 0);
digitalWrite(IN2, velocidade < 0);
analogWrite(ENA, abs(velocidade));
}
void setup() {
pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT); pinMode(ENA, OUTPUT);
}
void loop() {
motor(150); delay(2000); // avança a ~59 % de duty
motor(0); delay(500);
motor(-150); delay(2000); // recua
motor(0); delay(500);
}
Mudar de sentido de repente é violento para o motor e para o driver: pára primeiro, espera um pouco, inverte.
10.8 Motor de passo
O motor de passo roda em passos discretos e mantém a posição sem sensor. Um motor de 1,8° por passo dá 360 / 1,8 = 200 passos por volta. Os drivers (por exemplo A4988) simplificam o comando para dois sinais: STEP (cada impulso é um passo) e DIR (sentido); suportam micropasso (por exemplo 1/16, o que dá 3200 passos por volta).
Segurança com o driver A4988. O motor de passo não se alimenta pelo Arduino: o driver tem uma alimentação própria para o motor (a tensão admissível está na folha de características do driver) e a alimentação lógica vem do Arduino. Regula o limite de corrente do driver para a corrente do motor usado, segundo a folha de características do driver e do motor, antes de o ligar. As massas do Arduino e da fonte do motor ficam ligadas (massa comum). Nunca ligues nem desligues o motor com o driver alimentado: o pico de tensão pode destruir o driver, e uma ligação solta com a fonte ligada é um risco para ti. Corta a alimentação do motor, liga ou desliga, e só depois voltas a alimentar. Trabalha sempre com a fonte de baixa tensão do motor, nunca com a rede de 230 V.
Exemplo resolvido · Velocidade de um motor de passo
Pretende-se 60 rotações por minuto num motor de 200 passos por volta, em passo completo.
- Rotações por segundo: 60 / 60 = 1 volta/s.
- Passos por segundo: 200 × 1 = 200 passos/s.
- Intervalo entre passos: 1 / 200 = 5 ms.
const int STEP = 3, DIR = 2;
void setup() { pinMode(STEP, OUTPUT); pinMode(DIR, OUTPUT); }
void loop() {
digitalWrite(DIR, HIGH); // sentido 1
for (int i = 0; i < 200; i++) { // uma volta
digitalWrite(STEP, HIGH); delayMicroseconds(10);
digitalWrite(STEP, LOW); delay(5); // 5 ms entre passos
}
delay(1000);
}
Com micropasso 1/16 seriam 3200 passos por volta e um intervalo de 312 µs para a mesma velocidade.
11. Interrupções e temporização
11.1 Sondagem contra interrupção
Há duas formas de reagir a um evento (um botão, um pulso de um sensor):
- Sondagem (polling): o programa pergunta, volta após volta, "já aconteceu?". Simples, mas se o
loop()demora (por causa dedelay()ou de uma leitura lenta) o evento pode ser perdido. - Interrupção: o hardware interrompe a CPU no instante do evento; esta guarda o ponto onde estava, executa uma pequena função (a ISR, interrupt service routine) e retoma o programa.
loop(): ...instrução... ----[evento!]----> ISR() executa -> volta ao loop() no mesmo ponto
11.2 Interrupções externas
No UNO, os pinos 2 (INT0) e 3 (INT1) geram interrupções por hardware. Modos de disparo:
| Modo | Dispara quando |
|---|---|
LOW |
O pino está a LOW |
CHANGE |
O nível muda |
RISING |
Passa de LOW a HIGH |
FALLING |
Passa de HIGH a LOW |
const int BTN = 2;
volatile unsigned int toques = 0; // volatile: alterada na ISR
void contar() { // ISR: curta e sem delay()
toques++;
}
void setup() {
pinMode(BTN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BTN), contar, FALLING);
Serial.begin(9600);
}
void loop() {
noInterrupts(); // leitura atómica de 16 bits
unsigned int copia = toques;
interrupts();
Serial.println(copia);
delay(500);
}
11.3 Regras de ouro das ISR
- Curtas e rápidas: altera uma variável, ativa uma bandeira (flag) e sai. O trabalho pesado faz-se no
loop(). - Sem
delay(); omillis()não avança dentro da ISR;Serial.printdentro da ISR é má ideia (usa interrupções e pode bloquear). - Variáveis partilhadas com o
loop()sãovolatile: dizem ao compilador que o valor pode mudar a qualquer momento e que tem de o ler da memória sempre. - Variáveis maiores do que 8 bits (como
intelong) são lidas em vários passos; uma interrupção a meio dá um valor corrompido. A leitura faz-se com as interrupções desativadas por instantes (noInterrupts()/interrupts()). - O botão com ressalto dispara a ISR várias vezes: faz debounce comparando
millis()com o instante do último evento válido.
11.4 millis() em vez de delay()
delay(5000) pára tudo durante 5 s: botões, sensores, comunicação. A alternativa é guardar o instante da última execução e comparar com o tempo atual.
const unsigned long INTERVALO = 1000; // ms
unsigned long ultimo = 0;
void loop() {
unsigned long agora = millis();
if (agora - ultimo >= INTERVALO) {
ultimo = agora;
// tarefa periódica
}
// aqui continuam a correr as outras tarefas
}
millis() devolve milissegundos desde o arranque num unsigned long e volta a zero ao fim de cerca de 49,7 dias (2³² ms). A subtração agora - ultimo funciona na mesma, porque a aritmética sem sinal "dá a volta" igualmente: se ultimo = 4 294 967 000 e agora = 200, então agora - ultimo = 200 - 4 294 967 000 + 4 294 967 296 = 496 ms. A comparação agora >= ultimo + INTERVALO falharia neste caso. Usa sempre a subtração.
Exemplo resolvido · Duas tarefas a ritmos diferentes sem bloquear
LED a piscar a 500 ms e leitura de temperatura a cada 2 s.
const int LED = 9;
unsigned long tLed = 0, tTemp = 0;
bool estadoLed = false;
void setup() {
pinMode(LED, OUTPUT);
Serial.begin(9600);
}
void loop() {
unsigned long agora = millis();
if (agora - tLed >= 500) { // tarefa 1
tLed = agora;
estadoLed = !estadoLed;
digitalWrite(LED, estadoLed);
}
if (agora - tTemp >= 2000) { // tarefa 2
tTemp = agora;
float t = analogRead(A0) * 5.0 / 1024.0 * 100.0;
Serial.println(t);
}
}
Com delay() seria impossível: ou piscava, ou lia. Este padrão é a base de qualquer programa com várias funções.
11.5 Temporizadores (timers)
O ATmega328P tem três temporizadores: Timer0 (8 bits), Timer1 (16 bits) e Timer2 (8 bits). Cada um conta impulsos do relógio divididos por um prescaler e pode gerar interrupções ao atingir um valor. O Timer0 e o Timer1 usam o prescaler 1, 8, 64, 256 ou 1024; o Timer2 tem ainda 32 e 128 (ver a folha de características). O Timer0 está reservado ao Arduino: transborda cada 1,024 ms (16 MHz / 64 / 256 = 976,56 Hz) e é daí que sai o millis().
Para gerar uma interrupção periódica exata usa-se o modo CTC (Clear Timer on Compare Match): o contador reinicia ao atingir o valor de comparação OCR1A.
OCR1A = f_clk / (prescaler × f_desejada) - 1
Exemplo resolvido · Interrupção de Timer1 uma vez por segundo
Dados: f_clk = 16 MHz, prescaler 256, f_desejada = 1 Hz.
- Frequência de contagem: 16 000 000 / 256 = 62 500 Hz.
- OCR1A = 62 500 / 1 - 1 = 62 499 (cabe em 16 bits, máximo 65 535).
volatile bool passouSegundo = false;
ISR(TIMER1_COMPA_vect) { // vetor de interrupção do Timer1
passouSegundo = true;
}
void setup() {
Serial.begin(9600);
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0; // contador a zero
OCR1A = 62499; // valor de comparação
TCCR1B |= (1 << WGM12); // modo CTC
TCCR1B |= (1 << CS12); // prescaler 256
TIMSK1 |= (1 << OCIE1A); // ativa a interrupção
interrupts();
}
void loop() {
if (passouSegundo) {
passouSegundo = false;
Serial.println("1 s");
}
}
A ISR só ativa uma bandeira; o Serial.println está no loop().
12. Comunicação série e protocolos
12.1 UART e o Monitor Série
A UART é uma comunicação assíncrona com dois fios (TX e RX) e uma velocidade combinada entre as duas partes (baud rate). A trama habitual 8N1 tem 1 bit de início, 8 bits de dados, sem paridade e 1 bit de paragem, ou seja, 10 bits por byte.
repouso (HIGH) start d0 d1 d2 d3 d4 d5 d6 d7 stop repouso
______________ _ _____________________________ ______
\_/
Exemplo resolvido · Quanto demora a enviar uma linha a 9600 baud
A mensagem Valor: 1023 seguida de fim de linha (\r\n) tem 11 + 2 = 13 carateres = 13 bytes.
- Bits por byte na trama 8N1: 10.
- Bits totais: 13 × 10 = 130.
- Tempo: 130 / 9600 = 13,5 ms.
Se o loop() enviar isto a cada 10 ms, a porta não consegue acompanhar e o programa fica parado à espera. Subir a velocidade (115200) ou enviar menos dados resolve.
Funções principais:
Serial.begin(9600); // no setup
Serial.print("T = "); Serial.println(25.4, 1); // 1 casa decimal
if (Serial.available() > 0) { // há bytes recebidos?
char c = Serial.read(); // lê um byte
}
A velocidade do Monitor Série tem de ser igual à do Serial.begin; se vires carateres estranhos, é quase sempre isto.
// Comandar um LED a partir do teclado: 'a' acende, 'p' apaga
void setup() { pinMode(13, OUTPUT); Serial.begin(9600); }
void loop() {
if (Serial.available() > 0) {
char c = Serial.read();
if (c == 'a') digitalWrite(13, HIGH);
if (c == 'p') digitalWrite(13, LOW);
}
}
12.2 I²C
O I²C usa dois fios: SDA (dados) e SCL (relógio), com resistências de pull-up (tipicamente alguns kΩ; muitos módulos já as trazem). Há um mestre (o microcontrolador) e vários escravos, cada um com um endereço de 7 bits. No UNO: SDA = A4, SCL = A5.
Endereços habituais (podem variar com o módulo e a ligação dos pinos de endereço):
| Dispositivo | Endereço comum |
|---|---|
| Ecrã LCD com adaptador PCF8574 | 0x27 ou 0x3F |
| Relógio DS3231 | 0x68 |
| Sensor BME280 | 0x76 ou 0x77 |
Dois dispositivos com o mesmo endereço no mesmo barramento colidem. O programa de varrimento (scanner) descobre os que estão presentes:
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(9600);
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) { // 0 = o dispositivo respondeu
Serial.print("Encontrado em 0x");
Serial.println(addr, HEX);
}
}
}
void loop() {}
12.3 SPI
O SPI é um barramento síncrono e rápido, com quatro fios: MOSI (mestre para escravo), MISO (escravo para mestre), SCK (relógio) e CS/SS (seleção do escravo, um por dispositivo). Usa-se em cartões SD, ecrãs TFT, módulos de rádio. No UNO: MOSI = 11, MISO = 12, SCK = 13, SS = 10.
12.4 1-Wire
Um único fio de dados e vários dispositivos, cada um com identificador próprio. Típico do sensor DS18B20, com pull-up indicado na folha de características.
12.5 Comparação dos protocolos
| Protocolo | Fios | Velocidade | Dispositivos | Uso típico |
|---|---|---|---|---|
| UART | 2 (TX, RX) | Média | Ponto a ponto | Depuração, GPS, módulos |
| I²C | 2 (SDA, SCL) | Média | Muitos (endereços) | Sensores, ecrãs, RTC |
| SPI | 4 + 1 CS por escravo | Alta | Poucos | SD, TFT, rádio |
| 1-Wire | 1 | Baixa | Muitos | Temperatura DS18B20 |
12.6 Wi-Fi e Internet das Coisas
O ESP32 e o ESP8266 têm Wi-Fi integrado. Um nó IoT típico lê um sensor, liga-se à rede e envia os dados para um servidor, por HTTP (pedido e resposta) ou MQTT (publicação e subscrição de mensagens por tópicos, leve e adequada a sensores).
[sensor] -> [ESP32] --Wi-Fi--> [broker MQTT] --> [painel / base de dados]
Cuidados mínimos de segurança: nunca deixar palavras-passe escritas no código partilhado, usar ligação cifrada sempre que o serviço a permita, atualizar o firmware e não expor o dispositivo à Internet sem necessidade.
13. Ensaio e depuração
13.1 Os três tipos de erro
| Tipo | Quando aparece | Sintoma | Exemplo |
|---|---|---|---|
| Sintático | Ao compilar | O IDE recusa e indica linha e mensagem | Falta de ;, nome não declarado |
| Lógico | Ao correr | Compila e corre, mas faz a coisa errada | = em vez de ==; limiar trocado |
| De tempo de execução | Ao correr, por vezes só depois de horas | Reinícios, valores corrompidos, ficar "preso" | Overflow, falta de SRAM, delay que bloqueia, ISR demorada |
Exemplo resolvido · Encontrar e corrigir os três erros
Código com erros propositados (não compila):
const int LED = 13;
int contador = 0
void setup() {
pinMode(LED, OUTPUT);
}
void loop() {
if (contador = 10) {
digitalWrite(LED, HIGH);
}
contador++;
delay(1000);
}
- Erro sintático: falta
;emint contador = 0. O compilador indica o erro na linha seguinte (expected ',' or ';' before 'void'), porque só percebe que falta quando encontra a palavra seguinte. A linha reportada pode estar depois do erro real. - Erro lógico:
contador = 10atribui 10 e é sempre verdadeiro. O LED acende logo na primeira volta e o contador nunca chega a ser incrementado de forma útil. Corrigir paracontador == 10. - Erro de execução: depois de corrigir o erro lógico, surge o terceiro. O
contadoré uminte nunca pára de crescer. Ao fim de 32 767 voltas (cerca de 9 horas, uma volta por segundo) passa a -32 768 e a contagem deixa de fazer sentido. Para um comportamento "depois de 10 s, acender e ficar assim", a contagem deve parar em 10 (if (contador < 10) contador++;).
13.2 Depuração com o Monitor Série
Num microcontrolador não há ecrã nem consola, por isso a ferramenta mais comum é escrever valores no Monitor Série (printf debugging).
- Imprime os valores brutos (leitura do ADC, estado do pino), não apenas os calculados.
- Imprime marcas de fluxo ("entrei na função X", "ramo Y") para confirmar por onde o programa passa.
- Usa o Serial Plotter para ver sinais ao longo do tempo (ruído, oscilação, resposta a um degrau).
- Etiqueta tudo:
Serial.print("T="); Serial.println(t);. - Remove ou desativa as mensagens no fim; ocupam tempo e SRAM.
13.3 Depuradores e pontos de pausa
Um depurador (debugger) controla a execução do programa no próprio microcontrolador. Permite:
- Pontos de pausa (breakpoints): o programa pára numa linha escolhida.
- Execução passo a passo: step over (executa a linha), step into (entra na função), step out (sai da função).
- Inspeção de variáveis (watch) e da pilha de chamadas (call stack).
- Leitura de registos e da memória.
Um depurador de hardware é um aparelho que liga o PC ao microcontrolador por uma interface de depuração: SWD ou JTAG nos ARM (por exemplo um ST-LINK para STM32), debugWIRE ou JTAG em alguns AVR. O ATmega328P do UNO suporta debugWIRE, mas a placa não traz a ligação preparada: exige uma sonda externa (por exemplo um Atmel-ICE) e a alteração de um fuse. Muitos ESP32 e Raspberry Pi Pico dispõem de depuração por JTAG ou SWD com uma sonda de baixo custo. O Arduino IDE 2 e o PlatformIO integram o depurador nas placas que o suportam. Consulta a documentação da placa antes de comprar a sonda.
13.4 Depuração do circuito
Muitos "erros de programa" são erros de ligação. Material: multímetro e, quando disponível, osciloscópio.
| Sintoma | Verificar |
|---|---|
| Nada acontece | Alimentação (5 V na placa?), cabo USB de dados, porta e placa escolhidas |
| Botão lê sempre o mesmo valor | Pull-up, ligação a GND, pino certo |
| Leitura analógica sempre 0 ou 1023 | Sensor ligado ao pino certo, divisor montado, massa comum |
| Leitura analógica a tremer | Fios longos, falta de condensador, alimentação instável |
| Motor não gira / placa reinicia | Fonte própria, corrente suficiente, díodo de roda livre, massa comum |
Método: isola. Testa um periférico de cada vez com um programa mínimo; só juntas quando cada um funciona. Muda uma coisa de cada vez e regista o resultado.
13.5 Plano de ensaios
Um ensaio é uma verificação com resultado previsto. Antes de entregar um sistema, escreve a tabela:
| N.º | Condição de teste | Resultado esperado | Resultado obtido | Aprovado |
|---|---|---|---|---|
| 1 | Temperatura a 20 °C | Relé desligado | ||
| 2 | Temperatura sobe a 27 °C | Relé liga | ||
| 3 | Temperatura desce a 25 °C | Relé continua ligado (histerese) | ||
| 4 | Temperatura desce a 23 °C | Relé desliga | ||
| 5 | Sensor desligado | Relé desliga e mostra erro |
Os casos de fronteira (limiar exato, sensor desligado, arranque com a carga já ligada) são os que mais revelam erros.
14. Projeto integrado: ler, decidir, atuar
14.1 O padrão sensor, decisão, atuador
Quase toda a automação segue o ciclo ler, decidir, atuar (e monitorizar):
+--------+ +-----------+ +----------+
| SENSOR | ---> | DECISÃO | ---> | ATUADOR |
+--------+ | (programa)| +----------+
^ +-----------+ |
| | |
+------ mundo físico <--------------+
|
Monitor Série / ecrã / nuvem
O mundo físico fecha o ciclo: o atuador altera o ambiente, o sensor volta a medi-lo. Um termóstato, uma rega automática, um alarme e uma estufa são variações deste esquema.
14.2 Histerese
Um controlo com um único limiar (liga a 26 °C, desliga a 26 °C) faz o atuador comutar rapidamente quando a medida oscila à volta do valor. A solução é a histerese: dois limiares.
const int RELE = 7; // pino que comanda o módulo de relé
const int RELE_ATIVO = LOW; // módulo ativo a LOW (o mais comum); se for ativo a HIGH, trocar para HIGH
const float LIGA = 26.0, DESLIGA = 24.0;
bool ventoinha = false;
void setup() {
digitalWrite(RELE, !RELE_ATIVO); // nível seguro (desligado) antes de ser saída
pinMode(RELE, OUTPUT);
}
void controlar(float t) {
if (!ventoinha && t > LIGA) ventoinha = true;
if (ventoinha && t < DESLIGA) ventoinha = false;
digitalWrite(RELE, ventoinha ? RELE_ATIVO : !RELE_ATIVO);
}
Lê a documentação do módulo para saber se liga com LOW ou com HIGH e ajusta RELE_ATIVO.
14.3 Máquina de estados
Quando o sistema tem modos (repouso, armado, disparado), uma máquina de estados finita (FSM) torna o código claro: o sistema está sempre num estado, e transições bem definidas levam a outro.
botão PIR deteta
DESARMADO ----------> ARMADO ---------------> DISPARADO
^ ^ |
| botão | botão (desativa) |
+-------------------+--------------------------+
Exemplo resolvido · Alarme com PIR, buzzer e botão
Requisitos: botão (pino 2, pull-up) alterna entre desarmado e armado; em modo armado, o PIR (pino 4) dispara o buzzer (pino 8); o botão desliga o alarme.
enum Estado { DESARMADO, ARMADO, DISPARADO };
Estado estado = DESARMADO;
const int BTN = 2, PIR = 4, BUZZER = 8, LED = 13;
int btnAnterior = HIGH;
void setup() {
pinMode(BTN, INPUT_PULLUP);
pinMode(PIR, INPUT);
pinMode(BUZZER, OUTPUT);
pinMode(LED, OUTPUT);
Serial.begin(9600);
}
bool botaoPremido() { // flanco descendente
int atual = digitalRead(BTN);
bool premiu = (btnAnterior == HIGH && atual == LOW);
btnAnterior = atual;
if (premiu) delay(30); // debounce simples
return premiu;
}
void loop() {
bool botao = botaoPremido();
switch (estado) {
case DESARMADO:
digitalWrite(LED, LOW);
noTone(BUZZER);
if (botao) { estado = ARMADO; Serial.println("ARMADO"); }
break;
case ARMADO:
digitalWrite(LED, HIGH);
if (botao) { estado = DESARMADO; Serial.println("DESARMADO"); }
else if (digitalRead(PIR) == HIGH) {
estado = DISPARADO; Serial.println("DISPARADO");
}
break;
case DISPARADO:
tone(BUZZER, 1000);
if (botao) { estado = DESARMADO; Serial.println("DESARMADO"); }
break;
}
}
Cada case contém: o que fazer neste estado, e a condição de saída. Para acrescentar um novo estado (por exemplo ATRASO de saída) escreve-se um case novo, sem mexer nos outros.
14.4 Guardar dados: EEPROM
Para que um valor sobreviva a um corte de energia (calibração, contador, estado), usa-se a EEPROM. A biblioteca EEPROM tem read, write, update, put e get. Prefere update/put, que só escrevem se o valor mudou e poupam ciclos de escrita.
#include <EEPROM.h>
int limiar;
void setup() {
EEPROM.get(0, limiar); // lê 2 bytes a partir do endereço 0
if (limiar < 0 || limiar > 1023) { // primeira vez: valor inválido
limiar = 512;
EEPROM.put(0, limiar);
}
}
14.5 Estruturar o projeto
Num projeto com mais de 100 linhas: um cabeçalho (descrição, autor, data, versão); constantes nomeadas para pinos e limiares; funções pequenas (lerSensor, decidir, comandar, mostrar); um loop() curto que só as chama; texto na Flash com F("..."); esquema elétrico atualizado (Fritzing); registo dos ensaios; e Git desde o primeiro dia.
Erros comuns
- Entrada a flutuar: pino de entrada sem pull-up nem pull-down lê ruído. Usar
INPUT_PULLUPou resistência externa. - Lógica invertida esquecida: com pull-up o botão premido é LOW; módulos de relé comuns ligam com LOW.
=em vez de==numa condição: atribui e fica sempre verdadeira.- Divisão inteira e overflow:
leitura * 5 / 1023com inteiros perde decimais;leitura * 500numintpassa os 32 767. Usar5.0elong. - Motor ou relé direto no pino: queima o pino. Usar transístor, driver ou módulo de relé, fonte própria e massa comum.
- Sem díodo de roda livre numa carga indutiva montada à mão.
delay()longo que torna o sistema surdo. Usarmillis().- ISR demorada ou sem
volatilenas variáveis partilhadas. - Baud rate diferente no Monitor Série e no
Serial.begin. - Placa ou porta erradas no menu
Ferramentas. - Misturar 5 V com 3,3 V num ESP32 sem adaptação de níveis.
- Tudo no
loop()sem funções, sem constantes nomeadas e sem comentários. - Sensor sem calibração ou sem tratar leituras inválidas.
- Escrever na EEPROM em cada volta do
loop(), esgotando os ciclos de escrita.
Glossário
- ADC: conversor analógico-digital; no UNO tem 10 bits (0 a 1023).
- ALU: unidade lógica e aritmética, executa as operações sobre os registos.
- Baud rate: velocidade da comunicação série em símbolos por segundo.
- Bootloader: pequeno programa na Flash que permite carregar o sketch por USB.
- Breakpoint: ponto de pausa no código, usado na depuração.
- Debounce: tratamento do ressalto dos contactos mecânicos.
- Duty cycle: fração do período em que um sinal PWM está a nível alto.
- EEPROM: memória não volátil para dados que sobrevivem a desligar.
- Firmware: programa gravado na memória do microcontrolador.
- Flash: memória não volátil onde fica o programa.
- GPIO: General Purpose Input/Output, pino de entrada ou saída de uso geral.
- Histerese: diferença entre os limiares de ligar e desligar, que evita comutações rápidas.
- I²C: barramento de dois fios (SDA e SCL) com dispositivos endereçáveis.
- IDE: ambiente de desenvolvimento integrado.
- ISR: rotina de serviço de interrupção, curta e rápida.
- Prescaler: divisor da frequência de relógio de um temporizador.
- Pull-up e pull-down: resistência que define o estado de repouso de uma entrada.
- PWM: modulação por largura de impulso.
- Registo: posição de memória de 8 bits que controla um periférico (DDRx, PORTx, PINx).
- SPI: barramento síncrono rápido com MOSI, MISO, SCK e CS.
- SRAM: memória volátil de dados, variáveis e pilha.
- Sketch: programa Arduino.
- UART: comunicação série assíncrona com TX e RX.
- volatile: qualificador que obriga o compilador a ler a variável da memória sempre.
- µC e µP: microcontrolador e microprocessador.
Síntese
Um microcontrolador é um computador completo num chip: CPU, Flash, SRAM, EEPROM e periféricos ligados por registos. O programa começa com especificação e fluxograma, escreve-se em C/C++ com setup() e loop(), com atenção aos tipos (overflow, divisão inteira) e ao âmbito das variáveis.
As entradas digitais usam pull-up e debounce; as analógicas usam o ADC (10 bits, tensão = leitura × Vref / 1024); a saída "analógica" é PWM. Sensores resistivos leem-se com divisor de tensão e calibram-se. Atuadores de potência nunca vão ao pino: transístor, driver ou relé, fonte própria, massa comum e díodo de roda livre.
Para responder depressa sem bloquear, usam-se millis(), interrupções (ISR curta, volatile) e temporizadores. Os dispositivos comunicam por UART, I²C, SPI ou Wi-Fi. A depuração combina Monitor Série, simulador, multímetro e, quando existe, depurador de hardware com pontos de pausa. O padrão final é sempre ler, decidir, atuar, com plano de ensaios.
Exercícios resolvidos
1. O ADC de 10 bits do UNO devolve 768 com Vref = 5 V. Qual a tensão?
Resolução:
V = 768 × 5 / 1024 = 3,75 V. Em 768/1024 = 75 % da escala, 75 % de 5 V = 3,75 V.
2. Um botão ligado ao pino 2 com INPUT_PULLUP está solto. Qual o valor lido e porquê?
Resolução:
HIGH. A resistência interna puxa o pino para Vcc; só ao premir o botão o pino liga a GND e lêLOW.
3. Calcula a resistência em série para um LED verde (Vf = 2,1 V, 10 mA) a 5 V.
Resolução: R = (5 - 2,1) / 0,010 = 290 Ω. Valor normalizado acima: 330 Ω (corrente real 8,8 mA). Escolher o valor imediatamente acima garante que a corrente não excede a pretendida.
4. analogWrite(9, 64) produz que duty cycle e que tensão média?
Resolução: duty = 64 / 255 = 25,1 %. Tensão média = 0,251 × 5 V = 1,25 V.
5. Um HC-SR04 devolve pulseIn = 2915 µs. Qual a distância?
Resolução: 2915 × 0,0343 / 2 = 49,99, ou seja 50 cm.
6. Prescaler 64 e OCR1A = 24 999 no Timer1 em modo CTC (16 MHz). Qual a frequência da interrupção?
Resolução: frequência de contagem = 16 000 000 / 64 = 250 000 Hz. f = 250 000 / (24 999 + 1) = 10 Hz (uma interrupção de 100 em 100 ms).
7. Porque pode um sistema com delay(2000) perder o toque num botão? Como se corrige?
Resolução: durante o
delaya CPU não lê o pino. Corrige-se commillis()para a tarefa periódica e leitura do botão em cada volta doloop(), ou com interrupção no pino 2 ou 3.
8. Escreve a máscara para pôr a 1 os bits 3 e 5 de PORTD sem alterar os outros.
Resolução:
PORTD |= (1 << 3) | (1 << 5);A máscara é0b0010 1000= 0x28. Os restantes bits mantêm-se porque OU com 0 não altera. Escolhem-se bits que não são os 0 e 1, que correspondem aos pinos RX e TX da porta série.
Apêndices
A · Cheatsheet Arduino
pinMode(p, INPUT | OUTPUT | INPUT_PULLUP);
digitalRead(p); digitalWrite(p, HIGH | LOW);
analogRead(A0); analogWrite(p, 0..255);
Serial.begin(9600); Serial.print(x); Serial.println(x);
millis(); micros(); delay(ms);
tone(p, freq); noTone(p);
attachInterrupt(digitalPinToInterrupt(2), isr, FALLING);
map(v, a, b, c, d); constrain(v, min, max);
#include <Servo.h> <Wire.h> <SPI.h> <EEPROM.h>
B · Fórmulas
| Grandeza | Fórmula |
|---|---|
| Tensão do ADC | V = leitura × Vref / 1024 |
| Resistência em série de um LED | R = (Vcc - Vf) / I |
| Divisor de tensão | Vout = Vcc × R2 / (R1 + R2) |
| Duty cycle PWM | valor / 255 |
| OCR em modo CTC | f_clk / (prescaler × f) - 1 |
| Distância por eco | tempo × 0,0343 / 2 (cm, µs) |
C · Recursos
- arduino.cc · referência da linguagem e tutoriais.
- Tinkercad Circuits e Wokwi · simuladores online de Arduino e ESP32.
- Folhas de características do ATmega328P e dos módulos usados (sempre a fonte final de valores).
- Fritzing · esquemas de montagem.