Семантика языка описания аппаратуры HaSCoL

Сер. 10. 2012. Вып. 2

ВЕСТНИК САНКТ-ПЕТЕРБУРГСКОГО УНИВЕРСИТЕТА

УДК 004.4'422:621.3.049.771.14 О. В. Медведев

СЕМАНТИКА ЯЗЫКА ОПИСАНИЯ АППАРАТУРЫ HaSCoL

1. Введение. Согласно закону Мура, прогнозирующему прогресс в развитии технологий изготовления интегральных схем (ИС), каждые 1.5-2 года количество логических вентилей, которое удается уместить на 1 см2 площади кристалла, примерно удваивается. Этот факт не только влечет развитие многоядерных процессоров и ослабление позиций последовательного «однопроцессорного» программирования, но и предоставляет новые возможности в области разработки самих ИС.

Одна из таких возможностей - развитие программируемых логических ИС (FPGA, «гибкие кристаллы»). Эта технология позволяет применять аппаратное ускорение в десятки-сотни раз для многих задач, которые традиционно решаются чисто программными средствами (например, задачи биоинформатики [1], фильтрации XML [2]). При этом разработанный аппаратный ускоритель не нужно серийно выпускать на фабрике, а можно использовать как конфигурацию для «гибкого кристалла».

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

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

То что традиционные языки разработки цифровых синхронных ИС (VHDL, Verilog) стали, по современным меркам, слишком низкоуровневыми, широко обсуждается. Например, данным проблемам посвящен сборник статей [3], в котором представлено более десяти коммерческих и исследовательских средств, позволяющих вести разработку систем на кристалле для бытовой техники на более высоком уровне.

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

Медведев Олег Валерьевич — младший научный сотрудник Научно-исследовательского института информационных технологий Санкт-Петербургского государственного университета. Научные направления: организация высокопроизводительных вычислений за счет аппаратного ускорения, оптимизирующая компиляция. E-mail: Oleg.Medvedev@lanit-tercom.com, dours@mail.ru.

© О. В. Медведев, 2012

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

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

Более того, высокоуровневые конструкции языка имеют простые проекции в более низкоуровневые, которые, фактически, и задают их семантику. Настоящая работа посвящена описанию проекций и семантики самого низкого уровня языка.

Неформальное описание языка в том виде, в котором он сейчас применяется, частично приведено в [4,5]. Однако в данной статье описано развитие языка, с точки зрения автора, поэтому оно здесь даже синтаксически немного отличается от описаний по ссылкам.

2. Обзор литературы. К идеологии нашего подхода наиболее близки симуля-ционные среды SystemC [6] и SpecC [7]. SystemC - это библиотека для C++ с прилагаемым к ней набором методик, описывающих, как при помощи C+—+ и данной библиотеки следует выражать те или иные модели вычисления. В частности, в библиотеке реализован класс «сигнал», позволяющий вместе с макросами «процесс» и «модуль» вести описание на уровне регистровых пересылок (Register Transfer Level, RTL). Также, например, в библиотеке реализована очередь, что дает возможность делать реализацию в рамках модели Kahn Process Network. Часть проектируемого устройства также можно описывать как обычную программу на C++ с указанием конкретных длин типов данных в битах или без этого. Также можно относительно бесшовно соединять модели с разных уровней абстракции, поскольку все они представлены программами на C+—Ъ

Авторы SystemC предлагают использовать его следующим образом: сначала вся система (как программная часть, так и аппаратная) моделируется обычной программой на C+—Ъ После проверки функциональной корректности модели те части, которые надо реализовать аппаратно, переписываются вручную на более низкие уровни абстракции вплоть до RTL. SystemC представляется лишь как средство для симуляции, никаких предложений по автоматической генерации аппаратуры из SystemC не делается. Не даются и средства оценки производительности получаемых ИС, поэтому разбиение на программную и аппаратную части приходится делать «на глазок».

Достоинством предлагаемой системы является возможность автоматической генерации синтезируемого описания на VHDL, т. е. генерации аппаратуры, недостатком - более ограниченный набор абстракций, выражаемых в нашем языке, в частности, отсутствие поддержки C++ с динамической памятью и полностью последовательной семантикой.

Существенный недостаток SystemC - необходимость выражать концепции разных моделей вычисления в терминах C+—+ (классы, шаблоны) и правильно интерпретировать ошибки компиляции C++, а также отлаживать ошибки времени исполнения, возникшие из-за особенностей окружения моделирования, а не моделируемого устройства.

Очевидно, что из описания на SystemC уровня RTL возможно породить аппаратуру (например, переведя его в VHDL RTL). Существуют средства, которые это делают (в частности, Cynthesizer [8]). Само по себе это не было бы ценно, поскольку есть более удобные и очень распространенные языки для описания RTL (VHDL/Verilog). Ценность

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

Cynthesizer также позволяет задавать явные директивы распараллеливания, такие как «развернуть цикл», «конвейеризовать цикл», что очень полезно для упрощения аппаратной реализации программ на С.

SpecC представляет собой C+—Ь, расширенный моделью событий (поток может ждать нескольких событий и просыпается по возникновении любого из них), а также возможностями порождать потоки. В отличие от SystemC, данный подход добавляет к синтаксису Си дополнительные конструкции, позволяющие удобнее выражать параллельное и конвейерное исполнение явно. Этим он очень похож на предлагаемый нами. Разница состоит в том, что мы строим все синтаксические расширения поверх модели, отражающей функционирование синхронного аппаратного устройства, что очень удобно, поскольку почти вся современная вычислительная техника представляет собой именно схемы, работающие под управлением тактового сигнала. В SpecC, основанном на асинхронном обмене событиями, синхронность и тактовый сигнал надо выражать явно.

3. Синтаксис и семантика базового уровня. Здесь описана семантика самого низкого уровня нашего языка. Также описана семантика модели, называемой «RTL» (Register Transfer Level), в которую транслируются описания с нижнего уровня нашего языка.

3.1. Цифровой синхронный вычислитель. Такое устройство имеет несколько входных и выходных ножек (или сигналов), каждый из которых способен представлять однобитовое значение. Через специальную ножку подается тактовый импульс, который меняет свое значение периодически. Промежуток времени между соседними переходами значения тактового сигнала из 0 в 1 называется тактом. Значения всех сигналов в течение такта можно считать неизменными.

Пусть insi, i ^ 0, - ряд значений входных сигналов (insi - битовый вектор), outsi -значения выходов, regsi - значения вектора внутреннего состояния устройства, init -вектор начального внутреннего состояния. Тогда устройство работает по правилу

outsi = f (regsi, insi), i ^ 0, regsi+i = g(regsi,insi), i > 0, regso = init,

где f, g - функции. Время дискретно, момент времени задается числом i G N U {0}. Работу инженера, создающего конкретное устройство, можно условно представить как выбор разрядности векторов ins, regs, outs и функций f, g.

3.2. Register Transfer Level. На практике цифровые синхронные ИС описываются на специальных языках в терминах «регистровой пересылки». Подобные языки позволяют группировать отдельные входные-выходные биты в «сигналы», а отдельные биты внутреннего состояния - в «регистры». Также допустимо создавать внутренние сигналы, благодаря чему функции f и g можно разумным образом представить в виде композиций элементарных функций (арифметики, булевой логики, пользовательских функций).

Модель вычисления RTL удобнее представить в виде ориентированного графа без циклов (V,E). Каждый входной сигнал соответствует какому-то корню этого графа, каждый выходной - листу, каждый регистр - одному корню (значение регистра на такте к) и одному листу (новое значение того же регистра - на такте к + 1). У каждого листа может быть только одно входящее ребро. Функция w: E ^ N задает ширину ребер в битах. Функция name: E ^ Name задает имена ребер, где Name - счетное линейно-упорядоченное множество. При этом все входящие ребра любой вершины должны иметь разные имена. Все исходящие из одной вершины ребра имеют одинаковую ширину и одно имя. Каждой внутренней вершине v соответствует функция fv: Bw(ei) х ••• х Bw(ek) ^ Bw(outE), где ei ,...,en - упорядоченный по именам набор входящих в v ребер, а outE - любое из исходящих, а через B здесь и далее обозначаем {0,1}. Таким образом, каждая вершина моделирует вычисление значения одного сигнала и распространение его всем, кто читает его. Модель запрещает различным вершинам вычислять значение одного сигнала, чтобы не сталкиваться с конфликтами.

Работа одного такта устройства моделируется в два этапа. Сначала исходящим ребрам корней присваиваются текущие значения регистров и входов ИС. Далее в порядке топологической сортировки применяются функции вершин fv - по входящим ребрам вершины вычисляются исходящие. Значения входящих ребер листьев принимаются значениями выходов и новыми значениями регистров.

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

Определение 1. В дальнейшем подобный граф, построенный для устройства, мы будем называть RTL-графом данного устройства.

Определение 2. Значением RTL-графа устройства мы будем называть соответствующие функции f, g.

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

Рис. 1. RTL-граф

На рис. 1 в рамках рассматриваемой модели представлено устройство, вычисляющее сумму произведений двух входящих рядов чисел Ai, Bi (и выдающее на выход текущее значение). Оно содержит регистр Accum, два функциональных блока - сумматор и умножитель, входы (A, B), выход (Out).

3.3. Синтаксис базового языка. Базовый уровень нашего языка отличается от языка для задания RTL-графов лишь тем, что каждый сигнал снабжается дополнительным управляющим битом, который показывает, есть ли в данном сигнале осмысленное значение. Таким образом, у нас получается модель обмена сообщениями - на каждом такте каждый функциональный блок либо посылает какое-то сообщение (управляющий бит равен 1), либо нет. Синтаксис базового уровня (BNF) таков:

устройство ::= (регистр | описание канала | обработчик)* регистр ::= "reg" имя ":" тип "=" начальное значение ";" канал ::= (" local" | " in" | "out") имя "(" тип ")" ";" обработчик ::= "{" оператор "}" простой оператор ::= "skip" |

"inform" имя канала "(" выражение? ")" | имя регистра ":=" выражение параллельная композиция ::= простой оператор ( "|" простой оператор )* оператор ::=

параллельная композиция |

" if" условие "then" оператор [" else" оператор ] " fi" условие ::= элементарное условие ("and" элементарное условие)* элементарное условие ::= ожидание сообщения "|"

выражение ожидание сообщения ::= имя канала "(" имя параметра? ")"

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

Здесь не приведен синтаксис описания типов (нетерминал тип). Скажем лишь, что в качестве типов можно использовать битовые вектора, а также целые значения, состоящие из любого количества битов, структуры, массивы, кортежи.

Все остальные неопределенные нетерминалы (имена разных объектов) являются идентификаторами.

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

Оператор inform задает посылку в канал, := - присвоение в регистр, skip - пустой оператор, а | обозначает параллельную композицию. Синтаксис запрещает посылать сообщение в определенный канал, равно как и присваивать новое значение в определенный регистр более чем в одном обработчике.

3.4. Семантика базового языка. Сначала зададим семантику для программ на базовом языке, у которых в каждом операторе присваивается только один регистр либо посылается сообщение лишь в один канал (возможно, несколько раз, в разных

ветках вложенных if-then-else). По такой программе создается RTL-граф следующим образом:

• регистрам и входным/выходным каналам ставятся в соответствие корни/листья графа;

• обработчику ставится в соответствие внутренняя вершина, причем, если он читает какой-либо регистр либо слушает канал, то проводится ребро от регистра/канала к обработчику; если же он пишет в регистр либо шлет сообщение в канал, то проводится соответствующее исходящее ребро. При этом имя ребра равно имени регистра/канала;

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

Функция fv на вершине v, соответствующей обработчику, задается его кодом:

• в условном операторе вычисляется условие (при этом значение конструкции ожидание сообщения для канала с именем X принимается истинным тогда и только тогда, когда управляющий бит входного ребра с именем X равен 1). Таким образом, выясняется, какая из веток выполняется в данном такте. В случае, если это ветка «then», именам параметров сообщений из каналов, упомянутых в условии, ставятся в соответствие значения параметров;

• если исполняемый оператор есть skip, то контрольный бит выходных ребер устанавливается в 0. Иначе контрольный бит устанавливается в 1, а биты данных -в правую часть присваивания, если оператор является присваиванием, либо в значение параметра, посылаемого в канал оператором inform.

Данные, идущие из корней-регистров, равны содержимому регистров, контрольный бит всегда единичен.

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

Если в каком-либо обработчике происходит посылка/присваивание в разные каналы/регистры, то семантика такого обработчика задается путем разбиения его на несколько независимых. У каждого из них такая же структура вложенных «if-then-else», как и у исходного, но все операции только с одним из каналов/регистров.

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

in a(integer(16)); in b(integer(16)); reg accum : integer(40) = 0;

{ if a(x) and b(y) then inform accumulate(x * y) fi } local accumulate (integer (32));

{ if accumulate (x) then accum := accum + x fi }

3.5. Расширение семантики для управляющих сигналов. Заметим, что в синтаксисе, описанном в п. 3.3, можно написать код, не имеющий семантики, согласно п. 3.4. Например, устройству

local a(integer (10)); { if a(x) then inform a(x +1) fi }

соответствует ИГЬ-граф с циклом - обработчик пересылает сам себе сообщения в одном и том же такте неограниченное число раз. Вместе с тем во всех практических приложениях время, занимаемое исполнением одного такта вычислений, должно быть не только определяемым статически, но и весьма коротким (порядка наносекунд).

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

Рассмотрим ИГЬ-граф некоторого устройства Со — (Vо,Eо), в котором есть циклы. Сначала отделим зависимости по управляющим сигналам от остальных зависимостей. Для этого каждую внутреннюю вершину разобьем на две. Одна будет вычислять управляющий бит, соответствующий ребрам, исходящим из исходной вершины, а другая - все остальные биты. Для корневых вершин также заведем дублеров, выдающих управляющий бит. Проведем новые ребра в соответствии с зависимостями (например, значение исходящих битов данных может не зависеть от значений входящих контрольных битов). Назовем новый граф С — (V, Е), где Е — ЕС0П(Г01 ^Еааы - в графе появилось в явном виде множество управляющих ребер. Заметим, что для ациклических графов их значения (в смысле определения 3.2) до и после такого преобразования совпадают.

Пусть Еа11СуС1е.э - множество ребер всех сильно связных компонент С и пусть Еа11СуС1ев С ЕС0гЛг01. Иными словами, любой цикл проходит только по управляющим ребрам.

Рассмотрим произвольную компоненту сильной связности С ^ С. Пусть X - вектор значений всех внутренних ребер С, а I, О - вектора значений ребер, входящих и выходящих из компоненты. Функции, написанные на вершинах компоненты, задают уравнение X — Е(Х,1) на значения внутренних ребер, а также О — С(Х,1), которое позволяет посчитать выходы, зная значения входов и внутренних ребер. Таким образом, чтобы посчитать выходы С по входам, достаточно решить уравнение на X.

Пусть У1 (а ^ Ь) ^ Е(а, I) ^ Е(Ь, I), где порядок на битовых векторах задан покомпонентно, т. е. Е монотонна по X.

Лемма 1. Пусть Е : Вп ^ Вп, Уа,Ь(а > Ь ^ Е(а) > Е(Ь)). Тогда ЗX' : (X' — Е(X') А УУ(У — Е(У) ^ X' > У)).

Доказательство. Рассмотрим последовательность XI, г ^ 0: Xо — (1,...,1), Xj+l — Е(XI). Докажем, что XI ^ Xi+l по индукции. Очевидно, Xо - наибольший элемент множества битовых векторов, поэтому Xо ^ XI, что является базой индукции для г — 0. Пусть Xi-1 ^ XI. Тогда по монотонности Е верно Е(Xi-1) ^ Е(XI), т. е. XI — Е(Xi-l) ^ Е— Xi+l. Поскольку последовательность Xi убывает, а ее элементы принадлежат конечному множеству, Зг : Xi — Xi+l. Возьмем первое такое г и положим X' — X.,. Заметим, что X' — X— Xi+1 — Е(X').

Пусть теперь У — Е(У). Докажем, что УiXi ^ У по индукции. База для г — 0 очевидна. Пусть Xi ^ У. Тогда Е^^ ^ Е(У) по монотонности, т. е. Xi+1 — Е^^ ^ Е (У) — У.

Таким образом, для С всегда можно положить О — С(^,1), где X - наибольшее решение уравнения X — Е(X, I), если Е монотонна. Вспомним, что X - вектор из значений управляющих сигналов, и каждый сигнал занимает 1 компоненту вектора, причем единичное значение сигнала означает наличие в соответствующем канале сообщения. Тогда в выборе наибольшего решения системы есть смысл - мы выбираем такое решение, которое передает наибольшее множество сообщений.

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

циклов, на каждой вершине которого написана функция. Несложно определить функцию и для такого графа в целом.

Заметим, что старая семантика для графов без циклов согласуется с новой.

4. Иерархия структурных блоков. Для удобства изложения проекций в следующих пунктах, мы расширим базовый язык возможностью создания устройств путем соединения других устройств. Синтаксис расширения таков:

блок ::= "begin"

подблок*

устройство "end"

подблок ::= "process" имя процесса (блок | имя процесса) "with" соединение* ";" соединение ::= имя канала подблока "=" имя канала объемлющего блока

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

5. Семантика уровня транзакций. Представим расширение языка, которое добавляет возможность надежной (блокирующей) посылки сообщения при помощи конструкции send. Обработчик, посылающий сообщение надежным образом, будет блокироваться на те такты, во время которых получатель не готов принять сообщение. Будет описан протокол, при помощи которого получатель и приемник договариваются о передаче сообщения. Также все операторы языка будут снабжены новой семантикой (с учетом возможности блокировки), описанной в терминах базового языка.

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

5.1. Протокол многотактовой транзакции. На конструкции передачи сообщений из базового языка основывается протокол надежной передачи сообщений. Каналы на задаваемом уровне языка являются составными. Для каждого канала c(T) неявно объявляются 3 подканала: c.data(T) - основной для передачи сообщения, а также вспомогательные c.ready и c.commit. Последние два идут в обратном направлении, т. е. те, кто ждут сообщения из c.data, посылают сообщения в c.ready, c.commit, и наоборот.

Транзакция начинается в том такте, в котором приемник посылает сообщение в c.ready, а передатчик посылает сообщение в c.data. После этого на каждом такте до такта окончания транзакции включительно передатчик должен посылать в c.data то же самое сообщение. В последнем такте транзакции приемник посылает сообщение в c.commit.

Если передатчик посылает сообщение в c в том такте, в котором транзакция не происходит, то приемник его не обрабатывает (т. е. сообщение теряется, игнорируется).

Транзакция может быть однотактовой - тогда приемник пошлет c.ready и c.commit одновременно (именно для этого введены два отдельных канала).

Пример обмена в рамках протокола изображен в виде MSC диаграммы на рис. 2. Сообщение M1 будет потеряно, а M2 - передано в процессе двухтактовой транзакции.

Рис. 2. Надежная доставка сообщения

Условие на поддержание канала c.data в одном и том же состоянии в течение транзакции вводится для поддержания атомарности, упомянутой в начале п. 5.

5.2. Синтаксис. Расширим синтаксис оператора следующим образом:

простой оператор :: = "skip" |

"inform" имя канала "(" выражение ")" | "send" имя канала "(" выражение ")" | имя регистра ":=" выражение оператор ::= простой оператор |

"if" условие "then" оператор (" else" оператор)? " fi" | оператор ("|" | "=>") оператор

Также снимается ограничение на посылки в канал из разных обработчиков.

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

Вот содержимое блока, реализующего оператор inform channel(expr) (где parameters - все данные, необходимые для вычисления expr, кроме регистров и констант) :

{ if control(parameters) then

inform channel. data

fi

}

Аналог для send channel(expr) (в отличие от inform - не игнорирует сигнальные сообщения, управляющие транзакцией):

{ if inControl(parameters) then inform channel.data(expr) fi } { if channel.ready () then inform control.ready () fi } { if channel. commit () then inform control. commit () fi }

5.4. Ожидание канала. Пусть дан оператор ожидания сообщений из каналов такого вида:

if ci (pi) and c2(p2) ... and ck(pk) and expr then S1 else S2 fi

Через proc(S1), proc(S2) обозначим реализации подоператоров в виде отдельных блоков. Тогда реализация всего оператора такова:

process Keepi = Keep with inp = ci, outp = ci ';

process Keepk = Keep with inp = ck, outp = ck ';

reg elseTransaction : bool = false ;

reg thenTransaction : bool = false ; {

if cl '(pi) and c2 '(p2) ... and ck '(pk) and expr and not elseTransaction then if control. data(pctrl) or thenTransaction then inform Si. control. data ((pctrl, pi , ... , pk )) | if S2. control.rdy () and S2. control. commit () then

inform cl '.rdy

| inform cl '.commit () | ... | inform ck '.commit () | inform control. commit () else

if S2. control.rdy () then thenTransaction := true | inform cl '.rdy () | ... | inform ck '.rdy () | inform control. rdy () else

if S2.control.commit() then thenTransaction := false

| inform cl '.commit () | ... | inform ck '.commit () | inform control. commit () fi fi fi

fi

else

if control. data (pctrl ) then

inform S2. control. data (pctrl) | if S2. control.rdy () and S2. control. commit () then

inform conrol. rdy () | inform control. commit () else if S2. control.rdy () then inform control.rdy () | elseTransaction := true else if S2. control. commit () then inform control. commit () | elseTransaction := false fi fi fi

fi

fi

}

process Sl = proc(Sl); process S2 = proc(S2);

В этом коде cl', ..., ck' - локальные каналы с теми же типами параметров, что и входные cl, ... .

Блок типа Keep следит за тем, чтобы данные, посылаемые в выходной канал (ci' в этом случае) не менялись в течение всей транзакции, т. е. поддерживает атомарность. Все изменения входа во время транзакции блок игнорирует. Реализация блока также выполнена на языке базового уровня.

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

Смысл основного обработчика в приведенной выше реализации состоит в том, что транзакция с каналами c* начинается в том такте, когда в каждый из них кто-то шлет по сообщению и блок S1 готов начать исполняться, а заканчивается, когда S1 закончил исполняться. Кроме того, если else ветка вошла в транзакцию, то в then ветку войти нельзя, и каналы c* не принимают сообщений, пока транзакция не закончится.

5.5. Энергичная параллельная композиция. Зададим семантику для оператора S, равного S1 | S2. «Энергичность» S выражается в том, что он начинает обрабатывать входное сообщение как только хоть один из S1, S2 сигнализирует о том, что он начал обрабатывать свою часть. Заканчивается обработка, соответственно, когда самый медленный из пары ее заканчивает.

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

5.6. Цепная композиция. Она задается синтаксисом S1 => S2 и активирует S2 только в том такте, в котором S1 готов завершить транзакцию. Заметим, что если, например, обе части цепной композиции исполняются ровно за один такт, то цепная композиция оказывается эквивалентна энергичной параллельной композиции.

5.7. Конфликты по каналам. Описываемый уровень допускает посылку несколькими обработчиками разных сообщений в один и тот же канал в одном и том же такте. Это приводит к необходимости осуществлять арбитраж - выбирать одно из сообщений, которое будет пропущено в канал, и задерживать все остальные.

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

новый канал ::= канал "[" имя порта ("," имя порта )* "]" ";"

В синтаксис оператора посылки вводится возможность указывать порт канала, в который посылается сообщение:

оператор посылки ::= ("inform" | "send") имя канала "'" имя порта "(" выражение ")"

В реализации для каждого порта p канала c заводится по дополнительному каналу c_p, в который и осуществляются посылки. Все дополнительные каналы ведут в блок-арбитр, выбирающий наиболее высокоприоритетный при помощи конструкции типа if c_p1(x) then send c(x) else if c_p2(x) then send c(x) ... .

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

5.8. Регистры без ограничений. Чтобы ввести регистры, в которые можно присваивать из разных обработчиков, а также читать, ожидая атомарности, описанной в п. 5.4, выразим их через «старые» регистры, окружив последние прокладками из каналов и обработчиков. Новые регистры будут вводиться ключевым словом data и иметь при себе список портов:

глобальная переменная ::= "data" имя

"[" имя порта ("," имя порта )* "]" ":" тип ("=" выражение)? ";"

Реализация переменной data X [P1, ..., Pk] : T = value; такова:

reg X : T = value; in setX(T)[P1, ... , Pk];

{ if setx(x) then X := x fi | inform getX(X) } out getX(T);

При этом в остальной программе X := expr будет заменяться на inform setX(expr), а оператор S, который читает X, - на if getX(X) then S fi .

6. Уровень конвейеров. Расширим синтаксис и семантику операторов до конструкций, которые могут выполняться несколько тактов в конвейерном режиме (конвейерный оператор на каждом такте принимает новые данные на обработку и выдает результат очередных вычислений). Семантика новых конструкций задается в терминах предыдущего уровня языка:

6.1. Синтаксис.

локальная переменная ::= имя "=" выражение

список переменных ::= локальная переменная ("," локальная переменная)* оператор ::=

простой оператор | локальная переменная |

" if" условие "then" оператор " else" оператор " fi " | оператор бинарный конструктор оператор |

"for" "(" список переменных ";" выражение ";" список переменных ")" оператор бинарный конструктор ::= "|" | "=>" | ";"

То есть мы ввели последовательную композицию вида S1; S2, создающую конвейер, а также разрешили применять остальные виды композиции к многотактовым операторам. Кроме того, появилась необходимость передавать данные между стадиями конвейера, работающими в разных тактах. Для этого применяются так называемые «локальные переменные», которые вводятся определением, в котором указано имя и значение переменной. Этой переменной можно пользоваться в следующих стадиях конвейера.

6.2. Семантика базовых конструкций. Каждый оператор, по аналогии с предыдущим уровнем языка, будет выражаться отдельным блоком, но кроме входного канала inControl для активации блока, у каждого из них будет также выходной канал outControl. Последний нужен для того, чтобы сообщать о том, что конвейер обработал очередную порцию данных, а также выдавать результат. Поскольку разные конвейеры выдают различные результаты, а при их композиции результаты, идущие в общий outControl, тоже надо будет как-то объединить, то для каждой синтаксической конструкции, кроме семантики, будет задаваться множество параметров, передаваемых в outControl. Каждый параметр при этом будет характеризоваться парой (имя, тип).

Вот блок, реализующий оператор send channel(expr):

{ if inControl (x) then send channel (expr) | send outControl (x) fi}

Для операторов send и inform выходное множество параметров совпадает со входным.

Объявление локальной переменной name = expr: { if inControl(xl) then send outControl(xO, expr) fi }

Здесь выходное множество параметров O = I U (name,T), где I - входное множество, T - тип выражения expr. Через xO обозначается кортеж xI, из которого убрана переменная, совпадающая по имени с объявляемой, если такая была.

Пусть дан оператор ожидания сообщений из каналов такого вида if Cond then S1 else S2 fi.

Реализуется он как

{ if Cond and inControl(x) then send S1. inControl(x) else send S2. inControl(x) fi} process S1 = proc(S1) with outControl = outControl' A; process S2 = proc(S2) with outControl = outControl' B; out outControl (T) [ A, B ];

Выходное множество O = I then Gleise - пересечение выходных множеств веток условного оператора.

Конструкция S1 | S2 реализуется как параллельное исполнение обоих операторов с синхронизацией по окончании работы. Выходное множество O = (Isi UIs2)\(Isi П1^2).

6.3. Конвейерная композиция. Конструкция S1; S2 реализуется как

reg buf : T;

data bufFull[A, B] : bool = false; { if not bufFull then inform buflsEmpty() fi } process S1 = proc(S1) with inControl = inControl; process S2 = proc(S2) with outControl = outControl; { if S1. outControl(x) and buflsEmpty () then buf := x | bufFull'A := true

fi

}

{ if bufFull then

send S2. inControl (buf) => { bufFull' B := false | inform buflsEmpty () }

fi

}

где T - тип управляющего сообщения, которое передается от S1 к S2. Выходное множество конструкции совпадает с выходным множеством S2.

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

6.4. Цикл «for». Он аналогичен циклу из C+—Ъ Первый список локальных переменных в заголовке задает начальные значения для цикла в том такте, в котором цикл активизирован. Тело начинает исполняться в том же такте, получив начальные значения, если условие (второе выражение в скобках) истинно. Если оно ложно, то цикл посылает сообщение об окончании работы. После окончания работы тела оно запускается заново на переменных, заданных во втором списке заголовка. Это происходит в том же такте, в котором оно закончило работу, потому тело должно занимать хотя бы 1 такт.

Пример: for (i = 0; i < 5; i = i + 1) { a[i] := a[i] + x; skip }

Для цикла общего вида for (init; condition; newValues) S имеем

process Init = proc( init) with inControl = inControl, outControl = tryRun' B; local tryRun(T)[A, B];

{

if tryRun(x) then

if condition then send S. inControl (x) else send S. outControl (x) fi

fi

}

process S = proc (S) with outControl = newValues . inControl; process newValues = proc(newValues) with outControl = tryRun'A;

7. Практическое применение. В статье описана семантика языка, который реализован в пакете под общим названием «coolkit» и доступен для скачивания по адресу http://oops.math.spbu.ru/projects/coolkit. Пакет содержит транслятор из данного языка в синтезируемый VHDL, который далее можно использовать в существующих средствах автоматизированного проектирования ИС. Существующий транслятор нацелен в первую очередь на использование с ПЛИС фирмы Xilinx.

Данная статья представляет небольшое развитие языка: в существующем инструменте вместо цикла for реализован цикл while (различие в том, что while не позволяет объявлять в своем заголовке локальные переменные и их приходится инициализировать за один такт до старта первой итерации цикла). Также пока не поддержана возможность явно указывать приоритеты портов глобальных переменных (data) и неявно использовать комбинационные циклы по сигналам управления (семантика которых задана в п. 3.5).

Транслятор был использован как для реализации некоторых научных ([5, 9]) и образовательных (http://code.google.eom/p/hascolplayground/) проектов, так и в коммерческом проекте (реализация клона RISC процессора Xilinx Microblaze).

Последнее применение было наиболее значительным, ему посвящены статьи [10, 11]. Удалось реализовать клон, отличающийся от оригинала фирмы Xilinx версии 2011 года не более чем на 50% по скорости и ресурсоемкости. Сложность проекта примерно характеризуется тем, что на процессоре успешно работает ОС Linux Snapgear с ядром 2.6. Далее мы кратко опишем, как возможности языка использовались в процессоре.

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

2. Не менее полезна конструкция ожидания сообщений из нескольких каналов (в описанной семантике это конструкция вида if channell (paramll, param12) and channel2 (...) ... , в текущей версии транслятора такая конструкция может быть только самой внешней в теле обработчика). Например, для инструкции, которая работает с внешней шиной, последняя стадия конвейера является, фактически, барьером синхронизации, который не позволяет последующим инструкциям сохранить результаты выполнения, пока не придет ответ от шины для предыдущей. Код барьера выглядит примерно так:

if fromMMU(readResult) — в этот канал приходит ответ шины and WaitMMU(rNum) — сюда приходит сообщение с номером регистра

от предыдущей стадии конвейера

then

если ответ шины — не исключение, происходит запись нового значения регистра

3. Некоторые ручные оптимизации на базовом уровне языка используются в блоке виртуальной памяти. Там send выражается вручную через inform, чтобы избежать генерации ненужных блоков типа Keep (п. 5.4), которые не убираются оптимизацией, реализованной в трансляторе.

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

5. Также применялись возможность явного разрешения конфликтов доступа к каналу и «энергичность» параллельной композиции операторов (п. 5.5).

Большее количество примеров кода процессора можно увидеть в [10]. Также по адресу http://code.google.eom/p/hascolplayground/wiki/MiniTutorial можно прочитать краткое введение в язык с несколькими простыми, но зато законченными примерами.

8. Заключение. В настоящей работе приведена семантика многоуровневого языка описания синхронных цифровых интегральных схем. Язык уже был реализован ранее (см. [4]). При помощи этой реализации был создан клон процессора Microblaze для встроенных систем. Также на данном языке было реализовано устройство для поиска сетевых атак в потоке TCP соединений [5]. Такие факты позволяют говорить о хороших шансах языка на применимость на практике, однако семантика текущей реализации никогда не была описана в явном виде, а некоторые конструкции были реализованы как заплатки к предыдущей версии. Это привело к неортогональности некоторых операторов языка, а также к неожиданному поведению некоторых программ. Кроме того, в процессе реализации процессора были накоплены некоторые пожелания к языку, которые необходимо было учесть.

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

Литература

1. Li I., Shum W., Truong K. 160-fold acceleration of the Smith-Waterman algorithm using a field programmable gate array (FPGA) // BMC Bioinformatics. 2007. Vol. 8, N 1. P. 185 (also URL: http://www.biomedcentral.com/1471-2105/8/185).

2. Mitra A., Vieira M. R., Bakalov P. e. a. Boosting XML filtering through a scalable FPGA-based architecture // CIDR. www.crdrdb.org, 2009.

3. Coussy P., Morawiec A. High-Level Synthesis: from Algorithm to Digital Circuit. 1st ed. Berlin e. a.: Springer Publishing Company, Incorporated, 2008. 300 p.

4. Boulytchev D., Medvedev O. Hardware Description Language Based on Message Passing and Implicit Pipelining // East-West Design and Test (EWDTS), 2009. 7th IEEE Intern. Symposium. 2009. P. 279-282.

5. Medvedev O., Posov I. Using hardware-software codesign language to implement CANSCID // Formal Methods and Models for Codesign (MEMOCODE), 2010. 8th IEEE/ACM Intern. Conference. 2010. P. 85-88.

6. Thorsten Grotker G. M., Stan Liao, Swan S. System Design with SystemC. Kluwer Acad. Publ., 2002. 240 p.

7. Zhu J., Domer R., Gajski D. D. Syntax and Semantics of the SpecC Language // Proc. of the SASIMI Workshop. 1997. P. 75-82.

8. Meredith M. High-Level SystemC Synthesis with Forte's Cynthesizer // High-Level Synthesis / ed. by P. Coussy, A. Morawiec. Springer Netherlands, 2008. P. 75-97.

9. Medvedev O. Accelerating multiple alignment on FPGA with a high-level hardware description language // Central & Eastern European Software Engineering Conference in Russia. 2012 (to published).

10. Медведев О. Обзор высокоуровневого языка разработки аппаратуры HaSCoL на примере клона процессора Xilinx Microblaze // Вторая науч.-техн. конференция молодых специалистов «Старт в будущее». 2011. P. 231-234.

11. Медведев О. Use case: отладка реализации RISC процессора для FPGA // Материалы второй межвузовской науч. конференции по проблемам информатики СПИСОК-2011. 2011. P. 7-12.

Статья рекомендована к печати проф. А. Н. Тереховым. Статья принята к печати 28 февраля 2012 г.