Accelerometer, Gyroscope, and Magnetometer Analysis with Raspberry Pi Part I: Basic Readings

mpu9250_basics_main_rpi_zoom.JPG

“As an Amazon Associates Program member, clicking on links may result in Maker Portal receiving a small commission that helps support future projects.”

The MPU9250 is a powerful inertial measurement unit (IMU) consisting of three primary sensors: an accelerometer, a gyroscope, and a magnetometer. Each sensor measures a 3-axis signal in the cartesian reference x,y,z. Each of the 9-degrees of freedom is converted into a 16-bit digital signal, which can be read at different speeds depending on the sensor. The accelerometer is capable of measuring ±2, ±4, ±8, and ±16g at a sample rate of 1 kHz, the gyroscope can measure ±250°/s, ±500°/s, ±1000°/s, and ±2000°/s (dps or degrees per second) at a sample rate of 8 kHz, and the magnetometer can measure ±4800μT at 100 Hz.

A Raspberry Pi will be used to read the MPU9250 3-axis acceleration, 3-axis angular rotation speed, and 3-axis magnetic flux (MPU9250 product page can be found here). The output and limitations of the MPU9250 will be explored, which will help define the limitations of applications for each sensor. This is only the first entry into the MPU9250 IMU series, where in the breadth of the articles we will apply advanced techniques in Python to analyze each of the 9-axes of the IMU and develop real-world applications for the sensor, which may be useful to engineers interested in vibration analysis, navigation, vehicle control, and many other areas.


Parts List and Wiring

This tutorial uses two primary components: An MPU9250 9-DoF IMU and a Raspberry Pi computer. Any Rapsberry Pi will do as long as it has I2C communication and is capable of running Python 3.x. I have listed the parts and where I purchased them below, along with some other components that may make following along with the tutorial more seamless:

  1. Raspberry Pi 4 Computer - $38.40 - $62.00 [1GB on Amazon, 2GB Amazon, 4GB Amazon], $55.00 [2GB from Our Store]

  2. MPU9250 IMU - $13.00 [Our Store]

  3. Mini Breadboard - $3.00 [Our Store]

  4. Jumper Wires - $0.90 (6 pcs) [Our Store]

  5. Raspberry Pi 4 Kit (HDMI cables, USB cables, etc.) - $97.99 [Amazon]

The wiring digram for the MPU9250 and Raspberry Pi is given below (and in the neighboring table). In actuality, I will be using the Rapsberry Pi 4 (despite the diagram stating it is a RPi 3), however the pinout and protocols are all the same. Take note of the second I2C (EDA/ECL) wiring - this is important because it wires both the MPU6050 (accelerometer/gyroscope) and AK8963 (magnetometer) to the RPi’s I2C port. This also means that we will be communicating with two I2C devices (more on this later).

RPI Pin MPU9250 Pin
3.3V (1) VCC
GND (6) GND
SDA (3) SDA
SCL (5) SCL
ECL → SCL
EDA → SDA

Raspberry Pi I2C Port

The MPU9250 will communicate with the Raspberry Pi using the I2C protocol. In order to read and write data via I2C, we must first enable the I2C ports on the RPi. The way we do this is either using the command line or by navigating to the Preferences → Raspberry Pi Configuration. Adafruit has a great tutorial outlining this process, but an abridged version will be given below using screenshots of the RPi configuration window.

i2c_pref_window.png

1. Preferences → Raspberry Pi Configuration

i2c_enable.png

2. Interfaces → Enable I2C

i2c_list_devices_boxes.png

3. Open Command Window and type “sudo i2cdetect -y 1”

Verify 0x68 (MPU6050) and 0x0C (AK8963) as I2C devices

Once both MPU9250 devices show up on the I2C detector in the RPi command window, we are ready to read the MPU6050 (0x68 device address) and AK8963 (0x0C device address). The reason why we need both addresses is that we have wired them to the same I2C port, so we now use their addresses to control them in a program. In this tutorial, Python will be used. The device addresses can be found in their respective datasheets or by testing them individually by wiring them one-by-one. We know that the MPU6050 (accel/gyro) is 0x68 from its datasheet, and we know the AK8963 (magnetometer) is 0x0C from its datasheet, so the wiring test is not necessary.

If you only see one device address, recheck the wiring; and if no devices are showing up also check the wiring and ensure there is power to the MPU9250 and also that the I2C has been enabled on the RPi! Going forward, it is assumed that the MPU9250 has been wired to the RPi and that the device addresses are identical to the ones given above. Of course, we can easily change the device addresses in the Python code, so if your device for some reason has different addresses, the user will need to change that in the codes that follow.

Lastly, we will need to increase the speed of the I2C baud rate in order to get the fastest response from the MPU9250, which we can do by entering the following into the command line:

i2c_boot_config_txt.png

1. sudo nano /boot/config.txt

i2c_baud_rate_txt.png

2. Add Line in Next to I2C Setting

All we are doing here is setting the baud rate to 1 Mbps. This should give us a sample rate of about 400Hz - 500Hz (after conversion to real-world units). We can achieve a much higher sample rate for the gyroscope and a slightly higher sample rate for the accelerometer, but that will not be explored in this series.

mpu9250_basics_main_rpi.JPG

Interfacing the MPU9250 with Python

In Python, the I2C port can be accessed using a particular library called ‘smbus.’ The smbus is intialized using the following simple routine:

import smbus
bus = smbus.SMBus(1)

Using the ‘bus’ specified in the code above, we will use the MPU9250 Register Map document to gives us information about how to communicate with the accelerometer, gyroscope, and magnetometer (MPU6050 and AK8963). The I2C bus methods used in Python are outside of the scope of this tutorial, so they will not be described in great detail; therefore, the code used to communicate back and forth to the MPU6050 and AK8963 are given below without much description. The full details and capabilities of each sensor are given in the datasheet to the MPU9250, where many questions regarding the registers and corresponding values can be explored, if desired.

# this is to be saved in the local folder under the name "mpu9250_i2c.py"
# it will be used as the I2C controller and function harbor for the project 
# refer to datasheet and register map for full explanation

import smbus,time

def MPU6050_start():
    # alter sample rate (stability)
    samp_rate_div = 0 # sample rate = 8 kHz/(1+samp_rate_div)
    bus.write_byte_data(MPU6050_ADDR, SMPLRT_DIV, samp_rate_div)
    time.sleep(0.1)
    # reset all sensors
    bus.write_byte_data(MPU6050_ADDR,PWR_MGMT_1,0x00)
    time.sleep(0.1)
    # power management and crystal settings
    bus.write_byte_data(MPU6050_ADDR, PWR_MGMT_1, 0x01)
    time.sleep(0.1)
    #Write to Configuration register
    bus.write_byte_data(MPU6050_ADDR, CONFIG, 0)
    time.sleep(0.1)
    #Write to Gyro configuration register
    gyro_config_sel = [0b00000,0b010000,0b10000,0b11000] # byte registers
    gyro_config_vals = [250.0,500.0,1000.0,2000.0] # degrees/sec
    gyro_indx = 0
    bus.write_byte_data(MPU6050_ADDR, GYRO_CONFIG, int(gyro_config_sel[gyro_indx]))
    time.sleep(0.1)
    #Write to Accel configuration register
    accel_config_sel = [0b00000,0b01000,0b10000,0b11000] # byte registers
    accel_config_vals = [2.0,4.0,8.0,16.0] # g (g = 9.81 m/s^2)
    accel_indx = 0                            
    bus.write_byte_data(MPU6050_ADDR, ACCEL_CONFIG, int(accel_config_sel[accel_indx]))
    time.sleep(0.1)
    # interrupt register (related to overflow of data [FIFO])
    bus.write_byte_data(MPU6050_ADDR, INT_ENABLE, 1)
    time.sleep(0.1)
    return gyro_config_vals[gyro_indx],accel_config_vals[accel_indx]
    
def read_raw_bits(register):
    # read accel and gyro values
    high = bus.read_byte_data(MPU6050_ADDR, register)
    low = bus.read_byte_data(MPU6050_ADDR, register+1)

    # combine higha and low for unsigned bit value
    value = ((high << 8) | low)
    
    # convert to +- value
    if(value > 32768):
        value -= 65536
    return value

def mpu6050_conv():
    # raw acceleration bits
    acc_x = read_raw_bits(ACCEL_XOUT_H)
    acc_y = read_raw_bits(ACCEL_YOUT_H)
    acc_z = read_raw_bits(ACCEL_ZOUT_H)

    # raw temp bits
##    t_val = read_raw_bits(TEMP_OUT_H) # uncomment to read temp
    
    # raw gyroscope bits
    gyro_x = read_raw_bits(GYRO_XOUT_H)
    gyro_y = read_raw_bits(GYRO_YOUT_H)
    gyro_z = read_raw_bits(GYRO_ZOUT_H)

    #convert to acceleration in g and gyro dps
    a_x = (acc_x/(2.0**15.0))*accel_sens
    a_y = (acc_y/(2.0**15.0))*accel_sens
    a_z = (acc_z/(2.0**15.0))*accel_sens

    w_x = (gyro_x/(2.0**15.0))*gyro_sens
    w_y = (gyro_y/(2.0**15.0))*gyro_sens
    w_z = (gyro_z/(2.0**15.0))*gyro_sens

##    temp = ((t_val)/333.87)+21.0 # uncomment and add below in return
    return a_x,a_y,a_z,w_x,w_y,w_z

def AK8963_start():
    bus.write_byte_data(AK8963_ADDR,AK8963_CNTL,0x00)
    time.sleep(0.1)
    AK8963_bit_res = 0b0001 # 0b0001 = 16-bit
    AK8963_samp_rate = 0b0110 # 0b0010 = 8 Hz, 0b0110 = 100 Hz
    AK8963_mode = (AK8963_bit_res <<4)+AK8963_samp_rate # bit conversion
    bus.write_byte_data(AK8963_ADDR,AK8963_CNTL,AK8963_mode)
    time.sleep(0.1)
    
def AK8963_reader(register):
    # read magnetometer values
    low = bus.read_byte_data(AK8963_ADDR, register-1)
    high = bus.read_byte_data(AK8963_ADDR, register)
    # combine higha and low for unsigned bit value
    value = ((high << 8) | low)
    # convert to +- value
    if(value > 32768):
        value -= 65536
    return value

def AK8963_conv():
    # raw magnetometer bits

    loop_count = 0
    while 1:
        mag_x = AK8963_reader(HXH)
        mag_y = AK8963_reader(HYH)
        mag_z = AK8963_reader(HZH)

        # the next line is needed for AK8963
        if bin(bus.read_byte_data(AK8963_ADDR,AK8963_ST2))=='0b10000':
            break
        loop_count+=1
        
    #convert to acceleration in g and gyro dps
    m_x = (mag_x/(2.0**15.0))*mag_sens
    m_y = (mag_y/(2.0**15.0))*mag_sens
    m_z = (mag_z/(2.0**15.0))*mag_sens

    return m_x,m_y,m_z
    
# MPU6050 Registers
MPU6050_ADDR = 0x68
PWR_MGMT_1   = 0x6B
SMPLRT_DIV   = 0x19
CONFIG       = 0x1A
GYRO_CONFIG  = 0x1B
ACCEL_CONFIG = 0x1C
INT_ENABLE   = 0x38
ACCEL_XOUT_H = 0x3B
ACCEL_YOUT_H = 0x3D
ACCEL_ZOUT_H = 0x3F
TEMP_OUT_H   = 0x41
GYRO_XOUT_H  = 0x43
GYRO_YOUT_H  = 0x45
GYRO_ZOUT_H  = 0x47
#AK8963 registers
AK8963_ADDR   = 0x0C
AK8963_ST1    = 0x02
HXH          = 0x04
HYH          = 0x06
HZH          = 0x08
AK8963_ST2   = 0x09
AK8963_CNTL  = 0x0A
mag_sens = 4900.0 # magnetometer sensitivity: 4800 uT

# start I2C driver
bus = smbus.SMBus(1) # start comm with i2c bus
gyro_sens,accel_sens = MPU6050_start() # instantiate gyro/accel
AK8963_start() # instantiate magnetometer

The code block given above handles the startup for each I2C sensor (MPU6050 and AK8963) and also the conversion from bits to real-world values (gravitation, degrees per second, and Teslas). The code block should be saved in the local folder under the name ‘mpu6050_i2c.py’ - this library will be imported in the example below. All we do is call the conversion script for each sensor and we have the outputs from each of the nine variables. Simple!

The example usage code is given below, along with the sample readouts printed to the Python console:

# MPU6050 9-DoF Example Printout

from mpu9250_i2c import *

time.sleep(1) # delay necessary to allow mpu9250 to settle

print('recording data')
while 1:
    try:
        ax,ay,az,wx,wy,wz = mpu6050_conv() # read and convert mpu6050 data
        mx,my,mz = AK8963_conv() # read and convert AK8963 magnetometer data
    except:
        continue
    
    print('{}'.format('-'*30))
    print('accel [g]: x = {0:2.2f}, y = {1:2.2f}, z {2:2.2f}= '.format(ax,ay,az))
    print('gyro [dps]:  x = {0:2.2f}, y = {1:2.2f}, z = {2:2.2f}'.format(wx,wy,wz))
    print('mag [uT]:   x = {0:2.2f}, y = {1:2.2f}, z = {2:2.2f}'.format(mx,my,mz))
    print('{}'.format('-'*30))
    time.sleep(1)
mpu9250_printout_example.png

MPU9250 9-DoF Example Printout

The printout above can be used to verify that the sensor and code are working correctly. The following should be noted:

  • In the z-direction we have a value near 1, this means that gravity is acting in the vertical direction and positive is downward

  • The gyro is reading values close to 0, and in this case we haven’t moved the device so they should be close to 0

  • The magnetometer is showing values between -10μT-40μT in the x,y directions, which is roughly the approximation of the earth’s magnetic field strength in New York City (where the measurements were taken).

With these values verified, we can state that the MPU9250 sensors are working and we can begin our investigations and some simple calculations!


Visualizing Acceleration, Angular Velocity, and Magnetic Field Strength

Now that we can verify that each sensor is returning meaningful values, we can go on to investigate the sensor in practice. The raw printouts shown in the previous section can be plotted as a function of time:

The code to replicate the plot above is given below:

# MPU9250 Simple Visualization Code
# In order for this to run, the mpu9250_i2c file needs to 
# be in the local folder

from mpu9250_i2c import *
import smbus,time,datetime
import numpy as np
import matplotlib.pyplot as plt

plt.style.use('ggplot') # matplotlib visual style setting

time.sleep(1) # wait for mpu9250 sensor to settle

ii = 1000 # number of points
t1 = time.time() # for calculating sample rate

# prepping for visualization
mpu6050_str = ['accel-x','accel-y','accel-z','gyro-x','gyro-y','gyro-z']
AK8963_str = ['mag-x','mag-y','mag-z']
mpu6050_vec,AK8963_vec,t_vec = [],[],[]

print('recording data')
for ii in range(0,ii):
    
    try:
        ax,ay,az,wx,wy,wz = mpu6050_conv() # read and convert mpu6050 data
        mx,my,mz = AK8963_conv() # read and convert AK8963 magnetometer data
    except:
        continue
    t_vec.append(time.time()) # capture timestamp
    AK8963_vec.append([mx,my,mz])
    mpu6050_vec.append([ax,ay,az,wx,wy,wz])

print('sample rate accel: {} Hz'.format(ii/(time.time()-t1))) # print the sample rate
t_vec = np.subtract(t_vec,t_vec[0])

# plot the resulting data in 3-subplots, with each data axis
fig,axs = plt.subplots(3,1,figsize=(12,7),sharex=True)
cmap = plt.cm.Set1

ax = axs[0] # plot accelerometer data
for zz in range(0,np.shape(mpu6050_vec)[1]-3):
    data_vec = [ii[zz] for ii in mpu6050_vec]
    ax.plot(t_vec,data_vec,label=mpu6050_str[zz],color=cmap(zz))
ax.legend(bbox_to_anchor=(1.12,0.9))
ax.set_ylabel('Acceleration [g]',fontsize=12)

ax2 = axs[1] # plot gyroscope data
for zz in range(3,np.shape(mpu6050_vec)[1]):
    data_vec = [ii[zz] for ii in mpu6050_vec]
    ax2.plot(t_vec,data_vec,label=mpu6050_str[zz],color=cmap(zz))
ax2.legend(bbox_to_anchor=(1.12,0.9))
ax2.set_ylabel('Angular Vel. [dps]',fontsize=12)

ax3 = axs[2] # plot magnetometer data
for zz in range(0,np.shape(AK8963_vec)[1]):
    data_vec = [ii[zz] for ii in AK8963_vec]
    ax3.plot(t_vec,data_vec,label=AK8963_str[zz],color=cmap(zz+6))
ax3.legend(bbox_to_anchor=(1.12,0.9))
ax3.set_ylabel('Magn. Field [μT]',fontsize=12)
ax3.set_xlabel('Time [s]',fontsize=14)

fig.align_ylabels(axs)
plt.show()

We can also start to explore the sensors by rotating the device. For example, if we flip the sensor on its side such that the x-direction is pointed upwards, we can visualize how each of the 9 degrees of freedom responds:

mpu9250_rotation_about_x.png

A 3D, real-time GIF visualization is shown below, where each axis can be tracked. It’s a great visualization tool to understand just how each axis behaves under specific rotations:

mpu9250_cube_rotation_compressed.gif

A few observations can be made on the plot behavior above:

  1. The rotation about the y-axis results in the gravitational acceleration in the x-direction

  2. The rotation shows a negative angular velocity in the y-direction

  3. The magnetic field shifts from the x and y directions to the y and z-directions

In the upcoming tutorials, I will explore the accelerometer, gyroscope, and magnetometer individually, as well as the fusion of all three to create meaningful relationships to real world engineering.


Conclusion and Continuation

This tutorial introduced the MPU9250 accelerometer, gyroscope, and magnetometer and how to use the Raspberry Pi computer to communicate with the device. Using the I2C protocol, we were able to read 9 different variables, one for each of the three cartesian axes for each of the three sensors. Then, each of the 9 variables was visualized and plotted for a given movement. In the specific example used above, I demonstrated the behavior of each sensor under a given axis rotation, where we learned how each sensor responded. The magnitudes of each sensor are important and provide information about real-world applications, and in the next few tutorials, the accelerometer, gyroscope, and magnetometer will individually explored to great lengths in order to provide a full working sensor fusion system that is able to reproduce physical movements and translations in 3-dimensional space.

mpu9250_solo.JPG
Citation for This Page:
 

See More in Raspberry Pi and Sensors: