Skip to content

Instantly share code, notes, and snippets.

@jones2126
Created June 30, 2021 11:03
Show Gist options
  • Save jones2126/c33afeca3ea9e3e777dfe29f610b1a06 to your computer and use it in GitHub Desktop.
Save jones2126/c33afeca3ea9e3e777dfe29f610b1a06 to your computer and use it in GitHub Desktop.
Reads rotation vector quaternion report from BNO085 from Adafruit using "new" I2C Library using Nano (168p)
/* BNO080 Rock bottom code for Arduino Atmega 328P 16Mhz (Arduino Nano etc)
by: JP Schramel https://github.com/jps2000/BNO080/blob/master/Arduino_BNO080_1.ino
date: August 25, 2017
Disclaimer:
Freeware: The code is provided "AS IS",without warranty of any kind.
# Demonstrates basic functionality 9DOF fused quaternions up to 400Hz data rate.
# Stores calibration data in flash.
# Tares the quaternion output to (1,0,0,0) in any arbitrary position
# Enables control about dynamic automatic calibration
# heading accurracy estimation
It uses I2C.h library @ 400kHz for communication.
Two buttons store dynamic calibration data in flash and for tare function:
Other feature reports than quaternions (gravities, linear acceleration etc) can be implemented similarly (see HillcrestLab data sheets and application notes)
Data are available via Serial.print. Note that this may take significant time and limits max data rate.
Hardware requirements:
ATmega 328p based boards (Nano..., 16MHz)
BNO080 breakout board e.g. Hillcrest FSM300
Needs a 3V3 supply and connect I2C Bus via a level converter 5V --> 3V3!!! SDA = A4; SCL = A5.
LED via 470 Ohm --> pin 5 (pin 13), to ground.
Button pin A3 to GND => Tare function
Button pin 6 to GND = Calibration function
*/
/*************************************
INCLUDES
*************************************/
#include <I2C.h> // This library allows you to communicate with I2C / TWI devices
#include <Arduino.h>
/*************************************
DEFINITIONS
*************************************/
#define Led 13 // Led (Arduino nano = 13) to measure loop duration (3)
#define btn_TARE A3 // input pin for button TARE
#define btn_CAL 6 // input pin for button TARE
int plot_interval = 2000; // plot interval in ms
unsigned long prev_time_stamp_read = 0;
#define BNO_ADDRESS 0x4A // Device address when SA0 Pin 17 = GND; 0x4B SA0 Pin 17 = VDD
uint8_t cargo[23];
uint8_t next_data_seqNum = 0x10; // next data sequence number
float q0,q1,q2,q3; // quaternions q0 = qw 1 = i; 2 = j; 3 = k;
float h_est; // heading accurracy estimation
uint8_t stat; // Status (0-3)
const uint8_t quat_report = 0x05; // defines kind of rotation vector (0x05), geomagnetic (0x09), AR/VR (0x28), // without magnetometer : game rotation vector (0x08), AR/VR Game (0x29)
const int reporting_frequency = 200; // reporting frequency in Hz // note that serial output strongly reduces data rate
const uint8_t B0_rate = 1000000 / reporting_frequency; //calculate LSB (byte 0)
const uint8_t B1_rate = B0_rate >> 8; //calculate byte 1
/******* Conversions *************/
#define QP(n) (1.0f / (1 << n)) // 1 << n == 2^-n
#define radtodeg (180.0f / PI)
/*************************************
SETUP
*************************************/
void setup() {
// pin definitions
pinMode(Led, OUTPUT);
pinMode(btn_TARE, INPUT_PULLUP);
pinMode(btn_CAL, INPUT_PULLUP);
digitalWrite(Led, HIGH); // initial state of LED (on)
// communication
Serial.begin(115200); // 115200 baud
I2c.begin(); // start I2C communication
I2c.setSpeed(1); // set I2C to 400kHz
Serial.println ("*********************************************************************");
Serial.println (" Rock bottom code for BNO080 on Arduino Nano etc. (Atmega 328p) V1.0 2017-08-24");
Serial.println ("*********************************************************************");
// BNO settings
while (I2c.read(BNO_ADDRESS,0) != 0); //wait until device is responding (32 kHz XTO running)
delay(100); //needed to accept feature command; minimum not tested
set_feature_cmd_QUAT(); // set the required feature report data rate are generated at preset report interva
ME_cal(1,1,1,0); // switch autocal on @ booting (otherwise gyro is not on)
}
/*************************************
LOOP
*************************************/
void loop() {
get_QUAT(); // get actual QUAT data (if new are available)
if (millis() - prev_time_stamp_read >= plot_interval) {
// if (millis() - plot_interval > 0){
Serial.print ("S "); Serial.print (stat);
Serial.print ("; E "); Serial.print (h_est + 0.05f,1); // including rounding
Serial.print ("; q0 "); Serial.print (q0 + 0.00005f,4); // = qw (more digits to find out north direction (y axis N --> q0 = 1)
Serial.print ("; q1 "); Serial.print (q1 + 0.0005f,3);
Serial.print ("; q2 "); Serial.print (q2 + 0.0005f,3);
Serial.print ("; q3 "); Serial.println (q3 + 0.0005f,3);
//plot_interval = millis();
prev_time_stamp_read = millis();
}
if (stat == 3) digitalWrite(Led, !digitalRead(Led)); // blink Led every loop run (--> to measure loop duration);
else digitalWrite(Led, LOW); // status Led
// ************* buttons *******************************
if(digitalRead(btn_TARE) == LOW ){ // button pressed stores actual phi as mean value and saves actual calibration to flash
delay(200);
while(digitalRead(btn_TARE) == LOW); // wait for button release
//actions follow here
TARE();
}
if(digitalRead(btn_CAL) == LOW){
delay(200);
while(digitalRead(btn_CAL) == LOW ); // wait for button release
//actions follow here
save_DCD(); // store cal in flash
delay(200);
ME_cal(0,0,1,0); //autocal acc + gyro stop; magnetometer cal continues
}
//*******************************************************
} // loop ends here
/*************************************
Subroutines
/*************************************/
/*******************************************************************************************************************
/* This code reads quaternions
* kind of reporting (quat_report) is defined above
*/
void get_QUAT(){
if (quat_report == 0x08 || quat_report == 0x29) I2c.read(BNO_ADDRESS,21, cargo);
else I2c.read(BNO_ADDRESS,23, cargo);
if((cargo[9] == quat_report) && ((cargo[10]) == next_data_seqNum )){ // check for report and incrementing data seqNum
next_data_seqNum = ++cargo[10]; // predict next data seqNum
stat = cargo[11] & 0x03; // bits 1:0 contain the status (0,1,2,3)
q1 = (((int16_t)cargo[14] << 8) | cargo[13] );
q2 = (((int16_t)cargo[16] << 8) | cargo[15] );
q3 = (((int16_t)cargo[18] << 8) | cargo[17] );
q0 = (((int16_t)cargo[20] << 8) | cargo[19] );
q0 *= QP(14); q1 *= QP(14); q2 *= QP(14); q3 *= QP(14); // apply Q point (quats are already unity vector)
if (quat_report == 0x05 || quat_report == 0x09 || quat_report == 0x28 ){ // heading accurracy only in some reports available
h_est = (((int16_t)cargo[22] << 8) | cargo[21] ); // heading accurracy estimation
h_est *= QP(12); // apply Q point
h_est *= radtodeg; // convert to degrees
}
}
}
//************************************************************************
// COMMANDS
//************************************************************************
/* This code activates quaternion output at defined rate
*/
void set_feature_cmd_QUAT(){ // quat_report determines the kind of quaternions (see data sheets)
uint8_t quat_setup[20] = {0,2,0,0xFD,quat_report,0,0,0,B0_rate,B1_rate,0,0,0,0,0,0,0,0,0,0};
I2c.write(BNO_ADDRESS, sizeof(quat_setup)+1, quat_setup, sizeof(quat_setup)); // sizeof(quat_setup)+1 is the LSB length in the SHTP header
}
//***********************************************************************************************************************************************
/* This code tares and stores results in flash after correct positioning and calibration (follow Tare procedure Hillcrest BNO080 Tare function Usage Guide 1000-4045)
* Make sure to run rotation vector (0x05) and watch h_est and stat (should be << 10 deg and 3 respectively)
* NOTE: tare and calibration are different things
*/
void TARE(){
uint8_t RV; // rotation vector used to tare defined in quat_report
if(quat_report == 0x05) RV = 0x00;
if(quat_report == 0x08) RV = 0x01;
if(quat_report == 0x09) RV = 0x02;
uint8_t tare_now[16] = {16,0,2,0,0xF2,0,0x03,0,0x07,RV,0,0,0,0,0,0}; //0x07 means all axes 0x04 = Z axis only; based on rotation vector
uint8_t tare_persist[16] = {16,0,2,0,0xF2,0,0x03,0x01,RV,0,0,0,0,0,0,0};
I2c.write(BNO_ADDRESS, sizeof(tare_now)+1, tare_now, sizeof(tare_now));
//I2c.write(BNO_ADDRESS, sizeof(tare_persist)+1, tare_persist, sizeof(tare_persist)); // uncomment for tare persist;
// NOTE: to undo tare persist requires calibration of the device followed by another tare persist with the device oriented y = north bfz = vertical before.
}
//***********************************************************************************************************************************************
/* The calibration data in RAM is always updated in background. 'Save DCD' push the RAM record into the FLASH for reload @ next boot.
The algorithm will only update the calibration data when it feels the current accuracy is good. You don't need to care about the status or heading error.
Save before power off or whenever you would like to do. (Hillcrest information)
*/
void save_DCD(){
uint8_t save_dcd[15] = {0,2,0,0xF2,0,0x06,0,0,0,0,0,0,0,0,0};
I2c.write(BNO_ADDRESS, sizeof(save_dcd)+1, save_dcd, sizeof(save_dcd));
}
//***********************************************************************************************************************************************
/* This code disables the calibration running in the background of the accelerometer gyro and magnetometer.
* sensors can be set individually on and off (chapter 6.4.7.1)
* P0 = accelerometer, P1 = gyro; P2 =magnetometer; P4 = planar accelerometer (without Z axis)
* 0 = disable; 1 = enable
*/
void ME_cal(uint8_t P0, uint8_t P1, uint8_t P2, uint8_t P4){
uint8_t me_cal[15] = {0,2,0,0xF2,0,0x07,P0,P1,P2,0,P4,0,0,0,0};
I2c.write(BNO_ADDRESS, sizeof(me_cal)+1, me_cal, sizeof(me_cal));
}
//***********************************************************************************************************************************************
/* Utilities (Quaternion mathematics)
// q0 = qw
q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3 = 1
// Gravity vector from quaternions
gx = q1 * q3 - q0 * q2; // R20
gy = q2 * q3 + q0 * q1; // R21
gz = q0 * q0 + q3 * q3 -0.5f; // R22
norm = sqrtf(gx * gx + gy * gy + gz * gz);
norm = 1.0f / norm;
gx *= norm; gy *= norm; gz *= norm;
// Euler angles from quaternions (radians)
yaw = atan2((q1 * q2 + q0 * q3), ((q0 * q0 + q1 * q1) - 0.5f));
pitch = -asin(2.0f * (q1 * q3 - q0 * q2));
roll = atan2((q0 * q1 + q2 * q3), ((q0 * q0 + q3 * q3) - 0.5f));
yaw *= radtodeg; pitch *= radtodeg; roll *= radtodeg;
# Rotation matrix (half)
R00 = q0 * q0 + q1 * q1 -0.5f;
R01 = q1 * q2 - q0 * q3;
R02 = q1 * q3 + q0 * q2;
R10 = q1 * q2 + q0 * q3;
R11 = q0 * q0 + q2 * q2 -0.5;
R12 = q2 * q3 - q0 * q1;
R20 = q1 * q3 - q0 * q2;
R21 = q2 * q3 + q0 * q1;
R22 = q0 * q0 + q3 * q3 -0.5f;
//***********************************************************************************
tare multiplication
//before tare store tare coefficients (^-1 = konjugate komplex)
t0 = q0;
t1 = -q1;
t2 = -q2;
t3 = -q3;
// qt are the tared coefficients
qt0 = q0 * t0 - q1 * t1 - q2 * t2 - q3 * t3; // multiplication matrix
qt1 = q0 * t1 + q1 * t0 + q2 * t3 - q3 * t2;
qt2 = q0 * t2 - q1 * t3 + q2 * t0 + q3 * t1;
qt3 = q0 * t3 + q1 * t2 - q2 * t1 + q3 * t0;
alternative:
//before tare store tare coefficients (^-1 = konjugate komplex)
t0 = q0;
t1 = q1;
t2 = q2;
t3 = q3;
// qt are the tared coefficients
qt0 = q0 * t0 + q1 * t1 + q2 * t2 + q3 * t3; // qt are the tared quaternions to achieve 1,0,0,0 at moment of tare
qt1 = -q0 * t1 + q1 * t0 - q2 * t3 + q3 * t2; // konjugated multiplication matrix t1, t2, t3 negative sign
qt2 = -q0 * t2 + q1 * t3 + q2 * t0 - q3 * t1;
qt3 = -q0 * t3 - q1 * t2 + q2 * t1 + q3 * t0;
//*************************************************************************************
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment