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

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

Руководство программиста.
Часть 1. Базовое подмножество языка.

Введение.

Гибридный реконфигурируемый вычислитель – это универсальная ЭВМ, снабженная реконфигурируемым вычислительным модулем на базе ПЛИС. Вычислительная задача в целом решается на универсальной ЭВМ, некоторые вычислительно емкие фрагменты для ускорения счета реализуются в реконфигурируемом вычислительном модуле. Для прикладного программиста использование реконфигурируемого модуля обычно выглядит, после выполнения необходимых подготовительных шагов, как обращение к функции (подпрограмме).

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

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

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

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

В настоящем документе описывается базовое подмножество Автокода. Более сложные и мощные возможности языка описаны в Части 3 Руководства.

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

Начало схемы примера Х:

...

Конец схемы примера Х.

Примеры занумерованы подряд от 1.

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

Первая схема на Автокоде (пример №1).

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

С точки зрения программы, выполняющейся на универсальном процессоре, сопроцессор выглядит как набор регистров и областей памяти. Регистры соответствуют одиночным параметрам, значениями которых обмениваются программа и схема, области памяти – параметрам-массивам. В выбранном на сегодня конкретном варианте системы связи программы с сопроцессором регистров два, область памяти – одна. Ширина регистра соответствует типу данных int, ширина слова памяти – тоже, размер области памяти – 16384 слова.

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

void to_register( int nreg, int val )
– записать значение val в регистр сопроцессора номер nreg,
void from_register( int nreg, int *val )
– прочитать по адресу val значение из регистра сопроцессора номер nreg.
void to_coprocessor( int offs, int *arr, int leng )
– записать целочисленный массив arr длиной leng в область памяти сопроцессора, с целочисленным смещением offs,
void from_coprocessor( int offs, int *arr, int leng )
– прочитать данные из области памяти сопроцессора, с целочисленным смещением offs, длиной leng целых чисел, в массив arr.

Как выглядит этот интерфейс на стороне сопроцессора, то есть из текста схемы на Автокоде?

Всякая схема на Автокоде начинается с заголовка, описывающего внешний интерфейс. Подобно тому, как любая программа на C начинается с

int main( int argc, char *argv[] ) {

Схема на Автокоде имеет стандартный заголовок, вид и смысл которого определяется соглашениями о внешнем интерфейсе. Заголовок этот имеет вид:

program vector_proc_32
out 32 DO
in 32 ADDR
in 32 DI
in 1 EN
in 1 WE
in 32 REG_IN_A
in 32 REG_IN_B
out 32 REG_OUT_A
out 32 REG_OUT_B
in 1 REG_WE_A
in 1 REG_WE_B
in 0 Clk
in 0 Reset
endprogram

Каждая строка этого текста определяет именованный программный объект, похожий на формальный параметр процедуры в традиционных языках. Слово «in» означает, что параметр входной, «out» - что выходной. Параметры имеют имена и являются целочисленными. Число перед именем означает ширину значения в битах. Параметры Clk и Reset, ширина которых указана равной нулю, имеют специальный смысл. Их указание в заголовке обязательно, но в последующем тексте их упоминание может отсутствовать. Пока будем рассматривать их как «заклинание».

Параметры DO, ADDR, DI, EN и WE связаны с реализацией видимой основному процессору области памяти, остальные – с регистрами A и B, соответственно. При этом регистру A на стороне процессора соответствует номер 6, регистру B – номер 7.

Рассмотрим, что происходит со значениями REG_IN_A, REG_WE_A и REG_OUT_A, когда программа на основном процессоре обращается к to_register(6, n) или from_register(6, &n).

В течение некоторого промежутка времени значение REG_WE_A, обычно равное 0, становится равным 1. В это же самое время REG_IN_A становится равным тому значению, которое программа на основном процессоре записала в регистр A. При попытке программы на основном процессоре прочитать регистр A она получит в ответ значение, положенное схемой в REG_OUT_A. Регистр B работает точно так же.

Отложив совсем не надолго прояснение совершенно естественного вопроса о том, что такое «некоторый промежуток времени», напишем «наивную» (и, тем не менее, правильную) версию схемы на Автокоде, которая складывает значения, записанные в A и B, выдавая результат через A.

Начало схемы примера 1:

program vector_proc_32
out 32 DO
	in 32 ADDR
	in 32 DI
	in 1 EN
	in 1 WE
	in 32 REG_IN_A
	in 32 REG_IN_B
	out 32 REG_OUT_A
	out 32 REG_OUT_B
	in 1 REG_WE_A
	in 1 REG_WE_B
	in 0 Clk
	in 0 Reset
endprogram
	declare
	  reg 32 a
	  reg 32 b
	  reg 32 sum
	enddeclare
	REG_OUT_A = sum
  [
	a = 0
	b = 0
	sum = 0
  []
	if ( REG_WE_A == 1 )
	  a = REG_IN_A
	endif
	if ( REG_WE_B == 1 )
	  b = REG_IN_B
	endif
	sum = a + b
  ]

Конец схемы примера 1.

Программа, тестирующая эту схему, могла бы выглядеть так:

#include <stdio.h>
#include <avtokod/comm.h>
	int fpga_main( void )
{
	int result;
	from_register( 6, &result );
	printf( "result: %d\n", result );
	to_register( 6, 2 );
	to_register( 7, 3 );
	from_register( 6, &result );
	printf( "result: %d\n", result );
	to_register( 7, 5 );
	from_register( 6, &result );
	printf( "result: %d\n", result );
	return 0;
}

Результат работы этой программы выглядит так:

result: 0
result: 5
result: 7

В первой строке выдачи мы не послали в сопроцессор никаких значений слагаемых, а сразу спросили результат. В ответ мы получили 0 – некоторое начальное значение по умолчанию. Во второй строке выдачи мы предварительно послали в первое слагаемое значение 2, во второе – значение 3, и получили сумму этих значений. В третьей строке мы изменили значение второго слагаемого с 3-х на 5, и снова получили сумму.

Теперь посмотрим, как при этом работала схема сопроцессора.

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

Далее, от declare до enddeclare идут объявления переменных. Простейшим видом переменной в Автокоде является скалярный регистр (reg). Он представляет собой именованное целочисленное значение указанной ширины в битах (32). Объявлены три регистра – a, b и sum. Первые два будут использоваться для хранения текущих значений слагаемых, третий – для хранения суммы.

Далее следует раздел комбинационной логики, состоящий в этой схеме из единственной строки: REG_OUT_A = sum

По форме эта запись похожа на присваивание. По существу записи в этом разделе следует рассматривать, скорее, как алгебраические равенства. Смысл приведенной выше строки читается так: «Сделать значение выходного параметра REG_OUT_A тождественно равным (всегда) значению существующего внутри схемы регистра sum». Другими словами: «Жестко соединить выход sum со входом REG_OUT_A».

Комбинационные присваивания задают, таким образом, не действия, как операторы программы, а структуру схемы, и соответствуют, в конечном итоге, просто «припаиванию» выходов одних блоков схемы ко входам других. Уместна также алгебраическая аналогия. В алгебре запись: y = sin(x) означает, что для всех значений x, символ y есть результат применения к x функции «синус». Это отличается от смысла той же записи в программе на Фортране или С, предписывающей однократно выполнить вычисление синуса, и затем положить результат в переменную y. Как мы увидим далее, в Автокоде применяются оба вида присваиваний: и «алгебраические», и «алгоритмические». «Алгебраические» присваивания записываются в разделе комбинационной логики, «алгоритмические» - в других разделах схемы.

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

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

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

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

Тем самым, данный раздел схемы есть просто тело бесконечного цикла.

У читателя – программиста в этом месте должен возникнуть совершенно закономерный вопрос: предусмотрена ли в Автокоде возможность записи привычных в традиционном программировании последовательных действий, выполняемых в том порядке, в котором они записаны, пусть и с поправкой на дискретность времени? Да, конечно, в реальных схемах такие действия записываются в завершающем разделе схемы – в разделе последовательных действий. Однако, в тривиальной схеме, рассмотренной только что, действий для этого раздела просто не нашлось, в силу ее (схемы) простоты.

Вторая схема: суммирование массива (пример №2).

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

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

Программа тестирования этой схемы имеет вид:

#include <stdio.h>
#include <avtokod/comm.h>
#define L 128
	int fpga_main( void )
{
	int result, i;
	static int array[L];
	for ( i = 0; i < L; i++ ) array[i] = i;
	to_coprocessor( 0, array, L );
	to_register( 6, L );
done:
	from_register( 7, &result );
	if ( !result ) goto done;
	from_register( 6, &result );
	printf( "result: %d\n", result );
	return 0;
}

Результат работы этой программы выглядит так:

result: 8128

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

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

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

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

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

Начало схемы примера 2:

program vector_proc_32
	out 32 DO
	in 32 ADDR
	in 32 DI
	in 1 EN
	in 1 WE
	in 32 REG_IN_A
	in 32 REG_IN_B
	out 32 REG_OUT_A
	out 32 REG_OUT_B
	in 1 REG_WE_A
	in 1 REG_WE_B
	in 0 Clk
	in 0 Reset
endprogram
	declare
	  reg 32 L
	  reg 32 sum
	  reg 32 ready
	  ram 32 array(1, 16384)
	enddeclare
	REG_OUT_A = sum
	REG_OUT_B = ready
	array.dina[0] = DI
	array.addra[0] = ADDR
	array.wea[0] = WE
	DO = array.douta[0] 
  [
	ready = 0
	sum = 0
	array.web[0] = 0
  []
  ]

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

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

В разделе объявления переменных появился новый схемный объект, специфицируемый ключевым словом «ram». Это – батарея блоков адресуемой оперативной памяти, то есть объект, по смыслу похожий на массив традиционных языков программирования. Массив этот двумерный, причем индекс по первому измерению обязан быть константой, а по второму – может изменяться динамически, то есть быть регистром. Для простоты рассматриваемая схема построена таким образом, что фактически массив используется как одномерный: статическая размерность задана равной единице. Батарея блоков, таким образом, состоит из единственного блока.

Блок памяти выглядит в схеме как набор регистров доступа к блоку, имена которых указываются вслед за именем блока, через точку, как поля структуры в традиционных языках программирования. Например, запись: array.douta[0] означает: «регистр выходных данных порта «a» нулевого блока батареи «array».

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

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

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

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

Задаваемое при объявлении батареи памяти после слова «ram» число означает ширину ячейки памяти (и, тем самым, регистров входных и выходных данных) в битах. Ширина адреса (регистра адреса) всегда равна 32-м. Вторая размерность (после запятой в скобках) означает суммарную емкость батареи в словах (поделив ее на первую размерность, получим размер блока в словах).

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

Как легко видеть, в комбинационной части схемы регистры доступа порта «a» блока «array[0]» соединены с одноименными интерфейсными регистрами диапазона памяти, доступного программе. Этого достаточно для того, чтобы обеспечить передачу массива из программы в схему. В самом деле, при выполнении обращения к функции to_coprocessor() аппаратура сопряжения процессора с сопроцессором выполняет передачу указанного программой массива согласно тем правилам, которые мы только что описали: перебирая значения адреса на интерфейсном регистре «ADDR», одновременно закладывает соответствующие значения элементов массива в регистр «DI», поддерживая равным единице регистр «WE». Чтение при выполнении «from_coprocessor()» происходит аналогично. Если, как в случае нашей первой схемы, использование интерфейсного диапазона памяти в схеме не предусмотрено, а программа все же выполнит, например, «to_coprocessor()», все ее усилия по передаче данных в схему пропадут напрасно: к регистрам, в которые программа трудолюбиво «вкачивает» содержимое массива, никакая реальная память в схеме не подключена. При выполнении программой «from_coprocessor()» в этом случае в программу будет прочитан «мусор». В данном же случае мы озаботились тем, чтобы за интерфейсным диапазоном стояла реальная память, и для этого подключили батарею «array» к интерфейсным регистрам по порту «a». Для внутреннего доступа к этой памяти схеме придется довольствоваться портом «b».

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

Сначала напишем цикл ожидания записи программой длины массива в интерфейсный регистр «A», означающий, что пора начинать работу, затем – цикл суммирования, после него – выдачу готовности через интерфейсный регистр «B» и возврат к первому циклу:

loop0:
	{
	  if ( REG_WE_A == 1 )
	    L = REG_IN_A
	    array.addrb[0] = 0
	    ready = 0
	    sum = 0
	   else
	     next loop0
	   endif
	}
	{
	   array.addrb[0]++
	   L--
	}
loop1:
	{
	  sum = sum + array.doutb[0]
	  array.addrb[0]++
	  L--
	  if ( L != 0 ) 
	    next loop1 
	  else
	    ready = 1
	    next loop0
	  endif
	}

Конец схемы примера 2.

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

Как легко видеть, текст разбивается на группы операторов, заключенные в фигурные скобки (которые, кстати, обязательны в этом разделе схемы, даже если группа состоит из единственного оператора). Каждая такая группа представляет собой состояние схемы, то есть набор действий, выполняемых, одновременно и независимо, на одном такте. Результат присваивания при этом виден на следующем такте. Состояния выполняются, как операторы программы на традиционном языке, последовательно, в том порядке, в котором они записаны: следующий такт – следующее (по тексту) состояние. Оператор next — это оператор перехода, а упомянутые в нем идентификаторы – метки. Оператор этот намеренно назван словом, отличным от goto, чтобы подчеркнуть, что это – не совсем «классический» оператор перехода. Переход, как и присваивание, отложен на такт. Все составляющие состояние операторы выполняются, с учетом условных проверок, и только после этого работает (или не работает) переход на указанное состояние. Ни «выйти по переходу» из состояния, не выполнив его до конца, ни перейти «внутрь какого-либо состояния» при помощи next невозможно, поскольку, как уже отмечалось выше, все операторы состояния выполняются одновременно и независимо. По этой же причине не важно, в каком порядке записаны операторы (в том числе next) внутри фигурных скобок.

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

  1. В отличие от программ на традиционных языках, схема на Автокоде существенным образом двумерна: помимо последовательности действий, записываемой в разделах начального сброса, действий на каждом такте и последовательных действий (временное измерение), в схеме присутствует описание структуры (пространственное измерение), представленное разделом комбинационной логики. Пока мы использовали комбинационную логику только для организации связи схемы с внешним миром, но в более сложных, реальных схемах комбинационная логика широко используется также для построения внутренней структуры схемы.
  2. В отличие от программ на традиционных языках, время в схеме на Автокоде дискретно. Действия, выполняемые на любом данном такте, выполняются одновременно и независимо. При этом условные проверки любой степени сложности времени не занимают, а присваивания и переходы занимают ровно один такт, или, что то же самое, отложены на такт.
  3. В отношении присваиваний регистрам значений действует правило единственности источника значения. Оно состоит в том, что каждому регистру можно присвоить значение либо только в разделе комбинационной логики, причем только один раз, либо не более одного раза на каждом такте. Например, если регистр используется в левой части безусловного оператора присваивания в разделе действий на каждом такте, то в разделе последовательных действий ему уже ничего присваивать нельзя. Менее очевидный пример: если в программах на С мы часто пишем что-то вроде:
	a = b;
	if ( n ) a = c;
    то в схеме на Автокоде следует писать:
	{
	  if ( n != 0 )
	    a = c
	  else
	    a = b
	  endif
	}
    поскольку в одном состоянии (на одном такте) нельзя сначала присвоить регистру a одно, а затем – другое. Такт – это квант времени, не имеющий протяжения, никаких «сначала» и «потом» внутри такта нет.

  1. Автокод является языком спецификации схемы с точностью до такта (cycle-accurate language). Это означает, что программист контролирует скорость работы схемы с абсолютной точностью, подобно тому, как программист на языке ассемблера с абсолютной точностью контролирует двоичный образ разрабатываемой им программы, хотя и не выписывает вручную составляющую эту программу биты. Отсюда – название языка: когда-то давно языки, называемые сегодня языками ассемблера, в русскоязычной терминологии назывались «автокодами один в один».

Более сложная схема: вычисление определенного интеграла методом трапеций с фиксированной точкой (пример №3).

Возвращаясь к рассмотренным примерам схем, мы вынуждены отметить, что они пока никак не демонстрируют даже потенциальных преимуществ именно схемной реализации вычислений. В самом деле, рабочие частоты ПЛИС почти на порядок ниже рабочих частот современных процессоров общего назначения. Для того, чтобы получить скоростной выигрыш, совершенно необходимо писать схемы, выполняющие за такт не одну, а несколько арифметических операций. Попробуем написать действительно эффективную схему вычисления определенного интеграла с фиксированной точкой методом трапеций. Если мы допустим, что интегрируемая функция задана таблично на равномерной сетке, а данные нормированы таким образом, что шаг равен 1, задача фактически сведется к предыдущей, с одним уточнением: суммируя массив, значения его первого и последнего элементов требуется поделить пополам. Кроме того, будем выполнять одновременно не одно, а восемь сложений. Для простоты потребуем также, чтобы размер сетки, на которой задана интегрируемая функция, делился на 8.

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

result: 8064

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

Начало схемы примера 3:

program vector_proc_32
	out 32 DO
	in 32 ADDR
	in 32 DI
	in 1 EN
	in 1 WE
	in 32 REG_IN_A
	in 32 REG_IN_B
	out 32 REG_OUT_A
	out 32 REG_OUT_B
	in 1 REG_WE_A
	in 1 REG_WE_B
	in 0 Clk
	in 0 Reset
endprogram
	declare
	    reg 32 L
	    reg 32 sum
	    reg 32 ready
	    ram 32 array(8, 16384)
	    reg 3 old_addr_low
	    reg 32 partsum(8)
	    reg 32 dout_half(8)
	enddeclare
	REG_OUT_A = sum
	REG_OUT_B = ready
	do @1 = 0, 7
	     array.dina[@1] = DI
	     array.addra[@1](28:0) = ADDR(31:3)
	     array.addra[@1](31:29) = 0
	     array.wea[@1] = ( (@1 == ADDR(2:0)) && (WE == 1) ) ? 1 : 0
	     DO = ( @1 == old_addr_low ) ? array.douta[@1] : 'Z' 
	     dout_half[@1](30:0) = array.doutb[@1](31:1)
	     dout_half[@1](31:31) = 0
	enddo
  [
	ready = 0
	sum = 0
	partsum = 0
	array.web = 0
	L = 0
  []
	old_addr_low = ADDR(2:0)
  ]

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

ram 32 array(8, 16384)
, мы это учли. Как нам теперь подключить эту батарею к интерфейсной области памяти? Нам ведь требуется, чтобы поступающие из программы значения раскладывались по блокам батареи с периодом 8: нулевое слово массива – в нулевое слово нулевого блока, первое слово массива – в нулевое слово первого блока, второе слово массива – в нулевое слово второго блока, …, восьмое слово массива – в первое слово нулевого блока, и т. д.

Для решения этой проблемы мы написали в разделе комбинационной логики цикл do — все, что расположено от do до enddo, включительно. Синтаксически запись довольно очевидна. Переменная цикла не была (и не должна быть) объявлена в разделе объявления переменных, причем имеет специальный вид @1 (вместо единицы можно было использовать любую цифру). Сначала рассмотрим, как выполнено подключение батареи памяти на прием данных из программы. Поскольку восемь (число блоков в батарее) – степень двойки, мы может расщепить адрес слова в массиве, поступающий из программы, на два поля. Младшие три бита мы будем трактовать как номер блока в батарее (от 0 до 7), а старшие биты — как номер слова в блоке. В Автокоде диапазон битов в пределах регистра задается в круглых скобках через двоеточие, например: ADDR(31:3), ADDR(2:0).

Теперь мы можем подать поступающее из программы значение (DI) на входы данных всех блоков, старшую часть адреса (ADDR(31:3)) – на адресные входы всех блоков, а младшую часть адреса использовать для выборочной подачи разрешения записи на тот блок батареи, номеру которого она равна. Формально анализируя текст тела цикла do, легко убедиться, что именно это там и написано.

Может возникнуть вполне законный вопрос: где и в каком ритме выполняется этот цикл do, сколько тактов занимает, и почему записан в комбинационном разделе схемы? Ответ прост. Данный цикл выполняется при трансляции схемы, и только. Он является не более чем сокращенной формой записи восьми отдельных комбинационных соотношений, каждое из которых имеет традиционный для комбинационного раздела «алгебраический» смысл (переменную цикла при этом следует трактовать как свободную переменную). Попросту говоря, это цикл не выполнения, а построения схемы, цикл в пространстве, а не во времени. Конечно, границы изменения переменной такого цикла должны быть константами, что в нашем случае имеет место.

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

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

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

Начнем с первой проблемы. Ее решение тривиально. Чтобы постоянно иметь в своем распоряжении «вчерашнее» значение младшей части адреса, объявим специальный регистр old_addr_low, и будем присваивать ему младшую часть адреса на каждом такте. Поскольку присваивание, как мы знаем, выполняется с задержкой на такт, проблема решена. Расположено это присваивание, естественно, в разделе действий на каждом такте.

Решение второй проблемы записано в последней строке тела цикла, непосредственно перед enddo:

DO = ( @1 == old_addr_low ) ? array.douta[@1] : 'Z'

Как мы уже отмечали, эта запись в теле цикла эквивалентна следующим восьми операторам, записанным без использования цикла:

DO = ( 0 == old_addr_low ) ? array.douta[0] : 'Z'
DO = ( 1 == old_addr_low ) ? array.douta[1] : 'Z'
...
DO = ( 7 == old_addr_low ) ? array.douta[7] : 'Z'

«Расшифруем», например, первый из этих операторов. В нем записано: «Если значение регистра old_addr_low равно 0, то считать, что DO тождественно равно array.douta[0], иначе не считать значение DO определенным в данном операторе». Символ 'Z' специально предназначен для записи конструкций такого рода, которые в схемотехнике называют мультиплексорами. Использование мультиплексоров не противоречит правилу единственности источника значения, если области определения ветвей мультиплексора не пересекаются. В данном случае они точно не пересекаются: значение old_addr_low может быть равно в каждый момент времени лишь одному числу в диапазоне от 0 до 7. Использование некорректных (с пересекающимися областями определения ветвей) мультиплексоров ведет к построению транслятором, вообще говоря, электрически некорректных схем, эффект от работы которых подобен эффекту выхода за границы массива в программе на традиционном языке программирования.

Завершая анализ представленной выше части схемы, отметим еще два отличия от предыдущего примера.

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

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

	array.web = 0

но можно было написать и:

	do @1 = 0, 7
	      array.web[@1] = 0
	enddo

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

loop0:
	 {
	   if ( REG_WE_A == 1 )
	     L(28:0) = REG_IN_A(31:3)
	     array.addrb = 0
	     ready = 0
	     sum = 0
	    else
	      next loop0
	    endif
	 }
	 {
	    array.addrb = array.addrb + 1
	    L = L - 1
	 }
loop1:
	 {
	   do @1 = 0, 7
	     if (  ((@1 == 0) && (array.addrb[0] == 1)) || ((@1 == 7) && (L == 0)) )
	       partsum[@1] = partsum[@1] + dout_half[@1]
	     else
	       partsum[@1] = partsum[@1] + array.doutb[@1]
	     endif
	   enddo
	   array.addrb = array.addrb + 1
	   L = L - 1
	   if ( L != 0 ) 
	     next loop1 
	   endif
	}

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

do @1 = 0, 7
 sum = sum + partsum[@1]
enddo

но разум подсказывает, что так делать нельзя. Цикл do, как мы знаем, пространственный, а не временной, и ничего, кроме вопиющего нарушения правила единственности источника значения, из приведенной только что записи не получится. Тогда, может быть, следует написать:

	sum = partsum[0] + partsum[1] + partsum[2] + ...?

Такая запись формально правильна, но трансляция схемы с ней заведомо окончится ошибкой. Электронное устройство, складывающее сразу восемь чисел, очень сложное, и тактовая частота, на которой оно смогло бы работать, во много раз ниже, чем частота, на которой способно работать все написанное нами до сих пор. А ведь в схеме может быть всего одна частота. Обычно она подбирается такой, чтобы суммирование или вычитание двух целых 32-разрядных чисел укладывалось в один такт с некоторым небольшим запасом. Поэтому записей сложных выражений в схеме следует избегать. Придется разбить суммирование частичных сумм на такты вручную:

{
	do @1 = 0, 3
	  partsum[@1] = partsum[@1] + partsum[@1+4]
	enddo
}
{
	do @1 = 0, 1
	  partsum[@1] = partsum[@1] + partsum[@1+2]
	enddo
}
{
	sum = partsum[0] + partsum[1]
	ready = 1
	next loop0
}

Конец схемы примера 3.

Теперь схема написана окончательно.

Еще более сложная схема: вычисление определенного интеграла методом трапеций с плавающей точкой (примеры №№4, 5 и 6).

К сожалению, современные ПЛИС не умеют, подобно универсальным процессорам, выполнять арифметические операции с плавающей точкой так же быстро, как с фиксированной. По этой причине соответствующие типы данных и операции над ними в Автокод не включены. При вычислениях с плавающей точкой значения хранятся в регистрах достаточной битовой ширины, а сами вычисления выполняются путем использования специальных библиотечных компонентов, вставляемых в схему. Слово «компонент» в данном случае – не эпитет, а термин. Структурно компонент имеет довольно много общего с функцией (подпрограммой) традиционных языков программирования, но, с учетом различий в моделях программирования, по смыслу сильно от нее отличается. Целей построения настоящего примера – две:

- освоить использование компонентов,

- научиться строить конвейерные схемы.

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

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

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

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

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

Начало схемы примера 4:

program vector_proc_32
	out 32 DO
	in 32 ADDR
	in 32 DI
	in 1 EN
	in 1 WE
	in 32 REG_IN_A
	in 32 REG_IN_B
	out 32 REG_OUT_A
	out 32 REG_OUT_B
	in 1 REG_WE_A
	in 1 REG_WE_B
	in 0 Clk
	in 0 Reset
endprogram
	declare
	    reg 32 a
	    reg 32 b
	    reg 32 sum
	    component summator
	enddeclare
	REG_OUT_A = sum
	insert summator
	    .add1( a )
	    .add2( b )
	    .result( sum )
	    .Clk( Clk )
	    .Reset( Reset )
	endinsert
  [
	a = 0
	b = 0
  []
	if ( REG_WE_A == 1 )
	      a = REG_IN_A
	endif
	if ( REG_WE_B == 1 )
	      b = REG_IN_B
	endif
  ]

Сразу же напишем на Автокоде схему компонента «сумматор»:

program summator
in 32 add1
in 32 add2
out 32 result
in 0 Clk
in 0 Reset
endprogram
   declare
   enddeclare 
  [
	    result = 0
  []
	    result = add1 + add2
  ]

Конец схемы примера 4.

Схема компонента «сумматор» получилась совсем простая. Она состоит только из заголовка и разделов начального сброса и действий на каждом такте. В последнем выходному интерфейсному регистру result присваивается сумма входных интерфейсных регистров add1 и add2.

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

В терминах аналогии с традиционными языками:

— компонент «сумматор» — вызываемая функция,

add1, add2 и result — ее формальные параметры,

— модифицированная схема сложения двух чисел — вызывающая программа,

— оператор «insert» — вызов функции,

a, b и sum — аргументы вызова (фактические параметры),

Clk и Reset — скрытые параметры, смысл которых разъясняется в конце раздела, посвященного систематическому описанию Автокода.

Теперь поговорим о различиях компонентов и функций (подпрограмм) традиционных языков программирования.

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

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

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

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

Обсуждая семантику вызова функций и подпрограмм в традиционных языках программирования, обычно довольно много времени уделяют такому явлению, как побочный эффект вызова функции (подпрограммы). В С и Фортране, например, вызываемая функция (подпрограмма) имеет массу возможностей изменить значения переменных, видимых в вызывающей программе, но не являющихся фактическими параметрами вызова. В Автокоде побочных эффектов не бывает. Всякое взаимодействие «вызывающего» и «вызываемого» — только через «разъем» (интерфейсные регистры).

Теперь мы готовы строить тот содержательный пример, которому посвящен настоящий раздел. Тестирующая его программа отличается от тестирующей программы примера из предыдущего раздела только типами передаваемых в схему данных и, в предположении, что размеры машинного представления типов int и float совпадают, имеет вид:

#include <stdio.h>
#include <avtokod/comm.h>
#define L 128
	    int fpga_main( void )
{
	    int i, r;
	    float result;
	    static float array[L];
	    for ( i = 0; i < L; i++ ) array[i] = (float)i;
	    to_coprocessor( 0, (int*)array, L );
	    to_register( 6, L );
done:
	    from_register( 7, &r );
	    if ( !r ) goto done;
	    from_register( 6, (int*)(&result) );
	    printf( "result: %f\n", result );
	    return 0;
}

Результат работы этой программы выглядит так:

result: 8128.00000

Для выполнения операции сложения чисел с плавающей точкой используем готовый библиотечный компонент floating32_add, со следующими интерфейсными регистрами:
a — первое слагаемое,
b — второе слагаемое,
result — результат сложения,
operation_nd — разрешение приема слагаемых на данном такте,
clk — скрытый параметр для «заклинания», к смыслу алгоритма отношения не имеющий.

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

Нам также потребуется библиотечный умножитель для деления на 2 (умножения на 0.5) первого и последнего элементов массива. Этот компонент называется floating32_mul, имеет тот же внешний интерфейс, что и floating32_add, и ту же конвейерную задержку.

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

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

Как и в предыдущих примерах, будем выписывать схему по частям, давая по ходу необходимые пояснения.

Начало схемы примера 5:

program vector_proc_32
out 32 DO
in 32 ADDR
in 32 DI
in 1 EN
in 1 WE
in 32 REG_IN_A
in 32 REG_IN_B
out 32 REG_OUT_A
out 32 REG_OUT_B
in 1 REG_WE_A
in 1 REG_WE_B
in 0 Clk
in 0 Reset
endprogram
	declare
	    reg 32 L
	    reg 32 sum
	    reg 32 ready
	    ram 32 array(8, 16384)
	    reg 3 old_addr_low
	    reg 32 partsum(8)
	    reg 32 summator_in_a(8)
	    reg 32 summator_in_b(8)
	    reg 32 summator_in_a_aux(8)
	    reg 32 summator_in_b_aux(8)
	    reg 32 summator_out(8)
	    reg 1 enable_sum
	    reg 1 enable_head
	    reg 1 enable_tail
	    reg 4 cycle
	    component floating32_add
	enddeclare
	REG_OUT_A = sum
	REG_OUT_B = ready
	do @1 = 0, 7
	     array.dina[@1] = DI
	     array.addra[@1](28:0) = ADDR(31:3)
	     array.addra[@1](31:29) = 0
	     array.wea[@1] = ( (@1 == ADDR(2:0)) && (WE == 1) ) ? 1 : 0
	     DO = ( @1 == old_addr_low ) ? array.douta[@1] : 'Z' 
	enddo
	do @1 = 0, 7
	   insert floating32_add
	     .a( summator_in_a[@1] )
	     .b( summator_in_b[@1] )
	     .result( summator_out[@1] )
	     .operation_nd( enable_sum(0) )
	     .clk( Clk )
	   endinsert
	   summator_in_a[@1] =  (enable_head == 1) ? 0
	: (enable_tail == 1) ? summator_in_a_aux[@1]
	: summator_out[@1]

	   summator_in_b[@1] = (enable_tail == 1) ? summator_in_b_aux[@1]
	: array.doutb[@1]
	enddo
  [
	ready = 0
	sum = 0
	array.web = 0
	enable_sum = 0
	enable_head = 1
	enable_tail = 0
	cycle = 0
	L = 0
  []
	    old_addr_low = ADDR(2:0)
  ]

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

Основной режим работы сумматора при суммировании массива – это подключение к одному из входов сумматора его выхода, а ко второму – регистра выходных данных массива. К сожалению, ограничиться таким подключением мы не можем, поскольку сумматор выдает результаты суммирования с задержкой на 5 тактов. Из-за этой задержки нам придется специальным образом отрабатывать начало и конец суммирования. Для этого, в свою очередь, необходимо обеспечить подачу на входы сумматора данных от разных источников, а выбор источника осуществлять специальными управляющими сигналами. Вариантов выбора подаваемых на сумматор данных всего три:

— на первый вход подаются нули, на второй – текущий элемент массива. Это режим обработки «головы» массива.

— на первый вход подается выход сумматора, на второй – текущий элемент массива. Это режим обработки середины массива (основной рабочий режим).

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

Первый режим включается управляющим сигналом enable_head, третий – сигналом enable_tail.

Управлять регистрами разрешения, имена которых начинаются с enable_, будем в разделе последовательных действий. Для этого нам потребуется регистр cycle — счетчик тактов, ширина которого заведомо достаточна для представления числа «5».

Начало текста раздела последовательных действий выглядит так:

loop0:
	{
	  if ( REG_WE_A == 1 )
	    L(28:0) = REG_IN_A(31:3)
	    array.addrb = 0
	    ready = 0
	    sum = 0
	    array.web = 0
	    enable_sum = 0
	    enable_head = 1
	    enable_tail = 0
	    cycle = 0
	   else
	     next loop0
	   endif
	}
	{
	   array.addrb = array.addrb + 1
	   L = L – 1
	   cycle = 4
	   enable_sum = 1
	}
loop1:
	{
	  array.addrb = array.addrb + 1
	  cycle = cycle – 1
	  if ( cycle == 0 )
	    enable_head = 0
	  endif
	  L = L - 1
	  if ( L != 0 ) 
	   next loop1
	  else
	    enable_tail = 1
	  endif
	}

В этой части текста отрабатывается 5-тактная задержка появления первого результата на выходе сумматора, после чего включается подача выхода сумматора на его первый вход. Попутно отсчитывается заданное число выбираемых из массива элементов. К моменту, когда на второй вход сумматора поступил последний из элементов массива, в сумматоре остались пять частичных сумм, образованные элементами массива со следующими номерами:
0, 5, 10, 15, …
1, 6, 11, 16, …
2, 7, 12, 17, …
3, 8, 13, 18, …
4, 9, 14, 19, …

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

{
  partsum = summator_out
}
{
  summator_in_a_aux = partsum
  summator_in_b_aux = summator_out
}
{
  partsum = summator_out
}
{
  summator_in_a_aux = partsum
  summator_in_b_aux = summator_out
}
{
  partsum = summator_out
}
{
  summator_in_a_aux = partsum
  summator_in_b_aux = summator_out
}
{
}
{
  partsum = summator_out
}
{
}
{
  summator_in_a_aux = partsum
  summator_in_b_aux = summator_out
}
{
}
{
  partsum = summator_out
}
{
}
{
}
{
}
{
  summator_in_a_aux = partsum
  summator_in_b_aux = summator_out
}

Теперь в каждом из восьми сумматоров находится готовая частичная сумма, через 5 тактов она будет доступна. Эти восемь сумм требуется снова просуммировать:

{
}
{
}
{
}
{
}
{
}
{
	do @1 = 0, 3
	      summator_in_a_aux[@1] = summator_out[@1] 
	      summator_in_b_aux[@1] = summator_out[@1+4]
	enddo
}
{
}
{
}
{
}
{
}
{
}
{
	do @1 = 0, 1
	      summator_in_a_aux[@1] = summator_out[@1]
	      summator_in_b_aux[@1] = summator_out[@1+2]
	enddo
}
{
}
{
}
{
}
{
}
{
}
{
	summator_in_a_aux[0] = summator_out[0]
	summator_in_b_aux[0] = summator_out[1]
}
{
}
{
}
{
}
{
}
{
}
{
	sum = summator_out[0]
	ready = 1
	next loop0
}

Конец схемы примера 5.

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

Начнем с деления пополам (то есть с умножения на 0.5, поскольку у умножителя задержка намного меньше, чем у делителя) первого и последнего элемента суммируемого массива.

Результат работы тестирующей эту схему программы будет выглядеть так:

result: 8064.50000

Значение 0.5 в Автокоде задается в виде: float(0.5).

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

Начало схемы примера 6:

program vector_proc_32
out 32 DO
in 32 ADDR
in 32 DI
in 1 EN
in 1 WE
in 32 REG_IN_A
in 32 REG_IN_B
out 32 REG_OUT_A
out 32 REG_OUT_B
in 1 REG_WE_A
in 1 REG_WE_B
in 0 Clk
in 0 Reset
endprogram
	declare
	    reg 32 L
	    reg 32 sum
	    reg 32 ready
	    ram 32 array(8, 16384)
	    reg 3 old_addr_low
	    reg 32 partsum(8)
	    reg 32 summator_in_a(8)
	    reg 32 summator_in_b(8)
	    reg 32 summator_in_a_aux(8)
	    reg 32 summator_in_b_aux(8)
	    reg 32 summator_out(8)
	    reg 1 enable_sum
	    reg 1 enable_head
	    reg 1 enable_tail
	    reg 4 cycle
	    reg 32 saved_first
	    reg 32 saved_last
	    component floating32_add
	    component floating32_mul
	    reg 32 mul_a
	    reg 32 mul_result
	enddeclare
	REG_OUT_A = sum
	REG_OUT_B = ready
	do @1 = 0, 7
	  array.dina[@1] = DI
	  array.addra[@1](28:0) = ADDR(31:3)
	  array.addra[@1](31:29) = 0
	  array.wea[@1] = ( (@1 == ADDR(2:0)) && (WE == 1) ) ? 1 : 0
	  DO = ( @1 == old_addr_low ) ? array.douta[@1] : 'Z' 
	enddo
	do @1 = 0, 7
	  insert floating32_add
	     .a( summator_in_a[@1] )
	     .b( summator_in_b[@1] )
	     .result( summator_out[@1] )
	     .operation_nd( enable_sum(0) )
	     .clk( Clk )
	  endinsert
	  summator_in_a[@1] =  (enable_head == 1) ? 0
    : (enable_tail == 1) ? summator_in_a_aux[@1]
    : summator_out[@1]

	  summator_in_b[@1] = (enable_tail == 1) ? summator_in_b_aux[@1]
	: array.doutb[@1]
 
	enddo
	insert floating32_mul
	    .a( mul_a )
	    .b(float(0.5))
	    .result( mul_result )
	    .operation_nd( '1' )
	    .clk( Clk )
	endinsert
  [
	    ready = 0
	    sum = 0
	    array.web = 0
	    enable_sum = 0
	    enable_head = 1
	    enable_tail = 0
	    cycle = 0
	    L = 0
  []
	    old_addr_low = ADDR(2:0)
  ]
loop0:
	{
	  if ( REG_WE_A == 1 )
	    L(28:0) = REG_IN_A(31:3)
	    array.addrb = 0
	    ready = 0
	    sum = 0
	    array.web = 0
	    enable_sum = 0
	    enable_head = 1
	    enable_tail = 0
	    cycle = 0
	   else
	     next loop0
	   endif
	}
	{
	  array.addrb = L - 1
	}
	{
	  mul_a = array.doutb[0]
	  saved_first = array.doutb[0] 
	}
	{
	  mul_a = array.doutb[7]
	  saved_last = array.doutb[7]
	  cycle = 4
	}
loop5:
	{
	  cycle = cycle – 1
	  if ( cycle != 0 )
	   next loop5
	  else
	   array.addrb[0] = 0
	   array.dinb[0] = mul_result
	   array.web[0] = 1
	  endif
	}
	{
	  array.web[0] = 0
	  array.web[7] = 1
	  array.addrb = L – 1
	  array.dinb[7] = mul_result
	}
	{
	  array.addrb = 0
	  array.web[7] = 0
	}
	{
	   array.addrb = array.addrb + 1
	   L = L – 1
	   cycle = 4
	   enable_sum = 1
	}
loop1:
	{
	  array.addrb = array.addrb + 1
	  cycle = cycle – 1
	  if ( cycle == 0 )
	    enable_head = 0
	  endif
	  L = L - 1
	  if ( L != 0 ) 
	   next loop1
	  else
	    enable_tail = 1
	  endif
	}
	{
	  partsum = summator_out
	}
	{
	  summator_in_a_aux = partsum
	  summator_in_b_aux = summator_out
	}
	{
	  partsum = summator_out
	}
	{
	  summator_in_a_aux = partsum
	  summator_in_b_aux = summator_out
	}
	{
	  partsum = summator_out
	}
	{
	  summator_in_a_aux = partsum
	  summator_in_b_aux = summator_out
	}
	{
	}
	{
	  partsum = summator_out
	}
	{
	}
	{
	  summator_in_a_aux = partsum
	  summator_in_b_aux = summator_out
	}
	{
	}
	{
	  partsum = summator_out
	}
	{
	}
	{
	}
	{
	}
	{
	  summator_in_a_aux = partsum
	  summator_in_b_aux = summator_out
	}
	{
}

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

	{
	  array.addrb[0] = 0
	  array.dinb[0] = saved_first
	  array.web[0] = 1
	}
	{
	  array.web[0] = 0
	  array.dinb[7] = L – 1
	  array.dinb[7] = saved_last
	  array.web[7] = 1
	}
	{
	  array.web[7] = 0
	}
	{
	}
	{
	    do @1 = 0, 3
	      summator_in_a_aux[@1] = summator_out[@1] 
	      summator_in_b_aux[@1] = summator_out[@1+4]
	    enddo
	}
	{
	}
	{
	}
	{
	}
	{
	}
	{
	}
	{
	    do @1 = 0, 1
	      summator_in_a_aux[@1] = summator_out[@1]
	      summator_in_b_aux[@1] = summator_out[@1+2]
	    enddo
	}
	{
	}
	{
	}
	{
	}
	{
	}
	{
	}
	{
	    summator_in_a_aux[0] = summator_out[0]
	    summator_in_b_aux[0] = summator_out[1]
	}
	{
	}
	{
	}
	{
	}
	{
	}
	{
	}
	{
	    sum = summator_out[0]
	    ready = 1
	    next loop0
}

Конец схемы примера 6.

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

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

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

Краткое полуформальное описание Автокода.

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

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

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

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

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

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

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

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

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

Заголовок схемы имеет вид:

program <имя компонента>
<описание интерфейсного регистра>
...
<описание интерфейсного регистра>
endprogram

 

Например:

program main
in 10 input
out 10 output
endprogram

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

Заголовок описывает внешний интерфейс («разъем») создаваемой схемы, в данном случае – входной регистр input и выходной регистр output, оба шириной по 10 бит.

Тело схемы имеет вид:

declare
Объявления переменных
enddeclare
Комбинационная логика
	[
Действия по начальному сбросу
	[]
Действия, выполняемые на каждом такте
	]
Состояния 

 

Именные скобки declare и enddeclare не должны иметь комментариев в той же строке. Сами скобки в любой схеме обязательны, даже если между ними ничего нет.

Пример бессмысленной, но формально правильной схемы:

 

program main
in 10 input
out 10 output
in 0 Clk
in 0 Reset
endprogram
// This is a comment;
// Declarations follow:
   declare
    reg 12 a
    ram 18 b(20,33)
    ram 18 d(20,33)
    reg 14 c(20)
    reg 10 w
   enddeclare
// Combinational assignments:
    a = 5 
    [
// This is performed on initial reset:
    c(0:3) = 3
    []
// This is performed during any cycle:
    b.dina(10:13) = d.doutb(11:14)
    d.wea(0:2) = {1,0}
    ]
// The states follow, they are executed one by one:
    {
     a = 7
    }
loop0: // This is a label, used to go to the state below:
    {
     if ( w == 11 )
      next loop0
     else
      a = 9
     endif  
     b(10:15) = b(15:20)
    }

 

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

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

Константы в автокоде — это целые числа, записанные в десятичной или в расширенной двоичной системе счисления. Запись чисел типа float возможна при помощи специальной функции-преобразователя, например:

a = float(3.5)
, где регистр a обязан иметь ширину 32 разряда.

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

Примеры:

	1
	-18
	00000000101
	'0'
	'Z'

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

	q = 1
	q = 1
	q(0) = 1
	q(0) = '1'

Перед трансляцией исходного текста собственно автокодом, он обрабатывается макропроцессором m4. Это позволяет использовать символические константы, заданные при помощи директивы define. При этом следует помнить, что автокод не умеет свертывать константные выражения, а m4 – умеет, если использовать встроенные макросы decr, incr и eval (уменьшение на 1, увеличение на 1 и произвольное арифметическое выражение, соответственно). Именно этими макросами m4 следует пользоваться, если в некотором месте схемы по правилам автокода полагается константа, а хочется написать константное выражение.

Например:

	define(MY_CONST1,4)
	define(MY_CONST2,eval(MY_CONST1 * 5))
	       ...
	b[decr(MY_CONST1)]

3.2. Переменные.

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

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

Скалярный регистр — это одномерный массив битов шириной width. Должен быть явно объявлен в тексте схемы.

Объявление скалярного регистра b с width=32 бит:

reg 32 b

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

Векторный регистр — это двумерный массив шириной блока (размером вектора) wblock и шириной каждого элемента блока (скалярного регистра) width. Должен быть явно объявлен в тексте схемы.

Объявление векторного регистра d с wblock=4 и width=10 бит:

reg 10 d(4)

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

Блок двухпортовой (a и b) векторной памяти — это объект, работающий согласно временной диаграмме Xilinx BRAM, доступ к которому осуществляется через набор согласованно используемых векторных регистров.

Должен быть явно объявлен в тексте схемы. Объявление блока векторной памяти f с wblock=4, width=32 бит и объемом = 80 элементов (10 векторов):

ram 32 f(4,80)
где объем вычисляется по формуле: (width/wblock) * <число_векторов>

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

f.addrb
– регистр адреса порта b блока векторной памяти f.

Предусмотрены следующие регистры управления:

.addra, .addrb— векторные регистры адреса (размером вектора <wblock> и шириной 32 бит);
.dina,  .dinb — векторные регистры входных данных (размером вектора <wblock> и шириной <width>);
.douta, .doutb— векторные регистры выходных данных (размером вектора <wblock> и шириной <width>);
.wea,   .web  — векторные регистры разрешения записи (размером вектора <wblock> и шириной 1 бит).

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

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

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

Счетчик цикла. Имеет вид: @<целое_положительное_число>
Текущий элемент вектора – перечисления. Имеет вид: @_                          

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

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

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

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

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

Пример:

	reg 32 a(4)      // вектор
	reg 32 b(4)      // вектор
	reg 20 d(4)      // вектор
	reg 32 f(8)      // вектор
	reg 32 c         // скаляр
	  ...
	a = b + 1        // правильная запись
	a = c + 1        // правильная запись
	a = d + 1        // неправильная запись
	a = f            // неправильная запись

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

Индекс из вектора делает скаляр, а из скаляра произвольной ширины – скаляр шириной в один бит.

Имеет вид:

<имя>[число или индексное_выражение] — для векторов
<имя>(число)                         — для скаляров

Число — десятичное целое без знака.

Индексное_выражение — это арифметическое выражение с участием целых десятичных чисел и переменных цикла или переменная цикла.

 

Диапазоны меняют размеры регистров: ширину wblock или width.

Имеют вид: <имя>(число:число)

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

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

	a(0:3) = f(4:7)
	a[0](0:19) = d[1](0:19) + 1 

или

	a(0:3)(0:19) = d(0:3)(0:19) + 1

Для векторных регистров указание диапазона единичной ширины, т.е. <имя>(N:N), превращает диапазон в индекс N.

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

4.1. Операции.

Побитная инверсия: ~                   
Операции присваивания: =  ++  --  +=  -=   
Арифметические операции: +  -  *  /          
Операции сравнения: ==  !=  >  >=  <  <=
Логические операции: &&  ||              

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

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

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

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

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

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

  • Оператор присваивания вида: <переменная> = <выражение>

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

Примеры:

	reg 32 a(4)   // вектор
	reg 32 b(4)   // вектор
	reg 1 d(4)    // вектор
	reg 32 c      // скаляр
	reg 1 f       // скаляр
	reg 1 g       // скаляр
	...
	a = b
	c(10:15) = a[0](16:21)
	a = b + 8
	a(0:2) = a(0:2) - b(1:3)
	f = d[3] && ~g

 

  • Оператор присваивания вида: <вектор>= {a1,a2,...,aN} , где a1 ... aN — список констант и скаляров, width скаляров должен совпадать с width вектора.

Данная операция означает последовательное присваивание элементов из списка элементам данного вектора.

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

Примеры:

	reg 10 g(8)   // вектор
	reg 10 f(4)   // вектор
	reg 10 b      // скаляр
	reg 10 c      // скаляр
	...
	g  = {1,-2,b,c,b,3,f[0],0}
	g(0:6) = {b,1}

Во втором примере скаляру g[0] присвоится значение скаляра b, а остальным пяти элементам вектора g присвоится 1.

 

  • Оператор присваивания вида: {a1,a2,...,aN} = <вектор>, или {a1,a2,...,aN} = <скаляр> , где a1 ... aN — список скаляров, width скаляров должен совпадать с width вектора(скаляра).

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

Пример:

	reg 10 g(4)    // вектор
	reg 10 a       // скаляр
	reg 10 b       // скаляр
	reg 10 c       // скаляр
	...
	{a,c,b}= g(1:3)

Что означает: a = g[1], c = g[2] и b = g[3].

 

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

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

 

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

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

Имеет вид:

       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 и т.д.

  • Оператор цикла с перечислением (for).

Имеет вид:

       for(a1,a2,...,aN) do @n
	 операторы
       enddo

где a1,a2,...,aN — массив скаляров,

@n — счетчик цикла, проходящий значения в диапазоне от 0 до (размер_массива - 1).

Типы переменных в массиве должны быть одинаковы. На каждой итерации цикла в операциях участвует @n-й элемент массива. Имя текущего элемента массива в операторе имеет вид: @_. Циклы for не могут быть вложенными.

Пример:
	reg 10 g(3)   // вектор
	reg 10 a      // скаляр
	reg 10 b      // скаляр
	reg 10 c      // скаляр
	reg  3 f      // скаляр
	...
	for(a,b,c) do @1
	  if(f == @1)
	     @_ = g[@1]
	  else
	     @_ = @_
	  endif
	enddo

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

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

Имеет вид:

       next <имя>

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

<имя>:

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

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

Пример:

loop3:
	{
	     a = b+1
	     next loop3
	}

Имя метки не должно совпадать с именами переменных и ключевыми словами схемы.

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

  • Оператор if

Общий вид:

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

 

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

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

Операторы 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
  • Оператор when

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

Однострочный оператор when имеет вид:

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

Вложенный оператор when также может быть записан в одну строку.

Многострочный оператор when имеет вид:

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

либо:

	<переменная> = (<условие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'

5. Построение схемы из многих компонентов.

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

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

  1. В секции объявления переменных объявить данный компонент:
  2.         component <имя_компонента>     
  3. В директорию заголовочных файлов USERCOMPONENTS поместить файл с именем <имя_компонента>.h, в котором должно присутствовать описание заголовка данного компонента. Заголовок должен быть написан на VHDL и заключен в именные скобки component <имя_компонента>, end component. Для компонента, написанного на Автокоде, данный пункт можно выполнить, воспользовавшись командой make_headfile, передав в качестве аргумента имя файла с описанием компонента на Автокоде: make_headfile my_component.avt.
  4. В разделе комбинационной логики включить соединения интерфейсных регистров (портов) компонента с регистрами данной схемы.

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

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

Следует обратить внимание на два последних порта. Порты Clk и Reset являются служебными. В отличие от основной схемы, где оба эти порта должны присутствовать в заголовке обязательно, в компоненты они включаются по необходимости. Использование в компоненте, написанном на Автокоде, тактированной части определяет наличие порта Clk, а использование непустого раздела начального сброса – наличие порта Reset. Для библиотечных компонентов перечень всех портов приведен в разделе библиотечные устройства плавающей арифметики.

Если компонент имеет GENERIC-параметры, значения для них записываются после оператора insert, до описания соединения портов:

insert FLOAT_SUM
	    WORDSIZE=32
	    .oper1(reg1)
	       ...
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
Часть 1.  Часть 2.  Часть 3.
 
 
 
 
 
 
 
 
  Тел. +7(499)220-79-72; E-mail: inform@kiam.ru