From e144c3e86a02edbcad49dd54421997d14d38ac85 Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Mon, 9 Nov 2026 00:15:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B5=20=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=D0=B8=20Zynq,=20Ethernet=20DSView=20=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=B5=D0=BA=D0=BE=D0=B4=D0=B5=D1=80=20=D0=A8=D0=98=D0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 + c/zynq-net/README.md | 15 + c/zynq-net/include/zynq_net.h | 41 + c/zynq-net/ports/rtl/axi_simple_io.sv | 74 + c/zynq-net/src/zynq_net.c | 49 + c/zynq-net/tests/tb_axi_simple_io.sv | 49 + c/zynq-net/tests/test_zynq_net.c | 36 + hdl/logic-analyzer/README.md | 46 + hdl/logic-analyzer/include/zynq_la.h | 24 + hdl/logic-analyzer/include/zynq_la_stream.h | 23 + hdl/logic-analyzer/rtl/axi_logic_analyzer.sv | 204 +++ .../rtl/axi_logic_analyzer_bd.v | 47 + hdl/logic-analyzer/rtl/la_advanced_trigger.v | 98 ++ hdl/logic-analyzer/rtl/la_run_encoder.v | 92 + hdl/logic-analyzer/rtl/la_trigger_buffer.v | 134 ++ hdl/logic-analyzer/rtl/zynq_la_stream.v | 184 ++ .../tests/tb_axi_logic_analyzer.sv | 133 ++ .../tests/tb_la_advanced_trigger.sv | 86 + hdl/logic-analyzer/tests/tb_zynq_la_stream.sv | 101 ++ hdl/logic-hdmi-preview/README.md | 18 + .../rtl/zynq_logic_preview.v | 134 ++ .../tests/tb_zynq_logic_preview.sv | 51 + hdl/logic-udp-ethernet/README.md | 76 + hdl/logic-udp-ethernet/include/zynq_la_pl.h | 44 + hdl/logic-udp-ethernet/rtl/rtl8211f_init.v | 105 ++ hdl/logic-udp-ethernet/rtl/zynq_pl_capture.v | 481 +++++ hdl/logic-udp-ethernet/rtl/zynq_pl_ethernet.v | 409 +++++ hdl/logic-udp-ethernet/rtl/zynq_pl_vfd.v | 183 ++ .../tests/check_zynq_rj2.py | 144 ++ .../tests/tb_rtl8211f_init.sv | 81 + .../tests/tb_zynq_pl_capture.sv | 267 +++ .../tests/tb_zynq_pl_ethernet.sv | 104 ++ hdl/scope-acquisition/README.md | 13 + hdl/scope-acquisition/include/scope_regs.h | 84 + .../rtl/acquisition_buffer.sv | 99 ++ hdl/scope-acquisition/rtl/adc_capture.sv | 48 + hdl/scope-acquisition/rtl/adc_capture_dual.sv | 19 + hdl/scope-acquisition/rtl/adc_frontend4.sv | 42 + .../rtl/async_sample_fifo.sv | 115 ++ hdl/scope-acquisition/rtl/axi_scope_regs.sv | 247 +++ hdl/scope-acquisition/rtl/capture_stream.sv | 57 + hdl/scope-acquisition/rtl/reset_sync.sv | 13 + hdl/scope-acquisition/rtl/sample_pairer.sv | 64 + hdl/scope-acquisition/rtl/scope_core.sv | 91 + hdl/scope-acquisition/rtl/trigger_engine.sv | 121 ++ .../rtl/triggered_acquisition.sv | 49 + .../tests/tb_acquisition_buffer.sv | 130 ++ hdl/scope-acquisition/tests/tb_adc_capture.sv | 277 +++ .../tests/tb_adc_frontend4.sv | 61 + .../tests/tb_async_sample_fifo.sv | 253 +++ .../tests/tb_axi_scope_regs.sv | 314 ++++ .../tests/tb_capture_stream.sv | 58 + hdl/scope-acquisition/tests/tb_reset_sync.sv | 19 + .../tests/tb_sample_pairer.sv | 279 +++ hdl/scope-acquisition/tests/tb_scope_core.sv | 148 ++ .../tests/tb_trigger_engine.sv | 286 +++ .../tests/tb_triggered_acquisition.sv | 58 + hdl/vfd-pwm/README.md | 13 + hdl/vfd-pwm/rtl/vfd_pwm.sv | 222 +++ hdl/vfd-pwm/tests/tb_vfd_pwm.sv | 63 + hdl/vfd-pwm/tools/gen_vfd_sine.py | 12 + .../logic_analyzer/decoders/pwm_waveform.py | 69 + tools/dsview/README.md | 8 + tools/dsview/build.py | 10 +- tools/dsview/decoders/pwm_waveform/README.md | 48 + .../dsview/decoders/pwm_waveform/__init__.py | 1 + tools/dsview/decoders/pwm_waveform/pd.py | 93 + tools/dsview/native/pwm_waveform.h | 29 + tools/dsview/protocol-files.json | 5 +- tools/dsview/pwm-waveform.patch | 71 + tools/dsview/tests/check_pwm_native.py | 92 + tools/dsview/tests/test_install_decoders.py | 2 +- tools/dsview/tests/test_pwm_waveform.py | 117 ++ tools/dsview/zynq-ethernet/README.md | 62 + tools/dsview/zynq-ethernet/build.py | 60 + tools/dsview/zynq-ethernet/ethernet.patch | 1543 +++++++++++++++++ tools/dsview/zynq-ethernet/package_arm.py | 72 + tools/dsview/zynq-ethernet/zynq.c | 1197 +++++++++++++ tools/dsview/zynq-ethernet/zynq_record.c | 503 ++++++ tools/dsview/zynq-ethernet/zynq_record.h | 15 + 80 files changed, 10790 insertions(+), 7 deletions(-) create mode 100644 c/zynq-net/README.md create mode 100644 c/zynq-net/include/zynq_net.h create mode 100644 c/zynq-net/ports/rtl/axi_simple_io.sv create mode 100644 c/zynq-net/src/zynq_net.c create mode 100644 c/zynq-net/tests/tb_axi_simple_io.sv create mode 100644 c/zynq-net/tests/test_zynq_net.c create mode 100644 hdl/logic-analyzer/include/zynq_la.h create mode 100644 hdl/logic-analyzer/include/zynq_la_stream.h create mode 100644 hdl/logic-analyzer/rtl/axi_logic_analyzer.sv create mode 100644 hdl/logic-analyzer/rtl/axi_logic_analyzer_bd.v create mode 100644 hdl/logic-analyzer/rtl/la_advanced_trigger.v create mode 100644 hdl/logic-analyzer/rtl/la_run_encoder.v create mode 100644 hdl/logic-analyzer/rtl/la_trigger_buffer.v create mode 100644 hdl/logic-analyzer/rtl/zynq_la_stream.v create mode 100644 hdl/logic-analyzer/tests/tb_axi_logic_analyzer.sv create mode 100644 hdl/logic-analyzer/tests/tb_la_advanced_trigger.sv create mode 100644 hdl/logic-analyzer/tests/tb_zynq_la_stream.sv create mode 100644 hdl/logic-hdmi-preview/README.md create mode 100644 hdl/logic-hdmi-preview/rtl/zynq_logic_preview.v create mode 100644 hdl/logic-hdmi-preview/tests/tb_zynq_logic_preview.sv create mode 100644 hdl/logic-udp-ethernet/README.md create mode 100644 hdl/logic-udp-ethernet/include/zynq_la_pl.h create mode 100644 hdl/logic-udp-ethernet/rtl/rtl8211f_init.v create mode 100644 hdl/logic-udp-ethernet/rtl/zynq_pl_capture.v create mode 100644 hdl/logic-udp-ethernet/rtl/zynq_pl_ethernet.v create mode 100644 hdl/logic-udp-ethernet/rtl/zynq_pl_vfd.v create mode 100644 hdl/logic-udp-ethernet/tests/check_zynq_rj2.py create mode 100644 hdl/logic-udp-ethernet/tests/tb_rtl8211f_init.sv create mode 100644 hdl/logic-udp-ethernet/tests/tb_zynq_pl_capture.sv create mode 100644 hdl/logic-udp-ethernet/tests/tb_zynq_pl_ethernet.sv create mode 100644 hdl/scope-acquisition/README.md create mode 100644 hdl/scope-acquisition/include/scope_regs.h create mode 100644 hdl/scope-acquisition/rtl/acquisition_buffer.sv create mode 100644 hdl/scope-acquisition/rtl/adc_capture.sv create mode 100644 hdl/scope-acquisition/rtl/adc_capture_dual.sv create mode 100644 hdl/scope-acquisition/rtl/adc_frontend4.sv create mode 100644 hdl/scope-acquisition/rtl/async_sample_fifo.sv create mode 100644 hdl/scope-acquisition/rtl/axi_scope_regs.sv create mode 100644 hdl/scope-acquisition/rtl/capture_stream.sv create mode 100644 hdl/scope-acquisition/rtl/reset_sync.sv create mode 100644 hdl/scope-acquisition/rtl/sample_pairer.sv create mode 100644 hdl/scope-acquisition/rtl/scope_core.sv create mode 100644 hdl/scope-acquisition/rtl/trigger_engine.sv create mode 100644 hdl/scope-acquisition/rtl/triggered_acquisition.sv create mode 100644 hdl/scope-acquisition/tests/tb_acquisition_buffer.sv create mode 100644 hdl/scope-acquisition/tests/tb_adc_capture.sv create mode 100644 hdl/scope-acquisition/tests/tb_adc_frontend4.sv create mode 100644 hdl/scope-acquisition/tests/tb_async_sample_fifo.sv create mode 100644 hdl/scope-acquisition/tests/tb_axi_scope_regs.sv create mode 100644 hdl/scope-acquisition/tests/tb_capture_stream.sv create mode 100644 hdl/scope-acquisition/tests/tb_reset_sync.sv create mode 100644 hdl/scope-acquisition/tests/tb_sample_pairer.sv create mode 100644 hdl/scope-acquisition/tests/tb_scope_core.sv create mode 100644 hdl/scope-acquisition/tests/tb_trigger_engine.sv create mode 100644 hdl/scope-acquisition/tests/tb_triggered_acquisition.sv create mode 100644 hdl/vfd-pwm/README.md create mode 100644 hdl/vfd-pwm/rtl/vfd_pwm.sv create mode 100644 hdl/vfd-pwm/tests/tb_vfd_pwm.sv create mode 100644 hdl/vfd-pwm/tools/gen_vfd_sine.py create mode 100644 python/logic_analyzer/decoders/pwm_waveform.py create mode 100644 tools/dsview/decoders/pwm_waveform/README.md create mode 100644 tools/dsview/decoders/pwm_waveform/__init__.py create mode 100644 tools/dsview/decoders/pwm_waveform/pd.py create mode 100644 tools/dsview/native/pwm_waveform.h create mode 100644 tools/dsview/pwm-waveform.patch create mode 100644 tools/dsview/tests/check_pwm_native.py create mode 100644 tools/dsview/tests/test_pwm_waveform.py create mode 100644 tools/dsview/zynq-ethernet/README.md create mode 100644 tools/dsview/zynq-ethernet/build.py create mode 100644 tools/dsview/zynq-ethernet/ethernet.patch create mode 100644 tools/dsview/zynq-ethernet/package_arm.py create mode 100644 tools/dsview/zynq-ethernet/zynq.c create mode 100644 tools/dsview/zynq-ethernet/zynq_record.c create mode 100644 tools/dsview/zynq-ethernet/zynq_record.h diff --git a/README.md b/README.md index 7b51cb8..47ce767 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,18 @@ templates/ ## Что лежит +### FPGA и DSView + +| Блок | Назначение | +|---|---| +| [Блоки логического анализатора](hdl/logic-analyzer/README.md) | Аппаратный триггер, BRAM, AXI и кольцевой буфер DDR | +| [Осциллограф](hdl/scope-acquisition/README.md) | Синхронизация каналов ADC, триггер, буфер и выдача записи | +| [Ethernet в FPGA](hdl/logic-udp-ethernet/README.md) | RGMII, MDIO для RTL8211F, UDP-поток и буфер восьми входов | +| [HDMI-отрисовка](hdl/logic-hdmi-preview/README.md) | График восьми цифровых каналов из входов или потока | +| [Трёхфазный ШИМ](hdl/vfd-pwm/README.md) | DDS, рампа, комплементарные выходы и мёртвое время | +| [DSView Ethernet](tools/dsview/zynq-ethernet/README.md) | Нативное подключение TCP/UDP, запуск и остановка | +| [ZNET](c/zynq-net/README.md) | C99-кодек Ethernet-команд ввода-вывода | + ### C | Библиотека | Что делает | Зависимости | Что нужно от платформы | diff --git a/c/zynq-net/README.md b/c/zynq-net/README.md new file mode 100644 index 0000000..49f512b --- /dev/null +++ b/c/zynq-net/README.md @@ -0,0 +1,15 @@ +# ZNET Ethernet-команды + +`include/zynq_net.h` задаёт 64-байтный UDP wire-контракт версии 1. +`src/zynq_net.c` кодирует пакет, проверяет длину и допустимость +команд и применяет операции к состоянию ARM. Транспорт передаёт в +кодек выделенные вызывающим буферы. + +`ports/rtl/axi_simple_io.sv` — регистровый AXI4-Lite блок для +физических входов и выходов. Транспортная служба сопоставляет команды +регистрам, таблицу пинов определяет проект платы. + +```sh +cc -std=c99 -Wall -Wextra -Iinclude src/zynq_net.c tests/test_zynq_net.c -o znet-check +./znet-check +``` diff --git a/c/zynq-net/include/zynq_net.h b/c/zynq-net/include/zynq_net.h new file mode 100644 index 0000000..eed9681 --- /dev/null +++ b/c/zynq-net/include/zynq_net.h @@ -0,0 +1,41 @@ +#ifndef ZYNQ_NET_H /* Защищаем единый переносимый кодек Ethernet-обмена от повторного включения. */ +#define ZYNQ_NET_H /* Один и тот же ABI используется ARM-службой и библиотекой настольного клиента. */ +#include /* Размеры пользовательских буферов передаются явно через size_t. */ +#include /* Фиксированные разрядности исключают различия ARMv7 и Windows x64. */ +#if defined(_WIN32) /* Windows требует явного экспорта функций из отдельной библиотеки кодека. */ +#define ZNET_API __declspec(dllexport) /* Экспортируем только функции, вызываемые тонким ctypes-портом. */ +#else /* В ELF видимость по умолчанию подходит статической и динамической сборке. */ +#define ZNET_API /* Кодек не зависит от конкретной ОС и сетевого стека. */ +#endif /* Завершаем выбор атрибутов публичного ABI. */ +#define ZNET_SIZE 64u /* Версия 1 передаёт ровно 64 байта UDP payload без зависимости от padding структур. */ +#define ZNET_PORT 37570u /* Выделенный пользовательский UDP-порт проекта, одинаковый для поиска и обмена. */ +#define ZNET_DISCOVER 1u /* Запрос сведений об устройстве допускает broadcast и не меняет состояние. */ +#define ZNET_READ 2u /* Читаем два поля и восемь дискретов выбранной цели. */ +#define ZNET_WRITE 3u /* Устанавливаем абсолютные значения: повтор пакета не переключает выходы заново. */ +#define ZNET_WRITE_OUTPUTS 4u /* Записываем только DO, сохраняя счётчики A/B, которые независимо меняются кнопками платы. */ +#define ZNET_REPLY 128u /* Старший бит операции отличает ответ от запроса, исключая отражение ответов. */ +#define ZNET_ARM 0u /* Цель PS хранит учебные поля в памяти процесса Cortex-A9. */ +#define ZNET_FPGA 1u /* Цель PL обслуживается только при явно подключённом UIO-модуле. */ +#define ZNET_OK 0u /* Успешное выполнение позволяет применять полученные значения к интерфейсу. */ +#define ZNET_UNAVAILABLE 1u /* Загруженный bitstream не предоставляет нужный модуль либо UIO не настроен. */ +#define ZNET_BAD_REQUEST 2u /* Некорректные значения отклоняются до изменения ARM или AXI-регистров. */ +#define ZNET_CAP_ARM 1u /* Бит capabilities подтверждает наличие учебного ARM-хранилища. */ +#define ZNET_CAP_FPGA 2u /* Бит capabilities выставляется только после проверки аппаратных ID и ABI. */ +#define ZNET_CAP_VIRTUAL_DI 4u /* Входы ARM являются программным эхом выходов, а не физическими контактами. */ +#define ZNET_CAP_BOARD_IO 8u /* ARM управляет четырьмя штатными LED и читает четыре кнопки через драйверы Linux. */ +#define ZNET_PL_ID 0x5a494f31u /* ASCII ZIO1 идентифицирует отдельный AXI-модуль простого обмена. */ +#define ZNET_PL_VERSION 0x00010000u /* Старшая половина задаёт несовместимую версию аппаратной карты 1.0. */ +typedef struct ZnetMessage { /* Host-представление никогда не отправляется в сеть прямым копированием. */ + uint32_t operation, target, sequence, status; /* Операция, цель, номер запроса и код результата доступны через стабильный ABI. */ + uint32_t field_a, field_b, inputs, outputs; /* Два беззнаковых 32-битных поля и младшие восемь бит входов/выходов. */ + uint32_t capabilities, counter; /* Возможности и счётчик принятых корректных запросов облегчают диагностику. */ + char name[24]; /* Имя устройства ограничено 23 ASCII-символами и завершающим нулём. */ +} ZnetMessage; /* Размер структуры не определяет размер сетевого пакета: сериализация всегда явная. */ +typedef struct ZnetArmState { /* Вызывающий владеет независимым состоянием каждого ARM-экземпляра. */ + uint32_t field_a, field_b, outputs; /* Поля живут до перезапуска; Linux-порт при наличии BOARD_IO связывает младшие четыре DO со светодиодами. */ +} ZnetArmState; /* Переносимое хранилище не использует heap и глобальные переменные. */ +ZNET_API int znet_encode(uint8_t *wire, size_t capacity, const ZnetMessage *message); /* Проверяем поля и записываем ровно один пакет в буфер вызывающего. */ +ZNET_API int znet_decode(ZnetMessage *message, const uint8_t *wire, size_t length); /* Отвергаем чужие magic/version/размеры до обращения к состоянию. */ +ZNET_API uint32_t znet_arm_apply(ZnetArmState *state, const ZnetMessage *request, ZnetMessage *reply); /* Общая логика ARM READ/WRITE тестируется независимо от Linux-сокетов. */ +ZNET_API void znet_arm_key(ZnetArmState *state, uint32_t *held, unsigned key, int value); /* Обрабатываем фронт кнопки 0:A+, 1:A−, 2:B+, 3:B−; evdev repeat=2 не меняет счётчик. */ +#endif /* Завершаем публичный контракт версии 1. */ diff --git a/c/zynq-net/ports/rtl/axi_simple_io.sv b/c/zynq-net/ports/rtl/axi_simple_io.sv new file mode 100644 index 0000000..17997df --- /dev/null +++ b/c/zynq-net/ports/rtl/axi_simple_io.sv @@ -0,0 +1,74 @@ +`timescale 1ns/1ps // Единица симуляции 1 нс и точность 1 пс не влияют на синтез регистров. +module axi_simple_io ( // Независимый AXI4-Lite slave добавляет учебный обмен, сохраняя ABI осциллографа. + (* X_INTERFACE_INFO = "xilinx.com:signal:clock:1.0 clk CLK", X_INTERFACE_PARAMETER = "ASSOCIATED_BUSIF s_axi, ASSOCIATED_RESET aresetn" *) input wire clk, // Vivado связывает AXI-интерфейс с единственным processing clock. + (* X_INTERFACE_INFO = "xilinx.com:signal:reset:1.0 aresetn RST", X_INTERFACE_PARAMETER = "POLARITY ACTIVE_LOW" *) input wire aresetn, // Сброс снимается синхронно в clock domain AXI после platform reset controller. + input wire [11:0] s_axi_awaddr, input wire [2:0] s_axi_awprot, // Окно 4 КиБ выбирает GP interconnect; PROT не влияет на учебный банк. + input wire s_axi_awvalid, output wire s_axi_awready, // Адрес записи принимается независимо от данных. + input wire [31:0] s_axi_wdata, input wire [3:0] s_axi_wstrb, // Четыре WSTRB управляют отдельными байтами 32-битного регистра. + input wire s_axi_wvalid, output wire s_axi_wready, // Канал W допускает приход раньше либо позже AW. + output reg [1:0] s_axi_bresp, output reg s_axi_bvalid, input wire s_axi_bready, // Ответ записи удерживается до подтверждения master. + input wire [11:0] s_axi_araddr, input wire [2:0] s_axi_arprot, // Чтение адресуется байтовым смещением внутри того же окна. + input wire s_axi_arvalid, output wire s_axi_arready, // Один read response обслуживается независимо от незавершённой записи. + output reg [31:0] s_axi_rdata, output reg [1:0] s_axi_rresp, // Данные и код ответа фиксируются на AR handshake. + output reg s_axi_rvalid, input wire s_axi_rready, // Backpressure не меняет уже выданный ответ. + input wire [7:0] digital_inputs, output reg [7:0] digital_outputs, // Входы предназначены для медленных независимых дискретов, а не когерентной многобитной шины. + output reg [31:0] field_a, output reg [31:0] field_b // Два поля становятся видимыми прикладной PL-логике одновременно с COMMIT. +); // Физические пины DIO назначаются только после проверки электрического интерфейса конкретной платы. + localparam [1:0] OKAY = 2'b00, SLVERR = 2'b10; // Стандарт AXI кодирует успешный ответ 00, ошибку slave 10. + reg aw_pending, w_pending; // Одноэлементные буферы связывают независимые AW и W в одну транзакцию. + reg [11:0] write_address; reg [31:0] write_data; reg [3:0] write_strobes; // Сохраняем все атрибуты до выполнения записи. + reg [31:0] shadow_a, shadow_b, shadow_outputs; // Shadow-банк позволяет передать несколько параметров до атомарного применения. + (* ASYNC_REG = "TRUE" *) reg [7:0] input_meta, input_sync; // Две ступени снижают риск метастабильности каждого независимого асинхронного входа. + assign s_axi_awready = aresetn && !aw_pending && !s_axi_bvalid; // Новый адрес не перезаписывает ни ожидающую пару, ни удерживаемый ответ. + assign s_axi_wready = aresetn && !w_pending && !s_axi_bvalid; // Новый payload принимается только в свободный буфер W. + assign s_axi_arready = aresetn && !s_axi_rvalid; // До принятия R текущая выборка остаётся неизменной. + function automatic [31:0] merge_bytes(input [31:0] old_value, input [31:0] new_value, input [3:0] strobes); // Объединяем только явно разрешённые byte lanes. + integer i; begin // Счётчик разворачивается синтезатором в четыре независимых мультиплексора. + merge_bytes = old_value; // Невыбранные байты сохраняют прежнее значение. + for (i = 0; i < 4; i = i + 1) if (strobes[i]) merge_bytes[i*8 +: 8] = new_value[i*8 +: 8]; // Номер WSTRB совпадает с номером байта на AXI32. + end // Завершаем вычисление маскированной записи. + endfunction // Нулевая маска strobes даёт no-op. + always @(posedge clk) begin // Вся управляющая логика, включая synchronizer, работает в одной AXI-области. + if (!aresetn) begin // Сброс отменяет незавершённые транзакции и переводит все выходы в ноль. + aw_pending <= 0; w_pending <= 0; write_address <= 0; write_data <= 0; write_strobes <= 0; // Старые половины записи не переживают reset. + s_axi_bvalid <= 0; s_axi_bresp <= OKAY; s_axi_rvalid <= 0; s_axi_rresp <= OKAY; s_axi_rdata <= 0; // Ответы AXI начинают работу в неактивном состоянии. + shadow_a <= 0; shadow_b <= 0; shadow_outputs <= 0; field_a <= 0; field_b <= 0; digital_outputs <= 0; // Нулевые настройки исключают непреднамеренное включение учебных DO после старта. + input_meta <= 0; input_sync <= 0; // Первые две выборки после reset заполняют synchronizer заново. + end else begin // Нормальная работа не зависит от состояния Ethernet-службы. + input_meta <= digital_inputs; input_sync <= input_meta; // Уровень каждого DI достигает read-bank через два фронта clk. + if (s_axi_bvalid && s_axi_bready) s_axi_bvalid <= 0; // Отпускаем ответ записи только после handshake B. + if (s_axi_rvalid && s_axi_rready) s_axi_rvalid <= 0; // Отпускаем read response только после handshake R. + if (s_axi_awvalid && s_axi_awready) begin write_address <= s_axi_awaddr; aw_pending <= 1; end // Сохраняем AW даже при отсутствующем W. + if (s_axi_wvalid && s_axi_wready) begin write_data <= s_axi_wdata; write_strobes <= s_axi_wstrb; w_pending <= 1; end // Аналогично W не зависит от готовности адреса. + if (aw_pending && w_pending && !s_axi_bvalid) begin // Пара выполняется один раз; B backpressure не дублирует COMMIT. + aw_pending <= 0; w_pending <= 0; s_axi_bvalid <= 1; s_axi_bresp <= SLVERR; // По умолчанию неизвестный либо read-only адрес даёт ошибку. + case (write_address) // Полное сравнение также отклоняет невыровненные адреса и alias верхних битов. + 12'h008: begin shadow_a <= merge_bytes(shadow_a, write_data, write_strobes); s_axi_bresp <= OKAY; end // Смещение 0x08 хранит первое shadow-поле uint32. + 12'h00c: begin shadow_b <= merge_bytes(shadow_b, write_data, write_strobes); s_axi_bresp <= OKAY; end // Смещение 0x0C хранит второе shadow-поле uint32. + 12'h010: begin shadow_outputs <= merge_bytes(shadow_outputs, write_data, write_strobes) & 32'hff; s_axi_bresp <= OKAY; end // Только биты 7..0 соответствуют восьми выходам. + 12'h018: begin // Регистр команды применяет весь набор настроек за один фронт. + if (!write_strobes[0] || !(write_data & 32'hff)) s_axi_bresp <= OKAY; // Незаписанный младший байт либо нулевая команда являются no-op. + else if ((write_data & 32'hff) == 1) begin field_a <= shadow_a; field_b <= shadow_b; digital_outputs <= shadow_outputs[7:0]; s_axi_bresp <= OKAY; end // Команда 1 одновременно обновляет поля и дискреты. + end // Другие команды отклоняются без изменения активного набора. + default: s_axi_bresp <= SLVERR; // ID, версия, DI, активные копии и reserved-адреса не записываются. + endcase // Каждая запись завершается ровно одним B-ответом. + end // Буферы AW/W освободятся для следующей транзакции после B handshake. + if (s_axi_arvalid && s_axi_arready) begin // Snapshot одного read word удерживается независимо от дальнейших изменений DI. + s_axi_rvalid <= 1; s_axi_rresp <= OKAY; // Подтверждение AR создаёт единственный ожидающий R. + case (s_axi_araddr) // Карта совместима с UIO-портом server.c. + 12'h000: s_axi_rdata <= 32'h5a494f31; // ASCII ZIO1 отличает этот модуль от scope_core и заводского HDMI. + 12'h004: s_axi_rdata <= 32'h00010000; // Версия ABI 1.0 проверяется ARM до любых аппаратных записей. + 12'h008: s_axi_rdata <= shadow_a; // Позволяем проверить подготовленное первое поле до COMMIT. + 12'h00c: s_axi_rdata <= shadow_b; // Второе shadow-поле доступно тем же способом. + 12'h010: s_axi_rdata <= shadow_outputs; // Возвращаем подготовленную восьмибитную маску DO. + 12'h014: s_axi_rdata <= {24'd0, input_sync}; // Уровни DI являются индивидуально синхронизированными медленными сигналами. + 12'h018: s_axi_rdata <= 0; // Команда не хранится и не повторяется при чтении. + 12'h01c: s_axi_rdata <= field_a; // Активная копия первого поля подтверждает результат COMMIT. + 12'h020: s_axi_rdata <= field_b; // Активная копия второго поля читается отдельно от подготовки следующей записи. + 12'h024: s_axi_rdata <= {24'd0, digital_outputs}; // Активные DO доступны для подтверждения применения всей группы. + default: begin s_axi_rdata <= 0; s_axi_rresp <= SLVERR; end // Reserved и невыровненные чтения явно сообщают об ошибке адреса. + endcase // Данные и код ответа больше не меняются до RREADY. + end // Независимое чтение допускается и при задержанной записи. + end // Завершаем цикл работы при снятом reset. + end // Одна clocked-процедура владеет каждым регистром блока. +endmodule // Доступ к PS Ethernet идёт через ARM-службу, отдельный PL Ethernet MAC не требуется. diff --git a/c/zynq-net/src/zynq_net.c b/c/zynq-net/src/zynq_net.c new file mode 100644 index 0000000..7d7df50 --- /dev/null +++ b/c/zynq-net/src/zynq_net.c @@ -0,0 +1,49 @@ +#include "zynq_net.h" /* Общий заголовок задаёт единственный wire-контракт ARM и клиента. */ +#include /* memcpy и memset работают с фиксированными буферами без динамического выделения. */ +static uint32_t get32(const uint8_t *p) { /* Декодируем сетевой big-endian независимо от endian процессора. */ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; /* Сдвиги соответствуют весу каждого из четырёх октетов. */ +} /* Доступ по байтам не требует выравнивания UDP-буфера. */ +static void put32(uint8_t *p, uint32_t v) { /* Записываем сетевое представление без type-punning и unaligned access. */ + p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16); p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v; /* Первый октет содержит старшие восемь бит значения. */ +} /* Все 32-битные поля проходят один и тот же путь сериализации. */ +static int valid(const ZnetMessage *m) { /* Проверяем структурные ограничения до записи либо принятия пакета. */ + uint32_t op = m->operation & ~ZNET_REPLY; /* Ответ сохраняет исходную операцию в младших семи битах. */ + return m->operation <= 255u && op >= ZNET_DISCOVER && op <= ZNET_WRITE_OUTPUTS && m->target <= ZNET_FPGA && m->inputs <= 255u && m->outputs <= 255u && memchr(m->name, 0, sizeof(m->name)) != NULL; /* Дополнительная операция DO сохраняет 64-байтный ABI, дискреты ограничены восемью битами. */ +} /* Бизнес-ошибки запроса дополнительно проверяются обработчиком цели. */ +int znet_encode(uint8_t *wire, size_t capacity, const ZnetMessage *m) { /* Преобразуем модель ABI в фиксированный UDP payload. */ + if (!wire || !m || capacity < ZNET_SIZE || !valid(m)) return -1; /* Ошибка не оставляет частично записанного кадра. */ + memset(wire, 0, ZNET_SIZE); memcpy(wire, "ZNET", 4); wire[5] = 1; wire[6] = (uint8_t)m->operation; wire[7] = (uint8_t)m->target; /* Magic и 16-битная версия 1 занимают первые шесть байт. */ + put32(wire + 8, m->sequence); put32(wire + 12, m->status); /* Номер запроса сопоставляет асинхронный ответ, status передаёт результат. */ + put32(wire + 16, m->field_a); put32(wire + 20, m->field_b); /* Поля являются uint32 без масштаба и физических единиц. */ + put32(wire + 24, m->inputs); put32(wire + 28, m->outputs); /* Маски дискретов передаются 32-битно для стабильного выравнивания ABI. */ + put32(wire + 32, m->capabilities); put32(wire + 36, m->counter); memcpy(wire + 40, m->name, sizeof(m->name)); /* Последние 24 байта отведены идентификатору устройства. */ + return (int)ZNET_SIZE; /* Возвращаем точное число байт для sendto. */ +} /* Wire-формат не зависит от sizeof структуры. */ +int znet_decode(ZnetMessage *m, const uint8_t *wire, size_t length) { /* Разбираем только полный пакет поддерживаемой версии. */ + ZnetMessage decoded; /* Временная модель сохраняет прежнее значение выхода при неуспешном разборе. */ + if (!m || !wire || length != ZNET_SIZE || memcmp(wire, "ZNET", 4) || wire[4] || wire[5] != 1) return -1; /* Точный размер исключает усечённые пакеты и незаметное принятие будущего ABI. */ + memset(&decoded, 0, sizeof(decoded)); decoded.operation = wire[6]; decoded.target = wire[7]; /* Обнуляем padding, затем заполняем стабильные поля модели. */ + decoded.sequence = get32(wire + 8); decoded.status = get32(wire + 12); /* Значения читаются без требований к выравниванию. */ + decoded.field_a = get32(wire + 16); decoded.field_b = get32(wire + 20); /* Числовые поля сохраняют все 32 бита. */ + decoded.inputs = get32(wire + 24); decoded.outputs = get32(wire + 28); /* Некорректные старшие биты масок позже отклоняет valid. */ + decoded.capabilities = get32(wire + 32); decoded.counter = get32(wire + 36); memcpy(decoded.name, wire + 40, sizeof(decoded.name)); /* Имя проверяется на наличие завершающего нуля. */ + if (!valid(&decoded)) return -1; /* Не публикуем непроверенный пакет вызывающему коду. */ + *m = decoded; return 0; /* Целая модель становится доступной только после успешной проверки. */ +} /* Клиент и сервер используют один разборщик, исключая расхождение offset/endian. */ +uint32_t znet_arm_apply(ZnetArmState *state, const ZnetMessage *request, ZnetMessage *reply) { /* Обрабатываем простое программное хранилище PS. */ + if (!state || !request || !reply || request->target != ZNET_ARM || request->status || request->inputs || request->outputs > 255u || (request->operation != ZNET_READ && request->operation != ZNET_WRITE && request->operation != ZNET_DISCOVER && request->operation != ZNET_WRITE_OUTPUTS)) return ZNET_BAD_REQUEST; /* Принимаем только известные ARM-команды с нулевыми полями ответа и допустимой маской DO. */ + if (request->operation == ZNET_WRITE_OUTPUTS && (request->field_a || request->field_b)) return ZNET_BAD_REQUEST; /* Ненулевые A/B в команде только DO считаются ошибкой клиента, а не потерянной записью счётчиков. */ + if (request->operation == ZNET_WRITE) { state->field_a = request->field_a; state->field_b = request->field_b; state->outputs = request->outputs; } /* Абсолютная запись идемпотентна при потере ответа и повторной отправке. */ + if (request->operation == ZNET_WRITE_OUTPUTS) state->outputs = request->outputs; /* Одно присваивание DO сохраняет последние значения счётчиков даже при нажатии кнопки перед запросом. */ + reply->field_a = state->field_a; reply->field_b = state->field_b; reply->outputs = state->outputs; reply->inputs = state->outputs; /* DI ARM явно являются виртуальным echo DO для проверки первого обмена. */ + return ZNET_OK; /* Метаданные устройства и сетевой ответ добавляет платформенный сервер. */ +} /* Аппаратная PL-цель реализуется отдельным Linux UIO-портом. */ +void znet_arm_key(ZnetArmState *state, uint32_t *held, unsigned key, int value) { /* Чистая логика кнопок проверяется одинаково на Windows и Cortex-A9. */ + if (!state || !held || key >= 4u || (value != 0 && value != 1)) return; /* Игнорируем неизвестные кнопки, повреждённые события и аппаратный autorepeat со значением 2. */ + uint32_t mask = 1u << key; /* Порядок четырёх младших DI совпадает с PS_KEY1, PS_KEY2, PL_KEY1 и PL_KEY2. */ + if (!value) { *held &= ~mask; return; } /* Отпускание очищает DI и разрешает следующий отдельный шаг счётчика. */ + if (*held & mask) return; /* Повторное событие нажатия при удержании не должно повторно менять A/B. */ + *held |= mask; uint32_t *field = key < 2u ? &state->field_a : &state->field_b; /* Первая пара кнопок управляет A, вторая B; DI показывает физическое удержание. */ + if (!(key & 1u)) { if (*field < UINT32_MAX) ++*field; } /* KEY1 увеличивает поле на единицу с насыщением на максимуме uint32, без переполнения. */ + else if (*field) --*field; /* KEY2 уменьшает поле до нуля, не превращая нулевой счётчик в 4294967295. */ +} /* Значения сохраняются в ARM-хранилище и публикуются следующим ответом READ или WRITE_OUTPUTS. */ diff --git a/c/zynq-net/tests/tb_axi_simple_io.sv b/c/zynq-net/tests/tb_axi_simple_io.sv new file mode 100644 index 0000000..e258634 --- /dev/null +++ b/c/zynq-net/tests/tb_axi_simple_io.sv @@ -0,0 +1,49 @@ +`timescale 1ns/1ps // Тест воспроизводит независимые AXI-каналы при периоде clk 10 нс. +module tb_axi_simple_io; // Проверяем реальный RTL учебного банка без поведенческой замены slave. + reg clk=0; always #5 clk=~clk; reg aresetn=0; // Модель 100 МГц и активного низкого reset задаёт границы транзакций. + reg [11:0] awaddr=0, araddr=0; reg awvalid=0,wvalid=0,bready=0,arvalid=0,rready=0; // Адреса и VALID управляются независимо от READY. + reg [31:0] wdata=0; reg [3:0] wstrb=0; reg [7:0] digital_inputs=0; // Payload и входные дискреты изменяет только testbench. + wire awready,wready,bvalid,arready,rvalid; wire [1:0] bresp,rresp; wire [31:0] rdata,field_a,field_b; wire [7:0] digital_outputs; // Наблюдаем протокольные ответы и активные прикладные выходы. + axi_simple_io dut(.clk(clk),.aresetn(aresetn),.s_axi_awaddr(awaddr),.s_axi_awprot(3'd0),.s_axi_awvalid(awvalid),.s_axi_awready(awready),.s_axi_wdata(wdata),.s_axi_wstrb(wstrb),.s_axi_wvalid(wvalid),.s_axi_wready(wready),.s_axi_bresp(bresp),.s_axi_bvalid(bvalid),.s_axi_bready(bready),.s_axi_araddr(araddr),.s_axi_arprot(3'd0),.s_axi_arvalid(arvalid),.s_axi_arready(arready),.s_axi_rdata(rdata),.s_axi_rresp(rresp),.s_axi_rvalid(rvalid),.s_axi_rready(rready),.digital_inputs(digital_inputs),.digital_outputs(digital_outputs),.field_a(field_a),.field_b(field_b)); // Подключаем все AXI-сигналы и восемь DIO. + task automatic address_write(input [11:0] address); begin // Передаём AW независимо от W для проверки обоих порядков прихода. + @(negedge clk); awaddr=address; awvalid=1; do @(posedge clk); while(!awready); @(negedge clk); awvalid=0; // VALID и адрес сохраняются до подтверждённого AW handshake. + end endtask // Возвращаем управление после освобождения канала адреса. + task automatic data_write(input [31:0] data_value,input [3:0] strobes); begin // Отдельный канал позволяет моделировать W раньше AW. + @(negedge clk); wdata=data_value; wstrb=strobes; wvalid=1; do @(posedge clk); while(!wready); @(negedge clk); wvalid=0; // Сохраняем payload и маску до W handshake. + end endtask // DUT теперь должен удерживать данные в своём буфере. + task automatic response_write(input [1:0] expected); begin // Проверяем B backpressure и единственность ответа. + wait(bvalid); repeat(4) begin @(negedge clk); if(!bvalid || bresp!==expected || awready || wready) $fatal(1,"B hold/buffering"); end // При задержке master ответ остаётся стабильным, новые AW/W не принимаются. + bready=1; @(posedge clk); @(negedge clk); bready=0; if(bvalid) $fatal(1,"duplicated B response"); // Принятый ответ снимается без повторения записи. + end endtask // Следующая транзакция может использовать освободившиеся буферы. + task automatic write_word(input [11:0] address,input [31:0] value,input [3:0] strobes,input integer order,input [1:0] expected); begin // order выбирает AW-before-W, W-before-AW либо одновременную передачу. + if(order==0) begin address_write(address); repeat(2) @(posedge clk); data_write(value,strobes); end // Проверяем хранение одного AW при задержанном W. + else if(order==1) begin data_write(value,strobes); repeat(2) @(posedge clk); address_write(address); end // Проверяем хранение одного W при задержанном AW. + else fork address_write(address); data_write(value,strobes); join // Оба VALID могут быть подтверждены одним фронтом. + response_write(expected); // Каждая пара должна породить ровно один B с ожидаемым status. + end endtask // Одинаковые проверки ответа применяются ко всем порядкам. + task automatic read_word(input [11:0] address,input [31:0] expected,input [1:0] response); reg [31:0] held; begin // Read response проверяется также при изменении живых входов. + @(negedge clk); araddr=address; arvalid=1; do @(posedge clk); while(!arready); @(negedge clk); arvalid=0; wait(rvalid); held=rdata; // Принимаем AR, затем задерживаем RREADY. + if(held!==expected || rresp!==response) $fatal(1,"read %h got %h/%h expected %h/%h",address,rdata,rresp,expected,response); // Сверяем фактическую карту и ошибочные адреса. + repeat(3) begin @(negedge clk); digital_inputs=~digital_inputs; if(!rvalid || rdata!==held || rresp!==response) $fatal(1,"R hold"); end // Live DI не должны менять уже выданные данные. + rready=1; @(posedge clk); @(negedge clk); rready=0; // Завершаем чтение только после проверки стабильности. + end endtask // Следующий AR допускается после снятия RVALID. + initial begin // Последовательность охватывает ABI, masked writes, COMMIT и reset незавершённой пары. + repeat(3) @(posedge clk); @(negedge clk); aresetn=1; // Выходим из reset вдали от активного фронта DUT. + read_word(0,32'h5a494f31,0); read_word(4,32'h00010000,0); // Аппаратная идентификация совпадает с Linux UIO-портом. + write_word('h008,32'h12345678,4'hf,0,0); write_word('h00c,32'hffffffff,4'hf,1,0); write_word('h010,32'ha5,4'hf,2,0); // Три порядка AW/W готовят полный shadow-набор. + if(field_a!==0 || field_b!==0 || digital_outputs!==0) $fatal(1,"shadow leaked before commit"); // Отдельные записи не изменяют активный набор до COMMIT. + write_word('h008,32'h0000ab00,4'b0010,2,0); read_word('h008,32'h1234ab78,0); // WSTRB изменяет только второй байт первого поля. + write_word('h018,1,0,0,0); if(field_a!==0) $fatal(1,"zero strobe committed"); // Нулевая маска байтов должна быть no-op даже для команды 1. + write_word('h018,2,1,1,2); if(field_a!==0) $fatal(1,"bad command committed"); // Неизвестная команда отклоняется без прикладного эффекта. + write_word('h018,1,1,2,0); if(field_a!==32'h1234ab78 || field_b!==32'hffffffff || digital_outputs!==8'ha5) $fatal(1,"atomic commit"); // Все три активных значения обновляются одним фронтом команды. + read_word('h01c,32'h1234ab78,0); read_word('h020,32'hffffffff,0); read_word('h024,32'ha5,0); // Активные readbacks подтверждают применение shadow-набора. + @(negedge clk); digital_inputs=8'h3c; repeat(3) @(posedge clk); read_word('h014,32'h3c,0); // Медленные DI проходят через две ступени synchronizer перед чтением. + write_word(0,0,15,2,2); write_word('h014,0,15,0,2); write_word('h009,0,15,1,2); // Read-only ID/DI и невыровненная запись обязаны вернуть SLVERR. + read_word('h028,0,2); read_word('h005,0,2); // Reserved и невыровненное чтение не образуют alias корректных регистров. + address_write('h008); @(negedge clk); aresetn=0; repeat(2) @(posedge clk); @(negedge clk); aresetn=1; // Reset отменяет принятый AW без W и сбрасывает активные выходы. + if(field_a!==0 || field_b!==0 || digital_outputs!==0 || bvalid || rvalid) $fatal(1,"reset state"); // Никакая старая транзакция не переживает reset. + write_word('h00c,17,15,2,0); read_word('h008,0,0); read_word('h00c,17,0); // Следующая W не связывается с адресом до сброса. + $display("PASS tb_axi_simple_io AXI_AW_W_order_strobes_backpressure_DI_atomic_commit_errors_reset"); $finish; // PASS относится к исполнившейся симуляции реального slave. + end // Завершаем сценарий при отсутствии ошибок. + initial begin #100000; $fatal(1,"AXI timeout"); end // Watchdog 100 мкс превращает протокольное зависание в явный FAIL. +endmodule // Существующий tests/run_hdl.ps1 автоматически включает этот testbench в общий регресс. diff --git a/c/zynq-net/tests/test_zynq_net.c b/c/zynq-net/tests/test_zynq_net.c new file mode 100644 index 0000000..bc5f6e7 --- /dev/null +++ b/c/zynq-net/tests/test_zynq_net.c @@ -0,0 +1,36 @@ +#include "zynq_net.h" /* Проверяем тот же C-кодек, который связывается с ARM-службой и ctypes. */ +#include /* Проверки остаются активными благодаря -UNDEBUG в сборочном сценарии. */ +#include /* PASS marker позволяет отличить выполненные проверки от одной успешной компиляции. */ +#include /* Эталонные байты и состояние сравниваются без повторной реализации кодека. */ +int main(void) { /* Проверки не требуют ОС, сокетов либо FPGA. */ + uint8_t wire[ZNET_SIZE + 1], saved[ZNET_SIZE]; ZnetMessage input = {0}, output = {0}, reply = {0}; ZnetArmState state = {0}; /* Изолированные буферы позволяют проверить ошибки и неизменность состояния. */ + input.operation = ZNET_WRITE; input.target = ZNET_ARM; input.sequence = 0x12345678u; input.field_a = 0xffffffffu; input.field_b = 0x01020304u; input.outputs = 0xa5; /* Граничное поле, несимметричные endian-байты и разноимённые биты DO. */ + assert(znet_encode(wire, sizeof(wire), &input) == 64); /* ABI v1 обязан вернуть ровно 64 байта. */ + const uint8_t prefix[] = {'Z','N','E','T',0,1,3,0,0x12,0x34,0x56,0x78,0,0,0,0,0xff,0xff,0xff,0xff,1,2,3,4,0,0,0,0,0,0,0,0xa5}; /* Независимый wire-вектор фиксирует magic, version, op, endian и offsets. */ + assert(!memcmp(wire, prefix, sizeof(prefix))); memcpy(saved, wire, ZNET_SIZE); /* Roundtrip сам по себе недостаточен: проверяем конкретные эталонные байты. */ + assert(!znet_decode(&output, wire, ZNET_SIZE)); assert(output.sequence == input.sequence && output.field_a == 0xffffffffu && output.field_b == 0x01020304u && output.outputs == 0xa5); /* uint32 сохраняет все биты на обеих архитектурах. */ + assert(znet_arm_apply(&state, &output, &reply) == ZNET_OK && reply.inputs == 0xa5 && reply.outputs == 0xa5); /* Виртуальные DI документированно повторяют DO. */ + assert(znet_arm_apply(&state, &output, &reply) == ZNET_OK && state.outputs == 0xa5); /* Повтор WRITE задаёт состояние, а не переключает его второй раз. */ + output.operation = ZNET_READ; output.field_a = 0; output.field_b = 0; output.outputs = 0; assert(znet_arm_apply(&state, &output, &reply) == ZNET_OK && reply.field_a == 0xffffffffu && reply.field_b == 0x01020304u); /* READ не изменяет ранее записанные значения. */ + ZnetArmState before = state; output.operation = ZNET_WRITE | ZNET_REPLY; assert(znet_arm_apply(&state, &output, &reply) == ZNET_BAD_REQUEST && !memcmp(&state, &before, sizeof(state))); /* Ответ, ошибочно пришедший как запрос, не меняет хранилище. */ + output.operation = ZNET_WRITE; output.status = 1; assert(znet_arm_apply(&state, &output, &reply) == ZNET_BAD_REQUEST); output.status = 0; output.inputs = 1; assert(znet_arm_apply(&state, &output, &reply) == ZNET_BAD_REQUEST); /* Клиенту запрещено задавать status и DI. */ + assert(znet_decode(&output, wire, 63) < 0 && znet_decode(&output, wire, 65) < 0); /* Ни усечённый, ни расширенный пакет не принимается как v1. */ + wire[0] ^= 1; assert(znet_decode(&output, wire, 64) < 0); memcpy(wire, saved, 64); /* Чужое magic не интерпретируется как команда устройства. */ + wire[5] = 2; assert(znet_decode(&output, wire, 64) < 0); memcpy(wire, saved, 64); /* Неизвестная версия отклоняется без предположения о совместимости. */ + wire[6] = 5; assert(znet_decode(&output, wire, 64) < 0); memcpy(wire, saved, 64); /* После добавления WRITE_OUTPUTS=4 первой неизвестной операцией является 5. */ + wire[7] = 2; assert(znet_decode(&output, wire, 64) < 0); memcpy(wire, saved, 64); /* Разрешены только ARM=0 и FPGA=1. */ + wire[28] = 1; assert(znet_decode(&output, wire, 64) < 0); memcpy(wire, saved, 64); /* Старшие биты восьмиканальных DO не могут проникнуть в обработчик. */ + memset(wire + 40, 'x', 24); assert(znet_decode(&output, wire, 64) < 0); /* Неограниченное строковое чтение исключается проверкой терминатора имени. */ + assert(znet_encode(wire, 63, &input) < 0 && znet_decode(NULL, wire, 64) < 0 && znet_encode(NULL, 64, &input) < 0); /* Ошибки размера и указателей детерминированно возвращают отказ. */ + state.field_a = 10; state.field_b = 20; uint32_t held = 0; znet_arm_key(&state, &held, 0, 1); assert(state.field_a == 11 && held == 1); /* PS_KEY1 увеличивает только A и устанавливает соответствующий физический DI. */ + znet_arm_key(&state, &held, 0, 2); znet_arm_key(&state, &held, 0, 1); assert(state.field_a == 11); /* Autorepeat и дублированный фронт удержанной кнопки не создают дополнительных шагов. */ + znet_arm_key(&state, &held, 0, 0); znet_arm_key(&state, &held, 0, 1); znet_arm_key(&state, &held, 1, 1); assert(state.field_a == 11 && held == 3); /* Отпускание разрешает новый шаг A+, а независимая PS_KEY2 выполняет A−. */ + znet_arm_key(&state, &held, 2, 1); znet_arm_key(&state, &held, 3, 1); assert(state.field_b == 20 && held == 15); /* PL_KEY1/2 меняют B на +1/−1 и используют отдельные DI2/3. */ + state.field_a = UINT32_MAX; held = 0; znet_arm_key(&state, &held, 0, 1); assert(state.field_a == UINT32_MAX); /* Инкремент насыщается на максимуме uint32 без переполнения в ноль. */ + state.field_b = 0; znet_arm_key(&state, &held, 3, 1); assert(state.field_b == 0); /* Декремент нулевого счётчика не превращает его в максимальное значение. */ + before = state; uint32_t saved_held = held; znet_arm_key(&state, &held, 4, 1); znet_arm_key(&state, &held, 0, -1); assert(!memcmp(&state, &before, sizeof(state)) && held == saved_held); /* Неизвестные индексы и повреждённые значения события не меняют состояние. */ + input.operation = ZNET_WRITE_OUTPUTS; input.field_a = 0; input.field_b = 0; input.outputs = 5; assert(znet_encode(wire, sizeof(wire), &input) == 64 && !znet_decode(&output, wire, 64)); /* Новая операция проходит тот же 64-байтный ABI без изменения версии и смещений. */ + assert(znet_arm_apply(&state, &output, &reply) == ZNET_OK && reply.field_a == UINT32_MAX && reply.field_b == 0 && reply.outputs == 5); /* Запись LED не затирает значения A/B, которые могли измениться независимо от черновика GUI. */ + output.field_a = 1; before = state; assert(znet_arm_apply(&state, &output, &reply) == ZNET_BAD_REQUEST && !memcmp(&state, &before, sizeof(state))); /* Ненулевой зарезервированный A в команде DO отклоняется до изменения состояния. */ + puts("PASS ZNET: golden bytes, uint32, malformed packets, idempotent write, button edges and saturation, output-only preservation"); return 0; /* Итоговый marker печатается после старых проверок ABI и новых сценариев реальных кнопок. */ +} /* Тест собирается для Windows host и native ARM Linux из одного файла. */ diff --git a/hdl/logic-analyzer/README.md b/hdl/logic-analyzer/README.md index 39c97f4..2eaad19 100644 --- a/hdl/logic-analyzer/README.md +++ b/hdl/logic-analyzer/README.md @@ -21,3 +21,49 @@ Example Quartus source assignment: ```tcl set_global_assignment -name VERILOG_FILE path/to/templates/hdl/logic-analyzer/rtl/la_capture_buffer.v ``` + +## Буфер с аппаратным триггером + +`rtl/la_trigger_buffer.v` принимает восемь синхронизированных входов. +После `arm` первый отсчёт, удовлетворяющий маскам уровня и фронтов, +попадает в адрес 0. Нулевые маски включают немедленную запись. +`divider` задаёт шаг в тактах (минимум 1), `last_address` — последний адрес +записи включительно. По умолчанию глубина равна 65 536 отсчётам. + +Модуль использует `rtl/la_capture_buffer.v`. После заполнения BRAM +он выдаёт отсчёты через `data_valid` и `ready`. Задержки читателя +влияют только на выгрузку. `stop` отменяет ожидание, запись или +выгрузку. Вход `clk` общий для обоих портов BRAM. + +При `EXTERNAL_TRIGGER=1` и `use_external=1` вход `pretrigger` +задаёт число отсчётов перед срабатыванием. Блок собирает их в кольце +до разрешения `trigger_tick`. Подключите `external_hit` к +`rtl/la_advanced_trigger.v` и подайте на этот блок тот же отсчёт и +`trigger_tick`. Он выполняет до 16 стадий или сопоставляет биты +Serial Trigger по фронтам Clock между флагами Start и Stop. + +`rtl/la_run_encoder.v` запускается по тому же типу триггера и кодирует +непрерывный поток. На каждый непрерывный уровень выдаёт два байта: +значение и число отсчётов минус один. Допустимая длина серии 1–256 +отсчётов. Готовность выхода влияет на выдачу. При переполнении +очереди выставляется `overflow`, такт выборки сохраняется. + +## Блоки AXI-захвата + +- `rtl/axi_logic_analyzer.sv` принимает восемь входов и сохраняет окно в + BRAM. Управляющий AXI4-Lite и входной синхронизатор работают в домене + `clk`. `CLOCK_HZ` передаёт фактическую частоту в регистр метаданных. + `ADDR_WIDTH` задаёт глубину BRAM. +- `rtl/axi_logic_analyzer_bd.v` выводит интерфейсы в Vivado Block Design. + Для другого домена управления подключите AXI Clock Converter. +- `rtl/zynq_la_stream.v` пишет 8-битные отсчёты в кольцо DDR через + 64-битный AXI4. Выделите 8 МиБ по параметру `DDR_BASE` и исключите + диапазон из общего распределителя DDR. Читатель подтверждает целые + блоки счётчиком CONSUMED. При отставании читателя блок выставляет ошибку. +- `include/zynq_la.h` и `include/zynq_la_stream.h` описывают регистры + и форматы Ethernet-сообщений ZLA1 и ZLS1. + +Для буферного блока добавьте [vfd_pwm.sv](../vfd-pwm/rtl/vfd_pwm.sv) +и `la_capture_buffer.v`. Папка `tests` содержит проверки BRAM, фронтов, +переполнения и выдачи DDR через модель AXI. Привязку пинов, регистровые +адреса AXI и допустимую частоту определяет проект платы. diff --git a/hdl/logic-analyzer/include/zynq_la.h b/hdl/logic-analyzer/include/zynq_la.h new file mode 100644 index 0000000..a6a32da --- /dev/null +++ b/hdl/logic-analyzer/include/zynq_la.h @@ -0,0 +1,24 @@ +#ifndef ZYNQ_LA_H +#define ZYNQ_LA_H +#include +/* All wire words are little endian. One request per TCP connection, port 37572. + * Request: magic, version, count, divider, level_mask, level_value, edge_mask, + * edge_value, test_enable, test_divider, test_duty (11 uint32 = 44 bytes). + * test_enable: 0 off, 1 PWM duty/256, 2 fast binary divided square waves. + * Reply: magic, version, status, clock_hz, divider, count, channels, flags + * (8 uint32 = 32 bytes), then count uint8 samples on success. + * flags bit 0: bounded snapshot, gaps between requests. No pre-trigger samples. + * Status: 0 success, 1 invalid request, 2 capture timeout, 3 hardware fault. + */ +#define ZLA_MAGIC UINT32_C(0x31414c5a) +#define ZLA_REQUEST UINT32_C(0x52414c5a) +#define ZLA_PORT 37572 +#define ZLA_MAX_SAMPLES 65536 +enum zla_register { + ZLA_ID=0, ZLA_STATUS=4, ZLA_CLOCK=8, ZLA_DEPTH=12, + ZLA_COUNT=16, ZLA_DIVIDER=20, ZLA_LEVEL_MASK=24, ZLA_LEVEL_VALUE=28, + ZLA_EDGE_MASK=32, ZLA_EDGE_VALUE=36, ZLA_COMMAND=40, ZLA_INDEX=44, + ZLA_DATA=48, ZLA_CAPTURED=52, ZLA_TEST_DIVIDER=56, + ZLA_TEST_DUTY=60, ZLA_TEST_ENABLE=64 +}; +#endif diff --git a/hdl/logic-analyzer/include/zynq_la_stream.h b/hdl/logic-analyzer/include/zynq_la_stream.h new file mode 100644 index 0000000..d55cef8 --- /dev/null +++ b/hdl/logic-analyzer/include/zynq_la_stream.h @@ -0,0 +1,23 @@ +#ifndef ZYNQ_LA_STREAM_H +#define ZYNQ_LA_STREAM_H +#include + +#define ZLS_MAGIC UINT32_C(0x31534c5a) +#define ZLS_PORT 37574 +#define ZLS_CLOCK_HZ UINT32_C(100000000) +#define ZLS_BLOCK UINT32_C(16384) +#define ZLS_SLOTS UINT32_C(512) +#define ZLS_DDR_BASE UINT32_C(0x3e000000) +#define ZLS_REG_BASE UINT32_C(0x43c40000) + +/* Little endian. Request: magic, version, divider, reserved. + * Header: magic, version, error, clock, divider, block bytes, channels, flags. + * Then frames: sequence, length, error, followed by length sample bytes. + * A zero length denotes a terminal error. Sequence starts at zero. + */ +enum zls_reg { + ZLS_ID=0, ZLS_STATUS=4, ZLS_CLOCK=8, ZLS_BLOCK_SIZE=12, + ZLS_DIVIDER=16, ZLS_COMMAND=20, ZLS_PRODUCED=24, ZLS_CONSUMED=28 +}; +enum zls_error { ZLS_BAD_REQUEST=1, ZLS_OVERFLOW=2, ZLS_DDR_ERROR=3, ZLS_NOT_READY=4 }; +#endif diff --git a/hdl/logic-analyzer/rtl/axi_logic_analyzer.sv b/hdl/logic-analyzer/rtl/axi_logic_analyzer.sv new file mode 100644 index 0000000..2ba03c7 --- /dev/null +++ b/hdl/logic-analyzer/rtl/axi_logic_analyzer.sv @@ -0,0 +1,204 @@ +`timescale 1ns/1ps +// Eight asynchronous digital inputs, bounded BRAM capture, AXI4-Lite readout. +// AXI and sampling intentionally share clk: no multi-bit CDC in the control path. +module axi_logic_analyzer #( + parameter integer CLOCK_HZ = 100000000, + parameter integer ADDR_WIDTH = 16 +)( + (* X_INTERFACE_INFO = "xilinx.com:signal:clock:1.0 clk CLK", X_INTERFACE_PARAMETER = "ASSOCIATED_BUSIF s_axi, ASSOCIATED_RESET aresetn" *) input wire clk, + (* X_INTERFACE_INFO = "xilinx.com:signal:reset:1.0 aresetn RST", X_INTERFACE_PARAMETER = "POLARITY ACTIVE_LOW" *) input wire aresetn, + input wire [11:0] s_axi_awaddr, input wire [2:0] s_axi_awprot, + input wire s_axi_awvalid, output wire s_axi_awready, + input wire [31:0] s_axi_wdata, input wire [3:0] s_axi_wstrb, + input wire s_axi_wvalid, output wire s_axi_wready, + output reg [1:0] s_axi_bresp, output reg s_axi_bvalid, input wire s_axi_bready, + input wire [11:0] s_axi_araddr, input wire [2:0] s_axi_arprot, + input wire s_axi_arvalid, output wire s_axi_arready, + output reg [31:0] s_axi_rdata, output reg [1:0] s_axi_rresp, + output reg s_axi_rvalid, input wire s_axi_rready, + input wire [7:0] digital_inputs, + output wire [7:0] test_outputs +); + localparam integer DEPTH = 1 << ADDR_WIDTH; + (* ASYNC_REG = "TRUE" *) reg [7:0] input_meta, input_sync; + reg [7:0] previous; + reg busy, done, triggered, previous_valid; + reg [2:0] settle; + reg [31:0] count_cfg, captured; + reg [15:0] divider, ticks; + reg [7:0] level_mask, level_value, edge_mask, edge_value; + reg [ADDR_WIDTH-1:0] read_index; + reg [15:0] test_divider, test_ticks, test_phase; + reg [8:0] test_duty; + reg [1:0] test_enable; + reg vf_start, vf_off, vf_stop, vf_apply, vf_heartbeat; + reg signed [31:0] vf_target_cfg; + reg [31:0] vf_up, vf_down; + reg [15:0] vf_top, vf_dead, vf_mod; + wire vf_enabled, vf_fault; + wire signed [31:0] vf_actual, vf_target; + wire [5:0] vf_gates; + localparam integer VF_MAX_STEP=21474836; // 500 Hz at a 100 kHz DDS tick. + wire vf_valid=(CLOCK_HZ%100000==0 && vf_target_cfg>=-VF_MAX_STEP && vf_target_cfg<=VF_MAX_STEP && + vf_up>=1 && vf_up<=VF_MAX_STEP && vf_down>=1 && vf_down<=VF_MAX_STEP && + vf_top>=CLOCK_HZ/100000 && vf_top<=CLOCK_HZ/2000 && vf_top>=4 && + vf_dead>=1 && vf_dead<=vf_top/4 && vf_mod<=29491); + vfd_pwm #(.CLOCK_HZ(CLOCK_HZ)) vfd ( + .clk(clk), .aresetn(aresetn), .start(vf_start), .off(vf_off), + .ramp_stop(vf_stop), .apply_target(vf_apply), .heartbeat(vf_heartbeat), + .target_cfg(vf_target_cfg), .up_cfg(vf_up), .down_cfg(vf_down), + .top_cfg(vf_top), .dead_cfg(vf_dead), .mod_cfg(vf_mod), + .enabled(vf_enabled), .fault(vf_fault), .actual(vf_actual), + .target(vf_target), .gates(vf_gates) + ); + wire [7:0] legacy_outputs; + assign test_outputs = vf_enabled ? {2'b0,vf_gates} : legacy_outputs; + genvar ch; + generate for (ch = 0; ch < 8; ch = ch + 1) begin: pwm + assign legacy_outputs[ch] = (test_enable == 2) ? test_phase[ch] : + ((test_enable == 1) && ({1'b0, test_phase[ch +: 8]} < test_duty)); + end endgenerate + wire [7:0] read_sample; + wire match_level = ((input_sync & level_mask) == (level_value & level_mask)); + wire match_edge = (edge_mask == 0) || (previous_valid && + (((input_sync ^ previous) & edge_mask) == edge_mask) && + ((input_sync & edge_mask) == (edge_value & edge_mask))); + wire sample_tick = busy && (settle == 0) && (ticks == 0); + wire store_sample = sample_tick && (triggered || (match_level && match_edge)); + // Reuse the portable capture memory from templates. No reset on the RAM. + la_capture_buffer #(.SAMPLE_WIDTH(8), .ADDR_WIDTH(ADDR_WIDTH)) memory ( + .wr_clk(clk), .wr_en(aresetn && store_sample), + .wr_addr(captured[ADDR_WIDTH-1:0]), .wr_data(input_sync), + .rd_clk(clk), .rd_addr(read_index), .rd_data(read_sample) + ); + reg aw_pending, w_pending; + reg [11:0] write_address; + reg [31:0] write_data; + reg [3:0] write_strobes; + assign s_axi_awready = aresetn && !aw_pending && !s_axi_bvalid; + assign s_axi_wready = aresetn && !w_pending && !s_axi_bvalid; + assign s_axi_arready = aresetn && !s_axi_rvalid; + always @(posedge clk) begin + if (!aresetn) begin + input_meta <= 0; input_sync <= 0; previous <= 0; + busy <= 0; done <= 0; triggered <= 0; previous_valid <= 0; + settle <= 0; ticks <= 0; captured <= 0; + count_cfg <= DEPTH; divider <= 0; + level_mask <= 0; level_value <= 0; edge_mask <= 0; edge_value <= 0; + read_index <= 0; aw_pending <= 0; w_pending <= 0; + test_divider <= 0; test_ticks <= 0; test_phase <= 0; + test_duty <= 128; test_enable <= 0; + vf_start<=0; vf_off<=0; vf_stop<=0; vf_apply<=0; vf_heartbeat<=0; + vf_target_cfg<=0; vf_up<=1; vf_down<=1; + vf_top<=2500; vf_dead<=100; vf_mod<=16384; + write_address <= 0; write_data <= 0; write_strobes <= 0; + s_axi_bvalid <= 0; s_axi_bresp <= 0; + s_axi_rvalid <= 0; s_axi_rresp <= 0; s_axi_rdata <= 0; + end else begin + vf_start<=0; vf_off<=0; vf_stop<=0; vf_apply<=0; vf_heartbeat<=0; + input_meta <= digital_inputs; input_sync <= input_meta; + if (!test_enable) begin test_ticks <= 0; test_phase <= 0; end + else if (test_ticks == 0) begin test_ticks <= test_divider; test_phase <= test_phase + 1'b1; end + else test_ticks <= test_ticks - 1'b1; + if (busy) begin + if (settle != 0) settle <= settle - 1'b1; + else if (ticks != 0) ticks <= ticks - 1'b1; + else begin + ticks <= divider; previous <= input_sync; previous_valid <= 1; + if (store_sample) begin + triggered <= 1; captured <= captured + 1; + if (captured == count_cfg - 1) begin busy <= 0; done <= 1; end + end + end + end + if (s_axi_bvalid && s_axi_bready) s_axi_bvalid <= 0; + if (s_axi_rvalid && s_axi_rready) s_axi_rvalid <= 0; + if (s_axi_awvalid && s_axi_awready) begin + write_address <= s_axi_awaddr; aw_pending <= 1; + end + if (s_axi_wvalid && s_axi_wready) begin + write_data <= s_axi_wdata; write_strobes <= s_axi_wstrb; w_pending <= 1; + end + if (aw_pending && w_pending && !s_axi_bvalid) begin + aw_pending <= 0; w_pending <= 0; s_axi_bvalid <= 1; s_axi_bresp <= 2; + // Explicit full-word register ABI; zero strobes are a no-op. + if (write_strobes == 0) s_axi_bresp <= 0; + else if (write_strobes == 15) begin + if (write_address >= 'h48 && write_address <= 'h60) begin + case(write_address) + 'h48: case(write_data) + 1: if(!vf_enabled && vf_valid) begin vf_start<=1; test_enable<=0; s_axi_bresp<=0; end + 2: begin vf_off<=1; s_axi_bresp<=0; end + 3: begin vf_stop<=1; s_axi_bresp<=0; end + 4: begin vf_heartbeat<=1; s_axi_bresp<=0; end + 5: if(vf_enabled && vf_target_cfg>=-VF_MAX_STEP && vf_target_cfg<=VF_MAX_STEP) begin vf_apply<=1; s_axi_bresp<=0; end + default: ; + endcase + 'h4c: if($signed(write_data)>=-VF_MAX_STEP && $signed(write_data)<=VF_MAX_STEP) begin vf_target_cfg<=write_data; s_axi_bresp<=0; end + 'h50: if(!vf_enabled && write_data>=1 && write_data<=VF_MAX_STEP) begin vf_up<=write_data; s_axi_bresp<=0; end + 'h54: if(!vf_enabled && write_data>=1 && write_data<=VF_MAX_STEP) begin vf_down<=write_data; s_axi_bresp<=0; end + 'h58: if(!vf_enabled && write_data>=4 && write_data<=65535) begin vf_top<=write_data[15:0]; s_axi_bresp<=0; end + 'h5c: if(!vf_enabled && write_data>=1 && write_data<=65535) begin vf_dead<=write_data[15:0]; s_axi_bresp<=0; end + 'h60: if(!vf_enabled && write_data<=29491) begin vf_mod<=write_data[15:0]; s_axi_bresp<=0; end + default: ; + endcase + end else if (write_address == 'h28 && write_data == 2) begin + busy <= 0; done <= 0; triggered <= 0; captured <= 0; + s_axi_bresp <= 0; + end else if (!busy) begin + case (write_address) + 'h10: if (write_data >= 1 && write_data <= DEPTH) begin count_cfg <= write_data; s_axi_bresp <= 0; end + 'h14: if (write_data <= 65535) begin divider <= write_data[15:0]; s_axi_bresp <= 0; end + 'h18: if (write_data <= 255) begin level_mask <= write_data[7:0]; s_axi_bresp <= 0; end + 'h1c: if (write_data <= 255) begin level_value <= write_data[7:0]; s_axi_bresp <= 0; end + 'h20: if (write_data <= 255) begin edge_mask <= write_data[7:0]; s_axi_bresp <= 0; end + 'h24: if (write_data <= 255) begin edge_value <= write_data[7:0]; s_axi_bresp <= 0; end + 'h28: if (write_data == 1) begin + busy <= 1; done <= 0; triggered <= 0; captured <= 0; + ticks <= 0; settle <= 3; previous_valid <= 0; s_axi_bresp <= 0; + end + 'h2c: if (write_data < DEPTH) begin read_index <= write_data[ADDR_WIDTH-1:0]; s_axi_bresp <= 0; end + 'h38: if (write_data <= 65535) begin test_divider <= write_data[15:0]; s_axi_bresp <= 0; end + 'h3c: if (write_data <= 256) begin test_duty <= write_data[8:0]; s_axi_bresp <= 0; end + 'h40: if (write_data <= 2 && (!vf_enabled || write_data==0)) begin test_enable <= write_data[1:0]; s_axi_bresp <= 0; end + default: ; + endcase + end + end + end + if (s_axi_arvalid && s_axi_arready) begin + s_axi_rvalid <= 1; s_axi_rresp <= 0; s_axi_rdata <= 0; + case (s_axi_araddr) + 'h00: s_axi_rdata <= 32'h31414c5a; // little-endian "ZLA1" + 'h04: s_axi_rdata <= {29'b0, triggered, done, busy}; + 'h08: s_axi_rdata <= CLOCK_HZ; + 'h0c: s_axi_rdata <= DEPTH; + 'h10: s_axi_rdata <= count_cfg; + 'h14: s_axi_rdata <= {16'b0, divider}; + 'h18: s_axi_rdata <= {24'b0, level_mask}; + 'h1c: s_axi_rdata <= {24'b0, level_value}; + 'h20: s_axi_rdata <= {24'b0, edge_mask}; + 'h24: s_axi_rdata <= {24'b0, edge_value}; + 'h2c: s_axi_rdata <= read_index; + 'h30: if (done && read_index < captured) s_axi_rdata <= {24'b0, read_sample}; else s_axi_rresp <= 2; + 'h34: s_axi_rdata <= captured; + 'h38: s_axi_rdata <= {16'b0, test_divider}; + 'h3c: s_axi_rdata <= {23'b0, test_duty}; + 'h40: s_axi_rdata <= {30'b0, test_enable}; + 'h44: s_axi_rdata <= 32'h3146565a; // ZVF1 capability + 'h4c: s_axi_rdata <= vf_target_cfg; + 'h50: s_axi_rdata <= vf_up; + 'h54: s_axi_rdata <= vf_down; + 'h58: s_axi_rdata <= {16'b0,vf_top}; + 'h5c: s_axi_rdata <= {16'b0,vf_dead}; + 'h60: s_axi_rdata <= {16'b0,vf_mod}; + 'h64: s_axi_rdata <= {29'b0,(vf_actual!=vf_target),vf_fault,vf_enabled}; + 'h68: s_axi_rdata <= vf_actual; + 'h6c: s_axi_rdata <= 100000; + 'h70: s_axi_rdata <= vf_target; + default: s_axi_rresp <= 2; + endcase + end + end + end +endmodule diff --git a/hdl/logic-analyzer/rtl/axi_logic_analyzer_bd.v b/hdl/logic-analyzer/rtl/axi_logic_analyzer_bd.v new file mode 100644 index 0000000..a752bac --- /dev/null +++ b/hdl/logic-analyzer/rtl/axi_logic_analyzer_bd.v @@ -0,0 +1,47 @@ +`timescale 1ns/1ps +// Eight asynchronous digital inputs, bounded BRAM capture, AXI4-Lite readout. +// AXI and sampling intentionally share clk: no multi-bit CDC in the control path. +module axi_logic_analyzer_bd #( + parameter integer CLOCK_HZ = 100000000, + parameter integer ADDR_WIDTH = 16 +)( + (* X_INTERFACE_INFO = "xilinx.com:signal:clock:1.0 clk CLK", X_INTERFACE_PARAMETER = "ASSOCIATED_BUSIF s_axi, ASSOCIATED_RESET aresetn" *) input wire clk, + (* X_INTERFACE_INFO = "xilinx.com:signal:reset:1.0 aresetn RST", X_INTERFACE_PARAMETER = "POLARITY ACTIVE_LOW" *) input wire aresetn, + input wire [11:0] s_axi_awaddr, input wire [2:0] s_axi_awprot, + input wire s_axi_awvalid, output wire s_axi_awready, + input wire [31:0] s_axi_wdata, input wire [3:0] s_axi_wstrb, + input wire s_axi_wvalid, output wire s_axi_wready, + output wire [1:0] s_axi_bresp, output wire s_axi_bvalid, input wire s_axi_bready, + input wire [11:0] s_axi_araddr, input wire [2:0] s_axi_arprot, + input wire s_axi_arvalid, output wire s_axi_arready, + output wire [31:0] s_axi_rdata, output wire [1:0] s_axi_rresp, + output wire s_axi_rvalid, input wire s_axi_rready, + input wire [7:0] digital_inputs, + output wire [7:0] test_outputs +); + axi_logic_analyzer #(.CLOCK_HZ(CLOCK_HZ), .ADDR_WIDTH(ADDR_WIDTH)) core ( + .clk(clk), + .aresetn(aresetn), + .s_axi_awaddr(s_axi_awaddr), + .s_axi_awprot(s_axi_awprot), + .s_axi_awvalid(s_axi_awvalid), + .s_axi_awready(s_axi_awready), + .s_axi_wdata(s_axi_wdata), + .s_axi_wstrb(s_axi_wstrb), + .s_axi_wvalid(s_axi_wvalid), + .s_axi_wready(s_axi_wready), + .s_axi_bresp(s_axi_bresp), + .s_axi_bvalid(s_axi_bvalid), + .s_axi_bready(s_axi_bready), + .s_axi_araddr(s_axi_araddr), + .s_axi_arprot(s_axi_arprot), + .s_axi_arvalid(s_axi_arvalid), + .s_axi_arready(s_axi_arready), + .s_axi_rdata(s_axi_rdata), + .s_axi_rresp(s_axi_rresp), + .s_axi_rvalid(s_axi_rvalid), + .s_axi_rready(s_axi_rready), + .digital_inputs(digital_inputs), + .test_outputs(test_outputs) + ); +endmodule diff --git a/hdl/logic-analyzer/rtl/la_advanced_trigger.v b/hdl/logic-analyzer/rtl/la_advanced_trigger.v new file mode 100644 index 0000000..18200b7 --- /dev/null +++ b/hdl/logic-analyzer/rtl/la_advanced_trigger.v @@ -0,0 +1,98 @@ +`timescale 1ns / 1ps +// Eight-channel DSView Stage and Serial triggers. Each pattern contains +// level mask, expected value, and required-change mask in ascending bytes. +module la_advanced_trigger ( + input wire clk, reset, arm, tick, + input wire [7:0] sample, + input wire serial_mode, + input wire [3:0] last_stage, + input wire config_write, + input wire [3:0] config_stage, config_flags, + input wire [23:0] config_a, config_b, + input wire [31:0] config_count, + output wire hit +); + reg [83:0] stages [0:15]; + reg [83:0] condition; + reg [3:0] stage, last; + reg [31:0] count; + reg serial; + reg [7:0] previous; + reg previous_valid; + reg [23:0] start_flag, stop_flag, clock_flag; + reg [2:0] data_channel; + reg [15:0] data_mask, data_value, shift; + reg [4:0] bits, received; + reg serial_active; + + function pattern_hit; + input [23:0] pattern; + input [7:0] value, prior; + input seen; + begin + pattern_hit = (value & pattern[7:0]) == pattern[15:8] && + (pattern[23:16] == 0 || + (seen && ((value ^ prior) & pattern[23:16]) == pattern[23:16])); + end + endfunction + + wire a = pattern_hit(condition[23:0], sample, previous, previous_valid) ^ condition[82]; + wire b = pattern_hit(condition[47:24], sample, previous, previous_valid) ^ condition[83]; + wire stage_match = condition[80] ? (a && b) : (a || b); + wire stage_done = stage_match && count == condition[79:48]; + wire start_hit = pattern_hit(start_flag, sample, previous, previous_valid); + wire stop_hit = stop_flag != 0 && pattern_hit(stop_flag, sample, previous, previous_valid); + wire clock_hit = pattern_hit(clock_flag, sample, previous, previous_valid); + wire [15:0] shifted = {shift[14:0], sample[data_channel]}; + wire serial_hit = serial_active && !stop_hit && clock_hit && + received >= bits && (shifted & data_mask) == data_value; + assign hit = tick && (serial ? serial_hit : stage_done && stage == last); + + always @(posedge clk) begin + if (config_write) begin + stages[config_stage] <= {config_flags, config_count, config_b, config_a}; + case (config_stage) + 0: begin start_flag <= config_a; stop_flag <= config_b; end + 1: clock_flag <= config_a; + 2: data_channel <= config_count[2:0]; + 3: begin data_mask <= config_b[15:0]; data_value <= config_a[15:0]; bits <= config_count[4:0]; end + default: ; + endcase + end + if (reset || arm) begin + condition <= stages[0]; + stage <= 0; + last <= last_stage; + count <= 0; + serial <= serial_mode; + previous_valid <= 0; + serial_active <= 0; + shift <= 0; + received <= 0; + end else if (tick) begin + previous <= sample; + previous_valid <= 1; + if (serial) begin + if (stop_hit) begin + serial_active <= 0; + shift <= 0; + received <= 0; + end else if (serial_active && clock_hit) begin + shift <= shifted; + if (received < bits) + received <= received + 1'b1; + end else if (start_hit) begin + serial_active <= 1; + end + end else if (stage_done && stage != last) begin + condition <= stages[stage + 4'd1]; + stage <= stage + 1'b1; + count <= 0; + end else if (stage_match) begin + count <= count + 1'b1; + end else if (condition[81]) begin + count <= 0; + end + end + end +endmodule diff --git a/hdl/logic-analyzer/rtl/la_run_encoder.v b/hdl/logic-analyzer/rtl/la_run_encoder.v new file mode 100644 index 0000000..3c42a2f --- /dev/null +++ b/hdl/logic-analyzer/rtl/la_run_encoder.v @@ -0,0 +1,92 @@ +`timescale 1ns / 1ps +// Triggered, lossless encoding of constant digital runs. Each run emits the +// sample byte followed by its duration minus one (1..256 sample ticks). +// The small queue absorbs clusters of edges. Overflow latches a fault +// instead of slowing the sampling clock or silently dropping edges. +module la_run_encoder ( + input wire clk, reset, arm, stop, tick, + input wire [7:0] sample, channel_mask, + input wire [7:0] level_mask, level_value, edge_mask, edge_value, any_edge_mask, + output wire stored, + output reg overflow, + output wire valid, + output wire [7:0] data, + input wire ready +); + reg [7:0] channels, levels, values, directed, direction, edges; + reg [7:0] previous, run_sample, run_length, run_input; + reg previous_valid, triggered, run_tick, run_started; + wire [7:0] selected = sample & channels; + wire level_hit = (selected & levels) == values; + wire edge_hit = edges == 0 || (previous_valid && + ((selected ^ previous) & edges) == edges && (selected & directed) == direction); + assign stored = tick && !overflow && (triggered || (level_hit && edge_hit)); + + (* ram_style = "distributed" *) reg [15:0] runs [0:31]; + reg [5:0] wr_ptr, rd_ptr; + reg [15:0] output_run; + reg output_valid, high_byte; + wire full = wr_ptr == {~rd_ptr[5], rd_ptr[4:0]}; + wire emit = run_tick && !overflow && run_started && + (run_input != run_sample || run_length == 255); + assign valid = output_valid && !overflow; + assign data = high_byte ? output_run[15:8] : output_run[7:0]; + + always @(posedge clk) begin + if (reset || stop || arm) begin + channels <= channel_mask; + levels <= level_mask & channel_mask; + values <= level_value & level_mask & channel_mask; + directed <= edge_mask & channel_mask; + direction <= edge_value & edge_mask & channel_mask; + edges <= (edge_mask | any_edge_mask) & channel_mask; + previous_valid <= 0; + triggered <= 0; + run_tick <= 0; + run_started <= 0; + overflow <= 0; + wr_ptr <= 0; + rd_ptr <= 0; + output_valid <= 0; + high_byte <= 0; + end else begin + // The trigger comparison and the run encoder each get a full cycle. + run_tick <= stored; + run_input <= selected; + if (tick) begin + previous <= selected; + previous_valid <= 1; + end + if (stored) + triggered <= 1; + if (run_tick && !overflow) begin + run_started <= 1; + if (!run_started || emit) begin + run_sample <= run_input; + run_length <= 0; + end else begin + run_length <= run_length + 1'b1; + end + end + if (emit) begin + if (full) + overflow <= 1; + else + wr_ptr <= wr_ptr + 1'b1; + end + if (!output_valid && rd_ptr != wr_ptr) begin + output_run <= runs[rd_ptr[4:0]]; + rd_ptr <= rd_ptr + 1'b1; + output_valid <= 1; + end else if (valid && ready) begin + high_byte <= !high_byte; + if (high_byte) + output_valid <= 0; + end + end + end + + always @(posedge clk) + if (!reset && !arm && !stop && emit && !full) + runs[wr_ptr[4:0]] <= {run_length, run_sample}; +endmodule diff --git a/hdl/logic-analyzer/rtl/la_trigger_buffer.v b/hdl/logic-analyzer/rtl/la_trigger_buffer.v new file mode 100644 index 0000000..647b28f --- /dev/null +++ b/hdl/logic-analyzer/rtl/la_trigger_buffer.v @@ -0,0 +1,134 @@ +`timescale 1ns / 1ps +// Record a finite window from the first matching sample, then read it out. +// Input samples are already synchronized to clk. Only the readout can stall. +module la_trigger_buffer #( + parameter integer ADDR_WIDTH = 16, + parameter integer EXTERNAL_TRIGGER = 0 +)( + input wire clk, reset, arm, stop, + input wire [7:0] sample, + input wire [16:0] divider, + input wire [ADDR_WIDTH-1:0] last_address, + input wire [ADDR_WIDTH-1:0] pretrigger, + input wire use_external, external_hit, + output wire trigger_tick, + input wire [7:0] level_mask, level_value, edge_mask, edge_value, any_edge_mask, + output reg capturing, + output wire stored, + output reg data_valid, + output reg [7:0] data, + input wire ready +); + reg [16:0] period, timer; + reg [ADDR_WIDTH-1:0] write_address, read_address, pre, history, post_left, read_left; + reg [7:0] levels, values, directed, direction, edges, previous; + reg previous_valid, triggered, external_mode; + reg [1:0] read_pending; + wire [7:0] memory_data; + wire sample_tick = capturing && timer == 0 && !reset && !stop && !arm; + wire match_level = (sample & levels) == values; + wire match_edge = edges == 0 || (previous_valid && + ((sample ^ previous) & edges) == edges && (sample & directed) == direction); + assign trigger_tick = sample_tick && !triggered && history == 0; + wire fire = trigger_tick && (external_mode ? external_hit : match_level && match_edge); + assign stored = sample_tick && (triggered || fire); + + reg write_pending; + reg [ADDR_WIDTH-1:0] pending_address; + reg [7:0] pending_sample; + always @(posedge clk) begin + write_pending <= sample_tick && (pre != 0 || triggered || fire); + pending_address <= write_address; + pending_sample <= sample; + end + + la_capture_buffer #(.SAMPLE_WIDTH(8), .ADDR_WIDTH(ADDR_WIDTH)) memory ( + .wr_clk(clk), .wr_en(write_pending && !reset), .wr_addr(pending_address), .wr_data(pending_sample), + .rd_clk(clk), .rd_addr(read_address), .rd_data(memory_data) + ); + + always @(posedge clk) begin + if (reset || stop) begin + capturing <= 0; + data_valid <= 0; + read_pending <= 0; + period <= 0; + timer <= 0; + pre <= 0; + history <= 0; + post_left <= 0; + read_left <= 0; + write_address <= 0; + read_address <= 0; + levels <= 0; + values <= 0; + directed <= 0; + direction <= 0; + edges <= 0; + previous <= 0; + previous_valid <= 0; + triggered <= 0; + external_mode <= 0; + end else if (arm) begin + capturing <= 1; + data_valid <= 0; + read_pending <= 0; + period <= divider - 1'b1; + timer <= 0; + pre <= EXTERNAL_TRIGGER && use_external ? pretrigger : 0; + history <= EXTERNAL_TRIGGER && use_external ? pretrigger : 0; + post_left <= last_address - (EXTERNAL_TRIGGER && use_external ? pretrigger : 0); + read_left <= last_address; + write_address <= 0; + read_address <= 0; + levels <= level_mask; + values <= level_value & level_mask; + directed <= edge_mask; + direction <= edge_value & edge_mask; + edges <= edge_mask | any_edge_mask; + previous_valid <= 0; + triggered <= 0; + external_mode <= EXTERNAL_TRIGGER && use_external; + end else if (capturing) begin + if (sample_tick) begin + timer <= period; + previous <= sample; + previous_valid <= 1; + if (history != 0) + history <= history - 1'b1; + if (pre != 0 || triggered || fire) + write_address <= write_address + 1'b1; + if (fire) begin + triggered <= 1; + read_address <= write_address - pre; + if (post_left == 0) begin + capturing <= 0; + read_pending <= 3; + end + end else if (triggered) begin + post_left <= post_left - 1'b1; + if (post_left == 1) begin + capturing <= 0; + read_pending <= 3; + end + end + end else begin + timer <= timer - 1'b1; + end + end else if (read_pending != 0) begin + read_pending <= read_pending - 1'b1; + // Keep the BRAM read mux out of the downstream FIFO write path. + if (read_pending == 1) begin + data <= memory_data; + data_valid <= 1; + end + end else if (data_valid && ready) begin + data_valid <= 0; + if (read_left != 0) begin + read_left <= read_left - 1'b1; + read_address <= read_address + 1'b1; + read_pending <= 2; + end + end + end +endmodule diff --git a/hdl/logic-analyzer/rtl/zynq_la_stream.v b/hdl/logic-analyzer/rtl/zynq_la_stream.v new file mode 100644 index 0000000..d84e79c --- /dev/null +++ b/hdl/logic-analyzer/rtl/zynq_la_stream.v @@ -0,0 +1,184 @@ +`timescale 1ns/1ps +// Continuous byte sampler and write-only DMA for the reserved 8 MiB DDR ring. +module zynq_la_stream #( + parameter [31:0] DDR_BASE = 32'h3e000000 +) ( + (* X_INTERFACE_INFO = "xilinx.com:signal:clock:1.0 clk CLK" *) + (* X_INTERFACE_PARAMETER = "ASSOCIATED_BUSIF s_axi:m_axi, ASSOCIATED_RESET aresetn" *) + input wire clk, + (* X_INTERFACE_PARAMETER = "POLARITY ACTIVE_LOW" *) + input wire aresetn, + input wire [7:0] digital_inputs, + (* X_INTERFACE_PARAMETER = "PROTOCOL AXI4LITE, ADDR_WIDTH 12, DATA_WIDTH 32" *) + input wire [11:0] s_axi_awaddr, + input wire s_axi_awvalid, + output wire s_axi_awready, + input wire [31:0] s_axi_wdata, + input wire [3:0] s_axi_wstrb, + input wire s_axi_wvalid, + output wire s_axi_wready, + output reg [1:0] s_axi_bresp, + output reg s_axi_bvalid, + input wire s_axi_bready, + input wire [11:0] s_axi_araddr, + input wire s_axi_arvalid, + output wire s_axi_arready, + output reg [31:0] s_axi_rdata, + output wire [1:0] s_axi_rresp, + output reg s_axi_rvalid, + input wire s_axi_rready, + (* X_INTERFACE_PARAMETER = "PROTOCOL AXI4, READ_WRITE_MODE WRITE_ONLY, ADDR_WIDTH 32, DATA_WIDTH 64, MAX_BURST_LENGTH 16, NUM_WRITE_OUTSTANDING 1" *) + output wire [31:0] m_axi_awaddr, + output wire [7:0] m_axi_awlen, + output wire [2:0] m_axi_awsize, + output wire [1:0] m_axi_awburst, + output wire [3:0] m_axi_awcache, + output wire [2:0] m_axi_awprot, + output wire m_axi_awvalid, + input wire m_axi_awready, + output wire [63:0] m_axi_wdata, + output wire [7:0] m_axi_wstrb, + output wire m_axi_wlast, + output wire m_axi_wvalid, + input wire m_axi_wready, + input wire [1:0] m_axi_bresp, + input wire m_axi_bvalid, + output wire m_axi_bready +); + localparam [1:0] IDLE=0, ADDR=1, DATA=2, RESP=3; + localparam [31:0] MAGIC=32'h31534c5a; + (* ASYNC_REG = "TRUE" *) reg [7:0] input_meta, input_sync; + reg [1:0] state; + reg running; + reg [1:0] fault; // 1: FIFO or ring overflow. 2: DDR AXI response error. + reg [15:0] divider, ticks; + reg [2:0] byte_index; + reg [63:0] assembly; + reg [7:0] wr_pos, rd_pos; + reg [8:0] used; + (* ram_style = "distributed" *) reg [63:0] fifo [0:255]; + reg [3:0] beat; + reg [6:0] burst_index; + reg [31:0] produced, consumed; + wire ring_full = produced-consumed == 32'd512; + wire sample_now = running && ticks==0; + wire pop = m_axi_wvalid && m_axi_wready; + wire word_now = sample_now && byte_index==7; + wire push = word_now && (used!=256 || pop); + wire [63:0] next_word = {input_sync, assembly[63:8]}; + + reg aw_pending, w_pending; + reg [11:0] aw_addr; + reg [31:0] w_data; + reg [3:0] w_strb; + wire commit = aw_pending && w_pending && !s_axi_bvalid; + wire start = commit && w_strb==4'hf && aw_addr==12'h014 && + w_data==1 && !running && state==IDLE; + wire stop = commit && w_strb==4'hf && aw_addr==12'h014 && w_data==2; + + assign s_axi_awready = aresetn && !aw_pending && !s_axi_bvalid; + assign s_axi_wready = aresetn && !w_pending && !s_axi_bvalid; + assign s_axi_arready = aresetn && !s_axi_rvalid; + assign s_axi_rresp = 2'b00; + + assign m_axi_awaddr = {DDR_BASE[31:23], produced[8:0], burst_index, 7'b0}; + assign m_axi_awlen = 8'd15; + assign m_axi_awsize = 3'd3; + assign m_axi_awburst = 2'b01; + assign m_axi_awcache = 4'b0010; // Normal noncacheable, nonbufferable. + assign m_axi_awprot = 3'b000; + assign m_axi_awvalid = state==ADDR; + assign m_axi_wdata = fifo[rd_pos]; + assign m_axi_wstrb = 8'hff; + assign m_axi_wlast = beat==15; + assign m_axi_wvalid = state==DATA; + assign m_axi_bready = state==RESP; + + always @(posedge clk) begin + input_meta <= digital_inputs; + input_sync <= input_meta; + if (!aresetn) begin + aw_pending<=0; w_pending<=0; + s_axi_bvalid<=0; s_axi_bresp<=0; s_axi_rvalid<=0; s_axi_rdata<=0; + divider<=10; consumed<=0; + end else begin + if (s_axi_awvalid && s_axi_awready) begin aw_addr<=s_axi_awaddr; aw_pending<=1; end + if (s_axi_wvalid && s_axi_wready) begin + w_data<=s_axi_wdata; w_strb<=s_axi_wstrb; w_pending<=1; + end + if (s_axi_bvalid && s_axi_bready) s_axi_bvalid<=0; + if (commit) begin + aw_pending<=0; w_pending<=0; s_axi_bvalid<=1; s_axi_bresp<=2'b10; + if (w_strb==4'hf) case (aw_addr) + 12'h010: if (!running && state==IDLE && w_data>=10 && w_data<=65535) begin + divider<=w_data[15:0]; s_axi_bresp<=0; + end + 12'h014: if (start || stop) s_axi_bresp<=0; + 12'h01c: if (w_data==consumed+1 && consumed!=produced) begin + consumed<=w_data; s_axi_bresp<=0; + end + default: ; + endcase + end + if (start) consumed<=0; + if (s_axi_rvalid && s_axi_rready) s_axi_rvalid<=0; + if (s_axi_arvalid && s_axi_arready) begin + s_axi_rvalid<=1; + case (s_axi_araddr) + 12'h000: s_axi_rdata<=MAGIC; + 12'h004: s_axi_rdata<={28'b0, fault, (state!=IDLE), running}; + 12'h008: s_axi_rdata<=100000000; + 12'h00c: s_axi_rdata<=16384; + 12'h010: s_axi_rdata<={16'b0,divider}; + 12'h018: s_axi_rdata<=produced; + 12'h01c: s_axi_rdata<=consumed; + default: s_axi_rdata<=0; + endcase + end + end + end + + always @(posedge clk) begin + if (push && aresetn) fifo[wr_pos] <= next_word; + if (!aresetn || start) begin + state<=IDLE; running<=aresetn; fault<=0; + ticks<=0; byte_index<=0; assembly<=0; + wr_pos<=0; rd_pos<=0; used<=0; + beat<=0; burst_index<=0; produced<=0; + end else begin + if (stop) running<=0; + if (running) begin + if (sample_now) begin + ticks<=divider-1'b1; + assembly<=next_word; + byte_index<=byte_index+1'b1; + end else ticks<=ticks-1'b1; + if (word_now && !push) begin running<=0; fault<=1; end + end + if (push) wr_pos<=wr_pos+1'b1; + if (pop) rd_pos<=rd_pos+1'b1; + case ({push,pop}) + 2'b10: used<=used+1'b1; + 2'b01: used<=used-1'b1; + default: ; + endcase + case (state) + IDLE: if (running && !stop && !ring_full && used>=16) state<=ADDR; + ADDR: if (m_axi_awready) begin state<=DATA; beat<=0; end + DATA: if (pop) begin + beat<=beat+1'b1; + if (m_axi_wlast) state<=RESP; + end + RESP: if (m_axi_bvalid) begin + state<=IDLE; + if (m_axi_bresp!=0) begin running<=0; fault<=2; end + else begin + burst_index<=burst_index+1'b1; + if (burst_index==127) produced<=produced+1'b1; + end + end + default: state<=IDLE; + endcase + end + end +endmodule diff --git a/hdl/logic-analyzer/tests/tb_axi_logic_analyzer.sv b/hdl/logic-analyzer/tests/tb_axi_logic_analyzer.sv new file mode 100644 index 0000000..9c15d80 --- /dev/null +++ b/hdl/logic-analyzer/tests/tb_axi_logic_analyzer.sv @@ -0,0 +1,133 @@ +`timescale 1ns/1ps +module tb_axi_logic_analyzer; + reg clk=0; always #5 clk=~clk; + reg aresetn=0; + reg [11:0] s_axi_awaddr=0,s_axi_araddr=0; + reg [2:0] s_axi_awprot=0,s_axi_arprot=0; + reg s_axi_awvalid=0,s_axi_wvalid=0,s_axi_bready=0,s_axi_arvalid=0,s_axi_rready=0; + wire s_axi_awready,s_axi_wready,s_axi_bvalid,s_axi_arready,s_axi_rvalid; + reg [31:0] s_axi_wdata=0; reg [3:0] s_axi_wstrb=15; + wire [31:0] s_axi_rdata; wire [1:0] s_axi_bresp,s_axi_rresp; + reg [7:0] digital_inputs=0; wire [7:0] test_outputs; + axi_logic_analyzer #(.ADDR_WIDTH(4)) dut(.*); + task write_reg(input [11:0] address,input [31:0] value,input [1:0] response); + begin + // W before AW exercises independent AXI channels. + @(negedge clk); s_axi_wdata=value; s_axi_wvalid=1; + do @(posedge clk); while(!s_axi_wready); + @(negedge clk); s_axi_wvalid=0; + repeat(2) @(negedge clk); + s_axi_awaddr=address; s_axi_awvalid=1; + do @(posedge clk); while(!s_axi_awready); + @(negedge clk); s_axi_awvalid=0; + wait(s_axi_bvalid); #1; + if(s_axi_bresp!==response) $fatal(1,"write response addr=%h",address); + repeat(2) begin @(posedge clk); #1; if(!s_axi_bvalid || s_axi_bresp!==response) $fatal(1,"B unstable"); end + @(negedge clk); s_axi_bready=1; + @(negedge clk); s_axi_bready=0; + end + endtask + task read_reg(input [11:0] address,input [31:0] value,input [1:0] response); + begin + @(negedge clk); s_axi_araddr=address; s_axi_arvalid=1; + do @(posedge clk); while(!s_axi_arready); + @(negedge clk); s_axi_arvalid=0; + wait(s_axi_rvalid); #1; + if(s_axi_rresp!==response || (response==0 && s_axi_rdata!==value)) + $fatal(1,"read addr=%h got=%h expected=%h resp=%h",address,s_axi_rdata,value,s_axi_rresp); + repeat(2) begin @(posedge clk); #1; if(!s_axi_rvalid || (response==0 && s_axi_rdata!==value)) $fatal(1,"R unstable"); end + @(negedge clk); s_axi_rready=1; + @(negedge clk); s_axi_rready=0; + end + endtask + integer n,seen=0,last_cycle=0,cycle=0; + integer ch,highs[0:7],last_rise[0:7]; + reg [7:0] prior_output; + reg tracking=0; reg [7:0] expected[0:15]; + always @(posedge clk) begin + cycle=cycle+1; + if(tracking && dut.store_sample) begin + if(seen>0 && cycle-last_cycle!=4) $fatal(1,"divider interval"); + expected[seen]=dut.input_sync; seen=seen+1; last_cycle=cycle; + end + end + initial begin + repeat(4) @(negedge clk); aresetn=1; + read_reg('h00,32'h31414c5a,0); read_reg('h0c,16,0); + read_reg('h30,0,2); + write_reg('h10,0,2); write_reg('h10,17,2); + write_reg('h14,65536,2); write_reg('h03,0,2); + s_axi_wstrb=1; write_reg('h10,4,2); s_axi_wstrb=15; + write_reg('h10,12,0); write_reg('h14,3,0); + tracking=1; write_reg('h28,1,0); + while(!dut.done) begin @(negedge clk); digital_inputs=digital_inputs+7; end + tracking=0; + if(seen!=12) $fatal(1,"wrong count"); + read_reg('h04,6,0); read_reg('h34,12,0); + for(n=0;n<12;n=n+1) begin write_reg('h2c,n,0); read_reg('h30,{24'b0,expected[n]},0); end + write_reg('h2c,12,0); read_reg('h30,0,2); + // Rearm, reject configuration while waiting, edge includes triggering sample. + digital_inputs=0; write_reg('h10,1,0); write_reg('h14,0,0); + write_reg('h20,128,0); write_reg('h24,128,0); write_reg('h28,1,0); + repeat(10) @(negedge clk); + read_reg('h04,1,0); write_reg('h10,2,2); + digital_inputs=128; wait(dut.done); + write_reg('h2c,0,0); read_reg('h30,128,0); read_reg('h34,1,0); + write_reg('h28,1,0); repeat(10) @(negedge clk); + // A static high is not a new rising edge. + read_reg('h04,1,0); write_reg('h28,2,0); read_reg('h04,0,0); + write_reg('h3c,256,0); write_reg('h40,1,0); + if(test_outputs!==255) $fatal(1,"PWM full duty"); + write_reg('h3c,0,0); if(test_outputs!==0) $fatal(1,"PWM zero duty"); + write_reg('h3c,257,2); write_reg('h40,0,0); + // Every generated channel: 50% duty and a factor-of-two period step. + write_reg('h3c,128,0); write_reg('h40,1,0); + for(ch=0;ch<8;ch=ch+1) begin highs[ch]=0; last_rise[ch]=-1; end + prior_output=test_outputs; + for(n=0;n<65536;n=n+1) begin + @(negedge clk); + for(ch=0;ch<8;ch=ch+1) begin + if(test_outputs[ch]) highs[ch]=highs[ch]+1; + if(test_outputs[ch] && !prior_output[ch]) begin + if(last_rise[ch]>=0 && n-last_rise[ch]!=(256<=0 && n-last_rise[ch]!=(2< 1 ? $clog2(LOCAL_DIVIDER) : 1; + reg [TIMER_BITS-1:0] sample_timer; + reg [20:0] trigger_wait; + reg prior_trigger, capturing, prior_stream; + + wire sample_due = stream_active ? stream_valid : sample_timer == 0; + wire [7:0] sample = stream_active ? stream_sample : input_sync; + wire bank_free = acknowledge_sync[1] == publish; + wire begin_frame = !capturing && ((!prior_trigger && sample[3]) || trigger_wait == AUTO_TICKS); + wire write_sample = !capture_reset && bank_free && sample_due && + prior_stream == stream_active && (capturing || begin_frame); + + always @(posedge capture_clk) begin + input_meta <= digital_inputs; + input_sync <= input_meta; + if (write_sample) + banks[{~publish, write_column}] <= sample; + if (capture_reset) begin + acknowledge_sync <= 0; + publish <= 0; + write_column <= 0; + sample_timer <= 0; + trigger_wait <= 0; + prior_trigger <= 0; + capturing <= 0; + prior_stream <= 0; + end else begin + acknowledge_sync <= {acknowledge_sync[0], acknowledge}; + prior_stream <= stream_active; + if (sample_due) + prior_trigger <= sample[3]; + sample_timer <= sample_timer == 0 ? LOCAL_DIVIDER - 1 : sample_timer - 1'b1; + + if (prior_stream != stream_active) begin + // Кадр целиком относится Рє РѕРґРЅРѕРјСѓ источнику Рё РѕРґРЅРѕР№ частоте. + write_column <= 0; + trigger_wait <= 0; + capturing <= 0; + end else if (write_sample) begin + trigger_wait <= 0; + write_column <= write_column + 1'b1; + capturing <= write_column != POINTS - 1; + if (write_column == POINTS - 1) + publish <= !publish; + end else if (bank_free && !capturing && trigger_wait < AUTO_TICKS) begin + trigger_wait <= trigger_wait + 1'b1; + end + end + end + + // 640x480 at 60 Hz (pixel clock 25.2 MHz), negative sync polarity. + reg [9:0] x, y; + reg [9:0] screen_x, screen_y; + reg [7:0] level, previous_level; + wire [8:0] read_column = x[8:0] - 9'd64; + always @(posedge pixel_clk) begin + level <= banks[{acknowledge, read_column}]; + previous_level <= level; + if (pixel_reset) begin + publish_sync <= 0; + acknowledge <= 0; + frame_ready <= 0; + x <= 0; + y <= 0; + screen_x <= 0; + screen_y <= 0; + de <= 0; + hsync <= 1; + vsync <= 1; + end else begin + publish_sync <= {publish_sync[0], publish}; + if (x == 0 && y == 480 && acknowledge != publish_sync[1]) begin + acknowledge <= publish_sync[1]; + frame_ready <= 1; + end + if (x == 799) begin + x <= 0; + y <= y == 524 ? 0 : y + 1'b1; + end else x <= x + 1'b1; + + screen_x <= x; + screen_y <= y; + de <= x < 640 && y < 480; + hsync <= !(x >= 656 && x < 752); + vsync <= !(y >= 490 && y < 492); + end + end + + // Р’РѕСЃСЊРјРё строкам отведено РїРѕ 56 пикселей. Вертикальный штрих рисует + // фронт между соседними точками, Р° РЅРµ соединяет РёС… наклонным отрезком. + wire [8:0] row = screen_y - 10'd16; + wire [2:0] channel = row >= 392 ? 7 : row >= 336 ? 6 : row >= 280 ? 5 : + row >= 224 ? 4 : row >= 168 ? 3 : row >= 112 ? 2 : row >= 56 ? 1 : 0; + wire [5:0] row_y = row - ({6'd0, channel} << 6) + ({6'd0, channel} << 3); + wire plot = screen_x >= 64 && screen_x < 576 && screen_y >= 16 && screen_y < 464; + wire trace = frame_ready && plot && + ((level[channel] ? (row_y == 17 || row_y == 18) : (row_y == 37 || row_y == 38)) || + (screen_x != 64 && level[channel] != previous_level[channel] && row_y >= 17 && row_y <= 38)); + wire grid = plot && (screen_x[5:0] == 0 || row_y == 0 || row_y == 27); + wire channel_mark = screen_x >= 42 && screen_x < 51 && + screen_y >= 16 && screen_y < 464 && row_y >= 23 && row_y < 32; + // rgb2dvi takes RGB in the order R, B, G on its three byte lanes. + assign rgb = trace ? 24'h7040ff : channel_mark ? 24'h8080a0 : + grid ? 24'h203028 : 24'h08120e; +endmodule diff --git a/hdl/logic-hdmi-preview/tests/tb_zynq_logic_preview.sv b/hdl/logic-hdmi-preview/tests/tb_zynq_logic_preview.sv new file mode 100644 index 0000000..22a4796 --- /dev/null +++ b/hdl/logic-hdmi-preview/tests/tb_zynq_logic_preview.sv @@ -0,0 +1,51 @@ +`timescale 1ns/1ps +module tb_zynq_logic_preview; + reg capture_clk = 0, pixel_clk = 0; + always #4 capture_clk = !capture_clk; + always #20 pixel_clk = !pixel_clk; + reg capture_reset = 1, pixel_reset = 1; + reg [7:0] data = 0; + reg [2:0] counter = 0; + always @(negedge capture_clk) begin + counter <= counter + 1'b1; + data <= counter[2] ? 8'hff : 8'h00; + end + + wire [23:0] rgb; + wire de, hsync, vsync; + zynq_logic_preview dut ( + .capture_clk(capture_clk), .capture_reset(capture_reset), .digital_inputs(8'd0), + .stream_active(1'b1), .stream_valid(1'b1), .stream_sample(data), + .pixel_clk(pixel_clk), .pixel_reset(pixel_reset), + .rgb(rgb), .de(de), .hsync(hsync), .vsync(vsync) + ); + + integer high_pixels = 0, low_pixels = 0, edges = 0, active_pixels = 0; + // D7 is the last row. It must be visible and carry the same waveform as D0. + always @(negedge pixel_clk) if (dut.frame_ready && de) begin + if (dut.screen_y == 16 + 56*7 + 17 && rgb == 24'h7040ff) + high_pixels = high_pixels + 1; + if (dut.screen_y == 16 + 56*7 + 37 && rgb == 24'h7040ff) + low_pixels = low_pixels + 1; + if (dut.screen_y == 16 + 56*7 + 27 && rgb == 24'h7040ff) + edges = edges + 1; + if (dut.screen_y == 16 + 56*7 + 17) + active_pixels = active_pixels + 1; + end + initial begin + #200 capture_reset = 0; + pixel_reset = 0; + wait(dut.frame_ready); + wait(dut.screen_y == 479); + if (high_pixels < 240 || low_pixels < 240 || edges < 120 || active_pixels != 640) + $fatal(1, "Wrong final row: high=%d low=%d edge=%d active=%d", + high_pixels, low_pixels, edges, active_pixels); + $display("PASS tb_zynq_logic_preview: all eight rows fit, D7 renders levels and vertical edges"); + $finish; + end + + initial begin + #50000000; + $fatal(1, "No complete HDMI frame"); + end +endmodule diff --git a/hdl/logic-udp-ethernet/README.md b/hdl/logic-udp-ethernet/README.md new file mode 100644 index 0000000..fd18c45 --- /dev/null +++ b/hdl/logic-udp-ethernet/README.md @@ -0,0 +1,76 @@ +# Ethernet-поток цифровых входов + +`zynq_pl_ethernet` соединяет 1G MAC, ARP/IPv4/UDP, FIFO отсчётов и PHY +RTL8211F. Передача выполняется в PL. На вход `clk` подайте 125 МГц +для MAC, на `sample_clk` — 200 МГц для захвата, на +`delay_ref_clk` — стабильные 200 МГц для IDELAYCTRL. Синхронизируйте +снятие `sample_reset` с `sample_clk`. Параметры +`LOCAL_IP` и `LOCAL_MAC` задаются при инстанцировании. Подключите ПК +и FPGA к одной подсети /24. ARP-блок использует `.1` этой подсети +при проверке адреса назначения. + +- `rtl8211f_init.v` формирует сброс PHY и по MDIO читает, меняет и + проверяет задержки RX/TX. Адрес PHY в этой конфигурации равен 1. + При MDC 1,25 МГц чтение выполняется через 400 нс после фронта. + Линии `phy_mdio_i`, `phy_mdio_o` и `phy_mdio_t` подключаются + к IOBUF на верхнем уровне проекта. В Vivado BD экспортируйте + интерфейс `mdio`, чтобы обёртка поставила IOBUF на вывод корпуса. +- `zynq_pl_capture.v` синхронизирует восемь входов, передаёт отсчёты + между доменами через асинхронный FIFO и выдаёт UDP-блоки + с номером и защищает захват идентификатором сессии. KEEPALIVE + продлевает захват, STOP освобождает FIFO. +- При `BUFFER_ENABLED=1` этот блок также принимает ZLB1 на том же + UDP-порту. Аппаратный триггер запускает запись до 65 536 отсчётов + на восьми каналах при 200 Мвыб/с. После записи окно передаётся через + общий FIFO. Подключите `../logic-analyzer/rtl/la_trigger_buffer.v` + и `../logic-analyzer/rtl/la_capture_buffer.v`. Обёртка + `zynq_pl_ethernet` включает этот режим. +- ZLT1 добавляет предысторию в BRAM, настройку до 16 стадий + и последовательный триггер. Подключите + `../logic-analyzer/rtl/la_advanced_trigger.v`. Каждая стадия + подтверждается перед загрузкой следующей. +- При `RLE_ENABLED=1` команда ZLR1 запускает непрерывную запись при + 200 Мвыб/с от аппаратного триггера. Подключите + `../logic-analyzer/rtl/la_run_encoder.v`. Блок передаёт пары + байтов «отсчёт, длина минус один» и сообщает о переполнении, если + сеть и очередь не успевают передать поток переходов. +- `zynq_pl_vfd.v` принимает ZVF1 на UDP 37573 и передаёт команды через + XPM handshake в домен генератора. Два слова перед ZVF1 содержат токен + и номер запроса. Повтор запроса возвращает сохранённый ответ. + `vfd_command` содержит 3 бита операции и шесть 32-битных параметров + в порядке target, up, down, top, dead, mod от младших разрядов. + `vfd_reply` содержит два бита ошибки, три флага, target и actual + от старших разрядов. Ответы чередуются с пакетами захвата на границах UDP. +- `preview_active`, `preview_valid` и `preview_sample` в домене + `sample_clk` передают те же отсчёты на отрисовку. +- `include/zynq_la_pl.h` задаёт форматы ZLT1, ZLR1, ZLB1, ZLP2 и ZLP1, UDP-порт 37575. + ZLP2 передаёт маску каналов в START. При 1–4 каналах на байт + приходится два отсчёта, максимальная частота 200 Мвыб/с. При 5–8 + каналах передаётся байт на отсчёт, предел 100 Мвыб/с. +- `tests/check_zynq_rj2.py ` проверяет порядок и измеряет темп + поступления отсчётов. Делитель выбирается через `--divider`, маска + каналов через `--mask` (например `0xa5` для D0, D2, D5 и D7). + Приёмник восстанавливает порядок блоков в окне на 4096 пакетов. + +Для синтеза нужны Xilinx 7 Series и модули +[verilog-ethernet](https://github.com/alexforencich/verilog-ethernet/tree/77320a9471d19c7dd383914bc049e02d9f4f1ffb) +включая `lib/axis`. Пины, синхронизацию и задержки RGMII задаёт +проект платы. XSIM-проверка блока отсчётов: + +```text +xvlog -sv ../logic-analyzer/rtl/la_capture_buffer.v ../logic-analyzer/rtl/la_trigger_buffer.v ../logic-analyzer/rtl/la_advanced_trigger.v ../logic-analyzer/rtl/la_run_encoder.v rtl/zynq_pl_capture.v tests/tb_zynq_pl_capture.sv +xvlog /data/verilog/src/glbl.v +xelab -L xpm tb_zynq_pl_capture glbl -s rj2_capture +xsim rj2_capture -runall +``` + +`tests/tb_zynq_pl_ethernet.sv` проверяет ARP и ответ ZLP2 на уровне +Ethernet-кадров. В проекте Vivado с RTL Ethernet добавьте этот файл в +набор источников симуляции и выберите `tb_zynq_pl_ethernet` как верхний модуль. + +Проверка инициализации PHY с задержкой ответа MDIO 300 нс: + +```text +iverilog -g2012 -s tb_rtl8211f_init -o mdio.vvp rtl/rtl8211f_init.v tests/tb_rtl8211f_init.sv +vvp mdio.vvp +``` diff --git a/hdl/logic-udp-ethernet/include/zynq_la_pl.h b/hdl/logic-udp-ethernet/include/zynq_la_pl.h new file mode 100644 index 0000000..5d6a031 --- /dev/null +++ b/hdl/logic-udp-ethernet/include/zynq_la_pl.h @@ -0,0 +1,44 @@ +#ifndef ZYNQ_LA_PL_H +#define ZYNQ_LA_PL_H + +/* ZLP1 and ZLP2 run over UDP on RJ2. Each word is little endian. + * ZLP2 START: divider in bits 0..15 and channel mask in bits 16..23. + * With 1..4 channels, each byte contains two samples, older nibble first. + * Nibble bits follow ascending enabled channel numbers. With 5..8 channels, + * the payload contains one sample byte at the original channel positions. + * Request: magic, operation, session (nonzero), divider. + * Reply: magic, type, session, value. DATA adds ZLP_BLOCK sample bytes. + * A START session expires if KEEPALIVE is absent for two seconds. + * ZLB1 captures eight bits per sample in BRAM before sending DATA. + * Its START appends three words to the four-word request: sample count + * (1024..65536, multiple of ZLP_BLOCK), level mask/value and directed + * edge mask/value (low to high bytes), and any-edge mask (low byte). + * PROBE, STOP, KEEPALIVE and all reply formats are shared. + * ZLR1 uses the ZLB1 START shape. The divider is 1, the channel mask + * occupies bits 16..23, and the count word is zero for continuous capture. + * DATA holds pairs of a sample byte and a run length minus one (1..256). + * If samples cannot be encoded and transferred in time, it reports OVERFLOW. + * ZLT1 uses the same buffered DATA and adds CONFIG (op 4). CONFIG appends + * two 24-bit patterns and a 32-bit count to the 16-byte request. Its value + * packs stage index (low nibble) and AND, contiguous, invert A/B flags. + * Patterns contain level mask, level value, and change mask. Reply type 3 + * acknowledges a stage. START appends count, serial/last-stage/pretrigger, + * and zero. Serial uses stages 0..3 for start/stop, clock, data pin, and + * data value/mask/bit count minus one. + */ +#define ZLP_MAGIC 0x31504c5au +#define ZLP2_MAGIC 0x32504c5au +#define ZLB_MAGIC 0x31424c5au +#define ZLR_MAGIC 0x31524c5au +#define ZLT_MAGIC 0x31544c5au +#define ZLP_PORT 37575 +#define ZLP_CLOCK_HZ 100000000u +#define ZLP2_CLOCK_HZ 200000000u +#define ZLP_LEGACY_CLOCK_HZ 125000000u +#define ZLP_BLOCK 1024u +#define ZLP_MIN_DIVIDER 1u +#define ZLP_LEGACY_MIN_DIVIDER 2u +enum zlp_operation { ZLP_PROBE, ZLP_START, ZLP_STOP, ZLP_KEEPALIVE, ZLP_CONFIG }; +enum zlp_type { ZLP_INFO, ZLP_DATA, ZLP_ERROR, ZLP_CONFIGURED }; +enum zlp_error { ZLP_BAD_DIVIDER=1, ZLP_BUSY, ZLP_OVERFLOW }; +#endif diff --git a/hdl/logic-udp-ethernet/rtl/rtl8211f_init.v b/hdl/logic-udp-ethernet/rtl/rtl8211f_init.v new file mode 100644 index 0000000..ffcec6a --- /dev/null +++ b/hdl/logic-udp-ethernet/rtl/rtl8211f_init.v @@ -0,0 +1,105 @@ +`timescale 1ns / 1ps +// Configure PHY2 at MDIO address 1 for PHY-provided RX and TX clock delays. +module rtl8211f_init ( + input wire clk, + input wire reset, + output wire phy_reset_n, + output reg mdc, + input wire mdio_i, + output wire mdio_o, + output wire mdio_t, + output reg ready +); + // At 125 MHz, 50 cycles give the PHY 400 ns to return each MDIO bit. + // RTL8211F can take up to 300 ns after MDC rises. + reg [22:0] boot_wait; + reg [5:0] phase; + reg [3:0] step; + reg [6:0] bits_left; + reg [63:0] frame; + reg reading; + reg [15:0] read_data; + reg read_ack; + reg [15:0] tx_control, rx_control; + wire waiting = boot_wait < 7500000; + assign phy_reset_n = !reset && boot_wait >= 1250000; + assign mdio_o = frame[63]; + assign mdio_t = bits_left == 0 || (reading && bits_left <= 18); + + task transaction; + input is_read; + input [4:0] addr; + input [15:0] value; + begin + frame <= {32'hffffffff, 2'b01, (is_read ? 2'b10 : 2'b01), + 5'd1, addr, 2'b10, value}; + reading <= is_read; + bits_left <= 64; + phase <= 0; + read_ack <= 0; + end + endtask + + always @(posedge clk) begin + if (reset) begin + boot_wait <= 0; + phase <= 0; + step <= 0; + bits_left <= 0; + frame <= 0; + reading <= 0; + read_data <= 0; + read_ack <= 0; + tx_control <= 0; + rx_control <= 0; + mdc <= 0; + ready <= 0; + end else if (waiting) begin + boot_wait <= boot_wait + 1'b1; + end else if (bits_left != 0) begin + if (phase == 49) begin + phase <= 0; + mdc <= !mdc; + if (mdc) begin + if (reading) begin + // PHY launches ACK during the first turnaround clock. + // The final clock lets it release the bus before the next preamble. + if (bits_left == 18) + read_ack <= !mdio_i; + else if (bits_left >= 2 && bits_left <= 17) + read_data <= {read_data[14:0], mdio_i}; + end + bits_left <= bits_left - 1'b1; + frame <= frame << 1; + if (bits_left == 1) + step <= step + 1'b1; + end + end else begin + phase <= phase + 1'b1; + end + end else begin + case (step) + 0: transaction(0, 5'h1f, 16'h0d08); + 1: transaction(1, 5'h11, 0); + 2: if (read_ack) begin + tx_control <= read_data | 16'h0100; + transaction(0, 5'h11, read_data | 16'h0100); + end else step <= 0; + 3: transaction(1, 5'h15, 0); + 4: if (read_ack) begin + rx_control <= read_data | 16'h0008; + transaction(0, 5'h15, read_data | 16'h0008); + end else step <= 0; + 5: transaction(1, 5'h11, 0); + 6: if (read_ack && read_data == tx_control) + transaction(1, 5'h15, 0); + else step <= 0; + 7: if (read_ack && read_data == rx_control) + transaction(0, 5'h1f, 0); + else step <= 0; + 8: ready <= 1; + default: step <= 0; + endcase + end + end +endmodule diff --git a/hdl/logic-udp-ethernet/rtl/zynq_pl_capture.v b/hdl/logic-udp-ethernet/rtl/zynq_pl_capture.v new file mode 100644 index 0000000..f3632c6 --- /dev/null +++ b/hdl/logic-udp-ethernet/rtl/zynq_pl_capture.v @@ -0,0 +1,481 @@ +`timescale 1ns / 1ps +// Sample eight digital inputs and packetize them for the RJ2 UDP endpoint. +module zynq_pl_capture #( + parameter integer CLOCK_HZ = 125000000, + parameter integer SAMPLE_HZ = 200000000, + parameter integer BLOCK = 1024, + parameter integer BUFFER_ENABLED = 0, + parameter integer RLE_ENABLED = 0, + parameter [15:0] PORT = 16'd37575 +)( + input wire clk, + input wire reset, + input wire sample_clk, + input wire sample_reset, + input wire [7:0] digital_inputs, + input wire rx_header_valid, + input wire [31:0] rx_ip, + input wire [15:0] rx_port, + input wire [15:0] rx_destination, + input wire [15:0] rx_length, + input wire [7:0] rx_data, + input wire rx_valid, + input wire rx_last, + input wire rx_error, + output wire tx_header_valid, + input wire tx_header_ready, + output reg [31:0] tx_ip, + output reg [15:0] tx_port, + output wire [15:0] tx_length, + output wire [7:0] tx_data, + output wire tx_valid, + input wire tx_ready, + output wire tx_last, + output reg preview_active, + output reg preview_valid, + output reg [7:0] preview_sample +); + localparam [31:0] ZLP1 = 32'h31504c5a, ZLP2 = 32'h32504c5a; + localparam [31:0] ZLB1 = 32'h31424c5a; + localparam [31:0] ZLR1 = 32'h31524c5a; + localparam [31:0] ZLT1 = 32'h31544c5a; + localparam integer FIFO_DEPTH = 8192; + localparam [1:0] TX_IDLE = 0, TX_HEADER = 1, TX_PAYLOAD = 2; + + (* ASYNC_REG = "TRUE" *) reg [7:0] input_meta, input_sync; + always @(posedge sample_clk) begin + input_meta <= digital_inputs; + input_sync <= input_meta; + end + + reg [223:0] request; + reg [4:0] rx_index; + reg rx_accept, rx_extended; + reg [31:0] request_ip; + reg [15:0] request_port; + reg command_valid; + always @(posedge clk) begin + command_valid <= 0; + if (reset) begin + request <= 0; + rx_index <= 0; + rx_accept <= 0; + rx_extended <= 0; + request_ip <= 0; + request_port <= 0; + end else begin + if (rx_header_valid) begin + rx_index <= 0; + rx_extended <= rx_length == 36; + rx_accept <= rx_destination == PORT && (rx_length == 24 || (BUFFER_ENABLED && rx_length == 36)); + request_ip <= rx_ip; + request_port <= rx_port; + end + if (rx_valid && rx_accept) begin + request[rx_index*8 +: 8] <= rx_data; + rx_index <= rx_index + 1'b1; + if (rx_last || rx_index == (rx_extended ? 27 : 15)) begin + rx_accept <= 0; + command_valid <= rx_last && rx_index == (rx_extended ? 27 : 15) && !rx_error; + end + end + end + end + + wire [31:0] command = request[63:32]; + wire [31:0] nonce = request[95:64]; + wire [31:0] request_magic = request[31:0]; + wire advanced_request = BUFFER_ENABLED && request_magic == ZLT1; + wire buffer_request = BUFFER_ENABLED && (request_magic == ZLB1 || advanced_request); + wire rle_request = RLE_ENABLED && request_magic == ZLR1; + wire request_valid = command_valid && nonce != 0 && + ((buffer_request || rle_request) ? rx_extended == (command == 1 || (advanced_request && command == 4)) : + !rx_extended && (request_magic == ZLP1 || request_magic == ZLP2)); + wire [7:0] channel_mask = request_magic == ZLP2 || rle_request ? request[119:112] : 8'hff; + wire [15:0] request_divider = request[111:96]; + wire [3:0] channel_count = channel_mask[0] + channel_mask[1] + channel_mask[2] + channel_mask[3] + + channel_mask[4] + channel_mask[5] + channel_mask[6] + channel_mask[7]; + wire packed_request = channel_count <= 4; + wire [16:0] sample_divider = request_magic == ZLP2 || buffer_request || rle_request ? + {1'b0, request_divider} : {request_divider, 1'b0}; + wire valid_stream_start = request_divider != 0 && channel_mask != 0 && request[127:120] == 0 && + (request_magic == ZLP2 || request[119:112] == 0) && + (packed_request || sample_divider >= 2); + wire [31:0] buffer_count = request[159:128]; + wire [15:0] buffer_last_address = buffer_count[15:0] - 1'b1; + wire [15:0] buffer_pretrigger = request[191:176]; + wire [3:0] advanced_last = request[171:168]; + wire advanced_serial = request[160]; + wire [3:0] config_index = request[99:96]; + reg [31:0] config_nonce, config_ip; + reg [15:0] config_port, config_seen; + wire config_owner = nonce == config_nonce && request_ip == config_ip && request_port == config_port; + wire [16:0] config_required = (17'd2 << advanced_last) - 1'b1; + wire valid_advanced_start = config_owner && + (config_seen & config_required[15:0]) == config_required[15:0] && + (!advanced_serial || advanced_last == 3) && buffer_pretrigger < buffer_count && + request[223:192] == 0 && request[175:172] == 0 && request[167:161] == 0; + wire valid_buffer_start = request_divider != 0 && request[127:112] == 0 && + buffer_count != 0 && buffer_count <= 65536 && (buffer_count & (BLOCK-1)) == 0 && + (advanced_request ? valid_advanced_start : request[223:200] == 0); + wire valid_rle_start = request_divider == 1 && channel_mask != 0 && + request[159:120] == 0 && request[223:200] == 0; + wire valid_start = buffer_request ? valid_buffer_start : + rle_request ? valid_rle_start : valid_stream_start; + + function [11:0] selected_channels(input [7:0] mask); + integer channel; + reg [2:0] lane; + reg [11:0] indices; + begin + lane = 0; + indices = 0; + for (channel = 0; channel < 8; channel = channel + 1) begin + if (mask[channel] && lane < 4) begin + indices[3*lane +: 3] = channel[2:0]; + lane = lane + 1'b1; + end + end + selected_channels = indices; + end + endfunction + + reg active; + reg [31:0] owner_nonce, owner_ip, owner_magic; + reg [15:0] owner_port; + wire from_owner = request_ip == owner_ip && request_port == owner_port && + nonce == owner_nonce && request_magic == owner_magic; + reg [111:0] sample_command_tx; + reg command_send, stopping; + wire command_received, sample_command_valid; + wire [111:0] sample_command; + wire command_idle = !command_send && !command_received; + wire config_command = sample_command_valid && sample_command[111]; + wire start_command = sample_command_valid && !sample_command[111]; + + // Send a complete divider across the boundary. Zero commands STOP. + xpm_cdc_handshake #( + .DEST_EXT_HSK(0), .DEST_SYNC_FF(3), .SRC_SYNC_FF(3), + .INIT_SYNC_FF(1), .WIDTH(112) + ) sample_control ( + .src_clk(clk), .src_in(sample_command_tx), .src_send(command_send), + .src_rcv(command_received), .dest_clk(sample_clk), + .dest_out(sample_command), .dest_req(sample_command_valid), .dest_ack(1'b0) + ); + + (* ASYNC_REG = "TRUE" *) reg [2:0] reset_in_sample = 3'b111; + always @(posedge sample_clk) + reset_in_sample <= {reset_in_sample[1:0], reset}; + wire fifo_reset = sample_reset || reset_in_sample[2]; + + reg sampling, sample_overflow; + reg [16:0] divider, sample_timer; + reg packed_mode, half_byte, rle_mode; + reg [11:0] channel_indices; + reg [3:0] first_sample; + wire [3:0] selected_bits = { + input_sync[channel_indices[11:9]], input_sync[channel_indices[8:6]], + input_sync[channel_indices[5:3]], input_sync[channel_indices[2:0]] + }; + (* ASYNC_REG = "TRUE" *) reg [1:0] overflow_in_net; + reg [31:0] lease, packet_seq; + + reg reply_pending; + reg [31:0] reply_ip, reply_nonce, reply_type, reply_value, reply_magic; + reg [15:0] reply_port; + reg [1:0] tx_state; + reg [127:0] tx_prefix; + reg [10:0] tx_index; + reg tx_samples; + wire sending_samples = tx_state != TX_IDLE && tx_samples; + + wire fifo_full, fifo_empty, fifo_read_busy, fifo_write_busy; + wire [7:0] fifo_data; + wire [13:0] fifo_count; + wire sample_due = sampling && !sample_overflow && sample_timer == 0; + wire sample_valid = sample_due && !rle_mode && !fifo_full && !fifo_write_busy; + wire buffer_active, buffer_stored, buffer_valid; + wire rle_stored, rle_valid, rle_overflow; + wire [7:0] buffer_data, rle_data; + wire buffer_ready = !fifo_full && !fifo_write_busy; + wire fifo_write = (sample_valid && (!packed_mode || half_byte)) || + ((buffer_valid || rle_valid) && buffer_ready); + wire fifo_read = !fifo_empty && !fifo_read_busy && + ((tx_valid && tx_ready && tx_index >= 16) || (!active && !sending_samples)); + + always @(posedge sample_clk) begin + if (fifo_reset) begin + sampling <= 0; + sample_overflow <= 0; + divider <= 0; + sample_timer <= 0; + packed_mode <= 0; + rle_mode <= 0; + half_byte <= 0; + channel_indices <= 0; + first_sample <= 0; + end else if (start_command) begin + sampling <= sample_command[16:0] != 0 && !sample_command[86]; + sample_overflow <= 0; + divider <= sample_command[16:0] - 1'b1; + packed_mode <= sample_command[29]; + rle_mode <= RLE_ENABLED && sample_command[85] && !sample_command[86]; + channel_indices <= sample_command[28:17]; + half_byte <= 0; + sample_timer <= 0; + end else if (sampling && !sample_overflow) begin + if (sample_due) begin + sample_timer <= divider; + if ((fifo_full && !rle_mode) || (rle_overflow && rle_mode)) + sample_overflow <= 1; + if (sample_valid && packed_mode) begin + half_byte <= !half_byte; + first_sample <= selected_bits; + end + end else begin + sample_timer <= sample_timer - 1'b1; + end + end + end + + always @(posedge clk) + if (reset) + overflow_in_net <= 0; + else + overflow_in_net <= {overflow_in_net[0], sample_overflow}; + + // Register the preview handoff so the 200 MHz trigger and display logic + // have separate timing paths. Keep valid and its sample aligned. + always @(posedge sample_clk) begin + preview_active <= !fifo_reset && (sampling || buffer_active); + preview_valid <= !fifo_reset && (sample_valid || buffer_stored || rle_stored); + preview_sample <= input_sync; + end + + generate if (BUFFER_ENABLED) begin : snapshot + wire buffer_arm = start_command && sample_command[86]; + wire trigger_tick, advanced_hit; + reg advanced_capture; + la_advanced_trigger advanced ( + .clk(sample_clk), .reset(fifo_reset), .arm(buffer_arm && sample_command[110]), + .tick(trigger_tick && advanced_capture), .sample(input_sync), + .serial_mode(sample_command[93]), .last_stage(sample_command[92:89]), + .config_write(config_command), .config_stage(sample_command[83:80]), + .config_flags(sample_command[87:84]), .config_a(sample_command[23:0]), + .config_b(sample_command[47:24]), .config_count(sample_command[79:48]), .hit(advanced_hit) + ); + always @(posedge sample_clk) + if (fifo_reset) + advanced_capture <= 0; + else if (start_command) + advanced_capture <= sample_command[110]; + + la_trigger_buffer #(.EXTERNAL_TRIGGER(1)) memory ( + .clk(sample_clk), .reset(fifo_reset), + .arm(buffer_arm), .stop(start_command && !sample_command[86]), + .sample(input_sync), .divider(sample_command[16:0]), + .last_address(sample_command[85:70]), + .pretrigger(sample_command[109:94]), .use_external(sample_command[110]), + .external_hit(advanced_hit), .trigger_tick(trigger_tick), + .level_mask(sample_command[37:30]), .level_value(sample_command[45:38]), + .edge_mask(sample_command[53:46]), .edge_value(sample_command[61:54]), + .any_edge_mask(sample_command[69:62]), .capturing(buffer_active), + .stored(buffer_stored), .data_valid(buffer_valid), .data(buffer_data), .ready(buffer_ready) + ); + end else begin : no_snapshot + assign buffer_active = 0; + assign buffer_stored = 0; + assign buffer_valid = 0; + assign buffer_data = 0; + end endgenerate + + generate if (RLE_ENABLED) begin : compressed + la_run_encoder encoder ( + .clk(sample_clk), .reset(fifo_reset), + .arm(start_command && !sample_command[86] && sample_command[85]), + .stop(start_command && (sample_command[86] || !sample_command[85])), + .tick(sample_due && rle_mode), .sample(input_sync), .channel_mask(sample_command[77:70]), + .level_mask(sample_command[37:30]), .level_value(sample_command[45:38]), + .edge_mask(sample_command[53:46]), .edge_value(sample_command[61:54]), + .any_edge_mask(sample_command[69:62]), .stored(rle_stored), + .overflow(rle_overflow), .valid(rle_valid), .data(rle_data), .ready(buffer_ready) + ); + end else begin : no_compressed + assign rle_stored = 0; + assign rle_valid = 0; + assign rle_overflow = 0; + assign rle_data = 0; + end endgenerate + + xpm_fifo_async #( + .FIFO_MEMORY_TYPE("block"), .FIFO_WRITE_DEPTH(FIFO_DEPTH), + .WRITE_DATA_WIDTH(8), .READ_DATA_WIDTH(8), .CDC_SYNC_STAGES(2), + .READ_MODE("fwft"), .FIFO_READ_LATENCY(0), + .RD_DATA_COUNT_WIDTH(14), .USE_ADV_FEATURES("0400") + ) sample_fifo ( + .wr_clk(sample_clk), .rd_clk(clk), .rst(fifo_reset), .sleep(1'b0), + .din(buffer_valid ? buffer_data : rle_valid ? rle_data : + packed_mode ? {selected_bits, first_sample} : input_sync), + .wr_en(fifo_write), .full(fifo_full), .wr_rst_busy(fifo_write_busy), + .dout(fifo_data), .rd_en(fifo_read), .empty(fifo_empty), .rd_rst_busy(fifo_read_busy), + .rd_data_count(fifo_count), .injectdbiterr(1'b0), .injectsbiterr(1'b0) + ); + + assign tx_header_valid = tx_state == TX_HEADER; + assign tx_length = tx_samples ? BLOCK + 24 : 24; + assign tx_valid = tx_state == TX_PAYLOAD; + assign tx_data = tx_index < 16 ? tx_prefix[tx_index[3:0]*8 +: 8] : fifo_data; + assign tx_last = tx_index == (tx_samples ? BLOCK + 15 : 15); + + task queue_reply; + input [31:0] ip, id, kind, value, magic; + input [15:0] port; + begin + reply_pending <= 1; + reply_ip <= ip; + reply_port <= port; + reply_nonce <= id; + reply_type <= kind; + reply_value <= value; + reply_magic <= magic; + end + endtask + + always @(posedge clk) begin + if (reset) begin + active <= 0; + owner_nonce <= 0; + owner_ip <= 0; + owner_port <= 0; + owner_magic <= 0; + config_nonce <= 0; + config_ip <= 0; + config_port <= 0; + config_seen <= 0; + sample_command_tx <= 0; + command_send <= 0; + stopping <= 0; + lease <= 0; + packet_seq <= 0; + reply_pending <= 0; + reply_ip <= 0; + reply_port <= 0; + reply_nonce <= 0; + reply_type <= 0; + reply_value <= 0; + reply_magic <= 0; + tx_state <= TX_IDLE; + tx_ip <= 0; + tx_port <= 0; + tx_prefix <= 0; + tx_index <= 0; + tx_samples <= 0; + end else begin + case (tx_state) + TX_IDLE: begin + tx_index <= 0; + if (reply_pending) begin + reply_pending <= 0; + tx_ip <= reply_ip; + tx_port <= reply_port; + tx_prefix <= {reply_value, reply_nonce, reply_type, reply_magic}; + tx_samples <= 0; + tx_state <= TX_HEADER; + end else if (active && fifo_count >= BLOCK) begin + tx_ip <= owner_ip; + tx_port <= owner_port; + tx_prefix <= {packet_seq, owner_nonce, 32'd1, owner_magic}; + tx_samples <= 1; + tx_state <= TX_HEADER; + packet_seq <= packet_seq + 1; + end + end + TX_HEADER: if (tx_header_ready) tx_state <= TX_PAYLOAD; + TX_PAYLOAD: if (tx_ready) begin + if (tx_last) + tx_state <= TX_IDLE; + else + tx_index <= tx_index + 1'b1; + end + default: tx_state <= TX_IDLE; + endcase + + if (command_send && command_received) + command_send <= 0; + if (stopping && command_idle) begin + sample_command_tx <= 0; + command_send <= 1; + stopping <= 0; + end + + if (active) begin + if (lease == 0) begin + active <= 0; + stopping <= 1; + end else + lease <= lease - 1'b1; + end + + if (request_valid) begin + case (command) + 0: if (!reply_pending) + queue_reply(request_ip, nonce, 0, + request_magic == ZLP1 ? SAMPLE_HZ/2 : SAMPLE_HZ, request_magic, request_port); + 1: if (active && from_owner) begin + lease <= 2*CLOCK_HZ; + end else if (!valid_start) begin + if (!reply_pending) + queue_reply(request_ip, nonce, 2, 1, request_magic, request_port); + end else if (!active && !stopping && command_idle && !sending_samples && fifo_empty && !fifo_read_busy) begin + active <= 1; + owner_nonce <= nonce; + owner_ip <= request_ip; + owner_port <= request_port; + owner_magic <= request_magic; + if (advanced_request) + sample_command_tx <= {2'b01, buffer_pretrigger, advanced_serial, + advanced_last, 2'd0, 1'b1, buffer_last_address, 53'd0, sample_divider}; + else if (buffer_request) + sample_command_tx <= {1'b1, buffer_last_address, request[199:160], 13'd0, sample_divider}; + else if (rle_request) + sample_command_tx <= {2'b01, 7'd0, channel_mask, request[199:160], 13'd0, sample_divider}; + else + sample_command_tx <= {packed_request, selected_channels(channel_mask), sample_divider}; + command_send <= 1; + packet_seq <= 0; + lease <= 2*CLOCK_HZ; + end else if (!reply_pending) begin + queue_reply(request_ip, nonce, 2, 2, request_magic, request_port); + end + 2: if (active && from_owner) begin + active <= 0; + stopping <= 1; + end + 3: if (active && from_owner) lease <= 2*CLOCK_HZ; + 4: if (advanced_request && !reply_pending) begin + if (!active && !stopping && command_idle && + (config_owner || config_index == 0) && + request[127:104] == 0 && request[159:152] == 0 && request[191:184] == 0) begin + config_nonce <= nonce; + config_ip <= request_ip; + config_port <= request_port; + config_seen <= (config_owner ? config_seen : 16'd0) | (16'd1 << config_index); + sample_command_tx <= {1'b1, 23'd0, request[103:96], + request[223:192], request[183:160], request[151:128]}; + command_send <= 1; + queue_reply(request_ip, nonce, 3, {28'd0, config_index}, request_magic, request_port); + end else begin + queue_reply(request_ip, nonce, 2, 2, request_magic, request_port); + end + end + endcase + end + + if (active && overflow_in_net[1]) begin + active <= 0; + stopping <= 1; + queue_reply(owner_ip, owner_nonce, 2, 3, owner_magic, owner_port); + end + end + end +endmodule diff --git a/hdl/logic-udp-ethernet/rtl/zynq_pl_ethernet.v b/hdl/logic-udp-ethernet/rtl/zynq_pl_ethernet.v new file mode 100644 index 0000000..2aed8f8 --- /dev/null +++ b/hdl/logic-udp-ethernet/rtl/zynq_pl_ethernet.v @@ -0,0 +1,409 @@ +/* + +Copyright (c) 2014-2018 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`timescale 1ns / 1ps +`default_nettype none +module zynq_pl_ethernet #( + parameter [31:0] LOCAL_IP = 32'hc0a84d02, + parameter [47:0] LOCAL_MAC = 48'h02_5a_4c_70_20_02, + parameter integer VFD_CLOCK_HZ = 175000000 +)( + input wire clk, + input wire sample_clk, + input wire sample_reset, + input wire delay_ref_clk, + input wire reset, + input wire [7:0] digital_inputs, + input wire phy_rx_clk, + input wire [3:0] phy_rxd, + input wire phy_rx_ctl, + output wire phy_tx_clk, + output wire [3:0] phy_txd, + output wire phy_tx_ctl, + output wire phy_reset_n, + (* X_INTERFACE_INFO = "xilinx.com:interface:mdio:1.0 mdio MDC", X_INTERFACE_MODE = "Master" *) + output wire phy_mdc, + (* X_INTERFACE_INFO = "xilinx.com:interface:mdio:1.0 mdio MDIO_I" *) + input wire phy_mdio_i, + (* X_INTERFACE_INFO = "xilinx.com:interface:mdio:1.0 mdio MDIO_O" *) + output wire phy_mdio_o, + (* X_INTERFACE_INFO = "xilinx.com:interface:mdio:1.0 mdio MDIO_T" *) + output wire phy_mdio_t, + output wire preview_active, + output wire preview_valid, + output wire [7:0] preview_sample, + input wire vfd_clk, vfd_resetn, + output wire vfd_command_valid, + output wire [194:0] vfd_command, + input wire vfd_reply_valid, + input wire [68:0] vfd_reply +); +wire phy_ready; +rtl8211f_init phy_config ( + .clk(clk), .reset(reset), .phy_reset_n(phy_reset_n), .mdc(phy_mdc), + .mdio_i(phy_mdio_i), .mdio_o(phy_mdio_o), .mdio_t(phy_mdio_t), .ready(phy_ready) +); +// IDELAY compensates for the IBUF/BUFIO clock insertion delay at the IDDRs. +wire delay_ready; +(* IODELAY_GROUP = "RJ2_RGMII" *) +IDELAYCTRL rx_delay_control (.REFCLK(delay_ref_clk), .RST(reset), .RDY(delay_ready)); +(* ASYNC_REG = "TRUE" *) reg [1:0] delay_ready_sync; +always @(posedge clk) + if (reset) delay_ready_sync <= 0; + else delay_ready_sync <= {delay_ready_sync[0], delay_ready}; +wire rst = reset || !phy_ready || !delay_ready_sync[1]; +wire [4:0] rx_pins = {phy_rx_ctl, phy_rxd}; +wire [4:0] rx_delayed; +genvar lane; +generate for (lane = 0; lane < 5; lane = lane + 1) begin : rj2_input + (* IODELAY_GROUP = "RJ2_RGMII" *) + IDELAYE2 #( + .DELAY_SRC("IDATAIN"), .IDELAY_TYPE("FIXED"), .IDELAY_VALUE(12), + .REFCLK_FREQUENCY(200.0), .HIGH_PERFORMANCE_MODE("TRUE"), .SIGNAL_PATTERN("DATA") + ) delay_data ( + .IDATAIN(rx_pins[lane]), .DATAOUT(rx_delayed[lane]), + .C(1'b0), .CE(1'b0), .CINVCTRL(1'b0), .CNTVALUEIN(5'd0), .CNTVALUEOUT(), + .DATAIN(1'b0), .INC(1'b0), .LD(1'b0), .LDPIPEEN(1'b0), .REGRST(1'b0) + ); +end endgenerate + +wire [7:0] rx_axis_tdata; +wire rx_axis_tvalid; +wire rx_axis_tready; +wire rx_axis_tlast; +wire rx_axis_tuser; +wire [7:0] tx_axis_tdata; +wire tx_axis_tvalid; +wire tx_axis_tready; +wire tx_axis_tlast; +wire tx_axis_tuser; +wire rx_eth_hdr_ready; +wire rx_eth_hdr_valid; +wire [47:0] rx_eth_dest_mac; +wire [47:0] rx_eth_src_mac; +wire [15:0] rx_eth_type; +wire [7:0] rx_eth_payload_axis_tdata; +wire rx_eth_payload_axis_tvalid; +wire rx_eth_payload_axis_tready; +wire rx_eth_payload_axis_tlast; +wire rx_eth_payload_axis_tuser; +wire tx_eth_hdr_ready; +wire tx_eth_hdr_valid; +wire [47:0] tx_eth_dest_mac; +wire [47:0] tx_eth_src_mac; +wire [15:0] tx_eth_type; +wire [7:0] tx_eth_payload_axis_tdata; +wire tx_eth_payload_axis_tvalid; +wire tx_eth_payload_axis_tready; +wire tx_eth_payload_axis_tlast; +wire tx_eth_payload_axis_tuser; +wire rx_udp_hdr_valid; +wire [31:0] rx_udp_ip_source_ip, rx_udp_ip_dest_ip; +wire [15:0] rx_udp_source_port, rx_udp_dest_port, rx_udp_length; +wire [7:0] rx_udp_payload_axis_tdata; +wire rx_udp_payload_axis_tvalid, rx_udp_payload_axis_tlast, rx_udp_payload_axis_tuser; +wire tx_udp_hdr_valid, tx_udp_hdr_ready; +wire [31:0] tx_udp_ip_dest_ip; +wire [15:0] tx_udp_dest_port, tx_udp_length; +wire [7:0] tx_udp_payload_axis_tdata; +wire tx_udp_payload_axis_tvalid, tx_udp_payload_axis_tready, tx_udp_payload_axis_tlast; +wire [15:0] tx_udp_source_port; + +wire capture_hdr, capture_hdr_ready, capture_valid, capture_ready, capture_last; +wire [31:0] capture_ip; +wire [15:0] capture_port, capture_length; +wire [7:0] capture_data; +wire vfd_hdr, vfd_hdr_ready, vfd_valid, vfd_ready, vfd_last; +wire [31:0] vfd_ip; +wire [15:0] vfd_port; +wire [7:0] vfd_data; +localparam [1:0] TX_IDLE = 0, TX_HEADER = 1, TX_PAYLOAD = 2; +reg [1:0] tx_state; +reg tx_vfd; + +// Control replies can go between sample packets. A payload keeps its owner +// until the UDP core accepts its last byte, including during backpressure. +assign tx_udp_hdr_valid = tx_state == TX_HEADER && (tx_vfd ? vfd_hdr : capture_hdr); +assign vfd_hdr_ready = tx_state == TX_HEADER && tx_vfd && tx_udp_hdr_ready; +assign capture_hdr_ready = tx_state == TX_HEADER && !tx_vfd && tx_udp_hdr_ready; +assign tx_udp_ip_dest_ip = tx_vfd ? vfd_ip : capture_ip; +assign tx_udp_source_port = tx_vfd ? 16'd37573 : 16'd37575; +assign tx_udp_dest_port = tx_vfd ? vfd_port : capture_port; +assign tx_udp_length = tx_vfd ? 16'd48 : capture_length; +assign tx_udp_payload_axis_tdata = tx_vfd ? vfd_data : capture_data; +assign tx_udp_payload_axis_tlast = tx_vfd ? vfd_last : capture_last; +assign tx_udp_payload_axis_tvalid = tx_state == TX_PAYLOAD && (tx_vfd ? vfd_valid : capture_valid); +assign vfd_ready = tx_state == TX_PAYLOAD && tx_vfd && tx_udp_payload_axis_tready; +assign capture_ready = tx_state == TX_PAYLOAD && !tx_vfd && tx_udp_payload_axis_tready; +always @(posedge clk) begin + if (rst) begin + tx_state <= TX_IDLE; + tx_vfd <= 0; + end else if (tx_state == TX_IDLE && (vfd_hdr || capture_hdr)) begin + tx_state <= TX_HEADER; + tx_vfd <= vfd_hdr; + end else if (tx_udp_hdr_valid && tx_udp_hdr_ready) begin + tx_state <= TX_PAYLOAD; + end else if (tx_udp_payload_axis_tvalid && tx_udp_payload_axis_tready && tx_udp_payload_axis_tlast) begin + tx_state <= TX_IDLE; + end +end + +zynq_pl_capture #(.BUFFER_ENABLED(1), .RLE_ENABLED(1)) capture ( + .clk(clk), .reset(rst), .sample_clk(sample_clk), .sample_reset(sample_reset), + .digital_inputs(digital_inputs), + .rx_header_valid(rx_udp_hdr_valid), .rx_ip(rx_udp_ip_source_ip), + .rx_port(rx_udp_source_port), .rx_destination(rx_udp_dest_port), .rx_length(rx_udp_length), + .rx_data(rx_udp_payload_axis_tdata), .rx_valid(rx_udp_payload_axis_tvalid), + .rx_last(rx_udp_payload_axis_tlast), .rx_error(rx_udp_payload_axis_tuser), + .tx_header_valid(capture_hdr), .tx_header_ready(capture_hdr_ready), + .tx_ip(capture_ip), .tx_port(capture_port), .tx_length(capture_length), + .tx_data(capture_data), .tx_valid(capture_valid), + .tx_ready(capture_ready), .tx_last(capture_last), + .preview_active(preview_active), .preview_valid(preview_valid), .preview_sample(preview_sample) +); + +zynq_pl_vfd #(.VFD_CLOCK_HZ(VFD_CLOCK_HZ)) pwm_control ( + .clk(clk), .reset(rst), + .rx_header_valid(rx_udp_hdr_valid && rx_udp_ip_dest_ip == LOCAL_IP), + .rx_ip(rx_udp_ip_source_ip), .rx_port(rx_udp_source_port), + .rx_destination(rx_udp_dest_port), .rx_length(rx_udp_length), + .rx_data(rx_udp_payload_axis_tdata), .rx_valid(rx_udp_payload_axis_tvalid), + .rx_last(rx_udp_payload_axis_tlast), .rx_error(rx_udp_payload_axis_tuser), + .tx_header_valid(vfd_hdr), .tx_header_ready(vfd_hdr_ready), + .tx_ip(vfd_ip), .tx_port(vfd_port), .tx_data(vfd_data), + .tx_valid(vfd_valid), .tx_ready(vfd_ready), .tx_last(vfd_last), + .vfd_clk(vfd_clk), .vfd_resetn(vfd_resetn), + .vfd_command_valid(vfd_command_valid), .vfd_command(vfd_command), + .vfd_reply_valid(vfd_reply_valid), .vfd_reply(vfd_reply) +); + +eth_mac_1g_rgmii_fifo #( + .TARGET("XILINX"), + .IODDR_STYLE("IODDR"), + .CLOCK_INPUT_STYLE("BUFR"), + .USE_CLK90("FALSE"), + .ENABLE_PADDING(1), + .MIN_FRAME_LENGTH(64), + .TX_FIFO_DEPTH(4096), + .TX_FRAME_FIFO(1), + .RX_FIFO_DEPTH(4096), + .RX_FRAME_FIFO(1) +) +eth_mac_inst ( + .gtx_clk(clk), + .gtx_clk90(clk), + .gtx_rst(rst), + .logic_clk(clk), + .logic_rst(rst), + + .tx_axis_tdata(tx_axis_tdata), + .tx_axis_tvalid(tx_axis_tvalid), + .tx_axis_tready(tx_axis_tready), + .tx_axis_tlast(tx_axis_tlast), + .tx_axis_tuser(tx_axis_tuser), + + .rx_axis_tdata(rx_axis_tdata), + .rx_axis_tvalid(rx_axis_tvalid), + .rx_axis_tready(rx_axis_tready), + .rx_axis_tlast(rx_axis_tlast), + .rx_axis_tuser(rx_axis_tuser), + + .rgmii_rx_clk(phy_rx_clk), + .rgmii_rxd(rx_delayed[3:0]), + .rgmii_rx_ctl(rx_delayed[4]), + .rgmii_tx_clk(phy_tx_clk), + .rgmii_txd(phy_txd), + .rgmii_tx_ctl(phy_tx_ctl), + + .tx_fifo_overflow(), + .tx_fifo_bad_frame(), + .tx_fifo_good_frame(), + .rx_error_bad_frame(), + .rx_error_bad_fcs(), + .rx_fifo_overflow(), + .rx_fifo_bad_frame(), + .rx_fifo_good_frame(), + .speed(), + + .cfg_ifg(8'd12), + .cfg_tx_enable(1'b1), + .cfg_rx_enable(1'b1) +); + +eth_axis_rx +eth_axis_rx_inst ( + .clk(clk), + .rst(rst), + // AXI input + .s_axis_tdata(rx_axis_tdata), + .s_axis_tvalid(rx_axis_tvalid), + .s_axis_tready(rx_axis_tready), + .s_axis_tlast(rx_axis_tlast), + .s_axis_tuser(rx_axis_tuser), + // Ethernet frame output + .m_eth_hdr_valid(rx_eth_hdr_valid), + .m_eth_hdr_ready(rx_eth_hdr_ready), + .m_eth_dest_mac(rx_eth_dest_mac), + .m_eth_src_mac(rx_eth_src_mac), + .m_eth_type(rx_eth_type), + .m_eth_payload_axis_tdata(rx_eth_payload_axis_tdata), + .m_eth_payload_axis_tvalid(rx_eth_payload_axis_tvalid), + .m_eth_payload_axis_tready(rx_eth_payload_axis_tready), + .m_eth_payload_axis_tlast(rx_eth_payload_axis_tlast), + .m_eth_payload_axis_tuser(rx_eth_payload_axis_tuser), + // Status signals + .busy(), + .error_header_early_termination() +); + +eth_axis_tx +eth_axis_tx_inst ( + .clk(clk), + .rst(rst), + // Ethernet frame input + .s_eth_hdr_valid(tx_eth_hdr_valid), + .s_eth_hdr_ready(tx_eth_hdr_ready), + .s_eth_dest_mac(tx_eth_dest_mac), + .s_eth_src_mac(tx_eth_src_mac), + .s_eth_type(tx_eth_type), + .s_eth_payload_axis_tdata(tx_eth_payload_axis_tdata), + .s_eth_payload_axis_tvalid(tx_eth_payload_axis_tvalid), + .s_eth_payload_axis_tready(tx_eth_payload_axis_tready), + .s_eth_payload_axis_tlast(tx_eth_payload_axis_tlast), + .s_eth_payload_axis_tuser(tx_eth_payload_axis_tuser), + // AXI output + .m_axis_tdata(tx_axis_tdata), + .m_axis_tvalid(tx_axis_tvalid), + .m_axis_tready(tx_axis_tready), + .m_axis_tlast(tx_axis_tlast), + .m_axis_tuser(tx_axis_tuser), + // Status signals + .busy() +); + +udp_complete +udp_complete_inst ( + .clk(clk), + .rst(rst), + // Ethernet frame input + .s_eth_hdr_valid(rx_eth_hdr_valid), + .s_eth_hdr_ready(rx_eth_hdr_ready), + .s_eth_dest_mac(rx_eth_dest_mac), + .s_eth_src_mac(rx_eth_src_mac), + .s_eth_type(rx_eth_type), + .s_eth_payload_axis_tdata(rx_eth_payload_axis_tdata), + .s_eth_payload_axis_tvalid(rx_eth_payload_axis_tvalid), + .s_eth_payload_axis_tready(rx_eth_payload_axis_tready), + .s_eth_payload_axis_tlast(rx_eth_payload_axis_tlast), + .s_eth_payload_axis_tuser(rx_eth_payload_axis_tuser), + // Ethernet frame output + .m_eth_hdr_valid(tx_eth_hdr_valid), + .m_eth_hdr_ready(tx_eth_hdr_ready), + .m_eth_dest_mac(tx_eth_dest_mac), + .m_eth_src_mac(tx_eth_src_mac), + .m_eth_type(tx_eth_type), + .m_eth_payload_axis_tdata(tx_eth_payload_axis_tdata), + .m_eth_payload_axis_tvalid(tx_eth_payload_axis_tvalid), + .m_eth_payload_axis_tready(tx_eth_payload_axis_tready), + .m_eth_payload_axis_tlast(tx_eth_payload_axis_tlast), + .m_eth_payload_axis_tuser(tx_eth_payload_axis_tuser), + .s_ip_hdr_valid(1'b0), .s_ip_dscp(6'd0), .s_ip_ecn(2'd0), .s_ip_length(16'd0), + .s_ip_ttl(8'd0), .s_ip_protocol(8'd0), .s_ip_source_ip(32'd0), .s_ip_dest_ip(32'd0), + .s_ip_payload_axis_tdata(8'd0), .s_ip_payload_axis_tvalid(1'b0), + .s_ip_payload_axis_tlast(1'b0), .s_ip_payload_axis_tuser(1'b0), + .m_ip_hdr_ready(1'b1), .m_ip_payload_axis_tready(1'b1), + // UDP frame input + .s_udp_hdr_valid(tx_udp_hdr_valid), + .s_udp_hdr_ready(tx_udp_hdr_ready), + .s_udp_ip_dscp(6'd0), + .s_udp_ip_ecn(2'd0), + .s_udp_ip_ttl(8'd64), + .s_udp_ip_source_ip(LOCAL_IP), + .s_udp_ip_dest_ip(tx_udp_ip_dest_ip), + .s_udp_source_port(tx_udp_source_port), + .s_udp_dest_port(tx_udp_dest_port), + .s_udp_length(tx_udp_length), + .s_udp_checksum(16'd0), + .s_udp_payload_axis_tdata(tx_udp_payload_axis_tdata), + .s_udp_payload_axis_tvalid(tx_udp_payload_axis_tvalid), + .s_udp_payload_axis_tready(tx_udp_payload_axis_tready), + .s_udp_payload_axis_tlast(tx_udp_payload_axis_tlast), + .s_udp_payload_axis_tuser(1'b0), + // UDP frame output + .m_udp_hdr_valid(rx_udp_hdr_valid), + .m_udp_hdr_ready(1'b1), + .m_udp_eth_dest_mac(), + .m_udp_eth_src_mac(), + .m_udp_eth_type(), + .m_udp_ip_version(), + .m_udp_ip_ihl(), + .m_udp_ip_dscp(), + .m_udp_ip_ecn(), + .m_udp_ip_length(), + .m_udp_ip_identification(), + .m_udp_ip_flags(), + .m_udp_ip_fragment_offset(), + .m_udp_ip_ttl(), + .m_udp_ip_protocol(), + .m_udp_ip_header_checksum(), + .m_udp_ip_source_ip(rx_udp_ip_source_ip), + .m_udp_ip_dest_ip(rx_udp_ip_dest_ip), + .m_udp_source_port(rx_udp_source_port), + .m_udp_dest_port(rx_udp_dest_port), + .m_udp_length(rx_udp_length), + .m_udp_checksum(), + .m_udp_payload_axis_tdata(rx_udp_payload_axis_tdata), + .m_udp_payload_axis_tvalid(rx_udp_payload_axis_tvalid), + .m_udp_payload_axis_tready(1'b1), + .m_udp_payload_axis_tlast(rx_udp_payload_axis_tlast), + .m_udp_payload_axis_tuser(rx_udp_payload_axis_tuser), + // Status signals + .ip_rx_busy(), + .ip_tx_busy(), + .udp_rx_busy(), + .udp_tx_busy(), + .ip_rx_error_header_early_termination(), + .ip_rx_error_payload_early_termination(), + .ip_rx_error_invalid_header(), + .ip_rx_error_invalid_checksum(), + .ip_tx_error_payload_early_termination(), + .ip_tx_error_arp_failed(), + .udp_rx_error_header_early_termination(), + .udp_rx_error_payload_early_termination(), + .udp_tx_error_payload_early_termination(), + // Configuration + .local_mac(LOCAL_MAC), + .local_ip(LOCAL_IP), + // The ARP core classifies local peers by comparing against the gateway's subnet. + .gateway_ip({LOCAL_IP[31:8], 8'd1}), + .subnet_mask(32'hffffff00), + .clear_arp_cache(1'b0) +); + +endmodule + +`default_nettype wire diff --git a/hdl/logic-udp-ethernet/rtl/zynq_pl_vfd.v b/hdl/logic-udp-ethernet/rtl/zynq_pl_vfd.v new file mode 100644 index 0000000..b217113 --- /dev/null +++ b/hdl/logic-udp-ethernet/rtl/zynq_pl_vfd.v @@ -0,0 +1,183 @@ +`timescale 1ns / 1ps +// ZVF1 test-generator commands with an 8-byte UDP session/sequence envelope. +module zynq_pl_vfd #( + parameter integer VFD_CLOCK_HZ = 175000000, + // The lease outlasts the generator's 500 ms watchdog. + parameter integer LEASE_CYCLES = 93750000 +)( + input wire clk, reset, + input wire rx_header_valid, + input wire [31:0] rx_ip, + input wire [15:0] rx_port, rx_destination, rx_length, + input wire [7:0] rx_data, + input wire rx_valid, rx_last, rx_error, + output wire tx_header_valid, + input wire tx_header_ready, + output reg [31:0] tx_ip, + output reg [15:0] tx_port, + output wire [7:0] tx_data, + output wire tx_valid, tx_last, + input wire tx_ready, + input wire vfd_clk, vfd_resetn, + output reg vfd_command_valid, + output reg [194:0] vfd_command, + input wire vfd_reply_valid, + input wire [68:0] vfd_reply +); + localparam [31:0] MAGIC = 32'h3146565a; + localparam [1:0] TX_IDLE = 0, TX_HEADER = 1, TX_PAYLOAD = 2; + reg [575:0] request; + reg [6:0] rx_index; + reg rx_accept, request_valid; + reg [31:0] request_ip; + reg [15:0] request_port; + wire [31:0] token = request[31:0], seq = request[63:32]; + wire [31:0] op = request[159:128]; + wire request_ok = token != 0 && seq != 0 && request[95:64] == MAGIC && + request[127:96] == 1 && op <= 4 && request[575:352] == 0; + + always @(posedge clk) begin + request_valid <= 0; + if (reset) begin + request <= 0; + rx_index <= 0; + rx_accept <= 0; + request_ip <= 0; + request_port <= 0; + end else begin + if (rx_header_valid) begin + rx_index <= 0; + rx_accept <= rx_destination == 37573 && rx_length == 80; + request_ip <= rx_ip; + request_port <= rx_port; + end + if (rx_valid && rx_accept) begin + request[rx_index*8 +: 8] <= rx_data; + rx_index <= rx_index + 1'b1; + if (rx_last || rx_index == 71) begin + rx_accept <= 0; + request_valid <= rx_last && rx_index == 71 && !rx_error; + end + end + end + end + + reg [194:0] command_hold; + reg command_send, waiting; + wire command_received, response_valid; + wire command_arrived; + wire [194:0] received_command; + wire [68:0] response; + reg [68:0] response_hold; + reg response_send; + wire response_received; + xpm_cdc_handshake #( + .DEST_EXT_HSK(0), .DEST_SYNC_FF(3), .SRC_SYNC_FF(3), + .INIT_SYNC_FF(1), .WIDTH(195) + ) to_vfd ( + .src_clk(clk), .src_in(command_hold), .src_send(command_send), .src_rcv(command_received), + .dest_clk(vfd_clk), .dest_out(received_command), .dest_req(command_arrived), .dest_ack(1'b0) + ); + xpm_cdc_handshake #( + .DEST_EXT_HSK(0), .DEST_SYNC_FF(3), .SRC_SYNC_FF(3), + .INIT_SYNC_FF(1), .WIDTH(69) + ) from_vfd ( + .src_clk(vfd_clk), .src_in(response_hold), .src_send(response_send), .src_rcv(response_received), + .dest_clk(clk), .dest_out(response), .dest_req(response_valid), .dest_ack(1'b0) + ); + always @(posedge vfd_clk) begin + // Keep the handshake registers separate from the generator's decode. + vfd_command <= received_command; + vfd_command_valid <= vfd_resetn && command_arrived; + if (!vfd_resetn) begin + response_hold <= 0; + response_send <= 0; + end else if (vfd_reply_valid) begin + response_hold <= vfd_reply; + response_send <= 1; + end else if (response_received) begin + response_send <= 0; + end + end + + reg [31:0] owner_ip, owner_token, last_seq, lease; + reg [15:0] owner_port; + reg [2:0] last_op; + wire owner = owner_token == token && owner_ip == request_ip && owner_port == request_port; + reg [255:0] cached_response; + reg [319:0] tx_payload; + reg [5:0] tx_index; + reg [1:0] tx_state; + wire [255:0] body = {32'd100000, 29'd0, response[66:64], response[63:0], + VFD_CLOCK_HZ[31:0], 30'd0, response[68:67], 32'd1, MAGIC}; + assign tx_header_valid = tx_state == TX_HEADER; + assign tx_valid = tx_state == TX_PAYLOAD; + assign tx_data = tx_payload[tx_index*8 +: 8]; + assign tx_last = tx_index == 39; + + always @(posedge clk) begin + if (reset) begin + command_hold <= 0; + command_send <= 0; + waiting <= 0; + owner_ip <= 0; + owner_port <= 0; + owner_token <= 0; + last_seq <= 0; + last_op <= 0; + lease <= 0; + cached_response <= 0; + tx_ip <= 0; + tx_port <= 0; + tx_state <= TX_IDLE; + tx_payload <= 0; + tx_index <= 0; + end else begin + if (lease != 0) lease <= lease - 1'b1; + if (command_received) command_send <= 0; + if (tx_header_valid && tx_header_ready) tx_state <= TX_PAYLOAD; + if (tx_valid && tx_ready) begin + tx_index <= tx_index + 1'b1; + if (tx_last) tx_state <= TX_IDLE; + end + + if (response_valid && waiting) begin + waiting <= 0; + cached_response <= body; + tx_payload <= {body,last_seq,owner_token}; + tx_ip <= owner_ip; + tx_port <= owner_port; + tx_state <= TX_HEADER; + tx_index <= 0; + if (last_op == 4) lease <= 0; + end else if (request_valid && request_ok && tx_state == TX_IDLE && + !waiting && !command_send && !command_received) begin + if (owner && seq == last_seq) begin + // Retry returns the original response without repeating START or OFF. + tx_payload <= {cached_response,seq,token}; + tx_ip <= request_ip; + tx_port <= request_port; + tx_state <= TX_HEADER; + tx_index <= 0; + end else if ((owner && seq > last_seq && (lease != 0 || op == 0)) || + (!owner && lease == 0 && op == 0)) begin + owner_ip <= request_ip; + owner_port <= request_port; + owner_token <= token; + last_seq <= seq; + last_op <= op[2:0]; + lease <= LEASE_CYCLES; + command_hold <= {op[2:0],request[351:160]}; + command_send <= 1; + waiting <= 1; + end else if (!owner && op == 0) begin + tx_payload <= {32'd100000,96'd0,VFD_CLOCK_HZ[31:0],32'd2,32'd1,MAGIC,seq,token}; + tx_ip <= request_ip; + tx_port <= request_port; + tx_state <= TX_HEADER; + tx_index <= 0; + end + end + end + end +endmodule diff --git a/hdl/logic-udp-ethernet/tests/check_zynq_rj2.py b/hdl/logic-udp-ethernet/tests/check_zynq_rj2.py new file mode 100644 index 0000000..9d03f54 --- /dev/null +++ b/hdl/logic-udp-ethernet/tests/check_zynq_rj2.py @@ -0,0 +1,144 @@ +"""Check the FPGA RJ2 UDP endpoint and measure ordered sample throughput.""" +import argparse +import secrets +import socket +import struct +import time + +HEADER = struct.Struct('<4I') +MAGIC = 0x31504C5A +PACKED_MAGIC = 0x32504C5A +PORT = 37575 +BLOCK = 1024 +REORDER_BLOCKS = 4096 + + +def capture(address, divider, count, channels=0xff): + nonce = secrets.randbits(32) or 1 + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 16 * 1024 * 1024) + sock.connect((address, PORT)) + sock.settimeout(.35) + + protocol = PACKED_MAGIC + + def send(operation, value=0): + sock.send(HEADER.pack(protocol, operation, nonce, value)) + + clock = 0 + for protocol in (PACKED_MAGIC, MAGIC): + for _ in range(4): + send(0) + try: + data = sock.recv(64) + except TimeoutError: + continue + if len(data) == HEADER.size: + magic, kind, session, value = HEADER.unpack(data) + if (magic, kind, session) == (protocol, 0, nonce): + clock = value + break + if clock: + break + + valid_clocks = (200_000_000,) if protocol == PACKED_MAGIC else (100_000_000, 125_000_000) + if clock not in valid_clocks: + raise RuntimeError(f'RJ2 did not report a supported sample clock: {clock}') + packed = protocol == PACKED_MAGIC and channels.bit_count() <= 4 + if protocol == MAGIC and channels != 0xff: + raise ValueError('Channel selection needs ZLP2 firmware') + if divider == 1 and (clock == 125_000_000 or (protocol == PACKED_MAGIC and not packed)): + raise ValueError('This channel configuration requires a divider of at least 2') + samples_per_byte = 2 if packed else 1 + start_value = divider | (channels << 16 if protocol == PACKED_MAGIC else 0) + + samples = bytearray() + sequence = 0 + pending = {} + deadline = time.monotonic() + 4 + heartbeat = 0 + started = None + try: + while len(samples) * samples_per_byte < count: + now = time.monotonic() + if now > deadline: + raise TimeoutError('RJ2 stopped sending samples') + if now >= heartbeat: + if started is None: + send(1, start_value) + else: + send(3) + heartbeat = now + .2 + + try: + data = sock.recv(BLOCK + HEADER.size + 1) + except TimeoutError: + continue + if len(data) < HEADER.size: + raise RuntimeError('Truncated RJ2 header') + magic, kind, session, value = HEADER.unpack_from(data) + if (magic, session) != (protocol, nonce): + continue + if kind == 2: + if value == 2 and started is None: + continue + raise RuntimeError(f'RJ2 capture error {value} at packet {sequence}') + if kind != 1 or len(data) != BLOCK + HEADER.size: + raise RuntimeError(f'Invalid sample packet: type {kind}, length {len(data)}') + ahead = (value - sequence) & 0xffffffff + if ahead >= 0x80000000 or value in pending: + continue + if ahead >= REORDER_BLOCKS: + raise RuntimeError(f'Gap exceeded the reorder window: expected {sequence}, got {value}') + + now = time.monotonic() + if started is None: + started = now + pending[value] = data[HEADER.size:] + while sequence in pending and len(samples) * samples_per_byte < count: + samples.extend(pending.pop(sequence)) + sequence = (sequence + 1) & 0xffffffff + deadline = now + 2 + elapsed = time.monotonic() - started + finally: + send(2) + + changed = 0 + if packed: + first = samples[0] & 15 + dense = 0 + for pair in samples: + dense |= ((pair & 15) ^ first) | ((pair >> 4) ^ first) + lane = 0 + for channel in range(8): + if channels & (1 << channel): + changed |= ((dense >> lane) & 1) << channel + lane += 1 + else: + first = samples[0] + for sample in samples: + changed |= sample ^ first + changed &= channels + expected = clock / divider + received = max(0, len(samples) - BLOCK) * samples_per_byte / max(elapsed, 1e-6) + return len(samples) * samples_per_byte, expected, received, changed + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('address') + parser.add_argument('--divider', type=int, default=10) + parser.add_argument('--mask', type=lambda value: int(value, 0), default=0xff) + parser.add_argument('--samples', type=int, default=4 * 1024 * 1024) + args = parser.parse_args() + if not 1 <= args.divider <= 65535 or not BLOCK <= args.samples <= 64 * 1024 * 1024: + parser.error('divider 1..65535; samples 1024..67108864') + if not 1 <= args.mask <= 0xff: + parser.error('mask must select at least one channel in 0x01..0xff') + count, expected, received, changed = capture(args.address, args.divider, args.samples, args.mask) + print(f'{count:,} samples; FPGA {expected/1e6:.3f} MS/s; ' + f'received {received/1e6:.3f} MS/s; toggling channels mask 0x{changed:02x}') + + +if __name__ == '__main__': + main() diff --git a/hdl/logic-udp-ethernet/tests/tb_rtl8211f_init.sv b/hdl/logic-udp-ethernet/tests/tb_rtl8211f_init.sv new file mode 100644 index 0000000..bd1e8f0 --- /dev/null +++ b/hdl/logic-udp-ethernet/tests/tb_rtl8211f_init.sv @@ -0,0 +1,81 @@ +`timescale 1ns / 1ps +module tb_rtl8211f_init; + reg clk = 0; + reg reset = 1; + always #4 clk = ~clk; + + tri1 mdio; + wire mdc, ready, mdio_o, mdio_t; + assign mdio = mdio_t ? 1'bz : mdio_o; + rtl8211f_init dut ( + .clk(clk), .reset(reset), .phy_reset_n(), .mdc(mdc), + .mdio_i(mdio), .mdio_o(mdio_o), .mdio_t(mdio_t), .ready(ready) + ); + + reg [63:0] frame = 0; + reg [15:0] page = 0; + reg [15:0] tx_control = 16'h0040; + reg [15:0] rx_control = 16'h5000; + reg [15:0] reply; + reg [4:0] register_number; + reg reading; + reg phy_mdio = 1'bz; + integer bit_number = 0; + integer transactions = 0; + assign mdio = phy_mdio; + + always @(posedge mdc) begin + frame = {frame[62:0], mdio}; + if (bit_number == 45) begin + if (frame[45:14] != 32'hffffffff || frame[13:12] != 2'b01 || + frame[9:5] != 1 || (frame[11:10] != 2'b10 && frame[11:10] != 2'b01)) + $fatal(1, "Invalid MDIO frame header: %h", frame[45:0]); + register_number = frame[4:0]; + reading = frame[11:10] == 2'b10; + reply = register_number == 17 ? tx_control : rx_control; + end + + // Realtek specifies up to 300 ns from the rising edge to valid PHY data. + if (reading && bit_number >= 46 && !mdio_t) + $fatal(1, "FPGA drove MDIO during the PHY reply"); + if (reading && bit_number == 46) + phy_mdio <= #300 1'b0; + else if (reading && bit_number >= 47 && bit_number <= 62) + phy_mdio <= #300 reply[62 - bit_number]; + + if (bit_number == 63) begin + if (reading) begin + phy_mdio <= #300 1'bz; + end else begin + if (frame[17:16] != 2'b10) + $fatal(1, "Invalid MDIO write turnaround"); + case (register_number) + 31: page = frame[15:0]; + 17: if (page == 16'h0d08) tx_control = frame[15:0]; + 21: if (page == 16'h0d08) rx_control = frame[15:0]; + endcase + end + transactions = transactions + 1; + bit_number = 0; + reading = 0; + end else begin + bit_number = bit_number + 1; + end + end + + initial begin + #80 reset = 0; + force dut.boot_wait = 7500000; + wait (ready); + if (page != 0 || tx_control != 16'h0140 || rx_control != 16'h5008 || transactions != 8) + $fatal(1, "Unexpected PHY configuration: %h %h %h in %d transactions", + page, tx_control, rx_control, transactions); + $display("PASS tb_rtl8211f_init"); + $finish; + end + + initial begin + #500000; + $fatal(1, "PHY initialization timed out at step %d", dut.step); + end +endmodule diff --git a/hdl/logic-udp-ethernet/tests/tb_zynq_pl_capture.sv b/hdl/logic-udp-ethernet/tests/tb_zynq_pl_capture.sv new file mode 100644 index 0000000..34ce795 --- /dev/null +++ b/hdl/logic-udp-ethernet/tests/tb_zynq_pl_capture.sv @@ -0,0 +1,267 @@ +`timescale 1ns/1ps +module tb_zynq_pl_capture; + reg clk = 0; + always #4 clk = !clk; + reg sample_clk = 0; + always #2.5 sample_clk = !sample_clk; + reg reset = 1; + reg [7:0] input_sample = 0; + reg stimulus = 1; + reg slow_pattern = 0; + reg [9:0] phase = 0; + always @(posedge sample_clk) begin + phase <= phase + 1'b1; + input_sample <= slow_pattern ? (phase < 384 ? 8'h88 : 8'h04) : + stimulus ? input_sample + 1 : 0; + end + reg rx_header_valid = 0, rx_valid = 0, rx_last = 0; + reg [15:0] rx_length = 24; + reg [7:0] rx_data = 0; + wire tx_header_valid, tx_valid, tx_last; + wire [31:0] tx_ip; + wire [15:0] tx_port, tx_length; + wire [7:0] tx_data; + wire preview_valid; + wire [7:0] preview_sample; + + localparam [31:0] MAGIC = 32'h32504c5a; + localparam [31:0] BUFFER_MAGIC = 32'h31424c5a; + localparam [31:0] RLE_MAGIC = 32'h31524c5a; + localparam [31:0] TRIGGER_MAGIC = 32'h31544c5a; + localparam [31:0] CLIENT = 32'hc0a84d01; + localparam [31:0] NONCE = 32'h90123456; + localparam integer BLOCK = 1024; + integer wire_overhead = 0; + reg snapshot_mode = 0, rle_mode = 0, advanced_mode = 0, alternate = 0; + always @(posedge clk) alternate <= !alternate; + wire data_ready = !snapshot_mode || alternate; + wire header_ready = wire_overhead == 0; + always @(posedge clk) begin + if (reset) + wire_overhead <= 0; + else if (tx_valid && data_ready && tx_last) + wire_overhead <= 66; + else if (wire_overhead != 0) + wire_overhead <= wire_overhead - 1; + end + + zynq_pl_capture #(.CLOCK_HZ(125000), .BLOCK(BLOCK), + .BUFFER_ENABLED(1), .RLE_ENABLED(1)) dut ( + .clk(clk), .reset(reset), .sample_clk(sample_clk), .sample_reset(reset), + .digital_inputs(input_sample), + .rx_header_valid(rx_header_valid), .rx_ip(CLIENT), + .rx_port(16'd32100), .rx_destination(16'd37575), .rx_length(rx_length), + .rx_data(rx_data), .rx_valid(rx_valid), .rx_last(rx_last), .rx_error(1'b0), + .tx_header_valid(tx_header_valid), .tx_header_ready(header_ready), .tx_ip(tx_ip), + .tx_port(tx_port), .tx_length(tx_length), .tx_data(tx_data), + .tx_valid(tx_valid), .tx_ready(data_ready), .tx_last(tx_last), + .preview_valid(preview_valid), .preview_sample(preview_sample) + ); + + task send_command; + input [223:0] request; + input integer bytes; + integer i; + begin + @(negedge clk); + rx_length = bytes + 8; + rx_header_valid = 1; + @(negedge clk); + rx_header_valid = 0; + for (i = 0; i < bytes; i = i + 1) begin + rx_valid = 1; + rx_last = i == bytes - 1; + rx_data = request[i*8 +: 8]; + @(negedge clk); + end + rx_valid = 0; + rx_last = 0; + end + endtask + + task command; + input [31:0] op, nonce, divider; + send_command({96'd0, divider, nonce, op, MAGIC}, 16); + endtask + + integer infos = 0, frames = 0, errors = 0, configs = 0, index = 0; + reg [127:0] prefix = 0; + reg [7:0] sample_order [0:300000]; + reg raw_mode = 0; + reg [7:0] run_value; + integer written = 0, read_index = 0, run_sample; + always @(posedge sample_clk) if (preview_valid) begin + if (raw_mode && !rle_mode && written != 0 && + preview_sample !== (sample_order[written - 1] + ((snapshot_mode || advanced_mode) ? 8'd1 : 8'd2))) + $fatal(1, "raw mode sample interval is incorrect"); + sample_order[written] = raw_mode ? preview_sample : + {4'b0, preview_sample[7], preview_sample[5], preview_sample[2], preview_sample[0]}; + written = written + 1; + end + reg [15:0] length; + always @(posedge clk) begin + if (tx_header_valid && header_ready) begin + if (tx_ip != CLIENT || tx_port != 32100) $fatal(1, "wrong response address"); + length = tx_length; + end + if (tx_valid && data_ready) begin + if (index < 16) begin + prefix[index*8 +: 8] = tx_data; + end else begin + if (advanced_mode) begin + if (tx_data !== read_index[7:0]) + $fatal(1, "bad ordered sample or pretrigger position at %d", read_index); + read_index = read_index + 1; + end else if (rle_mode) begin + if ((index & 1) == 0) + run_value = tx_data; + else for (run_sample = 0; run_sample <= tx_data; run_sample = run_sample + 1) begin + if (run_value !== sample_order[read_index]) + $fatal(1, "wrong RLE sample %d", read_index); + read_index = read_index + 1; + end + end else if (raw_mode) begin + if (snapshot_mode && written != 65536) + $fatal(1, "snapshot transmitted before the buffer was frozen"); + if (tx_data !== sample_order[read_index]) + $fatal(1, "incorrect raw sample at index %d", read_index); + read_index = read_index + 1; + end else begin + if (tx_data !== {sample_order[read_index + 1][3:0], sample_order[read_index][3:0]}) + $fatal(1, "incorrect packed channels or sample order at index %d", read_index); + read_index = read_index + 2; + end + end + if (tx_last) begin + if (prefix[31:0] != (advanced_mode ? TRIGGER_MAGIC : + rle_mode ? RLE_MAGIC : snapshot_mode ? BUFFER_MAGIC : MAGIC) || + prefix[95:64] != NONCE || length != index + 9) + $fatal(1, "malformed response"); + case (prefix[63:32]) + 0: begin + if (index != 15 || prefix[127:96] != 200000000) $fatal(1, "bad INFO"); + infos = infos + 1; + end + 1: begin + if (index != BLOCK + 15 || prefix[127:96] != frames) + $fatal(1, "bad sequence"); + frames = frames + 1; + end + 2: begin + if (index != 15 || prefix[127:96] != (rle_mode ? 3 : 1)) + $fatal(1, "bad error"); + errors = errors + 1; + end + 3: begin + if (!advanced_mode || index != 15 || prefix[127:96] != configs) + $fatal(1, "bad configuration acknowledgement"); + configs = configs + 1; + end + default: $fatal(1, "unknown response"); + endcase + index = 0; + end else index = index + 1; + end + end + + integer after_stop; + initial begin + repeat (20) @(negedge clk); + reset = 0; + repeat (30) @(negedge clk); + command(0, NONCE, 0); + wait (infos == 1); + command(1, NONCE, 32'h00ff0001); + wait (errors == 1); + command(1, NONCE, 32'h00a50001); + wait (frames == 2); + command(2, NONCE ^ 1, 0); + wait (frames == 3); + command(3, NONCE, 0); + wait (frames == 128); + command(2, NONCE, 0); + repeat (180) @(negedge clk); + after_stop = frames; + repeat (300) @(negedge clk); + if (frames != after_stop || dut.active) $fatal(1, "STOP failed"); + wait (dut.fifo_empty && dut.command_idle && !dut.sending_samples); + raw_mode = 1; + frames = 0; + written = 0; + read_index = 0; + command(1, NONCE, 32'h00ff0002); + wait (frames == 8); + command(2, NONCE, 0); + repeat (200) @(negedge clk); + if (dut.active || dut.sampling) $fatal(1, "raw capture STOP failed"); + $display("ZLP2 PASS: 128 packed frames at 200 MS/s, 8 raw frames at 100 MS/s, divider limit, ownership and STOP"); + + wait (dut.fifo_empty && dut.command_idle && !dut.sending_samples); + stimulus = 0; + repeat (20) @(negedge clk); + snapshot_mode = 1; + frames = 0; + written = 0; + read_index = 0; + send_command({96'd0, 32'd0, NONCE, 32'd0, BUFFER_MAGIC}, 16); + wait (infos == 2); + // D3 rising edge, full buffer, 200 MHz acquisition. + send_command({32'd0, 32'h08080000, 32'd65536, 32'd1, NONCE, 32'd1, BUFFER_MAGIC}, 28); + wait (dut.buffer_active); + repeat (120) @(negedge clk); + if (written != 0 || frames != 0) $fatal(1, "snapshot started without its trigger"); + stimulus = 1; + wait (frames == 64); + if (written != 65536 || read_index != 65536 || sample_order[0] != 8) + $fatal(1, "incorrect snapshot count or trigger sample"); + send_command({96'd0, 32'd0, NONCE, 32'd2, BUFFER_MAGIC}, 16); + $display("ZLB1 PASS: D3 trigger, 65536 ordered samples at 200 MS/s, frozen before readout"); + + wait (dut.fifo_empty && dut.command_idle && !dut.sending_samples && !dut.active); + stimulus = 0; + repeat (20) @(negedge clk); + snapshot_mode = 0; + rle_mode = 1; + frames = 0; + written = 0; + read_index = 0; + send_command({96'd0, 32'd0, NONCE, 32'd0, RLE_MAGIC}, 16); + wait (infos == 3); + send_command({32'd0, 32'h08080000, 32'd0, 32'h00ff0001, NONCE, 32'd1, RLE_MAGIC}, 28); + wait (dut.rle_mode); + repeat (120) @(negedge clk); + if (written != 0 || frames != 0) $fatal(1, "RLE started without its trigger"); + slow_pattern = 1; + wait (frames == 1); + if (read_index <= 65536 || sample_order[0] != 8'h88) + $fatal(1, "RLE did not preserve the trigger or exceed the snapshot size"); + // A rapidly changing signal must raise overflow rather than lose edges silently. + slow_pattern = 0; + stimulus = 1; + wait (errors == 2); + if (dut.active) $fatal(1, "RLE overflow did not end capture"); + $display("ZLR1 PASS: D3 trigger, %d exact samples at 200 MS/s, overflow detected", read_index); + + wait (dut.fifo_empty && dut.command_idle && !dut.sending_samples); + rle_mode = 0; + advanced_mode = 1; + read_index = 0; + written = 0; + frames = 0; + send_command({32'd0, 32'd0, 32'h000034ff, 32'h10, NONCE, 32'd4, TRIGGER_MAGIC}, 28); + wait (configs == 1); + send_command({32'd0, 32'd0, 32'h000080ff, 32'h11, NONCE, 32'd4, TRIGGER_MAGIC}, 28); + wait (configs == 2); + send_command({32'd0, 32'h00800100, 32'd1024, 32'd1, NONCE, 32'd1, TRIGGER_MAGIC}, 28); + wait (frames == 1); + if (read_index != 1024 || sample_order[0] != 8'h80) + $fatal(1, "wrong staged trigger result"); + $display("ZLT1 PASS: two stages, 128 pretrigger samples, ordered BRAM window"); + $finish; + end + + initial begin + #4000000; + $fatal(1, "test timed out"); + end +endmodule diff --git a/hdl/logic-udp-ethernet/tests/tb_zynq_pl_ethernet.sv b/hdl/logic-udp-ethernet/tests/tb_zynq_pl_ethernet.sv new file mode 100644 index 0000000..c93648d --- /dev/null +++ b/hdl/logic-udp-ethernet/tests/tb_zynq_pl_ethernet.sv @@ -0,0 +1,104 @@ +`timescale 1ns / 1ps +module tb_zynq_pl_ethernet; + localparam [31:0] BOARD_IP = 32'hc0a80145; + localparam [31:0] HOST_IP = 32'hc0a80147; + localparam [47:0] HOST_MAC = 48'h021122334455; + localparam [31:0] NONCE = 32'h2a94b7dd; + localparam [479:0] ARP_REQUEST = { + 48'hffffffffffff, HOST_MAC, 16'h0806, + 16'h0001, 16'h0800, 8'd6, 8'd4, 16'h0001, + HOST_MAC, HOST_IP, 48'd0, BOARD_IP, 144'd0 + }; + localparam [479:0] HELLO = { + 48'h025a4c702002, HOST_MAC, 16'h0800, + 16'h4500, 16'd44, 16'h4321, 16'd0, 8'd64, 8'd17, 16'hb3c3, HOST_IP, BOARD_IP, + 16'd55003, 16'd37575, 16'd24, 16'd0, + 32'h5a4c5032, 32'd0, 32'hddb7942a, 32'd0, 16'd0 + }; + + reg clk = 0, sample_clk = 0, reset = 1; + always #4 clk = ~clk; + always #2.5 sample_clk = ~sample_clk; + + zynq_pl_ethernet #(.LOCAL_IP(BOARD_IP)) dut ( + .clk(clk), .sample_clk(sample_clk), .sample_reset(reset), + .delay_ref_clk(sample_clk), .reset(reset), .digital_inputs(8'd0), + .phy_rx_clk(clk), .phy_rxd(4'd0), .phy_rx_ctl(1'b0), .phy_mdio_i(1'b1) + ); + + reg [7:0] receive_data; + reg receive_valid = 0, receive_last = 0; + reg [7:0] response [0:59]; + integer replies = 0, pos = 0; + + always @(posedge clk) begin + if (dut.tx_axis_tvalid && dut.tx_axis_tready) begin + response[pos] = dut.tx_axis_tdata; + if (dut.tx_axis_tlast) begin + if ({response[0], response[1], response[2], response[3], response[4], response[5]} !== HOST_MAC) + $fatal(1, "Reply did not use the local host MAC"); + if (replies == 0) begin + if (pos != 41 || {response[12], response[13]} !== 16'h0806 || + {response[20], response[21]} !== 16'h0002) + $fatal(1, "Expected an ARP reply"); + end else begin + if (pos != 57 || {response[12], response[13]} !== 16'h0800 || + {response[30], response[31], response[32], response[33]} !== HOST_IP || + {response[34], response[35]} !== 16'd37575 || + {response[36], response[37]} !== 16'd55003 || + {response[42], response[43], response[44], response[45]} !== 32'h5a4c5032 || + {response[46], response[47], response[48], response[49]} !== 32'd0 || + {response[53], response[52], response[51], response[50]} !== NONCE || + {response[57], response[56], response[55], response[54]} !== 32'd200000000) + $fatal(1, "Expected a ZLP2 hello reply to the local host"); + end + replies = replies + 1; + pos = 0; + end else begin + pos = pos + 1; + end + end + end + + task send_frame; + input [479:0] frame; + begin + for (integer i = 59; i >= 0; i = i - 1) begin + @(negedge clk); + receive_data = frame[8*i +: 8]; + receive_valid = 1; + receive_last = i == 0; + @(posedge clk); + while (!dut.rx_axis_tready) @(posedge clk); + end + @(negedge clk); + receive_valid = 0; + receive_last = 0; + end + endtask + + initial begin + // The PHY and MAC are exercised on the board. Here frames enter at the MAC's AXI port. + force dut.rst = reset; + force dut.rx_axis_tdata = receive_data; + force dut.rx_axis_tvalid = receive_valid; + force dut.rx_axis_tlast = receive_last; + force dut.rx_axis_tuser = 0; + force dut.tx_axis_tready = 1; + repeat (10) @(negedge clk); + reset = 0; + repeat (600) @(negedge clk); + send_frame(ARP_REQUEST); + wait (replies == 1); + repeat (20) @(negedge clk); + send_frame(HELLO); + wait (replies == 2); + $display("PASS tb_zynq_pl_ethernet"); + $finish; + end + + initial begin + #100000; + $fatal(1, "Timed out waiting for Ethernet replies"); + end +endmodule diff --git a/hdl/scope-acquisition/README.md b/hdl/scope-acquisition/README.md new file mode 100644 index 0000000..2ccb10b --- /dev/null +++ b/hdl/scope-acquisition/README.md @@ -0,0 +1,13 @@ +# Ядро захвата для осциллографа + +`adc_frontend4` формирует согласованный поток четырёх каналов из двух +ADC-доменов. `sample_pairer` объединяет пары по индексу. `scope_core` +связывает AXI4-Lite, триггер, память окна и выдачу результата. Формат +регистров для C-клиента определён в `include/scope_regs.h`. + +Подавайте `sample_data64` и `sample_index64` при подтверждённом +`coherent_epoch_valid`. При сбросе согласованно очистите оба ADC FIFO, +сборщик пар и ядро. Контроллер платы задаёт электрические параметры +входов и проверяет временные ограничения ADC. `tests/tb_scope_core.sv` +проверяет реальный AXI-обмен, сброс эпохи и захват до/после триггера. +Отдельные тесты покрывают ADC, FIFO, пары и буфер. diff --git a/hdl/scope-acquisition/include/scope_regs.h b/hdl/scope-acquisition/include/scope_regs.h new file mode 100644 index 0000000..f250c8d --- /dev/null +++ b/hdl/scope-acquisition/include/scope_regs.h @@ -0,0 +1,84 @@ +#ifndef SCOPE_REGS_H +#define SCOPE_REGS_H + +#include + +/* Register ABI v1: four-channel ADC + bounded BRAM acquisition. + * No DMA/DDR, calibration, interleave or DIO register ABI is implemented. */ +#define SCOPE_EXPECTED_ID UINT32_C(0x53434f50) +#define SCOPE_ABI_VERSION UINT32_C(0x00010000) +#define SCOPE_DEFAULT_REFERENCE_RATE_HZ UINT32_C(100000000) +#define SCOPE_DEFAULT_CAPTURE_DEPTH UINT32_C(256) +#define SCOPE_FIXED_CHANNEL_ENABLE UINT32_C(0x0000000f) + +enum scope_register_offset { + SCOPE_REG_ID = 0x00, + SCOPE_REG_VERSION = 0x04, + SCOPE_REG_CONTROL = 0x08, + SCOPE_REG_STATUS = 0x0c, + SCOPE_REG_REFERENCE_RATE_HZ = 0x10, + SCOPE_REG_CHANNEL_ENABLE = 0x14, + SCOPE_REG_CAPABILITIES = 0x18, + SCOPE_REG_DEPTH = 0x1c, + SCOPE_REG_TRIGGER_SOURCE = 0x20, + SCOPE_REG_TRIGGER_LEVEL = 0x24, + SCOPE_REG_TRIGGER_MODE = 0x28, + SCOPE_REG_TRIGGER_EDGE = 0x2c, + SCOPE_REG_PRE = 0x30, + SCOPE_REG_POST = 0x34, + SCOPE_REG_HYSTERESIS = 0x38, + SCOPE_REG_AUTO_TIMEOUT = 0x3c, + SCOPE_REG_TRIGGER_INDEX_LO = 0x50, + SCOPE_REG_TRIGGER_INDEX_HI = 0x54, + SCOPE_REG_FIRST_INDEX_LO = 0x58, + SCOPE_REG_FIRST_INDEX_HI = 0x5c, + SCOPE_REG_RECORD_LENGTH = 0x60, + SCOPE_REG_SNAPSHOT_STATUS = 0x64, + SCOPE_REG_SAMPLE_COUNT_LO = 0x68, + SCOPE_REG_SAMPLE_COUNT_HI = 0x6c, + SCOPE_REG_FIFO1_OVERFLOW = 0x70, + SCOPE_REG_FIFO2_OVERFLOW = 0x74, + SCOPE_REG_TRIGGER_COUNT = 0x78 +}; + +/* Write exactly one command, then await the write response. Zero is a no-op. */ +#define SCOPE_CONTROL_ARM UINT32_C(0x00000001) +#define SCOPE_CONTROL_STOP UINT32_C(0x00000002) +#define SCOPE_CONTROL_SOFT_RESET UINT32_C(0x00000004) +#define SCOPE_CONTROL_SNAPSHOT UINT32_C(0x00000008) + +#define SCOPE_CAP_ADC4 UINT32_C(0x00000001) +#define SCOPE_CAP_BRAM_CAPTURE UINT32_C(0x00000002) +#define SCOPE_CAP_EXTERNAL_EPOCH UINT32_C(0x00000004) +#define SCOPE_CAPABILITIES_V1 UINT32_C(0x00000007) + +/* scope_core STATUS/SNAPSHOT_STATUS bits; all other bits are reserved zero. */ +#define SCOPE_STATUS_ACTIVE UINT32_C(0x00000001) +#define SCOPE_STATUS_DONE UINT32_C(0x00000002) +#define SCOPE_STATUS_FAULT UINT32_C(0x00000004) +#define SCOPE_STATUS_EPOCH_VALID UINT32_C(0x00000008) +#define SCOPE_STATUS_STREAM_VALID UINT32_C(0x00000010) +#define SCOPE_STATUS_FRONTEND_FAULT UINT32_C(0x00000020) +#define SCOPE_STATUS_CORE_RESET UINT32_C(0x00000040) + +enum scope_trigger_source { + SCOPE_TRIGGER_CH1 = 0, + SCOPE_TRIGGER_CH2 = 1, + SCOPE_TRIGGER_CH3 = 2, + SCOPE_TRIGGER_CH4 = 3, + SCOPE_TRIGGER_EXTERNAL = 4 +}; +enum scope_trigger_mode { + SCOPE_TRIGGER_AUTO = 0, + SCOPE_TRIGGER_NORMAL = 1, + SCOPE_TRIGGER_SINGLE = 2 +}; +enum scope_trigger_edge { + SCOPE_TRIGGER_RISING = 0, + SCOPE_TRIGGER_FALLING = 1 +}; + +/* These constants are defaults, not substitutes for capability/depth queries. + * 64-bit counters/indices are read only from the completed SNAPSHOT bank. + * Platform code supplies MMIO accessors, barriers and the peripheral base. */ +#endif diff --git a/hdl/scope-acquisition/rtl/acquisition_buffer.sv b/hdl/scope-acquisition/rtl/acquisition_buffer.sv new file mode 100644 index 0000000..8566297 --- /dev/null +++ b/hdl/scope-acquisition/rtl/acquisition_buffer.sv @@ -0,0 +1,99 @@ +`timescale 1ns/1ps +// Bounded BRAM reference acquisition: pre + trigger + post, then frozen drain. +// This is not the later continuously DMA-written DDR ring. +module acquisition_buffer #( + parameter integer ADDR_BITS = 8 +) ( + input wire clk, rst, arm, stop, session_fault, + input wire [31:0] cfg_pre, cfg_post, + input wire [63:0] sample_data, sample_index, + input wire sample_valid, + input wire trigger_hit, + output wire eligible, + output reg active, done, fault, + output reg [63:0] trigger_index, first_index, + output reg [31:0] record_length, + output reg [63:0] m_data, m_index, + output wire m_valid, + input wire m_ready, + output wire m_last +); + localparam integer DEPTH = 1 << ADDR_BITS; + (* ram_style = "block" *) reg [63:0] ram [0:DEPTH-1]; + reg [ADDR_BITS-1:0] write_ptr, read_ptr; + reg [31:0] pre_length, post_length, history_count, post_remaining, fetch_count; + reg [63:0] expected_index; + reg seen_sample, triggered, output_valid, output_last; + wire [32:0] cfg_total = {1'b0,cfg_pre}+{1'b0,cfg_post}+33'd1; + wire bad_index = seen_sample && sample_index != expected_index; + wire take_sample = active && sample_valid && !arm && !stop && !session_fault && + !fault && !bad_index; + wire fetch = done && !fault && !arm && !stop && !session_fault && + (!output_valid || m_ready) && fetch_count < record_length; + assign eligible = active && !fault && history_count >= pre_length; + assign m_valid = output_valid; + assign m_last = output_last && output_valid; + + initial if (ADDR_BITS < 2 || ADDR_BITS > 20) $error("acquisition_buffer ADDR_BITS 2..20 required"); + // RAM has no reset. Valid/state owns initialized payload. + always @(posedge clk) begin + if (take_sample) ram[write_ptr] <= sample_data; + if (fetch) m_data <= ram[read_ptr]; + end + + always @(posedge clk) begin + if (rst) begin + write_ptr<=0; read_ptr<=0; pre_length<=0; post_length<=0; + history_count<=0; post_remaining<=0; fetch_count<=0; + expected_index<=0; seen_sample<=0; triggered<=0; + active<=0; done<=0; fault<=0; + trigger_index<=0; first_index<=0; record_length<=0; + output_valid<=0; output_last<=0; m_index<=0; + end else begin + if (output_valid && m_ready) output_valid<=0; + // Once stalled, preserve the pending output even through faults. + if (fetch) begin + read_ptr<=read_ptr+1'b1; + m_index<=first_index+fetch_count; + output_last<=fetch_count+1 == record_length; + output_valid<=1; + fetch_count<=fetch_count+1'b1; + end + if (session_fault) begin fault<=1; active<=0; done<=0; end + else if (stop) begin active<=0; done<=0; end + else if (arm) begin + // A new ARM is rejected until the old output has drained. + if (active || output_valid || (done && fetch_count < record_length)) begin + fault<=1; active<=0; done<=0; + end else begin + write_ptr<=0; read_ptr<=0; history_count<=0; + seen_sample<=0; triggered<=0; fetch_count<=0; post_remaining<=0; + trigger_index<=0; first_index<=0; + pre_length<=cfg_pre; post_length<=cfg_post; + record_length<=cfg_total[31:0]; done<=0; + if (cfg_total > DEPTH) begin fault<=1; active<=0; end + else if (!fault) active<=1; + end + end else if (active && sample_valid) begin + if (bad_index || (trigger_hit && (!eligible || sample_index < pre_length))) begin + fault<=1; active<=0; done<=0; + end else begin + write_ptr<=write_ptr+1'b1; + expected_index<=sample_index+1'b1; + seen_sample<=1; + if (history_count < pre_length) history_count<=history_count+1'b1; + if (!triggered && trigger_hit) begin + triggered<=1; trigger_index<=sample_index; + first_index<=sample_index-pre_length; + read_ptr<=write_ptr-pre_length; + post_remaining<=post_length; + if (post_length==0) begin active<=0; done<=1; end + end else if (triggered) begin + post_remaining<=post_remaining-1'b1; + if (post_remaining==1) begin active<=0; done<=1; end + end + end + end + end + end +endmodule diff --git a/hdl/scope-acquisition/rtl/adc_capture.sv b/hdl/scope-acquisition/rtl/adc_capture.sv new file mode 100644 index 0000000..2a6e7fe --- /dev/null +++ b/hdl/scope-acquisition/rtl/adc_capture.sv @@ -0,0 +1,48 @@ +`timescale 1ns/1ps +// One physical AD9216, cores A and B in one forwarded-clock domain. +// Raw pad registers have no reset or clock enable to permit IOB placement. +// Offset binary, right aligned: [15:10]=0, [31:26]=0. +// ADC wake-up/reference settling is external; ready must mean already settled. +module adc_capture ( + input wire adc_clk, + input wire arst_n, + input wire ready_async, + input wire [9:0] adc_data_a, + input wire [9:0] adc_data_b, + output reg [31:0] sample_word, + output reg sample_valid, + output reg [63:0] sample_index +); + wire rst; + reset_sync reset_i (.clk(adc_clk), .arst_n(arst_n), .rst(rst)); + (* IOB = "TRUE" *) reg [9:0] input_a, input_b; + (* ASYNC_REG = "TRUE", SHREG_EXTRACT = "NO" *) reg [1:0] ready_pipe; + reg warm; + reg [63:0] next_index; + always @(posedge adc_clk) begin + input_a <= adc_data_a; + input_b <= adc_data_b; + end + always @(posedge adc_clk or negedge arst_n) begin + if (!arst_n) ready_pipe <= 0; + else if (rst) ready_pipe <= 0; + else ready_pipe <= {ready_pipe[0], ready_async}; + end + always @(posedge adc_clk) begin + if (rst) begin + sample_word <= 0; + sample_valid <= 0; + sample_index <= 0; + next_index <= 0; + warm <= 0; + end else begin + sample_word <= {6'b0, input_b, 6'b0, input_a}; + warm <= ready_pipe[1]; + sample_valid <= ready_pipe[1] && warm; + if (ready_pipe[1] && warm) begin + sample_index <= next_index; + next_index <= next_index + 1'b1; + end + end + end +endmodule diff --git a/hdl/scope-acquisition/rtl/adc_capture_dual.sv b/hdl/scope-acquisition/rtl/adc_capture_dual.sv new file mode 100644 index 0000000..ebb815f --- /dev/null +++ b/hdl/scope-acquisition/rtl/adc_capture_dual.sv @@ -0,0 +1,19 @@ +`timescale 1ns/1ps +// Four channels; two outputs remain in two different clock domains. +// No pairing or direct multi-bit transfer between these outputs is permitted. +module adc_capture_dual ( + input wire arst_n, + input wire mezz_ready, + input wire adc1_clk, adc2_clk, + input wire [9:0] adc1_a, adc1_b, adc2_a, adc2_b, + output wire [31:0] chip1_word, chip2_word, + output wire chip1_valid, chip2_valid, + output wire [63:0] chip1_index, chip2_index +); + adc_capture chip1(.adc_clk(adc1_clk), .arst_n(arst_n), + .ready_async(mezz_ready), .adc_data_a(adc1_a), .adc_data_b(adc1_b), + .sample_word(chip1_word), .sample_valid(chip1_valid), .sample_index(chip1_index)); + adc_capture chip2(.adc_clk(adc2_clk), .arst_n(arst_n), + .ready_async(mezz_ready), .adc_data_a(adc2_a), .adc_data_b(adc2_b), + .sample_word(chip2_word), .sample_valid(chip2_valid), .sample_index(chip2_index)); +endmodule diff --git a/hdl/scope-acquisition/rtl/adc_frontend4.sv b/hdl/scope-acquisition/rtl/adc_frontend4.sv new file mode 100644 index 0000000..445a0f9 --- /dev/null +++ b/hdl/scope-acquisition/rtl/adc_frontend4.sv @@ -0,0 +1,42 @@ +`timescale 1ns/1ps +// Provisional four-channel front end; caller must establish physical epoch. +module adc_frontend4 #( + parameter integer FIFO_ADDR_BITS = 9 +) ( + input wire arst_n, + input wire mezz_ready, + input wire adc1_clk, adc2_clk, + input wire [9:0] adc1_a, adc1_b, adc2_a, adc2_b, + input wire stream_clk, + input wire coherent_epoch_valid, + output wire [63:0] sample_data, sample_index, + output wire sample_valid, + input wire sample_ready, + output wire session_fault, + output wire [31:0] adc1_overflow_count, adc2_overflow_count +); + wire [95:0] data1, data2; + wire valid1, valid2, ready1, ready2, fault1, fault2, pair_fault, stream_rst; + capture_stream #(.FIFO_ADDR_BITS(FIFO_ADDR_BITS)) stream1 ( + .arst_n(arst_n), .adc_clk(adc1_clk), .ready_async(mezz_ready), + .adc_data_a(adc1_a), .adc_data_b(adc1_b), .stream_clk(stream_clk), + .stream_data(data1), .stream_valid(valid1), .stream_ready(ready1), + .session_fault(fault1), .overflow_count_adc(adc1_overflow_count) + ); + capture_stream #(.FIFO_ADDR_BITS(FIFO_ADDR_BITS)) stream2 ( + .arst_n(arst_n), .adc_clk(adc2_clk), .ready_async(mezz_ready), + .adc_data_a(adc2_a), .adc_data_b(adc2_b), .stream_clk(stream_clk), + .stream_data(data2), .stream_valid(valid2), .stream_ready(ready2), + .session_fault(fault2), .overflow_count_adc(adc2_overflow_count) + ); + reset_sync stream_reset_i (.clk(stream_clk), .arst_n(arst_n), .rst(stream_rst)); + sample_pairer pair_i ( + .clk(stream_clk), .rst(stream_rst), .coherent_epoch_valid(coherent_epoch_valid), + .session_fault(fault1 || fault2), + .a_data(data1), .a_valid(valid1), .a_ready(ready1), + .b_data(data2), .b_valid(valid2), .b_ready(ready2), + .m_data(sample_data), .m_index(sample_index), + .m_valid(sample_valid), .m_ready(sample_ready), .fault(pair_fault) + ); + assign session_fault = fault1 || fault2 || pair_fault; +endmodule diff --git a/hdl/scope-acquisition/rtl/async_sample_fifo.sv b/hdl/scope-acquisition/rtl/async_sample_fifo.sv new file mode 100644 index 0000000..b697a66 --- /dev/null +++ b/hdl/scope-acquisition/rtl/async_sample_fifo.sv @@ -0,0 +1,115 @@ +`timescale 1ns/1ps +// Portable dual-clock FIFO. RAM words cross domains only through a clocked +// read port; Gray pointers cross through independent two-stage synchronizers. +// Reset is shared by both domains. The output register adds one holding slot +// beyond the 2**ADDR_BITS RAM locations after the read clock starts. +module async_sample_fifo #( + parameter integer WIDTH = 96, + parameter integer ADDR_BITS = 9 +) ( + input wire arst_n, + input wire wr_clk, + input wire wr_valid, + input wire [WIDTH-1:0] wr_data, + output wire wr_ready, + output reg wr_overflow, + output reg wr_overflow_sticky, + output reg [31:0] wr_overflow_count, + input wire rd_clk, + output wire rd_valid, + output reg [WIDTH-1:0] rd_data, + input wire rd_ready +); + localparam integer POINTER_BITS = ADDR_BITS + 1; + localparam integer DEPTH = 1 << ADDR_BITS; + wire wr_rst, rd_rst; + reset_sync wr_reset (.clk(wr_clk), .arst_n(arst_n), .rst(wr_rst)); + reset_sync rd_reset (.clk(rd_clk), .arst_n(arst_n), .rst(rd_rst)); + + (* ram_style = "block" *) reg [WIDTH-1:0] ram [0:DEPTH-1]; + reg [POINTER_BITS-1:0] wr_binary, wr_gray; + reg [POINTER_BITS-1:0] rd_binary, rd_gray; + (* ASYNC_REG = "TRUE", SHREG_EXTRACT = "NO" *) + reg [POINTER_BITS-1:0] rd_gray_w1, rd_gray_w2; + (* ASYNC_REG = "TRUE", SHREG_EXTRACT = "NO" *) + reg [POINTER_BITS-1:0] wr_gray_r1, wr_gray_r2; + reg wr_full, rd_empty, output_valid; + + assign wr_ready = arst_n && !wr_rst && !wr_full; + assign rd_valid = arst_n && !rd_rst && output_valid; + wire wr_accept = wr_valid && wr_ready; + wire rd_fetch = !rd_rst && arst_n && (!output_valid || rd_ready) && !rd_empty; + wire [POINTER_BITS-1:0] wr_binary_next = wr_binary + wr_accept; + wire [POINTER_BITS-1:0] rd_binary_next = rd_binary + rd_fetch; + wire [POINTER_BITS-1:0] wr_gray_next = (wr_binary_next >> 1) ^ wr_binary_next; + wire [POINTER_BITS-1:0] rd_gray_next = (rd_binary_next >> 1) ^ rd_binary_next; + wire wr_full_next = wr_gray_next == + {~rd_gray_w2[ADDR_BITS:ADDR_BITS-1], rd_gray_w2[ADDR_BITS-2:0]}; + wire rd_empty_next = rd_gray_next == wr_gray_r2; + + initial begin + if (WIDTH < 1 || ADDR_BITS < 2) + $error("async_sample_fifo requires WIDTH >= 1 and ADDR_BITS >= 2"); + end + + // No RAM/output-data reset: the valid flags own initialized state, and + // keeping these ports clocked without reset permits dual-port BRAM inference. + always @(posedge wr_clk) + if (wr_accept) ram[wr_binary[ADDR_BITS-1:0]] <= wr_data; + always @(posedge rd_clk) + if (rd_fetch) rd_data <= ram[rd_binary[ADDR_BITS-1:0]]; + + always @(posedge wr_clk or posedge wr_rst) begin + if (wr_rst) begin + rd_gray_w1 <= 0; + rd_gray_w2 <= 0; + end else begin + rd_gray_w1 <= rd_gray; + rd_gray_w2 <= rd_gray_w1; + end + end + always @(posedge rd_clk or posedge rd_rst) begin + if (rd_rst) begin + wr_gray_r1 <= 0; + wr_gray_r2 <= 0; + end else begin + wr_gray_r1 <= wr_gray; + wr_gray_r2 <= wr_gray_r1; + end + end + + always @(posedge wr_clk or posedge wr_rst) begin + if (wr_rst) begin + wr_binary <= 0; + wr_gray <= 0; + wr_full <= 0; + wr_overflow <= 0; + wr_overflow_sticky <= 0; + wr_overflow_count <= 0; + end else begin + wr_binary <= wr_binary_next; + wr_gray <= wr_gray_next; + wr_full <= wr_full_next; + wr_overflow <= wr_valid && !wr_ready; + if (wr_valid && !wr_ready) begin + wr_overflow_sticky <= 1'b1; + if (wr_overflow_count != 32'hffffffff) + wr_overflow_count <= wr_overflow_count + 1'b1; + end + end + end + always @(posedge rd_clk or posedge rd_rst) begin + if (rd_rst) begin + rd_binary <= 0; + rd_gray <= 0; + rd_empty <= 1; + output_valid <= 0; + end else begin + rd_binary <= rd_binary_next; + rd_gray <= rd_gray_next; + rd_empty <= rd_empty_next; + if (!output_valid || rd_ready) + output_valid <= rd_fetch; + end + end +endmodule diff --git a/hdl/scope-acquisition/rtl/axi_scope_regs.sv b/hdl/scope-acquisition/rtl/axi_scope_regs.sv new file mode 100644 index 0000000..fd2bb90 --- /dev/null +++ b/hdl/scope-acquisition/rtl/axi_scope_regs.sv @@ -0,0 +1,247 @@ +`timescale 1ns/1ps +// Single-clock AXI4-Lite register bank. All *_live inputs must already be +// coherent in clk; this block does not synchronize ADC-domain status buses. +module axi_scope_regs #( + parameter integer ADDR_WIDTH = 12, + parameter [31:0] REFERENCE_RATE_HZ = 32'd100000000, + parameter integer CAPTURE_ADDR_BITS = 8 +) ( + input wire clk, + input wire aresetn, + input wire [ADDR_WIDTH-1:0] s_axi_awaddr, + input wire [2:0] s_axi_awprot, + input wire s_axi_awvalid, + output wire s_axi_awready, + input wire [31:0] s_axi_wdata, + input wire [3:0] s_axi_wstrb, + input wire s_axi_wvalid, + output wire s_axi_wready, + output reg [1:0] s_axi_bresp, + output reg s_axi_bvalid, + input wire s_axi_bready, + input wire [ADDR_WIDTH-1:0] s_axi_araddr, + input wire [2:0] s_axi_arprot, + input wire s_axi_arvalid, + output wire s_axi_arready, + output reg [31:0] s_axi_rdata, + output reg [1:0] s_axi_rresp, + output reg s_axi_rvalid, + input wire s_axi_rready, + output reg [2:0] cfg_source, + output reg [1:0] cfg_mode, + output reg cfg_falling, + output reg [9:0] cfg_level, + output reg [9:0] cfg_hysteresis, + output reg [31:0] cfg_pre, + output reg [31:0] cfg_post, + output reg [31:0] cfg_auto_timeout, + output reg arm_pulse, + output reg stop_pulse, + output reg soft_reset_pulse, + output reg snapshot_pulse, + input wire [31:0] status_live, + input wire [63:0] trigger_index_live, + input wire [63:0] first_index_live, + input wire [31:0] record_length_live, + input wire [63:0] sample_count_live, + input wire [31:0] fifo1_overflow_live, + input wire [31:0] fifo2_overflow_live, + input wire [31:0] trigger_count_live +); + localparam [1:0] OKAY = 2'b00, SLVERR = 2'b10; + localparam [31:0] CAPTURE_DEPTH = 32'd1 << CAPTURE_ADDR_BITS; + reg aw_pending, w_pending; + reg [ADDR_WIDTH-1:0] write_address; + reg [31:0] write_data; + reg [3:0] write_strobes; + reg [63:0] trigger_snapshot, first_snapshot, samples_snapshot; + reg [31:0] length_snapshot, status_snapshot; + reg [31:0] fifo1_snapshot, fifo2_snapshot, trigger_count_snapshot; + reg [31:0] merged_value; + + assign s_axi_awready = aresetn && !aw_pending && !s_axi_bvalid; + assign s_axi_wready = aresetn && !w_pending && !s_axi_bvalid; + assign s_axi_arready = aresetn && !s_axi_rvalid; + + initial begin + if (ADDR_WIDTH < 8 || CAPTURE_ADDR_BITS < 1 || CAPTURE_ADDR_BITS > 30) + $error("axi_scope_regs requires ADDR_WIDTH >= 8 and CAPTURE_ADDR_BITS in 1..30"); + end + + function automatic [31:0] merge_bytes(input [31:0] old_value, + input [31:0] new_value, input [3:0] byte_strobes); + integer byte_number; + begin + merge_bytes = old_value; + for (byte_number = 0; byte_number < 4; byte_number = byte_number + 1) + if (byte_strobes[byte_number]) + merge_bytes[byte_number*8 +: 8] = new_value[byte_number*8 +: 8]; + end + endfunction + + // Read responses capture the addressed value at the AR handshake and + // retain data/response until RREADY. Snapshot words are a separate bank. + function automatic [33:0] read_value(input [ADDR_WIDTH-1:0] address); + begin + read_value = {SLVERR, 32'd0}; + if (address[1:0] == 0) begin + case (address) + 'h00: read_value = {OKAY, 32'h53434f50}; + 'h04: read_value = {OKAY, 32'h00010000}; + 'h08: read_value = {OKAY, 32'd0}; + 'h0c: read_value = {OKAY, status_live}; + 'h10: read_value = {OKAY, REFERENCE_RATE_HZ}; + 'h14: read_value = {OKAY, 32'h0000000f}; + 'h18: read_value = {OKAY, 32'h00000007}; + 'h1c: read_value = {OKAY, CAPTURE_DEPTH}; + 'h20: read_value = {OKAY, 29'd0, cfg_source}; + 'h24: read_value = {OKAY, 22'd0, cfg_level}; + 'h28: read_value = {OKAY, 30'd0, cfg_mode}; + 'h2c: read_value = {OKAY, 31'd0, cfg_falling}; + 'h30: read_value = {OKAY, cfg_pre}; + 'h34: read_value = {OKAY, cfg_post}; + 'h38: read_value = {OKAY, 22'd0, cfg_hysteresis}; + 'h3c: read_value = {OKAY, cfg_auto_timeout}; + 'h50: read_value = {OKAY, trigger_snapshot[31:0]}; + 'h54: read_value = {OKAY, trigger_snapshot[63:32]}; + 'h58: read_value = {OKAY, first_snapshot[31:0]}; + 'h5c: read_value = {OKAY, first_snapshot[63:32]}; + 'h60: read_value = {OKAY, length_snapshot}; + 'h64: read_value = {OKAY, status_snapshot}; + 'h68: read_value = {OKAY, samples_snapshot[31:0]}; + 'h6c: read_value = {OKAY, samples_snapshot[63:32]}; + 'h70: read_value = {OKAY, fifo1_snapshot}; + 'h74: read_value = {OKAY, fifo2_snapshot}; + 'h78: read_value = {OKAY, trigger_count_snapshot}; + default: read_value = {SLVERR, 32'd0}; + endcase + end + end + endfunction + + always @(posedge clk) begin + if (!aresetn) begin + aw_pending <= 0; + w_pending <= 0; + write_address <= 0; + write_data <= 0; + write_strobes <= 0; + s_axi_bvalid <= 0; + s_axi_bresp <= OKAY; + cfg_source <= 0; + cfg_level <= 512; + cfg_mode <= 1; + cfg_falling <= 0; + cfg_pre <= 64; + cfg_post <= 64; + cfg_hysteresis <= 4; + cfg_auto_timeout <= 1000000; + arm_pulse <= 0; + stop_pulse <= 0; + soft_reset_pulse <= 0; + snapshot_pulse <= 0; + trigger_snapshot <= 0; + first_snapshot <= 0; + samples_snapshot <= 0; + length_snapshot <= 0; + status_snapshot <= 0; + fifo1_snapshot <= 0; + fifo2_snapshot <= 0; + trigger_count_snapshot <= 0; + end else begin + arm_pulse <= 0; + stop_pulse <= 0; + soft_reset_pulse <= 0; + snapshot_pulse <= 0; + if (s_axi_bvalid && s_axi_bready) s_axi_bvalid <= 0; + if (s_axi_awvalid && s_axi_awready) begin + write_address <= s_axi_awaddr; + aw_pending <= 1; + end + if (s_axi_wvalid && s_axi_wready) begin + write_data <= s_axi_wdata; + write_strobes <= s_axi_wstrb; + w_pending <= 1; + end + if (aw_pending && w_pending && !s_axi_bvalid) begin + aw_pending <= 0; + w_pending <= 0; + s_axi_bvalid <= 1; + s_axi_bresp <= OKAY; + if (write_address[1:0] != 0) begin + s_axi_bresp <= SLVERR; + end else begin + case (write_address) + 'h08: begin + // Without low-byte enable this command is a no-op. + if (write_strobes[0]) begin + merged_value = merge_bytes(0, write_data, write_strobes); + case (merged_value) + 32'd0: begin end + 32'd1: arm_pulse <= 1; + 32'd2: stop_pulse <= 1; + 32'd4: soft_reset_pulse <= 1; + 32'd8: begin + snapshot_pulse <= 1; + trigger_snapshot <= trigger_index_live; + first_snapshot <= first_index_live; + samples_snapshot <= sample_count_live; + length_snapshot <= record_length_live; + status_snapshot <= status_live; + fifo1_snapshot <= fifo1_overflow_live; + fifo2_snapshot <= fifo2_overflow_live; + trigger_count_snapshot <= trigger_count_live; + end + default: s_axi_bresp <= SLVERR; + endcase + end + end + 'h20: begin + merged_value = merge_bytes({29'd0, cfg_source}, write_data, write_strobes); + if (merged_value <= 4) cfg_source <= merged_value[2:0]; + else s_axi_bresp <= SLVERR; + end + 'h24: begin + merged_value = merge_bytes({22'd0, cfg_level}, write_data, write_strobes); + if (merged_value <= 1023) cfg_level <= merged_value[9:0]; + else s_axi_bresp <= SLVERR; + end + 'h28: begin + merged_value = merge_bytes({30'd0, cfg_mode}, write_data, write_strobes); + if (merged_value <= 2) cfg_mode <= merged_value[1:0]; + else s_axi_bresp <= SLVERR; + end + 'h2c: begin + merged_value = merge_bytes({31'd0, cfg_falling}, write_data, write_strobes); + if (merged_value <= 1) cfg_falling <= merged_value[0]; + else s_axi_bresp <= SLVERR; + end + 'h30: cfg_pre <= merge_bytes(cfg_pre, write_data, write_strobes); + 'h34: cfg_post <= merge_bytes(cfg_post, write_data, write_strobes); + 'h38: begin + merged_value = merge_bytes({22'd0, cfg_hysteresis}, write_data, write_strobes); + if (merged_value <= 1023) cfg_hysteresis <= merged_value[9:0]; + else s_axi_bresp <= SLVERR; + end + 'h3c: cfg_auto_timeout <= merge_bytes(cfg_auto_timeout, write_data, write_strobes); + default: s_axi_bresp <= SLVERR; + endcase + end + end + end + end + + always @(posedge clk) begin + if (!aresetn) begin + s_axi_rvalid <= 0; + s_axi_rdata <= 0; + s_axi_rresp <= OKAY; + end else begin + if (s_axi_rvalid && s_axi_rready) s_axi_rvalid <= 0; + if (s_axi_arvalid && s_axi_arready) begin + {s_axi_rresp, s_axi_rdata} <= read_value(s_axi_araddr); + s_axi_rvalid <= 1; + end + end + end +endmodule diff --git a/hdl/scope-acquisition/rtl/capture_stream.sv b/hdl/scope-acquisition/rtl/capture_stream.sv new file mode 100644 index 0000000..04f1ecb --- /dev/null +++ b/hdl/scope-acquisition/rtl/capture_stream.sv @@ -0,0 +1,57 @@ +`timescale 1ns/1ps +// One ADC chip -> indexed asynchronous stream. Fault is sticky in rd_clk. +module capture_stream #( + parameter integer FIFO_ADDR_BITS = 9 +) ( + input wire arst_n, + input wire adc_clk, + input wire ready_async, + input wire [9:0] adc_data_a, adc_data_b, + input wire stream_clk, + output wire [95:0] stream_data, + output wire stream_valid, + input wire stream_ready, + output wire session_fault, + output wire [31:0] overflow_count_adc +); + wire [31:0] sample_word; + wire [63:0] sample_index; + wire sample_valid, adc_rst; + wire fifo_ready, overflow_pulse, overflow_sticky; + reg seen_valid, ready_lost; + (* ASYNC_REG = "TRUE", SHREG_EXTRACT = "NO" *) reg [1:0] fault_sync; + wire stream_rst; + + adc_capture capture_i ( + .adc_clk(adc_clk), .arst_n(arst_n), .ready_async(ready_async), + .adc_data_a(adc_data_a), .adc_data_b(adc_data_b), + .sample_word(sample_word), .sample_valid(sample_valid), + .sample_index(sample_index) + ); + reset_sync adc_reset_i (.clk(adc_clk), .arst_n(arst_n), .rst(adc_rst)); + reset_sync stream_reset_i (.clk(stream_clk), .arst_n(arst_n), .rst(stream_rst)); + async_sample_fifo #(.WIDTH(96), .ADDR_BITS(FIFO_ADDR_BITS)) fifo_i ( + .arst_n(arst_n), .wr_clk(adc_clk), .wr_valid(sample_valid), + .wr_data({sample_index, sample_word}), .wr_ready(fifo_ready), + .wr_overflow(overflow_pulse), .wr_overflow_sticky(overflow_sticky), + .wr_overflow_count(overflow_count_adc), + .rd_clk(stream_clk), .rd_valid(stream_valid), + .rd_data(stream_data), .rd_ready(stream_ready) + ); + + always @(posedge adc_clk or posedge adc_rst) begin + if (adc_rst) begin seen_valid <= 0; ready_lost <= 0; end + else begin + if (sample_valid) seen_valid <= 1; + if (seen_valid && !sample_valid) ready_lost <= 1; + end + end + always @(posedge stream_clk or posedge stream_rst) begin + if (stream_rst) fault_sync <= 0; + else fault_sync <= {fault_sync[0], ready_lost || overflow_sticky}; + end + assign session_fault = fault_sync[1]; + // overflow_count_adc belongs to adc_clk. ARM must use a coherent snapshot, + // not independent per-bit synchronizers. A stopped ADC clock needs an + // external watchdog: this block cannot detect absence of its own clock. +endmodule diff --git a/hdl/scope-acquisition/rtl/reset_sync.sv b/hdl/scope-acquisition/rtl/reset_sync.sv new file mode 100644 index 0000000..2df7ad6 --- /dev/null +++ b/hdl/scope-acquisition/rtl/reset_sync.sv @@ -0,0 +1,13 @@ +`timescale 1ns/1ps +// Asynchronous assertion, synchronous release in each destination clock. +module reset_sync ( + input wire clk, + input wire arst_n, + output wire rst +); + (* ASYNC_REG = "TRUE", SHREG_EXTRACT = "NO" *) reg [1:0] release_pipe; + always @(posedge clk or negedge arst_n) + if (!arst_n) release_pipe <= 2'b00; + else release_pipe <= {release_pipe[0], 1'b1}; + assign rst = ~release_pipe[1]; +endmodule diff --git a/hdl/scope-acquisition/rtl/sample_pairer.sv b/hdl/scope-acquisition/rtl/sample_pairer.sv new file mode 100644 index 0000000..df94f33 --- /dev/null +++ b/hdl/scope-acquisition/rtl/sample_pairer.sv @@ -0,0 +1,64 @@ +`timescale 1ns/1ps + +// Fixed four-channel pairing in the FIFO read/AXIS clock domain. +// Inputs are stable FIFO heads: {local_index[63:0], chip_word[31:0]}. +// Equality of local indices does not establish a common physical ADC epoch. +module sample_pairer ( + input wire clk, + input wire rst, + input wire coherent_epoch_valid, + input wire session_fault, + input wire [95:0] a_data, + input wire a_valid, + output wire a_ready, + input wire [95:0] b_data, + input wire b_valid, + output wire b_ready, + output reg [63:0] m_data, + output reg [63:0] m_index, + output reg m_valid, + input wire m_ready, + output reg fault +); + reg [63:0] expected_index; + reg epoch_seen; + wire both_valid = a_valid && b_valid; + wire index_bad = (a_data[95:32] != b_data[95:32]) || + (a_data[95:32] != expected_index); + wire epoch_lost = epoch_seen && !coherent_epoch_valid; + wire fault_event = session_fault || epoch_lost || + (coherent_epoch_valid && both_valid && index_bad); + wire slot_available = !m_valid || m_ready; + wire pair_fire = !rst && !fault && !fault_event && + coherent_epoch_valid && both_valid && slot_available; + + // A pair is consumed atomically. Neither FIFO may discard/rebase a head. + assign a_ready = pair_fire; + assign b_ready = pair_fire; + + always @(posedge clk) begin + if (rst) begin + m_data <= 0; + m_index <= 0; + m_valid <= 0; + fault <= 0; + expected_index <= 0; + epoch_seen <= 0; + end else begin + // A previously accepted word remains valid through a later fault. + // Once stalled, AXIS-like valid/payload may change only on transfer + // or an explicit interface reset; acquisition validity is external. + if (m_valid && m_ready) + m_valid <= 0; + if (fault_event) + fault <= 1; + if (pair_fire) begin + m_data <= {b_data[31:0], a_data[31:0]}; + m_index <= a_data[95:32]; + m_valid <= 1; + expected_index <= expected_index + 1'b1; + epoch_seen <= 1; + end + end + end +endmodule diff --git a/hdl/scope-acquisition/rtl/scope_core.sv b/hdl/scope-acquisition/rtl/scope_core.sv new file mode 100644 index 0000000..20107b2 --- /dev/null +++ b/hdl/scope-acquisition/rtl/scope_core.sv @@ -0,0 +1,91 @@ +`timescale 1ns/1ps +// AXI control + trigger/window reference, in one processing clock domain. +// An adc_frontend4 instance supplies the indexed stream; platform glue must +// route frontend_reset_request to the joint frontend reset and establish epoch. +module scope_core #( + parameter integer CAPTURE_ADDR_BITS = 8 +) ( + input wire clk, aresetn, + input wire [11:0] s_axi_awaddr, + input wire [2:0] s_axi_awprot, + input wire s_axi_awvalid, + output wire s_axi_awready, + input wire [31:0] s_axi_wdata, + input wire [3:0] s_axi_wstrb, + input wire s_axi_wvalid, + output wire s_axi_wready, + output wire [1:0] s_axi_bresp, + output wire s_axi_bvalid, + input wire s_axi_bready, + input wire [11:0] s_axi_araddr, + input wire [2:0] s_axi_arprot, + input wire s_axi_arvalid, + output wire s_axi_arready, + output wire [31:0] s_axi_rdata, + output wire [1:0] s_axi_rresp, + output wire s_axi_rvalid, + input wire s_axi_rready, + input wire [63:0] sample_data, sample_index, + input wire sample_valid, + output wire sample_ready, + input wire coherent_epoch_valid, frontend_fault, ext_level, + // These must already be coherent clk-domain snapshots, not ADC buses. + input wire [31:0] fifo1_overflow_live, fifo2_overflow_live, + output wire frontend_reset_request, + output wire [63:0] m_data, m_index, + output wire m_valid, m_last, + input wire m_ready, + output wire irq +); + wire [2:0] cfg_source; + wire [1:0] cfg_mode; + wire cfg_falling; + wire [9:0] cfg_level, cfg_hysteresis; + wire [31:0] cfg_pre, cfg_post, cfg_auto_timeout; + wire arm_pulse, stop_pulse, soft_reset_pulse, snapshot_pulse; + wire active, done, fault, core_rst, trigger_event; + wire [63:0] trigger_index_live, first_index_live; + wire [31:0] record_length_live; + reg [63:0] sample_count_live; + reg [31:0] trigger_count_live; + wire session_fault = frontend_fault || (!coherent_epoch_valid && (arm_pulse || active)); + wire [31:0] status_live = {25'b0, core_rst, frontend_fault, m_valid, + coherent_epoch_valid, (fault || frontend_fault), done, active}; + assign frontend_reset_request = soft_reset_pulse; + reset_sync core_reset_i (.clk(clk), .arst_n(aresetn && !soft_reset_pulse), .rst(core_rst)); + assign sample_ready = !core_rst; + assign irq = done || fault || frontend_fault; + + axi_scope_regs #(.CAPTURE_ADDR_BITS(CAPTURE_ADDR_BITS)) registers_i ( + .clk(clk), .aresetn(aresetn), + .s_axi_awaddr(s_axi_awaddr), .s_axi_awprot(s_axi_awprot), .s_axi_awvalid(s_axi_awvalid), .s_axi_awready(s_axi_awready), + .s_axi_wdata(s_axi_wdata), .s_axi_wstrb(s_axi_wstrb), .s_axi_wvalid(s_axi_wvalid), .s_axi_wready(s_axi_wready), + .s_axi_bresp(s_axi_bresp), .s_axi_bvalid(s_axi_bvalid), .s_axi_bready(s_axi_bready), + .s_axi_araddr(s_axi_araddr), .s_axi_arprot(s_axi_arprot), .s_axi_arvalid(s_axi_arvalid), .s_axi_arready(s_axi_arready), + .s_axi_rdata(s_axi_rdata), .s_axi_rresp(s_axi_rresp), .s_axi_rvalid(s_axi_rvalid), .s_axi_rready(s_axi_rready), + .cfg_source(cfg_source), .cfg_mode(cfg_mode), .cfg_falling(cfg_falling), + .cfg_level(cfg_level), .cfg_hysteresis(cfg_hysteresis), .cfg_pre(cfg_pre), .cfg_post(cfg_post), .cfg_auto_timeout(cfg_auto_timeout), + .arm_pulse(arm_pulse), .stop_pulse(stop_pulse), .soft_reset_pulse(soft_reset_pulse), .snapshot_pulse(snapshot_pulse), + .status_live(status_live), .trigger_index_live(trigger_index_live), .first_index_live(first_index_live), + .record_length_live(record_length_live), .sample_count_live(sample_count_live), + .fifo1_overflow_live(fifo1_overflow_live), .fifo2_overflow_live(fifo2_overflow_live), .trigger_count_live(trigger_count_live) + ); + triggered_acquisition #(.ADDR_BITS(CAPTURE_ADDR_BITS)) acquisition_i ( + .clk(clk), .rst(core_rst), .arm(arm_pulse), .stop(stop_pulse), .session_fault(session_fault), + .cfg_pre(cfg_pre), .cfg_post(cfg_post), .cfg_source(cfg_source), .cfg_mode(cfg_mode), + .cfg_falling(cfg_falling), .cfg_level(cfg_level), .cfg_hysteresis(cfg_hysteresis), .cfg_auto_timeout(cfg_auto_timeout), + .sample_data(sample_data), .sample_index(sample_index), .sample_valid(sample_valid && sample_ready), + .ext_level(ext_level), .active(active), .done(done), .fault(fault), + .trigger_index(trigger_index_live), .first_index(first_index_live), .record_length(record_length_live), + .trigger_event(trigger_event), .m_data(m_data), .m_index(m_index), .m_valid(m_valid), .m_ready(m_ready), .m_last(m_last) + ); + always @(posedge clk) begin + if (core_rst) begin sample_count_live<=0; trigger_count_live<=0; end + else begin + if (sample_valid && sample_ready && sample_count_live != 64'hffffffffffffffff) + sample_count_live<=sample_count_live+1'b1; + if (trigger_event && trigger_count_live != 32'hffffffff) + trigger_count_live<=trigger_count_live+1'b1; + end + end +endmodule diff --git a/hdl/scope-acquisition/rtl/trigger_engine.sv b/hdl/scope-acquisition/rtl/trigger_engine.sv new file mode 100644 index 0000000..aa438df --- /dev/null +++ b/hdl/scope-acquisition/rtl/trigger_engine.sv @@ -0,0 +1,121 @@ +`timescale 1ns/1ps +// Trigger decision is combinational for the currently accepted sample; the +// acquisition buffer can write that same sample as its trigger record. +module trigger_engine ( + input wire clk, + input wire rst, + input wire arm, + input wire stop, + input wire session_fault, + input wire [2:0] cfg_source, + input wire [1:0] cfg_mode, + input wire cfg_falling, + input wire [9:0] cfg_level, + input wire [9:0] cfg_hysteresis, + input wire [31:0] cfg_auto_timeout, + input wire [63:0] sample_data, + input wire [63:0] sample_index, + input wire sample_valid, + input wire ext_level, + input wire eligible, + output wire trigger_hit, + output wire auto_hit, + output reg armed, + output reg [63:0] trigger_index, + output reg config_error +); + reg [2:0] active_source; + reg [1:0] active_mode; + reg active_falling; + reg [9:0] active_level, active_hysteresis; + reg [31:0] active_timeout, accepted_count; + reg crossing_armed; + reg ext_seen, ext_previous; + reg [9:0] selected_sample; + wire [10:0] high_sum = {1'b0, active_level} + {1'b0, active_hysteresis}; + wire [9:0] high_threshold = high_sum > 11'd1023 ? 10'd1023 : high_sum[9:0]; + wire [9:0] low_threshold = active_level > active_hysteresis ? + active_level - active_hysteresis : 10'd0; + + always @* begin + case (active_source) + 3'd0: selected_sample = sample_data[9:0]; + 3'd1: selected_sample = sample_data[25:16]; + 3'd2: selected_sample = sample_data[41:32]; + 3'd3: selected_sample = sample_data[57:48]; + default: selected_sample = 10'd0; + endcase + end + + wire analog_arm_condition = active_falling ? + (active_hysteresis == 0 ? selected_sample > active_level : selected_sample >= high_threshold) : + (active_hysteresis == 0 ? selected_sample < active_level : selected_sample <= low_threshold); + wire analog_cross_condition = active_falling ? + selected_sample <= low_threshold : selected_sample >= high_threshold; + wire analog_event = crossing_armed && analog_cross_condition; + wire external_event = ext_seen && (active_falling ? + (ext_previous && !ext_level) : (!ext_previous && ext_level)); + wire edge_event = active_source == 3'd4 ? external_event : analog_event; + // Counter describes preceding accepted samples. N expires on sample N; + // zero expires immediately, but fallback still waits for eligibility. + wire auto_expired = active_timeout == 0 || accepted_count >= active_timeout - 1'b1; + wire auto_event = active_mode == 2'd0 && auto_expired; + assign trigger_hit = !rst && !arm && !stop && !session_fault && armed && + sample_valid && eligible && (edge_event || auto_event); + assign auto_hit = trigger_hit && !edge_event && auto_event; + + always @(posedge clk) begin + if (rst) begin + armed <= 0; + trigger_index <= 0; + config_error <= 0; + active_source <= 0; + active_mode <= 0; + active_falling <= 0; + active_level <= 0; + active_hysteresis <= 0; + active_timeout <= 0; + accepted_count <= 0; + crossing_armed <= 0; + ext_seen <= 0; + ext_previous <= 0; + end else if (stop || session_fault) begin + armed <= 0; + accepted_count <= 0; + crossing_armed <= 0; + ext_seen <= 0; + end else if (arm) begin + active_source <= cfg_source; + active_mode <= cfg_mode; + active_falling <= cfg_falling; + active_level <= cfg_level; + active_hysteresis <= cfg_hysteresis; + active_timeout <= cfg_auto_timeout; + armed <= (cfg_source <= 3'd4) && (cfg_mode <= 2'd2); + if (cfg_source > 3'd4 || cfg_mode > 2'd2) + config_error <= 1'b1; + trigger_index <= 0; + accepted_count <= 0; + crossing_armed <= 0; + ext_seen <= 0; + ext_previous <= 0; + end else if (armed && sample_valid) begin + if (accepted_count != 32'hffffffff) + accepted_count <= accepted_count + 1'b1; + if (active_source == 3'd4) begin + ext_previous <= ext_level; + ext_seen <= 1'b1; + end else if (analog_event) begin + // Consume the crossing even before sufficient prehistory; + // eligibility becoming true later must not replay this edge. + crossing_armed <= 1'b0; + end else if (analog_arm_condition) begin + crossing_armed <= 1'b1; + end + if (trigger_hit) begin + armed <= 1'b0; + trigger_index <= sample_index; + end + end + end +endmodule diff --git a/hdl/scope-acquisition/rtl/triggered_acquisition.sv b/hdl/scope-acquisition/rtl/triggered_acquisition.sv new file mode 100644 index 0000000..b399b38 --- /dev/null +++ b/hdl/scope-acquisition/rtl/triggered_acquisition.sv @@ -0,0 +1,49 @@ +`timescale 1ns/1ps +// Stream-domain composition; configuration applies atomically on ARM. +module triggered_acquisition #( + parameter integer ADDR_BITS = 8 +) ( + input wire clk, rst, arm, stop, session_fault, + input wire [31:0] cfg_pre, cfg_post, + input wire [2:0] cfg_source, + input wire [1:0] cfg_mode, + input wire cfg_falling, + input wire [9:0] cfg_level, cfg_hysteresis, + input wire [31:0] cfg_auto_timeout, + input wire [63:0] sample_data, sample_index, + input wire sample_valid, ext_level, + output wire active, done, fault, + output wire trigger_event, + output wire [63:0] trigger_index, first_index, + output wire [31:0] record_length, + output wire [63:0] m_data, m_index, + output wire m_valid, + input wire m_ready, + output wire m_last +); + wire eligible, hit, auto_hit, armed, config_error, buffer_fault; + wire [63:0] engine_trigger_index; + trigger_engine trigger_i ( + .clk(clk), .rst(rst), .arm(arm), .stop(stop), + .session_fault(session_fault || buffer_fault), + .cfg_source(cfg_source), .cfg_mode(cfg_mode), .cfg_falling(cfg_falling), + .cfg_level(cfg_level), .cfg_hysteresis(cfg_hysteresis), + .cfg_auto_timeout(cfg_auto_timeout), + .sample_data(sample_data), .sample_index(sample_index), + .sample_valid(sample_valid), .ext_level(ext_level), .eligible(eligible), + .trigger_hit(hit), .auto_hit(auto_hit), .armed(armed), + .trigger_index(engine_trigger_index), .config_error(config_error) + ); + acquisition_buffer #(.ADDR_BITS(ADDR_BITS)) buffer_i ( + .clk(clk), .rst(rst), .arm(arm), .stop(stop), + .session_fault(session_fault || config_error), + .cfg_pre(cfg_pre), .cfg_post(cfg_post), + .sample_data(sample_data), .sample_index(sample_index), + .sample_valid(sample_valid), .trigger_hit(hit), .eligible(eligible), + .active(active), .done(done), .fault(buffer_fault), + .trigger_index(trigger_index), .first_index(first_index), .record_length(record_length), + .m_data(m_data), .m_index(m_index), .m_valid(m_valid), .m_ready(m_ready), .m_last(m_last) + ); + assign fault = buffer_fault || config_error; + assign trigger_event = hit; +endmodule diff --git a/hdl/scope-acquisition/tests/tb_acquisition_buffer.sv b/hdl/scope-acquisition/tests/tb_acquisition_buffer.sv new file mode 100644 index 0000000..3b6668a --- /dev/null +++ b/hdl/scope-acquisition/tests/tb_acquisition_buffer.sv @@ -0,0 +1,130 @@ +`timescale 1ns/1ps +module tb_acquisition_buffer; + reg clk=0,rst=1,arm=0,stop=0,session_fault=0; + reg [31:0] cfg_pre=0,cfg_post=0; + reg [63:0] sample_data=0,sample_index=0; + reg sample_valid=0,trigger_hit=0,m_ready=0; + wire eligible,active,done,fault,m_valid,m_last; + wire [63:0] trigger_index,first_index,m_data,m_index; + wire [31:0] record_length; + integer checked=0,cases=0; + reg [63:0] stall_data,stall_index; + reg stall_last; + always #5 clk=~clk; + acquisition_buffer #(.ADDR_BITS(4)) dut(.*); + function automatic [63:0] pattern(input integer n); + pattern=64'h1234000000000000 | (n*64'h01000100010001); + endfunction + task reset_all; + begin + @(negedge clk); rst=1; arm=0; stop=0; session_fault=0; + sample_valid=0; trigger_hit=0; m_ready=0; + repeat(2) @(negedge clk); + rst=0; + end + endtask + task configure(input integer p,input integer q); + begin + @(negedge clk); cfg_pre=p;cfg_post=q; arm=1; + @(negedge clk); arm=0; + end + endtask + task send(input integer n,input reg trig); + begin + @(negedge clk); + sample_valid=1; sample_index=n;sample_data=pattern(n);trigger_hit=trig; + @(negedge clk); sample_valid=0;trigger_hit=0; + end + endtask + task drain(input integer first,input integer length); + integer got,c; reg was_stalled; + begin + got=0;c=0;was_stalled=0; + while(got 0; i = i - 1) begin + pipeline_a[i] = pipeline_a[i-1]; + pipeline_b[i] = pipeline_b[i-1]; + end + pipeline_a[0] = pattern(CHANNEL_BASE, conversion); + pipeline_b[0] = pattern(CHANNEL_BASE + 1, conversion); + conversion = conversion + 1; + for (i = 0; i < 10; i = i + 1) begin + data_a[i] <= #(4.20 + i * 0.06) launched_a[i]; + data_b[i] <= #(4.30 + (9-i) * 0.06) launched_b[i]; + end + end +endmodule + +// External-interface reference: pad sampling followed by one fabric stage; +// two reset-release stages, two READY stages and one READY warmup stage. +// Reference state is independent of DUT hierarchy and is updated before NBA. +module adc_capture_scoreboard #( + parameter integer CHIP = 1 +) ( + input wire clk, arst_n, ready_async, + input wire [9:0] data_a, data_b, + input wire [31:0] word_value, + input wire valid, + input wire [63:0] sample_index, + output integer checked_samples, + output reg [9:0] walking_one_a, walking_one_b, + output reg [9:0] walking_zero_a, walking_zero_b, + output reg [3:0] boundaries_a, boundaries_b +); + reg [1:0] reset_release = 0, ready_history = 0; + reg ready_warm = 0; + reg [19:0] pad_history = 20'bx; + reg [31:0] expected_word; + reg expected_valid; + reg [63:0] expected_next_index = 0, expected_index = 0; + reg local_reset; + integer bit_number; + + initial begin + checked_samples = 0; + walking_one_a = 0; walking_one_b = 0; + walking_zero_a = 0; walking_zero_b = 0; + boundaries_a = 0; boundaries_b = 0; + end + + always @(negedge arst_n) begin + reset_release = 0; + ready_history = 0; + end + + always @(posedge clk) begin + local_reset = !reset_release[1]; + expected_word = local_reset ? 32'd0 : + {6'b0, pad_history[19:10], 6'b0, pad_history[9:0]}; + expected_valid = !local_reset && ready_history[1] && ready_warm; + if (local_reset) begin + expected_next_index = 0; + expected_index = 0; + ready_warm = 0; + end else begin + ready_warm = ready_history[1]; + if (expected_valid) begin + expected_index = expected_next_index; + expected_next_index = expected_next_index + 1'b1; + end + end + ready_history = (!arst_n || local_reset) ? 2'b00 : + {ready_history[0], ready_async}; + reset_release = !arst_n ? 2'b00 : {reset_release[0], 1'b1}; + pad_history = {data_b, data_a}; + #0.1; + if (valid !== expected_valid) + $fatal(1, "chip%0d valid/reset/READY latency at t=%0t: got %b expected %b", + CHIP, $time, valid, expected_valid); + if (sample_index !== expected_index) + $fatal(1, "chip%0d index/reset/retention at t=%0t: got %0d expected %0d", + CHIP, $time, sample_index, expected_index); + // The data stage is updated even while invalid. Checking every edge + // detects an unintended CE, an extra stage or a stale restart sample. + if (word_value !== expected_word) + $fatal(1, "chip%0d data/pipeline alignment at t=%0t: got %h expected %h", + CHIP, $time, word_value, expected_word); + if (valid) begin + if ($isunknown(word_value)) + $fatal(1, "chip%0d unknown data was marked valid", CHIP); + if (word_value[15:10] !== 6'b0 || word_value[31:26] !== 6'b0) + $fatal(1, "chip%0d nonzero sample padding", CHIP); + checked_samples = checked_samples + 1; + for (bit_number = 0; bit_number < 10; bit_number = bit_number + 1) begin + if (word_value[9:0] == (10'b1 << bit_number)) + walking_one_a[bit_number] = 1; + if (word_value[25:16] == (10'b1 << bit_number)) + walking_one_b[bit_number] = 1; + if (word_value[9:0] == ~(10'b1 << bit_number)) + walking_zero_a[bit_number] = 1; + if (word_value[25:16] == ~(10'b1 << bit_number)) + walking_zero_b[bit_number] = 1; + end + case (word_value[9:0]) + 10'd0: boundaries_a[0] = 1; + 10'd1023: boundaries_a[1] = 1; + 10'd511: boundaries_a[2] = 1; + 10'd512: boundaries_a[3] = 1; + endcase + case (word_value[25:16]) + 10'd0: boundaries_b[0] = 1; + 10'd1023: boundaries_b[1] = 1; + 10'd511: boundaries_b[2] = 1; + 10'd512: boundaries_b[3] = 1; + endcase + end + end +endmodule + +module tb_adc_capture; + reg arst_n = 1, mezz_ready = 0; + reg adc1_clk = 0, adc2_clk = 0; + reg pins_connected = 1; + wire [9:0] adc1_a, adc1_b, adc2_a, adc2_b; + wire [31:0] chip1_word, chip2_word; + wire chip1_valid, chip2_valid; + wire [63:0] chip1_index, chip2_index; + wire [31:0] count1, count2; + wire [9:0] ones1a, ones1b, zeros1a, zeros1b; + wire [9:0] ones2a, ones2b, zeros2a, zeros2b; + wire [3:0] bounds1a, bounds1b, bounds2a, bounds2b; + integer stopped_count1, stopped_count2; + reg [63:0] stopped_index1, stopped_index2; + + always #5 adc1_clk = ~adc1_clk; + initial begin + #1.37; + forever #5.3 adc2_clk = ~adc2_clk; + end + + adc_capture_dual dut(.*); + adc_pin_model #(.CHANNEL_BASE(0)) source1( + .clk(adc1_clk), .pins_connected(pins_connected), .data_a(adc1_a), .data_b(adc1_b)); + adc_pin_model #(.CHANNEL_BASE(2)) source2( + .clk(adc2_clk), .pins_connected(pins_connected), .data_a(adc2_a), .data_b(adc2_b)); + adc_capture_scoreboard #(.CHIP(1)) check1( + .clk(adc1_clk), .arst_n(arst_n), .ready_async(mezz_ready), + .data_a(adc1_a), .data_b(adc1_b), .word_value(chip1_word), + .valid(chip1_valid), .sample_index(chip1_index), .checked_samples(count1), + .walking_one_a(ones1a), .walking_one_b(ones1b), + .walking_zero_a(zeros1a), .walking_zero_b(zeros1b), + .boundaries_a(bounds1a), .boundaries_b(bounds1b)); + adc_capture_scoreboard #(.CHIP(2)) check2( + .clk(adc2_clk), .arst_n(arst_n), .ready_async(mezz_ready), + .data_a(adc2_a), .data_b(adc2_b), .word_value(chip2_word), + .valid(chip2_valid), .sample_index(chip2_index), .checked_samples(count2), + .walking_one_a(ones2a), .walking_one_b(ones2b), + .walking_zero_a(zeros2a), .walking_zero_b(zeros2b), + .boundaries_a(bounds2a), .boundaries_b(bounds2b)); + + task automatic require_idle; + begin + if (chip1_valid !== 1'b0 || chip2_valid !== 1'b0) + $fatal(1, "valid outside a settled READY interval"); + end + endtask + + initial begin + // Reset release is deliberately unrelated to either forwarded clock. + #0.2 arst_n = 0; + #31.1 arst_n = 1; + #100 require_idle(); + mezz_ready = 1; + #14000; + // READY loss permits only the defined synchronizer tail. Disconnect + // the bus after that tail: high-Z/unpowered data is modeled as X. + mezz_ready = 0; + #100 require_idle(); + stopped_count1 = count1; stopped_count2 = count2; + stopped_index1 = chip1_index; stopped_index2 = chip2_index; + pins_connected = 0; + #200 require_idle(); + if (count1 != stopped_count1 || count2 != stopped_count2 || + chip1_index !== stopped_index1 || chip2_index !== stopped_index2) + $fatal(1, "READY-low interval advanced stream/index"); + pins_connected = 1; + #140 mezz_ready = 1; + #500; + if (count1 <= stopped_count1 || count2 <= stopped_count2) + $fatal(1, "capture did not resume after READY restoration"); + + // Reset while disconnected must not release stale IOB contents. + mezz_ready = 0; + #83.7 require_idle(); + pins_connected = 0; + #17.2 arst_n = 0; + #31.9 arst_n = 1; + #150 require_idle(); + if (chip1_index !== 64'd0 || chip2_index !== 64'd0) + $fatal(1, "reset did not clear sample indices"); + pins_connected = 1; + #140 mezz_ready = 1; + #2000; + + // Also reset during active capture with READY already asserted. + #13.1 arst_n = 0; + #27.3 arst_n = 1; + #400; + if (chip1_valid !== 1'b1 || chip2_valid !== 1'b1) + $fatal(1, "active-READY reset recovery failed"); + if (count1 < 1600 || count2 < 1500) + $fatal(1, "insufficient independent samples: %0d/%0d", count1, count2); + if ({ones1a, ones1b, ones2a, ones2b, zeros1a, zeros1b, zeros2a, zeros2b} + !== {80{1'b1}}) + $fatal(1, "incomplete walking-one/zero coverage"); + if ({bounds1a, bounds1b, bounds2a, bounds2b} !== 16'hffff) + $fatal(1, "incomplete boundary-code coverage"); + $display("PASS tb_adc_capture samples=%0d/%0d delayed/skewed pins, ramps, walking bits, boundaries, reset and READY", count1, count2); + $finish; + end + initial begin + #30000; + $fatal(1, "tb_adc_capture timeout"); + end +endmodule diff --git a/hdl/scope-acquisition/tests/tb_adc_frontend4.sv b/hdl/scope-acquisition/tests/tb_adc_frontend4.sv new file mode 100644 index 0000000..531f7e0 --- /dev/null +++ b/hdl/scope-acquisition/tests/tb_adc_frontend4.sv @@ -0,0 +1,61 @@ +`timescale 1ns/1ps +module tb_adc_frontend4; + reg arst_n=1, mezz_ready=0, adc1_clk=0, adc2_clk=0, stream_clk=0; + reg coherent_epoch_valid=0, sample_ready=1; + reg [9:0] adc1_a=0, adc1_b=200, adc2_a=400, adc2_b=600; + wire [63:0] sample_data,sample_index; + wire sample_valid,session_fault; + wire [31:0] adc1_overflow_count,adc2_overflow_count; + reg [63:0] next_index=0; + reg [9:0] first_code; + integer received=0, cycles=0; + reg checking=1; + always #5 adc1_clk=~adc1_clk; + initial begin #0.8; forever #5 adc2_clk=~adc2_clk; end + always #3 stream_clk=~stream_clk; + // Model a known common epoch only in this test. Both domains see the + // same conversion before each capture edge; hardware still needs proof. + always @(negedge adc1_clk) begin + adc1_a<=adc1_a+1'b1; adc1_b<=adc1_b+1'b1; + adc2_a<=adc2_a+1'b1; adc2_b<=adc2_b+1'b1; + end + adc_frontend4 #(.FIFO_ADDR_BITS(4)) dut (.*); + always @(negedge stream_clk) begin + cycles=cycles+1; + if (checking) sample_ready=(cycles%7 != 0); + end + always @(posedge stream_clk) begin + if (checking && sample_valid && sample_ready) begin + if (sample_index!==next_index) $fatal(1,"frontend index gap"); + if (received==0) first_code=sample_data[9:0]; + if (sample_data[9:0] !== ((first_code+next_index)&1023) || + sample_data[25:16] !== ((first_code+next_index+200)&1023) || + sample_data[41:32] !== ((first_code+next_index+400)&1023) || + sample_data[57:48] !== ((first_code+next_index+600)&1023)) + $fatal(1,"four-channel conversion association"); + if ((sample_data & 64'hfc00fc00fc00fc00) !== 0) $fatal(1,"zero alignment"); + received=received+1; next_index=next_index+1; + end + end + initial begin + #1 arst_n=0; + #30 arst_n=1; + #40 mezz_ready=1; + #70; + if (sample_valid) $fatal(1,"pairing without epoch permission"); + coherent_epoch_valid=1; + #7000; + if (session_fault || adc1_overflow_count || adc2_overflow_count || received<690) + $fatal(1,"continuous four-channel frontend"); + checking=0; sample_ready=0; + #500; + if (!session_fault) $fatal(1,"backpressure overflow not invalidated"); + arst_n=0; mezz_ready=0; coherent_epoch_valid=0; + #40 arst_n=1; + #40; + if (session_fault || sample_valid) $fatal(1,"joint reset not clearing frontend"); + $display("PASS tb_adc_frontend4 samples=%0d four_channels CDC_stalls_epoch_guard_overflow",received); + $finish; + end + initial begin #15000; $fatal(1,"timeout"); end +endmodule diff --git a/hdl/scope-acquisition/tests/tb_async_sample_fifo.sv b/hdl/scope-acquisition/tests/tb_async_sample_fifo.sv new file mode 100644 index 0000000..dd0e56d --- /dev/null +++ b/hdl/scope-acquisition/tests/tb_async_sample_fifo.sv @@ -0,0 +1,253 @@ +`timescale 1ns/1ps +module tb_async_sample_fifo; + localparam integer WIDTH = 96; + localparam integer ADDR_BITS = 4; + localparam integer DEPTH = 1 << ADDR_BITS; + reg arst_n = 1; + reg wr_clk = 0, rd_clk = 0; + reg wr_clock_running = 1, rd_clock_running = 1; + real wr_half_period = 5.0, rd_half_period = 7.1; + reg wr_valid = 0, rd_ready = 0; + reg [WIDTH-1:0] wr_data = 0; + wire wr_ready, wr_overflow, wr_overflow_sticky, rd_valid; + wire [31:0] wr_overflow_count; + wire [WIDTH-1:0] rd_data; + integer writer_mode = 0, reader_mode = 0; + reg [31:0] wr_random = 32'h87293a11, rd_random = 32'h171395ba; + reg [63:0] offered_index = 0; + + // External accepted-word scoreboard; rejected offers never enter it. + reg [WIDTH-1:0] expected_words [0:32767]; + integer expected_head = 0, expected_tail = 0; + integer accepted_total = 0, read_total = 0, overflow_total = 0; + integer stalls_checked = 0, full_cycles = 0; + reg [31:0] expected_overflows = 0; + reg expected_overflow_pulse; + reg previous_stall = 0; + reg check_burst = 0, burst_started = 0; + integer burst_reads = 0; + reg [WIDTH-1:0] stalled_payload; + integer accepted_before, dropped_before, queued_before_reset; + + async_sample_fifo #(.WIDTH(WIDTH), .ADDR_BITS(ADDR_BITS)) dut(.*); + + always begin + #(wr_half_period); + if (wr_clock_running) wr_clk = ~wr_clk; + else wr_clk = 0; + end + initial begin + // Fractional phase avoids coincident monitor edges at the initial rates. + #1.371; + forever begin + #(rd_half_period); + if (rd_clock_running) rd_clk = ~rd_clk; + else rd_clk = 0; + end + end + + always @(negedge wr_clk) begin + wr_random = wr_random * 32'd1664525 + 32'd1013904223; + wr_valid = arst_n && ((writer_mode == 2) || + ((writer_mode == 1) && wr_random[31:24] < 8'd190)); + if (wr_valid) begin + wr_data = {offered_index, (32'ha59c3721 ^ offered_index[31:0])}; + offered_index = offered_index + 1'b1; + end + end + always @(negedge rd_clk) begin + rd_random = rd_random * 32'd1664525 + 32'd1013904223; + rd_ready = arst_n && ((reader_mode == 2) || + ((reader_mode == 1) && rd_random[31:24] < 8'd137)); + end + + always @(negedge arst_n) begin + expected_head = 0; + expected_tail = 0; + expected_overflows = 0; + previous_stall = 0; + end + + always @(posedge wr_clk) begin + expected_overflow_pulse = 0; + if (arst_n) begin + if (wr_valid && wr_ready) begin + if (expected_tail >= 32768) + $fatal(1, "scoreboard capacity exceeded"); + expected_words[expected_tail] = wr_data; + expected_tail = expected_tail + 1; + accepted_total = accepted_total + 1; + end else if (wr_valid && !wr_ready) begin + // Offers are only enabled after reset-release warmup below. + expected_overflow_pulse = 1; + if (expected_overflows != 32'hffffffff) + expected_overflows = expected_overflows + 1'b1; + overflow_total = overflow_total + 1; + end + if (!wr_ready) full_cycles = full_cycles + 1; + end + #0.1; + if (wr_overflow !== expected_overflow_pulse) + $fatal(1, "wrong overflow pulse at t=%0t", $time); + if (wr_overflow_count !== expected_overflows || + wr_overflow_sticky !== (expected_overflows != 0)) + $fatal(1, "wrong overflow counter/sticky: got %0d expected %0d at t=%0t", + wr_overflow_count, expected_overflows, $time); + end + + always @(posedge rd_clk) begin + if (arst_n) begin + if (check_burst && burst_started && expected_head < expected_tail && + !(rd_valid && rd_ready)) + $fatal(1, "bubble in sustained ready/read burst at t=%0t", $time); + if (check_burst && rd_valid && rd_ready) begin + burst_started = 1; + burst_reads = burst_reads + 1; + end + if (previous_stall && (rd_valid !== 1'b1 || rd_data !== stalled_payload)) + $fatal(1, "read payload/valid changed through backpressure at t=%0t", $time); + if (rd_valid && rd_ready) begin + if (expected_head >= expected_tail) + $fatal(1, "read from empty scoreboard at t=%0t", $time); + if (rd_data !== expected_words[expected_head]) + $fatal(1, "read order/data mismatch at %0d: got %h expected %h", + expected_head, rd_data, expected_words[expected_head]); + expected_head = expected_head + 1; + read_total = read_total + 1; + end + previous_stall = rd_valid && !rd_ready; + if (previous_stall) begin + stalled_payload = rd_data; + stalls_checked = stalls_checked + 1; + end + end + #0.1; + if (!arst_n && rd_valid !== 1'b0) + $fatal(1, "read remained valid during shared reset"); + if (rd_valid) begin + if (expected_head >= expected_tail || rd_data !== expected_words[expected_head]) + $fatal(1, "prefetched output is stale/out of order at t=%0t", $time); + end + end + + task automatic wait_empty; + integer attempt; + begin : drain_wait + for (attempt = 0; attempt < 1000; attempt = attempt + 1) begin + @(posedge rd_clk); + #0.3; + if (expected_head == expected_tail && rd_valid === 1'b0) + disable drain_wait; + end + $fatal(1, "FIFO failed to drain: pending=%0d", expected_tail - expected_head); + end + endtask + + task automatic stop_writer; + begin + writer_mode = 0; + repeat (3) @(posedge wr_clk); + #0.3; + end + endtask + + initial begin + #0.2 arst_n = 0; + #29.3 arst_n = 1; + #100; + if (rd_valid !== 1'b0 || wr_ready !== 1'b1) + $fatal(1, "incorrect initial empty/ready state"); + + // Stalled reader lets RAM fill; the prefetched output adds one slot. + writer_mode = 2; + repeat (80) @(posedge wr_clk); + #0.3; + stop_writer(); + if (expected_tail - expected_head != DEPTH + 1 || wr_ready !== 1'b0) + $fatal(1, "incorrect full capacity: got %0d expected %0d", + expected_tail - expected_head, DEPTH + 1); + if (expected_overflows == 0 || rd_valid !== 1'b1) + $fatal(1, "full/overflow/output-prefetch was not exercised"); + check_burst = 1; + reader_mode = 2; + wait_empty(); + check_burst = 0; + if (burst_reads != DEPTH + 1) + $fatal(1, "sustained burst did not consume full capacity" ); + repeat (6) @(posedge wr_clk); + #0.3; + if (wr_ready !== 1'b1) + $fatal(1, "full flag failed to clear after drain"); + + // Fast writer, slower reader, randomized gaps and backpressure. + writer_mode = 1; reader_mode = 1; + repeat (5000) @(posedge wr_clk); + #0.233; + stop_writer(); + reader_mode = 2; + wait_empty(); + + // Reverse relative clock rates to exercise sustained empty transitions. + wr_half_period = 8.7; rd_half_period = 3.5; + writer_mode = 1; reader_mode = 1; + repeat (3000) @(posedge wr_clk); + #0.233; + stop_writer(); + reader_mode = 2; + wait_empty(); + if (read_total < DEPTH * 100 || stalls_checked < 100) + $fatal(1, "insufficient wrap/backpressure coverage"); + + // Populate again, pause both clocks, then jointly reset. Valid/ready + // must drop without an edge, and queued data must be discarded. + reader_mode = 0; + repeat (4) @(posedge rd_clk); + #0.233 writer_mode = 2; + repeat (80) @(posedge wr_clk); + #0.233; + stop_writer(); + queued_before_reset = expected_tail - expected_head; + if (queued_before_reset != DEPTH + 1) + $fatal(1, "queued-reset setup did not fill FIFO"); + wr_clock_running = 0; rd_clock_running = 0; + #40; + arst_n = 0; + #3; + if (rd_valid !== 1'b0 || wr_ready !== 1'b0 || wr_overflow !== 1'b0 || + wr_overflow_sticky !== 1'b0 || wr_overflow_count !== 32'd0) + $fatal(1, "paused-clock shared reset did not clear flags"); + #17 arst_n = 1; + #80; + if (rd_valid !== 1'b0 || wr_ready !== 1'b0) + $fatal(1, "reset released without local clock edges"); + + // Writer may recover first; reader remains reset while its clock is + // paused. Only the RAM slots are available until read prefetch resumes. + wr_clock_running = 1; + repeat (5) @(posedge wr_clk); + #0.233; + accepted_before = accepted_total; + dropped_before = overflow_total; + writer_mode = 2; + repeat (80) @(posedge wr_clk); + #0.233; + stop_writer(); + if (accepted_total - accepted_before != DEPTH || + overflow_total <= dropped_before || rd_valid !== 1'b0) + $fatal(1, "writer-first recovery/paused-reader capacity failed"); + reader_mode = 2; + rd_clock_running = 1; + wait_empty(); + repeat (6) @(posedge wr_clk); + #0.3; + if (wr_ready !== 1'b1 || rd_valid !== 1'b0) + $fatal(1, "incorrect final empty/ready state"); + $display("PASS tb_async_sample_fifo accepted=%0d read=%0d rejected=%0d stalls=%0d discarded_on_reset=%0d", + accepted_total, read_total, overflow_total, stalls_checked, queued_before_reset); + $finish; + end + initial begin + #300000; + $fatal(1, "tb_async_sample_fifo timeout"); + end +endmodule diff --git a/hdl/scope-acquisition/tests/tb_axi_scope_regs.sv b/hdl/scope-acquisition/tests/tb_axi_scope_regs.sv new file mode 100644 index 0000000..b89db81 --- /dev/null +++ b/hdl/scope-acquisition/tests/tb_axi_scope_regs.sv @@ -0,0 +1,314 @@ +`timescale 1ns/1ps +module tb_axi_scope_regs; + reg clk = 0, aresetn = 0; + reg [11:0] s_axi_awaddr = 0, s_axi_araddr = 0; + reg [2:0] s_axi_awprot = 0, s_axi_arprot = 0; + reg s_axi_awvalid = 0, s_axi_wvalid = 0, s_axi_bready = 0; + reg s_axi_arvalid = 0, s_axi_rready = 0; + reg [31:0] s_axi_wdata = 0; + reg [3:0] s_axi_wstrb = 0; + wire s_axi_awready, s_axi_wready, s_axi_bvalid, s_axi_arready, s_axi_rvalid; + wire [1:0] s_axi_bresp, s_axi_rresp; + wire [31:0] s_axi_rdata; + wire [2:0] cfg_source; + wire [1:0] cfg_mode; + wire cfg_falling; + wire [9:0] cfg_level, cfg_hysteresis; + wire [31:0] cfg_pre, cfg_post, cfg_auto_timeout; + wire arm_pulse, stop_pulse, soft_reset_pulse, snapshot_pulse; + reg [31:0] status_live = 32'h12345678; + reg [63:0] trigger_index_live = 0, first_index_live = 0, sample_count_live = 0; + reg [31:0] record_length_live = 0, fifo1_overflow_live = 0; + reg [31:0] fifo2_overflow_live = 0, trigger_count_live = 0; + integer reads = 0, writes = 0, arm_count = 0, stop_count = 0; + integer soft_reset_count = 0, snapshot_count = 0; + integer held_b_cycles = 0, held_r_cycles = 0; + reg held_b = 0, held_r = 0; + reg [1:0] held_bresp, held_rresp; + reg [31:0] held_rdata; + reg mutate_live = 0; + integer pulse_before, i; + reg [63:0] second_trigger, second_first, second_samples; + reg [31:0] second_length, second_status, second_fifo1, second_fifo2, second_count; + + always #5 clk = ~clk; + axi_scope_regs dut(.*); + + always @(posedge clk) begin + if (!aresetn) begin + held_b = 0; held_r = 0; + end else begin + if (held_b && (!s_axi_bvalid || s_axi_bresp !== held_bresp)) + $fatal(1, "B response changed/dropped under backpressure"); + if (held_r && (!s_axi_rvalid || s_axi_rresp !== held_rresp || s_axi_rdata !== held_rdata)) + $fatal(1, "R response changed/dropped under backpressure"); + held_b = s_axi_bvalid && !s_axi_bready; + held_r = s_axi_rvalid && !s_axi_rready; + if (held_b) begin held_bresp = s_axi_bresp; held_b_cycles = held_b_cycles + 1; end + if (held_r) begin + held_rresp = s_axi_rresp; held_rdata = s_axi_rdata; + held_r_cycles = held_r_cycles + 1; + end + end + #0.1; + if (aresetn) begin + if (arm_pulse) arm_count = arm_count + 1; + if (stop_pulse) stop_count = stop_count + 1; + if (soft_reset_pulse) soft_reset_count = soft_reset_count + 1; + if (snapshot_pulse) snapshot_count = snapshot_count + 1; + case ({snapshot_pulse, soft_reset_pulse, stop_pulse, arm_pulse}) + 4'b0000, 4'b0001, 4'b0010, 4'b0100, 4'b1000: begin end + default: $fatal(1, "multiple/unknown command pulses"); + endcase + end + end + + task automatic send_aw(input [11:0] address); + begin + @(negedge clk); s_axi_awaddr = address; s_axi_awvalid = 1; + @(posedge clk); + while (!s_axi_awready) @(posedge clk); + @(negedge clk); s_axi_awvalid = 0; + end + endtask + task automatic send_w(input [31:0] data, input [3:0] strobes); + begin + @(negedge clk); s_axi_wdata = data; s_axi_wstrb = strobes; s_axi_wvalid = 1; + @(posedge clk); + while (!s_axi_wready) @(posedge clk); + @(negedge clk); s_axi_wvalid = 0; + end + endtask + task automatic send_ar(input [11:0] address); + begin + @(negedge clk); s_axi_araddr = address; s_axi_arvalid = 1; + @(posedge clk); + while (!s_axi_arready) @(posedge clk); + @(negedge clk); s_axi_arvalid = 0; + end + endtask + + task automatic write_reg(input [11:0] address, input [31:0] data, + input [3:0] strobes, input integer order_value, hold_cycles, input [1:0] expected_response); + integer n; + begin + s_axi_bready = 0; + if (order_value == 1) begin + send_aw(address); + repeat (3) @(posedge clk); + if (s_axi_bvalid) $fatal(1, "write response before W"); + send_w(data, strobes); + end else if (order_value == 2) begin + send_w(data, strobes); + repeat (3) @(posedge clk); + if (s_axi_bvalid) $fatal(1, "write response before AW"); + send_aw(address); + end else fork + send_aw(address); + send_w(data, strobes); + join + while (!s_axi_bvalid) @(posedge clk); + #0.2; + if (s_axi_bresp !== expected_response) + $fatal(1, "wrong write response addr=%h data=%h strobes=%h got=%b expected=%b", + address, data, strobes, s_axi_bresp, expected_response); + for (n = 0; n < hold_cycles; n = n + 1) begin + @(posedge clk); #0.2; + if (!s_axi_bvalid || s_axi_bresp !== expected_response || s_axi_awready || s_axi_wready) + $fatal(1, "write response/readiness violated one-outstanding policy"); + // Input buses may change while not valid; buffered write is immutable. + s_axi_awaddr = 12'hffc; s_axi_wdata = ~data; s_axi_wstrb = ~strobes; + end + @(negedge clk); s_axi_bready = 1; + @(posedge clk); #0.2; + if (s_axi_bvalid) $fatal(1, "B response did not retire"); + @(negedge clk); s_axi_bready = 0; + writes = writes + 1; + end + endtask + + task automatic bump_live; + begin + status_live = status_live + 32'h01010101; + trigger_index_live = trigger_index_live + 64'h0000000100000011; + first_index_live = first_index_live + 64'h0000000300000023; + sample_count_live = sample_count_live + 64'h0000000500000037; + record_length_live = record_length_live + 1; + fifo1_overflow_live = fifo1_overflow_live + 3; + fifo2_overflow_live = fifo2_overflow_live + 5; + trigger_count_live = trigger_count_live + 7; + end + endtask + + task automatic read_reg(input [11:0] address, input [31:0] expected_data, + input [1:0] expected_response, input integer hold_cycles); + integer n; + begin + s_axi_rready = 0; + send_ar(address); + while (!s_axi_rvalid) @(posedge clk); + #0.2; + if (s_axi_rresp !== expected_response || s_axi_rdata !== expected_data) + $fatal(1, "wrong read addr=%h got data/resp=%h/%b expected=%h/%b", + address, s_axi_rdata, s_axi_rresp, expected_data, expected_response); + for (n = 0; n < hold_cycles; n = n + 1) begin + @(posedge clk); #0.2; + if (!s_axi_rvalid || s_axi_rresp !== expected_response || + s_axi_rdata !== expected_data || s_axi_arready) + $fatal(1, "read response/readiness violated holding contract"); + s_axi_araddr = 12'hffc; + if (mutate_live) bump_live(); + end + @(negedge clk); s_axi_rready = 1; + @(posedge clk); #0.2; + if (s_axi_rvalid) $fatal(1, "R response did not retire"); + @(negedge clk); s_axi_rready = 0; + reads = reads + 1; + end + endtask + + task automatic reset_bus; + begin + @(negedge clk); + aresetn = 0; + s_axi_awvalid = 0; s_axi_wvalid = 0; s_axi_arvalid = 0; + s_axi_bready = 0; s_axi_rready = 0; + repeat (2) @(posedge clk); + #0.2; + if (s_axi_bvalid || s_axi_rvalid || s_axi_awready || s_axi_wready || s_axi_arready || + arm_pulse || stop_pulse || soft_reset_pulse || snapshot_pulse) + $fatal(1, "AXI reset did not clear pending state/commands"); + if (cfg_source !== 0 || cfg_level !== 512 || cfg_mode !== 1 || cfg_falling !== 0 || + cfg_pre !== 64 || cfg_post !== 64 || cfg_hysteresis !== 4 || cfg_auto_timeout !== 1000000) + $fatal(1, "configuration reset defaults incorrect"); + @(negedge clk); aresetn = 1; + repeat (2) @(posedge clk); + #0.2; + end + endtask + + initial begin + reset_bus(); + read_reg('h00, 'h53434f50, 0, 3); + read_reg('h04, 'h00010000, 0, 2); + read_reg('h08, 0, 0, 1); + read_reg('h0c, 'h12345678, 0, 3); + read_reg('h10, 100000000, 0, 1); + read_reg('h14, 'hf, 0, 1); + read_reg('h18, 7, 0, 1); + read_reg('h1c, 256, 0, 1); + read_reg('h20, 0, 0, 1); read_reg('h24, 512, 0, 1); + read_reg('h28, 1, 0, 1); read_reg('h2c, 0, 0, 1); + read_reg('h30, 64, 0, 1); read_reg('h34, 64, 0, 1); + read_reg('h38, 4, 0, 1); read_reg('h3c, 1000000, 0, 1); + for (i = 'h50; i <= 'h78; i = i + 4) read_reg(i, 0, 0, 0); + + // Independent AW/W order and exact selected-byte merging. + write_reg('h20, 4, 'hf, 1, 3, 0); read_reg('h20, 4, 0, 1); + write_reg('h24, 'haa, 'h1, 2, 3, 0); read_reg('h24, 'h2aa, 0, 1); + write_reg('h24, 'h100, 'h2, 0, 2, 0); read_reg('h24, 'h1aa, 0, 1); + write_reg('h24, 'hffff03ff, 'h3, 0, 1, 0); read_reg('h24, 1023, 0, 1); + write_reg('h24, 'h400, 'h2, 0, 1, 2); read_reg('h24, 1023, 0, 1); + write_reg('h20, 6, 'h1, 0, 1, 2); read_reg('h20, 4, 0, 1); + write_reg('h20, 'h100, 'h2, 0, 1, 2); read_reg('h20, 4, 0, 1); + write_reg('h28, 2, 'h1, 0, 1, 0); write_reg('h28, 3, 'h1, 0, 1, 2); + read_reg('h28, 2, 0, 1); + write_reg('h2c, 1, 'h1, 0, 1, 0); write_reg('h2c, 2, 'h1, 0, 1, 2); + read_reg('h2c, 1, 0, 1); + write_reg('h38, 1023, 'hf, 0, 1, 0); write_reg('h38, 1024, 'hf, 0, 1, 2); + read_reg('h38, 1023, 0, 1); + write_reg('h30, 'h11223344, 'hf, 0, 1, 0); + write_reg('h30, 'haabbccdd, 'h5, 0, 1, 0); read_reg('h30, 'h11bb33dd, 0, 1); + write_reg('h30, 'hffffffff, 0, 0, 1, 0); read_reg('h30, 'h11bb33dd, 0, 1); + write_reg('h34, 'hfffffffe, 'hf, 0, 1, 0); read_reg('h34, 'hfffffffe, 0, 1); + write_reg('h3c, 'hffffffff, 'hf, 0, 1, 0); read_reg('h3c, 'hffffffff, 0, 1); + // A held B response must not prevent the independent read channel. + fork + write_reg('h30, 17, 'hf, 0, 8, 0); + read_reg('h24, 1023, 0, 4); + join + read_reg('h30, 17, 0, 1); + + pulse_before = arm_count + stop_count + soft_reset_count + snapshot_count; + write_reg('h08, 3, 'hf, 0, 2, 2); + write_reg('h08, 'h10, 'hf, 0, 2, 2); + write_reg('h08, 'h00010001, 'hf, 0, 2, 2); + write_reg('h08, 'hffffffff, 'he, 0, 2, 0); // low byte disabled => no-op + write_reg('h08, 0, 'hf, 0, 2, 0); + if (arm_count + stop_count + soft_reset_count + snapshot_count != pulse_before) + $fatal(1, "invalid/no-op command produced a pulse"); + write_reg('h08, 'haabbcc01, 'h1, 1, 3, 0); + write_reg('h08, 2, 'hf, 2, 3, 0); + write_reg('h08, 4, 'hf, 0, 3, 0); + if (arm_count != 1 || stop_count != 1 || soft_reset_count != 1) + $fatal(1, "command pulse was missing or duplicated while B was stalled"); + read_reg('h30, 17, 0, 1); // SOFT_RESET pulse does not reset this bank. + + // Coherent snapshot is immutable while all live fields change. + trigger_index_live = 64'h89abcdef01234567; + first_index_live = 64'h0123456789abcdef; + sample_count_live = 64'hf0123456789abcde; + record_length_live = 'h1234; status_live = 'h5a5a1010; + fifo1_overflow_live = 'h1133; fifo2_overflow_live = 'h4455; trigger_count_live = 'h6677; + write_reg('h08, 8, 'hf, 0, 3, 0); + if (snapshot_count != 1) $fatal(1, "SNAPSHOT pulse count incorrect"); + mutate_live = 1; + read_reg('h0c, 'h5a5a1010, 0, 3); // live read itself holds the AR-time value. + read_reg('h50, 'h01234567, 0, 2); read_reg('h54, 'h89abcdef, 0, 2); + read_reg('h58, 'h89abcdef, 0, 2); read_reg('h5c, 'h01234567, 0, 2); + read_reg('h60, 'h1234, 0, 2); read_reg('h64, 'h5a5a1010, 0, 2); + read_reg('h68, 'h789abcde, 0, 2); read_reg('h6c, 'hf0123456, 0, 2); + read_reg('h70, 'h1133, 0, 2); read_reg('h74, 'h4455, 0, 2); read_reg('h78, 'h6677, 0, 2); + mutate_live = 0; + second_trigger = trigger_index_live; second_first = first_index_live; + second_samples = sample_count_live; second_length = record_length_live; + second_status = status_live; second_fifo1 = fifo1_overflow_live; + second_fifo2 = fifo2_overflow_live; second_count = trigger_count_live; + write_reg('h08, 8, 'hf, 0, 2, 0); + read_reg('h50, second_trigger[31:0], 0, 1); read_reg('h54, second_trigger[63:32], 0, 1); + read_reg('h58, second_first[31:0], 0, 1); read_reg('h5c, second_first[63:32], 0, 1); + read_reg('h60, second_length, 0, 1); read_reg('h64, second_status, 0, 1); + read_reg('h68, second_samples[31:0], 0, 1); read_reg('h6c, second_samples[63:32], 0, 1); + read_reg('h70, second_fifo1, 0, 1); read_reg('h74, second_fifo2, 0, 1); read_reg('h78, second_count, 0, 1); + + // Read-only, reserved, unknown and unaligned addresses reject writes. + for (i = 0; i < 'h20; i = i + 4) + if (i != 'h08) write_reg(i, 'hffffffff, 0, 0, 1, 2); + for (i = 'h50; i <= 'h78; i = i + 4) write_reg(i, 'hffffffff, 'hf, 0, 1, 2); + write_reg('h21, 1, 'hf, 0, 1, 2); read_reg('h21, 0, 2, 2); + for (i = 'h40; i <= 'h4c; i = i + 4) begin + write_reg(i, 1, 'hf, 0, 1, 2); read_reg(i, 0, 2, 2); + end + write_reg('hf00, 1, 'hf, 0, 1, 2); read_reg('hf00, 0, 2, 2); + read_reg('hfff, 0, 2, 2); + + // Reset with only AW queued: later W must not pair with stale AW. + send_aw('h20); + reset_bus(); + send_w(3, 'hf); + repeat (5) @(posedge clk); #0.2; + if (s_axi_bvalid || cfg_source !== 0) $fatal(1, "stale AW survived reset"); + reset_bus(); + // Reset with only W queued: later AW must not pair with stale W. + send_w(4, 'hf); + reset_bus(); + send_aw('h20); + repeat (5) @(posedge clk); #0.2; + if (s_axi_bvalid || cfg_source !== 0) $fatal(1, "stale W survived reset"); + reset_bus(); + fork send_aw('h20); send_w(4, 'hf); join + while (!s_axi_bvalid) @(posedge clk); + reset_bus(); // held B response must be discarded by reset. + send_ar('h00); + while (!s_axi_rvalid) @(posedge clk); + reset_bus(); // held R response must be discarded by reset. + read_reg('h20, 0, 0, 1); read_reg('h24, 512, 0, 1); + read_reg('h50, 0, 0, 1); read_reg('h78, 0, 0, 1); + if (held_b_cycles < 40 || held_r_cycles < 40 || snapshot_count != 2) + $fatal(1, "insufficient AXI holding/snapshot coverage"); + $display("PASS tb_axi_scope_regs writes=%0d reads=%0d Bstall=%0d Rstall=%0d commands=%0d/%0d/%0d/%0d", + writes, reads, held_b_cycles, held_r_cycles, arm_count, stop_count, soft_reset_count, snapshot_count); + $finish; + end + initial begin #200000; $fatal(1, "tb_axi_scope_regs timeout"); end +endmodule diff --git a/hdl/scope-acquisition/tests/tb_capture_stream.sv b/hdl/scope-acquisition/tests/tb_capture_stream.sv new file mode 100644 index 0000000..92ec1cf --- /dev/null +++ b/hdl/scope-acquisition/tests/tb_capture_stream.sv @@ -0,0 +1,58 @@ +`timescale 1ns/1ps +module tb_capture_stream; + reg arst_n=1, adc_clk=0, ready_async=0, stream_clk=0, stream_ready=1; + reg [9:0] adc_data_a=0, adc_data_b=1023; + wire [95:0] stream_data; + wire stream_valid, session_fault; + wire [31:0] overflow_count_adc; + integer received=0; + reg [9:0] first_code; + reg [63:0] next_index=0; + reg checking=1; + always #5 adc_clk=~adc_clk; + always #3 stream_clk=~stream_clk; + always @(negedge adc_clk) begin + adc_data_a <= adc_data_a+1'b1; + adc_data_b <= adc_data_b-1'b1; + end + capture_stream #(.FIFO_ADDR_BITS(3)) dut (.*); + always @(posedge stream_clk) begin + if (stream_valid && stream_ready && checking) begin + if (stream_data[95:32] !== next_index) $fatal(1,"lost/reordered capture index"); + if (received==0) first_code=stream_data[9:0]; + if (stream_data[9:0] !== ((first_code+next_index) & 1023)) + $fatal(1,"capture/FIFO data association"); + if (stream_data[25:16] !== (~stream_data[9:0] & 1023)) + $fatal(1,"channel association"); + if ((stream_data[31:0] & 32'hfc00fc00) !== 0) + $fatal(1,"alignment"); + next_index=next_index+1; + received=received+1; + end + end + initial begin + #1 arst_n=0; + #30 arst_n=1; + #40 ready_async=1; + #5000; + if (received<480 || session_fault || overflow_count_adc) $fatal(1,"continuous capture failed"); + // A common READY gap does not produce an index gap: sticky sideband must catch it. + checking=0; ready_async=0; + #100; + if (!session_fault) $fatal(1,"READY loss not reported"); + ready_async=1; + #100; + if (!session_fault) $fatal(1,"fault was not sticky"); + // Joint reset discards this session and its queued data. + arst_n=0; stream_ready=0; ready_async=0; + #40 arst_n=1; + #40; + if (session_fault || overflow_count_adc) $fatal(1,"fault not reset"); + ready_async=1; + #500; + if (!session_fault || overflow_count_adc==0) $fatal(1,"overflow not reported"); + $display("PASS tb_capture_stream continuous_samples=%0d READY_loss_and_overflow",received); + $finish; + end + initial begin #10000; $fatal(1,"timeout"); end +endmodule diff --git a/hdl/scope-acquisition/tests/tb_reset_sync.sv b/hdl/scope-acquisition/tests/tb_reset_sync.sv new file mode 100644 index 0000000..8076d26 --- /dev/null +++ b/hdl/scope-acquisition/tests/tb_reset_sync.sv @@ -0,0 +1,19 @@ +`timescale 1ns/1ps +module tb_reset_sync; + reg clk=0, arst_n=1; wire rst; + always #5 clk=~clk; + reset_sync dut(.*); + initial begin + #0.1 arst_n=0; + #1; if(rst!==1) $fatal(1,"assertion failed"); + #1 arst_n=1; + @(posedge clk); #1; if(rst!==1) $fatal(1,"released too soon"); + @(posedge clk); #1; if(rst!==0) $fatal(1,"release failed"); + #2 arst_n=0; #0.1; if(rst!==1) $fatal(1,"asynchronous assert failed"); + #1 arst_n=1; + repeat(2) @(posedge clk); + #1; if(rst!==0) $fatal(1,"second release failed"); + $display("PASS tb_reset_sync"); $finish; + end + initial begin #1000; $fatal(1,"timeout"); end +endmodule diff --git a/hdl/scope-acquisition/tests/tb_sample_pairer.sv b/hdl/scope-acquisition/tests/tb_sample_pairer.sv new file mode 100644 index 0000000..c405e0f --- /dev/null +++ b/hdl/scope-acquisition/tests/tb_sample_pairer.sv @@ -0,0 +1,279 @@ +`timescale 1ns/1ps + +module tb_sample_pairer; + reg clk = 0; + reg rst = 1; + reg coherent_epoch_valid = 0, session_fault = 0; + reg [95:0] a_data = 0, b_data = 0; + reg a_valid = 0, b_valid = 0; + wire a_ready, b_ready; + wire [63:0] m_data, m_index; + wire m_valid, fault; + reg m_ready = 0; + + reg auto_source = 0, full_rate = 0; + integer source_limit = 0; + integer source_a = 0, source_b = 0, source_cycle = 0; + reg a_consumed = 0, b_consumed = 0; + reg [63:0] queue_data [0:511]; + reg [63:0] queue_index [0:511]; + integer queue_head = 0, queue_tail = 0; + integer transferred = 0, total_transferred = 0; + reg stalled = 0; + reg [63:0] stalled_data, stalled_index; + + always #4 clk = ~clk; + sample_pairer dut(.*); + + function automatic [31:0] chip_word(input integer chip, input integer n); + reg [9:0] lane_a, lane_b; + begin + lane_a = (n * 13 + chip * 101) & 1023; + lane_b = (n * 29 + chip * 211) & 1023; + chip_word = {6'b0, lane_b, 6'b0, lane_a}; + end + endfunction + + // Independent FIFO-head models. Valid does not wait for ready, and an + // offered head remains stable until it is consumed. Availability/stalls + // are deterministic and independently exercise both input interfaces. + always @(negedge clk) begin + if (auto_source && !rst) begin + source_cycle = source_cycle + 1; + if (!a_valid || a_consumed) begin + a_valid = source_a < source_limit && + (full_rate || source_cycle % 5 != 0); + a_data = {64'(source_a), chip_word(0, source_a)}; + end + if (!b_valid || b_consumed) begin + b_valid = source_b < source_limit && + (full_rate || source_cycle % 7 != 1); + b_data = {64'(source_b), chip_word(1, source_b)}; + end + m_ready = full_rate || + (source_cycle % 7 != 2 && source_cycle % 11 != 0); + end + end + + // Compare accepted input/output transactions, not DUT internal state. + always @(posedge clk) begin + if (rst) begin + source_a = 0; source_b = 0; source_cycle = 0; + a_consumed = 0; b_consumed = 0; + queue_head = 0; queue_tail = 0; transferred = 0; + stalled = 0; + end else begin + if (stalled && (m_valid !== 1'b1 || + m_data !== stalled_data || m_index !== stalled_index)) + $fatal(1, "stalled output changed, including fault path"); + if (a_ready !== b_ready) + $fatal(1, "input pair was not consumed atomically"); + if (fault && (a_ready || b_ready)) + $fatal(1, "input consumed after sticky fault"); + if (auto_source && full_rate && source_a > 0 && + source_a < source_limit && m_valid !== 1'b1) + $fatal(1, "elastic pairer inserted a full-rate output bubble"); + if (m_valid && m_ready) begin + if (queue_head >= queue_tail) + $fatal(1, "unexpected output without an accepted pair"); + if (m_data !== queue_data[queue_head] || + m_index !== queue_index[queue_head]) + $fatal(1, "output data/index/order mismatch at transfer %0d", queue_head); + queue_head = queue_head + 1; + transferred = transferred + 1; + total_transferred = total_transferred + 1; + end + a_consumed = a_valid && a_ready; + b_consumed = b_valid && b_ready; + if (a_consumed || b_consumed) begin + if (!a_consumed || !b_consumed || !coherent_epoch_valid || + session_fault || fault) + $fatal(1, "pair accepted without session/epoch permission"); + if (a_data[95:32] !== b_data[95:32] || + a_data[95:32] !== 64'(queue_tail)) + $fatal(1, "mismatched/noncontiguous index accepted"); + if (queue_tail >= 512) + $fatal(1, "reference queue overflow"); + queue_data[queue_tail] = {b_data[31:0], a_data[31:0]}; + queue_index[queue_tail] = a_data[95:32]; + queue_tail = queue_tail + 1; + if (auto_source) begin + source_a = source_a + 1; + source_b = source_b + 1; + end + end + stalled = m_valid && !m_ready; + stalled_data = m_data; + stalled_index = m_index; + end + end + + task automatic reset_session; + begin + @(negedge clk); + auto_source = 0; + rst = 1; + a_valid = 0; b_valid = 0; m_ready = 0; + coherent_epoch_valid = 0; session_fault = 0; + repeat (2) begin @(posedge clk); #0.2; end + if (m_valid !== 1'b0 || fault !== 1'b0 || + a_ready !== 1'b0 || b_ready !== 1'b0) + $fatal(1, "reset did not clear/gate the interface"); + @(negedge clk); rst = 0; + end + endtask + + task automatic offer_heads(input integer ai, input integer bi); + begin + @(negedge clk); + a_valid = 1; b_valid = 1; + a_data = {64'(ai), chip_word(0, ai)}; + b_data = {64'(bi), chip_word(1, bi)}; + end + endtask + + task automatic check_fault; + begin + @(posedge clk); #0.2; + if (fault !== 1'b1 || a_ready !== 1'b0 || b_ready !== 1'b0) + $fatal(1, "expected sticky fault with no new input consumption"); + end + endtask + + task automatic exercise_stream(input integer count, input reg continuous); + begin + reset_session(); + @(negedge clk); + source_limit = count; full_rate = continuous; + coherent_epoch_valid = 1; + auto_source = 1; + while (source_a < count) begin @(posedge clk); #0.2; end + @(negedge clk); + auto_source = 0; a_valid = 0; b_valid = 0; m_ready = 1; + repeat (3) begin @(posedge clk); #0.2; end + if (fault !== 1'b0 || transferred != count || + queue_head != queue_tail || m_valid !== 1'b0) + $fatal(1, "stream incomplete or faulted: sent=%0d out=%0d", count, transferred); + end + endtask + + initial begin + // Equal local index zero must not bypass the external epoch guard. + reset_session(); + offer_heads(0, 0); + repeat (4) begin + @(posedge clk); #0.2; + if (a_ready || b_ready || m_valid || fault) + $fatal(1, "unverified physical epoch released a pair"); + end + @(negedge clk); coherent_epoch_valid = 1; + @(posedge clk); #0.2; + if (!m_valid || m_index !== 64'd0) + $fatal(1, "epoch permission did not release initial pair"); + @(negedge clk); a_valid = 0; b_valid = 0; + repeat (5) begin @(posedge clk); #0.2; end + @(negedge clk); m_ready = 1; + @(posedge clk); #0.2; + if (transferred != 1 || m_valid) + $fatal(1, "stalled initial pair did not drain"); + + exercise_stream(128, 0); + exercise_stream(64, 1); + + // Different FIFO-head indices are rejected without consuming either. + reset_session(); + coherent_epoch_valid = 1; + offer_heads(0, 1); + check_fault(); + if (queue_tail != 0 || m_valid) + $fatal(1, "index mismatch leaked a pair"); + offer_heads(0, 0); + check_fault(); + + // A simultaneous drop is not detected by equality alone. Expected + // index rejects both heads jumping from 0 to 2; prior good output is + // held and may drain even after the acquisition has been invalidated. + reset_session(); + coherent_epoch_valid = 1; + offer_heads(0, 0); + @(posedge clk); #0.2; + offer_heads(2, 2); + check_fault(); + repeat (4) begin @(posedge clk); #0.2; end + if (queue_tail != 1 || !m_valid || m_index !== 64'd0) + $fatal(1, "both-stream gap changed prior pending output"); + @(negedge clk); m_ready = 1; + @(posedge clk); #0.2; + if (transferred != 1 || m_valid) + $fatal(1, "prior good output failed to drain after mismatch fault"); + + // An equal duplicate index is also a sequence violation. + reset_session(); + coherent_epoch_valid = 1; + offer_heads(0, 0); + @(posedge clk); #0.2; + offer_heads(0, 0); + check_fault(); + if (queue_tail != 1) + $fatal(1, "equal duplicate ordinal was consumed"); + + // ADC valid ordinals remain contiguous across a common READY gap. + // The mandatory session guard, also used for overflow/clock loss, + // must latch the loss even though later offered indices equal one. + reset_session(); + coherent_epoch_valid = 1; + offer_heads(0, 0); + @(posedge clk); #0.2; + @(negedge clk); + a_valid = 0; b_valid = 0; session_fault = 1; + check_fault(); + @(negedge clk); session_fault = 0; + repeat (3) begin @(posedge clk); #0.2; end + offer_heads(1, 1); + check_fault(); + if (queue_tail != 1 || !m_valid || m_index !== 64'd0) + $fatal(1, "session fault consumed resumed contiguous ordinals"); + @(negedge clk); m_ready = 1; + @(posedge clk); #0.2; + if (transferred != 1 || m_valid) + $fatal(1, "prior good output failed to drain after session fault"); + + // Loss of established epoch permission invalidates the session, but + // cannot retract an already offered stalled valid transaction. + reset_session(); + coherent_epoch_valid = 1; + offer_heads(0, 0); + @(posedge clk); #0.2; + @(negedge clk); + a_valid = 0; b_valid = 0; coherent_epoch_valid = 0; + check_fault(); + repeat (3) begin @(posedge clk); #0.2; end + @(negedge clk); m_ready = 1; + @(posedge clk); #0.2; + if (transferred != 1 || m_valid) + $fatal(1, "pending output failed to drain after epoch loss"); + + // Reset is the explicit transaction-abort boundary. It clears a + // pending word/fault and permits a new verified epoch starting at zero. + reset_session(); + coherent_epoch_valid = 1; + offer_heads(0, 0); + @(posedge clk); #0.2; + reset_session(); + coherent_epoch_valid = 1; + offer_heads(0, 0); + @(posedge clk); #0.2; + @(negedge clk); a_valid = 0; b_valid = 0; m_ready = 1; + @(posedge clk); #0.2; + if (fault || transferred != 1 || m_valid) + $fatal(1, "reset/re-arm did not start a clean index-zero session"); + + $display("PASS tb_sample_pairer transfers=%0d epoch guard, independent availability, stalls, full rate, mismatch, simultaneous gap, session fault and reset", total_transferred); + $finish; + end + + initial begin + #30000; + $fatal(1, "tb_sample_pairer timeout"); + end +endmodule diff --git a/hdl/scope-acquisition/tests/tb_scope_core.sv b/hdl/scope-acquisition/tests/tb_scope_core.sv new file mode 100644 index 0000000..37e5a23 --- /dev/null +++ b/hdl/scope-acquisition/tests/tb_scope_core.sv @@ -0,0 +1,148 @@ +`timescale 1ns/1ps +module tb_scope_core; + reg clk=0,aresetn=0; + reg [11:0] s_axi_awaddr=0,s_axi_araddr=0; + reg [2:0] s_axi_awprot=0,s_axi_arprot=0; + reg s_axi_awvalid=0,s_axi_wvalid=0,s_axi_bready=0,s_axi_arvalid=0,s_axi_rready=0; + wire s_axi_awready,s_axi_wready,s_axi_bvalid,s_axi_arready,s_axi_rvalid; + reg [31:0] s_axi_wdata=0; + reg [3:0] s_axi_wstrb=15; + wire [31:0] s_axi_rdata; + wire [1:0] s_axi_bresp,s_axi_rresp; + reg [63:0] sample_data=0,sample_index=0; + reg sample_valid=0,coherent_epoch_valid=1,frontend_fault=0,ext_level=0,m_ready=0; + reg [31:0] fifo1_overflow_live=7,fifo2_overflow_live=9; + wire sample_ready,frontend_reset_request,m_valid,m_last,irq; + wire [63:0] m_data,m_index; + integer n,received=0,cycles=0,resets=0; + reg check_record=1; + reg [63:0] held_data,held_index; + reg held_last; + reg [31:0] value; + always #5 clk=~clk; + scope_core #(.CAPTURE_ADDR_BITS(5)) dut(.*); + always @(posedge clk) if(frontend_reset_request) resets=resets+1; + function automatic [63:0] pattern(input integer i); + reg [9:0] a,b,c,d; + begin + a=(i<16)?80+(i%9):120+(i%9); + b=i+200;c=i+400;d=i+600; + pattern={6'b0,d,6'b0,c,6'b0,b,6'b0,a}; + end + endfunction + task write_reg(input [11:0] addr,input [31:0] data); + reg aw_done,w_done; + begin + @(negedge clk); s_axi_awaddr=addr;s_axi_wdata=data; + s_axi_awvalid=1;s_axi_wvalid=1;s_axi_bready=0;aw_done=0;w_done=0; + while(!aw_done || !w_done) begin + @(posedge clk); + if(s_axi_awvalid&&s_axi_awready) aw_done=1; + if(s_axi_wvalid&&s_axi_wready) w_done=1; + @(negedge clk); + if(aw_done) s_axi_awvalid=0; + if(w_done) s_axi_wvalid=0; + end + wait(s_axi_bvalid); + if(s_axi_bresp!==0) $fatal(1,"AXI write failed %h",addr); + @(negedge clk);s_axi_bready=1; + @(negedge clk);s_axi_bready=0; + end + endtask + task read_reg(input [11:0] addr,output [31:0] result); + begin + @(negedge clk);s_axi_araddr=addr;s_axi_arvalid=1;s_axi_rready=0; + do @(posedge clk); while(!s_axi_arready); + @(negedge clk);s_axi_arvalid=0; + wait(s_axi_rvalid); + if(s_axi_rresp!==0) $fatal(1,"AXI read failed %h",addr); + result=s_axi_rdata; + @(negedge clk);s_axi_rready=1; + @(negedge clk);s_axi_rready=0; + end + endtask + always @(posedge clk) if(check_record&&m_valid&&m_ready) begin + if(m_index!==13+received||m_data!==pattern(13+received)||m_last!==(received==7)) + $fatal(1,"register-driven record output %0d",received); + received=received+1; + end + initial begin + repeat(4) @(negedge clk);aresetn=1; + repeat(6) @(negedge clk); + read_reg('h00,value);if(value!==32'h53434f50)$fatal(1,"ID"); + write_reg('h30,3);write_reg('h34,4);write_reg('h20,0); + write_reg('h24,100);write_reg('h38,2);write_reg('h28,1); + write_reg('h08,1); + for(n=0;n<32;n=n+1) begin + @(negedge clk);sample_valid=1;sample_data=pattern(n);sample_index=n; + if(!sample_ready)$fatal(1,"stream stalled during capture"); + end + @(negedge clk);sample_valid=0; + if(!irq)$fatal(1,"no completion IRQ"); + write_reg('h08,8); // capture coherent metadata/counter bank + read_reg('h50,value);if(value!==16)$fatal(1,"trigger index"); + read_reg('h58,value);if(value!==13)$fatal(1,"first index"); + read_reg('h60,value);if(value!==8)$fatal(1,"record length"); + read_reg('h68,value);if(value!==32)$fatal(1,"sample counter"); + read_reg('h70,value);if(value!==7)$fatal(1,"FIFO1 snapshot"); + read_reg('h74,value);if(value!==9)$fatal(1,"FIFO2 snapshot"); + read_reg('h78,value);if(value!==1)$fatal(1,"trigger counter"); + // Shadow edits do not alter a completed record. + write_reg('h24,900); + while(received<8) begin + @(negedge clk);m_ready=(cycles%4!=0);cycles=cycles+1; + end + @(negedge clk);m_ready=1; + repeat(3) @(negedge clk); + if(m_valid)$fatal(1,"extra output"); + write_reg('h08,4); + repeat(8) @(negedge clk); + if(irq||resets!=1)$fatal(1,"soft reset"); + read_reg('h24,value);if(value!==900)$fatal(1,"soft reset lost shadow cfg"); + coherent_epoch_valid=0; + write_reg('h08,1);repeat(4) @(negedge clk); + if(!irq)$fatal(1,"ARM without epoch accepted"); + read_reg('h0c,value);if(!(value&4))$fatal(1,"fault status"); + write_reg('h08,4);repeat(8) @(negedge clk); + if(irq||resets!=2)$fatal(1,"fault reset failed"); + check_record=0;coherent_epoch_valid=1;m_ready=0; + write_reg('h24,100);write_reg('h30,3);write_reg('h34,4); + write_reg('h08,1); + @(negedge clk);sample_valid=1;sample_data=pattern(0);sample_index=200; + @(negedge clk);sample_valid=0;frontend_fault=1; + repeat(3) @(negedge clk); + read_reg('h0c,value); + if(!irq||(value&32'h24)!=32'h24||(value&1))$fatal(1,"frontend active fault status/IRQ"); + write_reg('h08,4);repeat(8) @(negedge clk); + if(!irq)$fatal(1,"soft reset hid live frontend fault"); + frontend_fault=0; + write_reg('h08,4);repeat(8) @(negedge clk); + if(irq)$fatal(1,"coordinated frontend recovery failed"); + write_reg('h30,0);write_reg('h34,0);write_reg('h08,1); + @(negedge clk);sample_valid=1;sample_data=pattern(0);sample_index=300; + @(negedge clk);sample_data=pattern(16);sample_index=301; + @(negedge clk);sample_valid=0; + repeat(3) @(negedge clk); + if(!m_valid||!irq||m_index!==301)$fatal(1,"recovered acquisition failed"); + held_data=m_data;held_index=m_index;held_last=m_last;frontend_fault=1; + repeat(3) @(negedge clk); + if(!m_valid||m_data!==held_data||m_index!==held_index||m_last!==held_last) + $fatal(1,"frontend fault corrupted stalled record"); + read_reg('h0c,value); + if(!irq||(value&32'h24)!=32'h24)$fatal(1,"pending frontend fault status"); + frontend_fault=0; + write_reg('h08,4);repeat(8) @(negedge clk); + if(irq||m_valid)$fatal(1,"coordinated record reset failed"); + write_reg('h08,1); + @(negedge clk);sample_valid=1;sample_data=pattern(0);sample_index=400; + @(negedge clk);sample_data=pattern(16);sample_index=401; + @(negedge clk);sample_valid=0; + repeat(3) @(negedge clk); + if(!m_valid||m_index!==401||!m_last||!irq)$fatal(1,"final re-arm failed"); + m_ready=1;repeat(3) @(negedge clk); + if(m_valid)$fatal(1,"recovery output did not drain"); + $display("PASS tb_scope_core AXI_config_ARM_trigger16_window13..20_snapshot_IRQ_softreset_epoch_guard_frontend_fault_recovery"); + $finish; + end + initial begin #30000;$fatal(1,"timeout");end +endmodule diff --git a/hdl/scope-acquisition/tests/tb_trigger_engine.sv b/hdl/scope-acquisition/tests/tb_trigger_engine.sv new file mode 100644 index 0000000..8646108 --- /dev/null +++ b/hdl/scope-acquisition/tests/tb_trigger_engine.sv @@ -0,0 +1,286 @@ +`timescale 1ns/1ps +module tb_trigger_engine; + reg clk = 0, rst = 1, arm = 0, stop = 0, session_fault = 0; + reg [2:0] cfg_source = 0; + reg [1:0] cfg_mode = 1; + reg cfg_falling = 0; + reg [9:0] cfg_level = 512, cfg_hysteresis = 0; + reg [31:0] cfg_auto_timeout = 0; + reg [63:0] sample_data = 0, sample_index = 0; + reg sample_valid = 0, ext_level = 0, eligible = 1; + wire trigger_hit, auto_hit, armed, config_error; + wire [63:0] trigger_index; + reg [63:0] next_sample_index = 64'h0000000100000000; + integer checks = 0, hits = 0, fallback_hits = 0; + integer source; + always #5 clk = ~clk; + trigger_engine dut(.*); + + function automatic [63:0] channels(input [9:0] a, b, c, d); + channels = {6'b0, d, 6'b0, c, 6'b0, b, 6'b0, a}; + endfunction + function automatic [63:0] selected(input integer channel, input [9:0] value); + begin + case (channel) + 0: selected = channels(value, 900, 900, 900); + 1: selected = channels(900, value, 900, 900); + 2: selected = channels(900, 900, value, 900); + 3: selected = channels(900, 900, 900, value); + default: selected = channels(900, 900, 900, 900); + endcase + end + endfunction + + task automatic arm_config(input integer source_value, mode_value, + input bit falling_value, input [9:0] level_value, hysteresis_value, + input [31:0] timeout_value); + begin + @(negedge clk); + cfg_source = source_value; + cfg_mode = mode_value; + cfg_falling = falling_value; + cfg_level = level_value; + cfg_hysteresis = hysteresis_value; + cfg_auto_timeout = timeout_value; + arm = 1; + sample_valid = 1; // ARM itself must never consume or trigger. + eligible = 1; + #0.3; + if (trigger_hit !== 1'b0 || auto_hit !== 1'b0) + $fatal(1, "trigger asserted on ARM"); + @(posedge clk); + #0.3; + if (armed !== ((source_value <= 4) && (mode_value <= 2))) + $fatal(1, "ARM/config validity mismatch for source=%0d mode=%0d", source_value, mode_value); + if (trigger_index !== 64'd0) + $fatal(1, "ARM did not reset trigger index"); + if ((source_value > 4 || mode_value > 2) && config_error !== 1'b1) + $fatal(1, "invalid configuration did not set sticky error"); + @(negedge clk); + arm = 0; + sample_valid = 0; + end + endtask + + task automatic step(input [63:0] word_value, + input bit valid_value, ext_value, eligible_value, expected_hit, expected_auto); + reg [63:0] presented_index; + begin + @(negedge clk); + arm = 0; stop = 0; session_fault = 0; + sample_data = word_value; + sample_valid = valid_value; + ext_level = ext_value; + eligible = eligible_value; + sample_index = next_sample_index; + presented_index = sample_index; + if (valid_value) next_sample_index = next_sample_index + 1'b1; + #0.3; + if (trigger_hit !== expected_hit || auto_hit !== expected_auto) + $fatal(1, "trigger decision check%0d index=%h data=%h ext=%b eligible=%b valid=%b: got hit/auto=%b/%b expected=%b/%b", + checks, sample_index, sample_data, ext_level, eligible, sample_valid, + trigger_hit, auto_hit, expected_hit, expected_auto); + // Decisions must still describe the sample presented at this edge. + @(posedge clk); + if (trigger_hit !== expected_hit || auto_hit !== expected_auto) + $fatal(1, "same-edge trigger contract failed at check%0d", checks); + #0.3; + if (expected_hit) begin + if (armed !== 1'b0 || trigger_index !== presented_index) + $fatal(1, "hit did not latch exact 64-bit sample index/disarm"); + hits = hits + 1; + if (expected_auto) fallback_hits = fallback_hits + 1; + end + checks = checks + 1; + end + endtask + + task automatic cancel(input bit fault_value, simultaneous_arm); + begin + @(negedge clk); + stop = !fault_value; + session_fault = fault_value; + arm = simultaneous_arm; + sample_valid = 1; + eligible = 1; + sample_data = channels(1023, 1023, 1023, 1023); + #0.3; + if (trigger_hit !== 1'b0 || auto_hit !== 1'b0) + $fatal(1, "STOP/fault allowed trigger"); + @(posedge clk); + #0.3; + if (armed !== 1'b0) + $fatal(1, "STOP/fault did not outrank ARM/disarm"); + @(negedge clk); + stop = 0; session_fault = 0; arm = 0; sample_valid = 0; + end + endtask + + initial begin + #21 rst = 0; + // Every analog source, both polarities, equality and invalid gaps. + for (source = 0; source < 4; source = source + 1) begin + arm_config(source, 1, 0, 512, 0, 1); + step(selected(source, 512), 1, 0, 1, 0, 0); + step(selected(source, 511), 1, 0, 1, 0, 0); + step(selected(source, 1023), 0, 0, 1, 0, 0); + step(selected(source, 512), 1, 0, 1, 1, 0); + step(selected(source, 0), 1, 0, 1, 0, 0); + step(selected(source, 1023), 1, 0, 1, 0, 0); + arm_config(source, 2, 1, 512, 0, 1); + step(selected(source, 512), 1, 0, 1, 0, 0); + step(selected(source, 513), 1, 0, 1, 0, 0); + step(selected(source, 0), 0, 0, 1, 0, 0); + step(selected(source, 512), 1, 0, 1, 1, 0); + end + + // Nonzero hysteresis is inclusive at both arm/fire thresholds. + arm_config(0, 1, 0, 500, 20, 0); + step(selected(0, 500), 1, 0, 1, 0, 0); + step(selected(0, 480), 1, 0, 1, 0, 0); + step(selected(0, 510), 1, 0, 1, 0, 0); + step(selected(0, 519), 1, 0, 1, 0, 0); + step(selected(0, 520), 1, 0, 1, 1, 0); + arm_config(1, 1, 1, 500, 20, 0); + step(selected(1, 519), 1, 0, 1, 0, 0); + step(selected(1, 520), 1, 0, 1, 0, 0); + step(selected(1, 500), 1, 0, 1, 0, 0); + step(selected(1, 481), 1, 0, 1, 0, 0); + step(selected(1, 480), 1, 0, 1, 1, 0); + + // Threshold arithmetic must clamp rather than wrap at the ADC limits. + arm_config(2, 1, 0, 5, 10, 0); + step(selected(2, 1), 1, 0, 1, 0, 0); + step(selected(2, 0), 1, 0, 1, 0, 0); + step(selected(2, 14), 1, 0, 1, 0, 0); + step(selected(2, 15), 1, 0, 1, 1, 0); + arm_config(3, 1, 1, 1018, 10, 0); + step(selected(3, 1022), 1, 0, 1, 0, 0); + step(selected(3, 1023), 1, 0, 1, 0, 0); + step(selected(3, 1009), 1, 0, 1, 0, 0); + step(selected(3, 1008), 1, 0, 1, 1, 0); + arm_config(0, 1, 0, 512, 1023, 0); + step(selected(0, 0), 1, 0, 1, 0, 0); + step(selected(0, 512), 1, 0, 1, 0, 0); + step(selected(0, 1023), 1, 0, 1, 1, 0); + arm_config(0, 1, 1, 512, 1023, 0); + step(selected(0, 1023), 1, 0, 1, 0, 0); + step(selected(0, 512), 1, 0, 1, 0, 0); + step(selected(0, 0), 1, 0, 1, 1, 0); + arm_config(0, 1, 0, 0, 0, 0); + step(selected(0, 0), 1, 0, 1, 0, 0); + step(selected(0, 1023), 1, 0, 1, 0, 0); + cancel(0, 0); + arm_config(0, 1, 1, 1023, 0, 0); + step(selected(0, 1023), 1, 0, 1, 0, 0); + step(selected(0, 0), 1, 0, 1, 0, 0); + cancel(0, 0); + arm_config(0, 1, 0, 1023, 0, 0); + step(selected(0, 1022), 1, 0, 1, 0, 0); + step(selected(0, 1023), 1, 0, 1, 1, 0); + arm_config(0, 1, 1, 0, 0, 0); + step(selected(0, 1), 1, 0, 1, 0, 0); + step(selected(0, 0), 1, 0, 1, 1, 0); + + // A prehistory crossing is consumed, not replayed when eligible rises. + arm_config(0, 1, 0, 500, 0, 0); + step(selected(0, 400), 1, 0, 0, 0, 0); + step(selected(0, 600), 1, 0, 0, 0, 0); + step(selected(0, 600), 1, 0, 1, 0, 0); + step(selected(0, 400), 1, 0, 1, 0, 0); + step(selected(0, 500), 1, 0, 1, 1, 0); + + // External level history starts with an observation, not a fake edge. + arm_config(4, 1, 0, 512, 0, 0); + step(0, 1, 1, 1, 0, 0); + step(0, 1, 0, 1, 0, 0); + step(0, 0, 1, 1, 0, 0); + step(0, 1, 1, 1, 1, 0); + arm_config(4, 2, 1, 512, 0, 0); + step(0, 1, 0, 1, 0, 0); + step(0, 1, 1, 1, 0, 0); + step(0, 1, 0, 1, 1, 0); + arm_config(4, 1, 0, 512, 0, 0); + step(0, 1, 0, 0, 0, 0); + step(0, 1, 1, 0, 0, 0); + step(0, 1, 1, 1, 0, 0); + step(0, 1, 0, 1, 0, 0); + step(0, 1, 1, 1, 1, 0); + + // Live config edits must not alter the armed session. + arm_config(0, 1, 0, 400, 0, 99); + cfg_source = 3; cfg_mode = 0; cfg_falling = 1; + cfg_level = 1; cfg_hysteresis = 1023; cfg_auto_timeout = 0; + step(channels(399, 0, 0, 0), 1, 0, 1, 0, 0); + step(channels(399, 0, 0, 1023), 1, 0, 1, 0, 0); + step(channels(400, 0, 0, 0), 1, 0, 1, 1, 0); + + // AUTO counts accepted samples, never idle clock/valid gaps. + arm_config(0, 0, 0, 500, 0, 3); + step(selected(0, 600), 1, 0, 1, 0, 0); + step(selected(0, 400), 0, 0, 1, 0, 0); + step(selected(0, 600), 1, 0, 1, 0, 0); + step(selected(0, 600), 0, 0, 1, 0, 0); + step(selected(0, 600), 1, 0, 1, 1, 1); + arm_config(0, 0, 0, 500, 0, 2); + step(selected(0, 600), 1, 0, 0, 0, 0); + step(selected(0, 600), 1, 0, 0, 0, 0); + step(selected(0, 600), 1, 0, 0, 0, 0); + step(selected(0, 600), 1, 0, 1, 1, 1); + arm_config(4, 0, 0, 500, 0, 0); + step(0, 1, 1, 0, 0, 0); + step(0, 0, 0, 1, 0, 0); + step(0, 1, 1, 1, 1, 1); + arm_config(0, 0, 0, 500, 0, 2); + step(selected(0, 400), 1, 0, 1, 0, 0); + step(selected(0, 500), 1, 0, 1, 1, 0); // real edge outranks timeout + + // NORMAL/SINGLE never time out; both fire once per explicit ARM. + arm_config(0, 1, 0, 500, 0, 1); + step(selected(0, 600), 1, 0, 1, 0, 0); + step(selected(0, 600), 1, 0, 1, 0, 0); + cancel(0, 0); + arm_config(0, 2, 0, 500, 0, 1); + step(selected(0, 600), 1, 0, 1, 0, 0); + step(selected(0, 400), 1, 0, 1, 0, 0); + step(selected(0, 500), 1, 0, 1, 1, 0); + step(selected(0, 400), 1, 0, 1, 0, 0); + step(selected(0, 500), 1, 0, 1, 0, 0); + + arm_config(0, 1, 0, 500, 0, 0); + step(selected(0, 400), 1, 0, 1, 0, 0); + cancel(0, 0); // STOP on prospective crossing + arm_config(0, 0, 0, 500, 0, 0); + cancel(1, 0); // fault also suppresses immediate AUTO fallback + arm_config(0, 1, 0, 500, 0, 0); + cancel(0, 1); // STOP wins over simultaneous ARM + arm_config(0, 1, 0, 500, 0, 0); + cancel(1, 1); // fault wins over simultaneous ARM + + for (source = 5; source < 8; source = source + 1) begin + arm_config(source, 1, 0, 500, 0, 0); + step(selected(0, 0), 1, 0, 1, 0, 0); + step(selected(0, 1023), 1, 1, 1, 0, 0); + end + arm_config(0, 3, 0, 500, 0, 0); + step(selected(0, 0), 1, 0, 1, 0, 0); + arm_config(0, 1, 0, 500, 0, 0); + if (config_error !== 1'b1) $fatal(1, "valid ARM cleared sticky config error"); + step(selected(0, 400), 1, 0, 1, 0, 0); + step(selected(0, 500), 1, 0, 1, 1, 0); + #1.7 rst = 1; + #25; + if (armed !== 1'b0 || config_error !== 1'b0 || trigger_index !== 64'd0 || + trigger_hit !== 1'b0 || auto_hit !== 1'b0) + $fatal(1, "reset did not clear trigger/config state"); + rst = 0; + if (checks < 100 || hits < 20 || fallback_hits != 3) + $fatal(1, "insufficient trigger coverage checks=%0d hits=%0d AUTO=%0d", checks, hits, fallback_hits); + $display("PASS tb_trigger_engine checks=%0d hits=%0d auto=%0d", checks, hits, fallback_hits); + $finish; + end + initial begin + #30000; + $fatal(1, "tb_trigger_engine timeout"); + end +endmodule diff --git a/hdl/scope-acquisition/tests/tb_triggered_acquisition.sv b/hdl/scope-acquisition/tests/tb_triggered_acquisition.sv new file mode 100644 index 0000000..4acd1df --- /dev/null +++ b/hdl/scope-acquisition/tests/tb_triggered_acquisition.sv @@ -0,0 +1,58 @@ +`timescale 1ns/1ps +module tb_triggered_acquisition; + reg clk=0,rst=1,arm=0,stop=0,session_fault=0; + reg [31:0] cfg_pre=5,cfg_post=7,cfg_auto_timeout=50; + reg [2:0] cfg_source=2; + reg [1:0] cfg_mode=1; + reg cfg_falling=0; + reg [9:0] cfg_level=500,cfg_hysteresis=5; + reg [63:0] sample_data=0,sample_index=0; + reg sample_valid=0,ext_level=0,m_ready=0; + wire active,done,fault,m_valid,m_last,trigger_event; + wire [63:0] trigger_index,first_index,m_data,m_index; + wire [31:0] record_length; + reg [63:0] expected[0:199]; + integer n,received=0, cycles=0; + real sine_value; + always #5 clk=~clk; + triggered_acquisition #(.ADDR_BITS(5)) dut(.*); + function automatic [63:0] waveform(input integer i); + reg [9:0] c1,c2,c3,c4; + real v; + begin + v=300.0+200.0*$sin(i*0.2); + c1=$rtoi(v); + c2=(i%20<10) ? 10'd200 : 10'd800; + // Deterministic bounded noise + one explicit threshold crossing. + c3=(i<40) ? (450+(i*37)%41) : (520+(i*11)%101); + c4=(i*73+91)%1024; + waveform={6'b0,c4,6'b0,c3,6'b0,c2,6'b0,c1}; + end + endfunction + always @(posedge clk) if(m_valid&&m_ready) begin + if(m_index!==35+received || m_data!==expected[35+received] || m_last!==(received==12)) + $fatal(1,"trigger/buffer alignment at output %0d",received); + received=received+1; + end + initial begin + repeat(2) @(negedge clk); rst=0;arm=1; + @(negedge clk); arm=0; + // Present consecutive accepted samples at full fabric rate. + for(n=0;n<90;n=n+1) begin + sample_valid=1;sample_index=n;sample_data=waveform(n);expected[n]=waveform(n); + @(negedge clk); + end + sample_valid=0; + if(!done||fault||active||trigger_index!=40||first_index!=35||record_length!=13) + $fatal(1,"exact trigger/pre/post metadata"); + while(received<13) begin + m_ready=(cycles%3!=0);cycles=cycles+1; + @(negedge clk); + end + m_ready=1; repeat(3) @(negedge clk); + if(m_valid||received!=13) $fatal(1,"incorrect record size"); + $display("PASS tb_triggered_acquisition trigger=40 window=35..47 full_rate sine_square_noise"); + $finish; + end + initial begin #20000;$fatal(1,"timeout");end +endmodule diff --git a/hdl/vfd-pwm/README.md b/hdl/vfd-pwm/README.md new file mode 100644 index 0000000..58a8236 --- /dev/null +++ b/hdl/vfd-pwm/README.md @@ -0,0 +1,13 @@ +# Трёхфазный генератор ШИМ для испытаний + +`rtl/vfd_pwm.sv` выдаёт шесть комплементарных сигналов с центрированной +несущей и мёртвым временем. DDS обновляет фазу с частотой `UPDATE_HZ`, +рампа пересчитывается каждую миллисекунду. Верхнее значение счётчика +несущей равно `CLOCK_HZ/(2*Fcarrier)`. Параметр `mod_cfg` задаёт +глубину модуляции в Q15. Для удержания работы подавайте импульсы +`heartbeat` чаще интервала `WATCHDOG_CYCLES`. + +Сигнал применяется для тестовых перемычек на входы анализатора. +`tools/gen_vfd_sine.py` воспроизводит таблицу четверти синуса. +`tests/tb_vfd_pwm.sv` проверяет рампы, блокировку одновременно открытых +выходов, остановку и watchdog. diff --git a/hdl/vfd-pwm/rtl/vfd_pwm.sv b/hdl/vfd-pwm/rtl/vfd_pwm.sv new file mode 100644 index 0000000..35142b0 --- /dev/null +++ b/hdl/vfd-pwm/rtl/vfd_pwm.sv @@ -0,0 +1,222 @@ +`timescale 1ns/1ps +// Test waveform source, not a closed-loop motor controller. +// Frequency DDS at 100 kHz; ramp at 1 kHz; center-aligned complementary SPWM. +module vfd_pwm #( + parameter integer CLOCK_HZ=100000000, + parameter integer UPDATE_HZ=100000, + parameter integer WATCHDOG_CYCLES=CLOCK_HZ/2 +)( + input wire clk, aresetn, + input wire start, off, ramp_stop, apply_target, heartbeat, + input wire signed [31:0] target_cfg, + input wire [31:0] up_cfg, down_cfg, + input wire [15:0] top_cfg, dead_cfg, mod_cfg, + output reg enabled, fault, + output reg signed [31:0] actual, target, + output wire [5:0] gates +); + localparam integer PHASE_DIV=CLOCK_HZ/UPDATE_HZ; + localparam integer RAMP_DIV=CLOCK_HZ/1000; + reg [31:0] phase_timer, ramp_timer, watchdog; + reg [31:0] phase, up_step, down_step; + reg [15:0] carrier, top, dead_ticks, modulation; + reg descending, stopping; + reg [15:0] threshold[0:2]; + reg [4:0] duty_valid; + reg [7:0] angle_index[0:2]; + reg signed [11:0] sine_sample[0:2]; + reg signed [28:0] modulated[0:2]; + reg [15:0] duty[0:2]; + reg [31:0] scaled_duty[0:2]; + wire [31:0] phase_b = phase + 32'h55555555; + wire [31:0] phase_c = phase + 32'haaaaaaaa; + reg [2:0] previous_raw; + reg [15:0] blank[0:2]; + reg [5:0] gate_reg; + assign gates = enabled && aresetn ? gate_reg : 6'b0; + wire [2:0] raw = {carrier < threshold[2], carrier < threshold[1], carrier < threshold[0]}; + // Quarter-wave table is generated reproducibly by scripts/gen_vfd_sine.py. + function automatic signed [11:0] quarter(input [6:0] index); + begin + case(index) + // VFD_SINE_TABLE + 7'd0: quarter=12'sd0; + 7'd1: quarter=12'sd50; + 7'd2: quarter=12'sd100; + 7'd3: quarter=12'sd151; + 7'd4: quarter=12'sd201; + 7'd5: quarter=12'sd251; + 7'd6: quarter=12'sd300; + 7'd7: quarter=12'sd350; + 7'd8: quarter=12'sd399; + 7'd9: quarter=12'sd449; + 7'd10: quarter=12'sd497; + 7'd11: quarter=12'sd546; + 7'd12: quarter=12'sd594; + 7'd13: quarter=12'sd642; + 7'd14: quarter=12'sd690; + 7'd15: quarter=12'sd737; + 7'd16: quarter=12'sd783; + 7'd17: quarter=12'sd830; + 7'd18: quarter=12'sd875; + 7'd19: quarter=12'sd920; + 7'd20: quarter=12'sd965; + 7'd21: quarter=12'sd1009; + 7'd22: quarter=12'sd1052; + 7'd23: quarter=12'sd1095; + 7'd24: quarter=12'sd1137; + 7'd25: quarter=12'sd1179; + 7'd26: quarter=12'sd1219; + 7'd27: quarter=12'sd1259; + 7'd28: quarter=12'sd1299; + 7'd29: quarter=12'sd1337; + 7'd30: quarter=12'sd1375; + 7'd31: quarter=12'sd1411; + 7'd32: quarter=12'sd1447; + 7'd33: quarter=12'sd1483; + 7'd34: quarter=12'sd1517; + 7'd35: quarter=12'sd1550; + 7'd36: quarter=12'sd1582; + 7'd37: quarter=12'sd1614; + 7'd38: quarter=12'sd1644; + 7'd39: quarter=12'sd1674; + 7'd40: quarter=12'sd1702; + 7'd41: quarter=12'sd1729; + 7'd42: quarter=12'sd1756; + 7'd43: quarter=12'sd1781; + 7'd44: quarter=12'sd1805; + 7'd45: quarter=12'sd1828; + 7'd46: quarter=12'sd1850; + 7'd47: quarter=12'sd1871; + 7'd48: quarter=12'sd1891; + 7'd49: quarter=12'sd1910; + 7'd50: quarter=12'sd1927; + 7'd51: quarter=12'sd1944; + 7'd52: quarter=12'sd1959; + 7'd53: quarter=12'sd1973; + 7'd54: quarter=12'sd1986; + 7'd55: quarter=12'sd1997; + 7'd56: quarter=12'sd2008; + 7'd57: quarter=12'sd2017; + 7'd58: quarter=12'sd2025; + 7'd59: quarter=12'sd2032; + 7'd60: quarter=12'sd2037; + 7'd61: quarter=12'sd2041; + 7'd62: quarter=12'sd2045; + 7'd63: quarter=12'sd2046; + 7'd64: quarter=12'sd2047; + default: quarter=12'sd0; + endcase + end + endfunction + function automatic signed [11:0] sine(input [7:0] index); + reg [6:0] x; + begin + x=index[6] ? 7'd64-{1'b0,index[5:0]} : {1'b0,index[5:0]}; + sine=index[7] ? -quarter(x) : quarter(x); + end + endfunction + // The five stages run ahead of the carrier boundary. All legs use + // the same DDS phase, and active thresholds change together at zero. + integer leg; + always @(posedge clk) begin + if(!aresetn || !enabled) duty_valid<=0; + else duty_valid<={duty_valid[3:0],1'b1}; + angle_index[0]<=phase[31:24]; + angle_index[1]<=phase_b[31:24]; + angle_index[2]<=phase_c[31:24]; + for(leg=0;leg<3;leg=leg+1) begin + sine_sample[leg]<=sine(angle_index[leg]); + modulated[leg]<=sine_sample[leg]*$signed({1'b0,modulation}); + duty[leg]<=16'h8000+modulated[leg][26:11]; + scaled_duty[leg]<=duty[leg]*top; + end + end + // Snapshot the goal on the 1 kHz tick. The result commits three + // clocks later, separating comparison, arithmetic and saturation. + reg [2:0] ramp_valid; + reg signed [31:0] ramp_goal; + reg signed [33:0] ramp_step, ramp_proposed; + reg ramp_add; + wire ramp_tick = ramp_timer==RAMP_DIV-1; + wire ramp_negative = actual[31] || ramp_goal[31]; + wire ramp_up = ramp_negative ? ramp_goalactual; + always @(posedge clk) begin + if(!aresetn || !enabled) ramp_valid<=0; + else ramp_valid<={ramp_valid[1:0],ramp_tick}; + if(ramp_tick) begin + // A reversal first decelerates to zero. + ramp_goal<=(actual!=0 && target!=0 && actual[31]!=target[31]) ? 0 : target; + end + if(ramp_valid[0]) begin + ramp_add<=actual<=ramp_goal; + ramp_step<={2'b00,(ramp_up ? up_step : down_step)}; + end + if(ramp_valid[1]) begin + ramp_proposed<=ramp_add ? $signed(actual)+ramp_step : $signed(actual)-ramp_step; + end + end + integer i; + always @(posedge clk) begin + if(!aresetn) begin + enabled<=0; fault<=0; actual<=0; target<=0; phase<=0; + phase_timer<=0; ramp_timer<=0; watchdog<=0; + carrier<=0; descending<=0; stopping<=0; + top<=100; dead_ticks<=1; modulation<=0; up_step<=1; down_step<=1; + previous_raw<=0; gate_reg<=0; + for(i=0;i<3;i=i+1) begin threshold[i]<=0; blank[i]<=0; end + end else begin + if(!enabled) begin + gate_reg<=0; previous_raw<=0; + for(i=0;i<3;i=i+1) blank[i]<=dead_ticks; + end else begin + watchdog<=watchdog+1; + if(phase_timer==PHASE_DIV-1) begin + phase_timer<=0; phase<=phase+actual; + end else phase_timer<=phase_timer+1; + if(ramp_tick) begin + ramp_timer<=0; + if(stopping && actual==0) enabled<=0; + end else ramp_timer<=ramp_timer+1; + if(ramp_valid[2]) begin + if((ramp_add && ramp_proposed>ramp_goal) || (!ramp_add && ramp_proposed=WATCHDOG_CYCLES-1 && !heartbeat) begin + enabled<=0; fault<=1; actual<=0; target<=0; + end + end + if(heartbeat) watchdog<=0; + if(start && !enabled) begin + enabled<=1; fault<=0; actual<=0; target<=target_cfg; + up_step<=up_cfg; down_step<=down_cfg; top<=top_cfg; + dead_ticks<=dead_cfg; modulation<=mod_cfg; + phase<=0; phase_timer<=0; ramp_timer<=0; watchdog<=0; + carrier<=0; descending<=0; stopping<=0; gate_reg<=0; + previous_raw<=0; + for(i=0;i<3;i=i+1) begin threshold[i]<=0; blank[i]<=dead_cfg; end + end + if(off) begin enabled<=0; actual<=0; target<=0; gate_reg<=0; end + end + end +endmodule diff --git a/hdl/vfd-pwm/tests/tb_vfd_pwm.sv b/hdl/vfd-pwm/tests/tb_vfd_pwm.sv new file mode 100644 index 0000000..71237de --- /dev/null +++ b/hdl/vfd-pwm/tests/tb_vfd_pwm.sv @@ -0,0 +1,63 @@ +`timescale 1ns/1ps +module tb_vfd_pwm; + reg clk=0; always #500 clk=~clk; + reg aresetn=0,start=0,off=0,ramp_stop=0,apply_target=0,heartbeat=1; + reg signed [31:0] target_cfg=100000; + reg [31:0] up_cfg=10000,down_cfg=20000; + reg [15:0] top_cfg=50,dead_cfg=3,mod_cfg=26214; + wire enabled,fault; wire signed [31:0] actual,target; wire [5:0] gates; + vfd_pwm #(.CLOCK_HZ(1000000),.WATCHDOG_CYCLES(40000)) dut(.*); + integer i,j,cycle=0; + integer last_high[0:2],last_low[0:2]; + reg [5:0] prior=0; + always @(posedge clk) begin + #1; cycle=cycle+1; + for(i=0;i<3;i=i+1) begin + if(gates[2*i] && gates[2*i+1]) $fatal(1,"overlap leg %d",i); + if(gates[2*i]) last_high[i]=cycle; + if(gates[2*i+1]) last_low[i]=cycle; + if(gates[2*i] && !prior[2*i] && cycle-last_low[i]<=3) $fatal(1,"short low->high blank"); + if(gates[2*i+1] && !prior[2*i+1] && cycle-last_high[i]<=3) $fatal(1,"short high->low blank"); + end + if(!enabled && gates!=0) $fatal(1,"disabled output"); + prior=gates; + end + task command_start; + begin @(negedge clk); start=1; @(negedge clk); start=0; end + endtask + task ramp_tick; + begin wait(dut.ramp_timer==999); repeat(4) @(posedge clk); #2; end + endtask + initial begin + for(j=0;j<3;j=j+1) begin last_high[j]=-100; last_low[j]=-100; end + repeat(4) @(negedge clk); aresetn=1; + command_start(); + repeat(110) @(negedge clk); + if(!(dut.threshold[1]>dut.threshold[0] && dut.threshold[0]>dut.threshold[2])) $fatal(1,"phase order 120 deg"); + if(dut.threshold[0]<24 || dut.threshold[0]>26 || dut.threshold[1]<41 || dut.threshold[2]>9) $fatal(1,"SPWM amplitude"); + for(j=1;j<=10;j=j+1) begin + ramp_tick(); if(actual!==j*10000) $fatal(1,"accel actual=%d tick=%d",actual,j); + @(negedge clk); + end + ramp_tick(); if(actual!==100000) $fatal(1,"target overshoot"); + @(negedge clk); target_cfg=-100000; apply_target=1; + @(negedge clk); apply_target=0; + for(j=1;j<=5;j=j+1) begin ramp_tick(); if(actual!==100000-j*20000) $fatal(1,"reverse decel"); @(negedge clk); end + for(j=1;j<=10;j=j+1) begin ramp_tick(); if(actual!==-j*10000) $fatal(1,"reverse accel"); @(negedge clk); end + ramp_stop=1; @(negedge clk); ramp_stop=0; + for(j=1;j<=5;j=j+1) begin ramp_tick(); if(actual!==-100000+j*20000) $fatal(1,"ramp stop"); @(negedge clk); end + ramp_tick(); if(enabled || gates) $fatal(1,"zero stop did not disable"); + @(negedge clk); target_cfg=1; command_start(); + ramp_tick(); if(actual!==1) $fatal(1,"small target saturation"); + @(negedge clk); heartbeat=0; + repeat(40002) @(negedge clk); + if(enabled || !fault || gates || actual) $fatal(1,"watchdog"); + heartbeat=1; repeat(10) @(negedge clk); + if(enabled) $fatal(1,"heartbeat auto restart"); + command_start(); if(!enabled || fault) $fatal(1,"explicit restart"); + off=1; @(negedge clk); off=0; + if(enabled || gates) $fatal(1,"immediate off"); + $display("PASS tb_vfd_pwm ramp reverse saturation stop watchdog deadtime SPWM"); $finish; + end + initial begin #200000000; $fatal(1,"timeout"); end +endmodule diff --git a/hdl/vfd-pwm/tools/gen_vfd_sine.py b/hdl/vfd-pwm/tools/gen_vfd_sine.py new file mode 100644 index 0000000..9207ab6 --- /dev/null +++ b/hdl/vfd-pwm/tools/gen_vfd_sine.py @@ -0,0 +1,12 @@ +"""Regenerate the 65-entry quarter-wave LUT embedded in vfd_pwm.sv.""" +import math +from pathlib import Path + +path = Path(__file__).resolve().parents[1]/'rtl/vfd_pwm.sv' +text = path.read_text(encoding='utf-8') +begin = text.index(' // VFD_SINE_TABLE') +end = text.index(' default:', begin) +table = ' // VFD_SINE_TABLE\n' +table += ''.join(f" 7'd{i}: quarter=12'sd{round(2047*math.sin(i*math.pi/128))};\n" + for i in range(65)) +path.write_text(text[:begin]+table+text[end:], encoding='utf-8', newline='\n') diff --git a/python/logic_analyzer/decoders/pwm_waveform.py b/python/logic_analyzer/decoders/pwm_waveform.py new file mode 100644 index 0000000..f33d1d8 --- /dev/null +++ b/python/logic_analyzer/decoders/pwm_waveform.py @@ -0,0 +1,69 @@ +"""Streaming PWM demodulation; sample coordinates, no GUI dependencies.""" +import math + + +class PwmChannel: + def __init__(self, samplerate, active_high=True, carrier_hz=0, smoothing_ms=0): + if not math.isfinite(samplerate) or samplerate <= 0: + raise ValueError('Samplerate must be positive.') + if not math.isfinite(carrier_hz) or carrier_hz < 0 or carrier_hz > samplerate / 2: + raise ValueError('Carrier must be 0 (automatic) or <= samplerate / 2.') + if not math.isfinite(smoothing_ms) or smoothing_ms < 0: + raise ValueError('Smoothing must be nonnegative.') + self.rate = samplerate + self.active_high = active_high + self.window = samplerate / carrier_hz if carrier_hz else None + self.tau = smoothing_ms * samplerate / 1000 + self.last = self.level = self.start = self.fall = None + self.origin = None + self.window_index = 1 + self.high = 0 + self.previous = None + + def next_boundary(self): + if self.window is None or self.origin is None: + return None + return round(self.origin + self.window_index * self.window) + + def _emit(self, start, end, high): + duty = high / (end - start) + value = 2 * duty - 1 + midpoint = (start + end) // 2 + previous = self.previous + if previous is not None and self.tau: + alpha = -math.expm1(-(midpoint - previous[0]) / self.tau) + value = previous[1] + alpha * (value - previous[1]) + self.previous = (midpoint, value) + return dict(start=start, end=end, duty=duty, value=value, + frequency=self.rate / (end - start), midpoint=midpoint, + previous=previous) + + def update(self, sample, pin): + level = bool(pin) == self.active_high + if self.last is None: + self.last, self.level = sample, level + if self.window is not None: + self.origin = self.start = sample + return [] + if sample < self.last: + raise ValueError('Samples must be monotonic.') + events = [] + if self.window is not None: + boundary = self.next_boundary() + while boundary <= sample: + self.high += (boundary - self.last) * self.level + events.append(self._emit(self.start, boundary, self.high)) + self.start = self.last = boundary + self.high = 0 + self.window_index += 1 + boundary = self.next_boundary() + self.high += (sample - self.last) * self.level + elif level != self.level: + if level: + if self.start is not None and self.fall is not None: + events.append(self._emit(self.start, sample, self.fall - self.start)) + self.start, self.fall = sample, None + elif self.start is not None: + self.fall = sample + self.last, self.level = sample, level + return events diff --git a/tools/dsview/README.md b/tools/dsview/README.md index e096361..a057456 100644 --- a/tools/dsview/README.md +++ b/tools/dsview/README.md @@ -225,3 +225,11 @@ section headings. The changes are packaged in `decoder-panel.patch`. Перед установкой закрыть целевой DSView, затем открыть его снова. Добавление пакетов обновляет Python-декодеры без пересборки EXE; изменение раздела «Пользовательские» применяется при следующей нативной сборке. + +## PWM waveform / ШИМ → синус + +Декодер восстанавливает кривые заполнения для 1–3 цифровых входов. +В SETGUI он находится в меню **Анализаторы SET → ШИМ → синус · 1–3 канала**. +[Настройки, режимы и ограничения](decoders/pwm_waveform/README.md). +Сборка включает `pwm-waveform.patch`; один установщик Python-декодеров +не добавляет графическую отрисовку в старый DSView. diff --git a/tools/dsview/build.py b/tools/dsview/build.py index c2872db..e4c580f 100644 --- a/tools/dsview/build.py +++ b/tools/dsview/build.py @@ -28,7 +28,7 @@ def protocol_payload(): # Одна ведомость управляет упа def recipe_digest(): """Identify the patch, packaging recipe, adapters and their shared cores.""" recipe = Path(__file__).resolve().parent - inputs = [recipe / 'build.py', recipe / 'protocol-files.json', recipe / 'setgui-host.patch', recipe / 'gate-pair-checkbox.patch', recipe / 'decoder-panel.patch', recipe / 'driver-summary.patch', recipe / 'windows-usb-events.patch', recipe / 'native/gate_summary.h'] # Ведомость и нативные патчи входят в версию рецепта. + inputs = [recipe / 'build.py', recipe / 'protocol-files.json', recipe / 'setgui-host.patch', recipe / 'gate-pair-checkbox.patch', recipe / 'decoder-panel.patch', recipe / 'driver-summary.patch', recipe / 'windows-usb-events.patch', recipe / 'native/gate_summary.h', recipe / 'pwm-waveform.patch', recipe / 'native/pwm_waveform.h'] # Ведомость и нативные патчи входят в версию рецепта. inputs += [source for source, _ in protocol_payload()] # Изменение парсера, адаптера или JSON-карты делает установленный компонент устаревшим. digest = hashlib.sha256() for item in inputs: @@ -78,12 +78,13 @@ def main(): if subprocess.run(['git', 'apply', '--ignore-space-change', '--reverse', '--check', str(retired)], cwd=source, capture_output=True).returncode == 0: subprocess.run(['git', 'apply', '--ignore-space-change', '--reverse', str(retired)], cwd=source, check=True) - for patch_name in ('setgui-host.patch', 'decoder-panel.patch', 'windows-usb-events.patch'): + for patch_name in ('setgui-host.patch', 'decoder-panel.patch', 'windows-usb-events.patch', 'pwm-waveform.patch'): patch = Path(__file__).with_name(patch_name) applied = subprocess.run(['git', 'apply', '--ignore-space-change', '--reverse', '--check', str(patch)], cwd=source, capture_output=True) if applied.returncode: subprocess.run(['git', 'apply', '--ignore-space-change', '--check', str(patch)], cwd=source, check=True) subprocess.run(['git', 'apply', '--ignore-space-change', str(patch)], cwd=source, check=True) + shutil.copy2(Path(__file__).with_name('native') / 'pwm_waveform.h', source / 'DSView/pv/view/pwm_waveform.h') env = dict(os.environ) env['PATH'] = os.pathsep.join([str(prefix/'bin'), str(args.msys_root/'usr/bin'), env.get('PATH', '')]) env['PKG_CONFIG_PATH'] = str(prefix/'lib/pkgconfig') @@ -133,9 +134,10 @@ def main(): if version != 'DSView host protocol 1': raise RuntimeError('Host protocol self-check failed: ' + version) (output/'host-protocol-1').write_text(version+'\n', encoding='utf-8') - for patch_name in ('setgui-host.patch', 'decoder-panel.patch', 'windows-usb-events.patch'): + for patch_name in ('setgui-host.patch', 'decoder-panel.patch', 'windows-usb-events.patch', 'pwm-waveform.patch'): shutil.copy2(Path(__file__).with_name(patch_name), output/patch_name) - (output/'SOURCE.txt').write_text('https://github.com/DreamSourceLab/DSView\nCommit: '+revision+'\nPatches: setgui-host.patch, decoder-panel.patch, windows-usb-events.patch\nLicense: GPL-3.0-or-later (COPYING)\n', encoding='utf-8') + shutil.copy2(Path(__file__).with_name('native') / 'pwm_waveform.h', output / 'pwm_waveform.h') + (output/'SOURCE.txt').write_text('https://github.com/DreamSourceLab/DSView\nCommit: '+revision+'\nPatches: setgui-host.patch, decoder-panel.patch, windows-usb-events.patch, pwm-waveform.patch (pwm_waveform.h)\nLicense: GPL-3.0-or-later (COPYING)\n', encoding='utf-8') (output/'host-build.json').write_text(json.dumps({ 'recipe': recipe_digest(), 'executable': hashlib.sha256((output/'DSView.exe').read_bytes()).hexdigest(), diff --git a/tools/dsview/decoders/pwm_waveform/README.md b/tools/dsview/decoders/pwm_waveform/README.md new file mode 100644 index 0000000..477c238 --- /dev/null +++ b/tools/dsview/decoders/pwm_waveform/README.md @@ -0,0 +1,48 @@ +# ШИМ → синус / PWM waveform + +В SETGUI: **DSView → Анализаторы SET → ШИМ → синус · 1–3 канала**. +В нативном списке DSView: **Пользовательские → PWM waveform**. +Выберите PWM A; PWM B/C необязательны. Каждый вход получает собственную +цветную кривую с общей временной шкалой записи и строку заполнения. +Для большего числа входов добавьте ещё один экземпляр декодера. + +Кривая — измеренное заполнение `2 * Ton / T - 1`, а не искусственный синус. +50% соответствует нулю, 0% — −1, 100% — +1. Значения между серединами +периодов соединены прямыми. Это нормированная модуляция, не измерение +напряжения, тока или выхода силового LC-фильтра. Для SVPWM в ней остаётся +синфазная составляющая: декодер не вычитает фазы друг из друга. + +- **Active level**: независимая инверсия каждого входа. +- **Carrier Hz = 0**: измерение между активными фронтами, включая изменения + частоты. Начальный и конечный неполные периоды пропускаются. При пропавших + импульсах период становится длиннее; постоянные 0/1 автоматически не + восстанавливаются. Для насыщения используйте заданную несущую. +- **Carrier Hz > 0**: интегрирование заполнения в равных временных окнах + длиной `1/f`, начиная с начала области декодирования. Работает на 0/100%. + Окна не синхронизированы с фронтами; при неверной несущей или быстро + меняющейся модуляции возможны биения/искажения. В таблице указана частота + окна, а не измеренная частота ШИМ. Неполное последнее окно пропускается. +- **Low-pass time constant (ms)**: 0 отключает фильтр; положительное значение + включает фильтр первого порядка. Он подавляет пульсации, но вносит задержку + и ослабление. Для начала используйте 0. + +Для уверенного восстановления нужны десятки отсчётов на период несущей и +достаточная длительность записи для нескольких периодов синуса. + +Ядро: `python/logic_analyzer/decoders/pwm_waveform.py`; адаптер и ядро входят +в `protocol-files.json`. Установщик копирует их по обычной схеме. +**Кривая требует сборки DSView с `pwm-waveform.patch` и +`native/pwm_waveform.h`** (штатный `tools/dsview/build.py` включает оба). +В старом/стандартном DSView будут только числовые аннотации. + +Проверки: `python -m unittest discover -s tools/dsview/tests -p test_pwm_waveform.py`. + +Нативная проверка (Python с PySide6, Windows): + +```powershell +python tools/dsview/tests/check_pwm_native.py --runtime C:/path/to/dsview --output C:/path/to/pwm-check +``` + +Она создаёт синтетическую трёхфазную запись, открывает её в отдельном DSView, +сохраняет `pwm-native.png` и закрывает процесс. На изображении должны быть +три синусоиды со сдвигом 120°. Аппаратный захват не запускается. diff --git a/tools/dsview/decoders/pwm_waveform/__init__.py b/tools/dsview/decoders/pwm_waveform/__init__.py new file mode 100644 index 0000000..e99f16b --- /dev/null +++ b/tools/dsview/decoders/pwm_waveform/__init__.py @@ -0,0 +1 @@ +from .pd import Decoder diff --git a/tools/dsview/decoders/pwm_waveform/pd.py b/tools/dsview/decoders/pwm_waveform/pd.py new file mode 100644 index 0000000..7655185 --- /dev/null +++ b/tools/dsview/decoders/pwm_waveform/pd.py @@ -0,0 +1,93 @@ +"""PWM envelope on up to three independent logic channels.""" +import sigrokdecode as srd +from common.setgui_decoders.pwm_waveform import PwmChannel + + +class Decoder(srd.Decoder): + api_version = 3 + id = 'pwm_waveform' + name = 'PWM waveform' + longname = 'PWM waveform / sine reconstruction' + desc = 'Reconstructs normalized waveforms from PWM duty on 1-3 channels.' + license = 'gplv2+' + inputs = ['logic'] + outputs = [] + tags = ['Clock/timing', 'Util'] + channels = ({'id': 'a', 'name': 'PWM A', 'desc': 'First PWM input'},) + optional_channels = ( + {'id': 'b', 'name': 'PWM B', 'desc': 'Second PWM input'}, + {'id': 'c', 'name': 'PWM C', 'desc': 'Third PWM input'}, + ) + options = tuple( + {'id': 'active_' + ch, 'desc': 'PWM ' + ch.upper() + ' active level', + 'default': 'high', 'values': ('high', 'low')} for ch in 'abc' + ) + ( + {'id': 'carrier_hz', 'desc': 'Carrier Hz (0 = edge-to-edge)', 'default': 0}, + {'id': 'smoothing_ms', 'desc': 'Low-pass time constant (ms, 0 = off)', 'default': 0.0}, + ) + annotations = ( + ('wave-a', 'Wave A'), ('wave-b', 'Wave B'), ('wave-c', 'Wave C'), + ('duty-a', 'Duty A'), ('duty-b', 'Duty B'), ('duty-c', 'Duty C'), + ) + annotation_rows = ( + ('wave_a', 'A: -1 .. +1', (0,)), + ('wave_b', 'B: -1 .. +1', (1,)), + ('wave_c', 'C: -1 .. +1', (2,)), + ('duty_a', 'A duty / frequency', (3,)), + ('duty_b', 'B duty / frequency', (4,)), + ('duty_c', 'C duty / frequency', (5,)), + ) + + def __init__(self): + self.reset() + + def reset(self): + self.samplerate = None + self.channels_state = {} + + def metadata(self, key, value): + if key == srd.SRD_CONF_SAMPLERATE: + self.samplerate = value + + def start(self): + self.out_ann = self.register(srd.OUTPUT_ANN) + + def _update(self, pins): + for ch, state in self.channels_state.items(): + for event in state.update(self.samplenum, pins[ch]): + label = 'D={:.3f}% {}'.format(event['duty'] * 100, + ('window ' if state.window else 'f=') + '{:.3f} Hz'.format(event['frequency'])) + self.put(event['start'], event['end'], self.out_ann, + [3 + ch, [label, '{:.2f}%'.format(event['duty'] * 100)]]) + previous = event['previous'] + if previous is not None: + # Last label is the versioned native renderer contract. + self.put(previous[0], event['midpoint'], self.out_ann, [ch, [ + 'PWM {}: {:+.6f}'.format('ABC'[ch], event['value']), + 'PWM1 {:.9f} {:.9f}'.format(previous[1], event['value'])]]) + + def decode(self): + if not self.samplerate: + raise ValueError('Cannot decode without samplerate.') + self.channels_state = {ch: PwmChannel(self.samplerate, + self.options['active_' + 'abc'[ch]] == 'high', + self.options['carrier_hz'], self.options['smoothing_ms']) + for ch in range(3) if self.has_channel(ch)} + pins = list(self.wait({})) + self._update(pins) + while True: + conditions = [{ch: 'e'} for ch in self.channels_state] + boundaries = [s.next_boundary() for s in self.channels_state.values() + if s.next_boundary() is not None] + if boundaries: + conditions.append({'skip': max(1, min(boundaries) - self.samplenum)}) + pins = list(self.wait(conditions)) + self._update(pins) + + def end(self): + # Complete fixed windows only; never infer a partial edge-based period. + if self.channels_state: + pins = [0, 0, 0] + for ch, state in self.channels_state.items(): + pins[ch] = state.level == state.active_high + self._update(pins) diff --git a/tools/dsview/native/pwm_waveform.h b/tools/dsview/native/pwm_waveform.h new file mode 100644 index 0000000..3cb722d --- /dev/null +++ b/tools/dsview/native/pwm_waveform.h @@ -0,0 +1,29 @@ +// PWM1 annotation rendering contract: normalized start/end values, -1..+1. +// Kept outside DecodeTrace so geometry can be exercised without a capture device. +#ifndef SET_PWM_WAVEFORM_H +#define SET_PWM_WAVEFORM_H +#include +#include +#include + +namespace set_pwm { +inline bool segment(const QString &text, double &a, double &b) +{ + const QStringList fields = text.split(' '); + if (fields.size() != 3 || fields[0] != "PWM1") return false; + bool okA = false, okB = false; + a = fields[1].toDouble(&okA); + b = fields[2].toDouble(&okB); + return okA && okB && std::isfinite(a) && std::isfinite(b) + && a >= -1 && a <= 1 && b >= -1 && b <= 1; +} + +inline void draw(QPainter &p, const QString &text, double x1, double x2, + double y, double amplitude) +{ + double a, b; + if (segment(text, a, b)) + p.drawLine(QPointF(x1, y - a * amplitude), QPointF(x2, y - b * amplitude)); +} +} +#endif diff --git a/tools/dsview/protocol-files.json b/tools/dsview/protocol-files.json index 975df95..8ea0cd1 100644 --- a/tools/dsview/protocol-files.json +++ b/tools/dsview/protocol-files.json @@ -1,6 +1,6 @@ { - "adapters": ["gate_driver_timing", "transistor_pair", "set_uart", "set_can", "pm35_uart", "tms_uart", "plates_pbus", "plates_sbus"], - "cores": ["__init__", "gate_timing", "transistor_pair", "set_uart", "set_can", "pm35_uart", "template_protocols", "tms_uart"], + "adapters": ["gate_driver_timing", "transistor_pair", "set_uart", "set_can", "pm35_uart", "tms_uart", "plates_pbus", "plates_sbus", "pwm_waveform"], + "cores": ["__init__", "gate_timing", "transistor_pair", "set_uart", "set_can", "pm35_uart", "template_protocols", "tms_uart", "pwm_waveform"], "template_files": [ "protocan/__init__.py", "protocan/native.py", @@ -10,6 +10,7 @@ "protocan/gas_167.py", "protocan/gas_167_registers.json", "set_devices/__init__.py", + "set_devices/deif_native.py", "set_devices/deif_xdi.py", "set_devices/ump_can.py", "set_devices/tms_terminal.py" diff --git a/tools/dsview/pwm-waveform.patch b/tools/dsview/pwm-waveform.patch new file mode 100644 index 0000000..8ca447d --- /dev/null +++ b/tools/dsview/pwm-waveform.patch @@ -0,0 +1,71 @@ +--- a/DSView/pv/view/decodetrace.cpp ++++ b/DSView/pv/view/decodetrace.cpp +@@ -33,6 +33,7 @@ + #include + #include + #include "decodetrace.h" ++#include "pwm_waveform.h" + #include "../sigsession.h" + #include "../data/decoderstack.h" + #include "../data/decode/decoder.h" +@@ -269,6 +270,36 @@ + if ((*i).second) { + const Row &row = (*i).first; + ++ // A PWM envelope stays a curve even when carrier periods ++ // are narrower than a pixel (normal annotation fallback ++ // would replace the entire sine with a solid box). ++ if (QString::fromUtf8(dec->decoder()->id) == "pwm_waveform" ++ && row.row() && QString::fromUtf8(row.row()->id).startsWith("wave_")) { ++ std::vector points; ++ _decoder_stack->get_annotation_subset(points, row, start_sample, end_sample); ++ p.save(); ++ const int wave_y = y + annotation_height; ++ p.setClipRect(QRectF(left, y - annotation_height * 0.5, ++ right - left, annotation_height * 3)); ++ p.setPen(QPen(fore, 1, Qt::DotLine)); ++ p.drawLine(QPointF(left, wave_y), QPointF(right, wave_y)); ++ p.setRenderHint(QPainter::Antialiasing, true); ++ for (Annotation *a : points) { ++ if (a->annotations().empty()) continue; ++ const QColor colors[] = {QColor(80, 210, 255), QColor(255, 194, 70), QColor(145, 230, 100)}; ++ p.setPen(QPen(colors[a->format() % 3], 1.6)); ++ set_pwm::draw(p, a->annotations().back(), ++ a->start_sample() / samples_per_pixel - pixels_offset, ++ a->end_sample() / samples_per_pixel - pixels_offset, ++ wave_y, annotation_height * 1.20); ++ } ++ p.restore(); ++ y += annotation_height * 3; ++ _cur_row_headings.push_back(row.title()); ++ _cur_row_headings.push_back(QString()); ++ _cur_row_headings.push_back(QString()); ++ continue; ++ } + + const uint64_t min_annotation = + _decoder_stack->get_min_annotation(row); +@@ -586,7 +617,12 @@ + if (_row.decoder() == dec->decoder() && + _decoder_stack->has_annotations((*i).first) && + (*i).second) ++ { + size++; ++ if (QString::fromUtf8(dec->decoder()->id) == "pwm_waveform" ++ && _row.row() && QString::fromUtf8(_row.row()->id).startsWith("wave_")) ++ size += 2; ++ } + } + } + else { +--- a/DSView/pv/dock/protocoldock.cpp ++++ b/DSView/pv/dock/protocoldock.cpp +@@ -1060,7 +1060,7 @@ + { + SearchComboBox *panel = new SearchComboBox(this); + +- const QStringList customIds = {"gate_driver_timing", "transistor_pair", "set_uart", "set_can", "pm35_uart", "tms_uart", "plates_pbus", "plates_sbus"}; // При сборке пользовательский раздел включает анализаторы обеих шин 96-контактного интерфейса. ++ const QStringList customIds = {"gate_driver_timing", "transistor_pair", "pwm_waveform", "set_uart", "set_can", "pm35_uart", "tms_uart", "plates_pbus", "plates_sbus"}; // При сборке пользовательский раздел включает анализаторы обеих шин 96-контактного интерфейса. + for (bool custom : {true, false}) { + for (auto info : _decoderInfoList) { + srd_decoder *dec = (srd_decoder *)(info->_data_handle); diff --git a/tools/dsview/tests/check_pwm_native.py b/tools/dsview/tests/check_pwm_native.py new file mode 100644 index 0000000..e210f86 --- /dev/null +++ b/tools/dsview/tests/check_pwm_native.py @@ -0,0 +1,92 @@ +"""Manual native smoke test: synthetic 3-phase PWM, embedded DSView screenshot. + +Run with a Python containing PySide6. No acquisition hardware is accessed. +""" +import argparse +import json +import math +import os +from pathlib import Path +import subprocess +import time +import zipfile + + +def fixture(path): + count, rate, period = 60032, 1000000, 500 + decoders = [dict(id='pwm_waveform', version=2, label='PWM sine A / B / C', + view_index=3, channel=[{'a': 0}, {'b': 1}, {'c': 2}], + options=dict(active_a='high', active_b='high', active_c='high', + carrier_hz=0, smoothing_ms=0.0), + **{'stacked decoders': [], 'show': {'pwm_waveform': True, + **{'pwm_waveform: '+ch+' duty / frequency': False for ch in 'ABC'}}})] + session = dict(Device='virtual-session', DeviceMode=0, Version=3, Title='DSView v1.3.2', + decoder=decoders, channel=[dict(index=i, enabled=True, name='D'+str(i), + colour='default', strigger=0, type=10000, view_index=i) for i in range(3)], + **{'Sample count': str(count), 'Sample rate': str(rate), 'Max Height': '1X'}) + header = ('[version]\nversion = 3\n[header]\ndriver = virtual-session\n' + 'device mode = 0\ncapturefile = data\n' + f'total samples = {count}\ntotal probes = 3\ntotal blocks = 1\n' + f'samplerate = {rate} Hz\ntrigger time = 0\ntrigger pos = 0\n' + 'probe0 = D0\nprobe1 = D1\nprobe2 = D2\n') + with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED) as archive: + archive.writestr('header', header) + archive.writestr('session', json.dumps(session)) + archive.writestr('decoders', json.dumps(decoders)) + for ch in range(3): + bits = bytearray(count // 8) + for i in range(count): + width = round(250 + 200 * math.sin(2 * math.pi * (i // period) / 40 + ch * 2 * math.pi / 3)) + if i % period < width: + bits[i // 8] |= 1 << (i % 8) + archive.writestr(f'L-{ch}/0', bits) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--runtime', type=Path, required=True) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + from PySide6.QtWidgets import QApplication, QWidget + runtime, output = args.runtime.resolve(), args.output.resolve() + output.mkdir(parents=True, exist_ok=True) + capture = output / 'pwm-three-phase.dsl' + screenshot = output / 'pwm-native.png' + fixture(capture) + app = QApplication([]) + window = QWidget() + window.resize(1400, 850) + window.setWindowTitle('PWM native rendering test') + window.show() + env = {k: v for k, v in os.environ.items() if not k.startswith(('PYTHON', 'QT_', 'QML'))} + env['PATH'] = str(runtime) + os.pathsep + str(Path(os.environ['SystemRoot']) / 'System32') + env['DSVIEW_HOST_SCREENSHOT'] = str(screenshot) + began = time.time() + with (output / 'pwm-native.log').open('w', encoding='utf-8') as log: + process = subprocess.Popen([str(runtime / 'DSView.exe'), '--embed-parent', + str(int(window.winId())), str(capture)], cwd=runtime, env=env, + stdin=subprocess.PIPE, stdout=log, stderr=log, creationflags=subprocess.CREATE_NO_WINDOW) + try: + deadline = time.monotonic() + 8 + while time.monotonic() < deadline and process.poll() is None: + app.processEvents() + time.sleep(.02) + if process.poll() is None: + process.stdin.write(b'CLOSE\n') + process.stdin.flush() + deadline = time.monotonic() + 5 + while time.monotonic() < deadline and process.poll() is None: + app.processEvents() + time.sleep(.02) + assert process.poll() == 0, 'DSView did not close cleanly; inspect pwm-native.log' + assert screenshot.exists() and screenshot.stat().st_mtime >= began, 'No fresh render artifact' + finally: + if process.poll() is None: + process.terminate() + process.wait(timeout=5) + window.close() + print('Inspect three phase-shifted sine curves:', screenshot) + + +if __name__ == '__main__': + main() diff --git a/tools/dsview/tests/test_install_decoders.py b/tools/dsview/tests/test_install_decoders.py index e4009bb..192eec2 100644 --- a/tools/dsview/tests/test_install_decoders.py +++ b/tools/dsview/tests/test_install_decoders.py @@ -48,7 +48,7 @@ sys.path.insert(0, str(root)) stub = types.ModuleType('sigrokdecode') stub.Decoder = type('Decoder', (), {}) sys.modules['sigrokdecode'] = stub -for name in ('gate_driver_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart', 'tms_uart', 'plates_pbus', 'plates_sbus'): # Все анализаторы, включая обе 96-контактные шины, загружаются без установленного SETGUI. +for name in ('gate_driver_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart', 'tms_uart', 'plates_pbus', 'plates_sbus', 'pwm_waveform'): # Все анализаторы, включая обе 96-контактные шины, загружаются без установленного SETGUI. module = importlib.import_module(name + '.pd') assert module.Decoder.api_version == 3 core = importlib.import_module('common.setgui_decoders.gate_timing') diff --git a/tools/dsview/tests/test_pwm_waveform.py b/tools/dsview/tests/test_pwm_waveform.py new file mode 100644 index 0000000..06f9760 --- /dev/null +++ b/tools/dsview/tests/test_pwm_waveform.py @@ -0,0 +1,117 @@ +"""PWM reconstruction vectors and sigrok wait/annotation boundary tests.""" +import importlib.util +import math +from pathlib import Path +import sys +import types +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[3] +spec = importlib.util.spec_from_file_location('pwm_core', ROOT / 'python/logic_analyzer/decoders/pwm_waveform.py') +core = importlib.util.module_from_spec(spec) +spec.loader.exec_module(core) + + +class PwmTests(unittest.TestCase): + def test_sine_three_phases_and_polarity(self): + for phase in (0, 2 * math.pi / 3, 4 * math.pi / 3): + for active in (True, False): + state = core.PwmChannel(1000000, active) + result = [] + state.update(0, active) # Initial partial cycle must be ignored. + state.update(100, not active) + expected = [] + for i in range(101): + width = round(500 + 400 * math.sin(2 * math.pi * i / 100 + phase)) + result.extend(state.update(1000 + i * 1000, active)) + result.extend(state.update(1000 + i * 1000 + width, not active)) + expected.append(2 * width / 1000 - 1) + self.assertEqual(len(result), 100) + for e, value in zip(result, expected): + self.assertAlmostEqual(e['value'], value) + self.assertEqual(e['frequency'], 1000) + + def test_fixed_windows_include_zero_and_full_duty(self): + state = core.PwmChannel(1000, carrier_hz=100) + state.update(0, 0) + events = state.update(30, 1) + state.update(60, 0) + self.assertEqual([e['value'] for e in events], [-1]*3 + [1]*3) + self.assertEqual(state.next_boundary(), 70) + + def test_fractional_windows_do_not_accumulate_rounding_error(self): + state = core.PwmChannel(1000, carrier_hz=60) + state.update(0, 1) + events = state.update(100000, 1) + self.assertEqual(len(events), 6000) + self.assertEqual(events[-1]['end'], 100000) + self.assertTrue(all(e['duty'] == 1 for e in events)) + + def test_smoothing_step_response(self): + state = core.PwmChannel(1000, carrier_hz=100, smoothing_ms=10) + state.update(0, 0) + state.update(10, 1) + value = state.update(20, 1)[0]['value'] + self.assertAlmostEqual(value, 1 - 2 * math.exp(-1)) + + def test_partial_capture_has_no_invented_cycle(self): + state = core.PwmChannel(1000) + self.assertEqual(state.update(0, 1) + state.update(5, 0) + + state.update(10, 1) + state.update(12, 0), []) + + def test_invalid_settings(self): + for args in ((0,), (1000, True, -1), (1000, True, 501), + (1000, True, 0, -1), (float('nan'),)): + with self.assertRaises(ValueError): + core.PwmChannel(*args) + + def test_adapter_optional_channels_and_skip(self): + class StopCapture(Exception): + pass + + class FakeDecoder: + def register(self, kind): + return kind + + def has_channel(self, ch): + return ch in (0, 2) + + def put(self, ss, es, output, data): + self.emitted.append((ss, es, data)) + + def wait(self, conditions): + if conditions == {}: + self.samplenum = 0 + else: + self.test.assertEqual(conditions, [{0: 'e'}, {2: 'e'}, {'skip': 10}]) + self.samplenum += 10 + if self.samplenum > 30: + self.samplenum = 30 + raise StopCapture() + return (0, 255, 1) + + stub = types.ModuleType('sigrokdecode') + stub.Decoder, stub.OUTPUT_ANN, stub.SRD_CONF_SAMPLERATE = FakeDecoder, 0, 1 + with patch.dict(sys.modules, {'sigrokdecode': stub, 'common.setgui_decoders.pwm_waveform': core}): + spec = importlib.util.spec_from_file_location('pwm_adapter', ROOT / 'tools/dsview/decoders/pwm_waveform/pd.py') + adapter = importlib.util.module_from_spec(spec) + spec.loader.exec_module(adapter) + decoder = adapter.Decoder() + decoder.test = self + decoder.options = {o['id']: o['default'] for o in decoder.options} + decoder.options['carrier_hz'] = 100 + decoder.emitted = [] + decoder.metadata(1, 1000) + decoder.start() + with self.assertRaises(StopCapture): + decoder.decode() + decoder.end() + self.assertEqual(len(decoder.emitted), 10) + self.assertEqual({e[2][0] for e in decoder.emitted}, {0, 2, 3, 5}) + curves = [e for e in decoder.emitted if e[2][0] < 3] + self.assertEqual(curves[0][:2], (5, 15)) + self.assertEqual(curves[0][2][1][-1], 'PWM1 -1.000000000 -1.000000000') + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/dsview/zynq-ethernet/README.md b/tools/dsview/zynq-ethernet/README.md new file mode 100644 index 0000000..b96c0f1 --- /dev/null +++ b/tools/dsview/zynq-ethernet/README.md @@ -0,0 +1,62 @@ +# Нативный Zynq Ethernet для DSView + +Драйвер регистрирует сетевой прибор в списке DSView. Через нативные +Run и Stop он читает каналы по TCP (ZLA1/ZLS1) или UDP (ZLT1/ZLB1/ZLP1/ZLP2/ZLR1). +Фронты декодируются из тех же отсчётов, которые получает экран DSView. +Для RJ2 драйвер запрашивает буфер сокета 16 МиБ и восстанавливает +порядок отсчётов в окне на 4096 UDP-пакетов. + +Для сборки нужны MSYS2 UCRT64 и зависимости из +[основного рецепта](../README.md). Исходный DSView берётся из +[upstream](https://github.com/DreamSourceLab/DSView/tree/2e9e2c8e726df4ef5687d39b83d4f797cc44b574). +Рецепт применяет патчи SETGUI и `ethernet.patch`, копирует драйвер, +дисковый писатель и заголовки, затем собирает полный runtime. + +```powershell +git clone https://github.com/DreamSourceLab/DSView.git build/dsview-upstream +git -C build/dsview-upstream checkout 2e9e2c8e726df4ef5687d39b83d4f797cc44b574 +python tools/dsview/zynq-ethernet/build.py --msys-root C:/msys64 --output build/dsview +``` + +Для SETGUI положите полный каталог `dsview` рядом с приложением. +Внутри DSView нажмите **Zynq Ethernet…**, введите IPv4, выберите +режим и используйте обычные настройки частоты, длительности и каналов. +С прошивкой ZLP2 список частот учитывает число включённых каналов. +Для **Zynq RJ2** режим выбирается через **Options → Operation Mode**. +**FPGA Snapshot (Advanced)** передаёт окно из BRAM после аппаратного +срабатывания. Глубина до 65 536 отсчётов, до 200 Мвыб/с на всех +восьми каналах. ZLT1 включает меню **Advanced**: до 16 стадий, AND/OR, +инверсию, число совпадений, смежные совпадения и Serial Trigger +с условиями Start/Stop/Clock и шаблоном данных. Буфер хранит +предысторию по положению триггера. В 16-позиционных полях DSView +входы D0–D7 — восемь правых позиций. + +- **Buffer Mode** пишет один ZIP64 DSL порциями до Stop. Экран + показывает ожидание триггера и длительность записи после его + срабатывания. Полный захват виден после закрытия файла. +- **Stream Mode → Single** пишет выбранную длительность до 60 минут + и показывает текущее окно при записи. После окончания график + переключается на полный файл. **Repetitive** повторяет захваты, + **Loop** показывает поток в кольцевом окне. + +**Options → Max Height** задаёт высоту дорожек от 1X до 5X. +Перетаскивание двойной черты под меткой канала меняет высоту +этой дорожки. Колесо над метками прокручивает список каналов. + +Новые файлы содержат индекс `zynq_tiles.bin`. Общий масштаб читает +индекс, при увеличении график читает точные отсчёты нужных блоков. +Для старых файлов RJ2 индекс строится в памяти при открытии. +Время открытия зависит от размера записи и скорости диска. +При сетевой ошибке файл содержит непрерывный префикс принятых данных. +При ошибке диска может остаться временный файл `.part-*`. + +При поддержке ZLR1 частота 200 Мвыб/с доступна и для 5–8 каналов. +В этом случае FPGA запускает поток по аппаратному триггеру и сжимает +повторы уровней без потерь. Если входы меняются слишком часто и +очередь переполняется, захват завершается с ошибкой. На 1–4 каналах +200 Мвыб/с по-прежнему идут через ZLP2 с фиксированным темпом. +Прошивка и пины относятся к проекту платы. + +`package_arm.py` упаковывает тот же набор декодеров для Linux armhf. +Платформенная сборка, FPGA, ядро и переключатель AUX ETH / Local +относятся к проекту прибора. diff --git a/tools/dsview/zynq-ethernet/build.py b/tools/dsview/zynq-ethernet/build.py new file mode 100644 index 0000000..d78b6b8 --- /dev/null +++ b/tools/dsview/zynq-ethernet/build.py @@ -0,0 +1,60 @@ +"""Build DSView with the Zynq Ethernet driver using the SETGUI recipe.""" +import argparse +from pathlib import Path +import shutil +import subprocess +import sys + +HERE = Path(__file__).resolve().parent +TEMPLATES = HERE.parents[2] +TEMPLATE_PATCHES = ('setgui-host.patch', 'decoder-panel.patch', 'windows-usb-events.patch') + + +def apply_patch(source, patch): + command = ['git', 'apply', '--ignore-space-change'] + if subprocess.run(command + ['--reverse', '--check', str(patch)], + cwd=source, capture_output=True).returncode: + subprocess.run(command + ['--check', str(patch)], cwd=source, check=True) + subprocess.run(command + [str(patch)], cwd=source, check=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source', type=Path, default=Path('build/dsview-upstream')) + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--msys-root', type=Path, required=True) + parser.add_argument('--recipe', type=Path, default=HERE.parent) + args = parser.parse_args() + + source, output, recipe = (p.resolve() for p in (args.source, args.output, args.recipe)) + revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=source, text=True).strip() + if not revision.startswith('2e9e2c8e'): + parser.error('DSView checkout must be at revision 2e9e2c8e') + + for name in TEMPLATE_PATCHES: + apply_patch(source, recipe / name) + apply_patch(source, HERE / 'ethernet.patch') + + analyzer = TEMPLATES / 'hdl/logic-analyzer/include' + ethernet = TEMPLATES / 'hdl/logic-udp-ethernet/include' + inputs = (HERE / 'zynq.c', HERE / 'zynq_record.c', HERE / 'zynq_record.h', + analyzer / 'zynq_la.h', analyzer / 'zynq_la_stream.h', + ethernet / 'zynq_la_pl.h') + driver = source / 'libsigrok4DSL/hardware/zynq' + driver.mkdir(parents=True, exist_ok=True) + for path in inputs: + shutil.copy2(path, driver / path.name) + + subprocess.run([sys.executable, str(recipe / 'build.py'), '--source', str(source), + '--output', str(output), '--msys-root', str(args.msys_root.resolve())], check=True) + source_files = output / 'zynq-source' + source_files.mkdir(exist_ok=True) + for path in (*inputs, HERE / 'ethernet.patch'): + shutil.copy2(path, source_files / path.name) + with (output / 'SOURCE.txt').open('a', encoding='utf-8') as manifest: + manifest.write('Zynq Ethernet driver and integration patch: zynq-source/\n') + (output / 'zynq-ethernet-1').write_text('DSView Zynq Ethernet protocol 1\n', encoding='utf-8') + + +if __name__ == '__main__': + main() diff --git a/tools/dsview/zynq-ethernet/ethernet.patch b/tools/dsview/zynq-ethernet/ethernet.patch new file mode 100644 index 0000000..c3b3e00 --- /dev/null +++ b/tools/dsview/zynq-ethernet/ethernet.patch @@ -0,0 +1,1543 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 575eb85..6102914 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -94,6 +94,7 @@ include_directories( + #= glib-2.0 + #------------------------------------------------------------------------------- + pkg_search_module(GLIB glib-2.0) ++pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + + if(NOT GLIB_FOUND) + message(FATAL_ERROR "Please install glib!") +@@ -477,6 +478,8 @@ set(libsigrok4DSL_SOURCES + libsigrok4DSL/output/gnuplot.c + libsigrok4DSL/output/srzip.c + libsigrok4DSL/output/vcd.c ++ libsigrok4DSL/hardware/zynq/zynq.c ++ libsigrok4DSL/hardware/zynq/zynq_record.c + libsigrok4DSL/hardware/DSL/dslogic.c + libsigrok4DSL/hardware/common/usb.c + libsigrok4DSL/hardware/common/ezusb.c +@@ -619,6 +622,7 @@ add_compile_options(-O3) + set(DSVIEW_LINK_LIBS + -lz + -lglib-2.0 ++ PkgConfig::GIO + ${CMAKE_THREAD_LIBS_INIT} + ${QT_LIBRARIES} + ${LIBUSB_1_LIBRARIES} +diff --git a/DSView/main.cpp b/DSView/main.cpp +index 58d2d15..869a6c1 100644 +--- a/DSView/main.cpp ++++ b/DSView/main.cpp +@@ -38,6 +38,7 @@ + #include + #include + #include ++#include + + #ifdef _WIN32 + #include +@@ -224,7 +225,8 @@ bool bHighScale = true; + AppControl *control = AppControl::Instance(); + AppConfig &app = AppConfig::Instance(); + app.LoadAll(); //load app config +- if (embedParent) { ++ const bool localZynq = qEnvironmentVariableIsSet("DSVIEW_LOCAL_ZYNQ"); ++ if (embedParent || localZynq) { + app.frameOptions.style = "dark"; + app.appOptions.fontSize = 10; + QString captureDir = QString::fromUtf8(qgetenv("DSVIEW_CAPTURE_DIR")); +@@ -264,6 +266,15 @@ bool bHighScale = true; + dsv_err("init error!"); + return 1; + } ++ if (localZynq) { ++ ds_device_handle analyzer = 0; ++ if (ds_add_zynq_device("127.0.0.1", &analyzer) != SR_OK) { ++ dsv_err("Local Zynq capture service did not answer."); ++ control->UnInit(); ++ dsv_log_uninit(); ++ return 1; ++ } ++ } + + if (open_file != NULL){ + control->_open_file_name = open_file; +@@ -287,8 +298,12 @@ bool bHighScale = true; + } else + #endif + { +- w.ShowFormInit(); +- w.ShowHelpDocAsync(); ++ if (localZynq) ++ w.showMaximized(); ++ else { ++ w.ShowFormInit(); ++ w.ShowHelpDocAsync(); ++ } + } + + ret = a.exec(); //Run the application +diff --git a/DSView/pv/data/logicsnapshot.cpp b/DSView/pv/data/logicsnapshot.cpp +index b2226cd..59d5a0c 100644 +--- a/DSView/pv/data/logicsnapshot.cpp ++++ b/DSView/pv/data/logicsnapshot.cpp +@@ -64,11 +64,18 @@ LogicSnapshot::LogicSnapshot() : + + LogicSnapshot::~LogicSnapshot() + { ++ if (_disk) ++ ds_zynq_disk_close(_disk); + } + + void LogicSnapshot::free_data() + { + Snapshot::free_data(); ++ if (_disk) { ++ ds_zynq_disk_close(_disk); ++ _disk = nullptr; ++ _disk_cache.clear(); ++ } + + for(auto& iter : _ch_data) { + for(auto& iter_rn : iter) { +@@ -117,6 +124,92 @@ void LogicSnapshot::clear() + _free_count = 0; + } + ++bool LogicSnapshot::load_zynq_recording(const QString &file, bool dense_channels) ++{ ++ struct ds_zynq_record_info info; ++ auto path = file.toUtf8(); ++ auto *disk = ds_zynq_disk_open(path.constData(), &info); ++ if (!disk) ++ return false; ++ std::lock_guard lock(_mutex); ++ free_data(); ++ init_all(); ++ _disk = disk; ++ _is_loop = false; ++ _ch_index.clear(); ++ _samplerate = double(info.clock) / info.divider; ++ _total_sample_count = _sample_count = _ring_sample_count = info.samples; ++ const auto *tiles = ds_zynq_disk_tiles(disk); ++ const uint64_t tile_count = (info.samples + DS_ZYNQ_TILE_SAMPLES - 1) / DS_ZYNQ_TILE_SAMPLES; ++ const uint64_t leaves = (info.samples + LeafBlockSamples - 1) / LeafBlockSamples; ++ for (unsigned channel = 0; channel < 8; channel++) { ++ const unsigned mask = 1u << channel; ++ if (!(info.mask & mask)) ++ continue; ++ _disk_channels[_ch_index.size()] = channel; ++ _ch_index.push_back(dense_channels ? _ch_index.size() : channel); ++ _ch_data.emplace_back((leaves + RootScale - 1) / RootScale); ++ auto &roots = _ch_data.back(); ++ for (uint64_t leaf = 0; leaf < leaves; leaf++) { ++ const uint64_t first_tile = leaf * (LeafBlockSamples / DS_ZYNQ_TILE_SAMPLES); ++ const uint64_t stop_tile = min(tile_count, first_tile + LeafBlockSamples / DS_ZYNQ_TILE_SAMPLES); ++ const bool first = tiles[first_tile].first & mask; ++ bool last = first; ++ bool changes = false; ++ for (uint64_t tile = first_tile; tile < stop_tile; tile++) { ++ changes |= (tiles[tile].changes & mask) || (bool(tiles[tile].first & mask) != last); ++ last = tiles[tile].last & mask; ++ } ++ auto &root = roots[leaf / RootScale]; ++ const uint64_t bit = 1ULL << (leaf % RootScale); ++ if (first) root.first |= bit; ++ if (last) root.last |= bit; ++ if (changes) root.tog |= bit; ++ } ++ } ++ _channel_num = _ch_index.size(); ++ return true; ++} ++ ++std::shared_ptr LogicSnapshot::disk_leaf(unsigned order, uint64_t root, unsigned pos) ++{ ++ if (!(_ch_data[order][root].tog & (1ULL << pos))) ++ return {}; ++ const uint64_t leaf = root * RootScale + pos; ++ const uint64_t key = leaf * CHANNEL_MAX_COUNT + order; ++ for (auto i = _disk_cache.begin(); i != _disk_cache.end(); ++i) { ++ if (i->first == key) { ++ _disk_cache.splice(_disk_cache.begin(), _disk_cache, i); ++ return _disk_cache.front().second; ++ } ++ } ++ std::shared_ptr buffer(static_cast(calloc(1, LeafBlockSpace)), free); ++ if (!buffer || ds_zynq_disk_leaf(_disk, leaf, order, buffer.get(), LeafBlockSamples / 8) != SR_OK) { ++ dsv_err("Could not read an RJ2 recording block."); ++ return {}; ++ } ++ // Reuse DSView's edge index. The root summary comes from the recording. ++ auto &node = _ch_data[order][root]; ++ const auto first = node.first, last = node.last, tog = node.tog; ++ node.lbp[pos] = buffer.get(); ++ _last_calc_count[order] = 0; ++ calc_mipmap(order, root, pos, LeafBlockSamples, false); ++ node.lbp[pos] = nullptr; ++ node.first = first; ++ node.last = last; ++ node.tog = tog; ++ _disk_cache.emplace_front(key, buffer); ++ if (_disk_cache.size() > 24) ++ _disk_cache.pop_back(); ++ return buffer; ++} ++ ++uint8_t *LogicSnapshot::leaf_buffer(unsigned order, uint64_t root, unsigned pos) ++{ ++ return _disk ? disk_leaf(order, root, pos).get() : ++ static_cast(_ch_data[order][root].lbp[pos]); ++} ++ + void LogicSnapshot::first_payload(const sr_datafeed_logic &logic, uint64_t total_sample_count, GSList *channels, bool able_free) + { + bool channel_changed = false; +@@ -139,7 +232,7 @@ void LogicSnapshot::first_payload(const sr_datafeed_logic &logic, uint64_t total + } + } + +- if (total_sample_count != _total_sample_count ++ if (_disk || total_sample_count != _total_sample_count + || channel_num != _channel_num + || channel_changed + || _is_loop) { +@@ -464,7 +557,9 @@ void LogicSnapshot::capture_ended() + { + std::lock_guard lock(_mutex); + +- Snapshot::capture_ended(); ++ Snapshot::capture_ended(); ++ if (_disk) ++ return; + + _sample_count = _ring_sample_count; + _ring_sample_count += _loop_offset; +@@ -495,7 +590,7 @@ void LogicSnapshot::capture_ended() + } + } + +-void LogicSnapshot::calc_mipmap(unsigned int order, uint8_t index0, uint8_t index1, uint64_t samples, bool isEnd) ++void LogicSnapshot::calc_mipmap(unsigned int order, uint64_t index0, uint8_t index1, uint64_t samples, bool isEnd) + { + void *lbp = _ch_data[order][index0].lbp[index1]; + void *level1_ptr = (uint8_t*)lbp + LeafBlockSamples / 8; +@@ -610,6 +705,8 @@ const uint8_t *LogicSnapshot::get_samples(uint64_t start_sample, uint64_t &end_s + uint64_t logic_sample_index = start_sample + _loop_offset; + + int order = get_ch_order(sig_index); ++ if (order == -1) ++ return NULL; + // uint64_t index0 = logic_sample_index >> (LeafBlockPower + RootScalePower); + // uint64_t index1 = (logic_sample_index & RootMask) >> LeafBlockPower; + // uint64_t offset = (start_sample & LeafMask) / 8; +@@ -618,7 +715,15 @@ const uint8_t *LogicSnapshot::get_samples(uint64_t start_sample, uint64_t &end_s + uint64_t index1 = (logic_sample_index / LeafBlockSamples) % RootScale; + uint64_t offset = (start_sample % LeafBlockSamples) / 8; + +- uint8_t *block_buffer = (uint8_t*)_ch_data[order][index0].lbp[index1]; ++ uint8_t *block_buffer; ++ if (_disk) { ++ // A decoder uses several channel pointers outside the snapshot lock. ++ static thread_local std::shared_ptr pinned[CHANNEL_MAX_COUNT]; ++ pinned[order] = disk_leaf(order, index0, index1); ++ block_buffer = pinned[order].get(); ++ } else { ++ block_buffer = leaf_buffer(order, index0, index1); ++ } + + int block_num = get_block_num_unlock(); + +@@ -658,7 +763,7 @@ const uint8_t *LogicSnapshot::get_samples(uint64_t start_sample, uint64_t &end_s + } + else{ + if (lbp != NULL){ +- *lbp = _ch_data[order][index0].lbp[index1]; ++ *lbp = block_buffer; + } + + _cur_ref_block_indexs[order].root_index = index0; +@@ -701,8 +806,9 @@ bool LogicSnapshot::get_sample_self(uint64_t index, int sig_index) + return (_ch_data[order][index0].first & root_pos_mask) != 0; + } + else { +- uint64_t *lbp = (uint64_t*)_ch_data[order][index0].lbp[index1]; +- return *(lbp + ((index & LeafMask) >> ScalePower)) & index_mask; ++ const auto *lbp = reinterpret_cast(leaf_buffer(order, index0, index1)); ++ return lbp ? lbp[(index & LeafMask) >> ScalePower] & index_mask : ++ (_ch_data[order][index0].first & root_pos_mask) != 0; + } + } + +@@ -728,6 +834,9 @@ bool LogicSnapshot::get_display_edges(std::vector > &edges + assert(start <= end); + assert(min_length > 0); + ++ if (_disk && min_length >= DS_ZYNQ_TILE_SAMPLES) ++ return disk_display_edges(edges, togs, start, end, width, max_togs, pixels_offset, min_length, sig_index); ++ + uint64_t index = start; + bool last_sample; + bool start_sample; +@@ -771,6 +880,37 @@ bool LogicSnapshot::get_display_edges(std::vector > &edges + return start_sample; + } + ++bool LogicSnapshot::disk_display_edges(std::vector> &edges, ++ std::vector> &togs, uint64_t start, uint64_t end, ++ uint16_t width, uint16_t max_togs, double pixels_offset, double min_length, uint16_t sig_index) ++{ ++ const auto *tiles = ds_zynq_disk_tiles(_disk); ++ const int order = get_ch_order(sig_index); ++ if (order < 0) ++ return false; ++ const unsigned mask = 1u << _disk_channels[order]; ++ uint64_t tile = start / DS_ZYNQ_TILE_SAMPLES; ++ const bool first = tiles[tile].first & mask; ++ bool level = first; ++ togs.emplace_back(0, first); ++ for (unsigned pixel = 0; pixel < width; pixel++) { ++ const double pixel_end = (pixels_offset + pixel + 1) * min_length; ++ const uint64_t last_tile = uint64_t(max(double(start), min(double(end), pixel_end))) / DS_ZYNQ_TILE_SAMPLES; ++ bool changes = false; ++ while (tile <= last_tile) { ++ changes |= (tiles[tile].changes & mask) || bool(tiles[tile].first & mask) != level; ++ level = tiles[tile].last & mask; ++ tile++; ++ } ++ edges.emplace_back(changes, level); ++ if (changes && togs.size() < max_togs) ++ togs.emplace_back(pixel, level); ++ } ++ if (width && togs.size() < max_togs) ++ togs.emplace_back(width - 1, level); ++ return first; ++} ++ + bool LogicSnapshot::get_nxt_edge(uint64_t &index, bool last_sample, uint64_t end, + double min_length, int sig_index) + { +@@ -829,13 +969,13 @@ bool LogicSnapshot::get_nxt_edge_self(uint64_t &index, bool last_sample, uint64_ + } + + if (!edge_hit) { +- uint64_t *lbp = (uint64_t*)_ch_data[order][i].lbp[inner_tog_pos]; + uint64_t blk_start = (i << (LeafBlockPower + RootScalePower)) + (inner_tog_pos << LeafBlockPower); + index = max(blk_start, index); + + if (min_level < ScaleLevel) { + uint64_t block_end = min(index | LeafMask, end); +- edge_hit = block_nxt_edge(lbp, index, block_end, last_sample, min_level); ++ auto *lbp = reinterpret_cast(leaf_buffer(order, i, inner_tog_pos)); ++ edge_hit = lbp && block_nxt_edge(lbp, index, block_end, last_sample, min_level); + } + else { + edge_hit = true; +@@ -922,12 +1062,12 @@ bool LogicSnapshot::get_pre_edge_self(uint64_t &index, bool last_sample, + } + + if (!edge_hit) { +- uint64_t *lbp = (uint64_t*)_ch_data[order][i].lbp[inner_tog_pos]; + uint64_t blk_end = ((i << (LeafBlockPower + RootScalePower)) + + (inner_tog_pos << LeafBlockPower)) | LeafMask; + index = min(blk_end, index); + if (min_level < ScaleLevel) { +- edge_hit = block_pre_edge(lbp, index, last_sample, min_level, sig_index); ++ auto *lbp = reinterpret_cast(leaf_buffer(order, i, inner_tog_pos)); ++ edge_hit = lbp && block_pre_edge(lbp, index, last_sample, min_level, sig_index); + } else { + edge_hit = true; + } +@@ -1431,6 +1571,11 @@ uint64_t LogicSnapshot::get_block_size_unlock(int block_index) + assert(block_index < block_num); + + auto align_sample_count = _ring_sample_count; ++ if (_disk) { ++ const uint64_t count = min(LeafBlockSamples, ++ align_sample_count - uint64_t(block_index) * LeafBlockSamples); ++ return (count + Scale - 1) / Scale * ScaleSize; ++ } + + if (_loop_offset > 0) + { +@@ -1489,7 +1634,14 @@ uint8_t *LogicSnapshot::get_block_buf_unlock(int block_index, int sig_index, boo + + uint64_t index = block_index / RootScale; + uint8_t pos = block_index % RootScale; +- uint8_t *lbp = (uint8_t*)_ch_data[order][index].lbp[pos]; ++ uint8_t *lbp; ++ if (_disk) { ++ static thread_local std::shared_ptr pinned; ++ pinned = disk_leaf(order, index, pos); ++ lbp = pinned.get(); ++ } else { ++ lbp = leaf_buffer(order, index, pos); ++ } + + if (lbp == NULL){ + sample = (_ch_data[order][index].first & 1ULL << pos) != 0; +diff --git a/DSView/pv/data/logicsnapshot.h b/DSView/pv/data/logicsnapshot.h +index acd36dd..7893a3e 100644 +--- a/DSView/pv/data/logicsnapshot.h ++++ b/DSView/pv/data/logicsnapshot.h +@@ -30,6 +30,8 @@ + #include + #include + #include ++#include ++#include + + #define CHANNEL_MAX_COUNT 64 + +@@ -99,6 +101,8 @@ public: + void clear(); + + void init(); ++ bool load_zynq_recording(const QString &file, bool dense_channels = false); ++ bool is_disk_recording() const { return _disk != nullptr; } + + void first_payload(const sr_datafeed_logic &logic, uint64_t total_sample_count, GSList *channels, bool able_free); + +@@ -176,7 +180,13 @@ private: + + int get_ch_order(int sig_index); + +- void calc_mipmap(unsigned int order, uint8_t index0, uint8_t index1, uint64_t samples, bool isEnd); ++ void calc_mipmap(unsigned int order, uint64_t index0, uint8_t index1, uint64_t samples, bool isEnd); ++ std::shared_ptr disk_leaf(unsigned order, uint64_t root, unsigned pos); ++ uint8_t *leaf_buffer(unsigned order, uint64_t root, unsigned pos); ++ bool disk_display_edges(std::vector> &edges, ++ std::vector> &togs, uint64_t start, uint64_t end, ++ uint16_t width, uint16_t max_togs, double pixels_offset, double min_length, ++ uint16_t sig_index); + + void append_cross_payload(const sr_datafeed_logic &logic); + +@@ -249,6 +259,9 @@ private: + + private: + std::vector> _ch_data; ++ struct ds_zynq_disk *_disk = nullptr; ++ unsigned _disk_channels[8] = {}; ++ std::list>> _disk_cache; + uint8_t _byte_fraction; + uint16_t _ch_fraction; + uint8_t *_dest_ptr; +diff --git a/DSView/pv/deviceagent.h b/DSView/pv/deviceagent.h +index ae9132d..12a88a8 100644 +--- a/DSView/pv/deviceagent.h ++++ b/DSView/pv/deviceagent.h +@@ -72,7 +72,11 @@ public: + } + + inline bool is_hardware(){ +- return _dev_type == DEV_TYPE_USB; ++ return _dev_type == DEV_TYPE_USB || is_network(); ++ } ++ ++ inline bool is_network(){ ++ return _dev_type == DEV_TYPE_NETWORK; + } + + inline bool is_virtual(){ +diff --git a/DSView/pv/dock/triggerdock.cpp b/DSView/pv/dock/triggerdock.cpp +index 8280cdc..5716485 100644 +--- a/DSView/pv/dock/triggerdock.cpp ++++ b/DSView/pv/dock/triggerdock.cpp +@@ -270,11 +270,15 @@ void TriggerDock::device_updated() + _session->get_device()->get_config_bool(SR_CONF_STREAM, stream); + sample_limits = _session->get_device()->get_sample_limit(); + +- _adv_radioButton->setEnabled(!stream); +- _position_spinBox->setEnabled(!stream); +- _position_slider->setEnabled(!stream); +- +- if (stream) ++ bool zynq_advanced = false; ++ if (_session->get_device()->is_network()) ++ _session->get_device()->get_config_bool(SR_CONF_ZYNQ_ADVANCED, zynq_advanced); ++ const bool simple_only = stream || (_session->get_device()->is_network() && !zynq_advanced); ++ _adv_radioButton->setEnabled(!simple_only); ++ _position_spinBox->setEnabled(!simple_only); ++ _position_slider->setEnabled(!simple_only); ++ ++ if (simple_only) + maxRange = 1; + else if (hw_depth >= sample_limits) + maxRange = DS_MAX_TRIG_PERCENT; +@@ -284,7 +288,7 @@ void TriggerDock::device_updated() + _position_spinBox->setRange(MinTrigPosition, maxRange); + _position_slider->setRange(MinTrigPosition, maxRange); + +- if (_session->get_device()->is_virtual() || stream) { ++ if (_session->get_device()->is_virtual() || simple_only) { + _simple_radioButton->setChecked(true); + simple_trigger(); + } +@@ -305,6 +309,8 @@ void TriggerDock::device_updated() + + bool TriggerDock::commit_trigger() + { ++ // The standard editors always contain at least 16 positions. ++ const int editor_channels = qMax(_cur_ch_num, TriggerProbes); + // trigger position update + ds_trigger_set_pos(_position_slider->value()); + +@@ -334,7 +340,7 @@ bool TriggerDock::commit_trigger() + value0_str = _value0_lineEdit_list.at(i)->text(); + value1_str = _value1_lineEdit_list.at(i)->text(); + } +- ds_trigger_stage_set_value(i, _cur_ch_num, value0_str.toLocal8Bit().data(), ++ ds_trigger_stage_set_value(i, editor_channels, value0_str.toLocal8Bit().data(), + value1_str.toLocal8Bit().data()); + } + } else if(_adv_tabWidget->currentIndex() == 1){ +@@ -350,9 +356,9 @@ bool TriggerDock::commit_trigger() + edge_str = _serial_edge_lineEdit->text(); + comp_str = _value1_lineEdit_list.at(1)->text(); + } +- ds_trigger_stage_set_value(0, _cur_ch_num, start_str.toLocal8Bit().data(), ++ ds_trigger_stage_set_value(0, editor_channels, start_str.toLocal8Bit().data(), + stop_str.toLocal8Bit().data()); +- ds_trigger_stage_set_value(1, _cur_ch_num, edge_str.toLocal8Bit().data(), ++ ds_trigger_stage_set_value(1, editor_channels, edge_str.toLocal8Bit().data(), + comp_str.toLocal8Bit().data()); + + //_serial_data_comboBox +diff --git a/DSView/pv/mainwindow.cpp b/DSView/pv/mainwindow.cpp +index b63b17a..81b32f5 100644 +--- a/DSView/pv/mainwindow.cpp ++++ b/DSView/pv/mainwindow.cpp +@@ -492,6 +492,11 @@ namespace pv + title = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_PACKET_ERROR), "Packet Error"); + details = L_S(STR_PAGE_MSG, S_ID(IDS_MSG_PACKET_ERROR_DET), + "the content of received packet are not expected!"); ++ if (_device_agent->is_network()) { ++ QString reason; ++ if (_device_agent->get_config_string(SR_CONF_ZYNQ_LAST_ERROR, reason) && !reason.isEmpty()) ++ details = reason; ++ } + _session->refresh(0); + break; + case SigSession::Data_overflow: +@@ -1988,6 +1993,12 @@ namespace pv + { + update_toolbar_view_status(); + _protocol_widget->update_view_status(); ++ if (_session->show_zynq_recording() || _session->has_zynq_disk_view()) { ++ _view->signals_changed(nullptr); ++ _view->auto_set_max_scale(); ++ _view->set_scale_offset(_view->scale(), 0); ++ _view->update_view_port(); ++ } + break; + } + case DSV_MSG_CURRENT_DEVICE_CHANGE_PREV: +@@ -2074,6 +2085,8 @@ namespace pv + { + _trigger_widget->device_updated(); + _measure_widget->reload(); ++ if (_view->can_resize_logic_rows()) ++ _view->signals_changed(nullptr); + _view->check_calibration(); + break; + } +diff --git a/DSView/pv/sigsession.cpp b/DSView/pv/sigsession.cpp +index 1ad7cc4..cf8eaf3 100644 +--- a/DSView/pv/sigsession.cpp ++++ b/DSView/pv/sigsession.cpp +@@ -315,9 +315,7 @@ namespace pv + std::string file_name = pv::path::ToUnicodePath(name); + dsv_info("Load file: \"%s\"", file_name.c_str()); + +- std::string file_str = name.toUtf8().toStdString(); +- +- if (ds_device_from_file(file_str.c_str()) != SR_OK) ++ if (ds_device_from_file(name.toUtf8().constData()) != SR_OK) + { + dsv_err("Load file error!"); + return false; +@@ -455,6 +453,29 @@ namespace pv + _capture_data->_cur_samplelimits = samplelimits; + } + ++ bool SigSession::show_zynq_recording() ++ { ++ QString file; ++ if (!_device_agent.is_network() || ++ !_device_agent.get_config_string(SR_CONF_ZYNQ_LAST_RECORDING, file) || file.isEmpty()) ++ return false; ++ _device_agent.set_config_string(SR_CONF_ZYNQ_LAST_RECORDING, ""); ++ clear_all_decode_task2(); ++ auto *logic = _capture_data->get_logic(); ++ if (!logic->load_zynq_recording(file)) ++ return false; ++ set_cur_samplelimits(logic->get_sample_count()); ++ _view_data = _capture_data; ++ attach_data_to_signal(_view_data); ++ _callback->trigger_message(DSV_MSG_DATA_POOL_CHANGED); ++ return true; ++ } ++ ++ bool SigSession::has_zynq_disk_view() const ++ { ++ return _view_data->get_logic()->is_disk_recording(); ++ } ++ + void SigSession::capture_init() + { + // update instant setting +@@ -1223,7 +1244,10 @@ namespace pv + + if (_capture_data->get_logic()->last_ended()) + { +- _capture_data->get_logic()->set_loop(is_loop_mode()); ++ bool live_recording = false; ++ if (_device_agent.is_network()) ++ _device_agent.get_config_bool(SR_CONF_ZYNQ_LIVE_RECORDING, live_recording); ++ _capture_data->get_logic()->set_loop(is_loop_mode() || live_recording); + + bool bNotFree = _is_decoding && _view_data == _capture_data; + +@@ -1434,6 +1458,18 @@ namespace pv + feed_in_logic(*(const sr_datafeed_logic *)packet->payload); + break; + ++ case SR_DF_ZYNQ_DISK: ++ { ++ auto *logic = _capture_data->get_logic(); ++ if (logic->load_zynq_recording(QString::fromUtf8(static_cast(packet->payload)), true)) { ++ set_cur_samplelimits(logic->get_sample_count()); ++ } else { ++ _error = Pkt_data_err; ++ _callback->session_error(); ++ } ++ break; ++ } ++ + case SR_DF_DSO: + assert(packet->payload); + assert(!_is_task_end); +@@ -1812,7 +1848,15 @@ namespace pv + + void SigSession::nodata_timeout() + { +- int flag; ++ bool disk_buffer = false; ++ if (_device_agent.is_network() && ++ _device_agent.get_config_bool(SR_CONF_ZYNQ_DISK_BUFFER, disk_buffer) && disk_buffer) { ++ bool triggered; ++ int progress; ++ if (get_capture_status(triggered, progress) && triggered) ++ return; ++ } ++ int flag = -1; + _device_agent.get_config_byte(SR_CONF_TRIGGER_SOURCE, flag); + if (flag != DSO_TRIGGER_AUTO) + { +@@ -2247,7 +2291,7 @@ namespace pv + { + if (_device_agent.get_work_mode() == LOGIC) + { +- bool bAddDecoder = false; ++ bool bAddDecoder = show_zynq_recording() || has_zynq_disk_view(); + bool bSwapBuffer = false; + + if (is_single_mode()) +diff --git a/DSView/pv/sigsession.h b/DSView/pv/sigsession.h +index 3f000e3..9c34692 100644 +--- a/DSView/pv/sigsession.h ++++ b/DSView/pv/sigsession.h +@@ -167,6 +167,8 @@ public: + bool set_default_device(); + bool set_device(ds_device_handle dev_handle); + bool set_file(QString name); ++ bool show_zynq_recording(); ++ bool has_zynq_disk_view() const; + void close_file(ds_device_handle dev_handle); + bool start_capture(bool instant); + bool stop_capture(); +diff --git a/DSView/pv/toolbars/samplingbar.cpp b/DSView/pv/toolbars/samplingbar.cpp +index d82a3b5..80f9ffc 100644 +--- a/DSView/pv/toolbars/samplingbar.cpp ++++ b/DSView/pv/toolbars/samplingbar.cpp +@@ -22,6 +22,14 @@ + #include "samplingbar.h" + #include + #include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include + #include + #include + #include +@@ -101,6 +109,32 @@ namespace pv + _device_type.setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + addWidget(&_device_type); + addWidget(&_device_selector); ++ _action_zynq = addAction(tr("Zynq Ethernet...")); ++ connect(_action_zynq, &QAction::triggered, this, [this]() { ++ if (_session->is_working()) ++ return; ++ ++ QSettings settings; ++ bool accepted = false; ++ const auto address = QInputDialog::getText(this, tr("Connect Zynq"), ++ tr("Board IPv4 address"), QLineEdit::Normal, ++ settings.value("Zynq/IPv4", "192.168.1.68").toString(), &accepted).trimmed(); ++ if (!accepted) ++ return; ++ ++ ds_device_handle handle = 0; ++ const auto result = ds_add_zynq_device(address.toLatin1().constData(), &handle); ++ if (result != SR_OK) { ++ MsgBox::Show(result == SR_ERR_ARG ? tr("Enter a valid IPv4 address.") : ++ tr("No Zynq analyzer replied at %1 (UDP 37575 or TCP 37574/37572).").arg(address)); ++ return; ++ } ++ settings.setValue("Zynq/IPv4", address); ++ update_device_list(); ++ const int index = _device_selector.findData(QVariant::fromValue(handle)); ++ if (index >= 0) ++ _device_selector.setCurrentIndex(index); ++ }); + _configure_button.setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + addWidget(&_configure_button); + +@@ -268,6 +302,10 @@ namespace pv + + if (ret == QDialog::Accepted) + { ++ bool disk_buffer = false; ++ if (_device_agent->is_network() && ++ _device_agent->get_config_bool(SR_CONF_ZYNQ_DISK_BUFFER, disk_buffer) && disk_buffer) ++ _session->set_collect_mode(COLLECT_SINGLE); + if (_session->have_view_data() == false) + this->commit_settings(); + +@@ -472,6 +510,7 @@ namespace pv + void SamplingBar::update_sample_count_selector() + { + bool stream_mode = false; ++ bool disk_buffer = false; + uint64_t hw_depth = 0; + uint64_t sw_depth; + uint64_t rle_depth = 0; +@@ -496,6 +535,8 @@ namespace pv + _updating_sample_count = true; + + _device_agent->get_config_bool(SR_CONF_STREAM, stream_mode); ++ if (_device_agent->is_network()) ++ _device_agent->get_config_bool(SR_CONF_ZYNQ_DISK_BUFFER, disk_buffer); + _device_agent->get_config_uint64(SR_CONF_HW_DEPTH, hw_depth); + int mode = _device_agent->get_work_mode(); + +@@ -533,15 +574,36 @@ namespace pv + _sample_count.currentIndex()) + .value(); + _sample_count.clear(); ++ if (disk_buffer) { ++ _sample_count.addItem(QStringLiteral("До Stop"), QVariant::fromValue(double(0))); ++ _sample_count.setCurrentIndex(0); ++ _sample_count.setEnabled(false); ++ _updating_sample_count = false; ++ connect(&_sample_count, SIGNAL(currentIndexChanged(int)), this, SLOT(on_samplecount_sel(int))); ++ return; ++ } + const uint64_t samplerate = _sample_rate.itemData( + _sample_rate.currentIndex()) + .value(); + const double hw_duration = hw_depth / (samplerate * (1.0 / SR_SEC(1))); ++ uint64_t record_samples; ++ const bool timed_rj2 = stream_mode && _session->is_single_mode() && ++ _device_agent->is_network() && ++ _device_agent->get_config_uint64(SR_CONF_ZYNQ_RECORD_SAMPLES, record_samples); + + if (mode == DSO) + duration = max_timebase; ++ else if (timed_rj2) { ++ if (pre_duration <= 0 && samplerate) ++ pre_duration = double(record_samples) / samplerate * SR_SEC(1); ++ for (int minutes = 60; minutes >= 1; minutes--) ++ _sample_count.addItem(QStringLiteral("%1 мин").arg(minutes), ++ QVariant::fromValue(double(SR_MIN(minutes)))); ++ duration = SR_SEC(50); ++ } + else if (stream_mode) +- duration = sw_depth / (samplerate * (1.0 / SR_SEC(1))); ++ duration = (_device_agent->is_network() ? min(hw_depth, sw_depth) : sw_depth) ++ / (samplerate * (1.0 / SR_SEC(1))); + else if (rle_support) + duration = rle_depth / (samplerate * (1.0 / SR_SEC(1))); + else +@@ -615,7 +677,8 @@ namespace pv + } + _updating_sample_count = false; + +- update_sample_count_selector_value(); ++ if (!timed_rj2) ++ update_sample_count_selector_value(); + on_samplecount_sel(_sample_count.currentIndex()); + + connect(&_sample_count, SIGNAL(currentIndexChanged(int)), this, SLOT(on_samplecount_sel(int))); +@@ -643,7 +706,12 @@ namespace pv + } + else + { +- if (_device_agent->get_config_uint64(SR_CONF_LIMIT_SAMPLES, v)) ++ if (_session->is_single_mode() && _device_agent->is_network() && ++ _device_agent->get_config_uint64(SR_CONF_ZYNQ_RECORD_SAMPLES, v)) ++ { ++ duration = (double)v; ++ } ++ else if (_device_agent->get_config_uint64(SR_CONF_LIMIT_SAMPLES, v)) + { + duration = (double)v; + } +@@ -801,7 +869,10 @@ namespace pv + SR_CONF_SAMPLERATE, + sample_rate); + +- if (_device_agent->get_work_mode() != DSO) ++ bool disk_buffer = false; ++ if (_device_agent->is_network()) ++ _device_agent->get_config_bool(SR_CONF_ZYNQ_DISK_BUFFER, disk_buffer); ++ if (_device_agent->get_work_mode() != DSO && !disk_buffer) + { + const uint64_t sample_count = ((uint64_t)ceil(sample_duration / SR_SEC(1) * + sample_rate) + +@@ -853,6 +924,50 @@ namespace pv + + commit_settings(); + ++ uint64_t recording_samples; ++ bool disk_buffer = false; ++ if (_device_agent->is_network()) ++ _device_agent->get_config_bool(SR_CONF_ZYNQ_DISK_BUFFER, disk_buffer); ++ const bool record_rj2 = (disk_buffer || _session->is_single_mode()) && ++ _device_agent->is_network() && ++ _device_agent->get_config_uint64(SR_CONF_ZYNQ_RECORD_SAMPLES, recording_samples); ++ if (record_rj2) { ++ const auto rate = _device_agent->get_sample_rate(); ++ const uint64_t samples = disk_buffer ? 0 : llround(get_hori_res() / SR_SEC(1) * rate); ++ unsigned channels = _session->get_ch_num(SR_CHANNEL_LOGIC); ++ if (!channels || !rate) ++ return false; ++ QSettings settings; ++ QDir directory(settings.value("Zynq/RecordDirectory", ++ QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)).toString()); ++ const QString suggested = directory.filePath(QStringLiteral("RJ2-%1.dsl") ++ .arg(QDateTime::currentDateTime().toString("yyyyMMdd-HHmmss-zzz"))); ++ QString path = QFileDialog::getSaveFileName(this, ++ QStringLiteral("Сохранить захват RJ2"), suggested, "DSView (*.dsl)"); ++ if (path.isEmpty()) ++ return false; ++ if (!path.endsWith(".dsl", Qt::CaseInsensitive)) ++ path += ".dsl"; ++ QFileInfo info(path); ++ if (info.exists()) { ++ MsgBox::Show(QStringLiteral("Выберите имя нового файла.")); ++ return false; ++ } ++ const QStorageInfo storage(info.absolutePath()); ++ const uint64_t bytes = (samples + 7) / 8 * channels; ++ const bool rle = channels > 4 && rate > SR_MHZ(100); ++ if (!disk_buffer && !rle && storage.isReady() && storage.bytesAvailable() >= 0 && ++ uint64_t(storage.bytesAvailable()) < bytes + bytes / 100 + 1048576) { ++ MsgBox::Show(QStringLiteral("Для выбранной длительности нужно около %1 ГБ свободного места.") ++ .arg(bytes / 1e9, 0, 'f', 1)); ++ return false; ++ } ++ if ((!disk_buffer && !_device_agent->set_config_uint64(SR_CONF_ZYNQ_RECORD_SAMPLES, samples)) || ++ !_device_agent->set_config_string(SR_CONF_ZYNQ_RECORD_FILE, path.toUtf8().constData())) ++ return false; ++ settings.setValue("Zynq/RecordDirectory", info.absolutePath()); ++ } ++ + if (_device_agent->get_work_mode() == DSO) + { + bool zero; +@@ -884,6 +999,8 @@ namespace pv + + _is_run_as_instant = false; + bool ret = _session->start_capture(false); ++ if (!ret && record_rj2) ++ _device_agent->set_config_string(SR_CONF_ZYNQ_RECORD_FILE, ""); + + return ret; + } +@@ -1017,6 +1134,9 @@ namespace pv + void SamplingBar::reload() + { + QString iconPath = GetIconPath(); ++ bool disk_buffer = false; ++ if (_device_agent->is_network()) ++ _device_agent->get_config_bool(SR_CONF_ZYNQ_DISK_BUFFER, disk_buffer); + + _action_loop->setVisible(false); + +@@ -1029,7 +1149,7 @@ namespace pv + else + { + update_mode_icon(); +- _mode_action->setVisible(true); ++ _mode_action->setVisible(!disk_buffer); + _action_repeat->setVisible(true); + + if (_session->is_loop_mode() && _device_agent->is_stream_mode() == false +@@ -1041,7 +1161,7 @@ namespace pv + _action_loop->setVisible(true); + } + _run_stop_action->setVisible(true); +- _instant_action->setVisible(true); ++ _instant_action->setVisible(!disk_buffer); + } + else if (mode == ANALOG) + { +@@ -1112,6 +1232,10 @@ namespace pv + } + + update_mode_icon(); ++ uint64_t record_samples; ++ if (_device_agent->is_network() && ++ _device_agent->get_config_uint64(SR_CONF_ZYNQ_RECORD_SAMPLES, record_samples)) ++ update_sample_count_selector(); + } + + void SamplingBar::update_device_list() +@@ -1172,11 +1296,15 @@ namespace pv + { + int bEnable = _session->is_working() == false; + int mode = _session->get_device()->get_work_mode(); ++ bool disk_buffer = false; ++ if (_device_agent->is_network()) ++ _device_agent->get_config_bool(SR_CONF_ZYNQ_DISK_BUFFER, disk_buffer); + + _device_type.setEnabled(bEnable); + _mode_button.setEnabled(bEnable); + _configure_button.setEnabled(bEnable); + _device_selector.setEnabled(bEnable); ++ _action_zynq->setEnabled(bEnable); + _action_loop->setVisible(false); + + if (_session->get_device()->is_file()){ +@@ -1237,6 +1365,13 @@ namespace pv + } + + update_mode_icon(); ++ if (disk_buffer) ++ { ++ _sample_count.setEnabled(false); ++ if (_sample_count.count()) ++ _sample_count.setItemText(0, bEnable ? QStringLiteral("До Stop") : ++ QStringLiteral("Запись до Stop")); ++ } + + if (_session->get_device()->is_demo() && bEnable) + { +diff --git a/DSView/pv/toolbars/samplingbar.h b/DSView/pv/toolbars/samplingbar.h +index fde2169..724b535 100644 +--- a/DSView/pv/toolbars/samplingbar.h ++++ b/DSView/pv/toolbars/samplingbar.h +@@ -162,6 +162,7 @@ namespace pv + + QMenu *_mode_menu; + QAction *_action_repeat; ++ QAction *_action_zynq; + QAction *_action_single; + QAction *_action_loop; + +diff --git a/DSView/pv/view/header.cpp b/DSView/pv/view/header.cpp +index 0fab4b6..cd60ecb 100644 +--- a/DSView/pv/view/header.cpp ++++ b/DSView/pv/view/header.cpp +@@ -28,6 +28,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -112,6 +113,25 @@ pv::view::Trace* Header::get_mTrace(int &action, const QPoint &pt) + return NULL; + } + ++static int handle_y(Trace *trace) ++{ ++ return trace->get_y() + trace->get_totalHeight() / 2 + View::SignalMargin; ++} ++ ++Trace* Header::resize_handle_at(const QPoint &point) ++{ ++ if (!_view.can_resize_logic_rows() || point.x() < width() - 30) ++ return nullptr; ++ std::vector traces; ++ _view.get_traces(TIME_VIEW, traces); ++ for (auto *trace : traces) { ++ if (trace->get_type() == SR_CHANNEL_LOGIC && trace->enabled() && ++ qAbs(point.y() - handle_y(trace)) <= 4) ++ return trace; ++ } ++ return nullptr; ++} ++ + void Header::paintEvent(QPaintEvent*) + { + using pv::view::Trace; +@@ -141,6 +161,19 @@ void Header::paintEvent(QPaintEvent*) + t->paint_label(painter, w, dragging ? QPoint(-1, -1) : _mouse_point, fore); + } + ++ if (_view.can_resize_logic_rows()) { ++ auto grip = fore; ++ grip.setAlpha(125); ++ painter.setPen(grip); ++ for (auto *trace : traces) { ++ if (trace->get_type() == SR_CHANNEL_LOGIC && trace->enabled()) { ++ const int y = handle_y(trace); ++ painter.drawLine(w - 25, y, w - 5, y); ++ painter.drawLine(w - 25, y + 2, w - 5, y + 2); ++ } ++ } ++ } ++ + painter.end(); + } + +@@ -175,6 +208,17 @@ void Header::mousePressEvent(QMouseEvent *event) + { + assert(event); + ++ if (event->button() == Qt::LeftButton) { ++ if (auto *trace = resize_handle_at(event->pos())) { ++ _resize_channel = trace->get_index(); ++ _resize_start_y = event->globalY(); ++ _resize_start_height = trace->get_totalHeight(); ++ setCursor(Qt::SizeVerCursor); ++ event->accept(); ++ return; ++ } ++ } ++ + _mouse_is_down = true; + + std::vector traces; +@@ -210,7 +254,7 @@ void Header::mousePressEvent(QMouseEvent *event) + if (~QApplication::keyboardModifiers() & Qt::ControlModifier) + _drag_traces.clear(); + +- _drag_traces.push_back(make_pair(mTrace, mTrace->get_zero_vpos())); ++ _drag_traces.push_back(make_pair(mTrace, mTrace->get_zero_vpos() + _view.vertical_offset())); + mTrace->set_old_v_offset(mTrace->get_v_offset()); + } + +@@ -239,6 +283,14 @@ void Header::mouseReleaseEvent(QMouseEvent *event) + { + assert(event); + ++ if (_resize_channel >= 0 && event->button() == Qt::LeftButton) { ++ _view.resize_logic_row(_resize_channel, _resize_start_height + event->globalY() - _resize_start_y); ++ _resize_channel = -1; ++ unsetCursor(); ++ event->accept(); ++ return; ++ } ++ + _mouse_is_down = false; + + // judge for color / name / trigger / move +@@ -302,6 +354,13 @@ void Header::mouseReleaseEvent(QMouseEvent *event) + void Header::wheelEvent(QWheelEvent *event) + { + assert(event); ++ auto *scroll = _view.verticalScrollBar(); ++ const int vertical_delta = event->angleDelta().y(); ++ if (scroll->maximum() > 0 && vertical_delta) { ++ scroll->setValue(scroll->value() - vertical_delta / 3); ++ event->accept(); ++ return; ++ } + + int x = 0; + int y = 0; +@@ -414,6 +473,19 @@ void Header::mouseMoveEvent(QMouseEvent *event) + { + assert(event); + ++ if (_resize_channel >= 0) { ++ if (event->buttons() & Qt::LeftButton) { ++ _view.resize_logic_row(_resize_channel, _resize_start_height + event->globalY() - _resize_start_y); ++ event->accept(); ++ return; ++ } ++ _resize_channel = -1; ++ } ++ if (_drag_traces.empty() && resize_handle_at(event->pos())) ++ setCursor(Qt::SizeVerCursor); ++ else ++ unsetCursor(); ++ + if (_view.session().is_working() && _view.session().get_device()->get_work_mode() == LOGIC){ + //Disable the hover status of trig button on left pannel. + return; +@@ -470,6 +542,8 @@ void Header::mouseMoveEvent(QMouseEvent *event) + + void Header::leaveEvent(QEvent*) + { ++ if (_resize_channel < 0) ++ unsetCursor(); + _mouse_point = QPoint(-1, -1); + update(); + } +diff --git a/DSView/pv/view/header.h b/DSView/pv/view/header.h +index 51f6164..0191c03 100644 +--- a/DSView/pv/view/header.h ++++ b/DSView/pv/view/header.h +@@ -49,6 +49,7 @@ public: + + private: + pv::view::Trace* get_mTrace(int &action, const QPoint &pt); ++ Trace* resize_handle_at(const QPoint &point); + + private: + void retranslateUi(); +@@ -101,6 +102,9 @@ private: + std::list > _drag_traces; + Trace *_context_trace; + bool _mouse_is_down; ++ int _resize_channel = -1; ++ int _resize_start_y = 0; ++ int _resize_start_height = 0; + }; + + } // namespace view +diff --git a/DSView/pv/view/trace.cpp b/DSView/pv/view/trace.cpp +index 5540899..ad68b39 100644 +--- a/DSView/pv/view/trace.cpp ++++ b/DSView/pv/view/trace.cpp +@@ -127,7 +127,12 @@ void Trace::set_index_list(const std::list &index_list) + + int Trace::get_zero_vpos() + { +- return _v_offset; ++ return get_y(); ++} ++ ++int Trace::get_y() const ++{ ++ return _v_offset - (_view ? _view->vertical_offset() : 0); + } + + void Trace::resize() +@@ -138,7 +143,7 @@ void Trace::set_view(pv::view::View *view) + { + assert(view); + _view = view; +- connect(_view, SIGNAL(resize()), this, SLOT(resize())); ++ connect(_view, SIGNAL(resize()), this, SLOT(resize()), Qt::UniqueConnection); + } + + void Trace::set_viewport(pv::view::Viewport *viewport) +diff --git a/DSView/pv/view/trace.h b/DSView/pv/view/trace.h +index d05c8fb..32e3a0d 100644 +--- a/DSView/pv/view/trace.h ++++ b/DSView/pv/view/trace.h +@@ -239,9 +239,7 @@ public: + /** + * Gets the y-offset of the axis. + */ +- inline int get_y(){ +- return _v_offset; +- } ++ int get_y() const; + + /** + * Determines if a point is in the header rect. +diff --git a/DSView/pv/view/view.cpp b/DSView/pv/view/view.cpp +index c35f8f8..088d6f8 100644 +--- a/DSView/pv/view/view.cpp ++++ b/DSView/pv/view/view.cpp +@@ -578,6 +578,9 @@ void View::set_search_pos(uint64_t search_pos, bool hit) + + void View::normalize_layout() + { ++ if (can_resize_logic_rows()) ++ return; ++ + int v_min = INT_MAX; + std::vector traces; + get_traces(ALL_VIEW, traces); +@@ -635,8 +638,10 @@ void View::update_scroll() + _updating_scroll = false; + + // Set the vertical scrollbar +- verticalScrollBar()->setPageStep(areaSize.height()); +- verticalScrollBar()->setRange(0,0); ++ const int visible_height = _time_viewport->height(); ++ verticalScrollBar()->setPageStep(visible_height); ++ verticalScrollBar()->setRange(0, can_resize_logic_rows() ? ++ max(0, _logic_content_height - visible_height) : 0); + } + + void View::update_scale_offset() +@@ -673,12 +678,37 @@ void View::mode_changed() + _scale = max(min(_scale, _maxscale), _minscale); + } + ++bool View::can_resize_logic_rows() const ++{ ++ return _device_agent->is_network() || ++ (_device_agent->is_file() && _session->has_zynq_disk_view()); ++} ++ ++int View::vertical_offset() const ++{ ++ return verticalScrollBar()->value(); ++} ++ ++void View::resize_logic_row(int channel, int height) ++{ ++ if (!can_resize_logic_rows()) ++ return; ++ const int requested = max(12, min(5 * MaxHeightUnit, height)); ++ auto current = _logic_row_heights.find(channel); ++ if (current != _logic_row_heights.end() && current->second == requested) ++ return; ++ _logic_row_heights[channel] = requested; ++ signals_changed(nullptr); ++} ++ + void View::signals_changed(const Trace* eventTrace) + { + double actualMargin = SignalMargin; + int total_rows = 0; + int label_size = 0; + uint8_t max_height = MaxHeightUnit; ++ const bool resizable_logic = can_resize_logic_rows(); ++ _logic_content_height = 0; + std::vector time_traces; + std::vector fft_traces; + std::vector traces; +@@ -762,7 +792,15 @@ void View::signals_changed(const Trace* eventTrace) + max_height = (v + 1) * MaxHeightUnit; + } + +- if (height < 2*actualMargin) { ++ if (resizable_logic) { ++ // Use the selected multiplier even when the tracks need scrolling. ++ if (_logic_max_height != max_height) { ++ _logic_row_heights.clear(); ++ _logic_max_height = max_height; ++ } ++ _signalHeight = max_height; ++ } ++ else if (height < 2*actualMargin) { + actualMargin /= 2; + _signalHeight = max(1.0, (_time_viewport->height() + - 2 * actualMargin * label_size) * 1.0 / total_rows); +@@ -815,7 +853,13 @@ void View::signals_changed(const Trace* eventTrace) + if (t->rows_size() == 0) + continue; + +- const double traceHeight = _signalHeight*t->rows_size(); ++ int row_height = _signalHeight; ++ if (resizable_logic && t->get_type() == SR_CHANNEL_LOGIC) { ++ const auto custom = _logic_row_heights.find(t->get_index()); ++ if (custom != _logic_row_heights.end()) ++ row_height = custom->second; ++ } ++ const int traceHeight = row_height * t->rows_size(); + t->set_totalHeight((int)traceHeight); + t->set_v_offset(next_v_offset + 0.5 * traceHeight + actualMargin); + next_v_offset += traceHeight + 2 * actualMargin; +@@ -831,6 +875,7 @@ void View::signals_changed(const Trace* eventTrace) + sig->set_scale(sig->get_totalHeight()); + } + } ++ _logic_content_height = next_v_offset; + _time_viewport->clear_measure(); + _session->update_dso_data_scale(); + } +@@ -969,6 +1014,7 @@ void View::v_scroll_value_changed(int value) + { + (void)value; + _header->update(); ++ set_update(_time_viewport, true); + viewport_update(); + } + +@@ -1367,6 +1413,10 @@ void View::splitterMoved(int pos, int index) + + void View::reload() + { ++ _logic_row_heights.clear(); ++ _logic_max_height = 0; ++ _logic_content_height = 0; ++ verticalScrollBar()->setValue(0); + clear(); + + /* +diff --git a/DSView/pv/view/view.h b/DSView/pv/view/view.h +index aa738ec..9ab29bc 100644 +--- a/DSView/pv/view/view.h ++++ b/DSView/pv/view/view.h +@@ -25,6 +25,7 @@ + #define DSVIEW_PV_VIEW_VIEW_H + + #include ++#include + #include + #include + +@@ -192,6 +193,9 @@ public: + inline int get_signalHeight(){ + return _signalHeight; + } ++ bool can_resize_logic_rows() const; ++ void resize_logic_row(int channel, int height); ++ int vertical_offset() const; + + int headerWidth(); + +@@ -459,6 +463,9 @@ private: + int64_t _preOffset; + int _spanY; + int _signalHeight; ++ std::map _logic_row_heights; ++ int _logic_max_height = 0; ++ int _logic_content_height = 0; + bool _updating_scroll; + + // trigger position fix +diff --git a/DSView/pv/view/viewport.cpp b/DSView/pv/view/viewport.cpp +index dea3b91..a9e6550 100644 +--- a/DSView/pv/view/viewport.cpp ++++ b/DSView/pv/view/viewport.cpp +@@ -600,7 +600,18 @@ void Viewport::paintProgress(QPainter &p, QColor fore, QColor back) + + QRect status_rect = QRect(cenPos.x() - radius, cenPos.y() + radius * 0.4, radius * 2, radius * 0.5); + +- if (triggered) { ++ auto *device = _view.session().get_device(); ++ bool disk_buffer = false; ++ if (device->is_network() && ++ device->get_config_bool(SR_CONF_ZYNQ_DISK_BUFFER, disk_buffer) && disk_buffer) { ++ uint64_t tenths = 0; ++ device->get_config_uint64(SR_CONF_ZYNQ_RECORD_TENTHS, tenths); ++ const QString status = triggered ++ ? QStringLiteral("Запись в файл · %1 с · Stop для завершения").arg(tenths / 10.0, 0, 'f', 1) ++ : QStringLiteral("Ожидание триггера"); ++ p.drawText(status_rect, Qt::AlignCenter | Qt::AlignVCenter, status); ++ } ++ else if (triggered) { + p.drawText(status_rect, + Qt::AlignCenter | Qt::AlignVCenter, + L_S(STR_PAGE_DLG, S_ID(IDS_DLG_TRIGGERED), "Triggered! ") + QString::number(captured_progress) +diff --git a/libsigrok4DSL/lib_main.c b/libsigrok4DSL/lib_main.c +index 054cddd..3efd5fe 100644 +--- a/libsigrok4DSL/lib_main.c ++++ b/libsigrok4DSL/lib_main.c +@@ -21,6 +21,7 @@ + + #include "libsigrok-internal.h" + #include "log.h" ++#include + #include + #include + #include +@@ -336,6 +337,54 @@ SR_API int ds_get_device_list(struct ds_device_base_info **out_list, int *out_co + return SR_OK; + } + ++SR_API int ds_add_zynq_device(const char *address, ds_device_handle *preferred) ++{ ++ if (!address || !preferred || ds_is_collecting()) ++ return SR_ERR_ARG; ++ ++ GInetAddress *ip = g_inet_address_new_from_string(address); ++ if (!ip) ++ return SR_ERR_ARG; ++ const gboolean ipv4 = g_inet_address_get_family(ip) == G_SOCKET_FAMILY_IPV4; ++ g_object_unref(ip); ++ if (!ipv4) ++ return SR_ERR_ARG; ++ ++ *preferred = 0; ++ for (enum zynq_transport mode = ZYNQ_PL_STREAM; mode <= ZYNQ_TCP_BUFFER; mode++) { ++ struct sr_dev_inst *candidate = sr_zynq_probe(address, mode); ++ if (!candidate) { ++ if (mode == ZYNQ_PL_BUFFER && *preferred) ++ break; ++ continue; ++ } ++ ++ pthread_mutex_lock(&lib_ctx.mutext); ++ struct sr_dev_inst *existing = NULL; ++ for (const GSList *entry = lib_ctx.device_list; entry; entry = entry->next) { ++ struct sr_dev_inst *device = entry->data; ++ if (device->dev_type == DEV_TYPE_NETWORK && ++ strcmp(device->name, candidate->name) == 0) { ++ existing = device; ++ break; ++ } ++ } ++ ++ if (existing) { ++ sr_dev_inst_free(candidate); ++ candidate = existing; ++ } else { ++ lib_ctx.device_list = g_slist_append(lib_ctx.device_list, candidate); ++ } ++ if (!*preferred) ++ *preferred = candidate->handle; ++ pthread_mutex_unlock(&lib_ctx.mutext); ++ if (mode == ZYNQ_PL_STREAM || mode == ZYNQ_PL_BUFFER) ++ break; ++ } ++ return *preferred ? SR_OK : SR_ERR; ++} ++ + /** + * Active a device. + */ +diff --git a/libsigrok4DSL/libsigrok-internal.h b/libsigrok4DSL/libsigrok-internal.h +index a5534c0..4676313 100644 +--- a/libsigrok4DSL/libsigrok-internal.h ++++ b/libsigrok4DSL/libsigrok-internal.h +@@ -249,6 +249,9 @@ struct ds_trigger { + }; + + ++enum zynq_transport { ZYNQ_PL_STREAM, ZYNQ_PL_BUFFER, ZYNQ_TCP_STREAM, ZYNQ_TCP_BUFFER }; ++SR_PRIV struct sr_dev_inst *sr_zynq_probe(const char *address, enum zynq_transport transport); ++ + /*--- device.c --------------------------------------------------------------*/ + + SR_PRIV struct sr_channel *sr_channel_new(uint16_t index, int type, gboolean enabled, const char *name); +diff --git a/libsigrok4DSL/libsigrok.h b/libsigrok4DSL/libsigrok.h +index 28d8a0c..a55d387 100644 +--- a/libsigrok4DSL/libsigrok.h ++++ b/libsigrok4DSL/libsigrok.h +@@ -174,6 +174,7 @@ enum sr_device_type{ + DEV_TYPE_FILELOG = 2, + DEV_TYPE_USB = 3, + DEV_TYPE_SERIAL = 4, ++ DEV_TYPE_NETWORK = 5, + }; + + /** Data types used by sr_config_info(). */ +@@ -202,6 +203,7 @@ enum sr_datafeed_packet_type { + SR_DF_FRAME_BEGIN, + SR_DF_FRAME_END, + SR_DF_OVERFLOW, ++ SR_DF_ZYNQ_DISK, + }; + + /** Values for sr_datafeed_analog.mq. */ +@@ -323,6 +325,7 @@ enum DSLOGIC_OPERATION_MODE + LO_OP_EXTEST = 3, + /** SDRAM loopback test mode */ + LO_OP_LPTEST = 4, ++ LO_OP_ZYNQ_SNAPSHOT = 100, + }; + + enum DSO_MEASURE_TYPE { +@@ -933,6 +936,14 @@ enum sr_config_option_id + + /** Stream */ + SR_CONF_STREAM = 30052, ++ SR_CONF_ZYNQ_RECORD_FILE = 30990, ++ SR_CONF_ZYNQ_RECORD_SAMPLES = 30991, ++ SR_CONF_ZYNQ_LAST_ERROR = 30992, ++ SR_CONF_ZYNQ_ADVANCED = 30993, ++ SR_CONF_ZYNQ_DISK_BUFFER = 30994, ++ SR_CONF_ZYNQ_LAST_RECORDING = 30995, ++ SR_CONF_ZYNQ_LIVE_RECORDING = 30996, ++ SR_CONF_ZYNQ_RECORD_TENTHS = 30997, + + /** DSO Roll */ + SR_CONF_ROLL = 30053, +@@ -1409,6 +1420,7 @@ SR_API void ds_set_user_data_dir(const char *dir); + * User need call g_free() to release the buffer. If the list is empty, the out_list is null. + */ + SR_API int ds_get_device_list(struct ds_device_base_info** out_list, int *out_count); ++SR_API int ds_add_zynq_device(const char *address, ds_device_handle *preferred); + + /** + * Active a device. +@@ -1436,6 +1448,24 @@ SR_API int ds_have_actived_device(); + */ + SR_API int ds_device_from_file(const char *file_path); + ++struct ds_zynq_record_info { ++ uint64_t samples; ++ unsigned clock, divider, mask, leaf_samples; ++ gboolean indexed; ++}; ++SR_API int ds_zynq_record_info(const char *path, struct ds_zynq_record_info *info); ++SR_API int ds_zynq_record_window(const char *source, const char *target, unsigned first_leaf); ++struct ds_zynq_disk; ++struct ds_zynq_tile { ++ uint8_t first, last, changes; ++}; ++#define DS_ZYNQ_TILE_SAMPLES 262144u ++SR_API struct ds_zynq_disk *ds_zynq_disk_open(const char *path, struct ds_zynq_record_info *info); ++SR_API void ds_zynq_disk_close(struct ds_zynq_disk *disk); ++SR_API const struct ds_zynq_tile *ds_zynq_disk_tiles(const struct ds_zynq_disk *disk); ++SR_API int ds_zynq_disk_leaf(struct ds_zynq_disk *disk, unsigned leaf, unsigned channel, ++ void *data, unsigned capacity); ++ + /** + * Remove one device from the list, and destory it. + * User need to call ds_get_device_list() to get the new list. +diff --git a/libsigrok4DSL/session_driver.c b/libsigrok4DSL/session_driver.c +index 3b298d4..90d0ff1 100644 +--- a/libsigrok4DSL/session_driver.c ++++ b/libsigrok4DSL/session_driver.c +@@ -1356,6 +1356,22 @@ static int dev_status_get(const struct sr_dev_inst *sdi, struct sr_status *statu + } + } + ++static int receive_disk_recording(int fd, int revents, const struct sr_dev_inst *sdi) ++{ ++ (void)fd; ++ if (revents != -1) { ++ const struct sr_datafeed_packet disk = { ++ .type = SR_DF_ZYNQ_DISK, .status = SR_PKT_OK, .payload = sdi->path ++ }; ++ ds_data_forward(sdi, &disk); ++ } ++ const struct sr_datafeed_packet end = {.type = SR_DF_END, .status = SR_PKT_OK}; ++ ds_data_forward(sdi, &end); ++ sr_session_source_remove(-1); ++ close_archive(sdi->priv); ++ return FALSE; ++} ++ + static int dev_acquisition_start(struct sr_dev_inst *sdi, void *cb_data) + { + (void)cb_data; +@@ -1399,6 +1415,14 @@ static int dev_acquisition_start(struct sr_dev_inst *sdi, void *cb_data) + return SR_ERR; + } + ++ if (sdi->mode == LOGIC && unzLocateFile(vdev->archive, "zynq_recording.json", 0) == UNZ_OK) { ++ std_session_send_df_header(sdi, LOG_PREFIX); ++ int result = sr_session_source_add(-1, 0, 0, receive_disk_recording, sdi); ++ if (result != SR_OK) ++ close_archive(vdev); ++ return result; ++ } ++ + if (vdev->version == 1) + { + if (unzLocateFile(vdev->archive, "data", 0) != UNZ_OK) diff --git a/tools/dsview/zynq-ethernet/package_arm.py b/tools/dsview/zynq-ethernet/package_arm.py new file mode 100644 index 0000000..95ae034 --- /dev/null +++ b/tools/dsview/zynq-ethernet/package_arm.py @@ -0,0 +1,72 @@ +"""Stage the ARM DSView runtime and the canonical templates decoder package.""" +import argparse +import importlib.util +from pathlib import Path +import re +import shutil +import subprocess + + +def copy(source, target): + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + + +def stage(source, binary, protocol, sysroot, objdump, templates, output): + target_lib = output / "lib" + target_share = output / "share/DSView" + machine = "arm-linux-gnueabihf" + lib_dirs = (sysroot / "usr/lib" / machine, sysroot / "lib" / machine) + copy(binary, output / "bin/DSView") + copy(protocol, target_lib / "libsetprotocol.so") + shutil.copytree(sysroot / "usr/lib/python3.11", target_lib / "python3.11", dirs_exist_ok=True, + ignore=shutil.ignore_patterns("__pycache__", "test", "tests", "tkinter", "idlelib", "ensurepip")) + plugins = lib_dirs[0] / "qt5/plugins" + for plugin in ("platforms/libqlinuxfb.so", "imageformats/libqjpeg.so", "imageformats/libqsvg.so", + "iconengines/libqsvgicon.so"): + copy(plugins / plugin, output / "plugins" / plugin) + for name in ("res", "demo"): + shutil.copytree(source / "DSView" / name, target_share / name, dirs_exist_ok=True) + shutil.copytree(source / "lang", target_share / "lang", dirs_exist_ok=True) + shutil.copytree(source / "libsigrokdecode4DSL/decoders", target_share / "decoders", dirs_exist_ok=True, + ignore=shutil.ignore_patterns("__pycache__")) + recipe = templates / "tools/dsview/build.py" + spec = importlib.util.spec_from_file_location("dsview_recipe", recipe) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + module.package_decoders(target_share) + shutil.copytree(sysroot / "usr/share/fonts/truetype/dejavu", output / "fonts", dirs_exist_ok=True) + (output / "fonts.conf").write_text( + 'fonts' + '/run/scope-logic/fonts\n', encoding="ascii") + copy(source / "COPYING", output / "COPYING-DSView") + + # Use an isolated glibc loader because the factory Linux has its own libc. + copy(lib_dirs[1] / "ld-linux-armhf.so.3", target_lib / "ld-linux-armhf.so.3") + candidates = [output / "bin/DSView", target_lib / "ld-linux-armhf.so.3"] + candidates.extend(output.rglob("*.so")) + known = set() + while candidates: + candidate = candidates.pop() + if candidate in known: + continue + known.add(candidate) + header = subprocess.check_output([objdump, "-p", candidate], text=True) + for name in re.findall(r"^\s*NEEDED\s+(\S+)", header, re.MULTILINE): + destination = target_lib / name + if destination.is_file(): + continue + dependency = next((path / name for path in lib_dirs if (path / name).is_file()), None) + if dependency is None: + raise FileNotFoundError(f"{candidate.name} requires {name}") + copy(dependency, destination) + candidates.append(destination) + print(f"ARM runtime: {len(known)} ELF files, " + f"{sum(1 for _ in (target_share / 'decoders').glob('*/pd.py'))} protocol decoders") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + for option in ("source", "binary", "protocol", "sysroot", "objdump", "templates", "output"): + parser.add_argument("--" + option, type=Path, required=True) + stage(**{name: path.resolve() for name, path in vars(parser.parse_args()).items()}) diff --git a/tools/dsview/zynq-ethernet/zynq.c b/tools/dsview/zynq-ethernet/zynq.c new file mode 100644 index 0000000..e43a1a1 --- /dev/null +++ b/tools/dsview/zynq-ethernet/zynq.c @@ -0,0 +1,1197 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#include +#include +#include + +#include "libsigrok.h" +#include "libsigrok-internal.h" +#include "../../log.h" +#include "zynq_la.h" +#include "zynq_la_stream.h" +#include "zynq_la_pl.h" +#include "zynq_record.h" + +#define CHANNELS 8 +#define SAMPLE_GROUP 64 +#define STREAM_DEPTH (64u * 1024u * 1024u) +#define RECORD_SECONDS 3600 +#define IO_TIMEOUT (5 * G_TIME_SPAN_SECOND) +#define SNAPSHOT_TIMEOUT (65 * G_TIME_SPAN_SECOND) +#define STREAM_MIN_DIVIDER 10 +#define PL_HEARTBEAT (250 * G_TIME_SPAN_MILLISECOND) +#define PL_REORDER_BLOCKS 4096u + +enum receive_state { REPLY, FRAME_HEADER, SAMPLES }; + +struct pl_sample_block { + gboolean ready; + uint8_t data[ZLP_BLOCK]; +}; + +struct zynq_device { + gboolean stream; + gboolean pl; + gboolean disk_buffer; + gboolean loop; + gboolean source_active; + uint8_t max_height; + uint32_t clock; + uint32_t divider; + uint64_t limit; + GSocketConnection *connection; + GSocket *datagrams; + uint32_t nonce; + gint64 keepalive; + gboolean pl_started; + size_t pl_count; + unsigned pl_pending; + uint32_t pl_magic; + uint32_t pl_stream_magic, pl_snapshot_magic; + gboolean pl_packed, pl_rle_available; + uint32_t stages[TriggerStages][4]; + unsigned stage_count, stage_sent, pretrigger; + gboolean serial_trigger, external_trigger; + uint8_t pl_expand[16]; + struct pl_sample_block *pl_reorder; + char *record_file, *active_record, *last_recording; + char *last_error; + uint64_t record_samples, delivered; + struct zynq_recorder *recorder; + + enum receive_state state; + gint64 deadline; + size_t received; + size_t wanted; + uint32_t sequence; + gint progress; + gint triggered; + gint recorded_tenths; + unsigned enabled_mask; + unsigned level_mask, level_value; + unsigned edge_mask, edge_value, any_edge_mask; + uint8_t previous; + gboolean previous_valid; + size_t pending_count; + uint8_t pending[SAMPLE_GROUP]; + uint8_t input[ZLS_BLOCK]; + uint8_t output[ZLS_BLOCK]; +}; + +static const uint32_t dividers[] = { + 50000, 20000, 10000, 5000, 2000, 1000, 500, 200, 100, 50, 40, 25, + 20, 14, 10, 7, 5, 4, 2, 1 +}; +static const char *const max_heights[] = {"1X", "2X", "3X", "4X", "5X"}; + +static uint64_t max_samples(const struct zynq_device *device) +{ + return device->stream ? STREAM_DEPTH : ZLA_MAX_SAMPLES; +} + +static uint32_t word(const uint8_t *data, size_t index) +{ + uint32_t value; + memcpy(&value, data + index * sizeof(value), sizeof(value)); + return GUINT32_FROM_LE(value); +} + +static GSocket *connect_udp(const char *address) +{ + GInetAddress *ip = g_inet_address_new_from_string(address); + GSocketAddress *peer = g_inet_socket_address_new(ip, ZLP_PORT); + g_object_unref(ip); + GSocket *socket = g_socket_new(G_SOCKET_FAMILY_IPV4, G_SOCKET_TYPE_DATAGRAM, + G_SOCKET_PROTOCOL_UDP, NULL); + if (socket && !g_socket_connect(socket, peer, NULL, NULL)) + g_clear_object(&socket); + g_object_unref(peer); + + if (socket) { + // Buffer bursts while the host schedules the receiver at 100 MB/s. + g_socket_set_option(socket, SOL_SOCKET, SO_RCVBUF, 16 * 1024 * 1024, NULL); + g_socket_set_blocking(socket, FALSE); + } + return socket; +} + +static gboolean send_udp(GSocket *socket, uint32_t magic, uint32_t op, uint32_t nonce, uint32_t value) +{ + uint32_t request[] = {GUINT32_TO_LE(magic), GUINT32_TO_LE(op), + GUINT32_TO_LE(nonce), GUINT32_TO_LE(value)}; + return g_socket_send(socket, (const char *)request, sizeof(request), NULL, NULL) == + sizeof(request); +} + +static uint32_t probe_pl(const char *address, uint32_t magic) +{ + GSocket *socket = connect_udp(address); + if (!socket) + return 0; + + uint32_t nonce = g_random_int() | 1u; + uint32_t clock = 0; + for (unsigned attempt = 0; attempt < 2 && !clock; attempt++) { + if (!send_udp(socket, magic, ZLP_PROBE, nonce, 0) || + !g_socket_condition_timed_wait(socket, G_IO_IN, + (magic == ZLR_MAGIC ? 100 : 450) * G_TIME_SPAN_MILLISECOND, NULL, NULL)) + continue; + + uint8_t response[16]; + gssize size = g_socket_receive(socket, (char *)response, sizeof(response), NULL, NULL); + if (size != sizeof(response) || word(response, 0) != magic || + word(response, 1) != ZLP_INFO || word(response, 2) != nonce) + continue; + uint32_t reported = word(response, 3); + if ((magic != ZLP_MAGIC && reported == ZLP2_CLOCK_HZ) || + (magic == ZLP_MAGIC && (reported == ZLP_CLOCK_HZ || reported == ZLP_LEGACY_CLOCK_HZ))) + clock = reported; + } + g_object_unref(socket); + return clock; +} + +static unsigned channel_mask(const struct sr_dev_inst *sdi) +{ + unsigned mask = 0; + for (const GSList *entry = sdi->channels; entry; entry = entry->next) { + const struct sr_channel *ch = entry->data; + if (ch->enabled) + mask |= 1u << ch->index; + } + return mask; +} + +static gboolean pack_channels(unsigned mask) +{ + unsigned count = 0; + for (unsigned channel = 0; channel < CHANNELS; channel++) + count += (mask >> channel) & 1u; + return count > 0 && count <= 4; +} + +static unsigned min_divider(const struct sr_dev_inst *sdi) +{ + const struct zynq_device *device = sdi->priv; + if (device->pl_magic == ZLP2_MAGIC || device->pl_magic == ZLR_MAGIC) + return device->pl_rle_available || pack_channels(channel_mask(sdi)) ? 1 : 2; + if (device->pl) + return device->clock == ZLP_LEGACY_CLOCK_HZ ? ZLP_LEGACY_MIN_DIVIDER : ZLP_MIN_DIVIDER; + return device->stream ? STREAM_MIN_DIVIDER : 1; +} + +static GSocketConnection *connect_to(const char *address, unsigned port) +{ + GSocketClient *client = g_socket_client_new(); + g_socket_client_set_enable_proxy(client, FALSE); + g_socket_client_set_timeout(client, 2); + GSocketConnection *connection = + g_socket_client_connect_to_host(client, address, port, NULL, NULL); + g_object_unref(client); + return connection; +} + +static gboolean send_request(GSocketConnection *connection, uint32_t *request, size_t count) +{ + for (size_t i = 0; i < count; i++) + request[i] = GUINT32_TO_LE(request[i]); + + return g_output_stream_write_all( + g_io_stream_get_output_stream(G_IO_STREAM(connection)), + request, count * sizeof(*request), NULL, NULL, NULL); +} + +static uint32_t probe_clock(const char *address, gboolean stream) +{ + GSocketConnection *connection = connect_to(address, stream ? ZLS_PORT : ZLA_PORT); + if (!connection) + return 0; + + /* An invalid divider/count returns metadata without arming either engine. */ + uint32_t request[11] = {stream ? ZLS_MAGIC : ZLA_REQUEST, 1}; + uint8_t reply[32]; + gsize bytes = 0; + gboolean ok = send_request(connection, request, stream ? 4 : 11) && + g_input_stream_read_all(g_io_stream_get_input_stream(G_IO_STREAM(connection)), + reply, sizeof(reply), &bytes, NULL, NULL) && bytes == sizeof(reply) && + word(reply, 0) == (stream ? ZLS_MAGIC : ZLA_MAGIC) && + word(reply, 1) == 1 && word(reply, 2) == 1 && word(reply, 6) == CHANNELS && + word(reply, 5) == (stream ? ZLS_BLOCK : 0) && word(reply, 7) == (stream ? 0 : 1); + g_object_unref(connection); + return ok ? word(reply, 3) : 0; +} + +static int config_get(int key, GVariant **data, const struct sr_dev_inst *sdi, + const struct sr_channel *ch, const struct sr_channel_group *cg) +{ + (void)ch; + (void)cg; + const struct zynq_device *device = sdi->priv; + switch (key) { + case SR_CONF_DEVICE_MODE: + *data = g_variant_new_int16(LOGIC); + break; + case SR_CONF_OPERATION_MODE: + *data = g_variant_new_int16(device->disk_buffer ? LO_OP_BUFFER : + device->stream ? LO_OP_STREAM : device->pl_stream_magic ? LO_OP_ZYNQ_SNAPSHOT : LO_OP_BUFFER); + break; + case SR_CONF_MAX_HEIGHT: + *data = g_variant_new_string(max_heights[device->max_height]); + break; + case SR_CONF_MAX_HEIGHT_VALUE: + *data = g_variant_new_byte(device->max_height); + break; + case SR_CONF_SAMPLERATE: + *data = g_variant_new_uint64(device->clock / MAX(device->divider, min_divider(sdi))); + break; + case SR_CONF_LIMIT_SAMPLES: + *data = g_variant_new_uint64(device->limit); + break; + case SR_CONF_HW_DEPTH: + *data = g_variant_new_uint64(max_samples(device)); + break; + case SR_CONF_STREAM: + *data = g_variant_new_boolean(device->stream && !device->disk_buffer); + break; + case SR_CONF_ZYNQ_RECORD_FILE: + if (!device->pl || !device->stream) + return SR_ERR_NA; + *data = g_variant_new_string(device->record_file ? device->record_file : ""); + break; + case SR_CONF_ZYNQ_RECORD_SAMPLES: + if (!device->pl || !device->stream) + return SR_ERR_NA; + *data = g_variant_new_uint64(device->record_samples); + break; + case SR_CONF_ZYNQ_LAST_ERROR: + *data = g_variant_new_string(device->last_error ? device->last_error : ""); + break; + case SR_CONF_ZYNQ_ADVANCED: + *data = g_variant_new_boolean(device->pl_magic == ZLT_MAGIC); + break; + case SR_CONF_ZYNQ_DISK_BUFFER: + *data = g_variant_new_boolean(device->disk_buffer); + break; + case SR_CONF_ZYNQ_RECORD_TENTHS: + *data = g_variant_new_uint64(g_atomic_int_get(&device->recorded_tenths)); + break; + case SR_CONF_ZYNQ_LIVE_RECORDING: + *data = g_variant_new_boolean(device->recorder && !device->disk_buffer); + break; + case SR_CONF_ZYNQ_LAST_RECORDING: + *data = g_variant_new_string(device->last_recording ? device->last_recording : ""); + break; + case SR_CONF_TOTAL_CH_NUM: + case SR_CONF_VLD_CH_NUM: + *data = g_variant_new_int16(CHANNELS); + break; + case SR_CONF_RLE_SUPPORT: + case SR_CONF_TEST: + case SR_CONF_WAIT_UPLOAD: + *data = g_variant_new_boolean(FALSE); + break; + default: + return SR_ERR_NA; + } + return SR_OK; +} + +static int config_set(int key, GVariant *data, struct sr_dev_inst *sdi, + struct sr_channel *ch, struct sr_channel_group *cg) +{ + (void)ch; + (void)cg; + struct zynq_device *device = sdi->priv; + switch (key) { + case SR_CONF_MAX_HEIGHT: + for (size_t i = 0; i < ARRAY_SIZE(max_heights); i++) { + if (!strcmp(g_variant_get_string(data, NULL), max_heights[i])) { + device->max_height = i; + return SR_OK; + } + } + return SR_ERR_ARG; + case SR_CONF_OPERATION_MODE: { + const int mode = g_variant_get_int16(data); + if (device->source_active || !device->pl_stream_magic || + (mode != LO_OP_BUFFER && mode != LO_OP_STREAM && + !(mode == LO_OP_ZYNQ_SNAPSHOT && device->pl_snapshot_magic))) + return SR_ERR_ARG; + device->stream = mode != LO_OP_ZYNQ_SNAPSHOT; + device->disk_buffer = mode == LO_OP_BUFFER; + device->pl_magic = device->stream ? device->pl_stream_magic : device->pl_snapshot_magic; + device->divider = MAX(device->divider, min_divider(sdi)); + device->limit = MIN(device->limit, max_samples(device)); + return SR_OK; + } + case SR_CONF_SAMPLERATE: { + const uint64_t rate = g_variant_get_uint64(data); + for (size_t i = 0; i < ARRAY_SIZE(dividers); i++) { + if (dividers[i] < min_divider(sdi)) + break; + if (device->clock / dividers[i] == rate) { + device->divider = dividers[i]; + return SR_OK; + } + } + return SR_ERR_ARG; + } + case SR_CONF_LIMIT_SAMPLES: + device->limit = CLAMP(g_variant_get_uint64(data), SAMPLE_GROUP, max_samples(device)); + return SR_OK; + case SR_CONF_LOOP_MODE: + device->loop = device->stream && !device->disk_buffer && g_variant_get_boolean(data); + return SR_OK; + case SR_CONF_ZYNQ_LAST_RECORDING: + if (device->source_active || *g_variant_get_string(data, NULL)) + return SR_ERR_ARG; + g_clear_pointer(&device->last_recording, g_free); + return SR_OK; + case SR_CONF_ZYNQ_RECORD_FILE: + if (!device->pl || !device->stream || device->source_active) + return SR_ERR_NA; + g_free(device->record_file); + device->record_file = g_variant_dup_string(data, NULL); + return SR_OK; + case SR_CONF_ZYNQ_RECORD_SAMPLES: + if (!device->pl || !device->stream || device->source_active) + return SR_ERR_NA; + device->record_samples = CLAMP(g_variant_get_uint64(data), SAMPLE_GROUP, + (uint64_t)device->clock * RECORD_SECONDS / MAX(device->divider, min_divider(sdi))); + return SR_OK; + case SR_CONF_DEVICE_MODE: + return g_variant_get_int16(data) == LOGIC ? SR_OK : SR_ERR_ARG; + case SR_CONF_INSTANT: + case SR_CONF_RLE: + return SR_OK; + default: + return SR_ERR_NA; + } +} + +static int config_list(int key, GVariant **data, const struct sr_dev_inst *sdi, + const struct sr_channel_group *cg) +{ + (void)cg; + const struct zynq_device *device = sdi->priv; + static const int32_t options[] = {SR_CONF_OPERATION_MODE, SR_CONF_MAX_HEIGHT}; + static const struct sr_list_item modes[] = { + {LO_OP_BUFFER, "Buffer Mode"}, + {LO_OP_STREAM, "Stream Mode"}, + {LO_OP_ZYNQ_SNAPSHOT, "FPGA Snapshot (Advanced)"}, + {-1, NULL} + }; + static const struct sr_list_item basic_modes[] = { + {LO_OP_BUFFER, "Buffer Mode"}, + {LO_OP_STREAM, "Stream Mode"}, + {-1, NULL} + }; + if (key == SR_CONF_DEVICE_OPTIONS || key == SR_CONF_DEVICE_SESSIONS) { + const size_t first = device->pl_stream_magic ? 0 : 1; + *data = g_variant_new_fixed_array(G_VARIANT_TYPE_INT32, options + first, + ARRAY_SIZE(options) - first, sizeof(options[0])); + return SR_OK; + } + if (key == SR_CONF_MAX_HEIGHT) { + *data = g_variant_new_strv(max_heights, ARRAY_SIZE(max_heights)); + return SR_OK; + } + if (device->pl_stream_magic && key == SR_CONF_OPERATION_MODE) { + *data = g_variant_new_uint64((uintptr_t)(device->pl_snapshot_magic ? modes : basic_modes)); + return SR_OK; + } + if (key != SR_CONF_SAMPLERATE) + return SR_ERR_NA; + + uint64_t rates[ARRAY_SIZE(dividers)]; + size_t count = 0; + for (size_t i = 0; i < ARRAY_SIZE(dividers); i++) { + if (dividers[i] < min_divider(sdi)) + break; + rates[count++] = device->clock / dividers[i]; + } + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&builder, "{sv}", "samplerates", + g_variant_new_fixed_array(G_VARIANT_TYPE_UINT64, rates, count, sizeof(rates[0]))); + *data = g_variant_builder_end(&builder); + return SR_OK; +} + +static void packet(const struct sr_dev_inst *sdi, int type, int status, const void *payload) +{ + struct sr_datafeed_packet feed = {.type = type, .status = status, .payload = payload}; + ds_data_forward(sdi, &feed); +} + +static gboolean deliver(const struct sr_dev_inst *sdi, size_t count); +static size_t pack_group(const uint8_t *samples, uint8_t *output, unsigned mask); + +static void set_error(struct zynq_device *device, const char *reason) +{ + g_free(device->last_error); + device->last_error = g_strdup(reason); +} + +static void finish(const struct sr_dev_inst *sdi, int status) +{ + struct zynq_device *device = sdi->priv; + if (!device->source_active) + return; + + device->source_active = FALSE; + sr_session_source_remove(-1); + g_clear_object(&device->connection); + if (device->datagrams) { + send_udp(device->datagrams, device->pl_magic, ZLP_STOP, device->nonce, 0); + g_clear_object(&device->datagrams); + } + g_clear_pointer(&device->pl_reorder, g_free); + if (device->recorder && device->pending_count) { + memset(device->pending + device->pending_count, 0, SAMPLE_GROUP - device->pending_count); + size_t bytes = pack_group(device->pending, device->output, device->enabled_mask); + zynq_record_write(device->recorder, device->output, bytes, device->pending_count); + device->pending_count = 0; + } + gboolean published = FALSE; + if (!zynq_record_close(device->recorder, &published)) { + set_error(device, "Ошибка записи RJ2. Проверьте свободное место и скорость диска."); + status = SR_PKT_DATA_ERROR; + } + device->recorder = NULL; + if (published) + device->last_recording = g_steal_pointer(&device->active_record); + g_clear_pointer(&device->active_record, g_free); + packet(sdi, SR_DF_END, status, NULL); +} + +static int stop(const struct sr_dev_inst *sdi, void *cb_data) +{ + (void)cb_data; + struct zynq_device *device = sdi->priv; + if (device->source_active && device->recorder && device->pl_count) { + deliver(sdi, device->pl_count); + device->pl_count = 0; + } + finish(sdi, SR_PKT_OK); + return SR_OK; +} + +static int open_device(struct sr_dev_inst *sdi) +{ + sdi->status = SR_ST_ACTIVE; + return SR_OK; +} + +static int close_device(struct sr_dev_inst *sdi) +{ + stop(sdi, NULL); + struct zynq_device *device = sdi->priv; + g_clear_pointer(&device->record_file, g_free); + g_clear_pointer(&device->active_record, g_free); + g_clear_pointer(&device->last_recording, g_free); + g_clear_pointer(&device->last_error, g_free); + sdi->status = SR_ST_INACTIVE; + return SR_OK; +} + +static int destroy_device(struct sr_dev_inst *sdi) +{ + close_device(sdi); + sr_dev_inst_free(sdi); + return SR_OK; +} + +static int get_status(const struct sr_dev_inst *sdi, struct sr_status *status, gboolean progress) +{ + (void)progress; + const struct zynq_device *device = sdi->priv; + const uint32_t captured = g_atomic_int_get(&device->progress); + const gboolean triggered = g_atomic_int_get(&device->triggered); + const uint32_t count = triggered ? device->limit - captured : captured; + memset(status, 0, sizeof(*status)); + status->trig_hit = triggered; + status->captured_cnt0 = count; + status->captured_cnt1 = count >> 8; + status->captured_cnt2 = count >> 16; + status->captured_cnt3 = count >> 24; + return SR_OK; +} + +static gboolean configure_trigger(struct zynq_device *device) +{ + device->level_mask = device->level_value = 0; + device->edge_mask = device->edge_value = device->any_edge_mask = 0; + + if (device->pl_magic == ZLT_MAGIC) + return TRUE; /* Hardware stages are prepared after the simple masks. */ + if (!trigger || !trigger->trigger_en) + return TRUE; + if (trigger->trigger_mode != SIMPLE_TRIGGER) + return FALSE; + + for (unsigned channel = 0; channel < CHANNELS; channel++) { + const unsigned bit = 1u << channel; + if (!(device->enabled_mask & bit)) + continue; + + switch (trigger->trigger0[TriggerStages][channel]) { + case '0': + device->level_mask |= bit; + break; + case '1': + device->level_mask |= bit; + device->level_value |= bit; + break; + case 'F': + device->edge_mask |= bit; + break; + case 'R': + device->edge_mask |= bit; + device->edge_value |= bit; + break; + case 'C': + device->any_edge_mask |= bit; + break; + } + } + return device->stream || device->pl || !device->any_edge_mask; +} + +static gboolean trigger_pattern(const char *pattern, uint32_t *result) +{ + *result = 0; + for (unsigned i = 0; i < MaxTriggerProbes; i++) { + const char flag = pattern[i]; + if (flag == 'X' || flag == ' ') + continue; + if (i >= CHANNELS) + return FALSE; + const unsigned bit = 1u << i; + if (flag == '0' || flag == '1' || flag == 'R' || flag == 'F') + *result |= bit; + if (flag == '1' || flag == 'R') + *result |= bit << 8; + if (flag == 'R' || flag == 'F' || flag == 'C') + *result |= bit << 16; + } + return TRUE; +} + +static gboolean prepare_hardware_trigger(struct zynq_device *device) +{ + memset(device->stages, 0, sizeof(device->stages)); + device->stage_sent = 0; + device->stage_count = 1; + device->pretrigger = 0; + device->serial_trigger = FALSE; + device->external_trigger = trigger && trigger->trigger_en; + if (!device->external_trigger) + return TRUE; + device->pretrigger = MIN(device->limit - 1, device->limit * trigger->trigger_pos / 100); + if (trigger->trigger_mode == SIMPLE_TRIGGER) { + device->stages[0][0] = 1u << 4; + return trigger_pattern(trigger->trigger0[TriggerStages], &device->stages[0][1]); + } + if (trigger->trigger_mode == ADV_TRIGGER) { + device->stage_count = MIN((unsigned)trigger->trigger_stages + 1, TriggerStages); + for (unsigned i = 0; i < device->stage_count; i++) { + uint32_t *stage = device->stages[i]; + stage[0] = i | (trigger->trigger_logic[i] & 3u) << 4 | + (trigger->trigger0_inv[i] & 1u) << 6 | (trigger->trigger1_inv[i] & 1u) << 7; + if (!trigger_pattern(trigger->trigger0[i], &stage[1]) || + !trigger_pattern(trigger->trigger1[i], &stage[2])) + return FALSE; + stage[3] = MAX(trigger->trigger0_count[i], 1u) - 1; + } + return TRUE; + } + if (trigger->trigger_mode != SERIAL_TRIGGER) + return FALSE; + device->serial_trigger = TRUE; + device->stage_count = 4; + for (unsigned i = 0; i < device->stage_count; i++) + device->stages[i][0] = i; + if (!trigger_pattern(trigger->trigger0[0], &device->stages[0][1]) || + !trigger_pattern(trigger->trigger1[0], &device->stages[0][2]) || + !trigger_pattern(trigger->trigger0[1], &device->stages[1][1]) || + !device->stages[1][1] || trigger->trigger0_count[STriggerDataStage] > 15) + return FALSE; + unsigned pin = 0; + while (pin < CHANNELS && trigger->trigger0[2][pin] != '0') + pin++; + if (pin == CHANNELS) + return FALSE; + device->stages[2][3] = pin; + const unsigned bits = trigger->trigger0_count[STriggerDataStage]; + device->stages[3][3] = bits; + for (unsigned i = 0; i <= bits; i++) { + const char value = trigger->trigger0[STriggerDataStage][i]; + if (value == '0' || value == '1') + device->stages[3][2] |= 1u << i; + if (value == '1') + device->stages[3][1] |= 1u << i; + } + return TRUE; +} + +static size_t trigger_offset(const struct sr_dev_inst *sdi, const uint8_t *samples, size_t count) +{ + struct zynq_device *device = sdi->priv; + if (g_atomic_int_get(&device->triggered)) + return 0; + + const unsigned edges = device->edge_mask | device->any_edge_mask; + for (size_t i = 0; i < count; i++) { + const unsigned sample = samples[i]; + gboolean matches = !device->stream || device->pl_magic == ZLR_MAGIC || + (((sample & device->level_mask) == device->level_value) && + (!edges || (device->previous_valid && + ((sample ^ device->previous) & edges) == edges && + (sample & device->edge_mask) == device->edge_value))); + device->previous = sample; + device->previous_valid = TRUE; + if (matches) { + const struct ds_trigger_pos position = { + .status = device->pl_magic == ZLT_MAGIC ? device->external_trigger : + (device->level_mask | edges) != 0, + .real_pos = device->pl_magic == ZLT_MAGIC ? device->pretrigger : 0 + }; + g_atomic_int_set(&device->triggered, 1); + packet(sdi, SR_DF_TRIGGER, SR_PKT_OK, &position); + return i; + } + } + return count; +} + +/* Turn eight samples (one byte each) into eight channel bytes. */ +static uint64_t transpose(const uint8_t *data) +{ + uint64_t matrix, swap; + memcpy(&matrix, data, sizeof(matrix)); + matrix = GUINT64_FROM_LE(matrix); + swap = (matrix ^ (matrix >> 7)) & UINT64_C(0x00aa00aa00aa00aa); + matrix ^= swap ^ (swap << 7); + swap = (matrix ^ (matrix >> 14)) & UINT64_C(0x0000cccc0000cccc); + matrix ^= swap ^ (swap << 14); + swap = (matrix ^ (matrix >> 28)) & UINT64_C(0x00000000f0f0f0f0); + return matrix ^ swap ^ (swap << 28); +} + +static size_t pack_group(const uint8_t *samples, uint8_t *output, unsigned mask) +{ + uint64_t planes[8]; + for (size_t i = 0; i < ARRAY_SIZE(planes); i++) + planes[i] = transpose(samples + i * 8); + + size_t bytes = 0; + for (unsigned channel = 0; channel < CHANNELS; channel++) { + if (mask & (1u << channel)) { + for (size_t i = 0; i < ARRAY_SIZE(planes); i++) + output[bytes++] = planes[i] >> (channel * 8); + } + } + return bytes; +} + +static gboolean deliver(const struct sr_dev_inst *sdi, size_t count) +{ + struct zynq_device *device = sdi->priv; + const size_t skip = trigger_offset(sdi, device->input, count); + const uint8_t *samples = device->input + skip; + const uint64_t progress = device->delivered; + const uint64_t limit = device->recorder ? device->record_samples : device->limit; + const gboolean bounded = device->recorder ? !device->disk_buffer : !device->loop; + size_t size = count - skip; + if (bounded) + size = MIN(size, limit - progress); + const uint64_t next = progress + size; + const gboolean done = bounded && next == limit; + const size_t available = size + device->pending_count; + size_t bytes = 0; + + if (device->pending_count) { + size_t take = MIN(size, SAMPLE_GROUP - device->pending_count); + memcpy(device->pending + device->pending_count, samples, take); + device->pending_count += take; + samples += take; + size -= take; + if (device->pending_count == SAMPLE_GROUP) { + bytes += pack_group(device->pending, device->output, device->enabled_mask); + device->pending_count = 0; + } + } + while (size >= SAMPLE_GROUP) { + bytes += pack_group(samples, device->output + bytes, device->enabled_mask); + samples += SAMPLE_GROUP; + size -= SAMPLE_GROUP; + } + if (size) { + memcpy(device->pending, samples, size); + device->pending_count = size; + } + + /* DSView's last packet is clipped to the requested sample count. */ + if (done && device->pending_count) { + memset(device->pending + device->pending_count, 0, SAMPLE_GROUP - device->pending_count); + bytes += pack_group(device->pending, device->output + bytes, device->enabled_mask); + device->pending_count = 0; + } + if (bytes) { + const size_t valid_samples = available - device->pending_count; + if (!zynq_record_write(device->recorder, device->output, bytes, valid_samples)) + return TRUE; + if (!device->disk_buffer) { + const struct sr_datafeed_logic logic = { + .format = LA_CROSS_DATA, .length = bytes, .unitsize = 1, .data = device->output + }; + packet(sdi, SR_DF_LOGIC, SR_PKT_OK, &logic); + } + } + device->delivered = next; + if (device->disk_buffer && device->recorder) + g_atomic_int_set(&device->recorded_tenths, + MIN(next * 10 / (device->clock / device->divider), G_MAXINT)); + uint64_t preview_progress = MIN(next, device->limit); + if (device->recorder) + preview_progress = bounded ? (double)next / limit * device->limit : 0; + g_atomic_int_set(&device->progress, preview_progress); + return done; +} + +static void prepare_receive(struct zynq_device *device, enum receive_state state, size_t size) +{ + device->state = state; + device->received = 0; + device->wanted = size; +} + +static int receive_failure(const struct sr_dev_inst *sdi, const char *reason) +{ + sr_err("%s: %s", sdi->name, reason); + set_error(sdi->priv, reason); + finish(sdi, SR_PKT_DATA_ERROR); + return FALSE; +} + +static gboolean append_pl_block(const struct sr_dev_inst *sdi, const uint8_t *data) +{ + struct zynq_device *device = sdi->priv; + if (device->pl_magic == ZLR_MAGIC) { + for (size_t i = 0; i < ZLP_BLOCK; i += 2) { + size_t left = (size_t)data[i + 1] + 1; + while (left) { + size_t size = MIN(left, ZLS_BLOCK - device->pl_count); + memset(device->input + device->pl_count, data[i], size); + device->pl_count += size; + left -= size; + if (device->pl_count == ZLS_BLOCK) { + device->pl_count = 0; + if (deliver(sdi, ZLS_BLOCK)) + return TRUE; + } + } + } + return FALSE; + } + if (device->pl_packed) { + for (size_t i = 0; i < ZLP_BLOCK; i++) { + uint8_t packed = data[i]; + device->input[device->pl_count++] = device->pl_expand[packed & 15]; + device->input[device->pl_count++] = device->pl_expand[packed >> 4]; + } + } else { + memcpy(device->input + device->pl_count, data, ZLP_BLOCK); + device->pl_count += ZLP_BLOCK; + } + if (device->pl_count < ZLS_BLOCK) + return FALSE; + + gboolean done = deliver(sdi, device->pl_count); + device->pl_count = 0; + return done; +} + +static void start_pl_triggered(struct zynq_device *device) +{ + const uint32_t count = (device->limit + ZLP_BLOCK - 1) / ZLP_BLOCK * ZLP_BLOCK; + const uint32_t trigger_config = device->level_mask | device->level_value << 8 | + device->edge_mask << 16 | device->edge_value << 24; + const gboolean rle = device->pl_magic == ZLR_MAGIC; + const uint32_t value = device->divider | (rle ? device->enabled_mask << 16 : 0); + uint32_t request[] = {device->pl_magic, ZLP_START, device->nonce, value, + rle ? 0 : count, trigger_config, device->any_edge_mask}; + if (device->pl_magic == ZLT_MAGIC) { + request[5] = (device->pretrigger << 16) | ((device->stage_count - 1) << 8) | + device->serial_trigger; + request[6] = 0; + } + for (size_t i = 0; i < ARRAY_SIZE(request); i++) + request[i] = GUINT32_TO_LE(request[i]); + g_socket_send(device->datagrams, (const char *)request, sizeof(request), NULL, NULL); +} + +static void send_trigger_stage(struct zynq_device *device) +{ + const uint32_t *stage = device->stages[device->stage_sent]; + uint32_t request[] = {ZLT_MAGIC, ZLP_CONFIG, device->nonce, stage[0], stage[1], stage[2], stage[3]}; + for (size_t i = 0; i < ARRAY_SIZE(request); i++) + request[i] = GUINT32_TO_LE(request[i]); + g_socket_send(device->datagrams, (const char *)request, sizeof(request), NULL, NULL); +} + +static int receive_pl(const struct sr_dev_inst *sdi) +{ + struct zynq_device *device = sdi->priv; + GSocket *socket = device->datagrams; + const gint64 now = g_get_monotonic_time(); + if (zynq_record_failed(device->recorder)) + return receive_failure(sdi, "RJ2 disk recording failed"); + if (now >= device->keepalive) { + uint32_t value = device->divider; + const gboolean hardware_trigger = !device->stream || device->pl_magic == ZLR_MAGIC; + if (device->pl_magic == ZLP2_MAGIC) + value |= device->enabled_mask << 16; + if (device->pl_magic == ZLT_MAGIC && device->stage_sent < device->stage_count) + send_trigger_stage(device); + else if (hardware_trigger && !device->pl_started) { + start_pl_triggered(device); + // Waiting for the trigger may take arbitrarily long. The probe + // distinguishes that wait from a disconnected Ethernet link. + send_udp(socket, device->pl_magic, ZLP_PROBE, device->nonce, 0); + } else { + send_udp(socket, device->pl_magic, device->pl_started ? ZLP_KEEPALIVE : ZLP_START, + device->nonce, value); + } + device->keepalive = now + PL_HEARTBEAT; + } + if (now >= device->deadline) + return receive_failure(sdi, device->pl_pending ? + "RJ2 потерял UDP-пакет. Уменьшите частоту или проверьте сеть." : + "RJ2 не отвечает. Проверьте Ethernet и адрес платы."); + + for (unsigned n = 0; n < 64; n++) { + uint8_t response[ZLP_BLOCK + 17]; + GError *error = NULL; + gssize bytes = g_socket_receive(socket, (char *)response, sizeof(response), NULL, &error); + if (bytes < 0) { + gboolean waiting = g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK); + g_clear_error(&error); + if (!waiting) + return receive_failure(sdi, "RJ2 socket failed"); + if (device->pl_count && deliver(sdi, device->pl_count)) { + finish(sdi, SR_PKT_OK); + return FALSE; + } + device->pl_count = 0; + g_socket_condition_timed_wait(socket, G_IO_IN, 2 * G_TIME_SPAN_MILLISECOND, NULL, NULL); + break; + } + if (bytes < 16 || word(response, 0) != device->pl_magic || word(response, 2) != device->nonce) + continue; + + const uint32_t type = word(response, 1); + const uint32_t value = word(response, 3); + if (type == ZLP_INFO && bytes == 16) { + if (!device->pl_started && value == device->clock) + device->deadline = now + IO_TIMEOUT; + continue; + } + if (device->pl_magic == ZLT_MAGIC && type == ZLP_CONFIGURED && bytes == 16) { + if (value == device->stage_sent && value < device->stage_count) { + device->stage_sent++; + device->keepalive = 0; + } + continue; + } + if (type == ZLP_ERROR && bytes == 16) { + if (value == ZLP_BUSY && !device->pl_started) + continue; /* Retry START while the last capture drains. */ + if (value == ZLP_OVERFLOW) { + return receive_failure(sdi, device->pl_magic == ZLR_MAGIC ? + "Переполнение FPGA. Для этих сигналов выберите 100 Мвыб/с или меньше каналов." : + "Переполнение FIFO FPGA. Проверьте сеть или снизьте частоту."); + } + return receive_failure(sdi, "RJ2 rejected the capture"); + } + if (type != ZLP_DATA || bytes != ZLP_BLOCK + 16) + return receive_failure(sdi, "Invalid RJ2 sample packet"); + // Unsigned distance keeps the window valid across sequence wraparound. + uint32_t ahead = value - device->sequence; + if (ahead >= (UINT32_C(1) << 31)) + continue; /* Old duplicate. */ + if (ahead >= PL_REORDER_BLOCKS) + return receive_failure(sdi, "RJ2 потерял UDP-пакет. Уменьшите частоту или проверьте сеть."); + + device->pl_started = TRUE; + struct pl_sample_block *block = &device->pl_reorder[value % PL_REORDER_BLOCKS]; + if (block->ready) + continue; /* Duplicate inside the window. */ + memcpy(block->data, response + 16, ZLP_BLOCK); + block->ready = TRUE; + device->pl_pending++; + block = &device->pl_reorder[device->sequence % PL_REORDER_BLOCKS]; + while (block->ready) { + block->ready = FALSE; + device->pl_pending--; + device->sequence++; + device->deadline = now + IO_TIMEOUT; + if (append_pl_block(sdi, block->data)) { + finish(sdi, SR_PKT_OK); + return FALSE; + } + if (!device->source_active) + return FALSE; + block = &device->pl_reorder[device->sequence % PL_REORDER_BLOCKS]; + } + } + return TRUE; +} + +static int receive_data(int fd, int revents, const struct sr_dev_inst *sdi) +{ + (void)fd; + if (revents < 0) + return TRUE; + + struct zynq_device *device = sdi->priv; + if (device->pl) + return receive_pl(sdi); + GSocket *socket = g_socket_connection_get_socket(device->connection); + + for (unsigned step = 0; step < 64; step++) { + GError *error = NULL; + gssize bytes = g_socket_receive(socket, (char *)device->input + device->received, + device->wanted - device->received, NULL, &error); + if (bytes <= 0) { + gboolean waiting = g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK); + g_clear_error(&error); + if (!waiting) + return receive_failure(sdi, "Ethernet connection closed before capture completed"); + if (g_get_monotonic_time() >= device->deadline) + return receive_failure(sdi, "Capture timed out"); + g_socket_condition_timed_wait(socket, G_IO_IN, 2 * G_TIME_SPAN_MILLISECOND, NULL, NULL); + return TRUE; + } + + device->deadline = g_get_monotonic_time() + IO_TIMEOUT; + device->received += bytes; + if (device->received < device->wanted) + continue; + + const uint8_t *input = device->input; + switch (device->state) { + case REPLY: + if (word(input, 0) != (device->stream ? ZLS_MAGIC : ZLA_MAGIC) || + word(input, 1) != 1 || word(input, 3) != device->clock || + word(input, 4) != device->divider - !device->stream || + word(input, 6) != CHANNELS || word(input, 7) != !device->stream) + return receive_failure(sdi, "Invalid capture reply"); + if (word(input, 2) != 0) + return receive_failure(sdi, !device->stream && word(input, 2) == 2 ? + "Trigger timed out" : "Board rejected the capture request"); + if (word(input, 5) != (device->stream ? ZLS_BLOCK : device->limit)) + return receive_failure(sdi, "Unexpected capture length"); + if (device->stream) + prepare_receive(device, FRAME_HEADER, 12); + else + prepare_receive(device, SAMPLES, MIN(device->limit, ZLS_BLOCK)); + break; + case FRAME_HEADER: + if (word(input, 0) != device->sequence++) + return receive_failure(sdi, "Ethernet stream lost sample order"); + if (word(input, 2) == ZLS_OVERFLOW && word(input, 1) == 0) { + packet(sdi, SR_DF_OVERFLOW, SR_PKT_OK, NULL); + finish(sdi, SR_PKT_OK); + return FALSE; + } + if (word(input, 1) != ZLS_BLOCK || word(input, 2)) + return receive_failure(sdi, "Invalid Ethernet stream frame"); + prepare_receive(device, SAMPLES, ZLS_BLOCK); + break; + case SAMPLES: + if (deliver(sdi, device->wanted)) { + finish(sdi, SR_PKT_OK); + return FALSE; + } + if (device->stream) + prepare_receive(device, FRAME_HEADER, 12); + else + prepare_receive(device, SAMPLES, + MIN(device->limit - g_atomic_int_get(&device->progress), ZLS_BLOCK)); + break; + } + } + return TRUE; +} + +static int start(struct sr_dev_inst *sdi, void *cb_data) +{ + (void)cb_data; + struct zynq_device *device = sdi->priv; + device->enabled_mask = channel_mask(sdi); + device->divider = MAX(device->divider, min_divider(sdi)); + g_clear_pointer(&device->last_error, g_free); + g_clear_pointer(&device->last_recording, g_free); + if (device->disk_buffer && (!device->record_file || !*device->record_file)) + return SR_ERR_ARG; + if (!device->enabled_mask || !configure_trigger(device)) { + sr_err("%s: enable a channel and use a supported simple trigger", sdi->name); + return SR_ERR_ARG; + } + if (device->pl_magic == ZLT_MAGIC && !prepare_hardware_trigger(device)) { + set_error(device, "Проверьте настройки триггера. Доступны входы D0–D7."); + sr_err("%s: invalid advanced trigger", sdi->name); + return SR_ERR_ARG; + } + + if (device->pl) { + device->datagrams = connect_udp(sdi->path); + device->nonce = g_random_int() | 1u; + device->keepalive = 0; + device->pl_started = FALSE; + device->pl_count = 0; + device->pl_pending = 0; + if (device->pl_rle_available && device->stream) + device->pl_magic = device->divider == 1 && !pack_channels(device->enabled_mask) ? + ZLR_MAGIC : ZLP2_MAGIC; + device->pl_packed = device->pl_magic == ZLP2_MAGIC && pack_channels(device->enabled_mask); + if (device->pl_packed) { + for (unsigned nibble = 0; nibble < ARRAY_SIZE(device->pl_expand); nibble++) { + unsigned bit = 0; + unsigned raw = 0; + for (unsigned channel = 0; channel < CHANNELS; channel++) { + if (device->enabled_mask & (1u << channel)) + raw |= ((nibble >> bit++) & 1u) << channel; + } + device->pl_expand[nibble] = raw; + } + } + if (!device->datagrams) + return SR_ERR; + device->pl_reorder = g_try_new0(struct pl_sample_block, PL_REORDER_BLOCKS); + if (!device->pl_reorder) { + g_clear_object(&device->datagrams); + return SR_ERR_MALLOC; + } + } else { + GSocketConnection *connection = connect_to(sdi->path, device->stream ? ZLS_PORT : ZLA_PORT); + if (!connection) { + sr_err("%s: cannot open capture connection", sdi->name); + return SR_ERR; + } + uint32_t stream_request[] = {ZLS_MAGIC, 1, device->divider, 0}; + uint32_t snapshot_request[] = {ZLA_REQUEST, 1, device->limit, device->divider - 1, + device->level_mask, device->level_value, device->edge_mask, device->edge_value, + 0, 0, 0}; + gboolean sent = device->stream ? + send_request(connection, stream_request, ARRAY_SIZE(stream_request)) : + send_request(connection, snapshot_request, ARRAY_SIZE(snapshot_request)); + if (!sent) { + g_object_unref(connection); + return SR_ERR; + } + g_socket_set_blocking(g_socket_connection_get_socket(connection), FALSE); + device->connection = connection; + } + + if (device->record_file && *device->record_file) { + char *file = g_steal_pointer(&device->record_file); + device->record_samples = CLAMP(device->record_samples, SAMPLE_GROUP, + (uint64_t)device->clock * RECORD_SECONDS / device->divider); + device->recorder = zynq_record_open(file, device->clock, + device->divider, device->enabled_mask, device->pl_magic == ZLR_MAGIC); + if (!device->recorder) { + g_free(file); + g_clear_object(&device->datagrams); + g_clear_pointer(&device->pl_reorder, g_free); + return SR_ERR; + } + device->active_record = file; + } + /* Use the dummy-source loop. On Windows g_poll() expects HANDLEs. */ + if (sr_session_source_add(-1, 0, 0, receive_data, sdi) != SR_OK) { + g_clear_object(&device->connection); + g_clear_object(&device->datagrams); + g_clear_pointer(&device->pl_reorder, g_free); + zynq_record_close(device->recorder, NULL); + device->recorder = NULL; + g_clear_pointer(&device->active_record, g_free); + return SR_ERR; + } + device->source_active = TRUE; + device->sequence = 0; + device->previous_valid = FALSE; + device->pending_count = 0; + device->delivered = 0; + g_atomic_int_set(&device->progress, 0); + g_atomic_int_set(&device->triggered, 0); + g_atomic_int_set(&device->recorded_tenths, 0); + device->deadline = g_get_monotonic_time() + + (device->stream && device->pl_magic != ZLR_MAGIC ? IO_TIMEOUT : SNAPSHOT_TIMEOUT); + prepare_receive(device, REPLY, 32); + packet(sdi, SR_DF_HEADER, SR_PKT_OK, NULL); + return SR_OK; +} + +static struct sr_dev_driver zynq_driver = { + .name = "Zynq Ethernet", + .longname = "Zynq Ethernet Logic Analyzer", + .api_version = 1, + .config_get = config_get, + .config_set = config_set, + .config_list = config_list, + .dev_open = open_device, + .dev_close = close_device, + .dev_destroy = destroy_device, + .dev_status_get = get_status, + .dev_acquisition_start = start, + .dev_acquisition_stop = stop +}; + +SR_PRIV struct sr_dev_inst *sr_zynq_probe(const char *address, enum zynq_transport transport) +{ + const gboolean pl = transport == ZYNQ_PL_STREAM || transport == ZYNQ_PL_BUFFER; + const gboolean stream = transport == ZYNQ_PL_STREAM || transport == ZYNQ_TCP_STREAM; + uint32_t buffer_magic = ZLT_MAGIC; + uint32_t clock = pl ? probe_pl(address, stream ? ZLP2_MAGIC : buffer_magic) : probe_clock(address, stream); + if (!clock && pl) { + buffer_magic = ZLB_MAGIC; + clock = probe_pl(address, stream ? ZLP_MAGIC : buffer_magic); + } + if (!clock) + return NULL; + + const char *mode = stream ? (pl ? "RJ2" : "Stream") : (pl ? "RJ2 Snapshot" : "Buffer"); + char *name = g_strdup_printf("Zynq %s (%s)", mode, address); + struct sr_dev_inst *sdi = sr_dev_inst_new(LOGIC, SR_ST_INACTIVE, "Zynq", name, "1"); + g_free(name); + if (!sdi) + return NULL; + + struct zynq_device *device = g_new0(struct zynq_device, 1); + device->stream = stream; + device->pl = pl; + device->clock = clock; + device->pl_magic = pl ? (stream ? (clock == ZLP2_CLOCK_HZ ? ZLP2_MAGIC : ZLP_MAGIC) : buffer_magic) : 0; + device->pl_rle_available = device->pl_magic == ZLP2_MAGIC && probe_pl(address, ZLR_MAGIC); + if (pl && stream) { + device->pl_stream_magic = device->pl_magic; + if (probe_pl(address, ZLT_MAGIC)) + device->pl_snapshot_magic = ZLT_MAGIC; + else if (probe_pl(address, ZLB_MAGIC)) + device->pl_snapshot_magic = ZLB_MAGIC; + } + sdi->priv = device; + device->divider = pl && stream ? 2 : min_divider(sdi); + device->limit = stream ? 1000000 : ZLA_MAX_SAMPLES; + device->record_samples = device->limit; + sdi->driver = &zynq_driver; + sdi->dev_type = DEV_TYPE_NETWORK; + sdi->path = g_strdup(address); + for (unsigned channel = 0; channel < CHANNELS; channel++) { + char label[3] = {'D', '0' + channel, '\0'}; + sdi->channels = g_slist_append(sdi->channels, sr_channel_new(channel, SR_CHANNEL_LOGIC, TRUE, label)); + } + return sdi; +} diff --git a/tools/dsview/zynq-ethernet/zynq_record.c b/tools/dsview/zynq-ethernet/zynq_record.c new file mode 100644 index 0000000..c972fcf --- /dev/null +++ b/tools/dsview/zynq-ethernet/zynq_record.c @@ -0,0 +1,503 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#include "zynq_record.h" +#include +#include +#include +#include +#include +#include +#include +#include "../../log.h" +#include "libsigrok.h" + +/* DSView DSL v3 uses 16 Mi samples per channel leaf. One ZIP64 archive + * receives all leaves, with one leaf per channel held in RAM. */ +#define LEAF_BYTES (2u * 1024u * 1024u) +#define QUEUE_BYTES (64u * 1024u * 1024u) + +struct zynq_recorder { + unsigned clock, divider, mask, channels; + GAsyncQueue *queue; + GThread *thread; + gint queued, failed; + char stop; + gboolean compress; + gboolean published; + uint8_t *planes; + size_t used; + unsigned leaves; + uint64_t samples, first_sample; + GArray *tiles; + zipFile archive; + char *file, *partial; +}; + +struct chunk { + size_t size; + size_t samples; + uint8_t data[]; +}; + +static void fail(struct zynq_recorder *r, const char *reason) +{ + if (g_atomic_int_compare_and_exchange(&r->failed, 0, 1)) + sr_err("RJ2 recording %s: %s", r->file, reason); +} + +static voidpf ZCALLBACK open_utf8(voidpf opaque, const void *name, int mode) +{ + (void)opaque; + return g_fopen(name, mode & ZLIB_FILEFUNC_MODE_CREATE ? "wb" : "rb"); +} + +static gboolean entry(zipFile archive, const char *name, const void *data, unsigned size, int level) +{ + if (zipOpenNewFileInZip(archive, name, NULL, NULL, 0, NULL, 0, NULL, + level ? Z_DEFLATED : 0, level) != ZIP_OK) + return FALSE; + int written = zipWriteInFileInZip(archive, data, size); + return zipCloseFileInZip(archive) == ZIP_OK && written == ZIP_OK; +} + +static gboolean begin_file(struct zynq_recorder *r) +{ + r->partial = g_strconcat(r->file, ".part-XXXXXX", NULL); + int fd = g_mkstemp(r->partial); + if (fd < 0) { + g_clear_pointer(&r->partial, g_free); + return FALSE; + } + close(fd); + zlib_filefunc64_def io; + fill_fopen64_filefunc(&io); + io.zopen64_file = open_utf8; + r->archive = zipOpen2_64(r->partial, APPEND_STATUS_CREATE, NULL, &io); + return r->archive != NULL; +} + +static void summarize_plane(struct ds_zynq_tile *summary, const uint8_t *plane, + unsigned samples, unsigned mask) +{ + for (unsigned offset = 0; offset < samples; offset += DS_ZYNQ_TILE_SAMPLES, summary++) { + const unsigned count = MIN(DS_ZYNQ_TILE_SAMPLES, samples - offset); + const uint8_t *data = plane + offset / 8; + const gboolean first = data[0] & 1; + if (first) + summary->first |= mask; + if ((data[(count - 1) / 8] >> ((count - 1) % 8)) & 1) + summary->last |= mask; + const uint8_t expected = first ? 255 : 0; + unsigned i = 0; + while (i < count / 8 && data[i] == expected) + i++; + if (i < count / 8 || ((count % 8) && ((data[i] ^ expected) & ((1u << (count % 8)) - 1)))) + summary->changes |= mask; + } +} + +static gboolean write_leaf(struct zynq_recorder *r) +{ + if (!r->archive && !begin_file(r)) + return FALSE; + const unsigned samples = MIN(r->samples - (uint64_t)r->leaves * LEAF_BYTES * 8, r->used * 8); + const unsigned tile_count = (samples + DS_ZYNQ_TILE_SAMPLES - 1) / DS_ZYNQ_TILE_SAMPLES; + const unsigned first_tile = r->tiles->len; + g_array_set_size(r->tiles, first_tile + tile_count); + unsigned bit = 0; + for (unsigned channel = 0; channel < r->channels; channel++) { + while (!(r->mask & (1u << bit))) + bit++; + const uint8_t *plane = r->planes + channel * LEAF_BYTES; + summarize_plane(&g_array_index(r->tiles, struct ds_zynq_tile, first_tile), + plane, samples, 1u << bit++); + char name[32]; + g_snprintf(name, sizeof(name), "L-%u/%u", channel, r->leaves); + if (!entry(r->archive, name, r->planes + channel * LEAF_BYTES, r->used, r->compress ? 1 : 0)) + return FALSE; + } + r->leaves++; + r->used = 0; + return TRUE; +} + +static gboolean publish_file(struct zynq_recorder *r) +{ + unsigned rate = r->clock / r->divider; + GString *header = g_string_new("[version]\nversion = 3\n[header]\n" + "driver = virtual-session\ndevice mode = 0\ncapturefile = data\n"); + g_string_append_printf(header, "total samples = %" G_GUINT64_FORMAT + "\ntotal probes = %u\ntotal blocks = %u\nsamplerate = %u Hz\n" + "trigger time = 0\ntrigger pos = 0\n", r->samples, r->channels, r->leaves, rate); + GString *session = g_string_new("{\"Device\":\"virtual-session\",\"DeviceMode\":0," + "\"Version\":3,\"Title\":\"DSView v1.3.2\",\"Max Height\":\"1X\",\"decoder\":[],"); + g_string_append_printf(session, "\"Sample count\":\"%" G_GUINT64_FORMAT + "\",\"Sample rate\":\"%u\",\"channel\":[", r->samples, rate); + unsigned dense = 0; + for (unsigned channel = 0; channel < 8; channel++) { + if (!(r->mask & (1u << channel))) + continue; + g_string_append_printf(header, "probe%u = D%u\n", dense, channel); + g_string_append_printf(session, "%s{\"colour\":\"default\",\"enabled\":true," + "\"index\":%u,\"name\":\"D%u\",\"strigger\":0,\"type\":10000,\"view_index\":%u}", + dense ? "," : "", dense, channel, dense); + dense++; + } + g_string_append(session, "]}"); + char *info = g_strdup_printf("{\"first_sample\":%" G_GUINT64_FORMAT ",\"samples\":%" G_GUINT64_FORMAT + ",\"clock_hz\":%u,\"divider\":%u,\"mask\":%u,\"leaf_samples\":%u}", + r->first_sample, r->samples, r->clock, r->divider, r->mask, LEAF_BYTES * 8); + gboolean ok = entry(r->archive, "header", header->str, header->len, 0) && + entry(r->archive, "session", session->str, session->len, 0) && + entry(r->archive, "decoders", "[]", 2, 0) && + entry(r->archive, "zynq_recording.json", info, strlen(info), 0) && + (!r->tiles || entry(r->archive, "zynq_tiles.bin", r->tiles->data, + r->tiles->len * sizeof(struct ds_zynq_tile), 0)); + g_free(info); + g_string_free(session, TRUE); + g_string_free(header, TRUE); + ok = zipClose(r->archive, NULL) == ZIP_OK && ok; + r->archive = NULL; + if (!ok) + return FALSE; + GFile *source = g_file_new_for_path(r->partial); + GFile *target = g_file_new_for_path(r->file); + ok = g_file_move(source, target, G_FILE_COPY_NONE, NULL, NULL, NULL, NULL); + g_object_unref(target); + g_object_unref(source); + if (ok) { + r->published = TRUE; + g_clear_pointer(&r->partial, g_free); + } + return ok; +} + +static gboolean append(struct zynq_recorder *r, const struct chunk *chunk) +{ + size_t stride = 8 * r->channels; + r->samples += chunk->samples; + for (size_t offset = 0; offset < chunk->size; offset += stride) { + for (unsigned channel = 0; channel < r->channels; channel++) + memcpy(r->planes + channel * LEAF_BYTES + r->used, + chunk->data + offset + channel * 8, 8); + r->used += 8; + if (r->used == LEAF_BYTES && !write_leaf(r)) + return FALSE; + } + return TRUE; +} + +static gpointer writer(gpointer context) +{ + struct zynq_recorder *r = context; + gpointer item; + gboolean disk_ok = TRUE; + while ((item = g_async_queue_pop(r->queue)) != &r->stop) { + struct chunk *chunk = item; + if (disk_ok && !append(r, chunk)) { + disk_ok = FALSE; + fail(r, "disk write failed or the disk is full"); + } + g_atomic_int_add(&r->queued, -(gint)chunk->size); + g_free(chunk); + } + if (disk_ok && ((r->used && !write_leaf(r)) || + (r->samples && !publish_file(r)))) + fail(r, "could not finish the DSL file"); + if (r->archive) + zipClose(r->archive, NULL); + if (r->partial && !zynq_record_failed(r)) + g_unlink(r->partial); + return NULL; +} + +struct zynq_recorder *zynq_record_open(const char *file, uint32_t clock, + unsigned divider, unsigned mask, gboolean compress) +{ + struct zynq_recorder *r = g_new0(struct zynq_recorder, 1); + r->clock = clock; + r->divider = divider; + r->mask = mask; + r->compress = compress; + for (unsigned channel = 0; channel < 8; channel++) + r->channels += (mask >> channel) & 1u; + r->planes = g_try_malloc_n(r->channels, LEAF_BYTES); + r->file = g_strdup(file); + if (!r->planes || g_file_test(file, G_FILE_TEST_EXISTS) || !begin_file(r)) { + sr_err("Could not create RJ2 recording %s", file); + if (r->partial) + g_unlink(r->partial); + g_free(r->partial); + g_free(r->file); + g_free(r->planes); + g_free(r); + return NULL; + } + sr_info("RJ2 recording file: %s", r->file); + r->tiles = g_array_new(FALSE, TRUE, sizeof(struct ds_zynq_tile)); + r->queue = g_async_queue_new(); + r->thread = g_thread_new("zynq-disk", writer, r); + return r; +} + +gboolean zynq_record_failed(const struct zynq_recorder *r) +{ + return r && g_atomic_int_get(&r->failed); +} + +gboolean zynq_record_write(struct zynq_recorder *r, const void *data, size_t size, size_t samples) +{ + if (!r) + return TRUE; + if (zynq_record_failed(r)) + return FALSE; + struct chunk *chunk = NULL; + if (size <= QUEUE_BYTES - (unsigned)g_atomic_int_get(&r->queued)) + chunk = g_try_malloc(sizeof(*chunk) + size); + if (!chunk) { + fail(r, "disk queue could not keep up with capture"); + return FALSE; + } + chunk->size = size; + chunk->samples = samples; + memcpy(chunk->data, data, size); + g_atomic_int_add(&r->queued, size); + g_async_queue_push(r->queue, chunk); + return TRUE; +} + +gboolean zynq_record_close(struct zynq_recorder *r, gboolean *published) +{ + if (published) + *published = FALSE; + if (!r) + return TRUE; + g_async_queue_push(r->queue, &r->stop); + g_thread_join(r->thread); + gboolean ok = !zynq_record_failed(r); + if (published) + *published = r->published; + g_array_unref(r->tiles); + g_async_queue_unref(r->queue); + g_free(r->planes); + g_free(r->partial); + g_free(r->file); + g_free(r); + return ok; +} + +static unzFile open_capture(const char *path) +{ + zlib_filefunc64_def io; + fill_fopen64_filefunc(&io); + io.zopen64_file = open_utf8; + return unzOpen2_64(path, &io); +} + +static gboolean read_info(unzFile archive, struct ds_zynq_record_info *info) +{ + if (unzLocateFile(archive, "zynq_recording.json", 0) != UNZ_OK || + unzOpenCurrentFile(archive) != UNZ_OK) + return FALSE; + char json[256] = {0}; + int bytes = unzReadCurrentFile(archive, json, sizeof(json) - 1); + if (unzCloseCurrentFile(archive) != UNZ_OK || bytes <= 0) + return FALSE; + uint64_t first; + return sscanf(json, "{\"first_sample\":%" G_GUINT64_FORMAT ",\"samples\":%" G_GUINT64_FORMAT + ",\"clock_hz\":%u,\"divider\":%u,\"mask\":%u,\"leaf_samples\":%u}", + &first, &info->samples, &info->clock, &info->divider, &info->mask, &info->leaf_samples) == 6 && + (info->clock == 100000000 || info->clock == 125000000 || info->clock == 200000000) && + info->divider && info->divider <= 65535 && + info->mask && info->mask <= 255 && info->samples && + info->samples <= (uint64_t)G_MAXUINT / 8 * LEAF_BYTES * 8 && + info->leaf_samples == LEAF_BYTES * 8; +} + +SR_API int ds_zynq_record_info(const char *path, struct ds_zynq_record_info *info) +{ + unzFile archive = open_capture(path); + if (!archive) + return SR_ERR_NA; + gboolean ok = read_info(archive, info); + if (ok) + info->indexed = unzLocateFile(archive, "zynq_tiles.bin", 0) == UNZ_OK; + unzClose(archive); + return ok ? SR_OK : SR_ERR_NA; +} + +struct ds_zynq_disk { + unzFile archive; + struct ds_zynq_record_info info; + unsigned channels, leaves; + struct ds_zynq_tile *tiles; + unz64_file_pos *positions; +}; + +static gboolean read_member(unzFile archive, void *data, unsigned size) +{ + unz_file_info64 info; + if (unzGetCurrentFileInfo64(archive, &info, NULL, 0, NULL, 0, NULL, 0) != UNZ_OK || + info.uncompressed_size != size || unzOpenCurrentFile(archive) != UNZ_OK) + return FALSE; + int bytes = unzReadCurrentFile(archive, data, size); + return unzCloseCurrentFile(archive) == UNZ_OK && bytes == (int)size; +} + +static unsigned leaf_samples(const struct ds_zynq_disk *disk, unsigned leaf) +{ + return MIN(disk->info.leaf_samples, + disk->info.samples - (uint64_t)leaf * disk->info.leaf_samples); +} + +static gboolean read_leaf_member(unzFile archive, unsigned leaf, unsigned channel, + void *data, unsigned bytes) +{ + char name[32], actual[32] = {0}; + g_snprintf(name, sizeof(name), "L-%u/%u", channel, leaf); + return unzGetCurrentFileInfo64(archive, NULL, actual, sizeof(actual), NULL, 0, NULL, 0) == UNZ_OK && + !strncmp(name, actual, sizeof(actual)) && read_member(archive, data, bytes); +} + +static gboolean index_leaves(struct ds_zynq_disk *disk) +{ + const unsigned entries = disk->channels * disk->leaves; + disk->positions = g_try_new(unz64_file_pos, entries); + if (!disk->positions || unzGoToFirstFile(disk->archive) != UNZ_OK) + return FALSE; + + unsigned masks[8], count = 0; + for (unsigned bit = 0; bit < 8; bit++) { + if (disk->info.mask & (1u << bit)) + masks[count++] = 1u << bit; + } + // Older archives have the same sample blocks but no saved overview. + // Build it in one sequential read with a single channel block in memory. + g_autofree uint8_t *plane = disk->info.indexed ? NULL : g_try_malloc(LEAF_BYTES); + if (!disk->info.indexed && !plane) + return FALSE; + for (unsigned i = 0; i < entries; i++) { + if (unzGetFilePos64(disk->archive, &disk->positions[i]) != UNZ_OK) + return FALSE; + if (plane) { + const unsigned leaf = i / disk->channels, channel = i % disk->channels; + const unsigned samples = leaf_samples(disk, leaf); + if (!read_leaf_member(disk->archive, leaf, channel, plane, (samples + 63) / 64 * 8)) + return FALSE; + const uint64_t first_tile = (uint64_t)leaf * disk->info.leaf_samples / DS_ZYNQ_TILE_SAMPLES; + summarize_plane(disk->tiles + first_tile, plane, samples, masks[channel]); + } + if (i + 1 < entries && unzGoToNextFile(disk->archive) != UNZ_OK) + return FALSE; + } + return TRUE; +} + +SR_API struct ds_zynq_disk *ds_zynq_disk_open(const char *path, struct ds_zynq_record_info *info) +{ + struct ds_zynq_disk *disk = g_new0(struct ds_zynq_disk, 1); + disk->archive = open_capture(path); + if (!disk->archive || !read_info(disk->archive, &disk->info)) + goto error; + const uint64_t tiles = (disk->info.samples + DS_ZYNQ_TILE_SAMPLES - 1) / DS_ZYNQ_TILE_SAMPLES; + if (tiles > G_MAXINT / sizeof(struct ds_zynq_tile)) + goto error; + disk->info.indexed = unzLocateFile(disk->archive, "zynq_tiles.bin", 0) == UNZ_OK; + disk->tiles = g_try_new0(struct ds_zynq_tile, tiles); + if (!disk->tiles || (disk->info.indexed && + !read_member(disk->archive, disk->tiles, tiles * sizeof(struct ds_zynq_tile)))) + goto error; + for (unsigned bit = 0; bit < 8; bit++) + disk->channels += (disk->info.mask >> bit) & 1u; + disk->leaves = (disk->info.samples + disk->info.leaf_samples - 1) / disk->info.leaf_samples; + if (!index_leaves(disk)) + goto error; + *info = disk->info; + return disk; +error: + ds_zynq_disk_close(disk); + return NULL; +} + +SR_API void ds_zynq_disk_close(struct ds_zynq_disk *disk) +{ + if (!disk) + return; + if (disk->archive) + unzClose(disk->archive); + g_free(disk->positions); + g_free(disk->tiles); + g_free(disk); +} + +SR_API const struct ds_zynq_tile *ds_zynq_disk_tiles(const struct ds_zynq_disk *disk) +{ + return disk->tiles; +} + +SR_API int ds_zynq_disk_leaf(struct ds_zynq_disk *disk, unsigned leaf, unsigned channel, + void *data, unsigned capacity) +{ + if (leaf >= disk->leaves || channel >= disk->channels) + return SR_ERR_ARG; + const unsigned samples = leaf_samples(disk, leaf); + const unsigned bytes = (samples + 63) / 64 * 8; + if (bytes > capacity || + unzGoToFilePos64(disk->archive, &disk->positions[leaf * disk->channels + channel]) != UNZ_OK || + !read_leaf_member(disk->archive, leaf, channel, data, bytes)) + return SR_ERR; + return SR_OK; +} + +SR_API int ds_zynq_record_window(const char *source, const char *target, unsigned first_leaf) +{ + struct ds_zynq_record_info info; + unzFile input = open_capture(source); + if (!input) + return SR_ERR; + gboolean ok = read_info(input, &info); + uint64_t first = ok ? (uint64_t)first_leaf * info.leaf_samples : 0; + if (!ok || first >= info.samples || g_file_test(target, G_FILE_TEST_EXISTS)) { + unzClose(input); + return SR_ERR_ARG; + } + struct zynq_recorder output = {.file = (char *)target, .clock = info.clock, + .divider = info.divider, .mask = info.mask, .first_sample = first}; + for (unsigned bit = 0; bit < 8; bit++) + output.channels += (info.mask >> bit) & 1u; + output.samples = MIN(info.samples - first, (uint64_t)4 * info.leaf_samples); + output.leaves = (output.samples + info.leaf_samples - 1) / info.leaf_samples; + uint8_t *buffer = g_try_malloc(LEAF_BYTES); + ok = buffer && begin_file(&output) && unzGoToFirstFile(input) == UNZ_OK; + const unsigned skip = first_leaf * output.channels; + for (unsigned i = 0; ok && i < skip; i++) + ok = unzGoToNextFile(input) == UNZ_OK; + for (unsigned leaf = 0; ok && leaf < output.leaves; leaf++) { + const unsigned samples = MIN(info.leaf_samples, output.samples - (uint64_t)leaf * info.leaf_samples); + const unsigned expected = (samples + 63) / 64 * 8; + for (unsigned channel = 0; ok && channel < output.channels; channel++) { + char name[32], actual[32]; + g_snprintf(name, sizeof(name), "L-%u/%u", channel, first_leaf + leaf); + unz_file_info64 data_info; + if (unzGetCurrentFileInfo64(input, &data_info, actual, sizeof(actual), NULL, 0, NULL, 0) != UNZ_OK || + data_info.uncompressed_size != expected || strcmp(name, actual) || + unzOpenCurrentFile(input) != UNZ_OK) { + ok = FALSE; + break; + } + int received = unzReadCurrentFile(input, buffer, expected); + g_snprintf(name, sizeof(name), "L-%u/%u", channel, leaf); + ok = unzCloseCurrentFile(input) == UNZ_OK && received == (int)expected && + entry(output.archive, name, buffer, expected, 0) && unzGoToNextFile(input) == UNZ_OK; + } + } + if (ok) + ok = publish_file(&output); + if (output.archive) + zipClose(output.archive, NULL); + if (output.partial) + g_unlink(output.partial); + g_free(output.partial); + g_free(buffer); + unzClose(input); + return ok ? SR_OK : SR_ERR; +} diff --git a/tools/dsview/zynq-ethernet/zynq_record.h b/tools/dsview/zynq-ethernet/zynq_record.h new file mode 100644 index 0000000..69baf1d --- /dev/null +++ b/tools/dsview/zynq-ethernet/zynq_record.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#ifndef ZYNQ_RECORD_H +#define ZYNQ_RECORD_H + +#include +#include + +struct zynq_recorder; +struct zynq_recorder *zynq_record_open(const char *file, uint32_t clock, + unsigned divider, unsigned mask, gboolean compress); +gboolean zynq_record_write(struct zynq_recorder *recorder, const void *data, size_t size, size_t samples); +gboolean zynq_record_failed(const struct zynq_recorder *recorder); +gboolean zynq_record_close(struct zynq_recorder *recorder, gboolean *published); + +#endif