Схема эксперимента
Схема эксперимента

В своей предыдущей статье я написал, как произвести оцифровку звукового сигнала платой FPGA MCY316. В том проекте данные полученные из АЦП просто передавались в компьютер через последовательный порт. Уже на компьютере данные принимались из последовательного порта программой на питоне и отображались графиками в окне.

А сейчас я хочу модифицировать этот проект и добавить в FPGA еще цифровой КИХ фильтр, чтобы разобраться, как он работает.

Рисунок выше показывает схему моего эксперимента.

КИХ фильтр это фильтр с конечной импульсной характеристикой. Если на такой фильтр подается короткий импульс, то на его выходе через некоторое время от сигнала не останется и следа. А всё потому, что описывается фильтр вот каким математическим выражением:

F_{t}=\sum_{k=0}^{N-1} Y_{t-k}\cdot S_{t}

Здесь St это отсчёты сигнала, а Yt-k это коэффициенты фильтра. Всего отсчётов N.

Получается вот такая схема фильтра:

Традиционная схема КИХ фильтра
Традиционная схема КИХ фильтра

Квадратики S это цепочка регистров. С каждым новым приходящим отсчетом сигнала каждый регистр передает хранящийся в нем отсчет следующему регистру. При частоте оцифровки F соседние регистры хранят значения сигнала с интервалом времени 1/F.

Кружочки это умножители на соответствующий коэффициент Y.

Искусство создания цифрового КИХ фильтра состоит в выборе длины цепочки регистров и расчёте коэффициентов Y. Как можно рассчитать фильтр? Если сказать просто, то фильтр должен выделять нужный нам сигнал. Коэффициенты Y хранящиеся в цифровом фильтре это отсчеты эталонного сигнала, который мы хотим обнаружить. Если на такой фильтр подать одиночный импульс, то на выходе как раз получится эталонный сигнал.

Существует множество методов расчёта фильтров и для них написаны готовые программы и библиотеки для Mathlab, GNU Octave, Python.

Предположим, что я хочу создать для звукового сигнала полосовой фильтр (bandpass) с пропусканием от 600Гц до 2200Гц. Программа на питоне для расчёта такого фильтра может выглядеть вот так:

import numpy as np
from scipy import signal
import matplotlib.pyplot as plt

num_taps = 501 # it helps to use an odd number of taps
cut_off = [600.0, 2200.0] # Hz
sample_rate = 46875 # Hz

#T = signal.firwin(num_taps, cut_off, window = "hamming", fs=sample_rate )
T = signal.firwin(num_taps, cutoff = cut_off, window = "hamming", fs=sample_rate, pass_zero = False)
Ta = np.array(T)
Ta_abs_max = np.amax(np.abs(Ta))
Ta_scaled = Ta*(32766/Ta_abs_max)
Ta_scaled_int = Ta_scaled.astype(int)
print( Ta )
print( Ta_scaled_int )
np.savetxt('fir-coeffs.txt', Ta_scaled_int, fmt='%d' ) 

print("========= Altera Memory Initialization File ===================")
print("WIDTH = 16;")
print("DEPTH = 512;")
print("ADDRESS_RADIX = HEX;")
print("DATA_RADIX = HEX;")
print("CONTENT BEGIN")
i=0
while i < len(Ta_scaled_int) :
	print( f"{i:04x}",":", f"{Ta_scaled_int[i]&0xffff:04x}",";")
	i=i+1
print("END")
print("================================================================")

w, h = signal.freqz(T,fs=sample_rate,include_nyquist=False)

fig, ax1 = plt.subplots()
ax1.set_title('Digital filter frequency response')
ax1.plot(w, 20 * np.log10(abs(h)), 'b')
ax1.set_ylabel('Amplitude [dB]', color='b')
ax1.set_xlabel('Frequency [rad/sample]')
ax2 = ax1.twinx()
angles = np.unwrap(np.angle(h))
ax2.plot(w, angles, 'g')
ax2.set_ylabel('Angle (radians)', color='g')
ax2.grid(True)
ax2.axis('tight')
plt.show()

Такая питоновская программа отработает и нарисует вот такую итоговую частотную характеристику:

Амплитудно-частотная и фазо-частотные характеристики фильтра
Амплитудно-частотная и фазо-частотные характеристики фильтра

Ну и конечно, эта программа еще выдаст список более 500 коэффициентов фильтра:

[-1.98161865e-04 -1.88693513e-04 -1.70507493e-04 -1.44797941e-04
 -1.13348819e-04 -7.83990087e-05 -4.24696335e-05 -8.16611985e-06
  2.20303956e-05  4.59664915e-05  6.19988370e-05  6.91407007e-05
  6.71599779e-05  5.66193512e-05  3.88532870e-05  1.58812526e-05
 -9.73848540e-06 -3.51052202e-05 -5.72086133e-05 -7.31806289e-05
 -8.05468297e-05 -7.74562747e-05 -6.28698992e-05 -3.66895801e-05
  1.85968102e-07  4.58869791e-05  9.76822284e-05  1.52167589e-04
  2.05521707e-04  2.53810532e-04  2.93317172e-04  3.20869991e-04
  3.34140421e-04  3.31882943e-04  3.14093089e-04  2.82065134e-04
  2.38338818e-04  1.86533662e-04  1.31079230e-04  7.68594287e-05
  2.87976107e-05 -8.58395309e-06 -3.15924150e-05 -3.77416223e-05
 -2.60417625e-05  2.82941136e-06  4.64957021e-05  1.01002501e-04
  1.61085330e-04  2.20570999e-04  2.72882654e-04  3.11609038e-04
  3.31090312e-04  3.26968676e-04  2.96652381e-04  2.39646763e-04
  1.57715444e-04  5.48482047e-05 -6.29717152e-05 -1.88190415e-04
 -3.12268004e-04 -4.26380806e-04 -5.22183857e-04 -5.92568032e-04
 -6.32343297e-04 -6.38782772e-04 -6.11971626e-04 -5.54919647e-04
 -4.73415723e-04 -3.75624708e-04 -2.71450537e-04 -1.71711753e-04
 -8.71948709e-05 -2.76652008e-05 -9.22474985e-07 -1.19887444e-05
 -6.25082253e-05 -1.50423395e-04 -2.69969696e-04 -4.12004401e-04
 -5.64655741e-04 -7.14248804e-04 -8.46437746e-04 -9.47452001e-04
 -1.00534965e-03 -1.01116567e-03 -9.59847193e-04 -8.50882173e-04
 -6.88551514e-04 -4.81765292e-04 -2.43479579e-04  1.02721050e-05
  2.61662531e-04  4.92568115e-04  6.86154767e-04  8.28413800e-04
  9.09508078e-04  9.24799747e-04  8.75454053e-04  7.68547449e-04
  6.16649687e-04  4.36895576e-04  2.49608530e-04  7.65808001e-05
 -6.08496040e-05 -1.43761365e-04 -1.57477525e-04 -9.30892382e-05
  5.14731461e-05  2.70935874e-04  5.52812515e-04  8.78183614e-04
  1.22308179e-03  1.56038302e-03  1.86205560e-03  2.10158077e-03
  2.25633609e-03  2.30972824e-03  2.25287556e-03  2.08567371e-03
  1.81712579e-03  1.46487975e-03  1.05398419e-03  6.14943586e-04
  1.81219535e-04 -2.13620600e-04 -5.38867392e-04 -7.69486161e-04
 -8.88531054e-04 -8.88926887e-04 -7.74436926e-04 -5.59700440e-04
 -2.69302192e-04  6.40793507e-05  4.02314761e-04  7.05052077e-04
  9.33266078e-04  1.05282547e-03  1.03776235e-03  8.72943226e-04
  5.55882890e-04  9.75089362e-05 -4.78229308e-04 -1.13591537e-03
 -1.83128900e-03 -2.51474575e-03 -3.13543575e-03 -3.64562187e-03
 -4.00492239e-03 -4.18405998e-03 -4.16776900e-03 -3.95657476e-03
 -3.56724787e-03 -3.03184641e-03 -2.39538016e-03 -1.71225352e-03
 -1.04175727e-03 -4.42972267e-04  3.04868860e-05  3.35430765e-04
  4.43309317e-04  3.43283242e-04  4.38408466e-05 -4.27234784e-04
 -1.02460552e-03 -1.68873435e-03 -2.35062225e-03 -2.93756072e-03
 -3.37943246e-03 -3.61502785e-03 -3.59782419e-03 -3.30070166e-03
 -2.71914354e-03 -1.87258441e-03 -8.03719544e-04  4.24240422e-04
  1.73220653e-03  3.03140643e-03  4.23065541e-03  5.24411002e-03
  5.99884697e-03  6.44160060e-03  6.54404046e-03  6.30607423e-03
  5.75681306e-03  4.95302562e-03  3.97511819e-03  2.92089363e-03
  1.89754428e-03  1.01250393e-03  3.63906941e-04  3.14657810e-05
  6.85739925e-05  4.96368127e-04  1.30034283e-03  2.42991777e-03
  3.80111778e-03  5.30226763e-03  6.80234030e-03  8.16135667e-03
  9.24203385e-03  9.92173992e-03  1.01037470e-02  9.72679111e-03
  8.77204880e-03  7.26682036e-03  5.28445650e-03  2.94036135e-03
  3.84227125e-04 -2.21102237e-03 -4.66280516e-03 -6.79395348e-03
 -8.44809614e-03 -9.50408227e-03 -9.88819881e-03 -9.58305782e-03
 -8.63225034e-03 -7.14016640e-03 -5.26674513e-03 -3.21731695e-03
 -1.22810184e-03  4.51699508e-04  1.57997057e-03  1.94242115e-03
  1.37240771e-03 -2.31767523e-04 -2.89355342e-03 -6.55007415e-03
 -1.10501644e-02 -1.61589715e-02 -2.15691018e-02 -2.69177966e-02
 -3.18091506e-02 -3.58399760e-02 -3.86275921e-02 -3.98376219e-02
 -3.92098062e-02 -3.65799301e-02 -3.18961728e-02 -2.52285447e-02
 -1.67705300e-02 -6.83258217e-03  4.17231352e-03  1.57502346e-02
  2.73537830e-02  3.84136884e-02  4.83725176e-02  5.67182725e-02
  6.30156468e-02  6.69328436e-02  6.82621456e-02  6.69328436e-02
  6.30156468e-02  5.67182725e-02  4.83725176e-02  3.84136884e-02
  2.73537830e-02  1.57502346e-02  4.17231352e-03 -6.83258217e-03
 -1.67705300e-02 -2.52285447e-02 -3.18961728e-02 -3.65799301e-02
 -3.92098062e-02 -3.98376219e-02 -3.86275921e-02 -3.58399760e-02
 -3.18091506e-02 -2.69177966e-02 -2.15691018e-02 -1.61589715e-02
 -1.10501644e-02 -6.55007415e-03 -2.89355342e-03 -2.31767523e-04
  1.37240771e-03  1.94242115e-03  1.57997057e-03  4.51699508e-04
 -1.22810184e-03 -3.21731695e-03 -5.26674513e-03 -7.14016640e-03
 -8.63225034e-03 -9.58305782e-03 -9.88819881e-03 -9.50408227e-03
 -8.44809614e-03 -6.79395348e-03 -4.66280516e-03 -2.21102237e-03
  3.84227125e-04  2.94036135e-03  5.28445650e-03  7.26682036e-03
  8.77204880e-03  9.72679111e-03  1.01037470e-02  9.92173992e-03
  9.24203385e-03  8.16135667e-03  6.80234030e-03  5.30226763e-03
  3.80111778e-03  2.42991777e-03  1.30034283e-03  4.96368127e-04
  6.85739925e-05  3.14657810e-05  3.63906941e-04  1.01250393e-03
  1.89754428e-03  2.92089363e-03  3.97511819e-03  4.95302562e-03
  5.75681306e-03  6.30607423e-03  6.54404046e-03  6.44160060e-03
  5.99884697e-03  5.24411002e-03  4.23065541e-03  3.03140643e-03
  1.73220653e-03  4.24240422e-04 -8.03719544e-04 -1.87258441e-03
 -2.71914354e-03 -3.30070166e-03 -3.59782419e-03 -3.61502785e-03
 -3.37943246e-03 -2.93756072e-03 -2.35062225e-03 -1.68873435e-03
 -1.02460552e-03 -4.27234784e-04  4.38408466e-05  3.43283242e-04
  4.43309317e-04  3.35430765e-04  3.04868860e-05 -4.42972267e-04
 -1.04175727e-03 -1.71225352e-03 -2.39538016e-03 -3.03184641e-03
 -3.56724787e-03 -3.95657476e-03 -4.16776900e-03 -4.18405998e-03
 -4.00492239e-03 -3.64562187e-03 -3.13543575e-03 -2.51474575e-03
 -1.83128900e-03 -1.13591537e-03 -4.78229308e-04  9.75089362e-05
  5.55882890e-04  8.72943226e-04  1.03776235e-03  1.05282547e-03
  9.33266078e-04  7.05052077e-04  4.02314761e-04  6.40793507e-05
 -2.69302192e-04 -5.59700440e-04 -7.74436926e-04 -8.88926887e-04
 -8.88531054e-04 -7.69486161e-04 -5.38867392e-04 -2.13620600e-04
  1.81219535e-04  6.14943586e-04  1.05398419e-03  1.46487975e-03
  1.81712579e-03  2.08567371e-03  2.25287556e-03  2.30972824e-03
  2.25633609e-03  2.10158077e-03  1.86205560e-03  1.56038302e-03
  1.22308179e-03  8.78183614e-04  5.52812515e-04  2.70935874e-04
  5.14731461e-05 -9.30892382e-05 -1.57477525e-04 -1.43761365e-04
 -6.08496040e-05  7.65808001e-05  2.49608530e-04  4.36895576e-04
  6.16649687e-04  7.68547449e-04  8.75454053e-04  9.24799747e-04
  9.09508078e-04  8.28413800e-04  6.86154767e-04  4.92568115e-04
  2.61662531e-04  1.02721050e-05 -2.43479579e-04 -4.81765292e-04
 -6.88551514e-04 -8.50882173e-04 -9.59847193e-04 -1.01116567e-03
 -1.00534965e-03 -9.47452001e-04 -8.46437746e-04 -7.14248804e-04
 -5.64655741e-04 -4.12004401e-04 -2.69969696e-04 -1.50423395e-04
 -6.25082253e-05 -1.19887444e-05 -9.22474985e-07 -2.76652008e-05
 -8.71948709e-05 -1.71711753e-04 -2.71450537e-04 -3.75624708e-04
 -4.73415723e-04 -5.54919647e-04 -6.11971626e-04 -6.38782772e-04
 -6.32343297e-04 -5.92568032e-04 -5.22183857e-04 -4.26380806e-04
 -3.12268004e-04 -1.88190415e-04 -6.29717152e-05  5.48482047e-05
  1.57715444e-04  2.39646763e-04  2.96652381e-04  3.26968676e-04
  3.31090312e-04  3.11609038e-04  2.72882654e-04  2.20570999e-04
  1.61085330e-04  1.01002501e-04  4.64957021e-05  2.82941136e-06
 -2.60417625e-05 -3.77416223e-05 -3.15924150e-05 -8.58395309e-06
  2.87976107e-05  7.68594287e-05  1.31079230e-04  1.86533662e-04
  2.38338818e-04  2.82065134e-04  3.14093089e-04  3.31882943e-04
  3.34140421e-04  3.20869991e-04  2.93317172e-04  2.53810532e-04
  2.05521707e-04  1.52167589e-04  9.76822284e-05  4.58869791e-05
  1.85968102e-07 -3.66895801e-05 -6.28698992e-05 -7.74562747e-05
 -8.05468297e-05 -7.31806289e-05 -5.72086133e-05 -3.51052202e-05
 -9.73848540e-06  1.58812526e-05  3.88532870e-05  5.66193512e-05
  6.71599779e-05  6.91407007e-05  6.19988370e-05  4.59664915e-05
  2.20303956e-05 -8.16611985e-06 -4.24696335e-05 -7.83990087e-05
 -1.13348819e-04 -1.44797941e-04 -1.70507493e-04 -1.88693513e-04
 -1.98161865e-04]

Поскольку я собираюсь реализовать этот фильтр в ПЛИС, то использование плавающей точки хоть и возможно, но скорее всего неоправданно. Можно отмасштабировать эти коэффициенты хотя бы к знаковому 16ти битному значению. Тут нужно понимать, что всякая реализация требует ресурсов: памяти, регистров, умножителей. Нужно найти компромисс между точностью, занятыми ресурсами, временем, затраченным на реализацию проекта.

Вот у меня в этом фильтре сейчас 501 коэффициент фильтра. Значит и для хранения входных отсчетов нужно столько же регистров. В FPGA можно построить такую цепочку:

Умножаю 501 на 16 бит сигнала получается 8016 триггеров/логических элементов будет занято в ПЛИС. Довольно много так-то. Какие еще есть варианты? Можно использовать встроенную в ПЛИС статическую память.

Выделяю в FPGA два блока памяти длиной 512 элементов: один для входящих сигналов, а второй для хранения коэффициентов фильтра. Блоки памяти в виде циклических буферов.

Программа, показанная выше, так же напечатает содержимое MIF файла, для блока памяти Альтеры, используемой в FPGA.

Каждый приходящий новый отсчет сигнала записывается по указателю записи, который потом инкрементируется. Но! Пока не пришёл следующий отсчет сигнала нужно быстренько пробежаться по всему буферу и перечитать его полностью. Так же пробежаться по массиву коэффициентов и тоже перечитать его попутно производя перемножения и суммирование результатов. Если рабочая частота у меня будет 24МГц, то частота оцифровки получится 24000000/512=46875Гц. Вполне подходит для оцифровки звука и моих простых экспериментов. При такой частоте оцифровки по теоремме Котельникова я могу рассчитывать на обработку звуковых сигналов до 23,4КГц.

Реализую такой фильтр в ПЛИС на языке Verilog HDL (кстати, когда уже Хабр внедрит в свой редактор поддержку Verilog HDL?).

`timescale 1ns/1ps

module fir_filter(
		input wire nreset,
		input wire clk,						//idata sampling frequency
		input wire [15:0]idata,				//samples unsigned
		output reg signed [47:0]out_val,
		output reg out_ready
	);

reg  [8:0]rd_addr;
reg  [8:0]wr_addr;

//read samples from cyclic buffer
always @(posedge clk or negedge nreset)
	if( ~nreset )
		rd_addr <= 0;
	else
		rd_addr <= rd_addr + 1;

//"wr" signal -> writes new sample to cyclic buffer
reg wr;
always @(posedge clk or negedge nreset)
	if( ~nreset )
		wr <= 1'b0;
	else
		wr <= (rd_addr==9'h1ff);

always @(posedge clk or negedge nreset)
	if( ~nreset )
		wr_addr <= 0;
	else
	if( rd_addr==9'h1ff )
		wr_addr <= wr_addr + 1;

wire signed [15:0]odata;

//cyclic buffer for samples
`ifdef ICARUS
dp_mem_1clk_p #( .DATA_WIDTH(16), .ADDR_WIDTH(9), .RAM_DEPTH(1 << 9) )mem_samples
	(
	.Clk( clk ),
	.Reset_N( nreset ),
	.we( wr ),
	.rd( nreset ),
	.wr_addr( wr_addr ),
	.rd_addr( rd_addr ),
	.data_in( idata ),
	.data_out( odata )
	);
`else
fir_ram	fir_ram_inst (
	.clock ( clk ),
	.data ( idata ),
	.rdaddress ( rd_addr ),
	.wraddress ( wr_addr ),
	.wren ( wr ),
	.q ( odata )
	);
`endif

//fir coefficients contiguously extracted from filter embeddef memory
wire [8:0]rd_addr_coeff; 
assign rd_addr_coeff = 512 - rd_addr + wr_addr;
wire signed [15:0]fir_coeff;

`ifdef ICARUS
dp_mem_1clk_p #( .DATA_WIDTH(16), .ADDR_WIDTH(9), .RAM_DEPTH(1 << 9) )mem_coeff
	(
	.Clk( clk ),
	.Reset_N( nreset ),
	.we( 1'b0 ),
	.rd( nreset ),
	.wr_addr( 9'd0 ),
	.rd_addr( rd_addr_coeff ),
	.data_in( 16'd0 ),
	.data_out( fir_coeff )
	);
`else
fir_rom fir_rom_inst (
	.address ( rd_addr_coeff ),
	.clock ( clk ),
	.q ( fir_coeff )
	);
`endif

reg signed [47:0]filter_val_acc;
always @(posedge clk or negedge nreset)
	if( ~nreset )
	begin
		out_val <= 0;
		filter_val_acc <=0;
	end
	else
	if( wr )
	begin
		out_val <= filter_val_acc;
		filter_val_acc <= fir_coeff * odata;
	end
	else
	begin
		filter_val_acc <= filter_val_acc + fir_coeff * odata;
	end

always @( posedge clk or negedge nreset )
	if( ~nreset )
		out_ready <= 0;
	else
		out_ready <= wr;

endmodule

А для проверки работоспособности этого безобразия напишу тестбенч Verilog:

`timescale 1ns / 1ps

module tb();

reg reset;

//assume basic clock is 10Mhz
reg clk;
initial clk=0;
always
	#50 clk = ~clk;

//fir clk is sampling freq * 512
//sampling freq is 46875
reg fir_clk;
initial fir_clk=0;
always
	#20.833 fir_clk = ~fir_clk;

//function calculating sinus 
function real sin;
input x;
real x;
real x1,y,y2,y3,y5,y7,sum,sign;
begin
	sign = 1.0;
	x1 = x;
	if (x1<0)
	begin
		x1 = -x1;
		sign = -1.0;
	end
	while (x1 > 3.14159265/2.0)
	begin
		x1 = x1 - 3.14159265;
		sign = -1.0*sign;
	end  
	y = x1*2/3.14159265;
	y2 = y*y;
	y3 = y*y2;
	y5 = y3*y2;
	y7 = y5*y2;
	sum = 1.570794*y - 0.645962*y3 +
		0.079692*y5 - 0.004681712*y7;
	sin = sign*sum;
end
endfunction

//generate requested "freq" digital
integer freq;
reg [31:0]cnt;
reg cnt_edge;
always @(posedge clk or posedge reset)
begin
	if(reset)
	begin
		cnt <=0;
		cnt_edge <= 1'b0;
	end
	else
	if( cnt>=(10000000/(freq*64)-1) )
	begin
		cnt<=0;
		cnt_edge <= 1'b1;
	end
	else
	begin
		cnt<=cnt+1;
		cnt_edge <= 1'b0;
	end
end

real my_time;
real sin_real;
reg signed [15:0]sin_val;

//generate requested "freq" sinus
always @(posedge cnt_edge)
begin
	sin_real <= sin(my_time);
	sin_val  <= sin_real*32767; //fit 16bit signed short word
	my_time  <= my_time+3.14159265*2/64;
end

wire signed [47:0]out;
wire out_rdy;
fir_filter fir_(
		.nreset( ~reset ),
		.clk( fir_clk ),
		.idata( sin_val ),
		.out_val( out ),
		.out_ready( out_rdy )
	);

wire signed [15:0]out_val_; assign out_val_ = out[35:20];

initial
begin
	$dumpfile("out.vcd");
		
	$dumpvars(1,
		tb.freq,
		tb.sin_val,
		tb.out_val_
		);

	reset = 1;
	#1000;
	reset = 0;

	read_bp_coeff();
	
	my_time=0;

	for ( freq=300; freq<4000; freq=freq+100 )
	begin
		#20000000;
		if( freq>1000 && freq<2000 )
			freq=freq+200;
	end

	$finish; 
end

integer file_filter;
integer i;
integer scan_result;
reg signed [15:0]coeff;

task read_bp_coeff;
begin
	file_filter = $fopen("python/fir-coeffs.txt", "r");
	if (file_filter == 0) begin
		$display("file bp filter handle was NULL");
		$finish;
	end
	for( i=0; i<512; i=i+1 )
	begin
		scan_result = $fscanf(file_filter, "%d\n", coeff); 
		if ( scan_result!=1 ) 
			coeff = 0;
		//$display("coeff %d = %d",i,coeff);
		fir_.mem_coeff.mem[i] = coeff;
	end
	$fclose(file_filter);
end
endtask

endmodule

Тут должен заметить, что симулировать проект я буду в Icarus Verilog, мне так проще, а он естественно не знает про существование Алтеровских блоков памяти. Поэтому я в проект вставляю их "эквивалент" типа dp_mem_1clk_p.v. Тестбенч считает текстовый файл python/fir-coeffs.txt" с коэффициентами фильтра, которые ранее были найдены с помощью питоновской программы и запишет эти значения в память fir_.mem_coeff.mem. Ну и потом только тестбенч начнет работать. Он вычисляет синусоидальный сигнал меняя его от 300Гц до 4000Гц и подает на фильтр. Вообще у меня в проекте используется условная компиляция по макросу ICARUS. Если он определен, то используются простые модули памяти dp_mem. А если макрос не определен, то будут использоваться блоки памяти сгенерированные в альтеровском MegaWizard. В этом визарде для блоков памяти типа ALT_DP_ROM можно задавать Memory Initialization FIle (MIF), который у меня уже сгенерирован программой на Python.

Запустить icarus verilog можно вот такой последовательностью команд (компиляция, симуляция, отображение):

>iverilog -DICARUS=1 -o tb.bin tb.v fir_filter.v dp_mem_1clk_p.v
>vvp tb.bin
>gtkwave out.vcd

Симулятор vvp во время симуляции сгенерирует файл изменений сигналов проекта во времени out.vcd. Его передаем в GtkWave. Тут я смогу рассмотреть выход фильтра:

Результат симуляции фильтра, частоты от 300Гц до 900Гц
Результат симуляции фильтра, частоты от 300Гц до 900Гц

И вот еще:

Результат симуляции фильтра, частоты от 1700Гц до 2500Гц
Результат симуляции фильтра, частоты от 1700Гц до 2500Гц

Судя по симулятору всё работает как нужно. Нужные частоты проходят через фильтр, а не нужные частоты подавляются фильтром. В тестбенче сигнал sin_val это синусоида которая подается на фильтр, а out_val_ это выход фильтра.

Теперь вопрос, как это будет работать в реальном железе.

Я использую плату FPGA MCY316 с USB-JTAG программатором MBFTDI. На плате стоит ПЛИС Altera Cyclone III с почти 16ю тысячами логических элементов. Ну и на плате стоит звуковое АЦП PCM1801 и аудио разъемы для подключения сигнала.

Плата MCY316 с установленным программатором выглядит вот так:

Полное описание платы можно почитать вот здесь.

Я собираюсь подключить мой смартфон аудио кабелем к этой плате. На смартфоне Андроид я установил программу генератора сигналов и буду подавать синусоидальный сигнал разной частоты в плату. Собственно я это уже делал в своем предыдущем проекте, почитайте пожалуйста вот здесь. Но теперь проект усовершенствован, в него добавлен цифровой полосовой фильтр описанный выше. Проект, работающий в FPGA, будет получать сигнал от АЦП, установленной на плате, и передавать на фильтр. Далее в компьютер будет передаваться левый канал как чистый сигнал, а правый канал как отфильтрованный сигнал. А в программе на питоне отрисуем графики обоих сигналов. Вот что получается:

В смартфоне на лету меняю частоту генератора и вижу на экране компьютера в окне плота, как выглядит прямой и отфильтрованный сигнал. Все работает! Частоты до 600Гц и выше 2200Гц не проходят фильтр, а остальные проходят!

Все исходники проектов можно взять на гитхаб.

Вот такие интересные эксперименты можно проводить с FPGA и обычным смартфоном. Добавлю, что уверен, что этот проект должен / будет работать на более дешевой плате MCY112.

И ещё, если в FPGA проект вставить три таких фильтра, но только на нижние частоты, средние и высокие, то есть подобрать соответствующие коэффициенты для этих фильтров, то можно сделать цветомузыку. Особенно эфектно она может смотреться при управлении адресной светодиодной лентой. Впрочем, я такое уже когда-то делал для другой платы.

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


  1. YakovK
    10.10.2023 08:12

    КИХ фильтр это FIR?


    1. nckma Автор
      10.10.2023 08:12
      +1

      Да, КИХ Это фильтр с Конечной Импульсной Характеристикой. То же самое как FIR - Finite Impulse Response.


  1. aamonster
    10.10.2023 08:12

    А можно не надо?

    В смысле я понимаю, что у взятой ПЛИС есть дофига ресурсов, которые можно потратить на параллельное умножение 500 чисел, и FIR на неё идеально ложится, но не оптимизировать фильтры – это ж путь в пекло (ну в смысле тепловыделение всё больше). Конечно, это просто учебный пример, но дети ж поверят, что так и надо...


    1. nckma Автор
      10.10.2023 08:12

      Ну конкретно в этом примере нет параллельного умножения 500 чисел. Есть последовательное.


      1. aamonster
        10.10.2023 08:12

        Тем более :-) (в смысле, "бонусом" у нас ещё и понижение производительности из-за того, что используем последовательно 500 умножений там, где хватило бы десятка).

        Впрочем, простите за невнимательность. Потом почитаю статью как следует, тема-то для меня интересная (давно хочу освоить) и примеры кажутся достаточно простыми.


  1. yamifa_1234
    10.10.2023 08:12

    А окно из 500 отсчётов скользящее? Или шагает по 500 отсчётов? Вы ыглядит как шагающее окно. И ещё вопрос, почему такой тип окна(скользящее или нет...) выбрали?


    1. nckma Автор
      10.10.2023 08:12

      Окно конечно скользящее. Одна выборка сигнала пришла и одна ушла.

      А что бывает по другому?


      1. yamifa_1234
        10.10.2023 08:12

        Можно набирать по 500 отсчётов и выдавать по 500 отсчётов. Недостаток у такого режима в том что стыки между окон кривые будут. Ну или можно набирать новые 100 отсчётов а потом выдавать 100, в итоге получится шаг по 100 отсчётов, тогда стыки окон не будут такими плохими.

        P.s. я начинаю чувствовать что сильно ошибаюсь...


        1. aamonster
          10.10.2023 08:12

          1. Это уже не FIR будет, а какая-то нелинейщина.

          2. Формула с 500 отсчётами даёт одно значение на выходе. Так что чтобы получить 500 значений – вам надо 500 раз посчитать по ней, так что выгоды от работы по кускам нету (ну, есть другие фильтры, где есть – например, порезать по N отсчётов, бпф, фильтрация, обратное бпф – тут, понятно, по блокам надо)


          1. yamifa_1234
            10.10.2023 08:12

            Действительно, я ошибся.


  1. Gudd-Head
    10.10.2023 08:12

    частота оцифровки получится 24000000/512=46875Гц

    Почему 512, если отчётов у вас только 501?


    1. nckma Автор
      10.10.2023 08:12

      Всё равно память в FPGA выделяется блоками. Нельзя сделать блок длиной 501 элементов.

      Да и указатели циклического буфера легче перемещать. После ячейки по адресу 511 сразу идет ячейка по адресу ноль. Ну и коэффициенты по адресам выше 501 равны нулю.


  1. alex_dow
    10.10.2023 08:12

    Самый простой способ поиграть с ких - SigmaDSP ADAU1761, от чипдип, 1500р, все из коробки уже есть. Я на нем делал фонокорректор, ну и много чего там еще интересного.
    И да нужен SigmaLink-USBi программатор 1700р


    1. nckma Автор
      10.10.2023 08:12
      +1

      В мире вообще много чего есть готового и интересного. Но свой изобретенный велосипед всегда интересней и познавательней.