Two More Cents

A Lesson in Debugging - Using Double-Precision Floats in MicroPython

Skip to the solution if you aren’t interested in the story.

As part of a team project that I worked on this semester, I helped design a device that collects and processes GPS coordinates. We combined the GPS readings with an Intertial Measurement Unit, which provided data on velocity and acceleration, and allowed us to increase the precision of our readings. As we waited for the next GPS reading, we would use the IMU’s data to update the position.

This is an illustration of how the device worked:

The workhorse of our system was a Raspberry Pi Pico running, as you probably guessed, MicroPython (technically, it was running CircuitPython, which is a variant of MicroPython, but that’s irrelevant for the purposes of this article).

The Problem

Early in the semester, we ran into a bug where our IMU values were being discarded - The IMU would calculate the distance that the device had moved, but when we tried to add this value to the coordinates we received from the GPS (after performing the appropriate unit conversions), the coordinate would remain unchanged.

A simple example:

  1. GPS recognizes you are at 30° N, 30°W.
  2. IMU reads your velocity and calculates you have moved a meter to the north.
  3. Rather than updating your position to 30.00...1°N, your position remains at 30°N.

We spent a while debugging this issue. To gather more information, we added additional data to our log file, and conducted more tests. The bug persisted. Eventually, a teammate came up with the idea that perhaps the value obtained by the IMU is simply too small - the program considers it to be close enough to zero, that it disregards it.

This piqued my curiosity, and I soon found that MicroPython uses single-precision floating point values by default. Since all our values were stored as float, this meant that precision was only guaranteed up to ~6 digits after the decimal point. Therefore, it wasn’t necessarily MicroPython’s fault, it was an inherent limitation of the data type we were using.

The solution was to use double-precision floating point values, which offer much greater precision (at least enough for our use case). Unfortunately, MicroPython doesn’t support double-precision out-of-the-box.

The Solution

After some online research, I found a way to enable double-precision floating point values in MicroPython (credit to this post on the Raspberry Pi forums by user hippy). Here are the steps:

  1. Download the MicroPython source code: https://github.com/micropython/micropython.

  2. Under ports/{your_port}, you should find a file called mpconfigport.h. Replace your_port with the specific device you are using.

  3. In mpconfigport.h, edit the line that says:

    #define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_FLOAT)

    Replace IMPL_FLOAT with IMPL_DOUBLE so that it now reads:

    #define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_DOUBLE)

  4. You now need to recompile MicroPython, using CMake (install CMake and make if you already haven’t):

     mkdir build
     cd build
     cmake ..
     make
    • At this point, you will likely get an error about setting an environment variable for the compiler. This is because you are compiling MicroPython for the embedded device, so your regular gcc will not work.
    • Since I was compiling for an ARM target, I had to download the ARM GNU Toolchain. Choose the relevant download for your Operating System.
    • After downloading and unzipping the file, invoke the cmake command again, setting the environment variable to the unzipped directory.
  5. After compiling, you should see the resulting image files in your build directory. You can use these to reflash your device.

  6. You should now be able to do double-precision floating point arithmetic. Any use of float should automatically use double-precision. I have only tested this on a Raspberry Pi Pico, since that’s the device I was using. You may encounter errors with other devices.

Lesson learnt

This was probably my single most valuable contribution to the team this semester - We were able to drastically improve the precision of our data and achieve better results. I have two graphs from our tests below: the first is a graph of different positions with single-precision float values, and the second is with double-precision (obviously, these are from different tests, where the device moved in different directions).

Pretty neat, huh?

This experience taught me that errors often hide in the most unexpected places. In my case, as I mentioned before, our code had done no wrong - it was an issue with the data type. I learned that it’s important to check all possible sources of error. Do not assume a program will behave a certain way, because you think it should work that way (somewhat related: Kernighan’s Law).

I think there’s also a lesson somewhere in there, about not trusting floating point values.