ai: circuitpython — устойчивый цикл: ошибки печатаются в консоль (не в REPL), эхо принятой команды ->, свип до f1 <= f_s/6

This commit is contained in:
mss
2026-10-26 16:28:25 +04:00
parent 7f8b52aa2c
commit f1db2eb6c1
+30 -8
View File
@@ -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,12 +251,15 @@ 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:
try:
phase = dds_tick(phase)
# свип: точка готова, когда прошло dwell
@@ -273,15 +281,29 @@ def main():
# командная строка (неблокирующий опрос 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__":
main()