Skip to content

Chapter 11: QMC6309 Magnetometer and Compass Heading ​

📂 Example Code: 11_mag_compass

11.1 Key Points ​

  • QMC6309 3-axis magnetometer principle (Hall effect, I2C, address 0x7C)
  • Hard-iron interference: fixed offset from permanent magnets on the PCB, corrected with min/max calibration
  • Soft-iron interference: ferromagnetic materials distort the field into an ellipse, corrected with a scaling matrix
  • Tilt-compensated heading: project horizontal field components onto the horizontal plane using roll/pitch
  • Complementary filter fusing gyroscope integration and magnetometer absolute heading

11.2 Course Content ​

This chapter demonstrates the complete QMC6309 workflow: initialization, raw data reading, hard/soft iron calibration, tilt-compensated heading calculation, and USB CDC console interaction. After this chapter you can integrate the magnetometer into an AHRS for absolute heading estimation.

11.3 Basic Learning ​

QMC6309 Register Map ​

RegisterAddressDescription
CHIP_ID0x00Fixed 0x90, used to verify communication
X_LSB0x01X-axis data LSB (6 bytes total for XYZ)
STATUS0x09bit0 = DRDY (data ready)
CTRL10x0AOSR, operating mode
CTRL20x0BSoft reset, ODR, range

Init sequence: soft reset → verify CHIP_ID → configure CTRL1 (OSR) → configure CTRL2 (ODR, range) → set continuous measurement mode.

Hard-Iron Calibration ​

Rotate the device 360° in the horizontal plane and record min/max for X and Y:

c
hard_iron[0] = (max_x + min_x) / 2.0f;
hard_iron[1] = (max_y + min_y) / 2.0f;

After calibration: x_cal = x_raw - hard_iron[0]

Soft-Iron Calibration ​

When X and Y ranges differ, the field trace is an ellipse. A diagonal scaling matrix stretches it back to a circle:

c
float avg_range = (range_x + range_y) / 2.0f;
soft_iron[0] = avg_range / range_x;   // X scale
soft_iron[4] = avg_range / range_y;   // Y scale
soft_iron[8] = 1.0f;                  // Z unchanged (ground vehicle)

Tilt Compensation ​

atan2(my, mx) is only accurate when the board is level. When tilted, project the field onto the horizontal plane first:

c
float mx_h = mx * cosf(pitch) + mz * sinf(pitch);
float my_h = mx * sinf(roll) * sinf(pitch)
           + my * cosf(roll)
           - mz * sinf(roll) * cosf(pitch);
float heading = atan2f(-my_h, mx_h) * 180.0f / M_PI;
if (heading < 0) heading += 360.0f;

Roll and pitch come from the IMU (Chapter 10 Madgwick AHRS).

Complementary Filter ​

The magnetometer has noise but no drift; the gyroscope is precise short-term but drifts. A complementary filter combines both:

c
// alpha = 0.98 static, 0.995 moving
yaw_cf += gyro_z * dt;
float err = mag_heading - yaw_cf;
// normalize err to [-π, π]
yaw_cf += (1.0f - alpha) * err;

11.4 Program Study ​

Initialization ​

c
void qmc6309_init(i2c_master_dev_handle_t dev) {
    s_dev = dev;
    // soft reset
    write_reg(QMC_REG_CTRL2, CTRL2_SOFT_RST);
    vTaskDelay(pdMS_TO_TICKS(20));
    // verify CHIP_ID
    uint8_t id = 0;
    read_reg(QMC_REG_CHIP_ID, &id, 100);
    if (id != 0x90) printf("WARN: QMC6309 CHIP_ID=0x%02X\n", id);
    // configure ODR=200Hz, range=8G, continuous mode
    uint8_t ctrl1 = CTRL1_OSR2(4) | CTRL1_OSR1(3) | CTRL1_MODE(0);
    write_reg(QMC_REG_CTRL1, ctrl1);
    write_reg(QMC_REG_CTRL2, CTRL2_ODR(4) | CTRL2_RNG(2));
    ctrl1 = (ctrl1 & ~0x03) | CTRL1_MODE(3);
    write_reg(QMC_REG_CTRL1, ctrl1);
    vTaskDelay(pdMS_TO_TICKS(100));
}

Read and Apply Calibration ​

c
bool qmc6309_read(qmc6309_data_t *out) {
    uint8_t status = 0;
    read_reg(QMC_REG_STATUS, &status, 2);
    if (!(status & STATUS_DRDY)) return false;

    uint8_t data[6];
    read_burst(QMC_REG_X_LSB, data, 6);
    float rx = (int16_t)((data[1]<<8)|data[0]) / SENSITIVITY_8G;
    float ry = (int16_t)((data[3]<<8)|data[2]) / SENSITIVITY_8G;
    float rz = (int16_t)((data[5]<<8)|data[4]) / SENSITIVITY_8G;

    // apply hard-iron + soft-iron
    float dx = rx - s_hard_iron[0];
    float dy = ry - s_hard_iron[1];
    float dz = rz - s_hard_iron[2];
    out->x = s_soft_iron[0]*dx + s_soft_iron[1]*dy + s_soft_iron[2]*dz;
    out->y = s_soft_iron[3]*dx + s_soft_iron[4]*dy + s_soft_iron[5]*dz;
    out->z = s_soft_iron[6]*dx + s_soft_iron[7]*dy + s_soft_iron[8]*dz;

    out->heading = atan2f(out->y, out->x) * 180.0f / M_PI;
    if (out->heading < 0) out->heading += 360.0f;
    return true;
}

Main Loop ​

c
while (1) {
    qmc6309_data_t d;
    if (qmc6309_read(&d))
        printf("hdg=%.1f x=%.4f y=%.4f z=%.4f\n", d.heading, d.x, d.y, d.z);

    // non-blocking USB CDC command parsing
    // cal [sec] / heading / help
    vTaskDelay(pdMS_TO_TICKS(100));
}

11.5 Chapter Summary ​

This chapter covered QMC6309 I2C driver implementation, the two-step hard/soft iron calibration workflow, and tilt-compensated heading calculation. The magnetometer provides an absolute heading reference but is sensitive to electromagnetic interference. In the Chapter 12 full example, a complementary filter fuses the magnetometer with the gyroscope to produce a stable compass heading under dynamic conditions.

Built for OSRCORE robot development board.