このICってどうなっているのだ

ESP32でI2C接続: QMC5883L(HMC5883L)を使ってみる でひとまずデータを取ることができるようになったのだが、 このICの仕組みはどうなっているのだろう。 ということで仕様書を見ながら実装をしてみる。 今回は仕様書の最初に載っている方法を実装する。

参考:

連続測定モード

7.1 Continuous Mode Setup Example

  • Write Register 0BH by 0x01 (Define Set/Reset period)
  • Write Register 09H by 0x1D (Define OSR = 512, Full Scale Range = 8Gauss, ODR = 200Hz, set continuous measurement mode)

Addr. 7 6 5 4 3 2 1 0 Access
00H Data Output X LSB Register XOUT[7:0] Read only
01H Data Output X MSB Register XOUT[15:8] Read only
02H Data Output Y LSB Register YOUT[7:0] Read only
03H Data Output Y MSB Register YOUT[15:8] Read only
04H Data Output Z LSB Register ZOUT[7:0] Read only
05H Data Output Z MSB Register ZOUT[15:8] Read only
06H DOR OVL DRDY Read only
07H TOUT[7:0] Read only
08H TOUT[15:8] Read only
09H OSR[1:0] RNG[1:0] ODR[1:0] MODE[1:0] Read/Write
0AH SOFT_RST ROL_PNT INT_ENB R/W, Read only on blanks
0BH SET/RESET Period FBR [7:0] Read/Write
0CH Reserved Read only
0DH Chip ID Read only

Example として最初に載っていたのが Continuous Mode Setup Example だった。連続測定モードらしい。

この記述を見る限り、アドレス 0x0b0x01 を書き込み、 0x090x1d を書き込んでしまえばとりあえずは動くらしい。 この設定がひとまず普通の動作を提供するのだろうか。

そしてレジスターのアドレス表を見ると 0x01 から 0x05 までに計測データが入っているらしい。

上記の設定を書き込んだ後、ここを読み出すようにすれば方角によって値が変化するのが確認できるらしいので、 書き込んでから連続で読み込むまでを実装してみる。


#include <Wire.h>
#include <string>
#include <sstream>
#include <iomanip>

const uint8_t ADDRESS = 0x0d;
const uint8_t X = 0x00;
const uint8_t Y = 0x02;
const uint8_t Z = 0x04;

typedef struct{
  byte msb;
  byte lsb;
} DATA;

std :: ostream& operator<< (std :: ostream& out, const uint8_t value);

inline const byte readData (const uint8_t _address, const uint8_t _register, DATA& _data);
inline void       printData(const std :: string& _prefix, const DATA& _data);


void setup() {

  Wire.begin();
  
  Serial.begin(115200);
  while( ! Serial);
  
  Serial.println("QML5883L raw compass data");



  // access 0x0b register and write 0x01
  uint8_t err;
  Wire.beginTransmission(ADDRESS);
  Wire.write(0x0b);
  Wire.write(0x01);
  err = Wire.endTransmission();
  if(err){
    Serial.print("error Set/Reset period FBR : ");
    Serial.println(err);
  }



  // access 0x09 register and write 0x1d
  Wire.beginTransmission(ADDRESS);
  Wire.write(0x09);
  Wire.write(0x1d);
  err = Wire.endTransmission();
  if(err){
    Serial.print("error set continuous measurement mode");
    Serial.println(err);
  }

}

void loop() {


  DATA x, y, z;

  byte err;
  std :: stringstream ss;

  // read X data
  err = readData(ADDRESS, X, x);
  if(err){

    ss << "X error : " << err;
    Serial.println( ss.str().c_str() );
    ss.str("");
  }

  // read Y data
  err = readData(ADDRESS, Y, y);
  if(err){
    ss << "Y error : " << err;
    Serial.println( ss.str().c_str() );
    ss.str("");
  }

  // read Z data
  err = readData(ADDRESS, Z, z);
  if(err){
    ss << "Z error : " << err;
    Serial.println( ss.str().c_str() );
    ss.str("");
  }


  // output measurement data
  printData("X", x);
  printData("Y", y);
  printData("Z", z);

  delay(2000);
}



//function for output measurement data
void printData(const std :: string& _prefix, const DATA& _data){

  std :: stringstream ss;

  ss << _prefix << " MSB = 0x" << std :: hex << _data.msb;
  Serial.println( ss.str().c_str() );
  ss.str("");

  ss << _prefix << " LSB = 0x" << std :: hex << _data.lsb;
  Serial.println( ss.str().c_str() );
  ss.str("");


}

//function for read measurement data
const byte readData(const uint8_t _address, const uint8_t _register, DATA& _data){


  byte err;

  std :: stringstream ss;

  //X LSB
  Wire.beginTransmission(_address);
  Wire.write(_register);
  err = Wire.endTransmission(false);

  if(err){

    ss << "error : 0x" << std :: setfill('0') << std :: setw(2) << std :: hex << _address;
    ss << ":0x"        << std :: setfill('0') << std :: setw(2) << std :: hex << _register;
    ss << " -> "       << err;

    Serial.println( ss.str().c_str() );
    ss.str("");

    return err;
  }
  Wire.requestFrom(_address, static_cast<uint8_t>(1));
  _data.lsb = Wire.read();
  Wire.endTransmission();
 


   //X MSB
  Wire.beginTransmission(_address);
  Wire.write(_register + 1);
  err = Wire.endTransmission(false);

  if(err){

    ss << "error : 0x" << std :: setfill('0') << std :: setw(2) << std :: hex << _address;
    ss << ":0x"        << std :: setfill('0') << std :: setw(2) << std :: hex << _register + 1;
    ss << " -> "       << err;

    Serial.println( ss.str().c_str() );
    ss.str("");

    return err;
  }

  Wire.requestFrom(_address, static_cast<uint8_t>(1) );
  _data.msb = Wire.read();
  Wire.endTransmission();

  return static_cast<byte>(0);

}


//extend std::stringstream operator for uint_8t
std :: ostream& operator<< (std :: ostream& out, const uint8_t value){
  out << static_cast<int>(value);
  return out;
}

いきなりとても長いが、ひとまず出力結果がこちら

QML5883L raw compass data
X MSB = 0x2
X LSB = 0x5
Y MSB = 0xff
Y LSB = 0x67
Z MSB = 0xfc
Z LSB = 0x10
X MSB = 0x1
X LSB = 0xca
Y MSB = 0xfd
Y LSB = 0x51
Z MSB = 0xfc
Z LSB = 0x8e
X MSB = 0x1
X LSB = 0xf
Y MSB = 0x0
Y LSB = 0xc3
Z MSB = 0xfc
Z LSB = 0x13
X MSB = 0xfd
X LSB = 0x89
Y MSB = 0xfe
Y LSB = 0xdd
Z MSB = 0xff
Z LSB = 0x8b
....

ぐるぐる回してみたが、ちゃんと反映されているようだ。

ソースコードについて

std :: ostream& operator<< (std :: ostream& out, const uint8_t value){
  out << static_cast<int>(value);
  return out;
}

std::stringstreamuint8_t をちゃんと扱ってくれないため、 uint8_tint へキャストする様に operator<< をオーバーロードしている。



//function for read measurement data
const byte readData(const uint8_t _address, const uint8_t _register, DATA& _data){

  //X LSB
  Wire.beginTransmission(_address);
  Wire.write(_register);
  err = Wire.endTransmission(false);

  if(err){

    ss << "error : 0x" << std :: setfill('0') << std :: setw(2) << std :: hex << _address;
    ss << ":0x"        << std :: setfill('0') << std :: setw(2) << std :: hex << _register;
    ss << " -> "       << err;

    Serial.println( ss.str().c_str() );
    ss.str("");

    return err;
  }
  Wire.requestFrom(_address, static_cast<uint8_t>(1));
  _data.lsb = Wire.read();
  Wire.endTransmission();



   //X MSB
  Wire.beginTransmission(_address);
  Wire.write(_register + 1);
  err = Wire.endTransmission(false);

  if(err){

    ss << "error : 0x" << std :: setfill('0') << std :: setw(2) << std :: hex << _address;
    ss << ":0x"        << std :: setfill('0') << std :: setw(2) << std :: hex << _register + 1;
    ss << " -> "       << err;

    Serial.println( ss.str().c_str() );
    ss.str("");

    return err;
  }

  Wire.requestFrom(_address, static_cast<uint8_t>(1) );
  _data.msb = Wire.read();
  Wire.endTransmission();

  return static_cast<byte>(0);
}

LSBとMSBという言葉は、ありがちですが、最後のBを文脈によってバイト(Byte)かビット(Bit)か読み分ける必要があるようです。 ビットと読んだ場合、

MSB(Most Significant Bit)
最上位ビット
LSB(Least Significant Bit)
最下位ビット

ということになります。 1を1バイトで表現したとき、ビット列が以下のように並んだと考えると、

(MSB)                   (LSB)
  0 0 0 0   0 0 0 1  

となります。上の桁の方がMSBです。

このICの出力データは16bitだが、各レジスタは8bitしかない。 なので上位バイトと下位バイトをそれぞれ別のレジスタから取得する。 実際に利用する時は、別々に取得したあと上位バイトを8bit分左シフトしたものと 下位バイトを合算させて用いる。

今回のソースコードはただ単純に下位バイトと上位バイトをそれぞれ別に取得、出力している。 上位バイトは必ず 下位バイトのアドレス + 1 となっているので、printData内部はこの法則を利用して上位バイトを取得している。

//function for output measurement data
void printData(const std :: string& _prefix, const DATA& _data){

  std :: stringstream ss;

  ss << _prefix << " MSB = 0x" << std :: hex << _data.msb;
  Serial.println( ss.str().c_str() );
  ss.str("");

  ss << _prefix << " LSB = 0x" << std :: hex << _data.lsb;
  Serial.println( ss.str().c_str() );
  ss.str("");


}

取得したデータを構造体から抽出して出力している。 今回、データを一時格納するために

typedef struct{
  byte msb;
  byte lsb;
} DATA;

という構造体を用いてみた。