Поведенческая модель процессора БПФ
(Из журнала ARGC & ARGV)
А.М.Сергиенко
Чтобы оценить возможности VHDL при исследовании алгоритмов цифровой обработки сигналов и начать на практике их использование, рассмотрим разработку и применение блока для вычисления БПФ.
Процедура БПФ широко используется в цифровой обработке сигналов для получения амплитудного и фазового спектров сигнала, а также для свертки длинных последовательностей отсчетов сигналов. В Matlab’е встроена процедура FFT, которая вычисляет БПФ для массива N комплексных данных с плавающей запятой. Если N равно степени двойки, то эта процедура выполняется довольно быстро. Результаты БПФ можно получить в виде графиков, выполнив процедуру PLOT.
В симуляторах VHDL обычно цифровые сигналы поступают, вырабатываются и наблюдаются в виде графиков, получаемых в процессе моделирования алгоритма. Но алгоритм БПФ предполагает, что данные поступают и выдаются сразу в виде массивов. Также многие другие алгоритмы обработки, как, например, решение систем уравнений, требуют аналогичного поступления данных. Поэтому целесообразно вычислять БПФ как цепочку процедур. Первая из них, называемая Gather_С, накапливает входной массив комплексных исходных данных. Вторая процедура – FFT – выполняет алгоритм БПФ, а заключительная процедура Scatter_С выдает результирующий массив в виде последовательности данных.
Указанные процедуры выполняются объектом FFTbeh. Этот объект можно вставлять в нужное место разрабатываемого проекта, как компонент, с целью измерить спектр интересующих сигналов или отобразить сигнал из временной области в частотную. Интерфейс этого объекта и перечень используемых стандартных библиотек выглядит так:
library IEEE; use IEEE.std_logic_1164.all; use IEEE.std_logic_ARITH.all; use IEEE.MATH_REAL.all; use IEEE.MATH_COMPLEX.all; entity FFTbeh is generic( N:positive:=8; -- 2**N -длина преобразования IDCT:natural:=0; -- 0- прямое, 1- обратное преобразование PT:natural -- масштаб результата =2**PT ); port ( CLK: in STD_LOGIC; -- синхросерия RST: in STD_LOGIC; -- асинхронный сброс START: in STD_LOGIC; -- начало накопления входного массива EDIN: in STD_LOGIC; -- строб данных DIN_RE: in integer; -- реальная часть данных DIN_IM: in integer; -- мнимая часть данных DOUT_RE: out integer:=0;-- реальная часть спектра DOUT_IM: out integer:=0;-- мнимая часть спектра NUM: out integer:=0; -- номер выходного отсчета READY: out STD_LOGIC -- начало вывода результатов ); end FFTbeh;
В библиотеках IEEE.MATH_REAL и IEEE.MATH_COMPLEX хранятся объявление типа комплексного данного, такие функции, как синус, косинус, квадратный корень, функции от комплексных аргументов и другие, которые применяются для вычисления БПФ.
Настроечная переменная N задает длину преобразования. При IDCT = 0 выполняется прямое преобразование, а при 1 – обратное. Переменная PT задает масштаб результата. Таким образом, преобразование равно
где Xi – отсчет входных комплексных данных: DIN_RE +j*DIN_IM, Yk – отсчет комплексного спектра: DOUT_RE +j*DOUT_IM.
По сигналу, приходящему в порт START, начинается накопление массива входных данных. Окончание вычислений БПФ указывается сигналом с выходного порта READY.
Выдаваемые отсчеты спектра сопровождаются номером отсчета NUM, по которому удобно находить значение частоты той или иной спектральной линии.
Различные блоки обработки сигналов целесообразно выполнить с интерфейсом, аналогичным интерфейсу объекта FFTbeh. Тогда их можно соединять между собой последовательно в соответствии с основным алгоритмом. При этом выходной порт READY одного блока соединяется с входным портом START следующего блока.
Сигнал на порте EDIN стробирует прием входных данных. В общем случае, этот сигнал разрешает работу блока в отдельных тактах. Это необходимо для моделирования и стыковки блоков, работающих с взаимно кратными периодами дискретизации. Например, с помощью блока FFTbeh можно определять узкополосный спектр после предварительной фильтрации полосовым фильтром и снижения частоты дискретизации в М раз. При этом сигнал EDIN должен иметь скважность 1:М.
Поведение объекта FFTbeh описано в архитектуре FFT_int. Для представления массивов данных в ней объявлены следующие типы массивов комплексных и действительных данных.
constant NN: integer:=2**N; -- длина преобразования type MEMOC is array (0 to NN) of complex; --комплексный массив type MEMOC_2 is array (0 to NN/2-1 ) of complex; type MEMOR is array (0 to NN-1) of real; --вещественный массив
В следующем тексте описана процедура, по которой накапливается массив входных комплексных данных.
procedure GATHER_C(signal CLK,START,EDIN:in std_logic; signal DR,DI: in integer; --реальная и мнимая часть отсчетов входных данных signal rdy: out std_logic; --сигнал готовности signal datact: inout natural;--номер данного signal RAM:out MEMOC) -- рабочий массив is variable TR,TI:real; begin wait until CLK= '1'; -- запуск процедуры по фронту CLK if EDIN = '1' then -- проверка разрешения приема TR:= real(DR); -- формирование комплексного данного TI:= real(DI); RAM(datact)<=(TR,TI); -- и запись его в массив if datact < RAM'right then datact<=datact+1; -- счетчик данных end if; if START='1' then datact<=RAM'left; --обнуление счетчика данных end if; rdy<='0'; if datact =RAM'right-1 then rdy<='1'; --массив готов end if; end if; end procedure;
Такая процедура существенно отличается от процедур, к которым привыкло большинство программистов. В интерфейсе процедуры явно указываются не только тип данных, но и то, что они – сигналы, а также их направление: входные или выходные данные. При вызове процедуры ей не просто передается управление, а ее копия переписывается в модуль, который ее вызывает и там она связывается. Выполнение этой процедуры запускается по фронту синхросигнала CLK в операторе wait и останавливается после выполнения ее последнего оператора.
В данной процедуре при каждом ее запуске при EDIN = '1' в массив RAM по счетчику datact записывается очередная пара данных .
Процедура SCATTER_C выглядит следующим образом.
procedure SCATTER_C(signal CLK,START,EDIN:in std_logic; pt:natural; -- масштаб результата signal RAM:in MEMOC; -- входной массив signal datact: inout natural; -- счетчик данных signal rdy: out std_logic; -- конец вывода массива signal DR,DI: out integer ) -- выходные данные is variable s:real:=real(2**pt); -- масштабный коэффициент begin wait until CLK='1'; if START='1' then datact<=RAM'left; elsif EDIN='1' then RDY<='0'; DR<= integer(RAM2(datact).RE/s); DI<= integer(RAM2(datact).IM/s); if datact< RAM'right then datact<=datact+1; end if; if datact=RAM'right-1 then RDY<='1'; end if; end if; end procedure;
Эта процедура работает так же, как и предыдущая. Но наоборот, накопленные в массиве данные последовательно выдаются на ее выход (gather – собирать, scatter – разбрасывать).
Наконец, процедура, вычисляющая БПФ массива данных выглядит так:
procedure FFT(N:positive; -- размер преобразования =2**N signal rdy:in std_logic; -- запуск преобразования signal RAM_I: in MEMOC; -- входной массив данных signal RAM_O:out MEMOC) -- выходной массив спектров is constant NN:positive:=2**N; variable TWIDDLE:MEMOC_2; -- массив вращающих коэффициентов variable RAM:MEMOC; -- рабочий массив variable a,b,c,d,w: complex; variable al:real; variable base,itera,datact,twiddlect,twiddleinc: natural; variable delta:integer:=1; variable addrf: std_LOGIC_VECTOR(N-1 downto 0); variable addri: std_LOGIC_VECTOR(N-1 downto 0); begin -- формування таблиці обертаючих коефіцієнтів for i in 0 to NN/2-1 loop al:= (MATH_PI*real(2*i))/real(NN); if IDCT=0 then TWIDDLE(i):=(COS(al),-SIN(al)); -- для прямого БПФ else TWIDDLE(i):=(COS(al), SIN(al)); -- для обратного БПФ end if; end loop; loop -- основний цикл wait until RDY='1'; -- начало БПФ -- двоично-инверсная перестановка входных данных addrf:=(others=>'0'); -- вектор адреса для прямого порядка datact:=0; for i in 1 to NN loop for ind in 0 to N-1 loop -- инвертирование порядка бит addri(ind):=addrf(N-1-ind); -- инверсный адрес end loop; RAM(CONV_INTEGER(unsigned(addri))):=RAM_I(CONV_INTEGER(unsigned(addrf))); addrf:=unsigned(addrf)+1; end loop; -- собственно БПФ itera:=0; delta:=1; twiddleinc:=0; for itera in 1 to N loop --начало итерации base:=0; twiddlect:=0; for butterfly in 0 to NN/2 - 1 loop a:=RAM(base); -- начало базовой операции base:=base+delta; b:=RAM(base); w:=TWIDDLE(twiddlect); c:=a + b * w; -- собственно бабочка d:=a - b * w; RAM(base-delta):=c; RAM(base):=d; -- конец базовой операции -- модификация параметров базовой операции base:= base + delta; if base >= NN then base:=base-NN+1; twiddlect:=twiddlect+twiddleinc; end if; end loop; -- конец итерации -- модификация параметров итерации delta:=delta*2; if itera=1 then twiddleinc:=NN/4; else twiddleinc:=twiddleinc/2; end if; end loop; RAM_O<=RAM; -- результаты БПФ end loop; end procedure;
Здесь выполняется известный алгоритм БПФ по основанию 2 с прореживанием по времени с замещением данных [5]. Процедура имеет два участка: первый из них выполняется однократно в начале моделирования, а второй – собственно БПФ - выполняется периодически. При выполнении первого участка формируется таблица вращающих коэффициентов, размер которой равен длине преобразования и которая затем используется алгоритмом БПФ.
Второй участок представляет собой бесконечный цикл. Цикл запускается, как только готов массив исходных данных, подготавливаемый процедурой GATHER_C. После запуска данные переписываются из входного массива в рабочий массив. При этом выполняется двоичная инверсия адресов записи. Следует отметить простую и оригинальную возможность двоичной инверсии адреса, которую предоставляет язык VHDL. Адрес представляется битовым вектором и биты в нем переставляются в инверсном порядке непосредственно. В обычных языках для этого необходимо выполнить сложный и не всегда очевидный алгоритм.
Как и все алгоритмы БПФ, данный алгоритм представляет собой гнездо циклов. Во внутреннем цикле выполняются базовые операции БПФ, а внешний цикл образован N итерациями алгоритма БПФ.
Исполнительная часть архитектуры содержит вызовы описанных выше процедур:
begin INPUT:GATHER_C(CLK,START,EDIN, DR=>DIN_Re,DI=>DIN_IM, rdy=>idatardy,datact=>datact_I,RAM=>RAM1); SPECTRUM:FFT(N,idatardy,RAM_I=>RAM1,RAM_O=>RAM2); OUTPUT: SCATTER_C(CLK,idatardy,EDIN,PT, RAM=>RAM2,DATACT=>datact_o, rdy=>rdy,DR=>DOUT_RE,DI=>DOUT_IM); DEL:process(CLK) begin if CLK='1' and CLK'event and EDIN='1' then READY<=idatardy; NUM<= datact_o; end if; end process; end FFT_int;
Для понимания исполнения программы следует напомнить, что параллельный вызов процедуры, в которой применяется оператор wait, эквивалентен оператору процесса, тело которого состоит из тела процедуры. Таким образом, все три процедуры, как и эквивалентные им процессы, исполняются параллельно в некотором конвейерном режиме. При этом данные и управление передаются от процедуры к процедуре как между ступенями конвейера. В результате, объект FFTbeh обрабатывает непрерывный поток данных, сегментируя его на блоки длиной 2**N.
Процесс DEL выполняет задержку сигнала готовности READY и потока номеров NUM отсчетов результата на один такт, чтобы они соответствовали выходным отсчетам сигналов DOUT_RE и DOUT_IM .
Рассмотрим простой пример применения объекта FFTbeh для измерения спектра сигнала, выдаваемого генератором прямоугольных импульсов. Графическая программа, подготовленная в среде Aldec Active HDL, показана на рис.1.
Здесь компонент U1 представляет собой программируемый генератор прямоугольных импульсов DATA с выдачей синхросерии CLK, сигналов START и ENA. Компонент U2 - это описанный выше блок БПФ, а U3 – преобразователь комплексных чисел из прямоугольной системы координат в полярную.
VHDL – симулятор может выдавать на экран целочисленный сигнал в виде графика. Как привило, этот график с осями представлен с помощью средств векторной графики. Поэтому его можно выделить и через "карман" ПК переместить, например, в редактор текста, такой как MS Word. На рис.2. показаны графики сигналов, сгенерировнных при моделировании модели на рис.1. В этом эксперименте определялся спектр прямоугольного импульса с периодом 256 тактов и длительностью 10 тактов с помощью БПФ длиной 256 отсчетов.
Как видим, предложенный блок БПФ эффективен в использовании. Получаемые с помощью его спектра графики удобны как для восприятия, так и для документирования и сохранения. Причем скорость вывода графиков и быстродействие системы при их изучении (перемещение, изменение масштаба, измерение в заданных точках) многократно выше, чем при использовании Matlab'a.
При необходимости, описанный блок БПФ можно без особого труда доработать для того, чтобы входные и выходные данные были с плавающей запятой. Такая доработка сводится лишь к замене целого типа данных на тип real.