Skip to content

Commit e3961b1

Browse files
committed
V0.1 Alpha
1 parent 8775fd4 commit e3961b1

File tree

4 files changed

+398
-1
lines changed

4 files changed

+398
-1
lines changed

README.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,43 @@
11
# micropython-fusion
2-
Sensor fusion calculating yaw, pitch and roll from the outputs of motion tracking devices
2+
Sensor fusion calculating yaw, pitch and roll from the outputs of motion tracking devices. This
3+
uses the Madgwick algorithm, widely used in multicopter designs for its speed and quality. An
4+
update takes about 1.6mS on the Pyboard
5+
6+
### Fusion Class
7+
8+
The module supports this one class. A Fusion object needs to be periodically updated with data
9+
from the sensor. When queried by the ``angle`` method, it returns yaw, pitch and roll values in
10+
degrees. Note that if you use a 6 degrees of freedom (DOF) sensor, yaw will be invalid.
11+
12+
Methods
13+
-------
14+
15+
```angle()```
16+
Return yaw, pitch and roll in degrees
17+
18+
```update(accel, gyro, mag)```
19+
For 9DOF sensors. Accepts 3-tuples (x, y, z) of accelerometer, gyro and magnetometer data and
20+
updates the filters. This should be called periodically, depending on the required response
21+
speed. Units:
22+
accel: Typically G. Values are normalised in the algorithm so units are irrelevant.
23+
gyro: Degrees per second.
24+
magnetometer: Microteslas.
25+
26+
```update_nomag(accel, gyro)```
27+
For 6DOF sensors. Accepts 3-tuples (x, y, z) of accelerometer and gyro data and
28+
updates the filters. This should be called periodically, depending on the required response
29+
speed. Units:
30+
accel: Typically G. Values are normalised in the algorithm so units are irrelevant.
31+
gyro: Degrees per second.
32+
33+
### Notes for beginners
34+
35+
If you're designing a machine using a motion sensing device consider the effects of vibration.
36+
This can be surprisingly high (use the sensor to measure it). No amount of digital filtering
37+
will remove it because it is likely to contain frequencies above the sampling rate of the sensor:
38+
these will be aliased down into the filter passband and affect the results. It's normally
39+
neccessary to isolate the sensor with a mechanical filter, typically a mass supported on very
40+
soft rubber mounts.
41+
42+
Update rate - TODO
43+

fusion.py

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
# Sensor fusion for the micropython board. 21st May 2015
2+
# Ported to Python by Peter Hinch
3+
# V0.1 Experimental
4+
5+
import pyb
6+
from math import sqrt, atan2, asin, degrees, radians
7+
'''
8+
Supports 6 and 9 degrees of freedom sensors. Tested with InvenSense MPU-9150 9DOF sensor.
9+
Source https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU.git
10+
also https://github.com/kriswiner/MPU-9250.git
11+
Ported to Python. Integrator timing adapted for pyboard.
12+
User should repeatedly call the appropriate update method and extract the yaw pitch and roll angles as
13+
reuired using the angles method.
14+
'''
15+
class Fusion(object):
16+
'''
17+
Class provides sensor fusion allowing yaw, pitch and roll to be extracted. This uses the Madgwick algorithm.
18+
The update method must be called peiodically. The calculations take 1.6mS on the Pyboard. Suggested update
19+
rate 50Hz (20mS)
20+
'''
21+
magbias = [0, 0, 0] # local bias factors: average value of each axis when rotated 2*pi radians
22+
declination = 0 # Option to add offset for true north
23+
def __init__(self):
24+
self.start_time = None # Record time between updates
25+
self.q = [1.0, 0.0, 0.0, 0.0] # vector to hold quaternion
26+
GyroMeasError = radians(40) # Original code indicates this leads to a 2 sec response time
27+
self.beta = sqrt(3.0 / 4.0) * GyroMeasError # compute beta
28+
29+
def angles(self):
30+
yaw = degrees(atan2(2.0 * (self.q[1] * self.q[2] + self.q[0] * self.q[3]),
31+
self.q[0] * self.q[0] + self.q[1] * self.q[1] - self.q[2] * self.q[2] - self.q[3] * self.q[3]))
32+
pitch = degrees(-asin(2.0 * (self.q[1] * self.q[3] - self.q[0] * self.q[2])))
33+
roll = degrees(atan2(2.0 * (self.q[0] * self.q[1] + self.q[2] * self.q[3]),
34+
self.q[0] * self.q[0] - self.q[1] * self.q[1] - self.q[2] * self.q[2] + self.q[3] * self.q[3]))
35+
return yaw + self.declination, pitch, roll
36+
37+
def update(self, accel, gyro, mag): # 3-tuples (x, y, z) for accel, gyro and mag data
38+
mx, my, mz = (mag[x] - self.magbias[x] for x in range(3)) # Units uT
39+
ax, ay, az = accel # Units G (but later normalised)
40+
gx, gy, gz = (radians(x) for x in gyro) # Units deg/s
41+
if self.start_time is None:
42+
self.start_time = pyb.micros() # First run
43+
q1, q2, q3, q4 = (self.q[x] for x in range(4)) # short name local variable for readability
44+
# Auxiliary variables to avoid repeated arithmetic
45+
_2q1 = 2.0 * q1
46+
_2q2 = 2.0 * q2
47+
_2q3 = 2.0 * q3
48+
_2q4 = 2.0 * q4
49+
_2q1q3 = 2.0 * q1 * q3
50+
_2q3q4 = 2.0 * q3 * q4
51+
q1q1 = q1 * q1
52+
q1q2 = q1 * q2
53+
q1q3 = q1 * q3
54+
q1q4 = q1 * q4
55+
q2q2 = q2 * q2
56+
q2q3 = q2 * q3
57+
q2q4 = q2 * q4
58+
q3q3 = q3 * q3
59+
q3q4 = q3 * q4
60+
q4q4 = q4 * q4
61+
62+
# Normalise accelerometer measurement
63+
norm = sqrt(ax * ax + ay * ay + az * az)
64+
if (norm == 0.0):
65+
return # handle NaN
66+
norm = 1.0/norm
67+
ax *= norm
68+
ay *= norm
69+
az *= norm
70+
71+
# Normalise magnetometer measurement
72+
norm = sqrt(mx * mx + my * my + mz * mz)
73+
if (norm == 0.0):
74+
return # handle NaN
75+
norm = 1.0/norm
76+
mx *= norm
77+
my *= norm
78+
mz *= norm
79+
80+
# Reference direction of Earth's magnetic field
81+
_2q1mx = 2.0 * q1 * mx
82+
_2q1my = 2.0 * q1 * my
83+
_2q1mz = 2.0 * q1 * mz
84+
_2q2mx = 2.0 * q2 * mx
85+
hx = mx * q1q1 - _2q1my * q4 + _2q1mz * q3 + mx * q2q2 + _2q2 * my * q3 + _2q2 * mz * q4 - mx * q3q3 - mx * q4q4
86+
hy = _2q1mx * q4 + my * q1q1 - _2q1mz * q2 + _2q2mx * q3 - my * q2q2 + my * q3q3 + _2q3 * mz * q4 - my * q4q4
87+
_2bx = sqrt(hx * hx + hy * hy)
88+
_2bz = -_2q1mx * q3 + _2q1my * q2 + mz * q1q1 + _2q2mx * q4 - mz * q2q2 + _2q3 * my * q4 - mz * q3q3 + mz * q4q4
89+
_4bx = 2.0 * _2bx
90+
_4bz = 2.0 * _2bz
91+
92+
# Gradient descent algorithm corrective step
93+
s1 = (-_2q3 * (2.0 * q2q4 - _2q1q3 - ax) + _2q2 * (2.0 * q1q2 + _2q3q4 - ay) - _2bz * q3 *
94+
(_2bx * (0.5 - q3q3 - q4q4) + _2bz * (q2q4 - q1q3) - mx) + (-_2bx * q4 + _2bz * q2) *
95+
(_2bx * (q2q3 - q1q4) + _2bz * (q1q2 + q3q4) - my) + _2bx * q3 * (_2bx * (q1q3 + q2q4) +
96+
_2bz * (0.5 - q2q2 - q3q3) - mz))
97+
s2 = (_2q4 * (2.0 * q2q4 - _2q1q3 - ax) + _2q1 * (2.0 * q1q2 + _2q3q4 - ay) - 4.0 * q2 *
98+
(1.0 - 2.0 * q2q2 - 2.0 * q3q3 - az) + _2bz * q4 * (_2bx * (0.5 - q3q3 - q4q4) + _2bz *
99+
(q2q4 - q1q3) - mx) + (_2bx * q3 + _2bz * q1) * (_2bx * (q2q3 - q1q4) + _2bz *
100+
(q1q2 + q3q4) - my) + (_2bx * q4 - _4bz * q2) * (_2bx * (q1q3 + q2q4) + _2bz * (0.5 - q2q2 - q3q3) - mz))
101+
s3 = (-_2q1 * (2.0 * q2q4 - _2q1q3 - ax) + _2q4 * (2.0 * q1q2 + _2q3q4 - ay) - 4.0 * q3 *
102+
(1.0 - 2.0 * q2q2 - 2.0 * q3q3 - az) + (-_4bx * q3 - _2bz * q1) * (_2bx * (0.5 - q3q3 - q4q4) +
103+
_2bz * (q2q4 - q1q3) - mx) + (_2bx * q2 + _2bz * q4) * (_2bx * (q2q3 - q1q4) +
104+
_2bz * (q1q2 + q3q4) - my) + (_2bx * q1 - _4bz * q3) * (_2bx * (q1q3 + q2q4) + _2bz * (0.5 - q2q2 - q3q3) - mz))
105+
s4 = (_2q2 * (2.0 * q2q4 - _2q1q3 - ax) + _2q3 * (2.0 * q1q2 + _2q3q4 - ay) + (-_4bx * q4 + _2bz * q2) *
106+
(_2bx * (0.5 - q3q3 - q4q4) + _2bz * (q2q4 - q1q3) - mx) + (-_2bx * q1 + _2bz * q3) *
107+
(_2bx * (q2q3 - q1q4) + _2bz * (q1q2 + q3q4) - my) + _2bx * q2 * (_2bx * (q1q3 + q2q4) +
108+
_2bz * (0.5 - q2q2 - q3q3) - mz))
109+
norm = sqrt(s1 * s1 + s2 * s2 + s3 * s3 + s4 * s4) # normalise step magnitude
110+
norm = 1.0/norm
111+
s1 *= norm
112+
s2 *= norm
113+
s3 *= norm
114+
s4 *= norm
115+
116+
# Compute rate of change of quaternion
117+
qDot1 = 0.5 * (-q2 * gx - q3 * gy - q4 * gz) - self.beta * s1
118+
qDot2 = 0.5 * (q1 * gx + q3 * gz - q4 * gy) - self.beta * s2
119+
qDot3 = 0.5 * (q1 * gy - q2 * gz + q4 * gx) - self.beta * s3
120+
qDot4 = 0.5 * (q1 * gz + q2 * gy - q3 * gx) - self.beta * s4
121+
122+
# Integrate to yield quaternion
123+
deltat = pyb.elapsed_micros(self.start_time) / 1000000
124+
self.start_time = pyb.micros()
125+
q1 += qDot1 * deltat
126+
q2 += qDot2 * deltat
127+
q3 += qDot3 * deltat
128+
q4 += qDot4 * deltat
129+
norm = sqrt(q1 * q1 + q2 * q2 + q3 * q3 + q4 * q4) # normalise quaternion
130+
norm = 1.0/norm
131+
self.q = q1 * norm, q2 * norm, q3 * norm, q4 * norm
132+
133+
def update_nomag(self, accel, gyro): # 3-tuples (x, y, z) for accel, gyro
134+
ax, ay, az = accel # Units G (but later normalised)
135+
gx, gy, gz = (radians(x) for x in gyro) # Units deg/s
136+
if self.start_time is None:
137+
self.start_time = pyb.micros() # First run
138+
q1, q2, q3, q4 = (self.q[x] for x in range(4)) # short name local variable for readability
139+
# Auxiliary variables to avoid repeated arithmetic
140+
_2q1 = 2 * q1
141+
_2q2 = 2 * q2
142+
_2q3 = 2 * q3
143+
_2q4 = 2 * q4
144+
_4q1 = 4 * q1
145+
_4q2 = 4 * q2
146+
_4q3 = 4 * q3
147+
_8q2 = 8 * q2
148+
_8q3 = 8 * q3
149+
q1q1 = q1 * q1
150+
q2q2 = q2 * q2
151+
q3q3 = q3 * q3
152+
q4q4 = q4 * q4
153+
154+
# Normalise accelerometer measurement
155+
norm = sqrt(ax * ax + ay * ay + az * az)
156+
if (norm == 0):
157+
return # handle NaN
158+
norm = 1 / norm # use reciprocal for division
159+
ax *= norm
160+
ay *= norm
161+
az *= norm
162+
163+
# Gradient decent algorithm corrective step
164+
s1 = _4q1 * q3q3 + _2q3 * ax + _4q1 * q2q2 - _2q2 * ay
165+
s2 = _4q2 * q4q4 - _2q4 * ax + 4 * q1q1 * q2 - _2q1 * ay - _4q2 + _8q2 * q2q2 + _8q2 * q3q3 + _4q2 * az
166+
s3 = 4 * q1q1 * q3 + _2q1 * ax + _4q3 * q4q4 - _2q4 * ay - _4q3 + _8q3 * q2q2 + _8q3 * q3q3 + _4q3 * az
167+
s4 = 4 * q2q2 * q4 - _2q2 * ax + 4 * q3q3 * q4 - _2q3 * ay
168+
norm = 1 / sqrt(s1 * s1 + s2 * s2 + s3 * s3 + s4 * s4) # normalise step magnitude
169+
s1 *= norm
170+
s2 *= norm
171+
s3 *= norm
172+
s4 *= norm
173+
174+
# Compute rate of change of quaternion
175+
qDot1 = 0.5 * (-q2 * gx - q3 * gy - q4 * gz) - self.beta * s1
176+
qDot2 = 0.5 * (q1 * gx + q3 * gz - q4 * gy) - self.beta * s2
177+
qDot3 = 0.5 * (q1 * gy - q2 * gz + q4 * gx) - self.beta * s3
178+
qDot4 = 0.5 * (q1 * gz + q2 * gy - q3 * gx) - self.beta * s4
179+
180+
# Integrate to yield quaternion
181+
deltat = pyb.elapsed_micros(self.start_time) / 1000000
182+
self.start_time = pyb.micros()
183+
q1 += qDot1 * deltat
184+
q2 += qDot2 * deltat
185+
q3 += qDot3 * deltat
186+
q4 += qDot4 * deltat
187+
norm = 1 / sqrt(q1 * q1 + q2 * q2 + q3 * q3 + q4 * q4) # normalise quaternion
188+
self.q = q1 * norm, q2 * norm, q3 * norm, q4 * norm
189+
190+
def Update(self, accel, gyro, mag): # 3-tuples (x, y, z) for accel, gyro and mag data
191+
mx, my, mz = (mag[x] - self.magbias[x] for x in range(3)) # Units uT
192+
ax, ay, az = accel # Units G (but later normalised)
193+
gx, gy, gz = (radians(x) for x in gyro) # Units deg/s
194+
if self.start_time is None:
195+
self.start_time = pyb.micros() # First run
196+
q1, q2, q3, q4 = (self.q[x] for x in range(4)) # short name local variable for readability
197+
# Auxiliary variables to avoid repeated arithmetic
198+
_2q1 = 2 * q1
199+
_2q2 = 2 * q2
200+
_2q3 = 2 * q3
201+
_2q4 = 2 * q4
202+
_2q1q3 = 2 * q1 * q3
203+
_2q3q4 = 2 * q3 * q4
204+
q1q1 = q1 * q1
205+
q1q2 = q1 * q2
206+
q1q3 = q1 * q3
207+
q1q4 = q1 * q4
208+
q2q2 = q2 * q2
209+
q2q3 = q2 * q3
210+
q2q4 = q2 * q4
211+
q3q3 = q3 * q3
212+
q3q4 = q3 * q4
213+
q4q4 = q4 * q4
214+
215+
# Normalise accelerometer measurement
216+
norm = sqrt(ax * ax + ay * ay + az * az)
217+
if (norm == 0):
218+
return # handle NaN
219+
norm = 1 / norm # use reciprocal for division
220+
ax *= norm
221+
ay *= norm
222+
az *= norm
223+
224+
# Normalise magnetometer measurement
225+
norm = sqrt(mx * mx + my * my + mz * mz)
226+
if (norm == 0):
227+
return # handle NaN
228+
norm = 1 / norm # use reciprocal for division
229+
mx *= norm
230+
my *= norm
231+
mz *= norm
232+
233+
# Reference direction of Earth's magnetic field
234+
_2q1mx = 2 * q1 * mx
235+
_2q1my = 2 * q1 * my
236+
_2q1mz = 2 * q1 * mz
237+
_2q2mx = 2 * q2 * mx
238+
hx = mx * q1q1 - _2q1my * q4 + _2q1mz * q3 + mx * q2q2 + _2q2 * my * q3 + _2q2 * mz * q4 - mx * q3q3 - mx * q4q4
239+
hy = _2q1mx * q4 + my * q1q1 - _2q1mz * q2 + _2q2mx * q3 - my * q2q2 + my * q3q3 + _2q3 * mz * q4 - my * q4q4
240+
_2bx = sqrt(hx * hx + hy * hy)
241+
_2bz = -_2q1mx * q3 + _2q1my * q2 + mz * q1q1 + _2q2mx * q4 - mz * q2q2 + _2q3 * my * q4 - mz * q3q3 + mz * q4q4
242+
_4bx = 2 * _2bx
243+
_4bz = 2 * _2bz
244+
245+
# Gradient descent algorithm corrective step
246+
s1 = (-_2q3 * (2 * q2q4 - _2q1q3 - ax) + _2q2 * (2 * q1q2 + _2q3q4 - ay) - _2bz * q3 * (_2bx * (0.5 - q3q3 - q4q4)
247+
+ _2bz * (q2q4 - q1q3) - mx) + (-_2bx * q4 + _2bz * q2) * (_2bx * (q2q3 - q1q4) + _2bz * (q1q2 + q3q4) - my)
248+
+ _2bx * q3 * (_2bx * (q1q3 + q2q4) + _2bz * (0.5 - q2q2 - q3q3) - mz))
249+
250+
s2 = (_2q4 * (2 * q2q4 - _2q1q3 - ax) + _2q1 * (2 * q1q2 + _2q3q4 - ay) - 4 * q2 * (1 - 2 * q2q2 - 2 * q3q3 - az)
251+
+ _2bz * q4 * (_2bx * (0.5 - q3q3 - q4q4) + _2bz * (q2q4 - q1q3) - mx) + (_2bx * q3 + _2bz * q1) * (_2bx * (q2q3 - q1q4)
252+
+ _2bz * (q1q2 + q3q4) - my) + (_2bx * q4 - _4bz * q2) * (_2bx * (q1q3 + q2q4) + _2bz * (0.5 - q2q2 - q3q3) - mz))
253+
254+
s3 = (-_2q1 * (2 * q2q4 - _2q1q3 - ax) + _2q4 * (2 * q1q2 + _2q3q4 - ay) - 4 * q3 * (1 - 2 * q2q2 - 2 * q3q3 - az)
255+
+ (-_4bx * q3 - _2bz * q1) * (_2bx * (0.5 - q3q3 - q4q4) + _2bz * (q2q4 - q1q3) - mx)
256+
+ (_2bx * q2 + _2bz * q4) * (_2bx * (q2q3 - q1q4) + _2bz * (q1q2 + q3q4) - my)
257+
+ (_2bx * q1 - _4bz * q3) * (_2bx * (q1q3 + q2q4) + _2bz * (0.5 - q2q2 - q3q3) - mz))
258+
259+
s4 = (_2q2 * (2 * q2q4 - _2q1q3 - ax) + _2q3 * (2 * q1q2 + _2q3q4 - ay) + (-_4bx * q4 + _2bz * q2) * (_2bx * (0.5 - q3q3 - q4q4)
260+
+ _2bz * (q2q4 - q1q3) - mx) + (-_2bx * q1 + _2bz * q3) * (_2bx * (q2q3 - q1q4) + _2bz * (q1q2 + q3q4) - my)
261+
+ _2bx * q2 * (_2bx * (q1q3 + q2q4) + _2bz * (0.5 - q2q2 - q3q3) - mz))
262+
263+
norm = 1 / sqrt(s1 * s1 + s2 * s2 + s3 * s3 + s4 * s4) # normalise step magnitude
264+
s1 *= norm
265+
s2 *= norm
266+
s3 *= norm
267+
s4 *= norm
268+
269+
# Compute rate of change of quaternion
270+
qDot1 = 0.5 * (-q2 * gx - q3 * gy - q4 * gz) - self.beta * s1
271+
qDot2 = 0.5 * (q1 * gx + q3 * gz - q4 * gy) - self.beta * s2
272+
qDot3 = 0.5 * (q1 * gy - q2 * gz + q4 * gx) - self.beta * s3
273+
qDot4 = 0.5 * (q1 * gz + q2 * gy - q3 * gx) - self.beta * s4
274+
275+
# Integrate to yield quaternion
276+
deltat = pyb.elapsed_micros(self.start_time) / 1000000
277+
self.start_time = pyb.micros()
278+
q1 += qDot1 * deltat
279+
q2 += qDot2 * deltat
280+
q3 += qDot3 * deltat
281+
q4 += qDot4 * deltat
282+
norm = 1 / sqrt(q1 * q1 + q2 * q2 + q3 * q3 + q4 * q4) # normalise quaternion
283+
self.q = q1 * norm, q2 * norm, q3 * norm, q4 * norm

fusionlcd.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import gc
2+
from mpu9150 import MPU9150
3+
from fusion import Fusion
4+
from usched import Sched, wait, Poller
5+
from lcdthread import LCD, PINLIST # Library supporting Hitachi LCD module
6+
7+
imu = MPU9150('Y', 1, True)
8+
imu.gyro_range(0)
9+
imu.accel_range(0)
10+
11+
fuse = Fusion()
12+
13+
def makefilt(delta, T = None): # Build a single pole IIR filter with time conatant T
14+
if T is None: # For testing
15+
alpha, beta = 0, 1
16+
else:
17+
alpha = 1 - delta/T
18+
beta = 1 - alpha
19+
output = 0
20+
def runfilt(inp):
21+
nonlocal output
22+
output = alpha * output + beta * inp
23+
return output
24+
return runfilt
25+
26+
gc.disable()
27+
# In systems with a continuously running loop, to avoid occasional delays
28+
# of around 2mS do a manual gc on each pass
29+
30+
def lcd_thread(mylcd, imu):
31+
mylcd[0] = "{:8s}{:8s}{:8s}".format("Yaw","Pitch","Roll")
32+
count = 0
33+
while True:
34+
yield from wait(0.02) # IMU updates at 50Hz
35+
count += 1
36+
if count % 25 == 0:
37+
yaw, pitch, roll = fuse.angles()
38+
mylcd[1] = "{:7.0f} {:7.0f} {:7.0f}".format(yaw, pitch, roll)
39+
fuse.Update(imu.get_accel(), imu.get_gyro(), imu.get_mag()) # Note blocking mag read
40+
# fuse.update_nomag(imu.get_accel(), imu.get_gyro())
41+
gc.collect()
42+
43+
objSched = Sched()
44+
lcd0 = LCD(PINLIST, objSched, cols = 24)
45+
objSched.add_thread(lcd_thread(lcd0, imu))
46+
objSched.run()
47+
gc.enable()

0 commit comments

Comments
 (0)