Подключение энкодера 🕹️ (ДУПа) к STM32 (CMSIS и HAL)

ДУП (энкодер) в быту

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

Приобрести на Aliexpress

🏷️ KY-040 360 Degrees EC11 (0.71💲): https://ali.ski/jO8Yy5
🏷️ 360 Degrees Rotary Encoder (0.46💲): https://ali.ski/gLMvN
🏷️ USB Logic Analyze 24M 8CH (4.69💲): https://ali.ski/nD1A0
🏷️ Logic Analyzer 24M 8CH (5.30💲): https://ali.ski/xQOmAG
🏷️ STM32F030F4P6 Board (1.20💲): https://ali.ski/OB3kK
🏷️ 0.91 inch OLED (1.42💲): https://ali.ski/zjDu4f
🏷️ SG90 Servo 9g (0.77💲): https://ali.ski/NqE4g

🛒 Мой мультиметр T21D RM113D ( ~13.81💲): https://ali.ski/-04vOL

Как работают инкрементальные энкодеры

Не вдаваясь в конструкционные особенности сразу подключим датчик к осциллографу или  логическому анализатору:

Ага, при вращении вала датчика по часовой стрелки сначала изменяется уровень на выводе DT, и только через небольшой промежуток времени на CLK. Если же вращение осуществлять против часовой стрелки, то первым идет CLK, а запаздывает DT.

Поддержка

Сначала нужно понять что какой-то из таймеров может работать с энкодером, в случае с STM32F030F4P6 это Таймер 3, который имеет аппаратную поддержку обработки сигналов с ДУПа.

Теперь нужно узнать по блок-схеме таймера к каким каналам идут входы:

Здесь это первый и второй каналы Таймера 3:

TIM3_CH1 TI1FP1
TI1FP2

TIM3_CH2 TI1FP1
TI1FP2

Теперь находим номера этих выводов:

Они должны быть настроены на альтернативный вход.

Схема подключение к микроконтроллеру

Здесь также можно подключить кнопку SW.

Программирование

Настраиваем два входа (первый и второй канал Таймера 3) (PA6, PA7) как альтернативный вход, без подтяжки (если резисторы установлены на плате).

/* * PA6 - TIM3_CH1 * PA7 - TIM3_CH1 */ /* GPIOA Clock */ RCC->AHBENR |= RCC_AHBENR_GPIOAEN; /* 10: Alternate function mode */ GPIOA->MODER &= ~GPIO_MODER_MODER6_0; GPIOA->MODER |= GPIO_MODER_MODER6_1; /* 00: No pull-up, pull-down */ GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR6_0; GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR6_1; /* 0001: AF1 */ GPIOA->AFR[0] |= (1UL << GPIO_AFRL_AFSEL6_Pos); /* 10: Alternate function mode */ GPIOA->MODER &= ~GPIO_MODER_MODER7_0; GPIOA->MODER |= GPIO_MODER_MODER7_1; /* 00: No pull-up, pull-down */ GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR7_0; GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR7_1; /* 0001: AF1 */ GPIOA->AFR[0] |= (1UL << GPIO_AFRL_AFSEL7_Pos);

Чтобы узнать номер альтернативной функции обращаемся к основному документу (здесь это AF1):

Теперь собственно настройка Таймера 3 в режиме поворотного (инкрементального) энкодера:

/* Encoder Initialization */ /* TIM3 Clock */ RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; /* 01: CC1 channel is configured as input, IC1 is mapped on TI1 * 01: CC2 channel is configured as input, IC2 is mapped on TI2 */ TIM3->CCMR1 |= (TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_0); TIM3->CCMR1 &= ~(TIM_CCMR1_CC1S_1 | TIM_CCMR1_CC2S_1); /* 00: noninverted/rising edge */ TIM3->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC2P); TIM3->CCER &= ~(TIM_CCER_CC2NP | TIM_CCER_CC2NP); /* 001: Encoder mode 1 - Counter counts up/down on TI2FP1 edge depending on TI1FP2 level */ TIM3->SMCR |= TIM_SMCR_SMS_0; TIM3->SMCR &= ~TIM_SMCR_SMS_1; TIM3->SMCR &= ~TIM_SMCR_SMS_2; /* 1111: fSAMPLING = fDTS / 32, N = 8 */ TIM3->CCMR1 |= (TIM_CCMR1_IC1F_0 | TIM_CCMR1_IC1F_1 | TIM_CCMR1_IC1F_2 | TIM_CCMR1_IC1F_3); TIM3->CCMR1 |= (TIM_CCMR1_IC2F_0 | TIM_CCMR1_IC2F_1 | TIM_CCMR1_IC2F_2 | TIM_CCMR1_IC2F_3); /* Auto-Reload Register (MAX counter number) */ TIM3->ARR = 30; /* 1: Counter enabled */ TIM3->CR1 |= TIM_CR1_CEN;

То же самое на HAL с кубом (STM32CubeIDE)

Новый проект: File -> New -> STM32 Project

Выбор микроконтроллера: STM32F030F4P6

Любое имя: Encoder-HAL

Врубаем отладчик: SYS -> Debug Serial Wire
Включаем подключение энкодера к Таймеру 3: Timers -> TIM3 -> Combined Channels -> Encoder Mode
Также советую врубить программный фильтр: Input Filter: 6

Открываем файл: main.c

Остается запустить таймер в режиме энкодера:

/* USER CODE BEGIN 2 */ /* Rotary (Incremental) encoder */ HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL); /* USER CODE END 2 */

Проверка (отладка)

Итак, сначала нужно правильно подключить (согласное монтажной схеме вначале):

Для проверки можно смотреть на счетчик-регистр, но в таком случае нужно останавливать работу программы:

Но лучше создать глобальную переменную Enc_Counter

/* USER CODE BEGIN PV */ /* Enc. vars */ uint8_t Enc_Counter = 0; /* USER CODE END PV */

А в главном цикле присваивать ей значения из регистра-счетчика:

/* USER CODE BEGIN WHILE */ while (1) { Enc_Counter = TIM3->CNT; /* USER CODE END WHILE */

Так гораздо удобней, т.к. изменения сразу видны:

Сравнение занимаемой памяти

Здесь можно глянуть сколько памяти занимает код настройки таймера в режиме энкодера и соответствующих выводов при непосредственном записи в регистры (CMSIS)  и с использованием библиотеки HAL:

💽 RAM: 0.48 кБ
💾 FLASH: 1.91 кБ

💽RAM: 0 кБ
💾FLASH: 0.284 кБ

Таким образом в сравнении  :

💽RAM: на 0.48 кБ больше
💾FLASH: в 6.71 раз больше

Прерывание при изменении значения (прерывания таймера stm32 в режиме энкодера)

Может возникнуть необходимость обновления показаний, например на  светодиодный индикатор, в таком случае можно не выводить постоянно с какой-то периодичностью значение из счетчика (TIM3->CNT), а делать это только тогда, когда происходить инкрементация или декрементация.

Здесь это сделано с помощью триггера Trigger Inout 1 Filtered Edge Detector, ну то есть он реагирует на изменение фронта.

/* Trigger Edge Detector */ /* 100: TI1 Edge Detector (TI1F_ED) */ TIM3->SMCR &= ~(TIM_SMCR_TS_0 | TIM_SMCR_TS_1); TIM3->SMCR |= TIM_SMCR_TS_2; /* 1: Trigger interrupt enabled. */ TIM3->DIER |= TIM_DIER_TIE; NVIC_EnableIRQ(TIM3_IRQn);

И также обработчик прерывания, в котором проверяем откуда именно пришло прерывания, делаем что нужно (здесь это переключение светодиода) и обязательно сбрасываем бит соответствующий (иначе застрянете в прерывании!).

void TIM3_IRQHandler(void){ if(TIM3->SR & TIM_SR_TIF){ /* LED */ GPIOA->ODR ^= GPIO_ODR_4; /* Interrupt enabled */ TIM3->SR &= ~TIM_SR_TIF; } }

Также естественно нужно не забыть настроить вывод PA4 (к нему подключен светодиод).

RCC->AHBENR |= RCC_AHBENR_GPIOAEN; /* GPIOA Clock */ GPIOA->MODER |= GPIO_MODER_MODER4_0; /* 01: General purpose output mode */ GPIOA->MODER &= ~GPIO_MODER_MODER4_1; GPIOA->OTYPER &= ~GPIO_OTYPER_OT_4; /* 0: Output push-pull */ GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR4_0; /* 01: Medium speed */ GPIOA->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR4_1; GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR4_0; /* 00: No pull-up, pull-down */ GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR4_1; GPIOA->BSRR |= GPIO_BSRR_BS_4; /* LED OFF */

Пример 

Дешевые сервомашинки управляются ШИМ-сигналом 50 Гц путем изменения ширины импульса (в диапазоне (1...2) мс при периоде соответственно 1/50 = 20 мс). Умея создавать ШИМ-сигнал с помощью таймера можно сделать управление с помощью ДУПа (который известно уже как подключать).

RCC->AHBENR |= RCC_AHBENR_GPIOAEN; /* GPIOA Clock */ RCC->APB1ENR |= RCC_APB1ENR_TIM14EN; /* TIM14 Clock */ GPIOA->MODER &= ~GPIO_MODER_MODER4_0; /* 10: Alternate function mode */ GPIOA->MODER |= GPIO_MODER_MODER4_1; GPIOA->OTYPER &= ~GPIO_OTYPER_OT_4; /* 0: Output push-pull (reset state) */ GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR4_0; /* 01: Medium speed */ GPIOA->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR4_1; GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR4; /* 00: No pull-up, pull-down */ GPIOA->AFR[0] |= (4UL << GPIO_AFRL_AFRL4_Pos); TIM14->PSC = 160-1; TIM14->ARR = 1000-1; TIM14->CCR1 = 0; TIM14->CCMR1 |= (TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE); TIM14->CCER |= TIM_CCER_CC1E; TIM14->BDTR |= TIM_BDTR_MOE; TIM14->CR1 |= TIM_CR1_CEN; TIM14->EGR |= TIM_EGR_UG;

Можно было бы в главном цикле постоянно запихивать значение из счетчика Таймера 3 (TIM3->CNT) в регистр захвата/сравнения (TIMx->CCR1) - и соответственно вращением вала энкодера вращать вал сервы. Но в таком случае даже когда нет изменений это будет выполнятся. Умея создавать прерывания при изменении фронта на входе TIM3-CH1 можно осуществить присваивание нового значения только при переходах в обработчике прерывания.

void TIM3_IRQHandler(void){ if(TIM3->SR & TIM_SR_TIF){ TIM14->CCR1 = 25 + TIM3->CNT; /* Clear flag */ TIM3->SR &= ~TIM_SR_TIF; } }

Видос

Итого

По итогу отмечу, что настройка выводов на серии F1XX будет проще, чем на F0XX.
Полные проекты на Github: https://github.com/Egoruch/Incremental-Encoder-STM32-CMSIS-HAL

Занимаемая память для МК с 16 кБ ПЗУ великовата, поэтому можно было бы посоветовать прогать на регистрах, НО я бы советовал просто сменить микросхему, ведь добавив пару центов можно получить удвоение по памяти, больше ножек и возможностей (например заменой может быть STM32F030K6T6 32 кБ Флеш, IO: 25, но решать вам.

3360
RSS
Владимир
21:52

Да, сунулся было энкодер подключить к F030F4 и тут то понял — в нём существует только один TIM3, способный на это. По идее TIM1 тоже должен поддерживать, но… В добавок ко всему ещё и SPI (для дисплея) на тех же ножках сидит, а ремапа нет! Разочаровываюсь в STM32F030F4. Жаль, корпус у него малюсенький, удобный для миниатюрных вещей.

23:30

да, мк stm32f030f4p6 вроде и крутой, но вот и мне он во многом не подходит

хорошей заменой стал STM32F030K6T6, но уже в LQFP-32, он стоит (стоял 2$/5шт. на али) почти столько же, но больше памяти и ножек, советую присмотреться

Загрузка...