Дешёвый хронограф для пневматики своими руками

в 10:45, , рубрики: arduino, Digispark, diy или сделай сам, tm1637

Дешёвый хронограф для пневматики своими руками - 1

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

1. Детали и принадлежности

На этом заканчиваются детали, которые необходимо покупать. Резисторы можно не заказывать, похожие по номиналу (но не меньше!) можно выдернуть из ненужной бытовой электроники. Таким образом, суммарные затраты менее 350 рублей, это ничто по сравнению с ценой нового заводского хронографа (over 1000р за самый простой, который по факту еще примитивнее нашего сабжа). Кроме деталей нам пригодятся:

  • Провода — найти в оффлайне бесплатно не проблема
  • Кусок пластиковой водопроводной трубы длиной более 10см (диаметр по вкусу) — так же легко найти
  • Паяльные принадлежности
  • Мультиметр (опционально)

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

1.1. Digispark

Представляет собой простую миниатюрную Arduino-совместимую плату с ATtiny85 на борту. Как подключить к Arduino IDE читаем на официальном сайте проекта, там же можно найти драйвера для нее. Существует два основных вида этой платы: с microUSB и более брутальный с USB коннектором, разведенным прямо на плате.

Дешёвый хронограф для пневматики своими руками - 2

Мой хронограф не имеет собственного источника питания, поэтому я выбрал первый вариант платы. Встроенная батарейка/аккумулятор сильно повысит цену, не добавив при этом практически ничего к юзабилити. Power bank и кабель для зарядки телефона валяется практически у каждого.

Характеристики само собой унаследованы от ATtiny85, его возможностей в нашем случае достаточно с головой. Фактически МК в хронографе не делает ничего, кроме опроса двух датчиков и управления дисплеем. Для тех, кто впервые сталкивается с Digispark-ом, я свёл наиболее важные особенности в таблицу:

Flash память 6Кб (2Кб заняты загрузчиком)
RAM 512 байт
EEPROM 512 байт
Частота 16,5 МГц (по-умолчанию)
Количество I/O пинов 6
Питание на VIN 5-12В
Pin 0 PWM, SDA
Pin 1 PWM
Pin 2 SCK, ADC1
Pin 3 USB+, ADC3
Pin 4 PWM, USB-, ADC2
Pin 5 PWM, ADC0

Эту табличку я использую как шпаргалку при разработке различных девайсов на базе этой платы. Как вы наверное заметили, нумерация пинов для функции analogRead() отличается, это следует учитывать. И еще одна особенность: на третьем пине висит подтягивающий резистор на 1.5кОм, т.к. он используется в USB.

1.2. Дисплей на базе TM1637

Следующая важная деталь — цифровой дисплей, на который будет выводиться информация. Дисплей можно использовать любой, мой выбор обусловлен только дешевизной и простотой работы с ним. От дисплея в принципе можно вообще отказаться и выводить данные по кабелю на ПК, тогда девайс станет еще дешевле. Для работы понадобится библиотека DigitalTube. Сабж, на который я дал ссылку в начале поста, представляет собой клон дисплея Grove. Вид спереди:

Дешёвый хронограф для пневматики своими руками - 3

Сзади:

Дешёвый хронограф для пневматики своими руками - 4

Между цифрами расстояние одинаковое, поэтому при выключенном двоеточии числовые значения читаются нормально. Вместе со стандартной библиотекой поставляется пример, который работает с Digispark-ом без плясок с бубном:

Дешёвый хронограф для пневматики своими руками - 5

Все, что умеет стандартная библиотека, — выводить числа 0-9 и буквы a-f, а так же менять яркость всего дисплея целиком. Значение цифры задается функцией display(int 0-3, int 0-15).

Экспресс-курс по использованию дисплея

// 1. Объявить заголовочный файл
#include <TM1637.h>
// 2. Задать пины
#define CLK 0
#define DIO 1
// 3. Объявить объект
TM1637 tm1637(CLK, DIO);
// 4. Проинициализировать
void setup() {
  tm1637.init();
  tm1637.set(6); // Яркость
}
// 5. Использовать
void loop() {
  // Вывод числа x на дисплей
  int x = 1234;
  tm1637.display(0, x / 1000);
  tm1637.display(1, x / 100 % 10); 
  tm1637.display(2, x / 10 % 10);
  tm1637.display(3, x % 10);
  delay(500);
}

Если попытаться вывести символ с кодом за границами [0, 15], то дисплей показывает чушь, которая при этом не статичная, поэтому схитрить для вывода спецсимволов (градусов, минуса) без бубна не получится:

Дешёвый хронограф для пневматики своими руками - 6

Это меня не устраивало, так как в своем хронографе я хотел предусмотреть вывод не только скорости, но и энергии пули (вычисляемой на основе заранее прописанной в скетче массы), эти два значения должны выводиться последовательно. Чтобы понять, что показывает дисплей в данный момент времени, нужно как-то разделять эти два значения визуально, например, при помощи символа «J». Конечно, можно тупо задействовать символ двоеточия как флаг-индикатор, но это же не тру и не кошерно) Поэтому я полез разбираться в библиотеку и на базе функции display сделал функцию setSegments(byte addr, byte data), которая зажигает в цифре с номером addr сегменты, закодированные в data:

void setSegments(byte addr, byte data)
{
  tm1637.start();
  tm1637.writeByte(ADDR_FIXED);
  tm1637.stop();
  tm1637.start();
  tm1637.writeByte(addr|0xc0);
  tm1637.writeByte(data);
  tm1637.stop();
  tm1637.start();
  tm1637.writeByte(tm1637.Cmd_DispCtrl);
  tm1637.stop();
}

Кодируются сегменты предельно просто: младший бит data отвечает за самый верхний сегмент, и т.д. по часовой стрелке, седьмой бит отвечает за центральный сегмент. Например, символ '1' кодируется как 0b00000110. Восьмой, старший бит используется только во второй цифре и отвечает за двоеточие, во всех остальных цифрах он игнорируется. Чтобы облегчить себе жизнь я, как и полагается любому ленивому айтишнику, автоматизировал процесс получения кодов символов при помощи excel:

Дешёвый хронограф для пневматики своими руками - 7

Теперь можно легко сделать так:

Дешёвый хронограф для пневматики своими руками - 8

Или так:

Дешёвый хронограф для пневматики своими руками - 9

Let's say HELLO

#include <TM1637.h>
#define CLK 0
#define DIO 1
TM1637 tm1637(CLK, DIO);

void setSegments(byte addr, byte data)
{
  tm1637.start();
  tm1637.writeByte(ADDR_FIXED);
  tm1637.stop();
  tm1637.start();
  tm1637.writeByte(addr|0xc0);
  tm1637.writeByte(data);
  tm1637.stop();
  tm1637.start();
  tm1637.writeByte(tm1637.Cmd_DispCtrl);
  tm1637.stop();
}

void setup() {
  tm1637.init();
  tm1637.set(6);
}

void loop() {
  // Вывод Hello
  setSegments(0, 118);
  setSegments(1, 121);
  setSegments(2, 54);
  setSegments(3, 63);
  delay(500);
}

1.3. Датчики

Тут я, к сожалению, не могу ничего особо сказать, потому что на странице товара нет ни слова о характеристиках или хотя бы маркировки, по которой можно было бы откопать даташит. Типичный noname. Известна только длина волны 940нм.

Дешёвый хронограф для пневматики своими руками - 10

Ценой одного светодиода определил, что ток больше 40мА для них смертелен, а напряжение питания должно быть ниже 3.3В. Фототранзистор немного прозрачный и реагирует на свет

2. Подготовка деталей и сборка

Схема очень простая и незамысловатая, из всех пинов digispark-a нам понадобятся только P0, P1 — для работы с дисплеем, а так же P2 — для работы с датчиками:

Дешёвый хронограф для пневматики своими руками - 11

Как видно, один резистор ограничивает ток на светодиодах, второй — стягивает P2 к земле. Фототранзисторы соединены последовательно, поэтому прохождение пули перед любой оптопарой приводит к уменьшению напряжения на P2. Путем регистрации двух последовательных скачков напряжения и замера времени между ними мы можем определить скорость движения пули (зная расстояние между датчиками, ессно). Использование одного пина для замеров имеет еще один плюс — нет никакого требуемого направления движения пули, можно стрелять с обоих концов. Собирать будем из этой горстки деталей:

Дешёвый хронограф для пневматики своими руками - 12

Я пошел по пути миниатюризации и решил сделать бутерброд при помощи куска макетной платы:

Дешёвый хронограф для пневматики своими руками - 13

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

Дешёвый хронограф для пневматики своими руками - 14

Дешёвый хронограф для пневматики своими руками - 15

Остается только разместить датчики в трубке и припаять провода:

Дешёвый хронограф для пневматики своими руками - 16

На фото видно, что я разместил дополнительный электролит на 100мКф параллельно светодиодам, чтобы при питании от повербанка не было пульсаций ИК диодов.

Дешёвый хронограф для пневматики своими руками - 17

Пин P2 в качестве входа был выбран не просто так. Напомню, что P3 и P4 используются в USB, поэтому использование P2 дает возможность прошивать девайс уже в собранном виде. Во-вторых, P2 — аналоговый вход, поэтому можно не использовать прерывания, а просто мерить разницу в цикле между предыдущим и текущим значением на нем, если разница выше некоторого порога — значит пуля проходит между одной из оптопар. Но есть одна программная хитрость, без которой приведенная схема не взлетит, о ней поговорим далее.

3. Прошивка

3.1. Пару слов о prescaler

Prescaler представляет собой делитель частоты, по-умолчанию в arduino-подобных платах он равен 128. От значения этой величины зависит максимальная частота опроса АЦП, по дефолту для 16 мГц контроллера получается 16/128 = 125 кГц. На каждую оцифровку уходит 13 операций, поэтому максимальная частота опроса пина — 9600 кГц (в теории, на практике реально не выше 7 кГц). Т.е. интервал между замерами примерно 120 мкс, это очень и очень много. Пуля, летящая со скоростью 300 м/с пролетит за это время 3,6 см — контроллер просто не успеет засечь факт прохождения пули через оптопару. Для нормальной работы нужен интервал между замерами как минимум 20 мкс, необходимое значение делителя для этого равно 16. Я пошел еще дальше и в своем девайсе использую делитель 8, делается это следующим образом:

#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

void setup() {
  sbi(ADCSRA,ADPS2);
  cbi(ADCSRA,ADPS1);
  cbi(ADCSRA,ADPS0);
  ...
}

Реальные замеры интервала analogRead на разных делителях:

Дешёвый хронограф для пневматики своими руками - 18

3.2. Итоговый скетч

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

  • Первый цикл — измеряется разница между текущим и предыдущим значением на пине
  • Если разница больше заданного порога, то выходим из цикла и запоминаем текущее время (micros())
  • Второй цикл — аналогично предыдущему + счетчик времени в цикле
  • Если счетчик достиг заданной величины, то информирование об ошибке и переход к началу. Это позволяет не уходить циклу в вечность, если пуля по каким-то причинам не была замечена вторым датчиком
  • Если счетчик не переполнился и разница значений больше порога, то замеряем текущее время (micros())
  • На основе разницы во времени и расстоянии между датчиками вычисляем скорость и выводим на экран
  • Переход в начало

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

Собственно, весь код

/*
 * Хронограф для измерения скорости движения пули, © SinuX 23.03.2016
 */

#include <TM1637.h>

#define CLK 1   // Пин дисплея
#define DIO 0   // Пин дисплея
#define START_PIN 1   // Аналоговый пин старта
#define END_PIN 1     // Аналоговый пин финиша
#define START_LEV 50 // Порог срабатывания старта
#define END_LEV 50   // Порог срабатывания финиша
#define TIMEOUT 10000 // Время ожидания финиша в микросекундах
#define BULLET_WEIGHT 0.00051 // Масса пули в килограммах (для вычисления энергии)
#define ENCODER_DIST 0.1      // Расстояние между датчиками в метрах (10см = 0.1м)
#define SHOW_DELAY 3000       // Время показа результата

// Для ускорения analogRead
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

// Служебные переменные
int prevVal, curVal;
unsigned long startTime, endTime;
TM1637 tm1637(CLK, DIO);

/* Переделанная функция TM1637::display(), которая позволяет зажигать отдельные сегменты
 * Нумерация сегментов: младший бит - верхний сегмент и т.д. по часовой стрелке
 * Центральный сегмент - старший бит */
void setSegments(byte addr, byte data)
{
  tm1637.start();
  tm1637.writeByte(ADDR_FIXED);
  tm1637.stop();
  tm1637.start();
  tm1637.writeByte(addr|0xc0);
  tm1637.writeByte(data);
  tm1637.stop();
  tm1637.start();
  tm1637.writeByte(tm1637.Cmd_DispCtrl);
  tm1637.stop();
}

// Инициализация
void setup() {
  // Устанавливаем prescaler на 8 для ускорения analogRead
  cbi(ADCSRA,ADPS2);
  sbi(ADCSRA,ADPS1);
  sbi(ADCSRA,ADPS0);
  
  // Инициализация дисплея
  tm1637.init();
  tm1637.set(6);
  // Отображение приветствия
  setSegments(0, 118);
  setSegments(1, 121);
  setSegments(2, 54);
  setSegments(3, 63);
  delay(1000);
}

// Главный цикл
void loop() {
  // Заставка ожидания
  showReady();  
  // Ожидание старта
  curVal = analogRead(START_PIN);
  do
  {
     prevVal = curVal;
     curVal = analogRead(START_PIN);
  } while (curVal - prevVal < START_LEV);
  startTime = micros();

  // Ожидание финиша
  curVal = analogRead(END_PIN);
  do
  {
     prevVal = curVal;
     curVal = analogRead(END_PIN);
     // Если превышен интервал ожидания - показ ошибки и выход из цикла
     if (micros() - startTime >= TIMEOUT) { showError(); return; }
  } while (curVal - prevVal < END_LEV);
  endTime = micros();

  // Вычисление и отображение результата
  showResult();
}

// Отображение заставки ожидания выстрела
void showReady()
{
  setSegments(0, 73);
  setSegments(1, 73);
  setSegments(2, 73);
  setSegments(3, 73);
  delay(100);
}

// Вычисление и отображение скорости, энергии пули
void showResult()
{
  // Вычисление скорости пули в м/с и вывод на дисплей
  float bulletSpeed = ENCODER_DIST * 1000000 / (endTime - startTime);
  tm1637.display(0, (int)bulletSpeed / 100 % 10); 
  tm1637.display(1, (int)bulletSpeed / 10 % 10);
  tm1637.display(2, (int)bulletSpeed % 10);
  setSegments(3, 84);
  delay(SHOW_DELAY);
  
  // Вычисление энергии в джоулях и вывод на дисплей
  float bulletEnergy = BULLET_WEIGHT * bulletSpeed * bulletSpeed / 2;
  tm1637.point(1); // Вместо точки ':' - костыль, но пойдет)
  tm1637.display(0, (int)bulletEnergy / 10 % 10);
  tm1637.display(1, (int)bulletEnergy % 10);
  tm1637.display(2, (int)(bulletEnergy * 10) % 10);
  setSegments(3, 30);
  delay(SHOW_DELAY);
  tm1637.point(0);
}

// Вывод ошибки при превышении времени ожидания пули
void showError()
{
  setSegments(0, 121);
  setSegments(1, 80);
  setSegments(2, 80);
  setSegments(3, 0);
  delay(SHOW_DELAY);
}

4. Примеры работы

При правильном подключении девайс взлетел практически сразу, единственный обнаруженный недостаток — он негативно реагирует на светодиодное и люминисцентное освещение (частота пульсаций около 40 кГц), отсюда могут появляться спонтанные ошибки. Всего в девайсе предусмотрено 3 режима работы:

Приветствие после включения и переход в режим ожидания выстрела (экран заполняется полосками):

Дешёвый хронограф для пневматики своими руками - 19

В случае ошибки — отображается «Err», и снова переход в режим ожидания:

Дешёвый хронограф для пневматики своими руками - 20

Ну и сам замер скорости:

Дешёвый хронограф для пневматики своими руками - 21

После выстрела сначала показывается скорость пули (с символом 'n'), затем — энергия (символ 'J'), причем энергия вычисляется с точностью до одного знака после запятой (на гифке видно, что при показе джоулей горит двоеточие). Корпус покрасивее найти пока не смог, поэтому просто залил все термосоплями:

Дешёвый хронограф для пневматики своими руками - 22

Пожалуй, на этом у меня все, надеюсь, кому-то был полезен.

Автор: SinuX

Источник

  1. Димон:

    В итоговом коде нужно поменять местами пины, т.е. где 1 там 0 и наоборот. Либо же распаять в соответствии со скетчем.

    #define CLK 1 // Пин дисплея
    #define DIO 0 // Пин дисплея

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js