In my previous post, I presented my [»] CNC-STEP High-Z S400/T CNC milling machine and gave a few advice on how to set it up properly. I however left for later an important aspect CNC machining which is writing CNC programs! Hopefully, that post will answer all your questions and make you an expert in G-code writing :)
I will start with a bit of history on G-code/CNC programming before explaining how to write basic programs, then move to more advanced features and conclude with the limitations of G-code and how to solve them. Be sure to stay to the end of the post as I will also introduce my latest [∞] web application to assist you in writing your G-code programs!
A quick note before we start: we will illustrate some G-code by small programs. I recommend that you use a thick MDF board as a test plate, such as illustrated in Figure 1. The material is cheap and soft enough to limit risks as you are in the learning phase. I also recommend to simulate your CNC programs before you run them to rule out path errors. Finally, be sure to understand how to configure your machine properly (check [»] here if you have the same machine as mine).

Before we dig into the details of CNC programming, it is important to understand where it originates from.
G-code is a programming language used to control CNC (Computer Numerical Control) machines that was developed to formalize the way instructions were given to control machine tools, allowing a clear and repeatable definition of operations such as cutting, drilling, and tool changes. It has been standardized under ISO 6983, originally introduced in the 1970s, and remains the most common method as of today for defining tool motion and machine operations in manufacturing contexts.
The language was initially developed in the context of early numerical control systems, where instructions were fed to machines using punched tapes. ISO 6983 provided a standardized way to describe machine paths and actions independently from machine architecture. This standardization made it possible to exchange machining programs between compatible systems and reduced the need for hardwired machine logic. Although various evolutions have occurred since its inception, the basic structure of G-code remains largely unchanged. Such standard G-code will be referred here to as standard, or, ISO 6983 compliant.
G-code programs are plain text files composed of sequential instructions where each line represents a command or set of parameters to be interpreted by the CNC controller. These commands are typically organized around movement instructions (denoted by the letter G), machine functions (denoted by M), and various axis coordinates, feedrates, and tool specifications. The instructions are interpreted in the order in which they appear and do not include programming structures such as loops or conditional logic in their basic form although most vendors include their own set of loop and conditional instructions, albeit they are non-standard and therefore non-portable across machine types.
In the remainder of this article, we will examine the most frequently used instructions in G-code programming for milling operations. These include rapid positioning (G00), linear interpolation (G01), circular interpolation (G02 and G03), spindle control (M3, M5), coolant activation, and tool changes as well as some more advanced features. The examples are intended to clarify how each command is used and how it translates into machine motion or auxiliary operations.
As briefly mentioned already, it is important to note that there exist variations in how G-code is implemented across different machine controllers. Many manufacturers have introduced proprietary extensions or modified behaviors, leading to a lack of full compatibility between different systems. As such, G-code is not universally portable without modification. For the purposes of this article, we will refer to the syntax and behavior expected by the KineticNC controller used by my [»] milling machine, which follows a relatively standard interpretation of ISO 6983 and does not introduce major deviations from the core specification. I will also skip non-standard operations of the KineticNC controller such as to provide a generic introduction to CNC programming.
Now that we understand where G-code originates from, lets dig into some basic notions of CNC controller logic.
CNC machines rely on coordinate systems to define the position of the tool in space relative to the machine and to the workpiece. These systems allow users to define origin points, manage multiple setups, and control how toolpaths are interpreted by the controller.
The machine coordinate system, also known as the absolute coordinate system, is defined by the manufacturer and typically corresponds to the physical limits of travel. It is accessed using the G53 command, which tells the controller to interpret all position values in relation to the machines fixed zero point. Because G53 operates outside of user-defined work offsets, it is commonly used for tool changes, homing movements, or retracting the tool safely at the end of a cycle. Note that some machines propose to eventually flip axis coordinates, so it is important to consult your machine documentation beforehand to understand how the G53 axes are defined.
In contrast, G54 through G59 represent relative coordinate systems, allowing the user to define up to six separate coordinate origins. These offsets are stored in the controller and are applied automatically when any of the corresponding G-codes are active. Typically, G54 is used as the primary coordinate system, and additional systems (G55G59) can be defined for multi-part setups, fixture changes, or different sides of the same workpiece. These coordinate systems are referenced relative to the machine coordinates and allow for consistent part positioning across multiple sessions. Setting the offset is usually done through a zeroing operation, such as demonstrated in my [»] previous post. Note that some manufacturers offer non-standard offsets like G54.1, G54.2 etc. to increase the maximum number of work offsets.
Beyond selecting which coordinate system to use, the controller must also be told how to interpret each movement command: either as an absolute position from the active origin, or as an incremental step from the current position. This is controlled by G90 and G91, respectively. When G90 is active, all coordinates are treated as absolute values relative to the current work origin. For example, a command such as G01 X10 Y10 moves the tool to the position (10, 10) in the active coordinate system. With G91 active, the same command would move the tool 10 millimeters in X and 10 millimeters in Y from its current location, regardless of its prior position.
Switching between G90 and G91 is modal and affects all subsequent motion instructions until changed. While G90 is more common for programmed toolpaths generated from CAM software, G91 can be useful in manual programming or for relative jogs in custom macros. Note that any confusion between G90 and G91 modes can be catastrophic as G01 Z-1 can either be understood as move down by 1 mm (G91 mode), or as move to coordinate -1 mm (G90 mode). Since it is often convenient to assign the Z=0 coordinate to the part top height, you could crash into your part instead of approaching it carefully by 1 mm!
A final option worth mentioning is G92, which allows a temporary shift in the current coordinate system by redefining the current tool position to specific values. This can be useful for temporary adjustments, but its behavior varies significantly between controllers. On the KineticNC controller, G92 is not fully supported and may not behave consistently across sessions or with certain machine states. For this reason, it is recommended to avoid G92 in production code and instead use properly defined work offsets (G54G59) for consistent and predictable results. For obvious reasons, no program should machine parts directly in G53 mode as well.
A functional G-code program must include a minimum set of instructions to initialize the controller, define units, manage spindle direction, and signal the end of execution. Although more complex operations require motion commands and offsets, even a minimal program must establish basic operating conditions before any movement takes place.
Units are defined at the beginning of the program using either G21 (millimeters) or G20 (inches). This choice determines how the controller interprets all linear dimensions in subsequent commands. It is strongly recommended to declare units explicitly at the top of every program, regardless of whether the default matches the intended values. Omitting this can lead to catastrophic errors if the controller interprets millimeters as inches or vice versa. Remember we want to write portable code!
Spindle control is handled using the M3 or M4 command, which start the spindle in clockwise or counterclockwise rotation, respectively. These commands are usually combined with an S command to specify the spindle speed in RPM. Most milling operations are performed with the spindle rotating clockwise (M3). If no S value is given, the spindle may start at a previously used speed or fail to start depending on controller behavior. On [»] my machine, the spindle speed is actually set on the spindle itself using a rotary knob such that all values provided by software to the controller are ignored.
To finish a program, either M2 or M30 can be used. Both mark the end of the program, but there is a subtle difference: M2 stops execution and leaves most modal states unchanged, while M30 performs a reset and rewinds to the beginning of the file. In most practical scenarios, the distinction is minimal, but M30 is often used when the program is intended to be repeated, and M2 is sufficient for single-pass jobs.
Below is an example of a minimal G-code program that sets metric units, starts the spindle, waits briefly, and ends the program using M2. Note that every program must start with a % on a blank line to be recognized as valid G-code files.
%
G21 ; Set units to millimeters
G90 ; Absolute positioning (optional but good practice)
S12000 ; Set spindle speed to 12000 RPM
M3 ; Start spindle clockwise
G4 H5 ; Dwell for 5 seconds (P in seconds on most controllers but KineticNC requires H)
M30 ; End of program
This example does not include any movement or tool paths but is syntactically valid and executable. It can be used for basic spindle testing, warm-up routines, or macros. The G4 dwell command pauses execution for a set duration, useful for allowing the spindle to reach full speed before any cutting operations begin.
Now that we can start the spindle and understand how the controller axes are configured (G53 vs. G54-G59), its time to machine some parts!
In G-code, there are only two fundamental types of motion: linear and circular. All tool movement, whether it follows a straight line or a curve, is described using variations of these two operations. Each motion type is specified with a dedicated command, and the controller interprets the geometry based on the active coordinate system and motion mode.
Linear motion is handled using two instructions: G00 for rapid positioning, and G01 for controlled feed movement. The difference lies in how the machine moves between two points. With G00, the controller moves the tool to the target coordinates as quickly as the machine allows, typically in a straight line per axis, not necessarily along a straight diagonal. This command is used for non-cutting moves where speed is more important than path precision. In contrast, G01 moves the tool at a defined feedrate and is used during actual cutting operations where controlled motion is required.
Example of a basic program using both types of linear motion:
%
G21 ; Set units to millimeters
S12000 ; Set spindle speed to 12000 RPM
M3 ; Start spindle clockwise
G4 H5 ; Dwell for 5 seconds (P in seconds on most controllers but KineticNC requires H)
G90 ; Absolute positioning
G21 ; Units in millimetres
G00 Z5 ; Retract tool safely above part
G00 X0 Y0 ; Rapid move to origin
G01 Z-1 F100 ; Controlled plunge into material at 100 mm/min
G01 X50 Y0 F300 ; Linear cut to X50 Y0 at 300 mm/min
G01 X50 Y50 ; Continue cut to X50 Y50 (same feedrate continues)
G00 Z5 ; Retract tool after cut
M30 ; End of program
The cutting speed during G01 moves is defined by the feedrate, set using the F command. Feedrate values are interpreted in units per minute (typically millimeters per minute when using G21). Once set, the feedrate remains active until it is changed or the program ends. Note that feedrate is modal: it applies to all subsequent G01, G02, and G03 motions unless explicitly overridden.
Circular or arc motion is defined using G02 for clockwise arcs and G03 for counterclockwise arcs. These commands describe a circular interpolation in the XY plane (by default), and the arc is defined using the endpoint coordinates along with offset values I, J, and K that specify the arc center relative to the start point. The offsets are given in the current coordinate system: I is the X-offset to the center, J is the Y-offset, and K is used only for arcs in the XZ or YZ planes.
This method provides unambiguous control over the arc geometry and is preferred over the alternative R parameter (radius), which can result in unpredictable results when the arc exceeds 180 degrees. Using IJK offsets avoids this ambiguity.
Example of an arc motion using IJK notation:
%
G21 ; Set units to millimeters
S12000 ; Set spindle speed to 12000 RPM
M3 ; Start spindle clockwise
G4 H5 ; Dwell for 5 seconds (P in seconds on most controllers but KineticNC requires H)
G90 ; Absolute positioning
G00 Z5 ; Retract tool
G00 X0 Y0 ; Move to arc starting point
G01 Z-1 F100 ; Plunge into material
G02 X0 Y50 I25 J25 F300 ; Clockwise arc ending at X0 Y50, with arc center offset 25 mm right and 25 mm up
G01 Z5 ; Retract
M30 ; End of program
In this example, the tool moves from (0, 0) to (0, 50) following a clockwise circular path. The arc center is at (25, 25), which lies 25 mm in X and 25 mm in Y from the start point, creating a quarter-circle motion. The I and J parameters always define the vector from the arcs start point to the center, regardless of the end point. This makes arc commands precise and consistent, especially in manually written programs or when debugging post-processed code.
The G17, G18, and G19 commands are used to select the active plane for circular interpolation: G17 corresponds to the XY plane, G18 to the XZ plane, and G19 to the YZ plane. These settings determine how the controller interprets arc motions (G02 and G03) and are required on controllers that support multi-plane machining. However, in KineticNC, these plane selection commands are not implemented. Instead, the controller automatically infers the active plane based on which of the I, J, or K parameters are nonzero in the arc command. This implicit selection simplifies programming but requires careful use of IJK values to ensure unambiguous geometry.
As final note, I must say that arcs are tricky because only circular arcs are supported but the parameters provided may break the circle conditions. Since we provide the coordinates for the arc center relative to the current position, the radius of the circle/arc is fixed. But remember we can specify any (x, y) target position, such that it is easy to end up with a different radius for the target point! In such cases, most controllers will stop and report an error. It is therefore important to double-check what center and target coordinates you give to the controller. A typical error when writing arcs manually is to invert the IJK coordinates offsets, resulting in a radius error.
Very few parts can be made using only one tool so you can expect to see tool changes occur in a CNC program.
Tool changes in G-code are performed using the M6 command. This instruction tells the controller to switch to a different tool, typically specified by a T markup. For example, the sequence M6 T2 requests the controller to change to tool number 2. On most budget-friendly CNC systems, including those using KineticNC, the tool change operation is semi-automatic: the controller pauses execution and waits for the operator to insert the correct tool and confirm the change before resuming machining operations. More advanced machines can use pneumatic systems to automate the tool change completely. Check my [»] previous post to see how to implement tool change on your machine.
Once a new tool is inserted, it is essential to update the controller with the correct tool length to ensure accurate Z-axis positioning. This is handled using the tool length compensation command G43, which applies a length offset stored in the controllers tool table. The syntax is typically G43 Hn (where n corresponds to the tool number), or just G43 on the KineticNC controller. To disable the tool length compensation, G49 is used. Without proper length compensation, the tool tip will not align with the programmed Z-levels, resulting in machining errors and potential collisions. Again, you can check my [»] previous post for more information on the practical aspects of tool change implementations.
In KineticNC software, the tool length can be updated using the G79 macro, which is automatically called during a tool change operation (M6). This macro activates a probing sequence that measures the length of the new tool and stores the value in the appropriate register. As long as the tool is properly configured, this process is reliable and ensures that Z-axis offsets remain consistent across tool changes. This also eliminates the need to manually input tool lengths into the controller.
A quick note about feedrates which were briefly mentioned in a previous section. Feedrates must be adapted to the type of tool in use as a tool change not only affects geometry and length, but also dictates what feedrates are mechanically safe and efficient. Using inappropriate feedrates can result in tool breakage, excessive wear, or poor surface finish.
Several common tool types are frequently used in small milling setups:
- Endmill: general-purpose tool used for profiling, pocketing, and facing. Suitable for moderate to high feedrates.
- T-slot cutter: side-cutting tool designed for undercuts or keyways. Requires slow feedrates due to fragile geometry and lateral loading.
- Chamfer mill: used for deburring and angled cuts; typically run at medium feedrates, depending on the material and desired edge quality.
- Thread mill: used for internal or external thread generation; operates with slow, precise feedrates due to small diameters and interrupted cutting.
Each tool has its own optimal cutting parameters based on material, diameter, and machine rigidity. When changing tools, it's good practice to reassess both feedrate (F) and spindle speed (S) to match the requirements of the new cutter. It is difficult to come up with precise numbers, but you should always consult the tool datasheet for recommendations and adjust the value to account for things like tool wear, material hardness (use slower feedrates on harder materials like steel), machine power and frame rigidity.
It is also important to understand how larger federate also affects machining tolerances. On direction changes, the machine needs to decelerate the tool before it reaches target to avoid overshoot. If deceleration is poorly implemented or disabled (as when using the G00 command sometimes), the tool will go beyond the target point hence affecting the machining quality.
Lets continue our discussion on tools and machining quality by introducing the concept of tool radius compensation.
In CNC milling, the physical tool diameter is never perfectly identical to its nominal value. Even freshly manufactured tools may deviate by a few hundredths of a millimeter, and wear during use gradually reduces their effective diameter. Cutter radius compensation was introduced to address this issue, allowing the toolpath to be calculated with respect to the intended geometry, while the machine offsets the motion based on the actual tool radius.
While originally conceived as a means to compensate for wear, radius compensation also improves geometric accuracy during machining: by measuring the real diameter of the tool with a tool presetting device or probe, the machinist can enter the effective radius into the controller. This allows the same G-code program to remain valid across tool changes or size variations, with the path dynamically shifted left or right to maintain dimensional correctness.
The G-code standard defines three radius compensation modes: G40: Cancel cutter compensation. G41: Enable cutter compensation to the left of the programmed path (when moving forward). G42: Enable cutter compensation to the right of the programmed path (when moving forward). The "left" or "right" designation is always with respect to the direction of tool motion not the workpiece, and not the absolute X-Y plane. For a clockwise contour, G42 places the tool outside the part, while G41 places it inside.
Cutter compensation typically requires the controller to know the tool radius, which is defined in a tool table or set via manual input. When using G41 or G42, the motion must begin with a lead-in move that allows the controller to transition from the compensated to the uncompensated path. Similarly, compensation must be cancelled with G40 before retracting from the workpiece. Check my [»] former post to see how to introduce exact tool diameter in the KineticNC controller.
Below is an example showing two circular contours: an internal circle (Ø5 mm) and an external circle (Ø10 mm). A Ø2 mm endmill is used, and compensation is applied accordingly.
%
(--- Set up units and plane ---)
G21 ; Use metric units
G17 ; XY plane (optional in KineticNC)
G90 ; Absolute positioning
G40 ; Cancel any previous compensation
(--- Spindle and tool setup ---)
M6 T1 ; Select tool 1
G43 ; Apply tool length compensation
M3 S12000 ; Spindle on clockwise at 12000 RPM
(--- Move to safe height and position ---)
G00 Z5.0 ; Rapid to clearance height
G00 X0 Y0 ; Move to center of internal circle
(--- Machine internal circle Ø5 mm with cutter on inside ---)
G41 ; Enable radius compensation to the left
G01 X-2.5 ; Lead-in move to the edge of the inner circle
G01 Z-1.0 F200 ; Plunge to cutting depth
G03 X-2.5 Y0 I2.5 J0 ; Full counter-clockwise circle (internal)
G00 Z5.0 ; retract
G01 X0 Y0 ; Lead-out
G40 ; Cancel compensation
(--- Machine external circle Ø10 mm with cutter on outside ---)
G00 X0 Y0 ; Move to new center point
G42 ; Enable radius compensation to the right
G01 X-5 ; Lead-in move to the edge of the outer circle
G01 Z-1.0 F200 ; Plunge to depth
G02 X-5 Y0 I5 J0 ; Full clockwise circle (external)
G00 Z5.0 ; retract
G01 X0 Y0 ; Lead-out
G40 ; Cancel compensation
(--- End of program ---)
M5 ; Spindle stop
M30 ; End of program
The use of G3 (counter-clockwise) for the internal contour and G2 (clockwise) for the external one ensures the toolpath moves in the correct direction so that compensation is applied on the correct side. Always remember: the motion direction combined with G41/G42 defines where the compensation is applied. Misunderstanding this can lead to undersized or oversized parts!
Last but not least, I have experimented some weird behavior on the KineticNC controller when mixing G00 and G01 during transition from G41/G42 to G40. It seems that KineticNC ignores G00 commands before it enters G40 mode and throws an error. To alleviate this issue, I am using only G01 when in radius compensation mode and use G00 only after G40 as been called. I have no specific explanation on why the controller seems to be operating like that but it illustrates very well the concept that each controller has its own interpretation of the ISO standard, making G-code not so portable even when you restrict yourself to the ISO-compliant operations.
We saw that G-code only supports linear and arc motions. This falls a bit short when you need to trace smooth curves like letter arts! The only solution is to approximate them with series of tiny linear movements.
Path approximation becomes very handy in such circumstances because it allows overriding the default behavior of the CNC controller which is to stop at each vertex of the trace by decelerating the machine to avoid overshoots. This repeated deceleration and acceleration results in a noticeable slowdown of the machining process. While geometrically accurate, this exact stop behavior (G61) introduces inefficiency and may lead to inconsistent surface finish due to changes in feedrate. Also, it generates a series of broken lines and not the smooth curve we were initially looking for. In exact stop mode, the only way to achieve smoothness is to use a lot of linear subdivisions, making the program extremely slow and verbose.
To address this, most CNC controllers support constant velocity modes, which permit the tool to slightly deviate from the exact path in order to maintain a smoother trajectory. In the G-code standard, this is controlled by the G64 command, which activates path blending, also called look-ahead motion or corner rounding, allowing the controller to avoid complete stops at corners. Instead, it dynamically calculates an adjusted path that merges adjacent segments with a continuous, flowing motion.
The extent of this deviation is controlled by a system-defined parameter, often referred to as path tolerance or maximum deviation. On the KineticNC controller, this tolerance can be adjusted in the Machine Settings pane. It defines the maximum distance the tool is allowed to deviate from the programmed polyline path. A smaller value will result in tighter adherence to the intended geometry but may still slow down the tool at sharp corners. A larger value results in smoother motion but introduces greater geometric deviation.
Figure 2 illustrates the difference between these modes. The original path is shown as a sequence of sharp line segments forming a polyline. In exact stop mode (G61), the tool path follows each vertex precisely, requiring deceleration at every corner. The figure also shows two approximated paths using G64 with different tolerance values: one with a tight deviation radius that rounds the corners slightly, and one with a more aggressive smoothing that departs more visibly from the polyline. The trade-off between speed and accuracy becomes evident from the comparison.

To regain precise control at specific segments (e.g. when a tight internal corner or pocket feature requires exact geometry) the controller can temporarily switch back to exact stop mode using G61. After the critical operation is completed, constant velocity mode (G64) can be resumed. This selective switching allows the programmer to balance accuracy and efficiency throughout the toolpath.
Some other operations that you will need to perform frequently are hole drilling and taping.
Hole drilling operations are typically handled using canned cycles, which simplify the generation of repetitive vertical motions required for drilling holes at multiple locations. These cycles reduce code size and ensure consistent motion patterns across the operation. The most frequently used drilling cycles are G81, G82, G83, and G73, each tailored to specific drilling scenarios. The general format for these cycles includes parameters such as the target depth (Z), retract height (R), feedrate (F), and dwell time if applicable. As mentioned in my [»] previous post, on the KineticNC controller, each of these commands are actually macros that you can edit.
The G81 cycle is the simplest form of drilling. It performs a rapid approach to the retract height (R), then a feedrate plunge to the target depth (Z), and finally a rapid retract to the initial height. It is used for general-purpose drilling in materials where chip evacuation is not a concern. Parameters required are Z, R, and F.
The G82 cycle adds a dwell time at the bottom of the hole. After reaching the programmed depth, the tool pauses for a fixed duration before retracting. This is useful for producing cleaner hole bottoms, especially in soft materials or when drilling through thin walls. In addition to Z, R, and F, the P parameter is used to set the dwell time.
The G83 cycle is the standard deep-hole peck drilling routine. It breaks the drilling action into smaller pecks to help evacuate chips and reduce heat buildup. After each peck, the tool retracts slightly to clear chips before continuing deeper. Required parameters include the final depth (Z), retract height (R), peck depth (Q), and feedrate (F). Some controllers also allow setting a dwell time with P, but this is optional.
The G73 cycle is a high-speed version of peck drilling, designed for shallow holes or materials where full retracts are not necessary. Instead of retracting completely between pecks, the tool makes quick retraction movements that reduce cycle time. Like G83, it uses Z, R, Q, and F parameters, but does not provide the same chip clearing efficiency.
Several additional canned cycles exist in the G-code standard to support specialized hole machining operations beyond simple drilling. These include tapping, reaming, boring, and countersinking. While they are standardized in ISO 6983, it is important to note that these commands are not implemented in the KineticNC controller, and as such, will not be discussed in detail in this article.
The G84 cycle is intended for tapping operations. It synchronizes spindle rotation and Z-axis feed to cut internal threads using a rigid tap. The tool reverses direction at the bottom of the hole to exit along the same path. This requires hardware support for spindle encoder feedback, which is not available in KineticNC.
The G85 cycle is a feed-in/feed-out routine commonly used for reaming or rubbing a hole to improve surface finish. The tool moves down and retracts at the same feedrate, without stopping or dwelling.
G86 is used for boring operations. The tool feeds into the hole, stops the spindle at the bottom, and then retracts rapidly. Because the spindle halts mid-operation, this cycle assumes the use of non-rotating boring bars or tools that should not be spinning during retraction.
G87 performs a back countersink using a special retract sequence that allows the tool to perform a cutting action on the underside of the hole. This operation is usually supported only on high-end machines with specific toolholders.
G88 is a variation of G82, designed for spot facing or pit countersinking. It includes a dwell at the bottom followed by a manual or programmatic retraction, often used with operator intervention.
Finally, G89 is similar to G85 but includes a dwell at the bottom of the hole, typically used for finish reaming or trimming where timing of the pause is critical for surface quality.
Because the KineticNC firmware does not interpret these cycles, equivalent toolpaths must be constructed manually using basic motion commands if these operations are needed.
G-code subprograms are a way to reuse blocks of motion or machining instructions by defining a repeatable sequence under a unique program number. The main program can then call this subprogram multiple times using the M98 command. Each time the subprogram is executed, it performs the same sequence of operations from its entry point until it returns via M99. This approach is particularly efficient when machining identical features at different locations such as holes, pockets, or repeating contours.
Using subprograms reduces the overall size of the G-code file and improves maintainability. Rather than duplicating the same set of tool movements at multiple locations, the logic is defined once and referenced many times. This allows for cleaner, more modular programming, and ensures consistency: if a change is needed, it only needs to be made in one place. Most G-code controllers, including KineticNC, support this mechanism, provided the program numbers used are unique and stored properly.
To call a subprogram, the M98 command is used, with the following syntax:
Here, Pxxxx refers to the subprogram number (e.g., P1000 for O1000), and L defines how many times it should loop. The subprogram must end with M99, which returns control to the calling program. If the L parameter is omitted, the subprogram runs once. Subprograms can be stored in the same file or as separate .nc or .tap files, depending on controller configuration.
Below is an example that defines a subprogram to mill a circular profile, as we did earlier. The subprogram is written using relative coordinates (G91), so it can be reused at any X offset. The main program sets the origin, selects the tool, and then calls the subprogram multiple times along the X-axis at intervals of 25 mm.
%
(--- Main Program ---)
G21 ; Metric units
G17 ; XY plane
G90 ; Absolute positioning
G40 ; Cancel compensation
M6 T1 ; Select tool
G43 H1 ; Apply length compensation
M3 S12000 ; Spindle on clockwise
G00 Z5.0 ; Safe height
G00 X0 Y0 ; First part position
M98 P1000 ; Call subprogram at X = 0
G00 X25 Y0 ; Shift to next position
M98 P1000 ; Call subprogram at X = 25
G00 X50 Y0
M98 P1000 ; Call subprogram at X = 50
M5 ; Spindle stop
G00 Z5.0
M30 ; End of main program
(--- Subprogram O1000 ---)
O1000
G91 ; Relative positioning
G01 Z-1.0 F200 ; Plunge
G03 X0 Y0 I0 J2.5 ; Full CCW circle, internal radius 2.5 mm
G01 Z5.0 ; Retract
G90 ; Restore absolute mode
M99 ; Return to main program
In this example, the subprogram O1000 executes a circular motion starting from the current X-Y position. Because it operates in G91 mode (relative), it can be used repeatedly at different base points without modification. Each invocation mills a 5 mm diameter circle, ideal for identical features such as holes or boss outlines. This approach not only reduces code duplication but also allows for easy adjustment of the pattern: modifying the loop in the main program or adjusting offsets will instantly reflect across all repeated features.
Although the present documentation focuses on the ISO 6983 standard, it is worth noting that the KineticNC controller, as well as other controllers, supports a number of non-standard extensions that are not part of the G-code specification. These additions introduce elements of logic and interactivity into CNC programming, offering constructs that resemble high-level programming languages. Such extensions are typically used for advanced automation, conditional control, or operator interaction.
Among the supported commands by the KineticNC controller are IF, THEN, UNTIL, and REPEAT, which allow conditional branching and loops. The controller also supports CALL, which enables calling named routines or parameterized procedures beyond simple subprograms. For debugging or runtime feedback, PRINT is available to output text messages to the console. Additionally, a set of user-prompting commands (ASKBOOL, ASKINT, and ASKFLT) can be used to request boolean, integer, or floating-point inputs directly from the machine operator during execution.
These constructs can be powerful in contexts such as semi-automated production setups, manual intervention checkpoints, or interactive job macros. However, they are not standardized and are not guaranteed to be portable across different controllers or machines.
For the sake of clarity and reproducibility, this article will limit itself to ISO-standard instructions. All code examples provided avoid the use of non-compliant commands to ensure maximum compatibility and portability with other G-code interpreters that conform to ISO 6983.
Now that we have a good understanding of G-code and that you are proficient in G-code writing, lets take a critical look at the G-code standard.
As we have seen, a central problem in current G-code implementation is the lack of adherence to the standard. Despite the ISO 6983 instructions set being fairly complete, every manufacturer has its own interpretation of the standard. It rarely poses any issue when doing basic programs, but you quickly fall into inter-operability loopholes as you go towards edge cases as I mentioned with the KineticNC doing fancy things with G00/G01 during G41/G42 cycles. CAM softwares often go through a post-processing step to alleviate these issues by translating code to machine-specific tailored programs, but it is a pity that such problems exists in the first place. Im not even mentioning here all the non-standard operations, forcing you to go through a new learning curve every time you change machines in your workshop (hopefully, not that frequently!).
Another problem, which you will discover very quickly as you start writing CNC programs, is the verbosity of the programming language. G-code is low-level only and despite it can be read as text files, they remain at extremely low level. There are no higher-level concepts such as cut a disk in G-code, only floods of instructions such as goto and arc to. It makes CNC programs very hard to draft by hand without making a handful of errors, and makes reviewing G-code even worse. You can compare this to assembler programming language something very close to the hardware but that very few people actually use.
Also, as mentioned previously, there is a lack of representation of smooth curves such as Bezier and b-splines. Curves must be split into line segments and smoothed out using constant velocity mode of the controller. This increases the verbosity of the programs and makes hand-writing of smooth curve almost impossible! You could view this as a consequence of the low-level nature of G-code, but remember that SVG image file format, despite being a markup language, not only has instructions for lines and arcs, but also for quadratic and cubic splines as well, with the interpolation being handled by the computer.
Still continuing the comparison with SVG file format, we could also complain about the lack of native support for generalized transformation such as scaling, mirroring, rotating toolpaths etc. without having to modify every coordinate directly. This may seem overkilled at first sight but it is a very critical feature once you start machining. Imagine you set up your work and measure that instead of being exactly parallel to the X-axis, it is off by 0.7°. If you do not fix this, you will machine features off by 1 mm only 10 cm away from the origin point! You only have two solutions here: either modify your CNC program (which is almost impossible is you did it by hand), or to adjust your part until it is perfectly horizontal which it will never exactly be. The best solution would have been to tell the controller my coordinate system is 0.7° off around this point and it would correct every coordinate in the program using a simple matrix transform Unfortunately, no such support exists in the standard!
Similarly, there is no mechanism foreseen in the standard to adjust speed and feedrates to the material or frame rigidity, although in the KineticNC controller you can adjust federate and spindle speed using some gain from 50% to 150%. Again, it would be very convenient to have a master gain to freely adjust all feedrates depending on your tool wear, material being machined or the frame rigidity of your machine hereby increasing the portability and exchange of CNC programs.
Last but not least, G-code is not a safe language to program in! As we saw when introducing arc commands, it is very easy to provide buggous instructions to the controller making it stop and throw an exception. Similarly to arcs, attempting to machine in G41/G42 mode inner features that are smaller than the tool radius will throw exceptions as well and stop your program with a dreadful radius compensation error at line XXX. Even worse than that, there is absolutely no mechanism to protect yourself from crashing tools or the spindle into your part, irreversibly damaging your expensive tools not even mentioning being a direct threat to nearby operators! This is not something we can blame the standard for, as it falls clearly outside of the scope of low-level programming, but it is something that should make you very reluctant to write your own program without having some sort of proof checking on top of them.
With the limitations of G-code clearly defined, its now time to address higher-level working environments.
Computer Aided Manufacturing (CAM) softwares, are there to fill-in the gaps of G-code. CAM softwares work with a higher level of abstraction and uses features instead of line/arc commands. Features are then turned into passes which themselves consist of segments of lines and arcs.
The most common features in CAM softwares are:
- Facing: Flattening the top surface of a workpiece to create a uniform Z-height;
- Pocketing: Removing material from an enclosed area inside the part, typically using a zig-zag or spiral toolpath;
- Contour (Profiling): Following the outer boundary of a part or feature to cut its final shape;
- Drilling: Creating circular holes by plunging the tool vertically at fixed XY coordinates;
- Boring: Enlarging an existing hole to precise dimensions using controlled circular interpolation;
- Chamfering: Cutting a 45° (or other angle) bevel along an edge for deburring or aesthetic purposes;
- Thread Milling: Cutting internal or external threads by following a helical toolpath with a small cutter;
- Engraving: Etching shallow text or symbols onto a surface, usually with a V-bit or small endmill;
- Slotting: Cutting a straight channel through the material, typically the same width as the tool;
- Tapping: Cutting internal threads using a tap tool, often requiring spindle synchronization.
Most CAM softwares work directly from a 3D model and do not let you enter operations directly. The most known, affordable, CAM softwares on the market are Fusion 360, SolidWorks Standard CAM, FreeCAD, BobCAD, EstlCAM and FreeCAD. This list is not-exhaustive as the CAM market is quite developed.
I would personally recommend giving a try to Fusion 360 although I have no experience with FreeCAD nor BobCAD. I would recommend staying away from EstlCAM which is way too basic for the price (considering Fusion 360 has a free version that is much superior) and I had an extremely bad experience with NC Shop Floor Programmer. NC Shop Floor Programmer replaces SolidWorks Standard CAM tool on all 3DExperience accounts (the new license model of Dassault). Since I was already trained on the SolidWorks Standard CAM tool I opted in for the NC Shop Floor Programmer add-on which was presented to me as being similar to the old tool (SolidWorks Standard CAM) that had been deprecated. This is highly incorrect and I can tell you that (1) it is horribly expensive (2,916/year!), (2) it is absolutely not similar to SolidWorks Standard CAM, (3) there is very little help/tutorial available and you have to pay for training, (4) I was never able to find a post-processor that would be compliant with my KineticNC controller despite it sticks relatively closely to the standard. After having spent 40 hours+ and obtaining no result with NC Shop Floor Programmer, I cancelled my subscription but never received any credit note or any other form of refund. To be complete, I have to add that I had a 90% discount on NC Shop Floor in the first place because I was in Dassault startup program (thats still 300 lost for a program that never work). By the way, 2,916/yr was just the fee for the standard version. If you want to import STEP files into NC Shop Floor Programmer you have to pay extra money :) Fusion 360 is therefore currently the best option for small CNC jobs.
Finally, and for the sake of completeness, I also need to mention that there exist multiple open-source semi CAM project that do not start from 3D models but instead let you enter shapes manually or start from an SVG image. Since I dont have experience with these programs, I will not comment on them.
That being said, whatever CAM software you choose to use, it is extremely important that you understand G-code programming in the first place. I started directly with CAM softwares but didnt understand any of the errors the controller was reporting (typically radius compensation error that made me lost days trying to figure out what was going on). By understanding how G-code works in the first place and how your machine controller work, especially regarding tools management, you will get a much better understanding on how to use your CAM software as well.
In the context of the #DevOptical website, I implemented a simple semi CAM tool to assist in G-code programming. The application is available in [∞] one click and does not require any installation process. The tool was designed to act as a medium-layer bridge between higher-level CAM applications and low-level G-code programming.
Just like other CAM softwares, the application will offer multiple operation types (facing, pocketing, path, hole drilling, thread milling, chamfering etc.) and will generate all the G-code machining passes based on your inputs such as pass size, tool speed and others. The application also integrates a crash detection engine, currently in BETA version, which is aimed as a safety layer to detect potential crashes of the tool. I will dedicate a complete article on how crash detection is implemented because it makes use of very sophisticated math tools :)
The main screen of the application is shown in Figure 3 with a toolbar on top (build program, export program), a global settings pane, a tool pane and an operations pane.

To add machining operations, first create a tool of the desired type (e.g. an endmill) in the tool pane, then create the desired operation (e.g. a disk pocket) in the operations pane. Once all the settings have been entered, you can click on the build button in the toolbar to generate your G-code.
The easiest way to test the application is to actually use it through one of my higher-level applications, such as the [∞] aperture creator. Once your aperture has been configured properly, just click on the edit button as shown in Figure 4. The NC application will open in another window with all the tool paths and settings already entered. You can then fine-tune the program from there and rebuild the program.

The application will generate a preview image of your tool passes. Actual tool position is shown using thick, continuous, lines with an arrow indicating the direction of the motion. Desired feature dimensions are shown as dashed-lines. Desired feature dimensions are the path traces that makes uses of the G41/G42 radius compensation commands of the G-code for which you can choose either inner or outer path trace behavior. Note that pocketing operations always apply a radius compensated path as finishing pass as illustrated in Figure 5.

Check on Figure 5 how the radii adapt between the rough passes and the finish pass to take into account the tool diameter. Also, despite the finish pass seems to be far away from the rough passes, the actual amount of material removed by the finishing operation is very small (0.2 mm here). Remember that the dashed curve represent the actual cut shape whereas the plain lines represent the tool center to which you need to add the tool diameter to get a correct representation of the amount of material cut!
I will continue working on this app in the future as I am not 100% happy with the current interface which still feels a bit verbose to me. Its not excluded that I will implement a basic/expert mode switch to fill-in most of the fields such that you can focus on making your parts faster :) Also, the path/pocketing operations are currently limited to disks, rectangles, slots and arc-slots which are the basic shapes of most CAD programs. I had to remove the polygon type temporarily because I had some difficulty implementing it in the crash detection engine but it will come back soon enough!
This has been a pretty long post but you should now be an expert in G-code programming! I hope this series of posts on CNC machining ([»] rapid manufacturing of lenses mounts and [»] presentation of my CNC-STEPS machine) convinced you on how fun CNC can be! I tried to put in the last two posts all the info I wished I had learned when I started with CNC machining back in 2022.
I will conclude this series with two more posts on CNC machining, before my workshop starts freezing during the winter season :) After that, I will resume my work in experimental optics with, I hope, 3 more posts before the end of the year.
Do not hesitate to share your thoughts on the [∞] community board to let me know if you enjoyed this post!
I would like to give a big thanks to Sebastian, Alex, Stephen, Lilith, James, Jesse, Jon, Kausban, Karel, Michael, Sivaraman, Samy, Zach, Shaun, Onur, Themulticaster, Tayyab, Sunanda, Benjamin, Marcel, Dennis, M, Natan and RottenSpinach who have supported this post through [∞] Patreon. I also take the occasion to invite you to donate through Patreon, even as little as $1. I cannot stress it more, you can really help me to post more content and make more experiments!
You may also like:
[»] RSA Cryptography Implementation Tips
[»] #DevOptical Part 8: Raytracing 101