Что делать, если нужно разместить большой цифровой фильтр на FPGA? А если плата уже разведена? Железо старое? В проекте осталось мало места? В этом топике будет рассмотрена одна из возможных реализаций цифрового КИХ фильтра на FPGA Altera Cyclone II EP2C15. По сути это продолжение вот этой темы из песочницы.
Будет рассказано, как сделать сдвиговый регистр на RAM, уменьшив при этом затраты LE, и как из этого получить цифровой фильтр.
Как работает фильтр? Базовая операция — умножение с накоплением. Коэффициенты фильтра перемножаются со значениями в сдвиговом регистре и суммируются. Все, если не вдаваться в подробности. Необходимые ингредиенты озвучены, теперь перейдем к делу.
Считаем, что мы уже определились и желаемым видом АЧХ фильтра, с порядком фильтра, получили его коэффициенты, знаем скорость входных данных. Еще лучше, если эти параметры каким-либо образом параметризовать. Так и попытаемся сделать. Вот такая получилась у меня реализация умножения с накоплением:
Почему ADDR_WIDTH = 9? Потому что порядок фильтра подобран равным 2^9 = 512. Во-первых, это сделано для простоты получения частоты с делителя или PLL. Во-вторых у меня была возможность повышать частоту в 512 раз, потому что sample rate был 16 кГц. Но об этом дальше. Конечно не очень читабельно из-за параметризации, но разобраться можно.
Прочли топик из песочницы по ссылке, что была наверху? Там был шаблон RAM? Вот этот шаблон больше нас не устраивает. Не получилось у меня заставить ту RAM читать/писать за один такт. Может все от не знания, но коэффициенты фильтра хранятся теперь вот в таком модуле:
Примерно 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 аналогична случаю с хранением коэффициентов фильтра:
Единственное, что непроинициализировав RAM она автоматически заполняется нулями. Кстати, этим приемом можно пользоваться при записи коэффициентов фильтра, если их меньше, чем 2^N.
Теперь сама обертка:
Один и тот же адрес подается на RAM с коэффициентами и сдвиговым регистром. По обратной связи через RAM со сдвигового регистра подается на модуль предыдущее значение, которое записывается по текущему адресу. Таким образом сдвиг осуществляется не за один такт, а за каждый по одному значению. На каждый нулевой адрес записывается входное слово.
Зачем я упорно пользуюсь конечным автоматом, хоть некоторые состояния не задействованы? Вспоминаем, что было написано по ссылке в самом начале. Теперь этот модуль работает в два раза быстрее, а значит при прочих равных еще и простаивает половину времени. Теоретически, эту половину можно чем-нибудь занять. Это может быть пересчет коэффициентов фильтра для адаптивной фильтрации, или работа второго фильтра (что-то вроде тайм слота). Тут этого ничего нету и FSM тут не нужен, но я все равно оставил этот атавизм. Убрать FSM всегда проще, чем вписывать его.
Тут приведу топовый файл, который получился из шимантика:
Сразу видно, что можно поправить, чтобы стало красивее.
Теперь еще раз о том, что получилось. Главный минус — данный фильтр full serial. То есть частоту работы фильтра нужно поднимать в 2^(ADDR_WIDTH) раз относительно скорости входных данных. Эту проблему можно решить, если импульсный отклик фильтра симметричный, но при этом RAM сдвигового регистра придется разбивать на два модуля, в которые будут посылаться 2 адреса, значения будут из RAM будут складываться и умножаться в модуле mult, которому придется дописывать еще один вход. Тогда частоту нужно будет поднимать в 2^(ADDR_WIDTH-1) раз.
Исходники и проект в Quartus 9.0
ifolder.ru/27556340
Будет рассказано, как сделать сдвиговый регистр на 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