Программирование LEGO NXT роботов на языке NXC - Задачи и функции

Материал из roboforum.ru Wiki
Перейти к: навигация, поиск

Автор: Daniele Benedettelli

Перевод: © Ботов Антон aka =DeaD=, 2009

Эксклюзивно для www.roboforum.ru
копирование на другие ресурсы и публикация перевода
без разрешения его автора запрещены

Задачи и функции

До этого момента все наши программы состояли из просто одной задачи. Но программы на языке NXC могут иметь множественные задачи. Также возможно размещать куски программы в так называемых функциях, которые вы можете использовать в различных местах своей программы. Использование задач и функций делает вашу программу более лёгкой для понимания и более компактной. В этой главе мы рассмотрим эти возможности.

Задачи

Каждая программа NXC может состоять максимум из 255 задач; каждая из которых имеет уникальное имя. Задача с именем "main" всегда должна присутствовать в любой программе, так как это первая задача которая отправляется на выполнение. Другие задачи будут выполняться только если уже запущенная на выполнение задача скажет системе чтобы она их выполнила, или они могут быть неявно запланированы к выполнению; главная задача должна завершиться в этом случае перед тем, как будут выполнены такие задачи. После её завершения запланированные задачи будут выполняться параллельно.

Давайте я покажу вам использование задач на следующем примере. Мы хотим сделать программу по которой робот ездит по квадрату, как и раньше, но если он встречает препятствие - он должен на него среагировать. Это тяжело реализовать в одной задаче, так как робот должен делать две вещи в один и тот же момент - ездить по нужной траектории (т.е. включать\выключать моторы в нужные моменты времени) и следить за сенсорами. Так что лучше мы будем использовать для этого две задачи, первая из которых будет ездить по квадрату, а вторая реагировать на показания датчиков. Вот текст этой программы:

mutex moveMutex;
task move_square()
{
  while (true)
  {
    Acquire(moveMutex);
    OnFwd(OUT_AC, 75); Wait(1000);
    OnRev(OUT_C, 75); Wait(500);
    Release(moveMutex);
  }
}

task check_sensors()
{
  while (true)
  {
    if (SENSOR_1 == 1)
    {
      Acquire(moveMutex);
      OnRev(OUT_AC, 75); Wait(500);
      OnFwd(OUT_A, 75); Wait(500);
      Release(moveMutex);
    }
  }
}

task main()
{
  Precedes(move_square, check_sensors);
  SetSensorTouch(IN_1);
}

Главная задача просто объявляет какой сенсор куда подключен и после этого запускает две другие задачи, планируя их к выполнению, после чего завершает свою работу. Задача "move_square" бесконечно перемещает робота по квадрату. Задача "check_sensors" проверяет, сработал ли датчик касания и, если это так, отводит робота от препятствия.

Очень важно помнить, что будучи запущенными задачи работают одновременно, что может привести к непредсказуемым результатам, если задачи одновременно попытаются получить управление моторами робота.

Чтобы избежать таких проблем, мы объявили странный тип переменной, "mutex" (в русском языке этому слову соответствует "мьютекс", это сокращение от "mutual exclusion", т.е. взаимное исключение): мы можем взаимодействовать с таким типом переменных только с помощью функций Acquire и Release, записывая критические куски кода между этими функциями, при этом оставаясь уверенными, что только одна задача в один момент времени имеет полное управление над моторами робота.

Такие "мьютекс"-переменные еще называются семафорами, а такая техника программирования называется конкурентной. Более подробно это обсуждается в 10-й главе.

Функции

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

sub turn_around(int pwr)
{
  OnRev(OUT_C, pwr); Wait(900);
  OnFwd(OUT_AC, pwr);
}

task main()
{
  OnFwd(OUT_AC, 75);
  Wait(1000);
  turn_around(75);
  Wait(2000);
  turn_around(75);
  Wait(1000);
  turn_around(75);
  Off(OUT_AC);
}

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

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

Основное преимущество функций в том, что они хранятся в NXT только раз и таким образом экономится память. Но когда функции короткие, может быть лучше использовать встроенные ("inline") функции. Они не хранятся отдельно, а копия их размещается в каждом месте, откуда вызывается функция. Это использует больше памяти, но на количество таких функций ограничения нет. Такие функции могут быть объявлены следующим образом:

inline int Name( Args ) {
  //body;
  return x*y;
}

Определение и вызов встроенных функций делается так же как и для обычных. Смотрите пример использования этих функций ниже:

inline void turn_around()
{
  OnRev(OUT_C, 75); Wait(900);
  OnFwd(OUT_AC, 75);
}

task main()
{
  OnFwd(OUT_AC, 75);
  Wait(1000);
  turn_around();
  Wait(2000);
  turn_around();
  Wait(1000);
  turn_around();
  Off(OUT_AC);
}

В указанном выше примере, мы можем сделать время для разворота аргументом функции, как показано тут:

inline void turn_around(int pwr, int turntime)
{
  OnRev(OUT_C, pwr);
  Wait(turntime);
  OnFwd(OUT_AC, pwr);
}

task main()
{
  OnFwd(OUT_AC, 75);
  Wait(1000);
  turn_around(75, 2000);
  Wait(2000);
  turn_around(75, 500);
  Wait(1000);
  turn_around(75, 3000);
  Off(OUT_AC);
}

Заметим, что при определении в скобках после имени функции мы указываем аргументы этой функции. В этом случае мы показываем, что аргумент функции - целое число (можно выбрать другие типы), а имя этой переменной - "turntime". Когда у функции больше аргументов, вы должны разделять их запятыми. Обратите внимание, что в NXC, ключевое "sub" является синонимом "void"; также, функции могут иметь другой тип, отличный от "void", они могут возвращать целые числа, строковые значения вызывающему, для подробной информации на эту тему смотрите полную инструкцию по языку NXC.

Определяем макрос

Существует еще один способ дать небольшому фрагменту кода собственное имя для обращения. В языке NXC вы можете определить макрос ( не путайте с макросами BricxCC). Мы видели ранее, что можем определять константы используя ключевое слово "#define", назначая им имена. Но на самом деле мы можем таким же образом определить любой кусок кода. Вот та же самая программа, но с использованием макроса для отделения фукнции разворота.

#define turn_around \
OnRev(OUT_B, 75); Wait(3400);OnFwd(OUT_AB, 75);
task main()
{
  OnFwd(OUT_AB, 75);
  Wait(1000);
  turn_around;
  Wait(2000);
  turn_around;
  Wait(1000);
  turn_around;
  Off(OUT_AB);
}

После ключевого слова #define идёт имя turn_around которое будет означать текст указанный следом. Теперь, где бы вы не указали turn_around, это имя будет замещено определенным вами ранее текстом. Обратите внимание, что текст должен быть в одну строку. (Вообще то есть способ использования множественных строк внутри ключевого слова #define, но такой подход не рекомендован к использованию.)

На самом деле возможности #define еще более широкие. Они так же могут иметь аргументы. Например, мы можем сделать время поворота аргументом. Вот пример в котором мы определяем 4 макро: двигаться вперед, двигаться назад, поворачивать налево и поворачивать направо. Каждый из них имеет 2 аргумента - скорость и время выполнения.

#define turn_right(s,t) \
OnFwd(OUT_A, s);OnRev(OUT_B, s);Wait(t);
#define turn_left(s,t) \
OnRev(OUT_A, s);OnFwd(OUT_B, s);Wait(t);
#define forwards(s,t) OnFwd(OUT_AB, s);Wait(t);
#define backwards(s,t) OnRev(OUT_AB, s);Wait(t);

task main()
{
  backwards(50,10000);
  forwards(50,10000);
  turn_left(75,750);
  forwards(75,1000);
  backwards(75,2000);
  forwards(75,1000);
  turn_right(75,750);
  forwards(30,2000);
  Off(OUT_AB);
}

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

Подводим итоги

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

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

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