1i7

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

Jan 13, 2013 01:07

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

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

3. Основные модули дизайна HDL

Вспоминаем основные элементы, которые определяют текущее состояние системы - счетчик программы (program counter), файл регистров (register file), память инструкций (instruction memory) и память данных (data memory) - они и станут основными блоками дизайна - определим их в виде соответствующих модулей.

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

В нашем случае это будет как раз простой однотактовый процессор, который на каждый такт периодического сигнала Clock выбирает ровно одну команду из памяти инструкций и на этот же такт исполняет ее.

Чтобы оценить объем дизайна, можно сразу заглянуть в исходные файлы проекта:
mips.v - модули основной логики.
datamem.v - память данных.
instrmem.v - память инструкций (пусть объем файла не пугает - 99% строк в нем занимает двоичное представление тестовой программы).
mips_top.v - модуль верхнего уровня - для генерации файла прошивки ПЛИС и подключения устройств ввода-вывода.

Далее подробнее в описании каждого модуля.

Счетчик программы

Модуль счетчик программы (program counter) позволяет установить следующее значение счетчика программы на каждый такт синхросигнала.




На входе - 1бит на тактовый сигнал clk (Clock) и 32хбитное значение для следующего значения счетчика программы pc' (program counter next - pc_next). На выходе - текущее значение счетчика программы pc (program counter). Значение на выходе pc меняется на значение входа pc' на каждый такт сигнала clk.

Код на Verilog очевидно реализует именно эту простую логику.

mips.v

/**
 * Счетчик программы - переход на следующее значение на каждый такт.
 *
 * @param clk - тактовый сигнал clock
 *
 * @param pc_next - следующее значение для счетчика программы (program counter)
 * @param pc - счетчик программы (program counter)
 */
module pc(input clk,
    input [31:0] pc_next, output reg [31:0] pc);

always @(posedge clk)
        pc <= pc_next;
endmodule

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

Модуль файл регистров (register file) хранит значения внутренних регистров процессора, позволяет получать 32хбитное значение регистра по 5тибитному адресу и делать запись 32хбитного значения в регистр по 5тибитному адресу.




Модуль устроен так, что читать можно два значения одновременно. Запись делается в один регистр на один такт сиглана Clock.

Весь код модуля.

mips.v

module regfile(input clk,
    /* Чтение 2х регистров */
    input [4:0] ra1, input [4:0] ra2,
    output [31:0] rd1, output [31:0] rd2,

/* Запись в регистр */
    input we, input [4:0] wa, input [31:0] wd);

reg [31:0] rf [31:0];

always @(posedge clk)
        if(we) rf[wa] <= wd;

assign rd1 = ra1 ? rf[ra1] : 0; // reg[0] is zero
    assign rd2 = ra2 ? rf[ra2] : 0; // reg[0] is zero
endmodule
Параметры

Для операций чтения

Входы:
ra1 - адрес чтения (read address) регистра-источника 1 - 5 бит
ra2 - адрес чтения (read address) регистра-источника 2 - 5 бит

Выходы:
rd1 - считываемые данные (read data) регистра-источника 1 - 32 бит
rd2 - считываемые данные (read data) регистра-источника 2 - 32 бит

Для операций записи - все входы:
clk - тактовый сигнал clock - запись в регистр-назначение осуществляется на каждый тактовый сигнал при включенном флаге we
we - флаг разрешения записи (write enabled)
wa - адрес записи (write address) регистра-назначения - 5 бит
wd - записываемые данные для (write data) регистра-назначения - 32 бит

module regfile(input clk,
    /* Чтение 2х регистров */
    input [4:0] ra1, input [4:0] ra2,
    output [31:0] rd1, output [31:0] rd2,

/* Запись в регистр */
    input we, input [4:0] wa, input [31:0] wd);
Массив бит 32x32 для хранения данных регистров.
    reg [31:0] rf [31:0];
Производим операцию записи данных wd (write data) в регистр по адресу wa (write address) на каждый такт сигнала clk (clock) если флаг we (write enabled) равен 1.

always @(posedge clk)
        if(we) rf[wa] <= wd;
Производим операцию чтения из двух регистров - по адресам ra1 и ra2. Значения считываемых из регистров данных появляются на выходах модуля rd1 и rd2 в момент присвоения значений адресов входам ra1 и ra2 без участия тактового сигнала clock. Для регистра по адресу 0 всегда возвращается значение 0 (см список регистров с ролями - регистр $0 - константа ноль).

assign rd1 = ra1 ? rf[ra1] : 0; // reg[0] всегда ноль
    assign rd2 = ra2 ? rf[ra2] : 0; // reg[0] всегда ноль

Память данных

Модуль память данных (data memory) - RAM (random access memory) - запоминающее устройство с произвольным доступом на чтение и запись. Адресуем память 32хбитным указателем 2^32 байт=4096 Мегабайт. Память организована словами (блоками) по 4 байта, загрузка и сохранение осуществляется также словами по 4 байта. С 32хбитных адресом получаем 2^32 / 4 = 1'073'741'824 4хбайтовых слов виртуальной памяти в доступном адресном пространстве.




Код в большой степени аналогичен коду модуля файла регистров только с парой нюансов.

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

Замечание 2: Еще далее модуль будет дополнен расширениями для адресации устройств ввода-вывода.

datamem.v

/**
 * Память данных.
 *
 * @param clk - clock
 *
 * @param we - флаг разрешения записи (write enabled)
 * @param addr - адрес доступа на чтение/запись (address)
 * @param wd - записываемые данные (write data)
 * @param rd - считанные данные (read data)
 */
module datamem_plain (input clk,
    input we, input [31:0] addr,
    input [31:0] wd,
    output [31:0] rd);

// массив памяти данных
    reg [31:0] RAM [63:0];

// запись данных в RAM если флаг we (write enabled) равен '1'
    always @(posedge clk)
        if(we) RAM[addr[31:2]] <= wd;

// чтение данных из RAM
    // выравнивание по словам (word aligned) - поделить адрес addr на 4
    // (просто отбросить 2 последних бита)
    assign rd = RAM[addr[31:2]];
endmodule
Массив слов 32xN - доступная физическая память. N - количество слов, выделенных под память; максимальное количество, которое можно адресовать в рамках 32хбитной архитектуры - 2^32 / 4 = 1'073'741'824, но т.к. на такой объем памяти нам никаких вентилей на ПЛИС не хватит, берем столько, сколько потребуется программе - в данном случае N=63 взято "с потолка" с очень большим запасом.

// массив памяти данных
    reg [31:0] RAM [63:0];
Чтение и запись данных в RAM. Запись на каждый такт clock, если флаг we (write enabled) равен 1. Чтение по факту присвоения нового значения входу с адресом addr. Важный нюанс при работе с адресом - выравнивание памяти по словам (word aligned): для обращения к 4хбайтовому слову по адресу байта адрес addr требуется поделить на 4 (просто отбросить 2 последних бита).

// запись данных в RAM если флаг we (write enabled) равен '1'
    always @(posedge clk)
        if(we) RAM[addr[31:2]] <= wd;

// чтение данных из RAM
    assign rd = RAM[addr[31:2]];

Память инструкций

Модуль память инструкций (instruction memory) - ROM (read-only memory) - память, доступная только для чтения. По логике адресации полная аналогия с памятью данных - 32хбитный адрес, выравнивание по 4 слова. Главное отличие от модуля памяти данных - отсутствие интерфейса записи.




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

Код программы будет представлен в виде двоичных констант с заранее определенными адресами: одна инструкция - одно 4хбитное слово. Для этого предварительно код программы на ассемблере необходимо вручную перевести в двоичный вид по таблицам, которые были приведены в первой части лабораторной работы.

Например для такой ассемблерной программы из 2х строк

addi $s0, $0, b0000000011111111
sw $s0, 0xf000 ($0)
модуль памяти инструкций на языке Verilog будет выглядеть следующим образом:
instrmem.v
module instrmem_test_7segment_draw_8 (
    /* адрес чтения инструкции */
    input [31:0] addr,

/* значение инструкции */
    output reg [31:0] instr);

// hardcode program data - as soon as instruction memory is read-only,
    // implement it in ROM-way
    always @ (addr)
        case (addr)
            32'h00000000: instr <= 32'b001000_00000_10000_0000000011111111; // addi $s0, $0, b0000000011111111
            32'h00000004: instr <= 32'b101011_00000_10000_1111000000000000; // sw $s0, 0xf000 ($0)
            default: instr <= 0;
        endcase
endmodule
На входе:
addr - 32хбитный адрес инструкции - должен подключиться к текущему значению счетчика программы (program counter).

На выходе:
instr - 32хбитное значение инструкции - двоичное представление ассемблерной команды, на которую указывает адрес addr (или счетчик программы).

Замечание: Подобный подход с ручным конвертированием программы на ассемблере в двоичный код и встраивание этого кода напрямую в кода модуля Verilog конечно же применим только для первоначального тестирования и быстрой демонстрации работоспособности процессора в рамках лабораторной работы. Для практического применения потребуется подключение специальной внешней памяти, на которой будет располагаться двоичный код программы, созданный компилятором и записанный предназначенным для этого способом.

Продолжение следует. Осталось совсем немного - разобрать ключевой модуль с дизайном логики процессора datapath_and_controller, подключить простые устройства ввода-вывода и запустить на ПЛИС тестовые программы.

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

плис, цифровая электроника для программистов, verilog, процессоры, mips

Previous post Next post
Up