Можно ли добавить в микропроцессор инструкции (команды)? Если вы используете микросхемы ПЛИС / FPGA с реконфигурируемой логикой и микропроцессорное ядро, которые синтезирутся из описания на языках Verilog и VHDL, то можете. Причем это будет «честное», настоящее расширение системы команд, а не трюк типа программной эмуляции инструкции в обработчике исключения от зарезервированной команды, и не «микрокод», популярный в исторических процессорах 1970-х годов.

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

Главная проблема с модификацией исходников дизайна процессора на Verilog или VHDL — трудоемкость. Нужно понять, как работает логика различных блоков и избежать нежелательных побочных эффектов. К счастью, существует способ расширения процессора, который превращает семестровый студенческий проект в нечто, что студент может спроектировать за одну лабораторную работу. Этот способ — интерфейс CorExtend / UDI (User Defined Instructions) в микропроцессорном ядре MIPS microAptiv UP, которое используется в пакете для образования MIPSfpga.

В рамках университетской программы MIPSfpga компании Imagination Technologies можно скачать настоящий индустриальный код на Verilog процессора MIPS microAptiv UP.
https://community.imgtec.com/university/resources/

Одним из распространенных применений UDI является манипуляции битами в алгоритмах шифрования. Другой пример — создание специальных инструкций для ускорения алгоритмов ЦОС Accelerating DSP Filter Loops with MIPS® CorExtend® Instructions.

Однако в наборе документации к MIPSfpga интерфейс между ядром и CorExtend описан недостаточно подробно. Подробная документация предоставляется только лицензиатам ядер. В этой статье представлено мое описание данного интерфейса на основе изучения исходного кода. Его можно также скачать в формате pdf MIPS microAptiv UP Processor CorExtend UDI interface protocol guide.

CorExtend занимает следующее место в RTL иерархии ядра m14k microAptiv.

CorExtend RTL Hierarchy

Все сигналы уровня m14k_cpu, включая CorExtend UDI, описаны в документе MIPS32 microAptiv UP Processor Core Family Integrators Guide (Таблица 2.3 Signal Descriptions for m14k cpu Level). Лучше смотреть там, но для наглядности ниже приведена выдержка оттуда исключительно с сигналами CorExtend UDI.
Название сигнала Тип Описание
UDI_ir_e[31:0] Out Полное слово инструкции. Хотя модуль получает rs и rt операнды, передается инструкция целиком, чтобы была возможность передавать данные в полях адресов операндов. Обратите внимание, что тот, кто будет реализовывать собственный UDI блок, должен самостоятельно декодировать поля Opcode и Function field.
UDI_irvalid_e Out Сигнал valid для слова инструкции (UDI_ir_e).
UDI_rs_e[31:0] Out Операнд rs.
UDI_rt_e[31:0] Out Операнд rt.
UDI_endianb_e Out Сигнализирует, что инструкция исполняется в режиме Big Endian. Сигнал обычно не нужен, кроме случаев, когда a) инструкция UDI оперирует с частью слова данных и зависима от endian, b) UDI блок работает в режиме big-endian.
UDI_kd_mode e Out Сигнализирует, что инструкция исполняется в kernel или debug режиме. Может быть использовано для предотвращения исполнения некоторых инструкций в kernel или debug режимах.
UDI_kill_m Out Сигнал kill от исключения, сгенерированного предыдущей инструкцией. Может быть использован для снятия UDI_stall_m, что уменьшит задержку у многотактовых UDI инструкций, чьи результаты не будут использоваться.
UDI_start_e Out Сигнал mpc_run_ie из блока управления (Master Pipeline Control).
UDI_run_m Out Сигнал mpc_run_m, использующийся как valid для UDI_kill_m.
UDI_greset Out Reset, может быть использован для сброса автоматов в UDI блоке.
UDI_gclk Out Clock input в UDI блоке.
UDI gscanenable Out Global scan enable.
UDI_ri_e In Сигнализирует Master Pipeline Control (MPC) о том, что исполняемая в данный момент инструкция зарезервирована. Однако MPC примет его во внимание, только если инструкция входит в подмножество user-defined инструкций SPECIAL2 (биты [5:4] в инструкции 2'b01).
UDI_rd_m[31:0] In 32-битный результат выполненной инструкции, доступный на стадии M (Memory fetch).
UDI_wrreg_e[4:0] In Адрес регистра для записи результата выполнения user-defined инструкции. Также передается в MPC.
UDI_stall_m In Сигнализирует, что UDI блок выполняет многотактовую инструкцию и должен остановить конвейер перед записью в регистр общего назначения. должен быть установлен в 0 для однотактовых инструкций. Сигнал стадии M.
UDI_present In Статический сигнал, означающий что UDI блок доступен.
UDI_honor_cee In Показывает, должен ли процессор принимать во внимание CorExtend Enable (CEE) бит Status регистра. Если UDI_honor_cee установлен в единицу и Status.CEE бит Status регистра не установлен, сгенерируется исключение CorExtend Unusable Exception.

Кроме этих сигналов у CorExtend блока есть внешние сигналы с количеством бит, определяемым разработчиком.
Название сигнала Тип Описание
UDI_toudi[x-1:0] In Внешний вход CorExtend блока переменной длины.
UDI_fromudi[x-1:0] Out Внешний выход CorExtend блока переменной длины.

Чтобы создать свой CorExtend блок необходимо изменить файлы m14k_edp_buf misc и m14k_udi_stub should. В файле
m14k_edp_buf_misc входные и выходные порты можно соединить, например, вот так.

assign UDI_ir_e[31:0]		= mpc_ir_e	;
assign UDI_irvalid_e		= mpc_irval_e	;
assign UDI_rs_e[31:0]		= edp_abus_e	;
assign UDI_rt_e[31:0]		= edp_bbus_e	;
assign UDI_endianb_e		= cpz_rbigend_e	;
assign UDI_kd_mode_e		= cpz_kuc_e	;
assign UDI_kill_m		= mpc_killmd_m	;
assign UDI_start_e		= mpc_run_ie    ;
assign UDI_run_m		= mpc_run_m	;
assign UDI_greset		= greset	;
assign UDI_gscanenable		= gscanenable	;
assign UDI_gclk			= gclk		;

assign edp_udi_wrreg_e[4:0]	= UDI_wrreg_e	;
assign edp_udi_ri_e		= UDI_ri_e	;
assign edp_udi_stall_m		= UDI_stall_m	;
assign edp_udi_present		= UDI_present	;
assign edp_udi_honor_cee	= UDI_honor_cee	;
mvp_mux2 #(32) _res_m_31_0_(res_m[31:0],mpc_udislt_sel_m, asp_m, UDI_rd_m);


Сам CorExtend блок должен заменить файл m14k_udi_stub. Пример взаимодействия между CorExtend и ядром microAptiv UP представлена на временной диаграмме ниже.
CorExtend interface protocol waveform

Сигнал UDI_present должен быть подтянут к единице. UDI_honor_cee может быть подтянут к нулю. Если его подтянуть к единице, то прежде чем выполнять инструкции CorExtend необходимо будет взвести бит Status CEE инструкцией mtc0. Если этого не сделать, появится исключение CorExtend unusable exception и на следующем такте после UDI_start_e выставится сигнал UDI_kill_m на два такта.

Частично инструкция должна быть декодирована на том же такте, на котором появился сигнал UDI_start_e. Это необходимо для формирования сигнала UDI_ri_e, который следует выставлять одновременно с появлением UDI_start_e в случае если инструкция зарезервирована. Если инструкция подразумевает запись результата в регистр общего назначения, его адрес также должен быть выставлен на UDI_wrreg_e[4:0] одновременно с UDI_start_e. Остальные поля инструкции можно записать в регистр и декодировать позднее.

Сигнал UDI_wrreg_e[4:0] может адресовать 31 регистр общего назначения, значение 5'd0 означает отсутствие записи в регистры.

Результат UDI инструкции, который должен быть записан в регистр общего назначения должен выставляться на UDI_rd_m[31:0] на следующем такте после UDI_start_e. Если он должен быть записан позднее, на следующем такте после UDI_start_e следует выставить UDI_stall_m. UDI_stall_m следует сбросить в ноль за один такт перед выставлением результата на UDI_rd_m[31:0].

На рисунке ниже представлен общий формат инструкции UDI. Поле Major opcode входит в подмножество special2 и равно 6'd28. Поля RS и RT содержат адреса регистров операндов. Биты 15..6 могут быть использованы по усмотрению разработчика. Например, туда можно записать адрес регистра назначения для записи результата или передать мгновенное значение. Поле Function field состоит из бит 5..4, которые всегда равны 2'b01, и бит 3..0, с помощью которых можно закодировать до 16 UDI инструкций.



Разработка CorExtend блока проиллюстрирована следующим примером DSP ускорителя, который рассчитывает мгновенную мощность комплексного сигнала, который определен, как
P(t) = a2(t) + b2(t),
где a(t) и b(t) — действительная и мнимая части сигнала соответственно.
Эта операция полезна для детектирования сигнала путем его сравнения с пороговым значением.
В таблице ниже приведены инструкции DSP ускорителя.
Инструкция Описание function field
UDI0 RD; RS; RT RD = RS[31:16]2 + RT[31:16]2 6'b010000
UDI1 RD; RS; RT RD = (RS[31:16]2 + RT[31:16]2) >> 1 6'b010001
UDI2 RD; RS RD = RS[31:16]2 6'b010010
UDI3 RS stored_threshold = RS 6'b010011
UDI4 RD; RS; RT RD = ( (RS[31:16]2 + RT[31:16]2) > stored_threshold )? 1:0 6'b010100
UDI5 RD; RS; RT RD = ( ((RS[31:16]2 + RT[31:16]2) >> 1) > stored_threshold )? 1:0 6'b010101
UDI6 RD; RS; RT RD = ( RS[31:16]2 > stored_threshold )? 1:0 6'b010110

UDI0 вычисляет мгновенную мощность сигнала. Операнды RS и RT содержат 16-битные действительные и мнимые части сигнала. 32-битный результат записывается в регистр общего назначения по адресу RD.

UDI1 делает то же, что и UDI0. Различие в том, что UDI1 сдвигает результат для предотвращения переполнения.

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

UDI3 записывает 32-битное пороговое значение во внутренний регистр блока CorExtend, результат не возвращается.

UDI4, UDI5 и UDI6 выполняют операции инструкций UDI0, UDI1 и UDI2 соответственно и сравнивают результат с сохраненным пороговым значением. Если порог превышен, возвращается результат 32'd1, иначе 32'd0.

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

Код ниже показывает пример программы на языке MIPS ассемблера для тестирования всех разработанных UDI инструкций.
Machine Code	Instruction Address		Assembly Code
3c088000	// bfc00000:		        lui	$8, 0x8000
3c09beaf	// bfc00004:		        lui	$9, 0xbeaf
71095010	// bfc00008:		        udi0	$8 $9 $10
71095011	// bfc0000c:		        udi1	$8 $9 $10
71005012	// bfc00010:		        udi2	$8 $10
3c0bbeaf	// bfc00014:		        lui	$11, 0xbeaf
356bdead	// bfc00018:		        ori	$11,$11, 0xdead
71600013	// bfc0001c:		        udi3	$11
71095014	// bfc00020:		  L1:   udi4	$8 $9 $10
71095015	// bfc00024:		        udi5	$8 $9 $10
71095016	// bfc00028:		        udi6	$8 $9 $10 
3c0b0001	// bfc0002c:		        lui	$11, 0x0001
356bfeed	// bfc00030:		        ori	$11,$11, 0xfeed
71600013	// bfc00034:		        udi3	$11 
1000fff9	// bfc00038:		        beq	$0, $0, L1
00000000	// bfc0003c:		        nop

Пример проекта с реализацией блока CorExtend с вышеперечисленными инструкциями можно скачать по ссылке https://github.com/zatslogic/UDI_example.

Проект включает в себя исходный код для симуляции за исключением файлов из директории rtl_up. Чтобы их получить необходимо зарегистрироваться в Imagination University Programme и с делать запрос на загрузку (https://community.imgtec.com/downloads/mipsfpga-getting-started-version-1-2). Также для запуска симуляции необходимо иметь XilinxCorelib, его можно скомпилировать в Vivado командой compile_simlib.

Проект включает в себя два варианта реализации CorExtend блока. В первом случае все инструкции UDI выполняются за один такт. Во втором были вставлены дополнительные регистры и некоторые инструкции требуют больше тактов для исполнения. Это было сделано специально, чтобы задействовать сигнал UDI_stall_m.

На временных диаграммах далее представлена симуляция приведенной выше программы на ассемблере.

Для первого однотактного варианта блока инструкции UDI0, UDI1 и UDI2 выполняются следующим образом.


Можно видеть, что инструкции появляются на UDI_ir_e с сигналами UDI_irvalid_e и UDI_start_e. Операнды также доступны на этом такте. На этом же такте формируется адрес записи результата в регистр общего назначения. Результат выставляется на UDI_rd_m на следующем такте.
Кроме того, на временной диаграмме представлены сигналы блока регистров общего назначения. Адрес записи в них может быть виден на mpc_dest_w. Данные представлены на edp_wrdata_w с сигналом write enable на mpc_rfwrite_w. Сигналы mpc_rega_i и mpc_regb_i содержат адреса операндов, считываемых из регистров общего назначения.

На данной временной диаграмме показано выполнение инструкций UDI3, UDI4, UDI5 и UDI6.


Как видно из листинга программы на ассемблере UDI3 записывает 0xbeafdead в stored_threshold. Результаты выполнения инструкций UDI4, UDI5 и UDI6 — нули, т.к порог ни разу не был превышен.

На следующей временной диаграмме выполняются инструкции UDI3, UDI4, UDI5 и UDI6 после условного перехода. Теперь пороговое значение меньше результатов вычислений и можно видеть, что в регистры результатов пишется 0x000001.


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

На данной временной диаграмме видно исполнение инструкций UDI0, UDI1 и UDI2. На время выполнения вычислений выставляется сигнал UDI_stall_m. На следующий такт после его снятия выставляется результат на UDI_rd_m. Еще через такт результат записывается в регистры общего назначения.


На данной временной диаграмме представлены инструкции UDI4, UDI5 и UDI6 с сигналом UDI_stall_m.


На после ней временно диаграмме выполняются инструкции UDI4, UDI5 и UDI6 после условного перехода.


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

Комментарии (3)


  1. nerudo
    08.02.2016 11:43

    Если это студенческая лабораторная работа, то в NIOSе это и вовсе тогда делается в рамках 5-минутного перекура…


    1. kirill90
      08.02.2016 11:51
      +1

      Безусловно, только здесь смысл не в том, чтобы сделать рабочий проект для ПЛИС, а в том, чтобы добавить инструкции в RTL код процессора, который потом применяется в ASIC. И работа с NIOS не заменит студентам этот опыт.


    1. YuriPanchul
      08.02.2016 11:54
      +2

      Насчет NIOS II против MIPSfpga. MIPSfpga служить для совсем других целей, чем NIOS II:

      1. NIOS II не предназначен для изучения внутренностей процессора и экспериментирования с одновременным изменением софтвера и хардвера, а вот MIPSfpga — предназначен. NIOS II используется как правило как «черный ящик» для построения систем. В MIPSfpga можно делать добавления к конвейеру, проверять, как это меняет поведение программ, и разбираться что как устроено.

      2. NIOS II — это процессор, привязанный к Альтере и состоящий из альтера-специфичных блоков. Хотя у NIOS II есть вариант для ASIC от Synopsys, но студенты не могут использовать один и тот же код для FPGA и ASIC. И уж тем более для Xilinx.

      3. MIPS microAptiv UP внутри MIPSfpga используется десятками лицензиатов для создания ASIC-ов, в том числе Samsung Artik 1 и Microchip PIC32MZ. С NIOS II студент не знакомится с популярным ASIC-овым ядром.