Gyro Crosshair
Gyro Crosshair
What I wanted to learn, and why it interested me: I wanted to learn how to get values from a gyroscope and how to use it in projects. It interested me because when I did robotics in highschool, we had to use a gyro to balance on a seesaw, and I never felt like we got to use the gyro to its full potential.
Final outcome: The final build is a gyroscope attached to a display that roughly displays the roll and pitch of the gyro on the breadboard using a crosshair pattern that moves around the display.
Images of final creative/exploratory output
Final Product
Final image of creative/exploratory output
Process images from development towards creative/exploratory output
Testing the display with buttons to get it to work
Finally recieving values from the gyro in terms of roll/pitch/yaw
Process and reflection:
The process started with me deciding between a gyro and and accelerometer for the project, and I went with the gyro after realizing the accelerometer doesn't do as much as the gyro does. After soldering the gyroscope pins to the gyroscope, I attached it to a breadboard, looked up code for the specific model and started getting values. The first and only real major pitfall was that overtime, the roll, pitch and yaw would increase slowly, and while I never had the time to fix this for the yaw, the video seen above contains the corrected roll and pitch values. From here it was mapping those values to a crosshair with x being the roll and y being the pitch of the crosshair on the display.
I learned a good amount during this project, a little bit of physics with the rotational orientation logic, and some new programming strategies in terms of using acceleration to accurately determine the rotational axes. I wish I had a little more time to implement something related to the yaw, or even add more displays but other than that, I'm happy with the project.
Technical details
[Electrical schematic. Add a brief caption for clarity; this could be as simple as "Electrical schematic" but you can also use the space to describe anything that would be useful for your reader, e.g. "The 12V power came from a LiPo battery, not drawn."]
/*
60-223 Intro to Physical Computing, fall 2025
Domain-specific Skill Building exercise: Gyro Crosshair
This sketch reads the values from the MPU6050 Accelerometer and Gyroscope
and displays the roll and pitch onto a 8x8 display in the form of a crosshair
Pin mapping:
Arduino pin | role | details
------------------------------
SDA input accelerometer
SCL input accelerometer
SDA output gyro
SCL output gyro
Released to the public domain by the author, October 2025
Helios Gayibor, hgayibor@andrew.cmu.edu
*/
/*
Arduino and MPU6050 Accelerometer and Gyroscope Sensor Tutorial
by Dejan, https://howtomechatronics.com
https://howtomechatronics.com/tutorials/arduino/arduino-and-mpu6050-accelerometer-and-gyroscope-tutorial/
*/
#include <Wire.h>
#include <Adafruit_LEDBackpack.h>
#include "Adafruit_GFX.h"
Adafruit_8x8matrix matrix = Adafruit_8x8matrix();
const int MPU = 0x68; // MPU6050 I2C address
float AccX, AccY, AccZ;
float GyroX, GyroY, GyroZ;
float accAngleX, accAngleY, gyroAngleX, gyroAngleY, gyroAngleZ;
float roll, pitch, yaw;
float AccErrorX, AccErrorY, GyroErrorX, GyroErrorY, GyroErrorZ;
float elapsedTime, currentTime, previousTime;
int c = 0;
void setup() {
Serial.begin(19200);
Wire.begin(); // Initialize comunication
Wire.beginTransmission(MPU); // Start communication with MPU6050 // MPU=0x68
Wire.write(0x6B); // Talk to the register 6B
Wire.write(0x00); // Make reset - place a 0 into the 6B register
Wire.endTransmission(true); //end the transmission
matrix.begin(0x70); //initalizing matrix
/*
// Configure Accelerometer Sensitivity - Full Scale Range (default +/- 2g)
Wire.beginTransmission(MPU);
Wire.write(0x1C); //Talk to the ACCEL_CONFIG register (1C hex)
Wire.write(0x10); //Set the register bits as 00010000 (+/- 8g full scale range)
Wire.endTransmission(true);
// Configure Gyro Sensitivity - Full Scale Range (default +/- 250deg/s)
Wire.beginTransmission(MPU);
Wire.write(0x1B); // Talk to the GYRO_CONFIG register (1B hex)
Wire.write(0x10); // Set the register bits as 00010000 (1000deg/s full scale)
Wire.endTransmission(true);
delay(20);
*/
// Call this function if you need to get the IMU error values for your module
calculate_IMU_error();
delay(20);
}
void loop() {
matrix_setup();
// === Read acceleromter data === //
Wire.beginTransmission(MPU);
Wire.write(0x3B); // Start with register 0x3B (ACCEL_XOUT_H)
Wire.endTransmission(false);
Wire.requestFrom(MPU, 6, true); // Read 6 registers total, each axis value is stored in 2 registers
//For a range of +-2g, we need to divide the raw values by 16384, according to the datasheet
AccX = (Wire.read() << 8 | Wire.read()) / 16384.0; // X-axis value
AccY = (Wire.read() << 8 | Wire.read()) / 16384.0; // Y-axis value
AccZ = (Wire.read() << 8 | Wire.read()) / 16384.0; // Z-axis value
// Calculating Roll and Pitch from the accelerometer data
accAngleX = (atan(AccY / sqrt(pow(AccX, 2) + pow(AccZ, 2))) * 180 / PI) - 0.58; // AccErrorX ~(0.58) See the calculate_IMU_error()custom function for more details
accAngleY = (atan(-1 * AccX / sqrt(pow(AccY, 2) + pow(AccZ, 2))) * 180 / PI) + 1.58; // AccErrorY ~(-1.58)
// === Read gyroscope data === //
previousTime = currentTime; // Previous time is stored before the actual time read
currentTime = millis(); // Current time actual time read
elapsedTime = (currentTime - previousTime) / 1000; // Divide by 1000 to get seconds
Wire.beginTransmission(MPU);
Wire.write(0x43); // Gyro data first register address 0x43
Wire.endTransmission(false);
Wire.requestFrom(MPU, 6, true); // Read 4 registers total, each axis value is stored in 2 registers
GyroX = (Wire.read() << 8 | Wire.read()) / 131.0; // For a 250deg/s range we have to divide first the raw value by 131.0, according to the datasheet
GyroY = (Wire.read() << 8 | Wire.read()) / 131.0;
GyroZ = (Wire.read() << 8 | Wire.read()) / 131.0;
// Correct the outputs with the calculated error values
GyroX = GyroX + 0.56; // GyroErrorX ~(-0.56)
GyroY = GyroY - 2; // GyroErrorY ~(2)
GyroZ = GyroZ + 0.79; // GyroErrorZ ~ (-0.8)
// Currently the raw values are in degrees per seconds, deg/s, so we need to multiply by sendonds (s) to get the angle in degrees
gyroAngleX = gyroAngleX + GyroX * elapsedTime; // deg/s * s = deg
gyroAngleY = gyroAngleY + GyroY * elapsedTime;
yaw = yaw + GyroZ * elapsedTime;
// Complementary filter - combine acceleromter and gyro angle values
gyroAngleX = 0.96 * gyroAngleX + 0.04 * accAngleX;
gyroAngleY = 0.96 * gyroAngleY + 0.04 * accAngleY;
roll = gyroAngleX + 2.14;
pitch = gyroAngleY - 1.6;
// Print the values on the serial monitor
Serial.print(roll);
Serial.print("/");
Serial.print(pitch);
Serial.print("/");
Serial.println(yaw);
draw_crosshair(roll, pitch, yaw);
matrix.writeDisplay();
}
/*I don't use this function but it could be used to calculate error in the unit*/
void calculate_IMU_error() {
// We can call this funtion in the setup section to calculate the accelerometer and gyro data error. From here we will get the error values used in the above equations printed on the Serial Monitor.
// Note that we should place the IMU flat in order to get the proper values, so that we then can the correct values
// Read accelerometer values 200 times
while (c < 200) {
Wire.beginTransmission(MPU);
Wire.write(0x3B);
Wire.endTransmission(false);
Wire.requestFrom(MPU, 6, true);
AccX = (Wire.read() << 8 | Wire.read()) / 16384.0 ;
AccY = (Wire.read() << 8 | Wire.read()) / 16384.0 ;
AccZ = (Wire.read() << 8 | Wire.read()) / 16384.0 ;
// Sum all readings
AccErrorX = AccErrorX + ((atan((AccY) / sqrt(pow((AccX), 2) + pow((AccZ), 2))) * 180 / PI));
AccErrorY = AccErrorY + ((atan(-1 * (AccX) / sqrt(pow((AccY), 2) + pow((AccZ), 2))) * 180 / PI));
c++;
}
//Divide the sum by 200 to get the error value
AccErrorX = AccErrorX / 200;
AccErrorY = AccErrorY / 200;
c = 0;
// Read gyro values 200 times
while (c < 200) {
Wire.beginTransmission(MPU);
Wire.write(0x43);
Wire.endTransmission(false);
Wire.requestFrom(MPU, 6, true);
GyroX = Wire.read() << 8 | Wire.read();
GyroY = Wire.read() << 8 | Wire.read();
GyroZ = Wire.read() << 8 | Wire.read();
// Sum all readings
GyroErrorX = GyroErrorX + (GyroX / 131.0);
GyroErrorY = GyroErrorY + (GyroY / 131.0);
GyroErrorZ = GyroErrorZ + (GyroZ / 131.0);
c++;
}
//Divide the sum by 200 to get the error value
GyroErrorX = GyroErrorX / 200;
GyroErrorY = GyroErrorY / 200;
GyroErrorZ = GyroErrorZ / 200;
// Print the error values on the Serial Monitor
Serial.print("AccErrorX: ");
Serial.println(AccErrorX);
Serial.print("AccErrorY: ");
Serial.println(AccErrorY);
Serial.print("GyroErrorX: ");
Serial.println(GyroErrorX);
Serial.print("GyroErrorY: ");
Serial.println(GyroErrorY);
Serial.print("GyroErrorZ: ");
Serial.println(GyroErrorZ);
}
/*This draws the crosshair onto the display*/
void draw_crosshair(double roll, double pitch, double yaw) {
int ratio = 8;
double display_roll = -roll / ratio;
double display_pitch = pitch / ratio;
double display_yaw = yaw / ratio;
matrix.drawLine(3 + display_roll, 1 + display_pitch, 3 + display_roll, 5 + display_pitch, LED_ON); //vert
matrix.drawLine(1 + display_roll, 3 + display_pitch, 5 + display_roll, 3 + display_pitch, LED_ON); //horz
}
/*Sets up the display*/
void matrix_setup() {
matrix.setTextSize(1);
matrix.setTextColor(LED_ON);
matrix.clear();
matrix.setCursor(0,0);
}