I greatly appreciate Adafruit_support_bill's information below. Unfortunately, the recommendations in the links seem to require a lot more computer programming skill than I possess. I think, however, I have found a solution that may be useful to the layperson like me. My quest began with this comment posted on Adafruit's Learning page that Bill recommended:
Manually Setting In Code
The least desirable, but sometimes necessary, option is to simply set the calibration at the top of your sketch/code. It's not something we like to do because its easy to lose the calibration, but it's always an option!
However there is nothing further in that regard. On a separate forum post regarding sensor drift, sj_remington recommended to me that I not only re-perform my sensor calibration but also that I consider hardcoding the sensor input in order to perform a quality test. That got me to thinking that calibration data could be retrieved from hardcode input vice being retrieved from the EEPROM and used in the calculations. For many folks that may be a suitable option. Sure... it's not optimal but it may be good enough if it works. For a Due, however, it is required.
The first thing to start with is to perform a good calibration. Initially, I used an Arduino Uno and the MotionCal software tool to capture the calibration data as specified in many posts by others. Unfortunately, I **thought** I had captured a suitable calibration but it appears that my effort was not good enough. Another calibration effort provided the following and using this new calibration data I was able to eliminate what I thought to be 'drifting' sensor output.
- MotionCal_Calibrated_Sensor_Data-20221231.png (162.45 KiB) Viewed 384 times
My further recommendation, but not one that I have extensively tested, is that the calibration values displayed on the MotionCal screen should be used in your sketch while MotionCal's Send function (which is supposed to write the calibration data into the device's EEPROM) should not be relied upon. FYI, it took me about 30 minutes to get the values (Gaps, Variance, Wobble, and Fit Error) but your mileage may vary. Get a good screenshot of your calibration and save that for reference.
The next step is to edit a few files to enable hardcoding the calibration data and bypassing any reliance upon EEPROM memory reading/writing functions - which is required if you are going to use an Arduino Due. The first place to start is the Libraries->Adafruit_Sensor_Calibration->Adafruit_Sensor_Calibration_EEPROM.cpp file where the offsets[] variables are replaced with your calibration data. Here is my edited file but you will need to edit this file with your specific sensor's calibration data.
Code: Select all
// This file was drawn from the Examples->Adafruit_Sensor_Calibration->Adafruit_Sensor_Calibration_EEPROM.cpp
// file provided with the Arduino IDE. It was then extensively modified to hardcode sensor calibration data
// obtained from the MotionCal software tool that reflects successful LSM6DSOX_LIS3MDL sensor calibration.
// Actions were disabled which attempt to read or write to the EEPROM.
#include "Adafruit_Sensor_Calibration.h"
#if defined(ADAFRUIT_SENSOR_CALIBRATION_USE_EEPROM)
/**************************************************************************/
/*!
@brief Initializes Flash and filesystem
@returns False if any failure to initialize flash or filesys
*/
/**************************************************************************/
bool Adafruit_Sensor_Calibration_EEPROM::begin(uint8_t eeprom_addr) {
Serial.println(F("Begin"));
/*
ee_addr = eeprom_addr;
#if defined(ESP8266) || defined(ESP32)
EEPROM.begin(512);
#endif
*/
return true;
}
bool Adafruit_Sensor_Calibration_EEPROM::saveCalibration(void) {
Serial.println(F("Save Cal"));
/*
uint8_t buf[EEPROM_CAL_SIZE];
memset(buf, 0, EEPROM_CAL_SIZE);
buf[0] = 0x75;
buf[1] = 0x54;
float offsets[16];
memcpy(offsets, accel_zerog, 12); // 3 x 4-byte floats
memcpy(offsets + 3, gyro_zerorate, 12); // 3 x 4-byte floats
memcpy(offsets + 6, mag_hardiron, 12); // 3 x 4-byte floats
offsets[9] = mag_field;
offsets[10] = mag_softiron[0];
offsets[11] = mag_softiron[4];
offsets[12] = mag_softiron[8];
offsets[13] = mag_softiron[1];
offsets[14] = mag_softiron[2];
offsets[15] = mag_softiron[5];
memcpy(buf + 2, offsets, 16 * 4);
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < EEPROM_CAL_SIZE - 2; i++) {
crc = crc16_update(crc, buf[i]);
}
Serial.print("CRC: ");
Serial.println(crc, HEX);
buf[EEPROM_CAL_SIZE - 2] = crc & 0xFF;
buf[EEPROM_CAL_SIZE - 1] = crc >> 8;
for (uint16_t a = 0; a < EEPROM_CAL_SIZE; a++) {
EEPROM.write(a + ee_addr, buf[a]);
}
#if defined(ESP8266) || defined(ESP32)
EEPROM.commit();
#endif
*/
return true;
}
bool Adafruit_Sensor_Calibration_EEPROM::loadCalibration(void) {
Serial.println(F("Load Cal"));
/*
uint8_t buf[EEPROM_CAL_SIZE];
uint16_t crc = 0xFFFF;
for (uint16_t a = 0; a < EEPROM_CAL_SIZE; a++) {
buf[a] = EEPROM.read(a + ee_addr);
crc = crc16_update(crc, buf[a]);
}
if (crc != 0 || buf[0] != 0x75 || buf[1] != 0x54) {
Serial.print("CRC: ");
Serial.println(crc, HEX);
return false;
}
*/
float offsets[16];
/*
memcpy(offsets, buf + 2, 16 * 4);
accel_zerog[0] = offsets[0];
accel_zerog[1] = offsets[1];
accel_zerog[2] = offsets[2];
gyro_zerorate[0] = offsets[3];
gyro_zerorate[1] = offsets[4];
gyro_zerorate[2] = offsets[5];
mag_hardiron[0] = offsets[6];
mag_hardiron[1] = offsets[7];
mag_hardiron[2] = offsets[8];
mag_field = offsets[9];
mag_softiron[0] = offsets[10];
mag_softiron[1] = offsets[13];
mag_softiron[2] = offsets[14];
mag_softiron[3] = offsets[13];
mag_softiron[4] = offsets[11];
mag_softiron[5] = offsets[15];
mag_softiron[6] = offsets[14];
mag_softiron[7] = offsets[15];
mag_softiron[8] = offsets[12];
*/
// The below data was obtained using the MotionCal software tool.
// Edit the below to suit your device's calibration.
// Do not rely upon MotionCal to transfer data to your device.
// Recommend capturing a screen shot of MotionCal's screen and using those values.
accel_zerog[0] = 0.0;
accel_zerog[1] = 0.0;
accel_zerog[2] = 0.0;
gyro_zerorate[0] = 0.0;
gyro_zerorate[1] = 0.0;
gyro_zerorate[2] = 0.0;
mag_hardiron[0] = -41.84;
mag_hardiron[1] = 23.76;
mag_hardiron[2] = -29.44;
mag_field = 45.71;
mag_softiron[0] = 0.993;
mag_softiron[1] = 0.043;
mag_softiron[2] = -0.014;
mag_softiron[3] = 0.043;
mag_softiron[4] = 1.019;
mag_softiron[5] = 0.000;
mag_softiron[6] = -0.014;
mag_softiron[7] = 0.000;
mag_softiron[8] = 0.991;
return true;
}
bool Adafruit_Sensor_Calibration_EEPROM::printSavedCalibration(void) {
Serial.println(F("Print saved cal"));
Serial.println(F("------------"));
/*
for (uint16_t a = ee_addr; a < ee_addr + EEPROM_CAL_SIZE; a++) {
uint8_t c = EEPROM.read(a);
Serial.print("0x");
if (c < 0x10)
Serial.print('0');
Serial.print(c, HEX);
Serial.print(", ");
if ((a - ee_addr) % 16 == 15) {
Serial.println();
}
}
*/
Serial.println(F("\n------------"));
return true;
}
#endif
Once you do that you will need to lightly edit the Libraries->Adafruit_Sensor_Calibration->Adafruit_Sensor_Calibration_EEPROM.h file. Editing this file is easy... just comment out the reference to including EEPROM.h (i.e., make it: // #include <EEPROM.h>). Commenting this line out worked for me... and I couldn't find it in any of my other library files... so I hope I am not committing a significant faux pas by commenting it out.
The next step is to edit the Libraries->Adafruit_Sensor_Calibration->Adafruit_Sensor_Calibration.h file. Specifically, all you need to do is make this definition block define only one option. Since the Arduino Due is not specifically identified in the #if line you need to spoof the logic that determines where there is, or is not, EEPROM memory storage capability in your microprocessor. Here is the edited code that I used (which you should easily be able to find and edit yourself):
Code: Select all
#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega32U4__) || \
defined(__AVR_ATmega2560__) || defined(ESP8266) || defined(ESP32) || \
defined(TEENSYDUINO)
#define ADAFRUIT_SENSOR_CALIBRATION_USE_EEPROM
#else
#define ADAFRUIT_SENSOR_CALIBRATION_USE_EEPROM
// #define ADAFRUIT_SENSOR_CALIBRATION_USE_SDFAT
#endif
If you edit these three files you should then be able to use an Arduino Due to perform the Examples->Adafruit_AHRS->calibrated_orientation->calibration_orientation.ino sketch. Using that sketch enables you to use any of the three filters in order to determine a good Heading that compensates for tilt (I have only used the Mahoney filter so far). I EXTENSIVELY modified that sketch and I offer it to you below for your consideration.
Code: Select all
// This sketch is loosely based upon the Arduino Examples-> Adafruit AHRS-> calibrated_orientation sketch.
// The purpose of this modification is to capture data to explore the effects of Mahoney filter calculations on different microprocessors.
// Two instances of simulated 9DOF sensor data are injected into the data stream in order to control the test's input.
// The output data was formatted as a .csv in order to utilize spreadsheet graphical tools and analysis.
//
// This sketch injects 'hardcoded' static LSM6DSOX and LIS3MDL sensor data but data from any other similar sensor can be used.
// Doing so will reveal whether observed Heading 'drift' is due to the sensor or calculations and
// whether or not the microprocessor / sensor / filter combination provides suitable, timeley results.
//
// This sketch also relies upon a customized version of the
// Examples->Adafruit_Sensor_Calibration->Adafruit_Sensor_Calibration_EEPROM.cpp where calibration sensor
// data obtained from the MotionCal software tool is specified in the sketch vice relying upon EEPROM
// memory. While the Arduino Uno and Mega have EEPROM memory the Due dues not. Thus, this modification
// enables a standardized comparison between the three microprocessors regardless of EEPROM capability.
//
// Additionally, the Examples->Adafruit_Sensor_Calibration->Adafruit_Sensor_Calibration.h file was slightly
// modified in order to force definition of ADAFRUIT_SENSOR_CALIBRATION_USE_EEPROM vice providing an option for
// ADAFRUIT_SENSOR_CALIBRATION_USE_SDFAT.
//
// Sketch output was modified in order to produce .csv utility.
//
// Full orientation sensing using NXP/Madgwick/Mahony and a range of 9-DoF sensor sets.
// You *must* perform a magnetic calibration before this code will work.
//
// To view this data, use the Arduino Serial Monitor to watch the
// scrolling angles, or run the OrientationVisualiser example in Processing.
// Based on https://github.com/PaulStoffregen/NXPMotionSense with adjustments to Adafruit Unified Sensor interface
#include <Adafruit_Sensor_Calibration.h>
#include <Adafruit_AHRS.h>
Adafruit_Sensor *accelerometer, *gyroscope, *magnetometer;
// Specify the combo 9-DoF (pick only one)
#include "LSM6DS_LIS3MDL.h" // can adjust to LSM6DS33, LSM6DS3U, LSM6DSOX...
//#include "LSM9DS.h" // LSM9DS1 or LSM9DS0
//#include "NXP_FXOS_FXAS.h" // NXP 9-DoF breakout
// Specify the filter (pick only one) (slower = better quality output)
//Adafruit_NXPSensorFusion filter; // slowest
//Adafruit_Madgwick filter; // faster than NXP
Adafruit_Mahony filter; // fastest/smalleset
// Specify the calibration library with calibration data hardcoded into it.
Adafruit_Sensor_Calibration_EEPROM cal;
// Specify the filter update frequency
#define FILTER_UPDATE_RATE_HZ 100
// Declare the timer-related variables
unsigned long StartTime0; // Used to determine test duration elapsed time, in milliseconds
unsigned long StartTime1; // Used to determine filter update elapsed time, in milliseconds
unsigned long StartTime2; // Used to determine data recording elapsed time, in milliseconds
unsigned long ElapsedTime0; // Used to calculate elapsed time, in milliseconds
unsigned long ElapsedTime1; // Used to calculate elapsed time, in milliseconds
unsigned long ElapsedTime2; // Used to calculate elapsed time, in milliseconds
unsigned long TestDuration = 480 * 1000; // Defines testing duration in seconds (480 seconds = 8 minutes), in milliseconds
unsigned long IntervalDuration = 500; // Defines data recording interval in seconds, in milliseconds
void setup()
{
// Initialize serial port to enable Arduino IDE Serial Monitor
Serial.begin(115200);
while (!Serial)
{
delay(100);
}
Serial.println("\n\nCalibrated Orientation - Two orientation Mahoney filter response test.\n");
if (!cal.begin())
{
Serial.println("Failed to initialize calibration helper");
while(true) delay(100);
}
else
{
if (! cal.loadCalibration())
{
Serial.println("No calibration loaded/found");
while(true) delay(100);
}
else
{
if (!init_sensors())
{
Serial.println("Failed to find sensors");
while(true) delay(100);
}
else
{
// Obtain sensor details and print them out.
accelerometer->printSensorDetails();
gyroscope->printSensorDetails();
magnetometer->printSensorDetails();
// Initialise sensor
setup_sensors();
filter.begin(FILTER_UPDATE_RATE_HZ);
Wire.setClock(400000); // 400KHz
// Print header for .csv utility
Serial.println("Time, Orientation, ,");
Serial.println("(seconds), Heading, Pitch, Roll");
// Start timers
StartTime0 = millis(); // This is the total execution time timer start
StartTime1 = StartTime0; // This is the filter refresh cycle timer start
StartTime2 = StartTime0; // This is the sensor data print cycle timer start
}
}
}
}
void loop()
{
float heading, bearing, roll, pitch;
float gx, gy, gz;
// INPUT DATA: The Examples->Adafruit AHRS->calibration sketch was used to capture the following data:
/*
REFERENCE ORIENTATION - 110 degrees - 20220102
Adafruit AHRS - IMU Calibration!
Calibration filesys test
Begin
Load Cal
Loaded existing calibration
------------------------------------
Raw:159,-170,8255,2,2,-16,-458,358,-674
Uni:0.19,-0.20,9.88,0.0023,0.0023,-0.0183,-45.89,35.87,-67.47
TEST ORIENTATION - 250 degrees - 20220102
Adafruit AHRS - IMU Calibration!
Calibration filesys test
Begin
Load Cal
Loaded existing calibration
------------------------------------
Raw:-13,21,8261,2,3,-16,-468,115,-666
Uni:-0.02,0.03,9.88,0.0029,0.0037,-0.0180,-46.86,11.52,-66.66
*/
ElapsedTime0 = millis() - StartTime0;
ElapsedTime1 = millis() - StartTime1;
ElapsedTime2 = millis() - StartTime2;
// Ensure program execution is within testing timeframe
if(ElapsedTime0 <= (2 * TestDuration) + IntervalDuration)
{
// Ensure enough time is allocated for filter calculations
if (ElapsedTime1 >= 1000 / FILTER_UPDATE_RATE_HZ)
{
// Recieve data from the motion sensors
sensors_event_t accel, gyro, mag;
accelerometer->getEvent(&accel);
gyroscope->getEvent(&gyro);
magnetometer->getEvent(&mag);
// The following replaces the received sensor data with defined data
// For the first 480 seconds, use the data obtained where X faces North
// For the second 480 seconds, use the data obtained where X faces West
// Terminate at 960 seconds (16 minutes)
/*
// Overwrite sensor data with specified test data. See comments above for data source information.
// This if/else section may be commented out in order to utilize 'live' sensor data captured above.
if(ElapsedTime0 <= (1 * TestDuration) + IntervalDuration)
{
// Specify data captured in the reference orientation.
// Uni:0.19,-0.20,9.88,0.0023,0.0023,-0.0183,-45.89,35.87,-67.47
accel.acceleration.x = 0.19;
accel.acceleration.y = -0.20;
accel.acceleration.z = 9.88;
gx = 0.0023;
gy = 0.0023;
gz = -0.0183;
mag.magnetic.x = -45.89;
mag.magnetic.y = 35.87;
mag.magnetic.z = -6.47;
}
else
{
// Specify data captured in the test orientation.
// Uni:-0.02,0.03,9.88,0.0029,0.0037,-0.0180,-46.86,11.52,-66.66
accel.acceleration.x = -0.02;
accel.acceleration.y = 0.03;
accel.acceleration.z = 9.88;
gx = 0.0029;
gy = 0.0037;
gz = -0.0180;
mag.magnetic.x = -46.86;
mag.magnetic.y = 11.52;
mag.magnetic.z = -66.66;
}
*/
// Perform calibration on the sensor data
cal.calibrate(mag);
cal.calibrate(accel);
cal.calibrate(gyro);
// Gyroscope needs to be converted from Rad/s to Degree/s
gx = gyro.gyro.x * SENSORS_RADS_TO_DPS;
gy = gyro.gyro.y * SENSORS_RADS_TO_DPS;
gz = gyro.gyro.z * SENSORS_RADS_TO_DPS;
// Update the SensorFusion filter
filter.update(gx, gy, gz,
accel.acceleration.x, accel.acceleration.y, accel.acceleration.z,
mag.magnetic.x, mag.magnetic.y, mag.magnetic.z);
StartTime1 = millis(); // Reset the timer associated with the FILTER_UPDATE_RATE_HZ calculation
// Ensure program output is on specified schedule
if(ElapsedTime2 >= IntervalDuration)
{
// Calculate the heading, bearing, pitch and roll
heading = filter.getYaw();
bearing = heading + 180.0;
while(bearing >= 360.0)
{
bearing = bearing - 360.0;
}
bearing = 360.0 - bearing;
while(bearing < 0.0)
{
bearing = bearing + 360.0;
}
pitch = filter.getPitch();
roll = filter.getRoll();
Serial.print(float(ElapsedTime0) / 1000.0);
Serial.print(", ");
Serial.print(bearing);
Serial.print(", ");
Serial.print(pitch);
Serial.print(", ");
Serial.println(roll);
StartTime2 = millis(); // Reset the timer associated with the data output
}
else
{
;
}
}
else
{
;
}
}
else
{
Serial.println("\n\n\nProgram execution complete.\n\n");
while(true) delay(100);
}
}
I then used the above code to conduct a comparison between how the Mahoney filter calculations perform when fed hardcoded data (which eliminates sensor input) versus 'live' data (but using a static/immobile sensor). I started the test with an 8-minute stabilization period and then either simulated or physically moved the sensor to a new position for an additional 8 minutes. The resulting curves were extremely different but both curves did allay my initial concerns that I had a 'drifting' sensor.
- Sensor_Data_Response_Test-20230102.jpg (582.53 KiB) Viewed 384 times
There are several conclusions that can be drawn from the above graphic but that is **not** the point of my post on this forum. My intent was to enable the capability for me to use my Arduino Due to use the Mahoney filter to get high quality Heading data and this required bypassing the EEPROM data storage limitation found in the various Adafruit codes. Having found a way to do so I am sharing my method with everyone for their consideration.