ASi (Anansi) Robô Companheiro: Construa um Robô Amigável com Arduino Nano e Display LED
Componentes e Materiais
| Arduino Nano R3 | 1 | |
| Micro Servomotor SG90 | 1 | |
| Módulo Display Matriz LED MAX7219 Vermelho | 1 | |
| Suporte para 4 Pilhas AA | Uma bateria lítio-íon 3.7V 200mAh também funciona | 1 |
| Parafusos | 1 | |
| Casca de Molde de Bomba de Banho em Acrílico Transparente | 1 | |
| Adafruit Trinket - Mini Microcontrolador - Lógica 5V | 1 |
Ferramentas e Máquinas Necessárias
- Impressora 3D (genérica)
- Pistola de Cola Quente (genérica)
Sobre o Projeto
História
Este projeto originalmente seria um Xpider, do Thingiverse. Então vi o Archimedes, da Alex Glow. Me deixou impressionado. Eu queria um desesperadamente. Então comecei a trabalhar no meu próprio companheiro robótico. Percebi que não sou do tipo "coruja", e queria meu familiar especial. Então lembrei da história africana de Anansi, a aranha, trapaceira e deusa das histórias. Decidi projetar o bot com a ideia de uma história, e assim nasceu o Asi (na verdade é Asi_v4, v1-3 foram protótipos).
Montagem
O design original era um Xpider, mas levei a outro nível editando muitas coisas, jogando muita coisa fora e mexendo. Estas são a maioria das peças que você precisa imprimir.
A montagem é autoexplicativa, mas aqui estão algumas fotos mesmo assim.
Para a eletrônica, você geralmente pode colocá-los na parte superior ou inferior da cabeça (sua escolha), mas o servo deve ser anexado como mostrado. Certifique-se de que a engrenagem está nele, por favor.
O Asi é controlado por dois microcontroladores, um Trinket e um Arduino Nano. O Trinket controla o movimento do olho, graças ao servo ele gira para frente e para trás em intervalos aleatórios. O Arduino Nano controla o olho. Também é aleatório enquanto ele olha ao redor e tal, geralmente as pessoas assumem que ele está olhando para elas.
Nota: Eu os tenho em duas protoboards pequenas que são ligadas em cascata com 4 pilhas AA. (Você também pode usar bateria lítio-íon 3.7V 200mAh. Com a bateria li-on, tive vezes por algum motivo em que não funcionava, então usaria duas baterias de lítio separadas e as conectaria ao mesmo interruptor liga/desliga para tudo iniciar de uma vez.)
A cabeça é apenas uma esfera de Natal que comprei nas festas. Dei um passo para trás e pintei levemente com tinta spray preta. Então pegue um pouco de cola quente e cole no pescoço do Asi e PRONTO, lá está a cabeça!
Por fim, como eu o uso. Peguei um pouco de arame de armadura e passei pelos furos na parte inferior do Asi, e enrolei na alça do ombro da minha mochila.
Código
- Olho do Asi
- Pescoço do Asi com Trinket
Olho do Asi Arduino
O código para o olho do Asi
#include <LedControl.h> // Sempre incluir a biblioteca LedControl
#include "LedControl.h"
/* Criar objeto LetControl, definir conexões de pinos
Temos 2 MAX72XX para os olhos. */
#define PIN_EYES_DIN 12
#define PIN_EYES_CS 11
#define PIN_EYES_CLK 10
LedControl lc = LedControl(PIN_EYES_DIN, PIN_EYES_CLK, PIN_EYES_CS, 2);
// rotação
bool rotateMatrix0 = false; // rotacionar matriz 0 em 180 graus
bool rotateMatrix1 = false; // rotacionar matriz 1 em 180 graus
// definir olho sem pupila
byte eyeBall[8] = {
B00111100,
B01111110,
B11111111,
B11111111,
B11111111,
B11111111,
B01111110,
B00111100
};
byte eyePupil = B11100111;
// armazena estado atual dos LEDs
byte eyeCurrent[8];
int currentX;
int currentY;
int cntLoop = 0;
int cntEffect = 0;
// posições min e max
#define MIN -2
#define MAX 2
// atrasos
#define DELAY_BLINK 40
// realizar efeito a cada # iterações do loop, 0 para desativar
#define EFFECT_ITERATION 4
/* Arduino setup */
void setup() {
// MAX72XX está em modo economia de energia na inicialização, precisamos fazer wakeup
lc.shutdown(0, false);
lc.shutdown(1, false);
// definir brilho para baixo
lc.setIntensity(0, 1);
lc.setIntensity(1, 1);
// limpar ambos módulos
lc.clearDisplay(0);
lc.clearDisplay(1);
// Teste LED - linha vertical
byte b = B10000000;
for (int c = 0; c <= 7; c++) {
for (int r = 0; r <= 7; r++) {
setRow(0, r, b);
setRow(1, r, b);
}
b = b >> 1;
delay(50);
}
// módulo cheio
b = B11111111;
for (int r = 0; r <= 7; r++) {
setRow(0, r, b);
setRow(1, r, b);
}
delay(500);
// limpar ambos módulos
lc.clearDisplay(0);
lc.clearDisplay(1);
delay(500);
// seed aleatório
randomSeed(analogRead(0));
// centralizar olhos, piscada maluca
displayEyes(0, 0);
delay(2000);
blinkEyes(true, false);
blinkEyes(false, true);
delay(1000);
}
/* Arduino loop */
void loop() {
// mover para posição aleatória, esperar tempo aleatório
moveEyes(random(MIN, MAX + 1), random(MIN, MAX + 1), 50);
delay(random(5, 7) * 500);
// hora de piscar?
if (random(0, 5) == 0) {
delay(500);
blinkEyes();
delay(500);
}
// hora de efeito?
if (EFFECT_ITERATION > 0) {
cntLoop++;
if (cntLoop == EFFECT_ITERATION) {
cntLoop = 0;
if (cntEffect > 6) cntEffect = 0;
switch(cntEffect) {
case 0: // cruzar olhos
crossEyes();
delay(1000);
break;
case 1: // giro redondo
roundSpin(2);
delay(1000);
break;
case 2: // giro maluco
crazySpin(2);
delay(1000);
break;
case 3: // olhos "meth"
methEyes();
delay(1000);
break;
case 4: // olho preguiçoso
lazyEye();
delay(1000);
break;
case 5: // piscada maluca
blinkEyes(true, false);
blinkEyes(false, true);
delay(1000);
break;
case 6: // brilho
glowEyes(3);
delay(1000);
break;
default:
break;
}
cntEffect++;
}
}
}
/* Este método pisca ambos os olhos */
void blinkEyes() {
blinkEyes(true, true);
}
/* Este método pisca olhos conforme parâmetros fornecidos */
void blinkEyes(boolean blinkLeft, boolean blinkRight) {
// piscar?
if (!blinkLeft && !blinkRight) return;
// fechar pálpebras
for (int i = 0; i <= 3; i++) {
if (blinkLeft) {
setRow(0, i, 0);
setRow(0, 7-i, 0);
}
if (blinkRight) {
setRow(1, i, 0);
setRow(1, 7-i, 0);
}
delay(DELAY_BLINK);
}
// abrir pálpebras
for (int i = 3; i >= 0; i--) {
if (blinkLeft) {
setRow(0, i, eyeCurrent[i]);
setRow(0, 7-i, eyeCurrent[7-i]);
}
if (blinkRight) {
setRow(1, i, eyeCurrent[i]);
setRow(1, 7-i, eyeCurrent[7-i]);
}
delay(DELAY_BLINK);
}
}
/* Este método move olhos para posição central, depois move horizontalmente com wrap nas bordas */
void crazySpin(int times) {
if (times == 0) return;
moveEyes(0, 0, 50);
delay(500);
byte row = eyePupil;
for (int t = 0; t < times; t++) {
// girar do centro para L
for (int i = 0; i < 5; i++) {
row = row >> 1;
row = row | B10000000;
setRow(0, 3, row);
setRow(1, 3, row);
setRow(0, 4, row);
setRow(1, 4, row);
delay(50);
if (t == 0) delay((5-i)*10); // aumentar delay na 1ª rolagem (efeito aceleração)
}
// girar de R para centro
for (int i = 0; i < 5; i++) {
row = row >> 1;
if (i >= 2) row = row | B10000000;
setRow(0, 3, row);
setRow(1, 3, row);
setRow(0, 4, row);
setRow(1, 4, row);
delay(50);
if (t == (times-1)) delay((i+1)*10); // aumentar delay na última rolagem (efeito desaceleração)
}
}
}
/* Este método cruza os olhos */
void crossEyes() {
moveEyes(0, 0, 50);
delay(500);
byte pupilR = eyePupil;
byte pupilL = eyePupil;
// mover pupilas juntas
for (int i = 0; i < 2; i++) {
pupilR = pupilR >> 1;
pupilR = pupilR | B10000000;
pupilL = pupilL << 1;
pupilL = pupilL | B1;
setRow(0, 3, pupilR);
setRow(1, 3, pupilL);
setRow(0, 4, pupilR);
setRow(1, 4, pupilL);
delay(100);
}
delay(2000);
// mover pupilas de volta ao centro
for (int i = 0; i < 2; i++) {
pupilR = pupilR << 1;
pupilR = pupilR | B1;
pupilL = pupilL >> 1;
pupilL = pupilL | B10000000;
setRow(0, 3, pupilR);
setRow(1, 3, pupilL);
setRow(0, 4, pupilR);
setRow(1, 4, pupilL);
delay(100);
}
}
/* Este método exibe olho com pupila deslocada por X, Y do centro. X e Y válidos [MIN,MAX] Ambos módulos LED mostram olhos idênticos */
void displayEyes(int offsetX, int offsetY) {
// garantir offsets em ranges válidos
offsetX = getValidValue(offsetX);
offsetY = getValidValue(offsetY);
// calcular índices para linhas da pupila (realizar offset Y)
int row1 = 3 - offsetY;
int row2 = 4 - offsetY;
// definir linha da pupila
byte pupilRow = eyePupil;
// realizar offset X
// bit shift e preencher novo bit com 1
if (offsetX > 0) {
for (int i = 1; i <= offsetX; i++) {
pupilRow = pupilRow >> 1;
pupilRow = pupilRow | B10000000;
}
} else if (offsetX < 0) {
for (int i = -1; i >= offsetX; i--) {
pupilRow = pupilRow << 1;
pupilRow = pupilRow | B1;
}
}
// linha da pupila não pode ter 1s onde eyeBall tem 0s
byte pupilRow1 = pupilRow & eyeBall[row1];
byte pupilRow2 = pupilRow & eyeBall[row2];
// exibir na matriz LCD, atualizar eyeCurrent
for(int r = 0; r < 8; r++) {
if (r == row1) {
setRow(0, r, pupilRow1);
setRow(1, r, pupilRow1);
eyeCurrent[r] = pupilRow1;
} else if (r == row2) {
setRow(0, r, pupilRow2);
setRow(1, r, pupilRow2);
eyeCurrent[r] = pupilRow2;
} else {
setRow(0, r, eyeBall[r]);
setRow(1, r, eyeBall[r]);
eyeCurrent[r] = eyeBall[r];
}
}
// atualizar X e Y atuais
currentX = offsetX;
currentY = offsetY;
}
/* Este método corrige valor de coordenada fornecido */
int getValidValue(int value) {
if (value > MAX) return MAX;
else if (value < MIN) return MIN;
else return value;
}
/* Este método pulsa olho (muda brilho LED) */
void glowEyes(int times) {
for (int t = 0; t < times; t++) {
for (int i = 2; i <= 8; i++) {
lc.setIntensity(0, i);
lc.setIntensity(1, i);
delay(50);
}
delay(250);
for (int i = 7; i >= 1; i--) {
lc.setIntensity(0, i);
lc.setIntensity(1, i);
delay(25);
}
delay(150);
}
}
/* Este método move olhos para centro, fora e depois volta ao centro */
void methEyes() {
moveEyes(0, 0, 50);
delay(500);
byte pupilR = eyePupil;
byte pupilL = eyePupil;
// mover pupilas para fora
for (int i = 0; i < 2; i++) {
pupilR = pupilR << 1;
pupilR = pupilR | B1;
pupilL = pupilL >> 1;
pupilL = pupilL | B10000000;
setRow(0, 3, pupilR);
setRow(1, 3, pupilL);
setRow(0, 4, pupilR);
setRow(1, 4, pupilL);
delay(100);
}
delay(2000);
// mover pupilas de volta ao centro
for (int i = 0; i < 2; i++) {
pupilR = pupilR >> 1;
pupilR = pupilR | B10000000;
pupilL = pupilL << 1;
pupilL = pupilL | B1;
setRow(0, 3, pupilR);
setRow(1, 3, pupilL);
setRow(0, 4, pupilR);
setRow(1, 4, pupilL);
delay(100);
}
}
/* Este método move ambos olhos da posição atual para nova posição */
void moveEyes(int newX, int newY, int stepDelay) {
// definir posição atual como início
int startX = currentX;
int startY = currentY;
// corrigir novos X Y inválidos
newX = getValidValue(newX);
newY = getValidValue(newY);
// avaliar passos
int stepsX = abs(currentX - newX);
int stepsY = abs(currentY - newY);
// precisa mudar pelo menos uma posição
if ((stepsX == 0) && (stepsY == 0)) return;
// avaliar direção, # passos, mudança por passo X Y, realizar movimento
int dirX = (newX >= currentX) ? 1 : -1;
int dirY = (newY >= currentY) ? 1 : -1;
int steps = (stepsX > stepsY) ? stepsX : stepsY;
int intX, intY;
float changeX = (float)stepsX / (float)steps;
float changeY = (float)stepsY / (float)steps;
for (int i = 1; i <= steps; i++) {
intX = startX + round(changeX * i * dirX);
intY = startY + round(changeY * i * dirY);
displayEyes(intX, intY);
delay(stepDelay);
}
}
/* Este método abaixa e levanta apenas pupila direita */
void lazyEye() {
moveEyes(0, 1, 50);
delay(500);
// abaixar pupila esquerda lentamente
for (int i = 0; i < 3; i++) {
setRow(1, i+2, eyeBall[i+2]);
setRow(1, i+3, eyeBall[i+3] & eyePupil);
setRow(1, i+4, eyeBall[i+4] & eyePupil);
delay(150);
}
delay(1000);
// levantar pupila esquerda rapidamente
for (int i = 0; i < 3; i++) {
setRow(1, 4-i, eyeBall[4-i] & eyePupil);
setRow(1, 5-i, eyeBall[5-i] & eyePupil);
setRow(1, 6-i, eyeBall[6-i]);
delay(25);
}
}
/* Este método gira pupilas no sentido horário */
void roundSpin(int times) {
if (times == 0) return;
moveEyes(2, 0, 50);
delay(500);
for (int i = 0; i < times; i++) {
displayEyes(2, -1); delay(40); if (i==0) delay(40);
displayEyes(1, -2); delay(40); if (i==0) delay(30);
displayEyes(0, -2); delay(40); if (i==0) delay(20);
displayEyes(-1, -2); delay(40); if (i==0) delay(10);
displayEyes(-2, -1); delay(40);
displayEyes(-2, 0); delay(40);
displayEyes(-2, 1); delay(40); if (i==(times-1)) delay(10);
displayEyes(-1, 2); delay(40); if (i==(times-1)) delay(20);
displayEyes(0, 2); delay(40); if (i==(times-1)) delay(30);
displayEyes(1, 2); delay(40); if (i==(times-1)) delay(40);
displayEyes(2, 1); delay(40); if (i==(times-1)) delay(50);
displayEyes(2, 0); delay(40);
}
}
/* Este método define valores para linha da matriz. Realiza rotação 180 se necessário */
void setRow(int addr, int row, byte rowValue) {
if (((addr == 0) && (rotateMatrix0)) || (addr == 1 && rotateMatrix1)) {
row = abs(row - 7);
rowValue = bitswap(rowValue);
}
lc.setRow(addr, row, rowValue);
}
/* Inverter bits em byte https://www.nrtm.org/index.php/2013/07/25/reverse-bits-in-a-byte/ */
byte bitswap(byte x) {
byte result;
asm("mov __tmp_reg__, %[in] \n\t"
"lsl __tmp_reg__ \n\t" // shift out high bit to carry
"ror %[out] \n\t" // rotate carry __tmp_reg__to low bit (eventually)
"lsl __tmp_reg__ \n\t" // 2
"ror %[out] \n\t"
"lsl __tmp_reg__ \n\t" // 3
"ror %[out] \n\t"
"lsl __tmp_reg__ \n\t" // 4
"ror %[out] \n\t"
"lsl __tmp_reg__ \n\t" // 5
"ror %[out] \n\t"
"lsl __tmp_reg__ \n\t" // 6
"ror %[out] \n\t"
"lsl __tmp_reg__ \n\t" // 7
"ror %[out] \n\t"
"lsl __tmp_reg__ \n\t" // 8
"ror %[out] \n\t"
: [out] "=r" (result) : [in] "r" (x));
return(result);
}
Pescoço do Asi com Trinket Arduino
Isto é o que uso para fazer o pescoço do Asi mover para frente e para trás
// Parâmetros do servo. Pino DEVE ser 1 ou 4 no Trinket. Posição do servo
// é especificada em ticks brutos do timer/contador (1 tick = 0.128 milissegundos).
// Tempo de pulso do servo é tipicamente 1-2 ms, mas pode variar ligeiramente entre
// servos, então pode precisar ajustar estes limites para combinar com sua realidade.
#define SERVO_PIN 4 // Pinos 1 ou 4 suportados no Trinket
#define SERVO_MIN 4 // ~1 ms pulso
#define SERVO_MAX 26 // ~2 ms pulso
Adafruit_TiCoServo servo;
void setup(void) {
#if (F_CPU == 16000000L) // Trinket 16 MHz requer setting prescale para timing correto.
// Isto DEVE ser feito ANTES de servo.attach()!
clock_prescale_set(clock_div_1);
#endif
servo.attach(SERVO_PIN);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH);
}
uint32_t lastLookTime = 0; // Tempo da última virada de cabeça
void loop(void) {
unsigned long t = millis(); // Tempo atual
// Se mais de 1/2 segundo passou desde última virada de cabeça...
if((t - lastLookTime) > 500) {
if(random(10) == 0) { // Há chance 1-em-10...
// ...de mover cabeça aleatoriamente em nova direção:
servo.write(random(SERVO_MIN, SERVO_MAX));
lastLookTime = t; // Salvar tempo da virada para referência futura
}
}
// Não relacionado à verificação de virada de cabeça, se
if(random(10) == 0) { // há chance 1-em-10...
// ...de um "piscar de olho":
digitalWrite(LED_PIN, LOW); // LED apaga
delay(random(50, 250)); // por momento aleatório curto
digitalWrite(LED_PIN, HIGH); // depois acende
}
delay(100); // Repetir loop() ~10 vezes/segundo
}
Peças Personalizadas e Gabinetes
Peça final da perna
Esqueci de adicionar a engrenagem de virar para o servo, geralmente imprimo e encaixo o acessório do servo no meio e parafuso no servo
O corpo do Asi
A base do Asi
Peça da coxa do Asi
Junta da perna
Parafuso que uso para unir pernas do Asi
Porca que vai com o parafuso
Engrenagem do servo usada para fazer a cabeça do Asi virar, recomendaria usar peça do servo e colocar no meio
Peça do meio do Asi
Suporte do servo para engrenagem do Asi
Placa do pescoço do Asi, onde a esfera acrílica é colada ao pescoço
Esquemáticos
O circuito que usei para o pescoço do Asi
Estes são os que usei para iniciar o Asi. Você pode ligá-los em cascata para alimentar ambos a partir de pequenas placas de circuito
Processo de manufatura
- Construindo um Robô com Arduino e Raspberry Pi: Guia Passo a Passo
- Robô DIY Inspirado no Wall‑E com Caixa de CD e Raspberry Pi
- PiCy: Construa Seu Próprio Mini Robô Alimentado por Raspberry Pi
- Robô Alarme Rolante: Sistema Móvel Inteligente de Despertar com Raspberry Pi e Google Calendar
- Construa um Robô Creeper Motorizado do Minecraft com Arduino
- Arquimedes: O Robô Coruja Sensor de Emoções com IA
- Construa Robô de Visão Computacional Controlado por Fala no Windows 10 IoT Core
- Robô ROS: Navegação Controlada por Terminal no Raspberry Pi
- Simple Pi Robot – Construa Rover Autônomo 2-WD com Raspberry Pi
- Entendendo Robôs de Bebidas: Tecnologia, Automação e Papel na Indústria de Bebidas