Предфинальный пост в серии Лабораторная работа 5: делаем процессор MIPS на Верилоге.
Итак, на прошлых лабах мы уже полностью сделали процессор с выбранным подмножеством архитектуры MIPS на Verilog - cсылки на предыдущие части лабы:
1. Определение архитектуры процессора, язык ассемблер:
Лабораторная работа 5: делаем процессор MIPS (1) Реализация на Verilog:
2. Ключевые модули - файл регистров, память данных, память инструкций, счетчик программы:
Лабораторная работа 5: делаем процессор MIPS (2),
3. Шина данных и контроллер:
Лабораторная работа 5: делаем процессор MIPS (3) и
4. Реализация команд ассемблера, ядро MIPS и модуль верхнего уровня:
Лабораторная работа 5: делаем процессор MIPS (4) Осталось провести его демонстрацию и тестовые испытания - для этого подключим базовые устройства ввода-вывода и запустим демонстрационные программы на ПЛИС.
6. Подключение устройств ввода-вывода и демо-запуск на ПЛИС
Для того, чтобы иметь возможность хоть каким-то образом убедиться в том, что процессор на Verilog действительно работает внутри ПЛИС и правильно выполняет загруженные туда программы, а также для того, чтобы иметь возможность интерактивно влиять на ход выподления этих программ, подключим к нашей плату ПЛИС несколько простых устройств ввода-вывода и определим интерфейсы взаимодействия с этими внешними устройствами в дизайн нашего процессора.
Как уже
было заранее решено, в качестве устройства вывода используем уже знакомый любимый семисегментный диодный дисплей. Подключим к плате внешний дисплей к портам ввода-вывода общего назначения PIO. Всего для дисплея задействовано 8 портов (7 сегментов + 1 точка), т.е. 8мибитное двоичное число, направленное на эти порты, будет определять текущее значение, которое отображено на дисплее (каждый бит числа - один сегмент на дисплее) - далее будем называть его v (video memory - типа видео-память).
Для ввода данных будем использовать очевидно совершенно новый потрясающий способ ввода информации - рычажковый. На плате Digilent Basys2, на которой демонстрируются все примеры во всех лабораторных работах, изначально уже доступно 8 встроенных рычажков - будем использовать их. К другим платам, на которых таких рычажков нет, легко подключить внешние рычажки через те же порты ввода-вывода общего назначения.
Из 8ми доступных рычажков сделаем 2 устройства ввода:
- 4 рычага bs (button switch) - для ввода чисел в двоичной форме - каждый рычаг определяет один бит 1/0 в двоичном числе (4 рычага - 4 бита - максимальное число, которое можно ввести таким образом - 2^4=16 - для наших тестов будет вполне достаточно, но при желании можно подключить и дополнительные рычажки) - каждый рычаг является значени.
- один рычаг bsf (button switch flag): однобитный флаг 1/0 или вкл/выкл - его можно использовать, когда например потребуется подтвеждение некоторого действия (типа кнопки Enter на клавиатуре).
Технические подробности подключения рычажков и дисплея к плате ПЛИС можно вспомнить из материалов предыдущих лабораторных работ
Лабораторная работа2: знакомство с платой ПЛИС (FPGA) - основы комбинаторной логики (1) (работа с рычажками) и
Лабораторная работа2: знакомство с платой ПЛИС (FPGA) - основы комбинаторной логики (2) (подключение дисплея).
Также как уже было определено в
первой части лабораторной работы, наш процессор будет иметь возможность общаться с устройствами ввода-вывода через память данных через обычные операции чтения/записи lw/sw, т.е. наша задача сейчас правильно спроецировать порты ввода-вывода перечисленных устройств на адресное пространство памяти данных.
Там же было решено, что в нашем случае адреса устройств ввода-вывода внутри памяти данных будут начинаться с адреса 0x0000f000.
Видео-память 7мисегментный дисплей v
Видео-память 7мисегментный дисплей v подключим к слову в памяти данных по адресу 0x0000f000. Из всего 32хбитного слова к сегментам дисплея подключены только младшие 8 бит, остальные старшие 24 бит - просто всегда нули. Другими словами, единичка, записанная (при помощи операции sw) в один из 8ми младших битов слова в памяти данных по адресу 0x0000f000 будет зажигать соответствующий сегмент на дисплее; нолик, записанный в этот же бит - гасить.
Например, чтобы отобразить на дисплее цифру 5, сначала определим для нее последовательность включенных и выключенных сегментов дисплея (слева направо от 7го к 0му): v[7]=1, v[6]=1, v[5]=1, v[4]=0, v[3]=0 (точка выключена), v[2]=1, v[1]=1, v[=0] или в виде двоичного числа - 11100110. Теперь, если выполнить операцию sw (сохранить слово из регистра в память данных) с исходным значением в регистре b'00000000_00000000_00000000_11100110 в память данных по адресу 0x0000f000, на дисплее загорятся нужные сегменты и мы увидим цифру 5 (без точки).
Массив из 4х рычажков bs
Аналогичным образом массив из 4х рычажков bs подключен к слову в памяти данных по адресу 0x0000f004. Текущее положение внешних рычажков отражают 4 младшие бита, остальные старшие 28 - всегда нули. В данном случае устройство предназначено только для чтения - если в момент чтения слова по адресу 0x0000f004 из памяти данных (при помощи операции lw) определенный рычажок будет находиться в позиции "вкл", то соответствующий бит в считанном слове будет равен 1, если рычок в позиции "выкл", то 0.
Например, чтобы ввести число 5, сначала переводим его в двоичное представление: 5=b'101. Далее рычаги нужно перевести в следующие положения: bs[3]=выкл=0, bs[2]=вкл=1, bs[1]=выкл=0, bs[0]=вкл=1, те. выставить на них значение 0101. Теперь, если выполнить операцию lw (загрузить слово из памяти в регистр) из памяти по адресу 0x0000f0004, то в регистр будет записано двоичное значение: b'00000000_00000000_00000000_0000_0101 (в десятичном представлении это 5).
Однобитный флаг bsf
Однобитный флаг bsf подключен к слову по адресу 0x0000f0008. Все в точности так же, как для bs, только значащим является один младший бит, подключенный к единственному рычажку.
Дополняем модуль Память данных
За работу с памятью данных у нас отвечает модуль память данных (data memory), который мы
уже рассматривали ранее. Вполне логично, что его предыдущую версию следует дополнить функциями доступа к устройствам ввода-вывода по описанной выше схеме.
datamem.v /**
* Память данных. Устройства ввода-вывода спроецированы на
* адреса начиная с 0x0000f000.
*
* v (7мисегментынй дисплей "видео-память") подключен к слову по адресу 0x0000f000,
* bs (массив рычажковых переключателей - button switches) подключен к слову по адресу 0x0000f004,
* bsf (однобитный флаг - рычажковый переключатель) подключен к слову по адресу 0x0000f008.
*
* @param clk - clock
*
* @param we - флаг разрешения записи (write enabled)
* @param addr - адрес доступа на чтение/запись (address)
* @param wd - записываемые данные (write data)
* @param rd - считанные данные (read data)
*
* @param bs - устройство ввода рычажковый переключатель (button switch)
* @param bsf - устройство ввода однобитный флаг (buttons switch flag)
* @param v - устройство вывода 7мегментный дисплей ("video")
*/
module datamem (input clk,
input we, input [31:0] addr,
input [31:0] wd,
output [31:0] rd,
/* Простой ввод/вывод */
input [31:0] bs, input bsf, output [31:0] v);
// массив памяти данных
reg [31:0] RAM [63:0];
// работа с простым вводом/выводом - спроецировать некоторые адреса на внешние устройства:
// "видео-память" - значение для 7мисегментного дисплея
reg [31:0] video;
// запись данных в RAM, если флаг we (write enabled) равен '1'
always @(posedge clk)
if(we)
begin
if(addr == 32'h0000f000)
// запись в видео-память по адресу 0x0000f000
video <= wd;
else
// запись в память данных
RAM[addr[31:2]] <= wd;
end
// bs (массив рычажков - button switches), bsf (флаг button switch flag)
assign rd = addr == 32'h0000f004 ? bs : (addr == 32'h0000f008 ? bsf : RAM[addr[31:2]]);
// и v (7мисегментный дисплей "видео-память")
assign v = ~video; // инвертировать значение для дисплея с общим анодом
endmodule
Видим, что по сравнению со старой версией у нас появилось 3 новых параметра - по одному на каждое подключаемое устройство:
bs - устройство ввода рычажковый переключатель (button switch)
bsf - устройство ввода однобитный флаг (buttons switch flag)
v - устройство вывода 7мегментный дисплей ("video")
module datamem (
...
/* Простой ввод/вывод */
input [31:0] bs, input bsf, output [31:0] v);
Заведем для видео-памяти специальный регистр, хотя в принципе можно было бы обойтись и основным массивом RAM.
reg [31:0] video;
Если в качестве адреса записи указан адрес видео-памяти 0x0000f000, на такт clock осуществляем запись в регистр видеопамяти, во всех остальных случаях записываем как обычно в RAM.
always @(posedge clk)
if(we)
begin
if(addr == 32'h0000f000)
video <= wd;
else
RAM[addr[31:2]] <= wd;
end
И наконец подключаем значение внутреннего регистра видео-памяти к внешнему выходу, ведущему к 7мисегментному дисплею. Также инвертируем биты для дисплея с общим анодом (для дисплеев с общим катодом инверсию '~' делать не нужно).
assign v = ~video;
Теперь любое значение, которое будет записано в память данных по адресу 0x0000f000, отправится в выход модуля v и далее на 7мисегментный дисплей. Видео подключено.
Подключаем массив рычажков bs и однобитный флаг bsf - все в одной строке вместе с чтением значения из обычной памяти данных RAM. Если адрес чтения равен 0x0000f0004, возвращаем результат чтения значение входа bs. Если адрес чтения равен 0x0000f0008, возвращаем результат чтения значение входа bsf. Во всех остальных случаях возвращаем значение из внутреннего массива RAM памяти данных.
assign rd = addr == 32'h0000f004 ? bs : (addr == 32'h0000f008 ? bsf : RAM[addr[31:2]]);
Рычажки подключены тоже.
Дополняем модуль верхнего уровня
Теперь назначим на дисплей и рычажки уже конкретные порты ввода-вывода в модуле верхнего уровня и перейдем к тестированию дизайна на ПЛИС.
mips_top.v /**
* Модуль верхнего уровня для процессора MIPS - подключить clock
* и устройства ввода/вывода:
* 4+1 рычажка для ввода 4хбитных и однобитных значений
* 4+1 светодиодов для подстветки значений рычажков,
* 8 портов вывода для 7мисегментного дисплея.
*/
module mips_top(input clk,
output [7:0] v,
input [3:0] bs,
input bsf,
output [3:0] ld,
output ldf);
// Память инструкций
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,
bs, bsf, v);
// Ядро процессора MIPS
mips mips(clk,
pc, instr,
dmem_we, dmem_addr, dmem_wd, dmem_rd);
// используем светодиоды для подсветки значений рычажковых переключателей
assign ldf = bsf;
assign ld = bs;
endmodule
По сравнению с предыдущей версией появились новые параметры:
Все те же устройства ввода-вывода:
bs - устройство ввода рычажковый переключатель (button switch)
bsf - устройство ввода однобитный флаг (buttons switch flag)
v - устройство вывода 7мегментный дисплей ("video")
Плюс несколько необязательных диодных лампочек, которые мы просто подключим к рычажкам для большей наглядности (рычажок включен - лампочка горит, выключен - не горит):
ld - индикация для рычагов bs
ldf - индикация для рычага bsf
Просто передаем их (ввод-вывод без лампочек) в обновленный модуль память данных.
datamem dmem(clk, dmem_we, dmem_addr, dmem_wd, dmem_rd,
bs, bsf, v);
Замечание: При вводе данных с внешних устройств, особенно с кнопок и переключателей, может происходить такое явление как дребезг контактов (bounce). Это когда рычаг переключен один раз из положения 1 в положение 0, а на подключенном к нему порте ввода-вывода после этого за долю секунды значение сменилось с 1 на 0 и обратно несколько раз, хотя в конечном итоге все-таки остановилось на финальном положении 0. Во многих случаях этим эффектом можно пренебречь, т.к. он в конечном итоге может не повлиять на результат работы устройства. Но в некоторых случаях, особенно когда мы ловим и обрабатываем входящее значение несколько миллионов раз в секунду, он может вызвать слабопредсказуемые проблемы, поэтому для его подавления используются специальные механизмы - модули-фильтры-дебоунсеры (debounce). В данной лабе мы его рассматривать не будем - без дебоунсера будет работать большинство примеров, хотя некоторые непредсказуемые глюки могут возникнуть в последнем калькуляторе именно из-за дребезга контактов на рычаге-флаге bsf. Описание и реализация дебоунсера будут приведены в отдельном разделе за пределами процессорной лабы.
Подключаем лампочки к рычажкам для большей наглядности:
assign ldf = bsf;
assign ld = bs;
Назначаем параметры на адреса портов на плате - для продуктов Xilinx через специальны ucf-файл.
basys2.ucf # Pin assignment for LEDs (show states for slide switches)
NET "ld<3>" LOC = "p6" ;
NET "ld<2>" LOC = "p7" ;
NET "ld<1>" LOC = "m11" ;
NET "ld<0>" LOC = "m5" ;
NET "ldf" LOC = "g1" ;
# Pin assignment for slide switches
NET "bs<3>" LOC = "b4";
NET "bs<2>" LOC = "k3";
NET "bs<1>" LOC = "l3";
NET "bs<0>" LOC = "p11";
NET "bsf" LOC = "n3";
# Pin assignment for clock
NET "clk" LOC = "b8";
# Pin assignment for 7segment display
NET "v<0>" LOC = "B2";
NET "v<1>" LOC = "A3";
NET "v<2>" LOC = "J3";
NET "v<3>" LOC = "B5";
NET "v<4>" LOC = "C6";
NET "v<5>" LOC = "B6";
NET "v<6>" LOC = "C5";
NET "v<7>" LOC = "B7";
Устройства ввода-вывода подключены везде, где нужно. Выбираем нужный тест, генерируем прошивку и запускаем процессор с программой на ПЛИС. Подробные инструкции по геренации файла прошивки и запуску его на ПЛИС можно посмотреть в старой работе "
Лабораторная работа2: знакомство с платой ПЛИС (FPGA) - основы комбинаторной логики (1)". Продолжение
Лабораторная работа 5: делаем процессор MIPS (6) >>