Need Help Understanding runsOnATimer() in Firmware

I’m trying to get a better understanding of the firmware, and in particular, the movement loop. It appears that the following function (runsOnATimer), well, runs on a timer and is called based upon the LOOPINTERVAL (I removed the SIMAVR part of the code)

void runsOnATimer(){
    movementUpdated = false;
    leftAxis.computePID();
    rightAxis.computePID();
    zAxis.computePID();
}

As I understand it, during the movement loop (i.e., coordinatedMove, singleAxisMove, arc), the next “step” isn’t written to the axes until movementUpdated is false. So, when this function occurs, movementUpdated gets set to false and then the computePID() functions are called for each axis. I’ve never dealt with motor controls before and would like my someone to confirm (or correct) my understanding of the process:

  1. These functions ultimately just determine the PWM duty cycle needed to complete the move “exactly” within specific LOOPINTERVAL (10,000 microseconds) and commands the arduino to output the particular PWM signal and the function return before the move is actually completed, correct? That way all motors move in relative unison.

  2. Even though new position values are written to the axes before the moves are completed, they aren’t used until runsOnATimer is called, correct?

  3. If the motor is having a hard time completing this move within the LOOPINTERVAL (because of not enough power, whatever), the encoder error will increase and when it exceeds a particular limit (2.0 mm as default), it will throw the system into alarm. Correct?

I’m trying to get a better understanding of the firmware, and in particular, the movement loop. It appears that the following function (runsOnATimer), well, runs on a timer and is called based upon the LOOPINTERVAL (I removed the SIMAVR part of the code)

void runsOnATimer(){
movementUpdated = false;
leftAxis.computePID();
rightAxis.computePID();
zAxis.computePID();
}

As I understand it, during the movement loop (i.e., coordinatedMove, singleAxisMove, arc), the next “step” isn’t written to the axes until movementUpdated is false. So, when this function occurs, movementUpdated gets set to false and then the computePID() functions are called for each axis. I’ve never dealt with motor controls before and would like my someone to confirm (or correct) my understanding of the process:

  1. These functions ultimately just determine the PWM duty cycle needed to
    complete the move “exactly” within specific LOOPINTERVAL (10,000 microseconds)
    and commands the arduino to output the particular PWM signal and the function
    return before the move is actually completed, correct? That way all motors
    move in relative unison.

These determine the PWM setting to use. This isn’t to complete the move exactly,
it may be trying to catch up, and it ramps up gradually, so it may not catch up
inone loop.

  1. Even though new position values are written to the axes before the moves
    are completed, they aren’t used until runsOnATimer is called, correct?

I don’t see anything in this loop that sets the new position values.

Note that the encoder values are updated in real time, and the current position
should be calculated from that.

Then you have the desired position that needs to be calculated for each loop

  1. If the motor is having a hard time completing this move within the
    LOOPINTERVAL (because of not enough power, whatever), the encoder error will
    increase and when it exceeds a particular limit (2.0 mm as default), it will
    throw the system into alarm. Correct?

correct.

David Lang

Thanks for the reply. The position values I refer to are the target “chain lengths” from the kinematics.inverse function that the “movement loop” (i.e., coordinatedMove, singleAxismove, arc) requests. These movement loops divide up the particular gcode move into a number of steps (depending upon the size and speed of the move) and uses the inverse function to determine the target chain length for each particular step. The movement loops in motion.cpp don’t have any feedback… they just send the next target position, via the axis.write() function without care where it currently is… That’s why I asked the questions I did. I assume, however, that the computePID function does look at the encoder feedback when its called and that’s where it tries to make up for the error… and I assume it does because I think that’s what a PID controller does.

I ask all of this because if someone wanted to slave a second arduino/motor shield to an existing one to get control over more motors, it appears to me that all that the master controller would need to do to manage a move is to send the desired “chain length” for the two extra motors during each step to the slave controller and then just relay positional error messages to GC that it receives from the slave (and the latter part isn’t technically required). If this is correct, then just a simple 3-wire serial connection between the two is needed. Of course there are other things that need to be managed (settings, etc.) but for a move command, it seems straightforward if the master arduino has the horsepower to calculate four motor positions (or five for z-axis).

I believe that the runsOnATimer loop is ONLY used to do the PID computations. The PID math has to happen at a set interval for it to work correctly (at least in the optimized form we are using) so the functions in that loop just looks at where each motor is and where it should be and computes what the PWM duty cycle should be to correctly position that motor. I believe only the math is done in those functions. The actual command to write the PWM frequency to the driver is done elsewhere to keep this loop as small as possible

Well, I dug in and traced the code and this is how I think it works:

When a gcode line gets parsed, it either calls (from motion.cpp) coordinatedMove, singleAxisMove, or arc. Inside each of those functions, the particular move is chopped into a number of steps and, in a loop, kinematics::inverse is called to get a target left and right “chain length” for each step. These chain lengths gets written to each respective axis using a write(chainlength) command and a movementUpdated variable is set to true. There is really no computation that occurs during the axis.write commands other than dividing the chainlength by the mmPerRotation to set _pidSetpoint for that axis… The move loop checks to see whether or not movementUpdated has been cleared (i.e., set to false) and when it is cleared, it writes the next chainlength. This loop runs until all the steps are complete.

The clearing of movementUpdated occurs only in the runsOnATimer function. This function is called on every 10,000 ms using the TimerOne class. The runsOnATimer clears the movementUpdated variable and then, for each axis, the function calls the computePID function. The particular axis’ computePID function calls its instance of the PIDController to compute the required _pidOutput using the PID settings and the pidSetpoint that was previously written in the move loop. This value is written to the axis’ instance of motorGearBoxEncoder and then the axis calls motorGearBoxEncoder.computePID(). motorGearBoxEncoder.ComputePID() calls its computeSpeed() function to calculate a new _RPM (not sure why since its doesn’t appear to be used) and then calls upon its instance of PIDController to Compute() and, using the resultant _pidOutput, calls it’s motor instance’s additiveWrite() function. The motor instance, then writes to the arduino pins and sets the new duty cycles based upon this value. Since all of this is “blocking”, the movement loop won’t check to see if movementUpdated is cleared until all the PWM duty cycles have been set… so, it seems good. Long story short, the computation of PWM duty cycle and writing to the pins all occurs during the runsOnATimer loop. I’m not sure how the encoder plays into this, but I assume it’s also interrupt driven?

3 Likes

That sounds spot on to me. Great sleuthing. I’m sorry my answer wasn’t complete, I didn’t fully understand what you were tracing.

The encoders are interrupt driven and pretty much keep to themselves.

1 Like

Am I correct in my understanding that the PWM is adjusted by the encoder’s feedback only at the the start of each “step”? For example, assume a move command gets chopped up into 20 steps… at the start of each step, there is an adjustment to the PWM signal duty cycle based upon the encoder’s feedback and there is no adjustment going on during each step’s 10,000 microsecond loop interval? So at the end, there may be a residual error amount and that’s what the axis::endMove command that gets issued after all the steps are completed is intended to resolve?

hmmmm I hadn’t quite thought about it like that, but I believe you are correct. I would think of it as that the PWM frequency can’t be updated more often than the PID values are recomputed, because we use the output of the PID to decide the PWM frequency.

The endMove command is really there to prevent rounding errors from accumulating over many sequential moves by setting the final target to exactly what the gcode said it should be. It also does some other cleanup I believe like preparing to save the position to eeprom if the machine does not move again for two seconds.