Показаны сообщения с ярлыком NXC. Показать все сообщения
Показаны сообщения с ярлыком NXC. Показать все сообщения

воскресенье, 1 октября 2017 г.

НейроБашня

В 94-м проекте мы построим робота, который будет играть с нами в известную игру "11 палочек". В этот раз мы снова коснемся темы нейросетевых технологий. Нейронная сеть робота сможет обучаться как в результате игры с человеком, реагируя на его стратегию, так и в результате "игры в уме", играя в памяти с себе подобной сетью.

Суть игры такова: в кучке 11 палочек, два игрока ходят по очереди. За каждый ход можно взять 1 или 2 палочки. Выигрывает тот, кто взял последнюю палочку.

Вместо палочек наш робот будет работать с лего-кубиками. 11 блоков из кубиков, сложенных друг на друга, образуют высокую башню, что и дало название нашему проекту, "НейроБашня".



В этот раз мы построили конструкцию из старого доброго LEGO Mindstorms NXT. Это связано с тем, что параллельно с Python мы изучаем C/C++, а для платформы NXT есть замечательная свободно распространяемая среда разработки BricxCC, Си-компилятором в которой мы и воспользуемся для программирования робота.

Инструкцию по сборке нашего робота в формате LEGO Digital Designer можно скачать по ссылке. В конструкции мы используем средний EV3-мотор в силу его компактности, однако вам ничего не помешает использовать NXT-мотор, изменив способ его крепления, изменений в коде это не потребует.


Робот может работать в двух режимах:

  • Игра на двоих. В этом режиме играют два человека. В башню заряжено 11 лего-кубиков. Игроки по очереди нажимают 1 или 2 раза на кнопку со своей стороны, забирая себе 1 или 2 кубика. Выигрывает игрок, который забрал последний кубик.
  • Игра человека против нейронной сети робота. В этом режиме игрок использует правую кнопку, за левого игрока думает робот.
Для объяснения алгоритма обучения нейронной сети робота давайте снова воспользуемся терминологией коробков и камушков, как доступной для пониманию робототехниками любого возраста.

Так как у нашего робота 11 состояний (по количеству оставшихся кубиков в башне), представим 11 коробков, в каждом из которых в начале игры по 1 камню белого и черного цветов. Когда наступает ход нейронной сети, она "смотрит" сколько кубиков осталось в башне и выбирает соответствующий коробок, затем наугад вытаскивает из него камень. Если он белый - она берет 2 кубика, если черный, то 1 кубик, при этом запоминая сколько она взяла в каждый ход. После того как камень вытащен и его цвет определен, он помещается назад в коробок.
По окончанию игры наступает пора поощрений и наказаний:

  • Если нейронная сеть выиграла, она добавляет в каждый задействованный ею коробок камень того цвета, который она вытащила, делая соответствующий ход. 
  • Если нейронная сеть проиграла, то она убирает из последнего задействованного ею коробка камень того цвета, который она вытащила. При этом, если камень этого цвета последний, то он остается в коробке.
Этот способ был опубликован в серии книг «Берсеркер» (издавались в рамках «Боевая фантастика») Фреда Саберхагена в 1995 году. По сюжету, главный герой должен был замаскировать своё отсутствие на космокорабле. Он обучил методу «спичечных коробков» свою мартышку, которая и имитировала игру с жутким космическим монстром. 
Ранее, в 70-x годах задача «Самообучающаяся машина из спичечных коробков» была опубликована известным математиком Мартином Гарднером в книге "Математические досуги".

В самом начале игры наша нейронная сеть не обучена, ее ходы случайны, как как в каждом "коробке" у нее 50% белых и 50% черных камней. Каждый такой коробок - это нейрон сети.
По мере игры с человеком нейронная сеть набирается опыта, подстраивается под стратегии игрока, не давая реализовывать ему выигрышные комбинации. Такой обучение эффективно в плане выстраивания качественных взаимосвязей между нейронами сети, так как сеть играет с думающим соперником, однако длится оно может очень долго. Сеть начнет играть на уровне игрока только через несколько часов игры. Мы реализовали такой метод обучения нейронной сети самым первым, но использование только его нас не устроило в силу трудоемкости обучения робота.
Второй способ обучения, который мы попробовали, стало обучение игре с виртуальным игроком, делающим случайные ходы. Параллельно игре с человеком в памяти робота проводятся непрерывная последовательность игр нейронной сети, которую нужно обучить, с генератором чисел 1 и 2. Этот способ малоэффективен в силу того, что генератор "не думает", однако из-за того что таких игр проводится много (более сотни каждую секунду), сумма различных игровых ситуаций и исходов игр в итоге приводят к образованию связей, необходимых для выигрыша нейронной сети. Такая сеть начинает играть сильнее человека через полчаса, но нас результат снова не устроил и мы двинулись дальше.
Третьим способом, который мы применили, явилось создание еще одной нейронной сети и проведение непрерывной последовательности фоновых игр между сетями. Обе сети обучаются одновременно, по каждый ведется статистика побед.. Этот способ оказался самым эффективным, правда удвоенная вычислительная нагрузка снизила количество фоновых игр (теперь их 70-80 в секунду), однако качество обучения заметно выросло.

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

Интересно, что в процессе обучения явный перевес получает сеть, которая ходит первой. Если право первого хода отдавать всегда одной и той же нейронной сети, то она будет "заточена" под победу в играх, где она ходит первой, в то время как вторая сеть будет сильно играть в партиях, где она ходит второй. Этим фактом мы воспользовались, и теперь, в играх, где право первого хода выпадает человеку, он играет со второй сетью, а в партиях, где человек ходит вторым, играет первая нейросеть.



Исходный код программы приведен ниже. Скачать программу можно по ссылке. Мотор подключается в порт А, датчики кнопки - в порты 1 и 2. Чтобы озвучить игры мы записали набор звуков в формате RSO, которые нужно поместить в память робота, их вы тоже найдете по ссылке.

//время ожидания второго нажатия
long timeToPress=0;

//переменные с количеством выигрышей нейросетей
long winNN1=0;
long winNN2=0;

//количество игр NN против NN (здесь и далее NN - нейросеть)
long game=0;

//сколько осталось кубиков в игре с человеком
int box1 = 0;
//сколько осталось кубиков в игре NN с NN
int box2=0;

//win это переменная обозначает чей-то выйгрыш
//win2 это тоже самое, только в подпроцессе  learn
int win1=0;
int win2=0;

//7 строк обозначают 7 строк экрана, мы в них храним то, что надо вывести
string line1,line2,line3,line4,line5,line6,line7;

//buttonPress это количество нажатий на кнопку
int buttonPress=0;

//ошибка и старая ошибка PD-регулятора
int ERR = 0;
int ERRo=0;

//управляющее воздействие
int u = 10;

//коФф-ты PD регулятора
float P = 0.5;
float D = 1.5;

//точка, которой мы хотим достичь при повороте мотора
int target=0;

//время для поворота влево или вправо
long timeToMove=0;

//два массива пуговиц NN1
int white1[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
int black1[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};

//два массива пуговиц NN2
int white2[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
int black2[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};

//три временных массива 2 для learn и 1 для main
int tmp1[11];
int tmp2[11];
int tmp[11];

//блок для поворота по регулятору налево
sub Left (int raz)
{
  //устонавливаем точку, которой хотим достичь
  target=MotorRotationCount(OUT_A)+360*raz;

  //засекаем время
  timeToMove=CurrentTick();

  //цикл, работоющий пока мы не достигнем точки или времени
  while(abs(MotorRotationCount(OUT_A)-target)>5 && CurrentTick()-timeToMove < 1000){
    //с помощью регулятора подаем нагрузку на мотор
    ERR=target-MotorRotationCount(OUT_A);
    u=P*ERR+D*(ERR-ERRo);
    if(u>100)u=100;
    if(u<-100)u=-100;
    OnFwd(OUT_A,u);
    ERRo=ERR;
  }

  //выключаем мотор
  Off(OUT_A);
}

//блок для поворота по регулятору направо
sub Right(int raz)
{
  //устанавливаем точку, которой хотим достичь
  timeToMove=CurrentTick();
  
  //засекаем время
  target=MotorRotationCount(OUT_A)-360*raz;

  //цикл, работающий пока мы не достигнем точки или времени
  while(abs(MotorRotationCount(OUT_A)-target)>5 && CurrentTick()-timeToMove < 1000){
    //с помощью регулятора подаем нагрузку на мотор
    ERR=target-MotorRotationCount(OUT_A);
    u=P*ERR+D*(ERR-ERRo);
    if(u>100)u=100;
    if(u<-100)u=-100;
    OnFwd(OUT_A,u);
    ERRo=ERR;
  }

  //выключаем мотор
  Off(OUT_A);
}

//функция для подсчета нажатий на кнопки
int touch(int sensor) {
  //переменная, в которой мы храним количевство нажатий
  int countPress=0;
  //условие, проверяющее на каком датчике ждать нажатия
  if(sensor==1){
    //ожидание первого нажатия
    while(Sensor(S1)==0){
      Wait(1);
    }
    //защина от шума
    Wait(100);
    //ожидаем отжатия
    while(Sensor(S1)!=0);
    long timeToPress=CurrentTick();
    //цикл, ожидающий второго нажатия заданое время
    while(Sensor(S1)==0 && CurrentTick()-timeToPress < 1000);
    //если вылетели не по времени, а по нажатию
    if(CurrentTick()-timeToPress < 1000) {
      //зашита от шума
      Wait(100);
      //устанавливаем количество нажатий в 2(0 - одно нажитие, 1 - два нажатие)
      countPress=1;
      //ожидаем отжатия
      while(Sensor(S1)!=0);
    }
    //возвращаем значение
    return countPress+1;
  }
  //если проверяется второй датчик
  if(sensor==2){
    //ожидаем нажатия
    while(Sensor(S2)==0){
      Wait(1);
    }
    //защита от шума
    Wait(100);
    //ожидание отжатия
    while(Sensor(S2)!=0);
    long timeToPress=CurrentTick();
    //цикл, ожидающий второго нажатия заданое время
    while(Sensor(S2)==0 && CurrentTick()-timeToPress < 1000);
    //если вылетели не по времени, а по нажатию
    if(CurrentTick()-timeToPress < 1000) {
      //защита от шума
      Wait(100);
      //устанавливаем количество нажатий в 2
      countPress=1;
      //ожидаем отжатия
      while(Sensor(S2)!=0);
    }
    //возвращаем значение
    return countPress+1;
  }

// параллельный процесс для обучения нейронных сетей
}
task learn(){
  Wait(2000);
  //говорим "начало обучения нейронной сети"
  PlayFile("start.rso");
  Wait(2000);
  //запоминаем время
  long timeToLearn = CurrentTick();
  //главный цикл
  while(true)
  {
    //обнуляем tmp1 и tmp2
    for(int i=0;i<=10;i++){
      tmp1[i]=0;
      tmp2[i]=0;
    }
    //если прошла секунда? (каждую секунду)
    if(CurrentTick()-timeToLearn>=1000){
      //вывод на экран  количевства игр и выигрышей
      line1=NumToStr(game)+" "+NumToStr(winNN1)+" "+NumToStr(winNN2);
      //вывод на экран выигрывающей нейросети
      if(winNN1>=winNN2){
        line2="HC1";
      }
      else{
        line2="NC2";
      }
      //вывод строк
      ClearScreen();
      TextOut(0,LCD_LINE1,line1);
      TextOut(0,LCD_LINE2,line2);
      TextOut(0,LCD_LINE3,line3);
      TextOut(0,LCD_LINE4,line4);
      TextOut(0,LCD_LINE5,line5);
      TextOut(0,LCD_LINE6,line6);
      TextOut(0,LCD_LINE7,line7);
      //очистка строк
      line1 = "";
      line2 = "";
      line4 = "";
      line6 = "";
      line7 = "";
      //перезаписываем время
      timeToLearn = CurrentTick();
    }
    //увеличиваем число игр
    game++;
    //переменная кто выиграл
    win2=0;
    //сколько кубиков осталось
    box2=11;

    //цикл игры
    while(true){

      //ход НС2
      //вытягиваем камень
      if(Random(100) < white2[box2-1]*1.0/(white2[box2-1]+black2[box2-1])*100.0){
        //запоминаем какой использовали коробок
        tmp2[box2-1]=2;
        //уменьшаем количество оставшихся кубиков в кучке
        box2-=2;
      }
      else{
        //запоминаем какой использовали коробок
        tmp2[box2-1]=1;
        //уменьшаем количество оставшихся кубиков в кучке
        box2-=1;
      }
      //проверка на выигрыш
      if(box2<=2){
        win2=1;
        break;
      }

      //ход НС1
      //вытягиваем камень
      if(Random(100) < white1[box2-1]*1.0/(white1[box2-1]+black1[box2-1])*100.0){
        //запоминаем какой использовали коробок
        tmp1[box2-1]=2;
        //уменьшаем количество оставшихся кубиков в кучке
        box2-=2;
      }
      else{
        //запоминаем какой использовали коробок
        tmp1[box2-1]=1;
        //уменьшаем количество оставшихся кубиков в кучке
        box2-=1;
      }
      //проверка на выигрыш
      if(box2<=2){
        win2=2;
        break;
      }

    }
    //поощрение-наказание
    //NN1 выиграла
    if(win2==1){
      winNN1++;
      for(int i = 0;i<=10;i++){
        if(tmp1[i]==1) black1[i]++;
        if(tmp1[i]==2) white1[i]++;
      }
    }
    else{
      for(int i = 0;i<=10;i++){
        if (tmp1[i] > 0)
        {
          if(tmp1[i]==1 && black1[i]>1) black1[i]--;
          if(tmp1[i]==2 && white1[i]>1) white1[i]--;
          break;
        }
      }
    }
    //NN2
    if(win2==2){
      winNN2++;
      for(int i = 0;i<=10;i++){
        if(tmp2[i]==1) black2[i]++;
        if(tmp2[i]==2) white2[i]++;
      }
    }
    else{
      for(int i = 0;i<=10;i++){
        if (tmp2[i] > 0)
        {
          if(tmp2[i]==1 && black2[i]>1) black2[i]--;
          if(tmp2[i]==2 && white2[i]>1) white2[i]--;
          break;
        }
      }
    }
  }
}

task main()
{
  //подключаем датчики
  SetSensorTouch(IN_1);
  SetSensorTouch(IN_2);
  ClearScreen();
  line1 = "";
  line2 = "";
  line3 = "";
  line4 = "";
  line5 = "";
  line6 = "";
  line7 = "";

  //делаем случайные числа действительно случайными
  for(int i=0; i<=CurrentTick()%1000; i++) { int tmp_r = Random(); }

  //сбрасываем tmp
  for(int i=0;i<=10;i++){
    tmp1[i]=0;
    tmp2[i]=0;
    tmp[i]= 0;
  }
  
  //запуск параллельной задачи learn для обучения нейросетей
  start learn;


  //Ожидание нажатия на кнопки
  line5="left 2 right 1";
  while (!ButtonPressed(BTNRIGHT, true) && !ButtonPressed(BTNLEFT, true));
  line5 = "";
  // если нажат левый датчик, игра человек-человек
  if(ButtonPressed(BTNLEFT, true))
  {
    //бесконечный цикл игры
    while(true)
    {
      //сброс tmp
      for(int i=0;i<=10;i++){
        tmp[i]= 0;
      }
      //Кубиков 11
      box1=11;
      while(true)
      {
        //ход игрока1
        buttonPress=touch(2);
        //выкидываем столько кубиков, сколько нажатий
        Left(buttonPress);
        //запоминаем сколько выкинули
        box1-=buttonPress;

        line4=NumToStr(box1);
        //проверка на выигрыш
        if(box1<=2){
          win1=1;
          line3="Player1 Win";
          break;
        }
        //ход игрока2
        buttonPress=touch(1);
        //выкидываем столько кубиков, сколько нажатий
        Right(buttonPress);
        //запоминаем сколько выкинули
        box1-=buttonPress;

        line4=NumToStr(box1);
        if(box1<=2){
          //проверка на выигрыш
          win1=2;
          line3="Player Win";
          break;
        }
      }
      //ожидания нажатия для новой игры(загрузка кубиков)
      while (!ButtonPressed(BTNCENTER, true));
      line3 = "";
    }
  }

  //если нажат правый датчик, игра человек-NN
  if(ButtonPressed(BTNRIGHT, true))
  {
    while(true)
    {
      //переменная кто выйграл
      win1=0;
      //сколько кубиков осталось
      box1=11;
      //рандомно определям первый ход
      if (Random(100)>50)
      {
        //первый ходит человек
        while(true){
          //ход игрока
          PlayFile("playerh.rso");
          Wait(1000);
          buttonPress=touch(2);
          if(buttonPress==1) {
            PlayFile("player1.rso");
            Wait(2000);
          }
          else{
            PlayFile("player2.rso");
            Wait(2000);
          }
          //выкидываем кубики по количеству нажатий
          Left(buttonPress);
          box1-=buttonPress;
          line4=NumToStr(box1);

          //проверка на выигрыш
          if(box1<=2){
            PlayFile("networkw.rso");
            Wait(2000);
            win1=1;
            line3="Robot Win";
            break;
          }
          PlayFile("networkh.rso");
          Wait(2000);
          PlayFile("networkd.rso");
          Wait(Random(10000)+2000);
          
          //ход НС
          if(Random(100) < white1[box1-1]*1.0/(white1[box1-1]+black1[box1-1])*100.0){
            PlayFile("network2.rso");
            Wait(2000);
            //берёт 2 кубика
            Right(2);
            box1-=2;
          }
          else{
            PlayFile("network1.rso");
            Wait(2000);
            //берёт 1 кубик
            Right(1);
            box1-=1;
          }

          line4=NumToStr(box1);
          
          //проверка на выигрыш
          if(box1<=2){
            PlayFile("playerw.rso");
            Wait(2000);
            win1=2;
            line3="Player Win";
            break;
          }
        }
      }
      // если первой ходит NN
      else
      {
        while(true){
          //ход НС
          if(Random(100) < white2[box1-1]*1.0/(white2[box1-1]+black2[box1-1])*100.0){
            PlayFile("network2.rso");
            Wait(2000);
            //берёт 2 кубика
            Right(2);
            box1-=2;
          }
          else{
            PlayFile("network1.rso");
            Wait(2000);
            // берёт 1 кубик
            Right(1);
            box1-=1;
          }

          line4=NumToStr(box1);
          // проверка на выигрыш
          if(box1<=2){
            PlayFile("playerw.rso");
            Wait(2000);
            win1=2;
            line3="Player Win";
            break;
          }

          //ход игрока
          PlayFile("playerh.rso");
          Wait(1000);
          buttonPress=touch(2);
          if(buttonPress==1) {
            PlayFile("player1.rso");
            Wait(2000);
          }
          else{
            PlayFile("player2.rso");
            Wait(2000);
          }
          // выкидываем по количеству нажатий
          Left(buttonPress);
          box1-=buttonPress;
          line4=NumToStr(box1);

          //проверка на выигрыш
          if(box1<=2){
            PlayFile("networkw.rso");
            Wait(2000);
            win1=1;
            line3="Robot Win";
            break;
          }
          PlayFile("networkh.rso");
          Wait(2000);
          PlayFile("networkd.rso");
          Wait(Random(10000)+2000);
        }
      }

      // одидания нажатия для продолжения игры
      PlayFile("networkl.rso");
      Wait(3000);
      PlayFile("next.rso");
      Wait(2000);
      while (!ButtonPressed(BTNCENTER, true));
        line3 = "";
      }
    }
  }
}



суббота, 14 января 2017 г.

LEGO UART. Бит за битом

Наверняка каждый робототехник знает, что роботы, как и компьютеры, умеют мыслить только нулями и единичками и, например число 3 запомнить им не под силу. Они долго думают и в итоге заменяют его двумя единицами, переводя тем самым в "двоичный код".
Многие из робототехников привыкли иметь дело с высокоуровневой разработкой - готовые модули, готовые библиотеки, им не особо интересно, что там, внутри, как устроен тот или иной позаимствованый компонент.


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


Основная наша задача в данном проекте - разобраться с тем, а что же это такое - двоичное число, как перевести привычное нам десятичное чисто в двоичное и наоборот. Но, стоп! Это же такая скукота, зачем это вообще нужно? Давайте будем действовать от реальной задачи.
Мы построили двух одинаковых роботов - Правого и Левого. Они - полные зеркальные копии друг друга. Кстати, вы тоже можете собрать таких, а чтобы облегчить вам задачу мы подготовили и выложили инструкцию по их сборке в формате LEGO Digital Designer.


Роботы друг с другом никак не соединены, но им нравится стоять рядом, ведь вместе они создают очень симметричную композицию. Мы будем поворачивать ротор у Правого робота и симметрия будет нарушаться. Задача Левого робота - поддерживать эту симметрию, поворачивая свой ротор в тоже положение, в которое повернут ротор у Правого.



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

0 - светодиод выключена
1 - светодиод включен

Если нам нужно передать число большее 1, на помощь придет двоичный код. Посмотрите на таблицу ниже, число 2 будет представлено как 10, число 6 как 110, а число 12 как 1100.


Этот принцип сохраняется и для больших чисел, просто последовательности 0 и 1 будут длиннее. Для перевода из десятичного числа в двоичное есть несколько способов, например вот такой:


Итак, угол поворота ротора в двоичном виде мы получили, как теперь его передать? Светодиод у Правого робота и датчик освещенности у Левого позволяют нам создать такое распространенное устройство как Универсальный асинхронный приёмопередатчик (УАПП, англ. Universal Asynchronous Receiver-Transmitter, UART) — узел вычислительных устройств, предназначенный для организации связи с другими цифровыми устройствами. Преобразует передаваемые данные в последовательный вид так, чтобы было возможно передать их по одной физической цифровой линии другому аналогичному устройству. 
Передача данных в UART осуществляется по одному биту в равные промежутки времени. Этот временной промежуток определяется заданной скоростью UART и для конкретного соединения указывается в бодах (что в данном случае соответствует битам в секунду).

Для начала давайте проведем небольшой эксперимент, будем включать и выключать в цикле светодиод датчика и посмотрим за сколько выполнится 1000 таких переключений: В конструкции роботов мы использовали блоки LEGO NXT, для их программирования воспользуемся языком NXC:

int t;

task main()
{
  t = 0;
  while(t < 1000)
  {
    t = t + 1;
    SetSensorLight(IN_3,true);
    SetSensorLight(IN_3,false);
  }
  ClearScreen();
  NumOut(0, LCD_LINE4, CurrentTick());
  Wait(10000);
}

Запустив эту программу мы с удивлением обнаружили цифру 42000. Значит каждое полное переключение занимает целых 42000/1000 = 42 мс, а переключение из режима в режим вероятно около - 42/2 = 21 мс. Это довольно много, быстрый UART нам не создать. С другой стороны наш проект учебный, для нас главное - наглядность и понимание принципов работы устройства, гигабитных скоростей будем достигать совсем в других проектах :).

Теперь давайте напишем программу для Правого, передающего робота. 

// угол, на который повернут ротор у Правого робота
int a;
// длительность одного бита данных, мс
int t;
// массив для хранения числа a в двоичной форме
bool b[8];
// в этой переменной запоминаем горит ли светодиод на датчике
bool y;

task main()
{

  while(true)
  {
    // светодиод на датчике выключен
    y = false;
    // сохраняем в переменной а угол поворота ротора
    a = MotorRotationCount(OUT_B);
    // Это время в мс, в течении которого передается 1 бит информации
    t = 200;

    // ограничиваем а (угол поворота ротора) пределами 0..255
    if (a < 0)
    {
      a = 0;
    }
    // Мы будем передавать только 1 байт, это максимум 255
    if(a > 255)
    {
      a = 255;
    }

    // Очищаем экран и выводим на него a в десятичной форме
    ClearScreen();
    NumOut(0,13,a);

    // Переводим a в двочиный вид и сохраняем в массив b[8]    
    // выводим на экран также двоичиное пердставление числа
    for (int i=7; i >= 0; i--)
    {
      if (a / pow(2, i) >= 1)
      {
         b[i] = true;
         a = a - pow(2,i);
         NumOut((7-i)*8,0,1);
      }
      else
      {
         b[i] = false;
         NumOut((7-i)*8,0,0);
      }
    }

    // включаем светодиод и передаем стартовый бит
    SetSensorLight(IN_3,true);
    y = true;
    // Время передачи меньше на 21 мс, именно столько включается светодиод
    Wait(t-21);

    // передаем байт данных бит за битом мз массива b[8]
    for(int i = 7;i >= 0;i--)
    {
      if(b[i] == true)
      {
        SetSensorLight(IN_3,true);
        // длительность задержки различна, в зависимости от того горел ли светодиод
        if(y == true)
        {
          Wait(t-1);
        }
        else
        {
          Wait(t-21);
        }
        y = true;
      }
      else
      {
        SetSensorLight(IN_3,false);
        if(y == false)
        {
          Wait(t-1);
        }
        else
        {
          Wait(t-21);
        }
        y = false;
      }
    }

    // включаем светодиод и передаем стоповый, 10-й, бит
    SetSensorLight(IN_3,true);
    if(y == true)
    {
      Wait(t-1);
    }
    else
    {
      Wait(t-21);
    }
    y = true;

    // выключаем светодиод, готовимся к следующей итерации цикла
    SetSensorLight(IN_3,false);
    y = false;
    Wait(t*4);
  }
}

После запуска программы робот начинает бодро моргать светодиодом датчика, передавая бит за битом показания энкодера на моторе с ротором. Теперь второму, Левому роботу, нужно принять эту двоичную посылку и перевести ее в десятичную форму. Принцип такого перекодирования также довольно прост:

После того как число переведено в десятичную форму вступит в дело ПД-регулятор и развернет ротор принимающего робота в тоже положение, что и у передающего.

Программа для Левого, принимающего робота у нас выглядит следующим образом.

// принятое число в десятичной форме
int a, a_tmp;
// массив для хранения принятого числа в двоичной форме
bool b[8];
// длительность одного бита данных, мс
int t;
// управляющее воздействие ПД-регулятора
int u;
// пропорциональный коэтф-т ПД-регулятора
int pk;
// дифф.коэф-т ПД-регулятора
int dk;
// "старая" ошибка в дифф.компоненты ПД-регулятора
int es;
// данные с датчика
int D1;
// ошибка ПД-регулятора
int e;

// ПД-регулятор устанавливает ротор в положение a градусов
task motor()
{
  // значение коэф-тов ПД-реглятора
  pk = 2;
  dk = 4;
  es = 0;
  while(true)
  {
    // вычисляем ошибку (отклонение от целевой величины)
    e = a - MotorRotationCount(OUT_B);
    // формируем управляющее воздействие
    u = e*pk+dk*(e-es);
    // juhfybxbdftv управляющее воздействие диапазоном -100..100
    if(u < -100)
    {
      u = -100;
    }
    if(u > 100)
    {
      u = 100;
    }
    // опдаем управляющее воздействие на мотор
    OnFwd(OUT_B,u);
    // сохраняем значение ошибки
    es = e;
  }
}

task main()
{

  SetSensorLight(IN_3,false);
  a = 0;
  t = 200;
  start motor;
  while(true)
  {
    // ждем стартовый бит
    while(Sensor(IN_3)<65);
    // ждем первый бит данных
    Wait(t+t/2);

    // считываем все биты данных и записываем в массив b[8]
    for(int i = 7;i >= 0;i--)
    {
      if(Sensor(IN_3)>65)
      {
        b[i] = true;
        Wait(t);
      }
      else
      {
        b[i] = false;
        Wait(t);
      }
    }
    // очищаем экран
    ClearScreen();
    // если стоповый бит не получен - ошибка приема-передачи   
    if(Sensor(IN_3)<65) TextOut(0, LCD_LINE6, "ERROR");
    
    // выводим на экран принятое двоичное число
    for(int i = 7;i >= 0;i--)    
    {
      if(b[i] == true)
      {
         NumOut((7-i)*8,0,1);
      }
      else
      {
         NumOut((7-i)*8,0,0);
      }
    }

    // переводим число в десятичное
    a_tmp = 0;
    for(int i = 7;i >= 0;i--)
    {
      if(b[i] == true)
      {
        a_tmp = a_tmp + 1 * pow(2, i);

      }
    }
    a = a_tmp;
    // выводим десятичное число на экран
    NumOut(0,13,a);
    Wait(t*2);
  }
}

С учетом стартовых и стоповых битов скорость нашего UART составляет 4 бита/сек.

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





Самое популярное