1i7 (1i7) wrote,
1i7
1i7

Category:

Лабораторная работа 5: делаем процессор MIPS (3)

Продолжение, начало "Лабораторная работа 5: делаем процессор MIPS (1)" и "Лабораторная работа 5: делаем процессор MIPS (2)" <<

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

4. Шина данных и контроллер

Модуль шина данных и контроллер (datapath and controller) - осуществляет разбор потока команд ассемблерной программы, который приходит к нему из памяти инструкций, и осуществляет выполнение каждой команды на один такт синхросигнала процессора.

Замечание 1: В классических реализациях однотактового процессора (в частности в той, которая приведена в качестве примера в книге "Harris and Harris") части "controller" и "datapath" логически разнесены по разным модулям и включают некоторые дополнительные модули типа ALU (Arithmetical logic unit - Арифметико-логическое устройство). Controller отвечает за разбор команд и арифметических операций (блок ALU decoder). Datapath определяет логику путешествия данных внутри процессора (между файлом регистров и памятью данных, установка счетчика программы и т.п.) в зависимости от текущей операции. Подобное разбиение целесообразно производить в силу ряда объективных причин - модульность и гибкость дизайна, оптимизация использования доступных аппаратных ресурсов (уменьшение количества используемых строительных блоков и сокращение количества микроопераций, выполняемых между двумя тактами) и т.п. Однако введение таких дополнительных сущностей также в некоторой степени усложнит дизайн (увеличится количество витков в "клубке проводов", по которым гуляют биты между каждыми двумя тактами процессора) и описывающее его повествование (каждый дополнительный модуль - лишний раздел с дополнительными разъяснениями зачем он нужен и каким образом вписывается в общий дизайн). По этой причине некоторые дополнительные сущности и разбиения из дизайна удалены, а вся необходимая функциональность помещена в один большой модуль. Его структура и логика будут более понятны в рамках текущего цикла лабораторных работ, а результат его работы в достаточном приближении будет точно таким же, как результат работы более правильных дизайнов.

Замечание 2: Выбранный способ реализации данного модуля судя по всему также должен быть более понятен программистам, привыкшим к процедурным парадигмам, т.к. в нем прослеживается некоторая последовательность действий, хотя в некоторых случаях она и мнимая. В то время как в примере из "Harris and Harris" дизайн на каждом шаге как бы "сворачивается в клубок" из последовательности вложенных модулей и архитектору системы требуется видеть, по какой из петлей пробежит входящий сигнал в зависимости от тех или иных условий, сразу на всех уровнях.

Итак, судя по названию, в данный модуль входят два основных логических блока:
Контроллер (controller) - разбор двоичных инструкций (машинных кодов) процессора, которые поступают в модуль из памяти инструкций.
Шина данных (datapath) - логика путешествия данных между блоками процессора - между файлом регистром и памятью данных, установка счетчика программы и т.п. в зависимости от значения текущей инструкции.

Код модуля, приведенный ниже полностью, на каждый такт процессора делает примерно следующее:
1. Разбор текущего значения 32хбитной инструкции на составляющие поля (см команды ассемблера).
2. В зависимости от типа инструкции и значений ее внутренних полей производится вычисление значений, которые должны быть отправлены в файл регистров, память данных или счетчик программы по факту выполнения текущей команды.
3. Собственно в момент такта процессора clock происходит запись значений, подготовленных на предыдущем этапе, уже непосредственно в файл регистров, память данных и счетчик инструкций.
4. И так вечно и бесконечно.

И последнее напутствующее замечание: для вычисления результатов работы каждой команды и записи вычисленных значений по назначению на каждый такт синхросигнала clock используются всё те же два блока always - always @(*) и always @(posedge clk), что и в Лабораторной работе 4 по знакомству с простыми конечными автоматами. Первый блок always @(*) вычисляет значения результата работы инструкций и находится прямо в текущем модуле шина данных и контроллер (datapath and controller), а второй always @(posedge clk) в трех экземплярах размазан по модулям счетчик программы (pc - program counter), файл регистров (register file) и память данных (data memory). Про подробности работы разных блоков always и их отличия можно почитать в описании 4й лабораторной работы, в описании ниже на этих нюансах останавливаться уже не будем.

Теперь собственно технические подробности и код Verilog (модуль полностью можно находится в файле mips.v, ниже сразу идет разбор построчно).




Параметры

clk - тактовый сигнал clock

Всё необходимое для работы с памятью инструкций.
pc - счетчик программы (program counter)
instr - значение текущей инструкции

Всё необходимое для работы с памятью данных.
dmem_we - флаг разрешения записи (we - write enabled) в память данных (dmem - data memory)
dmem_addr - адрес (addr) доступа к памяти данных для чтения/записи
dmem_wd - данные для записи в память данных (wd - write data)
dmem_rd - данные чтения из памяти данных (rd - read data)


/**
 * Шина данных и контроллер (datapath and contoller) - переключает счетчик программы
 * (program counter), содержит файла регистров (register file) и обрабатывает инструкции.
 *
 * @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 datapath_and_controller(input clk,
    /* Счетчик программы и текущая инструкция */
    output [31:0] pc, input [31:0] instr,

    /* Работа с памятью данных */
    output reg dmem_we,
    output reg [31:0] dmem_addr,
    output reg [31:0] dmem_wd,
    input [31:0] dmem_rd);

Счетчик инструкций и память инструкций

Подключаем первый уже знакомый модуль Счетчик инструкций (program counter) - параметры clk (clock) и pc (program counter) идут извне, для манипуляции адресом следующей инструкции pc_next заводим локальный регистр - все, что в него попадет ниже между тактами процессора в процессе исполнения команды, будет записано в выход pc на каждый тактовый сигнал clk.

Также забегая немного вперед (к моменту подключения модулей еще на один уровень выше, хотя здесь этому пояснению самое место), стоит сообщить, что снаружи модуля Шина данных и контроллер (datapath and controller) параметры выход pc и вход instr соответственно подключены к входу addr и выходу instr модуля Память инструкций (instruction memory), поэтому как только адрес инструкции pc (program counter) внутри модуля Счетчик инструкций (program counter) получает новое значение (а это происходит на каждый такт сигнала clk), значение входа инстукции instr обновляется на значение инструкции, которая расположена по соответствуещему адресу в памяти инструкций.



Таким образом, исходя из всего сказанного выше, следует простой результат - если записать новое значение с адресом инструкции в регистр pc_next в процессе выполнения текущей команды, то на следующий такт сигнала clk на входе instr появится 32хбитное значение инструкции, расположенной в памяти инструкций по этому адресу.
    reg [31:0] pc_next;
    // Program counter
    pc pcount(clk, pc_next, pc);

Файл регистров

Подключаем второй уже известный нам модуль - Файл регистров (register file). Все управляющие входы/выходы продублированы в виде локальных переменных - проводов и регистров. Пояснения к механизмам чтения-записи в целом аналогичны описанным выше счетчику инструкций и памяти инструкций, только управляющие входы/выходы расположены здесь более компактно.

Чтение происходит можно сказать мнговенно - как только регистр rf_ra1 (register file read address 1) получает новое значение в виде 5тибитного адреса внутри файла регистров, провод rf_rd1 (register file read data 1) получает 32хбитное значение, которое хранится в файле ригстров по указанному адресу rf_ra1. Аналогично запись в регистр fr_ra2 (register file read address 2) автоматически инициирует мгновенное получение значения на 32хбитном проводе rf_rd2 (register file read data 2).

Запись синхронизирована с тактовым сигналом процессора clk (clock). Устанавливаем rf_we (register file write enable) в 1 (те запись разрешена), в rf_wa (register file write address) 5тибитный адрес регистра, в который будет произведена запись, в rf_wd (register file write data) - 32хбитное значение для записи: на следующий такт сигнала clk значение rf_wd отправится в регистр по адресу rf_wa. Если флаг rf_we установлен в 0, то запись не производится.
    // Register file
    reg [4:0] rf_ra1, rf_ra2;
    wire [31:0] rf_rd1, rf_rd2;

    reg rf_we;
    reg [4:0] rf_wa;
    reg [31:0] rf_wd;

    regfile rf(clk,
        rf_ra1, rf_ra2, rf_rd1, rf_rd2,
        rf_we, rf_wa, rf_wd);

Разбор команд (контроллер)

Разбор инструкций на составляющие поля происходит элементарно - просто на каждое поле создается переменная wire с нужной разрядностью и присасывается при помощи оператора assign к соответствующим диапазонам битов внутри 32хбитной инструкции instr.

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

Первое поле общее для всех типов инструкций - код операции op (opcode) - 6 старших бит.
    // Instructions
    wire [5:0] instr_op;
    assign instr_op = instr[31:26]; // 6 bits

Для инструкций типа R-type поля: rs, rt, rd, shamt (не используется), funct.
    // R-type
    wire [4:0] instr_rtype_rs;
    wire [4:0] instr_rtype_rt;
    wire [4:0] instr_rtype_rd;
    //wire [4:0] instr_rtype_shamt;
    wire [5:0] instr_rtype_funct;

    assign instr_rtype_rs = instr[25:21]; // 5 bits
    assign instr_rtype_rt = instr[20:16]; // 5 bits
    assign instr_rtype_rd = instr[15:11]; // 5 bits
    //assign instr_rtype_shamt = instr[10:6]; // 5 bits - not used here
    assign instr_rtype_funct = instr[5:0]; // 6 bits

Для инструкций типа I-type поля: rs, rt, imm.
    // I-type
    wire [4:0] instr_itype_rs;
    wire [4:0] instr_itype_rt;
    wire [15:0] instr_itype_imm;

    assign instr_itype_rs = instr[25:21]; // 5 bits
    assign instr_itype_rt = instr[20:16]; // 5 bits
    assign instr_itype_imm = instr[15:0]; // 16 bits

Для инструкций типа J-type поля: addr, instr.
    // J-type
    wire [25:0] instr_jtype_addr;
    assign instr_jtype_addr = instr[25:0]; // 26 bits

Замечание: из кода выше очевидно, что все переменные-поля получают свои значение одновременно без каких-либо проверок на то, с каким типом инструкции мы на данный момент на самом имеем дело. Т.е. например если у нас текущая инструкция add, которая относится к типу R-type, то все переменные-поля, относящиеся к R-type, получат необходимые осмысленные значения. Одновременно с этим переменные-поля, относящиеся к другим типам инструкций (например поле addr для инструкций типа J-type), также получат некоторые значения, которые однако не будут иметь смысла. Это так, но в этом нет никакой проблемы, т.к. дальнейшая логика выполнения инструкций будет разветвлена при помощи ключевого поля op (код операции) таким образом, что некорректные ситуации просто никогда не будут приняты к исполнению.

Выполнение команд (шина данных)

Для начала заведем себе для удобства несколько констрант с значениями полей op (код операции - общий для всех) и funct (для команд типа R-type) для всех команд, которые будет уметь исполнять наш процессор: lw, sw, addi, beq, j, add, sub.

    parameter INSTR_OP_LW = 6'b100011;
    parameter INSTR_OP_SW = 6'b101011;
    parameter INSTR_OP_ADDI = 6'b001000;
    parameter INSTR_OP_BEQ = 6'b000100;
    parameter INSTR_OP_J = 6'b000010;
    parameter INSTR_OP_RTYPE = 6'b000000;

    parameter INSTR_RTYPE_FUNCT_ADD = 6'b100000;
    parameter INSTR_RTYPE_FUNCT_SUB = 6'b100010;

Итак, входим в первый always: в стартовой секции устанавливаем значения по умолчанию для всех команд - переводим счетчик инструкций на следующую команду (+4 байта к текущему значению - у нас процессор 32хбитный), запрещаем запись в файл регистров и память данных, на всякий случай сбрасываем остальные значения в 0 и открываем ветвление case по полю instr_op (код операции).
    always @(*)
    begin
        // по умолчанию переводим счетчик на следующую инструкцию
        pc_next = pc + 4;

        // запретить запись в файл регистров и память данных
        rf_we = 0;
        dmem_we = 0;

        // сбросить остальные значения в 0 по умолчанию
        rf_ra1 = 0;
        rf_ra2 = 0;
        rf_wa = 0;
        rf_wd = 0;

        dmem_addr = 0;
        dmem_wd = 0;

        case(instr_op)


Подошли совсем вплотную к кульминации нашего дизайна - реализация ассемблерных программ процессора, в следующем посте.
Tags: mips, verilog, процессоры, цифровая электроника для программистов
Subscribe

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 1 comment