A Robotics Project Page
Google
 
Web    www.PhilBot.com
My other sites
  • OurCoolHouse
  • Phil's Resume
  • Web Portfolio

  • [Home] [Projects] [Hardware] [Software] [Books] [Links] [Downloads]


    BOB: Software: Lesson G - Closed Loop Wheel Speed Control using Virtual Circuits.          [Back to BOB Software]        [Back to BOB index]

    This is my most complicated Virtual Circuit (VC) program so far.  It uses a range of VC objects to monitor the wheel speed, determine if it's too fast or slow, and then automatically adjust the motor drive signal.  To run two motors it's cutting very close to the maximum Object Memory limit, although this is aggravated by the fact that the oMathC (Clocked Math object) doesn't seem to work.

    To prepare for this program I had to determine some things:

    1. What is the actual maximum speed of an unloaded wheel, with the motor drive at full power.
    2. What control cycle rate did I want (ie: how many times a second should the motor drive be updated)
    3. What range of motor drive signals correspond to the minimum and maximum measured wheel speeds.

    I did some experimentation with BOB and came up with these answers.

    1. According to the oQEncoder Object, when I tested my hacked Twin Motor Gearbox, the encoder speed ranged from 0 to 40 clicks per second.  This is with the PWM ranging from 0 to 100% drive, and the drive battery fully charged.
    2. A motor control rate of 4 cycles per second was probably best for BOB.  The Tamiya Gearboxes respond fairly slowly to changes in motor drive and at 4 cycles per second, full speed would correspond to a maximum of 10 encoder clicks per cycle.  This means I could perhaps hope for 10 steps of speed control.
    3. With a PWM Period of 100, the TMG didn't start turning until a control signal of 20 was applied, and then it appeared to be running at full speed when the drive signal was increased to 75.

    So now I had my general operating environment nailed down, so I could start assembling a VC to do the job.  A full closed loop control system might use a PID controller.  This uses three feedback terms (Proportional, Integral & Differential) to control the drive signal.  Most controllers usually only need two of these terms, and for the sake of simplicity mine is going to only use one term.  I'm going to just use the I (Integral) term.  The reason that I need to use an Integral gain term is because I need to increase or decrease the Motor drive based on the speed error.  Once the speed is correct, the drive needs to stay the same.  This is how the I on a PID works.

    I started thinking about how I could measure the speed "error".  Since the only feedback I have is the Encoder counts, I need to see how many counts occur over the cycle time to calculate speed, then I need to see how this compares with the Set-point speed, to calculate an error.  My mind went back to [Lesson F] where I used the encoder reaching zero to tell me when to stop.  I wondered if I could do a similar thing to determine error.  It occurred to me that if I knew how many clicks I expected to get per cycle at a given speed, I could preset the encoder with that number at the start of each cycle and have the encoder count down instead of up. If the actual speed was correct, it would count down to zero at the end of the cycle.  If the speed was incorrect, then whatever was left in the encoder at the end of the cycle would actually be the speed error.

    For example... I know that at half speed I expect to get 5 clicks per cycle (1/4 Sec), so at the start of each cycle I preload the encoder with 5.  Now at the end of the cycle, if the wheels are turning at half speed, the encoder would have counted down to 0... no error.  But if the wheel was actually turning at 1/4 speed, the encoder would only have counted down to 2 or 3.  This is the speed error.  I can now use this value (with some gain applied) to increase the motor drive to increase the wheel speed.  Slick eh?

    OK, so what objects can I use, and what other constants are involved?

    As always, in this program (PBot_BOB-G_VC6.osc) I start using the I/O definitions described on the main BOB [Software page].   Then I need to add some special values that I'll use when issuing movement commands.  I like to give these values names (using Const) and define them at the top of the program so I can change all occurrences in one easy location:  Here are the constants I've defined:

    //-----------------------------------------------------------------
    // Constants, used for default values
    //-----------------------------------------------------------------
    Const   TAMIYA_HIGH_GEAR    =   0;      // The high and low gear ratios drive the 
    Const   TAMIYA_LOW_GEAR     =   1;      // wheels in opposite direction, so here is 
                                            // where we compensate.

    Const   GEAR_SELECTION      = TAMIYA_LOW_GEAR;  // set to LOW_GEAR or HIGH_GEAR
    Const   PID_RATE            = 185;      // Rate divider for 4Hz.    
    Const   PID_LIMIT           =  75;      // Limit active Drive Signal
    Const   MOTOR_DEAD_BAND     =  20;      // Minimum drive offset

    I use all the usual Hardware Objects:

    //-----------------------------------------------------------------
    // User Objects.  Use these to access BOB hardware.
    //-----------------------------------------------------------------
    oCountDownO ActionTimer = New oCountDownO;  // User Timer   

    // Wheel motor driver Objects
    oDCMotor2 LeftMotor     = New oDCMotor2// Driver For the Left Wheel
    oDCMotor2 RightMotor    = New oDCMotor2// Driver for the Right Wheel

    // Wheel Optical Encoder Objects
    oQencode LeftClicks     = New oQencode// Value for Left Encoder
    oQencode RightClicks    = New oQencode// Value for Right Encoder

    But I also needed to define a NEW bunch of Virtual Circuit Objects:

    //-----------------------------------------------------------------
    // Virtual Circuits.  Use these to manipulate BOB hardware.
    //-----------------------------------------------------------------
    oClock  PID_Clock       = New oClock;   // Set to run at PID Rate

    oBusIC  SetSpeedLeft    = New oBusIC;   // Holds the desired clicks
    oMath   PIDLeft         = New oMath;    // Updates the Drive Signal
    oBusIC  DriveLeft       = New oBusIC;   // Holds the motor output
    oCompare0 LimitLeft     = New oCompare0;    // Indicates out of limit condition 
    oWire   LimitLeftWire   = New oWire;    // Transfers outoflimit condition 
     
    oBusIC  SetSpeedRight   = New oBusIC;   // Holds the desired clicks
    oMath   PIDRight        = New oMath;    // Updates the Drive Signal
    oBusIC  DriveRight      = New oBusIC;   // Holds the motor output
    oCompare0 LimitRight    = New oCompare0;    // Indicates out of limit condition 
    oWire   LimitRightWire  = New oWire;    // Transfers outoflimit condition 

    The first thing that's new with these VC's is that many of them are "clocked".  This means that I can control when they perform their action.  I'm going to use a common clock for all of them, and I use both the negative or positive edge of the clock to indicate the start and end of the cycle respectively.  I use PID_Clock to generate the common clock rate that corresponds to the Controller cycle rate.

    SetSpeedLeft is used to hold the speed set-point and PIDLeft is used to add the current error to the current motor drive signal.  DriveLeft is used to clock the new drive value into the motor controller.

    A common problem with PID's that use Integral gain like ours is something called "wind-up".  This is caused by a situation where no matter how high the drive goes, the controller cannot reach the set-point.  When this happens, the integral term just gets bigger and bigger.  Then when it comes time to change speed or direction, the Integral term has to slowly un-wind itself before the output changes.  To prevent this, PID's have anti-wind-up logic.  In our case we will test to see if the drive signal is going to get above the maximum effective vale, and if so, we turn off the integral term.

    LimitLeft is used to detect that the new drive signal will be out of range, and LimitLeftWire is used to inhibit DriveLeft from updating the motor drive.

    Here's the SetupVC( ) function that configures these VC's:

    //-----------------------------------------------------------------
    //  SetupVC()
    //  Configure all the Virtual Circuits here
    //-----------------------------------------------------------------
    Void    SetupVC( Void )
    {
        // PID Clocks
        PID_Clock.Mode          = 1;            // Pulse High
        PID_Clock.Rate          = PID_RATE;     // Desired divider
        PID_Clock.Operate       = cvTrue;       // Run the PID clock

        // Left Wheel PID
        SetSpeedLeft.ClockIn.Link(PID_Clock);   // Use PID Clock
        SetSpeedLeft.InvertC    = cvTrue;       // Transfer on negative clock edge
        SetSpeedLeft.Output.Link(LeftClicks);   // Clock Data into Encoder
        SetSpeedLeft.Operate    = cvTrue;       // Enable VC

        PIDLeft.Input1.Link(LeftClicks);        // Read Encoder Value
        PIDLeft.Input2.Link(LeftMotor);         // Read Motor Value
        PIDLeft.Output.Link(DriveLeft);         // Send Sum to Potential Drive Value
        PIDLeft.Mode            = cvAdd;        // Add the inputs
        PIDLeft.Operate         = cvTrue;       // Enable VC

        LimitLeft.Input.Link(DriveLeft);        // Test Potential Drive Signal
        LimitLeft.Fuzziness     = PID_LIMIT;    // Set invalid width
        LimitLeft.Operate       = cvTrue;       // Enable VC

        DriveLeft.ClockIn.Link(PID_Clock);      // Use PID Clock
        DriveLeft.Output.Link(LeftMotor);       // Clock Data into Motor

        LimitLeftWire.Input.Link(LimitLeft.Between);   // Transfer "Between" to enable Drive
        LimitLeftWire.Output.Link(DriveLeft.Operate);   
        LimitLeftWire.Operate   = cvTrue;       // Enabled


        // Right Wheel PID
        SetSpeedRight.ClockIn.Link(PID_Clock);  // Use PID Clock
        SetSpeedRight.InvertC   = cvTrue;       // Transfer on negative clock edge
        SetSpeedRight.Output.Link(RightClicks); // Clock Data into Encoder
        SetSpeedRight.Operate   = cvTrue;       // Enable VC

        PIDRight.Input1.Link(RightClicks);      // Read Encoder Value
        PIDRight.Input2.Link(RightMotor);       // Read Motor Value
        PIDRight.Output.Link(DriveRight);       // Send Sum to Potential Drive Value
        PIDRight.Mode           = cvAdd;        // Add the inputs
        PIDRight.Operate        = cvTrue;       // Enable VC

        LimitRight.Input.Link(DriveRight);      // Test Potential Drive Signal
        LimitRight.Fuzziness    = PID_LIMIT;    // Set invalid width
        LimitRight.Operate      = cvTrue;       // Enable VC

        DriveRight.ClockIn.Link(PID_Clock);     // Use PID Clock
        DriveRight.Output.Link(RightMotor);     // Clock Data into Motor

        LimitRightWire.Input.Link(LimitRight.Between);   // Transfer "Between" to enable Drive
        LimitRightWire.Output.Link(DriveRight.Operate); 
        LimitRightWire.Operate  = cvTrue;       // Enabled
    }

    The super tricky thing here is that the oMath object is continually calculating what the New Motor drive might be, and writing it into the input side of  the oBusIC object.  At the end of the cycle (positive clock edge) the oCompare object looks at this new value and decides if it is safe to write it into the motor.  The OK state is signaled by the  oCompare's between output.  Between is wired to the operate member of the oBusIC object, so only if the new value is OK, is it written into the motor on the next positive clock edge. 

    All that's left is to get things rolling.  I created a function called SetSpeed( ) that takes a few standardized parameters and scales them into good values to start the PID with.  For example, I permit a speed input of +/- 128.  Inside Set Speed this is tested against zero (the stop condition) and if a speed is called for, it converts this to a Clicks per Cycle number in the range of +/- 9.  The math is messy, but that's what it's doing.  Likewise, an initial value for the motor drive is calculated to get things started quickly (effectively preloading the integrator).  I calculate an initial motor drive value in the range of  20 - 56.  Here's SetSpeed( )

    //-----------------------------------------------------------------
    //  Set Speed(LeftSpeed, RightSpeed, Time)
    //  This function drives both wheels at the indicated speed.
    //  The motion is held for "Time" tenth of a second
    //  No time limit is placed if Time = 0
    //
    //  Speed range is +/- 128
    //  Time Range is 0 - 255
    //-----------------------------------------------------------------
    Void SetSpeed(Byte LeftSpeed, Byte RightSpeed, Byte DriveTime)

        // Set Left Speed
        If (LeftSpeed != 0)
        {
            // Turn off brake
            LeftMotor.Brake = cvOff;        
            
            // Do Setup based in direction of motion.
            If (LeftSpeed > 128)  // Reverse speed
            {
                // Move speed range to (-1 to -9) and set initial drive signal
                LeftSpeed       = ((255 - LeftSpeed) >> 4) + 1;
                LeftMotor.Value = - ((LeftSpeed << 2) + MOTOR_DEAD_BAND) ;
                SetSpeedLeft    = - LeftSpeed ;
            }
            Else
            {
                // Move speed range to (1 to 9) and set initial drive signal
                LeftSpeed = (LeftSpeed >> 4) + 1;
                LeftMotor.Value = (LeftSpeed << 2) + MOTOR_DEAD_BAND ;
                SetSpeedLeft    = LeftSpeed ;
            }
        }
        Else
        {
            // Stop motor and feedback
            LeftMotor.Value = 0 ;
            SetSpeedLeft    = 0 ;
            LeftMotor.Brake = cvOn;     
        }

        // Set Right Speed
        If (RightSpeed != 0)
        {
            // Turn off brake
            RightMotor.Brake = cvOff;       

            // Do Setup based in direction of motion.
            If (RightSpeed > 128)  // Reverse speed
            {
                // Move speed range to (-1 to -9) and set initial drive signal
                RightSpeed       = ((255 - RightSpeed) >> 4) + 1;
                RightMotor.Value = - ((RightSpeed << 2) + MOTOR_DEAD_BAND) ;
                SetSpeedRight    = - RightSpeed ;
            }
            Else
            {
                // Move speed range to (1 to 9) and set initial drive signal
                RightSpeed = (RightSpeed >> 4) + 1;
                RightMotor.Value = (RightSpeed << 2) + MOTOR_DEAD_BAND ;
                SetSpeedRight    = RightSpeed ;
            }
        }
        Else
        {
            // Stop motor and feedback
            RightMotor.Value = 0 ;
            SetSpeedRight    = 0 ;
            RightMotor.Brake = cvOn;        
        }


        // Continue action for requested time period in tenths.
        If (DriveTime > 0)
        {
            ActionTimer = DriveTime;
            While(ActionTimer.NonZero);
        }
    }

    My Main program just puts BOB through some simple motions to illustrate how to set the speeds.

    //-----------------------------------------------------------------
    //  BOB Main Program
    //-----------------------------------------------------------------
    Void Main(Void)
    {
        // Always setup the required IO and VC's first.
        SetupIO();
        SetupVC();
        
        // Wait for 5 second "Start Up" to elapse
        While(ActionTimer.NonZero) ;

        // Do a range of motions
        SetSpeed(  10,   10,  100); // Straight forward real slow for 10 seconds
        SetSpeed(  30,  -30,   20); // Spin right for 2 seconds
        SetSpeed( -80,  -80,  100); // Back up real fast for 10 seconds
        SetSpeed(   0,   0,   0);   // Stop;
        
        While(1);                   // Wait.
        
    }

    The most important thing that came out of this lesson was the new ability to control BOB at VERY slow speeds.  Until now it was necessary to run the motors at a level that was guaranteed to overcome any gear friction or drag on the ground.  Now with the Closed Loop control I can set a very slow speed and the controller will quickly ramp up the drive until BOB moves, and then it will reduce the power to maintain the desired crawl.  Also if I push against BOB's motion with my hand, the drive signal ramps up to overcome the force I'm applying.  Real closed loop control!!!

     

     

    Web content is copyright © PhilBot.com 2005, Deep Creek Lake, MD.
    Contact: Phil Malone 301.387.2331, webmaster@PhilBot.com