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

Описания RDS

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

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

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

§2.14.4. Отдельный расчет подсистемы

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

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

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

#### §2.14.4. Отдельный расчет подсистемы

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

Собирая схемы в RDS, нужно всегда помнить, что в режиме расчета каждый блок срабатывает за один такт, поэтому значение на выходе цепочки из N последовательно соединенных блоков установится только через N тактов, даже если это алгебраические блоки, то есть блоки, выходы которых теоретически должны были бы вычисляться без какой-либо задержки. Мы уже сталкивались с задержками, которые возникают из-за цепочек блоков, при обсуждении установки начальных значений на входе блока расчета баллистики. В схемах с планировщиком вычислений, который управляет динамической переменной времени «DynTime», это не так важно, поскольку в таких схемах основные вычисления производятся в моменты изменения времени, а в параметрах планировщика можно задать такое количество дополнительных тактов, за которое все цепочки алгебраических блоков успеют сработать. Однако, в логических схемах и схемах, работающих по тактам, зависимость времени установки значения от длины цепочки последовательно соединенных блоков может привести к временному появлению неверных значений на выходе некоторых блоков.

Рассмотрим, например, схему на рис. 105. В ней значение x с поля ввода подается на две параллельные ветви: в одной блок «Sum1» прибавляет к этому значению число 6, в другой – блоки «Sum2», «Sum3» и «Sum4» три раза последовательно прибавляют к нему число 2. Выходы обоих ветвей вычитаются. Все блоки в этой схеме взяты из библиотеки, и их модели запускаются при срабатывании любой из подключенных к входам связей.

Рис. 105. Параллельные цепочки блоков разной длины

С точки зрения математики, значение на выходе блока вычитания «Diff1» всегда должно быть нулевым, поскольку верхняя ветвь реализует формулу x+6, а нижняя – x+2+2+2. Однако, если рассмотреть работу этой схемы по тактам, можно заметить, что нулевое значение на выходе «Diff1» установится не сразу. Допустим, в исходном состоянии x=0 и на выходах всех блоков тоже нулевые значения. Изменим значение x на 1 и посмотрим, что будет происходить в схеме.

В первом после изменения x такте расчета запустятся модели блоков «Sum1» и «Sum2», поскольку сработала связь, соединяющая их входы с полем ввода x. Таким образом, в конце первого такта на выходе блока «Sum1» появится число 7, а на выходе «Sum2» – число 3.

В втором такте запустятся модели блоков «Diff1» (сработала связь, соединяющая его с «Sum1») и «Sum3» (из-за связи с «Sum2»). На выходе «Sum3» появится значение 5 (3+2), а на выходе «Diff1» – значение 7 (7–0). Таким образом, из-за того, что верхняя ветвь сработала быстрее, на выходе схемы появилось ненулевое значение – блок «Sum4» еще не успел сработать.

В третьем такте запустится модель блока «Sum4», и на его выходе появится значение 7 (5+2). Модель блока «Diff1» работать не будет, поэтому на его выходе останется значение 7.

Наконец, в четвертом такте модель блока «Diff1», наконец, запустится из-за срабатывания связи, соединяющей его с «Sum4», и на его выходе установится нулевое (7–7) значение.

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

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

Для включения и выключения отдельного расчета подсистемы используется сервисная функция rdsSetExclusiveCalc:

      BOOL RDSCALL rdsSetExclusiveCalc(
        RDS_BHANDLE system, // Идентификатор подсистемы
        BOOL on);           // Включить (TRUE) или выключить (FALSE)

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

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

Прежде чем разбираться с самим механизмом отдельного расчета, сделаем один вспомогательный блок, который поможет нам наблюдать отставание или опережение срабатывания параллельных ветвей схемы. Этот блок должен будет ждать изменения входной вещественной переменной, и, после него, начать считать такты расчета, выдавая количество прошедших тактов на свой выход. Таким образом, мы сможем построить график зависимости значения на выходе схемы (например, выхода блока «Diff1» на рис. 105) от номера такта расчета. Кроме того, блок должен останавливать расчет после заданного числа тактов (вручную мы не успеем его остановить – такты следуют друг за другом слишком быстро) и иметь вход разрешения работы, чтобы можно было отключить его на время начальных переходных процессов в схеме при первом запуске расчета. Зададим для блока следующую структуру переменных:

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение | Назначение | Номер
--- | --- | --- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0 | Стандартный сигнал запуска | 0
1 | Ready | Сигнал | 1 | Выход |  | 0 | Стандартный сигнал готовности | 1
2 | Count | int | 4 | Выход |  | 0 | Число тактов, прошедших с момента изменения входа «x» | 2
6 | Stop | int | 4 | Вход |  | 0 | Число тактов, после которого нужно остановить расчет (настроечный параметр) | 3
10 | Enabled | Логический | 1 | Вход | ✓ | 0 | Вход разрешения работы блока | 4
11 | x | double | 8 | Вход | ✓ | 0 | Вход, изменение которого отслеживается | 5
19 | xold | double | 8 | Внутренняя |  | 0 | Предыдущее значение входа «x» для отслеживания его изменения | 6

Модель блока будет иметь следующий вид:

      // Счетчик тактов
      extern "C" __declspec(dllexport)
          int RDSCALL ChgCalcTickCount(int CallMode,
                              RDS_PBLOCKDATA BlockData,
                              LPVOID ExtParam)
      {
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define Count (*((RDSINT32 *)(pStart+2*RDS_VSZ_S)))
      #define Stop (*((RDSINT32 *)(pStart+2*RDS_VSZ_S+RDS_VSZ_I)))
      #define Enabled (*((char *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_I)))
      #define x (*((double *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_I+RDS_VSZ_L)))
      #define xold (*((double *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_I+RDS_VSZ_L+RDS_VSZ_D)))
        switch(CallMode)
          { // Проверка типов переменных
            case RDS_BFM_VARCHECK:
              return strcmp((char*)ExtParam,"{SSIILDD}")?
                RDS_BFR_BADVARSMSG:RDS_BFR_DONE;

            // Запуск расчета
            case RDS_BFM_STARTCALC:
              Start=1; // Принудительный запуск модели в следующем такте
              break;

            // Один такт моделирования
            case RDS_BFM_MODEL:
              if(!Enabled) // Работа блока не разрешена
                { xold=x; // Запоминаем значение входа
                  break;
                }
              // Работа блока разрешена
              if(xold==x && Count==0)
                break; // Вход не изменился или счет не идет
              // Изменился вход (x!=xold) или уже считаем (Count!=0)
              Count++; // Увеличиваем число тактов
              if(Count>Stop) // Пора остановить расчет
                rdsStopCalc();
              Start=1; // Принудительный перезапуск модели
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef xold
      #undef x
      #undef Enabled
      #undef Stop
      #undef Count
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

При запуске расчета (режим RDS_BFM_STARTCALC) эта модель на всякий случай взводит сигнал Start, чтобы запустить себя в первом же такте расчета. В такте расчета (RDS_BFM_MODEL) прежде всего проверяется состояние логического входа Enabled, разрешающего работу блока. Если он имеет нулевое значение, значит, работа запрещена. В этом случае внутренней переменной xold присваивается значение входа x (таким образом отключенный блок будет игнорировать изменения входа, поскольку текущее значение входа всегда будет равно запомненному) и работа модели завершается. Если же работа блока разрешена, модель проверяет, нужно ли увеличить на единицу счетчик тактов Count. Блок должен начать считать такты при изменении входа x, а затем продолжать счет независимо от изменений этого входа, поэтому проверка состоит из двух частей: во-первых, значение x сравнивается с запомненным xold (если они отличаются, значит, вход изменился), и, во-вторых, значение Count сравнивается с нулем (если Count!=0, значит, счет уже начался, и его нужно продолжать). Если вход не изменился и значение Count нулевое, модель завершается (она автоматически запустится снова при изменении Enabled или x, поскольку для этих переменных установлен флаг «пуск»). В противном случае Count увеличивается на 1 и сравнивается с параметром Stop: как только Count превысит Stop, расчет будет остановлен функцией rdsStopCalc. В самом конце взводится сигнал Start, чтобы модель снова запустилась в следующем такте. Можно было бы просто установить для блока флаг «запуск каждый такт», но такое принудительное взведение Start улучшает надежность модели – она будет работать независимо от того, установлен ли этот флаг в параметрах блока.

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

Рис. 106. Схема сравнения скоростей срабатывания одиночного блока и цепочки блоков

Верхняя ветвь схемы состоит из единственного блока «Kx+C», созданного нами в §2.7.4. В нижнюю ветвь схемы включена подсистема, реализующая ту же самую формулу при помощи последовательного соединения двух таких же блоков, на первый из которых подается значение «K» при нулевом «С», а на второй – значение «С» при единичном «K». В результате эта цепочка блоков выполняет ту же самую функцию: «1(Kx+0)+C». Можно было бы собрать в подсистеме схему из библиотечных блоков умножения на константу и сумматора, однако, в алгебраические библиотечные блоки встроены дополнительные средства ускорения прохождения данных, поэтому эксперимент был бы не совсем чистым. Значения «x», «K» и «C» подаются в обе ветви с одних и тех же полей ввода, поэтому выходы обеих ветвей должны быть одинаковыми. Выходы блока и подсистемы поданы на блок вычитания, выход которого, в свою очередь, выведен на график. На вход времени графика «Time» подан сигнал с выхода «Count» нашего блока-счетчика тактов, при этом в параметрах графика указано, что значения времени он должен брать не из динамической переменной «DynTime», как он это делает по умолчанию, а со своего входа. Дополнительно, для улучшения внешнего вида графика, для него установлен тип линии «ступенька посередине» – процесс у нас дискретный, и кусочно-линейный график здесь ни к чему.

С входом «Enabled» блока-счетчика соединена кнопка «On», к входу «Stop» подключено поле ввода со значением 5, а к входу «x» – связь от того же самого поля ввода, которое идет на вход обеих параллельных ветвей схемы. Таким образом, если запустить расчет, нажать кнопку «On», а затем изменить значение поля ввода «x», блок-счетчик начнет каждый такт увеличивать значение своего выхода «Count» на 1. При этом будет строиться график разности выходов двух ветвей схемы в каждом такте расчета. Отработав 5 тактов, блок остановит расчет, и мы сможем на графике увидеть выбросы, возникшие из-за разного времени срабатывания двух ветвей.

В данном случае мы видим выброс вверх длиной в один такт расчета. Из-за того, что верхняя ветвь состоит из одного блока, а нижняя – из двух, значение на выходе подсистемы запоздало на один такт, в течение которого вход «x1» блока вычитания уже имел правильное значение 2, а вход «x2» все еще оставался нулевым.

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

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

Блок будет иметь следующую структуру переменных:

Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение
--- | --- | --- | --- | --- | --- | ---
0 | Start | Сигнал | 1 | Вход | ✓ | 0
1 | Ready | Сигнал | 1 | Выход |  | 0
2 | Lock | Сигнал | 1 | Вход | ✓ | 0
3 | Unlock | Сигнал | 1 | Вход | ✓ | 0
4 | Locked | Логический | 1 | Внутренняя |  | 0

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

      // Включение/выключение отдельного расчета
      extern "C" __declspec(dllexport)
        int RDSCALL SetExclusiveCalc(int CallMode,
              RDS_PBLOCKDATA BlockData,
              LPVOID ExtParam)
      {
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define Lock (*((char *)(pStart+2*RDS_VSZ_S)))
      #define Unlock (*((char *)(pStart+3*RDS_VSZ_S)))
      #define Locked (*((char *)(pStart+4*RDS_VSZ_S)))
        switch(CallMode)
          { // Проверка типов переменных
            case RDS_BFM_VARCHECK:
              return strcmp((char*)ExtParam,"{SSSSL}")?
                  RDS_BFR_BADVARSMSG:RDS_BFR_DONE;

            // Такт расчета
            case RDS_BFM_MODEL:
              if(Lock) // Поступил сигнал включения отдельного расчета
                { if(Locked) // Отдельный расчет уже включен
                    { Lock=0; // Сбрасываем сигнал, завершаем модель
                      break;
                    }
                  // Включаем отдельный расчет родительской подсистемы
                  if(rdsSetExclusiveCalc(BlockData->Parent,TRUE))
                    { // Включить удалось
                      Lock=0;   // Сбрасываем сигнал
                      Locked=1; // Запоминаем факт включения
                    }
                }
              if(Unlock) // Поступил сигнал выключения
                { if(Locked) // Отдельный расчет был включен - выключаем
                    rdsSetExclusiveCalc(BlockData->Parent,FALSE);
                  Locked=Lock=Unlock=0; // Сбрасываем сигналы и состояние
                }
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef Locked
      #undef Unlock
      #undef Lock
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

Модель получилась достаточно простой. В такте расчета мы сначала проверяем, не пришел ли сигнал Lock. Если он взведен, мы проверяем, не включен ли уже отдельный расчет родительской подсистемы (для запоминания факта включения мы используем логическую переменную состояния Locked), и, если он включен, мы сбрасываем сигнал Lock и завершаем работу модели – повторные попытки включить режим отдельного расчета игнорируются блоком. Если же он не был включен, включаем его функцией rdsSetExclusiveCalc, в которую передаем идентификатор родительской подсистемы блока BlockData->Parent, сбрасываем Lock и взводим переменную состояния Locked.

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

Изменим теперь схему на рис. 106 так, чтобы подсистема в нижней ветви считалась с приостановкой расчета остальной схемы. Для этого включим в нее созданный нами блок отдельного расчета, подключим к его входу «Lock» все входные связи подсистемы, а выход последнего блока цепочки подадим на вход «Unlock» (рис. 107). В результате этого при срабатывании любой входной связи подсистемы сигнал «Lock» взведется, и подсистема перейдет в режим отдельного расчета. Как только последний блок сработает и данные его выхода передадутся по связи наружу подсистемы, взведется сигнал «Unlock», и расчет остальной части схемы возобновится.

Рис. 107. Подключение блока для отдельного расчета подсистемы

Если теперь запустить расчет, нажать кнопку «On» и изменить значение в поле ввода «x», можно будет увидеть, что на графике нет никаких выбросов: поскольку расчет всей схемы останавливался на время расчета подсистемы, получилось, что обе параллельных ветви схемы сработали одновременно.

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

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

      switch(CallMode)
        { // …

          // Срабатывание таймера
          case RDS_BFM_TIMER:
            // … РЕАКЦИЯ НА ТАЙМЕР …
            break;

          // Изменение динамической переменной
          case RDS_BFM_DYNVARCHANGE:
            // … РЕАКЦИЯ НА ДИНАМИЧЕСКУЮ ПЕРЕМЕННУЮ …
            break;

          // Такт расчета
          case RDS_BFM_MODEL:
            // … ДЕЙСТВИЯ В ТАКТЕ РАСЧЕТА …
            break;
        } // switch

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

      switch(CallMode)
        { // …

          // Срабатывание таймера
          case RDS_BFM_TIMER:
            TimerOk=1;  // Запоминаем факт срабатывания таймера
            Start=1;    // Запускаем модель в следующем такте
            break;

          // Изменение динамической переменной
          case RDS_BFM_DYNVARCHANGE:
            DynVarOk=1; // Запоминаем факт изменения переменной
            Start=1;    // Запускаем модель в следующем такте
            break;

          // Такт расчета
          case RDS_BFM_MODEL:
            if(TimerOk) // Было срабатывание таймера
              { // … РЕАКЦИЯ НА ТАЙМЕР …
                TimerOk=0; // Реакция выполнена, больше помнить не нужно
              }
            if(DynVarOk) // Было изменение переменной
              { // … РЕАКЦИЯ НА ДИНАМИЧЕСКУЮ ПЕРЕМЕННУЮ …
                DynVarOk=0; // Реакция выполнена, больше помнить не нужно
              }
            // … ДЕЙСТВИЯ В ТАКТЕ РАСЧЕТА …
            break;
        } // switch

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