Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions drivers/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ rsource "ads101x/Kconfig"
rsource "bmx055/Kconfig"
rsource "fxos8700/Kconfig"
rsource "gp2y10xx/Kconfig"
rsource "inc_encoder/Kconfig"
rsource "hdc1000/Kconfig"
rsource "hm330x/Kconfig"
rsource "hsc/Kconfig"
Expand Down
38 changes: 38 additions & 0 deletions drivers/inc_encoder/Kconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: 2025 TU Dresden
# SPDX-License-Identifier: LGPL-2.1-only

config MODULE_INC_ENCODER
bool "Incremental Rotary Encoder"
depends on TEST_KCONFIG


menu "Incremental Rotary Encoder Driver"
depends on USEMODULE_INC_ENCODER

config INC_ENCODER_MAX_RPM
int "Maximum RPM"
default 210
help
Defines the maximum RPM the encoder is expected to handle.

config INC_ENCODER_GEAR_RED_RATIO
int "Gear Reduction Ratio (in tenths)"
default 204
help
Defines the gear reduction ratio. For example a gear reduction ratio
of 1:20.4 would result in a value of 204.

config INC_ENCODER_PPR
int "Pulses per Revolution"
default 13
help
Number of Rising Edges per Revolution.

config INC_ENCODER_HARDWARE_PERIOD_MS
int "RPM Calculation Period (in ms)"
depends on USEMODULE_INC_ENCODER_HARDWARE
default 200
help
Time period in milliseconds for RPM calculation.

endmenu # Incremental Encoder Driver
9 changes: 9 additions & 0 deletions drivers/inc_encoder/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ifneq (,$(filter inc_encoder_hardware,$(USEMODULE)))
DIRS += backends/hardware
endif

ifneq (,$(filter inc_encoder_software,$(USEMODULE)))
DIRS += backends/software
endif

include $(RIOTMAKE)/driver_with_saul.mk
10 changes: 10 additions & 0 deletions drivers/inc_encoder/Makefile.dep
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ifneq (,$(filter inc_encoder_hardware,$(USEMODULE)))
FEATURES_REQUIRED += periph_qdec
endif

ifneq (,$(filter inc_encoder_software,$(USEMODULE)))
FEATURES_REQUIRED += periph_gpio
FEATURES_REQUIRED += periph_gpio_irq
endif

USEMODULE += ztimer_usec
7 changes: 7 additions & 0 deletions drivers/inc_encoder/Makefile.include
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ifneq (1,$(words $(filter inc_encoder_%,$(USEMODULE))))
$(error "Please specify exactly one inc_encoder backend: \
inc_encoder_hardware or inc_encoder_software!")
endif

USEMODULE_INCLUDES_inc_encoder := $(LAST_MAKEFILEDIR)/include
USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_inc_encoder)
9 changes: 9 additions & 0 deletions drivers/inc_encoder/backends/hardware/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
MODULE := inc_encoder_hardware
BASE_MODULE := inc_encoder

SRC := inc_encoder_hardware.c

# enable submodules
SUBMODULES := 1

include $(RIOTBASE)/Makefile.base
135 changes: 135 additions & 0 deletions drivers/inc_encoder/backends/hardware/inc_encoder_hardware.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* SPDX-FileCopyrightText: 2025 TU Dresden
* SPDX-License-Identifier: LGPL-2.1-only
*/

/**
* @ingroup drivers_inc_encoder
* @{
*
* @file
* @brief Device driver implementation for a generic incremental rotary encoder
*
* @author Leonard Herbst <[email protected]>
*
* @}
*/

#include "inc_encoder.h"
#include "inc_encoder_params.h"
#include "inc_encoder_constants.h"

#include <errno.h>
#include "log.h"
#include "ztimer.h"
#include "time_units.h"

/* The maximum delta_count that does not cause an overflow in the RPM calculation */
#define DELTA_COUNT_MAX (INT32_MAX / (SEC_PER_MIN * MS_PER_SEC * GEAR_RED_RATIO_SCALE))

/* Maximum RPM before we accumulate more than DELTA_COUNT_MAX pulses per calculation period,
* which would cause an overflow. */
#define MAX_RPM ((DELTA_COUNT_MAX * SEC_PER_MIN * MS_PER_SEC * GEAR_RED_RATIO_SCALE) \
/ (CONFIG_INC_ENCODER_PPR \
* CONFIG_INC_ENCODER_GEAR_RED_RATIO \
* CONFIG_INC_ENCODER_HARDWARE_PERIOD_MS \
* 4))

#if (MAX_RPM < CONFIG_INC_ENCODER_MAX_RPM)
# error With the current configuration the RPM calculation can overflow. \
Please reduce the period, pulses per revolution, gear reduction ratio, or the max RPM.
#endif

/* Prototypes */
static bool _rpm_calc_timer_cb(void *arg);
static void _acc_overflow_cb(void *args);

/* Public API */
int inc_encoder_init(inc_encoder_t *dev, const inc_encoder_params_t *params)
{
dev->params = *params;

if (qdec_init(dev->params.qdec_dev, QDEC_X4, _acc_overflow_cb, (void *) dev)) {
LOG_ERROR("[inc_encoder] Qdec mode not supported!\n");
return -EINVAL;
}

dev->extended_count = 0;
dev->prev_count = 0;
dev->leftover_count = 0;
dev->last_rpm = 0;

/* Task to periodically calculate RPM */
ztimer_periodic_init(ZTIMER_USEC, &dev->rpm_timer, _rpm_calc_timer_cb, (void *) dev,
CONFIG_INC_ENCODER_HARDWARE_PERIOD_MS * US_PER_MS);

ztimer_periodic_start(&dev->rpm_timer);

return 0;
}

int inc_encoder_read_rpm(inc_encoder_t *dev, int32_t *rpm)
{
int irq_state = irq_disable();
*rpm = dev->last_rpm;
irq_restore(irq_state);
return 0;
}

int inc_encoder_read_reset_ceti_revs(inc_encoder_t *dev, int32_t *pulse_counter)
{
int32_t total_count;
int32_t delta_count;

int irq_state = irq_disable();
total_count = qdec_read_and_reset(dev->params.qdec_dev);
total_count += dev->extended_count;
delta_count = total_count - dev->prev_count;

/* We reset the counter but we need to keep the number
* of pulses since last read for the RPM calculation */
dev->leftover_count = delta_count;
dev->extended_count = 0;
dev->prev_count = 0;
irq_restore(irq_state);

/* The 4X mode counts all falling and rising edges */
*pulse_counter = (int32_t) total_count / 4;

*pulse_counter *= UNIT_SCALE * GEAR_RED_RATIO_SCALE;
*pulse_counter /= CONFIG_INC_ENCODER_PPR;
*pulse_counter /= CONFIG_INC_ENCODER_GEAR_RED_RATIO;
return 0;
}

/* Private API */
static bool _rpm_calc_timer_cb(void *arg)
{
inc_encoder_t *dev = (inc_encoder_t *) arg;
int32_t delta_count;
int32_t rpm;
int32_t total_count;

total_count = dev->extended_count + qdec_read(dev->params.qdec_dev);
delta_count = total_count - dev->prev_count;
if (dev->leftover_count != 0) {
/* Add leftover count from last reset */
delta_count += dev->leftover_count;
dev->leftover_count = 0;
}
dev->prev_count = total_count;

rpm = (int32_t)(SEC_PER_MIN * MS_PER_SEC * GEAR_RED_RATIO_SCALE * delta_count) /
(int32_t)(CONFIG_INC_ENCODER_PPR * CONFIG_INC_ENCODER_GEAR_RED_RATIO
* CONFIG_INC_ENCODER_HARDWARE_PERIOD_MS * 4); /* 4X mode counts all edges */

dev->last_rpm = rpm;
return true;
}

static void _acc_overflow_cb(void *args)
{
inc_encoder_t *dev = (inc_encoder_t *) args;

dev->extended_count += qdec_read_and_reset(dev->params.qdec_dev);
}
9 changes: 9 additions & 0 deletions drivers/inc_encoder/backends/software/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
MODULE := inc_encoder_software
BASE_MODULE := inc_encoder

SRC := inc_encoder_software.c

# enable submodules
SUBMODULES := 1

include $(RIOTBASE)/Makefile.base
160 changes: 160 additions & 0 deletions drivers/inc_encoder/backends/software/inc_encoder_software.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* SPDX-FileCopyrightText: 2025 TU Dresden
* SPDX-License-Identifier: LGPL-2.1-only
*/

/**
* @ingroup drivers_inc_encoder
* @{
*
* @file
* @brief Device driver implementation for a generic incremental rotary encoder
*
* @author Leonard Herbst <[email protected]>
*
* @}
*/

#include "inc_encoder.h"
#include "inc_encoder_params.h"
#include "inc_encoder_constants.h"

#include <errno.h>
#include "log.h"
#include "time_units.h"

/* If delta_t exceeds this threshold, the calculated RPM will be less than one
* and will be truncated to zero.
* When delta_t is larger than this threshold, we directly return zero
* and prevent potential overflows in the RPM calculation.
*
* An overflow would occur when delta_t > INT32_MAX / (PPR * GEAR_RED_RATIO),
* but our threshold is always lower than that because:
*
* INT32_MAX > SEC_PER_MIN * US_PER_SEC * GEAR_RED_RATIO_SCALE
*/
#define DELTA_T_THRESHOLD ((SEC_PER_MIN * US_PER_SEC * GEAR_RED_RATIO_SCALE) \
/ (CONFIG_INC_ENCODER_PPR * CONFIG_INC_ENCODER_GEAR_RED_RATIO))

/* Prototypes */
static void _pulse_cb(void *arg);
static bool _read_delta_t_direction(inc_encoder_t *dev, uint32_t *delta_t, bool *cw);

/* Public API */

int inc_encoder_init(inc_encoder_t *dev, const inc_encoder_params_t *params)
{
dev->params = *params;
if (gpio_init(dev->params.direction, GPIO_IN)) {
LOG_ERROR("[inc_encoder] Failed configuring the direction pin as an input!\n");
return -EIO;
}

dev->delta_t = 0;
dev->pulse_counter = 0;
dev->cw = false;
dev->stale = true;
dev->last_read_time = ztimer_now(ZTIMER_USEC);

if (gpio_init_int(dev->params.interrupt, GPIO_IN, GPIO_RISING, _pulse_cb, (void *) dev)) {
LOG_ERROR("[inc_encoder] Failed configuring the interrupt pin!\n");
return -EIO;
}

return 0;
}

int inc_encoder_read_rpm(inc_encoder_t *dev, int32_t *rpm)
{
uint32_t delta_t;
bool cw;
if (!_read_delta_t_direction(dev, &delta_t, &cw) || (delta_t >= DELTA_T_THRESHOLD)) {
*rpm = 0;
return 0;
}

/* delta_t represents the number of microseconds since the last pulse.
* Invert and divide by the number of microseconds per minute
* to obtain the RPM. Apply scaling factors like gear reduction
* or pulses per revolution. */
*rpm = SEC_PER_MIN * US_PER_SEC * GEAR_RED_RATIO_SCALE
/ (delta_t * CONFIG_INC_ENCODER_PPR * CONFIG_INC_ENCODER_GEAR_RED_RATIO);
if (!cw) {
*rpm *= -1;
}

return 0;
}

int inc_encoder_read_reset_ceti_revs(inc_encoder_t *dev, int32_t *pulse_counter)
{
int irq_state = irq_disable();
*pulse_counter = dev->pulse_counter;
dev->pulse_counter = 0;
irq_restore(irq_state);

*pulse_counter *= UNIT_SCALE * GEAR_RED_RATIO_SCALE;
*pulse_counter /= CONFIG_INC_ENCODER_PPR;
*pulse_counter /= CONFIG_INC_ENCODER_GEAR_RED_RATIO;
return 0;
}

/* Private API */

/* Triggered on the rising edge of a pulse */
static void _pulse_cb(void *arg)
{
inc_encoder_t *dev = (inc_encoder_t *) arg;
uint32_t now = ztimer_now(ZTIMER_USEC);

/* Reading the shifted phase: high -> cw, low -> ccw */
dev->cw = gpio_read(dev->params.direction);

if (now < dev->last_read_time) {
dev->delta_t = UINT32_MAX - dev->last_read_time + now + 1;
}
else {
dev->delta_t = now - dev->last_read_time;
}

dev->last_read_time = now;
dev->pulse_counter += dev->cw ? 1 : -1;
/* data is no longer stale */
dev->stale= false;
}

static bool _read_delta_t_direction(inc_encoder_t *dev, uint32_t *delta_t, bool *cw)
{
uint32_t now;
uint32_t pulse_age;
int irq_state = irq_disable();

if (dev->stale) {
/* Rotation stopped */
irq_restore(irq_state);
return false;
}
now = ztimer_now(ZTIMER_USEC);

/* Handle potential overflows */
if (now < dev->last_read_time) {
pulse_age = UINT32_MAX - dev->last_read_time + now + 1;
}
else {
pulse_age = now - dev->last_read_time;
}

if (pulse_age >= dev->delta_t) {
/* Data is stale if the time elapsed since the last pulse
* is longer than delta_t */
*delta_t = pulse_age;
dev->stale= true;
}
else {
*delta_t = dev->delta_t;
}
*cw = dev->cw;

irq_restore(irq_state);
return true;
}
Loading
Loading