MPU-6050 connected to a Raspberry-Pi through the i2c bus.

Here’s how to use the MPU6050 on Raspberry-Pi or any embedded Linux system. The MPU-6050 & MPU-6000 are interesting components from Invensense. They include an accelerometer, a gyroscope and a temperature sensor. It is possible to connect additional i2c components to the MPU-6050 & MPU-6000, and more importantly, it is possible to configure them to provide a sample rate and read out useful data from a FIFO at regular intervals. This is a good choice for signal processing.

There are libraries for Arduino, a driver in the official Linux sources. I need to understand how it works, so I decided to study it. For that I got its Register map, connected it on the i2c-1 bus of my Raspberry-pi and played with i2cget & i2cset.

MPU6050 on Raspberry-Pi with i2cget/i2cset

Setting up the i2c bus on the Raspberry-pi

Make sure that /dev/i2c-1 exists on your Raspberrry-Pi. If not, check our article on the i2c bus of the Raspberry-Pi.

MPU6050’s default address is 0x68, you can test it responds correctly by asking for its address.

i2cget -y 1 0x68 0x75
0x68 # Yes we know that but thanks !

Reading Acceleration Data on MPU6050

Power supply register PWR_MGMT_1

In order to read the accelerometer registers, the power supply of the MPU-60X0 (register PWR_MGMT_1, 0x6B ) must first be configured. It ensures that the chip is not in sleep mode and that the temperature measurement is activated. It is not recommended to use the internal oscillator, so we will use the gyroscope’s X axis PLL. Thus we write 0x01 in this register:

i2cset -y 1 0x68 0x6B 0x01
MPU-6050 accelerometer ranges
copyright: Invensense (MPU6050 register map)

Accelerometer configuration register ACCEL_CONFIG

Here we want to provide the measuring range of the accelerometer, here are the possible options:

For this demonstration we will use the smallest range of 2g so we write 0 into the register:

i2cset -y 1 0x68 0x1C 0x00

Reading the Z-axis of the accelerometer

Now we will read the Z-axis of the accelerometer, it is perpendicular to the component, it is the easiest to handle. The data can be read on 2 registers 0x3F (high order bits) and 0x40 (low order bits). By holding the component horizontally, component facing upwards to measure gravity acceleration, I make a measurement by reading the 2 bits (with i2cget’s w option) in a single reading :

i2cget -y 1 0x68 0x3F w
0x6845

The display of i2cget inverts the high and low bits, the real result is 0x4568.

In the datasheet, it is indicated that the data are on 16 bits in two’s complement, the result in decimal is 17768. Let 1.08g ( on at 16384 LSB per g), not so bad considering I did not calibrate the sensor.

Sample rate configuration with SMPRT_DIV & CONFIG registers

Data are copied from internal private register to public registers at a certain frequency. There is no external signal that can be used for synchronization. The filtering is also forced to 0:

i2cset -y 1 0x68 0x1A 0x00

Now we can configure the sample rate divider (SMPRT_DIV). I need a sample rate of 1kHz. According to the datasheet this is 8kHz / (1+SMPLRT_DIV). So we will write 7 in SMPLRT_DIV:

i2cset -y 1 0x68 0x19 0x07 # For 1 kHz - 8kHz /(1 + 7)
i2cset -y 1 0x68 0x19 0x4F # For 100Hz - 8kHz /(1 + 79)

Configuration of the internal FIFO with FIFO_EN and USER_CTRL

The MPU-6050 has a 1024 byte internal FIFO, which is useful when reading data with a high sample rate. You must first activate this FIFO (and RESET the data as it passes by):

i2cset -y 1 0x68 0x6A 0x44

We will activate the copy of the accelerometer data into the FIFO:

i2cset -y 1 0x68 0x23 0x08 # To get the data from the accelerometer
i2cset -y 1 0x68 0x23 0x80 # To get the temperature data instead

Now the data from the accelerometer is copied into the fifo. There are 6 bytes for the 3 axes of the accelerometer. As the FIFO is 1024 bytes it gives us about 170 measurements (1024/6) before filling the FIFO. That’s 170 milliseconds with our 1 kHz sample rate.

Reading MPU6050 data FIFO

Several registers can help us to read FIFO data, starting with FIFO_COUNT_L and FIFO_COUNT_H (0x72 and 0x73):

i2cget -y 1 0x68 0x72 w # read two registers at once
0x0004 # equals 0x0400 (i2cget reverses bits), 1024 in decimal, as expected the read fifo is full

Now it is possible to read the content of the FIFO, byte by byte :

i2cget -y 1 0x68 0x74

At this stage, it is obvious that using i2cget is no longer sufficient and that a programming language such as C or C++ must be used. To increase the sampling rate it will also be necessary to increase the speed of the i2c bus or to switch to SPI.

Increase the i2c bus speed of the Raspberry-pi

The maximum operating speed of the MPU6050 i2c bus is 400kHz. By default the Raspberry-pi i2c bus operating frequency is 100kHz, so it is necessary to increase the bus operating speed to improve performance. To achieve this, modify the content of the file “/boot/config.txt” of the Raspberry-Pi and add it (again, check out this article if you don’t know how to do this) :

dtparam=i2c_arm_baudrate=400000

I managed to increase the speed up to 2MHz, it works with this component, but this may not be the case for others. Restart the Raspberry-Pi to apply the change.

Write a program in C to communicate with the mpu6050

There is a driver for the MPU6050 in the official Linux sources. Unfortunately I can’t use it because it is not optimized for my use case. I only need the acceleration data, but I need it with a high frequency. Unlike the driver which reads all the information (gyroscope, temperature, acceleration).

So we’re going to write a C program that does exactly what we did with i2cget/i2cset. The Linux Kernel provides an i2c programming interface for the user space. You can get it simply here on github.

This program reads acceleration and temperature data at a frequency of 1000Hz and requires a significant increase in bus operating speed.

/*
 * This file is an MPU6050 demonstration.
 * https://openest.io/en/2020/01/21/mpu6050-accelerometer-on-raspberry-pi/
 * Copyright (c) 2020 Julien Grossholtz - https://openest.io.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>

#define MPU6050_I2C_ADDR 0x68

#define REG_ACCEL_ZOUT_H 0x3F
#define REG_ACCEL_ZOUT_L 0x40
#define REG_PWR_MGMT_1 0x6B
#define REG_ACCEL_CONFIG 0x1C
#define REG_SMPRT_DIV 0x19
#define REG_CONFIG 0x1A
#define REG_FIFO_EN 0x23
#define REG_USER_CTRL 0x6A
#define REG_FIFO_COUNT_L 0x72
#define REG_FIFO_COUNT_H 0x73
#define REG_FIFO 0x74
#define REG_WHO_AM_I 0x75

int file = -1;

// Please note, this is not the recommanded way to write data
// to i2c devices from user space.
void i2c_write(__u8 reg_address, __u8 val) {
	char buf[2];
	if(file < 0) {
		printf("Error, i2c bus is not available\n");
		exit(1);
	}

	buf[0] = reg_address;
	buf[1] = val;

	if (write(file, buf, 2) != 2) {
		printf("Error, unable to write to i2c device\n");
		exit(1);
	}

}

// Please note, this is not thre recommanded way to read data
// from i2c devices from user space.
char i2c_read(uint8_t reg_address) {
	char buf[1];
	if(file < 0) {
		printf("Error, i2c bus is not available\n");
		exit(1);
	}

	buf[0] = reg_address;

	if (write(file, buf, 1) != 1) {
		printf("Error, unable to write to i2c device\n");
		exit(1);
	}


	if (read(file, buf, 1) != 1) {
		printf("Error, unable to read from i2c device\n");
		exit(1);
	}

	return buf[0];

}

uint16_t merge_bytes( uint8_t LSB, uint8_t MSB) {
	return  (uint16_t) ((( LSB & 0xFF) << 8) | MSB);
}

// 16 bits data on the MPU6050 are in two registers,
// encoded in two complement. So we convert those to int16_t
int16_t two_complement_to_int( uint8_t LSB, uint8_t MSB) {
	int16_t signed_int = 0;
	uint16_t word;

	word = merge_bytes(LSB, MSB);

	if((word & 0x8000) == 0x8000) { // negative number
		signed_int = (int16_t) -(~word);
	} else {
		signed_int = (int16_t) (word & 0x7fff);
	}

	return signed_int;
}

int main(int argc, char *argv[]) {
	int adapter_nr = 1; /* probably dynamically determined */
	char bus_filename[250];
	char accel_x_h,accel_x_l,accel_y_h,accel_y_l,accel_z_h,accel_z_l,temp_h,temp_l;
	uint16_t fifo_len = 0;
	int16_t x_accel = 0;
	int16_t y_accel = 0;
	int16_t z_accel = 0;
	int16_t temp = 0;
	float x_accel_g, y_accel_g, z_accel_g, temp_f;

	snprintf(bus_filename, 250, "/dev/i2c-1", adapter_nr);
	file = open(bus_filename, O_RDWR);
	if (file < 0) {
		/* ERROR HANDLING; you can check errno to see what went wrong */
		exit(1);
	}


	if (ioctl(file, I2C_SLAVE, MPU6050_I2C_ADDR) < 0) {
		/* ERROR HANDLING; you can check errno to see what went wrong */
		exit(1);
	}

	i2c_write(REG_PWR_MGMT_1, 0x01);
	i2c_write(REG_ACCEL_CONFIG, 0x00);
	i2c_write(REG_SMPRT_DIV, 0x07);
	i2c_write(REG_CONFIG, 0x00);
	i2c_write(REG_FIFO_EN, 0x88);
	i2c_write(REG_USER_CTRL, 0x44);

	while(fifo_len != 1024) {
		accel_x_h = i2c_read(REG_FIFO_COUNT_L);
		accel_x_l = i2c_read(REG_FIFO_COUNT_H);
		fifo_len = merge_bytes(accel_x_h,accel_x_l);

		if(fifo_len == 1024) {
			printf("fifo overflow !\n");
			i2c_write(REG_USER_CTRL, 0x44);
			continue;
		}

		if(fifo_len >= 8) {
			accel_x_h = i2c_read(REG_FIFO);
			accel_x_l = i2c_read(REG_FIFO);
			accel_y_h = i2c_read(REG_FIFO);
			accel_y_l = i2c_read(REG_FIFO);
			accel_z_h = i2c_read(REG_FIFO);
			accel_z_l = i2c_read(REG_FIFO);
			temp_h = i2c_read(REG_FIFO);
			temp_l= i2c_read(REG_FIFO);

			x_accel= two_complement_to_int(accel_x_h,accel_x_l);
			x_accel_g = ((float) x_accel)/16384;

			y_accel= two_complement_to_int(accel_y_h,accel_y_l);
			y_accel_g = ((float) y_accel)/16384;

			z_accel= two_complement_to_int(accel_z_h,accel_z_l);
			z_accel_g = ((float) z_accel)/16384;

			temp = two_complement_to_int(temp_h, temp_l);
			temp_f = (float)temp/340 + 36.53; // calculated as described in the MPU60%) register map document

			printf("x_accel %.3fg	y_accel %.3fg	z_accel %.3fg	temp=%.1fc         \r", x_accel_g, y_accel_g, z_accel_g, temp_f);
		} else {
			usleep(10000);
		}

	}

	return 0;
}