§ Начало
Для меня разработка любого небольшого процессора всегда начинается с создания файла testbench, в котором есть минимальный набор тех инструментов, с помощью которых я буду делать процессор. Это обязательно: объявление памяти на 1 мб или 64 кб, это контроллер памяти (простейший), подключение тактовых сигналов на 100 и 25 мгц, подключение самого процессора. Минимум того, что надо сделать, всегда находится в файлеtb.v
. Так что сегодня тоже не будет исключением начать разработку с этого кода.! Могу лишь предупредить, что у меня есть привычка не дописывать материалы до конца, так что вполне вероятно, что и этот сезон написания статей по процессору не будет сделан. Это нормально для меня и не стоит ничему удивляться, если внезапно материал не будет завершен.
Что я буду делать? Как и обычно, процессор на базе архитектуры Intel x86, 8 битный. Даже не 16 битный, а именно 8 битный. Постараюсь как можно более подробно рассказать о разработке процессора без спешки. Мне некуда в этом деле спешить и рваться делать как можно быстрее, да и я даже могу отвлекаться на посторонние разговоры. Какая кому разница, если это мой сайт и я пишу тут что захочу? Вот в том и дело.
§ *nix
Начну с того, что я сижу в *nix подобной системе и потому зачастую работаю из командной строки, но это не значит, что нельзя разрабатывать процессор и для Windows XP, например. Рекомендую эту систему как самую лучшую в 2024 году, помимо *nix. Итак, поскольку я сейчас в данный момент в *nix-системе, а точнее, *buntu-подобной, то следует сказать, чем я пользуюсь для создания процессоров на коленке.- Icarus Verilog —
sudo apt install iverilog
, компилятор verilog-файлов - GTKWave —
sudo apt install gtkwave
, визуализатор того, что было скомпилировано и симулировано - Verilator —
sudo apt install verilator g++
, компилятор в файлы Си - Makefile —
sudo apt install makefile
, сборщик проектов
§ TB.V
Файл для отладки всегда начинается одинаково, потому что я уже давно использую шаблон для этого.
`timescale 10ns / 1ns
module i386;
В самой первой строке находится указание для gtkwave, в котором рассказывается о том, как отображать картинку на экране. В качестве системы отсчета берется 10 наносекунд, что равно 100 мгц, если считать с точки зрения тактовых сигналов. На самом деле, 10 нс здесь означает просто одну единицу. То есть, если сказать иначе, за основу единицы измерения берется 10 нс, и это примерно так, как если бы мы взяли за основу 1 см в качестве точки отсчета. Последующий за ним 1ns указывает, что минимальный временной диапазон в проекте может быть не менее чем 1 ns, то есть, мы, даже если и захотим, не сможем создать тактовую частоту более чем 1 Ггц, так как минимальным промежутком времени является 1 нс в проекте.reg reset_n; reg clock_hi; always #0.5 clock_hi = ~clock_hi; reg clock_25; always #2.0 clock_25 = ~clock_25;Выше в тексте, в тестовом файле, создаются тактовые генераторы, которые основаны на регистрах
clock_hi
и clock_25
. Перед регистром clock_hi стоит параметр always #0.5, который означает, что каждые 0.5 единиц времени (а это 10 на, умноженное на 0.5), то есть, каждые 5 нс, перебрасывается с 0 на 1, и с 1 на 0, то есть, другими словами, создается тактовый сигнал на 100 мгц, который в течении 10 нс делает две переброски сигнала.Аналогично с clock_25, но переброска сигнала осуществляется раз в 25 нс, то есть, создается тактовая частота, эквивалентная 25 Мгц. Для меня эти частоты имеют особенное значение, потому что я люблю реализовывать видеопроцессор, который выдает картинку VGA: 640 на 400 (или 480), и который работает на частоте 25 мгц. Я не отступаю уже очень много лет и все мои видеопроцессоры поддерживают только одно лишь разрешение.
Рассмотрим далее.
initial begin $dumpfile("i386.vcd"); $dumpvars(0, i386); clock_hi = 0; clock_25 = 0; reset_n = 0; #3.0 reset_n = 1; #1000 $finish; endДля компилятора iverilog есть специальные команды, такие как $dumpfile и $dumpvars, в которых указывается, куда выгружать результат компиляции. Директива $dumpfile показывает, что результат будет выгружен в файл i386.vcd, а директива $dumpvars выгружает сигналы (0-все сигналы) из модуля i386, самого высокого уровня модуля, в файл i386.vcd. При этом выгружаются все сигналы вообще и даже с субмодулей.
Здесь есть еще и регистр reset_n, который сначала становится равным 0, а через небольшое количество времени 1. Этот регистр отвечает за сброс процессора.
Директива $finish останавливает симуляцию схемы, это нужно будет для симулятора. То есть файл сначала надо скомпилировать с помощью программы
iverilog
, а потом при помощи программы vvp
запустить симуляцию. Симуляция будет происходит до того момента, пока не достигнет директивы $finish, на чем и остановится.§ Контроллер памяти
Иногда, плис содержит небольшое количество внутренней блочной памяти. Эта память размещена рядом с массивами блоков с логическими элементами, которые отвечают за эмуляцию цифровой логики, и обычно, один блок памяти содержит 1 килобайт. Как ее подключать, это уже отдельно рассказывать надо, а сейчас необходимо ее эмулировать.Эмуляция заключается в том, чтобы создать необходимое количество регистров по 8 бит, например 65536 регистров, объединить их в массив. Старая версия классического верилога не позволяет этого сделать, так что приходится использовать версию верилога, который называется system verilog. Он включается отдельной опцией при компиляции v-файлов в makefile.
reg [ 7:0] ram[65536]; reg [ 7:0] in; wire [ 7:0] out; wire we; wire [15:0] address; always @(posedge clock_hi) begin in <= ram[address]; if (we) ram[address] <= out; endТак выглядит простейший контроллер памяти, который будет весьма полезен в дальнейшем. Во-первых, объявляются 65536 регистров, по 8 бит, первой строчкой. Во-вторых, объявляются провода
in
, out
, we
и address
.Адрес указывает место в памяти, откуда будет читаться в регистр
in
, как видно далее по тексту, на каждом такте (100 мгц) записывается новое значение из ram в in по адресу address. В регистре out
из процессора приходят данные, которые запишутся при наличии сигнала we=1
, и это произойдет на следующем такте clock_hi
.Чтение и запись из памяти происходит не сразу. Чтобы прочитать из памяти, сначала надо установить адрес, после чего подождать 1 такт, и уже прочитать на следующем такте новые данные. Так же и с записью, после того как запись была пройдена, надо подождать еще 1 такт, чтобы прочесть уже новые данные, то есть на запись требуется 2 такта.
Поскольку скорость процессора у меня всего 25 мгц, а скорость внутренней памяти всегда 100 мгц, то для чтения и записи отводится 4 такта, что вполне хватает для всех задач.
§ Подключение процессора
И последнее, что необходимо пока что сделать в тестовом файле, это подключить процессор.core CORE ( .clock (clock_25), .reset_n (reset_n), .ce (1'b1), .a (address), .i (in), .o (out), .w (we) );Здесь все как обычно, классика. Определяется минимальный набор пинов, clock — для тактирования процессора, reset_n — для первоначального сброса, ce — chip enabled, чтобы указать, что процессор работает (1 — всегда работает). Ну а остальные пины и так очевидны.
§ Итоговый
Код в итоге получится такой.1`timescale 10ns / 1ns 2module i386; 3 4// Тестбенчевые сигналы 5// ============================================================================= 6reg reset_n; 7reg clock_hi; always #0.5 clock_hi = ~clock_hi; 8reg clock_25; always #2.0 clock_25 = ~clock_25; 9 10initial begin $dumpfile("i386.vcd"); $dumpvars(0, i386); $readmemh("i386.hex", ram, 16'h0100); end 11initial begin clock_hi = 0; clock_25 = 0; reset_n = 0; #3.0 reset_n = 1; #1000 $finish; end 12 13// Контроллер блочной памяти 14// ============================================================================= 15 16reg [ 7:0] ram[65536]; 17reg [ 7:0] in; 18wire [ 7:0] out; 19wire we; 20wire [15:0] address; 21 22always @(posedge clock_hi) begin in <= ram[address]; if (we) ram[address] <= out; end 23 24// Подключить няшный процессор 25// ============================================================================= 26core CORE 27( 28 .clock (clock_25), 29 .reset_n (reset_n), 30 .ce (1'b1), 31 .a (address), 32 .i (in), 33 .o (out), 34 .w (we) 35); 36 37endmoduleИз нового добавленного в коде будет только
$readmemh("i386.hex", ram, 16'h0100);
который читает содержимое i386.hex файла в память RAM по адресу 100h. То есть первые 256 байт будут отведены под IRQ и стек.