Here you'll find some tutorials to write drive code for the ArgoBot (or any robot) using LabVIEW or C++
This project is maintained by FRC1756-Argos
Debounce is a great way to prevent unwanted input change in digital inputs, but what about joysticks and other analog inputs? If we want to control how quickly an analog input can change values, we can use an approach called speed ramping.
Analog inputs, unlike digital inputs, can have a range of values. Sometimes analog inputs change rapidly, but these rapidly changing values aren’t as useful as more steady values. For example, if you go from full forward to full reverse on the robot very quickly, the quick change in direction can stretch chain and put additional wear on drivetrain components.
Consider the following:
Notice how the raw input has a fast change from positive to negative, but the output smoothly transitions. This linear ramping is a simple way to prevent “jerkiness” on the output.
To perform this ramping, we will utilize similar concepts to debounce where the program will compare the desired output to the previous output and ensure the difference between the previous output and the new output is below some threshold value. This is effectively finding the slope the line in the diagram above and enforcing a maximum slope.
This will be similar to debounce, but now we’ll use time to control our ramp speed. We’re also going to need to maintain state for each ramping instance. To do this, we will be making an object like in debounce. We’ll get started and go through each of these concepts along the way.
DriveSubsystem.h
and add a class declaration for SpeedRamp
in the private
section:
class SpeedRamp {
public:
private:
};
class SpeedRamp {
public:
private:
const double m_pctPerSecond;
double m_lastOutput;
std::chrono::time_point<std::chrono::steady_clock> m_lastUpdateTime;
};
m_pctPerSecond
is how fast our ramping allows values to change in percent per second.m_lastOutput
is the previous output value. We’ll use this to ensure values don’t change too quickly.m_lastUpdateTime
is the time we last updated the output. We’ll use this to calculate the maximum change between samples class SpeedRamp {
public:
SpeedRamp(double pctPerSecond);
private:
const double m_pctPerSecond;
double m_lastOutput;
std::chrono::time_point<std::chrono::steady_clock> m_lastUpdateTime;
};
Our constructor takes one parameter pctPerSecond
since the other two values are only used internally.
class SpeedRamp {
public:
SpeedRamp(double pctPerSecond);
double operator()(const double newSample);
private:
const double m_pctPerSecond;
double m_lastOutput;
std::chrono::time_point<std::chrono::steady_clock> m_lastUpdateTime;
};
operator()
is a special function that lets us use parentheses like our class instance (an object) can be called like a function. We’ll see this in action soon.
std::chrono::time_point<std::chrono::steady_clock>
is part of the C++ time library that lets us calculate elapsed times easily.
chrono
library to our include list at the top of DriveSubsystem.h
#include <chrono>
DriveSubsystem.cpp
DriveSubsystem::SpeedRamp::SpeedRamp(double pctPerSecond)
: m_pctPerSecond{pctPerSecond}
, m_lastOutput{0}
, m_lastUpdateTime{std::chrono::steady_clock::now()} {}
This is very similar to other constructors. The one new piece is how we initialize m_lastUpdateTime. std::chrono::steady_clock::now()
reads the current time so we can do time differences later.
operator()
definition
double DriveSubsystem::SpeedRamp::operator()(const double newSample) {
}
double DriveSubsystem::SpeedRamp::operator()(const double newSample) {
auto now = std::chrono::steady_clock::now();
}
double DriveSubsystem::SpeedRamp::operator()(const double newSample) {
auto now = std::chrono::steady_clock::now();
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>((now - m_lastUpdateTime)).count();
auto seconds = milliseconds / 1000.0;
}
This is a little complicated, but let’s break it down.
(now - m_lastUpdateTime)
calculates the elapsed time since last updatestd::chrono::duration_cast<std::chrono::miliseconds>()
converts this elapsed time to milliseconds since we want fractional seconds in the end and many std::chrono
functions use integer values..count()
returns the number of millisecondsmilliseconds / 1000.0
converts the integer milliseconds to a decimal seconds valuedouble DriveSubsystem::SpeedRamp::operator()(const double newSample) {
auto now = std::chrono::steady_clock::now();
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>((now - m_lastUpdateTime)).count();
auto seconds = milliseconds / 1000.0;
auto changeDirection = std::copysign(1.0, newSample - m_lastOutput);
}
std::copysign
is used to copy the sign (positive or negative) from the second parameter (newSample - m_lastOutput
) to the first parameter (1.0
). In this case, changeDirection
will be either +1.0
or -1.0
.
double DriveSubsystem::SpeedRamp::operator()(const double newSample) {
auto now = std::chrono::steady_clock::now();
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>((now - m_lastUpdateTime)).count();
auto seconds = milliseconds / 1000.0;
auto changeDirection = std::copysign(1.0, newSample - m_lastOutput);
auto desiredChangeMagnitude = std::abs(newSample - m_lastOutput);
}
double DriveSubsystem::SpeedRamp::operator()(const double newSample) {
auto now = std::chrono::steady_clock::now();
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>((now - m_lastUpdateTime)).count();
auto seconds = milliseconds / 1000.0;
auto changeDirection = std::copysign(1.0, newSample - m_lastOutput);
auto desiredChangeMagnitude = std::abs(newSample - m_lastOutput);
auto maxTimeRampMagnitude = m_pctPerSecond * seconds;
}
double DriveSubsystem::SpeedRamp::operator()(const double newSample) {
auto now = std::chrono::steady_clock::now();
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>((now - m_lastUpdateTime)).count();
auto seconds = milliseconds / 1000.0;
auto changeDirection = std::copysign(1.0, newSample - m_lastOutput);
auto desiredChangeMagnitude = std::abs(newSample - m_lastOutput);
auto maxTimeRampMagnitude = m_pctPerSecond * seconds;
auto delta = changeDirection * std::min(desiredChangeMagnitude, maxTimeRampMagnitude);
}
std::min
is a function that returns the smallest value from a list of parameters.
double DriveSubsystem::SpeedRamp::operator()(const double newSample) {
auto now = std::chrono::steady_clock::now();
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>((now - m_lastUpdateTime)).count();
auto seconds = milliseconds / 1000.0;
auto changeDirection = std::copysign(1.0, newSample - m_lastOutput);
auto desiredChangeMagnitude = std::abs(newSample - m_lastOutput);
auto maxTimeRampMagnitude = m_pctPerSecond * seconds;
auto delta = changeDirection * std::min(desiredChangeMagnitude, maxTimeRampMagnitude);
m_lastOutput += delta;
return m_lastOutput;
}
Notice that we’re using the +=
operator. This is equivalent to saying m_lastOutput = m_lastOutput + delta
. We have to make sure we update the stored value so we can use it in the future.
You just made a new class! Congratulations! Next, we’ll add this to Arcade Drive and try it out.
ArcadeDrive()
function we have in DriveSubsystem.cpp
void DriveSubsystem::ArcadeDrive(const double forwardSpeed, const double turnSpeed) {
m_leftDrive.Set(ControlMode::PercentOutput, forwardSpeed + turnSpeed);
m_rightDrive.Set(ControlMode::PercentOutput, forwardSpeed - turnSpeed);
}
void DriveSubsystem::ArcadeDrive(const double forwardSpeed, const double turnSpeed) {
static auto forwardRamp = SpeedRamp(0.5);
static auto turnRamp = SpeedRamp(0.5);
m_leftDrive.Set(ControlMode::PercentOutput, forwardSpeed + turnSpeed);
m_rightDrive.Set(ControlMode::PercentOutput, forwardSpeed - turnSpeed);
}
static
indicates that we want to re-use these objects across calls to this function. If we left this out, the ramp parameters would be reset continuously and we’d always get 0 output.
void DriveSubsystem::ArcadeDrive(const double forwardSpeed, const double turnSpeed) {
static auto forwardRamp = SpeedRamp(0.5);
static auto turnRamp = SpeedRamp(0.5);
const auto rampedForwardSpeed = forwardRamp(forwardSpeed);
const auto rampedTurnSpeed = turnRamp(turnSpeed);
m_leftDrive.Set(ControlMode::PercentOutput, rampedForwardSpeed + rampedTurnSpeed);
m_rightDrive.Set(ControlMode::PercentOutput, rampedForwardSpeed - rampedTurnSpeed);
}
Notice how we can do forwardRamp(forwardSpeed)
to use our ramp objects? This is how we use operator()
.
That’s it! you now have a speed ramp added to your Arcade Drive function.
RobotContainer
constructor.SpeedRamp.vi
with two numeric inputs and one numeric outputIn Range and Coerce
VI. This VI both detects if an input is between two values and generates an output value that is guaranteed to be between two values. For example, if we provide an expected range of 0 to 1 with an input of 1.2, the In Range and Coerce
VI would output false
because 1.2 is greater than 1 and the coerced output would be 1
. You can find the In Range and Coerce
VI here:
variables: MaxRampRate, CommandedChange, In, PrevOut, Out
if firstRun:
PrevOut = In
SampleCount = 0
CommandedChange = In - PrevOut
if CommandedChange > MaxRampRate:
CommandedChange = MaxRampRate
else if CommandedChange < -MaxRampRate:
CommandedChange = -MaxRampRate
Out = PrevOut + CommandedChange
PrevOut = Out
Loop
if firstRun
block is known as ‘initialization’ and is necessary to set the values of the feedback nodes at the beginning. The input at the bottom of the feedback node is for initializationPrevOut
variable in the pseudocode corresponds to a feedback node in the solutionif ... else if
block in the middle can be represented by a In Range and Coerce
VIGreat! Now that you’re getting more comfortable in LabVIEW, you should be able to translate concepts into code more easily. Don’t worry if you had to look at the solution, but if you try it on your own first the solution may help you learn more. Now let’s use your new code
Drive_Arcade.vi
and go to the block diagramDrive_Arcade.vi
into ArgoBot_Main.vi
You just uncovered one of the tricky parts of making VIs in LabVIEW. It turns out that feedback nodes from different copies of the same VI can share values and interfere with each other. We didn’t see this when we made debounce because we only had one debounce block in our program.
Thankfully, this is easy to fix. We’ll have to change the properties of SpeedRamp.vi
.
SpeedRamp.vi
againWhat did you notice when using the speed ramped output? Was there a noticeable difference between low and high max ramp rates?
Congratulations! You’re getting the hang of using history to enhance drive code! Next, we’ll be using what we’ve learned so far to make a turbo button!
<-Previous | Index | Next-> |