Наверняка многие из вас использовали для точного измерения длины каких либо предметов такой инструмент как штангенциркуль. Но он не очень хорошо подходит для точного измерения расстояний каких-нибудь кривых. Для этих целей хорошо подходит инструмент для измерения расстояний хорошо знакомый всем военнослужащим – курвиметр. Его фото показано на следующем рисунке.
В данной статье мы рассмотрим создание электронного аналога курвиметра – цифрового колеса для измерения расстояний с точностью до миллиметра на основе платы 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(); } |