Стенд: ЦАП PCM5102A, кнопка усиления, линия LVDS, UART через BL616

- вторая ветвь тракта: отсчёты ФНЧ -> пересчёт в 48 828 Гц -> I2S на
  PCM5102A (dac_stream.v, i2s_transmitter.v), тест tb_dac_stream.v;
- кнопка S1 (H11): усиление ветви ЦАП ступенями по +6 дБ, показ ступени
  светодиодом (gain_button.vhd), тест tb_gain_button.vhd;
- микрофон через линию SN65LVDS: точка выборки MIC_SMP_CNT = 12 по
  измерению на плате (pdm_rx.vhd), в tb_top модель линии;
- новая распиновка: MIC_CLK K8, MIC_DAT K9, ЦАП G1/G2/L4/L3, UART_TX C3
  на встроенный мост BL616;
- сборка под GW5A-25 ревизии A, настройки PnR (CPU/SSPI/READY как GPIO);
- отладочный выход PLL 80 МГц на E3.
This commit is contained in:
2026-10-02 14:21:06 +03:00
parent 668c0564e9
commit 2672f8b8a8
16 changed files with 2181 additions and 40 deletions
+279
View File
@@ -0,0 +1,279 @@
`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