Manual

do

Maker

.

com

MPU6050 com ESP8266 (GY-521)

MPU6050 com ESP8266 (GY-521)

Atualmente estou em um projeto que envolve o ESP8266 com um giroscópio MPU6050, então aproveitei pra escrever um tutorial a respeito, mas o que vou fazer é bastante básico, portanto não pretendo abordar aqui todos os recursos disponíveis nesse módulo GY-521.

MPU6050 (GY-521)

O módulo pode ser adquirido a um bom preço nesse link da Auto Core Robótica. Ele opera entre 3V3 e 5V, por isso é fácil utilizá-lo junto a qualquer MCU/CPU que tenha suporte a I²C. O que ocasionalmente ocorre é uma diferença no regulador de tensão da board, que pode não permitir operar em 3V3, por isso você deve estar atento a esse detalhe quando for comprar seu módulo por aí, caso não queira pegar na Auto Core.

Já escrevi um artigo sobre ele, onde o utilizei para fazer um robô de auto-balanço utilizando um controlador PID que eu mesmo escrevi. Se quiser dar uma olhada no artigo clicando aqui.

Primeiro passo - identifique as coordenadas

É fundamental posicionar os eixos X,Y e Z adequadamente para que você possa pegar o deslocamento correto nos eixos. Para isso, olhe sobre o CI dessa board e procure o ponto, que deverá estar em um dos cantos. A partir daí, o posicionamento é esse:

mpu6050-eixos.webp

Não encontrei bibliotecas para Arduino, mas de qualquer modo eu preferi escrever o código para utilizar no ESP8266, portanto não sei se alguma dessas bibliotecas funcionaria no ESP8266, ainda mais porque utilizo meu próprio firmware com Sming. Isso não quer dizer que você não pode utilizar o meu código na IDE do Arduino, uma vez que ele é totalmente genérico.

Caso deseje dar uma olhadela no datasheet, esse é o linkMPU-6000-Datasheet1.pdf.

Você precisará do mapa de registradores também, que você encontra aquiMPU-6500-Register-Map2.pdf.

Codificando

A primeira coisa a fazer é estabelecer uma conexão I²C com o dispositivo. Previamente, é necessário saber o endereço padrão; 0x68 com o pino AD0 desconectado e 0x69 com o pino AD0 conectado à 3v3. Sabendo disso podemos iniciar nosso código.

Não há necessidade de includes, uma vez que a biblioteca Wire do Sming é global e por acaso, extremamente semelhante (ou idêntica) à biblioteca Wire do Arduino.

Se você não é familiarizado com o barramento I²C, não se preocupe. Trata-se de uma comunicação serial também, só que obedece a um determinado protocolo. O I²C utiliza-se de 2 pinos da MCU/CPU, sendo SCL e SDA. Para facilitar, decore da seguinte maneira; tire o "S" da frente e você tem as iniciais de CLock e DAta.

Se você tentar utilizar o barramento sem inicializá-lo, terá problemas, portanto esse é o primeiro passo; inicializar o barramento I²C fazendo uso da biblioteca Wire. Nessa inicialização você pode especificar quais pinos utilizar para SDA e SCL, sendo o default 0 e 2 para SCL e SDA, respectivamente.

Wire.pins (0,2); //selecao dos pinos
Wire.begin();    //inicializacao do I2C

Antes de dar sequência à leitura dos dados do MPU6050, é fundamental que você saiba utilizar o barramento I²C para ler e escrever, por isso vou introduzir a utilização da biblioteca Wire e em seguida iniciaremos a manipulação dos dados do nosso módulo.

Ler e escrever no barramento I²C

Isso é tudo o que você quererá fazer; ler e escrever no barramento. Basicamente, há uma regra a seguir:

Wire.beginTransmission(MPU6050_ADDR); //inicializar a transmissão sempre
Wire.requestFrom(MPU6050_ADDR,length);//requisição de leitura

//enquanto houver dados...
while(Wire.available()){
    for(int i=0;i<length;i++){
        buf[i] = Wire.read();//buffer de dados
    }
}

Wire.endTransmission();//sempre finalizar a transmissão

Essa é a regra; após inicializar o barramento, inicializa a transmissão, faz a requisição, lê os dados requeridos e finaliza a transmissão. Simples assim.

Quando se tratar de um dispositivo I²C sem registradores como por exemplo, o PCF8574, pode-se fazer da seguinte maneira:

byte readPCF7485(int PCF8574_ADDR) {
     byte val;
     Wire.beginTransmission(PCF8574_ADDR);
     Wire.requestFrom(PCF8574_ADDR, 1);
     b = Wire.read();
     Wire.endTransmission();
     return b;
}

Dispositivos que possuem registradores (como o MPU6050) necessitam de indicação do endereço do registrador a ser lido. Isso veremos de agora em diante nesse artigo.

Ler registrador do MPU6050

Não dá pra adivinhar o que deve ser lido em nenhum desses dispositivos com registradores. Para utilizá-los da maneira mais simples, você precisa de uma biblioteca pronta, mas nem sempre você encontrará bibliotecas prontas, pois isso depende da MCU/CPU e no caso do ESP8266, depende também do firmware que estiver sendo utilizado. Como no artigo de hoje estou utilizando o Sming como framework C++ para programar o ESP e consequentemente estou utilizando meu próprio firmware, preferi não utilizar uma biblioteca de Arduino (sim, elas são compatíveis com Sming). Mas sim, você pode utilizar, deve ser compatível, se houver alguma. Isso porque a biblioteca Wire me parece ter um funcionamento muito parecido e isso é tudo o que você precisa para fazer a comunicação I²C.

No datasheet está disposta a seguinte informação sobre a leitura do MPU6050:

i2c-mpu6050-read-300x231.webp

Na tabela ao final da imagem, estão as descrições de cada um dos comandos na seqüência de leitura, mas isso não é o suficiente para saber o que fazer. O documento supracitado (relacionado ao mapa de registradores) é fundamental para saber o que fazer. Vou citar as partes que considero importantes.

PWR_MGMT_1

Esse é o registrador 107, ou 0x6B em hexadecimal. Os registradores do MPU6050 tem 8 bits e esse registrador em específico controla o comportamento do MPU6050. Os bits relacionados são:

pwr_mgmt_1.webp

Não é fundamental que você saiba interpretar datasheet, uma vez que terá o código disponível aqui, mas apenas para efeito de informação, lemos o registrador da direita para a esquerda, pois trata-se de binário.

Como você pode ver, os valores do registrador estão dispostos em hexadecimal e em decimal e você pode utilizar qualquer uma das bases para escrever para o dispositivo. Anota-se então o registrador e inicia-se a configuração dos bits. Mas esse registrador é fácil demais para configurar, considerando que você quererá mantê-lo permanentemente ligado. Para tal, todos os bits devem ser configurados para o valor 0. Se por acaso você quisesse desabilitar o sensor de temperatura, bastaria ajustar o respectivo bit para 1.Os primeiros 3 bits são relacionados ao clock do dispositivo e deixando esses 3 bits em 0, ele utiliza o oscilador interno de 8MHz.

O bit seguinte é o sensor de temperatura, que para ser desligado receberia o bit 1. Até aqui você tem o valor 0b1000, que representa 8 em hexa ou em decimal. O próximo bit é o ciclo, utilizado quando você quer fazer wake up e sleep continuamente, com leituras dentro de uma certa freqüência. O próximo bit é o sleep, que para manter o dispositivo ligado continuamente deve ser configurado para 0. Para o caso do uso cíclico, ajustado para 1. O último bit é o reset, que devolve todos os valores padrão para os registradores internos, de modo que devemos então deixá-lo em 0. Se fosse utlilizar o modo cíclico, o registrador 108 também precisaria ser configurado. É um pouquinho de trabalho, mas é só seguir as especificações do mapa de registradores e tudo acaba bem. No nosso caso, queremos apenas que o dispositivo envie suas informações, sem nos preocuparmos com a questão de gerenciamento de energia,por isso configuramos todos os bits para 0. A configuração inicial do MPU6050 fica assim:

void init_mpu6050(){
  //inicia a transmissao no barramento i2c
  Wire.beginTransmission(MPU6050_ADDR); 
  //PWR_MGMT_1 - registrador de gerenciamento de energia
  Wire.write(107);
  //configura os bits para 0
  Wire.write(0);
  //finaliza a transmissao
  Wire.endTransmission(true);
}

Essa inicialização do dispositivo é fundamental porque ele inicia em sleep.

Validar o dispositivo no barramento é uma boa opção para debugging, mas só nesse primeiro momento. Eu mantenho o código normalmente, porque fica fácil saber assim se o dispositivo não está mais respondendo no barramento e desse modo considerar que seja um mal contato, por exemplo.

int mpu6050_status(){
    /* Verifica se o dispositivo responde em MPU6050_ADDR*/
    Wire.beginTransmission(MPU6050_ADDR); 
    int error = -1; 
    error = Wire.endTransmission(true);
    if (error == 0){
        Serial.println("Dispositivo Ok!");
        return 0;
    }
    return -1;
}

Como você pode reparar, a função endTransmission(bool) retorna o sucesso ou falha da finalização da comunicação. Sendo diferente de 0, o dispositivo não está presente. E esse recurso é interessante, porque dá pra fazer um scanner de barramento para dispositivos I²C que você não saiba qual é o endereço. Claro, eu fiz, não resisti:

int scan(){
    int error;
    for (int i=1;i<127;i++){
        Serial.print("Endereco: ");
        Serial.println(i);
        Wire.beginTransmission(i);
        error = Wire.endTransmission(true);
        Serial.print("Codigo de retorno: ");
        Serial.println(error);
        WDT.alive();
        if (error == 0){
            return i;
        }
    }
    return 0;
}

Você pode ter até 126 dispositivos no mesmo barramento (2 MPU6050 no máximo). Supondo que você esteja utilizando apenas 1 dispositivo I²C e quer descobrir o endereço, basta rodar esse scan e ele retornará assim que o dispositivo for encontrado. Ou 0 se não for.

Contratempos e esquisitíces

Eu sugiro que você dê uma pesquisada no google e procure mais códigos de exemplo. Eu procurei um código pra me basear, encontrei alguns (uns, cópias de outros), mas nenhum deles funcionou pra mim. Não é possível que não funcionem em algum caso, pois tem até print da tela de leitura, mas sério, não funcionou pra mim. O que fiz no final foi realmente ler o datasheet de especificações do produto e o datasheet do mapa de registradores, então implementei tudo do zero. Ainda assim tive problemas, porque o dispositivo estava dormindo mesmo após enviar o byte zerado para o registrador de gerenciamento de energia, logo, eu posso ter me equivocado e meu código não está bonito, mas funciona! Também postei a situação anterior na wiki do Sming, mas nesse meio tempo consegui fazer funcionar com uma coisa que considero gambiarra, que é pedir pro dispositivo enviar o WHO_I_AM a cada chamada. Pra concluir a ideia, me ocorreu que um reset em todos os registradores tenha me auxiliado na reconfiguração do dispositivo, porque o comportamento dele se altera conforme a ordem que você configura os bits. Mas isso vou testar em outro módulo igual que tenho aqui e cito em outra oportunidade.

Em relação às esquisitices, bem, se você reparar no código, estou pegando um valor a cada 5 segundos e esse tempo vai aumentar no meu projeto (que não posso contar o que é). Nunca antes eu havia pensado em utilizar um dispositivo de tempo real para fazer leitura periódica, mas isso se dá pelo fato da estatística envolvida no meu projeto, cujos valores serão utilizados no modo raw. Não repare, é pra ser maluco mesmo. Além disso, note que ajustei a precisão do dado raw, porque isso é tudo o que preciso. Com certeza esse é outro comportamento atípico. O resultado dessa fase inicial (que ainda é depuração) é esse:

mpu6050-showing_raw_data-183x300.webp

E o código para obter o resultado exibido é esse:

#include <user_config.h>
#include <SmingCore/SmingCore.h>

//endereço i2c
#define MPU6050_ADDR 0x68
#define WHO_I_AM     0x75
#define SCALE        1000.0

//variaveis para armazenamento das leituras
int accelX        = 0;
int accelY        = 0;
int accelZ        = 0;
int girosX        = 0;
int girosY        = 0;
int girosZ        = 0;
int temp          = 0;
float temperature = 0.0;

float sensors[7] = {0};

//execucao da coleta de dados via timer
Timer getData;

/* O uso de delay nao eh recomendado porque pode gerar reset por WDT. Utilizando
 * a funcao millis() a CPU fica livre pra executar todos os processos de 
 * background sem interrupcoes.
 */
void sleep(int t){
    long start = 0;
    long now  = 0;
    int  delta = 0;
    
    start = millis();
    while (delta < t){
        now   = millis();
        delta = now - start;
        WDT.alive();
    }
}

//leitura do registrador do MPU6050
void mpu6050_read(){
    Wire.beginTransmission(MPU6050_ADDR);
    //para pegar o MSB (bit 8 a 15)
    Wire.write(0x3B);
    Wire.endTransmission(false);
    //requisita os dados e finaliza a transmissao
    Wire.requestFrom(MPU6050_ADDR,14,true);
    /* O MPU6050 tem um sensor de temperatura. Seu valor esta sendo lido acima,
     mas ainda eh necessario aplicar uma pequena formula para converter o valor
     para temperatura legivel em graus Celsius. Isso esta descrito na pagina 31
     do documento contendo o mapa de registradores. O link esta no artigo.
     Os enderecos do acelerometro estao na pagina 30.
     Os enderecos da temperatura estao na pagina 31.
     * Os enderecos do gyroscopio estao na pagina 32.
     * MSB representa os 8 bits mais signifcantes (Most Significant Bits)
     * LSB representa os 8 bits menos significantes (Least Significant Bits)
     * Abaixo, le um byte e desloca ele para a esquerda, entao concatena o
     * proximo byte.
     */
    accelX  = Wire.read()<<8;
    accelX |= Wire.read();   
    accelY  = Wire.read()<<8;
    accelY |= Wire.read(); 
    accelZ  = Wire.read()<<8;
    accelZ |= Wire.read();  
    temp    = Wire.read()<<8;
    temp   |= Wire.read();  
    girosX  = Wire.read()<<8;
    girosX |= Wire.read();  
    girosY  = Wire.read()<<8;
    girosY |= Wire.read();  
    girosZ  = Wire.read()<<8;
    girosZ |= Wire.read();  
    
    temp        = temp/340.0 + 36.53;
    //temperature = temp/10.0;
    
    sensors[0]  = accelX/SCALE;
    sensors[1]  = accelY/SCALE;
    sensors[2]  = accelZ/SCALE;
    sensors[3]  = girosX/SCALE;
    sensors[4]  = girosY/SCALE;
    sensors[5]  = girosZ/SCALE;
    sensors[6]  = temp/10.0;
}

/* PWR_MGMT_1
 * Esse eh o registrador 107 (ou 0x6B em hexa), descrito na pagina 48 do mapa
 * de registradores. A configuracao inicial eh bastante simples, e os bits estao
 * descritos no artigo.
 * 
 */
void init_mpu6050(){
    int regs[] = {107,0x1B,0x1C};
    for (int i=0;i<3;i++){
        Wire.beginTransmission(MPU6050_ADDR);
        Wire.write(regs[i]);
        Wire.write(0);
        Wire.endTransmission(true);
        sleep(100);
    }
//    Wire.beginTransmission(MPU6050_ADDR);
//    Wire.write(108);
//    Wire.write(3);
//    Wire.endTransmission(true);
}

void whoIam(){
    Wire.beginTransmission(MPU6050_ADDR);
        Wire.write(WHO_I_AM);
        Wire.requestFrom(MPU6050_ADDR,1,true);
        int identity = Wire.read();
        Serial.print("Identificacao: ");
        Serial.println(identity);
}

int scan(){
    int error;
    for (int i=1;i<127;i++){
        Serial.print("Endereco: ");
        Serial.println(i);
        Wire.beginTransmission(i);
        error = Wire.endTransmission(true);
        Serial.print("Codigo de retorno: ");
        Serial.println(error);
        WDT.alive();
        if (error == 0){
            return i;
        }
    }
    return 0;
}

int mpu6050_status(){
    /* Verifica se o dispositivo responde em MPU6050_ADDR*/
    Wire.beginTransmission(MPU6050_ADDR); 
    int error = -1; 
    error = Wire.endTransmission(true);
    if (error == 0){
        Serial.println("Dispositivo Ok!");
        return 0;
    }
    return -1;
}

void enableInterrupt(int threshold, int axis){
    //ativa interrupcao
    Wire.beginTransmission(MPU6050_ADDR);     
    Wire.write(56);
    Wire.write(0b01000000);         
    Wire.endTransmission(true);
  
    //gatilho
    Wire.beginTransmission(MPU6050_ADDR);     
    Wire.write(0x1F);
    Wire.write(threshold); //1 eh o minimo         
    Wire.endTransmission(true);
}

void showValues(){
    Serial.print("Acelerometro (X): ");
    Serial.println(sensors[0]);
    Serial.print("Acelerometro (Y): ");
    Serial.println(sensors[1]);
    Serial.print("Acelerometro (Z): ");
    Serial.println(sensors[2]);
    Serial.print("Giroscopio (X): ");
    Serial.println(sensors[3]);
    Serial.print("Giroscopio (Y): ");
    Serial.println(sensors[4]);
    Serial.print("Giroscopio (Z): ");
    Serial.println(sensors[5]);
    Serial.print("Temperatura: ");
    Serial.println(sensors[6]);
}

/* Essa funcao serve apenas para verificar se o MPU6050 saiu do modo sleep.*/
int isSleeping(){
  int val;
  Wire.beginTransmission(MPU6050_ADDR);     
  Wire.write(0x6B);                       //registro do gerenciamento de energia                     
  Wire.endTransmission(false);            //nao finalizar anter de ler         
  Wire.requestFrom(MPU6050_ADDR, 1,true); //le um byte  
  val = Wire.read();                      //le a resposta
  return val;                          
}

void mpu6050_getData(){
    whoIam();
    accelX = 0;
    int t = Wire.available();
    //Serial.print("Available? ");
    //Serial.println(t);
    while (accelX == 0){
        mpu6050_read();
        showValues();
    }
    int stat = -1;
    stat = isSleeping();
    if (stat != 64){
        //Serial.print("Dormindo como um bebe... (");
        //Serial.print(stat);
        //Serial.println(")");
        int _null = 0;
    }
    //whoIam();
}

void init()
{
    Serial.begin(115200);
    //inicia o barramento
    Wire.pins(12,14);
    Wire.begin();
    sleep(100);
    int isThereOne = scan();
    
    //valida o dispositivo
    int found;
    found = mpu6050_status();
    if (found != 0){
        Serial.println("Dispositivo nao detectado. Abortando");
        Serial.println(found);
        return;
    }
    
    Serial.println("inicializando MPU6050");
    //inicia o MPU6050
    init_mpu6050();
    sleep(100);
    
    //faz leituras periodicas
    Serial.println("Inicializando timer para 5 segundos");
    getData.initializeMs(5000, mpu6050_getData).start(); 
}

O código não está organizado, mas fica por sua conta. Eu já comecei arrumar o meu aqui, divirta-se também!

Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.