Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

2830.Встроенные микропроцессорные системы

..pdf
Скачиваний:
41
Добавлен:
15.11.2022
Размер:
52.65 Mб
Скачать

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

Синхронный язык объединяет конечный автомат и язык программирования в одну модель. Синхронный язык может выражать сложные вычисления, но исполнительной моделью является конечный автомат. Такой язык описывает одновременно действующие автоматы. Детерминированное поведение достигается за счет того, что параллельно работающие автоматы одновременно выполняют переходы между состояниями. Это предполагает наличие единого глобального тактового сигнала, а не своего тактового сигнала для каждого из автоматов. В каждом такте принимаются во внимание все входы, вычисляются выходы и следующие состояния, что требует быстрого широковещательного механизма для всех частей модели. Такой идеалистический взгляд на одновременность гарантирует детерминированное поведение. Это является ограничением, если сравнивать с моделью CFSM, в которой каждая FSM имеет свой собственный тактовый сигнал. Синхронный язык, по сути, отражает принцип работы синхронных схем.

Пусть даны две переменные d и x. Каждый тик в синхронной модели они обновляются следующим образом (d’ и x’ – значения в следующий момент времени):

d’ = –d если x = 0 ; d иначе x’ = x + d

x: 0 -1 0 1 0 -1 0 1 d: -1 1 1 -1 -1 1 1 1

В80-х гг. прошлого века был предложен синхронный язык Lustre

[25]для разработки надежных систем в таких критических областях, как аэрокосмическая, ядерная энергетика, поезда без водителей.

131

Lustre базируется на парадигме программирования, которая представляет преобразования в виде модели синхронного потока данных. Эта парадигма полагается на следующие моменты:

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

входы и выходы, таким образом, являются потоками бесконечных последовательностей значений некоторого типа;

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

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

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

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

ся как v1, v2, v3, ...

3 означает поток констант 3, 3, 3, ...;

a + b означает поток a1 + b1, a2 + b2, a3 + b3, ...;

пусть v = expr есть выражение для потока v. Тогда в других вы-

ражениях v может быть безопасно заменен на expr в соответствии с принципом замещения.

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

ESTEREL Technologies разработала коммерческий инструмент SCADE для моделирования, тестирования и верификации синхронных систем, использующих Lustre. SCADE де-факто стала европейским стандартом. Она используется в Airbus, Merlin-Gerin, «Сухой» и др. SCADE является комплектом программ, работающих под Windows и Linux, и состоит из следующих программных компонентов:

132

GUI-интерфейс для графической манипуляции с моделями Lustre;

симулятор;

инструмент для верификации;

генератор исполняемого кода;

инструмент генерации отчетов.

Lustre опирается на 3 базовые концепции:

flow (поток) – последовательность значений данных. Все значения потока принадлежат к одному типу, например integer flow: 0, 1, 2, 3, ...;

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

node rising (X:bool) returns (Y:bool) ; let

Y = false –> X and not pre(X); tel

Оператор pre (предыдущее значение): если A = (a1, a2, a3, ..., an, ...),

тогда pre(A) = (nil, a1, a2, a3, ..., an–1, ...). Оператор –> (инициализация flow): если A = (a1, a2, a3, ..., an, ...) и B = (b1, b2, b3, ..., bn, ...), то A–>B = = (a1, b2, b3, ..., bn, ...).

Функция rising примера обнаруживает моменты изменения символов входного потока Х с 0 на 1 и формирует соответствующий выходной поток Y. Однажды определенный узел становится оператором и может быть вызван в другом узле. На рис. 72 приведено графическое представление rising в SCADE;

Рис. 72. Графическое представление rising в SCADE

133

cycle (цикл) – один шаг выполнения программы. В каждом цикле вычисляются новые значения и добавляются в каждый поток. Каждый цикл соответствует тику.

Модели систем могут быть смоделированы в SCADE. Симулятор вычисляет значения каждого потока в каждом цикле. Разработчик может ввести значения для входных потоков и наблюдать выходные потоки. На рис. 73 приведены результаты моделирования узла rising.

X:F T T F F T T

Y:F F F F F T F re(X): nil T T T F F T

not pre(X): nil T F F F T T F

X and not pre(X): nil T F F F F T F False: F F F F F F F F

false -> X and not pre(X): F T F F F F T F Cycle: 0 1 2 3 4 5 6 7

Рис. 73. Временная диаграмма rising

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

Рис. 74. Узел Monitor для верификации rising

Monitor затем подключают к узлу rising (рис. 75) и убеждаются, что выход Monitor всегда принимает значение true.

134

Рис. 75. Верификации rising

Преобразование линейных систем после дискретизации в программы на Lustre является очевидной задачей. Если система выражена в форме z-преобразований, то оператор 1/z транслируется в 0,0 –> pre(). Пусть передаточная функция второго порядка имеет вид

H(z) = (a*z2 + b*z + c)/(z2 + d*z + e),

y = H(z)*x,

у = a*x + (b*x d*y)*1/z + (c*x – e*y)*1/z2. Отсюда программа:

const a, b, c, d, e: real.

node SECOND_ODER (x: real) returns (y: real) ; let

y = a*x + (0,0-> pre(u) );

u= b*x – d*y + (0,0-> pre(v) );

v= c*x – e*y;

tel.

SCADE может генерировать Си-код из модели. Каждый узел транслируется в функцию так, что входные и выходные аргументы функции соответствуют входным и выходным аргументам узла. Сгенерированный код обладает следующими характеристиками:

как и модель, является детерминированным;

не содержит динамически выделяемой памяти;

чистое отображение между кодом и моделью;

последнее позволяет производить трассировку программы при отладке.

Рассмотрим простой пример проектирования контроллера «Газовая горелка» (рис. 76) [26]. Контроллер должен сохранять температуру внутри заданного интервала (например, между 50 и 60 °С включительно). Изначально температура находится в заданном интервале. Контроллер преобразует входной поток Т (значения температуры) в выходной булев поток В.

135

Рис. 76. Газовая горелка

Предлагается следующий алгоритм работы: когда температура достигает 50 °С, горелка включается; когда температура достигает 60 °С, горелка выключается. На Lustre это выглядит так:

B = false -> (if T=50 then true else if T=60 then false else pre(B))

На рис. 77 представлена модель контроллера газовой горелки в

SCADE.

Рис. 77. Модель газовой горелки в SCADE

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

когда горелка включается, температура повышается;

когда горелка выключается, температура понижается;

136

горелка переключается мгновенно;

когда нагрева нет, температура в баке уменьшается со скоростью

1°С в единицу времени;

когда происходит нагрев, температура в баке повышается со скоростью 3 °С в единицу времени;

датчик температуры точный.

Описание датчика температуры:

вход temp – температура в баке, действительная величина (real);

выход reading – отсчет;

преобразование: temp = reading.

Описание горелки:

вход switch – команда контроллера;

выход heating – the «пламя», булева величина (вoolean);

преобразование: heating = switch.

Описание бака:

вход heated – булева величина, индицирующая нагрев или его отсутствие;

выход temp – температура;

преобразование: temp = 55 –> (if heated then pre(temp)+3 else pre(temp)–1).

Моделирование в SCADE позволяет найти ошибку: для некоторой

начальной температуры никогда не достигается температура точно в 60 °С и горелка никогда не выключится. Это связано с ограничениями на частоту дискретизации CPU. Поэтому необходимо вместо ‘if T=60 then false’ перейти к нестрогому неравенству ‘if T>=57 then false’, где Т – температура, вычисленная в предыдущем цикле.

2.5. Многозадачность

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

137

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

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

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

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

2.5.1. Язык программирования Си

Программы с языка проектирования автоматически или полуавтоматически транслируются сначала в программы на языке программирования Си, а затем компилируются в объектный код. Этот процесс называют генерацией кода. Часть разработчиков встроенных систем предпочитают писать проекты сразу на Си, даже для «голого железа» (без использования операционных систем).

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

Рассмотрим модель памяти в Си. Си-программы сохраняют данные в стеке, «куче» (heap) и фиксированных ячейках памяти, назначаемых компилятором. Рассмотрим пример Си-программы.

1 int a = 2;

2 void foo(int b, int* c) {

3...

4}

5int main(void) {

6int d;

7int* e;

8

d = …;

// присвоение некоторой величины d.

9

e = malloc(sizeInBytes);

// выделение памяти для e.

10

*e = …;

// присвоение некоторой величины e.

11foo(d, e);

12

13}

138

Переменная ‘a’ является глобальной переменной, так как объявлена вне определений функций. Компилятор назначит ее на определенное место в памяти. Переменные ‘b’ и ‘c’ являются параметрами. Им выделяется место в стеке, когда вызывается функция foo (компилятор может также назначить их на регистры). Переменные ‘d’ и ‘e’ – локальные переменные. Они объявляются внутри тела функции (в примере – в main). Компилятор зарезервирует для них место в стеке.

Когда в строке 11 вызывается функция foo, ячейка стека, назначенная для ‘b’, получает копию переменной ‘d’, установленной в строке 8. Это является примером передачи параметров в функцию. Данные, передаваемые указателем ‘e’, наоборот запоминаются в памяти, выделенной под кучу, и проходят через ссылку (указатель на ‘e’ проходит как величина). Адрес запоминается в ячейке стека для ‘c’. Если foo содержит оператор присвоения для *c, то после возврата из foo это значение может бать прочитано разименованием ‘e’.

Рассмотрим некоторые ключевые моменты Си на примере программы (рис. 78). Эта программа реализует часто используемый шаблон, называемый observer (наблюдатель). В этом шаблоне функция update изменяет величину переменной ‘х’. Наблюдатели (другие программы или части программы) будут оповещаться (notify) функцией обратного вызова (callback) всякий раз, когда изменяется ‘х’. В программе используется связанный список – структура данных для хранения списка элементов, длина которого может изменяться во время выполнения программы. Каждый элемент списка содержит полезную нагрузку (значение элемента) и указатель на следующий элемент в списке (или нуль-указатель, если элемент последний).

Для программы на рис. 78 структура данных связанного списка определяется следующим образом:

1typedef void notifyProcedure(int);

2struct element {

3notifyProcedure* listener;

4struct element* next;

5};

6typedef struct element element_t;

7element_t* head = 0;

8element_t* tail = 0;

139

typedef struct element element_t;

// Тип элемента списка функций оповещения.

element_t* head = 0;

// Указатель на начало списка.

element_t* tail = 0;

// Указатель на конец списка.

void addListener(notifyProcedure* listener) { // Функция регистрации слушателей. if (head == 0) {

head = malloc(sizeof(element_t));//Динамическое выделение памяти под пере head->listener = listener; // менную типа element_t.

head->next = 0; tail = head;

}

else {

tail->next = malloc(sizeof(element_t)); tail = tail->next;

tail->listener = listener; tail->next = 0; }

}

void update(int newx) { // Функция обновления x x = newx;

element_t* element = head;

while (element != 0) { // Оповещение всех зарегистрированных слушателей

(*(element->listener))(newx); element = element->next;

}

}

void print(int arg) {// Пример callback-функции оповещения. printf("%d ", arg);

}

Рис. 78. Пример программы-шаблона observer на Си

Первая строка декларирует, что notifyProcedure принадлежит к типу Си-функций с аргументом типа int, функция ничего не возвращает. Строки 2...5 декларируют struct (структура) – составной тип данных в С. Эта структура состоит из двух элементов: listener типа notifyProcedure* (указатель на функцию) и next – указатель на экземпляр этой же структуры. Строка 7 декларирует, что element_t является типом, относящимся к экземпляру структуры element. Строка 7 декларирует указатель head на список element. head инициализируется значением 0, индицирующим пустой список. Функция addListener создает первый элемент списка, используя следующий код:

1head = malloc(sizeof(element_t));

2head->listener = listener;

3head->next = 0;

4tail = head;

140