Проектирование с использованием процессоров Analog Devices.
Цифровой КИХ-фильтр
Александр СОТНИКОВ
В этой статье на примере простого проекта цифрового фильтра с конечной импульсной характеристикой обсуждаются некоторые ключевые особенности архитектуры процессоров В1аскЛп, повышающие производительность в задачах цифровой обработки сигналов. А также рассмотрен вопрос интерфейса между отдельными частями кода, написанными на языке ассемблера и языке С/С++.
Цифровые КИХ-фильтры
Цифровая фильтрация является одной из самых распространенных операций в задачах цифровой обработки сигналов. Цифровые фильтры бывают двух типов — с конечной импульсной характеристикой (КИХ-фильтры) и с бесконечной импульсной характеристикой (БИХ-фильтры). Основное их отличие состоит в том, что выходной сигнал КИХ-фильтров определяется только входными отсчетами и набором коэффициентов, а выходной сигнал БИХ-фильтров зависит также от значений выходных отсчетов в предшествующие моменты времени. КИХ-фильтры более просты с точки зрения анализа и реализации, и именно этот тип фильтров мы будем рассматривать.
Структурная схема КИХ-фильтра изображена на рис. 1. Как можно видеть, выходной сигнал фильтра представляет собой взвешенную сумму конечного числа отсчетов входного сигнала. Весовые коэффициенты, определяющие импульсную характеристику фильтра, называются коэффициентами фильтра. Сигнал на выходе КИХ-фильтра во временной области описывается при помощи свертки входного сигнала с набором коэффициентов:
N-1
уШ = У'1х\к-п\Мп).
л=0
где у[к] — отсчет выходного сигнала в момент времени X = кТ5; х[к-п] — отсчет входного сигнала в момент времени X = (к-п) Т5; ^п) — п-й коэффициент фильтра; N — порядок фильтра; а Т5 — интервал дискретизации входного сигнала.
Из этой формулы следует, что вычисление отдельно взятого выходного отсчета КИХ-
фильтра сводится к выполнению в цикле перемножения двух чисел (входного отсчета и коэффициента) и накопления результата. Наличие выделенного аппаратного блока, осуществляющего операцию умножения двух операндов и накопления результата в выходном регистре (МиШр1у-АсситиЫе, МАС) за один процессорный цикл, является характерным свойством любого цифрового сигнального процессора, однако помимо этого цифровые сигнальные процессоры имеют еще целый ряд свойств, направленных на дальнейшую оптимизацию вычисления свертки. К ним относятся циклическая адресация буферов в памяти, аппаратная поддержка циклов, наличие многофункциональных команд, возможность одновременной выборки нескольких операндов из памяти за один процессорный цикл и многофункциональные команды. Стоит отметить, что
обсуждаемые архитектурные особенности повышают эффективность не только при создании цифровых фильтров, но и во многих других задачах цифровой обработки сигналов. Далее мы подробно рассмотрим каждое из этих свойств на примере архитектуры процессоров ВЪскАп.
Циклическая адресация
Циклическая адресация — это один из специализированных режимов адресации процессора, который может быть использован для организации цифровых линий задержки. Поддержка циклической адресации позволяет программисту настраивать в памяти процессора циклические (кольцевые) буферы и автоматически, без программного вмешательства выполнять адресацию их элементов со смещением указателя. Генератор адресов
Адрес
0 0x00000001 Ґ 1-е обращение 0x00000001
4 0x00000002 0x00000002
8 0x00000003 0x00000003
С 0x00000004 0x00000004
10 0x00000005 / 2-е обращение 0x00000005
14 0x00000006 0x00000006
18 0x00000007 0x00000007
1С 0x00000008 0x00000008
20 0x00000009 3-є обращение 0x00000009
24 ОхОООООООА ОхОООООООА
28 0x0000000В 0x0000000В
* Базовый адрес и адрес первого элемента — 0x0
* Индексный регистр 10 указывает на адрес 0x0
* Длина буфера I. = 44
(11 элементов данных * 4 байта/элемент)
* Значение регистра модификации МО = 16 (4 элемента * 4 байта/элемент)
Пример:
1?0 = [10++М0]
І?1 = [І0++М0]
132 = [10++М0]
1*3 = [10++М0]
1*4 = [10++М0]
4-е обращение
5-е обращение
// И)=1, после выполнения Ю // указывает на 0x10 //1?1=5, после выполнения Ю // указывает на 0x20 //1?2=9, после выполнения Ю // указывает на 0x04 //1?3=2, после выполнения Ю // указывает на 0x14 //1?4=6, после выполнения Ю // указывает на 0x24
Рис. 2. Адресация циклических буферов
в процессоре может работать со смещениями, отличными от единицы, и, что более важно, автоматически выполняет возврат к началу буфера при выходе за его границу, как показано на рис. 2. В случае обычной линейной адресации для организации циклического буфера потребовалось бы при каждом положительном (отрицательном) приращении указателя сравнивать его значение с адресом последней (первой) ячейки буфера, что неизбежно привело бы к непроизводительным издержкам.
Для реализации циклических буферов в процессоре ВЪскйп имеется четыре набора специализированных регистров:
• Индексные регистры (10-13), которые содержат значение, выдаваемое генератором адресов данных на шину адреса процессора.
• Регистры модификации (М0-М3), которые определяют величину положительного или отрицательного приращения индексного регистра, выполняемого после каждого обращения к памяти.
• Регистры длины ^0-Ь3), которые определяют размер циклического буфера и диапазон адресов индексных регистров.
• Регистры базового адреса (В0-В3), в которых хранится адрес первого элемента циклического буфера.
Буфер А Буфер В
І-1
І
І+1
Положения указателей показаны на момент перед выборкой первого элемента данных и первого коэффициента
Указатель А
х[к—І] 0 Указатель В ► 1 И(М-1)
х[к—І+1] И(1Ч-2)
х[к—1] -1 +1 И(М—і+1)
х[к] И(М-І)
х[к—N+1] И(М—і—1)
N-2 N-1
х[к і 2] И(1)
х[к—і—1] т
Рис. 3. Буферы линии задержки и коэффициентов КИХ-фильтра
Каждая пара регистров В и L всегда используется только совместно с соответствующим регистром I (например, В0, L0, 10). На регистры М это правило не распространяется, и они могут быть использованы с любым из регистров I.
Рассмотрим, как этот режим адресации применяется для программной реализации КИХ-фильтров. В операции вычисления каждого выходного отсчета КИХ-фильтра участвует текущий входной отсчет и N-1 предыдущих входных отсчетов (Ы — порядок фильтра). То есть циклический буфер для линии задержки сигнала, равно как и буфер, в котором хранятся коэффициенты, должен иметь N элементов. Как показано на рис. 3, очередной принимаемый входной отсчет х[к], соответствующий моменту времени кТ5, записывается в ячейку буфера линии задержки (буфер А) с номером ] поверх содержащегося в ней самого старого отсчета х[к-Ы], соответствующего моменту времени (к-Ы)Т8. При этом указатель буфера А смещается на одну позицию, так что при первом чтении буфера А из памяти будет выбран отсчет х[ к-Ы+1].
Указатель буфера коэффициентов (буфер В) на данном этапе не изменяется и должен указывать на первый элемент буфера, в котором хранится последний коэффициент фильтра, ^Ы]. После выполнения N операций чтения данных из буферов А и В, необходимых для вычисления выходного отсчета у[к], указатель В возвращается на исходную позицию (в начало буфера), а указатель А будет указывать на ячейку, где хранится отсчет х[к-Ы+2]. Следует учесть, что, в общем случае, как показано на рис. 3, значения в буфер коэффициентов должны записываться в порядке, обратном реальному порядку следования коэффициентов фильтра. То есть в первой ячейке буфера должен храниться последний коэффициент фильтра, во второй ячейке — предпоследний коэффициент и т. д. Однако в большинстве ситуаций на практике предпочтительнее использовать КИХ-фильтры с симметричными коэффициентами, имеющие линейную фазовую характеристику, и в таком случае подобное упорядочивание не требуется.
Аппаратные циклы
Еще одна особенность цифровых сигнальных процессоров в целом и процессоров Віаскйп в частности, которая используется для повышения производительности при реализации цифровых фильтров, — это аппаратная поддержка циклов. Для многих алгоритмов цифровой обработки сигналов характерно повторяющееся выполнение одинаковых операций над массивами данных. В случае применения КИХ-фильтра — это операции выборки из памяти отсчета входного сигнала и коэффициента фильтра, их перемножение и накопление результата в регистре.
ARCHITECrURE(ADSP-XXXXX) //Описание архитектуры
SEARCH_DIR(...) //Описание путей поиска
SLIBRARIES .... //Пользовательские макросы
SOBJECTS ....
$COMMAND_LINE_OBJECTS
MEMORY { //Описание сегментов физической памяти
имя_сегмента^РЕ( ) WIDTH( ) START( ) END( )}
}
PROCESSOR xx { //Формирование выходного файла
OUTPUT(имя_файла)
SECTIONS{
имя_вых_секции [тип_секции] {INPUT_SECTIONS( )}[>сег-мент_памяти]
}
}
Для организации программных циклов без непроизводительных издержек в процессоре В1аскПп имеется два набора регистров адреса начала цикла ^Т0, LT1), адреса конца цикла ^В0, LB1) и счетчика цикла ^С0, LC1). Наличие двух наборов регистров позволяет реализовывать не только одиночные, но и вложенные двухуровневые циклы. При этом цикл 1, настраиваемый при помощи регистров ЦГ1, LB1 и LC1, имеет больший приоритет (внутренний цикл). При написании программ на языке ассемблера каждый из трех регистров цикла может быть инициализирован по отдельности, однако удобнее использовать для этой цели специальную команду LSETUP, которая имеет формат:
Здесь loop_start — метка, отмечающая начало цикла, или непосредственное значение смещения первой команды цикла относительно команды LSETUP; loop_end — метка, отмечающая конец цикла, или непосредственное значение смещения последней команды цикла относительно команды LSETUP; reg — один из регистров указателей P0-P5.
Пример настройки одиночного цикла без вложения:
Дополнительное повышение производительности при выполнении циклов в процессоре достигается благодаря специальному буферу команд цикла глубиной четыре элемента. Если код цикла содержит не более четырех команд, то, независимо от количества итераций, обращений к памяти не потребуется, поскольку команды будут выбираться из буфера цикла.
Оптимальное размещение данных
Для максимально эффективного выполнения операции умножения с накоплением необходимо, чтобы процессор мог за один процессорный цикл выбирать из памяти команду и два операнда (в случае применения КИХ-фильтра — отсчет из линии задержки сигнала и коэффициент фильтра). В разных сигнальных процессорах эта возможность достигается по-своему. Так, например, в самых первых сигнальных процессорах Analog Devices ADSP-21xx (вплоть до ADSP-219x), имеющих классическую модифицированную гарвардскую архитектуру, данные могут храниться как в памяти данных, так и в памяти программ, а обращение к памяти программ происходит на удвоенной частоте (в первой половине процессор-
ного цикла происходит выборка команды, а во второй — слова данных). Процессоры ADSP-219x и ADSP-2106x/2116x также основаны на модифицированной гарвардской архитектуре, однако вместо обращения к памяти программ на удвоенной частоте в них используется аппаратный кэш команд. Таким образом, за один процессорный цикл в этих процессорах может выполняться выборка одного операнда из памяти данных, одного операнда из памяти программ и команды из кэша.
В процессорах Blackfin в памяти программ могут содержаться только команды, однако память данных состоит из двух банков, каждый из которых, в свою очередь, разделен на несколько блоков. Сложная структура внутренних шин позволяет одновременно обращаться к любой паре таких блоков за один цикл. Таким образом, для оптимизации обращений к памяти при выполнении свертки в процессорах Blackfin следует разместить массивы линии задержки и коэффициентов фильтра в разных блоках памяти. Для этого необходимо сделать две вещи. Во-первых, при написании кода на языке ассемблера или языке C необходимо в явном виде указать, что массивы должны помещаться в разные секции генерируемого объектного кода (входные секции). Во-вторых, в файле описания линкера (Linker Description File, LDF) необходимо надлежащим образом установить соответствие между входными секциями и сегментами физической памяти процессора.
Размещением команд и данных во входные секции при написании программ на языке ассемблера управляет директива:
SECTION section_name;
Здесь section_name — это имя входной секции. Все переменные или команды, которые следуют в тексте программы за этой директивой, будут располагаться во входной секции с указанным именем. При создании программ на языках C/C++ для этих же целей используется квалификатор section(“section_name”), который добавляется при объявлении переменной или функции перед указанием ее типа. По умолчанию компилятор C/C++ процессоров Blackfin использует следующие имена секций: program для кода программы, data1 для глобальных и статических переменных, constdata для переменных, объявленных как const, bsz для глобальных переменных, инициализированных нулями, stack для хранения стека и, наконец, heap для «кучи» (динамически выделяемой памяти).
Файл описания линкера содержит управляющие директивы, которые используются на этапе компоновки для формирования исполняемого файла. Как и объектные файлы, исполняемые файлы разбиваются на вы-
Рис. 4. Структура файла LDF
ходные секции, привязанные к карте памяти процессора. В файле LDF определяется соответствие между входными и выходными секциями, а также приводится дополнительная информация, необходимая для компоновки.
Типовой файл LDF имеет следующую структуру (рис. 4).
• Команда ARCHITECTURE{ADSP-xxxxx} задает архитектуру процессора, для которого формируется исполняемый файл. Эта команда естественным образом диктует возможную разрядность и диапазон памяти, набор регистров и другую информацию, используемую отладчиком, линкером и загрузчиком.
• Команда SEARCH_DIR{} указывает пути для поиска библиотек и объектных файлов.
• При помощи команды $LIBRARIES программист указывает набор библиотек и объектных файлов, в которых линкер будет осуществлять поиск символов, на которые существуют ссылки в исходных файлах.
• С помощью макроса $OBJECTS задается список входных файлов, в которых линкер будет искать компонуемые объекты. Макросы упрощают чтение LDF-файла, позволяя вместо многократных перечислений одних и тех же имен файлов использовать более компактные записи. Например, запись вида:
$OBJECTS = main.doj, fft.doj;
dxe_program {INPUT_SECTIONS ($DOJS(program))} > mem_
program;
эквивалентна следующей записи:
dxe_program {
INPUT_SECTIONS (main.doj(program)
fft.doj(program))} > mem_program
• Команда MEMORY{} описывает физическую память разрабатываемой системы. Каждая из строк, являющихся ее аргументами, задает имя, границы, разрядность и тип сегмента памяти. Имена сегментов образуют отдельное пространство имен и могут,
LSETUP(loop_start, loop_end) LCx = preg;
Р5 = 0x20;
LSETUP(lp_start, 1р_е^) LCO = Р5; lp_start:
... //Одна, или несколько команд цикла !р_е^: ... //Последняя команда цикла
в общем случае, пересекаться с именами входных или выходных секций.
Команда PROCESSOR формирует исполняемый DXE-файл. Аргументами команды PROCESSOR являются команда OUTPUT, задающая имя выходного файла, и команда SECTIONS{}, которая непосредственно управляет размещением отдельных объектов из объектных и библиотечных файлов в физической памяти системы. Аргументы команды SECTIONS{} имеют следующий синтаксис:
имя_выходной_секции [тип_секции] {команды/выражения} [>сегмент_памяти]
В рассматриваемом примере используется команда INPUT_SECTЮNS, которая указывает линкеру, какие из входных секций необходимо поместить в конкретную выходную секцию и отобразить в заданный сегмент памяти.
В состав VisualDSP++ входят стандартные файлы LDF для каждого из процессоров, которые автоматически подключаются к проекту, если пользователь не добавил в него свой собственный LDF-файл, и определяют соответствие между упомянутыми выше входными секциями, используемыми по умолчанию, и выходными секциями/сегментами памяти.
Многофункциональные команды и вычисления в режиме SIMD
Для максимально эффективного использования аппаратных ресурсов процессора могут быть использованы многофункциональные команды. Процессор Blackfin не является суперскалярным, однако он допускает параллельный вызов до трех команд, с некоторыми ограничениями. Подобные составные многофункциональные команды, которые также называют векторными, имеют длину 64 бита и состоят из одной 32-битной и двух 16-битных команд. Время исполнения многофункциональной команды будет определяться временем исполнения самой медленной из них.
Первой командой, как правило, является команда умножителя или АЛУ. Иногда в качестве первой команды используется команда MNOP — 32-битная версия команды NOP (No Operation — «Нет операции»). В качестве 16-битных команд могут использоваться команды загрузки значения в регистр из памяти, сохранения значения регистра в память или модификации индексного регистра, а также обычная 16-битная команда NOP. На применение данных команд накладывается целый ряд ограничений, которые подробно описаны в руководстве по программированию процессоров Blackfin (Blackfin Processor Programming
Reference). В тексте программы на языке ассемблера отдельные команды, входящие в состав многофункциональной, разделяются двумя вертикальными чертами ("II").
С точки зрения реализации цифрового КИХ-фильтра интерес представляет многофункциональная команда, совмещающая в себе операцию умножения с накоплением в регистре аккумулятора и две операции загрузки регистров данных из памяти с модификацией индексных регистров. Эта команда имеет общий вид типа:
Ax += Dreg_lo_hi * Dreg_lo_hi II Dreg = [Ireg++] II Dreg = [Ireg++];
Здесь Ax — это регистр аккумулятора (A0 или A1), Dreg — один из регистров регистрового файла данных (Ry, у = 0-7), Dreg_lo_hi — младшая или старшая половина регистра, Ireg — индексный регистр (Iz, z = 0-3). При использовании подобной многофункциональной команды для вычисления свертки удобно выполнить первую загрузку данных до входа в цикл, а последнюю операцию умножения с накоплением — после выхода из цикла. Тогда цикл будет включать в себя только указанную выше команду.
И наконец, поскольку в состав ядра процессора Blackfin входят два идентичных вычислительных блока, он может выполнять сразу две операции умножения-накопления за один процессорный цикл над четырьмя входными операндами в режиме SIMD (одна команда, разные данные). Такой режим позволяет в два раза уменьшить количество итераций, необходимых для вычисления одного выходного отсчета обычного КИХ-фильтра, или реализовать эффективную обработку двух потоков данных (например, комплексного сигнала и стереофонического аудиосигнала). Пример команды, осуществляющей одновременно две операции умножения-накопления:
A1 += R1.H * R2.L, A0 += R1.L * R2.H;
В первой операции старшая половина 32-битного регистра R1 умножается на младшую половину 32-битного регистра R2 и складывается с содержимым аккумулятора А1. Во второй операции младшая половина регистра R1 умножается на старшую половину R2 и складывается с содержимым аккумулятора А0.
Помимо рассмотренной выше команды, существует также много других векторных команд, сочетающих вычислительные команды АЛУ и умножителя с командами загрузки/сохранения или командами модификации индексных регистров.
Описанные выше архитектурные особенности будут использованы в проекте цифрового фильтра для платы EZ-KIT, который будет рассматриваться во второй части статьи. ■