Tuesday, 2 December 2014

Using Inverse Kinematics to Develop a Biped Robot Walking Gait (C#)

When I created the eight degree of freedom (8 DOF) biped robot in my last blog post, I wrote a C# application to calculate servo positions, which in turn generated a smooth, life-like, walking gait. In this post I will walk through the application logic in more detail. The application generates a motion plan, runs the inverse kinematics calculations and allows me to visualise the results as rendered a stick man. The complete source code is below.

According to Wikipedia, “Inverse kinematics refers to the use of the kinematics equations of a robot to determine the joint parameters that provide a desired position of the end-effector. Specification of the movement of a robot so that its end-effector achieves a desired task is known as motion planning. Inverse kinematics transforms the motion plan into joint actuator trajectories for the robot.”

Starting with the motion plan and using the two rules established in my previous post (#1 static hip height and #2 clipped sinusoidal foot motion), I knew roughly what I wanted each joint to do. Next I had to model that mathematically. Hip height was easy, as it’s constant. Left and right feet follow the pattern illustrated below.

Biped Robot Hip & Foot Height Against Time

I created a system of triangles to represent each of the robot joints and could now begin to calculate the relative angles between them for each time interval. To preserve symmetry, I decided that the feet would always remain parallel with the body (and floor) and that the horizontal plane would always bisect the knee angle. These principles helped determine many of the joint angles using some simple trigonometry.



Biped Robot Limb Angles
All that remained was to solve any outstanding angles using the law of cosines. The law of cosines can be used in a number of ways - such as calculating the third side of a triangle when two sides and their enclosed angle are known, or to determine the angles of a triangle if all three sides are known. Once the angles are known, these can be translated into servo positions, with the appropriate amount of offset and direction applied.

This could now be implemented in code – the motion plan (determining foot geometry), the inverse kinematics (determining servo angle) calculation and the joint visualisation. I won’t walk through every step as the source code is pretty easy to decipher.

Note about the code: I separate the code into a few classes to keep key objects and values partitioned (legs, hip, etc). In this post I’ve compressed everything into a single file, which will execute - just paste the entire block into a new Windows Form project. However, if you want to modify the code, for maintainability, it would be best to break out again into discrete class files.

I hope you find this useful.

Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.IO;
  5. using System.Threading;
  6. using System.Windows.Forms;
  7.  
  8. namespace Biped
  9. {
  10.     public class Canvass : Form
  11.     {
  12.         private List<int[]> _patterns = new List<int[]>();
  13.         private Hip _hip = new Hip();
  14.         private int _p = 0;
  15.  
  16.         [STAThread]
  17.         static void Main()
  18.         {
  19.             Application.Run(new Canvass());
  20.         }
  21.  
  22.         public Canvass()
  23.         {
  24.             this.Paint += Render;
  25.             this.Height = 300;
  26.             this.Width = 250;
  27.             CalcFeetCoordinates();
  28.         }
  29.  
  30.         private void CalcFeetCoordinates()
  31.         {
  32.             // Move left leg forward.
  33.             for (int i = 0; i < 20; i++)
  34.             {
  35.                 double x = (i - 10) * (Leg.StrideLength / 20.0);
  36.                 double y = Leg.HipHeight;
  37.                 AddStridePosition(x, y, -45);
  38.             }
  39.  
  40.             // Move left leg backward.
  41.             for (int i = 0; i < 20; i++)
  42.             {
  43.                 double x = (10 - i) * (Leg.StrideLength / 20.0);
  44.                 double y = FootHeight(x);
  45.                 AddStridePosition(x, y, 45);
  46.             }
  47.  
  48.             // Build right leg from phase shift clone of left.
  49.             for (int i = 0; i < 40; i++)
  50.                 for (int j = 0; j < 4; j++)
  51.                     _patterns[i][j + 4] = -_patterns[(i + 20) % 40][j];
  52.  
  53.             // Roll ankles on transition.
  54.             RollAnkle(19, 20, 6);
  55.             RollAnkle(45, 0, 6);
  56.  
  57.             // Write servo positions to file.
  58.             DumpToFile();
  59.         }
  60.  
  61.         private double FootHeight(double x)
  62.         {
  63.             return Leg.HipHeight - Leg.FootLift * Math.Cos(Math.Abs(x * Math.PI / Leg.StrideLength));
  64.         }
  65.  
  66.         private void AddStridePosition(double x, double y, int tilt)
  67.         {
  68.             // Cosine rule: cos A = (b^2 + c^2 - a^2) / 2bc
  69.             int[] pos = new int[8];
  70.             double hypSqrd = Math.Pow(x, 2) + Math.Pow(y, 2);
  71.             double hyp = Math.Sqrt(hypSqrd);
  72.             pos[0] = 0 - RadToStep(Math.Acos(hyp / (2 * Leg.Bone)) - Math.Atan2(x, y));
  73.             pos[1] = RadToStep(Math.Acos((2 * Leg.BoneSqrd - hypSqrd) / (2 * Leg.BoneSqrd))) - 512;
  74.             pos[2] = pos[0] - pos[1];
  75.             pos[3] = tilt;
  76.             _patterns.Add(pos);
  77.         }
  78.  
  79.         private void RollAnkle(int r1, int r2, int steps)
  80.         {
  81.             int[] row1 = _patterns[r1];
  82.             int[] row2 = _patterns[r2];
  83.             for (int i = 0; i < steps; i++)
  84.             {
  85.                 int[] pos = new int[8];
  86.                 for (int j = 0; j < 8; j++)
  87.                     pos[j] = row1[j] - ((row1[j] - row2[j]) * (i + 1)) / (steps + 1);
  88.                 _patterns.Insert(r1 + 1 + i, pos);
  89.             }
  90.         }
  91.  
  92.         private void Render(object sender, PaintEventArgs e)
  93.         {
  94.             _hip.Render(_patterns[_p++], e.Graphics);
  95.             if (_p == _patterns.Count) _p = 0;
  96.             this.Invalidate();
  97.             Thread.Sleep(100);
  98.         }
  99.  
  100.         private int RadToStep(double rads)
  101.         {
  102.             return (int)(rads * 512 / Math.PI);
  103.         }
  104.  
  105.         private void DumpToFile()
  106.         {
  107.             using (TextWriter tw = new StreamWriter("biped.csv", false))
  108.             {
  109.                 foreach (int[] pos in _patterns)
  110.                     tw.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}",
  111.                         pos[0], pos[1], pos[2], pos[3], pos[4], pos[5], pos[6], pos[7]);
  112.                 tw.Close();
  113.             }
  114.         }
  115.     }
  116.  
  117.     public class Hip
  118.     {
  119.         private Leg _leftLeg = new Leg();
  120.         private Leg _rightLeg = new Leg();
  121.  
  122.         public void Render(int[] steps, Graphics graph)
  123.         {
  124.             _leftLeg.SetServos(steps[0], steps[1], steps[2], -1);
  125.             _leftLeg.Render(graph, Pens.Black);
  126.  
  127.             _rightLeg.SetServos(steps[4], steps[5], steps[6], 1);
  128.             _rightLeg.Render(graph, Pens.Blue);
  129.         }
  130.     }
  131.  
  132.     public class Leg
  133.     {
  134.         public static int Bone = 100;
  135.         public static int BoneSqrd = Bone * Bone;
  136.         public static int HipHeight = 180;
  137.         public static int StrideLength = 60;
  138.         public static int FootLift = 20;
  139.  
  140.         private static int _foot = Bone / 5;
  141.         private double[] _joints = new double[3];
  142.  
  143.         public void SetServos(int hip, int knee, int ankle, int direction)
  144.         {
  145.             _joints[0] = StepToRad(hip * direction);
  146.             _joints[1] = StepToRad(-knee * direction);
  147.             _joints[2] = StepToRad(-ankle * direction + 256);
  148.         }
  149.  
  150.         public void Render(Graphics g, Pen pen)
  151.         {
  152.             Point[] points = new Point[4];
  153.             points[0] = new Point(100, 40);
  154.  
  155.             points[1] = new Point();
  156.             points[1].X = points[0].X + (int)(Math.Sin(_joints[0]) * Bone);
  157.             points[1].Y = points[0].Y + (int)(Math.Cos(_joints[0]) * Bone);
  158.  
  159.             points[2] = new Point();
  160.             points[2].X = points[1].X + (int)(Math.Sin(_joints[0] + _joints[1]) * Bone);
  161.             points[2].Y = points[1].Y + (int)(Math.Cos(_joints[0] + _joints[1]) * Bone);
  162.  
  163.             points[3] = new Point();
  164.             points[3].X = points[2].X + (int)(Math.Sin(_joints[0] + _joints[1] + _joints[2]) * _foot);
  165.             points[3].Y = points[2].Y + (int)(Math.Cos(_joints[0] + _joints[1] + _joints[2]) * _foot);
  166.  
  167.             for (int i = 0; i < 3; i++)
  168.                 g.DrawLine(pen, points[i], points[i + 1]);
  169.         }
  170.  
  171.         private double StepToRad(int steps)
  172.         {
  173.             return Math.PI * steps / 512.0;
  174.         }
  175.     }
  176. }

Sunday, 30 November 2014

8 DOF Biped Robot using Dynamixel AX-12A Servos and Arduino

Buoyed by the success of my 6 DOF biped I decide to take the next step (no pun intended).

I purchased another Dynamixel AX-12A servo for each leg to give me eight degrees of freedom (DOF) in total. The hope was that this would result in a much more life like walking gait. Whilst ordering the servos, I also bought some more Robotis plastic frames to ease bolting this lot together.

The new servos and frames were fixed together similar to the previous design, but now with an enhanced ankle joint. With 8 DOF, I could no longer work out joint ankles in my head. It was time to break out some inverse kinematics!

Designing a walking gait from scratch is not that simple. I started by watching how people walk and tried to establish some simple rules I could emulate in code. My first observation was that humans have a really efficient walking gait. Our bodies carry a large mass above the waistline and we tend to keep that fairly stable whilst walking.

Rule #1: The robot’s hip height should remain constant.

Secondly, we raise and lower our feet very smoothly, just enough to achieve forward movement, which peaks in the middle of our stride.

Rule #2: The robot’s feet should follow clipped sine wave.

With these two rules established I could now generate a system of triangles to calculate all servo positions at each point of the stride. I could solve any missing angles using the law of cosines. To help me with this job I wrote a C# application to crunch the numbers and visualise the task. The result was a [52, 8] matrix of servo positions that I could paste into a very small Arduino program.

I will walk through the C# application in detail in my next post.

The resulting Arduino code is posted below. Like before, the program refers to my Dynamixel class created in a previous post.

I’m really pleased with the results. Here is a video of my new 8 DOF biped walking across a glass table - it looks and sounds pretty sinister…

video


Code Snippet
  1. #include "Dynamixel.h"
  2. #include "Wire.h"
  3.  
  4. #define WALK_SWITCH  8
  5.  
  6. Dynamixel servo;
  7.  
  8. int pos[52][8] = {
  9. {-95, -138, 43, -45, 41, 138, -97, -45},
  10. {-93, -140, 47, -45, 50, 151, -101, -45},
  11. {-92, -141, 49, -45, 59, 164, -105, -45},
  12. {-90, -143, 53, -45, 67, 174, -107, -45},
  13. {-88, -144, 56, -45, 74, 184, -110, -45},
  14. {-85, -145, 60, -45, 80, 192, -112, -45},
  15. {-83, -146, 63, -45, 87, 198, -111, -45},
  16. {-81, -147, 66, -45, 92, 204, -112, -45},
  17. {-78, -147, 69, -45, 97, 207, -110, -45},
  18. {-76, -147, 71, -45, 101, 210, -109, -45},
  19. {-73, -148, 75, -45, 104, 210, -106, -45},
  20. {-70, -147, 77, -45, 107, 210, -103, -45},
  21. {-67, -147, 80, -45, 109, 207, -98, -45},
  22. {-64, -147, 83, -45, 110, 204, -94, -45},
  23. {-61, -146, 85, -45, 110, 198, -88, -45},
  24. {-58, -145, 87, -45, 110, 192, -82, -45},
  25. {-55, -144, 89, -45, 109, 184, -75, -45},
  26. {-52, -143, 91, -45, 106, 174, -68, -45},
  27. {-48, -141, 93, -45, 103, 164, -61, -45},
  28. {-45, -140, 95, -45, 100, 151, -51, -45},
  29. {-45, -140, 95, -33, 100, 150, -50, -33},
  30. {-44, -140, 95, -20, 99, 148, -49, -20},
  31. {-44, -140, 95, -7, 98, 146, -48, -7},
  32. {-43, -139, 96, 6, 98, 144, -47, 6},
  33. {-43, -139, 96, 19, 97, 142, -46, 19},
  34. {-42, -139, 96, 32, 96, 140, -45, 32},
  35. {-41, -138, 97, 45, 95, 138, -43, 45},
  36. {-50, -151, 101, 45, 93, 140, -47, 45},
  37. {-59, -164, 105, 45, 92, 141, -49, 45},
  38. {-67, -174, 107, 45, 90, 143, -53, 45},
  39. {-74, -184, 110, 45, 88, 144, -56, 45},
  40. {-80, -192, 112, 45, 85, 145, -60, 45},
  41. {-87, -198, 111, 45, 83, 146, -63, 45},
  42. {-92, -204, 112, 45, 81, 147, -66, 45},
  43. {-97, -207, 110, 45, 78, 147, -69, 45},
  44. {-101, -210, 109, 45, 76, 147, -71, 45},
  45. {-104, -210, 106, 45, 73, 148, -75, 45},
  46. {-107, -210, 103, 45, 70, 147, -77, 45},
  47. {-109, -207, 98, 45, 67, 147, -80, 45},
  48. {-110, -204, 94, 45, 64, 147, -83, 45},
  49. {-110, -198, 88, 45, 61, 146, -85, 45},
  50. {-110, -192, 82, 45, 58, 145, -87, 45},
  51. {-109, -184, 75, 45, 55, 144, -89, 45},
  52. {-106, -174, 68, 45, 52, 143, -91, 45},
  53. {-103, -164, 61, 45, 48, 141, -93, 45},
  54. {-100, -151, 51, 45, 45, 140, -95, 45},
  55. {-100, -150, 50, 33, 45, 140, -95, 33},
  56. {-99, -148, 49, 20, 44, 140, -95, 20},
  57. {-98, -146, 48, 7, 44, 140, -95, 7},
  58. {-98, -144, 47, -6, 43, 139, -96, -6},
  59. {-97, -142, 46, -19, 43, 139, -96, -19},
  60. {-96, -140, 45, -32, 42, 139, -96, -32}
  61. };
  62.  
  63. int centre = 512;
  64. byte p = 0;
  65.  
  66. void setup() {
  67.   pinMode(WALK_SWITCH, INPUT);
  68.   Serial.begin(1000000);
  69. }
  70.  
  71. void loop() {
  72.   if (digitalRead(WALK_SWITCH)) {
  73.     update(p++);
  74.     delay(38);
  75.     if (p > 51) p = 0;
  76.   }
  77. }
  78.  
  79. void update(byte p) {
  80.   int fix = 0;
  81.   for (byte i = 0; i < 8; i++)
  82.   {
  83.     if (i==0) fix = -120;
  84.     else if (i==4) fix = 120;
  85.     else fix = 0;
  86.     servo.setPos(i + 1, centre + pos[p][i] + fix, 0);
  87.   }
  88. }

Tuesday, 1 April 2014

6 DOF Biped Robot using Dynamixel AX-12A Servos and Arduino

Having mastered driving Robotis Dynamixel AX Servos with an Arduino, I wanted to do something practical with that knowledge. How about building a biped robot?

There are plenty of biped robot kits available, like the Lynxmotion BRAT and the Robotis Bioloid, but I wanted to build something from the parts I already had lying around.

Each of the six Dynamixel AX-12A Servos I recently purchased came supplied with a U shaped bracket and mounting plate. I decided that if I bolted these together I’d get a pretty decent pair of legs. This configuration would give me a total of six degrees of freedom (DOF) - three in each leg - hip and knee joints on the sagittal plane and an ankle joint on the coronal plane.

I cut and drilled a strip of aluminium to form a pelvis and tie the legs together. I had a sheet of 3mm HDPE (the same stuff plastic chopping boards are made from) lying around, so cut this into rectangles to form the feet.

For power, I reused a 5000mAH LiPo battery, which I cable tied to the aluminium pelvis. This placed the centre of gravity nice and high, which is actually useful. On top of that, I taped an Arduino mounted in a plastic case and finally added the CDS55xx Driver Board (to interface the Arduino to the servos).

It's not the prettiest robot, but that completed the mechanical build.


Next came the software part, which turned out to be pretty simple. The key to getting a biped to walk well is developing an efficient walking gait. There are two different ways to approach this:

Static gait - the centre of gravity is projected inside the polygon formed by the robot’s feet. This is the simplest form, although looks artificial.

Dynamic gait - the centre of gravity is not necessarily projected within the polygon of the robot’s feet, however, dynamic balance is maintained. This is far more complex, but results in more natural movement.

For my first experiment, I chose a static gait. As I only had to contend with 6 DOF I decided I could dispense with complex inverse kinematics calculations and do it by hand. And, by keeping the motion of each leg symmetrical it’s easier to keep the centre of gravity central.

I settled on a repeating pattern of four poses that would make up the gait:

- Rotate ankles clockwise, shifting centre gravity to the left and lifting right leg.
- Extend right leg forward and push backwards with left leg.
- Rotate ankles anticlockwise, shifting centre gravity to the right and lifting left leg.
- Extend left leg forward and push backwards with right leg.

Here is a video of the completed biped walking happily across my floor.

video

Here is the code. Please note, I reference the Dynamixel class as featured in my last blog post.

Biped.ino
#include "Dynamixel.h"
#include "Wire.h"
 
Dynamixel servo;
int velocity = 90;
int centre = 511;
byte c = 0;
byte p = 0;
 
/* Offset array from centre position
 * L_HIP, L_KNEE, L_ANKLE, R_HIP, R_KNEE, R_ANKLE */
int pos[5][6] = {
  {   0,   0,   0,   0,   0,   0 }, // Stand upright
  { -55, -55,  25, -55, -55,  25 }, // Right leg forward
  { -55, -55, -25, -55, -55, -25 }, // Lean right
  {  55,  55, -25,  55,  55, -25 }, // Left leg forward
  {  55,  55,  25,  55,  55,  25 }  // Lean left
};
 
void setup() {
  Serial.begin(1000000);
  delay(5000);
}
 
void loop() {
  if (c < 4)
  {
    update(p++);
    if (p > 4)
    { 
      c++;
      p = 1;
    }
  }
  else update(0);
}
 
void update(byte p)
{
  // Update each servo position.
  for (byte i = 0; i < 6; i++)
    servo.setPos(i + 1, centre + pos[p][i], velocity);
 
  // Wait for motion to complete.
  delay(500);
}
Thanks for reading.
John

Sunday, 23 March 2014

Driving Robotis Dynamixel AX-12A Servos from an Arduino

In my continuing quest for knowledge about robotics I recently bought some Robotis Dynamixel AX-12A servos, with the intention of hooking up to an Arduino. These awesome little servos pack a real punch, with over 15 kg/cm torque. There are plenty of hobby servos that have similar torque, but what sets these apart is that they also have the ability to track and report their speed, temperature, shaft position, voltage, and load. This level of feedback is essential for building advanced robotics applications.

Robotis Dynamixal AX-12A
Unlike hobby servos, these servos operate using a serial half-duplex TTL RS232 protocol. This is actually good news, as your micro-controller doesn’t need to worry about generating individual PWM signals for each servo. Instead, all sensor management and position control is handled by the servo's built-in micro-controller. Position and speed can be controlled with a 1024 step resolution. Wiring is pretty simple with two connectors on each servo allowing a daisy chain to be constructed.

The main controller communicates with the Dynamixel servos by sending and receiving data packets. There are two types of packets; the Instruction Packet (sent from the main controller to the servos) and the Status Packet (sent from the servos to the main controller). By default the communication speed is 1Mbps. A factory fresh servo has a preset ID of 01, which can easily be updated if you intend to run more than one servo from the same micro-controller.

The next challenge was to hook these up to an Arduino. This is not quite as simple as a regular hobby servo, as you must convert the full-duplex signal coming from the Arduino RX/TX pins to the half-duplex required by the Dynamixel servos. Luckily, there is a simple piece of hardware that will do the job for you. I purchased the CDS55xx Driver Board from Robosavvy. This board integrates a half-duplex and a voltage regulator circuit and thus makes it possible to directly connect Dynamixel AX servos to an Arduino.


Next I needed some code to drive these units. If you’ve read my previous post, you’ll know I don’t like bloated software libraries, so instead I created a bare bones class that allows me to set the servo ID and read & write servo positions – enough for current needs. The code is below – hope you find it useful…

Dynamixel.h
#include "Arduino.h"
 
// Registers
#define P_ID 3                   // ID {RD/WR}
#define P_GOAL_POSITION_L 30     // Goal Position L {RD/WR}
#define P_PRESENT_POSITION_L 36  // Present Position L {RD}
 
// Instructions
#define INST_READ 0x02           // Reading values in the Control Table.
#define INST_WRITE 0x03          // Writing values to the Control Table.
 
class Dynamixel{
public:
  Dynamixel();
  void setPos(byte id, int pos, int vel);
  void setID(byte id, byte newId);
  int getPos(byte id);
 
private:
  void WriteHeader(byte id, byte length, byte type);
};

Dynamixel.cpp
#include "Dynamixel.h"
#include "Wire.h"  
 
Dynamixel::Dynamixel() {}
 
/* id = servo ID [0 - 255]
** pos = new position [0 - 1023]
** vel = servo velocity [0 - 1023] */
void Dynamixel::setPos(byte id, int pos, int vel)
{
  int writeLength = 7;
  byte pos_h = pos / 255;
  byte pos_l = pos % 255; 
  byte vel_h = vel / 255;
  byte vel_l = vel % 255;
 
  // Write standard header.
  WriteHeader(id, writeLength, INST_WRITE);
  // Starting address of where the data is to be written.
  Serial.write(P_GOAL_POSITION_L);
  // Write position low byte.
  Serial.write(pos_l);
  // Write position high byte.
  Serial.write(pos_h);
  // Write velocity low byte.
  Serial.write(vel_l);
  // Write velocity high byte.
  Serial.write(vel_h);
  // Check Sum.  
  Serial.write((~(id + writeLength + INST_WRITE + P_GOAL_POSITION_L + pos_l + pos_h + vel_l + vel_h))&0xFF);
  // Wait for instruction to be processed.
  delay(2);
  // Discard return data.
  while (Serial.read() >= 0){}
}
 
/* id = servo ID [0 - 255] */
int Dynamixel::getPos(byte id)
{
  int writeLength = 4;
  int readLength = 2;
 
  // Write standard header.
  WriteHeader(id, writeLength, INST_READ);
  // Starting address of where the data is to be read.
  Serial.write(P_PRESENT_POSITION_L);
  // The length of data to read.
  Serial.write(readLength);
  // Check Sum.  
  Serial.write((~(id + writeLength + INST_READ + P_PRESENT_POSITION_L + readLength))&0xFF);
  // Wait for instruction to be processed.
  delay(2);
  // Discard extra data.
  for (int i = 0; i < 5; i++) Serial.read();
  // Read low byte.
  int low_Byte = Serial.read();
  // Read low byte.
  int high_byte = Serial.read();
  // Discard returned checksum.
  Serial.read();
  // Return position.
  return (int)high_byte << 8 | low_Byte;
}
 
/* id = servo ID [0 - 255]
** newId = new servo ID [0 - 255] */
void Dynamixel::setID(byte id, byte newId)
{
  int writeLength = 4;
 
  // Write standard header.
  WriteHeader(id, writeLength, INST_WRITE);
  // Starting address of where the data is to be written.
  Serial.write(P_ID);
  // New ID.
  Serial.write(newId);
  // Check Sum.
  Serial.write((~(id + writeLength + INST_WRITE + P_ID + newId))&0xFF);
}
 
void Dynamixel::WriteHeader(byte id, byte length, byte type)
{
  Serial.write(0xFF);
  Serial.write(0xFF);
  Serial.write(id);
  Serial.write(length);
  Serial.write(type);
}

Thursday, 6 March 2014

Using an MPU-6050 Gyroscope & Accelerometer with an Arduino

I recently purchased a SparkFun (InvenSense) MPU-6050, six degrees of freedom Gyroscope & Accelerometer from Robosavvy. It's a great bit of kit, which combines a 3-axis gyroscope and a 3-axis accelerometer on the same board. It hooks up easily to an Arduino using the I2C bus. So far, so good...


I then begin searching the Internet for example code with these two devices working together. My search always led me to a huge, unwieldy library, which seemed very bloated, considering all I wanted to do was read some values from the board.

I began to dig deeper and experiment, which enabled me to create the code sample below. It relies on using default values, which are fine for my application - and it's light! The accelerometer channels are very twitchy, so the general advice is to incorporate a low pass filter, which I've done.

Hopefully, you will also find it useful for your projects.

Main Program
#include "Wire.h"
#include "MPU6050.h"
 
MPU6050 mpu;  
int ax, ay, az, gx, gy, gz;
float smooth_ax, smooth_ay, smooth_az;
 
void setup()
{
  Wire.begin();
  Serial.begin(38400);
  mpu.wakeup();  
}
 
void loop()
{
  /* Read device to extract current
   accelerometer and gyroscope values. */
  mpu.read6dof(&ax, &ay, &az, &gx, &gy, &gz);
 
  /* Apply low pass filter to smooth
   accelerometer values. */
  smooth_ax = 0.95 * smooth_ax + 0.05 * ax;
  smooth_ay = 0.95 * smooth_ay + 0.05 * ay;
  smooth_az = 0.95 * smooth_az + 0.05 * az;
 
  /* Output to serial monitor. */
  Serial.print(smooth_ax);
  Serial.print("\t");
  Serial.print(smooth_ay);
  Serial.print("\t");
  Serial.print(smooth_az);
  Serial.print("\t");
  Serial.print(gx);
  Serial.print("\t");
  Serial.print(gy);
  Serial.print("\t");
  Serial.println(gz);
}

MPU6050.h
#include "Arduino.h"
#include "Wire.h"
 
#define MPU6050_DEVICE_ADDRESS   0x68
#define MPU6050_RA_ACCEL_XOUT_H  0x3B
#define MPU6050_RA_PWR_MGMT_1    0x6B
#define MPU6050_PWR1_SLEEP_BIT   6
 
class MPU6050 {
public:
  MPU6050();
  void wakeup();
  void read6dof(int* ax, int* ay, int* az, int* gx, int* gy, int* gz);
 
private:
  byte id;
  byte buffer[14];
  void readByte(byte reg, byte *data);
  void readBytes(byte reg, byte len, byte *data);
  void writeBit(byte reg, byte num, byte data);
  void writeByte(byte reg, byte data);
};

MPU6050.cpp
#include "MPU6050.h"
 
MPU6050::MPU6050() {
  id = MPU6050_DEVICE_ADDRESS;
}
 
/* Wake up device and use default values for
 accelerometer (±2g) and gyroscope (±250°/sec). */
void MPU6050::wakeup() {
  writeBit(MPU6050_RA_PWR_MGMT_1, MPU6050_PWR1_SLEEP_BIT, 0);
}
 
/* Read device memory to extract current
 accelerometer and gyroscope values. */
void MPU6050::read6dof(int* ax, int* ay, int* az, int* gx, int* gy, int* gz) {
  readBytes(MPU6050_RA_ACCEL_XOUT_H, 14, buffer);
  *ax = (((int)buffer[0]) << 8) | buffer[1];
  *ay = (((int)buffer[2]) << 8) | buffer[3];
  *az = (((int)buffer[4]) << 8) | buffer[5];
  *gx = (((int)buffer[8]) << 8) | buffer[9];
  *gy = (((int)buffer[10]) << 8) | buffer[11];
  *gz = (((int)buffer[12]) << 8) | buffer[13];
}
 
/* Read a single byte from specified register. */
void MPU6050::readByte(byte reg, byte *data) {
  readBytes(reg, 1, data);
}
 
/* Read multiple bytes starting at specified register. */
void MPU6050::readBytes(byte reg, byte len, byte *data) {
  byte count = 0;
  Wire.beginTransmission(id);
  Wire.write(reg);
  Wire.requestFrom(id, len);
  while (Wire.available()) data[count++] = Wire.read();
  Wire.endTransmission();
}
 
/* Write bit to specified register and location. */
void MPU6050::writeBit(byte reg, byte num, byte data) {
  byte b;
  readByte(reg, &b);
  b = (data != 0) ? (b | (1 << num)) : (b & ~(1 << num));
  writeByte(reg, b);
}
 
/* Write byte to specified register. */
void MPU6050::writeByte(byte reg, byte data) {
  Wire.beginTransmission(id);
  Wire.write(reg); 
  Wire.write(data);
  Wire.endTransmission(); 
}