#include "doctest/doctest.h" #include "fgc/TmcCurrent.h" #include using namespace fgc; namespace { // The IOTA tower's driver board. constexpr double kR = 0.075; } // namespace TEST_CASE("the RMS current formula matches the datasheet") { // Full scale: GLOBALSCALER 256 (encoded 0), IRUN 31. CHECK(tmcRmsCurrentMa(0, 31, kR) == doctest::Approx(2419.0).epsilon(0.001)); // The fleet's setting. CHECK(tmcRmsCurrentMa(80, 31, kR) == doctest::Approx(756.0).epsilon(0.002)); // A non-positive sense resistor is a config error, not a division to attempt. CHECK(tmcRmsCurrentMa(80, 31, 0.0) == 0.0); } TEST_CASE("the fleet's run current lands on the values the firmware already ships") { // 80 is what firmware src/config.h carries for yaw, arrived at independently. // If this test fails, either the formula or that constant has drifted. auto p = planTmcCurrent(kR, 756.0, 200.0); REQUIRE(p.ok); CHECK(p.global_scaler == 80); CHECK(p.run_irun == 31); CHECK(p.homing_irun == 7); CHECK(p.run_ma == doctest::Approx(756.0).epsilon(0.002)); CHECK(p.homing_ma == doctest::Approx(189.0).epsilon(0.01)); CHECK(p.warnings.empty()); } TEST_CASE("an achieved current is recomputed from the registers, never echoed back") { // The log must not flatter the result: what is reported has to be what the // chosen registers deliver. for (double r : {0.05, 0.075, 0.11}) { for (double run : {300.0, 756.0, 1500.0}) { auto p = planTmcCurrent(r, run, run / 4.0); REQUIRE(p.ok); CHECK(p.run_ma == doctest::Approx(tmcRmsCurrentMa(p.global_scaler, p.run_irun, r))); CHECK(p.homing_ma == doctest::Approx(tmcRmsCurrentMa(p.global_scaler, p.homing_irun, r))); } } } TEST_CASE("GLOBALSCALER never lands in the band the datasheet forbids") { // 1..31 is disallowed, and 0 means 256 (full scale) - emitting either would // mean silently maximum current on a rig whose endstop yields to full torque. for (double r : {0.05, 0.075, 0.11, 0.22}) { for (double run = 10.0; run <= 3000.0; run += 10.0) { auto p = planTmcCurrent(r, run, run / 2.0); REQUIRE(p.ok); CHECK(p.global_scaler >= 32); CHECK(p.global_scaler <= 255); } } } TEST_CASE("a run current the driver cannot reach is clamped and named") { auto p = planTmcCurrent(kR, 5000.0, 200.0); REQUIRE(p.ok); CHECK(p.global_scaler == 255); CHECK(p.run_ma < 2500.0); REQUIRE(p.warnings.size() >= 1); CHECK(p.warnings[0].find("5000") != std::string::npos); } TEST_CASE("a homing current above the run current is refused, not obeyed") { auto p = planTmcCurrent(kR, 756.0, 900.0); REQUIRE(p.ok); CHECK(p.homing_irun == p.run_irun); REQUIRE(p.warnings.size() >= 1); CHECK(p.warnings[0].find("above the run current") != std::string::npos); } TEST_CASE("a homing current below the smallest expressible step says so") { auto p = planTmcCurrent(kR, 756.0, 5.0); REQUIRE(p.ok); CHECK(p.homing_irun == 0); CHECK(p.homing_ma > 5.0); // stronger than asked - the direction worth naming bool named = false; for (const auto& w : p.warnings) if (w.find("smallest step") != std::string::npos) named = true; CHECK(named); } TEST_CASE("the homing current never exceeds the run current after rounding") { for (double r : {0.05, 0.075, 0.11}) { for (double run = 50.0; run <= 2000.0; run += 50.0) { auto p = planTmcCurrent(r, run, run * 0.99); REQUIRE(p.ok); CHECK(p.homing_irun <= p.run_irun); } } } TEST_CASE("non-positive inputs are rejected rather than computed") { CHECK_FALSE(planTmcCurrent(0.0, 756.0, 200.0).ok); CHECK_FALSE(planTmcCurrent(kR, 0.0, 200.0).ok); CHECK_FALSE(planTmcCurrent(kR, 756.0, 0.0).ok); CHECK(planTmcCurrent(0.0, 756.0, 200.0).warnings.size() == 1); }