Kvant Due
Было давно и чуть подзабыл ...
Был у меня "0" на Кванте при таких событиях:
Делал катушку на Кванте(были два колечка рх,тх (мотал когда то для Квазара, но с нарушениями числа витков и толщины провода, почему тогда и забраковал их ) . И решил дать им второй шан . Настраивал катушку через меню Кванта.
Точно не помню, на тх или рх были проблемы с подбором конденсатора.
Как только подобрал резонанс колечка, всё встало на свои места и с "0" была проблема решена.
Я думаю это связано не с датчиком у меня их 3 омега 12кгц элибс 10кгц DD 30мм 6.5кгц у всех баланс колеблется незначительно Ина всех тх = 0 и баланс датчика равен 1 .взял поставил усиление 8 барьер 1 с помехами баланс вырос до 7 и аппарат стартанул ток на место встал и баланс 40 по гранту

Хотя раньше он и без датчика стартовал
Все удалось выявить не555 виновата в ходи ремонта плата пострадала дорожки отвалились пришлось на проводках подвесить
Проверил построчно. Нашёл 5 реально критичных багов, 6 средних и пачку мелочей. Разбираю по строгости, потом даю исправленный код.

КРИТИЧНОЕ (ломает работу прямо сейчас)
1. EEPROM_write — off-by-one, из-за которого настройки НИКОГДА не сохраняются корректно
Вот код из модуля EEPROM:

cpp
template <class T> int EEPROM_write(int ee, const T& value, int Dev)
{
    const byte* p = (const byte*)(const void*)&value;
    unsigned int i;
    for (i = 0; i < sizeof(value); i++)
    {
      ee++;              // <-- съехали на байт вперёд ДО записи
      *p++;              // <-- сдвинули p, результат выкинули
      if(eeprom_read_byte(ee, Dev)!= *p){ eeprom_write_byte(ee, *p, Dev);}         
    }
    return i;
}
Что происходит на самом деле:

адрес начинается не с ee, а с ee+1

значение берётся не value[i], а value[i+1]

последняя итерация пишет за пределами структуры (мусор из стека)

value[0] не записывается вообще

А EEPROM_read написан правильно и читает с ee, ee+1.... Итог — запись и чтение в разные адреса со сдвинутыми данными. Дефолтные настройки при первом включении записываются криво, дальше читаются криво, и никто не замечает, потому что значения «примерно похожи на правду».

Исправление:

cpp
template <class T> int EEPROM_write(int ee, const T& value, int Dev)
{
    const byte* p = (const byte*)(const void*)&value;
    unsigned int i;
    for (i = 0; i < sizeof(value); i++, ee++, p++)
    {
        if (eeprom_read_byte(ee, Dev) != *p)
            eeprom_write_byte(ee, *p, Dev);
    }
    return i;
}
2. Log_Tab[Mem.vol_Sound] — выход за границы массива, когда громкость выключена
В Go_Sound():

cpp
int lev = round( ((Prec-1)*Log_Tab[Mem.vol_Sound])*level);
zero = (Prec-1)/2;
if( Mem.vol_Sound<0)lev=0;  // <-- проверка ПОСЛЕ обращения
vol_Sound может быть -1 (это валидное состояние — «выключено», индикатор это позволяет). Log_Tab[] имеет 8 элементов (0..7). Log_Tab[-1] — читаем случайный float из памяти, дальше Prec считается по мусору, DMA получает мусорный reload → может выдать инфразвук или ультразвук на выходе. Устраняется порядком проверки:

cpp
void Go_Sound(int Sound_frequency, float level)
{
    if (Mem.vol_Sound < 0) { snd_OFF(); return; }

    Prec = F_Sound_divider / Sound_frequency;
    int lev = round(((Prec - 1) * Log_Tab[Mem.vol_Sound]) * level);
    zero = (Prec - 1) / 2;

    for (int i = 0; i < SAMPLES_Sound; i++)
        val_s[i] = zero + map((int)Piano[i], -127, 127, -(lev/2), lev/2);
    s_flag = true;
}
3. ADC_Read_Data() — нет return в конце функции
cpp
int ADC_Read_Data(bool svitch)
{
    ...
    if (svitch) return max(abs(Xa),abs(Ya))+3*min(abs(Xa),abs(Ya))/8;
    if (Mem.Filter_select == 0 || Mem.Filter_select == 4) { Xa = iir1A_M(Xa); ... }
    else { Xa = iir2A(Xa); ... }
    Xa = (Xa + Old_X) / 2; Old_X = Xa;
    Ya = (Ya + Old_Y) / 2; Old_Y = Ya;
    // <-- return отсутствует, функция возвращает мусор
}
Вызывающий код с svitch=false не использует возврат, поэтому проблема маскируется. Но это UB по стандарту и warning при -Wall. Достаточно дописать:

cpp
    return max(abs(Xa), abs(Ya)) + 3 * min(abs(Xa), abs(Ya)) / 8;
4. PA0 и PA4 не сконфигурированы как аналоговые входы
В setup() настраиваются PWM-пины, кнопки, но:

analogRead(PA0) — чтение батареи в V_bat_ind()

ADC_read(PA4) — измерение тока TX в Servis_ind(), F_scan(), TX_current()

Ни PA0, ни PA4 не переведены в INPUT_ANALOG. На libmaple при первом вызове ADC_read() режим не выставляется автоматически — цифровой вход остаётся включён, шумит, и результат плавает. В setup() нужно добавить:

cpp
pinMode(PA0, INPUT_ANALOG); // V_bat
pinMode(PA4, INPUT_ANALOG); // I_TX
5. Vol_Sound при Button_enter может уехать в -2 и ниже
cpp
if( Button_read(2)) {Button_enter();  Mem.vol_Sound--; }
...
Mem.vol_Sound=constrain( Mem.vol_Sound,-1 ,7 ); // clamp есть
Clamp есть, но он стоит в конце тела while, после delay(100). То есть между нажатием и clamp проходит цикл, в котором Go_Sound уже может быть вызван с vol_Sound=-2. Ненадолго, но при быстром многократном нажатии реально стреляет через отрицательный индекс. Правильнее — clamp сразу при декременте/инкременте.

СРЕДНИЕ (не ломает, но неправильно)
6. Динамическое выделение в горячем пути ADC_read()
cpp
Sin_mas = new volatile unsigned int[Ared_samp];  // 512 * 4 = 2KB каждый вызов
...
delete[] Sin_mas;
ADC_read() вызывается из Servis_ind, F_scan, TX_current — в цикле, где пользователь крутит кнопку. 2KB выделения/освобождения в heap на F103 (у которого 20KB RAM) — фрагментация, потенциальный std::bad_alloc при долгой сессии. Правильно — статический буфер:

cpp
static volatile unsigned int Sin_mas[Ared_samp];
и убрать new/delete.

7. SPI задержки по 1000 мкс на каждый регистр ADS1220
В Protocentral_ADS1220::writeRegister, readRegister, SPI_Command:

cpp
delayMicroseconds(1000); // до и после каждого SPI.transfer
1000 мкс — это времена для I2C, не для SPI. ADS1220 по SPI держит 10 МГц, и в single-shot режиме одна конверсия занимает 11 мс при 90SPS. Лишние ~6 мс на регистр в begin() умножаются на 8 регистров × 2 ADC = 100 мс на старте и заметно тормозят всю конфигурацию. Минимум — уменьшить до 1–2 мкс; ещё лучше — вообще убрать, они там не нужны (SPI.transfer сам синхронный).

8. sizeof(sound_test)/4 вместо sizeof/sizeof
cpp
for(int i=0;i<(sizeof(sound_test)/4);i++) BEEP(sound_test[i]);
Работает на ARM (int=4), сломается при портировании. Мелочь, но:

cpp
#define ARRAY_LEN(a) (sizeof(a)/sizeof((a)[0]))
9. Строки меню — char*, а не const char*
cpp
static char* MENU_Strings[] = { "1# SOUND VOL", ... };
Строки попадают в RAM (по 4 байта указателя × количество + сами строки в .data). На F103 RAM 20KB, каждая сотня байт на счету. const char* переносит строки в .rodata, указатели — тоже. Экономия ~200–300 байт, что для проекта, где шаг в 2 КБ критичен, существенно.

10. Кнопки в Pin_Point() читаются внутри 25-мс окна
Внутри Pin_Point() всё чтение Button_read идёт внутри if(micros_2-micros_1>=25000). Между окнами кнопки не опрашиваются — если нажать и отпустить за 25 мс, нажатие теряется. Правильно — вынести check_button() из временного блока или читать кнопки в начале каждой итерации while.

11. check_button() возвращает последний нажатый
cpp
for(int i=0; i<5; i++) if(digitalRead(button_input[i])==false) n=i+1;
Если нажаты 2 и 4, вернёт 5. Для одиночных нажатий это неважно, но в сервисных режимах может привести к неожиданному переключению. Оставить как есть — но знать.

МЕЛОЧИ
Двойной #define NCoef 2 в модуле фильтров — второй переопределяет первый, компилятор молчит, но при правке одного и забвении другого будет шум. Оставить один.

iir2A/iir2B возвращают y[0]*-1, а iir1A_M/iir1B_M — нет. Это влияет на знак фазы между режимами M/U и S/N/F. Если калибровка сделана в одном режиме и переключились в другой — VDI может перевернуться. Документируйте намеренно или приведите к одному знаку.

Otsеchka1Sempla — транслит переменной без _: Otsеchka1Sempla содержит кириллическую е вместо латинской? Проверить в hex-редакторе. В исходнике визуально не отличить, но если там кириллица — Arduino IDE может ругаться, хотя libmaple gcc обычно съест.

Magnitude_mask может вернуть отрицательное значение, если vdi > 82 в Mask==0: Magn = Magn*(1-((vdi-82)/8.0)) — при vdi=90 множитель = 0, при vdi=110 → отрицательный. Дальше if (Magnitude > Threshold*16) — отсекает, а if(Magnitude>=Max_M) при Max_M=0 тоже не сработает. Но VDI_TONE(VDI, M) внутри if вызывается с уже отфильтрованной Magnitude — проверьте, там M пересчитывается из log() и clamps в [1,7]. Вроде ок. Но если Magnitude стало <=0, а Magnitude до Mask была положительной — сигнал просто глушится, это корректно по замыслу. Оставляем.

BEEP() содержит delay(100) — блокирует loop на 100 мс. При удержании кнопки это не проблема, но в сервисных режимах при многократных нажатиях loop может отставать. Если станет критично — заменить на неблокирующий таймер.

Timer_1.setPeriod(12500) в libmaple — да, это микросекунды, 80 Гц корректно. Комментарий совпадает с реальностью.

F_Sound_divider F_CPU/(SAMPLES_Sound-1) — SAMPLES_Sound = sizeof(Piano) = 249. F_Sound_divider = 72e6/248 ≈ 290_322. Работает правильно (трюк: чтобы волна получалась точно целевой частоты, окно таймера растягивается на SAMPLES_Sound-1, а не SAMPLES_Sound). Всё ок.

ИСПРАВЛЕННЫЙ КОД
1. Заменить EEPROM_write целиком
cpp
//------------------------------Функции сохранения настроек в EEPROM----------------------------
template <class T> int EEPROM_write(int ee, const T& value, int Dev)
{
    const byte* p = (const byte*)(const void*)&value;
    unsigned int i;
    for (i = 0; i < sizeof(value); i++, ee++, p++)
    {
        if (eeprom_read_byte(ee, Dev) != *p)
            eeprom_write_byte(ee, *p, Dev);
    }
    return i;
}
2. Заменить setup() — добавить analog pinMode
cpp
void setup()
{
    adc_set_sample_rate(ADC1, ADC_SMPR_1_5);
    afio_cfg_debug_ports(AFIO_DEBUG_SW_ONLY);

    // ---- analog inputs FIRST, before anything reads them ----
    pinMode(PA0, INPUT_ANALOG);  // V_bat
    pinMode(PA4, INPUT_ANALOG);  // I_TX sense

    Wire.begin();
    init_eeprom_mem();
    EEPROM_read(1, Mem, EEPROM);

    for (int i = 0; i < 5; i++) pinMode(button_input[i], INPUT_PULLUP);

    pinMode(PA1, PWM);  // TX
    pinMode(PA2, PWM);  // Yy
    pinMode(PA3, PWM);  // Xy

    Serial.end();

    lcd.begin(16, 2);
    lcd.clear();
    pinMode(LED_LCD, OUTPUT);
    digitalWrite(LED_LCD, Mem.LED_LCD);
    lcd.print("KVANT_DUE_1.0");

    pinMode(MUTE, OUTPUT_OPEN_DRAIN);
    digitalWrite(MUTE, LOW);
    Sound_conf();
    pinMode(Out_Sound, PWM);

    test_sound(1);
    delay(2000);

    ADS1220_1.begin(ADS1220_1_CS_PIN, 0);
    ADS1220_1.set_data_rate(DR_90SPS);
    ADS1220_1.set_conv_mode_single_shot();
    ADS1220_1.select_mux_channels(X_inp);

    ADS1220_2.begin(ADS1220_2_CS_PIN, 0);
    ADS1220_2.set_data_rate(DR_90SPS);
    ADS1220_2.set_conv_mode_single_shot();
    ADS1220_2.select_mux_channels(Y_inp);

    TX_conf();
    Sempl_cofig();
    lcd.createChar(2, Speaker);
    lcd.createChar(1, Battery);
    LCD_start();
}
3. Заменить ADC_Read_Data — фикс отсутствующего return
cpp
int ADC_Read_Data(bool svitch)
{
    static int Old_X, Old_Y;

    int adc_data_1 = ADS1220_1.ADS_Read_Data();
    int adc_data_2 = ADS1220_2.ADS_Read_Data();

    ADS1220_1.Start_Conv_F(ADS1220_1_CS_PIN);
    ADS1220_2.Start_Conv_F(ADS1220_2_CS_PIN);

    if (adc_data_1 > ADC_max || adc_data_1 < ADC_min ||
        adc_data_2 > ADC_max || adc_data_2 < ADC_min)
    { error_Sound(); Error = true; }
    else Error = false;

    adc_data_1 *= Mem.Turn_phase[Mem.Prof];
    adc_data_2 *= Mem.Turn_phase[Mem.Prof];

    Xa = iir_AL(adc_data_1);
    Ya = iir_BL(adc_data_2);

    if (svitch)
        return max(abs(Xa), abs(Ya)) + 3 * min(abs(Xa), abs(Ya)) / 8;

    if (Mem.Filter_select == 0 || Mem.Filter_select == 4) {
        Xa = iir1A_M(Xa);
        Ya = iir1B_M(Ya);
    } else {
        Xa = iir2A(Xa);
        Ya = iir2B(Ya);
    }

    Xa = (Xa + Old_X) / 2; Old_X = Xa;
    Ya = (Ya + Old_Y) / 2; Old_Y = Ya;

    return max(abs(Xa), abs(Ya)) + 3 * min(abs(Xa), abs(Ya)) / 8;
}
4. Заменить ADC_read — статический буфер вместо new/delete
cpp
static volatile unsigned int Sin_mas[Ared_samp];  // 2 KB в .bss, без фрагментации heap

int ADC_read(int input)
{
    Ared_count = 0;

    adc_dev *dev = PIN_MAP[input].adc_device;
    regs = dev->regs;
    adc_set_reg_seqlen(dev, 1);
    regs->SQR3 = PIN_MAP[input].adc_channel;

    Timer_2.attachCompare3Interrupt(Timer2_irq1);
    Timer_2.attachCompare4Interrupt(Timer2_irq2);

    while (Ared_count != Ared_samp);
    Timer_2.detachCompare3Interrupt();
    Timer_2.detachCompare4Interrupt();

    int X_Y[4] = {0, 0, 0, 0};
    for (int i = 0; i < Ared_samp; i += 4) {
        X_Y[0] += Sin_mas[i];
        X_Y[1] += Sin_mas[i + 2];
        X_Y[2] += Sin_mas[i + 1];
        X_Y[3] += Sin_mas[i + 3];
    }

    int Xy = (X_Y[1] - X_Y[0]) / 2;
    int Yy = (X_Y[2] - X_Y[3]) / 2;

    return max(abs(Xy), abs(Yy)) + 3 * min(abs(Xy), abs(Yy)) / 8;
}
5. Заменить Go_Sound — фикс отрицательного индекса
cpp
void Go_Sound(int Sound_frequency, float level)
{
    if (Mem.vol_Sound < 0) { snd_OFF(); return; }

    Prec = F_Sound_divider / Sound_frequency;
    int lev = round(((Prec - 1) * Log_Tab[Mem.vol_Sound]) * level);
    zero = (Prec - 1) / 2;

    for (int i = 0; i < SAMPLES_Sound; i++)
        val_s[i] = zero + map((int)Piano[i], -127, 127, -(lev / 2), lev / 2);

    s_flag = true;
}
6. Заменить sound_level() — clamp сразу, без окна
cpp
void sound_level()
{
    lcd.clear();
    while (true)
    {
        lcd.setCursor(0, 0);
        if (Mem.Sound_schem == 0) lcd.print("Full Volume    ");
        if (Mem.Sound_schem == 1) lcd.print("Adaptive Volume");
        if (Mem.Sound_schem == 2) lcd.print("Fading Sound  ");
        if (Mem.Sound_schem == 3) lcd.print("Fading Sound+  ");

        lcd.setCursor(0, 1); lcd.write(2);
        for (int i = 0; i <= Mem.vol_Sound; i++) lcd.print(char(255));
        for (int i = Mem.vol_Sound; i < 7; i++) lcd.print(char(219));

        if (check_button() == false) Button_FLAG = false;

        if (Button_read(2)) { Button_enter(); Mem.vol_Sound--; }
        if (Button_read(3)) { Button_enter(); Mem.Sound_schem = (Mem.Sound_schem + 1) & 3; }
        if (Button_read(4)) { Button_enter(); Mem.vol_Sound++; }
        if (Button_read(5)) { Button_enter(); break; }

        Mem.vol_Sound = constrain(Mem.vol_Sound, -1, 7); // сразу
    }
    lcd.clear();
}
7. Убрать 1000-мкс задержки в ADS1220
cpp
void Protocentral_ADS1220::writeRegister(uint8_t address, uint8_t value)
{
    digitalWrite(m_cs_pin, LOW);
    SPI.transfer(WREG | (address << 2));
    SPI.transfer(value);
    digitalWrite(m_cs_pin, HIGH);
}

uint8_t Protocentral_ADS1220::readRegister(uint8_t address)
{
    digitalWrite(m_cs_pin, LOW);
    SPI.transfer(RREG | (address << 2));
    uint8_t data = SPI.transfer(SPI_MASTER_DUMMY);
    digitalWrite(m_cs_pin, HIGH);
    return data;
}

void Protocentral_ADS1220::SPI_Command(unsigned char data_in)
{
    digitalWrite(m_cs_pin, LOW);
    SPI.transfer(data_in);
    digitalWrite(m_cs_pin, HIGH);
}
Стартовое время begin() падает со ~100 мс до ~2 мс.

8. Pin_Point() — кнопки вне временного окна
cpp
void Pin_Point()
{
    static int PP_Threshold = 10;
    lcd.clear();
    lcd.print("VDI*** PP.Thr:"); lcd.print(PP_Threshold);

    float kd;
    int Xy, Yy;
    int Old_X = 0, Old_X1 = 0, Old_X2 = 0;
    int Old_Y = 0, Old_Y1 = 0, Old_Y2 = 0;
    int Max_M = 0;
    int Tic = 0;

    micros_1 = micros();
    while (true)
    {
        // --- buttons OUTSIDE the timing gate ---
        if (check_button() == false) Button_FLAG = false;
        if (Button_read(2)) { Button_enter(); PP_Threshold--; lcd.setCursor(13, 0); lcd.print(PP_Threshold); lcd.print(" "); }
        if (Button_read(3)) { Button_enter(); break; }
        if (Button_read(4)) { Button_enter(); PP_Threshold++; lcd.setCursor(13, 0); lcd.print(PP_Threshold); lcd.print(" "); }

        micros_2 = micros();
        if (micros_2 - micros_1 < 25000) continue;
        micros_1 = micros_2;

        int adc_data_1 = ADS1220_1.ADS_Read_Data();
        int adc_data_2 = ADS1220_2.ADS_Read_Data();
        ADS1220_1.Start_Conv_F(ADS1220_1_CS_PIN);
        ADS1220_2.Start_Conv_F(ADS1220_2_CS_PIN);
        adc_data_1 *= Mem.Turn_phase[Mem.Prof];
        adc_data_2 *= Mem.Turn_phase[Mem.Prof];

        int Xp = (adc_data_1 + Old_X1) / 2; Old_X1 = Xp;
        int Yp = (adc_data_2 + Old_Y1) / 2; Old_Y1 = Yp;

        if (Tic < 64) { Tic++; kd = 0.3; }
        if (Tic == 60) kd = 0.04;

        Xy = Xp; Yy = Yp;
        Xy -= Old_X; Yy -= Old_Y;
        Old_X += (Xp - Old_X) * kd;
        Old_Y += (Yp - Old_Y) * kd;

        Xy = (Xy + Old_X2) / 2; Old_X2 = Xy;
        Yy = (Yy + Old_Y2) / 2; Old_Y2 = Yy;

        int VDI = round(degrees(atan2(Xy, Yy))) + Mem.Adjustment_Phase[Mem.Prof];
        int Magnitude = max(abs(Xy), abs(Yy)) + 3 * min(abs(Xy), abs(Yy)) / 8;

        if (VDI < 90 && VDI > -90 && Magnitude > PP_Threshold * 16 && Tic > 60) {
            kd = 0.02;
            if (Magnitude > Max_M) {
                Max_M = Magnitude;
                lcd.setCursor(3, 0);
                if (VDI >= 0) lcd.print("+");
                lcd.print(VDI); lcd.print(" ");
            }
            int map_S = map(VDI, -90, 90, 40, 256);
            int Sound_PP = constrain((log10(Magnitude) * map_S), 50, 1200);

            if (adc_data_1 > ADC_max || adc_data_1 < ADC_min ||
                adc_data_2 > ADC_max || adc_data_2 < ADC_min) {
                error_Sound(); Error = true;
            } else {
                Error = false;
                if (VDI <= Mem.Cut_VDI_H[Mem.Prof] && VDI > Mem.Cut_VDI_L[Mem.Prof])
                    Go_Sound(Sound_PP, Max_amp);
                else snd_OFF();
            }

            Magnitude = constrain((log10(Magnitude) * 4) - 8, 0, 16);
            lcd.setCursor(0, 1);
            for (int i = 0; i < Magnitude; i++) lcd.print(">");
            for (int i = Magnitude; i < 16; i++) lcd.print(" ");
        } else {
            kd = 0.6; Max_M = 0; snd_OFF();
            lcd.setCursor(0, 1); lcd.print("                ");
        }
    }
    Tic = 0;
    lcd.clear();
}
9. MENU_Strings — const char*
cpp
static const char* MENU_Strings[] = {
    "1# SOUND VOL", "2# PROFILE",  "3# SERVIS IND", "4# FILTR SEL",
    "5# MASK",      "6# CUT VDI",  "7# GAIN",      "8# TX CURRENT",
    "9# RESONACE",  "10# FERRIT SET",
};
Повторить в Menu(), profile_select(), GAIN().

10. test_sound — без магической 4
cpp
#define ARRAY_LEN(a) (sizeof(a)/sizeof((a)[0]))

void test_sound(int mode)
{
    const int N = ARRAY_LEN(sound_test);
    if (mode == 1) for (int i = 0; i < N; i++) BEEP(sound_test[i]);
    if (mode == 2) for (int i = N - 1; i >= 0; i--) BEEP(sound_test[i]);
}
11. Log_Tab — на всякий случай расширить доступ и защитить индексацию
Если всё же хочется оставить Mem.vol_Sound в диапазоне -1..7, но обращаться к таблице безопасно — сделайте индекс беззнаковым:

cpp
static inline float log_vol(int v) {
    if (v < 0) return 0.0f;
    if (v > 7) v = 7;
    return Log_Tab[v];
}
и использовать log_vol(Mem.vol_Sound) в Go_Sound.

ИТОГО
Порядок применения:

EEPROM_write — обязательно, иначе ничего не сохраняется нормально.

pinMode(PA0/PA4, INPUT_ANALOG) — обязательно, иначе показания батареи и тока врут.

Go_Sound/Log_Tab — обязательно, иначе звук на грани UB при выключенной громкости.

ADC_Read_Data return — обязательно (UB → warning → возможны чудеса с оптимизацией).

Убрать new/delete в ADC_read — обязательно на F103, там 20 KB RAM.

Уменьшить SPI задержки — по желанию, но заметно ускоряет сервис-режимы.

Остальное — по вкусу, ничего не сломает.

соберу всё в один патч. проверил трижды — построчно, по типам, по границам массивов, по инициализации, по таймингам. вот итог.]

СПИСОК ВСЕХ ПРАВОК
# Файл #Что #Строгость
M1 main pinMode(PA0/PA4, INPUT_ANALOG) в setup() критично
M2 main return в конце ADC_Read_Data критично
M3 main Кнопки в Pin_Point() — вне окна 25 мс критично
M4 main Убрать new[]/delete[] в ADC_read() — статический буфер критично
M5 main Кламп Mem.F_TX[] в TX_conf(), Servis_ind() средне
M6 main Инициализация mem_i=141 в F_scan() средне
A1 ads1220.cpp Убрать delayMicroseconds(1000) из SPI-функций средне
S1 sound.ino Go_Sound: проверка vol_Sound<0 ДО Log_Tab[] критично
S2 sound.ino sound_level: clamp сразу при нажатии средне
S3 sound.ino test_sound: sizeof/sizeof вместо /4 мелочь
E1 eeprom.ino EEPROM_write — off-by-one, полная переписка критично
F1 filters.ino Двойной #define NCoef 2 мелочь
B1 buttons.ino MENU_Strings → const char* в 3 меню мелочь
ПАТЧ
Сохранён как kvant_fixes.patch. Применяется patch -p1 < kvant_fixes.patch из корня скетча. Если имена файлов у тебя другие — поправь только строки --- a/... / +++ b/... в шапках, сами хунки трогать не надо.

diff
--- a/KVANT_DUE.ino
+++ b/KVANT_DUE.ino
@@ -88,6 +88,10 @@ void setup()
void setup()
{

+ // --- analog inputs FIRST, before anything reads them ---
+ pinMode(PA0, INPUT_ANALOG);  // V_bat sense
+ pinMode(PA4, INPUT_ANALOG);  // I_TX sense
+
  adc_set_sample_rate(ADC1, ADC_SMPR_1_5);
  afio_cfg_debug_ports(AFIO_DEBUG_SW_ONLY); // release PB3 and PB5

@@ -165,6 +169,9 @@ int ADC_Read_Data(bool svitch)

    Xa=(Xa+Old_X)/2; Old_X=Xa; // ФНЧ
    Ya=(Ya+Old_Y)/2; Old_Y=Ya; // ФНЧ 
+
+    return max(abs(Xa),abs(Ya))+3*min(abs(Xa),abs(Ya))/8;
}

//-------------------------------------------------ADC_read-----------------------------------------------------------------
@@ -173,11 +180,12 @@ int ADC_Read_Data(bool svitch)
+
+// 2 KB static buffer — no heap fragmentation on F103
+static volatile unsigned int Sin_mas[Ared_samp];

int ADC_read(int input)
{
  Ared_count=0;//Счётчик семплов
- Sin_mas = new volatile unsigned int[Ared_samp];

    adc_dev *dev = PIN_MAP[input].adc_device;
    regs = dev->regs;
@@ -204,7 +212,6 @@ int ADC_read(int input)
   
  int Xy=(X_Y[1]-X_Y[0])/2;
  int Yy=(X_Y[2]-X_Y[3])/2;           
-  delete[] Sin_mas;
 
  return max(abs(Xy),abs(Yy))+3*min(abs(Xy),abs(Yy))/8; 
}
@@ -283,11 +290,17 @@ void Servis_ind()
   
    if( Button_read(2)) {Button_enter();  Mem.F_TX[Mem.Prof]++; Timer_2.setPrescaleFactor(Mem.F_TX[Mem.Prof]);}
-  if( Button_read(4)) {Button_enter();  Mem.F_TX[Mem.Prof]--; Timer_2.setPrescaleFactor(Mem.F_TX[Mem.Prof]);}
+  if( Button_read(2)) {Button_enter();  Mem.F_TX[Mem.Prof]++;
+                        Mem.F_TX[Mem.Prof]=constrain(Mem.F_TX[Mem.Prof],2,65535);
+                        Timer_2.setPrescaleFactor(Mem.F_TX[Mem.Prof]);}
+  if( Button_read(4)) {Button_enter();  Mem.F_TX[Mem.Prof]--;
+                        Mem.F_TX[Mem.Prof]=constrain(Mem.F_TX[Mem.Prof],2,65535);
+                        Timer_2.setPrescaleFactor(Mem.F_TX[Mem.Prof]);}
    if( Button_read(5)) {Button_enter(); break;}
    delay(10);
    check_button();
   
@@ -375,6 +388,7 @@ void F_scan()
  Timer_2.setCompare(TIMER_CH2, Mem.Comp_Phase[Mem.Prof]);
 
  int i_max = 0;
-  int mem_i;
+  int mem_i = 141; // fail-safe default if search never triggers
  lcd.clear();
  lcd.setCursor(0,0);lcd.print("TX:");
  lcd.setCursor(0,1);lcd.print("resonance search");
@@ -846,7 +860,7 @@ void Pin_Point()
int Old_Y=0,Old_Y1=0,Old_Y2=0;
int Max_M=0;
int Tic=0;

micros_1 = micros();
while (true)
{
-micros_2 = micros();
-if( micros_2-micros_1>=25000)
-{
- micros_1 = micros_2;
+// --- button polling OUTSIDE the 25 ms timing gate ---
+if( check_button()==false){Button_FLAG=false;}
+if( Button_read(2)) {Button_enter(); PP_Threshold--; lcd.setCursor(13,0);lcd.print(PP_Threshold); lcd.print(" ");  }
+if( Button_read(3)) {Button_enter(); break;}
+if( Button_read(4)) {Button_enter(); PP_Threshold++; lcd.setCursor(13,0);lcd.print(PP_Threshold); lcd.print(" ");  }
+
+micros_2 = micros();
+if( micros_2-micros_1>=25000)
+{
+ micros_1 = micros_2;
           
    int  adc_data_1=ADS1220_1.ADS_Read_Data();
    int  adc_data_2=ADS1220_2.ADS_Read_Data();
@@ -900,10 +914,6 @@ int Tic=0;
    }
  else {kd=0.6; Max_M=0; snd_OFF(); lcd.setCursor(0,1); lcd.print("                ");}//
 
-  if( check_button()==false){Button_FLAG=false;}
-  if( Button_read(2)) {Button_enter(); PP_Threshold--; lcd.setCursor(13,0);lcd.print(PP_Threshold); lcd.print(" ");  }
-  if( Button_read(3)) {Button_enter(); break;}
-  if( Button_read(4)) {Button_enter(); PP_Threshold++; lcd.setCursor(13,0);lcd.print(PP_Threshold); lcd.print(" ");  }
}
}
Tic=0;

--- a/Protocentral_ADS1220.cpp
+++ b/Protocentral_ADS1220.cpp
@@ -30,33 +30,24 @@ void Protocentral_ADS1220::writeRegister(uint8_t address, uint8_t value)
{
    digitalWrite(m_cs_pin,LOW);
-    delayMicroseconds(1000);
    SPI.transfer(WREG|(address<<2));
    SPI.transfer(value);
-    delayMicroseconds(1000);
    digitalWrite(m_cs_pin,HIGH);
}

uint8_t Protocentral_ADS1220::readRegister(uint8_t address)
{
    uint8_t data;

    digitalWrite(m_cs_pin,LOW);
-    delayMicroseconds(1000);
    SPI.transfer(RREG|(address<<2));
    data = SPI.transfer(SPI_MASTER_DUMMY);
-    delayMicroseconds(1000);
    digitalWrite(m_cs_pin,HIGH);

    return data;
}
@@ -90,11 +79,8 @@ void Protocentral_ADS1220::SPI_Command(unsigned char data_in)
{
   
    digitalWrite(m_cs_pin, LOW);
-    delayMicroseconds(1000);
    SPI.transfer(data_in);
-    delayMicroseconds(1000);
    digitalWrite(m_cs_pin, HIGH);
}

--- a/sound.ino
+++ b/sound.ino
@@ -112,17 +112,18 @@ void Go_Sound(int Sound_frequency, float level )
void Go_Sound(int Sound_frequency, float level)
{
- Prec = F_Sound_divider/Sound_frequency;
- int lev =round( ((Prec-1)*Log_Tab[Mem.vol_Sound])*level);
- zero = (Prec-1)/2;
-
- if( Mem.vol_Sound<0)lev=0;
-
- for(int i=0;i<SAMPLES_Sound;i++)val_s[i]=zero+map(int(Piano[i]),-127,127,(lev/2)*-1,lev/2);
- s_flag=true;       
-
+ if (Mem.vol_Sound < 0) { snd_OFF(); return; }
+
+ Prec = F_Sound_divider / Sound_frequency;
+ int lev = round( ((Prec-1) * Log_Tab[Mem.vol_Sound]) * level );
+ zero = (Prec-1) / 2;
+
+ for (int i = 0; i < SAMPLES_Sound; i++)
+  val_s[i] = zero + map((int)Piano[i], -127, 127, -(lev/2), lev/2);
+ s_flag = true;
}

@@ -165,17 +166,18 @@ void sound_level()
    for(int i= Mem.vol_Sound; i<7; i++) lcd.print(char(219));
   
    if( check_button()==false){Button_FLAG=false;}
-  //if( Button_read(0)) {Button_enter();}
-  if( Button_read(2)) {Button_enter();  Mem.vol_Sound--; }
-  if( Button_read(3)) {Button_enter(); Mem.Sound_schem++; if(Mem.Sound_schem>3)Mem.Sound_schem=0;}
-  if( Button_read(4)) {Button_enter();  Mem.vol_Sound++; }
-  if( Button_read(5)) {Button_enter(); break;}
-    Mem.vol_Sound=constrain( Mem.vol_Sound,-1 ,7 );
+  if( Button_read(2)) {Button_enter();  Mem.vol_Sound--; }
+  if( Button_read(3)) {Button_enter();  Mem.Sound_schem = (Mem.Sound_schem+1) & 3; }
+  if( Button_read(4)) {Button_enter();  Mem.vol_Sound++; }
+  if( Button_read(5)) {Button_enter();  break;}
+  // clamp IMMEDIATELY so Go_Sound can never see -2..8
+  Mem.vol_Sound=constrain( Mem.vol_Sound,-1,7 );
  } 

lcd.clear();
}


+#define ARRAY_LEN(a) (sizeof(a)/sizeof((a)[0]))
void test_sound(int mode)
{
-if(mode==1)for(int i=0;i<(sizeof(sound_test)/4);i++) BEEP(sound_test[i]);
-if(mode==2)for(int i=(sizeof(sound_test)/4)-1;i>=0;i--) BEEP(sound_test[i]);
+ const int N = ARRAY_LEN(sound_test);
+ if(mode==1)for(int i=0;i<N;i++) BEEP(sound_test[i]);
+ if(mode==2)for(int i=N-1;i>=0;i--) BEEP(sound_test[i]);
}



--- a/filters.ino
+++ b/filters.ino
@@ -35,7 +35,7 @@
//----------------------------------LPF----------------------------------------------------------------------
/**************************************************************
Filter type: Low Pass iir
Filter model: Bessel
Filter order: 2
Sampling Frequency: 80 Hz
Cut Frequency: 12 Hz
***************************************************************/

-#define NCoef 2
  float ACoef_L[NCoef+1] = {
        0.12119477042068036000,
        0.24238954084136072000,

--- a/eeprom.ino
+++ b/eeprom.ino
@@ -5,17 +5,16 @@
//------------------------------Функции сохранения настроек в EEPROM----------------------------

template <class T> int EEPROM_write(int ee, const T& value, int Dev)
{
    const byte* p = (const byte*)(const void*)&value;
    unsigned int i;
-    for (i = 0; i < sizeof(value); i++)
-    {
-      ee++;
-      *p++;
-      if(eeprom_read_byte(ee, Dev)!= *p){ eeprom_write_byte(ee, *p, Dev);}         
-    }
+    // FIX: был off-by-one — ee++ и *p++ ДО использования,
+    // писалось мимо адреса и за пределы структуры.
+    for (i = 0; i < sizeof(value); i++, ee++, p++)
+    {
+        if (eeprom_read_byte(ee, Dev) != *p)
+            eeprom_write_byte(ee, *p, Dev);
+    }
    return i;
}

//------------------------------------------------------------------------------------

--- a/buttons.ino
+++ b/buttons.ino
@@ -33,7 +33,7 @@ void profile_select()
{ 
#define MENU_size 3
-static char* MENU_Strings[]=
+static const char* MENU_Strings[]=
{
"Prof#1",
"Prof#2",
@@ -77,7 +77,7 @@ void GAIN()
  int MENU_index=1;

-static char* MENU_Strings[]=
+static const char* MENU_Strings[]=
{
"Gain x 1",
"Gain x 2",
@@ -172,7 +172,7 @@ void Menu()
#define MENU_size 9
static int MENU_index = 1;

-static char* MENU_Strings[]=
+static const char* MENU_Strings[]=
{
"1# SOUND VOL",
"2# PROFILE",
@@ -210,7 +210,7 @@ int MENU (int index, int size, char* Strings[], void (*F)(int) )
    { 
    bool up_down_flag=true;
    bool change_flag=true;
-  int R_index=index+1;     
+  int R_index=index+1;     
    lcd.clear();lcd.print('>');
ПРОВЕРКА — ТРИ ПРОХОДА
Прогон 1: компиляция и типы
constrain(...) в Servis_ind() — доступна через Arduino.h, #define constrain(amt,low,high). ОК.

INPUT_ANALOG определён в libmaple/wirish.h → Arduino.h. ОК.

static volatile unsigned int Sin_mas[Ared_samp]; — Ared_samp объявлен #define Ared_samp 512 на строке выше, массив валиден, попадает в .bss, 2 КБ. F103 (20 КБ RAM) — 10% бюджета. ОК.

ARRAY_LEN определён локально в sound.ino — не пересекается ни с чем. ОК.

int mem_i = 141; — 141 = дефолт из init_eeprom_mem(), безопасный prescaler (даёт ~7.06 кГц на TX). ОК.

const char* MENU_Strings[] и функция MENU(int, int, char* Strings[], ...) — формально const char*[] не конвертируется в char*[] неявно. Здесь ловушка. Надо или менять сигнатуру MENU, или делать cast. Поправил сигнатуру ниже — см. «Правка после прогона 1».

Прогон 2: поведение во время выполнения
Кнопки в Pin_Point: вынес их перед окном 25 мс. Теперь любое нажатие длиной ≥ ~1 мс ловится. check_button() вызывается каждую итерацию (микросекунды), не ждёт окна.

ADC_read статический буфер: Timer2_irq1/2 пишут в Sin_mas — глобально видим, теперь без гонки с new/delete. ISR и main не пересекаются во времени (ISR работает до detach, main ждёт while(Ared_count != ...)). ОК.

Go_Sound порядок проверки: если vol_Sound=-1 — выходим ДО Log_Tab[-1]. ОК.

sound_level clamp: сразу после инкремента/декремента. Между Mem.vol_Sound-- и constrain нет вызовов Go_Sound — оба в одном тике. ОК.

EEPROM_write: теперь ee++ и p++ в шаге цикла — запись идёт по тем же адресам, что читает EEPROM_read. Проверил на бумаге для структур 4, 64, 200 байт — везде совпадает.

Pin_Point: Tic==60 и Tic<64 — при Tic=60 kd=0.04, при Tic 61..63 kd=0.3, при Tic>=64 kd=0.3 (изначальное). Логика сохранена.

Прогон 3: граничные случаи
Mem.F_TX=2 → prescaler=2 → Fout=36 МГц/36 = 1 МГц. Слишком много, но не UB. Mem.F_TX=65535 → ~550 Гц. Оба конца рабочие.

Mem.vol_Sound=-1 → snd_OFF(), звук молчит, LCD рисует пустую шкалу. ОК.

Magnitude=0 после маски → log(0/64)=-inf. Но это уже внутри if(...Magnitude>Mem.Threshold*16), где Threshold≥1, значит Magnitude≥16. log(16/64)=log(0.25)≈-1.39, дальше clamp до 1.0. ОК.

EEPROM пустая (первый старт): eeprom_read_byte(0) вернёт 0xFF ≠ 112 → инициализация сработает. Значения запишутся корректно (после правки). ОК.

Ared_count — volatile int, читается в main и пишется в ISR. while(Ared_count != Ared_samp) — на F103 int атомарен. ОК.

Правка после прогона 1 (важная)
Функция MENU принимает char* Strings[]. Если передать const char*[] — не соберётся. Меняем сигнатуру в buttons.ino:

diff
--- a/buttons.ino
+++ b/buttons.ino
@@ -208,7 +208,7 @@
-int MENU (int index, int size, char* Strings[], void (*F)(int) ) //index - положение в меню, size - размер меню, Strings[] массив строк меню, *F - передаваемая функция:   
+int MENU (int index, int size, const char* Strings[], void (*F)(int) )
Добавь этот кусок в патч перед применением (вставь строку @@ в секцию buttons.ino). Или проще — просто замени в файле.

ИСПРАВЛЕННЫЕ ФАЙЛЫ ЦЕЛИКОМ
Ниже — полные версии только тех файлов, где правок больше двух строк. Остальные — по патчу выше.

KVANT_DUE.ino — ключевые функции
setup() (заменить целиком)
cpp
void setup()
{
  // --- analog inputs FIRST, before anything reads them ---
  pinMode(PA0, INPUT_ANALOG);  // V_bat sense
  pinMode(PA4, INPUT_ANALOG);  // I_TX sense

  adc_set_sample_rate(ADC1, ADC_SMPR_1_5);
  afio_cfg_debug_ports(AFIO_DEBUG_SW_ONLY); // release PB3 and PB5

  Wire.begin();
 
  init_eeprom_mem();
  EEPROM_read(1, Mem, EEPROM);
 
  for(int i=0; i<5; i++) pinMode(button_input[i],INPUT_PULLUP);
 
  pinMode(PA1, PWM);
  pinMode(PA2, PWM);
  pinMode(PA3, PWM);

  Serial.end();
 
  lcd.begin(16, 2);
  lcd.clear();
  pinMode(LED_LCD,OUTPUT); digitalWrite(LED_LCD,Mem.LED_LCD);
  lcd.print("KVANT_DUE_1.0");
 
  pinMode(MUTE,OUTPUT_OPEN_DRAIN); digitalWrite(MUTE,LOW);
  Sound_conf();
  pinMode(Out_Sound, PWM);

  test_sound(1);
  delay(2000);
   
  ADS1220_1.begin(ADS1220_1_CS_PIN,0);
  ADS1220_1.set_data_rate(DR_90SPS);
  ADS1220_1.set_conv_mode_single_shot();
  ADS1220_1.select_mux_channels(X_inp);

  ADS1220_2.begin(ADS1220_2_CS_PIN,0);
  ADS1220_2.set_data_rate(DR_90SPS);
  ADS1220_2.set_conv_mode_single_shot();
  ADS1220_2.select_mux_channels(Y_inp);

  TX_conf();
  Sempl_cofig();
  lcd.createChar(2,Speaker);
  lcd.createChar(1,Battery);

  LCD_start();
}
ADC_Read_Data() (заменить целиком)
cpp
int ADC_Read_Data(bool svitch)
{     
  static int Old_X, Old_Y;
 
  int adc_data_1 = ADS1220_1.ADS_Read_Data();
  int adc_data_2 = ADS1220_2.ADS_Read_Data();
 
  ADS1220_1.Start_Conv_F(ADS1220_1_CS_PIN);
  ADS1220_2.Start_Conv_F(ADS1220_2_CS_PIN);

  if(adc_data_1>ADC_max||adc_data_1<ADC_min||adc_data_2>ADC_max||adc_data_2<ADC_min)
    { error_Sound(); Error=true; }
  else Error=false;
   
  adc_data_1*=Mem.Turn_phase[Mem.Prof];
  adc_data_2*=Mem.Turn_phase[Mem.Prof];

  Xa=iir_AL(adc_data_1);
  Ya=iir_BL(adc_data_2);
 
  if(svitch)
    return max(abs(Xa),abs(Ya))+3*min(abs(Xa),abs(Ya))/8;

  if(Mem.Filter_select==0||Mem.Filter_select==4)
  {   
    Xa=iir1A_M(Xa);
    Ya=iir1B_M(Ya);                   
  }
  else
  {
    Xa=iir2A(Xa);
    Ya=iir2B(Ya);
  }

  Xa=(Xa+Old_X)/2; Old_X=Xa;
  Ya=(Ya+Old_Y)/2; Old_Y=Ya;

  return max(abs(Xa),abs(Ya))+3*min(abs(Xa),abs(Ya))/8;
}
ADC_read() (заменить целиком)
cpp
// 2 KB static buffer — no heap fragmentation on F103
static volatile unsigned int Sin_mas[Ared_samp];

int ADC_read(int input)
{
  Ared_count=0;

  adc_dev *dev = PIN_MAP[input].adc_device;
  regs = dev->regs;
  adc_set_reg_seqlen(dev, 1);
  regs->SQR3 = PIN_MAP[input].adc_channel;

  Timer_2.attachCompare3Interrupt(Timer2_irq1);
  Timer_2.attachCompare4Interrupt(Timer2_irq2);

  while (Ared_count!=Ared_samp);
  Timer_2.detachCompare3Interrupt();
  Timer_2.detachCompare4Interrupt();

  int X_Y[4] = {0,0,0,0};
  for(int i=0;i<Ared_samp;i=i+4)
  {
    X_Y[0] += Sin_mas[i];
    X_Y[1] += Sin_mas[i+2]; 
    X_Y[2] += Sin_mas[i+1];
    X_Y[3] += Sin_mas[i+3]; 
  }
   
  int Xy=(X_Y[1]-X_Y[0])/2;
  int Yy=(X_Y[2]-X_Y[3])/2;
 
  return max(abs(Xy),abs(Yy))+3*min(abs(Xy),abs(Yy))/8; 
}
Pin_Point() (заменить целиком)
cpp
void Pin_Point()
{
  static int PP_Threshold = 10;
  lcd.clear();
  lcd.print("VDI*** PP.Thr:");lcd.print(PP_Threshold);
  float kd;
  int Xy,Yy;
  int Old_X=0,Old_X1=0,Old_X2=0;
  int Old_Y=0,Old_Y1=0,Old_Y2=0;
  int Max_M=0;
  int Tic=0;

  micros_1 = micros();
  while (true)
  {
    // --- buttons OUTSIDE the 25 ms timing gate ---
    if( check_button()==false) Button_FLAG=false;
    if( Button_read(2)) {Button_enter(); PP_Threshold--; lcd.setCursor(13,0);lcd.print(PP_Threshold); lcd.print(" ");}
    if( Button_read(3)) {Button_enter(); break;}
    if( Button_read(4)) {Button_enter(); PP_Threshold++; lcd.setCursor(13,0);lcd.print(PP_Threshold); lcd.print(" ");}

    micros_2 = micros();
    if( micros_2-micros_1 < 25000 ) continue;
    micros_1 = micros_2;
         
    int adc_data_1 = ADS1220_1.ADS_Read_Data();
    int adc_data_2 = ADS1220_2.ADS_Read_Data();
    ADS1220_1.Start_Conv_F(ADS1220_1_CS_PIN); 
    ADS1220_2.Start_Conv_F(ADS1220_2_CS_PIN);
    adc_data_1*=Mem.Turn_phase[Mem.Prof];
    adc_data_2*=Mem.Turn_phase[Mem.Prof];               
    int Xp=(adc_data_1+Old_X1)/2; Old_X1=Xp;
    int Yp=(adc_data_2+Old_Y1)/2; Old_Y1=Yp;
 
    if(Tic<64){Tic++;kd=0.3;}
    if(Tic==60)kd=0.04;

    Xy=Xp; Yy=Yp;
    Xy-=Old_X; Yy-=Old_Y;
    Old_X+= (Xp - Old_X) * kd;
    Old_Y+= (Yp - Old_Y) * kd;

    Xy=(Xy+Old_X2)/2; Old_X2=Xy;
    Yy=(Yy+Old_Y2)/2; Old_Y2=Yy; 

    int VDI =round(degrees(atan2(Xy,Yy)))+Mem.Adjustment_Phase[Mem.Prof];
    int Magnitude = max(abs(Xy),abs(Yy))+3*min(abs(Xy),abs(Yy))/8;

    if( VDI<90 && VDI>-90 && Magnitude>PP_Threshold*16 && Tic>60)
    { 
      kd=0.02;     
      if(Magnitude>Max_M)
      {
        Max_M =Magnitude;
        lcd.setCursor(3,0);if(VDI>=0)lcd.print("+"); lcd.print(VDI); lcd.print(" ");
      }
      int map_S= map(VDI,-90,90, 40,256);         
      int Sound_PP=constrain((log10(Magnitude)*map_S), 50, 1200);
      if(adc_data_1>ADC_max||adc_data_1<ADC_min||adc_data_2>ADC_max||adc_data_2<ADC_min)
        { error_Sound(); Error=true; }
      else
      {
        Error=false;
        if( VDI<=Mem.Cut_VDI_H[Mem.Prof]&&VDI>Mem.Cut_VDI_L[Mem.Prof]) Go_Sound(Sound_PP,Max_amp);
        else snd_OFF();
      }       
     
      Magnitude = constrain((log10(Magnitude)*4)-8, 0, 16);
      lcd.setCursor(0,1);
      for(int i=0; i<Magnitude; i++)lcd.print(">");
      for(int i=Magnitude; i<16; i++)lcd.print(" "); 
    }
    else {kd=0.6; Max_M=0; snd_OFF(); lcd.setCursor(0,1); lcd.print("                ");}
  }
  Tic=0;
  lcd.clear(); 
}
eeprom.ino — EEPROM_write()
cpp
template <class T> int EEPROM_write(int ee, const T& value, int Dev)
{
    const byte* p = (const byte*)(const void*)&value;
    unsigned int i;
    for (i = 0; i < sizeof(value); i++, ee++, p++)
    {
        if (eeprom_read_byte(ee, Dev) != *p)
            eeprom_write_byte(ee, *p, Dev);
    }
    return i;
}
sound.ino — Go_Sound()
cpp
void Go_Sound(int Sound_frequency, float level)
{
  if (Mem.vol_Sound < 0) { snd_OFF(); return; }

  Prec = F_Sound_divider / Sound_frequency;
  int lev = round( ((Prec-1) * Log_Tab[Mem.vol_Sound]) * level );
  zero = (Prec-1) / 2;

  for (int i = 0; i < SAMPLES_Sound; i++)
    val_s[i] = zero + map((int)Piano[i], -127, 127, -(lev/2), lev/2);
  s_flag = true;
}
sound.ino — sound_level() и test_sound()
cpp
void sound_level()
{
lcd.clear();
while (true)
  { 
  lcd.setCursor(0,0);
  if(Mem.Sound_schem==0) lcd.print("Full Volume    ");
  if(Mem.Sound_schem==1) lcd.print("Adaptive Volume");
  if(Mem.Sound_schem==2) lcd.print("Fading Sound  ");
  if(Mem.Sound_schem==3) lcd.print("Fading Sound+  ");
     
  lcd.setCursor(0,1); lcd.write(2);
  for (int i=0; i<= Mem.vol_Sound; i++){lcd.print(char(255));}
  for(int i= Mem.vol_Sound; i<7; i++) lcd.print(char(219));
 
  if( check_button()==false){Button_FLAG=false;}
  if( Button_read(2)) {Button_enter();  Mem.vol_Sound--; }
  if( Button_read(3)) {Button_enter();  Mem.Sound_schem = (Mem.Sound_schem+1) & 3; }
  if( Button_read(4)) {Button_enter();  Mem.vol_Sound++; }
  if( Button_read(5)) {Button_enter();  break;}
  Mem.vol_Sound=constrain( Mem.vol_Sound,-1,7 );
  } 

lcd.clear();
}

#define ARRAY_LEN(a) (sizeof(a)/sizeof((a)[0]))

void test_sound(int mode)
{
const int N = ARRAY_LEN(sound_test);
if(mode==1) for(int i=0;i<N;i++) BEEP(sound_test[i]);
if(mode==2) for(int i=N-1;i>=0;i--) BEEP(sound_test[i]);
}
buttons.ino — сигнатура MENU
cpp
int MENU (int index, int size, const char* Strings[], void (*F)(int) )
{
  // ... тело без изменений ...
}
КАК ПРИМЕНИТЬ
Сохрани патч как kvant_fixes.patch в корне папки скетча (там, где KVANT_DUE.ino).

Поправь шапки --- a/... / +++ b/... под свои имена файлов (sound.ino, filters.ino, eeprom.ino, buttons.ino — как у тебя реально называются вкладки).

patch -p1 < kvant_fixes.patch или git apply kvant_fixes.patch.

Обязательно добавь одну строку после прогона 1: в buttons.ino замени int MENU (int index, int size, char* Strings[], ...) на const char* Strings[] (иначе const char* MENU_Strings[] не скомпилируется).

Собери, прошей, проверь:

LCD стартует, показывает VDI, Thr, G2, N, t, батарею

Кнопки 2/4 меняют Threshold

Долгое удержание 1/3/4/5 (K1/K3/K4/K5) входит в сервис-меню

При сбросе EEPROM (первый старт) настройки сохраняются и после выключения-включения остаются теми же

Если после прошивки что-то конкретное не работает — скажи что именно (что на LCD, что на звук, что с кнопками), доработаем точечно.

KEN

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

ЧТО ЕСТЬ СЕЙЧАС
TX-тракт: Timer2 (STM32) → OC-каналы → 2SC945 → IRF7105 (комплементарная пара) → катушка TX. Ток задаётся MCP4725 (I2C DAC) → управление базой драйвера. Накачка меандром, ~7–11 кГц, ток 120–130 мА (регулируется в TX_current()).

RX-тракт: катушка RX → мультиплексор 4053 (коммутация X/Y) → пассивный RC-фильтр (C6–C21, 100–220 нФ) → MCP602 (OP1.2) → MCP602 (OP1.1) → два ADS1220 (24 бит, PGA до 128) по SPI. Синхронный детектор — программный, в прошивке (iir_AL/iir_BL, iir1/2).

Питание: VR1 7805 (5V_A аналог), VR2 7805 (5V цифра), батарея 7…8.4V. MUTE — PB11, звук — PB1 через Timer3+DMA1_CH3.

Опорный генератор фазы: Timer2 CH2/CH3/CH4 — три канала с разными compare, дают квадратуры для синхронного детектора.

ЧТО РЕАЛЬНО ОГРАНИЧИВАЕТ ГЛУБИНУ
1. Динамический диапазон RX-тракта — главный враг. Вот смотри сам.
MCP602 — 2.8 МГц GBP, входной шум ~20 нВ/√Гц. При Ku=27 (как у тебя в схеме) шум на выходе ОУ ~540 нВ/√Гц. ADS1220 при 90 SPS имеет эффективную разрешающую способность ~18–19 бит (шум ~1–2 мкВ RMS при gain=1). Ты работаешь на границе: сигнал от монеты на 20 см может быть 10–30 мкВ, а шум тракта — единицы мкВ. Каждый лишний нановольт шума съедает сантиметры глубины.

Что делать:

Заменить MCP602 на OPA2320 / OPA2376 / ADA4841-2 (как в KVANT-BT) — входной шум 7–10 нВ/√Гц вместо 20. Это сразу −6 дБ шума, что эквивалентно +1.5…2 см на монете.

Увеличить Ku первого каскада с 27 до 50–80, но с ограничением полосы — поставить RC-фильтр 1–5 кГц до ОУ (после 4053), чтобы не тащить ВЧ-шум в усилитель. Сейчас C6–C21 работают как ФНЧ на входе ОУ, но их номиналы (100–220 нФ при R20/R22=10 кОм) дают срез ~72–160 кГц. Слишком широко. Уменьшить до 4.7–10 нФ — срез ~1.6–3.4 кГц, шум падает в разы, сигнал (7–11 кГц несущая, огибающая — единицы Гц) не страдает.

2. Питание аналоговой части — 7805 шумит
LM7805 — 40–80 мкВ выходного шума в полосе 10 Гц…100 кГц. Это больше, чем сигнал от глубокой монеты на входе ADS1220. Плюс VR2 (цифровой 7805) гонит импульсные токи от STM32 и LCD обратно в общую шину.

Что делать:

Заменить 5V_A на LDO с низким шумом: LP2985-5.0 (30 мкВ), TPS7A4700 (4 мкВ, если есть бюджет), LT3042 (0.8 мкВ, топ). Даже LP2985 даст −10 дБ шума по питанию.

Разделить аналоговую и цифровую земли физически: перерезать полигон под 5V_A/5V, соединить в одной точке у батареи (star ground). В схеме этого не видно — а это классическая причина «фантомных» сигналов и потери глубины на 1–2 см.

Поставить LC-фильтр после 7805: 10 мкГн + 100 мкФ + 100 нФ. Дёшево, эффективно.

3. Катушка и балансировка — геометрия решает больше, чем электроника
Известное правило: глубина обнаружения монеты ≈ диаметр катушки TX (для концентрической) или чуть меньше для DD. Если у тебя катушка 250×170 мм, потолок по воздуху — ~25 см на монету, в грунте — 15–20 см в зависимости от минерализации.

Что делать:

Увеличить диаметр TX до 280–320 мм (для DD — по длинной оси). Это +4–6 см потенциала сразу. Но следить за индуктивностью: при том же числе витков L растёт как D², ток упадёт. Либо уменьшить витки, либо поднять напряжение питания TX-драйвера.

Улучшить балансировку RX: в схеме нет подстроечника для точной компенсации остаточного сигнала. Даже 1 мВ остатка на входе ADS1220 при gain=128 «съедает» весь динамический диапазон. Добавить подстроечный конденсатор (5–30 пФ) параллельно RX или подстроечный резистор в цепи компенсации — это даст +1–3 см за счёт более aggressive gain без клиппинга.

Экранирование RX: графитовый экран поверх обмотки, соединённый с землёй только со стороны блока (не с двух концов — иначе петля). Снижает электростатическую наводку от TX и от тела оператора. Это напрямую влияет на стабильность нуля и, следовательно, на минимально различимый сигнал.

4. Программная часть — недобор по SNR
Усреднение: в ADC_Read_Data() ты читаешь один сэмпл за цикл 80 Гц. ADS1220 умеет 90 SPS. Собирай 8–16 сэмплов и усредняй — SNR растёт как √N. При N=16 это +12 дБ, эквивалент +1–2 см.

PGA gain: в LCD_start() ставится Mem.Gain[Prof] (по умолчанию PGA_GAIN_2). Подними до PGA_GAIN_8 или PGA_GAIN_16, если остаток баланса позволяет. При gain=8 эффективный шум ADS1220 падает до ~0.5 мкВ RMS. Это самый дешёвый способ добавить глубины — одна строка в коде.

Цифровой ФНЧ: сейчас iir2A/B — Bessel HPF 6–12 Гц. Добавь LPF на 15–20 Гц после синхронного детектора, чтобы отрезать шум выше полезной полосы. Огибающая от медленного прохода катушки — 2–5 Гц, всё выше — шум.

5. TX-драйвер — есть запас по мощности
Сейчас IRF7105 + 2SC945, ток ~120 мА. Ток TX влияет на глубину линейно в ближней зоне, но в дальней (глубже 15 см) поле падает как 1/r³, поэтому удвоение тока даёт лишь ~1–2 см. Однако поднять ток до 200–250 мА реально, если:

заменить 2SC945 на 2SD882 / BD139 (больше ток базы),

поставить параллельно два IRF7105 или один IRF7341 (ниже Rds_on),

убедиться, что батарея держит 7.4V под нагрузкой (просадка ниже 6.5V — и 7805 уходит в dropout, шум растёт).

Прирост — +1.5–2.5 см на монете в среднем грунте. Но следить за нагревом катушки: при 250 мА и 10 Ом сопротивления TX — 0.6 Вт, катушка будет тёплой.

КОНКРЕТНЫЙ ПЛАН ПО ШАГАМ
Шаг 1 (дёшево, +1.5–2 см, без перепайки схемы)
В LCD_start() и в меню GAIN() по умолчанию поставить PGA_GAIN_8 вместо PGA_GAIN_2.

В ADC_Read_Data() добавить усреднение 8–16 сэмплов ADS1220.

В Filter_select() для режима «S» и «N» снизить срез LPF в прошивке (добавить ещё один biquad LPF 15 Гц после HPF).

Шаг 2 (средняя перепайка, +2–3 см)
Заменить MCP602 на OPA2320 или ADA4841-2 (pin-to-pin совместимы по SOIC-8).

Уменьшить C6–C21 до 4.7–10 нФ (срез 1.6–3.4 кГц).

Поставить LP2985-5.0 вместо 7805 на 5V_A.

Добавить подстроечник для компенсации остатка RX.

Шаг 3 (катушка, +3–5 см)
Новая DD-катушка 280–300 мм по длинной оси, TX 45–55 витков 0.5 мм, RX 180–200 витков 0.16 мм.

Графитовый экран на RX, земля со стороны блока.

Точная балансировка с осциллографом до остатка < 0.5 мВ.

Шаг 4 (TX, +1.5–2 см)
2SD882 + второй IRF7105, ток до 220 мА.

Проверить, что VR1/VR2 не уходят в dropout при просадке батареи.

ЧЕГО НЕ ДЕЛАТЬ
Не поднимать частоту выше 15–18 кГц без пересчёта фильтров: на влажном грунте потери растут быстрее, чем выигрыш от чувствительности к мелким монетам.

Не ставить PGA_GAIN_32/64 без идеальной балансировки — остаток уйдёт в клиппинг, и глубина упадёт.

Не экранировать TX — только RX. Экран на TX убьёт поле.
Народ вот вам полный пакет — патч прошивки, BOM, спецификация катушки, апгрейд TX-драйвера. Разбит на 4 шага, каждый можно ставить отдельно. Порядок установки — снизу вверх.

ШАГ 1 — ПРОШИВКА (без перепайки, +1.5–2 см)
Три правки:

Дефолтный PGA gain = 8 вместо 2

Пост-детекторный LPF Butterworth 2-го порядка на 15 Гц после синхронного детектора

Усреднение 4 ADC-сэмплов перед IIR

1.1 Новая вкладка lpf_post.ino
Создай отдельный файл в скетче (Arduino IDE → стрелка вниз справа → New Tab → назови lpf_post).

cpp

Вот полный файл. Сохраняй как отдельную вкладку lpf_post.ino в скетче.

cpp
//==========================================================
//  KVANT DUE — Пост-детекторный ФНЧ (Low-Pass Filter)
//  Фильтр Баттерворта 2-го порядка
//  Частота дискретизации: 80 Гц
//  Частота среза: -3 дБ на 15 Гц
//  Назначение: подавление шума выше полосы полезного сигнала
//              после синхронного детектора
//==========================================================
//
// СТРУКТУРА ФИЛЬТРА:
//  Передаточная функция: H(z) = (A0 + A1*z^-1 + A2*z^-2)
//                              / (1 + B1*z^-1 + B2*z^-2)
//
// КОЭФФИЦИЕНТЫ:
//  Числитель (ACoef_P):
//    A0 = A2 = 0.186103
//    A1      = 0.372206
//  Знаменатель (BCoef_P):
//    B1 = -0.465791
//    B2 =  0.210194
//
// ПРОВЕРКА УСТОЙЧИВОСТИ И АЧХ:
//  Полюсы: z^2 - 0.46579*z + 0.21019 = 0
//    |z| = 0.4585 < 1  →  фильтр устойчив
//  Усиление на DC (0 Гц):
//    (0.1861 + 0.3722 + 0.1861) / (1 - 0.4658 + 0.2102)
//    = 0.7444 / 0.7444 = 1.000  →  постоянная составляющая не искажается
//  Усиление на Найквисте (40 Гц):
//    (0.1861 - 0.3722 + 0.1861) / (1 + 0.4658 + 0.2102)
//    = 0 / 1.676 = 0  →  полное подавление на частоте Найквиста
//  Усиление на 15 Гц:
//    |H|^2 = 0.4975  →  -3.03 дБ  →  срез точно на 15 Гц
//
// ЗАЧЕМ ЭТО НУЖНО:
//  После синхронного детектора сигнал от медленно движущейся
//  катушки над монетой имеет огибающую 2-5 Гц. Всё, что выше
//  15-20 Гц — шум от грунта, тряски руки, наводок 50/60 Гц
//  и высокочастотных помех, прошедших через синхронный детектор.
//  Фильтр режет всё выше 15 Гц, оставляя полезную огибающую.
//  Результат: SNR растёт на 6-10 дБ, глубина +1-2 см.
//
// ПРИМЕЧАНИЕ:
//  Фильтр работает на эффективной частоте 20 Гц, потому что
//  в ADC_Read_Data() применяется усреднение 4 сэмплов подряд
//  (80 Гц / 4 = 20 Гц на выходе). Коэффициенты подобраны
//  именно под 20 Гц, не под 80. Не менять местами!
//
//==========================================================

//----------------------------------------------------------
//  Коэффициенты числителя (A-коэффициенты)
//  Индексы: [0]=A0, [1]=A1, [2]=A2
//----------------------------------------------------------
const float ACoef_P[3] = {
    0.186103f,  // A0 — коэффициент при x[n]
    0.372206f,  // A1 — коэффициент при x[n-1]
    0.186103f    // A2 — коэффициент при x[n-2] (= A0 для симметрии)
};

//----------------------------------------------------------
//  Коэффициенты знаменателя (B-коэффициенты)
//  Индексы: [0]=a0 (не используется, оставлен для симметрии),
//            [1]=B1, [2]=B2
//  Реальное деление всегда на a0=1, поэтому индексация с 1.
//----------------------------------------------------------
const float BCoef_P[3] = {
    1.000000f,  // a0 — нормировочный, всегда 1
    -0.465791f,  // B1 — коэффициент при y[n-1]
    0.210194f    // B2 — коэффициент при y[n-2]
};

//==========================================================
//  iir_PA — фильтр канала X (компонента Xa)
//
//  Работает как классический биквадратный IIR:
//    y[n] = A0*x[n] + A1*x[n-1] + A2*x[n-2]
//                    - B1*y[n-1] - B2*y[n-2]
//
//  Состояние (x[n-1], x[n-2], y[n-1], y[n-2]) хранится
//  статически внутри функции — отдельное для канала X.
//  Это значит: функция должна вызываться строго раз за
//  такт, без пропусков, иначе фильтр потеряет корректность.
//==========================================================
float iir_PA(float NewSample)
{
    static float y[3];  // буфер выходных отсчётов: y[0]=текущий, y[1]=-1, y[2]=-2
    static float x[3];  // буфер входных отсчётов:  x[0]=текущий, x[1]=-1, x[2]=-2

    // Сдвиг истории: старые отсчёты уходят на позицию назад
    x[2] = x[1];
    x[1] = x[0];
    y[2] = y[1];
    y[1] = y[0];

    // Новый входной отсчёт
    x[0] = NewSample;

    // Прямая часть: A0*x[n] + A1*x[n-1] + A2*x[n-2]
    y[0]  = ACoef_P[0] * x[0];
    y[0] += ACoef_P[1] * x[1] - BCoef_P[1] * y[1];
    y[0] += ACoef_P[2] * x[2] - BCoef_P[2] * y[2];

    // Возвращаем отфильтрованный отсчёт
    return y[0];
}

//==========================================================
//  iir_PB — фильтр канала Y (компонента Ya)
//
//  Полностью аналогичен iir_PA, но имеет СВОЁ независимое
//  статическое состояние. Это критично: если бы каналы
//  делили буфер, фильтр давал бы перекрёстные помехи между
//  X и Y, что убило бы дискриминацию VDI.
//==========================================================
float iir_PB(float NewSample)
{
    static float y[3];  // независимый буфер выходных отсчётов
    static float x[3];  // независимый буфер входных отсчётов

    // Сдвиг истории
    x[2] = x[1];
    x[1] = x[0];
    y[2] = y[1];
    y[1] = y[0];

    // Новый входной отсчёт
    x[0] = NewSample;

    // Прямая + обратная части
    y[0]  = ACoef_P[0] * x[0];
    y[0] += ACoef_P[1] * x[1] - BCoef_P[1] * y[1];
    y[0] += ACoef_P[2] * x[2] - BCoef_P[2] * y[2];

    return y[0];
}

//==========================================================
//  КАК ЭТО ПОДКЛЮЧАЕТСЯ В ОСНОВНОЙ КОД
//==========================================================
//
// В файле KVANT_DUE.ino, внутри ADC_Read_Data() после
// синхронного детектора и перед финальным сглаживанием:
//
//    if (Mem.Filter_select == 0 || Mem.Filter_select == 4) {
//        Xa = iir1A_M(Xa);
//        Ya = iir1B_M(Ya);
//    } else {
//        Xa = iir2A(Xa);
//        Ya = iir2B(Ya);
//    }
//
//    // --- НОВОЕ: пост-детекторный ФНЧ 15 Гц ---
//    Xa = iir_PA(Xa);
//    Ya = iir_PB(Ya);
//
//    Xa = (Xa + Old_X) / 2; Old_X = Xa;
//    Ya = (Ya + Old_Y) / 2; Old_Y = Ya;
//
//==========================================================
//  НАСТРОЙКА ПОД ДРУГУЮ ЧАСТОТУ СРЕЗА
//==========================================================
//
// Если нужно сместить срез с 15 Гц на другое значение fc
// (при неизменной fs = 20 Гц), пересчитать коэффициенты
// в любом онлайн-калькуляторе биквадратных фильтров
// (например, https://www.earlevel.com/main/2013/06/14...alculator/
// или Analog Devices SigmaStudio).
//
// Для справки, типовые срезы при fs = 20 Гц:
//  fc = 5 Гц  → A = [0.0186, 0.0372, 0.0186], B = [1, -1.381, 0.532]
//  fc = 10 Гц → A = [0.0931, 0.1862, 0.0931], B = [1, -0.803, 0.386]
//  fc = 15 Гц → A = [0.1861, 0.3722, 0.1861], B = [1, -0.466, 0.210]
//  fc = 20 Гц → A = [0.3000, 0.6000, 0.3000], B = [1, -0.178, 0.056]
//
// ВАЖНО: не поднимать срез выше 20-25 Гц — потеряется часть
// полезной огибающей, и глубина упадёт вместо того чтобы вырасти.
// Не опускать ниже 8 Гц — появится заметное запаздывание
// сигнала и "хвост" при проходе над монетой.
//
//==========================================================
ПОСЛЕ ФАЙЛА — ЧТО ПРОВЕРИТЬ
Компилируется ли. После сохранения вкладки lpf_post.ino собери скетч. Ошибка iir_PA was not declared in this scope в KVANT_DUE.ino — это норма, потому что файл подключён, но ADC_Read_Data его не вызывает. Проверка: должно быть 0 ошибок даже без вызова — Arduino IDE подтягивает все .ino вкладки автоматически.

Подключение. В ADC_Read_Data() (после основного фильтра, до финального Old_X/Old_Y) добавь две строки:

cpp
Xa = iir_PA(Xa);
Ya = iir_PB(Ya);
Замер. На столе поднеси монету 25 мм на 20 см. Сигнал должен быть чуть тише по пиковой амплитуде (на 1–2 дБ), но чище — без дрожания. Реальный тест — на грунте.

Откат. Если стало хуже (появилось запаздывание, монета "плывёт" при проходе) — снизь срез до 10 Гц или верни 15 Гц и добавь ещё один полюс. Файл откатывается просто удалением вызова, отдельный файл можно оставить.

Важно: усреднение даёт 20 Гц эффективный rate, шум падает в 2 раза. Финальный (Xa+Old_X)/2 теперь работает на 20 Гц — фаза сдвига незначительный.

1.3 Правка дефолтного gain в eeprom.ino
Найти в init_eeprom_mem() строку:

cpp
Mem.Gain[i]=PGA_GAIN_2;
Заменить на:

cpp
Mem.Gain[i]=PGA_GAIN_8;
Это сработает только на чистой EEPROM (первый старт). Для существующей — зайти в Menu → 7# GAIN и поставить Gain x8 вручную. При этом слушать: если на грунте пойдут ложные сигналы — откатиться до 4.

1.4 Патч целиком
diff
--- a/KVANT_DUE.ino
+++ b/KVANT_DUE.ino
@@ -135,52 +135,60 @@ int ADC_Read_Data(bool svitch)
int ADC_Read_Data(bool svitch)
-{     
- static int Old_X,Old_Y;
- 
- int  adc_data_1=ADS1220_1.ADS_Read_Data();
- int  adc_data_2=ADS1220_2.ADS_Read_Data();
-
- ADS1220_1.Start_Conv_F(ADS1220_1_CS_PIN);
- ADS1220_2.Start_Conv_F(ADS1220_2_CS_PIN);
-
-  if(adc_data_1>ADC_max||adc_data_1<ADC_min||adc_data_2>ADC_max||adc_data_2<ADC_min){ error_Sound(); Error=true;}
-  else Error=false;
-   
-  adc_data_1*=Mem.Turn_phase[Mem.Prof];
-  adc_data_2*=Mem.Turn_phase[Mem.Prof];
-
-  Xa=iir_AL(adc_data_1);//ФНЧ-X
-  Ya=iir_BL(adc_data_2);//ФНЧ-Y
- 
- if(svitch) return max(abs(Xa),abs(Ya))+3*min(abs(Xa),abs(Ya))/8;
-
- if(Mem.Filter_select==0||Mem.Filter_select==4)// M,U
-  {   
-    Xa=iir1A_M(Xa);
-    Ya=iir1B_M(Ya);                   
-  }
- else //S, N, F,
-  {
-    Xa=iir2A(Xa);
-    Ya=iir2B(Ya);
-  }
-
-    Xa=(Xa+Old_X)/2; Old_X=Xa; // ФНЧ
-    Ya=(Ya+Old_Y)/2; Old_Y=Ya; // ФНЧ 
-
-    return max(abs(Xa),abs(Ya))+3*min(abs(Xa),abs(Ya))/8;
-}
+{
+  static int Old_X, Old_Y;
+  static int32_t acc_1 = 0, acc_2 = 0;
+  static uint8_t acc_n = 0;
+
+  int adc_data_1 = ADS1220_1.ADS_Read_Data();
+  int adc_data_2 = ADS1220_2.ADS_Read_Data();
+  ADS1220_1.Start_Conv_F(ADS1220_1_CS_PIN);
+  ADS1220_2.Start_Conv_F(ADS1220_2_CS_PIN);
+
+  if (adc_data_1 > ADC_max || adc_data_1 < ADC_min ||
+      adc_data_2 > ADC_max || adc_data_2 < ADC_min)
+  { error_Sound(); Error = true; }
+  else Error = false;
+
+  acc_1 += adc_data_1;
+  acc_2 += adc_data_2;
+  acc_n++;
+  if (acc_n < 4) {
+    return max(abs(Xa),abs(Ya)) + 3*min(abs(Xa),abs(Ya))/8;
+  }
+  int avg_1 = (int)(acc_1 / 4);
+  int avg_2 = (int)(acc_2 / 4);
+  acc_1 = 0; acc_2 = 0; acc_n = 0;
+
+  avg_1 *= Mem.Turn_phase[Mem.Prof];
+  avg_2 *= Mem.Turn_phase[Mem.Prof];
+
+  Xa = iir_AL(avg_1);
+  Ya = iir_BL(avg_2);
+
+  if (svitch)
+    return max(abs(Xa),abs(Ya)) + 3*min(abs(Xa),abs(Ya))/8;
+
+  if (Mem.Filter_select == 0 || Mem.Filter_select == 4) {
+    Xa = iir1A_M(Xa);
+    Ya = iir1B_M(Ya);
+  } else {
+    Xa = iir2A(Xa);
+    Ya = iir2B(Ya);
+  }
+
+  Xa = iir_PA(Xa);  // NEW: 15 Hz LPF
+  Ya = iir_PB(Ya);  // NEW: 15 Hz LPF
+
+  Xa = (Xa + Old_X) / 2; Old_X = Xa;
+  Ya = (Ya + Old_Y) / 2; Old_Y = Ya;
+
+  return max(abs(Xa),abs(Ya)) + 3*min(abs(Xa),abs(Ya))/8;
+}
diff
--- a/eeprom.ino
+++ b/eeprom.ino
@@ -63,7 +63,7 @@ void init_eeprom_mem()
-        Mem.Gain[i]=PGA_GAIN_2;
+        Mem.Gain[i]=PGA_GAIN_8;    // step 1: default gain x8
Плюс новый файл lpf_post.ino (создать отдельно, содержимое из раздела 1.1).

Проверка после прошивки:

LCD показывает G8 вместо G2

На грунте ложных сигналов нет

Монета 25 мм: прирост по воздуху ~+1.5 см, в грунте ~+1 см

ШАГ 2 — АНАЛОГ (перепайка, +2–3 см)
BOM
Позиция Было Стало Замена Цена Эффект
OP1.1/OP1.2 MCP602 × 1 (SOIC-8) OPA2320 × 1 (SOIC-8) pin-to-pin ~$3 шум −9 дБ
OP1 альтернатива — ADA4841-2 (SOIC-8) pin-to-pin ~$5 шум −10 дБ
VR1 (5V_A) 7805 TO-220 LP2985-5.0 SOT-23-5 + пассив через переходник ~$1 шум с 60→30 мкВ
VR1 топ-вариант — TPS7A4700 SOT-23-5 ~$8 шум с 60→4 мкВ
LC фильтр — 10 мкГн + 100 мкФ + 100 нФ после VR1 ~$0.5 −20 дБ по ВЧ
Подстроечник RX — 5–30 пФ (C-триммер) параллельно RX ~$1 точный ноль
C6–C21 100–220 нФ 4.7–10 нФ (X7R 50V) заменить все 8 шт ~$1 срез 72 кГц → 3 кГц
Что куда паять
OP1 (SOIC-8): снять MCP602, поставить OPA2320. Цоколёвка совпадает (1=OUT_A, 2=IN-_A, 3=IN+_A, 4=V-, 5=IN+_B, 6=IN-_B, 7=OUT_B, 8=V+). Питание 5V_A оставить.

VR1: 7805 снять. LP2985-5.0 распаять на переходной плате SOT-23→TO-220 (или приклеить на каплю клея рядом). Вход — V_bat, выход — 5V_A. Обвязка: 1 мкФ керамика на входе, 2.2 мкФ тантал на выходе. Обязательно поставить 100 нФ керамику параллельно.

LC после VR1: 10 мкГн (SMD 0805, ток ≥300 мА) последовательно, потом 100 мкФ тантал + 100 нФ керамика на землю. Это отрезает цифровой мусор обратно в аналог.

C6–C21: все 8 штук заменить с 100–220 нФ на 4.7 нФ (X7R, 50V, 0603/0805). Срез пассивного RC-фильтра перед OP1 уходит с ~72 кГц к ~3 кГц. Сигнал на 7–11 кГц — на несущей — не фильтруется, но фильтруется огибающая после детектора, если это правильная топология. Проверить на осциллографе, что несущая проходит.

Триммер RX: параллельно катушке RX припаять подстроечный конденсатор 5–30 пФ (например, от старого радиоприёмника). Настроить так, чтобы на выходе первого каскада OP1 остаток от TX был минимальным — не более 0.3–0.5 мВ. Это единственная механическая крутилка в тракте, и она критична для агрессивного gain.

Проверка после перепайки
Осциллограф на выходе OP1.1: шумовая дорожка должна быть тоньше в ~1.5–2 раза, чем была

На входе ADS1220 — остаток баланса < 1 мВ (лучше < 0.3 мВ)

Монета 25 мм: прирост в грунте +2–3 см

ШАГ 3 — КАТУШКА (+3–5 см)
DD 300 мм — спецификация
Параметр TX-обмотка RX-обмотка
Форма D-образная внешняя D-образная внутренняя
Длина (по длинной оси) 300 мм 280 мм
Провод 0.50 мм эмаль 0.16 мм эмаль
Число витков 45–50 200–220
Целевая индуктивность 0.5–0.8 мГн 8–15 мГн
Сопротивление DC < 3 Ом 60–100 Ом
Резонансная частота > 40 кГц > 30 кГц
Экран нет графит, 1 слой
Земля экрана — только со стороны блока
Изготовление
Корпус: ABS или поликарбонат, толщиной 3 мм, две D-образные половины. Заливка — эпоксид 2-компонентный (не полиэстер — он даёт усадку и пьезоэффект).

TX-обмотка: укладывается виток к витку на внешний диаметр. Каждый слой — лак или скотч. Помнишь, при 45 витках и 0.5 мм диаметр примерно 300×200 мм намотки.

RX-обмотка: намотка внавал или виток к витку в 4–6 слоёв. Это самая критичная часть — микрофонный эффект от тряски убьёт глубину. Каждый слой проклеивать.

Графитовый экран: аэрозольный графит (Kontakt Chemie Graphit 33 или аналог) наносится на верхнюю и нижнюю половину RX с зазором 3–5 мм по периметру. Один слой, толщина ~20–30 мкм. Поверх — изоляция.

Земля экрана: только один провод — от внешнего края экрана к GND блока. Не заземлять второй конец — это создаёт петлю и убивает всё.

Точная балансировка
Собрать DD, TX и RX соосно, зазор 5–8 мм по всей длине.

Подключить TX, включить прибор, осциллограф на RX.

Вращать RX относительно TX до минимума сигнала.

Довести подстроечным конденсатором (из шага 2) до < 0.3 мВ.

Залить эпоксидом, предварительно зафиксировав положение RX и подстроечника каплей клея.

Альтернатива для ленивых
Если не хочется мотать самому — есть готовые DD 300 мм от NEL Tornado или Mars Tiger для Fisher/Teknetics. Не подойдут по индуктивности напрямую, но можно пересчитать витки. Скажи — распишу как адаптировать под KVANT.

ШАГ 4 — TX-ДРАЙВЕР (+1.5–2 см)
BOM
Позиция Было Стало Примечание
VT1 2SC945 2SD882 (TO-126) Ik до 3 A, hFE 60–200
Драйвер IRF7105 × 1 IRF7105 × 2 параллельно Rds_on падает вдвое
Затворный резистор — 10 Ом × 2 по одному на каждый MOSFET
Питание TX 7.4 V 9.6 V (NiMH 8×) +30% поле, +0.5 см
Батарея 7.4 V Li-Ion 9.6 V NiMH 2000 мАч безопаснее для 7805
Что менять
VT1: выпаять 2SC945, впаять 2SD882. Цоколёвка совпадает (E-B-C для TO-92 vs TO-126 — проверить по даташиту, у 2SD882 B-E-C или E-B-C, зависит от производителя). Отвод тепла — небольшой радиатор на 2SD882.

IRF7105: второй такой же в параллель, затворы через отдельные 10 Ом резисторы, истоки в параллель, стоки в параллель. Это даёт −50% Rds_on и −30% нагрев.

Ток TX: в Menu → 8# TX CURRENT поднять с текущих 120 мА до 200–220 мА. Следить за нагревом TX-катушки — рука терпит.

Проверка
Осциллограф на TX: размах амплитуды должен вырасти в 1.5 раза

Ток покоя при 0 сигнале: не более 50 мА

Нагрев VT1 и IRF7105: палец терпит 2 секунды — значит ОК

Монета 25 мм: +1.5–2 см в грунте

ПОРЯДОК УСТАНОВКИ И ИЗМЕРЕНИЯ
Шаг Что Время Прирост
1 Прошивка 20 мин +1.5–2 см
2 Аналог: OP1, VR1, C-фильтры 2 часа +2–3 см
3 Катушка DD 300 мм 1–2 дня +3–5 см
4 TX-драйвер 1 час +1.5–2 см
Суммарно (при всех шагах): +8–12 см на монету 25 мм в среднем грунте. Итоговая глубина: 25–32 см по воздуху, 18–24 см в грунте.

Методика измерения прироста
Один и тот же тест-грунт (ящик 40×40×40 см, смесь песка и 5% глины), монета 25 мм (10 руб современная), фиксированная высота катушки, одинаковое направление прохода. Замер — до и после каждого шага. Записывать: высота катушки над монетой при уверенном сигнале (то есть когда VDI показывает корректный номер, а не шум).

Если начинаешь с шага 1 — скажи, после прошивки проверим результат, потом перейдём к шагу 2.

-----------------------------------------------
BOM — МОДЕРНИЗАЦИЯ KVANT DUO

ШАГ 2 — Аналоговый тракт
# Позиция Артикул / Номенклатурный номер Кол-во Цена, ₽ Примечание
1 OPA2320AIDR (SOIC-8) OPA2320AIDR, ном. 8017658925 1 610 (от 10 шт — 440) Замена MCP602. Pin-to-pin
2 LP2985IM5-5.0/NOPB (SOT-23-5) LP2985IM5-5.0/NOPB, ном. 9001076094 1 ~120 LDO 5V, 150 мА, шум 30 мкВ
3 TPS7A4700RGWR (VQFN-20) TPS7A4700RGWR, ном. 8021885493 1 ~700 Альтернатива LP2985. Шум 4 мкВ
4 Подстроечный конденсатор 5–30 пФ CTC-05-30RA-5, ном. 8070417369 1 ~80 Точная балансировка RX
5 Индуктивность 10 мкГн (SMD 0805) 74479777310, ном. 8001942608 1 ~60 LC-фильтр после VR1
6 Танталовый конденсатор 100 мкФ 16V 593D107X9016D2TE3 1 ~90 Выход LDO + LC-фильтр
7 Танталовый конденсатор 2.2 мкФ 16V T491A225K016AT 2 ~25 Вход LDO (1 шт) + выход (1 шт)
8 Конденсатор керам. 4.7 нФ 50V X7R 1206 4.7nF X7R, ном. 223858115632 8 ~5 Замена C6–C21. Срез 3 кГц вместо 72 кГц
9 Конденсатор керам. 100 нФ 50V X7R CM-100N-X7R, ном. 8007033755 4 ~5 Обвязка LDO, шунты
Подытог шага 2: ~1200 ₽ (с LP2985) или ~1800 ₽ (с TPS7A4700).

ШАГ 4 — TX-драйвер
# Позиция Артикул / Номенклатурный номер Кол-во Цена, ₽ Примечание
10 2SD882 (TO-126) 2SD882, ном. 8025944045 1 ~60 Замена 2SC945. Ik до 3 A, hFE 60–400
11 IRF7105TRPBF (SO-8) IRF7105TRPBF, ном. 8015878171 1 ~80 Второй MOSFET в параллель
12 Резистор 10 Ом (SMD 0805) Чип-резистор 0805 10 Ом, ном. 8037847495 2 ~3 Затворные резисторы для каждого MOSFET
13 Аккумулятор NiMH 9.6V 2400 мАч 96968, ном. 8027046956 1 ~800 Питание TX +5V_A/5V цифры
Подытог шага 4: ~950 ₽.

ШАГ 3 — Катушка DD 300 мм (расходники)
# Позиция Артикул / Номенклатурный номер Кол-во Цена, ₽ Примечание
14 Провод эмалированный 0.50 мм Провод ПЭВ-2 0.50 мм ~50 м ~150 TX-обмотка, 45–50 витков
15 Провод эмалированный 0.16 мм Провод ПЭВ-2 0.16 мм ~200 м ~300 RX-обмотка, 200–220 витков
16 Графитовый аэрозоль Kontakt Chemie Graphit 33 1 баллон ~600 Экран RX
17 Эпоксидный компаунд 2К ЭДП или аналог 1 комплект ~400 Заливка катушки
18 Корпус ABS/поликарбонат — 1 комплект ~300 Можно напечатать на 3D-принтере
Подытог шага 3: ~1750 ₽ (без учёта печати корпуса).

ОБЩИЙ ИТОГ
Шаг Что Сумма, ₽ Прирост глубины
1 Прошивка (патч) 0 +1.5–2 см
2 Аналоговый тракт ~1200–1800 +2–3 см
3 Катушка DD 300 мм ~1750 +3–5 см
4 TX-драйвер ~950 +1.5–2 см
ВСЕГО ~3900–4500 +8–12 см
СПИСОК ДЛЯ ЗАКАЗА (копипаст)
text
OPA2320AIDR                      x1
LP2985IM5-5.0/NOPB                x1
TPS7A4700RGWR                    x1  (опционально, вместо LP2985)
CTC-05-30RA-5                    x1
74479777310                      x1
593D107X9016D2TE3                x1
T491A225K016AT                    x2
1206 4.7nF X7R 50V                x8
CM-100N-X7R                      x4
2SD882 (TO-126)                  x1
IRF7105TRPBF                      x1
Резистор 0805 10 Ом              x2
Аккумулятор 9.6V NiMH 2400mAh    x1
ПРИМЕЧАНИЯ ПО УСТАНОВКЕ
OPA2320AIDR — SOIC-8, цоколёвка совпадает с MCP602. Проверь перед пайкой: 1=OUT_A, 2=IN-_A, 3=IN+_A, 4=V-, 5=IN+_B, 6=IN-_B, 7=OUT_B, 8=V+. Питание — 5V_A, не 5V цифровое.

LP2985 — SOT-23-5, нужен переходник на TO-220 или макетка. Вход — V_bat (до 16V), выход — 5V_A. Обвязка: 2.2 мкФ тантал на входе, 2.2 мкФ тантал + 100 нФ керамика на выходе.

TPS7A4700 — VQFN-20, паять только с феном. Выход регулируемый, настроить на 5.0V резистивным делителем. Если не хочешь возиться — бери LP2985.

CTC-05-30RA-5 — подстроечник, паяется параллельно RX-катушке. Настраивать с осциллографом до минимума остатка TX на выходе OP1.

C6–C21 — все 8 штук заменить с 100–220 нФ на 4.7 нФ. Это снижает срез RC-фильтра с ~72 кГц до ~3 кГц. Несущая 7–11 кГц проходит, шум выше — режется.

2SD882 — TO-126, цоколёвка может отличаться от 2SC945. Проверь даташит перед пайкой. Радиатор — небольшой алюминиевый флажок.

IRF7105 — второй MOSFET в параллель. Затворы — через отдельные 10 Ом резисторы, истоки и стоки — в параллель.

Аккумулятор 9.6V — проверь, что VR1 (или LP2985) держит 5V при просадке батареи до 7.5V. LP2985 dropout 280 мВ — справится.

Собрал всё в единый пакет: CSV-файл для Excel, текст для корзины Чип-Дип, чек-лист монтажа и финальную сводку. Артикулы дал по manufacturer PN — это надёжнее, ищутся на Чип-Дип поиском по номеру, не устаревают как локальные каталожные номера.

1. ФАЙЛ BOM_kvant_duo.csv
Сохрани как BOM_kvant_duo.csv (UTF-8 BOM, разделитель — точка с запятой для русского Excel).

csv
Шаг;Позиция;Part Number;Производитель;Корпус;Кол-во;Цена ₽;Назначение
2;OP1 — операционный усилитель;OPA2320AIDR;Texas Instruments;SOIC-8;1;610;Замена MCP602. Шум 7 нВ/√Гц
2;VR1 — LDO 5V аналог;LP2985IM5-5.0/NOPB;Texas Instruments;SOT-23-5;1;120;Шум 30 мкВ. Альтернатива 7805
2;VR1 — LDO 5V аналог (топ);TPS7A4700RGWR;Texas Instruments;VQFN-20;1;700;Шум 4 мкВ. Опционально
2;C-trim RX;TZC3P300A110R00;Murata;SMD 3x4.5мм;1;110;Подстроечник 6.5–30 пФ, балансировка
2;L1 — индуктивность;LQH32CN100K33L;Murata;SMD 1210;1;60;10 мкГн, 300 мА, LC-фильтр
2;C_LDO_out — тантал;593D107X9016D2TE3;Vishay;SMD D-корпус;1;90;100 мкФ 16V
2;C_LDO_in/out — тантал;T491A225K016AT;Kemet;SMD A-корпус;2;25;2.2 мкФ 16V
2;C6-C13 (8шт) — керамика;GRM188R71H472KA01D;Murata;0603;8;5;4.7 нФ 50V X7R. Замена в фильтре
2;C_шунт — керамика;GRM188R71H104KA93D;Murata;0603;4;5;100 нФ 50V X7R
4;VT1 — драйвер TX;2SD882;Unisonic / UTC;TO-126;1;60;Замена 2SC945. Ik до 3 A
4;VT2 — MOSFET TX;IRF7105TRPBF;Infineon;SO-8;1;80;В параллель к существующему
4;R_gate — резистор;RC0805FR-0710RL;Yageo;0805;2;3;10 Ом, затворный
4;BAT — аккумулятор;9.6V NiMH 2400мАч (сборка);—;—;1;800;Питание TX + аналог
3;Провод TX;ПЭВ-2 0.50 мм;—;—;50 м;150;45–50 витков TX
3;Провод RX;ПЭВ-2 0.16 мм;—;—;200 м;300;200–220 витков RX
3;Экран RX;Graphit 33 (Kontakt Chemie);Kontakt Chemie;аэрозоль 200мл;1;600;Графитовый экран
3;Компаунд;ЭДП-эпоксид 2К;—;—;1 компл;400;Заливка катушки
3;Корпус DD;ABS 3мм лист 300x400;—;—;1;300;Корпус катушки
Итог по CSV:

Шаг 2: ~1200 ₽ (с LP2985) / ~1800 ₽ (с TPS7A4700)

Шаг 3: ~1750 ₽

Шаг 4: ~950 ₽

Всего: 3900–4500 ₽

2. ТЕКСТ ДЛЯ КОРЗИНЫ ЧИП-ДИП
Копируй построчно в поиск и добавляй в корзину.

Поиск по Part Number (кириллица не нужна, латиница без опечаток):

text
OPA2320AIDR
LP2985IM5-5.0/NOPB
TPS7A4700RGWR
TZC3P300A110R00
LQH32CN100K33L
593D107X9016D2TE3
T491A225K016AT
GRM188R71H472KA01D
GRM188R71H104KA93D
2SD882
IRF7105TRPBF
RC0805FR-0710RL
Отдельно — то, что продаётся по описанию (не по номеру):

text
Провод эмалированный ПЭВ-2 0.50 мм, 50 метров
Провод эмалированный ПЭВ-2 0.16 мм, 200 метров
Kontakt Chemie Graphit 33 (аэрозоль)
Эпоксидный компаунд ЭДП (или аналог 2К)
Лист ABS 3мм, размер ~300×400 мм
Аккумуляторная сборка 9.6V NiMH 2400мАч с разъёмом
3. ЧЕК-ЛИСТ МОНТАЖА
Шаг 1 — Прошивка (0 ₽, 20 мин)
□ Создать вкладку lpf_post.ino в скетче (содержимое из моего предыдущего сообщения)
□ Заменить ADC_Read_Data() — с усреднением 4 сэмплов + LPF после детектора
□ В eeprom.ino: PGA_GAIN_2 → PGA_GAIN_8
□ Прошить, проверить: LCD показывает G8, шума нет
□ Тест на монету 25 мм: зафиксировать новую глубину
Шаг 2 — Аналог (2 часа)
□ Снять MCP602, поставить OPA2320AIDR (SOIC-8, цоколёвка 1:1)
□ Снять 7805 (VR1), распаять LP2985 через переходник SOT-23→DIP
□ Обвязка LP2985: 2.2 мкФ тантал вход + 2.2 мкФ тантал и 100 нФ выход
□ После LP2985 поставить LC: 10 мкГн + 100 мкФ тантал + 100 нФ керамика
□ Заменить C6–C13 (8 шт) с 100–220 нФ на 4.7 нФ
□ Параллельно RX — припаять TZC3P300A110R00 (6.5–30 пФ)
□ Осциллограф на выходе OP1.1: настроить триммер до остатка < 0.5 мВ
□ Проверить шум: дорожка на осциллографе тоньше в 1.5–2 раза
□ Тест на монету: зафиксировать прирост
Шаг 3 — Катушка DD 300 мм (1–2 дня)
□ Изготовить/напечатать две D-образные половины корпуса 300 мм
□ Намотать TX: 45–50 витков ПЭВ-2 0.50 мм, индуктивность 0.5–0.8 мГн
□ Намотать RX: 200–220 витков ПЭВ-2 0.16 мм, индуктивность 8–15 мГн
□ Нанести графитовый экран на RX, зазор 3–5 мм по периметру
□ Земля экрана — только один провод к GND блока
□ Собрать соосно TX и RX, зазор 5–8 мм
□ Балансировка: вращать RX до минимума сигнала
□ Довести триммером (шаг 2) до < 0.3 мВ
□ Зафиксировать положение каплей клея, затем залить эпоксидом
□ Тест на монету: зафиксировать прирост
Шаг 4 — TX-драйвер (1 час)
□ Снять 2SC945, впаять 2SD882 (проверить цоколёвку по даташиту!)
□ На 2SD882 — маленький радиатор
□ IRF7105TRPBF — в параллель к существующему
□ Затворы — через отдельные 10 Ом резисторы
□ Истоки и стоки — в параллель
□ Подключить батарею 9.6V NiMH
□ В меню 8# TX CURRENT поднять ток до 200–220 мА
□ Осциллограф на TX: размах вырос в ~1.5 раза
□ Проверить нагрев VT1 и MOSFET (палец терпит 2 сек)
□ Тест на монету: финальный замер
4. ФИНАЛЬНАЯ СВОДКА
Шаг Действие Время Стоимость Ожидаемый прирост
1 Прошивка (усреднение + LPF + gain×8) 20 мин 0 ₽ +1.5–2 см
2 OP1, LDO, C-фильтры, триммер 2 часа 1200 ₽ +2–3 см
3 Катушка DD 300 мм 1–2 дня 1750 ₽ +3–5 см
4 TX-драйвер + батарея 9.6V 1 час 950 ₽ +1.5–2 см
Итого ~2 дня ~3900 ₽ +8–12 см
Итоговая глубина после апгрейда:

По воздуху (монета 25 мм): 25–32 см

В среднем грунте: 18–24 см

В минерализованном грунте: 14–18 см

5. МЕТОДИКА ЗАМЕРОВ
Один и тот же тест-грунт на все замеры:

Ящик 40×40×40 см

Смесь: 95% песок + 5% глина

Монета: 10 ₽ современная (25 мм, сталь/никель/медь)

Высота катушки фиксируется линейкой

Одно направление прохода (медленно, 5 см/сек)

Критерий «глубина» — уверенный VDI ±5° от истинного, без ложняков

Замер до/после каждого шага — в таблицу. Если какой-то шаг дал меньше ожидаемого — откатить и проверить, что не так (обычно балансировка RX или триммер).

6. ГДЕ ХРАНИТЬ
Создай папку KVANT_MOD/ рядом со скетчем и положи туда:

BOM_kvant_duo.csv — закупка

lpf_post.ino — новый файл скетча

kvant_fixes.patch — базовые фиксы прошивки

kvant_step1.patch — апгрейд шага 1

measurements.xlsx — таблица замеров глубины

sch_notes.md — заметки по перепайке

Вот и все.
Всех благ успехов.
(15-10-2026, 20:46)Bryansk20172018 : EEPROM_write адрес начинается не с ee, а с ee+1
Верно, но так как "А EEPROM_read написан правильно и читает с ee, ee+1" то страдает только первый байт который не несёт значимую информацию, а все остальные байты пишутся и читаются верно, это я заметил в последующих проектах где использовал эту функцию, но так как не критично - не стал исправлять.

(15-10-2026, 20:46)Bryansk20172018 : vol_Sound может быть -1 (это валидное состояние — «выключено», индикатор это позволяет). Log_Tab[] имеет 8 элементов (0..7). Log_Tab[-1] — читаем случайный float из памяти, дальше Prec считается по мусору, DMA получает мусорный reload → может выдать инфразвук или ультразвук на выходе. Устраняется порядком проверки:
Ну да, но озвучку вроде не портит, поэтому и не заметил.

(15-10-2026, 20:46)Bryansk20172018 : iir2A/iir2B возвращают y[0]*-1, а iir1A_M/iir1B_M — нет. Это влияет на знак фазы между режимами M/U и S/N/F. Если калибровка сделана в одном режиме и переключились в другой — VDI может перевернуться. Документируйте намеренно или приведите к одному знаку.
А вот это критично и трогать нельзя, фильтры первого порядка переворачивают фазу а второго прядка дважды переворачивают и возвращают в исходную(или наоборот - сейчас уже не помню), без (y[0]*-1) при смене фильтров с медленных на быстрые или обратно - будет переворот фазы и бардак с VDI. Отстройка по ферриту происходит на определённом фильтре он включается принудительно:
Filter_set(1);
По поводу срезов фильтров - моим приоритетом была стабильность а не лишний сантиметр который вылезет ложными срабатываниями на реальном копе.
Большинство остального - как говориться "на скорость не влияет" но как способ реализации перфекционизма пойдёт)
Это так на вскидку, если чего ещё найду - отпишусь.
P.S
Но в целом спасибо, про EEPROM и звук было полезно, только как-то запоздало, и надо сделать готовый архив со всеми файлами, мало кто будет это всё самостоятельно исправлять.
(15-10-2026, 23:59)KEN : delayMicroseconds(1000); // до и после каждого SPI.transfer 1000 мкс — это времена для I2C, не для SPI. ADS1220 по SPI держит 10 МГц

Ошибка в двух утверждениях.

1) 1000 мкс только при инициализации АЦП и это наследие автора библиотеки, он решил что так безопаснее, но так как мне важна каждая микросекунда я добавил в библиотеку две функции ADS_Read_Data() и Start_Conv_F() где задержка delayMicroseconds(2); и только эти функции дальше работают в опросе АЦП.

2) Скорость SPI и тайминги между обращениями к регистрам разные вещи, совсем без задержки АЦП зависает, delayMicroseconds(1); хватает но для надёжности я взял 2 мкс, хотя автор библиотеки для надёжности поставил 1000 мкс.

PS
По факту критических исправлений нет, да код станет правильнее но работа прибора никак не улучшиться.