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
30 changes: 30 additions & 0 deletions cpu/nrf5x_common/include/periph_cpu_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,36 @@ typedef struct {
uint8_t sample_period; /**< Sample period used, e.g. QDEC_SAMPLEPER_SAMPLEPER_128us */
bool debounce_filter; /**< Enable/disable debounce filter */
bool led_active_state; /**< Active state of the LED. True is active high, false is active low */
/**
* @brief Period of sample reports
*
* Use, for instance, QDEC_REPORTPER_REPORTPER_10Smpl. See the note on the callback_threshold
* parameter to understand how this value affects the callback.
*/
uint8_t report_period;
/**
* @brief Threshold count for the user callback
*
* The callback is called once when the counter reaches or surpasses +callback_threshold or
* -callback_threshold. The counter is not reset by the interrupt on the driver side, so no
* further callback happens until it is back within the range. Users may reset the counter in
* the callback using @ref qdec_read_and_reset to get another callback after at least
* +/- callback_threshold counts. Setting this to 0 will disable interrupts. Set this value to 1
* to get a callback on every step (with the caveats noted below). A value of 4 is useful with
* many rotary knobs.
*
* @note The counter is checked only once per report period to decide whether to call the user
* callback. Depending on this period, the callback may be called when the counter is
* already be beyond +/- callback_threshold. The upper limit of this deviation is one
* report period's worth of steps. Since this interrupt will never reset the counter,
* the user can read it via @ref qdec_read_and_reset. To mitigate the behaviour of
* getting a callback when the counter is already beyond +/- callback_threshold, use
* `QDEC_REPORTPER_REPORTPER_1Smpl` as a report period. With this configuration, the
* driver will call the callback exactly at +callback_threshold or -callback_threshold,
* at the cost of up to one interrupt (not callback!) per sample while the encoder is
* moving.
*/
uint32_t callback_threshold;
} qdec_conf_t;

/**
Expand Down
65 changes: 56 additions & 9 deletions cpu/nrf5x_common/periph/qdec.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2021 Koen Zandberg <koen@bergzand.net>
* SPDX-FileCopyrightText: 2026 Technische Universität Hamburg
* SPDX-License-Identifier: LGPL-2.1-only
*/

Expand All @@ -12,12 +13,14 @@
* @brief Low-level QDEC driver implementation
*
* @author Koen Zandberg <koen@bergzand.net>
* @author Leandro Lanzieri <leandro.lanzieri@tuhh.de>
*
* @}
*/
#include <errno.h>

#include "cpu.h"
#include "irq.h"
#include "periph_conf.h"
#include "periph/qdec.h"
#include "periph/gpio.h"
Expand All @@ -29,6 +32,11 @@
*/
static qdec_isr_ctx_t isr_ctx[QDEC_NUMOF];

/**
* @brief Software accumulator for each configured qdec
*/
static int32_t _acc[QDEC_NUMOF];

static inline NRF_QDEC_Type* dev(qdec_t qdec)
{
(void)qdec;
Expand All @@ -40,12 +48,24 @@ static inline const qdec_conf_t* conf(qdec_t qdec)
return &qdec_config[qdec];
}

/**
* @brief Read the hardware accumulator into the software one
*
* @param[in] qdec the qdec device from which to read the hardware accumulator
*/
static void _read_hw_accumulator(qdec_t qdec)
{
dev(qdec)->TASKS_RDCLRACC = 1;
_acc[qdec] += (int32_t)dev(qdec)->ACCREAD;
}

int32_t qdec_init(qdec_t qdec, qdec_mode_t mode, qdec_cb_t cb, void *arg)
{
(void)mode;
/* Verify parameters */
assert((qdec < QDEC_NUMOF));
assert(conf(qdec)->sample_period <= QDEC_SAMPLEPER_SAMPLEPER_131ms);
assert(conf(qdec)->callback_threshold <= INT32_MAX);

/* The nrf5x peripheral counts all edges */
if (mode != QDEC_X4) {
Expand All @@ -54,14 +74,16 @@ int32_t qdec_init(qdec_t qdec, qdec_mode_t mode, qdec_cb_t cb, void *arg)

isr_ctx[qdec].cb = cb;
isr_ctx[qdec].arg = arg;
_acc[qdec] = 0;

if (cb) {
if (cb && (conf(qdec)->callback_threshold > 0)) {
dev(qdec)->EVENTS_REPORTRDY = 0;
dev(qdec)->INTENSET = QDEC_INTENSET_REPORTRDY_Msk;
NVIC_EnableIRQ(QDEC_IRQn);
dev(qdec)->INTENSET = QDEC_INTENSET_ACCOF_Msk;
}
else {
NVIC_DisableIRQ(QDEC_IRQn);
dev(qdec)->INTENCLR = QDEC_INTENCLR_ACCOF_Msk;
dev(qdec)->INTENCLR = QDEC_INTENCLR_REPORTRDY_Msk;
}
Comment on lines +79 to 87

@kfessel kfessel Sep 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about keeping the overflow behaviour when callback_threshold is 0

Suggested change
if (cb && (conf(qdec)->callback_threshold > 0)) {
dev(qdec)->EVENTS_REPORTRDY = 0;
dev(qdec)->INTENSET = QDEC_INTENSET_REPORTRDY_Msk;
NVIC_EnableIRQ(QDEC_IRQn);
dev(qdec)->INTENSET = QDEC_INTENSET_ACCOF_Msk;
}
else {
NVIC_DisableIRQ(QDEC_IRQn);
dev(qdec)->INTENCLR = QDEC_INTENCLR_ACCOF_Msk;
dev(qdec)->INTENCLR = QDEC_INTENCLR_REPORTRDY_Msk;
}
NVIC_DisableIRQ(QDEC_IRQn);
if ((conf(qdec)->callback_threshold > 0)) {
dev(qdec)->EVENTS_REPORTRDY = 0;
dev(qdec)->INTENSET = QDEC_INTENSET_REPORTRDY_Msk;
}
else {
dev(qdec)->INTENSET = QDEC_INTENCLR_ACCOF_Msk;
}
if (cb) {
NVIC_EnableIRQ(QDEC_IRQn);
}

reasoning:
EVENTS_REPORTRDY is time based (and count) -> less sleep

overflow will happen less often (depends on the only on the counted distance) -> has a real advantage

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about keeping the overflow behaviour when callback_threshold is 0

Do you mean that the user would get a callback when the count reaches 1024 or keep the software counter and call back every multiple of 1024? I'm not sure about how this would work.

Keep in mind that this PR also include the software counter.

We could have callback_threshold = 0 a sort of legacy mode that restricts count to +/- 1024 and only uses the overflow interrupt. Probably not the nicest API but it would cover both cases.

Do you have any thoughts on that?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my thought and reasoning is in the follow up comment

so yes I think it would be nice to have callback_threshold == 0 + callback function to behave like a legacy fallback (overflow only) to avoid the extra interrupts that might wake up the cpu and only have overflow interrupts)


gpio_init(conf(qdec)->a_pin, GPIO_IN_PU);
Expand All @@ -81,6 +103,7 @@ int32_t qdec_init(qdec_t qdec, qdec_mode_t mode, qdec_cb_t cb, void *arg)

dev(qdec)->DBFEN = conf(qdec)->debounce_filter ? 1 : 0;
dev(qdec)->SAMPLEPER = conf(qdec)->sample_period;
dev(qdec)->REPORTPER = conf(qdec)->report_period;

/* Enable the peripheral */
dev(qdec)->ENABLE = 1;
Expand All @@ -90,14 +113,25 @@ int32_t qdec_init(qdec_t qdec, qdec_mode_t mode, qdec_cb_t cb, void *arg)

int32_t qdec_read_and_reset(qdec_t qdec)
{
/* Read and clear ACC register */
dev(qdec)->TASKS_RDCLRACC = 1;
return dev(qdec)->ACCREAD;
unsigned state = irq_disable();

_read_hw_accumulator(qdec);
int32_t value = _acc[qdec];
_acc[qdec] = 0;

irq_restore(state);
return value;
}

int32_t qdec_read(qdec_t qdec)
{
return dev(qdec)->ACC;
unsigned state = irq_disable();

_read_hw_accumulator(qdec);
int32_t value = _acc[qdec];

irq_restore(state);
return value;
}

void qdec_start(qdec_t qdec)
Expand All @@ -115,7 +149,20 @@ void qdec_stop(qdec_t qdec)
void isr_qdec(void)
{
qdec_t qdec = QDEC_DEV(0); /* only one available */
dev(qdec)->EVENTS_ACCOF = 0;
isr_ctx[qdec].cb(isr_ctx[qdec].arg);

if (dev(qdec)->EVENTS_REPORTRDY) {
dev(qdec)->EVENTS_REPORTRDY = 0;

int32_t prev = _acc[qdec];
_read_hw_accumulator(qdec);

/* Only call the callback when the counter crosses +/-callback_threshold */
int32_t max = conf(qdec)->callback_threshold;
if ((_acc[qdec] >= max && prev < max) || (_acc[qdec] <= -max && prev > -max)) {
isr_ctx[qdec].cb(isr_ctx[qdec].arg);
}
}

cortexm_isr_end();
Comment thread
kfessel marked this conversation as resolved.
}
#endif
3 changes: 2 additions & 1 deletion drivers/include/periph/qdec.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ typedef struct {
* See description above for more details about modes.
*
* On QDEC counter overflow, an interrupt is triggered.
* The interruption calls the callback defined.
* The interruption calls the callback defined. Some platforms allow configuring
* the maximum counter value in the board's `qdec_conf_t`.
*
* @pre The QDEC device must not be active when calling `qdec_init()`. It
* must either not have been initialized before or it must be stopped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ static const qdec_conf_t qdec_config[] = {
.led_pin = GPIO_PIN(0, 13), /* And the first LED */
.sample_period = QDEC_SAMPLEPER_SAMPLEPER_128us,
.debounce_filter = true,
.led_active_state = false
.led_active_state = false,
.report_period = QDEC_REPORTPER_REPORTPER_1Smpl,
.callback_threshold = 4,
},
};
#define QDEC_NUMOF ARRAY_SIZE(qdec_config)
Expand Down
Loading