Comments 67
Клёвый проект!
В хаб FPGA не хотите добавить? Вроде релевантно.
А можно поподробнее, как физически реализовали двойной буфер? В какой момент происходит перезапись из одного буфера в другой?
Буфер представляет собой 2 отдельные микросхемы. У буфера есть вход select. Когда select равен нулю , то происходит запись в первую микросхему и чтение из второй микросхемы. Когда select равен единице, то происходит запись во вторую микросхему и чтение из первой. Не происходит перезаписи из одной микросхемы в другую, они просто меняются местами.
Меняются местами по команде хоста?
Каждый полученный байт инкрементирует счётчик пикселей. Когда счётчик досчитывает до конца, то сигнал select инвертируется. Получается, что буфер переворачивается, когда полностью заполняется.
Понятно) спасибо за подробности.
Хм, а если хотим только часть кадра перерисовать? Ведь быстрее будет.
Отрисовка в буфер обычно занимает существенно больше времени, чем время вывода одного кадра. Если программа не успеет, то могут появиться неприятные визуальные артефакты. Поэтому, ИМХО, было бы не плохо дать программе возможность управлять процессом переключения буферов.
Я могу хоть 3 часа писать в первый буфер. Картинка то выводится из второго и они не поменяются местами, пока запись не завершится. Буферы меняются местами только после полной перезаписи записываемого буфера. При таком подходе они могут поменяться прямо посреди кадра , но это происходит так быстро , что этого не видно.
Если бы смена буфера осуществлялась между кадрами на каждом кадре , то действительно, надо было бы успевать перезаписывать буфер в течение одного кадра.
Если они и правда меняются в середине кадра, то как бы вы быстро не меняли их, будут артефакты на быстрых движениях. Надо менять строго во время обратного хода луча. Это вообще обязательно.
Мне кажется, не критично. Ну, испортится один кадр артефактами. Следующие кадры ведь воспроизведутся верно. Глаз не сможет заметить повреждение одного кадра.
Если это один кадр, то да видно не будет. Только дело в том, что если вы анимацию показываете, то таких кадров будет очень много если не все. И тогда они становятся видны и очень даже сильно. Всякие движущиеся полосы получаются и другие непотребства.
Ну, это точно не случай, описанный в статье. Гнать анимированную картинку в более-менее приемлемом разрешении по последовательному интерфейсу у вас вряд ли получится.
Ну-у-у, надо считать. На Apple II с частотой процессора 1 MHz и скорость записи в видеопамяти в несколько раз меньше (примерно 2..5мкс на байт) , игры и анимации были возможны.
Да, как ни считай - все одно и выходит. На apple II (и на всех машинах того времени) графическая картинка рисовалась на экране несколько секунд (и это при тех крошечных разрешениях). Более-менее что то шустрое можно было сделать с помощью спрайтовой графики. Спрайты подготавливались заранее и выводились аппаратно. На денди, например, специальный видеопроцессор - полностью спрайтовый.
Если у вас 5 мкс на байт, то чтобы вывести картинку 640х480 глубиной цвета 1 байт нужно 1,5 с. И это при том, что процессор ничем другим не будет заниматься и память - статическая.
Да вы что??? Какие несколько секунд? Я сам писал системы анимации на Apple II. Вполне даже шустро работали. Больше 200ms было признаком низкой квалификации программиста. Не говорю, о профессиональных продуктах, которые и в 3D могли, хоть и ограничено. Никакой специализированный хардуер в AppleII не был – программа и видеопамять.
Я так понимаю, вы про демки говорите? Там есть свои особенности. Во-первых, из системы выжимают все до последнего клока, а во вторых, кроме этой задачи проц ничем не занят. Но и то, 200 мс - это скорее слайд шоу, а не анимация.
Вы же сами предлагали посчитать, я и насчитал 1,5 с. Что не так в расчетах?
Нет, не про демки говорю. Вполне прикладное ПО: Обучающие программы для школы – по химии, физике, игры, презентации всякие. Вы пожалуй слишком молодой. Вот скачайте эмулятор Apple II – скорость компьютера там эмулируется корректно. И сами посмотрите что к чему.
Вы же сами предлагали посчитать, я и насчитал 1,5 с. Что не так в расчетах?
А все там не так. Графический режим в Apple II, это 280х192 пикселей. Один байт кодирует 7 пикселей. Строка: 40 байт x 192 строк = 7680 байт x 5мкс = 38мс. Но это конечно, если не делать никакие обработки. Например очистить экран – будет около того. Но в анимациях очень редко приходилось перерисовать весь экран. Поэтому время хватало и на дополнительные вычисления.
Вы всё верно говорите. Если показывать каждый кадр абсолютно новое изображение, то разрыв кадра будет очень заметен, но в реальном видео почти нет резких переходов и этих артефактов вообще не видно. Это надо прям стоять и выискивать.
Если смена буфера произойдёт посреди кадра, то там не будет битых пикселей, просто первая половина экрана покажет старый кадр, а вторая покажет новый. Если кадры отличаются не сильно, то этого не видно.
Есть sram с двойной шиной данных и адреса. Удивительная вещь. Когда подключал к FPGA было очень удобно писать туда с МК и просто переключать адрес чтения при выводе в vga
Чтобы всем было понятно, нужно нарисовать схему со всеми компонентами или хотябы блок-схему связей.
Интересный проект.
Хабр Торт! Искренне радуюсь, когда вижу подобные статьи. Пробуйте больше с ПЛИС и пишите :)
Супер! И даже Bad Apple не забыли. :)
Позавчера читал вашу статью. Глаза на лоб полезли. Очень круто получилось!
Я рад что Вам понравилось. :-)
Вашу статью я отправил своим студентам как пример того какие безграничные возможности есть у ПЛИС. И да, спасибо за то, что не стали рекламировать чей-то очередной телеграм канальчик. ;)
Было бы круто если попробовали сделать полноценный TBC (корректор временны́х искажений), пусть даже строчный, а не полнокадровый... Очень нужная для работы с аналоговыми источниками, но редкая вещь и новых никто не производит(
TBC - это по сути АЦП, буффер и ЦАП. Строки цифруются и сохраняются в память, а потом "выдаются", но уже через равные промежутки времени, что позволяет это все потом без потерь кадров захватить...
Ох, ковырял их и когда-то... Но сейчас ИМХО проще организовать оцифровку с избыточностью, причём на аппаратном уровне магнитофона. Ведь на не потребно реальное время, а вычислительной мощности сегодня предостаточно... Я был готов реализовать такой проект в середине нулевых, но не нашлось должного количества потенциальных заказов, в отличии на сканирование звуковых аналоговых лент.
Хоть я так и не полюбил ПЛИС-ы, но автору респект. Сам когда-то писал "велосипеды" для вывода картинок/текста на зомбоящик на AVR, STM32 и RISC-V.
Годно. Но автору следует поизучать схемотехнику старых восьмибитных компьютеров с графическим экраном, например Специалист или Орион. И тогда окажется, что даже VGA развёртку можно уместить в 32 ячейки, лол.

128 ячеек самой простой CPLD хватает на синхрогенератор и контроллер обычной DRAM (для SDRAM понятно нужно чуть больше). И ещё бы на маппер хватило, но тут я упёрся в лимит ног, поэтому пришлось ставить второй CPLD. Что внутри CPLD:

По теме: как-то лет 20 назад была мысль собрать тупой терминал, который бы генерировал картинку на стандартный VGA монитор в альфавитно-цифровом виде или даже поддерживал графику и просто бы эмулировал какой-нибудь терминал на UART вроде VT52. Так же он должен был уметь в X/Y/Z-Modem. Смысл: подключаться к своим устройствам на МК, у которых для отладки просто UART. А потом кто-то сначала запилил телетерминал на AVR и PIC32 ив конце концов тема перестала быть актуальной, увы. С другой стороны, устройств с консолью на UART ещё хватает (вроде роутеров всяких), так что может быть такой терминальчик сделать на LCD панельке ещё есть смысл?
Скрытый текст
PS Вот, например, логгер-визуализатор активности шины Z80 на VGA мониторе. Требуется выводить регулярные структуры, поэтому хватило внутренней памяти M9K. Девборда:

Результат работы:


Код:
// Z80 VGA
module Z80_VGA (
// Такты
input CLK, // 50MHz
// Кнопка
input KEY0,
// Лампочки
output LED,
output reg [2:0]ROW,
output reg [7:0]DIGIT,
// VGA
output reg [4:0]R,
output reg [5:0]G,
output reg [4:0]B,
output reg HSYNC,
output reg VSYNC,
// Z80
input [15:0]ADR,
input nM1,
input nBUSA,
input nRD,
input nWR,
// Отладка
output [3:0]Debug0,
output [3:0]Debug1,
output [3:0]Debug2,
output [3:0]Debug3,
output [3:0]Debug4,
output [3:0]Debug5
);
// PLL
wire MAINCLK;
wire LEDCLK;
PLL PLL1(CLK, MAINCLK, LEDCLK);
// RAM
wire [5:0]RamPix;
RAM RAM1(RamAdr[15:0],MAINCLK,NewPix[5:0],RamUpdate,RamPix[5:0]);
// Переменные
reg [1:0]Button;
reg Mode;
reg [1:0]Scan;
reg [11:0]Digits;
reg [3:0]Arb;
reg [9:0]X;
reg [9:0]Y;
reg WinSync;
reg FrameX;
reg FrameY;
reg ArrowX;
reg ArrowY;
reg YMSync;
reg YMX;
reg YMY;
reg PSGSync;
reg PSGX;
reg PSGY;
reg BankSync;
reg BankX;
reg BankY;
reg [15:0]SyncAdr;
reg SyncM1;
reg SyncBUSA;
reg SyncRD;
reg SyncWR;
reg [15:0]DelayAdr0;
reg [15:0]DelayAdr1;
reg [15:0]DelayAdr2;
reg [2:0]DelayM1;
reg [2:0]DelayBUSA;
reg [1:0]EdgeRD;
reg [1:0]EdgeWR;
reg [15:0]ReqAdr;
reg [2:0]ReqCol;
reg [15:0]WrAdr;
reg [5:0]WrPix;
reg [15:0]RamAdr;
reg [5:0]NewPix;
reg RamUpdate;
reg [5:0]Frames;
// Комбинаторика
assign LED = ~Mode;
wire WrStb;
assign WrStb = ~EdgeWR[1] & EdgeWR[0];
wire RdStb;
assign RdStb = ~EdgeRD[1] & EdgeRD[0];
wire Zoom;
assign Zoom = (YMSync | PSGSync | BankSync) ? Y[4] & Y[3] & Y[2] & Y[1] & Y[0] & X[3] & X[2] & X[1] & X[0] : (Y[1] | Y[8]) & Y[0] & (X[0] | Y[8]);
wire [2:0]Decay;
assign Decay[2:0] = (~Frames[4] & ~Frames[3] & ~Frames[2] & ~Frames[1] & ~Frames[0] & Zoom) ? 3'h1 : 3'h0;
// BIN
wire [3:0]BIN;
assign BIN[3:0] = (~Scan[1]) ?
(~Scan[0]) ? Digits[3:0] : Digits[7:4]
:
(~Scan[0]) ? Digits[11:8] : 4'h0;
// Отладка
assign Debug0[3:0] = Arb[3:0];
assign Debug1[3:0] = {DelayBUSA[2],DelayM1[2], WrStb, RdStb};
assign Debug2[3:0] = DelayAdr2[3:0];
assign Debug3[3:0] = DelayAdr2[7:4];
assign Debug4[3:0] = DelayAdr2[11:8];
assign Debug5[3:0] = DelayAdr2[15:12];
// Экранчик
always @(posedge LEDCLK) begin
// Отображение экранчика
Scan[1:0] <= Scan[1:0] + 2'h1;
// Сканирование разрядов
ROW[0] <= Scan[1] & ~Scan[0];
ROW[1] <= ~Scan[1] & Scan[0];
ROW[2] <= ~Scan[1] & ~Scan[0];
// Преобразование HEX
case (BIN[3:0]) // D : HGCB AFED
4'h0 : DIGIT[7:0] <= 8'hC0; // 0 : 1100 0000
4'h1 : DIGIT[7:0] <= 8'hCF; // 1 : 1100 1111
4'h2 : DIGIT[7:0] <= 8'hA4; // 2 : 1010 0100
4'h3 : DIGIT[7:0] <= 8'h86; // 3 : 1000 0110
4'h4 : DIGIT[7:0] <= 8'h8B; // 4 : 1000 1011
4'h5 : DIGIT[7:0] <= 8'h92; // 5 : 1001 0010
4'h6 : DIGIT[7:0] <= 8'h90; // 6 : 1001 0000
4'h7 : DIGIT[7:0] <= 8'hC7; // 7 : 1100 0111
4'h8 : DIGIT[7:0] <= 8'h80; // 8 : 1000 0000
4'h9 : DIGIT[7:0] <= 8'h82; // 9 : 1000 0010
4'hA : DIGIT[7:0] <= 8'h81; // A : 1000 0001
4'hB : DIGIT[7:0] <= 8'h98; // B : 1001 1000
4'hC : DIGIT[7:0] <= 8'hF0; // C : 1111 0000
4'hD : DIGIT[7:0] <= 8'h8C; // D : 1000 1100
4'hE : DIGIT[7:0] <= 8'hB0; // E : 1011 0000
4'hF : DIGIT[7:0] <= 8'hB1; // F : 1011 0001
endcase
end
// Синхронная логика
always @(posedge MAINCLK) begin
// Считаем арбитер
Arb[3:0] <= Arb[3:0] + 4'h1;
// Синхронизация
if (~Arb[0]) begin
SyncAdr[15:0] <= ADR[15:0];
SyncM1 <= nM1;
SyncBUSA <= nBUSA;
SyncRD <= nRD;
SyncWR <= nWR;
end
// Детектор строба
if (~Arb[3] & ~Arb[2] & ~Arb[1] & ~Arb[0]) begin
// Сохраняем адрес
DelayAdr0[15:0] <= SyncAdr[15:0];
DelayAdr1[15:0] <= DelayAdr0[15:0];
DelayAdr2[15:0] <= DelayAdr1[15:0];
// Задержка M1
DelayM1[2:0] <= {DelayM1[1:0],SyncM1};
// Задержка BUSA
DelayBUSA[2:0] <= {DelayBUSA[1:0],SyncBUSA};
// Синхронизируем стробы и адрес
EdgeRD[1:0] <= {EdgeRD[0],SyncRD};
EdgeWR[1:0] <= {EdgeWR[0],SyncWR};
// Синхронизируем адрес
ReqAdr[15:0] <= DelayAdr2[15:0];
// Формируем запрос
ReqCol[0] <= WrStb | (RdStb & ~DelayBUSA[2]);
ReqCol[1] <= RdStb;
ReqCol[2] <= (WrStb & ~DelayBUSA[2]) | (RdStb & DelayBUSA[2] & ~DelayM1[2]);
end
// Только 1 раз из 4
if (Arb[1] & Arb[0]) begin
// Арбитраж
case (Arb[3:2])
// Состояние 1
2'h0 : begin // Считываем и отображаем текущий пиксель
if (~(FrameX & FrameY)) begin
R[4:0] <= 5'h00; G[5:0] <= 6'h00; B[4:0] <= 5'h00;
end else
if (WinSync | YMSync | PSGSync | BankSync) begin
// Получаем и модифицируем пиксель
if (RamPix[2] | RamPix[1] | RamPix[0]) NewPix[5:0] <= {RamPix[5:3],RamPix[2:0] - Decay[2:0]}; else NewPix[5:0] <= 6'h00;
// Отображаем пиксель
R[4:0] <= (RamPix[3]) ? {RamPix[2:0],RamPix[0] | (Y[8] & ~BankSync),RamPix[0] | (Y[8] & ~BankSync)} : {3'h0,Y[8] & ~BankSync,Y[8] & ~BankSync};
G[5:0] <= (RamPix[4]) ? {RamPix[2:0],RamPix[0] | (Y[8] & ~BankSync),RamPix[0] | (Y[8] & ~BankSync),RamPix[0] | (Y[8] & ~BankSync)} : {3'h0,Y[8] & ~BankSync,Y[8] & ~BankSync,Y[8] & ~BankSync};
B[4:0] <= (RamPix[5]) ? {RamPix[2:0],RamPix[0] | (Y[8] & ~BankSync),RamPix[0] | (Y[8] & ~BankSync)} : {3'h0,Y[8] & ~BankSync,Y[8] & ~BankSync};
// Заказываем обновление пикселя
RamUpdate <= 1'b1;
end else
if (ArrowX & ArrowY & Mode) begin
R[4:0] <= 5'h1F; G[5:0] <= 6'h3F; B[4:0] <= 5'h1F;
end else begin
R[4:0] <= 5'h08; G[5:0] <= 6'h10; B[4:0] <= 5'h08;
end
// Переносим запрос
if (ReqCol[2] | ReqCol[1] | ReqCol[0]) begin
WrAdr[15:0] <= ReqAdr[15:0];
WrPix[5:0] <= {ReqCol[2:0],3'h7};
ReqCol[2:0] <= 3'h0;
end
end
// Состояние 2
2'h1 : begin // Снимаем флаг обновления
RamUpdate <= 1'b0;
// Подготавливаем пиксель
NewPix[5:0] <= WrPix[5:0];
// Устанавливаем адрес записи
RamAdr[15:0] <= WrAdr[15:0];
end
// Состояние 3
2'h2 : begin // Управление стробом записи
if (WrPix[5] | WrPix[4] | WrPix[3]) begin
RamUpdate <= 1'b1;
Digits[11:0] <= RamAdr[15:4];
end
end
// Состояние 4
2'h3 : begin // Синхронизируем окно
WinSync <= ~Y[9] & ~(X[9] | X[8]);
YMSync <= YMX & YMY;
PSGSync <= PSGX & PSGY;
BankSync <= BankX & BankY;
// Считаем X
if (X[9:0] == 10'h20F) X[9:0] <= 10'h000; else X[9:0] <= X[9:0] + 10'h001;
// Синхронизация HSYNC
if (X[9:0] == 10'h15C) begin
HSYNC <= 1'b0;
// Считаем Y
if (Y[9:0] == 10'h273) Y[9:0] <= 10'h000; else Y[9:0] <= Y[9:0] + 10'h001;
end else if (X[9:0] == 10'h19C) HSYNC <= 1'b1;
// Синхронизация VSYNC
if (Y[9:0] == 10'h22D) begin
VSYNC <= 1'b0;
Frames[4:0] <= Frames[4:0] + 5'h1;
Button[1:0] <= {Button[0],KEY0};
if (Button[1] & ~Button[0]) Mode <= ~Mode;
end else if (Y[9:0] == 10'h231) VSYNC <= 1'b1;
// Рамка
if (X[9:0] == 10'h1C8) FrameX <= 1'b1; else if (X[9:0] == 10'h148) FrameX <= 1'b0;
if (Y[9:0] == 10'h249) FrameY <= 1'b1; else if (Y[9:0] == 10'h22C) FrameY <= 1'b0;
// Стрелка
if (X[9:0] == 10'h200) ArrowX <= 1'b1; else if (X[9:0] == 10'h20E) ArrowX <= 1'b0;
if (Y[9:0] == 10'h07F) ArrowY <= 1'b1; else if (Y[9:0] == 10'h080) ArrowY <= 1'b0;
// Поле YM
if (X[9:0] == 10'h110) YMX <= 1'b1; else if (X[9:0] == 10'h120) YMX <= 1'b0;
if (Y[9:0] == 10'h000) YMY <= 1'b1; else if (Y[9:0] == 10'h080) YMY <= 1'b0;
// Поле PSG
if (X[9:0] == 10'h110) PSGX <= 1'b1; else if (X[9:0] == 10'h120) PSGX <= 1'b0;
if (Y[9:0] == 10'h0A0) PSGY <= 1'b1; else if (Y[9:0] == 10'h0C0) PSGY <= 1'b0;
// Поле BANK
if (X[9:0] == 10'h110) BankX <= 1'b1; else if (X[9:0] == 10'h120) BankX <= 1'b0;
if (Y[9:0] == 10'h100) BankY <= 1'b1; else if (Y[9:0] == 10'h120) BankY <= 1'b0;
// Снимаем строб записи
RamUpdate <= 1'b0;
// Снимаем запрос
WrPix[5:0] <= 6'h00;
// Выставляем адрес пикселя
RamAdr[15:0] <= (YMX & YMY) ? {14'h1000,Y[6:5]} : (PSGX & PSGY) ? 16'h7F11 : (BankX & BankY) ? 16'h6000 : (Y[8]) ? {1'b1,Y[7:1],X[7:0]} : (Mode) ? {3'h0,Y[6:2],Y[7],X[7:1]} : {3'h0,Y[7:2],X[7:1]};
end
endcase
end
end
// Конец
endmodule
Ресурсы (с учётом реализации HEX индикации и прочего не относящегося к VGA:
Синхронизация VGA намного проще , чем синхронизация PAL. Я совсем не удивлён, что её можно впихнуть в 32 макроячейки. Если вы напишете модуль, который может то же самое, что и мой , но влезает в 32 макроячейки , то я буду аплодировать стоя :)
У тебя не PAL, ибо цвета у тебя нет. В CPLD выше тоже формируется ТВ развёртка в ыормате 15625/50, но это не PAL. Я делал генератор для реального PAL, со вспышками и поднесущими. Там во-первых определённые частоты, а во-вторых нужен прям ЦАП, чтобы поднесущую можно было наложить на яркость. Это всё есть в моей реализации PPU NES на FPGA. Но я делал и для Спектрума тоже.
Если вы напишете модуль, который может то же самое, что и мой , но влезает в 32 макроячейки , то я буду аплодировать стоя :)
Такое в 32 не влезет только по причине обслуживания DRAM. Там мульииплексоров будет как раз на 32 LE. Но прилично ещё ужать ваш проект без потери функционала реально.
Полный видеосигнал можно генерить на r2r ЦАПе. Я делал на 8 бит. Если s-video, то можно 3 бита на цветовую поднесущую и 5 бит на яркость+синхросигнал. Поднесущая генерится с помощью DDS. Вот тут видео с цветовыми полосами: https://t.me/enginegger/107
Но ведь это ни разу не PAL. PAL - это стандарт цветного видеосигнала, а тут простой монохромный.
Не, чё годно, кто плавал - оценит! Однако где здесь PAL? Это называется композитным видеосигналом ПТС, по частотам и разложению его можно отнести к PAL\SECAM-ам, но там речь о цвете...
...предлагаю для повышения кликбейтности переименовать PAL в SECAM, дабы попавши на глаза, он вызывал в мозге зрителя сигнал "ШТА!?!?" ;-)
И, все-таки, несмотря на то, что все это очень интересно... В очередной раз, упоминание PAL здесь совсем неуместно. Статью однозначно следовало назвать как-то так: "ч/б видеоадаптер к ТВ на FPGA". С таким же успехом можно было бы взять идентичную временную диаграмму от SECAM, или "неидентичную" от NTSC. А до PAL здесь еще очень далеко: генератор поднесущей, квадратурные модуляторы, коммутаторы четных/нечетных строк, сумматоры,...
Вот тоже баловался с программными кодерами PAL, NTSC, SECAM на stm32:
Выше уже писали про это и я согласен с этим замечанием.
Ой, а запилите хотя-бы пост, кратенько по итогу, сколько времени оно кушает...
Что кушает? На что?
это же stm32 :-) там вычисления занимают конкретное время и можно прикинуть на что его ещё остаётся, например подцепить камеру, пройтись по кадру свёртками, подписи какие-то добавить, итд...
Это мне вопрос. :) В зависимости от типа кодера (PAL, NTSC, SECAM) по-разному. Для pal затраты на "видеоадаптер" составляют в пределах 15% процессорного времени, для NTSC - 13%. Для SECAM не смотрел, но, естественно, явно больше и косвенно можно оценить в сравнении с PAL. Если в PAL, например, демка дает до 36 fps, то в SECAM - до 20 fps. Соответственно, в SECAM "видеоадаптер" кушает в пределах 27% времени ядра.
Здесь надо смотреть еще и в сторону пропускной способности DMA. По дефолту 407 "камень" не потянет такую скорость (~18 Msps PAL, ~15 Msps NTSC, 13.5 Msps SECAM) для DAC. Плюс еще один поток 44100 Гц для звука. Плюс еще один DMA поток для обслуживания "видеоадаптера" ("заливка" и т.п.).
Линейность ЦАПа (соответствие расчётам) смотрели осциллографом?
Господа, это лайк! Лайк и репост!
Хорошо бы поставить на выход ЦАПа буфер (например, эмиттерный повторитель).
На UART тоже можно было бы FSM натянуть. ИМХО, было бы нагляднее и возможно по ресурсам по-приятнее.
Не нужен там FSM. Сдвиговый регистр, триггер запуска и BRR для формирования нового тактового домена.


Зачем? Там счетчик используется достаточно эффективно, автомат будет больше места занимать
Большое спасибо за статью, очень пригодилась.
Пробовал в 623 строке, как у вас делать импульс длительностью 2,35мкс - 120 тактов (при 50МГц)
21'd1990400: temp_sync_1 <= 1'b0;
21'd1990520: temp_sync_1 <= 1'b1;
и 4,7мкс, как по стандарту - разницы в отображении не было.
Искал что не так, когда поймал баг, который я так и не смог решить:
Если попытаться вывести чередуя строки белая-черная-белая, то вместо чередующихся строк - будет мерцание экрана.
Возможно это проблема конкретно на моем контроллере с видеовходом, но похоже что происходит восприятие по формату как на картинке:

И когда дошел до 623 строки, подумал что все, баг найден, но баг не ушел)
PAL видеоадаптер на FPGA с буфером кадра