From f1db2eb6c188b43ba052b32097edde9a345fa23e Mon Sep 17 00:00:00 2001 From: mss Date: Mon, 26 Oct 2026 16:28:25 +0400 Subject: [PATCH] =?UTF-8?q?ai:=20circuitpython=20=E2=80=94=20=D1=83=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=B9=D1=87=D0=B8=D0=B2=D1=8B=D0=B9=20=D1=86=D0=B8?= =?UTF-8?q?=D0=BA=D0=BB:=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=87=D0=B0=D1=82=D0=B0=D1=8E=D1=82=D1=81=D1=8F=20=D0=B2?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D1=81=D0=BE=D0=BB=D1=8C=20(=D0=BD=D0=B5?= =?UTF-8?q?=20=D0=B2=20REPL),=20=D1=8D=D1=85=D0=BE=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D0=BE=D0=B9=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD?= =?UTF-8?q?=D0=B4=D1=8B=20->,=20=D1=81=D0=B2=D0=B8=D0=BF=20=D0=B4=D0=BE=20?= =?UTF-8?q?f1=20<=3D=20f=5Fs/6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stub/01-dds-generator-rp2040-circuitpython.py | 92 ++++++++++++------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/stub/01-dds-generator-rp2040-circuitpython.py b/stub/01-dds-generator-rp2040-circuitpython.py index b4078e7..2083556 100644 --- a/stub/01-dds-generator-rp2040-circuitpython.py +++ b/stub/01-dds-generator-rp2040-circuitpython.py @@ -11,14 +11,17 @@ # Архитектура — без потоков (threading есть не на всех портах): # один цикл одновременно обновляет выборку синуса и опрашивает USB-консоль # (supervisor.runtime.serial_bytes_available). f_s измеряется на старте -# (обычно ~10–40 кГц на RP2040) и печатается; под неё и подбираются -# номиналы RC-фильтра (см. README); +# (обычно 3–40 кГц на RP2040) и печатается; свип держите до f1 <= f_s/6. # - прямоугольник от 20 Гц — аппаратный ШИМ pwmio (точная частота и # скважность, как в Arduino); ниже 20 Гц — «программный» прямоугольник # в основном цикле; # - команды те же: F / W / D / M / A / SWEEP / STOP (через USB-консоль, # терминал: 115200). # +# УСТОЙЧИВОСТЬ: ошибки не роняют скетч в REPL — они печатаются в консоль +# (строки "! ..."), а генератор продолжает работать. Каждая принятая строка +# логируется как "-> ..." — видно, что реально пришло с терминала. +# # Протокол команд (ввод строкой в терминале, Enter = конец команды): # F <Гц> — установить частоту (1..200000) # W Q | W S — режим: Q — прямоугольник, S — синус @@ -52,13 +55,14 @@ import supervisor PIN_OUT = board.GP15 # выход сигнала (синус — через RC-фильтр) PIN_TRIG = board.GP14 # синхроимпульс (для осциллографа) -PIN_ADC = board.GP26 # ADC0 = A0 — вход измерения (через делитель!) +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 до калибровки +STEP_2P32 = 1 << 32 # аккумулятор фазы 32 бит # ─────────────────── состояние генератора ───────────────────── state = { @@ -119,13 +123,14 @@ def dds_tick(phase): «программный» прямоугольник. В режиме Q с f >= SW_SQ_MIN_HZ ШИМ работает аппаратно, функция только возвращает фазу. """ - step = int(state["freq"] * (1 << 32) / fs_eff) + f = state["freq"] + step = f * STEP_2P32 // 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: + elif f < SW_SQ_MIN_HZ: pwm_duty(255 if (phase >> 24) < state["duty"] else 0) return phase @@ -202,7 +207,7 @@ def trigger_pulse(): def handle(line, phase): - """Выполнить команду. Возвращает (ok, phase).""" + """Выполнить команду. Возвращает (ok, phase). Любые ошибки — внутрь.""" line = line.strip() if not line: return False, phase @@ -246,41 +251,58 @@ 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 Гц, - # аппаратный ШИМ: тики холостые + try: + fs_eff, phase = calibrate_fs(phase) + except Exception as e: + print("! calibrate error:", e) print("# sample rate:", fs_eff, "Hz") buf = "" while True: - phase = dds_tick(phase) + try: + 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() + # свип: точка готова, когда прошло 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 + # командная строка (неблокирующий опрос USB-консоли) + while supervisor.runtime.serial_bytes_available: + try: + c = sys.stdin.read(1) + except Exception as e: + print("! serial read error:", e) + break + if not c: + break + if c == "\n" or c == "\r": + if buf.strip(): + print("->", buf.strip()) # эхо принятой строки + try: + _, phase = handle(buf, phase) + except Exception as e: + print("! command error:", e) + buf = "" + elif c != "\x00": + buf += c + + except Exception as e: + # НЕ умираем в REPL: печатаем и продолжаем + print("! loop error:", e) + time.sleep(0.05) if __name__ == "__main__":