Оглавление
- Принцип работы
- Назначение пинов
- Сигналы синхронизации
- Объявление регистров
- Функционирование
- Обработка одного символа
§ Принцип работы
Видеоконтроллер ВГ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.