Files
eumirts5-dds/stub/04-dds-pio-rp2040.ino

619 lines
29 KiB
Arduino

/*
* 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 <Arduino.h>
#include <math.h>
// Проверка ядра: 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;
}
}
}