Shop OBEX P1 Docs P2 Docs Learn Events
CNC - I May Not Be Crazy After All.... Multi-Axis Stage Synchronization — Parallax Forums

CNC - I May Not Be Crazy After All.... Multi-Axis Stage Synchronization

idbruceidbruce Posts: 6,197
edited 2017-06-17 18:07 in Propeller 1
This thread is a follow up thread to this previous thread: forums.parallax.com/discussion/166320/cnc-i-may-be-crazy-but

I am starting a new thread for a fresh start, with fresh discussion, to bypass all the jibber jabber in the other thread. So please forgive me.

Anyhow, the previous thread referred to an analogy of using gears to simulate synchronized motion. After thinking about this and studying this for a good portion of the morning, I have come to the conclusion that the gear analogy can be applied to the Propeller for synchronization amongst several axes. I now believe that the key to implementing this analogy is the dreaded floating point support and being able to do comparisons with floating points.

However, I will most likely need a little help from some members to actually prove my theory.

When it comes to the gear analogy, as applied to stepper driven axes, think of the required steps for each axis, as the teeth of individual gears. For example, consider the following step requirements to complete a G0 or G1 movement:

X Axis 137 steps
Y Axis 83 steps
Z Axis 19 steps

137 / 83 = 1.650602409638554 ratio
137 / 19 = 7.210526315789474 ratio

For the sake of discussion and for a quick source to cut and paste from, here is some code that I wrote about a year ago, for synchronizing two axes.
CON

  _CLKMODE            = XTAL1 + PLL16X
  _XINFREQ            = 5_000_000

  'Clock Frequency Equation
  CLK_FREQ            = ((_CLKMODE - XTAL1) >> 6) * _XINFREQ 

  'X Axis  
  X_DIRECTION         = 22
  X_STEP              = 23
  X_DISABLE           = 24

  X_PULSE_WIDTH       = CLK_FREQ / 1_000_000 '= 80
  X_MIN_SPEED         = CLK_FREQ / 5_714 '= 14_000 Rounded
  X_MAX_SPEED         = CLK_FREQ / 16_000 '= 5_000
  X_RAMP_INC_DEC      = CLK_FREQ / 26_666_666 '= 3 Rounded
  X_INC_DIR           = 0 '= Counter-clockwise rotation
  X_DEC_DIR           = 1 '= Clockwise rotation
  X_STEPS_PER_REV     = 2_000 '= 200 Full steps * 10 microsteps
  X_STEPS_PER_METER   = 55_555
  X_RAMPING_STEPS     = (X_MIN_SPEED - X_MAX_SPEED) / X_RAMP_INC_DEC
  X_TOTAL_RAMPING_STEPS = X_RAMPING_STEPS * 2
  X_MAX_STEPS         = 16_933 '12 inches of linear travel
  
  'Y Axis
  Y_DISABLE           = 25
  Y_STEP              = 26  
  Y_DIRECTION         = 27

  Y_PULSE_WIDTH       = CLK_FREQ / 1_000_000 '= 80
  Y_MIN_SPEED         = CLK_FREQ / 5_714 '= 14_000 Rounded
  Y_MAX_SPEED         = CLK_FREQ / 16_000 '= 5_000
  Y_RAMP_INC_DEC      = CLK_FREQ / 26_666_666 '= 3 Rounded
  Y_INC_DIR           = 1 '= Counter-clockwise rotation
  Y_DEC_DIR           = 0 '= Clockwise rotation
  Y_STEPS_PER_REV     = 2_000 '= 200 Full steps * 10 microsteps
  Y_STEPS_PER_METER   = 55_555  
  Y_RAMPING_STEPS     = (Y_MIN_SPEED - Y_MAX_SPEED) / Y_RAMP_INC_DEC
  Y_TOTAL_RAMPING_STEPS = Y_RAMPING_STEPS * 2
  Y_MAX_STEPS         = 16_933 '12 inches of linear travel

VAR

  LONG lMoveNow

  BYTE bX_Direction
  LONG lX_MoveComplete
  LONG lX_RampingSteps
  LONG lX_RunningSteps
  LONG lX_Stack[100]
  
  BYTE bY_Direction
  LONG lY_MoveComplete  
  LONG lY_RampingSteps
  LONG lY_RunningSteps
  LONG lY_Stack[100]

PUB Main

  COGNEW(X_Stage, @lX_Stack)
  COGNEW(Y_Stage, @lY_Stack)
  
  WAITCNT(CNT + (3 * CLKFREQ))

  TestStageSyncronization

PUB TestStageSyncronization

  lX_MoveComplete := TRUE
  lY_MoveComplete := TRUE

  'Prepare X stage for positive movement
  UpdateX_RampingAndRunningSteps(X_MAX_STEPS)
  bX_Direction := X_INC_DIR

  'Prepare Y stage for positive movement  
  UpdateY_RampingAndRunningSteps(Y_MAX_STEPS)
  bY_Direction := Y_INC_DIR

  'Now make the moves
  lMoveNow := TRUE

  'Loop until the moves are complete and then reinitlize the variables
  REPEAT
  WHILE lX_MoveComplete == FALSE AND lY_MoveComplete == FALSE

  lX_MoveComplete := FALSE
  lY_MoveComplete := FALSE

  'Wait a bit for test purposes
  WAITCNT(CNT + (5 * CLKFREQ))

  'Now get ready to go in the opposite direction

  'Prepare X stage for negative movement
  UpdateX_RampingAndRunningSteps(X_MAX_STEPS)
  bX_Direction := X_DEC_DIR

  'Prepare Y stage for negative movement  
  UpdateY_RampingAndRunningSteps(Y_MAX_STEPS)
  bY_Direction := Y_DEC_DIR

  'Now make the moves
  lMoveNow := TRUE

PUB UpdateX_RampingAndRunningSteps(TotalSteps)

  'If maximum speed can be obtained, determine the actual number of
  'ramping and running steps.
  IF TotalSteps > X_TOTAL_RAMPING_STEPS

    'Copy the global amount of ramping steps
    lX_RampingSteps := X_RAMPING_STEPS

    'Determine the number of full speed steps.  
    lX_RunningSteps := TotalSteps - X_TOTAL_RAMPING_STEPS    

   'Else ramp upto the half-way point and then ramp down.    
  ELSE
  
    lX_RampingSteps := TotalSteps / 2
    lX_RunningSteps := TotalSteps // 2

PUB UpdateY_RampingAndRunningSteps(TotalSteps)

  'If maximum speed can be obtained, determine the actual number of
  'ramping and running steps.
  IF TotalSteps > Y_TOTAL_RAMPING_STEPS

    'Copy the global amount of ramping steps
    lY_RampingSteps := Y_RAMPING_STEPS

    'Determine the number of full speed steps.  
    lY_RunningSteps := TotalSteps - Y_TOTAL_RAMPING_STEPS    

   'Else ramp upto the half-way point and then ramp down.    
  ELSE
  
    lY_RampingSteps := TotalSteps / 2
    lY_RunningSteps := TotalSteps // 2

PUB X_Stage | Counter, StartStopSpeed

  DIRA[X_DIRECTION] := 1

  REPEAT

    IF lMoveNow == TRUE

      lMoveNow := FALSE

      StartStopSpeed := X_MIN_SPEED

      'Set up the CTRMODE of Counter A for NCO/PWM single-ended.
      CTRA[30..26] := %00100

      'Set the output pin for Counter A.
      CTRA[5..0] := X_STEP

      'Set the value to be added to PHSA with every clock cycle.
      FRQA := 1

      'Set APIN as an output.
      DIRA[X_STEP] := 1

      'Set the OUTA register to match the desired direction of rotation.
      OUTA[X_DIRECTION] := bX_Direction

      'Get the current System Counter value.
      Counter := CNT

      'Ramp up the stepper motor to maximum speed.
      REPEAT lX_RampingSteps
  
        'Send out a high pulse on the step pin for the desired duration.
        PHSA := -X_PULSE_WIDTH

        'Wait for a specified period of time before sending another
        'high pulse to the step pin.
        WAITCNT(Counter += StartStopSpeed -= X_RAMP_INC_DEC)

      'Maintain maximum speed    
      REPEAT lX_RunningSteps
   
        'Send out a high pulse on the step pin for the desired duration.
        PHSA := -X_PULSE_WIDTH

        'Wait for a specified period of time before sending another
        'high pulse to the step pin.
        WAITCNT(Counter += StartStopSpeed)

      'Ramp down the stepper motor and come to a stop. 
      REPEAT lX_RampingSteps
  
        'Send out a high pulse on the step pin for the desired duration.
        PHSA := -X_PULSE_WIDTH

        'Wait for a specified period of time before sending another
        'high pulse to the step pin.
        WAITCNT(Counter += StartStopSpeed += X_RAMP_INC_DEC)

      lX_MoveComplete := TRUE

PUB Y_Stage | Counter, StartStopSpeed

  DIRA[Y_DIRECTION] := 1

  REPEAT

    IF lMoveNow == TRUE

      lMoveNow := FALSE

      StartStopSpeed := Y_MIN_SPEED

      'Set up the CTRMODE of Counter A for NCO/PWM single-ended.
      CTRA[30..26] := %00100

      'Set the output pin for Counter A.
      CTRA[5..0] := Y_STEP

      'Set the value to be added to PHSA with every clock cycle.
      FRQA := 1

      'Set APIN as an output.
      DIRA[Y_STEP] := 1

      'Set the OUTA register to match the desired direction of rotation.
      OUTA[Y_DIRECTION] := bY_Direction

      'Get the current System Counter value.
      Counter := CNT

      'Ramp up the stepper motor to maximum speed.
      REPEAT lY_RampingSteps
  
        'Send out a high pulse on the step pin for the desired duration.
        PHSA := -Y_PULSE_WIDTH

        'Wait for a specified period of time before sending another
        'high pulse to the step pin.
        WAITCNT(Counter += StartStopSpeed -= Y_RAMP_INC_DEC)

      'Maintain maximum speed    
      REPEAT lY_RunningSteps
   
        'Send out a high pulse on the step pin for the desired duration.
        PHSA := -Y_PULSE_WIDTH

        'Wait for a specified period of time before sending another
        'high pulse to the step pin.
        WAITCNT(Counter += StartStopSpeed)

      'Ramp down the stepper motor and come to a stop. 
      REPEAT lY_RampingSteps
  
        'Send out a high pulse on the step pin for the desired duration.
        PHSA := -Y_PULSE_WIDTH

        'Wait for a specified period of time before sending another
        'high pulse to the step pin.
        WAITCNT(Counter += StartStopSpeed += Y_RAMP_INC_DEC)

      lY_MoveComplete := TRUE

From this code, hopefully you can see the attempt at the synchronization. Anyhow, lets examine a small portion of this code:
      'Ramp up the stepper motor to maximum speed.
      REPEAT lY_RampingSteps
  
        'Send out a high pulse on the step pin for the desired duration.
        PHSA := -Y_PULSE_WIDTH

        'Wait for a specified period of time before sending another
        'high pulse to the step pin.
        WAITCNT(Counter += StartStopSpeed -= Y_RAMP_INC_DEC)

      'Maintain maximum speed    
      REPEAT lY_RunningSteps
   
        'Send out a high pulse on the step pin for the desired duration.
        PHSA := -Y_PULSE_WIDTH

        'Wait for a specified period of time before sending another
        'high pulse to the step pin.
        WAITCNT(Counter += StartStopSpeed)

      'Ramp down the stepper motor and come to a stop. 
      REPEAT lY_RampingSteps
  
        'Send out a high pulse on the step pin for the desired duration.
        PHSA := -Y_PULSE_WIDTH

        'Wait for a specified period of time before sending another
        'high pulse to the step pin.
        WAITCNT(Counter += StartStopSpeed += Y_RAMP_INC_DEC)

      lY_MoveComplete := TRUE


This small section of code actually works quite well for being written in Spin, however for what I will be proposing, I believe that Spin will be far to slow, but I will be discussing it in Spin, just so that most folks can participate in the discussion.

Okay... So now let's assume that in addition to ramping up steps, running steps, and ramping down steps, I also maintain the current count of steps taken, and let's assume that the axis with the most steps is the master. During each iteration, of the control loop for the master, we will have the current step number. A comparison is made for the individual slave ratios, for the current step number and the next step number. Can multiples of a slave ratio fit between these to steps, and if so, then UPDATE a global variable with a timing offset, so that the slave makes a step at he precise time, according to the ratio of the slave.

So basically, each of the loops, shown immediately above, would do these comparisons, which would all be floating point comparisons.

I believe that by incorporating floating point support into the above loops, we could achieve the gear ratio analogy, which would include ramp ups and ramp downs of various axes.
«13

Comments

  • I am not sure about your need for Floating Point.

    With 32-bit longs one could use Fix-Point integer to do the math, on the end you need integer as result anyways. What Bresenham does (your analogy with gears) is to synchronize two (or more) axis by letting the axis with less movement wait longer between steps so that both(all) axis start to move at the same time and end their moves at the same time.

    When using one Cog per axis we can use CNT to sync since all Cogs have the same cnt value.

    So for your given numbers-ratios one could say

    wait 100 cnt counts between each step of the x-axis
    wait 165 cnt counts between each step of the y-axis
    wait 721 cnt counts between each step of the z-axis.

    Now we need to include the numbers of cnt counts used by one step. This can vary for different axis. Say one axis is a stepper, one a servo, one a linear actuator.

    say x-axis needs 10 PASM-Instructions for one step, say 40 cnt counts.
    say y-axis needs 20 PASM-Instructions for one step. say 80 cnt-counts.
    say z-axis needs 15 PASM-Instructions for one step. say 60 cnt-counts.

    so x-axis needs to wait 100-40=60 counts after each step.
    and y-axis needs to wait 165-80=85 counts after each step.
    and z-axis needs to wait 721-60=661 counts after each step.

    you can't lose any steps by rounding errors (like adding float increments to position values will give you).

    it takes into account how long a axis need to make the move of one step (and even ask a encoder if step done).

    it is easy adaptable to a variety of PASM drivers for different servo/encoder/actuator. And automagically adapts to more then 3 axis.

    Just run 6 PASM driver for U,V,W,X,Y,Z

    Enjoy!

    Mike
  • idbruceidbruce Posts: 6,197
    edited 2017-06-17 20:15
    Mike

    The waitcnts are never consistent amongst the axes, due to ramp ups and ramp downs. The master sets the pace, according to it's specific ramp and ramp downs. The ratio is used to determine when the slaves fire based upon the specific pace and step index of the master. Just before the master makes a step, it sets a timer for each of the slave axes, based upon the masters pace and step index, providing it is within the slave ratio parameters for a step time.
  • I think I could write a slow moving example of what I am talking, but it will not be until later. To make it faster, it could be rewritten in C or PASM.
  • msrobotsmsrobots Posts: 3,709
    edited 2017-06-17 21:02
    The PASM driver are quite simple, then.

    Parameter Block would contain a start-cnt, new-position, delay-cnt (between steps)
    COG would wait for command in commandloop.
    then waits for CNT to start synchronized with other COGs
    then repeat
      do step
      then wait delay-cnt counts
    when finished back to commandloop
    

    Main COG gets next G-code calculates delays for each axis, fills parameter blocks and when ready activate commands for all axis by writing a start-cnt value.

    Enjoy!

    Mike
  • idbruce wrote: »
    Mike

    The waitcnts are never consistent amongst the axes, due to ramp ups and ramp downs. The master sets the pace, according to it's specific ramp and ramp downs. The ratio is used to determine when the slaves fire based upon the specific pace and step index of the master. Just before the master makes a step, it sets a timer for each of the slave axes, based upon the masters pace and step index, providing it is within the slave ratio parameters for a step time.

    Exactly, I think we are barking here at the same tree.

    to activate a movement, I want the master to write the ratio in the parameter blocks for each axis and then a (future) cnt value into the parameter blocks of all a axis. All COGS will then wait for that cnt value and start their movements syncron. That is the same as your:

    Just before the master makes a step, it sets a timer for each of the slave axes, based upon the masters pace and step index, providing it is within the slave ratio parameters for a step time.


    But each axis just gets start cnt, new position and - based on ratio - a ideal step-time per step. Ramping would be done by each axis like you are doing it now. This could also be synchronized via a count value, 'ramp up in 200 counts then full speed' or something along this line.

    The main goal is that the axis-COG does all the movement for the command, reporting continuously back in the Parameter block if success or error.

    The master COG then can issue a movement to all axis-Cogs synchronized, and then can monitor the parameter blocks for actual position and failure-conditions.

    Basically what your spin code does, except the axis-cogs do the steps by them self and the master just sends complete movements, not steps.

    Because on a multiprocessor COGs can do work at the same time, even if in most Propeller programs the calling COG waits for the result of the called cog, But especially here it makes sense to use the independence of a COG to drive the movement smooth, without having to serialize all steps of all axis to send the movement for each step from a single master COG.

    And that is something the P1/P2 can and ARM/PIC/Arduino/... can't.

    They have to run it from one single process, serial/sequencial, and resort to interrupts to get stuff done in between, from the same cpu, just interrupting the main serial/sequential process. That's why the TeaCup software is such a mess and not easy to port to the P1.

    It has to be done the Propeller (and @chip) way. Then it simplifies itself.

    Enjoy!

    Mike





  • jmgjmg Posts: 15,173
    idbruce wrote: »
    When it comes to the gear analogy, as applied to stepper driven axes, think of the required steps for each axis, as the teeth of individual gears. For example, consider the following step requirements to complete a G0 or G1 movement:

    X Axis 137 steps
    Y Axis 83 steps
    Z Axis 19 steps

    137 / 83 = 1.650602409638554 ratio
    137 / 19 = 7.210526315789474 ratio


    This small section of code actually works quite well for being written in Spin, however for what I will be proposing, I believe that Spin will be far to slow, but I will be discussing it in Spin, just so that most folks can participate in the discussion.

    ...
    So basically, each of the loops, shown immediately above, would do these comparisons, which would all be floating point comparisons.

    I believe that by incorporating floating point support into the above loops, we could achieve the gear ratio analogy, which would include ramp ups and ramp downs of various axes.
    The low level code here includes time, so this is very similar to my way-points suggestion.

    As mentioned above, I'm not sure the lowest level code needs to be floating point - sure, you can calculate the fit using floating point, but then those final WAITCNT lines are all using 32b values, not floating point.
    Notice in all cases, the Steps, and dT's can be whole numbers.


    This keeps floating point away from the fastest code, and also means you have more chance of fitting the move-to code into a COG, in PASM.

    Then you have the choice of doing the floating point fits, in a separate slower COG, or upstream in something better able to manage floating point.
    That's quite a clean delineation of which code does what.

  • Mike

    I still don't think you understand what I am trying to say. Let's look at the step requirements first...

    X Axis 137 steps
    Y Axis 83 steps
    Z Axis 19 steps

    137 / 83 = 1.650602409638554 ratio
    137 / 19 = 7.210526315789474 ratio

    In this scenario, X is the master, with the most steps. So now let's look at the ramp up loop for the master.
          'Ramp up the stepper motor to maximum speed.
          REPEAT lX_RampingSteps
      
            'Send out a high pulse on the step pin for the desired duration.
            PHSA := -X_PULSE_WIDTH
    
            'Wait for a specified period of time before sending another
            'high pulse to the step pin.
            WAITCNT(Counter += StartStopSpeed -= X_RAMP_INC_DEC)
    

    And now a modified loop
          'Ramp up the stepper motor to maximum speed.
          REPEAT lX_RampingSteps
      
            'Send out a high pulse on the step pin for the desired duration.
            PHSA := -X_PULSE_WIDTH
    
            'update Y timer
    
            if(a multiple of Y ratio is in between current step and the next step)
    
            'multiples: 1.650602409638554, 3.301204819277108, 4.951807228915662,
            '6.602409638554216, 8.25301204819277, 9.903614457831324, etc...
    
            'So Y axis should only step in between steps 1 and 2, 3 and 4, 4 and 5, 6 and 7,
            '8 and 9, '9 and 10, etc...
    
                    UpdateYStepTime(Y_WaitTime) 'Based upon a portion of StartStopSpeed
    
            'update Z timer
    
            if(a multiple of Z ratio is in between current step and the next step)
    
            'multiples: 7.210526315789474 , 14.42105263157895, 21.63157894736842,
            '28.8421052631579, 36.05263157894737, 43.26315789473684, etc...
    
            'So Z axis should only step in between steps 7 and 8, 14 and 15, 21 and 22,
            '28 and 29, '36 and 37, '43 and 44, etc...
    
                    UpdateZStepTime(Z_WaitTime)  'Based upon a portion of StartStopSpeed
    
            'Wait for a specified period of time before sending another
            'high pulse to the step pin.
            WAITCNT(Counter += StartStopSpeed -= X_RAMP_INC_DEC)
    
  • jmg
    This keeps floating point away from the fastest code, and also means you have more chance of fitting the move-to code into a COG, in PASM.

    In my last post, pertaining to the modified loop, floating point support would be necessary for comparisons, but most importantly these calculations:
                    UpdateYStepTime(Y_WaitTime) 'Based upon a portion of StartStopSpeed
                    UpdateZStepTime(Z_WaitTime)  'Based upon a portion of StartStopSpeed
    

    I believe the portion of StartStopSpeed which would be used to set the timer offsets would be calculated using the fractional portion of the individual ratios
  • jmgjmg Posts: 15,173
    edited 2017-06-18 01:16
    idbruce wrote: »
    In my last post, pertaining to the modified loop, floating point support would be necessary for comparisons, but most importantly these calculations:
    ...
    I believe the portion of StartStopSpeed which would be used to set the timer offsets would be calculated using the fractional portion of the individual ratios

    perhaps if we work backwards in WAITCNT units, this will be clearer
    If I use your numbers :
    X Axis 137 steps
    Y Axis 83 steps
    Z Axis 19 steps

    137 / 83 = 1.650602409638554 ratio
    137 / 19 = 7.210526315789474 ratio

    Suppose that X axis has a step dT of 10,000 SysCLKS at 80MHz, for a step rate of 125us, and a path transit time here, of 17.125ms
    The ideal Y dT becomes 16506.02409 SysCLKS, and Z dT is 72105.26315

    The INT-rounded Transit times are 83*16506/80M = 17.124975ms and 19*72105/80M = 17.1249375ms, & the latter case finished 62.5ns early.

    If that 62.5ns is a problem, you could use a fixed-point rate-multiplier type fractional dither, and here a 1 in 4 == 0.25 fraction, would finish within 3.125ns, or well under 1 SysCLK.
    Notice how the WAITCNT values here are all whole numbers, and if you wanted to preserve precision over many whole numbers, there are a couple of choices - you can calculate and send each dT, or you can use an approximation via fixed-point rate multiplier.
    In the rate-multiplier case, you dynamically set dT to a choice of either 72105, or 72106, and [72105,72105,72105,72106] averages to 72105.25

  • idbruceidbruce Posts: 6,197
    edited 2017-06-18 01:58
    jmg

    I only managed to grasp a small portion of what you were trying to tell me. Is there any possibility I could talk you into providing a pseudo code example? Perhaps that will help me to get a better hold of this.
  • jmgjmg Posts: 15,173
    idbruce wrote: »
    jmg

    I only managed to grasp a small portion of what you were trying to tell me. Is there any possibility I could talk you into providing a pseudo code example? Perhaps that will help me to get a better hold of this.
    There is not much difference to what you have already around WAITCNT, - you chain together a lot of WAITCNTs to get the profiles and tracking you want.
    The key point, was the WAITCNTs are all integer, or possibly fractional fixed-point. No floating point is needed at this layer.
    I'd suggest starting without complex acceleration profiles at the lowest levels, then see if there is code space to add that.

    You work in the time-domain, and make the transit-times match, for all relevant axes.

    There is some cost to adding bits to a rate multiplier fraction, so I'd work backwards - if we take 80M and let's say 26 bits of time, that allows 12.5ns LSB to a slowest step rate of
    just under 1 second (838.8608ms), that gives 6 bits spare for fixed fractional steps, in a 32bit dT value calculated.

    A 6 bit Rate multiplier is 6 IF..ELSIF statements, and runs a 6b counter (Fc), and the 6b Fractional value, and you can average over many delays, to a finer-resolution.


  • Try working on a machine where X,Y, and Z axis have a different pitch and keeping position accurately down to one ten thousandth ... Of the X,Y and Z axis, two are metric and one is standard, and one is a double start thread. All of the math is done in a single threaded PIC 18F8722 microcontroller with all of the code written in Assembly language. The user is able to select between inches or mm on the fly so there is that conversion to contend with as well and still maintain accuracy.

    You can do this Bruce, just need to think outside of the box a little bit... I had to write math routines that perform operations as if you were doing them long hand ... 10 Digits of BCD to 24-bit binary and back in longhand.
  • kwinnkwinn Posts: 8,697
    IIRC we have had this discussion before, but I do not recall what if anything was concluded. The requirement seems pretty straight forward to me, and seems to be similar to what jmg posted:
    Generate a motion clock (sysclk) to synchronize movement of all axis.
    Calculate the number of steps required by each axis to move from A to B
    Use the largest number of steps to calculate the ratio for the other axis - use fixed point math.
    Use the ratio to calculate the number of sysclk’s per step for each axis – also fixed point math.
    Start with a low frequency sysclk and increase it to control speed and acceleration.
    

    Probably simpler to use a pin or hub memory location for sysclk to simplify acceleration control.
  • You don't need float for the inner loop - you need each of (n) axis to take some number of steps, and Bresenham will synchronize the axes with fewer steps to the one with the most steps, and you can reduce jitter by doubling or quadrupling the "driver" steps. It's quite easy to implement, all in integer math, in one cog - you wasting horsepower by running a motor per cog - honest. It sounds appealing, but you'd be using parallel CPU's to solve the wrong problem.
  • idbruceidbruce Posts: 6,197
    edited 2017-06-18 12:42
    Jason

    I honestly don't know how this will all work out, but if I can get the steppers to attain and maintain a decent speed, then I think that there could be benefits to this code, even though I maybe wasting horsepower.

    The way I see it, this code has the potential to eliminate a lot of the messy and difficult code associated with CNC operation, such as trajectory planning, lookahead, etc..., because those operations will be done on the fly.

    I currently do not have any particular use for this code and it is more just of an experiment to see if I can achieve master and follower operation, using the gearing analogy.

    I have linked to this particular IC chip numerous times throughout the forum, but at the risk of being overly redundant here it is again. Chip: novaelec.co.jp/eng/MCX514_e.html

    Instead of being applicable to any particular CNC machine or operation, this code is meant to experiment with some similarities between the Propellers abilities and the above linked chip.
  • msrobotsmsrobots Posts: 3,709
    edited 2017-06-18 22:55
    @kwinn, @jmg,

    you solved a problem for me, I was stumbling about, the ramping.

    My plan was to use CNT to sync all axis-COGS. But using the counters and provide a pin-based master frequency would solve all the ramping issues by just varying the frequency of that sysclock-pin. Can't do that with CNT.

    Neat.

    So back to simple steps without ramping, way more easy for the axis-cog.

    I am just throwing together a simple test harness for my plan using one COG each axis. Not that I need a G-Code controller at all, its more some sort of brain-gymnastic to keep my mind off my current job for a while.

    Never did much with the counters of P1, need to read up there. But a hub-location would be quite easy to use.

    Enjoy!

    Mike

  • jmgjmg Posts: 15,173
    msrobots wrote: »
    @kwinn, @jmg,

    you solved a problem for me, I was stumbling about, the ramping.

    My plan was to use CNT to sync all axis-COGS. But using the counters and provide a pin-based master frequency would solve all the ramping issues by just varying the frequency of that sysclock-pin. Can't do that with CNT.

    Neat.

    So back to simple steps without ramping, way more easy for the axis-cog.

    I am just throwing together a simple test harness for my plan using one COG each axis. Not that I need a G-Code controller at all, its more some sort of brain-gymnastic to keep my mind off my current job for a while.

    Never did much with the counters of P1, need to read up there. But a hub-location would be quite easy to use.
    Yup, the real power in P1 is in the granular ability of WAITCNT, and in more than one place. AVR's or even ARM's cannot come close to that.

    Jason is right, in that you do not have to use multiple COGs, but they are there already, and have some benefits on allowing differing drive types (any mix of Step.Dirn and Qa.Qb is possible), and fast fault protection, easier debug, less caveats, etc.

    Quick exercise:
    a) The simplest, single COG low level code could accept 6 bits of direct port XOR as 3 axis Step.Dirn, and 32-6=26 bits of dT (838ms), as 32b streamed data.
    The streamed info can then also embed the Step pulse width, and can locate any edge to SysCLK levels of precision, with some caveats around very close pulses.

    b) Multi-COG approach could be broadly similar encoding, but splits the hub mailboxes to a per-axis basis (with larger average dT), and less caveats as adjacent (same COG) pulses are now step-rate spaced (a large number of SysCLKs) and Step pulse width can be locally generated, which lowers bandwidths.

    One parameter to watch here, is how fast you can feed coordinates, and I think QuadSPI has benefits over classic Serial.
    QuadSPI allows R/W to Serial FLASH, and also to USB-QuadSPI bridge parts with minimal changes.



  • Kwinn, Jason, jmg, msrobots,
    I may be wrong, because this is way above my head, but it seems you guys (aka as the P1 beyond-the-box brains trust ) seem to have reached a broad consensus methodology on the way forward for P1 CNC control.
    Now who can code up a starter demo?
  • kwinnkwinn Posts: 8,697
    macrobeak wrote: »
    Kwinn, Jason, jmg, msrobots,
    I may be wrong, because this is way above my head, but it seems you guys (aka as the P1 beyond-the-box brains trust ) seem to have reached a broad consensus methodology on the way forward for P1 CNC control.
    Now who can code up a starter demo?

    I'm flattered, but not me. I have little or no time for the foreseeable future, and I get the impression that Jason, jmg, and msrobots have far more hands on experience in that area than I do.
  • macrobreak

    It will most likely be Jason, Dave Hein, and myself, that resolve the CNC endeavor, because all three of us have been knee deep into the CNC code, but apparently I am insignificant, because you left my name off the list :)
  • My apologies idbruce, unfortunately oversight on my part.
  • Okay Guys...
    This small section of code actually works quite well for being written in Spin, however for what I will be proposing, I believe that Spin will be far to slow, but I will be discussing it in Spin, just so that most folks can participate in the discussion.

    Considering my past efforts, I now believe it is best to abandon the Spin ship, because most of my serious efforts have been done in C.

    It took me a while to decide what I wanted, needed, and what code I had available for testing my present idea, as well as testing the ideas of others. After altering some of my past code, to make it adaptable to the PBOE, instead of my controller boards, I think I have come pretty close to a test container, that we can use to test various schemes.

    I must freely admit that it has been a while since I last messed with this code. However, I do believe this code, or a good portion of it, should be able to open an SD file, and parse G1 commands, and initiate movement. And perhaps I am jumping the gun a bit, but I am tossing it up here for now, just in case someone wants to toy with it.

    Tomorrow I will be testing various aspects of it in greater detail.

    In it's current state, the build is very close to reaching the memory barrier.


  • Hi idbruce,

    I haven't read the whole thread but I'm not sure if you have the right "scope" of what problems really need to be solved to build a CNC motion controller. The "gear" idea doesn't help much. You need to consider...

    * Direction can change during a movement, so assigning a master axis doesn't make much sense. Only G0 movements can be done isolated, e.g. as unique ramp-up, travel, ramp-down sequence. For a circular movement the velocity of each axis can cross zero and change sign while moving.

    * Ramp-up and down can span several G1 moves. Or in other words, you really don't want to stop at the end of each G1 move. Ramps and axis synchronisation (trajectory interpolation) are ideally handled separately.

    * Bresenham doesn't work well because it generates frequency jitter. Assume the master axis runs at 10kHz and has to do 3000 steps while the slave makes 2000 steps. The slave would end up making a series of step and no-step cycles resulting in a frequency range of 5 to 10kHz. Pixels on a graphic screen don't have inertia, but mechanics do. The motor had to accelerate and decelerate each time to half/double the current speed making it very sensible to stalling. Even with microstepping the available torque drops drastically the more frequency jitter is present.

    For all this reasons I would vote for the multi-cog solution. One cog for trajectory planning and ramps and one cog for each axis.
  • ManAtWork
    Direction can change during a movement, so assigning a master axis doesn't make much sense.

    Can you please provide a G-Code example, as well as the type of equipment?
    For a circular movement the velocity of each axis can cross zero and change sign while moving.

    I realize this. A linear movement can also cross zero and change sign while moving.
    Ramp-up and down can span several G1 moves. Or in other words, you really don't want to stop at the end of each G1 move.

    Such as machining a square or rectangle with rounded corners?
    Ramps and axis synchronisation (trajectory interpolation) are ideally handled separately.

    Perhaps this comment and the last may be true for some machining processes, but I do not believe it will hold true for all CNC equipment. For example a drilling CNC machine or a pick and place machine.
    The slave would end up making a series of step and no-step cycles resulting in a frequency range of 5 to 10kHz.

    If you are using stepper motors, instead of servo motors, how can this be avoided? You will always have one step rate that is higher and one that is lower, unless they are moving equal distances. And once again, unless you are removing material during axis travel, I do not believe this would be a problem.

    There are a lot of CNC machines that can be built and based upon starting at zero and stopping at zero
  • Frequency jitter is one of the ways that complex stepper drivers deal with resonance, so it might not be as much of a problem as you think. Microstepping takes care of some of it, inertia buffers some of it, and having an increased internal "virtual" axis that drives the others works well to reduce it too.
  • idbruceidbruce Posts: 6,197
    edited 2017-06-20 13:42
    I have not had much time to work on this project, but for the last couple of days, I have been bouncing ideas around in my head. As you may have noticed, a couple posts back, I announced that I was now changing this endeavor towards C programming, and also, as you may have noticed, I also included a SimpleIDE attachment. This attachment includes four axis drivers, so that each axis may run in it's own cog, and it also includes files for a GCODE_STRUCT, which attempts to provide all the data necessary for a four axis movement. In the source code below, you will find the source for one axis driver and a current copy of the GCODE_STRUCT.
    #include "x_axis_driver.h"
    #include "config.h"
    #include "gcode_struct.h"
    #include "Gearing.h"
    
    /// cog variables
    static int x_axis_cog = 0;
    static int x_axis_stack[100];
    
    volatile bool x_move_complete;
    
    void x_axis_driver(void *par);
    
    void x_axis_init()
    {
    	if(x_axis_cog == 0)
    	{
    		x_axis_cog = 1 + cogstart(&x_axis_driver, NULL,
    			x_axis_stack, sizeof x_axis_stack);
    	}
    }
    
    /// stop x_axis cog
    void x_axis_stop()
    {
    	if(x_axis_cog > 0)
    	{
    		cogstop(x_axis_cog - 1);
    		x_axis_cog = 0;
    	}
    }
    
    void x_axis_driver(void *par)
    {
    	int32_t x_counter;
    
    	// Set DIRA as an output.
    	DIRA |= 1 << X_DIRECTION;
    
    	do
    	{
    		if(app_move_now_x == true)
    		{
    			app_move_now_x = false;
    
    			// Set up the CTRMODE of Counter A for NCO/PWM single-ended.
    			CTRA = (4 << 26) | X_STEP;
    
    			// Set the value to be added to PHSA with every clock cycle.
    			FRQA = 1;
    
    			// Set DIRA as an output.
    			DIRA |= 1 << X_STEP;
    
    			// Set the OUTA register to match the desired direction of rotation.
    			if(current_gcode.x_dir == 0)
    			{
    				OUTA &= (~1 << X_DIRECTION);
    			}
    			else
    			{
    				OUTA |= 1 << X_DIRECTION;
    			}
    
    			// Get the current System Counter value.
    			x_counter = CNT;
    
    			// Ramp up the stepper motor a predetermined amount of steps
    			while(current_gcode.x_ramp_up_steps != 0)
    			{
    				// Send out a high pulse on the step pin for the desired duration.
    				PHSA = -X_PULSE_WIDTH;
    
    				// Wait for a specified period of time before sending another
    				// high pulse to the step pin.
    				waitcnt(x_counter += current_gcode.x_start_stop_speed -= current_gcode.x_ramp_inc_dec);
    
    				// Decrement ramp_up_steps
    				current_gcode.x_ramp_up_steps--;
    			}
    
    			// Run the stepper motor at the current speed for a predetermined amount of steps
    			while(current_gcode.x_running_steps != 0)
    			{
    				// Send out a high pulse on the step pin for the desired duration.
    				PHSA = -X_PULSE_WIDTH;
    
    				// Wait for a specified period of time before sending another
    				// high pulse to the step pin.
    				waitcnt(x_counter += current_gcode.x_start_stop_speed);
    
    				// Decrement running_steps
    				current_gcode.x_running_steps--;
    			}
    
    			// Ramp down the stepper motor a predetermined amount of steps
    			while(current_gcode.x_ramp_down_steps != 0)
    			{
    				// Send out a high pulse on the step pin for the desired duration.
    				PHSA = -X_PULSE_WIDTH;
    
    				// Wait for a specified period of time before sending another
    				// high pulse to the step pin.
    				waitcnt(x_counter += current_gcode.x_start_stop_speed += current_gcode.x_ramp_inc_dec);
    
    				// Decrement ramp_down_steps
    				current_gcode.x_ramp_down_steps--;
    			}
    
    			x_move_complete = true;
    		}
    	}
    	while(x_axis_cog > 0);
    }
    
    #ifndef _GCODE_STRUCT_H
    #define _GCODE_STRUCT_H
    
    #include <stdint.h>
    
    /// Structure for holding GCODE fileline data
    typedef struct
    {
    	char valid[10];
    	char gcode_fileline[100];
    
    	char g_char[3];
    	char m_char[3];
    	char t_char[1];
    	char s_char[5];
    	char p_char[5];
    	char x_char[9];
    	char y_char[8];
    	char z_char[8];
    	char e_char[8];
    	char f_char[8];
     
    	uint8_t g_num;
    	uint8_t m_num;
    	uint8_t t_num;
    	uint16_t s_num;
    	uint16_t p_num;
    	double x_num;
    	double y_num;
    	double z_num;
    	double e_num;
    	double f_num;
    
    	double x_travel;
    	double y_travel;
    	double z_travel;
    	double e_travel;
    
    	uint8_t x_dir; 
    	uint32_t x_start_stop_speed;
    	uint32_t x_total_steps;
    	uint32_t x_ramp_up_steps;
    	uint32_t x_running_steps;
    	uint32_t x_ramp_down_steps;
    	uint32_t x_ramp_inc_dec;
    
    	uint8_t y_dir;
    	uint32_t y_start_stop_speed;
    	uint32_t y_total_steps;
    	uint32_t y_ramp_up_steps;
    	uint32_t y_running_steps;
    	uint32_t y_ramp_down_steps;
    	uint32_t y_ramp_inc_dec;
    
    	uint8_t z_dir;
    	uint32_t z_start_stop_speed;
    	uint32_t z_total_steps;
    	uint32_t z_ramp_up_steps;
    	uint32_t z_running_steps;
    	uint32_t z_ramp_down_steps;
    	uint32_t z_ramp_inc_dec;
    
    	uint8_t e_dir;
    	uint32_t e_start_stop_speed;
    	uint32_t e_total_steps;
    	uint32_t e_ramp_up_steps;
    	uint32_t e_running_steps;
    	uint32_t e_ramp_down_steps;
    	uint32_t e_ramp_inc_dec;
    
    } GCODE_STRUCT;
    
    /// Structures for holding the current and
    /// previous GCODE fileline data.
    extern volatile GCODE_STRUCT current_gcode;
    extern GCODE_STRUCT previous_gcode;
    
    #endif /* _GCODE_STRUCT_H */
    

    The reason I am posting this code, is because I now have a new concept, pertaining to the gearing concept and the attached, which is as follows. The current program model includes four drivers intended to run in separate cogs, but considering the concept of the gearing scenario, I eventually came to the conclusion that all movement should be contained within one cog. However, the problem then became an issue of master/follower identification, relationship, pin identification, and pin states to establish direction of rotation. While thinking about the gearing concept, I thought to myself, "It sure would be nice if I could set master/follower parameters, within one cog, before initiating movement". This morning, I think I have come up with the answer. I believe the answer is to alter the GCODE_STRUCT to include pin identifications and axis rotation information, as well as the ratios required for synchronization.

    Yep... I do believe that this might be the answer to a problem that I have been trying to solve for a very long time and if not, then I guess I will jump on board with Jason and Bressenham :)

    EDIT: Okay, that will not work, considering the following...
                    UpdateYStepTime(Y_WaitTime) 'Based upon a portion of StartStopSpeed
                    UpdateZStepTime(Z_WaitTime)  'Based upon a portion of StartStopSpeed
    

    These updates will have to take place in other cogs, otherwise they will slow down the master axis and remove any possibility of synchronization.
  • idbruceidbruce Posts: 6,197
    edited 2017-06-20 16:17
    Within the attachment that I provided earlier, you will find move_struct.c and move_struct.h. These files were pretty much useless, because they were not being used. However move_struct.h has now been altered to fit my proposed experiment, and the MOVE_STRUCT is now defined as follows:
    #ifndef _MOVE_STRUCT_H
    #define _MOVE_STRUCT_H
    
    #include <stdint.h>
    
    /// Structure for holding movement data
    typedef struct
    {
    	uint8_t master_dir_pin;
    	uint8_t master_step_pin;
    	uint8_t master_pulse_width;
    	uint8_t master_dir; 
    	uint32_t master_start_stop_speed;
    	uint32_t master_total_steps;
    	uint32_t master_ramp_steps;
    	uint32_t master_run_steps;
    	uint32_t master_ramp_inc_dec;
    
    	uint8_t slave_1_dir_pin;
    	uint8_t slave_1_step_pin;
    	uint8_t slave_1_pulse_width;
    	uint8_t slave_1_dir;
    	uint32_t slave_1_ratio; 
    	uint32_t slave_1_integer_part;
    	uint32_t slave_1_fractional_part;
    
    	uint8_t slave_2_dir_pin;
    	uint8_t slave_2_step_pin;
    	uint8_t slave_2_pulse_width;
    	uint8_t slave_2_dir;
    	uint32_t slave_2_ratio; 
    	uint32_t slave_2_integer_part;
    	uint32_t slave_2_fractional_part;
    
    	uint8_t slave_3_dir_pin;
    	uint8_t slave_3_step_pin;
    	uint8_t slave_3_pulse_width;
    	uint8_t slave_3_dir;
    	uint32_t slave_3_ratio; 
    	uint32_t slave_3_integer_part;
    	uint32_t slave_3_fractional_part;
    
    } MOVE_STRUCT;
    
    /// Structure for holding the current
    /// movement data.
    extern MOVE_STRUCT current_move;
    
    #endif /* _MOVE_STRUCT_H */
    

    In the programs current state, a G-Code file is parsed, and a GCODE_STRUCT is filled from various computations of the parsing. This struct is then passed onto the stepper drivers for movement. However this will all be changing very shortly, whereas the GCODE_STRUCT will simply become a stepping stone for the MOVE_STRUCT. Once the GCODE_STRUCT has been filled from parsing, additional computations will be made from this data, and the MOVE_STRUCT will be filled from these additional computations.

    The MOVE_STRUCT will eventually replace the GCODE_STRUCT within the stepper drivers.

    Nothing is written in stone just yet, but this is the direction I am heading.

    EDIT: It is also worth noting that I believe this effort will significantly shrink the current build size and increase the speed at runtime. This effort should eliminate a lot of the current computations being made and a lot of the variables being used by the G-Code struct, because once the master has been established, and the parameters for the master has been filled, the main parameters for the slaves are the pin numbers, pin states, and the ratio to the master. By utilizing ratios within the master stepper driver loops, many of the computations required for the other axes will no longer be required.

    I am slowly, but surely, becoming a little more enthused about this experiment.
  • I have been modifying code for a good portion of the day and I think I am getting pretty darn close to being able to test the output with some motors. As far as I can tell, without actually testing, I believe the only things that are still necessary, is the slave drivers, and slight, but critical modifications to the master driver.

    If anybody cares to take a peek, I am attaching the source with all the modifications.
  • I am playing around with some Pasm-Code and while looking thru my Project folder I stumble about this

    Written by Don Starkey between 2011 and 2013

    CNC in Spin with Float-Math.

    Haven't looked closely at it, since I am playing with my own code

    Anyways I upload it for the interested.

    Enjoy!

    Mike
  • MicksterMickster Posts: 2,693
    edited 2017-06-21 07:52
    Hey Bruce, l realise that you do lots of research but have you checked-out this forum?

    https://groups.google.com/forum/m/#!topic/machinekit/MsQHBIOJQv0

    Some pretty interesting discussions.

    Also:

    http://www.machinekit.io/docs/code/Code_Notes/#motion-controller-introduction
Sign In or Register to comment.