I2S в FPGA на Verilog


Сначала хотел описать регистры конфигурации кодека WM8731, но потом решил начать с описания интерфейса I2S, потому что описание регистров это, можно считать, практически перевод датащита. В следующей статье напишу о регистрах конфигурации.
I2S (Integrated Interchip Sound) – это последовательная шина для передачи цифрового звука. Впервые стандарт был представлен в 1986 году, а последняя ревизия вышла в 1996-м. У I2S раздельные линии тактирования и данных, поэтому джиттер будет намного меньше, чем, например, в интерфейсах подобных S/PDIF.
Для передачи звука по I2S в одну сторону требуется как минимум 3 линии
Bit clock — BCLK (тактирование);
Word select — LRCLK (линия выбора канала);
Data line – DAT (линия передачи аудио данных).
При этом аудиокодек может работать в двух режимах — ведущем и ведомом. В ведущем режиме кодек сам генерирует импульсы BCLK и LRCLK, а в ведомом их должен генерировать, например, ваш контроллер или ПЛИС.

Посмотрите на диаграмму ниже.

Линия LRCLK указывает, данные какого канала сейчас передаются, низкий уровень соответствует левому каналу, высокий — правому, изменение LRCLK происходит на отрицательном фронте BCLK. Передатчик изменяет значение линии данных DAT при отрицательном фронте сигнала BCLK, приемник считывает при положительном. Старший бит слова передается на втором положительном фронте сигнала BCLK после изменения сигнала LRCLK.
Сейчас в большинстве датащитов линию LRCLK называют именно так, но иногда она может называться WS (word select — выбор слова). В первый версиях I2S разрядность сэмпла была 16 бит, от этого и название. Потом 16 бит стало мало. Кодек WM8731, независимо от настройки (16/20/24/32 бита) в регистрах, всегда передает 32 бита на канал.
Еще у кодека WM8731, как впрочем и у многих других, есть несколько форматов, похожих на I2S, но с небольшими изменениями — режимы выравнивания слова «слева» и «справа», DSP режим, когда данные каналов передаются сразу один за другим. Вы можете посмотреть временные диаграммы в датащите на кодек, я покажу реализацию «чистого» I2S.
Теперь о реализации на FPGA.
Документ «Cпецификация I2S» от Philips за 1986 год рекомендует следующие варианты реализации передатчика и приемника:

Возможная реализация передатчика

Возможная реализация приемника
Мы будем писать модуль на языке описания Verilog HDL.
Кодек WM8731 может тактироваться от внутреннего генератора с кварцевой стабилизацией, либо от внешнего генератора. На плате DE2-115 производитель не установил для кодека кварц, а тактовый вход кодека подключен к FPGA. Это не слишком хорошо, потому что для генерации нужной частоты (12.288 МГц или 18.432 МГц) средствами Cyclone IV придется использовать блок PLL, а их всего 4.
Конечно в крайнем случае можно разделить 50 МГц на 4 и тактировать кодек сигналом 12.5 МГц и он будет работать, тогда частота дискретизации вместо, напимер, 48 кГц будет 48.83 кГц. Но отдавать такой сигнал «наружу», например через S/PDIF уже нельзя. Хотя, если честно, я, интереса ради попробовал, ресивер Yamaha RX-V2500 сигнал распознал, но когда я зашел посмотреть в статус, там был «прочерк» в графе «частота дискретизации».
Итак, какие сигналы будут у нашего модуля:
module i2s #(parameter SAMPLE_WIDTH = 16)(
input CLK,
input BCLK, // Bit clock от кодека
input LRCLK, // LR clock от кодека
input ADCDA, // оцифрованные АЦП данные от кодека
input [SAMPLE_WIDTH-1:0] LEFT_IN, // вектор левого канала для вывода на ЦАП
input [SAMPLE_WIDTH-1:0] RIGHT_IN, // вектор правого канала для вывода на ЦАП
output reg [SAMPLE_WIDTH-1:0] LEFT_OUT, // десериализованные данные АЦП, левый канал
output reg [SAMPLE_WIDTH-1:0] RIGHT_OUT, // десериализованные данные АЦП, правый канал
output reg DATAREADY, // строб - данные обновлены
output BCLK_S, // синхронизированный сигнал BCLK
output LRCLK_S, // синхронизированный сигнал LRCLK
output DACDA); // выход данных на ЦАП кодека
Разрядность может меняться, поэтому предусмотрим для задания параметр.
Так как проект тактируется от генератора 50 МГц, а кодек может быть со своим генератором, то первое, что мы должны сделать — это обязательно синхронизировать все входные сигналы от кодека (синхронизация домена).
reg [2:0] bclk_trg; always @(posedge CLK) bclk_trg <= { bclk_trg[1:0], BCLK };
assign BCLK_S = bclk_trg[1]; // синхронизированный BCLK
wire BCLK_PE = ~bclk_trg[2] & BCLK_S; // выделенный передний фронт BCLK
wire BCLK_NE = bclk_trg[2] & ~BCLK_S; // выделенный задний фронт BCLK
reg [2:0] lrclk_trg; always @(posedge CLK) lrclk_trg <= { lrclk_trg[1:0], LRCLK };
assign LRCLK_S = lrclk_trg[1]; // синхронизированный LRCLK
wire LRCLK_PRV = lrclk_trg[2]; // предыдущее значение LRCLK
wire LRCLK_CH = LRCLK_PRV ^ LRCLK_S; // любое измененение LRCLK
reg [1:0] adcda_trg; always @(posedge CLK) adcda_trg <= { adcda_trg[0], ADCDA };
wire ADCDA_S = adcda_trg[1]; // синхронизированный DAT
В приведенном коде я использую стандартную схему. Для синхронизации используется два последовательно соединенных триггера. Этот же участок кода служит для выделения обоих фронтов сигнала BCLK и момента изменения сигнала LRCLK. Для выделения фронтов также используется триггер. Т. е. у нас три последовательно соединенных триггера. Возможно это сходу не видно в коде, там используется сдвиговый регистр.
Подробно о синхронизации вы можете почитать, например, здесь.
На выходе синтезатора получается такая картина (это участок модуля).

Для сохранения полученных данных нужен 32-х битный вектор. При обнаружении положительного фронта сигнала BCLK производим захват данных и задвигаем в регистр.
wire [31:0] shift_w = { shift[30:0], ADCDA_S };
reg [31:0] shift; always @(posedge CLK) if (BCLK_PE) shift <= shift_w;
При обнаружении изменения сигнала LRCLK мы сохраняем полученные данные в соответствующем каналу векторе и генерируем строб, сигнализирующий, что данные обновились. Естественно частота строба будет равна частоте дискретизации.
always @(posedge CLK)
begin
if (LRCLK_CH)
begin
if (LRCLK_PRV)
begin
RIGHT_OUT <= shift_w[31:32-SAMPLE_WIDTH];
DATAREADY <= 1'b1;
end
else
LEFT_OUT <= shift_w[31:32-SAMPLE_WIDTH];
end
else
DATAREADY <= 1'b0;
end
Вот и весь приемник.
Передатчик не сложнее.
Заведем два вектора, в которые будут защелкиваться входные данные. И указатель (счетчик от 0 до 31), а также регистр, по которому будет выбирать канал.
reg [SAMPLE_WIDTH-1:0] lb;
reg [SAMPLE_WIDTH-1:0] rb;
reg [4:0] bit_cnt; // указатель текущего бита
reg actualLR;
Теперь произведем расчет указателя. При изменении сигнала LRCLK ставим указатель на 31 ячейку. Хоть LRCLK изменился и «говорит», что канал, например, правый, на самом деле правый канал начнет передаваться только на втором отрицательном фронте BCLK. Поэтому инвертируем значение LRCLK для 31 ячейки.
При отрицательном фронте сигнала BCLK увеличиваем указатель на единицу и, если это первая ячейка, то защелкиваем входные данные. LRCLK теперь отражает правильное значение и инвертировать его не надо.
always @(posedge CLK)
begin
if (LRCLK_CH)
begin
bit_cnt <= 5'd31;
actualLR <= ~LRCLK_S;
end
else
if (BCLK_NE)
begin
actualLR <= LRCLK_S;
bit_cnt <= bit_cnt + 1'b1;
if (bit_cnt == 5'd31)
begin
lb <= LEFT_IN;
rb <= RIGHT_IN;
end
end
end
теперь непрерывным присваиванием получаем значение выходного сигнала для передачи на кодек.
wire [4:0] bit_ptr = (~bit_cnt - (32-SAMPLE_WIDTH));
assign DACDA = (bit_cnt < SAMPLE_WIDTH) ? actualLR ? lb[bit_ptr] : rb[bit_ptr] : 1'b0;
Вот и весь передатчик.
Теперь код полностью
module i2s #(parameter SAMPLE_WIDTH = 16)(
input CLK,
input BCLK, // Bit clock от кодека
input LRCLK, // LR clock от кодека
input ADCDA, // оцифрованные АЦП данные от кодека
input [SAMPLE_WIDTH-1:0] LEFT_IN, // вектор левого канала для вывода на ЦАП
input [SAMPLE_WIDTH-1:0] RIGHT_IN, // вектор правого канала для вывода на ЦАП
output reg [SAMPLE_WIDTH-1:0] LEFT_OUT, // десериализованные данные АЦП, левый канал
output reg [SAMPLE_WIDTH-1:0] RIGHT_OUT, // десериализованные данные АЦП, правый канал
output reg DATAREADY, // строб - данные обновлены
output BCLK_S, // синхронизированный сигнал BCLK
output LRCLK_S, // синхронизированный сигнал LRCLK
output DACDA); // выход данных на ЦАП кодека
// ==============================================================
// синхронизация клоковых доменов и выделение фронтов
// ==============================================================
reg [2:0] bclk_trg; always @(posedge CLK) bclk_trg <= { bclk_trg[1:0], BCLK };
assign BCLK_S = bclk_trg[1]; // синхронизированный BCLK
wire BCLK_PE = ~bclk_trg[2] & BCLK_S; // выделенный передний фронт BCLK
wire BCLK_NE = bclk_trg[2] & ~BCLK_S; // выделенный задний фронт BCLK
reg [2:0] lrclk_trg; always @(posedge CLK) lrclk_trg <= { lrclk_trg[1:0], LRCLK };
assign LRCLK_S = lrclk_trg[1]; // синхронизированный LRCLK
wire LRCLK_PRV = lrclk_trg[2]; // предыдущее значение LRCLK
wire LRCLK_CH = LRCLK_PRV ^ LRCLK_S; // любое измененение LRCLK
reg [1:0] adcda_trg; always @(posedge CLK) adcda_trg <= { adcda_trg[0], ADCDA };
wire ADCDA_S = adcda_trg[1]; // синхронизированный DAT
// ==============================================================
// сдвиговый регистр входных данных I2S
// ==============================================================
wire [31:0] shift_w = { shift[30:0], ADCDA_S };
reg [31:0] shift; always @(posedge CLK) if (BCLK_PE) shift <= shift_w;
// вектора для входных данных (для ЦАП)
reg [SAMPLE_WIDTH-1:0] lb;
reg [SAMPLE_WIDTH-1:0] rb;
reg [4:0] bit_cnt; // указатель текущего бита
reg actualLR;
// битстрим выход на ЦАП
wire [4:0] bit_ptr = (~bit_cnt - (32-SAMPLE_WIDTH));
assign DACDA = (bit_cnt < SAMPLE_WIDTH) ? actualLR ? lb[bit_ptr] : rb[bit_ptr] : 1'b0;
// вычисление указателя, защелкивание данных для выхода на ЦАП
always @(posedge CLK)
begin
if (LRCLK_CH)
begin
bit_cnt <= 5'd31;
actualLR <= ~LRCLK_S;
end
else
if (BCLK_NE)
begin
actualLR <= LRCLK_S;
bit_cnt <= bit_cnt + 1'b1;
if (bit_cnt == 5'd31)
begin
lb <= LEFT_IN;
rb <= RIGHT_IN;
end
end
end
// захват данных АЦП
always @(posedge CLK)
begin
if (LRCLK_CH)
begin
if (LRCLK_PRV)
begin
RIGHT_OUT <= shift_w[31:32-SAMPLE_WIDTH];
DATAREADY <= 1'b1;
end
else
LEFT_OUT <= shift_w[31:32-SAMPLE_WIDTH];
end
else
DATAREADY <= 1'b0;
end
endmodule
Не мудрствуя особо проверим в проекте Quartus II. Можно просто подключить выход ко входу, можно пропустить через FIFO и получить задержку. Модуль работает. Я проверял его для 16 и 24 бит.

Спасибо за внимание!
- +5
- 19 января 2013, 13:43
- Karsakbayev
Подскажите правильно ли я понимаю:
«отрицательном фронте BCLK» — это по спаду BCLK
«положительном фронте сигнала BCLK» — это по фронту BCLK
«отрицательном фронте BCLK» — это по спаду BCLK
«положительном фронте сигнала BCLK» — это по фронту BCLK
- Dmitriynik
- 20 января 2013, 11:47
- ↓
Да, вы совершенно правы.
Прошу прощения если как-то непривычно буду называть. Я самоучка, учился в основном по буржуйской литературе, поэтому незадумываясь говорю «коэффициент заполнения» вместо скважности, положительный/отрицательный фронт и т.д.
Прошу прощения если как-то непривычно буду называть. Я самоучка, учился в основном по буржуйской литературе, поэтому незадумываясь говорю «коэффициент заполнения» вместо скважности, положительный/отрицательный фронт и т.д.
- Karsakbayev
- 20 января 2013, 12:28
- ↑
- ↓
Прощаю :) И спасибо за статью, интересно было посмотреть реализацию на Verilog, а то я только VHDL знаю. Буду и Verilog изучать, похоже он удобнее будет.
- Dmitriynik
- 20 января 2013, 16:10
- ↑
- ↓
Я когда начинал, выбирал Verilog или VHDL. Выбрал Verilog, он мне как-то действительно удобнее и нагляднее показался.
- Karsakbayev
- 20 января 2013, 20:31
- ↑
- ↓
Наверное меня сейчас будут ругать, но конкретно для этот модуль я не симулировал. Он заработал можно сказать сразу.
- Karsakbayev
- 06 февраля 2013, 14:38
- ↑
- ↓
Старший бит слова передается на втором положительном фронте сигнала BCLK после изменения сигнала LRCLK.наверно, всеж имелось ввиду, что старший бит слова появляется на втором отрицательном фронте BCLK, да? На картинке вроде так нарисовано; да и не логично менять событие синхронизации (negedge -> posedge) в середине передачи :)
Да, да конечно. Хотел написать «должен приниматься на втором положительном», а написал так.
- Karsakbayev
- 06 февраля 2013, 14:39
- ↑
- ↓
Завёл на вход I2S сигнал со S/PDIF преобразователя и все работает!
Теперь бы сам S/PDIF преобразователь в ПЛИС запихать. Вот только не знаю как правильно делать восстановление клока из потока чтоб джиттер был не большой…
Жду статью «S/PDIF to I2S»;)
Удачи!
Теперь бы сам S/PDIF преобразователь в ПЛИС запихать. Вот только не знаю как правильно делать восстановление клока из потока чтоб джиттер был не большой…
Жду статью «S/PDIF to I2S»;)
Удачи!
Инетерсно, я один не понимаю массовой «истерии» по поводу джиттера в цифровом сигнале?
Ну он есть он, и что? Это явление вообще неустранимо. Это же цифровая схема, 0101 от него не ломаются.
Ну он есть он, и что? Это явление вообще неустранимо. Это же цифровая схема, 0101 от него не ломаются.
Добрый день! Спасибо за отличную статью )
В ваш код закралось несколько ошибок
— теряется последний бит при передаче
— при приеме перепутываются каналы
— проблемы с приемом семплов произвольной длины
Я немного переделал блок и залил его с простым тест бенчем сюда:
github.com/level-two/Modules/tree/master/i2s
Он тянет за собой crossdomain_signal отсюда:
github.com/level-two/Modules/tree/master/crossdomain
Также я добавил сигналы reset и data_sampled.
Надеюсь, это будет кому-то полезно =)
В ваш код закралось несколько ошибок
— теряется последний бит при передаче
— при приеме перепутываются каналы
— проблемы с приемом семплов произвольной длины
Я немного переделал блок и залил его с простым тест бенчем сюда:
github.com/level-two/Modules/tree/master/i2s
Он тянет за собой crossdomain_signal отсюда:
github.com/level-two/Modules/tree/master/crossdomain
Также я добавил сигналы reset и data_sampled.
Надеюсь, это будет кому-то полезно =)
- yauheni_lychkouski
- 26 июля 2017, 18:34
- ↓
Комментарии (18)
RSS свернуть / развернуть