Fixed incorrect behaviour when a tool change using the built-in workflow is aborted, the selected tool was set as the current tool.

Added support for G66.1 macro call.
Changed behaviour of G50 and G51 (and the G48 and G49 shortcuts) controlling feed rate and spindle RPM overrides.
Added support for G10L0, reload file based tool table.
Fixed buggy handling of G43 - apply tool offset from tool table.
Refactored the encoder HAL/API to make it more flexible and as a first step to support rigid tapping.
See the changelog for further details.
This commit is contained in:
Terje Io
2026-02-26 10:30:46 +01:00
parent 54c58ee177
commit aa3a80f47f
26 changed files with 930 additions and 394 deletions

View File

@@ -42,6 +42,7 @@ target_sources(grbl INTERFACE
${CMAKE_CURRENT_LIST_DIR}/utf8.c
${CMAKE_CURRENT_LIST_DIR}/vfs.c
${CMAKE_CURRENT_LIST_DIR}/canbus.c
${CMAKE_CURRENT_LIST_DIR}/encoders.c
${CMAKE_CURRENT_LIST_DIR}/pid.c
${CMAKE_CURRENT_LIST_DIR}/fs_device.c
${CMAKE_CURRENT_LIST_DIR}/kinematics/corexy.c

View File

@@ -1,6 +1,6 @@
## grblHAL ##
Latest build date is 20260218, see the [changelog](changelog.md) for details.
Latest build date is 20260225, 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.

View File

@@ -1,10 +1,55 @@
## grblHAL changelog
<a name="20260225">Build 20260225
Core:
* Fix incorrect behaviour when a tool change using the built-in workflow is aborted, the selected tool was set as the current tool.
* Clarification: `G66` macros calls are not run on their first invocation and will only be run on subsequent blocks containing axis words for `G0` or `G1` motion.
Subsequent blocks may contain either the `G0` or `G1` command word, if not their modal state will be used. The macro will be called after the motion has been sent to the planner.
* Added support for `G66.1` macro call. Unlike `G66` this is run on the first invocation and for each subsequent block containing parameter word\(s\) used in the first invocation.
Parameter values changed in subsequent blocks are "sticky", that is they will keep their value for subsequent blocks until changed again.
> [!NOTE]
> `G66` and `G66.1` behaviour may change in later builds since I have not found any definite specification, it seems that there are different implementations between controllers.
> grbHAL aims to adopt the Fanuc behaviour, but this has not been verified.
* Changed behaviour of `G50` and `G51` \(and the `G48` and `G49` shortcuts\) controlling feed rate and spindle RPM overrides.
When used with `P0` to turn off the feed rate and/or the spindle RPM will be reverted to their programmed values.
The override values can still be changed and any changed value will be output in the real time report.
When turned back on the current override value\(s\) will be reapplied.
* Added support for `G10L0`, reload file based tool table. Can only be used when no tool (tool 0) is in the spindle.
* Fixed buggy handling of `G43` - apply tool offset from tool table.
* Refactored the encoder HAL/API to make it more flexible and as a first step to support rigid tapping.
For programmers: encoders are now registered with the core allowing them to be added from both drivers and plugins.
Plugins can now claim encoders, and code has been added to the core to make it easy for drivers and plugins to bind a slow encoder to interrupt capable auxiliary inputs.
> [!NOTE]
> The new HAL/API will be extended as needed later.
Drivers:
* iMXRT1062, STM32F4xx, STM32F7xx: updated for the new encoder HAL/API.
* RP2040: fixed typos affecting limit switch inversion for A+ axes.
Plugins:
* Encoder: updated to use the new encoder HAL/API.
* Misc, Tool table: added support for `G10L0`, reload tool table.
---
<a name="20260218">Build 20260218
Core:
* Changed `$pinstates` command output, now reports pin number instead of pin id.
* Changed `$pinstate` command output, now reports pin number instead of pin id.
* For developers: added API call that returns Modbus stream handlers.

View File

@@ -143,6 +143,7 @@ typedef tool_table_entry_t *(*get_tool_ptr)(tool_id_t tool_id);
typedef tool_table_entry_t *(*get_tool_by_idx_ptr)(uint32_t idx);
typedef bool (*set_tool_data_ptr)(tool_data_t *tool_data);
typedef bool (*clear_tool_data_ptr)(void);
typedef status_code_t (*reload_tool_data_ptr)(void);
typedef struct {
uint32_t n_tools;
@@ -150,6 +151,7 @@ typedef struct {
get_tool_by_idx_ptr get_tool_by_idx;
set_tool_data_ptr set_tool;
clear_tool_data_ptr clear;
reload_tool_data_ptr reload;
} tool_table_t;
/*****************

View File

@@ -5,7 +5,7 @@
Part of grblHAL
Copyright (c) 2020-2025 Terje Io
Copyright (c) 2020-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
@@ -450,12 +450,18 @@
#endif
#endif
#ifndef ENCODER_ENABLE
#define ENCODER_ENABLE 0
#endif
#ifndef QEI_ENABLE
#if ENCODER_ENABLE
#define QEI_ENABLE 1
#else
#define QEI_ENABLE 0
#endif
#ifndef QEI_SELECT_ENABLE
#define QEI_SELECT_ENABLE 0
#endif
#ifndef ODOMETER_ENABLE
#define ODOMETER_ENABLE 0
#endif

View File

@@ -5,7 +5,7 @@
Part of grblHAL
Copyright (c) 2024-2025 Terje Io
Copyright (c) 2024-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
@@ -43,6 +43,10 @@
#warning "Selected spindle 1 is not supported!"
#endif
#if ENCODER_ENABLE > 0 && !(defined(QEI_A_PIN) && defined(QEI_B_PIN))
#warning "ENCODER_ENABLE requires encoder input pins A and B to be defined!"
#endif
#endif
#if MPG_ENABLE == 1 && !defined(MPG_MODE_PIN)

79
encoders.c Normal file
View File

@@ -0,0 +1,79 @@
/*
encoders.c - quadrature encoder interface (API)
Part of grblHAL
Copyright (c) 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
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
grblHAL is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with grblHAL. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdint.h>
#include <stdbool.h>
#include "hal.h"
#include "encoders.h"
static struct encoders {
encoder_t *encoder;
struct encoders *next;
} *encoders = NULL;
static uint8_t n_encoders = 0;
typedef bool (*encoder_enumerate_callback_ptr)(encoder_t *properties, void *data);
static void dummy_event_handler (encoder_t *encoder, encoder_event_t *events)
{
UNUSED(encoder);
events->value = 0;
}
void encoder_register (encoder_t *encoder)
{
struct encoders *add, *last;
if((add = malloc(sizeof(struct encoders)))) {
add->next = NULL;
add->encoder = encoder;
if((last = encoders)) {
while(last->next)
last = last->next;
last->next = add;
} else
encoders = add;
n_encoders++;
}
}
bool encoders_enumerate (encoder_enumerate_callback_ptr callback, void *data)
{
bool ok = false;
struct encoders *encoder = encoders;
if(encoder) do {
ok = callback(encoder->encoder, data);
} while(!ok && (encoder = encoder->next));
return ok;
}
uint8_t encoders_get_count (void)
{
return n_encoders;
}

318
encoders.h Normal file
View File

@@ -0,0 +1,318 @@
/*
encoders.c - quadrature encoders interface (API)
Part of grblHAL
Copyright (c) 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
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
grblHAL is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with grblHAL. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _ENCODERS_H_
#define _ENCODERS_H_
#include "plugins.h"
// Quadrature encoder interface
typedef union {
uint8_t value;
uint8_t events;
struct {
uint8_t position_changed :1,
direction_changed :1,
click :1,
dbl_click :1,
long_click :1,
index_pulse :1,
unused :2;
};
} encoder_event_t;
typedef union {
uint8_t value;
uint8_t mask;
struct {
uint8_t bidirectional :1,
select :1,
index :1,
spindle_rpm :1,
spindle_pos :1,
unused :3;
};
} encoder_caps_t;
typedef struct {
uint32_t vel_timeout;
uint32_t dbl_click_window; //!< ms.
} encoder_cfg_t;
typedef struct {
int32_t position;
uint32_t velocity;
} encoder_data_t;
struct encoder;
typedef struct encoder encoder_t;
/*! \brief Pointer to callback function to receive encoder events.
\param encoder pointer to a \a encoder_t struct.
\param events pointer to a \a encoder_event_t struct.
\param context pointer to the context passed to the encoders claim function.
*/
typedef void (*encoder_on_event_ptr)(encoder_t *encoder, encoder_event_t *events, void *context);
/*! \brief Pointer to function for resetting encoder data.
\param encoder pointer to a \a encoder_t struct.
*/
typedef void (*encoder_reset_ptr)(encoder_t *encoder);
/*! \brief Pointer to function for claiming an encoder.
\param event_handler pointer to to the event handler callback.
\param context pointer to the context to be passed to event handler.
\returns \a true when claim was successful, \a false to otherwise.
*/
typedef bool (*encoder_claim_ptr)(encoder_on_event_ptr event_handler, void *context);
/*! \brief Pointer to function for getting encoder data.
\param encoder pointer to a \a encoder_t struct.
\returns pointer to a \a encoder_data_t struct containing the data.
*/
typedef encoder_data_t *(*encoder_get_data_ptr)(encoder_t *encoder);
/*! \brief Pointer to the callbak function to be called by encoders_enumerate().
\param encoder pointer to a \a encoder_t struct.
\returns \a true to stop the enumeration and return true from encoders_enumerate(), \a false otherwise.
*/
typedef bool (*encoder_enumerate_callback_ptr)(encoder_t *encoder, void *data);
/*! \brief Pointer to function for configuring an encoder.
\param encoder pointer to a \a encoder_t struct.
\param encoder pointer to a \a encoder_cfg_t struct.
\returns \a true when claim was successful, \a false to otherwise.
*/
typedef bool (*encoder_configure_ptr)(encoder_t *encoder, encoder_cfg_t *settings);
void encoder_register (encoder_t *encoder);
bool encoders_enumerate (encoder_enumerate_callback_ptr callback, void *data);
uint8_t encoders_get_count (void);
struct encoder {
encoder_caps_t caps;
encoder_claim_ptr claim;
encoder_reset_ptr reset;
encoder_get_data_ptr get_data;
encoder_configure_ptr configure;
};
#endif // _ENCODERS_H_
// Quadrature Encoder Interface - static code for drivers/plugins
#if QEI_ENABLE && defined(QEI_A_PIN) && defined(QEI_B_PIN)
typedef enum {
QEI_DirUnknown = 0,
QEI_DirCW,
QEI_DirCCW
} qei_dir_t;
typedef union {
uint_fast8_t pins;
struct {
uint_fast8_t a :1,
b :1;
};
} qei_state_t;
typedef struct {
encoder_t encoder;
encoder_data_t data;
encoder_event_t event;
void *context;
int32_t vel_count;
uint_fast16_t state;
qei_dir_t dir;
uint8_t port_a, port_b, port_select;
volatile uint32_t dbl_click_timeout;
volatile uint32_t vel_timeout;
uint32_t vel_timestamp;
encoder_on_event_ptr on_event;
encoder_cfg_t settings;
} qei_t;
static qei_t qei = {
.port_a = IOPORT_UNASSIGNED,
.port_b = IOPORT_UNASSIGNED,
.port_select = IOPORT_UNASSIGNED,
.settings.dbl_click_window = 500,
.encoder.caps.bidirectional = On
};
static void qei_post_event (void *data)
{
qei.on_event(&qei.encoder, &qei.event, qei.context);
}
static void qei_dblclk_event (void *data)
{
qei.event.dbl_click = On;
qei.on_event(&qei.encoder, &qei.event, qei.context);
}
static void qei_reset (encoder_t *encoder)
{
qei.vel_timeout = 0;
qei.dir = QEI_DirUnknown;
qei.data.position = qei.vel_count = 0;
qei.vel_timestamp = hal.get_elapsed_ticks();
qei.vel_timeout = qei.settings.vel_timeout;
}
static bool qei_configure (encoder_t *encoder, encoder_cfg_t *settings)
{
if(qei.vel_timeout != settings->vel_timeout)
qei.vel_timestamp = hal.get_elapsed_ticks();
memcpy(&qei.settings, settings, sizeof(encoder_cfg_t));
return true;
}
static encoder_data_t *qei_get_data (encoder_t *encoder)
{
return &qei.data;
}
static void qei_poll (void *data)
{
if(qei.vel_timeout && !(--qei.vel_timeout)) {
uint32_t time = hal.get_elapsed_ticks();
qei.data.velocity = abs(qei.data.position - qei.vel_count) * 1000 / (time - qei.vel_timestamp);
qei.vel_timestamp = time;
qei.vel_timeout = qei.settings.vel_timeout;
if((qei.event.position_changed = !qei.dbl_click_timeout || qei.data.velocity == 0))
qei.on_event(&qei.encoder, &qei.event, qei.context);
qei.vel_count = qei.data.position;
}
if(qei.dbl_click_timeout && !(--qei.dbl_click_timeout)) {
qei.event.click = On;
task_delete(qei_dblclk_event, NULL);
qei.on_event(&qei.encoder, &qei.event, qei.context);
}
}
static void qei_ab_irq (uint8_t port, bool high)
{
const uint8_t encoder_valid_state[] = {0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0};
static qei_state_t state = {0};
if(port == qei.port_a)
state.a = high;
else
state.b = high;
uint_fast8_t idx = (((qei.state << 2) & 0x0F) | state.pins);
if(encoder_valid_state[idx] ) {
// int32_t count = qei.count;
qei.state = ((qei.state << 4) | idx) & 0xFF;
if(qei.state == 0x42 || qei.state == 0xD4 || qei.state == 0x2B || qei.state == 0xBD) {
qei.data.position--;
if(qei.vel_timeout == 0 || qei.dir == QEI_DirCW) {
qei.dir = QEI_DirCCW;
qei.event.position_changed = On;
task_add_immediate(qei_post_event, NULL);
}
} else if(qei.state == 0x81 || qei.state == 0x17 || qei.state == 0xE8 || qei.state == 0x7E) {
qei.data.position++;
if(qei.vel_timeout == 0 || qei.dir == QEI_DirCCW) {
qei.dir = QEI_DirCW;
qei.event.position_changed = On;
task_add_immediate(qei_post_event, NULL);
}
}
}
}
static void qei_select_irq (uint8_t port, bool high)
{
if(high)
return;
if(!qei.dbl_click_timeout) {
qei.dbl_click_timeout = qei.settings.dbl_click_window;
} else if(qei.dbl_click_timeout < qei.settings.dbl_click_window) {
qei.dbl_click_timeout = 0;
task_delete(qei_dblclk_event, NULL);
task_add_immediate(qei_dblclk_event, NULL);
}
}
static bool qei_claim (encoder_on_event_ptr event_handler, void *context)
{
if(event_handler == NULL || qei.on_event)
return false;
qei.context = context;
qei.on_event = event_handler;
qei.encoder.reset = qei_reset;
qei.encoder.get_data = qei_get_data;
qei.encoder.configure = qei_configure;
if(qei.port_b != IOPORT_UNASSIGNED) {
ioport_enable_irq(qei.port_a, IRQ_Mode_Change, qei_ab_irq);
ioport_enable_irq(qei.port_b, IRQ_Mode_Change, qei_ab_irq);
}
if(qei.port_select != IOPORT_UNASSIGNED)
ioport_enable_irq(qei.port_select, IRQ_Mode_Change, qei_select_irq);
task_add_systick(qei_poll, NULL);
return true;
}
static inline void encoder_pin_claimed (uint8_t port, xbar_t *pin)
{
switch(pin->function) {
case Input_QEI_A:
qei.port_a = port;
break;
case Input_QEI_B:
qei.port_b = port;
qei.encoder.claim = qei_claim;
if(qei.port_a != IOPORT_UNASSIGNED)
encoder_register(&qei.encoder);
break;
case Input_QEI_Select:
qei.port_select = port;
qei.encoder.caps.select = On;
break;
default: break;
}
}
#endif

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
@@ -107,7 +107,9 @@ PROGMEM static const status_detail_t status_detail[] = {
#endif
{ Status_FileOpenFailed, "Could not open file." },
{ Status_UserException, "User defined error occured." },
{ Status_AuxiliaryPortUnusable, "Port is not usable." }
{ Status_AuxiliaryPortUnusable, "Port is not usable." },
{ Status_ToolInSPindle, "Tool in spindle." },
{ Status_NoToolInSPindle, "No tool in spindle." }
};
static error_details_t details = {

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
@@ -119,7 +119,9 @@ typedef enum {
Status_FileOpenFailed = 84,
Status_FsFormatFailed = 85,
Status_AuxiliaryPortUnusable = 86,
Status_StatusMax = Status_AuxiliaryPortUnusable,
Status_ToolInSPindle = 87,
Status_NoToolInSPindle = 88,
Status_StatusMax = Status_NoToolInSPindle,
Status_UserException = 253,
Status_Handled, // For internal use only
Status_Unhandled // For internal use only

364
gcode.c

File diff suppressed because it is too large Load Diff

28
gcode.h
View File

@@ -51,27 +51,36 @@ Do not alter values!
typedef enum {
NonModal_NoAction = 0, //!< 0 - Default, must be zero
NonModal_Dwell = 4, //!< 4 - G4
NonModal_SetCoordinateData = 10, //!< 10 - G10
NonModal_Settings = 10, //!< 10 - G10
NonModal_GoHome_0 = 28, //!< 28 - G28
NonModal_SetHome_0 = 38, //!< 38 - G28.1
NonModal_GoHome_1 = 30, //!< 30 - G30
NonModal_SetHome_1 = 40, //!< 40 - G30.1
NonModal_AbsoluteOverride = 53, //!< 53 - G53
NonModal_MacroCall = 65, //!< 65 - G65
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
NonModal_RestoreCoordinateOffset = 122, //!< 122 - G92.3
NonModal_SetAccelerationProfile = 187 //!< 187 - G187
#else
NonModal_RestoreCoordinateOffset = 122 //!< 122 - G92.3
NonModal_RestoreCoordinateOffset = 122 //!< 122 - G92.3
#endif
} non_modal_t;
typedef enum {
ToolAction_None = 0, //!< 0 - Default, must be zero
ToolAction_Change = 6, //!< 6 - M6
ToolAction_Set = 61, //!< 61 - M61
} tool_action_t;
typedef enum {
MacroCall_End = 0, //!< 0 - Default, must be zero (G67)
MacroCall_NonModal = 65, //!< 65 - G65
MacroCall_Modal = 66, //!< 66 - G66
MacroCall_Modal1 = 166, //!< 166 - G66.1
MacroCall_NonModal98 = 98, //!< 98 - M98
} macro_call_t;
typedef enum {
ModalState_NoAction = 0, //!< 0 - Default, must be zero
@@ -602,6 +611,7 @@ typedef struct {
typedef struct g66_arguments
{
macro_call_t call;
uint32_t call_level;
gc_values_t values;
parameter_words_t words;
@@ -624,10 +634,10 @@ typedef struct {
float path_tolerance; //!< Path blending tolerance
float cam_tolerance; //!< Naive CAM tolerance
#endif
uint32_t line_number; //!< Last line number sent
uint32_t line_number; //!< Last line number sent
tool_id_t tool_pending; //!< Tool to be selected on next M6
#if NGC_EXPRESSIONS_ENABLE
uint32_t g43_pending; //!< Tool offset to be selected on next M6, for macro ATC
tool_id_t g43_pending; //!< Tool offset to be selected on next M6, for macro ATC
#endif
bool file_run; //!< Tracks % command
bool file_stream; //!< Tracks streaming from file
@@ -663,6 +673,7 @@ It will also be passed to mc_jog_execute() and any user M-code validation and ex
*/
typedef struct {
non_modal_t non_modal_command; //!< Non modal command
tool_action_t tool_action; //!< Non modal tool change
override_mode_t override_command; //!< Override command TODO: add to non_modal above?
user_mcode_t user_mcode; //!< Set > 0 if a user M-code is found.
bool user_mcode_sync; //!< Set to \a true by M-code validation handler if M-code is to be executed after synchronization.
@@ -674,6 +685,7 @@ typedef struct {
uint32_t arc_turns; //
parameter_words_t g65_words; //!< Parameter words to pass to G65 macro.
#if NGC_PARAMETERS_ENABLE
macro_call_t macro_call;
modal_state_action_t state_action; //!< M70-M73 modal state action
#endif
#if N_AXIS > 3

2
grbl.h
View File

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

29
hal.h
View File

@@ -453,32 +453,6 @@ typedef struct {
atc_get_state_ptr atc_get_state; //!< Optional handler for checking ATC status.
} tool_ptrs_t;
/*******************
* Encoder input *
*******************/
/*! \brief Pointer to function for getting number of encoders supported.
\returns number of encoders.
*/
typedef uint8_t (*encoder_get_n_encoders_ptr)(void);
/*! \brief Pointer to callback function to receive encoder events.
\param encoder pointer to a \a encoder_t struct.
\param position encoder position.
*/
typedef void (*encoder_on_event_ptr)(encoder_t *encoder, int32_t position);
/*! \brief Pointer to function for resetting encoder data.
\param id encoder id.
*/
typedef void (*encoder_reset_ptr)(uint_fast8_t id);
typedef struct {
encoder_get_n_encoders_ptr get_n_encoders; //!< Optional handler for getting number of encoders supported.
encoder_on_event_ptr on_event; //!< Optional callback handler for receiving encoder events.
encoder_reset_ptr reset; //!< Optional handler for resetting data for an encoder.
} encoder_ptrs_t;
/*! \brief Pointer to callback function to receive spindle encoder index events.
\param count index pulse count.
*/
@@ -513,7 +487,7 @@ typedef union {
comp1 :1, //!< Timer supports compare interrupt 0
comp2 :1, //!< Timer supports compare interrupt 1
ext_clk :1, //!< External clock supported
encoder :1, //!< Emcode mode supported
encoder :1, //!< Encode mode supported
unused :2;
};
} timer_cap_t;
@@ -691,7 +665,6 @@ typedef struct {
pallet_shuttle_ptr pallet_shuttle; //!< Optional handler for performing a pallet shuttle on program end (M60).
void (*reboot)(void); //!< Optoional handler for rebooting the controller. This will be called when #ASCII_ESC followed by #CMD_REBOOT is received.
encoder_ptrs_t encoder; //!< Optional handlers for encoder support.
spindle_encoder_on_index_ptr spindle_encoder_on_index; //!< Optional handler (callback) to receive spindle encoder index event.
/*! \brief Optional handler for getting the current axis positions.

View File

@@ -223,14 +223,14 @@ FLASHMEM static void rx_packet (modbus_message_t *msg)
break;
case ModBus_ReadCoils:
case ModBus_ReadDiscreteInputs:
response.num_values = min(msg->adu[2], 3); // byte count, not /2
response.num_values = min(msg->adu[2], MODBUS_MAX_REGISTERS); // byte count
for(idx = 0; idx < response.num_values; idx++)
response.values[idx] = msg->adu[3 + idx]; // single bytes, not u16
response.values[idx] = msg->adu[3 + idx];
break;
default:;
response.num_values = cmds[response.function].single_register || cmds[response.function].is_write ? 2 : msg->adu[2] / 2;
response.num_values = min(response.num_values, 3);
response.num_values = min(response.num_values, MODBUS_MAX_REGISTERS);
uint_fast8_t pos = cmds[response.function].single_register || cmds[response.function].is_write ? 2 : 3;
for(idx = 0; idx < response.num_values; idx++)
response.values[idx] = modbus_read_u16(&msg->adu[pos + (idx << 1)]);
@@ -303,9 +303,9 @@ FLASHMEM status_code_t modbus_message (uint8_t server, modbus_function_t functio
cmd.adu[5] = (uint8_t)(registers & 0xFF);
if(function == ModBus_ReadCoils || function == ModBus_ReadDiscreteInputs)
cmd.rx_length = 5 + ((registers + 7) / 8); // bit-packed, ceil(n/8) bytes
cmd.rx_length = 5 + ((registers + 7) / 8); // bit-packed, ceil(n/8) bytes
else
cmd.rx_length = 5 + (registers << 1); // 2 bytes per register
cmd.rx_length = 5 + (registers << 1); // 2 bytes per register
}
}

View File

@@ -5,7 +5,7 @@
Part of grblHAL
Copyright (c) 2021-2025 Terje Io
Copyright (c) 2021-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
@@ -49,7 +49,7 @@
#error "MPG mode input is not supported in this configuration!"
#endif
#if QEI_SELECT_ENABLE && !defined(QEI_SELECT_PIN)
#if (ENCODER_ENABLE & 1) && !defined(QEI_SELECT_PIN)
#error "Encoder select input is not supported in this configuration!"
#endif
@@ -157,12 +157,16 @@ static aux_ctrl_t aux_ctrl[] = {
#if MPG_ENABLE == 1 && defined(MPG_MODE_PIN)
add_aux_input(Input_MPGSelect, MPG_MODE, IRQ_Mode_Change, 0)
#endif
#if QEI_SELECT_ENABLE && defined(QEI_SELECT_PIN)
#if QEI_ENABLE && defined(QEI_A_PIN) && defined(QEI_B_PIN)
add_aux_input(Input_QEI_A, QEI_A, IRQ_Mode_Change, 0)
add_aux_input(Input_QEI_B, QEI_B, IRQ_Mode_Change, 0)
#endif
#if (ENCODER_ENABLE & 1) && defined(QEI_SELECT_PIN)
add_aux_input(Input_QEI_Select, QEI_SELECT, IRQ_Mode_RisingFalling, 0)
#endif
// Probe pins can be bound explicitly and can be "degraded" to not interrupt capable.
#if PROBE_ENABLE && defined(PROBE_PIN)
add_aux_input(Input_Probe, PROBE, IRQ_Mode_RisingFalling, 0)
add_aux_input(Input_Probe, PROBE, IRQ_Mode_Change, 0)
#endif
#if PROBE2_ENABLE && defined(PROBE2_PIN)
add_aux_input(Input_Probe2, PROBE2, IRQ_Mode_RisingFalling, 0)
@@ -198,11 +202,19 @@ static aux_ctrl_t aux_ctrl[] = {
#endif
};
// General inputs
static inline bool aux_ctrl_is_probe (pin_function_t function)
{
return function == Input_Probe || function == Input_Probe2 || function == Input_Toolsetter;
}
static inline bool aux_ctrl_is_encoder (pin_function_t function)
{
return function == Input_QEI_A || function == Input_QEI_B || function == Input_QEI_Select;
}
#ifdef STM32_PLATFORM
static inline aux_ctrl_t *aux_ctrl_get_fn (aux_gpio_t gpio)
@@ -228,11 +240,10 @@ static inline xbar_t *aux_ctrl_claim_port (aux_ctrl_t *aux_ctrl)
if(aux_ctrl) {
if(aux_ctrl->port != IOPORT_UNASSIGNED && (pin = ioport_claim(Port_Digital, Port_Input, &aux_ctrl->port, NULL))) {
aux_ctrl->gpio.port = pin->port;
aux_ctrl->gpio.pin = pin->pin;
ioport_set_function(pin, aux_ctrl->function, &aux_ctrl->signal);
if(ioport_set_function(pin, aux_ctrl->function, &aux_ctrl->signal))
pin->function = aux_ctrl->function;
} else
aux_ctrl->port = IOPORT_UNASSIGNED;
}
@@ -279,7 +290,7 @@ static inline void aux_ctrl_irq_enable (settings_t *settings, ioport_interrupt_c
if(idx) do {
if(aux_ctrl[--idx].port != 0xFF && aux_ctrl[idx].irq_mode != IRQ_Mode_None) {
if(!aux_ctrl_is_probe(aux_ctrl[idx].function)) {
if(!(aux_ctrl_is_probe(aux_ctrl[idx].function) || aux_ctrl_is_encoder(aux_ctrl[idx].function))) {
pin_irq_mode_t irq_mode;
if((irq_mode = aux_ctrl[idx].irq_mode) & IRQ_Mode_RisingFalling)
irq_mode = (settings->control_invert.mask & aux_ctrl[idx].signal.mask) ? IRQ_Mode_Falling : IRQ_Mode_Rising;
@@ -560,7 +571,7 @@ static inline void aux_ctrl_claim_out_ports (aux_claim_explicit_out_ptr aux_clai
#endif
// IRQ enabled input singnals
/*
#if QEI_ENABLE
#ifndef QEI_A_BIT
#define QEI_A_BIT (1<<QEI_A_PIN)
@@ -568,10 +579,10 @@ static inline void aux_ctrl_claim_out_ports (aux_claim_explicit_out_ptr aux_clai
#ifndef QEI_B_BIT
#define QEI_B_BIT (1<<QEI_B_PIN)
#endif
#else
#else*/
#define QEI_A_BIT 0
#define QEI_B_BIT 0
#endif
//#endif
#ifndef QEI_SELECT_BIT
#define QEI_SELECT_BIT 0

View File

@@ -252,6 +252,9 @@ FLASHMEM bool plan_reset (void)
memset(&pl, 0, sizeof(planner_t)); // Clear planner struct
pl.override.feed_rate = sys.override.feed_rate;
pl.override.rapid_rate = sys.override.rapid_rate;
plan_reset_buffer(&block_buffer, block_buffer.blocks[0].next == NULL);
return true;
@@ -313,12 +316,12 @@ float plan_compute_profile_nominal_speed (plan_block_t *block)
: block->programmed_rate;
if(block->condition.rapid_motion)
nominal_speed *= (0.01f * (float)sys.override.rapid_rate);
nominal_speed *= (0.01f * (float)pl.override.rapid_rate);
else {
if(sys.override.feed_rate != 100 && !block->condition.no_feed_override) {
if(pl.override.feed_rate != 100 && !block->condition.no_feed_override) {
if(nominal_speed > block->rapid_rate)
nominal_speed = block->rapid_rate;
nominal_speed *= (0.01f * (float)sys.override.feed_rate);
nominal_speed *= (0.01f * (float)pl.override.feed_rate);
}
if(nominal_speed > block->rapid_rate)
nominal_speed = block->rapid_rate;
@@ -741,13 +744,29 @@ void plan_sync_velocity (void *block)
}
// Set feed overrides
FLASHMEM static void _plan_feed_override (override_t feed_rate, override_t rapid_rate)
{
bool feedrate_changed, rapidrate_changed = false;
if(((feedrate_changed = feed_rate != pl.override.feed_rate) || (rapidrate_changed = rapid_rate != pl.override.rapid_rate))) {
pl.override.feed_rate = feed_rate;
pl.override.rapid_rate = rapid_rate;
if(plan_update_velocity_profile_parameters())
plan_cycle_reinitialize();
if(grbl.on_override_changed) {
if(feedrate_changed)
grbl.on_override_changed(OverrideChanged_FeedRate);
if(rapidrate_changed)
grbl.on_override_changed(OverrideChanged_RapidRate);
}
}
}
FLASHMEM void plan_feed_override (override_t feed_override, override_t rapid_override)
{
bool feedrate_changed = false, rapidrate_changed = false;
if(sys.override.control.feed_rates_disable)
return;
if(feed_override == 0)
feed_override = sys.override.feed_rate;
else
@@ -758,20 +777,18 @@ FLASHMEM void plan_feed_override (override_t feed_override, override_t rapid_ove
else
rapid_override = constrain(rapid_override, 5, 100);
if((feedrate_changed = feed_override != sys.override.feed_rate) ||
(rapidrate_changed = rapid_override != sys.override.rapid_rate)) {
if(feed_override != sys.override.feed_rate || rapid_override != sys.override.rapid_rate) {
sys.override.feed_rate = feed_override;
sys.override.rapid_rate = rapid_override;
report_add_realtime(Report_Overrides); // Set to report change immediately
if(plan_update_velocity_profile_parameters())
plan_cycle_reinitialize();
if(grbl.on_override_changed) {
if(feedrate_changed)
grbl.on_override_changed(OverrideChanged_FeedRate);
if(rapidrate_changed)
grbl.on_override_changed(OverrideChanged_RapidRate);
}
}
if(sys.override.control.feed_rates_disable)
_plan_feed_override(DEFAULT_FEED_OVERRIDE, DEFAULT_RAPID_OVERRIDE);
else
_plan_feed_override(sys.override.feed_rate, sys.override.rapid_rate);
}
FLASHMEM void plan_data_init (plan_line_data_t *plan_data)
@@ -779,6 +796,7 @@ FLASHMEM void plan_data_init (plan_line_data_t *plan_data)
memset(plan_data, 0, sizeof(plan_line_data_t));
plan_data->offset_id = gc_state.offset_id;
plan_data->spindle.hal = gc_spindle_get(-1)->hal;
plan_data->condition.no_feed_override = sys.override.control.feed_rates_disable;
plan_data->condition.target_validated = plan_data->condition.target_valid = sys.soft_limits.mask == 0;
#ifdef KINEMATICS_API
plan_data->rate_multiplier = 1.0f;

View File

@@ -3,7 +3,7 @@
Part of grblHAL
Copyright (c) 2019-2025 Terje Io
Copyright (c) 2019-2026 Terje Io
Copyright (c) 2011-2016 Sungeun K. Jeon for Gnea Research LLC
Copyright (c) 2009-2011 Simen Svale Skogsrud
@@ -162,6 +162,10 @@ typedef struct {
float previous_unit_vec[N_AXIS]; // Unit vector of previous path line segment
float previous_nominal_speed; // Nominal speed of previous path line segment
float actual_rpm; // Updated when in units per revolution blocks (G95) mode.
struct {
override_t feed_rate; //!< Feed rate override value in percent
override_t rapid_rate; //!< Rapids override value in percent
} override;
} planner_t;
// Initialize and reset the motion plan subsystem

View File

@@ -7,7 +7,7 @@
Part of grblHAL
Copyright (c) 2020-2025 Terje Io
Copyright (c) 2020-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
@@ -154,25 +154,7 @@ typedef struct {
uint8_t id;
} modbus_tcp_settings_t;
// Quadrature encoder interface
typedef enum {
Encoder_Universal = 0,
Encoder_FeedRate,
Encoder_RapidRate,
Encoder_Spindle_RPM,
Encoder_MPG,
Encoder_MPG_X,
Encoder_MPG_Y,
Encoder_MPG_Z,
Encoder_MPG_A,
Encoder_MPG_B,
Encoder_MPG_C,
Encoder_MPG_U,
Encoder_MPG_V,
Encoder_MPG_W,
Encoder_Spindle_Position
} encoder_mode_t;
// Encoder settings offsets
typedef enum {
Setting_EncoderMode = 0,
@@ -181,45 +163,6 @@ typedef enum {
Setting_EncoderDblClickWindow = 3 // ms
} encoder_setting_id_t;
typedef union {
uint8_t events;
struct {
uint8_t position_changed :1,
direction_changed :1,
click :1,
dbl_click :1,
long_click :1,
index_pulse :1,
unused :2;
};
} encoder_event_t;
typedef union {
uint8_t flags;
uint8_t value;
struct {
uint8_t single_count_per_detent :1;
};
} encoder_flags_t;
typedef struct {
encoder_mode_t mode;
uint32_t cpr; //!< Count per revolution.
uint32_t cpd; //!< Count per detent.
uint32_t dbl_click_window; //!< ms.
encoder_flags_t flags;
} encoder_settings_t;
typedef struct {
encoder_mode_t mode;
uint_fast8_t id;
uint_fast8_t axis; //!< Axis index for MPG encoders, 0xFF for others.
int32_t position;
uint32_t velocity;
encoder_event_t event;
encoder_settings_t *settings;
} encoder_t;
// DISPLAYS:
// Interfaces

View File

@@ -7,7 +7,7 @@
Part of grblHAL
Copyright (c) 2021-2025 Terje Io
Copyright (c) 2021-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
@@ -181,6 +181,11 @@
homing_pulloff_init();
#endif
#if ENCODER_ENABLE
extern bool encoder_init (void);
encoder_init();
#endif
extern void my_plugin_init (void);
my_plugin_init();

View File

@@ -29,10 +29,12 @@
#include "hal.h"
#include "config.h"
#include "encoders.h"
#include "machine_limits.h"
#include "nvs_buffer.h"
#include "tool_change.h"
#include "state_machine.h"
#include "strutils.h"
#if ENABLE_BACKLASH_COMPENSATION
#include "motion_control.h"
#endif
@@ -738,17 +740,26 @@ static status_code_t set_pwm_mode (setting_id_t id, uint_fast16_t int_value)
static status_code_t set_pwm_options (setting_id_t id, uint_fast16_t int_value)
{
if(int_value & 0b0001) {
if(int_value > 0b1111)
#if N_SPINDLE > 1
if(int_value > 0b11111)
return Status_SettingValueOutOfRange;
#else
if(int_value > 0b01111)
return Status_SettingValueOutOfRange;
#endif
settings.pwm_spindle.flags.pwm_disable = Off;
settings.pwm_spindle.flags.enable_rpm_controlled = !!(int_value & 0b0010);
settings.pwm_spindle.flags.laser_mode_disable = !!(int_value & 0b0100);
settings.pwm_spindle.flags.pwm_ramped = !!(int_value & 0b1000);
settings.pwm_spindle.flags.enable_rpm_controlled = !!(int_value & 0b00010);
settings.pwm_spindle.flags.laser_mode_disable = !!(int_value & 0b00100);
settings.pwm_spindle.flags.pwm_ramped = !!(int_value & 0b01000);
#if N_SPINDLE > 1
settings.pwm_spindle.flags.ignore_delays = !!(int_value & 0b10000);
#endif
} else {
settings.pwm_spindle.flags.pwm_disable = On;
settings.pwm_spindle.flags.enable_rpm_controlled =
settings.pwm_spindle.flags.laser_mode_disable =
settings.pwm_spindle.flags.pwm_ramped = Off;
settings.pwm_spindle.flags.pwm_ramped =
settings.pwm_spindle.flags.ignore_delays = Off;
}
return Status_OK;
@@ -1520,9 +1531,10 @@ FLASHMEM static uint32_t get_int (setting_id_t id)
value = settings.pwm_spindle.flags.pwm_disable
? 0
: (0b0001 |
(settings.pwm_spindle.flags.enable_rpm_controlled ? 0b0010 : 0) |
(settings.pwm_spindle.flags.laser_mode_disable ? 0b0100 : 0) |
(settings.pwm_spindle.flags.pwm_ramped ? 0b1000 : 0));
(settings.pwm_spindle.flags.enable_rpm_controlled ? 0b00010 : 0) |
(settings.pwm_spindle.flags.laser_mode_disable ? 0b00100 : 0) |
(settings.pwm_spindle.flags.pwm_ramped ? 0b01000 : 0) |
(settings.pwm_spindle.flags.ignore_delays ? 0b10000 : 0));
break;
case Setting_Mode:
@@ -2068,7 +2080,7 @@ PROGMEM static const setting_detail_t setting_detail[] = {
{ Setting_InvertProbePin, Group_Probing, "Invert probe inputs", NULL, Format_Bitfield, probe_signals, NULL, NULL, Setting_IsLegacyFn, set_probe_invert, get_int, is_setting_available },
{ Setting_SpindlePWMBehaviour, Group_Spindle, "Deprecated", NULL, Format_Bool, NULL, NULL, NULL, Setting_IsLegacyFn, set_pwm_mode, get_int, is_setting_available },
{ Setting_GangedDirInvertMask, Group_Stepper, "Ganged axes direction invert", NULL, Format_Bitfield, ganged_axes, NULL, NULL, Setting_IsExtendedFn, set_ganged_dir_invert, get_int, is_setting_available },
{ Setting_SpindlePWMOptions, Group_Spindle, "PWM spindle options", NULL, Format_XBitfield, "Enable,RPM controls spindle enable signal,Disable laser mode capability,Enable ramping", NULL, NULL, Setting_IsExtendedFn, set_pwm_options, get_int, is_setting_available },
{ Setting_SpindlePWMOptions, Group_Spindle, "PWM spindle options", NULL, Format_XBitfield, "Enable,RPM controls spindle enable signal,Disable laser mode capability,Enable ramping" PWM_SPINDLE_NO_DELAYS, NULL, NULL, Setting_IsExtendedFn, set_pwm_options, get_int, is_setting_available },
#if COMPATIBILITY_LEVEL <= 1
{ Setting_StatusReportMask, Group_General, "Status report options", NULL, Format_Bitfield, "Position in machine coordinate,Buffer state,Line numbers,Feed & speed,Pin state,Work coordinate offset,Overrides,Probe coordinates,Buffer sync on WCO change,Parser state,Alarm substatus,Run substatus,Enable when homing,Distance-to-go", NULL, NULL, Setting_IsExtendedFn, set_report_mask, get_int, NULL },
#else
@@ -2685,6 +2697,10 @@ FLASHMEM static void sanity_check (void)
settings.steppers.rotary_wrap.mask &= settings.steppers.is_rotary.mask;
#endif
#if N_SPINDLE == 1
settings.pwm_spindle.flags.ignore_delays = Off;
#endif
settings.control_invert.mask |= limits_override.mask;
settings.control_disable_pullup.mask &= ~limits_override.mask;
}
@@ -2906,7 +2922,7 @@ static inline const setting_detail_t *_setting_get_details (setting_id_t id, uin
if(details->settings[idx].group == Group_Axis0 && grbl.on_set_axis_setting_unit)
set_axis_unit(&details->settings[idx], grbl.on_set_axis_setting_unit(details->settings[idx].id, offset));
if(offset && details->iterator == NULL && offset >= (details->settings[idx].group == Group_Encoder0 ? hal.encoder.get_n_encoders() : N_AXIS))
if(offset && details->iterator == NULL && offset >= (details->settings[idx].group == Group_Encoder0 ? encoders_get_count() : N_AXIS))
return NULL;
if(set)
@@ -3062,22 +3078,6 @@ FLASHMEM static status_code_t validate_uint_value (const setting_detail_t *setti
return Status_OK;
}
FLASHMEM static uint32_t strnumentries (const char *s, const char delimiter)
{
if(s == NULL || *s == '\0')
return 0;
char *p = (char *)s;
uint32_t entries = 1;
while((p = strchr(p, delimiter))) {
p++;
entries++;
}
return entries;
}
FLASHMEM setting_datatype_t setting_datatype_to_external (setting_datatype_t datatype)
{
switch(datatype) {

View File

@@ -117,9 +117,14 @@ FLASHMEM static bool spindle_activate (spindle_id_t spindle_id, spindle_num_t sp
sys_spindle[spindle_num].param.hal = &sys_spindle[spindle_num].hal;
if(sys_spindle[spindle_num].param.override_pct == 0)
sys_spindle[spindle_num].param.override_pct = DEFAULT_SPINDLE_RPM_OVERRIDE;
if(spindle_hal.type == SpindleType_PWM && !spindle_hal.cap.laser && spindle_hal.context.pwm->flags.ramp_pwm ) {
sys_spindle[spindle_num].param.ramp_up = settings.spindle.on_delay > 0;
sys_spindle[spindle_num].param.ramp_down = settings.spindle.off_delay > 0;
sys_spindle[spindle_num].param.option.ramp_up = sys_spindle[spindle_num].param.option.ramp_down = sys_spindle[spindle_num].param.option.ignore_delays = Off;
if(spindle_hal.type == SpindleType_PWM) {
if(!spindle_hal.cap.laser && spindle_hal.context.pwm->flags.ramp_pwm ) {
sys_spindle[spindle_num].param.option.ramp_up = settings.spindle.on_delay > 0;
sys_spindle[spindle_num].param.option.ramp_down = settings.spindle.off_delay > 0;
}
if(!sys_spindle[spindle_num].param.option.ramp_up)
sys_spindle[spindle_num].param.option.ignore_delays = spindle_hal.context.pwm->settings->flags.ignore_delays;
}
spindle_hal.param = &sys_spindle[spindle_num].param;
memcpy(&sys_spindle[spindle_num].hal, &spindle_hal, sizeof(spindle_ptrs_t));
@@ -291,11 +296,12 @@ FLASHMEM void spindle_update_caps (spindle_ptrs_t *spindle, spindle_pwm_t *pwm_c
sys_spindle[idx].hal.rpm_max = spindle->rpm_max;
sys_spindle[idx].hal.pwm_off_value = spindle->pwm_off_value;
if((sys_spindle[idx].hal.cap.laser = spindle->cap.laser) || spindle->type != SpindleType_PWM || !spindle->context.pwm->flags.ramp_pwm)
sys_spindle[idx].param.ramp_up = sys_spindle[idx].param.ramp_down = Off;
sys_spindle[idx].param.option.ramp_up = sys_spindle[idx].param.option.ramp_down = Off;
else {
sys_spindle[idx].param.ramp_up = settings.spindle.on_delay > 0;
sys_spindle[idx].param.ramp_down = settings.spindle.off_delay > 0;
sys_spindle[idx].param.option.ramp_up = settings.spindle.on_delay > 0;
sys_spindle[idx].param.option.ramp_down = settings.spindle.off_delay > 0;
}
sys_spindle[idx].param.option.ignore_delays = spindle->type == SpindleType_PWM && !sys_spindle[idx].param.option.ramp_up && spindle->context.pwm->settings->flags.ignore_delays;
break;
}
} while(idx);
@@ -609,37 +615,33 @@ __NOTE:__ Unlike motion overrides, spindle overrides do not require a planner re
*/
FLASHMEM float spindle_set_override (spindle_ptrs_t *spindle, override_t speed_override)
{
if(speed_override == DEFAULT_SPINDLE_RPM_OVERRIDE || !spindle->param->state.override_disable) {
speed_override = speed_override == 0 ? spindle->param->override_pct : constrain(speed_override, MIN_SPINDLE_RPM_OVERRIDE, MAX_SPINDLE_RPM_OVERRIDE);
speed_override = constrain(speed_override, MIN_SPINDLE_RPM_OVERRIDE, MAX_SPINDLE_RPM_OVERRIDE);
if((uint8_t)speed_override != spindle->param->override_pct || spindle->param->state.override_disable != spindle->param->option.override_disable) {
if((uint8_t)speed_override != spindle->param->override_pct) {
float rpm = spindle->param->rpm_overridden;
float rpm = spindle->param->rpm_overridden;
sys_state_t state = state_get();
spindle_set_rpm(spindle, spindle->param->rpm, speed_override);
spindle_set_rpm(spindle, spindle->param->rpm, speed_override);
if(!(spindle->param->ramp_up &&
spindle->get_state(spindle).on &&
spindle_ramp_override(spindle, rpm, spindle->param->rpm_overridden))) {
if(state == STATE_IDLE) {
if(spindle->get_pwm && spindle->update_pwm)
spindle->update_pwm(spindle, spindle->get_pwm(spindle, spindle->param->rpm_overridden));
else if(spindle->update_rpm)
spindle->update_rpm(spindle, spindle->param->rpm_overridden);
} else
sys.step_control.update_spindle_rpm = On;
}
report_add_realtime(Report_Overrides); // Set to report change immediately
// if(grbl.on_spindle_programmed)
// grbl.on_spindle_programmed(spindle, spindle->param->state, spindle->param->rpm, spindle->param->rpm_mode);
if(grbl.on_override_changed)
grbl.on_override_changed(OverrideChanged_SpindleRPM);
if(!(spindle->param->option.ramp_up &&
spindle->get_state(spindle).on &&
spindle_ramp_override(spindle, rpm, spindle->param->rpm_overridden))) {
if(state_get() == STATE_IDLE) {
if(spindle->get_pwm && spindle->update_pwm)
spindle->update_pwm(spindle, spindle->get_pwm(spindle, spindle->param->rpm_overridden));
else if(spindle->update_rpm)
spindle->update_rpm(spindle, spindle->param->rpm_overridden);
} else
sys.step_control.update_spindle_rpm = On;
}
report_add_realtime(Report_Overrides); // Set to report change immediately
// if(grbl.on_spindle_programmed)
// grbl.on_spindle_programmed(spindle, spindle->param->state, spindle->param->rpm, spindle->param->rpm_mode);
if(grbl.on_override_changed)
grbl.on_override_changed(OverrideChanged_SpindleRPM);
}
return spindle->param->rpm_overridden;
@@ -647,10 +649,8 @@ FLASHMEM float spindle_set_override (spindle_ptrs_t *spindle, override_t speed_o
FLASHMEM bool spindle_override_disable (spindle_ptrs_t *spindle, bool disable)
{
if(disable && !spindle->param->state.override_disable)
spindle_set_override(spindle, DEFAULT_SPINDLE_RPM_OVERRIDE);
spindle->param->state.override_disable = disable;
spindle->param->option.override_disable = disable;
spindle_set_override(spindle, spindle->param->override_pct);
return disable;
}
@@ -723,7 +723,7 @@ FLASHMEM static bool _spindle_set_state (spindle_ptrs_t *spindle, spindle_state_
if(!state.on) { // Halt or set spindle direction and rpm.
rpm = 0.0f;
if(spindle->param->ramp_down)
if(spindle->param->option.ramp_down)
spindle_ramp(spindle, spindle->param->state, spindle->param->rpm_overridden, 0.0f, settings.spindle.off_delay);
spindle->set_state(spindle, (spindle_state_t){0}, (spindle->param->rpm_overridden = 0.0f));
} else {
@@ -732,7 +732,7 @@ FLASHMEM static bool _spindle_set_state (spindle_ptrs_t *spindle, spindle_state_
if(spindle->cap.laser && state.ccw)
rpm = 0.0f; // TODO: May need to be rpm_min*(100/MAX_SPINDLE_RPM_OVERRIDE);
if(spindle->param->ramp_up) {
if(spindle->param->option.ramp_up) {
if(spindle->param->state.on && spindle->param->state.ccw != state.ccw) {
spindle_ramp(spindle, spindle->param->state, spindle->param->rpm_overridden, 0.0f, settings.spindle.off_delay);
spindle->param->rpm = spindle->param->rpm_overridden = 0.0f;
@@ -775,7 +775,9 @@ FLASHMEM static bool spindle_set_state_wait (spindle_ptrs_t *spindle, spindle_st
if((ok = _spindle_set_state(spindle, state, rpm, delay_ms))) {
if(sys.override.control.spindle_wait_disable || (state.on ? spindle->param->ramp_up : spindle->param->ramp_down)) {
if(sys.override.control.spindle_wait_disable ||
spindle->param->option.ignore_delays ||
(state.on ? spindle->param->option.ramp_up : spindle->param->option.ramp_down)) {
sys.override.control.spindle_wait_disable = Off;
} else {
@@ -859,15 +861,16 @@ FLASHMEM bool spindle_restore (spindle_ptrs_t *spindle, spindle_state_t state, f
*/
float spindle_set_rpm (spindle_ptrs_t *spindle, float rpm, override_t override_pct)
{
if(override_pct != 100)
if(override_pct != 100 && !spindle->param->option.override_disable)
rpm *= 0.01f * (float)override_pct; // Scale RPM by override value.
rpm = rpm <= 0.0f ? 0.0f : constrain(rpm, spindle->rpm_min, spindle->rpm_max);
spindle->param->rpm_overridden = rpm;
spindle->param->override_pct = override_pct;
spindle->param->state.override_disable = spindle->param->option.override_disable;
return spindle->param->rpm_overridden;
return rpm;
}
/*! \brief Turn off all enabled spindles.
@@ -880,7 +883,7 @@ FLASHMEM void spindle_all_off (bool reset)
do {
if((spindle = spindle_get(--spindle_num))) {
if(!reset && spindle->param->ramp_down)
if(!reset && spindle->param->option.ramp_down)
spindle_set_state(spindle, (spindle_state_t){0}, 0.0f);
spindle->param->rpm = spindle->param->rpm_overridden = 0.0f;
@@ -1118,17 +1121,26 @@ FLASHMEM static status_code_t set_spindle_invert (setting_id_t id, uint_fast16_t
FLASHMEM static status_code_t set_pwm_options (setting_id_t id, uint_fast16_t int_value)
{
if(int_value & 0b0001) {
if(int_value > 0b1111)
#if N_SPINDLE > 1
if(int_value > 0b11111)
return Status_SettingValueOutOfRange;
#else
if(int_value > 0b01111)
return Status_SettingValueOutOfRange;
#endif
sp1_settings.cfg.flags.pwm_disable = Off;
sp1_settings.cfg.flags.enable_rpm_controlled = !!(int_value & 0b0010);
sp1_settings.cfg.flags.laser_mode_disable = !!(int_value & 0b0100);
sp1_settings.cfg.flags.pwm_ramped = !!(int_value & 0b1000);
} else {
sp1_settings.cfg.flags.enable_rpm_controlled = !!(int_value & 0b00010);
sp1_settings.cfg.flags.laser_mode_disable = !!(int_value & 0b00100);
sp1_settings.cfg.flags.pwm_ramped = !!(int_value & 0b01000);
#if N_SPINDLE > 1
sp1_settings.cfg.flags.ignore_delays = !!(int_value & 0b10000);
#endif
} else {
sp1_settings.cfg.flags.pwm_disable = On;
sp1_settings.cfg.flags.enable_rpm_controlled =
sp1_settings.cfg.flags.laser_mode_disable =
sp1_settings.cfg.flags.pwm_ramped = Off;
sp1_settings.cfg.flags.pwm_ramped =
sp1_settings.cfg.flags.ignore_delays = Off;
}
return Status_OK;
@@ -1144,9 +1156,10 @@ FLASHMEM static uint32_t get_int (setting_id_t id)
value = sp1_settings.cfg.flags.pwm_disable
? 0
: (0b0001 |
(sp1_settings.cfg.flags.enable_rpm_controlled ? 0b0010 : 0) |
(sp1_settings.cfg.flags.laser_mode_disable ? 0b0100 : 0) |
(sp1_settings.cfg.flags.pwm_ramped ? 0b1000 : 0));
(sp1_settings.cfg.flags.enable_rpm_controlled ? 0b00010 : 0) |
(sp1_settings.cfg.flags.laser_mode_disable ? 0b00100 : 0) |
(sp1_settings.cfg.flags.pwm_ramped ? 0b01000 : 0) |
(sp1_settings.cfg.flags.ignore_delays ? 0b10000 : 0));
break;
case Setting_SpindleInvertMask1:
@@ -1232,7 +1245,7 @@ PROGMEM static const setting_detail_t spindle1_settings[] = {
{ Setting_Spindle_DirPort, Group_AuxPorts, "PWM2 spindle direction port", NULL, Format_Decimal, "-#0", "-1", max_dport, Setting_NonCoreFn, set_port, get_port, has_ports, { .reboot_required = On } },
{ Setting_SpindleInvertMask1, Group_Spindle, "PWM2 spindle signals invert", NULL, Format_Bitfield, spindle_signals, NULL, NULL, Setting_IsExtendedFn, set_spindle_invert, get_int, NULL, { .reboot_required = On } },
{ Setting_Spindle_PWMPort, Group_AuxPorts, "PWM2 spindle PWM port", NULL, Format_Decimal, "-#0", "0", max_aport, Setting_NonCoreFn, set_port, get_port, has_ports, { .reboot_required = On } },
{ Setting_SpindlePWMOptions1, Group_Spindle, "PWM2 spindle options", NULL, Format_XBitfield, "Enable,RPM controls spindle enable signal,Disable laser mode capability,Enable ramping", NULL, NULL, Setting_IsExtendedFn, set_pwm_options, get_int, has_pwm },
{ Setting_SpindlePWMOptions1, Group_Spindle, "PWM2 spindle options", NULL, Format_XBitfield, "Enable,RPM controls spindle enable signal,Disable laser mode capability,Enable ramping" PWM_SPINDLE_NO_DELAYS, NULL, NULL, Setting_IsExtendedFn, set_pwm_options, get_int, has_pwm },
{ Setting_RpmMax1, Group_Spindle, "PWM2 spindle max speed", "RPM", Format_Decimal, "#####0.000", NULL, NULL, Setting_IsLegacy, &sp1_settings.cfg.rpm_max, NULL, has_pwm },
{ Setting_RpmMin1, Group_Spindle, "PWM2 spindle min speed", "RPM", Format_Decimal, "#####0.000", NULL, NULL, Setting_IsLegacy, &sp1_settings.cfg.rpm_min, NULL, has_pwm },
{ Setting_PWMFreq1, Group_Spindle, "PWM2 spindle PWM frequency", "Hz", Format_Decimal, "#####0", NULL, NULL, Setting_IsExtended, &sp1_settings.cfg.pwm_freq, NULL, has_freq },
@@ -1363,6 +1376,10 @@ FLASHMEM static void spindle1_settings_load (void)
{
if((hal.nvs.memcpy_from_nvs((uint8_t *)&sp1_settings, nvs_address, sizeof(spindle1_pwm_settings_t), true) != NVS_TransferResult_OK))
spindle1_settings_restore();
#if N_SPINDLE == 1
sp1_settings.cfg.flags.ignore_delays = Off;
#endif
}
FLASHMEM spindle1_pwm_settings_t *spindle1_settings_add (bool claim_ports)

View File

@@ -3,7 +3,7 @@
Part of grblHAL
Copyright (c) 2017-2025 Terje Io
Copyright (c) 2017-2026 Terje Io
Copyright (c) 2012-2015 Sungeun K. Jeon
Copyright (c) 2009-2011 Simen Svale Skogsrud
@@ -220,10 +220,18 @@ typedef union {
pwm_disable :1, // PWM spindle only
g92offset :1,
pwm_ramped :1, // PWM spindle only
unassigned :3;
ignore_delays :1, // PWM spindle only
unassigned :2;
};
} spindle_settings_flags_t;
#if N_SPINDLE == 1
#define PWM_SPINDLE_NO_DELAYS
#else
#define PWM_SPINDLE_NO_DELAYS ",Ignore on/off delays"
#endif
typedef union {
uint8_t value;
uint8_t mask;
@@ -348,8 +356,12 @@ typedef struct spindle_param {
spindle_state_t state;
override_t override_pct; //!< Spindle RPM override value in percent
spindle_css_data_t css; //!< Data used for Constant Surface Speed Mode (CSS) calculations, NULL if not in CSS mode.
bool ramp_up;
bool ramp_down;
struct {
uint8_t ramp_up :1,
ramp_down :1,
ignore_delays :1,
override_disable :1;
} option;
spindle_ptrs_t *hal;
} spindle_param_t;

View File

@@ -30,6 +30,8 @@
#include "state_machine.h"
#include "override.h"
extern void gc_tool_changed (void);
static void state_idle (uint_fast16_t new_state);
static void state_cycle (uint_fast16_t rt_exec);
static void state_await_hold (uint_fast16_t rt_exec);
@@ -456,16 +458,13 @@ static void state_cycle (uint_fast16_t rt_exec)
*/
FLASHMEM static void state_await_toolchanged (uint_fast16_t rt_exec)
{
if (rt_exec & EXEC_CYCLE_START) {
if (!gc_state.tool_change) {
if(rt_exec & EXEC_CYCLE_START) {
if(!gc_state.tool_change) {
if (hal.stream.suspend_read)
if(hal.stream.suspend_read)
hal.stream.suspend_read(false); // Tool change complete, restore "normal" stream input.
if(grbl.on_tool_changed)
grbl.on_tool_changed(gc_state.tool);
report_add_realtime(Report_Tool);
gc_tool_changed();
}
pending_state = gc_state.tool_change ? STATE_TOOL_CHANGE : STATE_IDLE;
state_set(STATE_IDLE);

View File

@@ -25,16 +25,15 @@
#include <stdbool.h>
#include <stdio.h>
#include "grbl.h"
#include "strutils.h"
#define CAPS(c) ((c >= 'a' && c <= 'z') ? c & 0x5F : c)
/*! \brief Case insensitive search for first occurrence of a string inside another.
\param s1 pointer to string to search.
\param s2 pointer to string to search for.
\returns pointer to found string or NULL if not found.
*/
char *stristr (const char *s1, const char *s2)
FLASHMEM char *stristr (const char *s1, const char *s2)
{
const char *s = s1, *p = s2, *r = NULL;
@@ -69,7 +68,7 @@ char *stristr (const char *s1, const char *s2)
\param len max. number of characters to compare.
\returns pointer to found string or NULL if not found.
*/
char *strnistr (const char *s1, const char *s2, size_t len)
FLASHMEM char *strnistr (const char *s1, const char *s2, size_t len)
{
size_t slen = len;
const char *s = s1, *p = s2, *r = NULL;
@@ -102,8 +101,9 @@ char *strnistr (const char *s1, const char *s2, size_t len)
return slen == 0 || *p == '\0' ? (char *)r : NULL;
}
// NOTE: ensure buf is large enough to hold concatenated strings!
char *strappend (char *buf, int argc, ...)
// NOTE: Ensure buf is large enough to hold concatenated strings!
// Do NOT use for several int/float conversions as these share the same underlying buffer!
FLASHMEM char *strappend (char *buf, int argc, ...)
{
char c, *s = buf, *arg;
@@ -123,10 +123,10 @@ char *strappend (char *buf, int argc, ...)
return buf;
}
uint32_t strnumentries (const char *s, const char delimiter)
FLASHMEM uint32_t strnumentries (const char *s, const char delimiter)
{
char *p = (char *)s;
uint32_t entries = *s ? 1 : 0;
uint32_t entries = (s && *s) ? 1 : 0;
while(entries && (p = strchr(p, delimiter))) {
p++;
@@ -136,7 +136,7 @@ uint32_t strnumentries (const char *s, const char delimiter)
return entries;
}
char *strgetentry (char *res, const char *s, uint32_t entry, const char delimiter)
FLASHMEM char *strgetentry (char *res, const char *s, uint32_t entry, const char delimiter)
{
char *e, *p = (char *)s;
@@ -163,7 +163,7 @@ char *strgetentry (char *res, const char *s, uint32_t entry, const char delimite
return res;
}
int32_t strlookup (const char *s1, const char *s2, const char delimiter)
FLASHMEM int32_t strlookup (const char *s1, const char *s2, const char delimiter)
{
bool found = false;
char *e, *p = (char *)s2;
@@ -188,7 +188,7 @@ int32_t strlookup (const char *s1, const char *s2, const char delimiter)
return found ? entry : -1;
}
bool strtotime (char *s, struct tm *time)
FLASHMEM bool strtotime (char *s, struct tm *time)
{
char c, *s1 = s;
uint_fast16_t idx = 0;
@@ -285,7 +285,7 @@ bool strtotime (char *s, struct tm *time)
return idx >= 5;
}
char *strtoisodt (struct tm *dt)
FLASHMEM char *strtoisodt (struct tm *dt)
{
static char buf[21];
@@ -295,9 +295,9 @@ char *strtoisodt (struct tm *dt)
return buf;
}
char *strtointernetdt (struct tm *dt)
FLASHMEM char *strtointernetdt (struct tm *dt)
{
static const char *month_table[12] = {
PROGMEM static const char *month_table[12] = {
"Jan",
"Feb",
"Mar",
@@ -312,7 +312,7 @@ char *strtointernetdt (struct tm *dt)
"Dec"
};
static const char *day_table[7] = {
PROGMEM static const char *day_table[7] = {
"Sun",
"Mon",
"Tue",

View File

@@ -115,23 +115,10 @@ FLASHMEM static void change_completed (void)
#endif
}
// Reset claimed HAL entry points and restore previous tool if needed on soft restart.
// Reset claimed HAL entry points on soft restart.
// Called from EXEC_RESET and EXEC_STOP handlers (via HAL).
FLASHMEM static void reset (void)
{
if(next_tool) { //TODO: move to gc_xxx() function?
// Restore previous tool if reset is during change
if(current_tool.tool_id != next_tool->tool_id) {
if(grbl.tool_table.n_tools)
memcpy(gc_state.tool, &current_tool, sizeof(tool_data_t));
else
memcpy(next_tool, &current_tool, sizeof(tool_data_t));
report_add_realtime(Report_Tool);
}
gc_state.tool_pending = gc_state.tool->tool_id;
next_tool = NULL;
}
change_completed();
driver_reset();
}