Наверняка многие из вас использовали для точного измерения длины каких либо предметов такой инструмент как штангенциркуль. Но он не очень хорошо подходит для точного измерения расстояний каких-нибудь кривых. Для этих целей хорошо подходит инструмент для измерения расстояний хорошо знакомый всем военнослужащим – курвиметр. Его фото показано на следующем рисунке.
В данной статье мы рассмотрим создание электронного аналога курвиметра – цифрового колеса для измерения расстояний с точностью до миллиметра на основе платы Arduino и инкрементального энкодера.
А на следующем видео показан принцип работы нашего электронного курвиметра.
Более подробно про подключение инкрементального энкодера к плате Arduino можно прочитать в этой статье.
Необходимые компоненты
- Плата Arduino Pro Mini (купить на AliExpress).
- Модуль USB To TTL (для программирования платы Arduino Pro Mini).
- Инкрементальный энкодер (с импульсом High на оборот).
- OLED дисплей 128х64 с интерфейсом SPI (купить на AliExpress).
- Модуль TP4056 (купить на AliExpress).
- Литий-полимерная батарея 700mAh.
- Кнопка.
- Переключатель SPST типа.
Принципы работы нашего проекта
Принцип работы нашего проекта будет основан на принципе работы инкрементального энкодера, который преобразует вращение своей оси в серию импульсов на своем выходе. Серии этих импульсов на двух выходах энкодера сдвинуты относительно друг друга по фазе на 90 градусов. Оценивая направление этого сдвига мы можем определить направление, в котором вращается ось энкодера.
Для сброса показаний счетчика оборотов колеса мы будем использовать кнопку. Управлять всеми процессами в нашем проекте будет плата Arduino Pro Mini – она будет обрабатывать импульсы от инкрементального энкодера и отображать измеренное значение на OLED дисплее, который будет подключаться к плате Arduino через интерфейс SPI.
Схема проекта
Схема цифрового колеса для измерения расстояний на основе платы Arduino представлена на следующем рисунке.
В представленной схеме литий-полимерная батарея подключена к модулю TP4056, который подключен к переключателю и с помощью которого запитываются все компоненты схемы.
Кнопка подключена к контакту 3 платы Arduino – ее нажатия будут обрабатываться с помощью прерываний.
Выходные контакты инкрементального энкодера подключены к контактам 2 и 6 платы Arduino.
0.96” OLED дисплей подключен к контактам 8, 9, 10, 12 и 13 платы Arduino по протоколу SPI.
Объяснение программы для Arduino
Полный код программы приведен в конце статьи, здесь же мы кратко рассмотрим его основные фрагменты.
Хотя для обработки импульсов от инкрементального энкодера нам не нужно никаких библиотек, нам необходимы будут библиотеки для работы с OLED дисплеем. Для этого установим библиотеки Adafruit GFX и Adafruit SSD 1306. Запустим менеджер библиотек из меню Tools в Arduino IDE.
Выполним поиск библиотек Adafruit GFX и Adafruit SSD1306 и установим их нажав на кнопку install.
Первым делом в программе подключим все используемые библиотеки.
1 2 3 4 |
#include <SPI.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> |
Затем дадим используемым контактам осмысленные имена и создадим объект для работы с OLED дисплеем.
1 2 3 4 5 6 7 8 9 10 |
#define OLED_MOSI 12 #define OLED_CLK 13 #define OLED_DC 9 #define OLED_CS 10 #define OLED_RESET 8 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS); #define outputA 2 #define outputB 6 #define SetZero 3 |
Далее мы объявим используемые в нашем проекте константы – это диаметр нашего колеса (70 мм) и число счетов (импульсов) на один оборот (counts per revolution, CPR) для каждой фазы нашего энкодера.
Из даташита на наш инкрементальный энкодер мы знаем, что каждая фаза передает 400 импульсов за один оборот и, поскольку каждый импульс имеет два перехода, мы получаем необходимое нам число 800 (400X2) – его мы и вводим в качестве CPR.
Также мы объявим переменную с именем factor – она будет определять пройденное за один счет (импульс) расстояние. В дальнейшем в программе мы будем умножать ее на число импульсов.
1 2 3 |
#define Diameter 70 //in mm #define CPR 800 //Counts per revolution of each phase. float factor= (3.1415*Diameter)/(CPR); |
Также объявим ряд дополнительных переменных, которые нам потребуются в программе.
1 2 3 4 5 6 7 |
bool aState; bool aLastState; unsigned long int lastdata=0; unsigned long int lastDisplay=0; bool led=0; long units=0; bool i=0; |
В функции void setup() мы зададим режимы работы используемых контактов (на ввод данных с использованием подтягивающих резисторов), настроим обработку прерываний, инициализируем OLED дисплей и выведем нулевое значение измеренного расстояния на экран дисплея.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
void setup() { pinMode (outputA,INPUT_PULLUP); pinMode (outputB,INPUT_PULLUP); pinMode (SetZero,INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(2),ISR1,CHANGE); attachInterrupt(digitalPinToInterrupt(3),ISR2,FALLING); Serial.begin(9600); if(!display.begin(SSD1306_SWITCHCAPVCC)) { Serial.println(F("SSD1306 allocation failed")); for(;;); // Don't proceed, loop forever } // Clear the buffer display.clearDisplay(); display.display(); display.setTextColor(WHITE); aLastState = PIND&(1<<2)?1:0; display.clearDisplay(); display.setTextSize(2); displayCenter("Distance",2); display.setTextSize(3); displayCenter(String(units),30); lastDisplay=millis(); } |
В функции void loop() мы будем выполнять сравнительно мало действий поскольку основной функционал нашей программы будет реализован в функциях обработки прерываний, а также в функции updateDisplay(). Мы будем использовать переменную i в качестве флага, который будем устанавливать в 1 всегда при формировании импульса инкрементальным энкодером или при сбросе переменной счетчика. Мы будем вызывать функцию updateDisplay() только когда переменная флага равна 1.
Также мы будем использовать простой код с использованием переменной lastData и функции millis() для сброса счетчика обратно в 0 в случае если нет никакого ввода в течение последних 10 секунд.
1 2 3 4 5 6 7 8 9 10 11 |
void loop() { if(i==1) { updateDisplay(); } if(millis()-lastdata>10000) { units=0; updateDisplay(); lastdata=millis(); } } |
Поскольку мы подключили одну фазу инкрементального энкодера к контакту 2 платы Arduino, функция ISR1 будет выполняться всегда при переходе уровня на этом контакте. Из принципа работы инкрементального энкодера мы знаем, что если фазы не равны, то ось энкодера вращается против часовой стрелки, в этом случае мы будем уменьшать значение счетчика, а если фазы равны мы будем увеличивать значение счетчика.
Заметьте, что вместо использования простой функции digitalRead() мы для считывания состояния контакта использовали команду bool(PIND&(1<<6)). Это команда используется для считывания состояния всего порта D, но для целей оценки состояния 6-го контакта используется только 6-й бит считанного значения порта.
1 2 3 4 5 6 7 8 9 10 11 |
void ISR1() { if ( bool(PIND&(1<<6)) != bool(PIND&(1<<2)) ) { units --; } else { units ++; } i=1; } |
Кнопка подключена к контакту 3 и ее нажатие приводит к вызову второй функции обработки прерывания – ISR2. Эта функция просто сбрасывает значение переменной счетчика до нуля.
1 2 3 4 |
void ISR2() { units=0; i=1; } |
Функция updateDisplay() используется для очистки экрана OLED дисплея и отображения на нем нового значения измеренного расстояния. Эта функция производит обновление каждые 500 мс чтобы беречь ресурсы процессора и быть уверенным в том, что мы не пропустим какой либо импульс от энкодера. Также это предотвращает мерцание экрана.
Функция displayCenter(), как это следует из ее названия, выравнивает текст на экране дисплея горизонтально по центру.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void updateDisplay() { if(millis() - lastDisplay>500) { display.clearDisplay(); display.setTextSize(2); displayCenter("Distance",2); display.setTextSize(3); displayCenter(String(float(units*factor)),30); lastDisplay=millis(); lastdata = lastDisplay; i=0; } } void displayCenter(String text, int line) { int16_t x1; int16_t y1; uint16_t width; uint16_t height; display.getTextBounds(text, 0, 0, &x1, &y1, &width, &height); // display on horizontal center display.setCursor((SCREEN_WIDTH - width) / 2, line); display.println(text); // text to display display.display(); } |
Сборка конструкции проекта
Для нашего проекта измерительного колеса были разработаны STL файлы, которые можно использовать для печати корпуса проекта на 3D принтере. Скачать их можно по ссылке ниже. На следующем рисунке представлено расположение электронных компонентов проекта внутри данного корпуса.
Колесо прикрепляется к оси инкрементального энкодера.
После сборки всей конструкции проекта можно приступать к тестированию его работы.
Исходный код программы (скетча)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
#include <SPI.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #define SCREEN_WIDTH 128 // ширина OLED дисплея, в пикселах #define SCREEN_HEIGHT 64 // высота OLED дисплея, в пикселах // Declaration for SSD1306 display connected using software SPI (default case): #define OLED_MOSI 12 #define OLED_CLK 13 #define OLED_DC 9 #define OLED_CS 10 #define OLED_RESET 8 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS); #define outputA 2 #define outputB 6 #define SetZero 3 #define Diameter 70 //in mm #define CPR 800 //Counts per revolution of each phase. float factor= (3.1415*Diameter)/(CPR); bool aState; bool aLastState; unsigned long int lastdata=0; unsigned long int lastDisplay=0; bool led=0; long units=0; bool i=0; void setup() { pinMode (outputA,INPUT_PULLUP); pinMode (outputB,INPUT_PULLUP); pinMode (SetZero,INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(2),ISR1,CHANGE); attachInterrupt(digitalPinToInterrupt(3),ISR2,FALLING); Serial.begin(9600); if(!display.begin(SSD1306_SWITCHCAPVCC)) { Serial.println(F("SSD1306 allocation failed")); for(;;); // Don't proceed, loop forever } // очищаем буфер display.clearDisplay(); display.display(); display.setTextColor(WHITE); aLastState = PIND&(1<<2)?1:0; display.clearDisplay(); display.setTextSize(2); displayCenter("Length(mm)",15); display.setTextSize(3); displayCenter(String(units),42); lastDisplay=millis(); } void loop() { if(i==1) { updateDisplay(); } if(millis()-lastdata>10000) { units=0; updateDisplay(); lastdata=millis(); } } void ISR1() { if ( bool(PIND&(1<<6)) != bool(PIND&(1<<2)) ) { units --; } else { units ++; } i=1; } void ISR2() { units=0; i=1; } void updateDisplay() { if(millis() - lastDisplay>500) { display.clearDisplay(); display.setTextSize(2); displayCenter("Length(mm)",15); display.setTextSize(3); displayCenter(String(float(units*factor)),42); lastDisplay=millis(); lastdata = lastDisplay; i=0; } } void displayCenter(String text, int line) { int16_t x1; int16_t y1; uint16_t width; uint16_t height; display.getTextBounds(text, 0, 0, &x1, &y1, &width, &height); // display on horizontal center display.setCursor((SCREEN_WIDTH - width) / 2, line); display.println(text); // текст для отображения на дисплее display.display(); } |
Добрый день! Работает вот такой
https://aliexpress.ru/item/1005002132981406.html?sku_id=12000018845576505&af=1954_4090455&cn=2urussl398n5ohx524f9pvvysoebcvj8&cv=2&cv_source=default&dp=2urussl398n5ohx524f9pvvysoebcvj8&sub=45ssl398urhkotz9jikgsq78k64ime0i&utm_campaign=1954_4090455&utm_content=2&utm_medium=cpa&utm_source=aerkol&aff_fcid=5fedc6ea2f484f68a10b46d33b623786-1741066172513-03115-_DkVp2LF&aff_fsk=_DkVp2LF&aff_platform=api-new-link-generate&sk=_DkVp2LF&aff_trace_key=5fedc6ea2f484f68a10b46d33b623786-1741066172513-03115-_DkVp2LF&terminal_id=1c9af60eddef4816947d881af63fdbfc
А не работает вот такой
https://aliexpress.ru/item/1005007972875043.html?spm=a2g2w.orderdetail.0.0.12714aa6Yo5VbA&sku_id=12000043095812529
Доброе утро. Такой энкодер как по второй ссылке у вас, я даже ни разу и не видел. У меня во всех статьях на сайте используется примерно такой, как у вас по первой ссылке. Хорошо бы в даташите посмотреть какие у него диаграммы выходных сигналов, к осциллографу его подключить, проверить как уровни сигналов меняются. Так вот, удаленно, мне к сожалению трудно вам подсказать что то конкретное.
Добрый день! Как вы не видели если в этой статье (Колесо для измерения расстояний (курвиметр) на Arduino и инкрементальном энкодере) у Вас такой энкодер!?
Доброе утро. Ну этот проект лично я не собирал (статья переводная, стоит ссылка на первоисточник), поэтому вживую такой энкодер и не видел
Теперь понял!)) Спасибо!
Да не за что. Заходите к нам еще. ))
Просто на сайте более тысячи статей и когда какой-нибудь посетитель нашего сайта пишет в комментариях к одной из статей свой вопрос он почему то почти всегда думает что я эту статью только вчера написал и при этом лично собрал описанный в ней проект, и поэтому считает что я знаю содержание этой статьи наизусть и легко отвечу на его вопрос. На самом деле, это, конечно же не так, и многие статьи, которые писались давно, уже подзабылись, и приходится заново читать эту статью чтобы попытаться ответить на его вопрос
Можно ссылку на энкодер пожалуйста.
Ссылка на этот компонент, к сожалению, быстро умирает, поэтому не добавлял ее в статью. Вам проще самостоятельно на алиэкспрессе ввести в строке поиска "энкодер" или "rotary encoder" и выбрать из полученных результатов нужный вам энкодер
Добрый день! Все купил на алиэксперсс. только ардуино нано у меня, и никак не получается запустить энкодер. Экран работает, все нормально, а вот на движение ротором энкодеда никакой реакции. Подскажите, в чем может быть причина?
Добрый вечер. Попробуйте просто подключить энкодер к Ардуино как описано в этой статье и посмотрите заработает ли он у вас. В статье по ссылке описаны и возможные проблемы при работе с энкодером
Да, такой энкодер как в статье у меня работает. А вот тот что купил на алиэкспрес, как у Вас в статье "Колесо для измерения расстояний (курвиметр) на Arduino и инкрементальном энкодере" не работает.
Ну может бракованный экземпляр попался? Я для своих ссылок стараюсь выбирать магазины на Алиэкспресс с хорошими отзывами. Если бы они продавали косячные энкодеры это нашло бы отражение в отзывах. А в чем проблема заменить один на другой, на тот который у вас работает?