NavBot: Navigator

Screen Shot 2014-02-23 at 2.06.45 PMOne of the core components of NavBot is the Navigator. Its job is to encapsulate the localization information of the robot, i.e. where in the world it is.

For this implementation I am only considering a two dimensional world as the bot will mostly likely only travel around my office floor or some other flat surface. At some later point if we want the bot to travel over general terrain we’d need to factor in elevation too.

But for now the navigator tracks the following data:

  • Position (x,y) – The x and y location of the bot from its center, in millimeters, from some origin (0,0)
  • Heading (h) – The orientation of the bot in degrees from North about its center.
  • Speed (v) – The speed of motion, in millimeters per second, in the current heading from its center.
  • Turn Rate – The degrees per second the bot is turning about its center.

NavBot - Pose

Implementation

I’ve implemented the Navigator as it’s own C++ class and tried to make it as modular as possible so it can be deployed in any application requiring localization.

You can view the current implementation here on GitHub in Navigator.h and Navigator.cpp.

For simplicity it uses floating point math. However, as I also want to be able to change over to fixed point math Navigator abstracts out the some basic data types:

//----------------------------------------
// Base Types
//----------------------------------------

typedef float       nvCoord;        // millimeters
typedef float       nvDegrees;      // degrees
typedef float       nvRadians;      // radians
typedef nvDegrees   nvHeading;      // degrees from North
typedef float       nvRate;         // change per second
typedef float       nvDistance;     // millimeters
typedef uint32_t    nvTime;         // time in milliseconds

And as you can see from the comments we are using millimeters, degrees and milliseconds as the units of measurement.

Everything is floating point except time, which is an unsigned 32 bit integer.

Each type has an “nv” prefix to avoid possible name clashes with other libraries or code.

To make the code more readable, and to facilitate possible conversion to fixed point at a later date, Navigator provides some helper macros:

//----------------------------------------
// Helper Macros
//----------------------------------------

#define nvMM(D)         ((nvDistance)(D))
#define nvMETERS(D)     ((nvDistance)((D)*1000))
#define nvMS(MS)        ((nvTime)(MS))
#define nvSECONDS(S)    ((nvTime)((S)*1000))
#define nvDEGREES(D)    ((nvDegrees)(D))
#define nvRADIANSS(R)   ((nvRadians)(R))

#define nvNORTH         nvDEGREES(0)
#define nvNORTHEAST     nvDEGREES(45)
#define nvEAST          nvDEGREES(90)
#define nvSOUTHEAST     nvDEGREES(135)
#define nvSOUTH         nvDEGREES(180)
#define nvSOUTHWEST     nvDEGREES(225)
#define nvWEST          nvDEGREES(270)
#define nvNORTHWEST     nvDEGREES(315)

Navigator also uses the following data abstractions:

//----------------------------------------
// nvPosition
//----------------------------------------

struct nvPosition
{
    nvCoord     x;              // mm from origin
    nvCoord     y;              // mm from origin
};

//----------------------------------------
// nvPose
//----------------------------------------

struct nvPose
{
    nvPosition  position;       // mm from (0, 0)
    nvHeading   heading;        // degrees from North
};

Using Navigator

To use Navigator we first need to create an instance of the class:


#include "Navigator.h"

Navigator navigator;

Then in setup we need to initialize the instance:


// Navigator defines
#define WHEEL_BASE      nvMM(83.5)
#define WHEEL_DIAMETER  nvMM(35.4)
#define TICKS_PER_REV   1204

setup()
{
 :
 :
  // set up navigation
  navigator.InitEncoder( WHEEL_DIAMETER, WHEEL_BASE, TICKS_PER_REV );

 :
 :

}

This particular implementation uses dead reckoning via wheel encoders so we pass in the wheel diameter, distance of the wheel base and the number of encoder ticks per revolution.

NavBot

With this information the Navigator can calculate the number of millimeters travelled per encoder tick by taking the circumference of the wheels and dividing it by the number of encoder ticks.

//----------------------------------------
//
//----------------------------------------

void Navigator::InitEncoder( nvDistance wheel_diameter, nvDistance wheel_base, uint16_t ticks_per_rev )
{
    m_dist_per_tick = ticks_per_rev > 0 ? wheel_diameter*M_PI/ticks_per_rev : 0.0f;
    m_wheel_base = wheel_base > 1.0f ? wheel_base : 1.0f;
}

We also store the distance of the wheel base as we will need it later for calculating changes in the bot’s heading.

By default the Navigator assumes that the bot is at location (0, 0) and heading (0). If we want to set a different starting location we use these two functions:


    navigator.SetStartPosition( nvMM(200), nvMM(300));
    navigator.SetStartHeading( nvEAST );

or


    nvPose pose;

    pose.position.x = nvMM(200);
    pose.position.y = nvMM(300);
    pose.heading = nvEAST;

    navigator.SetStartPose( pose );

To enable the navigator to track the movement of the robot we need first call its Reset() method and then regularly call the UpdateTicks() method.

So first call Reset():


    navigator.Reset( millis() );

passing in the current time in milliseconds. This will initialize the navigator and set its location to the starting position and heading.

After Reset() we must regularly call UpdateTicks() thus:


    navigator.UpdateTicks( lticks, rticks, millis() );

passing in the number of encoder ticks for the left and right wheels since we last called UpdateTicks() and/or Reset(), and the current time in milliseconds.

Using the tick counts and time the navigator can calculate changes in the bot’s location.

We can see this better if we look at the code for both Rest() and UpdateTicks().

First Reset():

//----------------------------------------
//
//----------------------------------------

void Navigator::Reset( nvTime now )
{
    m_last_ticks_time = now;
    m_dt = m_lticks = m_rticks = 0.0f;
    m_pose = m_init_pose;
    m_heading = nvDegToRad(m_pose.heading);
    m_speed = nvMM(0.0);
    m_turn_rate = nvDEGREES(0.0);
}

It saves the time that it was called and sets the tick counts, m_lticks and m_rticks, and time elapsed, m_dt, to zero. It then resets the current pose to the initial pose, which is either the default pose or the one set using the SetStartPose(), or SetStartPosition() and SetStartHeading() methods.

It also makes a copy of the bots heading, converting it from degrees to radians, as it will use radians later on while doing dead reckoning calculations.

Finally it sets the speed and turn rate to zero.

When UpdateTicks() is called it does the following:

//----------------------------------------
//
//----------------------------------------

bool Navigator::UpdateTicks( int16_t lticks, int16_t rticks, nvTime now )
{
    // update delta values
    m_dt +=  nvDeltaTime( m_last_ticks_time, now );
    m_lticks += lticks;
    m_rticks += rticks;

    // remember time for next call
    m_last_ticks_time = now;

    // see if we have accumulated min time delta
    if ( m_dt < m_min_dt )
    {
        // no, so wait
        return false;
    }

    // use ticks and time delta to update position

    nvDistance dr = ((nvDistance)m_rticks)*m_dist_per_tick;
    nvDistance dl = ((nvDistance)m_lticks)*m_dist_per_tick;
    nvDistance dd =  (dr + dl)/2;

    // calc and update change in heading
    nvRadians dh = (dl - dr)/m_wheel_base;
    m_heading = nvClipRadians( m_heading + dh);

    // update velocities
    m_speed = (dd*1000.0f)/m_dt;
    m_turn_rate = (nvRadToDeg(dh)*1000.0f)/m_dt;

    // update pose
    m_pose.heading = nvRadToDeg(m_heading);
    m_pose.position.x += dd*sin(m_heading);
    m_pose.position.y += dd*cos(m_heading);

    // reset delta values
    m_dt = 0;
    m_lticks = 0;
    m_rticks = 0;

    return true;
}

This code is not too complicated. It first adds up the time since it was last called and keeps a running total of left and right ticks.

It then checks to see if a minimum time delta has passed. This time delta defaults to 100 milliseconds but can be changed by calling:


    navigator.SetMinInterval( nvMS(50) );

Once the minimum time interval has elapsed it calculates the new location of the bot based on the tick counts and the amount of time elapsed.

Keep in mind that the calculations are an approximation. I am using the equations defined in [6] of A Tutorial and Elementary Trajectory Model for the Differential Steering System of Robot Wheel Actuators by G.W. Lucas. At some later date I plan to implement the more complete equations in section [5].

dr and dl are the distance each wheel has moved. dd is the distance that the bot as a whole has moved about its center point.

dh is the change of heading, in radians, due to the difference in dl and dr.

It adds the change in heading to the internal heading variable, m_heading, and clips the value so it is always in the range 0 to 2π.

It then calculates the rate of change of dd and dh in seconds based on the time delta of the measurement.

Finally from the new heading and the amount of displacement of the robot, i.e. dd, it calculates the changes to x and y, and updates the current pose information with the new values.

At any time we can query this information from the navigator by using various getter methods:


    nvPose  pose = navigator.Pose();

    Serial.print("Heading: ");
    Serial.println( pose.heading );

    Serial.print("Position: (");
    Serial.print( pose.position.x );
    Serial.print(", ");
    Serial.print( pose.position.y );
    Serial.println(")");

    Serial.print("Speed: ");
    Serial.println( navigator.Speed() );

    Serial.print("Turn Rate: ");
    Serial.println( navigator.TurnRate() );

Notes about this Implementation

This is an initial implementation of the navigator. You can view all the code on GitHub. However, it would be extremely naive to think that this code is useable in its current form. I know for a fact that there are two major issues that need to be addressed.

The first is that the Zumo bot I am using is track based. It does not have nice thin wheels with small contact areas. The tracks will have some amount of slippage while turning and the navigator will need to compensate for this.

I also know, again from previous experience, that even when each track is turning the same number of ticks, the bot’s path is not straight. I believe this is an alignment issue but still have to explore the problem in more depth.

In any event, the navigator will need to handle any systematic biases inherent in the physical characteristics of the bot. Once the basic Pilot code is functional I can better address those issues.

But for now the navigator seems to work as expected. In my tests I discovered that my wheel diameter value was wrong. I had it at 35.4mm when it should have been 38.9mm.

Here is a short video showing the navigator in action. Keep in mind that the webcam has the video flipped from left to right which makes my -90° turn (to 270°) look like a +90° turn.

I was very surprised by the accuracy of the turn. I thought it would be way off.

Next part is to get the Pilot working. I have a lot of it done but it is much more involved piece of software than the Navigator.

Comments welcome