Учим iCEstick передавать видео-сигнал по VGA

5 марта 2018

Одно из традиционных развлечений с FPGA заключается в генерации видео-сигнала для VGA-мониторов. В этой заметке будет рассмотрено решение этой задачи на примере платы iCEstick и открытого стека разработки под нее в лице проекта IceStorm. Если у вас нет iCEstick, но есть другая плата на базе FPGA серии ICE40 от Lattice, например, TinyFPGA B2 или Nandland Go Board, они тоже подойдут и потребуют внесения минимальных изменений в коде проекта. Также сгодятся платы на базе других FPGA, однако они потребуют внесения более существенных изменений в код, особенно в части, касающейся PLL. Кроме того, потребуется установка соответствующего проприетарного ПО, например, Quartus для FPGA от Intel / Altera или Vivado для устройств производства Xilinx.

Про железо

Полагаю, VGA-разъемы все видели. Но не все знают, какой из 15-и пинов для чего нужен. Ответить на этот вопрос поможет следующая иллюстрация (которая была позаимствована мной отсюда):

Распиновка VGA-разъема

GND — это, понятно, земля. H-SYNC и V-SYNC представляют собой некие синхронизирующие сигналы, которые мы более подробно рассмотрим далее. Три пина используются для передачи цвета в RGB. Например, чем выше напряжение на пине 1, тем больше красной компоненты имеет текущий пиксель. Другими словами, сигнал передается не в цифровом виде, а в аналоговом. Как видите, большинство пинов в разъеме оказались не очень-то используемыми. Мой внутренний минималист негодует!

Из коробки FPGA не очень-то дружат с аналоговыми сигналами. К счастью, генерировать аналоговый сигнал из цифрового мы научились в заметке Генерация синусоидального сигнала, а следовательно и звука, на FPGA. То есть, все, что нам понадобится для решения проблемы — это VGA-разъем, немного резисторов, подходящий операционный усилитель, например, четырехканальный MCP6044 (даташит [PDF]), и свободный вечер. Владельцам платы Nandland Go Board так и вовсе ничего не придется паять, так как плата имеет все необходимое прямо на борту. Если же вы являетесь счастливым обладателем TinyFPGA B2, вас может заинтересовать соответствующая плата расширений, также имеющая VGA-разъем.

Впрочем, я пошел другим путем, и купил готовую плату PmodVGA производства Digilent. В России ее оказалось быстрее и дешевле всего купить в Чип-и-Дипе, хотя доставка и заняла пару недель. Схема платы доступна здесь [PDF].

Использованный в ней самодельный ЦАП работает на знакомом нам принципе — та же R-2R лестница, а вместо операционного усилителя использован неинвертирующий приемопередатчик 74ALVC245. Плата позволяет генерировать 12-и битный цвет, по 4 бита на красный, зеленый и синий. При этом удобно, что все входные пины подтянуты к земле на случай, если мы решим использовать только часть из них.

Мне вот как раз пришлось ограничиться 8-и битной палитрой R3G3B2, так как интересующая меня конфигурация, написанная в лоб, иначе не вмещалась в iCEstick. Напомню, что использованный в ней чип ICE40HX1K имеет всего лишь 1280 логических ячеек (LUT). Уходить же в оптимизацию кода на SystemVerilog мне вот прямо сейчас не особо хотелось. Кстати, TinyFPGA B2 работает на базе чипа ICE40LP8K с 7680 логическими ячейками, поэтому под эту плату можно написать и более интересную конфигурацию.

Плата iCEstick, соединенная с PmodVGA:

Плата iCEstick соединенная с PmodVGA

Это что касалось чисто железной части. Теперь же попробуем понять часть, написанную на SystemVerilog.

Настройка PLL

Прежде всего стоит отметить, что цвета пикселей передаются с некоторой частотой, которая зависит от выбранного разрешения экрана и фреймов в секунду. Точные цифры можно посмотреть, к примеру, здесь и здесь. Я решил рисовать картинку 800x600, 56 фреймов в секунду («почти 60 FPS»). Для этого нужно, чтобы пиксели передавались с частотой 38.1 МГц. Но в плате iCEstick используется кварцевый резонатор всего лишь на 12 МГц. Проблемка.

К счастью, чипы серии ICE40 (как и многие другие FPGA и микроконтроллеры) имеют встроенное устройство под названием phase-locked loop или PLL. Это настраиваемое устройство, позволяющее получить из внешнего сигнала некой известной частоты сигнал какой-то другой частоты, больше или меньше частоты исходного. Параметры PLL называются DIVF, DIVQ и DIFR. Если REF — это частота входного сигнала, то частоту выходного можно определить по формуле:

out = (ref * (divf + 1)) / ((2 ** divq) * (divr + 1))

Для быстрого подбора значений параметров в пакет IceStorm входит утилита icepll. Пример ее использования:

icepll -i 12 -o 38.1

Пример вывода:

F_PLLIN:    12.000 MHz (given)
F_PLLOUT:   38.100 MHz (requested)
F_PLLOUT:   38.250 MHz (achieved)

FEEDBACK: SIMPLE
F_PFD:   12.000 MHz
F_VCO:  612.000 MHz

DIVR:  0 (4'b0000)
DIVF: 50 (7'b0110010)
DIVQ:  4 (3'b100)

FILTER_RANGE: 1 (3'b001)

Теперь достаточно указать эти параметры в нашем коде таким образом:

SB_PLL40_CORE #(
    .FEEDBACK_PATH("SIMPLE"),
    .PLLOUT_SELECT("GENCLK"),
    .DIVR(4'b0000),
    .DIVF(7'b0110010),
    .DIVQ(3'b100),
    .FILTER_RANGE(3'b001),
) uut (
    .REFERENCECLK(clkin),
    .PLLOUTCORE(clk),
    .LOCK(D5), // keep this!
    .RESETB(1'b1),
    .BYPASS(1'b0)
);

… и мы получаем сигнал clk с частотой 38.25 МГц, что достаточно близко к желаемой. Максимальная же частота, на которой могут работать FPGA серии ICE40, составляет 275 МГц. Стоит однако отметить, что на таких частотах начинает играть большую роль ограничение на скорость распространения сигнала (скорость света). Чтобы проверить, будет ли ваша конфигурация корректно работать на заданной частоте, можно воспользоваться утилитой icetime:

icetime -c 38.25 main.asc

Пример вывода:

...
// Creating timing netlist..
// Timing estimate: 22.72 ns (44.02 MHz)
// Checking 26.14 ns (38.25 MHz) clock constraint: PASSED.

Для корректной работы на высоких частотах используют специальную технику, пайплайнинг, но это уже сильно выходит за рамки данной статьи. Собственно, это еще одна причина, помимо моей лени и малого количества LUT в ICE40HX1K, почему в этой статье рассматривается лишь видео 800x600 с 8-и битной палитрой.

VGA-сигнал

Итак, мы все выяснили про частоту сигнала. Также мы выяснили, что цвета пикселей передаются аналогово в формате RGB. Но остается открытым вопрос о порядке передачи пикселей, а также о роли сигналов H-SYNC и V-SYNC.

Видео-сигнал передается построчно. Сначала последовательно слева направо передаются цвета всех пикселей первой строки, затем второй, и так далее. Но не все переданные пиксели отображаются на экране! Связано это с тем, что VGA активно использовался во времена кинескопических дисплеев. Особенность этих дисплеев заключалась в том, что после завершения рисования очередной строки электронный луч не мог моментально перейти к началу следующей строки. Поэтому в VGA заложены паузы в передаче сигнала, в течение которых электронный луч мог бы успеть переместиться.

Проще всего это объяснить на такой картинке:

Картинка, объясняющая VGA-сигнал

Как по вертикали, так и по горизонтали, каждый фрейм окружен некоторым колличеством игнорируемых пикселей — так называемые заднее крыльцо (back porch) и переднее крыльцо (front porch). Плюс к этому дисплею передаются синхронизирующие сигналы, те самые H-SYNC и V-SYNC, говорящие о том, что началась следующая строка или следующий фрейм. Передавать пиксели вместе с сигналами, отвечающими за их синхронизацию, было бы весьма опрометчиво. Поэтому в передаче видео-сигнала возникают еще и паузы продолжительностью сигналов H-SYNC и V-SYNC.

Важно! Заметьте, что как H-SYNC, так и V-SYNC инвертированы, то есть низкое напряжение означает наличие сигнала, а высокое — его отсутствие.

Основной код

Сколько тактов длятся H-SYNC, V-SYNC и каждое крыльцо, можно подсмотреть там же, где мы смотрели частоту сигнала (раз, два). Ниже приведены значения для выбранного мной режима 800x600@56Hz:

parameter horiz_sync_pulse = 128;
parameter horiz_back_porch = 128;
parameter horiz_active_pixels = 800;
parameter horiz_front_porch = 32;

parameter vert_sync_pulse = 4;
parameter vert_back_porch = 14;
parameter vert_active_pixels = 600;
parameter vert_front_porch = 1;

Собственно, теперь у нас есть абсолютно все необходимые знания. Осталось лишь самая малость — перевести эти знания в код на SystemVerilog:

parameter horiz_pixels = (horiz_sync_pulse + horiz_back_porch +
                          horiz_active_pixels + horiz_front_porch);
parameter horiz_vis_begin = (horiz_sync_pulse + horiz_back_porch);
parameter horiz_vis_end = (horiz_pixels - horiz_front_porch);

parameter vert_pixels = (vert_sync_pulse + vert_back_porch +
                         vert_active_pixels + vert_front_porch);
parameter vert_vis_begin = (vert_sync_pulse + vert_back_porch);
parameter vert_vis_end = (vert_pixels - vert_front_porch);

logic [11:0] hctr; // 12 bits will be enough for <= 1920x1080
logic [10:0] vctr; // 10 bits will be enough for <= 1920x1080

always_ff @(posedge clk)
begin
    if(hctr < horiz_pixels - 1)
    begin
        hctr <= hctr + 1;
    end
    else
    begin
        hctr <= 0;
        if(vctr < vert_pixels - 1)
        begin
            vctr <= vctr + 1;
        end
        else
        begin
            vctr <= 0;
        end
    end
end

assign hsync = (hctr < horiz_sync_pulse) ? 0 : 1;
assign vsync = (vctr < vert_sync_pulse) ? 0 : 1;

always_ff @(hctr, vctr)
begin
    if(hctr >= horiz_vis_begin && hctr < horiz_vis_end &&
       vctr >= vert_vis_begin  && vctr < vert_vis_end)
    begin
        // vvv generated code begin, see gen.py vvv

        if(hctr < (horiz_vis_begin + (horiz_active_pixels/16)*1))
        begin
            r[0] <= 0;
            r[1] <= 0;
            r[2] <= 0;
            g[0] <= 0;
        end
        else
        if(hctr < (horiz_vis_begin + (horiz_active_pixels/16)*2))
        begin
            r[0] <= 1;
            r[1] <= 0;
            r[2] <= 0;
            g[0] <= 0;
        end
        else
        // ... (пропущено) ...

        // ^^^ generated code end, see gen.py ^^^
    end
    // currently no color will be displayed anyway
    else
    begin
        r[0:2] <= 0;
        g[0:2] <= 0;
        b[0:1] <= 0;
    end
end

endmodule

Основной код довольно скучен, так как он просто выводит на разные участки экрана все доступные нам 256 цветов. Чтобы не писать его руками, был написан вот такой скрипт на Python:

#!/usr/bin/env python3

# vim: set ai et ts=4 sw=4:

def gen(arg, begin, pixels, arr):
    for i in range(0, int(2**len(arr))):
        if i > 0:
            print("else");
        if i != int(2**len(arr)) - 1:
            print("if({} < ({} + ({}/{})*{}))".format(
                arg, begin, pixels, int(2**len(arr)), i+1))
        print("begin")
        for j in range(0, len(arr)):
            print((" " * 4) + "{} <= {};".format(
                arr[j], "1" if i & (1 << j) else "0"))
        print("end")


gen("hctr", "horiz_vis_begin", "horiz_active_pixels",
    ["r[0]", "r[1]", "r[2]", "g[0]"])
gen("vctr", "vert_vis_begin", "vert_active_pixels",
    ["g[1]", "g[2]", "b[0]", "b[1]"])

А вот и получившаяся картинка:

Восьмибитная палитра, сгенерированная на FPGA

К сожалению, встроенная в смартфон камера не очень хорошо передает цвета в условиях домашнего освещения. Но на глаз все цвета действительно выглядят разными.

Заключение

Полученным знаниям можно найти много применений, притом не обязательно в контексте FPGA. Те же микроконтроллеры STM32, к примеру, прекрасно справляются с задачей генерации VGA-сигнала 800x600. Можно писать свои игры, хотя бы простые, вроде змеек и тетрисов (хотя почему бы и не трехмерные). Тем более, что работать со звуком и джойстиком от Sega мы уже умеем. Можно использовать VGA-дисплей для вывода текстовой информации. Тут уже начинает попахивать и своей операционной системой. А еще можно выводить информацию поверх какого-то другого видео (OSD), сохранять скриншоты экрана (вспоминаем сериал Mr.Robot), писать какие-то красночные скринсейверы… В общем, простор для творчества здесь действительно безграничный.

В качестве источников дополнительной информации я бы рекомендовал следующие:

  • В прекрасном блоге timetoexplore.net есть статьи, посвященные работе с VGA на примере платы Arty Artix-7. На момент написания этих строк было доступно два поста: первый, второй.
  • На сайте nandland.com можно найти пример игры Pong, работающей полностью на FPGA. Код писался под плату Nandland Go Board, но в теории должен быть портируем на другие платы без большого труда.
  • На сайте Linus Akesson можно посмотреть потрясающее демо, демонстрирующее, что можно сделать с VGA на микроконтроллере ATmega88. Для скачивания доступны все исходники, а также музыкальный трек из демо в формате mp3.
  • Видео How a TV Works in Slow Motion на канале The Slow Mo Guys показвает в замедленной съемке отрисовку картинки дисплеями — как кинескопическими, так и современными LED-дисплеями.
  • Кое-какие подробности касаемо PLL в FPGA серии ICE40 можно найти в документе iCE40 sysCLOCK PLL Design and Usage Guide [PDF].

Исходники к заметке, как обычно, вы найдете на GitHub.

Дополнение: Вас также могут заинтересовать посты Считываем и декодируем информацию о VGA-мониторе и Превращаем VGA-монитор в «большой OLED-экранчик» с помощью iCEstick.

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.