Стенд: ЦАП 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
+183
View File
@@ -0,0 +1,183 @@
----------------------------------------------------------------------------------
-- Module Name: tb_gain_button
-- Project Name: DigMic_prj
-- Description:
-- Тест кнопки усиления ЦАП. Такт уменьшен до 100 кГц: показ длится секунды,
-- а в миллисекундах модуль считает сам, так что поведение то же.
--
-- Проверяется:
-- импульс короче времени дребезга -- не нажатие;
-- дребезг на обоих фронтах даёт ровно один шаг, отпускание -- ни одного;
-- ступени идут по кругу 1..GAIN_MAX -> 0;
-- долгое удержание -- один шаг, без автоповтора;
-- показ: N коротких вспышек, для 0 одна длинная, потом светодиод свободен;
-- нажатие во время показа начинает показ заново с новым значением.
----------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.env.all;
entity tb_gain_button is
end entity tb_gain_button;
architecture sim of tb_gain_button is
constant CLK_HZ : integer := 100_000;
constant CLK_T : time := 1 sec / CLK_HZ;
constant GMAX : integer := 3;
signal clk : std_logic := '0';
signal run : boolean := true;
signal btn : std_logic := '0';
signal gain : unsigned(3 downto 0);
signal show : std_logic;
signal led : std_logic;
begin
clk <= not clk after CLK_T / 2 when run else '0';
dut : entity work.gain_button(rtl)
generic map(
SYS_CLK_HZ => CLK_HZ,
GAIN_MAX => GMAX,
BTN_ACTIVE => '1',
DEBOUNCE_MS => 20
)
port map
(
clk => clk,
btn => btn,
gain => gain,
show => show,
led => led
);
stim_proc : process
variable errors : integer := 0;
variable want : integer := 0;
-- Фронт кнопки с дребезгом: четыре отскока по 1-2 мс
procedure bounce(level : std_logic) is
begin
for i in 1 to 4 loop
btn <= level;
wait for 2 ms;
btn <= not level;
wait for 1 ms;
end loop;
btn <= level;
end procedure;
procedure press is
begin
bounce('1');
wait for 100 ms;
bounce('0');
wait for 50 ms;
end procedure;
procedure check_gain(expect : integer; what : string) is
begin
if to_integer(gain) /= expect then
report what & ": gain " & integer'image(to_integer(gain)) &
", want " & integer'image(expect) severity error;
errors := errors + 1;
end if;
end procedure;
-- Вспышки до конца показа: для 0 одна длинная, иначе expect коротких
procedure check_flashes(expect : integer) is
variable n : integer := 0;
variable t0 : time;
variable w : time;
variable wn : time;
begin
if expect = 0 then
wn := 1000 ms;
else
wn := 150 ms;
end if;
if show /= '1' then
report "no display after press" severity error;
errors := errors + 1;
return;
end if;
loop
wait until led = '1' or show = '0';
exit when show = '0';
t0 := now;
wait until led = '0';
w := now - t0;
n := n + 1;
if w < wn - 3 ms or w > wn + 3 ms then
report "flash " & integer'image(n) & " lasts " & time'image(w) &
", want " & time'image(wn) severity error;
errors := errors + 1;
end if;
end loop;
if (expect = 0 and n /= 1) or (expect > 0 and n /= expect) then
report "flashes " & integer'image(n) & " for gain " &
integer'image(expect) severity error;
errors := errors + 1;
else
report "gain " & integer'image(expect) & ": " & integer'image(n) &
" flash(es) of " & time'image(wn);
end if;
end procedure;
begin
wait for 50 ms;
check_gain(0, "after power-up");
assert show = '0' report "display active without a press" severity error;
-- Короткий импульс -- помеха, не нажатие
btn <= '1';
wait for 5 ms;
btn <= '0';
wait for 100 ms;
check_gain(0, "after 5 ms glitch");
if show /= '0' then
report "glitch started a display" severity error;
errors := errors + 1;
end if;
-- По кругу: 1, 2, 3, 0, 1
for k in 1 to GMAX + 2 loop
press;
want := (want + 1) mod (GMAX + 1);
check_gain(want, "press " & integer'image(k));
check_flashes(want);
end loop;
-- Долгое удержание -- один шаг
bounce('1');
wait for 2 sec;
bounce('0');
wait for 50 ms;
want := (want + 1) mod (GMAX + 1);
check_gain(want, "long hold");
if show = '1' then
wait until show = '0';
end if;
-- Нажатие посреди показа: показ заново, с новым значением
press;
want := (want + 1) mod (GMAX + 1);
wait for 700 ms;
press;
want := (want + 1) mod (GMAX + 1);
check_gain(want, "press during display");
check_flashes(want);
if errors = 0 then
report "TEST PASSED";
else
report "TEST FAILED: errors = " & integer'image(errors) severity error;
end if;
run <= false;
stop;
end process stim_proc;
end architecture sim;
+37 -10
View File
@@ -14,6 +14,12 @@
-- Тракт настраивается около 10 мс (запаздывание ФНЧ, заполнение FIFO),
-- поэтому в каждом режиме кадры читаются до первого сошедшегося, но не
-- больше MAX_TRY.
--
-- Микрофон подключён как на железе, через линию LVDS: задержка в обе
-- стороны и удержание уровня, пока микрофон отпускает линию. Задержка
-- подобрана так, чтобы смена данных приходилась на ту же фазу MIC_CLK, что
-- измерена на плате (между тактами 1 и 2 из 16), -- тогда проверяется ровно
-- та точка выборки, что стоит в Top.
----------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
@@ -38,12 +44,21 @@ architecture sim of tb_top is
constant PAYLOAD_LEN : integer := 2 + 1 + SPF * 3; -- CNT + STATUS + DATA
-- Линия: смена данных через 16 + 18 (TDD модели) + 16 = 50 нс после спада
-- MIC_CLK -- между тактами 1 и 2. Сами SN65LVDS дают единицы наносекунд,
-- остальное -- кабель; для выборки важна только получившаяся фаза
constant LINE_FWD : time := 16 ns;
constant LINE_BACK : time := 16 ns;
signal mic_clk_far : std_logic;
signal mic_dat_far : std_logic;
signal mic_dat_kept : std_logic := '0';
signal clk : std_logic := '0';
signal run : boolean := true;
signal mic_clk : std_logic;
signal mic_dat : std_logic;
signal mic_sel : std_logic;
signal uart_tx : std_logic;
signal led : std_logic;
@@ -104,12 +119,12 @@ begin
)
port map
(
clk => clk,
MIC_DAT => mic_dat,
MIC_CLK => mic_clk,
MIC_SEL => mic_sel,
UART_TX => uart_tx,
LED => led
clk => clk,
MIC_DAT => mic_dat,
MIC_CLK => mic_clk,
UART_TX => uart_tx,
BTN_GAIN => '0',
LED => led
);
mic : entity work.pdm_mic_model(model)
@@ -121,11 +136,24 @@ begin
)
port map
(
mic_clk => mic_clk,
mic_clk => mic_clk_far,
mode => mode,
mic_dat => mic_dat
mic_dat => mic_dat_far
);
mic_clk_far <= transport mic_clk after LINE_FWD;
-- Пока микрофон отпускает вывод, вход драйвера LVDS держит последний
-- уровень: на плате смены данных видны только в одной фазе
keep_proc : process (mic_dat_far)
begin
if mic_dat_far = '0' or mic_dat_far = '1' then
mic_dat_kept <= mic_dat_far;
end if;
end process keep_proc;
mic_dat <= transport mic_dat_kept after LINE_BACK;
check_proc : process
variable b : std_logic_vector(7 downto 0);
variable prev : std_logic_vector(7 downto 0);
@@ -144,7 +172,6 @@ begin
variable errors : integer := 0;
begin
wait for CLK_T;
assert mic_sel = '0' report "MIC_SEL does not match the sampling phase" severity error;
report "start: FIR_DC_CODE = " & integer'image(FIR_DC_CODE);
for m in 0 to 3 loop