62 lines
3.3 KiB
Python
62 lines
3.3 KiB
Python
"""Reusable recipes and tables; interpolation/quantization live in C."""
|
||
from __future__ import annotations
|
||
import math
|
||
import json
|
||
from dataclasses import dataclass
|
||
from .signal_reconstruction import reconstruct, dac12
|
||
|
||
@dataclass(frozen=True)
|
||
class Waveform:
|
||
points: tuple
|
||
codes: tuple
|
||
sample_rate: int
|
||
vref: float
|
||
period_ms: float
|
||
|
||
def generate(points, sample_rate=1000, vref=3.3, method="pchip", degree=2):
|
||
"""One cyclic period, last endpoint excluded (no duplicated seam sample)."""
|
||
if type(sample_rate) is not int or not 1 <= sample_rate <= 1000000:
|
||
raise ValueError("Частота отсчётов должна быть 1…1000000 Гц")
|
||
if not math.isfinite(vref) or not 0 < vref <= 3.6:
|
||
raise ValueError("Vref должен быть в диапазоне 0…3,6 В")
|
||
if not 2 <= len(points) <= 4096 or points[0][0] != 0:
|
||
raise ValueError("Нужно 2…4096 точек; первая временная метка — 0 мс")
|
||
if any(not math.isfinite(x) or not math.isfinite(y) or not 0 <= y <= vref for x, y in points):
|
||
raise ValueError("В точках нужны конечные числа, напряжение 0…Vref")
|
||
if any(b[0] <= a[0] for a, b in zip(points, points[1:])):
|
||
raise ValueError("Время точек должно строго возрастать")
|
||
period = points[-1][0]
|
||
exact_count = period * sample_rate / 1000
|
||
count = round(exact_count)
|
||
if not 2 <= count <= 4096:
|
||
raise ValueError("Период × частота должны давать от 2 до 4096 отсчётов")
|
||
if abs(exact_count - count) > 1e-7:
|
||
raise ValueError("Период должен содержать целое число отсчётов при выбранной частоте")
|
||
result = reconstruct(points, method, count, degree, endpoint=False)
|
||
# Check the endpoint too: polynomial fitting can move its value.
|
||
checked = reconstruct(points, method, count + 1, degree)
|
||
dac12([p[1] for p in checked.points], vref)
|
||
codes = dac12([p[1] for p in result.points], vref)
|
||
return Waveform(tuple(result.points), codes, sample_rate, vref, period)
|
||
|
||
def recipe(points, sample_rate, vref, method, degree):
|
||
return {"version": 1, "points_ms_volts": [list(pair) for pair in points], "sample_rate": sample_rate,
|
||
"vref": vref, "method": method, "degree": degree}
|
||
|
||
def load_recipe(text):
|
||
data = json.loads(text)
|
||
if not isinstance(data, dict) or data.get("version") != 1:
|
||
raise ValueError("Неизвестная версия задания генератора")
|
||
points = tuple((float(x), float(y)) for x, y in data["points_ms_volts"])
|
||
generate(points, data["sample_rate"], data["vref"], data["method"], data["degree"])
|
||
return points, data
|
||
|
||
def c_header(wave):
|
||
values = [", ".join(str(v) for v in wave.codes[i:i+16]) for i in range(0, len(wave.codes), 16)]
|
||
return ("/* Generated cyclic 12-bit DAC table. DMA-accessible SRAM, not CCM. */\n"
|
||
"#pragma once\n#include <stdint.h>\n"
|
||
f"#define WAVE_SAMPLE_RATE_HZ {wave.sample_rate}u\n"
|
||
f"#define WAVE_SAMPLE_COUNT {len(wave.codes)}u\n"
|
||
f"/* Vref = {wave.vref:g} V; period = {wave.period_ms:g} ms. */\n"
|
||
"static uint16_t wave_samples[WAVE_SAMPLE_COUNT] = {\n " + ",\n ".join(values) + "\n};\n")
|