Первые эксперименты со смешанным Litex+Verilog проектом для ПЛИС

В предыдущей статье мы начали осваивать построение шинно-ориентированных систем на базе среды Litex (которая всё делает на Питоне) с внедрением собственных модулей на Верилоге. Статья так разрослась, что практические опыты мы оставили на потом. Пришла пора провести их. Сегодня мы подключимся к VGA-монитору и поуправляем изображением, которое выдаёт модуль gpu, описанный в файле gpu.v, то есть, реализованный на языке Verilog. Управлять мы им будем через регистр команд, расположенный в блоке CSR, спроецированном на шину Wishbone. Все эти сущности, в свою очередь относятся к среде Litex. Инструменты для опытов мы тоже будем использовать штатные, Litex-овские. Приступаем!

Луч спит – развёртка идёт

Итак, мы проверяем пример, который формирует изображение через шнур VGA. Но что за константы в найденном на просторах сети коде? Они какие-то непонятные. Многие статьи просто ссылаются на готовые таблицы… Давайте я расскажу подробнее, откуда они взялись. Если я спрошу, сколько строк на экране в развёртке VGA, многие ответят, что 480. А если я спрошу, сколько строк было на экране отечественного телевизора, многие также ответят, что 625. Сейчас мы узнаем страшную тайну. Хоть эти цифры и не должны совпадать (первый ответ родом из шестидесятигерцовой NTSC, а второй – из нашей пятидесятигерцовой системы), но относятся к совершенно разным измерениям.

Дело в том, что 480 строк в VGA – это всего лишь видимые строки. А 625 строк в отечественной развёртке – это именно строки, передаваемые в сигнале. В чём разница?

Во-первых, в кинескопе лучу нужно время для обратного хода. Дойдя до нижней точки экрана, он должен пролететь назад. И ему нужно время, гораздо большее, чем время хода одной строки. У очень старых чёрно-белых телевизоров при максимальной яркости эти линии даже можно было разглядеть. Я в детстве так любил баловаться. Вот я нашёл на Гугле типичный рисунок:

Получается, что в сигнале должно быть предусмотрено время на этот процесс. Что в это время передаётся – не важно, так как луч здорового телевизора при этом погашен. Но время на сам процесс выделено быть должно.

То же касается и строчного обратного хода, хоть там время и намного меньше.

Дальше, аналоговые системы, да ещё и докварцевых времён, требовали некоторой подстройки. Вот таблица УЭИТ:

Белые тонкие линии на реперной области по краям экрана, должны были совмещаться с его границами. Что за линией – не должно быть видно. Ну, это на случай, если изображение будет немного плавать. А в сигнале эти точки присутствовать должны!

Вот и получается, что не все сигнальные строки (да и точки строк) являются видимыми. Из отечественных 625 строк, видимыми являлись только 576. А в американском стандарте, 480 строк – это видимая часть экрана при 525 строках в сигнале.

Шло время. Ушли в прошлое кинескопы. Их заменили матрицы, в которых нет ни электронного луча, ни его обратного хода, а картинка для них хранится в ОЗУ, поэтому непрерывность развёртки уже не требуется. Но для совместимости сигнал остался прежним. Мало того, если цикл статей не прервётся из-за отсутствия спроса, и мы дойдём до полностью цифрового HDMI, то увидим, что даже там есть невидимые участки растра! Поэтому, формируя картинку 640х480 точек, мы реально должны передавать по проводам область несколько большего размера.

Здесь я опустил массу эфирных штучек: замешивание синхроимпульсов в сам сигнал, чересстрочную развёртку и прочее, прочее, прочее. Мы сейчас просто посмотрели саму идею, откуда взялись эти лишние строки и точки внутри строк, привязав их к мониторным сущностям. Что же до точных цифр, то мы их вычислять не должны, так что все нюансы нам не потребуются. Есть масса статей, где приведены таблицы, сколько точек надо добавить к картинке. Причём на самом деле, добавленные значения измеряются в микросекундах, но внутри ПЛИС мы оперируем понятиями «точка» и «строка». Поэтому существуют таблицы либо калькуляторы, которые позволят нам узнать или вычислить эти дополнительные параметры именно в таких единицах измерения для каждого конкретного разрешения при конкретной частоте кадров.

Вот хорошая статья с картинкой, которая поясняет суть добавок к сигналу с привязкой к ПЛИС (но без того, что я описал выше): Учим iCEstick передавать видео-сигнал по VGA | Записки программиста (eax.me)

Оттуда есть ссылка на полезные таблицы martin.hinner.info/vga/timing.html

А вот – забавный онлайн-калькулятор: Video Timings Calculator (tomverbeure.github.io)

Уффф! Теперь становится понятно, что значат константы в найденном нами на просторах Интернета файле hvsync_generator.v. Сравните сами:

Кое-что не совпало, но находится в пределах погрешности.

Результаты работы онлайн-калькулятора, кстати, совпадают точнее:

Пара слов про файл hvsync_generator.v

Собственно, если мы уж заговорили про файл hvsync_generator.v, то с ним всё просто. Он формирует кадровый и строчный синхроимпульсы, согласно заданным параметрам. Чтобы изменить разрешение, надо эти параметры задать согласно требованиям, а также не забыть сменить частоту следования точек, о чём мы поговорим в следующем разделе. А вот так из базовых параметров рассчитываются рабочие:

  // derived constants
  parameter H_SYNC_START    = H_DISPLAY + H_FRONT;
  parameter H_SYNC_END      = H_DISPLAY + H_FRONT + H_SYNC - 1;
  parameter H_MAX           = H_DISPLAY + H_BACK + H_FRONT + H_SYNC - 1;
  parameter V_SYNC_START    = V_DISPLAY + V_BOTTOM;
  parameter V_SYNC_END      = V_DISPLAY + V_BOTTOM + V_SYNC - 1;
  parameter V_MAX           = V_DISPLAY + V_TOP + V_BOTTOM + V_SYNC - 1;

Вот так формируются многократно используемые условия:

  wire hmaxxed = (hpos == H_MAX) || reset;  // set when hpos is maximum
  wire vmaxxed = (vpos == V_MAX) || reset;  // set when vpos is maximum

Ну, и просто работают счётчики строк и формирователи синхроимпульсов.

  // horizontal position counter
  always @(posedge clk)
  begin
    hsync <= (hpos>=H_SYNC_START && hpos<=H_SYNC_END);
    if(hmaxxed)
      hpos <= 0;
    else
      hpos <= hpos + 1;
  end

  // vertical position counter
  always @(posedge clk)
  begin
    vsync <= (vpos>=V_SYNC_START && vpos<=V_SYNC_END);
    if(hmaxxed)
      if (vmaxxed)
        vpos <= 0;
      else
        vpos <= vpos + 1;
  end

  // display_on is set when beam is in "safe" visible frame
  assign display_on = (hpos

Обратите внимание, что отсюда импульсы выходят всегда в прямой полярности. Кажется, калькулятор допускает это дело:

Просто тот же режим 640х480 при других частотах требует другой полярности синхроимпульсов. Но здесь – нужна такая. Я проверил, работает.  А где брать цифры для других видеорежимов - вы теперь знаете.

Что у нас с тактовой частотой

В прошлый раз я уже упоминал, что нам предстоит разобраться с тактовой частотой. Только что из таблиц мы поняли, что нам необходима частота, близкая к 25 МГц. А в коде на Питоне я определял частоту вот так:

Вроде, я использую сущность clk. Но давайте не будем долго рассуждать, а посмотрим сгенерированный Верилоговский файл:

То же самое текстом.
gpu gpu(
	.clk(sys_clk),
	.x0(main_x0_storage),
	.x1(main_x1_storage),
	.y0(main_y0_storage),
	.y1(main_y1_storage),
	.color(gpio0),
	.hsync(gpio2),
	.vsync(gpio3)
);

Погодите, какой ещё sys_clk? Мы же просили просто clk… Потому что нам повезло, у нас на плате припаян генератор на 25 МГц. Но ещё есть шанс, что это какое-то хитрое именование… Ну-ка… Ищем, где этот сигнал формируется…

assign sys_clk = basesoc_crg_clkout0;

где:

То же самое текстом.
(* FREQUENCY_PIN_CLKI = "25.0", FREQUENCY_PIN_CLKOP = "60.0", FREQUENCY_PIN_CLKOS = "60.0", ICP_CURRENT = "6", LPF_RESISTOR = "16", MFG_ENABLE_FILTEROPAMP = "1", MFG_GMCREF_SEL = "2" *) EHXPLLL #(
	.CLKFB_DIV(5'd24),
	.CLKI_DIV(1'd1),
	.CLKOP_CPHASE(4'd9),
	.CLKOP_DIV(4'd10),
	.CLKOP_ENABLE("ENABLED"),
	.CLKOP_FPHASE(1'd0),
	.CLKOS2_CPHASE(1'd0),
	.CLKOS2_DIV(1'd1),
	.CLKOS2_ENABLE("ENABLED"),
	.CLKOS2_FPHASE(1'd0),
	.CLKOS_CPHASE(4'd14),
	.CLKOS_DIV(4'd10),
	.CLKOS_ENABLE("ENABLED"),
	.CLKOS_FPHASE(1'd0),
	.FEEDBK_PATH("INT_OS2")
) EHXPLLL (
	.CLKI(basesoc_crg_clkin),
	.RST(basesoc_crg_reset),
	.STDBY(basesoc_crg_stdby),
	.CLKOP(basesoc_crg_clkout0),
	.CLKOS(basesoc_crg_clkout1),
	.CLKOS2(builder_subfragments_crg_ecp5pll),
	.LOCK(builder_subfragments_crg_locked)
);

Последние сомнения развеяны! Перед нами самые что ни на есть 60 мегагерц. А нам надо 25! И они в системе есть, надо только правильно поправить Питоновский скрипт. Давайте я суну нос в описание генератора тактовых сигналов в уже хорошо нам знакомом файле Litexlitex-boardslitex_boardstargetscolorlight_5a_75x.py.

# Clk / Rst
        if not use_internal_osc:
            clk = platform.request("clk25")
            clk_freq = 25e6
        else:
            clk = Signal()
            div = 5
            self.specials += Instance("OSCG",
                                p_DIV = div,
                                o_OSC = clk)
            clk_freq = 310e6/div

        rst_n = 1 if not with_rst else platform.request("user_btn_n", 0)

        # PLL
        self.submodules.pll = pll = ECP5PLL()
        self.comb += pll.reset.eq(~rst_n | self.rst)
        pll.register_clkin(clk, clk_freq)
        pll.create_clkout(self.cd_sys,    sys_clk_freq)
        if sdram_rate == "1:2":
            pll.create_clkout(self.cd_sys2x,    2*sys_clk_freq)
            pll.create_clkout(self.cd_sys2x_ps, 2*sys_clk_freq, phase=180) # Idealy 90° but needs to be increased.
        else:
           pll.create_clkout(self.cd_sys_ps, sys_clk_freq, phase=180) # Idealy 90° but needs to be increased.

Дело ясное, что дело тёмное. Вот я вижу clk. Вроде как, это вход… Короче, кончаем пытаться разбираться по-сухому! Зря я что ли создавал проект в Visual Studio? Давайте попробуем трассернуть и посмотреть, где можно взять сигнал, не дожидаясь милости от природы! Идём в код, который написали в прошлый раз, и ставим точку останова здесь:

Запускаем проект на исполнение, и сначала, чтобы настроить глазомер, смотрим значение той переменной, которая заносится сейчас:

А теперь я нахально ставлю курсор на поле cd_sys и начинаю ездить по всем полям этой переменной, пытаясь найти что-то, похожее на то, что я уже видел, но содержащее что-то, похожее на вход PLL. Не найдя ничего приличного, сдвинусь левее… И так, осматривая реальную систему, когда она уже запущена, постепенно нахожу параметр, очень похожий по значению. Давайте я даже сделаю анимированный gif.

В общем, в переменной soc.crg.pll есть замечательное поле clkin_freq, равное 25000000 (это наши 25 мегагерц, заданные в герцах), а поле clkin похоже на описание ножки… Вот давайте и заменим строку:

clk = soc.crg.cd_sys.clk

на:

clk = soc.crg.pll.clkin

Собираем, запускаем, проверяем – нам повезло! Мы угадали с первой попытки. Трассировка – великая вещь! А кто тянется ставить минус за "низкий технический уровень", приговаривая, что такое надо находить в справочниках – попробуйте, найдите и дайте ссылочку, а также путь, как нашли. А потом поговорим. Я долго искал. Трассировка же всё выявила за 5 минут. Поэтому и пропагандирую её использование.

То же самое текстом.
gpu gpu(
	.clk(basesoc_crg_clkin),
	.x0(main_x0_storage),
	.x1(main_x1_storage),
	.y0(main_y0_storage),
	.y1(main_y1_storage),
	.color(gpio0),
	.hsync(gpio2),
	.vsync(gpio3)
);

где:

assign basesoc_crg_clkin = clk25;

Уффф. С этим разобрались! Да здравствует живая трассировка! Проверяем на мониторе? Давайте не будем торопиться. Подключимся сначала к логическому анализатору и проверим картинку там.

Выявляем проблему там, где не ждали

Итак. Собираем получившийся проект, загружаем его в плату, запускаем, смотрим на анализаторе. С виду — всё красиво. Видны кадровые синхроимпульсы, видны строчные синхроимпульсы, видны отображаемые точки:

Всё? Проверяем на мониторе? Не спешите! Измеряем частоту кадровых импульсов (на рисунке выше видна стрелочка между парой из них). Получаем примерно 120 Гц.

Строчная частота тоже удвоенная. Ох, и посидел я с экспериментами. Ну разумеется, во всём была обвинена неверно установленная частота! Но не тут-то было! Верная она! 25 МГц честные приходят. Ларчик просто открывался. Файл gpu.v я нашёл в Интернете и, так как он простенький, доверял ему всецело. А зря. Вот как он выглядит:

То же самое текстом.
// VGA 640x480 @60Hz needs a 25.175MHz pixel clock
  // but the PLL is already in use
  // I split the horizontal resolution in half and use the existing 12MHz clock instead
  // front and back porch are manually adjusted until VSYNC reaches the expected 60Hz
  hvsync_generator #(
      .H_DISPLAY(320), // horizontal display width
      .H_BACK(12), // horizontal left border (back porch)
      .H_FRONT(8), // horizontal right border (front porch)
      .H_SYNC(48), // horizontal sync width
      
      .V_DISPLAY(480), // vertical display height
      .V_TOP(33), // vertical top border
      .V_BOTTOM(8), // vertical bottom border
      .V_SYNC(2), // vertical s
    ) hvsync_gen (
    .clk(clk),
    .reset(0),
    .hsync(hsync),
    .vsync(vsync),
    .display_on(display_on),
    .hpos(hpos),
    .vpos(vpos),
  );

Классно, да? Обещаем 640 точек по горизонтали, а в модуль развёрток передаём всего 320. Вот он быстрее и работает! Потому что у автора кварц был на 12 МГц, вот он всё и пересчитал под такую частоту. Короче, удаляем все эти параметры, благо внутри модуля развёрток есть хорошо описанные! Остаётся:

  hvsync_generator hvsync_gen (
    .clk(clk),
    .reset(0),
    .hsync(hsync),
    .vsync(vsync),
    .display_on(display_on),
    .hpos(hpos),
    .vpos(vpos),
  );
    
  assign color = display_on && (hpos >= x0 && hpos < x1 && vpos >= y0 && vpos < y1);

Вот и верь после этого людям! Доверяй, но проверяй!

Уффф. Теперь можно экспериментировать с монитором. Как мы будем к нему подключаться?

Добавляем VGA разъём к плате

Вдохновение при изготовлении разъёма мы будем черпать тут: fpga4fun.com - Pong Game

Единственно, что у нашей платы выходы пятивольтовые, так что резисторы в их схеме надо заменить на 470 Ом. А так – я скопирую схему включения оттуда:

Но самое ценное там – конструктив. Вот как это сделано на той странице:

Это же гениально! А этот разъём нужно подключить Ардуиновскими проводочками к нашей макетке. Красный и синий провода – заземлить (благо земляных ножек у нас много), а зелёный и синхросигналы – подать на контакты, которые мы недавно назначили. Кстати, обратите внимание, как неудобно их искать! Вот наш список использованных gpio линий:

    touch_pins = [
           soc.platform.request("gpio", 0),
           soc.platform.request("gpio", 1),
           soc.platform.request("gpio", 2),
           soc.platform.request("gpio", 3)
       ]

Сначала надо посмотреть, к каким контактам разъёма они подключены:

_gpios = [
    # Attn. Jx/pin descriptions are 1-based, but zero based defs. used!

    # J1
    ("gpio", 0, Pins("j1:0"), IOStandard("LVCMOS33")), # Input now
    ("gpio", 1, Pins("j1:1"), IOStandard("LVCMOS33")), # Input now
    ("gpio", 2, Pins("j1:2"), IOStandard("LVCMOS33")), # Input now   
    # GND
    ("gpio", 3, Pins("j1:4"), IOStandard("LVCMOS33")), # Input now

Затем – каким цепям соответствуют:

То же самое текстом.
        self.comb += [
            pins[1].eq(0),
        ]
        self.specials += Instance(
            'gpu',
            i_clk=clk,
            i_x0=self.x0.storage,
            i_x1=self.x1.storage,
            i_y0=self.y0.storage,
            i_y1=self.y1.storage,
            o_hsync=pins[2],
            o_vsync=pins[3],
            o_color=pins[0]
        )

На самом деле, язык Питон позволяет всё это оформить более наглядно. Так что если будем развивать тему, то в следующей статье мы как раз наведём красоту. Пока же – пользуемся тем, что найдено в минималистичных примерах.

С моими кривыми руками получилась вот такая штука:

Подключаем плату к монитору, заливаем в неё «прошивку» и видим такую красоту:

Начинаем эксперименты

Наконец-то мы подготовили всю инфраструктуру для основной части эксперимента. Мы же всего-навсего собирались записывать что-то в порты Verilog модуля, пользуясь механизмом CSR. Приступаем!

До сих пор мы работали с каталогом PyTest2buildcolorlight_5a_75bgateware. Теперь перейдём в PyTest2buildcolorlight_5a_75bsoftware. Там нас интересует файл с говорящим именем

PyTest2buildcolorlight_5a_75bsoftwareincludegeneratedcsr.h

В текущей нашей реализации информации там самый минимум. Смотрим:

То же самое текстом.
/* gpu */
#define CSR_GPU_BASE (CSR_BASE + 0x0L)
#define CSR_GPU_X0_ADDR (CSR_BASE + 0x0L)
#define CSR_GPU_X0_SIZE 1
static inline uint32_t gpu_x0_read(void) {
	return csr_read_simple(CSR_BASE + 0x0L);
}
static inline void gpu_x0_write(uint32_t v) {
	csr_write_simple(v, CSR_BASE + 0x0L);
}
#define CSR_GPU_X1_ADDR (CSR_BASE + 0x4L)
#define CSR_GPU_X1_SIZE 1
static inline uint32_t gpu_x1_read(void) {
	return csr_read_simple(CSR_BASE + 0x4L);
}
static inline void gpu_x1_write(uint32_t v) {
	csr_write_simple(v, CSR_BASE + 0x4L);
}
#define CSR_GPU_Y0_ADDR (CSR_BASE + 0x8L)
#define CSR_GPU_Y0_SIZE 1
static inline uint32_t gpu_y0_read(void) {
	return csr_read_simple(CSR_BASE + 0x8L);
}
static inline void gpu_y0_write(uint32_t v) {
	csr_write_simple(v, CSR_BASE + 0x8L);
}
#define CSR_GPU_Y1_ADDR (CSR_BASE + 0xcL)
#define CSR_GPU_Y1_SIZE 1
static inline uint32_t gpu_y1_read(void) {
	return csr_read_simple(CSR_BASE + 0xcL);
}
static inline void gpu_y1_write(uint32_t v) {
	csr_write_simple(v, CSR_BASE + 0xcL);
}

Итак, нами было создано четыре регистра, в каждом из которых используется по 16 бит, а 16 бит – зарезервированы. Можно ли объединять данные в одном регистре, а если да, то как – тема для отдельной статьи. Также в отдельной статье можно рассмотреть, как сделать код на Питоне самодокументирующимся. Чтобы из него автоматически вставлялись бы подробности в этот заголовочный файл. Пока – просто запомнили регистры. Да на самом деле одного регистра хватит, X1. У него смещение +4. А относительно чего смещение? Это нам подскажет содержимое файла mem.h:

#ifndef CSR_BASE
#define CSR_BASE 0x00000000L
#define CSR_SIZE 0x00010000
#endif

Как замечательно! Базовый адрес CSR равен нулю.

Напомню, в прошлой статье я указывал опции сборки системы:

--with-etherbone --eth-ip=192.168.2.128

Для доступа к шине через сеть, нам нужна утилита wishbone-tool. Готовую сборку под свою ОС можно скачать здесь:

Releases · litex-hub/wishbone-utils (github.com)

Распаковываем и пишем по адресу шины 4 значение 600 при помощи команды:

wishbone-tool.exe --ethernet-host 192.168.2.128 4 600

Это я параметр X2 сделал равным шестистам (при разрешении экрана 640х480). Прямоугольник стал таким:

И, собственно, всё. Мы убедились, что всё проецируется на шину, а мы имеем возможность управлять значениями через регистр команд…

Упрощаем себе жизнь

Как-то даже обидно, так много готовились и так мало экспериментировали. Давайте, что ли проведём один маленький эксперимент по упрощению себе жизни.

В файловой системе, на уровне скрипта, который мы делали в прошлой статье, добавляем каталог scripts

А в сам наш многострадальный Питоновский скрипт вставляем буквально одну строчку. Я покажу её вместе с теми, между которыми она помещена:

То же самое текстом.
    soc.platform.add_source("hvsync_generator.v")
    soc.platform.add_source("gpu.v")

    args.csr_csv = "scripts/csr.csv"

    builder = Builder(soc, **builder_argdict(args))
    builder.build(**trellis_argdict(args), run=args.build)

Прогоняем скрипт, и в каталоге Scripts возникает новый файл csr.csv. Вот его начало:

Теперь нам не надо ни смотреть никакие заголовки, ни даже скачивать wishbone-tool. Поместим в каталог scripts файл moverect.py со следующим содержимым:

#!/usr/bin/env python3

import time

from litex import RemoteClient

wb = RemoteClient()
wb.open()

# # #

wb.regs.gpu_y0.write (200)
wb.regs.gpu_y1.write (240)

for i in range(100):
    for j in range (600):
         wb.regs.gpu_x1.write (j+39)
         wb.regs.gpu_x0.write (j)
         time.sleep(0.001)

wb.close()

Теперь в отдельном терминале идём в каталог Litexlitexlitextools… Вот его содержимое для справки:

И там запускаем отладочный сервер строкой:

python litex_server.py --udp --udp-ip=192.168.2.128

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

Теперь в основном терминале можно запустить и только что написанный скрипт. Он будет работать через этот сервер. Итак, пишем:

python moverect.py localhost

И гипнотизируем экран, по которому слева направо ездит квадрат. В этом опыте мы не смотрели никаких констант. Мы просто пользовались символическими именами, взятыми из файла csr.csv. Плюс к тому, мы не использовали никаких инструментов, не входящих в поставку Litex. Правда, сервер в отдельном окне нам запустить пришлось.  Но этот сервер организован на Питоновском скрипте, входящем в основной комплект Litex.

Немного философии

Давным-давно, у меня БК-шка была подключена к ламповому чёрно-белому телевизору. Потом мне выдали ламповый цветной, с более устойчивым растром. И я сказал, что стало намного лучше. Потом я раздобыл монитор МС6105 с антибликовым кинескопом. И сказал, что стало намного лучше. Шло время. При смене ЭВМ МС6105 сменился на Samsung SyncMaster 3NE. И я сказал, что стало намного лучше, так как нет того пятидесятигерцового мерцания в глазах, даже когда я просто хочу заснуть. Потом была чреда других мониторов с кинескопами, каждый из которых создавал свои проблемы, которая завершилась покупкой первого ЖК монитора. И я сказал, что стало намного лучше. Никакого мерцания!

Дальше монитор сменился на новый, имеющий кабель DVI. И я бы даже не сказал, что стало намного лучше. Но когда через несколько месяцев пришлось поработать с VGA кабелем – я понял, что к хорошему быстро привыкаешь. В аналоговом кабеле немного гуляют вертикальные линии, в аналоговом кабеле «кипит» однотонное изображение. Это дико раздражает. Особенно когда привык к чёткой цифровой картинке.

Кто скажет, что виной всему тот монтаж, которым я собрал свою платочку, тот будет неправ. Я пробовал выводить изображение при помощи платы, содержащей на себе видео ЦАП и штатный разъём VGA. Было только хуже. Ну, потому что там я пользовался примером fpga4fun.com - Pong Game (устранив в нём ошибки в генераторе синхроимпульсов: там надо не 768, а 800 точек в строке сделать). Там используются более изящные цвета, они, имея больше уровней, кипят ещё сильнее, чем наш весьма цифровой «либо зелёный, либо чёрный». А ещё там используются тонкие линии. Они плавают ещё сильнее, чем наша граница прямоугольника.

Цифра – это цифра, аналог – это аналог. До цифровой революции ему не было замены. Сейчас – есть. Поэтому руку с VGA мы набили, а вот пользоваться этим – лично я не собираюсь. Мне нервное спокойствие дороже. Если цикл будет востребован – сделаем и HDMI выход, благо я упоминал макетку с ПЛИС Lattice, содержащую такой разъём. А VGA – это просто красивый пример, где можно быстро сделать аппаратуру и на простых, удобных сущностях освоить вывод в регистр команд.

Заключение

Мы испытали работу с регистрами команд при помощи утилиты wishbone-tool (по абсолютным адресам) и при помощи штатных средств из поставки Litex (для чего нам пришлось сделать символический файл csr.csv). 

Финальный пример, разобранный в статье, можно посмотреть тут.

Дальше следует рассмотреть работу с регистрами состояния (то есть, чтение из Verilog модулей). Также полезным будет добавление возможности самодокументирования к разрабатываемому коду, чтобы не пришлось долго елозить, выявляя связи сигналов и портов CSR. Следующий этап - подключение модулей не через CSR, а через прямую проекцию на шину (Wishbone или AXI). Это самый минимум, которым полезно владеть. Но, глядя на рейтинг предыдущей статьи, я делаю вывод, что вообще-то это мало кому нужно. При сохранении тенденции, тема будет закрыта. Но те, кому это интересно, смогут изучить всё самостоятельно, используя эту пару статей как трамплин, потому что везде, где я читал, были только теоретические изыски, а тут я постарался изложить всё в ключе, чтобы можно было собрать первую рабочую систему от и до. 

 

Источник

Читайте также

Меню