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
+57 -35
View File
@@ -11,14 +11,17 @@
# Архитектура — без потоков (threading есть не на всех портах): # Архитектура — без потоков (threading есть не на всех портах):
# один цикл одновременно обновляет выборку синуса и опрашивает USB-консоль # один цикл одновременно обновляет выборку синуса и опрашивает USB-консоль
# (supervisor.runtime.serial_bytes_available). f_s измеряется на старте # (supervisor.runtime.serial_bytes_available). f_s измеряется на старте
# (обычно ~10–40 кГц на RP2040) и печатается; под неё и подбираются # (обычно 3–40 кГц на RP2040) и печатается; свип держите до f1 <= f_s/6.
# номиналы RC-фильтра (см. README);
# - прямоугольник от 20 Гц — аппаратный ШИМ pwmio (точная частота и # - прямоугольник от 20 Гц — аппаратный ШИМ pwmio (точная частота и
# скважность, как в Arduino); ниже 20 Гц — «программный» прямоугольник # скважность, как в Arduino); ниже 20 Гц — «программный» прямоугольник
# в основном цикле; # в основном цикле;
# - команды те же: F / W / D / M / A / SWEEP / STOP (через USB-консоль, # - команды те же: F / W / D / M / A / SWEEP / STOP (через USB-консоль,
# терминал: 115200). # терминал: 115200).
# #
# УСТОЙЧИВОСТЬ: ошибки не роняют скетч в REPL — они печатаются в консоль
# (строки "! ..."), а генератор продолжает работать. Каждая принятая строка
# логируется как "-> ..." — видно, что реально пришло с терминала.
#
# Протокол команд (ввод строкой в терминале, Enter = конец команды): # Протокол команд (ввод строкой в терминале, Enter = конец команды):
# F <Гц> — установить частоту (1..200000) # F <Гц> — установить частоту (1..200000)
# W Q | W S — режим: Q — прямоугольник, S — синус # W Q | W S — режим: Q — прямоугольник, S — синус
@@ -52,13 +55,14 @@ import supervisor
PIN_OUT = board.GP15 # выход сигнала (синус — через RC-фильтр) PIN_OUT = board.GP15 # выход сигнала (синус — через RC-фильтр)
PIN_TRIG = board.GP14 # синхроимпульс (для осциллографа) PIN_TRIG = board.GP14 # синхроимпульс (для осциллографа)
PIN_ADC = board.GP26 # ADC0 = A0 — вход измерения (через делитель!) PIN_ADC = board.GP26 # ADC0 = A0 — вход измерения (через делителем)
CARRIER_HZ = 125000 # несущая ШИМ — много выше f_s, биений нет CARRIER_HZ = 125000 # несущая ШИМ — много выше f_s, биений нет
PWM_RANGE = 65535 # duty_cycle pwmio — 16 бит PWM_RANGE = 65535 # duty_cycle pwmio — 16 бит
AMP_MAX = 127 # максимальная амплитуда синуса AMP_MAX = 127 # максимальная амплитуда синуса
SW_SQ_MIN_HZ = 20 # ниже — «программный» прямоугольник в цикле SW_SQ_MIN_HZ = 20 # ниже — «программный» прямоугольник в цикле
FS_INIT = 20000 # оценка f_s до калибровки FS_INIT = 20000 # оценка f_s до калибровки
STEP_2P32 = 1 << 32 # аккумулятор фазы 32 бит
# ─────────────────── состояние генератора ───────────────────── # ─────────────────── состояние генератора ─────────────────────
state = { state = {
@@ -119,13 +123,14 @@ def dds_tick(phase):
«программный» прямоугольник. В режиме Q с f >= SW_SQ_MIN_HZ ШИМ работает «программный» прямоугольник. В режиме 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 phase = (phase + step) & 0xFFFFFFFF
if state["mode"] == "S": if state["mode"] == "S":
idx = phase >> 24 idx = phase >> 24
s = LUT[idx] * state["amp"] // 127 s = LUT[idx] * state["amp"] // 127
pwm_duty(128 + s) 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) pwm_duty(255 if (phase >> 24) < state["duty"] else 0)
return phase return phase
@@ -202,7 +207,7 @@ def trigger_pulse():
def handle(line, phase): def handle(line, phase):
"""Выполнить команду. Возвращает (ok, phase).""" """Выполнить команду. Возвращает (ok, phase). Любые ошибки — внутрь."""
line = line.strip() line = line.strip()
if not line: if not line:
return False, phase return False, phase
@@ -246,41 +251,58 @@ def main():
global fs_eff global fs_eff
print("# DDS generator ready (CircuitPython). help: F/W/D/A/SWEEP/STOP") print("# DDS generator ready (CircuitPython). help: F/W/D/A/SWEEP/STOP")
phase = 0 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") print("# sample rate:", fs_eff, "Hz")
buf = "" buf = ""
while True: while True:
phase = dds_tick(phase) try:
phase = dds_tick(phase)
# свип: точка готова, когда прошло dwell # свип: точка готова, когда прошло dwell
if state["sweep"]: if state["sweep"]:
now = time.monotonic() now = time.monotonic()
if state["sweep_t0"] == 0.0 or now - state["sweep_t0"] >= state["sweep_dwell"]: if state["sweep_t0"] == 0.0 or now - state["sweep_t0"] >= state["sweep_dwell"]:
mv, phase = measure_amplitude_mv(phase) mv, phase = measure_amplitude_mv(phase)
print("P", int(round(state["freq"])), mv) print("P", int(round(state["freq"])), mv)
state["sweep_idx"] += 1 state["sweep_idx"] += 1
if state["sweep_idx"] >= state["sweep_n"]: if state["sweep_idx"] >= state["sweep_n"]:
state["sweep"] = False state["sweep"] = False
print("DONE") print("DONE")
cmd_f(str(int(round(state["sweep_f0"])))) cmd_f(str(int(round(state["sweep_f0"]))))
else: else:
f = state["sweep_f0"] * (state["sweep_ratio"] ** state["sweep_idx"]) f = state["sweep_f0"] * (state["sweep_ratio"] ** state["sweep_idx"])
cmd_f(str(int(round(f)))) cmd_f(str(int(round(f))))
trigger_pulse() trigger_pulse()
state["sweep_t0"] = time.monotonic() state["sweep_t0"] = time.monotonic()
# командная строка (неблокирующий опрос USB-консоли) # командная строка (неблокирующий опрос USB-консоли)
while supervisor.runtime.serial_bytes_available: while supervisor.runtime.serial_bytes_available:
c = sys.stdin.read(1) try:
if not c: c = sys.stdin.read(1)
break except Exception as e:
if c == "\n" or c == "\r": print("! serial read error:", e)
_, phase = handle(buf, phase) break
buf = "" if not c:
elif c != "\x00": break
buf += c 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__": if __name__ == "__main__":