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

Сегодня я расширю возможности видеоадаптера, сделав его приблизительно равным стандартному текстовому режиму, который есть в 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:
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;
}
Я закреплю файл на php, с помощью которого я генерирую данные для font.bin и font.mif. Это может пригодится.

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

Для начала я сделаю небольшую модификацию существующего кода 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
  • В такте 0 умножаем на 2 (то есть, это будет просто сдвиг влево), что будет означать, что на одно знакоместо будет 2 байта.
  • В такте 1 меняем размерность data[6:0] (7 бит) на data (8 бит); также, убираем запись в tcolor — потому что уже не требуется
  • И в такте 2 убирается запись в rcolor. Так что два регистра rcolor / tcolor можно уже удалить из модуля.
И эти два небольших изменения уже покажут правильную картинку на экране, правда, пока что черно-белую. А теперь уберем выставление инверсного цвета:
wire pix = rmask[ ~xv[2:0] ];
Результатом, после всех модификации, будет вот такой экран:

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

Итак, пришло время для вычисления цветов. Теперь, раз мы собираемся делать 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
Теперь объясню, что тут происходит.
  • Такт 0: ничего не меняется, устанавливается необходимый адрес для извлечения номера символа, этот адрес всегда четный
  • Такт 1: устанавливается нечетный адрес, чтобы извлечь оттуда информацию о цвете знакогенератора, а только что полученные данные (номер символа), сохраняется во временный регистр tdata
  • Такт 2: сохраненный ранее в tdata номер символа записывается как часть адреса в знакогенератор, а полученный из памяти цвет (data) теперь сохраняется в tdata
  • Такт 7: ранее сохраненный в tdata цвет теперь переходит в тот цвет, который будет отображаться на следующем такте (rcolor), и полученное значение data из знакогенератора будет теперь в регистре rmask. Оба эти регистра не будут меняться 8 тактов, то есть, они пошли в конвейер на отрисовку
Поскольку, добавился цвет, нужно поправить пару строк
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 битов в старшем ниббле. Так теперь выглядит цветная версия:

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

Перед тем, как добавить мерцание, я понял, что сделал ошибку в коде и должен исправить:
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. Интересный факт, что курсор мерцать будет в противоположную сторону от мерцания символа. Это сделано намеренно, чтобы, в случае нахождения курсора в мерцающей области, его можно было бы увидеть.
Вот и все, текстовый видеоадаптер готов.

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

Решил, что без этой темы никуда не обойтись. Дописал код, который сохраняет фреймы в файлы, если есть папка 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
Итоговый файл с проектом находится тут. Также в файле находится папка de0 для тестового синтеза в реальной схеме.

25 апр, 2022
© 2007-2022 Лажовая вещь ползла