# Red Pitaya: деплой, запуск и работа со спектрометром Связка из трёх частей: | Часть | Где работает | Что делает | |---|---|---| | `spec_server` ([spec_server.c](spec_server.c)) | Red Pitaya (ARM, root) | Отображает регистры ПЛИС по `0x43000000`, слушает TCP-порт **5001**, принимает текстовые команды построчно | | `spec_client.py` | ПК | Оболочка / прогон `.spec`-файлов; та же копия лежит в `spectrometer_service/mserv00/bin/` для Django | | Django-сервис `mserv00` | ПК | REST API измерения; `RedPitayaEngine` вызывает `spec_client.py --run <файл>` вместо HackRF | Адрес платы в инструкции — `169.254.156.186` (link-local, плата подключена кабелем напрямую в ПК; Windows сама выдаёт себе адрес из `169.254.0.0/16`, DHCP не нужен). Логин `root`, пароль по умолчанию `root`. --- ## 1. Прошивка ПЛИС На плате (по ssh, от root): ```sh fpgautil -b system_wrapper3.bit.bin -f Full cat /sys/class/fpga_manager/fpga0/state # должно быть: operating ``` Что важно: - Нужен именно `.bit.bin` (после bootgen), а не `.bit`. - Прошивка **не переживает перезагрузку** платы: после каждого reboot — повторить. - Если `spec_server` уже запущен — остановить его (Ctrl+C) **до** перепрошивки, потом запустить снова. - Не открывать штатный веб-интерфейс Red Pitaya, пока работаете с сервером: его приложения грузят в ПЛИС свою прошивку. ## 2. Перенос исходников на плату (WinSCP) WinSCP → New Session: - File protocol: **SCP** (или SFTP) - Host name: `169.254.156.186`, port `22` - User name: `root`, password: `root` Скопировать в `/root/` на плате: - `rp_apps/spec_server.c` - `rp_apps/Makefile` - `system_wrapper3.bit.bin` (если ещё не на плате) ## 3. Сборка на плате ```sh ssh root@169.254.156.186 cd /root make ``` ## 4. Запуск сервера ```sh ./spec_server --bind 169.254.156.186 ``` Ожидаемый вывод: ``` spec_server: /dev/mem 0x43000000 отображён, слушаю 169.254.156.186:5001 ``` Дальше сервер печатает подключения/отключения клиентов. Останов — Ctrl+C. Обслуживается **один клиент за раз** (второй ждёт в очереди `accept`, пока первый не отключится). Параметры (все необязательны): | Ключ | По умолчанию | Смысл | |---|---|---| | `--bind ` | `0.0.0.0` | На каком адресе слушать | | `--port ` | `5001` | TCP-порт | | `--base <адрес>` | `0x43000000` | База регистров в AXI | | `--dev <файл>` | `/dev/mem` | Что отображать; обычный файл на 4 КБ даёт «стенд без платы» | | `--force` | — | Не проверять состояние `fpga_manager` (опасно) | Чтобы сервер жил после закрытия ssh-сессии: ```sh nohup ./spec_server --bind 169.254.156.186 > spec_server.log 2>&1 & ``` ## 5а. Клиент: интерактивная оболочка На ПК (Python 3, сторонних пакетов не нужно): ```sh cd rp_apps python spec_client.py --host 169.254.156.186 ``` ``` OK spec_server, база 0x43000000, help — список команд help — команды сервера, ? — команды клиента spec> status OK status=0x08 busy=0 last_valid=0 buf_valid=0 ready=1 overflow=0 seq_busy=0 seq_len=0 spec> set freq 2500000 OK inc=0x051eb852 (2500000.0 Гц) spec> pulse 20 2500000 0x0002 ... spec> quit ``` `help` — список команд сервера, `?` — команд клиента, `? set` — справка по конкретной. Всё, что оболочка не знает сама, уходит на сервер как есть. Ответ сервера всегда одна строка: `OK ...` либо `ERR ...`. Одна команда без входа в оболочку: ```sh python spec_client.py --host 169.254.156.186 status ``` Ключи: `--port` (5001), `--timeout` (5 с). ## 5б. Клиент: прогон файла последовательности ```sh python spec_client.py --host 169.254.156.186 --run test_sequence.spec ``` Файл исполняется построчно; пустые строки и всё после `#` игнорируются. Каждая строка печатается с именем файла, номером и ответом. По умолчанию прогон **останавливается на первой ошибке**; `-k` — продолжать до конца. Код возврата: `0` — без ошибок, `1` — были. Готовые файлы: - [test_sequence.spec](test_sequence.spec) — полный проверочный прогон: TTL, РЧ, цепочки с `waitbuf`, три сценария с аппаратным секвенсором, `seqabort`. Должен пройти без единой ошибки. - [test_contracts.spec](test_contracts.spec) — намеренно ошибочные команды, проверка отказов. Гонять с `-k`, ненулевой код возврата — норма. ## 5в. Django-сервис с `RedPitayaEngine` ### Запуск сервиса ```bat cd spectrometer_service\mserv00 autorun_default_venv.bat :: первый раз: создаёт mvenv, ставит зависимости autorun_default.bat :: далее ``` Что делают батники: убивают старый `pico-tcp.exe`, активируют venv, `pip install -r requirements.txt`, запускают `bin\pico-tcp.exe` (TCP-сервер Picoscope на порту 5003) и `python manage.py runserver` (порт 8000). ```bat python -m venv mvenv mvenv\Scripts\activate.bat pip install -r requirements.txt python manage.py migrate python manage.py createsuperuser :: AddDevices.bat ждёт specadmin/specadmin AddDevices.bat :: один раз: завести устройства в БД start bin\pico-tcp.exe python manage.py runserver ``` Важно: `runserver` должен быть запущен **из активированного venv** и из каталога `mserv00` — движок вызывает `python bin\spec_client.py` через `PATH`, а пути к `.spec`-файлам разрешаются относительно текущего каталога процесса. ### Отличия `RedPitayaEngine` от `DefaultEngine` Движок выбирается полем `"engine"` в `info` при `POST /api/measure/` ([engine.py:793](../spectrometer_service/mserv00/spectrometer/engine.py#L793)). | | `DefaultEngine` | `RedPitayaEngine` | |---|---|---| | Передатчик | HackRF через `bin\hackrftrans00.exe -t -f -s -a -x ` | Red Pitaya: `python bin\spec_client.py --host 169.254.156.186 --run ` | | `isdr.file` | `.bin` с IQ-отсчётами | **путь к `.spec`-файлу** (абсолютный или относительно `mserv00`) | | `isdr.freq`, `srate`, `ampl`, `gain` | Передаются в HackRF | **Игнорируются** — несущая, амплитуда и т. д. задаются командами `set` внутри `.spec` | | Адрес платы | — | Зашит в [engine.py:666-668](../spectrometer_service/mserv00/spectrometer/engine.py#L666-L668) (`rp_sdr(..., ip='169.254.156.186')`); другой адрес — править там | | Синхронизатор (Arduino DuePP, `Sync.exe`) | `syncUp` → `syncWait` перед каждым измерением, триггер АЦП от Arduino | **Не используется вовсе**: ни `syncUp`, ни `syncWait`, COM-порт не открывается. Триггер АЦП должен приходить **с TTL-линии Red Pitaya** | | Блок `isync` в JSON | Обязателен | Обязателен (сериализатор требует), но содержимое не используется — оставьте как в шаблоне | | Порядок в одном измерении | sync → ADC arm → 1 с → HackRF | ADC arm (`adc_interface.start` в отдельном потоке) → **1 с** → прогон `.spec` → ожидание АЦП → запись данных | | Усреднение | `iadc.averaging` повторов | То же: `.spec` прогоняется **`averaging` раз целиком**, между повторами пауза `averaging_delay` с | | Градиенты (`igrax/y/z`) | одинаково | одинаково | ### Пример запроса Взять [templates/template_post.json](../spectrometer_service/templates/template_post.json), поменять `engine` и `isdr`: ```json { "info": { "infostr": "fid_rp", "engine": "RedPitayaEngine", "time": null, "iadc": { "device_model": "PS4000A", "srate": 80000000, "points": [1000], "n_channels": 2, "channel_ranges": [8, 8], "n_triggers": 1, "averaging": 4, "averaging_delay": 1.0, "trigger_channel": 0, "trig_direction": 0, "threshold": 5000, "auto_measure_time": 10000, "enabled": true }, "isync": { "device_model": "DuePP", "file": "test.xml", "port": 7 }, # плевать, но что-то должно стоять "isdr": { "device_model": "RP", "srate": 100000, "freq": 100000, "ampl": true, "gain": 35, # плевать, но что-то должно стоять "file": "C:\\Work\\lowfield_mri_programs\\rp_apps\\fid.spec" }, "igrax": { "device_model": "GRU", "ip": "127.0.0.1", "file": "test.txt", "enabled": false }, "igray": { "device_model": "GRU", "ip": "127.0.0.1", "file": "test.txt", "enabled": false }, "igraz": { "device_model": "GRU", "ip": "127.0.0.1", "file": "test.txt", "enabled": false } } } ``` ```sh curl -u specadmin:specadmin -H "Content-Type: application/json" -d @post.json http://localhost:8000/api/measure/ # -> {"status":"created","measurement_id":N,...} curl -u specadmin:specadmin http://localhost:8000/api/measure/N/state/ # статус curl -u specadmin:specadmin http://localhost:8000/api/measure/N/data/ # данные (base64 int16) ``` --- ## Команды спектрометра ### Модель Единица работы тракта — **событие**. `start` ставит событие в очередь; когда до него доходит очередь, на `dur` мкс поднимаются TTL-линии из `mask`, и — если это `start rf` — одновременно излучается РЧ-импульс: несущая `freq`, начальная фаза `phase`, форма огибающей `env` (таблица `id` 0..3, амплитуда `scale`), длительность `pulse`. События идут строго друг за другом, без зазоров. Глубина очереди — три события (буфер + слот + идущее). Параметры задаются `set`-командами **до** `start` и действуют на все последующие события, пока их не поменяют. Сервер держит теневую копию параметров (`params`), поскольку регистры ПЛИС только на запись. Пауза между импульсами — это тоже событие: `set mask 0x0000`, `set dur <мкс>`, `start ttl`. ### Команды сервера | Команда | Описание | |---|---| | `status` | `busy` — событие идёт; `buf_valid` — команда ещё в буфере (параметры не защёлкнуты); `ready` — очередь примет команду; `overflow` — команда отвергнута (липкий!); `seq_busy` — секвенсор играет; `seq_len` — слов загружено | | `params` | Теневые параметры живого тракта | | `defaults` | Сброс: `dur=1000`, `mask=0`, `freq=2.5 МГц`, `phase=0`, `step=0x400` (≈524 мкс), `env 0 8191` | | `set dur <мкс>` | Длительность окна TTL, целое > 0 | | `set mask <0..0xFFFF>` | Какие из 16 TTL-линий подняты в окне | | `set freq <Гц>` | Несущая РЧ, `0 < f < 62.5e6` (Найквист при 125 МГц) | | `set inc <32 бита>` | То же, сырой фазовый инкремент | | `set phase <град>` | Начальная фаза несущей (любое число, берётся по модулю 360) | | `set pulse <мкс>` | Длительность огибающей; предел ≈ 536 870 мкс | | `set step <32 бита>` | То же, сырой шаг Q16.16 по таблице огибающей (0 запрещён — бесконечное излучение) | | `set env ` | Форма огибающей и амплитуда. **`scale ≤ 8191`**, выше — переполнение и искажение формы | | `start ttl` | Событие только с TTL | | `start rf` | Событие с TTL и РЧ. Требование: огибающая не длиннее окна (`pulse ≤ dur`), иначе `ERR` | | `seqbegin` | Начать загрузку программы в секвенсор: все дальнейшие `set/start/op/defaults` пишутся в BRAM (до 16384 слов), а не в тракт | | `seqend` | Закончить загрузку, вернуться в живой режим | | `seqrun [N]` | Проиграть загруженную программу (все слова или первые N) на 125 МГц без участия сети | | `seqabort` | Немедленно прекратить выдачу слов (текущее событие доиграет) | | `op <код> <параметр>` | Сырая команда без проверок | | `peek <смещ.>` / `poke <смещ.> <знач.>` | Прямое чтение/запись регистра (`0x00` OPCODE, `0x04` PARAM, `0x08` STATUS) | | `help`, `quit` | | ### Привязка бит маски к пинам TTL-линии выведены на разъём **E1** платы (STEMlab 125-14 Gen 2): ряд `DIO*_P` и ряд `DIO*_N`. Бит в `set mask` → пин: | Бит | Пин E1 | | Бит | Пин E1 | |---|---|---|---|---| | `0x0002` | DIO0_P | | `0x0200` | DIO0_N | | `0x0004` | DIO1_P (?) | | `0x0400` | DIO1_N | | `0x0004` | DIO2_P (?) | | `0x0800` | DIO2_N | | `0x0008` | DIO3_P | | `0x1000` | DIO3_N | | `0x0010` | DIO4_P | | `0x2000` | DIO4_N | | `0x0020` | DIO5_P | | `0x4000` | DIO5_N | | `0x0040` | DIO6_P | | `0x8000` | DIO6_N | | `0x0080` | DIO7_P | | ? | DIO7_N (не работает, причина не выяснена) | Что пока не проверено: - `DIO1_P` и `DIO2_P` — по текущим наблюдениям оба отзываются на `0x0004`; какой бит на самом деле у каждого — уточнить. - `DIO7_N` — не удалось получить сигнал ни на одном бите. - Биты `0x0001` и `0x0100` в таблице не заняты. Примеры: `set mask 0x0002` — только DIO0_P; `set mask 0x0082` — DIO0_P + DIO7_P; `set mask 0xFFFF` — все линии. ### Директивы клиента (работают в оболочке и в `.spec`) | Директива | Описание | |---|---| | `echo <текст>` | Напечатать (заголовок раздела в файле) | | `wait <мс>` | Пауза на ПК | | `waitidle [мс]` | Ждать `busy=0` — все события доиграли (по умолчанию 5000 мс) | | `waitbuf [мс]` | Ждать `buf_valid=0` — команда ушла из буфера в тракт, параметры защёлкнуты (1000 мс) | | `waitseq [мс]` | Ждать `seq_busy=0` — секвенсор выдал все слова (1000 мс). Не значит, что последнее событие доиграло: после `waitseq` нужен ещё `waitidle` | | `run [-k] <файл>` | Выполнить файл вживую (вложенность до 8) | | `load <файл>` | Тот же файл, но обернуть в `seqbegin … seqend` — загрузить в секвенсор, не исполняя. Внутри такого файла `wait*` бессмысленны | | `pulse <мкс> [Гц] [маска]` | Составная: `set dur` (×1.2), `set pulse`, `set freq`, `set mask`, `start rf` | | `connect [хост [порт]]` | Переподключиться | ### Живой режим против секвенсора **Живой режим** — каждая строка идёт по сети. `mask` и `dur` защёлкиваются только в момент, когда `start` доходит до `ttl_controller`. Если поменять их, пока предыдущий `start` лежит в буфере (`buf_valid=1`), уже поставленное событие уедет с новой маской. Поэтому между звеньями цепочки в живом режиме — **`waitbuf`**, а не `wait`: ``` set dur 20 set mask 0xFFFF set pulse 10 start rf waitbuf 1000 # дождаться, пока старт защёлкнул mask/dur set dur 50 set mask 0x0000 start ttl waitbuf 1000 ``` **Секвенсор** — вся программа сначала кладётся в BRAM (`seqbegin … seqend`), потом `seqrun` играет её на 125 МГц. Между словами нет сетевого окна, гонка невозможна, `waitbuf` внутри не нужен. Тайминги детерминированы — для реальных последовательностей использовать именно этот режим. Все контракты проверяются при загрузке так же, как вживую (`ERR` на `start rf` с длинной огибающей вылетит на этапе `load`). `seq_len` в `status` знает только сервер: после его перезапуска счётчик обнуляется, хотя BRAM на плате не тронута — тогда `seqrun N` с явным числом слов. ### Пример: ``` seqbegin defaults set freq 2500000 # несущая 2.5 МГц set env 0 8191 # огибающая №0, максимальная амплитуда # 90°: 20 мкс РЧ в окне 30 мкс, ключ ТХ поднят set dur 30 set mask 0x0002 set phase 0 set pulse 20 start rf # τ = 500 мкс тишины (событие без линий) set dur 500 set mask 0x0000 start ttl # 180°: 40 мкс, фаза 90° set dur 50 set mask 0x0002 set phase 90 set pulse 40 start rf # пауза до эха, затем строб АЦП (линия 0) на время приёма set dur 450 set mask 0x0000 start ttl set dur 1000 set mask 0x0001 start ttl seqend status # seq_len=22 seqrun waitseq 1000 # секвенсор выдал все слова waitidle 5000 # последнее событие (окно приёма) доиграло status # busy=0 overflow=0 — успех defaults ``` Тот же файл можно разнести: тело программы (без `seqbegin/seqend/seqrun/wait*`) — в `se_prog.spec`, а обёртка: ``` # se_run.spec load se_prog.spec seqrun waitseq 1000 waitidle 5000 status ``` ### Пример: одиночный импульс вживую (FID) ``` # fid.spec — строб АЦП, затем один РЧ-импульс. Живой режим. defaults set freq 2500000 set env 0 8191 set dur 10 set mask 0x0001 # строб на триггерный вход АЦП start ttl waitbuf 1000 set dur 30 set mask 0x0002 set pulse 20 start rf waitidle 5000 status ``` ### Типичные `ERR` и что они значат | Ответ | Причина | |---|---| | `огибающая … длиннее окна TTL … (контракт 2)` | `pulse > dur` перед `start rf` — увеличить `dur` или укоротить `pulse` | | `set step: … нуль = бесконечное излучение (контракт 1)` | `step 0` / слишком длинный `pulse` | | `set freq: нужно 0 < f < 62500000 Гц` | Выше Найквиста | | `set env: id 0..3, scale 0..16383` | Вне диапазона | | `очередь занята (ready=0, buf_valid=1)` | Три события уже в очереди — добавить `waitbuf`/`waitidle` | | `команда отвергнута, поднят overflow` | Очередь переполнилась на стороне ПЛИС; флаг липкий — перезагрузить битстрим (п. 1) | | `последовательность заполнена (16384 слов)` | Программа не влезает в BRAM | | `seqrun: нечего играть (0 слов)` | Не было `seqbegin … seqend` (или сервер перезапущен — задать `seqrun N`) | | `fpga_manager в состоянии '…', а не 'operating'` (при старте сервера) | Битстрим не загружен — п. 1 |