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 до ARRstate 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, и он должен совпадать с ручкой.
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 Итоги
- Квадратурный код: по часовой AB 11 → 01 → 00 → 10 → 11, один бит за
переход. Ручки бывают на 4 и на 2 перехода на щелчок — один щелчок и
разность
CNTпокажут; делитель не тот — 10 щелчков из 20 или 40. - Прерывание по спаду A с чтением B на 135 щелчках с дребезгом дало −70 вместо 45 и 1746 прерываний; RC 10 нФ (τ 0,1 и 0,2 мс) — −1; RC 100 нФ гасит весь дребезг модели, но на 200 щелчках в секунду ест переходы.
- Автомат по таблице из 16 переходов с щелчком только в покое AB = 11 считает точно; опрос раз в 1 мс выдерживает 80 щелчков в секунду при дребезге до 2 мс и 300 без дребезга.
- TIM4:
CC1S=CC2S= 01,SMS= 011,IC1F= 1111 иCKD= ÷4 — фильтр 14,2 мкс: убрал все 995 искр, без него потерян 1 щелчок из 80; точен до 120 щелчков в секунду при дребезге до 2 мс. - Энкодер — только на CH1 и CH2 одного таймера; TIM4 на PB6, PB7 и TIM1 на PA8, PA9 терпят 5 В, TIM2 и TIM3 на PA0, PA1, PA6, PA7 — нет.
ARRне 0xFFFF — разность по модулюARR+ 1: 12 и 28 вместо 0 и 58.- Щелчок — гистерезисом в целый щелчок от прошлого: 135 событий на 135
щелчков против 165 у округления; позиция при запуске — с фазы на
выводах, иначе после подтяжек
CNT= 2 и щелчок теряется на реверсе. - Кнопка: 20 мс и 800 мс, нажатие с поворотом — отдельно; 120 нажатий распознаны все. Ускорение: 50 и 25 мс, шаги 1, 5 и 25 — 706 за 58 щелчков, весь ход 1000 за 1,218 с.
Полные исходники
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 и таймером.