Doing the math

A project log for Light Aircraft Fuel Timer

Device to assist with monitoring fuel in a light aircraft's multiple tanks.

Stephen Holdaway 03/05/2019 at 09:200 Comments

From the original list of features I sketched out a year or two ago, I primarily want address the following:

• As a pilot I want to know how much fuel is onboard, expressed as time.

• As a pilot I want to be alerted when I am approaching my reserve fuel time.

• As a pilot I want to know when the active fuel tank needs to be changed so I can keep the tanks balanced.

• As a pilot I want to know when my search and rescue deadline (SARTIME) is approaching so I can update or terminate it.

• As a pilot I want this device to keep state accurately across loss of power or reset so I do not lose information.

Common to all of these are two calculations:

• Difference between two date/times
• Time remaining given a quantity and rate of consumption

These calculations have to be reliable, but not necessarily fast. As long as the system stays responsive on a human scale (eg. a few hundred milliseconds when a button is pushed). This makes the main constraints program size and memory.

Half of the ATMega328p's 2KB of SRAM is currently taken up by a screen buffer, but that could be reduced at the cost of draw time if needed.

Diffing date/times on an AVR

Time is fairly straight forward to store and read back. The real-time clock source for this project is a Maxim DS3231, which provides a date and time in binary coded decimal (BCD) format. This can be simply be stored as a series of 8-bit integers to avoid dealing with BCD values outside of the RTC read/write routines:

```typedef struct RtcTime {
uint8_t hour;
uint8_t minute;
uint8_t second;
uint8_t day;
uint8_t month;
uint8_t year;
} RtcTime;```

It's easy to display this information, but as soon as you want to do any calculations the firmware needs to know all about how calendars work: days in months, months in year, and how leap years work. Reducing the problem to diffing times within the same day isn't suitable here as the clock needs to be set to UTC to compare against a configured SAR time. Where I live is 12-13 hours ahead of UTC, so it's very common for a flight to cross the boundary of a UTC day.

AVR libc provides a version of time.h with some modifications to make it more suitable for AVR microcontrollers, like using 32-bit integers instead of double-precision floats, but it still uses a fairly large chunk of program space just to diff two times. Calling only `mktime` and `difftime` increases the program size by around 1,500 bytes: 4.5% of the total program space on the 328p! Each `tm` struct also wastes 6 bytes of SRAM with fields that won't be used here.

This project doesn't need completely general date/time functions (currently), so a more specialised time diff implementation can save a bit of space:

• The DS3231 uses a two-digit year. Limiting the year to 2000-2099 simplifies leap year checks to bitwise operations.

• Timezones and daylight saving offsets can be ignored as we're always comparing two UTC date/times.

• The difference between times can be returned in minutes instead of seconds. A range of -22 to +22 days can be represented as minutes using a 16-bit value, which is more than enough here.

An implementation with these assumptions uses only 334 bytes of program space. With another small function, the seconds columns of two `RtcTime` structs could be diffed to supplement the delta of minutes with a remainder of seconds.

How much fuel is left?

Fuel is a different beast to keep track of. The burn rate in flight (litres per hour) and capacity of each tank (litres) both fit comfortably into the 0-255 range for light aircraft, but to find the quantity remaining after an elapsed time requires much greater resolution than this. Enter fixed point math.

The resolution a fixed point number can represent is simply 1 over the scaling factor, so finding an appropriate scaling factor is fairly easy. For reference, a burn rate of 38L/h is exactly 10mL per second, or 600mL per minute.

 Scaling factor Required type Resolution (approx) 28 Unsigned 16-bit 3.9mL 216 Unsigned 24-bit 0.15mL 224 Unsigned 32-bit 0.00006mL

Scaling by 216 would be the minimum suitable resolution in this case to avoid accumulating too much error, but as 24-bit values aren't typically used (though they're technically supported by GCC), it's easier to use a 32-bit value and go with a 224 scaling factor. The result is a "UQ8.24" fixed-point value - enough resolution to only be millilitres out after days of operation.

Once we have a fixed point representation for the fuel quantity in each tank, we'll need a strategy to keep track of how much fuel is onboard. Let's explore a few:

Reduce the selected tank's quantity by the per-second burn rate for every second that passes.

I was considering using this for a while, and it would technically work, but it has some problems:

• - Accumulates rounding error when splitting consumption between tanks (how much depends on chosen fixed-point precision and how evenly the burn rate divides by that)

• - Requires a "catch-up" function to account for the time elapsed since the last persisted state after loss of power or reset.

• - Potentially requires persisting state frequently to help reduce the catch-up required.

Store the start and stop time of each state of flight. Calculate remaining fuel as total fuel minus the burn in each recorded period.

This solves the issues from the previous idea:

• + Little accumulation of rounding error
• + Only need to persist data when state changes
• + No need for a "catch-up" routine as the calculation always reflects the given time.

However, it introduces one major issue:

• - Requires an unbounded amount of persistent storage

How about a small modification to keep storage bounded:

Store the start time of the current state of flight. When the state of flight changes, calculate the fuel used during the last state and subtract it from the current tank

• + Little accumulation of rounding error
• + Only need to persist data when state changes
• + No need for a "catch-up" routine
• + Requires a fixed amount of storage

This is the approach I'm planning to implement.