AD7766 на Tang Primer 25K: отладочная прошивка, структура после рефакторинга

This commit is contained in:
nikit3s
2026-08-11 12:16:18 +03:00
commit 6b78fcc34f
20 changed files with 2741 additions and 0 deletions
+14
View File
@@ -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__/
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1" encoding="UTF-8"?>
<!DOCTYPE gowin-fpga-project>
<Project>
<Template>FPGA</Template>
<Version>5</Version>
<Device name="" pn="">gw5a25b-003</Device>
<FileList>
<File path="src/UART.vhd" type="file.vhdl" enable="1"/>
<File path="src/led_status.vhd" type="file.vhdl" enable="1"/>
<File path="src/SPI_Master.vhd" type="file.vhdl" enable="1"/>
<File path="src/SPI_Ctrl.vhd" type="file.vhdl" enable="1"/>
<File path="src/decimator.vhd" type="file.vhdl" enable="1"/>
<File path="src/fifo_top.vhd" type="file.vhdl" enable="1"/>
<File path="src/Packets_creator.vhd" type="file.vhdl" enable="1"/>
<File path="src/Top.vhd" type="file.vhdl" enable="1"/>
<File path="src/AD7766.cst" type="file.cst" enable="1"/>
<File path="src/AD7766.sdc" type="file.sdc" enable="1"/>
</FileList>
</Project>
+72
View File
@@ -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`):
формат описан в одном месте, так что просмотр и запись не могут разойтись.
+60
View File
@@ -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 добивается нулями,
чтобы не сдвинулась ось времени. Места вставок пишутся в лог.
+22
View File
@@ -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 |
+93
View File
@@ -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
}
+29
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
// Тактовый генератор платы: 50 МГц -> период 20 нс.
// Без этого статический анализ не проверяет ни одного пути и выдаёт TA1132.
create_clock -name clk -period 20 -waveform {0 10} [get_ports {clk}]
+288
View File
@@ -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;
+199
View File
@@ -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;
+163
View File
@@ -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;
+211
View File
@@ -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;
+198
View File
@@ -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;
+126
View File
@@ -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;
+104
View File
@@ -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;
+77
View File
@@ -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;
+93
View File
@@ -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;
+223
View File
@@ -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;
+326
View File
@@ -0,0 +1,326 @@
#!/usr/bin/env python3
"""Приём кадров AD7766 по UART.
Пишет два файла:
<out>.raw все байты из порта, до разбора -- отладка канала
<out>.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())
+421
View File
@@ -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())