Защита от дребезга механического энкодера

На плате отладочной платы RLN Electro имеется энкодер. Удобная штука для навигации, вот только подход к нему несколько иной, нежели к описанному ЗДЕСЬ энкодеру. Ибо механический он, дребезгу подвержен.
Сам дребезг выглядит вот так вот:

Такая вот гадость приносит много радости тому, кто следит за энкодером при помощи прерываний, а не опрашивает его с определенной частотой.

Представим себе для начала кнопку, которая сидит на INT и по которой мы считаем нарастающие фронты. Каждый фронт +1 к переменной.
Только дребезг, помимо основного лишних +20 накидает. Непорядок. Нужна защита от дребезга.
Один из способов — аппаратный. Ставим небольшой фильтр на кнопку и дело в шляпе. Смазываем фронт и не паримся по поводу фронта. Да вот только у нас микроконтроллер, и лишние детали снаружи — чаще всего таки лишние детали снаружи.
Программное подавление дребезга. Одним из дубовых методов программного подавления дребезга является метод Set/Reset. Его достоинство — гарантированная защита от дребезга. Недостаток — необходим трехстабильный вход. В микроконтроллерных условиях — два входа и кнопка, имеющая гарантированный гистерезис между состояниями Вкл и Выкл. Фронт на первом проводе устанавливает единицу, фронт на втором проводе — сбрасывает. Стандартный триггер.
Выглядит это примерно следующим образом:

В качестве кнопки вполне может оказаться большая красная кнопка, задача которой — Экстренно что-то там остановить, при этом дребезг никак другим способом не убрать.
Но в нашем случае у нас уже имеется аналог нашей трехстабильной кнопки — наш энкодер. У него имеется два сигнала с гарантированным сдвигом друг относительно друга. Если сдвига нет, то, это плохой энкодер.
Сигналом Set тогда будет нарастающий фронт сигнала A, сигналом Reset — сигнала B.
Энкодер наш подключен к выводам PJ5 и PJ6, на которых висит функция PCINT14 и PCINT15. Здесь кроется первая подстава.
Несмотря на то, что ноги вроде как 5 и 6 биты порта, PCINT14 и PCINT15 являются 6 и 7 битами!

Сделаем для простоты пару дефайнов и объявим пару переменных:

#define LEDDIGITS 4//количесвто разрядов индикатора
#define ENCODERINPORT PINJ
#define ENCODEROUTPORT PORTJ
#define ENCODERSHIFT  5
#define PCINTA PCINT14 
#define PCINTB PCINT15
#include <util/delay.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include "taskmanager.h"
uint8_t EncPortData[2];//старое(0) и текущее (1) значение кода с энкодера
uint8_t EncFixData[2];//старое(0) и текущее (1) значение кода с энкодера после обработки
uint8_t DriveRealDirection;//направление вращения
int16_t DataLog;//Вывод данных на индикатор
uint8_t number_x=0;//текущий разряд
uint8_t hexnumber[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x67,0x77,0x7C,0x39,0x5E,0x79,0x71};

Функция настройки энкодера определяет прерывание, подтягивает Pull-Up резисторы и выбирает ножки, за которыми необходимо следить:
void EncoderSetup(){//настройка выводов энкодера
	ENCODEROUTPORT|=(0x03<<ENCODERSHIFT);//подводим PullUp резисторы
	PCMSK1|=(1<<PCINTA)|(1<<PCINTB);//активируем ноги энкодера
	PCICR|=(1<<PCIE1);//активация PCINT1
}

Добавляем кусок кода для вывода информации на индикатор:
void led(){//функция выводит число на индикатор.
		PORTC=hexnumber[(DataLog>>(number_x*4))&0x0F];
		PORTA=(1<<(4+number_x));
		number_x++;
		if (number_x>(LEDDIGITS-1)) number_x=0;
}

Добавляем функцию main, настраивающую ножки, запускающую менеджер и разрешающую прерывания:
int main(void)
{
	DDRA=0xF0;//индикатор. разряды
	DDRC=0xFF;//индикатор. сегменты
	EncoderSetup();
	TSetup();
	TStart();
	TReload(0,*led,1);
	sei();
    while(1) {   }
}

Ну и самое главное, функция обработки прерывания.
Как оказалось при эксплуатации энкодера, его надо не только от дребезга спасать, но и от пользователя. Поскольку энкодер имеет четко устойчивые положения за счет кулачкового механизма, после отпускания ручки энкодера, он будет стремиться занять устойчивое положение, добавляя или уменьшая показания. Тут поможет только небольшая задержка.

ISR (PCINT1_vect){//срабатывает энкодер
_delay_us(40);
EncPortData[1]=((ENCODERINPORT)>>ENCODERSHIFT)&0x03;//прочли сырое значение
if ((EncPortData[1]&0x01) && !(EncPortData[0]&0x01))//нарастающий фронт по А!
    EncFixData[1]=1;
if (((EncPortData[1]>>1)&0x01) && !((EncPortData[0]>>1)&0x01))//нарастающий фронт по B!
	EncFixData[1]=0;
if (EncFixData[1] && !(EncFixData[0]))//нарастающий фронт нового цикла
	{//проверяем направление вращения:
		DriveRealDirection=(EncPortData[1]>>1)&0x01;
		if (!DriveRealDirection)//вправо крутим
			DataLog++;
		else
			DataLog--;
	}
EncPortData[0]=EncPortData[1];//обращаем значения	
EncFixData[0]=EncFixData[1];//обращаем значения		
}

Спасибо N1X за то, что напомнил про приоритет операции логического отрицания и что скобки решают многое

Что касается форсированного энкодера, то я себе такую задачу не ставлю по одной простой причине — наплатный механический энкодер в форсировании не нуждается, поскольку он должен позволять пользователю изменять переменную ровно на единицу за один шаг. А современные промышленные энкодеры бесконтактные, так что там проблемы дребезга просто нет.

Ну и небольшое демонстрационное видео:

2 комментария

avatar
Мне кажется, что функции типа «вывод информации на индикатор» неплохо бы оформить в качестве библиотеки. Потому, что когда читаешь статью про энкодер, кусок кода для вывода на индикатор как-то не в тему смотрится.

По идее, должно быть что-то типа такого:

#include "RlnElectro.h"

void main()
{
    RlnInit();
    RlnSegmentDisplay_Show(1234);
}
avatar
Будет. Реализую знаковый и десятичный вывод на индикатор и будет.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.