/********************************************************************** * Author: tstern * Copyright (C) 2018 MisfitTech, All rights reserved. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Written by Trampas Stern for MisfitTech. Misfit Tech invests time and resources providing this open source code, please support MisfitTech and open-source hardware by purchasing products from MisfitTech, www.misifittech.net! *********************************************************************/ #include "stepper_controller.h" #include "nonvolatile.h" //for programmable parameters #include #include #include "steppin.h" #pragma GCC push_options #pragma GCC optimize ("-Ofast") #define WAIT_TC16_REGS_SYNC(x) while(x->COUNT16.STATUS.bit.SYNCBUSY); volatile bool TC5_ISR_Enabled=false; void setupTCInterrupts() { // Enable GCLK for TC4 and TC5 (timer counter input clock) GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID(GCM_TC4_TC5)); while (GCLK->STATUS.bit.SYNCBUSY); TC5->COUNT16.CTRLA.reg &= ~TC_CTRLA_ENABLE; // Disable TCx WAIT_TC16_REGS_SYNC(TC5) // wait for sync TC5->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16; // Set Timer counter Mode to 16 bits WAIT_TC16_REGS_SYNC(TC5) TC5->COUNT16.CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ; // Set TC as normal Normal Frq WAIT_TC16_REGS_SYNC(TC5) TC5->COUNT16.CTRLA.reg |= TC_CTRLA_PRESCALER_DIV1; // Set perscaler WAIT_TC16_REGS_SYNC(TC5) TC5->COUNT16.CC[0].reg = F_CPU/NZS_CONTROL_LOOP_HZ; WAIT_TC16_REGS_SYNC(TC5) TC5->COUNT16.INTENSET.reg = 0; // disable all interrupts TC5->COUNT16.INTENSET.bit.OVF = 1; // enable overfollow // TC5->COUNT16.INTENSET.bit.MC0 = 1; // enable compare match to CC0 NVIC_SetPriority(TC5_IRQn, 2); // Enable InterruptVector NVIC_EnableIRQ(TC5_IRQn); // Enable TC TC5->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE; WAIT_TC16_REGS_SYNC(TC5) } static void enableTCInterrupts() { TC5_ISR_Enabled=true; NVIC_EnableIRQ(TC5_IRQn); TC5->COUNT16.INTENSET.bit.OVF = 1; // TC5->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE; //Enable TC5 // WAIT_TC16_REGS_SYNC(TC5) //wait for sync } static void disableTCInterrupts() { TC5_ISR_Enabled=false; //NVIC_DisableIRQ(TC5_IRQn); TC5->COUNT16.INTENCLR.bit.OVF = 1; } static bool enterCriticalSection() { bool state=TC5_ISR_Enabled; disableTCInterrupts(); return state; } static void exitCriticalSection(bool prevState) { if (prevState) { enableTCInterrupts(); } //else do nothing } void StepperCtrl::updateParamsFromNVM(void) { bool state=enterCriticalSection(); pPID.Kd=NVM->pPID.Kd*CTRL_PID_SCALING; pPID.Ki=NVM->pPID.Ki*CTRL_PID_SCALING; pPID.Kp=NVM->pPID.Kp*CTRL_PID_SCALING; vPID.Kd=NVM->vPID.Kd*CTRL_PID_SCALING; vPID.Ki=NVM->vPID.Ki*CTRL_PID_SCALING; vPID.Kp=NVM->vPID.Kp*CTRL_PID_SCALING; sPID.Kd=NVM->sPID.Kd*CTRL_PID_SCALING; sPID.Ki=NVM->sPID.Ki*CTRL_PID_SCALING; sPID.Kp=NVM->sPID.Kp*CTRL_PID_SCALING; if (NVM->SystemParams.parametersVaild) { memcpy((void *)&systemParams, (void *)&NVM->SystemParams, sizeof(systemParams)); LOG("Home pin %d",systemParams.homePin); }else { ERROR("This should never happen but just in case"); systemParams.microsteps=16; #if defined(CTRL_POS_PID_AS_DEFAULT) systemParams.controllerMode=CTRL_POS_PID; #else systemParams.controllerMode=CTRL_SIMPLE; #endif systemParams.dirPinRotation=CW_ROTATION; //default to clockwise rotation when dir is high systemParams.errorLimit=(int32_t)ANGLE_FROM_DEGREES(1.8); systemParams.errorPinMode=ERROR_PIN_MODE_ENABLE; //default to enable pin systemParams.errorLogic=false; systemParams.homeAngleDelay=ANGLE_FROM_DEGREES(10); systemParams.homePin=-1; //no homing pin configured } //default the error pin to input, if it is an error pin the // handler for this will change the pin to be an output. // for bidirection error it has to handle input/output it's self as well. // This is not the cleanest way to handle this... // TODO implement this cleaner? pinMode(PIN_ERROR, INPUT_PULLUP); //we have input pin if (NVM->motorParams.parametersVaild) { memcpy((void *)&motorParams, (void *)&NVM->motorParams, sizeof(motorParams)); } else { //MotorParams_t Params; motorParams.fullStepsPerRotation=200; motorParams.currentHoldMa=500; motorParams.currentMa=800; motorParams.homeHoldMa=200; motorParams.homeMa=800; motorParams.motorWiring=true; //memcpy((void *)&Params, (void *)&motorParams, sizeof(motorParams)); //nvmWriteMotorParms(Params); } stepperDriver.setRotationDirection(motorParams.motorWiring); exitCriticalSection(state); } void StepperCtrl::motorReset(void) { //when we reset the motor we want to also sync the motor // phase. Therefore we move forward a few full steps then back // to sync motor phasing, leaving the motor at "phase 0" bool state=enterCriticalSection(); // stepperDriver.move(0,motorParams.currentMa); // delay(100); // stepperDriver.move(A4954_NUM_MICROSTEPS,motorParams.currentMa); // delay(100); // stepperDriver.move(A4954_NUM_MICROSTEPS*2,motorParams.currentMa); // delay(100); // stepperDriver.move(A4954_NUM_MICROSTEPS*3,motorParams.currentMa); // delay(100); // stepperDriver.move(A4954_NUM_MICROSTEPS*2,motorParams.currentMa); // delay(100); // stepperDriver.move(A4954_NUM_MICROSTEPS,motorParams.currentMa); // delay(100); stepperDriver.move(0,motorParams.currentMa); delay(1000); setLocationFromEncoder(); //measure new starting point exitCriticalSection(state); } void StepperCtrl::setLocationFromEncoder(void) { numSteps=0; currentLocation=0; if (calTable.calValid()) { int32_t n,x; int32_t calIndex; Angle a; //set our angles based on previous cal data x=sampleMeanEncoder(200); a=calTable.fastReverseLookup(x); //our cal table starts at angle zero, so lets set starting based on this and stepsize LOG("start angle %d, encoder %d", (uint16_t)a,x); // we were rounding to nearest full step, but this should not be needed TBS 10/12/2017 // //TODO we need to handle 0.9 degree motor // if (CALIBRATION_TABLE_SIZE == motorParams.fullStepsPerRotation) // { // n=(int32_t)ANGLE_STEPS/CALIBRATION_TABLE_SIZE; // // calIndex=((int32_t)((uint16_t)a+n/2)*CALIBRATION_TABLE_SIZE)/ANGLE_STEPS; //find calibration index // if (calIndex>CALIBRATION_TABLE_SIZE) // { // calIndex-=CALIBRATION_TABLE_SIZE; // } // a=(uint16_t)((calIndex*ANGLE_STEPS)/CALIBRATION_TABLE_SIZE); // } x=(int32_t)((((float)(uint16_t)a)*360.0/(float)ANGLE_STEPS)*1000); LOG("start angle after rounding %d %d.%03d", (uint16_t)a,x/1000,x%1000); //we need to set our numSteps numSteps=DIVIDE_WITH_ROUND( ((int32_t)a *motorParams.fullStepsPerRotation*systemParams.microsteps),ANGLE_STEPS); currentLocation=(uint16_t)a; } zeroAngleOffset=getCurrentLocation(); //zero the angle shown on LCD } void StepperCtrl::acceptPositionAndStealthSwitchMode(feedbackCtrl_t m) { bool state=enterCriticalSection(); currentLocationIsDesiredLocation(); systemParams.controllerMode=m; exitCriticalSection(state); } void StepperCtrl::stealthSwitchMode(feedbackCtrl_t m) { bool state=enterCriticalSection(); systemParams.controllerMode=m; exitCriticalSection(state); } int64_t StepperCtrl::getZeroAngleOffset(void) { int64_t x; bool state=enterCriticalSection(); x=zeroAngleOffset; exitCriticalSection(state); return x; } void StepperCtrl::setAngle(int64_t angle) { bool state=enterCriticalSection(); zeroAngleOffset=getCurrentLocation()-angle; exitCriticalSection(state); } void StepperCtrl::setZero(void) { //we want to set the starting angle to zero. bool state=enterCriticalSection(); zeroAngleOffset=getCurrentLocation(); exitCriticalSection(state); } void StepperCtrl::encoderDiagnostics(char *ptrStr) { bool state=TC5_ISR_Enabled; disableTCInterrupts(); encoder.diagnostics(ptrStr); if (state) enableTCInterrupts(); } //TODO This function does two things, set rotation direction // and measures step size, it should be two functions. //return is anlge in degreesx100 ie 360.0 is returned as 36000 float StepperCtrl::measureStepSize(void) { int32_t angle1,angle2,x,i; bool feedback=enableFeedback; int32_t microsteps=systemParams.microsteps; systemParams.microsteps=1; enableFeedback=false; motorParams.motorWiring=true; //assume we are forward wiring to start with stepperDriver.setRotationDirection(motorParams.motorWiring); ///////////////////////////////////////// //// Measure the full step size ///// /// Note we assume machine can take one step without issue/// LOG("reset motor"); motorReset(); //this puts stepper motor at stepAngle of zero LOG("sample encoder"); angle1=sampleMeanEncoder(200); LOG("move"); stepperDriver.move(A4954_NUM_MICROSTEPS/2,motorParams.currentMa); //move one half step 'forward' delay(100); stepperDriver.move(A4954_NUM_MICROSTEPS,motorParams.currentMa); //move one half step 'forward' delay(500); LOG("sample encoder"); angle2=sampleMeanEncoder(200); LOG("Angles %d %d",angle1,angle2); if ((abs(angle2-angle1))>(ANGLE_STEPS/2)) { //we crossed the wrap around if (angle1>angle2) { angle1=angle1+(int32_t)ANGLE_STEPS; }else { angle2=angle2+(int32_t)ANGLE_STEPS; } } LOG("Angles %d %d",angle1,angle2); //when we are increase the steps in the stepperDriver.move() command // we want the encoder increasing. This ensures motor is moving clock wise // when encoder is increasing. // if (angle2>angle1) // { // motorParams.motorWiring=true; // stepperDriver.setRotationDirection(true); // LOG("Forward rotating"); // }else // { // //the motor is wired backwards so correct in stepperDriver // motorParams.motorWiring=false; // stepperDriver.setRotationDirection(false); // LOG("Reverse rotating"); // } x=((int64_t)(angle2-angle1)*36000)/(int32_t)ANGLE_STEPS; // if x is ~180 we have a 1.8 degree step motor, if it is ~90 we have 0.9 degree step LOG("%angle delta %d %d (%d %d)",x,abs(angle2-angle1),angle1,angle2 ); //move back stepperDriver.move(-A4954_NUM_MICROSTEPS/2,motorParams.currentMa); //move one half step 'forward' delay(100); stepperDriver.move(-A4954_NUM_MICROSTEPS,motorParams.currentMa); //move one half step 'forward' systemParams.microsteps=microsteps; enableFeedback=feedback; return ((float)x)/100.0; } int32_t StepperCtrl::measureError(void) { //LOG("current %d desired %d %d",(int32_t) currentLocation, (int32_t)getDesiredLocation(), numSteps); return ((int32_t)currentLocation-(int32_t)getDesiredLocation()); } /* bool StepperCtrl::changeMicrostep(uint16_t microSteps) { bool state=TC5_ISR_Enabled; disableTCInterrupts(); systemParams.microsteps=microSteps; motorReset(); if (state) enableTCInterrupts(); return true; } */ Angle StepperCtrl::maxCalibrationError(void) { //Angle startingAngle; bool done=false; int32_t mean; int32_t maxError=0, j; int16_t dist; uint16_t angle=0; bool feedback=enableFeedback; uint16_t microSteps=systemParams.microsteps; int32_t steps; bool state=TC5_ISR_Enabled; disableTCInterrupts(); if (false == calTable.calValid()) { return ANGLE_MAX; } enableFeedback=false; j=0; LOG("Running calibration test"); systemParams.microsteps=1; motorReset(); steps=0; while(!done) { Angle cal; Angle act, desiredAngle; //todo we should measure mean and wait until stable. delay(200); mean=sampleMeanEncoder(200); desiredAngle=(uint16_t)(getDesiredLocation() & 0x0FFFFLL); cal=calTable.getCal(desiredAngle); dist=Angle(mean)-cal; act=calTable.fastReverseLookup(cal); LOG("actual %d, cal %d",mean,(uint16_t)cal); LOG("desired %d",(uint16_t)desiredAngle); LOG("numSteps %d", numSteps); LOG("cal error for step %d is %d",j,dist); LOG("mean %d, cal %d",mean, (uint16_t)cal); updateStep(0,1); // move one half step at a time, a full step move could cause a move backwards depending on how current ramps down steps+=A4954_NUM_MICROSTEPS/2; stepperDriver.move(steps,motorParams.currentMa); delay(50); steps+=A4954_NUM_MICROSTEPS/2; stepperDriver.move(steps,motorParams.currentMa); if (400==motorParams.fullStepsPerRotation) { delay(100); updateStep(0,1); // move one half step at a time, a full step move could cause a move backwards depending on how current ramps down steps+=A4954_NUM_MICROSTEPS/2; stepperDriver.move(steps,motorParams.currentMa); delay(100); steps+=A4954_NUM_MICROSTEPS/2; stepperDriver.move(steps,motorParams.currentMa); } //delay(400); if (abs(dist)>maxError) { maxError=abs(dist); } j++; if (j>=(1*CALIBRATION_TABLE_SIZE+3)) { done=true; } } systemParams.microsteps=microSteps; motorReset(); enableFeedback=feedback; if (state) enableTCInterrupts(); LOG("max error is %d cnts", maxError); return Angle((uint16_t)maxError); } //The encoder needs to be calibrated to the motor. // we will assume full step detents are correct, // ex 1.8 degree motor will have 200 steps for 360 degrees. // We also need to calibrate the phasing of the motor // to the A4954. This requires that the A4954 "step angle" of // zero is the first entry in the calibration table. bool StepperCtrl::calibrateEncoder(void) { int32_t x,i,j; uint32_t angle=0; int32_t steps; bool done=false; int32_t mean; uint16_t microSteps=systemParams.microsteps; bool feedback=enableFeedback; bool state=TC5_ISR_Enabled; disableTCInterrupts(); enableFeedback=false; systemParams.microsteps=1; LOG("reset motor"); motorReset(); LOG("Starting calibration"); delay(200); steps=0; j=0; while(!done) { int ii,N; Angle cal,desiredAngle; desiredAngle=(uint16_t)(getDesiredLocation() & 0x0FFFFLL); cal=calTable.getCal(desiredAngle); delay(200); mean=sampleMeanEncoder(200); LOG("Previous cal distance %d, %d, mean %d, cal %d",j, cal-Angle((uint16_t)mean), mean, (uint16_t)cal); calTable.updateTableValue(j,mean); updateStep(0,1); N=2; // move one half step at a time, a full step move could cause a move backwards depending on how current ramps down for (ii=0; ii=CALIBRATION_TABLE_SIZE) { done=true; } } //calTable.printCalTable(); //calTable.smoothTable(); //calTable.printCalTable(); calTable.saveToFlash(); //saves the calibration to flash calTable.printCalTable(); systemParams.microsteps=microSteps; motorReset(); enableFeedback=feedback; if (state) enableTCInterrupts(); return done; } stepCtrlError_t StepperCtrl::begin(void) { int i; float x; enableFeedback=false; velocity=0; currentLocation=0; numSteps=0; //we have to update from NVM before moving motor updateParamsFromNVM(); //update the local cache from the NVM LOG("start up encoder"); if (false == encoder.begin(PIN_AS5047D_CS)) { return STEPCTRL_NO_ENCODER; } LOG("cal table init"); calTable.init(); startUpEncoder=(uint16_t)getEncoderAngle(); WARNING("start up encoder %d",startUpEncoder); LOG("start stepper driver"); stepperDriver.begin(); #ifdef NEMA17_SMART_STEPPER_3_21_2017 if (NVM->motorParams.parametersVaild) { //lets read the motor voltage if (GetMotorVoltage()<5) { //if we have less than 5 volts the motor is not powered uint32_t x; x=(uint32_t)(GetMotorVoltage()*1000.0); ERROR("Motor voltage is %" PRId32 "mV",x); //default printf does not support floating point numbers ERROR("Motor may not have power"); return STEPCTRL_NO_POWER; } bool state=enterCriticalSection(); setLocationFromEncoder(); //measure new starting point exitCriticalSection(state); }else { LOG("measuring step size"); x=measureStepSize(); if (abs(x)<0.5) { ERROR("Motor may not have power"); return STEPCTRL_NO_POWER; } } #else LOG("measuring step size"); x=measureStepSize(); if (abs(x)<0.5) { ERROR("Motor may not have power"); return STEPCTRL_NO_POWER; } #endif LOG("Checking the motor parameters"); //todo we might want to move this up a level to the NZS // especially since it has default values if (false == NVM->motorParams.parametersVaild) { MotorParams_t params; WARNING("NVM motor parameters are not set, we will update"); //power could have just been applied and step size read wrong // if we are more than 200 steps/rotation which is most common // lets read again just to be sure. if (abs(x)<1.5) { //run step test a second time to be sure x=measureStepSize(); } if (x>0) { motorParams.motorWiring=true; } else { motorParams.motorWiring=false; } if (abs(x)<=1.2) { motorParams.fullStepsPerRotation=400; }else { motorParams.fullStepsPerRotation=200; } memcpy((void *)¶ms, (void *)&motorParams,sizeof(motorParams)); nvmWriteMotorParms(params); } LOG("Motor params are now good"); LOG("fullSteps %d", motorParams.fullStepsPerRotation); LOG("motorWiring %d", motorParams.motorWiring); LOG("currentMa %d", motorParams.currentMa); LOG("holdCurrentMa %d", motorParams.currentHoldMa); LOG("homeMa %d", motorParams.homeMa); LOG("homeHoldMa %d", motorParams.homeHoldMa); updateParamsFromNVM(); //update the local cache from the NVM if (false == calTable.calValid()) { return STEPCTRL_NO_CAL; } enableFeedback=true; setupTCInterrupts(); enableTCInterrupts(); return STEPCTRL_NO_ERROR; } Angle StepperCtrl::sampleAngle(void) { uint16_t angle; int32_t x,y; #ifdef NZS_AS5047_PIPELINE //read encoder twice such that we get the latest sample as the pipeline is always once sample behind y=encoder.readEncoderAnglePipeLineRead(); //convert the 14 bit encoder value to a 16 bit number x=encoder.readEncoderAnglePipeLineRead(); angle=((uint32_t)(x)*4); //convert the 14 bit encoder value to a 16 bit number #else angle=((uint32_t)encoder.readEncoderAngle())<<2; //convert the 14 bit encoder value to a 16 bit number #endif return Angle(angle); } //when sampling the mean of encoder if we are on roll over // edge we can have an issue so we have this function // to do the mean correctly Angle StepperCtrl::sampleMeanEncoder(int32_t numSamples) { int32_t i,last,x=0; int64_t mean=0; int32_t min,max; mean=0; for (i=0; i<(numSamples); i++) { int32_t d; last=x; x=(((int32_t)encoder.readEncoderAngle())*4); if (encoder.getError()) { #ifndef MECHADUINO_HARDWARE SerialUSB.println("AS5047 Error"); #else Serial5.print("AS5047 Error"); #endif delay(1000); return 0; } if(i==0) { last=x; min=x; max=x; } //LOG("i %d,min %d, max %d, last %d, x %d", i, min, max, last, x); if (abs(last-x)>65536/2) { if (last>x) { x=x+65536; } else { x=x-65536; } } if (x>max) { max=x; } if (xn) { ret-=n; } while(ret<-n) { ret+=n; } n=(int32_t)(ret); LOG("s is %d %d",n,steps); stepperDriver.move(n,motorParams.currentMa); } } int64_t StepperCtrl::getDesiredLocation(void) { int64_t ret; int32_t n; bool state=TC5_ISR_Enabled; disableTCInterrupts(); n=motorParams.fullStepsPerRotation * systemParams.microsteps; ret=((int64_t)numSteps * (int64_t)ANGLE_STEPS+(n/2))/n ; if (state) enableTCInterrupts(); return ret; } //int32_t StepperCtrl::getSteps(void) //{ // int32_t ret; // bool state=enterCriticalSection(); // ret=numSteps; // exitCriticalSection(state); // return ret; //} void StepperCtrl::moveToAbsAngle(int32_t a) { int64_t ret; int32_t n; n=motorParams.fullStepsPerRotation * systemParams.microsteps; ret=(((int64_t)a+zeroAngleOffset)*n+ANGLE_STEPS/2)/(int32_t)ANGLE_STEPS; bool state=enterCriticalSection(); numSteps=ret; exitCriticalSection(state); } void StepperCtrl::moveToAngle(int32_t a, uint32_t ma) { //we need to convert 'Angle' to A4954 steps a=a % ANGLE_STEPS; //we only interested in the current angle a=DIVIDE_WITH_ROUND( (a*motorParams.fullStepsPerRotation*A4954_NUM_MICROSTEPS), ANGLE_STEPS); //LOG("move %d %d",a,ma); stepperDriver.move(a,ma); } Angle StepperCtrl::getEncoderAngle(void) { Angle a; bool state=enterCriticalSection(); a=calTable.fastReverseLookup(sampleAngle()); exitCriticalSection(state); return a; } int64_t StepperCtrl::getCurrentLocation(void) { Angle a; int32_t x; bool state=TC5_ISR_Enabled; disableTCInterrupts(); a=calTable.fastReverseLookup(sampleAngle()); x=(int32_t)a - (int32_t)((currentLocation) & ANGLE_MAX); if (x>((int32_t)ANGLE_STEPS/2)) { currentLocation -= ANGLE_STEPS; } if (x<-((int32_t)ANGLE_STEPS/2)) { currentLocation += ANGLE_STEPS; } currentLocation=(currentLocation & 0xFFFFFFFFFFFF0000UL) | (uint16_t)a; if (state) enableTCInterrupts(); return currentLocation; } int64_t StepperCtrl::getCurrentAngle(void) { int64_t x; x=getCurrentLocation()-zeroAngleOffset; return x; } int64_t StepperCtrl::getDesiredAngle(void) { int64_t x; x=getDesiredLocation()-zeroAngleOffset; return x; } void StepperCtrl::setVelocity(int64_t vel) { bool state=enterCriticalSection(); velocity=vel; exitCriticalSection(state); } void StepperCtrl::setTorque(int8_t tor) { bool state=enterCriticalSection(); torque=tor; exitCriticalSection(state); } int8_t StepperCtrl::getTorque(void) { int8_t tor; bool state=enterCriticalSection(); tor=torque; exitCriticalSection(state); return tor; } int64_t StepperCtrl::getVelocity(void) { int64_t vel; bool state=enterCriticalSection(); vel=velocity; exitCriticalSection(state); return vel; } bool StepperCtrl::torqueLoop(int64_t currentLoc, Control_t *ptrCtrl) { int32_t U, ma; int64_t u, y; int32_t fullStep=ANGLE_STEPS/motorParams.fullStepsPerRotation; y=currentLoc; ma=motorParams.currentMa; u=torque*ma/128; // Make ma the maximum torque at 128 // If dir pin changes meaning then torque also changes direction if (CW_ROTATION == systemParams.dirPinRotation) { u=-u; } U=abs(u); if (U>ma) U=ma; if (u>0) y=y+fullStep; else y=y-fullStep; ptrCtrl->ma=U; ptrCtrl->angle=(int32_t)y; moveToAngle(y,U); } void StepperCtrl::PrintData(void) { char s[128]; bool state=enterCriticalSection(); sprintf(s, "%u,%u,%u", (uint32_t)numSteps,(uint32_t)getDesiredAngle(),(uint32_t)getCurrentAngle()); #ifndef MECHADUINO_HARDWARE SerialUSB.println(s); #endif exitCriticalSection(state); } //this is the velocity PID feedback loop bool StepperCtrl::vpidFeedback(int64_t desiredLoc, int64_t currentLoc, Control_t *ptrCtrl) { int32_t fullStep=ANGLE_STEPS/motorParams.fullStepsPerRotation; static int64_t lastY=getCurrentLocation(); static int32_t lastError=0; static int64_t Iterm=0; int64_t y,z; int64_t v,dy; int64_t u; //get the current location y =currentLoc; v=y-lastY; //add in phase prediction y=y+calculatePhasePrediction(currentLoc); z=y; lastY=y; v=v*NZS_CONTROL_LOOP_HZ; if (enableFeedback) //if ((micros()-lastCall)>(updateRate/10)) { int64_t error,U; error = velocity-v; Iterm += (vPID.Ki * error); if (Iterm>(16*4096*CTRL_PID_SCALING *(int64_t)motorParams.currentMa)) { Iterm=(16*4096*CTRL_PID_SCALING *(int64_t)motorParams.currentMa); } if (Iterm<-(16*4096*CTRL_PID_SCALING *(int64_t)motorParams.currentMa)) { Iterm=-(16*4096*CTRL_PID_SCALING*(int64_t)motorParams.currentMa); } u=((vPID.Kp * error) + Iterm - (vPID.Kd *(lastError-error))); U=abs(u)/CTRL_PID_SCALING/1024; //scale the error to make PID params close to 1.0;//scale the error to make PID params close to 1.0 by dividing by 1024 if (U>motorParams.currentMa) { U=motorParams.currentMa; } //when error is positive we need to move reverse direction if (u>0) { z=z+(fullStep); }else { z=z-(fullStep); } ptrCtrl->ma=U; ptrCtrl->angle=(int32_t)z; moveToAngle(z,U); loopError=error; lastError=error; } else { lastError=0; Iterm=0; } if (abs(lastError)>(systemParams.errorLimit)) { return 1; } return 0; } //Since we are doing fixed point math our // threshold needs to be large. // We need a large threshold when we have fast update // rate as well. But for most part it is random bool StepperCtrl::pidFeedback(int64_t desiredLoc, int64_t currentLoc, Control_t *ptrCtrl) { static int count=0; static int32_t maxError=0; static int32_t lastError=0; static int32_t Iterm=0; int32_t ma; int64_t y; int32_t fullStep=ANGLE_STEPS/motorParams.fullStepsPerRotation; int32_t dy; y=currentLoc; //add in phase prediction y=y+calculatePhasePrediction(currentLoc); if (enableFeedback) //if ((micros()-lastCall)>(updateRate/10)) { int64_t error,u; int32_t U,x; //error is in units of degrees when 360 degrees == 65536 error=(desiredLoc-y); //error is currentPos-desiredPos Iterm+=(pPID.Ki * error); if (systemParams.homePin>0 && digitalRead(systemParams.homePin)==0) { ma=motorParams.homeMa; } else { ma=motorParams.currentMa; } //Over the long term we do not want error // to be much more than our threshold if (Iterm> (ma*CTRL_PID_SCALING) ) { Iterm=(ma*CTRL_PID_SCALING) ; } if (Iterm<-(ma*CTRL_PID_SCALING) ) { Iterm=-(ma*CTRL_PID_SCALING) ; } //u=((pPID.Kp * error) + Iterm - (pPID.Kd *(lastError-error))); if(abs(error) > 90304) // 540 degrees { u=(int64_t)((float)ma*(float)CTRL_PID_SCALING*90304.0/(float)error); } else { u=((pPID.Kp * error) + Iterm - (pPID.Kd *(lastError-error))); } U=abs(u)/CTRL_PID_SCALING; if (U>ma) { U=ma; } //when error is positive we need to move reverse direction if (u>0) { y=y+fullStep; }else { y=y-fullStep; } ptrCtrl->ma=U; ptrCtrl->angle=(int32_t)y; moveToAngle(y,U); loopError=error; lastError=error; }else { lastError=0; Iterm=0; } if (abs(lastError)>(systemParams.errorLimit)) { return 1; } return 0; } // the phase prediction tries to predict error from sensor based // on current location and previous location. // TODO our error can help in the phase prediction. // if the error int64_t StepperCtrl::calculatePhasePrediction(int64_t currentLoc) { static int64_t lastLoc=0; static int32_t mean=0; int32_t dx,x; #ifndef ENABLE_PHASE_PREDICTION return 0; #endif //what was our change in the location dx=currentLoc-lastLoc; //max value is typically less than 327(1.8 degrees) or 163(0.9 degree) //if the motor direction changes, zero phase prediction if (SIGN(dx) != SIGN(mean)) { //last thing we want is phase prediction during direction change. mean=0; } else { if (abs(dx)>abs(mean)) { //increase mean really slowly, 2048 ~ 1/3 second with 6khz processing loop // in fixed point since the dx is so small we need to scale it up to average // dx has be be greater than 8 to change the mean... // this limits the acceleration of motor above max processing speed (6k*1.8)=1800RPM // however I doubt the motor can accelerate that fast with any load... // The average helps prevent external impulse error from causing prediction to cause issues. mean=DIVIDE_WITH_ROUND(2047*mean + dx*128, 2048); }else { //decrease fast //do not add more phase prediction than the difference in last two samples. mean=dx*128; } } lastLoc=currentLoc; x= mean/128; //scale back to normal return x; } bool StepperCtrl::determineError(int64_t currentLoc,int64_t error) { static int64_t lastLocation=0; static int64_t lastError=0; static int64_t lastVelocity=0; int64_t velocity; //since this is called on periodic timer the velocity // is propotional to the change in location // one rotation per second is velocity of 10, assumming 6khz update rate // one rotation per minute is 10/60 velocity units // since this is less than 1 we will scale the velo velocity=(currentLoc-lastLocation); if (velocity>0 && lastVelocity>0) { } lastVelocity=velocity; lastError=error; lastLocation=currentLoc; } //this was written to do the PID loop not modeling a DC servo // but rather using features of stepper motor. bool StepperCtrl::simpleFeedback(int64_t desiredLoc, int64_t currentLoc, Control_t *ptrCtrl) { static uint32_t t0=0; static uint32_t calls=0; bool ret=false; static int32_t maxError=0; static int32_t lastError=0; static int32_t i=0; static int32_t iTerm=0; //static int64_t lastY=getCurrentLocation(); static int32_t velocity=0; static int32_t errorCount=0; static bool lastProbeState=false; static int64_t probeStartAngle=0; static int32_t maxMa=0; static int64_t filteredError=0; static int32_t probeCount=0; int32_t fullStep=ANGLE_STEPS/motorParams.fullStepsPerRotation; int32_t ma=0; int64_t y; //estimate our current location based on the encoder y=currentLoc; //add in phase prediction y=y+calculatePhasePrediction(currentLoc); //we can limit the velocity by controlling the amount we move per call to this function // this only works for velocity greater than 100rpm /* if (velocity!=0) { fullStep=velocity/NZS_CONTROL_LOOP_HZ; } if (fullStep==0) { fullStep=1; //this RPM of (1*NZS_CONTROL_LOOP_HZ)/60 ie at 6Khz it is 100RPM } */ if (enableFeedback) { int64_t error; int32_t u; int32_t x; int32_t kp; //error is in units of degrees when 360 degrees == 65536 error=(desiredLoc-y);//measureError(); //error is currentPos-desiredPos //data[i]=(int16_t)error; //i++; if (i>=N_DATA) { i=0; } kp=sPID.Kp; if (1)//(abs(error)<(fullStep)) { iTerm+=(sPID.Ki*error); x=iTerm/CTRL_PID_SCALING; }else { kp=(CTRL_PID_SCALING*9)/10; x=0; iTerm=0; } if (x>fullStep) { x=fullStep; iTerm=fullStep; } if (x<-fullStep) { x=-fullStep; iTerm=-fullStep; } //u=(kp * error)/CTRL_PID_SCALING+x+(sPID.Kd *(error-lastError))/CTRL_PID_SCALING; // Lower effort if we're very far off if(abs(error) > 90304) // 540 degrees { u=(int64_t)((float)fullStep*90304.0/(float)error); } else { u=(kp * error)/CTRL_PID_SCALING+x+(sPID.Kd *(error-lastError))/CTRL_PID_SCALING; } //limit error to full step if (u>fullStep) { u=fullStep; } if (u<-fullStep) { u=-fullStep; } ma=(abs(u)*(motorParams.currentMa-motorParams.currentHoldMa))/ fullStep + motorParams.currentHoldMa; if (ma>motorParams.currentMa) { ma=motorParams.currentMa; } //maxMa=motorParams.currentMa; if (systemParams.homePin>=0) { if (digitalRead(systemParams.homePin)==0) { if (lastProbeState==false) { //record our current angle for homing probeStartAngle=desiredLoc; probeCount=0; maxMa=0; } lastProbeState=true; probeCount++; //we will lower current after whe have moved some amount if (probeCount > NZS_CONTROL_LOOP_HZ && probeCount <(2* NZS_CONTROL_LOOP_HZ)) { maxMa+=ma; if (abs(error)>maxError) { maxError=abs(error); } } if (probeCount>(2*NZS_CONTROL_LOOP_HZ)) { // ma=(abs(u)*(maxMa))/ fullStep;// + motorParams.homeHoldMa; // if (ma>motorParams.homeMa) // { // ma=motorParams.homeMa; // } //if (ma>maxMa/NZS_CONTROL_LOOP_HZ) { ma=((maxMa/NZS_CONTROL_LOOP_HZ)*9)/10; } } } else { lastProbeState=false; } }else { maxError=0; probeCount=0; //maxMa=0; } y=y+u; ptrCtrl->ma=ma; ptrCtrl->angle=(int32_t)y; moveToAngle(y,ma); //35us lastError=error; loopError=error; //stepperDriver.limitCurrent(99); } //filteredError=(filteredError*15+lastError)/16; if (probeCount>(2*NZS_CONTROL_LOOP_HZ)) { if (abs(lastError) > maxError ) { errorCount++; if (errorCount>(10)) { return 1; } return 0; } } else { if (abs(lastError) > systemParams.errorLimit) { errorCount++; if (errorCount>(NZS_CONTROL_LOOP_HZ/128)) // error needs to exist for some time period { return 1; } return 0; } } if (errorCount>0) { errorCount--; } //errorCount=0; stepperDriver.limitCurrent(99); //reduce noise on low error return 0; } void StepperCtrl::currentLocationIsDesiredLocation(){ bool state=enterCriticalSection(); moveToAbsAngle(getCurrentAngle()); exitCriticalSection(state); } void StepperCtrl::enable(bool enable) { bool state=TC5_ISR_Enabled; disableTCInterrupts(); bool feedback=enableFeedback; stepperDriver.enable(enable); //enable or disable the stepper driver as needed if (enabled==true && enable==false) { feedback = false; } if (enabled==false && enable==true) //if we are enabling previous disabled motor { feedback = true; setLocationFromEncoder(); } enabled=enable; enableFeedback=feedback; if (state) enableTCInterrupts(); } /* void StepperCtrl::testRinging(void) { uint16_t c; int32_t steps; int32_t microSteps=systemParams.microsteps; bool feedback=enableFeedback; enableFeedback=false; systemParams.microsteps=1; motorReset(); for (c=2000; c>10; c=c-10) { SerialUSB.print("Current "); SerialUSB.println(c); steps+=A4954_NUM_MICROSTEPS; stepperDriver.move(steps,NVM->SystemParams.currentMa); currentLimit=false; measure(); } systemParams.microsteps=microSteps; motorReset(); enableFeedback=feedback; } */ //returns -1 if no data, else returns number of data points remaining. int32_t StepperCtrl::getLocation(Location_t *ptrLoc) { bool state=enterCriticalSection(); int32_t n; //check for empty if (locReadIndx==locWriteIndx) { //empty data exitCriticalSection(state); return -1; } //else read data memcpy(ptrLoc,(void *)&locs[locReadIndx], sizeof(Location_t)); //update the read index locReadIndx=(locReadIndx+1)%MAX_NUM_LOCATIONS; //calculate number of locations left n=((locWriteIndx+MAX_NUM_LOCATIONS)-locReadIndx)%MAX_NUM_LOCATIONS; exitCriticalSection(state); return n; } void StepperCtrl::updateLocTable(int64_t desiredLoc, int64_t currentLoc, Control_t *ptrCtrl) { bool state=enterCriticalSection(); int32_t next; // set the next write location next=(locWriteIndx+1)%MAX_NUM_LOCATIONS; if (next==locReadIndx) { //we are full, exit exitCriticalSection(state); //RED_LED(true); //turn Red LED on to indciate buffer full return; } //use ticks for the moment so we can tell if we miss data on the print. locs[locWriteIndx].microSecs=(int32_t)micros(); locs[locWriteIndx].desiredLoc=(int32_t)(desiredLoc-zeroAngleOffset); locs[locWriteIndx].actualLoc=(int32_t)(currentLoc-zeroAngleOffset); locs[locWriteIndx].angle=(ptrCtrl->angle-zeroAngleOffset); locs[locWriteIndx].ma=ptrCtrl->ma; locWriteIndx=next; exitCriticalSection(state); } bool StepperCtrl::processFeedback(void) { bool ret; int32_t us,j; Control_t ctrl; int64_t desiredLoc; int64_t currentLoc; int32_t steps; static int64_t mean=0;; us=micros(); #ifdef USE_TC_STEP static int64_t lastSteps; int64_t x; x=getSteps()-lastSteps; updateSteps(x); lastSteps+=x; #endif // steps=getSteps(); // if (steps>0) // { // requestStep(1, (uint16_t)steps); // }else // { // requestStep(0, (uint16_t)(-steps)); // } desiredLoc=getDesiredLocation(); currentLoc=getCurrentLocation(); mean=(31*mean+currentLoc+16)/32; #ifdef A5995_DRIVER //the A5995 is has more driver noise if (abs(currentLoc-mean)PIDparams.Kp; // pKi=NVM->PIDparams.Ki; // pKd=NVM->PIDparams.Kd; // threshold=NVM->PIDparams.Threshold; //enableTCInterrupts(); moveToAngle(angle,motorParams.currentMa); //moveToAngle(angle,NVM->SystemParams.currentMa); /* //next lets measure our noise on the encoder noiseMin=(int32_t)ANGLE_MAX; noiseMax=-(int32_t)ANGLE_MAX; mean=0; j=1000000UL/NZS_CONTROL_LOOP_HZ; prevAngle=sampleAngle(); for (i=0; i<(NZS_CONTROL_LOOP_HZ/2); i++) { Angle a; a=sampleAngle(); error=(int32_t)(prevAngle-a); if (errornoiseMax) { noiseMax=error; } mean=mean+(int32_t)a; delayMicroseconds(j); } mean=mean/i; while (mean>ANGLE_MAX) { mean=mean-ANGLE_STEPS; } while (mean<0) { mean=mean+ANGLE_STEPS; } //mean is the average of the encoder. */ stepperDriver.move(0,motorParams.currentMa); delay(1000); //now we need to do the relay control for (i=0; i<10; i++) { startAngle=getCurrentLocation(); LOG("Start %d", (int32_t)startAngle); } thres=startAngle + (int32_t)((ANGLE_STEPS/motorParams.fullStepsPerRotation)*10/9); LOG("Thres %d, start %d",(int32_t)thres,(int32_t)startAngle); eMin=(int32_t)ANGLE_MAX; eMax=-(int32_t)ANGLE_MAX; int32_t reset=0; int32_t force=(motorParams.currentMa); for (i=0; i<100; i++) { int32_t error; if (reset) { motorReset(); stepperDriver.move(0,motorParams.currentMa); delay(1000); startAngle=getCurrentLocation(); LOG("Start %d", (int32_t)startAngle); force=force-100; eMin=(int32_t)ANGLE_MAX; eMax=-(int32_t)ANGLE_MAX; if (force<100) { i=100; break; } LOG("force set to %d",force); i=0; } reset=0; stepperDriver.move(A4954_NUM_MICROSTEPS,force); //moveToAngle(startAngle+(ANGLE_STEPS/motorParams.fullStepsPerRotation),force); //stepperDriver.move(A4954_NUM_MICROSTEPS,NVM->SystemParams.currentMa); t0=micros(); error=0; while(error<=((ANGLE_STEPS/motorParams.fullStepsPerRotation)/2+40)) { int32_t y; y=getCurrentLocation(); error=y-startAngle; //LOG("Error1 %d",error); if (erroreMax) { eMax=error; } if (abs(error)>ANGLE_STEPS/motorParams.fullStepsPerRotation*2) { LOG("large Error1 %d, %d, %d",error, y, startAngle); reset=1; break; } } stepperDriver.move(0,force); //stepperDriver.move(0,NVM->SystemParams.currentMa); t1=micros(); error=(ANGLE_STEPS/motorParams.fullStepsPerRotation); while(error>=((ANGLE_STEPS/motorParams.fullStepsPerRotation)/2-40)) { error=getCurrentLocation()-startAngle; //LOG("Error2 %d",error); if (erroreMax) { eMax=error; } if (abs(error)>ANGLE_STEPS/motorParams.fullStepsPerRotation*2) { LOG("large Error2 %d",error); reset=1; break; } } times[i]=t1-t0; } for (i=0; i<100; i++) { LOG("Time %d %d",i,times[i]); } LOG("errorMin=%d",eMin); LOG("errorMax=%d",eMax); motorReset(); systemParams.controllerMode=prevCtrl; systemParams.microsteps=microSteps; enableFeedback=feedback; if (state) enableTCInterrupts(); } //void StepperCtrl::printData(void) //{ // bool state=TC5_ISR_Enabled; // disableTCInterrupts(); // int32_t i; // for(i=0; i