Лисья Нора

Оглавление


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

Ну что, начинаем новый цикл, снова и снова круговорот процессоров 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));
}
Вот и скриншот экрана. Он пока что работает довольно быстро, потому что в данный момент не навешано разнообразных модулей, которые до невозможности замедляют симуляцию кода.
ch2-v386.png

§ Тестбенч

Теперь же надо создать код для запуска и отладки будущего процессора. Первым будет файл 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)
);
Просто прицепляя нужные пину к нужным проводам. На этом мои размышления на сегодня заканчиваются.