Подключение энкодера ?️ (ДУПа) к 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, но решать вам.

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

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

23:30

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

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

Сергей
11:17

Всё хорошо, но прерывания при изменении не работают никак. Думал что-то я не так делаю, скачал полный проект, залил, и тоже тишина. Счётчик считает, но прерываний не происходит (не поднимается флаг TIF).

21:41

Проверил:

у меня работает отлично, заходит в обработчик прерывания:

Вот на всякий случай копия кода:

 
#include "main.h"
/* Global Variables */
uint8_t Enc_Counter = 0;
/* Function prototypes */
void Enc_Trig_Int(void);
void GPIO_Initialization(void);
int main(void)
{
	//GPIO_Initialization(); // PA4 = TIM14-CH1 !!!
	CMSIS_PWM_Conf();
	/*
	 * PA6 - TIM3_CH1
	 * PA7 - TIM3_CH1
	 */
	/****** GPIO Initialization ******/
	/* 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);
	/* 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 = 100;
	Enc_Trig_Int();
	/* 1: Counter enabled */
	TIM3->CR1 |= TIM_CR1_CEN;
while(1){
	  Enc_Counter = TIM3->CNT;
	}
}
void Enc_Trig_Int(void){
	/* 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 GPIO_Initialization(void){
  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; /* Low Level Out */
}
void TIM3_IRQHandler(void){
	if(TIM3->SR & TIM_SR_TIF){
		TIM14->CCR1 = 25 + TIM3->CNT;
		TIM3->SR &= ~TIM_SR_TIF;
	}
}
 
Загрузка...