Programming for nested call of subroutines in numerical control processing - ST
  • About
  • Blog
  • Contact

Programming for nested call of subroutines in numerical control processing

CNC Subprogram Nesting and Calling: How to Structure Programs That Actually Work on the Shop Floor

Every CNC programmer eventually hits the same wall. The main program is 400 lines long, half of it is repeated code for identical operations, and every time you need to change something you have to hunt through the entire file. Subprograms fix this. But nesting them — calling a subprogram from inside another subprogram — is where things get interesting and where most programmers get stuck.

This is not a textbook explanation. It is about how subprogram nesting actually behaves on real controllers, what the depth limits are, how to pass variables between levels, and the specific mistakes that turn a clean nested structure into a debugging nightmare at two in the morning.


Why Subprograms Exist and Why Nesting Matters

A subprogram is a self-contained block of code stored separately from the main program. You call it with M98, it runs, and it returns to where it was called from with M99. Simple enough.

But real parts are not simple. You have a main program that calls a drilling subprogram. That drilling subprogram needs to call a peck drilling routine. The peck routine might need to call a chip-clear pause. That is three levels deep. And some controllers let you go even deeper.

Nesting lets you break a complex operation into layers. Each layer handles one specific task. The main program manages the overall sequence. The first subprogram handles positioning. The second subprogram handles the cutting cycle. The third handles retract logic. Change any layer without touching the others.

This modular approach is what separates a program that survives a design change from one that falls apart the moment the engineer adds two more holes.


How M98 and M99 Actually Work

The Basic Call and Return

M98 Pxxxx Lnn calls subprogram number xxxx and repeats it nn times. M99 returns control to the line after the M98 call. If you do not specify L, the default is one repetition.

The critical detail most people miss: M99 does not just return to the main program. It returns to whoever called it. If subprogram O1000 calls O2000 with M98 P2000, and O2000 ends with M99, control goes back to the line in O1000 right after the M98 call. Then O1000 continues until it hits its own M99, which sends control back to the main program.

This chain is what makes nesting possible. Each M99 unwinds one level. The controller keeps a return stack in memory, and each M98 pushes a new return address onto that stack. Each M99 pops it off.

The L Parameter and Repeat Counts

The L value on M98 tells the controller how many times to repeat the subprogram. L3 means run it three times and then return. But here is where nesting gets tricky: the repeat count applies to that specific call, not to the entire nested tree.

If your main program calls O1000 with L2, and O1000 internally calls O2000 with L3, O2000 runs three times for each of the two O1000 calls. That is six total executions of O2000. The repeat counts multiply, not add.

This is fine when you plan for it. It is a disaster when you forget. Always calculate the total number of executions in your head before you run a nested program with repeat counts on multiple levels.


Nesting Depth: How Deep Can You Actually Go

Controller Limits Vary Wildly

There is no universal nesting limit. Some controllers allow two levels. Some allow five. A few go as deep as ten or more. Check your controller manual before you design a nesting structure that assumes a specific depth.

On most Fanuc controllers, the practical limit is around four to five levels. On Siemens, you can nest deeper but the program becomes unreadable fast. On Haas, the limit depends on the control series. Older series cap out lower. Newer ones handle more.

The stack that stores return addresses has finite memory. Push too many M98 calls without enough M99 returns and you get a stack overflow alarm. The machine stops. The program halts. You lose your place.

A Practical Rule: Keep It Under Three Levels

Four levels deep works. Five works if you are careful. Beyond that, you are building a house of cards. Every programmer I have worked with who went beyond five levels eventually regretted it. The program becomes impossible to trace, variables collide, and debugging takes three times longer than writing the code in the first place.

Three levels is the sweet spot. Main program calls subprogram A. Subprogram A calls subprogram B. Subprogram B does the actual work and returns. Clean. Readable. Debuggable. If you need more complexity, break it into separate subprograms that the main program calls in sequence rather than nesting them deeper.


Passing Variables Between Nesting Levels

The Argument List on M98

Most controllers let you pass values into a subprogram through the M98 call. The syntax looks like M98 P1000 L1 X10.0 Y20.0 Z-5.0. The X, Y, Z values after the L parameter get stored in local variables inside the subprogram.

On Fanuc, these map to #24, #25, #26 (X, Y, Z). On Siemens, they map to R1, R2, R3 or similar depending on the controller series. The exact mapping varies, but the concept is the same: you feed numbers into the call, the subprogram reads them as variables, and it uses those values instead of hardcoded numbers.

This is how you make a subprogram reusable. Instead of writing ten different drilling subprograms for ten different depths, you write one drilling subprogram that reads the depth from the argument list. The main program passes the depth each time it calls the subprogram. One subprogram. Ten depths. Zero code duplication.

Local vs Global Variables: The Collision Problem

Here is where nesting gets dangerous. Local variables (#1-#33 on Fanuc) are shared across all nesting levels. If subprogram O1000 sets #10 = 50.0, and then calls O2000, and O2000 also uses #10 for something else, the second assignment overwrites the first. When O2000 returns, #10 no longer holds the value O1000 expected.

This does not always cause a crash. Sometimes it causes a silent error. The tool moves to the wrong position by 0.001mm and you never notice until the inspector rejects the part.

The fix is disciplined variable management. Use a range of variables for each nesting level and never overlap them. O1000 uses #1-#10. O2000 uses #20-#30. O3000 uses #40-#50. Write it down. Stick to it. When you run out of local variables, switch to common variables (#100-#199) which persist across all levels but are not affected by nested calls — though they are affected by everything else running on the machine, so use them with caution.

Returning Values with M99

M99 can also return values. On some controllers, M99 Pxxxx passes a value back to the calling program. This lets a subprogram compute something — like a measured offset or a calculated position — and hand it back to the level above.

Not all controllers support this. On Fanuc, you use local variables or common variables to pass data back. On Siemens, the R parameters can be returned directly. Check what your controller supports before you design a data-passing scheme that relies on a feature it does not have.


Structuring a Nested Program That Does Not Fall Apart

Start From the Bottom Up

Do not write the main program first. Write the deepest subprogram first — the one that does the actual cutting or drilling. Test it standalone. Make sure it works with variable inputs. Then write the subprogram that calls it. Test that. Then write the main program.

This bottom-up approach catches errors early. If the deepest subprogram has a bug, you find it before you have built five layers on top of it. Debugging a nested program from the top down is painful because you have to mentally unwind every level to find where the logic breaks.

Use Comments at Every Call Site

Every M98 call should have a comment explaining what the subprogram does, what variables it expects, and what it returns. Something like:

M98 P2000 L1 X50.0 Y0.0 Z-10.0 ; call peck drill, depth 10mm, center at X50 Y0

Without comments, a nested program becomes unreadable within a week. You will forget which subprogram does what. You will pass the wrong variables. You will call the wrong program. Comments are not optional — they are load-bearing walls in the structure.

Keep Each Subprogram Under 50 Lines

A subprogram that runs 200 lines is not a subprogram. It is a second main program. If a subprogram is getting long, break it into smaller pieces. The goal of nesting is modularity, not just code relocation.

A good subprogram does one thing. It receives inputs, performs one operation, and returns. If you find yourself writing IF statements inside a subprogram to handle different cases, that subprogram is doing too much. Split it.


Common Nesting Errors That Stop Production

M99 in the Wrong Place

M99 must be the last executable line in a subprogram. If you put code after M99, the controller never runs it. This sounds obvious, but in a long subprogram it is easy to add a line after the return and forget that nothing after M99 executes.

Worse, some controllers ignore M99 if it is not on its own line. A line like N50 M99 G00 X0 Y0 might not return at all depending on the controller. Always put M99 on a line by itself. Always.

Calling the Same Subprogram With Different Variables Mid-Nesting

If O1000 calls O2000 twice with different arguments, O2000 runs twice with different values. But if O2000 uses local variables that O1000 also uses, the second call might overwrite values that O1000 still needs.

The pattern looks like this: O1000 sets #10 = 100. Calls O2000. O2000 uses #10 and changes it to 200. Returns. O1000 still thinks #10 is 100 but it is now 200. The rest of O1000 runs with the wrong value.

Fix this by either using separate variable ranges or by having O2000 save and restore any variables it touches. The save-and-restore approach adds a few lines but it makes O2000 safe to call from anywhere.

Forgetting That Modal Codes Carry Through Nesting

G codes are modal. If the main program is in G43 (length compensation) when it calls a subprogram, that subprogram runs with G43 still active. If the subprogram changes to G44 and does not change back, the main program resumes with G44 — which might be wrong.

The same applies to feed rates, spindle speeds, and plane selections. A subprogram should set the modal state it needs at the start and restore the original state before it returns. Or at minimum, it should not rely on modal codes being in a specific state when it is called.

Write subprograms as if they can be called from anywhere with any modal state active. This defensive approach saves more time than it costs.


When Nesting Is Better Than a Flat Program

Repetitive Operations Across Multiple Features

If you have a part with four identical pockets and each pocket uses the same roughing and finishing sequence, do not write the roughing code four times. Write one roughing subprogram. Call it four times from the main program, passing the pocket position each time. Change the pocket locations in one place when the design updates.

Multi-Tool Setups With Shared Operations

When you run six tools and each tool does the same peck drilling pattern at different depths, write one peck drilling subprogram. The main program calls it six times with different Z values. Update the peck parameters in one subprogram instead of six separate code blocks.

Complex Profiles Broken Into Stages

A contoured surface might need a rough pass, a semi-finish pass, and a finish pass. Each pass is a subprogram. The main program calls them in sequence. The rough pass subprogram might call a stock removal subprogram. The finish pass might call a spring pass subprogram. Three levels deep, but each level has a clear purpose and the whole thing reads like a recipe.


Testing Nested Programs Before They Touch Production Stock

Run every subprogram standalone first. Feed it test values. Watch the tool path on the graphics. Confirm it does what you expect. Then test the first level of nesting. Then the second. Do not jump straight to running the full nested program on a real part.

Use single-block mode with dry run. Step through each M98 call and watch what happens. Verify that variables are being passed correctly. Check that M99 returns to the right place. If anything looks off, stop and fix it before you add more layers.

A nested program that works on the first try is rare. Plan for three or four debug cycles. Each cycle strips away one layer of confusion until the whole thing runs clean.

Email
Email: [email protected]
WhatsApp
WhatsApp QR Code
(0/8)