Code
[num].dmpGetQuaternion(&q, fifoBuffer);
dmpMpu[num].dmpGetGravity(&gravity, &q);
dmpMpu[num].dmpGetYawPitchRoll(ypr, &q, &gravity); dmpMpu
This document describes how the orientation of the IMU in terms of yaw,pitch, and roll is calculated from the information provided by the IMU (we use the MPU-9250).
The result of the calculations in the Digital Motion Processor (DMP) done in the IMU are reported as quaternions (basically a short coding of the rotation between the IMU and the fixed laboratory system). Technically they are send to the Aduino (as a byte vector the fifoBuffer
). On the Aduino the following steps happen:
The result of the FIFO Buffer is parsed and written in a Quaternion with components \(q=(q_w, q_x, q_y, q_z)\) this is done in `dmpGetQuaternion`
The vector of gravity is calculated in dmpGetGravity
From that vector (and the gravity) the yaw, pitch, and roll angles are calculated `dmpGetYawPitchRoll`
[num].dmpGetQuaternion(&q, fifoBuffer);
dmpMpu[num].dmpGetGravity(&gravity, &q);
dmpMpu[num].dmpGetYawPitchRoll(ypr, &q, &gravity); dmpMpu
In the first line the quaternion send from the DSP is read from the fifoBuffer
and parsed into the variable q
. The quaternion q
contains all information about a single IMU. This is done with the following code:
uint8_t MPU6050::dmpGetQuaternion(int16_t *data, const uint8_t* packet) {
// TODO: accommodate different arrangements of sent data (ONLY default supported now)
if (packet == 0) packet = dmpPacketBuffer;
[0] = ((packet[0] << 8) | packet[1]);
data[1] = ((packet[4] << 8) | packet[5]);
data[2] = ((packet[8] << 8) | packet[9]);
data[3] = ((packet[12] << 8) | packet[13]);
datareturn 0;
}
uint8_t MPU6050::dmpGetQuaternion(Quaternion *q, const uint8_t* packet) {
// TODO: accommodate different arrangements of sent data (ONLY default supported now)
int16_t qI[4];
uint8_t status = dmpGetQuaternion(qI, packet);
if (status == 0) {
-> w = (float)qI[0] / 16384.0f;
q -> x = (float)qI[1] / 16384.0f;
q -> y = (float)qI[2] / 16384.0f;
q -> z = (float)qI[3] / 16384.0f;
q return 0;
}
return status; // int16 return value, indicates error if this line is reached
}
The yaw, pitch, roll information is calculated from the quaternion in two steps. First the vector pointing to the direction of gravity is calculated in dmpGetGravity
This is the code from MPU6050_6Axis_MotionApps20.h
uint8_t MPU6050::dmpGetGravity(VectorFloat *v, Quaternion *q) {
-> x = 2 * (q -> x*q -> z - q -> w*q -> y);
v -> y = 2 * (q -> w*q -> x + q -> y*q -> z);
v -> z = q -> w*q -> w - q -> x*q -> x - q -> y*q -> y + q -> z*q -> z;
v return 0;
}
Mathematically speaking this code constructs the vector of gravity as follows:
\[ \begin{align} v_x &= 2 (q_x q_z - q_w q_y) \\ v_y &= 2 (q_w q_x + q_y q_z) \\ v_z &= q_w^2 - q_x^2 - q_y^2 + q_z^2 \end{align} \]
This translates into the following R code:
= function(q){
dmpGetGravity = q[1]
qw = q[2]
qx = q[3]
qy = q[4]
qz = 2 * (qx * qz - qw*qy)
vx = 2 * (qw * qx + qy*qz)
vy = qw^2 - qx^2 - qy^2 + qz^2
vz return(c(vx,vy,vz))
}= as.matrix(c(0.32, 0.30, 0.29, -0.85), ncol=1)
q.vec dmpGetGravity(q.vec)
[1] -0.6956 -0.3010 0.6508
What is happening in the code? First, we note that quaternion \(q\), given back from the IMU transforms from the IMU coordinate system into the lab system. To make this clear, we write \(g = g_{L \leftarrow I}\) in the following. We are asking “How does the gravity” looks like on the IMU \(g_{I}\)? The gravity in the laboratory frame is given by \(g_{L}=(0,0,1)\). Note that, we could also define a different lab frame in which the gravity is e.q. given by \((0,1,0)\). From the Quaternion Algebra (see e.g. Note_on_Quaternion), we know that (L = Lab, I = IMU):
\[ q_{L} = q_{L \leftarrow I} \; q_{I} \; q_{L \leftarrow I}^{-1} \]
Where \(q_{\tt{L}}=(0,0,0,1)\) and \(q_{I}=(0,g_x, g_y, g_z)\) are pure quaternions, i.e. they take the vectors and set the real part \(w\) to 0. To solve for \(q_{IMU}\), we multiply with \(q^{-1}\) and \(q\) from left and right. Getting:
\[ q_{I} = q_{L \leftarrow I}^{-1} \; q_{L} \; q_{L \leftarrow I} = q_{I \leftarrow L} \; q_{L} \; q_{I \leftarrow L}^{-1} \]
We can verify this with the R-Code:
= as.quaternion(q.vec)
q = quaternion(Re = 0, k = 1)
qz qz
[1]
Re 0
i 0
j 0
k 1
^-1 * qz * q q
Re
Re -2.220446e-16
i -6.962963e-01
j -3.013013e-01
k 6.514515e-01
= dmpGetGravity(q.vec)
gravity gravity
[1] -0.6956 -0.3010 0.6508
The code in MPU6050_6Axis_MotionApps20.h
used looks like follows:
uint8_t MPU6050::dmpGetYawPitchRoll(float *data, Quaternion *q, VectorFloat *gravity) {
// yaw: (about Z axis)
[0] = atan2(2*q -> x*q -> y - 2*q -> w*q -> z, 2*q -> w*q -> w + 2*q -> x*q -> x - 1);
data// pitch: (nose up/down, about Y axis)
[1] = atan(gravity -> x / sqrt(gravity -> y*gravity -> y + gravity -> z*gravity -> z));
data// roll: (tilt left/right, about X axis)
[2] = atan(gravity -> y / sqrt(gravity -> x*gravity -> x + gravity -> z*gravity -> z));
datareturn 0;
}
In math:
$$ \[\begin{align} {\tt yaw} &= {\tt atan2} (2 q_x q_y - 2 q_w q_z, 2 q_w^2 + 2 q_x^2 -1) \\ {\tt pitch} &= {\tt atan} (\frac{g_x}{\sqrt{g_y^2 + g_z ^2}}) \\ {\tt roll} &= {\tt atan} (\frac{g_y}{\sqrt{g_x^2 + g_z ^2}} ) \end{align}\] $$
This translate to the following R-Code.
= function(q, gravity){
dmpGetYawPitchRoll = q[1]
qw = q[2]
qx = q[3]
qy = q[4]
qz = atan2(2*qx*qy - 2*qw*qz, 2*qw*qw+2*qx*qx-1)
yaw = atan(gravity[1]/sqrt(gravity[2]^2+gravity[3]^2))
pitch = atan(gravity[2]/sqrt(gravity[1]^2+gravity[3]^2))
roll return(c(yaw, pitch, roll))
}= dmpGetYawPitchRoll(q@x, gravity)
as_arduino round(as_arduino,2)
[1] 2.28 -0.77 -0.31
The result from the Arduino is: (2.28, -0.77, -0.31). We can reproduce the results from the Arduino (see Comparison for more examples).
For yaw it extracts the angle using quaternions. For pitch it uses the gravity calculated before (in the IMU space). Let’s start with the pitch and roll. From (https://ahrs.readthedocs.io/en/latest/filters/tilt.html we find (with \(a\) being the normalized gravity vector in the IMU space).
\[ \begin{align} {\tt pitch} &= \arctan2(-a_x, \sqrt{a_y^2 + a_z^2}) \\ {\tt roll} &= \arctan2(a_y, a_z) \end{align} \]
Pitch:
#Pitch as As done on Aduino
atan(gravity[1] / sqrt(gravity[2]^2 + gravity[3]^2))
[1] -0.7702244
#Pitch from
= gravity/sqrt(sum(gravity^2)) #Normalizing (we are almost normalized)
a atan2(-a[1], sqrt(a[2]^2 + a[3]^2))
[1] 0.7702244
Except the sign, we got the same value as the code on the Arduino.
Roll:
#Roll
atan(gravity[2] / sqrt(gravity[1]^2 + gravity[3]^2)) #As done in Arduino
[1] -0.3060571
atan2(gravity[2], gravity[3])
[1] -0.4332065
For the roll, we get a different value compared to the Arduino.
Usually the YPR is calculated differently not via the gravity first. In the following we compare different solutions, we found on the internet. The all use more or less the following
\[ \begin{align*} \text{roll} &= \text{atan2}(2 \cdot (w \cdot x + y \cdot z), 1 - 2 \cdot (x^2 + y^2)) \\ \text{pitch} &= \text{asin}(2 \cdot (w \cdot y - z \cdot x)) \\ \text{yaw} &= \text{atan2}(2 \cdot (w \cdot z + x \cdot y), 1 - 2 \cdot (y^2 + z^2)) \end{align*} \]
= function(q){
quart_to_ypr_as_arduino = dmpGetGravity(q)
gr return(dmpGetYawPitchRoll(q, gr))
}#From poyu (value9250.py)
#def quart_to_rpy(w, x, y, z):
# roll = math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y))
# pitch = math.asin(2 * (w * y - x * z))
# yaw = math.atan2(2 * (w * z + x * y), 1 - 2 * (z * z + y * y))
# return yaw, pitch, roll
= function(q){
quart_to_ypr = q[1]
w = q[2]
x = q[3]
y = q[4]
z = atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y))
roll = asin(2 * (w * y - x * z))
pitch = atan2(2 * (w * z + x * y), 1 - 2 * (z * z + y * y))
yaw return(c(yaw,pitch,roll))
}
quart_to_ypr_as_arduino(as.numeric(q))
[1] 2.2792391 -0.7702244 -0.3060571
quart_to_ypr(as.numeric(q))
[1] -2.5986827 0.7692547 -0.4326218
Note that the quaternion can be understood as a transformation of the IMU system into the fixed space system. Given a rotation about \(\theta\) and the rotational \(v_1, v_2, v_3\) the quaternion would be
\[ q=(\cos(\theta/2), \sin(\theta/2)v_1,\sin(\theta/2)v_2, \sin(\theta/2)v_3) \]
Let’s construct some special quaternions and see if the results are correct. Checking the yaw
= c(0.30, 0, 0, 1.0)
q = q / sqrt(sum(q^2))
q 2*acos(q[1])
[1] 2.558679
= c('yaw', 'pitch', 'roll')
rnames data.frame(
arduino = quart_to_ypr_as_arduino(q),
quart_to_ypr = quart_to_ypr(q),
row.names = rnames
)
arduino quart_to_ypr
yaw -2.558679 2.558679
pitch 0.000000 0.000000
roll 0.000000 0.000000
= c(0.10, 0, 1.0, 0.0)
q = q / sqrt(sum(q^2))
q #The angle is
2*acos(q[1])
[1] 2.942255
#of
- 2*acos(q[1]) pi
[1] 0.1993373
data.frame(
arduino = quart_to_ypr_as_arduino(q),
quart_to_ypr = quart_to_ypr(q),
row.names = rnames
)
arduino quart_to_ypr
yaw 3.1415927 3.1415927
pitch -0.1993373 0.1993373
roll 0.0000000 3.1415927
There seems to be a numerical artifact.
Let’s look at a single roll about the angle:
= c(0.30, 1.0, 0, 0.0)
q = q / sqrt(sum(q^2))
q 2*acos(q[1])
[1] 2.558679
data.frame(
arduino = quart_to_ypr_as_arduino(q),
quart_to_ypr = quart_to_ypr(q),
row.names = rnames
%>%
) ::kable() kableExtra
arduino | quart_to_ypr | |
---|---|---|
yaw | 0.0000000 | 0.000000 |
pitch | 0.0000000 | 0.000000 |
roll | 0.5829136 | 2.558679 |
This code shows that, we can reproduce the calculation done on the Ardunio for more examples.
#Loading
<- read.csv("../../data/value06.csv",header=TRUE)
training = rename(training, IMU=X)
training
$IMU = as.factor(training$IMU)
training#nrow(training)
$t=rep(1:(1+nrow(training)/6), each=6)[1:nrow(training)]
training
= training %>% filter(IMU != "0")
dat = dat %>% select(starts_with('q')) dat_q
#YPR Calculation
= matrix(NA, ncol=3, nrow = nrow(dat_q))
ypr_calc for (i in 1:nrow(dat_q)){
= dmpGetGravity(t(dat_q[i,]))
gr = dmpGetYawPitchRoll(t(dat_q[i,]), gr)
ypr_calc[i,]
}= 360/(2*pi) r2d
plot(r2d*ypr_calc[,1], r2d*dat$yaw, main='Calculated YAW (R vs Aduino) in degrees')
plot(r2d*ypr_calc[,2], r2d*dat$pitch, main='Calculated Pitch (R vs Aduino) in degress')
plot(r2d*ypr_calc[,3], r2d*dat$roll, main='Calculated Roll (R vs Aduino) in degrees')
These plots show that, we can reproduce the Aduino values in R.