/* * DDS-генератор сигналов на PIO (RP2040) — вариант для Arduino IDE * * ЧЕРНОВИК. Требует проверки преподавателем на реальном стенде. * * ВАЖНО: этот скетч требует ядра **earlephilhower (arduino-pico)**. * В официальном mbed-ядре pico-sdk скрыт от пользовательского кода, * и PIO/DMA недоступны. В шапке это указано не случайно: на mbed-ядре * скетч не соберётся. * Установка ядра: Boards Manager -> "Raspberry Pi Pico/RP2040" * (github.com/earlephilhower/arduino-pico) * * ─── ЧЕМ ОТЛИЧАЕТСЯ ОТ 01-dds-generator-rp2040.ino ─── * * 01-...ino формирует синус программно: каждый такт CPU (в прерывании) * считает очередную выборку и пишет её в ШИМ. Чем выше несущая, тем * больше нагрузка на CPU, и частота выборок ограничена. * * Здесь сигнал формирует аппаратный блок PIO, а данные ему подаёт DMA. * CPU один раз считает буфер и больше не участвует. Программа PIO — * всего две инструкции: * * pull block ; взять 32-битное слово из FIFO (через DMA) * out pins, BITS ; вывести BITS младших бит слова в порт * * ─── О ФОРМЕ СИГНАЛА: ЗАДАЁТСЯ В КОДЕ, А НЕ КОМАНДОЙ ─── * * В C++ нет разбора выражений «на лету» (как команда FP в CircuitPython). * Форма задаётся функцией waveform(t) ниже — студент меняет её и * ПЕРЕКОМПИЛИРУЕТ скетч. Это надёжнее и проще: никакого eval. * * t — «время внутри периода»: 0 в начале, 1 в конце, * шаг 1/m, где m = N*OVERSAMPLE (число точек буфера). * Функция возвращает значение в диапазоне [-1, 1]. * * Примеры (раскомментировать нужную): * * return sinf(2.0f * M_PI * t); // чистый синус * return 0.3f*sinf(2*M_PI*t) + 0.5f*sinf(4*M_PI*t); // сумма гармоник * return 4.0f * fabsf(t - 0.5f) - 1.0f; // треугольник * return 2.0f * t - 1.0f; // пила * return (t < 0.5f) ? 1.0f : -1.0f; // меандр * * ─── ТОЧНОСТЬ УРОВНЯ: ДЕЛЬТА-СИГМА ─── * * Выход меняется только на +1/-1/0 за шаг, поэтому СОСЕДНИЕ ЗНАЧЕНИЯ * буфера всегда отличаются не более чем на 1 (никаких скачков). * Дробную точность даёт чередование соседних кодов, которое усредняет * внешний ФНЧ: число различимых уровней = (2^BITS - 1) * m + 1. * * БИТНОСТЬ и РАЗРЕШЕНИЕ СРЕДНЕГО — разные величины: BITS задаёт * мгновенный код на порту, а разрешение среднего растёт с длиной * буфера m. * * ─── ПИНЫ (выравнивание по СТАРШЕМУ биту) ─── * * Старший бит значения всегда на GP15, данные занимают GP(16-BITS)..GP15: * * BITS=8 -> GP8 ... GP15 (вся гребёнка) * BITS=4 -> GP12 ... GP15, GP8..GP11 не трогаются * BITS=3 -> GP13 ... GP15 * * Выравнивание по старшему биту сохраняет полную шкалу при любом BITS. * * ─── СХЕМА СИНУСА (обязательна после ШИМ/PIO) ─── * * GP15 ──[R1 1к]──┬──[R2 1к]──┬──► OUT (вход DUT) * │ │ * [C1 47н] [C2 47н] * │ │ * GND GND * * При несущей ~62 кГц фильтр даёт f_c ~ 3,4 кГц и подавляет несущую * примерно на 50 дБ. Постоянная составляющая = Uпит/2; где смещение * недопустимо — разделительный конденсатор 1 мкФ + 47 кОм на землю. * * ─── КОМАНДЫ (совместимы с 01-...ino и CircuitPython-версией) ─── * * F <Гц> — частота (1..200000) * SF <Гц> — частота синуса * W Q | W S — режим: Q — прямоугольник (ШИМ), S — синус (PIO) * D <0..255> — скважность прямоугольника * M <0..127> — амплитуда синуса * A — размах на ADC0 (GP26), мВ -> "A <мВ>" * SWEEP f0 f1 n ms — логарифмический свип: "P <Гц> <мВ>", затем "DONE" * STOP — прервать свип * HELP | ? — подробная справка * * ДОПОЛНИТЕЛЬНО (настройки PIO): * * B <1..8> — число бит порта * O <1..64> — точек на сэмпл (m = N*O) * N <16..4096> — длина периода в «полезных» сэмплах * CLC <1..8> — тактов PIO на сэмпл (калибровка частоты) * CAL <Гц> — автокалибровка CPC по измеренной частоте * I — текущие настройки и расчётные частоты * * ВНИМАНИЕ: ADC0 (GP26) — вход 0..3,3 В. Сигнал больше — только через * делитель. Схему без питания к ADC не подключать. */ #include #include // Проверка ядра: PIO требует arduino-pico (earlephilhower) #if !defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_ARCH_MBED) #error "Этот скетч требует ядра earlephilhower (arduino-pico): в mbed-ядре PIO/DMA недоступны. См. шапку файла." #endif #include "hardware/pio.h" #include "hardware/dma.h" #include "hardware/pwm.h" #include "hardware/clocks.h" // ─────────────────────────── пины ─────────────────────────── #define PIN_OUT_MSB 15 // старший бит (data); диапазон GP(16-BITS)..GP15 #define PIN_TRIG 16 // синхроимпульс (вне диапазона PIO) #define PIN_ADC 26 // ADC0 = A0 // ─────────────────────── параметры ────────────────────────── #define CLK_SYS_HZ 125000000UL #define PWM_STEPS 255 #define AMP_MAX 127 // Лимит буфера: длина m должна быть СТЕПЕНЬЮ ДВОЙКИ, чтобы работал // кольцевой режим DMA (ring). Размер кольца ограничен 15 битами = // 32768 байт = 8192 сэмпла по 4 байта. #define MAX_M 8192 // ───────────────────────── состояние ──────────────────────── static volatile uint32_t g_freq = 1000; static volatile uint8_t g_duty = 128; static volatile uint8_t g_amp = AMP_MAX; static volatile bool g_sweep = false; static volatile int g_bits = 8; static volatile int g_oversample = 4; static volatile int g_n = 256; static volatile int g_cpc = 2; // тактов PIO на сэмпл // Буфер сэмплов и ресурсы PIO/DMA. // ВАЖНО: ring DMA требует, чтобы адрес буфера был КРАТЕН его размеру // (например буфер 4096 байт должен стоять на адресе, кратном 4096). // Обычный malloc такого не гарантирует, поэтому выделяем с запасом и // выравниваем указатель вручную (см. buildBuffer). static uint32_t *g_buf = nullptr; // выровненный указатель (для DMA) static void *g_buf_raw = nullptr; // исходный блок (для free) static volatile size_t g_buf_len = 0; static PIO g_pio; static int g_sm = -1; static int g_dma = -1; static int g_prog_offset = -1; // куда загружена PIO-программа static float g_freq_actual = 0.0f; // ══════════════════════════════════════════════════════════════ // ФОРМА СИГНАЛА — МЕНЯТЬ ЗДЕСЬ // ══════════════════════════════════════════════════════════════ // t — время внутри периода, 0..1 с шагом 1/m. Возврат в [-1, 1]. static float waveform(float t) { // Чистый синус (по умолчанию): return sinf(2.0f * (float)M_PI * t); // Студент заменяет строку выше на свою, например: // return 0.3f*sinf(2*M_PI*t) + 0.5f*sinf(4*M_PI*t); // return 4.0f*fabsf(t - 0.5f) - 1.0f; // треугольник // return 2.0f*t - 1.0f; // пила // return (t < 0.5f) ? 1.0f : -1.0f; // меандр } // ─────────────── PIO-программа (две инструкции) ────────────── // Собирается из кодировщиков pio_instructions.h на этапе выполнения — // pioasm не нужен, инструкции тривиальны. static uint16_t g_prog[2]; static void buildPioProgram(int bits) { // pull block -> 0x80a0 g_prog[0] = (uint16_t)pio_encode_pull(false, true); // out pins, bits -> 0x6000 | bits g_prog[1] = (uint16_t)pio_encode_out(pio_pins, (uint)bits); } // ─────────────── расчёт буфера (дельта-сигма) ──────────────── static void freeBuffer() { if (g_buf_raw) { free(g_buf_raw); g_buf_raw = nullptr; } g_buf = nullptr; g_buf_len = 0; } static bool buildBuffer() { int bits = g_bits; long m = (long)g_n * g_oversample; // Кольцевой DMA требует: длина m — степень двойки, адрес кратен m*4. // Подгоняем m вверх до ближайшей степени двойки. long mp = 8; while (mp < m) mp <<= 1; if (mp > MAX_M) return false; m = mp; uint32_t full = (1u << bits) - 1; float amp = (float)g_amp / (float)AMP_MAX; freeBuffer(); size_t bytes = (size_t)m * sizeof(uint32_t); // Выделяем с запасом и выравниваем вручную: ring DMA «заворачивает» // только младшие биты адреса, поэтому адрес обязан быть кратен // размеру буфера — иначе кольцо сместится относительно данных. g_buf_raw = malloc(bytes + bytes); if (!g_buf_raw) return false; uintptr_t raw = (uintptr_t)g_buf_raw; uintptr_t aligned = (raw + bytes - 1) & ~(uintptr_t)(bytes - 1); g_buf = (uint32_t *)aligned; g_buf_len = (size_t)m; // дельта-сигма 1-го порядка; два прохода (второй — установившийся) float v = 0.0f, acc = 0.0f, prev_u = 0.0f; bool have_prev = false; // Период сигнала должен занимать m точек (k/m от 0 до 1). // m — подогнанная до степени двойки длина, поэтому фаза доходит до 1. size_t idx = 0; for (int period = 0; period < 2; ++period) { for (long k = 0; k < m; ++k) { float t = (float)k / (float)m; float u = (waveform(t) + 1.0f) * 0.5f * (float)full * amp; if (u < 0.0f) u = 0.0f; if (u > (float)full) u = (float)full; // ограничение наклона цели до 1 за шаг if (have_prev) { float d = u - prev_u; if (d > 1.0f) u = prev_u + 1.0f; else if (d < -1.0f) u = prev_u - 1.0f; } prev_u = u; have_prev = true; if (period == 0) { // первый проход: только устанавливаем аккумулятор acc += u - v; if (acc >= 1.0f) { v += 1.0f; acc -= 1.0f; } else if (acc <= -1.0f) { v -= 1.0f; acc += 1.0f; } if (v < 0.0f) v = 0.0f; else if (v > (float)full) v = (float)full; } else { acc += u - v; if (acc >= 1.0f) { v += 1.0f; acc -= 1.0f; } else if (acc <= -1.0f) { v -= 1.0f; acc += 1.0f; } if (v < 0.0f) v = 0.0f; else if (v > (float)full) v = (float)full; g_buf[idx++] = (uint32_t)v; } } } return true; } // ─────────────── настройка PIO + DMA ─────────────── static void stopPio() { if (g_sm >= 0) { pio_sm_set_enabled(g_pio, g_sm, false); pio_sm_clear_fifos(g_pio, g_sm); } if (g_dma >= 0) { dma_channel_abort(g_dma); } } static void releasePio() { stopPio(); if (g_prog_offset >= 0) { // убрать программу из памяти PIO: она всего 32 инструкции, // при частых перезапусках без этого память переполнится pio_program_t prog = { g_prog, 2, -1, 0 #if PICO_PIO_VERSION > 0 , 0 #endif }; pio_remove_program(g_pio, &prog, (uint)g_prog_offset); g_prog_offset = -1; } if (g_sm >= 0) { pio_sm_unclaim(g_pio, g_sm); g_sm = -1; } if (g_dma >= 0) { dma_channel_unclaim(g_dma); g_dma = -1; } } static bool startPio() { releasePio(); if (!g_buf || g_buf_len == 0) return false; int bits = g_bits; if (bits < 1 || bits > 8) return false; buildPioProgram(bits); // выбрать свободный блок PIO g_pio = pio0; g_sm = pio_claim_unused_sm(g_pio, false); if (g_sm < 0) { g_pio = pio1; g_sm = pio_claim_unused_sm(g_pio, false); } if (g_sm < 0) return false; pio_program_t prog = { g_prog, 2, -1, 0 #if PICO_PIO_VERSION > 0 , 0 #endif }; int offset = pio_add_program(g_pio, &prog); if (offset < 0) { releasePio(); return false; } g_prog_offset = offset; // пины: диапазон GP(16-bits)..GP15, выравнивание по старшему биту int first = 16 - bits; for (int p = first; p < 16; ++p) { pio_gpio_init(g_pio, p); } pio_sm_set_consecutive_pindirs(g_pio, g_sm, first, bits, true); pio_sm_config c = pio_get_default_sm_config(); sm_config_set_wrap(&c, offset, offset + 1); sm_config_set_out_pins(&c, first, bits); sm_config_set_out_shift(&c, true, false, 32); // right, без autopull sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); // 8 слов TX pio_sm_init(g_pio, g_sm, offset, &c); // частота PIO: f_pio = f_out * CPC * m float f_pio = (float)g_freq * (float)g_cpc * (float)g_buf_len; if (f_pio < 1.0f) f_pio = 1.0f; float max_f = (float)CLK_SYS_HZ; if (f_pio > max_f) f_pio = max_f; pio_sm_set_clkdiv(g_pio, g_sm, (float)CLK_SYS_HZ / f_pio); g_freq_actual = f_pio / ((float)g_cpc * (float)g_buf_len); // DMA: циклический перенос буфера в TX FIFO state machine g_dma = dma_claim_unused_channel(false); if (g_dma < 0) { releasePio(); return false; } dma_channel_config dc = dma_channel_get_default_config(g_dma); channel_config_set_transfer_data_size(&dc, DMA_SIZE_32); channel_config_set_read_increment(&dc, true); channel_config_set_write_increment(&dc, false); channel_config_set_dreq(&dc, pio_get_dreq(g_pio, g_sm, true)); // КОЛЬЦЕВОЙ РЕЖИМ. Требование: длина буфера m — степень двойки, и // адрес буфера выровнен на его размер. Тогда ring сам «заворачивает» // адрес чтения, и один запуск DMA крутит буфер бесконечно: // transfer_count = 0xFFFFFFFF (максимум), а адрес оборачивается // каждые m слов. Без этого после одного прохода DMA остановится // и PIO встанет на pull block — сигнал оборвётся. size_t bytes = g_buf_len * sizeof(uint32_t); uint ring_bits = 0; while ((1u << ring_bits) < bytes) ++ring_bits; channel_config_set_ring(&dc, false, ring_bits); dma_channel_configure(g_dma, &dc, &g_pio->txf[g_sm], // куда: TX FIFO PIO g_buf, // откуда: буфер 0xFFFFFFFFu, // практически бесконечно true); // запустить сразу pio_sm_set_enabled(g_pio, g_sm, true); return true; } // ─────────────── прямоугольник (аппаратный ШИМ) ─────────────── static void squareOn(uint32_t hz, uint8_t duty) { if (hz < 1) hz = 1; gpio_set_function(PIN_OUT_MSB, GPIO_FUNC_PWM); uint slice = pwm_gpio_to_slice_num(PIN_OUT_MSB); uint32_t top = CLK_SYS_HZ / (hz * (PWM_STEPS + 1)); if (top == 0) top = 1; if (top > 65534) top = 65534; // пересчёт фактической частоты uint32_t hz_real = CLK_SYS_HZ / (top * (PWM_STEPS + 1)); pwm_set_clkdiv(slice, 1.0f); pwm_set_wrap(slice, (uint16_t)top); pwm_set_gpio_level(PIN_OUT_MSB, (uint32_t)duty * (top + 1) / 255); pwm_set_enabled(slice, true); g_freq_actual = (float)hz_real; } static void squareOff() { uint slice = pwm_gpio_to_slice_num(PIN_OUT_MSB); pwm_set_enabled(slice, false); gpio_set_function(PIN_OUT_MSB, GPIO_FUNC_SIO); } // ─────────────── режимы ─────────────── static bool g_sine_mode = false; static void applyMode(bool sine) { if (sine) { squareOff(); startPio(); } else { stopPio(); releasePio(); squareOn(g_freq, g_duty); } g_sine_mode = sine; } static void setFrequency(uint32_t hz) { if (hz < 1) hz = 1; if (hz > 200000) hz = 200000; g_freq = hz; if (g_sine_mode) startPio(); else squareOn(g_freq, g_duty); } // ─────────────── измерение амплитуды ─────────────── static uint32_t measureAmplitudeMv(uint32_t window_ms = 50) { uint32_t t0 = millis(); uint16_t vmin = 4095, vmax = 0; while ((millis() - t0) < window_ms) { uint16_t v = (uint16_t)analogRead(PIN_ADC); if (v < vmin) vmin = v; if (v > vmax) vmax = v; } return (uint32_t)(vmax - vmin) * 3300UL / 4095UL; } static void triggerPulse() { digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(50); digitalWrite(PIN_TRIG, LOW); } // ─────────────── свип ─────────────── static void runSweep(float f0, float f1, int n, uint32_t dwell_ms) { if (n < 2) n = 2; if (f0 < 1.0f) f0 = 1.0f; if (f1 <= f0) f1 = f0 * 10.0f; g_sweep = true; float ratio = powf(f1 / f0, 1.0f / (float)(n - 1)); float f = f0; for (int i = 0; i < n && g_sweep; ++i) { setFrequency((uint32_t)f); triggerPulse(); delay(dwell_ms); uint32_t mv = measureAmplitudeMv(); Serial.print("P "); Serial.print((uint32_t)f); Serial.print(' '); Serial.println(mv); f *= ratio; } Serial.println("DONE"); setFrequency((uint32_t)f0); } // ─────────────── справка ─────────────── static void printHelp() { Serial.println(F("# ─────────── DDS-генератор на PIO (Arduino): команды ───────────")); Serial.println(F("# РЕЖИМ И ЧАСТОТА")); Serial.println(F("# W Q / W S — прямоугольник (ШИМ) / синус (PIO)")); Serial.println(F("# F <Гц> — частота, 1..200000")); Serial.println(F("# SF <Гц> — частота синуса")); Serial.println(F("# ФОРМА")); Serial.println(F("# D <0..255> — скважность (128 = 50 %), режим Q")); Serial.println(F("# M <0..127> — амплитуда синуса")); Serial.println(F("# ПРИБОР")); Serial.println(F("# A — размах на ADC0 (GP26), мВ")); Serial.println(F("# SWEEP f0 f1 n ms — логарифмический свип")); Serial.println(F("# STOP — прервать свип")); Serial.println(F("# НАСТРОЙКИ PIO")); Serial.println(F("# B <1..8> — число бит порта (MSB на GP15)")); Serial.println(F("# O <1..64> — точек на сэмпл (m = N*O)")); Serial.println(F("# N <16..4096> — длина периода в сэмплах")); Serial.println(F("# CLC <1..8> — тактов PIO на сэмпл (калибровка)")); Serial.println(F("# CAL <Гц> — автокалибровка по измеренной частоте")); Serial.println(F("# I — текущие настройки")); Serial.println(F("# СПРАВКА: HELP или ?")); Serial.println(F("# ФОРМА СИГНАЛА задаётся функцией waveform(t) в коде!")); Serial.println(F("# ─────────────────────────────────────────────")); } static void printInfo() { // Фактическая длина буфера — g_buf_len (подогнана до степени двойки), // а не N*O: кольцевой DMA требует степень двойки. long m = (g_buf_len > 0) ? (long)g_buf_len : (long)g_n * g_oversample; float f_pio = (float)g_freq * (float)g_cpc * (float)m; float f_max = (float)CLK_SYS_HZ / ((float)g_cpc * (float)m); float lv = (float)((1u << g_bits) - 1) * (float)m + 1.0f; float ebits = logf(lv) / logf(2.0f); Serial.print(F("# BITS=")); Serial.print(g_bits); Serial.print(F(" OVERSAMPLE=")); Serial.print(g_oversample); Serial.print(F(" (эфф.разрядность ")); Serial.print(ebits, 1); Serial.print(F(" бит) N=")); Serial.print(g_n); Serial.print(F(" BUFFER=")); Serial.print(m); Serial.print(F(" (степень 2)")); Serial.print(F(" AMP=")); Serial.print(g_amp); Serial.print(F(" f_out=")); Serial.print(g_freq); Serial.print(F(" (факт ")); Serial.print(g_freq_actual, 0); Serial.print(F(") CPC=")); Serial.print(g_cpc); Serial.print(F(" f_max=")); Serial.print(f_max, 0); Serial.print(F(" Гц режим=")); Serial.print(g_sine_mode ? "S" : "Q"); if (g_sine_mode && f_pio > (float)CLK_SYS_HZ) { Serial.print(F(" ! ЗАПРОС ВЫШЕ ПОТОЛКА")); } Serial.println(); } // ─────────────── разбор команд ─────────────── static void handleLine(char *line) { char *cmd = strtok(line, " \t"); if (!cmd) return; if (!strcasecmp(cmd, "HELP") || !strcmp(cmd, "?")) { printHelp(); } else if (!strcasecmp(cmd, "I")) { printInfo(); } else if (!strcasecmp(cmd, "F")) { char *a = strtok(nullptr, " \t"); if (a) setFrequency((uint32_t)strtoul(a, nullptr, 10)); Serial.print("OK F "); Serial.println(g_freq); } else if (!strcasecmp(cmd, "SF")) { char *a = strtok(nullptr, " \t"); if (a) setFrequency((uint32_t)strtoul(a, nullptr, 10)); Serial.print("OK SF "); Serial.println(g_freq); } else if (!strcasecmp(cmd, "W")) { char *a = strtok(nullptr, " \t"); if (a) applyMode(!strcasecmp(a, "S")); Serial.print("OK W "); Serial.println(g_sine_mode ? "S" : "Q"); } else if (!strcasecmp(cmd, "D")) { char *a = strtok(nullptr, " \t"); if (a) { g_duty = (uint8_t)constrain(atoi(a), 0, 255); if (!g_sine_mode) squareOn(g_freq, g_duty); } Serial.print("OK D "); Serial.println(g_duty); } else if (!strcasecmp(cmd, "M")) { char *a = strtok(nullptr, " \t"); if (a) { g_amp = (uint8_t)constrain(atoi(a), 0, AMP_MAX); buildBuffer(); if (g_sine_mode) startPio(); } Serial.print("OK M "); Serial.println(g_amp); } else if (!strcasecmp(cmd, "A")) { Serial.print("A "); Serial.println(measureAmplitudeMv()); } else if (!strcasecmp(cmd, "SWEEP")) { char *a0 = strtok(nullptr, " \t"); char *a1 = strtok(nullptr, " \t"); char *a2 = strtok(nullptr, " \t"); char *a3 = strtok(nullptr, " \t"); if (a0 && a1 && a2 && a3) { runSweep(atof(a0), atof(a1), atoi(a2), strtoul(a3, nullptr, 10)); } } else if (!strcasecmp(cmd, "STOP")) { g_sweep = false; Serial.println(F("OK STOP")); } else if (!strcasecmp(cmd, "B")) { char *a = strtok(nullptr, " \t"); if (a) { g_bits = constrain(atoi(a), 1, 8); if (buildBuffer() && g_sine_mode) startPio(); } Serial.print("OK B "); Serial.println(g_bits); } else if (!strcasecmp(cmd, "O")) { char *a = strtok(nullptr, " \t"); if (a) { g_oversample = constrain(atoi(a), 1, 64); if (buildBuffer() && g_sine_mode) startPio(); } Serial.print("OK O "); Serial.println(g_oversample); } else if (!strcasecmp(cmd, "N")) { char *a = strtok(nullptr, " \t"); if (a) { g_n = constrain(atoi(a), 16, 4096); if (buildBuffer() && g_sine_mode) startPio(); } Serial.print("OK N "); Serial.println(g_n); } else if (!strcasecmp(cmd, "CLC")) { char *a = strtok(nullptr, " \t"); if (a) { g_cpc = constrain(atoi(a), 1, 8); if (g_sine_mode) startPio(); } Serial.print("OK CLC "); Serial.println(g_cpc); } else if (!strcasecmp(cmd, "CAL")) { char *a = strtok(nullptr, " \t"); if (a) { float meas = atof(a); if (meas > 0.0f) { float t = (float)g_freq * (float)g_cpc / meas; g_cpc = constrain((int)lroundf(t), 1, 8); if (g_sine_mode) startPio(); } } Serial.print("OK CAL CPC="); Serial.println(g_cpc); } else { Serial.print(F("ERR unknown: ")); Serial.println(cmd); } } // ─────────────── setup / loop ─────────────── void setup() { Serial.begin(115200); pinMode(PIN_TRIG, OUTPUT); digitalWrite(PIN_TRIG, LOW); analogReadResolution(12); if (!buildBuffer()) { Serial.println(F("! ОШИБКА: не удалось построить буфер")); } squareOn(g_freq, g_duty); Serial.println(F("# DDS на PIO (Arduino/earlephilhower). Подробный хелп: HELP или ?")); printHelp(); printInfo(); } void loop() { static char buf[96]; static size_t len = 0; while (Serial.available()) { char c = (char)Serial.read(); if (c == '\n' || c == '\r') { buf[len] = '\0'; if (len) handleLine(buf); len = 0; } else if (len < sizeof(buf) - 1) { buf[len++] = c; } } }