This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

Problem Configuring AK8963 Magnetometer Using Tiva



Been completed most of the tasks in the Tiva TM4C Workshop that TI had kindly provided, controlled some motors using PWM and PID control and read sensors so I decided to jump in something "big" (for me apparently) - design a gimbaled system using IMU sensors. 

The microcontroller I'm using is EK-TM4123GXL Evaluation Launchpad, which I use one I2C module to interface with the MPU-9250 breakout board which I purchased from this link: world.tmall.com/.../42322982187.htm

The MPU-9250 (develops from 9150 model) from Invensense is a 9-DOF (accelerometer, gyrometer, magnetometer XYZ) IMU sensor for orientation measure. Actually it consists of a MPU-6050 which is known as a popular 6-DOF IMU sensor and an AK8963 3-axis magnetometer so the configuration work in fact can be split into 2 ICs separately. The only interface standard that MPU9250 can communicate with other controller is I2C. The connection is shown below:

Tiva                             MPU-9250

3v3                              VCC

GND                           GND

PA6 (I2C1-SCL)       SCL

PA7 (I2C1-SDA)       SDA

After hours of aquiring knowledge about measuring orientation angle (Roll, Pitch) using accelerometer data, correcting and de-noising these values using Kalman filter with gyrometer data I have achieved desired Roll and Pitch values from the MPU-6050 IC inside of MPU-9250. Next challenge will be acquiring raw magnetometer data from magnetometer and use another Kalman filter to estimate the Yaw angle (which, I've learned from internet, is such a pain because of noises). 

First thing to do, like with MPU-6050, is to understand the register map of AK8963, which is kindly provided by Invensense at this link: store.invensense.com/ProductDetail/MPU9250-InvenSense-Inc/487537/

I then define the necessary registers as shown below:

// Read-only Reg
#define AK8963_I2C_ADDR 0x0C // slave address for the AK8963
#define AK8963_DEVICE_ID 0x48
#define AK8963_WIA 0x00
#define AK8963_INFO 0x01
#define AK8963_ST1 0x02 // data ready status bit 0
#define AK8963_XOUT_L 0x03 // data
#define AK8963_XOUT_H 0x04
#define AK8963_YOUT_L 0x05
#define AK8963_YOUT_H 0x06
#define AK8963_ZOUT_L 0x07
#define AK8963_ZOUT_H 0x08
#define AK8963_ST2 0x09 // Data overflow bit 3 and data read error status bit 2
#define AK8963_CNTL 0x0A // Power down (0000), single-measurement (0001), self-test (1000) and Fuse ROM (1111) modes on bits 3:0
#define AK8963_ASTC 0x0C // Self test control
#define AK8963_I2CDIS 0x0F // I2C disable
#define AK8963_ASAX 0x10 // Fuse ROM x-axis sensitivity adjustment value
#define AK8963_ASAY 0x11 // Fuse ROM y-axis sensitivity adjustment value
#define AK8963_ASAZ 0x12 // Fuse ROM z-axis sensitivity adjustment value

// Write/Read Reg
#define AK8963_CNTL1 0x0A
#define AK8963_CNTL2 0x0B
#define AK8963_ASTC 0x0C
#define AK8963_TS1 0x0D
#define AK8963_TS2 0x0E
#define AK8963_I2CDIS 0x0F


// Read-only Reg ( ROM )
#define AK8963_ASAX 0x10
#define AK8963_ASAY 0x11
#define AK8963_ASAZ 0x12

It is noticeable that the address of AKB8963 is 0x0C (discriminate from MPU-6050 which is 0x68)

Then I reuse some of my old I2C read and write handlers (they're proofed to work well with the MPU-6050 config)

void i2cWriteData(uint8_t addr, uint8_t regAddr, uint8_t *data, uint8_t length)               // write data with arbitrary length
{
I2CMasterSlaveAddrSet(I2C_PORT, addr, false); // Set to write mode

I2CMasterDataPut(I2C_PORT, regAddr); // Place address into data register // The first to be sent
I2CMasterControl(I2C_PORT, I2C_MASTER_CMD_BURST_SEND_START); // Send start condition
while (I2CMasterBusy(I2C_PORT)); // Wait until transfer is done
uint8_t i = 0;
for (i = 0; i < length - 1; i++) { // Just send n-1 data after
I2CMasterDataPut(I2C_PORT, data[i]); // Place data into data register
I2CMasterControl(I2C_PORT, I2C_MASTER_CMD_BURST_SEND_CONT); // Send continues condition
while (I2CMasterBusy(I2C_PORT)); // Wait until transfer is done
}

I2CMasterDataPut(I2C_PORT, data[length - 1]); // Place data into data register // The last to be sent
I2CMasterControl(I2C_PORT, I2C_MASTER_CMD_BURST_SEND_FINISH); // Send finish condition
while (I2CMasterBusy(I2C_PORT)); // Wait until transfer is done
}


void i2cWrite(uint8_t addr, uint8_t regAddr, uint8_t data)                     // write 1-byte data (Read handlers are the same)
{
i2cWriteData(addr, regAddr, &data, 1);
}


uint8_t i2cRead(uint8_t addr, uint8_t regAddr)
{
I2CMasterSlaveAddrSet(I2C_PORT, addr, false); // Set to write mode

I2CMasterDataPut(I2C_PORT, regAddr); // Place address into data register
I2CMasterControl(I2C_PORT, I2C_MASTER_CMD_SINGLE_SEND); // Send data - just the regAddr
while (I2CMasterBusy(I2C_PORT)); // Wait until transfer is done

I2CMasterSlaveAddrSet(I2C_PORT, addr, true); // Set to read mode

I2CMasterControl(I2C_PORT, I2C_MASTER_CMD_SINGLE_RECEIVE); // Tell master to read data
while (I2CMasterBusy(I2C_PORT)); // Wait until transfer is done
return I2CMasterDataGet(I2C_PORT); // Read data
}

void i2cReadData(uint8_t addr, uint8_t regAddr, uint8_t *data, uint8_t length)
{
I2CMasterSlaveAddrSet(I2C_PORT, addr, false); // Set to write mode

I2CMasterDataPut(I2C_PORT, regAddr); // Place address into data register
I2CMasterControl(I2C_PORT, I2C_MASTER_CMD_SINGLE_SEND); // Send data
while (I2CMasterBusy(I2C_PORT)); // Wait until transfer is done

I2CMasterSlaveAddrSet(I2C_PORT, addr, true); // Set to read mode

I2CMasterControl(I2C_PORT, I2C_MASTER_CMD_BURST_RECEIVE_START); // Send start condition
while (I2CMasterBusy(I2C_PORT)); // Wait until transfer is done
data[0] = I2CMasterDataGet(I2C_PORT); // Place data into data register
uint8_t i = 1;
for (i = 1; i < length - 1; i++) {
I2CMasterControl(I2C_PORT, I2C_MASTER_CMD_BURST_RECEIVE_CONT); // Send continues condition
while (I2CMasterBusy(I2C_PORT)); // Wait until transfer is done
data[i] = I2CMasterDataGet(I2C_PORT); // Place data into data register
}

I2CMasterControl(I2C_PORT, I2C_MASTER_CMD_BURST_RECEIVE_FINISH); // Send finish condition
while (I2CMasterBusy(I2C_PORT)); // Wait until transfer is done
data[length - 1] = I2CMasterDataGet(I2C_PORT); // Place data into data register
}

So now I'm ready to config the AK8963. The "destination" array variable contains the constant factory-calibrated values that can be use to correct raw data from magnetometer). the AK8963_MAGNET_RES macro is defined 0 if using 14-bit ADC and 1 if using 16-bit ADC (in this case = 1) and the AK8963_MAGNET_FREQ = 0x02 shown that the frequency of magnetic measurement is 8 Hz.

void initAK8963(float * destination)
{
// First extract the factory calibration for each magnetometer axis
uint8_t rawData[3]; // x/y/z gyro calibration data stored here
i2cWrite(AK8963_I2C_ADDR, AK8963_CNTL, 0x00); // Power down magnetometer
SysCtlDelay(8000);
i2cWrite(AK8963_I2C_ADDR, AK8963_CNTL, 0x0F); // Enter Fuse ROM access mode
SysCtlDelay(8000);
i2cReadData(AK8963_I2C_ADDR, AK8963_ASAX, &rawData[0], 3); // Read the x-, y-, and z-axis calibration values
destination[0] = (float)(rawData[0] - 128)/256. + 1.; // Return x-axis sensitivity adjustment values, etc.
destination[1] = (float)(rawData[1] - 128)/256. + 1.;
destination[2] = (float)(rawData[2] - 128)/256. + 1.;
i2cWrite(AK8963_I2C_ADDR, AK8963_CNTL, 0x00); // Power down magnetometer
SysCtlDelay(8000);
// Configure the magnetometer for continuous read and highest resolution
// set bit 4 of AK8963_CNTL reg to 1 (0) to enable 16 (14) bit resolution in CNTL register,
// and enable continuous mode data acquisition frequency (bits [3:0]), 0010 for 8 Hz and 0110 for 100 Hz sample rates
i2cWrite(AK8963_I2C_ADDR, AK8963_CNTL, (AK8963_MAGNET_RES << 4) | AK8963_MAGNET_FREQ); // Set magnetometer data resolution and sample ODR
SysCtlDelay(8000);
}

Depend on the choice of AK8963_MAGNET_RES (14 or 16 bits) I calculate the resolution (like the "destination" above, the "Mres" variable should unchange).

void GetMres()
{
switch (AK8963_MAGNET_RES)
{
// Possible magnetometer scales (and their register bit settings) are:
// 14 bit resolution (0) and 16 bit resolution (1)
case 0:
mRes = 10.*4912./8190.; // Proper scale to return milliGauss
break;
case 1:
mRes = 10.*4912./32760.0; // Proper scale to return milliGauss
break;
}
}

Now it's the time to get the raw data (really the same as accelerometer and gyrometer):

void ak8963_Read_MagnetXYZ(int16_t *x, int16_t *y, int16_t *z)
{
uint8_t m[7]; // x/y/z gyro register data, ST2 register stored here, must read ST2 at end of data acquisition
if(i2cRead(AK8963_I2C_ADDR, AK8963_ST1) & 0x01) { // wait for magnetometer data ready bit to be set
i2cReadData(AK8963_I2C_ADDR, AK8963_XOUT_L, &m[0], 7); // Read the six raw data and ST2 registers sequentially into data array
int8_t c = m[6]; // End data read by reading ST2 register
if(!(c & 0x08)) { // Check if magnetic sensor overflow set, if not then report data
*x = ((int16_t)m[1] << 8) | m[0] ; // Turn the MSB and LSB into a signed 16-bit value
*y = ((int16_t)m[3] << 8) | m[2] ; // Data stored as little Endian
*z = ((int16_t)m[5] << 8) | m[4] ;
}
}
}

Now back to the main.c, I create the variables for the data and factory-calibrated value array

int16_t MagnetX, MagnetY, MagnetZ;

float magCalibration[3] = {0, 0, 0};

Then call the function (store the factory-calibrated values into magCalibration array):

initAK8963(magCalibration);

I create a 150ms interval timer to calculate the 'real' data after each time-out interrupt. These are the codes inside the interrupt handler (which is used to calculate Roll and Pitch Kalman filter at the same time)

int16_t MagnetX, MagnetY, MagnetZ;

extern float mRes;

extern float magCalibration[3];
double magX, magY, magZ;

ak8963_Read_MagnetXYZ(&MagnetX, &MagnetY, &MagnetZ);
GetMres();
magX = (double)MagnetX*mRes*magCalibration[0];
magY = (double)MagnetY*mRes*magCalibration[1];
magZ = (double)MagnetZ*mRes*magCalibration[2];

And the result is:

One of my friend used another scheme on this same module (to assure that none of them are malfunction) and produced non-zero value (which weren't proofed to be right at all). This is his result (notice that the factory-calibrated values, which should be the same, also differ from mine) (The "magCount" array values are equivalent to my MagnetX,Y,Z)

In case if needed, I provide the 1) Source code of 6 DOF Kalman filtered data (which worked); 2) The developing code to 9 DOF from 1); 3) The source code from my friend (with his approval to use) which produces 9 unfiltered data.

1)

6 DOF IMU Kalman.rar

2)

9 DOF IMU Kalman.rar

3)

Test.rar

Pardon me for any grammar issues, thank you very much. 

  • I wanted to memorialize the time & effort you've spent in composing this post - very good job!

    Yet - your issues land - by far - more heavily upon external devices - not your MCU nor API code. Teasing out full & proper results from (others') devices often proves a challenge. (i.e. "finger-pointing" is a predictable result - offering little comfort to you.)

    It's great that you've got a qualified friend to assist - and to explore a "parallel path." (my small tech firm often uses such approach - when competing methods bubble up - and we're unsure which method proves best...)

    Suggest that you replace (temporarily) those sensors w/simple external devices - and (really) confirm that your TM4C "Is fully able to properly transact w/similar, yet simpler, devices. BTW - your grammar is quite good - and your post is, "top cabin."
  • I have a similar issue. When you read the WIA register from the AK8963 what do you get? Remember that the AK8963 is a device on its own and that you can't access it directly from the I2C line of the TM4C. You have to first enable a bypass (Set INT_PIN_CFG to 0x22) in order to access it. In the MPU9250 you should get 0x48 when you successfully read the WIA register from the AK8963 module. Do you get this? If not then you are not communicating properly with it.

  • While your issue is (similar) it is undescribed. Firm/I have no background w/either device - yet perhaps others do.

    "Similar Issue" registers much like, "Does not work" - and your forum team is unlikely to resolve such vagueness...
  • I am not asking for help from the forum, I am just telling Vu Duc Thong that I have a similar issue. Geez..... Anyways Vu Duc Thong if you are able to read back a 0x48 I would like to know if you edited in any way your I2C read and write functions since I am using the same ones.
  • cb1-: Like I said before, the MPU6050 "inside" the MPU9050 has received correct data (so that I could play with Roll and Pitch things), furthermore the reason I mentioned my friend's work above is, yes I think I used the wrong words above, in this case we used just one MPU-9250 (he has his but I gave him this module to confirm that it returned non-zero magnetometer value while me didn't). So may I assume that this external device is working and my I2C transaction between the module and my MCU is good enough?

    Joana Aguirre: Thank you so much! How can I forget to read this in the register map. While looking on my friend's work I think myself had noticed this but, the fact that he used interrupt and just update everything in the while() loop made me think these interrupt-related register configuration are unnecessary and that's so naive of me. The WIA register return 0x48 as you said and I somehow acquired non-zero values (still a way to make it useful).

    I appreciate the help from both of you cb1- and Joana Aguirre, I have to learn and read more carefully from now on. Pardon me for my naivetée. Looking forward to learning more from you.
  • Joana Aguirre said:
    I have a similar issue

    If something looks like a duck - walks like a duck - and sounds like?

    Some way/how - "having an issue" (most always) signals a request for help.   (and not a very good signaling.)

    Geez...indeed.

    I do applaud your clarity in assisting our poster - having answered (way more) than my share of, "Does not Work" - you may accept my (tres gentle) protest...

  • Hello Vu Duc,
    I am using your code to work with a MPU9250 and a TM4C129X device.
    Once I have the connection with the I2C device, I try to use your code to calculate the quaternions (MadgwickQuaternionUpdate, etc) but it does not update the values of those quaternion, which are fixed to 1,0,0,0.

    Is it working for you?

    I think I tried with the Test.rar example you provided.
    Thank you
  • Well, I doubled checked your code and the variable deltat is empty....that's why quaternions are at initial state.
    In mbed delta is calculated with a timer, in Tiva you can use HWREG(DWT_BASE + DWT_O_CYCCNT).

    If somebody is willing to help,I could share what I have achieved at the moment. Probably the missing part would be a nice Madgwick implementation, but I ahve seen some I2CBusy errors and infinite whiles......I don't what to fight the IMU alone again.