Lab 2: The IMU

Reading and Filtering Accelerometer / Gyroscope Data

1. Setup the IMU

In the second lab, we learn how to collect and process data from an Inertial Measurement Unit (IMU) connected to our Artemis Nano microcontroller, applying filters to pitch and roll information drawn from the IMU's accelerometer and gyroscope to help remove noise.

To begin the lab, I installed the “SparkFun 9DOF IMU Breakout_ICM 20948_Arduino Library” from the Arduino Library Manager and connected the IMU to the Artemis Nano using the QWIIC connector. This step would allow us to test out example code to verify the IMU's functionality. By running Example1_Basics, we can observe a set of information being repeatedly printed out on the Serial Monitor, including accelerometer and gyroscope data.

Artemis IMU Connection
The IMU connected to the Artemis Nano via QWIIC.
Output of Running Example IMU Code
Output of Running Example IMU Code.

AD0_VAL Definition

In the example code, the AD0_VAL definition represents the value of the last bit of the I2C address. On the SparkFun breakout board, the ADR jumper is open by default, which corresponds to a value of 1. If we were to solder this jumper closed, the value would be 0. This feature is significant because it allows us to connect two identical IMUs to the same I2C bus without address conflicts (one set to 0, one set to 1).

Acceleration & Gyroscope Data

By running the example code and observing the raw sensor data, we can very quickly check if the reported information is as expected:

Visual Feedback (LED Blink)

To provide a visual indication that the IMU has initialized successfully and the code is running, I added a startup sequence that blinks the onboard LED three times.

Startup Blink Code
Code added to initial startup for visual feedback.

LED blinking on startup.

2. Accelerometer

The accelerometer measures linear acceleration (including gravity). By analyzing the gravity vector, we can compute the pitch and roll of the board.

1. Pitch and Roll Calculation

Using the equations shown in class, I converted the raw accelerometer data into angles. I implemented a helper function get_pitch_roll containing these equations to calculate this data and then print_pitch_roll in order to quickly check our values:

Pitch & Roll Equations
pitch = atan2(accX, accZ) × 180 / π
roll = atan2(accY, accZ) × 180 / π
get_pitch_roll
Code containing pitch and roll equations.
print_pitch_roll
Code to print out pitch and roll data for easy observation.

2. Two-Point Calibration

To check the accuracy of the accelerometer, I placed the IMU on a flat surface and rotated it to -90, 0, and 90 degrees. As shown in the video below, the sensor output appeared to be quite close to what was expected. While difficult to get a "correct" measurement of 90 or -90 degrees from placing the IMU upright against the table, the most consistent and easily verifiable part of this test was laying the IMU flat against the surface. As shown in the video, pitch and roll when the IMU was flat neared zero degrees, which showed that there was very little baseline skew at the very least within the IMU's readings. This observation led me to conclude that applying two-point calibration would not be necessary.

Verifying Pitch/Roll accuracy at different orientations.

3. Noise Analysis (FFT)

Accelerometers are inherently noisy and susceptible to mechanical vibrations. To analyze this, I recorded data under three conditions: 1) with the IMU stationary to measure baseline noise, 2) with the table shaking to simulate motor vibrations expected in the future, and 3) with the IMU being actively moved and rotated around. This assortment of conditions would provide good variety that would supply us with the different kinds of frequency readings that we would need in order to properly define a cutoff frequency for our low pass filter. Shaking the table for example would help us get a lot more high frequency data points while the rotating the IMU around would provide low frequency data points.

I used the Python script below to perform a Fourier Transform (FFT) on the collected data, which was based on the example provided in the lab instructions. This allowed me to visualize the frequency spectrum and distinguish between the "good" low-frequency data (actual tilting) and the "bad" high-frequency noise (vibrations).

FFT Calculation Code
Python code used to calculate the FFT.
Pitch Time Domain
Pitch data in Time Domain (Raw).
Pitch Frequency Domain
Pitch data in Frequency Domain (FFT).
Roll Time Domain
Roll data in Time Domain (Raw).
Roll Frequency Domain
Roll data in Frequency Domain (FFT).

The FFT graphs show a slight bump in amplitude around 10 Hz, likely corresponding to mechanical noise. To effectively filter this out while keeping the board responsive, I chose a cutoff frequency of 5 Hz.

4. Low Pass Filter (LPF) Design

To implement the filter, I first calculated the sampling rate of my loop. As shown in the screenshot below, the loop runs at approximately 47.67 Hz.

Sample Rate Calculation
Measured sampling rate of ~47 Hz.

Using the sampling rate (Ts ≈ 0.021s) and my desired cutoff frequency (fc = 5Hz), I calculated the smoothing factor α:

Low Pass Filter Calculation
RC = 1 / (2π × fc) = 1 / (2π × 5) 0.0318
α = Ts / (Ts + RC) = 0.021 / (0.021 + 0.0318) 0.4

I implemented this filter in the Arduino code as shown below:

LPF Code Implementation
Code snippet implementing the LPF logic.

The plots below compare the raw accelerometer data (blue/orange) with the filtered output (overlaid). The LPF successfully reduces the amplitude of the high-frequency oscillations (noise) while tracking the general motion of the IMU.

Pitch LPF Result
Pitch: Raw vs Filtered.
Roll LPF Result
Roll: Raw vs Filtered.

3. Gyroscope & Sensor Fusion

While the accelerometer calculates orientation using gravity, the gyroscope measures angular velocity (ω). To get the actual angle (Pitch/Roll/Yaw), we must integrate this velocity over time:

Gyroscope Integration
θnew = θold + (ω × dt)

1. Drift vs. Noise

The plots below compare the angles derived from the Accelerometer (Light Blue / Orange) versus the Gyroscope (Dashed Blue / Dashed Red / Green) under different conditions.

Gyro Stationary
Stationary: Note the gyro drift over time.
Gyro Moving
Moving: Gyro is smooth but offset.

2. Effect of Sampling Frequency

To test the importance of a fast sampling loop, I artificially added a delay (changing delay(20) to delay(60)) and redid the the test with moving the IMU around. As shown in the plots, there is a similar offset that grows after a couple of seconds as seen before, but looking at the y-axis, it becomes immediately clear that the magnitude of the discrepancy has effectively multiplied compared to when the delay was shorter before. This happens because integration relies on the assumption that the angular velocity is constant during the time step dt. When dt becomes too large, we miss the nuances of the motion curve. A sudden spike in velocity might happen entirely between two samples, causing the integration to "underestimate" the total rotation, leading to massive drift.

Gyro with Delay
Gyroscope integration error with slower sampling.

3. Complementary Filter

To get the best of both worlds, I implemented a complementary filter. This fuses the signals by acting as a high-pass filter for the gyroscope (trusting it for fast changes) and a low-pass filter for the accelerometer (trusting it for long-term stability).

Complementary Filter Formula
θcomp = (α × (θcomp + ω × dt)) + ((1 α) × θacc)
Filter Code Part 1
Filter Code Part 2

Implementation of Gyro Integration and Complementary Filter.

I selected an alpha value of 0.8. This means the filter relies 80% on the gyroscope and 20% on the accelerometer at each step. This value provided a good balance: it was high enough to smooth out the accelerometer's vibrations but low enough to correct the gyroscope's drift quickly. As shown in the plots below, the recorded data received is not only smooth but stays consistent over time.

Complementary Filter Stationary
Filter eliminates gyro drift while stationary.
Complementary Filter Moving
Filter tracks motion smoothly without noise.

4. Sample Data & Speed

As shown in the pictures of code below, a new implementation of the data collection was set in place, where sending a command would set high a flag which would indicate in the main loop to start collecting data. This would allow us to continuously receive data values from the IMU where we could then use on the Python side. As instructed in the lab manual, I removed all delay() calls and Serial.print statements from the main loop in order to try to speed up the data collection as much as possible. I used a single array to store the output data as that would help simplify having the synchronize gyro and accelerometer data with time on the Python side of the code while also potentially saving memory from not needing to have multiple arrays. I used a float data type in this case in order to allow for higher precision of data collection and calculation for the angle values we might receive. which would be important when calculating values such as derivatives.

I set the array size to 400 samples. As shown in the picture, we have a collection time of 6.309 seconds (satisfying the >5s requirement), meaning we have a sample rate of approximately 63.4Hz. Given the fact that we store 8 floats (4 bytes each) for every sample, that means one sample of measurement takes 32 bytes. With our memory size of 384kB, this leads us to an absolute maximum of total of around 12,000 measurements that we could theoretically store (though the real number is very likely lower). Using the previously calculated sampling rate, we would be able to capture data for approximately 189.3 , or just over 3 minutes, which is quite good.

Python Proof
Command to start collection.
Python Proof
Helper function called in main loop.
Python Proof
Adjusted main loop.
Python Proof
Python console output confirming >5 seconds of data received.

5. RC Stunt

The video below showcases some of the RC car's capabilities. After playing around for a bit, it was observed that the car's controls were very sensitive, meaning that trying to align the car in a specific direction was quite challenging. The car's rotation speed was fast, allowing it to make very quick turns, and the max speed was also quite high. Most interesting though would be the car's ability to effectively flip on its back and still drive properly, so one of the cooler tricks would be to have the car roll against the wall and flip upside down.

Driving the RC car.