Files
grblHAL/encoders.h
Terje Io 949fa53c42 Added some reentry locks.
Second step in refactoring encoder HAL/API.
2026-03-03 22:34:30 +01:00

338 lines
9.6 KiB
C

/*
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 encoder pointer to a \a encoder_t struct.
\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_t *encoder, 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 {
void *hw;
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_
// Interrupt driven Quadrature Encoder Interface - static code for driver/plugin use
#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;
} iqei_t;
static iqei_t iqei = {
.port_a = IOPORT_UNASSIGNED,
.port_b = IOPORT_UNASSIGNED,
.port_select = IOPORT_UNASSIGNED,
.settings.dbl_click_window = 500,
.encoder.caps.bidirectional = On
};
static void iqei_select_irq (uint8_t port, bool high);
static void iqei_post_event (void *data)
{
iqei.event.events |= ((encoder_event_t *)data)->events;
iqei.on_event(&iqei.encoder, &iqei.event, iqei.context);
}
static void iqei_reset (encoder_t *encoder)
{
iqei.vel_timeout = 0;
iqei.dir = QEI_DirUnknown;
iqei.data.position = iqei.vel_count = 0;
iqei.vel_timestamp = hal.get_elapsed_ticks();
iqei.vel_timeout = iqei.settings.vel_timeout;
}
static bool iqei_configure (encoder_t *encoder, encoder_cfg_t *settings)
{
if(iqei.vel_timeout != settings->vel_timeout)
iqei.vel_timestamp = hal.get_elapsed_ticks();
memcpy(&iqei.settings, settings, sizeof(encoder_cfg_t));
return true;
}
static encoder_data_t *iqei_get_data (encoder_t *encoder)
{
return &iqei.data;
}
static void iqei_poll (void *data)
{
if(iqei.vel_timeout && !(--iqei.vel_timeout)) {
uint32_t time = hal.get_elapsed_ticks();
iqei.data.velocity = abs(iqei.data.position - iqei.vel_count) * 1000 / (time - iqei.vel_timestamp);
iqei.vel_timestamp = time;
iqei.vel_timeout = iqei.settings.vel_timeout;
if((iqei.event.position_changed = !iqei.dbl_click_timeout || iqei.data.velocity == 0))
iqei.on_event(&iqei.encoder, &iqei.event, iqei.context);
iqei.vel_count = iqei.data.position;
}
if(iqei.dbl_click_timeout && !(--iqei.dbl_click_timeout)) {
iqei.event.click = On;
iqei.on_event(&iqei.encoder, &iqei.event, iqei.context);
}
}
static void iqei_ab_irq (uint8_t port, bool high)
{
PROGMEM static const uint8_t encoder_valid_state[] = {0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0};
PROGMEM static const encoder_event_t dir_changed = { .direction_changed = On, .position_changed = On };
static qei_state_t state = {0};
if(port == iqei.port_a)
state.a = high;
else
state.b = high;
uint_fast8_t idx = (((iqei.state << 2) & 0x0F) | state.pins);
if(encoder_valid_state[idx] ) {
// int32_t count = iqei.count;
iqei.state = ((iqei.state << 4) | idx) & 0xFF;
if(iqei.state == 0x42 || iqei.state == 0xD4 || iqei.state == 0x2B || iqei.state == 0xBD) {
iqei.data.position--;
if(iqei.vel_timeout == 0 || iqei.dir == QEI_DirCW) {
iqei.dir = QEI_DirCCW;
task_add_immediate(iqei_post_event, &dir_changed);
}
} else if(iqei.state == 0x81 || iqei.state == 0x17 || iqei.state == 0xE8 || iqei.state == 0x7E) {
iqei.data.position++;
if(iqei.vel_timeout == 0 || iqei.dir == QEI_DirCCW) {
iqei.dir = QEI_DirCW;
task_add_immediate(iqei_post_event, &dir_changed);
}
}
}
}
static void iqei_select (void *data)
{
static uint8_t clicks = 0;
if(!iqei.dbl_click_timeout) {
clicks = 1;
iqei.dbl_click_timeout = iqei.settings.dbl_click_window;
} else if(iqei.dbl_click_timeout < iqei.settings.dbl_click_window && ++clicks == 2) {
iqei.dbl_click_timeout = 0;
iqei.event.dbl_click = On;
iqei.on_event(&iqei.encoder, &iqei.event, iqei.context);
}
}
static void iqei_select_irq (uint8_t port, bool high)
{
static bool lock = false;
if(high || lock)
return;
// lock = true;
task_add_immediate(iqei_select, NULL);
// lock = false;
}
static bool iqei_claim (encoder_t *encoder, encoder_on_event_ptr event_handler, void *context)
{
if(event_handler == NULL || iqei.on_event)
return false;
iqei.context = context;
iqei.on_event = event_handler;
iqei.encoder.reset = iqei_reset;
iqei.encoder.get_data = iqei_get_data;
iqei.encoder.configure = iqei_configure;
if(iqei.port_b != IOPORT_UNASSIGNED) {
ioport_enable_irq(iqei.port_a, IRQ_Mode_Change, iqei_ab_irq);
ioport_enable_irq(iqei.port_b, IRQ_Mode_Change, iqei_ab_irq);
}
if(iqei.port_select != IOPORT_UNASSIGNED)
ioport_enable_irq(iqei.port_select, IRQ_Mode_Change, iqei_select_irq);
task_add_systick(iqei_poll, NULL);
return true;
}
static inline void _encoder_pin_claimed (uint8_t port, xbar_t *pin)
{
switch(pin->function) {
case Input_QEI_A:
iqei.port_a = port;
break;
case Input_QEI_B:
iqei.port_b = port;
iqei.encoder.claim = iqei_claim;
if(iqei.port_a != IOPORT_UNASSIGNED)
encoder_register(&iqei.encoder);
break;
case Input_QEI_Select:
iqei.port_select = port;
iqei.encoder.caps.select = On;
if(pin->config) {
gpio_in_config_t config = {
.debounce = On,
.pull_mode = PullMode_Up
};
pin->config(pin, &config, false);
}
break;
default: break;
}
}
#endif