Текущий раздел:

Описания RDS

Руководство программиста

Глава 2. Создание моделей блоков

§2.14. Программное управление расчетом

§2.14.2. Сброс подсистемы в начальное состояние

# Руководство программиста

## Глава 2. Создание моделей блоков

### §2.14. Программное управление расчетом

#### §2.14.2. Сброс подсистемы в начальное состояние

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

Сброс расчета – совершенно необходимая операция при работе с некоторыми видами схем. Например, если схема моделирует какой-либо протяженный во времени процесс, перед его расчетом с новыми параметрами следует сбросить все блоки в начальное состояние. Часто возникают задачи, в которых необходимо многократно повторять расчет для получения нужного результата. К таким задачам относятся, например, задачи оптимизации, в которых требуется подобрать такие параметры схемы, при которых она ведет себя наилучшим образом. Что такое «вести себя наилучшим образом» обычно формально задается при помощи вычисления некоторого критерия, позволяющего оценить качество поведения системы. Многократно повторяя расчет с разными значениями параметров, находят такой их набор, при котором значение этого критерия будет минимальным (или максимальным – все зависит от того, как задать критерий). Другой пример задачи, требующей многократного моделирования – построение графика зависимости установившегося значения на выходе схемы от одного из ее параметров. Поскольку значение на выходе схемы устанавливается не сразу, для каждого значения параметра нужно запускать расчет на некоторое время, затем фиксировать очередную точку графика, сбрасывать расчет, менять параметр, снова запускать расчет на некоторое время и т.д. Естественно, проводить все эти сбросы и перезапуски вручную было бы слишком трудоемко, такие процессы необходимо автоматизировать.

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

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

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

Пусть наша метательная машина имеет ствол, который может поворачиваться относительно горизонтальной оси, расположенной на высоте h0 от поверхности земли (рис. 94).

Рис. 94. Метательная машина в системе координат

Расстояние от оси вращения ствола до его конца (точки вылета снаряда) назовем l0. Полет снаряда мы будем рассматривать в двумерной системе координат «высота-дальность», как на рисунке: ось x будет лежать на поверхности земли, а ось y будет направлена вертикально вверх, проходя через точку поворота ствола. Будем считать, что при любом угле возвышения α наша метательная машина обеспечивает снаряду при вылете из ствола заданную начальную скорость v0, которая не зависит от угла возвышения. Таким образом, в точке вылета (x0,y0) снаряд будет иметь скорость v0, вектор которой направлен под углом α к горизонту. Координаты точки вылета можно вычислить следующим образом:

С момента вылета из ствола на снаряд будут действовать всего две силы: сила тяжести mg, направленная вертикально вниз (m – масса снаряда), и сила сопротивления воздуха F, направленная против вектора мгновенной скорости снаряда v (рис. 95).

Рис. 95. Силы, действующие на снаряд в полете

Величина силы сопротивления воздуха F зависит от модуля вектора скорости снаряда. Для ее вычисления мы будем пользоваться формулой, полученной Н. А. Забудским:

,

где

- R – радиус поперечного сечения снаряда (м),
- Δ – плотность воздуха в момент выстрела,
- Δ0 – плотность воздуха при температуре 15°С и давлении 760 мм.рт.ст.,
- v – скорость снаряда (м/с),
- A, n – параметры, зависящие от скорости снаряда следующим образом:

Диапазон v, м/с | A | n
--- | --- | ---
0…240 | 0.014 | 2
240…295 | 0.0000583 | 3
295…375 | 0.00000000067 | 5
375…419 | 0.000094 | 3
419…550 | 0.0394 | 2
550…800 | 0.2616 | 1.7
800…1000 | 0.713 | 1.55

Для простоты мы не будем учитывать разницу плотностей воздуха – будем считать, что мы стреляем именно при температуре 15°С и давлении 760 мм. Заменив в формуле радиус поперечного сечения снаряда R на его диаметр D (мы договорились задавать именно диаметр), получим:

Разложим силу сопротивления воздуха F на горизонтальную и вертикальную составляющие Fx и Fy. Считая, что вектор мгновенной скорости снаряда v в данный момент направлен под углом φ к горизонту, то есть имеет горизонтальную составляющую vx=v cos φ и вертикальную vy=v sin φ, получим:

Обозначив через ax и ay горизонтальную и вертикальную составляющие ускорения снаряда соответственно, из второго закона Ньютона получим:

Поскольку ускорение – это первая производная скорости по времени, получаем следующие дифференциальные уравнения:

Нулевым моментом времени (t=0) будем считать момент вылета снаряда из ствола. В этом случае начальными условиями для приведенных выше уравнений будут горизонтальная и вертикальная составляющие вектора начальной скорости v0 (см. рис. 94):

Нам нужно рассчитать траекторию полета снаряда, то есть функции x(t) и y(t). Поскольку скорость – первая производная координаты по времени, получаем следующие дифференциальные уравнения (начальными условиями для них будут координаты точки вылета):

Теперь, объединив все приведенные выше уравнения и добавив к ним вычисление модуля вектора скорости по его компонентам vx и vy, получим систему дифференциальных и алгебраических уравнений, описывающих полет нашего снаряда:

Рис. 96. Переход к дискретному времени

Из-за нелинейной зависимости силы сопротивления воздуха от скорости полета снаряда эта система не имеет аналитического решения. Мы будем решать ее численно, используя простейший метод численного интегрирования – метод Эйлера. От непрерывного времени t мы перейдем к дискретным отсчетам tk, интервал между которыми постоянен и равен Δt (рис. 96). Для некоторой непрерывной функции z(t) моменту времени tk будет соответствовать значение zk=z(tk). Если функция z(t) задана дифференциальным уравнением вида

,

то при достаточно малом Δt можно приближенно заменить dz/dt на Δz/Δt, то есть заменить дифференциалы конечными разностями, и получить следующее соотношение для момента времени tk:

.

Поскольку

,

рекуррентное выражение, позволяющее вычислить значение zk+1 по известному zk и правой части дифференциального уравнения f(t), будет выглядеть следующим образом:

.

Фактически, мы считаем, что на интервале времени Δt производная функции z(t), то есть скорость изменения этой функции, не меняется и равна f(tk). Чем меньше будет интервал времени Δt, тем точнее набор отсчетов {z0, z1, …, zk, …} будет совпадать с функцией z(t).

Применив те же рассуждения к дифференциальным уравнениям, описывающим полет снаряда, получим следующую систему разностных уравнений:

Эти уравнения позволяют по текущим значениям x, y, vx и vy вычислить значения, которые примут эти переменные через интервал времени Δt. Такие уравнения уже можно закладывать в программу модели блока.

Рис. 97. Определение точки встречи
снаряда с поверхностью

По условиям задачи нам требуется определить дальность полета снаряда при выстреле с заданным углом возвышения α. Дальность полета L – это значение горизонтальной координаты снаряда в тот момент, когда его вертикальная координата стала равна нулю (см. рис. 94). Поскольку при моделировании полета снаряда мы численно интегрируем описывающую его движение систему дифференциальных уравнений, траекторию (x(t), y(t)) мы получаем в виде набора отсчетов (xk, yk), отстоящих друг от друга на интервал времени Δt. Мы считаем, что на этом временном интервале производные координат и компонентов скорости снаряда не изменяются, то есть движение снаряда между вычисляемыми точками траектории считается прямолинейным с постоянной скоростью. Таким образом, траектория снаряда в сделанном нами приближении представляет собой кусочно-линейную функцию. Для того, чтобы определить дальность полета, нам нужно дождаться, когда вертикальная координата снаряда станет отрицательной и определить точку пересечения отрезка, соединяющего предпоследний и последний отсчеты траектории, с горизонтальной осью (рис. 97). Поскольку прямая, проходящая через точки (xk−1, yk−1) и (xk, yk) описывается уравнением

,

координата L пересечения этой прямой с горизонтальной осью (y=0) вычисляется следующим образом:

Теперь у нас есть все формулы, необходимые для расчета траектории и дальности полета снаряда. Однако, прежде чем приступать к созданию функции модели блока, который будет производить этот расчет, напишем одну вспомогательную функцию, которая существенно облегчит нам настройку этого и некоторых других блоков. В нашем блоке довольно много вещественных параметров: диаметр снаряда, его масса, начальная скорость и т.п. Для удобства пользователя следует предусмотреть в блоке функцию настройки этих параметров, причем хранить эти параметры имеет смысл в статических переменных блока, чтобы у пользователя был выбор: вводить значения параметров в окне настройки или получать их значения по связям от других блоков. В §2.7.4, мы создали универсальную функцию AddWinEditOrDisplayDouble, которая добавляет в окно настроек поле ввода для редактирования значения по умолчанию вещественной статической переменной блока, если к ней в данный момент не подключена связь. Сейчас, на ее основе, мы напишем универсальную функцию настройки, в которую передаются номера переменных блока и заголовки для соответствующих им полей ввода, а функция самостоятельно создает эти поля, открывает окно, ждет ввода пользователя и, если он нажал в окне кнопку «OK», записывает введенные им значения обратно в переменные. Эта функция пригодится нам не только для блока моделирования полета снаряда, но и для других блоков, все параметры которых – вещественные числа.

Для того, чтобы в универсальную функцию настройки можно было передать произвольное число пар «номер переменной – заголовок поля», сделаем одним из ее параметров массив строк. Каждая строка будет начинаться с символьного представления номера переменной (нужно помнить, что нумерация переменных в RDS начинается с нуля), за которым без пробела будет следовать заголовок поля ввода. Например, если в пятнадцатой переменной блока хранится масса снаряда в килограммах, в массив нужно будет включить строку «15Масса снаряда, кг». Чтобы не передавать в функцию вместе с указателем на начало массива еще и его размер, будем считать, что он всегда завершается значением NULL. В этом случае функция, встретив в массиве NULL вместо очередной строки, будет знать, что элементы массива закончились.

Универсальная функция настройки вещественных параметров будет выглядеть следующим образом:

      // Функция настройки вещественных параметров, хранящихся в
      // статических переменных блока.
      // Возвращает: 1 – ОК, 0 – отмена.
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      int SetupDoubleVars(
        RDS_BHANDLE Block,    // Настраиваемый блок
        const char *wintitle, // Заголовок окна настройки
        const char **vars)    // Массив строк-описаний полей ввода
      { RDS_HOBJECT window; // Идентификатор вспомогательного объекта
        BOOL ok;            // Пользователь нажал "OK"
        int count=0;
        BOOL *Connections;
        int *VarNums;

        if(vars==NULL) // Ошибка – массив не передан
          return 0;

        // Подсчитываем число полей ввода в массиве vars
        while(vars[count]!=NULL)
          count++;
        if(!count) // Ошибка – массив пуст
          return 0;

        // Отводим вспомогательный массив логических значений, в котором
        // будем запоминать наличие связей, присоединенных к переменным,
        // а также массив целых чисел для номеров переменных, считанных
        // из строк массива vars
        Connections=new BOOL[count];
        VarNums=new int[count];

        // Создаем окно с заголовком wintitle
        window=rdsFORMCreate(FALSE,-1,-1,wintitle);

        // Добавляем поля ввода из массива vars, заполняя массивы
        // Connections и VarNums
        for(int i=0;i<count;i++)
          { char *str;
            // Считываем из строки номер переменной. Указатель на первый
            // после номера символ запишется в str.
            VarNums[i]=strtol(vars[i],&str,0);
            // Если после номера строка кончается, присваиваем str
            // значение NULL, чтобы в качестве заголовка было
            // использовано имя переменной
            if(*str==0)
              str=NULL;
            // Добавляем поле ввода, запоминаем наличие связи
            Connections[i]=AddWinEditOrDisplayDouble(window,Block,
                      VarNums[i],i,str);
          }

        // Открываем окно
        ok=rdsFORMShowModalEx(window,NULL);
        if(ok)
          { // Нажата кнопка OK – запись параметров в переменные
            for(int i=0;i<count;i++)
              if(!Connections[i]) // У переменной нет связи
                { char *str=rdsGetObjectStr(window,i,RDS_FORMVAL_VALUE);
                  // Устанавливаем новое значение по умолчанию
                  rdsSetBlockVarDefValueStr(Block,VarNums[i],str);
                }
          }
        // Удаляем вспомогательные массивы
        delete[] Connections;
        delete[] VarNums;
        // Уничтожаем окно
        rdsDeleteObject(window);
        // Возвращаем 1 или 0
        return ok?1:0;
      }
      //=========================================

Функция принимает три параметра: идентификатор блока, для которого вызывается функция настройки (Block), текст заголовка окна (wintitle) и массив строк описанного выше формата (vars). Эта функция, прежде всего, подсчитывает число строк в массиве vars, перебирая их до тех пор, пока не встретит значение NULL. Число обнаруженных строк записывается в переменную count. Затем отводятся два вспомогательных массива из count элементов: логический массив Connections, в котором будет запоминаться наличие связи у каждой из перечисленных в массиве vars переменных, и целый массив VarNums, в который будут записаны номера переменных блока, полученные при разборе строк из vars. Затем, как обычно, сервисной функцией RDS rdsFORMCreate создается вспомогательный объект-окно, идентификатор которого присваивается переменной window.

Далее в цикле перебираются строки из массива vars, каждая из которых разбивается на номер переменной и заголовок поля при помощи функции strtol из стандартной библиотеки языка C (ее прототип описан в файле «stdlib.h»). Эта функция преобразует строку, переданную в ее первом параметре, в целое число, и возвращает полученное значение. В третьем параметре функции передается основание системы счисления преобразуемого числа (в нашем случае передается 0, при этом функция сама определит систему счисления по форме записи). Во втором параметре функции передается указатель на переменную, в которую записывается указатель на первый символ строки, который функция не смогла распознать, то есть первый не относящийся к числу символ. Нам именно это и нужно: в строке из массива vars сразу за числом, указывающим номер переменной в блоке, следует текст заголовка поля ввода, и мы получим указатель на первый следующий за числом символ, то есть на начало заголовка. Например, если в vars[i] будет записана строка «15Масса снаряда, кг», то после вызова

      VarNums[i]=strtol(vars[i],&str,0);

VarNums[i] получит значение 15, а str будет указывать на символ «М» в слове «Масса». Таким образом, при помощи одного вызова мы получаем и номер переменной, и указатель на начало заголовка поля ввода.

Если str указывает на нулевой символ, то есть на маркер конца строки, значит, строка содержала только номер переменной, а заголовка поля ввода в ней не было. В этом случае переменной str принудительно присваивается значение NULL, поскольку вызываемая нами функция AddWinEditOrDisplayDouble написана так, чтобы при передаче ей NULL в качестве заголовка поля ввода использовалось имя переменной блока (иногда имена переменных достаточно информативны сами по себе). В качестве номера переменной в функцию передается только что полученное нами значение VarNums[i], в качестве номера поля ввода – значение индекса цикла i, а результат ее возврата, то есть наличие связи у переменной, записывается в элемент массива Connections[i]. Теперь можно открывать окно функцией rdsFORMShowModalEx, которая вернет управление только тогда, когда пользователь закроет это окно кнопками «OK» или «Отмена».

Если пользователь нажал «OK» (переменная ok в этом случае будет иметь значение TRUE), мы снова перебираем все переменные из массива vars в цикле от 0 до count-1 и для тех из них, у которых нет присоединенной связи (Connections[i] имеет значение FALSE), записываем строку, введенную пользователем в поле ввода i, в значение по умолчанию переменной с номером VarNums[i].

Перед завершением функции уничтожаются вспомогательные массивы и объект-окно, после чего функция возвращает 1, если пользователь нажал в окне кнопку «OK», и 0, если кнопку «Отмена» (именно такие значения должна возвращать функция модели, при наличии и отсутствии изменений после вызова функции настройки).

С использованием функции SetupDoubleVars написание моделей блоков, в которых нужно настраивать вещественные параметры, сильно упрощается. Например, если у нас есть три переменных блока типа double с номерами 2, 3 и 8, которые используются для хранения настроечных параметров, то часть модели, ответственная за вызов функции настройки, может выглядеть так:

      // Массив строк, описывающих номера переменных и заголовки полей
      // ввода (объявлен как static, чтобы не занимал место в стеке)
      static const char *setup[]={
        "2Параметр 1",
        "3Параметр 2",
        "8Еще один параметр",
        NULL};

        …

        // Вызов функции настройки
        case RDS_BFM_SETUP:
          return SetupDoubleVars(BlockData->Block,
            "Заголовок окна",setup);

Вернемся к блоку, моделирующему полет снаряда, и определимся с необходимыми для его работы переменными. Будем считать, что выстрел производится при запуске расчета. При выстреле блок должен будет установить начальные значения переменных vx и vy, вычислив их по значениям модуля начальной скорости v0 и угла возвышения α, которые мы сделаем входами блока. Здесь мы встретимся с проблемой, на которой нужно остановиться подробно.

Согласно логике расчета, значения v0 и α нужны блоку только в момент выстрела, поскольку они используются для определения начальных значений компонентов вектора скорости. Дальше все вычисления ведутся уже с переменными vx и vy согласно разностным уравнениям. Если значения на входы блока поступают по связям с других блоков, может случиться так, что блок считает v0 и α до того, как установятся их правильные значения. Например, если значение на вход блока будет подаваться по цепочке из трех последовательно соединенных блоков, значение этого входа установится только к третьему такту расчета. Мы не можем заранее знать, сколько тактов потребуется для того, чтобы правильные значения добрались до входа блока, но мы можем оценить это число сверху. Если в во всей схеме содержится N простых блоков (именно простых, поскольку в расчете участвуют только простые блоки), длина любой цепочки из соединенных блоков в этой схеме не может быть больше N, а, значит, пропустив N тактов, мы гарантируем установку всех начальных значений на входах всех блоков схемы. Таким образом, мы могли бы отсчитать N тактов после запуска расчета, и только потом устанавливать начальные значения переменных vx и vy.

К счастью, возможность пропуска заданного числа тактов уже реализована в стандартном блоке-планировщике динамического расчета. Число пропускаемых тактов задается в настройках этого блока (рис. 98), причем вместо того, чтобы вводить число тактов вручную, можно установить флаг «авто» – в этом случае число начальных тактов будет равно числу блоков в подсистеме с учетом подсистем, вложенных в нее.

Рис. 98. Настройки планировщика для расчета траектории снаряда

Блок-планировщик изменит значение динамической переменной времени «DynTime» только после пропуска заданного числа начальных тактов, поэтому модель нашего блока должна дождаться изменения «DynTime» и только после этого считать значения v0 и α и начать расчет разностных уравнений. Для управления моментом считывания введем в блок внутреннюю логическую переменную «ValSet» с начальным значением 0. Пока это значение будет оставаться нулевым, модель не будет вести расчет. Как только «DynTime» изменится при нулевом значении «ValSet», модель считает v0 и α и присвоит «ValSet» значение 1, разрешив тем самым расчет траектории.

После встречи снаряда с землей блок должен прекратить вычисление его координат и скоростей: в любой момент времени после падения горизонтальная координата снаряда x должна быть равна дальности полета L (см. рис. 94), а вертикальная координата y и обе компоненты вектора скорости vx и vy должны быть равны нулю. Кроме того, после падения снаряда на землю блок должен выдать на выход сигнал, сообщающий другим блокам о том, что дальность полета вычислена. Назовем этот сигнальный выход «Impact», позже мы подключим к нему блок, который будет подбирать угол вылета для достижения заданной дальности. Для прекращения моделирования полета после падения снаряда будем использовать внутреннюю логическую переменную «InFlight» с нулевым начальным значением: блок будет считать траекторию снаряда только тогда, когда ее значение равно единице. В момент начала расчета траектории модель присвоит ей значение 1, а затем, как только снаряд встретится с землей, модель обнулит ее, и расчет траектории прекратится.

Таким образом, наша модель может находиться в одном из следующих трех состояний:

ValSet | InFlight | Состояние модели
--- | --- | ---
0 | 0 | Расчет сброшен, модель ждет, пока планировщик не отсчитает заданное число начальных тактов и не изменит значение времени
1 | 1 | Снаряд в полете, идет расчет траектории
1 | 0 | Снаряд встретился с землей (при переходе в это состояние выдается сигнал «Impact»)

Нужно отметить, что, хотя сигнал «Impact» и будет получать единичное значение синхронно с обнулением переменной «InFlight», для описания состояния машины он не подходит, поскольку при передаче по связям сигнальные выходы автоматически сбрасываются.

Кроме трех перечисленных переменных, управляющих логикой работы блока, и вещественных переменных для хранения координат, компонентов скорости и параметров снаряда, нам потребуется еще одна дополнительная внутренняя вещественная переменная. Для вычисления интервала времени Δt в разностных уравнениях, нам нужно запоминать значение времени предыдущего шага расчета. Течением времени в нашей схеме будет управлять стандартный блок – планировщик динамического расчета, поэтому значение текущего времени наш блок будет брать из динамической переменной «DynTime», как мы уже неоднократно делали ранее. Если мы введем в блок статическую переменную «t0» и будем каждый раз запоминать в ней текущее значение времени, то при изменении «DynTime» мы сможем вычислить величину изменения времени, то есть значение Δt, как DynTime-t0. Мы уже использовали такой механизм когда создавали модель блока, движущегося внутри окна подсистемы с заданной скоростью (см. §2.6.4).

Структура переменных блока моделирования полета снаряда будет следующей:

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение | Назначение | Номер
--- | --- | --- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0 | Стандартный сигнал запуска | 0
1 | Ready | Сигнал | 1 | Выход |  | 0 | Стандартный сигнал готовности | 1
2 | D | double | 8 | Вход |  | 0.1 | Диаметр снаряда, м | 2
10 | m | double | 8 | Вход |  | 1 | Масса снаряда, кг | 3
18 | v0 | double | 8 | Вход |  | 0 | Начальная скорость снаряда, м/с | 4
26 | Angle | double | 8 | Вход |  | 0 | Угол возвышения, градусов | 5
34 | l0 | double | 8 | Вход |  | 0 | Длина ствола (направляющей), м | 6
42 | h0 | double | 8 | Вход |  | 0 | Высота оси ствола (направляющей), м | 7
50 | ValSet | Логический | 1 | Внутренняя |  | 0 | Признак того, что «v0» и «Angle» считаны | 8
51 | x | double | 8 | Выход |  | 0 | Координата x снаряда, м | 9
59 | y | double | 8 | Выход |  | 0 | Координата y снаряда, м | 10
67 | vx | double | 8 | Выход |  | 0 | Горизонтальная скорость vx, м/с | 11
75 | vy | double | 8 | Выход |  | 0 | Вертикальная скорость vy, м/с | 12
83 | v | double | 8 | Выход |  | 0 | Модуль вектора скорости, м/с | 13
91 | Impact | Сигнал | 1 | Выход |  | 0 | Снаряд встретился с землей | 14
92 | t0 | double | 8 | Внутренняя |  | 0 | Значение времени предыдущего шага, с | 15
100 | InFlight | Логический | 1 | Выход |  | 0 | Снаряд в данный момент – в полете | 16

К выходам блока мы добавили переменную «v» – модуль вектора скорости. Для решения поставленной задачи этот выход нам не нужен, но он даст возможность, при желании, построить график зависимости скорости снаряда от времени.

Теперь можно написать модель блока:

      // Расчет внешней баллистики
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      extern "C" __declspec(dllexport)
          int RDSCALL Ballistics(int CallMode,
                                 RDS_PBLOCKDATA BlockData,
                                 LPVOID ExtParam)
      { RDS_PDYNVARLINK Link;
        double t,dt,A,n,F,Fx,Fy,xp,yp;
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define D (*((double *)(pStart+2*RDS_VSZ_S)))
      #define m (*((double *)(pStart+2*RDS_VSZ_S+RDS_VSZ_D)))
      #define v0 (*((double *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_D)))
      #define Angle (*((double *)(pStart+2*RDS_VSZ_S+3*RDS_VSZ_D)))
      #define l0 (*((double *)(pStart+2*RDS_VSZ_S+4*RDS_VSZ_D)))
      #define h0 (*((double *)(pStart+2*RDS_VSZ_S+5*RDS_VSZ_D)))
      #define ValSet (*((char *)(pStart+2*RDS_VSZ_S+6*RDS_VSZ_D)))
      #define x (*((double *)(pStart+2*RDS_VSZ_S+6*RDS_VSZ_D+RDS_VSZ_L)))
      #define y (*((double *)(pStart+2*RDS_VSZ_S+7*RDS_VSZ_D+RDS_VSZ_L)))
      #define vx (*((double *)(pStart+2*RDS_VSZ_S+8*RDS_VSZ_D+RDS_VSZ_L)))
      #define vy (*((double *)(pStart+2*RDS_VSZ_S+9*RDS_VSZ_D+RDS_VSZ_L)))
      #define v (*((double *)(pStart+2*RDS_VSZ_S+10*RDS_VSZ_D+RDS_VSZ_L)))
      #define Impact (*((char *)(pStart+2*RDS_VSZ_S+11*RDS_VSZ_D+RDS_VSZ_L)))
      #define t0 (*((double *)(pStart+3*RDS_VSZ_S+11*RDS_VSZ_D+RDS_VSZ_L)))
      #define InFlight (*((char *)(pStart+3*RDS_VSZ_S+12*RDS_VSZ_D+RDS_VSZ_L)))
        // Массив описания параметров для универсальной функции настройки
        static const char *setup[]={
          "6Длина ствола, м",
          "7Высота оси, м",
          "2Диаметр снаряда, м",
          "3Масса снаряда, кг",
          "4Начальная скорость, м/с",
          "5Угол вылета, град.",
          NULL};

        switch(CallMode)
          { // Инициализация
            case RDS_BFM_INIT:
              // Подписка на динамическую переменную "DynTime"
              Link=rdsSubscribeToDynamicVar(RDS_DVPARENT,"DynTime",
                    "D",TRUE);
              BlockData->BlockData=Link;
              break;

            // Очистка
            case RDS_BFM_CLEANUP:
              // Прекращение подписки на "DynTime"
              rdsUnsubscribeFromDynamicVar(
                  (RDS_PDYNVARLINK)BlockData->BlockData);
              break;

            // Проверка типов переменных
            case RDS_BFM_VARCHECK:
              return strcmp((char*)ExtParam,"{SSDDDDDDLDDDDDSDL}")?
                RDS_BFR_BADVARSMSG:RDS_BFR_DONE;

            // Вызов функции настройки
            case RDS_BFM_SETUP:
              return SetupDoubleVars(BlockData->Block,
                      "Внешняя баллистика",setup);

            // Изменение динамической переменной
            case RDS_BFM_DYNVARCHANGE:
              // Получаем доступ к динамической переменной времени
              Link=(RDS_PDYNVARLINK)BlockData->BlockData;
              if(Link==NULL || Link->Data==NULL)	// Нет доступа
                break;
              t=*((double*)Link->Data); // В t – текущее время
              if(t==t0) // Время не изменилось - ждем
                break;
              dt=t-t0; // В dt – интервал времени с прошлого шага
              t0=t;    // Запоминаем время в t0 чтобы при следующем
                       // изменени DynTime (t) можно было вычислить dt
              if(!ValSet) // Начальные значения еще не считаны
                { // Начинаем расчет траектории снаряда
                  double alpha=Angle*M_PI/180.0; // Угол в радианах
                  // Занесение в переменные начальных значений
                  vx=v0*cos(alpha);
                  vy=v0*sin(alpha);
                  v=v0;
                  x=l0*cos(alpha);
                  y=h0+l0*sin(alpha);
                  // Сбрасываем сигнал падения
                  Impact=0;
                  // Разрешаем расчет и взводим ValSet
                  InFlight=ValSet=1;
                }
              else if(!InFlight) // Снаряд не в полете – ничего не делаем
                break;
              // Вычисление модуля вектора скорости и силы сопротивления
              // по формуле Забудского
              v=sqrt(vx*vx+vy*vy);
              if(v<240.0)
                { A=0.0140; n=2; }
              else if(v<295.0)
                { A=0.0000583; n=3; }
              else if(v<375.0)
                { A=0.000000000670; n=5; }
              else if(v<419.0)
                { A=0.0000940; n=3; }
              else if(v<550.0)
                { A=0.0394; n=2; }
              else if(v<800.0)
                { A=0.2616; n=1.7; }
              else
                { A=0.713; n=1.55; }
              F=A*M_PI*D*D*pow(v,n)/4.0;	// Сила сопротивления
              // Горизонтальная и вертикальная компоненты F
              Fx=F*vx/v;
              Fy=F*vy/v;
              // Запоминаем текущие значения координат
              xp=x; yp=y;
              // Вычисляем новые значения координат по
              // разностным уравнениям
              x+=dt*vx;
              y+=dt*vy;
              // Вычисляем новые значения компонент вектора скорости
              // по разностным уравнениям
              vx-=dt*Fx/m;
              vy-=dt*(9.807+Fy/m);
              if(y<0.0) // Снаряд встретился с поверхностью
                { Impact=1;   // Взводим выходной сигнал падения снаряда
                  InFlight=0; // Прекращаем расчет траектории
                  // Вычисляем координату встречи с поверхностью
                  x=xp-yp*(x-xp)/(y-yp);
                  y=vx=vy=0;
                }
              Ready=1; // Для передачи выходов по связям
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений переменных
      #undef InFlight
      #undef t0
      #undef Impact
      #undef v
      #undef vy
      #undef vx
      #undef y
      #undef x
      #undef ValSet
      #undef h0
      #undef l0
      #undef Angle
      #undef v0
      #undef m
      #undef D
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

В самом начале функции модели, после описаний локальных переменных и макроопределений, описывается статический массив строк setup, который будет использоваться для вызова написанной нами ранее универсальной функции настройки SetupDoubleVars. Массив описан как статический, чтобы он не занимал место в стеке функции – его содержимое будет общим для всех блоков с моделью Ballistics и не будет изменяться в процессе их работы. Каждая строка массива содержит номер переменной в символьном представлении и заголовок соответствующего ей поля ввода, которое будет добавлено в окно настройки. Всего в этом блоке у нас шесть параметров, которые пользователь сможет ввести в окне настройки, либо, при необходимости, подать с других блоков через связи.

При инициализации блока (вызов с параметром RDS_BFM_CLEANUP) модель отменяет подписку, передавая запомненный указатель в функцию rdsUnsubscribeFromDynamicVar. Эти функции уже не один раз встречались в описываемых примерах. В реакции модели на проверку типа переменных (RDS_BFM_VARCHECK) тоже нет ничего нового: переданная в параметре ExtParam строка сравнивается со строкой, соответствующей нужной нам последовательности переменных, и, в зависимости от результата сравнения, возвращается константа RDS_BFR_BADVARSMSG или RDS_BFR_DONE.

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

Расчет траектории полета снаряда производится в реакции на изменение динамической переменной RDS_BFM_DYNVARCHANGE, реакция на такт моделирования RDS_BFM_MODEL в этой модели отсутствует: все действия выполняются только при изменении времени. Блок подписан на единственную динамическую переменную – время «DynTime», поэтому вызов RDS_BFM_DYNVARCHANGE означает, что время в системе изменилось. Прежде всего модель проверяет, есть ли у нее доступ к переменной времени. Если его нет, функция модели немедленно завершается, если есть – значение времени записывается во вспомогательную переменную t. Для расчета координат и составляющих вектора скорости снаряда по разностным уравнениям необходимо знать интервал времени Δt, прошедший с последнего вычисления этих значений. Если значение t будет равно значению t0, то есть значению времени на момент последнего вычисления, значит, время в системе с прошлого раза не изменилось, и ничего вычислять не нужно – модель немедленно завершается. В противном случае из текущего значения времени t вычитается значение времени при прошлом вычислении t0 и полученная разность присваивается переменной dt – это и есть интересующий нас интервал времени. Затем переменной t0 присваивается текущее время, чтобы в следующий раз при изменении времени можно было снова определить интервал Δt.

После того, как интервал dt вычислен, и переменной t0 присвоено новое значение времени, модель проверяет, считаны ли уже значения начальной скорости снаряда и угла возвышения метательной машины. Если значение логической переменной ValSet нулевое, эти значения нужно считать сейчас: мы уже знаем, что системное время изменилось, значит, планировщик уже выполнил все начальные такты, заданные в его настройках. В этом случае угол Angle переводится в радианы и записывается во вспомогательную переменную alpha, вычисляются начальные значения горизонтальной и вертикальной составляющих скорости снаряда vx и vy и начальные координаты снаряда x и y (они соответствуют координатам конца ствола метательной машины при данном угле возвышения). После этого сбрасывается сигнал падения снаряда Impact, и взводятся логические переменные ValSet (мы уже считали v0 и Angle и больше ничего не ждем) и InFlight (снаряд теперь в полете, и мы начинаем расчет его траектории).

Если значение ValSet было ненулевым, мы проверяем InFlight: если ее значение нулевое, значит, снаряд уже закончил свой полет, и модель немедленно завершается.

После того, как модель убедилась, что снаряд находится в полете, вычисляется модуль вектора его скорости v – он нужен для определения силы сопротивления воздуха. В зависимости от скорости по приведенной выше таблице определяются параметры A и n, используемые в формуле Забудского, после чего вычисляется модуль силы сопротивления F, который затем раскладывается на горизонтальную и вертикальную составляющие Fx и Fy. По приведенным выше разностным уравнениям вычисляются новые значения координат снаряда x и y и составляющих его скорости vx и vy, причем предыдущие значения координат запоминаются в переменных xp и yp – они понадобятся для вычисления координат точки встречи снаряда с поверхностью, если она произошла.

Если очередное вычисленное значение координаты y стало отрицательным, значит, траектория снаряда пересеклась с поверхностью земли. В этом случае выходному сигналу Impact присваивается единица, а логической переменной InFlight – ноль, что прекратит дальнейший расчет траектории. Затем, согласно рис. 97, вычисляется горизонтальная координата встречи снаряда с поверхностью, которая присваивается переменной x. Переменной y присваивается значение 0 – снаряд лежит на земле. Поскольку, начиная с этого момента, дальнейший расчет траектории вестись не будет, значения x и y больше не изменятся, и значение x на момент выдачи блоком сигнала Impact можно считать дальностью полета снаряда.

В самом конце реакции на вызов RDS_BFM_DYNVARCHANGE переменной Ready присваивается значение 1, чтобы выходы, вычисленные в этом вызове, передались по связям. На этом работа модели блока завершается.

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

Рис. 99. Схема для проверки блока расчета траектории снаряда

Рис. 100. Окно настройки блока расчета
траектории снаряда

Все параметры блока, кроме начальной скорости снаряда «v0» и угла возвышения «Angle», заданы через функцию настройки (рис. 100). К выходам «x» и «y» нашего блока присоединен стандартный график для построения фазового портрета, то есть параметрической функции (x(t), y(t)) – он будет отображать траекторию полета. Выход «Impact» нашего блока соединен с входом «Stop» блока запуска и остановки расчета, созданного нами в §2.14.1. Как только снаряд достигнет поверхности и наш блок выдаст сигнал «Impact», этот блок остановит расчет.

Для правильной работы схемы необходимо настроить блок-планировщик согласно рис. 98. Во-первых, необходимо отключить завершение расчета по времени – для остановки расчета после падения снаряда мы подключили специальный блок. Во-вторых, нужно выбрать шаг расчета. Самые быстро изменяющиеся величины в модели полета снаряда – это координаты «x» и «y», максимально возможная скорость снаряда, для которой мы еще можем считать сопротивление воздуха, равна 1000 м/с, поэтому для определения дальности с точностью около одного метра можно выбрать шаг порядка одной тысячной секунды. И, в-третьих, как уже было указано выше, следует задать некоторое количество начальных тактов или, лучше всего, установить флаг «авто». Так мы будем уверены, что на момент первого изменения времени планировщиком значения «v0» и «Angle» уже успеют установиться.

Результат моделирования полета снаряда с указанными в настройках параметрами при начальной скорости 700 м/с и угле возвышения 45° изображен на рис. 99. Видно, что из-за влияния сопротивления воздуха траектория снаряда отличается от параболы.

Теперь мы перейдем к главной задаче: сделаем блок, который подберет такой угол возвышения метательной машины, чтобы дальность полета снаряда была как можно ближе к заданной. Это достаточно обычная оптимизационная задача, для решения которой существует множество способов. Будем считать, что у метательной машины есть допустимый диапазон углов возвышения (например, от 0° до 90°), в пределах которого мы можем установить любой угол, но только с заданной точностью. Например, если точность установки угла будет равной одной десятой градуса, мы сможем установить углы в 30.1°, 30.2°, 30.3°, но не сможем установить угол в 30.15° – у нас просто не хватит точности исполнительного механизма, или цены деления шкалы, если угол устанавливается вручную. Будем решать нашу задачу следующим образом: установим максимально возможный угол возвышения, выполним расчет и, дождавшись падения снаряда на землю (получив от блока расчета траектории сигнал «Impact»), определим величину промаха, то есть разность между заданной и получившейся дальностью для этого угла. Выберем какой-либо шаг, с которым мы будем изменять угол – например, одну пятую всего диапазона. Сбросим в исходное состояние подсистему, в которой будет находиться наш блок вместе с блоком расчета траектории (моделью Ballistics), и уменьшим угол на выбранную величину шага (мы начали с максимально возможного угла возвышения, поэтому мы можем только уменьшать его). Поскольку расчет мы не останавливали, полет снаряда будет промоделирован снова, но уже с другим углом возвышения. Снова дождемся падения снаряда, и определим новую величину промаха. Если она стала меньше, будем уменьшать угол с этим шагом, каждый раз сбрасывая расчет и повторяя его заново до тех пор, пока промах будет уменьшаться. Как только промах начнет увеличиваться, вернемся к предыдущему значению угла, уменьшим шаг по углу вдвое и снова попробуем уменьшить угол, но уже с новым шагом. Если промах уменьшился, продолжим уменьшать угол, если же он увеличился – попробуем, наоборот, увеличивать его. Если и увеличение, и уменьшение угла с данным шагом приводит к увеличению промаха, еще раз уменьшим шаг по углу вдвое и снова попытаемся уменьшать или увеличивать угол. Так мы будем действовать до тех пор, пока шаг, с которым мы изменяем угол, не станет меньше точности установки угла, определяемой конструкцией механизма наведения нашей машины. Тогда последнее значение угла, при котором промах был наименьшим, и будет искомым углом возвышения для заданной дальности. Описанный метод поиска является упрощением стандартного метода покоординатного спуска для случая единственной координаты – угла возвышения.

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

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

      //==============================================
      // Поиск угла возвышения для заданной дальности
      //==============================================
      // Личная область данных блока
      class TArtSearchData
      { public:
          BOOL SelfReset;   // Блок сам сбросил подсистему
          int Mode;         // Текущее состояние алгоритма:
            #define ASMODE_READY      0 // Готов к поиску
            #define ASMODE_SEARCHING  1 // Идет поиск
            #define ASMODE_FINALRUN   2 // Последний прогон
            #define ASMODE_FINISHED   3 // Поиск завершен
          double AngleStep; // Текущий шаг изменения угла
          double AngleToSet;// Угол, который нужно установить
                            // после сброса
          double OptAngle;  // Наилучший на данный момент угол
          double OptMiss;   // Наименьший на данный момент промах
          BOOL FirstStep;   // Проведенное моделирование – первое с
                            // новым шагом изменения угла

          // Ограничение диапазона и точности установки угла
          double FixAngle(double a,double amin,double amax,double acc)
            { if(a<amin) return amin;
              if(a>amax) return amax;
              return floor((a-amin)/acc)*acc+amin;
            };

          // Вывод сообщения о результатах поиска
          // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
          //   в противном случае необходимо использовать версии функций
          //   с суффиксом "W" и символьные константы с префиксом "L"
          void ShowResults(void)
            { char *str,
                   *angle=rdsDtoA(OptAngle,-1,NULL),
                   *miss=rdsDtoA(OptMiss,0,NULL);
              // Формирование динамической строки с сообщением
              str=rdsDynStrCat("Угол возвышения: ",angle,FALSE);
              rdsAddToDynStr(&str," гр.\nПромах: ",FALSE);
              rdsAddToDynStr(&str,miss,FALSE);
              rdsAddToDynStr(&str," м",FALSE);
              // Вывод текста
              rdsMessageBox(str,"Поиск завершен",MB_OK);
              // Освобождение всех динамических строк
              rdsFree(str);
              rdsFree(angle);
              rdsFree(miss);
            };

          // Конструктор класса
          TArtSearchData(void)
            { SelfReset=FALSE;
              Mode=ASMODE_READY;
            };
      };
      //=========================================

Наш блок должен по-разному реагировать на сброс и на запуск расчета в зависимости от того, чем он в данный момент занят. Он должен отличать сброс расчета, выполненный пользователем, от сброса состояния подсистемы, выполненного самим блоком: в первом случае нужно прекратить поиск и вернуть все параметры в исходное состояние, во втором – продолжить поиск с новым значением угла. Для того, чтобы различать эти два случая, мы ввели в класс личной области данных блока логическое поле SelfReset с исходным значением FALSE (исходное значение задается в конструкторе класса). Перед программным сбросом модель будет присваивать этому полю значение TRUE, а в конце реакции на сброс расчета (RDS_BFM_RESETCALC) снова сбрасывать его в FALSE. Таким образом, если расчет сброшен пользователем, при вызове модели в режиме RDS_BFM_RESETCALC поле SelfReset будет иметь значение FALSE, а если расчет сброшен моделью блока – TRUE.

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

- ASMODE_READY – поиск угла еще не проводился, блок в исходном состоянии (это значение присваивается полю Mode в конструкторе класса);
- ASMODE_SEARCHING – идет поиск угла;
- ASMODE_FINALRUN – идет последний расчет с найденным значением угла возвышения;
- ASMODE_FINISHED – поиск завершен.

При самом первом запуске расчета или после сброса расчета пользователем блок будет находиться в состоянии ASMODE_READY. Как только расчет будет запущен, блок перейдет в состояние ASMODE_SEARCHING, и будет находиться в нем до тех пор, пока алгоритм не закончит работу. Затем блок перейдет в состояние ASMODE_FINALRUN и будет проведен последний расчет для найденного значения угла. После завершения расчета состояние блока изменится на ASMODE_FINISHED, и расчет остановится. В зависимости от значения переменной Mode блок будет по-разному реагировать на запуск расчета и на получение значения дальности полета снаряда от блока расчета траектории (то есть на конец очередного расчета дальности).

Для того чтобы в процессе поиска после очередного программного сброса состояния подсистемы установить новое значение угла, в личной области данных нам потребуется поле для хранения этого значения: значение, которое нужно установить, мы сможем вычислить только в конце очередного расчета дальности, а установить его можно только после сброса, иначе установленное значение сбросится вместе со всеми остальными переменными. С этой целью в класс введено вещественное поле AngleToSet: в конце расчета мы запишем в него новое значение угла, а в реакции модели на сброс расчета скопируем значение из этого поля в соответствующий выход блока, который будет подключен к входу Angle блока расчета траектории. Три оставшихся вещественных поля класса хранят текущие параметры алгоритма: шаг по углу, используемый в данный момент (AngleStep), наилучший на данный момент угол возвышения (OptAngle) и промах при этом значении угла (OptMiss).

Последнее логическое поле класса, FirstStep, будет использоваться для переключения направления изменения угла. При каждом уменьшении шага по углу мы будем присваивать этому полю значение TRUE, а после любого изменения угла – FALSE. Согласно алгоритму поиска, после очередного уменьшения величины шага мы сначала должны попытаться уменьшить угол с этим шагом, а если промах при этом возрастет, попытаться увеличить угол. Если при возрастании промаха поле FirstStep имело значение TRUE, значит, это была первая попытка изменить угол с новым шагом, и нужно изменить знак шага, то есть перейти от уменьшения угла к увеличению с тем же шагом. Если же поле имело значение FALSE, значит, либо это не первый шаг по углу в этом направлении (в этом случае двигаться обратно с тем же шагом бессмысленно – мы там уже побывали), либо мы только что изменили знак шага, не меняя его величину, то есть начали изменять угол в другом направлении, поскольку старое направление изменения угла приводило к возрастанию промаха. В обоих случаях нужно будет уменьшать шаг и снова пытаться изменить угол, но уже с новым шагом. Таким образом, если при очередном расчете промах увеличится и FirstStep будет иметь значение FALSE, нужно уменьшить шаг по углу вдвое.

На самом деле, не важно, будем ли мы после уменьшения шага сначала пытаться уменьшать угол, а потом увеличивать, или, наоборот, сначала увеличивать, а потом уменьшать. Поэтому мы не будем следить за знаком шага при его уменьшении – будем оставлять его прежним, а если промах возрастет, менять знак шага на противоположный.

Кроме полей, необходимых для реализации алгоритма поиска, в классе личной области данных блока описаны две вспомогательные функции. Первая из них, FixAngle, корректирует переданное ей значение угла a таким образом, чтобы оно, во-первых, оставалось в заданном диапазоне amin…amax, и, во-вторых, соответствовало заданной точности acc. Изменяя угол в процессе поиска, мы можем выйти из допустимого диапазона углов возвышения, а уменьшая шаг по углу – получить значение угла, которое не может быть установлено механизмом нашей метательной машины. Допустим, метательная машина позволяет установить угол возвышения с точностью до 0.1°, текущее значение угла – 45°, а текущий шаг по углу – 0.5°. Если мы, согласно алгоритму, уменьшим шаг вдвое и попытаемся уменьшить угол возвышения на этот шаг, мы получим 45°−0.25°=44.75°. Такой угол не может быть установлен с точностью 0.1°, и нам придется выбрать ближайшее допустимое значение, то есть 44.8°. Функция FixAngle округляет значение угла до заданной точности, возвращая для переданного ей угла a ближайшее значение вида amin+N×acc, где N – целое число.

Вторая функция класса, ShowResults, служит для демонстрации результатов поиска пользователю. В ней значения найденного угла OptAngle и соответствующего ему промаха OptMiss преобразуются в динамически отводимые строки при помощи уже использовавшейся ранее сервисной функции RDS rdsDtoA. Затем при помощи функций rdsDynStrCat и rdsAddToDynStr из этих строк формируется сообщение пользователю, которое выводится функцией rdsMessageBox. В конце функции все динамически созданные строки уничтожаются при помощи rdsFree.

Для того, чтобы блок поиска угла можно было присоединить к блоку расчета траектории, и чтобы было где хранить настроечные параметры поиска, потребуется следующая структура переменных:

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение | Назначение | Номер
--- | --- | --- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0 | Стандартный сигнал запуска | 0
1 | Ready | Сигнал | 1 | Выход |  | 0 | Стандартный сигнал готовности | 1
2 | MinAngle | double | 8 | Внутренняя |  | 0 | Минимально возможный угол возвышения, градусов | 2
10 | MaxAngle | double | 8 | Внутренняя |  | 90 | Максимально возможный угол возвышения, градусов | 3
18 | Accuracy | double | 8 | Внутренняя |  | 0.1 | Точность установки угла, градусов | 4
26 | Distance | double | 8 | Внутренняя |  | 100 | Дальность полета, для которой ищется угол возвышения, м | 5
34 | x | double | 8 | Вход |  | 0 | Текущая горизонтальная координата снаряда, м (от блока расчета траектории) | 6
42 | Impact | Сигнал | 1 | Вход | ✓ | 0 | Сигнал о падении снаряда (от блока расчета траектории) | 7
43 | Angle | double | 8 | Выход |  | 0 | Текущий угол возвышения, градусов (к блоку расчета траектории) | 8

Значения переменных MinAngle, MaxAngle, Accuracy и Distance будут задаваться пользователем в функции настройки блока – это параметры алгоритма поиска. Входы x и Impact нужно будет соединить с одноименными выходами блока расчета траектории снаряда: когда блок расчета определит точку падения снаряда и выдаст сигнал Impact, с входа x можно будет считать дальность полета снаряда. Выход Angle будет соединяться с одноименным входом блока расчета траектории, через него блок поиска угла передаст текущий угол возвышения, для которого нужно определить дальность.

Модель блока поиска угла будет такой:

      // Модель блока поиска угла для заданной дальности
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      extern "C" __declspec(dllexport)
          int RDSCALL ArtSearch(int CallMode,
                                RDS_PBLOCKDATA BlockData,
                                LPVOID ExtParam)
      { TArtSearchData *data=(TArtSearchData*)(BlockData->BlockData);
        double delta;
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define MinAngle (*((double *)(pStart+2*RDS_VSZ_S)))
      #define MaxAngle (*((double *)(pStart+2*RDS_VSZ_S+RDS_VSZ_D)))
      #define Accuracy (*((double *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_D)))
      #define Distance (*((double *)(pStart+2*RDS_VSZ_S+3*RDS_VSZ_D)))
      #define x (*((double *)(pStart+2*RDS_VSZ_S+4*RDS_VSZ_D)))
      #define Impact (*((char *)(pStart+2*RDS_VSZ_S+5*RDS_VSZ_D)))
      #define Angle (*((double *)(pStart+3*RDS_VSZ_S+5*RDS_VSZ_D)))
        // Массив описания параметров для универсальной функции настройки
        static const char *setup[]={
          "2Минимальный угол, град.",
          "3Максимальный угол, град.",
          "4Точность по углу, град.",
          "5Дальность цели, м",
          NULL};

        switch(CallMode)
          { // Инициализация
            case RDS_BFM_INIT:
              BlockData->BlockData=new TArtSearchData();
              break;

            // Очистка
            case RDS_BFM_CLEANUP:
              delete data;
              break;

            // Проверка типов переменных
            case RDS_BFM_VARCHECK:
              return strcmp((char*)ExtParam,"{SSDDDDDSD}")?
                RDS_BFR_BADVARSMSG:RDS_BFR_DONE;

            // Вызов функции настройки
            case RDS_BFM_SETUP:
              return SetupDoubleVars(BlockData->Block,
                  "Поиск угла",setup);

            // Запуск расчета
            case RDS_BFM_STARTCALC:
              switch(data->Mode)
                { case ASMODE_READY:
                    // Начать поиск угла
                    Angle=MaxAngle;	// Начальное значение
                    data->OptMiss=DoubleErrorValue; // Пока не определено
                    data->AngleStep=-(MaxAngle-MinAngle)/5;
                    data->FirstStep=TRUE;
                    data->Mode=ASMODE_SEARCHING;
                    Ready=1; // Для передачи угла по связи
                    break;
                  case ASMODE_FINISHED:
                    // Поиск уже проведен – показать результаты
                    rdsStopCalc();
                    data->ShowResults();
                    break;
                }
              break;

            // Сброс расчета
            case RDS_BFM_RESETCALC:
              if(data->SelfReset)
                { // Блок сам сбросил подсистему
                  data->SelfReset=FALSE; // Очиска признака самосброса
                  // Устанавливаем новый угол
                  Angle=data->AngleToSet;
                  Ready=1; // Для передачи угла по связи
                }
              else // Расчет сброшен пользователем
                data->Mode=ASMODE_READY;
              break;

            // Один такт моделирования
            case RDS_BFM_MODEL:
              if(!Impact) // Снаряд еще не долетел
                break;

              // Снаряд долетел – дальность полета в x
              Impact=0; // Сбрасываем входной сигнал
              if(data->Mode==ASMODE_FINALRUN)
                { // Это был последний (демонстрационный) прогон
                  data->Mode=ASMODE_FINISHED;
                  rdsStopCalc();
                  data->ShowResults();
                  break;
                }
              // Это – очередной прогон в поиске угла
              delta=fabs(x-Distance); // Промах при угле Angle
              if(data->OptMiss==DoubleErrorValue || // Первый прогон
                 delta<data->OptMiss) // При новом угле попали точнее
                { // Запоминаем новый наилучший угол и промах
                  data->OptAngle=Angle;
                  data->OptMiss=delta;
                  // Двигаемся дальше с тем же шагом
                  data->AngleToSet=Angle+data->AngleStep;
                  // Новый расчет будет уже не первым с этим шагом
                  data->FirstStep=FALSE;
                }
              else // Промах увеличился или не изменился
                { if(data->FirstStep)
                    { // Мы только что провели первый расчет с новым
                      // значением шага. Промах увеличился – попробуем
                      // двигаться в обратном направлении с тем же
                      // шагом
                      data->AngleStep=-data->AngleStep;
                      // Новый расчет будет уже не первым с этим шагом
                      data->FirstStep=FALSE;
                    }
                  else
                    { // Это был не первый расчет с данным значением
                      // шага. Мы либо сделали несколько шагов в этом
                      // направлении, либо уже пробовали двигаться
                      // в обратном. Теперь нужно уменьшить шаг вдвое,
                      //  если это возможно.
                      if(fabs(data->AngleStep)<=Accuracy)
                        { // Достигли минимального шага по углу -
                          // выполняем последний расчет
                          data->Mode=ASMODE_FINALRUN;
                          // Устанавливаем найденный угол
                          data->AngleToSet=data->OptAngle;
                          data->SelfReset=TRUE; // Флаг самосброса
                          // Сбрасываем родительскую подсистему
                          rdsResetSystemState(BlockData->Parent);
                          break;
                        }
                       // Минимальный шаг еще не достигнут
                       // Дальше будем двигаться от наилучшего на данный
                       // момент угла с меньшим шагом
                       data->AngleStep=data->AngleStep/2.0;
                       // Не даем шагу стать меньше минимального
                       if(fabs(data->AngleStep)<Accuracy)
                         data->AngleStep=(data->AngleStep<0)?
                                              (-Accuracy):Accuracy;
                       // Новый расчет будет первым с этим значением шага
                       data->FirstStep=TRUE;
                    }
                  // Новый угол, который установиться после сброса
                  data->AngleToSet=data->OptAngle+data->AngleStep;
                }
              // Ограничиваем угол и привязываем его к точности
              data->AngleToSet=data->FixAngle(
                  data->AngleToSet,MinAngle,MaxAngle,Accuracy);
              // Сбрасываем родительскую подсистему
              data->SelfReset=TRUE; // Флаг самосброса
              rdsResetSystemState(BlockData->Parent);
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef Angle
      #undef Impact
      #undef x
      #undef Distance
      #undef Accuracy
      #undef MaxAngle
      #undef MinAngle
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

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

Реакция модели на запуск расчета (RDS_BFM_STARTCALC) зависит от того, в каком режиме находится блок. Если поиск угла еще не проводился, то есть поле Mode класса личной области данных имеет значение ASMODE_READY, необходимо установить начальное значение угла, с которого мы начнем поиск (выбираем в качестве начального максимально возможный угол возвышения, то есть MaxAngle) и начальный шаг изменения угла (выбираем одну пятую всего диапазона, при этом мы будем уменьшать угол от максимального, поэтому шаг делается отрицательным). Полю OptMiss, в котором должно храниться наименьшее на данный момент значение промаха, мы присваиваем специальное значение – индикатор ошибки из глобальной переменной DoubleErrorValue, поскольку мы еще не провели ни одного расчета, и у нас пока нет ни наилучшего на данный момент значения угла, ни минимального значения промаха. Полю FirstStep присваивается значение TRUE (это самый первый расчет с таким значением шага), и блок переводится в режим ASMODE_SEARCHING: алгоритм поиска угла начал свою работу. Затем взводится сигнал Ready, чтобы установленное значение угла Angle передалось по связям, и реакция на этом завершается.

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

Следует отметить, что реакция на запуск расчета будет вызываться чаще, чем может показаться на первый взгляд. Дело в том, что наша модель будет время от времени сбрасывать состояние подсистемы при работающем расчете. RDS технически не может выполнить сброс, пока расчет работает, поэтому при вызове сервисной функции rdsResetSystemState, которой мы пользуемся для сброса, расчет сначала останавливается, затем состояние подсистемы сбрасывается, и расчет запускается снова. В результате модель нашего блока, как и модели всех остальных блоков схемы, будет вызываться в режимах RDS_BFM_STOPCALC и RDS_BFM_STARTCALC не только при остановке и запуске расчета пользователем, но и при каждом сбросе состояния подсистемы. Тем не менее, на работу нашей модели этот факт никак не повлияет.

При сбросе расчета (режим RDS_BFM_RESETCALC) модель прежде всего проверяет, сам ли блок сбросил состояние подсистемы, или это сделал кто-то другой, то есть пользователь или какой-то другой блок. Для этого проверяется значение поля SelfReset: мы договорились, что перед сбросом состояния подсистемы модель будет присваивать этому полю значение TRUE. Таким образом, если значение SelfReset истинно, то блок сам сбросил расчет в процессе поиска угла, и модель устанавливает на выходе блока Angle новое значение из поля AngleToSet. Кроме этого, SelfReset снова сбрасывается в FALSE до следующего программного сброса, и взводится сигнал Ready для передачи выхода Angle по связям. Если же поле SelfReset имело значение FALSE, блок переводится в состояние готовности к поиску ASMODE_READY. Таким образом, если пользователь сбросит, а затем запустит расчет, поиск начнется заново.

Основная работа алгоритма поиска выполняется в реакции на такт расчета RDS_BFM_MODEL. Прежде всего проверяется значение сигнального входа Impact, на котором должна появиться единица, как только снаряд упадет на землю. Если Impact имеет нулевое значение, модель немедленно завершается: снаряд еще в полете, и дальность, соответствующая текущему углу возвышения, пока неизвестна. Как только сигнал Impact станет равным 1, модель сбрасывает его (любая модель должна самостоятельно сбрасывать все свои входные сигналы), а дальше действует в зависимости от текущего режима работы блока. При запущенном расчете блок может находиться в одном из двух состояний: ASMODE_SEARCHING в процессе поиска угла и ASMODE_FINALRUN при последнем, «демонстрационном», расчете с найденным углом возвышения, который выполняется только для того, чтобы показать пользователю графики и значения, соответствующие именно этому, наилучшему, значению угла. Если поле Mode имеет значение ASMODE_FINALRUN, значит, только что был проведен последний расчет. При этом блок переводится в состояние ASMODE_FINISHED (поиск окончен), расчет останавливается, и вызывается функция ShowResults для демонстрации результатов пользователю. В противном случае поиск еще не завершен – нужно вычислять новое значение промаха, соответствующее текущему углу возвышения Angle, и сравнивать его с запомненным.

Значение промаха delta вычисляется как модуль разности текущей дальности полета, которая считывается с входа x, и заданной в настройках блока дальности Distance. Этот промах нужно сравнить с наименьшим на данный момент промахом OptMiss. После самого первого расчета, когда у нас еще нет наименьшего запомненного промаха, в OptMiss будет находится значение-индикатор ошибки DoubleErrorValue. В этом случае наилучшим на данный момент нужно считать текущее значение угла, поскольку никакого другого у нас еще нет. Текущее значение угла нужно считать наилучшим и в том случае, если с углом Angle промах получился меньше запомненного, то есть delta<data->OptMiss. Таким образом, в обоих этих случаях полю OptAngle присваивается текущее значение угла Angle, а полю OptMiss – текущий промах delta: текущее значение угла теперь будет наилучшим на данный момент. Раз изменение угла с текущим шагом улучшило ситуацию, мы продолжаем двигаться в том же направлении с тем же шагом: полю AngleToSet, откуда будет взято новое значение угла после программного сброса, присваивается сумма текущего значения Angle и текущего шага AngleStep. Затем сбрасывается логическое поле FirstStep: это будет уже не первый шаг в данном направлении.

Если же значение промаха delta получилось большим OptMiss, то есть сделанный шаг по углу привел к ухудшению ситуации, нам нужно либо изменить знак шага (если мы увеличивали угол – начать уменьшать его, если уменьшали – начать увеличивать), либо, если мы уже попробовали оба знака, уменьшить шаг вдвое. Если значение поля FirstStep истинно, значит, мы еще не пробовали менять знак шага – в этом случае знак AngleStep меняется на противоположный, а FirstStep сбрасывается, поскольку это будет уже не первая попытка изменить угол с этим абсолютным значением шага. В противном случае нужно уменьшать шаг, но сначала нужно проверить, не достигли ли мы уже заданной точности по углу.

Если текущий шаг AngleStep по модулю меньше или равен точности Accuracy, значит, уменьшать его дальше невозможно. В этом случае блок переводится в режим ASMODE_FINALRUN, устанавливается наилучшее на данный момент значение угла возвышения, и проводится последний расчет, чтобы показать пользователю правильные графики. Для этого полю SelfReset присваивается значение TRUE (сейчас блок будет сам сбрасывать расчет) и вызывается функция rdsResetSystemState для родительской подсистемы данного блока, то есть для подсистемы, в которой он находится вместе с блоком расчета траектории. Затем функция модели завершается.

Если заданная точность еще не достигнута, шаг уменьшается в два раза и ограничивается снизу значением точности по углу Accuracy (мы не можем устанавливать угол точнее). Затем полю FirstStep присваивается значение TRUE, поскольку это будет первый расчет с новым значением шага – если новый шаг ухудшит ситуацию, нам нужно будет менять знак шага, а не его абсолютное значение.

Наконец, когда все ситуации, возникающие при увеличении промаха, рассмотрены, в поле AngleToSet записывается новое значение угла, которое нужно будет установить после сброса, то есть наилучший на данный момент угол OptAngle, к которому прибавлено новое значение шага AngleStep (оно либо поменяло знак, либо уменьшилось вдвое).

В самом конце реакции на такт расчета новое значение угла возвышения AngleToSet «пропускается» через функцию FixAngle, которая ограничит его диапазоном MinAngle…MaxAngle и округлит до точности Accuracy. Затем родительская подсистема сбрасывается в начальное состояние: реагируя на сброс, как было описано выше, модель нашего блока подаст на выход Angle новое значение AngleToSet, и расчет повторится снова.

Для тестирования созданной модели ArtSearch нужно поместить в одну подсистему блок с этой моделью и блок, моделирующий полет снаряда, и собрать схему, подобную изображенной на рис. 101.

Рис. 101. Схема поиска угла возвышения для заданной дальности

Рис. 102. Настройки блока поиска угла

В этой схеме выход «Angle» блока поиска угла подается на одноименный вход блока расчета траектории не напрямую, а через поле ввода – так удобнее наблюдать за изменяющимся в процессе поиска значением угла. Перед запуском расчета нужно настроить параметры блока поиска (рис. 102) и блока расчета траектории (в данном случае начальная скорость снаряда установлена в 200 м/с, а все остальные параметры блока соответствуют рис. 100). В процессе расчета график траектории будет постоянно изменяться, пока, в конце концов, блок поиска не проведет последний расчет в режиме ASMODE_FINALRUN и не выведет сообщение об окончании поиска.

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

В рассмотренном примере очень важно расположить блок поиска угла в одной подсистеме со всеми блоками, ответственными за расчет (в нашем случае это блок моделирования полета снаряда и планировщик вычислений). Это связано с тем, что блок поиска угла сбрасывает свою родительскую подсистему со всеми подсистемами, вложенными в нее, поэтому, чтобы можно было сбрасывать расчет траектории и начинать его заново для другого значения угла возвышения, все блоки, участвующие в этом расчете, должны находиться в пределах действия этого сброса. Если мы вставим схему на рис. 101 в подсистему внутри другой подсистемы, «внешняя» подсистема в процессе работы алгоритма сбрасываться не будет. Этим можно воспользоваться, например, для построения графика зависимости дальности полета снаряда от угла возвышения. Для построения такого графика нужно создать блок, который будет изменять угол с заданным шагом, дожидаться конца расчета траектории и сбрасывать расчет. Этот блок нужно поместить в подсистему вместе с блоком расчета траектории, а сам график вместе с обслуживающими его блоками разместить снаружи этой подсистемы. При этом при сбросе внутренней подсистемы график сбрасываться не будет, и сможет, точка за точкой, построить кривую зависимости дальности от угла.

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

Блок поиска угла, рассмотренный ранее, получал от блока расчета траектории сигнал «Impact» вместе с новым значением дальности, вычислял промах и тут же сбрасывал подсистему в начальное состояние, начиная новый расчет. В новом блоке, который будет менять угол для построения графика, сбрасывать подсистему сразу же при получении сигнала «Impact» нельзя. Дело в том, что значение дальности полета здесь будет передаваться наружу подсистемы, в график, поэтому расчет можно сбрасывать только тогда, когда это значение будет принято и обработано блоком графика. Мы не знаем заранее, когда это случится: между подсистемой и графиком может оказаться длинная цепочка из блоков, которые как-то обрабатывают это значение. Фактически, нам нужно задержать сигнал «Impact», сформированный блоком расчета траектории, на некоторое, заранее неизвестное, число тактов. Если этого не сделать, подсистема вместе со всеми своими переменными, включая вычисленную дальность полета, может сброситься раньше, чем график успеет считать со своего входа очередное значение дальности. Конечно, можно подсчитать число простых блоков во всей схеме, и задержать сброс подсистемы на такое же число тактов, тогда график точно успеет сработать. Однако, есть более простое решение – можно заставить блок изменения угла ждать не только окончания расчета траектории, но и готовности графика и остальных блоков, находящихся снаружи подсистемы. Для этого нужно предусмотреть в блоке два сигнальных входа. На один из них будет подаваться сигнал окончания расчета траектории «Impact», а на другой – сигнал готовности графика снаружи подсистемы. Только когда оба этих сигнала станут равными единице, блок сбросит подсистему. Формирование этого сигнала готовности снаружи подсистемы – отдельный вопрос, и мы займемся им, когда будем создавать схему. А пока можно заняться моделью блока изменения угла, который будет иметь следующую структуру переменных:

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение | Назначение | Номер
--- | --- | --- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0 | Стандартный сигнал запуска | 0
1 | Ready | Сигнал | 1 | Выход |  | 0 | Стандартный сигнал готовности | 1
2 | MinAngle | double | 8 | Внутренняя |  | 0 | Минимально возможный угол возвышения, градусов | 2
10 | MaxAngle | double | 8 | Внутренняя |  | 90 | Максимально возможный угол возвышения, градусов | 3
18 | Accuracy | double | 8 | Внутренняя |  | 0.1 | Шаг изменения угла, градусов | 4
26 | Impact | Сигнал | 1 | Вход | ✓ | 0 | Сигнал о падении снаряда (от блока расчета траектории) | 5
27 | GraphReady | Сигнал | 1 | Вход | ✓ | 0 | Сигнал готовности графика и других блоков снаружи подсистемы | 6
28 | Angle | double | 8 | Выход |  | 0 | Текущий угол возвышения, градусов (к блоку расчета траектории) | 7

Сама модель будет такой:

      //============================================
      // Блок перебора углов для построения графика
      // зависимости дальности от угла
      //============================================
      // Личная область данных блока
      class TArtWalkData
      { public:
          BOOL SelfReset;    // Блок сам сбросил подсистему
          double AngleToSet; // Угол, который нужно установить
                             // после сброса
          BOOL FirstStart;   // Начать построение графика с начала

          // Конструктор класса
          TArtWalkData(void)
            { SelfReset=FALSE; FirstStart=TRUE; };
      };
      //=========================================
      // Функция модели блока
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      extern "C" __declspec(dllexport)
          int RDSCALL ArtWalk(int CallMode,
                              RDS_PBLOCKDATA BlockData,
                              LPVOID ExtParam)
      { TArtWalkData *data=(TArtWalkData*)(BlockData->BlockData);
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define MinAngle (*((double *)(pStart+2*RDS_VSZ_S)))
      #define MaxAngle (*((double *)(pStart+2*RDS_VSZ_S+RDS_VSZ_D)))
      #define Accuracy (*((double *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_D)))
      #define Impact (*((char *)(pStart+2*RDS_VSZ_S+3*RDS_VSZ_D)))
      #define GraphReady (*((char *)(pStart+3*RDS_VSZ_S+3*RDS_VSZ_D)))
      #define Angle (*((double *)(pStart+4*RDS_VSZ_S+3*RDS_VSZ_D)))
        // Массив описания параметров для универсальной функции настройки
        static const char *setup[]={
          "2Минимальный угол, град.",
          "3Маскимальный угол, град.",
          "4Шаг по углу, град.",
          NULL};

        switch(CallMode)
          { // Инициализация
            case RDS_BFM_INIT:
              BlockData->BlockData=new TArtWalkData();
              break;
            // Очистка
            case RDS_BFM_CLEANUP:
              delete data;
              break;
            // Проверка типов переменных
            case RDS_BFM_VARCHECK:
              return strcmp((char*)ExtParam,"{SSDDDSSD}")?
                RDS_BFR_BADVARSMSG:RDS_BFR_DONE;
            // Вызов функции настройки
            case RDS_BFM_SETUP:
              return SetupDoubleVars(BlockData->Block,
                  "Перебор углов",setup);

            // Запуск расчета
            case RDS_BFM_STARTCALC:
              if(data->FirstStart)
                { // Самый первый запуск – начало графика
                  Angle=MinAngle; // Начинаем с начала диапазона
                  data->FirstStart=FALSE;
                  Impact=GraphReady=0;
                  Ready=1; // Для передачи по связям
                }
              break;

            // Сброс расчета
            case RDS_BFM_RESETCALC:
              if(data->SelfReset)
                { // Блок сам сбросил подсистему
                  data->SelfReset=FALSE;
                  // Устанавливаем новый угол
                  Angle=data->AngleToSet;
                  Ready=1; // Для передачи по связям
                }
              else // Расчет сброшен кем=то еще
                data->FirstStart=TRUE;
              break;

            // Один такт расчета
            case RDS_BFM_MODEL:
              if(!Impact) // Снаряд не долетел
                { // Сбрасываем сигнал готовности графика
                  GraphReady=0;
                  break;
                }
              // Снаряд долетел – проверяем готовность графика
              if(!GraphReady)
                break;
              // График готов
              Impact=GraphReady=0; // Сбрасываем оба сигнала
              if(Angle>=MaxAngle)
                { // Достигли конца диапазона – прекращаем расчет
                  rdsStopCalc();
                  break;
                }
              // Увеличиваем угол на один шаг
              data->AngleToSet=Angle+Accuracy;
              if(data->AngleToSet>MaxAngle) // Не допускаем выход
                data->AngleToSet=MaxAngle; // за MaxAngle
              data->SelfReset=TRUE; // Сейчас блок сам сбросит подсистему
              rdsResetSystemState(BlockData->Parent);
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef Angle
      #undef GraphReady
      #undef Impact
      #undef Accuracy
      #undef MaxAngle
      #undef MinAngle
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

Не будем подробно останавливаться на личной области данных этого блока: фактически, она представляет собой урезанный вариант класса TArtSearchData, в котором остался только флаг самостоятельного сброса SelfReset и поле AngleToSet для хранения значения угла, которое нужно установить после сброса. Вместо целого поля для режима работы блока в этом классе используется логическое FirstStart – блок может находиться всего в двух режимах: либо он ничего не делает в данный момент и готов к работе (FirstStart истинно), либо он работает, увеличивая угол шаг за шагом (FirstStart ложно).

Из всех реакций этой модели подробно рассмотрим только три: RDS_BFM_STARTCALC, RDS_BFM_RESETCALC и RDS_BFM_MODEL, поскольку остальные устроены очевидным образом и похожи на реакции других моделей, уже рассматривавшихся раньше. В реакции на запуск расчета RDS_BFM_STARTCALC модель проверяет состояние блока, и, если он до сих пор ничего не делал, инициализирует выход Angle началом диапазона углов MinAngle, сбрасывает флаг FirstStart и входные сигналы Impact и GraphReady – теперь блок установил на своем выходе самое первое значение угла, для которого нужно вычислить дальность, и готов к приему сигнала конца расчета траектории Impact и сигнала готовности графика GraphReady. После этого взводится выходной сигнал Ready, чтобы значение Angle передалось по связям в блок расчета траектории.

Реакция на сброс расчета RDS_BFM_RESETCALC, в целом, похожа на реакцию предыдущей модели: если блок сам сбросил подсистему, на выход выдается новое значение угла из поля AngleToSet, если нет – переменной FirstStart присваивается значение TRUE, при следующем запуске расчета это приведет к тому, что блок снова начнет перебирать углы от начала диапазона с заданным шагом.

В реакции на такт расчета RDS_BFM_MODEL прежде всего проверяется, закончен ли расчет траектории, то есть взведен ли сигнал Impact. Если он сброшен, значит, снаряд еще летит, и нас не интересует сигнал готовности графика GraphReady (он сбрасывается), и функция модели на этом завершается. Если же сигнал Impact взведен, нужно ждать готовности графика: проверяется сигнал GraphReady, и, если он сброшен, модель завершается. Если оба сигнала взведены, значит, и расчет траектории уже закончен, и график уже считал нужные ему значения, в этом случае работа функции модели будет продолжена.

Может возникнуть вопрос: если нам нужно продолжать работу модели и изменять угол только после поступления обоих сигналов Impact и GraphReady, почему бы не проверить эти сигналы одновременно, написав проверку так:

      if(Impact && GraphReady)
        { Impact=GraphReady=0;
          // … дальнейшие действия …
        }

Почему в модели сначала проверяется сигнал Impact, и, если он не взведен, GraphReady сбрасывается? Дело в том, что мы пока не знаем, как и какими блоками будет формироваться сигнал, поступающий на вход GraphReady. Если он будет взят с выхода готовности одного из блоков непосредственно перед графиком, не исключены ложные срабатывания сигнала, если этот блок по какой-либо причине запустится до завершения расчета траектории. Мы точно знаем только одно: при нормальной работе схемы сигнал готовности графика может прийти только после завершения расчета траектории, то есть сигнал GraphReady может быть взведен только после того, как взведется сигнал Impact. Таким образом, все срабатывания GraphReady до Impact можно считать ложными, и сбрасывать их. Без этого нам пришлось бы жестко требовать от пользователя, который будет собирать схему на основе нашего блока, исключения ложных срабатываний GraphReady, что, несомненно, усложнит его работу.

Вернемся к реакции модели на такт расчета. Когда оба входных сигнала взведены, модель сбрасывает их и сравнивает текущий угол с верхней границей диапазона. Если угол достиг конца диапазона, значит, все углы уже перебраны, и расчет завершается. В противном случае к текущему значению угла Angle добавляется шаг Accuracy, и сумма записывается в поле AngleToSet личной области данных – после сброса расчета это значение будет переписано в выход блока Angle и передастся в блок расчета траектории. Затем взводится флаг SelfReset и родительская подсистема сбрасывается в начальное состояние.

Теперь, используя созданный блок, можно собрать схему для построения графика, изображенную на рис. 103. В этой схеме блок-планировщик, блок перебора углов и блок расчета траектории помещены в подсистему «Sys1», которая, в свою очередь, помещена в корневую подсистему. Устанавливаемое блоком перебора значение угла «Angle» подается на одноименный вход блока расчета траектории и выведено на внешний выход подсистемы. Сигнал «Impact» с блока расчета траектории подан на одноименный вход блока перебора углов и на внешний выход «OK» подсистемы. Горизонтальная координата снаряда «x» выведена наружу подсистемы через внешний выход «L». Наконец, с внешнего входа подсистемы «Ready» сигнал подается на вход «GraphReady» блока перебора углов – это и есть сигнал готовности графика, который формируется снаружи подсистемы «Sys1».

Рис. 103. Построение графика зависимости дальности от угла

Снаружи подсистемы «Sys1» все устроено достаточно просто. Сигнал «OK» (к нему внутри подсистемы подключен сигнал «Impact») подан на вход счетчика, который, таким образом, будет считать число расчетов траектории, увеличивая свой выход «N» каждый раз, как снаряд будет падать на землю. Выход «N» счетчика соединен со входом времени «Time» двухкоординатного графика. Чтобы схема работала, в параметрах этого графика нужно указать, что значение времени нужно брать не из динамической переменной «DynTime», а с входа «Time» – в этом случае график будет запоминать очередной отсчет при изменении этого входа. К входу «X» графика присоединен выход подсистемы «Angle», а к входу «Y» – выход «L». Таким образом, на момент окончания расчета траектории, у графика на входе «X» окажется значение угла возвышения, а на входе «Y» – соответствующая ему дальность. В качестве сигнала готовности графика (внешний вход «Ready» подсистемы) используется выход счетчика «N»: в RDS с сигнальным входом можно соединять выход любого типа, при этом сигнал взведется при срабатывании связи, идущей от этого выхода, какое бы значение ни было передано по этой связи.

Схема будет работать следующим образом: при самом первом запуске расчета блок перебора выдаст на выход «Angle» начало диапазона углов, и блок расчета траектории начнет моделировать полет снаряда. На выходе счетчика в корневой подсистеме, а, значит, и на входе «Time» графика при этом будет начальное значение счетчика, то есть 0. На входе «X» графика установится начальное значение угла, а на вход «Y» будет постоянно передаваться меняющаяся горизонтальная координата снаряда. Как только снаряд упадет на землю, блок расчета траектории выдаст сигнал «Impact», который попадет на вход блока перебора углов и, через выход подсистемы «OK», на вход счетчика. Блок перебора углов не сделает ничего, поскольку он пока еще не получил сигнал готовности графика. Счетчик увеличит значение на 1, и это значение одновременно попадет на вход «Time» графика и взведет сигнал «GraphReady» у блока перебора углов через внешний вход «Ready» подсистемы. График при этом запомнит очередной отсчет «угол–дальность», а блок перебора сбросит расчет. Затем все повторится снова: по окончании расчета траектории счетчик увеличит выходное значение, график запишет очередной отсчет, расчет будет сброшен и т.д. Так будет продолжаться до тех пор, пока угол не достигнет верхней границы диапазона. В результате получится график зависимости дальности полета снаряда от угла возвышения для заданного диапазона углов.

График на рис. 103 построен для начальной скорости 200 м/с в диапазоне углов 0…90° с шагом по углу 0.1°. Поскольку ось нашей метательной машины поднята на 1 м от поверхности земли (см. рис. 100), при нулевом угле возвышения дальность полета получилась не нулевой. При угле возвышения 90°, то есть при выстреле вертикально вверх, снаряд, как и следовало ожидать, имеет нулевую дальность полета. Видно, что дальности 3000 м, для которой при тех же параметрах блоком поиска угла был найден угол возвышения 36° (рис. 101), соответствует еще и угол около 50°. Таким образом, в прошлый раз наш блок поиска угла нашел угол возвышения для настильной траектории.

Оба рассмотренных примера используют сброс подсистемы для повторного проведения расчета с новыми параметрами. В принципе, можно было бы написать блок расчета траектории снаряда так, чтобы его можно было перезапускать при помощи специального входного сигнала, тогда программный сброс расчета не понадобился бы. Однако, это несколько усложнило бы модель блока. Кроме того, многие стандартные блоки (например, графики) и блоки, написанные сторонними программистами, могут не иметь возможности перезапуска, поэтому при работе с ними программный сброс расчета – единственный выход. Использование программного сброса для многократного проведения расчета делает схему универсальной, поскольку она при этом не зависит от особенностей реализации моделей входящих в нее блоков: если они работают при сбросе и запуске расчета пользователем, они будут работать и при программном сбросе.