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

Ну что, начинаем новый цикл, снова и снова круговорот процессоров 86-х в природе. Теперь же я нацелился на поддержку 386-го, сделать процессор, который будет поддерживать 32-х битные инструкции, а также защищенный режим.
Как и обычно, надо начать с некоторого шаблона, и он будет предельно простым пока что:
1#include "src/app.cc"
2
3int main(int argc, char** argv) {
4
5    App* v386 = new App(argc, argv);
6    while (v386->main()) { }
7    return v386->destroy();
8}
Код файла 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) пока что все очень просто:
1        org     0
2        jmp     $
3times   65536-16-$ db 0
4        jmp     far 0F000h : 0
5db      '27/04/22'
6db      0x00, 0xFE, 0x00
Создается 64Кб файл, который будет находится в старших адресах, начиная с F0000h и заканчивая FFFFFh.

§ Добавим VGA

Теперь быстрыми темпами усложняем tb.cc:
1#include "src/app.cc"
2#include "obj_dir/Vvga.h"
3
4int main(int argc, char** argv) {
5
6    int   instr = 125000;
7    float target = 100;
8
9    Verilated::commandArgs(argc, argv);
10    App* v386 = new App(argc, argv);
11
12    while (v386->main()) {
13
14        Uint32 start = SDL_GetTicks();
15
16        // Автоматическая коррекция кол-ва инструкции в секунду
17        for (int i = 0; i < instr; i++) v386->tick();
18
19        // Коррекция тактов
20        Uint32 delay = (SDL_GetTicks() - start);
21        instr = (instr * (0.5 * target) / (float)delay);
22        instr = instr < 1000 ? 1000 : instr;
23
24        if (Verilated::gotFinish()) break;
25    }
26
27    return v386->destroy();
28}
В проект был добавлен модуль VGA #include "obj_dir/Vvga.h", но пока что без процессора, но есть одна интересная особенность, это автоматическая подстройка на % загрузки хост-процессора. Перед исполнением 125000 тактов, они всегда исполняются, замеряется время в миллисекундах, и корректируется в сторону уменьшения или увеличения. Если, к примеру, target=100%, то 0.5*100 = 50 мс (20 кадров в секунду) — это целевое время, за которое должен проходить instr. Если это время составило 100 мс, то количество instr уменьшается в 2 раза, и наоборот, увеличивается, если прошло быстрее, тем самым постоянно автоматически корректируясь.
Поскольку модуль vga я и так ранее разрабатывал, то ни код, ни разбор приводить здесь не буду, он все равно есть в файле.
Команда сборки makefile:
1VRL=/usr/share/verilator/include
2
3all: verilate tbc
4icarus:
5tbc:
6	g++ -o tb -I$(VRL) $(VRL)/verilated.cpp tb.cc \
7	obj_dir/Vvga__ALL.a \
8	-lSDL2
9	./tb
10verilate:
11	verilator -cc vga.v
12	cd obj_dir && make -f Vvga.mk
Цель all собирает сначала секцию verilate, а потом tbc. Модуль пока что один.
Метод tick работает пока что очень просто. Как и в прошлых статьях, здесь процессор работает на скорости 25 мгц, так же, как и vga-модуль.
1void tick() {
2
3    // Чтение из памяти
4    vga_mod->data = memory[0xb8000 + vga_mod->address];
5
6    // Запуск модулей
7    vga_mod->clock = 0; vga_mod->eval();
8    vga_mod->clock = 1; vga_mod->eval();
9
10    vga(vga_mod->hs, vga_mod->vs, (vga_mod->r*16)*65536 + (vga_mod->g*16)*256 + (vga_mod->b*16));
11}
Вот и скриншот экрана. Он пока что работает довольно быстро, потому что в данный момент не навешано разнообразных модулей, которые до невозможности замедляют симуляцию кода.

§ Тестбенч

Теперь же надо создать код для запуска и отладки будущего процессора. Первым будет файл tb.v:
1`timescale 10ns / 1ns
2module tb;
3
4// ---------------------------------------------------------------------
5reg         clock;
6reg         clock_25;
7reg [7:0]   memory[1024*1024];
8
9always  #0.5  clock    = ~clock;
10always  #1.5  clock_25 = ~clock_25;
11initial begin clock = 0; clock_25 = 0; #2000 $finish; end
12initial begin $dumpfile("tb.vcd"); $dumpvars(0, tb); end
13initial begin $readmemh("bios/bios.hex", memory, 20'hF0000); end
14// ---------------------------------------------------------------------
15
16wire [31:0] address;
17reg  [ 7:0] in;
18wire [ 7:0] out;
19wire        we;
20// Контроллер блочной памяти
21always @(posedge clock) begin in <= memory[address[19:0]]; if (we) memory[address[19:0]] <= out; end
22// ---------------------------------------------------------------------
23
24endmodule
Как и обычно, задаются две частоты — это 100 (clock) и 25 мгц (clock_25), и читается bios.hex в память, последние 64Кб, якобы там ROM BIOS, хотя на самом деле там будет RAM BIOS. Мне кажется, нельзя так делать, честно говоря, но я все равно так делаю, потому что bios по определению, затирать нельзя, а у меня можно, вот такие вот странные вещи творятся.
Далее, контроллер блочной памяти. При появлении сигнала we, на следующем такте будет записано новое значение и прочитано тоже, как и работает в реальной схеме внутри ПЛИС.
Объявляются пины у процессора:
1module v386
2(
3    // Тактовый генератор
4    input               clock,
5    input               reset_n,
6    input               locked,
7    // Магистраль данных 8 битная
8    output      [31:0]  address,
9    input       [ 7:0]  in,
10    output reg  [ 7:0]  out,
11    output reg          we
12);
13
14endmodule
Легко заметить, как я постоянно на одни и те же грабли наступаю, например, что шина данных 8 битная, хотя в 386 она уже была 32-х битная, но мне как-то то ли лень делать, то ли влом, но делаю так, чтобы данные читались байт за байтом, что замедляет скорость работы в процессора в разы, но упрощает его разработку.
В файле tb.v объявляю блок процессора:
1v386 v386_inst
2(
3    .clock      (clock_25),
4    .reset_n    (1'b1),
5    .locked     (1'b1),
6    .address    (address),
7    .in         (in),
8    .out        (out),
9    .we         (we)
10);
Просто прицепляя нужные пину к нужным проводам. На этом мои размышления на сегодня заканчиваются.
Файлы проекта