§ Какие изменения
Сегодня я расширю возможности видеоадаптера, сделав его приблизительно равным стандартному текстовому режиму, который есть в MSDOS, например.- Сделаны все 256 символов CP866
- Будет добавлен цвет в знакоместа
- Мерцание символов
- Курсор
§ Устройство видеобуфера
Как видимо, видеобуфер придется увеличить в 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:1FILE* fp = fopen("font.bin", "rb"); 2fread(vmemory + 4096, 1, 4096, fp); 3fclose(fp);А сам vmemory добавлю в protected-область:
1protected: 2 ... 3 unsigned char vmemory[8192]; 4 ... 5 Vvga* vga_mod; 6 Vps2* ps2_mod; 7 ... 8 int ps_clock, ps_data;Поскольку нет более смысла хранить объекты модулей vga, ps2 в main.cc, я их также перенесу в класс, и выполню их инициализацию там же. Тут же, рядом с инициализацией, выполним заполнение тестовыми данными
1vga_mod = new Vvga; 2ps2_mod = new Vps2; 3 4ps_clock = 1; 5ps_data = 1; 6 7for (int i = 0; i < 4096; i += 2) { 8 9 vmemory[i] = i & 255; // Символ 10 vmemory[i+1] = 0x17; // Синий фон, серые буквы 11}И конечно, придется из-за изменений сделать большие замены в методе:
1void tick25() { 2 3 ps2_mod->clock = 0; ps2_mod->eval(); 4 vga_mod->clock = 0; vga_mod->eval(); 5 6 // Чтение из видеопамяти 7 vga_mod->data = vmemory[ vga_mod->address ]; 8 9 // Эмуляция PS/2 кнопки 10 kbd_pop(ps_clock, ps_data); 11 12 // Контроллер PS/2 13 ps2_mod->ps_clock = ps_clock; 14 ps2_mod->ps_data = ps_data; 15 if (ps2_mod->done) printf("%02x ", ps2_mod->data); 16 17 ps2_mod->clock = 1; ps2_mod->eval(); 18 vga_mod->clock = 1; vga_mod->eval(); 19 20 // Видеоконтроллер 21 vga(vga_mod->hs, vga_mod->vs, vga_mod->r*(128*65536) + vga_mod->b*(128*256) + vga_mod->g*128); 22}И сам main.cc выглядит теперь так:
1#include <stdlib.h> 2#include "obj_dir/Vvga.h" 3#include "obj_dir/Vps2.h" 4#include "app.cc" 5 6int main(int argc, char **argv) { 7 8 Verilated::commandArgs(argc, argv); 9 App* app = new App(); 10 11 while (app->main()) { 12 13 // 125k тактов 20 раз в секунду (2.5 Мгц) 14 for (int i = 0; i < 125000; i++) { 15 app->tick25(); 16 } 17 18 if (Verilated::gotFinish()) break; 19 } 20 21 app->destroy(); 22 return 0; 23}Я закреплю файл на php, с помощью которого я генерирую данные для font.bin и font.mif. Это может пригодится.
§ Модификация для вывода символов
Для начала я сделаю небольшую модификацию существующего кода vga.v для того, чтобы скорректировать вывод символов так, чтобы они вообще выводились.Как я ранее рассказывал, теперь на одно знакоместо требуется 2 байта. Ну и придется в два раза увеличить память (с 4к до 8к), а значит, в адресную шину добавить +1 бит:
1module vga 2( 3 ... 4 output reg [12:0] address, 5 ... 6);А теперь, какие изменения необходимо сделать в коде always, чтобы получить результат:
1case (xv[2:0]) 2 3 3'h0: begin address <= xv[9:3]*2 + yv[8:4]*160; end 4 3'h1: begin address <= {1'b1, data, yv[3:0]}; end 5 3'h7: begin rmask <= data; end 6 7endcase
- В такте 0 умножаем на 2 (то есть, это будет просто сдвиг влево), что будет означать, что на одно знакоместо будет 2 байта.
- В такте 1 меняем размерность data[6:0] (7 бит) на data (8 бит); также, убираем запись в tcolor — потому что уже не требуется
- И в такте 2 убирается запись в rcolor. Так что два регистра rcolor / tcolor можно уже удалить из модуля.
1wire pix = rmask[ ~xv[2:0] ];Результатом, после всех модификации, будет вот такой экран:
§ Добавление цветности
Итак, пришло время для вычисления цветов. Теперь, раз мы собираемся делать 16 цветов, придется расширить количество цветов на выходе вдвое и сделать исходящие данные шире, чтобы получилось вывести нужную палитру.1module vga 2( 3 input clock, 4 output [1:0] r, 5 output [1:0] g, 6 output [1:0] b, 7 output hs, 8 output vs, 9 output reg [12:0] address, 10 input [ 7:0] data 11);То есть, вместо одного бита, будет 2 бита r/g/b. Соответственно, теперь придется поменять формирование цвета:
1assign {r, g, b} = visible & pix ? 6'b11_11_11 : 6'b00_00_00;То есть, теперь будет назначаться белый цвет на выход. Подправим еще один момент в методе tick25():
1int cl = 2 ((vga_mod->r&1)*0x7F + (vga_mod->r&2)*0x40) * 65536 + 3 ((vga_mod->g&1)*0x7F + (vga_mod->g&2)*0x40) * 256 + 4 ((vga_mod->b&1)*0x7F + (vga_mod->b&2)*0x40); 5 6if (cl == 0x808080) cl = 0xc0c0c0; // Цвет 7 7if (cl == 0x7F7F7F) cl = 0x606060; // Цвет 8 8 9vga(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 бита:
1wire [ 5:0] outcolor = 2 // RR GG BB 3 curcolor == 4'h0 ? 6'b00_00_00 : // Черный 4 curcolor == 4'h1 ? 6'b00_00_01 : // Синий 5 curcolor == 4'h2 ? 6'b00_01_00 : // Зеленый 6 curcolor == 4'h3 ? 6'b00_01_01 : // Бирюзовый 7 curcolor == 4'h4 ? 6'b01_00_00 : // Красный 8 curcolor == 4'h5 ? 6'b01_00_01 : // Пурпурный 9 curcolor == 4'h6 ? 6'b01_01_00 : // Коричневый 10 curcolor == 4'h7 ? 6'b10_10_10 : // Светло-серый 11 curcolor == 4'h8 ? 6'b01_01_01 : // Темно-серый 12 curcolor == 4'h9 ? 6'b00_00_11 : // Ярко-синий 13 curcolor == 4'hA ? 6'b00_11_00 : // Ярко-зеленый 14 curcolor == 4'hB ? 6'b00_11_11 : // Голубой 15 curcolor == 4'hC ? 6'b11_00_00 : // Ярко-красный 16 curcolor == 4'hD ? 6'b11_00_11 : // Розовый 17 curcolor == 4'hE ? 6'b11_11_00 : // Желтый 18 6'b11_11_11; // БелыйПровод outcolor будет принимать одно из 16 значений с помощью вот такой вот цепочки из тернарных операторов. Мне жаль, что в верилоге нельзя использовать для этой цели case, потому что его надо обязательно записать в always. Хотя, можно и в case все это запихнуть, но оставлю так.
В этом фрагмента кода цвет выбирается из провода curcolor, который будет принимать текущее значение цвета для точки. Для начала, необходимо этот цвет получить.
Внесем некоторые изменения теперь в блок always:
1 2always @(posedge clock) begin 3 4 x <= xborder ? 1'b0 : x + 1'b1; 5 y <= yborder ? 1'b0 : (xborder ? y + 1'b1 : y); 6 7 /* verilator lint_off CASEINCOMPLETE */ 8 case (xv[2:0]) 9 10 // Запрос символа 11 3'h0: begin address <= xv[9:3]*2 + yv[8:4]*160; end 12 // Сохранить символ в tdata, запрос цвета (соседний байт) 13 3'h1: begin address[0] <= 1'b1; tdata <= data; end 14 // Запрос знакогенератора, сохранение цвета 15 3'h2: begin address <= {1'b1, tdata, yv[3:0]}; tdata <= data; end 16 // Новые значения для конвейера 17 3'h7: begin rmask <= data; rcolor <= tdata; end 18 19 endcase 20 21endТеперь объясню, что тут происходит.
- Такт 0: ничего не меняется, устанавливается необходимый адрес для извлечения номера символа, этот адрес всегда четный
- Такт 1: устанавливается нечетный адрес, чтобы извлечь оттуда информацию о цвете знакогенератора, а только что полученные данные (номер символа), сохраняется во временный регистр tdata
- Такт 2: сохраненный ранее в tdata номер символа записывается как часть адреса в знакогенератор, а полученный из памяти цвет (data) теперь сохраняется в tdata
- Такт 7: ранее сохраненный в tdata цвет теперь переходит в тот цвет, который будет отображаться на следующем такте (rcolor), и полученное значение data из знакогенератора будет теперь в регистре rmask. Оба эти регистра не будут меняться 8 тактов, то есть, они пошли в конвейер на отрисовку
1wire [ 3:0] curcolor = pix ? rcolor[3:0] : {1'b0, rcolor[6:4]}; 2... 3assign {r, g, b} = visible ? outcolor : 6'b00_00_00;Теперь, если текущий рисуемый пиксель равен 1, то тогда достается цвет из младших битов атрибута цветов (rcolor), иначе из 3 битов в старшем ниббле. Так теперь выглядит цветная версия:
§ Добавление мерцания
Перед тем, как добавить мерцание, я понял, что сделал ошибку в коде и должен исправить:1x <= xborder ? 1'b0 : x + 1'b1; 2y <= xborder && yborder ? 1'b0 : (xborder ? y + 1'b1 : y);То есть, y переносится в 0 тогда и только тогда, когда y=448 и x=799. Это важно, хотя и работало раньше.
Теперь же добавим новые регистры:
1reg flash = 1'b1; 2reg [ 3:0] ticker = 1'b0;Здесь flash - это текущее состояние мерцания у экрана, оно меняется каждые 0.25 сек на противоположное, а ticker - счетчик кадров, для того, чтобы пересбросить flash = ~flash.
Сам же счетчик кадров программируем в always-секции:
1if (xborder && yborder) begin 2 3 ticker <= ticker + 1'b1; 4 flash <= ticker ? flash : ~flash; 5 6endТо есть, когда xborder и yborder равны 1, то это значит, что точка находится в самом крайнем углу внизу, и следующий такт перебросит (x,y) в (0,0). Это отсчет кадров.
Регистр ticker 4-х битный, он отсчитывает кадры с переполнением. Когда ticker становится равным 0, то flash меняется на противоположный. Если делается 70 кадров в секунду, то тогда 16/70 = 0.22 сек, за такое время перебрасывается flash.
Логика работы мерцания такова, что когда атрибут цвета (старший бит), установлен в 1, то тогда, в случае если flash стал равным 1, то для таких знакомест цвет переднего плана становится цветом фона.
1wire enfore = ~(flash & rcolor[7]);Здесь если enfore=0, то условие мерцания выполняется и весь символ становится цветом фона, а это делается так:
1wire [ 3:0] curcolor = pix & enfore ? rcolor[3:0] : rcolor[6:4];Просто к pix добавляем enfore. Мерцание работает! Все отлично.
§ Вывод курсора
Все, теперь финишная прямая. Единственное, что осталось сделать, это вывод курсора. Курсор опирается на мерцание, и обычно выводится как две линии по 8 пикселей. Положение курсора задается номером знакоместа от 0 до 1999. Номер 0 находится в самом верхнем левом углу, а номер 1999 в самом нижнем правом.Вычисляется он так
cursor = x + 80*y
. Это, кстати, в точности совпадает с номером адреса, который устанавливается в память для чтения нового символа, потому я выведу его в отдельный провод:1wire [10:0] cplace = yv[8:4]*80 + xv[9:3];И теперь можно замену в always сделать:
13'h0: begin address <= {cplace, 1'b0}; endcplace сдвигается влево на 1 бит, что равнозначно умножению на 2. Добавим входящий провод cursor размерностью 11 бит, чтобы можно было адресовать от 0 до 1999.
1module vga 2( 3 input clock, 4 ... 5 input [10:0] cursor 6);Этот параметр передается, например, от процессора или от контроллера, он задает то, где находится курсор в данный момент. Если мы хотим скрыть курсор, то надо установить cursor >= 2000, и тогда курсор исчезнет с экрана.
Вот теперь один из самых интересных моментов:
1wire 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.
Вновь сделаем правку кода:
1wire [ 3:0] curcolor = (pix & enfore) | (acursor & flash) ? rcolor[3:0] : rcolor[6:4];В котором добавится
(acursor & flash)
, который будет выводить пиксель в том случае, если 1) сейчас рисуется курсор 2) и если flash=1. Интересный факт, что курсор мерцать будет в противоположную сторону от мерцания символа. Это сделано намеренно, чтобы, в случае нахождения курсора в мерцающей области, его можно было бы увидеть.Вот и все, текстовый видеоадаптер готов.
§ Сохранение фреймов
Решил, что без этой темы никуда не обойтись. Дописал код, который сохраняет фреймы в файлы, если есть папкаout
.1void saveframe(const char* dir) { 2 3 char fn[256]; 4 sprintf(fn, "%s/%08d.ppm", dir, frame_id); 5 6 FILE* fp = fopen(fn, "wb"); 7 if (fp) { 8 9 fprintf(fp, "P6\n# Verilator\n640 400\n255\n"); 10 for (int y = 0; y < 400; y++) 11 for (int x = 0; x < 640; x++) { 12 13 int cl = screen_buffer[2*(y*width + x)]; 14 int vl = ((cl >> 16) & 255) + (cl & 0xFF00) + ((cl&255)<<16); 15 fwrite(&vl, 1, 3, fp); 16 } 17 18 fclose(fp); 19 } 20 21 frame_id++; 22}Необходима модификация метода vga:
1if (_vs == 1 && vs == 0) { x = 0; y = 0; saveframe("out"); }А также объявить frame_id = 0. Но это уже слишком просто и очевидно. И конечно же, нужно показать, как эти фреймы склеить в один файл mp4:
1ffmpeg -framerate 70 -r 60 -i out/%08d.ppm \ 2 -vf "scale=w=1280:h=800,pad=width=1920:height=1080:x=320:y=140:color=black" \ 3 -sws_flags neighbor -sws_dither none -f mp4 -q:v 0 -vcodec mpeg4 -y record.mp4Итоговый файл с проектом находится тут. Также в файле находится папка de0 для тестового синтеза в реальной схеме.