Anvelina PRO III v2.2.14: Полная хирургия DSP-тракта
Anvelina PRO III v2.2.14: Полная хирургия DSP-тракта
Введение
После выпуска v2.2.13 я провел глубокий аудит DSP-тракта трансивера. Цель была простая: убедиться, что математика корректна, нет скрытых багов, а тайминги закрываются с запасом. То, что мы нашли, заставило меня переписать критические узлы CIC-фильтров, FIR-дециматоров и wideband-потока.
В первых статьях я уже частично правил DSP-цепочки, но тракт оказался намного сложнее, чем казалось. Потребовалось переписать 5 модулей и удалить 1 неиспользуемый:
Переписанные файлы:
wideband_smooth.v— исправлен баг saturationOrion.v— обновлены подключенияPolyphase_FIR/firx2r2.v— расширены аккумуляторы, добавлена saturationcic.v— добавлена saturation на выходахreceiver2.v— добавлены pipeline-регистры для CDC
Удалённый файл:
cic_comb.v(модульcic_comp) — мёртвый код
Это была, пожалуй, самая сложная работа за всю историю проекта. Тракт DSP — это каскадная система, где ошибка в одном модуле проявляется совсем в другом месте. Нужен был полный аудит от АЦП до ЦАП.
Эта статья — подробный разбор того, где прячутся «призраки» в DSP-цепочках и как их поймать.
1. Анатомия Wrap-around: Почему +FS превращается в -FS
Проблема в wideband_smooth.v
В модуле сглаживания wideband-потока был найден критический баг. Код выглядел так:
// БЫЛО (неправильно)
wire signed [15:0] div_result = scaled_shifted[31:16]; // Усечение до 16 бит
wire signed [15:0] saturated = (div_result > 16'sd32767) ? 16'sd32767 : ...
Что было не так: Сравнение шло по усечённому 16-битному значению, хотя scaled_shifted был 36-битным. Когда сигнал достигал полного масштаба, старшие биты отбрасывались, и компаратор «не видел» переполнения. Результат: wrap-around (заворот фазы).
Исправление:
// СТАЛО (правильно)
localparam signed [35:0] MAX_36 = 36'sd34359738367;
localparam signed [35:0] MIN_36 = -36'sd34359738368;
wire signed [35:0] clamped = (scaled_shifted > MAX_36) ? MAX_36 :
(scaled_shifted < MIN_36) ? MIN_36 :
scaled_shifted;
wire signed [15:0] div_result = clamped[31:16];
Теперь saturation работает по полному 36-битному слову, и wrap-around исключён.
Формула роста разрядности CIC
Почему мы вообще работаем с 36 битами? Это следует из фундаментальной формулы CIC-фильтров:
Bmax = Bin + ⌈N · log₂(R)⌉
Где:
- Bin — входная разрядность (22 бита после CORDIC)
- N — количество каскадов CIC (3 для первого CIC)
- R — коэффициент децимации (до 2560 для 48 кГц)
Для максимального режима:
Bmax = 22 + ⌈3 · log₂(2560)⌉ = 22 + ⌈3 · 11.32⌉ = 22 + 34 = 56 бит
Но на практике мы используем 40-битные аккумуляторы, потому что:
- Децимация 2560× применяется редко (только для 48 кГц)
- Реальные сигналы редко достигают полного масштаба одновременно на всех каскадах
- 40 бит дают достаточный headroom (~18 бит запаса)
2. Отсутствие Headroom в firx2r2.v
Проблема
Финальный полифазный FIR-фильтр firx2r2.v имел аккумуляторы на 24 бита без защиты от переполнения. При сильных блокерах (мощных соседних станциях) аккумуляторы переполнялись, вызывая:
- Искажения в аудио
- Артефакты на водопаде
- Нестабильность AGC
Исправление
Расширили аккумуляторы до 26 бит (добавили 2 бита headroom):
// БЫЛО
reg signed [23:0] Racc, Iacc;
// СТАЛО
reg signed [25:0] Racc, Iacc; // +2 бита для защиты от переполнения
Добавили Hard Saturation на выходе:
localparam signed [23:0] MAX_24 = 24'sd8388607;
localparam signed [23:0] MIN_24 = -24'sd8388608;
wire signed [25:0] y_real_raw = Racc;
wire signed [23:0] y_real = (y_real_raw > MAX_24) ? MAX_24 :
(y_real_raw < MIN_24) ? MIN_24 :
y_real_raw[23:0];
Теперь даже при экстремальных перегрузках выход остаётся в допустимом диапазоне.
3. Saturation на выходах CIC
Проблема
Выходы CIC-фильтров (cic.v) использовали simple round-half-up без защиты от переполнения. При полномасштабных сигналах округление могло вызвать wrap-around.
Исправление
Добавили saturation-константы и логику ограничения:
localparam signed [23:0] MAX_OUT = 24'sd8388607;
localparam signed [23:0] MIN_OUT = -24'sd8388608;
// Округление + saturation
wire signed [24:0] rounded = integrator_out + 24'sd1; // round-half-up
wire signed [23:0] output = (rounded > MAX_OUT) ? MAX_OUT :
(rounded < MIN_OUT) ? MIN_OUT :
rounded[23:0];
4. CDC-кошмар в receiver2.v
Проблема
Сигнал sample_rate приходит от ПК через Ethernet (другой тактовый домен). Он использовался напрямую в case-конструкции для управления децимацией CIC:
// БЫЛО (CDC-нарушение)
always @(*) begin
case (sample_rate)
16'd48: decimation = 2560;
16'd96: decimation = 1280;
16'd192: decimation = 640;
// ...
endcase
end
Это создавало длинный комбинационный путь от асинхронного sample_rate к логике управления CIC. На частоте 122.88 МГц (период 8.14 нс) Quartus не успевал закрыть тайминги.
Исправление
Добавили pipeline-регистры для разрыва критического пути:
// СТАЛО (правильный CDC)
reg [15:0] rate0, rate1;
always @(posedge clock) begin
rate0 <= sample_rate;
rate1 <= rate0;
end
// Используем rate1 (зарегистрированный сигнал)
always @(*) begin
case (rate1)
16'd48: decimation = 2560;
16'd96: decimation = 1280;
16'd192: decimation = 640;
// ...
endcase
end
Теперь sample_rate проходит через два регистра синхронизации перед использованием. Это:
- Устраняет метастабильность
- Разрывает длинный комбинационный путь
- Даёт Fitter’у Quartus свободу для оптимизации
5. TPDF Dither: Эксперимент с белым шумом
Проблема «травы» квантования
Без dither ошибка квантования АЦП коррелирована с входным сигналом. На панораме это проявляется как:
- «Трава» (ложные всплески) вокруг мощных станций
- Пульсации на шумовой полке
- Маскировка слабых DX-сигналов
Решение: TPDF (Triangular Probability Density Function)
В качестве эксперимента мы добавили генератор псевдошума на основе LFSR:
reg [15:0] lfsr_wb;
always @(posedge C122_clk or negedge C122_run) begin
if (!C122_run)
lfsr_wb <= 16'hACE1;
else
lfsr_wb <= {lfsr_wb[14:0], lfsr_wb[15] ^ lfsr_wb[13] ^
lfsr_wb[12] ^ lfsr_wb[10]};
end
// TPDF = сумма двух равномерных распределений (zero-mean)
wire signed [4:0] dither_wb = $signed({1'b0, lfsr_wb[3:0]}) +
$signed({1'b0, lfsr_wb[7:4]});
Почему TPDF, а не равномерный шум?
- Равномерный шум (RPDF) убирает корреляцию, но оставляет гармоники
- TPDF (треугольное распределение) полностью линеаризует характеристику квантования
- Математическое ожидание = 0 (нет DC bias)
Добавляем dither к сигналу с защитой от переполнения:
wire signed [16:0] sum_adc0 = temp_ADC[0] + {{12{dither_wb[4]}}, dither_wb};
wire signed [15:0] dithered_ADC0 = (sum_adc0 > MAX_16) ? MAX_16 :
(sum_adc0 < MIN_16) ? MIN_16 :
sum_adc0[15:0];
Результат эксперимента
Честно говоря, большой разницы не видно. Шум становится чуть более однородным, но изменения минимальны. В полосе 1.5 МГц эффект чуть заметнее, но всё равно нет разительной разницы.
Почему так?
- В RX-тракте dither усредняется CIC-фильтром (эффект сильный)
- В wideband-потоке нет CIC — dither остаётся в сигнале
- Для заметного эффекта нужно усреднение FFT на стороне ПК
Это был интересный эксперимент, но для production-версии мы оставили его как опциональное улучшение. Для старых ревизий плат (с менее мощным питанием ядра) рекомендуется версия без dither для максимальной стабильности.
6. Тайминги и режимы компиляции
Performance vs Power (High Effort)
В v2.2.13 мы использовали режим Performance — агрессивная оптимизация таймингов любой ценой. Quartus перестраивал размещение логики, чтобы закрыть критические пути, но это ухудшало качество звука.
Для v2.2.14 мы переключились на Power (High Effort) — режим качественного размещения. Здесь приоритет не «тайминги любой ценой», а оптимальное размещение логики с учётом:
- Близости DSP-блоков к мультипликаторам
- Минимизации crosstalk между цифровыми и аналоговыми доменами
- Предсказуемого routing
Результат превзошёл ожидания:
- Звук стал качественнее, чем в Performance-режиме
- Тайминги всё ещё закрываются с хорошим запасом
- Размещение логики более естественное
Это важный урок для SDR-разработчиков: не всегда лучшая оптимизация таймингов = лучшее качество сигнала. Иногда «достаточно хорошие» тайминги с качественным размещением дают лучший результат.
7. Итоговые метрики v2.2.14 — Разбор для большой аудитории
Многие спрашивают: «Что означают все эти цифры? Почему это важно?» Давайте разберём подробно.
Что такое тайминги и почему они важны?
FPGA работает на частоте 122.88 МГц. Это значит, что каждый такт длится 8.138 наносекунды. За это время сигнал должен пройти от одного регистра до другого. Если сигнал не успевает — происходит сбой, и трансивер работает нестабильно или вообще не работает.
Setup Slack — это запас времени, который остаётся после того, как сигнал дошёл до следующего регистра. Если slack отрицательный — сигнал не успевает, это критическая ошибка.
Hold Slack — это минимальное время, которое сигнал должен «продержаться» после тактового импульса. Если hold slack отрицательный — сигнал меняется слишком быстро, и следующий регистр может захватить неправильное значение.
Таблица таймингов по всем температурным углам
| Угол (температура/напряжение) | Worst Setup Slack | Worst Hold Slack | Recovery | Removal |
|---|---|---|---|---|
| Slow 1200mV 100C (самый тяжёлый: горячий кристалл, низкое напряжение) | +0.912 ns | +0.353 ns | +1.625 ns | +2.515 ns |
| Slow 1200mV -40C (холодный кристалл, низкое напряжение) | +1.692 ns | +0.319 ns | +2.514 ns | +2.151 ns |
| Fast 1200mV -40C (холодный кристалл, высокое напряжение) | +2.951 ns | +0.138 ns | +4.749 ns | +1.150 ns |
Что означают эти цифры простыми словами?
Setup Slack +0.912 ns на самом тяжёлом углу:
Представьте, что у вас есть 8.138 наносекунды, чтобы добежать от одного здания до другого. Вы добежали за 7.226 наносекунды. У вас осталось 0.912 наносекунды запаса — вы можете даже остановиться перевести дух. Это значит, что даже если кристалл FPGA нагреется до +100°C (что бывает в жаркую погоду при полной нагрузке) и напряжение питания просядет до минимума, трансивер всё равно будет работать стабильно.
Hold Slack +0.353 ns:
Это значит, что сигнал «держится» достаточно долго после тактового импульса. Нет «гонок» — ситуаций, когда сигнал меняется слишком быстро, и следующий регистр захватывает неправильное значение. Если бы hold slack был отрицательным, трансивер работал бы со случайными сбоями, которые невозможно было бы предсказать.
TNS (Total Negative Slack) = 0.000 во всех углах:
Это самый важный показатель. TNS — это сумма всех отрицательных slack’ов в проекте. Если TNS = 0, значит, ни одного пути в проекте не нарушает тайминги. Это production-ready прошивка, которую можно смело использовать.
Критические пути (PHY_RX_CLOCK)
Самый нагруженный тактовый домен в проекте — это PHY_RX_CLOCK (122.88 МГц, приём данных от АЦП). Вот его метрики:
| Параметр | Значение | Что это значит |
|---|---|---|
| Setup Slack | +0.912 ns | Запас 11.2% от периода. Сигнал успевает дойти с комфортом. |
| Hold Slack | +0.353 ns | Нет гонок сигналов. Регистры захватывают правильные значения. |
| Pulse Width | +3.472 ns | Тактовые импульсы стабильны, не слишком короткие. |
Метастабильность — что это и почему важно?
Метастабильность — это явление, когда сигнал переходит из одного тактового домена в другой (например, от Ethernet-контроллера, работающего на 125 МГц, к нашему DSP-тракту на 122.88 МГц). Если не использовать правильные синхронизаторы, сигнал может попасть в «неопределённое состояние» — ни 0, ни 1. Это вызывает случайные сбои, которые очень сложно отладить.
Результаты анализа метастабильности:
- 825 цепочек синхронизации найдено в проекте — это правильно, все переходы между доменами защищены
- MTBF = 1×10⁹ лет (миллиард лет) — среднее время между сбоями из-за метастабильности. Это практически исключает проблему
- Кратчайшая цепочка синхронизации: 2 регистра — это минимально необходимое количество для надёжной работы
- Worst Case Settling Time: 9.160 нс — время, за которое сигнал гарантированно стабилизируется после перехода между доменами
Простыми словами: если бы вы включили трансивер и оставили его работать на 1 миллиард лет, в среднем только один раз за это время мог бы произойти сбой из-за метастабильности. На практике это означает, что проблема полностью решена.
Ресурсы FPGA
| Ресурс | Использование | % от доступного |
|---|---|---|
| Total logic elements (логические ячейки) | 97 612 | 86% |
| Dedicated logic registers (регистры) | 76 082 | — |
| Embedded Multiplier 9-bit (DSP-блоки) | 298 | 58% |
| Total memory bits (память) | 1 229 728 | 31% |
Мы используем 86% логики. Это плотная, но комфортная компоновка, оставляющая роутеру Quartus достаточно свободы для построения оптимальных связей без паразитных наводок.
Скачать прошивку для Анвелина Про3 можно тут: https://eu2av.net/download/file.php?id=2061
Заключение и Благодарности
Версия v2.2.14 — это результат колоссальной работы по аудиту DSP-тракта. Главный урок: в DSP мелочей не бывает. Усечение разрядности, отсутствие saturation, CDC-нарушения — всё это «призраки», которые могут жить в коде годами. Тракт DSP — это каскадная система, и нужен полный аудит от входа до выхода.
Отдельная и огромная благодарность нашему сообществу за поддержку! После выхода прошлых версий пришло множество писем с благодарностями, отчётами о тестировании и предложениями. Это невероятно мотивирует продолжать работу. Когда ты видишь, что твоя работа реально помогает людям работать в эфире, это лучшая награда.
Особая благодарность Рику (N1GP) — все обновления оперативно интегрируются в линейку трансиверов Anan. Благодаря Рику улучшения Anvelina PRO III становятся доступными широкому кругу радиолюбителей, и мы видим большую и очень тёплую обратную связь от пользователей по всему миру.
Систематический аудит + правильные инженерные практики + обратная связь от сообщества = надёжный трансивер, который радует пользователей.
Юрий, EU2AV
Разработчик SDR, радиолюбитель
eu2av.com | OpenHPSDR Community Спасибо сообществу за тестирование и обратную связь. 73!
Юрий, здравствуйте!
Сегодня активно поработал на последней прошивке.
Проверил в том числе и на линуксе PiHpSDR v.3 , это вообще песня!
Ребята мне транслировали запись сигнала, ну здорово!
Я Вас поздравляю, все работает очень достойно!
Спасибо!