Tabela de conteúdos
TP3 - Introdução à Robótica
Confira nosso Diário de Construção.
Introdução
ㅤRobôs móveis são robôs dotados de um sistema de locomoção que os torna capazes de navegar através do seu ambiente de trabalho, interagindo com este ambiente na realização de tarefas pelo uso de recursos próprios de sensoriamento e tomada de decisão. O trabalho prático 3 propõe a construção de um desses robôs que, com ajuda de sensores e atuadores, realiza as tarefas previamente propostas. Essas tarefas são um sistema de localização, odométrica, controle e navegação (line-following) que com auxílio de um menu serão escolhidas de maneira simples.
Equipamentos Utilizados:
ㅤPara realização da disciplina o grupo decidiu por trabalhar com o Arduino Mega 2560. Para isso, foi emprestado pelo professor uma ADAFRUIT MOTOR SHIELD que faz o controle de mais de um motor simultaneamente, o que é necessário para a realização da tarefa, uma SHIELD LCD 16×2 COM KEYPAD (tela e teclado simples),e uma SHIELD de expansão para as entradas do Arduino. Por fim a lista de componentes necessários para a construção do manipulador foi a seguinte:
- 2 Motores DC;
- Arduino Mega 2560;
- Peças do Kit Lego;
- ADAFRUIT MOTOR SHIELD;
- SHIELD LCD 16×2 COM KEYPAD;
- SHIELD de expansão de pinos;
- Caixa para 8 pilhas AA e adaptador output;
- Fios para ligar os motores;
- 2 Sensores break-beam;
- 2 Sensores óptico reflexivo;
- 3 Sensores LDR ;
- 1 LED RGB;
- 2 filtros de luz polarizada;
Tarefas e Algoritmo
Menu:
| a) Todas as tarefas devem ser facilmente acessadas através de um menu. A facilidade de uso desse menu também será avaliado. |
|---|
ㅤNosso objetivo era manter o menu navegável. Para isso o algorítimo modificava no loop() índices através dos botões do LCD e mostrava na tela o menu pela função mostra(). A vantagem de colocar o menu no loop() do Arduino é que depois de executar as outras tarefas a programação sempre volta ao loop().
ㅤNo loop() uma variável global determina em que posição estamos no menu e com a manipulação dela é possível caminhar pelo mesmo. Inicialmente começamos no menu principal que existem 4 posições (de 0 a 3) LOCALIZAR, CAMINHOS, SEGUIR LINHA E CALIBRAR 1). Para caminharmos verticalmente no menu principal, somamos ou subtraímos 1 aos índices de cada posição. Após chegarmos na posição desejada do menu principal o próximo passo é adentrar para o menu secundário, caso necessário. Para que isso ocorra, é somado 4 unidades no índice do menu principal e a navegação vertical somamos ou subtraímos 4 aos índices de cada posição. Após chegar a uma posição desejada é necessário apertar a tecla SELECT para selecionar a ação. Por fim, para sair dos níveis secundários calculamos o resto da divisão dos índices por 4 (%4) e assim, voltamos ao menu principal.
ㅤDepois do cálculo de cada índice acionamos a função mostra() que vai, de acordo com o índice, mostrar na tela a posição no menu correspondente.
ㅤProblemas principais: Anteriormente as posições do menu principal (0 a 3) não eram restringidas e podíamos acessar os indicies depois do 3 sem que haja algo. Outro problema era que havia um delay de 1 segundo para qualquer movimentação do menu o que atrapalhava a fluidez das escolhas.
ㅤSoluções: Foram criadas restrições para os índices do menu principal e a posição máxima atingida é a 3. Além disso, o delay de 1 segundo foi retirado para facilitar a navegação.
ㅤALGORITMO:
void loop() { lcd_key = read_LCD_buttons(); delay(100); if (lcd_key == btnUP) { if (tela > 0 && tela < 4) { tela = tela - 1; mostra(); } if (tela > 9) { tela = tela - 4; mostra(); } } if (lcd_key == btnDOWN) { if (tela >= 0 && tela < 3) { tela = tela + 1; mostra(); return; } if (tela >= 9 && tela < 17) { tela = tela + 4; mostra(); return; } } if (lcd_key == btnRIGHT && tela == 1) { tela = tela + 8; mostra(); return; } if (lcd_key == btnLEFT && tela > 4) { tela = tela % 4; mostra(); return; } //______________________escolher if (lcd_key == btnSELECT) { switch (tela) { case 0: { localiza(); break; return; } case 9: { reta(); break; return; } case 13: { quadrado(); break; return; } case 17: { triangulo(); break; return; } case 2: { segueLinha(); break; return; } case 3: { calibrar(); break; return; } } mostra(); } }
void mostra() { switch (tela) { case 0: lcd.clear(); lcd.setCursor(0, 0); lcd.print("-> Localizar"); lcd.setCursor(0, 1); lcd.print(" Caminhos"); lcd.setCursor(14, 1); lcd.print(tela); break; case 1: lcd.clear(); lcd.setCursor(0, 0); lcd.print(" Localizar"); lcd.setCursor(0, 1); lcd.print("-> Caminhos"); lcd.setCursor(14, 1); lcd.print(tela); break; case 2: lcd.clear(); lcd.setCursor(0, 0); lcd.print("-> Seguir Linha"); lcd.setCursor(0, 1); lcd.print(" Calibrar"); lcd.setCursor(14, 1); lcd.print(tela); break; case 3: lcd.clear(); lcd.setCursor(0, 0); lcd.print(" Seguir Linha"); lcd.setCursor(0, 1); lcd.print("-> Calibrar"); lcd.setCursor(14, 1); lcd.print(tela); break; case 9: lcd.clear(); lcd.setCursor(0, 0); lcd.print("-> Reta"); lcd.setCursor(0, 1); lcd.print(" Quadrado"); lcd.setCursor(14, 1); lcd.print(tela); break; case 13: lcd.clear(); lcd.setCursor(0, 0); lcd.print(" Reta"); lcd.setCursor(0, 1); lcd.print("-> Quadrado"); lcd.setCursor(14, 1); lcd.print(tela); break; case 17: lcd.clear(); lcd.setCursor(0, 0); lcd.print("-> Triângulo"); lcd.setCursor(14, 1); lcd.print(tela); break; } }
Localização:
| a) Utilizando os conceitos do LDR diferencial, o robô deve ser capaz de se alinhar com a fonte de luz mais próxima presente no campo (ver Aula 15 - a partir do Slide 27). OBS: O robô não será informado de qual lado está! |
|---|
ㅤEm cada lado do campo as luzes estão polarizadas de formas diferentes. Isso nos dá instrumento para podermos, através de um sensor LDR diferencial nos localizamos no campo. Esse sensor LDR diferencial retorna um valor analógico que vai de 0 a 1024. Para cada posição no campo há um intervalo específico que o sensor vai retornar, o valor nesse intervalo depende do sentido e direção que o robô se encontra. Por exemplo, nesse trabalho, o nosso LDR sempre retornava um valor maior quando estava direcionado à luz próxima à parede, independentemente da posição dele no campo.
ㅤO desafio era tentar fazer então o robô se alinhar à fonte mais próxima, pois ele sabia em que direção estava a luz da parede, mas onde estava a luz mais próxima? Percebemos que na metade do campo mais próximo à parede o valor máximo era sempre maior que 490. Assim, se numa busca encontrássemos o maior e o menor valor possível naquela posição, descobriríamos em que lado do campo estávamos simplesmente comparando o maior valor com 490 e para nos alinhar a fonte mais próximas bastava retornar o robô à posição de máximo ou de mínimo, dependendo da comparação.
ㅤProblema principal: Uma das luzes é mais intensa que a outra ou a polarização não é totalmente perpendicular à outra e por isso quando de um lado a posição dos filtros bloqueia totalmente a luz em um deles, do outro lado o oposto não ocorre.
ㅤSolução: Inicialmente isso era um problema, mas quando entendemos realmente o funcionamento das luzes procuramos uma característica que diferenciasse os dois lados do campo.
ㅤALGORITMO:
void localiza() { int minimo = 1023; int maximo = 0; int leitura = 0; unsigned long int ti = millis(); M1->setSpeed(90); M2->setSpeed(100); M1->run(FORWARD); M2->run(BACKWARD); leitura = analogRead(LDRdiferencial); delay(300); while ((millis() - ti) < 7000) { leitura = analogRead(LDRdiferencial); if (leitura > maximo) { maximo = leitura; } else if (leitura < minimo) { minimo = leitura; } } if (maximo > 490) { while (analogRead(LDRdiferencial) < maximo - 10) { // Usamos maximo - 10 por precaução delay(100); } M1->run(RELEASE); M2->run(RELEASE); } else { while (analogRead(LDRdiferencial) > minimo + 10) { // Usamos minimo + 10 por precaução delay(100); } M1->run(RELEASE); M2->run(RELEASE); } }
Odometria e controle:
| a) Utilize sensores break-beam para construir shaft-encoders (Seções 3.7 e 3.8 [RE]). b)Implemente um controlador PD para controlar a velocidade do robô. O robô deve ser capaz de realizar um determinado caminho que será selecionado através do menu (Reta, Quadrado, Triângulo). OBS: O comprimento da reta e do lado das figuras geométricas deverá ser escolhido pelo menu. |
|---|
ㅤProblemas com o break-beam: não sabíamos qual emitia e qual recebia, a posição que eles tinham que ficar deveria ser muito precisa porque se isso não acontecesse eles travavam a roda. Para o cálculo da velocidade usamos a função millis() que retorna quanto tempo, em milissegundos, o Arduino está ligado, porém sempre que chegávamos a 35.000ms (35s) aproximadamente o código que calculava a velocidade parava de funcionar.
ㅤSoluções: Encontramos depois de muitas buscas na internet qual era o receptor e o emissor do break-beam. Após exaustivas tentativas conseguimos uma posição ideal para que os dois sensores não bloqueassem o movimento das rodas. Sobre a função velocidade descobrimos que o problema estava ao usar o tempo como int, pois esse tipo só percorre um intervalo de -32.768 a 32.767. Assim, quando passamos a usar unsigned long int para a variável tempo o problema deixou de existir em tempo útil. (unsigned long int vai de 0 a 4.294.967.295)
ㅤALGORITMO:
ㅤPara definir o tamanho da linha a seguir usamos a função tamanho() que possibilitava ao usuário escolher entre 20cm, 30cm, 40cm e 50cm. A ideia era construir um sub-menu que só seria usado quando as funções LINHA, TRIÂNGULO e QUADRADO fossem acionadas.
int tamanho() { lcd.clear(); lcd.setCursor(0, 0); lcd.print("escolha o tamanho:"); lcd.setCursor(0, 1); lcd.print("->20cm 30cm"); int tam = 1; int u=0; lcd_key=btnNONE; while (1) { lcd_key=read_LCD_buttons(); if (lcd_key == btnRIGHT && tam != 4) { tam = tam + 1; delay(400); show(tam); } if (lcd_key == btnLEFT && tam != 1) { tam = tam - 1; delay(400); show(tam); } if (lcd_key == btnSELECT) { delay(400); tam = (tam + 1) * 10; show(tam); return tam; } } } void show(int tam) { lcd.clear(); lcd.setCursor(0, 0); lcd.print("escolha o tamanho:"); lcd.setCursor(0, 1); if (tam == 1) { lcd.print("->20cm 30cm"); } if (tam == 2) { lcd.print(" 20cm ->30cm"); } if (tam == 3) { lcd.print(" ->40cm 50cm "); } if (tam == 4) { lcd.print(" 40cm ->50cm "); } }
Navegação:
| a) O robô deverá seguir as linhas marcadas no campo de competição. Para isso deverá utilizar no máximo dois sensores ópticos à escolha do grupo (óptico-reflexivo ou LDR). (Ver Aula 16 e Seção 6.5 [RE]). OBS: Se necessário, insira um item no menu para realizar uma calibração antes da tarefa, minimizando assim a influência da luz ambiente. |
|---|
ㅤPara a construção da ação de seguir linha foi induzida uma lógica no qual o robô inicia seu movimento de forma livre. Em seguida as ações foram tomadas de acordo com a posição em que os sensores identificavam a linha preta. Caso o robô estivesse identificando a linha preta pela direita, a ideia é seguir pela esquerda e vice-versa. Caso ambos os sensores identificassem a linha preta ao mesmo tempo o robô daria preferência por virar à direita, já que o mesmo não é capaz de tomar decisões em encruzilhadas por exemplo.
ㅤProblema principal:* Inicialmente a ideia era utilizar um sensor LDR com um LED RGB em baixo do robô para diferenciar a linha do chão. Entretanto, pelo LDR estar numa porta analógica era retornado dois intervalos (um para a cor da linha e outro para a cor do chão), o que dificulta tratar esses resultados no programa.
ㅤSolução: Começamos a utilizar dois sensores ópticos reflexivos, pois, por estarem em uma porta digital, eles retornam 0 ou 1 para as cores da linha e do chão, o que é mais fácil tratar dentro do código.
ㅤALGORITMO:
void segueLinha() { //essa função vai seguir a linha preta M1->setSpeed(90); M2->setSpeed(100); M1->run(BACKWARD); M2->run(BACKWARD); while (analogRead(LDRfrente) <= 880) { //conferir if (digitalRead(INFRAleft) == 0 && digitalRead(INFRAright) == 0 ) { M1->run(BACKWARD); M2->run(BACKWARD); } else if (digitalRead(INFRAleft) == 1 && digitalRead(INFRAright) == 0 ) { M1->run(RELEASE); M2->run(RELEASE); M1->run(BACKWARD); M2->run(FORWARD); delay(5); M1->run(BACKWARD); M2->run(BACKWARD); } else if (digitalRead(INFRAleft) == 0 && digitalRead(INFRAright) == 1 ) { M1->run(RELEASE); M2->run(RELEASE); M1->run(FORWARD); M2->run(BACKWARD); delay(5); M1->run(BACKWARD); M2->run(BACKWARD); } else if (digitalRead(INFRAleft) == 1 && digitalRead(INFRAright) == 1 ) { M1->run(RELEASE); M2->run(RELEASE); M1->run(FORWARD); M2->run(BACKWARD); delay(5); M1->run(BACKWARD); M2->run(BACKWARD); } } M1->run(RELEASE); M2->run(RELEASE); }
Construção
ㅤNo início da construção do robô o grupo fez duas reduções equivalentes e simétricas e em cada redução foi colocada uma roda. Essas rodas não poderiam ser grandes demais, pois ia aumentar o centro de gravidade da máquina e, por consequência, o robô ficaria mais instável. A melhor opção presente eram as rodas que estamos utilizando. Entretanto, tivemos que adaptar elas para que encachem no eixo. Logo após, construímos um suporte em cima das duas reduções para o Arduido, pois nessa posição adquirimos uma estabilidade maior. O próximo passo foi colocar os motores a uma distância das reduções e em prol da maior estabilidade estática do robô, colocamos uma terceira roda (esférica) em baixo dos motores. Logo após, definimos a frente do robô como o lado oposto à roda esférica pelo posicionamento dos sensores. Por fim, começamos com a construção dos sensores e por consequência, a estrutura do robô foi gradativamente adaptada para acoplar todos os sensores.
ㅤModificações após o Trabalho Prático 02: Anteriormente, os motores usavam uma polia que desgastava e alterava nos resultados. Para resolver esse impasse, nesse trabalho não utilizamos mais as polias, agora utilizamos as correntes de lego. Outro problema era que os motores, mesmo rodando com 70% da potência máxima, eram muito fracos e eram diferentes entre si. Logo, para resolver esse problema trocamos ambos motores por motores iguais e mais potentes.
Algoritmo de Controle
Você pode saber mais dicas sobre o Arduíno em O que é um Arduino?
ㅤ Iniciamos, com as bibliotecas e as definições:
#include <Wire.h> #include <Adafruit_MotorShield.h> #include <LiquidCrystal.h> #include <time.h> #include <math.h> #define btnRIGHT 0 #define btnUP 1 #define btnDOWN 2 #define btnLEFT 3 #define btnSELECT 4 #define btnNONE 5 LiquidCrystal lcd(8, 9, 4, 5, 6, 7); Adafruit_MotorShield AFMS = Adafruit_MotorShield(); Adafruit_DCMotor *M1 = AFMS.getMotor(1); Adafruit_DCMotor *M2 = AFMS.getMotor(2); int LDRfrente = A9; int LDRdiferencial = A13; int INFRAleft = 33; int INFRAright = 35; int FLIPleft = 23; int FLIPright = 27; int lcd_key = 0; int adc_key_in = 0; int tela = 0;
ㅤAgora, vem o void setup(), a primeira função a rodar e só uma vez.
void setup() { Serial.begin(9600); AFMS.begin(); lcd.begin(16, 2); //inicializa o LCD pinMode(LDRfrente, INPUT); pinMode(LDRdiferencial, INPUT); pinMode(INFRAleft, INPUT); pinMode(INFRAright, INPUT); pinMode(FLIPleft, INPUT); pinMode(FLIPright, INPUT); lcd.clear(); //limpa a tela M1->setSpeed(70); M2->setSpeed(65); int leitura=0; delay(1000); mostra(); tela = 0; }
No void Loop() está o Menu. As outras funções, já apresentadas, ficam abaixo ou em outras abas.
ㅤUma outra função importante é a que lê qual botão é pressionado no LCD:
int read_LCD_buttons() { adc_key_in = analogRead(0); // read the value from the sensor // my buttons when read are centered at these valies: 0, 144, 329, 504, 741 // we add approx 50 to those values and check to see if we are close if (adc_key_in > 1000) return btnNONE; // We make this the 1st option for speed reasons since it will be the most likely result if (adc_key_in < 50) return btnRIGHT; if (adc_key_in < 195) return btnUP; if (adc_key_in < 380) return btnDOWN; if (adc_key_in < 555) return btnLEFT; if (adc_key_in < 790) return btnSELECT; return btnNONE; // when all others fail, return this... }
