§ Шаблонный код
Ну что, начинаем новый цикл, снова и снова круговорот процессоров 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);Просто прицепляя нужные пину к нужным проводам. На этом мои размышления на сегодня заканчиваются.
Файлы проекта
[Оглавление] [Следующая >>]