Подключение энкодера ?️ (ДУПа) к 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, но решать вам.
Да, сунулся было энкодер подключить к F030F4 и тут то понял — в нём существует только один TIM3, способный на это. По идее TIM1 тоже должен поддерживать, но… В добавок ко всему ещё и SPI (для дисплея) на тех же ножках сидит, а ремапа нет! Разочаровываюсь в STM32F030F4. Жаль, корпус у него малюсенький, удобный для миниатюрных вещей.
да, мк stm32f030f4p6 вроде и крутой, но вот и мне он во многом не подходит
хорошей заменой стал STM32F030K6T6, но уже в LQFP-32, он стоит (стоял 2$/5шт. на али) почти столько же, но больше памяти и ножек, советую присмотреться
Всё хорошо, но прерывания при изменении не работают никак. Думал что-то я не так делаю, скачал полный проект, залил, и тоже тишина. Счётчик считает, но прерываний не происходит (не поднимается флаг TIF).
Проверил:
у меня работает отлично, заходит в обработчик прерывания:
Вот на всякий случай копия кода: