I2S в FPGA на Verilog

WM8731Добрый день, уважаемые! Помню, в прошлый раз в статье S/PDIF на FPGA я пообещал продолжение о работе с аудиокодеком. Вот, наконец, нашел время написать продолжение.
Сначала хотел описать регистры конфигурации кодека WM8731, но потом решил начать с описания интерфейса I2S, потому что описание регистров это, можно считать, практически перевод датащита. В следующей статье напишу о регистрах конфигурации.
I2S (Integrated Interchip Sound) – это последовательная шина для передачи цифрового звука. Впервые стандарт был представлен в 1986 году, а последняя ревизия вышла в 1996-м. У I2S раздельные линии тактирования и данных, поэтому джиттер будет намного меньше, чем, например, в интерфейсах подобных S/PDIF.

Для передачи звука по I2S в одну сторону требуется как минимум 3 линии

Bit clock — BCLK (тактирование);
Word select — LRCLK (линия выбора канала);
Data line – DAT (линия передачи аудио данных).

При этом аудиокодек может работать в двух режимах — ведущем и ведомом. В ведущем режиме кодек сам генерирует импульсы BCLK и LRCLK, а в ведомом их должен генерировать, например, ваш контроллер или ПЛИС.
I2S Master/Slave
Посмотрите на диаграмму ниже.
I2S Timings
Линия 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. Для выделения фронтов также используется триггер. Т. е. у нас три последовательно соединенных триггера. Возможно это сходу не видно в коде, там используется сдвиговый регистр.
Подробно о синхронизации вы можете почитать, например, здесь.
На выходе синтезатора получается такая картина (это участок модуля).
Crossing clock domains sync
Для сохранения полученных данных нужен 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 бит.
Диаграма Quartus
Спасибо за внимание!

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

RSS свернуть / развернуть
нифига какая милипуська на первой картинке
0
  • avatar
  • 21h
  • 19 января 2013, 22:02
Это кажется, просто керамика там огромная :). А чип QFN28.
0
Подскажите правильно ли я понимаю:
«отрицательном фронте BCLK» — это по спаду BCLK
«положительном фронте сигнала BCLK» — это по фронту BCLK
0
Да, вы совершенно правы.
Прошу прощения если как-то непривычно буду называть. Я самоучка, учился в основном по буржуйской литературе, поэтому незадумываясь говорю «коэффициент заполнения» вместо скважности, положительный/отрицательный фронт и т.д.
0
Прощаю :) И спасибо за статью, интересно было посмотреть реализацию на Verilog, а то я только VHDL знаю. Буду и Verilog изучать, похоже он удобнее будет.
0
Я когда начинал, выбирал Verilog или VHDL. Выбрал Verilog, он мне как-то действительно удобнее и нагляднее показался.
0
Интересно, а VHDL изначально поддерживает тип чисел — действительное (дроби всякие) при синтезировании в квартусе? Или, как с верилогом, надо свои модули писать для работы с действительными числами? А так-то да, верилог сильно попроще — за то он мне и понравился.
0
можно Ваши тестбенчи посмотреть?
0
Наверное меня сейчас будут ругать, но конкретно для этот модуль я не симулировал. Он заработал можно сказать сразу.
0
Старший бит слова передается на втором положительном фронте сигнала BCLK после изменения сигнала LRCLK.
наверно, всеж имелось ввиду, что старший бит слова появляется на втором отрицательном фронте BCLK, да? На картинке вроде так нарисовано; да и не логично менять событие синхронизации (negedge -> posedge) в середине передачи :)
0
Да, да конечно. Хотел написать «должен приниматься на втором положительном», а написал так.
0
Отлично! Только начал осваивать тему, а тут уже всё разжевали:) Попробую…
0
Завёл на вход I2S сигнал со S/PDIF преобразователя и все работает!
Теперь бы сам S/PDIF преобразователь в ПЛИС запихать. Вот только не знаю как правильно делать восстановление клока из потока чтоб джиттер был не большой…
Жду статью «S/PDIF to I2S»;)
Удачи!
0
Инетерсно, я один не понимаю массовой «истерии» по поводу джиттера в цифровом сигнале?
Ну он есть он, и что? Это явление вообще неустранимо. Это же цифровая схема, 0101 от него не ломаются.
0
Используя FIFO переходим на другой клок, и никаких джиттеров.
0
Я имел в виду вопли «аудиофилов» которые жалуются на джиттер в SPDIF (!) мотивируя тем что они его якобы слышат.
0
Ах, эти аудиофилы :)))
0
Добрый день! Спасибо за отличную статью )
В ваш код закралось несколько ошибок
— теряется последний бит при передаче
— при приеме перепутываются каналы
— проблемы с приемом семплов произвольной длины

Я немного переделал блок и залил его с простым тест бенчем сюда:
github.com/level-two/Modules/tree/master/i2s

Он тянет за собой crossdomain_signal отсюда:
github.com/level-two/Modules/tree/master/crossdomain

Также я добавил сигналы reset и data_sampled.

Надеюсь, это будет кому-то полезно =)
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.