diff --git a/drivers/Kconfig b/drivers/Kconfig index a2f3ee786768..842defd04474 100644 --- a/drivers/Kconfig +++ b/drivers/Kconfig @@ -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" diff --git a/drivers/inc_encoder/Kconfig b/drivers/inc_encoder/Kconfig new file mode 100644 index 000000000000..a1ea46e7492a --- /dev/null +++ b/drivers/inc_encoder/Kconfig @@ -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 diff --git a/drivers/inc_encoder/Makefile b/drivers/inc_encoder/Makefile new file mode 100644 index 000000000000..dba907dfc50c --- /dev/null +++ b/drivers/inc_encoder/Makefile @@ -0,0 +1,13 @@ +SUBMODULES := 1 + +# Since we have submodules we need to manually handle saul instead of +# including driver_with_saul.mk +MODULE ?= $(shell basename $(CURDIR)) +SAUL_INTERFACE ?= $(MODULE)_saul.c + +# only include _saul.c if saul module is used +ifneq (,$(filter saul,$(USEMODULE))) + SRC += $(SAUL_INTERFACE) +endif + +include $(RIOTBASE)/Makefile.base diff --git a/drivers/inc_encoder/Makefile.dep b/drivers/inc_encoder/Makefile.dep new file mode 100644 index 000000000000..ecda619f258c --- /dev/null +++ b/drivers/inc_encoder/Makefile.dep @@ -0,0 +1,10 @@ +ifneq (,$(filter inc_encoder_hardware,$(USEMODULE))) + FEATURES_REQUIRED += periph_qdec + USEMODULE += ztimer_msec +endif + +ifneq (,$(filter inc_encoder_software,$(USEMODULE))) + FEATURES_REQUIRED += periph_gpio + FEATURES_REQUIRED += periph_gpio_irq + USEMODULE += ztimer_usec +endif \ No newline at end of file diff --git a/drivers/inc_encoder/Makefile.include b/drivers/inc_encoder/Makefile.include new file mode 100644 index 000000000000..8c97e567af14 --- /dev/null +++ b/drivers/inc_encoder/Makefile.include @@ -0,0 +1,10 @@ +PSEUDOMODULES += inc_encoder_hardware +PSEUDOMODULES += inc_encoder_software + +ifneq (1,$(words $(filter inc_encoder_hardware inc_encoder_software,$(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) diff --git a/drivers/inc_encoder/hardware.c b/drivers/inc_encoder/hardware.c new file mode 100644 index 000000000000..f7a1bdc52944 --- /dev/null +++ b/drivers/inc_encoder/hardware.c @@ -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 + * + * @} + */ + +#include "inc_encoder.h" +#include "inc_encoder_params.h" +#include "inc_encoder_constants.h" + +#include +#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_MSEC, &dev->rpm_timer, _rpm_calc_timer_cb, (void *) dev, + CONFIG_INC_ENCODER_HARDWARE_PERIOD_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_milli_revs(inc_encoder_t *dev, int32_t *rev_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 */ + *rev_counter = (int32_t) total_count / 4; + + *rev_counter *= UNIT_SCALE * GEAR_RED_RATIO_SCALE; + *rev_counter /= CONFIG_INC_ENCODER_PPR; + *rev_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); +} diff --git a/drivers/inc_encoder/inc_encoder_saul.c b/drivers/inc_encoder/inc_encoder_saul.c new file mode 100644 index 000000000000..f54bfbc880c4 --- /dev/null +++ b/drivers/inc_encoder/inc_encoder_saul.c @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2025 TU Dresden + * SPDX-License-Identifier: LGPL-2.1-only + */ + +/** + * @ingroup drivers_inc_encoder + * @{ + * + * @file + * @brief Generic incremental rotary encoder adaption to the RIOT actuator/sensor interface + * + * @author Leonard Herbst + * + * @} + */ + +#include +#include + +#include "saul.h" +#include "inc_encoder.h" + +static int read_rpm(const void *dev, phydat_t *res) +{ + inc_encoder_t *d = (inc_encoder_t *) dev; + int32_t rpm; + if (inc_encoder_read_rpm(d, &rpm)) { + /* Read failure */ + return -ECANCELED; + } + res->val[0] = (uint16_t) rpm; + res->unit = UNIT_RPM; + res->scale = 0; + return 1; +} + +static int read_reset_rev_counter(const void *dev, phydat_t *res) +{ + inc_encoder_t *d = (inc_encoder_t *)dev; + int32_t rev_counter; + if (inc_encoder_read_reset_milli_revs(d, &rev_counter)) { + /* Read failure */ + return -ECANCELED; + } + res->val[0] = (int16_t) rev_counter; + res->unit = UNIT_CTS; + res->scale = -3; /* millirevolutions */ + return 1; +} + +const saul_driver_t inc_encoder_rpm_saul_driver = { + .read = read_rpm, + .write = saul_write_notsup, + .type = SAUL_SENSE_SPEED, +}; + +const saul_driver_t inc_encoder_rev_count_saul_driver = { + .read = read_reset_rev_counter, + .write = saul_write_notsup, + .type = SAUL_SENSE_COUNT, +}; diff --git a/drivers/inc_encoder/include/inc_encoder_constants.h b/drivers/inc_encoder/include/inc_encoder_constants.h new file mode 100644 index 000000000000..328a8e4396de --- /dev/null +++ b/drivers/inc_encoder/include/inc_encoder_constants.h @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2025 TU Dresden + * SPDX-License-Identifier: LGPL-2.1-only + */ + +#pragma once + +/** + * @ingroup drivers_inc_encoder + * @{ + * + * @file + * @brief Constants used in the incremental rotary encoder driver + * + * @author Leonard Herbst + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Scaling factor to apply to adjust for the gear reduction ratio being in tenths. + */ +#define GEAR_RED_RATIO_SCALE 10 + +/** + * @brief Scaling factor to convert revolutions per minute to millirevolutions per minute. + */ +#define UNIT_SCALE 1000 + +#ifdef __cplusplus +} +#endif +/** @} */ diff --git a/drivers/inc_encoder/include/inc_encoder_params.h b/drivers/inc_encoder/include/inc_encoder_params.h new file mode 100644 index 000000000000..641cd9b657b3 --- /dev/null +++ b/drivers/inc_encoder/include/inc_encoder_params.h @@ -0,0 +1,123 @@ +/* + * SPDX-FileCopyrightText: 2025 TU Dresden + * SPDX-License-Identifier: LGPL-2.1-only + */ + +#pragma once + +/** + * @ingroup drivers_inc_encoder + * + * @{ + * @file + * @brief Default configuration for a generic incremental rotary encoder. + * + * @author Leonard Herbst + */ + +#include "board.h" +#include "inc_encoder.h" +#include "saul_reg.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Default gear reduction ratio + */ +#ifndef CONFIG_INC_ENCODER_GEAR_RED_RATIO +# define CONFIG_INC_ENCODER_GEAR_RED_RATIO 204 +#endif + +/** + * @brief Default number of pulses per revolution + */ +#ifndef CONFIG_INC_ENCODER_PPR +# define CONFIG_INC_ENCODER_PPR 13 +#endif + +/** + * @brief Default number of pulses per revolution + */ +#if IS_USED(MODULE_INC_ENCODER_HARDWARE) +# ifndef CONFIG_INC_ENCODER_HARDWARE_PERIOD_MS +# define CONFIG_INC_ENCODER_HARDWARE_PERIOD_MS 200 +# endif +#endif + +/** + * @name default configuration parameters for a generic incremental rotary encoder + * @{ + */ +#if IS_USED(MODULE_INC_ENCODER_SOFTWARE) +/** + * @brief Default pin of the first phase used to trigger the interrupt for software decoding + */ +# ifndef INC_ENCODER_INTERRUPT +# define INC_ENCODER_INTERRUPT GPIO_PIN(1, 10) +# endif + +/** + * @brief Default pin of the second (shifted) phase used to determine + * the direction for software decoding + */ +# ifndef INC_ENCODER_DIRECTION +# define INC_ENCODER_DIRECTION GPIO_PIN(1, 11) +# endif +#endif /* IS_USED(MODULE_INC_ENCODER_SOFTWARE) */ + +#if IS_USED(MODULE_INC_ENCODER_HARDWARE) +/** + * @brief Default QDEC device used for hardware decoding + */ +#ifndef INC_ENCODER_QDEC_DEV +# define INC_ENCODER_QDEC_DEV QDEC_DEV(0) +#endif +#endif /* IS_USED(MODULE_INC_ENCODER_HARDWARE) */ + +/** + * @brief Default parameters + */ +#if IS_USED(MODULE_INC_ENCODER_SOFTWARE) +# ifndef INC_ENCODER_PARAMS +# define INC_ENCODER_PARAMS { .interrupt = INC_ENCODER_INTERRUPT, \ + .direction = INC_ENCODER_DIRECTION } +# endif +#elif IS_USED(MODULE_INC_ENCODER_HARDWARE) + +# ifndef INC_ENCODER_PARAMS +# define INC_ENCODER_PARAMS { .qdec_dev = INC_ENCODER_QDEC_DEV } +# endif +#endif +/**@}*/ + +/** + * @brief SAUL info for the RPM and pulse count driver + */ +#ifndef INC_ENCODER_SAUL_INFO +# define INC_ENCODER_SAUL_INFO { { .name = "Incremental Rotary Encoder RPM Sensor" }, \ + { .name = "Incremental Rotary Encoder Pulse Count Sensor" } } +#endif + +/** + * @brief Incremental rotary encoder configuration + */ +static const inc_encoder_params_t inc_encoder_params[] = +{ + INC_ENCODER_PARAMS +}; + +/** + * @brief Additional meta information to keep in the SAUL registry + */ +static const saul_reg_info_t inc_encoder_saul_info[][2] = +{ + INC_ENCODER_SAUL_INFO +}; + +#ifdef __cplusplus +} +#endif + +/** @} */ diff --git a/drivers/inc_encoder/software.c b/drivers/inc_encoder/software.c new file mode 100644 index 000000000000..53bcdd3a627d --- /dev/null +++ b/drivers/inc_encoder/software.c @@ -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 + * + * @} + */ + +#include "inc_encoder.h" +#include "inc_encoder_params.h" +#include "inc_encoder_constants.h" + +#include +#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_milli_revs(inc_encoder_t *dev, int32_t *rev_counter) +{ + int irq_state = irq_disable(); + *rev_counter = dev->pulse_counter; + dev->pulse_counter = 0; + irq_restore(irq_state); + + *rev_counter *= UNIT_SCALE * GEAR_RED_RATIO_SCALE; + *rev_counter /= CONFIG_INC_ENCODER_PPR; + *rev_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; +} diff --git a/drivers/include/inc_encoder.h b/drivers/include/inc_encoder.h new file mode 100644 index 000000000000..49e3b40f6596 --- /dev/null +++ b/drivers/include/inc_encoder.h @@ -0,0 +1,169 @@ +/* + * SPDX-FileCopyrightText: 2025 TU Dresden + * SPDX-License-Identifier: LGPL-2.1-only + */ + +#pragma once + +/** + * @defgroup drivers_inc_encoder Generic Incremental Rotary Encoder + * @ingroup drivers_sensors + * @brief Generic incremental rotary encoder to measur RPM and rotation count. + * + * ## Description + * This is a driver for a generic incremental rotary encoder. These sensors are most often + * used with motors. + * An example of such an encoder can be found [here](https://www.dfrobot.com/product-1617.html) + * + * The sensor generates a fixed number of pulses per rotation on two output pins. + * These signals are phase-shifted by 90 degrees, enabling the detection of rotation direction. + * + * These encoders typically have four wires: + * - GNR (Ground) + * - VCC (Power) + * - Phase A + * - Phase B + * + * Phase A should be connected to the pin configured as the interrupt pin or the QDEC A pin, + * while Phase B (the shifted signal) should be connected to the direction pin or QDEC B pin. + * If the Phase A and Phase B connections are swapped, + * the detected rotation direction will be reversed. + * + * The driver provides functions to read the current RPM + * and the total number of revolutions since the last measurement. The number of revolutions + * is provided in millirevolutions (thousandths of a full revolution) to allow for higher precision. + * + * Configuration options are available via Kconfig to specify + * the number of pulses per rotation, the maximum RPM, and the gear reduction ratio (in tenths). + * + * ## Backends + * This driver supports two backends for reading the encoder signals: + * - Hardware accelerated using the QDEC peripheral + * - Software based using GPIO interrupts + * + * To use either of the backends add the corresponding module to your application Makefile: + * ``` + * USEMODULE += inc_encoder_hardware + * ``` + * or + * ``` + * USEMODULE += inc_encoder_software + * ``` + * + * ### GPIO Interrupt Backend + * The GPIO interrupt backend uses interrupts to count the number of rising edges on Phase A and + * determines the rotation direction based on the state of Phase B. + * The RPM calculation is done based on the delta time between the last two rising edges. + * If only one phase is connected, the driver will still count the pulses, + * but there will be no direction detection. + * + * ### QDEC Backend + * The QDEC backend uses the microcontroller's Quadrature Decoder peripheral to handle the counting + * and direction detection in hardware. + * The RPM calculation is done periodically based on the + * pulse count provided by the QDEC peripheral. + * If only one phase is connected, the QDEC peripheral will not count any pulses. + * The period for RPM calculation can be configured via Kconfig. + * + * ## SAUL Interface + * This driver implements the SAUL sensor interface. + * It provides two SAUL devices: + * - One for reading the current RPM + * - One for reading the total number of revolutions since the last read. + * + * @note After 327 revolutions without reading and resetting + * the revolution counter, the `phydat` value will overflow. + * Use the regular driver interface instead of SAUL if necessary. + * + * @{ + * + * @file + * + * @author Leonard Herbst + */ + +#include + +#include "periph/gpio.h" +#include "irq.h" +#include "periph/qdec.h" +#include "ztimer.h" +#include "ztimer/periodic.h" +#include "kernel_defines.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Device initialization parameters + */ +typedef struct { +#if IS_USED(MODULE_INC_ENCODER_HARDWARE) + qdec_t qdec_dev; /**< QDEC device used for hardware decoding */ +#elif IS_USED(MODULE_INC_ENCODER_SOFTWARE) + gpio_t interrupt; /**< Interrupt pin (first phase) */ + gpio_t direction; /**< Pin used to determine the direction (shifted phase) */ +#endif +} inc_encoder_params_t; + +/** + * @brief Device descriptor for the driver + */ +typedef struct { + inc_encoder_params_t params; /**< configuration parameters */ +#if IS_USED(MODULE_INC_ENCODER_SOFTWARE) + uint32_t delta_t; /**< time delta since the last pulse */ + int32_t pulse_counter; /**< number of pulses since last read */ + uint32_t last_read_time; /**< time of the last read */ + bool cw; /**< clock wise rotation */ + bool stale; /**< indicates that there is no new data to be read */ +#elif IS_USED(MODULE_INC_ENCODER_HARDWARE) + int32_t extended_count; /**< accumulated count of pulse overflows */ + int32_t prev_count; /**< number of pulses at last RPM calculation */ + int32_t leftover_count; /**< leftover from last reset */ + int32_t last_rpm; /**< last calculated RPM value */ + ztimer_periodic_t rpm_timer; /**< timer used to periodically calculate RPM */ +#endif +} inc_encoder_t; + +/** + * @brief Initialize the given device + * + * @param[in, out] dev Device descriptor of incremental rotary encoder + * @param[in] params Initialization parameters + * + * @retval 0 on success + * @retval -EIO on failure setting up the pins + * @retval -EINVAL on invalid qdec configuration + */ +int inc_encoder_init(inc_encoder_t *dev, const inc_encoder_params_t *params); + +/** + * @brief Read the current RPM of the motor. + * + * @param[in] dev Device descriptor of incremental rotary encoder + * @param[out] rpm Revolutions per minute. + * Negative RPM responds to counter clock wise rotation. + * + * @return 0 on success + */ +int inc_encoder_read_rpm(inc_encoder_t *dev, int32_t *rpm); + +/** + * @brief Read and reset number of revolutions since the last readout in + * thousands of a full revolution. + * + * @param[in] dev Device descriptor of incremental rotary encoder + * @param[out] rev_counter Number of millirevolutions since the last read. + * Negative revolutions signal counter clock wise rotations. + * + * @return 0 on success + */ +int inc_encoder_read_reset_milli_revs(inc_encoder_t *dev, int32_t *rev_counter); + +#ifdef __cplusplus +} +#endif + +/** @} */ diff --git a/drivers/include/saul.h b/drivers/include/saul.h index 824b45f4f266..cc8f71508470 100644 --- a/drivers/include/saul.h +++ b/drivers/include/saul.h @@ -140,6 +140,7 @@ enum { SAUL_SENSE_ID_PH, /**< sensor: pH */ SAUL_SENSE_ID_POWER, /**< sensor: power */ SAUL_SENSE_ID_SIZE, /**< sensor: size */ + SAUL_SENSE_ID_SPEED, /**< sensor: speed */ SAUL_SENSE_NUMOF /**< Number of actuators supported */ /* Extend this list as needed, but keep SAUL_SENSE_ID_ANY the first and * SAUL_SENSE_NUMOF the last entry @@ -223,6 +224,8 @@ enum { SAUL_SENSE_POWER = SAUL_CAT_SENSE | SAUL_SENSE_ID_POWER, /** sensor: size */ SAUL_SENSE_SIZE = SAUL_CAT_SENSE | SAUL_SENSE_ID_SIZE, + /** sensor: speed */ + SAUL_SENSE_SPEED = SAUL_CAT_SENSE | SAUL_SENSE_ID_SPEED, /** any device - wildcard */ SAUL_CLASS_ANY = 0xff /* extend this list as needed... */ diff --git a/drivers/saul/init_devs/auto_init_inc_encoder.c b/drivers/saul/init_devs/auto_init_inc_encoder.c new file mode 100644 index 000000000000..c88341ea62fd --- /dev/null +++ b/drivers/saul/init_devs/auto_init_inc_encoder.c @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2025 TU Dresden + * SPDX-License-Identifier: LGPL-2.1-only + */ + +/** + * @ingroup drivers_inc_encoder + * @{ + * + * @file + * @brief SAUL adaption for generic incremental rotary encoder + * to measure the number as well as rate of rotation. + * + * @author Leonard Herbst + * @} + */ + +#include "log.h" +#include "saul_reg.h" +#include "inc_encoder_params.h" + +/** + * @brief Define the number of configured incremental rotary encoders + */ +#define INC_ENCODER_NUM ARRAY_SIZE(inc_encoder_params) + +/** + * @brief Memory for the device descriptors + */ +static inc_encoder_t inc_encoder_devs[INC_ENCODER_NUM]; + +/** + * @brief Memory for the SAUL registry entries + */ +static saul_reg_t saul_entries[INC_ENCODER_NUM * 2]; + +extern saul_driver_t inc_encoder_rpm_saul_driver; +extern saul_driver_t inc_encoder_rev_count_saul_driver; + +void auto_init_inc_encoder(void) +{ + for (unsigned i = 0; i < INC_ENCODER_NUM; i++) { + LOG_DEBUG("[auto_init_saul] initializing inc_encoder #%u\n", i); + + int res = inc_encoder_init(&inc_encoder_devs[i], &inc_encoder_params[i]); + if (res != 0) { + LOG_ERROR("[auto_init_saul] error initializing inc_encoder #%u\n", i); + continue; + } + + /* RPM */ + saul_entries[(i * 2)].dev = &(inc_encoder_devs[i]); + saul_entries[(i * 2)].name = inc_encoder_saul_info[i][0].name; + saul_entries[(i * 2)].driver = &inc_encoder_rpm_saul_driver; + + /* Pulse Count */ + saul_entries[(i * 2) + 1].dev = &(inc_encoder_devs[i]); + saul_entries[(i * 2) + 1].name = inc_encoder_saul_info[i][1].name; + saul_entries[(i * 2) + 1].driver = &inc_encoder_rev_count_saul_driver; + + saul_reg_add(&(saul_entries[(i * 2)])); + saul_reg_add(&(saul_entries[(i * 2) + 1])); + } +} diff --git a/drivers/saul/init_devs/init.c b/drivers/saul/init_devs/init.c index c6cdad13db6b..c91ac0c8cf3b 100644 --- a/drivers/saul/init_devs/init.c +++ b/drivers/saul/init_devs/init.c @@ -123,6 +123,10 @@ void saul_init_devs(void) extern void auto_init_grove_ledbar(void); auto_init_grove_ledbar(); } + if (IS_USED(MODULE_INC_ENCODER)) { + extern void auto_init_inc_encoder(void); + auto_init_inc_encoder(); + } if (IS_USED(MODULE_HMC5883L)) { extern void auto_init_hmc5883l(void); auto_init_hmc5883l(); diff --git a/drivers/saul/saul_str.c b/drivers/saul/saul_str.c index d9ea6abe913b..a33273e9c552 100644 --- a/drivers/saul/saul_str.c +++ b/drivers/saul/saul_str.c @@ -71,6 +71,7 @@ static FLASH_ATTR const char _sense_voltage[] = "SENSE_VOLTAGE"; static FLASH_ATTR const char _sense_ph[] = "SENSE_PH"; static FLASH_ATTR const char _sense_power[] = "SENSE_POWER"; static FLASH_ATTR const char _sense_size[] = "SENSE_SIZE"; +static FLASH_ATTR const char _sense_speed[] = "SENSE_SPEED"; static FLASH_ATTR const char * FLASH_ATTR const sensors[] = { [SAUL_SENSE_ID_ANY] = _sense_any, @@ -102,6 +103,7 @@ static FLASH_ATTR const char * FLASH_ATTR const sensors[] = { [SAUL_SENSE_ID_PH] = _sense_ph, [SAUL_SENSE_ID_POWER] = _sense_power, [SAUL_SENSE_ID_SIZE] = _sense_size, + [SAUL_SENSE_ID_SPEED] = _sense_speed, }; static FLASH_ATTR const char _class_undef[] = "CLASS_UNDEF"; diff --git a/sys/include/phydat.h b/sys/include/phydat.h index 166a95b7ca01..c2a895ac86c6 100644 --- a/sys/include/phydat.h +++ b/sys/include/phydat.h @@ -91,6 +91,7 @@ enum { UNIT_G_FORCE, /**< gravitational force equivalent */ UNIT_G = UNIT_G_FORCE, /**< @deprecated, use UNIT_G_FORCE instead */ UNIT_DPS, /**< degree per second */ + UNIT_RPM, /**< revolutions per minute */ /* weight */ UNIT_GRAM, /**< grams - not using the SI unit (kg) here to make scale * handling simpler */ diff --git a/sys/phydat/phydat_str.c b/sys/phydat/phydat_str.c index d22c2e35eb05..15d6fb496c5d 100644 --- a/sys/phydat/phydat_str.c +++ b/sys/phydat/phydat_str.c @@ -111,6 +111,7 @@ static FLASH_ATTR const char _unit_square_metre[] = "m^2"; static FLASH_ATTR const char _unit_cubic_metre[] = "m^3"; static FLASH_ATTR const char _unit_g_force[] = "gₙ"; static FLASH_ATTR const char _unit_degree_per_second[] = "dps"; +static FLASH_ATTR const char _unit_revolutions_per_minute[] = "RPM"; static FLASH_ATTR const char _unit_gram[] = "g"; static FLASH_ATTR const char _unit_ampere[] = "A"; static FLASH_ATTR const char _unit_volt[] = "V"; @@ -147,6 +148,7 @@ static FLASH_ATTR const char * FLASH_ATTR const _unit_to_str[] = { [UNIT_M3] = _unit_cubic_metre, [UNIT_G_FORCE] = _unit_g_force, [UNIT_DPS] = _unit_degree_per_second, + [UNIT_RPM] = _unit_revolutions_per_minute, [UNIT_GRAM] = _unit_gram, [UNIT_A] = _unit_ampere, [UNIT_V] = _unit_volt, diff --git a/sys/senml/phydat.c b/sys/senml/phydat.c index a8265b7a8950..fe5daab07aac 100644 --- a/sys/senml/phydat.c +++ b/sys/senml/phydat.c @@ -33,6 +33,7 @@ static uint8_t phydat_unit_to_senml_unit(uint8_t unit) case UNIT_PH: return SENML_UNIT_PH; case UNIT_PA: return SENML_UNIT_PASCAL; case UNIT_CD: return SENML_UNIT_CANDELA; + case UNIT_RPM: return SENML_UNIT_RPM; /* Compatible Secondary units */ case UNIT_DBM: return SENML_UNIT_DECIBEL_MILLIWATT; diff --git a/tests/drivers/inc_encoder/Makefile b/tests/drivers/inc_encoder/Makefile new file mode 100644 index 000000000000..aa54e63b7743 --- /dev/null +++ b/tests/drivers/inc_encoder/Makefile @@ -0,0 +1,10 @@ +include ../Makefile.drivers_common + +BOARD ?= nucleo-f401re + +USEMODULE += inc_encoder +#USEMODULE += inc_encoder_software +USEMODULE += inc_encoder_hardware +USEMODULE += ztimer_sec + +include $(RIOTBASE)/Makefile.include diff --git a/tests/drivers/inc_encoder/Makefile.ci b/tests/drivers/inc_encoder/Makefile.ci new file mode 100644 index 000000000000..89a09fdb8c99 --- /dev/null +++ b/tests/drivers/inc_encoder/Makefile.ci @@ -0,0 +1 @@ +BOARD_INSUFFICIENT_MEMORY := diff --git a/tests/drivers/inc_encoder/README.md b/tests/drivers/inc_encoder/README.md new file mode 100644 index 000000000000..708a6e0d9933 --- /dev/null +++ b/tests/drivers/inc_encoder/README.md @@ -0,0 +1,11 @@ +# About +This is a manual test application for a generic incremental rotary encoder. + +# Usage +Connect a sensor - like a magnetic rotary encoder found on some dc motors - to your mcu. +The test will periodically print the measured RPM and number of revolutions +since the last readout. +See [drivers/inc_encoder/include/inc_encoder.h][inc_encoder] +for details on how to connect your sensor. + +[inc_encoder]: ../../drivers/inc_encoder/include/inc_encoder.h \ No newline at end of file diff --git a/tests/drivers/inc_encoder/main.c b/tests/drivers/inc_encoder/main.c new file mode 100644 index 000000000000..abf4c6e0bb86 --- /dev/null +++ b/tests/drivers/inc_encoder/main.c @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2025 TU Dresden + * SPDX-License-Identifier: LGPL-2.1-only + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Test application for a generic incremental rotary encoder + * + * @author Leonard Herbst + * + * @} + */ + +#include +#include + +#include "inc_encoder.h" +#include "inc_encoder_params.h" +#include "ztimer.h" + +static inc_encoder_t dev; + +int main(void) +{ + puts("Generic incremental rotary encoder test application\n"); + if (inc_encoder_init(&dev, &inc_encoder_params[0]) == 0) { + puts("[OK]\n"); + } + else { + puts("[Failed]"); + return 1; + } + + puts("Printing sensor state every second."); + while (1) { + int32_t rpm; + int32_t revs; + if (inc_encoder_read_rpm(&dev, &rpm) + || inc_encoder_read_reset_milli_revs(&dev, &revs)) { + puts("[Failed]"); + return 1; + } + printf("SENSOR DATA:\n\tRPM : %ld\n\tMREVS: %ld\n", (long) rpm, (long) revs); + ztimer_sleep(ZTIMER_SEC, 1); + } +} diff --git a/tests/drivers/saul_drivers/Makefile b/tests/drivers/saul_drivers/Makefile index bb31d7b40472..983665e46992 100644 --- a/tests/drivers/saul_drivers/Makefile +++ b/tests/drivers/saul_drivers/Makefile @@ -28,6 +28,9 @@ endif ifneq (,$(filter bme680,$(DRIVERS))) USEMODULE += bme680_i2c endif +ifneq (,$(filter inc_encoder,$(DRIVERS))) + USEMODULE += inc_encoder_software +endif ifneq (,$(filter lm75,$(DRIVERS))) USEMODULE += tmp1075 endif