(a) Conceito e Arquitetura
Um assistente de bancada que apenas fala e escreve na tela é um terminal com voz. O movimento é o que transforma o conjunto em presença: o usuário entende “estou te ouvindo”, “não entendi” ou “preciso da sua atenção” antes de ler qualquer palavra. Este post cobre a camada de atuação desse assistente — o pescoço pan/tilt e, opcionalmente, braços, orelhas e antenas — usando servos de modelismo comandados por um driver PCA9685 via barramento I2C.
A arquitetura tem três camadas bem separadas: percepção (microfone I2S e câmera — assunto do próximo post), decisão (ESP32-S3 ou um host na rede) e atuação (PWM). O ponto central é que o microcontrolador não gera os 50 Hz de cada servo em software. Ele envia comandos de 12 bits para o PCA9685, que mantém 16 canais PWM estáveis de forma autônoma enquanto o firmware cuida de áudio, rede e inferência. Um único barramento de dois fios (SDA/SCL) endereça até 62 dessas placas (0x40 a 0x7F, selecionados pelos jumpers A0–A5), o que permite escalar de um pescoço de dois eixos para um torso completo sem consumir mais GPIOs.
Fluxo elétrico: uma fonte de 5–6 V alimenta exclusivamente a trilha V+ do driver; a lógica da placa (VCC) fica nos 3,3 V do MCU; o terra é único e comum a tudo. Cada canal entrega o pulso que o servo espera — em geral 500 a 2500 µs a 50 Hz, com o curso mecânico real entre 1 ms e 2 ms para 0°–180°, variando por modelo. Comando por I2C leva cerca de 1 ms para 16 canais a 400 kHz, então atualizar 60 vezes por segundo o pescoço inteiro é perfeitamente viável.
(b) BOM — Lista de Materiais
Os itens marcados como “crítico” são os que aparecem em 90% dos projetos que falham por alimentação.
| Item | Qtd | Especificação | Observação |
|---|---|---|---|
| MCU | 1 | ESP32-S3 DevKit (N16R8) ou ESP32 clássico | 3,3 V de lógica; I2C em 400 kHz |
| Driver PWM | 1 | Módulo PCA9685, 16 canais, 12 bits | Endereço padrão 0x40; VCC 3,3 V |
| Servo pan | 1 | MG996R (metal) ou DS3218 (20 kg·cm) | Maior curso e torque no eixo horizontal |
| Servo tilt | 1 | MG90S ou SG90 (metal/plástico) | Carga menor, mais rápido |
| Servos extras | 0–4 | SG90 para orelhas, braços, antenas | Movimento secundário, baixo torque |
| Fonte | 1 | Chaveada 5 V / 10 A (ou 6 V / 8 A) | CRÍTICO: não use USB do PC |
| Capacitor | 1 | Eletrolítico 1000 µF / 16 V + 100 nF cerâmico | CRÍTICO: junto ao V+ do driver |
| Fios | — | AWG 20–22 para V+ e GND; AWG 24–26 para sinal | CRÍTICO: fio fino causa queda de tensão |
| Barramento de terra | 1 | Borne ou placa de distribuição | Ponto estrela único |
| Mecânica | — | Suporte 3D, rolamentos 608ZZ no eixo pan | Evita folga e esforço do servo |
| Proteção | 1 | Fusível rearmável 5 A na linha V+ | Trava mecânica pode virar curto |
(c) Wiring & Esquemático
O PCA9685 tem quatro grupos de pinos: alimentação lógica, barramento I2C, saídas PWM e o pino OE (output enable, ativo em nível baixo). A pinagem abaixo é a que funciona sem surpresas no ESP32-S3; no ESP32 clássico, os pinos I2C padrão são GPIO21 (SDA) e GPIO22 (SCL).
| Pino (PCA9685) | Destino | Função / Observação |
|---|---|---|
| VCC | 3V3 do ESP32-S3 | Lógica da placa. Nunca 5 V: o I2C subiria para 5 V e queimaria o GPIO |
| GND | Terra comum (ponto estrela) | Obrigatório unir GND do MCU e GND da fonte de servos |
| SDA | GPIO8 (S3) / GPIO21 (ESP32) | Pull-up 4,7 kΩ para 3V3 se o módulo não tiver |
| SCL | GPIO9 (S3) / GPIO22 (ESP32) | Idem; mantenha o par junto com um fio de GND |
| OE | GND (ou GPIO livre) | Ativo baixo: ligado a GND habilita as saídas; via GPIO permite desligar todos os servos |
| V+ | +5 V da fonte dedicada | Trilha de potência. Não compartilhe com o USB nem com o VCC |
| Canal 0–15 | Sinal do servo (fio laranja/branco) | Cada servo vai à sua própria fonte: SIG no canal, +V e GND no borne da fonte |
Alimentação separada dos servos. O rail V+ deve vir da fonte chaveada, não do regulador da placa. O servo MG996R pode puxar 2,5 A no stall e o DS3218 passa de 3 A; quatro servos mal comportados somam mais de 10 A por alguns milissegundos. O regulador do DevKit entrega, no melhor caso, 1 A e o USB do PC, 500 mA.
Pico de corrente. Coloque o eletrolítico de 1000 µF o mais perto possível dos bornes V+ e GND do driver, e um cerâmico de 100 nF em paralelo para ruído de alta frequência. O capacitor não “aumenta” a fonte: ele absorve o transiente de partida e impede que a tensão caia abaixo do limiar do brownout.
Terra comum. Um único ponto de terra (estrela), com o fio de potência e o de lógica convergindo nele. Ligar o GND do servo em cascata por outro servo cria diferença de potencial e jitter.
Cuidados mecânicos. Antes de energizar, mova o pescoço à mão em todo o curso e confirme que não há batente mecânico dentro da faixa que o software vai usar. Travar um servo contra o próprio limite é a forma mais rápida de queimar o driver.
(d) Código — varredura, easing e limites de curso
A biblioteca oficial da Adafruit traz setPWM(canal, 0, ticks), em que ticks é o valor de 12 bits (0–4095) dentro do período de 50 Hz. A conversão de microssegundos para ticks é us × 4096 / 20000.
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40);
const uint16_t TICKS = 4096; // resolucao de 12 bits
const uint16_t PERIODO = 20000; // 20 ms @ 50 Hz
const uint16_t US_MIN = 500; // pulso minimo do servo
const uint16_t US_MAX = 2500; // pulso maximo do servo
uint16_t usParaTicks(uint16_t us) {
return (uint32_t)us * TICKS / PERIODO;
}
uint16_t anguloParaUs(float graus, float gMin, float gMax) {
graus = constrain(graus, gMin, gMax);
return US_MIN + (US_MAX - US_MIN) * (graus - gMin) / (gMax - gMin);
}
void setup() {
Wire.begin(8, 9); // SDA, SCL no ESP32-S3
Wire.setClock(400000); // 400 kHz
pwm.begin();
pwm.setOscillatorFrequency(27000000.0); // calibre com a sua placa
pwm.setPWMFreq(50); // 50 Hz para servo analogico
}
// interpola com easing cubico respeitando os limites mecanicos
void moveEixo(uint8_t ch, float &atual, float destino,
int16_t gMin, int16_t gMax,
uint16_t passoMs, uint16_t duracaoMs) {
destino = constrain(destino, (float)gMin, (float)gMax);
uint16_t passos = max(1, (int)(duracaoMs / passoMs));
float inicio = atual;
for (uint16_t i = 1; i <= passos; i++) {
float t = (float)i / passos;
float e = (t < 0.5) ? 4*t*t*t : 1 - pow(-2*t + 2, 3) / 2;
atual = inicio + (destino - inicio) * e;
pwm.setPWM(ch, 0, usParaTicks(anguloParaUs(atual, gMin, gMax)));
delay(passoMs);
}
}
void loop() {
static float pan = 90, tilt = 90;
moveEixo(0, pan, 130, 20, 160, 15, 700); // olha para a direita
moveEixo(1, tilt, 110, 35, 145, 15, 500);
delay(400);
moveEixo(0, pan, 50, 20, 160, 15, 700); // varredura de bancada
moveEixo(1, tilt, 80, 35, 145, 15, 500);
delay(400);
moveEixo(0, pan, 90, 20, 160, 15, 600); // retorna ao centro
moveEixo(1, tilt, 90, 35, 145, 15, 600);
delay(1500);
}
Três decisões importam mais que o resto. Limites de curso: os parâmetros gMin/gMax (20°–160° no pan, 35°–145° no tilt) impedem que o firmware mande o servo além do batente — o constrain é a última linha de defesa, não a primeira. Suavização: interpolar em passos de 15 ms com curva easeInOutCubic elimina o tranco que aparece quando se escreve o ângulo final direto; é isso que faz o movimento parecer “intencional” em vez de mecânico. Não bloqueante: o delay() acima serve para ilustrar; em produção, troque por agendamento com micros() e mova os eixos em paralelo, para que o assistente continue respondendo enquanto se move.
(e) Troubleshooting
| Sintoma | Causa provável | Correção |
|---|---|---|
| ESP32 reinicia quando o servo arranca | Brownout: rail compartilhado com o MCU e queda de tensão | Fonte separada para V+, terra comum, 1000 µF no driver, fio AWG 20 |
| Reinício persiste só com um servo | Servo em stall puxando pico alto | Alivie a carga mecânica ou troque por servo de maior torque |
| Jitter / tremor contínuo | Rampa de I2C longa, falta de pull-up, servo barato | Cabo I2C curto (<30 cm) e trançado, pull-up 4,7 kΩ, cerâmico 100 nF |
| Servo esquenta parado | Pulso fora do range do modelo | Ajuste US_MIN/US_MAX e calibre o oscilador do PCA9685 |
| Torque insuficiente / engasga | Queda de tensão no fio ou servo subdimensionado | AWG 20, fonte 5 A+, 6 V se o servo suportar, rolamento no eixo |
| Endereço I2C não encontrado | Jumpers A0–A5 ou VCC errado | Rode um scanner I2C e confira o endereço esperado (0x40) |
O reset por brownout é o sintoma mais comum e o mais mal diagnosticado. Desabilitar o detector (WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0)) só esconde o problema: a tensão ainda está caindo e o comportamento dos servos vai ficar errático. Corrija a alimentação. Já o jitter de alguns graus normalmente não vem do servo, e sim do barramento: I2C e PWM convivem mal quando o terra é compartilhado em série. Por fim, torque insuficiente quase nunca é culpa do servo antes de ser culpa do fio de 1 mm² que faz 0,4 V de queda em 3 A.
(f) Aplicações reais — o que o movimento comunica
Movimento é linguagem. Um assistente que inclina a cabeça quando está processando, faz um “não” curto ao rejeitar um comando e varre a bancada quando está em idle comunica estado sem gastar uma palavra. Na prática, os padrões que funcionam melhor são poucos: idle scan (varredura lenta de 30° a 60° com pausas), gaze (pan/tilt apontando para a pessoa detectada pela câmera), acknowledge (inclinação de 5° e retorno), negação (dois movimentos curtos no eixo pan) e alerta (subida rápida do tilt, seguida de congelamento).
Além da interação, a mesma arquitetura serve para funções utilitárias: gimbal do sensor de visão (fechando o loop com o post da câmera), posicionamento de uma câmera de inspeção em bancada de eletrônica, animatrônica e vitrines, telepresença com espelhamento de gestos e dispositivos de acessibilidade em que a cabeça robótica sinaliza para pessoas com baixa visão. Em todos os casos, o gargalo não é o servo: é a fonte e o terra.
Não some depois da matéria
Radar do hub: Núcleo Hits, Além do Oculto, Bobinho. Sem spray, sem lista comprada.
Radar do hub
Um e-mail quando sair matéria que importa — sem spray de newsletter genérica.

Deixe um comentário
Entre com Google ou Facebook para comentar. Nome e e-mail vêm da sua conta — sem preencher formulário.
Se o login falhar, configure Google e Facebook em Configurações → Nextend Social Login.
Outras opções de login