commit 6b78fcc34f03581182bbb1cd7a3f8cb5f5b2d8b6 Author: nikit3s Date: Tue Aug 11 12:16:18 2026 +0300 AD7766 на Tang Primer 25K: отладочная прошивка, структура после рефакторинга diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3412c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Gowin IDE build output, кроме настроек синтеза/PnR +impl/* +!impl/*_process_config.json +*.gprj.user + +# Даташит: большой, не трекаем +docs/ad7766.pdf +docs/ad7766_datasheet.md + +# Захваченные данные +*.bin +*.raw + +__pycache__/ diff --git a/AD7766.gprj b/AD7766.gprj new file mode 100644 index 0000000..5d68580 --- /dev/null +++ b/AD7766.gprj @@ -0,0 +1,19 @@ + + + + + 5 + gw5a25b-003 + + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a28e7a7 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# AD7766 на Tang Primer 25K + +Отладочная прошивка для AD7766: непрерывная оцифровка дифференциального входа +и выдача отсчётов по UART для просмотра в `F:\PyPrj\Signal_analayzer`. + +MCLK 1.000 МГц -> ODR 125 kSPS -> дециматор sinc^1 /10 -> **12 500 Гц** -> +кадры по 50 отсчётов -> UART 921600. + +Параметры и обоснования со ссылками на даташит — [docs/design.md](docs/design.md). +Распиновка — [docs/pinout.md](docs/pinout.md). + +## Состав + +| Файл | Назначение | +|:--|:--| +| `src/Top.vhd` | верхний уровень: только экземпляры и связи, без логики | +| `src/SPI_Master.vhd` | битовый уровень обмена: CS, SCLK, защёлка SDO | +| `src/SPI_Ctrl.vhd` | MCLK, сброс через SYNC/PD, запуск чтения по спаду DRDY | +| `src/decimator.vhd` | усреднение по 10 отсчётам | +| `src/fifo_top.vhd` | буфер отсчётов, заглушка под IP-ядро Gowin | +| `src/Packets_creator.vhd` | сборка кадра с CRC-16 | +| `src/UART.vhd` | передатчик, взят из `zvp_da_common` без изменений | +| `src/led_status.vhd` | индикация частотой мигания | +| `src/AD7766.cst` | привязка выводов | +| `tb/ad7766_model.vhd` | поведенческая модель АЦП по Table 3 | +| `tb/tb_top.vhd` | сквозной тест: модель -> ПЛИС -> UART -> разбор кадра | +| `tools/uart_capture.py` | приём кадров, запись `.raw` и `.bin` | +| `tools/uart_live.py` | живой просмотр: осциллограмма, спектр, счётчики | + +## Симуляция (GHDL) + +```bash +ghdl -a --std=08 --workdir=build src/fifo_top.vhd src/UART.vhd src/led_status.vhd src/SPI_Master.vhd src/SPI_Ctrl.vhd src/decimator.vhd src/Packets_creator.vhd src/Top.vhd tb/ad7766_model.vhd tb/tb_top.vhd && ghdl -e --std=08 --workdir=build tb_top && ./tb_top --stop-time=20ms +``` + +Модель выдаёт группами по 10 одинаковых слов, поэтому усреднённый отсчёт обязан +совпасть со значением группы побитово. Число проверяемых кадров задаётся +`-gFRAMES=N` (один кадр ~4 мс модельного времени). + +## Запись с железа + +```bash +py -3 tools/uart_capture.py --list +``` + +```bash +py -3 tools/uart_capture.py --port COM5 --seconds 10 --out test1 +``` + +Получаются `test1.raw` (весь поток как пришёл, для отладки канала), +`test1.bin` (только отсчёты, читается `read_24bit_bin()`) и `test1.log`. + +В `Signal_analayzer` для этих данных: `'FS': 12500`, `'V_REF': 5`. + +## Живой просмотр + +```bash +py -3 tools/uart_live.py --port COM10 +``` + +Осциллограмма, спектр и счётчики кадров в реальном времени — чтобы посмотреть +сигнал до того, как писать запись. Управление кнопками внизу окна: запись, +автомасштаб, вычитание постоянной составляющей, вид спектра, пауза. Те же +действия продублированы клавишами `r`, `a`, `d`, `s`, `f`, выход — `q`. + +Запись встроена в просмотр по необходимости: COM-порт держит один процесс, +и запустить `uart_capture.py` на том же порту параллельно нельзя. Для записи +без просмотра остаётся `uart_capture.py` — он же нужен, когда важно +фиксированное время записи. + +Разбор кадров у обоих инструментов общий (`FrameParser` в `uart_capture.py`): +формат описан в одном месте, так что просмотр и запись не могут разойтись. diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..a4f9ef1 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,60 @@ +# AD7766 — принятые решения + +Ссылки на страницы — по AD7766 Data Sheet Rev. D. + +## Тактирование и частота дискретизации + +| Параметр | Значение | Обоснование | +|:--|:--|:--| +| Системный клок | 50 МГц (E2) | генератор Tang Primer 25K | +| MCLK | 1.000 МГц = 50/50 | t2 min 100 нс, t3 max 900 нс, f max 1.024 МГц (Table 3, стр. 6-7) | +| ODR АЦП | 125 000 Гц | ODR = MCLK/8 для AD7766 (Table 6, стр. 15) | +| Дециматор | sinc^1 (среднее по 10) | понижение обязательно: 125 kSPS x 3 байта = 375 кБ/с не влезает в UART | +| Выходная ЧД | 12 500 Гц | совпадает с FS в F:\PyPrj\Signal_analayzer | +| SCLK | 6.25 МГц = 50/8 | 24 бита за 3.84 мкс при окне чтения 7.7 мкс | + +Окно чтения: DRDY низкий в течение tDRDY - t5 = 8000 - 294 = 7706 нс (Table 3). + +MCLK взят 1.000 МГц вместо максимальных 1.024 МГц ради целого деления от 50 МГц +(без PLL) и ради ровной выходной ЧД 12 500 Гц. + +## Интерфейс АЦП + +Регистров конфигурации у AD7766 нет — интерфейс только на чтение (стр. 17). + +- Сброс/синхронизация — низкий импульс на SYNC/PD (пин 7), цепь `RESET` на гребёнке. + События защёлкиваются по нарастающим фронтам MCLK (стр. 17). +- После снятия SYNC/PD выждать tSETTLING = 590 x tMCLK = 590 мкс (Table 7). +- Чтение: по спаду DRDY опустить CS, MSB появляется на SDO по спаду CS, + остальные 23 бита выдвигаются спадами SCLK (стр. 17, Figure 3). + Мастер защёлкивает биты по нарастающим фронтам SCLK. +- SDI (пин 15) — вход daisy-chain, для одиночного АЦП постоянный '0' (стр. 18). + +## Шкала + +VREF+ = 5 В -> синфазный уровень 2.5 В, каждый вход 0...5 В, дифференциально +-5 В +(стр. 16, 20). Полная шкала кода 0x7FFFFF = +5 В, 0x800000 = -5 В. +В Signal_analayzer нужно `'V_REF': 5`. + +## Формат кадра UART + +Бод 921600, 8N1. 50 отсчётов в кадре -> 157 байт, 250 кадров/с, 39.25 кБ/с (43% канала). + +| Поле | Байт | Описание | +|:--|--:|:--| +| SYNC | 2 | `A5 5A` | +| FRAME_CNT | 2 | uint16 BE, инкремент на кадр | +| STATUS | 1 | бит0 — переполнение FIFO, бит1 — сорван счёт DRDY | +| DATA | 150 | 50 отсчётов x 24 бита, MSB first | +| CRC16 | 2 | CCITT, poly 0x1021, init 0xFFFF, по FRAME_CNT..DATA | + +## Приём на ПК + +Пишутся два файла: + +- `*.raw` — байт в байт всё, что пришло из порта, до разбора. Отладочный. +- `*.bin` — только отсчёты, 3 байта BE на отсчёт, без служебных полей. + Читается функцией `read_24bit_bin()` из Signal_analayzer без переделок. + +Кадр с несошедшейся CRC отбрасывается, а пропуск по FRAME_CNT добивается нулями, +чтобы не сдвинулась ось времени. Места вставок пишутся в лог. diff --git a/docs/pinout.md b/docs/pinout.md new file mode 100644 index 0000000..a270afa --- /dev/null +++ b/docs/pinout.md @@ -0,0 +1,22 @@ +# Пинаут: плата AD7766 <-> Tang Primer 25K + +## Гребёнка платы АЦП + +| № на гребёнке | Название цепи | Вывод AD7766 (TSSOP-16) | Направление (со стороны ПЛИС) | Пин ПЛИС | +|:--:|:--|:--|:--|:--:| +| 1 | SDO | 10 — SDO | in | G2 | +| 2 | DRDY | 12 — DRDY | in | L4 | +| 3 | SCLK | 13 — SCLK | out | L1 | +| 4 | CLK_ADC_IN | 14 — MCLK | out | L2 | +| 5 | SDI | 15 — SDI | out | K4 | +| 6 | CS | 16 — CS | out | J4 | +| 7 | RESET | 7 — SYNC/PD | out | G1 | + +## Служебные цепи отладочной платы + +| Цепь | Назначение | Пин ПЛИС | +|:--|:--|:--:| +| clk_50m | тактовый генератор платы, 50 МГц | E2 | +| uart_tx | UART на внешний CP2102, ПЛИС -> ПК | **G5** | +| led0 | светодиод | E8 | +| key | кнопка, не используется | H11 | diff --git a/impl/AD7766_process_config.json b/impl/AD7766_process_config.json new file mode 100644 index 0000000..6858bbc --- /dev/null +++ b/impl/AD7766_process_config.json @@ -0,0 +1,93 @@ +{ + "BACKGROUND_PROGRAMMING" : "off", + "COMPRESS" : false, + "CPU" : true, + "CRC_CHECK" : true, + "Clock_Route_Order" : 0, + "Convert_SDP32_36_to_SDP16_18" : true, + "Correct_Hold_Violation" : true, + "DONE" : false, + "DOWNLOAD_SPEED" : "default", + "Disable_Insert_Pad" : false, + "ENABLE_CTP" : false, + "ENABLE_MERGE_MODE" : false, + "ENCRYPTION_KEY" : false, + "ENCRYPTION_KEY_TEXT" : "00000000000000000000000000000000", + "ERROR_DECTION_AND_CORRECTION" : false, + "ERROR_DECTION_ONLY" : false, + "ERROR_INJECTION" : false, + "EXTERNAL_MASTER_CONFIG_CLOCK" : false, + "Enable_DSRM" : false, + "FORMAT" : "binary", + "FREQUENCY_DIVIDER" : "1", + "Generate_Constraint_File_of_Ports" : false, + "Generate_IBIS_File" : false, + "Generate_Plain_Text_Timing_Report" : false, + "Generate_Post_PNR_Simulation_Model_File" : false, + "Generate_Post_Place_File" : false, + "Generate_SDF_File" : false, + "Generate_VHDL_Post_PNR_Simulation_Model_File" : false, + "Global_Freq" : "default", + "GwSyn_Loop_Limit" : 2000, + "HOTBOOT" : false, + "I2C" : false, + "I2C_SLAVE_ADDR" : "00", + "INCREMENTAL_PLACE_AND_ROUTING" : "0", + "INCREMENTAL_PLACE_ONLY" : "0", + "IncludePath" : [ + + ], + "Incremental_Compile" : "", + "Initialize_Primitives" : false, + "JTAG" : false, + "MODE_IO" : false, + "MSPI" : false, + "MSPI_JUMP" : false, + "MULTIBOOT_ADDRESS_WIDTH" : "24", + "MULTIBOOT_MODE" : "Single", + "MULTIBOOT_SPI_FLASH_ADDRESS" : "000000", + "MULTIJUMP_ADDRESS_WIDTH" : "24", + "MULTIJUMP_MODE" : "Single", + "MULTIJUMP_SPI_FLASH_ADDRESS" : "000000", + "Multi_Boot" : false, + "OUTPUT_BASE_NAME" : "AD7766", + "POWER_ON_RESET_MONITOR" : true, + "PRINT_BSRAM_VALUE" : true, + "PROGRAM_DONE_BYPASS" : false, + "PlaceInRegToIob" : true, + "PlaceIoRegToIob" : true, + "PlaceOutRegToIob" : true, + "Place_Option" : "0", + "Process_Configuration_Verion" : "1.0", + "Promote_Physical_Constraint_Warning_to_Error" : true, + "READY" : true, + "RECONFIG_N" : false, + "Ram_RW_Check" : false, + "Replicate_Resources" : false, + "Report_Auto-Placed_Io_Information" : false, + "Route_Maxfan" : 23, + "Route_Option" : "0", + "Run_Timing_Driven" : true, + "SECURE_MODE" : false, + "SECURITY_BIT" : true, + "SEU_HANDLER" : false, + "SEU_HANDLER_CHECKSUM" : false, + "SEU_HANDLER_MODE" : "auto", + "SSPI" : true, + "STOP_SEU_HANDLER" : false, + "SerDes_retiming" : false, + "Show_All_Warnings" : false, + "Synthesize_tool" : "GowinSyn", + "TclPre" : "", + "TopModule" : "", + "USERCODE" : "default", + "Unused_Pin" : "As_input_tri_stated_with_pull_up", + "VCC" : "0.9", + "VCCAUX" : "3.3", + "VCCX" : "3.3", + "VHDL_Standard" : "VHDL_Std_1993", + "Verilog_Standard" : "Vlg_Std_2001", + "WAKE_UP" : "0", + "show_all_warnings" : false, + "turn_off_bg" : false +} \ No newline at end of file diff --git a/src/AD7766.cst b/src/AD7766.cst new file mode 100644 index 0000000..1e70477 --- /dev/null +++ b/src/AD7766.cst @@ -0,0 +1,29 @@ +//Copyright (C)2014-2025 Gowin Semiconductor Corporation. +//All rights reserved. +//File Title: Physical Constraints file +//Tool Version: V1.9.12 (64-bit) +//Part Number: GW5A-LV25MG121NC1/I0 +//Device: GW5A-25 +//Device Version: B +//Created Time: Sat 08 08 12:42:06 2026 + +IO_LOC "LED" E8; +IO_PORT "LED" IO_TYPE=LVCMOS33 PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; +IO_LOC "UART_TX" G5; +IO_PORT "UART_TX" IO_TYPE=LVCMOS33 PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; +IO_LOC "ADC_SYNC_PD" G1; +IO_PORT "ADC_SYNC_PD" IO_TYPE=LVCMOS33 PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; +IO_LOC "ADC_CS_N" J4; +IO_PORT "ADC_CS_N" IO_TYPE=LVCMOS33 PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; +IO_LOC "ADC_SDI" K4; +IO_PORT "ADC_SDI" IO_TYPE=LVCMOS33 PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; +IO_LOC "ADC_MCLK" L2; +IO_PORT "ADC_MCLK" IO_TYPE=LVCMOS33 PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; +IO_LOC "ADC_SCLK" L1; +IO_PORT "ADC_SCLK" IO_TYPE=LVCMOS33 PULL_MODE=NONE DRIVE=8 BANK_VCCIO=3.3; +IO_LOC "ADC_DRDY" L4; +IO_PORT "ADC_DRDY" IO_TYPE=LVCMOS33 PULL_MODE=NONE BANK_VCCIO=3.3; +IO_LOC "ADC_SDO" G2; +IO_PORT "ADC_SDO" IO_TYPE=LVCMOS33 PULL_MODE=NONE BANK_VCCIO=3.3; +IO_LOC "clk" E2; +IO_PORT "clk" IO_TYPE=LVCMOS33 PULL_MODE=NONE BANK_VCCIO=3.3; diff --git a/src/AD7766.sdc b/src/AD7766.sdc new file mode 100644 index 0000000..88c7951 --- /dev/null +++ b/src/AD7766.sdc @@ -0,0 +1,3 @@ +// Тактовый генератор платы: 50 МГц -> период 20 нс. +// Без этого статический анализ не проверяет ни одного пути и выдаёт TA1132. +create_clock -name clk -period 20 -waveform {0 10} [get_ports {clk}] diff --git a/src/Packets_creator.vhd b/src/Packets_creator.vhd new file mode 100644 index 0000000..7377b10 --- /dev/null +++ b/src/Packets_creator.vhd @@ -0,0 +1,288 @@ +---------------------------------------------------------------------------------- +-- Company: +-- Engineer: +-- Create Date: +-- Design Name: +-- Module Name: Packets_creator - Behavioral +-- Project Name: AD7766_prj +-- Target Devices: Gowin GW5A-25 +-- Description: +-- Кадр (157 байт при 50 отсчётах): +-- SYNC 2 байта A5 5A +-- FRAME_CNT 2 байта uint16 BE +-- STATUS 1 байт бит0 переполнение FIFO, бит1 срыв чтения DRDY +-- DATA 150 байт 50 отсчётов x 24 бита, MSB first +-- CRC16 2 байта CCITT, poly 0x1021, init 0xFFFF, по FRAME_CNT..DATA +-- +-- Байты SYNC в CRC не входят: они нужны приёмнику для ресинхронизации +-- до того, как он начнёт что-либо считать. +---------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity Packets_creator is + generic ( + SAMPLES_PER_FRAME : integer := 50 + ); + port ( + clk : in std_logic; + rst_n : in std_logic; + + -- Со стороны FIFO + data_ADC : in std_logic_vector(23 downto 0); -- голова очереди + dready : in std_logic; -- набрался полный кадр + rd_en : out std_logic; + + -- Для флагов состояния + wr_en : in std_logic; -- запись в FIFO, вместе с fifo_full даёт переполнение + fifo_full : in std_logic; + err_overrun : in std_logic; + + -- Со стороны UART + busy_uart : in std_logic; + ena_uart : out std_logic; + dout : out std_logic_vector(7 downto 0); + + fault : out std_logic -- была хотя бы одна ошибка с момента сброса + ); +end entity Packets_creator; + +architecture Behavioral of Packets_creator is + + constant SYNC0 : std_logic_vector(7 downto 0) := x"A5"; + constant SYNC1 : std_logic_vector(7 downto 0) := x"5A"; + + type fstate_t is (F_WAIT, F_SYNC0, F_SYNC1, F_CNT_H, F_CNT_L, F_STATUS, + F_FETCH, F_FETCH1, F_FETCH2, F_DATA, F_CRC_H, F_CRC_L); + signal fstate : fstate_t := F_WAIT; + + signal waiting : std_logic := '0'; + signal byte_go : std_logic := '0'; + signal byte_val : std_logic_vector(7 downto 0) := (others => '0'); + signal byte_ack : std_logic := '0'; + signal sample_reg : std_logic_vector(23 downto 0) := (others => '0'); + signal sample_no : integer range 0 to SAMPLES_PER_FRAME - 1 := 0; + signal byte_idx : integer range 0 to 2 := 0; + + signal frame_cnt : unsigned(15 downto 0) := (others => '0'); + signal status_lat : std_logic_vector(7 downto 0) := (others => '0'); + signal crc_reg : std_logic_vector(15 downto 0) := (others => '1'); + + signal ovf_sticky : std_logic := '0'; + signal err_sticky : std_logic := '0'; + signal fault_r : std_logic := '0'; + + type tstate_t is (T_IDLE, T_SEND, T_WAIT); + signal tstate : tstate_t := T_IDLE; + signal hold : std_logic_vector(7 downto 0) := (others => '0'); + + -- CRC-16/CCITT по одному байту, MSB-first + function crc16_upd(crc : std_logic_vector(15 downto 0); + d : std_logic_vector(7 downto 0)) + return std_logic_vector is + variable c : std_logic_vector(15 downto 0) := crc; + variable fb : std_logic; + begin + for i in 7 downto 0 loop + fb := c(15) xor d(i); + c := c(14 downto 0) & '0'; + if fb = '1' then + c := c xor x"1021"; + end if; + end loop; + return c; + end function; + +begin + + fault <= fault_r; + + -- Отдача байта в UART: ждём освобождения передатчика, даём однотактовый ena, + -- подтверждаем вызывающему по факту взведения busy_uart + tx_proc : process (clk, rst_n) + begin + if rst_n = '0' then + tstate <= T_IDLE; + hold <= (others => '0'); + ena_uart <= '0'; + dout <= (others => '0'); + byte_ack <= '0'; + elsif rising_edge(clk) then + ena_uart <= '0'; + byte_ack <= '0'; + + case tstate is + when T_IDLE => + if byte_go = '1' then + hold <= byte_val; + tstate <= T_SEND; + end if; + + when T_SEND => + if busy_uart = '0' then + dout <= hold; + ena_uart <= '1'; + tstate <= T_WAIT; + end if; + + when T_WAIT => + if busy_uart = '1' then + byte_ack <= '1'; + tstate <= T_IDLE; + end if; + end case; + end if; + end process tx_proc; + + frame_proc : process (clk, rst_n) + variable b : std_logic_vector(7 downto 0); + begin + if rst_n = '0' then + fstate <= F_WAIT; + waiting <= '0'; + byte_go <= '0'; + byte_val <= (others => '0'); + rd_en <= '0'; + sample_reg <= (others => '0'); + sample_no <= 0; + byte_idx <= 0; + frame_cnt <= (others => '0'); + status_lat <= (others => '0'); + crc_reg <= (others => '1'); + ovf_sticky <= '0'; + err_sticky <= '0'; + fault_r <= '0'; + elsif rising_edge(clk) then + byte_go <= '0'; + rd_en <= '0'; + + if waiting = '1' then + if byte_ack = '1' then + waiting <= '0'; + case fstate is + when F_SYNC0 => fstate <= F_SYNC1; + when F_SYNC1 => fstate <= F_CNT_H; + when F_CNT_H => fstate <= F_CNT_L; + when F_CNT_L => fstate <= F_STATUS; + when F_STATUS => + sample_no <= 0; + fstate <= F_FETCH; + when F_DATA => + if byte_idx = 2 then + byte_idx <= 0; + if sample_no = SAMPLES_PER_FRAME - 1 then + fstate <= F_CRC_H; + else + sample_no <= sample_no + 1; + fstate <= F_FETCH; + end if; + else + byte_idx <= byte_idx + 1; + end if; + when F_CRC_H => fstate <= F_CRC_L; + when F_CRC_L => + frame_cnt <= frame_cnt + 1; + fstate <= F_WAIT; + when others => fstate <= F_WAIT; + end case; + end if; + + else + case fstate is + + when F_WAIT => + -- Кадр начинаем только когда отсчётов набралось на полный кадр: + -- иначе передатчик упрётся в пустой FIFO посреди кадра + if dready = '1' then + crc_reg <= (others => '1'); + status_lat <= (1 => err_sticky, 0 => ovf_sticky, others => '0'); + ovf_sticky <= '0'; + err_sticky <= '0'; + fstate <= F_SYNC0; + end if; + + when F_SYNC0 => + byte_val <= SYNC0; + byte_go <= '1'; + waiting <= '1'; + + when F_SYNC1 => + byte_val <= SYNC1; + byte_go <= '1'; + waiting <= '1'; + + when F_CNT_H => + b := std_logic_vector(frame_cnt(15 downto 8)); + byte_val <= b; + crc_reg <= crc16_upd(crc_reg, b); + byte_go <= '1'; + waiting <= '1'; + + when F_CNT_L => + b := std_logic_vector(frame_cnt(7 downto 0)); + byte_val <= b; + crc_reg <= crc16_upd(crc_reg, b); + byte_go <= '1'; + waiting <= '1'; + + when F_STATUS => + byte_val <= status_lat; + crc_reg <= crc16_upd(crc_reg, status_lat); + byte_go <= '1'; + waiting <= '1'; + + -- Импульс rd_en, и очередное слово + -- появляется на data_ADC со следующего такта. Два такта ожидания + -- вместо одного -- запас, обмен всё равно идёт раз в три байта UART. + when F_FETCH => + rd_en <= '1'; + fstate <= F_FETCH1; + + when F_FETCH1 => + fstate <= F_FETCH2; + + when F_FETCH2 => + sample_reg <= data_ADC; + byte_idx <= 0; + fstate <= F_DATA; + + when F_DATA => + case byte_idx is + when 0 => b := sample_reg(23 downto 16); + when 1 => b := sample_reg(15 downto 8); + when others => b := sample_reg(7 downto 0); + end case; + byte_val <= b; + crc_reg <= crc16_upd(crc_reg, b); + byte_go <= '1'; + waiting <= '1'; + + when F_CRC_H => + byte_val <= crc_reg(15 downto 8); + byte_go <= '1'; + waiting <= '1'; + + when F_CRC_L => + byte_val <= crc_reg(7 downto 0); + byte_go <= '1'; + waiting <= '1'; + + end case; + end if; + + -- Флаги ошибок взводятся после автомата, чтобы взвод не потерялся + -- на такте очистки в начале кадра + if wr_en = '1' and fifo_full = '1' then + ovf_sticky <= '1'; + fault_r <= '1'; + end if; + if err_overrun = '1' then + err_sticky <= '1'; + fault_r <= '1'; + end if; + + end if; + end process frame_proc; + +end architecture Behavioral; diff --git a/src/SPI_Ctrl.vhd b/src/SPI_Ctrl.vhd new file mode 100644 index 0000000..affeb48 --- /dev/null +++ b/src/SPI_Ctrl.vhd @@ -0,0 +1,199 @@ +---------------------------------------------------------------------------------- +-- Company: +-- Engineer: +-- Create Date: +-- Design Name: +-- Module Name: spi_ctrl - Behavioral +-- Project Name: AD7766_prj +-- Target Devices: Gowin GW5A-25 +-- Description: +-- Управление AD7766 поверх spi_master. +-- Обмен с мастером через spi_ena и spi_busy. +-- Сброс фильтра импульсом SYNC/PD и выдержка tSETTLING. +-- Тактирование АЦП: частота дискретизации задаётся входом MCLK и MCLK_HALF. +---------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity spi_ctrl is + generic ( + -- MCLK = clk / (2*MCLK_HALF). Ограничения: min 100 нс, max 900 нс, + -- При clk 50 МГц и MCLK_HALF = 25 -> 1.000 МГц (max 1.024 МГц). + MCLK_HALF : integer := 25; + + -- tSETTLING = 590 x tMCLK для AD7766 + SETTLING_MCLK : integer := 590; + + -- Длительность низкого уровня SYNC/PD в периодах MCLK + SYNC_LOW_MCLK : integer := 8 + ); + port ( + clk : in std_logic; + rst_n : in std_logic; + + -- Интерфейс с АЦП + adc_mclk : out std_logic; + adc_sync_pd_n : out std_logic; + adc_sdi : out std_logic; + adc_drdy : in std_logic; + + -- Интерфейс с SPI Master + spi_busy : in std_logic; + spi_ena : out std_logic; + spi_tx_data : out std_logic_vector(23 downto 0); + spi_rx : in std_logic_vector(23 downto 0); + + -- Отсчёты и статус + sample : out std_logic_vector(23 downto 0); + sample_valid : out std_logic; + ready : out std_logic; -- фильтр установился, идёт нормальный поток + err_overrun : out std_logic -- импульс: DRDY пришёл раньше конца чтения + ); +end entity spi_ctrl; + +architecture rtl of spi_ctrl is + + type fsm_state_t is ( + ST_SYNC_LOW, -- держим SYNC/PD низким + ST_SETTLE, -- ждём tSETTLING после снятия SYNC/PD + ST_IDLE, -- ждём спад DRDY + ST_READ -- идёт обмен, ждём освобождения мастера + ); + signal state : fsm_state_t := ST_SYNC_LOW; + + -- Генератор MCLK + signal mclk_cnt : integer range 0 to MCLK_HALF - 1 := 0; + signal mclk_s : std_logic := '0'; + signal mclk_rise : std_logic := '0'; + + -- Детекторы фронтов + signal drdy_sync1 : std_logic := '1'; + signal drdy_sync2 : std_logic := '1'; + signal drdy_sync3 : std_logic := '1'; + signal spi_busy_d : std_logic := '0'; + + signal adc_drdy_fall : std_logic; + signal spi_busy_fall : std_logic; + + signal mclk_ticks : integer range 0 to SETTLING_MCLK - 1 := 0; + +begin + + adc_mclk <= mclk_s; + adc_sdi <= '0'; + -- Писать в AD7766 нечего, но мастер требует слово для передачи + spi_tx_data <= (others => '0'); + ready <= '0' when state = ST_SYNC_LOW or state = ST_SETTLE else '1'; + -- Генератор MCLK. События SYNC/PD привязаны к нарастающим фронтам MCLK, + -- поэтому отдаём наружу ещё и однотактовый строб фронта. + mclk_proc : process (clk, rst_n) + begin + if rst_n = '0' then + mclk_cnt <= 0; + mclk_s <= '0'; + mclk_rise <= '0'; + elsif rising_edge(clk) then + mclk_rise <= '0'; + if mclk_cnt = MCLK_HALF - 1 then + mclk_cnt <= 0; + mclk_s <= not mclk_s; + if mclk_s = '0' then + mclk_rise <= '1'; + end if; + else + mclk_cnt <= mclk_cnt + 1; + end if; + end if; + end process mclk_proc; + + -- Детекторы фронтов + edge_proc : process (clk, rst_n) + begin + if rst_n = '0' then + drdy_sync1 <= '1'; + drdy_sync2 <= '1'; + drdy_sync3 <= '1'; + spi_busy_d <= '0'; + elsif rising_edge(clk) then + drdy_sync1 <= adc_drdy; + drdy_sync2 <= drdy_sync1; + drdy_sync3 <= drdy_sync2; + spi_busy_d <= spi_busy; + end if; + end process edge_proc; + + -- DRDY низкий почти весь период, наверх уходит коротким импульсом + -- t5 = 294 нс. Ловим спад: он отмечает готовность нового слова (стр. 17). + adc_drdy_fall <= '1' when drdy_sync3 = '1' and drdy_sync2 = '0' else + '0'; + spi_busy_fall <= '1' when spi_busy_d = '1' and spi_busy = '0' else + '0'; + + -- Основной конечный автомат + fsm_proc : process (clk, rst_n) + begin + if rst_n = '0' then + state <= ST_SYNC_LOW; + mclk_ticks <= 0; + adc_sync_pd_n <= '0'; + spi_ena <= '0'; + sample <= (others => '0'); + sample_valid <= '0'; + err_overrun <= '0'; + elsif rising_edge(clk) then + -- Значения по умолчанию для стробов + spi_ena <= '0'; + sample_valid <= '0'; + err_overrun <= '0'; + -- Новый DRDY во время чтения означает, что мы не уложились в окно + if adc_drdy_fall = '1' and state = ST_READ then + err_overrun <= '1'; + end if; + + case state is + + when ST_SYNC_LOW => + adc_sync_pd_n <= '0'; + if mclk_rise = '1' then + if mclk_ticks = SYNC_LOW_MCLK - 1 then + mclk_ticks <= 0; + state <= ST_SETTLE; + else + mclk_ticks <= mclk_ticks + 1; + end if; + end if; + + -- После снятия SYNC/PD фильтру нужно tSETTLING, до этого DRDY + -- не значит ничего + when ST_SETTLE => + adc_sync_pd_n <= '1'; + if mclk_rise = '1' then + if mclk_ticks = SETTLING_MCLK - 1 then + mclk_ticks <= 0; + state <= ST_IDLE; + else + mclk_ticks <= mclk_ticks + 1; + end if; + end if; + + when ST_IDLE => + if adc_drdy_fall = '1' then + spi_ena <= '1'; + state <= ST_READ; + end if; + + -- Мастер поднимает busy на следующем такте после enable, поэтому + -- ложного спада тут быть не может + when ST_READ => + if spi_busy_fall = '1' then + sample <= spi_rx; + sample_valid <= '1'; + state <= ST_IDLE; + end if; + + end case; + end if; + end process fsm_proc; + +end architecture rtl; diff --git a/src/SPI_Master.vhd b/src/SPI_Master.vhd new file mode 100644 index 0000000..d2edb46 --- /dev/null +++ b/src/SPI_Master.vhd @@ -0,0 +1,163 @@ +---------------------------------------------------------------------------------- +-- Company: +-- Engineer: +-- Create Date: +-- Design Name: +-- Module Name: spi_master - Behavioral +-- Project Name: AD7766_prj +-- Target Devices: Gowin GW5A-25 +-- Description: -- Интерфейс и устройство взяты от spi_master (Scott Larson, +-- Digi-Key/eewiki): тот же автомат init/execute, те же имена сигналов +-- и портов, включая неиспользуемые здесь mosi/tx/flag_tx. +-- +-- Два отличия, обязательные для AD7766: +-- +-- 1. Захват первого бита по спаду CS. AD7766 выставляет MSB на SDO в момент +-- спада CS, до первого фронта SCLK (datasheet Rev. D, стр. 17, Figure 3). +-- Донорский мастер защёлкивает miso только после переключений SCLK и этот +-- бит терял, собирая слово со сдвигом. Поведение выбирается через cpha: +-- cpha = '0' -- первый бит берётся при выставлении CS (AD7766) +-- cpha = '1' -- первый бит берётся после первого фронта (как у донора) +-- +-- 2. Делитель SCLK. Донор переключал SCLK каждый такт, то есть clk/2 = 25 МГц. +-- Формально в предел AD7766 это укладывается (tSCLK min = t8, Table 3), +-- но на проводах избыточно, поэтому полупериод задаётся generic'ом. +-- +-- Тактирование по нарастающему фронту, а не по спаду как у донора: обмен +-- с spi_ctrl идёт по одному фронту, без путей в полтакта между модулями. +---------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +entity spi_master is + generic ( + data_length : integer := 24; -- длина слова в битах + sclk_half : integer := 4 -- полупериод SCLK в тактах clk + ); + port ( + clk : in std_logic; + reset_n : in std_logic; + enable : in std_logic; + cpol : in std_logic; + cpha : in std_logic; + miso : in std_logic; + sclk : out std_logic; + ss_n : out std_logic; + mosi : out std_logic; + busy : out std_logic; + wr_ack : out std_logic; + flag_tx : in std_logic; + tx : in std_logic_vector(data_length - 1 downto 0); + rx : out std_logic_vector(data_length - 1 downto 0) + ); +end entity spi_master; + +architecture behavioural of spi_master is + + type FSM is (init, execute); + signal state : FSM := init; + + -- Тик = полупериод SCLK. Тик 0 отдан установлению SDO после спада CS, + -- дальше на каждый тик приходится одно переключение SCLK. + signal prescale : integer range 0 to sclk_half - 1 := 0; + signal clk_toggles : integer range 0 to data_length * 2 + 1 := 0; + signal last_toggle : integer range 0 to data_length * 2 + 1 := 0; + + signal rxBuffer : std_logic_vector(data_length - 1 downto 0) := (others => '0'); + signal txBuffer : std_logic_vector(data_length - 1 downto 0) := (others => '0'); + + signal INT_ss_n : std_logic := '1'; + signal INT_sclk : std_logic := '1'; + +begin + + ss_n <= INT_ss_n; + sclk <= INT_sclk; + + process (clk, reset_n) + begin + + if (reset_n = '0') then + busy <= '1'; + INT_ss_n <= '1'; + INT_sclk <= '1'; + mosi <= '0'; + wr_ack <= '0'; + rx <= (others => '0'); + state <= init; + + elsif (rising_edge(clk)) then + case state is + + when init => -- шина свободна + busy <= '0'; + INT_ss_n <= '1'; + INT_sclk <= cpol; + wr_ack <= '0'; + + if (enable = '1') then + busy <= '1'; + INT_ss_n <= '0'; -- CS вниз, АЦП выставляет MSB + INT_sclk <= cpol; + txBuffer <= tx; + rxBuffer <= (others => '0'); + prescale <= 0; + clk_toggles <= 0; + -- При cpha='0' первый бит берётся на тике 0, поэтому переключений + -- SCLK нужно на два меньше + if cpha = '0' then + last_toggle <= data_length * 2 - 2; + else + last_toggle <= data_length * 2; + end if; + state <= execute; + end if; + + when execute => + busy <= '1'; + wr_ack <= '0'; + + if (prescale < sclk_half - 1) then + prescale <= prescale + 1; + else + prescale <= 0; + + if (clk_toggles = 0) then + -- Тик установления: SCLK не трогаем. MSB уже на SDO, ждали + -- sclk_half тактов -- с запасом против t7 и t8 + if (cpha = '0') then + rxBuffer <= rxBuffer(data_length - 2 downto 0) & miso; + end if; + clk_toggles <= 1; + + elsif (clk_toggles <= last_toggle) then + INT_sclk <= not INT_sclk; + -- Данные АЦП меняет по спадам SCLK, поэтому защёлкиваем их + -- по подъёму, через полпериода после смены + if (INT_sclk = '0') then + rxBuffer <= rxBuffer(data_length - 2 downto 0) & miso; + else + mosi <= txBuffer(data_length - 1); + txBuffer <= txBuffer(data_length - 2 downto 0) & '0'; + end if; + clk_toggles <= clk_toggles + 1; + + else + -- Обмен завершён + INT_ss_n <= '1'; + INT_sclk <= cpol; + rx <= rxBuffer; + busy <= '0'; + if (flag_tx = '1') then + wr_ack <= '1'; + end if; + state <= init; + end if; + end if; + + end case; + end if; + end process; + +end architecture behavioural; diff --git a/src/Top.vhd b/src/Top.vhd new file mode 100644 index 0000000..fd94d24 --- /dev/null +++ b/src/Top.vhd @@ -0,0 +1,211 @@ +---------------------------------------------------------------------------------- +-- Company: +-- Engineer: +-- Create Date: +-- Design Name: +-- Module Name: Top - Behavioral +-- Project Name: AD7766_prj +-- Target Devices: Gowin GW5A-25 +-- Description: +-- Регистратор AD7766: MCLK 1.000 МГц -> ODR 125 kSPS -> дециматор sinc^1 /10 +-- -> 12.5 кГц -> кадры по 50 отсчётов -> UART 921600. +-- Параметры и обоснования — в docs/design.md. +---------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity Top is + generic ( + SYS_CLK_HZ : integer := 50_000_000; + BAUD_RATE : integer := 921600 + ); + port ( + clk : in std_logic; + + -- АЦП + ADC_SDO : in std_logic; + ADC_DRDY : in std_logic; + ADC_SCLK : out std_logic; + ADC_MCLK : out std_logic; + ADC_SDI : out std_logic; + ADC_CS_N : out std_logic; + ADC_SYNC_PD : out std_logic; + + -- UART на внешний USB-мост + UART_TX : out std_logic; + + LED : out std_logic + ); +end entity Top; + +architecture Behavioral of Top is + + signal rst_n : std_logic := '1'; + + signal sample : std_logic_vector(23 downto 0); + signal sample_valid : std_logic; + signal err_overrun : std_logic; + + signal dec_data : std_logic_vector(23 downto 0); + signal dec_valid : std_logic; + + signal uart_ena : std_logic; + signal uart_data : std_logic_vector(7 downto 0); + signal uart_busy : std_logic; + + signal spi_ena : std_logic; + signal spi_busy : std_logic; + signal spi_tx_data : std_logic_vector(23 downto 0); + signal spi_rx : std_logic_vector(23 downto 0); + + signal fifo_q : std_logic_vector(23 downto 0); + signal fifo_rd_en : std_logic; + signal fifo_dready : std_logic; + signal fifo_full : std_logic; + +begin + + rst_n <= '1'; + + spi_master_inst : entity work.spi_master(behavioural) + generic map( + data_length => 24, + sclk_half => 4 -- 50 МГц / 8 = 6.25 МГц + ) + port map + ( + clk => clk, + reset_n => rst_n, + enable => spi_ena, + cpol => '1', -- SCLK в покое высокий + cpha => '0', -- MSB берётся по спаду CS + miso => ADC_SDO, + sclk => ADC_SCLK, + ss_n => ADC_CS_N, + mosi => open, + busy => spi_busy, + wr_ack => open, + flag_tx => '0', + tx => spi_tx_data, + rx => spi_rx + ); + + spi_ctrl_inst : entity work.spi_ctrl(rtl) + generic map( + MCLK_HALF => 25, -- 50 МГц / 50 = 1.000 МГц + SETTLING_MCLK => 590, -- tSETTLING для AD7766 + SYNC_LOW_MCLK => 8 + ) + port map + ( + clk => clk, + rst_n => rst_n, + adc_mclk => ADC_MCLK, + adc_sync_pd_n => ADC_SYNC_PD, + adc_sdi => ADC_SDI, + adc_drdy => ADC_DRDY, + spi_busy => spi_busy, + spi_ena => spi_ena, + spi_tx_data => spi_tx_data, + spi_rx => spi_rx, + sample => sample, + sample_valid => sample_valid, + ready => open, + err_overrun => err_overrun + ); + + decimator_inst : entity work.decimator(rtl) + generic map( + DECIM => 10, + RECIP_M => 429496730, + RECIP_SH => 32 + ) + port map + ( + clk => clk, + rst_n => rst_n, + din => sample, + din_valid => sample_valid, + dout => dec_data, + dout_valid => dec_valid + ); + + fifo_top_inst : entity work.fifo_top(rtl) + generic map( + DWIDTH => 24, + DEPTH => 128, + AF_LEVEL => 50 -- порог начала кадра + ) + port map + ( + Data => dec_data, + WrClk => clk, + RdClk => clk, + WrEn => dec_valid, + RdEn => fifo_rd_en, + Q => fifo_q, + Empty => open, + Almost_Full => fifo_dready, + Full => fifo_full + ); + + packets_creator_inst : entity work.Packets_creator(Behavioral) + generic map( + SAMPLES_PER_FRAME => 50 + ) + port map + ( + clk => clk, + rst_n => rst_n, + data_ADC => fifo_q, + dready => fifo_dready, + rd_en => fifo_rd_en, + wr_en => dec_valid, + fifo_full => fifo_full, + err_overrun => err_overrun, + busy_uart => uart_busy, + ena_uart => uart_ena, + dout => uart_data, + fault => open + ); + + uart_inst : entity work.uart(logic) + generic map( + clk_freq => SYS_CLK_HZ, + baud_rate => BAUD_RATE, + os_rate => 16, + d_width => 8, + parity => 0, + parity_eo => '0' + ) + port map + ( + clk => clk, + reset_n => rst_n, + tx_ena => uart_ena, + tx_data => uart_data, + rx => '1', + rx_busy => open, + rx_error => open, + rx_data => open, + tx_busy => uart_busy, + tx => UART_TX + ); + + led_status_inst : entity work.led_status(rtl) + generic map( + SYS_CLK_HZ => SYS_CLK_HZ, + SLOW_HZ => 2, + FAST_HZ => 10, + ALIVE_MS => 200 + ) + port map + ( + clk => clk, + data_event => sample_valid, + led => LED + ); + + +end architecture Behavioral; diff --git a/src/UART.vhd b/src/UART.vhd new file mode 100644 index 0000000..2b66ef0 --- /dev/null +++ b/src/UART.vhd @@ -0,0 +1,198 @@ +-------------------------------------------------------------------------------- +-- +-- FileName: uart.vhd +-- Dependencies: none +-- Design Software: Quartus II 64-bit Version 13.1.0 Build 162 SJ Web Edition +-- +-- HDL CODE IS PROVIDED "AS IS." DIGI-KEY EXPRESSLY DISCLAIMS ANY +-- WARRANTY OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +-- PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL DIGI-KEY +-- BE LIABLE FOR ANY INCIDENTAL, SPECIAL, INDIRECT OR CONSEQUENTIAL +-- DAMAGES, LOST PROFITS OR LOST DATA, HARM TO YOUR EQUIPMENT, COST OF +-- PROCUREMENT OF SUBSTITUTE GOODS, TECHNOLOGY OR SERVICES, ANY CLAIMS +-- BY THIRD PARTIES (INCLUDING BUT NOT LIMITED TO ANY DEFENSE THEREOF), +-- ANY CLAIMS FOR INDEMNITY OR CONTRIBUTION, OR OTHER SIMILAR COSTS. +-- +-- Version History +-- Version 1.0 5/26/2017 Scott Larson +-- Initial Public Release +-- Version 1.1 8/3/2021 Scott Larson +-- Corrected rx start bit error checking +-- +-------------------------------------------------------------------------------- + +LIBRARY ieee; +USE ieee.std_logic_1164.all; + +ENTITY uart IS + GENERIC( + clk_freq : INTEGER := 50_000_000; --frequency of system clock in Hertz + baud_rate : INTEGER := 921600; --data link baud rate in bits/second + os_rate : INTEGER := 16; --oversampling rate to find center of receive bits (in samples per baud period) + d_width : INTEGER := 8; --data bus width + parity : INTEGER := 0; --0 for no parity, 1 for parity + parity_eo : STD_LOGIC := '0'); --'0' for even, '1' for odd parity + PORT( + clk : IN STD_LOGIC; --system clock + reset_n : IN STD_LOGIC; --ascynchronous reset + tx_ena : IN STD_LOGIC; --initiate transmission + tx_data : IN STD_LOGIC_VECTOR(d_width-1 DOWNTO 0); --data to transmit + rx : IN STD_LOGIC; --receive pin + rx_busy : OUT STD_LOGIC; --data reception in progress + rx_error : OUT STD_LOGIC; --start, parity, or stop bit error detected + rx_data : OUT STD_LOGIC_VECTOR(d_width-1 DOWNTO 0); --data received + tx_busy : OUT STD_LOGIC; --transmission in progress + tx : OUT STD_LOGIC); --transmit pin +END uart; + +ARCHITECTURE logic OF uart IS + TYPE tx_machine IS(idle, transmit); --tranmit state machine data type + TYPE rx_machine IS(idle, receive); --receive state machine data type + SIGNAL tx_state : tx_machine; --transmit state machine + SIGNAL rx_state : rx_machine; --receive state machine + SIGNAL baud_pulse : STD_LOGIC := '0'; --periodic pulse that occurs at the baud rate + SIGNAL os_pulse : STD_LOGIC := '0'; --periodic pulse that occurs at the oversampling rate + SIGNAL parity_error : STD_LOGIC; --receive parity error flag + SIGNAL rx_parity : STD_LOGIC_VECTOR(d_width DOWNTO 0); --calculation of receive parity + SIGNAL tx_parity : STD_LOGIC_VECTOR(d_width DOWNTO 0); --calculation of transmit parity + SIGNAL rx_buffer : STD_LOGIC_VECTOR(parity+d_width DOWNTO 0) := (OTHERS => '0'); --values received + SIGNAL tx_buffer : STD_LOGIC_VECTOR(parity+d_width+1 DOWNTO 0) := (OTHERS => '1'); --values to be transmitted +BEGIN + + --generate clock enable pulses at the baud rate and the oversampling rate + PROCESS(reset_n, clk) + VARIABLE count_baud : INTEGER RANGE 0 TO clk_freq/baud_rate-1 := 0; --counter to determine baud rate period + VARIABLE count_os : INTEGER RANGE 0 TO clk_freq/baud_rate/os_rate-1 := 0; --counter to determine oversampling period + BEGIN + IF(reset_n = '0') THEN --asynchronous reset asserted + baud_pulse <= '0'; --reset baud rate pulse + os_pulse <= '0'; --reset oversampling rate pulse + count_baud := 0; --reset baud period counter + count_os := 0; --reset oversampling period counter + ELSIF(clk'EVENT AND clk = '1') THEN + --create baud enable pulse + IF(count_baud < clk_freq/baud_rate-1) THEN --baud period not reached + count_baud := count_baud + 1; --increment baud period counter + baud_pulse <= '0'; --deassert baud rate pulse + ELSE --baud period reached + count_baud := 0; --reset baud period counter + baud_pulse <= '1'; --assert baud rate pulse + count_os := 0; --reset oversampling period counter to avoid cumulative error + END IF; + --create oversampling enable pulse + IF(count_os < clk_freq/baud_rate/os_rate-1) THEN --oversampling period not reached + count_os := count_os + 1; --increment oversampling period counter + os_pulse <= '0'; --deassert oversampling rate pulse + ELSE --oversampling period reached + count_os := 0; --reset oversampling period counter + os_pulse <= '1'; --assert oversampling pulse + END IF; + END IF; + END PROCESS; + + --receive state machine + PROCESS(reset_n, clk) + VARIABLE rx_count : INTEGER RANGE 0 TO parity+d_width+2 := 0; --count the bits received + VARIABLE os_count : INTEGER RANGE 0 TO os_rate-1 := 0; --count the oversampling rate pulses + BEGIN + IF(reset_n = '0') THEN --asynchronous reset asserted + os_count := 0; --clear oversampling pulse counter + rx_count := 0; --clear receive bit counter + rx_busy <= '0'; --clear receive busy signal + rx_error <= '0'; --clear receive errors + rx_data <= (OTHERS => '0'); --clear received data output + rx_state <= idle; --put in idle state + ELSIF(clk'EVENT AND clk = '1' AND os_pulse = '1') THEN --enable clock at oversampling rate + CASE rx_state IS + WHEN idle => --idle state + rx_busy <= '0'; --clear receive busy flag + IF(rx = '0') THEN --start bit might be present + IF(os_count < os_rate/2) THEN --oversampling pulse counter is not at start bit center + os_count := os_count + 1; --increment oversampling pulse counter + rx_state <= idle; --remain in idle state + ELSE --oversampling pulse counter is at bit center + os_count := 0; --clear oversampling pulse counter + rx_count := 0; --clear the bits received counter + rx_busy <= '1'; --assert busy flag + rx_buffer <= rx & rx_buffer(parity+d_width DOWNTO 1); --shift the start bit into receive buffer + rx_state <= receive; --advance to receive state + END IF; + ELSE --start bit not present + os_count := 0; --clear oversampling pulse counter + rx_state <= idle; --remain in idle state + END IF; + WHEN receive => --receive state + IF(os_count < os_rate-1) THEN --not center of bit + os_count := os_count + 1; --increment oversampling pulse counter + rx_state <= receive; --remain in receive state + ELSIF(rx_count < parity+d_width) THEN --center of bit and not all bits received + os_count := 0; --reset oversampling pulse counter + rx_count := rx_count + 1; --increment number of bits received counter + rx_buffer <= rx & rx_buffer(parity+d_width DOWNTO 1); --shift new received bit into receive buffer + rx_state <= receive; --remain in receive state + ELSE --center of stop bit + rx_data <= rx_buffer(d_width DOWNTO 1); --output data received to user logic + rx_error <= rx_buffer(0) OR parity_error OR NOT rx; --output start, parity, and stop bit error flag + rx_busy <= '0'; --deassert received busy flag + rx_state <= idle; --return to idle state + END IF; + END CASE; + END IF; + END PROCESS; + + --receive parity calculation logic + rx_parity(0) <= parity_eo; + rx_parity_logic: FOR i IN 0 to d_width-1 GENERATE + rx_parity(i+1) <= rx_parity(i) XOR rx_buffer(i+1); + END GENERATE; + WITH parity SELECT --compare calculated parity bit with received parity bit to determine error + parity_error <= rx_parity(d_width) XOR rx_buffer(parity+d_width) WHEN 1, --using parity + '0' WHEN OTHERS; --not using parity + + --transmit state machine + PROCESS(reset_n, clk) + VARIABLE tx_count : INTEGER RANGE 0 TO parity+d_width+3 := 0; --count bits transmitted + BEGIN + IF(reset_n = '0') THEN --asynchronous reset asserted + tx_count := 0; --clear transmit bit counter + tx <= '1'; --set tx pin to idle value of high + tx_busy <= '1'; --set transmit busy signal to indicate unavailable + tx_state <= idle; --set tx state machine to ready state + ELSIF(clk'EVENT AND clk = '1') THEN + CASE tx_state IS + WHEN idle => --idle state + IF(tx_ena = '1') THEN --new transaction latched in + tx_buffer(d_width+1 DOWNTO 0) <= tx_data & '0' & '1'; --latch in data for transmission and start/stop bits + IF(parity = 1) THEN --if parity is used + tx_buffer(parity+d_width+1) <= tx_parity(d_width); --latch in parity bit from parity logic + END IF; + tx_busy <= '1'; --assert transmit busy flag + tx_count := 0; --clear transmit bit count + tx_state <= transmit; --proceed to transmit state + ELSE --no new transaction initiated + tx_busy <= '0'; --clear transmit busy flag + tx_state <= idle; --remain in idle state + END IF; + WHEN transmit => --transmit state + IF(baud_pulse = '1') THEN --beginning of bit + tx_count := tx_count + 1; --increment transmit bit counter + tx_buffer <= '1' & tx_buffer(parity+d_width+1 DOWNTO 1); --shift transmit buffer to output next bit + END IF; + IF(tx_count < parity+d_width+3) THEN --not all bits transmitted + tx_state <= transmit; --remain in transmit state + ELSE --all bits transmitted + tx_state <= idle; --return to idle state + END IF; + END CASE; + tx <= tx_buffer(0); --output last bit in transmit transaction buffer + END IF; + END PROCESS; + + --transmit parity calculation logic + tx_parity(0) <= parity_eo; + tx_parity_logic: FOR i IN 0 to d_width-1 GENERATE + tx_parity(i+1) <= tx_parity(i) XOR tx_data(i); + END GENERATE; + +END logic; diff --git a/src/decimator.vhd b/src/decimator.vhd new file mode 100644 index 0000000..80b5074 --- /dev/null +++ b/src/decimator.vhd @@ -0,0 +1,126 @@ +---------------------------------------------------------------------------------- +-- Company: +-- Engineer: +-- Create Date: +-- Design Name: +-- Module Name: decimator - Behavioral +-- Project Name: AD7766_prj +-- Target Devices: Gowin GW5A-25 +-- Description: +-- Дециматор sinc^1: среднее по DECIM отсчётам. Понижает 125 kSPS с АЦП +-- до целевого 12.5 кГц +-- АЧХ (DECIM=10, вход 125 кГц): -0.09 дБ на 1 кГц, -0.83 дБ на 3 кГц, +-- -3.9 дБ на 6.25 кГц. +---------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity decimator is + generic ( + DECIM : integer := 10; + -- Деление на DECIM выполняется умножением на обратную величину: + -- x / DECIM = (x * RECIP_M) >> RECIP_SH, RECIP_M = ceil(2^RECIP_SH / DECIM) + -- Для DECIM=10 точность проверена перебором всего диапазона сумм (до 10*2^23). + -- При смене DECIM обе константы пересчитать. + RECIP_M : integer := 429496730; -- ceil(2^32 / 10) = 0x1999999A + RECIP_SH : integer := 32 + ); + port ( + clk : in std_logic; + rst_n : in std_logic; + + din : in std_logic_vector(23 downto 0); + din_valid : in std_logic; + + dout : out std_logic_vector(23 downto 0); + dout_valid : out std_logic + ); +end entity decimator; + +architecture rtl of decimator is + + -- Сумма DECIM отсчётов по 24 бита: 24 + ceil(log2(DECIM)) = 28 бит + constant ACC_W : integer := 28; + constant M_W : integer := 30; -- разрядность RECIP_M + + -- Начальные значения заданы явно: сброса в проекте нет (см. top.vhd) + signal acc : signed(ACC_W - 1 downto 0) := (others => '0'); + signal cnt : integer range 0 to DECIM - 1 := 0; + + signal sum_r : signed(ACC_W - 1 downto 0) := (others => '0'); + signal sum_vld : std_logic := '0'; + + signal mag : unsigned(ACC_W - 1 downto 0) := (others => '0'); + signal neg : std_logic := '0'; + signal mag_vld : std_logic := '0'; + + signal prod : unsigned(ACC_W + M_W - 1 downto 0) := (others => '0'); + signal neg_d : std_logic := '0'; + signal prod_vld : std_logic := '0'; + +begin + + acc_proc : process (clk, rst_n) + begin + if rst_n = '0' then + acc <= (others => '0'); + cnt <= 0; + sum_r <= (others => '0'); + sum_vld <= '0'; + elsif rising_edge(clk) then + sum_vld <= '0'; + if din_valid = '1' then + if cnt = DECIM - 1 then + sum_r <= acc + resize(signed(din), ACC_W); + sum_vld <= '1'; + acc <= (others => '0'); + cnt <= 0; + else + acc <= acc + resize(signed(din), ACC_W); + cnt <= cnt + 1; + end if; + end if; + end if; + end process acc_proc; + + -- Делим модуль и возвращаем знак: усечение получается симметричным + -- относительно нуля, без смещения на переходе через ноль + div_proc : process (clk, rst_n) + variable q : unsigned(ACC_W + M_W - 1 - RECIP_SH downto 0); + begin + if rst_n = '0' then + mag <= (others => '0'); + neg <= '0'; + mag_vld <= '0'; + prod <= (others => '0'); + neg_d <= '0'; + prod_vld <= '0'; + dout <= (others => '0'); + dout_valid <= '0'; + elsif rising_edge(clk) then + mag_vld <= sum_vld; + prod_vld <= mag_vld; + dout_valid <= prod_vld; + + if sum_r(ACC_W - 1) = '1' then + mag <= unsigned(-sum_r); + neg <= '1'; + else + mag <= unsigned(sum_r); + neg <= '0'; + end if; + + prod <= mag * to_unsigned(RECIP_M, M_W); + neg_d <= neg; + + q := prod(prod'high downto RECIP_SH); + if neg_d = '1' then + dout <= std_logic_vector(resize(-signed('0' & q), 24)); + else + dout <= std_logic_vector(resize(signed('0' & q), 24)); + end if; + end if; + end process div_proc; + +end architecture rtl; diff --git a/src/fifo_top.vhd b/src/fifo_top.vhd new file mode 100644 index 0000000..7a8d910 --- /dev/null +++ b/src/fifo_top.vhd @@ -0,0 +1,104 @@ +---------------------------------------------------------------------------------- +-- Company: +-- Engineer: +-- Create Date: +-- Design Name: +-- Module Name: fifo_top - Behavioral +-- Project Name: AD7766_prj +-- Target Devices: Gowin GW5A-25 +-- Description: +---------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity fifo_top is + generic ( + DWIDTH : natural := 24; -- разрядность слова + DEPTH : natural := 128; -- глубина + AF_LEVEL : natural := 50 -- порог Almost_Full + ); + port ( + Data : in std_logic_vector(DWIDTH - 1 downto 0); + WrClk : in std_logic; + RdClk : in std_logic; + WrEn : in std_logic; + RdEn : in std_logic; + Q : out std_logic_vector(DWIDTH - 1 downto 0); + Empty : out std_logic; + Almost_Full : out std_logic; + Full : out std_logic + ); +end entity fifo_top; + +architecture rtl of fifo_top is + + type mem_t is array (0 to DEPTH - 1) of std_logic_vector(DWIDTH - 1 downto 0); + signal mem : mem_t; + + signal wr_ptr : integer range 0 to DEPTH - 1 := 0; + signal rd_ptr : integer range 0 to DEPTH - 1 := 0; + signal count : integer range 0 to DEPTH := 0; + + signal full_i : std_logic; + signal empty_i : std_logic; + signal wr_ok : std_logic; + signal rd_ok : std_logic; + +begin + + full_i <= '1' when count = DEPTH else '0'; + empty_i <= '1' when count = 0 else '0'; + + Full <= full_i; + Empty <= empty_i; + Almost_Full <= '1' when count >= AF_LEVEL else '0'; + + -- Запись при полном буфере и чтение при пустом игнорируются, как у ядра + wr_ok <= WrEn and not full_i; + rd_ok <= RdEn and not empty_i; + + -- Содержимое памяти намеренно не сбрасывается: сброс массива не даёт + -- синтезатору вывести блочную память + mem_proc : process (WrClk) + begin + if rising_edge(WrClk) then + if wr_ok = '1' then + mem(wr_ptr) <= Data; + end if; + -- Q меняется только по состоявшемуся чтению: со следующего такта + -- после RdEn на нём лежит очередное слово + if rd_ok = '1' then + Q <= mem(rd_ptr); + end if; + end if; + end process mem_proc; + + ptr_proc : process (WrClk) + begin + if rising_edge(WrClk) then + if wr_ok = '1' then + if wr_ptr = DEPTH - 1 then + wr_ptr <= 0; + else + wr_ptr <= wr_ptr + 1; + end if; + end if; + + if rd_ok = '1' then + if rd_ptr = DEPTH - 1 then + rd_ptr <= 0; + else + rd_ptr <= rd_ptr + 1; + end if; + end if; + + if wr_ok = '1' and rd_ok = '0' then + count <= count + 1; + elsif wr_ok = '0' and rd_ok = '1' then + count <= count - 1; + end if; + end if; + end process ptr_proc; + +end architecture rtl; diff --git a/src/led_status.vhd b/src/led_status.vhd new file mode 100644 index 0000000..8579e88 --- /dev/null +++ b/src/led_status.vhd @@ -0,0 +1,77 @@ +---------------------------------------------------------------------------------- +-- Company: +-- Engineer: +-- Create Date: +-- Design Name: +-- Module Name: led_status - Behavioral +-- Project Name: AD7766_prj +-- Target Devices: Gowin GW5A-25 +-- Description: Индикация состояния тракта. +-- не меняется — тактирования на clk нет вообще +-- медленно — тактирование есть, отсчёты от АЦП не приходят +-- быстро — АЦП отвечает, отсчёты идут. Значит как минимум работает DRDY ADC. +-- +-- Сброс намеренно не используется: мигание должно доказывать наличие клока +-- даже если остальной тракт по какой-то причине стоит. +---------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; + +entity led_status is + generic ( + SYS_CLK_HZ : integer := 50_000_000; + SLOW_HZ : integer := 2; -- нет отсчётов + FAST_HZ : integer := 10; -- отсчёты идут + ALIVE_MS : integer := 200 -- за какое время без отсчётов считаем, что их нет + ); + port ( + clk : in std_logic; + data_event : in std_logic; -- строб прихода отсчёта + led : out std_logic + ); +end entity led_status; + +architecture rtl of led_status is + + constant HALF_SLOW : integer := SYS_CLK_HZ / (2 * SLOW_HZ); + constant HALF_FAST : integer := SYS_CLK_HZ / (2 * FAST_HZ); + constant ALIVE_TMO : integer := (SYS_CLK_HZ / 1000) * ALIVE_MS; + + signal alive_cnt : integer range 0 to ALIVE_TMO := 0; + signal data_alive : std_logic := '0'; + signal blk_cnt : integer range 0 to HALF_SLOW - 1 := 0; + signal blink : std_logic := '0'; + +begin + + led <= blink; + + led_proc : process (clk) + variable half : integer range 0 to HALF_SLOW; + begin + if rising_edge(clk) then + if data_event = '1' then + alive_cnt <= 0; + data_alive <= '1'; + elsif alive_cnt = ALIVE_TMO then + data_alive <= '0'; + else + alive_cnt <= alive_cnt + 1; + end if; + + if data_alive = '1' then + half := HALF_FAST; + else + half := HALF_SLOW; + end if; + + if blk_cnt >= half - 1 then + blk_cnt <= 0; + blink <= not blink; + else + blk_cnt <= blk_cnt + 1; + end if; + end if; + end process led_proc; + +end architecture rtl; diff --git a/tb/ad7766_model.vhd b/tb/ad7766_model.vhd new file mode 100644 index 0000000..3b2901a --- /dev/null +++ b/tb/ad7766_model.vhd @@ -0,0 +1,93 @@ +---------------------------------------------------------------------------------- +-- Module Name: ad7766_model +-- Project Name: AD7766_prj +-- Description: +-- Поведенческая модель AD7766 для симуляции. Не синтезируется. +-- Воспроизводит: сброс фильтра по SYNC/PD, задержку tSETTLING, период DRDY +-- в 8 периодов MCLK, выдачу MSB по спаду CS и остальных бит по спадам SCLK. +-- Времена tN — по AD7766 Data Sheet Rev. D, Table 3. +---------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity ad7766_model is + generic ( + SETTLING_MCLK : integer := 590; -- tSETTLING для AD7766, Table 7 + ODR_DIV : integer := 8; -- ODR = MCLK/8 для AD7766, Table 6 + T4 : time := 265 ns; + T5 : time := 294 ns; + T7 : time := 6 ns; + T8 : time := 24 ns + ); + port ( + mclk : in std_logic; + sync_pd_n : in std_logic; + cs_n : in std_logic; + sclk : in std_logic; + next_word : in std_logic_vector(23 downto 0); + + sdo : out std_logic; + drdy : out std_logic; + word_tick : out std_logic -- строб: слово защёлкнуто в выходной регистр + ); +end entity ad7766_model; + +architecture model of ad7766_model is + signal out_reg : std_logic_vector(23 downto 0) := (others => '0'); + signal bit_idx : integer range 0 to 23 := 23; + signal settled : std_logic := '0'; + signal s_cnt : integer := 0; + signal div : integer range 0 to ODR_DIV - 1 := 0; +begin + + drdy_proc : process (mclk, sync_pd_n) + begin + if sync_pd_n = '0' then + -- SYNC/PD низкий: выходной регистр недействителен, DRDY уходит вверх + drdy <= '1'; + settled <= '0'; + s_cnt <= 0; + div <= 0; + word_tick <= '0'; + elsif rising_edge(mclk) then + word_tick <= '0'; + if settled = '0' then + if s_cnt = SETTLING_MCLK - 1 then + settled <= '1'; + div <= 0; + out_reg <= next_word; + word_tick <= '1'; + drdy <= '1' after T4, '0' after T4 + T5; + else + s_cnt <= s_cnt + 1; + end if; + else + if div = ODR_DIV - 1 then + div <= 0; + out_reg <= next_word; + word_tick <= '1'; + drdy <= '1' after T4, '0' after T4 + T5; + else + div <= div + 1; + end if; + end if; + end if; + end process drdy_proc; + + -- MSB встаёт на SDO по спаду CS, остальные биты — спадами SCLK (стр. 17) + shift_proc : process (cs_n, sclk) + begin + if cs_n = '1' then + bit_idx <= 23; + elsif falling_edge(sclk) then + if bit_idx > 0 then + bit_idx <= bit_idx - 1; + end if; + end if; + end process shift_proc; + + sdo <= 'Z' after T7 when cs_n = '1' else + out_reg(bit_idx) after T8; + +end architecture model; diff --git a/tb/tb_top.vhd b/tb/tb_top.vhd new file mode 100644 index 0000000..03e0848 --- /dev/null +++ b/tb/tb_top.vhd @@ -0,0 +1,223 @@ +---------------------------------------------------------------------------------- +-- Module Name: tb_top +-- Project Name: AD7766_prj +-- Description: +-- Сквозной тест: модель AD7766 -> top -> UART -> разбор кадра в тестбенче. +-- Модель выдаёт группами по DECIM одинаковых слов, поэтому усреднённый +-- отсчёт обязан в точности совпасть со значением группы. Проверяются +-- разрядность, знак, порядок байт, CRC и счётчик кадров. +---------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use std.env.all; + +entity tb_top is + generic ( + FRAMES : integer := 2 -- сколько кадров проверяем, переопределяется -gFRAMES=N + ); +end entity tb_top; + +architecture sim of tb_top is + + constant CLK_T : time := 20 ns; + constant BAUD : integer := 921600; + constant BIT_T : time := 1 sec / BAUD; + constant DECIM : integer := 10; + constant SPF : integer := 50; -- отсчётов в кадре + + constant PAYLOAD_LEN : integer := 2 + 1 + SPF * 3; -- CNT + STATUS + DATA + + signal clk : std_logic := '0'; + signal run : boolean := true; + + signal adc_sdo : std_logic; + signal adc_drdy : std_logic; + signal adc_sclk : std_logic; + signal adc_mclk : std_logic; + signal adc_sdi : std_logic; + signal adc_cs_n : std_logic; + signal adc_sync_pd : std_logic; + signal uart_tx : std_logic; + signal led : std_logic; + + signal next_word : std_logic_vector(23 downto 0); + signal word_tick : std_logic; + signal word_idx : integer := 0; + + type byte_arr_t is array (natural range <>) of std_logic_vector(7 downto 0); + + -- Значения групп: крайние точки шкалы, единицы младшего разряда и оба знака + function grp_value(g : integer) return signed is + variable v : integer; + begin + case g mod 8 is + when 0 => v := 0; + when 1 => v := 8388607; -- +полная шкала + when 2 => v := - 8388608; -- -полная шкала + when 3 => v := 1; + when 4 => v := - 1; + when 5 => v := 5592405; -- 0x555555 + when 6 => v := - 5592406; -- 0xAAAAAA как знаковое + when others => v := 1193046; -- 0x123456 + end case; + return to_signed(v, 24); + end function; + + function crc16_upd(crc : std_logic_vector(15 downto 0); + d : std_logic_vector(7 downto 0)) + return std_logic_vector is + variable c : std_logic_vector(15 downto 0) := crc; + variable fb : std_logic; + begin + for i in 7 downto 0 loop + fb := c(15) xor d(i); + c := c(14 downto 0) & '0'; + if fb = '1' then + c := c xor x"1021"; + end if; + end loop; + return c; + end function; + + procedure get_byte(signal rx : in std_logic; + b : out std_logic_vector(7 downto 0)) is + variable v : std_logic_vector(7 downto 0); + begin + wait until rx = '0'; -- фронт старт-бита + wait for BIT_T / 2; + assert rx = '0' report "start bit not confirmed" severity error; + for i in 0 to 7 loop + wait for BIT_T; + v(i) := rx; + end loop; + wait for BIT_T; + assert rx = '1' report "stop bit not at expected position" severity error; + b := v; + end procedure; + +begin + + clk <= not clk after CLK_T / 2 when run else '0'; + + next_word <= std_logic_vector(grp_value(word_idx / DECIM)); + + word_cnt_proc : process + begin + wait until rising_edge(word_tick); + word_idx <= word_idx + 1; + end process word_cnt_proc; + + dut : entity work.Top(Behavioral) + generic map( + SYS_CLK_HZ => 50_000_000, + BAUD_RATE => BAUD + ) + port map + ( + clk => clk, + ADC_SDO => adc_sdo, + ADC_DRDY => adc_drdy, + ADC_SCLK => adc_sclk, + ADC_MCLK => adc_mclk, + ADC_SDI => adc_sdi, + ADC_CS_N => adc_cs_n, + ADC_SYNC_PD => adc_sync_pd, + UART_TX => uart_tx, + LED => led + ); + + adc : entity work.ad7766_model(model) + port map + ( + mclk => adc_mclk, + sync_pd_n => adc_sync_pd, + cs_n => adc_cs_n, + sclk => adc_sclk, + next_word => next_word, + sdo => adc_sdo, + drdy => adc_drdy, + word_tick => word_tick + ); + + check_proc : process + variable b : std_logic_vector(7 downto 0); + variable prev : std_logic_vector(7 downto 0) := (others => '0'); + variable pl : byte_arr_t(0 to PAYLOAD_LEN - 1); + variable crc : std_logic_vector(15 downto 0); + variable crc_rx : std_logic_vector(15 downto 0); + variable cnt_rx : integer; + variable got : signed(23 downto 0); + variable want : signed(23 downto 0); + variable errors : integer := 0; + variable frame_no : integer := 0; + begin + report "start: waiting for first frame"; + + while frame_no < FRAMES loop + + -- Ищем SYNC A5 5A, как это будет делать приёмник на ПК + loop + get_byte(uart_tx, b); + exit when prev = x"A5" and b = x"5A"; + prev := b; + end loop; + prev := (others => '0'); + + for i in 0 to PAYLOAD_LEN - 1 loop + get_byte(uart_tx, b); + pl(i) := b; + end loop; + get_byte(uart_tx, b); + crc_rx(15 downto 8) := b; + get_byte(uart_tx, b); + crc_rx(7 downto 0) := b; + + crc := x"FFFF"; + for i in 0 to PAYLOAD_LEN - 1 loop + crc := crc16_upd(crc, pl(i)); + end loop; + + if crc /= crc_rx then + report "frame " & integer'image(frame_no) & ": CRC mismatch" severity error; + errors := errors + 1; + end if; + + cnt_rx := to_integer(unsigned(std_logic_vector'(pl(0) & pl(1)))); + if cnt_rx /= frame_no then + report "frame " & integer'image(frame_no) & ": FRAME_CNT = " & + integer'image(cnt_rx) severity error; + errors := errors + 1; + end if; + + if pl(2) /= x"00" then + report "frame " & integer'image(frame_no) & ": STATUS not zero" severity error; + errors := errors + 1; + end if; + + for i in 0 to SPF - 1 loop + got := signed(std_logic_vector'(pl(3 + i * 3) & pl(4 + i * 3) & pl(5 + i * 3))); + want := grp_value(frame_no * SPF + i); + if got /= want then + report "frame " & integer'image(frame_no) & " sample " & integer'image(i) & + ": got " & integer'image(to_integer(got)) & + ", want " & integer'image(to_integer(want)) severity error; + errors := errors + 1; + end if; + end loop; + + report "frame " & integer'image(frame_no) & " parsed OK"; + frame_no := frame_no + 1; + end loop; + + if errors = 0 then + report "TEST PASSED: frames = " & integer'image(FRAMES) & ""; + else + report "TEST FAILED: errors = " & integer'image(errors) severity error; + end if; + + run <= false; + stop; + end process check_proc; + +end architecture sim; diff --git a/tools/uart_capture.py b/tools/uart_capture.py new file mode 100644 index 0000000..97f5f0d --- /dev/null +++ b/tools/uart_capture.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +"""Приём кадров AD7766 по UART. + +Пишет два файла: + .raw все байты из порта, до разбора -- отладка канала + .bin только отсчёты, 3 байта BE -- читается read_24bit_bin() + из F:\\PyPrj\\Signal_analayzer + +Кадр с несошедшейся CRC отбрасывается, пропуск восстанавливается по FRAME_CNT +и добивается нулями, чтобы не сдвинулась ось времени. +Формат кадра -- docs/design.md. +""" + +import argparse +import sys +import time + +import serial +from serial.tools import list_ports + +SYNC = b"\xA5\x5A" +SAMPLES_PER_FRAME = 50 +PAYLOAD_LEN = 2 + 1 + SAMPLES_PER_FRAME * 3 # FRAME_CNT + STATUS + DATA +FRAME_LEN = len(SYNC) + PAYLOAD_LEN + 2 # + CRC16 +FS_DEFAULT = 12500.0 +V_REF = 5.0 + + +def _make_crc_table(): + tbl = [] + for b in range(256): + c = b << 8 + for _ in range(8): + c = ((c << 1) ^ 0x1021) & 0xFFFF if c & 0x8000 else (c << 1) & 0xFFFF + tbl.append(c) + return tbl + + +_CRC_TBL = _make_crc_table() + + +def crc16_ccitt(data: bytes) -> int: + """CRC-16/CCITT, poly 0x1021, init 0xFFFF, MSB-first -- как в ПЛИС.""" + crc = 0xFFFF + for byte in data: + crc = ((crc << 8) & 0xFFFF) ^ _CRC_TBL[(crc >> 8) ^ byte] + return crc + + +class Stats: + def __init__(self): + self.frames_ok = 0 + self.crc_errors = 0 + self.resyncs = 0 + self.padded_frames = 0 + self.status_flags = 0 + self.samples = 0 + self.code_min = None + self.code_max = None + self.code_sum = 0 + self.code_sqsum = 0 + self.bits_or = 0 # какие биты хоть раз были 1 + self.bits_and = 0xFFFFFF # какие биты всегда были 1 + + +def decode_samples(data: bytes): + """150 байт -> 50 знаковых отсчётов.""" + out = [] + for i in range(0, len(data), 3): + v = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2] + if v & 0x800000: + v -= 0x1000000 + out.append(v) + return out + + +def open_port(port, baud): + ser = serial.Serial(port, baud, timeout=0.1) + try: + # Буфер COM-порта в Windows по умолчанию около 4 кБ -- это всего 100 мс + # потока, любая заминка в разборе стоит пачки байт + ser.set_buffer_size(rx_size=1 << 20, tx_size=1 << 12) + except Exception: + pass + ser.reset_input_buffer() + return ser + + +def run_dump(port, baud, duration, out_base): + """Только приём в файл, без разбора. Показывает, теряет ли байты сам канал.""" + raw_path = out_base + ".raw" + total = 0 + slowest = 0.0 + with open_port(port, baud) as ser, open(raw_path, "wb") as fraw: + t0 = time.time() + t_end = t0 + duration + while time.time() < t_end: + t1 = time.perf_counter() + chunk = ser.read(4096) + slowest = max(slowest, time.perf_counter() - t1) + if chunk: + fraw.write(chunk) + total += len(chunk) + elapsed = time.time() - t0 + + rate = total / elapsed + expect = FRAME_LEN * FS_DEFAULT / SAMPLES_PER_FRAME + print(f"принято : {total} байт за {elapsed:.2f} с") + print(f"скорость : {rate / 1000:.1f} кБ/с") + print(f"ожидалось : {expect / 1000:.1f} кБ/с ({FRAME_LEN} байт/кадр, " + f"{FS_DEFAULT / SAMPLES_PER_FRAME:.0f} кадров/с)") + print(f"дошло : {100.0 * rate / expect:.1f}%") + print(f"дольше всего ждали чтение: {slowest * 1000:.0f} мс") + if rate < expect * 0.95: + print("\nБайты теряются в канале, разбор тут ни при чём: " + "мост, драйвер или скорость порта.") + else: + print("\nКанал отдаёт весь поток. Значит потери возникают при разборе.") + return 0 + + +class FrameParser: + """Разбор потока кадров. Единственное место, где описан формат, -- + им пользуются и запись, и живой просмотр. + + feed() возвращает список (frame_cnt, status, data, gap): data -- 150 байт + отсчётов, gap -- сколько кадров потерялось перед этим. + """ + + def __init__(self): + self.buf = bytearray() + self.expected_cnt = None + self.frames_ok = 0 + self.crc_errors = 0 + self.resyncs = 0 + self.padded_frames = 0 + self.status_flags = 0 + + def feed(self, chunk): + out = [] + self.buf += chunk + while True: + idx = self.buf.find(SYNC) + if idx < 0: + # Оставляем хвост, в котором SYNC мог разорваться между чтениями + if len(self.buf) > 1: + del self.buf[:-1] + break + if idx > 0: + self.resyncs += 1 + del self.buf[:idx] + if len(self.buf) < FRAME_LEN: + break + + frame = bytes(self.buf[:FRAME_LEN]) + payload = frame[2:2 + PAYLOAD_LEN] + crc_rx = (frame[-2] << 8) | frame[-1] + + if crc16_ccitt(payload) != crc_rx: + self.crc_errors += 1 + # SYNC мог оказаться внутри данных: сдвигаемся и ищем дальше + del self.buf[:2] + continue + + del self.buf[:FRAME_LEN] + + cnt = (payload[0] << 8) | payload[1] + status = payload[2] + self.status_flags |= status + + gap = 0 + if self.expected_cnt is not None and cnt != self.expected_cnt: + gap = (cnt - self.expected_cnt) & 0xFFFF + self.padded_frames += gap + self.expected_cnt = (cnt + 1) & 0xFFFF + self.frames_ok += 1 + out.append((cnt, status, payload[3:], gap)) + return out + + +def run(port, baud, duration, out_base, quiet): + raw_path = out_base + ".raw" + bin_path = out_base + ".bin" + log_path = out_base + ".log" + + st = Stats() + log_lines = [] + + def log(msg): + log_lines.append(msg) + if not quiet: + print(msg) + + log(f"порт {port}, {baud} бод, длительность {duration} с") + + parser = FrameParser() + t_end = time.time() + duration + gaps_printed = 0 + + with open_port(port, baud) as ser, \ + open(raw_path, "wb") as fraw, open(bin_path, "wb") as fbin: + while time.time() < t_end: + chunk = ser.read(4096) + if not chunk: + continue + fraw.write(chunk) + + for cnt, status, data, gap in parser.feed(chunk): + if gap: + fbin.write(b"\x00" * (gap * SAMPLES_PER_FRAME * 3)) + st.samples += gap * SAMPLES_PER_FRAME + # Печатаем только первые сообщения: вывод в консоль медленный + # и на длинной записи сам становится причиной потерь + msg = (f"пропуск {gap} кадр(ов) перед FRAME_CNT={cnt}, " + f"добито {gap * SAMPLES_PER_FRAME} нулей") + log_lines.append(msg) + if gaps_printed < 10 and not quiet: + print(msg) + gaps_printed += 1 + + fbin.write(data) + st.samples += SAMPLES_PER_FRAME + + for v in decode_samples(data): + st.code_sum += v + st.code_sqsum += v * v + st.bits_or |= v & 0xFFFFFF + st.bits_and &= v & 0xFFFFFF + if st.code_min is None or v < st.code_min: + st.code_min = v + if st.code_max is None or v > st.code_max: + st.code_max = v + + st.frames_ok = parser.frames_ok + st.crc_errors = parser.crc_errors + st.resyncs = parser.resyncs + st.padded_frames = parser.padded_frames + st.status_flags = parser.status_flags + + log("") + log(f"кадров принято : {st.frames_ok}") + log(f"ошибок CRC : {st.crc_errors}") + log(f"ресинхронизаций : {st.resyncs}") + log(f"кадров добито нулями: {st.padded_frames}") + log(f"отсчётов в .bin : {st.samples} ({st.samples / FS_DEFAULT:.3f} с при {FS_DEFAULT:.0f} Гц)") + + if st.status_flags & 0x01: + log("STATUS: было переполнение FIFO в ПЛИС") + if st.status_flags & 0x02: + log("STATUS: был срыв чтения по DRDY") + if st.status_flags == 0 and st.frames_ok: + log("STATUS: чисто") + + if st.frames_ok: + scale = V_REF / (2 ** 23 - 1) + n = st.frames_ok * SAMPLES_PER_FRAME + mean = st.code_sum / n + var = max(st.code_sqsum / n - mean * mean, 0.0) + sigma = var ** 0.5 + log("") + log(f"код min/max/среднее : {st.code_min} / {st.code_max} / {mean:.1f}") + log(f"вольт min/max/сред. : {st.code_min * scale:+.6f} / " + f"{st.code_max * scale:+.6f} / {mean * scale:+.6f}") + log(f"размах : {(st.code_max - st.code_min) * scale:.6f} В " + f"(полная шкала {2 * V_REF:.1f} В)") + log(f"СКО шума : {sigma:.1f} код ({sigma * scale * 1e6:.2f} мкВ)") + + # Постоянные старшие биты -- норма: при малом размахе сигнала меняются + # только младшие. Тревожно обратное: залипшие младшие биты, которые + # обязан расшевелить хотя бы собственный шум АЦП. + frozen = ((~st.bits_or) | st.bits_and) & 0xFFFFFF + low_frozen = frozen & 0x0F + if frozen == 0xFFFFFF: + log("биты : НИ ОДИН из 24 не менялся — тракт отдаёт константу") + elif low_frozen: + log("биты : ВНИМАНИЕ, не менялись младшие " + ", ".join( + str(i) for i in range(3, -1, -1) if low_frozen >> i & 1) + + " — похоже на сбой чтения, а не на сигнал") + else: + top = 0 + for i in range(23, -1, -1): + if frozen >> i & 1: + top += 1 + else: + break + if top: + log(f"биты : младшие {24 - top} переключались, " + f"старшие {top} постоянны — норма при малом размахе") + else: + log("биты : все 24 переключались") + + with open(log_path, "w", encoding="utf-8") as flog: + flog.write("\n".join(log_lines) + "\n") + + return 1 if (st.crc_errors or st.status_flags or not st.frames_ok) else 0 + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--port", help="например COM5") + ap.add_argument("--baud", type=int, default=921600) + ap.add_argument("--seconds", type=float, default=10.0) + ap.add_argument("--out", default="capture", help="базовое имя файлов без расширения") + ap.add_argument("--quiet", action="store_true") + ap.add_argument("--list", action="store_true", help="показать доступные порты") + ap.add_argument("--dump", action="store_true", + help="только приём в .raw без разбора: проверка канала") + args = ap.parse_args() + + if args.list: + for p in list_ports.comports(): + print(f"{p.device} {p.description}") + return 0 + + if not args.port: + ap.error("нужен --port (список портов: --list)") + + if args.dump: + return run_dump(args.port, args.baud, args.seconds, args.out) + + return run(args.port, args.baud, args.seconds, args.out, args.quiet) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/uart_live.py b/tools/uart_live.py new file mode 100644 index 0000000..c03f662 --- /dev/null +++ b/tools/uart_live.py @@ -0,0 +1,421 @@ +#!/usr/bin/env python3 +"""Живой просмотр потока с AD7766: осциллограмма, спектр и счётчики. + +Нужен, чтобы глазами проконтролировать сигнал до того, как писать запись: +видно и форму, и шум, и наводки, и целостность кадров. + +Разбор кадров общий с uart_capture.py -- формат описан там в одном месте. +Запись сюда встроена не для удобства, а по необходимости: COM-порт держит +один процесс, и параллельно запустить uart_capture.py на том же порту нельзя. + +Управление -- кнопками внизу окна, те же действия продублированы клавишами: + r запись d убрать постоянную составляющую + a автомасштаб s спектр: плотность или амплитуда + f пауза q выход + +Зум и панорама -- штатной панелью инструментов matplotlib. Автомасштаб при +этом выключается сам, иначе он затирал бы выбранный масштаб на каждом кадре. +""" + +import argparse +import collections +import sys +import threading +import time + +import numpy as np + +import matplotlib +matplotlib.use("TkAgg") +import matplotlib.pyplot as plt +from matplotlib.widgets import Button + +import uart_capture as uc + +VREF = 5.0 +LSB = 2 * VREF / 2 ** 24 # 596.0 нВ +NFFT = 8192 + + +def decode(data: bytes) -> np.ndarray: + """150 байт кадра -> 50 знаковых отсчётов.""" + a = np.frombuffer(data, dtype=np.uint8).reshape(-1, 3).astype(np.int32) + v = (a[:, 0] << 16) | (a[:, 1] << 8) | a[:, 2] + return np.where(v & 0x800000, v - 0x1000000, v) + + +def minmax_decimate(y, buckets): + """Прореживание для отрисовки с сохранением огибающей: на каждый столбец + пикселей берём минимум и максимум, иначе выбросы шума пропадут.""" + n = len(y) + if n <= buckets * 2: + return np.arange(n), y + step = n // buckets + blocks = y[:step * buckets].reshape(buckets, step) + idx = np.arange(buckets) * step + x = np.empty(buckets * 2) + v = np.empty(buckets * 2) + x[0::2], x[1::2] = idx, idx + step - 1 + v[0::2], v[1::2] = blocks.min(axis=1), blocks.max(axis=1) + return x, v + + +class Reader(threading.Thread): + """Читает порт, разбирает кадры, копит отсчёты для отрисовки.""" + + daemon = True + + def __init__(self, port, baud, depth): + super().__init__() + self.port, self.baud = port, baud + self.samples = collections.deque(maxlen=depth) + self.lock = threading.Lock() + self.stop_evt = threading.Event() + self.parser = uc.FrameParser() + self.err = None + + self.bytes_total = 0 + self.t0 = None + self.rec_path = None # запрос от интерфейса: путь или None + self.rec_active = None # что пишется сейчас + self.rec_samples = 0 + + def run(self): + try: + ser = uc.open_port(self.port, self.baud) + except Exception as e: + self.err = str(e) + return + self.t0 = time.time() + fraw = fbin = None + try: + while not self.stop_evt.is_set(): + # Файлы открывает и закрывает только тот поток, что в них пишет + if self.rec_path != self.rec_active: + if fraw: + fraw.close(); fbin.close() + fraw = fbin = None + if self.rec_path: + fraw = open(self.rec_path + ".raw", "wb") + fbin = open(self.rec_path + ".bin", "wb") + self.rec_samples = 0 + self.rec_active = self.rec_path + + chunk = ser.read(4096) + if not chunk: + continue + self.bytes_total += len(chunk) + if fraw: + fraw.write(chunk) + + for _cnt, _status, data, gap in self.parser.feed(chunk): + if fbin: + if gap: + fbin.write(b"\x00" * (gap * uc.SAMPLES_PER_FRAME * 3)) + self.rec_samples += gap * uc.SAMPLES_PER_FRAME + fbin.write(data) + self.rec_samples += uc.SAMPLES_PER_FRAME + with self.lock: + self.samples.extend(decode(data)) + except Exception as e: + self.err = str(e) + finally: + if fraw: + fraw.close(); fbin.close() + ser.close() + + def snapshot(self): + with self.lock: + return np.fromiter(self.samples, dtype=np.int32, count=len(self.samples)) + + +class Live: + def __init__(self, reader, fs, window_s, out_base): + self.rd = reader + self.fs = fs + self.out_base = out_base + self.window_s = window_s + self.sub_dc = False + self.frozen = False + self.psd_mode = True + self.autoscale = True + self.rec_n = 0 + + self.fig = plt.figure(figsize=(11, 8)) + self.fig.canvas.manager.set_window_title("AD7766 -- живой просмотр") + self.ax_t = self.fig.add_axes([0.08, 0.50, 0.89, 0.33]) + self.ax_f = self.fig.add_axes([0.08, 0.14, 0.89, 0.27]) + + self.ln_t, = self.ax_t.plot([], [], lw=0.8) + self.ax_t.set_xlabel("время, с") + self.ax_t.set_ylabel("напряжение, В") + self.ax_t.grid(alpha=0.3) + self.ax_t.set_xlim(-window_s, 0) + + self.ln_f, = self.ax_f.plot([], [], lw=0.8) + self.ax_f.set_xlabel("частота, Гц") + self.ax_f.set_ylabel("нВ/√Гц") + self.ax_f.grid(alpha=0.3, which="both") + self.ax_f.set_xscale("log") + self.ax_f.set_yscale("log") + # Пределы обеих осей задаём сразу положительными: до прихода данных + # линия пуста, а логарифмическая шкала с нулём в пределах падает + self.ax_f.set_xlim(1, fs / 2) + self.ax_f.set_ylim(1, 1e6) + + self.txt = self.fig.text(0.01, 0.985, "", family="monospace", + fontsize=9, va="top") + + self.buttons = [] + self.b_rec = self._button(0.08, "● Запись", self.toggle_rec) + self.b_auto = self._button(0.24, "Автомасштаб: вкл", self.toggle_auto) + self.b_dc = self._button(0.40, "Как есть", self.toggle_dc) + self.b_sp = self._button(0.56, "Спектр: плотность", self.toggle_sp) + self.b_frz = self._button(0.72, "Пауза", self.toggle_frz) + + self.fig.canvas.mpl_connect("key_press_event", self.on_key) + + def _button(self, x, label, cb): + ax = self.fig.add_axes([x, 0.025, 0.15, 0.05]) + b = Button(ax, label) + b.on_clicked(cb) + self.buttons.append(b) + return b + + # --- переключатели --- + + def toggle_rec(self, _=None): + if self.rd.rec_path: + self.rd.rec_path = None + self.b_rec.label.set_text("● Запись") + self.b_rec.ax.set_facecolor("0.85") + else: + self.rec_n += 1 + self.rd.rec_path = f"{self.out_base}_{self.rec_n:02d}" + self.b_rec.label.set_text("■ Стоп") + self.b_rec.ax.set_facecolor("#ff9c9c") + + def toggle_auto(self, _=None): + self.autoscale = not self.autoscale + self.b_auto.label.set_text( + "Автомасштаб: вкл" if self.autoscale else "Автомасштаб: выкл") + + def toggle_dc(self, _=None): + self.sub_dc = not self.sub_dc + self.b_dc.label.set_text("Без постоянной" if self.sub_dc else "Как есть") + + def toggle_sp(self, _=None): + self.psd_mode = not self.psd_mode + self.b_sp.label.set_text( + "Спектр: плотность" if self.psd_mode else "Спектр: амплитуда") + + def toggle_frz(self, _=None): + self.frozen = not self.frozen + self.b_frz.label.set_text("Продолжить" if self.frozen else "Пауза") + + def on_key(self, ev): + {"q": lambda: plt.close(self.fig), "r": self.toggle_rec, + "a": self.toggle_auto, "d": self.toggle_dc, + "s": self.toggle_sp, "f": self.toggle_frz}.get(ev.key, lambda: None)() + + def _toolbar_busy(self): + """Пользователь работает лупой или рукой -- пределы осей не трогаем.""" + tb = getattr(self.fig.canvas, "toolbar", None) + return bool(getattr(tb, "mode", "")) if tb else False + + # --- отрисовка --- + + def update(self, _=None): + if self._toolbar_busy() and self.autoscale: + self.toggle_auto() + if self.frozen: + return self.ln_t, self.ln_f + + y = self.rd.snapshot() + if len(y) < NFFT: + self.txt.set_text(f"ждём данные... принято {len(y)} отсчётов" + + (f"\nОШИБКА: {self.rd.err}" if self.rd.err else "")) + return self.ln_t, self.ln_f + + v = y * LSB + mean = v.mean() + vv = v - mean if self.sub_dc else v + + x, d = minmax_decimate(vv, 1200) + self.ln_t.set_data((x - len(vv)) / self.fs, d) + + seg = v[-NFFT:] - v[-NFFT:].mean() + w = np.hanning(NFFT) + sp = np.abs(np.fft.rfft(seg * w)) ** 2 + f = np.fft.rfftfreq(NFFT, 1 / self.fs) + # Ноль на логарифмической оси недопустим, поэтому подрезаем снизу + # заведомо ничтожным уровнем + if self.psd_mode: + psd = sp / ((w ** 2).sum() * self.fs) * 2 + self.ln_f.set_data(f[1:], np.maximum(np.sqrt(psd[1:]) * 1e9, 1e-3)) + self.ax_f.set_ylabel("нВ/√Гц") + else: + amp = np.sqrt(sp[1:]) / w.sum() * 2 * 1e6 + self.ln_f.set_data(f[1:], np.maximum(amp, 1e-6)) + self.ax_f.set_ylabel("амплитуда, мкВ") + + if self.autoscale: + lo, hi = d.min(), d.max() + pad = max((hi - lo) * 0.1, 1e-7) + self.ax_t.set_ylim(lo - pad, hi + pad) + self.ax_t.set_xlim(-len(vv) / self.fs, 0) + + # Пределы спектра считаем сами. relim() с autoscale_view() здесь + # не годятся: лупа снимает у оси внутренний флаг autoscale_on, + # после чего эта пара молча перестаёт работать навсегда. + yd = np.asarray(self.ln_f.get_ydata()) + pos = yd[yd > 0] + if len(pos): + top = pos.max() * 3 + bot = max(np.percentile(pos, 1) / 3, top / 1e6) + self.ax_f.set_ylim(bot, top) + self.ax_f.set_xlim(1, self.fs / 2) + + p = self.rd.parser + el = max(time.time() - (self.rd.t0 or time.time()), 1e-3) + rec = (f"ЗАПИСЬ {self.rd.rec_active} {self.rd.rec_samples} отсч." + if self.rd.rec_active else "не пишем") + st = ("чисто" if p.status_flags == 0 else + f"FIFO={p.status_flags & 1} DRDY={(p.status_flags >> 1) & 1}") + self.txt.set_text( + f"среднее {mean * 1e3:+9.3f} мВ СКО {v.std() / LSB:7.1f} код " + f"({v.std() * 1e6:8.2f} мкВ) размах {(v.max() - v.min()) * 1e6:9.1f} мкВ\n" + f"кадров {p.frames_ok:<8d} CRC {p.crc_errors:<5d} потеряно {p.padded_frames:<6d} " + f"ресинхр {p.resyncs:<5d} STATUS {st}\n" + f"канал {self.rd.bytes_total / el / 1000:5.1f} кБ/с " + f"кадров/с {p.frames_ok / el:5.1f} окно {len(vv) / self.fs:.2f} с " + f"автомасштаб {'вкл' if self.autoscale else 'ВЫКЛ'} {rec}") + return self.ln_t, self.ln_f + + +def selftest(): + """Прогон конвейера и всех веток отрисовки без порта и без окна.""" + import struct + import types + matplotlib.use("Agg", force=True) + + parser = uc.FrameParser() + samples = [] + for k in range(400): + s = [int(8000 * np.sin(2 * np.pi * 300 * (k * 50 + i) / 12500)) + for i in range(uc.SAMPLES_PER_FRAME)] + data = b"".join(struct.pack(">i", v & 0xFFFFFF)[1:] for v in s) + payload = struct.pack(">HB", k, 0) + data + frame = uc.SYNC + payload + struct.pack(">H", uc.crc16_ccitt(payload)) + for _c, _st, d, _g in parser.feed(frame): + samples.extend(decode(d)) + y = np.asarray(samples, dtype=np.int32) + assert parser.frames_ok == 400 and parser.crc_errors == 0 + assert len(y) == 400 * uc.SAMPLES_PER_FRAME + + xs, dd = minmax_decimate(y, 500) + assert len(xs) == 1000 and dd.max() == y.max() and dd.min() == y.min() + sp = np.abs(np.fft.rfft((y[:NFFT] - y[:NFFT].mean()) * np.hanning(NFFT))) + peak = np.fft.rfftfreq(NFFT, 1 / 12500.0)[np.argmax(sp)] + assert abs(peak - 300) < 3, peak + print(f"конвейер: 400 кадров, {len(y)} отсчётов, пик спектра {peak:.1f} Гц") + + rd = types.SimpleNamespace( + parser=parser, bytes_total=390000, t0=time.time(), rec_path=None, + rec_active=None, rec_samples=0, err=None, + snapshot=lambda: np.zeros(10, dtype=np.int32)) + live = Live(rd, 12500.0, 2.0, "live") + + # Рисуем именно холст, а не только считаем: логарифмическая шкала падает + # на отрисовке, а не на подготовке данных. Пустое состояние -- первым. + live.update() + live.fig.canvas.draw() + print(" отрисовка без данных -- ок") + + rd.snapshot = lambda: y + live.update() + live.fig.canvas.draw() + print(" отрисовка с данными -- ок") + lim0 = live.ax_t.get_ylim() + for name, fn in (("автомасштаб", live.toggle_auto), ("постоянная", live.toggle_dc), + ("спектр", live.toggle_sp), ("пауза", live.toggle_frz)): + fn() + live.update() + live.fig.canvas.draw() + print(f" переключение: {name} -- ок") + live.toggle_frz() + + # С выключенным автомасштабом пределы обязаны остаться нашими + live.ax_t.set_ylim(-1.0, 1.0) + live.update() + assert live.ax_t.get_ylim() == (-1.0, 1.0), "автомасштаб затирает зум" + print(" зум при выключенном автомасштабе сохраняется -- ок") + + live.toggle_auto() + live.update() + assert live.ax_t.get_ylim() != (-1.0, 1.0) + assert live.ax_t.get_ylim() == lim0 + print(" автомасштаб обратно включается -- ок") + + # Спектр обязан следовать за уровнем сигнала + f_small = live.ax_f.get_ylim() + rd.snapshot = lambda: y * 100 + live.update() + f_big = live.ax_f.get_ylim() + assert f_big[1] > f_small[1] * 10, (f_small, f_big) + print(" спектр масштабируется за сигналом -- ок") + + # Лупа снимает у оси внутренний флаг autoscale_on -- после этого + # масштабирование обязано продолжать работать + live.ax_f.set_autoscaley_on(False) + live.ax_t.set_autoscaley_on(False) + rd.snapshot = lambda: y + live.update() + assert live.ax_f.get_ylim()[1] < f_big[1] / 10, live.ax_f.get_ylim() + assert live.ax_t.get_ylim() == lim0 + print(" масштабирование переживает работу лупой -- ок") + + live.toggle_rec() + assert rd.rec_path == "live_01" and "Стоп" in live.b_rec.label.get_text() + live.toggle_rec() + assert rd.rec_path is None and "Запись" in live.b_rec.label.get_text() + print(" кнопка записи -- ок") + return 0 + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--port") + ap.add_argument("--baud", type=int, default=921600) + ap.add_argument("--fs", type=float, default=uc.FS_DEFAULT) + ap.add_argument("--window", type=float, default=2.0, help="окно осциллограммы, с") + ap.add_argument("--out", default="live", help="базовое имя для записей") + ap.add_argument("--selftest", action="store_true") + args = ap.parse_args() + + if args.selftest: + return selftest() + if not args.port: + ap.error("нужен --port (список портов: uart_capture.py --list)") + + rd = Reader(args.port, args.baud, int(args.fs * max(args.window, 1.0))) + rd.start() + + live = Live(rd, args.fs, args.window, args.out) + from matplotlib.animation import FuncAnimation + # Ссылку держим на объекте: локальную переменную сборщик может забрать + live.anim = FuncAnimation(live.fig, live.update, interval=100, + blit=False, cache_frame_data=False) + plt.show() + + rd.stop_evt.set() + rd.join(timeout=2) + if rd.err: + print(f"ошибка чтения порта: {rd.err}") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main())