Лисья Нора

Оглавление


§ Какие изменения

Сегодня я расширю возможности видеоадаптера, сделав его приблизительно равным стандартному текстовому режиму, который есть в MSDOS, например.

§ Устройство видеобуфера

Как видимо, видеобуфер придется увеличить в 2 раза, а также в 2 раза увеличить знакогенератор, если 256 символов и каждый знак занимает 16 байт, то получится 4096 байт. Видеобуфер устроен таким образом, что одно знакоместо занимает 2 байта. Первый байт это сам знак, а второй байт это цвет этого знака.
7 | 6 5 4 | 3 2 1 0
B | Фон | Цвет
Цвет отвечает за рисуемый пиксель, а фон, наоборот, за пиксель, который равен 0. B – это признак мерцания цвета, то есть, когда установлен B=1, то каждые полсекунды цвет убирается и превращается в фоновый.
Вообще цвета забираются из палитры, но палитру я пока делать не буду, потому все цвета будут жестко закреплены:
0 Черный 1 Синий 2 Зеленый 3 Бирюзовый
4 Красный 5 Пурпурный 6 Коричневый 7 Серый
8 Темносерый 9 Ярко-синий 10 Ярко-зеленый 11 Голубой
12 Ярко-красный 13 Розовый 14 Желтый 15 Белый

§ Перенос методов в класс App

Для начала, обновлю font.bin, добавив туда оставшиеся 128 знаков, но я хочу еще провести некоторую реорганизацию кода. Первое, я перенесу код инициализации видеопамяти в конструктор класса App:
FILE* fp = fopen("font.bin", "rb");
fread(vmemory + 4096, 1, 4096, fp);
fclose(fp);
А сам vmemory добавлю в protected-область:
protected:
...
unsigned char vmemory[8192];
...
Vvga* vga_mod;
Vps2* ps2_mod;
...
int ps_clock, ps_data;
Поскольку нет более смысла хранить объекты модулей vga, ps2 в main.cc, я их также перенесу в класс, и выполню их инициализацию там же. Тут же, рядом с инициализацией, выполним заполнение тестовыми данными
vga_mod = new Vvga;
ps2_mod = new Vps2;
 
ps_clock = 1;
ps_data = 1;
 
for (int i = 0; i < 4096; i += 2) {
 
vmemory[i] = i & 255; // Символ
vmemory[i+1] = 0x17; // Синий фон, серые буквы
}
И конечно, придется из-за изменений сделать большие замены в методе:
void tick25() {
 
ps2_mod->clock = 0; ps2_mod->eval();
vga_mod->clock = 0; vga_mod->eval();
 
// Чтение из видеопамяти
vga_mod->data = vmemory[ vga_mod->address ];
 
// Эмуляция PS/2 кнопки
kbd_pop(ps_clock, ps_data);
 
// Контроллер PS/2
ps2_mod->ps_clock = ps_clock;
ps2_mod->ps_data = ps_data;
if (ps2_mod->done) printf("%02x ", ps2_mod->data);
 
ps2_mod->clock = 1; ps2_mod->eval();
vga_mod->clock = 1; vga_mod->eval();
 
// Видеоконтроллер
vga(vga_mod->hs, vga_mod->vs, vga_mod->r*(128*65536) + vga_mod->b*(128*256) + vga_mod->g*128);
}
И сам main.cc выглядит теперь так:
#include <stdlib.h>
#include "obj_dir/Vvga.h"
#include "obj_dir/Vps2.h"
#include "app.cc"
 
int main(int argc, char **argv) {
 
Verilated::commandArgs(argc, argv);
App* app = new App();
 
while (app->main()) {
 
// 125k тактов 20 раз в секунду (2.5 Мгц)
for (int i = 0; i < 125000; i++) {
app->tick25();
}
 
if (Verilated::gotFinish()) break;
}
 
app->destroy();
return 0;
}

§ Модификация для вывода символов

Для начала я сделаю небольшую модификацию существующего кода vga.v для того, чтобы скорректировать вывод символов так, чтобы они вообще выводились.
Как я ранее рассказывал, теперь на одно знакоместо требуется 2 байта. Ну и придется в два раза увеличить память (с 4к до 8к), а значит, в адресную шину добавить +1 бит:
module vga
(
...
output reg [12:0] address,
...
);
А теперь, какие изменения необходимо сделать в коде always, чтобы получить результат:
case (xv[2:0])
 
3'h0: begin address <= xv[9:3]*2 + yv[8:4]*160; end
3'h1: begin address <= {1'b1, data, yv[3:0]}; end
3'h7: begin rmask <= data; end
 
endcase
И эти два небольших изменения уже покажут правильную картинку на экране, правда, пока что черно-белую. А теперь уберем выставление инверсного цвета:
wire pix = rmask[ ~xv[2:0] ];
Результатом, после всех модификации, будет вот такой экран:
screen3.png

§ Добавление цветности

Итак, пришло время для вычисления цветов. Теперь, раз мы собираемся делать 16 цветов, придется расширить количество цветов на выходе вдвое и сделать исходящие данные шире, чтобы получилось вывести нужную палитру.
module vga
(
input clock,
output [1:0] r,
output [1:0] g,
output [1:0] b,
output hs,
output vs,
output reg [12:0] address,
input [ 7:0] data
);
То есть, вместо одного бита, будет 2 бита r/g/b. Соответственно, теперь придется поменять формирование цвета:
assign {r, g, b} = visible & pix ? 6'b11_11_11 : 6'b00_00_00;
То есть, теперь будет назначаться белый цвет на выход. Подправим еще один момент в методе tick25():
int cl =
((vga_mod->r&1)*0x7F + (vga_mod->r&2)*0x40) * 65536 +
((vga_mod->g&1)*0x7F + (vga_mod->g&2)*0x40) * 256 +
((vga_mod->b&1)*0x7F + (vga_mod->b&2)*0x40);
 
if (cl == 0x808080) cl = 0xc0c0c0; // Цвет 7
if (cl == 0x7F7F7F) cl = 0x606060; // Цвет 8
 
vga(vga_mod->hs, vga_mod->vs, cl);
Если ранее выводился серый цвет при r=1,g=1,b=1, то теперь при r=11,g=11,b=11 будет выводиться реально белый цвет. Я сделал так, что в младших 7 битах цвета было значение r[0], а в старшем – r[1], своеобразное квантование сигнала. Теперь цвет будет на экране более корректным.
Специальные случаи для цвета 7 и 8, для более корректного их выведения на экран.
Теперь важный момент – это палитра, которая будет представлена 16 цветами и преобразована в 3 цвета по 2 бита:
wire [ 5:0] outcolor =
// RR GG BB
curcolor == 4'h0 ? 6'b00_00_00 : // Черный
curcolor == 4'h1 ? 6'b00_00_01 : // Синий
curcolor == 4'h2 ? 6'b00_01_00 : // Зеленый
curcolor == 4'h3 ? 6'b00_01_01 : // Бирюзовый
curcolor == 4'h4 ? 6'b01_00_00 : // Красный
curcolor == 4'h5 ? 6'b01_00_01 : // Пурпурный
curcolor == 4'h6 ? 6'b01_01_00 : // Коричневый
curcolor == 4'h7 ? 6'b10_10_10 : // Светло-серый
curcolor == 4'h8 ? 6'b01_01_01 : // Темно-серый
curcolor == 4'h9 ? 6'b00_00_11 : // Ярко-синий
curcolor == 4'hA ? 6'b00_11_00 : // Ярко-зеленый
curcolor == 4'hB ? 6'b00_11_11 : // Голубой
curcolor == 4'hC ? 6'b11_00_00 : // Ярко-красный
curcolor == 4'hD ? 6'b11_00_11 : // Розовый
curcolor == 4'hE ? 6'b11_11_00 : // Желтый
6'b11_11_11; // Белый
Провод outcolor будет принимать одно из 16 значений с помощью вот такой вот цепочки из тернарных операторов. Мне жаль, что в верилоге нельзя использовать для этой цели case, потому что его надо обязательно записать в always. Хотя, можно и в case все это запихнуть, но оставлю так.
В этом фрагмента кода цвет выбирается из провода curcolor, который будет принимать текущее значение цвета для точки. Для начала, необходимо этот цвет получить.
Внесем некоторые изменения теперь в блок always:
always @(posedge clock) begin
 
x <= xborder ? 1'b0 : x + 1'b1;
y <= yborder ? 1'b0 : (xborder ? y + 1'b1 : y);
 
/* verilator lint_off CASEINCOMPLETE */
case (xv[2:0])
 
// Запрос символа
3'h0: begin address <= xv[9:3]*2 + yv[8:4]*160; end
// Сохранить символ в tdata, запрос цвета (соседний байт)
3'h1: begin address[0] <= 1'b1; tdata <= data; end
// Запрос знакогенератора, сохранение цвета
3'h2: begin address <= {1'b1, tdata, yv[3:0]}; tdata <= data; end
// Новые значения для конвейера
3'h7: begin rmask <= data; rcolor <= tdata; end
 
endcase
 
end
Теперь объясню, что тут происходит.
Поскольку, добавился цвет, нужно поправить пару строк
wire [ 3:0] curcolor = pix ? rcolor[3:0] : {1'b0, rcolor[6:4]};
...
assign {r, g, b} = visible ? outcolor : 6'b00_00_00;
Теперь, если текущий рисуемый пиксель равен 1, то тогда достается цвет из младших битов атрибута цветов (rcolor), иначе из 3 битов в старшем ниббле.
screen3-1.png

§ Добавление мерцания

Перед тем, как добавить мерцание, я понял, что сделал ошибку в коде и должен исправить:
x <= xborder ? 1'b0 : x + 1'b1;
y <= xborder && yborder ? 1'b0 : (xborder ? y + 1'b1 : y);
То есть, y переносится в 0 тогда и только тогда, когда y=448 и x=799. Это важно, хотя и работало раньше.
Теперь же добавим новые регистры:
reg flash = 1'b1;
reg [ 3:0] ticker = 1'b0;
Здесь flash – это текущее состояние мерцания у экрана, оно меняется каждые 0.25 сек на противоположное, а ticker – счетчик кадров, для того, чтобы пересбросить flash = ~flash.
Сам же счетчик кадров программируем в always-секции:
if (xborder && yborder) begin
 
ticker <= ticker + 1'b1;
flash <= ticker ? flash : ~flash;
 
end
То есть, когда xborder и yborder равны 1, то это значит, что точка находится в самом крайнем углу внизу, и следующий такт перебросит (x,y) в (0,0). Это отсчет кадров.
Регистр ticker 4-х битный, он отсчитывает кадры с переполнением. Когда ticker становится равным 0, то flash меняется на противоположный. Если делается 70 кадров в секунду, то тогда 16/70 = 0.22 сек, за такое время перебрасывается flash.
Логика работы мерцания такова, что когда атрибут цвета (старший бит), установлен в 1, то тогда, в случае если flash стал равным 1, то для таких знакомест цвет переднего плана становится цветом фона.
wire enfore = ~(flash & rcolor[7]);
Здесь если enfore=0, то условие мерцания выполняется и весь символ становится цветом фона, а это делается так:
wire [ 3:0] curcolor = pix & enfore ? rcolor[3:0] : rcolor[6:4];
Просто к pix добавляем enfore. Мерцание работает! Все отлично.

§ Вывод курсора

Все, теперь финишная прямая. Единственное, что осталось сделать, это вывод курсора. Курсор опирается на мерцание, и обычно выводится как две линии по 8 пикселей. Положение курсора задается номером знакоместа от 0 до 1999. Номер 0 находится в самом верхнем левом углу, а номер 1999 в самом нижнем правом.
Вычисляется он так cursor = x + 80*y. Это, кстати, в точности совпадает с номером адреса, который устанавливается в память для чтения нового символа, потому я выведу его в отдельный провод:
wire [10:0] cplace = yv[8:4]*80 + xv[9:3];
И теперь можно замену в always сделать:
3'h0: begin address <= {cplace, 1'b0}; end
cplace сдвигается влево на 1 бит, что равнозначно умножению на 2. Добавим входящий провод cursor размерностью 11 бит, чтобы можно было адресовать от 0 до 1999.
module vga
(
input clock,
...
input [10:0] cursor
);
Этот параметр передается, например, от процессора или от контроллера, он задает то, где находится курсор в данный момент. Если мы хотим скрыть курсор, то надо установить cursor >= 2000, и тогда курсор исчезнет с экрана.
Вот теперь один из самых интересных моментов:
wire acursor = (cursor + 1 == cplace) & (yv[3:0] >= 4'hE);
Что значит этот код? Если курсор (прибавляем +1 к нему), равен номеру знакоместа, который сейчас рисуется, а также младшие 4 бита по Y равны либо 14, либо 15 – это две линии снизу, то тогда на проводе acursor будет 1, что значит, есть разрешение рисовать курсор.
Почему тут cursor+1? Опять-таки, из-за конвейера, который рисует символ до того, как он появится на экране. Поэтому, если x=40, то это считается первым символов, что не так на самом деле, ибо только начиная с x=48 начинается рисование знакоместа, и это значит, что cplace = 1 для x=48. Поэтому и прибавляется cursor+1.
Вновь сделаем правку кода:
wire [ 3:0] curcolor = (pix & enfore) | (acursor & flash) ? rcolor[3:0] : rcolor[6:4];
В котором добавится (acursor & flash), который будет выводить пиксель в том случае, если 1) сейчас рисуется курсор 2) и если flash=1. Интересный факт, что курсор мерцать будет в противоположную сторону от мерцания символа. Это сделано намеренно, чтобы, в случае нахождения курсора в мерцающей области, его можно было бы увидеть.
screen3-2.png
Вот и все, текстовый видеоадаптер готов.

§ Сохранение фреймов

Решил, что без этой темы никуда не обойтись. Дописал код, который сохраняет фреймы в файлы, если есть папка out.
void saveframe(const char* dir) {
 
char fn[256];
sprintf(fn, "%s/%08d.ppm", dir, frame_id);
 
FILE* fp = fopen(fn, "wb");
if (fp) {
 
fprintf(fp, "P6\n# Verilator\n640 400\n255\n");
for (int y = 0; y < 400; y++)
for (int x = 0; x < 640; x++) {
 
int cl = screen_buffer[2*(y*width + x)];
int vl = ((cl >> 16) & 255) + (cl & 0xFF00) + ((cl&255)<<16);
fwrite(&vl, 1, 3, fp);
}
 
fclose(fp);
}
 
frame_id++;
}
Необходима модификация метода vga:
if (_vs == 1 && vs == 0) { x = 0; y = 0; saveframe("out"); }
А также объявить frame_id = 0. Но это уже слишком просто и очевидно. И конечно же, нужно показать, как эти фреймы склеить в один файл mp4:
ffmpeg -framerate 70 -r 60 -i out/%08d.ppm \
-vf "scale=w=1280:h=800,pad=width=1920:height=1080:x=320:y=140:color=black" \
-sws_flags neighbor -sws_dither none -f mp4 -q:v 0 -vcodec mpeg4 -y record.mp4