§ Начало

Итак, пришло время, наконец, создавать процессор, очень похожий на 8088, о создании которого я мечтал годами. У этого процессора будут серьезные отличия от 8088, например, работать он будет на частоте 25 мгц и немного побыстрее по декодированию и исполнению инструкции. До этого материала я уже делал несколько реализации процессора, в том числе делал эмулятор. Сегодня я хочу начать цикл статей по созданию такого процессора снова, с нуля.
За основу я выбрал именно 8088, потому что он имеет 8-битную шину данных, ибо, поскольку я не хочу в данный момент погружаться в дебри конвейееров и кешей, то все инструкции будут выполняться просто и последовательно, одна за другой, байт за байтом. Это сэкономит мне время и будет проще воспринять тому, кто все будет читать.
В целом, компьютер будет из себя представлять систему на чипе.
  • Сам процессор
  • Общая быстрая память - до 300Кб
  • Видеопамять - 8Кб
  • Контроллер прерываний
  • Контроллер PS/2
  • Контроллер SPI
  • SDRAM
Общая быстрая память зависит от того, какой в данный момент используется чип. Я буду использовать Циклон 3, поэтому там может быть до 300Кб памяти. Существует и память SDRAM, но она работает более медленно по сравнению с быстрой памятью и ее надо как-то кешировать, чтобы получать доступ к данным.

§ Первичный шаблон

Для разработки процессора я традиционно использую отладчик icarus verilog. Начнем, как и обычно, с создания tb.v:
1`timescale 10ns / 1ns
2module tb;
3
4reg clock;
5reg clock25;
6
7always  #0.5  clock   = ~clock;
8always  #2.0  clock25 = ~clock25;
9
10initial begin clock = 0; clock25 = 0; #2000 $finish; end
11initial begin $dumpfile("tb.vcd"); $dumpvars(0, tb); end
12
13// 1Мб памяти. Простой контроллер памяти
14// -----------------------------------------
15reg  [ 7:0] memory[1024*1024];
16wire [19:0] address; // Шина адреса
17reg  [ 7:0] i_data;  // Входящие данные в процессор
18wire [ 7:0] o_data;  // Исходящие
19wire        we;      // Сигнал на запись данных в память
20
21always @(posedge clock) begin
22
23    i_data <= memory[address];
24    if (we) memory[address] <= o_data;
25
26end
27// -----------------------------------------
28
29endmodule
Добавлен clock25, который генерирует тактовый сигнал в 4 раза медленнее, чем clock. Он и будет использоваться для тактирования процессорного ядра.
В модуле реализовано создание огромного массива памяти memory размером 1Мб по 8 бит. К проводу address будет подключен процессор, который и будет управлять шиной адреса. Также процессор подключен к i_data — входящими данными в него и o_data — исходящими. Как видно, ввод и вывод данных разделены, а не комбинированы, как это делается в обычных процессорах для экономии пинов. Поскольку это система на чипе (SoC), то здесь так можно сделать.
Провод we также контролируется процессором, потому если он =1, то на следующем такте (100 мгц), будет записаны данные o_data по адресу address в память. Здесь память является "плоской", без всяких переименований и перемещений, к тому же, можно записывать в любую ее область, даже в область BIOS. Не знаю, насколько это позволительно так делать, но если что, потом поменяю.
В обработчике always на каждом такте в i_data всякий раз записывается новое значение из памяти по адресу address.

§ Подключение процессорного модуля

Теперь же, чтобы к тестбенчу подключить новый модуль, надо явно указать это в списке подключаемых файлов:
1iverilog -g2005-sv -DICARUS=1 -o tb.qqq tb.v ps2.v cpu.v
Модуль процессора будет находится в cpu.v. Создам новый шаблон для процессора:
1module cpu
2(
3    input               clock,    // 25 Mhz
4    input               reset_n,  // =0 Сброс
5    input               locked,   // =1 Рабочий режим
6    /* verilator lint_off UNDRIVEN */
7    output      [19:0]  address,
8    input       [ 7:0]  i_data,
9    output  reg [ 7:0]  o_data,
10    output  reg         we
11);
12
13initial begin o_data = 8'b0; we = 1'b0; end
14
15endmodule
Здесь сразу скажу, что verilator lint_off UNDRIVEN нужен для того, чтобы верилятор не ругался на то, что сигнал address ни к чему не подключен. И я могу сказать, что он совершенно справедливо ругается. Потом подключение будет, но пока что так.
Помимо уже известных проводов и регистров, на входе есть некоторые пины:
  • reset_n - сигнал сброса, когда он равен 0, процессор необходимо сбросить
  • locked - стабилизация PLL, если он равен =1, то включить процессор в работу, но помимо этой функции, locked=0 еще при остановке выполнения процессора, когда например, данные все еще не готовы с i_data
В сам же файл tb.v можно добавить объявление модуля:
1
2cpu CPU8088
3(
4    .clock      (clock25),    // 25 Mhz!
5    .reset_n    (1'b1),
6    .locked     (1'b1),
7    .address    (address),
8    .i_data     (i_data),
9    .o_data     (o_data),
10    .we         (we)
11);
Как видно, в тестовых целях, reset_n и locked объявлены в 1, что значит то, что значения были уже проинициализированы правильно. Запустив симуляцию в gtkwave, видим картину маслом:

Тут все красное, ибо address никуда не указывает (он в находится в отключенном состоянии), i_data прочитывает из пустоты, и o_data с we равны 0, потому что они были инициализированы через initial в самом модуле процессора.

§ Связывание с верилятором

Поскольку добавился новый, очень важный модуль, такой как процессор, его надо синтезировать верилятором.
Расширяем список синтезируемых верилятором модулей уже до 3-х:
1tbc: verilate
2	g++ -o tb -I$(VINC) tb.cc $(VINC)/verilated.cpp obj_dir/Vvga__ALL.a obj_dir/Vps2__ALL.a obj_dir/Vcpu__ALL.a -lSDL2
3
4verilate:
5	verilator $(WARN) -cc vga.v
6	verilator $(WARN) -cc ps2.v
7	verilator $(WARN) -cc cpu.v
8	cd obj_dir && make -f Vvga.mk
9	cd obj_dir && make -f Vps2.mk
10	cd obj_dir && make -f Vcpu.mk
Встраиваем в tb.cc:
1#include "obj_dir/Vcpu.h"
И в app.cc добавляется:
1protected:
2    ...
3    Vcpu* cpu_mod;
4    ...
5
6App() {
7    ...
8    cpu_mod = new Vcpu();
То есть, добавляем создание нового класса Vcpu. И в методе tick25():
1cpu_mod->clock = 0; cpu_mod->eval(); // В начале
2...
3cpu_mod->clock = 1; cpu_mod->eval(); // В конце
В начале метода добавляется переход clock в 0, а в конце - в clock=1.
Но это еще не все. Процессор, конечно, будет так запущен в работу, но ведь у него нет доступа в память. А это надо организовать.
Вначале я перестрою память и уберу использование vmemory и заменю на memory:
1protected:
2  ...
3  unsigned char* memory;
4  ...
5App() {
6  ...
7  memory = (unsigned char*)malloc(1024*1024);
8  ...
9void destroy() {
10  ...
11  free(memory);
Создается переменная memory, инициализируется, а также не забываем освободить память после завершения программы.
Для видеоадаптера выделяется теперь совершенно другая область памяти, начиная с 0xb8000:
1// Загрузить знакогенератор
2FILE* fp = fopen("font.bin", "rb");
3fread(memory + 0xB8000 + 4096, 1, 4096, fp);
4fclose(fp);
5
6// Заполнить чем-нибудь видеобуфер
7for (int i = 0; i < 4096; i += 2) {
8
9    memory[0xb8000 + i]   = i; // (i>>1) & 255;
10    memory[0xb8000 + i+1] = i+1; //0x17;
11}
Обычно там и находится текстовый видеобуфер. В методе tick25 также не забудем поменять источник данных:
1vga_mod->data = memory[ 0xb8000 + vga_mod->address ];
Это первый этап. Второй этап это связь процессора с памятью. Для этого потребуется добавить 2 строки:
1if (cpu_mod->we) memory[ cpu_mod->address & 0xFFFFF ] = cpu_mod->o_data;
2cpu_mod->i_data = memory[ cpu_mod->address & 0xFFFFF ];
Если есть запись, записать, и потом прочесть новое значение. То есть, если we=1, то после записи из o_data, в i_data появится это значение сразу же.
В данный момент, пока процессор не работает, ничего не будет происходить вообще. Чтобы процессор заработал, предстоит еще очень и очень много чего сделать...
Но на этом пожалуй, я завершу эту статью.
Исходные коды скачать тут.