Skip to content

Commit 6720cf7

Browse files
committed
fix(pcnt): fix the accum_value missing when overflow
1 parent e5d11d1 commit 6720cf7

File tree

4 files changed

+93
-18
lines changed

4 files changed

+93
-18
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# PCNT Driver Design
2+
3+
## Concurrency
4+
5+
The count value and the overflow state of the count value are located in *different* registers, resulting in the software being unable to obtain information from both of them in the same read instruction.
6+
7+
The race condition case is as follow:
8+
9+
```mermaid
10+
sequenceDiagram
11+
participant HW as PCNT Hardware
12+
participant CPU0_ISR as CPU0_ISR
13+
participant CPU1_Task as CPU1_Task (pcnt_unit_get_count)
14+
participant REG as Reg and Soft accum counter State
15+
16+
CPU1_Task->>CPU1_Task: Call pcnt_unit_get_count()
17+
Note over REG: intr_status = 0<br/>cnt_reg = cnt_value<br/>accum_value = old_value
18+
CPU1_Task->>CPU1_Task: portENTER_CRITICAL_SAFE()
19+
CPU1_Task->>REG: Read intr_status
20+
Note over CPU1_Task: intr_status=0, no need to do compensation
21+
HW->>REG: Overflow interrupt triggered
22+
Note over REG: intr_status = 1<br/>cnt_reg = 0<br/>accum_value = old_value
23+
REG->>CPU0_ISR: ISR is called
24+
CPU0_ISR->>CPU0_ISR: try portENTER_CRITICAL_SAFE() but spin
25+
26+
CPU1_Task->>REG: Read cnt_reg(0) + accum_value(old)
27+
CPU1_Task->>CPU1_Task: portEXIT_CRITICAL_SAFE()
28+
29+
CPU0_ISR->>CPU0_ISR: portENTER_CRITICAL_SAFE()
30+
CPU0_ISR->>REG: Clear interrupt status and update accum_value
31+
Note over REG: intr_status = 0<br/>accum_value = new_value
32+
CPU0_ISR->>CPU0_ISR: portEXIT_CRITICAL_SAFE()
33+
34+
Note over CPU0_ISR: Process events
35+
Note over CPU1_Task: Return incorrect count ❌
36+
37+
```
38+
39+
In the software, we determine whether to perform compensation by checking whether the count value exceeds half of the limit. This can prevent counting errors when the overflow frequency is not high.

components/esp_driver_pcnt/src/pulse_cnt.c

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -475,10 +475,37 @@ esp_err_t pcnt_unit_get_count(pcnt_unit_handle_t unit, int *value)
475475
pcnt_group_t *group = NULL;
476476
ESP_RETURN_ON_FALSE_ISR(unit && value, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
477477
group = unit->group;
478+
int temp_value = 0;
478479

479480
// the accum_value is also accessed by the ISR, so adding a critical section
480481
portENTER_CRITICAL_SAFE(&unit->spinlock);
481-
*value = pcnt_ll_get_count(group->hal.dev, unit->unit_id) + unit->accum_value;
482+
temp_value = pcnt_ll_get_count(group->hal.dev, unit->unit_id) ;
483+
// Check for pending overflow interrupts that haven't been processed yet
484+
// Add compensation to get accurate count
485+
if (unit->flags.accum_count) {
486+
uint32_t intr_status = pcnt_ll_get_intr_status(group->hal.dev);
487+
if (intr_status & PCNT_LL_UNIT_WATCH_EVENT(unit->unit_id)) {
488+
uint32_t event_status = pcnt_ll_get_event_status(group->hal.dev, unit->unit_id);
489+
490+
// TODO: DIG-683
491+
// Note, the overflow may be triggered between `pcnt_ll_get_count` and `pcnt_ll_get_event_status`
492+
// In this case, we don't want to do the compensation.
493+
// so we should check the count value is greater(less) than the low(high) limit / 2 to filter this case.
494+
// This workaround is only valid for the case that the counter won't overflow twice between `pcnt_ll_get_count()` and `pcnt_ll_get_intr_status()`
495+
if (event_status & BIT(PCNT_LL_WATCH_EVENT_LOW_LIMIT) && temp_value >= unit->low_limit / 2) {
496+
temp_value += unit->low_limit;
497+
} else if (event_status & BIT(PCNT_LL_WATCH_EVENT_HIGH_LIMIT) && temp_value <= unit->high_limit / 2) {
498+
temp_value += unit->high_limit;
499+
}
500+
#if SOC_PCNT_SUPPORT_STEP_NOTIFY && PCNT_LL_STEP_NOTIFY_DIR_LIMIT
501+
else if (event_status & BIT(PCNT_LL_STEP_EVENT_REACH_LIMIT) && abs(temp_value) <= abs(unit->step_info.step_limit) / 2) {
502+
temp_value += unit->step_info.step_limit;
503+
}
504+
#endif
505+
}
506+
}
507+
508+
*value = temp_value + unit->accum_value;
482509
portEXIT_CRITICAL_SAFE(&unit->spinlock);
483510

484511
return ESP_OK;
@@ -975,11 +1002,27 @@ IRAM_ATTR static void pcnt_default_isr(void *args)
9751002

9761003
uint32_t intr_status = pcnt_ll_get_intr_status(group->hal.dev);
9771004
if (intr_status & PCNT_LL_UNIT_WATCH_EVENT(unit_id)) {
978-
pcnt_ll_clear_intr_status(group->hal.dev, PCNT_LL_UNIT_WATCH_EVENT(unit_id));
979-
9801005
// event status word contains information about the real watch event type
9811006
uint32_t event_status = pcnt_ll_get_event_status(group->hal.dev, unit_id);
9821007

1008+
// clear interrupt status and update accum_value atomically
1009+
portENTER_CRITICAL_ISR(&unit->spinlock);
1010+
pcnt_ll_clear_intr_status(group->hal.dev, PCNT_LL_UNIT_WATCH_EVENT(unit_id));
1011+
1012+
if (unit->flags.accum_count) {
1013+
if (event_status & BIT(PCNT_LL_WATCH_EVENT_LOW_LIMIT)) {
1014+
unit->accum_value += unit->low_limit;
1015+
} else if (event_status & BIT(PCNT_LL_WATCH_EVENT_HIGH_LIMIT)) {
1016+
unit->accum_value += unit->high_limit;
1017+
}
1018+
#if SOC_PCNT_SUPPORT_STEP_NOTIFY && PCNT_LL_STEP_NOTIFY_DIR_LIMIT
1019+
else if (event_status & BIT(PCNT_LL_STEP_EVENT_REACH_LIMIT)) {
1020+
unit->accum_value += unit->step_info.step_limit;
1021+
}
1022+
#endif
1023+
}
1024+
portEXIT_CRITICAL_ISR(&unit->spinlock);
1025+
9831026
int count_value = 0;
9841027
pcnt_unit_zero_cross_mode_t zc_mode = PCNT_UNIT_ZERO_CROSS_INVALID;
9851028

@@ -998,11 +1041,6 @@ IRAM_ATTR static void pcnt_default_isr(void *args)
9981041
event_status &= ~BIT(PCNT_LL_STEP_EVENT_REACH_LIMIT);
9991042
// adjust current count value to the step limit
10001043
count_value = unit->step_info.step_limit;
1001-
if (unit->flags.accum_count) {
1002-
portENTER_CRITICAL_ISR(&unit->spinlock);
1003-
unit->accum_value += unit->step_info.step_limit;
1004-
portEXIT_CRITICAL_ISR(&unit->spinlock);
1005-
}
10061044
}
10071045
#else
10081046
// step event has higher priority than pointer event
@@ -1027,20 +1065,10 @@ IRAM_ATTR static void pcnt_default_isr(void *args)
10271065
event_status &= ~(BIT(PCNT_LL_WATCH_EVENT_LOW_LIMIT));
10281066
// adjust the count value to reflect the actual watch point value
10291067
count_value = unit->low_limit;
1030-
if (unit->flags.accum_count) {
1031-
portENTER_CRITICAL_ISR(&unit->spinlock);
1032-
unit->accum_value += unit->low_limit;
1033-
portEXIT_CRITICAL_ISR(&unit->spinlock);
1034-
}
10351068
} else if (event_status & BIT(PCNT_LL_WATCH_EVENT_HIGH_LIMIT)) {
10361069
event_status &= ~(BIT(PCNT_LL_WATCH_EVENT_HIGH_LIMIT));
10371070
// adjust the count value to reflect the actual watch point value
10381071
count_value = unit->high_limit;
1039-
if (unit->flags.accum_count) {
1040-
portENTER_CRITICAL_ISR(&unit->spinlock);
1041-
unit->accum_value += unit->high_limit;
1042-
portEXIT_CRITICAL_ISR(&unit->spinlock);
1043-
}
10441072
} else if (event_status & BIT(PCNT_LL_WATCH_EVENT_THRES0)) {
10451073
event_status &= ~(BIT(PCNT_LL_WATCH_EVENT_THRES0));
10461074
// adjust the count value to reflect the actual watch point value

docs/en/api-reference/peripherals/pcnt.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,10 @@ The internal hardware counter will be cleared to zero automatically when it reac
329329

330330
:cpp:func:`pcnt_unit_clear_count` resets the accumulated count value as well.
331331

332+
.. note::
333+
334+
When enabling the count overflow compensation, it is recommended to use as large a high/low count limit as possible, as it can avoid frequent interrupt triggering, improve system performance, and avoid compensation failure due to multiple overflows.
335+
332336
.. _pcnt-power-management:
333337

334338
Power Management

docs/zh_CN/api-reference/peripherals/pcnt.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,10 @@ PCNT 内部的硬件计数器会在计数达到高/低门限的时候自动清
329329

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

332+
.. note::
333+
334+
启用计数溢出补偿时,建议使用尽可能大的高/低计数门限,因为它可以避免中断被频繁触发,提高系统性能,并且避免由于多次溢出而导致的补偿失败。
335+
332336
.. _pcnt-power-management:
333337

334338
电源管理

0 commit comments

Comments
 (0)