From ebd2ac546aea187198190536241917e3b7b50109 Mon Sep 17 00:00:00 2001 From: mss Date: Mon, 2 Nov 2026 13:46:40 +0400 Subject: [PATCH] =?UTF-8?q?ai:=20=D0=B2=D0=B0=D1=80=D0=B8=D0=B0=D0=BD?= =?UTF-8?q?=D1=82=206=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B0=20(PIO,=20=D0=BD=D0=B5=D0=B8=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D0=BC=D1=8B=D0=B5=20=D0=B1=D0=B8?= =?UTF-8?q?=D1=82=D1=8B=20=3D=200),=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=81=20=D0=A0?= =?UTF-8?q?=D0=9F=D0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stub/06-dds-pio-zero-rp2040.py: новый вариант; PIO всегда выводит 8 бит (out pins, 8), младшие неиспользуемые линии GP8..GP(15-BITS) — выходы в 0 - stub/04-dds-pio-rp2040.py: подтянута шапка с отметкой о проверке на стенде - task.md, print/: пересобраны из РПД (таблица вариантов + брошюра студента) - README.md: список вариантов приведён в соответствие --- README.md | 6 + ...peredatochnaya-funkciya-na-ou-student.docx | Bin 47746 -> 48040 bytes stub/04-dds-pio-rp2040.py | 9 +- stub/06-dds-pio-zero-rp2040.py | 1292 +++++++++++++++++ task.md | 9 +- 5 files changed, 1313 insertions(+), 3 deletions(-) create mode 100644 stub/06-dds-pio-zero-rp2040.py diff --git a/README.md b/README.md index 18daf92..e8fca74 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,9 @@ eumirts5-dds/ │ ├── 01-dds-generator-rp2040.ino ← вариант 1: Arduino C++ │ ├── 01-dds-generator-rp2040-circuitpython.py ← вариант 2: CircuitPython │ ├── 04-dds-pio-rp2040.py ← вариант 3: PIO + DMA +│ ├── 04-dds-pio-rp2040.ino ← вариант 4: PIO + DMA (Arduino) +│ ├── 05-dds-audiopwm-rp2040.py ← вариант 5: аудио-ШИМ +│ ├── 06-dds-pio-zero-rp2040.py ← вариант 6: PIO, лишние биты = 0 │ ├── 02-izmerenie-achh.py ← автоснятие АЧХ (на ПК) │ └── 03-blank-otchyota.md ← бланк отчёта └── print/ ← брошюра задания (DOCX) @@ -57,6 +60,9 @@ eumirts5-dds/ | `01-...ip2040.ino` | есть Arduino IDE | программный DDS, работает на обоих ядрах | | `01-...-circuitpython.py` | есть CircuitPython | то же, но медленнее (интерпретатор) | | `04-dds-pio-rp2040.py` | **рекомендуется** | аппаратный PIO + DMA, точная частота, CPU свободен | +| `04-dds-pio-rp2040.ino` | Arduino, earlephilhower | PIO + DMA; форма сигнала задаётся в коде | +| `05-dds-audiopwm-rp2040.py` | CircuitPython | `audiopwmio`: DMA играет буфер; только до ~3 кГц | +| `06-dds-pio-zero-rp2040.py` | CircuitPython | то же, что `04-...py`, но неиспользуемые младшие биты порта выведены в 0 | Для варианта на PIO нужна библиотека `adafruit_pioasm` в папке `/lib` на плате. diff --git a/print/methodical-lab5-peredatochnaya-funkciya-na-ou-student.docx b/print/methodical-lab5-peredatochnaya-funkciya-na-ou-student.docx index e9d5c72dc77307bb55a13540f2b4fa6879f0495f..060b8442eed8cf0e389b738c88ede653c610d17f 100644 GIT binary patch delta 12610 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 diff --git a/stub/04-dds-pio-rp2040.py b/stub/04-dds-pio-rp2040.py index 2d644de..6458961 100644 --- a/stub/04-dds-pio-rp2040.py +++ b/stub/04-dds-pio-rp2040.py @@ -5,8 +5,13 @@ # для режима «синус» формирует аппаратная PIO-машина RP2040, данные в неё # подаются через DMA, а АЦП читается библиотекой analogbufio. # -# ЧЕРНОВИК (AI-сгенерировано, 2026-10-27). Требует проверки преподавателем -# на реальном стенде. +# ПРОВЕРЕНО НА СТЕНДЕ 2026-10-28 (преподаватель): цифровой выход PIO +# снят логическим анализатором — форма синуса верная. Лестница R-2R при +# этой проверке НЕ запаивалась, поэтому аналоговая часть (размах после +# внешнего ФНЧ, реальная разрядность по постоянному уровню) ещё не +# измерялась. +# +# AI-сгенерировано, 2026-10-27; проверка цифровой части — 2026-10-28. # # Требует библиотеку: adafruit_pioasm -> скопировать в /lib на плате. # https://github.com/adafruit/Adafruit_CircuitPython_PIOASM diff --git a/stub/06-dds-pio-zero-rp2040.py b/stub/06-dds-pio-zero-rp2040.py new file mode 100644 index 0000000..4df1500 --- /dev/null +++ b/stub/06-dds-pio-zero-rp2040.py @@ -0,0 +1,1292 @@ +# DDS-генератор на PIO (RP2040) — CircuitPython +# +# Отдельный экземпляр варианта 3 (04-dds-pio-rp2040.py) с исправлением +# поведения неиспользуемых битов порта. +# +# ЧЕМ ОТЛИЧАЕТСЯ ОТ 04-dds-pio-rp2040.py: +# PIO-машина управляет ВСЕМИ восемью линиями GP8..GP15 независимо от +# BITS. Неиспользуемые младшие биты ВЫВОДЯТСЯ В 0 аппаратно, а не +# остаются входами/«плавающими». В прежней версии при BITS<8 пины +# GP8..GP(15-BITS) вообще не конфигурировались как выходы, хотя шапка +# обещала «= 0»: на макете они висели в воздухе и собирали наводки. +# Теперь: out pins, 8 (всегда 8 бит), буфер хранит значение, +# сдвинутое влево на (8-BITS) — выравнивание по старшему биту то же. +# +# Вариант 3 генератора для лабораторных работ. Совместим по системе команд +# с вариантами 1 (.ino, Arduino) и 2 (01-...-circuitpython.py), но сигнал +# для режима «синус» формирует аппаратная PIO-машина RP2040, данные в неё +# подаются через DMA, а АЦП читается библиотекой analogbufio. +# +# ПРОВЕРЕНО НА СТЕНДЕ 2026-10-28 (преподаватель): цифровой выход PIO +# снят логическим анализатором — форма синуса верная. Лестница R-2R при +# этой проверке НЕ запаивалась, поэтому аналоговая часть (размах после +# внешнего ФНЧ, реальная разрядность по постоянному уровню) ещё не +# измерялась. +# +# AI-сгенерировано, 2026-10-27; проверка цифровой части — 2026-10-28. +# +# Требует библиотеку: adafruit_pioasm -> скопировать в /lib на плате. +# https://github.com/adafruit/Adafruit_CircuitPython_PIOASM +# На плату положить как code.py (или запускать из REPL). +# +# ───────────────────────── ЗАЧЕМ ТРЕТИЙ ВАРИАНТ ───────────────────────── +# +# Варианты 1 и 2 формируют синус программно: каждый такт надо успеть +# посчитать следующую выборку и записать её в ШИМ. Чем выше несущая, +# тем больше нагрузка на CPU, и на CircuitPython (интерпретатор) частота +# выборок падает до единиц-десятков кГц (см. шапку варианта 2). +# +# Здесь считает PIO: CPU один раз готовит буфер, дальше вообще не +# участвует. PIO-машина делает ровно две вещи в цикле: +# +# pull block ; взять следующее 32-битное слово из FIFO (через DMA) +# out pins, 8 ; вывести 8 бит слова в GP8..GP15 (лишние = 0) +# +# То есть PIO = «проигрыватель» заранее посчитанного буфера. Никаких +# вычислений sin() в реальном времени — весь сигнал посчитан в Python +# заранее и лежит в массиве, который DMA подаёт в FIFO по кругу. +# +# Частота выходного сигнала НЕ зависит от содержимого буфера и задаётся +# только двумя вещами: +# +# f_вых = f_пио / (2 * длина_буфера) +# +# где 2 — число тактов PIO на один сэмпл (pull block + out pins). Длина +# буфера равна N * OVERSAMPLE (см. ниже). Меняя только частоту PIO, можно +# двигать f_вых, вообще не пересчитывая сигнал. +# +# ─────────────────── ЧИСЛО БИТ И ТОЧНОСТЬ УРОВНЯ ──────────── +# +# Число бит BITS задаёт разрядность порта (1..8). Если BITS мало, шаг +# квантования большой, и «в лоб» сигнал получился бы ступенчатым. +# +# Здесь применяется ДЕЛЬТА-СИГМА 1-го порядка: на каждом шаге выход +# меняется только на +1, -1 или 0. Отсюда два важных свойства: +# +# 1. СОСЕДНИЕ ЗНАЧЕНИЯ БУФЕРА ВСЕГДА ОТЛИЧАЮТСЯ НЕ БОЛЕЕ ЧЕМ НА 1. +# Это гарантия: никаких скачков «на половину диапазона». +# (В прежней версии через ШИМ чётной базы скачки доходили до 4 при +# 3 битах — это недопустимо.) +# +# 2. Средний уровень за период точно равен целевому: аккумулятор +# накапливает разность «цель минус выход» и отрабатывает её. +# Дробная точность возникает из чередования соседних кодов во +# времени, поэтому отдельный ШИМ младшего бита не нужен. +# +# БИТНОСТЬ и ЭФФЕКТИВНАЯ РАЗРЯДНОСТЬ — РАЗНЫЕ величины: +# * битность B — мгновенный код на пинах (всегда ровно B бит); +# * эфф. разрядность — разрешение СРЕДНЕГО после ФНЧ: +# число уровней = (2^B - 1) * m + 1 +# При малом B они заметно расходятся: B=1, m=256 даёт 257 уровней +# среднего (~8 бит) при одном бите на порту. +# +# При этом соседние ЧИСЛА не обязаны различаться одним битом: переход +# 7 -> 8 (0111 -> 1000) меняет все биты, и это НОРМАЛЬНО и полезно — +# так и выглядит переход между соседними уровнями квантования. +# +# ─── ОГРАНИЧЕНИЕ СКОРОСТИ (slew) ─── +# Дельта-сигма не может нарастать быстрее 1 за шаг. Если целевая функция +# меняется быстрее, буфер получится с искажением. Поэтому: +# - цель ограничивается по наклону до 1 на шаг; +# - буфер считается на ДВА периода, берётся второй: аккумулятор к тому +# моменту в установившемся режиме, и стык цикла (DMA крутит буфер +# по кругу) получается бесшовным. +# Команда I показывает max|Δu| — если он > 1, форма искажена, нужно +# увеличить N или уменьшить амплитуду M. +# +# ─── ПРОИЗВОЛЬНАЯ ФУНКЦИЯ ─── +# Форма сигнала задаётся функцией func(t), где t — «время внутри периода»: +# 0 в начале, 1 в конце, шаг 1/m (m = N*OVERSAMPLE). По умолчанию — +# sin(2*pi*t). Команда FN выбирает форму из набора, FP принимает +# произвольное выражение от t, например: +# FP 0.3*sin(2*pi*t)+0.5*sin(4*pi*t) +# Это даёт студенту генератор по любой введённой формуле. +# +# Оговорка: между соседними сэмплами сигнала base меняется на 2 (шаг пары +# «чётное/нечётное»). Это неизбежная цена того, что в паре кодов обязан +# меняться ровно один бит. Такие переходы в OVERSAMPLE раз реже тактов PIO +# и подавляются внешним ФНЧ. +# +# ───────────────────────── ПИНЫ ───────────────────────── +# +# Используются GP8..GP15. PIO управляет ВСЕМИ восемью линиями, поэтому +# неиспользуемые младшие биты всегда выведены в НОЛЬ (не «плавают»). +# Данные ВЫРАВНЕНЫ ПО СТАРШЕМУ биту порта: старший бит значения всегда +# на GP15, младший — на GP(16-B). Пины ниже принудительно в нуле: +# +# B=8 -> данные GP8 (мл.бит) ... GP15 (ст.бит) — вся гребёнка +# B=4 -> данные GP12 ... GP15, GP8..GP11 = 0 (выход, низкий уровень) +# B=3 -> данные GP13 ... GP15, GP8..GP12 = 0 (выход, низкий уровень) +# B=1 -> данные GP15, GP8..GP14 = 0 (выход, низкий уровень) +# +# Выравнивание по старшему биту даёт полную амплитуду на выходе при +# любой разрядности: 3-битное значение 0..7 занимает ВСЮ шкалу ЦАП, +# а не первые 3/8 её (как было бы при выравнивании по младшему биту). +# Полная аналоговая разрядность получается на лестнице R-2R, +# подключённой к GP8..GP15 (GP15 — старший разряд). +# +# ─────────────────────── ВНЕШНИЙ ФИЛЬТР ─────────────────────── +# +# GP15 ──[R1]──┬──[R2]──┬──────► OUT (вход DUT) +# │ │ +# [C1] [C2] +# │ │ +# GND GND +# +# При BITS=1 форма сигнала на GP15 — это ШИМ со средней скважностью, +# равной уровню сигнала. Для выделения синуса нужен ФНЧ, как в вариантах +# 1 и 2. При BITS>1 и лестнице R-2R ФНЧ нужен только для сглаживания +# ступенек дискретизации; f_c можно поднять. +# +# Постоянная составляющая: при полной амплитуде средний уровень = половина +# шкалы. Где смещение недопустимо — разделительный 1 мкФ + 47 кОм на землю. +# +# ─────────────────────── КОМАНДЫ (совместимы с вариантами 1 и 2) ─────── +# +# F <Гц> — частота (1..200000); в режиме S управляет частотой +# прямоугольника (историческая совместимость) +# SF <Гц> — частота СИНУСА (1..200000) +# W Q | W S — режим: Q — прямоугольник (PWM), S — синус (PIO) +# D <0..255> — скважность прямоугольника +# M <0..127> — амплитуда синуса +# A — измерить размах на ADC0 -> "A <мВ>" +# SWEEP f0 f1 n ms — логарифмический свип: на точку "P <Гц> <мВ>", затем "DONE" +# STOP — прервать свип +# +# ДОПОЛНИТЕЛЬНЫЕ команды этого варианта (их и стоит «крутить» на занятии): +# +# B <1..8> — число бит порта +# O <1..64> — точек на сэмпл (дробление периода: m = N*O) +# N <16..4096> — длина одного периода в «полезных» сэмплах +# FN <имя> — форма сигнала из набора: sin, sine2, triangle, +# saw, square +# FP <выражение> — произвольная функция от t (время внутри периода +# 0..1), например: +# FP 0.3*sin(2*pi*t)+0.5*sin(4*pi*t) +# I — показать настройки, f_min/f_max и наклон цели +# +# ОГРАНИЧЕНИЕ (важно для лаборатории): минимальная частота PIO = +# 125 МГц / 65536 ≈ 1907 Гц (аппаратный предел делителя clkdiv). +# Значит f_вых_min = 1907 / (2 * N * OVERSAMPLE). Для типовых параметров +# (N=256, OVERSAMPLE=4) это ≈ 0,93 Гц. Если нужна ещё ниже — увеличивайте +# N или OVERSAMPLE (растёт буфер). Команда I показывает обе частоты. +# +# ВНИМАНИЕ: ADC0 (GP26) — вход 0..3,3 В. Сигнал больше 3,3 В подавать +# только через делитель. Схему без питания к ADC не подключать. + +import array +import math +import sys +import time + +import board +import digitalio +import pwmio + +try: + import adafruit_pioasm +except ImportError: # pragma: no cover + print("Нужна библиотека adafruit_pioasm в /lib (см. шапку файла)") + raise + +try: + import rp2pio +except ImportError: # pragma: no cover + print("rp2pio доступен только на RP2040/RP2350") + raise + +try: + import analogbufio +except ImportError: # pragma: no cover + analogbufio = None + print("# analogbufio недоступен: команды A и SWEEP работают по-простому") + + +# ─────────────────────────── константы ─────────────────────────── + +PIN_MSB = board.GP15 # PWM для режима Q (прямоугольник) +PIN_TRIG = board.GP16 # синхроимпульс — ВНЕ диапазона PIO (GP8..GP15)! +PIN_ADC = board.GP26 # ADC0 — вход измерения +# +# ─── КАРТА ПИНОВ (важно: без пересечений) ─── +# GP8..GP15 — выход PIO в режиме S. Управляются ВСЕ 8 линий, поэтому +# неиспользуемые младшие биты гарантированно в нуле. +# Данные выровнены ПО СТАРШЕМУ биту: старший бит значения +# всегда на GP15, диапазон GP(16-B)..GP15, пины ниже — 0. +# GP15 — PWM в режиме Q (прямоугольник). С PIO не пересекается: +# режимы включаются по очереди (ensure_sm/release_sm). +# GP16 — синхроимпульс. ВАЖНО: не GP14! При B>=2 PIO занимает +# непрерывный диапазон, в который GP14 попадает. +# GP26 (A0) — вход ADC. +# Синхро и ADC вынесены за пределы GP8..GP15. + +SYS_CLK = 125_000_000 # тактовая RP2040, Гц +# Тактов PIO на один сэмпл. Программа = pull block + out pins = 2 такта, +# это подтверждено замером: при CPC=1 частота выходила ВДВОЕ меньше +# запрошенной (1000 -> 500), значит реально нужно CPC=2. +# При расхождении на конкретном стенде — подстроить командой CLC +# или автоматически: CAL <измеренная_Гц>. +CPC = 2 +MAX_CLKDIV = 65536 # аппаратный предел делителя +ADC_FULL_MV = 3300 # опорное напряжение ADC, мВ + +# Лимит буфера сэмплов. SRAM RP2040 — 264 КБ; CircuitPython, USB и ADC +# занимают ~60-70 КБ, под буфер реально доступно ~180 КБ. Берём 128 КБ +# (32 768 сэмплов по 4 байта) — с запасом под ADC-буфер и фрагментацию. +MAX_BUFFER_BYTES = 128 * 1024 + +# PIO-программа. Число бит в out — непосредственный операнд инструкции, +# поэтому программа собирается заново при смене BITS. Цикл организует +# автоматический wrap. +# +# ВАЖНО (это и есть исправление): выводим ВСЕГДА 8 бит — `out pins, 8` — +# независимо от BITS. Тогда PIO конфигурирует GP8..GP15 как выходы и +# каждый такт выставляет на них состояние из OSR. Неиспользуемые младшие +# биты при этом получают физический 0, потому что в буфер пишется +# значение, сдвинутое влево на (8-BITS) (см. _build_buffer). +# +# auto_pull=False => OSR заполняется ТОЛЬКО командой pull, поэтому +# одно слово FIFO = ровно один вывод out pins,8. Остаток бит слова +# отбрасывается при следующем pull (шунтировать его не нужно). +PROG_TEMPLATE = """ +.program dds +pull block +out pins, 8 +""" + + +def _default_wave(t): + """Форма по умолчанию: синус. t — доля периода [0,1).""" + return math.sin(2.0 * math.pi * t) + + +# ─────────────────────────── класс PioDDS ─────────────────────────── + +class PioDDS: + """DDS на PIO: буфер сэмплов -> DMA -> FIFO -> out pins. + + Свойства, которые студент может менять на занятии: + bits — число бит порта (1..8) + oversample — оверсемплинг младшего бита (1..64) + n — длина периода в «полезных» сэмплах (16..4096) + level — амплитуда (0..127, как команда M) + freq — частота выходного сигнала, Гц + """ + + def __init__(self, bits=8, oversample=4, n=256, freq=1000): + self.bits = max(1, min(8, int(bits))) + self.oversample = max(1, min(64, int(oversample))) + self.n = max(16, min(4096, int(n))) + self.level = 127 + self.freq = max(1, min(200000, int(freq))) + self.cpc = CPC # тактов PIO на сэмпл (калибровка) + self._func = _default_wave # форма сигнала (func(t)) + self._wave_name = "sin" # имя формы для команды I + self.buffer = None + self.sm = None + self._build_buffer() + + # ─────────────── управление ресурсом (GPIO) ─────────────── + # + # ВАЖНО: PWM (режим Q) и PIO (режим S) оба используют GP15, а + # CircuitPython по умолчанию выделяет пин эксклюзивно. Поэтому машина + # создаётся лениво при переходе в режим S и освобождается при возврате + # в Q. Одновременно пин держит только один периферийный блок. + + def ensure_sm(self): + """Создать PIO-машину, если её ещё нет (пин должен быть свободен).""" + if self.sm is None: + self._build_sm() + return self.sm + + def release_sm(self): + """Остановить PIO и вернуть пины (перед запуском PWM).""" + if self.sm is not None: + try: + self.sm.deinit() + except Exception: + pass + self.sm = None + + # ─────────────── расчёт буфера ─────────────── + + def _build_buffer(self): + """Посчитать N*OVERSAMPLE сэмплов синуса. + + Ключевой принцип: внутри одной группы оверсемплинга переключается + ТОЛЬКО МЛАДШИЙ бит. Старшие биты (base) на протяжении группы + НЕ меняются — иначе на выходе получались бы «скачки» всего порта + (например 127 = 0111 1111 -> 128 = 1000 0000 меняет все 8 бит). + + Как это устроено: + 1. base — ЧЁТНОЕ число: младший бит у него всегда 0 и свободен + под ШИМ. Тогда пара (base, base+1) отличается ровно одним + битом (например 126 и 127). + 2. Доля единиц в младшем бите за OVERSAMPLE слотов задаёт дробную + часть уровня (это ШИМ на одном бите). + 3. Остаток (то, что не влезло в дробь на этом шаге) переносится + на СЛЕДУЮЩИЙ сэмпл сигнала — error diffusion. Благодаря этому + среднее по времени всё равно точно равно целевому уровню, + хотя мгновенно доступны только значения base и base+1. + + Оговорка: base меняется на 2 между группами (ступенька в 2 LSB) — + это неизбежно, раз пара «шаг ШИМ» обязана быть чётной. Но такие + переходы происходят в OVERSAMPLE раз реже тактов PIO и подавляются + внешним ФНЧ. Внутри группы старшие биты стабильны. + """ + bits, oso, n = self.bits, self.oversample, self.n + full = (1 << bits) - 1 # максимум для данного числа бит + m = n * oso # всего точек сигнала на период + + # Защита по ОЗУ: RP2040 имеет 264 КБ, но Python в CircuitPython + # обычно может занять ~100-150 КБ. Буфер — array('I') по 4 байта + # на сэмпл. N=4096, OVERSAMPLE=64 дают 1 МБ и гарантированно + # уронят плату MemoryError. Ограничиваем заранее и говорим + # понятным текстом, что уменьшить. + max_samples = MAX_BUFFER_BYTES // 4 + if m > max_samples: + raise ValueError( + "Буфер слишком большой: N=%d x OVERSAMPLE=%d = %d сэмплов " + "(%d КБ), лимит %d КБ. Уменьшите N или OVERSAMPLE." + % (n, oso, m, m * 4 // 1024, MAX_BUFFER_BYTES // 1024) + ) + + # 1) Целевые уровни и дельта-сигма считаются ПОТОКОВО, без списка + # на два периода: раньше строился массив 2*m и брался второй + # период, что вдвое расходовало ОЗУ. Теперь нужен только + # буфер m сэмплов, а «первый период» проигрывается насквозь + # для установления аккумулятора. + # + # t — «время внутри периода», меняется 0..1 с шагом 1/m. + + def target_at(k): + return self._target((k % m) / m) + + # 2) Ограничение скорости изменения цели до 1 на шаг выполняется + # прямо в цикле ниже (переменная prev_u). + + # 3) Дельта-сигма 1-го порядка: выход меняется только на ±1 или 0, + # значит СОСЕДНИЕ ЗНАЧЕНИЯ ВСЕГДА ОТЛИЧАЮТСЯ НЕ БОЛЕЕ ЧЕМ НА 1. + full_f = float(full) + v = max(0, min(full, int(round(target_at(0))))) + acc = 0.0 + out = array.array("I", [0]) * m + slew_max = 0.0 + prev_u = None + idx = 0 + # два прохода: первый «холостой» (устанавливает аккумулятор), + # второй пишет в буфер + for period in (0, 1): + for k in range(m): + u = target_at(k) + # ограничение наклона + if prev_u is not None: + d = u - prev_u + if d > 1.0: + u = prev_u + 1.0 + elif d < -1.0: + u = prev_u - 1.0 + if abs(u - prev_u) > slew_max: + slew_max = abs(u - prev_u) + prev_u = u + # дельта-сигма + acc += u - v + if acc >= 1.0: + v += 1 + acc -= 1.0 + elif acc <= -1.0: + v -= 1 + acc += 1.0 + if v < 0: + v = 0 + elif v > full: + v = full + if period == 1: + # Выравнивание по старшему биту: значение 0..full + # занимает старшие BITS линий GP(16-B)..GP15, а + # младшие (8-BITS) остаются нулями. PIO выводит все + # 8 бит, поэтому эти нули попадают на пины физически. + out[idx] = v << (8 - bits) + idx += 1 + + self._slew_max = slew_max + self._slew_ok = slew_max <= 1.0 + self.buffer = out + + def _target(self, t): + """Целевой уровень в шкале [0, full] для момента t в [0,1). + + По умолчанию — синус. Может быть заменён произвольной функцией + студента (см. set_function): func(t) задаёт значение в диапазоне + [-1, 1], t — доля периода (0 в начале, 1 в конце, шаг 1/m). + """ + f = self._func(t) + if f < -1.0: + f = -1.0 + elif f > 1.0: + f = 1.0 + full = (1 << self.bits) - 1 + return (f + 1.0) * 0.5 * full * (self.level / 127.0) + + def set_function(self, func, name=None): + """Заменить форму сигнала функцией func(t). + + func принимает t в [0,1) и возвращает значение в [-1, 1]. + t — доля периода: 0 в начале, 1 в конце, шаг 1/(N*OVERSAMPLE). + + На занятии проще пользоваться командами генератора: + FN triangle — форма из набора + FP 0.3*sin(2*pi*t)+0.5*sin(4*pi*t) — своя формула + + Из кода можно передать любую именованную функцию: + def wave(t): + return math.sin(2*math.pi*t) + dds.set_function(wave) + """ + self._func = func + self._wave_name = name or getattr(func, "__name__", "custom") + self._check_slew() + self._build_buffer() + if self.sm is not None: + self._repush() + + def reset_function(self): + """Вернуть форму по умолчанию (чистый синус).""" + self._func = _default_wave + self._wave_name = "sin" + self._check_slew() + self._build_buffer() + if self.sm is not None: + self._repush() + + def _check_slew(self): + """Оценить, не превышает ли функция скорость, которую может + отработать дельта-сигма. Возвращает (max|Δu|, ok).""" + m = self.n * self.oversample + us = [self._target(k / m) for k in range(min(m, 4096))] + if len(us) < 2: + return 0.0, True + dmax = max(abs(a - b) for a, b in zip(us, us[1:])) + return dmax, dmax <= 1.0 + + def effective_bits(self): + """Точная оценка разрешения СРЕДНЕГО уровня за период. + + Дельта-сигма чередует соседние коды, а внешний ФНЧ усредняет. + Число различимых средних за m точек: + (2^B - 1) * m + 1 + (а не m * 2^B, как давала упрощённая формула B + log2(m) — + она завышала на 1 бит при малых B, например B=1 давала 9 бит + вместо фактических 8). + + В CircuitPython math.log2 НЕТ (в CPython есть) — считаем через + math.log(x) / math.log(2). + """ + full = (1 << self.bits) - 1 + levels = full * (self.n * self.oversample) + 1 + if levels <= 1: + return float(self.bits) + return math.log(levels) / math.log(2) + + # ─────────────── сборка PIO-машины ─────────────── + + def f_pio(self): + """Требуемая частота PIO: f_вых * тактов_на_сэмпл * длина_буфера.""" + return self.freq * self.cpc * (self.n * self.oversample) + + def f_out_max(self): + """Максимальная достижимая f_вых при текущих N и OVERSAMPLE. + + f_пио не может превысить тактовую (SYS_CLK), значит + f_вых_max = SYS_CLK / (self.cpc * N * OVERSAMPLE). + """ + return SYS_CLK / (self.cpc * self.n * self.oversample) + + def _clamp_f_pio(self): + return max(int(SYS_CLK / MAX_CLKDIV), min(SYS_CLK, int(self.f_pio()))) + + def _build_sm(self): + if self.sm is not None: + try: + self.sm.deinit() + except Exception: + pass + # Выравнивание ПО СТАРШЕМУ биту порта (left-aligned): старший бит + # значения всегда на GP15, данные занимают GP(16-bits)..GP15. + # Младшие пины диапазона GP8..GP(15-bits) выводятся в 0. + # + # B=8 -> данные GP8 ... GP15 (вся гребёнка) + # B=4 -> данные GP12 ... GP15 (GP8..GP11 = 0) + # B=3 -> данные GP13 ... GP15 (GP8..GP12 = 0) + # B=1 -> данные GP15 (GP8..GP14 = 0) + # + # ОТЛИЧИЕ ОТ 04-...: PIO всегда занимает ВЕСЬ диапазон GP8..GP15 + # (программа out pins,8), поэтому «лишние» линии — это реальные + # выходы с низким уровнем, а не свободные входы. + first_num = 8 + first_pin = getattr(board, "GP%d" % first_num) + + # Защита от наложения портов: PIO всегда занимает GP8..GP15. + # Синхроимпульс и ADC должны быть ВНЕ этого диапазона. + pio_pins = set(range(first_num, 16)) + for name, pin in (("PIN_TRIG", 16), ("PIN_ADC", 26)): + if pin in pio_pins: + raise ValueError( + "%s=GP%d попадает в диапазон PIO GP%d..GP15. " + "Перенесите этот пин." % (name, pin, first_num) + ) + # Диагностика: у пина TRIG не должно быть активного владельца. + # (CircuitPython сам бросит ValueError при попытке занять занятый + # пин, но лучше проверить заранее и сказать понятнее.) + + # ВАЖНО: у adafruit_pioasm.Program нет метода .assemble() — + # собранные инструкции лежат в свойстве .assembled, а настройки + # из директив — в словаре .pio_kwargs (см. документацию библиотеки: + # program = adafruit_pioasm.Program(text) + # sm = rp2pio.StateMachine(program.assembled, **program.pio_kwargs) + program = adafruit_pioasm.Program(PROG_TEMPLATE) + + # Собираем аргументы конструктора: настройки из .pio_kwargs плюс + # наши явные параметры. Через setdefault — чтобы будущие версии + # библиотеки не вызвали конфликт «duplicate keyword argument». + kwargs = dict(program.pio_kwargs) + kwargs.setdefault("frequency", self._clamp_f_pio()) + kwargs.setdefault("first_out_pin", first_pin) # всегда GP8 + kwargs.setdefault("out_pin_count", 8) # всегда все 8 линий + kwargs.setdefault("auto_pull", False) # тянем явно: pull block + kwargs.setdefault("pull_threshold", 32) + kwargs.setdefault("out_shift_right", True) # младшие биты -> порт + kwargs.setdefault("initial_out_pin_state", 0) + # Все 8 линий — выходы. Неиспользуемые младшие биты держат 0 + # (значение в буфере выровнено влево, см. _build_buffer). + kwargs.setdefault("initial_out_pin_direction", 0xFF) + + self.sm = rp2pio.StateMachine(program.assembled, **kwargs) + # DMA по кругу: буфер проигрывается бесконечно, CPU не участвует + self.sm.background_write(loop=self.buffer) + + def _repush(self): + """Перезапустить DMA на новый буфер (после его пересчёта).""" + if self.sm is None: + return + try: + self.sm.stop_background_write() + except Exception: + pass + self.sm.background_write(loop=self.buffer) + + # ─────────────── настройки ─────────────── + + def set_frequency(self, f): + """Установить частоту. Возвращает фактическую f_вых (может быть + меньше запрошенной, если упёрлись в тактовую PIO).""" + self.freq = max(1, min(200000, int(f))) + target = self._clamp_f_pio() + if self.sm is None: + return self.freq + try: + self.sm.frequency = target + # проверяем, что установка реально сработала: у rp2pio свойство + # frequency может отличаться от запрошенного (округление), но + # не должно отличаться в разы. Если отличается — пересобираем. + actual = int(self.sm.frequency) + if target > 0 and not (0.5 <= actual / target <= 2.0): + self._build_sm() + except Exception: + self._build_sm() # запасной путь: пересборка + # фактическая частота выхода после обрезки делителя/тактовой + f_real = self._clamp_f_pio() / (self.cpc * self.n * self.oversample) + return f_real + + def min_m_for_bits(self): + """Минимальное число точек на период, чтобы синус полной амплитуды + уложился в скорость дельта-сигмы (max|Δu| <= 1). + + Из max|Δu| = pi*full*amp/m <= 1 следует m >= pi*full*amp. + """ + full = (1 << self.bits) - 1 + return int(math.pi * full * (self.level / 127.0)) + 1 + + def slew_warning(self): + """Текст предупреждения, если форма не уложится в slew, или None.""" + if getattr(self, "_slew_ok", True): + return None + m = self.n * self.oversample + need = self.min_m_for_bits() + return ("форма не уложится: max|Δu|=%.2f > 1 при m=%d. Нужно m>=%d " + "(увеличьте N или OVERSAMPLE примерно в %d раз, либо " + "уменьшите амплитуду M)." + % (self._slew_max, m, need, + max(1, (need + m - 1) // m))) + + def set_cpc(self, cpc): + """Задать число тактов PIO на сэмпл (калибровка частоты).""" + self.cpc = max(1, min(8, int(cpc))) + + def calibrate(self, measured_hz): + """Автокалибровка CPC по измеренной частоте. + + Ставим частоту F, измеряем осциллографом M. Тогда + f_вых = f_пио / (T * m) = F * CPC / T => T = F * CPC / M + Новое CPC делаем равным T, чтобы в следующий раз факт совпал с + запросом. Возвращает (было, стало). + """ + was = self.cpc + if measured_hz <= 0: + return was, was + t_needed = self.freq * self.cpc / float(measured_hz) + self.cpc = max(1, min(8, int(round(t_needed)))) + self.set_frequency(self.freq) # применить новый f_пио + return was, self.cpc + + def needs_warning(self): + """True, если запрошенная частота недостижима при текущих N и O.""" + return self.f_pio() > SYS_CLK + + def suggest_reduction(self): + """Подсказка, что изменить, чтобы достичь requested-частоты.""" + # нужен меньший CPC*N*O: предлагаем уменьшить N или O + need = self.freq * self.cpc # требуется N*O не больше + max_n_o = max(1, int(SYS_CLK / need)) + return ("при N=%d и O=%d максимум %.0f Гц. Чтобы получить %d Гц, " + "нужно N*O <= %d (например N=%d, O=1)." + % (self.n, self.oversample, self.f_out_max(), + self.freq, max_n_o, min(self.n, max_n_o))) + + def set_amplitude(self, level): + self.level = max(0, min(127, int(level))) + self._build_buffer() + if self.sm is not None: + self._repush() + + def set_bits(self, bits): + bits = max(1, min(8, int(bits))) + if bits == self.bits: + return + was_active = self.sm is not None + self.bits = bits + self._build_buffer() # шкала зависит от числа бит + if was_active: + self._build_sm() # сдвиг в буфере зависит от бит + + def set_oversample(self, oso): + oso = max(1, min(64, int(oso))) + if oso == self.oversample: + return + was_active = self.sm is not None + old = self.oversample + self.oversample = oso + try: + self._build_buffer() + except ValueError: + self.oversample = old # откат: объект остаётся рабочим + raise + if was_active: + self._build_sm() + + def set_n(self, n): + n = max(16, min(4096, int(n))) + if n == self.n: + return + was_active = self.sm is not None + old = self.n + self.n = n + try: + self._build_buffer() # длина буфера изменилась + except ValueError: + self.n = old # откат + raise + if was_active: + self._build_sm() # f_pio и размер DMA-буфера + + def info(self): + f_pio = self.f_pio() + f_pio_clamped = self._clamp_f_pio() + f_min = (SYS_CLK / MAX_CLKDIV) / (self.cpc * self.n * self.oversample) + f_max = self.f_out_max() + # ФАКТИЧЕСКАЯ частота после обрезки f_pio по тактовой. Показывать + # только запрошенную f_out вводит в заблуждение: при запросе выше + # потолка реально выходит f_max (обычно чуть ниже из-за голодания + # FIFO на пределе). + f_real = f_pio_clamped / (self.cpc * self.n * self.oversample) + wf = " ! ЗАПРОС ВЫШЕ ПОТОЛКА: факт %.0f Гц" % f_real if \ + self.needs_warning() else "" + wave = getattr(self, "_wave_name", "sin") + slew = getattr(self, "_slew_max", 0.0) + sleww = " ok" if getattr(self, "_slew_ok", True) else \ + " ! ЦЕЛЬ СЛИШКОМ БЫСТРАЯ (max|Δu|=%.2f>1)" % slew + return ("BITS=%d OVERSAMPLE=%d (эфф.разрядность %.1f бит) N=%d " + "BUFFER=%d AMP=%d f_out=%d Гц (факт %.0f) f_pio=%d Гц " + "CPC=%d CLKDIV=%.2f f_min=%.2f f_max=%.0f Гц shape=%s " + "slew=%.3f%s%s" % ( + self.bits, self.oversample, self.effective_bits(), self.n, + self.n * self.oversample, self.level, + self.freq, f_real, f_pio, self.cpc, + SYS_CLK / max(1, f_pio_clamped), f_min, f_max, wave, slew, + sleww, wf)) + def deinit(self): + if self.sm is not None: + try: + self.sm.deinit() + except Exception: + pass + self.sm = None + + +# ───────────────────── прямоугольник на PWM ───────────────────── +# +# Режим Q оставлен на аппаратном ШИМ (как в вариантах 1 и 2): для +# прямоугольника PIO не нужен, а PWM проще и точнее по частоте. +# Пины разные (PWM — на GP15, PIO при BITS>1 занимает GP8..GP15), +# поэтому в режиме S PWM глушится, а в режиме Q глушится PIO — +# см. cmd_w ниже. + +class SquarePWM: + def __init__(self, pin=PIN_MSB): + self.pwm = pwmio.PWMOut(pin, frequency=1000, + duty_cycle=0, variable_frequency=True) + self.enabled = False + + 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=200000): + """Размах (пик-пик) на ADC0 в милливольтах. + + Чтение через analogbufio (аппаратный ADC + DMA). Если библиотеки нет — + откатываемся на простое аналоговое чтение (медленнее и грубее). + """ + if analogbufio is None: + import analogio + adc = analogio.AnalogIn(PIN_ADC) + vmin, vmax = 65535, 0 + t0 = time.monotonic() + while (time.monotonic() - t0) * 1000 < window_ms: + v = adc.value + if v < vmin: + vmin = v + if v > vmax: + vmax = v + adc.deinit() + return (vmax - vmin) * ADC_FULL_MV // 65535 + + length = int(rate * window_ms / 1000) + length = max(64, min(8192, length)) # ограничение по ОЗУ + buf = array.array("H", [0]) * length + + adc = analogbufio.BufferedIn(PIN_ADC, sample_rate=rate) + try: + adc.readinto(buf, loop=False) + finally: + adc.deinit() + + # при loop=False 12-битные значения масштабированы в 16 бит + return (max(buf) - min(buf)) * ADC_FULL_MV // 65535 + + +# ─────────────────────────── свип ─────────────────────────── +# +# ВАЖНО: периферия создаётся ЛЕНИВО, внутри ensure_trig(), а не на уровне +# модуля. Если создать DigitalInOut при загрузке файла и пин занят — упадёт +# весь модуль, и мы увидим «program запускается, ничего не выводит». +# Ленивая инициализация позволяет сначала напечатать HELP и сообщить +# понятную ошибку. + +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): + # ваш пример: 0.3*sin(t) + 0.5*sin(2t) в долях периода + 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. Ограничиваем окружение eval, чтобы +# студент не мог дотянуться до встроенных объектов, только математика. +_EXPR_NAMES = { + "sin": math.sin, "cos": math.cos, "tan": math.tan, + "pi": math.pi, "abs": abs, +} + + +def _eval_expr(expr, t): + """Вычислить выражение от t. Отдельная функция (не lambda) — на плате + CircuitPython синтаксис lambda в разборе команд не сработал. + + Окружение ограничено: только sin/cos/tan/pi/abs и переменная t. + Встроенные объекты закрыты, поэтому __import__ и open недоступны. + """ + env = dict(_EXPR_NAMES) + env["t"] = t + return eval(expr, {"__builtins__": {}}, env) + + +# защита от выражений, которые «зависают» или дают огромные значения +EXPR_LEN_MAX = 120 + + +def _expr_ok(expr): + """Проверить выражение до использования. Возвращает текст ошибки + или None, если всё в порядке.""" + if len(expr) > EXPR_LEN_MAX: + return "выражение слишком длинное (лимит %d)" % EXPR_LEN_MAX + # пробуем вычислить в нескольких точках — ловим синтаксис и деление на 0 + 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): + """Вернуть функцию func(t) по тексту выражения, без lambda.""" + def wave(t): + return _eval_expr(expr, t) + return wave + + +# ─────────────────────────── main ─────────────────────────── + +# ─────────────────────────── подробный хелп ─────────────────────────── + +def print_help(): + """Подробная справка по командам с форматами и примерами.""" + lines = [ + "# ─────────── DDS-генератор на PIO: команды ───────────", + "# Обозначения: <..> — обязательный аргумент.", + "# Команда вводится строкой, Enter — конец строки.", + "#", + "# РЕЖИМ И ЧАСТОТА", + "# W Q — режим прямоугольника (аппаратный ШИМ на GP15)", + "# W S — режим синуса (аппаратный PIO на GP8..GP15)", + "# F <Гц> — частота, 1..200000", + "# в режиме Q — прямоугольника; в S — синуса", + "# пример: F 1000", + "# SF <Гц> — частота СИНУСА (то же, но явно), 1..200000", + "# пример: SF 25", + "#", + "# ФОРМА СИГНАЛА", + "# D <0..255> — скважность прямоугольника (128 = 50 %)", + "# действует только в режиме Q", + "# пример: D 64 (25 % включено)", + "# M <0..127> — амплитуда синуса (127 = максимум)", + "# пример: M 32 (четверть шкалы)", + "#", + "# ИЗМЕРЕНИЕ И СВИП", + "# A — размах на ADC0 (GP26), мВ -> ответ 'A <мВ>'", + "# SWEEP ", + "# логарифмический свип f0..f1 за n точек,", + "# пауза dwell_ms на точку; печатает 'P <Гц> <мВ>',", + "# в конце 'DONE'", + "# пример: SWEEP 10 100000 25 150", + "# STOP — прервать свип (надёжнее Ctrl-C)", + "#", + "# КАРТА ПИНОВ (пересечений нет)", + "# GP8..GP15 — выход PIO в режиме S. Данные выровнены по", + "# СТАРШЕМУ биту: старший бит всегда на GP15,", + "# диапазон GP(16-B)..GP15; пины ниже = 0", + "# GP15 — ШИМ в режиме Q (прямоугольник)", + "# GP16 — синхроимпульс (вне диапазона PIO)", + "# GP26 (A0) — вход ADC, до 3.3 В (через делитель)", + "#", + "# НАСТРОЙКИ PIO (только этот вариант) — применяются в режиме S", + "# B <1..8> — число бит порта (разрядность, GP8..GP15)", + "# B 1 — только GP15 (как в вариантах 1 и 2)", + "# B 8 — все восемь пинов (нужна лестница R-2R)", + "# пример: B 1", + "# O <1..64> — точек на сэмпл (влияет на m = N*O, см. ниже)", + "# пример: O 4", + "# N <16..4096> — длина периода в «полезных» сэмплах", + "# пример: N 256", + "# I — текущие настройки, f_min/f_max, slew", + "# CLC <1..8> — тактов PIO на сэмпл вручную. По умолчанию 2", + "# (pull block + out pins). Если факт вдвое", + "# меньше/больше запроса — подстройте.", + "# CAL <Гц> — автокалибровка: поставьте частоту, измерьте", + "# осциллографом и введите измеренное значение,", + "# например: CAL 500 (при запросе 1000).", + "# CPC подстроится сам, факт совпадёт с запросом.", + "#", + "# ФОРМА СИГНАЛА (func(t), t — время внутри периода, 0..1)", + "# FN <имя> — форма из набора: sin, sine2, triangle,", + "# saw, square. пример: FN triangle", + "# FP <выражение> — своя функция от t; доступны sin, cos, tan,", + "# pi, abs. пример:", + "# FP 0.3*sin(2*pi*t)+0.5*sin(4*pi*t)", + "#", + "# КАК РАБОТАЕТ ТОЧНОСТЬ (дельта-сигма)", + "# Выход меняется только на +1/-1/0 за шаг, поэтому СОСЕДНИЕ", + "# ЗНАЧЕНИЯ БУФЕРА ВСЕГДА ОТЛИЧАЮТСЯ НЕ БОЛЕЕ ЧЕМ НА 1.", + "# Средний уровень за период точен; дробная точность возникает", + "# из чередования соседних кодов во времени.", + "# Ограничение: цель не должна меняться быстрее 1 за шаг.", + "# При max|Δu|>1 форма искажается — увеличьте N или уменьшите M.", + "# Для синуса полной амплитуды нужно m=N*O >= ~802 при B=8.", + "#", + "# ЛИМИТ ОЗУ: буфер = N*O сэмплов по 4 байта, не больше 64 КБ", + "# (16384 сэмплов). Например N=4096 и O=64 дадут 1 МБ и будут", + "# отклонены с ERR — уменьшите N или O.", + "#", + "# ПРЕДЕЛ ЧАСТОТЫ: f_вых = f_пио/(2*N*O), а f_пио <= 125 МГц.", + "# Значит f_вых_max = 125e6/(2*N*O) — при N=256, O=4 это ~61 кГц.", + "# Если запросить выше, будет выведено '! ВНИМАНИЕ' с подсказкой,", + "# какие N/O взять. Команда I показывает f_min и f_max.", + "#", + "# СПРАВКА", + "# HELP | ? — этот текст", + "#", + "# ─── СХЕМА (синус: нужен ФНЧ; RC как в вариантах 1 и 2) ───", + "# GP15 --[R1]--+--[R2]--+--> во вход схемы", + "# | |", + "# [C1] [C2] R1=R2=1к, C1=C2=47н", + "# | | (f_c ~ 3.4 кГц)", + "# GND GND", + "# При B>1 младшие биты — на GP8..GP14; без лестницы R-2R они", + "# практически не влияют (осциллограф на GP15 покажет 'огрубление').", + "# ADC0 — до 3.3 В (делитель!). Постоянная составляющая = Uпит/2.", + "# ─────────────────────────────────────────────", + ] + for ln in lines: + sys.stdout.write(ln + "\n") + + +def main(): + # Сначала сообщаем о старте — до создания периферии. Если что-то из + # железа не поднимется, мы это увидим (раньше первый print стоял + # ПОСЛЕ конструкторов, и при ошибке не выводилось ничего). + print("# DDS на PIO (RP2040 / CircuitPython).") + print("# Подробный хелп: HELP или ?") + + dds = None + square = None + try: + dds = PioDDS(bits=8, oversample=4, n=256, freq=1000) + dds.set_frequency(1000) + print("# " + dds.info()) + square = SquarePWM() + square.set_frequency(1000) + square.set_duty(128) + print(" (режим Q: PWM на GP15; режим S: PIO на GP8..GP15)") + except Exception as e: + # показываем причину, а не молча завершаемся + print("! ОШИБКА инициализации:", repr(e)) + if dds is not None: + try: + dds.deinit() + except Exception: + pass + if square is not None: + try: + square.deinit() + except Exception: + pass + return + + mode = "Q" + saved_square_hz = 1000 # частота режима Q, сохраняется при W S + + try: + while True: + line = sys.stdin.readline() + if not line: + # EOF (порт закрыт) — иначе бесконечный холостой цикл + break + 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) + if dds.needs_warning(): + print("OK F %d (синус)" % dds.freq) + print("! ВНИМАНИЕ: частота недостижима — %s" + % dds.suggest_reduction()) + print("! Сейчас реально: %.0f Гц" % dds.f_out_max()) + else: + print("OK F %d (синус)" % dds.freq) + else: + square.set_frequency(hz) + print("OK F %d (прямоугольник)" % hz) + + elif cmd == "SF" and len(parts) > 1: + dds.set_frequency(int(float(parts[1]))) + if dds.needs_warning(): + print("OK SF %d" % dds.freq) + print("! ВНИМАНИЕ: частота недостижима — %s" + % dds.suggest_reduction()) + print("! Сейчас реально: %.0f Гц" % dds.f_out_max()) + else: + print("OK SF %d" % dds.freq) + + 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": + # запомнить частоту PWM, чтобы не потерять её + # при возврате в режим Q (сценарий F 500 -> W S -> W Q) + saved_square_hz = square.pwm.frequency + # погасить выход перед освобождением пинов, + # затем отдать GPIO блоку PIO + square.off() + square.deinit() + dds.ensure_sm() + dds.set_frequency(dds.freq) + mode = "S" + print("OK W S (PIO: %s)" % dds.info()) + else: + # GPIO от PIO к PWM (обратный перехват) + dds.release_sm() + square = SquarePWM() + # вернуть ту частоту, что была в режиме Q + 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)) + mode = "Q" if want == "S" else "S" + + elif cmd == "D" and len(parts) > 1: + if mode == "Q": + 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) + w = dds.slew_warning() + if w: + print("! ВНИМАНИЕ:", w) + + 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 (применится в режиме S)" % 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) + if not dds._slew_ok: + print("! ВНИМАНИЕ: max|Δu|=%.2f > 1 — форма " + "исказится. Увеличьте N (сейчас %d) " + "или уменьшите амплитуду M." + % (dds._slew_max, dds.n)) + except Exception as e: + print("ERR FN:", repr(e)) + else: + print("ERR FN: неизвестная форма. Доступны: %s" + % ", ".join(sorted(WAVES))) + + elif cmd == "FP" and len(parts) > 1: + # FP <выражение от t> — своя функция, например: + # FP 0.3*sin(2*pi*t)+0.5*sin(4*pi*t) + expr = line.strip()[len("FP"):].strip() + if expr: + err = _expr_ok(expr) + if err: + print("ERR FP: %s" % err) + else: + try: + f = _make_expr_wave(expr) + dds.set_function(f, name="FP:" + expr) + print("OK FP %s" % expr) + if not dds._slew_ok: + print("! ВНИМАНИЕ: max|Δu|=%.2f > 1 — форма " + "исказится. Увеличьте N или уменьшите M." + % dds._slew_max) + 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: + try: + dds.set_cpc(int(parts[1])) + dds.set_frequency(dds.freq) # пересчитать f_pio + print("OK CLC %d (f_out=%.1f Гц)" % (dds.cpc, dds.freq)) + except Exception as e: + print("ERR CLC:", repr(e)) + + elif cmd == "CAL" and len(parts) > 1: + # CAL <измеренная_Гц> — автокалибровка по осциллографу + try: + was, now = dds.calibrate(float(parts[1])) + print("OK CAL: CPC %d -> %d (f_out=%.1f Гц)" + % (was, now, 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() + square.deinit() + if trig is not None: + try: + trig.deinit() + except Exception: + pass + print("stopped") + + +# ─────────────── точка входа ─────────────── +# +# На плате (файл code.py) запускаем генератор всегда. Стандартная проверка +# `if __name__ == "__main__":` ненадёжна в CircuitPython: имя модуля при +# автозапуске не всегда равно "__main__", и тогда скрипт «стартует, ничего +# не выводит и завершается». Поэтому запускаем безусловно, но с двумя +# исключениями: +# 1) файл импортируют как модуль для опытов в REPL — отключить +# автозапуск можно, поставив _START_GENERATOR = False; +# 2) ошибки печатаем, а не молчим. + +_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 668024a..33f0b6e 100644 --- a/task.md +++ b/task.md @@ -189,7 +189,7 @@ K(f) = 1 / √(1 + (f/f_c)²), f_c = 1/(2π·R·C) ### Задание A1 (0,5 ч). Прошивка генератора и первый запуск -В папке `stub/` три варианта генератора — **выберите один**: +В папке `stub/` несколько вариантов генератора — **выберите один**: | Файл | Платформа | Как формируется синус | |---|---|---| @@ -198,6 +198,7 @@ K(f) = 1 / √(1 + (f/f_c)²), f_c = 1/(2π·R·C) | `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 кГц | +| `06-dds-pio-zero-rp2040.py` | CircuitPython | **PIO + DMA**, все 8 линий GP8..GP15 — выходы, неиспользуемые биты = 0 | **Рекомендуется вариант на PIO** — точная частота, CPU свободен. Вариант 3 (`04-dds-pio-rp2040.py`) проверен на стенде: логическим @@ -209,6 +210,12 @@ K(f) = 1 / √(1 + (f/f_c)²), f_c = 1/(2π·R·C) **в коде** (перекомпилировать после правки) — в C++ нет разбора формул на лету, как в CircuitPython-версии. +`06-dds-pio-zero-rp2040.py` — тот же вариант 3, но PIO всегда управляет +всеми восемью линиями GP8..GP15: при `BITS < 8` неиспользуемые младшие +биты **выведены в 0** (низкий уровень), а не остаются свободными входами. +Берите его, если на макете видны наводки/«плавающие» уровни на младших +пинах гребёнки R-2R. + 1. Залейте файл на Pico (CircuitPython: переименовать в `code.py`). 2. Откройте консоль, введите `HELP` — увидите список команд. 3. Введите `I` — генератор покажет текущие настройки, включая