Zero value euler angles from quaternions

I'm using an Arduino Nano 33 IoT to sense movements. Firstly I tried to compute manually pitch-roll-yaw (yes I know yaw cannot be computed without a magnetometer but I don't have one in this IMU), then I found that to avoid gimbal lock it is better use quaternions, so I used the MadgwickAHRS library to extract them. This library also compute the euler angles but when I try to print it I find only zeros.

What am I missing?

#include <Arduino_LSM6DS3.h>
#include <ArduinoBLE.h>
#include <BasicLinearAlgebra.h>
#include <SimpleKalmanFilter.h>
#include <MadgwickAHRS.h>

// Program Variables

const int sampling = 1000;    // Sampling period in milliseconds (1 Hz)
const int window = 5000;      // Sliding window size in ms (5 seconds)
float threshold = 1.02;       // Threshold to detect significant movement (adjust based on your needs)
float acc_Buffer[1000];       // Buffer for acceleration
int bufferIndex_a = 0;

float ax, ay, az;		          // x, y, z accelerometer
float gx, gy, gz;		          // x, y, z gyroscope
float pitch = 0.0, roll = 0.0, yaw = 0.0; // Orientation angles
float displacement[3] = {0.0, 0.0, 0.0};
//float G[3] = {0.0, 0.0, 9.81};

float R_inv[3][3];
using namespace BLA;

Madgwick filter;  // Create Madgwick filter object
float mea_e = 2;  // Measurement Uncertainty 
float est_e = 2;  // Estimation Uncertainty 
float q = 0.01;   // Process Noise

SimpleKalmanFilter simpleKalmanFilter(mea_e, est_e, q);

const long SERIAL_REFRESH_TIME = 100;
long refresh_time;

float filtered = 0;


void setup() {
  Serial.begin(115200);
  BLE.begin();
  pinMode(LED_BUILTIN, OUTPUT);

  if (!IMU.begin()) {
    Serial.println("Failed to initialize IMU!");
    while (1);
  }
  Serial.println("IMU initializad.");

}

void loop() {

  static long preMillis = 0;
  if (central) 
    {

    while (central.connected()) 
      {
      
      long curMillis = millis();
      if (preMillis>curMillis) preMillis=0; // millis() rollover?
      if (curMillis - preMillis >= sampling) // check values every 10mS
        {
        preMillis = curMillis;
        updateValues(); // call function for updating value to send to central
        }
      } // still here while central connected

    // central disconnected:
    digitalWrite(LED_BUILTIN, LOW);
    Serial.print(F("Disconnected from central: "));
    Serial.println(central.address());
    } // no central
}

void updateValues(){ 

    if (IMU.accelerationAvailable()) {
      IMU.readAcceleration(ax, ay, az);
      BLA::Matrix<3, 1> acc_vector = {ax, ay, az};
      BLA::Matrix<3, 1> G = {0.0, 0.0, 9.81};	  
	  
      // Calculate pitch and roll from accelerometer
      //pitch = atan2(-ax, sqrt(ay * ay + az * az));
      //roll = atan2(-ay, sqrt(ax * ax + az * az));
	
      IMU.readGyroscope(gx, gy, gz);
      
	  
	    gx = radians(gx);
		  gy = radians(gy);
		  gz = radians(gz);
		
    filter.updateIMU(gx, gy, gz, ax, ay, az);

    // Get the quaternion values. In the library, locate MadgwickAHRS.h and change from private to public
    float qw = filter.q0;
    float qx = filter.q1;
    float qy = filter.q2;
    float qz = filter.q3;
    float pitch = filter.pitch;
    float roll = filter.roll;
    float yaw = filter.yaw;

      //yaw += gz * sampling; // Adjust yaw over time using gyro Z data

      //BLA::Matrix<3, 3> Rx = {1, 0, 0, 0, cos(pitch), -sin(pitch), 0,	sin(pitch), cos(pitch)};
      //BLA::Matrix<3, 3> Ry = {cos(roll), 0, sin(roll), 0, 1, 0, -sin(roll),	0, cos(roll)};
      //BLA::Matrix<3, 3> Rz = {cos(yaw), -sin(yaw), 0, sin(yaw), cos(yaw), 0, 0,	0, 1};

      //BLA::Matrix<3, 3> R = Rz * Ry * Rx; 

      // From https://web.archive.org/web/20180112063505/http://www.chrobotics.com/library/accel-position-velocity
      // to compensate the gravity I must compute displacement = R_inv*acc_vector + (0,0,g)

      BLA::Matrix<3, 3> R = { qw*qw + qx*qx - qy*qy -qz*qz, 2*qx*qy - 2*qw*qz, 2*qx*qy + 2*qw*qy,
                              2*qx*qy + 2*qw*qz , qw*qw - qx*qx + qy*qy - qz*qz , 2*qy*qz - 2*qw*qx, 
                              2*qx*qz - 2*qw*qy, 2*qy*qz + 2*qw*qz, qw*qw - qx*qx - qy*qy + qz*qz};
      BLA::Matrix<3, 3> R_inv = Inverse(R);
      BLA::Matrix<3, 1> displacement = R_inv * acc_vector; 
      //displacement += G;

      float abs = sqrt(displacement(1)*displacement(1) + displacement(2)*displacement(2) + displacement(3)*displacement(3));
      //float abs = sqrt(qw*qw + qx*qx + qy*qy + qz*qz);

      float filtered = simpleKalmanFilter.updateEstimate(abs);

      acc_Buffer[bufferIndex_a] = abs;
      
      bufferIndex_a = (bufferIndex_a + 1) % (window / sampling);

      // Print counting and call count function
      if (bufferIndex_a == 0) {
        int n_movements = counting(acc_Buffer, window / sampling);
        Serial.print("# movements per minute: ");
        Serial.println(n_movements);

        Serial.print(" Abs: ");
        Serial.println(abs);

        Serial.print(" Filtered: ");
        Serial.println(filtered);

        Serial.print(" x_a: ");
        Serial.println(pitch);
        
        Serial.print(" y_a: ");
        Serial.println(roll);

        Serial.print(" z_a: ");
        Serial.println(yaw);

      }
    }
}


// Function to calculate displacement rate from acceleration data
int counting(float *buffer, int size) {
  int zeroCrossings = 0;
  for (int i = 1; i < size; i++) {
    if ((buffer[i - 1] < threshold && buffer[i] > threshold) || (buffer[i - 1] > threshold && buffer[i] < threshold)) {
      zeroCrossings++;
    }
  }

  // Convert zero crossings to movements per minute
  int movementsPerMinute = (zeroCrossings / 2) * (60000 / window);
  chaBR.writeValue(movementsPerMinute); 

  return movementsPerMinute;
}

can you rather post the code here ? most helpers won't click and download a file to open in the IDE... We would read code right here in the forum.

Sure. I removed all the BLE parts that are useless here

This does not address the main problem, but the gyro must be calibrated, in order to remove the offsets.

I prefer to derive the Euler angles directly from the quaternion using this particular convention, keeping in mind that gimbal lock remains a problem.

 // Define Tait-Bryan angles. Strictly valid only for approximately level movement
      
      // Standard sensor orientation : X magnetic North, Y West, Z Up (NWU)
      // this code corrects for magnetic declination.
      // Pitch is angle between sensor x-axis and Earth ground plane, toward the
      // Earth is positive, up toward the sky is negative. Roll is angle between
      // sensor y-axis and Earth ground plane, y-axis up is positive roll.
      // Tait-Bryan angles as well as Euler angles are
      // non-commutative; that is, the get the correct orientation the rotations
      // must be applied in the correct order.
      //
      // http://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles
      // which has additional links.
            
      // WARNING: This angular conversion is for DEMONSTRATION PURPOSES ONLY. It WILL
      // MALFUNCTION for certain combinations of angles! See https://en.wikipedia.org/wiki/Gimbal_lock
      
      roll  = atan2((q[0] * q[1] + q[2] * q[3]), 0.5 - (q[1] * q[1] + q[2] * q[2]));
      pitch = asin(2.0 * (q[0] * q[2] - q[1] * q[3]));
      yaw   = atan2((q[1] * q[2] + q[0] * q[3]), 0.5 - ( q[2] * q[2] + q[3] * q[3]));
      // to degrees
      yaw   *= 180.0 / PI;
      pitch *= 180.0 / PI;
      roll *= 180.0 / PI;
      // http://www.ngdc.noaa.gov/geomag-web/#declination
      //conventional nav, yaw increases CW from North, corrected for local magnetic declination
     yaw = -(yaw + declination);

regarding

You need to double check with the library but if I remember correctly updateIMU() expects degrees/s and not radians/s

Good point. The conversion is done internally, so the scale factor is applied twice in the posted code, and the gyro contributions are essentially zero.

void Madgwick::updateIMU(float gx, float gy, float gz, float ax, float ay, float az) {
	float recipNorm;
	float s0, s1, s2, s3;
	float qDot1, qDot2, qDot3, qDot4;
	float _2q0, _2q1, _2q2, _2q3, _4q0, _4q1, _4q2 ,_8q1, _8q2, q0q0, q1q1, q2q2, q3q3;

	// Convert gyroscope degrees/sec to radians/sec
	gx *= 0.0174533f;
	gy *= 0.0174533f;
	gz *= 0.0174533f;

@shika93 After correcting the errors that have been pointed out, the problem should be solved. If not, put in Serial.prints to see (for example) if the quaternion is actually updated.

Thank you to point it out. I corrected but still get 0 results.

I printed some result:

Serial.print("# movements per minute: ");
        Serial.println(n_movements);

        Serial.print(" Abs: ");
        Serial.println(abs);

        Serial.print(" Filtered: ");
        Serial.println(filtered);

        Serial.print(" Pitch: ");
        Serial.println(pitch);
        
        Serial.print(" Roll: ");
        Serial.println(roll);

        Serial.print(" Yaw: ");
        Serial.println(yaw);

        Serial.print(" Displacement x ");
        Serial.println(displacement(1));
        Serial.print(" Displacement y ");
        Serial.println(displacement(2));
        Serial.print(" Displacement z ");
        Serial.println(displacement(3));

 movements per minute: 0
 Abs: 0.00
 Filtered: 0.00
 Pitch: 0.00
 Roll: 0.00
 Yaw: 0.00
 Displacement x -0.11
 Displacement y 1.01
 Displacement z 0.00

I don't understand why I see displacement(2) different from 0 if the board is orientated face up. I should see z = 1

Regarding the gyro compensation, you mean normalize it to zero to remove error?
I'm not sure how to normalize it, because even if the board is kept still, face up, gyro values change. Fixed values I see poping up to normalize it might be

gx = gx-0.244;
gy = gy +0.854;
gz = gz+0.061;

But still sometimes I don't see always 0

Please post ALL of the corrected code, using code tags.

Calibrate a gyro by holding it still, collecting a few hundred raw measurements on each axis, and average them.

The averages will be the offsets that you later subtract from the measured raw rotation rates.

Here the complete post with integrated the compensation part.

I'm not sure that I implemented correctly because keeping still the board I don't see 0 for acceleration and gyroscope axis as shown in these two screenshots. The first one is the screenshot for still board, while the second one moving the board.

#include <Arduino_LSM6DS3.h>
#include <BasicLinearAlgebra.h>
#include <SimpleKalmanFilter.h>
#include <MadgwickAHRS.h>

// Program Variables

const int sampling = 1000;    // Sampling period in milliseconds (1 Hz)
const int window = 5000;      // Sliding window size in ms (5 seconds)
float threshold = 1;          // Threshold to detect significant movement (adjust based on your needs)
float acc_Buffer[1000];       // Buffer for acceleration
int bufferIndex_a = 0;
int numSamples = 100;         // Normalization Samples
float ax_Mean = 0, ay_Mean = 0, az_Mean = 0;
float gx_Mean = 0, gy_Mean = 0, gz_Mean = 0;


float ax, ay, az;		          // x, y, z accelerometer
float gx, gy, gz;		          // x, y, z gyroscope
float pitch = 0.0, roll = 0.0, yaw = 0.0; // Orientation angles
float displacement[3] = {0.0, 0.0, 0.0};

float R_inv[3][3];
using namespace BLA;

Madgwick filter;  // Create Madgwick filter object
float mea_e = 2;  // Measurement Uncertainty 
float est_e = 2;  // Estimation Uncertainty 
float q = 0.01;   // Process Noise

SimpleKalmanFilter simpleKalmanFilter(mea_e, est_e, q);

const long SERIAL_REFRESH_TIME = 100;
long refresh_time;

float filtered = 0;


void setup() {
  Serial.begin(115200);
  BLE.begin();
  pinMode(LED_BUILTIN, OUTPUT);

  if (!IMU.begin()) {
    Serial.println("Failed to initialize IMU!");
    while (1);
  }
  Serial.println("IMU initializad.");

}

void loop() {

  static long preMillis = 0;
  BLEDevice central = BLE.central();

  if (central){
    digitalWrite(LED_BUILTIN, HIGH); // turn on the onboard led
    Serial.print("Connected to central: ");
    Serial.println(central.address()); // central device MAC address
    // Calibrate the IMU
    Serial.println("Calibrating...");
    calibration();
    Serial.println("Calibration Completed");
    
    while (central.connected()){
      long curMillis = millis();
      if (preMillis>curMillis) preMillis=0; // millis() rollover?
      if (curMillis - preMillis >= sampling) // check values every 10mS
        {
        preMillis = curMillis;
        updateValues(); // call function for updating value to send to central
        }
      } // still here while central connected

    // central disconnected:
    digitalWrite(LED_BUILTIN, LOW);
    Serial.print(F("Disconnected from central: "));
    Serial.println(central.address());
    } // no central
}

void updateValues(){ 

    if (IMU.accelerationAvailable()) {
      IMU.readAcceleration(ax, ay, az);
      IMU.readGyroscope(gx, gy, gz);

      ax -= ax_Mean;
      ay -= ay_Mean;
      az -= az_Mean;
      gx -= gx_Mean;
      gy -= gy_Mean;
      gz -= gz_Mean;

      BLA::Matrix<3, 1> acc_vector = {ax, ay, az};
      BLA::Matrix<3, 1> G = {0.0, 0.0, 9.81};	  
	  	
	  filter.updateIMU(gx, gy, gz, ax, ay, az);

    // Get the quaternion values. In the library, locate MadgwickAHRS.h and change from private to public
    float qw = filter.q0;
    float qx = filter.q1;
    float qy = filter.q2;
    float qz = filter.q3;
    float pitch = filter.pitch;
    float roll = filter.roll;
    float yaw = filter.yaw;

      // From https://web.archive.org/web/20180112063505/http://www.chrobotics.com/library/accel-position-velocity
      // to compensate the gravity I must compute displacement = R_inv*acc_vector + (0,0,g)

      BLA::Matrix<3, 3> R = { qw*qw + qx*qx - qy*qy -qz*qz, 2*qx*qy - 2*qw*qz, 2*qx*qy + 2*qw*qy,
                              2*qx*qy + 2*qw*qz , qw*qw - qx*qx + qy*qy - qz*qz , 2*qy*qz - 2*qw*qx, 
                              2*qx*qz - 2*qw*qy, 2*qy*qz + 2*qw*qz, qw*qw - qx*qx - qy*qy + qz*qz};
      BLA::Matrix<3, 3> R_inv = Inverse(R);
      BLA::Matrix<3, 1> displacement = R_inv * acc_vector; 
      displacement += G;

      //float abs = sqrt(displacement(1)*displacement(1) + displacement(2)*displacement(2) + displacement(3)*displacement(3));
            float abs = displacement(3);

      float filtered = simpleKalmanFilter.updateEstimate(abs);

      acc_Buffer[bufferIndex_a] = abs;
      
      bufferIndex_a = (bufferIndex_a + 1) % (window / sampling);

      // Print counting and call count function
      if (bufferIndex_a == 0) {
        int n_movements = counting(acc_Buffer, window / sampling);
        Serial.print("# movements per minute: ");
        Serial.println(n_movements);

        Serial.print(" Abs: ");
        Serial.println(abs);

        Serial.print(" Filtered: ");
        Serial.println(filtered);

        Serial.print(" Pitch: ");
        Serial.println(pitch);
        
        Serial.print(" Roll: ");
        Serial.println(roll);

        Serial.print(" Yaw: ");
        Serial.println(yaw);

        Serial.print(" Displacement x ");
        Serial.println(displacement(1));
        Serial.print(" Displacement y ");
        Serial.println(displacement(2));
        Serial.print(" Displacement z ");
        Serial.println(displacement(3));
      }
    }
}

void calibration() {
  float numSamples = 300;
  float ax_Sum = 0, ay_Sum = 0, az_Sum = 0;
  float gx_Sum = 0, gy_Sum = 0, gz_Sum = 0;

  for (int i = 0; i < numSamples; i++) {

    float ax, ay, az;
    float gx, gy, gz;

    IMU.readAcceleration(ax, ay, az);
    IMU.readGyroscope(gx, gy, gz);

    ax_Sum += ax;
    ay_Sum += ay;
    az_Sum += az;

    gx_Sum += gx;
    gy_Sum += gy;
    gz_Sum += gz;

    delay(10); 
  }

  // Calculate averages
  ax_Mean = ax_Sum / numSamples;
  ay_Mean = ay_Sum / numSamples;
  az_Mean = az_Sum / numSamples;
  gx_Mean = gx_Sum / numSamples;
  gy_Mean = gy_Sum / numSamples;
  gz_Mean = gz_Sum / numSamples;
}

// Function to calculate displacement rate from acceleration data
int counting(float *buffer, int size) {
  int zeroCrossings = 0;
  for (int i = 1; i < size; i++) {
    if ((buffer[i - 1] < threshold && buffer[i] > threshold) || (buffer[i - 1] > threshold && buffer[i] < threshold)) {
      zeroCrossings++;
    }
  }

  // Convert zero crossings to movements per minute
  int movementsPerMinute = (zeroCrossings / 2) * (60000 / window);
  chaBR.writeValue(movementsPerMinute); 

  return movementsPerMinute;
}

Here the serial monitor data.

Connected to central: 51:65:4e:bc:5e:ca
Calibrating...
Calibration Completed
# movements per minute: 0
 Abs: -0.01
 Filtered: -0.00
 Pitch: 0.00
 Roll: 0.00
 Yaw: 0.00
 Displacement x 0.00
 Displacement y -0.01
 Displacement z 0.00
# movements per minute: 0
 Abs: -0.01
 Filtered: -0.00
 Pitch: 0.00
 Roll: 0.00

Do the following lines return raw sensor values, or scaled?
Where are scale factors applied to gyro data?

      IMU.readAcceleration(ax, ay, az);
      IMU.readGyroscope(gx, gy, gz);

The following is completely wrong. The filter won't work at all with these lines. To learn how to calibrate an accelerometer, study this excellent tutorial.

      ax -= ax_Mean;
      ay -= ay_Mean;
      az -= az_Mean;

Here is one of many examples of fusion filter code for a six-axis IMU that works as expected.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.