Skip to content

Commit 4381f33

Browse files
committed
Add tests for command changes during deceleration
Add four tests to verify hysteresis behavior when the target position changes during deceleration: 1. CommandChangeDuringDecel_TargetFurtherOut: Moving target further out causes eventual switch to acceleration and reaches new target. 2. CommandChangeDuringDecel_TargetCloser: Moving target closer causes continued deceleration (with overshoot) then return to new target. 3. CommandChangeDuringDecel_SmallIncrease: Small target increases during late deceleration are handled correctly. 4. CommandChangeDuringDecel_HysteresisEffect: Documents that hysteresis causes continued deceleration for some steps even when switching to acceleration would be valid.
1 parent 8e5d1e5 commit 4381f33

File tree

1 file changed

+220
-0
lines changed

1 file changed

+220
-0
lines changed

fw/test/bldc_servo_position_test.cc

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,3 +740,223 @@ BOOST_AUTO_TEST_CASE(ControlAccelerationConsistent) {
740740
BOOST_TEST(end_of_trajectory_violations <= 15);
741741
BOOST_TEST(zero_violations == 0);
742742
}
743+
744+
// Test that changing the target position during deceleration works correctly.
745+
// When target is moved further out, we should switch to acceleration.
746+
BOOST_AUTO_TEST_CASE(CommandChangeDuringDecel_TargetFurtherOut,
747+
* boost::unit_test::tolerance(1e-3)) {
748+
Context ctx;
749+
750+
constexpr float kAccel = 1000.0f;
751+
constexpr float kInitialTarget = 0.5f;
752+
constexpr float kNewTarget = 1.0f; // Further out
753+
754+
ctx.data.position = kInitialTarget;
755+
ctx.data.accel_limit = kAccel;
756+
ctx.data.velocity_limit = NaN;
757+
ctx.rate_hz = 10000.0f;
758+
759+
ctx.set_velocity(0.0f);
760+
ctx.set_position(0.0f);
761+
762+
// For a 0.5 rev trajectory with a=1000:
763+
// - v_peak = sqrt(2*1000*0.25) ≈ 22.4 rev/s
764+
// - t_peak = v_peak/a = 0.0224s = 224 steps at 10kHz
765+
// Run until we're well into deceleration phase
766+
int step = 0;
767+
for (; step < 280; step++) {
768+
ctx.Call();
769+
}
770+
771+
// Verify we're decelerating (negative acceleration, positive velocity)
772+
BOOST_TEST(ctx.status.control_acceleration < 0.0f);
773+
BOOST_TEST(ctx.status.control_velocity.value() > 0.0f);
774+
775+
// Now move the target further out
776+
ctx.data.position = kNewTarget;
777+
ctx.data.position_relative_raw.reset(); // Force recalculation
778+
779+
// Continue running - we should eventually switch to acceleration
780+
bool switched_to_accel = false;
781+
for (; step < 10000; step++) {
782+
ctx.Call();
783+
784+
if (!switched_to_accel && ctx.status.control_acceleration > 0.0f) {
785+
switched_to_accel = true;
786+
}
787+
788+
if (ctx.status.trajectory_done) {
789+
break;
790+
}
791+
}
792+
793+
BOOST_TEST(switched_to_accel);
794+
// Should reach the new target
795+
BOOST_TEST(ctx.from_raw(ctx.status.control_position_raw.value()) == kNewTarget);
796+
BOOST_TEST(ctx.status.trajectory_done == true);
797+
}
798+
799+
// Test that changing the target position closer during deceleration works.
800+
// We should continue decelerating, overshoot, then accelerate back.
801+
BOOST_AUTO_TEST_CASE(CommandChangeDuringDecel_TargetCloser,
802+
* boost::unit_test::tolerance(1e-3)) {
803+
Context ctx;
804+
805+
constexpr float kAccel = 1000.0f;
806+
constexpr float kInitialTarget = 0.5f;
807+
constexpr float kNewTarget = 0.3f; // Closer
808+
809+
ctx.data.position = kInitialTarget;
810+
ctx.data.accel_limit = kAccel;
811+
ctx.data.velocity_limit = NaN;
812+
ctx.rate_hz = 10000.0f;
813+
814+
ctx.set_velocity(0.0f);
815+
ctx.set_position(0.0f);
816+
817+
// Run until we're well into deceleration phase
818+
int step = 0;
819+
for (; step < 280; step++) {
820+
ctx.Call();
821+
}
822+
823+
// Verify we're decelerating
824+
BOOST_TEST(ctx.status.control_acceleration < 0.0f);
825+
BOOST_TEST(ctx.status.control_velocity.value() > 0.0f);
826+
827+
// Move the target closer
828+
ctx.data.position = kNewTarget;
829+
ctx.data.position_relative_raw.reset(); // Force recalculation
830+
831+
// Continue running - we should continue decelerating (hysteresis),
832+
// overshoot the new target, then come back.
833+
bool ever_overshot = false;
834+
835+
for (; step < 10000; step++) {
836+
ctx.Call();
837+
float pos = ctx.from_raw(ctx.status.control_position_raw.value());
838+
839+
if (pos > kNewTarget + 0.001f) {
840+
ever_overshot = true;
841+
}
842+
843+
if (ctx.status.trajectory_done) {
844+
break;
845+
}
846+
}
847+
848+
// Should overshoot since we were going fast when target moved closer
849+
BOOST_TEST(ever_overshot);
850+
// Should reach the new target
851+
BOOST_TEST(ctx.from_raw(ctx.status.control_position_raw.value()) == kNewTarget);
852+
BOOST_TEST(ctx.status.trajectory_done == true);
853+
}
854+
855+
// Test that small position changes during deceleration are handled correctly.
856+
// A small increase in target position should still eventually reach it.
857+
BOOST_AUTO_TEST_CASE(CommandChangeDuringDecel_SmallIncrease,
858+
* boost::unit_test::tolerance(1e-3)) {
859+
Context ctx;
860+
861+
constexpr float kAccel = 1000.0f;
862+
constexpr float kInitialTarget = 0.5f;
863+
// Small increase - just a few decel distances worth
864+
constexpr float kSmallDelta = 0.02f;
865+
constexpr float kNewTarget = kInitialTarget + kSmallDelta;
866+
867+
ctx.data.position = kInitialTarget;
868+
ctx.data.accel_limit = kAccel;
869+
ctx.data.velocity_limit = NaN;
870+
ctx.rate_hz = 10000.0f;
871+
872+
ctx.set_velocity(0.0f);
873+
ctx.set_position(0.0f);
874+
875+
// Run until we're very late in deceleration phase (low velocity)
876+
// At step 400, we're near the end of a ~448 step trajectory
877+
int step = 0;
878+
for (; step < 400; step++) {
879+
ctx.Call();
880+
}
881+
882+
// Verify we're decelerating with relatively low velocity
883+
BOOST_TEST(ctx.status.control_acceleration < 0.0f);
884+
const float vel_at_change = ctx.status.control_velocity.value();
885+
BOOST_TEST(vel_at_change > 0.0f);
886+
BOOST_TEST(vel_at_change < 10.0f); // Should be slowing down significantly
887+
888+
// Small increase in target
889+
ctx.data.position = kNewTarget;
890+
ctx.data.position_relative_raw.reset(); // Force recalculation
891+
892+
// Continue and verify we reach the new target
893+
for (; step < 10000; step++) {
894+
ctx.Call();
895+
896+
if (ctx.status.trajectory_done) {
897+
break;
898+
}
899+
}
900+
901+
BOOST_TEST(ctx.from_raw(ctx.status.control_position_raw.value()) == kNewTarget);
902+
BOOST_TEST(ctx.status.trajectory_done == true);
903+
}
904+
905+
// Test behavior when hysteresis prevents immediate acceleration switch.
906+
// With a position increase during deceleration, the hysteresis causes
907+
// continued deceleration for a number of steps.
908+
BOOST_AUTO_TEST_CASE(CommandChangeDuringDecel_HysteresisEffect,
909+
* boost::unit_test::tolerance(1e-3)) {
910+
Context ctx;
911+
912+
constexpr float kAccel = 1000.0f;
913+
constexpr float kInitialTarget = 0.5f;
914+
constexpr float kNewTarget = 0.6f; // 0.1 rev further
915+
916+
ctx.data.position = kInitialTarget;
917+
ctx.data.accel_limit = kAccel;
918+
ctx.data.velocity_limit = NaN;
919+
ctx.rate_hz = 10000.0f;
920+
921+
ctx.set_velocity(0.0f);
922+
ctx.set_position(0.0f);
923+
924+
// Run until we're decelerating but still have significant velocity
925+
// At step 280, we're in the middle of deceleration
926+
int step = 0;
927+
for (; step < 280; step++) {
928+
ctx.Call();
929+
}
930+
931+
BOOST_TEST(ctx.status.control_acceleration < 0.0f);
932+
const float vel_before = ctx.status.control_velocity.value();
933+
BOOST_TEST(vel_before > 5.0f); // Still significant velocity
934+
935+
// Move target further out
936+
ctx.data.position = kNewTarget;
937+
ctx.data.position_relative_raw.reset(); // Force recalculation
938+
939+
// Track acceleration changes after the command change
940+
int steps_still_decelerating = 0;
941+
942+
for (; step < 10000; step++) {
943+
ctx.Call();
944+
float vel = ctx.status.control_velocity.value();
945+
946+
if (ctx.status.control_acceleration < 0.0f && vel > 0.0f) {
947+
steps_still_decelerating++;
948+
}
949+
950+
if (ctx.status.trajectory_done) {
951+
break;
952+
}
953+
}
954+
955+
// Due to hysteresis, we should continue decelerating for some steps
956+
// even though we could theoretically switch to acceleration immediately.
957+
BOOST_TEST(steps_still_decelerating > 0);
958+
959+
// Should still reach the new target
960+
BOOST_TEST(ctx.from_raw(ctx.status.control_position_raw.value()) == kNewTarget);
961+
BOOST_TEST(ctx.status.trajectory_done == true);
962+
}

0 commit comments

Comments
 (0)