Макарченко И.П.Процессор в ПЛИС - это очень просто!- здесь идет чисто подготовка и редактирование - Современная электроника давно перешагнула тот рубеж, когда сложные схемы требовали усилий огромных коллективов и серьезных финансовых вложений. Сейчас, когда существуют программируемые логические интегральные схемы (ПЛИС), разработка стала настолько простой, что сложнейшие схемы можно создавать за считанные часы силами одного инженера. Здесь я собираюсь представить одну из подобных разработок, которая покажет, как можно делать свои процессоры в ПЛИС. И покажет, насколько это просто на современном уровне развития цифровой электроники. Целью данной статьи является не получение "минимального", "оптимального", "лучшего" или еще какого-либо иного экстра-свойства процессора, а представление принципа его построения и построение некой основы, которая может подойти для постройки иных процессоров и осуществления задумок программистов, которые раньше, быть может, просто боялись касаться такой сложнейшей сферы, как процессоростроение. У процессоров есть много свойств, но одним из главнейших является способность процессора исполнять некую программу действий. И в этом смысле процессор является исполнительным устройством для выполнения программ. С точки зрения такого определения процессор может оказаться каким угодно. И механическим, и электрическим, и гидравлическим. Тем не менее, в данном случае нас интересует только один тип процессоров. Это процессор цифровой электронный. Если разбираться в цифровых электронных процессорах и начать с самого начала, с истории их создания, то достаточно быстро станет ясно, что подобный процессор состоит из цифровых элементов определенных типов. А именно, из развесистых логических схем и множества регистров, которые в свою очередь состоят из триггеров (обычно D-типа). Пошагово разбирая подобные схемы можно придти к определенному выводу. А именно: Процессор - это цифровая электронная схема, представляющая собой конечный автомат, предназначенный для исполнения программы, записанной цифровыми кодами. Именно это определение и заложено в основу разработки, описываемой в данной статье. ![]() Для стабильной работы КА необходимо, чтобы время отработки логической части схемы было меньше, чем период синхронизирующих импульсов, поступающих на вход CLK. Это замечание будет важно в дальнейшем. В этой статье будет использоваться альтеровский HDL - AHDL, a проверяться в альтеровском САПР - Quartus II. При желании, код может быть легко переведен на VHDL ввиду своей прозрачности и простоты применяемых конструкций. В соответствии с этими заявлениями, составим верхний иерархический элемент процессора на языке 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; ) В первую очередь напомню, что у нас логическое ядро - это чисто комбинарторная схема, а значит, в секции элементов схемы нет никаких тригеров и защелок. Так же, исключены и все элементы, описывающие структурую ПЛИС. Вся реализация ядра отдается "на откуп компилятору", а элементами назначаются только некие шины, которые удобны для описания схемы и вовсе не обязаны присутствовать в ПЛИС явно. Да это и не нужно. Секция элементов схемы логического ядра: 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; |