Лисья Нора

Оглавление


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

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

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

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

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

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

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

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

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

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

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

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