`timescale 1ns / 1ps // ============================================================================= // Module: tb_dac_stream // Project Name: DigMic_prj // Description: // Тест тракта на ЦАП: отсчёты 12 500 Гц -> dac_stream -> I2S на выводах -> // разбор слов в тестбенче, как это делает PCM5102A (по фронту BCK, MSB // через один BCK после смены LRCK). // // Каждое слово левого канала сверяется с эталоном двумя способами: // точно -- та же целочисленная интерполяция, что должна быть в ПЛИС, // с тем же моментом отсчёта; ловит ошибки разрядности, знака и сдвига; // приближённо -- интерполяция в вещественных числах; ловит ошибку в // самой формуле доли интервала. // Правый канал должен совпасть с левым. Отдельно меряются периоды SCK, // BCK и LRCK. // // WAVE = 0 -- синус AMP на FREQ; WAVE = 1 -- меандр между крайними кодами // шкалы, самые большие разности соседних отсчётов. // // Усиление: GAIN -- постоянное значение на входе gain. GAIN_STEP > 0 -- // каждые GAIN_STEP входных отсчётов gain растёт на 1 по кругу 0..15, то есть // заходит и за GAIN_MAX: там dac_stream обязан ограничить сдвиг. Эталон берёт // то усиление, что действовало в такт взятия каждого отсчёта. // Разобранные отсчёты пишутся в OUT_FILE для анализа спектра. // // iverilog -g2012 -o build/tb_dac_stream tb/tb_dac_stream.v src/dac_stream.v src/i2s_transmitter.v // vvp build/tb_dac_stream // ============================================================================= module tb_dac_stream; parameter integer GAIN = 0; parameter integer GAIN_MAX = 8; parameter integer GAIN_STEP = 0; parameter integer WAVE = 0; parameter real AMP = 0.5; // доля шкалы parameter real FREQ = 1000.0; parameter integer N_WORDS = 2500; // сколько слов ЦАП проверить parameter OUT_FILE = "build/dac_stream_out.txt"; localparam integer CLK_HZ = 50000000; localparam integer IN_HZ = 12500; localparam integer IN_PERIOD = CLK_HZ / IN_HZ; localparam integer RECIP = (1 << 26) / IN_PERIOD; localparam integer FS_MAX = 8388607; localparam integer FS_MIN = -8388608; // От фронта clk, на котором сменился LRCK, назад до такта, чьё состояние // попало в слово: 16 тактов от sample_tick до начала кадра и 3 ступени // конвейера интерполяции localparam integer LAT = 19; reg clk = 1'b0; always #10 clk = ~clk; integer cyc = 0; // номер текущего фронта clk always @(posedge clk) cyc <= cyc + 1; // ------------------------------------------------------------------------- // Источник отсчётов: строб раз в IN_PERIOD тактов, выставляется по спаду // ------------------------------------------------------------------------- reg [23:0] din = 24'd0; reg din_valid = 1'b0; integer ph = IN_PERIOD - 1234; // произвольная фаза относительно I2S integer k_in = 0; function integer gen(input integer k); real v; begin if (WAVE == 0) begin v = AMP * FS_MAX * $sin(2.0 * 3.14159265358979 * FREQ * k / IN_HZ); gen = (v >= 0.0) ? $rtoi(v + 0.5) : -$rtoi(-v + 0.5); end else begin gen = ((k / 7) % 2) ? FS_MIN : FS_MAX; end end endfunction function integer sat_gain(input integer x, input integer g); reg signed [63:0] w; begin w = x; w = w <<< ((g > GAIN_MAX) ? GAIN_MAX : g); if (w > FS_MAX) sat_gain = FS_MAX; else if (w < FS_MIN) sat_gain = FS_MIN; else sat_gain = w; end endfunction reg [3:0] gain = GAIN; always @(negedge clk) begin din_valid <= 1'b0; if (ph == IN_PERIOD - 1) begin ph <= 0; din <= gen(k_in); din_valid <= 1'b1; k_in <= k_in + 1; end else begin ph <= ph + 1; // Смена усиления в середине интервала между отсчётами if (GAIN_STEP > 0 && ph == IN_PERIOD / 2 && k_in % GAIN_STEP == 0) gain <= gain + 1'b1; end end // История входа: на каком фронте ПЛИС взяла отсчёт и что получила после усиления integer hist_t [0:8191]; integer hist_x [0:8191]; integer n_hist = 0; always @(posedge clk) begin if (din_valid) begin hist_t[n_hist] = cyc; hist_x[n_hist] = sat_gain($signed(din), gain); n_hist = n_hist + 1; end end // ------------------------------------------------------------------------- // Эталон: что должно быть на выходе по состоянию после фронта s // ------------------------------------------------------------------------- // Состояние кладётся в e_cur, e_prev, e_cnt: они же нужны для // вещественного эталона и сообщений об ошибке integer e_cur, e_prev, e_cnt; function integer ref_exact(input integer s); integer i; reg signed [63:0] d, f, p; begin i = n_hist - 1; while (i >= 0 && hist_t[i] > s) i = i - 1; e_cur = (i >= 0) ? hist_x[i] : 0; e_prev = (i >= 1) ? hist_x[i - 1] : 0; e_cnt = (i >= 0) ? s - hist_t[i] : s; if (e_cnt > IN_PERIOD - 1) e_cnt = IN_PERIOD - 1; d = e_cur - e_prev; f = (e_cnt * RECIP) >>> 10; p = (d * f + 32768) >>> 16; ref_exact = e_prev + p; end endfunction // ------------------------------------------------------------------------- // Устройство // ------------------------------------------------------------------------- wire i2s_bck, i2s_lrck, i2s_din, dac_sck; dac_stream #( .CLK_FREQ_HZ (CLK_HZ), .IN_RATE_HZ (IN_HZ), .SAMPLE_RATE_HZ (48000), .GAIN_MAX (GAIN_MAX) ) dut ( .clk (clk), .din (din), .din_valid (din_valid), .gain (gain), .i2s_bck (i2s_bck), .i2s_lrck (i2s_lrck), .i2s_din (i2s_din), .dac_sck (dac_sck) ); // ------------------------------------------------------------------------- // Начало кадра: спад LRCK, в тактах clk // ------------------------------------------------------------------------- reg lrck_d = 1'b0; integer p0 = -1; // фронт clk, на котором LRCK ушёл в 0 integer p0_prev = -1; integer errors = 0; always @(posedge clk) begin lrck_d <= i2s_lrck; if (lrck_d && !i2s_lrck) begin p0_prev = p0; p0 = cyc - 1; if (p0_prev >= 0 && p0 - p0_prev != 1024) begin $display("ERROR: LRCK period %0d clk, want 1024", p0 - p0_prev); errors = errors + 1; end end end // Периоды SCK и BCK integer t_sck = -1, t_bck = -1; always @(posedge dac_sck) begin if (t_sck >= 0 && cyc - t_sck != 4) begin $display("ERROR: SCK period %0d clk, want 4", cyc - t_sck); errors = errors + 1; end t_sck = cyc; end always @(posedge i2s_bck) begin if (t_bck >= 0 && cyc - t_bck != 16) begin $display("ERROR: BCK period %0d clk, want 16", cyc - t_bck); errors = errors + 1; end t_bck = cyc; end // ------------------------------------------------------------------------- // Приём I2S как в ЦАП // ------------------------------------------------------------------------- reg lr_b = 1'b0; integer bitn = 99; reg [23:0] sh = 24'd0; integer left = 0; integer n_w = 0; integer n_chk = 0; integer max_dev = 0; integer fd; integer got, want, dev; real ideal, tol; always @(posedge i2s_bck) begin if (i2s_lrck !== lr_b) begin lr_b = i2s_lrck; bitn = 0; // бит задержки I2S end else if (bitn < 24) begin sh = {sh[22:0], i2s_din}; bitn = bitn + 1; if (bitn == 24) begin got = $signed(sh); if (!lr_b) begin left = got; n_w = n_w + 1; // Первые кадры пропускаем: там ещё нет двух отсчётов в истории if (n_hist >= 3) begin want = ref_exact(p0 - LAT); if (got !== want) begin if (errors < 10) $display("ERROR: word %0d at clk %0d: got %0d, want %0d (cnt %0d)", n_w, p0, got, want, e_cnt); errors = errors + 1; end ideal = e_prev + (e_cur - e_prev) * (e_cnt * 1.0 / IN_PERIOD); tol = ((e_cur > e_prev) ? e_cur - e_prev : e_prev - e_cur) * 3.0e-5 + 1.0; dev = (got > ideal) ? $rtoi(got - ideal + 0.5) : $rtoi(ideal - got + 0.5); if (dev > max_dev) max_dev = dev; if (dev > tol) begin if (errors < 10) $display("ERROR: word %0d: got %0d, ideal %0f, tol %0f", n_w, got, ideal, tol); errors = errors + 1; end n_chk = n_chk + 1; $fdisplay(fd, "%0d %0d", p0, got); end end else if (got !== left) begin $display("ERROR: right %0d != left %0d", got, left); errors = errors + 1; end end end end initial begin fd = $fopen(OUT_FILE, "w"); $display("tb_dac_stream: GAIN=%0d GAIN_MAX=%0d GAIN_STEP=%0d WAVE=%0d AMP=%0f", GAIN, GAIN_MAX, GAIN_STEP, WAVE, AMP); wait (n_chk >= N_WORDS); $fclose(fd); $display("checked %0d words, inputs %0d, max deviation from ideal %0d LSB", n_chk, n_hist, max_dev); if (errors == 0) $display("TEST PASSED"); else $display("TEST FAILED: errors = %0d", errors); $finish; end endmodule