Компьютер на плис




Компьютер на плис

Компьютер на плис

Компьютер на плис

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


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

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


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

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

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

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 -- 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[WIDTHDEPTH-1..0] - Data Stack - регистры стека данных, оформленные одним общим регистром RST[WIDTHDEPTH-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[(WIDTHDEPTH-1)..0] = Kernel.oDST[(WIDTHDEPTH-1)..0]; BigReg.iRST[(WIDTHDEPTH-1)..0] = kernel.oRST[(WIDTHDEPTH-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[(WIDTHDEPTH-1)..0] = BigReg.oDST[(WIDTHDEPTH-1)..0]; Kernel.iRST[(WIDTHDEPTH-1)..0] = BigReg.oRST[(WIDTHDEPTH-1)..0]; END; Теперь оформляем модули. Модуль "Большого Регистра" элементарно прост (помним, что два минуса это комментарий до конца строки, а так же коментарием является многострочный текст, ограниченный с двух сторон знаками процента): -- Writed by WingLion -- 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[(WIDTHDEPTH-1)..0] : input; oDST[(WIDTHDEPTH-1)..0] : output; -- стек возвратов iRST[(WIDTHDEPTH-1)..0] : input; oRST[(WIDTHDEPTH-1)..0] : output; ) VARIABLE % Здесь все элементы имеют тип DFF - D-тригер - составляющие элементы Большого Регистра имена регистров повторяют выходные сигналы для упрощения описания % -- счетчик команд oPC[WIDTH-1..0] : DFF; -- верхний элемент стека данных oTOP[WIDTH-1..0] : DFF; -- регистр команды oCMD[WIDTH-1..0] : DFF; -- стек данных oDST[(WIDTHDEPTH-1)..0] : DFF; -- стек возвратов oRST[(WIDTHDEPTH-1)..0] : DFF; BEGIN -- трансляция сигналов, которые не меняются в процессоре oCLK = iCLK; oID[] = iID[]; -- все синхронно с входным iCLK! (oPC[WIDTH-1..0],oTOP[WIDTH-1..0],oCMD[WIDTH-1..0],oDST[(WIDTHDEPTH-1)..0],oRST[(WIDTHDEPTH-1)..0]).clk = iCLK; -- подключаем входы данных регистров к соответствующим входам модуля, -- a выходы подключаются автоматом к одноименным регистрам (oPC[WIDTH-1..0],oTOP[WIDTH-1..0],oCMD[WIDTH-1..0],oDST[(WIDTHDEPTH-1)..0],oRST[(WIDTHDEPTH-1)..0]) = (iPC[WIDTH-1..0],iTOP[WIDTH-1..0],iCMD[WIDTH-1..0],iDST[(WIDTHDEPTH-1)..0],iRST[(WIDTHDEPTH-1)..0]); END; Можно было бы и не оформлять большой регистр отдельным файлом, а вписать все его элементы в файл верхнего уровня, но так не сделано намеренно с учетом дальнейшей нацеленности на модификацию этого процессора от одноядерного к многоядерному варианту, в котором разделение регистров и логики имеет существенное значение.

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

-- Writed by WingLion -- 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[(WIDTHDEPTH-1)..0] : input; oDST[(WIDTHDEPTH-1)..0] : output; -- стек возвратов iRST[(WIDTHDEPTH-1)..0] : input; oRST[(WIDTHDEPTH-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[WIDTHDEPTH-1..0] : node; popDST[WIDTHDEPTH-1..0] : node; swapDST[WIDTHDEPTH-1..0] : node; pushRST[WIDTHDEPTH-1..0] : node; popRST[WIDTHDEPTH-1..0] : node; swapRST[WIDTHDEPTH-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[WIDTHDEPTH-1..WIDTH]); -- выталкивание данного из стека данных значение вершины удаляется, а в дно записывается нуль popDST[] = (iDST[WIDTH(DEPTH-1)-1..0],zero[]); -- обмен данных с вершиной стека swapDST[] = (toDST[],iDST[WIDTH(DEPTH-1)-1..0]); -- подвершина стека DSTO[] = iDST[WIDTHDEPTH-1..WIDTH(DEPTH-1)]; -- аналогично для стека возвратов pushRST[] = (toRST[],iRST[WIDTHDEPTH-1..WIDTH]); popRST[] = (iRST[WIDTH(DEPTH-1)-1..0],zero[]); swapRST[] = (toRST[],iRST[WIDTH(DEPTH-1)-1..0]); RSTO[] = iRST[WIDTHDEPTH-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;
Источник: http://ahdl.winglion.ru/processor.htm



Компьютер на плис фото



Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Компьютер на плис

Похожие записи: