ai: circuitpython — устойчивый цикл: ошибки печатаются в консоль (не в REPL), эхо принятой команды ->, свип до f1 <= f_s/6
This commit is contained in:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user