惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
博客园 - 【当耐特】
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Announcements
Recent Announcements
D
Docker
GbyAI
GbyAI
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
The Cloudflare Blog
雷峰网
雷峰网
A
About on SuperTechFans
小众软件
小众软件
博客园 - Franky
博客园 - 聂微东
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
C
Check Point Blog
MongoDB | Blog
MongoDB | Blog
G
Google Developers Blog
Microsoft Azure Blog
Microsoft Azure Blog
U
Unit 42
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
V2EX
Engineering at Meta
Engineering at Meta
宝玉的分享
宝玉的分享
aimingoo的专栏
aimingoo的专栏
量子位
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
罗磊的独立博客
Martin Fowler
Martin Fowler
D
DataBreaches.Net
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Secure Thoughts
Project Zero
Project Zero
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tailwind CSS Blog
S
Schneier on Security
Blog — PlanetScale
Blog — PlanetScale
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
Security Latest
Security Latest
NISL@THU
NISL@THU
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
J
Java Code Geeks

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
QPSK HDL приёмопередатчик
Сергей Новиков · 2026-05-31 · via Все публикации подряд на Хабре

В этой статье описано, как создать 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, сделано простое приложение и всё это правильно работает.

Спасибо.

С. Н.