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

axpywgsot5bpmjpwoqrjnqrambk.jpegЯ надеялся, да что говорить был уверен, что уложусь в три части. Однако всё-таки будет четвертая. В прошлой части мы добились стабильно считывания TOC. И нестабильно, глючного, но всё-таки запуска игр. А также разобрались с тем, что же такое SENS, и как именно приставка выполняет позиционирование при помощи команд управление кареткой. Нам осталось реализовать модуль для эмуляции SENS. И решить маленькие, но важные мелочи. Если вам всё ещё интересно, чем всё это закончится, добро пожаловать под кат.

Ссылки для тех, кто пропустил первые три части


9.5 Эмуляция SENS


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

Реализация SENS
module CXD2545_SENS(
		input sclk,
		
		input clk,
		input data,
		input xlat,
		
		input [15:0] sens_data,
		
		output reg sens
);

reg [3:0] cnt;
reg [7:0] shift_reg;
reg [3:0] select_reg;

reg prev_clk;
reg prev_xlat;

always @(posedge sclk) begin

	if((prev_xlat == 1'b1) && (xlat == 1'b0)) begin
		cnt <= 0;
		shift_reg[7:0] <= 0;
	end else begin
		if((prev_clk == 1'b0) && (clk == 1'b1)) begin
			shift_reg[7:0] <= {data, shift_reg[7:1]};
			if(cnt < 7) begin
				cnt <= cnt + 1'b1;
			end else begin
				select_reg <= {data, shift_reg[7:5]};
				cnt <= 0;
			end
		end
		
	end

	prev_xlat <= xlat;
	prev_clk <= clk;
	sens <= sens_data[select_reg];

end

endmodule


Ничего сложного тут нет, всё это я уже описывал и раньше. Только немного уточнений:
Модуль набирает данные по восемь бит, и в зависимости оттого, что получил, устанавливает на выход sens один из битов sens_data. Причем последнее действие делается каждый такт.
Чтобы управлять статусом этих пинов в систему был добавлен ещё один модуль PIO, на который и были подключены почти все пины SENS, кроме COUT:

CXD2545_SENS sens_inst(
	.sclk(CPU_CLK),
	.clk(REG_CLK),
	.data(REG_DATA),
	.xlat(REG_XLAT),
	.sens_data({SENS_PIN[15:13], trc_toggle, SENS_PIN[11:0]}),
	.sens(sens_out)
);


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

Более простой и возможно правильный вариант
module CXD2545_SENS(

		input clk,
		input data,
		input xlat,
		
		input [15:0] sens_data,
		
		output wire sens
);

reg [3:0] cnt;
reg [7:0] shift_reg;
reg [3:0] select_reg;

always @(posedge clk or negedge xlat) begin
	if(xlat == 1'b0) begin
		cnt <= 0;
		shift_reg[7:0] <= 0;
	end else begin
		shift_reg[7:0] <= {data, shift_reg[7:1]};
		if(cnt < 7) begin
				cnt <= cnt + 1'b1;
		end else begin
				select_reg <= {data, shift_reg[7:5]};
				cnt <= 0;
		end
	end
end

assign sens = sens_data[select_reg];

endmodule


9.5 Эмуляция COUT


Посмотрев снятые логи, я увидел, что приставка смотрит на COUT только когда использует «дальнее позиционирование» при помощи каретки. Поэтому было принято решение. Гнать туда меандр постоянно, но надо было угадать с частотой. Между двумя запросами COUT был промежуток примерно две миллисекунды, это где-то 250 герц максимум, что успеет воспринять приставка в таком темпе. Но это прям оочень максимум. Поэтому сразу сделал так чтобы можно было настраивать его, и по-хорошему наверно имело смысл повесить его на шину AvalonMM, но было лень. В итоге родился такой модуль:

CXD2545_TRACK_COUNTER
module CXD2545_TRACK_COUNTER(
	input clk,
	input [15:0] div,
	input [15:0] toggle_cnt,
	input trigger,
	
	output reg toggle_clk,
	output reg [31:0] track_count
);

reg [32:0] cnt_50;
reg [32:0] cnt_div_50;
reg prev_trigger;
reg [30:0] trc_cnt;


reg trk_clk;

always @(posedge clk) begin
	if(cnt_50 < div) begin
		cnt_50 <= cnt_50 + 1'b1;
	end else begin
		cnt_50 <= 0;
		trk_clk <= ~trk_clk;
	
		if(cnt_div_50 < 256) begin
			cnt_div_50 <= cnt_div_50 + 1'b1;
		end else begin
			cnt_div_50 <= 0;
			toggle_clk <= ~toggle_clk;
		end
	
		if((prev_trigger == 1'b1) && (trigger == 1'b0)) begin
			track_count <= {1'b1, trc_cnt};
		end else if((prev_trigger == 1'b0) && (trigger == 1'b1)) begin
			trc_cnt <= 0;
			track_count <= 0;
		end else begin
			trc_cnt <= trc_cnt + 1'b1;
		end

	
	prev_trigger <= trigger;
	end
end
endmodule


Как оно работает. На вход подается тактовая частота (у меня это 50 мегагерц), при помощи управляемого входа div мы получаем вторую частоту. Это как бы та частота, с которой мы пролетаем треки. Дальше при помощи toggle_cnt мы ещё раз делим полученную частоту, и получаем сигнал toggle_clk которые и идёт уже в модуль SENS. Но так как нам самим в коде надо будет знать, сколько треков мы насчитали, был добавлен сигнал trigger. И если был переход из низкого состояния в высокое, мы обнуляем счетчик треков, и начинаем их считать. А если был, наоборот, из высокого в низкий мы переносим подсчитанные треки в регистр track_count. Причем сам счетчик треков у нас размером в 31 бит, а 32ой бит используется как флаг того, что данные в регистре верные. Зачем так сделано? Мне было лень вешать этот модуль на шину AvalonMM и я его подключил к процессору через модули PIO. А так как у меня модуль, что-то делает на частоте полученной после предделителя, то после прихода триггера он ещё может значительное время не выставлять track_count. Поэтому в коде я мониторю 32 бит, и уже там понимаю, когда появились валидные данные. Подключен модуль так:

CXD2545_TRACK_COUNTER trc_cnt_inst(
	.clk(CPU_CLK),
	.div(trc_div),
	.toggle_cnt(256),
	.toggle_clk(trc_toggle),
	
	.trigger(trc_cnt_en),
	.track_count(track_count)
);


И вот там где стоит константа 256, нужно было завести значение регистра 0xB, но так как оно приставкой всегда ставилось именно в 256, я решил, что и так сойдёт. После связки всего этого воедино, и теста в аудио плеере. Оказалось, что приставка нормально воспринимает пределитель 720 и выше. Игры тоже нормально грузились. И я начал проходить FF7, неспешно решая, всякие мелкие проблемы. Однако на ледяном уровне которые идёт после сноуборда (там где можно замерзнуть), приставке начало сносить крышу. Она металась лазером туда сюда, и нормально не работала. Глюк был плавающий, то есть нормально повторению не поддавался. В итоге я вывел на клавиатуру подстройку этого предделителя, и побаловавшись подобрал значение 1400 при котором и позиционирование было быстрое, и глюков не вызывало. В общем, в этом месте я получил рабочий эмулятор. Всем спасибо за внимание.

gg5toup1xz1n-dw43ugxzlmirk4.jpeg

Не совсем. Остались ещё всякие мелочи, которые были в процессе, но хронологию их появления я не помню.

10. Всякие мелочи


10.1 A SGDMA, не умеет прерывать передачу


Да вот такой SGDMA бяка. Если началась передача, то остановить её уже не получится, он с головой уходит в работу и слышать ничего не хочет. Есть, конечно, вариант сделать ему ресет через регистр, но вот, что говорит официальная документация:

Executing a software reset when a DMA transfer is active may result in permanent bus lockup until the next system reset. Hence, Altera recommends that you use the software reset as
your last resort.


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

avalon_st_drainer
module avalon_st_drainer (
		input  wire        clk,       //       clock.clk
		input  wire        reset,     //  reset_sink.reset_n
		
		output wire        out_valid, //         out.valid
		output wire        out_sof,   //            .startofpacket
		output wire        out_eof,   //            .endofpacket
		output wire [31:0] out_data,  //            .data
		output wire [1:0]  out_empty, //            .empty
		input  wire        out_ready, //            .ready
		
		input  wire        in_valid,  //          in.valid
		input  wire        in_sof,    //            .startofpacket
		input  wire        in_eof,    //            .endofpacket
		input  wire [31:0] in_data,   //            .data
		input  wire [1:0]  in_empty,  //            .empty
		output wire        in_ready,  //            .ready
		input  wire        drain      // conduit_end.export
);


assign out_eof		= ((drain == 1'b0) && (lock_drain == 1'b0)) ? in_eof  	: 1'b0;
assign out_valid	= ((drain == 1'b0) && (lock_drain == 1'b0)) ? in_valid 	: 1'b0;
assign in_ready		= ((drain == 1'b0) || (in_sof == 1'b1)) ? out_ready	: 1'b1;
assign out_sof 		= in_sof;
assign out_empty	= in_empty;
assign out_data 	= in_data;


reg lock_drain;
always @(negedge clk) begin
	if(drain == 1'b1) begin 
		lock_drain <= 1'b1;
	end else begin
		if(in_sof == 1'b1) begin
			lock_drain <= 1'b0;
		end
	end
end
	
endmodule


Логика работы простая, модуль реализует сигналы шин Avalon-ST выхода и выхода. В обычном состоянии он просто шлёт вход на выход. Если пришёл сигнал drain модуль выставляет флаг lock_drain. И блокирует передачу сигнала out_valid чтобы модули, стоящие за ним, думали что данных нет. При этом сигнал in_ready наоборот рапортует, что готов принимать данные, в итоге данные принимаются, но не уходят. При поступлении сигнала in_sof, lock_drain сбрасывается в ноль и модуль просто начинает работать, посылая копию входа на выход. Управляется всё это опять же через обычный PIO.

Финальная версия SOPC теперь выглядит вот так:

_7wffncwun9fhijtcsv5hcjovvu.jpeg

10.2 Позиционирование гораздо хуже чем в оригинале


По факту оказалось, что приставка всё равно промахивается меньше при длинных переходах, чем мой код. Почему так происходит, я уже знал. Диск увеличивается от центра к краю, и длинна окружности растет. Данные же записаны с неизменным интервалом. И получается, что в конце диска на один оборот мы имеем больше секторов, чем в начале. Привод, ориентируясь на скорость поступления данных, регулирует скорость вращения диска, поэтому данные с привода, идут потоком равномерным. Но вот прыжок на один трек, в начале диска и в конце дают разный результат. Я где то читал (где уже не помню), что SUP-CPU имеет таблицу коррекции и при помощи неё он рассчитывает на сколько треков прыгнуть, чтобы оказаться в нужном месте. Я даже пытался её найти в прошивке, но надо сказать безуспешно. Тогда я написал письмо Martin’у Korth также известному, как Nocash с просьбой помочь в этом вопросе. Он достаточно быстро ответил, за, что ему огромное спасибо. И прислал нужные таблицы, с кусками ассемблерного кода, и пояснениями как оно все работает. Таблица была устроена следующим образом, в ней около 80 элементов (вообще там несколько таблиц для дисков с разным объемом записанных данных, но из за ошибки все равно используются только первые 71 элемент). И каждый элемент указывает сколько каждая минута звучания (ну и данных по совместительству) занимает треков. То есть элемент номер 4 у нас был 0xDD, значит 3 минута звучания, занимает 221 трек. А 57 элемент 0×6D, то есть 58 минута диска уже всего 109 треков. Приставка рассчитывает по этим данным как далеко переместить каретку. Я видоизменил таблицу, так чтобы мне было с ней удобно работать. И добавил в свой код, после чего позиционирование стало в разы лучше.

10.3 Вылет за пределы LeadOut


Иногда ещё до добавления правильно позиционирования, бывали моменты, когда мы улетали за пределы LeadOut. На SD карте обычно там был уже мусор, или образ другого диска. Поэтому приставку не хило колбасило. И заканчивалось перечитыванием TOC и попыткой повторить чтение данных. Это решилось просто, в заголовоке которые идёт в начале образа SD карты, описаны три параметра, первый это первый сектор образа, второй начало зоны LeadOut, и третий длина образа. Если мы вдруг оказались за пределами образа, я выполняю прыжок на начало зона LeadOut. Работает отлично.

11. Ответы на вопросы которые возможно у вас возникнут


Что нужно чтобы сделать такое на микроконтроллере ибо FPGA дорого и сложно?


Насколько я понимаю, основная заморочка это сигнал CDBCLK он должен, быть четко засинхронизирован с приставкой иначе на музыке точно будут щелчки. С остальными всё проще. Думаю идеально найти контролер, который может делать шину I2S с параметрами 24 бита на канал, и 24 бита слово. Либо 16 бит на канал и 24 на слово. STM32 когда я смотрел, вроде бы так не умели. Скорей всего отлично подойдут PSoC 5 и возможно ESP32, но в последних, не уверен. Сигналы управления шиной данных CXD2545 медленные порядка 70 килогерц, шины субканала порядка 133 килогерц, думаю можно ногодрыком реализовать. Ну, или SPI попробовать сконфигурировать для этого. SENS не смотря, что это мультиплексор, в целом приставкой читается как регистр. То есть посылает на шину данных, что именно хочет увидеть, а потом через паузу небольшую читает значения ноги SENS. Так что можно сильно не заморачиваться, и тоже сделать ногодрыком.

Планируются ли ещё работы в этом направлении?


В целом да, есть несколько идей:

  1. подключится к шине команд SUB-CPU чтобы точно знать куда позиционироваться
  2. полностью выкинуть SUB-CPU, работать вместо него и CXD2545
  3. эмулировать работу лазера чтобы можно было подключить к любой PS
  4. сделать модуль работы с SD картой через SDIO, а не SPI


У всего есть свои проблемы. Пункт первый мало интересен. Так как почти ничего нового не даст. Пункт второй тут очень желателен логический анализатор на 32 канала, в целом там хватит и 20, но у меня сейчас только 15 канальный (один канал умер). И тратится на 32 канала, пока не хочется. Конечно, появится возможность грузить игры по сети, и всё остальное, что может дать полный контроль подсистемы привода, но анализатора пока в наличии нет. А изготовления своего подзаморожено. Третий вариант самый захватывающий. Но он требует реверсить схему платы PU-18 либо PU-8 последний вариант мне больше нравится. И потом разводить свою плату с CXD2545/CX2510 чтобы удобно отлаживать. Идея заняться есть, но пока руки не доходя, особенно до разводки платы. Ну, а работа с картой через, SDIO как бы тоже не так уж и интересно.

12. Послесловие


uqyr_uxd-9_dngqw0isj9psqx0k.jpegВот теперь точно всё. Эмулятор готов. Работоспособность подтверждена практикой. Проект для меня получился очень интересным, хоть и невероятно длинным. Опыта с ним я хлебнул на славу. Но многое из того, с чем столкнулся не однократно пригодилось в жизни. В общем радиоэлектроника и программирование бывают очень интересными. А получить рабочие устройство это не передаваемое чувство радости и гордости. Поэтому если хотели попробовать этим заняться, возможно самое время. Не факт, что получится с первого раза, Но как говорится терпенье и труд до добра не доведут могут быть вознаграждены. Всем спасибо за внимание. Теперь точно конец.

PS. Чуть позже (1–2 недели) весь код (Си и Verilog) проекта будет выложен на гитхабе, мне просто очень хочется его немного «причесать». А затягивать из за этого последнюю часть не хотелось.

PSS. Если вас вдруг заинтересовало как хранятся данные на CD, вы прочитали указанную во второй части книгу. И вам хочется пощупать как же выглядит этот EFM сигнал в жизни (мне однажды очень захотелось). Здесь лежит файл снятого EFM сигнала, и частоты с пина C16M, которая должна меняться в зависимости от скорости данных. Когда то мне удалось вытащить из этого сигнала порядка двух полных секторов.

PSSS. Ну вот теперь уж точно конец.

© Habrahabr.ru