Added experimental support for M98 subroutines, internal subroutines are only supported for programs run from a local file system.

$700 controls whether they are scanned for internally in the main program (1) or always located externally (0).
If scanned for internally the program is run twice, initially in check mode to locate the subroutines before it is rewound and run in normal mode.
If stored externally the file P<macro number>.macro is run, <macro number> is the M98 P value.
This commit is contained in:
Terje Io
2026-02-02 22:25:31 +01:00
parent 08c778530c
commit c6aa574279
11 changed files with 225 additions and 58 deletions

View File

@@ -1,6 +1,6 @@
## grblHAL ##
Latest build date is 20260125, see the [changelog](changelog.md) for details.
Latest build date is 20260202, see the [changelog](changelog.md) for details.
> [!NOTE]
> A settings reset will be performed on an update of builds prior to 20241208. Backup and restore of settings is recommended.
@@ -89,4 +89,4 @@ G/M-codes not supported by [legacy Grbl](https://github.com/gnea/grbl/wiki) are
Some [plugins](https://github.com/grblHAL/plugins) implements additional M-codes.
---
20260125
20260202

View File

@@ -1,5 +1,32 @@
## grblHAL changelog
<a name="20260202">20260202
Core:
* Added _experimental_ support for M98 subroutines, internal subroutines are only supported for programs run from a local file system.
`$700` controls whether they are scanned for internally in the main program \(1\) or always located externally \(0\).
If scanned for internally the program is run twice, initially in check mode to locate the subroutines before it is rewound and run in normal mode.
If stored externally the file _P\<macro number\>.macro_ is run, _\<macro number\>_ is the `M98` `P` value.
Ref. discussion [789](https://github.com/grblHAL/core/discussions/789).
> [!NOTE]
> If a subroutine is not found in the main program it is assumed to be an external routine.
> Internal subroutines must be located _after_ the main program part which has to be terminated by `M2` or `M30`.
Drivers:
* ESP32, S3: fix for compilation error when USB serial comms is enabled.
* STM32F1xx: fix for copy/paste error in SVN board map.
Plugins:
* Spindle, offset: updated for core change.
* Keypad, I2C display interface: updated for core change.
---
<a name="20260128">20260131
Plugins:

184
gcode.c
View File

@@ -74,10 +74,11 @@ typedef union {
G8 :1, //!< [G43,G43.1,G49] Tool length offset
G10 :1, //!< [G98,G99] Return mode in canned cycles
G11 :1, //!< [G50,G51] Scaling
G12 :1, //!< [G54,G55,G56,G57,G58,G59,G59.1,G59.2,G59.3] Coordinate system selection
G13 :1, //!< [G61] Control mode
G14 :1, //!< [G96,G97] Spindle Speed Mode
G12 :1, //!< [G54,G55,G56,G57,G58,G59,G59.1,G59.2,G59.3] Coordinate system selection (14)
G13 :1, //!< [G61] Control mode (15)
G14 :1, //!< [G96,G97] Spindle Speed Mode (13)
G15 :1, //!< [G7,G8] Lathe Diameter Mode
// G16 :1, //!< [G66,G67] Modal macro call (12)
M4 :1, //!< [M0,M1,M2,M30] Stopping
M5 :1, //!< [M62,M63,M64,M65,M66,M67,M68] Aux I/O
@@ -116,11 +117,19 @@ typedef union {
};
} ijk_words_t;
typedef struct m98_macro {
macro_id_t id;
vfs_file_t *file;
size_t pos;
struct m98_macro *next;
} m98_macro_t;
// Declare gc extern struct
DCRAM parser_state_t gc_state;
#define RETURN(status) return gc_at_exit(status);
m98_macro_t *m98_macros = NULL;
static output_command_t *output_commands = NULL; // Linked list
static scale_factor_t scale_factor = {
.ijk[X_AXIS] = 1.0f,
@@ -604,6 +613,68 @@ bool gc_modal_state_restore (gc_modal_snapshot_t *snapshot)
#endif // NGC_PARAMETERS_ENABLE
static m98_macro_t *macro_find (macro_id_t id)
{
m98_macro_t *sub;
if((sub = m98_macros)) do {
if(sub->id == id)
break;
} while((sub = sub->next));
return sub;
}
size_t gc_macro_get_pos (macro_id_t id, vfs_file_t *file)
{
m98_macro_t *sub = macro_find(id);
return sub && sub->file == file ? sub->pos : 0;
}
/*
bool gc_macros_validate (void)
{
bool ok = true;
m98_macro_t *sub;
if((sub = m98_macros)) do {
ok = sub->pos != 0;
} while(ok && (sub = sub->next));
return ok;
}
*/
static void macros_clear (void)
{
m98_macro_t *next;
if(m98_macros) do {
next = m98_macros->next;
free(m98_macros);
} while((m98_macros = next));
}
static status_code_t macro_add (macro_id_t id, vfs_file_t *file)
{
m98_macro_t *sub = macro_find(id);
if(sub == NULL && (sub = calloc(sizeof(m98_macro_t), 1))) {
sub->id = id;
sub->file = file;
if(m98_macros) {
m98_macro_t *add = m98_macros;
while(add->next)
add = add->next;
add->next = sub;
} else
m98_macros = sub;
}
return sub ? Status_OK : Status_FlowControlOutOfMemory;
}
static status_code_t macro_call (macro_id_t macro, parameter_words_t args, uint8_t repeats)
{
#if NGC_PARAMETERS_ENABLE
@@ -622,11 +693,14 @@ static status_code_t macro_call (macro_id_t macro, parameter_words_t args, uint8
static status_code_t gc_at_exit (status_code_t status)
{
if(status != Status_OK) {
if(!(status == Status_OK || status == Status_Handled)) {
// Clear any pending output commands
gc_clear_output_commands(output_commands);
// Clear any registered M98 macros
macros_clear();
#if NGC_PARAMETERS_ENABLE
// Clear the G66 arguments stack
@@ -926,7 +1000,7 @@ char *gc_normalize_block (char *block, status_code_t *status, char **message)
break;
case ')':
if(comment && !gc_state.skip_blocks) {
if(comment && !(gc_state.skip_blocks || state_get() == STATE_CHECK_MODE)) {
*s1 = '\0';
if(!hal.driver_cap.no_gcode_message_handling) {
@@ -1076,8 +1150,11 @@ status_code_t gc_execute_block (char *block)
else if(!(gc_state.file_run = fs_changed)) {
protocol_buffer_synchronize(); // Empty planner buffer
grbl.report.feedback_message(Message_ProgramEnd);
if(grbl.on_program_completed)
grbl.on_program_completed(ProgramFlow_EndPercent, state_get() == STATE_CHECK_MODE);
if(grbl.on_program_completed) {
bool check_mode = state_get() == STATE_CHECK_MODE;
grbl.on_program_completed(ProgramFlow_EndPercent, check_mode);
gc_state.file_stream = !check_mode;
}
}
} else
gc_state.file_run = !gc_state.file_run;
@@ -1619,7 +1696,7 @@ status_code_t gc_execute_block (char *block)
case 66:
if(!ioports_can_do().wait_on_input || (ioports_unclaimed(Port_Digital, Port_Input) == 0 &&
ioports_unclaimed(Port_Analog, Port_Input) == 0))
ioports_unclaimed(Port_Analog, Port_Input) == 0))
RETURN(Status_GcodeUnsupportedCommand); // [Unsupported M command]
word_bit.modal_group.M5 = On;
port_command = (io_mcode_t)int_value;
@@ -1640,11 +1717,18 @@ status_code_t gc_execute_block (char *block)
break;
#endif
case 98:
if(mantissa != 0 || grbl.on_macro_execute == NULL)
RETURN(Status_GcodeUnsupportedCommand);
word_bit.modal_group.M4 = On;
gc_block.non_modal_command = NonModal_MacroCall2;
break;
case 99:
if(mantissa != 0 || !(!!hal.stream.file || !!grbl.on_macro_return))
RETURN(Status_GcodeUnsupportedCommand);
word_bit.modal_group.M4 = On;
gc_block.modal.program_flow = ProgramFlow_Return;
if(grbl.on_macro_return == NULL)
RETURN(Status_GcodeUnsupportedCommand);
break;
default:
@@ -1754,10 +1838,20 @@ status_code_t gc_execute_block (char *block)
break;
case 'O':
if (mantissa > 0)
if(mantissa > 0)
RETURN(Status_GcodeCommandValueNotInteger);
word_bit.parameter.o = On;
gc_block.values.o = int_value;
{
m98_macro_t *macro;
if(hal.stream.state.m98_macro_prescan && (macro = macro_find((macro_id_t)int_value))) {
macro->file = hal.stream.file;
macro->pos = vfs_tell(macro->file);
RETURN(Status_OK);
} else {
word_bit.parameter.o = On;
gc_block.values.o = int_value;
}
}
break;
case 'P': // NOTE: For certain commands, P value must be an integer, but none of these commands are supported.
@@ -1939,9 +2033,11 @@ status_code_t gc_execute_block (char *block)
// [0. Non-specific/common error-checks and miscellaneous setup]:
if(word_bit.modal_group.G0 &&
if((word_bit.modal_group.G0 &&
(gc_block.non_modal_command == Modal_MacroCall ||
gc_block.non_modal_command == NonModal_MacroCall)) {
gc_block.non_modal_command == NonModal_MacroCall)) ||
(word_bit.modal_group.M4 &&
gc_block.non_modal_command == NonModal_MacroCall2)) {
if(!gc_block.words.p)
RETURN(Status_GcodeValueWordMissing); // [P word missing]
@@ -1955,27 +2051,30 @@ status_code_t gc_execute_block (char *block)
#if NGC_PARAMETERS_ENABLE
// Remove axis and ijk words flags since values are to be passed unmodified.
axis_words.mask = ijk_words.mask = 0;
if(gc_block.non_modal_command != NonModal_MacroCall2) {
if(gc_block.non_modal_command == NonModal_MacroCall) {
// TODO: add context for local storage?
if(!ngc_call_push(&gc_state + ngc_call_level()))
RETURN(Status_FlowControlStackOverflow); // [Call level too deep]
gc_block.g65_words.mask = macro_arguments_push(&gc_block.values, gc_block.words).mask;
gc_block.words.mask &= ~gc_block.g65_words.mask; // Remove G65 arguments
} else { // G66
g66_arguments_t *args;
// if(gc_state.g66_args && gc_state.g66_args->call_level == ngc_call_level())
// RETURN(cannot have nested g66 calls on the same call level?);
if((args = malloc(sizeof(g66_arguments_t)))) {
args->prev = gc_state.g66_args;
gc_state.g66_args = args;
gc_state.g66_args->call_level = ngc_call_level();
gc_state.g66_args->words.mask = gc_block.words.mask;
memcpy(&gc_state.g66_args->values, &gc_block.values, sizeof(gc_values_t));
// Remove axis and ijk words flags since values are to be passed unmodified.
axis_words.mask = ijk_words.mask = 0;
if(gc_block.non_modal_command == NonModal_MacroCall) {
// TODO: add context for local storage?
if(!ngc_call_push(&gc_state + ngc_call_level()))
RETURN(Status_FlowControlStackOverflow); // [Call level too deep]
gc_block.g65_words.mask = macro_arguments_push(&gc_block.values, gc_block.words).mask;
gc_block.words.mask &= ~gc_block.g65_words.mask; // Remove G65 arguments
} else { // G66
g66_arguments_t *args;
// if(gc_state.g66_args && gc_state.g66_args->call_level == ngc_call_level())
// RETURN(cannot have nested g66 calls on the same call level?);
if((args = malloc(sizeof(g66_arguments_t)))) {
args->prev = gc_state.g66_args;
gc_state.g66_args = args;
gc_state.g66_args->call_level = ngc_call_level();
gc_state.g66_args->words.mask = gc_block.words.mask;
memcpy(&gc_state.g66_args->values, &gc_block.values, sizeof(gc_values_t));
}
RETURN(args ? Status_OK : Status_FlowControlStackOverflow);
}
RETURN(args ? Status_OK : Status_FlowControlStackOverflow);
}
#endif // NGC_PARAMETERS_ENABLE
}
@@ -4092,6 +4191,14 @@ status_code_t gc_execute_block (char *block)
break;
#endif
case NonModal_MacroCall2:
if(check_mode) {
RETURN(macro_add((macro_id_t)gc_block.values.p, hal.stream.file));
} else {
RETURN(macro_call((macro_id_t)gc_block.values.p, (parameter_words_t){ .$ = On}, gc_block.values.l));
}
break;
case NonModal_SetCoordinateOffset: // G92
add_offset((coord_data_t *)gc_block.values.xyz);
gc_state.g92_offset_applied = true; // TODO: check for all zero?
@@ -4315,6 +4422,8 @@ status_code_t gc_execute_block (char *block)
if(gc_state.modal.program_flow == ProgramFlow_Return) {
if(grbl.on_macro_return)
grbl.on_macro_return();
else if(grbl.on_program_completed)
grbl.on_program_completed(gc_state.modal.program_flow, check_mode);
} else if(gc_state.modal.program_flow == ProgramFlow_Paused || gc_block.modal.program_flow == ProgramFlow_OptionalStop || gc_block.modal.program_flow == ProgramFlow_CompletedM60 || sys.flags.single_block) {
if(!check_mode) {
if(gc_block.modal.program_flow == ProgramFlow_CompletedM60 && hal.pallet_shuttle)
@@ -4401,14 +4510,15 @@ status_code_t gc_execute_block (char *block)
if(grbl.on_program_completed)
grbl.on_program_completed(gc_state.modal.program_flow, check_mode);
// Clear any pending output commands
gc_clear_output_commands(output_commands);
// Clear any pending output commands etc...
gc_at_exit(hal.stream.state.m98_macro_prescan ? Status_Handled : Status_UserException);
#if NGC_PARAMETERS_ENABLE
ngc_modal_state_invalidate();
#endif
grbl.report.feedback_message(Message_ProgramEnd);
if(!check_mode || !settings.flags.m98_prescan_enable)
grbl.report.feedback_message(Message_ProgramEnd);
}
gc_state.modal.program_flow = ProgramFlow_Running; // Reset program flow.
}

View File

@@ -3,7 +3,7 @@
Part of grblHAL
Copyright (c) 2017-2025 Terje Io
Copyright (c) 2017-2026 Terje Io
Copyright (c) 2011-2016 Sungeun K. Jeon for Gnea Research LLC
Copyright (c) 2009-2011 Simen Svale Skogsrud
@@ -28,6 +28,7 @@
#include "coolant_control.h"
#include "spindle_control.h"
#include "errors.h"
#include "vfs.h"
#define MAX_OFFSET_ENTRIES 4 // must be a power of 2
@@ -60,6 +61,7 @@ typedef enum {
Modal_MacroCall = 66, //!< 66 - G66
Modal_MacroEnd = 67, //!< 67 - G67
NonModal_SetCoordinateOffset = 92, //!< 92 - G92
NonModal_MacroCall2 = 98, //!< 98 - M98
NonModal_ResetCoordinateOffset = 102, //!< 102 - G92.1
NonModal_ClearCoordinateOffset = 112, //!< 112 - G92.2
#if ENABLE_ACCELERATION_PROFILES
@@ -741,6 +743,7 @@ void gc_coolant (coolant_state_t state);
void gc_set_tool_offset (tool_offset_mode_t mode, uint_fast8_t idx, int32_t offset);
plane_t *gc_get_plane_data (plane_t *plane, plane_select_t select);
axes_signals_t gc_claim_axis_words (parser_block_t *gc_block, axes_signals_t validate);
size_t gc_macro_get_pos (macro_id_t id, vfs_file_t *file);
#if NGC_PARAMETERS_ENABLE
parameter_words_t gc_get_g65_arguments (void);

2
grbl.h
View File

@@ -42,7 +42,7 @@
#else
#define GRBL_VERSION "1.1f"
#endif
#define GRBL_BUILD 20260126
#define GRBL_BUILD 20260202
#define GRBL_URL "https://github.com/grblHAL"

View File

@@ -766,8 +766,12 @@ status_code_t ngc_flowctrl (uint32_t o_label, char *line, uint_fast8_t *pos, boo
*skip = false;
if(settings.flags.ngc_debug_out)
report_message(line, Message_Plain);
} else
} else {
*skip = skip_sub || (stack_idx >= 0 && stack[stack_idx].skip);
// char buf[200];
// sprintf(buf, "%d %d %d %s", *skip, stack_idx, stack[stack_idx].operation, line);
// report_message(buf, Message_Plain);
}
return status;
}

View File

@@ -916,6 +916,13 @@ static status_code_t set_enable_legacy_rt_commands (setting_id_t id, uint_fast16
return Status_OK;
}
static status_code_t set_suboptions (setting_id_t id, uint_fast16_t int_value)
{
settings.flags.m98_prescan_enable = int_value != 0;
return Status_OK;
}
#if !LATHE_UVW_OPTION
static status_code_t set_mode (setting_id_t id, uint_fast16_t int_value)
@@ -1693,6 +1700,10 @@ FLASHMEM static uint32_t get_int (setting_id_t id)
((!settings.flags.keep_feed_override_on_reset) << 3);
break;
case Setting_SubroutineOptions:
value = settings.flags.m98_prescan_enable;
break;
default:
break;
}
@@ -2005,6 +2016,7 @@ FLASHMEM static bool is_setting_available (const setting_detail_t *setting, uint
break;
case Setting_FSOptions:
case Setting_SubroutineOptions:
available = hal.driver_cap.sd_card || hal.driver_cap.littlefs;
break;
@@ -2204,6 +2216,7 @@ PROGMEM static const setting_detail_t setting_detail[] = {
{ Setting_MotorFaultsInvert, Group_Stepper, "Invert motor fault inputs", NULL, Format_AxisMask, NULL, NULL, NULL, Setting_IsExtended, &settings.motor_fault_invert, NULL, is_setting_available },
{ Setting_ResetActions, Group_General, "Reset actions", NULL, Format_Bitfield, "Clear homed status if position was lost,Clear offsets (except G92),Clear rapids override,Clear feed override", NULL, NULL, Setting_IsExtendedFn, set_reset_actions, get_int, NULL },
{ Setting_StepperEnableDelay, Group_Stepper, "Stepper enable delay", "ms", Format_Int16, "##0", NULL, "500", Setting_IsExtended, &settings.stepper_enable_delay, NULL, NULL },
{ Setting_SubroutineOptions, Group_General, "Subroutine options", NULL, Format_Bitfield, "Prescan for internal M98 subroutines", NULL, NULL, Setting_IsExtendedFn, set_suboptions, get_int, is_setting_available }
};
PROGMEM static const setting_descr_t setting_descr[] = {
@@ -2410,7 +2423,8 @@ PROGMEM static const setting_descr_t setting_descr[] = {
{ Setting_HomePinsInvertMask, "Inverts the axis home input signals." },
{ Setting_CoolantOnDelay, "Delay to allow coolant to start. 0 or 0.5 - 20s." },
{ Setting_ResetActions, "Controls actions taken on a soft reset." },
{ Setting_StepperEnableDelay, "Delay from stepper enable to first step output. The driver typically adds ~2ms to this." }
{ Setting_StepperEnableDelay, "Delay from stepper enable to first step output. The driver typically adds ~2ms to this." },
// { Setting_SubroutineOptions, "Enable prescan for internal M98 subroutines." }
/*
{ Setting_MotorWarningsEnable, "Motor warning enable" },
{ Setting_MotorWarningsInvert, "Invert motor warning inputs" },

View File

@@ -435,7 +435,6 @@ typedef enum {
Setting_Stepper4 = 654,
Setting_Stepper5 = 655,
Setting_Stepper6 = 656,
Setting_Stepper7 = 657,
Setting_Stepper8 = 658,
Setting_Stepper9 = 659,
Setting_Stepper10 = 660,
@@ -464,6 +463,8 @@ typedef enum {
Setting_THC_FeedFactor = 682,
// 683 - 689 - reserved for Sienci
Setting_SubroutineOptions = 700,
Setting_SpindlePWMOptions1 = 709,
Setting_SpindleInvertMask1 = 716,
@@ -605,7 +606,8 @@ typedef union {
tool_persistent :1,
keep_rapids_override_on_reset :1,
keep_feed_override_on_reset :1,
unassigned :9;
m98_prescan_enable :1,
unassigned :8;
};
} settingflags_t;

View File

@@ -3,7 +3,7 @@
Part of grblHAL
Copyright (c) 2019-2025 Terje Io
Copyright (c) 2019-2026 Terje Io
grblHAL is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -280,13 +280,14 @@ typedef union {
typedef union {
uint8_t value;
struct {
uint8_t webui_connected :1,
is_usb :1,
linestate_event :1, //!< Set when driver supports on_linestate_changed event.
passthru :1, //!< Set when stream is in passthru mode.
utf8 :1, //!< Set when stream is in UTF8 mode.
eof :1, //!< Set when a file stream reaches end-of-file.
unused :2;
uint8_t webui_connected :1,
is_usb :1,
linestate_event :1, //!< Set when driver supports on_linestate_changed event.
passthru :1, //!< Set when stream is in passthru mode.
utf8 :1, //!< Set when stream is in UTF8 mode.
eof :1, //!< Set when a file stream reaches end-of-file.
m98_macro_prescan :1, //!< Set when prescanning gcode for M98 macro definitions.
unused :1;
};
} io_stream_state_t;

6
vfs.c
View File

@@ -5,7 +5,7 @@
Part of grblHAL
Copyright (c) 2022-2025 Terje Io
Copyright (c) 2022-2026 Terje Io
grblHAL is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -267,7 +267,7 @@ vfs_file_t *vfs_open (const char *filename, const char *mode)
if(mount && (file = mount->vfs->fopen(get_filename(mount, filename), mode))) {
file->fs = mount->vfs;
file->update = !mount->mode.hidden && !!strchr(mode, 'w');
file->status.update = !mount->mode.hidden && !!strchr(mode, 'w');
}
return file;
@@ -279,7 +279,7 @@ void vfs_close (vfs_file_t *file)
((vfs_t *)(file->fs))->fclose(file);
if(file->update && vfs.on_fs_changed)
if(file->status.update && vfs.on_fs_changed)
vfs.on_fs_changed((vfs_t *)file->fs);
}

10
vfs.h
View File

@@ -5,7 +5,7 @@
Part of grblHAL
Copyright (c) 2022-2025 Terje Io
Copyright (c) 2022-2026 Terje Io
grblHAL is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -78,10 +78,16 @@ typedef struct {
#endif
} vfs_stat_t;
typedef struct {
uint8_t update :1,
is_temporary :1,
unused :6;
} vfs_file_status_t;
typedef struct {
const void *fs;
size_t size;
bool update;
vfs_file_status_t status;
uint8_t handle __attribute__ ((aligned (4))); // first byte of file handle structure
} vfs_file_t;