Гибридный реконфигурируемый вычислитель. Руководствo программиста.

С. С. Андреев, С. А. Дбар, А. О. Лацис, Е. А. Плоткина

Часть 2. Описание базового языка Автокод

  1. Основные черты лексики и синтаксиса.
  2. Основные термины.
  3. Разделы схемы.
  4. Константы, переменные, типы данных.
  5. Операции и операторы.
  6. Включение в схему других компонентов.
  7. Интерфейс с управляющей программой.

1. Основные черты лексики и синтаксиса

Базовый язык Автокод HDL (далее просто Автокод) представляет собой текст, состоящий из операторов. Каждый оператор Автокода записывается на отдельной строке, исключение составляет условный оператор when. Метки, операторные скобки разделов схемы, состояний, циклов и условных конструкций считаются отдельными операторами, и должны записываться на отдельной строке. Точки с запятой в конце строки нет. Признак комментария вплоть до конца строки - //. Внутри строки любая последовательность пробелов и табуляций эквивалентна одному пробелу. Допускаются строки комментариев, пустые строки, строки пробелов и табуляций.

Текст на Автокоде перед собственно трансляцией обрабатывается макропроцессором  m4, поскольку Автокод не свертывает константные выражения. Для использования в тексте программы константных выражений там, где по синтаксису Автокода требуются отдельные константы, следует использовать такие директивы m4, как define, incr (увеличить на 1), decr (уменьшить на 1), eval (свернуть произвольное константное выражение).

Текст на VHDL, в котроый транслируется схема на Автокоде, обладает рядом особенностей, которые необходимо учитывать при задании имен переменных:

  • нельзя заводить переменные, имена которых различаются только регистром: язык VHDL безразличен к регистру символов и  ADDR  и  addr — это для него одна переменная;
  • имя переменной не должно начинаться или заканчиваться знаком подчеркивания; внутри имени не более 1-го подчеркивания подряд;
  • в языке Автокод определено понятие структуры, обращение к полям которой записывается через точку (напр.  x.addra); поскольку для VHDL такая форма записи является ошибкой, то при трансляции точка заменяется на подчеркивание. Поэтому давать имена  x.addra  и  x_addra в одной схеме недопустимо.

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

2. Основные термины

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

По способу задания значения регистры делятся на два типа.

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

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

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

Комбинационная часть — часть схемы, где записываются операторы присваивания регистрам второго типа.

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

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

Со стороны исходной схемы эти регистры называются портами. Со стороны компонента — интерфейсными регистрами.

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

Платформенная разрядность — разрядность данных внешнего интерфейса основного компонента. Словами именно такой разрядности обмениваются схема и программа на управляющей ЭВМ. Возможны варианты — словами 32 бит, 64 бит и 128 бит. Конкретный выбор платформенной разрядности называется базовой платформой. Со стороны процессора выбор базовой платформы обеспечивается использованием соответствующей команды компиляции управляющей программы (fpga_compile_32, fpga_compile_64 или fpga_compile_128), а со стороны схемы — именем основного компонента (vector_proc_32, vector_proc_64 или vector_proc_128) и разрядностью портов данных в нем.

3. Разделы схемы

Разбиение схемы на разделы продиктовано спецификой описания схем: физический смысл записи операторов (напр. a = b) зависит не только от вида операторов, но и от их местоположения в схеме.

Все операторы схемы пишутся в разделах, которые обязаны следовать друг за другом в определенном порядке. Некоторые разделы могут быть пропущены. Одни разделы выделены именными скобками или именными метками, другие расположены между выделенными разделами

В общем случае структура схемы на Автокоде имеет вид:

3.1. Раздел заголовка схемы
Описание интерфейсных регистров
   — обязателен
3.2. Раздел деклараций
Описание внутренних переменных
   — обязателен
3.3 Раздел комбинационной логики
(комбинационная часть)
        Операторы
   — может отсутствовать
3.4. Раздел действий на каждом такте
(тактированная часть)
   — может отсутствовать
3.4.1 Секция начального сброса
3.4.2 Операторы
   — может отсутствовать
3.5. Раздел состояний
(тактированная часть)
        Блоки операторов
   — может отсутствовать

3.1. Заголовок схемы.

Заголовок описывает внешний интерфейс создаваемой схемы.

Имеет вид:

program  <имя компонента>
<описание интерфейсного регистра1>
……………
<описание интерфейсного регистра N >
endprogram
где program  и  endprogram — именные скобки, выделяющие заголовок.

Если создаваемая схема – это основной компонент, то заголовок (например, для 32-разрядной платформы) обязан иметь следующие порты:

program vector_proc_32
in 32 	DI           // прием массива данных в компонент через регистр DI
out 32	DO           // выдача массива данных из компонента через регистр DO
in 32 	ADDR         // адресация данных внутри массива
in 1  	WE           // сигнал разрешения записи данных с регистра DI
in 1  	EN           // сигнал разрешения чтения данных из регистра DO
in 32 	REG_IN_A     // прием одного слова на регистр A
in 32 	REG_IN_B     // прием одного слова на регистр B
out 32	REG_OUT_A    // выдача одного слова из регистра A
out 32	REG_OUT_B    // выдача одного слова из регистра B
in 2  	REG_WE_A     // 2-разрядный сигнал разрешения записи в регистр A
in 2  	REG_WE_B     // 2-разрядный сигнал разрешения записи в регистр B
in 0  	Clk          // битовый сигнал тактового генератора
in 0  	Reset        // битовый сигнал начального сброса
endprogram

Для других компонентов порты могут произвольными:

program my_component
    in   10 input    // входной регистр input разрядность 10 бит
    out  10 output   // выходной регистр output разрядность 10 бит
endprogram

В этом разделе обязательны именные скобки program <имя_схемы> и endprogram.

3.2. Раздел деклараций.

В этом разделе объявляются переменные, именованные константы, компоненты.

Имеет вид:

declare
		Объявления переменных
enddeclare

В этом разделе обязательны именные скобки declare и enddeclare. Подробно об объявлении переменных рассказывается в разделе «Переменные».

3.3. Раздел комбинационной логики.

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

a = b
в этом разделе означает: соединить навсегда выход  b  со входом  a.

Обычно в этом разделе записываются комбинационные связи между регистрами, через которые проходят потоки данных и управляющих сигналов. Разрешена запись циклов  do, операторов включения в схему компонентов  insert, операторов присваивания безусловных и условных типа  when. В операциях комбинационного присваивания переменную, стоящую слева от знака '=', в правой части этого выражения использовать нельзя.

Раздел не обязательный, но, если он есть, то следует сразу за разделом деклараций.

Тактированная часть.

Два последних раздела образуют тактированную часть схемы.

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

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

Метка:
  {
	оператор 1
	оператор 2
	    ……
	оператор N
  }

Операторами могут быть циклы do, операторы присваивания: безусловные и условные типа if. Все операторы внутри блока выполняются одновременно, результаты присваиваний будут доступны только на следующем такте, поэтому:
а) порядок записи операторов в блоке не важен;
б) дважды присвоить значение какой-либо переменной в одном блоке нельзя (кроме секции начального сброса в блоке действий на каждом такте - см. ниже).

Любой из двух разделов тактированной части не обязателен, но, если хотя бы один из них есть, то в разделе заголовка обязана быть строка:

  in    0    Clk      // битовый сигнал тактового генератора.
                      // В основном компоненте это объявление обязательно.

В чем же различия между двумя разделами?

3.4. Раздел действий на каждом такте.

Состоит из одного особого блока, поэтому имеет именную метку Background. В этом блоке записываются операторы, которые будут выполняться на каждом такте с момента запуска сигнала Clk. На этот блок и из этого блока нельзя сделать безусловный переход.

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

  in    0    Reset     // битовый сигнал начального сброса.
                       // В основном компоненте это объявление обязательно.

Хотя данная секция и записывается в тактированном блоке, все присваивания в ней происходят вне тактов: по сигналу Reset, который подается в схему один раз, сразу после прожига схемы, и еще до появления сигнала Clk. Это означает, что переменной можно присвоить значение в данном блоке дважды, если одно присваивание - в секции начального сброса, а второе - в самом разделе действий на каждом такте.

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

Секция действий по начальному сбросу имеет вид:

[ оператор ]
или
[
оператор 1
оператор 2
. . . . . .
оператор N
]

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

Общий вид раздела следующий:

 Background :
  {
    [
       Действия по начальному сбросу         // не обязателен
    ]
       Действия, выполняемые на каждом такте
  }

3.5. Раздел состояний.

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

4. Константы, переменные, типы данных

4.1. Константы.

Константы в Автокоде - это целые числа, записанные в десятичной или в расширенной двоичной системе счисления. При записи в расширенной двоичной системе счисления значениями бита могут быть: ноль (0), единица (1) и третье состояние (Z). Как и в VHDL, константы, обозначающие отдельный бит, записываются в двоичной системе счисления в одинарных кавычках, а константы, обозначающие регистр – в двойных (длина битовой последовательности должна совпадать с разрядностью регистра). Исключение составляет третье состояние, которое всегда записывается как 'Z' (как для отдельного бита, так и для регистра в целом). Константы, записанные в десятичной системе, во всех случаях пишутся без кавычек.

Примеры:

1, -18, '0', "00000000101", 'Z'

Если, к примеру, в регистр q, разрядностью 1 разряд, надо занести 1, то записать это можно следующими способами:

q = 1
или
q = "1"
или
q(0) = 1
или
q(0) = '1'

Для записи числа с плавающей точкой в Автокоде есть специальная функция

float (N, <вещественное_число>)
где N – точность числа: одинарная, двойная или четвертная (32, 64, 128 разряда, соответственно). Например:

q = float (64, 0.25)

Разумеется, разрядность регистра q в данном случае должна быть равна 64.

Константу можно объявить в разделе деклараций, как именованную:

constant 0  q  = '1'              // объявление битовой константы
constant 3  qq  = "100"           // объявление скалярной константы
constant 32  qqq  = {0, 10, -1}   // объявление векторной  константы

Далее в схеме q, qq и qqq будут фигурировать как обычные переменные, но доступные только для чтения.

4.2. Переменные.

Переменные в Автокоде - это отдельные биты, скалярные и векторные регистры, массивы многопортовой векторной памяти и переменные цикла.

4.2.1. Бит.

Переменная типа "бит" хранит 1 бит. Должна быть явно объявлена в тексте схемы как регистр. Чтобы отличить объявление бита от скалярного регистра разрядностью 1 бит, разрядность при объявлении бита указывается равной нулю. Объявление бита “a”:

reg  0  a

4.2.2. Скалярный регистр.

Скалярный регистр - это двоичное слово (одномерный массив битов) заданной разрядности. Должен быть явно объявлен в тексте схемы.

Объявление скалярного регистра "b" разрядностью 32 бита:

reg 32 b

4.2.3. Векторный регистр.

Векторный регистр - это одномерный массив заданной длины, элементы которого - слова заданной разрядности. Должен быть явно объявлен в тексте схемы.

Объявление векторного регистра "d" длиной 4 элемента, с разрядностю элемента 10 бит:

reg 10 d(4)

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

4.2.4. Векторная память.

Массив векторной памяти состоит из слов заданной при его объявлении разрядности, в заданном при его объявлении количестве, называемом размером массива. Массив разбит на слои, или блоки, равного размера, причем каждый блок - это одномерный массив слов. Количество слоев, на которые разбит массив векторной памяти, называется его векторностью, или шириной, и также задается при объявлении массива.

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

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

array.douta[0] 
означает: «регистр выходных данных (dout) порта «a» нулевого слоя массива «array». Таким образом, каждый слой (блок) памяти имеет свой набор регистров доступа, значениями которых можно управлять независимо.

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

  • регистр выходных данных (dout) доступен на чтение (может упоминаться в правой части оператора присваивания). Из этого регистра читается значение, лежащее по адресу, которому регистр адреса (addr) был равен на прошлом такте. Следовательно, чтобы получить значение из памяти, надо положить его адрес в регистр адреса, и на следующем после завершения присваивания адреса такте значение будет доступно в регистре выходных данных.
  • регистр входных данных (din) доступен на запись (может упоминаться в левой части оператора присваивания). Если значение требуется записать в некоторый адрес, его надо положить в регистр входных данных на том же такте, что и адрес - в регистр адреса. На этом же такте следует сделать равным единице регистр разрешения записи (we). На всех тактах, на которых запись не предусмотрена, регистр разрешения записи должен быть обнулен.

Таким образом, на каждом такте блок памяти:

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

Каждый блок памяти имеет несколько независимых портов (блоков регистров доступа): как минимум, «a» и «b». На одном и том же такте с регистрами разных портов можно работать независимо: читать или писать по разным адресам, например. Непредсказуем лишь эффект одновременной записи в одну и ту же ячейку блока (запишется не одно из двух значений, а «мусор»). Выбор порта определяется последней буквой в названии регистра, назначение регистра – предыдущими буквами названия.

Конкретное число и названия портов определяются типом массива (указывается при объявлении).

Массив векторной памяти должен быть явно объявлен в тексте схемы. Объявление массива векторной памяти "f" типа "ramb", шириной 4, с разрядностью слова 32 бита и размером 80 элементов (20 векторов), выглядит так:

ram 32 f(ramb, 4, 80)

Следует обратить внимание на то, что при объявлении массива векторной памяти задается его полный размер, а не размер слоя.

На сегодня существуют несколько типов векторной памяти: ramb, rams , rams2, ramd и ramd2. Подробнее о типах векторной памяти см. в "Библиотечные компоненты".

Для простейшего типа векторной памяти - ramb - предусмотрены следующие регистры доступа:

.addra, .addrb
векторные регистры адреса (размер вектора равен заданной при объявлении ширине массива, разрядность элемента вектора - 24 бита);
.dina, .dinb
векторные регистры входных данных (размер вектора равен заданной при объявлении ширине массива, разрядность элемента вектора равна заданной при объявлении разрядности слова массива);
.douta, .doutb
(размер вектора равен заданной при объявлении ширине массива, разрядность элемента вектора равна заданной при объявлении разрядности слова массива);
.wea, .web
векторные регистры разрешения записи (размер вектора равен заданной при объявлении ширине массива, разрядность элемента вектора - 1 бит).

Записанные таким образом обращения к векторным регистрам управления массивом векторной памяти ничем не отличаются синтаксически от записи обращения к обычному векторному регистру и/или его элементам.

4.2.5. Переменные цикла.

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

Имя переменной цикла имеет вид:

  @<целое_положительное_число>

Подробнее о переменных цикла см. в разделе "Безусловные операторы".

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

4.3. Типы переменных, индексы и диапазоны.

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

При указании только имени переменной, ее тип будет определяться, исходя из объявления.

Пример:

reg 32 a(4)     // вектор
reg 32 b(4)     // вектор
reg 32 h(8)     // вектор
reg 20 d(2)     // вектор
reg 32 c        // скаляр
reg  1 f        // скаляр
reg  0 g        // бит
.   .   .   .
     a = b      // правильная запись
     a = h      // неправильная запись (не совпадают длины векторов)
     a = d      // неправильная запись (не совпадают и разрядность элементов
                // и длины векторов)
     f = g      // неправильная запись (не совпадает тип регистра)
     c = f      // неправильная запись (не совпадает разрядность)
     a = f      // неправильная запись (не совпадает разрядность)
     a = c      // правильная запись

Последняя запись, a = c , является правильной. Вектору можно присваивать скаляр, это означает присваивание скаляра каждому элементу вектора.

Тип переменных можно изменить с помощью индексов и диапазонов.

4.3.1. Индексы.

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

Переменная с индексом имеет вид:

<имя>[индекс]

— для векторов

<имя>(индекс)

— для скаляров

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

       a[3] = h[7]
       c(5) = f(0)
       f(0) = g

4.3.2. Диапазоны.

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

Имеют вид:

<имя>( N1:N2)

Числа N1 и N2 - десятичные целые без знака.

Для векторов диапазон указывается от меньшего номера элемента к большему (N1 <= N2) Если N1 = N2, то диапазон интерпретируется как индекс, и тип регистра становится скаляром.

Для скаляров диапазон указывается от большего номера бита к меньшему (N1 >= N2). Если N1 = N2, то переменная все равно остается скаляром с разрядностью, равной 1.

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

c (5:5) =  f
a (0:3) =  h (4:7)
a [0](19:0) = d [1](19:0)
a (1:2)(10:0) =  d (0:1)(19:9)

5. Операции и операторы.

5.1. Операции.

5.1.1. Побитная инверсия.

"~"
имеет вид:
~<имя>
и применяется только к битам и скалярам.

5.1.2. Операции сдвига:

">>",  "<<",  ">>@ ",  "@<<";
имеют вид:
<имя> <операция> N
и применяется только к скалярам;
d >> 5
сдвиг "d" вправо на 5 разрядов;
старшие 5 разрядов — нули;
d << 5
сдвиг "d" влево на 5 разрядов;
младшие 5 разрядов — нули;
d >>@ 5
циклический сдвиг "d" вправо на 5 разрядов;
старшие 5 разрядов — младшие 5 разрядов "d";
d @<< 5
циклический сдвиг "d" влево на 5 разрядов;
младшие 5 разрядов — старшие 5 разрядов "d";

5.1.3. Логические операции

"&&",  "||",  "^";
имеют вид:
 <имя1> <операция> <имя2>
и применяется только к битам и скалярам;

a && b
— логическое "и";
a || b
— логическое "или";
a ^  b
— логическое "исключающее или";

5.1.4. Операции сравнения:

"==",  "!=",  ">",  ">=",  "<",  "<=";

Объектами операции сравнения могут быть биты, скаляры, переменные цикла и константы.

5.1.5. Арифметические операции.

"+",  "-",  "*",  "/" (деление возможно только на степень 2);

Объектами арифметических операций могут быть векторы, скаляры и константы.

Следует иметь в виду, что арифметические операции применимы только к целым числам. О вещественной арифметике будет рассказано ниже.

5.1.6. Операции присваивания.

"=",  "++",  "--",  "+=",  "-=";

5.1.7. Операции "кортеж"

{a1, a2, ..., aN}

Данная операция означает временное объединение списка переменных или констант в одну структуру для более компактной записи операторов присваивания.

5.2. Операторы.

5.2.1 Операторы присваивания.

5.2.1.1. Операторы присваивания

"++",  "--",  "+=",  "-=" имеют вид:
      <переменная>++
      <переменная>--
      <переменная>+=<число или переменная>
      <переменная>-=<число или переменная>

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

5.2.1.2. Оператор присваивания

вида:
<переменная> = <выражение>

Типы переменных справа и слева от знака "=" должны совпадать, кроме случаев, когда вектору присваивается скаляр, скалярное выражение или константа (в этом случае обязана совпадать только разрядность переменных). Слева должна стоять только одна переменная. Синтаксис оператора присваивания одинаков для всех разделов схемы: комбинационные («алгебраические») присваивания записываются так же, как и тактируемые («алгоритмические»).

Примеры:

      a = b 
      a = b << 4
      a = b + 8             // арифметическое выражение
      c = (~ d || g ) && f  // логическое выражение

5.2.1.3. Оператор присваивания

вида:
<вектор>= {a1, a2, ..., aN}
где a1 ... aN - список констант, скаляров и векторов; разрядность каждого элемента списка должна совпадать с разрядностью элемента вектора. Данная операция означает поэлементное присваивание элементов из списка элементам данного вектора.

Число элементов в списке (с учетом длины векторов, если они есть в списке) не может быть больше, чем длина вектора, либо диапазон, если он указан, но может быть меньше. В последнем случае, остатку элементов вектора присваивается последний элемент из списка.

Примеры:

      reg 10 g(8)                 // вектор
      reg 10 f(4)                 // вектор
      reg 10 b                    // скаляр
      reg 10 c                    // скаляр
      . . . . . . .
      g = { f,  b, c, 1, -2 }     // первым 4 элементам вектора g
                                  // присваивается 4 элемента вектора f, 
                                  // а оставшимся 4 элементам g 
                                  // – элементы списка b, c, 1, -2;
      g(0:6) = {b,0}              // g[0] присвоится значение скаляра b,
                                  // а остальным пяти элементам вектора g
                                  // присвоится 0.

5.2.1.4. Оператор присваивания

вида:
{ a1, a2, ..., aN } = < X >
где a1 ... aN - список переменных, разрядность каждого элемента списка должен совпадать с разрядностью X.

Возможны варианты:

  • X – вектор, тогда a1 … aN – это скаляры и/или векторы; число элементов в списке (с учетом длин векторов, если они есть) должно быть равно длине вектора X, либо диапазону, если он указан. Данная операция означает последовательное присваивание скалярам значения элементов вектора.
  • X – скаляр или константа, тогда a1 … aN – это скаляры и/или вектора. Данная операция означает присваивание каждому элементу слева значения X.
  • X – бит или битовая константа, тогда a1 … aN – это список битов. Данная операция означает присваивание каждому элементу слева значения X.

Пример:

   reg 10 a(4)                       // вектор
   reg 10 b(2)                       // вектор
   reg 10 c                          // скаляр
   reg 10 d                          // скаляр
   reg 0 g                           // бит
      .  .  .  
   { b, c, d }= a                    // что означает: b[0] = a[0], b[1] = a[1],
                                     //               c = a[2], d = a[3]
   { b , c }=  d                     // что означает: b[0] = d, b[1] = d, c = d
   { a[2](5), b[0](2), g } = '0'     // что означает: a[2](5) = '0',
                                     //               b[0](2) = '0', g = '0'

5.2.1.5. Использование функций split и join в операторах присваивания.

Данные функции позволяют выполнять операцию присваивания для переменных разных типов.

Оператор присваивания вида:

<вектор>= split (<скаляр>)

Для этого присваивания необходимо выполнение условия:

<длина вектора>*<разрядность элемента вектора> = <разрядность скаляра>

Например:

reg 10 a(4)     // вектор
reg 40 c        // скаляр
 . . . . . . .
a =  split ( c )
такая запись эквивалентна следующим присваиваниям:
a[0](9:0) = c (39:30)
a[1](9:0) = c(29:20)
a[2](9:0) = c(19:10)
a[3](9:0) = c(9:0)

Оператор присваивания вида: <скаляр>= join (<список>).

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

Например:

reg 10  a (2)    // - вектор
reg 30  c
reg 2  b
reg 0  d
 . . . . . . . . . .
c =  join(a , b ,  d , "0001110")
такая запись эквивалентна следующим присваиваниям:
c(29:20) =  a[0](9:0)
c(19:10) =  a[1](9:0)
c(9:8) = b
c(7) = d
c(6:0) = "0001110"

5.2.2. Безусловные операторы.

5.2.2.1. Оператор цикла.

Имеет вид:
      do @N = n1, n2
         операторы
      enddo
где @N - это счетчик цикла, n1 и n2 - это диапазон принимаемых им значений.

Пример:

      reg 10 g(8)   // вектор
      reg 10 f(4)   // вектор
      reg 10 b      // скаляр
      . . . . . .
      do @1=0, 3
        g[@1] = f[@1]
        g [@1+4] = b + 2
      enddo

Циклы могут быть вложенными, в этом случае переменная цикла во внутреннем цикле должна иметь другой идентификатор, напр. @2, @3 и т.д.

Операторы цикла — «пространственные», а не «временные», разворачиваются во время трансляции схемы.

5.2.2.2. Оператор перехода.

Имеет вид:
      next <имя>

Имя должно обозначать метку, которая имеет вид:

<имя>:

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

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

Пример:

loop3:
       {
         next loop3 
         a = b+1
       }

5.2.3. Условные операторы.

5.2.3.1.Оператор if

Можно использовать только в разделе действий на каждом такте и в разделе состояний.

В разделе комбинационной логики if использовать НЕЛЬЗЯ!

Имеет вид:

    if(<условие1>)
       операторы
    elsif (<условие2>)
       операторы
    elsif (<условие3>)
       операторы
     . . . . . . .
    else
       операторы
    endif

В выражении <условие> могут участвовать скаляры, отдельные биты, константы и переменные цикла (если if стоит внутри оператора цикла), связанные операциями сравнения и логическими операциями.

Ветви elsif и else могут отсутствовать. Допускаются вложенные if. Выражение if(<условие>) или elsif(<условие>) должно быть написано на одной строке.

Операторы if являются не «пространственными», а «временными» - проверка указанных в них условий происходит не во время трансляции схемы, а во время ее работы.

Пример:

      reg 10 g(4)    // вектор
      reg 10 a       // скаляр
      reg 10 c       // скаляр
      .  .  .  . 
      do @1=0, 3
         if((@1 > 0 && a(8) == '1') || g[@1] != a + 5)
             c = g[@1]
         else
             c = 15
         endif
      enddo

5.2.3.2. Оператор when

Можно использовать только в разделе комбинационной логики. В тактированной части when использовать НЕЛЬЗЯ!

Данный оператор является единственным в языке, который может занимать несколько строк.

Имеет следующий вид:

<переменная> = (<условие1>) ? <выражение1> : (<условие2>) ? <выражение2> 
: . . .
<выражение N > 

где <переменная> - это либо вектор, либо скаляр, либо бит;

<условие> в операторах when подчинено тем же правилам, что и в операторах if.

Может находиться внутри оператора цикла.

Пример:

     reg 10 g(4)
     reg 10 a 
     reg 10 b
     reg 10 c
     reg 10 d 
      .  .  .
     g[0] = (a(0:3) == 7 || b > c) ? c && d : 'Z'

6. Включение в схему других компонентов.

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

  1. Объявить компонент в разделе деклараций:
    component <имя_компонента>
  2. Если компонент не библиотечный, то в директории заголовочных файлов USERCOMPONENTS необходимо создать заголовочный файл <имя_компонента>.h, и в нем описать заголовок данного компонента. Заголовок должен быть написан на VHDL и заключен в именные скобки component <имя_компонента> end component

    Если исходный текст компонента написан на Автокоде или VHDL, то создать его заголовок и поместить в директорию USERCOMPONENTS может команда:

    extract_header <имя_файла>
  3. В разделе комбинационной логики включить соединения портов компонента с регистрами данной схемы. Если используется библиотечный компонент, то перечень его портов можно получить командой:

    component_info -h  <имя_компонента>

Вставка компонента в схему имеет следующий вид:

 insert <имя_компонента>
	.<имя_порта1>(<скалярный_регистр1>)
	.<имя_порта2>(<скалярный_регистр2>)
	       . . .
	.Clk ( Clk )       // если в компоненте используется тактированная часть
	.Reset ( Reset )   // если в компоненте используется начальный сброс
 endinsert

Если порты компонента должны подключаться к элементам векторного регистра, то вся запись помещается в оператор do.

Пример:

	reg 32 a(4)
	reg 32 b 
	reg 32 c(4)
	component my_component
	   .  .  .
	   do
	     @1=0,3
	     insert my_component
	       .P_in_A(a[@1])
	       .P_in_B(b)
	       .P_out_C(c[@1])
	       .  .  .
	       .Clk(Clk)
	       .Reset ( Reset )
	     endinsert
	   enddo

Любая арифметическая операция с вещественными числами реализована как библиотечный компонент.

7. Интерфейс с управляющей программой.

7.1. Общие замечания.

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

Как уже говорилось выше, программный интерфейс схемы представляет собой два регистра, с номерами 6 и 7, которым в схеме соответствуют регистры A и B, и диапазона адресов памяти. Программа может как читать, так и записывать значения регистров и слов диапазона адресов памяти. Регистры и слова диапазона адресов памяти имеют платформенную разрядность – 32, 64 или 128 разрядов. Выбор конкретной платформы (разрядности интерфейса) осуществляется на стороне программы путем выбора конкретной команды трансляции –

fpga_compile_32
или
fpga_compile_64
или
fpga_compile_128
Во всех случаях прототипы функций находятся в заголовочном файле comm.h, который должен быть включен в программу так:

#include <avtokod/comm.h>

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

Целочисленный тип данных платформенной разрядности определяется в этом заголовочном файле как WORD.

  • Для 32-разрядной платформы WORD – это int, а равный ему по разрядности вещественный тип данных – это float.
  • Для 64-разрядной платформы WORD – это long, а равный ему по разрядности вещественный тип данных – это double.
  • Для 128-разрядной платформы целочисленного типа данных платформенной разрядности не существует, и WORD определяется как struct { long lowpart; long highpart }, а равный ему по разрядности вещественный тип данных – это __float128 (в компиляторе gcc).

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

  1. На каждом узле запускается по 2 процесса, процессы с четными номерами инициализируют и используют ускорители номер 0, процессы с нечетными номерами – ускорители номер 1,
  2. На каждом узле запускается по 1 процессу, который инициализирует ускорители с номерами от 0 до 1 (то есть все), и работает с ними обоими, переключаясь при помощи обращений к функции «выбрать текущий ускоритель».

7.2. Базовые функции стандартной библиотеки.

7.2.1. Инициализация набора ускорителей.

int init_coprocessor( int from, int to )

Инициализировать и подготовить к использованию в данном процессе ускорители с номерами от from до to включительно. При успешном срабатывании возвращает 0. Текущим становится ускоритель с номером from. В схеме ничего не происходит.

7.2.2. Выбор текущего ускорителя.

void set_coprocessor( int  current )

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

7.2.3. Запись массива в память.

void to_coprocessor( int addr, void *data, int len )

В интерфейсный диапазон памяти, начиная со слова (WORD) номер addr, записывается массив data, длиной len слов платформенной разрядности.

В схеме len раз, для каждого из записываемых слов массива data, происходит следующее: в течение ровно одного такта сигнал WE становится равным 1, сигнал EN – нулю, в регистре ADDR появляется номер записываемого слова, в регистре DI – его значение. Во все остальное время сигнал WE равен 0, значение регистра DI не имеет смысла.

7.2.4. Чтение массива из памяти.

void from_coprocessor( int addr, void *data, int len )

Из интерфейсного диапазона памяти, начиная со слова (WORD) номер addr, данные, длиной len слов, записываются в массив data.

В схеме len раз, для каждого из читаемых слов массива data, происходит следующее: в течение как минимум одного такта сигнал EN становится равным 1, сигнал WE – нулю, в регистре ADDR появляется номер читаемого слова. То значение, которое схема на следующем такте обеспечит в регистре DO, будет прочитано программой в соответствующее слово массива data. В остальное время сигнал EN равен 0.

В отладочных целях, в основном – для отладки стандартных «системных» частей схемы, скрытых от прикладного программиста, используется функция «медленного, но надежного» чтения from_coprocessor_direct(). У нее те же аргументы, что и у from_coprocessor(), и тот же интерфейс со схемой, но она работает во много раз медленнее. Эта функция почти никогда не бывает нужна прикладному программисту. Расхождение результатов работы этих двух функций, обнаруженное при отладке, свидетельствует о серьезных разрушениях в схеме и/или в системном ПО.

7.2.5. Функции записи значения в регистр.

void to_register( int nreg, WORD val )

В течение ровно одного такта в регистре REG_WE_A (если nreg == 6) или REG_WE_B (если nreg == 7) появляется значение 1, а в регистре REG_IN_A ( REG_IN_B ) появляется значение val. В остальное время младший бит REG_WE_A (REG_WE_B) равен 0, значение REG_IN_A (REG_IN_B) не имеет смысла.

void to_register_int( int nreg, int val )

Эта функция отличается от to_register () тем, что передается значение типа int, причем используются старшие 32 разряда REG_IN_A (REG_IN_B). Для 32-разрядной платформы совпадает с to_register().

void to_register_masked( int nreg, int nbit, WORD val )

Эта функция отличается от to_register () тем, что в REG_WE_A ( REG_WE_B ) становится равным 1 бит номер nbit (считая с нуля от младшего). Если nbit == 0, то «загорается» нулевой бит (REG_WE_A (REG_WE_B) == 1, как в случае to_register ()), если же nbit == 1, то «загорается» первый бит (REG_WE_A (REG_WE_B) == 2).

void to_register_masked_int( int nreg, int nbit, int val )

Эта функция отличается от to_register_masked() тем же, чем to_register_int() от to_register().

7.2.6. Функции чтения значения из регистра.

void from_register( int reg, WORD *val ) 

Заносит по адресу val значение платформенной разрядности, помещенное схемой в REG_OUT_A (если nreg == 6) или в REG_OUT_B (если nreg == 7). Никакие управляющие сигналы при опросе указанных регистров не активируются.

void   from_register_int( int reg, int * val ) 

Отличается от from_register() тем, что передается не весь регистр, а только старшие 32 разряда. Для 32-разрядной платформы совпадает с from_register().

7.2.7. Опрос текущего астрономического времени в секундах.

double  cputime( void )
◄ Часть 1 Часть 3 ►
 
 
 
 
 
 
 
 
  Тел. +7(499)220-79-72; E-mail: inform@kiam.ru