Refactoring Skip Conditions

Filed under: FANUC TP Programming

In my last article we talked about FANUC’s Skip Conditions: how they work, where you might use them and why they can be confusing. We also discussed the SkipJump option which may have better semantics.

Today I want to use the Skip Condition syntax as an opportunity for refactoring. We’ll work with TP parameters and their corresponding argument registers (AR[]), side-step some argument register limitations, discuss return values and eventually create a pretty powerful two-stage search algorithm that has high accuracy without sacrificing cycle time.

A Quick Review

Let’s quickly review Skip Conditions. First you setup a condition to monitor (e.g. SKIP CONDITION RI[1]=ON), and then you use the Skip,LBL (or Skip,LBL,PR) option within your subsequent motion statements.

If the condition becomes true, the motion segment is halted, and the interpreter moves to the next line. If the condition stays false the whole time, the interpreter jumps to the provided label.

LBL[1] ;
  L PR[1:search start] 1000mm/sec CNT0 ;
  SKIP CONDITION RI[1]=ON ;
  L PR[2:search end] 250mm/sec CNT0 Skip,LBL[501] ;
  ! RI[1] turned on ;
  ! do something here ;
  END ;
 ;
LBL[501] ;
  ! RI[1] never turned on ;
  ! retry? ;
  JMP LBL[1] ;

I won’t rehash why the Skip,LBL option functionality feels awkward, but let’s refactor it into a subroutine that’s more explicit.

The SEARCH Subroutine

Sticking with the example above, I’ve extracted the search functionality into a subroutine called SEARCH.

Main routine:

L PR[1:search start] 1000mm/sec CNT0 ;
SKIP CONDITION RI[1]=ON ;
CALL SEARCH ;

SEARCH.TP:

L PR[2:search end] 250mm/sec CNT0 Skip,LBL[501] ;
! condition became true ;
END ;
 ;
LBL[501] ;
  ! RI[1] never turned on ;
  END ;

This obviously has some problems, but it’s a start.

I like how we can set the Skip Condition in the main routine, but right now the main routine has no way of knowing if the search was successful or not.

Let’s use a numeric register to hold the return status from SEARCH.

SEARCH.TP:

L PR[2:search end] 250mm/sec CNT0 Skip,LBL[501] ;
! condition became true ;
R[1:status]=0 ;
END ;
 ;
LBL[501] ;
  ! condition never became true ;
  R[1:status]=1 ;
  END ;

Returning 0 on a true condition might feel wrong to you, but let’s discuss why I’m using that value.

Why Zero?

FANUC tends to use 0 to indicate a successful or normal return status from a program or function; anything non-zero indicates an abnormal or error condition. (This is likely inspired by C’s convention of returning 0 for EXIT_SUCCESS and 1 for EXIT_FAILURE.)

Of course it’s up to you to decide how you want your programs to work, but it’s a good idea to follow conventions when they make sense.


So how does this affect our main program?

LBL[1] ;
  L PR[1:search start] 1000mm/sec CNT0 ;
  SKIP CONDITION RI[1]=ON ;
  CALL SEARCH ;
  IF R[1:status]<>0,JMP LBL[501] ;
  ! RI[1] turned on ;
  ! do something ;
  END ;
  ;
LBL[501] ;
  ! RI[1] never saw anything ;
  ! retry? ;
  JMP LBL[1] ;

At first glance this looks almost identical to the first example, but there is one important difference.

Where the Skip,LBL functionality is sometimes confusing, our IF R[1:status]<>0,JMP LBL[501] is explicit and unmistakable. One just has to be aware of what the 0 status means (see the comment?).


This is a good start, but I don’t like how R[1:status] is hard-coded into the SEARCH program. If our goal is to eventually create a loosely coupled program, we should probably clean that up.

FANUC allows us to provide parameters to our called programs. These can be constant values (e.g. 1, 3.14, 42), strings (e.g. ’foo’, ’bar’), numeric registers (e.g. R[1], R[2]) or argument registers (e.g. AR[1], AR[2]). These parameters are passed by value to the called program and are available in their corresponding argument register.

Example:

CALL FOO(1,42,3.14,’bar’,R[1]) ;

! when FOO.TP is called: ;
! AR[1]=1 ;
! AR[2]=42 ;
! AR[3]=3.14 ;
! AR[4]=’bar’ ;
! AR[5]=the value of R[1] when the CALL statement was executed ;

Let’s pass a numeric register index as a parameter to SEARCH and return the status to R[AR[1]].

SEARCH.TP:

L PR[2:search end] 250mm/sec CNT0 Skip,LBL[501] ;
! condition became true ;
R[AR[1]]=0 ;
END ;
 ;
LBL[501] ;
  ! condition never became true ;
  R[AR[1]]=1 ;
  END ;

Normally we specify a numeric register index directly, but we can use an indirect index by specifying AR[1] as the index. If AR[1]=1, we’ll send the status to R[1]. If AR[1]=42, we’ll send the status to R[42].

NOTE: You can also use numeric registers as indirect indices (e.g. R[R[x]]).

BE CAREFUL: parameter types are not checked until runtime. If we incorrectly provide a string parameter (e.g. CALL SEARCH(‘foo’)) we’ll get an interpreter error when the R[AR[1]] assignments are executed.


Our main routine now looks like this:

LBL[1] ;
  L PR[1:search start] 1000mm/sec CNT0 ;
  SKIP CONDITION RI[1]=ON ;
  CALL SEARCH(1) ;
  IF R[1:status]<>0,JMP LBL[501] ;
  ! RI[1] turned on ;
  ! do something ;
  END ;
 ;
LBL[501] ;
  ! RI[1] never saw anything ;
  ! retry? ;
  JMP LBL[1] ;

You can probably spot a few other places that could be converted into parameters. Let’s parameterize the destination PR index, the speed and the CNT-value.

SEARCH.TP:

! SEARCH performs a linear motion segment against ;
! a predefined skip condition and returns 0 if ;
! the condition becomes true, 1 otherwise. ;
! AR[1] - destination PR index ;
! AR[2] - speed (in mm/sec) ;
! AR[3] - CNT-value ;
! AR[4] - return numreg id ;
 ;
R[102:search speed]=AR[2] ;
R[103:search CNT]=AR[3] ;
 ;
L PR[AR[1]] R[102:search speed]mm/sec CNT R[103:search CNT] Skip,LBL[501] ;
! condition became true ;
R[AR[4]]=0 ;
END ;
 ;
LBL[501] ;
  ! condition never became true ;
  R[AR[4]]=1 ;
  END ;

Lots of changes here.

First I added a block of comments to explain how the SEARCH program works. It’s a good idea to describe how a program’s parameters are used, especially since they’re not named or type-checked.

I moved the status numeric register index to the end of the parameter list for two reasons:

  1. The program’s usage will be easier to understand if the parameters are given in a logical order. I would read the statement CALL SEARCH(2,250,0,1) ; in my head as “Search to PR[2] at 250mm/sec, terminating with CNT0 and return the status to R[1].”
  2. This seems to be FANUC’s convention as well. If you do any KAREL programming, you’ve probably seen many built-ins with a status output as the last parameter.

I also copied the values of AR[2] and AR[3] into numeric registers R[102:search speed] and R[103:search CNT] because TP does not support the use of argument registers to define speeds or CNT-values.

It’s probably a good idea to copy your argument registers over to numeric registers anyway. Not only are they not supported everywhere (SELECT statements too), but there’s also no status screen to see or edit argument registers on the teach pendant.

Let’s go ahead and make this change:

SEARCH.TP:

! SEARCH performs a linear motion segment against ;
! a predefined skip condition and returns 0 if ;
! the condition becomes true, 1 otherwise. ;
! AR[1] - destination PR index ;
! AR[2] - speed (in mm/sec) ;
! AR[3] - CNT-value ;
! AR[4] - return status numreg id ;
 ;
R[101:search pr]=AR[1] ;
R[102:search speed]=AR[2] ;
R[103:search CNT]=AR[3] ;
R[104:search status id]=AR[4] ;
 ;
L PR[R[101]] R[102:search speed]mm/sec CNT R[103:search CNT] Skip,LBL[501] ;
! condition became true ;
R[R[104:search status id]]=0 ;
END ;
 ;
LBL[501] ;
  ! condition never became true ;
  R[R[104:search status id]]=1 ;
  END ;

Let’s try it out in our main program:

LBL[1] ;
  L PR[1:search start] 1000mm/sec CNT0 ;
  SKIP CONDITION RI[1]=ON ;
  ! SEARCH(PR,speed,CNT,returnRegId) ;
  CALL SEARCH(2,250,0,1) ;
  IF R[1:status]<>0,JMP LBL[501] ;
  ! RI[1] turned on ;
  ! do something ;
  END ;
  ;
LBL[501] ;
  ! RI[1] never saw anything ;
  ! retry? ;
  JMP LBL[1] ;

I added a comment to remind us what the parameters are, but I’m pretty happy with the result.

NOTE: Of course you always have to be careful when specifying PR indices (and using motion in general). If you wanted to be extra careful, you could add extra parameters to make sure the robot is using the correct UFRAME and UTOOL in the SEARCH program. You could also validate the PR index. I’ll leave that as an exercise for the reader.

Going Further

Since we’re having so much fun with Skip Conditions and parameters, let’s take them a little further.

You may recall that there is also a Quick Skip (Skip,LBL,PR) motion option which accurately records the current robot position during the skip, but it’s limited to just 100mm/sec. This is inconvenient for long searches.

I’m not sure why it never dawned on me to try this, but a friend emailed me to point out that you can search in two stages to save time.

The first stage is done at a relatively high speed using the Skip,LBL option. The robot should overshoot a bit. You can then search in the opposite direction for the opposite condition with the Quick Skip option for just the overshoot distance without sacrificing accuracy for cycle time. (Thanks for the tip, Bartlomiej!)

Let’s modify our main routine to search in this manner:

LBL[1] ;
  L PR[1:search start] 1000mm/sec CNT0 ;
  SKIP CONDITION RI[1]=ON ;
  ! SEARCH(PR,speed,CNT, returnRegId) ;
  CALL SEARCH(2,250,0,1) ;
  IF R[1:status]<>0,JMP LBL[501] ;
  ! RI[1] turned on ;
  PR[3:search end2]=LPOS ;
  ! search slowly upward 100mm ;
  PR[3,3:search end2]=PR[3,3:search end2]+100 ;
  SKIP CONDITION RI[1]=OFF ;
  L PR[3:search end2] 100mm/sec CNT0 Skip,LBL[502],PR[4]=LPOS ;
  ! RI[1] turned off ;
  ! we now have an accurate LPOS in PR[4] ;
  END ;
  ;
LBL[501] ;
  ! RI[1] never saw anything ;
  ! retry? ;
  JMP LBL[1] ;
 ;
LBL[502] ;
  ! slow search failed ;
  ! go back to top? ;
  JMP LBL[1] ;

This should work, but I think we can refactor and pull a lot of this stuff out. I’ll skip the step where we cut/paste and move directly to a parameterized search function.

SEARCH_RI1.TP:

! SEARCH_RI1 performs a two-stage search ;
! on RI[1] and stores the result in the provided ;
! PR index ;
! AR[1] - destination PR id ;
! AR[2] - fast search speed ;
! AR[3] - fast search CNT ;
! AR[4] - slow search distance ;
! AR[5] - slow search speed ;
! AR[6] - slow search CNT ;
! AR[7] - result PR id ;
! AR[8] - return status numreg id ;
 ;
R[91:search pr id]=AR[1] ;
R[92:search fast speed]=AR[2] ;
R[93:search fast CNT]=AR[3] ;
R[94:search slow dist]=AR[4] ;
R[95:search slow speed]=AR[5] ;
R[96:search slow CNT]=AR[6]
R[97:search result PR]=AR[7] ;
R[98:search status id]=AR[8] ;
 ;
! fast search ;
SKIP CONDITION RI[1]=ON ;
! SEARCH(PR,speed,CNT,returnRegId) ;
CALL SEARCH(R[91:search pr id],R[92:search fast speed],R[93:fast search CNT],R[98:search status id]) ;
IF R[R[98:search status id]]<>0,JMP LBL[501] ;
 ;
! RI[1] turned on ;
PR[100:temp]=LPOS ;
PR[100,3:temp]=PR[100,3:temp]+R[94:search slow dist] ;
 ;
! slow search ;
SKIP CONDITION RI[1]=OFF ;
L PR[100:temp] R[95:search slow speed]mm/sec CNT R[96:search slow CNT] Skip,LBL[502],PR[R[97:search result PR]]=LPOS ;
! RI[1] turned off ;
! we now have an accurate LPOS ;
R[R[98:search status id]]=0 ;
END ;
  ;
LBL[501] ;
  ! fast search failed ;
  ! retain value in R[R[98:search status id]] ;
  ! from SEARCH ;
  END ;
 ;
LBL[502] ;
  ! slow search failed ;
  R[R[98:search status id]]=2 ;
  END ;

Again we copy all of our argument registers over to numeric registers for debugging and flexibility purposes. It would be nice if we didn’t have to do this, but at least we have some extra documentation.

We’ve hard-coded a few things into SEARCH_RI1: the RI index, the temp PR we’re using, and the search direction. TP supports up to 10 parameters in a program call, so you could parameterize those as well.

You could also provide some validation (e.g. check to make sure the provided slow search speed is <= 100mm/sec, make sure the search distance is greater than Xmm, etc.).

Why?

So what was the point of all this?

At first, we were just trying to hide the details of the Skip,LBL motion option, but we then took the opportunity to enhance what we extracted into a more general-purpose SEARCH routine. Going further, we created a useful two-stage search routine that lets our robot find something accurately without taking forever to do so.

All of this is neatly hidden behind a couple small programs:

LBL[1] ;
  L PR[1:search start] 1000mm/sec CNT0 ;
  ! SEARCH_RI1(destPRid,fastSpd,fastCNT,slowDist,slowSpd,slowCNT,resultPRid,statRegId) ;
  CALL SEARCH_RI1(2,250,0,100,100,0,5,1) ;
  IF R[1:status]<>0,JMP LBL[501] ;
  ! we have an accurate value in PR[5:LPOS] ;
  END ;
   ;
LBL[501] ;
  ! 1 - high speed search failed ;
  ! 2 - slow speed search failed ;
  ! retry? ;
  JMP LBL[1] ;

Extra Fanciness

If you want to get extra fancy, you could pass in string parameters for program names. You could use these to gain a sort of callback flexibility that could be use in the error-branches or even to define the actual Skip Conditions.

Main routine:

...
CALL SEARCH(2,250,0,'FAIL_CALLBACK',1) ;
...

SEARCH.TP:

...
LBL[501] ;
  ! condition never became true ;
  SR[1]=AR[4] ;
  ! sorry, can't CALL AR[4] ;
  CALL SR[1] ;
  R[R[104:search status id]]=1 ;
  END ;

Wrap it up already!

This all just goes to show how you can implement the same feature a million different ways. The “correct” or “best” way really comes down to experience, feel and what works best for your customer.

Does it work?
Is it easy to understand and maintain?
Does it make me more productive?

These are the questions I ask myself while writing code and refactoring. In general, I try my best to keep things simple and avoid repeating myself. Parameterized programs like these do just that.


There's more where that came from.

I email (almost) every Tuesday with the latest insights, tools and techniques for programming FANUC robots. Drop your email in the box below, and I'll send new articles straight to your inbox!

No spam, just robot programming. Unsubscribe any time. No hard feelings!