619 lines
29 KiB
Arduino
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|