Merge branch 'doc/pcnt_overflow_compensate' into 'master'

pcnt: support accumulate the count value in the high/low limit event

Closes IDFGH-8726 and IDF-5944

See merge request espressif/esp-idf!21076
pull/10224/head
morris 2022-11-21 15:13:44 +08:00
commit 71968a4856
8 zmienionych plików z 98 dodań i 48 usunięć

Wyświetl plik

@ -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;
/**

Wyświetl plik

@ -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
@ -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 {
@ -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
@ -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);
@ -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;
}
@ -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;
}
@ -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 = {

Wyświetl plik

@ -102,7 +102,6 @@ api-reference/peripherals/sdspi_host
api-reference/peripherals/dac
api-reference/peripherals/spi_slave
api-reference/peripherals/gptimer
api-reference/peripherals/pcnt
api-reference/peripherals/touch_element
api-reference/peripherals/lcd
api-reference/peripherals/secure_element

Wyświetl plik

@ -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.
@ -126,6 +127,12 @@ It is recommended to remove the unused watch point by :cpp:func:`pcnt_unit_remov
// add high limit watch point
ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, EXAMPLE_PCNT_HIGH_LIMIT));
.. only:: not SOC_PCNT_SUPPORT_RUNTIME_THRES_UPDATE
.. note::
Due to the hardware limitation, after adding a watch point, you should call :cpp:func:`pcnt_unit_clear_count` to make it take effect.
.. _pcnt-register-event-callbacks:
Register Event Callbacks
@ -223,17 +230,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 be cleared to zero automatically 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

Wyświetl plik

@ -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` 中,以供参考。
@ -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`
@ -126,6 +127,12 @@ PCNT 单元可被设置为观察几个特定的数值,这些被观察的数值
// add high limit watch point
ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, EXAMPLE_PCNT_HIGH_LIMIT));
.. only:: not SOC_PCNT_SUPPORT_RUNTIME_THRES_UPDATE
.. note::
由于硬件上的限制,在添加一个新的观察点后,你需要调用 :cpp:func:`pcnt_unit_clear_count` 函数来使之生效。
.. _pcnt-register-event-callbacks:
注册事件回调函数
@ -205,8 +212,8 @@ PCNT 单元的滤波器可滤除信号中的短时毛刺,:cpp:type:`pcnt_glitc
.. _pcnt-unit-io-control:
控制单元 IO
^^^^^^^^^^^^^^^
控制单元 IO 操作
^^^^^^^^^^^^^^^^
启用/停用及清零
^^^^^^^^^^^^^^^^^^
@ -223,17 +230,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:
电源管理

Wyświetl plik

@ -18,7 +18,7 @@ menu "Example Configuration"
config BLINK_GPIO
int "Blink GPIO number"
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
default 8 if IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32H4 || IDF_TARGET_ESP32C2
default 8 if IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32H4 || IDF_TARGET_ESP32C2 || IDF_TARGET_ESP32C6
default 18 if IDF_TARGET_ESP32S2
default 48 if IDF_TARGET_ESP32S3
default 5

Wyświetl plik

@ -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;
@ -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;
@ -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,
};
@ -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));
@ -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));

Wyświetl plik

@ -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 = {
@ -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
@ -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)
@ -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;
}