worklog: инкрементальный энкодер и STM32

Aug 19, 2017 19:39


Некоторые (но не все, сверяйтесь с даташитами) таймеры в STM32 имеют режим работы в качестве счётчика для инкрементального квадратурного энкодера. Для этого используется ввод внешних сигналов TI1 и TI2. Инициализация таймера выглядит примерно так (GPIO уже настроен как надо, таймер TIM4, чип STM32F746):

RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
RCC->APB1RSTR |= RCC_APB1RSTR_TIM4RST;
RCC->APB1RSTR &= ~RCC_APB1RSTR_TIM4RST;

TIM4->CR1 = 0;
TIM4->CR2 = 0;
TIM4->PSC = 0; // прескалера нет
TIM4->CNT = 0;
TIM4->ARR = -1; // roll-over через тут
TIM4->SMCR = (0x03 << TIM_SMCR_SMS_Pos); // Encoder mode 3
TIM4->DIER = 0; // ни DMA, ни прерывания не используются;
TIM4->DCR = 0;
TIM4->CCMR1 =
(0x0C << TIM_CCMR1_IC1F_Pos) | // fSAMPLING=fDTS/16, N=8
(0x0C << TIM_CCMR1_IC2F_Pos) | // fSAMPLING=fDTS/16, N=8
(0x01 << TIM_CCMR1_CC2S_Pos) | // CC2 channel is configured as input, IC2 is mapped on TI2
(0x01 << TIM_CCMR1_CC1S_Pos); // CC1 channel is configured as input, IC1 is mapped on TI1
TIM4->CCMR2 = 0;
TIM4->CCER =
(0 << TIM_CCER_CC1P_Pos) | // неинвертирующий вход
(0 << TIM_CCER_CC2P_Pos); // неинвертирующий вход
TIM4->CR1 |= TIM_CR1_CEN; // включить таймер

В этом режиме таймер представляет собой просто делитель-счётчик, одно- или двунаправленный (в зависимости от настроек в SMCR). При таких настройках таймера на один логический отсчёт энкодера значение счётчика меняется на плюс-минус 4 (т.к. это два фронта/спада двух сигналов), в более высокоуровневом коде это надо учитывать. Например, в большинстве распространённых механических энкодеров "с щелчком" (Alps/Bourns/китайский нонейм) на один щелчок будет именно 4 импульса. Хотя мне попадались странные энкодеры, выдававшие разное количество импульсов на щелчок (то 4, то 5) - видимо, такое качество изготовления. Ещё попадались энкодеры, у которых восходящие фронты разнесены по времени, как это и надо, а падающие происходят практически одновременно. В принципе, когда таймер затактирован очень высокой частотой (как в примере выше - прямо от PCLK1), он всё равно "чует" разницу моментов между ними, но при снижении частоты неизбежно ухудшается временное разрешение и такие энкодеры становятся неприменимы.

Для самого простого применения крутилки в качестве интерфейсного органа нужен будет ещё один таймер. Этим вторым таймером мы будем формировать периоды накопления импульсов, чтобы обновлять некую переменную, связанную с положением энкодера. Можно и напрямую с регистром CNT работать, но это не слишком неудобно, т.к. нет никакого автоматического способа узнать, что это значение изменилось. Поэтому заводим ещё один таймер (например, TIM14), чтобы он тикал с какой-то небольшой частотой порядка 5..10 герц. По его прерыванию будем заглядывать в регистр TIM4->CNT. Если его текущее значение отличается от предыдущего замера, вычисляем разницу (в целых со знаком) и прибавляем её к аккумулятору ("value"). Тут же выставляем флажок о том, что энкодер обновился.

То, что в инициализации таймера в регистр TIM4->ARR было вписано "-1", помогает упростить арифметику - не нужно специально следить за переходом через границы и т.п., это происходит как бы само собой из-за того, что типы переменных чётко определены.

{ // Проверка таймера TIM4 (Enc0)
IncEnc_t *enc = (IncEnc_t *)&gFlags.Enc[0];
enc->prev_reg = enc->reg;
enc->reg = TIM4->CNT;
enc->diff = (enc->reg - enc->prev_reg);
if (enc->diff != 0) {
enc->dir = (TIM4->CR1 & TIM_CR1_DIR) ? 1 : 0;
enc->value -= enc->diff; /* += или -= зависит от того, как мы хотим отобразить
физическое направление на направление счёта */
enc->updated = 1;
};
};
Здесь gFlags это некая структура, в которую вынесены глобальные переменные, мне кажется этот подход несколько более удобным для отладки (в дебагере можно смотреть сразу на всю структуру, не вынося отдельные переменные в отдельные записи watchlist). В эту структуру вложен массив других структур, хранящих инфу уже конкретно об энкодерах:

typedef struct {
uint16_t reg, prev_reg;
int16_t diff;
uint8_t updated:1, dir:1;
int32_t value;
int32_t min_value, max_value;
// ToDo: callback
} IncEnc_t;

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

радио

Previous post Next post
Up