Перейти к содержимому

ESP32 PCNT – аппаратные счётчики импульсов

Доброго здравия, уважаемые друзья и читатели!

В данной статье обсудим работу с аппаратными контроллерами счетчиков импульсов, встроенными в чипы ESP32.

Модуль PCNT (Pulse Counter) предназначен для подсчёта количества нарастающих и/или спадающих фронтов входных сигналов. Каждый PCNT unit — это независимый счётчик с двумя входными каналами, где каждый канал может увеличивать или уменьшать значение счётчика при обнаружении соответствующего фронта. Каналы могут быть сконфигурированы индивидуально.

PCNT поддерживает два типа сигналов:

  • Edge signal — основной счётный сигнал, по фронтам или спадам которого происходит подсчёт (например, импульсы с датчика или кнопки).
  • Level signal (control signal) — дополнительный управляющий сигнал, который может изменять режим подсчёта (например, инвертировать направление счета или временно его приостанавливать).

Также в PCNT реализован отдельный фильтр коротких импульсов (glitch filter), который позволяет отсеивать помехи от дребезга контактов и считать только “чистые” импульсы.

Каналы PCNT можно настроить так, чтобы они реагировали на оба фронта входных импульсов (т. е. нарастающий и спадающий фронт), а также можно настроить реакции на эти изменения – увеличение, уменьшение или отсутствие действий со счетчиком блока для каждого фронта. Сигнал уровня — это так называемый управляющий сигнал, который используется для управления режимом подсчета сигналов фронта, которые присоединены к одному и тому же каналу. Объединяя использование сигналов фронта и уровня, блок PCNT может работать как квадратурный декодер.

 

Количество PCNT модулей (PCNT units) в различных чипах семейства ESP32 следующее:

Таким образом, классические ESP32-чипы имеют 8 PCNT units, а более новые серии (S2, S3, C6) — 4 PCNT units.

Модуль PCNT можно использовать, например, в следующих случаях:

  • Расчет частоты периодического сигнала, путем подсчета количество импульсов в течение заданного временного отрезка: измерение скорости ветра, потока воды или любых других жидкостей с помощью импульсных счетчиков.
  • Аппаратная работа с вращающимися энкодерами.
  • Декодирование квадратурных сигналов в скорость и направление.

 


Обзор PCNT API

Подключить API счетчиков к вашей программе можно с помощью одной единственной строчки:

#include "driver/pulse_cnt.h"

 

Основные функций API счетчика импульсов (PCNT) можно разделить на несколько функциональных групп:

  • Создание дескрипторов и конфигурация модуля и каналов:
    Для работы с PCNT потребуются дескрипторы модуля (pcnt_unit_handle_t) и канала (pcnt_channel_handle_t). Их необходимо создать с помощью функций pcnt_new_unit() и pcnt_new_channel() с соответствующими структурами конфигурации.
    Подробнее о выделении ресурсов и конфигурации счетчиков

  • Включение счетчика и запуск / остановка счета:
  • Сброс и чтение значения счетчика:
  • Настройка фильтрации помех:
  • Работа с watch points:
    • pcnt_unit_add_watch_point() — позволяет добавить точку генерации события при достижении определённого значения счетчика
    • pcnt_unit_register_event_callbacks() — регистрирует callback-функции для обработки событий (например, достижения watch point)
      Точки наблюдения и события

  • Удаление и освобождение ресурсов:
    Если вы завершили работу со счетчиком и он вам более не понадобиться, необходимо освободить аппаратные ресурсы и память, вызвав соответствующие функции.

 


Настройка счетчика и выделение аппаратных ресурсов

Модуль и канал PCNT представлены переменными типа pcnt_unit_handle_t и pcnt_channel_handle_t соответственно. Все доступные для текущего чипа ESP32 модули и каналы поддерживаются драйвером в общем пуле ресурсов, поэтому вам не нужно знать точный идентификатор базового экземпляра.

Установка и настройка устройства PCNT unit

Для установки модуля PCNT необходимо заранее задать структуру конфигурации: pcnt_unit_config_t:

typedef struct {
    int low_limit;      /*!< Нижний предел счета, должен быть от -1 до -32767 включительно */
    int high_limit;     /*!< Верхний предел счета, должен быть от 1 до 32767 включительно */
    int intr_priority;  /*!< Приоритет прерывания PCNT: если установлено значение 0, драйвер попытается выделить прерывание с относительно низким приоритетом (1,2,3) */
    struct {
        uint32_t accum_count: 1; /*!< Нужно ли накапливать значение счетчика при переполнении на верхнем/нижнем пределе? */
#if SOC_PCNT_SUPPORT_STEP_NOTIFY
        uint32_t en_step_notify_up: 1;   /*!< Включить уведомление о шаге в положительном направлении */
        uint32_t en_step_notify_down: 1; /*!< Включить уведомление о шаге в отрицательном направлении */
#endif // SOC_PCNT_SUPPORT_STEP_NOTIFY
    } flags;       /*!< Extra опции */
} pcnt_unit_config_t; 

где:

  • low_limit и high_limit задают диапазон возможных значений для внутреннего аппаратного счётчика. Значение лимита не должно быть равно 0 и не должно превышать 32767, то есть для нижнего лимита значения должны быть в пределах -1~-32767, для верхнего 1~32767
    Счётчик автоматически сбрасывается в ноль при достижении верхнего или нижнего предела.
  • accum_count определяет, создавать ли внутренний аккумулятор для счётчика. Это полезно, если требуется расширить разрядность счётчика, которая определяется аппаратно и по умолчанию составляет не более 16 бит. См. также раздел «Компенсация потерь от переполнения», чтобы узнать, как использовать эту функцию.
  • intr_priority устанавливает приоритет прерывания. Если он равен 0, драйвер выделит прерывание с приоритетом по умолчанию, в противном случае драйвер будет использовать заданный приоритет.

Примечание. Поскольку все устройства PCNT используют один и тот же источник прерываний, при настройке нескольких устройств PCNT убедитесь, что приоритет прерывания intr_priority одинаков для каждого счетчика.

Выделение ресурсов и инициализация устройств счетчиков выполняется вызовом функции pcnt_new_unit() с pcnt_unit_config_t в качестве входного параметра. Функция вернет дескриптор устройства PCNT только при корректной работе. В частности, когда в пуле больше нет свободных аппаратных счетчиков PCNT (т.е. ресурсы счетчиков исчерпаны), эта функция вернет ошибку ESP_ERR_NOT_FOUND. Общее количество доступных устройств PCNT можно узнать с помощью SOC_PCNT_UNITS_PER_GROUP.

pcnt_unit_config_t unit_config = {0};
unit_config.high_limit = 100;
unit_config.low_limit = -100;

pcnt_unit_handle_t pcnt_unit = NULL;
ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &pcnt_unit));

Если ранее созданное устройство-счетчик PCNT больше не требуется, рекомендуется вернуть его в общий пул для повторного использования, вызвав функцию pcnt_del_unit(). Перед удалением устройства PCNT необходимо убедиться, что счетчик удовлетворяет двум условиям:

  • Счетчик находится в состоянии init, то есть он либо отключен функцией pcnt_unit_disable(), либо ещё не включен.
  • Все подключённые к данному счетчику каналы PCNT удалены функцией pcnt_del_channel().

 

Установка и настройка канала PCNT

Чтобы настроить канал PCNT, необходимо заполнить структуру pcnt_chan_config_t, а затем вызвать функцию pcnt_new_channel(). Поля конфигурации структуры pcnt_chan_config_t описаны ниже:

typedef struct {
    int edge_gpio_num;  /*!< Номер GPIO, используемый сигналом счёта. Установите в -1, если не используется. */
    int level_gpio_num; /*!< Номер GPIO, используемый сигналом управления. Установите в -1, если не используется. */
    struct {
        uint32_t invert_edge_input: 1;   /*!< Инвертировать входной сигнал счёта */
        uint32_t invert_level_input: 1;  /*!< Инвертировать входной управляющий сигнал */
        uint32_t virt_edge_io_level: 1;  /*!< Уровень для виртуального входа счёта: 0: низкий, 1: высокий. Действует только при значении edge_gpio_num -1. */
        uint32_t virt_level_io_level: 1; /*!< Уровень для виртуального входа управления: 0: низкий, 1: высокий. Действует только при значении level_gpio_num -1. */
        uint32_t io_loop_back: 1;        /*!< Для отладки/тестирования выходной сигнал GPIO также будет подаваться на входной тракт. 
                                              Обратите внимание, что этот флаг устарел и будет удалён в IDF версии 6.0. 
                                              Вместо этого можно настроить режим вывода, сначала вызвав gpio_config(), а затем настроив канал PCNT. 
                                              Необходимые настройки для использования входа PCNT будут добавлены. */
    } flags;                             /*!< Флаги конфигурации канала */
} pcnt_chan_config_t; 

где:

  • edge_gpio_num и level_gpio_num определяют номера GPIO, используемые сигналами типа «счетный фронт» и «управляющий уровень». Обратите внимание: любому из них можно присвоить значение -1, если он фактически не используется, и таким образом он будет считаться виртуальным входом.
    В некоторых простых приложениях подсчёта импульсов, где один из сигналов уровня или фронта всегда фиксирован (т.е. никогда не меняется), можно освободить GPIO, назначив его виртуальным входом при выделении канала. Установка сигнала уровня/фронта в качестве виртуального входа приводит к тому, что этот сигнал внутренне маршрутизируется на заданный фиксированный логический уровень «Высокий/Низкий», что позволяет сохранить GPIO для других целей.
    Параметры virt_edge_io_level и virt_level_io_level в этом случае определяют уровни виртуального ввода-вывода для входных сигналов. Обратите внимание: они действительны только в том случае, если edge_gpio_num или level_gpio_num присвоено значение -1.
  • Параметры invert_edge_input и invert_level_input используются для определения необходимости инвертирования входных сигналов перед их подачей на аппаратной устройство PCNT. Инвертирование выполняется матрицей GPIO, а не аппаратным обеспечением PCNT.

Выделение и инициализация каналов выполняется вызовом функции pcnt_new_channel() с указанным выше pcnt_chan_config_t в качестве входного параметра и дескриптором устройства PCNT, ранее созданным с помощью функции pcnt_new_unit(). Эта функция вернет дескриптор канала PCNT при корректной работе. В частности, когда в устройстве больше нет свободных каналов PCNT (т.е. ресурсы канала исчерпаны), функция вернет ошибку ESP_ERR_NOT_FOUND. Общее количество доступных каналов PCNT в устройстве определено в SOC_PCNT_CHANNELS_PER_UNIT. Обратите внимание, что при установке канала PCNT для конкретного устройства необходимо убедиться, что устройство находится в состоянии init, в противном случае функция вернет ошибку ESP_ERR_INVALID_STATE.

#define EXAMPLE_CHAN_GPIO_A 0
#define EXAMPLE_CHAN_GPIO_B 2

pcnt_chan_config_t chan_config = {0};
chan_config.edge_gpio_num = EXAMPLE_CHAN_GPIO_A;
chan_config.level_gpio_num = EXAMPLE_CHAN_GPIO_B;

pcnt_channel_handle_t pcnt_chan = NULL;
ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &chan_config, &pcnt_chan));

Если ранее созданный канал PCNT больше не нужен, рекомендуется освободить аппаратные ресурсы, вызвав функцию pcnt_del_channel(). Это, в свою очередь, позволит использовать аппаратное обеспечение канала для других целей.

Для GPIO, задействованных в PCNT, можно настроить подтягивание к питанию или земле после инициализации PCNT с использованием таких функций, как gpio_pullup_en() и gpio_pullup_dis().

 


Настройка действий для канала

PCNT может увеличивать, уменьшать или сохранять внутреннее значение счётчика при переключении уровня импульсного сигнала на входе. Вы можете задать различные действия для фронта сигнала и/или уровня сигнала.

Функция pcnt_channel_set_edge_action() задаёт определённые действия для нарастающего и спадающего фронта сигнала, подключенного к edge_gpio_num. Поддерживаемые действия перечислены в pcnt_channel_edge_action_t.

typedef enum {
    PCNT_CHANNEL_EDGE_ACTION_HOLD,     /*!< Не изменять значение счетчика */
    PCNT_CHANNEL_EDGE_ACTION_INCREASE, /*!< Увеличить значение счетчика */
    PCNT_CHANNEL_EDGE_ACTION_DECREASE, /*!< Уменьшить значение счетчика */
} pcnt_channel_edge_action_t; 

 

Функция pcnt_channel_set_level_action() задаёт определённые действия для высокого и низкого уровня сигнала, подключенного к level_gpio_num. Поддерживаемые действия перечислены в pcnt_channel_level_action_t. Эта функция не является обязательной, если level_gpio_num установлено в -1 при выделении канала PCNT функцией pcnt_new_channel().

typedef enum {
    PCNT_CHANNEL_LEVEL_ACTION_KEEP,    /*!< Сохранить текущий режим подсчета */
    PCNT_CHANNEL_LEVEL_ACTION_INVERSE, /*!< Инвертировать текущий режим счета (увеличение -> уменьшение, уменьшение -> увеличение) */
    PCNT_CHANNEL_LEVEL_ACTION_HOLD,    /*!< Удерживать текущее значение счетчика (заблокировать счет) */
} pcnt_channel_level_action_t; 

Примеры использования:

// Уменьшить счетчик по переднему фронту, увеличить счетчик по заднему фронту
ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE));

// Сохраняйте режим счета, когда уровень управляющего сигнала высокий, и переключайтесь на обратный режим счета, когда уровень управляющего сигнала низкий
ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE));

 


Watch Points

Каждый блок PCNT можно настроить на отслеживание нескольких различных значений, которые вас интересуют. Отслеживаемое значение также называется Watch Points или точкой наблюдения. Значение самой точки наблюдения не может выходить за пределы диапазона, установленного в pcnt_unit_config_t с помощью low_limit и high_limit. Когда счётчик достигает любой из заданных точек наблюдения, генерируется событие наблюдения, которое уведомляет вас прерыванием, если какой-либо callback для события наблюдения был когда-либо зарегистрирован через pcnt_unit_register_event_callbacks()

Точку наблюдения можно добавлять и удалять с помощью функций pcnt_unit_add_watch_point() и pcnt_unit_remove_watch_point(). Наиболее часто используемые точки наблюдения: пересечение нуля, максимальное или минимальное значение счётчика и другие пороговые значения. Количество доступных точек наблюдения ограничено. Функция pcnt_unit_add_watch_point() вернёт ошибку ESP_ERR_NOT_FOUND, если не найдёт свободного аппаратного ресурса для сохранения точки наблюдения. Нельзя добавлять одну и ту же точку наблюдения несколько раз, в противном случае будет возвращена ошибка ESP_ERR_INVALID_STATE.

Важно! Из-за аппаратных ограничений после добавления точки наблюдения необходимо вызвать pcnt_unit_clear_count(), чтобы изменения вступили в силу.

// Добавить контрольную точку на 0
ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, 0));
// Добавить точку наблюдения на верхний предел
ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, EXAMPLE_PCNT_HIGH_LIMIT));

Рекомендуется удалить неиспользуемую точку наблюдения с помощью функции pcnt_unit_remove_watch_point() для повторного использования ресурсов.

 


Регистрация callback функций для событий

Когда счетчик PCNT достигает любой из заданных точек наблюдения, генерируется прерывание. Если вам необходимо реагировать на это прерывание, необходимо создать функцию обратного вызова, которая должна выполняться при возникновении события, и подключить её к процедуре обработки прерываний, вызвав pcnt_unit_register_event_callbacks(). Все поддерживаемые обратные вызовы событий перечислены в pcnt_event_callbacks_t:

typedef struct {
    pcnt_watch_cb_t on_reach; /*!< Вызывается, когда счётчик PCNT достигает любой точки наблюдения или уведомляет о каждом шаге*/
} pcnt_event_callbacks_t;
 

где:

  • on_reach устанавливает функцию обратного вызова для события точки наблюдения. Поскольку эта функция вызывается в контексте ISR, необходимо убедиться, что функция не пытается блокироваться (например, убедившись, что внутри функции вызываются только API FreeRTOS с суффиксом ISR). Прототип функции объявлен в pcnt_watch_cb_t.
typedef bool (*pcnt_watch_cb_t)(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx);

Вы можете передать в pcnt_unit_register_event_callbacks() обработчик собственный контекст с помощью параметра user_ctx. Эти пользовательские данные будут напрямую переданы в функции обратного вызова.

В функции обратного вызова драйвер заполняет данные о событии. Например, данные о событии точки наблюдения или шага наблюдения объявляются как pcnt_watch_event_data_t:

typedef struct {
    int watch_point_value;                       /*!< Значение точки наблюдения, вызвавшее событие */
    pcnt_unit_zero_cross_mode_t zero_cross_mode; /*!< Режим пересечения нуля */
} pcnt_watch_event_data_t;  

где:

  • watch_point_value содержит значение счётчика при возникновении события.
  • zero_cross_mode содержит информацию о том, как модуль PCNT пересекал нулевую точку в последний раз. Возможные режимы пересечения нуля перечислены в pcnt_unit_zero_cross_mode_t. Обычно разные режимы пересечения нуля означают разные направления счёта и размер шага счёта.

Регистрация функции обратного вызова приводит к отложенной установке обслуживания прерываний, поэтому эту функцию следует вызывать только до включения модуля функцией pcnt_unit_enable(). В противном случае может быть возвращена ошибка ESP_ERR_INVALID_STATE.

static bool example_pcnt_on_reach(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx)
{
    BaseType_t high_task_wakeup;
    QueueHandle_t queue = (QueueHandle_t)user_ctx;
    // Отправить значение точки наблюдения в очередь
    xQueueSendFromISR(queue, &(edata->watch_point_value), &high_task_wakeup);
    // Проверяем, была ли эта функция активизирована высокоприоритетной задачей
    return (high_task_wakeup == pdTRUE);
}

pcnt_event_callbacks_t cbs = {
    .on_reach = example_pcnt_on_reach,
};
QueueHandle_t queue = xQueueCreate(10, sizeof(int));
ESP_ERROR_CHECK(pcnt_unit_register_event_callbacks(pcnt_unit, &cbs, queue));

 


Установка фильтра коротких помех

Каждый блок PCNT оснащен цифровыми фильтрами для подавления возможных коротких помех в сигналах, которые могут возникать, например, из-за дребезга контактов. Параметры, которые можно настроить для фильтра помех, перечислены в разделе pcnt_glitch_filter_config_t:

typedef struct {
    uint32_t max_glitch_ns; /*!< Ширина импульса, меньшая этого порога, будет рассматриваться как сбой и игнорироваться в единицах нс. */
} pcnt_glitch_filter_config_t; 

где:

  • max_glitch_ns – устанавливает максимальную длительность импульса помехи в наносекундах. Если длительность импульса сигнала меньше этого значения, он будет рассматриваться как шум и не будет влиять на значение внутреннего счётчика.

Вы можете включить фильтр помех для блока PCNT, вызвав pcnt_unit_set_glitch_filter() и передав конфигурацию фильтра, указанную выше. Вы также можете отключить фильтр помех позже, вызвав эту же функцию с конфигурацией NULL.

Эту функцию следует вызывать, когда устройство находится в состоянии init. В противном случае будет возвращена ошибка ESP_ERR_INVALID_STATE.

pcnt_glitch_filter_config_t filter_config = {
    .max_glitch_ns = 1000,
};
ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(pcnt_unit, &filter_config));

Примечание

Фильтр помех работает с использованием источника тактовой частоты APB. Чтобы счётчик не пропускал импульсы, максимальная длительность помехи должна быть больше одного цикла APB_CLK (обычно 12,5 нс при частоте APB 80 МГц). Поскольку частота APB может изменяться при динамическом масштабировании частоты (DFS), фильтр может работать некорректно. Поэтому драйвер PCNT может устанавливать блокировку управления питанием для каждого блока PCNT. Подробнее о стратегии управления питанием, используемой в драйвере PCNT, см. в разделе «Управление питанием».

 


Включение и отключение модуля счетчика

Прежде чем управлять счетом импульсов на каждом модуле PCNT, необходимо сначала включить его, вызвав pcnt_unit_enable(). Внутри эта функция выглядит так:

  • Переключает состояние драйвера PCNT с init на enable.
  • Включает службу прерываний, если она была ранее отложенно установлена с помощью pcnt_unit_register_event_callbacks().
  • Если выполнение прошло успешно, устанавливает блокировку управления питанием. Дополнительную информацию см. в разделе «Управление питанием».

Напротив, вызов  pcnt_unit_disable() приведёт к обратному результату, то есть вернёт драйвер PCNT в состояние init, отключит службу прерываний и снимет блокировку управления питанием.

 


Запуск/остановка счёта, сброс и чтение значения счётчика

Вызов pcnt_unit_start() запускает работу блока PCNT, после чего он начнет увеличивать или уменьшать счётчик в соответствии с входящими импульсными сигналами. Для остановки счетчика вызовите pcnt_unit_stop(). Обратите внимание: функции pcnt_unit_start() и pcnt_unit_stop() следует вызывать только после включения блока функцией pcnt_unit_enable(). В противном случае будет возвращена ошибка ESP_ERR_INVALID_STATE.

Сброс значения счётчика возможна с помощью вызова pcnt_unit_clear_count().

Вы можете считать текущее значение счётчика в любой момент, вызвав функцию pcnt_unit_get_count(). Возвращаемое значение — это целое число со знаком, который используется для указания направления.

int pulse_count = 0;
ESP_ERROR_CHECK(pcnt_unit_get_count(pcnt_unit, &pulse_count));

 


Компенсация переполнения

Внутренний аппаратный счётчик автоматически обнуляется при достижении верхнего или нижнего предела. Чтобы компенсировать эту потерю значения счётчика и увеличить разрядность счётчика, можно:

  1. Включить опцию accum_count при установке модуля PCNT.
  2. Добавить верхний/нижний предел в качестве контрольных точек.
  3. Теперь возвращаемое значение счётчика функцией pcnt_unit_get_count() не только отражает реальное аппаратное значение счётчика, но и суммирует с ним значения, сброшенные при переполнениях.

Примечание: pcnt_unit_clear_count() также сбросит и накопленное значение счётчика переполнений.

 


Управление питанием

Когда управление питанием для PCNT включено (т.е. опция CONFIG_PM_ENABLE включена), система автоматически корректирует опорную частоту APB перед переходом в режим лёгкого сна, что может привести к тому, что фильтр помех PCNT начнет ошибочно интерпретировать нормальные сигналы как шум. Чтобы предотвратить это, драйвер устанавливает блокировку управления питанием APB типа ESP_PM_APB_FREQ_MAX, гарантируя неизменность опорной частоты APB. Эта блокировка устанавливается при включении модуля PCNT через pcnt_unit_enable() и снимается при его отключении через pcnt_unit_disable().

 


Безопасность IRAM

По умолчанию прерывание PCNT будет отложено при отключении кэша, например, при записи или стирании Flash-памяти. Таким образом, прерывание счетчика не будет выполнено вовремя, что может быть не допустимым в приложениях реального времени.

Существует параметр Kconfig CONFIG_PCNT_ISR_IRAM_SAFE, который:

  • Включает обработку прерывания PCNT даже при отключенном кэше
  • Помещает все функции, используемые обработчиком прерывания (ISR) драйвера PCNT, в IRAM
  • Помещает объект драйвера в DRAM (на случай, если он случайно будет отображен в PSRAM)

Это позволяет прерыванию выполняться при отключенном кэше, но приводит к увеличению потребления IRAM.

Есть ещё один параметр Kconfig CONFIG_PCNT_CTRL_FUNC_IN_IRAM, который также позволяет помещать часто используемые функции управления PCNT в IRAM. Таким образом, перечисленные ниже функции могут быть выполнены и при отключенном кэше:

  • pcnt_unit_start()
  • pcnt_unit_stop()
  • pcnt_unit_clear_count()
  • pcnt_unit_get_count()

 


Потокобезопасность

Драйвер гарантирует потокобезопасность функций pcnt_new_unit() и pcnt_new_channel(), что означает, что их можно вызывать из разных задач RTOS без защиты дополнительными блокировками.

Следующие функции могут выполняться в контексте ISR. Драйвер использует критическую секцию, чтобы предотвратить их одновременный вызов как в задаче, так и в ISR.

  • pcnt_unit_start()
  • pcnt_unit_stop()
  • pcnt_unit_clear_count()
  • pcnt_unit_get_count()

Другие функции, принимающие pcnt_unit_handle_t и pcnt_channel_handle_t в качестве первого позиционного параметра, не считаются потокобезопасными. Это означает, что следует избегать их вызова из нескольких задач.

 


Параметры Kconfig

CONFIG_PCNT_CTRL_FUNC_IN_IRAM управляет размещением функций управления PCNT (IRAM или Flash). Подробнее см. выше в разделе «Безопасность IRAM».

CONFIG_PCNT_ISR_IRAM_SAFE управляет работой обработчика ISR по умолчанию при отключенном кэше. Подробнее см. выше в разделе «Безопасность IRAM».

CONFIG_PCNT_ENABLE_DEBUG_LOG используется для включения вывода журнала отладки PCNT. Включение этого параметра увеличивает размер двоичного файла прошивки.

 


Практические примеры

Конечно, посчитать количество импульсов на входе можно и без этих всяких специальных контроллеров – просто добавив прерывание на вход и таймер для “гашения” помех от дребезга контактов. Да так в 90% случаев все и делают! Но зачем?????

Ведь задействовав аппаратный контроллер PCNT, мы получим сплошные выгоды:

  • освободим CPU от необходимости реагировать на прерывания по каждом фронту / спаду импульса и считать их
  • нет необходимости в debounce-таймере, так как фильтрация осуществляется аппаратными средствами
  • аппаратный счетчик не зависит он FreeRTOS и многозадачности, поэтому сам счет будет вестись без пропусков и задержек

Недостаток я вижу только один:

  • необходимость освоения еще одного API ESP-IDF

 

1. Счетчик расхода воды / измеритель скорости ветра

Наиболее частая практическая задача, которая напрямую связана с подсчетом импульсов – измерение количества и (или) скорости количества расхода воды, воздуха или других сред с помощью различных импульсных датчиков. Для этого вам потребуется счетчик или анемометр с импульсным выходом:

 

Для каждого такого счетчика необходимо знать коэффициент пересчета количества импульсов на объем измеряемой среды (или скорости ветра) – K, например это может быть 25 мл  на импульс. Обычно эта информация указана в паспорте счетчика. Таким образом, объем перекачиваемой жидкости (или иной среды) можно узнать, просто умножив количество импульсов на этот коэффициент. Скорость потока можно вычислить, подсчитав количество импульсов за определённый интервал времени, например за минуту с учетом коэффициента пересчета.

Пример простой программы, которая считает скорость потока в секунду, приведен ниже:

#include "driver/pulse_cnt.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define PCNT_INPUT_IO 4        // GPIO, к которому подключён датчик 
#define PCNT_INPUT_COEF 2.25   // Коэффициент пересчета 2.25 мл на 1 импульс
#define PCNT_GLITCH_MAX 1000   // Длительность помехи в нс
#define PCNT_H_LIM_VAL 32767   // Максимальное значение счетчика 
#define PCNT_L_LIM_VAL -1      // Минимальное значение счетчика 

void app_main(void)
{
    // Шаг 1. Создаём PCNT unit
    pcnt_unit_config_t unit_config = {0};
    unit_config.high_limit = PCNT_H_LIM_VAL;        // Максимальное значение счетчика
    unit_config.low_limit = PCNT_L_LIM_VAL;         // Минимальное значение счетчика

    pcnt_unit_handle_t pcnt_unit = NULL;
    ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &pcnt_unit));

    // Шаг 2. Создаём PCNT channel
    pcnt_chan_config_t channel_config = {0};
    channel_config.edge_gpio_num = PCNT_INPUT_IO;   // GPIO, к которому подключён датчик 
    channel_config.level_gpio_num = -1;             // Не используется

    pcnt_channel_handle_t pcnt_channel = NULL;
    ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &channel_config, &pcnt_channel));

    // Шаг 3 (опционально). Настраиваем встроенную подтяжку GPIO к земле (или питанию - выберите нужное)
    ESP_ERROR_CHECK(gpio_set_pull_mode((gpio_num_t)PCNT_INPUT_IO, GPIO_PULLDOWN_ONLY));

    // Шаг 4 (опционально). Включаем фильтр debounce
    pcnt_glitch_filter_config_t filter_config = {0};
    filter_config.max_glitch_ns = PCNT_GLITCH_MAX;
    
    ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(pcnt_unit, &filter_config));

    // Шаг 5. Включаем unit и запускаем счёт
    ESP_ERROR_CHECK(pcnt_unit_enable(pcnt_unit));
    ESP_ERROR_CHECK(pcnt_unit_start(pcnt_unit));

    while (1) {
        int count = 0; 
        // Считываем значение счетчика
        ESP_ERROR_CHECK(pcnt_unit_get_count(pcnt_unit, &count));
        // Сбросить счётчик для следующего измерения
        ESP_ERROR_CHECK(pcnt_unit_clear_count(pcnt_unit));

        // Пересчитываем в скорость потока
        float flow_ml_per_sec = (float)count * PCNT_INPUT_COEF; 
        ESP_LOGI("FLOW", "Flow: %d pulses/sec, %.2f ml/sec", count, flow_ml_per_sec); 

        // Ждем заданный интервал времени
        vTaskDelay(pdMS_TO_TICKS(1000)); // 1 секунда
    }
}

Здесь мы подразумеваем, что выхода из основного цикла не будет, поэтому не освобождаем занятые ресурсы. Но если теоретически это может произойти, необходимо будет добавить код, который удалит pcnt_channel и pcnt_unit.

 


2. Счетчик количества импульсов с таймаутом

Допустим у нас некое устройство, которое выдает некое значение в виде пачки импульсов. В количестве импульсов закодированы какие-то данные. Причем “прилететь” такая пачка может когда угодно – период повторения пачек может быть произвольным. Например это могут быть данные, передаваемые по проводному или радио-каналу. Длительность каждого отдельного импульса в данном случае нам не важна. 

Необходимо среагировать на первый импульс, посчитать их количество, и по окончании пачки уведомить программу о полученном количестве импульсов.

Порядок решения данной задачи может быть следующим:

  1. Настроить счет импульсов как это было сделано в предыдущем примере
  2. Создать программный таймер с периодом, заведомо превышающим длительность одного импульса
  3. Добавить watch point с количеством импульсов, равным 1
  4. В обработчике прерывания для события watch point запустить однократный программный таймер. Таким образом, с приходом каждого последующего импульса в одной пачке таймер будет перезапускаться.
  5.  При срабатывании обработчика обратного вызова таймера понимаем, что пачка окончена и мы можем считать значение счетчика и сбросить счетчик. Результат можно передать в любую другую задачу с помощью очереди или посредством событий.

В демонстрационном примере ниже я использовал очередь, значения из которой считываются и выводятся в лог после завершения всех необходимых манипуляций. Однако вы можете использовать любой другой метод (даже отправить на MQTT, например, так как callback для таймера вызывается из задачи таймера, а не из прерывания).

#include "driver/pulse_cnt.h"
#include "driver/gpio.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

#define PCNT_INPUT_IO 4          // GPIO, к которому подключён источник импульсов 
#define PCNT_GLITCH_MAX 1000     // Длительность помехи в нс
#define PCNT_H_LIM_VAL 32767     // Максимальное значение счетчика 
#define PCNT_L_LIM_VAL -1        // Минимальное значение счетчика 
#define TIMER_TIMEOUT_US 1000000 // Таймаут обнаружения конца пачки, например 1 секунда (больше == надежнее, но задержка может мешать)

// Таймер обнаружения конца пачки
static esp_timer_handle_t batch_timer;

// Какая-то очередь, в которую мы будем скидывать результаты, она не обязательно должна быть объявлена здесь
static QueueHandle_t pcnt_evt_queue;

// Обработчик события PCNT watch point
static bool pcnt_on_reach(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx)
{
    // Перезапустим таймер пачки
    esp_timer_stop(batch_timer);
    esp_timer_start_once(batch_timer, TIMER_TIMEOUT_US);
    return false;
}

// Обработчик таймера окончания пачки
static void batch_timer_callback(void* arg)
{
    // Считываем значение счетчика
    int pulse_count = 0;
    pcnt_unit_get_count((pcnt_unit_handle_t)arg, &pulse_count);
    // Сбрасываем счётчик
    pcnt_unit_clear_count((pcnt_unit_handle_t)arg);
    // Отправить результат, например в очередь
    xQueueSend(pcnt_evt_queue, &pulse_count, 0);
}

void app_main(void)
{
    // Создадим очередь для передачи результата (это может быть где-то в другой задаче)
    pcnt_evt_queue = xQueueCreate(10, sizeof(int));
    
    // Шаг 1. Создаём PCNT unit
    pcnt_unit_config_t unit_config = {0};
    unit_config.high_limit = PCNT_H_LIM_VAL;        // Максимальное значение счетчика
    unit_config.low_limit = PCNT_L_LIM_VAL;         // Минимальное значение счетчика
    pcnt_unit_handle_t pcnt_unit = NULL;
    ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &pcnt_unit));

    // Шаг 2 (опционально). Включаем фильтр коротких помех в канале связи
    pcnt_glitch_filter_config_t filter_config = {0};
    filter_config.max_glitch_ns = PCNT_GLITCH_MAX;
    ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(pcnt_unit, &filter_config));

    // Шаг 3. Создаём PCNT channel и настроим счёт по фронтам импульса
    pcnt_chan_config_t channel_config = {0};
    channel_config.edge_gpio_num = PCNT_INPUT_IO;   // GPIO, к которому подключён источник импульсов
    channel_config.level_gpio_num = -1;             // Не используется
    pcnt_channel_handle_t pcnt_channel = NULL;
    ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &channel_config, &pcnt_channel));
    ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_channel, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_HOLD));

    // Шаг 4. Настраиваем watch point и активируем его
    ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, 1));
    ESP_ERROR_CHECK(pcnt_unit_clear_count(pcnt_unit));

    // Шаг 5. Зарегистрируем callback на созданный watch point
    pcnt_event_callbacks_t cbs = {0};
    cbs.on_reach = pcnt_on_reach;
    ESP_ERROR_CHECK(pcnt_unit_register_event_callbacks(pcnt_unit, &cbs, NULL));

    // Шаг 6. Создадим таймер для отслеживания окончания пачки
    esp_timer_create_args_t timer_args = {0};
    timer_args.callback = batch_timer_callback;
    timer_args.arg = (void*)pcnt_unit;
    timer_args.name = "batch_timer";
    ESP_ERROR_CHECK(esp_timer_create(&timer_args, &batch_timer));

    // Шаг 7. Включаем unit и запускаем счётчик
    ESP_ERROR_CHECK(pcnt_unit_enable(pcnt_unit));
    ESP_ERROR_CHECK(pcnt_unit_start(pcnt_unit));

    // Ожидание результата (это может быть где-то в другой задаче)
    int result = 0;
    while (1) {
        // Здесь result — количество импульсов в пачке
        if (xQueueReceive(pcnt_evt_queue, &result, portMAX_DELAY)) {
            ESP_LOGI("PCNT", "Recieved %d pulses", result);
        }
    }
}

Примечание: после вызова pcnt_unit_add_watch_point() нужно обязательно вызвать pcnt_unit_clear_count(), но сделать это можно и позже, например непосредственно pcnt_unit_start(). В данном примере я сделал так, в следующем – по другому. С той же целью я переместил настройку фильтра помех на один шаг выше – потому что так тоже можно. А вот в остальном последовательность лучше не нарушать.

Проверим… все работает как часы (оно и понятно, таймер “внутри”):

I (280) main_task: Started on CPU0
I (290) main_task: Calling app_main()
I (294520) PCNT: Recieved 15 pulses
I (297300) PCNT: Recieved 6 pulses
I (300270) PCNT: Recieved 16 pulses

 


3. Обработка данных с энкодера

Ещё один пример для эффективного применения счетчика импульсов – обработка сигналов с повортного энкодера. С его помощью можно удобно управлять устройствами, например перемещением по экранному меню.

Принцип работы энкодера заключается в том, что при вращении он выдает импульсы сразу на два выхода А и В, но сигналы с которых смещены на четверть периода. По тому, какой импульс (в канале А или в канале В) приходит первым – можно судить о направлении вращения, а по количеству импульсов – о величине поворота.

Источник изображения: Яндекс Картинки

Идея здесь заключается в одновременном использовании двух каналов счёта, подключенных к одному и тому же счётчику:

// Шаг 3.1. Создаём PCNT канал А
pcnt_chan_config_t chan_a_config = {
    .edge_gpio_num = EXAMPLE_EC11_GPIO_A,
    .level_gpio_num = EXAMPLE_EC11_GPIO_B,
};
pcnt_channel_handle_t pcnt_chan_a = NULL;
ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &chan_a_config, &pcnt_chan_a));

// Шаг 3.2. Создаём PCNT канал B
pcnt_chan_config_t chan_b_config = {
    .edge_gpio_num = EXAMPLE_EC11_GPIO_B,
    .level_gpio_num = EXAMPLE_EC11_GPIO_A,
};
pcnt_channel_handle_t pcnt_chan_b = NULL;
ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &chan_b_config, &pcnt_chan_b));
  • В первом канале GPIO A является счетным, а GPIO B – управляющим (например). При этом фронты импульсов на входе GPIO A приводят к уменьшению значения счетчика, а спады – к его увеличению. А на управляющем входе GPIO B фронт импульса ничего не меняет, а вот спад меняет направление счета на противоположное.
  • Во втором канале тот же самый GPIO B является счетным, а GPIO A – уже управляющим. При этом реакция на фронты импульсов изменилась на противоположную: на входе GPIO B приводят к увеличению значения счетчика, а спады – к его уменьшению. А на управляющем входе GPIO А ничего не изменилось: фронт импульса ничего не меняет, а вот спад меняет направление счета на противоположное.

Изначально в примере от Espressif было настроено так:

// Канал А
pcnt_channel_set_edge_action(pcnt_chan_a, 
  PCNT_CHANNEL_EDGE_ACTION_DECREASE,     // По фронту GPIO_A уменьшаем значение счетчика
  PCNT_CHANNEL_EDGE_ACTION_INCREASE);    // По спаду GPIO_A увеличиваем значение счетчика
pcnt_channel_set_level_action(pcnt_chan_a, 
  PCNT_CHANNEL_LEVEL_ACTION_KEEP,        // По фронту GPIO_B ничего не меняем
  PCNT_CHANNEL_LEVEL_ACTION_INVERSE);    // По спаду GPIO_B меняем направление счета на противоположное

// Канал B
pcnt_channel_set_edge_action(pcnt_chan_b, 
  PCNT_CHANNEL_EDGE_ACTION_INCREASE,     // По фронту GPIO_B увеличиваем значение счетчика
  PCNT_CHANNEL_EDGE_ACTION_DECREASE);    // По спаду GPIO_B уменьшаем значение счетчика
pcnt_channel_set_level_action(pcnt_chan_b, 
  PCNT_CHANNEL_LEVEL_ACTION_KEEP,        // По фронту GPIO_A ничего не меняем
  PCNT_CHANNEL_LEVEL_ACTION_INVERSE);    // По спаду GPIO_A меняем направление счета на противоположное

Но такое поведение не очень корректно работает – ниже вы увидите почему.

Поэтому я сделал так:

// Канал А
ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_a, 
  PCNT_CHANNEL_EDGE_ACTION_DECREASE,    // По фронту GPIO_A уменьшаем значение счетчика
  PCNT_CHANNEL_EDGE_ACTION_HOLD));      // По спаду GPIO_A ничего не делаем
ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_a, 
  PCNT_CHANNEL_LEVEL_ACTION_KEEP,       // По фронту GPIO_B cчитаем
  PCNT_CHANNEL_LEVEL_ACTION_HOLD));     // По фронту GPIO_B не cчитаем

// Канал B
ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_b, 
  PCNT_CHANNEL_EDGE_ACTION_INCREASE,    // По фронту GPIO_B уменьшаем значение счетчика
  PCNT_CHANNEL_EDGE_ACTION_HOLD));      // По спаду GPIO_B ничего не делаем
ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_b, 
  PCNT_CHANNEL_LEVEL_ACTION_KEEP,       // По фронту GPIO_A cчитаем
  PCNT_CHANNEL_LEVEL_ACTION_HOLD));     // По фронту GPIO_A не cчитаем

Суть работы проста как две копейки – “кто первый встал, того и тапки“. Пришедший первым импульс блокирует считает в одном канале, и одновременно блокирует противоположный канал.

Реагировать на импульсы с энкодера мы можем с помощью watch point, настроенный на единицу и минус единицу – тогда обработчик будет генерировать прерывание при каждом отдельном шаге энкодера. Либо можно использовать подход, изложенный в предыдущем примере – тогда callback будет вызываться уже после того, как энкодер будет повернут на один или несколько шагов. Как вам удобнее – думайте сами.

Пример для этого варианта есть в документации, но мне он не очень понравился:

#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/pulse_cnt.h"
#include "driver/gpio.h"

static const char *TAG = "PCNT";

#define EXAMPLE_PCNT_HIGH_LIMIT 100
#define EXAMPLE_PCNT_LOW_LIMIT  -100

#define EXAMPLE_EC11_GPIO_A 4
#define EXAMPLE_EC11_GPIO_B 16

// Какая-то очередь, в которую мы будем скидывать результаты, она не обязательно должна быть объявлена здесь
static QueueHandle_t pcnt_evt_queue;

// Обработчик события PCNT watch point
static bool pcnt_on_reach(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx)
{
    // Считываем значение счетчика
    int pulse_count = 0;
    pcnt_unit_get_count(unit, &pulse_count);
    // Сбрасываем счётчик
    pcnt_unit_clear_count(unit);
    // Отправить результат, например в очередь
    xQueueSend(pcnt_evt_queue, &pulse_count, 0);
    return false;
}

void app_main(void)
{
    // Создадим очередь для передачи результата (это может быть где-то в другой задаче)
    pcnt_evt_queue = xQueueCreate(10, sizeof(int));

    // Шаг 1. Создаём PCNT unit
    ESP_LOGI(TAG, "install pcnt unit");
    pcnt_unit_config_t unit_config = {
        .high_limit = EXAMPLE_PCNT_HIGH_LIMIT,
        .low_limit = EXAMPLE_PCNT_LOW_LIMIT,
    };
    pcnt_unit_handle_t pcnt_unit = NULL;
    ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &pcnt_unit));

    // Шаг 2. Включаем фильтр debounce
    ESP_LOGI(TAG, "set glitch filter");
    pcnt_glitch_filter_config_t filter_config = {
        .max_glitch_ns = 1000,
    };
    ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(pcnt_unit, &filter_config));

    // Шаг 3. Создаём PCNT каналы
    ESP_LOGI(TAG, "install pcnt channels");

    // Шаг 3.1. Создаём PCNT канал А
    pcnt_chan_config_t chan_a_config = {
        .edge_gpio_num = EXAMPLE_EC11_GPIO_A,
        .level_gpio_num = EXAMPLE_EC11_GPIO_B,
    };
    pcnt_channel_handle_t pcnt_chan_a = NULL;
    ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &chan_a_config, &pcnt_chan_a));

    // Шаг 3.2. Создаём PCNT канал B
    pcnt_chan_config_t chan_b_config = {
        .edge_gpio_num = EXAMPLE_EC11_GPIO_B,
        .level_gpio_num = EXAMPLE_EC11_GPIO_A,
    };
    pcnt_channel_handle_t pcnt_chan_b = NULL;
    ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &chan_b_config, &pcnt_chan_b));

    // Шаг 4. Задаем реакции на импульсы в разных каналах
    ESP_LOGI(TAG, "set edge and level actions for pcnt channels");
    ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_a, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE));
    ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_a, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE));
    ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_b, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE));
    ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_b, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE));

    // Шаг 5. Настраиваем watch point-ы
    ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, 1));
    ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, -1));

    // Шаг 6. Зарегистрируем callback на созданный watch point
    pcnt_event_callbacks_t cbs = {0};
    cbs.on_reach = pcnt_on_reach;
    ESP_ERROR_CHECK(pcnt_unit_register_event_callbacks(pcnt_unit, &cbs, NULL));

    // Шаг 7. Включаем и запускаем счетчик
    ESP_LOGI(TAG, "enable pcnt unit");
    ESP_ERROR_CHECK(pcnt_unit_enable(pcnt_unit));
    ESP_LOGI(TAG, "clear pcnt unit");
    ESP_ERROR_CHECK(pcnt_unit_clear_count(pcnt_unit));
    ESP_LOGI(TAG, "start pcnt unit");
    ESP_ERROR_CHECK(pcnt_unit_start(pcnt_unit));

    // Ожидание результата (это может быть где-то в другой задаче)
    int result = 0;
    while (1) {
        if (xQueueReceive(pcnt_evt_queue, &result, portMAX_DELAY)) {
            ESP_LOGI("PCNT", "Rotation: %d", result);
        }
    }
}

При проверке выяснилось, что на каждый одиночный поворот рукоятки энкодера последний выдает 4 импульса:

I (279) main_task: Started on CPU0
I (289) main_task: Calling app_main()
I (289) PCNT: install pcnt unit
I (289) PCNT: set glitch filter
I (289) PCNT: install pcnt channels
I (289) PCNT: set edge and level actions for pcnt channels
I (299) PCNT: enable pcnt unit
I (299) PCNT: clear pcnt unit
I (299) PCNT: start pcnt unit
I (2179) PCNT: Rotation: 1
I (2219) PCNT: Rotation: 1
I (2239) PCNT: Rotation: 1
I (2249) PCNT: Rotation: 1
I (3029) PCNT: Rotation: -1
I (3089) PCNT: Rotation: -1
I (3099) PCNT: Rotation: -1
I (3109) PCNT: Rotation: -1
I (3939) PCNT: Rotation: 1
I (3999) PCNT: Rotation: 1
I (4009) PCNT: Rotation: 1
I (4019) PCNT: Rotation: 1

Оно и понятно – китайцы слишком намутили в шаге 4.

Мой вариант:

#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/pulse_cnt.h"
#include "driver/gpio.h"

static const char *TAG = "PCNT";

#define EXAMPLE_PCNT_HIGH_LIMIT 100
#define EXAMPLE_PCNT_LOW_LIMIT  -100

#define EXAMPLE_EC11_GPIO_A 4
#define EXAMPLE_EC11_GPIO_B 16

// Какая-то очередь, в которую мы будем скидывать результаты, она не обязательно должна быть объявлена здесь
static QueueHandle_t pcnt_evt_queue;

// Обработчик события PCNT watch point
static bool pcnt_on_reach(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx)
{
    // Считываем значение счетчика
    int pulse_count = 0;
    pcnt_unit_get_count(unit, &pulse_count);
    // Сбрасываем счётчик
    pcnt_unit_clear_count(unit);
    // Отправить результат, например в очередь
    xQueueSend(pcnt_evt_queue, &pulse_count, 0);
    return false;
}

void app_main(void)
{
    // Создадим очередь для передачи результата (это может быть где-то в другой задаче)
    pcnt_evt_queue = xQueueCreate(10, sizeof(int));

    // Шаг 1. Создаём PCNT unit
    ESP_LOGI(TAG, "install pcnt unit");
    pcnt_unit_config_t unit_config = {
        .high_limit = EXAMPLE_PCNT_HIGH_LIMIT,
        .low_limit = EXAMPLE_PCNT_LOW_LIMIT,
    };
    pcnt_unit_handle_t pcnt_unit = NULL;
    ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &pcnt_unit));

    // Шаг 2. Включаем фильтр debounce
    ESP_LOGI(TAG, "set glitch filter");
    pcnt_glitch_filter_config_t filter_config = {
        .max_glitch_ns = 1000,
    };
    ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(pcnt_unit, &filter_config));

    // Шаг 3. Создаём PCNT каналы
    ESP_LOGI(TAG, "install pcnt channels");

    // Шаг 3.1. Создаём PCNT канал А
    pcnt_chan_config_t chan_a_config = {
        .edge_gpio_num = EXAMPLE_EC11_GPIO_A,
        .level_gpio_num = EXAMPLE_EC11_GPIO_B,
    };
    pcnt_channel_handle_t pcnt_chan_a = NULL;
    ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &chan_a_config, &pcnt_chan_a));

    // Шаг 3.2. Создаём PCNT канал B
    pcnt_chan_config_t chan_b_config = {
        .edge_gpio_num = EXAMPLE_EC11_GPIO_B,
        .level_gpio_num = EXAMPLE_EC11_GPIO_A,
    };
    pcnt_channel_handle_t pcnt_chan_b = NULL;
    ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &chan_b_config, &pcnt_chan_b));

    // Шаг 4. Задаем реакции на импульсы в разных каналах
    ESP_LOGI(TAG, "set edge and level actions for pcnt channels");
    // ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_a, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE));
    // ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_a, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE));
    // ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_b, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE));
    // 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_channel_set_edge_action(pcnt_chan_a, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_HOLD));
    ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_a, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_HOLD));
    ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_b, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_HOLD));
    ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_b, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_HOLD));

    // Шаг 5. Настраиваем watch point-ы
    ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, 1));
    ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, -1));

    // Шаг 6. Зарегистрируем callback на созданный watch point
    pcnt_event_callbacks_t cbs = {0};
    cbs.on_reach = pcnt_on_reach;
    ESP_ERROR_CHECK(pcnt_unit_register_event_callbacks(pcnt_unit, &cbs, NULL));

    // Шаг 7. Включаем и запускаем счетчик
    ESP_LOGI(TAG, "enable pcnt unit");
    ESP_ERROR_CHECK(pcnt_unit_enable(pcnt_unit));
    ESP_LOGI(TAG, "clear pcnt unit");
    ESP_ERROR_CHECK(pcnt_unit_clear_count(pcnt_unit));
    ESP_LOGI(TAG, "start pcnt unit");
    ESP_ERROR_CHECK(pcnt_unit_start(pcnt_unit));

    // Ожидание результата (это может быть где-то в другой задаче)
    int result = 0;
    while (1) {
        if (xQueueReceive(pcnt_evt_queue, &result, portMAX_DELAY)) {
            ESP_LOGI("PCNT", "Rotation: %d", result);
        }
    }
}

И теперь он работает совершенно корректно:

I (279) main_task: Started on CPU0
I (289) main_task: Calling app_main()
I (289) PCNT: install pcnt unit
I (289) PCNT: set glitch filter
I (289) PCNT: install pcnt channels
I (289) PCNT: set edge and level actions for pcnt channels
I (299) PCNT: enable pcnt unit
I (299) PCNT: clear pcnt unit
I (299) PCNT: start pcnt unit
I (1179) PCNT: Rotation: 1
I (2009) PCNT: Rotation: -1
I (2559) PCNT: Rotation: 1
I (3029) PCNT: Rotation: -1
I (3389) PCNT: Rotation: 1
I (3759) PCNT: Rotation: -1
I (4249) PCNT: Rotation: 1
I (4829) PCNT: Rotation: -1

 

Таким образом мы получаем надежный аппаратный обработчик сигналов энкодера с фильтрацией bounce-помех в полностью асинхронном режиме. Без регистрации и СМС таймеров и сложных обработчиков прерываний GPIO.

 


Техническое руководство

Данный раздел представляет собой перевод главы Техническое справочное руководство ESP32 > глава Контроллер подсчета импульсов. Для желающих глубже понять механизм работы контроллера. Можно уже и не читать далее…

Контроллер счетчика импульсов предназначен для подсчета количества фронтов и/или спадов входных импульсов. Каждый блок счетчика импульсов имеет 16-битный регистр счетчика со знаком и два канала, которые можно настроить для увеличения или уменьшения счетчика. Каждый канал имеет вход сигнала, который принимает фронты сигнала для обнаружения, а также управляющий вход, который можно использовать для включения или отключения счета импульсов. Входы имеют дополнительные фильтры, которые можно использовать для отбрасывания нежелательных помех в сигнале.

Счетчик импульсов имеет восемь независимых блоков, называемых PULSE_CNT_Un.

Максимальная частота импульсов, поддерживаемая счетчиком импульсов ESP32, составляет 40 МГц.

 

Функциональная схема

Архитектура блока счетчика импульсов показана на рисунке выше. Каждый блок имеет два канала: ch0 и ch1, которые функционально эквивалентны. Каждый канал имеет вход сигнала, а также вход управления, которые могут быть подключены к любым GPIO через IOMUX.

Поведение счетчика как на положительном, так и на отрицательном фронте сигнала, может быть настроено как увеличение, уменьшение или игнорирование со настраиваемым значением инкремента. Отдельно для обоих уровней управляющего сигнала контроллер может быть настроен для изменения действия фронта: инвертирования его, отключения или отсутствия действия.

Сам счетчик представляет собой 16-битный счетчик с прямым/обратным счетом. Его значение может быть считано программным обеспечением как напрямую, так и с помощью генерации прерываний компараторами.

 

Входы счетчика

Как уже говорилось выше, два входа каждого канала могут влиять на счетчик импульсов различными способами. Специфика этого поведения задается регистрами LCTRL_MODE и HCTRL_MODE, когда уровень на управляющем входе соответственно низкий или высокий, и POS_MODE и NEG_MODE для положительных и отрицательных фронтов входного сигнала.

Установка POS_MODE и NEG_MODE в 1 увеличит счетчик при обнаружении соответствующего фронта, установка их в 2 уменьшит счетчик, а установка в любое другое значение нейтрализует влияние фронта на счетчик.

LCTR_MODE и HCTR_MODE изменяют это поведение, когда сигнал на управляющем входе имеет соответствующее низкое или высокое значение: 0 не изменяет поведение NEG_MODE и POS_MODE, 1 инвертирует его (установка POS_MODE/NEG_MODE для увеличения счетчика теперь должна уменьшать счетчик и наоборот), а любое другое значение отключает работу счетчика для этого уровня сигнала.

В таблице ниже рассмотрено несколько примеров, где показано влияние на счетчик нарастающего фронта как для низкого, так и для высокого управляющего сигнала, а также для различных других параметров конфигурации. Для ясности после значений добавлено краткое описание в скобках. Примечание: x обозначает «неважно».

Эта таблица также действительна для отрицательных фронтов (sig h→l) при замене NEG_MODE на POS_MODE.

Каждый блок счетчика импульсов также имеет фильтр на каждом из четырех входов, что добавляет возможность игнорировать короткие помехи в сигналах. Регистр PCNT_FILTER_EN_Un можно настроить для фильтрации четырех входных сигналов каждого блока. Если этот фильтр включен, любые импульсы короче, чем REG_FILTER_THRES_Un числа тактов APB_CLK, будут отфильтрованы и не окажут никакого влияния на счетчик. При отключенном фильтре теоретически бесконечно короткие импульсы помехи могут вызывать обработку счетчиком импульсов. Однако на практике входы сигналов оцифровываются по фронтам APB_CLK, и даже при отключенном фильтре могут быть пропущены импульсы длительностью короче одного цикла APB_CLK.

Помимо входных сигналов, ваше программное обеспечение может также управлять счетчиком. Например, значение счетчика можно заморозить на текущем значении, настроив регистр PCNT_CNT_PAUSE_Un. Его также можно сбросить на 0, настроив регистр PCNT_PLUS_CNT_RST_Un.

 

Watchpoints

Счетчики импульсов имеют пять точек наблюдения, которые совместно используют одно прерывание. Генерация прерываний может быть включена или отключена для каждой отдельной точки наблюдения. Точки наблюдения могут быть следующие:

  • Максимальное значение счетчика: срабатывает, когда PULSE_CNT >= PCNT_CNT_H_LIM_Un. Кроме того, это сбросит значение счетчика на 0. PCNT_CNT_H_LIM_Un должно быть положительным числом.
  • Минимальное значение счетчика: срабатывает, когда PULSE_CNT <= PCNT_CNT_L_LIM_Un. Кроме того, это сбросит значение счетчика на 0. PCNT_CNT_L_LIM_Un должно быть отрицательным числом.
  • Два пороговых значения: срабатывает, когда PULSE_CNT = PCNT_THR_THRES0_Un или PCNT_THR_THRES1_Un.
  • Ноль: срабатывает, когда PULSE_CNT = 0

 

Примеры

На рисунке ниже проиллюстрировано, как канал 0 используется как счетчик с нарастанием.

Конфигурация канала 0 в данном случае такова:

  • CNT_CH0_POS_MODE_Un = 1: увеличить счетчик по восходящему фронту sig_ch0_un.
  • PCNT_CH0_NEG_MODE_Un = 0: не считать по спадающему фронту sig_ch0_un.
  • PCNT_CH0_LCTRL_MODE_Un = 0: не изменять режим счетчика, когда ctrl_ch0_un низкий (т.е. разрешить).
  • PCNT_CH0_HCTRL_MODE_Un = 2: не разрешать увеличение/уменьшение счетчика, когда ctrl_ch0_un высокий.
  • PCNT_CNT_H_LIM_Un = 5: PULSE_CNT сбрасывается в 0, когда значение счетчика увеличивается до 5.

 

На рисунке ниже изображен тот же канал 0, но в режиме счета на уменьшение.

В данном случае конфигурация канала отличается от приведенной выше в следующих двух аспектах:

  • PCNT_CH0_LCTRL_MODE_Un = 1: инвертировать режим счетчика, когда ctrl_ch0_un находится на низком уровне, поэтому он будет уменьшать, а не увеличивать значение счетчика.
  • PCNT_CNT_H_LIM_Un = –5: PULSE_CNT сбрасывается в 0, когда значение счетчика уменьшается до –5

 

Прерывания

PCNT_CNT_THR_EVENT_Un_INT: Это прерывание срабатывает, когда один из пяти канальных компараторов обнаруживает совпадение с заранее настроенными значениями.

 

Ну а на этом сегодня всё, разрешите откланяться. С вами был Александр aka kotyara12.


Ссылки

  1. Техническое справочное руководство ESP32
  2. Справочное руководство ESP-IDF

Пожалуйста, оцените статью:
[ 5 из 5, всего 3 оценок ]

-= Каталог статей (по разделам) =-   -= Архив статей (подряд) =-

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *