§ Шаблонный код

Ну что, начинаем новый цикл, снова и снова круговорот процессоров 86-х в природе. Теперь же я нацелился на поддержку 386-го, сделать процессор, который будет поддерживать 32-х битные инструкции, а также защищенный режим.
Как и обычно, надо начать с некоторого шаблона, и он будет предельно простым пока что:
#include "src/app.cc"

int main(int argc, char** argv) {

    App* v386 = new App(argc, argv);
    while (v386->main()) { }
    return v386->destroy();
}
Код файла app.cc я уже ранее приводил, да и к тому же, он будет в прикрепленном файле. Одна немаловажная вещь, которую я добавил сюда — это дизассемблер, который будет постоянно далее использоваться для отладки.

§ Структура проекта

Как выглядит структура проекта
src
-- app.h           Заголовочный файл для класса App
-- disasm.h        Для класса Disassemble
-- app.cc          Коды App
-- disasm.cc       Код для Disassemble
bin
-- font.bin        Шрифты 8x16
bios
-- bios.asm        Биос для компьютера
-- makefile        Сборка биоса
tb.cc              Исполняемый файл
tb.v               Тестбенч для проверки процессора
v386.v             Сам процессор
vga.v              Для отображения видеоданных
makefile           И сборка
С биосом (bios.asm) пока что все очень просто:
        org     0
        jmp     $
times   65536-16-$ db 0
        jmp     far 0F000h : 0
db      '27/04/22'
db      0x00, 0xFE, 0x00
Создается 64Кб файл, который будет находится в старших адресах, начиная с F0000h и заканчивая FFFFFh.

§ Добавим VGA

Теперь быстрыми темпами усложняем tb.cc:
#include "src/app.cc"
#include "obj_dir/Vvga.h"

int main(int argc, char** argv) {

    int   instr = 125000;
    float target = 100;

    Verilated::commandArgs(argc, argv);
    App* v386 = new App(argc, argv);

    while (v386->main()) {

        Uint32 start = SDL_GetTicks();

        // Автоматическая коррекция кол-ва инструкции в секунду
        for (int i = 0; i < instr; i++) v386->tick();

        // Коррекция тактов
        Uint32 delay = (SDL_GetTicks() - start);
        instr = (instr * (0.5 * target) / (float)delay);
        instr = instr < 1000 ? 1000 : instr;

        if (Verilated::gotFinish()) break;
    }

    return v386->destroy();
}
В проект был добавлен модуль VGA #include "obj_dir/Vvga.h", но пока что без процессора, но есть одна интересная особенность, это автоматическая подстройка на % загрузки хост-процессора. Перед исполнением 125000 тактов, они всегда исполняются, замеряется время в миллисекундах, и корректируется в сторону уменьшения или увеличения. Если, к примеру, target=100%, то 0.5*100 = 50 мс (20 кадров в секунду) — это целевое время, за которое должен проходить instr. Если это время составило 100 мс, то количество instr уменьшается в 2 раза, и наоборот, увеличивается, если прошло быстрее, тем самым постоянно автоматически корректируясь.
Поскольку модуль vga я и так ранее разрабатывал, то ни код, ни разбор приводить здесь не буду, он все равно есть в файле.
Команда сборки makefile:
VRL=/usr/share/verilator/include

all: verilate tbc
icarus:
tbc:
	g++ -o tb -I$(VRL) $(VRL)/verilated.cpp tb.cc \
	obj_dir/Vvga__ALL.a \
	-lSDL2
	./tb
verilate:
	verilator -cc vga.v
	cd obj_dir && make -f Vvga.mk
Цель all собирает сначала секцию verilate, а потом tbc. Модуль пока что один.
Метод tick работает пока что очень просто. Как и в прошлых статьях, здесь процессор работает на скорости 25 мгц, так же, как и vga-модуль.
void tick() {

    // Чтение из памяти
    vga_mod->data = memory[0xb8000 + vga_mod->address];

    // Запуск модулей
    vga_mod->clock = 0; vga_mod->eval();
    vga_mod->clock = 1; vga_mod->eval();

    vga(vga_mod->hs, vga_mod->vs, (vga_mod->r*16)*65536 + (vga_mod->g*16)*256 + (vga_mod->b*16));
}
Вот и скриншот экрана. Он пока что работает довольно быстро, потому что в данный момент не навешано разнообразных модулей, которые до невозможности замедляют симуляцию кода.

§ Тестбенч

Теперь же надо создать код для запуска и отладки будущего процессора. Первым будет файл tb.v:
`timescale 10ns / 1ns
module tb;

// ---------------------------------------------------------------------
reg         clock;
reg         clock_25;
reg [7:0]   memory[1024*1024];

always  #0.5  clock    = ~clock;
always  #1.5  clock_25 = ~clock_25;
initial begin clock = 0; clock_25 = 0; #2000 $finish; end
initial begin $dumpfile("tb.vcd"); $dumpvars(0, tb); end
initial begin $readmemh("bios/bios.hex", memory, 20'hF0000); end
// ---------------------------------------------------------------------

wire [31:0] address;
reg  [ 7:0] in;
wire [ 7:0] out;
wire        we;
// Контроллер блочной памяти
always @(posedge clock) begin in <= memory[address[19:0]]; if (we) memory[address[19:0]] <= out; end
// ---------------------------------------------------------------------

endmodule
Как и обычно, задаются две частоты — это 100 (clock) и 25 мгц (clock_25), и читается bios.hex в память, последние 64Кб, якобы там ROM BIOS, хотя на самом деле там будет RAM BIOS. Мне кажется, нельзя так делать, честно говоря, но я все равно так делаю, потому что bios по определению, затирать нельзя, а у меня можно, вот такие вот странные вещи творятся.
Далее, контроллер блочной памяти. При появлении сигнала we, на следующем такте будет записано новое значение и прочитано тоже, как и работает в реальной схеме внутри ПЛИС.
Объявляются пины у процессора:
module v386
(
    // Тактовый генератор
    input               clock,
    input               reset_n,
    input               locked,
    // Магистраль данных 8 битная
    output      [31:0]  address,
    input       [ 7:0]  in,
    output reg  [ 7:0]  out,
    output reg          we
);

endmodule
Легко заметить, как я постоянно на одни и те же грабли наступаю, например, что шина данных 8 битная, хотя в 386 она уже была 32-х битная, но мне как-то то ли лень делать, то ли влом, но делаю так, чтобы данные читались байт за байтом, что замедляет скорость работы в процессора в разы, но упрощает его разработку.
В файле tb.v объявляю блок процессора:
v386 v386_inst
(
    .clock      (clock_25),
    .reset_n    (1'b1),
    .locked     (1'b1),
    .address    (address),
    .in         (in),
    .out        (out),
    .we         (we)
);
Просто прицепляя нужные пину к нужным проводам. На этом мои размышления на сегодня заканчиваются.
Файлы проекта