Инкрементальный энкодер: подключение и обработка его с помощью AVR (ATmega8-16-32-168-328)

в 14:44, , рубрики: Atmega, avr, код Грея, метод прерываний, программирование микроконтроллеров, энкодер

В данной статье расскажу об энкодерах и попытаюсь объяснить, как их подключать и обрабатывать правильным способом с помощью микроконтроллера AVR (в примерах я использую ATmega8A-PU, но это должно работать на любом другом микроконтроллере, например, на ATmega32 или совместимом с Arduino ATmega168/328).

Немного теории

Инкрементальные энкодеры имеют два выхода, назовем их A и B. Когда мы вращаем ручку, на выходах A и B мы получаем фазовый сдвиг квадратичного сигнала. Этот сигнал представляет собой ничто иное, как двухбитный код Грея. На изображении ниже я нарисовал это в более читабельном виде.

Инкрементальный энкодер: подключение и обработка его с помощью AVR (ATmega8-16-32-168-328) - 1

Как мы видим на изображении, если энкодер поворачивается по часовой стрелке, то код Грея на выходах будет следующим: 2->3->1->0->2 и так далее. Если мы начнем поворачивать против часовой стрелки, мы получим следующую последовательность кода Грея на выходе: 3->2->0->1. Зная эту последовательность, мы можем определить направление вращения ручки. Это один из двух способов чтения направления энкодера.

Второй метод основан на обнаружении фронта спадающего сигнала на одном из выходов и проверки текущего состояния на другом выходе.

Инкрементальный энкодер: подключение и обработка его с помощью AVR (ATmega8-16-32-168-328) - 2

У этого метода есть одна очень важная проблема. Мы теряем половину точности вращательного энкодера, потому что мы обнаруживаем только каждый второй фронт сигнала. Это можно исправить, подключив второй выход энкодера к другому прерыванию в процессоре и обнаруживая противоположный фронт сигнала. Таким образом, если мы обнаруживаем спадающий фронт на выходе A, нам нужно обнаружить нарастающий фронт на выходе B.

Инкрементальный энкодер: подключение и обработка его с помощью AVR (ATmega8-16-32-168-328) - 3

Сделав это, мы восстановили нашу полную точность, но потеряли два прерывания в нашем микроконтроллере. Обычно нам не нужна такая высокая точность, и первое решение с использованием только одного прерывания на микроконтроллере достаточно хорошо.

Подключение к микроконтроллеру

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

Когда я подключаю один из этих энкодеров к микроконтроллеру, я всегда использую аппаратное подавление дребезга, потому что программное подавление дребезга может быть очень сложным и зависит от некоторых параметров, таких как текущая скорость вращения вала и т.д. Мое подавление дребезга - это простой RC-фильтр низкой частоты, как показано на рисунке ниже.

Инкрементальный энкодер: подключение и обработка его с помощью AVR (ATmega8-16-32-168-328) - 4

На схеме выше резистор и конденсатор, подключенные параллельно, создают фильтр низких частот с частотой среза, рассчитанной по следующей формуле: fg = 1 / 2*pi*R*C. Таким образом, если мы используем значения из схемы, мы получим:

fg = 1 / 2*3.1415*10000*0.0000001 ~= 159.2Hz

Решение на основе прерываний (пример на основе ATmega8)

Первый пример - это решение на основе прерываний, в котором мы обнаруживаем падающий и/или нарастающий фронт, и в зависимости от текущего состояния на втором пине мы определяем направление вращения вала.

Если мы подключим выходы A и B энкодера к пинам PD2 и PD3 микроконтроллера, нам нужно установить PD2 и PD3 как входы:

/* set PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD2);				/* PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD3);        
  PORTD |= (1 << PD3)|(1 << PD2);   /* PD2 and PD3 pull-up enabled   */

Затем мы включаем обработчик прерываний. Для этого нам нужно записать следующие биты в регистры GICR и MCUCR:

GICR |= (1<<INT0)|(1<<INT1);	// enable INT0 and INT1
MCUCR |= (1<<ISC01)|(1<<ISC11)|(1<<ISC10); // INT0 - falling edge, INT1 - raising edge

И мы также должны включить прерывания:

   /* enable interrupts */
  sei();

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

//INT0 interrupt 
ISR(INT0_vect ) 
{ 
	if(!bit_is_clear(PIND, PD3)) 
	{ 
		UART_putchar(*PSTR("+")); 
	} 
	else 
	{ 
		UART_putchar(*PSTR("-")); 
	} 
} 

//INT1 interrupt 
ISR(INT1_vect ) 
{ 
	if(!bit_is_clear(PIND, PD2)) 
	{ 
		UART_putchar(*PSTR("+")); 
	} 
	else 
	{ 
		UART_putchar(*PSTR("-")); 
	} 
}

Код для обработчика INT0 и INT1 очень похож. После возникновения прерывания, мы проверяем состояние второго входа, и это определяет текущее направление вала. В приведенном выше примере, если вал энкодера был повернут в правую сторону, отправляется символ "+", а если нет, то символ "-". Если мы удалим одно прерывание (INT0 или INT1 - это не имеет значения), следующий код все еще будет функциональным, но мы потеряем половину точности энкодера. Весь код выглядит следующим образом:

#define F_CPU 8000000 
#define UART_BAUD 9600				/* serial transmission speed */ 
#define UART_CONST F_CPU/16/UART_BAUD-1 

#include <stdio.h>
#include <avr/io.h>
#include <util/delay.h> 
#include <avr/pgmspace.h> 
#include <avr/interrupt.h> 

#include "uart.h" 

//INT0 interrupt 
ISR(INT0_vect ) 
{ 
	if(!bit_is_clear(PIND, PD3)) 
	{ 
		UART_putchar(*PSTR("+")); 
	} 
	else 
	{ 
		UART_putchar(*PSTR("-")); 
	} 
} 

//INT1 interrupt 
ISR(INT1_vect ) 
{ 
	if(!bit_is_clear(PIND, PD2)) 
	{ 
		UART_putchar(*PSTR("+")); 
	} 
	else 
	{ 
		UART_putchar(*PSTR("-")); 
	} 
} 

int main(void) 
{ 
  /* init uart */ 
  UART_init(UART_CONST); 

  /* set PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD2);				/* PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD3);        
  PORTD |= (1 << PD3)|(1 << PD2);   /* PD2 and PD3 pull-up enabled   */ 

  GICR |= (1<<INT0)|(1<<INT1);		/* enable INT0 and INT1 */ 
  MCUCR |= (1<<ISC01)|(1<<ISC11)|(1<<ISC10); /* INT0 - falling edge, INT1 - reising edge */ 

  /* enable interrupts */ 
  sei(); 


   while(1) 
   { 
	//do nothing ;) 
	_delay_ms(1); 
   } 

  return 0; 
}

Этот пример и все соответствующие файлы (makefile, исходный код и заголовочные файлы) свободно доступны в репозитории на GitHub здесь: https://github.com/leuconoeSH/avr-examples/tree/master/rotary-encoder/interrupt. В репозитории также содержатся процедуры для обработки передачи UART.

Метод кода Грея

Второй метод использует код Грея. В этом случае нам не нужны никакие прерывания вообще.

Давайте начнем с процедуры, которая преобразует два состояния на входах A и B (пин PD2 и пин PD3) в двоичное значение из 2 битов, где значение с PD2 будет первым старшим битом, а значение с PD3 будет нулевым старшим битом.

uint8_t read_gray_code_from_encoder(void ) 
{ 
 uint8_t val=0; 

  if(!bit_is_clear(PIND, PD2)) 
	val |= (1<<1); 

  if(!bit_is_clear(PIND, PD3)) 
	val |= (1<<0); 

  return val; 
}

В коде выше мы объявляем 8-битную беззнаковую переменную с начальным значением 0 (00000000b), затем проверяем, есть ли высокий логический уровень на пине PD2, и если есть, то ставим двоичную 1 на бит, который первый справа. Таким образом, мы получаем это значение: 00000010b. Затем мы делаем то же самое со вторым входным пином PD3, единственное отличие состоит в том, что теперь мы устанавливаем значение на позиции 0. Это дает нам 2-битовый код Грея, соответствующий состояниям на входах PD2 и PD3, в переменной val. Это значение может быть равно 0 (00b), 1 (01b), 2(10b) или 3 (11b).

Затем нам просто нужно записать это значение как начальное, и проверить, соответствует ли новое значение последовательности 2->3->1->0 или последовательности 3->2->0->1, после чего мы узнаем, в каком направлении повернулся вал нашего энкодера.

  /* ready start value */ 
  val = read_gray_code_from_encoder(); 

   while(1) 
   { 
	   val_tmp = read_gray_code_from_encoder(); 

	   if(val != val_tmp) 
	   { 
		   if( /*(val==2 && val_tmp==3) ||*/ 
			   (val==3 && val_tmp==1) || 
			   /*(val==1 && val_tmp==0) ||*/ 
			   (val==0 && val_tmp==2) 
			 ) 
		   { 
				UART_putchar(*PSTR("+")); 
		   } 
		   else if( /*(val==3 && val_tmp==2) ||*/ 
			   (val==2 && val_tmp==0) || 
			   /*(val==0 && val_tmp==1) ||*/ 
			   (val==1 && val_tmp==3) 
			 ) 
		   { 
				UART_putchar(*PSTR("-")); 
		   } 

		   val = val_tmp; 
	   } 

	   _delay_ms(1); 
   }

Используя код выше, последовательности 2->3, 1->0, 3->2 и 0->1 были закомментированы, потому что они соответствуют переходному состоянию энкодера, и если бы мы оставили их раскомментированными, то каждый отдельный "клик" энкодера генерировал бы два импульса.

Если мы не хотим включать процедуры энкодера в основной цикл нашей программы, мы можем использовать внутренний таймер/счётчик, чтобы запускать эту процедуру в прерывании каждый раз, когда счётчик переполняется. Весь код будет выглядеть так:

#define F_CPU 8000000						/* crystal f				 */ 
#define UART_BAUD 9600						/* serial transmission speed */ 
#define UART_CONST F_CPU/16/UART_BAUD-1 

#include <stdio.h> 
#include <avr/io.h> 
#include <util/delay.h> 
#include <avr/pgmspace.h> 
#include <avr/interrupt.h> 
#include "uart.h" 

uint8_t read_gray_code_from_encoder(void ) 
{ 
 uint8_t val=0; 

  if(!bit_is_clear(PIND, PD2)) 
	val |= (1<<1); 

  if(!bit_is_clear(PIND, PD3)) 
	val |= (1<<0); 

  return val; 
} 

int main(void) 
{ 
  uint8_t val=0, val_tmp =0; 

  /* init UART */ 
  UART_init(UART_CONST); 

  /* set PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD2);				/* PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD3);        
  PORTD |= (1 << PD3)|(1 << PD2);   /* PD2 and PD3 pull-up enabled   */ 

  /* ready start value */ 
  val = read_gray_code_from_encoder(); 

   while(1) 
   { 
	   val_tmp = read_gray_code_from_encoder(); 

	   if(val != val_tmp) 
	   { 
		   if( /*(val==2 && val_tmp==3) ||*/ 
			   (val==3 && val_tmp==1) || 
			   /*(val==1 && val_tmp==0) ||*/ 
			   (val==0 && val_tmp==2) 
			 ) 
		   { 
				UART_putchar(*PSTR("+")); 
		   } 
		   else if( /*(val==3 && val_tmp==2) ||*/ 
			   (val==2 && val_tmp==0) || 
			   /*(val==0 && val_tmp==1) ||*/ 
			   (val==1 && val_tmp==3) 
			 ) 
		   { 
				UART_putchar(*PSTR("-")); 
		   } 

		   val = val_tmp; 
	   } 

	   _delay_ms(1); 
   } 

  return 0; 
}

Весь код (включая makefile, исходный код и заголовочные файлы), а также uart.h и соответствующий uart.c, можно найти в репозитории GitHub здесь: https://github.com/leuconoeSH/avr-examples/tree/master/rotary-encoder/normal.

Заключение

Как вы можете видеть, обработка вращающегося инкрементального энкодера на самом деле очень проста, и большой проблемой является качество самого энкодера и его дребезг. Решением является использование оптического энкодера, но это очень дорогое решение. Например, оптический энкодер может стоить около 100 евро (~140 долларов США), в то время как дешевый механический стоит всего несколько центов. Так что не остается ничего другого, кроме как пожелать вам, мои дорогие читатели, множество успешных экспериментов и проектов с энкодерами!

Автор: Александр Францевич

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js