Lab 2

Objective

Lab 2 introduces the IMU. Through the tasks, data is collected via the IMU's accelerometer and gyroscope. Various filtering techniques are used to get clean, accurate signals. In this lab, we also received the physical robot and performed stunts with it.

Setup

AD0_VAL

According to lecture 4 code, AD0_VAL is the value of the last bit on the I2C address. It is currently 1 because the AD0 pin doesn't have anything soldered to it.

IMU example code

I ran the IMU’s example code to observe accelerometer and gyroscope data. The accelerometer measures in "mg" (milli-gravity), with one axis always reading ±1000 mg due to gravity, depending on orientation. The gyroscope, in "DPS" (degrees per second), is unaffected by gravity.

Blink LED

To help me indicate whether or not my board is running, I implemented a blink sequence in my setup function.

blink LED

Accelerometer

After validating that the IMU was set up correctly, I collected accelerometer data to compute pitch and roll angles (recall that the accelerometer cannot detect yaw).

Accelerometer roll
Accelerometer pitch

Two-point calibration

I noticed that the data is smoother if I do not touch the board. For the two videos above, I was applying a decent amount of force to the board to keep it in position. Thus, the data was quite noisy. The numbers are close enough to what I expect them to be, so I thought a two-point calibration was not necessary.

Data collection

In Python, I implemented a notification handler that would receive the accelerometer data (timestamps, pitch, roll) as a string and store it into separate lists as floats. Then, I used matplotlib to plot the data. In Arduino, I implemented a command, “GET_ACCEL”, that would collect accelerometer data for a specified duration. The command has two loops: the first computes pitch and roll angles from accelerometer data and adds them to separate arrays. The second send the data via BLE to the computer.

Notification handler in Python
Command used to get accelerometer data

For this task, I rotated the board about its x-axis so I expected pitch to be ~0. I chose to collect 3 seconds worth of data; I verified this by computing the time elapsed from the timestamps list that was sent over.

Fourier transform and low-pass filter

The raw accelerometer data is noisy, so a low-pass filter helps. A high cutoff keeps more noise, while a low cutoff over-smooths and may lose details. I needed to find the optimal cutoff frequency.

Raw accelerometer data

To determine the cutoff, I analyzed the signal’s frequency spectrum by performing a Fourier transform on my data. The raw data shows ~1Hz oscillations, confirmed by a frequency spectrum spike. Weak signals appear up to 3-4Hz, so I include these frequencies. Using α=T/(T+RC) and f=1/(2πRC), I find α≈0.07.

Frequency spectrum for this dataset
Periodogram generated using scipy.signal.periodogram()

I added the LPF code to my GET_ACCEL command. The updated code and filtered signal are shown:

Data collection loop in GET_ACCEL
Filtered signal with original

I also tested how effective the LPF would be when handling vibrational noise. I tapped the table twice while the IMU was sitting flat. The raw data from this experiment shows two main spikes. As seen in the image, the spikes and noise subside substantially after applying the LPF.


Gyroscope

Next, I collected data from the gyroscope to compute pitch, roll, and yaw angles. The data reflects the sensor being rotated about its x-axis, so I don't expect pitch or yaw to change much.

Comparison to accelerometer

There are clear differences between the gyroscope and accelerometer data. Unlike the accelerometer, the gyroscope is able to detect yaw by sensing acceleration about the z-axis. The gyroscope data is significantly less noisy than the accelerometer data, but drift is very apparent, especially in pitch data. The gyroscope’s roll signal also seems to undershoot its actual amplitude by a significant margin.



Complementary filter

I implemented the complementary filter as follows: angle_c = (weight)*angle_accel + (1-weight)*angle_gyro. I collected data for 4.5 seconds to validate that my complementary filter can mitigate drift. To stay consistent with previous trials, I rotated the board about its x-axis again, so I am expecting pitch to be ~0.


Weight = 0.3
Weight = 0.5
Weight = 0.8

I also tested to see if the complementary filter could mitigate quick vibrations by tapping on the table while the board laid flat.

Weight = 0.3
Weight = 0.5
Weight = 0.8

From the plots generated, a weight of 0.5 is a good choice.


Sampling data

I stored gyroscope and accelerometer data in separate arrays. When sending data, I loop through the arrays, combine each entry with a timestamp into a string, and write to the string characteristic. Keeping them separate simplifies looping, data combination, and debugging. For previous tasks, I computed pitch and roll angles in the same loop but stored them separately for the same reasons.

Data collection loop
Data arrays used in the lab

I needed a space-efficient data type since BLE limits me to 150 bytes per transfer. Strings were too large, and unsigned longs couldn’t store negative values. Between floats and doubles, floats take less space, so I chose floats.


I implemented a STREAM_DATA command to collect raw IMU data. I was able to collect 5 seconds of data at ~300Hz. For previous tasks, I observed a similar sampling rate.

Computed sampling time and rate

I used float arrays of 1600 (6.4kB each). At 300Hz sampling, this stores 5.28s of data. With 384kB RAM, the Artemis can hold up to 316.8s, though some RAM is needed for variables.

Stunts

Observations

The car indeed moves very fast. It didn't seem like there was any speed control, so it was very easy to crash the car into obstacles.

Discussion and acknowledgements

Discussion

Lab 2 provided valuable experience in collecting and processing IMU data while also offering insight into the Artemis board's data processing capabilities for future labs.

acknowledgements

My friends Rachel Arena and Kelvin Resch helped me catch a memory overflow error early on in the lab and kindly lent me a charged battery to use. I also referenced Daria Kot's page from Spring 2024 to get an idea of how large my arrays should be.