§ Развертка

Наконец-то пришло время оставить написание статей по очевидным вещам по верилогу, и начать разрабатывать видеоадаптер VGA.
Для того, чтобы сделать код на верилоге, надо для начала, разобраться, как работает видеосигнал D-SUB. Первое — сигнал этот аналоговый, а не цифровой, то есть на выход R/G/B (красный-зеленый-синий) компонент будет подаваться разное напряжение. Чем больше - тем точка ярче. Это еще пришло из тех времен, когда не было плоских мониторов, да и сами мониторы были с электронно-лучевой трубкой, с электронным лучом, "гуляющим" по экрану, который отклонялся с помощью электромагнитного воздействия. Тема сложная, я тут ее описывать не буду, но поскольку сигнал VGA формируется именно по тем принципам, то значит, надо рассматривать именно с точки зрения ЭЛТ (электронно-лучевой трубки).
Как работает луч? Он идет слева-направо, сверху вниз. Сначала луч идет слева направо, потом доходит до края монитора и выключается, возвращается обратно какое-то время (горизонтальная развертка). И так — каждую строку. Далее, когда луч доходит до нижней части экрана, то снова выключается и возвращается назад (вертикальная развертка).
Так выглядит растактовка сигнала по ширине (и по высоте тоже)
_____________________________________/```````\ Позитивный синхросигнал
| Back  | Visible         | Front    | Sync  |
/`\_/`\_/`\_/`\_/`\_/`\_/`\_/`\_/`\_/`\_/`\_/` Тактовая частота
Вначале идет "задний порожек" (Back). В этот момент на RGB выходе должен быть строгий 0 (GND, минус). Потом идет видимая область, здесь за N тактов происходит отрисовка строки. После видимой области, идет "передний порожек" (Front), где RGB=0. Когда передний порожек заканчивается, включается синхро-сигнал. Если он позитивный, то ставится 1, если негативный - то ставится 0 (до этого он должен быть равным 1).
Ровно то же самое происходит по вертикали. Сначала идет задний порожек, потом область видимости и потом уже передний порожек и синхросигнал (негативный или позитивный).
Монитор, опираясь на длительности синхросигналов, а также на то, негативные они или позитивные, определяет частоту развертки и частоту генератора пикселей. То есть, для монитора нет отдельного тактового генератора, он догадывается сам по тому, как и с какой частотой работают синхросигналы.
На картинке я попытался изобразить, как выглядят области рисования на экране:
Там где синяя область - это область горизонтального синхросигнала, а там где зеленый - область вертикального синхросигнала. Черная область - эта та область, в которой выводится изображение на экран.

§ Видеорежим 640 x 400

Для начала, я скажу, что буду разрабатывать далее на "отладочной доске" (devboard, девборд, плата разработки и т.п.) с установленном на ней чипом Cyclone V. В целом, это хорошо, но можно и другие чипы использовать, ничего с того не станет.
Начнем с определения входов и выходов модуля vga.v:
1module vga(input clock, output r, output g, output b, output hs, output vs);
2...
3endmodule
Здесь я объявил провода r,g,b как однобитные, это сделано для упрощения, и только. На самом деле, их можно сделать и многобитными, насколько хватит точности в ЦАП (Цифро-аналоговом преобразователе).
hs, vs - это горизонтальный и вертикальный синхросигнал. Эти провода всегда будут однобитными.
Итак, сегодня я буду делать видеоадаптер, который генерирует разрешение 640 x 400 x 70 герц, где 70 герц - это 70 обновлений экрана в секунду, или так сказать, FPS еще это называется в современном компьютерном жаргоне.
Чтобы не смотреть и не лазить по 100 раз в интернете где-то, я скопировал таблицу с параметрами для видеорежимов. Итак, заглянув в таблицу, выгружаем параметры для нужного нам разрешения:
  • Видеорежим 640 x 400 x 70
  • Скорость пиксель-клока 25.14 мгц. Но я возьму 25.000 Мгц ровно, потому что его удобно делить на 4 от базовой частоты 100 мгц.
  • Горизонтальная развертка, задний порожек back — 48, видимая область — 640 пикселей, передний порожек front — 16, и синхронизация hsync — 96 (негативная)
  • Вертикальная развертка, задний порожек back — 35, видимая область — 400 пикселей, передний порожек front — 12, и синхронизация hsync — 2 (позитивная)
Итак, общая длина линии получается 16+640+48+96=800 пикселей, и общая высота (количество строк в общем) 12+400+35+2=449 строк.
Если умножить 800 x 449 и на 70 герц, то и получаем частоту 25.144 Мгц. Но такой частоты сложно добиться, потому я буду использовать ровно 25.000, что снизит ненамного количество обновлений в секунду до 69.6 кадров в секунду.

§ Видеофрейм

Теперь же создадим счетчики. Итак, в ширину получается 800 пикселей. Сколько нужно взять битов в регистр, чтобы туда поместился 800? Например в 8 бит вместится число до 255, а 9 бит - до 511, все еще недостаточно, а значит, надо взять 10 битный регистр, для того, чтобы хранить значение по X. Для Y хватит 9-битного числа, так как максимальное значение там будет 449, что меньше 511.
1reg [9:0] x = 10'b0; // 10-битное число (от 9 до 0 бита включительно)
2reg [8:0] y = 9'b0;  // 9-битное число
Регистры объявили, но теперь надо на них сделать счетчик:
1wire xborder = x == 10'd799;
2always @(posedge clock)
3    x <= xborder ? 1'b0 : x + 1'b1;
В данном коде вне блока always есть логика, которая сравнивает x с числом 799 (предпоследним числом перед 800), а в блоке always, в зависимости от того, какое число сейчас в x, записывается новый x.
  • Если x=799, то записывается x=0, после позитивного фронта clock
  • Если x<799, то пишется x=x+1
Тем самым образом у нас получается "пробег" от 0 до 799.
Но это только по X, а что делать по Y? Для этого используем другой блок always:
1wire yborder = y == 9'd448;
2always @(posedge clock)
3    y <= xborder && yborder ? 1'b0 : (xborder ? y + 1'b1 : y);
Как можно заметить, тут схема немного посложнее будет.
  • Когда y=448, x=799, то y=0 (так же, как и в x)
  • Когда y<448, то y прибавляется +1 только тогда, когда x=799, и не изменяется, если x<799.
Почему это сделано? Это все из-за того, что к Y добавляется +1 тогда и только тогда, когда X перемещается в начало строки. А это происходит именно тогда, когда X=799.
На данный момент мы смогли сделать так, чтобы луч перемещался по (X,Y) в пределах заданных ограничений по всему фрейму, который равен (800,449). Но, помимо этого, необходимо и обязательно, чтобы также работали синхросигналы hs, vs.
  • Горизонтальный синхроимпульс hs равен 1 на протяжении того времени, когда луч пробегает по "переднему порожеку" + "видимой области" + "заднему порожеку" и 0 во время синхросигнала (негативный)
  • Вертикальный синхроимпульс vs работает аналогично горизонтальному, но, поскольку он позитивный, то большую часть времени он 0, и во время синхросигнала становится 1.
Позитивные и негативные синхроимпульсы зависят от выбранного видеоразрешения.
Теперь, как будет выглядеть код?
1assign hs = x  < 48+640+16; // hs=1 когда луч находится в области порожеков+видимой
2assign vs = y >= 35+400+12; // vs=1 когда луч находится в области синхронизации
Здесь все, разве что только сделаю замечание, что 16+640+48 — это по итогу будет 32-х битное число, которое компилятором все равно отрежется до 10 битов при беззнаковом (важно) сравнении двух чисел.
Теперь осталось определить ту область, которая будет показывать r,g,b сигнал. Для выбранного видеоразрешения, эта область будет начинаться с x=48 и заканчиваться на 48+640-1=687. По y=35 и заканчиваться на 400+35-1=434:
1wire visible = x >= 48 && x < 48+640 && y >= 35 && y <= 35+400;
2assign {r, g, b} = visible ? 3'b111 : 3'b000;
И тем самым образом, можно теперь определить, какие пиксели можно показывать и какие нет.

§ Полный код модуля

Объединяя все вышесказанное, приведу полный рабочий код модуля:
1module vga(input clock, output r, output g, output b, output hs, output vs);
2
3reg [9:0] x = 10'b0;
4reg [8:0] y = 9'b0;
5
6wire xborder = x == 10'd799;
7wire yborder = y ==  9'd448;
8wire visible = x >= 48 && x < 48+640 && y >= 35 && y <= 35+400;
9
10// Горизонтальная и вертикальная синхронизация
11assign hs = x  < 48+640+16;
12assign vs = y >= 35+400+12;
13
14// Вывод серого экрана
15assign {r, g, b} = visible ? 3'b111 : 3'b000;
16
17always @(posedge clock) begin
18
19    x <= xborder ? 1'b0 : x + 1'b1;
20    y <= xborder && yborder ? 1'b0 : (xborder ? y + 1'b1 : y);
21
22end
23
24endmodule
И также схему его подключения на DE0:
1vga VGAUnit
2(
3    .clock      (clock_25),
4    .r          (VGA_R[3]), // Здесь VGA_R - 4х битный ЦАП
5    .g          (VGA_G[3]), // Потому выход присваивается старшему разряду
6    .b          (VGA_B[3]), // Чтобы был серый цвет
7    .hs         (VGA_HS),
8    .vs         (VGA_VS)
9);