Лисья Нора

Оглавление


§ Для чего

Какую задачу я ставлю? Как и обычно, просто рассказать о том, как я в очередной раз создаю очередной процессор, и еще как-то начать его использовать для некоторых целей. Например, написать программы для него, создать управляющий софт, да и просто для души.
Итак, я снова начинаю свой дневник разработки процессора, похожего на процессор Z80. Отличия будут в том, что:
В этом цикле статей я последовательно расскажу о том, как с нуля создать процессор, похожий на Z80, симуляции в icarus/gtkwave, а также симуляции с верилятором в связке с SDL.

§ Самый первый testbench

Прежде чем приступать к разработке процессора, нужно создать первичный файл tb.v, который я буду далее тестировать с помощью компилятора icarus verilog. Как работает icarus? С его помощью можно откомпилировать файл, после чего выполнить симуляцию проекта и просмотреть результаты работы verilog-кода в программе gtkwave. Но для этого, нужно сделать тест-бенч tb.v:
`timescale 10ns / 1ns
module tb;
 
// -----------------------------------------------------------------------------
reg clock; always #0.5 clock = ~clock;
reg clock_25; always #1.0 clock_25 = ~clock_25;
// -----------------------------------------------------------------------------
initial begin clock = 0; clock_25 = 0; #2000 $finish; end
initial begin $dumpfile("tb.vcd"); $dumpvars(0, tb); end
// -----------------------------------------------------------------------------
 
endmodule
Он у меня всегда выглядит типично. Создается два источника тактовых частот clock – 100 mhz, clock_25 – 25 mhz, соответственно. Это виртуальные пины генерации частоты. Изначально они установлены в 0 в секции initial, и продолжительность всей симуляции указывается в #2000 $finish, что говорит о том, что симуляция составляет 10 ns * 2000 = 20 микросекунд. Один отсчет устанавливается в параметре timescale в самой первой строке, и он равен 10 наносекунд.
После компиляции проекта, дамп всех регистров ($dumpvars) из модуля tb выгружается в дампфайл ($dumpfile) по имени tb.vcd.
Перейдем к файлу makefile, который я активно использую для компиляции проектов. Для win-платформ такой файл тоже работает, если установить необходимые инструменты. На всякий случай, можно также использовать bat-файлы для этого, но только я привык работать с линукса, потому буду делать именно так.
all:
iverilog -g2005-sv -DICARUS=1 -o tb.qqq tb.v
vvp tb.qqq >> /dev/null
rm tb.qqq
В makefile крайне важно, чтобы отступы справа были именно табами, а не пробелами, так что это надо обязательно учитывать и проверять.
В файле сборки выполняются три команды.
Команда первая iverilog -g2005-sv -DICARUS=1 -o tb.qqq tb.v
Строка с командой vvp tb.qqq >> /dev/null выполняет симуляция скомпилированного файла tb.qqq в tb.vcd, указанному в тестбенче, и третья строка rm tb.qqq удаляет временный файл, поскольку он более не нужен будет далее.

§ Просмотр в gtkwave

Теперь, после того как получилось выполнить симуляцию проекта, его можно просмотреть в gtkwave. Для начала, нужно, чтобы эта программа вообще была установлена. Если это так, то из консоли папки с проектом надо запустить сначала следующую команду:
gtkwave tb.vcd
После запуска, откроется чистое окно, где потребуется добавить необходимые нам провода, за которыми будет вестись наблюдение.
ch1-gtkwave.png
Чтобы добавить провод на схему справа, нужно нажать правой кнопкой мыши на список проводов слева снизу в панели и нажать Append. В случае если необходимо поменять цвет у сигнала, нужно нажать в списке сигналов (панель посередине) правой кнопкой мыши, появится выпадающее меню, там выбрать Color Format и необходимый цвет. На картине я выбрал оранжевый вместо зеленого для clock.
После всех действий можно сохранить проект через комбинацию клавиш CTRL+S. Обычно я сохраняю в tb.gtkw и далее уже вызываю именно этот файл вместо tb.vcd.

§ SDL-приложение

Поскольку я буду делать процессор не только для icarus verilog, но также проверять его в verilator, для этого мне потребуется окно вывода для видеоадаптера собственного изготовления. Можно было бы не делать, но я все-таки, сделаю, потому что этот способ оказался наиболее удобным.
Тестовый и простой видеоадаптер будет иметь разрешение 640x400, но сам по себе вывод на экран будет в окне 256x192, но это не спектрумский экран, а текстовый, и его разрешение 32x24, что само по себе составляет 768 символов. Для начала – хватит. Количество символов не превышает 1кб. В данный момент текстовый режим полностью черно-белый, только 2 цвета. Для видеоадаптера требуется знакогенератор 8x8 на 256 символов. Каждый символ занимает 8 байт по 8 бит, и значит, потребуется 2Кб памяти ПЗУ для хранения этих данных. По итогу, нужно будет 3Кб памяти для тестового видеоадаптера. Еще раз повторю, что это минимальный видеоадаптер, потому что возможно, что он может потом как-то меняться в будущем на другой.
Пожалуй, начнем создание приложения tb.cc.
#include "tb.h"
 
int main(int argc, char* argv[]) {
 
App* app = new App(); // Создать окно приложения
while (app->main()) {} // Выполнение цикла каждые 1/50 сек
return app->destroy(); // Закрыть окно
}
В этом коде нет ничего сложного. Вначале подключается класс tb.h, в котором будет описан class App, отвечающий за работу приложения. В цикле while, а именно в методе main класса App будет ожидание 20 мс, после чего этот метод возвратит 1, что означает что необходимо обработать еще один фрейм. Я выбрал 1/50 в дань 25 (или 50 Гц) экранам. На самом же деле, экран VGA обновляется в 60 Гц, но здесь это заметно будет очень мало, да и еще потому что 1000 делить на 50 будет намного удобнее, чем на 60.
Если метод main возвращает 0, то это признак того, что окно надо закрыть и выйти из приложения.
Ниже приведу код самого минимального класса, который бы заработал в данном контексте:
#include <SDL2/SDL.h>
#include <stdlib.h>
#include <stdio.h>
 
class App {
protected:
public:
 
void frame();
int main() { return 0; }
void update() { }
int destroy() { return 0; }
void pset(int x, int y, Uint32 color) { }
};
Этот код пока что ничего не выполняет. Далее по тексту буду дописывать и дополнять код в методах.
[~] Кстати, хочу сразу сказать, что то, как я пишу код сейчас – очень убого и плохой стиль кода, но мне все равно. Дело в том, что я не хочу лишний раз разбивать код на разные файлы и части.
Приведу обновленный makefile:
all: icarus app
icarus:
iverilog -g2005-sv -DICARUS=1 -o tb.qqq tb.v
vvp tb.qqq >> /dev/null
rm tb.qqq
app:
g++ -o tb tb.cc -lSDL2
strip tb
Как он работает? Первым делом, при запуске задачи all (или запуск make без параметров), будет выполнены две подзадачи icarus и app. С первой задачей выше уже было разобрано, а со второй все просто. Компилируется файл tb.cc, результат выдается в tb, причем захватывая стадию как компиляции, так и линковки, с подключением библиотеки SDL2.

§ Создание и уничтожение окна SDL

Для того, чтобы создать минимально рабочий код, я разделю его на три части:
1) Объявление переменных, в которых будет храниться данные о буферах SDL
2) Открытие окна в конструкторе класса
3) Закрытие окна в методе destroy
Объявляются переменные в классе App, в области protected:
protected:
 
SDL_Window* sdl_window; // Объект окна SDL
SDL_Renderer* sdl_renderer; // Класс объекта-рисовщика на окне
SDL_Texture* sdl_screen_texture; // Текстура с экранным буфером
Uint32* screen_buffer; // Область памяти, где хранятся данные для текстуры
Собственно, здесь и так в комментариях я достаточно объяснил, что к чему, потому сразу переходим к коду конструктора класса. Сразу же замечу, что создается окно размером 1280 на 800 (вдвое больше), а не 640 на 400, поскольку разглядеть пиксели такого размера очень сложно на современных дисплеях, которые имеют разрешение как минимум 1920x1080, так что размер окна будет именно таким.
App() {
 
if (SDL_Init(SDL_INIT_VIDEO)) {
exit(1);
}
 
screen_buffer = (Uint32*) malloc(640 * 400 * sizeof(Uint32));
sdl_window = SDL_CreateWindow("M80", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 800, SDL_WINDOW_SHOWN);
sdl_renderer = SDL_CreateRenderer(sdl_window, -1, SDL_RENDERER_PRESENTVSYNC);
sdl_screen_texture = SDL_CreateTexture(sdl_renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, 640, 400);
SDL_SetTextureBlendMode(sdl_screen_texture, SDL_BLENDMODE_NONE);
}
Теперь распишу то, что происходит в коде:
Метод destroy() так же достаточно прост, и даже не требует пояснений.
int destroy() {
 
free(screen_buffer);
SDL_DestroyTexture (sdl_screen_texture);
SDL_DestroyRenderer (sdl_renderer);
SDL_DestroyWindow (sdl_window);
SDL_Quit();
return 0;
}

§ Главный цикл ожидания события SDL

Постепенно переходим к основному циклу, где будут как фиксироваться входящие события, такие как нажатие на клавишу или движение, клик мыши, закрытия окна (и многих других), так и ожидающий 20 мс, после истечения этого времени происходит выход из процедуры main для последующей передачи управления в тело цикла while в главном файле tb.cc.
Ниже я приведу код метода main класса App:
int main() {
 
SDL_Event evt;
 
for (;;) {
 
Uint32 ticks = SDL_GetTicks();
 
// Обработать все новые события
while (SDL_PollEvent(& evt)) {
 
switch (evt.type) {
 
// Выход из программы
case SDL_QUIT: return 0;
}
}
 
// Истечение таймаута: обновление экрана
if (ticks - pticks >= 20) {
 
pticks = ticks;
 
frame(); // Вызов обработчика кадра
update(); // Вызов обновления экрана
 
return 1;
}
 
SDL_Delay(1);
}
}
Теперь объясняю то, как работает алгоритм. Сам по себе код крутится в бесконечном цикле, в теле которого есть 2 части:
int pticks = 0;
Если событие не произошло и все события SDL были успешно обработаны, то для предотвращения загрузки процессора на 100% я делаю вызов SDL_Delay(1); который ожидает 1 мс. Этим методом, я снижаю ужасную нагрузки на CPU. Если не сделать, то будет плохо. Вот так.
Обработчик кадра вызывается отдельно и я его позже реализую в tb.cc:
void App::frame() { /* Заглушка */ }
Сейчас можно поставить лишь заглушку в виде метода, который никак не реализован.
Перейду к реализации метода update() для копирования из памяти в окно:
void update() {
 
SDL_Rect dstRect;
 
dstRect.x = 0;
dstRect.y = 0;
dstRect.w = 1280;
dstRect.h = 800;
 
SDL_UpdateTexture (sdl_screen_texture, NULL, screen_buffer, 640 * sizeof(Uint32));
SDL_SetRenderDrawColor (sdl_renderer, 0, 0, 0, 0);
SDL_RenderClear (sdl_renderer);
SDL_RenderCopy (sdl_renderer, sdl_screen_texture, NULL, &dstRect);
SDL_RenderPresent (sdl_renderer);
}
Вначале проставляется размерность той области, куда именно будет выведена текстура, то есть, на полную поверхность окна (1280 x 800).
И последнее, что сегодня будет реализовано, это базовая процедура рисования точки в памяти (буфер экрана):
void pset(int x, int y, Uint32 color) {
 
if (x < 0 || y < 0 || x > 640 || y >= 400)
return;
 
screen_buffer[y*640 + x] = color;
}
Думаю, тут достаточно все очевидно.
Сегодня получилось создать минимальное SDL-приложение и минимальный код на верилоге для проверки. На этом, сегодня я завершу первые приготовления к следующему этапу, о котором я расскажу в следующей статье.