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

НейроКачели

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


Наверное все мы в детстве любили качаться на качелях. Как вы думаете, если ребенка впервые посадить на качели и не показать ему как на них раскачиваться (и не давать соответственно подсмотреть как это делают другие), получится у него их раскачать, сохранять и контролировать скорость раскачивания? В его мозге нет устойчивых взаимосвязей и образов того как ему это сделать. Он начнет пробовать совершать произвольные действия, если качели начнут раскачиваться в мозг пойдет визуальная и вестибулярная обратная связь и по тем действиям, которые он только что предпринимал, начнут формироваться новые нейронные цепочки, связанные с "раскачать качели". Если какие то действия наоборот начнут тормозить раскачавшиеся качели, например ребенок отклонился не в ту сторону - он "запомнит" что так делать не нужно, соответствующие связи будут ослаблены или созданы новые.
Похожим образом работает и искусственная нейросеть, которую мы создадим в нашем сегодняшнем проекте.
Для начала соберем конструкцию. Качели мы построили на базе LEGO Mindstorms EV3, использовав несколько дополнительных деталей из ресурсных наборов. Инструкцию по сборке наиболее сложной части конструкции -  качающихся человечков - в формате LEGO Digital Designer вы можете скачать по ссылке. Остальную часть качелей каждый без труда сможет достроить из тех деталей, которые будут у него под рукой.


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

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

Понятно, что в самом начале человечки будут дергаться туда-сюда хаотично. Введем блок самообучения. Качество обучения будем оценивать до достигнутой качелями средней скорости без учета знака, по данным гироскопа. Если скорость растет - предпринятая попытка раскачать качели верная, падает - "человечки" ошиблись.

Алгоритм таков:
1) Смотрим куда качаются качели (по данным гироскопа) - влево или вправо и выбираем коробок - первый или второй. Следует заметить что мы не рассматриваем ситуацию когда качели имеют нулевую скорость, например при старте, развивая алгоритм дальше нужно добавить третью коробку с камушками, но пока обойдемся без нее.
2) Вытаскиваем наугад камень из выбранной коробки. Смотрим его цвет и кладем обратно. если камень белый - даем человечкам команду отклониться влево, если черный - вправо.
3) Оцениваем скорость качелей. Если она выросла - кладем в коробку, из которой мы брали камень еще один камень такого же цвета. Если скорость упала - убираем из коробки камень то го цвета, который был вытащен. Следует заметить что последний камень каждого цвета лучше не убирать, пусть остается.
4) Переходим к п.1

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


Программировать качели мы будем на Python.  Давайте посмотрим код:

# импортируем модуль для работы с железом EV3
from ev3dev.ev3 import *
# импортируем модуль математики
from math import *
# импортируем модуль для работы со временем
from time import *
# импортируем модуль генератора случайных чисел
from random import random
# импортируем модуль многопоточности
import threading

# начальная скорость моторов ("человечков")
speedB = 0
speedC = 0

# текущая средняя скорость движения качелей
moving_average = 0
# предыдущая средняя скорость движения качелей
moving_average_old = 0

# Позиция человечков -100..100
pos = 0

# направление движения качелей
state_gyro = 0

# описание нейрона сети
class brain():
    def __init__(self):
        self.Black = 1
        self.White = 1

# создаем пару нейронов
BrainLeft = brain()
BrainRight = brain()

# объект для отслеживания кнопок на блоке
btn = Button()

# объекты для моторов
B = LargeMotor('outB')
C = LargeMotor('outC')

# объект для работы с гироскопом
gyro = GyroSensor("in4")

# устраняем дрейф гироскопа и ставим его в режим измерения скорости
gyro.mode = "GYRO-RATE"
gyro.mode = "GYRO-ANG"
gyro.mode = "GYRO-RATE"

# человечков - в исходную позицию sleep(1)
B.run_forever(speed_sp=80)
C.run_forever(speed_sp=80)
sleep(3)

B.stop(stop_action="brake")
C.stop(stop_action="brake")
sleep(1)

B.reset()
C.reset()

B.run_to_rel_pos(position_sp=-120, speed_sp=80)
C.run_to_rel_pos(position_sp=-120, speed_sp=80)
while any(C.state): sleep(0.1)

B.stop(stop_action="brake")
C.stop(stop_action="brake")

sleep(1)
B.reset()
C.reset()

# готов к работе!
Sound.speak('Ready').wait()

stop = False

# П-регулятор для управления человечками
def reg():
    while not stop:
        speedB = ((-1*pos)-B.position)*8
        speedC = (pos-C.position)*8

        if(speedB > 900): speedB = 900
        if(speedB < -900): speedB = -900
        if(speedC > 900): speedC = 900
        if(speedC < -900): speedC = -900

        B.run_forever(speed_sp=speedB)
        C.run_forever(speed_sp=speedC)

        sleep(0.01)

# Запускаем регулятор в параллельном потоке
t = threading.Thread(target=reg)
t.daemon = True
t.start()

# Максимальная достигнутая скорость
max_speed = 0

while not stop:

    # вылет по кнопке "назад" на блоке     
    stop = btn.backspace
    
    # скорость по показаниям гироскопа
    state_gyro_speed = gyro.value()
        
    if(state_gyro_speed != 0): 
        # текущее направление движения качелей
        state_gyro = state_gyro_speed/abs(state_gyro_speed)
        if(state_gyro > 0):
            # если влево, и вытащили черный камень
            if(random() <= (BrainLeft.Black/(BrainLeft.Black + BrainLeft.White))): pos = -100
            # если белый 
            else: pos = 100
        else:
            # если вправо, и вытащили черный камень
            if(random() <= (BrainRight.Black/(BrainRight.Black + BrainRight.White))): pos = -100
            # если белый
            else: pos = 100
            
    moving_average_old = moving_average
       
    # ждем перед оценкой
    sleep(0.25)

    moving_average = abs(state_gyro_speed)*0.1 + moving_average*0.9

    if(state_gyro > 0):
        # если скорость не упала при движении влево
        if(moving_average >= moving_average_old): 
            if(pos<0): BrainLeft.Black += 1
            else: BrainLeft.White += 1
        else:
            # если скорость упала при движении влево
            if(pos<0 and BrainLeft.Black > 1): BrainLeft.Black -= 1
            elif(pos>0 and BrainLeft.White > 1): BrainLeft.White -= 1

    elif(state_gyro < 0):
        # если скорость не упала при движении вправо
        if(moving_average >= moving_average_old): 
            if(pos<0): BrainRight.Black += 1
            else: BrainRight.White += 1
        else:
        # если скорость упала при движении вправо
            if(pos<0 and BrainRight.Black > 1): BrainRight.Black -= 1
            elif(pos>0 and BrainRight.White > 1): BrainRight.White -= 1
    # выводим состояние нейронной сети     print("[[ " + str(BrainLeft.Black) + ", " + str(BrainLeft.White) + "][ " + str(BrainRight.Black) + ", " + str(BrainRight.White) + "]]",moving_average)

    # если скорость выросла на 25, фиксируем новый рекорд    
    if moving_average > max_speed + 25:
        print("[[ " + str(BrainLeft.Black) + ", " + str(BrainLeft.White) + "][ " + str(BrainRight.Black) + ", " + str(BrainRight.White) + "]]",moving_average)
        max_speed = moving_average
        # если скорость выросла до 200 - обучение завершено
        if max_speed < 200:
            Sound.speak('New Record')
        else:
            Sound.speak('Learning Complete')

Sound.beep().wait() 
B.stop(stop_action="brake")
C.stop(stop_action="brake")

# завершение работы
Sound.speak('Stop').wait()


print("[[ " + str(BrainLeft.Black) + ", " + str(BrainLeft.White) + "][ " + str(BrainRight.Black) + ", " + str(BrainRight.White) + "]]")

пятница, 7 июля 2017 г.

Немного о футболе роботов WRO gen.III. Часть 5

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


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

2014 год:

В этом году мы еще не участвовали в соревнованиях в данной категории, вели подготовительные работы, изучали доступные материалы и экспериментировали.
  • Первая версия нападающего с дриблингом и мощной пиналкой была собрана по мотивам робота команды "Омега", вскоре после этого дриблинг был запрещен и конструкция стала не актуальной.

2015 год:
  • Ориентация на поле с использованием датчика освещенности (поля в те времена были еще с цветными зонами)
  • Построение кратчайшей траектории объезда мяча, с учетом положения на поле
  • Выбор направления удара из 5 возможных (3 цветных поля, 2 белых)
  • Контроль застревания "по компасу"

2016 год:
  • Ориентация на поле с использованием ультразвукового датчика
  • Построение кратчайшей траектории объезда мяча, с учетом положения на поле, наличия бортов и "скатов"
  • Выбор направления удара из 9 возможных (визуально поле делим на 9 зон, матрица 3x3, из каждой зоны свое направление удара по воротам)
  • Контроль застревания "по компасу" и по наличию мяча
  • Взаимодействие роботов с использованием Bluetooth, роботы обмениваются информацией о том, в какой зоне сейчас мяч

2017 год:
  • Ориентация на поле с использованием ультразвукового датчика, энкодеров, датчика освещенности. Робот теперь знает где он с точностью 1-2 см по обоим координатным осям. Накопление информации о перемещениях робота и мяча для использования при построении маршрутов и маневров
  • Пересчет неравномерного распределения показаний компаса в идеальное для построения модели и расчетов в идеальной системе координат
  • Помехоустойчикое чтение датчиков
  • Определение направления на мяч "в градусах". Определение расстояния до мяча с точностью до 3-5 см
  • Построение кратчайшей траектории объезда мяча, с учетом положения на поле, наличия бортов и "скатов", пометки о возможных помехах на пути по факту застревания с корректировкой траектории
  • Выбор направления удара из 180 возможных, направление удара рассчитывается "на лету" с точностью 3-4 градуса в момент захода на мяч и учитывается при построении траектории движения к нему
  • Контроль застревания "по компасу", по наличию мяча, по наличию препятствий, по нагрузке на моторы
  • Взаимодействие роботов с использованием Bluetooth, роботы обмениваются информацией о координатах мяча на поле
  • Повышающий редуктор у нападающего, линейная скорость движения выше в 2 раза
  • Мощная "пиналка" у нападающего
  • Гибкое управление скоростью движения, плавные старты и остановы
  • Генерация роботами в процессе работы лог-файлов в формате srt-субтитров для возможности просмотра поверх снятого видео для анализа и отладки

Мы рассматриваем данные состязания в первую очередь как совокупность отличных задачек, в которых можно применить те навыки, которые были получены командой при работе над хоббийными проектами. Например, работа над такими роботами как NAVIDOZ3R и LEGO Pong позволила нам применить для локализации роботов полученные знания в области тригонометрии, которую до сих пор в школе пока не изучали.


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

Возможностей платформы NXT мы до сих пор не исчерпали, разве что памяти для сложных вычислений стало не хватать. Интересной находкой, реально дающей колоссальные возможности, стало использование приоритезации задач (процессов). Неприятной обнаруженной особенностью - необходимость работы с мьютексами при разделении "железа" между процессами. Почти все команды-соперники перешли на EV3, роботы на основе NXT встречаются все реже. Похоже перехода не избежать и нам.


В этом году уровень команд заметно вырос, игра стала более стремительной, не оставляющей времени "на подумать". Если в прошлом году была всего одна команда, с роботом на самодельных omni-колесах, позволяющих ему без разворотов перемещаться в любых направлениях, то в этом году и остальные команды, ездившие "на мир" в прошлом сезоне привезли оттуда идеи для своих omni-роботов. Решения, предложенные этими командами в итоге оказались очень интересными, не оставляющими шансов для роботов с традиционной конструкцией.


Помните серию "Футбол" из Смешариков? На полях творилась похожая картина. Пока наш нападающий с традиционной конструкцией "думал" и выполнял маневры, стремительные omni-соперники быстрее добирались до мяча и посылали его в сторону узкого борта с нашими воротами. 2-3 точных удара по мячу, которые успевал  за тайм сделать наш нападающий, в те момент когда он все же успевал подобрать мяч, не шли не в какое сравнение с 40-50 попытками забить гол нам. Пусть они были не точные, но теория вероятностей штука упрямая, ширина ворот - 25% от длины узкого борта.


Тем не менее решения, над которыми наша команда работала в этом сезоне не подвели нас, роботы вели себя так, как и планировалось, чем мы очень довольны. Как гласит лозунг WRO футбола, "ценно не то, что вы выиграете или проиграете, а то, как много вы узнаете".

Теперь мы планируем немного отдохнуть и начнем новый сезон наших хоббийных проектов. До скорых встреч!


воскресенье, 21 мая 2017 г.

Управляемый робот-футболист

Давненько не было вестей от нашей команды. Вы думаете что мы забросили роботов? Не дождетесь!) С середины января находимся в режиме планомерной подготовки к ВРО, снова готовимся к футболу роботов. В этом году алгоритмы машин снова переписаны заново и во многом не повторяют логику работы наших футболистов предыдущих поколений.


Учебный процесс тоже не стоял на месте, в нашей копилке теперь вот такой крупнокалиберный арсенал: "Введение в программирование (C++)" от Академии Яндекса, "Программирование на Python", "Python: основы и применение", "Введение в Linux" от Института биоинформатики, "Введение в архитектуру ЭВМ. Элементы операционных систем" от Computer Science Center.


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

Поиграть против своей автономной команды очень интересно и полезно:

  • Зная слабые места своих автономных роботов можно создавать на поле такие игровые ситуации, в которых они проявятся. Это важно для отладки алгоритмов и конструкций.
  • Управляя роботом ты моделируешь в голове работу некого алгоритма, который затем может быть перенесен в программу автономного игрока
  • Это реальный драйв - ты играешь в игру не на экране, а в реальном мире, с полноценной физикой и красивыми текстурами. Обзор 360 градусов и высокое разрешение!


Так как мы находимся в процессе изучения языка Python, то интересной учебной задачей стала реализация такого управляемого робота-оппонента на базе ev3dev. Можно конечно "не заморачиваться" и использовать смартфон и написанное кем-то приложение, но это не наш путь. На борту у EV3 есть Bluetooth, у китайцев на Aliexpress - дешевые блютузные джойстики - почему бы не поуправлять роботом с реальных кнопок и стика? Используя стандартное ПО LEGO EV3 такую связку заставить работать невозможно, ev3dev открывает перед нами такую возможность.

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


Чтобы джойстик заработал с EV3, необходимо "спарить" устройства привычным образом, после чего в /dev/input должен появиться новый девайс:

robot@ev3dev:~$ ls /dev/input
by-path  event0  event1  event2

Кроме Bluetooth-джойстика можно использовать беспроводной USB-джойстик, воткнув его USB-приемник в соответствующий порт на роботе. Беспроводная клавиатура с интерфейсом USB или Bluetooth тоже подойдет. Технически роботом можно управлять используя даже беспроводную мышку, но это вероятно не особенно удобно. Главное условие - после подключения устройства оно должно появляться в устройствах ввода, в /dev/input

Для получения данных с HID-устройств мы использовали стандартный модуль Python evdev. Ничего доустанавливать в ev3dev не требуется.

Нашу программу для робота можно скачать по ссылке. Основная фишка управления по сравнению со "смартфонным" - реализация плавного разгона и торможения, что обеспечивает комфортное управление. Выглядит программа следующим образом:

#!/usr/bin/env python3

# Подключаем модуль для управления EV3
from ev3dev.ev3 import *
# Подключаем модуль для чтения данных с HID-устройств
import evdev

# Создаем объект device, измените на ваш /dev/input/event2
device = evdev.InputDevice('/dev/input/event2')

StatusGo = 0
StatusLR = 0

# Целевая скорость, робот наберет ее когда разгонится
speed = 100

# Реальная скорость, с нее робот стартует
real_speedB = 0
real_speedC = 0

speedB = 0
speedC = 0

# Признак зарершения программы (нажат акнопка Start на джойстике)
STOP = False

# Создаем объекты - моторы B и C
B = LargeMotor('outB')
C = LargeMotor('outC')

# Цикл пока не нажата кнопка Start на джойстике
while not STOP:
    # Читаем список событий с джойстика
    gen = device.read()
    try:
       # Для всех событий в списке
       for event in gen:
           # Выделяем те, которые возникли при нажатиях кнопок
           if event.type == evdev.ecodes.EV_KEY:
               # преобразуем такие события в строку myStr
               myStr = str(event)
               
               # если отпущена кнопка "Влево"
               if myStr.find("code 168, type 01, val 00") >= 0:
                   StatusLR = 0
               # если нажата кнопка "Влево"
               if myStr.find("code 168, type 01, val 01") >= 0:
                   StatusLR = -1
               # если удерживается кнопка "Влево"
               if myStr.find("code 168, type 01, val 02") >= 0:
                   StatusLR = -2
               # если отпущена кнопка "Вправо"
               if myStr.find("code 208, type 01, val 00") >= 0:
                   StatusLR = 0
               # если нажата кнопка "Вправо"
               if myStr.find("code 208, type 01, val 01") >= 0:
                   StatusLR = 1
               # если удерживается кнопка "Вправо"
               if myStr.find("code 208, type 01, val 02") >= 0:
                   StatusLR = 2
               # если отпущена кнопка "Вперед"  
               if myStr.find("code 172, type 01, val 00") >= 0:
                   StatusGo = 0
               # если нажата кнопка "Вперед"
               if myStr.find("code 172, type 01, val 01") >= 0:
                   StatusGo = speed*0.75
               # если удерживается кнопка "Вперед"
               if myStr.find("code 172, type 01, val 02") >= 0:
                   StatusGo = speed
               # если отпущена кнопка "Назад"
               if myStr.find("code 114, type 01, val 00") >= 0:
                   StatusGo = 0
               # если нажата кнопка "Назад"
               if myStr.find("code 114, type 01, val 01") >= 0:
                   StatusGo = -1*(speed * 0.75)
               # если удерживается кнопка "Назад"
               if myStr.find("code 114, type 01, val 02") >= 0:
                   #print ("GO BREAK")
                   StatusGo = -1*speed
               # если нажата кнопка "Start"         
               if myStr.find("code 164, type 01, val 02") >= 0:
                   print ("BREAK! STOP PROGRAMM")
                   STOP = True
               # Кнопка - среднее значение скорости
               if myStr.find("code 164, type 01, val 01") >= 0:
                   speed = 75
               # Кнопка нажата - уменьшить скорость
               if myStr.find("code 115, type 01, val 01") >= 0:
                   speed = speed - 5
                   if(speed < 5):
                       speed = 5
               # Кнопка удерживается - уменьшить скорость        
               if myStr.find("code 115, type 01, val 02") >= 0:
                   speed = speed - 1
                   if(speed < 5): 
                       speed = 5
               # Кнопка нажата - увеличить скорость
               if myStr.find("code 113, type 01, val 01") >= 0:
                   speed = speed + 5
                   if(speed > 100): 
                       speed = 100
               # Кнопка удерживается - увеличить скорость
               if myStr.find("code 113, type 01, val 02") >= 0:
                   speed = speed + 1
                   if(speed > 100): 
                       speed = 100

    except IOError:
        pass
    
    # перебрасываем статусы нажатий в мощности моторов  
    speedB = StatusGo
    speedC = StatusGo
    
    # Поворот влево
    if(StatusLR < 0):
        speedB = speedB-(25*abs(StatusLR))
        speedC = speedC+(25*abs(StatusLR))         
    # поворот вправо
    if(StatusLR > 0):
        speedC = speedC-(25*StatusLR)
        speedB = speedB+(25*StatusLR)
    
    # ограничение скорости
    if(speedB > 100):
        speedB = 100
    if(speedC > 100):
        speedC = 100
    if(speedB < -100):
        speedB = -100
    if(speedC < -100):
        speedC = -100

    # плавный разгон и торможение
    if(abs(speedB) > 5 and abs(speedC) > 5):
        real_speedB = real_speedB*0.95 + speedB*0.05
        real_speedC = real_speedC*0.95 + speedC*0.05
    if(speedB == 0 and speedC == 0):
        real_speedB = real_speedB*0.95
        real_speedC = real_speedC*0.95
    if(speedB == 0 and abs(real_speedB) < 5):
        real_speedB = 0
    if(speedC == 0 and abs(real_speedC) < 5):
        real_speedC = 0
    
    # подаем рассчитанные мощности на моторы        
    B.run_forever(speed_sp=real_speedB*9)
    C.run_forever(speed_sp=real_speedC*9)

# останавливаем моторы после вылета из цикла         
B.stop(stop_action="hold")
C.stop(stop_action="hold")

# сигнал завершения программы
Sound.beep()

суббота, 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 бита/сек.

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





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