diff --git a/README.md b/README.md index a116da8..6bedf04 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,10 @@ labs/lab5-peredatochnaya-funkciya-na-ou/ ├── README.md ← этот файл ├── print/ ← брошюры DOCX (rpd methodical) └── 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 └── 03-blank-otchyota.md ← бланк отчёта студента ``` @@ -90,7 +93,8 @@ GP15 (ШИМ 62,5 кГц) ──[R1 1к]──┬──[R2 1к]──┬── 1. `task.md` + `stub/03-blank-otchyota.md` — студенту. 2. `stub/01-dds-generator-rp2040.ino` — прошить в RP2040 заранее - (или дать студенту в задании 4). + (или дать студенту в задании 4); альтернатива — `...-circuitpython.py` + на Pico с прошивкой CircuitPython (как `code.py`). 3. `stub/02-izmerenie-achh.py` — запускается на ПК студента (нужен pyserial). 4. `method.md` — только преподавателю. 5. Брошюры: `uv run rpd methodical lab5-peredatochnaya-funkciya-na-ou`. diff --git a/stub/01-dds-generator-rp2040-circuitpython.py b/stub/01-dds-generator-rp2040-circuitpython.py new file mode 100644 index 0000000..b4078e7 --- /dev/null +++ b/stub/01-dds-generator-rp2040-circuitpython.py @@ -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 +# — логарифмический свип; "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 ") + 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() diff --git a/stub/01-dds-generator-rp2040.ino b/stub/01-dds-generator-rp2040.ino index e92f39c..49656c4 100644 --- a/stub/01-dds-generator-rp2040.ino +++ b/stub/01-dds-generator-rp2040.ino @@ -3,11 +3,13 @@ * Платформа: RP2040 (Raspberry Pi Pico) * * Работает в ОБОИХ Arduino-ядрах: - * - earlephilhower (arduino-pico) — штатный ШИМ analogWriteFreq/analogWriteRange; - * - официальное ядро "Raspberry Pi Pico/RP2040" (Arduino Mbed OS) — analogWriteFreq - * в нём отсутствует, поэтому включается собственный драйвер ШИМ на регистрах - * pico-sdk (hardware/pwm.h). Никакой настройки не требуется — ветка выбирается - * автоматически по макросу ARDUINO_ARCH_MBED. + * - earlephilhower (arduino-pico) — штатный ШИМ analogWriteFreq/analogWriteRange + * и таймер add_repeating_timer_us (pico-sdk); + * - официальное ядро "Raspberry Pi Pico/RP2040" (Arduino Mbed OS) — в пользовательском + * коде НЕТ ни analogWriteFreq, ни pico-sdk-заголовков (hardware/pwm.h, hardware/timer.h), + * поэтому здесь используется только mbed API, который является частью самого ядра: + * mbed::PwmOut (ШИМ) + mbed::Ticker (тактовый генератор выборок 62,5 кГц). + * Ветка выбирается автоматически по макросу ARDUINO_ARCH_MBED. * * ЧЕРНОВИК (AI-сгенерировано, 2026-10-26). Требует проверки преподавателем * на реальном стенде: номиналы RC-фильтра, диапазоны частот, точность ADC. @@ -62,17 +64,17 @@ #include #include -#include "hardware/timer.h" // add_repeating_timer_us, repeating_timer_t — оба ядра -// ───────────────── выбор драйвера ШИМ ───────────────── -// Официальное mbed-ядро не имеет analogWriteFreq/analogWriteRange — -// используем регистры pico-sdk. earlephilhower — штатный analogWrite*. +// ───────────────── выбор бэкенда ───────────────── +// Официальное mbed-ядро: нет analogWriteFreq, нет pico-sdk в пользовательском +// коде -> только mbed API (PwmOut + Ticker). earlephilhower: analogWrite* + +// add_repeating_timer_us. #if defined(ARDUINO_ARCH_MBED) - #define USE_CORE_PWM 0 - #include "hardware/pwm.h" - #include "hardware/gpio.h" + #define USE_MBED_API 1 + #include "mbed.h" #else - #define USE_CORE_PWM 1 + #define USE_MBED_API 0 + #include "hardware/timer.h" // add_repeating_timer_us, repeating_timer_t #endif // ─────────────────────────── пины ─────────────────────────── @@ -110,9 +112,23 @@ static int8_t g_lut[256]; // sin: -127..127 // pwmStart() — включить ШИМ на PIN_OUT (idempotent) // pwmConfig(f, range) — настроить частоту несущей // 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 static void pwmStart() { /* analogWrite сам включает ШИМ */ } 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) { analogWrite(PIN_OUT, duty); } -static void pwmStop() { - // 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 +#endif // USE_MBED_API // ─────────────────────── таблица синуса ───────────────────── static void buildLut() { @@ -192,8 +147,12 @@ static void buildLut() { } } -// ───────────── ISR: обновление ШИМ для синуса (DDS) ───────── -static bool onSample(repeating_timer_t *) { +// ─────────────── тело ISR: обновление выборок ──────────────── +// В режиме 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) { // шаг фазы = f * 2^32 / Fs (64-бит, чтобы не переполнить) uint32_t step = @@ -204,24 +163,38 @@ static bool onSample(repeating_timer_t *) { int32_t s = ((int32_t)g_lut[idx] * g_amp) / 127; pwmDuty((uint8_t)(128 + s)); } else if (g_freq < SW_SQ_MIN_HZ) { - // «программный» прямоугольник: аппаратный ШИМ не тянет < 20 Гц uint32_t step = (uint32_t)((uint64_t)g_freq * (1ULL << 32) / SAMPLE_HZ); 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 void startSampleClock() { + add_repeating_timer_us(-(1000000L / SAMPLE_HZ), onSample, nullptr, &g_timer); +} +#endif // ─────────────────── применить режим/частоту ──────────────── static void applySquare() { if (g_freq < SW_SQ_MIN_HZ) { - // программный прямоугольник (ISR), ШИМ выключен - pwmStop(); - digitalWrite(PIN_OUT, g_duty > 0 ? HIGH : LOW); + // программный прямоугольник (ISR): несущая = SAMPLE_HZ + pwmStart(); + pwmConfig(SAMPLE_HZ, PWM_RANGE); + pwmDuty(g_duty > 0 ? PWM_RANGE : 0); } else { pwmStart(); pwmConfig(g_freq, PWM_RANGE); @@ -338,7 +311,7 @@ void setup() { pinMode(PIN_TRIG, OUTPUT); digitalWrite(PIN_TRIG, LOW); analogReadResolution(12); // ADC0 — 12 бит - add_repeating_timer_us(-(1000000L / SAMPLE_HZ), onSample, nullptr, &g_timer); + startSampleClock(); setMode(MODE_SQUARE); setFrequency(1000); Serial.println("# DDS generator ready (RP2040). help: F/W/D/A/SWEEP/STOP"); diff --git a/task.md b/task.md index e18dbaf..e0fe19e 100644 --- a/task.md +++ b/task.md @@ -111,8 +111,12 @@ R2 = 100 кОм, C1 = 0,1 мкФ (R2 || C1 в ООС). Использовать подготовленный генератор-свип: - **`stub/01-dds-generator-rp2040.ino`** — прошить в RP2040 (Raspberry Pi - Pico). Прямоугольник (ШИМ) и синус (DDS) + логарифмический свип + Pico) в Arduino IDE (работает в обоих ядрах: earlephilhower и официальном + mbed). Прямоугольник (ШИМ) и синус (DDS) + логарифмический свип + измерение амплитуды на ADC0. +- **`stub/01-dds-generator-rp2040-circuitpython.py`** — альтернатива без + Arduino: положить файл на Pico с прошивкой CircuitPython (переименовать + в code.py). Протокол команд тот же. - **`stub/02-izmerenie-achh.py`** — хост-скрипт: запускает свип по Serial, собирает точки, строит АЧХ.