Code for autonomous rover for Sparkfun AVC. DataBus won 3rd in 2012 and the same code was used on Troubled Child, a 1986 Jeep Grand Wagoneer to win 1st in 2014.

Dependencies:   mbed Watchdog SDFileSystem DigoleSerialDisp

updater.cpp

Committer:
shimniok
Date:
2013-06-06
Revision:
2:fbc6e3cf3ed8
Parent:
1:cb84b477886c
Child:
3:42f3821c4e54

File content as of revision 2:fbc6e3cf3ed8:

#include "mbed.h"
#include "util.h"
#include "globals.h"
#include "updater.h"
#include "Config.h"
#include "Actuators.h"
#include "Sensors.h"
#include "SystemState.h"
#include "Venus638flpx.h"
//#include "Ublox6.h"
#include "Steering.h"
#include "Servo.h"
#include "Mapping.h"
#include "CartPosition.h"
#include "GeoPosition.h"
#include "kalman.h"

#define UPDATE_PERIOD 0.010             // update period in s

#define _x_ 0
#define _y_ 1
#define _z_ 2

// The below is for main loop at 50ms = 20hz operation
//#define CTRL_SKIP 2 // 100ms, 10hz, control update
//#define MAG_SKIP 1  // 50ms, 20hz, magnetometer update
//#define LOG_SKIP 1  // 50ms, 20hz, log entry entered into fifo

// The following is for main loop at 10ms = 100hz
#define HDG_LAG 100
#define CTRL_SKIP 5 // 50ms (20hz), control update
#define MAG_SKIP 2  // 20ms (50hz), magnetometer update
#define LOG_SKIP 2  // 20ms (50hz), log entry entered into fifo
//#define LOG_SKIP 5  // 50ms (20hz), log entry entered into fifo

Ticker sched;                           // scheduler for interrupt driven routines

int control_count=CTRL_SKIP;
int update_count=MAG_SKIP;              // call Update_mag() every update_count calls to schedHandler()
int log_count=0;                        // buffer a new status entry for logging every log_count calls to schedHandler
int tReal;                              // calculate real elapsed time
int bufCount=0;

extern DigitalOut gpsStatus;

// TODO: 3 better encapsulation, please
extern Sensors sensors;
extern SystemState state[SSBUF];
extern unsigned char inState;
extern unsigned char outState;
extern bool ssBufOverrun;
extern Mapping mapper;
extern Steering steerCalc;              // steering calculator
extern Timer timer;
extern DigitalOut ahrsStatus;           // AHRS status LED

// Navigation
extern Config config;
int nextWaypoint = 0;                   // next waypoint destination
int lastWaypoint = 1;
double bearing;                         // bearing to next waypoint
double distance;                        // distance to next waypoint
float steerAngle;                       // steering angle

// Throttle PID
float speedDt=0;                        // dt for the speed PID
float integral=0;                       // error integral for speed PID
float lastError=0;                      // previous error, used for calculating derivative
bool go=false;                          // initiate throttle (or not)
float desiredSpeed;                     // speed set point
float nowSpeed;

// Pose Estimation
bool initNav = true;                    // initialize navigation estimates
bool doLog = false;                     // determines when to start and stop entering log data
float initialHeading=-999;              // initial heading
CartPosition cartHere;                  // position estimate, cartesian
GeoPosition here;                       // position estimate, lat/lon
int timeZero=0;
int lastTime=-1;                        // used to calculate dt for KF
int thisTime;                           // used to calculate dt for KF
float dt;                               // dt for the KF
float estLagHeading = 0;                   // lagged heading estimate
//GeoPosition lagHere;                  // lagged position estimate; use here as the current position estimate
float errAngle;                         // error between gyro hdg estimate and gps hdg estimate
float gyroBias=0;                       // exponentially weighted moving average of gyro error
float Ag = (2.0/(1000.0+1.0));          // Gyro bias filter alpha, gyro; # of 10ms steps
float Kbias = 0.995;            
float filtErrRate = 0;
float biasErrAngle = 0;

#define MAXHIST 128 // must be multiple of 0x08
#define inc(x)  (x) = ((x)+1)&(MAXHIST-1)
struct history_rec {
    float x;        // x coordinate
    float y;        // y coordinate
    float hdg;      // heading
    float dist;     // distance
    float gyro;     // heading rate
    //float ghdg;   // uncorrected gyro heading
    float dt;       // delta time
} history[MAXHIST]; // fifo for sensor data, position, heading, dt

int hCount;         // history counter; one > HDG_LAG, we can go back in time to reference gyro history
int now = 0;        // fifo input index, latest entry
int prev = 0;       // previous fifo iput index, next to latest entry
int lag = 0;        // fifo output index, entry from 1 second ago (HDG_LAG entries prior)
int lagPrev = 0;    // previous fifo output index, 101 entries prior

/** attach update to Ticker */
void startUpdater()
{
    // Initialize logging buffer
    // Needs to happen after we've reset the millisecond timer and after
    // the schedHandler() fires off at least once more with the new time
    sched.attach(&update, UPDATE_PERIOD);
}

/** set flag to initialize navigation at next schedHandler() call */
void restartNav()
{
    go = false;
    inState = outState = 0;    
    initNav = true;
    return;
}

/** instruct the controller to start running */
void beginRun()
{
    go = true;
    inState = outState = 0;    
    timeZero = thisTime; // initialize 
    bufCount = 0;
    return;
}

/** instruct the controller that we're done with the run */
void endRun()
{
    go = false;
    initNav = true;
    return;
}

/** get elasped time in update loop */
int getUpdateTime()
{
    return tReal;
}

/** Set the desired speed for the controller to attain */
void setSpeed(float speed) 
{
    if (desiredSpeed != speed) {
        desiredSpeed = speed;
        integral = 0; // reset the error integral
    }
    return;
}

/** update() runs the data collection, estimation, steering control, and throttle control */
void update()
{
    tReal = timer.read_us();
    bool useGps=false;

    // TODO 1 do we need to set up the dt stuff after initialization?

    ahrsStatus = 0;
    thisTime = timer.read_ms();
    dt = (lastTime < 0) ? 0 : ((float) thisTime - (float) lastTime) / 1000.0; // first pass let dt=0
    lastTime = thisTime;

    // Add up dt to speedDt
    // We're adding up distance over several update() calls so have to keep track of total time
    speedDt += dt;
    
    // Log Data Timestamp    
    int timestamp = timer.read_ms();

    //////////////////////////////////////////////////////////////////////////////
    // NAVIGATION INIT
    //////////////////////////////////////////////////////////////////////////////
    // initNav is set with call to restartNav() when the "go" button is pressed.  Up
    // to 10ms later this function is called and the code below will initialize the
    // dead reckoning position and estimated position variables
    // 
    if (initNav == true) {
        initNav = false;
        here.set(config.wpt[0]);
        nextWaypoint = 1; // Point to the next waypoint; 0th wpt is the starting point
        lastWaypoint = 0;
        
        // TODO 1 need to start off, briefly, at a slow speed
        
        // Initialize lag estimates
        //lagHere.set( here );
        hCount = 2; // lag entry and first now entry are two entries
        now = 0; 
        // initialize what will become lag data in 1 second from now
        history[now].dt = 0;
        history[now].dist = 0;
        // initial position is waypoint 0
        history[now].x = config.cwpt[0]._x;
        history[now].y = config.cwpt[0]._y;
        cartHere.set(history[now].x, history[now].y);
        // initialize heading to bearing between waypoint 0 and 1
        //history[now].hdg = here.bearingTo(config.wpt[nextWaypoint]);
        initialHeading = history[now].hdg = cartHere.bearingTo(config.cwpt[nextWaypoint]);
        //history[now].ghdg = history[now].hdg;
        // Initialize Kalman Filter
        headingKalmanInit(initialHeading);
        // point next fifo input to slot 1, slot 0 occupied/initialized, now
        lag = 0;
        lagPrev = 0;
        prev = now; // point to the most recently entered data
        now = 1;    // new input slot
    }


    //////////////////////////////////////////////////////////////////////////////
    // SENSOR UPDATES
    //////////////////////////////////////////////////////////////////////////////

    // TODO 3 This really should run infrequently
    sensors.Read_Power();

    sensors.Read_Encoders(); 
    nowSpeed = sensors.encSpeed;

    sensors.Read_Gyro(); 

    sensors.Read_Rangers();

    sensors.Read_Accel();

    //sensors.Read_Camera();

    //////////////////////////////////////////////////////////////////////////////
    // Obtain GPS data                        
    //////////////////////////////////////////////////////////////////////////////

    // synchronize when RMC and GGA sentences received w/ AHRS
    // Do this in schedHandler??  GPS data is coming in not entirely in sync
    // with the logging info
    if (sensors.gps.available()) {
        // update system status struct for logging
        gpsStatus = !gpsStatus;
        state[inState].gpsLatitude = sensors.gps.latitude();
        state[inState].gpsLongitude = sensors.gps.longitude();
        state[inState].gpsHDOP = sensors.gps.hdop();
        state[inState].gpsCourse_deg = sensors.gps.heading_deg();
        state[inState].gpsSpeed_mps = sensors.gps.speed_mps(); // if need to convert from mph to mps, use *0.44704
        state[inState].gpsSats = sensors.gps.sat_count();
        
        // May 26, 2013, moved the useGps setting in here, so that we'd only use the GPS heading in the
        // Kalman filter when it has just been received. Before this I had a bug where it was using the
        // last known GPS data at every call to this function, meaning the more stale the GPS data, the more
        // it would likely throw off the GPS/gyro error term. Hopefully this will be a tad more acccurate.
        // Only an issue when heading is changing, I think.
        
        // GPS heading is unavailable from this particular GPS at speeds < 0.5 mph
        // Also, best to only use GPS if we've got at least 4 sats active -- really should be like 5 or 6
        // Finally, it takes 3-5 secs of runtime for the gps heading to converge.
        useGps = (state[inState].gpsSats > 4 &&
                  state[inState].lrEncSpeed > 1.0 &&
                  state[inState].rrEncSpeed > 1.0 &&
                  (thisTime-timeZero) > 3000); // gps hdg converges by 3-5 sec.                
    }

    DigitalOut useGpsStat(LED1);
    useGpsStat = useGps;
    
    //////////////////////////////////////////////////////////////////////////////
    // HEADING AND POSITION UPDATE
    //////////////////////////////////////////////////////////////////////////////

    // TODO: 2 Position filtering
    //    position will be updated based on heading error from heading estimate
    // TODO: 2 Distance/speed filtering
    //    this might be useful, but not sure it's worth the effort

    // So the big pain in the ass is that the GPS data coming in represents the
    // state of the system ~1s ago. Yes, a full second of lag despite running
    // at 10hz (or whatever).  So if we try and fuse a lagged gps heading with a
    // relatively current gyro heading rate, the gyro is ignored and the heading
    // estimate lags reality
    //
    // In real life testing, the robot steering control was highly unstable with
    // too much gain (typical of any negative feedback system with a very high
    // phase shift and too much feedback). It'd drive around in circles trying to
    // hunt for the next waypoint.  Dropping the gain restored stability but the
    // steering overshot significantly, because of the lag.  It sort of worked but
    // it was ugly. So here's my lame attempt to fix all this. 
    // 
    // We'll find out how much error there is between gps heading and the integrated
    // gyro heading from a second ago.

    // Save current data into history fifo to use 1 second from now
    history[now].dist = (sensors.lrEncDistance + sensors.rrEncDistance) / 2.0; // current distance traveled
    history[now].gyro = sensors.gyro[_z_];  // current raw gyro data
    history[now].dt = dt; // current dt, to address jitter
    history[now].hdg = history[prev].hdg + dt*(history[now].gyro - gyroBias); // compute current heading from current gyro
    clamp(history[now].hdg, 0, 360.0);
    //if (history[now].hdg >= 360.0) history[now].hdg -= 360.0;
    //if (history[now].hdg < 0)      history[now].hdg += 360.0;
    float r = PI/180 * history[now].hdg;
    history[now].x = history[prev].x + history[now].dist * sin(r);    // update current position
    history[now].y = history[prev].y + history[now].dist * cos(r);

    // We can't do anything until the history buffer is full
    if (hCount < HDG_LAG) {
        estLagHeading = history[now].hdg;
        // Until the fifo is full, only keep track of current gyro heading
        hCount++; // after n iterations the fifo will be full
    } else {
        // Now that the buffer is full, we'll maintain a Kalman Filter estimate that is
        // time-shifted to the past because the GPS output represents the system state from
        // the past. We'll also correct our history of gyro readings from the past to the
        // present
        
        ////////////////////////////////////////////////////////////////////////////////
        // UPDATE LAGGED ESTIMATE
        
        // Recover data from 1 second ago which will be used to generate
        // updated lag estimates

        // This represents the best estimate for heading... for one second ago
        // If we do nothing else, the robot will think it is located at a position 1 second behind
        // where it actually is. That means that any control feedback needs to have a longer time
        // constant than 1 sec or the robot will have unstable heading correction.
        // Use gps data when available, always use gyro data

        // Clamp heading to initial heading when we're not moving; hopefully this will
        // give the KF a head start figuring out how to deal with the gyro
        //
        // TODO 1 maybe we should only call the gps version after moving
        if (go) {
            estLagHeading = headingKalman(history[lag].dt, state[inState].gpsCourse_deg, useGps, history[lag].gyro, true);
        } else {    
            estLagHeading = headingKalman(history[lag].dt, initialHeading, true, history[lag].gyro, true);
        }

        // TODO 1 are we adding history lag to lagprev or should we add lag+1 to lag or what?
        // Update the lagged position estimate
        history[lag].x = history[lagPrev].x + history[lag].dist * sin(estLagHeading);
        history[lag].y = history[lagPrev].y + history[lag].dist * cos(estLagHeading);
        
        ////////////////////////////////////////////////////////////////////////////////
        // UPDATE CURRENT ESTIMATE

        // Now we need to re-calculate the current heading and position, starting with the most recent
        // heading estimate representing the heading 1 second ago.
        //        
        // Nuance: lag and now have already been incremented so that works out beautifully for this loop
        //
        // Heading is easy. Original heading - estimated heading gives a tiny error. Add this to all the historical
        // heading data up to present.
        //
        // For position re-calculation, we iterate HDG_LAG times up to present record. Haversine is way, way too slow,
        // trig calcs is marginal. Update rate is 10ms and we can't hog more than maybe 2-3ms as the outer
        // loop has logging work to do. Rotating each point is faster; pre-calculate sin/cos, etc. for rotation
        // matrix.

        // Low pass filter the error correction.  Multiplying by 0.01, it takes HDG_LAG updates to correct a 
        // consistent error; that's 0.10s/0.01 = 1 sec.  0.005 is 2 sec, 0.0025 is 4 sec, etc.
        errAngle = (estLagHeading - history[lag].hdg); 
        // low pass filter error angle:
        clamp(errAngle, -180.0, 180.0);
        //if (errAngle > 180.0) errAngle -= 360.0;
        //if (errAngle <= -180.0) errAngle += 360.0;
        errAngle *= 0.01; // multiply only after clamping to -180:180 range.

        //fprintf(stdout, "%d %.2f, %.2f, %.4f %.4f\n", lag, estLagHeading, history[lag].hdg, estLagHeading - history[lag].hdg, errAngle);
        float cosA = cos(errAngle * PI / 180.0);
        float sinA = sin(errAngle * PI / 180.0);
        // Update position & heading from history[lag] through history[now]
        int i = lag;
        // TODO 2 parameterize heading lag -- for uBlox it seems to be ~ 600ms, for Venus, about 1000ms
        for (int j=0; j < HDG_LAG; j++) {
            //fprintf(stdout, "i=%d n=%d l=%d\n", i, now, lag);
            // Correct gyro-calculated headings from past to present including history[lag].hdg
            history[i].hdg += errAngle;
            clamp(history[i].hdg, 0, 360.0);
            //if (history[i].hdg >= 360.0) history[i].hdg -= 360.0;
            //if (history[i].hdg < 0) history[i].hdg += 360.0;
            // Rotate x, y by errAngle around pivot point; no need to rotate pivot point (j=0)
            if (j > 0) {
                float dx = history[i].x-history[lag].x;
                float dy = history[i].y-history[lag].y;
                // rotation matrix
                history[i].x = history[lag].x + dx*cosA - dy*sinA;
                history[i].y = history[lag].y + dx*sinA + dy*cosA;
            }
            inc(i);
        }
        // increment lag pointer and wrap        
        lagPrev = lag;
        inc(lag);
    }
    // "here" is the current position
    cartHere.set(history[now].x, history[now].y);

    //////////////////////////////////////////////////////////////////////////////
    // NAVIGATION UPDATE
    //////////////////////////////////////////////////////////////////////////////
    
    bearing = cartHere.bearingTo(config.cwpt[nextWaypoint]);
    distance = cartHere.distanceTo(config.cwpt[nextWaypoint]);
    float prevDistance = cartHere.distanceTo(config.cwpt[lastWaypoint]);
    double relativeBrg = bearing - history[now].hdg;
    // if correction angle is < -180, express as negative degree
    clamp(relativeBrg, -180.0, 180.0);
    //if (relativeBrg < -180.0) relativeBrg += 360.0;
    //if (relativeBrg > 180.0)  relativeBrg -= 360.0;

    // if within config.waypointDist distance threshold move to next waypoint
    // TODO 3 figure out how to output feedback on wpt arrival external to this function
    if (go) {

        // if we're within brakeDist of next or previous waypoint, run @ turn speed
        // This would normally mean we run at turn speed until we're brakeDist away
        // from waypoint 0, but we trick the algorithm by initializing prevWaypoint to waypoint 1
        if ( (thisTime - timeZero) < 3000 ) {
            setSpeed( config.startSpeed );
        } else if (distance < config.brakeDist || prevDistance < config.brakeDist) {
            setSpeed( config.turnSpeed );
        } else {
            setSpeed( config.topSpeed );
        }

        if (distance < config.waypointDist) {
            //fprintf(stdout, "Arrived at wpt %d\n", nextWaypoint);
            //speaker.beep(3000.0, 0.5); // non-blocking
            lastWaypoint = nextWaypoint;
            nextWaypoint++;
        }
        
    } else {
        setSpeed( 0.0 );
    }
    // Are we at the last waypoint?
    // currently handled external to this routine
        
    //////////////////////////////////////////////////////////////////////////////
    // OBSTACLE DETECTION & AVOIDANCE
    //////////////////////////////////////////////////////////////////////////////
    // TODO: 1 limit steering angle based on object detection ?
    // or limit relative brg perhaps?
    // TODO: 1 add vision obstacle detection and filtering
    // TODO: 1 add ranger obstacle detection and filtering/fusion with vision


    //////////////////////////////////////////////////////////////////////////////
    // CONTROL UPDATE
    //////////////////////////////////////////////////////////////////////////////

    // TODO: 1 improve the steering algorithm to take cross-track error into account

    if (--control_count == 0) {
  
        steerAngle = steerCalc.pathPursuitSA(history[now].hdg, history[now].x, history[now].y,
                                             config.cwpt[lastWaypoint]._x, config.cwpt[lastWaypoint]._y,
                                             config.cwpt[nextWaypoint]._x, config.cwpt[nextWaypoint]._y);
        
        // TODO 3: eliminate pursuit config item
        /*
        if (config.usePP) {
            steerAngle = steerCalc.purePursuitSA(history[now].hdg, 
                                                 history[now].x, history[now].y,
                                                 config.cwpt[lastWaypoint]._x, config.cwpt[lastWaypoint]._y,
                                                 config.cwpt[nextWaypoint]._x, config.cwpt[nextWaypoint]._y);
        } else {
            steerAngle = steerCalc.calcSA(relativeBrg, config.minRadius); // use the configured minimum turn radius
        }
        */
        
        
        // Apply gain factor for near straight line
        if (fabs(steerAngle) < config.steerGainAngle) steerAngle *= config.steerGain;

        // Curb avoidance
        /*
        if (sensors.rightRanger < config.curbThreshold) {
            steerAngle -= config.curbGain * (config.curbThreshold - sensors.rightRanger);
        }
        */
                    
        setSteering( steerAngle );

        // PID control for throttle
        // TODO: 3 move all this PID crap into Actuators.cpp
        // TODO: 3 probably should do KF or something for speed/dist; need to address GPS lag, too
        // if nothing else, at least average the encoder speed over mult. samples
        if (desiredSpeed <= 0.1) {
            setThrottle( config.escZero );
        } else {
            // PID loop for throttle control
            // http://www.codeproject.com/Articles/36459/PID-process-control-a-Cruise-Control-example
            float error = desiredSpeed - nowSpeed; 
            // track error over time, scaled to the timer interval
            integral += (error * speedDt);
            // determine the amount of change from the last time checked
            float derivative = (error - lastError) / speedDt; 
            // calculate how much to drive the output in order to get to the 
            // desired setpoint. 
            int output = config.escZero + (config.speedKp * error) + (config.speedKi * integral) + (config.speedKd * derivative);
            if (output > config.escMax) output = config.escMax;
            if (output < config.escMin) output = config.escMin;
            //fprintf(stdout, "s=%.1f d=%.1f o=%d\n", nowSpeed, desiredSpeed, output);
            setThrottle( output );
            // remember the error for the next time around.
            lastError = error; 
        }

        speedDt = 0; // reset dt to begin counting for next time
        control_count = CTRL_SKIP;
    }      

    //////////////////////////////////////////////////////////////////////////////
    // DATA FOR LOGGING
    //////////////////////////////////////////////////////////////////////////////

    // Periodically, we enter a new SystemState into the FIFO buffer
    // The main loop handles logging and will catch up to us provided 
    // we feed in new log entries slowly enough.
    // log_count initialized to 0 to begin with forcing initialization below
    // but no further updates until in go mode
    if (go && (log_count > 0)) --log_count;
    // Only enter new SystemState data when in "go" mode (armed to run, run,
    // and end run)
    // We should really only be adding to the state fifo if we're in 'go' mode
    if (log_count <= 0) {
        // Copy data into system state for logging
        inState++;                      // Get next state struct in the buffer
        inState &= SSBUF;               // Wrap around
        ssBufOverrun = (inState == outState);
        // Need to clear out encoder distance counters; these are incremented
        // each time this routine is called.
        // TODO 1 is there anything we can't clear out?
        // TODO 1 use clearState()
        state[inState].lrEncDistance = 0;
        state[inState].rrEncDistance = 0;
        // initialize gps data
        state[inState].gpsLatitude = 0;
        state[inState].gpsLongitude = 0;
        state[inState].gpsHDOP = 0;
        state[inState].gpsCourse_deg = 0;
        state[inState].gpsSpeed_mps = 0;
        state[inState].gpsSats = 0;
        log_count = LOG_SKIP;       // reset counter
        bufCount++;
    }

    // Log Data Timestamp    
    state[inState].millis = timestamp;

    // TODO: 3 recursive filtering on each of the state values
    state[inState].voltage = sensors.voltage;
    state[inState].current = sensors.current;
    for (int i=0; i < 3; i++) {
        state[inState].m[i] = sensors.m[i];
        state[inState].g[i] = sensors.g[i];
        state[inState].a[i] = sensors.a[i];
    }
    state[inState].gTemp = sensors.gTemp;
    state[inState].lrEncSpeed = sensors.lrEncSpeed;
    state[inState].rrEncSpeed = sensors.rrEncSpeed;
    state[inState].lrEncDistance += sensors.lrEncDistance;
    state[inState].rrEncDistance += sensors.rrEncDistance;
    //state[inState].encHeading += (state[inState].lrEncDistance - state[inState].rrEncDistance) / TRACK;
    state[inState].estHeading = history[now].hdg; // gyro heading estimate, current, corrected
    state[inState].estLagHeading = history[lag].hdg; // gyro heading, estimate, lagged
    mapper.cartToGeo(cartHere, &here);
    state[inState].estLatitude = here.latitude();
    state[inState].estLongitude = here.longitude();
    state[inState].estX = history[now].x;
    state[inState].estY = history[now].y;
    state[inState].bearing = bearing;
    state[inState].distance = distance;
    state[inState].nextWaypoint = nextWaypoint;
    state[inState].gbias = gyroBias;
    state[inState].errAngle = biasErrAngle;
    //state[inState].leftRanger = sensors.leftRanger;
    //state[inState].rightRanger = sensors.rightRanger;
    //state[inState].centerRanger = sensors.centerRanger;
    state[inState].steerAngle = steerAngle;
    // Copy AHRS data into logging data
    //state[inState].roll = ToDeg(ahrs.roll);
    //state[inState].pitch = ToDeg(ahrs.pitch);
    //state[inState].yaw = ToDeg(ahrs.yaw);

    // increment fifo pointers with wrap-around
    prev = now;
    inc(now);

    // timing
    tReal = timer.read_us() - tReal;

    ahrsStatus = 1;
}