Why six years?

It probably took a years at the beginning to get the system working at a basic level, and a couple of years at the end adding cool features like GPS location tracking, object-avoidance, human remote-control, custom cool lids etc.  So what happened in the intermediate three years?

The biggest timer-killer was drift in a hover: the accelerometer measures gravity and real acceleration along three perpendicular axes.  At the start of a flight, it reads a value for gravity before takeoff; during the flight, integration of new readings against the initial gravity reading provides velocity.  In a calm summer’s day, all worked well, but in other conditions, there were often collisions with brick-walls etc.  Essentially, the sensors say they weren’t moving whereas reality said it was.  It took years to recognise the correlation between winds, weather and crashes: lots of iterative speculation spanning the seasons were required to recognise the link to temperature variations*.

There are two factors: first, the IMU has been optimised to operate above 20°C – below that and small temperature changes lead to significant drift in values; secondly, once in flight, weather- and prop-breeze cools the sensor compared to the ground measurement of gravity.  I tried lots of ways to handle the temperature changes; at one point, I even bought a beer-fridge for mapping accelerometer gravity values against temperature!  It’s back to its intentional purpose now.

Digging back, it’s clear how others have coped:

  • DIY RC drones used synchronising the IMU and RC each flight along with sheltering the IMU from the wind.  The pilot wasn’t interested in static hover and is part of the feedback loop for where it to go.
  • at the bleeding edge, the DJI Mavic has a dynamic cooling system embedded in the depths of the frame keeping the IMU at a fixed temperature, along with two ground-facing cameras and GPS to long-term fine tuning.
  • All videos I saw were from the California coastline!!!

But I did it my way, the DIY experimentation way resulting ultimately with passive temperature stability by wrapping the IMU in a case to suppress wind cooling, combined with a Butterworth low-pass filter to extract gravity long-term, and the LiDAR / RPi camera to track the middle ground.  I perfected reinvention of the wheel!

Hindsight is wonderful, isn’t it!

*My apologies for the complexity of this sentence, it represents the frustration and complexity I encountered working this out over years!

The benefits of floppy props

The standard T-motor Air Gear 200 props are plastic and very flexible.  During takeoff they bend upwards as they spin up until the bend is strong enough to ‘lift’ the quad off the ground – this makes the lift very gentle.

In contrast, the CF props have no flex, any lift is directly and immediately applied to the frame, and therefore to the sensors.  The sensors feel everything; there’s no smoothing or caressing; sharp spikes in motor speed make it to the sensors, to the extent that acceleration may exceed the range of the sensors; I believe this is the cause of my negative G problem.

With the IMU configured to suppport a range of ±2g (the highest resolution), then an additional 1g spike a take-off will overflow.  With the hard light higher pitch CF props, it seems entirely possible this could happen.

There are several ways to fix this each with various pros and cons.  I could

  • increase the low pass filter, but this does still allow the overshoot but then filters it out, along with other valid data – this works though it’s a hack / workaround / sticky plaster, not a fix.
  • extend the range of the sensor to ±4g but there’s a corresponding reduction in sensor resolution which means larger undetectable drift
  • add physical buffering like the props do – I did this previously with the HoG floating on 8 very solt silicone gromits.  I don’t want to use this again (expensive and breakable) but something similar with thicker, softer foam tape sticking HoG to the frame could work
  • soften the flight plan transition in software.

What I’d going to do it a combination of safety precautions and diagnostics

  • I’ll add diagnostics to flag these 0g events, and to skip any data including such an event for safety reasons.
  • I’ll up the range of the sensors to ±4g, and add further diagnotics to flag overshoots over ±2g just to make sure I’m right!

I’ll decide the final solution based on what I find out.


Getting more than I’d bargained for.

Both Phoebe and Zoe now rocket up to the sky at at least 3ms-1 when they should be climbing at 0.3ms-1.

The main relevant change is that both were flying with alpf 0 – 460Hz.  Reducing this to 2 (92Hz) with Phoebe yesterday got the vertical climb rate under some level of control, but the horizontal drift was back.  I’m assuming Zoe will show the improved behaviour at alpf 2 also.

Clearly there’s something in the accelerometer readings that’s integrating (after gravity is removed) to lower velocities than expected.  Then it dawned on me: sampling at 1kHz with the minimal low pass filter set to 0 means that >2g spikes could get picked up; with the range of the sensor set to ±2g, the value could overflow, resulting in the <0g values I’d been seeing.

A very quick test with Phoebe in a howling wind proved me right.  I tried it with Zoe and the result was better but a long way from perfect so more work required there, including fixing her broken arm before tomorrow’s engineering conference.  Oops!

Accelerometer digital low pass filter

Yet more indoor testing, indirectly GERMS related, more aimed at investigating drift against the accelerometer dlpf setting.

Speculatively, the net is that

  • alpf of 3 or less (>= 41Hz) means there is no vertical drift, but there is horizontal drift because real X- and Y-axis noise is not being filtered out and the motion processing thinks there’s X- / Y-axis acceleration when there isn’t.*
  • alpf of 4 or higher (<= 20Hz) gives no horizontal drift, but there is vertical drift because real Z-axis acceleration is being filtered out as noise meaning the motion processing thinks there’s less acceleration than there is.*

Again speculative, I think X- and Y- axis noise was due to unbalanced props leading to asymmetric noise which when integrated leads to non-zero drift velocity: if one of the props has slight damage, this will generate the assymeteric noise very well.

My props do have nicks and muck on them, so I cleaned them up, and tried alpf 3 again, but the drift still remained – to some extent expected as the cleaned props still had nicks in them.  So out came a set of unflown props.  Balanced as best I can, I tried again at alpf 3. Some vertical drift had appeared unexpectedly, so I tried alpf 2, and it was perfect.  So that’s what I’ll be sticking with.

The only down side of this is that the noise getting through at alpf 2 means my germs idea in its basic form is a no go.  It’s shelved for the moment as it’s no longer necessary for my short flights, and instead, I’ll see how far I can extend the complementary filter to see how long a zero drift flight can be achieved.

*To be more precise, it the integration of the acceleration (i.e. the velocites) that are wrong. The Z-axis velocity is greater than zero in reality but zero according to the motion processing due to the acceleration that had been filtered out. In contrast, the X- and Y- velocities are zero according to motion processing, but drifting in reality as the dlpf is letting through asymmetric noise.


Better GERMS stats



Another day, another couple of indoor test flights, experimenting with the IMU accelerometer low pass filter, to see if that would help extract the GERMS info.  Previously, it was set to 2 (92Hz bandwidth); today I tried 4 (20Hz)

The good side is it clearly did – the acceleration and deceleration now stands out clearly at the 1.5, 3.5 and 5s points corresponds to the transitions to ascent, hover, and descent.

Also there was no horizontal drift – during yesterday’s flight, there was drift of half a meter or so.  Clearly yesterday some of the noise on the accelerometer X and Y axes had contributed to the X and Y axis velocities leading to horizontal drift.

The down side is that the lower bandwidth also filtered out some of the Z-axis real acceleration, so during nominal “hover”, Phoebe actually continued to climb slightly- that’s why there’s no spike at 7.5s like yesterday’s graph; Phoebe was still in the air when the flight ended so the post-flight ground impact didn’t register in the stats.

The gyro does offer a dlpf setting for each axis in the MPU-9250; it’s a shame there’s only a single setting of the accelerometer covering all 3 axes.

At least it seems like my GERMS filter idea could work in principle, even if in practise the MPU-9250 can’t be configured to play nicely together with the GERMS filter.

I’ll try 3 (41Hz) accelerometer DLPF tomorrow to see whether this is the perfect comprise.


Diagnostics from indoors flights

I collected these diagnostics from a couple of indoor flights this morning.  The flight is 7.s long:

  • 1.5s to get the props spinning to hover speed on the ground
  • 2s climb at 30cm/s
  • 2s hover
  • 2s descent at -30cm/s.

This was using the complementary filter with tau set to 5s but the GERMS stats were collected to get a better idea what’s going on.

GERMS stats

GERMS stats

This shows two things – there’s a lot of noise in the raw data blue line, and the red trend line cleary shows an oscillation which is hiding the peaks and troughs of net acceleration / deceleration at 1.5, 3.5 and 5.5 seconds.

This set show raw accelerometer values, and the measure of gravity that comes after the raw data has been passed through the butterworth filter.  Three things of interest here:

Acceleration and Gravity

Acceleration and Gravity

  1. The grey line of raw acceleration is still noisy, but does show the peaks and troughs better at 1.5, 3.5 and 5.5s
  2. The green butterworth filter line is doing a lovely job of extracting gravity from the noisy accelerometer data
  3. There’s oscillation in both the X and Y accelerometer readings, best seen in the blue and orange trend lines.

The flights were perfect to look at – only these diagnostics show these subtle problems.

The next steps then are to sort increase the P gain and decrease the I gain to stop the oscillations.  With those gone, it’ll hopefully allow the GERMS stats to show only the deviations from real gravity, and thereby filter it out from the angle calculation as the complementary filter does now.

One point in passing, I have to drop the hover speed PWM value from the long standing 1500ms to 1400ms PWM; The new 4S batteries are clearly showing that 3S is not enough.

Status update

I’ve just managed to squeeze in a couple of test flights before the rain came in, and something is messing up the germs fusion code; noise from the props caught by the accelerometer is my best guess.  I’ll keep tinkering so see whether I can improve this; I’m fairly sure the theory is good, but the reality is swamped by noise.

Just in case the theory is flawed, I’ve uploaded the code to GitHub – just search for germs – it’s commented out so this code is still flyable.  Feedback welcome via comments to this post.

In passing, this update includes a minor fix to the RTF timing, and some tweaks to the diagnotics to keeps the separation between screen and logs clearer.

Finally, I can confirm the 4S cell battery works much better, though I do wonder whether the extra power from the motors has actually increased the noise due to the extra torque making the prop motion more jittery as it steps between motor coils – the motors are able to curtail angular momentum of the props better, pulling them back from overshoot?


Caught a cold

I flew my new experimental fusion filter several times this morning; Phoebe barely got off the ground.  After several flights reconfiguring the GERMS (Gravity Error Root Mean Square) filter to no avail, I just ran the normally successful code, and that too failed to get off the ground in exactly the same way.  So it’s battery power at cold ambient temperature rather than the GERMS filter.  Phew!

So I now have a couple of 4S GensAce 2300mAh batteries on order; the higher voltage (compared to the 3S version) means that even with the higher internal impedance at lower temperatures, there should still be enough power for the motors; at the same time, the low temperature higher impedance should help to warm the cells and so keep the impedance low enough.  It’ll probably be Tuesday before the batteries arrive, and so Wednesday before GERMS testing can resume.  Weather forecast is looking good for the week ahead.  Onwards and upwards, hopefully!

Akin to Kalman

I had another quick go at understanding Kalman filters yesterday, and mostly failed miserably with one exception.  I think what I current have is working in a similar way to a kalman filter:

 # Collect the latest batch of data from the sensors, and sort out units and calibration
 qax, qay, qaz, qrx, qry, qrz, i_time, temp = sampling.collect()
 qax, qay, qaz, qrx, qry, qrz = mpu6050.scaleSensors(qax,

 # Track the number of motion loops and sampling loops; any discrepancy between these are the
 # missed samples or sampling errors.
 motion_loops += 1
 sampling_loops += i_time * dri_frequency

 # Angular predication: Now we know the time since the last batch of samples, update the
 # previous angles with the 'integral' of the previous euler rotation rates.
 urp, urr, ury = Body2EulerRates(qry, qrx, qrz, pa, ra)
 pa += urp * i_time
 ra += urr * i_time
 ya += ury * i_time

 # Based upon the predicted angles, rotate the new accelerometer data to earth frame.
 # Next, run the earth frame acceleration through the Butterworth LPF to extract gravity.
 # Finally, rotate revised gravity back to the quad frame.
 # This provides the best guestimation of current gravity orientation and value.
 eax, eay, eaz = RotateQ2E(qax, qay, qaz, pa, ra, ya)

 egx = bfx.filter(eax)
 egy = bfy.filter(eay)
 egz = bfz.filter(eaz)

 qgx, qgy, qgz = RotateE2Q(egx, egy, egz, pa, ra, ya)

 # To stop integration drift from the gyros, merge the short-term noise free predicted angles
 # with the long-term accurate acclerometer which short term are tainted with acceleration as
 # well as gravity. tau is the period during which we expect acceleration to average out leading
 # only the net gravity value. tau can be small if acceleration is short and sharp. There is
 # a balance here between the period to trust the integrated, reorientated gyro readings and
 # the period where accelerometer spikes average out.
 upa, ura = GetRotationAngles(qax, qay, qaz)
 tau_fraction = tau / (tau + i_time)
 pa = tau_fraction * pa + (1 - tau_fraction) * upa
 ra = tau_fraction * ra + (1 - tau_fraction) * ura

Breaking this down it:

  • reads the sensors
  • predicts the current pitch, roll yaw based upon the previous iteration’s angles and the gyro changes since
  • extracts gravity from the accelerometer readings using these predicted angles to orientate accelerometer readings to the earth frame and pass the acceleration  through the Butterworth IIR filter
  • fuses accelerometer angles with these latest angles.

I’m not happy with the section in red – there’s a choice here neither of which I like much:

  1. the version shown uses a complementary filter with a fixed value of tau and a time increment to fuse accelerometer angles with the best predicted angles: the problem here is that the time increment doesn’t take into account the sharp acceleration peaks and troughs in the flight plan. Instead it relies on the fact that overall these peaks and troughs will even out but in the short term real acceleration seeps into the measure of gravity and drift ensues.
  2. the alternate version does away with the complementary filter and instead uses the qgx, qgy and qgz to get the latest set of angles from the red call the GetRotationAngles: the problem here is that errors accumulate over time as there’s no feed from real accelerometer readings to curtail the integration errors which also will ultimately lead to drift.

Which leads to option 3: tau is tuned dynamically based upon the deviation from a stable value of accelerometer readings rather than incremental time – something along the lines of this:

 upa, ura = GetRotationAngles(qax, qay, qaz)
 tau_fraction = tau / (tau +  gain * ((qax - qgx)2 + (qay - qgy)2 + (qaz - qgz)2)0.5)
 pa = tau_fraction * pa + (1 - tau_fraction) * upa
 ra = tau_fraction * ra + (1 - tau_fraction) * ura

More thinking and testing needed to actually work out the right way to dynamically tweak tau on the fly.

The similarity with a Kalman filter is that it’s 2 stage, prediction and fusing, where the fuse fraction depends on an estimated accuracy of the value to be fused. Hence the raw acceleration value always gets through but how much gets through depends on how dependable the value is thought to be – i.e. it’s deviation from expectations.

For clarity I’ve rewritten the bit of speculative code to now remove tau and instead refer to the fusion_fraction:

 upa, ura = GetRotationAngles(qax, qay, qaz)
 fusion_fraction = 1 / (1 + gain * ((qax - qgx)2 + (qay - qgy)2 + (qaz - qgz)2)0.5)
 pa = (1 - fusion_fraction) * pa + fusion_fraction * upa
 ra = (1 - fusion_fraction) * ra + fusion_fraction * ura

So when there is net acceleration in the system, the RMS error

gain * ((qax - qgx)2 + (qay - qgy)2 + (qaz - qgz)2)0.5

is larger, meaning the fusion fraction is smaller, and a smaller fraction of the raw accelerometer value is used to update the angles. The first step of testing is simply to get an estimate for gain based upon the tau value (0.5 seconds) that’s worked well in the past.