Сбор данных с инкрементальных энкодеров с помощью МК AVR

Часто возникает необходимость сбора данных с инкрементальных энкодеров. Энкодер можно рассматривать как датчик, который позволяет измерить пройденный путь, скорость, ускорение. Последнее и предпоследнее более предпочтительно, так как позволяет получить вам векторную величину, имея вектор скорости двух точек робота в каждый момент времени можно построить траекторию робота с очень хорошей точностью. Мы называем этот уровень "Внешняя одометрия". Он представляет из себя 2 энкодера, платы измерения скорости и двухпоточное приложение, выполняющее роль интегратора, на выходе которого мы получаем плоские координаты.

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

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

Промышленный энкодер содержит,  как правило, следующие каналы:

Канал А  - основной канал энкодера

Канал B - второй канал, сдвинутый относительно первого на 90 градусов по фазе сигнала

Канал Z - обычно я считаю, что этот канал предназначен для счетчика оборотов и определения времени начала измерения ( нулевой точки)

Канал с чертой наверху - инвертированные каналы

Также энкодер имеет свое питание.

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

Чтобы понять работу энкодера нужно рассмотреть временные диаграммы:

При противоположном вращении канал B будет сдвинут на 180 градусов влево по фазе.

Правило определения направления вращения:

Обратим внимание на канал А и переход из нижнего уровня в верхний. Если во время этого перехода канал B имеет низкий уровень, то считаем, что энкодер вращается в обратном направлении, иначе в прямом.

Далее в нашей реализации мы будем делать это в прерывании, для определения направления нам понадобится всего лишь одна битовая операция.

Скорость измеряется как время между двумя логическими переходами одинакового вида ( для простоты из нижнего в верхний).

Зная разрешающую способность энкодера, несложно посчитать скорость в Гц. Чтобы обеспечить хорошую разрешающую способность мы будем измерять это время в микросекундах.

Обработка сигнала:

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

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

RC цепочка+триггер Шмидта

RC цепочка+триггер Шмидта

Сигнал на выходе RC цепочки

Сигнал на выходе цепочки

Сигнал на выходе триггера Шмидта

Сигнал на выходе триггера Шмидта

Input Capture Port with noise canceler

Input Capture Port with noise canceler

Итак, сигнал с канала А энкодера проходит через цепочку, а  с выхода триггера Шмидта подаем его на ножку ICP. Это специальный пин контроллера, позволяющий захватывать событие дискретного сигнала и довольно точно измерять частоту этого события. Не стоит забывать, что на этой ножке у контроллера реализована аппаратная фильтрация дребезга, что еще больше повышает надежность системы.  Через описанную выше цепь фильтра необходимо провести все сигналы энкодера. Сигнал с канала B можно подать на любой порт ввода/вывода.

Да, также можно подтянуть выход триггера Шмидта к верхнему уровню, но это можно и не делать, либо сделать это потом программно.

Теперь, практика показала, что применение стандартного триггера Шмидта SN74LS14N ни к чему хорошему не приводит. Я не вчитывался в тип используемой логики, но думаю что дело в несовместимости выхода SN74LS14N с входом контроллера AVR.  Зато отличную совместимость продемонстрировала отечественная схема КР1533ТЛ2, которую мы использовали уже в нескольких проектах.

Итак, вкратце с железной частью всё, перейдем к программной.

Мы будем использовать контроллер AVR для решения этой задачи, компилятор winAVR. На нужно:

1.  Таймер 1 для захвата события.

2.  Тем же самым таймером мы реализуем системный отсчет времени

3.   Необходимо настроить порты, куда подключены ваши каналы энкодера на вход

//Переменные, необходимые для проекта.
#define portChannelB  PIND
#define pinChannelB    2
#define timeScale 1000000               // коэффициент перевода времени в мкс
#define velocityRegister 15             // регистр modBus для хранения скорости
#define REG_HOLDING_START 1100          // начальный адрес holding регистров modBus
#define REG_HOLDING_NREGS 40            // количество регистров modBus
volatile uint32_t systemTimer=0;        // Системный таймер
volatile int32_t  counterEncoder=0;     // Число шагов, пройденное энкодером
volatile uint32_t tempTimerValue;       // Предыдущее значение таймера
volatile uint32_t curentTimerValue;     // Текущее значение таймера
volatile int32_t  timeBetweenSteps;     // Время между возникновением события ICP
volatile int8_t   direction;            // Направление вращения
static USHORT   usRegHoldingStart = REG_HOLDING_START;
static USHORT   usRegHoldingBuf[REG_HOLDING_NREGS];

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

#define setBit(x,bit) \
	(x)|=(1<

Настраиваем таймер 1

void initTimer()
{
	TCCR1B=0;
	setBit(TCCR1B,ICNC1); // предотвращение шума
	setBit(TCCR1B,ICES1); // прерывание по переходу из низкого уровня в высокий
	setBit(TCCR1B,CS10); //1/1024
	clearBit(TCCR1B,CS11);
	setBit(TCCR1B,CS12);
        TIMSK|=(1<

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

void enableICP()
{
	setBit(TIMSK,TICIE1);
}
 
void disableICP()
{
	clearBit(TIMSK,TICIE1);
}

Замечательным свойством прерывания ICP является то, что значение регистра времени таймера TCNT автоматически копируется в вспомогательный регистр ICR. Основой кода является работа в прерывании ICP

          ISR(TIMER1_CAPT_vect)
          {
	          cli();
                  if(ifBitSet(portChannelB,pinChannelB)&&((TCNT1-ICR1)>2))) direction=1; else
            	  if(!(ifBitSet(portChannelB,pinChannelB))&&((TCNT1-ICR1)>=(timeBetweenSteps>>2))) direction=1;
            	  else direction=-1;  /* это странное условие вводится на случай, если прерывание возникло,
 но было запрещено из-за входа в критическую секцию. На больших скоростях оно может возникнуть с
опозданием, что приведет к неправильному определению направления. Подробнее это можно понять, если
посмотреть на временную диаграмму работы энкодера.*/
                  counterEncoder+=direction;
        	  systemTimer+=ICR1;
        	  TCNT1=0;
        	  curentTimerValue=systemTimer;
        	  timeBetweenSteps=(uint32_t)((((curentTimerValue-tempTimerValue))*timeScale)/(F_CPU>>10));
        	  USHORT temp = timeBetweenSteps > 0x7FFF ? 0x7FFF : timeBetweenSteps;
        	  if( direction < 0 )
                  temp = ( ( ~ temp ) | 0x8000 ) + 1; // Это представление отрицательных чисел (дополнительный код)
        	  usRegHoldingBuf[velocityRegister]= temp;
                  tempTimerValue=curentTimerValue;
        	  sei();
             }

При переполнении регистра TCNT таймера увеличим значение системного таймера и сбросим регистр TCNT

ISR(TIMER1_OVF_vect)
                   {
        	  systemTimer+=65535;
                   	TCNT1=0;
                   }

Как видно, здесь используются регистры modBus для хранения результата. Причем нам не важно число пройденных шагов, поэтому мы не храним их в регистре. Тип USHORT на самом деле является типом uint16_t.

По сути, весь код работает в прерывании, в функции main вам нужно лишь провести инициализацию и уйти в бесконечный цикл.
Главным недостатком такой реализации является то, что значение скорости обновляется только после возникновения прерывания. Поэтому после остановки энкодера там будет сохраняться ненулевое значение и будет казаться, что энкодер движется с маленькой скоростью. Выходом из этого является периодический пересчет скорости, это можно делать в основном цикле, а можно задействовать прерывание по таймеру.
Именно такая реализация выбрана потому, что позволяет измерять скорость при каждом событии, то есть измерений скорости будет максимально возможное число. Это отличается от способа реализации этой задачи с использование встроенных в таймер счетчиков. Последний способ является:
- менее экономным к ресурсам контроллера, так как требует еще один таймер для отсчета времени
- менее точным
- не будет встроенной в контроллер фильтрации дребезга

Leave a Reply

You must be logged in to post a comment.