Продолжение, начало
Лабораторная работа 5: делаем процессор MIPS (1),
Лабораторная работа 5: делаем процессор MIPS (2) и
Лабораторная работа 5: делаем процессор MIPS (3) << Полный код модуля Verilog -
mips.v Подошли совсем вплотную к кульминация нашего дизайна - далее в каждой из веток case следует релизация соответствующей команды.
Команды типа R-type: add и sub
Вспоминаем, что поле opcode для всех команд R-type равно нулю, а окончательный тип команды определяет поле funct, поэтому сразу же открываем по этому полю 2е вложенное ветвление case, чтобы отличить add от sub.
INSTR_OP_RTYPE:
case(instr_rtype_funct)
Команда add
А вот и он, код для реализации команды сложения значений двух регистров add:
INSTR_RTYPE_FUNCT_ADD:
begin
// add $s0, $s1, $s2
// $s0 = $s1 + $s2
// rs=$s1, rt=$s2, rd=$s0
// rf_rd1 немедленно получит значение регистра rs
rf_ra1 = instr_rtype_rs;
// rf_rd2 немедленно получит значение регистра rt
rf_ra2 = instr_rtype_rt;
// записать значение в регистр rd на следующий такт clock
rf_wa = instr_rtype_rd;
rf_wd = rf_rd1 + rf_rd2;
rf_we = 1;
end
Для первого раза разберем по строкам.
Для начала вспомним, что команда add работает следующим образом:
add $s0, $s1, $s2
Результат работы команды:
$s0 = $s1 + $s2 - в регистр $s0 попадает сумма значений двух регистров $s1 и $s2
Поля операции:
rs=$s1, rt=$s2, rd=$s0
Считываем значение 1го операнда: устанавливаем адрес чтения регистра rf_ra1 в значение поля операции rs (5 бит) и вспоминаем, что сразу после этого 32хбитное значение регистра появится в переменной rf_rd1.
rf_ra1 = instr_rtype_rs;
Аналогично считываем значение 2го операнда: устанавливаем адрес чтения регистра rf_ra2 в значение поля операции rt (5 бит) и вспоминаем, что сразу после этого 32хбитное значение регистра появится в переменной rf_rd2.
rf_ra2 = instr_rtype_rt;
Значения операндов-источников rs и rt считали - на них указывают переменные rf_rd1 и rf_rd2 соответственно. Теперь готовим результат и запись в файл регистров по адресу, указанному в 3м операнде-назначении rd.
Устанавливаем значение адреса записи регистра rf_wa в значение поля операции rd.
rf_wa = instr_rtype_rd;
Устанавливаем значение для записи в регистр rf_wd (32 бит) - собственно сумма значений двух регистров rs и rt, тк. у нас операция сложения.
rf_wd = rf_rd1 + rf_rd2;
Включем флаг разрешения записи в файл регистров rf_we, таким образом новое значение с суммой регистров rs и rt отправится в регистр rd на следующий такт сигнала clock.
rf_we = 1;
Команда sub
В чем отличие от команды add предлагается разобрать самостоятельно.
INSTR_RTYPE_FUNCT_SUB:
begin
// sub $s0, $s1, $s2
// $s0 = $s1 - $s2
// rs=$s1, rt=$s2, rd=$s0
// rf_rd1 немедленно получит значение регистра rs
rf_ra1 = instr_rtype_rs;
// rf_rd2 немедленно получит значение регистра rt
rf_ra2 = instr_rtype_rt;
// записать значение в регистр rd на следующий такт clock
rf_wa = instr_rtype_rd;
rf_wd = rf_rd1 - rf_rd2;
rf_we = 1;
end
endcase
Закрываем вложенный case для команд R-type, продолжаем с остальными командами всех остальных типов.
Команда lw
Загрузить слово (32 бит) из памяти данных в регистр. По специкации команды адрес слова в памяти данных является суммой значений регистра-операнда rs и константы imm.
1. Получаем значение регистра rs в rf_rd1 присвоив адрес регистра переменной rf_ra1.
2. Вычисляем адрес как rf_rd1 + imm (значение регистра rs + константа imm) и одновременно получаем значение слова в памяти данных в переменной dmem_rd, присвоив значение вычисленного адреса переменной dmem_addr (механизм аналогичен описанному механизму получения значения из регистра).
3. Осуществляем запись значения dmem_rd в регистр rt на следующий такт clock уже известным способом.
INSTR_OP_LW:
begin
// lw $s0, 4 ($0)
// загрузить слово (32 bit) из памяти по адресу addr $0 + 4 в регистр $s0
// rs=$0, rt=$s0, imm=4
// rf_rd1 немедленно получит значение регистра rs
rf_ra1 = instr_itype_rs;
// прочитать значение из памяти,
// dmem_rd немедленно получит значение по адресу dmem_addr
dmem_addr = rf_rd1 + instr_itype_imm;
// записать значение в регистр rt на следующий такт clock
rf_wa = instr_itype_rt;
rf_wd = dmem_rd;
rf_we = 1;
end
Команда sw
Сохраняем слово (32 бит) из указанного регистра в память данных. По специкации команды адрес слова в памяти данных является суммой значений регистра-операнда rs и константы imm.
Механизм записи в память данных очевидно аналогичен механизму записи в файл регистров - указываем адрес, значение, включаем флаг записи и ждем следующий такт clock.
INSTR_OP_SW:
begin
// sw $s0, 4 ($0)
// сохранить слово (32 bit) в память по адресу addr $0 + 4 из регистра $s0
// rs=$0, rt=$s0, imm=4
// rf_rd1 немедленно получит значение регистра rs
rf_ra1 = instr_itype_rs;
// rf_rd2 немедленно получит значение регистра rt
rf_ra2 = instr_itype_rt;
// записать значение в память данных на следующий такт clock
dmem_addr = rf_rd1 + instr_itype_imm;
dmem_wd = rf_rd2;
dmem_we = 1;
end
Команда addi
Сложить значение регистра и константы, результат сохранить в регистр. Все очевидно.
INSTR_OP_ADDI:
begin
// addi $s0, $s1, 4
// $s0 = $s1 + 4
// rs=$s0, rt=$s1, imm=4
// rf_rd1 немедленно получит значение регистра rs
rf_ra1 = instr_itype_rs;
// записать значение в регистр rt на следующий такт clock
rf_wa = instr_itype_rt;
rf_wd = rf_rd1 + instr_itype_imm;
rf_we = 1;
end
Команда beq
От команд арифметики и манипулирования данными переходим к командам, влияющим на ход программы.
Команд beq - условный переход - позволяет организовать конструкцию "if" внутри ассемблерной программы. Если значения регистров-операндов равны, осуществляем передачу управления программой (просто переключаем счетчик program counter) на определяемый константой imm адрес. Если не равны, то просто ничего не делаем и двигаемся дальше.
Получение значений регистров-операндов стандартно. Передача управления на новый адрес в памяти инструкций осуществляется записью значения адреса в переменную-регистр pc_next - таким образом на следующий такт clock это значение будет присвоено актуальному значению pc со всеми вытекающими (механизм переключения счетчика программы и загрузки инстукций уже обсуждали выше).
Важное замечание: адрес перехода по спецификации команды в оригинальной архитектуре MIPS задается как относительное значение для текущего значения счетчика инструкций. Т.е. операнд-константа imm определяет не абсолютный адрес, а задает правило перехода типа "перейти на 4 строки вперед" или "перейти на 12 строк назад" относительно текущего положения счетчика инструкций. Это правильно и логично, т.к. с абсолютными адресами просто не получится адресовать всю память инструкций, т.к. полный адрес представляет 32хбитное значение, а на константу imm в команде beq отведено всего 16 бит. Но в данной лабе я решил немного облегчить себе жизнь и реализовать переход beq так, что операнд-константа imm задает абсолютный адрес перехода внутри памяти инструкций. Это не соответствует спецификации MIPS, но так мне не пришлось разбираться с форматом отрицательных двоичных значений, да и при ручной трансляции ассемблерной программы в машинные коды с абсолютными адресами работать немного легче. Вероятно этот нюанс будет исправлен в будущих версиях лабы, но для демонстрации работоспособности процессора все нормально и так - можно считать это особенностью нашей архитектуры.
INSTR_OP_BEQ:
begin
// beq $s0, $s1, 4
// jump to 4 ((!)абсолютный адрес для упрощения) if $s0 == $s1
// rs=$s0, rt=$s1, imm=4
// rf_rd1 немедленно получит значение регистра rs
rf_ra1 = instr_itype_rs;
// rf_rd2 немедленно получит значение регистра rt
rf_ra2 = instr_itype_rt;
if(rf_rd1 == rf_rd2)
pc_next = instr_itype_imm;
end
Команда j
Безусловный переход. Все очевидно.
Замечание про абсолютные и относительные адреса перехода для команды beq справедливо и здесь для команды j.
INSTR_OP_J:
begin
// j 4
// jump to 4 ((!)абсолютный адрес для упрощения)
// addr = 4
pc_next = instr_jtype_addr;
end
Ну вот и достаточно, закрываем case, always и сам модуль.
endcase
end
endmodule
Замечание: Конечно мы реализовали всего несколько команд из всего множества команд, входящих в спецификацию
ассемблера MIPS (в документации к чипу PIC32 я насчитал 85), но реализацию других команд при желании очевидно легко добавить в дизайн модуля, просто добавив необходимые блоки кода внутри case.
5. Ядро MIPS и модуль верхнего уровня
На этом дизайн внутренней логики работы процессора завершается - все составные части готовы. Осталось соединить их вместе в рамках единого ядра и подготовить модуль верхнего уровня для запуска прошивки с созданным ядром на ПЛИС или для отправки на фабрику на новую линию 90 нанометров в Зеленоград или Китай для производства чипа или же для продажи сторонним лицензиатам в виде интеллектуальной собственности. В рамках курса лабораторных работ производство партии чипов с приведенным дизайном вероятно будет излишним, тк. процесс изготовления как минимум займет некоторое время, которое скорее всего не впишется в расписание учебного плана, зато тестовый запуск на ПЛИС прошивки с рабочим процессором можно вполне провести прямо в аудитории. Также стоит отметить, что для варианта продажи дизайна в виде интеллектуальной собственности имеет смысл сначала решить возможные вопросы юридического характера с лицензированием архитектуры MIPS у ее владельца Imagination Technologies, в которую недавно вошла MIPS Technologes, а еще лучше на основе полученного опыта разработать собственную архитектуру вычислительной машины для решения каких-нибудь актуальных специализированных задач, с которыми традиционные процессоры общего назначения справляются недостаточно эффективно.
Ядро MIPS
Модуль Ядро MIPS (или просто MIPS) - представляет собой одно рабочее ядро процессора с архитектурой MIPS, которое далее можно в одном или нескольких экземплярах разместить на чипе.
На входе принимает все те же интерфейсы для подключения такик внешних элементов дизайна как тактовый сигнал, память инструкций и память данных.
Конкретно данный вариант дизайна внутри содержит единственный экземпляр модуля Шина данных и Контроллер (datapath and controller), подключенный к тем же самым входам и выходам, и по сути просто представляет собой внешний интерфейс этого модуля с другим именем, однако в случае разбиения модуля с Шиной данных и Контроллером на составные части, внешний модуль Ядро MIPS мог бы иметь чуть больше соединительной логики.
mips.v /**
* Ядро процессора MIPS.
*
* @param clk - тактовый сигнал clock
*
* @param pc - счетчик программы (program counter)
* @param instr - значение текущей инструкции
*
* @param dmem_we - флаг разрешения записи (we - write enabled) в память данных
* (dmem - data memory)
* @param dmem_addr - адрес (addr) доступа к памяти данных для чтения/записи
* @param dmem_wd - данные для записи в память данных (wd - write data)
* @param dmem_rd - данные чтения из памяти данных (rd - read data)
*/
module mips(input clk,
/* Работа с памятью инструкций */
output [31:0] pc, input [31:0] instr,
/* Работа с памятью данных */
output dmem_we, output [31:0] dmem_addr,
output [31:0] dmem_wd,
input [31:0] dmem_rd);
datapath_and_controller dpctrl(clk,
pc, instr,
dmem_we, dmem_addr, dmem_wd, dmem_rd);
endmodule
Модуль верхнего уровня
Модуль верхнего уровня (top module) - представляет собой конкретный вариант аппаратной системы, которая будет работать на чипе ПЛИС или при помощи какого-то другого способа запуска (специализированный чип или симуляция).
Добавляем модули память инструкций, память данных, одно или несколько ядер MIPS, соединяем их между собой через определенные заранее интерфейсы и подключаем генератор тактового сигнала clock.
mips_top.v/**
* Модуль верхнего уровня для проверки процессора MIPS.
*/
module mips_top(input clk);
// Память инструкций
wire [31:0] pc;
wire [31:0] instr;
instrmem instrmem(pc, instr);
// Память данных
wire dmem_we;
wire [31:0] dmem_addr;
wire [31:0] dmem_wd;
wire [31:0] dmem_rd;
datamem dmem(clk, dmem_we, dmem_addr, dmem_wd, dmem_rd);
// Ядро процессора MIPS
mips mips(clk,
pc, instr,
dmem_we, dmem_addr, dmem_wd, dmem_rd);
endmodule
Главный вход модуля - аппаратный генератор тактового сигнала clock, от которого зависит быстродействие процессора. Например на плате ПЛИС Digilent Basys2 частота генератора тактового сигнала равна 25МГц, т.е. на ней наш однотактовый процессор будет выполнять 25 миллионов инструкций MIPS в секунду.
module mips_top(input clk);
Подключаем память инструкций.
// Память инструкций
wire [31:0] pc;
wire [31:0] instr;
instrmem instrmem(pc, instr);
Подключаем память данных.
// Память данных
wire dmem_we;
wire [31:0] dmem_addr;
wire [31:0] dmem_wd;
wire [31:0] dmem_rd;
datamem dmem(clk, dmem_we, dmem_addr, dmem_wd, dmem_rd);
Добавляем один экземпляр ядра MIPS и подключаем к тактовому сигналу clock, памяти инструкций и памяти данных, определенные выше.
// Ядро MIPS
mips mips(clk,
pc, instr,
dmem_we, dmem_addr, dmem_wd, dmem_rd);
Замечание: В этом месте мы могли бы легко добавить еще несколько ядер MIPS, просто копируя и вставляя эту строку, и таким образом получили бы настоящую многоядерную систему с произвольным числом ядер, работающих по-настоящему параллельно. Их количество было бы ограничено только аппаратными характеристиками например чипа ПЛИС, на котором мы соберемся этот дизайн запускать. Ну и конечно перед этим стоило бы предварительно продумать схему их взаимодействия в частности при работе с разделяемыми ресурсами типа общей памяти, но это уже тема для отдельного курса или исследования.
И завершение модуля:
endmodule
Ну вот собственно и всё. Процессор и остальные блоки готовы и подключены между собой в рамках единого дизайна - можно генерировать прошивку для ПЛИС, загружать процессор в память ПЛИС и запускать на нем разные интересные программы. Последний нюанс, который осталось решить - в текущем виде процессор будет обязательно работать и выполнять уже расположенные внутри памяти инструкций команды. Однако, что там в действительности будет происходить с процессором внутри ПЛИС, мы пока никак не увидим и не узнаем потому, что у нас до сих пор не был определен способ доступа к результатам работы, которым мог бы воспользоваться обычный человек через свои традиционные органы чувств.
Поэтому в завершении -
подключение устройств ввода-вывода (конечно же это будут 7мисегментный диодный дисплей и совершенно новый потрясающий метод ввода данных - рычажковый) и
демонстрация работы тестовых ассемблерных программ, запущенных на созданном выше процессоре MIPS на плате ПЛИС >>.