AD7766 на Tang Primer 25K: отладочная прошивка, структура после рефакторинга
This commit is contained in:
+14
@@ -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
@@ -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>
|
||||
@@ -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`):
|
||||
формат описан в одном месте, так что просмотр и запись не могут разойтись.
|
||||
@@ -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 добивается нулями,
|
||||
чтобы не сдвинулась ось времени. Места вставок пишутся в лог.
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
// Тактовый генератор платы: 50 МГц -> период 20 нс.
|
||||
// Без этого статический анализ не проверяет ни одного пути и выдаёт TA1132.
|
||||
create_clock -name clk -period 20 -waveform {0 10} [get_ports {clk}]
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user