Разделы про роли и идентификацию устройства, прошивку STM (сборка/заливка
через ST-Link по SWD, подключения по ролям, проверка роли), кадр USB CDC,
сетевой слой (UDP, seq/новая сессия, HMAC, WireGuard), многоуровневый
failsafe и запуск моста.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Переносит управление с STM-передатчика на STM-приёмник через локальную
сеть/интернет двумя экземплярами одной программы.
- tools/link_proto.py: зеркало link_protocol.h на Python (кадрирование,
CRC16-CCITT, (де)сериализация CONTROL/IDENT, инкрементальный парсер с
ресинхронизацией) + --selftest с тест-векторами.
- tools/bridge.py: роль tx (USB->UDP) и rx (UDP->USB) из одного флага;
поиск порта по product string и handshake HELLO/IDENT; UDP с seq и
anti-replay (распознавание новой сессии при рестарте источника);
сетевой failsafe при тишине; опциональная HMAC-аутентификация пакетов
и фильтр источника.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Прошивка получает две роли, выбираемые compile-time env; общий код
собирается в оба env, специфичный — через build_src_filter, без мёртвого
кода неактивной роли. Роль публикуется в USB product string
(CART-TX/CART-RX), по которой ПК находит нужный порт.
- app_tx.cpp: как раньше формирует SBUS на локальный провод и
дополнительно шлёт те же ch[16] по USB кадром CONTROL; отвечает на
HELLO кадром IDENT. Старый проводной функционал не изменён.
- app_rx.cpp: принимает CONTROL по USB и гонит SBUS на провод строго
каждые 50 мс из последнего состояния. FAILSAFE: нет свежего CONTROL
дольше 150 мс -> нейтраль с флагом failsafe (срабатывает и при обрыве
USB/зависании ПК); сетевой флаг failsafe транслируется в бит SBUS.
- platformio.ini: env cart_tx/cart_rx, макросы роли и USB_PRODUCT_STRING.
- удалён монолитный main.cpp (логика разнесена по модулям).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Единый слой связи пульт<->ПК<->тележка, одинаковый на обеих ролях и
зеркалимый на Python.
- link_protocol.h: единственный источник истины — кадрирование
(SYNC/VER/TYPE/LEN/PAYLOAD/CRC16-CCITT), типы CONTROL/IDENT/HELLO/LOG,
layout полезных нагрузок, роли, тайминги failsafe.
- usb_link.{h,cpp}: CRC16, сериализация CONTROL/IDENT, отправка кадров и
стейт-машина приёма с ресинхронизацией по синхрослову и проверкой CRC.
- device_id.h: чтение 96-битного UID чипа для IDENT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Подготовка к ролям TX/RX: выделяю из монолитного main.cpp
переиспользуемые части без изменения поведения.
- sbus.{h,cpp}: упаковка 25-байтового SBUS-кадра, инициализация и
вывод инвертированного SBUS на USART1 (PA9) — единственный владелец
провода к драйверу, общий для будущих ролей.
- stick.{h,cpp}: калибровка стика, нормализация осей и профиль
газа/поворота, формирование ch[16]. Неиспользованные в прежнем loop
expo()/toSbus() и мёртвые константы не переносятся.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified on the bench that the modified master bitstream excites in every stage
and the RF switch routes: 16 traces per sweep, 62 dB between the two switch
positions of one master port, 9.9k measurements/s. Adds the built master image,
two run profiles, a verification script and librevna_protocol.md, and drops the
stale prototype driver copy that the firmware tree still carried.
PB2 now drives the switch on master port 1 and PB6 the one on port 2, both
updated in a single BSRR write so they move in the same cycle. The CDC report
gained a gap histogram, which showed the point boundary sits at 70-79 us against
62 us between stages, so the resync threshold only fires on the longer
inter-sweep pauses; the misleading "stages per point" counter is renamed to
match what it actually counts.
Ограничение MCU_MOSI OFFSET hold маргинально уже в стоке: запас +0.458 нс.
Поэтому любое изменение нетлиста переставляет размещение и может его опрокинуть.
A/B в ISE: сток - Timing Score 0, версия на маске при cost table 1 - 5480.
Cost table 2 даёт Timing Score 0, причём запас по hold на MCU_MOSI больше
стокового: +0.642 против +0.458 нс. Значение зафиксировано в VNA.xise одной
строкой, иначе следующая сборка снова словит нарушение.
Ресурсы финальной сборки: 2302 LUT против 2189 у стока (40% против 38% ёмкости
xc6slx9). Промежуточные 2182, которые я приводил раньше, были из сборки с
cost table 1 - её перебор потом переделал, и упаковка map изменилась.
Битстрим положен как top_master_per_point_switching.bin, а не поверх
top_master.bin: последний - одна из двух копий невосстановимого битстрима, из
которого собран текущий combined.vnafw.
Документ приведён в согласованное состояние: таблица контракта и описание
Sweep.vhd отставали от версии на маске.
Собрано и проверено на машине сборки: ISE 14.7 в виртуалке, xtclsh в батч-режиме.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Контрольная сборка стока в ISE показала, что первая версия правки внесла
нарушение тайминга: сток даёт Timing Score 0, а она — 1880 (hold на входе
MCU_MOSI). Причина — shift_right с переменным сдвигом синтезируется в barrel
shifter, а вызывался он четырежды: +142 LUT при +3 регистрах, из-за чего
поехало размещение и пострадала SPI-шина, по которой MCU пишет регистры FPGA.
Сбой там означал бы неверную конфигурацию развёртки.
Теперь младшие биты счётчика стадий просто маскируются, а маска декодируется
из SUBSTAGE_BITS тремя вентилями. Итог: 2182 LUT против 2189 у стока, то есть
логика стала дешевле стоковой. При SUBSTAGE_BITS=0 маска все единицы и
выражение вырождается в стоковое stage_cnt = port_stage буквально.
Заодно контракт стал однозначнее: хост присылает ПЕРВУЮ стадию блока порта, а
не индекс порта. Без под-стадий это то же число, поэтому стоковая совместимость
сохраняется, а зарезервированное "7 = порт выключен" больше не может совпасть с
настоящей стадией (при четырёх под-стадиях маскированная семёрка совпадала с
блоком второго порта).
Из VHDL убраны не-ASCII символы: GHDL отвергает их даже в комментариях.
Проверено: тестбенч проходит все четыре сценария и падает под мутацией
(8 сработавших проверок), 305 python-тестов зелёные, ISE собирает битстрим.
Остаточное нарушение hold на MCU_MOSI ловится подбором cost table размещателя —
ограничение маргинально уже в стоке (запас +0.458 нс).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Скрипты, которыми правки ставятся в дерево LibreVNA на сборочной машине и
собирается прошивка MCU мастера её штатным тулчейном (STM32CubeIDE 1.17.0,
gcc 12.3.1). Оригиналы шести изменённых файлов сохраняются в backup_original,
так что откат — обратный xcopy.
Проверено на месте: все 7 файлов легли побайтово (сверил sha256 каждого),
прошивка мастера собралась, эталон предыдущей прошивки остался цел в
extracted/master_stm32.bin.
Битстрим FPGA этими скриптами не собирается — нужна ISE 14.7 в виртуалке,
процедура описана в README.txt и per_point_switching.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Окно минимума было 4096 отсчётов, и это давало опасную асимметрию: при
УМЕНЬШЕНИИ межстадийной паузы минимум падает мгновенно, а при УВЕЛИЧЕНИИ
(подняли dwell_us или задержку PLL прибора) устаревшее маленькое значение живёт
тысячи стадий. Порог оказывался ниже реальной межстадийной паузы, детектор
границы срабатывал на каждой стадии и залипал ключ в положении A на пару
развёрток.
Окно сокращено до 16 отсчётов. Длинное окно не давало точности: пауза задаётся
счётчиком в FPGA и детерминирована, а межстадийная — самая частая (три из
четырёх при четырёх стадиях), так что минимум по десятку отсчётов уже равен ей.
Зато теперь смена настроек подхватывается за считанные точки, и случайная
короткая пауза от помехи вымывается так же быстро.
IFBW на порог не влияет и влиять не может: он меняет длительность оцифровки
(фронт-спад), а меряется пауза (спад-фронт). Плюс ключ переключается по спаду,
когда оцифровка закончена, а детектор границы во время оцифровки неактивен —
boundaryPending_ сбрасывается на фронте. Разобрано в README.
Прошито и проверено: после сброса ядро в loop, Verified OK.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Мок-бэкенд ставил каждому трейсу свой capture_end_ns, потому что метка бралась
в момент создания трейса. На реальном железе все комбинации коллекции выходят из
одного синхронизированного цикла и делят одно окно — мок теперь ведёт себя так
же, иначе он вводит в заблуждение при чтении записанных меток.
В README контроллера добавлена оговорка: прошивка рассчитана ровно на два
положения ключа. Хост допускает и 4, но тогда положение перестаёт быть
чередованием одного бита и нужен контроллер на две управляющие линии.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FPGA мастера: порт излучает не в одной стадии, а в блоке подряд идущих —
младшие SUBSTAGE_BITS бит счётчика стадий стали положением выходного ключа,
старшие выбирают порт. Вынесено в функцию port_excites в Sweep.vhd, новое поле
приезжает из свободных битов 8:6 регистра SweepSetup через SPIConfig и top.vhd.
Совместимость со стоком строгая: при SUBSTAGE_BITS=0 сдвиг нулевой, а
добавленная проверка "порт-стадия выше последней" ничего не меняет, потому что
счётчик стадий и так пробегает только 0..STAGES. Ручной режим и анализатор
спектра, где STAGES=0, ведут себя ровно как раньше.
MCU: у SetupSweep добавлен параметр substage_bits со значением по умолчанию 0,
поэтому вызовы из Manual.cpp и SpectrumAnalyzer.cpp не тронуты. VNA.cpp берёт
ширину под-стадии из port3Stage — порта 3 у этого железа нет, поле свободно, и
формат пакета не меняется.
Проверено: GHDL анализирует все три VHDL-файла чисто; новый самопроверяющийся
Test_Sweep.vhd прогоняет четыре сценария (сток, два положения, приёмник без
излучения, четыре положения) и проверен мутацией — с возвращённым стоковым
сравнением сценарии с под-стадиями падают. Обе сборки MCU собираются без ошибок.
Битовые позиции сверены сквозь всю цепочку: хост шлёт port3Stage=1 в слове
0x243, MCU пишет регистр 0x7041, FPGA читает SUBSTAGE_BITS=1.
Слейвы перепрошивать не нужно: им шлётся порт-стадия 7, которая со счётчиком
0..3 не совпадает и в стоковом сравнении.
Исходники теперь опережают лежащие рядом бинарники — предупреждение добавлено в
начало reproduce/README.md, хеш README в SHA256SUMS обновлён. Порядок ввода в
строй расписан в per_point_switching.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Прошивка STM32 (WeAct G431CB, тот же тулчейн, что у пульта тележки), которая
стоит в разрыве триггерной петли между мастером и первым слейвом и переключает
выходной ключ внутри частотной точки.
Положение ключа — младший бит номера стадии, а период 2 нацело делит 4 стадии,
поэтому чередование не рвётся на границе точки и контроллеру достаточно
инвертировать ногу на каждый цикл триггера. Переключение по СПАДУ: спад означает
«оцифровка закончилась», дальше идёт окно успокоения следующей стадии (~62 мкс
при dwell_us=2), которого ключу хватает с запасом, и кольцо удерживать не надо.
Один потерянный фронт инвертировал бы чётность навсегда и молча поменял антенны
местами, поэтому контроллер распознаёт границу точки по длине паузы (между
точками к успокоению добавляется перезагрузка регистров PLL) и принудительно
ставит положение A — чётность чинится на каждой точке, а не раз на развёртку.
Порог берётся как минимум паузы по скользящему окну с запасом, то есть
подстраивается сам; отдельный провод-маркер начала развёртки не нужен.
Задержку зеркалирования проверил по Sweep.vhd: мастер стартует оцифровку по
своему TRIGGER_IN с конца кольца, то есть тоже после контроллера. Задержка
общая для всех приборов и сокращается в отношении «приёмник/опорник» — фазовой
ошибки не вносит, только чуть удлиняет развёртку.
Собирается: RAM 13.9%, Flash 20.0%.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Матрица 4x4 за одну развёртку: каждый порт мастера измеряется на каждой частоте
дважды, с двумя положениями выходного ключа. Ключ дёргает контроллер в
триггерной петле, поэтому расбери переключателями больше не управляет —
SwitchedMatrixRadarService уходит из тракта, все 16 комбинаций снимаются в
пределах одной точки вместо разброса между тремя развёртками.
Выходы нумеруются позиция-мажорно, стадии порт-мажорно (старшие биты счётчика
стадий выбирают порт, младший — положение ключа). Из этого следует, что
положение ключа = младший бит номера стадии, а период 2 нацело делит 4 стадии —
поэтому контроллеру достаточно инвертировать ногу на каждый цикл триггера, без
абсолютного счётчика. Выходы 0 и 1 при этом сохраняют прежний смысл.
Приёмники слейвов пронумерованы от числа выходов + 1, а не от фиксированного 3:
иначе при 4 выходах прямой член «приёмник 3 <- выход 3» назывался бы так же, как
отражение выхода 3. При двух выходах даёт прежние порты 3..6.
Слейвам порт-стадии выставляются в 7: стоковый приём «одна за последней стадией»
перестаёт работать, когда порт выбирается старшими битами.
Всё за флагом multi_device.stimulus_switch_positions (1 = прежнее поведение,
байт в байт). Спецификация правок FPGA/STM в device_firmware/per_point_switching.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Полное дерево LibreVNA v1.6.5 с доработками master/slave, эталонные битстримы
и прошивки STM32, упаковщик .vnafw. Проверено: build.sh пересобирает обе
прошивки побайтово идентично лежащим рядом .vnafw.
Мастер-битстрим (top_master.bin / extracted/master_fpga.bin) больше нигде не
существует — place&route в ISE недетерминирован, из исходников он не
восстанавливается.
Положено в multi_libreVNA_firmware/reproduce, а не в device_firmware/reproduce
как сказано в README комплекта: SHA256SUMS и build.sh ссылаются на
../combined.vnafw, а .vnafw лежат здесь.
Корневые правила .gitignore build*/build/ выбрасывали 482 файла, включая сам
build.sh и эталонные прошивки STM32 из SHA256SUMS — дописано разыгноривание.
Оставшиеся 7 файлов перебивал вложенный апстримный .gitignore (более глубокий
имеет приоритет), они добавлены force-add'ом. Промежуточные .o/.d/.su из
build-каталогов убраны, .bin/.elf/.hex/.map оставлены.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>