fpga-systems-magazine

Реализация базовых компонентов ЦОС: КИХ фильтр

Главная » Статьи » Языки » VHDL
Amurak
11.01.2022 10:26
5593
2
4.2


Представленный материал является частью цикла статей, в которых рассматривается RTL реализация базовых компонентов цифровой обработки сигналов. В качестве языка описания используется VHDL-2008, проектирование ведется с учетом особенностей ПЛИС фирмы Xilinx. Для написания тестовых окружений используется UVM.

Исходные коды из статьи выложены на гитхабе (см ссылки в конце статьи)

Скачать статью в формате PDF

Содержание

1 Аннотация

В данной статье рассматриваются особенности реализации одного из базовых компонентов цифровой обработки сигналов – фильтра с конечной импульсной характеристикой. Разобраны отличия практической реализации базовой архитектуры КИХ фильтра от теоретической модели. Представлено RTL описание параметризируемого модуля на языке VHDL-2008. Представлено описание верификационного окружения для RTL модуля с использованием UVM.

2 Теоретическая часть

КИХ фильтр (фильтр с конечной импульсной характеристикой) – один из видов линейных цифровых фильтров, характерной особенностью которого является ограниченность по времени его импульсной характеристики. Выходной сигнал КИХ фильтра описывается уравнением 1:

                 N−1
 y(k) = a(nx(k n), где                                                          (1)
              
n=0

x(k) – отсчеты входного сигнала; y(k) – отсчеты выходного сигнала; a(n) – коэффициенты фильтра;

N – количество коэффициентов фильтра.

труктурная схема КИХ фильтра в простейшем виде представлена на рисунке 2.1.


Рисунок 2.1 – Структурная схема КИХ фильтра, N = 4

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

В интернете часто можно встретить прямое описание данной схемы на HDL, которое выглядит примерно следующим образом:

always @(posedge clk) begin 
  if (data_valid == 1’b1) begin 
  buff0 <= data; 
  buff1 <= buff0; 
  buff2 <= buff1; 
  buff3 <= buff2;
 end
end

assign acc0 = coef0 * buff0; 
assign acc1 = coef1 * buff1; 
assign acc2 = coef2 * buff2; 
assign acc3 = coef3 * buff3; 
assign result = acc0 + acc1 + acc2 + acc3;

Главный недостаток такого описания: с увеличением количества коэффициентов, растет количество слагаемых в итоговой сумме. Схемотехнически это приводит к многовходовому сумматору (либо каскаду сумматоров). Очевидно, что рабочая тактовая частота такой схемы будет крайне мала.

Чтобы избавиться от многовходового аккумулятора, можно разбить схему, представленную на рисунке 2.1, на стадии. На выходе каждой стадии ставятся дополнительные триггеры. Результат такого разбиения показан на рисунке 2.2.


Рисунок 2.2 – КИХ фильтр с дополнительными триггерами

Здесь, при увеличении числа коэффициентов, будет расти количество стадий, но при этом быстродействие не будет падать. Следует помнить, что дополнительные триггеры увеличивают латентность схемы.

Добавив еще пару триггеров к схеме, представленной на рисунке 2.2 и слегка реорганизовав ее, получим схему, представленную на рисунке 2.3.


Рисунок 2.3 – КИХ фильтр, модернизированная архитектура на блоках DSP48

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

3 RTL описание

3.1 Описание интерфейса компонента

Перечень параметров компонента представлен в таблице 3.1.

Таблица 3.1 – Параметры компонента

Имя Тип Описание
g_nof_taps integer длина фильтра (количество коэффициентов)
g_coef_dw integer разрядность коэффициентов
g_sample_dw integer разрядность отсчетов входного сигнала
g_iraxi_coef_dw 1 integer разрядность данных загрузки коэффициентов
g_iraxi_dw 2 integer разрядность данных входного сигнала
g_oraxi_dw 3 integer разрядность данных выходного сигнала

Перечень портов компонента представлен в таблице 3.2.

Таблица 3.2 – Порты компонента

Имя I/O Тип Описание
iclk in std_logic тактовый сигнал
icoef_rst in std_logic загрузка коэффициентов, сброс
icoef_valid in std_logic загрузка коэффициентов, валидность
icoef_data in std_logic_vector[1] загрузка коэффициентов, данные
ivalid in std_logic входной сигнал, валидность
idata in std_logic_vector[2] входной сигнал, данные
ovalid out std_logic выходной сигнал, валидность
odata out std_logic_vector[3] выходной сигнал, данные

Временная диаграмма загрузки коэффициентов фильтра представлена на рисунке 3.4.


Рисунок 3.4 – Временная диаграмма загрузки коэффициентов фильтра

Процесс загрузки коэффициентов начинается с подачи логической ’1’ на вход icoef_rst минимум на один такт. Это приведет к сбросу внутреннего массива коэффициентов, после чего на данный вход необходимо подать логический ’0’.

Коэффициенты загружаются последовательно, начиная с нулевого, через вход icoef_d, при этом каждый коэффициент должен сопровождаться активным уровнем сигнала icoef_v.

Обрабатываемые отсчеты подаются на порт idata и сопровождаются активным уровнем сигнала ivalid.

Результат обработки выдается с выхода odata и сопровождается активным уровнем сигнала ovalid.

 library ieee;
    use ieee.std_logic_1164.all;
    use ieee.numeric_std.all;

entity simple_fir_filter is
    generic(
          g_nof_taps                    : integer := 32
        ; g_coef_dw                     : integer := 16
        ; g_sample_dw                   : integer := 12
        ; g_iraxi_dw_coef               : integer := 16
        ; g_iraxi_dw                    : integer := 12
        ; g_oraxi_dw                    : integer := 48
    );
    port(
          iclk                          : in std_logic
        ; icoef_rst                     : in std_logic
        ; icoef_valid                   : in std_logic
        ; icoef_data                    : in std_logic_vector(g_iraxi_dw_coef - 1 downto 0)
        ; ivalid                        : in std_logic
        ; idata                         : in std_logic_vector(g_iraxi_dw - 1 downto 0)
        ; ovalid                        : out std_logic
        ; odata                         : out std_logic_vector(g_oraxi_dw - 1 downto 0)
    );
end;

3.2 Объявление констант, типов, сигналов

Для реализации КИХ фильтра необходимо объявить константу для разрядности результатов умножения:

constant c_mreg_dw : integer := g_sample_dw + g_coef_dw;

Далее объявим все необходимые типы сигналов:

type t_coef_array -- массивы коэффициентов
 is array (natural range <>) of signed(g_coef_dw - 1 downto 0);
type t_data_array -- массивы данных
 is array (natural range <>) of signed(g_sample_dw - 1 downto 0);
type t_mreg_array -- массивы результатов умножений
 is array (natural range <>) of signed(c_mreg_dw - 1 downto 0);
type t_signed48array -- массивы результатов сложений
 is array (natural range <>) of signed(47 downto 0);

Для простоты разрядность результатов сложений равна разрядности сумматора блока DSP48E1.

Объявим сигналы для приема данных со входных и выдачи данных на выходные порты:

 -- входные буферы
signal ib_coef_rst : std_logic := ’0’;
signal ib_coef_valid : std_logic := ’0’;
signal ib_coef_data : signed(g_coef_dw - 1 downto 0) := (others => ’0’);
signal ib_valid : std_logic := ’0’;
signal ib_data : signed(g_sample_dw - 1 downto 0) := (others => ’0’);

-- выходные буферы
signal ob_valid : std_logic := ’0’;
signal ob_data : signed(47 downto 0) := (others => ’0’);

Наконец, сигналы для реализации основной логики работы фильтра:

 -- массив для загрузки коэффициентов
signal coefs_array : t_coef_array(0 to g_nof_taps - 1) := (others => (others => ’0’));

-- непосредственно фильтр
signal fir_areg1 : t_data_array(0 to g_nof_taps - 1) := (others => (others => ’0’));
signal fir_areg2 : t_data_array(0 to g_nof_taps - 1) := (others => (others => ’0’));
signal fir_breg : t_coef_array(0 to g_nof_taps - 1) := (others => (others => ’0’));
signal fir_mreg : t_mreg_array(0 to g_nof_taps - 1) := (others => (others => ’0’));
signal fir_preg : t_signed48array(0 to g_nof_taps - 1) := (others => (others => ’0’));

3.3 Архитектура

Подключим сигналы для приема данных со входных и выдачи данных на выходные порты:

 -- вход
ib_coef_rst <= icoef_rst;
ib_coef_valid <= icoef_valid;
ib_coef_data <= signed(icoef_data);
ib_valid <= ivalid;
ib_data <= signed(idata);

-- выход
ovalid <= ob_valid;
odata <= std_logic_vector(ob_data);

Опишем процесс загрузки коэффициентов:

    p_coefs : process(iclk)
    begin
        if rising_edge(iclk) then
            if ib_coef_rst = '1' then
                coefs_array <= (others => (others => '0'));
            else
                if ib_coef_valid = '1' then
                    coefs_array <= coefs_array(1 to coefs_array'high) & ib_coef_data;
                end if;
            end if;
        end if;
    end process;

При активном сигнале сброса массив коэффициентов заполняется нулями. После снятия сброса, входные коэффициенты задвигаются в массив по сигналу валидности.

Наконец, непосредственно реализация фильтра:

  p_fir : process(iclk)
    begin
        if rising_edge(iclk) then
            fir_breg <= coefs_array;

            if ib_valid = '1' then
                for a in 0 to g_nof_taps - 1 loop
                    if a = 0 then
                        fir_areg1(a) <= ib_data;
                    else
                        fir_areg1(a) <= fir_areg2(a - 1);
                    end if;
                    fir_areg2(a) <= fir_areg1(a);

                    fir_mreg(a) <= fir_areg2(a) * fir_breg(a);

                    if a = 0 then
                        fir_preg(a) <= resize(fir_mreg(a), 48);
                    else
                        fir_preg(a) <= resize(fir_mreg(a), 48) + fir_preg(a - 1);
                    end if;
                end loop;
            end if;
        end if;
    end process;

    ob_valid                            <= ib_valid;
    ob_data                             <= fir_preg(g_nof_taps - 1);

Здесь описаны преобразования всех триггеров, представленных на рисунке 2.3.

При подаче результата умножения на сумматор, его разрядность расширяется со знаком до 48 бит.

Валидность входного сигнала транслируется на валидность выходного. Данные для выходного сигнала берутся с последнего сумматора.

4 UVM окружение

4.1 Верхний уровень

Верхним уровнем тестового окружения в данном примере является файл tb_simple_fir_filter.sv:

‘timescale 100ps/100ps

module tb_simple_fir_filter;
 import uvm_pkg::*; // [UVM]
 ‘include "uvm_macros.svh" // [UVM]
 import pkg_simplefir::*;
 bit clk = 0;
 always #5 clk = ~clk;
...
endmodule

Пакет pkg_simplefir содержит ссылки на все компоненты, необходимые для реализации тестового окружения.

На верхнем уровне заданы параметры, которые соответствуют параметрам КИХ фильтра, описанным в разделе 3.1:

// данные параметры могут быть заданы снаружи и переданы в тест
parameter G_NOF_TAPS = 32; // g_nof_taps
parameter G_COEF_DW = 16; // g_coef_dw
parameter G_SAMPLE_DW = 12; // g_sample_dw
// внутренние параметры
localparam G_IRAXI_DW_COEF = G_COEF_DW; // g_iraxi_dw_coef
localparam G_IRAXI_DW = G_SAMPLE_DW; // g_iraxi_dw
localparam G_ORAXI_DW = 48; // g_oraxi_dw

Для подключения тестируемого КИХ фильтра объявлены три интерфейса типа raxi_bfm:

raxi_bfm #(
 .DW(G_IRAXI_DW_COEF)
) iraxi_bfm_coef();
raxi_bfm #(
 .DW(G_IRAXI_DW)
) iraxi_bfm();
raxi_bfm #(
 .DW(G_ORAXI_DW)
) oraxi_bfm();

assign iraxi_bfm_coef.clk = clk;
assign iraxi_bfm.clk = clk;
assign oraxi_bfm.clk = clk;

iraxi_bfm – используется для подачи входного сигнала, разрядность данных задается параметром G_IRAXI_DW;

iraxi_bfm_coef – используется для загрузки коэффициентов, разрядность данных задается параметром G_IRAXI_DW_COEF;

oraxi_bfm – используется для считывания выходного сигнала, разрядность данных задается параметром G_ORAXI_DW.

Схема подключения тестируемого компонента показана на рисунке 4.5.


Рисунок 4.5 – Схема подключения тестируемого компонента

Все интерфейсы передаются в uvm_config_db, после чего запускается тест.

initial begin
 uvm_config_db #(virtual raxi_bfm #(G_IRAXI_DW_COEF))::set(
 null, "*", "iraxi_bfm_coef", iraxi_bfm_coef);
 uvm_config_db #(virtual raxi_bfm #(G_IRAXI_DW))::set(
 null, "*", "iraxi_bfm", iraxi_bfm);
 uvm_config_db #(virtual raxi_bfm #(G_ORAXI_DW))::set(
 null, "*", "oraxi_bfm", oraxi_bfm);
 run_test("simplefir_base_test_h");
end

Для реализации параметризируемого теста, его необходимо объявить на верхнем уровне следующим образом:

 typedef simplefir_base_test #(
 .G_NOF_TAPS(G_NOF_TAPS)
 , .G_COEF_DW(G_COEF_DW)
 , .G_SAMPLE_DW(G_SAMPLE_DW)
 , .G_IRAXI_DW_COEF(G_IRAXI_DW_COEF)
 , .G_IRAXI_DW(G_IRAXI_DW)
 , .G_ORAXI_DW(G_ORAXI_DW)
) simplefir_base_test_h;

4.2 Тест

Диаграмма классов, используемых в тесте, представлена на рисунке 4.6.


Рисунок 4.6 – Диаграмма классов теста

Класс simplefir_base_test наследуется от uvm_test.

Чтобы тест мог принимать параметры из верхнего уровня, его необходимо объявить следующим образом:

class simplefir_base_test #(
 G_NOF_TAPS
 , G_COEF_DW
 , G_SAMPLE_DW
 , G_IRAXI_DW_COEF
 , G_IRAXI_DW
 , G_ORAXI_DW
) extends uvm_test;

‘uvm_component_new // common_macros

typedef uvm_component_registry #(simplefir_base_test #(
 G_NOF_TAPS
 , G_COEF_DW
 , G_SAMPLE_DW
 , G_IRAXI_DW_COEF
 , G_IRAXI_DW
 , G_ORAXI_DW
), "simplefir_base_test") type_id;
...
endclass

В тесте используются компоненты, предназначенные для работы с интерфейсом raxi_bfm и транзакциями типа raxi_seqi: секвенсеры типа raxi_seqi, драйверы типа raxi_drvr и мониторы типа raxi_mont. С их помощью обеспечивается передача данных между тестируемым КИХ фильтром и тестовым окружением через интерфейсы, которые извлекаются из uvm_config_db.

Для загрузки коэффициентов в фильтр и для подачи на него входного сигнала используются, соответственно, генераторы транзакций simplefir_seqc_coef_h и simplefir_seqc_data_h. Генераторы запускаются по очереди:

task simplefir_base_test::run_phase(uvm_phase phase);
 phase.raise_objection(this);
 simplefir_seqc_coef_h.start(raxi_seqr_coef);
 #100
 simplefir_seqc_data_h.start(raxi_seqr_data);
 phase.drop_objection(this);
endtask

Для сравнения данных, полученных с выхода тестируемого КИХ фильтра, с эталонной моделью, используется чекер simplefir_scrb_h.

4.3 Генератор транзакций для загрузки коэффициентов

Диаграмма классов, используемых в генераторе транзакций, представлена на рисунке 4.7.


Рисунок 4.7 – Диаграмма классов генератора транзакций для загрузки коэффициентов

Класс simplefir_seqc_coef наследуется от uvm_sequence #(raxi_seqi).

Для формирования массива коэффициентов, которые будут загружены в фильтр, используется функция get_coefs_rcos() класса filter_design:

 

filter_design_h = new();
fir_coefficients = filter_design_h.get_coefs_rcos(0.35, 0.1, G_NOF_TAPS);
rg_firwr_cv = new[fir_coefficients.size];
for (int ii = 0; ii < fir_coefficients.size; ii++)
 rg_firwr_cv[ii] = $rtoi(fir_coefficients[ii] * $pow(2, G_COEF_DW-1));

В функцию передается значение длины фильтра G_NOF_TAPS, в результате функция вернет массив действительных значений такого же размера. Для перевода коэффициентов в фиксированную точку, они домножаются на число, которое зависит от заданной разрядности G_COEF_DW.

Далее генератор создает транзакции raxi_seqi_h для загрузки фильтра согласно временной диаграмме, указанной на рисунке 3.4:

// формирование сигнала сброса
start_item(raxi_seqi_h);
 raxi_seqi_h.rst = 1;
finish_item(raxi_seqi_h);
// последовательная выдача коэффициентов из массива
for (int ii = 0; ii < rg_firwr_cv.size; ii++) begin
 start_item(raxi_seqi_h);
 coef = rg_firwr_cv[ii];
 raxi_seqi_h.rst = 0;
 raxi_seqi_h.valid = 1;
 raxi_seqi_h.data = {<<{coef}};
 finish_item(raxi_seqi_h);
end
// сброс валидности
start_item(raxi_seqi_h);
 raxi_seqi_h.valid = 0;
finish_item(raxi_seqi_h); 

Схема включения генератора транзакций для загрузки коэффициентов в тест представлена на рисунке 4.8.


Рисунок 4.8 – Схема включения генератора транзакций для загрузки коэффициентов

Генератор запускается на секвенсере raxi_seqr_coef, который передает формируемые транзакции типа raxi_seqi в драйвер raxi_drvr_coef. Драйвер, в свою очередь, транслирует их на интерфейс загрузки коэффициентов iraxi_bfm_coef.

4.4 Генератор транзакций для входного сигнала

Диаграмма классов, используемых в генераторе транзакций, представлена на рисунке 4.9.


Рисунок 4.9 – Диаграмма классов генератора транзакций для входного сигнала

Класс simplefir_seqc_data наследуется от uvm_sequence #(raxi_seqi).

Генератор создает транзакции raxi_seqi_h для формирования входного сигнала:

 // тысяча и одна транзакция
repeat (1001) begin
 start_item(raxi_seqi_h);
 raxi_seqi_h.valid = $urandom_range(1, 1);
 if (raxi_seqi_h.valid == 1) begin
 data = $urandom_range(0, D_MAX) - D_MIN;
 end
 raxi_seqi_h.data = {<<{data}};
 finish_item(raxi_seqi_h);
end

Для каждой транзакции генерируется случайный сигнал валидности. В случае, если она оказалась равна ’1’, то формируется случайное значение в диапазоне −2G_SAMPLE_DW−1 ... 2G_SAMPLE_DW−1 −1.

Схема включения генератора транзакций для входного сигнала представлена на рисунке 4.10.


Рисунок 4.10 – Схема включения генератора транзакций для входного сигнала

Генератор запускается на секвенсере raxi_seqr_data, который передает формируемые транзакции типа raxi_seqi в драйвер raxi_drvr_data. Драйвер, в свою очередь, транслирует их на интерфейс подачи входного сигнала iraxi_bfm_data.

4.5 Чекер

Диаграмма классов, используемых в чекере, представлена на рисунке 4.11.


Рисунок 4.11 – Диаграмма классов чекера

Класс simplefir_scrb наследуется от raxi_scrb.

Чекер содержит порты анализа для транзакций коэффициентов iraxi_aprt_coef, входных iraxi_aprt_data и выходных oraxi_aprt_data данных. Данным портам соответствуют функции write_icoef, write_idata и write_odata соответственно.

При поступлении транзакции коэффициентов, вызывается функция add_coefficient() класса sim_simple_fir_filter. В результате коэффициенты, загружаемые в тестируемый КИХ фильтр, также загружаются и в эталонную модель.

При поступлении транзакции входных данных, она сохраняется в очередь iraxi_seqi_queue_data.

При поступлении транзакции выходных данных, она сохраняется в очередь oraxi_seqi_queue_data, после чего вызывается функция processing():

function void simplefir_scrb::processing();
 raxi_seqi iraxi_seqi_data;
 raxi_seqi oraxi_seqi_data;
 raxi_seqi oraxi_seqi_data_sim;
 string data_str;

 // из очередей берутся транзакции входных и выходных данных
 iraxi_seqi_data = iraxi_seqi_queue_data.pop_front();
 oraxi_seqi_data = oraxi_seqi_queue_data.pop_front();

 // формируется эталонная транзакции
 ‘uvm_object_create(raxi_seqi, oraxi_seqi_data_sim);
 sim_simple_fir_filter_h.simulate(iraxi_seqi_data, oraxi_seqi_data_sim);

  // формирование строки для вывода в лог
 data_good = 1;
 data_str = {
 "\n"
 , "input data : ", sim_simple_fir_filter_h.iraxi_iq_2string(iraxi_seqi_data)
 , "\n"
 , "got from RTL: ", sim_simple_fir_filter_h.oraxi_iq_2string(oraxi_seqi_data)
 , "\n"
 , "got from SIM: ", sim_simple_fir_filter_h.oraxi_iq_2string(oraxi_seqi_data_sim)
 , "\n"
 };

// сравнение транзакций, полученных с RTL и SIM
 if (!oraxi_seqi_data.compare(oraxi_seqi_data_sim)) begin
 ‘uvm_error("FAIL", data_str)
 fail_cnt++;
 end else
 ‘uvm_info("PASS", data_str, UVM_HIGH)
endfunction

Для формирования эталонной транзакции вызывается функция simulate() класса sim_simple_fir_filter, в которую передается входная транзакция. Работа данной функции полностью имитирует поведение тестируемого компонента.

Для сравнения транзакций используется функций compare() класса raxi_seqi.

Схема включения чекера в тест представлена на рисунке 4.12.


Рисунок 4.12 – Схема включения чекера

Мониторы iraxi_mont_coef, iraxi_mont_data и oraxi_mont_data анализируют соответствующие интерфейсы, формируют транзакции типа raxi_seqi и передают их на соответствующие порты анализа чекера.

5 Ссылки

Скачать статью в формате PDF

5593
2
4.2

Всего комментариев : 2
avatar
1 Titan2009 • 12:07, 18.01.2022
Неплохо было бы добавить использование FIR Compiler smile
avatar
0
2 KeisN13 • 12:16, 18.01.2022
Также пригодится




avatar

FPGA-Systems – это живое, постоянно обновляемое и растущее сообщество.
Хочешь быть в курсе всех новостей и актуальных событий в области?
Подпишись на рассылку

ePN