BiSS-C single-turn absolute encoder does not accumulate turns across the zero rollover (runaway / limit trip)
Summary
The mainline BiSS-C driver does not accumulate turns for single-turn absolute encoders (BISSC_SINGLE_TURN == ON). When the raw count crosses the encoder's full-scale→zero boundary, the reported position jumps a full revolution. The KTech/serial absolute driver already handles this (turn accumulation in KTech.cpp), so single-turn BiSS-C is the outlier.
This is latent for most users because the rollover point usually sits outside the axis working range. On a mount where it falls inside the working range it is reproducible and serious.
Symptoms observed (JTW GTR, 24-bit single-turn BiSS-C on axis1, Manticore/ESP32):
- Servo runaway → motor fault. Crossing the rollover produces an instantaneous ~full-scale "following error"; the servo drives at max velocity to chase it.
- Coordinate jump past the axis limit. Once the servo path is guarded, the reported coordinate still jumps ~360°, tripping the axis1 limit (presents as an under-pole / limit stall).
Both trace to the same missing turn-accumulation and are fixed together.
Proposed fix (mirrors what KTech already does; purely additive, gated, inert otherwise):
EncoderBase.h: new virtual int32_t countsPerTurn() { return 0; } (0 = disabled; every existing encoder inherits it unchanged).
Bissc.h: override countsPerTurn() to report full scale, plus 3 accumulator state members.
Bissc.cpp (inside #if BISSC_SINGLE_TURN == ON): detect the wrap (raw jumps between the top and bottom quarter of full-scale in one poll), keep a running turn count, fold it back into the returned count so the coordinate stays continuous. Fixes the limit/coordinate jump.
Servo.cpp (ServoMotor::poll(), guarded by isAbsolute() && countsPerTurn() > 0): fold the encoder reading to within half a turn of the motor position so a raw wrap can never present as a giant following error. Fast defense-in-depth on the hot path; inert unless |diff| > half a turn.
Safety / scope
Nothing removed or modified - 38 added lines. Three independent gates (isAbsolute(), countsPerTurn() > 0, #if BISSC_SINGLE_TURN == ON) mean multi-turn BiSS-C and all non-BiSS encoders compile and behave exactly as before.
Testing
Full-sky slews repeatedly through the previously-failing rollover spot: axis error stays 0, servo delta stays bounded, pointing correct. Regression-free on the axis that does not cross a rollover. Baseline commit 89c9ca4.
Proposed diff
diff --git a/src/lib/axis/motor/servo/Servo.cpp b/src/lib/axis/motor/servo/Servo.cpp
index 8ac8564..e9832da 100644
--- a/src/lib/axis/motor/servo/Servo.cpp
+++ b/src/lib/axis/motor/servo/Servo.cpp
@@ -308,6 +308,18 @@ void ServoMotor::poll() {
interrupts();
}
+ // Single-turn absolute encoder wrap handling: fold the encoder reading to within half a
+ // turn of the motor position so crossing the encoder full-scale boundary does not create a
+ // huge false following error (which would drive a run-away). Inert unless |diff| > half turn,
+ // which only happens at the physical rollover.
+ if (encoder->isAbsolute()) {
+ int32_t cpt = encoder->countsPerTurn();
+ if (cpt > 0) {
+ while ((int32_t)(encoderCounts - (int32_t)motorCounts) > (cpt >> 1)) encoderCounts -= cpt;
+ while ((int32_t)(encoderCounts - (int32_t)motorCounts) < -(cpt >> 1)) encoderCounts += cpt;
+ }
+ }
+
long unfilteredEncoderCounts = encoderCounts;
UNUSED(unfilteredEncoderCounts);
diff --git a/src/lib/encoder/EncoderBase.h b/src/lib/encoder/EncoderBase.h
index 50b1826..0e41b29 100644
--- a/src/lib/encoder/EncoderBase.h
+++ b/src/lib/encoder/EncoderBase.h
@@ -139,6 +139,9 @@ class Encoder {
// true when this encoder can report absolute position after boot
virtual bool isAbsolute() const { return false; }
+ // full-scale counts per revolution for single-turn absolute encoders (0 = disabled)
+ virtual int32_t countsPerTurn() { return 0; }
+
// set encoder origin
virtual void setOrigin(int32_t counts) { origin = counts; }
diff --git a/src/lib/encoder/bissc/Bissc.cpp b/src/lib/encoder/bissc/Bissc.cpp
index 8a6eb9d..7e68c89 100644
--- a/src/lib/encoder/bissc/Bissc.cpp
+++ b/src/lib/encoder/bissc/Bissc.cpp
@@ -202,6 +202,20 @@ bool Bissc::getCount(uint32_t &count) {
return false;
}
+#if BISSC_SINGLE_TURN == ON
+ // accumulate turns so the count stays continuous across the encoder zero rollover
+ // (otherwise the reported coordinate jumps a full turn and trips axis limits)
+ {
+ uint32_t hi = (encoderCounts >> 1) + (encoderCounts >> 2); // 3/4 scale
+ uint32_t lo = (encoderCounts >> 2); // 1/4 scale
+ if (turnAccumInit) {
+ if (lastRawSingle > hi && count < lo) accumTurns++;
+ else if (lastRawSingle < lo && count > hi) accumTurns--;
+ } else turnAccumInit = true;
+ lastRawSingle = count;
+ }
+#endif
+
#if BISSC_SINGLE_TURN != ON
// combine absolute and low order bits of multi-turn count for a 32 bit count
count = count | ((turns & encoderMultiTurnMask) << encoderBits);
@@ -213,6 +227,9 @@ bool Bissc::getCount(uint32_t &count) {
// center count about the +/- half counts range (a signed value stored unsigned)
// note: instead shifts an multiTurn count but for historical reasons thats preferred
count = count - (encoderCounts >> 1);
+#if BISSC_SINGLE_TURN == ON
+ count += (uint32_t)(accumTurns * (int32_t)encoderCounts);
+#endif
#ifdef BISSC_RESOLUTION_DIVISOR
count = (int32_t)count/(int32_t)(BISSC_RESOLUTION_DIVISOR);
diff --git a/src/lib/encoder/bissc/Bissc.h b/src/lib/encoder/bissc/Bissc.h
index 0991d64..86359ea 100644
--- a/src/lib/encoder/bissc/Bissc.h
+++ b/src/lib/encoder/bissc/Bissc.h
@@ -75,6 +75,7 @@
bool init();
bool isAbsolute() const override { return true; }
+ int32_t countsPerTurn() override { return (int32_t)encoderCounts; }
// set encoder origin
void setOrigin(int32_t counts);
@@ -109,6 +110,11 @@
uint32_t turns = 0;
+ // single-turn wrap -> continuous count accumulation
+ int32_t accumTurns = 0;
+ uint32_t lastRawSingle = 0;
+ bool turnAccumInit = false;
+
// this design allows for 8 to 31 bit encoders
char encoderName[16] = {0};
uint8_t encoderBits = 0;
BiSS-C single-turn absolute encoder does not accumulate turns across the zero rollover (runaway / limit trip)
Summary
The mainline BiSS-C driver does not accumulate turns for single-turn absolute encoders (
BISSC_SINGLE_TURN == ON). When the raw count crosses the encoder's full-scale→zero boundary, the reported position jumps a full revolution. The KTech/serial absolute driver already handles this (turn accumulation inKTech.cpp), so single-turn BiSS-C is the outlier.This is latent for most users because the rollover point usually sits outside the axis working range. On a mount where it falls inside the working range it is reproducible and serious.
Symptoms observed (JTW GTR, 24-bit single-turn BiSS-C on axis1, Manticore/ESP32):
Both trace to the same missing turn-accumulation and are fixed together.
Proposed fix (mirrors what KTech already does; purely additive, gated, inert otherwise):
EncoderBase.h: newvirtual int32_t countsPerTurn() { return 0; }(0 = disabled; every existing encoder inherits it unchanged).Bissc.h: overridecountsPerTurn()to report full scale, plus 3 accumulator state members.Bissc.cpp(inside#if BISSC_SINGLE_TURN == ON): detect the wrap (raw jumps between the top and bottom quarter of full-scale in one poll), keep a running turn count, fold it back into the returned count so the coordinate stays continuous. Fixes the limit/coordinate jump.Servo.cpp(ServoMotor::poll(), guarded byisAbsolute() && countsPerTurn() > 0): fold the encoder reading to within half a turn of the motor position so a raw wrap can never present as a giant following error. Fast defense-in-depth on the hot path; inert unless |diff| > half a turn.Safety / scope
Nothing removed or modified - 38 added lines. Three independent gates (
isAbsolute(),countsPerTurn() > 0,#if BISSC_SINGLE_TURN == ON) mean multi-turn BiSS-C and all non-BiSS encoders compile and behave exactly as before.Testing
Full-sky slews repeatedly through the previously-failing rollover spot: axis error stays 0, servo delta stays bounded, pointing correct. Regression-free on the axis that does not cross a rollover. Baseline commit 89c9ca4.
Proposed diff