Учимся работать с энкодером

Добрый день. Сегодня будем знакомиться с принципом работы, и способом подключения инкрементального энкодера к микроконтроллеру stm32.

Для начала - немного теории.

Энкодер — это электромеханическое устройство, преобразующее угол поворота в электрический сигнал.

chrome okQWLaJZM2

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

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

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

chrome 7xcZQcPnHvВращение энкодера по часовой стрелке

chrome lJzs460Ce8Вращение энкодера против часовой стрелки

Давайте проанализируем полученные диаграмы.

В начальный период времени - состояние на обоих линиях высокое.

При повороте по часовой стрелке, с начала зелёная линия подтягивается к нулю, фиолетовая при это остаётся в единице. Затем фиолетовая линия так же подтягивается к земле. После чего зелёная линия возвращается к единице. Спустя ещё какое-то время, на фиолетовой линии так же появляется сигнал. После чего, цикл повторяется. Итого цикл поворота состоит из 4 этапов.

Их мы можем описать следующей последовательностью:

Линия 1: 1 0 0 1 1
Линия 2: 1 1 0 0 1

При повороте же против часовой стрелки, последовательность изменилась, теперь первой к земле притягивается фиолетовая линия, и мы получаем последовательность

Линия 1: 1 1 0 0 1
Линия 2: 1 0 0 1 1

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

На много более информативно посмотреть на наложение графиков

chrome pNkEmdPJHFВращение по часовой стрелке

chrome DgB3BT7XXiВращение против часовой стрелки

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

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

Для решения нашей задачи соберём следующую схему: 

Добавить картинку

Для облегчения задачи - будем использовать класс Bounce, который мы создали во втором занятии, а так же класс DigitLed, созданный в продолжении третьего занятия.

В первую очередь - создадим проект в CubeMX. Настраиваем пины, согласно схеме. Пины 0,1,2,3,4,5,6,7 порта A и пины 12,13,14,15 порта B - как GPIO_Output. Пины 0 и 1 порта B, а так же пин 13 порта C - как GPIO_Input. Обязательно включаем отладку Serial Wire. Так же, необходимо включить подтяжку пинов, настроенных на вход. к плюсу источника питания при помощи встроенных резисторов. После чего генерируем проект, и переходим в VisualStudio, в которой импортируем проект CubeMX.

При создании проекта в CubeMX вы включили некую подтяжку, давайте разберёмся что это такое, и для чего нужно.

Давайте посмотрим в ???????? Reference manual, в именно в раздел ???????????????. В нём помимо того, что рассказывается какие варианты настройки пинов можно применить к пинам, так же даётся упрощённая схема внутреннего устройства одного пина. 

chrome YWSp57gNt7

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

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

После чего добавляем в наш проект файлы, содержащие исходный код классов Bounce и DigitLed.

Так как сигнальные линии энкодера необходимо отслеживать так же, как кнопку, давайте создадим новый класс, назовём Encoder, и так как энкодер содержит в себе кнопку, унаследуем класс Bounce в качестве базового класса. Таким образом заголовочный файл Encoder.h будет выглядеть следующим образом:

#pragma once
#include "Bounce.h"

class Encoder :
public Bounce
{
};

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

В качестве алгоритма будем использовать следующую последовательность:

1. Считываем состояние обоих пинов, учитывая необходимость устранения дребезга

2. Если на первой линии высокий уровень, а на второй линии - низкий, то фиксируем информацию о том, что вращаем в одну сторону.

3. Если на первой линии низкий уровень, а на второй линии - высокий, то фиксируем информацию о том, что вращаем в другую сторону.

4. Ждём когда оба сигнала вернутся в исходный уровень, после чего фиксируем шаг.

Давайте теперь алгоритм оформим в виде кода для микроконтроллера.

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

Encoder(GPIO_TypeDef* PortButton, uint16_t PinButton, GPIO_TypeDef* PortSignal1, uint16_t PinSignal1, GPIO_TypeDef* PortSignal2, uint16_t PinSignal2)
: Bounce(PortButton, PinButton) {
this->PortSignal1 = PortSignal1;
this->PinSignal1 = PinSignal1;
this->PortSignal2 = PortSignal2;
this->PinSignal2 = PinSignal2;
};

Так же в публичной части класса, требуется переопределить родительскую функцию

void updateState(void);

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

GPIO_TypeDef* PortSignal1, *PortSignal2;
uint16_t PinSignal1, PinSignal2;

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

Bounce::updateState();

После чего пишем алгоритм устранения дребезга, почти такой же, как мы писали во втором занятии.

 

//Алгоритм устранения дребезга
//Функции необходимые для алгоритма
bool Encoder::RisingEdge(uint8_t LineNumber)
{
if (LineNumber == 1)
{
if (risingSignal1 == 1)
{
risingSignal1 = 0;
return 1;
}
return 0;
}
else
{
if (risingSignal2 == 1)
{
risingSignal2 = 0;
return 1;
}
return 0;
}
}

bool Encoder::FallingEdge(uint8_t LineNumber)
{
if (LineNumber == 1)
{
if (fallingSignal1 == 1)
{
fallingSignal1 = 0;
return 1;
}
return 0;
}
else
{
if (fallingSignal2 == 1)
{
fallingSignal2 = 0;
return 1;
}
return 0;
}
}

//Сам алгоритм
uint32_t tickCur = HAL_GetTick();
// Проверяем что прошло более 2 мсек
if((tickCur - tickPrev) > 2)
{
tickPrev = tickCur;
//Обновление состояние первой сигнальной линии
GPIO_PinState Pin1State = HAL_GPIO_ReadPin(PortSignal1, PinSignal1);
if (Pin1State == GPIO_PIN_SET)
{
if (StateSignal1 == (maxState - 1))
{
risingSignal1 = 1;
}
if (StateSignal1 < maxState)
StateSignal1++;
}
else
{
if (StateSignal1 == 1)
{
fallingSignal1 = 1;
}
if (StateSignal1 > 0)
StateSignal1--;
}

//Обновление состояние второй сигнальной линии
GPIO_PinState Pin2State = HAL_GPIO_ReadPin(PortSignal2, PinSignal2);
if (Pin2State == GPIO_PIN_SET)
{
if (StateSignal2 == (maxState - 1))
{
risingSignal2 = 1;
}
if (StateSignal2 < maxState)
StateSignal2++;
}
else
{
if (StateSignal2 == 1)
{
fallingSignal2 = 1;
}
if (StateSignal2 > 0)
StateSignal2--;
}
}

 

После чего остаётся только при получении информации о том, что пин изменил состояние - сохранить его новое состояние:

//Обновляем информацию о том, в каком состоянии сейчас пины
if(RisingEdge(1) == true)
ValueSignal1 = 1;
if(RisingEdge(2) == true)
ValueSignal2 = 1;
if(FallingEdge(1) == true)
ValueSignal1 = 0;
if (FallingEdge(2) == true)
ValueSignal2 = 0;

И реализовать оставшуюся логику:

//Рассчитываем в какую сторону повернули энкодер
// Если на первой линии высокий уровень, а на второй линии - низкий, то вращаем в одну сторону
if(ValueSignal1 == 1 && ValueSignal2 == 0)
{
Direction = 1;
}
// Если на первой линии низкий уровень, а на второй линии - высокий, то вращаем в другую сторону
if(ValueSignal1 == 0 && ValueSignal2 == 1)
{
Direction = -1;
}
// как определились с направлением - ждём когда оба сигнала поднимутся в высокий уровень, фиксируем шаг, и сбрасываем флаг направления.
if(Direction != 0 && ValueSignal1 == 1 && ValueSignal2 == 1)
{
state = state + Direction;
Direction = 0;
}

Во время написания данного метода, у нас появилось ещё несколько переменных, которые необходимо объявить в закрытой части класса, и инициализировать в конструкторе.

//Объявляем
uint8_t StateSignal1, StateSignal2, maxState;
uint32_t tickPrev;
bool risingSignal1, fallingSignal1, risingSignal2, fallingSignal2, ValueSignal1, ValueSignal2;
bool RisingEdge(uint8_t LineNumber);
bool FallingEdge(uint8_t LineNumber);
int8_t state;
int8_t Direction;

//Инициализируем
this->StateSignal1 = 0;
this->StateSignal2 = 0;
this->maxState = 2;
this->ValueSignal1 = 0;
this->ValueSignal2 = 0;
this->Direction = 0;
this->state = 0;

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

int8_t Encoder::GetState(void)
{
int8_t ValueToReturn = this->state;
this->state = 0;
return ValueToReturn;
}

На этом создание класса завершено. Можно создавать объект класса, и использовать его. Для этого в main.cpp напишем:

/* USER CODE BEGIN Includes */
#include "Encoder.h"
#include "DigitLed.h"
/* USER CODE END Includes */

/* USER CODE BEGIN 2 */
Encoder MyEncoder(EncButton_GPIO_Port, EncButton_Pin, EncSignalA_GPIO_Port, EncSignalA_Pin, EncSignalB_GPIO_Port, EncSignalB_Pin);
DigitLed MyDigitLed(A_GPIO_Port, A_Pin,
B_GPIO_Port, B_Pin,
C_GPIO_Port, C_Pin,
D_GPIO_Port, D_Pin,
E_GPIO_Port, E_Pin,
F_GPIO_Port, F_Pin,
G_GPIO_Port, G_Pin,
DP_GPIO_Port, DP_Pin,
Segment1_GPIO_Port, Segment1_Pin,
Segment2_GPIO_Port, Segment2_Pin,
Segment3_GPIO_Port, Segment3_Pin,
Segment4_GPIO_Port, Segment4_Pin);
uint32_t Counter = 0;
/* USER CODE END 2 */

/* USER CODE BEGIN WHILE */
while (1)
{
MyEncoder.updateState();
Counter = Counter + MyEncoder.GetState();
MyDigitLed.print(Counter);
/* USER CODE END WHILE */

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

На этом занятие завершено. Итоговый проект можно найти...