ESP32-S3 com Rust e
MicroPython
Tutorial completo, comparativo e prático
Material orientado para quem já programa em Python e quer migrar para firmware profissional em Rust, usando
ESP32-S3.
1. Visão geral do ecossistema
O ESP32-S3 é uma placa excelente para estudar embarcados modernos porque reúne GPIO,
ADC, PWM, I2C, SPI, UART, Wi-Fi, BLE e USB nativos numa plataforma barata e muito
documentada. A ideia deste material é te mostrar o mesmo problema resolvido com duas
mentalidades: MicroPython para validação rápida e Rust para firmware robusto.
Figura 1 - Comparação de mentalidade entre MicroPython e Rust no ESP32-S3.
Quando escolher cada abordagem
MicroPython: ideal para validar pinagem, testar sensor, descobrir endereço I2C, confirmar
alimentação e prototipar rápido.
Rust com esp-hal: ideal para firmware bare-metal, previsível, com controle fino de
periféricos.
Rust com esp-idf-svc: ideal quando Wi-Fi, MQTT, HTTP, NVS e OTA entram forte no projeto.
2. Placa, pinagem e cuidados reais
Antes de escrever qualquer linha de código, confira três coisas: alimentação, tensão lógica e
pinos realmente livres. No ESP32-S3, alguns pinos têm função especial na placa de
desenvolvimento e alguns podem ficar ligados ao subsistema de flash/PSRAM, dependendo do
módulo.
Trabalhe sempre em 3,3 V na lógica do ESP32-S3.
Não aplique 5 V em GPIO nem em ADC.
Evite usar GPIO19/GPIO20 se estiver usando USB Serial/JTAG nativo.
Confirme no guia da sua placa quais pinos estão livres no header.
Figura 2 - Visão esquemática de uma DevKit ESP32-S3 e pontos que mais importam no início.
Checklist de bancada
Cabo USB de dados, não apenas de carga.
Protoboard, jumpers, resistor de 220 ohms para LED e um botão NA.
Sensor I2C simples como BME280 ou AHT20.
Display OLED SSD1306 I2C opcional, mas muito didático.
GPS UART opcional para o capítulo de serial.
3. Preparando o ambiente no Windows
3.1 MicroPython
Baixe o firmware correto da variante ESP32_GENERIC_S3 ou da placa equivalente.
Grave o firmware com esptool ou utilitário semelhante.
Acesse REPL por porta serial para validar que a placa está viva.
Use Thonny, mpremote ou terminal serial para subir scripts.
# Exemplo de gravação com esptool (ajuste a porta COM e o arquivo .bin)
[Link] --chip esp32s3 --port COM5 erase_flash
[Link] --chip esp32s3 --port COM5 --baud 460800 write_flash -z 0x0
ESP32_GENERIC_S3.bin
3.2 Rust no ESP32-S3
No ESP32-S3, o caminho mais estável é instalar o toolchain com suporte Xtensa via espup, e
depois usar espflash para gravar a placa. Para projeto bare-metal, use esp-hal. Para Wi-Fi e
serviços de aplicação, use esp-idf-svc.
cargo install espup --locked
espup install
cargo install espflash --locked
cargo install cargo-generate
No Windows, abra um novo terminal depois do espup para herdar o ambiente configurado.
Para projeto bare-metal, a base mais segura é o template/examples do ecossistema esp-hal.
Para projeto com ESP-IDF, o esp-idf-template reduz muito o atrito inicial.
4. Projeto 1 - LED e botão com debounce
Este é o projeto certo para começar porque ensina saída digital, entrada digital, pull-up e
tratamento de bounce. A mesma ligação serve para MicroPython e Rust.
Figura 3 - Ligação recomendada para LED externo e botão com pull-up interno.
Objetivo
Ao pressionar o botão, alternar o estado do LED.
Evitar múltiplos disparos causados pelo bounce mecânico.
MicroPython - versão completa
from machine import Pin
import time
led = Pin(2, [Link])
btn = Pin(0, [Link], Pin.PULL_UP)
estado_led = 0
ultimo_estado = 1
instante_mudanca = time.ticks_ms()
while True:
leitura = [Link]()
if leitura != ultimo_estado:
instante_mudanca = time.ticks_ms()
if time.ticks_diff(time.ticks_ms(), instante_mudanca) > 30:
if ultimo_estado == 1 and leitura == 0:
estado_led = 1 - estado_led
[Link](estado_led)
ultimo_estado = leitura
time.sleep_ms(5)
Leitura didática: o botão fica em nível alto por causa do pull-up. Ao pressionar, o pino é aterrado
e cai para 0. O atraso de 30 ms não é para 'deixar bonito'; ele existe para ignorar oscilações do
contato.
Rust com esp-hal - versão de estudo
#![no_std]
#![no_main]
use esp_backtrace as _;
use esp_hal::{
clock::CpuClock,
delay::Delay,
gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull},
main,
};
#[main]
fn main() -> ! {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
let mut delay = Delay::new();
let mut led = Output::new(
peripherals.GPIO2,
Level::Low,
OutputConfig::default(),
);
let button = Input::new(
peripherals.GPIO0,
InputConfig::default().with_pull(Pull::Up),
);
let mut led_on = false;
let mut last = true;
let mut stable_count = 0u8;
loop {
let now = button.is_high();
if now == last {
if stable_count < 10 {
stable_count += 1;
}
} else {
stable_count = 0;
}
if stable_count == 5 && last && !now {
led_on = !led_on;
if led_on {
led.set_high();
} else {
led.set_low();
}
}
last = now;
delay.delay_millis(5);
}
}
O que você aprende aqui
MicroPython te entrega o pino pronto para uso.
Rust te obriga a converter o pino em um driver tipado de entrada ou saída.
No Rust, ownership do periférico evita uso indevido e conflitos de inicialização.
5. Projeto 2 - Sensor I2C (BME280 ou AHT20)
Aqui a regra de ouro é simples: antes de tentar a biblioteca do sensor, prove que o barramento
está vivo. No I2C, a fase mais importante do bring-up é confirmar endereço, tensão, pull-up e
continuidade dos fios.
Figura 4 - Exemplo de barramento I2C compartilhando BME280 e OLED SSD1306.
MicroPython - primeiro passo: escanear o barramento
from machine import Pin, I2C
import time
i2c = I2C(0, scl=Pin(18), sda=Pin(17), freq=400000)
while True:
print("I2C scan:", [hex(x) for x in [Link]()])
[Link](2)
Se aparecer 0x76 ou 0x77, o BME280 provavelmente respondeu. Se aparecer 0x38, o AHT20
provavelmente respondeu. Se não aparecer nada, o erro normalmente é elétrico: alimentação
errada, fio trocado, GND ausente ou pinos incorretos.
MicroPython - leitura conceitual do sensor
# Exemplo didático: a forma exata depende da biblioteca instalada
# Supondo um driver BME280 em [Link]
import time
from machine import Pin, I2C
import bme280
i2c = I2C(0, scl=Pin(18), sda=Pin(17), freq=400000)
sensor = bme280.BME280(i2c=i2c)
while True:
temp, press, hum = [Link]
print("Temp:", temp, "Press:", press, "Umid:", hum)
[Link](1)
Rust - inicialização do barramento
#![no_std]
#![no_main]
use esp_backtrace as _;
use esp_hal::{
clock::CpuClock,
i2c::master::{Config as I2cConfig, I2c},
main,
};
#[main]
fn main() -> ! {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
let i2c = I2c::new(
peripherals.I2C0,
I2cConfig::default(),
)
.unwrap()
.with_sda(peripherals.GPIO17)
.with_scl(peripherals.GPIO18);
// Passe `i2c` para o driver do BME280/AHT20 compatível com embedded-hal.
loop {}
}
No Rust, o barramento vira um recurso tipado que você entrega a um driver de dispositivo. Essa
mudança de mentalidade é muito importante: em vez de pensar 'eu mando bytes na mão', você
passa a pensar 'eu uso um driver que encapsula o protocolo'.
Problemas clássicos em I2C
SDA e SCL invertidos.
Módulo alimentado em 5 V mas com pull-up inadequado para 3,3 V.
Falta de GND comum.
Biblioteca errada para o endereço real do módulo.
Módulo com seletor de endereço soldado para outro valor.
6. Projeto 3 - OLED SSD1306
O OLED é excelente para aprender barramento compartilhado, buffer de imagem e atualização
periódica sem precisar abrir terminal serial o tempo inteiro.
MicroPython - exemplo completo
from machine import Pin, I2C
import ssd1306
import time
i2c = I2C(0, scl=Pin(18), sda=Pin(17), freq=400000)
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
contador = 0
while True:
[Link](0)
[Link]("ESP32-S3", 0, 0)
[Link]("MicroPython", 0, 16)
[Link]("cont = {}".format(contador), 0, 32)
[Link]()
contador += 1
[Link](1)
Rust - raciocínio equivalente
// Esqueleto conceitual:
// 1) inicialize I2C
// 2) crie a interface para o display SSD1306
// 3) atualize o frame buffer
// 4) envie o buffer ao display
// No ecossistema Rust, procure crates compatíveis com embedded-hal
// como ssd1306 + embedded-graphics.
Para estudar, o importante é entender a separação: o barramento I2C pertence ao driver do
display, e o texto ou desenho pertence ao frame buffer. No Rust, essa fronteira costuma ficar
mais clara do que no MicroPython.
7. Projeto 4 - ADC com potenciômetro
Este projeto ensina leitura analógica e ruído. Ele parece simples, mas é o primeiro contato com
problemas que software sozinho não resolve.
Figura 5 - Ligação segura de um potenciômetro de 10k a 3,3 V para teste de ADC.
MicroPython - leitura direta
from machine import ADC, Pin
import time
adc = ADC(Pin(1))
while True:
print(adc.read_u16())
time.sleep_ms(200)
Rust - estrutura de estudo
// Fluxo típico no Rust:
// 1) inicialize a unidade ADC
// 2) configure o canal analógico (por exemplo GPIO1 em ADC1)
// 3) leia uma amostra
// 4) aplique filtragem se necessário
Filtro simples por média móvel
# MicroPython
amostras = [0] * 8
indice = 0
while True:
amostras[indice] = adc.read_u16()
indice = (indice + 1) % len(amostras)
media = sum(amostras) // len(amostras)
print("filtrado:", media)
A grande lição aqui é que 'ler ADC' não é o desafio principal. O desafio real é tratar ruído, usar
referência coerente e respeitar o range elétrico da entrada.
8. Projeto 5 - PWM para LED e servo
PWM é o momento em que muita gente percebe a diferença entre API fácil e hardware real. No
MicroPython você aplica PWM ao pino. No hardware, por baixo, existe timer + canal + saída.
MicroPython - dimmer de LED
from machine import Pin, PWM
import time
pwm = PWM(Pin(2), freq=1000, duty_u16=0)
while True:
for d in range(0, 65535, 2000):
pwm.duty_u16(d)
time.sleep_ms(15)
for d in range(65535, 0, -2000):
pwm.duty_u16(d)
time.sleep_ms(15)
Servo - ideia prática
Freqüência típica: 50 Hz.
Pulso mínimo aproximado: 1,0 ms.
Centro aproximado: 1,5 ms.
Pulso máximo aproximado: 2,0 ms.
No Rust, use o periférico de PWM entendendo a relação entre timer, frequência e duty cycle.
Isso melhora muito quando você precisa controlar mais de um canal sem bagunçar a
frequência.
9. Projeto 6 - UART com GPS
UART é um ótimo projeto para consolidar pinos, baud rate, fluxo de bytes e parsing. O exemplo
mais didático é um GPS NMEA.
Figura 6 - Ligação UART cruzando TX e RX entre ESP32-S3 e módulo GPS.
MicroPython - leitura simples
from machine import UART
import time
uart = UART(1, baudrate=9600, tx=17, rx=18)
while True:
if [Link]():
linha = [Link]()
if linha:
print(linha)
time.sleep_ms(100)
Rust - ideia equivalente
// No Rust:
// 1) configure UART1 ou UART0
// 2) associe TX e RX aos GPIOs
// 3) leia o buffer serial
// 4) faça parsing das sentenças NMEA ($GPGGA, $GPRMC, etc.)
Aqui o Rust brilha quando você começa a escrever parser robusto, trabalhar com checksum e
transformar a serial em uma máquina de estados.
10. Projeto 7 - Wi-Fi STA
Neste ponto do curso, a recomendação mais prática é: faça o primeiro teste de rede em
MicroPython, depois migre para Rust com esp-idf-svc se o projeto realmente for conectado.
MicroPython - conexão STA
import network
import time
SSID = "SEU_WIFI"
SENHA = "SUA_SENHA"
wlan = [Link]()
[Link](True)
[Link](SSID, SENHA)
while not [Link]():
print("Conectando...")
time.sleep_ms(300)
print("Conectado:", [Link]())
Rust com esp-idf-svc - fluxo mental
// Estrutura típica de um projeto com esp-idf-svc:
// 1) Peripherals::take()
// 2) criar event loop
// 3) abrir NVS padrão
// 4) criar EspWifi
// 5) aplicar ClientConfiguration
// 6) start(), connect(), wait_netif_up()
Quando a conectividade entra em cena, o esp-idf-svc compensa porque já embrulha serviços
importantes como Wi-Fi, HTTP, MQTT, NVS e OTA.
11. Projeto 8 - BLE e rádio
No ESP32-S3, BLE abre caminho para beacon, provisionamento e troca de dados locais. Para
prototipar comportamento, MicroPython é confortável. Para firmware bare-metal com rádio, o
crate esp-radio é o caminho atual no ecossistema Rust.
12. Como migrar um projeto de MicroPython para Rust
Etapa Em MicroPython Em Rust
Testar ligações, endereços Repetir a mesma ligação já
Bring-up elétrico
I2C, resposta serial validada
Usar Pin/I2C/UART/PWM Converter pinos e periféricos
Periférico
diretamente em drivers tipados
Mais rigoroso em compile
Tratamento de erro Mais flexível em runtime
time
Escalabilidade Ótimo para prova de conceito Melhor para firmware final
Regra prática que funciona muito bem: valide hardware em MicroPython, congele a pinagem e
só então porte a lógica estável para Rust.
13. Estrutura recomendada de estudo
Semana 1: LED, botão e UART.
Semana 2: I2C com scan, sensor e OLED.
Semana 3: ADC, PWM e servo.
Semana 4: Wi-Fi, HTTP/MQTT e persistência.
Semana 5: portar os dois primeiros projetos de MicroPython para Rust.
14. Checklist de depuração
A placa aparece na porta COM correta?
O cabo USB transporta dados?
Existe GND comum entre todos os módulos?
A tensão do módulo é realmente 3,3 V?
Os GPIO escolhidos não conflitam com USB/flash/PSRAM?
No I2C, o scan encontra algum endereço?
No UART, TX e RX estão cruzados corretamente?
No ADC, o sinal está dentro do range e sem 5 V?
15. Conclusão
MicroPython e Rust não competem; eles se complementam muito bem no ESP32-S3. Use
MicroPython para aprender o hardware rápido. Use Rust para consolidar o hardware
corretamente. Quando você adota essa estratégia, o desenvolvimento fica mais rápido, o
firmware fica mais confiável e a migração deixa de ser dolorosa.
Apêndice - Referências oficiais para continuar
Rust on ESP Book - toolchain, async, visão geral do ecossistema.
esp-hal - documentação e exemplos da HAL bare-metal.
ESP-IDF Programming Guide - GPIO, ADC, LEDC/PWM, USB Serial/JTAG.
ESP32-S3 DevKitC-1 User Guide e datasheet do ESP32-S3.
MicroPython - quick reference do ESP32, machine.I2C, [Link], tutorial PWM, tutorial
inicial do ESP32.
esp-idf-svc - wrappers de Wi-Fi, HTTP, MQTT, NVS e OTA.
esp-radio - Wi-Fi, BLE e ESP-NOW no ecossistema bare-metal.