Добавляем SPDIF в легендарный Roland MT-32
Этот проект можно считать частью №2 по цифровизации старых синтезаторов. В первой части я рассказал, как при помощи AK4103A можно добавить цифровой выход в практически любой синтезатор, где схема ЦАП подразумевает стереопоток, комбинированный в одну I2S DATA линию.
Roland MT-32, очень редкий и знакомый в 90е старичкам-геймерам разве что по слухам — еще более старое устройство, в котором применена достаточно распространенная в D-серии Roland схема использования параллельного DAC, который занимается не просто поточным преобразованием финального стерео, но и попутно выполняет услуги ЦАП для чипа реверберации.
Разбираем демуксер
Получается, что у нас нет в прямой доступности цифрового потока, содержащего в себе финальные аудио данные. В PCM54HP приходит поток, последовательно содержащий в себе не только чистый левый и правый каналы, но в том числе и отдельно реверберированные данные для левого и правого каналов.
Выглядит это примерно так (все фреймы 16bit, порядок предположительный): [RSYN1][LSYN1][REV R][REV L][RSYN2][LSYN2][RSYN1][LSYN1][REV R][REV L][RSYN2][LSYN2] и т.д. Преимущество параллельного DAC заключается в том, что он работает мнгновенно, т.е. нет совсем никакой задержки в том, чтобы снять текущие значения с резисторной (по сути) сборки и в следующий миг выполнять задачу по преобразованию совершенно другой картины. Демультиплексированием всей этой каши аудиоданных занимается широко известный CD4051. Переключение каналов в CD4051 осуществляется через смикшированные от LA и Reverb чипов управляющие линии SH1 SH2 SH2 (SH — Sample/Hold), а также линию INH которая отключает все каналы. На выходе демуксера образуются три пары аналоговых каналов, которые в дальнейшем уже микшируются и проходят финальную обработку в низкочастотном фильтре. LPF-фильтр должен иметь плоскую амплитудную характеристику в диапазоне 0–20kHz и высокую степень затухания выше 20kHz.
Разрядность и частота дискретизации MT-32 согласно заявленным характеристикам — 15bit 32kHz. В первой версии MT-32 (т.н. «old») последний 16-ый LSB бит на входе PCM54HP даже закорочен на землю, а в самой шине данных выпадает 14-ый бит (отсчитывая с нуля). Тем не менее, для нас ширина кадра всегда будет 16 бит (MT-32 второй версии имеет уже полноценную 16-битную шину). Теоретически, частота переключения каналов 0–1–2–3–4–5–6–7 каждый раз задействует изменение состояния 0/1 в управляющем сигнале A, поэтому можно ожидать на этой линии 128kHz, и на линиях B и C — 64 и 32kHz соответственно. Но нам не известен порядок следования фреймов. Даже если последовательно записывать все состояния параллельной шины — это будет бесполезно, не знай мы порядка переключения ABC. На практике, не имея в наличии трехканального осциллографа, можно попробовать поймать состояния хотя бы двух линий из трех (A и C), а затем записать секвенции AB и BC, A и INH, чтобы свести в дальнейшем картину в одно целое.
Итак, теперь мы знаем порядок фреймов: [L REV][RSYN2][LSYN2][R REV][RSYN1][LSYN1] [L REV][RSYN2][LSYN2][R REV][RSYN1][LSYN1] … и т.д. Если послушать эти пины в аналоге, то станет ясно, что SYN1 — чистый сигнал, REV — реверберированный возврат. SYN2 судя по всему тоже аналоговый, но слишком тихий для того, чтобы его можно было разборчиво записать;, но раз уж SYN2 тоже подмешивается в финальный микс, мы тоже будем так делать. Кстати, если посмотреть неиспользованные выходы CD4051 CH4 и CH5, там будут [почти] четкие 32kHz.
Управляющий сигнал INH работает на частоте 256kHz, значит нам надо будет считывать все порты на этой частоте. Отключение всех каналов необходимо, чтобы не было фальшивого срабатывания на состояния rising edge ABC, когда INH=1 говорит нам, что выдавать в serial нам ничего не надо. При INH=0 мы должны считать шину, и в зависимости от состояний ABC, раскидать ее в соответствующий выход. В идеале, нам нужно определить начало фрейма (возьмем для опорного кадра самый высокий пик INH), и все L/R кадры смикшировать в два финальных. Но для теста можно начать с отсылки двух кадров с чистой нереверберированной информацией (RSYN1, LSYN1). Сначала я думал заморочиться над железным определением начала секвенса кадров, но затем опустил эту часть, т.к. даже если логика на старте начнет работать с потоком в середине секвенса, определив LSYN1 как конец секвенса, мы сбросим счетчики и начнем работать уже в правильном порядке. Логика в данном случае будет выглядеть примерно так (я воспользуюсь тут псевдоязыком с понятным всем синтаксисом):
(R,L)=(0,0);
(FLAG_RSYN1,FLAG_LSYN1,FLAG_RSYN2,FLAG_LSYN2,FLAG_RREV,FLAG_LREV)=(0,0,0,0,0,0);
(RSYN1,LSYN1,RSYN2,LSYN2,RREV,LREV)=(0,0,0,0,0,0);
while (256kHz_cycle) {
input=read(PCM54_parallel);
A=read(CD4051_A); B=read(CD4051_B); C=read(CD4051_C); INH=read(CD4051_INH);
#INH==0 enables output
if (INH==0) {
if (A==1 && B==1 && C==0) {
RSYN2=input; FLAG_RSYN2=1; #RSYN2
}
elsif (A==0 && B==1 && C==0) {
LSYN2=input; FLAG_LSYN2=1; #LSYN2
}
elsif (A==0 && B==0 && C==1) {
RREV=input; FLAG_RREV=1; #RREV
}
elsif (A==0 && B==0 && C==0) {
LREV=input; FLAG_LREV=1; #LREV
}
elsif (A==1 && B==1 && C==1) {
RSYN1=input; FLAG_RSYN1=1; #RSYN1
}
elsif (A==0 && B==1 && C==1) {
LSYN1=input; #LSYN1 this is the last channel in frame
(FLAG_RSYN1,FLAG_LSYN1,FLAG_RSYN2,FLAG_LSYN2,FLAG_RREV,FLAG_LREV)=(1,1,1,1,1,1);
}
if (FLAG_RSYN1==1 && FLAG_LSYN1==1 && FLAG_RSYN2==1 && FLAG_LSYN2==1 && FLAG_RREV==1 && FLAG_LREV==1) {
#in merge there will be magic
L=merge(LSYN1,LSYN2,LREV);
R=merge(RSYN1,RSYN2,RREV);
write_serial(R,L);
#reset channel flags
(FLAG_RSYN1,FLAG_LSYN1,FLAG_RSYN2,FLAG_LSYN2,FLAG_RREV,FLAG_LREV)=(0,0,0,0,0,0);
}
}
}
Железная часть
Схематично план всего проекта нарисовался вот такой:
В этом плане участвует всего лишь только одна магическая фигура, и тут я честно скажу, на ум приходили гигантские конструкции из кучи логики, которые должны выполнять задачу микширования цифровых потоков. Мне подсказали, что стоит бросить заниматься фигней и освоить FPGA. Все сигналы DAC и A/B/C/INH имеют уровни CMOS, так что нам надо конвертировать их в уровень TTL при помощи CD4050B. Благодаря этому, мы также получаем выровненые уровни (желтый — TTL в другой шкале):
Синий — CMOS, желтый — TTL (увеличен)
DIT
Поскольку мы имеем дело с 16 битами, можно использовать обширное количество DIT, т.к. все они как минимум работают с 16-bit RJ. Для этих целей я выбрал DIT4192, т.к. у меня он уже был в наличии после экспериментов в процессе работы над 18-bit DIT. Здесь вся обвязка стандартна, конфигурация:
DIT4192 Hardware mode | |
---|---|
Mode operation | Slave (SYNC and SCLK are inputs) |
Format | 16-Bit Right-Justified |
Sampling frequency | 32kHz |
Master clock | 16.384MHz (512*fs) |
Bit clock | 1.048MHz (16×2*32KHz) |
Обвязка по даташиту DIT4192
Магическая часть
Для магической части была выбрана демо-плата FPGA TangNano 9K, и план приобрел конкретность:
А затем была набросана и остальная схема. Обращаю внимание на коннектор I2S — можно полностью опустить монтаж DIT4192, если его нет в наличии, и использовать любой другой внешний SPDIF трансивер, или даже просто транспорт I2S в ресивер с соответствующим входом. Я проверял схему с внешней платой на основе WM8804 — все прекрасно работает.
Два PCM54HP, т.к. один футпринт для сокета, второй для коннектора на плату MT-32
Прототип
Конечно же, схема не сразу приобрела финальный вид. Я начал с «бутерброда», разделяющего конвертер уровней от FPGA, пробуя различные варианты от резисторных делителей до CD4050 …
далеко не первая макетка
… и получив ровные уровни, я остановился на CD4050 и сел проектировать плату. Платы были достаточно быстро сделаны на известном PCBway (впервые воспользовался), ну и всем известно, что в РФ за такие смешные деньги прототип вообще не заказать.
Финальный дизайн предполагает, что ЦАП PCM54HP будет выпаян из MT-32 и сокетирован на плате DIT. После этого, плату DIT можно либо впаять, либо также сокетировать на PBS коннекторах. Но для начала, можно просто надеть плату DIT вторым этажом на микросхему ЦАП, как я и делал это с «бутербродом».
Учим verilog
Я не имел никакого опыта в дизайне FPGA-проектов. Но оказалось, что мой код на псевдо-языке очень даже похож на verilog! Через пару недель (я занимался проектом всего несколько часов в неделю) был написан первый рабочий код:
module top (
input mclk, //master clock //pin 51
input clk_inh, //256kHz INH clk input //pin 53
input [2:0] ch_id, //cd4051 sample/hold controls a/b/c 128/64/32kHz //pin a 77 b 76 c 48
input [15:0] dac, //parallel input from dac
input sys_rst_n, //reset input
output wire dtr, //data ready flag //pin54
output sdata, //16bit i2s sdata output //pin 49
output wire wclk, //i2s word select lrck output 32kHz //pin 31
output wire bck //i2s bit clock output 1024MHz //pin32
);
wire [31:0] data;
dac_decoder dac1(
.clk_inh(clk_inh),.ch_id(ch_id),.dac(dac),.data(data),.rst_n(sys_rst_n),.dtr(dtr)
);
i2s_serializer ser1 (
.mclk(mclk),.sdata(sdata),.wclk(wclk),.bck(bck),.data(data),.rst_n(sys_rst_n)
);
endmodule
module i2s_serializer (
input mclk, //master clock 16.384MHz
input [31:0] data, //input channels register
input wire rst_n, //reset button
output reg sdata, //i2s sdata output
output reg wclk, //i2s word select lrck output mclk/512 = 32kHz
output wire bck //[3] bit'mclk. i2s bit clock output
//16bit * 2 * 32000 = 1.024 MHz (16.384/16)
);
reg [31:0] mclk_counter; //32bit counter
assign bck=mclk_counter[3]; //1.024MHz divide
reg [31:0] data_buf; //i2s output buffer
reg [4:0] cbit; //0-15 current bit counter
initial begin
mclk_counter<=0; cbit<=0; wclk<=0; data_buf<=0;
end
always @(posedge mclk,negedge rst_n) begin
if(!rst_n) begin mclk_counter<=0; end
else begin mclk_counter<=mclk_counter+1; end
end
//i2s WCLK=0 left, =1 right
always @(negedge bck) begin //send sdata from buffer
if (wclk==0) begin sdata<=data_buf[31-cbit]; end //LSYN send
else if (wclk==1) begin sdata<=data_buf[15-cbit]; end //RSYN send
cbit<=cbit+4'b01;
if (cbit==15 && wclk==0)
begin cbit<=0; wclk<=1; end //LSYN1 end
else if (cbit==15 && wclk==1)
begin cbit<=0; wclk<=0; data_buf<=data; end //RSYN1 end, new buffer read
end
endmodule
module dac_decoder (
input wire rst_n,
input clk_inh, //256kHz INH clk input
input [2:0] ch_id, //cd4051 sample/hold controls a/b/c
input [15:0] dac, //parallel input from dac
output reg [31:0] data, //32 bit
output reg dtr //data ready flag
);
reg [15:0] ch0; //LREV
reg [15:0] ch6; //RSYN2
reg [15:0] ch2; //LSYN2
reg [15:0] ch1; //RREV
reg [15:0] ch7; //RSYN1
reg [15:0] ch3; //LSYN1
initial begin
ch0<=0; ch1<=0; ch2<=0; ch3<=0; ch6<=0; ch7<=0; dtr<=0; data<=0;
end
always @(negedge clk_inh,negedge rst_n) begin
if(!rst_n) begin
dtr<=0; data<=0;
end
else begin
case (ch_id)
4 : begin dtr<=0; end // empty
0 : begin dtr<=0; ch0<=dac; end // LREV
6 : begin dtr<=0; ch6<=dac; end // RSYN2
2 : begin dtr<=0; ch2<=dac; end // LSYN2
5 : begin dtr<=0; end // empty
1 : begin dtr<=0; ch1<=dac; end // RREV
7 : begin dtr<=0; ch7<=dac; end // RSYN
3 : begin dtr<=1; ch3<=dac; data = {dac,ch7}; end // LSYN
endcase
end
end
endmodule
Первое видео захвата цифры, пока что без реверберации (было ночью, уровень записи низкий):
Микширование 6 каналов в стерео
Самая простая логика аудиомикширования — суммирование сигналов. Эта логика работает и в цифре. Помните, реальная битность Roland MT-32 первой версии — 15. Так что можно использовать 17-битный буфер для суммирования всех трех каналов, а затем простейшим сдвигом поделить результат на 2 и остаться в пределах 16 бит. Заодно, я прикрутил кнопку отключения реверберации на одну из кнопок TangNano9K.
reg signed [16:0] l; reg signed [16:0] r;
reg signed [15:0] left; reg signed [15:0] right;
case (ch_id)
4 : begin dtr<=0; end // empty
0 : begin dtr<=0; lrev<=dac; end // LREV
6 : begin dtr<=0; rsyn2<=dac; end // RSYN2
2 : begin dtr<=0; lsyn2<=dac; end // LSYN2
5 : begin dtr<=0; end // empty
1 : begin dtr<=0; rrev<=dac; end // RREV
7 : begin dtr<=0; rsyn1<=dac; end // RSYN1
3 : begin dtr<=1; lsyn1<=dac; // LSYN1
if (!rev_sw) begin l<=dac; r<=rsyn1; end
else begin l<=dac+lrev+lsyn2; r<=rsyn1+rrev+rsyn2; end
left[15:0]<=l[15:0]; right[15:0]<=r[15:0]; data<={left,right};
end
endcase
Возможные проблемы
Щелчки и треск
На последнем этапе, я обнаружил, что частота DTR (флаг, сигнализирующий о готовности кадра) слегка быстрее, чем WCLK (что-то типа 32.0010kHz@DTR против ровных 32kHz@WCLK). После нескольких «научных тыков» было выявлено, что частота A/B/C/INH зависит от кварца на 32.768kHz, и судя по всему, из нее нельзя получить чистые 32kHz. Кстати, в «новом» MT-32 использован кварц на 16.384MHz, и вот из него как раз можно получить чистые 32kHz делением на 512, но это может подтвердить только владелец «нового» MT-32. Сначала я думал, что плавающая частота является причиной этих щелков;, но оказался неправ.
Первая причина в том, что вы не можете оперировать с регистрами много раз в пределах одного такта. Нужен конвейер, и логика
if (!rev_sw) begin l<=dac; r<=rsyn1; end
else begin l<=dac+lrev+lsyn2; r<=rsyn1+rrev+rsyn2; end
left[15:0]<=l[15:0]; right[15:0]<=r[15:0]; data<={left,right};
должна быть разделена:
always @(negedge clk_inh,negedge rst_n) begin
if (!rst_n) begin
dtr<=0; data<=0;
end
else begin
case (ch_id)
4 : begin dtr<=0; left[15:0]<=l[15:0]; right[15:0]<=r[15:0]; end // empty
0 : begin dtr<=0; lrev<=dac; end // LREV
6 : begin dtr<=0; rsyn2<=dac; end // RSYN2
2 : begin dtr<=0; lsyn2<=dac; end // LSYN2
5 : begin dtr<=1; data<={left,right}; end // empty
1 : begin dtr<=0; rrev<=dac; end // RREV
7 : begin dtr<=0; rsyn1<=dac; end // RSYN1
3 : begin dtr<=0; lsyn1<=dac; // LSYN1
if (!rev_sw) begin l<=dac; r<=rsyn1; end
else begin l<=dac+lrev+lsyn2; r<=rsyn1+rrev+rsyn2; end
end
endcase
end
end
Вторая причина — нужно избегать обновления регистра, если он находится в режиме чтения сериализатором. Частота сериализатора гораздо выше частоты декодера, и можно испортить кадр, т.к. сериализатор может прочитать уже какую-то часть регистра во время обновления. Используя эту логику, мы, конечно, получаем более низкий рейт обновления кадров для сериализатора, но этим можно пожертвовать, т.к. эксперименты с FIFO ни к чему не привели — если входящая частота выше, рано или поздно какие-то данные придется пропустить, и лучше сделать это сразу, нежели накапливать данные и при переполнении получать слышимый пропуск.
5 : begin dtr<=1; if (drq==0) begin data<={left,right}; dtw<=1; end end // empty
После этих исправлений, запись более 3х часов показала, что щелчки ушли.
3 часа записи — ни одного щелчка
Десинхронизация
Другая проблема может возникнуть, если кварцевый генератор, используемый для master clock, нестабилен, или имеет плохой контакт. Это заметно по срыву WCLK.
DC offset
тишина на уровне -6dB
Из-за известной проблемы «MT-32 digital overflow» и аппаратного битового сдвига, весь поток определяется звуковой картой на уровне -6dB. Цифровой DC offset легко решается вычитанием определенного числа из всех семплов потока. Но поскольку поток LA приходит на ЦАП в виде signed, а DIT и I2S по спецификации работают с unsigned, простым вычитанием фиксированного offset=16384 я добился только смещения до -12dB. Это пока уходит в список «доделать».
4 : begin dtr<=0; dtw<=0; left[15:0]<= $unsigned(l)-offset; right[15:0]<= $unsigned(r)-offset; end // empty
Монтаж
Проблема плохих контактов. Это был кошмар на протяжении многих недель — я искал проблему в verilog, в частотах, в битах, и совсем не подумал, что способ «надевания» макетки на чип совершенно ненадежен — несколько пинов в процессе частого «надевания» отогнулись чуть дальше своих соседей, вследствии чего я постоянно получал испорченные данные и испорченный звук.
Решение этой проблемы только одно — нужно выпаять ЦАП из платы MT-32 и сокетировать его на плате DIT. Процесс выпайки весьма сложен без соответствующего опыта. Сперва нужно заменить несколько ближайших керамических конденсаторов на низкопрофильные — чтоб не мешали установке DIT. Пленочные конденсаторы встают идеально. Затем нужно выпаять ЦАП.
на всякий случай приготовлен второй PCM54HP
Пока меняем конденсаторы, время подумать о том, как забирать сигналы A/B/C/INH от демуксера. Поначалу я просто подпаивал провода непосредственно к пинам микросхемы. Но провода часто гнулись, ломались, и я решил напаять вторым этажом кусочек макетки, чтобы иметь более легкий доступ к сигналам.
таким же «вторым этажом» стоял DIT над ЦАП и не контачил
Я использовал двурядные PBD коннекторы с шагом 2.54 для сокетирования DIT. Один ряд, конечно же, должен быть вынут. Почему PBD? Просто у меня их было в запасах гораздо больше, чем однорядных PBS:)
Можно установить плату в сокет.
Достанем плату MT-32 из корпуса. Время найти место для монтажа RCA.
Финальная картинка:
Попутно я решил заменить всю керамику и электролиты на свежие. Буржуйство, но использовать Wima и Nichicon — почему бы и нет? Выпайка старых конденсаторов показала, что старая керамика сильно гуляет от предписанных номиналов, а вот старые электролиты Sanyo показали себя на высоте — почти все не ниже номинала, и только в цепи питания парочка 1000uF действительно просела и нуждалась в замене.
Вместо заключения.
Если познать дзен, можно обойтись вообще без каких-либо внешних трансиверов, и реализовать логику I2S/SPDIF на том же самом FPGA. Данное решение (предположительно) может быть использовано для вывода цифрового звука из любого синтезатора Roland D-серии, т.к. архитектура LA-синтеза использует практически одну и ту же схематику. Проект почти закончен (готовится вторая версия платы с небольшим модом для улучшения качества аналогового выхода ЦАП и вариантом выбора трансивера — WM8804 или DIT4192).
Ссылки
MT-32 DIT на Github
Roland MT-32 service notes
PCM54HP datasheet
DIT4192 datasheet