Files
eumirts5-dds/stub/05-dds-audiopwm-rp2040.py

855 lines
38 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DDS-генератор на аудио-ШИМ (RP2040) — CircuitPython
#
# Вариант 5 генератора для лабораторных работ. Совместим по системе команд
# с вариантами 1–4 (F, SF, W, D, M, A, SWEEP, STOP, B, O, N, FN, FP, I,
# CAL, HELP), но выход сделан не через PIO и не через программный ШИМ,
# а через штатный звуковой модуль CircuitPython audiopwmio.
#
# ЧЕРНОВИК (AI-сгенерировано, 2026-10-28). Требует проверки преподавателем
# на реальном стенде: частоты и разрядность посчитаны по исходникам ядра,
# но реальный размах и уровень несущей надо проверить осциллографом.
#
# Дополнительные библиотеки НЕ нужны: audiopwmio и audiocore входят в
# прошивку CircuitPython для RP2040.
#
# ──────────────── ПОЧЕМУ НЕ analogio (важно) ────────────────
#
# В CircuitPython модуль analogio содержит ровно два класса:
#
# analogio.AnalogIn — вход АЦП (им и меряем, см. команду A);
# analogio.AnalogOut — выход ЦАП.
#
# AnalogOut работает только там, где есть НАСТОЯЩИЙ ЦАП. У RP2040 его нет,
# поэтому в документации прямо написано: «Not available on Nordic, RP2040,
# Spresense, as there is no on-chip DAC». То есть аналогового выхода на
# Pico через analogio не получить — ни в каком виде.
#
# Вывод «звука через ШИМ» в CircuitPython делает другой модуль — audiopwmio:
#
# audiopwmio.PWMAudioOut(pin) — канал вывода (ШИМ-слайс);
# audiocore.RawSample(буфер, ...) — проигрываемый сэмпл.
#
# Именно его и использует этот вариант.
#
# ──────────────── КАК ЭТО РАБОТАЕТ ────────────────
#
# На RP2040 внутри audiopwmio:
#
# * занимается один ШИМ-слайс (GP15 — это слайс 7, канал B);
# * TOP регистра скважности = 1023, поэтому несущая ШИМ:
#
# f_несущей = 125 МГц / 1024 = 122,07 кГц (фиксирована ядром)
#
# * скважность (10 бит из 16-битного сэмпла) обновляется НЕ программой,
# а DMA по таймеру темпа. Таймер настраивается дробным делителем
# 125 МГц * X / Y, поэтому частота сэмплирования задаётся очень точно:
#
# sample_rate = f_вых * m, где m = N * O — сэмплов на период
#
# * после RC-фильтра низких частот остаётся среднее значение скважности,
# то есть наш сигнал. Несущие 122 кГц фильтр давит, сигнал пропускает.
#
# Отличие от варианта 1 и 2 (программный ШИМ) — в том, что цикл Python в
# формировании сигнала НЕ участвует вообще. Отличие от вариантов 3 и 4
# (PIO) — в том, что вместо PIO-машины работает штатный аудио-тракт ядра.
#
# ──────────────── ЧТО ЭТО ДАЁТ И ЧЕГО НЕ ДАЁТ ────────────────
#
# Плюсы:
# * вывод идёт в фоне (DMA), CPU свободен — команды принимаются на ходу,
# АЦП можно читать ОДНОВРЕМЕННО с выводом сигнала;
# * несущая 122 кГц фильтруется тем же RC, что и в остальных вариантах
# (1к/47н, f_c ~ 3,4 кГц);
# * не нужны adafruit_pioasm и rp2pio — только прошивка.
#
# Минусы (важно для лаборатории):
# * это ЗВУКОВОЙ тракт: частота сэмплирования ограничена сверху ~48 кГц,
# потому что она должна быть заметно ниже несущей 122 кГц. Значит
# верхняя частота сигнала:
#
# f_вых_max = sample_rate_max / m
#
# при m = 16 это 3 кГц, при m = 48 — 1 кГц, при m = 256 — 188 Гц.
# Выше 3–4 кГц этот вариант не годится, там нужны PIO (варианты 3–4)
# или программный ШИМ (варианты 1–2).
# * разрядность скважности 10 бит — команда B больше 10 не имеет смысла
# (ядро всё равно оставит 10 старших бит).
# * при малом m форма ступенчатая: 16 точек на период — это грубо.
#
# ──────────────── ПИНЫ ────────────────
#
# GP15 — выход (ШИМ в режиме Q, аудио-ШИМ в режиме S);
# GP16 — синхроимпульс (вне зоны, которую занимает аудио);
# GP26 — вход ADC0 (A0), до 3,3 В, больше — только через делитель.
#
# ──────────────── КОМАНДЫ ────────────────
#
# F <Гц> — частота (1..200000, но см. предел выше)
# SF <Гц> — то же, явно для синуса
# W Q | W S — режим: Q — прямоугольник (pwmio), S — звук (audiopwmio)
# D <0..255> — скважность прямоугольника в режиме Q
# M <0..127> — амплитуда (127 = полная шкала)
# A — измерить размах на ADC0 -> "A <мВ>"
# SWEEP f0 f1 n ms — свип: "P <Гц> <мВ>" на точку, затем "DONE"
# STOP — прервать свип
# B <1..10> — разрядность скважности (10 — предел audiopwmio)
# O <1..64> — множитель сэмплов: m = N*O
# N <16..4096> — «полезных» сэмплов на период
# FN <имя> — форма: sin, sine2, triangle, saw, square
# FP <выражение> — своя функция от t (0..1)
# CAL <Гц> — автокалибровка по осциллографу (поправка частоты)
# CLC <n> — НЕ ПРИМЕНИМА (нет PIO); принимается для совместимости
# I — настройки, пределы частоты, фактическая частота
#
# ВНИМАНИЕ: ADC0 (GP26) — вход 0..3,3 В. Сигнал больше 3,3 В подавать
# только через делитель. Схему без питания к ADC не подключать.
import array
import math
import sys
import time
import board
import digitalio
import pwmio
import audiocore
import audiopwmio
try:
import analogbufio
except ImportError: # pragma: no cover
analogbufio = None
print("# analogbufio недоступен: команды A и SWEEP работают по-простому")
# ─────────────────────────── константы ───────────────────────────
PIN_OUT = board.GP15 # выход: PWM в режиме Q, аудио-ШИМ в режиме S
PIN_TRIG = board.GP16 # синхроимпульс
PIN_ADC = board.GP26 # ADC0 — вход измерения
SYS_CLK = 125_000_000 # тактовая RP2040, Гц
# Внутри audiopwmio на RP2040: BITS_PER_SAMPLE = 10, TOP = 2^10 - 1 = 1023.
# Отсюда фиксированная несущая ШИМ и предельная разрядность 10 бит.
PWM_BITS = 10
PWM_CARRIER_HZ = SYS_CLK / (1 << PWM_BITS) # 122 070,3 Гц
# Темп сэмплирования задаёт DMA-таймер с делителем до 65536, значит снизу
# частота ограничена ~1907 Гц (та же цифра, что предел clkdiv у PIO).
SR_MIN = SYS_CLK / 65536 # ≈ 1907 Гц
# Сверху ограничиваем «по-звуковому»: сэмплы должны быть заметно реже
# несущей, иначе ШИМ не успевает их отыграть и на выходе будет мусор.
SR_MAX = 48000
ADC_FULL_MV = 3300 # опорное напряжение ADC, мВ
# Буфер сэмплов: 2 байта на сэмпл ("H"), держим не больше 64 КБ.
MAX_BUFFER_BYTES = 64 * 1024
# Пределы для команд N и O (те же, что в вариантах 3–4, для совместимости).
N_MIN, N_MAX = 16, 4096
O_MIN, O_MAX = 1, 64
def _default_wave(t):
"""Форма по умолчанию: синус. t — доля периода [0,1)."""
return math.sin(2.0 * math.pi * t)
# ─────────────────────────── класс AudioPWMDDS ───────────────────────────
class AudioPWMDDS:
"""DDS на audiopwmio: буфер сэмплов играет DMA, CPU свободен."""
def __init__(self, bits=8, oversample=1, n=32, freq=1000, level=127):
self.bits = int(bits)
self.oversample = int(oversample)
self.n = int(n)
self.freq = int(freq)
self.level = int(level)
self.wave = _default_wave
self.wave_name = "sin"
self.corr = 1.0 # поправка частоты после CAL
self.audio = None # создаётся лениво, в ensure()
self._sample = None # RawSample — держим ссылку!
self._buf = None # array('H') — буфер должен жить
# ─── параметры ───
def samples_per_period(self):
"""m — сколько сэмплов приходится на один период сигнала."""
return self.n * self.oversample
def sample_rate_wanted(self):
"""Желаемая частота сэмплирования, Гц (до ограничения)."""
return self.freq * self.samples_per_period() * self.corr
def sample_rate_used(self):
"""Частота сэмплирования, которая реально будет выставлена."""
sr = int(round(self.sample_rate_wanted()))
if sr < int(SR_MIN) + 1:
sr = int(SR_MIN) + 1
if sr > SR_MAX:
sr = SR_MAX
return sr
def f_out_actual(self):
"""Фактическая частота сигнала при текущем sample_rate."""
return self.sample_rate_used() / float(self.samples_per_period())
def f_out_max(self):
return SR_MAX / float(self.samples_per_period())
def f_out_min(self):
return (int(SR_MIN) + 1) / float(self.samples_per_period())
def needs_warning(self):
"""True, если запрошенная частота не влезает в звуковой тракт."""
sr = self.sample_rate_wanted()
return sr > SR_MAX or sr < SR_MIN
def suggest_reduction(self):
"""Подсказка, что менять, если частота не достижима."""
m = self.samples_per_period()
if self.sample_rate_wanted() > SR_MAX:
ok = int(SR_MAX / float(self.freq * self.corr)) if self.freq else 0
return ("уменьшите N*O до %d (сейчас m=%d) или снизьте частоту "
"до %.1f Гц" % (max(N_MIN, ok), m, self.f_out_max()))
return ("увеличьте N*O (сейчас m=%d): минимум %.1f Гц"
% (m, self.f_out_min()))
def set_frequency(self, hz):
self.freq = max(1, min(200000, int(hz)))
if self.audio is not None:
self._reload()
def set_bits(self, b):
b = int(b)
if b < 1:
raise ValueError("B должно быть 1..%d" % PWM_BITS)
if b > PWM_BITS:
raise ValueError("для audiopwmio максимум B=%d (ядро оставляет "
"10 старших бит)" % PWM_BITS)
self.bits = b
if self.audio is not None:
self._reload()
def set_oversample(self, o):
o = int(o)
if o < O_MIN or o > O_MAX:
raise ValueError("O должно быть %d..%d" % (O_MIN, O_MAX))
self._check_buffer(self.n * o) # проверить ДО присваивания
self.oversample = o
if self.audio is not None:
self._reload()
def set_n(self, n):
n = int(n)
if n < N_MIN or n > N_MAX:
raise ValueError("N должно быть %d..%d" % (N_MIN, N_MAX))
self._check_buffer(n * self.oversample)
self.n = n
if self.audio is not None:
self._reload()
def set_amplitude(self, level):
self.level = max(0, min(127, int(level)))
if self.audio is not None:
self._reload()
def set_function(self, fn, name="func"):
self.wave = fn
self.wave_name = name
if self.audio is not None:
self._reload()
def reset_function(self):
self.set_function(_default_wave, "sin")
def slew_warning(self):
"""В этом варианте нет дельта-сигмы, поэтому ограничения наклона нет.
Метод оставлен для совместимости с вызовами из main()."""
return None
# ─── буфер и воспроизведение ───
def _check_buffer(self, m=None):
"""Проверить, что буфер для m сэмплов влезает в память."""
if m is None:
m = self.samples_per_period()
if m * 2 > MAX_BUFFER_BYTES:
raise ValueError("буфер %d байт > %d — уменьшите N или O"
% (m * 2, MAX_BUFFER_BYTES))
def _build(self):
"""Заполнить буфер: m сэмплов одного периода, 16 бит, 0..65535.
Квантование по B бит делается ЗДЕСЬ, а не в железе: уровень
округляется до 2^B значений скважности и растягивается на всю
16-битную шкалу. Так команда B честно показывает, сколько
ступенек увидит осциллограф.
"""
m = self.samples_per_period()
self._check_buffer(m)
levels = (1 << self.bits) - 1 # 1, 3, 7, ... 1023
amp = self.level / 127.0
buf = array.array("H", [0]) * m
for i in range(m):
u = self.wave(i / float(m)) * amp
if u > 1.0:
u = 1.0
elif u < -1.0:
u = -1.0
# 0.5 + 0.5*u -> 0..1; округляем до B-битной ступеньки
level = int(round((0.5 + 0.5 * u) * levels))
buf[i] = level * 65535 // levels
self._buf = buf
return m
def _reload(self):
"""Пересобрать буфер и перезапустить воспроизведение."""
self._build()
sr = self.sample_rate_used()
self._sample = audiocore.RawSample(self._buf,
channel_count=1,
sample_rate=sr)
if self.audio is not None:
self.audio.play(self._sample, loop=True)
def ensure(self):
"""Создать канал вывода и запустить сигнал (лениво)."""
if self.audio is None:
self.audio = audiopwmio.PWMAudioOut(PIN_OUT,
quiescent_value=0x8000)
self._reload()
return self
def release(self):
"""Остановить и освободить ШИМ-слайс (нужно для режима Q)."""
if self.audio is not None:
try:
self.audio.stop()
except Exception:
pass
try:
self.audio.deinit()
except Exception:
pass
self.audio = None
self._sample = None
self._buf = None
def deinit(self):
self.release()
def calibrate(self, measured_hz):
"""CAL <измеренная_Гц>: подстроить частоту сэмплирования.
Ошибка тут может быть только от кварца (доли промилле), но команда
оставлена для единообразия с вариантами 3–4: вводим измеренное,
получаем поправку corr и пересчёт.
"""
if self.needs_warning():
raise ValueError("сначала задайте достижимую частоту: сейчас "
"запрошено %d Гц при пределе %.1f Гц"
% (self.freq, self.f_out_max()))
measured = float(measured_hz)
if measured <= 0:
raise ValueError("измеренная частота должна быть > 0")
old = self.corr
self.corr = self.corr * self.freq / measured
self.corr = max(0.5, min(2.0, self.corr))
if self.audio is not None:
self._reload()
return old, self.corr
def info(self):
m = self.samples_per_period()
return ("аудио-ШИМ: f=%d Гц, форма=%s, B=%d, N=%d, O=%d, m=%d, "
"Fs=%d Гц, несущая=%.1f кГц, f_ном=%.2f Гц, "
"поправка=%.6f, f_min=%.1f, f_max=%.1f Гц"
% (self.freq, self.wave_name, self.bits, self.n,
self.oversample, m, self.sample_rate_used(),
PWM_CARRIER_HZ / 1000.0, self.f_out_actual(), self.corr,
self.f_out_min(), self.f_out_max()))
# ───────────────────── прямоугольник на PWM ─────────────────────
#
# Режим Q — обычный аппаратный ШИМ на том же GP15. Одновременно с
# аудио-ШИМ он работать не может: audiopwmio занимает весь слайс.
# Поэтому режимы включаются по очереди (ensure/release), как в варианте 3.
class SquarePWM:
def __init__(self, pin=PIN_OUT):
self.pwm = pwmio.PWMOut(pin, frequency=1000,
duty_cycle=0, variable_frequency=True)
def set_frequency(self, f):
self.pwm.frequency = max(1, min(200000, int(f)))
def set_duty(self, d):
d = max(0, min(255, int(d)))
self.pwm.duty_cycle = d * 65535 // 255
def off(self):
self.pwm.duty_cycle = 0
def deinit(self):
try:
self.pwm.deinit()
except Exception:
pass
# ─────────────────────────── АЦП ───────────────────────────
def measure_amplitude_mv(window_ms=50, rate=100000):
"""Размах (пик-пик) на ADC0 в милливольтах.
Чтение через analogbufio (аппаратный ADC + DMA). Если библиотеки нет
или DMA занят звуком — откатываемся на analogio.AnalogIn.
ВАЖНО: воспроизведение при этом не останавливается — audiopwmio играет
на своём DMA-канале, ADC работает независимо.
"""
if analogbufio is not None:
length = int(rate * window_ms / 1000)
length = max(64, min(8192, length))
buf = array.array("H", [0]) * length
adc = None
try:
adc = analogbufio.BufferedIn(PIN_ADC, sample_rate=rate)
adc.readinto(buf, loop=False)
return (max(buf) - min(buf)) * ADC_FULL_MV // 65535
except Exception:
pass # нет DMA-канала или частоты — ниже
finally:
if adc is not None:
try:
adc.deinit()
except Exception:
pass
import analogio
adc = analogio.AnalogIn(PIN_ADC)
vmin, vmax = 65535, 0
t0 = time.monotonic()
try:
while (time.monotonic() - t0) * 1000 < window_ms:
v = adc.value
if v < vmin:
vmin = v
if v > vmax:
vmax = v
finally:
adc.deinit()
return (vmax - vmin) * ADC_FULL_MV // 65535
# ─────────────────────────── свип ───────────────────────────
trig = None
def ensure_trig():
"""Создать синхроимпульс при первом использовании (лениво!)."""
global trig
if trig is None:
trig = digitalio.DigitalInOut(PIN_TRIG)
trig.direction = digitalio.Direction.OUTPUT
trig.value = False
return trig
def trigger_pulse():
t = ensure_trig()
t.value = True
time.sleep(0.00005)
t.value = False
def run_sweep(dds, square, mode, f0, f1, count, dwell_ms):
"""Логарифмический свип. Печатает 'P <Гц> <мВ>', в конце 'DONE'."""
count = max(2, count)
if f1 <= f0:
f1 = f0 * 10.0
ratio = (f1 / f0) ** (1.0 / (count - 1))
f = float(f0)
try:
for _ in range(count):
if mode == "S":
dds.set_frequency(int(f))
else:
square.set_frequency(int(f))
trigger_pulse()
time.sleep(dwell_ms / 1000.0)
mv = measure_amplitude_mv()
sys.stdout.write("P %d %d\n" % (int(f), mv))
f *= ratio
except KeyboardInterrupt:
sys.stdout.write("DONE\n")
raise
sys.stdout.write("DONE\n")
# ───────── предустановленные формы сигнала (для команды FN) ─────────
def _wave_sin(t):
return math.sin(2.0 * math.pi * t)
def _wave_sine2(t):
return (0.3 * math.sin(2.0 * math.pi * t) +
0.5 * math.sin(4.0 * math.pi * t))
def _wave_triangle(t):
return 4.0 * abs(t - 0.5) - 1.0
def _wave_saw(t):
return 2.0 * t - 1.0
def _wave_square(t):
return 1.0 if t < 0.5 else -1.0
WAVES = {
"sin": _wave_sin,
"sine2": _wave_sine2,
"triangle": _wave_triangle,
"saw": _wave_saw,
"square": _wave_square,
}
# Разрешённые имена для команд FP/FN: только математика, без встроенных.
_EXPR_NAMES = {
"sin": math.sin, "cos": math.cos, "tan": math.tan,
"pi": math.pi, "abs": abs,
}
EXPR_LEN_MAX = 120
def _eval_expr(expr, t):
"""Вычислить выражение от t. Отдельная функция, не lambda: на плате
разбор lambda в командах не срабатывал (см. вариант 3)."""
env = dict(_EXPR_NAMES)
env["t"] = t
return eval(expr, {"__builtins__": {}}, env)
def _expr_ok(expr):
"""Проверить выражение до использования. None, если всё в порядке."""
if len(expr) > EXPR_LEN_MAX:
return "выражение слишком длинное (лимит %d)" % EXPR_LEN_MAX
for t in (0.0, 0.125, 0.333, 0.5, 0.75):
try:
v = _eval_expr(expr, t)
except Exception as e:
return "%s (при t=%.3f)" % (e, t)
if v != v: # NaN
return "не числовой результат (NaN)"
if abs(v) > 1e6:
return "слишком большое значение (%g) — форма вне [-1,1]" % v
return None
def _make_expr_wave(expr):
def wave(t):
return _eval_expr(expr, t)
return wave
# ─────────────────────────── подробный хелп ───────────────────────────
def print_help():
lines = [
"# ─────── DDS-генератор на аудио-ШИМ (audiopwmio): команды ───────",
"# Обозначения: <..> — обязательный аргумент.",
"#",
"# РЕЖИМ И ЧАСТОТА",
"# W Q — режим прямоугольника (аппаратный ШИМ на GP15)",
"# W S — режим звука (audiopwmio, GP15)",
"# F <Гц> — частота, в режиме Q — прямоугольника,",
"# в S — сигнала. пример: F 440",
"# SF <Гц> — частота СИГНАЛА (то же, явно)",
"#",
"# ФОРМА СИГНАЛА",
"# D <0..255> — скважность прямоугольника (128 = 50 %)",
"# M <0..127> — амплитуда (127 = полная шкала)",
"# FN <имя> — форма: sin, sine2, triangle, saw, square",
"# FP <выражение> — своя функция от t (0..1), например:",
"# FP 0.3*sin(2*pi*t)+0.5*sin(4*pi*t)",
"#",
"# ИЗМЕРЕНИЕ И СВИП (работает ОДНОВРЕМЕННО с выводом)",
"# A — размах на ADC0 (GP26), мВ -> 'A <мВ>'",
"# SWEEP <f0> <f1> <n> <dwell_ms>",
"# логарифмический свип; печатает 'P <Гц> <мВ>',",
"# в конце 'DONE'. пример: SWEEP 20 3000 20 150",
"# STOP — прервать свип",
"#",
"# ПАРАМЕТРЫ СИНТЕЗА",
"# B <1..10> — разрядность скважности. 10 — предел ядра",
"# (внутри audiopwmio берутся 10 старших бит)",
"# O <1..64> — множитель сэмплов, m = N * O",
"# N <16..4096> — «полезных» сэмплов на период",
"# I — настройки, пределы, f_min/f_max, поправка",
"# CAL <Гц> — автокалибровка: подайте частоту, измерьте",
"# осциллографом, введите измеренное (CAL 440)",
"# CLC <n> — НЕ ПРИМЕНИМА: PIO здесь нет; принимается",
"# только чтобы не ломать общий сценарий команд",
"#",
"# ГЛАВНОЕ ОГРАНИЧЕНИЕ (в чём отличие от вариантов 3–4)",
"# Частота сэмплирования не больше 48000 Гц, поэтому",
"# f_max = 48000 / m.",
"# При m=16 это 3 кГц, при m=48 — 1 кГц, при m=256 — 188 Гц.",
"# Выше 3–4 кГц этот вариант не работает — берите PIO.",
"# Команда I показывает f_min и f_max для текущих N и O.",
"#",
"# КАРТА ПИНОВ",
"# GP15 — выход (ШИМ в Q, аудио-ШИМ в S)",
"# GP16 — синхроимпульс",
"# GP26 (A0) — вход ADC, до 3,3 В (через делитель)",
"#",
"# ЧЕМ ЭТО ОТЛИЧАЕТСЯ ОТ analogio",
"# analogio умеет только AnalogIn (вход) и AnalogOut (ЦАП).",
"# На RP2040 ЦАПА НЕТ, поэтому AnalogOut недоступен, и «звук через",
"# ШИМ» делается модулем audiopwmio — он и используется здесь.",
"#",
"# ФИЛЬТР НА ВЫХОДЕ (тот же, что в остальных вариантах)",
"# GP15 --[R1]--+--[R2]--+--> во вход схемы",
"# | |",
"# [C1] [C2] R1=R2=1к, C1=C2=47н",
"# | | (f_c ~ 3.4 кГц)",
"# GND GND",
"# Несущая ШИМ 122 кГц давится этим фильтром примерно на 60 дБ.",
"#",
"# СПРАВКА",
"# HELP | ? — этот текст",
]
for ln in lines:
sys.stdout.write(ln + "\n")
# ─────────────────────────── main ───────────────────────────
def main():
print("# DDS на аудио-ШИМ (RP2040 / CircuitPython).")
print("# Вывод: audiopwmio, вход: analogio/analogbufio.")
print("# Подробный хелп: HELP или ?")
dds = None
square = None
try:
dds = AudioPWMDDS(bits=8, oversample=1, n=32, freq=1000, level=127)
square = SquarePWM()
square.set_frequency(1000)
square.set_duty(128)
print("# " + dds.info())
print(" (режим Q: PWM на GP15; режим S: audiopwmio на GP15)")
except Exception as e:
print("! ОШИБКА инициализации:", repr(e))
if dds is not None:
dds.deinit()
if square is not None:
square.deinit()
return
mode = "Q"
saved_square_hz = 1000
try:
while True:
line = sys.stdin.readline()
if not line:
break # EOF: порт закрыт
parts = line.split()
if not parts:
continue
cmd = parts[0].upper()
if cmd in ("HELP", "?"):
print_help()
elif cmd == "F" and len(parts) > 1:
hz = int(float(parts[1]))
if mode == "S":
dds.set_frequency(hz)
print("OK F %d (сигнал)" % dds.freq)
if dds.needs_warning():
print("! ВНИМАНИЕ: частота недостижима — %s"
% dds.suggest_reduction())
print("! Сейчас реально: %.1f Гц" % dds.f_out_actual())
else:
square.set_frequency(hz)
print("OK F %d (прямоугольник)" % hz)
elif cmd == "SF" and len(parts) > 1:
dds.set_frequency(int(float(parts[1])))
print("OK SF %d" % dds.freq)
if dds.needs_warning():
print("! ВНИМАНИЕ: частота недостижима — %s"
% dds.suggest_reduction())
print("! Сейчас реально: %.1f Гц" % dds.f_out_actual())
elif cmd == "W" and len(parts) > 1:
want = "S" if parts[1].upper().startswith("S") else "Q"
if want == mode:
print("OK W %s (уже)" % mode)
else:
try:
if want == "S":
# запомнить частоту прямоугольника, погасить его,
# освободить слайс и запустить аудио-ШИМ
saved_square_hz = square.pwm.frequency
square.off()
square.deinit()
square = None
dds.ensure()
mode = "S"
print("OK W S (audiopwmio: %s)" % dds.info())
else:
# остановить аудио, вернуть ШИМ на тот же GP15
dds.release()
square = SquarePWM()
square.set_frequency(saved_square_hz)
square.set_duty(128)
mode = "Q"
print("OK W Q (PWM на GP15, %d Гц)"
% saved_square_hz)
except Exception as e:
print("ERR W %s:" % want, repr(e))
elif cmd == "D" and len(parts) > 1:
if mode == "Q" and square is not None:
square.set_duty(int(parts[1]))
print("OK D %s" % parts[1])
elif cmd == "M" and len(parts) > 1:
dds.set_amplitude(int(parts[1]))
print("OK M %d" % dds.level)
elif cmd == "A":
print("A %d" % measure_amplitude_mv())
elif cmd == "SWEEP" and len(parts) > 4:
run_sweep(dds, square, mode,
float(parts[1]), float(parts[2]),
int(parts[3]), int(parts[4]))
elif cmd == "STOP":
print("OK STOP (если свип идёт — надёжнее Ctrl-C)")
elif cmd == "B" and len(parts) > 1:
try:
dds.set_bits(int(parts[1]))
print("OK B %d" % dds.bits)
except Exception as e:
print("ERR B:", repr(e))
elif cmd == "O" and len(parts) > 1:
try:
dds.set_oversample(int(parts[1]))
print("OK O %d" % dds.oversample)
except Exception as e:
print("ERR O:", repr(e))
elif cmd == "N" and len(parts) > 1:
try:
dds.set_n(int(parts[1]))
print("OK N %d" % dds.n)
except Exception as e:
print("ERR N:", repr(e))
elif cmd == "FN" and len(parts) > 1:
name = parts[1].lower()
if name in WAVES:
try:
dds.set_function(WAVES[name], name=name)
print("OK FN %s" % name)
except Exception as e:
print("ERR FN:", repr(e))
else:
print("ERR FN: неизвестная форма. Доступны: %s"
% ", ".join(sorted(WAVES)))
elif cmd == "FP" and len(parts) > 1:
expr = line.strip()[len("FP"):].strip()
if expr:
err = _expr_ok(expr)
if err:
print("ERR FP: %s" % err)
else:
try:
dds.set_function(_make_expr_wave(expr),
name="FP:" + expr)
print("OK FP %s" % expr)
except Exception as e:
print("ERR FP:", repr(e))
dds.reset_function()
else:
print("ERR FP: укажите выражение, например "
"FP 0.3*sin(2*pi*t)+0.5*sin(4*pi*t)")
elif cmd == "CLC" and len(parts) > 1:
# Команда есть в вариантах 3–4 (тактов PIO на сэмпл).
# Здесь PIO нет: частоту задаёт DMA-таймер, подстраивать
# нечего. Принимаем, чтобы общий сценарий не падал.
print("OK CLC %s (не применима: здесь нет PIO; для поправки "
"частоты — CAL)" % parts[1])
elif cmd == "CAL" and len(parts) > 1:
try:
was, now = dds.calibrate(float(parts[1]))
print("OK CAL: поправка %.6f -> %.6f (Fs=%d Гц, "
"физическая частота должна стать %d Гц)"
% (was, now, dds.sample_rate_used(), dds.freq))
except Exception as e:
print("ERR CAL:", repr(e))
elif cmd == "I":
print("# " + dds.info())
else:
print("ERR unknown command: %s" % cmd)
except KeyboardInterrupt:
pass
finally:
dds.deinit()
if square is not None:
square.deinit()
if trig is not None:
try:
trig.deinit()
except Exception:
pass
print("stopped")
# ─────────────── точка входа ───────────────
#
# Как и в других вариантах, запускаем безусловно: в CircuitPython проверка
# `if __name__ == "__main__":` при автозапуске code.py ненадёжна.
_START_GENERATOR = True
if _START_GENERATOR:
try:
main()
except KeyboardInterrupt:
pass
except Exception as _e:
try:
import sys as _sys
_sys.print_exception(_e)
except Exception:
print("! ОШИБКА:", repr(_e))