Files
2026-11-03 19:23:03 +03:00

242 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Описание компонента zh_tachometer
**zh_tachometer** — драйвер для ESP-IDF, измеряющий скорость вращения на основе квадратурного энкодера. Компонент использует периферийный модуль PCNT ESP32 в режиме квадратурного декодера для подсчёта импульсов энкодера и ESP-Timer, работающего с частотой 100 Гц, для периодического считывания накопленного значения и вычисления оборотов в минуту (RPM). Результатом является неблокирующий, совместимый с RTOS интерфейс, подходящий для мониторинга скорости двигателей.
Компонент поддерживает одновременную работу нескольких тахометров, каждый из которых использует собственный модуль PCNT и таймер.
## Основные возможности
- **Поддержка квадратурного энкодера** — подсчёт импульсов по двум фазам (A/B) через периферийный модуль PCNT в режиме квадратурного декодера.
- **Автоматический расчёт RPM** — вычисление оборотов в минуту на основе настраиваемого количества импульсов на полный оборот энкодера.
- **Периодическое дискретное считывание** — ESP-Timer запускается с частотой 100 Гц (интервал 10 мс) для чтения накопленного счёта PCNT и перевода в RPM.
- **Настраиваемые подтягивающие резисторы GPIO** — возможность включения или отключения внутренних подтягивающих резисторов для линий A и B энкодера.
- **Фильтр глитчей PCNT** — подавление ложных переходов с настройкой 1000 нс через встроенный фильтр PCNT.
- **Поддержка нескольких тахометров** — каждый тахометр использует отдельный модуль PCNT и таймер ESP-Timer.
## Внутреннее устройство
### Структура `zh_tachometer_handle_t`
Внутренняя структура тахометра содержит ссылки на все используемые периферийные модули, последнее вычисленное значение RPM и конфигурацию энкодера.
| Поле | Тип | Описание |
| ------ | ----- | ---------- |
| `pcnt_unit_handle` | `pcnt_unit_handle_t` | Управляющий объект модуля PCNT для квадратурного декодирования |
| `pcnt_channel_a_handle` | `pcnt_channel_handle_t` | Управляющий объект канала PCNT A (детектор фронтов) |
| `pcnt_channel_b_handle` | `pcnt_channel_handle_t` | Управляющий объект канала PCNT B (детектор фронтов) |
| `esp_timer_handle` | `esp_timer_handle_t` | Управляющий объект ESP-Timer для дискретного считывания RPM (100 Гц) |
| `value` | `volatile uint16_t` | Последнее вычисленное значение RPM (абсолютное, направление отбрасывается) |
| `encoder_pulses` | `uint16_t` | Количество импульсов на один полный оборот энкодера |
Структура является непрозрачной для вызывающего кода — доступ осуществляется только через публичные функции API.
### Структура конфигурации `zh_tachometer_init_config_t`
Передаётся в функцию `zh_tachometer_init()` для настройки пин GPIO, подтягивающих резисторов и коэффициента импульсов на оборот.
| Поле | Тип | Описание |
| ------ | ----- | ---------- |
| `a_gpio_number` | `gpio_num_t` | Номер GPIO линии фазы A энкодера |
| `b_gpio_number` | `gpio_num_t` | Номер GPIO линии фазы B энкодера |
| `pullup` | `bool` | Включение или отключение подтягивающих резисторов GPIO |
| `encoder_pulses` | `uint16_t` | Количество импульсов на один полный оборот энкодера |
По умолчанию `ZH_TACHOMETER_INIT_CONFIG_DEFAULT()` устанавливает GPIO в `GPIO_NUM_MAX`, подтягивание в `true`, а импульсы в `0`.
### Управление памятью
Память для структуры `zh_tachometer_handle_t` выделяется через `heap_caps_calloc()` с флагом `MALLOC_CAP_8BIT` при вызове `zh_tachometer_init()` и освобождается через `heap_caps_free()` при вызове `zh_tachometer_deinit()`. Управляющие объекты PCNT и ESP-Timer освобождаются через соответствующие функции ESP-IDF API (`pcnt_del_unit()`, `pcnt_del_channel()`, `esp_timer_delete()`).
## API (интерфейс)
### Инициализация и освобождение
| Функция | Описание |
|---------|----------|
| `zh_tachometer_init()` | Создаёт объект тахометра, настраивает ESP-Timer (100 Гц) и периферийный модуль PCNT в режиме квадратурного декодера с фильтром глитчей |
| `zh_tachometer_deinit()` | Останавливает модуль PCNT, удаляет каналы и точки наблюдения, удаляет ESP-Timer и освобождает память объекта |
### Получение данных
| Функция | Описание |
|---------|----------|
| `zh_tachometer_get()` | Возвращает последнее вычисленное значение RPM. Значение является абсолютным (направление вращения отбрасывается), функция не блокирует выполнение и не обращается к периферийному модулю PCNT напрямую |
## Потокобезопасность
1. **Общий доступ к данным** — последнее значение RPM хранится в поле `value` с квалификатором `volatile`, что обеспечивает корректное чтение из любого контекса. Функция `zh_tachometer_get()` просто копирует значение из структуры, не требуя блокировок.
2. **Контекст таймера** — обратный вызов ESP-Timer выполняется в контексте задачи `esp_timer service task` (метод диспетчеризации `ESP_TIMER_TASK` по умолчанию), а не из прерывания. Блокирующие или длительные операции в обратном вызове следует избегать.
## Алгоритмы
### Формула расчёта RPM
Обратный вызов ESP-Timer выполняется каждые 10 мс (100 Гц). За этот интервал считывается накопленное значение PCNT, обнуляется и преобразуется в RPM по формуле:
```
RPM = (count * 100.0 / (4.0 * pulses_per_rev)) * 60.0
```
Где:
- `count` — накопленное значение PCNT за 10 мс.
- `100.0` — масштабный коэффициент для перевода 10 мс в секунду (1000 мс / 10 мс).
- `4.0` — коэффициент квадратурного кодирования: 4 фронта за один цикл (фазы A и B имеют сдвиг 90°).
- `pulses_per_rev` — количество импульсов на полный оборот энкодера, задаётся в конфигурации.
- `60.0` — перевод оборотов в секунду в обороты в минуту.
Результат берётся по модулю (`fabsf()`) — направление вращения игнорируется, возвращается абсолютное значение RPM.
### Конфигурация PCNT
Модуль PCNT настраивается в режиме квадратурного декодера:
- Канал A: фронт по фазе A вызывает увеличение (`INCREASE`) или уменьшение (`DECREASE`) счётчика в зависимости от уровня фазы B.
- Канал A: уровень фазы A удерживает счёт (`KEEP`) или инвертирует направление (`INVERSE`).
- Канал B: фронт по фазе B вызывает увеличение или уменьшение счётчика в зависимости от уровня фазы A (обратное каналу A).
- Канал B: уровень фазы B удерживает или инвертирует направление (обратное каналу A).
- Фильтр глитчей: 1000 нс.
- Точки наблюдения: ±32767 для обнаружения переполнения.
- Накопление: включено (`accum_count = true`).
### Процесс деинициализации
При вызове `zh_tachometer_deinit()` ресурсы освобождаются в строгом порядке:
1. Остановка ESP-Timer (`esp_timer_stop`).
2. Удаление ESP-Timer (`esp_timer_delete`).
3. Остановка PCNT (`pcnt_unit_stop`).
4. Отключение PCNT (`pcnt_unit_disable`).
5. Удаление точек наблюдения: +32767, затем -32767.
6. Удаление каналов A и B (`pcnt_del_channel`).
7. Удаление модуля PCNT (`pcnt_del_unit`).
8. Освобождение памяти объекта (`heap_caps_free`).
9. Обнуление указателя на объект.
Все шаги используют `ZH_ERROR_CHECK_CONT` — ошибка логируется, но процесс деинициализации продолжается до полного освобождения ресурсов.
## Примеры использования
### Базовое использование
```c
#include "zh_tachometer.h"
zh_tachometer_handle_t *tachometer_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_tachometer", ESP_LOG_ERROR);
zh_tachometer_init_config_t config = ZH_TACHOMETER_INIT_CONFIG_DEFAULT();
config.a_gpio_number = GPIO_NUM_26;
config.b_gpio_number = GPIO_NUM_27;
config.encoder_pulses = 3600;
esp_err_t ret = zh_tachometer_init(&config, &tachometer_handle);
if (ret != ESP_OK)
{
printf("Tachometer initialization error: %s\n", esp_err_to_name(ret));
return;
}
for (;;)
{
uint16_t value = 0;
ret = zh_tachometer_get(&tachometer_handle, &value);
if (ret == ESP_OK)
{
printf("Current RPM: %u\n", value);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
zh_tachometer_deinit(&tachometer_handle);
}
```
### Настройка с отключёнными подтягивающими резисторами
```c
#include "zh_tachometer.h"
zh_tachometer_handle_t *tachometer_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_tachometer", ESP_LOG_ERROR);
zh_tachometer_init_config_t config = ZH_TACHOMETER_INIT_CONFIG_DEFAULT();
config.a_gpio_number = GPIO_NUM_12;
config.b_gpio_number = GPIO_NUM_13;
config.pullup = false;
config.encoder_pulses = 1800;
esp_err_t ret = zh_tachometer_init(&config, &tachometer_handle);
if (ret != ESP_OK)
{
printf("Tachometer initialization error: %s\n", esp_err_to_name(ret));
return;
}
uint16_t rpm_value = 0;
ret = zh_tachometer_get(&tachometer_handle, &rpm_value);
if (ret == ESP_OK)
{
printf("RPM value: %u\n", rpm_value);
}
zh_tachometer_deinit(&tachometer_handle);
}
```
## Логирование
Компонент использует ESP-IDF логирование с тегом `"zh_tachometer"`.
```c
esp_log_level_set("zh_tachometer", ESP_LOG_ERROR);
```
Доступные уровни: `ESP_LOG_ERROR`, `ESP_LOG_INFO`. Макрос `ZH_LOGI` выводит информационные сообщения о начале и завершении операций. Макрос `ZH_LOGE` выводит сообщения об ошибках с указанием файла, строки и имени кода ошибки.
## Ошибки
| Код | Описание |
| ----- | ---------- |
| `ESP_OK` | Успешное выполнение |
| `ESP_ERR_INVALID_ARG` | Неверный аргумент: `config` или `handle` равен NULL, `encoder_pulses` равен нулю, номера GPIO недействительны или совпадают, `*handle` или `value` равен NULL |
| `ESP_ERR_INVALID_STATE` | Объект тахометра уже инициализирован (`*handle` не равен NULL) |
| `ESP_ERR_NO_MEM` | Не удалось выделить память для объекта тахометра |
| `ESP_FAIL` | Ошибка инициализации или конфигурации PCNT, ошибка создания или запуска ESP-Timer |
## Ограничения
1. **Переполнение PCNT счётчика** — модуль PCNT использует точки наблюдения на ±32767. При высоких значениях RPM накопленный счётчик может переполниться. Необходимо убедиться, что комбинация количества импульсов на оборот и ожидаемого диапазона RPM остаётся в безопасных пределах.
2. **Абсолютное значение RPM** — направление вращения игнорируется. Компонент возвращает только модуль скорости, без информации о направлении.
3. **16-битное значение RPM** — возвращаемое значение имеет тип `uint16_t`, максимальное значение — 65535. При очень высоких скоростях вращения значение может не поместиться.
4. **Один GPIO на фазу** — каждая фаза энкодера (A и B) должна быть назначена на отдельный GPIO. Назначение одинакового номера GPIO на обе фазы вернёт ошибку `ESP_ERR_INVALID_ARG`.
## Требования для сборки
- ESP-IDF (фреймворк для ESP32)
- FreeRTOS (входит в состав ESP-IDF)
- Компонент ESP-IDF `pulse_cnt` (драйвер PCNT)
- Компонент ESP-IDF `esp_timer` (ESP-Timer)
- Компонент ESP-IDF `driver` (GPIO driver)
## Установка
```bash
cd ваш_проект/components
git clone https://github.com/aZholtikov/zh_tachometer
```
```c
#include "zh_tachometer.h"
```