Improved lift control software

I’ve been working on software for improved lift control (I’m targeting linear lifts but this could also apply to 4-bar and 6-bar arms etc.). When I say improved that of course is subjective, it feels improved to me but you could also argue that it’s really just more (way more) complicated and isn’t worth it.

So we’ve all seen the most basic way of controlling lift motors, in ROBOTC it might look something like this.

    while(1)
        {
        if( abs(vexRT Ch2 ]) > 10 )
            {
            motor liftMotor_1 ] = vexRT Ch2 ];
            motor liftMotor_2 ] = vexRT Ch2 ];
            }
        else
            {
            motor liftMotor_1 ] = 0;
            motor liftMotor_2 ] = 0;
            }
        
        wait1Msec(25);
        }

Joystick channel 2 directly controls the motors, we have a small deadband so the motors will stop of the joystick does not return exactly to the zero position.

There are a number of problems with this for all but the very simplest of robots.

  1. The lift does not hold it’s current position when the joystick returns to zero. The operator has to keep applying some power which can be difficult and will trip the motor PTC protection if too much is applied.

  2. It’s hard to move the lift to an exact and repeatable position.

  3. The lift can move too high or too low and damage itself.

In order to improve on this, many teams implement the “infamous” PID control, although in reality what is used is a simple P controller. There’s a post about that type of control here.
Simple P controller for arm position

A generalized diagram showing this type of PID control would look something like this.

We give the control function a target position, the PID controller software compares this position to the existing position of the lift (usually the count from an IME or quadrature encoder) and moves the lift up or down as necessary. Now we have the solution to some of the limitations of the manual controller, we can have preset positions, we can have the lift remain at the target position, if the lift starts to drop the PID controller will increase motor power to compensate. We can add code to limit the movement of the lift to the safe limits, however, the PID controller above also has some drawbacks.

  1. Manual movement of the lift has now become difficult, the joystick cannot directly control the motor power and must gradually change the target position of the lift instead. If the rate of change of the target position does not match the dynamic response of the lift it will not move smoothly. (what I mean here is that the lift will accelerate and decelerate based on its physical characteristics, how heavy etc. If the lift can move quicker than you are changing the target position then it will sort of judder as you move it).

  2. Gravity can be a problem, tuning the PID controller can be more successful when the lift is moving up (or down) but then not work in the other direction.

  3. The PID controller can still send too much power to the motors when the lift is not moving.

There’s also an issue with this type of PID control due to the fact that the motors do not respond to control values in a linear fashion. Sending a control value of 64 to a motor does not make it run at half speed (as compared to a control value of 127 causing maximum speed). We have tried in the past to linearize the motor control values by remapping the output of the PID controller to an actual motor control command using a lookup table (LUT), but this only seems to work accurately with a given load on the motor and needs to be setup for a given design.

So what can we do to improve lift control and have all the functionality we would like. My proposed solution separates motor speed control from motor position control. The block diagram looks like this (same as I posted the other day).

Starting on the left, we first have the position PID controller. The input to this is “target position”, this is almost the same code the the PID control I described before, however, instead of the output driving the motor directly the output is now a target speed for the motor. This block allows for presets but still has the issues related to manual control of the motor.

In the next block we modify the target speed in a couple of ways.

  1. We limit the speed to a user determined maximum speed. By doing this we can acheive the manual control we desire, the joystick maps to maximum speed and the target is set as either the upper or lower limit of the lift. The position PID control will try and move the lift to that position but the speed is limited.

  2. We control acceleration and deceleration, what I have termed “slew rate” control in the past. We don’t want to try and instruct the motors to go from stop to full speed instantly as they cannot do this and the motor current will be high as they accelerate if you try. So motor speed is ramped over a few hundred mS to reduce absolute motor current during this period.

  3. We can reduce maximum speed as we near the absolute upper and lower limits of the lift, even when tuned properly a position PID controller can overshoot the target, if we limit the output then we can smoothly stop at the top and bottom whilst still having full speed for intermediate preset position.

The conditioned motor speed now goes into the motor speed PID controller. This controller creates a motor drive signal by comparing the actual motor speed to the requested target speed. If the target speed is 10 revs per minute (rpm) then this controller will try and maintain that speed. This removes the non linear motor response, the motor control values will be unique for that motor with the current load and battery condition, gravity will be compensated for. The key parameter needed for this to work properly is the actual motor speed, this is where things start to get tricky.

motor speed calculation.

We only have one piece of information coming from the motor, the current encoder count (I’m assuming an IME for this discussion and ignoring the fact that in some programming environments the IME can indicate also give velocity). The only way to calculate speed is to use the change in encoder count over time (we know v = dx/dt).

A 393 motor with standard gearing will give 627.2 counts per revolution (I’m going to use 627 for the calculations). This sounds really high, however, lets suppose we want the motor to be turning at 10 revs per minute, that would be 627 x 10 counts per minute or 6270 counts. In one second this will be 6270/60 or 104.5. We could calculate the motor speed every second by reversing this calculation speed = (104 * 60) / 627; speed would be 9.95 rpm.

For the motor to be successfully controlled we need to know the speed much more often, I’m running the speed control PID loop every 25mS (40 times per second), so the number of counts when the motor is running at 10rpm is 6270/(6040) = 2.6125. The encoder does not give us an encoder count as a decimal, the change in encoder count every 25mS can either be 2 or 3, it will average out to 2.6125 over a long period of time but the instantaneous change can only be 2 or 3. If the change is 2 then speed will be calculated as (24060)/627 = 7.65, if the change is 3 then the speed will be calculates to be (340*60)/627 = 11.48, start to see the problem.

If we plot calculated speed vs time with the motor running at exactly 10rpm the graph would be as follows. The green line is the actual speed, 10 rpm, the red line is the speed calculated from change in encoder position, ignore the blue line for now.

[ATTACH]8484[/ATTACH]

The error in speed calculation causes the PID controller to fight the motor when it doesn’t need to. As the speed decreases this situation gets worse, at 1 rpm we only see a change in encoder count every 100mS, above 10rpm things improve but even at full speed the calculated speed will fluctuate. So somehow we need to improve this and really the only tool available to us is to filter this signal somehow and remove the spikes. I decided to use a simple 4 sample low pass filter of the encoder count change (sum the last four delta values and then divide by 4) rather than filter the calculated speed as this gave a better result. This improvement is plotted as the blue line on the above graph. The calculated speed swings between 9.57 and 10.52 (which when I round to be an integer for subsequent calculations becomes 10 and 11), a 1 rpm error rather than a 4 rpm error should be ok.

Using velocity information from the IME can be a bit more accurate but there are still problems. The IME velocity data is a time value, 1/speed, that measures the time for half a revolution of the encoder gear in the 393 (according to the tech support FAQ) this works well for high speed but has similar issues at slow speed. Once the motor is stopped it takes several seconds to figure this out as the time value will keep increasing until it reaches its maximum value. ConVEX only uses this velocity data if the encoder count has changed, so at slow speeds <4 rpm, we have the same problem, an encoder count delta of zero may or may not mean the motor is stopped.


continued in part 2, I hit the 10000 forum character limit again :frowning:

2 Likes

part 2.

It’s important that the target speed sent into the speed PID loop is one that the lift can achieve, there’s no point telling the lift motor to run at 150rpm if the maximum is 80rpm. I measured the fastest free speed of the lift motor and then set the limit for the target speed a little lower than this (which was around 80rpm in my case).

The speed PID also has some motor control value limiting, unlike the positional PID control I don’t want any undershoot in the control values that would technically make the motor turn in the opposite direction. If we are moving forwards and find we need to slow down quickly there can be a tendency to create values below 0, these values are hard clipped. It also understands about “holding power” that is the minimum power required to overcome gravity, it’s a bit like using an offset into the motor control output but is only used when the lift is going upwards.

The final block in the lift controller implements some safety features, if we exceed the upper limit of the lift (which is above the highest normal preset position) motor power is cut. If the lower limit switch is tripped, then motor power is cut and the IME reset to 0. I don’t set a lower limit on the encoder count and use the limit switch exclusively, this allows the lift to learn where that limit is when first powered and also if the IME resets due to static.

So all of this works quite well, I have fairly accurate manual control of the lift including the ability to run it at a few mm per second. Presets work as expected, the only small issue left is created by the sprockets I’m using to drive the chain making the lift go up and down. As the sprockets rotate there seems to be a varying force that causes the lift to slightly change its speed with a one tooth (about 10mm) frequency. It’s a bit like driving on wheels that are not quite round, you would find it hard to have a constant speed. It’s not a big deal but stops the lift movement from being as smooth as I would like at really slow speeds.

Now the bad news, I’m not posting the code for this at present, I have a couple of reason for not doing this, one is that everything is written for ConVEX which is not much use for most of you. It also needs cleaning up a little, there’s also another reason that will be clearer in the next few days.

1 Like

Really interesting stuff here. Can’t wait to see the source code. How did you go about tuning the two PID loops? Seems like it would be difficult to determine which loop was causing instability.

I’m not planning to release source code until later this year (if ever). The code is very specific to the lift in this thread, it’s hard to make generic, anyway it’s the concept that matters not the code.

Tuning any PID controller is tricky. I tuned the speed control first to make it work well over a range of constant speeds. The position loop was tuned afterwards, this is actually a PD controller (well both position and speed are at present). Using integral on the position didn’t make much sense, I don’t want the lift to overshoot or try and correct for steady state errors. The lifts needs to grab the skyrise section (or cube, or both together :slight_smile: but that’s another story) raise perhaps 1 inch above the needed height, move to the sky rise base and drop the new section into position by lowering 4 inches.

The lift currently moves to the correct position within 10 counts with no overshoot (that’s theoretically about 4mm in height), plenty good enough.

1 Like

Yep, I understand. I was just curious to see your implementation of some of the more complicated concepts here like the acceleration slewing. Thanks for all of the information here. With the huge need for lift precision this season I’m definitely considering this type of PID control.

The slew rate control is well understood and used in code posted before (smartMotor library etc. make sure you look at the latest on github). It’s not that I want to keep the code to myself, it’s just that it’s not written to my usual standards at this point and that I have other reasons to keep it private for a couple of months.

1 Like

Of course. I’m fairly new to VEX and haven’t been exposed to a lot of the code considered standard among the top VEX teams. Thanks for mentioning smartMotor. It looks very useful at first glance.

Hi, really interesting post!

Can you comment on what the controls look like from the operator point of view? Does the position of the joystick map to the height of the lift or to its velocity? Since the left-most input to the PID controller is “Target Position”, I think it maps to the height, but then it would contradict that “the joystick maps to maximum speed”.

Does the neutral position of the joystick correspond to the middle height of the lift?

Also, I assume presets would be controlled by buttons. In that case, does the button press override the joystick setting?

I use button 8 Up and 8 down to control presets, pushing 8U moves to the next position up, 8D to the next position down. If you push a button multiple times, say three times, then you would move to that corresponding preset.

The analog joystick channel 2 manually moves the lift up and down, move the joystick half way forward and the lift moves up at half speed (well, not half, there’s a non linear map used on the joystick to give better low speed control). When the joystick is used the target is set to the maximum upper (or lower) lift position and the joystick value controls the maximum lift speed.

Two other buttons allow slow speed lift control at 20rpm. The slow down also allows the lift to reset the encoder to 0 using the limit switch by sending it to a position 25 inches below the normal low limit.

Well, here is the operator part of the code, I’m ok posting that today, it may not mean much out of context.

        // Next, prev or release preset button
        if( vexControllerGet(Btn8U) )
            preset_control = 1;
        else
        if( vexControllerGet(Btn8D) )
            preset_control = -1;
        else
            preset_control = 0;

        // Next preset
        if( ( preset_control > 0 ) && (!preset_action) )
            {
            // detect button push on first loop
            preset_action = 1;
            // next preset
            if( PresetGetNextPosition( &l.vp, l.p_target, 1 ) != (-1) )
                 l.p_target = PresetCurrentPosition( &l.vp );

            l.v_max = LIFT_MAX_RPM;
            }
        else
        // Previous preset
        if( ( preset_control < 0 ) && (!preset_action) )
            {
            // detect button push on first loop
            preset_action = 1;
            // previous preset
            if( PresetGetNextPosition( &l.vp, l.p_target, 0 ) != (-1) )
                 l.p_target = PresetCurrentPosition( &l.vp );

            l.v_max = LIFT_MAX_RPM;
            }
        else
            {
            // wait for button release
            if( preset_control == 0 )
                {
                // use manual
                preset_action = 0;

                // manual control
                // Slow down
                if( vexControllerGet( Btn6D ) )
                    {
                    // Recal
                    l.v_max = 20;
                    l.p_target = -2000;
                    }
                else
                // Slow Up
                if( vexControllerGet( Btn6U ) )
                    {
                    l.v_max = 20;
                    l.p_target = LIFT_UPPER_LIMIT;
                    }
                else
                // Joystick control
                    {
                    speed = vexControllerGet( Ch2 );
                    if( abs(speed) > 10 )
                        {
                        if( speed >= 0 )
                            l.p_target = LIFT_UPPER_LIMIT;
                        else
                            l.p_target = LIFT_LOWER_LIMIT;

                        // Use non-linear joystick to speed control
                        speed = (speed*speed)/128;
                        l.v_max = abs(speed);
                        }
                    else
                        {
                        // Not good, need to redo this !
                        if(l.v_max != LIFT_MAX_RPM)
                            {
                            l.v_max = 0;
                            l.p_target = l.p_current;
                            }
                        }
                    }
                }
            }
1 Like

Interesting concepts. Thanks for sharing this. :slight_smile:

Thanks, jpearson, for the detailed explanation. I really appreciate it.

The scheme for initializing the zero position using Btn6D is pretty clever – the operator doesn’t have to ensure that the lift is in a particular position when the robot is turned on.

I think I understand your overall control scheme but have a few follow up questions and a suggestion.

Can you also explain the [FONT=“Courier New”]l[/FONT] structure? [FONT=“Courier New”]v_max[/FONT] and [FONT=“Courier New”]p_target[/FONT] are pretty obvious, but I couldn’t figure out what the [FONT=“Courier New”]vp[/FONT] field is and why it is passed by reference to the [FONT=“Courier New”]PresetGetNextPosition[/FONT] function.

The last bit of code – where the comment says that you would like to redo it, is it for the case where previously the joystick position of > 10 disabled a preset and set a velocity and now when the joystick drops to near-zero so you’d like to maintain that position? One suggestion would be to implement a finite state machine (with states such as POSITION_MODE and VELOCITY_MODE) which would obviate the need for using [FONT=“Courier New”]preset_action[/FONT] and [FONT=“Courier New”]l.v_max[/FONT] to determine current control state.

Final code would make this automatic,

So I only released a code snippet to give you some idea of how I was controlling the lift. The “l” structure collects all the lift related variables together, which motors are used, the PID constants, working variables etc. “vp” is part of the structure, it itself is a structure holding the preset control related variables, so in the lift structure it is declared as.

	vexPreset		vp;

vexPreset is a small structure

// 10 presets should be enough
#define kMaxPresets                 10

typedef struct {
    short           preset_num;
    short           preset_ptr;
    int             presets[kMaxPresets];
    short           tolerance;
    } vexPreset;

There are functions in the preset library code that allow preset initialization and determining a new target position based on a current position (or current target position). So to initialize my presets I use code that looks like this.


    PresetInit( &l.vp );
    PresetAdd( &l.vp, 35 );
    PresetAdd( &l.vp, LIFT_COUNTS_PER_INCH * 2 );
    PresetAdd( &l.vp, LIFT_COUNTS_PER_INCH * 6 );
    PresetAdd( &l.vp, LIFT_COUNTS_PER_INCH * 10 );
    PresetAdd( &l.vp, LIFT_UPPER_LIMIT );

Most of the functions take a pointer to the preset structure, that’s why I pass a reference to PresetGetNextPosition.

(edit: The preset library was actually included with the “open source robot” code and released about a year ago on github.
https://github.com/jpearman/OSR_1/tree/master/Libraries
PstLib.c. For these tests I used a newer version ported over to ConVEX )

Yes

Understand that this code for me is just a hack, it’s not production code, something I just threw together to test the ideas for control as I’m building the lift, that’s one of the reasons I don’t want to do a full release right now. I wanted the manual control to always override the lift moving to a preset, that code where the joystick is near zero just allows presets to run, yes I could use another flag, state machine etc. I just didn’t bother with those details as I was testing, it would all be cleaned up during refactoring.

1 Like

Thanks again for the detailed explanation! The Presets library is really cool – you can set arbitrary values (up to a limit) and find the next or previous based on current target or current position. Really well thought out.

Is ConVEX legal for VEX competitions (e.g. for SkyRise)?

Yes (10 char)

Thanks for sharing this, I have a question regarding reading the rpm and slew rate.

I read part of your smart motor library, and I thought I could use your rpm calculation that’s used in the *SmartMotortask * and read the rpm every 25ms as you mentioned , but I noticed the rpm is for a 100ms read because of the first lines of the task

task
SmartMotorTask()
{
    static  int loopDelay = 10;
    static  int delayTimeMs = kNumbOfRealMotors * loopDelay;
    ......
}

Can I change the time to read the rpm in the library like this:

static int delayTimeMs = 25;

Or how can I achieve it, I really don’t understand why you calculate delayTimeMs and use it as a parameter later in the task in SmartMotorSpeed function, instead of using the loopDelay variable which is the rate that every run of the while loop occurs.


SmartMotorSpeed( m, delayTimeMs );

And my other question is, does the slew rate have to be applied to the first PID that calculates the target speed upon desired position and why? (maybe I 'm missing something) or can it be run in the background like in the Smart Motor library which runs every 15ms, and be just used only to control the final acceleration/decceleration that occurs on the second PID that controls the pwm to set the desired RPM.

1 Like

The smartmotor library services one motor every 10mS, so it takes 100mS to do all 10, it calculates rpm for one motor each time around the loop. I calculate delayTimeMs for each motor using the following code.

// may be overkill, could just use default from above
        delayTimeMs = nPgmTime - m->lastPgmTime;
        m->lastPgmTime = nPgmTime;
        m->delayTimeMs = delayTimeMs; // debug

I do this in case there are higher priority tasks running that take time away from the smartMotor tasks. delayTimeMs will be 100mS most of the time but can be higher.

The reason for only updating one motor each time around the loop is to reduce the load on the cortex as much as possible and also because calculation of the PTC temperature didn’t need to happen that often. If you want to use the part of the smartmotor library that calculates velocity then just pull that function out and call from a task running every 25mS (or whatever your chosen rate is). You have much more control over task priority and making sure the velocity calculation task runs every 25mS than I did with a generic library that other people are using.

It may work that way, I didn’t try.

I’m actually working on another version of this that does the control in a more conventional way, that is, a final PID loop designed to removed small errors preceded by a motion control generator that creates trapezoidal and s-curve motion profiles. No idea if it will work any better with the VEX system that the code I wrote for this experiment. I will post code at a later date but I’m waiting until after my programming contest ends in case someone is working on something similar.

1 Like

Do you think there’d be any problems If I create a particular task for reading the rpm of a lift more often and also use the smartmotor library in the background to limit the current of all my motors?


I'm actually working on another version of this that does the control in a more conventional way, that is, a final PID loop designed to removed small errors preceded by a motion control generator that creates trapezoidal and s-curve motion profiles. No idea if it will work any better with the VEX system that the code I wrote for this experiment. I will post code at a later date but I'm waiting until after my programming contest ends in case someone is working on something similar.

What do you mean with the motion control generator to create trapezoidal and s-curve motion profiles and what’s the purpose of it? Are there any links you can share so I can understand more this concepts and have a better overview of it.

You can calculate rpm for the lift independently from the smart motor library, read the encoder, use the same math etc. You can also leave the smart motor library running if you want to, perhaps reduce the slew rate for the motors on the lift.

This is an alternative to the slew rate used in the smart motor library, control acceleration and deceleration when performing positional moves. Probably more relevant to drive system, that’s what I’m thinking about.

Some background.

Mathematics of motion control profiles

S-curve profile deep dive

servo tuning deep dive

1 Like

These topics hint at the line in Wikipedia: "In the absence of knowledge of the underlying process, a PID controller has historically been considered to be the best controller.[2] "

“Trial and error using a motion measurement system is generally the best way to determine the right amount of “S”, because modelling the response to vibrations is complicated, and not always accurate.”

Since you designed the robot, you know the underlying process, so you can theoretically model the whole thing, if you can deal with the complications like vibrational response. Gravity compensation factor is a step toward the full model.

1 Like

Hi jpearman,

my students and I have been struggling with building lift arms that stay up by gearing. I would like to try the code for controlling the lift arm with a pot but can’t key it into easyc. never tried to type into easyc before is there a trick to doing it. I downloaded the zip file and opened it but easyc gives an error when it tries to open it.

any direction you can give will be helpful. I have pictures of the arm we built if you would like to see it.:slight_smile: