Хорошим тоном при обкатке новых технологий является демонстрация работы "Hello world". Для FPGA/CPLD решений этот неудобоваримый для них человеческий интерфейс заменяют другим приветствием — дружески помигать светодиодом. Мы поднимем планку чуть выше и покажем, как связка FPGA+ARM может использоваться в более сложных приложенях — радиолокационной обработке.


Прощание со шкафами


Для экспериментов выбираем Zynq на борту Red Pitaya. Плата имеет два выхода (DAC) и два входа (ADC), причем оцифровка идет на серьезной частоте — 30 МГц (на самом деле можно больше, но эта частота очень похожа на ту которая используется в тракте промежуточной частоты РЛС)


Про РЛС — радиолокационную станцию я упомянул уже два раза, поэтому сразу открою маленький секрет. Вряд ли вас пустят в разработку в отечественные именитые радиолокационные конторы с орденами "Знак почета" на проходных, где дедушки макая бородой в творог будут долго и нудно рассказывать как они делали первый искровой передатчик и сажали Буран. Но есть реальный шанс поучаствовать в модернизации стареньких систем типа РСП-10. И не нужно думать, что этому старью самое место на свалке: как раз наоборот, спрос на старые локаторы, которые умеют работать в диапазоне ниже 1 ГГц и видят stealth цели, только растет! Поэтому если вы выбросите из аппаратной целый шкаф обработки и поставите одну плату, вам будут только аплодировать и может быть даже заплатят )


Тракт приемный, тракт передающий


Наше оборудование для опытов будет выглядеть в минималистком варианте. Все что потребуется помимо самой платы Red Pitaya, это два ВЧ кабеля которыми мы соединим соответствующие выходы и входы. Как вы догадываетесь, на выходах мы будем имитировать сигнал передатчика локатора, а на входах — принимать и обрабатывать его.


Будет это выглядеть примерно так:


DAC1 -> ADC1
DAC2 -> ADC2

Зачем два канала? Как и в настоящем радиолокаторе, в ADC2 плата получит передающий импульс, в ADC1 — отраженный. Очевидно, что отраженный будет с некоторым временным сдвигом, пропорциональным дальности до цели. Обнаружение отраженного импульса и определение дальности и будем вести корреляционным методом.


Значение частоты 30 МГц — не надуманное, именно на эту частоту сносится несущая в радиотракте локатора для удобства работы. Это справедливо как для передающего, так и для приемного тракта. Так что наше устройство сделано по взрослому и его можно прямиком интегрировать в ту же систему РСП-10.


Но мы немного замечтались. Возвращаемся на землю и смотрим, что у нас с софтом.


Структура проекта


Все будет достаточно просто. Обработка импульсов будет жить в приложении proc.c. Имитатор/генератор импульсов — в приложении gen.c. Все это компилируется под ARM архитектуру и работает в реальном времени. Работа с FPGA будет скрыта за библиотекой librp.so: от нас всего лишь потребуется сформировать буфер для передачи и подготовить буфер для приема данных.


Результаты будем смотреть на экране в офлайне, для этого создадим приложение на Питоне: pshow.py. В этом же приложении будет заключительная часть обработки: нахождение корреляционной функции.


Итак, структура:


target # (Red Pitaya): gen.c, proc.c
host $ (мой комп): pshow.py.

Данные для отображения будут передаваться очень просто: через файл. Никакого реалтайма, зато наглядность не страдает.


Импульсная обработка: proc.c


Для начала готовим массивы x1, x2 для приема данных с ADC1, ADC2 (вспомогательный код показывать не буду) и инициализируем API:


if(rp_Init() != RP_OK)
{
    fprintf(stderr, "Rp api init failed!\n");
}
rp_AcqReset();
rp_AcqStart();

Дальше, настраиваем триггер — признак готовности буфера. Уровень напряжения срабатывания и источник для триггера — канал ADC2 (передающий импульс):


// 0.1V
rp_AcqSetTriggerLevel(0.1);
// CHA, CHB: trigger channels
rp_AcqSetTriggerSrc(RP_TRIG_SRC_CHB_PE);

Ну а теперь входим в режим ожидания: будем крутиться в цикле и ждать когда триггер сработает


uint32_t buffer_pos;
rp_acq_trig_state_t state;

while(--retries > 0)
{
    rp_AcqGetTriggerState(&state);
    rp_AcqGetWritePointer(&buffer_pos);
    if(state == RP_TRIG_STATE_TRIGGERED)
        break;
    fprintf(stderr, "retries: %d, buffer pos: %d\n", retries, buffer_pos);
    usleep(100);
}

С этого момента будем немного подробнее. Я задал ограниченное число попыток считывания триггера (retries). Информация о состоянии триггера возвращается в переменной state. Для вас я сохраняю переменную buffer_pos, которую сам дальше не использую: это указатель начала данных в кольцевом буфере. Вы можете вручную снять данные, начиная с указателя, дойти до конца буфера и затем продолжить с его начала. Мы будем использовать более удобный метод — функцию API которая сама разбирается с закольцованностью буфера.


После срабатывания триггера — поступления опорного импульса ADC2 продолжаем выполнение программы:


#define SIZE (256)
uint32_t size1= SIZE;
uint32_t size2= SIZE;
uint32_t trigger_pos;

if(retries != 0)
{
    rp_AcqGetWritePointerAtTrig(&trigger_pos);
    // get data from trigger position
    rp_AcqGetDataV(RP_CH_2, trigger_pos-10, &size2, x2);
    rp_AcqGetDataV(RP_CH_1, trigger_pos-10, &size1, x1);
    fprintf(stderr, "TRIGGERED: retries: %d, buffer pos: %d, trigger pos: %d\n", retries, buffer_pos, trigger_pos);
    // x1: ADC1 target pulse
    // x2: ADC2 reference pulse
    to_file(x1, x2, SIZE);
}
else
{
    fprintf(stderr, "Not triggered!\n")
}

Здесь после сохранения смещения в буфере, на котором сработал триггер, мы вызываем функции rp_AcqGetDataV которые снимают данные с закольцованного буфера и копируют в x1, x2. В свою очередь, эти данные с помощью функции to_file() сохраняются в файл. Замечу, что данные мы снимаем чуть раньше, чем сработал триггер (на 10 позиций): это позволяет не потерять начало импульса.


В заключение не забываем освободить ресурсы:


 if(rp_Release() != RP_OK)
        fprintf(stderr, "rp_Release\n");

Корреляционная обработка: pshow.py


Полученный файл преобразуется в массив data, в котором опорные и сигнальные отсчеты чередуются. Если вы захотите бесшовный процесс — без промежуточного хранения данных в файле, тогда всю нижеприведенную часть нужно реализовать также на Си. Далее делаем входной импульс комплексным, добавляя мнимые значения:


// reference signal: ADC2
ref = data[0::2]
xref = ref + 1j*np.roll(ref, 1)
// target signal: ADC1
sig = data[1::2]
xsig = sig + 1j*np.roll(sig, 1)

Емкость и выразительность Питона скрывают существенные нюансы ) Сдвиг на единицу выбран потому, что на период частоты 30 МГц приходится 4 выборки. Смещение на 1 выборку — это фазовый сдвиг сигнала на 90 градусов, таким образом получаем комплексный, или аналитический сигнал.


Корреляционная функция аналитического сигнала:


cor = np.correlate(xsig, xref, "full")[xref.size - 1:]

Пик корреляционной функции соответствует внесенной задержке, то есть дальности до цели.


Имитатор сигнала: gen.c


Вначале как обычно инициализация:


if(rp_Init() != RP_OK)
{
    fprintf(stderr, "Rp api init failed!\n");
}
rp_GenReset();

Пусть зондирующий импульс будет представлять из себя ЛЧМ сигнал:


for (int i = 0; i < SIZE; ++i)
{
    double w = 1.0 - cos(2*M_PI*(double)i/(double)SIZE);
    x1[i] = cos(2*M_PI*(double)i*(30.0)/(double)SIZE) * w / 2.0;
    x2[i] = cos(2*M_PI*(double)i*(30.0)/(double)SIZE) * w / 2.0;
}

Посылаем два импульса подряд, вначале — канал цели DAC1, затем — опорный канал DAC2:


rp_GenReset();

// конструируем опорный импульс
gen_pulse(x2, SIZE, RP_CH_2, cnt, rpt, freq_ref, per);
// конструируем импульс цели
gen_pulse(x1, SIZE, RP_CH_1, cnt, rpt, freq_sig, per);

// посылаем опорный импульс
rp_GenOutEnable(RP_CH_2);
// посылаем импульс цели
rp_GenOutEnable(RP_CH_1);

if(rp_Release() != RP_OK)
    fprintf(stderr, "rp_Release\n");

Основную работу выполняет функция gen_pulse(). Возникает хороший вопрос: каким образом задается задержка между опорным импульсом и импульсом цели? А никак. Точнее, она возникает естественным путем благодаря задержке между открытием двух каналов DAC rp_GenOutEnable(). Генератор импульсов выглядит следующим образом. Дальнейшее повествование продолжаем в комментариях:


void gen_pulse(float* buf, int N, int ch, int cnt, int rpt, int freq, int per)
{
    // пока отключаем канал
    rp_GenOutDisable(ch);
    // задаем частоту радиоимпульса
    rp_GenFreq(ch, freq); // whole buffer corresponds to 1 us 
    // устанавливаем амплитуду 1.0V
    rp_GenAmp(ch, 1.0);
    // говорим что импульс произвольной формы
    rp_GenWaveform(ch, RP_WAVEFORM_ARBITRARY);
    // формируем импульс из массива
    rp_GenArbWaveform(ch, buf, N);
    // режим формирования - пакетный (в отличие от непрерывного)
    rp_GenMode(ch, RP_GEN_MODE_BURST);
    // сколько waveforms будет в одном пакете
    rp_GenBurstCount(ch, cnt);
    // сколько раз будет повторяться пакет, с учетом его периода
    rp_GenBurstRepetitions(ch, rpt);
    // время выделенное на пакет
    rp_GenBurstPeriod(ch, per);
}

Собираем, запускаем


Приложения gen.c, proc.c собираются обычным образом под ARM. Процедура запуска выглядит следующим образом: первой стартует программа proc и ждет импульса, второй очередью запускаем имитатор импульсов gen. Полученные импульсы — цели и опорный сохраняются в файле, который отдаем приложению pshow.py. В результате оно нарисует нам следующую картинку:



На левой диаграмме красным цветом показан опорный импульс — тот который излучает передатчик РЛС. Импульс, "переотраженный" от цели показан зеленым цветом.
Формирователь gen.c я настраивал следующим образом:


  • частота заполнения радиоимпульса: 30 МГц
  • длительность радиоимпульса: 1 мкс
  • количество выборок: 4 на период

На диаграмме видно, что начало опорного импульса было обнаружено с некоторым запозданием. Чтобы оправдать себя, будем думать что это момент запуска магнетрона передатчика, когда его параметры еще не вышли на стабильный уровень.


Корреляционная функция показана на правой диаграмме. 100 делений по оси абсцисс соответствуют 1 мкс. Пик функции наблюдается в точке 0.4 мкс, что в принципе визуально согласуется с величиной взаимного смещения двух импульсов. Можете сами подсчитать, какой дальности до цели соответствует эта задержка.


Послесловие


Вот мы и собрали на небольшой платке Red Pitaya практически полноценный радиолокационный тракт обработки. В нем не хватает одной вкусной вещи — селекции движущихся целей, которую можно делать фазовым детектором параллельно с корреляционной обработкой. Об этом может быть я расскажу в других публикациях.

Комментарии (0)