Closed-Loop Control

Now that we've covered Open-Loop control, we can move on to Closed-Loop control. Closed-Loop control is on a basic level pretty simple to understand, but it can get complicated (and interesting) pretty fast. The main idea behind Closed-Loop control is that rather than output being based on purely the input, the output is based on the input, as well as the measured distance between the output and input. This allows the system to correct for errors in the system, and adapt to changing conditions.

Proportional Control

Proportional control is the simplest form of Closed-Loop control, and is the most common form of Closed-Loop control used in FRC. Proportional control simply takes the distance (or error) between the desired value and the actual value, and multiplies it by a constant, called the Proportional Gain or K_p. This value is then output to the motor.

Alternatively, rather than simply outputting that value, you can add it to the output of an Open-Loop control system, such as Feedforward control. This is incredibly useful as pure Proportional control is good at error correction, but it can be difficult to find a K_p that both quickly reaches the target, but doesn't overshoot it. By using Feedforward control to get close to the target, and Proportional control to correct for errors, you can get the best of both worlds.

Implementing Proportional Control

Lucky for us, WPILib has a built-in class that can be used to implement Proportional control, called PIDController. This class can be used to implement other forms of Closed-Loop control as well, but we'll get to that later.

val K_p = 0.1
val motor = CANSparkMax(1, MotorType.kBrushless)

val controller: PIDController =
    PIDController(K_p, 0.0, 0.0) // Create a new PIDController with a K_p of 0.1, for now ignore the other values
controller.setSetpoint(100.0) // Set the desired position of the motor to 100 rotations

// Placeholder function for something getting called periodically
fun periodic() {
    val output =
        controller.calculate(motor.encoder.position) // Calculate the output of the PIDController based on the current position of the motor, and the desired position
    motor.setVoltage(output) // Set the motor to the output of the PIDController
}

As you can see, implementing Proportional control is pretty simple. The PIDController class takes care of all the math for you, and all you need to do is call the calculate() function with the current position of the motor, and the desired position. The setVoltage() function is used to set the motor to the output of the PIDController.

Implementing Feedforward and Proportional Control

Implementing Feedforward and Proportional control is also pretty simple. All you need to do is add the output of the PIDController to the output of the SimpleMotorFeedforward class.

val K_p = 0.1
val motor = CANSparkMax(1, MotorType.kBrushless)

val feedforward = SimpleMotorFeedforward(0.1, 0.2, 0.05) // K_v, K_a, K_s
val controller: PIDController =
    PIDController(K_p, 0.0, 0.0) // Create a new PIDController with a K_p of 0.1, for now ignore the other values
controller.setSetpoint(100.0) // Set the desired velocity of the motor to 100 rotations per minute

// Placeholder function for something getting called periodically
fun periodic() {
    val output =
        feedforward.calculate(controller.setpoint) + controller.calculate(motor.encoder.position) // Calculate the output of the PIDController based on the current position of the motor, and the desired position
    motor.setVoltage(output) // Set the motor to the output of the PIDController
}

As you can see, implementing Feedforward and Proportional control is pretty simple. All you need to do is add the output of the PIDController to the output of the SimpleMotorFeedforward class, and set the motor to the output of that calculation.

Implementing Proportional Control and Feedforward Control on a SparkMax

SparkMaxs have a built-in PID controller that can be used to implement Proportional control. This is a lot simpler than using the PIDController class, as the SparkMax takes care of all the math for you.

val K_p = 0.1
val motor = CANSparkMax(1, MotorType.kBrushless)

val controller = motor.pidController // Get the built-in PID controller of the SparkMax

controller.p = K_p // Set the Proportional gain of the PID controller to 0.1
controller.setReference(100.0, ControlType.kPosition) // Set the desired position of the motor to 100 rotations

As you can see, implementing Proportional control on a SparkMax is pretty simple. All you need to do is set the Proportional gain of the built-in PID controller, and set the desired position of the motor. Unlike with the PIDController class, you only need to set the setpoint once, and the SparkMax will take care of the rest.

Feedforward control can be added to the setReference() function by adding some extra parameters.

val K_p = 0.1
val motor = CANSparkMax(1, MotorType.kBrushless)

val feedforward = SimpleMotorFeedforward(0.1, 0.2, 0.05) // K_v, K_a, K_s
val controller = motor.pidController // Get the built-in PID controller of the SparkMax

controller.p = K_p // Set the Proportional gain of the PID controller to 0.1

controller.setReference(
    100.0,
    ControlType.kVelocity,
    0, // Slot 0 (ignore this, just needed to add feedforward)
    feedforward.calculate(
        100.0,
        0.0
    ), // Calculate the feedforward output based on the desired velocity and acceleration
    ArbFFUnits.kVoltage // Set the units of the arbitrary feedforward to volts
)

As you can see, adding Feedforward control to the SparkMax is pretty simple. All you need to do is calculate the output of the SimpleMotorFeedforward class, and add it to the setReference() function. You also need to set the units of the arbitrary feedforward to volts, as the SparkMax expects the output of the feedforward to be in volts.

Unfortunately, the SparkMax's feedforward capabilities are limited, and require you to calculate the feedforward output on the RoboRIO, which means that we're limited in how fast we can run the feedforward loop.

What unit is K_p in?

The unit of K_p is similar to the units of the feedforward gains, where it is based on the units of the input and output. For example, if the input is in meters, and the output is in volts, then K_p would be in volts per meter. But rather than being volts per meter of the target, it is volts per meter of error (or distance from the target).

Conclusion

In this chapter, we covered the concept of Closed-Loop control, and how it can be used to correct for errors in the system. We specifically covered Proportional control, and how it can be used to correct for errors in the system. We also covered how to implement Proportional control using the PIDController class, and how to implement Proportional control on a SparkMax. We also covered how to implement Feedforward and Proportional control, and how to implement Feedforward and Proportional control on a SparkMax.

Additional Resources

The information below covers PID control, which is a more advanced form of Closed-Loop control. It is not necessary for basic parts of our robot, but if you want to be more involved in the complex and interesting parts of our robot, it is definitely worth looking into.

  • PID Control WPILib Docs - Comprehensive introduction to PID control concepts, including detailed explanations of proportional, integral, and derivative terms with practical examples of their effects on system behavior.
  • Combining Feedforward and PID Control - Explains how to effectively combine feedforward and PID control for better system performance, with examples showing how each component contributes to the final control output.
  • MatLab PID Video - Visual introduction to PID control using animated examples and real-world applications, making complex control concepts more approachable and intuitive.
  • WPILib's Flywheel Tuning Guide - Provides interactive simulations demonstrating different control strategies (bang-bang, feedforward, PID, and combined feedforward-PID) for tuning a flywheel velocity controller. The visual simulations make it easy to understand how different control parameters affect system behavior.