The small error that starts the whole problem
Suppose a motor needs to accelerate from 1000 Hz to 2000 Hz in three steps. The total change is 1000 Hz, and dividing that by 3 gives 333 Hz per step.
1 2 3
脉冲1:1000 → 1333
脉冲2:1333 → 1666
脉冲3:1666 → 1999
The final frequency lands at 1999 Hz, not 2000 Hz. One hertz has disappeared.
That missing 1 Hz is the result of integer-division truncation. On an embedded MCU, using floating point for this kind of frequency calculation is usually not a good answer: timers want integer values, floating-point operations are slower, and truncation still has to be dealt with when pulse counts accumulate.
The practical solution is to use a DDA algorithm and distribute that leftover 1 Hz across the steps instead of throwing it away.
What remainderStep is really doing
The basic DDA implementation revolves around three variables:
integerStep = freqDiff / steps; // 每步固定的频率变化量
remainderStep = freqDiff % steps; // 无法整除的余数
remainderAcc = 0; // 余数累加器
integerStep is straightforward: it is the guaranteed frequency increment applied at every step. remainderStep is less obvious. At first glance, it looks like a minor patch for the leftover part of the division. One might think the remainder could simply be added at the end.
But the actual mechanism is more precise than that:
remainderAcc += remainderStep;
if (remainderAcc >= steps) {
currentFreq++;
remainderAcc -= steps;
}
At every step, the accumulator adds remainderStep. When the accumulator overflows past steps, the frequency receives one extra hertz of compensation, and the accumulator is reduced by steps.
This can feel counterintuitive. steps is a count of motion steps, while remainderStep comes from a frequency difference. Why are they being compared directly?
Looking at it as a phase accumulator
The trick is not to read the code purely as frequency arithmetic. It is better understood as phase accumulation.
DDA behaves like a phase accumulator. Imagine a circular dial with steps tick marks. On each motion step, the pointer advances by remainderStep ticks. Whenever the pointer completes a full revolution, one extra 1 Hz correction is emitted.
In other words:
- the physical fractional increment per step is
remainderStep / steps; - the accumulator stores the numerator,
remainderStep; - the overflow threshold is the denominator,
steps; - each overflow means the fractional phase has completed one full unit.
With that interpretation, remainderAcc -= steps is no longer subtracting two unrelated quantities. It is just a modulo operation that brings the accumulator back into the valid range [0, steps-1].
A concrete run-through
Take freqDiff = 2000 and steps = 3:
integerStep = 2000 / 3 = 666
remainderStep = 2000 % 3 = 2
The sequence becomes:
脉冲1:基础+666,Acc=0→2,不溢出,频率=1666
脉冲2:基础+666,Acc=2→4,溢出,Acc=1,补偿+1,频率=2333
脉冲3:基础+666,Acc=1→3,溢出,Acc=0,补偿+1,频率=3000
There are two compensation events in total, exactly equal to remainderStep. More importantly, they are spread across the timeline instead of being dumped into the last pulse. The second and third steps each receive one correction.
That is the strength of DDA here: with only integer operations, it produces frequency interpolation with floating-point-like accuracy, while keeping the instantaneous error within 1 Hz.
Why add remainderStep instead of 1?
Another easy misunderstanding is to think the accumulator should simply add 1 at every step:
脉冲1:Acc=1,不溢出
脉冲2:Acc=2,不溢出
脉冲3:Acc=3,溢出,补偿1次
That produces only one compensation event, but the calculation needs two. The result is still off by 1 Hz.
The reason is that remainderStep represents how many extra compensation events must occur over steps steps. If the accumulator only advances by 1 each time, one full cycle can trigger only one correction. To trigger remainderStep corrections within the same number of steps, the accumulator must advance by remainderStep each time.
A candy-sharing analogy makes the same idea clearer: there are 3 children (steps=3) and 2 candies (remainderStep=2) to distribute. Each time a child arrives, the counter increases by 2. If the counter reaches or exceeds 3, that child receives one extra candy and the counter subtracts 3.
小朋友1:Acc=2,不拿
小朋友2:Acc=4,拿糖,Acc=1
小朋友3:Acc=3,拿糖,Acc=0
The second and third children each get one candy. The distribution is even, and the total is exact.
The special case when starting from 0 Hz
There is also a small but important initialization detail:
if ((startFreq == 0u) && (endFreq != 0u) &&
(integerStep == 0u) && (remainderStep != 0u)) {
remainderAcc = steps - remainderStep;
}
When the starting frequency is 0 Hz and integerStep is also 0, the frequency change is smaller than the number of steps. Without special handling, the first generated frequency may still be 0 Hz. But 0 Hz means no pulse, and no pulse means the motor will not move.
By initializing the accumulator to a value that is already close to overflow, the first step can immediately advance to a positive frequency. It is a small boundary condition, but it prevents a deadlock that would otherwise be easy to miss.
Converging from ramp time to pulse count
DDA answers one question: how to distribute a frequency change evenly using integers. There is another question before that: given an acceleration or deceleration time, how many steps should the transition use?
The PlsrProfileGetTransitionSteps function handles this with iterative convergence:
- Estimate an initial step count using a continuous model:
expectedPulses = (startFreq + endFreq) * rampTimeMs / 2000 - Simulate the actual elapsed time under DDA for that step count.
- Correct the step count by time ratio:
adjustedSteps = steps * targetTime / actualTime - Repeat the simulation and correction until the count converges, with at most 4 iterations.
The nice part of this method is that it avoids trying to derive an exact closed-form solution for DDA step count, which is hardly practical. Instead, it uses simulation plus feedback to approach the target time. For engineering purposes, four iterations are already enough.
Falling back to a triangular profile
If the total number of pulses is not enough to build a full trapezoidal profile with acceleration, cruise, and deceleration, the generator falls back to a triangular profile. In that case, there is no constant-speed segment; the motion only accelerates and then decelerates.
PlsrProfileFindTriangleFreq uses binary search to find the peak frequency of that triangle. Under the constraint of the available pulse count, it searches for the highest reachable frequency such that the acceleration and deceleration steps together match the available steps.
The search bounds matter. In an acceleration-style case where the peak frequency is above both the start and end frequencies, the lower bound is the larger of the start and end frequencies, while the upper bound is the target frequency. In a deceleration-style case, the direction is reversed. This keeps the binary search inside a meaningful range instead of wasting iterations on impossible values.
The state machine around it
The profile generator itself is built around a simple state machine:
ACCEL → CRUISE → DECEL → COMPLETE
Each state advances one step, updates the current frequency, and then checks whether it should transition to the next state.
PlsrProfileAdvance is the external interface. Each call returns one frequency value. The caller only needs to program the timer with that frequency, and the result is a smooth pulse sequence for acceleration and deceleration.
Why the details matter
The code is not long, but each variable and helper function has a reason to exist. DDA uses plain integer arithmetic to perform accurate frequency interpolation. The iterative method turns a difficult timing problem into simulation plus feedback. The triangular-profile logic uses binary search to find the best feasible peak frequency under pulse-count constraints.
None of these techniques is new. DDA is closely related to the ideas behind Bresenham-style line drawing, iterative convergence is a standard numerical method, and binary search is as classic as algorithms get. The real value is in combining them carefully enough to solve a concrete embedded motion-control problem without losing precision at the edges.