Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3

в 15:28, , рубрики: CXD2545, diy или сделай сам, NRG, ps1, psx, Verilog
Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 1В первой части. Мы поверхностно посмотрели, как работает микросхема CXD2545. В второй смогли частично и с ошибками проэмулировать привод. И вот пришла пора закончить эту эпопею(не совсем). которая признаю честно казалась проектом на пару вечеров. В этой части мы всё переделаем, причем дважды, разгадаем загадку SENS, и попутно ещё решим кучу разных маленьких, но нужных моментов. А тем временем с момента последнего ковыряния эмулятора по ощущениям прошло, наверное, месяцев девять или даже десять, и да автор таки разродился. Ну а точней случился новый год, а это неделя когда можно спокойно заниматься своими проектами. Поэтому приставка была перевезена из города, где я работаю, туда, где я предпочитаю жить. С этого момента и начинается третья часть, о том как всё удалось сделать.

7. Попытка понять из за чего, все не работало

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 2Ночи зимой длинные, по этому продуктивные. Достал я приставку DE1 соединил всё это дело между собой. Запустил программу, попробовал, как работает, а в итоге всё как раньше. И тут решил я для теста вывести сигнал Underflow из модуля FIFO наружу. Хотя, казалось бы, зачем, SD карта работает на двадцать пять мегагерц, а данные идут из приставки всего на четырех при 2x скорости. Поставил аудиотрек на воспроизведение и стал наблюдать. И вот во время очередного щелчка, и пролетел сигнал Underflow. Вот так дела, значит, таки не успеваем, опять черепаха обогнала зайца. Попробовав разные варианты с оптимизацией кода, стало ясно, что так далеко не уехать.

8. Это всё сделано неправильно надо всё переделать

Посмотрев на Verilog код модулей, и частично на Си код. Как и любой программист, я пришёл к выводу, что дальше это терпеть нельзя. Всё неправильно и всё нужно переделать. Да так чтобы было правильно.

8.1 Я сделаю новую систему с DMA и оптимизацией

Одним из моих проектов на работе является система тестирования экранов разными хитрыми интерфейсами. Нас на проекте двое разработчик FPGA(привет Сергей). Он делает весь дизайн проекта в FPGA. И я занят написание прошивки под Microblaze(тоже, что и Nios II, но уже для Xilinx). В силу специфики мне иногда приходится вникать в то, как это всё внутри крутится, вертится. В общем, то при работе с этими проектами я и узнал о том, что есть ещё шины AXI-Stream. И практически её копия Avalon Stream, уже для систем от Altera. Посмотрев на принципы работы, мне показалось, что это прям идеальная шина для реализации дата интерфейса. Сигнал начала пакета, сигнал окончания. данные, и сигнал о том, что ведомое устройство готово получать данные. Так из системы улетели FIFO и появился SGDMA контроллер.

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 3

SG это значит ScaterGater. Достаточно хитрый DMA контроллер, который позволяет перекидывать байты из памяти в память, из памяти в устройство, из устройства в память. А также брать пакеты с шины Avalon Stream и кидать их в память, и что важно было для меня брать данные из памяти и кидать их в Avalon Stream. При этом работал со связкой дескрипторов, то есть можно было описать сразу несколько блоков, чтобы передавать пачку пакетов, не занимая этим процессор. То, что нужно. Также была добавлена статическая память(присутствует на DE плате), так как памяти на самом чипе не много, а данных я планировал хранить прилично. Ну и ещё память на чипе тратилась на SignalTap в общем ценный ресурс.

8.2 Под новую систему нужно заново писать вывод субканальных данных, и дата шины сидирома.

Вот тут я допустил очередную ошибку. Я решил, что это будет один модуль. Один модуль одна проблема. Немного подумав и посмотрев устройство SGDMA. я придумал следующее. По сигналу начала пакета, мы сбрасываем свой внутренний счетчик, и начинаем выбирать данные с шины SGDMA во внутренний регистр сразу по 32 бита(16 левый канал и 16 правый). Как только во внутреннем регистре появляются данные, мы их перекидываем в регистр, отправки. И оттуда последовательно их бросаем на шину данных сидирома, и когда набросаем 32а бита, то берем следующие 32 бита, и повторяем всё это. При этом считаем, как только получили 2352 байта. Перестаем перекидывать их в регистр отправки, а начинаем складывать в регистр субканальных данных. По приходу сигнала конца пакета, выставляем сигнал SCOR чтобы приставка могла забрать данные субканала. При этом шина у нас работает на 50 мегагерц, и она параллельная 32 бита, а приставка отдает на скорости четыре мегагерца, и последовательно. Да ещё у SGDMA есть фифо, в общем, у нас будет куча времени, чтобы отправить туда ещё один пакет и продолжить непрерывную передачу. Надеюсь, вы всё поняли, потому, что я по описанию с трудом понимаю, что смог это закодировать на Verilog. А если ты не можешь нормально объяснить, что написал это очень дурной знак. Пришлось, также переписать формат образа диска для SD карты он сильно упростился, теперь там были просто голые сектора 2352 байта, плюс 12 байт субканала. Что сильно упрощало формат и его анализ в случае чего. К тому же мне лень было развязывать клоки, и я всё сделал на детекторе фронтов. Оглядываясь назад могу сказать одно, я создал монстра.

Как то так оно работало

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 4

8.3 Чуда не случилось снова

Переписав Си часть, я проверил воспроизведение аудио. оно было идеально. Чтения TOC иногда затягивалось, но тоже работало. А вот загрузка игры не шла дальше заставки с логотипом PS. Немного протестировав, я обнаружил, что опять есть опустошения буфера на скорости 2х. Посмотрев как приставка орудует секторами, я решил, что возможно, где-то в коде есть временные затыки. Потому, что ну скорости карты явно должно хватать. И в порыве очередного приступа энтузиазма, наваял систему кэширования с опережающим чтением на N секторов, а также с кэшем уже отправленных секторов. И поигравшись с размером этих кэшей, добился того, что система сдвигалась чуть дальше логотипа PS. А именно уходила в черный экран. А дальше новогодняя неделя внезапно закончилась. Вести приставку в город где работаю не хотелось, там куча проводов, логический анализатор и всё это на «соплях» держится. В итоге ковыряние проекта свелось, к тому, что на выходных я пробовал пару гипотез, потом неделю думал, чтобы ещё попробовать. Ну и, в конце концов, опять всё заглохло. Потом случилась святая удаленка, и казалось бы, можно уже ковырять проект эмулятор каждый день. Но на работе оказался проект, который меня интересовал больше и он отнимал всё время.

8.4 Пускай сегодня не повезло, но игра продолжается

И так плавно случился декабрь. А в декабре я обычно беру месяц отпуска, и еду кататься на сноуборде в снежный Шерегеш. Чтобы по возвращению было время отдохнуть от отпуска, отметить Новый Год, ну итд. В тот год мне не повезло, и я получил синяк на всю грудь, болящую левую руку, и полное осознание того, что никакой активности на Новый Год уже не будет. Нужно восстанавливаться. В конце концов, руки дошли и до эмулятора. Посмотрев на это всё свежим взглядом, я через два дня осознал, что уже с трудом сам понимаю, как оно работает, или работало. Концепция один модуль одна проблема, привела к тому, что проблема оказалась неподъемной. Мало того я ещё внимательней присмотрелся, как работает наш дизайнер FPGA. А он любил, как можно больше использовать стандартных модулей, и как можно меньше писать что-то вручную. В итоге я просмотрел доступные ядра, и решил произвести декомпозицию проекта. И вот, что получилось:

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 5

Во первых, я отделил блок субканальных данных(CXD2545_SUBQ_OUT) от блока основных данных(CXD2545S) шины CDRom. При этом на них шёл один поток, который разделялся при помощи Avalon-ST Spliter. Перед сплитером, я поставил блок FIFO на 64 элемента, просто чтобы было время заслать следующий блок в DMA, и данные шли непрерывным потоком. Также в систему был добавлен второй клок. специально для шины данных сидирома. И добавил Dual Clock FIFO чтобы отвязаться от клока системы и “перейти” на клок данных сидирома. Не смотря на кажущиеся усложнения система получилась гораздо проще, и даже сейчас(через год после запуска) я могу понять как она работает.

8.5 Модуль CXD2545_SUBQ_OUT

Посмотрим на код модуля CXD2545_SUBQ_OUT отвечающий за субканал:

Код целиком

module CXD2545S_SUBQ (
	input  wire        clk,       //     Clock.clk
	input  wire        reset,     //     Reset.reset

	input  wire        in_eof,    // SUBQ_Sink.endofpacket
	input  wire        in_sof,    //          .startofpacket
	output wire        out_ready, //          .ready
	input  wire        in_valid,  //          .valid
	input  wire [31:0] in_data,   //          .data
	input  wire [1:0]  in_empty,  //          .empty

	input  wire        in_sqck,   //  SUBQ_OUT.export
	output reg         out_sqso,  //          .export
	output reg         out_scor,  //          .export		
	output wire        out_emph   //          .export		
);

assign out_ready = 1'b1;

reg [95:0] sub_q;
reg [31:0] sub_q_2;
reg [31:0] sub_q_1;

reg sqck_cur;
reg sqck_prev;
reg [3:0] scor_latch;

assign out_emph = 1'b0;

always @(negedge clk) begin

	if(in_valid == 1'b1) begin
		sub_q_1 <= in_data;
		sub_q_2 <= sub_q_1;
		
		if(in_eof == 1'b1) begin
			sub_q <= {sub_q_2, sub_q_1, in_data};
		end
	end
	
	if((in_valid == 1'b1) && (in_eof == 1'b1)) begin
			scor_latch <= 4'b1111;
			out_scor <= 1'b1;
	end else begin

		if(scor_latch > 0) begin
			scor_latch <= scor_latch - 1'b1;
		end else begin
			out_scor <= 1'b0;
		end
	
	end

	if((sqck_cur == 1'b0) && (sqck_prev == 1'b1)) begin
		out_sqso <= sub_q[95];
		sub_q <= {sub_q[94:0], 1'b0};
	end

	sqck_cur  <= in_sqck; 
	sqck_prev <= sqck_cur;
end

endmodule

Ну и по порядку:

input  wire        in_sqck,   //  SUBQ_OUT.export
output reg         out_sqso,  //          .export
output reg         out_scor,  //          .export		
output wire        out_emph   //          .export	

Сигналы шины субканала, in_sqck — входящий тактовый импульс, out_sqso — выход данных, out_scor — сигнал что субканальные данные получены(это не совсем так. но в целом можно считать что так), out_emph — информация о том, что используется предискажение трека, в реальной жизни я таких дисков не видел поэтому подзабил на реализацию, хотя она и очень примитивна.

assign out_ready = 1'b1;

Говорим шине Avalon-ST, что мы готовы принимать данные, а мы постоянно готовы это делать.

reg [95:0] sub_q;
reg [31:0] sub_q_2;
reg [31:0] sub_q_1;

Регистры: sub_q используется для хранения текущего значения субканала, sub_q_1 и sub_q_2 части где храним формируемые данные для субканала.

if(in_valid == 1'b1) begin
	sub_q_1 <= in_data;
	sub_q_2 <= sub_q_1;
	
	if(in_eof == 1'b1) begin
		sub_q <= {sub_q_2, sub_q_1, in_data};
	end
end

Если на шине Avalon-ST есть данные, то полученные данные мы записываем в sub_q_1, а то, что было в sub_q_1, переносим в sub_q_2. Напоминаю, что в Verilog всё работает параллельно. Поэтому, несмотря на такой порядок строк, выполнится всё именно так, как я описал.

Если же пришел флаг in_eof(конец пакета), то мы формируем регистр sub_q в которые записываем три последних полученных 32х битных слова. Напоминаю, до выхода из блока Always значения регистров не поменяются, поэтому у нас in_data текущее значение шины данных Avalon-ST, sub_q_1 — прошлое, sub_q_2 дважды прошлое.

if((in_valid == 1'b1) && (in_eof == 1'b1)) begin
		scor_latch <= 4'b1111;
		out_scor <= 1'b1;
end else begin
	if(scor_latch > 0) begin
		scor_latch <= scor_latch - 1'b1;
	end else begin
		out_scor <= 1'b0;
	end
end

Блок формирования scor. Если пришёл данные шины Avalon-ST валидны, и пришел флаг окончания пакета, переводим сигнал scor в высокое состояние, и устанавливаем счетчик в значение 15. Если условия не верны, данные не валидны или не конец пакета, декрементим счетчик пока он не станет равным нулю, и тогда опускаем сигнал scor. Код, по-моему, понятней, чем описание. И, наконец:

if((sqck_cur == 1'b0) && (sqck_prev == 1'b1)) begin
	out_sqso <= sub_q[95];
	sub_q <= {sub_q[94:0], 1'b0};
end

sqck_cur  <= in_sqck; 
sqck_prev <= sqck_cur;

Отвязывать данные субканала от основного клока системы особой нужды нет, поэтому тот просто сделана проверка, что, клок перешел из высокого состояния в низкое. И тогда на линию out_sqso выводится значение 96 бита из sub_q, а данные самого sub_q сдвигаются влево на 1 бит. По сути обычный сдвиговый регистр.

Как это работает в целом, данные в SGDMA у нас передаются блоками по 2352+12 байт. SGDMA в начале каждой передаче при передаче первого байта(слова там всётаки 32 бита) флаг SOF, а при передаче последнего слова флаг EOF. Двенадцать байт это три 32х битных слова. Этот модуль постоянно помнит два последних переданных слова, и при получении флага EOF из них и текущего слова формирует значения сдвиговый регистра на 96 бит. Который приставка и вытаскивает своим клоком.

8.6 Модуль CXD2545S_OUT

Теперь разберем модуль CXD2545S_OUT который отвечает за вывод данных на дата шину эмулируемой CXD2545:

Код целиком

module CXD2545S_OUT (
		input  wire        clk,      //   clock.clk
		input  wire        rst,      //        .reset_n
		output wire        in_ready, //   	   .ready
		input  wire        in_valid, //        .valid
		input  wire [31:0] in_data,  //        .data
		input  wire        in_sof,   //        .startofpacket
		input  wire        in_eof,   //        .endofpacket
		input  wire [1:0]  in_empty, //        .empty
		

		
		output reg	      cd_c2po,
		output wire 	   cd_clk,
		output reg	 	   cd_lr,
		output reg		   cd_data
	);

	assign in_ready = wait_data;	
	
	reg [47:0] cddata;
	reg [47:0] cddata_new;

	reg wait_data;
	
	
	reg [9:0] word_cnt;

	reg new_c2po;
	
	always @(negedge clk) begin
		if(rst == 0) begin
			wait_data <= 1'b1;
			word_cnt <= 10'h000;
			new_c2po <= 1'b0;
		end else begin
			
			if((wait_data == 1) && (in_valid == 1)) begin
				if(in_sof == 1'b1) begin
					word_cnt <= 10'h000;
				end else begin
					word_cnt <= word_cnt + 1'b1;
				end
					
				if((word_cnt < 587) || (in_sof == 1'b1)) begin
					cddata_new <= { in_data[31], in_data[31], in_data[31], in_data[31], in_data[31], in_data[31], in_data[31], in_data[31], in_data[31:16],
										 in_data[15], in_data[15], in_data[15], in_data[15], in_data[15], in_data[15], in_data[15], in_data[15], in_data[15:0]
									  };
					wait_data  <= 1'b0;
				end
				
			
			end

			if((cd_cnt == 0) && (clk_out == 1)) begin
				if(wait_data == 0) begin
					wait_data <= 1'b1;
					cddata 	 <= cddata_new;
					new_c2po  <= 1'b0;
				end else begin
					cddata 	<= 48'h000000000000;
					new_c2po <= 1'b1;
				end
			end

		end
	end

	reg [5:0] cd_cnt;
	reg clk_out;
	reg [47:0] cdout_data;
	reg c2po_delay;
	always @(negedge clk) begin
		clk_out <= ~clk_out;
		if(clk_out == 1'b1) begin
			cd_c2po <= c2po_delay;
			if(cd_cnt > 0) begin
				cd_cnt <= cd_cnt - 1'b1;
			end else begin
				cd_cnt <= 47;
				cdout_data <= cddata;
				c2po_delay <= new_c2po;
			end
			cd_lr   <= (cd_cnt < 24) ? 1'b0 : 1'b1;
			cd_data <= cdout_data[cd_cnt];
		end;
	end

	assign cd_clk = clk_out;
	
endmodule

Выходные сигналы шины данных:

output reg			cd_c2po,
output wire			cd_clk,
output reg			cd_lr,
output reg			cd_data

Тут уже знакомые нам сигналы cd_clk клок данных сидирома, cd_lr передаваемый канал левый правый, cd_data сами данные каналов. А вот сигнал cd_c2po, что-то новое. На любом физическом носители при чтении неизбежно время от времени возникают ошибки. Это особенность физического мира. Много ошибок CXD2545 может исправить сама, и мы про это даже не узнаем, но если ошибок слишком много то исправить их уже не получится. Тогда CXD2545 сообщает сигналом C2PO, что прочитанное слово содержит ошибки. Что делать с этим решает уже принимающая сторона, аудиоданные можно например интерполировать, бинарные данные интерполировать нельзя, но можно попытаться восстановить их, благо в секторе сеть для этого специальные поля(не всегда).

assign in_ready = wait_data;

Здесь как с субканальными данными всегда принимать данные не получится, получаем мы их сразу по 32 бита, а выводим по одному. Поэтому мы должны говорить шине Avalon-ST когда мы приняли от неё данные, и она может передавать следующее слово.

if(rst == 0) begin
	wait_data <= 1'b1;
	word_cnt <= 10'h000;
	new_c2po <= 1'b0;

если к нам пришёл ресет, то сбрасываем счетчик переданных слов, сбрасываем новое значение с2po и говорим что мы ждем новых данных.

if((wait_data == 1) && (in_valid == 1)) begin

Если мы ждем данных, и они появились на шине:

if(in_sof == 1'b1) begin
	word_cnt <= 10'h000;
end else begin
	word_cnt <= word_cnt + 1'b1;
end

Если это первое слово в пакете сбрасываем счетчик переданных слов, если нет то увеличиваем его на единицу.

if((word_cnt < 587) || (in_sof == 1'b1)) begin
	cddata_new <= { in_data[31], in_data[31], in_data[31], in_data[31], in_data[31], in_data[31], in_data[31], in_data[31], in_data[31:16],
			in_data[15], in_data[15], in_data[15], in_data[15], in_data[15], in_data[15], in_data[15], in_data[15], in_data[15:0]
			};
	wait_data  <= 1'b0;
end

Если счетчик слов меньше 587 или мы получили первое слово в пакете, то загружаем его в регистр cddata_new. А так как данные мы передаем по шестнадцать бит на канал, а CXD2545 передавал их по двадцать четыре бита, при этом первые девять бит всегда одинаковы. Мы размножаем первые девять бит каждого канала и таким способом получаем 24х битный регистр, очень удобная возможность FPGA. И после этого говорим, что данные нам теперь не нужны. Почему 587 у нас вектор передается как 2352+12, сначала данные сектора потом субканал. SGDMA настроен с шириной шины в 32 бита, то есть четыре байта. 2352/4 = 588. Но, учитывая особенность FPGA выполнять всё параллельно, счетчик будет инкрементится уже, после того как мы проверим его значение, поэтому и получается 587.

if((cd_cnt == 0) && (clk_out == 1)) begin
	if(wait_data == 0) begin
		wait_data <= 1'b1;
		cddata 	  <= cddata_new;
		new_c2po  <= 1'b0;
	end else begin
		cddata 	 <= 48'h000000000000;
		new_c2po <= 1'b1;
	end
end

Тут всё чуточку запутанней, cd_cnt это счетчик переданных бит идёт на уменьшение. Так вот когда он доходит до 0 и при этом клок в фазе фиксации сигналов. Мы проверяем если ли у нас данные для отправки. Если есть мы их закидываем в регистр cddata, выставляем флаг ошибок C2PO в ноль(нету ошибок) и ставим флаг что нам нужен очередной блок данных. Если же данных нет, мы кидаем в регистр данных нули, и ставим флаг С2PO, что у нас валят ошибки. В целом это не совсем верная работа этого флага, но оно работает.

Но в этом модуле у нас есть ещё и второй блок Always который правда привязан к тому-же событию, что и первый. Это потому, что этот модуль наследие “монстра”, а не написан с нуля как модуль субканальных данных.

always @(negedge clk) begin
	clk_out <= ~clk_out;
	if(clk_out == 1'b1) begin
		cd_c2po <= c2po_delay;
		if(cd_cnt > 0) begin
			cd_cnt <= cd_cnt - 1'b1;
		end else begin
			cd_cnt <= 47;
			cdout_data <= cddata;
			c2po_delay <= new_c2po;
		end
		cd_lr   <= (cd_cnt < 24) ? 1'b0 : 1'b1;
		cd_data <= cdout_data[cd_cnt];
	end;
end

Всё комментировать уже сил нет, да и думаю, вас тоже это уже, изрядно утомило. Вкратце, здесь генерируется выходная частота clk_out которая будет в два раза ниже чем входящая clk. Идёт подсчет переданных бит. И в момент передачи последнего, мы обновляем данные в регистре передачи данных. А также в зависимости от текущего передаваемого бита, генерируем сигнал CDLR. Ну и сигнал c2po_delay синхронизируется с передаваемыми данными точно до такта. Все эти заморочки нужны, для того чтобы у нас было прокэшировано одно слово. Это даст нам время на заполнение шины данных сидирома, пока будут лететь субканальные данные, и будет происходить старт новой передачи в SGDMA. В итоге проверил, что всё работает не хуже чем раньше(но и не лучше). И как не странно каникулы закончились, начались трудовые будни. Времени стало не хватать, да и появился ещё один более интересный проект. В общем, всё снова ложится в долгий ящик.

9. Моя попытка номер пять

Не уверен, что это была именно пятая попытка. Но время шло, пришла весна, а что можно делать весной, никогда не догадаетесь простудиться. И вот лежал я в конце апреля, и смотрел мимимишное прохождение FinalFantasy IX от 7Tiphs. И вроде смотрел про девятую часть, но там неоднократно упоминалась седьмая часть. А я, её не проходил, восьмую проходил, девятую тоже, даже ХроноКросс был пройден, а седьмая нет. А раз уж болею, ну а почему бы и нет. PS1 есть, игра есть ну, что ешё надо? Ах да у нас нерабочий привод, точней там уже деталей нет, и эмулятор сидирома не работает. Идея проходить на эмулятре приставки была отброшена, не спортивно. И так открываю долгий ящик.(Здесь я уже хотел закончить третью часть но, как то прогресса не видно пока, поэтому запасаемся чаем и едем дальше)

9.1 Откуда у нас опустошение буфера?

Действительно карта работает на целых мегагерц, приставка на 4х данные забирает. Ну, как может случаться опустошение? Да никак. Но, судя по выводу дебага, медленная черепаха раз за разом обгоняла шустрого зайца. И как-то получалось, что теория с практикой, расходится больше чем в теории. Пришло время заняться замерами всего этого безобразия. Добавил GPIO сигнал, чтобы можно было замерить время, и снял дамп анализатором:

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 6

Здесь должны быть картинка кота который говорит ачё всмысле, но у нас серьёзная статья и кота здесь не будет. А суть такова, что заяц у нас хромой, и да черепаха таки быстрей. Это, учитывая, что уже использовался вариант с чтением SDкарты через команду CMD18(READ_MULTIPLE_BLOCK), а не через чтения отдельных секторов. Как же так, не должно так быть, у нас же там целых двадцать пять мегагерц. Проверил код вроде всё должно быть норм, и подключился на шину SDкарты, а там вот такая картина:

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 7

Байт на шину отправляется/принимается на тех самых 25 мегагерцах, а потом откуда-то берется пауза, в четыре раза длинней чем посылка самого байта. Один полюс четыре, будет пять, двадцать пять поделить на пять, оказывается тоже пять. Итого можно сказать, что по факту SD карта работала на пяти мегагерцах. А вывод на приставку, шёл на четыре с половиной мегагерца. Но карта данные передаёт не всегда, там есть задержки CRC и прочее, вот и не хватало скорости. Я стал глядеть драйвер SPI из BSP Altera, пару мест там конечно можно было оптимизировать. Но глобального прироста это не давало. Видимо проблема в само SPI ядре, зачем-то оно там задерживает данные. И при всей оптимизации, что я смог сделать, я всё ещё не укладывался по скорости. Дальше было два варианта. Первый написать своё ядро для работы с SD картой, но это был явно не быстрый путь, в целом знаний вроде и хватает, чтобы это сделать. А вот желания это делать вообще нулевое. Второй вариант, проиграться с настройками ядра SPI. Вот только играться там особо не с чем. Только с битностью передач по шине. Поставил ради интереса 32 бита, и посмотрел, как выглядит передача логическим анализатором:

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 8

Однако. Теперь мы передам четыре байта, примерно за то время, что раньше передавали один байт. Но благодаря тому, что у Альтеры нельзя динамически из кода менять битность передач, нам теперь всегда надо оперировать 4х байтными словами, что добавляет много нехорошего в код работы с SD картой. В итоге сектор теперь читается за 4,6 миллисекунды. Это учитывая, что я перешел на команды чтения по одному сектору. Вместо чтения нескольких секторов. При чтении несколько секторов время можно было догнать до 3.3 миллисекунд, но там были ещё просторы для оптимизации, однако это усложняло код. И в итоге 4.6 это вполне нормальный результат. И о чудо опустошение буфера сошло на нет. Я вот сейчас думаю посмотри я сразу на скорость чтения, может и самый первый вариант эмулятора заработал бы. У меня начала грузиться Forsaken. Не идеально, не всегда с первого раза, но грузился.

9.2 Заставляем TOC читаться стабильно

Я проверил на всякий, стал ли стабильно читаться TOC, и нет, не стал. Посмотрев дампы анализатора, глаз снова зацепился за странное чтение субканальных данных:

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 9

Почему-то читается 18 бит и всё. И таких чтений очень много целый диапазон. При этом приставка читает TOC как будто дважды, Первый раз(скорей всего в этот момент идёт проверка на лицензионность диска) с этими аномалиями, второй без них. Перед этими странными чтениями всегда идёт команда 0x8. Повнимательней почитав, стало ясно, что это команда переводит SUBQ шину в режим выдачи служебной информации. Где можно читать всякие джиттеры, ошибки C1/C2 и прочие служебные данные. Я даже начал писать реализацию этой команды. Но, листая даташит, глаз зацепился за описание интерфейса SUBQ:

96-bit Sub Q is input, and if the CRC is OK, it is output to SQSO with CRCF = 1. In addition, the 80 bits are loaded into the parallel/serial register.

When SQSO goes high after SCOR is output, the CPU determines that new data (which passed the CRC check) has been loaded.

А вот это я упустил при реализации, у меня пин не уходит в состояние High. Ну, это то исправить уже точно не проблема:

if(in_eof == 1'b1) begin
	sub_q <= {sub_q_2, sub_q_1, in_data};
	out_sqso <= 1'b1; // <- Вот и всё исправление
end

После этого TOC читался без проблем и игра грузилась стабильно плохо. То есть повторяем плохо а не абы как.

9.3 Конвертация NRG в формат эмулятора

Захотелось экспериментов с другими играми. Образы PS1 игры обычно идут в формате CUE. Это вроде бы простой формат, но у него может быть куча разночтений. Может быть указан pre-gap а может нет, могут быть указаны индексы в треках, а могут и нет, итд. Мало того аудиотреки идут отдельными файлами в Wav формате, и тоже может быть отдельными файлами, а может одним файлом с указанием времени каждого трека. То есть надо было парсить отдельно CUE отдельно Wav. Учитывать кучу мелочей итд. Со стародавних времен я помню, что Nero5 могла в своём формате хранить все в одной куче. К тому же NRG бы явно бинарным файлом, а на Си проще работать с бинарными файлами, а не парсить текст. И Nero5 могла конвертировать CUE в свой формат. Описание формата было в википедии да и не только, и немного посидев с hex редактором, я написал утилиту, которая конвертирует, один или несколько файлов в образ понятный моему эмулятору. А потом ещё первый сектор отвел под заголовок, чтобы можно было хранить несколько игр на одной карте. Ознакомиться с исходниками можно здесь.

9.4 Эмуляция механики

Видно было, что проблемы начинаются при чтении секторов расположенных далеко друг от друга. Например если запустить на воспроизведение второй трек, то он достаточно быстро начнет воспроизводиться. А вот если сразу восьмой, то там будет куча команд позиционирования, и воспроизводиться может начать, например четвертый или пятый. Приставка сначала пытается сделать позиционирование при помощи работы каретки напрямую командой 0x2, потом при помощи команд 0x4 пытается компенсировать ошибку. Но либо есть ограничение на количество команд, либо на время выполнения команда. И в итоге она не успевает это сделать. Либо вообще отказываясь воспроизводить трек, либо воспроизводит не тот. С играми также пока сектора игры близко всё ок, но если стоят дальше и идёт позиционирование через 0x2 то начинаются глюки. В целом я уже читал, что приставка рассчитывает время нужное, чтобы каретке примерно приехать в нужную область, и даёт команду движения 0x2 в нужную сторону, потом на некоторое время она дает команду движения в противоположную сторону для компенсации инерции. Отсюда вырисовался очень простой план. После 0x2 запускаем таймер, и если пришла команда двигаться в противоположную сторону, останавливаем таймер. И по полученному времени выясняем примерно, где мы должны были оказаться. Дальше была попытка подобрать нужно число секторов в пересчете на время. Но выходила странная ситуация я мог подобрать например время для трека семь. Прям вот идеально. Но когда переходил на допустим трек двенадцать, я получал, тоже самое время. Я посмотрел логи которые снимал до отпайки CXD2545. Там время на разных треках разнилось. Например, можно посмотреть здесь файлы Forsaken_play_track_6.dsl и Forsaken_play_track_12.dsl У меня же оно было всегда примерно одинаково и сильно больше указанного. Остаётся вопрос почему? Я долго сидел и смотрел на команды в анализаторе. Это было не очень удобно. А по работе как раз шел проект, для которого, я писал вспомогательные тулины. Для отладки протокола. Которые генерили более удобочитаемый формат. Я адаптировал их под свою задачу. Посмотреть получаемые логи можно тут. Все, что было видно, что приставка даёт команду 0x2 потом 0xB потом через нужное время останавливает каретку.

Вот, например, для трека номер семь:

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 10

Или вот трек десять:

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 11

Ну всё как ожидалось, но почему у меня она стремится судя по времени аж за границу диска. Долго ли коротко ли(сразу скажу долго). Но перечитывая описание команды 0xB:

The traverse monitor count is set when the traverse status is monitored by the SENS output COMP and COUT.

Как-то оно мониторится через SENS выводя COMP и COUT. Опять этот SENS. Я уже даже нашёл к тому моменту прошивку SUB-CPU и поковырялся в ней. Да есть места, где вроде бы читается SENS. По прошивке SENS читался после посылки 8 бит по шине управления CXD2545, причем посылать XLAT было не обязательно. По даташиту зачастую первых 4х бит достаточно. Да там был список сигналов, которые можно получить с этого пина SENS. Некоторые при этом были продублированы физическими пинами чипа. И почему, после того как выбрали нужное значение SENS, оно может произвольно, где ему вздумается.

Мои мысли о пине SENS

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 12

Чтение описания как работают команды позиционирования, вообще вводили в диссонанс. Сами команды ориентировались на эти сигналы COUT и COMP. Как-то всё было странно(а может простуда делала свое дело). Создавалось ощущение что автосеквенсор прям отдельный модуль от системы позиционирования. И тут я натыкаюсь, что ещё бывала связка CXA1372+CXD2510Q и оказывается, причем за функции позиционирование отвечала как раз CXA1372, а за декодирование данных CXD2510Q. А наша CXD2545 возможно была получена упаковкой их в один корпус. Интересно конечно, но ответов не прибавилось. Однако чтение того, как же работает эта самая SENS в разных случаях, привело простой и банальной мысли. Это мультиплексор. Управляемый по шине команд. Поэтому свое состояние он может менять, не зависимо от клока. Ну-ка посмотрим, что у нас творится на дата шине, во время позиционирования:

Эмулятор CD-Rom для SonyPlaystation который я писал больше десяти лет. Часть 3 - 13

К итак непонятно зачем летающим, 0xA0 и 0x50 добавилась еще, какая то 0xC9, судя по даташиту 0x5X это FOK(Focus OK), 0xAX это GFS, а 0xCX это тот самый COUT. Ладно, с этим вроде бы ясно. А теперь что такое этот COUT. Одноименный пин говорит, что это:

Track count signal input.

Пока ничего не ясно, идём дальше:

Counts the number of tracks set with Reg B.
High when Reg B is latched, toggles each time the Reg B number is input by CNIN. While $44
and $45 are being executed, toggles with each CNIN 8-count instead of the Reg B number

Вот бы была возможность посмотреть анализатором что происходит, но чипа CXD2545 отпаян, и возможности такой нет. Зная как он работает, я сейчас могу понять, что это значит, но на тот момент я понять не смог. Но как я уже писал выше, я к тому моменту ковырял прошивку SUB-CPU и решил, может быть IDA, что ни будь подскажет:

Подсказка от IDA

Если просто, то в seek_dist_msb хранится рассчитанная дистанция. Дальше код закидывает на шину 0xC5(потому что от другой версии приставки, но это аналогично 0xC9 нас интересуют только старшие 4 бита). И проверяет, если относительно прошлого раза SENS выдает новое значение, ну был 0 стал 1, или наоборот. То значение seek_dist_msb декриментится. Когда дошло до 0, выходим из этой части кода. Получается COUT, генерирует переход из одного состояние в другое при прохождении определенного числа треков. Которое и задаётся командой 0xB(toggles each time the Reg B number is input by CNIN). Приставка значит не бездумно, водит кареткой ориентируясь на время, а мониторит сколько треков уже пролетело, и на основе этого решат когда останавливать каретку. Вот так сюрприз. И это значило одно, придётся делать функциональный блок для правильно работы SENS.

Конец третьей серии.

На сегодня хватит, в следующей части думаю мы закончим этот цикл статей осталось совсем немного. Выводов немного, не сотвори монстра, не отпаивай раньше времени, проверять нужно иногда даже очевидные вещи ну кто же знал особенности работы SPI от Альтеры. Если вам понравилось до скорой встречи, если нет извините за зря потраченное время.

Автор:
VBKesha

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js