ADC Controller (AD7401) — техническая документация

Плата AC7020C (Zynq-7020), 4-канальный измеритель тока на сигма-дельта АЦП AD7401A. Базовый адрес 0x43C1_0000 (пример).


1. Принцип измерения (аппаратная логика)

1.1 AD7401A — сигма-дельта модулятор

АЦП измеряет дифференциальное напряжение на шунте (ток через резистор) и выдаёт однобитный PDM-поток (MDAT) — плотность импульсов пропорциональна входному напряжению.

I ──×R_shunt──► V_шунта ──AD7401A──► PDM-поток MDAT (плотность = V_in / V_ref)
Параметр Значение
Такт MCLK 20 МГц (F_clk=100 МГц / G_MCLK_DIV=5)
Полная шкала ±320 мВ (AD7401A)
Шунт 0.5 Ом

1.2 Формирование отсчёта (OSR)

Каждые G_OSR = 256 PDM-битов формируется один отсчёт. Подсчитывается число единиц ones ∈ [0..256]:

ones = 128  →  нулевой ток (50% плотность PDM)
ones > 128  →  прямой ток (положительное напряжение)
ones < 128  →  обратный ток (отрицательное напряжение)

Период отсчёта:

ΔT_osr = G_OSR / F_mclk = 256 / 20 МГц = 12.8 мкс

1.3 Двухтрактовая архитектура

Batch Engine (периодный, защёлкивается по спаду RUN): - Классификация каждого отсчёта на прямой/обратный. - Накопление заряда за окно RUN. - По спаду RUN → защёлка + аппаратное деление (среднее).

Stream Engine (потоковый, обновляется каждый отсчёт): - Цифровой ФНЧ 1-го порядка (EMA) прямого и обратного тока. - Непрерывный мониторинг (для OCP-защиты и наблюдения в паузах).

        MDAT ──► [PDM → ones] ──► (ones-128)
                        │
                        ├──► Batch: FWD/REV аккумуляторы ──(спад RUN)──► *_ACC_LAT/CNT_LAT ──► Делитель ──► *_AVG
                        │
                        └──► Stream: EMA-фильтры (Q16.16) ──► *_EMA

2. Логика работы DSP (подробно)

2.1 Полный тракт обработки

MDAT (бит) ──синхронизация──► PDM-поток
   │
   └─► [счётчик OSR + счётчик единиц] ──► ones (0..256) каждые 12.8 мкс
          │
          ├─► ones > 128  →  FWD_ACC += (ones−128), FWD_CNT++, обновление FWD_EMA
          ├─► ones < 128  →  REV_ACC += (128−ones), REV_CNT++, обновление REV_EMA
          └─► COUNT++ (всегда, любой знак)

2.2 Генератор MCLK

Делитель такта на G_MCLK_DIV=5: 100 МГц → 20 МГц. Счётчик 0..4; по достижении G_MCLK_DIV−1 взводится MCLK, по G_MCLK_DIV−2 формируется sample_tick (один раз за период). По sample_tick читается бит MDAT.

2.3 Счётчик OSR

pdm_counter считает биты 0..255. На 256-м бите (pdm_counter = G_OSR−1) блок завершён: ones = число единиц в 256 битах. Это один валидный отсчёт.

2.4 Классификация направления

ones = 128 → ноль (не накапливается)
ones > 128 → прямой ток (FWD)
ones < 128 → обратный ток (REV)

Физический смысл (схема заряд/разряд конденсатора):

  • FWD (прямой) — ток идёт от источника питания в конденсатор (заряд).
  • REV (обратный) — ток идёт из конденсатора обратно в источник (разряд/рекуперация).

По сумме за окно RUN:

  • REV_ACC > FWD_ACC → суммарный ток поступает в источник питания (рекуперация);
  • FWD_ACC > REV_ACC → суммарный ток идёт из источника в нагрузку (заряд).

Ограничение (важно): ADC меряет средний ток за 12.8 мкс (ones = плотность PDM), а не мгновенный. Переменная (реактивная) составляющая с периодом заметно меньше окна усредняется в ноль:

  • реактивный ток → ones ≈ 128 → FACC=0, RACC=0, хотя энергия реально передаётся (например, через трансформатор);
  • направление быстрых переменных токов не выделяется — классифицируется только постоянная (DC) составляющая.

Это ограничение метода, а не ошибка: для реактивной передачи энергии КПД по E_ИП = U × I_ср × t не вычислить (I_ср = 0).

Резонанс и прямой заряд. В резонансном контуре (импульсный заряд конденсатора через трансформатор) FACC>0 появляется только на резонансе — когда частота накачки совпадает с собственной частотой контура.

Механизм: источник (ИП) однонаправленный — он «толкает» ток вперёд при замкнутом ключе. На резонансе каждый толчок попадает в фазу с колебанием и усиливает прямую полуволну — колебание перекашивается вперёд (прямой полупериод больше обратного). Этот перекос и есть постоянная (прямая) составляющая → FACC>0. Вне резонанса толчки попадают не в фазу, колебание остаётся симметричным (вперёд = назад) → среднее 0 → FACC=0.

Аналогия: резонанс — толкаешь качели в такт (смещение вперёд растёт); вне резонанса — вразнобой (качели дёргаются симметрично).

При этом в обоих случаях конденсатор заряжается: во вторичной цепи диод Шоттки выпрямляет ток, поэтому заряд конденсатора не уменьшается и растёт до порога компаратора. Ноль в ADC относится только к постоянной составляющей первичного тока в шунте, а не к заряду конденсатора.

Фиксация реактивной составляющей. Наличие реактивной (переменной) составляющей фиксируется косвенно — по комбинации регистров:

  • COUNT > 0 — ADC активно сэмплирует (отсчёты идут);
  • FACC = 0 и RACC = 0 — ни прямого, ни обратного накопления (средний ток нулевой).

Совокупность COUNT>0 + FACC=0 + RACC=0 = реактивный режим (ток есть, но постоянной составляющей нет). Чтобы отличить его от «тока нет», смотрят на напряжение конденсатора: если оно растёт при FACC=0 — энергия передаётся реактивно.

Величину реактивной составляющей измерить нельзя. Амплитуда переменного тока определяет, насколько плотность PDM отклоняется от 50%, но за окно OSR (12.8 мкс) любая симметричная осцилляция усредняется ровно в 50% (ones=128) независимо от амплитуды:

  • 10 мА → плотность 49.2% ↔ 50.8% → среднее 50% → ones=128;
  • 100 мА → плотность 42.2% ↔ 57.8% → среднее 50% → ones=128;
  • 640 мА (полная шкала) → плотность 0% ↔ 100% → среднее 50% → ones=128.

То есть усреднение стирает и направление, и амплитуду — ADC индицирует только «среднее = 0». Для измерения величины нужен другой тракт: короткий OSR, пиковый детектор на шунт или быстрый АЦП (pipeline/SAR на 10–20 MSPS).

Главное: ADC измеряет ток, а для задачи нужна энергия. ADC показывает средний (DC) ток, а не энергию:

  • средний ток = (1/T)·∫ I(t) dt — для симметричного колебания равен 0;
  • энергия = ∫ V(t)·I(t) dt — мгновенная мощность, не равна 0.

Отсюда «парадокс»: ADC видит ноль, хотя конденсатор заряжается. Он меряет не ту величину — средний ток, а энергию переносит мгновенная мощность V×I.

Аналогия (качели): толкая качели вперёд-назад, рука ходит туда-сюда (среднее положение = 0), но энергия передаётся, потому что толчок приходится в фазе с движением. Так же в контуре: ток «качается» (среднее 0), но энергия идёт в моменты импульса, где V×I > 0.

Аналогия (весы): ADC — как весы, которые показывают только равновесие (перекос FWD − REV), а не вес (амплитуду). Прямой ток — левая чаша, обратный — правая. При реактивном токе «влево» и «вправо» поровну → весы уравновешены → стрелка на нуле, но вес каждой чаши (амплитуда колебаний) неизвестен. Энергия переносится именно «весом» (мгновенной мощностью), а не перекосом.

Поэтому для измерения реальной энергии нужна мгновенная мощность: быстро мерить I(t) и V(t) одновременно и перемножать — а не полагаться на средний ток ADC.

Это не ошибка ADC, а несовпадение назначений. ADC корректно выполняет свою задачу — измеряет средний ток (постоянную составляющую). Для задач, где нужен именно ток (направление, амплитуда DC, заряд по постоянной составляющей), он подходит. Для задач, где нужно мерить энергию (мгновенную мощность V×I при переменном/реактивном токе), он не подходит — это другой класс измерений, требующий отдельного тракта: быстрые замеры I(t) и V(t) одновременно.

2.5 Batch Engine (накопление + защёлка)

Во время RUN (на каждом OSR-отсчёте): - FWD_ACC += (ones−128), FWD_CNT++ — если прямой; - REV_ACC += (128−ones), REV_CNT++ — если обратный.

По спаду RUN (run_fell): - рабочие накопители копируются в защёлкнутые регистры (*_lat); - запускается делитель, AVG_VALID=0.

По переднему фронту RUN (run_rose): - рабочие накопители обнуляются (для нового окна); - делитель сбрасывается.

Обработка run_rose/run_fell выполняется независимо от LOCK/START_STOP — защёлка срабатывает даже при активном LOCK.

2.6 Делитель Q8 (восстанавливающее деление)

Вычисляет AVG = ACC × 256 / CNT (Q8) за две фазы по 40 тактов (~82 такта):

  • FWD-фаза: FWD_AVG = FWD_ACC × 256 / FWD_CNT;
  • REV-фаза: REV_AVG = REV_ACC × 256 / REV_CNT;
  • по завершении — AVG_VALID = 1.

Алгоритм — классическое восстанавливающее деление: на каждом такте остаток сдвигается влево, подтягивается очередной бит делимого, вычитается делитель (если умещается) и фиксируется бит частного. CNT=0 → результат 0.

2.7 Stream Engine (EMA)

ФНЧ 1-го порядка, α = 2⁻⁶ = 1/64:

y[k] = y[k-1] + (x[k] − y[k-1]) / 64
x[k] = |ones − 128|   (для своего направления; иначе 0)

Внутренняя точность Q16.16, выход Q8 (биты 15:0). Обновляется на каждом OSR-отсчёте, фронтами RUN не сбрасывается.

2.8 Управляющие сигналы (приоритет)

RST_COUNT > LOCK > START_STOP
  • RST_COUNT=1 — сброс COUNT и ERROR (строб, самоочистка).
  • LOCK=1 — остановить PDM-приём, отбросить частичный блок (фронты RUN всё равно обрабатываются).
  • START_STOP=0 — канал остановлен.

3. Регистровая карта (4 канала × 11 регистров)

Страйд канала = 0x40 (64 байта). Декодирование: канал = addr[7:6], регистр = addr[5:2].

Канал Диапазон
CH0 0x00 – 0x28
CH1 0x40 – 0x68
CH2 0x80 – 0xA8
CH3 0xC0 – 0xE8

Регистры одного канала (смещение от базы канала)

Смещение Имя Доступ Разр. Формат Описание
0x00 CTRL RW 32 биты управление
0x04 STATUS RO 32 биты статус
0x08 COUNT RO 32 u32 всего OSR-отсчётов с RST_COUNT
0x0C FWD_ACC RO 32 u32 (кванты) интеграл прямого заряда за RUN
0x10 FWD_CNT RO 32 u32 прямых отсчётов за RUN
0x14 FWD_AVG RO 32 Q8 средний прямой ток
0x18 FWD_EMA RO 32 Q8 (15:0) EMA прямого тока
0x1C REV_ACC RO 32 u32 (кванты) интеграл обратного заряда за RUN
0x20 REV_CNT RO 32 u32 обратных отсчётов за RUN
0x24 REV_AVG RO 32 Q8 средний обратный ток
0x28 REV_EMA RO 32 Q8 (15:0) EMA обратного тока

Q8 — формат с фиксированной точкой: значение × 256 (младшие 8 бит — дробная часть).


4. Регистр CTRL (0x00, RW)

Бит Имя Тип Смысл
8 LOCK уровень 1 = заморозить тракт оцифровки (отбросить частичный PDM-блок, зафиксировать накопители)
9 RST_COUNT строб запись 1 → сброс COUNT и ERROR (самоочищается за 1 такт)
10 START_STOP уровень 1 = канал разрешён; 0 = канал остановлен
остальные — — резерв (читаются 0)

Семантика:

  • START_STOP=1 И LOCK=0 — канал принимает PDM и накапливает.
  • LOCK=1 — PDM-приём остановлен (частичный блок отброшен), но фронты RUN продолжают обрабатываться (защёлка/делитель работают).
  • RST_COUNT — разовый сброс (строб), не уровень.
  • Сброс по reset_n — все нули → канал остановлен (START_STOP=0).

5. Регистр STATUS (0x04, RO)

Бит Имя Смысл
0 READY 1 = был хотя бы один валидный отсчёт с момента сброса
1 RUN_STATE текущий синхронизированный уровень RUN (1 = генератор активен)
2 ERROR 1 = переполнение COUNT (0xFFFFFFFF), сброс по RST_COUNT
3 AVG_VALID 1 = делитель завершил, регистры ACC/CNT/AVG консистентны (защёлкнуты)

Протокол чтения (контракт):

  1. ARM ждёт AVG_VALID=1 (завершение окна RUN).
  2. Читает FWD_ACC/FWD_CNT/FWD_AVG и REV_ACC/REV_CNT/REV_AVG — они защёлкнуты и согласованы.
  3. AVG_VALID сбрасывается по переднему фронту следующего RUN.

6. Математика для ARM

5.1 Константы

Константа Значение
F_MCLK 20 МГц
OSR 256 бит/отсчёт
DT_OSR 12.8 мкс (256 / 20 МГц)
V_REF 320 мВ (полная шкала)
R_SHUNT 0.5 Ом
мА на квант 5.0 (2.5 мВ / 0.5 Ом)

5.2 Ток (из AVG или EMA, формат Q8)

I_fwd_mA = FWD_AVG / 256 × 5

Целочисленно (без float):

ones = FWD_AVG >> 8        // средние кванты (0..128)
I_mA = (ones - 128) × 5    // знаковый ток, мА

Это теоретическая формула (шунт ровно 0.5 Ом, офсет 0). Рабочая откалиброванная формула — в разделе 5.8.

5.3 Средний ток (деление аппаратное, уже в регистре)

Аппаратный делитель уже считает:

FWD_AVG = FWD_ACC × 256 / FWD_CNT   (Q8; = 0 при FWD_CNT=0)
REV_AVG = REV_ACC × 256 / REV_CNT

ARM не обязан делить — *_AVG уже готовое среднее (Q8). Для контроля доступно сырое деление:

fwd_avg = FWD_ACC / FWD_CNT    // кванты (если CNT > 0)

5.4 Заряд (Кулоны)

Квант заряда:

Q_quant = (V_REF/128) / R_SHUNT × ΔT_osr
        = 2.5 мВ / 0.5 Ом × 12.8 мкс
        = 5 мА × 12.8 мкс
        = 64 нКл
Q_fwd_C = FWD_ACC × 64e-9    // прямой заряд, Кл
Q_rev_C = REV_ACC × 64e-9    // обратный заряд, Кл

5.5 Время (по счётчикам)

t_fwd_s   = FWD_CNT × 12.8 мкс
t_rev_s   = REV_CNT × 12.8 мкс
t_total_s = COUNT   × 12.8 мкс

Аналитика (DCM/разрывный режим): если FWD_CNT + REV_CNT < COUNT за окно — разница = длительность паузы с нулевым током.

5.6 EMA (Stream Engine, реализация в ПЛИС)

Фильтр 1-го порядка, α = 2⁻⁶ = 1/64:

y[k] = y[k-1] + (x[k] − y[k-1]) / 64
x[k] = |ones − 128|   (для своего направления; иначе 0)

Внутренняя точность Q16.16, выход *_EMA — Q8 (биты 15:0). Для ARM это мгновенный сглаженный ток:

I_ema_fwd_mA = (FWD_EMA & 0xFFFF) / 256 × 5

5.7 Итоговый знаковый ток

I_net_mA     = (FWD_AVG − REV_AVG) / 256 × 5
I_net_ema_mA = ((FWD_EMA & 0xFFFF) − (REV_EMA & 0xFFFF)) / 256 × 5

5.8 Калибровка (2026-10-10)

Источник ODP6033 в режиме CC (нагрузка ~47 Ом, уставка 50 В). Калибровка по серии 51–301 мА, контроль по 25–275 мА (независимая самопроверка).

Формула пересчёта FWD_AVG (Q8) → ток (мкА), целочисленно:

int64_t t   = (int64_t)FWD_AVG * 1287443 + 32768;  // × 19.6448, с округлением
int32_t I_uA = (int32_t)((t >> 16) - 548);         // вычесть офсет 548 мкА
if (I_uA < 0) I_uA = 0;                            // ограничение снизу нулём

Смысл коэффициентов:

  • наклон 5.029 мА/квант (идеал 5.0, фактический шунт/шкала +0.58%);
  • офсет 548 мкА (вычитается до ограничения снизу).

Точность: максимальное расхождение на контрольных точках 0.126 мА.

Лимит, мА I факт, мА FWD_AVG (Q8) I калибр., мА Ошибка, мА
25 26 1347 25.914 −0.086
50 51 2621 50.941 −0.059
75 76 3894 75.949 −0.051
100 101 5167 100.957 −0.043
125 126 6444 126.043 +0.043
150 151 7715 151.012 +0.012
175 176 8986 175.980 −0.020
200 201 10263 201.067 +0.067
225 226 11532 225.996 −0.004
250 251 12802 250.945 −0.055
275 276 14071 275.874 −0.126
300 301 15346 300.921 −0.079

Обратный ток (REV), 2026-10-10

Отдельная серия с обратной полярностью (25–300 мА, шаг 25 мА). Регрессия:

I_rev_мА = 0.01951758695 × REV_AVG + 3.2187987

Формула пересчёта REV_AVG (Q8) → величина обратного тока (мкА):

int64_t t   = (int64_t)REV_AVG * 1279105 + 32768;  // × 19.5176, с округлением
int32_t I_uA = (int32_t)((t >> 16) + 3219);        // прибавить офсет 3219 мкА
if (I_uA < 0) I_uA = 0;                            // ограничение снизу нулём

Формула даёт положительную величину обратного тока; знак определяется REV-трактом. Максимальное расхождение на контрольных точках 0.583 мА.


7. Жизненный цикл окна RUN

RUN=1 (генератор) → накопление FWD/REV, COUNT++, READY=1, EMA обновляется
   │
   └── RUN=0 (спад) → защёлка ACC/CNT → делитель (82 такта) → AVG_VALID=1
   │
   └── RUN=1 (следующий фронт) → сброс накопителей, AVG_VALID=0
  • RUN приходит от timing_controller (не от ARM).
  • Во время непрерывного RUN: ACC/CNT/AVG заморожены (результат прошлого окна), живое — COUNT и EMA.
----------------------------------------------------------------------------------
-- File    : ad7401_channel.vhd
-- Project : AC7020C / Zynq-7020
-- Device  : AD7401
--
-- Один канал сигма-дельта АЦП AD7401 с двухтрактовой обработкой.
--
-- Тракт 1 (Batch Engine): накопление прямого (FWD) и обратного (REV) заряда
--   за окно RUN, защёлкивание по спаду RUN и аппаратное деление (Q8).
-- Тракт 2 (Stream Engine): экспоненциальное скользящее среднее (EMA, Q16.16)
--   прямого/обратного тока, обновляется на каждом OSR-отсчёте.
--
-- RUN   : от timing_controller (не от ARM).
-- LOCK  : ARM -> PL. 1 = заморозить тракт, отбросить частичный PDM-блок.
-- RST_COUNT : ARM -> PL. 1 = сбросить COUNT и ERROR.
-- START_STOP : ARM -> PL. 1 = канал разрешён, 0 = остановлен.
--
-- Регистры (PL -> ARM):
--   READY     : хотя бы один валидный OSR-отсчёт с момента сброса.
--   RUN_STATE : синхронизированное состояние входа RUN.
--   ERROR     : переполнение COUNT (0xFFFF_FFFF).
--   AVG_VALID : делитель завершил FWD_AVG/REV_AVG, регистры периода консистентны.
--   COUNT     : общее число валидных OSR-отсчётов (сброс по RST_COUNT).
--   FWD_ACC / FWD_CNT / FWD_AVG / FWD_EMA
--   REV_ACC / REV_CNT / REV_AVG / REV_EMA
----------------------------------------------------------------------------------

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity ad7401_channel is
    generic (
        -- F_MCLK = F_CLK / G_MCLK_DIV  (100 MHz / 5 = 20 MHz)
        G_MCLK_DIV  : positive := 5;
        -- Количество PDM-битов на один готовый отсчёт
        G_OSR       : positive := 256;
        -- Коэффициент EMA: alpha = 2^-G_EMA_SHIFT
        G_EMA_SHIFT : natural  := 6;
        -- Число дробных бит результата AVG (Q8)
        C_AVG_FRAC  : natural  := 8
    );
    port (
        -- Системные сигналы
        clk        : in  std_logic;
        reset_n    : in  std_logic;

        -- Окно измерения от timing_controller
        run        : in  std_logic;

        -- ARM -> PL
        lock       : in  std_logic;
        rst_count  : in  std_logic;
        start_stop : in  std_logic;

        -- AD7401
        mdat       : in  std_logic;
        mclkin     : out std_logic;

        -- PL -> ARM
        ready      : out std_logic;
        run_state  : out std_logic;
        error      : out std_logic;
        avg_valid  : out std_logic;
        count      : out std_logic_vector(31 downto 0);
        fwd_acc    : out std_logic_vector(31 downto 0);
        fwd_cnt    : out std_logic_vector(31 downto 0);
        fwd_avg    : out std_logic_vector(31 downto 0);
        fwd_ema    : out std_logic_vector(31 downto 0);
        rev_acc    : out std_logic_vector(31 downto 0);
        rev_cnt    : out std_logic_vector(31 downto 0);
        rev_avg    : out std_logic_vector(31 downto 0);
        rev_ema    : out std_logic_vector(31 downto 0)
    );
end entity ad7401_channel;

architecture Behavioral of ad7401_channel is

    function clog2(n : positive) return natural is
        variable v : natural := n - 1;
        variable r : natural := 0;
    begin
        while v > 0 loop
            v := v / 2;
            r := r + 1;
        end loop;
        if r = 0 then
            return 1;
        else
            return r;
        end if;
    end function;

    constant C_MCLK_DIV_WIDTH : natural := clog2(G_MCLK_DIV);
    constant C_OSR_WIDTH      : natural := clog2(G_OSR);

    constant MAX_COUNT : unsigned(31 downto 0) := (others => '1');

    -- MCLK
    signal mclk_counter : unsigned(C_MCLK_DIV_WIDTH-1 downto 0) := (others => '0');
    signal mclk_reg     : std_logic := '0';
    signal sample_tick  : std_logic := '0';

    -- MDAT synchronizer
    signal mdat_meta : std_logic := '0';
    signal mdat_sync : std_logic := '0';

    -- RUN synchronizer
    signal run_meta : std_logic := '0';
    signal run_sync : std_logic := '0';
    signal run_prev : std_logic := '0';
    signal run_rose : std_logic;
    signal run_fell : std_logic;

    -- PDM accumulation
    signal pdm_counter  : unsigned(C_OSR_WIDTH-1 downto 0) := (others => '0');
    signal ones_counter : unsigned(C_OSR_WIDTH downto 0)   := (others => '0');

    -- Cumulative count / flags
    signal count_reg : unsigned(31 downto 0) := (others => '0');
    signal ready_reg : std_logic := '0';
    signal error_reg : std_logic := '0';

    -- Batch working accumulators (current RUN window)
    signal fwd_acc_reg : unsigned(31 downto 0) := (others => '0');
    signal fwd_cnt_reg : unsigned(31 downto 0) := (others => '0');
    signal rev_acc_reg : unsigned(31 downto 0) := (others => '0');
    signal rev_cnt_reg : unsigned(31 downto 0) := (others => '0');

    -- Batch latched registers (frozen at RUN falling edge)
    signal fwd_acc_lat : unsigned(31 downto 0) := (others => '0');
    signal fwd_cnt_lat : unsigned(31 downto 0) := (others => '0');
    signal rev_acc_lat : unsigned(31 downto 0) := (others => '0');
    signal rev_cnt_lat : unsigned(31 downto 0) := (others => '0');

    -- Divider outputs (Q8)
    signal fwd_avg_reg : unsigned(31 downto 0) := (others => '0');
    signal rev_avg_reg : unsigned(31 downto 0) := (others => '0');
    signal avg_valid_reg : std_logic := '0';

    -- EMA accumulators (Q16.16, signed)
    signal ema_fwd_acc : signed(31 downto 0) := (others => '0');
    signal ema_rev_acc : signed(31 downto 0) := (others => '0');

    -- Divider FSM
    type div_state_t is (DIV_IDLE, DIV_FWD, DIV_REV, DIV_DONE);
    signal div_state : div_state_t := DIV_IDLE;
    signal div_cnt   : integer range 0 to 40 := 0;
    signal div_rem   : unsigned(31 downto 0) := (others => '0');
    signal div_q     : unsigned(39 downto 0) := (others => '0');
    signal div_num   : unsigned(39 downto 0) := (others => '0');
    signal div_den   : unsigned(31 downto 0) := (others => '0');

begin

    --------------------------------------------------------------------------
    -- Outputs
    --------------------------------------------------------------------------
    mclkin    <= mclk_reg;
    ready     <= ready_reg;
    run_state <= run_sync;
    error     <= error_reg;
    avg_valid <= avg_valid_reg;
    count     <= std_logic_vector(count_reg);
    fwd_acc   <= std_logic_vector(fwd_acc_lat);
    fwd_cnt   <= std_logic_vector(fwd_cnt_lat);
    fwd_avg   <= std_logic_vector(fwd_avg_reg);
    rev_acc   <= std_logic_vector(rev_acc_lat);
    rev_cnt   <= std_logic_vector(rev_cnt_lat);
    rev_avg   <= std_logic_vector(rev_avg_reg);
    fwd_ema   <= x"0000" & std_logic_vector(ema_fwd_acc(23 downto 8));
    rev_ema   <= x"0000" & std_logic_vector(ema_rev_acc(23 downto 8));

    run_rose  <= '1' when run_prev = '0' and run_sync = '1' else '0';
    run_fell  <= '1' when run_prev = '1' and run_sync = '0' else '0';

    --------------------------------------------------------------------------
    -- Проверка параметров
    --------------------------------------------------------------------------
    assert G_MCLK_DIV >= 4
        report "G_MCLK_DIV must be >= 4"
        severity failure;

    assert G_OSR = 256
        report "Current scaling requires G_OSR = 256"
        severity failure;

    --------------------------------------------------------------------------
    -- Синхронизация внешних сигналов MDAT и RUN
    --------------------------------------------------------------------------
    process(clk)
    begin
        if rising_edge(clk) then
            if reset_n = '0' then
                mdat_meta <= '0';
                mdat_sync <= '0';
                run_meta  <= '0';
                run_sync  <= '0';
                run_prev  <= '0';
            else
                mdat_meta <= mdat;
                mdat_sync <= mdat_meta;
                run_meta  <= run;
                run_sync  <= run_meta;
                run_prev  <= run_sync;
            end if;
        end if;
    end process;

    --------------------------------------------------------------------------
    -- Генератор MCLK (делитель G_MCLK_DIV), sample_tick один раз за период
    --------------------------------------------------------------------------
    process(clk)
        variable v_counter : natural;
    begin
        if rising_edge(clk) then
            if reset_n = '0' then
                mclk_counter <= (others => '0');
                mclk_reg     <= '0';
                sample_tick  <= '0';
            else
                sample_tick <= '0';
                v_counter := to_integer(mclk_counter);

                if v_counter = G_MCLK_DIV - 1 then
                    mclk_counter <= (others => '0');
                    mclk_reg     <= '1';
                elsif v_counter = 1 then
                    mclk_counter <= mclk_counter + 1;
                    mclk_reg     <= '0';
                elsif v_counter = G_MCLK_DIV - 2 then
                    mclk_counter <= mclk_counter + 1;
                    sample_tick  <= '1';
                else
                    mclk_counter <= mclk_counter + 1;
                end if;
            end if;
        end if;
    end process;

    --------------------------------------------------------------------------
    -- Основной DSP: Batch + EMA + Divider (один процесс, нет multi-driver)
    --------------------------------------------------------------------------
    process(clk)
        variable v_ones   : unsigned(C_OSR_WIDTH downto 0);
        variable in_q16   : signed(31 downto 0);
        variable diff_q16 : signed(31 downto 0);
        variable tent_rem : unsigned(32 downto 0);
    begin
        if rising_edge(clk) then
            if reset_n = '0' then
                pdm_counter  <= (others => '0');
                ones_counter <= (others => '0');
                count_reg    <= (others => '0');
                ready_reg    <= '0';
                error_reg    <= '0';
                fwd_acc_reg  <= (others => '0');
                fwd_cnt_reg  <= (others => '0');
                rev_acc_reg  <= (others => '0');
                rev_cnt_reg  <= (others => '0');
                fwd_acc_lat  <= (others => '0');
                fwd_cnt_lat  <= (others => '0');
                rev_acc_lat  <= (others => '0');
                rev_cnt_lat  <= (others => '0');
                fwd_avg_reg  <= (others => '0');
                rev_avg_reg  <= (others => '0');
                avg_valid_reg <= '0';
                ema_fwd_acc  <= (others => '0');
                ema_rev_acc  <= (others => '0');
                div_state    <= DIV_IDLE;
                div_cnt      <= 0;
                div_rem      <= (others => '0');
                div_q        <= (others => '0');
                div_num      <= (others => '0');
                div_den      <= (others => '0');
            else

                ------------------------------------------------------------------
                -- RST_COUNT: сброс COUNT и ERROR (приоритет над остальным)
                ------------------------------------------------------------------
                if rst_count = '1' then
                    count_reg <= (others => '0');
                    error_reg <= '0';
                end if;

                ------------------------------------------------------------------
                -- Передний фронт RUN: обнулить рабочие накопители, сбросить делитель
                -- (обрабатывается независимо от LOCK / START_STOP)
                ------------------------------------------------------------------
                if run_rose = '1' then
                    fwd_acc_reg  <= (others => '0');
                    fwd_cnt_reg  <= (others => '0');
                    rev_acc_reg  <= (others => '0');
                    rev_cnt_reg  <= (others => '0');
                    div_state    <= DIV_IDLE;
                    avg_valid_reg <= '0';
                end if;

                ------------------------------------------------------------------
                -- Спад RUN: защёлкнуть накопители, запустить делитель
                -- (обрабатывается независимо от LOCK / START_STOP)
                ------------------------------------------------------------------
                if run_fell = '1' then
                    fwd_acc_lat  <= fwd_acc_reg;
                    fwd_cnt_lat  <= fwd_cnt_reg;
                    rev_acc_lat  <= rev_acc_reg;
                    rev_cnt_lat  <= rev_cnt_reg;
                    div_state    <= DIV_FWD;
                    div_cnt      <= 0;
                    div_rem      <= (others => '0');
                    div_q        <= (others => '0');
                    div_num      <= shift_left(resize(fwd_acc_reg, 40), 8);
                    div_den      <= fwd_cnt_reg;
                    avg_valid_reg <= '0';
                end if;

                ------------------------------------------------------------------
                -- Приём PDM (зависит от LOCK / START_STOP)
                ------------------------------------------------------------------
                if lock = '1' or start_stop = '0' then
                    pdm_counter  <= (others => '0');
                    ones_counter <= (others => '0');
                elsif run_sync = '1' then
                        if sample_tick = '1' then
                            v_ones := ones_counter;
                            if mdat_sync = '1' then
                                v_ones := ones_counter + 1;
                            end if;

                            if to_integer(pdm_counter) = G_OSR - 1 then
                                --------------------------------------------------------
                                -- Полный OSR-блок: новый валидный отсчёт
                                --------------------------------------------------------
                                if v_ones > 128 then
                                    fwd_acc_reg <= fwd_acc_reg + resize(v_ones - 128, 32);
                                    fwd_cnt_reg <= fwd_cnt_reg + 1;
                                elsif v_ones < 128 then
                                    rev_acc_reg <= rev_acc_reg + resize(128 - v_ones, 32);
                                    rev_cnt_reg <= rev_cnt_reg + 1;
                                end if;

                                if count_reg = MAX_COUNT then
                                    error_reg <= '1';
                                else
                                    count_reg <= count_reg + 1;
                                end if;
                                ready_reg <= '1';

                                --------------------------------------------------------
                                -- EMA FWD
                                --------------------------------------------------------
                                if v_ones > 128 then
                                    in_q16 := shift_left(to_signed(to_integer(v_ones) - 128, 32), 16);
                                else
                                    in_q16 := (others => '0');
                                end if;
                                diff_q16 := in_q16 - ema_fwd_acc;
                                ema_fwd_acc <= ema_fwd_acc + shift_right(diff_q16, G_EMA_SHIFT);

                                --------------------------------------------------------
                                -- EMA REV
                                --------------------------------------------------------
                                if v_ones < 128 then
                                    in_q16 := shift_left(to_signed(128 - to_integer(v_ones), 32), 16);
                                else
                                    in_q16 := (others => '0');
                                end if;
                                diff_q16 := in_q16 - ema_rev_acc;
                                ema_rev_acc <= ema_rev_acc + shift_right(diff_q16, G_EMA_SHIFT);

                                pdm_counter  <= (others => '0');
                                ones_counter <= (others => '0');
                            else
                                pdm_counter  <= pdm_counter + 1;
                                ones_counter <= v_ones;
                            end if;
                        end if;
                    end if;

                ------------------------------------------------------------------
                -- Делитель (восстанавливающий, Q8): 2 фазы по 40 тактов
                ------------------------------------------------------------------
                case div_state is
                    when DIV_IDLE =>
                        null;

                    when DIV_FWD =>
                        if div_den = 0 then
                            fwd_avg_reg <= (others => '0');
                            div_state   <= DIV_REV;
                            div_cnt     <= 0;
                            div_rem     <= (others => '0');
                            div_q       <= (others => '0');
                            div_num     <= shift_left(resize(rev_acc_lat, 40), 8);
                            div_den     <= rev_cnt_lat;
                        elsif div_cnt = 40 then
                            fwd_avg_reg <= div_q(31 downto 0);
                            div_state   <= DIV_REV;
                            div_cnt     <= 0;
                            div_rem     <= (others => '0');
                            div_q       <= (others => '0');
                            div_num     <= shift_left(resize(rev_acc_lat, 40), 8);
                            div_den     <= rev_cnt_lat;
                        else
                            -- Восстанавливающее деление: частное = (ACC << 8) / CNT (Q8).
                            -- Такт: сдвиг остатка влево + бит делимого, сравнение с делителем,
                            -- вычитание и фиксация бита частного. Через 40 тактов div_q(31:0) = результат.
                            tent_rem := div_rem & div_num(39);
                            if tent_rem >= resize(div_den, 33) then
                                div_rem <= resize(tent_rem - resize(div_den, 33), 32);
                                div_q   <= div_q(38 downto 0) & '1';
                            else
                                div_rem <= tent_rem(31 downto 0);
                                div_q   <= div_q(38 downto 0) & '0';
                            end if;
                            div_num <= div_num(38 downto 0) & '0';
                            div_cnt <= div_cnt + 1;
                        end if;

                    when DIV_REV =>
                        if div_den = 0 then
                            rev_avg_reg  <= (others => '0');
                            div_state    <= DIV_DONE;
                            avg_valid_reg <= '1';
                        elsif div_cnt = 40 then
                            rev_avg_reg  <= div_q(31 downto 0);
                            div_state    <= DIV_DONE;
                            avg_valid_reg <= '1';
                        else
                            -- Восстанавливающее деление: частное = (ACC << 8) / CNT (Q8).
                            -- Такт: сдвиг остатка влево + бит делимого, сравнение с делителем,
                            -- вычитание и фиксация бита частного. Через 40 тактов div_q(31:0) = результат.
                            tent_rem := div_rem & div_num(39);
                            if tent_rem >= resize(div_den, 33) then
                                div_rem <= resize(tent_rem - resize(div_den, 33), 32);
                                div_q   <= div_q(38 downto 0) & '1';
                            else
                                div_rem <= tent_rem(31 downto 0);
                                div_q   <= div_q(38 downto 0) & '0';
                            end if;
                            div_num <= div_num(38 downto 0) & '0';
                            div_cnt <= div_cnt + 1;
                        end if;

                    when DIV_DONE =>
                        null;
                end case;
            end if;
        end if;
    end process;

end architecture Behavioral;