Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public DCMotor(
this.rOhms = nominalVoltageVolts / this.stallCurrentAmps;
this.KvRadPerSecPerVolt =
freeSpeedRadPerSec / (nominalVoltageVolts - rOhms * this.freeCurrentAmps);
this.KtNMPerAmp = this.stallTorqueNewtonMeters / this.stallCurrentAmps;
this.KtNMPerAmp = this.stallTorqueNewtonMeters / (this.stallCurrentAmps - this.freeCurrentAmps);
}

/**
Expand All @@ -89,7 +89,7 @@ public double getCurrent(double speedRadiansPerSec, double voltageInputVolts) {
* @return The current drawn by the motor.
*/
public double getCurrent(double torqueNm) {
return torqueNm / KtNMPerAmp;
return torqueNm / KtNMPerAmp + freeCurrentAmps;
}

/**
Expand All @@ -99,7 +99,7 @@ public double getCurrent(double torqueNm) {
* @return The torque output.
*/
public double getTorque(double currentAmpere) {
return currentAmpere * KtNMPerAmp;
return (currentAmpere - freeCurrentAmps) * KtNMPerAmp;
Copy link
Contributor

@bhall-ctre bhall-ctre Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns a negative torque at 0 A, but at 0 velocity, 0 A actually corresponds to 0 torque. (This and getSpeed() are what makes accounting for free current so difficult/annoying.)

}

/**
Expand All @@ -110,7 +110,7 @@ public double getTorque(double currentAmpere) {
* @return The voltage of the motor.
*/
public double getVoltage(double torqueNm, double speedRadiansPerSec) {
return 1.0 / KvRadPerSecPerVolt * speedRadiansPerSec + 1.0 / KtNMPerAmp * rOhms * torqueNm;
return 1.0 / KvRadPerSecPerVolt * speedRadiansPerSec + 1.0 / KtNMPerAmp * rOhms * torqueNm + freeCurrentAmps * rOhms;
}

/**
Expand All @@ -122,7 +122,7 @@ public double getVoltage(double torqueNm, double speedRadiansPerSec) {
*/
public double getSpeed(double torqueNm, double voltageInputVolts) {
return voltageInputVolts * KvRadPerSecPerVolt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem right, since plugging in a torque and voltage of zero results in a nonzero speed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

free current scales with voltage right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. I was only ever taught about free current with respect to the free speed at the test voltage. I'm unable to find more info online either.

- 1.0 / KtNMPerAmp * torqueNm * rOhms * KvRadPerSecPerVolt;
- 1.0 / KtNMPerAmp * torqueNm * rOhms * KvRadPerSecPerVolt - KvRadPerSecPerVolt * freeCurrentAmps * rOhms;
}

/**
Expand Down
11 changes: 6 additions & 5 deletions wpimath/src/main/native/include/frc/system/plant/DCMotor.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class WPILIB_DLLEXPORT DCMotor {
freeSpeed(freeSpeed),
R(nominalVoltage / this->stallCurrent),
Kv(freeSpeed / (nominalVoltage - R * this->freeCurrent)),
Kt(this->stallTorque / this->stallCurrent) {}
Kt(this->stallTorque / (this->stallCurrent - this->freeCurrent)) {}

/**
* Returns current drawn by motor with given speed and input voltage.
Expand All @@ -90,7 +90,7 @@ class WPILIB_DLLEXPORT DCMotor {
* @param torque The torque produced by the motor.
*/
constexpr units::ampere_t Current(units::newton_meter_t torque) const {
return torque / Kt;
return torque / Kt + freeCurrent;
}

/**
Expand All @@ -99,7 +99,7 @@ class WPILIB_DLLEXPORT DCMotor {
* @param current The current drawn by the motor.
*/
constexpr units::newton_meter_t Torque(units::ampere_t current) const {
return current * Kt;
return (current - freeCurrent) * Kt;
}

/**
Expand All @@ -111,7 +111,7 @@ class WPILIB_DLLEXPORT DCMotor {
*/
constexpr units::volt_t Voltage(units::newton_meter_t torque,
units::radians_per_second_t speed) const {
return 1.0 / Kv * speed + 1.0 / Kt * R * torque;
return 1.0 / Kv * speed + 1.0 / Kt * R * torque + freeCurrent * R;
}

/**
Expand All @@ -123,7 +123,8 @@ class WPILIB_DLLEXPORT DCMotor {
*/
constexpr units::radians_per_second_t Speed(
units::newton_meter_t torque, units::volt_t inputVoltage) const {
return inputVoltage * Kv - 1.0 / Kt * torque * R * Kv;
return inputVoltage * Kv - 1.0 / Kt * torque * R * Kv -
Kv * freeCurrent * R;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ void testLQROnElevator() {

var K = new LinearQuadraticRegulator<>(plant, qElms, rElms, dt).getK();

assertEquals(522.87006795347486, K.get(0, 0), 1e-6);
assertEquals(38.239878385020411, K.get(0, 1), 1e-6);
assertEquals(522.59, K.get(0, 0), 1e-2);
assertEquals(38.17, K.get(0, 1), 1e-2);
}

@Test
Expand All @@ -49,7 +49,7 @@ void testFourMotorElevator() {
new LinearQuadraticRegulator<>(plant, VecBuilder.fill(0.1, 0.2), VecBuilder.fill(12.0), dt)
.getK();

assertEquals(10.381, K.get(0, 0), 1e-2);
assertEquals(10.365, K.get(0, 0), 1e-2);
assertEquals(0.6929, K.get(0, 1), 1e-2);
}

Expand All @@ -69,8 +69,8 @@ void testLQROnArm() {

var K = new LinearQuadraticRegulator<>(plant, qElms, rElms, dt).getK();

assertEquals(19.339349883583761, K.get(0, 0), 1e-6);
assertEquals(3.3542559517421582, K.get(0, 1), 1e-6);
assertEquals(19.247, K.get(0, 0), 1e-2);
assertEquals(3.335, K.get(0, 1), 1e-2);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ void testDrivetrainVelocitySystem() {
model
.getA()
.isEqual(
MatBuilder.fill(Nat.N2(), Nat.N2(), -10.14132, 3.06598, 3.06598, -10.14132),
MatBuilder.fill(Nat.N2(), Nat.N2(), -10.3182, 3.11946, 3.11946, -10.3182),
0.001));

assertTrue(
model
.getB()
.isEqual(
MatBuilder.fill(Nat.N2(), Nat.N2(), 4.2590, -1.28762, -1.2876, 4.2590), 0.001));
MatBuilder.fill(Nat.N2(), Nat.N2(), 4.33333, -1.31008, -1.31008, 4.33333), 0.001));

assertTrue(
model.getC().isEqual(MatBuilder.fill(Nat.N2(), Nat.N2(), 1.0, 0.0, 0.0, 1.0), 0.001));
Expand All @@ -44,9 +44,9 @@ void testDrivetrainVelocitySystem() {
void testElevatorSystem() {
var model = LinearSystemId.createElevatorSystem(DCMotor.getNEO(2), 5, 0.05, 12);
assertTrue(
model.getA().isEqual(MatBuilder.fill(Nat.N2(), Nat.N2(), 0, 1, 0, -99.05473), 0.001));
model.getA().isEqual(MatBuilder.fill(Nat.N2(), Nat.N2(), 0, 1, 0, -100.782), 0.001));

assertTrue(model.getB().isEqual(VecBuilder.fill(0, 20.8), 0.001));
assertTrue(model.getB().isEqual(VecBuilder.fill(0, 21.1628), 0.001));

assertTrue(model.getC().isEqual(MatBuilder.fill(Nat.N2(), Nat.N2(), 1, 0, 0, 1), 0.001));

Expand All @@ -56,9 +56,9 @@ void testElevatorSystem() {
@Test
void testFlywheelSystem() {
var model = LinearSystemId.createFlywheelSystem(DCMotor.getNEO(2), 0.00032, 1.0);
assertTrue(model.getA().isEqual(VecBuilder.fill(-26.87032), 0.001));
assertTrue(model.getA().isEqual(VecBuilder.fill(-27.339), 0.001));

assertTrue(model.getB().isEqual(VecBuilder.fill(1354.166667), 0.001));
assertTrue(model.getB().isEqual(VecBuilder.fill(1377.785), 0.001));

assertTrue(model.getC().isEqual(VecBuilder.fill(1), 0.001));

Expand All @@ -69,9 +69,9 @@ void testFlywheelSystem() {
void testDCMotorSystem() {
var model = LinearSystemId.createDCMotorSystem(DCMotor.getNEO(2), 0.00032, 1.0);
assertTrue(
model.getA().isEqual(MatBuilder.fill(Nat.N2(), Nat.N2(), 0, 1, 0, -26.87032), 0.001));
model.getA().isEqual(MatBuilder.fill(Nat.N2(), Nat.N2(), 0, 1, 0, -27.339), 0.001));

assertTrue(model.getB().isEqual(VecBuilder.fill(0, 1354.166667), 0.001));
assertTrue(model.getB().isEqual(VecBuilder.fill(0, 1377.785), 0.001));

assertTrue(model.getC().isEqual(Matrix.eye(Nat.N2()), 0.001));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.

package edu.wpi.first.math.system.plant;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.function.Supplier;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class DCMotorTest {
private static final double kTolerance = 1e-12;

static Stream<Arguments> motors() {
return Stream.of(
Arguments.of("CIM", (Supplier<DCMotor>) () -> DCMotor.getCIM(1)),
Arguments.of("MiniCIM", (Supplier<DCMotor>) () -> DCMotor.getMiniCIM(1)),
Arguments.of("Bag", (Supplier<DCMotor>) () -> DCMotor.getBag(1)),
Arguments.of("Vex775Pro", (Supplier<DCMotor>) () -> DCMotor.getVex775Pro(1)),
Arguments.of("AndymarkRs775_125", (Supplier<DCMotor>) () -> DCMotor.getAndymarkRs775_125(1)),
Arguments.of("BanebotsRs775", (Supplier<DCMotor>) () -> DCMotor.getBanebotsRs775(1)),
Arguments.of("Andymark9015", (Supplier<DCMotor>) () -> DCMotor.getAndymark9015(1)),
Arguments.of("BanebotsRs550", (Supplier<DCMotor>) () -> DCMotor.getBanebotsRs550(1)),
Arguments.of("NEO", (Supplier<DCMotor>) () -> DCMotor.getNEO(1)),
Arguments.of("Neo550", (Supplier<DCMotor>) () -> DCMotor.getNeo550(1)),
Arguments.of("Falcon500", (Supplier<DCMotor>) () -> DCMotor.getFalcon500(1)),
Arguments.of("Falcon500Foc", (Supplier<DCMotor>) () -> DCMotor.getFalcon500Foc(1)),
Arguments.of("RomiBuiltIn", (Supplier<DCMotor>) () -> DCMotor.getRomiBuiltIn(1)),
Arguments.of("KrakenX60", (Supplier<DCMotor>) () -> DCMotor.getKrakenX60(1)),
Arguments.of("KrakenX60Foc", (Supplier<DCMotor>) () -> DCMotor.getKrakenX60Foc(1)),
Arguments.of("KrakenX44", (Supplier<DCMotor>) () -> DCMotor.getKrakenX44(1)),
Arguments.of("KrakenX44Foc", (Supplier<DCMotor>) () -> DCMotor.getKrakenX44Foc(1)),
Arguments.of("Minion", (Supplier<DCMotor>) () -> DCMotor.getMinion(1)),
Arguments.of("NeoVortex", (Supplier<DCMotor>) () -> DCMotor.getNeoVortex(1)));
}

@ParameterizedTest(name = "{0}")
@MethodSource("motors")
void testCurrentAtZeroSpeedAndNominalVoltageIsStallCurrent(
String name, Supplier<DCMotor> motorFactory) {
var motor = motorFactory.get();
assertEquals(
motor.stallCurrentAmps,
motor.getCurrent(0.0, motor.nominalVoltageVolts),
kTolerance);
}

@ParameterizedTest(name = "{0}")
@MethodSource("motors")
void testCurrentAtFreeSpeedAndNominalVoltageIsFreeCurrent(
String name, Supplier<DCMotor> motorFactory) {
var motor = motorFactory.get();
assertEquals(
motor.freeCurrentAmps,
motor.getCurrent(motor.freeSpeedRadPerSec, motor.nominalVoltageVolts),
kTolerance);
}

@ParameterizedTest(name = "{0}")
@MethodSource("motors")
void testCurrentAtStallTorqueIsStallCurrent(String name, Supplier<DCMotor> motorFactory) {
var motor = motorFactory.get();
assertEquals(motor.stallCurrentAmps, motor.getCurrent(motor.stallTorqueNewtonMeters), kTolerance);
}

@ParameterizedTest(name = "{0}")
@MethodSource("motors")
void testCurrentAtZeroTorqueIsFreeCurrent(String name, Supplier<DCMotor> motorFactory) {
var motor = motorFactory.get();
assertEquals(motor.freeCurrentAmps, motor.getCurrent(0.0), kTolerance);
}

@ParameterizedTest(name = "{0}")
@MethodSource("motors")
void testTorqueAtFreeCurrentIsZero(String name, Supplier<DCMotor> motorFactory) {
var motor = motorFactory.get();
assertEquals(0.0, motor.getTorque(motor.freeCurrentAmps), kTolerance);
}

@ParameterizedTest(name = "{0}")
@MethodSource("motors")
void testTorqueAtStallCurrentIsStallTorque(String name, Supplier<DCMotor> motorFactory) {
var motor = motorFactory.get();
assertEquals(
motor.stallTorqueNewtonMeters, motor.getTorque(motor.stallCurrentAmps), kTolerance);
}

@ParameterizedTest(name = "{0}")
@MethodSource("motors")
void testVoltageAtStallTorqueAndZeroSpeedIsNominalVoltage(
String name, Supplier<DCMotor> motorFactory) {
var motor = motorFactory.get();
assertEquals(
motor.nominalVoltageVolts,
motor.getVoltage(motor.stallTorqueNewtonMeters, 0.0),
kTolerance);
}

@ParameterizedTest(name = "{0}")
@MethodSource("motors")
void testVoltageAtZeroTorqueAndFreeSpeedIsNominalVoltage(
String name, Supplier<DCMotor> motorFactory) {
var motor = motorFactory.get();
assertEquals(
motor.nominalVoltageVolts, motor.getVoltage(0.0, motor.freeSpeedRadPerSec), kTolerance);
}

@ParameterizedTest(name = "{0}")
@MethodSource("motors")
void testSpeedAtStallTorqueAndNominalVoltageIsZero(
String name, Supplier<DCMotor> motorFactory) {
var motor = motorFactory.get();
assertEquals(
0.0, motor.getSpeed(motor.stallTorqueNewtonMeters, motor.nominalVoltageVolts), kTolerance);
}

@ParameterizedTest(name = "{0}")
@MethodSource("motors")
void testSpeedAtZeroTorqueAndNominalVoltageIsFreeSpeed(
String name, Supplier<DCMotor> motorFactory) {
var motor = motorFactory.get();
assertEquals(
motor.freeSpeedRadPerSec, motor.getSpeed(0.0, motor.nominalVoltageVolts), kTolerance);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ TEST(LinearQuadraticRegulatorTest, ElevatorGains) {
Matrixd<1, 2> K =
LinearQuadraticRegulator<2, 1>{plant, {0.02, 0.4}, {12.0}, 5_ms}.K();

EXPECT_NEAR(522.87006795347486, K(0, 0), 1e-6);
EXPECT_NEAR(38.239878385020411, K(0, 1), 1e-6);
EXPECT_NEAR(522.58976745337009, K(0, 0), 1e-6);
EXPECT_NEAR(38.170344262642963, K(0, 1), 1e-6);
}

TEST(LinearQuadraticRegulatorTest, ArmGains) {
Expand All @@ -59,8 +59,8 @@ TEST(LinearQuadraticRegulatorTest, ArmGains) {
LinearQuadraticRegulator<2, 1>{plant, {0.01745, 0.08726}, {12.0}, 5_ms}
.K();

EXPECT_NEAR(19.339349883583761, K(0, 0), 1e-6);
EXPECT_NEAR(3.3542559517421582, K(0, 1), 1e-6);
EXPECT_NEAR(19.24674874537445, K(0, 0), 1e-6);
EXPECT_NEAR(3.3352730789607676, K(0, 1), 1e-6);
}

TEST(LinearQuadraticRegulatorTest, FourMotorElevator) {
Expand Down
16 changes: 8 additions & 8 deletions wpimath/src/test/native/cpp/system/LinearSystemIDTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ TEST(LinearSystemIDTest, IdentifyDrivetrainVelocitySystem) {
#endif

ASSERT_TRUE(model.A().isApprox(
frc::Matrixd<2, 2>{{-10.14132, 3.06598}, {3.06598, -10.14132}}, 0.001));
frc::Matrixd<2, 2>{{-10.3182, 3.11946}, {3.11946, -10.3182}}, 0.001));
ASSERT_TRUE(model.B().isApprox(
frc::Matrixd<2, 2>{{4.2590, -1.28762}, {-1.2876, 4.2590}}, 0.001));
frc::Matrixd<2, 2>{{4.33333, -1.31008}, {-1.31008, 4.33333}}, 0.001));
ASSERT_TRUE(
model.C().isApprox(frc::Matrixd<2, 2>{{1.0, 0.0}, {0.0, 1.0}}, 0.001));
ASSERT_TRUE(
Expand All @@ -35,8 +35,8 @@ TEST(LinearSystemIDTest, ElevatorSystem) {
0.05_m, 12)
.Slice(0);
ASSERT_TRUE(model.A().isApprox(
frc::Matrixd<2, 2>{{0.0, 1.0}, {0.0, -99.05473}}, 0.001));
ASSERT_TRUE(model.B().isApprox(frc::Matrixd<2, 1>{0.0, 20.8}, 0.001));
frc::Matrixd<2, 2>{{0.0, 1.0}, {0.0, -100.782}}, 0.001));
ASSERT_TRUE(model.B().isApprox(frc::Matrixd<2, 1>{0.0, 21.1628}, 0.001));
ASSERT_TRUE(model.C().isApprox(frc::Matrixd<1, 2>{1.0, 0.0}, 0.001));
ASSERT_TRUE(model.D().isApprox(frc::Matrixd<1, 1>{0.0}, 0.001));
}
Expand All @@ -50,8 +50,8 @@ TEST(LinearSystemIDTest, FlywheelSystem) {
frc::DCMotor::NEO(2), 0.00032_kg_sq_m, 1.0);
#endif

ASSERT_TRUE(model.A().isApprox(frc::Matrixd<1, 1>{-26.87032}, 0.001));
ASSERT_TRUE(model.B().isApprox(frc::Matrixd<1, 1>{1354.166667}, 0.001));
ASSERT_TRUE(model.A().isApprox(frc::Matrixd<1, 1>{-27.339}, 0.001));
ASSERT_TRUE(model.B().isApprox(frc::Matrixd<1, 1>{1377.79}, 0.001));
ASSERT_TRUE(model.C().isApprox(frc::Matrixd<1, 1>{1.0}, 0.001));
ASSERT_TRUE(model.D().isApprox(frc::Matrixd<1, 1>{0.0}, 0.001));
}
Expand All @@ -66,8 +66,8 @@ TEST(LinearSystemIDTest, DCMotorSystem) {
#endif

ASSERT_TRUE(
model.A().isApprox(frc::Matrixd<2, 2>{{0, 1}, {0, -26.87032}}, 0.001));
ASSERT_TRUE(model.B().isApprox(frc::Matrixd<2, 1>{0, 1354.166667}, 0.001));
model.A().isApprox(frc::Matrixd<2, 2>{{0, 1}, {0, -27.339}}, 0.001));
ASSERT_TRUE(model.B().isApprox(frc::Matrixd<2, 1>{0, 1377.79}, 0.001));
ASSERT_TRUE(
model.C().isApprox(frc::Matrixd<2, 2>{{1.0, 0.0}, {0.0, 1.0}}, 0.001));
ASSERT_TRUE(model.D().isApprox(frc::Matrixd<2, 1>{0.0, 0.0}, 0.001));
Expand Down
Loading
Loading