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

Category:

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

И наконец, финальный пост в серии Лабораторная работа 5: делаем процессор MIPS на Верилоге.

На прошлых лабах мы уже полностью сделали процессор с выбранным подмножеством архитектуры MIPS на Verilog - cсылки на предыдущие части лабы:

1. Определение архитектуры процессора, язык ассемблер: Лабораторная работа 5: делаем процессор MIPS (1)

Реализация на Verilog:
2. Ключевые модули - файл регистров, память данных, память инструкций, счетчик программы: Лабораторная работа 5: делаем процессор MIPS (2),
3. Шина данных и контроллер: Лабораторная работа 5: делаем процессор MIPS (3) и
4. Реализация команд ассемблера, ядро MIPS и модуль верхнего уровня: Лабораторная работа 5: делаем процессор MIPS (4)
5. Подключение простых устройств ввода-вывода: Лабораторная работа 5: делаем процессор MIPS (5)


Весь Verilog код проекта:
mips.v - модули основной логики.
datamem.v - память данных.
instrmem.v - память инструкций.
mips_top.v - модуль верхнего уровня - для генерации файла прошивки ПЛИС и подключения устройств ввода-вывода.

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

7. Тестовые программы

Итак, процессор под себя мы написали, пришло время загрузить его в память.

Внутри модуля instrmem перечислены все тестовые программы, каждая из которых находится в отдельном модуле - перед запуском на ПЛИС нужно выбрать один, раскомментировать соответствующую строку и перегенерировать прошивку. Ничего концептуального - чисто для удобства организации кода.

instrmem.v
/**
 * Instruction memory - contains program instructions.
 */

module instrmem (
    /* read address */
    input [31:0] addr,
    /* instruction value */
    output [31:0] instr);

    // раскомментировать нужный модуль для теста

    //instrmem_test_7segment_draw_8 instrmem_program(addr, instr);
    //instrmem_test_7segment_draw_5 instrmem_program(addr, instr);
    //instrmem_test_beq_input instrmem_program(addr, instr);
    //instrmem_test_sw_lw instrmem_program(addr, instr);
    //instrmem_test_input_4bits instrmem_program(addr, instr);
    instrmem_test_io_calc instrmem_program(addr, instr);
endmodule

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

Проверка "видео-памяти" - выводим цифру 8

Проверим, что все сегменты дисплея подключены - нарисуем цифру 8 с точкой. Отправляем b'11111111 в регистр $s0 при помощи операции addi (прибавляем константу в выставленными битами к нулю $0). Из $s0 отправляем в видео-память - sw в память данных по адресу 0x0000f000.

  // put 11111111 value (8 with dot 'on') to s0 (switch on all display segments)
    addi $s0, $0, b0000000011111111
    // send value to video memory (7-segment display) device at 0xf000
    sw $s0, 0x0000f000 ($0)

То же самое на верилоге:
instrmem.v
module instrmem_test_7segment_draw_8 (
    /* read address */
    input [31:0] addr,
    /* instruction value */
    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

Видим результат - простая цифра 8 с точкой на дисплее, но мы-то знаем, что это цифра 8 нарисована не прямым проводом от батарейки и даже не простым комбинаторным модулем, как на первых лабах, а при помощи настоящего аппаратного процессора.





Проверка "видео-памяти" - выводим цифру 5

Аналогичным образом рисуем цифру 5 без точки, чтобы убедитьсяв том, что у нас не просто горяд все сегменты из-за всех единиц на всех портах вывода, а мы этим процессом действительно управляем. Отправляем последовательность b'11100110 в видео-память аналогичным образом.

  // put 11111111 (5 with dot off) value to s0 (switch on all display segments)
    addi $s0, $0, b0000000011100110
    // send value to video memory (7-segment display) device at 0xf000
    sw $s0, 0x0000f000 ($0)

Всё то же на верилоге:
instrmem.v
module instrmem_test_7segment_draw_5 (
    /* read address */
    input [31:0] addr,
    /* instruction value */
    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_0000000011100110; // addi $s0, $0, b0000000011100110
            32'h00000004: instr <= 32'b101011_00000_10000_1111000000000000; // sw $s0, 0xf000 ($0)

            default: instr <= 0;
    endcase
endmodule

Цифра 5, нарисована процессором:


Проверка ввода (1 бит - порт bsf) и условного перехода beq

Добавим немного динамики - проверим команду условного перехода beq и заодно ввода 1го бита информации рычажковым способом с порта bsf.

Смысл программы - в бесконечном цикле загружаем текущее значение рычага bsf (всего возможно два варианта - 0 или 1) из памяти данных по адресу 0x0000f0008 в регистр $s0. Далее при помощи команды beq сравниваем его с нулем (регистр $0 - ноль, который всегда под рукой). Если $s0 равен нулю (рычаг выключен), переходим к рисованию на дисплее '0', иначе рисуем на дисплее '1'. Т.к. цикл бесконечный (в конце программы прыгаем на начало при помощи команды j), можно переключать рычажок bsf и значение на дисплее будет меняться 1/0/1/0/...

loop:
    // load value from 1-bit input (button switch) device at 0xf008
    lw $s0, 0xf008 ($0)
    // go to 'display_0' if input bit is 0
    beq $s0, $0, display_0 // 0x0014
  display_1:
    // display "1" on 7-segment display (if input is 1)
    addi $s1, $0, b00011100
    sw $s1, 0xf000 ($0)
    // jump to beginning
    j loop // 0x0000
  display_0:
    // display "0" on 7-segment display (if input is 0)
    addi $s1, $0, b01111111
    sw $s1, 0xf000 ($0)
    // jump to beginning
    j loop // 0x0000

Всё то же на верилоге:
instrmem.v
module instrmem_test_beq_input (
    /* read address */
    input [31:0] addr,
    /* instruction value */
    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)
            // loop:
            32'h00000000: instr <= 32'b100011_00000_10000_1111000000001000; // lw $s0, 0xf008 ($0)
            32'h00000004: instr <= 32'b000100_10000_00000_0000000000010100; // beq $s0, $0, display_0 // 0x0014
            // display_1:
            32'h00000008: instr <= 32'b001000_00000_10001_0000000000011100; // addi $s1, $0, b00011100
            32'h0000000c: instr <= 32'b101011_00000_10001_1111000000000000; // sw $s1, 0xf000 ($0)
            32'h00000010: instr <= 32'b000010_00000000000000000000000000; // j loop // 0x0000
            // display_0:
            32'h00000014: instr <= 32'b001000_00000_10001_0000000001111111; // addi $s1, $0, b01111111
            32'h00000018: instr <= 32'b101011_00000_10001_1111000000000000; // sw $s1, 0xf000 ($0)
            32'h0000001c: instr <= 32'b000010_00000000000000000000000000; // j loop // 0x0000

            default: instr <= 0;
    endcase
endmodule

Наблюдаем результат в динамике - рычажок bsf переключает значения на дисплее с 0 на 1 и обратно:


Проверка сохранения слова (sw) и загрузки слова (lw)

Проверим, что память данных реально может хранить произвольные значения и мы можем использовать ее например в качестве буфера, чтобы сохранять результаты вычислений, производимых при помощи регистров (внутренних регистров у процессора мало, а память большая). Запишем в регистр $s0 код для отображения на дисплее все той же цифры '5': b'11100110, далее сохраним это значение в память данных по адресу 0x0 (sw - store word). Далее загрузим значение из памяти данных по этому же адресу 0x0 в регистр $s1 (lw - load word), а из $s1 отправим его в видео-память привычным способом - сохранить слово в память данных на адрес 0x0000f000. Если все сработало правильно, те. нужное значение попало из регистра $s0 в регистр $s1 через промежуточную ячейку внутри памяти данных, дисплей отобразит цифру '5'.

  addi $s0, $0, b0000000011100110
    sw $s0, 0 ($0)
    lw $s1, 0 ($0)
    sw $s1, 0xf000 ($0)

Всё то же на верилоге:
instrmem.v
module instrmem_test_sw_lw (
    /* read address */
    input [31:0] addr,
    /* instruction value */
    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_0000000011100110; // addi $s0, $0, b0000000011100110
            32'h00000004: instr <= 32'b101011_00000_10000_0000000000000000; // sw $s0, 0 ($0)
            32'h00000008: instr <= 32'b100011_00000_10001_0000000000000000; // lw $s1, 0 ($0)
            32'h0000000c: instr <= 32'b101011_00000_10001_1111000000000000; // sw $s1, 0xf000 ($0)

            default: instr <= 0;
        endcase
endmodule

Все та же цифра '5', только теперь ее рисует куда более сложная программа:


Проверка рычажкового способа ввода данных

Теперь проверим ввод с 4х рычажков bs. Загружаем текущее 4хбитное значение с устройства bs (прочитать память данных по адресу 0x0000f004) и сразу отправляем их как есть в видео-память (записать слово в память данных по адресу 0x0000f000). Младшие значащие 4 бита рычажков должны переключать 3 нижних сегмента дисплея и точку, т.к. они подключены к младшим 4м битам видео-памяти. Верхние сегменты дисплея должны быть погашены, т.к. все старшие биты слова bs выше 4го разряда равны нулю.

loop:
    lw $s1, 0xf004 ($0)
    sw $s1, 0xf000 ($0)
    j loop

Всё то же на верилоге:
instrmem.v
module instrmem_test_input_4bits (
    /* read address */
    input [31:0] addr,
    /* instruction value */
    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)
            // loop:
            32'h00000000: instr <= 32'b100011_00000_10001_1111000000000100; // lw $s1, 0xf004 ($0)
            32'h00000004: instr <= 32'b101011_00000_10001_1111000000000000; // sw $s1, 0xf000 ($0)
            32'h00000008: instr <= 32'b000010_00000000000000000000000000; // j loop // 0x0000

            default: instr <= 0;
        endcase
endmodule

Результат в динамике - 4 рычажка включают и выключают 3 нижних сегмента 7мисегментного дисплея и точку.


Калькулятор

Ввести два числа, выставленных рычагами на плате, в 2 ячейки памяти, сложить их значения и показать результат на 7мисегментром дисплее. Если результат больше 9ти (те. число не умещается в 1 разряд дисплея), дисплей показывает код ошибки "E." (Error).

- Первое значение устанавливается на 4хбитных рычагах при значении 1битного рычага 1, затем вводится в память при переключении 1битного рычага из 1 в 0.
- После этого второе значение устанавливается на этих же 4хбитных рычагах при значении 1битного рычага 0, затем вводится в память при переключении 1битного рычага из 0 в 1.
- После этого выполняется сложение двух введенных значений.
- Результат сложения отображается на 7мисегментном дисплее.

Самая длинная из всех тестовых программ, хотя значительную её часть занимает драйвер дисплея, который умеет отображать на нем произвольное число от 0я до 9ти или E при выходе за эти границы, - его код однообразен и создан большей частью методом копи-пасты. В деталях реализации ассемблерной программы предлагается разобраться самостоятельно в качестве самостоятельного упражнения.

instrmem.v
module instrmem_test_io_calc (
    /* read address */
    input [31:0] addr,
    /* instruction value */
    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)
// # Ввод 1го значения
// # ввод произойдет при bsf=0 (1битный рычаг ввода по адресу 0xf008)
// loop:
        32'h00000000: instr <= 32'b001000_00000_01000_0000000000000000; // addi $t0, $0, 0
// enter_1st:
// # загрузить в $s0 значение рычага bsf (0 или 1)
        32'h00000004: instr <= 32'b100011_00000_10000_1111000000001000; // lw $s0, 0xf008 ($0)
// # если рычаг =0, переходим к считыванию 4хбитного значения
        32'h00000008: instr <= 32'b000100_10000_01000_0000000000010000; // beq $s0, $t0, enter_1st_finish
// # ждать рычага =0 бесконечно до победного
        32'h0000000c: instr <= 32'b000010_00000000000000000000000100; // j enter_1st
// enter_1st_finish:
// # загрузить текущее значение рычагов bs (4 бита по адресу 0xf004) в память по адресу 0x00000000
        32'h00000010: instr <= 32'b100011_00000_10001_1111000000000100; // lw $s1, 0xf004 ($0)
        32'h00000014: instr <= 32'b101011_00000_10001_0000000000000000; // sw $s1, 0 ($0)
//
// # Ввод 2го значения
// # ввод произойдет при bsf=1 (1битный рычаг ввода по адресу 0xf008)
        32'h00000018: instr <= 32'b001000_00000_01000_0000000000000001; // addi $t0, $0, 1
// enter_2nd:
// # загрузить в $s0 значение рычага bsf (0 или 1)
        32'h0000001c: instr <= 32'b100011_00000_10000_1111000000001000; // lw $s0, 0xf008 ($0)
// # если рычаг =1, переходим к считыванию 4хбитного значения
        32'h00000020: instr <= 32'b000100_10000_01000_0000000000101000; // beq $s0, $t0, enter_2nd_finish
// # ждать рычага =1 бесконечно до победного
        32'h00000024: instr <= 32'b000010_00000000000000000000011100; // j enter_2nd
// enter_2nd_finish:
// # загрузить текущее значение рычагов bs (4 бита по адресу 0xf004) в память по адресу 0x00000004
        32'h00000028: instr <= 32'b100011_00000_10001_1111000000000100; // lw $s1, 0xf004 ($0)
        32'h0000002c: instr <= 32'b101011_00000_10001_0000000000000100; // sw $s1, 4 ($0)
//
// ####################################################################
// # Сложить введенные числа
// # Загрузить введенные значения из памяти в регистры
        32'h00000030: instr <= 32'b100011_00000_10000_0000000000000000; // lw $s0, 0 ($0)
        32'h00000034: instr <= 32'b100011_00000_10001_0000000000000100; // lw $s1, 4 ($0)
//
// # Получить результат сложения: $s2 = $s0+$s1
        32'h00000038: instr <= 32'b000000_10000_10001_10010_00000_100000; // add $s2, $s0, $s1
//
// # Сохранить результат в память по адресу 0x00000008
        32'h0000003c: instr <= 32'b101011_00000_10010_0000000000001000; // sw $s2, 8 ($0)
//
// ####################################################################
// # Отобразить результат на 7мисегментном дисплее
// # (соответственно для простоты значение должно быть <=9, иначе покажет 'E' - Error)
//
// # Загрузить результат из памяти по адресу 0x00000008
        32'h00000040: instr <= 32'b100011_00000_10011_0000000000001000; // lw $s3, 8 ($0)
//32'h00000040: instr <= 32'b100011_00000_10011_0000000000000000; // lw $s3, 8 ($0)
//32'h00000040: instr <= 32'b001000_00000_10011_0000000000000000;
//
// # Определить текущее значение - загрузим в $s4 значение, которое поймет драйвер 7мисегментного дисплея
// # (побитовое включение сегментов, нужно установить правильные значения 8ми младших битов регистра $v).
// # Уникальны 1е три команды для одного значения, остальное на 99% копипаста
//
// # значение 0
        32'h00000044: instr <= 32'b001000_00000_10100_0000000001110111; // addi $s4, $0, 16'b00000000 01110111
// # $t0 = константа "0"
        32'h00000048: instr <= 32'b001000_00000_01000_0000000000000000; // addi $t0, $0, 0
// # результат равен "0" - можно отображать
        32'h0000004c: instr <= 32'b000100_10011_01000_0000000011000000; // beq $s3, $t0, display
//
// # значение 1
        32'h00000050: instr <= 32'b001000_00000_10100_0000000000010100; // addi $s4, $0, 16'b00000000 00010100
// # $t0 = константа "1"
        32'h00000054: instr <= 32'b001000_00000_01000_0000000000000001; // addi $t0, $0, 1
// # результат равен "1" - можно отображать
        32'h00000058: instr <= 32'b000100_10011_01000_0000000011000000; // beq $s3, $t0, display

... // (опустим код вывода остальных цифр - он полностью аналогичен - полную версию см в исходном файле)
// # значение 9
        32'h000000b0: instr <= 32'b001000_00000_10100_0000000011110110; // addi $s4, $0, 16'b00000000 11110110
// # $t0 = константа "9"
        32'h000000b4: instr <= 32'b001000_00000_01000_0000000000001001; // addi $t0, $0, 9
// # результат равен "9" - можно отображать
        32'h000000b8: instr <= 32'b000100_10011_01000_0000000011000000; // beq $s3, $t0, display
//
// # не подошел ни один вариант - значение E (Error)
        32'h000000bc: instr <= 32'b001000_00000_10100_0000000011101011; // addi $s4, $0, 16'b00000000 11101011
//
// # Отобразить результат - отправить значение из регистра $s4 в "видеопамять" (7мисегментный дисплей)
// # по адресу 0xf000
// display:
        32'h000000c0: instr <= 32'b101011_00000_10100_1111000000000000; // sw $s4, 0xf000 ($0)
//
// # Зациклим в начало
        32'h000000c4: instr <= 32'b000010_00000000000000000000000000; // j loop

        default: instr <= 0;

    endcase
endmodule


Хотим сложить числа 1 и 2 и увидеть на диплее результат. Стартовое положение рычаг bsf=1 (вкл) - ожидание ввода 1го слагаемого. Вводим 1е слагаемое - число 1 в двоичной форме - перемещаем рычажки bs в положение 0001. Переводим рычаг bsf=0 (выкл) - происходит ввод 1го слагаемого в память и одновременно начинается ожидание ввода 2го слагаемого. Вводим 2е слагаемое - число 2 в двоичной форме - рычажки bs=0010. Переводим рычаг bsf=1(вкл) - происходит ввод 2го числа, вычисляется сумма 1+2=3 и результат '3' отображается на дисплее. Т.к. мы опять оказались в исходном положении (bsf=1), можно повторить ввод 1го слагаемого с другим значением и далее по кругу.

Результат работы в динамике:



8. Лаба окончена

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

Но по цифровому дизайну на верилоге лабы в рамках курса окончены. Зато после них начинается самое интересное - практическое применение полученных навыков. Я бы рекомендовал двинуться в следующих направлениях:
- Научиться подключить ПЛИС к обычному контроллеру в виде внешнего устройства, на которое можно перекладывать вспомогательные задачи, для которых может быть целесообразно аппаратное ускорение.
- Подключить к ПЛИС модуль внешней памяти и сделать так, чтобы процессор научился с ним работать через модуль память данных - без решения этой задачи вообще сложно говорить о каком-то сколько-нибудь полезном применении этого дизайна.
- Далее для особо усидчивых - реализовать все 85 команд архитектуры MIPS и попробовать запустить на своем ядре какой-нибудь линукс.
- Гораздо более интересно - разработать собственный аппаратный вычислитель для каких-нибудь специфических задач и получить на них ускорение в 60 раз по сравнению с процессорами общего назначения.
- В частности можно двигаться в направлении многоядерных систем и параллельных вычислений.
- Например можно попробовать сделать проект уровня adapteva.
- Зайти на сайт проекта opencores.org и попробовать сделать там что-нибудь полезное.

По случаю окончания верилоговской части курса хочу провести небольшой опрос.

Poll #1892395 Цифровая микроэлектроника для математиков и программистов

Помог ли вам курс лабораторных работ по ПЛИС и Verilog пересечь пропасть от физики к программированию?

Помог
2(50.0%)
Не помог
1(25.0%)
Для меня и так не было никакой пропасти
1(25.0%)
Пропасть есть, но я её пересекать не собираюсь
0(0.0%)
Не понимаю, какая еще пропасть
0(0.0%)

Есть ли у вас плата ПЛИС (FPGA)?

Есть и я на ней воспроизвел некоторые лабы из курса
1(25.0%)
Есть, но я и так знаю, что с ней делать, без курса
1(25.0%)
Нет, но как раз собираюсь купить и попробовать запустить лабы из курса
1(25.0%)
Мне хватило фоток и видео в отчетах по лабам
1(25.0%)
Нет и не собираюсь покупать
0(0.0%)
Я не знаю, ни что такое ПЛИС, ни FPGA
0(0.0%)

Интересен ли вам цифровой дизайн аппаратного обеспечения с Verilog и ПЛИС?

Я Java/PHP-программист и мне интересно знать, как все устроено на аппаратном уровне
1(25.0%)
Я Java/Php-программист и хочу заняться цифровым дизайном, чтобы создавать новые интересные аппаратно-программные системы
1(25.0%)
Я Java/Php-программист и мне цифровой дизайн не интересен
0(0.0%)
Я и так занимаюсь цифровым дизайном
2(50.0%)
Просто не интересен
0(0.0%)
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.
  • 5 comments