/* * DDA.cpp * * Created on: 7 Dec 2014 * Author: David */ #include "RepRapFirmware.h" #ifdef DUET_NG # define DDA_MOVE_DEBUG (1) #else // On the wired Duets we don't have enough RAM to support this # define DDA_MOVE_DEBUG (0) #endif #if DDA_MOVE_DEBUG // Structure to hold the essential parameters of a move, for debugging struct MoveParameters { float accelDistance; float steadyDistance; float decelDistance; float startSpeed; float topSpeed; float endSpeed; MoveParameters() { accelDistance = steadyDistance = decelDistance = startSpeed = topSpeed = endSpeed = 0.0; } }; const size_t NumSavedMoves = 256; static MoveParameters savedMoves[NumSavedMoves]; static size_t savedMovePointer = 0; // Print the saved moves in CSV format for analysis /*static*/ void DDA::PrintMoves() { // Print the saved moved in CSV format for (size_t i = 0; i < NumSavedMoves; ++i) { const MoveParameters& m = savedMoves[savedMovePointer]; reprap.GetPlatform()->MessageF(DEBUG_MESSAGE, "%f,%f,%f,%f,%f,%f\n", m.accelDistance, m.steadyDistance, m.decelDistance, m.startSpeed, m.topSpeed, m.endSpeed); savedMovePointer = (savedMovePointer + 1) % NumSavedMoves; } } #else /*static*/ void DDA::PrintMoves() { } #endif DDA::DDA(DDA* n) : next(n), prev(nullptr), state(empty) { memset(ddm, 0, sizeof(ddm)); //DEBUG to clear stepError field } // Return the number of clocks this DDA still needs to execute. // This could be slightly negative, if the move is overdue for completion. int32_t DDA::GetTimeLeft() const pre(state == executing || state == frozen || state == completed) { return (state == completed) ? 0 : (state == executing) ? (int32_t)(moveStartTime + clocksNeeded - Platform::GetInterruptClocks()) : (int32_t)clocksNeeded; } void DDA::DebugPrintVector(const char *name, const float *vec, size_t len) const { debugPrintf("%s=", name); for (size_t i = 0; i < len; ++i) { debugPrintf("%c%f", ((i == 0) ? '[' : ' '), vec[i]); } debugPrintf("]"); } void DDA::DebugPrint() const { const size_t numAxes = reprap.GetGCodes()->GetNumAxes(); debugPrintf("DDA:"); if (endCoordinatesValid) { float startCoordinates[MAX_AXES]; for (size_t i = 0; i < numAxes; ++i) { startCoordinates[i] = endCoordinates[i] - (totalDistance * directionVector[i]); } DebugPrintVector(" start", startCoordinates, numAxes); DebugPrintVector(" end", endCoordinates, numAxes); } debugPrintf(" d=%f", totalDistance); DebugPrintVector(" vec", directionVector, 5); debugPrintf("\na=%f reqv=%f topv=%f startv=%f endv=%f\n" "daccel=%f ddecel=%f cks=%u\n", acceleration, requestedSpeed, topSpeed, startSpeed, endSpeed, accelDistance, decelDistance, clocksNeeded); for (size_t axis = 0; axis < numAxes; ++axis) { ddm[axis].DebugPrint(reprap.GetGCodes()->axisLetters[axis], isDeltaMovement); } for (size_t i = numAxes; i < DRIVES; ++i) { if (ddm[i].state != DMState::idle) { ddm[i].DebugPrint((char)('0' + (i - numAxes)), false); } } } // This is called by Move to initialize all DDAs void DDA::Init() { // Set the endpoints to zero, because Move asks for them. // They will be wrong if we are on a delta. We take care of that when we process the M665 command in config.g. for (size_t drive = 0; drive < DRIVES; ++drive) { endPoint[drive] = 0; ddm[drive].state = DMState::idle; } state = empty; endCoordinatesValid = false; } // Set up a real move. Return true if it represents real movement, else false. bool DDA::Init(const GCodes::RawMove &nextMove, bool doMotorMapping) { // 1. Compute the new endpoints and the movement vector const int32_t *positionNow = prev->DriveCoordinates(); const Move *move = reprap.GetMove(); if (doMotorMapping) { move->MotorTransform(nextMove.coords, endPoint); // transform the axis coordinates if on a delta or CoreXY printer isDeltaMovement = move->IsDeltaMode() && (endPoint[X_AXIS] != positionNow[X_AXIS] || endPoint[Y_AXIS] != positionNow[Y_AXIS] || endPoint[Z_AXIS] != positionNow[Z_AXIS]); } else { isDeltaMovement = false; } isPrintingMove = false; bool realMove = false, xyMoving = false; const bool isSpecialDeltaMove = (move->IsDeltaMode() && !doMotorMapping); float accelerations[DRIVES]; const float *normalAccelerations = reprap.GetPlatform()->Accelerations(); const size_t numAxes = reprap.GetGCodes()->GetNumAxes(); for (size_t drive = 0; drive < DRIVES; drive++) { accelerations[drive] = normalAccelerations[drive]; if (drive >= numAxes || !doMotorMapping) { endPoint[drive] = Move::MotorEndPointToMachine(drive, nextMove.coords[drive]); } int32_t delta; if (drive < numAxes) { endCoordinates[drive] = nextMove.coords[drive]; delta = endPoint[drive] - positionNow[drive]; } else { delta = endPoint[drive]; } DriveMovement& dm = ddm[drive]; if (drive < numAxes && !isSpecialDeltaMove) { directionVector[drive] = nextMove.coords[drive] - prev->GetEndCoordinate(drive, false); dm.state = (isDeltaMovement || delta != 0) ? DMState::moving // on a delta printer, if one tower moves then we assume they all do : DMState::idle; } else { directionVector[drive] = (float)delta/reprap.GetPlatform()->DriveStepsPerUnit(drive); dm.state = (delta != 0) ? DMState::moving : DMState::idle; } if (dm.state == DMState::moving) { dm.totalSteps = labs(delta); // for now this is the number of net steps, but gets adjusted later if there is a reverse in direction dm.direction = (delta >= 0); // for now this is the direction of net movement, but gets adjusted later if it is a delta movement realMove = true; if (drive < numAxes && drive != Z_AXIS) { xyMoving = true; } if (drive >= numAxes && xyMoving) { if (delta > 0) { isPrintingMove = true; // we have both movement and extrusion } const float compensationTime = reprap.GetPlatform()->GetPressureAdvance(drive); if (compensationTime > 0.0) { // Compensation causes instant velocity changes equal to acceleration * k, so we may need to limit the acceleration accelerations[drive] = min(accelerations[drive], reprap.GetPlatform()->ConfiguredInstantDv(drive)/compensationTime); } } } } // 2. Throw it away if there's no real movement. if (!realMove) { return false; } // 3. Store some values endStopsToCheck = nextMove.endStopsToCheck; filePos = nextMove.filePos; usePressureAdvance = nextMove.usePressureAdvance; hadLookaheadUnderrun = false; // The end coordinates will be valid at the end of this move if it does not involve endstop checks and is not a special move on a delta printer endCoordinatesValid = (endStopsToCheck == 0) && (doMotorMapping || !move->IsDeltaMode()); // 4. Normalise the direction vector and compute the amount of motion. // If there is any XYZ movement, then we normalise it so that the total XYZ movement has unit length. // This means that the user gets the feed rate that he asked for. It also makes the delta calculations simpler. if (xyMoving || ddm[Z_AXIS].state == DMState::moving) { if (isDeltaMovement) { // Add on the Z movement needed to compensate for bed tilt const DeltaParameters& dparams = move->GetDeltaParams(); directionVector[Z_AXIS] += (directionVector[X_AXIS] * dparams.GetXTilt()) + (directionVector[Y_AXIS] * dparams.GetYTilt()); } totalDistance = Normalise(directionVector, DRIVES, numAxes); if (isDeltaMovement) { // The following are only needed when doing delta movements. We could defer computing them until Prepare(), which would make simulation faster. a2plusb2 = fsquare(directionVector[X_AXIS]) + fsquare(directionVector[Y_AXIS]); cKc = (int32_t)(directionVector[Z_AXIS] * DriveMovement::Kc); const float initialX = prev->GetEndCoordinate(X_AXIS, false); const float initialY = prev->GetEndCoordinate(Y_AXIS, false); const DeltaParameters& dparams = move->GetDeltaParams(); const float diagonalSquared = fsquare(dparams.GetDiagonal()); const float a2b2D2 = a2plusb2 * diagonalSquared; for (size_t drive = 0; drive < DELTA_AXES; ++drive) { const float A = initialX - dparams.GetTowerX(drive); const float B = initialY - dparams.GetTowerY(drive); const float stepsPerMm = reprap.GetPlatform()->DriveStepsPerUnit(drive); DriveMovement& dm = ddm[drive]; const float aAplusbB = A * directionVector[X_AXIS] + B * directionVector[Y_AXIS]; const float dSquaredMinusAsquaredMinusBsquared = diagonalSquared - fsquare(A) - fsquare(B); float h0MinusZ0 = sqrtf(dSquaredMinusAsquaredMinusBsquared); dm.mp.delta.hmz0sK = (int32_t)(h0MinusZ0 * stepsPerMm * DriveMovement::K2); dm.mp.delta.minusAaPlusBbTimesKs = -(int32_t)(aAplusbB * stepsPerMm * DriveMovement::K2); dm.mp.delta.dSquaredMinusAsquaredMinusBsquaredTimesKsquaredSsquared = (int64_t)(dSquaredMinusAsquaredMinusBsquared * fsquare(stepsPerMm * DriveMovement::K2)); // Calculate the distance at which we need to reverse direction. if (a2plusb2 <= 0.0) { // Pure Z movement. We can't use the main calculation because it divides by a2plusb2. dm.direction = (directionVector[Z_AXIS] >= 0.0); dm.mp.delta.reverseStartStep = dm.totalSteps + 1; } else { // The distance to reversal is the solution to a quadratic equation. One root corresponds to the carriages being above the bed, // the other root corresponds to the carriages being above the bed. const float drev = ((directionVector[Z_AXIS] * sqrt(a2b2D2 - fsquare(A * directionVector[Y_AXIS] - B * directionVector[X_AXIS]))) - aAplusbB)/a2plusb2; if (drev > 0.0 && drev < totalDistance) // if the reversal point is within range { // Calculate how many steps we need to move up before reversing const float hrev = directionVector[Z_AXIS] * drev + sqrt(dSquaredMinusAsquaredMinusBsquared - 2 * drev * aAplusbB - a2plusb2 * fsquare(drev)); int32_t numStepsUp = (int32_t)((hrev - h0MinusZ0) * stepsPerMm); // We may be almost at the peak height already, in which case we don't really have a reversal. if (numStepsUp < 1 || (dm.direction && (uint32_t)numStepsUp <= dm.totalSteps)) { dm.mp.delta.reverseStartStep = dm.totalSteps + 1; } else { dm.mp.delta.reverseStartStep = (uint32_t)numStepsUp + 1; // Correct the initial direction and the total number of steps if (dm.direction) { // Net movement is up, so we will go up a bit and then down by a lesser amount dm.totalSteps = (2 * numStepsUp) - dm.totalSteps; } else { // Net movement is down, so we will go up first and then down by a greater amount dm.direction = true; dm.totalSteps = (2 * numStepsUp) + dm.totalSteps; } } } else { dm.mp.delta.reverseStartStep = dm.totalSteps + 1; } } } } } else { totalDistance = Normalise(directionVector, DRIVES, DRIVES); } // 5. Compute the maximum acceleration available and maximum top speed float normalisedDirectionVector[DRIVES]; // Used to hold a unit-length vector in the direction of motion memcpy(normalisedDirectionVector, directionVector, sizeof(normalisedDirectionVector)); Absolute(normalisedDirectionVector, DRIVES); acceleration = VectorBoxIntersection(normalisedDirectionVector, accelerations, DRIVES); // Set the speed to the smaller of the requested and maximum speed. // Also enforce a minimum speed of 0.5mm/sec. We need a minimum speed to avoid overflow in the movement calculations. float reqSpeed = nextMove.feedRate; if (isSpecialDeltaMove) { // Special case of a raw or homing move on a delta printer // We use the Cartesian motion system to implement these moves, so the feed rate will be interpreted in Cartesian coordinates. // This is wrong, we want the feed rate to apply to the drive that is moving the farthest. float maxDistance = 0.0; for (size_t axis = 0; axis < DELTA_AXES; ++axis) { if (normalisedDirectionVector[axis] > maxDistance) { maxDistance = normalisedDirectionVector[axis]; } } if (maxDistance != 0.0) // should always be true { reqSpeed /= maxDistance; // because normalisedDirectionVector is unit-normalised } } requestedSpeed = max(0.5, min(reqSpeed, VectorBoxIntersection(normalisedDirectionVector, reprap.GetPlatform()->MaxFeedrates(), DRIVES))); // On a Cartesian or CoreXY printer, it is OK to limit the X and Y speeds and accelerations independently, and in consequence to allow greater values // for diagonal moves. On a delta, this is not OK and any movement in the XY plane should be limited to the X/Y axis values, which we assume to be equal. if (isDeltaMovement) { const float xyFactor = sqrt(fsquare(normalisedDirectionVector[X_AXIS]) + fsquare(normalisedDirectionVector[X_AXIS])); const float maxSpeed = reprap.GetPlatform()->MaxFeedrates()[X_AXIS]; if (requestedSpeed * xyFactor > maxSpeed) { requestedSpeed = maxSpeed/xyFactor; } const float maxAcceleration = normalAccelerations[X_AXIS]; if (acceleration * xyFactor > maxAcceleration) { acceleration = maxAcceleration/xyFactor; } } // 6. Calculate the provisional accelerate and decelerate distances and the top speed endSpeed = 0.0; // until the next move asks us to adjust it if (prev->state != provisional) { // There is no previous move that we can adjust, so this move must start at zero speed. startSpeed = 0.0; } else { // Try to meld this move to the previous move to avoid stop/start // Assuming that this move ends with zero speed, calculate the maximum possible starting speed: u^2 = v^2 - 2as prev->targetNextSpeed = min(sqrtf(acceleration * totalDistance * 2.0), requestedSpeed); DoLookahead(prev); startSpeed = prev->targetNextSpeed; } RecalculateMove(); state = provisional; return true; } float DDA::GetMotorPosition(size_t drive) const { return Move::MotorEndpointToPosition(endPoint[drive], drive); } // Return true if this move is or might have been intended to be a deceleration-only move // A move planned as a deceleration-only move may have a short acceleration segment at the start because of rounding error // We declare this inline because it is only used once, in DDA::DoLookahead inline bool DDA::IsDecelerationMove() const { return decelDistance == totalDistance // the simple case - is a deceleration-only move || (topSpeed < requestedSpeed // can't have been intended as deceleration-only if it reaches the requested speed && decelDistance > 0.98 * totalDistance // rounding error can only go so far && prev->state == DDA::provisional // if we can't adjust the previous move then we don't care (and its figures may not be reliable if it has been recycled already) && prev->decelDistance > 0.0); // if the previous move has no deceleration phase then no point in adjus6ting it } // Try to increase the ending speed of this move to allow the next move to start at targetNextSpeed /*static*/ void DDA::DoLookahead(DDA *laDDA) pre(state == provisional) { // if (reprap.Debug(moduleDda)) debugPrintf("Adjusting, %f\n", laDDA->targetNextSpeed); unsigned int laDepth = 0; bool goingUp = true; for(;;) // this loop is used to nest lookahead without making recursive calls { bool recurse = false; if (goingUp) { // We have been asked to adjust the end speed of this move to match the next move starting at targetNextSpeed if (laDDA->topSpeed >= laDDA->requestedSpeed) { // This move already reaches its top speed, so just need to adjust the deceleration part laDDA->endSpeed = laDDA->requestedSpeed; // remove the deceleration phase laDDA->CalcNewSpeeds(); // put it back if necessary } else if (laDDA->IsDecelerationMove()) { // This is a deceleration-only move, so we may have to adjust the previous move as well to get optimum behaviour if (laDDA->prev->state == provisional) { laDDA->endSpeed = laDDA->requestedSpeed; laDDA->CalcNewSpeeds(); const float maxStartSpeed = sqrtf(fsquare(laDDA->endSpeed) + (2 * laDDA->acceleration * laDDA->totalDistance)); if (maxStartSpeed < laDDA->requestedSpeed) { laDDA->prev->targetNextSpeed = maxStartSpeed; // Still provisionally a decelerate-only move } else { laDDA->prev->targetNextSpeed = laDDA->requestedSpeed; } recurse = true; } else { // This move is a deceleration-only move but we can't adjust the previous one laDDA->hadLookaheadUnderrun = true; laDDA->endSpeed = min(sqrtf(fsquare(laDDA->startSpeed) + (2 * laDDA->acceleration * laDDA->totalDistance)), laDDA->requestedSpeed); laDDA->CalcNewSpeeds(); } } else { // This move doesn't reach its requested speed, but it isn't a deceleration-only move // Set its end speed to the minimum of the requested speed and the highest we can reach laDDA->endSpeed = min(sqrtf(fsquare(laDDA->startSpeed) + (2 * laDDA->acceleration * laDDA->totalDistance)), laDDA->requestedSpeed); laDDA->CalcNewSpeeds(); } } else { // Going back down the list // We have adjusted the end speed of the previous move as much as is possible and it has adjusted its targetNextSpeed accordingly. // Adjust this move to match it. laDDA->startSpeed = laDDA->prev->targetNextSpeed; const float maxEndSpeed = sqrtf(fsquare(laDDA->startSpeed) + (2 * laDDA->acceleration * laDDA->totalDistance)); if (maxEndSpeed < laDDA->endSpeed) { // Oh dear, we were too optimistic! Have another go. laDDA->endSpeed = maxEndSpeed; laDDA->CalcNewSpeeds(); } } if (recurse) { // Still going up laDDA = laDDA->prev; ++laDepth; if (reprap.Debug(moduleDda)) debugPrintf("Recursion start %u\n", laDepth); } else { // Either just stopped going up. or going down laDDA->RecalculateMove(); if (laDepth == 0) { // if (reprap.Debug(moduleDda)) debugPrintf("Complete, %f\n", laDDA->targetNextSpeed); return; } laDDA = laDDA->next; --laDepth; goingUp = false; } } } // Recalculate the top speed, acceleration distance and deceleration distance, and whether we can pause after this move // This may cause a move that we intended to be a deceleration-only move to have a tiny acceleration segment at the start void DDA::RecalculateMove() { accelDistance = (fsquare(requestedSpeed) - fsquare(startSpeed))/(2.0 * acceleration); decelDistance = (fsquare(requestedSpeed) - fsquare(endSpeed))/(2.0 * acceleration); if (accelDistance + decelDistance >= totalDistance) { // This move has no steady-speed phase, so it's accelerate-decelerate or accelerate-only move. // If V is the peak speed, then (V^2 - u^2)/2a + (V^2 - v^2)/2a = distance. // So (2V^2 - u^2 - v^2)/2a = distance // So V^2 = a * distance + 0.5(u^2 + v^2) float vsquared = (acceleration * totalDistance) + 0.5 * (fsquare(startSpeed) + fsquare(endSpeed)); // Calculate accelerate distance from: V^2 = u^2 + 2as if (vsquared >= 0.0) { accelDistance = max((vsquared - fsquare(startSpeed))/(2.0 * acceleration), 0.0); decelDistance = totalDistance - accelDistance; topSpeed = sqrtf(vsquared); } else if (startSpeed < endSpeed) { // This would ideally never happen, but might because of rounding errors accelDistance = totalDistance; decelDistance = 0.0; topSpeed = endSpeed; } else { // This would ideally never happen, but might because of rounding errors accelDistance = 0.0; decelDistance = totalDistance; topSpeed = startSpeed; } } else { topSpeed = requestedSpeed; } canPause = (endStopsToCheck == 0); if (canPause && endSpeed != 0.0) { const Platform *p = reprap.GetPlatform(); for (size_t drive = 0; drive < DRIVES; ++drive) { if (ddm[drive].state == DMState::moving && endSpeed * fabsf(directionVector[drive]) > p->ActualInstantDv(drive)) { canPause = false; break; } } } } // Decide what speed we would really like this move to end at. // On entry, endSpeed is our proposed ending speed and targetNextSpeed is the proposed starting speed of the next move // On return, targetNextSpeed is the speed we would like the next move to start at, and endSpeed is the corresponding end speed of this move. void DDA::CalcNewSpeeds() { // We may have to make multiple passes, because reducing one of the speeds may solve some problems but actually make matters worse on another axis. bool limited; do { // debugPrintf(" Pass, start=%f end=%f\n", targetStartSpeed, endSpeed); limited = false; for (size_t drive = 0; drive < DRIVES; ++drive) { const float thisMoveFraction = directionVector[drive]; const float nextMoveFraction = next->directionVector[drive]; const DriveMovement& thisMoveDm = ddm[drive]; const DriveMovement& nextMoveDm = next->ddm[drive]; if (thisMoveDm.state == DMState::moving || nextMoveDm.state == DMState::moving) { const float thisMoveSpeed = endSpeed * thisMoveFraction; const float nextMoveSpeed = targetNextSpeed * nextMoveFraction; const float idealDeltaV = fabsf(thisMoveSpeed - nextMoveSpeed); float maxDeltaV = reprap.GetPlatform()->ActualInstantDv(drive); if (idealDeltaV > maxDeltaV) { // This drive can't change speed fast enough, so reduce the start and/or end speeds // This algorithm sometimes converges very slowly, requiring many passes. // To ensure it converges at all, and to speed up convergence, we over-adjust the speed to achieve an even lower deltaV. maxDeltaV *= 0.8; if ((thisMoveFraction >= 0.0) == (nextMoveFraction >= 0.0)) { // Drive moving in the same direction for this move and the next one, so we must reduce speed of the faster one if (fabsf(thisMoveSpeed) > fabsf(nextMoveSpeed)) { endSpeed = (fabsf(nextMoveSpeed) + maxDeltaV)/fabsf(thisMoveFraction); } else { targetNextSpeed = (fabsf(thisMoveSpeed) + maxDeltaV)/fabsf(nextMoveFraction); } } else if (fabsf(thisMoveSpeed) * 2 < maxDeltaV) { targetNextSpeed = (maxDeltaV - fabsf(thisMoveSpeed))/fabsf(nextMoveFraction); } else if (fabsf(nextMoveSpeed) * 2 < maxDeltaV) { endSpeed = (maxDeltaV - fabsf(nextMoveSpeed))/fabsf(thisMoveFraction); } else { targetNextSpeed = maxDeltaV/(2 * fabsf(nextMoveFraction)); endSpeed = maxDeltaV/(2 * fabsf(thisMoveFraction)); } limited = true; // Most conflicts are between X and Y. So if we just did Y, start another pass immediately to save time. if (drive == 1) { break; } } } } } while (limited); } // This is called by Move::CurrentMoveCompleted to update the live coordinates from the move that has just finished bool DDA::FetchEndPosition(volatile int32_t ep[DRIVES], volatile float endCoords[MAX_AXES]) { for (size_t drive = 0; drive < DRIVES; ++drive) { ep[drive] = endPoint[drive]; } if (endCoordinatesValid) { const size_t numAxes = reprap.GetGCodes()->GetNumAxes(); for (size_t axis = 0; axis < numAxes; ++axis) { endCoords[axis] = endCoordinates[axis]; } } return endCoordinatesValid; } void DDA::SetPositions(const float move[DRIVES], size_t numDrives) { reprap.GetMove()->EndPointToMachine(move, endPoint, numDrives); const size_t numAxes = reprap.GetGCodes()->GetNumAxes(); for (size_t axis = 0; axis < numAxes; ++axis) { endCoordinates[axis] = move[axis]; } endCoordinatesValid = true; } // Get a Cartesian end coordinate from this move float DDA::GetEndCoordinate(size_t drive, bool disableDeltaMapping) pre(disableDeltaMapping || drive < MAX_AXES) { if (disableDeltaMapping) { return Move::MotorEndpointToPosition(endPoint[drive], drive); } else { const size_t numAxes = reprap.GetGCodes()->GetNumAxes(); if (drive < numAxes && !endCoordinatesValid) { reprap.GetMove()->MachineToEndPoint(endPoint, endCoordinates, numAxes); endCoordinatesValid = true; } return endCoordinates[drive]; } } // Calculate the time needed for this move. Called instead of Prepare when we are in simulation mode. float DDA::CalcTime() const { return (topSpeed - startSpeed)/acceleration // acceleration time + (totalDistance - accelDistance - decelDistance)/topSpeed // steady speed time + (topSpeed - endSpeed)/acceleration; // deceleration time } // Prepare this DDA for execution. // This must not be called with interrupts disabled, because it calls Platform::EnableDrive. void DDA::Prepare() { //debugPrintf("Prep\n"); PrepParams params; params.decelStartDistance = totalDistance - decelDistance; // Convert the accelerate/decelerate distances to times float accelStopTime = (topSpeed - startSpeed)/acceleration; float decelStartTime = accelStopTime + (params.decelStartDistance - accelDistance)/topSpeed; float totalTime = decelStartTime + (topSpeed - endSpeed)/acceleration; // Enforce the maximum average acceleration if (isPrintingMove && topSpeed > startSpeed && topSpeed > endSpeed) { const float maxAverageAcceleration = reprap.GetPlatform()->GetMaxAverageAcceleration(); if (2 * topSpeed - startSpeed - endSpeed > totalTime * maxAverageAcceleration) { // Reduce the top speed to comply with the maximum average acceleration const float a2 = fsquare(acceleration); const float am2 = fsquare(maxAverageAcceleration); const float aam = acceleration * maxAverageAcceleration; const float discriminant = (a2 + (2 * aam) - am2) * (fsquare(startSpeed) + fsquare(endSpeed)) + (2 * a2 + 2 * am2 - 4 * aam) * startSpeed * endSpeed + (8 * a2 * maxAverageAcceleration - 4 * acceleration * am2) * totalDistance; const float oldTopSpeed = topSpeed; if (discriminant < 0.0) { topSpeed = max(startSpeed, endSpeed); } else { const float temp = (sqrtf(discriminant) + (acceleration - maxAverageAcceleration) * (startSpeed + endSpeed)) /(4 * acceleration - 2 * maxAverageAcceleration); topSpeed = max(max(temp, startSpeed), endSpeed); } //DEBUG debugPrintf("ots %f nts %f ss %f es %f\n", oldTopSpeed, topSpeed, startSpeed, endSpeed); // Recalculate parameters accelDistance = (fsquare(topSpeed) - fsquare(startSpeed))/(2 * acceleration); decelDistance = (fsquare(topSpeed) - fsquare(endSpeed))/(2 * acceleration); params.decelStartDistance = totalDistance - decelDistance; accelStopTime = (topSpeed - startSpeed)/acceleration; decelStartTime = accelStopTime + (params.decelStartDistance - accelDistance)/topSpeed; totalTime = decelStartTime + (topSpeed - endSpeed)/acceleration; } } clocksNeeded = (uint32_t)(totalTime * stepClockRate); params.startSpeedTimesCdivA = (uint32_t)((startSpeed * stepClockRate)/acceleration); params.topSpeedTimesCdivA = (uint32_t)((topSpeed * stepClockRate)/acceleration); params.decelStartClocks = (uint32_t)(decelStartTime * stepClockRate); params.topSpeedTimesCdivAPlusDecelStartClocks = params.topSpeedTimesCdivA + params.decelStartClocks; params.accelClocksMinusAccelDistanceTimesCdivTopSpeed = (uint32_t)((accelStopTime - (accelDistance/topSpeed)) * stepClockRate); params.compFactor = 1.0 - startSpeed/topSpeed; goingSlow = false; firstDM = nullptr; const size_t numAxes = reprap.GetGCodes()->GetNumAxes(); for (size_t drive = 0; drive < DRIVES; ++drive) { DriveMovement& dm = ddm[drive]; if (dm.state == DMState::moving) { dm.drive = drive; reprap.GetPlatform()->EnableDrive(drive); if (drive >= numAxes) { dm.PrepareExtruder(*this, params, usePressureAdvance); // Check for sensible values, print them if they look dubious if (reprap.Debug(moduleDda) && ( dm.totalSteps > 1000000 || dm.mp.cart.reverseStartStep < dm.mp.cart.decelStartStep || (dm.mp.cart.reverseStartStep <= dm.totalSteps && dm.mp.cart.fourMaxStepDistanceMinusTwoDistanceToStopTimesCsquaredDivA > (int64_t)(dm.mp.cart.twoCsquaredTimesMmPerStepDivA * dm.mp.cart.reverseStartStep)) ) ) { DebugPrint(); } } else if (isDeltaMovement) { dm.PrepareDeltaAxis(*this, params); // Check for sensible values, print them if they look dubious if (reprap.Debug(moduleDda) && dm.totalSteps > 1000000) { DebugPrint(); } } else { dm.PrepareCartesianAxis(*this, params); // Check for sensible values, print them if they look dubious if (reprap.Debug(moduleDda) && dm.totalSteps > 1000000) { DebugPrint(); } } // Prepare for the first step dm.nextStep = 0; dm.nextStepTime = 0; dm.stepInterval = 999999; // initialise to a large value so that we will calculate the time for just one step dm.stepsTillRecalc = 0; // so that we don't skip the calculation bool stepsToDo = (isDeltaMovement && drive < numAxes) ? dm.CalcNextStepTimeDelta(*this, false) : dm.CalcNextStepTimeCartesian(*this, false); if (stepsToDo) { InsertDM(&dm); } else { dm.state = DMState::idle; } } } if (reprap.Debug(moduleDda) && reprap.Debug(moduleMove)) // temp show the prepared DDA if debug enabled for both modules { DebugPrint(); } #if DDA_MOVE_DEBUG MoveParameters& m = savedMoves[savedMovePointer]; m.accelDistance = accelDistance; m.decelDistance = decelDistance; m.steadyDistance = totalDistance - accelDistance - decelDistance; m.startSpeed = startSpeed; m.topSpeed = topSpeed; m.endSpeed = endSpeed; savedMovePointer = (savedMovePointer + 1) % NumSavedMoves; #endif state = frozen; // must do this last so that the ISR doesn't start executing it before we have finished setting it up } // The remaining functions are speed-critical, so use full optimisation #pragma GCC optimize ("O3") // Start executing this move, returning true if Step() needs to be called immediately. Must be called with interrupts disabled, to avoid a race condition. // Returns true if the caller needs to call the step ISR immediately. bool DDA::Start(uint32_t tim) pre(state == frozen) { moveStartTime = tim; state = executing; if (firstDM == nullptr) { // No steps are pending. This should not happen! return true; // schedule another interrupt immediately } else { unsigned int extrusions = 0, retractions = 0; // bitmaps of extruding and retracting drives const size_t numAxes = reprap.GetGCodes()->GetNumAxes(); for (size_t i = 0; i < DRIVES; ++i) { DriveMovement& dm = ddm[i]; if (dm.state == DMState::moving) { reprap.GetPlatform()->SetDirection(i, dm.direction); if (i >= numAxes) { if (dm.direction == FORWARDS) { extrusions |= (1 << (i - numAxes)); } else { retractions |= (1 << (i - numAxes)); } } } } bool extruding = false; if (extrusions != 0 || retractions != 0) { const unsigned int prohibitedMovements = reprap.GetProhibitedExtruderMovements(extrusions, retractions); for (DriveMovement **dmpp = &firstDM; *dmpp != nullptr; ) { bool thisDriveExtruding = (*dmpp)->drive >= numAxes; if (thisDriveExtruding && (prohibitedMovements & (1 << ((*dmpp)->drive - numAxes))) != 0) { *dmpp = (*dmpp)->nextDM; } else { extruding = extruding || thisDriveExtruding; dmpp = &((*dmpp)->nextDM); } } } Platform *platform = reprap.GetPlatform(); if (extruding) { platform->ExtrudeOn(); } else { platform->ExtrudeOff(); } if (firstDM != nullptr) { return platform->ScheduleInterrupt(firstDM->nextStepTime + moveStartTime); } else { return true; } } } extern uint32_t maxReps; // This is called by the interrupt service routine to execute steps. // It returns true if it needs to be called again on the DDA of the new current move, otherwise false. // This must be as fast as possible, because it determines the maximum movement speed. bool DDA::Step() { Platform *platform = reprap.GetPlatform(); uint32_t lastStepPulseTime = platform->GetInterruptClocks(); bool repeat; uint32_t numReps = 0; do { // Keep this loop as fast as possible, in the case that there are no endstops to check! // 1. Check endstop switches and Z probe if asked. This is not speed critical because fast moves do not use endstops or the Z probe. if (endStopsToCheck != 0) // if any homing switches or the Z probe is enabled in this move { if ((endStopsToCheck & ZProbeActive) != 0) // if the Z probe is enabled in this move { // Check whether the Z probe has been triggered. On a delta at least, this must be done separately from endstop checks, // because we have both a high endstop and a Z probe, and the Z motor is not the same thing as the Z axis. switch (platform->GetZProbeResult()) { case EndStopHit::lowHit: MoveAborted(); // set the state to completed and recalculate the endpoints reprap.GetMove()->ZProbeTriggered(this); break; case EndStopHit::lowNear: ReduceHomingSpeed(); break; default: break; } } const size_t numAxes = reprap.GetGCodes()->GetNumAxes(); for (size_t drive = 0; drive < numAxes; ++drive) { if ((endStopsToCheck & (1 << drive)) != 0) { switch(platform->Stopped(drive)) { case EndStopHit::lowHit: endStopsToCheck &= ~(1 << drive); // clear this check so that we can check for more if (endStopsToCheck == 0 || reprap.GetMove()->IsCoreXYAxis(drive)) // if no more endstops to check, or this axis uses shared motors { MoveAborted(); } else { StopDrive(drive); } reprap.GetMove()->HitLowStop(drive, this); break; case EndStopHit::highHit: endStopsToCheck &= ~(1 << drive); // clear this check so that we can check for more if (endStopsToCheck == 0 || reprap.GetMove()->IsCoreXYAxis(drive)) // if no more endstops to check, or this axis uses shared motors { MoveAborted(); } else { StopDrive(drive); } reprap.GetMove()->HitHighStop(drive, this); break; case EndStopHit::lowNear: // Only reduce homing speed if there are no more axes to be homed. // This allows us to home X and Y simultaneously. if (endStopsToCheck == (1 << drive)) { ReduceHomingSpeed(); } break; default: break; } } } if (state == completed) // we may have completed the move due to triggering an endstop switch or Z probe { break; } } // 2. Determine which drivers are due for stepping, overdue, or will be due very shortly DriveMovement* dm = firstDM; const uint32_t elapsedTime = (Platform::GetInterruptClocks() - moveStartTime) + minInterruptInterval; uint32_t driversStepping = 0; while (dm != nullptr && elapsedTime >= dm->nextStepTime) // if the next step is due { ++numReps; driversStepping |= platform->GetDriversBitmap(dm->drive); dm = dm->nextDM; //uint32_t t3 = Platform::GetInterruptClocks() - t2; //if (t3 > maxCalcTime) maxCalcTime = t3; //if (t3 < minCalcTime) minCalcTime = t3; } // 3. Step the drivers if ((driversStepping & platform->GetSlowDrivers()) != 0) { while (Platform::GetInterruptClocks() - lastStepPulseTime < platform->GetSlowDriverClocks()) {} Platform::StepDriversHigh(driversStepping); // generate the steps lastStepPulseTime = Platform::GetInterruptClocks(); } else { Platform::StepDriversHigh(driversStepping); // generate the steps } // 4. Remove those drives from the list, calculate the next step times, update the direction pins where necessary, // and re-insert them so as to keep the list in step-time order. We assume that meeting the direction pin hold time // is not a problem for any driver type. This is not necessarily true. DriveMovement *dmToInsert = firstDM; // head of the chain we need to re-insert firstDM = dm; // remove the chain from the list while (dmToInsert != dm) // note that both of these may be nullptr { const bool hasMoreSteps = (isDeltaMovement && dmToInsert->drive < DELTA_AXES) ? dmToInsert->CalcNextStepTimeDelta(*this, true) : dmToInsert->CalcNextStepTimeCartesian(*this, true); DriveMovement * const nextToInsert = dmToInsert->nextDM; if (hasMoreSteps) { InsertDM(dmToInsert); } dmToInsert = nextToInsert; } // 5. Reset all step pins low if ((driversStepping & platform->GetSlowDrivers()) != 0) { while (Platform::GetInterruptClocks() - lastStepPulseTime < platform->GetSlowDriverClocks()) {} Platform::StepDriversLow(); // set all step pins low lastStepPulseTime = Platform::GetInterruptClocks(); } else { Platform::StepDriversLow(); // set all step pins low } // 6. Check for move completed if (firstDM == nullptr) { state = completed; break; } // 7. Schedule next interrupt, or if it would be too soon, generate more steps immediately repeat = platform->ScheduleInterrupt(firstDM->nextStepTime + moveStartTime); } while (repeat); if (numReps > maxReps) { maxReps = numReps; } if (state == completed) { uint32_t finishTime = moveStartTime + clocksNeeded; // calculate how long this move should take Move *move = reprap.GetMove(); move->CurrentMoveCompleted(); // tell Move that the current move is complete return move->TryStartNextMove(finishTime); // schedule the next move } return false; } // Stop a drive and re-calculate the corresponding endpoint void DDA::StopDrive(size_t drive) { DriveMovement& dm = ddm[drive]; if (dm.state == DMState::moving) { int32_t stepsLeft = dm.totalSteps - dm.nextStep + 1; if (dm.direction) { endPoint[drive] -= stepsLeft; // we were going forwards } else { endPoint[drive] += stepsLeft; // we were going backwards } dm.state = DMState::idle; if (drive < reprap.GetGCodes()->GetNumAxes()) { endCoordinatesValid = false; // the XYZ position is no longer valid } RemoveDM(drive); if (firstDM == nullptr) { state = completed; } } } // This is called when we abort a move because we have hit an endstop. // It adjusts the end points of the current move to account for how far through the move we got. void DDA::MoveAborted() { for (size_t drive = 0; drive < DRIVES; ++drive) { StopDrive(drive); } state = completed; } // Reduce the speed of this move to the indicated speed. // This is called from the ISR, so interrupts are disabled and nothing else can mess with us. // As this is only called for homing moves and with very low speeds, we assume that we don't need acceleration or deceleration phases. void DDA::ReduceHomingSpeed() { if (!goingSlow) { goingSlow = true; const float factor = 3.0; // the factor by which we are reducing the speed topSpeed /= factor; for (size_t drive = 0; drive < DRIVES; ++drive) { DriveMovement& dm = ddm[drive]; if (dm.state == DMState::moving) { dm.ReduceSpeed(*this, factor); RemoveDM(dm.drive); InsertDM(&dm); } } // We also need to adjust the total clocks needed, to prevent step errors being recorded const uint32_t clocksSoFar = Platform::GetInterruptClocks() - moveStartTime; if (clocksSoFar < clocksNeeded) { const uint32_t clocksLeft = clocksNeeded - clocksSoFar; clocksNeeded += (uint32_t)(clocksLeft * (factor - 1.0)); } } } bool DDA::HasStepError() const { for (size_t drive = 0; drive < DRIVES; ++drive) { const DriveMovement& dm = ddm[drive]; if (dm.state == DMState::stepError) { return true; } } return false; } // Remove this drive from the list of drives with steps due, and return its DM or nullptr if not there // Called from the step ISR only. DriveMovement *DDA::RemoveDM(size_t drive) { DriveMovement **dmp = &firstDM; while (*dmp != nullptr) { DriveMovement *dm = *dmp; if (dm->drive == drive) { (*dmp) = dm->nextDM; return dm; } dmp = &(dm->nextDM); } return nullptr; } // Take a unit positive-hyperquadrant vector, and return the factor needed to obtain // length of the vector as projected to touch box[]. float DDA::VectorBoxIntersection(const float v[], const float box[], size_t dimensions) { // Generate a vector length that is guaranteed to exceed the size of the box float biggerThanBoxDiagonal = 2.0*Magnitude(box, dimensions); float magnitude = biggerThanBoxDiagonal; for (size_t d = 0; d < dimensions; d++) { if (biggerThanBoxDiagonal*v[d] > box[d]) { float a = box[d]/v[d]; if (a < magnitude) { magnitude = a; } } } return magnitude; } // Normalise a vector with dim1 dimensions so that it is unit in the first dim2 dimensions, and also return its previous magnitude in dim2 dimensions float DDA::Normalise(float v[], size_t dim1, size_t dim2) { float magnitude = Magnitude(v, dim2); if (magnitude <= 0.0) { return 0.0; } Scale(v, 1.0/magnitude, dim1); return magnitude; } // Return the magnitude of a vector float DDA::Magnitude(const float v[], size_t dimensions) { float magnitude = 0.0; for (size_t d = 0; d < dimensions; d++) { magnitude += v[d]*v[d]; } magnitude = sqrtf(magnitude); return magnitude; } // Multiply a vector by a scalar void DDA::Scale(float v[], float scale, size_t dimensions) { for(size_t d = 0; d < dimensions; d++) { v[d] = scale*v[d]; } } // Move a vector into the positive hyperquadrant void DDA::Absolute(float v[], size_t dimensions) { for(size_t d = 0; d < dimensions; d++) { v[d] = fabsf(v[d]); } } // End