Руководство программиста
Глава 2. Создание моделей блоков
§2.5. Статические переменные блоков
Описывается работа со статическими переменными блоков (то есть переменными фиксированной структуры), которые могут служить им входами и выходами. Рассматриваются особенности работы с сигнальными переменными, матрицами и массивами, структурами, строками произвольной длины, а также с переменными, меняющими свой тип в процессе работы. Приводятся примеры моделей блоков, иллюстрирующие доступ к переменным каждого типа.
§2.5.1. Доступ к статическим переменным и работа в режиме расчета
Описывается способ чтения и записи значений статических переменных блока. Рассматривается событие проверки допустимости типа статических переменных RDS_BFM_VARCHECK и событие выполнения такта расчета RDS_BFM_MODEL. Приводится пример блока, выдающего в режиме расчета на выход разность двух вещественных входов. В примере предусмотрена проверка значений входов блока на специальное значение, символизирующее ошибку вещественных вычислений.
Для написания простейшей модели, работающей в режиме расчета, необходимо задать реакцию всего на два события: проверки типа статических переменных RDS_BFM_VARCHECK и выполнения одного такта расчета RDS_BFM_MODEL.
Обычно модели блоков, работающих в режиме расчета, производят какие-либо вычисления со значениями входов и внутренних переменных и присваивают результаты своим выходам, то есть работают, в основном, со статическими переменными блока. Перед обращением к статическим переменным модель должна проверить, удовлетворяет ли их структура необходимым требованиям. Если этого не сделать, при подключении модели к блоку с неподходящими для этой модели переменными могут возникнуть серьезные проблемы, например, ошибки общей защиты из-за обращения к не отведенной памяти. При этом такие ошибки могут проявится не сразу: если окажется, что блок памяти, к которому ошибочно обращается модель, вместо данных переменной содержит какие-либо важные для RDS данные, при записи в этот блок работа RDS может нарушиться самым непредсказуемым образом: например, начнут возникать ошибки в других моделях, ошибки при попытке сохранения схемы (с возможной потерей данных) и т.п. Поэтому к проверке типов переменных блока программист должен относиться очень ответственно.
Для проверки типов статических переменных служит вызов модели с параметром RDS_BFM_VARCHECK, при этом через параметр ExtParam передается строка типа переменных блока, которую модель должна проанализировать и возвратить один из следующих результатов:
- RDS_BFR_DONE – структура переменных блока удовлетворяет требованиям модели;
- RDS_BFR_ERROR – структура переменных блока не удовлетворяет требованиям модели;
- RDS_BFR_BADVARSMSG – структура переменных блока не удовлетворяет требованиям модели, RDS выведет предупреждающее сообщение для пользователя.
Если в ответ на событие RDS_BFM_VARCHECK модель вернет RDS_BFR_ERROR или RDS_BFR_BADVARSMSG, все вызовы модели, кроме последующих RDS_BFM_VARCHECK при новых изменениях структуры переменных, а также реакции на отключение модели RDS_BFM_CLEANUP, будут заблокированы. Разумеется, вызов RDS_BFM_INIT также не блокируется, поскольку он производится самым первым, еще до RDS_BFM_VARCHECK. Таким образом, введя в модель реакцию на это событие, в реакциях на все остальные события (кроме RDS_BFM_INIT и RDS_BFM_CLEANUP, в которых обращение к статическим переменным запрещено) можно обращаться к статическим переменным без каких-либо дополнительных проверок – все проверки уже сделаны в RDS_BFM_VARCHECK.
В качестве примера рассмотрим блок с двумя вещественными входами x1 и x2 и выходом y, на который выдается разность входов (x1–x2). С учетом двух обязательных сигналов Start и Ready структура переменных блока будет выглядеть следующим образом:
| Смещение | Имя | Тип | Размер | Вход/выход |
|---|---|---|---|---|
| 0 | Start | Сигнал | 1 | Вход |
| 1 | Ready | Сигнал | 1 | Выход |
| 2 | x1 | double | 8 | Вход |
| 10 | x2 | double | 8 | Вход |
| 18 | y | double | 8 | Выход |
Структура переменных задается в окне параметров блока на вкладке «» (см. рис. 8, кнопка «»). Для редактирования переменных блока открывается отдельное окно, в котором также отображается смещение для каждой из них (вычисляется автоматически) и строка типа для всей структуры переменных (см. рис. 9).
Функция модели блока будет выглядеть следующим образом:
extern "C" __declspec(dllexport) int RDSCALL TestSub(int CallMode, RDS_PBLOCKDATA BlockData, LPVOID ExtParam) { // Макроопределения для статических переменных #define pStart ((char *)(BlockData->VarTreeData)) #define Start (*((char *)(pStart))) // 0 #define Ready (*((char *)(pStart+RDS_VSZ_S))) // 1 #define x1 (*((double *)(pStart+2*RDS_VSZ_S))) // 2 #define x2 (*((double *)(pStart+2*RDS_VSZ_S+RDS_VSZ_D))) // 3 #define y (*((double *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_D))) // 4 switch(CallMode) { // Проверка типа переменных case RDS_BFM_VARCHECK: if(strcmp((char*)ExtParam,"{SSDDD}")==0) return RDS_BFR_DONE; return RDS_BFR_BADVARSMSG; // Выполнение такта расчета case RDS_BFM_MODEL: y=x1-x2; // Вычисление значения выхода break; } return RDS_BFR_DONE; // Отмена макроопределений #undef y #undef x2 #undef x1 #undef Ready #undef Start #undef pStart } //=========================================
В тексте этой модели используются макроопределения для доступа к статическим переменным, что позволяет сделать текст программы более читаемым. Сначала вводится определение pStart – это указатель на начало дерева переменных, приведенный к типу «указатель на char»:
#define pStart ((char *)(BlockData->VarTreeData))
Тип char выбран из-за своего размера в один байт: прибавление целого числа к такому указателю смещает его на заданное число байтов. Теперь, если требуется обратиться к переменной со смещением N, указатель на нее можно получить, добавив N к pStart. Например, чтобы получить начальный адрес области памяти, занимаемой переменной y, смещение к которой равно восемнадцати байтам, можно написать pStart+18. Естественно, для доступа к этой переменной необходимо привести этот адрес к типу «указатель на double», то есть использовать конструкцию (double*)(pStart+18). А для того, чтобы можно было непосредственно использовать имя переменной в выражениях (как в правой части, так и в левой), можно ввести макроопределение вида
#define y ( *((double*)(pStart+18)) )
Вместо числовой константы 18 можно использовать символические константы размеров переменных RDS RDS_VSZ_* из «RdsDef.h»:
- RDS_VSZ_S – сигнал (1 байт);
- RDS_VSZ_L – логическая переменная (1 байт);
- RDS_VSZ_C – char (1 байт);
- RDS_VSZ_H – short int, короткая целая переменная (2 байта);
- RDS_VSZ_I – int, RDSINT32, целая переменная (4 байта);
- RDS_VSZ_F – float, вещественная переменная (4 байта);
- RDS_VSZ_D – double, вещественная переменная (8 байтов);
- RDS_VSZ_A – указатель на строку в кодировке UTF-8 (8 байтов);
- RDS_VSZ_V – переменная произвольного типа (16 байтов);
- RDS_VSZ_M – матрица или массив (16 байтов);
- RDS_VSZ_STRUCT – указатель на структуру (16 байтов).
Следует учитывать, что под указатели RDS всегда отводит 8 байтов, несмотря на то, что в тридцатидвухбитной версии они занимают только 4. Это позволяет не менять описания при компиляции модели для разных платформ. В тридцатидвухбитных моделях указатель будет занимать первые 4 байта отведенной под переменную область памяти, в шестидесятичетырехбитной – всю область.
С символическими константами макроопределение для переменной y будет выглядеть следующим образом:
#define y (*((double *)(pStart+2*RDS_VSZ_S+2*RDS_VSZ_D)))
В конце функции модели все макроопределения уничтожаются командами #undef, чтобы в следующей функции модели можно было, при желании, ввести новые определения для других переменных с теми же именами.
Вместо того, чтобы писать все эти определения вручную, можно получить их у RDS, нажав правую кнопку мыши на строке типа в левом нижнем углу окна редактирования переменных и выбрав в меню пункт «» (рис. 16). При этом в буфер обмена Windows будет помещен текст описаний для всех статических переменных блока вместе с командами #undef.
Рис. 16. Вызов меню для копирования в буфер обмена всех макросов переменных блока
Следует помнить, что макроопределения для доступа к статическим переменным можно использовать только в функциях, в которые передается указатель на структуру данных блока BlockData, поскольку все макросы ссылаются на этот указатель. Сначала вводится описание указателя на начало дерева переменных pStart, а затем все остальные переменные определяются относительно него. Когда препроцессор языка C, обрабатывая текст приведенной выше модели, встретит в нем слово «y», он сначала подставит вместо него текст
(*((double *)(pStart+18)))
а затем, раскрывая макроопределение для pStart, преобразует его в
(*((double *)(((char *)(BlockData->VarTreeData))+18)))
Пока переменная y используется внутри функции модели блока, развернутое макроопределение для нее будет компилироваться успешно – в функцию модели передается параметр BlockData, и конструкция BlockData->VarTreeData будет понята компилятором. Если же переменную необходимо использовать и в других функциях, вызываемых из функции модели, в эти функции также необходимо передавать параметр RDS_PBLOCKDATA BlockData, иначе развернутый макрос не будет скомпилирован. Кроме того, тексты всех этих функций должны размещаться после макроопределений для переменных, чтобы на момент их компиляции макросы уже существовали. Это несколько ухудшает читаемость текста программы: в функцию передается параметр, который, на первый взгляд, нигде не используется, при этом переменная y, фигурирующая в функции, не описана ни как параметр, ни как внутренняя. К тому же, вероятнее всего, значение y нельзя будет посмотреть в отладчике, поскольку это не настоящая переменная, а макрос. Тем не менее, при написании функций, интенсивно работающих с переменными блока, использование макросов позволяет передавать в функцию один параметр BlockData вместо множества указателей на все переменные блока, а ухудшение читаемости текста можно компенсировать подробными комментариями.
Вернемся к модели блока, вычисляющего разность своих входов. При вызове этой модели с параметром RDS_BFM_VARCHECK производится сравнение строки типа переменных блока, переданной через параметр ExtParam, со строкой, заложенной в программу. В данном случае строка, соответствующая структуре переменных, для которой разработана эта модель, выглядит как «{SSDDD}» – две сигнальных («SS») и три вещественных («DDD») переменных двойной точности. Сравнение производится стандартной библиотечной функцией strcmp (из-за нее может потребоваться включение в исходный текст команды #include <string.h>), перед вызовом которой ExtParam приводится к типу «char*». Если строки совпадают, эта функция возвращает значение 0. Функция модели в этом случае вернет константу RDS_BFR_DONE, информируя RDS о том, что структура статических переменных подходит для данной модели. Если же строки не совпадут, функция модели вернет константу RDS_BFR_BADVARSMSG, в результате чего RDS заблокирует все последующие вызовы этой модели (за исключением RDS_BFM_VARCHECK и RDS_BFM_CLEANUP) и выведет пользователю сообщение о несовместимости данной модели с данной структурой переменных. Если бы модель вернула константу RDS_BFR_ERROR, все вызовы также были бы заблокированы, но без какого-либо сообщения пользователю.
Строку типа, соответствующую структуре переменных блока, не обязательно записывать вручную. Так же, как и макросы переменных, ее можно получить у RDS, нажав правую кнопку мыши на строке типа, отображаемой в левом нижнем углу окна редактирования переменных, и выбрав в появившемся меню пункт «» (см. рис. 16). При этом в буфер обмена Windows будет помещена указанная строка, которую можно будет вставить в текст программы.
При вызове модели нашего блока с параметром RDS_BFM_MODEL значение выхода y вычисляется как разность входов x1 и x2, при этом для доступа к переменным используются описанные выше макросы. Чтобы значение y всегда соответствовало значениям входов, наша модель должна вызываться либо постоянно в каждом такте расчета, либо при изменении любого из входов. В первом случае можно задать для блока с этой моделью запуск каждый такт расчета (в окне параметров блока на вкладке «»), во втором – задать для переменной Start начальное значение 1 и установить флаги «» для входов x1 и x2 (в окне редактирования переменных). Второй вариант предпочтительнее, поскольку он позволит избежать ненужных вызовов модели, если входы не изменялись: модель будет вызываться в режиме RDS_BFM_MODEL только тогда, когда сработают связи, подключенные к x1 или x2, то есть тогда, когда значение входов изменится.
Приведенная выше модель имеет один серьезный недостаток: в ней не проверяется допустимость выполнения операции вычитания. Если на одном из входов блока окажется значение, используемое математическими функциями для индикации ошибки или переполнения, попытка выполнить с ним какую-либо операцию приведет к возникновению исключения. Чтобы этого избежать, необходимо перед вычитанием сравнить значения входов с этим специальным значением-индикатором. Можно использовать константу HUGE_VAL, описанную в стандартном заголовочном файле «math.h», но лучше всего получить это значение у RDS при помощи сервисной функции rdsGetHugeDouble. Этот вариант более надежен, потому что все модели, использующие эту функцию, гарантированно получают одно и то же значение, не зависящее от версии RDS и версий библиотек, которые используются в DLL моделей.
Лучше всего вызывать функцию rdsGetHugeDouble один раз в главной функции DLL и записывать полученное значение в какую-нибудь глобальную переменную, которую смогут использовать все функции моделей этой DLL. В этом случае главная функция примет следующий вид (изменения относительно примера в §2.4 выделены цветом):
// Глобальная переменная для значения ошибки double DoubleErrorValue; //========== Главная функция DLL ========== int WINAPI DllMain(HINSTANCE /*hinst*/, unsigned long reason, void* /*lpReserved*/) { if(reason==DLL_PROCESS_ATTACH) // Загрузка DLL { // Получение доступа к функциям if(!GetInterfaceFunctions()) RDS_SERV_ERROR_MSGW // Сообщение: старая версия RDS else rdsGetHugeDouble(&DoubleErrorValue); } return 1; } //=========================================
Функции rdsGetHugeDouble передается указатель на глобальную переменную DoubleErrorValue, в которую она заносит значение-индикатор ошибки. Теперь реакцию модели на выполнение одного такта моделирования можно изменить следующим образом:
// Выполнение такта моделирования case RDS_BFM_MODEL: if(x1==DoubleErrorValue || x2==DoubleErrorValue) y=DoubleErrorValue; else y=x1-x2; break;
Если хотя бы на одном входе блока появляется значение, сигнализирующее об ошибке, выход тоже получает это значение.