В этой статье описано, как создать QPSK передатчик и приёмник на HDL языке, интегрировать их в ПЛИС и проверить работоспособность. В одной из прошлых статей было рассказано, что нужно чтобы инициализировать ad9361 на PlutoSDR. Эта статья может считаться продолжением работы, которая была начата в той статье. Для создания HDL реализации QPSK приёмопередатчика использованы Matlab и Simulink c их возможностями генерации HDL кода из моделей. В качестве моделей использованы примеры, которые предоставляются с дополнительными пакетами, разработанные специально для демонстрации возможностей генерации HDL кода. Запустим этот код на плате — ещё одном клоне PlutoSDR с более ресурсоёмкой ПЛИС Z7020. И посмотрим на график созвездия.

Я не эксперт в области программирования ad9361. Это та область, которую я хочу изучить. Я пытаюсь разобраться в основах и мне проще учиться, когда я вижу, как это работает. Вот почему я это делаю. Не потому, что я уже всё знаю об ad9361. И если вы тоже чему-то научитесь, прочитав мою статью, это будет здорово. Но, пожалуйста, не полагайтесь на мои статьи, чтобы понять теорию. Есть множество отличных книг и других источников по этой теме, где вы сможете найти более подробную информацию. Я надеюсь, что благодаря этой статье вы научитесь применять некоторые базовые принципы программирования ad936x в реальных системах. Если вы заметите ошибку или что-то, что можно улучшить, пожалуйста, напишите мне в комментариях или личных сообщениях. Тогда мы сможем учиться вместе, и, возможно, я смогу исправить это и показать в новых статьях.

HDL реализация передатчика и приёмника

В статье используется Matlab 2020b с пакетами Communications Toolbox, HDL Coder и Simulink.

>> demo('Communications Toolbox')

Если в командной строке Matlab ввести команду, указанную выше, то откроется много интересных примеров, среди которых ряд оптимизированных под HDL реализацию.

Рис. 1. Примеры Communications Toolbox
Рис. 1. Примеры Communications Toolbox

Сама модель QPSK передатчика имеет кое-какое описание и несколько команд, как из неё сгенерировать HDL. Откроем модель и запустим её, предварительно изменив в Matlab каталог на рабочий.

Рис. 2. Результаты работы модели QPSK передатчика
Рис. 2. Результаты работы модели QPSK передатчика

Модель запустилась, проработала положенное время (в настройках указана 1 секунда) параллельно строя график созвездия. Видно, что точки просто идеально сосредоточены в своих четвертях. Посмотрим, что делает модель приёмника.

Рис. 3. Результаты работы модели QPSK приёмника
Рис. 3. Результаты работы модели QPSK приёмника

В этой модели есть графики с созвездием после частотной синхронизации и символьной синхронизации, а также сообщение, которое принимается и качественный показатель принятого сообщения. Здесь в качестве источника данных заранее подготовленный файл. Но для экспериментов можно скопировать из модели передатчика блок HDLTx и вставить его в модель приёмника. Закомментируем блок Captured Data и используем в качестве источника данных модель передатчика. Тогда чтобы всё корректно работало, всё равно необходимо открывать модель передатчика, а потом модель приёмника с блоком HDLTx в качестве источника данных. Позже станет понятно, почему так. А пока соединим всё и запустим модель приёмника с новым источником данных.

Рис. 4. Результаты работы модели QPSK приёмника с HDLTx в качестве источника данных.
Рис. 4. Результаты работы модели QPSK приёмника с HDLTx в качестве источника данных.

И это работает. Похуже, конечно, но сейчас не это главное. Сейчас главное, что модель из коробки работает с частотой дискретизации 200 КГц. А минимальная частота дискретизации, которую позволяет установить драйвер No-OS от Analog Devices для ad9361 это 2 МГц с небольшим. Поэтому перед нами встаёт задача увеличить частоту дискретизации этой модели, при этом сохранив возможность передавать данные. И это возможно. Путей как всегда много, но мы здесь рассмотрим самый простой.

На выходе передатчика и на входе приёмника стоят согласованные фильтры с приподнятым косинусом. В их параметрах есть поля upsampling и downsampling и это то что нужно. Ещё придётся использовать другие коэффициенты для этих фильтров, но это тоже несложно сделать. Путём недолгого изучения моделей можно понять, что они используют preload файлы на этапе загрузки программного обеспечения, откуда и подтягиваются все параметры. Если путь установки Matlab был стандартным, то файлы найдутся по пути C:\Program Files\Polyspace\R2020b\toolbox\comm\commdemos. Нас интересуют файлы commqpsktxhdl_init.m для передатчика и commqpskrxhdl_init.m для приёмника. Начнём с передатчика.

Скрытый текст
function SimParams = commqpsktxhdl_init
% Set simulation parameters
% SimParams = sdruqpsktx_init

%   Copyright 2012-2013 The MathWorks, Inc.

% General simulation parameters
SimParams.Upsampling =4; % Upsampling factor for root raised cosine transmit filter
SimParams.Fs = 2e5; % Sample rate
SimParams.Ts = 1/SimParams.Fs; % Sample time
SimParams.PacketSize = 100; % Number of modulated symbols per packet

% Tx parameters
SimParams.BarkerLength = 13; % Number of Barker code symbols
SimParams.DataLength = (SimParams.PacketSize - SimParams.BarkerLength)*2; % Number of data payload bits per packet
SimParams.PreambleLength = (SimParams.BarkerLength *2); % Number of preamble bits per packet
SimParams.RCGroupDelay = 5; % Group delay for the root raised cosine receive filter


load ('Data4commQPSKTxHDL', 'data');
SimParams.data = data;
load ('Data4commQPSKTxHDL', 'preamble');
SimParams.preamble = preamble;
load ('Data4commQPSKTxHDL', 'rcRxFilt');
SimParams.rcRxFilt = rcRxFilt;
load ('Data4commQPSKTxHDL', 'rcTxFilt');
SimParams.rcTxFilt = rcTxFilt;


В коде выше можно найти строки, которые устанавливают частоту дискретизации равной 200 КГц (строка 9), интерполяцию в 4 раза (строка 8) и загружают пару фильтров. Один фильтр используется в модуле HDLTx в качестве согласованного фильтра с приподнятым косинусом, а второй фильтр rcRxFilt используется в отладочном модуле Signal Quality Measurement. Его тоже можно сделать для отладки.

С частотой дискретизации и интерполяцией надо быть осторожными. Нельзя просто так взять и установить параметры как вздумается. Здесь всё связано. Важно сохранить символьную скорость, которая в этих моделях равна 50 КГц. Это несложно понять, разделив значение частоты дискретизации на значение интерполяции.

\frac{Fs}{Upsampling}=50E3

Нам необходимо подобрать такие параметры, чтобы сохранить символьную скорость равной 50 килогерцам. Но и сильно задирать не стоит, чтобы не раздувать коэффициенты для согласованных фильтров, ведь мы ещё хотим это уместить в не очень-то и ресурсоёмкие ПЛИС. В статье остановимся на частоте дискретизации 700 КГц, для сохранения символьной скорости установим значение интерполяции равным 14 (700000/14=50000). И перейдём к фильтрам. Строки 24, 25, 26 и 27 можно просто закомментировать и использовать для создания фильтров функцию rcosdesign.

rcRxFilt = rcosdesign(0.5, 3, SimParams.Upsampling, 'sqrt');
SimParams.rcRxFilt = rcRxFilt;

rcTxFilt = rcosdesign(0.5, 3, SimParams.Upsampling, 'sqrt');
SimParams.rcTxFilt = rcTxFilt;

Первый параметр — это Rolloff factor, установим его пока равным 0.5, а потом поэкспериментируем с ним. Следующий параметр групповой задержки сильно влияет на количество коэффициентов в фильтре. Для этой статьи использовано значение всего 3. Это годится только для очень бюджетных систем. Но ведь мы здесь не спутниковую систему передачи данных строим:) Ну а дальше всё стандартно.

Скрытый текст
function SimParams = commqpskrxhdl_init
% Set simulation parameters
% SimParams = sdruqpskrx_init

%   Copyright 2011-2012 The MathWorks, Inc.

% General simulation parameters
SimParams.M = 4; % M-PSK alphabet size
SimParams.Upsampling = 4; % Upsampling factor
SimParams.Downsampling = 2; % Downsampling factor
SimParams.Fs = 2e5; % Sample rate
SimParams.Ts = 1/SimParams.Fs; % Sample time
SimParams.FrameSize = 100; % Number of modulated symbols per frame

% Tx parameters
SimParams.BarkerLength = 13; % Number of Barker code symbols
SimParams.DataLength = (SimParams.FrameSize - SimParams.BarkerLength)*log2(SimParams.M); % Number of data payload bits per frame
SimParams.MsgLength = 105;
SimParams.RCGroupDelay = 5; % Group delay for the raised cosine receive filter

% Rx parameters
K = 1;
A = 1/sqrt(2);
% Look into model for details for details of PLL parameter choice.
SimParams.FineFreqPEDGain = 2*K*A^2+2*K*A^2; % K_p for Fine Frequency Compensation PLL, determined by 2KA^2 (for binary PAM), QPSK could be treated as two individual binary PAM
SimParams.FineFreqCompensateGain = 1; % K_0 for Fine Frequency Compensation PLL
SimParams.TimingRecTEDGain = 2.7*2*K*A^2+2.7*2*K*A^2; % K_p for Timing Recovery PLL, determined by 2KA^2*2.7 (for binary PAM), QPSK could be treated as two individual binary PAM, 2.7 is for raised cosine filter with roll-off factor 0.5
SimParams.TimingRecCompensateGain = -1; % K_0 for Timing Recovery PLL, fixed due to modulo-1 counter structure

load ('Data4commQPSKRxHDL', 'data_in');
assignin('base', 'cdfq', data_in);
load ('Data4commQPSKRxHDL', 'rcRxFilt1');
SimParams.rcRxFilt = rcRxFilt1;

% For CORDIC
SimParams.rad_WL = 16;
SimParams.rad_FL = 13;
SimParams.car_WL = 32;
SimParams.car_FL = 26;
SimParams.tb = [0.4636    0.2450    0.1244    0.0624    0.0312];

% The CORDIC output represents an angle, say alpha. Given M=3 and the fact 
% that CFC raises the input signal to the power of SimParams.M (4 in the 
% case of QPSK modulation), the actual frequency is estimated as
%
%    f_hat = alpha/[pi*SimParams.Ts*(3+1)]/SimParams.M;
%
% which, in one sample period, translates into a phase shift of
%
%    phase_hat = 2pi*f_hat*SimParams.Ts 
%              = alpha/(2*SimParams.M)
%
% This phase needs to multiply 2^17/(2pi) before feeding into NCO,
% therefore, the net constant should be (-1 is for compensation)
%
%    SimParams.CFC_Const = (-1)*[1/(2*SimParams.M)]*[2^17/(2pi)];
%                        = -(2^15)/(pi*SimParams.M)
% However, the (1/pi) term is already taken care of by Normalized Radians
% representation in Complex To Magnitude Angle HDL Optimized, so:

SimParams.CFC_Const = -(2^15)/(SimParams.M);

% Phase value needs to multiply 2^17/(2pi) before feeding into NCO,
% do not use HDl NCO block 
SimParams.FFC_NCOConst = -(2^16)/pi;

SimParams.WL_acc = 18;             % accumulator word length
SimParams.WL_lut = 16;             % w. length of quantized accumulator bits (input to LUT)
SimParams.WL_out=16;               % output word length
SimParams.FFC_Const = -2^(SimParams.WL_acc)/(2*pi);

SimParams.phMSB = SimParams.WL_acc-3;
SimParams.phLSB = SimParams.phMSB- SimParams.WL_lut+2+1;

invec = 0:2^(SimParams.WL_lut-2)-1;
SimParams.sin_tbl = sin(2*pi*invec./2^SimParams.WL_lut);
SimParams.cos_tbl = cos(2*pi*invec./2^SimParams.WL_lut);


Код для приёмника длиннее, но суть та же. Нас интересуют строки 9, 10, 11 и фильтр на 32, 33. Частота дискретизации 700 КГц, децимация 7 (приёмник немного по-другому спроектирован), интерполяция 14.

rcRxFilt = rcosdesign(0.5, 3, SimParams.Upsampling, 'sqrt');
SimParams.rcRxFilt = rcRxFilt;

Фильтр реализован точно, такой же. Только параметр теперь называется по-другому. Сохраним эти файлы и перезагрузим Matlab. Включим на вкладке Debug отображение частот дискретизации. Но запускать пока не надо спешить. В передатчике надо задержать данные на выходе на значение, равное значению интерполяции. Это делается в HDLTx / Pipeline Register 3 (рис. 6). А в приёмнике есть две очень удобные константы с полем Sample time, изменив которые вся модель начнёт работать на новой частоте дискретизации. Они в HDLRx / Autimatic Gain Control / Reference и Loop gain. Дело в том, что значениям полей Sample time присваивается значение переменной commqpskrx_hdl.Ts. Получается что

Sample time = SimParams.Ts = \frac{1}{SimParams.Fs }= 1.4285714285714286e-6

То есть период равен 1.4285714285714286 мкс (рис. 7). Тогда частота равна

\frac{1} {0.0000014285714285714286} = 699 999.999999999986

Примерно 700 КГц. Но модель построена для 200 КГц и, чтобы не лезть глубоко в эту модель, лучше оставить пока что эту частоту в покое. А как из 700 КГц получить 200 КГц? Поделить на 3.5 (рис. 8). Так и поступим.

Рис. 5. Согласованный фильтр передатчика с новым значением интерполяции
Рис. 5. Согласованный фильтр передатчика с новым значением интерполяции
Рис. 6. Задерживаем данные на новое значение
Рис. 6. Задерживаем данные на новое значение
Рис. 7. Неверные значения периода дискретизации
Рис. 7. Неверные значения периода дискретизации
Рис. 8. Сохранение периода дискретизации
Рис. 8. Сохранение периода дискретизации

Теперь можно запустить модель.

Рис. 9. Результаты работы модели QPSK приёмопередатчика с выходной частотой дискретизации 2.45 МГц
Рис. 9. Результаты работы модели QPSK приёмопередатчика с выходной частотой дискретизации 2.45 МГц

На рисунке 9 показаны результаты работы модели QPSK приёмопередатчика с новой частотой дискретизации. Не спутниковая система, конечно, но для DIY работает достаточно сносно. Обратите внимание на стрелки. Из передатчика выходят семплы с частотой 2.45 МГц. А эту частоту уже можно установить через драйвер No-OS от Analog Devices. Фильтр получился с 43 коэффициентами (рис. 10) и это хорошо, потому что умножители в Z7010 и Z7020 надо экономить. И это позволяет нам двигаться дальше.

Рис. 10. Импульсная характеристика согласованного фильтра
Рис. 10. Импульсная характеристика согласованного фильтра

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

>> makehdl('commqpsktxhdl/HDLTx','TargetLanguage','Verilog')
>> makehdl('commqpskrxhdl/HDLRx','TargetLanguage','Verilog')

Синтез, имплементация и генерация bitstream

Для проверки на первом этапе в проект можно добавить сгенерированные исходные файлы передатчика и приёмника, соединив их напрямую. К сожалению для такого на z010 уже не хватит ресурсов. Но можно воспользоваться z020. Есть много доступных бюджетных плат — клонов PlutoSDR на z020 и 9361. Когда стало понятно, что необходима более ресурсоёмкая ПЛИС, выбор пал на конкретную плату после переписки с продавцом, который предоставил проекты VIvado и Vitis для платы, а также ещё много полезной информации в архиве.

Чтобы построить график созвездия, необходимо как-то получить из платы данные. Здесь можно воспользоваться готовым функционалом передачи данных из модуля axi_ad9361 в процессорную систему через библиотечный элемент util_ad9361_adc_pack.

Рис. 11. Необходимые шаги для добавления нового исходного кода
Рис. 11. Необходимые шаги для добавления нового исходного кода
Рис. 12. Необходимые шаги для добавления нового исходного кода
Рис. 12. Необходимые шаги для добавления нового исходного кода
Рис. 13. Необходимые шаги для добавления нового исходного кода
Рис. 13. Необходимые шаги для добавления нового исходного кода

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

Рис. 14. Модуль передатчика на блок-схеме.
Рис. 14. Модуль передатчика на блок-схеме.
Рис. 15. Тактовый сигнал передатчика и приёмника.
Рис. 15. Тактовый сигнал передатчика и приёмника.

Входы модулей передатчика и приёмника clk можно подключить к 50 МГц. А вообще, в Matlab & Simulink можно получить максимальную тактовую частоту, на которой способен работать модуль. Вход reset подключается через логическое НЕ к sys_rstgen/peripheral_aresetn. clk_enable можно просто подключить к константной единице. Выход ce_out можно пока оставить неподключённым. Выход QPSK_RRC_Shaped_re подключаем к входу HDLRx_0/dataIn_re, а QPSK_RRC_Shaped_im к HDLRx_0/dataIn_im. Чтобы подключиться к util_ad9361_adc_pack из HDLRx, необходимо вывести I/Q отсчёты. Можно поэкспериментировать и вывести отсчёты после грубой и тонкой частотной синхронизации или после символьной синхронизации. Всё как в модели Simulink, только теперь на реальном железе. Здесь выведем отсчёты уже после символьной синхронизации. Для этого, откроем файл HDLRx.v и добавим в модуль три выхода.

output  testValid;
output  signed [15:0] test_data_re;
output  signed [15:0] test_data_im;

Символьная синхронизация выполняется в модуле Timing_Recovery, поэтому подключим новые выходы к этим данным

assign testValid = Timing_Recovery_out1;
assign test_data_re = Timing_Recovery_out2_re;
assign test_data_im = Timing_Recovery_out2_im;

Тогда у модуля HDLRx появятся новые выходы. Модуль стал немного безобразным из-за того, что надо поднастроить Vivado, но это не сделано. Хотя на проверку это никак не повлияет.

Рис. 16. Модуль HDLRx с дополнительными отладочными выходами
Рис. 16. Модуль HDLRx с дополнительными отладочными выходами
Рис. 17. Библиотечный элемент, который позволит получить тестовые данные в процессорной системе.
Рис. 17. Библиотечный элемент, который позволит получить тестовые данные в процессорной системе.

Можно подключиться и к первому каналу (нулевому, если с нуля считать - прим. автора), это пока не имеет значения. Теперь важно уделить внимание файлу .xdc, иначе тайминги не позволят ничего сделать.

Рис. 18. Результат имплементации с нерабочими таймингами.
Рис. 18. Результат имплементации с нерабочими таймингами.
Рис. 19. Важная строка
Рис. 19. Важная строка

Почему так получилось? В файле system_constr.xdc есть строчка create_clock -name rx_clk -period 16.27 [get_ports rx_clk_in]. У микросхемы ad9361 есть одноимённый выход, через который на вход ПЛИС поступает тактовый сигнал для шины данных Rx. Чтобы обеспечить пропускную способность 56 МГц, скорость передачи данных I/Q на AD9361 должна быть установлена на максимальное значение — 61,44 млн отсчётов в секунду. Для работы в режиме 2T2R сигнал DATA_CLK должен иметь частоту в 4 раза выше частоты I/Q, то есть 245,76 МГц. В этом случае мы имеем дело с PlutoSDR, который использует ad9363 и по счастливой случайности этой микросхемой можно пользоваться так же как и ad9361 (два канала, диапазон и многое другое). Позже мы заглянем в проект именно для ad9361 и вообще увидим там в этой строчке значение 4 нс, как раз для частоты 250 МГц. А пока что всё рассчитано для максимальной частоты 61.44 МГц

\frac{1}{0,00000001627}=61 462 814,996926859250153657037492

Так как сигнал входной для ПЛИС, Vivado ничего не знает о нём, если ей не указать явно, для какой частоты делать имплементацию. Мы же собираемся работать на частоте всего 2.45 МГц, поэтому

2.45*4=9.8\frac{1}{9800000}=1,0204081632653061224489795918367e-7

Или 102,04081632653061224489795918367 нс. Лучше округлить немного, чтобы был запас, поэтому укажем в той строке значение ровно 100 нс. Таким образом, всё будет рассчитано для частоты 10 МГц.

create_clock -name rx_clk -period  100.00 [get_ports rx_clk_in]

После имплементации можно увидеть допустимые значения таймингов.

Рис. 20. Допустимые значения таймингов
Рис. 20. Допустимые значения таймингов

Дальше всё как в прошлой статье, генерация bitstream, экспорт hardware и создаём приложение из .xsa файла. Импортируем исходный код. Пока что шаг настройки цифрового интерфейса придётся пропустить, поэтому в структуре AD9361_InitParam default_init_param

/* Digital Interface Control */
2,		//digital_interface_tune_skip_mode *** adi,digital-interface-tune-skip-mode

Здесь хоть к первому каналу подключайте HDL передатчик, хоть ко второму, всё равно придётся делать мультиплексор данных, чтобы этот тюнинг работал. (Об этом вскользь упоминается в переводе, который я опубликовал под названием «Простой процессор основной полосы частот для радиочастотных приёмопередатчиков» — прим. автора).

Рис. 21. Результат программирования платы с включённым digital_interface_tune
Рис. 21. Результат программирования платы с включённым digital_interface_tune

Теперь, чтобы получить данные и не усложнять, просто дополним секцию под #ifdef TDD_SWITCH_STATE_EXAMPLE. Настраивается частота дискретизации и ширина полосы на 2.45 МГц. Микросхема переводится на приём в режиме TDD. И 4 раза считывается по 4096 отсчётов. Формат I0 -> Q0 -> I1 -> Q1. мы подключены к нулевому каналу, поэтому забираем только необходимые I0 и Q0.

Скрытый текст
#ifdef TDD_SWITCH_STATE_EXAMPLE
	uint32_t ensm_mode;
	struct no_os_gpio_init_param  gpio_init = {
		.platform_ops = GPIO_OPS,
		.extra = GPIO_PARAM
	};
	struct no_os_gpio_desc 	*gpio_enable_pin;
	struct no_os_gpio_desc 	*gpio_txnrx_pin;

	status = ad9361_set_rx_sampling_freq(ad9361_phy, 2.45E+6);
	if(status != 0){
		printf("set_rx_sampling_freq: ERROR!\n");
	}
	uint32_t sampling_freq_hz_rx=777;
	ad9361_get_rx_sampling_freq(ad9361_phy, &sampling_freq_hz_rx);
	printf("sampling_freq_hz_rx: %lu\n", sampling_freq_hz_rx);

	status = ad9361_set_rx_rf_bandwidth(ad9361_phy, 2.45E+6);
	if(status != 0){
		printf("set_rx_rf_bandwidth: ERROR!\n");
	}

	uint32_t bandwidth_hz_rx=77;
	ad9361_get_rx_rf_bandwidth(ad9361_phy, &bandwidth_hz_rx);
	printf("bandwidth_hz_rx: %lu\n", bandwidth_hz_rx);

	uint8_t flag = 1;

	if (!ad9361_phy->pdata->fdd) {
		if (ad9361_phy->pdata->ensm_pin_ctrl) {
			gpio_init.number = GPIO_ENABLE_PIN;
			status = no_os_gpio_get(&gpio_enable_pin, &gpio_init);
			if (status != 0) {
				printf("no_os_gpio_get() error: %"PRIi32"\n", status);
				return status;
			}
			no_os_gpio_direction_output(gpio_enable_pin, 1);
			gpio_init.number = GPIO_TXNRX_PIN;
			status = no_os_gpio_get(&gpio_txnrx_pin, &gpio_init);
			if (status != 0) {
				printf("no_os_gpio_get() error: %"PRIi32"\n", status);
				return status;
			}
			no_os_gpio_direction_output(gpio_txnrx_pin, 0);
			no_os_udelay(10);
			ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
			printf("TXNRX control - Alert: %s\n",
			       ensm_mode == ENSM_MODE_ALERT ? "OK" : "Error");
			no_os_mdelay(1000);

			if (ad9361_phy->pdata->ensm_pin_pulse_mode) {
				while(1) {
					no_os_gpio_set_value(gpio_txnrx_pin, 0);
					no_os_udelay(10);
					no_os_gpio_set_value(gpio_enable_pin, 1);
					no_os_udelay(10);
					no_os_gpio_set_value(gpio_enable_pin, 0);
					ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
					printf("TXNRX Pulse control - RX: %s\n",
					       ensm_mode == ENSM_MODE_RX ? "OK" : "Error");
					no_os_mdelay(1000);

					no_os_gpio_set_value(gpio_enable_pin, 1);
					no_os_udelay(10);
					no_os_gpio_set_value(gpio_enable_pin, 0);
					ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
					printf("TXNRX Pulse control - Alert: %s\n",
					       ensm_mode == ENSM_MODE_ALERT ? "OK" : "Error");
					no_os_mdelay(1000);

					no_os_gpio_set_value(gpio_txnrx_pin, 1);
					no_os_udelay(10);
					no_os_gpio_set_value(gpio_enable_pin, 1);
					no_os_udelay(10);
					no_os_gpio_set_value(gpio_enable_pin, 0);
					ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
					printf("TXNRX Pulse control - TX: %s\n",
					       ensm_mode == ENSM_MODE_TX ? "OK" : "Error");
					no_os_mdelay(1000);

					no_os_gpio_set_value(gpio_enable_pin, 1);
					no_os_udelay(10);
					no_os_gpio_set_value(gpio_enable_pin, 0);
					ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
					printf("TXNRX Pulse control - Alert: %s\n",
					       ensm_mode == ENSM_MODE_ALERT ? "OK" : "Error");
					no_os_mdelay(1000);
				}
			} else {
				while(1) {
					no_os_gpio_set_value(gpio_txnrx_pin, 0);
					no_os_udelay(10);
					no_os_gpio_set_value(gpio_enable_pin, 1);
					no_os_udelay(10);
					ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
					printf("TXNRX control - RX: %s\n",
					       ensm_mode == ENSM_MODE_RX ? "OK" : "Error");
					no_os_mdelay(1000);

					no_os_gpio_set_value(gpio_enable_pin, 0);
					no_os_udelay(10);
					ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
					printf("TXNRX control - Alert: %s\n",
					       ensm_mode == ENSM_MODE_ALERT ? "OK" : "Error");
					no_os_mdelay(1000);

					no_os_gpio_set_value(gpio_txnrx_pin, 1);
					no_os_udelay(10);
					no_os_gpio_set_value(gpio_enable_pin, 1);
					no_os_udelay(10);
					ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
					printf("TXNRX control - TX: %s\n",
					       ensm_mode == ENSM_MODE_TX ? "OK" : "Error");
					no_os_mdelay(1000);

					no_os_gpio_set_value(gpio_enable_pin, 0);
					no_os_udelay(10);
					ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
					printf("TXNRX control - Alert: %s\n",
					       ensm_mode == ENSM_MODE_ALERT ? "OK" : "Error");
					no_os_mdelay(1000);
				}
			}
		} else {
			uint8_t j = 4;
			while(j--) {
				if(flag--){
					ad9361_set_en_state_machine_mode(ad9361_phy, ENSM_MODE_RX);
					ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
					printf("SPI control - RX: %s\n", ensm_mode == ENSM_MODE_RX ? "OK" : "Error");
					no_os_mdelay(1000);
				}
				struct rf_rssi ch0;
				struct rf_rssi ch1;

				ad9361_get_rx_rssi(ad9361_phy, 0, &ch0);
				ad9361_get_rx_rssi(ad9361_phy, 1, &ch1);
				printf("ch0: %lu %lu %lu %ld %d\n", ch0.ant, ch0.symbol, ch0.preamble, ch0.multiplier, ch0.duration);
				printf("ch1: %lu %lu %lu %ld %d\n", ch1.ant, ch1.symbol, ch1.preamble, ch1.multiplier, ch1.duration);

				/* Read the data from the ADC DMA. */
				axi_dmac_transfer_start(rx_dmac, &read_transfer);

				/* Wait until transfer finishes */
				status = axi_dmac_transfer_wait_completion(rx_dmac, 500);
				if(status < 0)
				    return status;

				Xil_DCacheInvalidateRange((uintptr_t)adc_buffer, sizeof(adc_buffer));

				//----------------------------------------------------------------------------------------------------
				for(int i = 0; i < 4096; i++){
				    if(i%4==0){
				        int16_t sample = (int16_t)adc_buffer[i];
				        printf("%d %d\n", sample, (int16_t)adc_buffer[i+1]);
				    }
				}
				//----------------------------------------------------------------------------------------------------
			#if 0
				ad9361_set_en_state_machine_mode(ad9361_phy, ENSM_MODE_ALERT);
				ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
				printf("SPI control - Alert: %s\n",
				       ensm_mode == ENSM_MODE_ALERT ? "OK" : "Error");
				no_os_mdelay(1000);
			#endif
			#if 0
				ad9361_set_en_state_machine_mode(ad9361_phy, ENSM_MODE_TX);
				ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
				printf("SPI control - TX: %s\n",
				       ensm_mode == ENSM_MODE_TX ? "OK" : "Error");
				no_os_mdelay(1000);
			#endif
			#if 0
				ad9361_set_en_state_machine_mode(ad9361_phy, ENSM_MODE_ALERT);
				ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode);
				printf("SPI control - Alert: %s\n",
				       ensm_mode == ENSM_MODE_ALERT ? "OK" : "Error");
				no_os_mdelay(1000);
			#endif
			}
		}
	}
#endif

Выше под спойлером исходный код. В самом начале используются функции No-OS от Analog Devices, хорошо описанные в файле ad9361_api, они простые и понятные. С помощью них в No-OS меняются параметры несущей частоты, частоты дискретизации, ширины полосы и ещё много чего интересного, обязательно посмотрите. Для нас пока главное — установить частоту дискретизации равной 2.45 МГц и заодно ширину полосы с таким же значением. В статье используется переключение микросхемы на передачу и приём в режиме tdd, поэтому весь наш код в самом конце. Цикл while выполняется 4 раза. Теперь необходимо только запрограммировать плату этой прошивкой, внимательно следя за отладочной информацией в терминале.

Рис. 22. Отладочная информация от ad9361
Рис. 22. Отладочная информация от ad9361

Если всё сделано правильно, то в терминале будет 4 группы I/Q отсчётов. Чтобы из них простроить график созвездия, можно воспользоваться простой программой на Python.

import matplotlib.pyplot as plt

# Чтение данных из файла
with open('../res/test23/data3.txt', 'r') as f:
    lines = f.readlines()

I = []
Q = []

for line in lines:
    line = line.strip()
    if line:
        i_val, q_val = map(int, line.split())
        I.append(i_val)
        Q.append(q_val)

# Построение созвездия
plt.figure(figsize=(8, 8))
plt.scatter(I, Q, s=1, alpha=0.6, c='blue')
plt.axhline(0, color='black', linewidth=0.5)
plt.axvline(0, color='black', linewidth=0.5)
plt.grid(True, alpha=0.3)
plt.xlabel('I (In-phase)')
plt.ylabel('Q (Quadrature)')
plt.title('QPSK Constellation Diagram')
plt.axis('equal')
plt.tight_layout()
plt.show()

Запустив программу, получится вполне себе удовлетворительный график

Рис. 23. График созвездия из данных с платы
Рис. 23. График созвездия из данных с платы

Подведём промежуточный итог. Получилось реализовать HDL QPSK передатчик и приёмник и использовать их в плате PlutoSDR. Судя по графику созвездия, мы на верном пути и можно переходить к передаче данных через радиоканал, чтобы получить такой же график созвездия. Или можно продолжить отладку и реализовать передачу декодированных данных из PL в PS и написать код под процессор, для вывода переданной информации и статистики (как в примере Matlab) в последовательный порт. В любом случае ещё предстоит много работы. В конечном счёте хотелось бы передавать не жёстко зашитые в HDL данные, а информацию, которая поступает в последовательный порт или ещё какую-нибудь полезную информацию (изображения?..).

Рис. 24. Микросхемы на новой плате
Рис. 24. Микросхемы на новой плате

Для приёмника пришлось использовать другую плату. Реализация HDLRx никак не хотела помещаться в Z010 и по старой доброй традиции был выбран чип с большим количеством ресурсов на Z020. Выбор пал конкретно на эту плату, потому что продавец с известного китайского маркетплейса любезно предоставил проект с исходными кодами. Проект был синтезирован, имплементирован и сгенерирован bitstream, сделано простое приложение и всё это правильно работает.

Спасибо.

С. Н.