Skip to content

Commit 643cec9

Browse files
committed
DC servo: add overflow-safe sigma–delta PWM dithering for sub-count resolution
- Introduce SERVO_SIGMA_DELTA_DITHERING (residual, non-overflowing implementation) - Dither float PWM counts to integer per tick so long-term average matches target for DC motors - Improve effective torque resolution near sidereal - Reduces bias and micro-jitter vs. trunc/round-only outputs - Reset residual on zero-output/disable to avoid stale carryover - Uses single-precision math
1 parent 2e74086 commit 643cec9

File tree

3 files changed

+84
-5
lines changed

3 files changed

+84
-5
lines changed

src/lib/axis/motor/servo/ServoDriver.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010

1111
#ifdef SERVO_MOTOR_PRESENT
1212

13+
#ifdef SERVO_SIGMA_DELTA_DITHERING
14+
#include "dc/SigmaDeltaDither.h"
15+
#endif
16+
1317
typedef struct ServoPins {
1418
int16_t ph1; // step
1519
int16_t ph1State;
@@ -71,7 +75,7 @@ class ServoDriver {
7175
// get status info.
7276
// this is a required method for the Axis class
7377
DriverStatus getStatus() { return status; }
74-
78+
7579
// calibrate the motor if required
7680
virtual void calibrateDriver() {}
7781

@@ -80,7 +84,7 @@ class ServoDriver {
8084

8185
protected:
8286
virtual void readStatus() {}
83-
87+
8488
int axisNumber;
8589
char axisPrefix[32]; // prefix for debug messages
8690

@@ -115,6 +119,11 @@ class ServoDriver {
115119

116120
const int numParameters = 2;
117121
AxisParameter* parameter[2] = {&invalid, &acceleration};
122+
123+
#ifdef SERVO_SIGMA_DELTA_DITHERING
124+
SigmaDeltaDither sigmaDelta; // carries fractional residue between ticks
125+
#endif
126+
118127
};
119128

120129
#endif

src/lib/axis/motor/servo/dc/DcServoDriver.h

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
#define SERVO_HYST_EXIT_CPS 0.6f // drop below this to RETURN to zero
2727
#endif
2828

29+
ifdef SERVO_SIGMA_DELTA_DITHERING
30+
#include "SigmaDeltaDither.h"
31+
#endif
2932

3033
#include "../ServoDriver.h"
3134

@@ -64,6 +67,10 @@ class ServoDcDriver : public ServoDriver {
6467
zeroHoldSign = 0; // ensure immediate exit and clear latch on exact zero
6568
#endif
6669

70+
#ifdef SERVO_SIGMA_DELTA_DITHERING
71+
sigmaDelta.reset(); // reset dithering residue when output is zero
72+
#endif
73+
6774
return 0; // early out
6875
}
6976

@@ -81,7 +88,13 @@ class ServoDcDriver : public ServoDriver {
8188
zeroHoldSign = (sign >= 0) ? 1 : -1; // latch direction
8289
} else {
8390
// currently moving: if we drop below exit threshold, snap back to zero
84-
if (vAbs < SERVO_HYST_EXIT_CPS) { zeroHoldSign = 0; return 0; }
91+
if (vAbs < SERVO_HYST_EXIT_CPS) {
92+
zeroHoldSign = 0;
93+
#ifdef SERVO_SIGMA_DELTA_DITHERING
94+
sigmaDelta.reset(); // also reset when snapping back to zero
95+
#endif
96+
return 0;
97+
}
8598
// allow direction change if command flips while above thresholds
8699
if ((sign >= 0 ? 1 : -1) != zeroHoldSign) zeroHoldSign = (sign >= 0) ? 1 : -1;
87100
}
@@ -93,8 +106,13 @@ class ServoDcDriver : public ServoDriver {
93106
// fmaf keeps one rounding and is very fast on some FPUS(M7).
94107
float countsF = fmaf(vAbs, velToCountsGain, (float)countsMinCached);
95108

96-
// Single float→int rounding at the end
97-
long power = (long)lroundf(countsF);
109+
#ifdef SERVO_SIGMA_DELTA_DITHERING
110+
// Dither the floating counts to an integer so the time-average equals countsF
111+
int32_t power = sigmaDelta.dither_counts(countsF, countsMinCached, countsMaxCached);
112+
#else
113+
// Single float→int rounding at the end
114+
long power = (long)lroundf(countsF);
115+
#endif
98116

99117
return power * sign;
100118
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// -----------------------------------------------------------------------------
2+
// Overflow-safe Sigma–Delta PWM Dithering
3+
// Enable with: #define SERVO_SIGMA_DELTA_DITHERING ON
4+
//
5+
// Purpose:
6+
// When PWM resolution is low (e.g., sidereal ≈ a few counts), this modulator
7+
// time-averages between adjacent integer counts so the long-term average
8+
// equals a floating target (e.g., 4.4 → mix of 4 and 5). This code never
9+
// overflows because it only carries the fractional residual in [0,1).
10+
// -----------------------------------------------------------------------------
11+
12+
#ifdef SERVO_SIGMA_DELTA_DITHERING
13+
14+
// Lightweight state holder for first-order sigma–delta dithering
15+
// We store only the fractional residual between ticks
16+
struct SigmaDeltaDither {
17+
// Fractional residue in [0,1). Carries the part we couldn't emit this tick
18+
float resid = 0.0f;
19+
20+
// Reset internal state (call when you reinitialize control or modes change)
21+
inline void reset() { resid = 0.0f; }
22+
23+
// convert a floating desired count into an integer count for this tick,
24+
// such that the time-average of outputs equals the floating target
25+
inline int32_t dither_counts(float desiredCounts,
26+
int32_t minCount,
27+
int32_t maxCount)
28+
{
29+
// Safety clamp the desired average to valid bounds.
30+
if (desiredCounts < (float)minCount) desiredCounts = (float)minCount;
31+
if (desiredCounts > (float)maxCount) desiredCounts = (float)maxCount;
32+
33+
// Add residual: carry fractional error forward from previous tick
34+
// e.g. 4.4 + 0.7 = 5.1 → emit 5 now, resid becomes 0.1
35+
float sum = desiredCounts + resid;
36+
37+
// Emit the integer part of this tick
38+
int32_t out = (int32_t)floorf(sum);
39+
40+
// Keep the fractional residue for next tick (always in [0,1))
41+
resid = sum - (float)out;
42+
43+
// safety clamp (probably not needed, but just in case)
44+
if (out < minCount) out = minCount;
45+
if (out > maxCount) out = maxCount;
46+
47+
return out; // Integer PWM counts to write this tick
48+
}
49+
50+
};
51+
52+
#endif

0 commit comments

Comments
 (0)