§ Начало

Для меня разработка любого небольшого процессора всегда начинается с создания файла 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, сборщик проектов
Это 4 основных программы, в которых и происходит разработка процессоров.

§ 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 и стек.