From c6aa57427913aeaecfd5e7e5c7531db8ea053a52 Mon Sep 17 00:00:00 2001 From: Terje Io Date: Mon, 2 Feb 2026 22:25:31 +0100 Subject: [PATCH] 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 is run, is the M98 P value. --- README.md | 4 +- changelog.md | 27 ++++++++ gcode.c | 184 +++++++++++++++++++++++++++++++++++++++---------- gcode.h | 5 +- grbl.h | 2 +- ngc_flowctrl.c | 6 +- settings.c | 16 ++++- settings.h | 6 +- stream.h | 17 ++--- vfs.c | 6 +- vfs.h | 10 ++- 11 files changed, 225 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 2b79732..27eb15d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/changelog.md b/changelog.md index 3d6361c..5c0a4bb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,32 @@ ## grblHAL changelog +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_ is run, _\_ 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. + +--- + 20260131 Plugins: diff --git a/gcode.c b/gcode.c index e4b25e0..eb5628c 100644 --- a/gcode.c +++ b/gcode.c @@ -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. } diff --git a/gcode.h b/gcode.h index ce5fd27..c4a9cd1 100644 --- a/gcode.h +++ b/gcode.h @@ -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); diff --git a/grbl.h b/grbl.h index 1d68c7a..78d80b9 100644 --- a/grbl.h +++ b/grbl.h @@ -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" diff --git a/ngc_flowctrl.c b/ngc_flowctrl.c index 67b11b0..18df987 100644 --- a/ngc_flowctrl.c +++ b/ngc_flowctrl.c @@ -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; } diff --git a/settings.c b/settings.c index 12e6874..2672c53 100644 --- a/settings.c +++ b/settings.c @@ -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" }, diff --git a/settings.h b/settings.h index 6f5e6e2..aa91ca7 100644 --- a/settings.h +++ b/settings.h @@ -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; diff --git a/stream.h b/stream.h index 1844e60..4277927 100644 --- a/stream.h +++ b/stream.h @@ -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; diff --git a/vfs.c b/vfs.c index f3f4349..48f39bd 100644 --- a/vfs.c +++ b/vfs.c @@ -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); } diff --git a/vfs.h b/vfs.h index c415a5c..ec8bb23 100644 --- a/vfs.h +++ b/vfs.h @@ -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;