Связка из трёх частей:
| Часть | Где работает | Что делает |
|---|---|---|
spec_server (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.
На плате (по ssh, от root):
fpgautil -b system_wrapper3.bit.bin -f Full
cat /sys/class/fpga_manager/fpga0/state # должно быть: operating
Что важно:
.bit.bin (после bootgen), а не .bit.spec_server уже запущен — остановить его (Ctrl+C) до перепрошивки, потом запустить снова.WinSCP → New Session:
169.254.156.186, port 22root, password: rootСкопировать в /root/ на плате:
rp_apps/spec_server.crp_apps/Makefilesystem_wrapper3.bit.bin (если ещё не на плате)ssh root@169.254.156.186
cd /root
make
./spec_server --bind 169.254.156.186
Ожидаемый вывод:
spec_server: /dev/mem 0x43000000 отображён, слушаю 169.254.156.186:5001
Дальше сервер печатает подключения/отключения клиентов. Останов — Ctrl+C. Обслуживается один клиент за раз (второй ждёт в очереди accept, пока первый не отключится).
Параметры (все необязательны):
| Ключ | По умолчанию | Смысл |
|---|---|---|
--bind <ip> |
0.0.0.0 |
На каком адресе слушать |
--port <N> |
5001 |
TCP-порт |
--base <адрес> |
0x43000000 |
База регистров в AXI |
--dev <файл> |
/dev/mem |
Что отображать; обычный файл на 4 КБ даёт «стенд без платы» |
--force |
— | Не проверять состояние fpga_manager (опасно) |
Чтобы сервер жил после закрытия ssh-сессии:
nohup ./spec_server --bind 169.254.156.186 > spec_server.log 2>&1 &
На ПК (Python 3, сторонних пакетов не нужно):
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 ....
Одна команда без входа в оболочку:
python spec_client.py --host 169.254.156.186 status
Ключи: --port (5001), --timeout (5 с).
python spec_client.py --host 169.254.156.186 --run test_sequence.spec
Файл исполняется построчно; пустые строки и всё после # игнорируются. Каждая строка печатается с именем файла, номером и ответом. По умолчанию прогон останавливается на первой ошибке; -k — продолжать до конца. Код возврата: 0 — без ошибок, 1 — были.
Готовые файлы:
waitbuf, три сценария с аппаратным секвенсором, seqabort. Должен пройти без единой ошибки.-k, ненулевой код возврата — норма.RedPitayaEnginecd 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).
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).
DefaultEngine |
RedPitayaEngine |
|
|---|---|---|
| Передатчик | HackRF через bin\hackrftrans00.exe -t <file> -f <freq> -s <srate> -a <ampl> -x <gain> |
Red Pitaya: python bin\spec_client.py --host 169.254.156.186 --run <file> |
isdr.file |
.bin с IQ-отсчётами |
путь к .spec-файлу (абсолютный или относительно mserv00) |
isdr.freq, srate, ampl, gain |
Передаются в HackRF | Игнорируются — несущая, амплитуда и т. д. задаются командами set внутри .spec |
| Адрес платы | — | Зашит в engine.py:666-668 (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, поменять engine и isdr:
{
"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 }
}
}
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 <id 0..3> <scale 0..16383> |
Форма огибающей и амплитуда. 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.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 |