CJMCU-008 (HSCDTD008A) Magnetometer Module, extracting data sucess

I received a CJMCU-008 Magnetometer module.
My module contained a HSCDTD008A Chip (Caution :- not an AK09911C as advertised in the ebay listing)
Here is my first stab at retrieving data out of the chip.
The code sets up the chip for duty and then extracts/prints the complete registry.

#include <Wire.h> //I2C Arduino Library
#define addr 0x0C //I2C Address 
int x;
void setup() {
  // put your setup code here, to run once:
Serial.begin(9600);
Wire.begin(); 
Wire.beginTransmission(addr); //configure to Active mode,100Hz 
Wire.write(0x1B);
Wire.write(0x9A);
Wire.endTransmission();
Wire.beginTransmission(addr); //enable Data Ready with  ACTIVE HIGH control
Wire.write(0x1C);
Wire.write(0x0C);
Wire.endTransmission();

}

void loop() {
  // put your main code here, to run repeatedly:
Wire.beginTransmission(addr); //Poll device for measurement start (returns to 0 after Poll)
Wire.write(0x1D);
Wire.write(0x40);
Wire.endTransmission();
Wire.beginTransmission(addr); // Read all the registers out from 0x00
Wire.write(0x00);
Wire.endTransmission();
Wire.requestFrom(addr, 0x32);

if (0x32 <= Wire.available()) {
     for (int i=0; i <= 0x31; i++){
      x = Wire.read();Serial.print("<");Serial.print(i,HEX);Serial.print(">");Serial.print(x,HEX);Serial.print(">");Serial.println(x,BIN);
      delay(100);
   } 
Serial.println();
 
}

}

The actual raw magnetometer data is between 0x10 - 0x15

<00>00>0
<01>00>0
<02>00>0
<03>00>0
<04>00>0
<05>00>0
<06>00>0
<07>00>0
<08>00>0
<09>00>0
<0A>00>0
<0B>00>0
<0C>55>01010101     Self Test response
<0D>11>00010001      More info Version
<0E>15>00010101      More info Alps
<0F>49>01001001      Who am I
<10>7D>01111101      Mag X LSB
<11>FF>11111111      Mag X MSB
<12>25>00100101      Mag Y LSB
<13>00>00000000      Mag Y MSB
<14>67>01100111      Mag Z LSB
<15>FE>11111110      Mag Z MSB
<16>00>0                  
<17>00>0    
<18>00>0                Staus
<19>01>00000001     FIFO pointer
<1A>00>0
<1B>9A>10011010    Control 1
<1C>0C>00001100    Control 2
<1D>00>00000000    Control 3      Bit-0 Start calibrate Offset !!
<1E>80>10000000    Control 4
<1F>00>0
<20>00>0                Offset X LSB   maybe calibration values!!!
<21>00>0                Offset X MSB
<22>00>0                Offset Y LSB
<23>00>0                Offset Y MSB
<24>00>0                Offset Z LSB
<25>00>0                Offset Z MSB
<26>00>0
<27>00>0
<28>00>0
<29>00>0
<2A>00>0
<2B>00>0
<2C>00>0
<2D>00>0
<2E>00>0
<2F>00>0
<30>00>0
<31>19>00011001  Temperature

Now its time to convert to Heading…
. any help here would be welcome :slight_smile:

For best results, and perhaps even for useful results, you will need to calibrate the magnetometer. The built in procedure probably won't work well, as all it does is estimate the offsets.

There is a good overview of the calibration process here.

Yes indeed there is a calibrate mode :-
0x1D bit zero to 1=Action "Start to Calibrate Offset in Active Mode"

my present guess is that the calibration data is then stored to locations 0x20 to 0x25

However there is little documentation to indicate what you have to do in calibration mode....
....Wild guess ... wave it around in the breeze.

my present guess

The register locations and contents are very clearly described in the data sheet.

Appears that the Offset registers are only good for slewing the values (i.e. to account for I guess local magnetic fields/etc bending the signal… my guess is you blip this calibrate offset one time during setup up and it centres your data values)

First working Heading code :-

// Deployed onto an EPS32
#include <Wire.h> //I2C Arduino Library
#define addr 0x0C //I2C Address 
int16_t x;
int16_t y;
int16_t z;
int scale =1.0;
void setup() {
  // put your setup code here, to run once:
Serial.begin(9600);
Wire.begin(21,23);  // these are form my application on an ESP32 else simply use Wire.begin();
Wire.beginTransmission(addr); // Set up the registers
Wire.write(0x1B);
Wire.write(0x98); //0x9A
Wire.endTransmission();
Wire.beginTransmission(addr); 
Wire.write(0x1C);
Wire.write(0x0C);
Wire.endTransmission();
Wire.beginTransmission(addr);
Wire.write(0x1E);
Wire.write(0x90);
Wire.endTransmission();
}

void loop() { 
Wire.beginTransmission(addr); 
Wire.write(0x10);
Wire.endTransmission();
Wire.requestFrom(addr, 6);

if (6 <= Wire.available()) {
x = Wire.read(); x |= Wire.read()<<8; x=x*scale;
y = Wire.read(); y |= Wire.read()<<8; y=y*scale;
z = Wire.read(); z |= Wire.read()<<8; z=z*scale;
 } 
float bearing=((atan2(y,x))*180)/PI;//values will range from +180 to -180 degrees
 if (bearing < 0)  { bearing = 360 + bearing; }

// Show Values
Serial.print(" Bearing:> "); Serial.println(bearing);
delay(300);
}
 Bearing:> 6.34
 Bearing:> 59.42
 Bearing:> 60.26
 Bearing:> 64.20
 Bearing:> 72.50
 Bearing:> 79.81
 Bearing:> 87.29
 Bearing:> 93.47
 Bearing:> 97.64
 Bearing:> 101.49
 Bearing:> 110.94
 Bearing:> 120.78
 Bearing:> 129.08
 Bearing:> 137.59
 Bearing:> 145.57
 Bearing:> 159.95
 Bearing:> 170.33
 Bearing:> 181.07
 Bearing:> 187.06
 Bearing:> 197.99
 Bearing:> 207.37
 Bearing:> 157.77
 Bearing:> 162.83
 Bearing:> 168.04
 Bearing:> 189.90
 Bearing:> 201.39
 Bearing:> 227.73
 Bearing:> 252.60
 Bearing:> 289.65
 Bearing:> 323.91
 Bearing:> 354.47

Scale factor seems to be pretty good at 1.0!!!

As long as you avoid mathematical difficulties like numerical overflow and zero, it doesn't matter what value you choose for that overall scale factor.

It does matter that you use the correct offsets in X, Y and Z and the correct relative scale factors that put all sensor axes on the same scale.

Hey all,

I too was seduced by these Chinese modules. When they arrived I looked for info on how to use them with Arduino (and by extension in my case PlatformIO) and got to this page. It already offered the crucial info on what component is being used and hence led me to the datasheet.

Unfortunately the datasheet isn’t very clear on how to correctly initialize the component. So, I continued trying the code mentioned above and … it did something, but the values returned were completely skewed. I never got any value above 3 (so most values were negative) and often some of the axis would return completely bogus values (0 or -16384).

Ah … and although the datasheet mentioned the I2C address would always be 0x0C, mine is at 0x0F - so probably it’s a clone. The identification registers however do identify it as the HSCDTD008A, so maybe it’s just a custom batch …

So, with the axis skewed so much to the negative end, my first guess was that somehow I really would require some form of calibration, but how? Note that I was not interested in using it as a compass, but rather to measure the magnetic field created by the internals of a water meter which has no other way to trigger a digital pulse. Anyway, I tried some stuff - breaking one of my sensors by using a magnet a bit too close to the sensor - so close that it attracted the pins and shorted out stuff …

Anyway, the end result was positive for my purpose at least. I nowhere want to claim that this offers decent calibration or even that I would understand what it actually does, but with this extra calibration step at least my sensors are now working very well for my purpose. And since the articles here have helped me out several times, the least I can do is supply my code back and hope it might help someone else … So, below C++ class can be used like:

HSCDTD008A  compas(Wire);

void setup(void) {
  ...
  Wire.begin();
  Wire.status(); // To clear the I2C bus after a recent while slave was responding
  compas.begin(false);
  compas.calibrate();
  ...
}

void loop() {
    ...
    if (compas.measure()) {
      int16_t x = compas.x();
      int16_t y = compas.y();
      int16_t z = compas.z();
      ...
    }
    ...
}

The HSCDTD008A.h header file I use with PlatformIO (on an ESP8266). With some minor changes (like removing the Arduino.h include) I would expect it also works in the normal Arduino IDE.

#pragma once

#include <Arduino.h>
#include <Wire.h>

class HSCDTD008A {
private:
  TwoWire&      _twi;
  const uint8_t _addr;
  int16_t       _x, _y, _z;

public:
  HSCDTD008A(TwoWire& twi = Wire, uint8_t addr = 0x0F) : _twi(twi), _addr(addr), _x(0), _y(0), _z(0) {}

  inline bool begin(bool initTwi = true) {
    if (initTwi) _twi.begin();
    _twi.beginTransmission(_addr);
    _twi.write(0x1B);        //  Control1
    _twi.write(0b10001010);  //  Active Mode, Force State
    if (_twi.endTransmission()) return false;
    _twi.beginTransmission(_addr);
    _twi.write(0x1C);        //  Control2
    _twi.write(0b00011100);  //  DRDY high + FIFO
    if (_twi.endTransmission()) return false;
    _twi.beginTransmission(_addr);
    _twi.write(0x1D);        //  Control3
    _twi.write(0b00000000);  //  Nothing special
    if (_twi.endTransmission()) return false;
    _twi.beginTransmission(_addr);
    _twi.write(0x1E);        //  Control4
    _twi.write(0b10000000);  //  14bit resolution
    if (_twi.endTransmission()) return false;
    return true;
  }

  inline bool calibrate() {
    //  Ensure we are in active force state ...
    _twi.beginTransmission(_addr);
    _twi.write(0x1B);        //  Control1
    _twi.write(0b10001010);  //  Active Mode, Force State
    if (_twi.endTransmission()) return false;

    //  Now trigger temperature calibration and
    //  wait for it to finish.
    _twi.beginTransmission(_addr);
    _twi.write(0x1D);        //  Control3
    _twi.write(0b00000010);  //  Start Temp Calibration
    if (_twi.endTransmission()) return false;
    
    //  Wait a bit and then check whether done ...
    for (;;) {
      delay(1);
      _twi.beginTransmission(_addr);
      _twi.write(0x1D);
      if (_twi.endTransmission()) return false;
      _twi.requestFrom(_addr, 1UL, true);
      if (_twi.available() >= 1) {
        if (!(_twi.read() & 0b00000010)) break;
      }
    }

    //  Now basically the same thing, but for the offset
    //  calibration ...
    _twi.beginTransmission(_addr);
    _twi.write(0x1D);        //  Control3
    _twi.write(0b00000001);  //  Start Offset Calibration
    if (_twi.endTransmission()) return false;
    
    //  Wait a bit and then check whether done ...
    for (;;) {
      delay(1);
      _twi.beginTransmission(_addr);
      _twi.write(0x1D);
      if (_twi.endTransmission()) return false;
      _twi.requestFrom(_addr, 1UL, true);
      if (_twi.available() >= 1) {
        if (!(_twi.read() & 0b00000001)) break;
      }
    }

    return true;
  }

  inline bool measure() {
    bool success = false;

    //  Check whether there is data ready ...
    _twi.beginTransmission(_addr);
    _twi.write(0x18);
    if (_twi.endTransmission()) return false;
    _twi.requestFrom(_addr, 1UL, true);
    if (_twi.available() >= 1) {
      if (_twi.read() & 0b01000000) {
        //  There is data pending ... Updating our
        //  coordinates ...
        _twi.beginTransmission(_addr);
        _twi.write(0x10);
        if (_twi.endTransmission()) return false;
        _twi.requestFrom(_addr, 6UL, true);
        if (_twi.available() >= 6) {
          _x = (int16_t)(_twi.read() | (((uint16_t)_twi.read()) << 8));
          _y = (int16_t)(_twi.read() | (((uint16_t)_twi.read()) << 8));
          _z = (int16_t)(_twi.read() | (((uint16_t)_twi.read()) << 8));
          success = true;
        }
      }
    }

    //  Trigger a force mode read if not still active ...
    _twi.beginTransmission(_addr);
    _twi.write(0x1D);        //  Control3
    if (_twi.endTransmission()) return success;
    _twi.requestFrom(_addr, 1UL, true);
    if (_twi.available() >= 1) {
      if (!(_twi.read() & 0b01000000)) {
        _twi.beginTransmission(_addr);
        _twi.write(0x1D);        //  Control3
        _twi.write(0b01000000);
        _twi.endTransmission();
      }
    }
    return success;
  }

  inline int16_t x() { return _x; }
  inline int16_t y() { return _y; }
  inline int16_t z() { return _z; }
};