Переводим спортивное табло на управление по Bluetooth и контроллер arduino

от автора

Есть у нас в институте старенькое спортивное табло eltablo. По нему я, ещё будучи студентом, мячом попадал. И есть (точнее была) у него неприятная проблема: это табло управляется по страшному проводному пульту (как этот пульт работает, я до сих пор не разобрался). Длина провода от пульта до табло на глаз метра 3-4. В стоке его хватает, только чтобы сидеть прямо под ним, что, естественно, неудобно (не видно счёт, неправильный ракурс для судейства и т.д.). Поэтому наши физруки им управляют с противоположной стороны зала, что тоже не совсем удобно, но хотя бы видно, что на этом табло происходит.

В этом, собственно, и заключается проблема: чтобы подключить пульт, пришлось прокинуть не хилой длины проводок, на вскидку, метров 20. Из-за этого табло управляется не всегда стабильно. Это меня и попросили решить. Естественно, я решил, что проводам и пульту место на помойке, а таблом будем рулить по беспроводному соединению и с телефона!

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

Бывший мозг нашего табло. Megawin MPC89E52AE

Бывший мозг нашего табло. Megawin MPC89E52AE

Изначально я рассматривал esp с поднятой точкой доступа Wi-Fi и красивым веб-интерфейсом управления. Но мне не очень хотелось каждый раз подключаться к точке доступа, да и в целом это мне показалось менее удобным и менее универсальным.
Поэтому я решил рассмотреть вариант с arduino и управлением по UART. Этот вариант позволяет управлять и по Bluetooth, и по проводу. В качестве пульта можно использовать кучу уже существующих приложений, а если потребуется, легко написать свое или даже смастерить кнопочный пульт.

❯ Для начала разберёмся с аппаратным обеспечением этого табло

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

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

Регистры соединены последовательно шлейфом. Для передачи информации на каждый регистр нужен 1 байт, соответственно, на всё табло надо передавать 11 байт, или 88 бит.
На самом деле, наибольшую сложность вызвало как раз управление этими регистрами.

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

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

С распиновкой драйверов проблем не возникло, даташит нашелся легко, а вот с распиновкой шлейфа пришлось повозиться.

Распиновка DM114, DM115

Распиновка DM114, DM115

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

По сути, из всего шлейфа нам нужны всего 3 провода — latchPin, clockPin и dataPin.

Так как в Arduino уже есть встроенная функция работы со сдвиговыми регистрами, я попробовал использовать её.

void out_595_shift(byte x) {   digitalWrite(LATCH_PIN, LOW);                         // "открываем защелку"   shiftOut(DATA_PIN, CLOCK_PIN, LSBFIRST, 0b10110110);  // отправляем данные   digitalWrite(LATCH_PIN, HIGH);                        // "закрываем защелку", выходные ножки регистра установлены   delay(10); } 

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

И собрал библиотеку цифр для этих индикаторов от 0 до 9.

byte numbers[11] = {   0b01111110,  //0   0b00010100,  //1   0b01011011,  //2   0b01010111,  //3   0b00110101,  //4   0b01100111,  //5   0b01101111,  //6   0b01010100,  //7   0b01111111,  //8   0b01110111,  //9   0b10000000   //: };

Я попробовал передать 11 байт в цикле, но вменяемых результатов не вышло, табло просто наполнилось восьмерками.

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

❯ Для начала пришлось разобраться с логикой работы dm114

Если коротко:

  1. В начале передачи latchPin устанавливается в low;

  2. Перед передачей каждого бита clockPin ставится в low;

  3. dataPin ставится в high или в low в зависимости от бита;

  4. После передачи бита clockPin ставится в high, dataPin в low и clockPin опять в low;

  5. После передачи ставим latchPin в high.

Передача происходит побитово, поэтому удобно хранить биты для всех индикаторов в одном массиве и передавать за раз. Чтобы не заморачиваться, я создал булевый массив bitData на 88 элементов.

Для установки портов latchPin, clockPin и dataPin в значения high и low я написал соответствующие функции (на Arduino я выбрал порты 9, 10 и 11). Для передачи буфера из 88 бит на драйвера dm114 — функцию Show(). А для заполнения буфера цифрами из библиотеки — setData().

bool bitData[88]; //буфер на 88 булевых значений   static inline void latchPinH() {   bitSet(PORTB, 1); //установка D9 в high     } static inline void latchPinL() {   bitClear(PORTB, 1); //установка D9 в low  } static inline void clockPinH() {   bitSet(PORTB, 2); //установка D10 в high     } static inline void clockPinL() {   bitClear(PORTB, 2); //установка D10 в low  } static inline void dataPinH() {   bitSet(PORTB, 3); //установка D11 в high     } static inline void dataPinL() {   bitClear(PORTB, 3); //установка D11 в low  }  //Принимает байт (из библиотеки цифр) и на какое место (какой индикатор) его поставить void setData(byte number, int num) {   for(int i = 0; i<8; i++)   {     if (number & (0B10000000 >> i)) bitData[num*8-i] = 1;     else bitData[num*8-i] = 0;       } }  void Show() {    //Начало передачи latchPin устанавливается в low;   // latchPinL(); Почему закоментировано, далее в статье   clockPinL(); //Перед передачей первого бита ставим clockPin и dataPin в low   dataPinL();      for (int i = 0; i < 88; i++) {     if (bitData[i]) dataPinH(); //передаем нужный бит     else dataPinL();      //После передачи бита clockPin ставится в high, dataPin в low и  clockPin опять в low;     clockPinH();     dataPinL();     clockPinL();   }   //После передачи ставим latchPin в high.   latchPinH();     }   

Значения битов для вывода цифр я взял из массива numbers, составленного ранее. И в итоге все заработало!

Вывод цифр практически заработал. Но на табло есть 8 попарно соединенных индикаторов. Для удобной работы с ними, я решил поделить все индикаторы на 7 дисплеев (4 двухразрядных, и 3 одноразрядных) и написать функцию, принимающую число, которое надо вывести, и номер дисплея. Так как передача происходит по очереди, с первого по одиннадцатый индикатор, получается, что в начале передаются данные для последнего индикатора, потом для предпоследнего и т.д., то есть в обратном порядке. Это надо учесть.
А еще у нас есть двоеточие на часах, оно управляется старшим битом из переданных 8 на индикатор. Это надо тоже учесть.

В итоге вышла немного замороченная функция SetNumberToDisplay, но зато удобная для дальнейшей работы.

void SetNumberToDisplay(int number, int displayNumberL,bool AddDot) {   //Так как наши экраны могут всего в 2 разряда, перевести число в цифру не сложно   int firstDigit = number/10;   int secondDigit = number%10;    //Так как индикаторы расположены в странном порядке, и передача идет наоборот приходится костылить   //указываем, какой индикатор надо использовать для первой и второй цифры   int firstDigitNumber = 0; //Номер индикатора для первой цифры   int secondDigitNumber = 0;//Номер индикатора для второй цифры   switch (displayNumberL)   {     case 1:        firstDigitNumber = 11;       secondDigitNumber = 10;       break;     case 2:        firstDigitNumber = -1; //Индикатор с одной секцией       secondDigitNumber = 9;       break;     case 3:        firstDigitNumber = 8;       secondDigitNumber = 7;       break;     case 4:        firstDigitNumber = -1; //Индикатор с одной секцией       secondDigitNumber = 6;       break;     case 5:        firstDigitNumber = 5;       secondDigitNumber = 4;       break;     case 6:        firstDigitNumber = 3;       secondDigitNumber = 2;       break;     case 7:        firstDigitNumber = -1; //Индикатор с одной секцией       secondDigitNumber = 1;       break;     default:               break;   }      if(firstDigit>0 && firstDigitNumber>=0) setData(numbers[firstDigit], firstDigitNumber);   if(displayNumberL == 5 || displayNumberL == 6 && firstDigitNumber>=0) setData(numbers[firstDigit], firstDigitNumber); // костыль на часы для отображения нуля         if(!AddDot) setData(numbers[secondDigit], secondDigitNumber);   else setData(numbers[10] + numbers[secondDigit], secondDigitNumber); // двоеточие на часах управляется самым первым битов, просто прибавляем её  }

Теперь стало возможно задавать значения на семь дисплеев, а не на каждый отдельный индикатор.

❯ Остается только разобраться с логикой работы самого табло

Табло имеет два режима. Режим игры — в нем отображаются значения очков, фолов и периодов, ещё можно запустить таймер или секундомер. И обычный режим — в нем отображаются только часы.

Для подсчета очков, периодов и фолов я написал структуру ScoreCounter. А для вывода этих значений — соответствующие функции.

struct ScoreCounter { public:   int OwnerScore = 0;   int VisitorScore = 0;    int OwnerFoul = 0;   int VisitorFoul = 0;    int Period = 0;    void ClearScore() {     OwnerScore = 0;     VisitorScore = 0;   }    void ClearFoul() {     OwnerFoul = 0;     VisitorFoul = 0;   }   void ClearPeriod()   {     Period = 0;   }    void ClearAll()   {     ClearScore();     ClearFoul();     ClearPeriod();   } } Score;   void PrintScore() {      SetNumberToDisplay(Score.OwnerScore, 1,false);   SetNumberToDisplay(Score.VisitorScore, 3,false);RunСommand }  void PrintPeriod() {   SetNumberToDisplay(Score.Period, 2,false); }  void PrintFoul() {   SetNumberToDisplay(Score.OwnerFoul, 4,false);   SetNumberToDisplay(Score.VisitorFoul, 7,false); }

Напомню, управлять таблом я планировал через UART при помощи простых текстовых команд. Это оказалась самая простая и самая интересная часть работы. Для чтения команды я написал функцию ReadCommand(), с провода команды читаются через Serial, а для работы по bluetooth я использовал библиотеку SoftwareSerial. Все стандартно.

void ReadCommand() {   if (Serial.available()) {     String command = Serial.readString();     Serial.println("OK");     RunCommand(command);   }    if (mySerial.available()) {     String command = mySerial.readString();     mySerial.println("OK");     RunCommand(command);   } } 

Для обработки и выполнения команд используется страшная функция на миллион if — RunCommand(). Для включения/выключения режима игры используется флаг OperatingMode. Для переключения между отображением времени и секундомером/таймером — флаг Chronometer.

void RunCommand(String command) {   if(command.indexOf("GAME") == 0) OperatingMode = !OperatingMode; //Вкл-выкл режим игры   else if(command.indexOf("OSCOREADD") == 0) Score.OwnerScore+=command.substring(9).toInt(); //ДОБАВИТЬ ОЧКОВ ХОЗЯИНУ   else if(command.indexOf("OSCORETAKE") == 0) Score.OwnerScore-=command.substring(10).toInt(); //отнять ОЧКОВ ХОЗЯИНУ   else if(command.indexOf("VSCOREADD") == 0) Score.VisitorScore+=command.substring(9).toInt(); //ДОБАВИТЬ ОЧКОВ ГОСТЮ   else if(command.indexOf("VSCORETAKE") == 0) Score.VisitorScore-=command.substring(10).toInt(); //отнять ОЧКОВ Гостю    else if(command.indexOf("OSCORECLEAR") == 0) Score.OwnerScore=0; //ОЧИСТИТЬ ИГРОКА   else if(command.indexOf("VSCORECLEAR") == 0) Score.VisitorScore=0; //ОЧИСТИТЬ ГОСТЯ   else if(command.indexOf("OSCORESET") == 0) Score.OwnerScore=command.substring(9).toInt(); //ЗАДАТЬ ОЧКИ ХОЗЯИНУ   else if(command.indexOf("VSCORESET") == 0) Score.VisitorScore=command.substring(9).toInt(); //АДАТЬ ОЧКИ ГОСТЮ     else if(command.indexOf("PERIODADD") == 0) Score.Period++;   else if(command.indexOf("PERIODTAKE") == 0) Score.Period--;   else if(command.indexOf("PERIODCLERA") == 0) Score.Period=0;   else if(command.indexOf("PERIODSET") == 0) Score.Period = command.substring(9).toInt();    else if(command.indexOf("OFOULADD") == 0) Score.OwnerFoul+=command.substring(8).toInt();   else if(command.indexOf("OFOULTAKE") == 0) Score.OwnerFoul-=command.substring(9).toInt();   else if(command.indexOf("VFOULADD") == 0) Score.VisitorFoul+=command.substring(8).toInt();   else if(command.indexOf("VFOULTAKE") == 0) Score.VisitorFoul-=command.substring(9).toInt();   else if(command.indexOf("OFOULCLEAR") == 0) Score.OwnerFoul=0;   else if(command.indexOf("VFOULCLEAR") == 0) Score.VisitorFoul=0;   else if(command.indexOf("OFOULSET") == 0) Score.OwnerFoul=command.substring(8).toInt();   else if(command.indexOf("VFOULSET") == 0) Score.VisitorFoul=command.substring(8).toInt();    else if(command.indexOf("CHRONOMETERCL") == 0) ChronometerClear();      else if(command.indexOf("CHRONOMETERSTART") == 0) StartChronometer = !StartChronometer;    else if(command.indexOf("CHRONOMETER") == 0) Chronometer = !Chronometer;         else if(command.indexOf("SETTIMER") == 0) SetTimerStr(command);   else if(command.indexOf("TIMER") == 0) StartTimer =!StartTimer;     else if(command.indexOf("CL") == 0) Score.ClearAll();    else if(command.indexOf("DEBUGTIME") == 0) debugTime = !debugTime;   else if(command.indexOf("DEBUG") == 0) debug = !debug;     else if(command.indexOf("TIMEAD") == 0) SetTimeAdjustment(command.substring(7).toInt());   else if(command.indexOf("TIME") == 0) SetTimeStr(command);   else if(command.indexOf("REBOOT") == 0) Reboot(); }

Этими командами можно:

  • Включать/выключать режим игры;

  • Увеличивать/уменьшать на указанное значение очки хозяину и гостю;

  • Обнулять и задавать очки хозяину и гостю;

  • Увеличивать/уменьшать на указанное значение периоды и фолы;

  • Обнулять и задавать периоды и фолы;

  • Очищать счет, периоды и фолы полностью;

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

  • Задавать и запускать таймер;

  • Производить подстройку и настройку времени;

  • Включать/выключать debug режим.

В loop у нас последовательное чтение команд, вывод счета, работа со временем и обновление индикаторов.

unsigned long oldShow; void loop() {   ReadCommand(); //Чтение и выполнение команды   if (OperatingMode) //Вывод счета в режиме игры   {     PrintScore();     PrintPeriod();     PrintFoul();   }    if(Chronometer) //Вывод времени или таймера/секундомера   {         showTimer();   }   else   {     showTime();     }    if(StartChronometer) //вкл секундомер   {     ChronometerTick();         }   else if(StartTimer) //вкл таймер   {     TimerTick();   }     if(millis() - oldShow > ShowDelay) //обновление индикаторов по времени   {     Show();     oldShow = millis();   }    clearData(); //чистим буфер   delay(5); }

В статье я опустил работу со временем, т.к. и так получилось много кода. По возможностям: есть таймер и секундомер, можно задавать время, и есть функция подстройки. Модуль часов DS1302, мягко говоря, не очень точный и сильно убегает даже за день. Поэтому потребовалась функция подстройки, кстати, в оригинальном контроллере эта функция тоже была. Работает она очень просто: раз в час от времени отнимается или прибавляется до 59 секунд (изначально это происходило раз в сутки, но этого не хватало). Подробнее можно посмотреть в файле watch.ino в репозитории проекта.

Далее собрал ‭«новые мозги‭» для табло. В наличии у нас контроллер Arduino nano, модуль часов реального времени, bluetooth модуль HC-06 и понижающий стабилизатор для питания всего этого. Проект штучный, плату разводить не стал.

❯ Далее начался процесс тестирования

В процессе тестирования начала происходить какая-то вакханалия 🙂

После запуска все работало отлично, но спустя время начинали зажигаться лишние сегменты, и так, пока не будут гореть одни восьмерки. С этим багом тоже пришлось немного повозиться, оказалось, в функции Show() для положительных бит я подавал на dataPin высокий уровень сигнала, а для нулей забыл. Из-за чего со временем на сегменты вместо нуля начинала попадать единица (сверху уже исправленный код).

Еще индикаторы неприятно моргали при передачи данных. Чтобы это решить, достаточно было убрать latchPinL(); в начале функции Show(). Возможно, я неверно прозвонил шлейф, соединяющий индикаторы, и latchPin это на самом деле EnablePin.

После исправления этих нюансов с индикаторами больше проблем не было.

Табло постояло неделю включенным, при этом освещая окна адским красным светом 🙂

❯ Управление с телефона

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

Я нашёл два подходящих:

Bluetooth Remote for Arduino (слева) и RoboRemo (справа)

Bluetooth Remote for Arduino (слева) и RoboRemo (справа)

Bluetooth Remote for Arduino:

  • бесплатное;

  • можно настраивать несколько интерфейсов;

  • cо встроенной рекламой;

  • нельзя импортировать настроенные интерфейсы.

И RoboRemo:

  • платное, в demo версии можно настроить до 5 кнопок;

  • можно настраивать несколько интерфейсов;

  • нет рекламы;

  • легко импортировать настроенные интерфейсы.

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

Изначально использовали первое приложение, но спустя полгода у него начались проблемы со стабильностью, и на телефонах наших физруков начали пропадать настроенные интерфейсы. Бонусом, реклама при открытии приложения постоянно мешала. Ещё немного поискав, мне попалось RoboRemo, и на мой взгляд — это отличное приложение для подобных задач. Реклама больше не мешает, и со стабильностью все отлично.

На данный момент, табло в таком конфиге работает уже второй год. За все это время возникла всего одна аппаратная проблема — от перегрева (а может из-за чего-то другого) сгорел bluetooth-модуль, этому я был удивлен, учитывая, какой колхоз я собрал (думал сдохнет раньше 🙂 ).

Новый модуль расположили более удачно, чтобы он получше продувался, заодно проклеили все соединения для защиты от вибраций. Прошивка за это время показала себя хорошо. Хотел в неё ещё добавить всякие пасхалки и вывод текста, но у меня не хватило времени, физрукам уже нужно было табло для соревнований :(. Зато я получил заслуженный тортик за разработку :).

Полную прошивку из статьи можно найти у меня на GitHub.


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале 

Опробовать ↩


ссылка на оригинал статьи https://habr.com/ru/articles/911392/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *