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 Гц — программный (тот же ШИМ)
This commit is contained in:
@@ -19,7 +19,10 @@ labs/lab5-peredatochnaya-funkciya-na-ou/
|
|||||||
├── README.md ← этот файл
|
├── README.md ← этот файл
|
||||||
├── print/ ← брошюры DOCX (rpd methodical)
|
├── print/ ← брошюры DOCX (rpd methodical)
|
||||||
└── stub/
|
└── stub/
|
||||||
├── 01-dds-generator-rp2040.ino ← генератор-свип (Arduino, RP2040)
|
├── 01-dds-generator-rp2040.ino ← генератор-свип (Arduino RP2040;
|
||||||
|
│ оба ядра: earlephilhower и mbed)
|
||||||
|
├── 01-dds-generator-rp2040-circuitpython.py
|
||||||
|
│ ← то же на CircuitPython (code.py)
|
||||||
├── 02-izmerenie-achh.py ← автоснятие АЧХ по Serial
|
├── 02-izmerenie-achh.py ← автоснятие АЧХ по Serial
|
||||||
└── 03-blank-otchyota.md ← бланк отчёта студента
|
└── 03-blank-otchyota.md ← бланк отчёта студента
|
||||||
```
|
```
|
||||||
@@ -90,7 +93,8 @@ GP15 (ШИМ 62,5 кГц) ──[R1 1к]──┬──[R2 1к]──┬──
|
|||||||
|
|
||||||
1. `task.md` + `stub/03-blank-otchyota.md` — студенту.
|
1. `task.md` + `stub/03-blank-otchyota.md` — студенту.
|
||||||
2. `stub/01-dds-generator-rp2040.ino` — прошить в RP2040 заранее
|
2. `stub/01-dds-generator-rp2040.ino` — прошить в RP2040 заранее
|
||||||
(или дать студенту в задании 4).
|
(или дать студенту в задании 4); альтернатива — `...-circuitpython.py`
|
||||||
|
на Pico с прошивкой CircuitPython (как `code.py`).
|
||||||
3. `stub/02-izmerenie-achh.py` — запускается на ПК студента (нужен pyserial).
|
3. `stub/02-izmerenie-achh.py` — запускается на ПК студента (нужен pyserial).
|
||||||
4. `method.md` — только преподавателю.
|
4. `method.md` — только преподавателю.
|
||||||
5. Брошюры: `uv run rpd methodical lab5-peredatochnaya-funkciya-na-ou`.
|
5. Брошюры: `uv run rpd methodical lab5-peredatochnaya-funkciya-na-ou`.
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
# 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()
|
||||||
@@ -3,11 +3,13 @@
|
|||||||
* Платформа: RP2040 (Raspberry Pi Pico)
|
* Платформа: RP2040 (Raspberry Pi Pico)
|
||||||
*
|
*
|
||||||
* Работает в ОБОИХ Arduino-ядрах:
|
* Работает в ОБОИХ Arduino-ядрах:
|
||||||
* - earlephilhower (arduino-pico) — штатный ШИМ analogWriteFreq/analogWriteRange;
|
* - earlephilhower (arduino-pico) — штатный ШИМ analogWriteFreq/analogWriteRange
|
||||||
* - официальное ядро "Raspberry Pi Pico/RP2040" (Arduino Mbed OS) — analogWriteFreq
|
* и таймер add_repeating_timer_us (pico-sdk);
|
||||||
* в нём отсутствует, поэтому включается собственный драйвер ШИМ на регистрах
|
* - официальное ядро "Raspberry Pi Pico/RP2040" (Arduino Mbed OS) — в пользовательском
|
||||||
* pico-sdk (hardware/pwm.h). Никакой настройки не требуется — ветка выбирается
|
* коде НЕТ ни analogWriteFreq, ни pico-sdk-заголовков (hardware/pwm.h, hardware/timer.h),
|
||||||
* автоматически по макросу ARDUINO_ARCH_MBED.
|
* поэтому здесь используется только mbed API, который является частью самого ядра:
|
||||||
|
* mbed::PwmOut (ШИМ) + mbed::Ticker (тактовый генератор выборок 62,5 кГц).
|
||||||
|
* Ветка выбирается автоматически по макросу ARDUINO_ARCH_MBED.
|
||||||
*
|
*
|
||||||
* ЧЕРНОВИК (AI-сгенерировано, 2026-10-26). Требует проверки преподавателем
|
* ЧЕРНОВИК (AI-сгенерировано, 2026-10-26). Требует проверки преподавателем
|
||||||
* на реальном стенде: номиналы RC-фильтра, диапазоны частот, точность ADC.
|
* на реальном стенде: номиналы RC-фильтра, диапазоны частот, точность ADC.
|
||||||
@@ -62,17 +64,17 @@
|
|||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include "hardware/timer.h" // add_repeating_timer_us, repeating_timer_t — оба ядра
|
|
||||||
|
|
||||||
// ───────────────── выбор драйвера ШИМ ─────────────────
|
// ───────────────── выбор бэкенда ─────────────────
|
||||||
// Официальное mbed-ядро не имеет analogWriteFreq/analogWriteRange —
|
// Официальное mbed-ядро: нет analogWriteFreq, нет pico-sdk в пользовательском
|
||||||
// используем регистры pico-sdk. earlephilhower — штатный analogWrite*.
|
// коде -> только mbed API (PwmOut + Ticker). earlephilhower: analogWrite* +
|
||||||
|
// add_repeating_timer_us.
|
||||||
#if defined(ARDUINO_ARCH_MBED)
|
#if defined(ARDUINO_ARCH_MBED)
|
||||||
#define USE_CORE_PWM 0
|
#define USE_MBED_API 1
|
||||||
#include "hardware/pwm.h"
|
#include "mbed.h"
|
||||||
#include "hardware/gpio.h"
|
|
||||||
#else
|
#else
|
||||||
#define USE_CORE_PWM 1
|
#define USE_MBED_API 0
|
||||||
|
#include "hardware/timer.h" // add_repeating_timer_us, repeating_timer_t
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ─────────────────────────── пины ───────────────────────────
|
// ─────────────────────────── пины ───────────────────────────
|
||||||
@@ -110,9 +112,23 @@ static int8_t g_lut[256]; // sin: -127..127
|
|||||||
// pwmStart() — включить ШИМ на PIN_OUT (idempotent)
|
// pwmStart() — включить ШИМ на PIN_OUT (idempotent)
|
||||||
// pwmConfig(f, range) — настроить частоту несущей
|
// pwmConfig(f, range) — настроить частоту несущей
|
||||||
// pwmDuty(d) — установить скважность (0..255, масштабируется на текущий wrap)
|
// pwmDuty(d) — установить скважность (0..255, масштабируется на текущий wrap)
|
||||||
// pwmStop() — отключить ШИМ, пин -> обычный GPIO (для программного ШИМ)
|
|
||||||
|
|
||||||
#if USE_CORE_PWM
|
#if USE_MBED_API
|
||||||
|
// официальное mbed-ядро: PwmOut (16-бит размах внутри ядра)
|
||||||
|
static mbed::PwmOut g_pwm(digitalPinToPinName(PIN_OUT));
|
||||||
|
|
||||||
|
static void pwmStart() { /* PwmOut активен сразу */ }
|
||||||
|
static void pwmConfig(uint32_t freq_hz, uint32_t range) {
|
||||||
|
(void)range;
|
||||||
|
if (freq_hz < 1) freq_hz = 1;
|
||||||
|
g_pwm.period_us(1000000UL / freq_hz); // период в мкс (для 62,5 кГц = 16 мкс)
|
||||||
|
}
|
||||||
|
static void pwmDuty(uint32_t duty) {
|
||||||
|
if (duty > PWM_RANGE) duty = PWM_RANGE;
|
||||||
|
g_pwm.write((float)duty / (float)PWM_RANGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
// earlephilhower arduino-pico
|
// earlephilhower arduino-pico
|
||||||
static void pwmStart() { /* analogWrite сам включает ШИМ */ }
|
static void pwmStart() { /* analogWrite сам включает ШИМ */ }
|
||||||
static void pwmConfig(uint32_t freq_hz, uint32_t range) {
|
static void pwmConfig(uint32_t freq_hz, uint32_t range) {
|
||||||
@@ -122,68 +138,7 @@ static void pwmConfig(uint32_t freq_hz, uint32_t range) {
|
|||||||
static void pwmDuty(uint32_t duty) {
|
static void pwmDuty(uint32_t duty) {
|
||||||
analogWrite(PIN_OUT, duty);
|
analogWrite(PIN_OUT, duty);
|
||||||
}
|
}
|
||||||
static void pwmStop() {
|
#endif // USE_MBED_API
|
||||||
// digitalWrite сам переводит пин из ШИМ в GPIO
|
|
||||||
digitalWrite(PIN_OUT, LOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
|
||||||
// официальное mbed-ядро: драйвер на регистрах pico-sdk
|
|
||||||
static uint slice_num, chan_num;
|
|
||||||
static uint32_t g_wrap = PWM_RANGE;
|
|
||||||
|
|
||||||
// Точный подбор пары (wrap, clkdiv 8.4): фактическая частота несущей
|
|
||||||
// f = 125 МГц * 16 / ((wrap+1) * div8_4)
|
|
||||||
// Перебираем wrap в окне возле range (чтобы не терять разрядность ШИМ)
|
|
||||||
// и минимальной по модулю ошибке.
|
|
||||||
static void pwmConfig(uint32_t freq_hz, uint32_t range) {
|
|
||||||
const uint64_t CLK16 = 125000000ULL * 16;
|
|
||||||
uint32_t w_lo = 1, w_hi = 65535;
|
|
||||||
if (range > 1) {
|
|
||||||
w_lo = range - 1;
|
|
||||||
w_hi = (uint32_t)range + 1024;
|
|
||||||
if (w_hi > 65535) w_hi = 65535;
|
|
||||||
}
|
|
||||||
uint32_t best_wrap = g_wrap, best_div = 16;
|
|
||||||
int64_t best_err = INT64_MAX;
|
|
||||||
for (uint32_t wrap = w_lo; wrap <= w_hi; ++wrap) {
|
|
||||||
uint64_t den = (uint64_t)(wrap + 1) * freq_hz;
|
|
||||||
uint32_t d = (uint32_t)((CLK16 + den / 2) / den);
|
|
||||||
if (d < 16) d = 16; // div >= 1
|
|
||||||
if (d > 4095) d = 4095; // div <= 255,9375 (поле 8.4)
|
|
||||||
int64_t fa = (int64_t)(CLK16 / ((uint64_t)(wrap + 1) * d));
|
|
||||||
int64_t err = fa - (int64_t)freq_hz;
|
|
||||||
if (err < 0) err = -err;
|
|
||||||
if (err < best_err) { best_err = err; best_wrap = wrap; best_div = d; }
|
|
||||||
}
|
|
||||||
g_wrap = best_wrap;
|
|
||||||
pwm_set_clkdiv(slice_num, best_div / 16.0f);
|
|
||||||
pwm_set_wrap(slice_num, best_wrap);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void pwmDuty(uint32_t duty) {
|
|
||||||
// масштабируем duty (0..255) на текущий wrap
|
|
||||||
uint32_t level = (uint32_t)((uint64_t)duty * g_wrap / 255);
|
|
||||||
pwm_set_chan_level(slice_num, chan_num, level);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void pwmStart() {
|
|
||||||
slice_num = pwm_gpio_to_slice_num(PIN_OUT);
|
|
||||||
chan_num = pwm_gpio_to_channel(PIN_OUT);
|
|
||||||
gpio_set_function(PIN_OUT, GPIO_FUNC_PWM);
|
|
||||||
pwm_set_clkdiv(slice_num, 1.0f);
|
|
||||||
pwm_set_wrap(slice_num, PWM_RANGE);
|
|
||||||
pwm_set_chan_level(slice_num, chan_num, 0);
|
|
||||||
pwm_set_enabled(slice_num, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void pwmStop() {
|
|
||||||
pwm_set_enabled(slice_num, false);
|
|
||||||
gpio_set_function(PIN_OUT, GPIO_FUNC_SIO);
|
|
||||||
gpio_set_dir(PIN_OUT, GPIO_OUT);
|
|
||||||
gpio_put(PIN_OUT, LOW);
|
|
||||||
}
|
|
||||||
#endif // USE_CORE_PWM
|
|
||||||
|
|
||||||
// ─────────────────────── таблица синуса ─────────────────────
|
// ─────────────────────── таблица синуса ─────────────────────
|
||||||
static void buildLut() {
|
static void buildLut() {
|
||||||
@@ -192,8 +147,12 @@ static void buildLut() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────────── ISR: обновление ШИМ для синуса (DDS) ─────────
|
// ─────────────── тело ISR: обновление выборок ────────────────
|
||||||
static bool onSample(repeating_timer_t *) {
|
// В режиме SINE — DDS-шаг фазы и новая скважность каждые 16 мкс;
|
||||||
|
// в режиме SQUARE с f < SW_SQ_MIN_HZ — программный прямоугольник
|
||||||
|
// (тот же ШИМ, только скважность 255 или 0 по фазе);
|
||||||
|
// в режиме SQUARE с f >= SW_SQ_MIN_HZ ШИМ работает аппаратно, ISR молчит.
|
||||||
|
static void sampleIsrBody() {
|
||||||
if (g_mode == MODE_SINE) {
|
if (g_mode == MODE_SINE) {
|
||||||
// шаг фазы = f * 2^32 / Fs (64-бит, чтобы не переполнить)
|
// шаг фазы = f * 2^32 / Fs (64-бит, чтобы не переполнить)
|
||||||
uint32_t step =
|
uint32_t step =
|
||||||
@@ -204,24 +163,38 @@ static bool onSample(repeating_timer_t *) {
|
|||||||
int32_t s = ((int32_t)g_lut[idx] * g_amp) / 127;
|
int32_t s = ((int32_t)g_lut[idx] * g_amp) / 127;
|
||||||
pwmDuty((uint8_t)(128 + s));
|
pwmDuty((uint8_t)(128 + s));
|
||||||
} else if (g_freq < SW_SQ_MIN_HZ) {
|
} else if (g_freq < SW_SQ_MIN_HZ) {
|
||||||
// «программный» прямоугольник: аппаратный ШИМ не тянет < 20 Гц
|
|
||||||
uint32_t step =
|
uint32_t step =
|
||||||
(uint32_t)((uint64_t)g_freq * (1ULL << 32) / SAMPLE_HZ);
|
(uint32_t)((uint64_t)g_freq * (1ULL << 32) / SAMPLE_HZ);
|
||||||
g_phase += step;
|
g_phase += step;
|
||||||
digitalWrite(PIN_OUT, ((g_phase >> 24) < g_duty) ? HIGH : LOW);
|
pwmDuty(((g_phase >> 24) < g_duty) ? PWM_RANGE : 0);
|
||||||
}
|
}
|
||||||
// в режиме SQUARE с f >= SW_SQ_MIN_HZ ШИМ работает аппаратно, ISR молчит
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if USE_MBED_API
|
||||||
|
static void sampleTick() { sampleIsrBody(); }
|
||||||
|
static mbed::Ticker g_tick;
|
||||||
|
static void startSampleClock() {
|
||||||
|
// 16 мкс -> 62,5 кГц; Ticker — аппаратный таймер mbed
|
||||||
|
g_tick.attach_us(&sampleTick, 1000000L / SAMPLE_HZ);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static bool onSample(repeating_timer_t *) {
|
||||||
|
sampleIsrBody();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
static repeating_timer_t g_timer;
|
static repeating_timer_t g_timer;
|
||||||
|
static void startSampleClock() {
|
||||||
|
add_repeating_timer_us(-(1000000L / SAMPLE_HZ), onSample, nullptr, &g_timer);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// ─────────────────── применить режим/частоту ────────────────
|
// ─────────────────── применить режим/частоту ────────────────
|
||||||
static void applySquare() {
|
static void applySquare() {
|
||||||
if (g_freq < SW_SQ_MIN_HZ) {
|
if (g_freq < SW_SQ_MIN_HZ) {
|
||||||
// программный прямоугольник (ISR), ШИМ выключен
|
// программный прямоугольник (ISR): несущая = SAMPLE_HZ
|
||||||
pwmStop();
|
pwmStart();
|
||||||
digitalWrite(PIN_OUT, g_duty > 0 ? HIGH : LOW);
|
pwmConfig(SAMPLE_HZ, PWM_RANGE);
|
||||||
|
pwmDuty(g_duty > 0 ? PWM_RANGE : 0);
|
||||||
} else {
|
} else {
|
||||||
pwmStart();
|
pwmStart();
|
||||||
pwmConfig(g_freq, PWM_RANGE);
|
pwmConfig(g_freq, PWM_RANGE);
|
||||||
@@ -338,7 +311,7 @@ void setup() {
|
|||||||
pinMode(PIN_TRIG, OUTPUT);
|
pinMode(PIN_TRIG, OUTPUT);
|
||||||
digitalWrite(PIN_TRIG, LOW);
|
digitalWrite(PIN_TRIG, LOW);
|
||||||
analogReadResolution(12); // ADC0 — 12 бит
|
analogReadResolution(12); // ADC0 — 12 бит
|
||||||
add_repeating_timer_us(-(1000000L / SAMPLE_HZ), onSample, nullptr, &g_timer);
|
startSampleClock();
|
||||||
setMode(MODE_SQUARE);
|
setMode(MODE_SQUARE);
|
||||||
setFrequency(1000);
|
setFrequency(1000);
|
||||||
Serial.println("# DDS generator ready (RP2040). help: F/W/D/A/SWEEP/STOP");
|
Serial.println("# DDS generator ready (RP2040). help: F/W/D/A/SWEEP/STOP");
|
||||||
|
|||||||
@@ -111,8 +111,12 @@ R2 = 100 кОм, C1 = 0,1 мкФ (R2 || C1 в ООС).
|
|||||||
Использовать подготовленный генератор-свип:
|
Использовать подготовленный генератор-свип:
|
||||||
|
|
||||||
- **`stub/01-dds-generator-rp2040.ino`** — прошить в RP2040 (Raspberry Pi
|
- **`stub/01-dds-generator-rp2040.ino`** — прошить в RP2040 (Raspberry Pi
|
||||||
Pico). Прямоугольник (ШИМ) и синус (DDS) + логарифмический свип
|
Pico) в Arduino IDE (работает в обоих ядрах: earlephilhower и официальном
|
||||||
|
mbed). Прямоугольник (ШИМ) и синус (DDS) + логарифмический свип
|
||||||
+ измерение амплитуды на ADC0.
|
+ измерение амплитуды на ADC0.
|
||||||
|
- **`stub/01-dds-generator-rp2040-circuitpython.py`** — альтернатива без
|
||||||
|
Arduino: положить файл на Pico с прошивкой CircuitPython (переименовать
|
||||||
|
в code.py). Протокол команд тот же.
|
||||||
- **`stub/02-izmerenie-achh.py`** — хост-скрипт: запускает свип по Serial,
|
- **`stub/02-izmerenie-achh.py`** — хост-скрипт: запускает свип по Serial,
|
||||||
собирает точки, строит АЧХ.
|
собирает точки, строит АЧХ.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user