CANTalon PIDF close loop error is offset by a large amount

Hey everyone!

My team is trying to tune our robot’s shooter velocity with TalonSRX’s built-in PIDF closed loop. However, when running the shooter motors the closed loop error is significantly below its expected value. This makes the motor controller think that the shooter motor is always below the target speed, even when that isn’t the case.

We’re using Talon SRX’s with a Versaplanetary encoder stage from Vexpro. They are running two 775 motors in parallel at the same time. We’re using the two motors in a master-follower setup.

Here are two graphs we recorded on SmartDashboard during testing. The graph on the left is the recorded motor speed while the graph on the right is the closed loop error calculated by the Talon motor controller.

https://lh5.googleusercontent.com/xZaD4VHK3keTuRYzAjrjbjWslfvoBlhAnmxgq2KEhgdk2f2ui_d8-SV0nMkgZ7R7LM71d5eM=w1366-h700-rw

The recorded speed values are accurate, and every number input in the picture is used in our closed loop.

We’ve tried many different approaches to fixing the problem, including reversing the motor sensors, updating the Java SRX libraries and the firmware on each Talon (both currently at the latest firmware version), and have searched through the Talon SRX software reference manual

Here are screenshots from the Rio Motor Controller Web Server:

Master Motor:
https://lh6.googleusercontent.com/uy-rMeY9qRs6Z5pV0puwqWzZ6UYZ6YF5OoctK5aO-m2Ggs9ptMQWInZHJzJTpUzQlWd5nkg7=w1366-h700-rw

Follower/Slave Motor:
https://lh4.googleusercontent.com/Q0eUU33YNT0ZLBilUWbEZCELHt-3mnUnCz4CI9CfWHy7x76Xv1aM7EVZ_lEMTeiYj-DnaVvI=w1366-h700-rw

(Note, These motors were updated with a Closed Loop P value of 0.1, not 0.6 as seen in the graphs.)

Here is the Java source code we used for our tests:




public class Robot extends IterativeRobot {

    CANTalon shooterMotorA, shooterMotorB;

    @Override
    public void robotInit() {
        shooterMotorA = new CANTalon(0);
        shooterMotorB = new CANTalon(5);

        shooterMotorA.enableBrakeMode(false);
        shooterMotorB.enableBrakeMode(false);

        shooterMotorA.setInverted(true);
        shooterMotorB.setInverted(true);

        shooterMotorA.setFeedbackDevice(FeedbackDevice.CtreMagEncoder_Relative);
        shooterMotorB.setFeedbackDevice(FeedbackDevice.CtreMagEncoder_Relative);

        shooterMotorA.changeControlMode(TalonControlMode.Speed);
        shooterMotorB.changeControlMode(TalonControlMode.Follower);

        shooterMotorB.set(shooterMotorA.getDeviceID());

        shooterMotorA.configEncoderCodesPerRev(1024);

        shooterMotorA.reverseSensor(true);

        SmartDashboard.putNumber("Shooter Target Speed", 0.0);

        SmartDashboard.putNumber("P ShooterAccelerate", 0.0);
        SmartDashboard.putNumber("I ShooterAccelerate", 0.0);
        SmartDashboard.putNumber("D ShooterAccelerate", 0.0);
        SmartDashboard.putNumber("F ShooterAccelerate", 0.0);
    }


    @Override
    public void teleopPeriodic() {
        shooterMotorA.setPID(
                SmartDashboard.getNumber("P ShooterAccelerate", 0.0),
                SmartDashboard.getNumber("I ShooterAccelerate", 0.0),
                SmartDashboard.getNumber("D ShooterAccelerate", 0.0)
                );
        shooterMotorA.setF(SmartDashboard.getNumber("F ShooterAccelerate", 0.0));

        shooterMotorA.set(SmartDashboard.getNumber("Shooter Target Speed", 0.0));

        // Logs and Feedback
        SmartDashboard.putNumber("PID ShooterAccelerate OUTPUT", shooterMotorA.getEncVelocity());
        SmartDashboard.putNumber("PID ShooterAccelerate ERROR", shooterMotorA.getClosedLoopError());
        System.out.println("spd: " + shooterMotorA.getEncVelocity() + ", err: " + shooterMotorA.getClosedLoopError());
    }

}

If the screenshots are not accessible because Google drive could not locate the image Here are the direct sharing links to access the images (just in case):

Graph

Master Motor

Follower/Slave Motor

getEncVelocity() returns the “raw” encoder output, without inversion or unit scaling. Can you retry and post the graphs with getSpeed() instead?

At the great risk of posting a link that goes over people’s heads, the link below is an online video from a course on control systems that talks about steady state errors where the system settles down to a value that’s way off the reference output (i.e., the desired output). The link is for section 11.1 of the course. Two videos later the instructor is showing how a carefully tuned PID controller (actually a PI controller) can reduce the steady state error to 0.

Section 11.1 of Control System course

Control systems is not an easy topic. This is really for the highly motivated student, generally well beyond high school. The course pretty much assumes knowledge of calculus and by Section 11.1 also assumes Laplace Transforms. A student that is in calculus may be able to track with much of the lecture series to get the gist, but will almost certainly have to backtrack through earlier lectures. On the plus side, a walk away is that the integral and derivative constant values can help drive down the error that exists in a system using only the proportional constant. Experimentation can often get you favorable results without having to know the somewhat advanced math.

Control systems have applicability across the engineering spectrum, including mechanical and aerospace engineering, electrical engineering and industrial control. Many control system courses sprinkle in examples of both mechanical and electrical control systems (as this one does), because the math is identical.

Thanks for the reply!

I’m 90% sure that we tried using getSpeed() and got the exact same results. It definitely didn’t fix the problem. Either way I’ll try to get those graphs up as soon as I get back to testing.

As Noah alluded to, what is happening here is that you are giving it a setpoint of 30,000 RPM (units are RPM as you have called configEncoderCodesPerRev()), which is thoroughly unachievable. You are looking at speeds and errors in native units, and both are correct; your setpoint looks to be a bit over 200,000 native units.

I strongly recommend not leveraging the internal unit scaling with configEncoderCodesPerRev() as it results in, as you have observed here, some methods still returning in native units, thus resulting in a mixed-unit object that is very hard to make sense of. If you want unit handling, write your own wrapper on the CANTalon class and do it yourself.

Thanks for the feedback! I have tried to remove scaling by not using configEncoderCodesPerRev() and it didn’t fix the problem, but I haven’t assessed the effect it has on the closed loop error. I’ll try to get back to you soon on that!

I may be missing something here, but I believe that the units for the closed loop error are consistent with the units used for getting the shooter RPM. If you look at the rpm graph on the left, it shows the shooter speed changing from 0 to approximately 40,000 rpm. On the closed loop error graph, the error goes from approximately -205,000 to -165,000, which is a change of 40,000. If the error units were scaled from the motor RPM, I would imagine these values wouldn’t have the same change and would be proportional.

Also here’s another piece of information that I did not mention before:

In the error graph, there is a huge gap between the displayed error value and what the actual error value should be. Since the error doesn’t appear to be scaled (it changes at the same rate that the shooter rpm changes), there instead appears to be an offset applied to the error. (on the graph that offset is roughly -175,000)

While testing, we discovered that this offset seems to change depending on the target speed (I’ll post more graph logs to verify that). The higher the target speed, the bigger the offset.

Both values have the same change because both values are in native units, not RPM. The Talon’s getClosedLoopError method always returns native units, regardless of whether unit scaling is leveraged or not - as I said before, this is why you shouldn’t use their unit scaling.

The fact that it’s reaching 40,000 should be a dead giveaway - the fastest FRC motors have a free speed on the order of 20,000 RPM, so unless you’re actually gearing the thing up there’s literally no way you’re getting anywhere close to that (and if you did gear the thing up to spin at 40,000 RPM, it would take a very long time to reach speed).

In the error graph, there is a huge gap between the displayed error value and what the actual error value should be. Since the error doesn’t appear to be scaled (it changes at the same rate that the shooter rpm changes), there instead appears to be an offset applied to the error. (on the graph that offset is roughly -175,000)

While testing, we discovered that this offset seems to change depending on the target speed (I’ll post more graph logs to verify that). The higher the target speed, the bigger the offset.

If you think about it, you’ll see why this is precisely consistent with the diagnosis I’ve given you.

I understand (and completely agree with) the concerns about unit scaling, but in any case - shouldn’t a loop set to a setpoint of 30,000 have an error of 0 when it reaches 30,000, regardless of what units they are in? I know this loop quickly overshoots its target, but the error should at least be zero in that moment, right?

The setpoint is in RPM, the graphs are in native units. The setpoint in native units is just over 200,000 - corresponding to the “offset” seen.