diff --git a/print/methodical-lab5-peredatochnaya-funkciya-na-ou-student.docx b/print/methodical-lab5-peredatochnaya-funkciya-na-ou-student.docx index f5ff278..e9d5c72 100644 Binary files a/print/methodical-lab5-peredatochnaya-funkciya-na-ou-student.docx and b/print/methodical-lab5-peredatochnaya-funkciya-na-ou-student.docx differ diff --git a/print/methodical-lab5-peredatochnaya-funkciya-na-ou-teacher.docx b/print/methodical-lab5-peredatochnaya-funkciya-na-ou-teacher.docx new file mode 100644 index 0000000..1272e95 Binary files /dev/null and b/print/methodical-lab5-peredatochnaya-funkciya-na-ou-teacher.docx differ diff --git a/stub/05-dds-audiopwm-rp2040.py b/stub/05-dds-audiopwm-rp2040.py new file mode 100644 index 0000000..9d5aed5 --- /dev/null +++ b/stub/05-dds-audiopwm-rp2040.py @@ -0,0 +1,854 @@ +# 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 — НЕ ПРИМЕНИМА (нет 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 ", + "# логарифмический свип; печатает '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 — НЕ ПРИМЕНИМА: 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)) diff --git a/task.md b/task.md index 7c64766..668024a 100644 --- a/task.md +++ b/task.md @@ -197,8 +197,14 @@ K(f) = 1 / √(1 + (f/f_c)²), f_c = 1/(2π·R·C) | `01-...-circuitpython.py` | CircuitPython | программный DDS + ШИМ | | `04-dds-pio-rp2040.py` | CircuitPython | аппаратный PIO + DMA | | `04-dds-pio-rp2040.ino` | Arduino, **только earlephilhower** | аппаратный PIO + DMA | +| `05-dds-audiopwm-rp2040.py` | CircuitPython | аудио-ШИМ (`audiopwmio`) + DMA; только до ~3 кГц | **Рекомендуется вариант на PIO** — точная частота, CPU свободен. +Вариант 3 (`04-dds-pio-rp2040.py`) проверен на стенде: логическим +анализатором снят цифровой выход, форма синуса верная. +Вариант 5 — если интересно, как звук выводят штатными средствами ядра +(`audiopwmio`); он ограничен сверху ~3 кГц, потому что частота +сэмплирования не может быть выше 48 кГц. У `.ino`-варианта на PIO форма сигнала задаётся функцией `waveform(t)` **в коде** (перекомпилировать после правки) — в C++ нет разбора формул на лету, как в CircuitPython-версии.