quarta-feira, dezembro 25, 2019

Módulo Bússola com HMC5883L ou QMC5883L

Mais uma vez minha memória me prega peças... Comecei a escrever este post supondo que era a primeira vez que mexia com uma  "bússola eletrônica" (ou magnetômetro), mas já fiz alguns posts sobre isso em 2015! Peço desculpas pelas repetições de algumas explicações.

O módulo que vamos ver aqui eu adquiri na FilipeFlop, mas módulos semelhantes são achados em todos os botequins que vendem Arduino. É um módulo de bússola com três eixos (já explico), com interface I2C. Na hora de colocar para funcionar surgiram algumas questões interessantes (ou frustrantes, dependendo do ponto de vista).


HMC5883L ou QMC5838L

Como mostrado na foto, a placa tem a indicação HMC5883L. Porém o site da FilipeFlop aponta para um datasheet do QMC5883L. Inicialmente ignorei isto e fui direto procurar informações sobre o HMC5883L; os links que eu encontrei estão listados no final, o google não sugeriu os meus posts :(. Apesar de encontrar várias informações úteis, as primeiras tentativas de uso do módulo foram um fracasso.

Indo direto para o importante: o módulo que testei usa o QMC5883L que tem diferenças cruciais em relação ao HMC5883L:
  • Sem suporte a SPI
  • Endereço I2C diferente (0x0D para o QMC e 0x1E para o HMC)
  • Endereço (número) dos registradores diferentes (ver adiante)
  • No QMC os registradores de saída estão na ordem X low, X high, Y low, Y high, Z low e Z high. No HMC a ordem é X high, X low, Z high, Z low, Y high, Y low
  • O QMC tem um registrador Set/Reset Period no qual precisa ser escrito 1
  • Outras diferenças nos registradores de configuração e status
Some o fato do QMC ser chinês e já vai ter gente gritando "componente falso". Mas não é isto. O HMC5883L era fabricado pela Honeywell, mas saiu de linha em 2016. A QST licenciou o chip mas, sabe-se lá porque, decidiu fabricar com alterações.

O Chip Por Dentro

O diagrama abaixo, extraído do datasheet, dá uma ideia de como a coisa funciona. Temos três sensores magnéticos em posições ortogonais (ou seja, alinhados com três eixos). Um conversor analógico digital (ADC) ira converter a medida da intensidade magnética em um número, que é salvo nos registradores. Embora sejam três sensores, o chip tem só um ADC e um mux seleciona um sensor por vez (tudo isto é transparente para quem usa o chip).


Como dito, o mapeamento dos registradores é diferente entre o QMC e o HMC:


Obtendo a Direção do Norte

Os sensores fornecem um vetor que aponta para o Norte magnético da Terra. Para converter isto em uma direção (como a agulha de uma bússola tradicional) é preciso considerar como estão alinhados os três eixos do chip. Para facilitar, a placa tem impressa a direção dos eixos X e Y  (o eixo Z aponta da placa para cima). Supondo que a placa está paralela ao chão, vamos desprezar o eixo Z e usar a função arco tangente para determinar a direção do Norte magnético em relação ao eixo X indicado na placa.

Fazendo uns testes, reparei que os ângulos obtidos estavam meio estranhos. Listando os valores obtidos para X e Y à medida que girava o sensor, descobri que as faixas de valores não estão centradas em zero e são diferentes entre os dois eixos. Um processo de calibração (inspirado em informações encontradas na internet) determina as faixas e calcula offsets e escalas para deixar os valores centrados em zero e com a mesma amplitude de variação. O resultado é razoável, mas existe ainda uma não linearidade que precisaria ser compensada.

Um refinamento visto em alguns dos exemplos nos links é considerar a diferença entre o polo Norte geográfico e o polo Norte magnético. Isto é dado pela declinação magnética e pode ser consultada para a sua localidade neste site.

Montagem Para Teste

A montagem é trivial: alimentação, terra, SCL e SDA. O módulo permite alimentação a 3.3V ou 5V, eu liguei no 3.3V do Arduino. Como já vimos em outros sensores, SCL e SDA são para operação a 3.3V, mas vamos abusar um pouco e ligar direto no Arduino.


Aplicação de Teste

Nos artigos listados no final tem link para várias bibliotecas para o HMC5883L. Como é meu costume, preferi não usar biblioteca e interagir direto com o chip. O exemplo abaixo identifica se é um HMC ou QMC e se adapta a isto. Inicialmente é necessário proceder a um calibração, que consiste em girar o sensor em círculo por um minuto, para determinarmos a faixa de valores retornados.

#include <Wire.h>

// Classe simples para tratar a bússola
class Bussola {
  public:
    typedef enum { INDEFINIDO, HMC, QMC } TIPO;
    Bussola(TIPO tipo);
    bool inicia(void);
    TIPO getTipo(void);
    void setDeclination (int graus , int mins, char dir);
    float leDirecao(void);
    void iniciaCal();
    void encerraCal();

  private:
    static const int ender_HMC = 0x1E; // endereço I2C do HMC5883
    static const int regMODE_HMC = 2;  // registrador de modo
    static const int regXH_HMC = 3;    // primeiro registrador de dados
    static const int regST_HMC = 9;    // registrador de status
    
    static const int ender_QMC = 0x0D; // endereço I2C do QMC5883
    static const int regCR1_QMC = 9;   // registrador de configuração
    static const int regSR_QMC = 11;   // registador set/reset
    static const int regXL_QMC = 0;    // primeiro registrador de dados
    static const int regST_QMC = 6;    // registrador de status

    // fatores de correção determinados na calibração
    int16_t xMin, yMin, xMax, yMax;
    float escX = 1.0;
    float escY = 1.0;
    int16_t offX = 0;
    int16_t offY = 0;

    // Edereço e tipo do chip
    int ender;
    TIPO tipo;

    // Diferença entre o Polo Magnético e o Geográfico
    float declination = 0.0;

    // Rotina para disparar leitura dos dados
    void pedeDados(int regStatus, int regDados);
};

Bussola bussola(Bussola::INDEFINIDO);

// Iniciação do programa
void setup(){
  
  Wire.begin();
  Serial.begin(115200);
  if (!bussola.inicia()) {
    Serial.println ("Nao encontrou a bussola!");
    for (;;) {
      delay(100);
    }
  }
  Serial.print ("Achou bussola ");
  Serial.println (bussola.getTipo() == Bussola::QMC ?
      "QMC5883L" : "HMC5883L");

  Serial.println ("Calibrando... rode o sensor em um círculo");
  bussola.iniciaCal();
  long tmpFim = millis()+60000L;
  while (millis() < tmpFim) {
    bussola.leDirecao();
    delay(20);
  }
  bussola.encerraCal();
  Serial.println ("Calibrado");
}

// Laço principal
void loop(){
  float direcao = bussola.leDirecao();
  Serial.println (direcao);
  delay(1000);
}

// Construtor
Bussola::Bussola(TIPO tipo) {
  this->tipo = tipo;
}

// Inicia comunicação com a bússola
bool Bussola::inicia() {
  if (tipo == INDEFINIDO) {
    // Tenta identificar o chip
    Wire.beginTransmission(ender_HMC);
    if (Wire.endTransmission() == 0) {
      tipo = HMC;
      ender = ender_HMC;
    } else {
      Wire.beginTransmission(ender_QMC);
      if (Wire.endTransmission() == 0) {
        tipo = QMC;
        ender = ender_QMC;
      }
    }
  }

  // Inicia o chip para modo contínuo
  if (tipo == HMC) {
    Wire.beginTransmission(ender);
    Wire.write(regMODE_HMC);
    Wire.write(0x00);
    Wire.endTransmission();
  } else if (tipo == QMC) {
    Wire.beginTransmission(ender);
    Wire.write(regSR_QMC);
    Wire.write(0x01);
    Wire.endTransmission();
    Wire.beginTransmission(ender);
    Wire.write(regCR1_QMC);
    Wire.write(0x0D);
    Wire.endTransmission();
  }

  return tipo != INDEFINIDO;
}

// Informa o tipo de bússola
Bussola::TIPO Bussola::getTipo(void) { 
  return tipo; 
}

// Define a declinação (correção entre o Norte magnético e o Norte geofráfico)
// ver http://www.magnetic-declination.com/
void Bussola::setDeclination (int graus , int mins, char dir) {
  declination = (graus + mins/60.0) * PI / 180.0;
  if (dir == 'W') {
    declination = - declination;
  }
  Serial.println (declination);
}


// Le a direção da bússola em graus (0 a 360) em relação à marcação do eixo X
// Assume que a bússola esta na horizontal
float Bussola::leDirecao(void) {
  int16_t x, y, z;

  if (tipo == INDEFINIDO) {
    return 0.0;
  }

  // Le a intesidade do campo magnético
  if (tipo == HMC) {
    pedeDados (regST_HMC, regXH_HMC);
    x = Wire.read() << 8;     //MSB  x
    x |= Wire.read();         //LSB  x
    z = Wire.read() << 8;     //MSB  z
    z |= Wire.read();         //LSB  z
    y = Wire.read() << 8;     //MSB  y 
    y |= Wire.read();         //LSB  y
  } else if (tipo == QMC) {
    pedeDados(regST_QMC, regXL_QMC);
    x = Wire.read();          //LSB  x 
    x |= Wire.read() << 8;    //MSB  x
    y = Wire.read();          //LSB y
    y |= Wire.read() << 8;    //MSB y
    z = Wire.read();          //LSB  z
    z |= Wire.read() << 8;    //MSB z
  }

  // Registra mínimo e máximo para a calibração
  if (x < xMin) {
    xMin = x;
  }
  if (xMax < x) {
    xMax = x;
  }
  if (y < yMin) {
    yMin = y;
  }
  if (yMax < y) {
    yMax = y;
  }

  // corrige e calcula o angulo em radianos
  float xC = (x - offX) * escX;
  float yC = (y - offY) * escY;
  float angulo = atan2 (yC, xC) + declination;

  // Garante que está entre 0 e 2*PI
  if (angulo < 0) {
    angulo += 2.0*PI;
  } else if (angulo > 2*PI) {
    angulo -= 2.0*PI;
  }

  // Converte para graus
  return (angulo*180.0)/PI;
}

void Bussola::pedeDados(int regStatus, int regDados) {
    // Espera ter um dado a ler
    do {
      Wire.beginTransmission(ender);
      Wire.write(regStatus);
      Wire.endTransmission();
      Wire.requestFrom(ender, 1);
    } while ((Wire.read() & 1) == 0);

    Wire.beginTransmission(ender);
    Wire.write(regDados);
    Wire.endTransmission();
    Wire.requestFrom(ender, 6);
}


// Inicia processo de calibração
void Bussola::iniciaCal() {
  xMax = yMax = -32768;
  xMin = yMin = 32767;
}

// Encerra a calibração
void Bussola::encerraCal() {
  Serial.print ("X: ");
  Serial.print (xMin);
  Serial.print (" - ");
  Serial.println (xMax);
  Serial.print ("Y: ");
  Serial.print (yMin);
  Serial.print (" - ");
  Serial.println (yMax);
  
  // Offset para centralizar leituras em zero
  offX = (xMax + xMin)/2;
  offY = (yMax + yMin)/2;

  // Escala para ter a mesma variação nos dois eixos
  int16_t varX = xMax - xMin;
  int16_t varY = yMax - yMin;
  if (varY > varX) {
    escY = 1.0;
    escX = (float) varY / varX;
  } else {
    escX = 1.0;
    escY = (float) varX / varY;
  }
}

Links

Módulo na FilipeFlop:
https://www.filipeflop.com/produto/modulo-bussola-eletronica-hmc5883l/

Datasheet do HMC5883L:
https://cdn-shop.adafruit.com/datasheets/HMC5883L_3-Axis_Digital_Compass_IC.pdf

Datasheet do QMC5883L:
http://img.filipeflop.com/files/download/Datasheet-QMC5883L-1.0%20.pdf

Aviso de descontinuação do HMC5883L:
https://media.digikey.com/pdf/PCNs/Honeywell/HMC5883L,%20HMC5983%20EOL%20Update.pdf

Artigo 1:
https://www.dobitaobyte.com.br/magnetometro-hmc5883l-com-arduino-bussola/

Artigo 2:
https://www.usinainfo.com.br/blog/magnetometro-arduino-hmc5883l-projeto-pratico-com-display/

Artigo 3:
http://labdegaragem.com/profiles/blogs/tutorial-bussola-eletronica-com-hmc5883l

Biblioteca que fala em calibração:
https://github.com/jarzebski/Arduino-HMC5883L

Datasheet + Application Notes da Honeywell:
https://www.jameco.com/Jameco/Products/ProdDS/2150248.pdf


21/06/23: Por ato falho, em vários locais eu escrevi 5833 ao invés de 5883... corrigido!


2 comentários:

Anônimo disse...

Parabéns pela sabedoria em divulgar de uma forma simples e completa o funcionamento desses 5883, revirei a internet sobre o assunto e você foi o único que consegui enterder. obrigado por disponibilizar um pouco do seu conhecimento.

Anônimo disse...

Perfeito seu sketch!!! Fiz várias tentativas para realizar a calibração e fiz várias pesquisas, porém sem sucesso. Agradeço muito a sua contribuição.