Add shared waveform processing, generation and firmware image support

This commit is contained in:
2026-09-27 01:45:08 +03:00
parent 2ad29e7ffd
commit cc22c803d1
36 changed files with 2591 additions and 61 deletions

View File

@@ -0,0 +1,61 @@
"""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")