Предыстория

Более 10 лет назад, когда было много свободного времени и чесались руки, я, вдохновившись чужими CarPC проектами, тоже собрал себе компьютер в машину (Москвич 214145). В то время подрабатывал системным администратором и мне стало очень удобно не таскать с собой каждый день ноутбук.

Москвич 214145
Москвич 214145

Размещение экрана было очень неудобным, но тогда не было такой доступной 3D печати и выкручивались как могли. На фото CarPC установлен в торпедо от Opel Vectra A.

Настоящее время

Шло время, все менялось и теперь у меня есть автомобиль Opel Astra H, больше знаний и опыта, но «дурная голова рукам покоя не дает» и я решил снова собрать CarPC.

Цель всего проекта не CarPC, как результат, а самообразование. На рынке много готовых решений, которые будут дешевле. Я лишь делюсь описанием небольшого этапа.

Консоль

Следующим образом выглядит торпедо в автомобиле Opel Astra H

Консоль Opel Astra H
Консоль Opel Astra H

Штатный монохромный графический экран имеет удобное местоположение на торпедо, можно его убрать, распечатать переходную рамку под новую матрицу, но есть большое НО: штатный экран (у меня это трехстрочный монохромный графический экран, именуемый как GID) является шлюзом между разными CAN-шинами. Если его отключить, то не будет работать климатическая система.

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

Можно спрятать GID за торпедо, но на него выводится много полезной информации и не хочется ее терять.

В 2020 году купил подходящую матрицу экрана и плату конвертера на контроллере RTD2662. Напечатал рамку, примерил и на этом мои силы кончились, так как ушли в домашний ремонт.

Рамка и LCD
Рамка и LCD

Прошло 5 лет, я снова вернулся к этому увлекательному проекту. Но пора заканчивать эту небольшую лирику и переходить к сути

Ломаем GID

Я решил и GID спрятать и информацию с него получать.

Можно ловить CAN-пакеты. Благодаря замечательному сообществу, именуемому как «Astra H CAN хакеры», декодировано и разобрано на биты огромное количество пакетов и параметров, которые в них содержатся.

Но я решил пойти другим путем: буду ловить данные, которые идут непосредственно на матрицу экрана. Таким образом плата от GID останется на родном месте.

Чтобы случайно не сломать мой экран, приобрел такой-же на разборке. Чтобы он включился на столе – необходима магнитола и соединить их по CAN шине. В таком случае экран включается при включении магнитолы.

Матрица экрана с кодовым названием 80509CN выглядит следующим образом:

80509CN
80509CN

Что примечательно – у нее два шлейфа. Ранее мне такие не попадались. По всевозможным номерам, которые есть на матрице – не ищется никакой документации. Значит буду проводить обратную разработку.

Матрица подключается к основной плате GID-а.

Плата GID
Плата GID

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

Установлен микроконтроллер NEC v850eca2. Пролистав документацию, не увидел специализированного контроллера для экрана. Скорее всего экран сидит или на внешней шине данных, или на GPIO.

Не стал реверсить схему подключения экрана к микроконтроллеру, а решил пробежать по выводам. Внешне похоже, что оба разъема экрана имеют одинаковую распиновку. Прозвонил выводы двух разъемов на взаимное соединение, отметил КРАСНЫМ – линии, которые попарно соединены, а ЖЕЛТЫМ – которые у каждого разъема индивидуальны.

Подключение
Подключение

И действительно, большее число выводов подключено параллельно.

Случайно наткнулся в интернете на информацию, что если перевернуть экран на 180 градусов и подключить, то он тоже будет работать (внутри корпуса есть выступы, препятствующие этому, но один человек отколол часть матрицы и у него перестала работать нижняя половина, на которой наибольшее количество важной информации. Он удалил ограничители и перевернул экран)

Получается, что внутри экрана ДВА одинаковых контроллера. Что сильно облегчает дальнейшую работу.

Далее я взял осциллограф и начал смотреть, как себя ведут разные линии.

На фотографии ниже сделал отметки: если напряжение стабильное, то указал уровень. Сигнал положительной полярности (преобладает низкий уровень), то отметил как «p», сигнал отрицательной полярности (преобладает высокий уровень), то отметил как «n», если идет постоянный меандр, то указал частоту и заполнение. Цифровые сигналы имеют лог. Уровень 5в.

Подключение выводы
Подключение выводы

Из этой картины явно выделяется шина данных на 8 бит и сигналы выбора верхнего/нижнего контроллера (единственный сигнальный вывод на разъеме, который не объединен).

Подключил логический анализатор DSLogic, начал смотреть обмен на шине.

Логический анализатор
Логический анализатор

Сняв лог с выборкой 10ns в течение 1с, начал его просматривать и обнаружил интересные данные

Надпись
Надпись

Битовые данные на 8-битной шине повторяют надпись, которая отображается на экране.

Также увидел сигналы выбора верхнего/нижнего контроллера (на картинке - зеленым)

Анализ
Анализ

Сигнал отрицательной полярности, скорее всего – nWE, по его спаду (заднему фронту) контроллер защелкивает данные (на картинке - красным)

nWE
nWE

Далее по диаграмме нашел место, где начинается выборка нижнего контроллера.

После перехода должны идти команды. И действительно, последняя неизвестная линия – является сигналом выбора данные/команда. На картинке – оранжевым.

A0
A0

Подсчитал разрешение матрицы

Размер
Размер

Получилось, что GID у Astra H имеет разрешение 218х138 точек.

Анализируем полученные данные

Небольшая часть работы с паяльником уже проделана.

Предполагаю, что матрица экрана изготовлена под заказ GM, но стоит ли внутри известный контроллер или какой-то свой? Что ж, в этот момент я надеялся, что если контроллер и проприетарный, то писался с взглядом на существующие.

За полный цикл выдачи данных на одну верхнюю половину экрана, происходят 8 посылок следующего содержания: 3 байта команд и 208 байт данных.

Рассмотрим эти 3 байта команд. На картинке начало 4х посылок

4 посылки
4 посылки

В них последовательно инкрементируется единичка в первом командном слове. Таким образом становятся понятны MSB и LSB шины данных.

Полученная распиновка экрана на картинке:

Распиновка
Распиновка

Но вернемся к нашим четырем посылкам. Как же удобны цифровые анализаторы с программными декодерами.

Декодер
Декодер

Но посмотрим не только их, а все команды

8 команд перед посылками для верхней половины экрана:

[0xB0, 0x10, 0x05]

[0xB1, 0x10, 0x05]

[0xB2, 0x10, 0x05]

[0xB3, 0x10, 0x05]

[0xB4, 0x10, 0x05]

[0xB5, 0x10, 0x05]

[0xB6, 0x10, 0x05]

[0xB7, 0x10, 0x05]

8 команд перед посылками для нижней половины экрана:

[0xB0, 0x12, 0x05]

[0xB1, 0x12, 0x05]

[0xB2, 0x12, 0x05]

[0xB3, 0x12, 0x05]

[0xB4, 0x12, 0x05]

[0xB5, 0x12, 0x05]

[0xB6, 0x12, 0x05]

[0xB7, 0x12, 0x05]

Ширина экрана 218, но данных приходит только на 208. Но и на экране слева и справа отступы по 5 пикселей. Скорее всего 3-й байт данных – номер столбца, а младшие 4 бита в первом байте команды – номер «строки». Почему в кавычках – данные выдаются сразу на 8 строк. Получается, что это больше указатель на сектор строк.

После вышеуказанных 16 больших команд с данными приходит «дозаполнение» пустых ячеек.

Дозаполнение
Дозаполнение

Тут видно, что половины экрана «независимы». Приходят команды для каждой половинки, а затем данные идут сразу на две половинки.

Предположу, что внутри основного микроконтроллера NEC v850eca2 буфер видеокадра рассчитан на 208x128 точек, так как некоторые строки и столбцы прячутся за рамку экрана.

После полного заполнения «видимой части экрана», «дозаполнение» приходит на все оставшиеся пиксели (в том числе обрезки строк, шириной по 5 пикселей).

Экран обновляется раз в секунду.

Помимо буфера обновления экрана – иногда по шине приходят команды конфигурации экрана.

Из всех вышеперечисленных данных я нашел наиболее подходящий, по командам и описанию, контроллер - ST7565, только в моем случае – разрешение экрана побольше.

Но не все команды удалось найти в описании. Вот пример конфигурации экрана:

Конфиг
Конфиг

В машине, в настройках экрана можно включать инверсию. Теперь я понял, что передача буфера данных на экран не меняется – меняется только конфигурация для включения инверсии.

Пишем программный декодер

В целом, собранных данных достаточно, чтобы написать примитивный декодер. Его я буду писать на C# и выводить графику.

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

Выгружу его в формат csv и буду читать из файла.

CSV
CSV

Алгоритм работы программы, следующий:

С делал PictureBox размером 872x512 пикселей – в 4 раза больше, чем реальное разрешение экрана, буду выводить точки размером 4х4 (чтобы было не мелко). Сам видеобуфер размером 218x16 байт.

В обработчике PictureBox_Paint() буду отрисовывать данные из видеобуфера.

В основном цикле программы я пробегаю по всем строкам входного файла, разбираю значения, храню прошлое состояние nWE. Ищу спад (задний фронт), т.е. если старое значение 1, а новое 0, то выполняю действие.

Завел две глобальные переменные на номер строки и столбца.

Дополнительно у меня есть флаг валидности данных.

Фильтрую первую пришедшую команду по маске 0xF8. Если команда по маске 0xB0, беру из нее номер строки по маске 0x07. Если при этом выбрана верхняя часть экрана, то в номер строки пишу этот номер, если выбрана нижняя, то этот номер + 8. Следующие две команды – вспомогательные. Вторую не обрабатываю, из третьей беру номер столбца. Если эти три команды прошли, то поднимаю флаг валидности данных, что следующие данные можно писать в буфер. Каждая запись инкрементирует номер столбца.

Если в первую пришедшую команду по маске 0xF8 пришла команда отличная от 0xB0, то снимаю флаг валидности данных.

На картинке результат работы программного декодера.

Программный декодер
Программный декодер

Пишем аппаратный декодер

Для аппаратного декодера я использовал ПЛИС altera cyclone iv ep4ce10. У меня как раз лежит одна с выгоревшим jtag портом, но без проблем считывает конфигурацию с SPI флеш-памяти. У нее на борту есть аппаратные блоки двухпортовой SRAM-памяти. Это очень полезно, так как чтение и запись будут производиться разными модулями.

Так как уровни логических сигналов GID 5в, а у моей ПЛИС 3.3в, были использованы микросхемы для согласования уровней. У них есть сигнал OE. Его подключил к выходу ПЛИС, где генерирую постоянную лог «1». ПЛИС запускается не сразу, а сначала считывает конфигурацию. Чтобы в этот момент не было конфликтов (согласователи с автоматическим определением направления) – решил сделать так.

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

Скрытый текст
module decoder(
	input         clk,
	
	input  [7:0]  data,
	input         nWE,
	input         DC,
	input         CS_up,
	input         CS_down,
	
	output [12:0] mem_address,
	output [7:0]  mem_data,
	output        mem_we,
	output        mem_clk
);

reg [1:0]  cmd_pos = 2'd0;
reg        decoder_mem_we = 1'b0;
reg [7:0]  col_adr = 8'h00;
reg [3:0]  row_adr = 4'h0;
reg        reg_mem_clk = 0;
reg        mem_clk_cnt = 0;


always @(posedge nWE) begin
	if(DC == 1'b0) begin
		if(cmd_pos == 2'd0) begin
			if((data & 8'hF8) == 8'hB0) begin
				if(CS_up) begin
					row_adr <= data & 8'h0F;
					col_adr <= 0;
				end else if(CS_down) begin
					row_adr <= (data & 8'h0F) + 8'd8;
					col_adr <= 0;
				end
				cmd_pos <= 1;
			end 
			decoder_mem_we <= 1'b0;
		end else if(cmd_pos == 2'd1) begin 
			col_adr <= col_adr | (data & 8'h01)<<4;
			cmd_pos <= 2'd2;
		end else if(cmd_pos == 2'd2) begin
			col_adr <= col_adr | (data & 8'h0F);
			cmd_pos <= 2'd0;
			decoder_mem_we <= 1'b1;
		end
	end
	
	if(decoder_mem_we & DC) begin
		col_adr <= col_adr + 1;
	end
end

assign mem_we = decoder_mem_we & DC & ~nWE;

assign mem_address = {row_adr[3:0], col_adr[7:0]};
assign mem_data = data;

always @(posedge clk)begin
	if(~nWE) begin
		if(~mem_clk_cnt) begin
			if(~reg_mem_clk)begin
				reg_mem_clk <= 1'b1;
			end else begin
				reg_mem_clk <= 1'b0;
				mem_clk_cnt <= 1'b1;
			end
		end
	end else begin
		reg_mem_clk <= 1'b0;
		mem_clk_cnt <= 1'b0;
	end
end

assign mem_clk = reg_mem_clk;

endmodule

Код синтезировался и даже работает

Синтез декодера
Синтез декодера

Для отладки выводил данные, которые пишутся в SRAM на внешние выводы, но потом понял, что сразу в железе отладить не получится и придется писать тестбенч.

Взял добрый уютный iverilog. Очень мало времени работаю с ПЛИС, не знаю многих подходов, потому вместо чтения csv файла средствами iverilog – в моем программном декодере написал конвертер входных данных в строки для тестбенча (да закидают меня тапками, сейчас я уже знаю как правильно, но на тот момент это было быстрое рабочее решение)

Получил строки вида:

тестбенч
тестбенч

И с головой ушел в моделирование

Моделирование
Моделирование

В результате – модуль декодера стучится в SRAM, генерирует необходимые сигналы: адрес, данные, разрешение записи и clk для защелкивания.

Пишем аппаратный вывод данных

Вывод данных должен быть универсальным. В том числе, чтобы в машину (кто еще не потерял нить рассуждений и дочитал до этого момента, я же делаю все это для машины) можно было поставить китайскую магнитолу, а ее экран перенести наверх (если я не доберусь до постройки CarPC). Обычно у магнитол из доступных разъемов – только CVBS, а у используемого мной контроллера экрана RTD2662 доступно до 4х аналоговых видеовходов, то его и будем использовать.

Первым делом полез искать готовые реализации. Наткнулся на статью https://habr.com/ru/articles/882626/ и для теста реализовал этот вариант, но с небольшими изменениями – использовал 8-битный DAC R2R (но на каждый выход использовал 2 порта ввода-вывода, чтобы повысить ток. В общей сложности использовал 16 портов), а сигнал синхронизации - суммировал к текущему уровню.

CVBS
CVBS

Спасибо автору - статья очень помогла быстро запустить CVBS и написать его заново под свои нужды.

Начал разбираться и заметил особенность. Что в моей реализации, что в реализации из статьи: если попытаться передать чередующиеся строки белая-черная-белая, то экран вместо строк начинает мерцать. Если 2 строки белые – 2 черные – 2 белые, то все ОК, но разрешение падает в 2 раза (мне не критично, т.к. мне нужно вообще 128 строк).

Под рукой всегда держал шпаргалку:

Composite
Composite

Затем сделал некоторые свои тесты с нужным разрешением экрана

TEST
TEST

Таким получился мой модуль вывода Monochrome Composite Video.

Скрытый текст
module PAL(
	input  clk,
	output [15:0]mem_address,
	input  mem_data,
	output mem_clk,
	
	output [7:0]video_out
);

wire sync;
reg frame = 0;

wire [7:0]video_in;


assign video_in = (mem_data) ? BRIGHT : 8'd0; //test

wire [7:0]video;
assign video = (frame) ? video_in : 8'd0;
assign video_out = (sync) ? (video + 8'd25) : 8'd0;

localparam BRIGHT             = 8'h80;

localparam BROAD_SYNC_SECTION1 = 5'd0; //5
localparam SHORT_SYNC_SECTION1 = 5'd1; //5
localparam EMPTY_LINE1         = 5'd3; //18
localparam FULL_LINE1          = 5'd4; //287
localparam SHORT_SYNC_SECTION2 = 5'd5; //5
localparam BROAD_SYNC_SECTION2 = 5'd6; //5
localparam SHORT_SYNC_SECTION3 = 5'd7; //5
localparam EMPTY_SYNC_SECTION  = 5'd8; //1
localparam EMPTY_LINE2         = 5'd10; //17
localparam FULL_LINE2          = 5'd11; //287
localparam SHORT_LINE          = 5'd12; //1
localparam SHORT_SYNC_SECTION4 = 5'd13; //5

localparam X_END_HALF_LINE     = 12'd1600;
localparam X_END_FULL_LINE     = 12'd3200;

localparam IMAGE_START_CLK     = 12'd573;//12'd520;
localparam IMAGE_STOP_CLK      = 12'd2971;//12'd2923;  2644-900 = 8clk/1px

reg [12:0]x_end_counter        = 12'd0;
reg [12:0]y_end_counter        = 12'd0;
reg [12:0]sync_start_counter   = 12'd0;

reg [12:0]x_clk_count          = 12'd0;
reg [12:0]y_clk_count          = 12'd0;

reg [5:0] current_state        = 6'h00;
reg [5:0] next_state           = 6'h00;
reg [8:0] count_states_max     = 9'h000;

reg [12:0]image_clk_count      = 12'd0;

reg [9:0]x_pos = 10'h000;
reg [3:0]x_tmp_pos = 4'd0;

//assign x_pos[9:0] = image_clk_count[12:3];

wire [12:0]y_tmp_clk_count;
assign y_tmp_clk_count = y_clk_count - 16;
wire [9:0]y_pos;
assign y_pos[9:0] = y_tmp_clk_count[10:1];

always @(posedge clk) begin
	if(x_clk_count >= x_end_counter - 1) begin
		frame <= 0;
		x_clk_count     <= 12'd0;
		image_clk_count <= 12'd0;
		x_pos <= 10'd0;
		if(y_clk_count >= y_end_counter - 1) begin
			current_state <= next_state;
			y_clk_count <= 12'd0;
		end else begin
			y_clk_count <= y_clk_count + 1;
		end
		
	end else begin 
		x_clk_count <= x_clk_count + 1;
		if(x_clk_count >= IMAGE_START_CLK && x_clk_count < IMAGE_STOP_CLK && (current_state == FULL_LINE1 || current_state == FULL_LINE2))begin
			if(y_clk_count >= 16 && y_clk_count < 272) //16-271 ; 256/64 4line/1px
			begin
				frame <= 1;
			end else begin
				frame <= 0;
			end
			//if(image_clk_count )
			
			if(x_tmp_pos == 4'd10)begin
				x_tmp_pos <= 0;
				x_pos <= x_pos + 1;
			end else begin
				x_tmp_pos <= x_tmp_pos + 1;
			end
			
			image_clk_count <= image_clk_count + 1;
		end else begin
			frame <= 0;
		end
		
	end
end

assign sync = (x_clk_count >= sync_start_counter) ? 1'b1 : 1'b0;

always @(*) begin
	case (current_state)
		BROAD_SYNC_SECTION1: begin //0 - 2.5
			next_state         = SHORT_SYNC_SECTION1;
			x_end_counter      = X_END_HALF_LINE;
			y_end_counter      = 12'd5;
			sync_start_counter = 12'd1365;
		end
		SHORT_SYNC_SECTION1: begin //2.5 - 5
			next_state         = EMPTY_LINE1;
			x_end_counter      = X_END_HALF_LINE;
			y_end_counter      = 12'd5;
			sync_start_counter = 12'd120;
		end
		EMPTY_LINE1: begin //7-23
			next_state         = FULL_LINE1;
			x_end_counter      = X_END_FULL_LINE;
			y_end_counter      = 12'd18;
			sync_start_counter = 12'd235;
		end
		FULL_LINE1: begin //24-310
			next_state         = SHORT_SYNC_SECTION2;
			x_end_counter      = X_END_FULL_LINE;
			y_end_counter      = 12'd287;
			sync_start_counter = 12'd235;

		end
		SHORT_SYNC_SECTION2: begin //311-312.5
			next_state         = BROAD_SYNC_SECTION2;
			x_end_counter      = X_END_HALF_LINE;
			y_end_counter      = 12'd5;
			sync_start_counter = 12'd120;
		end
		BROAD_SYNC_SECTION2: begin //312.5-315
			next_state         = SHORT_SYNC_SECTION3;
			x_end_counter      = X_END_HALF_LINE;
			y_end_counter      = 12'd5;
			sync_start_counter = 12'd1365;
		end
		SHORT_SYNC_SECTION3: begin //316-317.5 
			next_state         = EMPTY_SYNC_SECTION;
			x_end_counter      = X_END_HALF_LINE;
			y_end_counter      = 12'd5;
			sync_start_counter = 12'd120;
		end
		EMPTY_SYNC_SECTION: begin //317.5
			next_state         = EMPTY_LINE2;
			x_end_counter      = X_END_HALF_LINE;
			y_end_counter      = 12'd1;
			sync_start_counter = 12'd0;
		end
		EMPTY_LINE2: begin
			next_state         = FULL_LINE2;
			x_end_counter      = X_END_FULL_LINE;
			y_end_counter      = 12'd17;
			sync_start_counter = 12'd235;
		end
		FULL_LINE2: begin
			next_state         = SHORT_LINE;
			x_end_counter      = X_END_FULL_LINE;
			y_end_counter      = 12'd287;
			sync_start_counter = 12'd235;
		end
		SHORT_LINE: begin
			next_state         = SHORT_SYNC_SECTION4;
			x_end_counter      = X_END_HALF_LINE;
			y_end_counter      = 12'd1;
			sync_start_counter = 12'd235;
		end
		SHORT_SYNC_SECTION4: begin
			next_state         = BROAD_SYNC_SECTION1;
			x_end_counter      = X_END_HALF_LINE;
			y_end_counter      = 12'd5;
			sync_start_counter = 12'd120;
		end
	endcase
end

assign mem_address[15:0] = {y_pos[6:3], x_pos[7:0], y_pos[2:0]};

assign mem_clk = clk & frame;

endmodule

Много лишнего, не оптимально, так что прошу строго не судить.

Модуль верхнего уровня и память

Память двухпортовая. Причем на вход она 8-битная, а на выход – 1-битная.

Отлаживать и моделировать пришлось долго, так как было много глупых ошибок, вроде такой:

Ошибка
Ошибка

Так как моделирую в iverilog, то накидал простую модель

Скрытый текст
module SRAM(
	input	[12:0]  address_a,
	input	[7:0]   data_a,
	input	        clock_a,
	input	        wren_a,
	output	[7:0]   q_a,
	
	input	[15:0]  address_b,
	input	[7:0]   data_b,
	input	        clock_b,
	input	        wren_b,
	output	        q_b
	
);

reg [7:0]mem[8191:0];

reg [7:0]r_q_a = 8'h00;
assign q_a = r_q_a;
reg      r_q_b = 1'b0;
assign q_b = r_q_b;

always @(posedge clock_a) begin
	r_q_a <= mem[address_a];

	if(wren_a == 1'b1)begin
		mem[address_a] <= data_a;
	end
end

always @(posedge clock_b) begin
	
	r_q_b <= mem[address_b[15:3]][address_b[2:0]];
	
	if(wren_b == 1'b1)begin
		mem[address_b] <= data_b;
	end
end

endmodule

Вот как выглядит мой TOP-модуль

Скрытый текст
module GID(
	input clk,
	input [7:0]lcd_data,
	input lcd_nWE,
	input lcd_DC,
	input lcd_CS_up,
	input lcd_CS_down,
	output [7:0]LED,
	output driver_oe,
	output [7:0]video_out,
	output [7:0]video_out2
);

assign driver_oe = 1'b1;

wire [7:0]video;
assign video_out = video;
assign video_out2 = video;

wire [7:0] test_ledout;
assign test_ledout[7:0] = address_a[7:0];
assign LED = ~test_ledout;

wire [12:0] address_a;
wire [7:0]  data_a;
wire        wren_a;
wire        mem_clk_a;

wire [15:0] address_b;
wire        data_b;
wire        mem_clk_b;

decoder decoder(
	.clk         ( clk         ),
	
	.data        ( lcd_data    ),
	.nWE         ( lcd_nWE     ),
	.DC          ( lcd_DC      ),
	.CS_up       ( lcd_CS_up   ),
	.CS_down     ( lcd_CS_down ),
	
	.mem_address ( address_a   ),
	.mem_data    ( data_a      ),
	.mem_we      ( wren_a      ),
	.mem_clk     ( mem_clk_a   )
);

SRAM SRAM(
	.address_a   ( address_a   ),
	.data_a      ( data_a      ),
	.clock_a     ( mem_clk_a   ),
	.wren_a      ( wren_a      ),
	.q_a         (  ),
	
	.address_b   ( address_b   ),
	.data_b      ( ),
	.clock_b     ( mem_clk_b   ),
	.wren_b      ( 1'b0        ),
	.q_b         ( data_b      )
);

PAL PAL(
	.clk         ( clk         ),
	
	.mem_address ( address_b   ),
	.mem_data    ( data_b      ),
	.mem_clk     ( mem_clk_b   ),
	
	.video_out   ( video       )
);

endmodule

Схема в блоках выглядит даже не плохо, если не залезать внутрь

Синтез
Синтез

Результат

Ну и вот что мы имеем на выходе:

Понимаю, что все не очень оптимально, сделано на коленке за 3-4 вечера, но я очень доволен результатом и много чего полезного узнал.

Спасибо за внимание.

Комментарии (1)


  1. Z55
    08.06.2025 16:17

    Круто!