Files
eumirts5-dds/stub/01-dds-generator-rp2040-circuitpython.py
T
mss 7f8b52aa2c ai: lab5 — скетч без зависимости от pico-sdk в mbed-ядре (mbed::PwmOut + mbed::Ticker) + альтернатива на CircuitPython (01-dds-generator-rp2040-circuitpython.py)
- mbed-ядро: только mbed API, hardware/pwm.h и hardware/timer.h не нужны
- CircuitPython: один цикл DDS + опрос USB-консоли (без threading), f_s меряется на старте
- прямоугольник < 20 Гц — программный (тот же ШИМ)
2026-10-26 15:43:35 +04:00

288 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DDS-генератор сигналов для лабораторных работ — версия CircuitPython
# Платформа: Raspberry Pi Pico / RP2040 с прошивкой CircuitPython
# (https://circuitpython.org/downloads — выбрать "Raspberry Pi Pico")
#
# ЧЕРНОВИК (AI-сгенерировано, 2026-10-26). Требует проверки преподавателем
# на реальном стенде: номиналы RC-фильтра, диапазоны частот, точность ADC.
#
# Чем отличается от Arduino-версии (stub/01-dds-generator-rp2040.ino):
# - CircuitPython — интерпретатор: «настоящего» таймерного ISR на 62,5 кГц
# нет, поэтому частота выборок f_s ниже и зависит от скорости Python.
# Архитектура — без потоков (threading есть не на всех портах):
# один цикл одновременно обновляет выборку синуса и опрашивает USB-консоль
# (supervisor.runtime.serial_bytes_available). f_s измеряется на старте
# (обычно ~10–40 кГц на RP2040) и печатается; под неё и подбираются
# номиналы RC-фильтра (см. README);
# - прямоугольник от 20 Гц — аппаратный ШИМ pwmio (точная частота и
# скважность, как в Arduino); ниже 20 Гц — «программный» прямоугольник
# в основном цикле;
# - команды те же: F / W / D / M / A / SWEEP / STOP (через USB-консоль,
# терминал: 115200).
#
# Протокол команд (ввод строкой в терминале, Enter = конец команды):
# F <Гц> — установить частоту (1..200000)
# W Q | W S — режим: Q — прямоугольник, S — синус
# D <0..255> — скважность прямоугольника (128 = 50 %)
# M <0..127> — амплитуда синуса (127 = максимум)
# A — однократно измерить размах на ADC0 -> "A <мВ>"
# SWEEP <f0> <f1> <n> <dwell_ms>
# — логарифмический свип; "P <Гц> <мВ>" + импульс на GP14
# STOP — прервать свип
#
# ─── СХЕМА СИНУСА (обязательна: ШИМ даёт меандр, а не синус) ───
# GP15 ──[R1]──┬──[R2]──┬──────► OUT (вход DUT)
# │ │
# [C1] [C2]
# │ │
# GND GND
# R1 = R2 = 1 кОм, C1 = C2 = 47 нФ -> f_c ≈ 3,4 кГц (для f_s >= 20 кГц);
# если фактическая f_s ниже — увеличить C (см. README: f_c ~= f_s/6).
# Входам DUT выше 3,3 В нужен делитель перед ADC0 (GP26).
import array
import math
import sys
import time
import analogio
import board
import digitalio
import pwmio
import supervisor
PIN_OUT = board.GP15 # выход сигнала (синус — через RC-фильтр)
PIN_TRIG = board.GP14 # синхроимпульс (для осциллографа)
PIN_ADC = board.GP26 # ADC0 = A0 — вход измерения (через делитель!)
CARRIER_HZ = 125000 # несущая ШИМ — много выше f_s, биений нет
PWM_RANGE = 65535 # duty_cycle pwmio — 16 бит
AMP_MAX = 127 # максимальная амплитуда синуса
SW_SQ_MIN_HZ = 20 # ниже — «программный» прямоугольник в цикле
FS_INIT = 20000 # оценка f_s до калибровки
# ─────────────────── состояние генератора ─────────────────────
state = {
"freq": 1000, # Гц
"duty": 128, # 0..255
"amp": AMP_MAX, # 0..127
"mode": "Q", # 'Q' — прямоугольник, 'S' — синус
# поля свипа (активен, если "sweep": True)
"sweep": False,
"sweep_f0": 0.0,
"sweep_ratio": 1.0,
"sweep_idx": 0,
"sweep_n": 0,
"sweep_dwell": 0.05,
"sweep_t0": 0.0,
}
fs_eff = FS_INIT # фактическая частота выборок (из калибровки)
# Таблица синуса 256 точек, -127..127
LUT = array.array("b", [
int(127 * math.sin(2 * math.pi * i / 256))
for i in range(256)
])
# ─────────────────────── периферия ─────────────────────────────
pwm = pwmio.PWMOut(PIN_OUT, frequency=CARRIER_HZ, duty_cycle=0,
variable_frequency=True)
trig = digitalio.DigitalInOut(PIN_TRIG)
trig.direction = digitalio.Direction.OUTPUT
adc = analogio.AnalogIn(PIN_ADC)
# ─────────────────────── слой ШИМ ─────────────────────────────
def pwm_config(freq_hz):
"""Частота несущей (прямоугольник) — аппаратный pwmio."""
pwm.frequency = max(1, int(freq_hz))
def pwm_duty(duty):
"""Скважность 0..255 -> duty_cycle 0..65535."""
if duty < 0:
duty = 0
if duty > 255:
duty = 255
pwm.duty_cycle = duty * PWM_RANGE // 255
def pwm_set_square(freq_hz, duty):
pwm_config(freq_hz)
pwm_duty(duty)
# ─────────────────── DDS: одна выборка ─────────────────────────
def dds_tick(phase):
"""Шаг аккумулятора фазы + запись скважности. Возвращает новую фазу.
В режиме SINE — выборка синуса; в режиме Q с f < SW_SQ_MIN_HZ —
«программный» прямоугольник. В режиме Q с f >= SW_SQ_MIN_HZ ШИМ работает
аппаратно, функция только возвращает фазу.
"""
step = int(state["freq"] * (1 << 32) / fs_eff)
phase = (phase + step) & 0xFFFFFFFF
if state["mode"] == "S":
idx = phase >> 24
s = LUT[idx] * state["amp"] // 127
pwm_duty(128 + s)
elif state["freq"] < SW_SQ_MIN_HZ:
pwm_duty(255 if (phase >> 24) < state["duty"] else 0)
return phase
# ───────────── измерение размаха (ADC), не останавливая DDS ────
def measure_amplitude_mv(phase, window_s=0.05):
"""Размах (пик-пик) в мВ за окно; DDS при этом продолжает тикать."""
vmin = 65535
vmax = 0
t0 = time.monotonic()
while time.monotonic() - t0 < window_s:
phase = dds_tick(phase)
v = adc.value
if v < vmin:
vmin = v
if v > vmax:
vmax = v
# ADC 12 бит в 16-битной шкале CircuitPython, Vref = 3,3 В
return (vmax - vmin) * 3300 // 65535, phase
# ─────────────────────────── команды ───────────────────────────
def cmd_f(a):
hz = min(200000, max(1, int(float(a))))
state["freq"] = hz
if state["mode"] == "Q":
pwm_set_square(hz, state["duty"])
print("OK F", hz)
def cmd_w(a):
state["mode"] = "S" if a.upper().startswith("S") else "Q"
if state["mode"] == "Q":
pwm_set_square(state["freq"], state["duty"])
else:
pwm_config(CARRIER_HZ) # выборки пишет dds_tick
print("OK W", state["mode"])
def cmd_d(a):
state["duty"] = min(255, max(0, int(a)))
if state["mode"] == "Q":
pwm_set_square(state["freq"], state["duty"])
print("OK D", state["duty"])
def cmd_m(a):
state["amp"] = min(AMP_MAX, max(0, int(a)))
print("OK M", state["amp"])
def cmd_sweep_parts(a):
parts = a.split()
if len(parts) != 4:
print("ERR SWEEP <f0> <f1> <n> <dwell_ms>")
return
f0 = max(1.0, float(parts[0]))
f1 = float(parts[1])
if f1 <= f0:
f1 = f0 * 10
n = max(2, int(parts[2]))
dwell = max(0.01, int(parts[3]) / 1000.0)
ratio = (f1 / f0) ** (1.0 / (n - 1))
state.update(sweep=True, sweep_f0=f0, sweep_ratio=ratio,
sweep_idx=0, sweep_n=n, sweep_dwell=dwell, sweep_t0=0.0)
cmd_f(str(int(round(f0))))
print("OK SWEEP")
def trigger_pulse():
trig.value = True
time.sleep(0.00005)
trig.value = False
def handle(line, phase):
"""Выполнить команду. Возвращает (ok, phase)."""
line = line.strip()
if not line:
return False, phase
parts = line.split(maxsplit=1)
cmd = parts[0].upper()
a = parts[1] if len(parts) > 1 else ""
if cmd == "F":
cmd_f(a)
elif cmd == "W":
cmd_w(a)
elif cmd == "D":
cmd_d(a)
elif cmd == "M":
cmd_m(a)
elif cmd == "A":
mv, phase = measure_amplitude_mv(phase)
print("A", mv)
elif cmd == "SWEEP":
cmd_sweep_parts(a)
elif cmd == "STOP":
state["sweep"] = False
print("OK STOP")
else:
print("ERR unknown:", line)
return True, phase
# ─────────────────── калибровка f_s (1 с) ──────────────────────
def calibrate_fs(phase):
"""Прогон DDS в течение 1 с и подсчёт фактической f_s."""
t0 = time.monotonic()
n = 0
while time.monotonic() - t0 < 1.0:
phase = dds_tick(phase)
n += 1
return n, phase
# ─────────────────────────── main ─────────────────────────────
def main():
global fs_eff
print("# DDS generator ready (CircuitPython). help: F/W/D/A/SWEEP/STOP")
phase = 0
fs_eff, phase = calibrate_fs(phase) # в режиме 'Q', f=1000 Гц,
# аппаратный ШИМ: тики холостые
print("# sample rate:", fs_eff, "Hz")
buf = ""
while True:
phase = dds_tick(phase)
# свип: точка готова, когда прошло dwell
if state["sweep"]:
now = time.monotonic()
if state["sweep_t0"] == 0.0 or now - state["sweep_t0"] >= state["sweep_dwell"]:
mv, phase = measure_amplitude_mv(phase)
print("P", int(round(state["freq"])), mv)
state["sweep_idx"] += 1
if state["sweep_idx"] >= state["sweep_n"]:
state["sweep"] = False
print("DONE")
cmd_f(str(int(round(state["sweep_f0"]))))
else:
f = state["sweep_f0"] * (state["sweep_ratio"] ** state["sweep_idx"])
cmd_f(str(int(round(f))))
trigger_pulse()
state["sweep_t0"] = time.monotonic()
# командная строка (неблокирующий опрос USB-консоли)
while supervisor.runtime.serial_bytes_available:
c = sys.stdin.read(1)
if not c:
break
if c == "\n" or c == "\r":
_, phase = handle(buf, phase)
buf = ""
elif c != "\x00":
buf += c
if __name__ == "__main__":
main()