Как стать автором
Обновить
2018.21
Timeweb Cloud
То самое облако

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

Уровень сложностиСредний
Время на прочтение11 мин
Количество просмотров2.1K

Есть у нас в институте старенькое спортивное табло 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-канале 

Опробовать ↩
Теги:
Хабы:
+27
Комментарии10

Публикации

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud