Оглавление


§ Принцип работы

Видеоконтроллер ВГ75 я знаю очень плохо, поэтому могу ли только в каком-то смысле пока что предполагать принцип его работы. Основная характеристика видеорежима необычна. Дело в том, что один символ на экране занимает по ширине 6 пикселей, по высоте — может варьироваться, но в основном это 8 пикселей на символ и 2 пикселя на междустрочный интервал, что составляет 10 пикселей по высоте, один символ. Обычно в этих 2 линиях располагается курсор. Знакогенератор в ПЗУ хранит символы в виде 8 битных значений, но реально используется лишь только 6 младших битов оттуда из-за ширины в 6 пикселей.
Еще одной интересной особенностью видеоадаптера является то, что он кодирует лишь 7 бит из знакогенератора, а 8-й бит используется для определенных целей, для задания атрибутов. Но до конца я не разобрался, как это работает.
В случае использования видеоразрешения 640 x 400, учитывая, что число столбцов составляет до 80, и строк в среднем 30, то ширина 80*6 = 480 пикселей и высота 30*10 = 300 пикселей, составляет 480x300, получаем в итоге бордюр черного цвета с расположенными в центре символами. Количество строк и столбцов в ВГ75 тоже программируется, как и старт видеопамяти, что отличает его от того же Спектрума, который имеет жестко заданное положение видео области.
Сегодня я лишь расскажу о программировании видеоконтроллера в верилоге.

§ Назначение пинов

Любой модуль имеет пины на вход, выход и, иногда, двунаправленные. Я не использую почти никогда двунаправленные.
1module vg75
2(
3    input               clock,          // 25 Мгц тактовый генератор
4    input               reset_n,        // Сброс чипа на =0
5    output reg          r,
6    output reg          g,
7    output reg          b,
8    output              hs,
9    output              vs,
10    output reg [15:0]   address,        // Указатель на адрес в памяти
11    output reg [10:0]   font_address,   // Указатель на FONT-ROM
12    input       [7:0]   in,             // Считывание из памяти
13    input       [7:0]   font_in         // Считывание из знакогенератора
14);
Из-за того что режим работы видеоадаптера черно-белый, то здесь не используется градация у пинов r,g,b, да и вообще даже они особо то не нужны, можно было бы объединить их в 1 провод, но делать я так не стану, поскольку цвета мне потом придется использовать для определенных целей, касательно уже реального воплощения на плисе и выдаче на современные мониторы. Пины hs,vs являются синхронизирующими.
16-битный адрес используется для извлечения кодов символов, причем они могут быть извлечены из любой области памяти. Соответствующие данные приходят на пин in, но есть некоторое отличие от реальной микросхемы, в которой все намного сложнее. Микросхема ВГ75 использует контроллер прямого доступа к памяти (ПДП), которая выполнена в виде отдельной микросхемы и читает по N байт за раз, после чего включает процессор и по этой причине процессор работает крайне нестабильно, будучи постоянно прерываемым видеоадаптером.
Адрес 11-битный font-address указывает на знакогенератор объемом 2кб. 3 бита кодируют номер строки в определенном знаке, а 8 бит для самих 256 знаков. Но, по правде, ВГ75 работает лишь с 7-битными.

§ Сигналы синхронизации

Развернуто рассказывать о том, как именно работает VGA-сигнал, нет смысла, поскольку об этом я уже говорил много раз в других своих статьях.
1localparam
2    hz_back    = 48,  vt_back    = 35,
3    hz_visible = 640, vt_visible = 400,
4    hz_front   = 16,  vt_front   = 12,
5    hz_sync    = 96,  vt_sync    = 2,
6    hz_whole   = 800, vt_whole   = 449;
Объявляются константы для видеоразрешения 640 x 400, что видно из hz_visible и vt_visible. Помимо этих таймингов, есть различного рода "порожеки", в 48 пикселей перед видимой областью по горизонтали, 16 после видимой области, и 96 тактов на сигнал синхронизации. И также для вертикальной области.
1assign hs = x  < (hz_back + hz_visible + hz_front); // NEG.
2assign vs = y >= (vt_back + vt_visible + vt_front); // POS.
В зависимости от выбранного режима видео, сигналы синхронизации разные. В конце каждой строки (которая в целом составляет 800 тактов), сигнал hs опускается в 0 ровно на 96 тактов. Так же это и для полного кадра, количество строк в котором составляет 499, но там vs равен 1 в последних 2-х строчках одного кадра.
Всего кадров - примерно 60 в секунду.
1wire xmax = (x == hz_whole - 1);
2wire ymax = (y == vt_whole - 1);
3wire vis  = (x >= hz_back && x < hz_visible + hz_back && y >= vt_back && y < vt_visible + vt_back);
Объявляю провода, которые необходимы будут далее для вычисления границ строки и кадра в целом, а также vis — области визуального отображения, и если vis=1, то это значит, что луч сейчас идет в данный момент по части экрана, которое мы увидим.

§ Объявление регистров

Для функционирования этого видеоадаптера требуется несколько больше регистров, чем обычно я использую для других адаптеров, это связано с тем, что ширина и высота символов не кратна степени 2, то есть не 8 и не 16 по ширине и высоте.
1reg  [10:0] x    = 0;
2reg  [10:0] y    = 0;
3reg  [ 2:0] cnt_xfine;  // 0..7
4reg  [ 3:0] cnt_yfine;  // 0..9
5reg  [ 6:0] cnt_x;      // 0..79
6reg  [ 5:0] cnt_y;      // 0..29
7reg         flash;      // Состояние курсора
8reg  [ 5:0] frame;      // Количество фреймов
9reg  [ 7:0] mask;       // Строка знакоместа
В регистрах x, y хранится текущее положение луча на мониторе, в cnt_xfine, cnt_yfine — положение луча внутри одного знакоместа, а cnt_x, cnt_y — номер рисуемого в данный момент столбца и строки (символа) соответственно.
Регистр flash указывает на то, показан ли сейчас в данный момент курсор или нет, а frame необходим для отсчета количества фреймов для того чтобы перебрасывать flash из одного состояния в другое.
Регистр mask нужен для хранения временного значения битовой маски строки у знакоместа, которая будет прочитана наперед. То есть, видеоадаптер в течении 6 тактов прочитывает номер символа, потом читает маску и на 6-м такте пишет следующее ее значение, чтобы с 0 до 5 пикселя начать ее отрисовывать.
1reg  [ 6:0] cursor_x;
2reg  [ 5:0] cursor_y;
3reg  [15:0] base_address;
4reg  [ 2:0] sym_height;
5reg  [ 1:0] sym_gap;
Помимо регистров оперативного функционирования, которые непосредственно требуются во время рисования на экране, существуют также конфигурационные регистры. Здесь cursor_x и cursor_y — текущее положение курсора на экране, base_address — базовый адрес (нижняя граница памяти), откуда происходит чтение символьных данных. sym_height — высота символа от 1 до 8 (т.е. 0..7), и sym_gap — количество строк от 0 до 3, которые пропускаются после рисования последней строки символа.
К слову говоря:
1wire [ 7:0] cursor_area = (cursor_x == cnt_x && cursor_y == cnt_y && flash && cnt_yfine == (sym_height + 1)) ? 8'hFF : 8'h00;
Именно так вычисляется курсор. Проверяется что текущий символ находится в области рисования курсора, также проверяется что строка находится на линии, следующей за символом, и конечно, если курсор показывается (flash).

§ Функционирование

После объявления всех регистров и прочих проводов, пришло время перейти к самой интересной части, это написанию синхронной логики. Начнем с самого очевидного, это сброса определенных конфигурационных регистров в начальное состояние.
1always @(negedge clock)
2if (reset_n == 0) begin
3
4    base_address <= 16'hE6A0; // 0xF000 - 2400 = 0xE6A0
5    sym_height   <= 7;        // 8 px высота символа
6    sym_gap      <= 2;        // 2 px междустрочный интервал
7    cursor_x     <= 0;
8    cursor_y     <= 0;
9
10end
При наличии сигнала reset_n=0, на негативном фронте (здесь это я сделал намеренно так), сбрасываются следующие параметры.
  • Базовый адрес видеопамяти. Он находится в области RAM обычно и занимает 2400 (80 x 30) символов
  • Высота символа 7 (на самом деле +1 = 8)
  • Междустрочный интервал 2
  • И положение курсора на (0, 0)
Без сброса этих параметров видеопроцессор будет работать корректно лишь если его настроить внешним способом, но пока что такой логики в данный момент тут не предусмотрено.
Начнем с простых счетчиков, которые занимаются отсчетом количества тактов в строке, и количества строк.
1x <= xmax ?         0 : x + 1;
2y <= xmax ? (ymax ? 0 : y + 1) : y;
Вот этот код эквивалентен такому коду на QBasic:
1WHILE 1
2FOR y = 0 TO ymax
3FOR x = 0 TO xmax
4...
5NEXT
6NEXT
7WEND
Но в отличии от QBasic, тут не синхронная логика, то есть значение x,y доступно везде по всей программе. Чтобы запрограммировать мигание курсора, достаточно сделать такие счетчики:
1if (xmax && ymax) begin
2
3    frame <= (frame == 29) ? 0 : frame + 1;
4    if (frame == 29) flash <= ~flash;
5
6end
В конце каждого фрейма будет срабатывать счетчик frame, отсчитывающий от 0 до 29, и на 29-м кадре (ровно 0.5 с), переключать flash на противоположное состояние.
Теперь самое важное, это запись точки на экран.
1{r, g, b} <= vis & mask[5 - cnt_xfine] ? 3'b111 : 3'b000;
Белая точка устанавливается тогда, когда:
  • Луч сейчас находится в визуальной области
  • Соответствующий бит из маски знакоместа сейчас есть

§ Обработка одного символа

Ниже приведен код для считывания информации о следующей битовой маске.
1if (x >= hz_back + 74 && x < hz_back + 79 + (80*6) && y >= vt_back + 50 && cnt_y < 30) begin
2
3    // 6 пикселей по ширине
4    cnt_xfine <= cnt_xfine + 1;
5
6    case (cnt_xfine)
7    0: begin address      <= cnt_x + (80*cnt_y) + base_address; end
8    1: begin font_address <= {in, cnt_yfine[2:0]}; end
9    5: begin
10
11        mask      <= (cnt_yfine > sym_height ? cursor_area : font_in);
12        cnt_xfine <= 0;
13        cnt_x     <= cnt_x + 1;
14
15    end
16    endcase
17
18end
Здесь реализован счетчик cnt_xfine, который меняется от 0 до 5, и в конце 5-го цикла увеличивает cnt_x (номер знакоместа по X) на +1.
Старт рисования начинается не с 80-й строки видимой области хода луча, а с 74, на 6 пикселей раньше по той причине, что перед тем, как загрузить следующую битмаску, потребуется 6 тактов, и когда эти 6 тактов пройдут, луч будет на положении 80 как раз и начнет эту битмаску рисовать. Так что тут имеет место "предвыборка".
  • На такте 0 устанавливается адрес в памяти, который начинается с base_address, и вычисляется в зависимости от счетчиков. Здесь вычисляется жестко заданная строка, хотя можно было сделать и счетчиком, но я сделал так.
  • Такт 1 вычисляет адрес битмаски в адресе указателя шрифтов, причем если область находится за шрифтом высотой sym_height (обычно 8), то в знакоместо может попасть значение курсора
  • На такте 5, последнем, защелкивается новая битмаска, сбрасывается счетчик cnt_xfine в 0 и переходит к следующему знакоместу
Интересное замечание почему x < hz_back + 79, а не +80, это связано с тем что в {r, g, b} попадает значение из предыдущего такта, так что последним записанным значением цвета будет именно в позиции +80, и на этом же такте mask будет очищен, что не дает появляться дополнительной лишней линии.
1...
2else begin
3
4    cnt_xfine <= 0;
5    cnt_x     <= 0;
6    mask      <= 0;
7
8    if (xmax) begin
9
10        if (y >= vt_back + 50 && cnt_y < 30) begin
11
12            cnt_yfine <= (cnt_yfine == (sym_height + sym_gap) ? 0 : cnt_yfine + 1);
13            if (cnt_yfine == (sym_height + sym_gap)) cnt_y <= cnt_y + 1;
14
15        end
16
17        if (ymax) begin cnt_y <= 0; cnt_yfine <= 0; end
18
19    end
20
21end
Все области рисования экрана происходит следующее.
  • Сбрасываются все оперативные счетчики ни линии (cnt_xfine, cnt_x, mask), что не позволяет более рисовать на экране остатки символов и ожидает следующей строки, когда эти счетчики заработают снова
  • При достижении конца линии (xmax), проверяется, что линия эта находится в области рисования экрана, и если это так, то запускаются счетчики вертикальные (cnt_yfine и cnt_y). Первый отсчитывает от 0 до sym_height + sym_gap, который обычно составляет 7+2=9 (то есть от 0 до 9), что равно высоте одного знакоместа, а второй, при достижении высоты 9 (sym_height + sym_gap) будет считать уже знакоместо от 0 до 29 (можно будет высоту потом задать)
  • При достижении конца фрейма сбрасываются вертикальные счетчики cnt_y и cnt_yfine.
На этом простейший видеоадаптер для Радио86 пока что готов. Но у него нет возможности его программирования, а это уже отдельный разговор.