ВЕРНУТЬСЯ

Макарченко И.П.

Процессор в ПЛИС - это очень просто!

- здесь идет чисто подготовка и редактирование -

Предисловие или немного лирики.

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

Здесь я собираюсь представить одну из подобных разработок, которая покажет, как можно делать свои процессоры в ПЛИС. И покажет, насколько это просто на современном уровне развития цифровой электроники.

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

1. Что такое процессор?

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

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

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

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

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

2. Конечный автомат

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

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

3. Способ описания процессора

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

В этой статье будет использоваться альтеровский HDL - AHDL, a проверяться в альтеровском САПР - Quartus II. При желании, код может быть легко переведен на VHDL ввиду своей прозрачности и простоты применяемых конструкций.

4. Составные части процессора

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

В соответствии с этими заявлениями, составим верхний иерархический элемент процессора на языке AHDL:

-- Writed by WingLion admin@winglion.ru
-- ver 1.00 
-- 22.01.2011
------------------------ 
-- 22.01.2011 - Start Kernel development


	TITLE "Single Core processor";
	
PARAMETERS (
	WIDTH = 16,
	DEPTH = 3
);	

-- подключение модулей, описываемых отдельно
include "LogicKernel"; -- "Логическое Ядро"
include "BigRegister"; -- "Большой Регистр"
	
SUBDESIGN SingleCore (
	CLK					: input;
	INPUTS[Ni-1..0]		: input = GND;
	OUTPUTS[No-1..0]	: output;
)
VARIABLE
	Kernel : LogicKernel;
	BigReg : BigRegister;
BEGIN
	-- помним, что CLK подается только на регистры
	BigReg.iCLK = CLK;
	
	-- входы и выходы всего процессора подключаются к логическому ядру
	Kernel.inputs[] = INPUTS[];
	OUTPUTS[] = Kernel.outputs[];

END;
Это очень обобщенная схема процессора с обезличенными входами и выходами. В реальной схеме входы и выходы приобретут конкретные названия и параметры ширины шин. Заметим только, что входы и выходы цепей, соединенных с регистром в обоих модулях процессора названы одинаково, и входы предваряются символом 'i', а выходы символом 'o'. Для AHDL эти символы являются просто частями имен и ничего не значат. Можно, например, у регистра названия входов и выходов поменять местами, и тогда последние строчки описания будут соединять одноименные выводы логического ядра и большого регистра. В каких-то случаях это может оказаться удобно, но здесь применяется другая идея. Смысл которой в том, чтобы программисту было как можно удобнее понимать, что куда подключается. Все входы начинаются с символа 'i', а выходы с символа 'о'.

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

Набор регистров будет следующим:

PC[WIDTH-1..0] - Programm Counter - регистр счетчика команд
CMD[WIDTH-1..0] - Commands Register - регистр команд
TOP[WIDTH-1..0] - Top of Stack - регистр вершины стека данных
DST[WIDTH*DEPTH-1..0] - Data Stack - регистры стека данных, 
                               оформленные одним общим регистром
RST[WIDTH*DEPTH-1..0] - Return Stack - регистры стека возвратов, 
                               оформленные одним общим регистром
Набор входов/выходов процессора:

iData[WIDTH-1..0] - входные данные, считанные из памяти или устройств
oData[WIDTH-1..0] - выходные данные, направляемые в память или устройство
oAddr[WIDTH-1..0] - выход адреса для памяти
memRD,memWR,memCS - сигналы управления памятью
devRD,devWR,devCS	- сигналы управления устройствами
Шины данных и адреса для устройств и памяти считаются совмещенными, хотя могут быть легко разделены при желании.

В соответствии с этими идеями, редактируем верхний файл проекта:

TITLE "Single Core processor";
	
PARAMETERS (
	WIDTH = 16,
	DEPTH = 3
);	

-- подключение модулей, описываемых отдельно
include "LogicKernel"; -- "Логическое Ядро"
include "BigRegister"; -- "Большой Регистр"
	
SUBDESIGN SingleCore (
	CLK					: input;
--	INPUTS[Ni-1..0]		: input = GND;
--	OUTPUTS[No-1..0]	: output;

	-- названия даются значимыми, 
	-- чтобы не требовалось особых объяснений для чего это выводы предназначены
	-- шина данных
	iData[WIDTH-1..0]	: input;
	oData[WIDTH-1..0]	: output;
	-- шина адреса		
--	iAddr[WIDTH-1..0]	: input; -- вход для адреса не нужен
	oAddr[WIDTH-1..0]	: output;
	-- управление памятью и устройствами -- только выходы
	memRD,memWR,memCS	: output;
	devRD,devWR,devCS	: output;	
)
VARIABLE
	-- подключаемые модули с параметрами верхнего модуля проекта
	Kernel : LogicKernel WITH (WIDTH = WIDTH,DEPTH = DEPTH);
	BigReg : BigRegister WITH (WIDTH = WIDTH,DEPTH = DEPTH);
BEGIN
	-- помним, что CLK подается только на регистры
	BigReg.clk = CLK;
	
	-- входы и выходы всего процессора подключаются к логическому ядру
--	Kernel.inputs[] = INPUTS[];
--	OUTPUTS[] = Kernel.outputs[];
	Kernel.iData[] = iData[];
	oData[] = Kernel.oData[];
	oAddr[] = Kernel.oAddr[];
	(memRD,memWR,memCS) = (Kernel.memRD,Kernel.memWR,kernel.memCS);
	(devRD,devWR,devCS) = (Kernel.devRD,Kernel.devWR,kernel.devCS);
	
	-- подключение входов регистра к логическому ядру
--	BigReg.iRG[] = Kernel.oRG[];
	BigReg.iCMD[]	= Kernel.oCMD[WIDTH-1..0];
	BigReg.iPC[WIDTH-1..0] = Kernel.oPC[WIDTH-1..0];
	BigReg.iTOP[WIDTH-1..0]	= kernel.oTOP[WIDTH-1..0];
	BigReg.iDST[(WIDTH*DEPTH-1)..0]	= Kernel.oDST[(WIDTH*DEPTH-1)..0];
	BigReg.iRST[(WIDTH*DEPTH-1)..0]	= kernel.oRST[(WIDTH*DEPTH-1)..0];

	
	
	-- и включаем линии обратной связи конечного автомата
--	Kernel.iRG[] = BigReg.oRG[];
	Kernel.iCMD[]	= BigReg.oCMD[WIDTH-1..0];
	Kernel.iPC[WIDTH-1..0] = BigReg.oPC[WIDTH-1..0];
	Kernel.iTOP[WIDTH-1..0]	= BigReg.oTOP[WIDTH-1..0];
	Kernel.iDST[(WIDTH*DEPTH-1)..0]	= BigReg.oDST[(WIDTH*DEPTH-1)..0];
	Kernel.iRST[(WIDTH*DEPTH-1)..0]	= BigReg.oRST[(WIDTH*DEPTH-1)..0];

END;
Теперь оформляем модули. Модуль "Большого Регистра" элементарно прост (помним, что два минуса это комментарий до конца строки, а так же коментарием является многострочный текст, ограниченный с двух сторон знаками процента):

-- Writed by WingLion admin@winglion.ru
-- ver 1.00 
-- 22.01.2011
------------------------ 
-- 22.01.2011 - Start Kernel development

	TITLE "Logic Kernel module for FPGA-processor";
	
PARAMETERS (
%
 Параметры, определяющие объем процессорного ядра
 процессор описывается таким образом, чтобы не возникало проблем при правильном изменении этих параметров
 правильное, значит изменение по правилам
 Первые правила следующие:
 - Ширина шины выбирается кратной 4-м битам
 - Глубина стеков не должна быть меньше 3-х
%
	WIDTH = 16, -- ширина шины
	DEPTH = 8 -- глубина стеков
);

SUBDESIGN BigRegister (
%
	для синхронизации нужен лишь один вход, но здесь введен 
	и выход синхронизации для того, чтобы графическое изображение модуля было симметрично
%
	-- синхронизация 
	iCLK				: input;
	oCLK				: output;

	-- идентификатор ядра процессора (пока не более 256 штук)
	-- в одноядерном варианте фактически не нужен, но входы введены, чтобы не забыть о них позже
	iID[7..0]			: input;
	oID[7..0]			: output;

	-- собственно входы и выходы логического ядра, подключаемые "наружу"
	-- в модуле "Большого Регистра" эти входы/выходы не нужны, поэтому закоментированы
	-- названия даются значимыми, 
	-- чтобы не требовалось особых объяснений для чего это выводы предназначены

--	-- шина данных
--	iData[WIDTH-1..0]	: input;
--	oData[WIDTH-1..0]	: output;
--	-- шина адреса		
--	iAddr[WIDTH-1..0]	: input;
--	oAddr[WIDTH-1..0]	: output;
--	-- управление памятью и устройствами
--	memRD,memWR,memCS	: output;
--	devRD,devWR,devCS	: output;	

	-- входы и выходы, подключаемые к "Большому Регистру"

	-- регистр команды
	iCMD[WIDTH-1..0]	: input;
	oCMD[WIDTH-1..0]	: output;	
	-- счетчик команд
	iPC[WIDTH-1..0]		: input;
	oPC[WIDTH-1..0]		: output;
	-- верхний элемент стека данных
	iTOP[WIDTH-1..0]	: input;
	oTOP[WIDTH-1..0]	: output;

%	
 Построение стека на логических ячейках является одним из самых простых вариантов, 
 поэтому здесь применено такое построение для стеков данных и возвратов
 и, естественно, стеки оказываются частями "Большого Регистра", а алгоритм их работы
 обеспечивает "Логическое Ядро"
% 
	-- стек данных
	iDST[(WIDTH*DEPTH-1)..0]	: input;
	oDST[(WIDTH*DEPTH-1)..0]	: output;
	-- стек возвратов
	iRST[(WIDTH*DEPTH-1)..0]	: input;
	oRST[(WIDTH*DEPTH-1)..0]	: output;	
	
)
VARIABLE
%
	Здесь все элементы имеют тип DFF - D-тригер - составляющие элементы Большого Регистра
	имена регистров повторяют выходные сигналы для упрощения описания
%
	-- счетчик команд
	oPC[WIDTH-1..0]		: DFF;
	-- верхний элемент стека данных
	oTOP[WIDTH-1..0]	: DFF;
	-- регистр команды
	oCMD[WIDTH-1..0]	: DFF;
	-- стек данных
	oDST[(WIDTH*DEPTH-1)..0]	: DFF;
	-- стек возвратов
	oRST[(WIDTH*DEPTH-1)..0]	: DFF;
	
BEGIN
-- трансляция сигналов, которые не меняются в процессоре
	oCLK = iCLK;
	oID[] = iID[];

	-- все синхронно с входным iCLK!
	(oPC[WIDTH-1..0],oTOP[WIDTH-1..0],oCMD[WIDTH-1..0],oDST[(WIDTH*DEPTH-1)..0],oRST[(WIDTH*DEPTH-1)..0]).clk = iCLK; 

	-- подключаем входы данных регистров к соответствующим входам модуля, 
	-- a выходы подключаются автоматом к одноименным регистрам
	(oPC[WIDTH-1..0],oTOP[WIDTH-1..0],oCMD[WIDTH-1..0],oDST[(WIDTH*DEPTH-1)..0],oRST[(WIDTH*DEPTH-1)..0]) =
	(iPC[WIDTH-1..0],iTOP[WIDTH-1..0],iCMD[WIDTH-1..0],iDST[(WIDTH*DEPTH-1)..0],iRST[(WIDTH*DEPTH-1)..0]);


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

Начало файла логического ядра:

-- Writed by WingLion admin@winglion.ru
-- ver 1.00 
-- 22.01.2011
------------------------ 
-- 22.01.2011 - Start Kernel development

	TITLE "Logic Kernel module for FPGA-processor";

-- константы для удобства описания работы с памятью и устройствами	
constant RDmem = B"010"; 
constant WRmem = B"100";
constant NOPmem = B"111";
	
PARAMETERS (
%
 Параметры, определяющие объем процессорного ядра
 процессор описывается таким образом, чтобы не возникало проблем при правильном изменении этих параметров
 правильное, значит изменение по правилам
 Первые правила следующие:
 - Ширина шины выбирается кратной 4-м битам
 - Глубина стеков не должна быть меньше 2-х
%
	WIDTH = 16, -- ширина шины
	DEPTH = 8 -- глубина стеков
);
SUBDESIGN LogicKernel (
%
 для синхронизации нужен лишь один вход, но здесь введен и выход синхронизации для того, 
 чтобы графическое изображение ядра было симметрично
%
	-- синхронизация в ядре формально не нужна, 
	-- так как это чисто комбинаторная схема, поэтому выводы закоментированы
--	iCLK				: input;
--	oCLK				: output;
	-- сигнал сброса подается непосредственно на Большой Регистр и здесь тоже лишний
--	iRESET				: input;

	-- идентификатор ядра процессора (зарезервировано пока не более 256 штук)
	-- в одноядерном варианте фактически не нужен, но входы введены, чтобы не забыть о них позже
	iID[7..0]			: input;
	oID[7..0]			: output;

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

	-- шина данных
	iData[WIDTH-1..0]	: input;
	oData[WIDTH-1..0]	: output;
	-- шина адреса		
	iAddr[WIDTH-1..0]	: input;
	oAddr[WIDTH-1..0]	: output;
	-- управление памятью и устройствами
	memRD,memWR,memCS	: output;
	devRD,devWR,devCS	: output;	

	-- входы и выходы, подключаемые к "Большому Регистру"

	-- регистр команды
	iCMD[WIDTH-1..0]	: input;
	oCMD[WIDTH-1..0]	: output;	
	-- счетчик команд
	iPC[WIDTH-1..0]		: input;
	oPC[WIDTH-1..0]		: output;
	-- верхний элемент стека данных
	iTOP[WIDTH-1..0]	: input;
	oTOP[WIDTH-1..0]	: output;

%	
 Построение стека на логических ячейках является одним из самых простых вариантов, 
 поэтому здесь применено такое построение для стеков данных и возвратов
 и, естественно, стеки оказываются частями "Большого Регистра", а алгоритм их работы
 обеспечивает "Логическое Ядро". Алгоритм стеков Можно было бы вынести из ядра, 
 но это запутало бы схему и сделало ее менее удобочитаемой.
% 
	-- стек данных
	iDST[(WIDTH*DEPTH-1)..0]	: input;
	oDST[(WIDTH*DEPTH-1)..0]	: output;
	-- стек возвратов
	iRST[(WIDTH*DEPTH-1)..0]	: input;
	oRST[(WIDTH*DEPTH-1)..0]	: output;	
	
	-- тестовые выходы
%
 введены для последующего тестирования ядра в симуляторе.
 эти выводы ничуть не мешают описанию схемы на AHDL, 
 поэтому не предпринимается никаких усилий для их исключения 
 в рабочем варианте схемы, что можно было бы сделать, например, с помощью параметризации
%
	test_CMD[3..0]			: output;
	test_PREF[3..0]			: output;
	test_TOP[WIDTH-1..0]		: output;
	test_PC[WIDTH-1..0]		: output;
	test_RSTo[WIDTH-1..0]		: output;
	test_DSTo[WIDTH-1..0]		: output;
	
)
5. Логическое ядро

Логическое ядро процессора требует особого внимания, поэтому исходный текст логического ядра в предыдущем параграфе и был оборван "на самом интересном месте".

В первую очередь напомню, что у нас логическое ядро - это чисто комбинарторная схема, а значит, в секции элементов схемы нет никаких тригеров и защелок. Так же, исключены и все элементы, описывающие структурую ПЛИС. Вся реализация ядра отдается "на откуп компилятору", а элементами назначаются только некие шины, которые удобны для описания схемы и вовсе не обязаны присутствовать в ПЛИС явно. Да это и не нужно.

Секция элементов схемы логического ядра:
VARIABLE
%
 элементы типа node - это просто провода внутри ПЛИС. Сами по себе они не занимают ресурсы ПЛИС, 
 а здесь используются только для наименования конкретных внутренних групп сигналов для удобства описания
%

	-- вспомогательные шины для упрощения описания работы с регистром команд. 
	CMD[3..0]	: node; -- четырехбитная команда
	PREF[3..0]	: node; -- четырехбитная префиксная команда

	CMDsh1_[WIDTH-1..0]	: node; -- сдвиг регистра команд на 4 бита 
	-- для выборки следующей команды после выполнения одной простой команды
	CMDsh2_[WIDTH-1..0]	: node; -- сдвиг регистра команд на 8 бита 
	-- для выборки следующей команды после выполнения команды с префиксом

	-- вспомогательные шины для упрощения описания стеков
	pushDST[WIDTH*DEPTH-1..0] : node;
	popDST[WIDTH*DEPTH-1..0] : node;
	swapDST[WIDTH*DEPTH-1..0] : node;
	pushRST[WIDTH*DEPTH-1..0] : node;
	popRST[WIDTH*DEPTH-1..0] : node;
	swapRST[WIDTH*DEPTH-1..0] : node;

	-- другие вспомогательные шины
	DSTo[WIDTH-1..0]	: node; -- подвершина стека данных
	RSTo[WIDTH-1..0]	: node; -- вершина стека возвратов
	toDST[WIDTH-1..0]	: node; -- шина данных записываемых в стек данных
	toRST[WIDTH-1..0]	: node; -- шина данных записываемых в стек возвратов
	zero[WIDTH-1..0]	: node; -- нулевая шина (нужна для удобства описания)
	
	-- вспомогательные шины для управления памятью
	RWC[2..0]	: node;
%
 шины для ALU
 на этих шинах формируются результаты соответствующих арифметическо-логических операций
 если по-хорошему, то АЛУ процессора так же как память надо вынести наружу, но здесь для простоты это не сделано
 результатом такого решения станет значительное уменьшение скорости работы этого процессора
%	
	ALU_DUP1[WIDTH-1..0]	: node; -- для действия с вершиной
	ALU_DUP2[WIDTH-1..0]	: node; -- для действия с подвершиной
	ALU_SWAP1[WIDTH-1..0]	: node; -- для действия с вершиной
	ALU_SWAP2[WIDTH-1..0]	: node; -- для действия с подвершиной
	ALU_DROP1[WIDTH-1..0]	: node; -- для действия с вершиной
	ALU_DROP2[WIDTH-1..0]	: node; -- для действия с подвершиной

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

Секция логического описания:
	
BEGIN
	-- заглушка для идентификатора ядра
	oID[] = iID[]; 
	
	-- подключение вспомогательных шин
	zero[] = GND; 
	CMD[] = iCMD[3..0];  -- четырехбитная внутренняя команда 
	PREF[] = iCMD[7..4]; -- четырехбитная команда - модификатор префикса
	CMDsh1_[] = (zero[3..0],iCMD[WIDTH-1..4]); -- сдвиг регистра команды на четыре бита
						   -- для выборки следующей четверки 
	CMDsh2_[] = (zero[7..0],iCMD[WIDTH-1..8]); -- сдвиг регистр команды на восемь бит
						   -- для выборки следующей четверки после команды с префиксом

	-- для ясности, считаем, что вершина стека - это старшие WIDTH разрядов 
	-- части "Большого Регистра", отвечающего за стек

	-- заталкивание в стек данных вершины стека (работа с самой вершиной описывается не здесь)
	pushDST[] = (toDST[],iDST[WIDTH*DEPTH-1..WIDTH]);
	-- выталкивание данного из стека данных значение вершины удаляется, а в дно записывается нуль
	popDST[] = (iDST[WIDTH*(DEPTH-1)-1..0],zero[]);
	-- обмен данных с вершиной стека
	swapDST[] = (toDST[],iDST[WIDTH*(DEPTH-1)-1..0]);
	-- подвершина стека
	DSTO[] = iDST[WIDTH*DEPTH-1..WIDTH*(DEPTH-1)];
	
	-- аналогично для стека возвратов
	pushRST[] = (toRST[],iRST[WIDTH*DEPTH-1..WIDTH]);
	popRST[] = (iRST[WIDTH*(DEPTH-1)-1..0],zero[]);
	swapRST[] = (toRST[],iRST[WIDTH*(DEPTH-1)-1..0]);
	RSTO[] = iRST[WIDTH*DEPTH-1..WIDTH*(DEPTH-1)];
	
	-- Главный дешифратор команды - оператор выбора по четырехбитному коду
	-- каждая команда описывается полностью. Все выходы ядра зависят от входов.
	-- 
	CASE CMD[] IS
		-- команда NOP - выполняет выборку следующей группы команд из памяти и, 
		-- соответственно, увеличивает счетчик команд на единицу. 
		-- Все остальные части процессора в этот момент ничего не делают
		WHEN 0 => 
			oData[] = iData[];	-- данные со входа транслируются на выход
			oAddr[] = iPC[];	-- адрес, откуда выбирается команда
			oPC[] = iPC[] + 1;	-- следующй адрес
			RWC[] = RDmem; 		-- читать данные из памяти			
			oCMD[] = iData[];	-- и помещать в регистр команд
			-- в осатальном ничего не делать
			oTOP[] = iTOP[]; 
			oDST[] = iDST[]; 
			oRST[] = iRST[]; 
			toDST[] = iTOP[];
			toRST[] = RSTo[];
		-- команда LIT - выбирает данное из потока команд и помещает на вершину стека данных
		WHEN 1 => 
			oData[] = iData[]; 
			oAddr[] = iPC[];	-- адрес, откуда читается литерал
			oPC[] = iPC[] + 1;	-- следующий адрес
			RWC[] = RDmem;		-- читать из памяти литерал
			oTOP[] = iData[];	-- считанный литерал запоминается в вершине стека данных
			toDST[] = iTOP[]; 	-- а данное, находившееся там			
			oDST[] = pushDST[]; -- заталкивается в стек данных
			oCMD[] = CMDsh1_[]; -- происходит сдвиг регистра команд для подачи следующей команды
			-- на стеке возвратов мертвая тишина
			oRST[] = iRST[];	
			toRST[] = RSTo[];
		-- команда CALL - вызов подпрограммы 
		WHEN 2 => 
			oData[] = iData[]; 
			oAddr[] = iPC[];	-- адрес, откуда читается адрес подпрограммы
			RWC[] = RDmem;		-- считанный адрес 
			oPC[] = iData[];	-- помещается в счетчик команд - переход на подпрограмму
			toRST[] = iPC[] + 1;-- адрес следующей исполнимой команды 
			oRST[] = pushRST[]; -- запоминается в стеке возвратов 
			oCMD[] = CMDsh1_[]; 
			-- на стеке данных ничего не делать
			toDST[] = iTOP[]; 
			oTOP[] = iTOP[];
			oDST[] = iDST[]; 
		-- команда RET - возврат из подпрограммы с непосредственной загрузкой следующей команды
		WHEN 3 => 
			oData[] = iData[]; 
			oAddr[] = RSTo[];	-- на шину адреса сразу же подается адрес из стека возвратов
			oPC[] = RSTo[] + 1; -- a в счетчик команд возвращается следующий адрес
			RWC[] = RDmem;		-- считанные из памяти данные 
			oCMD[] = iData[]; 	-- немедленно помещаются в регистр команды 
			oRST[] = popRST[];	-- возвращенный адрес выталкивается из стека возвратов
			-- в остальном ничего не делать
			oTOP[] = iTOP[]; 
			oDST[] = iDST[]; 
			toDST[] = iTOP[]; 
			toRST[] = RSTo[];
			
		-- команда IF
		WHEN 4 => oData[] = iData[]; 
			oAddr[] = iPC[];	-- адрес, с которого читается адрес перехода
			RWC[] = RDmem;		-- из памяти читается данное
			-- и одновременно проверяется условие для перехода
			-- здесь условие можно сделать и другим. 
			-- Можно, например, оформить команду IF как префикс, 
			-- и задавать условие перехода в следующей четверке бит
			-- чтобы сделать переход, если на стеке лежит не ноль, для фортового IF, 
			-- надо ставить LCONV перед таким IF
			-- здесь сделано так, чтобы комбинация TRUE IF работалa как JMP
			IF !(iTOP[] == 0) THEN 
				oPC[] = iData[]; -- если на стеке данных ноль, происходит переход
			ELSE 
				oPC[] = iPC[] + 1; -- иначе, счетчик команд переключается на следующий адрес
			END IF;
			oCMD[] = CMDsh1_[]; -- следующая команда кодируется за командой перехода, и если она нуль, 
				-- то перход  на исполнение новой ветки кода происходит сразу же
				-- в ином случае, после переключения счетчика команд происходит 
				-- исполнение еще нескольких команд. 
				-- например, дальше можно задать DROP для удаления флага со стека 
				-- и сделать это независимо от того, в какую ветку перейдет исполнение
			-- а дальше ни стек данных, ни стек возвратов, ни вершина стека данных не меняются
			oTOP[] = iTOP[]; 
			oDST[] = iDST[]; 
			oRST[] = iRST[]; 
			toDST[] = iTOP[];
			toRST[] = RSTo[];			
		
		-- команда DUP - префиксная команда арифметико-логическая, увеличивающая 
		-- количество элементов на стеке данных
		-- здесь счетчик команд остается неизменным. А шина памяти не активна.
		WHEN 5 => 
			oData[] = iData[]; 
			oAddr[] = iPC[]; -- на адрес транслируется состояние счетчика команд, 
			oPC[] = iPC[]; -- который не меняется
			RWC[] = NOPmem; -- и операция с памятью не производится
			oTOP[] = ALU_DUP1[]; -- в вершину загружается логическая функция ALU_DUP1, 
			-- которая задана в данном тексте после главного CASE 
			-- и зависит от второй четверки бит префиксной команды
			oDST[] = pushDST[];	 -- загрузка в стек данных
			toDST[] = ALU_DUP2[];	 -- второго логического результата 
			oCMD[] = CMDsh2_[];	 -- команда в регистре команд сдвигается сразу на две четверки
			oRST[] = iRST[];	 -- на стеке возвратов ничего не делается
		-- команда SWAP - префиксная команда арифметико-логическая, не меняющая 
		-- количество элементов на стеке данных
		WHEN 6 => 
			oData[] = iData[]; 
			oAddr[] = iPC[]; -- на адрес транслируется состояние счетчика команд, 
			oPC[] = iPC[]; -- который не меняется
			RWC[] = NOPmem; -- и операция с памятью не производится
			oTOP[] = ALU_SWAP1[]; -- на вершину стека загружается логическая функция, зависящая от PREF[]
			oDST[] = swapDST[];	  -- на стеке данных производится замена подвершины стека 
			toDST[] = ALU_SWAP2[]; -- на вторую логическую функцию
			oCMD[] = CMDsh2_[];	 -- команда в регистре команд сдвигается сразу на две четверки
			oRST[] = iRST[];	 -- на стеке возвратов ничего не делается
			
		-- команда DROP - префиксная команда арифметико-логическая, уменьшающая 
		-- количество элементов на стеке данных
		WHEN 7 => 
			oData[] = iData[]; 
			oAddr[] = iPC[]; -- на адрес транслируется состояние счетчика команд, 
			oPC[] = iPC[]; -- который не меняется
			RWC[] = NOPmem; -- и операция с памятью не производится
			oTOP[] = ALU_DROP1[]; -- первая логическая функция для вершины
			oDST[] = popDST[];    -- операция выталкивания верхнего элемента из стека данных
			toDST[] = ALU_DROP2[]; -- вторая функция здесь фактически не нужна
			oCMD[] = CMDsh2_[];	 -- команда в регистре команд сдвигается сразу на две четверки
			oRST[] = iRST[];	 -- на стеке возвратов ничего не делается
		-- команда @ - разыменование производится за один такт, не считая выборки команды
		WHEN 8 => 
			oData[] = iData[]; 
			oAddr[] = iTOP[];	-- на адресную шину подается значение из стека данных
			oPC[] = iPC[];		-- счетчик команд не меняется
			RWC[] = RDmem;		-- производится чтение из памяти
			oTOP[] = iData[];	-- считанное значение загружается в вершину, заменяя адрес
			oCMD[] = CMDsh1_[]; -- следующая команда получается сдвигом регистра команд на четыре бита
			-- а дальше ни стек данных, ни стек возвратов, не меняются
			oDST[] = iDST[]; 
			oRST[] = iRST[]; 
			toDST[] = iTOP[];
			toRST[] = RSTo[];
		-- команда ! - присвоение
		WHEN 9 => 
			oData[] = RSTo[]; 	-- на выходную шину данных подается подвершина стека данных
			oAddr[] = iTOP[];	-- на адресную шину подается значение вершины стека данных
			oPC[] = iPC[];		-- счетчик команд не меняется
			RWC[] = WRmem;		-- производится запись в память
			oCMD[] = CMDsh1_[]; -- следующая команда получается сдвигом регистра команд на четыре бита
			-- внимание!!, дальше идет "грязный хак!" - двойной DROP со стека данных
			-- компилятор такую схему сделает, но увеличит логический объем, занимаемым 
			-- реализацией стека данных, что, безусловно, замедлит процессор
			oTOP[] = iDST[WIDTH*(DEPTH-1)-1..WIDTH*(DEPTH-2)]; -- в вершину перемещается третий элемент
			oDST[] = (iDST[WIDTH*(DEPTH-2)-1..0],zero[],zero[]); -- и стек сдвигается сразу на два слова
			oRST[] = iRST[];	-- а стек возвратов не меняется
		-- команда R> -- 
		WHEN 10 => 
			oData[] = iData[]; -- на выход транслируются входные данные
			oAddr[] = iPC[];-- на адрес транслируется состояние счетчика команд, 
			oPC[] = iPC[]; 	-- который не меняется
			RWC[] = NOPmem; -- и операция с памятью не производится
			oCMD[] = CMDsh1_[]; -- следующая команда получается сдвигом регистра команд на четыре бита
			oTOP[] = RSTo[]; -- на вершину стека данных перемещается вершина стека возвратов
			oRST[] = popRST[]; -- которая выталкивается из стека возвратов
			oDST[] = pushDST[]; -- в стек данных заталкивается старое значение вершины
			toDST[] = iTOP[];	-- старое значение вершины
		-- команда >R --
		WHEN 11 =>
			oData[] = iData[]; -- на выход транслируются входные данные
			oAddr[] = iPC[];-- на адрес транслируется состояние счетчика команд, 
			oPC[] = iPC[]; 	-- который не меняется
			RWC[] = NOPmem; -- и операция с памятью не производится
			oCMD[] = CMDsh1_[]; -- следующая команда получается сдвигом регистра команд на четыре бита
			oTOP[] = DSTo[]; -- значение на вершину берестя из подвершины,
			oDST[] = popDST[]; -- которая выталкивается из стека данных
			toRST[] = iTOP[]; -- а удаленное значение 
			oRST[] = pushRST[]; -- пишется в стек возвратов
			
		-- дальше можно и другие команды сделать, но тут я пока закончу, 
		-- а чтобы не городить лишний текст, остальные операции будут эквивалентны NOP
		WHEN others => 
			oData[] = iData[];	-- данные со входа транслируются на выход
			oAddr[] = iPC[];	-- адрес, откуда выбирается команда
			oPC[] = iPC[] + 1;	-- следующй адрес
			RWC[] = RDmem; 		-- читать данные из памяти			
			oCMD[] = iData[];	-- и помещать в регистр команд
			-- в осатальном ничего не делать
			oTOP[] = iTOP[]; 
			oDST[] = iDST[]; 
			oRST[] = iRST[]; 
			toDST[] = iTOP[];
			toRST[] = RSTo[];
	END CASE;

%
 описание АЛУ для разных групп операций
 код операции АЛУ задается второй частью префиксной команды,
 и так же, как основная команда процессора, реализуется через CASE
%

-- CASE для команд типа DUP

	CASE PREF[] IS
		-- DUP самый обычный фортовый DUP
		WHEN 0 => ALU_DUP1[] = iTOP[]; ALU_DUP2[] = iTOP[];
		-- ZERO (FALSE) положить на стек 0
		WHEN 1 => ALU_DUP1[] = zero[]; ALU_DUP2[] = iTOP[];
		-- MINUS-ONE (TRUE) положить на стек -1
		WHEN 2 => ALU_DUP1[] = not zero[]; ALU_DUP2[] = iTOP[];
		-- ONE положить на стек 1
		WHEN 3 => ALU_DUP1[] = 1; ALU_DUP2[] = iTOP[];
		-- OVER скопировать второй элвмент стека
		WHEN 4 => ALU_DUP1[] = DSTo[]; ALU_DUP2[] = iTOP[];		
		-- R@ скопировать на вершину стека данных значение с вершины стека возвратов
		WHEN 4 => ALU_DUP1[] = RSTo[]; ALU_DUP2[] = iTOP[];	
			
%
 тут можно придумать еще много команд, берущих данные из воздуха и кладущих на стек 
 можно, например, читать данные с внешней шины, которую надо описать в секции со входами/выходами,
 а можно получить и внутренний сигнал самого процессора. Надо лишь помнить, 
 что в этом типе команд в подвершину стека запишется то, что здесь попадет на ALU_DUP2, a на вершину - ALU_DUP1
%		
		-- все остальное - обычный фортовый DUP
		WHEN others => ALU_DUP1[] = iTOP[]; ALU_DUP2[] = iTOP[];
	END CASE;

-- аналогичный  CASE для команд типа SWAP

	CASE PREF[] IS
		-- SWAP самый обычный фортовый SWAP
		WHEN 0 => ALU_SWAP1[] = DSTo[]; ALU_SWAP2[] = iTOP[];
		-- INC увеличить вершину стека на единицу
		WHEN 1 => ALU_SWAP1[] = iTOP[]+1; ALU_SWAP2[] = DSTo[];
		-- DEC уменьшить вершину стека на единицу
		WHEN 2 => ALU_SWAP1[] = iTOP[]-1; ALU_SWAP2[] = DSTo[];
		-- INV инвертировать вершину стека
		WHEN 3 => ALU_SWAP1[] = !iTOP[]; ALU_SWAP2[] = DSTo[];
		-- NEG поменять арифметический знак вершины стека
		WHEN 4 => ALU_SWAP1[] = - iTOP[]; ALU_SWAP2[] = DSTo[];
		-- BSWAP - перестановка двух половинок слова местами 
		-- если ширина данных будет отличаться от 16 или 32, опрация приобретет очень сомнительный смысл
		WHEN 5 => ALU_SWAP1[] = (iTOP[(WIDTH / 2)-1..0],iTOP[WIDTH-1..(WIDTH / 2)]); ALU_SWAP2[] = DSTo[];
		-- LCONV логическое конвертирование слова с инверсией
		WHEN 6 => ALU_SWAP2[] = DSTo[];
			-- конвертирует ноль в TRUE, а не ноль в FALSE
			IF (iTOP[] == 0) THEN ALU_SWAP1[] = -1; ELSE ALU_SWAP1[] = 0; END IF;
%
 на это место очень просится команда умножения, которая из двух слов делает двойное слово с их произведением и
 таким образом не меняет глубину стека данных, но я этого здесь делать не буду, чтобы не увеличивать объем примера
 сюда надо помещать сдвиги и иные операции с одним операндом, 
 здесь же, возможно, имеет смысл определить команды ROT и -ROT.
%		
		-- остальные - обычный фортовый SWAP
		WHEN others => ALU_SWAP1[] = DSTo[]; ALU_SWAP2[] = iTOP[];
	END CASE;

%
 операции с уменьшением глубины стека 
 сюда же попадают арифметические и логические двухместные операции
%	
	
	CASE PREF[] IS
		-- DROP
		WHEN 0 => ALU_DROP1[] = DSTo[]; -- ALU_DROP2[] - по сути просто не нужно
		-- NIP
		WHEN 1 => ALU_DROP1[] = iTOP[];
		-- ADD
		WHEN 2 => ALU_DROP1[] = iTOP[] + DSTo[];
		-- SUB
		WHEN 3 => ALU_DROP1[] = DSTo[] - iTOP[];
		-- AND
		WHEN 4 => ALU_DROP1[] = iTOP[] and DSTo[];
		-- OR
		WHEN 5 => ALU_DROP1[] = iTOP[] or DSTo[];
		-- XOR
		WHEN 6 => ALU_DROP1[] = iTOP[] xor DSTo[];
		
%
	что еще можно здесь делать, перечислять не буду
%
--		WHEN 7 => ALU_DROP1[] = iTOP[] + DSTo[];
		-- все остальные DROP
		WHEN others => ALU_DROP1[] = DSTo[];
		
	END CASE;
	ALU_DROP2[] = zero[]; -- по сути эта шина не нужна здесь просто заглушка


%
 сигналы управления памятью
%	
	(memRD,memWR,memCS) = RWC[];
	(devRD,devWR,devCS) = NOPmem; -- заглушка - команд для работы с устройствами пока просто нет


%
	тестовые выходы для проверки работы процессорного ядра в симуляторе
%
	test_CMD[] = CMD[];
	test_PREF[] = PREF[] and ((CMD[] == 5) or (CMD[] == 6) or (CMD[] == 7));
	test_TOP[] = oTOP[];
	test_PC[] = oPC[];
	
	test_RSTo[] = RSTo[];
	test_DSTo[] = DSTo[];
	

END;