Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103 · уровень средний · 28 мин чтения
Открыть в интерактивном справочнике

13г. Механический энкодер с кнопкой (EC11, KY-040): дребезг, режим энкодера таймера и ускорение

В разделе 13.7 таймер считал энкодер сам, в 13.10 ручка меняла яркость светодиода, в 57з.4 энкодер на валу мерил скорость двигателя. Здесь — ручка интерфейса: механический энкодер со щелчками и кнопкой на оси, как EC11 или плата KY-040. От ручки ждут одного: сколько щелчков сделал человек, столько и шагов в меню, ни одним больше. Мешают дребезг контактов, разные ручки (4 или 2 перехода на щелчок), порядок включения и перенос счётчика через край. Здесь — квадратурный код и щелчок, почему прерывание по фронту считает лишнее, автомат по таблице переходов, режим энкодера TIM4 с фильтром входа, привязка счёта к щелчкам, кнопка с долгим нажатием и ускорение при быстром вращении.

Опоры: RM0008 (DocID13902 Rev 17): вход с подтяжкой, IDR читается каждый такт APB2, таблица 20, с. 160; вход канала таймера — вход без подтяжки, таблица 23, с. 165; режим энкодера, счёт от 0 до ARR, с. 391; таблица 85 — направление счёта, с. 392; инверсия TI1 битом CC1P, с. 393; CKD, с. 403; SMS, с. 407; IC1F и CC1S, с. 414. DocID13587 Rev 17 (STM32F103x8/xB): PA0, PA1 — TIM2_CH1_ETR, TIM2_CH2, PA6, PA7 — TIM3_CH1, TIM3_CH2, все без допуска 5 В, с. 29; PA8, PA9 — TIM1_CH1, TIM1_CH2, FT, с. 31; PB6, PB7 — TIM4_CH1, TIM4_CH2, FT, с. 32; PB8, PB9 — TIM4_CH3, TIM4_CH4, с. 33; предельное напряжение входа и ток инжекции, таблицы 6 и 7, с. 37; инжекции избегать, с. 61; VIL, VIH, подтяжка RPU, таблица 35, с. 62. Документов на EC11 и KY-040 в проекте нет: число переходов на щелчок, состояние контактов в щелчке и дребезг — допущения модели, а не данные производителя. Все числа — прогон _verify/enc13e: модель ручки, RC-цепи, фильтра и счётчика таймера на Python 3.11 подаёт поток в тот же код enc13e.c, собранный arm-none-eabi-gcc 12.2.1 под Cortex-M3 и запущенный в QEMU 7.2; прошивка собрана armclang 6.24.

Как это называется в документации

Оригинал Перевод
rotary encoder, incremental encoder энкодер, инкрементный энкодер
quadrature signals A and B квадратурные сигналы A и B: сдвиг на четверть периода
detent щелчок — фиксированное положение ручки
contact bounce дребезг контактов
encoder interface mode (SMS = 011) режим энкодера: счёт по обоим фронтам обоих каналов
input filter (ICxF), f_SAMPLING, N фильтр входа: частота выборки и сколько выборок подряд
clock division (CKD), t_DTS делитель такта выборки фильтра
auto-reload (ARR) предел счёта: счётчик ходит от 0 до ARR
state transition table таблица переходов
acceleration ускорение: шаг растёт при быстром вращении
long press долгое нажатие
five-volt tolerant (FT) вывод с допуском 5 В

13г.1 Квадратурный код: сколько переходов на щелчок

В ручке два контакта, A и B, и общий вывод C на землю. При повороте они замыкаются и размыкаются со сдвигом на четверть цикла, поэтому между двумя соседними состояниями меняется ровно один бит. В модели по часовой стрелке AB идёт 11 → 01 → 00 → 10 → 11: первым размыкается A. Против часовой — тот же круг в обратную сторону. Направление — это порядок, в каком меняются A и B, а не уровень одного из них. Какой контакт у вашей ручки первый, зависит от разводки; если счёт идёт не туда — 13г.7.

Сколько переходов приходится на щелчок, решает ручка. Бывает полный цикл на щелчок — четыре перехода, в щелчке оба контакта разомкнуты, AB = 11. Бывает полцикла — два перехода, и щелчки стоят то на 11, то на 00. Документов на EC11 и KY-040 в проекте нет, поэтому проверяют своей ручкой: один щелчок и разность CNT таймера — 4 или 2. В модели ручка «полцикла» на 30 щелчков вперёд и 10 назад довела CNT до 40, то есть по 2 на щелчок, а ручка «полный цикл» на тех же поворотах — до 80.

Ручка Делим на Сделано: итог / всего CNT в конце Насчитано: итог / всего
2 перехода на щелчок 4 20 / 40 40 10 / 20 — каждый второй
2 перехода на щелчок 2 20 / 40 40 20 / 40
4 перехода на щелчок 2 20 / 40 80 40 / 80 — вдвое больше

«Итог» — щелчки со знаком, «всего» — все щелчки в обе стороны. Делитель в коде — per, 4 или 2, и он должен совпадать с ручкой.

Вверху — подключение ручки к STM32F103: линия A через R2 10 кОм к PB6 (TIM4_CH1), на выводе конденсатор C 10 нФ на землю, подтяжка R1 10 кОм к 3,3 В у контакта; линия B — та же цепь к PB7 (TIM4_CH2), кнопка — на PB8 с подтяжкой внутри, общий вывод C ручки и кнопка — на землю; постоянные времени RC 0,1 и 0,2 мс, фильтр TIM4 IC1F 1111 при CKD ÷4 — 14,2 мкс. Внизу — один щелчок по часовой: A размыкается первым, AB идёт 11, 01, 00, 10, 11, CNT 0, 1, 2, 3, 4, после каждого перехода дребезг; округление (CNT + 2) / 4 меняет уровень на переходе 01 → 00, и дребезг этого перехода даёт лишние события, а автомат и гистерезис засчитывают щелчок по приходу в AB 11 при CNT 4


13г.2 Дребезг: почему прерывание по фронту считает лишнее

Контакт при замыкании и размыкании отскакивает. В модели после каждого перехода — от 0 до 6 возвратов к старому уровню: пауза 20…400 мкс, возврат 2…250 мкс, вся пачка не дольше 2 мс. Пока контакт замкнут, скольжение даёт ещё и искры — размыкания на 1…12 мкс, в среднем одно на 10 мс. Это допущения модели, а не данные производителя.

Самый простой код — прерывание по спаду A и чтение B: B = 1 — шаг вперёд, B = 0 — назад. Без дребезга это по шагу на щелчок. С дребезгом каждый отскок — ещё один спад. На спаде A вперёд они добавляют шаги, а на подъёме A (отскок обратно в ноль) — шаги назад, ведь B там уже 0. В модели сделали 60 щелчков вперёд, 45 назад и 30 вперёд — итог 45, всего 135:

Способ Прерываний Насчитано
спад A, чтение B 1746 −70 вместо 45
то же через RC 10 нФ 471 −1 вместо 45
автомат по таблице, опрос 1 мс (13г.3) — 45, всего 135
TIM4 в режиме энкодера (13г.4, 13г.5) — 45, всего 135

Фронтов на линиях было 7190 при 540 настоящих (135 × 4). Прерывание ещё и грузит процессор: 1746 входов в обработчик на 135 щелчков.

RC-цепь. Схема та же, что у кнопки в 50.7: подтяжка R1 к 3,3 В у контакта, R2 последовательно к выводу и конденсатор на выводе. Замкнулся контакт — конденсатор разряжается через R2, разомкнулся — заряжается через R1 + R2. Пороги входа F103 — VIL = 0,28·(VDD − 2) + 0,8 и VIH = 0,41·(VDD − 2) + 1,3 (таблица 35, с. 62), при 3,3 В — 1,164 и 1,833 В; модель переключает триггер Шмитта ровно на этих границах.

R1, R2, C τ разряда τ заряда Задержка спада Задержка подъёма
10 кОм, 10 кОм, 10 нФ 100 мкс 200 мкс 104,2 мкс 162,1 мкс
подтяжка внутри (40 кОм), 10 кОм, 10 нФ 100 мкс 500 мкс 104,2 мкс 405,4 мкс
10 кОм, 10 кОм, 100 нФ 1000 мкс 2000 мкс 1042,1 мкс 1621,4 мкс

Подтяжка внутри — 30…50 кОм, обычно 40 (таблица 35, с. 62): заряд выходит в 2,5 раза медленнее. Цепь 10 нФ не пропускает одиночный отскок короче задержки: размыкание на 160 мкс не прошло, на 170 мкс — прошло; замыкание на 100 мкс не прошло, на 110 мкс — прошло. Отскоки длиннее проходят, поэтому прерыванию 10 нФ не хватило.

Конденсатор в 10 раз больше гасит весь дребезг модели, но и сам задерживает переходы на 1…1,6 мс. По три прогона по 200 щелчков:

Темп Без RC 10 нФ 100 нФ
50 щелчков/с: фронтов (настоящих 2400) 16 392 8744 2400
50 щелчков/с: прерывание точно 0 из 3 0 из 3 3 из 3
100 щелчков/с: прерывание / автомат точно 0 / 1 из 3 0 / 1 из 3 3 / 3 из 3
200 щелчков/с: фронтов (настоящих 2400) 16 186 8578 2030
200 щелчков/с: таймер точно 0 из 3 0 из 3 0 из 3

На 200 щелчках в секунду цепь 100 нФ съела настоящие переходы — фронтов меньше, чем сделано. Ёмкость выбирают между длиной дребезга и шагом между переходами A и B. С таймером и автоматом RC для счёта не нужна (13г.3, 13г.4), а 10 нФ стоят на схеме ради искр и помех на проводе; 100 нФ — если счёт по прерыванию.


13г.3 Автомат по таблице переходов

Программное решение без деталей: раз в 1 мс читать AB и смотреть, из какого состояния в какое перешли. Пар «было — стало» шестнадцать, и на каждую в таблице ответ: шаг вперёд, назад или ничего.

/* индекс — (прошлое AB << 2) | новое AB; +1 — шаг по часовой (11 → 01 → 00 → 10 → 11), −1 — против,
 * 0 — стоим на месте или перескочили через состояние (оба бита сразу: направление неизвестно) */
static const int8_t qtab[16] = {
     0, -1, +1,  0,     /* из 00: в 00, 01, 10, 11 */
    +1,  0,  0, -1,     /* из 01 */
    -1,  0,  0, +1,     /* из 10 */
     0, +1, -1,  0,     /* из 11 */
};

Дребезг для таблицы безвреден: отскок туда и обратно — это +1 и −1. Шаги копятся, а щелчок засчитывается только в состоянии покоя — в AB = 11 (у ручки на 2 перехода ещё и в 00). Там же счёт шагов сбрасывается, и автомат снова привязан к щелчкам:

/* щелчок засчитывается только в состоянии покоя, и там же сбрасывается счёт шагов: дребезг даёт +1 −1
 * и до покоя не доходит, а один пропущенный шаг (перескок) округление ещё прощает */
int enc_qdec_step(enc_qdec_t *q, uint8_t ab)
{
    int ev = 0, half = q->per / 2;
    ab &= 3u;
    q->acc = (int8_t)(q->acc + qtab[(q->ab << 2) | ab]);
    q->ab = ab;
    if (ab == ENC_REST || (q->per == 2u && ab == 0u)) {
        ev = q->acc >= 0 ? (q->acc + half) / q->per : -((-q->acc + half) / q->per);
        q->acc = 0;
    }
    return ev;
}

Перескок — оба бита сменились между двумя опросами — таблица считает нулём: направление неизвестно. Один такой перескок на щелчок округление прощает: в покое накопится 2 из 4, и это всё ещё щелчок. Два — уже нет.

Сколько выдерживает опрос раз в 1 мс, модель проверила постоянным темпом: 100 щелчков вперёд и 100 назад, по три прогона на темп с дребезгом и по одному без него. Обороты — при 20 щелчках на оборот (допущение).

Щелчков/с Об/с Шаг между переходами С дребезгом: автомат С дребезгом: таймер Без дребезга: автомат
20 1 12,5 мс 3 из 3 3 из 3
50 2,5 5 мс 3 из 3 3 из 3
80 4 3,125 мс 3 из 3 3 из 3
100 5 2,5 мс 0 из 3 3 из 3
120 6 2,083 мс 0 из 3 3 из 3
150 7,5 1,667 мс 0 из 3 0 из 3 200 из 200
300 15 0,833 мс 0 из 3 0 из 3 200 из 200
400 20 0,625 мс 100 из 200

С дребезгом до 2 мс автомат точен до 80 щелчков в секунду: шаг между переходами должен вместить и дребезг, и ещё один период опроса. Без дребезга его хватает до 300 щелчков в секунду, на 400 — половина щелчков (при ровном темпе опрос попадает на одни и те же фазы). Выше 120 щелчков в секунду ошибается и таймер: если шаг короче дребезга, один контакт ещё прыгает, когда переключается второй, и сама последовательность AB на выводах уже не та. Такой поворот — 6 оборотов в секунду — для ручки интерфейса редкость.


13г.4 Режим энкодера таймера: регистры, фильтр, ARR

Таймер считает переходы сам, по таблице 85 (с. 392): оба фронта обоих каналов, направление — по уровню другого канала. Входы — только TI1 и TI2, то есть каналы 1 и 2 одного таймера (с. 391):

Таймер A (CH1) B (CH2) Допуск 5 В
TIM2 PA0 PA1 нет (с. 29)
TIM3 PA6 PA7 нет (с. 29)
TIM4 PB6 PB7 есть (с. 32)
TIM1 PA8 PA9 есть (с. 31)

Здесь — TIM4 на PB6 и PB7: выводы терпят 5 В (13г.7). Настройка:

static void enc_hw_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
    /* PB6, PB7 — вход без подтяжки (0100): подтяжки на плате; PB8 — вход с подтяжкой (1000 и ODR8 = 1) */
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0x44000000u;
    GPIOB->CRH = (GPIOB->CRH & ~0xFu) | 0x8u;
    GPIOB->BSRR = 1u << 8;
    /* TI1 и TI2 — входы каналов 1 и 2, фильтр 1111: 8 выборок на f_DTS / 32 */
    TIM4->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_0 | TIM_CCMR1_IC1F | TIM_CCMR1_IC2F;
    TIM4->CCER = 0;                                     /* CC1P = CC2P = 0; CC1P = 1 — счёт в обратную сторону */
    TIM4->SMCR = TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0;       /* 011: оба фронта обоих каналов */
    TIM4->ARR = 0xFFFFu;
    TIM4->CR1 = TIM_CR1_CKD_1 | TIM_CR1_CEN;            /* f_DTS = 72 / 4 = 18 МГц: 8 / 562,5 кГц = 14,2 мкс */
}

CC1S = CC2S = 01 — каналы стали входами TI1 и TI2 (с. 414). SMS = 011 — счёт по обоим фронтам обоих каналов (с. 407); 001 и 010 считали бы по одному каналу, по 2 на цикл. Вход канала таймера по таблице 23 (с. 165) — вход без подтяжки, у нас подтяжки на плате.

Фильтр входа. IC1F задаёт частоту выборки и N — сколько выборок подряд с новым уровнем нужно, чтобы переход прошёл (с. 414). Самый длинный — 1111: f_DTS / 32 и N = 8. CKD делит такт для фильтра на 1, 2 или 4 (с. 403). Такт TIM4 — 72 МГц, при CKD = ÷4 f_DTS = 18 МГц, выборка раз в 1,78 мкс. Импульс короче 7 выборок (12,44 мкс) не пройдёт никогда, длиннее 8 (14,22 мкс) — всегда; между ними решает фаза: в модели импульс 12,5 мкс прошёл при 7 из 200 фаз выборки, 14 мкс — при 176.

Один и тот же поворот, 40 щелчков туда и 40 обратно, с разными фильтрами:

IC1F, CKD Выборка Окно N выборок Фронтов на входе Фронтов до счётчика Насчитано, итог / всего
0000, ÷1 0,0139 мкс 0,01 мкс 4106 4106 −1 / 79
0011, ÷1 0,0139 мкс 0,11 мкс 4106 4106 −1 / 79
1111, ÷1 0,4444 мкс 3,56 мкс 4106 3616 −1 / 79
1111, ÷4 1,7778 мкс 14,22 мкс 4106 2030 0 / 80

Сделано — 0 и 80. Без длинного фильтра таймер потерял щелчок, и виноват не дребезг, а совпадение: искра на B длиной 11,47 мкс пришлась на отскок A, и последовательность на выводах прошла круг в обратную сторону — −3 вместо +1. Фильтр 14,2 мкс убрал все 995 искр модели: самая длинная — 11,99 мкс, короче 12,44. Отскоков было 898, и большая часть их длиннее фильтра: их гасит сам квадратурный счёт, +1 и −1 на месте. Сколько это даёт против дребезга длиной в миллисекунды — почти ничего, против коротких искр и помех на проводе — всё.

ARR и перенос через 0. Счётчик ходит от 0 до ARR и через край (с. 391). С ARR = 0xFFFF разность показаний со знаком (int16_t)(uint16_t)(cnt − last) верна и через 0 (13.10). Но иногда ARR ставят под меню: 12 пунктов по 4 перехода — ARR = 47, и CNT сам ходит по кругу 0…47. Тогда разность надо брать по модулю 48:

/* разность показаний по модулю ARR + 1, к ближайшему; щелчок — когда позиция ушла от прошлого щелчка
 * на целый щелчок: гистерезис в щелчок, дребезг на любом переходе событий не плодит */
int enc_tim_step(enc_tim_t *e, uint16_t cnt)
{
    int32_t d, top = e->top ? e->top : 65536;
    int ev = 0;
    d = (int32_t)cnt - (int32_t)e->last;
    if (d > top / 2)
        d -= top;
    else if (d < -top / 2)
        d += top;
    e->last = cnt;
    e->pos += d;
    while (e->pos - e->ref >= e->per) {
        e->ref += e->per;
        ev++;
    }
    while (e->pos - e->ref <= -(int32_t)e->per) {
        e->ref -= e->per;
        ev--;
    }
    return ev;
}

В модели с ARR = 47 ручку увели на 5 щелчков назад от нуля, потом на 20 вперёд и на 3 назад: итог 12, всего 28, CNT в конце — 0. Разность по модулю 48 дала 12 и 28. Разность int16_t дала итог 0 и 58 событий: переход 0 → 47 она считает шагом на +47.


13г.5 Деление на 4 и привязка к щелчкам

В 13.10 уровень яркости — (отсчёты + 2) / 4: граница уровня лежит между щелчками, и дребезг у щелчка уровень не дёргает. Но граница лежит ровно на втором переходе щелчка, 01 → 00 (рисунок), а он дребезжит так же, как все. Для яркости мигание на один уровень посреди поворота незаметно; для меню это лишнее событие, для ускорения — короткий интервал между событиями. В модели на тех же 135 щелчках из 13г.2 округление дало 165 событий — 30 лишних, через RC 10 нФ — 145.

enc_tim_step делает иначе: щелчок засчитан, когда позиция ушла от прошлого щелчка на целый щелчок, на 4 отсчёта. Это гистерезис шириной в щелчок: щелчок засчитывается по приходу в следующий, а дребезг последнего перехода ходит 4 ↔ 3 и до следующей границы не дотягивается. Те же 135 щелчков — ровно 135 событий.

Привязка при запуске. Гистерезис работает, если отсчёт щелчка совпадает со щелчком. В модели TIM4 включили раньше, чем поднялись подтяжки: A и B росли с нуля, A прошла порог через 180 мкс, B — через 260 мкс, и CNT насчитал 00 → 10 → 11 — два шага, CNT = 2. Щелчки теперь стоят на 2, 6, 10… — посередине между отсчётами.

Запуск Сделано: итог / всего Гистерезис Округление Автомат
позиция с нуля 20 / 60 21 / 59 21 / 69 20 / 60
позиция с фазы на выводах 20 / 60 20 / 60 20 / 68 20 / 60

Лечится тем, что позиция начинается не с нуля, а с фазы, прочитанной на выводах, когда линии уже успокоились. Прошивка ждёт 5 мс — 25 постоянных времени RC по 0,2 мс — и только потом читает CNT и IDR:

/* CNT в момент запуска может стоять не на щелчке: подтяжки поднимали линии, ручку держали между
 * щелчками. Поэтому позиция начинается с фазы, прочитанной на выводах, а не с нуля */
void enc_tim_init(enc_tim_t *e, uint16_t cnt, uint8_t ab, uint8_t per, uint16_t arr)
{
    e->last = cnt;
    e->top = (uint16_t)(arr + 1u);                      /* ARR = 0xFFFF → 0: полный круг 65536 */
    e->per = per;
    e->pos = enc_phase(ab) % per;
    if (e->pos > per / 2)
        e->pos -= per;                                  /* к ближайшему щелчку: фаза 3 — это −1 */
    e->ref = 0;
}

Автомату привязка при запуске не нужна: он сбрасывает счёт шагов в каждом покое и ошибку на полщелчка не тащит. Округление и с правильным запуском дало 8 лишних событий из-за дребезга на среднем переходе.

IDR обновляется каждый такт APB2 (с. 160), и прошивка читает A и B прямо с PB6 и PB7, пока они работают входами таймера. На этом же чтении в обработчике SysTick идёт и автомат — для сверки: tim_sum и qdec_sum должны совпадать.


13г.6 Кнопка и ускорение

Кнопка на оси — обычный контакт на землю, антидребезг тот же, что в 2ж.5: уровню верят, когда он 20 мс не менялся, долгое нажатие — 800 мс. Добавлено одно: если ручку крутили с нажатой кнопкой, при отпускании приходит не «короткое», а ENC_KEY_TURN, и долгое не срабатывает — нажатие с поворотом становится отдельным жестом. В прошивке нажатая кнопка переключает ручку с значения на пункт меню. В модели — 120 нажатий с дребезгом до 8 мс при нажатии и отпускании, 12 из них с поворотом на 3 щелчка: распознаны все 72 коротких, 36 долгих и 12 с поворотом.

Ускорение. Значение от 0 до 1000 по одному на щелчок — это 1000 щелчков, 50 оборотов при 20 щелчках на оборот. Шаг делают зависимым от того, как часто идут щелчки. Пороги считаются от оборотов: медленнее оборота в секунду — точная подстройка, щелчок реже чем раз в 50 мс, шаг 1; быстрее двух оборотов в секунду — чаще чем раз в 25 мс, и весь ход должен уложиться в 2 оборота, 40 щелчков: шаг 1000 / 40 = 25. Между ними — шаг 5. Смена направления и первый щелчок после паузы — снова шаг 1, иначе перелёт цели не исправить.

/* шаг по времени с прошлого щелчка в ту же сторону; смена направления или пауза — снова шаг 1 */
int32_t enc_accel(enc_accel_t *a, int ev, uint32_t now_ms)
{
    int8_t dir;
    int32_t step = 1;
    if (ev == 0)
        return 0;
    dir = ev > 0 ? 1 : -1;
    if (dir == a->dir) {
        uint32_t dt = now_ms - a->t_last;
        if (dt < ENC_ACC_FAST_MS)
            step = ENC_STEP_FAST;
        else if (dt < ENC_ACC_SLOW_MS)
            step = ENC_STEP_MID;
    }
    a->t_last = now_ms;
    a->dir = dir;
    return ev * step;
}
Как крутили Щелчков Время Значение Без ускорения
28 быстро (15…18 мс на щелчок), пауза, 30 медленно (90…140 мс) 58 4,163 с 706 58
73 быстро (15…18 мс) 73 1,218 с 1000 — упор 73
60 средне (30…45 мс) 60 2,291 с 296 60
60 медленно (60…120 мс) 60 5,434 с 60 60

Быстрая серия дала 1 + 27 × 25 = 676, медленная добавила 30 по одному: значение 706 набрано за 58 щелчков вместо 706. Средний темп — 1 + 59 × 5 = 296, медленный не ускоряется вовсе. Интервал прошивка меряет опросом раз в 1 мс, и щелчки по 15…18 мс в модели виделись от 14 мс: у порога 25 мс это неважно, но порог ближе к темпу руки дал бы шаг то 5, то 25. Пороги — выбор под 20 щелчков на оборот и ход 1000; на другой ручке и другом диапазоне их пересчитывают так же.


13г.7 Грабли

Таймер не считает вовсе. Каналы не той пары: B на PB8 — это TIM4_CH3 (с. 33), или A на TIM2, а B на TIM3. Режим энкодера берёт только TI1 и TI2 своего таймера (с. 391). В модели второй вход стоял в 1: 250 фронтов A дошли до счётчика, а CNT по таблице 85 ходил туда-сюда и закончил на 0 — ни одного щелчка из 20.

Счёт в обратную сторону. A и B перепутаны или первым размыкается не тот контакт. В модели вместо 20 щелчков вперёд — −20, CNT = 65456, то есть 65536 − 80. То же даёт CC1P = 1 — инверсия TI1 (с. 393). Чинится перестановкой проводов, битом CC1P или знаком в коде.

Щелчок через раз или по два. Делитель не тот: ручка на 2 перехода с делением на 4 — 10 щелчков из 20, ручка на 4 с делением на 2 — 40 (13г.1).

Меню мигает на соседний пункт посреди поворота. Уровень считают округлением (CNT + 2) / 4: дребезг среднего перехода — 30 лишних событий на 135 щелчков (13г.5).

После включения щелчки стоят посередине. Таймер запустили раньше, чем поднялись подтяжки: CNT = 2, на реверсе теряется щелчок. Позиция — с фазы на выводах после паузы (13г.5).

Меню перескакивает через край. ARR = 47 и разность int16_t: 0 → 47 — это +47, 58 событий вместо 28 (13г.4).

KY-040 с подтяжками к 5 В на 3,3-вольтовом входе. Если на плате подтяжки идут к выводу «+», а он от 5 В, то и A, B — 5 В. Выводы без допуска 5 В (PA0, PA1, PA6, PA7 — с. 29) выдерживают на входе не больше 4,0 В (таблица 6, с. 37); ток пойдёт через защитный диод в питание 3,3 В, при подтяжке 10 кОм — до (5 − 3,3) / 10 кОм = 0,17 мА. Таблица 7 разрешает ±5 мА, но в обычной работе такой инжекции надо избегать (с. 61). Выводы FT (PB6, PB7) 5 В держат, но только с выключенной подтяжкой внутри (примечание к таблице 35, с. 62) — поэтому в прошивке PB6 и PB7 настроены входом без подтяжки, 0100. Проще всего питать KY-040 от 3,3 В.

Быстрый поворот теряет щелчки. Дребезг дольше шага между переходами: при дребезге до 2 мс таймер точен до 120 щелчков в секунду, автомат с опросом 1 мс — до 80 (13г.3). RC 100 нФ на 200 щелчках в секунду съедает и настоящие переходы (13г.2).

Фильтр выкрутили на максимум, а дребезг остался. Самый длинный фильтр при 72 МГц — 14,2 мкс, дребезг — миллисекунды. Фильтр убирает искры и помехи, дребезг убирает квадратурный счёт (13г.4).


13г.8 Итоги

Полные исходники

enc13e.h — типы, пороги ускорения и кнопки, функции автомата, счёта по CNT, ускорения и кнопки.

/* enc13e.h — раздел 13г: механический энкодер с кнопкой (EC11, KY-040) без регистров микроконтроллера.
 * Автомат по таблице переходов (опрос выводов), счёт щелчков по CNT таймера в режиме энкодера,
 * ускорение и кнопка. Один и тот же код собирается в прошивку fw13e.c (armclang) и в стенд bench13e.c
 * (arm-none-eabi-gcc, QEMU). AB — два бита: A — старший (канал TI1), B — младший (TI2); 1 — контакт разомкнут. */
#ifndef ENC13E_H
#define ENC13E_H
#include <stdint.h>

#define ENC_REST           3u       /* в щелчке оба контакта разомкнуты: A = B = 1 */
#define ENC_ACC_SLOW_MS    50u      /* щелчки реже — шаг 1 (меньше 1 оборота в секунду при 20 щелчках) */
#define ENC_ACC_FAST_MS    25u      /* чаще — шаг ENC_STEP_FAST (больше 2 оборотов в секунду) */
#define ENC_STEP_MID       5u
#define ENC_STEP_FAST      25u      /* 1000 / 40: весь ход за 2 оборота по 20 щелчков */
#define ENC_VMAX           1000u
#define ENC_KEY_STABLE_MS  20u      /* антидребезг кнопки: уровень не менялся 20 мс */
#define ENC_KEY_LONG_MS    800u

/* ── автомат по таблице переходов: опрос AB ─────────────────────────── */
typedef struct {
    uint8_t ab;                     /* прошлое состояние AB */
    uint8_t per;                    /* переходов на щелчок: 4 или 2 */
    int8_t acc;                     /* шаги со времени последнего щелчка */
} enc_qdec_t;

void enc_qdec_init(enc_qdec_t *q, uint8_t ab, uint8_t per);
int enc_qdec_step(enc_qdec_t *q, uint8_t ab);           /* +1, −1 — щелчок; 0 — нет */

/* ── счётчик таймера: щелчки по CNT ───────────────────────────────────── */
typedef struct {
    uint16_t last;                  /* прошлое показание CNT */
    uint16_t top;                   /* ARR + 1 */
    uint8_t per;
    int32_t pos;                    /* отсчёты с начала, со знаком */
    int32_t ref;                    /* отсчёт последнего щелчка */
} enc_tim_t;

int enc_phase(uint8_t ab);                              /* AB → фаза 0…3 по часовой стрелке */
void enc_tim_init(enc_tim_t *e, uint16_t cnt, uint8_t ab, uint8_t per, uint16_t arr);
int enc_tim_step(enc_tim_t *e, uint16_t cnt);           /* щелчков со знаком с прошлого вызова */

/* ── ускорение: шаг растёт, когда щелчки идут часто ──────────────────── */
typedef struct {
    uint32_t t_last;
    int8_t dir;                     /* 0 — щелчков ещё не было */
} enc_accel_t;

int32_t enc_accel(enc_accel_t *a, int ev, uint32_t now_ms);

/* ── кнопка: антидребезг, короткое и долгое нажатие, нажатие с поворотом ─ */
enum { ENC_KEY_NONE = 0, ENC_KEY_SHORT, ENC_KEY_LONG, ENC_KEY_TURN };

typedef struct {
    uint8_t raw, stable, long_sent, turned;
    uint32_t t_change, t_down;
} enc_key_t;

int enc_key_step(enc_key_t *k, int pressed, int turned, uint32_t now_ms);

#endif

enc13e.c — таблица переходов, щелчки по CNT с гистерезисом и модулем ARR + 1, ускорение, кнопка.

/* enc13e.c — раздел 13г: автомат по таблице переходов, щелчки по CNT таймера, ускорение и кнопка.
 * Микроконтроллера не знает: на вход — AB, CNT и время в миллисекундах. */
#include "enc13e.h"

/* индекс — (прошлое AB << 2) | новое AB; +1 — шаг по часовой (11 → 01 → 00 → 10 → 11), −1 — против,
 * 0 — стоим на месте или перескочили через состояние (оба бита сразу: направление неизвестно) */
static const int8_t qtab[16] = {
     0, -1, +1,  0,     /* из 00: в 00, 01, 10, 11 */
    +1,  0,  0, -1,     /* из 01 */
    -1,  0,  0, +1,     /* из 10 */
     0, +1, -1,  0,     /* из 11 */
};

void enc_qdec_init(enc_qdec_t *q, uint8_t ab, uint8_t per)
{
    q->ab = ab & 3u;
    q->per = per;
    q->acc = 0;
}

/* щелчок засчитывается только в состоянии покоя, и там же сбрасывается счёт шагов: дребезг даёт +1 −1
 * и до покоя не доходит, а один пропущенный шаг (перескок) округление ещё прощает */
int enc_qdec_step(enc_qdec_t *q, uint8_t ab)
{
    int ev = 0, half = q->per / 2;
    ab &= 3u;
    q->acc = (int8_t)(q->acc + qtab[(q->ab << 2) | ab]);
    q->ab = ab;
    if (ab == ENC_REST || (q->per == 2u && ab == 0u)) {
        ev = q->acc >= 0 ? (q->acc + half) / q->per : -((-q->acc + half) / q->per);
        q->acc = 0;
    }
    return ev;
}

int enc_phase(uint8_t ab)
{
    static const uint8_t ph[4] = {2, 1, 3, 0};          /* AB 00, 01, 10, 11 → фаза */
    return ph[ab & 3u];
}

/* CNT в момент запуска может стоять не на щелчке: подтяжки поднимали линии, ручку держали между
 * щелчками. Поэтому позиция начинается с фазы, прочитанной на выводах, а не с нуля */
void enc_tim_init(enc_tim_t *e, uint16_t cnt, uint8_t ab, uint8_t per, uint16_t arr)
{
    e->last = cnt;
    e->top = (uint16_t)(arr + 1u);                      /* ARR = 0xFFFF → 0: полный круг 65536 */
    e->per = per;
    e->pos = enc_phase(ab) % per;
    if (e->pos > per / 2)
        e->pos -= per;                                  /* к ближайшему щелчку: фаза 3 — это −1 */
    e->ref = 0;
}

/* разность показаний по модулю ARR + 1, к ближайшему; щелчок — когда позиция ушла от прошлого щелчка
 * на целый щелчок: гистерезис в щелчок, дребезг на любом переходе событий не плодит */
int enc_tim_step(enc_tim_t *e, uint16_t cnt)
{
    int32_t d, top = e->top ? e->top : 65536;
    int ev = 0;
    d = (int32_t)cnt - (int32_t)e->last;
    if (d > top / 2)
        d -= top;
    else if (d < -top / 2)
        d += top;
    e->last = cnt;
    e->pos += d;
    while (e->pos - e->ref >= e->per) {
        e->ref += e->per;
        ev++;
    }
    while (e->pos - e->ref <= -(int32_t)e->per) {
        e->ref -= e->per;
        ev--;
    }
    return ev;
}

/* шаг по времени с прошлого щелчка в ту же сторону; смена направления или пауза — снова шаг 1 */
int32_t enc_accel(enc_accel_t *a, int ev, uint32_t now_ms)
{
    int8_t dir;
    int32_t step = 1;
    if (ev == 0)
        return 0;
    dir = ev > 0 ? 1 : -1;
    if (dir == a->dir) {
        uint32_t dt = now_ms - a->t_last;
        if (dt < ENC_ACC_FAST_MS)
            step = ENC_STEP_FAST;
        else if (dt < ENC_ACC_SLOW_MS)
            step = ENC_STEP_MID;
    }
    a->t_last = now_ms;
    a->dir = dir;
    return ev * step;
}

/* звать раз в 1 мс; turned — был щелчок в этот вызов */
int enc_key_step(enc_key_t *k, int pressed, int turned, uint32_t now_ms)
{
    uint8_t r = pressed ? 1u : 0u;
    if (k->stable && turned)
        k->turned = 1u;                                 /* крутили с нажатой кнопкой */
    if (r != k->raw) {                                  /* дребезг или настоящее изменение — ждём */
        k->raw = r;
        k->t_change = now_ms;
        return ENC_KEY_NONE;
    }
    if (r != k->stable && now_ms - k->t_change >= ENC_KEY_STABLE_MS) {
        k->stable = r;
        if (r) {
            k->t_down = now_ms;
            k->long_sent = k->turned = 0u;
            return ENC_KEY_NONE;
        }
        if (k->turned)
            return ENC_KEY_TURN;                        /* нажатие с поворотом — не короткое */
        return k->long_sent ? ENC_KEY_NONE : ENC_KEY_SHORT;
    }
    if (k->stable && !k->long_sent && !k->turned && now_ms - k->t_down >= ENC_KEY_LONG_MS) {
        k->long_sent = 1u;
        return ENC_KEY_LONG;
    }
    return ENC_KEY_NONE;
}

fw13e.c — прошивка STM32F103: TIM4 в режиме энкодера на PB6 и PB7, кнопка на PB8, всё в SysTick раз в 1 мс.

/* fw13e.c — раздел 13г: ручка-энкодер с кнопкой на STM32F103 (BluePill), 72 МГц, такт TIM4 — 72 МГц.
 * TIM4 в режиме энкодера: PB6 — A (TIM4_CH1), PB7 — B (TIM4_CH2), оба с допуском 5 В. На плате у каждой
 * линии подтяжка 10 кОм к 3,3 В, 10 кОм последовательно к выводу и 10 нФ на землю. Кнопка — PB8 на землю,
 * подтяжка внутри. SysTick раз в 1 мс: щелчки по CNT, тот же счёт автоматом по выводам (для сверки),
 * ускорение, кнопка. Отпущенная кнопка — ручка меняет value (0…1000) с ускорением; нажатая — пункт меню
 * по одному. Тактирование до 72 МГц — SystemInit (раздел 6). */
#include "stm32f10x.h"
#include "enc13e.h"

#define CPU_HZ   72000000u
#define PER      4u                                     /* переходов на щелчок: проверить своей ручкой */

static enc_tim_t et;
static enc_qdec_t eq;
static enc_accel_t ea;
static enc_key_t ek;
volatile uint32_t ms;
volatile int32_t value, menu, tim_sum, qdec_sum;        /* tim_sum ≠ qdec_sum — что-то не так с линиями */
volatile uint32_t key_short, key_long, key_turn;

static uint8_t read_ab(void)
{
    uint32_t idr = GPIOB->IDR;
    return (uint8_t)((((idr >> 6) & 1u) << 1) | ((idr >> 7) & 1u));
}

static void delay_ms(uint32_t n)
{
    uint32_t t0 = DWT->CYCCNT;
    while (DWT->CYCCNT - t0 < n * (CPU_HZ / 1000u)) {
    }
}

static void enc_hw_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
    /* PB6, PB7 — вход без подтяжки (0100): подтяжки на плате; PB8 — вход с подтяжкой (1000 и ODR8 = 1) */
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0x44000000u;
    GPIOB->CRH = (GPIOB->CRH & ~0xFu) | 0x8u;
    GPIOB->BSRR = 1u << 8;
    /* TI1 и TI2 — входы каналов 1 и 2, фильтр 1111: 8 выборок на f_DTS / 32 */
    TIM4->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_0 | TIM_CCMR1_IC1F | TIM_CCMR1_IC2F;
    TIM4->CCER = 0;                                     /* CC1P = CC2P = 0; CC1P = 1 — счёт в обратную сторону */
    TIM4->SMCR = TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0;       /* 011: оба фронта обоих каналов */
    TIM4->ARR = 0xFFFFu;
    TIM4->CR1 = TIM_CR1_CKD_1 | TIM_CR1_CEN;            /* f_DTS = 72 / 4 = 18 МГц: 8 / 562,5 кГц = 14,2 мкс */
}

void SysTick_Handler(void)
{
    int ev = enc_tim_step(&et, (uint16_t)TIM4->CNT);
    int32_t v;
    ms++;
    tim_sum += ev;
    qdec_sum += enc_qdec_step(&eq, read_ab());
    if (ek.stable)
        menu += ev;                                     /* кнопка нажата: пункт меню по одному */
    else {
        v = value + enc_accel(&ea, ev, ms);
        value = v < 0 ? 0 : (v > (int32_t)ENC_VMAX ? (int32_t)ENC_VMAX : v);
    }
    switch (enc_key_step(&ek, (GPIOB->IDR & (1u << 8)) == 0u, ev != 0, ms)) {
    case ENC_KEY_SHORT: key_short++; break;
    case ENC_KEY_LONG: key_long++; break;
    case ENC_KEY_TURN: key_turn++; break;
    default: break;
    }
}

int main(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    enc_hw_init();
    delay_ms(5u);                                       /* линии успокоились: 25 постоянных времени RC по 0,2 мс */
    enc_tim_init(&et, (uint16_t)TIM4->CNT, read_ab(), PER, 0xFFFFu);
    enc_qdec_init(&eq, read_ab(), PER);
    (void)SysTick_Config(CPU_HZ / 1000u);
    for (;;)
        __WFI();
}

Дальше: раздел 14 «АЦП» — оцифровка сигналов, режимы преобразования и связка с DMA и таймером.

Открыть в интерактивном справочнике