Pull to refresh

Verilog. Цифровой фильтр на RAM

Reading time 5 min
Views 24K
Что делать, если нужно разместить большой цифровой фильтр на FPGA? А если плата уже разведена? Железо старое? В проекте осталось мало места? В этом топике будет рассмотрена одна из возможных реализаций цифрового КИХ фильтра на FPGA Altera Cyclone II EP2C15. По сути это продолжение вот этой темы из песочницы.
Будет рассказано, как сделать сдвиговый регистр на RAM, уменьшив при этом затраты LE, и как из этого получить цифровой фильтр.


Как работает фильтр? Базовая операция — умножение с накоплением. Коэффициенты фильтра перемножаются со значениями в сдвиговом регистре и суммируются. Все, если не вдаваться в подробности. Необходимые ингредиенты озвучены, теперь перейдем к делу.

Умножение с накоплением

Считаем, что мы уже определились и желаемым видом АЧХ фильтра, с порядком фильтра, получили его коэффициенты, знаем скорость входных данных. Еще лучше, если эти параметры каким-либо образом параметризовать. Так и попытаемся сделать. Вот такая получилась у меня реализация умножения с накоплением:
module mult
#(parameter COEF_WIDTH = 24, parameter DATA_WIDTH = 16, parameter ADDR_WIDTH = 9, parameter MULT_WIDTH = COEF_WIDTH + DATA_WIDTH)
    (
    input   wire                                    clk,
    input   wire                                    en,
    input   wire            [ (ADDR_WIDTH-1) :  0 ] ad,
    input   wire    signed  [ (COEF_WIDTH-1) :  0 ] coe,
    input   wire    signed  [ (DATA_WIDTH-1) :  0 ] pip,
    output  wire    signed  [ (DATA_WIDTH-1) :  0 ] dout
    );

wire signed [(MULT_WIDTH-1) :  0 ] mu = coe * pip;

reg signed [ (MULT_WIDTH-1) :  0 ] rac = {(MULT_WIDTH){1'b0}};
reg signed [ (DATA_WIDTH-1) :  0 ] ro = {DATA_WIDTH{1'b0}};

assign dout = ro;

always @(posedge clk)
if(en)
    if(ad == {ADDR_WIDTH{1'b0}})
    begin
        rac <= mu;
        ro <= rac[ (MULT_WIDTH-2) -: (DATA_WIDTH) ];
    end
    else
        rac <= rac + mu;

endmodule


Почему ADDR_WIDTH = 9? Потому что порядок фильтра подобран равным 2^9 = 512. Во-первых, это сделано для простоты получения частоты с делителя или PLL. Во-вторых у меня была возможность повышать частоту в 512 раз, потому что sample rate был 16 кГц. Но об этом дальше. Конечно не очень читабельно из-за параметризации, но разобраться можно.

Коэффициенты фильтра

Прочли топик из песочницы по ссылке, что была наверху? Там был шаблон RAM? Вот этот шаблон больше нас не устраивает. Не получилось у меня заставить ту RAM читать/писать за один такт. Может все от не знания, но коэффициенты фильтра хранятся теперь вот в таком модуле:

module coef
#(parameter DATA_WIDTH=24, parameter ADDR_WIDTH=9)
    (
    input wire [(DATA_WIDTH-1):0] data,
    input wire [(ADDR_WIDTH-1):0] addr,
    input wire we,
    input wire clk,
    output wire [(DATA_WIDTH-1):0] coef_rom
    );

reg [DATA_WIDTH-1:0] rom[2**ADDR_WIDTH-1:0];

reg [(DATA_WIDTH-1):0] data_out;

assign coef_rom = data_out;

initial
begin
  rom[0  ] = 24'b000000000000000000000000;
  rom[1  ] = 24'b000000000000000000000001;
//new year tree 
  rom[510] = 24'b000000000000000000000001;
  rom[511] = 24'b000000000000000000000000;


	end

always @ (posedge clk)
begin
    data_out <= rom[addr];

    if (we)
        rom[addr] <= data;
end
endmodule


Примерно 508 коэффициентов были пропущены, чтобы не нагонять уныние. Почему 24 бита, а не 16? Спектр мне больше нравится. Но это не принципиально. Поменять коэффициенты — занятие не долгое. К тому же можно прикрепить файл инициализации памяти скриптом $readmemb или $readmemh после initial begin.

Сдвиговый регистр

Вот собственно основная причина, почему я это пишу. Может кто-то подумает про себя, что это и так знал. Может еще что подумает об авторе хорошего, что-то там про колесо.
Тут будет написано, как на RAM сделать сдвиговый регистр при помощи обертки. Наверно каждый читал в handbook на свою FPGA о том, что RAM может работать, как сдвиговый регистр. Как? У меня получилось, в этом нет ничего сложного. Только зачем? Семейство Cyclone позиционируется, как устройства с уклоном на память «devices feature embedded memory structures to address the on-chip memory needs of FPGA designs.» И нужно уметь этой памятью пользоваться. Задача решается в два эта: RAM и обертка. RAM аналогична случаю с хранением коэффициентов фильтра:

module pip
#(parameter DATA_WIDTH=16, parameter ADDR_WIDTH=9)
    (
    input wire [(DATA_WIDTH-1):0] data,
    input wire [(ADDR_WIDTH-1):0] read_addr, write_addr,
    input wire we,
    input wire clk,
    output wire [(DATA_WIDTH-1):0] pip_ram
    );

reg [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH-1:0];

reg [(DATA_WIDTH-1):0] data_out;

assign pip_ram = data_out;

always @ (posedge clk)
begin
    data_out <= ram[read_addr];
    if (we)
        ram[write_addr] <= data;
end

endmodule


Единственное, что непроинициализировав RAM она автоматически заполняется нулями. Кстати, этим приемом можно пользоваться при записи коэффициентов фильтра, если их меньше, чем 2^N.
Теперь сама обертка:

module upr
#(parameter COEF_WIDTH = 24, parameter DATA_WIDTH = 16, parameter ADDR_WIDTH = 9) 
    (
    input wire                          clk,
    input wire                          en,
    input wire  [ (DATA_WIDTH-1) :  0 ] ram_upr,
    input wire  [ (DATA_WIDTH-1) :  0 ] data_in,

    output wire [ (DATA_WIDTH-1) :  0 ] upr_ram,
    output wire                         we_ram,
    output wire [ (ADDR_WIDTH-1) :  0 ] adr_out
    );

assign upr_ram = (r_adr == {ADDR_WIDTH{1'b0}}) ? data_in : ram_upr;
assign we_ram = (r_state == state1) ? 1'b1 : 1'b0;
assign adr_out = r_adr;

reg [  2 :  0 ] r_state = state0;
localparam      state0 = 3'b001,
                state1 = 3'b010,
                state2 = 3'b100;

reg [ (ADDR_WIDTH-1) :  0 ] r_adr = {ADDR_WIDTH{1'b0}};

always @(posedge clk)
if(en)
begin
    case(r_state)
        state0:
            r_state <= state1;

        state1:
            r_state <= state1;

        state2:
            begin
            end
    endcase
end

always @(posedge clk)
case(r_state)
    state0:
        r_adr <= {ADDR_WIDTH{1'b0}};

    state1:
        r_adr <= r_adr + 1'b1;

    state2:
        begin
        end
endcase

endmodule

Один и тот же адрес подается на RAM с коэффициентами и сдвиговым регистром. По обратной связи через RAM со сдвигового регистра подается на модуль предыдущее значение, которое записывается по текущему адресу. Таким образом сдвиг осуществляется не за один такт, а за каждый по одному значению. На каждый нулевой адрес записывается входное слово.
Зачем я упорно пользуюсь конечным автоматом, хоть некоторые состояния не задействованы? Вспоминаем, что было написано по ссылке в самом начале. Теперь этот модуль работает в два раза быстрее, а значит при прочих равных еще и простаивает половину времени. Теоретически, эту половину можно чем-нибудь занять. Это может быть пересчет коэффициентов фильтра для адаптивной фильтрации, или работа второго фильтра (что-то вроде тайм слота). Тут этого ничего нету и FSM тут не нужен, но я все равно оставил этот атавизм. Убрать FSM всегда проще, чем вписывать его.

Итого

Тут приведу топовый файл, который получился из шимантика:

module filtr_ram(
	CLK,
	D_IN,
	MULT
);

input	CLK;
input	[15:0] D_IN;
output	[15:0] MULT;

wire	SYNTHESIZED_WIRE_13;
wire	[15:0] SYNTHESIZED_WIRE_1;
wire	[8:0] SYNTHESIZED_WIRE_14;
wire	SYNTHESIZED_WIRE_4;
wire	[15:0] SYNTHESIZED_WIRE_15;
wire	SYNTHESIZED_WIRE_6;
wire	[0:23] SYNTHESIZED_WIRE_8;
wire	[23:0] SYNTHESIZED_WIRE_11;

assign	SYNTHESIZED_WIRE_4 = 1;
assign	SYNTHESIZED_WIRE_6 = 0;
assign	SYNTHESIZED_WIRE_8 = 0;

pip	b2v_inst(
	.we(SYNTHESIZED_WIRE_13),
	.clk(CLK),
	.data(SYNTHESIZED_WIRE_1),
	.read_addr(SYNTHESIZED_WIRE_14),
	.write_addr(SYNTHESIZED_WIRE_14),
	.pip_ram(SYNTHESIZED_WIRE_15));
	defparam	b2v_inst.ADDR_WIDTH = 9;
	defparam	b2v_inst.DATA_WIDTH = 16;

upr	b2v_inst1(
	.clk(CLK),
	.en(SYNTHESIZED_WIRE_4),
	.data_in(D_IN),
	.ram_upr(SYNTHESIZED_WIRE_15),
	.we_ram(SYNTHESIZED_WIRE_13),
	.adr_out(SYNTHESIZED_WIRE_14),
	.upr_ram(SYNTHESIZED_WIRE_1));
	defparam	b2v_inst1.ADDR_WIDTH = 9;
	defparam	b2v_inst1.COEF_WIDTH = 24;
	defparam	b2v_inst1.DATA_WIDTH = 16;

coef	b2v_inst3(
	.we(SYNTHESIZED_WIRE_6),
	.clk(CLK),
	.addr(SYNTHESIZED_WIRE_14),
	.data(SYNTHESIZED_WIRE_8),
	.coef_rom(SYNTHESIZED_WIRE_11));
	defparam	b2v_inst3.ADDR_WIDTH = 9;
	defparam	b2v_inst3.DATA_WIDTH = 24;

mult	b2v_inst5(
	.clk(CLK),
	.en(SYNTHESIZED_WIRE_13),
	.ad(SYNTHESIZED_WIRE_14),
	.coe(SYNTHESIZED_WIRE_11),
	.pip(SYNTHESIZED_WIRE_15),
	.dout(MULT));
	defparam	b2v_inst5.ADDR_WIDTH = 9;
	defparam	b2v_inst5.COEF_WIDTH = 24;
	defparam	b2v_inst5.DATA_WIDTH = 16;

endmodule


Сразу видно, что можно поправить, чтобы стало красивее.
Теперь еще раз о том, что получилось. Главный минус — данный фильтр full serial. То есть частоту работы фильтра нужно поднимать в 2^(ADDR_WIDTH) раз относительно скорости входных данных. Эту проблему можно решить, если импульсный отклик фильтра симметричный, но при этом RAM сдвигового регистра придется разбивать на два модуля, в которые будут посылаться 2 адреса, значения будут из RAM будут складываться и умножаться в модуле mult, которому придется дописывать еще один вход. Тогда частоту нужно будет поднимать в 2^(ADDR_WIDTH-1) раз.

Исходники и проект в Quartus 9.0
ifolder.ru/27556340
Tags:
Hubs:
+12
Comments 0
Comments Leave a comment

Articles