OpenZynqSDR
OpenZynqSDR

В этой статье я расскажу о том, как я делал предельно дешевый КВ Web-SDR приемник на SoC Zynq.
Основа приемника - Б/У плата Antminer S9. Фактически этот проект является форком уже существующего приемника WEB-888, который, в свою очередь, является продолжением приемника KiwiSDR.

Введение

Для начала, думаю, стоит немного рассказать про приемник KiwiSDR.
Это онлайн-SDR приемник, то есть приемник, рассчитанный на то, что к его данным через Интернет могут получать доступ пользователи со всего мира. Проект разработан John Seamons, ZL/KF6VO, появился в 2016 году и является полностью Open Source. Большое спасибо ему за эту разработку!

Сам по себе приемник представляет из себя плату-шилд для одноплатного компьютера BeagleBone:

Фото платы приемника с сайта http://kiwisdr.com/
Фото платы приемника с сайта http://kiwisdr.com/

Приемник может принимать сигналы в диапазоне 10 кГц - 30 МГц и использует принцип DDC (Digital Down Conversion), то есть сигнал с антенны усиливается и подается прямо на вход АЦП, в данном случае - LTC2248 14-bit 65 MSPS.
Для первичной обработки данных от АЦП в приемнике имеется ПЛИС Xilinx Artix-7. Дополнительно на плате приёмника есть GPS front-end MAX2769B, а в ПЛИС реализована обработка данных от него. Принятые GPS-данные используются для получения точной информации о частоте кварцевого генератора приемника и для точной синхронизации времени.

Стоит отметить, что термин WEBSDR обычно описывает приемники, построенные по принципу, хорошо описанному тут: https://22dx.ru/kak-rabotaet-server-websdr/
Ну а самый первый WEBSDR приемник, и информация про него находятся тут: http://websdr.ewi.utwente.nl:8901/

Вкратце про принцип работы таких приемников:

  • Физический приемник подключается к компьютеру, имеющему доступ в сеть. Приемник передает данные изо всей своей полосы приема на этот компьютер. Замечу, что поток этих "сырых" данных довольно велик. К примеру, для 60 MSPS и 16 бит это будет практически 1 Гбит/с. Через Интернет напрямую такой поток данных передать непросто.

  • Компьютер (сервер) обрабатывает поток данных, используя методы DSP. Для каждого пользователя, который подключается к серверу, на компьютере создается отдельный виртуальный канал приема, с заданными пользователем свойствами (частота, вид модуляции). Данные для "водопада" вычисляются отдельно.

  • Компьютер передает эти данные клиенту - у которого имеется специализированный SDR-софт для их обработки, или в браузер, с запущенным софтом на WEB-странице.

Хорошо видно, что для обработки данных нужна серьезная вычислительная мощность, а значит - мощный компьютер. В KiwiSDR, основанному на одноплатнике, используется несколько другой подход - первичная обработка данных, особенно аудиоданных, переложена на ПЛИС. Это позволяет очень сильно разгрузить CPU, но зато растут требования к ПЛИС. В частности, именно ПЛИС будет определять максимально возможное число одновременных подключений к приемнику.
Автор KiwiSDR выложил довольно полезный документ, описывающий этот приемник "изнутри": KiwiSDR design review. В нем есть структурная схема приемника:

Структурная схема KiwiSDR, я добавил на нее несколько подписей.
Структурная схема KiwiSDR, я добавил на нее несколько подписей.

Из схемы видно, что ПЛИС (FPGA) тут переносит данные от АЦП на более низкую частоту дискретизации. На этой схеме не показано, но блоки Spectrum DDC/Audio DDC - многоканальные, каждому пользователю выделяется свой канал. В результат объем данных, попадающих на одноплатный компьютер и передаваемых по сети, очень заметно сокращается. Ширина полосы пропускания аудиоканала - ±6кГц (хотя есть варианты прошивок с более широкой полосой).

На самом одноплатнике установлена модификация OpenWebRX - специальный софт и WEB-сервер, благодаря которому к приемнику можно подключаться через браузер. Прямо из браузера можно запускать различные декодеры принятых радиосигналов.

Вокруг этих приемников сформировалось довольно большое сообщество энтузиастов, которые запускают приемники на длительное время, а благодаря сервисам сайта kiwisdr.com пользователи могут вывести приемники в сеть, даже находясь за NAT. Cписок активных приемников можно видеть здесь - http://kiwisdr.com/public/ (на момент написания статьи по всему миру в online их было более 800).
Стоимость приемника вместе с BeagleBone сейчас около 400$.

Приемник WEB-888

Этот приемник появился достаточно недавно, в 2024 году. Он во многом основан на исходном коде KiwiSDR, но его основой является чип SoC Zynq7010, содержащий двуядерный процессор ARM A9 и ПЛИС. В результате приемник вышел довольно компактным:

Фото отсюда: https://aliexpress.ru/item/1005007633185126.html

АЦП здесь - куда более продвинутый, чем в KiwiSDR: LTC2208 16-bit 130 MSPS, так что приемник может принимать сигналы в полосе до 62MHz, есть возможность принимать сигналы во второй зоне Найквиста - 118MHz-150MHz. В отличие от KiwiSDR, здесь для приема аудио доступны 13 каналов. Вместо "программного" GPS тут использован готовый GPS модуль, который может быть использован для подстройки частоты PLL.
Разработчики тоже поддерживают сервер, на котором каждый может зарегистрировать свой приемник, и который также обеспечивает проксирование: Ссылка

К сожалению, этот приемник куда более закрытый, чем KiwiSDR - его схемы, исходников программы ПЛИС, исходников драйвера, обеспечивающего взаимодействие ресурсов ПЛИС и основной программы приемника - в открытом доступе нет.
Зато, в соответствии с принципами Open Source, исходные коды главной управляющей программы (это ведь форк KiwiSDR) выложены на Github: Ссылка
Кроме того, есть небольшое, но довольно полезное описание структуры программы ПЛИС: Ссылка.

Стоимость этого приемника - в районе 260$.

А теперь - про самодельный приемник

Осенью 2024 годя на Хабре мне попалась вот эта статья: Отладочная плата с жирной ПЛИС за 500р или что делать со старым антмайнером — как я делаю собственную платформу (спасибо автору @Astranome за нее).
Собственно сама статья - об использовании в качестве отладочных плат очень дешевых старых плат управления (КПУ), входящих в состав ASIC майнеров "Antminer S9".
Плата содержит как раз SoC Zynq7010 вместе со всей необходимой ему обвязкой - 512МБ ОЗУ, NAND flash, разъем для SD-карты, PHY для гигабитного Ethernent, всю систему питания, светодиоды. К сожалению, несмотря на большое количество разъемов на плате, пользователю доступны не так уж и много линий GPIO - таковы особенности схемотехники платы. На плату есть даже полноценная схема! В общем, при стоимости в 500р (около 5$), плата выглядит отличным вариантов для экспериментов с SoC Zynq, так что я ее сразу и заказал. Выглядит плата так:

Фото взято отсюда: ссылка
Фото взято отсюда: ссылка

Стоит отметить, что есть похожая на нее плата EBAZ4205, но она стоит дороже. Есть довольно подробное описание, как начинать работу с этой платой, оно может быть полезно и тут: [1]

Изначально у меня были мысли сделать более продвинутую версию своего старого приемника: [3], но позже я решил, что куда более интересно будет сделать упрощенный Open Source аналог WEB-888. Как я упоминал выше, у WEB-888 закрытыми являются программа/конфигурация для ПЛИС и драйвер, вот их реализацию и нужно было написать.

В конечном итоге, получилась такая структурная схема приемника:

Структурная схема самодельного приемника
Структурная схема самодельного приемника

Плата АЦП

Микросхемы АЦП, использованные в KiwiSDR и в WEB-888, даже в Китае стоят довольно дорого:
LTC2248 - около 100$
LTC2208 - около 70-100$
Поэтому я решил использовать куда более дешевый АЦП - AD9226 (я смог купить его менее, чем за 5$). Это 12-bit 65 MSPS АЦП. Конечно, 12 бит хуже, чем 14 или 16, но я посчитал, что в сильно зашумленном эфире города нет великого смысла гнаться за разрядностью АЦП. Также я решил, что мне хватит и частоты дискретизации - 40 MSPS. Напряжение питания этого АЦП - 5В.

Перед АЦП довольно полезно иметь предусилитель, и здесь я использовал AD8370 - усилитель с программно регулируемым коэффициентом усиления (VGA), имеет коэффициент усиления -11..+34 дБ. Также этот усилитель нужен, чтобы преобразовать single-ended сигнал от антенны в дифференциальный, необходимый АЦП. В принципе, включение АЦП и усилителя тут типовые. Перед усилителем установлен ФНЧ на 30 МГц, взятый из схемы KiwiSDR.
Схема платы АЦП:

Схема платы АЦП
Схема платы АЦП

Эту плату я изготовил самостоятельно, используя фоторезист. Для упрощения конструкции она сделана с одним слоем сигнальных проводников, задний (второй) слой меди - земля. В результате некоторые линии пришлось пробрасывать проводами-перемычками.

Плата АЦП проектировалась для установки прямо на плату Antminer. Она занимает 4 разъема 2x9 (разъемы имеют шаг 2мм). От каждого разъема на плате к ПЛИС-части (PL = Programmable Logic) идут только 4 отдельных линии, еще одна линия идет к процессорной части Zynq (PS = Processing System). Соответственно, 3 разъема нужны только для передачи 12-битных данных. Еще один - для линий тактирования АЦП.
Три линии PS используются для управления настройками VGA.
Так как на эти разъемы выведены только 3.3В, напряжение 5В для питания АЦП приходится подводить на плату отдельным проводом.

Стоит отметить, что разъемы на плате Antminer рассчитаны на подключение к ним кабелей, так что разработчики платы не стали заморачиваться с расстановкой разъемов по сетке - размер шага между ними внешне выглядит постоянным, но на самом деле плавает где-то на 0.3мм, так что мне пришлось все аккуратно измерить во время трассировки платы АЦП.

Внешний вид получившейся платы:

Плата АЦП
Плата АЦП

В этот раз самодельная зеленая защитная маска легла как-то неудачно, и плохо держалась, из-за чего плата выглядит некрасиво. Вокруг усилителя имеется зона без маски, к которой я позже припаял экран из медной фольги.
Собранная плата заработала без каких-то серьезных проблем.

В итоге, плата Antminer с установленной платой АЦП выглядит так:

Antminer S9 + ADC
Antminer S9 + ADC

Среднее потребление платы Antminer с установленной на нее платой АЦП - 0.5А при внешнем напряжении питания 5В.

Итоговая стоимость приемника выходит около 15-20$.

Программирование ПЛИС

Как я уже упоминал выше, SoC Zynq содержит в себе ПЛИС (PL). Мой Zynq имеет ПЛИС с таким характеристиками:

Характеристики Zynq 7010
Характеристики Zynq 7010

Эта ПЛИС традиционно не имеет какой-то энергонезависимой памяти, внешнюю память к ней подключить нельзя, так что ее конфигурирование (загрузку туда Bitstream) после включения должен выполнить процессор (PS).

Вся разработка конфигурации ПЛИС велась в среде Vivado 2022.2.

Что же должна выполнять ПЛИС в данном случае? И в случае аудиоданных, и для формирования "водопада" - прием скоростного потока данных от АЦП (12-bit x 40MSPS = 480 Mbit/s), перенос сигналов на более низкую частоту, понижение частоты выборок и передачу получившихся данных на PS. Про это все я подробно писал в [3].

Формирование аудиоданных

Структурная схема системы аудиоданных у меня вышла такая:

Структурная схема системы аудиоданных.
Структурная схема системы аудиоданных.

Здесь я реализовал 8 отдельных каналов данных звука. В принципе, ресурсов ПЛИС хватило бы и на большее число.

Для переноса сигнала на более низкую частоту используется связка из цифрового генератора сигнала (DDS/NCO), двух умножителей и ФНЧ.
В качестве DDS (Direct Digital Synthesis) я использовал готовый блок из Vivado (DDS Compiler 6.0). Его настройки:

Настройки DDS
Настройки DDS

Этот DDS имеет вход "s_axis_config_tdata" - просто 32-битная шина, значение на которой определяет частоту выдаваемого сигнала (величина приращения аккумулятора).
Выход DDS - "m_axis_data_tdata" - тоже 32-битная шина, однако из рисунка выше видно, что на нее выведены два знаковых 14-битных сигнала - SINE/COSINE.
Откуда взялось число "14" ? Конфигуратор вычисляет его из введенного значения "Spurious Free Dynamic Range", который я задал равным 80дБ. Честно сказать, я подобрал это значение, чтобы установить разрядность в 14 бит. А 14 бит выбраны, как дополнительный запас, если я захочу в будущем использовать АЦП с большей разрядностью.

Перемножив данные АЦП на данные от генератора, я получил на выходе два потока знаковых данных (I/Q), их ширину я установил равной 32 бит (это больше, чем получается при перемножении (14 bit * 12 bit), но дает запас, а при сборке ненужные линии не будут создаваться). Данные все еще идут с частотой 40 MSPS. Чтобы уменьшить частоту дискретизации и отфильтровать лишние высокочастотные колебания, я использовал CIC-фильтр, тоже из Vivado (CIC Compiler 4.0). Его настройки:

Настройки CIC1
Настройки CIC1

Видно, что это фильтр-дециматор, с порядком N=3, коэффициент децимации R=238. Вход и выход этого фильтра имеют разрядность 32-бита - это обычные знаковые параллельные шины. Стоит отметить, что этот фильтр, уменьшая частоту дискретизации, позволяет добиться увеличения разрядности данных, в данной конфигурации дополнительные младшие байты отбрасываются внутри CIC. На один канал аудиоданных требуется два CIC-фильтра.

CIC-фильтр из Vivado может работать и в многоканальном режиме, так что я использовал этот режим для дополнительной фильтрации сигнала (так же сделано в WEB-888). Это возможно благодаря тому, что первый фильтр уже понизил частоту семплирования, а CIC2 тактируется от общей частоты 40 МГц.
Настройки этого фильтра:

Настройки CIC2
Настройки CIC2

Видно, что это фильтр-дециматор, с порядком N=5, коэффициент децимации R=14.
Вместе два фильтра дают коэффициент децимации 238*14=3332, соответственно, частота дискретизации данных на выходе CIC2 будет 40MHz / 3332 = 12004.8 Гц. К сожалению, более круглую частоту получить проблематично. 16 каналов у CIC2 - так как каждый аудиоканал выдает пару квадратурных сигналов - I/Q.

Для того, чтобы CIC мог работать в многоканальном режиме, ему нужно знать, в какой последовательности идут данные. Выше я везде писал, что используется параллельная 32-битная шина. На блоках выше она обозначается "AXIS". Это сокращение от полного названия "AXI4-Stream". Это действительно параллельная шина; кроме данных, в простейшем варианте здесь еще присутствуют сигналы tvalid и tready. Шины адреса тут нет. Подробнее про сигналы tvalid и tready можно почитать в [5]. В шине есть поддержка многоканальности, что и используется в CIC. На изображении выше на входе CIC виден еще один сигнал - "data_tlast". С его помощью источник данных обозначает, что передаются данные последнего канала.

Таким образом, чтобы соединить вход CIC2 со выходами каналов данных звука, нужен специальный мультиплексор, способный поддерживать передачу данных по AXIS. Сигнал "data_tvalid" от выхода первого CIC1 используется мультиплексором и является источником данных о том, что CIC2 может забирать данные со всех каналов аудиоданных.

В итоге на выходе CIC2 имеем поток данных (12ksps * 16 каналов * 32) = ~6.1Mbit/s. Для передачи этого потока в PS я использовал DMA. В принципе, можно было бы использовать блок DMA, входящий в состав Vivado, как это описано в здесь [6] и здесь [7].
Но этот способ показался мне довольно муторным, DMA нужно было настраивать со стороны PS. При этом, как я знал, в WEB-888 разработчики использовали собственный DMA.
Так что я нашел такую инструкцию: HLS AXI master and Yocto kernel driver for Zynq [8].
В ней описано, как запустить сделать простой DMA, используя Vitis HLS - среду разработки, которая позволяет синтезировать RTL-блоки из кода, написанного на C/C++. Еще один пример, и описание как использовать Vitis HLS, можно найти здесь: Lab: AXI4-Burst Mode [9].
Можно увидеть, что с точки зрения программиста, запуск передачи DMA выглядит как вызов функции memcpy, в которую передается адрес назначения в ОЗУ и указатель на данные для передачи.

Код моего модуля DMA можно посмотреть здесь: Ссылка

Самая важная его часть - ниже:

//hls::axis<ap_uint<WData>, WUser, WId, WDest>
typedef ap_axis<32,16,0,0> data_t; //Describe input AXIS bus, data width - 32 bit
// "tuser" width - 16 bit
// Tip: If WUser, WId, and WDest are set to 0, the generated RTL will not include the optional 
// TUSER, TID, and TDEST signals in the interface.

// in_bus - input AXI4-Sterem bus
// out_bus - ouptut AXI4 bus
// interrupt_out - "DMA done" IRQ single line
void sound_sender(hls::stream<data_t> &in_bus,  volatile int *out_bus, bool *interrupt_out )
{
    //Describe the buses
	#pragma HLS INTERFACE mode=axis register_mode=both port=in_bus register
	#pragma HLS INTERFACE mode=m_axi port=out_bus offset=off
	#pragma HLS INTERFACE ap_ctrl_none port=return

	int i;
    //Temporary buffer
	uint32_t data_buf[BURST_SIZE];
	static uint16_t burst_count_reg = 0; //transfers counter
	static bool int_pending_reg = false;

	data_t tmp_in_item;//Data from in_bus
	do{
#pragma HLS PIPELINE
        // Read data from "in_bus". This operation will wait untill data source
        // will be ready
		tmp_in_item = in_bus.read(); 
        // Copy to the "data_buf", destination will be taken from the channel index ("user")
        // which is also pesent at AXIS
		data_buf[tmp_in_item.user & 31] = tmp_in_item.data; 
	} while(!tmp_in_item.last); //Wait for the last item at the AXIS bus

    // Send stored data to the PS RAM.
	memcpy((int *)(out_bus + RAM_WORD_OFFSET + burst_count_reg * BURST_SIZE), data_buf, BURST_SIZE * sizeof(int));
	// Increment DMA destination counter
    burst_count_reg++;

Сначала идет описание модуля: сигналы, входящие и выходящие из него, задаются, как аргументы функции. Эта "функция" будет как-бы "вызываться" бесконечно в процессе работы модуля.
Блок CIC2 выдает аудиоданные каналов пачками (burst) с частотой 12 кГц, потому в своем модуле DMA я сделал небольшой промежуточный буфер для этих пачек. Задача модуля довольно проста - при появлении данных на входе, заполнить этот буфер, а потом запустить передачу DMA. После каждой передачи счетчик переданных пачек инкрементируется.
Все настройки DMA, включая адреса, куда будет производится запись, жестко прописаны прямо в коде. В этом модуле случае я реализовал двойную буферизацию - закончив заполнять один буфер (в ОЗУ PS), DMA начинает заполнять другой. При смене буфера блок DMA "программно" выдает сигнал прерывания на отдельный выход. Также при этом блок записывает по определенному адресу (регистр статуса) номер заполненного буфера.
Размер буфера - 200 пачек, то есть 200 семплов аудиоданных.

Теперь нужно подключить блок DMA к процессору PS. Для этого я использовал интерфейсы AXI HP. PS имеет 4 таких интерфейса, на каждом именно PL выступает мастером (то есть инициализирует передачу):

Картинка отсюда: Ссылка
Картинка отсюда: Ссылка

Подробнее про интерфейсы можно почитать здесь [10].

Однако, интерфейс AXI HP имеет тип AXI3, в то время, как выход модуля DMA - AXI4 (к сожалению, Vitis HLS не умеет синтезировать AXI3). Для преобразования интерфейсов нужен блок AXI Interconnect, это часть Vivado, причем Vivado может создать этот блок автоматически (Block Automation), обнаружив несоединенные интерфейсы. В настройках блока указывается ширина интерфейсов (Data Width of the AXI Crossbar) - 32 bit.

Можно заметить, что выше нигде не был упомянут корректирующий FIR-фильтр, который обычно устанавливается после CIC. В проекте KiwiSDR этот фильтр реализован программно на стороне CPU, так что в моем случае он тоже не нужен.

Формирование данных "водопада"

Как я упоминал выше, в WebSDR данные для построения спектра/водопада передаются отдельно от аудиоданных. В KiwiSDR есть варианты - [4 пользователя/4 водопада] либо [8 пользователей/2 водопада, доступные первым подключившимся]. Возможно сейчас есть и другие комбинации, проект постоянно изменяется.
В WEB-888 подход несколько другой - каналов водопада всего два, но они могут быть разделены между большим числом пользователей (но это приводит к падению FPS).
Я использовал у себя подход именно от WEB-888.

Формирование данных водопада производится аналогично формированию аудиоданных.
Структурная схема одного из каналов системы формирования водопада выглядит так:

Структурная схема одного из каналов системы формирования водопада
Структурная схема одного из каналов системы формирования водопада

NCO здесь - тот же самый, что и используется для формирования аудиоданных. Разрядность данных после перемножения - 25 бит.
Для формирования водопада нужен CIC с возможностью переключать коэффициент децимации вплоть до 1. CIC из состава Vivado так не умеет, так что я использовал CIC из проекта KiwiSDR. Его входная разрядность - 24 бит, выходная - 16 бит ("дополнительные" биты, получившиеся в процессе CIC-фильтрации, отбрасываются, как и в случае со звуком).
Для отображения водопада хватает разрядности и в 16 бит. Два потока квадратурных данных собираются в одну 32-битную шину AXIS, ее линия TDATA подключается к выходу CIC.

Для передачи захваченных данных в PS используется аналогичный, даже более простой DMA. Здесь не нужна двойная буферизация, не нужно записывать данные в регистр статуса.
Однако здесь возникает другая сложность - на минимальном, единичном коэффициенте децимации CIC получается, что данные приходят на вход DMA со скоростью 40 MSPS, то есть с частотой, которая у меня выбрана для тактирования всей PL. При этом шина AXI3 не позволяет передавать слишком большие объемы информации за раз - одиночная передача (burst) не может больше 16 элементов (beats). После этого нужно заново передавать адрес.
В таком случае интерфейс AXI3 не справлялся бы с приходящим на него потоком данных. В принципе. можно было бы сделать буферизацию, но я пошел более простым путем - переделал выход DMA на 64-битный (входные данные так и остаются 32-битными). Теперь DMA дожидается прихода двух 32-битных слов, собирает их них одно 64-битное, и отправляет их к PS (с точки зрения кода это уже не "memcpy", а просто запись данных по определенному адресу):
"*(uint64_t *)(out_bus + write_offset_64 + item_count_reg) = tmp_data64;"
За счет того, что передача по AXI4 с выхода DMA идет с частотой 20 MSPS (в 2 раза меньше тактовой частоты), интерфейс успевает передавать и адрес.
Этот DMA после запуска предает 4096 64-битных слов, запускать его нужно сигналом, передаваемым по отдельной линии "start_send".
Так как в проекте два канала водопада, то нужны и два модуля DMA. Адрес в памяти, по которому DMA будет производить запись, прописан в коде модуля DMA. Для того, чтобы эти адреса были разными, у модулей сделана специальная входная линия "module_idx".
Разрядность выходной шины DMA настраивается в настройках проекта:

После DMA также нужен блок AXI Interconnect. У меня два канала и два DMA, так что и блоков AXI Interconnect я использовал два. Вот тут я толком не разобрался - можно ли обойтись только одним блоком с другой конфигурацией (я пробовал, но у меня не получилось).

Управление блоками

И блоки формирования звука, и блоки формирования данных водопада требуют управления. Наиболее "правильный" метод для этого - использование интерфейса AXI GP, однако к тому моменту, когда мне потребовалось управление, у меня еще не было опыта работы с этим интерфейсом, так что я решил использовать куда более привычный SPI. Аппаратный модуль SPI встроен в PS, линии данных могут быть перенаправлены в PL через линии EMIO, в Linux есть драйвер для него, и для тестов данные можно передавать, просто отправляя команды в консоль Linux, а сам SPI можно вывести наружу чипа.

В проекте приемника SPI заведен на специальный блок-декодер, который принимает 6-байтовые посылки от PS, и формирует нужные значения на двух параллельных шинах (команды/данные). Эти шины заходят на все блоки, требующие управления, и блоки уже сами декодируют данные команд.

В блоках формирования аудиоданных нужно изменять только частоту DDS, у каждого из 8 каналов она может быть своя. Она задается 32-битным числом.
С блоками водопада сложнее - там требуется передавать не только частоту DDS, но и коэффициент децимации (13-битный), а также сигнал на запуск DMA. Все эти команды передаются, как отдельные SPI-пакеты.

Итоговая схема соединений блоков приемника (Block Design):

Тут хорошо видны цепочки блоков - формирователи аудиоданных/водопада -> DMA -> AXI Interconnect, которые сходятся на блоке процессора PS. Так же тут есть простенький блок для обнаружения перегрузки АЦП (индикация светодиодом); справа от PS: блок формирования сигналов сброса и блок декодирования данных SPI.

Настройки процессора (PS)

Определенная настройка режима работы периферии процессора может быть сделана именно в Vivado. Здесь можно указать, какие периферийные блоки будут включены, для некоторых прописать настройки, здесь же указываются настройки DDR памяти, установленной на плате. Конфигурацию можно настроить через GUI. В целом, пример настройки можно посмотреть в [1].

Настройки моего проекта:
  • В "PS-PL Configuration" нужно включить 3 шины AXI HP. У вcех должна быть установлена ширина - 64 бит.

  • В "MIO Configuration" - указать, на какие линии будут подключены некоторые аппаратные модули (SPI, Ethernet, SD-карта, отладочный UART, SPI (его линии через EMIO заводятся внутрь PL)).

  • В "Clock Configuration" производится настройка тактирования. Именно PS содержит в себе PLL, и сигнал с кварцевого генератора заводится туда, а не в PL (ПЛИС). Поэтому нужно включить линию FCLK_CLK0 - это один из выходов PLL PS, выведенный в ПЛИС.

  • В "Interrupts" нужно включить Fabric Interrupts - по этим линиям PS будет получать прерывания от блоков внутри ПЛИС. На всякий случай - LSB соответствует линия #61.

  • В "DDR Configuration" указываются настройки чипов, установленных на плате.

Использование ресурсов ПЛИС

Получившийся проект занимает 63% LUT, остальных ресурсов - еще меньше:

Это значит, что можно увеличить число аудиоканалов / добавить еще один блок формирования данных водопада / использовать АЦП более высокой разрядности.

Загрузка конфигурации в ПЛИС

Как я уже упоминал выше, Bitstream в ПЛИС загружает PS. В целях отладки Bitstream можно загрузить и через JTAG, на плате Antminer для этого есть соответствующий разъем J10. Лично я для загрузки использовал китайский программатор J-Link. Чтобы "состыковать" его с Vivado, нужно использовать утилиту "J-Link XVCD Server", входящую в набор утилит программатора J-Link. Пример использования есть здесь.
Также J-Link можно использовать для отладки проекта, используя его в связке с встраиваемым логическим анализатором ILA (часть Vivado).

Linux

Традиционный способ использования процессора Zynq - запуск на нем Linux (хотя ничто не мешает вообще не использовать операционную систему - для этого есть среда Xilinx Vitis).

Способов запустить Linux на ARM существует довольно много. Так как до этого у меня вообще не было такого опыта, я решил использовать самый простой способ, рекомендованный производителем чипов Zynq - собрать Linux, используя набор инструментов PetaLinux Tools. Полезные ссылки про использование PetaLinux: [11], [12], [13], [14].

В документации Xilinx работа с PetaLinux показана такой картинкой (я несколько модифицировал ее):

PetaLinux позволяет разом собрать FSBL (First Stage Bootloader) - первичный загрузчик, собрать загрузчик U-Boot, ядро Linux, сконфигурировать Device Tree, создать rootfs, и упаковать их вместе.

Далее - те шаги, которые понадобились мне для запуска Linux.

Подготовка файла с описанием "железа"

Первым делом нужно экспортировать из Vivado описание аппаратной конфигурации. Оно описано в [12]. В Vivado File -> Export -> Export Hardware. В диалоге нужно выбрать "Include bitstream" и указать путь к создаваемого файлу. В результате будет создан файл с расширением "xsa". После этого Vitis может быть нужен только для отладки через ILA.

Содержимое созданного xsa файла
Содержимое созданного xsa файла

Этот файл является архивом, если его распаковать, то можно увидеть такое содержимое.
"zynq_wrapper.bit" - это и есть конфигурация ПЛИС.
В "ps7_init.html" можно посмотреть настройки PS (те самые, что были описаны выше).
Файлы "*.c/*.h" - содержат в себе команды для настройки периферии PS в соответствии с заданными режимами и используются для сборки FSBL.

Сборка Linux

Все операции я выполнял в Lubuntu 20.04, запущенной на виртуальной машине. Версия Petalinux: v2022.2.

Первым делом нужно из терминала выполнить команду вида
"source /tools/Xilinx/PetaLinux/2022.2/tool/settings.sh" (путь к дистрибутиву Petalinux может быть другим). Это нужно для настройки среды выполнения Petalinux. Этот скрипт нужно запускать в терминале каждый раз, когда терминал перезапускается.

После этого можно перейти в нужную папку и выполнить команду:
petalinux-create -t project -n LinuxBoot1 --template zynq
которая запустит создание проекта "LinuxBoot1".

Переходим к нему: cd LinuxBoot1 и запускаем
petalinux-config --get-hw-description=./zynq_wrapper.xsa
Файл "zynq_wrapper.xsa" - тот самый, что создан Vitis, его нужно скопировать в папку проекта.
После этого должно появится окно конфигурации:

misc/config System Configuration
misc/config System Configuration

Здесь нужно зайти в Image Packaging Configuration -> Root File System Type -> и выбрать EXT4 (по умолчанию там INTRD). Это позволит Linux загружать rootfs с SD-карты. Сохраняем изменения и выходим из конфигуратора.

Далее нужно запустить petalinux-config -c kernel
Аналогично появляется меню конфигурации. Здесь я включал:
Kernel hacking -> Generic Kernel Debugging Instruments -> Debug Filesystem (CONFIG_DEBUG_FS).
Нужно было для того, чтобы в собранном Linux работала отладочная команда
cat /sys/kernel/debug/clk/clk_summary  - она показывает состояние системы тактирования процессора.
Также нужно включить: Device Drivers -> SPI Support -> включить User mode SPI device driver support

На этом этапе вроде можно также отредактировать Device Tree (отмечу, что я не уверен, что это можно сделать до запуска сборки - я сам вносил правки уже только после первой сборки).
Для этого нужно отредактировать файл "/project-path/project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi" - это набор пользовательских добавлений в финальный файл Device Tree.
У меня он выглядит так:

system-user.dtsi
/include/ "system-conf.dtsi"
/ {
    chosen {
    	bootargs = "console=ttyPS0,115200 clk_ignore_unused earlycon root=/dev/mmcblk0p2 rw rootwait";
    };
    
    reserved-memory {
      #address-cells = <1>;
      #size-cells = <1>;
      ranges;
  
      reserved: buffer@0x1f400000 {
         no-map;
         reg = <0x1f400000 0x200000>;
      };
   };
  
   reserved-driver@0 {
      compatible = "xlnx,reserved-memory";
      memory-region = <&reserved>;
   };
   
   openzynqsdr {
		compatible = "openzynqsdr";
		interrupt-parent = <0x04>;
		interrupts = <0x00 0x1d 0x01 0x00 0x1e 0x01 0x00 0x1f 0x01>;
	};
};

&spi0 {
			compatible = "xlnx,zynq-spi-r1p6";
			reg = <0xe0006000 0x1000>;
			status = "okay";
			interrupt-parent = <0x04>;
			interrupts = <0x00 0x1a 0x04>;
			clocks = <0x01 0x19 0x01 0x22>;
			clock-names = "ref_clk\0pclk";
			#address-cells = <0x01>;
			#size-cells = <0x00>;
			is-decoded-cs = <0x00>;
			num-cs = <0x01>;
			
			spidev@0x00 {
				compatible = "rohm,dh2228fv";
				spi-max-frequency = <8000000>;
				reg = <0x00>;
			};
};
  • "bootargs" здесь - аргументы, которые U-Boot передает в ядро Linux перед его запуском.

  • "reserved-memory" - принудительно резервируем область памяти, которую Linux не будет использовать для своих целей. Можно видеть, что зарезервирована область памяти по адресу 0x1F400000=500 Мбайт размером 0x200000=2 Мбайт. Именно в эту обласnь записывают данные три DMA, созданные в ПЛИС.

  • "openzynqsdr" - устройство, созданное только для того, чтобы описать прерывания от ПЛИС. Они описываются строкой:
    "interrupts = <0x00 0x1d 0x01 0x00 0x1e 0x01 0x00 0x1f 0x01>;"
    Каждый источник прерываний описывается группой по три байта, второй из них - номер прерывания. Подробнее - в [15]. Из конфигурации процессора в Vivado видно, что линия прерываний, подключённая к LSB входу шины, имеет номер 61. Вычитаем из него 32, и получаем 29 = 0x1d.

  • "spi0" - описание аппаратного модуля SPI.

На этом этапе можно сконфигурировать и другие части ОС и загрузчиков, используя другие конфигураторы. Про это можно почитать по ссылкам, что я привел выше.

После этого можно запускать сборку ОС:
petalinux-build
Она может занять достаточно много времени и потребует подключения к интернету - Petalinux сам скачает исходники ядра Linux.

После завершения сборки нужно запустить упаковку собранной ОС для запуска с карты:
petalinux-package --boot --force --fsbl images/linux/zynq_fsbl.elf --fpga images/linux/system.bit --u-boot
После сборки в папке /LinuxBoot1/images/linux мы получаем набор файлов. Для дальнейшей работы потребуются только три из них:

  • BOOT.BIN (содержит в себе FSBL, ядро Linux, Bitstream)

  • boot.scr (скрипт, в котором содержатся правила загрузки ядра Linux)

  • image.ub (собранный U-Boot)

В случае, если поменялась конфигурация ПЛИС, и, соответственно, Bitstream, можно еще раз выполнить petalinux-config --get-hw-description=./zynq_wrapper.xsa (она распакует xsa во временную папку внутри проекта) и запустить пересборку и упаковку данных. Однако, пересборка системы занимает много времени, и при изменении Bitstream не нужна.
Так что в таком случае можно просто выполнить команду
petalinux-package --boot --force --fsbl images/linux/zynq_fsbl.elf  --fpga project-spec/hw-description/zynq_wrapper.bit --u-boot - можно видеть, что здесь использован другой путь к Bitstream.

А вот если в Vivado был изменены настройки процессора, то даже запуск пересборки системы не поможет - по каким-то причинам Petalinux "не видит", что конфигурация изменилась. Я смог побороть это, принудительно запуская пересборку FSBL:
petalinux-build -c bootloader -x clean
petalinux-config -c bootloader
petalinux-build

Подготовка карты памяти

Во всех своих экспериментах я использовал SD-карту в качестве загрузочного диска. Zynq может загружаться с разных источников, для выбора источника данных на плате Antminer есть набор джамперов JP1-4, а указание того, как их расставлять, есть в схеме.
Для того, чтобы Zynq мог загрузить ОС с карты, она должна быть правильно отформатирована. Для Petalinux на ней должно быть два раздела - первый FAT32 (тут хватит и < 100 Мб), и второй - EXT4 (занимает оставшееся место на карте). Для создания разделов можно использовать AOMEI Partition Assistant (Windows) или gparted (Linux).
Упомянутые выше три файла нужно скопировать в первый раздел (FAT32).

Дистрибутив Linux

Для полноценной работы Linux нужен дистрибутив - набор программ и библиотек.
Я пробовал использовать много вариантов, вот этот вроде бы запустился: https://cdimage.ubuntu.com/ubuntu-base/releases/24.04/release/ - но он показался мне слишком урезанным, сеть там не работала.
Так что в итоге взял сборку отcюда: https://www.rcn-ee.com/rootfs/eewiki/minfs/ - вариант "ubuntu-20.04.6-minimal-armhf-2023-08-22.tar"
Этот дистрибутив нужно записать на второй раздел карты памяти (EXT4). Я делал это в Linux.
В терминале нужно зайти на раздел карты памяти: cd /media/osboxes/rootfs (путь может быть совершенно другим). После этого можно выполнить:
sudo tar -xvf  /path-to-distr/ubuntu-20.04.6-minimal-armhf-2023-08-22.tar
Если я не ошибаюсь, без sudo файлы могут записаться на карту с неверными правами.

После установки карты памяти в плату Antminer, и подачи на нее питания (я использую 5В, но можно использовать и более высокой напряжение), к ней можно подключится по UART через разъем J12. Скорость интерфейса - 115200.
Если все собрано верно, плата должна выдать в UART логи запуска U-Boot, ядра Linux, а потом уже должны запустится части дистрибутива и должен появиться стандартный диалог авторизации Linux.

Мне для запуска приемника было необходимо полноценное сетевое подключение, однако, сразу оно не запустилось.
Помогло исправление настроек: sudo nano /usr/lib/NetworkManager/conf.d/10-globally-managed-devices.conf
[keyfile]
unmanaged-devices=*,except:type:wifi,except:type:gsm,except:type:cdma, except:type:ethernet
Жирным выделена часть строки, которую я добавил. После этого Ethernet нормально заработал.

После того, как сеть нормально заработала, я обновил по сети сам дистрибутив при помощи sudo do-release-upgrade - сначала до Ubuntu 22, потом - до Ubuntu 24.04. Без этого не собиралось приложение WEB-888 - ему нужны были новые версии библиотек.

log2ram
Для того, чтобы Linux не так сильно "убивал" карту памяти постоянной записью логов, я использовал проект log2ram. Он достаточно прост и хорошо описан.
В настройках системы я установил "SystemMaxUse=20M".

SPI
SPI тоже нужно дополнительно настроить.
Для этого нужно выполнить:
sudo touch /etc/udev/rules.d/90-gpio-spi.rules
sudo nano /etc/udev/rules.d/90-gpio-spi.rules

и добавить редакторе в этот файл строку
KERNEL=="spidev0.0", OWNER="root", GROUP="spi"

Затем нужно выполнить:
sudo groupadd -f --system spi // создается группа "spi"
sudo usermod -a -G spi ubuntu // пользователь"ubuntu" добавляется в группу "spi"

При помощи такой команды можно отправить данные в SPI:
echo -ne "\x10\x00\x00\x00\x00\x00">/dev/spidev0.0

GPIO
Для управления GPIO я использовал библиотеку gpiod:
sudo apt-get install -y libgpiod-dev gpiod
Для того, чтобы она могла управлять GPIO, нужно поправить настройки системы:
sudo nano /etc/udev/rules.d/90-gpio-spi.rules
И добавить в файл:
SUBSYSTEM=="gpio", KERNEL=="gpiochip0", GROUP="gpio", MODE="0660"

Проверка работы DMA
Блок аудиоданных постоянно передает данные в ОЗУ при помощи DMA. Просто так прочитать содержимое ОЗУ в Linux нельзя, поэтому во время различных тестов я проверял содержимое ОЗУ при помощи вот этой маленькой программы.

Загружаемый модуль ядра

В этом проекте я решил использовать загружаемый модуль ядра для связи DMA и приложения SDR-приемника - так же, как это сделано в WEB-888. Там этот модуль ядра (драйвер) не только обеспечивает передачу принятых данных в приложение, но и позволяет управлять узлами приемника, расположенными в ПЛИС.

В принципе, этот драйвер не обязателен для работы SDR-приемника - можно было бы сделать работу с DMA и управление напрямую из приложения. Думаю, в WEB-888 драйвер сделан в первую очередь для сокрытия части исходного кода, связанного с управлением ПЛИС - его не нужно включать в состав приложения, которое должно быть Open Source по условиям лицензии.

Я же в своем проекте решил использовать модуль ядра только для самообразования - до этого мне никогда не приходилось их создавать. Для упрощения модуля я не стал реализовывать управление приемником (ПЛИС) через него, поэтому приложение приемника самостоятельно отправляет команды в SPI.

Для создания шаблонного проекта модуля я использовал Petalinux, как это показано в [16].
Из папки проекта Linux нужно выполнить:
petalinux-create -t modules -n sdrdma //sdrdma - название модуля
Команда выведет сообщение, что по определенному пути создан новый модуль.
Он состоит из файла исходного кода "sdrdma_main.c", и нескольких файлов, нужных для сборки модуля.
Для запуска сборки модуля достаточно выполнить petalinux-build -c sdrdma
Собранный модуль (файл "sdrdma.ko") в моем случае находился по такому пути (номер версии ядра может быть другим):
/path-to-proj/Peta/LinuxBoot1/build/tmp/sysroots-components/zynq_generic/sdrdma/lib/modules/5.15.36-xilinx-v2022.2/extra
Этот файл нужно перенести на диск Zynq, и уже в его Linux выполнить команду:
sudo insmod sdrdma.ko // загрузка модуля в Linux
Выгрузить его можно командой:
sudo rmmod sdrdma

Исходный код моего варианта драйвера можно посмотреть здесь: https://github.com/iliasam/OpenZynqSDRApp/tree/develop/kernel_dma_driver

Драйвер должен выполнять три задачи:

  1. Он должен ожидать прерываний от ПЛИС - они генерируются DMA при заполнении буфера в ОЗУ данными аудио или водопада.

  2. Обнаружив прерывание, нужно максимально быстро скопировать данные из буфера DMA в промежуточный буфер.

  3. По запросу от стороннего приложения, нужно передать ему данные из промежуточного буфера.

Про использование прерываний в Linux есть такие статьи: [17], [18].
Сама по себе регистрация обработчика прерываний в коде драйвера очень проста - она выполняется при помощи функции request_irq(). Однако в эту функцию нужно передать номер прерывания, и с этим у меня возникли сложности. Во многих примерах этот номер задается как константа. Я пробовал разные варианты значений, и ни один из них не заработал, так что я использовал более сложный вариант - номер берется из данных DTS.
Для этого нужно использовать функцию
struct resource *res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
Последний аргумент в данном случае (0) - номер прерывания в списке DTS для устройства "openzynqsdr".
Значение "res->start" и даст искомый номер. У меня он вышел равным 52.
После вызова request_irq() это прерывание будет прописано в Linux, и его можно будет увидеть в списке других прерываний:
cat /proc/interrupts , также тут можно видеть счетчик числа вызовов обработчика прерывания.

Обработка прерываний от каналов аудиоданных и водопадов различается - прерывание от первых имеет более высокую частоту (61 Гц) и никак не зависит от CPU. Для хранения аудиоданных я использовал FIFO, и доступа к нему я использовал mutex. Я посчитал, что из обработчика прерывания лучше не работать с mutex напрямую, так что в самом обработчике прерывания аудиоданные копируются в одиночный временный буфер, а затем в системную очередь задач добавляется Workqueue (это решение как раз описано в [18]).
И уже обработчик Workqueue, вызываемый Linux, копирует данные из временного буфера в FIFO.
С двумя другими прерываниями - данные водопада, такие сложности не нужны, тут не требуется FIFO и можно копировать данные в буфер прямо в обработчике прерывания.

В Linux нельзя просто так получить доступ к определенному участку памяти даже для чтения.
Сначала нужно указать область памяти, к которой нужен доступ и получить специальный указатель на нее:
volatile unsigned long *sound_virtual_base = ioremap(0x1F400000, 100 * sizeof(uint32_t));
Для копирования данных из ОЗУ используется функция
ioread32(sound_virtual_base) для чтения одиночного слова
или memcpy_fromio(). Подробнее - в документации на Linux.

Связь между приложением и модулем ядра сделана так же, как и в WEB-888 - через специальный файл устройства, который создается модулем. Описание и примеры можно посмотреть в [19], [20]. В этом модуле я не стал реализовывать стандартные для файла операции вроде read/write. Все операции производятся через системный вызов ioctl. Использование ioctl описано тут: [21]. Именно при помощи ioctl приложение может "попросить" модуль передать актуальные аудиоданные или данные водопада.

Для того, чтобы обеспечить автоматическую загрузку этого модуля ядра при запуске Linux, мне пришлось:

  • В папке /etc/modules-load.d создать файл sdrdma.conf со строкой sdrdma

  • Создать папку  /lib/modules/5.15.36-xilinx-v2022.2 - ее у меня не было

  • Выполнить sudo depmod

  • Поместить модуль ядра sdrdma.ko в папку /lib/modules/5.15.36-xilinx-v2022.2/extra

  • Еще раз выполнить sudo depmod

После этого можно вручную загрузить модуль - sudo modeprobe sdrdma

Приложение приемника

Как я упоминал выше, я использовал приложение от приемника WEB-888. Приложение рассчитано на работу под Alpine Linux, так что мне пришлось модифицировать его для работы в Ubuntu.
Обмен данными с ПЛИС в WEB-888 был реализован в файле /zynq/peri.cpp , так что его больше всего пришлось переделывать.

Приложение я собирал прямо на Zynq - это медленно (пересборка занимает больше 40 минут), но зато нет сложностей с окружением.
Запуск сборки выглядит так:
mkdir build
cd build
cmake ..
cmake --build .
В результате в папке /build/ создается исполняемый файл websdr.bin. Его лучше запускать от sudo - так приложение получает возможность управлять операционной системой.
Для работы приложению нужны файлы конфигурации, которые должны находится по пути, указанному в CMakeLists.txt - DIR_CFG / DIR_SAMPLES.

Это приложение я добавил в автозагрузку, используя systemd. При завершении приложения оно автоматически перезапускается средствами Linux.
Как и в приемнике KiwiSDR, через веб-сервер, реализуемый приложением, можно перезапустить или выключить сам компьютер. Можно посмотреть логи приложения или даже выполнить какую-то команду в виртуальном терминале.

Думаю, тут стоит отметить особенность обработки данных "водопада" в приложении. Аппаратных источников данных "водопада" - всего два. Так как клиентов приемника может быть больше, приложение должно разделять ресурсы этих источников между всеми пользователями. Для этого приложение быстро изменяет настройки источники данных - (частоту и коэффициент децимации), в соответствии с настройками клиентов. Только после этого запускается захват данных.
Скорость передачи данных от источника напрямую зависит от выбранного коэффициента децимации, а он зависит от выбранного пользователем увеличения участка спектра. На максимальном значении децимации (2^11) и при частоте АЦП, равной 40МГц, время передачи всех элементов водопада (8192) занимает около 0.4 сек, что приводит к довольно низкому FPS водопада. Очевидно, что если все клиенты приемника используют большие увеличения спектра, то FPS упадет еще ниже. Насколько я понял, и в WEB-888 и в KiwiSDR для решения этой проблемы используют какой-то хитрый режим работы аппаратных источников данных водопада, но как он работает, я толком не понял, так что не стал его реализовывать.

Использование приемника

После запуска приложения оно запускает web-сервер, на который можно зайти из браузера: http://my_sdr_ip:8073
Получившаяся страница выглядит так:

Web-страница приемника
Web-страница приемника

Справа находятся довольно стандартные для SDR-приемников элементы управления. Наверху, вместо графика спектра, могут отображаться данные одного из расширений (extension) - встроенных в приемник программ обработки данных.

Антенна

От выбранной антенны и ее расположения зависит очень многое. У меня была возможность установить антенну только на балконе панельного дома. Это довольно паршивый случай, так как нет возможности установить длинную антенну, дом является источником помех, а железобетонные панели дома не способствуют хорошему приему радиосигнала.

Так как описанный приемник предназначен для приема сигналов в широкой полосе, я не стал использовать резонансную магнитную рамку, как ранее в [3], а использовал нерезонансную антенну с усилителем LZ1AQ - китайского производства:

Китайский набор антенны с усилителем LZ1AQ
Китайский набор антенны с усилителем LZ1AQ

В принципе, она показала себе неплохо, по сравнению с самодельной MiniWhip. Наилучших результатов от нее я добивался, высунув ее в окно максимально далеко на деревянной палке. Вынос антенны из дома, даже на 2м, значительно улучшает ситуацию с шумом, и несколько увеличивает уровни принимаемых сигналов:

Верхняя часть - антенна внутри дома, около окна; нижняя часть - антенна выставлена за окно.
Верхняя часть - антенна внутри дома, около окна; нижняя часть - антенна выставлена за окно.

В некоторых случаях уровень шума падает еще более значительно, а уровень принимаемого сигнала ощутимо увеличивается.

Подстройка частоты
Кварцевый генератор, установленный на плате, может иметь некое отклонение частоты от точного значения, что приводит к тому, что и частота тактирования АЦП отклоняется от желаемых 40 МГц, в результате возникает ошибка по частоте у всех принимаемых сигналов.
Так как в моем приемнике нет GPS-приемника, единственный способ откалибровать частоту - по принимаемым сигналам. В KiwiSDR такая функциональность заложена - нужно точно настроиться на некую радиостанцию с известной частотой (она должна быть кратна 1 кГц), и вызвать специальную команду через экранное меню. У меня получилось, что мой кварцевый генератор оказался довольно неточным - ошибка оказалась около 110 ppm. Однако стабильность у него вышла сносной - в процессе тестов я не видел позже, чтобы ошибка по частотам принимаемых станций превышала 20 Гц.

Калибровка уровней принимаемых сигналов
Для того, чтобы приемник можно было сравнивать с другими, его нужно откалибровать. Для калибровки я использовал трансивер HackRF One с аттенюатором (он используется как источник тестового сигнала) и осциллограф для контроля за уровнем сигнала на входе приемника. В настройках приемника можно подстраивать отдельно показания S-метра (они вычисляются по аудиоданным) и смещение значений на графике спектра. Важно отметить, что в этом приемнике уровень шумовой полки плавает в зависимости от выбранного увеличения спектра (так как увеличение коэффициента децимации работает как фильтрация и приводит к уменьшению шума).
После калибровки уровень шума приемника без антенны в режиме модуляции USB с полосой 2400 Гц по показаниям S-метра составляет около -100дБм, при увеличении усиления VGA это значение увеличивается на несколько дБ. Такая картина наблюдается во всей полосе частот, принимаемых приемником.

Использование SDRangel
Данные от приемника можно принимать не только в браузере, но и в программе SDRangel - она поддерживает протокол передачи данных KiwiSDR. Однако данные водопада, предаваемые приемником, при этом не используются - водопад строится только по аудиоданным. Выглядит это так:

Прием и декодирование данных FT8 в SDRangel
Прием и декодирование данных FT8 в SDRangel

Что можно принять на приемник

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

Диапазон 40м, слева - телеграф, в центре - голос на LSB, справа - несколько мощных вещательных радиостанций
Диапазон 40м, слева - телеграф, в центре - голос на LSB, справа - несколько мощных вещательных радиостанций

Приемник имеет два вида автоматических декодеров телеграфа, но мне не понравилось, как они работают. Возможно, в моем случае отношение сигнал-шум было слишком низким.

Вещательные станции на КВ принимаются неплохо, особенно к вечеру. Также вечером можно услышать несколько станций на СВ, но там высокий уровень шума.
Вещательные станции на КВ принимаются неплохо, особенно к вечеру. Также вечером можно услышать несколько станций на СВ, но там высокий уровень шума.

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

Можно декодировать сигналы довольно молодого, но популярного вида модуляции FT8/FT4:

Прием FT8 на 20м. Принятые данные отображаются вверху.
Прием FT8 на 20м. Принятые данные отображаются вверху.

По числу декодированных сообщений можно косвенно оценивать состояние эфира.
При этом можно запустить в разных окнах несколько декодеров, принимающих данные на разных диапазонах.
Декодер приемника может периодически передавать информацию о принятых сообщениях на сайт pskreporter.info:

Изображение с pskreporter.info, собрано с нескольких диапазонов за несколько часов приема.
Изображение с pskreporter.info, собрано с нескольких диапазонов за несколько часов приема.
Это же изображение, но в более крупном масштабе.
Это же изображение, но в более крупном масштабе.

Также можно принимать и декодировать сигнала радиомаяков WSPR (радиолюбительские маяки, передающие сигнала на малой мощности):

Прием данных радиомаяков WSPR. Один цикл передачи занимает около 2 минут. В окне декодера можно видеть мощности передатчиков.
Прием данных радиомаяков WSPR. Один цикл передачи занимает около 2 минут. В окне декодера можно видеть мощности передатчиков.

Декодер также может передавать данные на сайт WSPRnet:

Маяки WSPR, принятые за несколько часов.
Маяки WSPR, принятые за несколько часов.

Стоит отметить, что приемник позволяет запускать декодеры сигналов FT8/FT4/WSPR в скрытом режиме, при этом, очевидно, на каждый декодер выделяется канал аудиоданных, что сокращает число доступных внешних подключений. Таким образом владелец приемника может постоянно передавать информацию на pskreporter.info/WSPRnet.

Можно и декодировать сообщения радиотелетайпа (RTTY), и SSTV. К сожалению, SSTV я практически не смог принять, а на RTTY либо не упалось принять что-то интересное, либо я не сделал нужные скриншоты.

Можно декодировать метеофаксы:

Видно, что качество принятого изображения не очень высокое. Возможно, это связано с тем, что  антенна - довольно маленькая.
Видно, что качество принятого изображения не очень высокое. Возможно, это связано с тем, что антенна - довольно маленькая.

Также можно декодировать данные радиостанций, передающих время. Мне удалось принять российскую станцию RBU и немецкую DCF77 (вторая принимается хуже и не всегда):

Прием данных RBU
Прием данных RBU

Можно просто включить отображение графика текущего уровня сигнала:

На скриншоте - сигнал станции RWM, импульсы с частотой 1 Гц
На скриншоте - сигнал станции RWM, импульсы с частотой 1 Гц

Можно даже принимать цифровое радиовещание - DRM.

Также приемник имеет декодеры некоторых других цифровых протоколов, но я не смог их принять - в моей местности таких сигналов нет.

Репозитории проекта

Приложение для Linux и модуль ядра: https://github.com/iliasam/OpenZynqSDRApp
Проект PCB, проект Vivado, Vitis HLS: https://github.com/iliasam/OpenZynqSDR_HW

Дополнительные ссылки
  • [1] fpga-systems.ru - ZYNQ HW: EBAZ4205: Ссылка

  • [2] habr.com - Отладочная плата с жирной ПЛИС за 500р или что делать со старым антмайнером — как я делаю собственную платформу: Ссылка

  • [3] habr.com - Простой SDR приёмник на ПЛИС: Ссылка

  • [4] www.rx-888.com FPGA Design: Ссылка

  • [5] habr.com - Разработка и тестирование целочисленного сумматора с AXI-Stream интерфейсами, часть 2: Ссылка

  • [6] bastibl.net - FutureSDR 2 (описание работы с Vivado DMA): Ссылка

  • [7] community.element14.com - Use the ZYNQ XADC with DMA part 1: bare metal: Ссылка

  • [8] github.com - HLS AXI master and Yocto kernel driver for Zynq: Ссылка

  • [9] pp4fpgas.readthedocs.io - Lab: AXI4-Burst Mode (m_axi): Ссылка

  • [10] habr.com - Zynq. Передача данных между процессорным модулем и программируемой логикой: Ссылка

  • [11] habr.com - Установка OC Linux на плату Kintex 7 (KC705) от Xilinx с софт-процессором MicroBlaze часть 2 : Ссылка (тут использован процессор MicroBlaze, а не ARM).

  • [12] nuclearrambo.com - Running Petalinux on Zynq SoC from scratch – Zybo board: Ссылка (вариант из web.archive.org)

  • [13] xilinx.github.io - Step 2: Create the Software Components with PetaLinux: Ссылка (вариант для ZynqMP)

  • [14] dspsandbox.org - Ubuntu on Zynq and ZynqMP devices: Ссылка

  • [15] billauer.co.il - Interrupt definitions in DTS (device tree) files for Xilinx Zynq-7000 / ARM: Ссылка

  • [16] xilinx.github.io - Creating Custom IP and Device Drivers for Linux - Example 12: Device Driver Development: Ссылка

  • [17] embetronicx.com - Interrupts Example Program in Linux Kernel: Ссылка

  • [18] embetronicx.com - Threaded IRQ in Linux Device Driver: Ссылка

  • [19] embetronicx.com - Device File Creation: Ссылка

  • [20] embetronicx.com - Cdev structure and File Operations: Ссылка

  • [21] embetronicx.com - IOCTL in Linux (I/O Control): Ссылка

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


  1. lesha108
    17.06.2025 06:46

    Здорово! Многие ждали что-то подобное. Вопрос такой - не пришлось ли бороться с разной длиной проводников от ADC к Zynq?


    1. Gudd-Head
      17.06.2025 06:46

      Разная длина на 40 МГц?)


    1. iliasam Автор
      17.06.2025 06:46

      Нет, не было такой проблемы. Действительно, 40 МГц - это 25нс, свет проходит за это время около 7.5м. Это значительно больше, чем имеющаяся разность длин проводников.


  1. nv13
    17.06.2025 06:46

    Круто. А у Вас такт на АЦП прямо с SoC подаётся? Если это цифровой вывод общего назначения, то там джиттер может быть большой, а это приводит к снижению с/ш приёмника за счёт размазывания спектров внеполосовых сигналов и наложения. Кроме того, хорошо бы смоделировать тракт и померять ачх приёмника, потому что комбинация сик фильтров с децимацией может давать наложения.


    1. iliasam Автор
      17.06.2025 06:46

      Да, тактирование прямо с PLL PS идет. Я тоже думал про джиттер, поэтому на плате АЦП сделал посадочное место под TCXO генератор, также имеются перемычки и отдельная линия, позволяющая завести тактовый сигнал в ПЛИС.
      Однако сейчас у меня эфир довольно шумный, так что у меня просто не было стимула добиваться более низкого уровня самого приемника.


      1. nv13
        17.06.2025 06:46

        Понял, всё правильно тогда. Но джиттер как раз и существенен при шумном эфире - из за него хвосты от внеполосных спектров лезут в полосу приёмника. То есть динам диапазон вроде и высокий, а внеполосные шумы и сигналы сразу гробят эф. динам диапазон

        И ещё - может я неправильно понял схему буферизации и обмена с cpu/user space, но я в своё время использовал dma для заполнения страниц кольцевого буфера в kernel space, а блокирующий read device для его чтения и освобождения, ioctl только для настройки и управления приёмником. Правда приёмник работал в стробах по несколько мс


  1. Strijar
    17.06.2025 06:46

    Спасибо, интересно почитать как кто-то еще решает задачи через которые прошел. Я когда задумывал собственный трансивер с Zynq думал про AD9866. Даже плату развел и распаял. Но в итоге нашелся в компанию электронщик и мы сделали на LTC2208 + DAC904E. Кстати FFT я делал прямо на PL части, правда потом отказался потому что влазило только в 7020, а хотелось иметь возможность и на 7010 собирать. Теперь у меня FFT считается на сопроцессоре в виде MicroBlaze (; Кстати наш проект открытый, если интересно - буду рад видеть в команде разработчиков.

    Кстати STTV днем довольно активно идет на 14.230, а RTTY на 10.100 передают погоду на Балтийский регион. Еще интересно послушать Olivia на 14.106 - правда не помню умеет ли KiwiSDR ее декодировать.