В предыдущей статье я писал об изготовлении самодельного микрофонного шилда к плате Марсоход3bis.
Для чего мне понадобилась такая плата? Мне захотелось сделать "новогодний проект" - "Цветомузыка". Я хочу сделать автономное устройство, которое будет слушать микрофоном окружающий звук, оцифровывать его с помощью АЦП в ПЛИС платы MAX10, далее фильтровать на низкие, средние и высокие частоты и светить соответственно тремя цветами: красным, зеленым и синим в такт музыки.
Конечно, я понимаю, что оцифровывать звук таким маленьким электретным микрофоном типа CZN-15E - это не очень здоровое занятие.. но хотелось получить именно автономное устройство, которое бы не зависило от проигрывателя, компьютера или еще чего..
Пожалуй самый сложный вопрос в этом проекте будет цифровой фильтр. Далее я расскажу, как собираюсь делать фильтр в FPGA MAX10 платы Марсоход3bis.
Есть два способа реализации цифрового фильтра в ПЛИС Intel/Altera. Первый способ - использовать встроенные мегафунции САПР Intel Quartus Prime. Для этого можно через меню квартуса Tools => IP Catalog поискать в библиотеках Library => DSP => Filters => FIR II и запустить визард, который поможет создать нужные экземпляры устройств фильтров. Фильтры, предоставляемые САПР Quartus Prime, имеют множество настроек:
Действительно мощный инструмент, однако, реальное использование многих мегафункций из IP Catalog Quartus Prime может потребовать дополнительной платной лицензии. Это не наш путь. Попробую пойти другим путем, написать на Verilog свой собственный цифровой фильтр. Если вы следите за нашим блогом, то увидите, что у меня уже была статья про КИХ фильтр, написанный на верилоге. Этот фильтр в общем замечательно работает в симуляции, однако, честно говоря я сомневаюсь, что его получится использовать в этом проекте. Причина простая - тот фильтр представляет в точности схему, как ее рисуют в книжках: линия задержки на регистрах, умножители по числу регистров задержки, итоговый сумматор. Вот так:
В реальном проекте может оказаться, что число регистров в линии задержки велико, значит потребуется много регистров и много умножителей. Думаю в обычную ПЛИС такое может и не поместится.
Попробуем рассчитать коэффициенты КИХ фильтра средних частот в онлайн-калькуляторе t-filter:
Например, при частоте оцифровки звука 50кГц, для полосового КИХ фильтра средних частот от 700Гц до 2700Гц потребуется линия задержки в 327 регистров. Столько же нужно умножителей. это слишком много. Но что же делать в этом случае? Выход прост. Частота оцифровки - всего 50кГц. Можно записывать отсчеты в память ПЛИС в циклический буфер, а за интервал времени между записями можно перечитывать все ранее накопленные отсчеты, перемножать на нужные коэффициенты из ПЗУ и складывать в регистр накопитель результата.
Попробую проиллюстрировать идею следующим рисунком:
Понятно, что на такой фильтр нужно подавать рабочую частоту в 512 раз выше частота оцифровки полезного сигнала. То есть, если оцифровывать звук на частоте 50кГц, то рабочая частота такого модуля фильтра будет 50000*512 = 25600000 Гц. Это всего 25,6 МГц и это совсем не много для FPGA.
Каждые 512 тактов рабочей частоты будет производиться всего одна запись очередной выборки сигнала в циклическую память. Однако, за эти же 512 тактов нужно перечитать каждый элемент из блока памяти и параллельно каждый элемент из постоянной памяти, где хранятся коэффициенты фильтра.
Накапливающий регистр обнуляется при каждой записи выборки сигнала, и потом туда суммируются результаты умножения отсчетов фильтра на соответствующие коэффициенты фильтра.
Итак, я написал вот такой модуль на языке Verilog HDL:
`timescale 1ns/1ps
module fir_filter(
input wire nreset,
input wire clk, //idata12 sampling frequency
input wire [11:0]idata12, //samples unsigned
output reg signed [47:0]out_val,
output reg out_ready
);
parameter MIF = "coeffs/mid/coeffs-450-5800.mif";
reg [8:0]rd_addr;
reg [8:0]wr_addr;
//make signed input sample from unsigned
wire signed [15:0]idata; assign idata = { idata12, 4'h0 }-16'h8000;
//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_coef_rom_param #(.MIF(MIF)) fir_coef_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
Здесь в модуле имеются девятиразрядный регистр-счетчик указатель чтения из памяти rd_addr, и девятиразрядный регистр-счетчик указатель записи в память wr_addr. При этом, регистр rd_addr увеличивается постоянно с каждым тактом, а адрес записи wr_addr увеличивается лишь каждые 512 тактов. Одновременно с увеличением регистра адреса wr_addr вырабатывается импульс записи wr, который записывает в память очередной отсчет входных оцифрованных данных. В модуле установлена память для хранения входных отсчетов. Я для симуляции использую Icarus Verilog, мне так удобнее, поэтому для икаруса с помошью условной компиляции я вставляю модуль памяти типа dp_mem_1clk_p (экземпляр mem_samples) - это компонент взятый мной когда-то с opencores. Количество элементов в памяти - 512 шестнадцатибитных слов. Получается если все время инкрементировать адреса, то после адреса 0x1FF будет адрес 0x000 - циклический буфер. Для компиляции квартуса я сделаю визардом Quartus модуль Dual Port RAM (DPRAM). И я знаю квартусовский DPRAM работает так же как и простой модуль dp_mem_1clk_p. Но различие конечно есть - квартусовский библиотечный DPRAM будет использовать встроенные блоки памяти из FPGA, а не регистры.
Так же здесь установлена постоянная память, в которой хранятся коэффициенты КИХ фильтра. Для икаруса, этот тот же dp_mem_1clk_p (экземпляр mem_coeff). Здесь так же 512 слов по 16 бит. Когда буду компилировать квартусом, то тут будет установлен компонент созданный визардом квартуса ROM и туда будет загружен Memory Initialization File (*.MIF).
Отмечу, что адреса на ROM растут в противоположном направлении, не так как хранятся отсчеты в памяти данных, так как более старые выборки оказываются позади головы - указателя записи. И к более старым отсчетам нужно прилагать коэффициенты фильтра, которые расположены в более высоких адресах.
Для накопления разультата фильтра используется 48-ми разрядный регистр filter_val_acc. Думаю этой разрядности должно хватить.
Ниже приведен тестбенч на Verilog. С его помощью я хочу проверить, как мой фильтр подавляет не нужные частоты. Я проверяю Middle-Pass-Filter. Фильтр, который пропускается средние частоты где-то с 700Гц до 2700Гц на зеленую лампу цветомузыки. Коффициенты фильтра рассчитаны в онлайн-калькуляторе t-filter. Частотная характеристика фильтра должна получиться такая, как на рисунке выше.
`timescale 1ns / 1ps
module tb();
reg reset;
//assume basic clock is 10Mhz
reg clk;
initial clk=0;
always
#50 clk = ~clk;
//fir clk
reg fir_clk;
initial fir_clk=0;
always
#19.531 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 [11:0]sin_val;
//generate requested "freq" sinus
always @(posedge cnt_edge)
begin
sin_real <= sin(my_time);
sin_val <= sin_real*2047+2047;
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 ),
.idata12( sin_val ),
.out_val( out ),
.out_ready( out_rdy )
);
initial
begin
$dumpfile("out.vcd");
$dumpvars(1,
tb.freq,
tb.fir_.idata12,
tb.fir_.out_val
);
reset = 1;
#1000;
reset = 0;
read_bp_coeff();
my_time=0;
for ( freq=300; freq<4000; freq=freq+200 )
begin
#20000000;
if( freq>1000 )
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("coeffs/mid/fresp_700_2700.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
В этом тестбенче есть синтезатор синусоиды. Про него подробнее можно почитать здесь. Тестбенч управляет этим генератором и он выдает частоты по очереди в интервале от 300Гц до 4000Гц. Эта частота подается на экземпляр моего фильтра fir_filter fir_. Внутренняя память фильтра для хранения коэффициентов КИХ фильтра заполняется тестбенчем из задачи read_bp_coeff(). Там буквально открывается текстовый файл coeffs/mid/fresp_700_2700.txt и из него построчно считываются значения коэффициентов и вписываются в память fir_.mem_coeff.mem[i].
Тестбенч записывает изменения сигналов модулей в выходной файл out.vcd. Это определяется системной функцией $dumpfile. Этот файл потом можно посмотреть в программе GtkWave. Сигналы, которые мне интересны для просмотра я задаю системной функцией $dumpvars. Можно конечно записать в VCD файл вообще все сигналы, но тогда файл получается просто огромный, гигабайты. Это и медленно и не удобно.
Теперь покажу, что получилось в результате симуляции. Как обычно, я запускаю компилятор Icarus Verilog, затем симулятор икаруса и GtkWave:
>iverilog -DICARUS=1 -o qqq tb.v fir_filter.v dp_mem_1clk_p.v
>vvp qqq
>gtkwave out.vcd
После запуска GtkWave вижу вот такие сигналы:
На временной диаграмме видно, что на выходе фильтра частоты до 700Гц и после 3000Гц не проходят. Так и должно быть, это соответствует расчетным значениям и желаемой Амплитудно-Частотной Характеристике (АЧХ).
Для проекта цветомузыки потребуется как минимум три фильтра: один для низких частот (красный цвет), этот фильтр для средних частот (зеленый цвет) и высоких частот (синий цвет).
В следующих статьях надеюсь будет уже полностью опубликован проект цветомузыки.
Подробнее...