Numeric registers are global. Position registers are global. I/O is global.
I remember being bothered by this way back in 2008 when I first learned to program FANUC robots and had a stronger hairline. Coming from a strong Ruby background, I hated having to address variables by raw index and having to keep a spreadsheet to make sure I wasn’t using PR[1] for two different things.
I built TP+ in 2014 and Fexcel in 2021 to attack this. Both let you write some variant of the name “home” without the index, but they only solve the naming problem, not the global-scope problem. Under the hood, everything still compiles down to global registers at specific indices. We are still tied to the spreadsheet and manually laying out our memory usage.
So I was genuinely interested when FANUC quietly shipped local register support with v9.40, though I admittedly did not use them much until recently. This is the first feature that helps fix the scope problem, not just naming.
I was working on a CRX Plugin that provided some motion and logic functionality, and I couldn’t just take over a range of registers or position registers from the user. This is the perfect use-case for local registers: utility programs being loaded into an unknown environment/memory layout that must not affect global state.
Global vs. Local
Maybe you don’t come from a software engineering background, so the idea of global vs. local (variable scope) doesn’t make sense to you.
Think of it this way: global variables (like FANUC numeric registers and position registers) are accessible to any program on the robot at any time. In a way, this can be convenient. You never have to give a program access to PR[1:home] – it’s just always there, ready for you to use. The downside is that there’s no operating system/interpreter-level guarantee that some other program isn’t going to mess up that position. It’s just you and your code and whoever else is writing code on that robot.
Let’s just hope no one ever adds this line of code anywhere:
PR[1:home]=PR[2:punch through the floor]
Some programming languages (and FANUC TP now) can declare variables in a local scope, where the variable can only be referenced from inside the current program or function block. This provides a lot more safety, but sharing data gets a little more involved.
The use of global variables in modern software engineering projects is usually considered a bit of a code smell, but it really depends on the codebase.
What we get with FANUC
FANUC provides local numeric registers (R[]), local position registers (PR[]) and local string registers (SR[]). You declare them at the top of your LS file in the /ATTR section like so:
/ATTR
LOCAL_REGISTERS = numRegisters,numPRs,numSRs ;You can also edit the number allocated to your program if you scroll to the bottom of the program detail screen when creating a program or hitting F2 DETAIL on the SELECT menu.
Local registers are then available in the 10000+ range. E.g. if you decided to allocate 2 local numeric registers and 2 local position registers, you can now use R[10001], R[10002], PR[10001] and PR[10002] in your programs.
Limits
| Type | Register | Max per program |
|---|---|---|
| Numeric | R[] |
100 |
| Position | PR[] |
30 |
| String | SR[] |
20 |
Runtime behavior
Local numeric registers start with a zero value, but local position registers are uninitialized. You must initialize them from an argument, LPOS, JPOS, another PR/Position or a variable.
Local register values are held while the robot is paused, but they are cleared any time the current program is unloaded off the call stack (e.g. abort, END, etc.).
What they’re good for
For most applications, probably not a lot, honestly.
Local Numeric Registers
Looking back on a recent project, I had something like this:
R[1:ProgramId]=(GI[1:ProgramId]) ;
SELECT R[1:ProgramId]=1,CALL TASK1 ;
...
CALL RECORD_TIME(R[1:ProgramId]) ;I searched the project, and R[1:ProgramId] is only used in this program, so it might as well be local, right?
OK, we declare a local variable and update our program, but we can’t add a comment!
R[10001]=(GI[1:ProgramId]) ;
SELECT R[10001]=1,CALL TASK1 ;
...
CALL RECORD_TIME(R[10001]) ;We’ve gained some scope safety but lost program readability… may not be worth the trade.
But let’s look closer at RECORD_TIME. This routine uses a scratch area of global registers (code smell, I know) to record the last cycle time for the given routine. Something like this:
R[20:temp]=AR[1]+30 ;
R[R[20:temp]]=TIMER[1] ;I would argue that this routine would benefit from a local register:
R[10001]=AR[1]+30 ;
R[R[10001]]=TIMER[1] ;I don’t think the program’s any less readable. It’s small, and it’s pretty obvious that we are just doing the AR[1]+30 calculation because TP can’t index with an expression (i.e. we can’t do R[(AR[1]+30)]=TIMER[1]).
You could even use arguments to make RECORD_TIME drop-in for any project without worrying about global register collisions:
! AR[1] - program number ;
! AR[2] - start of time buffer ;
R[10001]=AR[1]+AR[2] ;
R[R[10001]]=TIMER[1] ;On another application, the PLC delivers some position data that we validate (never trust the PLC guy):
! get_place_pos ;
R[1:PlaceX]=(GI[1:PlaceX]/10) ;
R[2:PlaceY]=(GI[2:PlaceY]/10) ;
R[3:PlaceZ]=(GI[3:PlaceZ]/10) ;
! validate ;
IF (R[1:PlaceX]<100),JMP LBL[500] ;
IF (R[1:PlaceX]>200),JMP LBL[500] ;
IF (R[2:PlaceY]<100),JMP LBL[501] ;
IF (R[2:PlaceY]>200),JMP LBL[501] ;
IF (R[3:PlaceZ]<0),JMP LBL[502] ;
IF (R[3:PlaceZ]>200),JMP LBL[502] ;
PR[20,1:Place]=R[1:PlaceX] ;
PR[20,2:Place]=R[2:PlaceY] ;
PR[20,3:Place]=R[3:PlaceZ] ;These numeric registers are only used in this routine, and though there’s little danger of another program modifying them since they are immediately used and no multi-tasking is involved, you could use local registers here at a slight cost to readability:
! get_place_pos ;
R[10001]=(GI[1:PlaceX]/10) ;
R[10002]=(GI[2:PlaceY]/10) ;
R[10003]=(GI[3:PlaceZ]/10) ;
! validate ;
IF (R[10001]<100),JMP LBL[500] ;
IF (R[10001]>200),JMP LBL[500] ;
IF (R[10002]<100),JMP LBL[501] ;
IF (R[10002]>200),JMP LBL[501] ;
IF (R[10003]<0),JMP LBL[502] ;
IF (R[10003]>200),JMP LBL[502] ;
PR[20,1:Place]=R[10001] ;
PR[20,2:Place]=R[10002] ;
PR[20,3:Place]=R[10003] ;Local String Registers
I don’t have much use for string registers in general, but I could see you potentially calling a job with a dynamic ID e.g.:
! AR[1] - job id ;
SR[10001]=SR[1:prefix]+AR[1] ;
CALL SR[10001] ;Local Position Registers
On another project, MV_PICK has something like this:
! mv_pick ;
UFRAME_NUM=0 ;
UTOOL_NUM=1 ;
J PR[10:pick perch] 100% CNT100 ;
L PR[11:pick] 500mm/sec CNT25 Offset,PR[12:pick APOS] ;
L PR[11:pick] 250mm/sec FINE ;Would local PRs be helpful here? Probably not… maybe if you wanted to generalize some of this motion. Maybe you find yourself constantly writing stuff like this in your code:
L PR[11:pick] 500mm/sec CNT25 Offset,PR[12:pick APOS] ;
L PR[11:pick] 250mm/sec FINE ;And the offset is ALWAYS only in Z. And the speeds and terminations are always the same.
You could write a generalized routine with a couple of local PRs:
! move_to_with_z_offset ;
! AR[1] - destination ;
! AR[2] - offset Z ;
PR[10001]=AR[1] ;
PR[10002,1]=0 ;
PR[10002,2]=0 ;
PR[10002,3]=AR[2] ;
PR[10002,4]=0 ;
PR[10002,5]=0 ;
PR[10002,6]=0 ;
L PR[10001] 500mm/sec CNT25 Offset,PR[10002] ;
L PR[10001] 250mm/sec FINE ;NOTE: Why are we setting all the
PR[10002]components first? Local PRs are uninitialized when they are allocated on the stack, so we need to set each component before using it, otherwise you will get anINTP-311 Uninitialized data is usederror.
INTERESTING NOTE: Traditionally I’ve used something like
PR[10001]=LPOSto ensure a PR is fully defined before zeroing it out, but testing showed that’s not necessary. Writing all 6 components directly is enough to initialize the PR, and it takes the defaultNDB 000config. (You do need all 6, though… a partial set will still leave it uninitialized.)
Then you could call this in your program:
UFRAME_NUM=0 ;
UTOOL_NUM=1 ;
J PR[10:pick perch] 100% CNT100 ;
CALL MOVE_TO_WITH_Z_OFFSET(PR[11:pick], 100) ;Though I would take issue with not declaring the UFRAME and UTOOL in MOVE_TO_WITH_Z_OFFSET, so it should probably take a couple more parameters:
! move_to_with_z_offset ;
! AR[1] - uframe ;
! AR[2] - utool ;
! AR[3] - destination ;
! AR[4] - offset Z ;
UFRAME_NUM=AR[1] ;
UTOOL_NUM=AR[2] ;
PR[10001]=AR[3] ;
PR[10002,1]=0 ;
PR[10002,2]=0 ;
PR[10002,3]=AR[4] ;
PR[10002,4]=0 ;
PR[10002,5]=0 ;
PR[10002,6]=0 ;
L PR[10001] 500mm/sec CNT25 Offset,PR[10002] ;
L PR[10001] 250mm/sec FINE ;UFRAME_NUM=0 ;
UTOOL_NUM=1 ;
J PR[10:pick perch] 100% CNT100 ;
CALL MOVE_TO_WITH_Z_OFFSET(0, 1, PR[11:pick], 100) ;Is this worth it? Probably not. You trade one global offset PR and 2 lines of code (times however many instances you use this) for a program dependency with an ugly interface.
One thing I’ve found over the years is that with FANUC robots, clear is 100x better than clever. Duplication is preferred over cleverness.
Putting it together: a reusable SEARCH
A more useful use-case for local registers is something like a search with skip conditions:
! AR[1] - search speed ;
! AR[2] - return speed ;
! AR[3] - search component ;
! AR[4] - search distance ;
! AR[5] - status reg id ;
! AR[6] - result PR id ;
R[10001]=AR[1] ;
R[10002]=AR[2] ;
PR[10001]=LPOS ;
PR[10002,1]=0 ;
PR[10002,2]=0 ;
PR[10002,3]=0 ;
PR[10002,4]=0 ;
PR[10002,5]=0 ;
PR[10002,6]=0 ;
IF (AR[3]=1),PR[10002,1]=(AR[4]) ;
IF (AR[3]=2),PR[10002,2]=(AR[4]) ;
IF (AR[3]=3),PR[10002,3]=(AR[4]) ;
;
IF (RI[1]),JMP LBL[500] ;
SKIP CONDITION RI[1]=ON ;
;
L PR[10002] R[10001]mm/sec FINE INC Skip,LBL[501],PR[10002]=LPOS ;
! skip occurred ;
L PR[10001] R[10002]mm/sec FINE ;
R[AR[5]]=0 ;
PR[AR[6]]=PR[10002] ;
END ;
;
LBL[500] ;
! RI[1] already on ;
R[AR[5]]=1 ;
END ;
;
LBL[501] ;
! failed to skip ;
L PR[10001] R[10002]mm/sec FINE ;
R[AR[5]]=2 ;
END ;Then you would use it like this:
J P[1] 100% FINE ;
! search 100mm in x ;
CALL SEARCH(100, 500, 1, 100, 1, 2) ;
IF R[1:status]<>0,JMP LBL[500] ;
! PR[2] has search result ;You could drop this SEARCH routine into any project without worrying about any data collisions. You could optionally improve the search to do a more high-speed search like we did in this post on refactoring skip conditions. The caller doesn’t care about the implementation – just the interface contract.
A couple of notes:
- We have to copy arguments 1 and 2 into local registers, because argument registers (
AR[]) are not allowed as indirect speeds in motion statements. - We use two local PRs: one for our starting position, so we can return to it, and another for an incremental search move. You could also use an Offset here instead.
Gotchas and Limitations
Local registers were only introduced in v9.40, so any programs you develop won’t be compatible with older versions.
You can’t pass local PRs to a KAREL program, e.g. CALL KAREL_PROG(PR[10001]). That’s really a KAREL limitation, since it doesn’t accept position-type arguments at all.
If you were thinking about getting around this limitation by using KAREL’s GET_POS_REG() builtin, unfortunately local registers are not supported here either.
You won’t see a comment on your local registers. You can include one in your LS files, but they won’t survive the LS to TP translation, and there’s no way to add comments within the TP editor.
There’s no runtime visibility. The DATA screens, Numeric Registers and Position Registers have no visibility into the values of local registers. This is not really a big deal, but we’ve essentially gotten used to having a debugger always present on our robot when we’re stepping through programs. Unfortunately these local values live on the call stack, and I’m not aware of any way to debug them outside of a poor man’s printf (e.g. R[1:debug]=R[10001], etc.).
That also means they consume some of your program’s stack allocation. How call stacks work is beyond the scope of this article, but just know that running a program requires a bit of memory on the controller for doing math, calling other programs, etc. Local variables take up a bit of that memory. Numeric registers don’t take much. That’s why you get 100 of them, but local PRs and SRs take up a lot of space so FANUC limits the max number of these quite a bit. If you run into INTP-302 Stack overflow, you may need to increase the stack size of your program (and/or parent program) via the SELECT menu’s program DETAIL panel.
Final Thought
Local registers could actually improve TP+ and Fexcel themselves. Right now both are coupled to the memory-map spreadsheet. Every named variable still maps to a specific global index you have to manage. If these tools emitted local registers for program-scoped variables, you could declare a variable and genuinely not care where it’s allocated. You wouldn’t need a spreadsheet entry or worry about collisions. The translator just grabs the next available local slot. That would finally decouple program-internal variables from the global memory map.
If you haven’t tried it yet, give Fexcel a try or browse the source code on GitHub. Pull requests welcome!