2026-10-27 20:23:07 +04:00
|
|
|
|
# DDS-генератор на PIO (RP2040) — CircuitPython
|
|
|
|
|
|
#
|
|
|
|
|
|
# Вариант 3 генератора для лабораторных работ. Совместим по системе команд
|
|
|
|
|
|
# с вариантами 1 (.ino, Arduino) и 2 (01-...-circuitpython.py), но сигнал
|
|
|
|
|
|
# для режима «синус» формирует аппаратная PIO-машина RP2040, данные в неё
|
|
|
|
|
|
# подаются через DMA, а АЦП читается библиотекой analogbufio.
|
|
|
|
|
|
#
|
2026-11-02 13:46:40 +04:00
|
|
|
|
# ПРОВЕРЕНО НА СТЕНДЕ 2026-10-28 (преподаватель): цифровой выход PIO
|
|
|
|
|
|
# снят логическим анализатором — форма синуса верная. Лестница R-2R при
|
|
|
|
|
|
# этой проверке НЕ запаивалась, поэтому аналоговая часть (размах после
|
|
|
|
|
|
# внешнего ФНЧ, реальная разрядность по постоянному уровню) ещё не
|
|
|
|
|
|
# измерялась.
|
|
|
|
|
|
#
|
|
|
|
|
|
# AI-сгенерировано, 2026-10-27; проверка цифровой части — 2026-10-28.
|
2026-10-27 20:23:07 +04:00
|
|
|
|
#
|
|
|
|
|
|
# Требует библиотеку: 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, BITS ; вывести BITS младших бит слова в порт
|
|
|
|
|
|
#
|
|
|
|
|
|
# То есть 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. Данные ВЫРАВНЕНЫ ПО СТАРШЕМУ биту порта:
|
|
|
|
|
|
# старший бит значения всегда на 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. Данные выровнены ПО СТАРШЕМУ
|
|
|
|
|
|
# биту: старший бит значения всегда на GP15, диапазон
|
|
|
|
|
|
# GP(16-B)..GP15; пины ниже не управляются.
|
|
|
|
|
|
# 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 <n>
|
|
|
|
|
|
# или автоматически: 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-программа. BITS подставляется при сборке: у PIO число бит в out —
|
|
|
|
|
|
# это непосредственный операнд инструкции, поэтому при смене BITS
|
|
|
|
|
|
# программу собираем заново. Цикл организует автоматический wrap.
|
|
|
|
|
|
#
|
|
|
|
|
|
# auto_pull=False => OSR заполняется ТОЛЬКО командой pull, поэтому
|
|
|
|
|
|
# одно слово FIFO = ровно один вывод out pins,B. Остаток бит слова
|
|
|
|
|
|
# отбрасывается при следующем pull (шунтировать его не нужно).
|
|
|
|
|
|
PROG_TEMPLATE = """
|
|
|
|
|
|
.program dds
|
|
|
|
|
|
pull block
|
|
|
|
|
|
out pins, {bits}
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
out[idx] = v
|
|
|
|
|
|
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) при этом не управляются.
|
|
|
|
|
|
#
|
|
|
|
|
|
# B=8 -> данные GP8 ... GP15 (вся гребёнка)
|
|
|
|
|
|
# B=4 -> данные GP12 ... GP15 (GP8..GP11 не трогаются)
|
|
|
|
|
|
# B=3 -> данные GP13 ... GP15 (GP8..GP12 не трогаются)
|
|
|
|
|
|
# B=1 -> данные GP15
|
|
|
|
|
|
first_num = 16 - self.bits
|
|
|
|
|
|
first_pin = getattr(board, "GP%d" % first_num)
|
|
|
|
|
|
|
|
|
|
|
|
# Защита от наложения портов: PIO занимает GP(first_num)..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 при BITS=%d. "
|
|
|
|
|
|
"Уменьшите BITS или перенесите этот пин."
|
|
|
|
|
|
% (name, pin, first_num, self.bits)
|
|
|
|
|
|
)
|
|
|
|
|
|
# Диагностика: у пина 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.format(bits=self.bits)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Собираем аргументы конструктора: настройки из .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)
|
|
|
|
|
|
kwargs.setdefault("out_pin_count", self.bits)
|
|
|
|
|
|
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)
|
|
|
|
|
|
kwargs.setdefault("initial_out_pin_direction", (1 << self.bits) - 1)
|
|
|
|
|
|
|
|
|
|
|
|
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() # меняется out_pin_count
|
|
|
|
|
|
|
|
|
|
|
|
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>",
|
|
|
|
|
|
"# логарифмический свип 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))
|