Skip to content

Commit

Permalink
pcnt: support accumulate count value
Browse files Browse the repository at this point in the history
Closes #10167
  • Loading branch information
suda-morris committed Nov 21, 2022
1 parent 919b3e6 commit 0dbbf7b
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 46 deletions.
3 changes: 3 additions & 0 deletions components/driver/include/driver/pulse_cnt.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ typedef struct {
typedef struct {
int low_limit; /*!< Low limitation of the count unit, should be lower than 0 */
int high_limit; /*!< High limitation of the count unit, should be higher than 0 */
struct {
uint32_t accum_count: 1; /*!< Whether to accumulate the count value when overflows at the high/low limit */
} flags; /*!< Extra flags */
} pcnt_unit_config_t;

/**
Expand Down
40 changes: 38 additions & 2 deletions components/driver/pulse_cnt.c
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ struct pcnt_unit_t {
int unit_id; // allocated unit numerical ID
int low_limit; // low limit value
int high_limit; // high limit value
int accum_value; // accumulated count value
pcnt_chan_t *channels[SOC_PCNT_CHANNELS_PER_UNIT]; // array of PCNT channels
pcnt_watch_point_t watchers[PCNT_LL_WATCH_EVENT_MAX]; // array of PCNT watchers
intr_handle_t intr; // interrupt handle
Expand All @@ -94,6 +95,9 @@ struct pcnt_unit_t {
pcnt_unit_fsm_t fsm; // record PCNT unit's driver state
pcnt_watch_cb_t on_reach; // user registered callback function
void *user_data; // user data registered by user, which would be passed to the right callback function
struct {
uint32_t accum_count: 1; /*!< Whether to accumulate the count value when overflows at the high/low limit */
} flags;
};

struct pcnt_chan_t {
Expand Down Expand Up @@ -186,6 +190,16 @@ esp_err_t pcnt_new_unit(const pcnt_unit_config_t *config, pcnt_unit_handle_t *re
int group_id = group->group_id;
int unit_id = unit->unit_id;

// to accumulate count value, we should install the interrupt handler first, and in the ISR we do the accumulation
bool to_install_isr = (config->flags.accum_count == 1);
if (to_install_isr) {
int isr_flags = PCNT_INTR_ALLOC_FLAGS;
ESP_GOTO_ON_ERROR(esp_intr_alloc_intrstatus(pcnt_periph_signals.groups[group_id].irq, isr_flags,
(uint32_t)pcnt_ll_get_intr_status_reg(group->hal.dev), PCNT_LL_UNIT_WATCH_EVENT(unit_id),
pcnt_default_isr, unit, &unit->intr), err,
TAG, "install interrupt service failed");
}

// some events are enabled by default, disable them all
pcnt_ll_disable_all_events(group->hal.dev, unit_id);
// disable filter by default
Expand All @@ -196,12 +210,15 @@ esp_err_t pcnt_new_unit(const pcnt_unit_config_t *config, pcnt_unit_handle_t *re
pcnt_ll_set_low_limit_value(group->hal.dev, unit_id, config->low_limit);
unit->high_limit = config->high_limit;
unit->low_limit = config->low_limit;
unit->accum_value = 0;
unit->flags.accum_count = config->flags.accum_count;

// clear/pause register is shared by all units, so using group's spinlock
portENTER_CRITICAL(&group->spinlock);
pcnt_ll_stop_count(group->hal.dev, unit_id);
pcnt_ll_clear_count(group->hal.dev, unit_id);
pcnt_ll_enable_intr(group->hal.dev, PCNT_LL_UNIT_WATCH_EVENT(unit_id), false);
// enable the interrupt if we want to accumulate the counter in the ISR
pcnt_ll_enable_intr(group->hal.dev, PCNT_LL_UNIT_WATCH_EVENT(unit_id), to_install_isr);
pcnt_ll_clear_intr_status(group->hal.dev, PCNT_LL_UNIT_WATCH_EVENT(unit_id));
portEXIT_CRITICAL(&group->spinlock);

Expand Down Expand Up @@ -349,6 +366,11 @@ esp_err_t pcnt_unit_clear_count(pcnt_unit_handle_t unit)
pcnt_ll_clear_count(group->hal.dev, unit->unit_id);
portEXIT_CRITICAL_SAFE(&group->spinlock);

// reset the accumulated count as well
portENTER_CRITICAL_SAFE(&unit->spinlock);
unit->accum_value = 0;
portEXIT_CRITICAL_SAFE(&unit->spinlock);

return ESP_OK;
}

Expand All @@ -357,7 +379,11 @@ esp_err_t pcnt_unit_get_count(pcnt_unit_handle_t unit, int *value)
pcnt_group_t *group = NULL;
ESP_RETURN_ON_FALSE_ISR(unit && value, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
group = unit->group;
*value = pcnt_ll_get_count(group->hal.dev, unit->unit_id);

// the accum_value is also accessed by the ISR, so adding a critical section
portENTER_CRITICAL_SAFE(&unit->spinlock);
*value = pcnt_ll_get_count(group->hal.dev, unit->unit_id) + unit->accum_value;
portEXIT_CRITICAL_SAFE(&unit->spinlock);

return ESP_OK;
}
Expand Down Expand Up @@ -723,6 +749,16 @@ IRAM_ATTR static void pcnt_default_isr(void *args)
int event_id = __builtin_ffs(event_status) - 1;
event_status &= (event_status - 1); // clear the right most bit

portENTER_CRITICAL_ISR(&unit->spinlock);
if (unit->flags.accum_count) {
if (event_id == PCNT_LL_WATCH_EVENT_LOW_LIMIT) {
unit->accum_value += unit->low_limit;
} else if (event_id == PCNT_LL_WATCH_EVENT_HIGH_LIMIT) {
unit->accum_value += unit->high_limit;
}
}
portEXIT_CRITICAL_ISR(&unit->spinlock);

// invoked user registered callback
if (on_reach) {
pcnt_watch_event_data_t edata = {
Expand Down
24 changes: 18 additions & 6 deletions docs/en/api-reference/peripherals/pcnt.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ Install PCNT Unit

To install a PCNT unit, there's a configuration structure that needs to be given in advance: :cpp:type:`pcnt_unit_config_t`:

- :cpp:member:`pcnt_unit_config_t::low_limit` and :cpp:member:`pcnt_unit_config_t::high_limit` specify the range for the internal counter. Counter will back to zero when it crosses either limit value.
- :cpp:member:`pcnt_unit_config_t::low_limit` and :cpp:member:`pcnt_unit_config_t::high_limit` specify the range for the internal hardware counter. The counter will reset to zero automatically when it crosses either the high or low limit.
- :cpp:member:`pcnt_unit_config_t::accum_count` sets whether to create an internal accumulator for the counter. This is helpful when you want to extend the counter's width, which by default is 16bit at most, defined in the hardware. See also :ref:`pcnt-compensate-overflow-loss` for how to use this feature to compensate the overflow loss.

Unit allocation and initialization is done by calling a function :cpp:func:`pcnt_new_unit` with :cpp:type:`pcnt_unit_config_t` as an input parameter. The function will return a PCNT unit handle only when it runs correctly. Specifically, when there are no more free PCNT units in the pool (i.e. unit resources have been used up), then this function will return :c:macro:`ESP_ERR_NOT_FOUND` error. The total number of available PCNT units is recorded by :c:macro:`SOC_PCNT_UNITS_PER_GROUP` for reference.

Expand Down Expand Up @@ -223,17 +224,28 @@ Note, :cpp:func:`pcnt_unit_start` and :cpp:func:`pcnt_unit_stop` should be calle
Get Count Value
~~~~~~~~~~~~~~~

You can check current count value at any time by calling :cpp:func:`pcnt_unit_get_count`.

.. note::

The returned count value is a **signed** integer, where the sign can be used to reflect the direction. The internal counter will overflow when it reaches high or low limit, but this function doesn't compensate for that loss.
You can read current count value at any time by calling :cpp:func:`pcnt_unit_get_count`. The returned count value is a **signed** integer, where the sign can be used to reflect the direction.

.. code:: c
int pulse_count = 0;
ESP_ERROR_CHECK(pcnt_unit_get_count(pcnt_unit, &pulse_count));
.. _pcnt-compensate-overflow-loss:

Compensate Overflow Loss
~~~~~~~~~~~~~~~~~~~~~~~~

The internal hardware counter will be cleared to zero automatically when it reaches high or low limit. If you want to compensate for that count loss and extend the counter's bit-width, you can:

1. Enable :cpp:member:`pcnt_unit_config_t::accum_count` when installing the PCNT unit.
2. Add the high/low limit as the :ref:`pcnt-watch-points`.
3. Now, the returned count value from the :cpp:func:`pcnt_unit_get_count` function not only reflects the hardware's count value, but also accumulates the high/low overflow loss to it.

.. note::

:cpp:func:`pcnt_unit_clear_count` will reset the accumulated count value as well.

.. _pcnt-power-management:

Power Management
Expand Down
30 changes: 21 additions & 9 deletions docs/zh_CN/api-reference/peripherals/pcnt.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ PCNT 单元和通道分别用 :cpp:type:`pcnt_unit_handle_t` 与 :cpp:type:`pcnt
安装 PCNT 单元时,需要先完成配置 :cpp:type:`pcnt_unit_config_t`:

- :cpp:member:`pcnt_unit_config_t::low_limit` 与 :cpp:member:`pcnt_unit_config_t::high_limit` 用于指定内部计数器的最小值和最大值。当计数器超过任一限值时,计数器将归零。
- :cpp:member:`pcnt_unit_config_t::accum_count` 用于设置是否需要软件在硬件计数值溢出的时候进行累加保存,这有助于“拓宽”计数器的实际位宽。默认情况下,计数器的位宽最高只有 16 比特。请参考 :ref:`pcnt-compensate-overflow-loss` 了解如何利用此功能来补偿硬件计数器的溢出损失。

调用函数 :cpp:func:`pcnt_new_unit` 并将 :cpp:type:`pcnt_unit_config_t` 作为其输入值,可对 PCNT 单元进行分配和初始化。该函数正常运行时,会返回一个 PCNT 单元句柄。没有可用的 PCNT 单元时(即 PCNT 单元全部被占用),该函数会返回错误 :c:macro:`ESP_ERR_NOT_FOUND`。可用的 PCNT 单元总数记录在 :c:macro:`SOC_PCNT_UNITS_PER_GROUP` 中,以供参考。

Expand Down Expand Up @@ -110,8 +111,8 @@ PCNT 单元和通道分别用 :cpp:type:`pcnt_unit_handle_t` 与 :cpp:type:`pcnt
.. _pcnt-watch-points:

配置观察点
^^^^^^^^^^
PCNT 观察点
^^^^^^^^^^^

PCNT 单元可被设置为观察几个特定的数值,这些被观察的数值被称为 **观察点**。观察点不能超过 :cpp:type:`pcnt_unit_config_t` 设置的范围,最小值和最大值分别为 :cpp:member:`pcnt_unit_config_t::low_limit` 和 :cpp:member:`pcnt_unit_config_t::high_limit`。当计数器到达任一观察点时,会触发一个观察事件,如果在 :cpp:func:`pcnt_unit_register_event_callbacks` 注册过事件回调函数,该事件就会通过中断通知您。关于如何注册事件回调函数,请参考 :ref:`pcnt-register-event-callbacks`。

Expand Down Expand Up @@ -205,8 +206,8 @@ PCNT 单元的滤波器可滤除信号中的短时毛刺,:cpp:type:`pcnt_glitc
.. _pcnt-unit-io-control:

控制单元 IO
^^^^^^^^^^^^^^^
控制单元 IO 操作
^^^^^^^^^^^^^^^^

启用/停用及清零
^^^^^^^^^^^^^^^^^^
Expand All @@ -223,17 +224,28 @@ PCNT 单元的滤波器可滤除信号中的短时毛刺,:cpp:type:`pcnt_glitc
获取计数器数值
^^^^^^^^^^^^^^^^^^^

通过调用 :cpp:func:`pcnt_unit_get_count` 可随时获取当前计数器的数值。

.. note::

返回的计数器数值是一个 **带符号** 的整数,符号代表计数方向。计数器的数值大于等于最大值或小于等于最小值时,计数器会溢出。
调用 :cpp:func:`pcnt_unit_get_count` 可随时获取当前计数器的数值。返回的计数值是一个 **带符号** 的整型数,其符号反映了计数的方向。

.. code:: c
int pulse_count = 0;
ESP_ERROR_CHECK(pcnt_unit_get_count(pcnt_unit, &pulse_count));
.. _pcnt-compensate-overflow-loss:

计数溢出补偿
~~~~~~~~~~~~

PCNT 内部的硬件计数器会在计数达到高/低门限的时候自动清零。如果你想补偿该计数值的溢出损失,以期进一步拓宽计数器的实际位宽,你可以:

1. 在安装 PCNT 计数单元的时候使能 :cpp:member:`pcnt_unit_config_t::accum_count` 选项。
2. 将高/低计数门限设置为 :ref:`pcnt-watch-points`.
3. 现在,:cpp:func:`pcnt_unit_get_count` 函数返回的计数值就会包含硬件计数器当前的计数值,累加上计数器溢出造成的损失。

.. note::

:cpp:func:`pcnt_unit_clear_count` 会复位该软件累加器。

.. _pcnt-power-management:

电源管理
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,9 @@ typedef struct {
bdc_motor_handle_t motor;
pcnt_unit_handle_t pcnt_encoder;
pid_ctrl_block_handle_t pid_ctrl;
int accumu_count;
int report_pulses;
} motor_control_context_t;

static bool example_pcnt_on_reach(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx)
{
int *accumu_count = (int *)user_ctx;
*accumu_count += edata->watch_point_value;
return false;
}

static void pid_loop_cb(void *args)
{
static int last_pulse_count = 0;
Expand All @@ -60,7 +52,6 @@ static void pid_loop_cb(void *args)
// get the result from rotary encoder
int cur_pulse_count = 0;
pcnt_unit_get_count(pcnt_unit, &cur_pulse_count);
cur_pulse_count += ctx->accumu_count;
int real_pulses = cur_pulse_count - last_pulse_count;
last_pulse_count = cur_pulse_count;
ctx->report_pulses = real_pulses;
Expand All @@ -77,7 +68,6 @@ static void pid_loop_cb(void *args)
void app_main(void)
{
static motor_control_context_t motor_ctrl_ctx = {
.accumu_count = 0,
.pcnt_encoder = NULL,
};

Expand All @@ -99,6 +89,7 @@ void app_main(void)
pcnt_unit_config_t unit_config = {
.high_limit = BDC_ENCODER_PCNT_HIGH_LIMIT,
.low_limit = BDC_ENCODER_PCNT_LOW_LIMIT,
.flags.accum_count = true, // enable counter accumulation
};
pcnt_unit_handle_t pcnt_unit = NULL;
ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &pcnt_unit));
Expand All @@ -124,10 +115,6 @@ void app_main(void)
ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_b, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE));
ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, BDC_ENCODER_PCNT_HIGH_LIMIT));
ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, BDC_ENCODER_PCNT_LOW_LIMIT));
pcnt_event_callbacks_t pcnt_cbs = {
.on_reach = example_pcnt_on_reach, // accumulate the overflow in the callback
};
ESP_ERROR_CHECK(pcnt_unit_register_event_callbacks(pcnt_unit, &pcnt_cbs, &motor_ctrl_ctx.accumu_count));
ESP_ERROR_CHECK(pcnt_unit_enable(pcnt_unit));
ESP_ERROR_CHECK(pcnt_unit_clear_count(pcnt_unit));
ESP_ERROR_CHECK(pcnt_unit_start(pcnt_unit));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,13 @@ static rmt_channel_handle_t s_rmt_chan;
static rmt_encoder_handle_t s_rmt_encoder;
static volatile uint32_t s_milliseconds;

static bool on_reach_watch_point(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx)
{
s_milliseconds += REF_CLOCK_PRESCALER_MS;
return false;
}

void ref_clock_init(void)
{
// Initialize PCNT
pcnt_unit_config_t unit_config = {
.high_limit = REF_CLOCK_PRESCALER_MS * 1000,
.low_limit = -100, // any minus value is OK, in this case, we don't count down
.flags.accum_count = true, // accumulate the counter value
};
TEST_ESP_OK(pcnt_new_unit(&unit_config, &s_pcnt_unit));
pcnt_chan_config_t chan_config = {
Expand All @@ -63,13 +58,8 @@ void ref_clock_init(void)
TEST_ESP_OK(pcnt_channel_set_edge_action(s_pcnt_chan, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE));
// don't care level change
TEST_ESP_OK(pcnt_channel_set_level_action(s_pcnt_chan, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_KEEP));
// add watch point
// add watch point: high limit
TEST_ESP_OK(pcnt_unit_add_watch_point(s_pcnt_unit, REF_CLOCK_PRESCALER_MS * 1000));
// register watch event
pcnt_event_callbacks_t cbs = {
.on_reach = on_reach_watch_point,
};
TEST_ESP_OK(pcnt_unit_register_event_callbacks(s_pcnt_unit, &cbs, NULL));
// enable pcnt
TEST_ESP_OK(pcnt_unit_enable(s_pcnt_unit));
// start pcnt
Expand Down Expand Up @@ -110,8 +100,6 @@ void ref_clock_init(void)
.flags.eot_level = 1,
};
TEST_ESP_OK(rmt_transmit(s_rmt_chan, s_rmt_encoder, &data, sizeof(data), &trans_config));

s_milliseconds = 0;
}

void ref_clock_deinit(void)
Expand All @@ -133,5 +121,6 @@ uint64_t ref_clock_get(void)
{
int microseconds = 0;
TEST_ESP_OK(pcnt_unit_get_count(s_pcnt_unit, &microseconds));
return 1000 * (uint64_t)s_milliseconds + (uint64_t)microseconds;
// because the PCNT is configured to always count up, it's impossible to get a negative value
return (uint64_t)microseconds;
}

0 comments on commit 0dbbf7b

Please sign in to comment.