В предыдущей статье мы начали осваивать построение шинно-ориентированных систем на базе среды 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). Это самый минимум, которым полезно владеть. Но, глядя на рейтинг предыдущей статьи, я делаю вывод, что вообще-то это мало кому нужно. При сохранении тенденции, тема будет закрыта. Но те, кому это интересно, смогут изучить всё самостоятельно, используя эту пару статей как трамплин, потому что везде, где я читал, были только теоретические изыски, а тут я постарался изложить всё в ключе, чтобы можно было собрать первую рабочую систему от и до.