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

@@ -6,6 +6,7 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
# SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
set(SETPROTOCOL_V2_SOURCES
src/firmware_image.c
src/altera_logic.c
src/altera_stream.c
src/set_protocol.c
@@ -15,6 +16,8 @@ set(SETPROTOCOL_V2_SOURCES
src/set_plot.c
src/set_trends.c
src/set_spectrum.c
src/set_signal.c
src/set_wavegen.c
)
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
@@ -81,6 +84,12 @@ endif()
option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS)
enable_testing()
add_executable(test_signal_wave tests/test_signal_wave.c)
target_link_libraries(test_signal_wave PRIVATE setprotocol_static)
add_test(NAME signal_wave COMMAND test_signal_wave)
add_executable(test_firmware_image tests/test_firmware_image.c)
target_link_libraries(test_firmware_image PRIVATE setprotocol_static)
add_test(NAME firmware_image COMMAND test_firmware_image)
add_executable(test_altera_logic tests/test_altera_logic.c)
target_link_libraries(test_altera_logic PRIVATE setprotocol_static)
add_test(NAME altera_logic COMMAND test_altera_logic)

View File

@@ -1,5 +1,8 @@
# SETProtocol
Генератор произвольного сигнала, интерполяция, таблицы ЦАП и порты
STM32F407/G474: [SIGNAL_GENERATOR.md](docs/SIGNAL_GENERATOR.md).
Единое переносимое протокольное ядро SET для `SETGUI`, Android GUI, устройств
и сервисных утилит. В одном C99-проекте собраны:
@@ -94,3 +97,9 @@ FFI-клиенты подключают `setprotocol_abi.h`. Имена функ
- Изменение wire contract требует новой версии и тестовых векторов.
Полная интерактивная документация: [`../../doc/setprotocol.html`](../../doc/setprotocol.html).
Обработка кривых: МНК, линейная интерполяция, PCHIP, кубический сплайн,
общие модели и Qt-панель для разных графиков:
[PLOT_PROCESSING.md](docs/PLOT_PROCESSING.md).
Генератор сигнала, USB и порты ЦАП F407/G474:
[SIGNAL_GENERATOR.md](docs/SIGNAL_GENERATOR.md).

View File

@@ -0,0 +1,146 @@
# Общая обработка графиков и сигналов
Аппроксимация, интерполяция и восстановление кривой по редким отсчётам имеют
один расчётный API. Источник может быть временным графиком, журналом, спектром
или таблицей ручного генератора. Выбор устройства и отрисовка остаются в порте.
| Метод | Назначение | Ограничения |
|---|---|---|
| `polynomial` | Аппроксимация полиномом МНК, степени 1–5; сглаживание шумных отсчётов | Нужно не меньше `degree + 1` различных X; степень задаёт пользователь |
| `linear` | Линейная интерполяция | Минимум два различных X; изломы в узлах |
| `pchip` | Интерполяция с сохранением формы | Минимум два различных X; подходит для фронтов и монотонных участков |
| `spline` | Восстановление естественным кубическим сплайном | Минимум два различных X; возможны выбросы между узлами |
Все методы строят сетку только между крайними выбранными отсчётами.
Восстановление по малому числу точек — оценка, зависящая от метода; утраченные
высокочастотные детали не определяются однозначно. СКО считается на исходных
измерениях, поэтому нулевая СКО интерполяции не означает нулевую ошибку между ними.
Одинаковые X усредняются, нечисловые и бесконечные значения отклоняются.
## Слои и зависимости
```text
График / журнал → Snapshot → Request → set_signal_reconstruct (C99) → Curve
Ручные точки генератора → waveform.generate → то же C99-ядро → таблица ЦАП
Qt-панель → worker → Curve → отдельный слой графика / CSV
МК / Android / другой GUI → C ABI → свой порт отображения или вывода
```
| Файл | Роль / зависимости |
|---|---|
| `include/set_signal.h`, `src/set_signal.c` | Все четыре численных метода и квантование ЦАП; C99, без Qt, heap и файловой системы |
| `python/set_devices/signal_reconstruction.py` | Тонкий вызов C через ctypes, без второго алгоритма |
| `python/set_devices/plot_processing.py` | Неизменяемые модели, выбор канала и диапазона, явные единицы, CSV; без Qt |
| `python/set_devices/qt_ports/plot_processing.py` | Общая панель, фоновый расчёт, проверка актуальности, слой отрисовки; PySide6 или PySide2 |
| `python/set_devices/waveform.py` | Периодическая сетка генератора и DAC12 через то же ядро |
Перед вызовом расчёта потребитель задаёт `SETPROTOCOL_LIBRARY` либо устанавливает
собранную библиотеку в штатный каталог `protocan/native`. Никакие соседние
репозитории или каталоги приложений автоматически не подключаются.
## Контракт графика
- `Series(key, label, points, visible, discrete, y_unit)` копирует пары X/Y.
Ключи уникальны внутри снимка. Расчёт доступен только видимым аналоговым каналам.
- `Axis(label, unit, encoding)` явно задаёт область X. `numeric` сохраняет числа,
`unix_ms` сохраняет UTC ISO timestamp. Большое число само по себе не является датой.
- `Snapshot(series, axis, source, x_range, blocked_reason)` описывает снимок источника.
`x_range=(left, right)` включает точки на обеих границах; `None` означает весь снимок.
`source` должен различать файлы/источники, если переключение между ними требует
сброса результата даже при совпадающих числах. `blocked_reason` запрещает расчёт.
- `prepare(snapshot, key, method, output_count, degree)` создаёт сравнимый запрос.
`process(request)` возвращает `Curve` с точками, числом измерений/уникальных X и СКО.
- `write_csv(curve, stream)` сохраняет отдельный результат, подпись метода и единицы.
Исходные данные не изменяются. При визуальном множителе Y адаптер передаёт
отображаемые значения и указывает множитель в `y_unit`, например `В (×2)`.
Нативное ядро допускает до 100000 входных и 10000 выходных точек. UI задаёт
2–10000 точек результата. После обрезки по X должно остаться достаточно узлов
для выбранного метода. Расчёт не добавляет искусственные узлы на границах окна.
## Пример без GUI
```python
from set_devices.plot_processing import Axis, Series, Snapshot, prepare, process, write_csv
samples = [(0, 0), (10, 2), (20, 1), (30, 0)]
snapshot = Snapshot((Series("voltage", "Напряжение", samples, y_unit="В"),),
Axis("Время", "мс"), source="bench-1")
request = prepare(snapshot, "voltage", method="pchip", output_count=301)
curve = process(request)
with open("calculated.csv", "w", encoding="utf-8-sig", newline="") as stream:
write_csv(curve, stream)
```
Рабочий CLI-пример для всех методов:
`python python/examples/plot_processing.py --method spline --output curve.csv`.
Добавьте `templates/python` в `PYTHONPATH` или установите пакет из этого каталога.
## Подключение нового Qt-графика
`PlotProcessingAttachment(parent, snapshot, repaint)` принимает два callback:
```python
def snapshot() -> Snapshot: ... # текущие отображаемые данные и видимые границы X
def repaint() -> None: ... # обычно QWidget.update
```
1. Создайте attachment и добавьте его `button` в панель графика.
2. После изменения данных, каналов, единиц или окна вызывайте `source_changed()`.
Пока панель не открывали, снимки не строятся. Уведомления одного прохода
event loop объединяются; устаревшая линия сразу скрывается.
3. После исходных кривых вызывайте `attachment.paint(painter, analog_rect, project)`.
`project(x, y, rect) -> QPointF` использует ту же проекцию, что исходные данные.
4. Вызов `open()` открывает немодальное окно с выбором всех четырёх методов,
числа точек, степени полинома, расчётом, удалением и экспортом.
```python
self.processing = PlotProcessingAttachment(self, self.processing_snapshot, self.update)
self.toolbar.layout().addWidget(self.processing.button)
# после обновления источника/масштаба:
self.processing.source_changed()
# внутри paintEvent, после исходных линий:
self.processing.paint(painter, self.analog_rect(), self.project)
```
Для встроенной панели используйте `SignalProcessingPanel.set_snapshot(snapshot)`
и сигнал `changed` для перерисовки. Передавайте `panel.curve` слою через
`set_external_curve(curve, x_offset=0)`. В этом варианте владелец обновляет снимок
и слой вместе. `x_offset` используется только при рисовании, например при сдвиге
epoch на графике наносекундного масштаба; исходные X и CSV сохраняются.
Панель сравнивает снимок выбранного канала, единицы, источник, диапазон и параметры.
При изменении результат убирается, запоздалый ответ worker игнорируется.
Для обработки живого потока остановите его обновление. Y-масштаб не меняет запрос,
если X и данные остались прежними. Кривая обрезается текущей областью Y; при выбросе
сплайна можно увеличить диапазон Y. Панель не меняет историю, autoscale, курсоры,
FFT и цифровые дорожки графика.
## Порты и расширение
SETGUI использует компонент в «Логах и графиках», SignalPlot (CAN, температуры,
УМП), TrendPlot и SpectrumPlot. В спектре методы обрабатывают зависимость уровня
от частоты в Гц; восстановление временного сигнала из амплитудного спектра этим
не выполняется. Временная панель «Логов» блокируется при включённом FFT.
Генератор использует те же методы через `waveform.generate`; для циклической
таблицы последний отсчёт периода не дублируется.
Для нового графика достаточно адаптера `Snapshot` и существующей панели/слоя.
Для другого GUI используйте модель без Qt или непосредственно C ABI.
Для MCU вызывайте `set_signal_reconstruct` с буфером `14 * count + 128` double,
выделенным вызывающей стороной; рабочая память должна соответствовать RAM платы.
Контракт вывода ЦАП и готовые порты F407/G474 описаны в [SIGNAL_GENERATOR.md](SIGNAL_GENERATOR.md).
Для нового численного метода сначала расширьте C API, C-тесты и соответствие
`METHODS` в ctypes-порте; затем добавьте эталон в `test_plot_processing.py`.
Алгоритмы в обработчиках отдельных графиков не дублируются.
Проверки библиотеки:
```text
python -m unittest discover -s python/tests -p test_plot_processing*.py
python -m unittest discover -s python/tests -p test_shared_library_boundary.py
```
Для тестов Qt нужен PySide6/PySide2; для headless-прогона задайте
`QT_QPA_PLATFORM=offscreen`. Численные тесты `test_plot_processing.py` Qt не требуют.

View File

@@ -0,0 +1,146 @@
# Генератор произвольного сигнала WG v1/v2
## Расширение карты v2: F407 до 1 000 000 отсчётов/с
Версия карты в статусе — 2, `set_wave_state.rate` — uint32_t.
Для записи частоты сначала остановить выход, записать старшие 16 бит в
`0x1308`, затем младшие 16 бит в `0x1303`. Вторая запись проверяет диапазон
1…1 000 000 и применяет частоту; обе записи снимают ready. Чтение `0x1308`
возвращает старшие биты применённой частоты, а слово 3 статуса — младшие.
Host ABI: операция 9 — запись старшего слова, 8 — его чтение, 2 — запись
младшего слова (включая ноль). Остальные команды не меняются.
Это версия карты регистров, а не транспорт: SETGUI использует SET v2
через EmulatorSerialPort, с резервным RTU для старой прошивки. Новый клиент
понимает статус карт v1/v2; для v1 предел остаётся 50 000. Старый клиент
отклоняет незнакомую версию 2 до записи. Код F407 и GUI поддерживают 1 МГц;
порт G474 пока сохраняет прежний аппаратный лимит 50 кГц.
Ниже описание исходной карты v1; пределы и регистры частоты заменены этим
расширением для v2. На высоких частотах аналоговое установление зависит
от величины скачка и нагрузки; аппаратная проверка осциллографом обязательна
для оценки точности конкретного сигнала.
Общее C99-ядро строит кривые по точкам, преобразует напряжения в коды ЦАП и
принимает таблицы через RTU поверх USB CDC/COM. Оно не зависит от Qt, HAL или ОС.
Память предоставляет вызывающий; скрытого heap и глобального состояния нет.
```text
SETGUI: точки -> Python/ctypes -> set_signal.c -> график / CSV / C
-> set_wavegen.c -> USB CDC
MCU: USB stream -> set_wave_rtu -> set_wave_port -> TIM6/DMA/DAC
```
| Файл | Зависимости и назначение |
|---|---|
| `include/set_signal.h`, `src/set_signal.c` | C99/math: МНК 1…5, linear, PCHIP, natural cubic spline, 12-bit DAC |
| `include/set_wavegen.h`, `src/set_wavegen.c` | C99 + `set_crc.c`: транзакционная загрузка и wire codec |
| `ports/stm32f407-wavegen` | CMSIS F407: PA4/DAC1, TIM6, DMA1 Stream5 Channel7 |
| `ports/stm32g474-wavegen` | STM32G4 HAL: готовые DAC/TIM/DMA handles из CubeMX |
| `python/set_devices/signal_reconstruction.py`, `waveform.py` | Модели, JSON/экспорт, ctypes; алгоритмов на Python нет |
| `python/set_devices/wavegen_protocol.py`, `qt_ports/wavegen.py` | ctypes-кодек и Qt COM worker с проверкой чтением обратно |
## Интерполяция и таблица
`set_signal_reconstruct` принимает раздельные X/Y и workspace `14*N+128`
элементов double. Сортировка, усреднение одинаковых времён, нормализация,
МНК через QR и интерполяция выполняются в C. Максимум 100000 входных/10000
выходных точек. Переданный workspace может повторно использоваться.
Большие расчёты предназначены для хоста; на MCU можно строить малые таблицы
или принимать готовую таблицу без затрат на интерполяцию.
`endpoint=1` включает последний X (анализ логов); `endpoint=0` строит один
период `[0,T)` без дублированного отсчёта стыка. Частота Fs и период T задают
целое `N=Fs*T`; для WG максимум N=4096, Fs=1…50000 Гц, один канал, циклический
выход. При разных значениях первой и последней точки на стыке будет скачок.
Напряжения 0…Vref округляются к ближайшему коду 0…4095; выход за диапазон
отклоняется целиком. Это относится и к выбросам сплайна. Vref должен
соответствовать фактическому VDDA/VREF+ платы; это не программируемое питание.
Пример хоста (DLL передаётся через `SETPROTOCOL_LIBRARY`):
```python
from set_devices.waveform import generate, c_header
points = [(0, 0), (10, 3.3), (20, 0)]
wave = generate(points, sample_rate=10000, vref=3.3, method="linear")
assert len(wave.codes) == 200
with open("waveform.h", "w", encoding="utf-8") as output:
output.write(c_header(wave))
```
## Контракт порта
`start(context, samples, count, sample_rate)` возвращает 0 при успехе;
`stop(context)` синхронно прекращает DMA и устанавливает нулевой выход.
Память таблицы принадлежит `set_wave_state`, её нельзя менять до stop.
Вызывайте RTU и обслуживание состояния из одного основного потока, не из IRQ.
IRQ USB помещает байты в очередь; USB-пакеты не являются границами RTU.
```c
static uint16_t samples[SET_WAVE_MAX]; /* DMA-accessible SRAM, not CCM */
static set_wave_state wave;
static set_wave_f407 hardware = {72000000};
void application_init(void) {
set_wave_port port = set_wave_f407_port(&hardware);
set_wave_init(&wave, samples, SET_WAVE_MAX, &port);
}
size_t on_rtu(const uint8_t *frame, size_t n, uint8_t reply[256]) {
return set_wave_rtu(&wave, 16, frame, n, reply, 256);
}
```
Для F407 DAC_CH1 — PA4, TIM6 TRGO_UPDATE, DMA1 Stream5/Channel7. Порт использует
только нижние 16 бит DAC_CR и не меняет канал 2. Таблица лежит в SRAM1/2
`0x20000000…0x2001FFFF`, не в CCM. Перед первым отсчётом есть один такт
предзагрузки последнего значения предыдущего периода. Для гарантированной
целой частоты порт принимает только делители входной частоты TIM6; при
72 МГц подходят, например, 100, 1000, 5000, 10000, 20000 Гц.
Частоту таймера передавайте с учётом удвоения при делителе APB1 > 1.
Ошибку DMA/underrun проверяйте через `set_wave_f407_fault()` и останавливайте
состояние. На F407-проекте этот вызов включён в главный цикл.
Для G474 настройте CubeMX: DAC1 channel1 (PA4), output buffer enabled,
TIM6 TRGO_UPDATE, DMA memory-to-peripheral, circular, halfword/halfword,
memory increment, запрос `DMA_REQUEST_DAC1_CHANNEL1`; подключите HAL IRQ
для выбранного канала DMA и DAC. Передайте handles и timer_hz в
`set_wave_g474`. Обработчики ошибок DMA/DAC в приложении должны вызвать stop
и сбросить `wave.running/ready`. Порт проверен компиляцией с CubeG4 1.6.1;
конкретная плата G474 здесь не прошита.
## USB / Modbus RTU
Адрес по умолчанию 16, FC03/FC06, CRC16 Modbus. Максимальный запрос — 8 байт,
ответ — 21 байт; существующий потоковый USB RTU parser F407 подходит без
изменения формата. Все поля строит/проверяет C-кодек, GUI не пакует байты.
| Регистр | Значение |
|---|---|
| `0x1300`, FC03, 8 слов | `0x5747`, версия 1, running, Fs, count, received, ready, capacity |
| `0x1302`, FC06 | 0: stop и нулевой выход; 1: start только после commit |
| `0x1303`, FC06 | Частота 1…50000; сбрасывает ready |
| `0x1304`, FC06 | Начать загрузку, число отсчётов 2…4096; сброс received/ready |
| `0xA000+i`, FC06 | Последовательная загрузка отсчёта 0…4095; повтор того же значения допустим |
| `0xA000+i`, FC03, 1 слово | Чтение загруженного отсчёта для проверки |
| `0x1306`, FC06 | `0xA55A`: commit, только если все отсчёты загружены |
Во время running разрешены чтение и stop; запись/перезагрузка дают исключение
6. Неполная таблица не запускается. Повреждённый CRC и чужой адрес не меняют
состояние. При reset ready/running=0, таблица не сохраняется во Flash.
Профиль и регистры генератора не являются регистратором УМП или протоколом 2812.
Хост выполняет probe → stop → Fs → count → samples → readback каждого
отсчёта → commit → status. Пуск — отдельная пользовательская команда.
После потери USB уже запущенный DAC продолжает автономное воспроизведение;
при таймауте состояние неизвестно до повторного status/stop. Одновременно
используйте одного клиента, не открывайте этот же COM во вкладке подключения.
## Потребители и проверки
- SETGUI: «Логи и графики → Генератор», JSON, CSV и C header.
- `home/407vet6_emul_TMS_Periph`: USB CDC и UART, штатный адрес настройки STM,
отдельный PA4 DAC выход; HAL/CMSIS и настройки платы остаются в проекте.
- G474: переносимый HAL-порт; board init остаётся в целевой прошивке.
- `tests/test_signal_wave.c`: C-векторы интерполяции, DAC, CRC, неполной
загрузки, readback и блокировки running. Общие байты проверены Python
ctypes и фактическим USB stream parser эмулятора.
Аппаратная проверка амплитуды, периода и формы осциллографом не выполнена.

View File

@@ -0,0 +1,24 @@
#ifndef SET_FIRMWARE_IMAGE_H
#define SET_FIRMWARE_IMAGE_H
#include "pcan_abi.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Caller-owned workspace; the parser never allocates memory or accesses files. */
typedef struct { uint32_t address; uint8_t value; } firmware_hex_cell_t;
enum { FW_HEX_OK, FW_HEX_FORMAT, FW_HEX_CHECKSUM, FW_HEX_RECORD,
FW_HEX_EOF, FW_HEX_OVERLAP, FW_HEX_RANGE, FW_HEX_CAPACITY,
FW_HEX_PREFIX, FW_HEX_DIGITS, FW_HEX_LENGTH };
/* Strict HEX: reject any overlap and any non-whitespace after EOF. On failure
* count is zero. Cells are sorted by address only on successful completion. */
PCAN_ABI_API int firmware_hex_parse(const char *text, size_t length,
firmware_hex_cell_t *cells, size_t capacity, size_t max_records,
size_t *count, size_t *error_line);
/* Return one contiguous segment starting at offset in a successfully parsed
* workspace; the caller uses its address and copies its values into its model. */
PCAN_ABI_API size_t firmware_hex_segment_size(const firmware_hex_cell_t *cells,
size_t count, size_t offset);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,24 @@
/** @file set_signal.h Portable interpolation and DAC waveform preparation. */
#ifndef SET_SIGNAL_H
#define SET_SIGNAL_H
#include "pcan_abi.h"
#ifdef __cplusplus
extern "C" {
#endif
enum { SET_SIGNAL_POLYNOMIAL, SET_SIGNAL_LINEAR, SET_SIGNAL_PCHIP, SET_SIGNAL_SPLINE };
/* Caller-owned workspace: at least 14*count+128 doubles. No heap or globals.
* Inputs may be unsorted; duplicate times are averaged. No extrapolation.
* meta[0..2] = original count, unique count, RMSE at original measurements.
* endpoint=1 includes the last time; 0 samples a cyclic period [first,last).
* Return: 0 success, 1 bounds, 2 nonfinite/degenerate data, 3 rank deficient. */
PCAN_ABI_API int set_signal_reconstruct(const double *x, const double *y, size_t count,
int method, unsigned degree, size_t output_count, int endpoint,
double *out_x, double *out_y, double *meta, double *work, size_t work_count);
/* Reject out-of-range voltages, never silently clip. vref must be positive.
* All input is validated before any output is written. */
PCAN_ABI_API int set_signal_dac12(const double *volts, size_t count, double vref,
uint16_t *codes);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,59 @@
/** @file set_wavegen.h Transactional table upload and cyclic DAC playback. */
#ifndef SET_WAVEGEN_H
#define SET_WAVEGEN_H
#include "pcan_abi.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_WAVE_MAX 4096u
#define SET_WAVE_MAX_RATE 1000000u
#define SET_WAVE_BLOCK_MAX 120u
#define SET_WAVE_BASE 0x1300u
#define SET_WAVE_DATA 0xA000u
typedef struct {
void *context;
/* Return 0 on success. Samples remain owned by state until stop returns.
* start must arrange periodic output, typically DAC + timer + DMA.
* stop must quiesce DMA before returning and set output to zero. */
int (*start)(void *, const uint16_t *, size_t, uint32_t);
void (*stop)(void *);
} set_wave_port;
typedef struct {
uint16_t *samples;
size_t capacity;
uint16_t count, received, ready, running, rate_high;
uint32_t rate;
set_wave_port port;
} set_wave_state;
void set_wave_init(set_wave_state *, uint16_t *, size_t, const set_wave_port *);
/* Atomic sample-block transfer. values=NULL builds a read; otherwise a write.
* At most 120 words: request <=249 bytes, response <=245 bytes. */
PCAN_ABI_API size_t set_wave_block_request(unsigned device,unsigned index,
const uint16_t *values,size_t count,uint8_t *out,size_t capacity);
PCAN_ABI_API int set_wave_block_response(const uint8_t *request,size_t request_size,
const uint8_t *reply,size_t reply_size,uint16_t *words,size_t capacity);
size_t set_wave_block_rtu(set_wave_state *,unsigned device,const uint8_t *,size_t,
uint8_t *,size_t);
/* Relative register callbacks for set_regmap; status=8 words, samples=4096.
* These are the same state/validation used by the legacy RTU wrapper. */
PCAN_ABI_API unsigned set_wave_read(void *,uint16_t,uint16_t,uint16_t *);
PCAN_ABI_API unsigned set_wave_write(void *,uint16_t,uint16_t);
PCAN_ABI_API unsigned set_wave_samples_read(void *,uint16_t,uint16_t,uint16_t *);
PCAN_ABI_API unsigned set_wave_samples_write(void *,uint16_t,uint16_t);
/* Caller serializes calls in the main loop. Returns complete reply size,
* 0 for unrelated/invalid frames. CRC verified before all state changes. */
size_t set_wave_rtu(set_wave_state *, unsigned device, const uint8_t *, size_t,
uint8_t *, size_t);
/* Thin host transport ABI. Operations: 0=status, 1=stop, 2=sample rate,
* 3=begin upload(count), 4=sample(index,value), 5=commit, 6=start.
* 7=read a sample(index) for verification. Return 8 or 0. */
PCAN_ABI_API size_t set_wave_request(unsigned device,unsigned operation,unsigned index,
unsigned value,uint8_t *out,size_t capacity);
/* Return 0=need more bytes, positive=word count, negative=invalid/exception.
* Validates exact echo/read shape, CRC, address and signature on status. */
PCAN_ABI_API int set_wave_response(const uint8_t *request,const uint8_t *reply,
size_t size,uint16_t *words,size_t capacity);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,53 @@
#include "set_wave_f407.h"
#include "stm32f4xx.h"
static void stop(void *context) {
unsigned limit=100000; (void)context;
RCC->AHB1ENR|=RCC_AHB1ENR_GPIOAEN|RCC_AHB1ENR_DMA1EN;
RCC->APB1ENR|=RCC_APB1ENR_DACEN|RCC_APB1ENR_TIM6EN;
(void)RCC->APB1ENR;
TIM6->CR1=0;
DAC->CR&=~(DAC_CR_DMAEN1|DAC_CR_TEN1);
DMA1_Stream5->CR&=~DMA_SxCR_EN;
while((DMA1_Stream5->CR&DMA_SxCR_EN)&&--limit) {}
/* Leave other DAC channel untouched. Channel 1 outputs zero on stop. */
DAC->DHR12R1=0;
DMA1->HIFCR=0xF40u;
DAC->SR=DAC_SR_DMAUDR1;
}
static int start(void *context,const uint16_t *samples,size_t count,uint32_t rate) {
set_wave_f407 *c=(set_wave_f407 *)context;
uint32_t ticks,psc,arr; uintptr_t address=(uintptr_t)samples;
if(!c||!rate||rate>SET_WAVE_MAX_RATE||count<2||count>SET_WAVE_MAX||c->timer_hz%rate
||address<0x20000000u||address+count*2>0x20020000u) return 1;
ticks=c->timer_hz/rate;
for(psc=1;psc<=65536;++psc) if(ticks%psc==0&&ticks/psc<=65536) break;
if(psc>65536||ticks<psc) return 1;
arr=ticks/psc;
stop(context);
if(DMA1_Stream5->CR&DMA_SxCR_EN) return 1;
GPIOA->MODER|=3u<<8;
GPIOA->PUPDR&=~(3u<<8);
TIM6->PSC=psc-1;TIM6->ARR=arr-1;TIM6->CNT=0;
TIM6->CR2=0;TIM6->EGR=TIM_EGR_UG;TIM6->SR=0;
TIM6->CR2=TIM_CR2_MMS_1; /* update -> TRGO */
DMA1_Stream5->PAR=(uint32_t)&DAC->DHR12R1;
DMA1_Stream5->M0AR=(uint32_t)samples;DMA1_Stream5->NDTR=(uint32_t)count;
DMA1_Stream5->FCR=0;
DMA1_Stream5->CR=(7u<<25)|DMA_SxCR_DIR_0|DMA_SxCR_MINC|DMA_SxCR_CIRC
|DMA_SxCR_PSIZE_0|DMA_SxCR_MSIZE_0|DMA_SxCR_PL_1;
/* Prime the previous period's last sample; DMA supplies the next value
* after each trigger. Cyclic order is last,0,1,... with one priming tick. */
DAC->CR&=~0xFFFFu;
DAC->DHR12R1=samples[count-1];
DAC->CR|=DAC_CR_EN1|DAC_CR_TEN1|DAC_CR_DMAEN1; /* TSEL1=0: TIM6 */
DMA1_Stream5->CR|=DMA_SxCR_EN;
TIM6->CR1=TIM_CR1_CEN;
return 0;
}
set_wave_port set_wave_f407_port(set_wave_f407 *context) {
set_wave_port port={context,start,stop};return port;
}
int set_wave_f407_fault(void) {
return (DMA1->HISR&(DMA_HISR_TEIF5|DMA_HISR_DMEIF5|DMA_HISR_FEIF5))
||(DAC->SR&DAC_SR_DMAUDR1);
}

View File

@@ -0,0 +1,12 @@
#ifndef SET_WAVE_F407_H
#define SET_WAVE_F407_H
#include "../../include/set_wavegen.h"
/* Owns PA4, DAC channel 1, TIM6 and DMA1 stream 5/channel 7.
* timer_hz is TIM6 input clock (twice APB1 when APB1 prescaler !=1).
* Rate: 1..SET_WAVE_MAX_RATE (1 MS/s), exact divisors of timer_hz only.
* Context and sample buffer must outlive playback; buffer must be in SRAM. */
typedef struct { uint32_t timer_hz; } set_wave_f407;
set_wave_port set_wave_f407_port(set_wave_f407 *context);
/* Nonzero on DMA error or DAC underrun; caller stops state on failure. */
int set_wave_f407_fault(void);
#endif

View File

@@ -0,0 +1,37 @@
#include "set_wave_g474.h"
static void stop(void *context) {
set_wave_g474 *c=(set_wave_g474 *)context;
HAL_TIM_Base_Stop(c->timer);
HAL_DAC_Stop_DMA(c->dac,DAC_CHANNEL_1);
/* Disable triggering so zero is applied without another timer edge. */
CLEAR_BIT(c->dac->Instance->CR,DAC_CR_TEN1);
HAL_DAC_SetValue(c->dac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,0);
HAL_DAC_Start(c->dac,DAC_CHANNEL_1);
}
static int start(void *context,const uint16_t *samples,size_t count,uint32_t rate) {
set_wave_g474 *c=(set_wave_g474 *)context;
uint32_t ticks,psc; DMA_HandleTypeDef *dma;
if(!c||!c->dac||!c->timer||c->dac->Instance!=DAC1||c->timer->Instance!=TIM6
||!rate||rate>50000||count<2||count>SET_WAVE_MAX||c->timer_hz%rate) return 1;
dma=c->dac->DMA_Handle1;
if(!dma||dma->Init.Mode!=DMA_CIRCULAR||dma->Init.MemDataAlignment!=DMA_MDATAALIGN_HALFWORD
||dma->Init.PeriphDataAlignment!=DMA_PDATAALIGN_HALFWORD
||dma->Init.Request!=DMA_REQUEST_DAC1_CHANNEL1||dma->Init.MemInc!=DMA_MINC_ENABLE) return 1;
ticks=c->timer_hz/rate;
for(psc=1;psc<=65536;++psc) if(ticks%psc==0&&ticks/psc<=65536) break;
if(psc>65536||ticks<psc) return 1;
stop(context);
__HAL_TIM_SET_PRESCALER(c->timer,psc-1);
__HAL_TIM_SET_AUTORELOAD(c->timer,ticks/psc-1);
__HAL_TIM_SET_COUNTER(c->timer,0);
c->timer->Instance->EGR=TIM_EGR_UG;
__HAL_TIM_CLEAR_FLAG(c->timer,TIM_FLAG_UPDATE);
MODIFY_REG(c->dac->Instance->CR,DAC_CR_TSEL1|DAC_CR_TEN1,DAC_TRIGGER_T6_TRGO);
HAL_DAC_SetValue(c->dac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,samples[count-1]);
if(HAL_DAC_Start_DMA(c->dac,DAC_CHANNEL_1,(uint32_t *)samples,(uint32_t)count,DAC_ALIGN_12B_R)!=HAL_OK) return 1;
if(HAL_TIM_Base_Start(c->timer)!=HAL_OK) {stop(context);return 1;}
return 0;
}
set_wave_port set_wave_g474_port(set_wave_g474 *context) {
set_wave_port port={context,start,stop};return port;
}

View File

@@ -0,0 +1,10 @@
#ifndef SET_WAVE_G474_H
#define SET_WAVE_G474_H
#include "stm32g4xx_hal.h"
#include "../../include/set_wavegen.h"
/* CubeMX owns initialization: DAC1 channel1 PA4, TIM6 TRGO_UPDATE,
* DMA_REQUEST_DAC1_CHANNEL1, circular, halfword/halfword, memory increment.
* Supply TIM6 input clock, not APB bus clock. DMA buffer in SRAM. */
typedef struct { DAC_HandleTypeDef *dac; TIM_HandleTypeDef *timer; uint32_t timer_hz; } set_wave_g474;
set_wave_port set_wave_g474_port(set_wave_g474 *context);
#endif

View File

@@ -0,0 +1,88 @@
#include "firmware_image.h"
#include <stdlib.h>
static int hex_digit(char c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return -1;
}
static int space(char c) { return c == ' ' || c == '\t' || c == '\r'; }
static int compare_cells(const void *a, const void *b)
{
uint32_t x = ((const firmware_hex_cell_t *)a)->address;
uint32_t y = ((const firmware_hex_cell_t *)b)->address;
return (x > y) - (x < y);
}
int firmware_hex_parse(const char *text, size_t length,
firmware_hex_cell_t *cells, size_t capacity, size_t max_records,
size_t *count, size_t *error_line)
{
size_t pos = 0, line = 0, used = 0, records = 0, i;
uint32_t upper = 0;
int eof = 0;
if (count == NULL || error_line == NULL) return FW_HEX_FORMAT;
*count = 0; *error_line = 0;
if (text == NULL || cells == NULL) return FW_HEX_FORMAT;
while (pos < length) {
size_t begin = pos, end, n;
uint8_t bytes[260];
unsigned sum = 0, size, kind, address;
while (pos < length && text[pos] != '\n') ++pos;
end = pos;
if (pos < length) ++pos;
++line;
while (begin < end && space(text[begin])) ++begin;
while (end > begin && space(text[end-1])) --end;
if (begin == end) continue;
*error_line = line;
if (eof) return FW_HEX_EOF;
if (++records > max_records) return FW_HEX_CAPACITY;
if (text[begin++] != ':') return FW_HEX_PREFIX;
if ((end-begin) & 1U) return FW_HEX_DIGITS;
n = (end-begin)/2;
if (n < 5 || n > sizeof(bytes)) return FW_HEX_FORMAT;
for (i = 0; i < n; ++i) {
int hi = hex_digit(text[begin+2*i]), lo = hex_digit(text[begin+2*i+1]);
if (hi < 0 || lo < 0) return FW_HEX_DIGITS;
bytes[i] = (uint8_t)(hi*16+lo); sum += bytes[i];
}
size = bytes[0]; address = ((unsigned)bytes[1]<<8) | bytes[2]; kind = bytes[3];
if (n != size+5U) return FW_HEX_LENGTH;
if (sum & 255U) return FW_HEX_CHECKSUM;
if (kind == 0) {
uint64_t absolute = (uint64_t)upper + address;
if (size && absolute + size - 1 > UINT32_MAX) return FW_HEX_RANGE;
if (size > capacity-used) return FW_HEX_CAPACITY;
for (i = 0; i < size; ++i) {
cells[used].address = (uint32_t)(absolute+i);
cells[used++].value = bytes[4+i];
}
} else if (kind == 1) {
if (size || address) return FW_HEX_RECORD;
eof = 1;
} else if (kind == 2 || kind == 4) {
if (size != 2 || address) return FW_HEX_RECORD;
upper = ((uint32_t)bytes[4]<<8) | bytes[5];
upper <<= kind == 2 ? 4 : 16;
} else if (kind == 3 || kind == 5) {
if (size != 4 || address) return FW_HEX_RECORD;
} else return FW_HEX_RECORD;
}
if (!eof || !used) return FW_HEX_EOF;
qsort(cells, used, sizeof(*cells), compare_cells);
for (i = 1; i < used; ++i)
if (cells[i-1].address == cells[i].address) { *error_line = 0; return FW_HEX_OVERLAP; }
*count = used; *error_line = 0;
return FW_HEX_OK;
}
size_t firmware_hex_segment_size(const firmware_hex_cell_t *cells, size_t count, size_t offset)
{
size_t end;
if (cells == NULL || offset >= count) return 0;
end = offset+1;
while (end < count && cells[end-1].address != UINT32_MAX &&
cells[end].address == cells[end-1].address+1U) ++end;
return end-offset;
}

View File

@@ -0,0 +1,115 @@
#include "set_signal.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
typedef struct { double x, y; } point;
static int compare_point(const void *a, const void *b) {
double x=((const point *)a)->x, y=((const point *)b)->x;
return (x>y)-(x<y);
}
static double edge(double h0,double h1,double d0,double d1) {
double d=((2*h0+h1)*d0-h0*d1)/(h0+h1);
if(d0==0 || (d>0)!=(d0>0)) return 0;
if((d0>0)!=(d1>0) && fabs(d)>3*fabs(d0)) return 3*d0;
return d;
}
static double evaluate(double t,const double *x,const double *y,size_t n,
int method,const double *d,const double *coef,unsigned degree) {
size_t lo=0,hi=n-1,i; double u,a,b,h,value=0;
if(method==SET_SIGNAL_POLYNOMIAL) {
int j; for(j=(int)degree;j>=0;--j) value=value*(2*t-1)+coef[j]; return value;
}
while(hi-lo>1) { i=(hi+lo)/2; if(t<x[i]) hi=i; else lo=i; }
i=lo; h=x[i+1]-x[i]; u=(t-x[i])/h; a=1-u; b=u;
if(method==SET_SIGNAL_LINEAR) return a*y[i]+b*y[i+1];
if(method==SET_SIGNAL_SPLINE)
return a*y[i]+b*y[i+1]+h*h/6*((a*a*a-a)*d[i]+(b*b*b-b)*d[i+1]);
return (2*u*u*u-3*u*u+1)*y[i]+(u*u*u-2*u*u+u)*h*d[i]
+(-2*u*u*u+3*u*u)*y[i+1]+(u*u*u-u*u)*h*d[i+1];
}
int set_signal_reconstruct(const double *input_x,const double *input_y,size_t count,
int method,unsigned degree,size_t output_count,int endpoint,double *out_x,
double *out_y,double *meta,double *work,size_t work_count) {
point *pairs; double *x,*y,*d,*h,*s,*q,*column;
double coef[6]={0},r[6][6]={{0}},origin,span,scale=1,rmse=0;
size_t i,j,k,n=0; unsigned pass;
if(!input_x||!input_y||!out_x||!out_y||!meta||!work || count<2||count>100000
||output_count<2||output_count>10000||work_count<14*count+128
||method<0||method>3||(endpoint!=0&&endpoint!=1)) return 1;
pairs=(point *)work; x=work+2*count; y=x+count; d=y+count; h=d+count;
s=h+count; q=s+count; column=q+6*count;
for(i=0;i<count;++i) {
if(!isfinite(input_x[i])||!isfinite(input_y[i])) return 2;
pairs[i].x=input_x[i]; pairs[i].y=input_y[i];
}
qsort(pairs,count,sizeof(point),compare_point);
for(i=0;i<count;i=j) {
double sum=0; for(j=i+1;j<count&&pairs[j].x==pairs[i].x;++j) {}
for(k=i;k<j;++k) sum+=pairs[k].y/(double)(j-i);
if(!isfinite(sum)) return 2;
x[n]=pairs[i].x; y[n++]=sum; if(fabs(sum)>scale) scale=fabs(sum);
}
if(n<2) return 2;
origin=x[0]; span=x[n-1]-origin;
if(!isfinite(span)||span<=0) return 2;
for(i=0;i<n;++i) { x[i]=(x[i]-origin)/span; y[i]/=scale; d[i]=0; }
for(i=0;i<n-1;++i) {
h[i]=x[i+1]-x[i]; if(h[i]<=0) return 2;
s[i]=(y[i+1]-y[i])/h[i]; if(!isfinite(s[i])) return 2;
}
if(method==SET_SIGNAL_POLYNOMIAL) {
if(degree<1||degree>5||n<=degree) return 1;
for(j=0;j<=degree;++j) {
double norm=0;
for(i=0;i<n;++i) column[i]=pow(2*x[i]-1,(double)j);
for(pass=0;pass<2;++pass) for(k=0;k<j;++k) {
double c=0; for(i=0;i<n;++i) c+=column[i]*q[k*count+i];
r[k][j]+=c; for(i=0;i<n;++i) column[i]-=c*q[k*count+i];
}
for(i=0;i<n;++i) norm+=column[i]*column[i]; norm=sqrt(norm);
if(norm<1e-12*sqrt((double)n)) return 3;
r[j][j]=norm;
for(i=0;i<n;++i) { q[j*count+i]=column[i]/norm; coef[j]+=q[j*count+i]*y[i]; }
}
for(i=degree+1;i>0;--i) {
j=i-1; for(k=j+1;k<=degree;++k) coef[j]-=r[j][k]*coef[k];
coef[j]/=r[j][j];
}
} else if(method==SET_SIGNAL_PCHIP) {
d[0]=d[n-1]=s[0];
if(n>2) {
d[0]=edge(h[0],h[1],s[0],s[1]);
d[n-1]=edge(h[n-2],h[n-3],s[n-2],s[n-3]);
for(i=1;i<n-1;++i) if(s[i-1]!=0&&s[i]!=0&&(s[i-1]>0)==(s[i]>0)) {
double w1=2*h[i]+h[i-1],w2=h[i]+2*h[i-1];
d[i]=(w1+w2)/(w1/s[i-1]+w2/s[i]);
}
}
} else if(method==SET_SIGNAL_SPLINE) {
q[0]=column[0]=0;
for(i=1;i<n-1;++i) {
double diagonal=2*(h[i-1]+h[i])-h[i-1]*q[i-1];
q[i]=h[i]/diagonal;
column[i]=(6*(s[i]-s[i-1])-h[i-1]*column[i-1])/diagonal;
}
for(i=n-2;i>0;--i) d[i]=column[i]-q[i]*d[i+1];
}
for(i=0;i<output_count;++i) {
double t=(double)i/(double)(output_count-(endpoint?1:0));
out_x[i]=origin+span*t; out_y[i]=evaluate(t,x,y,n,method,d,coef,degree)*scale;
if(!isfinite(out_y[i])) return 2;
}
for(i=0;i<count;++i) {
double delta=evaluate((input_x[i]-origin)/span,x,y,n,method,d,coef,degree)-input_y[i]/scale;
rmse+=delta*delta/(double)count;
}
meta[0]=(double)count; meta[1]=(double)n; meta[2]=sqrt(rmse)*scale;
return isfinite(meta[2])?0:2;
}
int set_signal_dac12(const double *volts,size_t count,double vref,uint16_t *codes) {
size_t i;
if(!volts||!codes||count<2||count>10000||!isfinite(vref)||vref<=0) return 1;
for(i=0;i<count;++i) if(!isfinite(volts[i])||volts[i]<0||volts[i]>vref) return 2;
for(i=0;i<count;++i) codes[i]=(uint16_t)floor(volts[i]/vref*4095+0.5);
return 0;
}

View File

@@ -0,0 +1,214 @@
#include "set_wavegen.h"
#include "set_crc.h"
#include <string.h>
static uint16_t read16(const uint8_t *p) {return (uint16_t)((p[0]<<8)|p[1]);}
static void write16(uint8_t *p,unsigned value) {p[0]=(uint8_t)(value>>8);p[1]=(uint8_t)value;}
static size_t seal(uint8_t *p,size_t n) {
uint16_t crc=set_crc16_modbus(p,n);p[n]=(uint8_t)crc;p[n+1]=(uint8_t)(crc>>8);return n+2;
}
void set_wave_init(set_wave_state *s,uint16_t *samples,size_t capacity,const set_wave_port *port) {
memset(s,0,sizeof(*s));s->samples=samples;s->capacity=capacity;s->rate=1000;
if(port) s->port=*port;
}
static unsigned write_register(set_wave_state *s,unsigned reg,unsigned value) {
if(reg==SET_WAVE_BASE+2 && value==0) {
if(s->port.stop) s->port.stop(s->port.context);
s->running=0; return 0;
}
if(s->running) return 6;
if(reg==SET_WAVE_BASE+2 && value==1) {
if(!s->ready||!s->port.start) return 3;
if(s->port.start(s->port.context,s->samples,s->count,s->rate)) return 4;
s->running=1;return 0;
}
if(reg==SET_WAVE_BASE+3) {
uint32_t rate=((uint32_t)s->rate_high<<16)|value;
if(rate<1||rate>SET_WAVE_MAX_RATE) return 3;
s->rate=rate;s->ready=0;return 0;
}
if(reg==SET_WAVE_BASE+8) {
if(value>(SET_WAVE_MAX_RATE>>16)) return 3;
s->rate_high=(uint16_t)value;s->ready=0;return 0;
}
if(reg==SET_WAVE_BASE+4) {
if(value<2||value>SET_WAVE_MAX||value>s->capacity) return 3;
s->count=(uint16_t)value;s->received=s->ready=0;return 0;
}
if(reg>=SET_WAVE_DATA&&reg<SET_WAVE_DATA+SET_WAVE_MAX) {
unsigned index=reg-SET_WAVE_DATA;
if(index>=s->count||index>s->received||value>4095||s->ready) return 3;
if(index<s->received) return s->samples[index]==value?0:3;
s->samples[index]=(uint16_t)value;++s->received;return 0;
}
if(reg==SET_WAVE_BASE+6) {
if(value!=0xA55A||s->count<2||s->received!=s->count) return 3;
s->ready=1;return 0;
}
return 2;
}
unsigned set_wave_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out) {
set_wave_state *s=(set_wave_state *)ctx;uint16_t w[9];unsigned i;
if(!s||!s->samples||!out||!count||(uint32_t)off+count>9)return 2;
w[0]=0x5747;w[1]=3;w[2]=s->running;w[3]=(uint16_t)s->rate;
w[4]=s->count;w[5]=s->received;w[6]=s->ready;w[7]=(uint16_t)s->capacity;w[8]=(uint16_t)(s->rate>>16);
for(i=0;i<count;++i)out[i]=w[off+i];
return 0;
}
unsigned set_wave_write(void *ctx,uint16_t off,uint16_t value) {
set_wave_state *s=(set_wave_state *)ctx;
if(!s||!s->samples||off>=9)return 2;
return write_register(s,SET_WAVE_BASE+off,value);
}
unsigned set_wave_samples_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out) {
set_wave_state *s=(set_wave_state *)ctx;unsigned i;
if(!s||!s->samples||!out||!count||(uint32_t)off+count>s->received)return 2;
for(i=0;i<count;++i)out[i]=s->samples[off+i];
return 0;
}
unsigned set_wave_samples_write(void *ctx,uint16_t off,uint16_t value) {
set_wave_state *s=(set_wave_state *)ctx;
if(!s||!s->samples||off>=SET_WAVE_MAX)return 2;
return write_register(s,SET_WAVE_DATA+off,value);
}
size_t set_wave_rtu(set_wave_state *s,unsigned device,const uint8_t *req,size_t n,uint8_t *out,size_t cap) {
unsigned reg,value,error=0,i;uint16_t status[8];size_t length=0;
if(!s||!s->samples||!req||!out||cap<21||n!=8||device<1||device>247
||req[0]!=device||set_crc16_modbus(req,n)) return 0;
reg=read16(req+2);value=read16(req+4);
if(!((reg>=SET_WAVE_BASE&&reg<SET_WAVE_BASE+9)||(reg>=SET_WAVE_DATA&&reg<SET_WAVE_DATA+SET_WAVE_MAX))) return 0;
out[0]=(uint8_t)device;out[1]=req[1];
if(req[1]==6) {error=write_register(s,reg,value);memcpy(out,req,6);length=6;}
else if(req[1]==3) {
(void)set_wave_read(s,0,8,status);
if(reg==SET_WAVE_BASE&&value==8) {
out[2]=16;for(i=0;i<8;++i) write16(out+3+2*i,status[i]);length=19;
} else if(reg==SET_WAVE_BASE+8&&value==1) {
out[2]=2;write16(out+3,s->rate>>16);length=5;
} else if(reg>=SET_WAVE_DATA&&value==1&&reg-SET_WAVE_DATA<s->received) {
out[2]=2;write16(out+3,s->samples[reg-SET_WAVE_DATA]);length=5;
} else error=2;
} else error=1;
if(error) {out[1]|=0x80;out[2]=(uint8_t)error;length=3;}
return seal(out,length);
}
size_t set_wave_request(unsigned device,unsigned op,unsigned index,unsigned value,uint8_t *out,size_t cap) {
unsigned reg=SET_WAVE_BASE,fn=6;
if(!out||cap<8||device<1||device>247) return 0;
switch(op) {
case 0: fn=3;value=8;break;
case 1: reg+=2;value=0;break;
case 2: reg+=3;if(value>65535)return 0;break;
case 3: reg+=4;if(value<2||value>SET_WAVE_MAX)return 0;break;
case 4: if(index>=SET_WAVE_MAX||value>4095)return 0;reg=SET_WAVE_DATA+index;break;
case 5: reg+=6;value=0xA55A;break;
case 6: reg+=2;value=1;break;
case 7: if(index>=SET_WAVE_MAX)return 0;reg=SET_WAVE_DATA+index;fn=3;value=1;break;
case 8: reg+=8;fn=3;value=1;break;
case 9: reg+=8;if(value>(SET_WAVE_MAX_RATE>>16))return 0;break;
default:return 0;
}
out[0]=(uint8_t)device;out[1]=(uint8_t)fn;write16(out+2,reg);write16(out+4,value);return seal(out,6);
}
int set_wave_response(const uint8_t *req,const uint8_t *reply,size_t size,uint16_t *words,size_t cap) {
size_t expected,i;
if(!req||!reply||!words||set_crc16_modbus(req,8))return -1;
if(size<2)return 0;
if(reply[0]!=req[0])return -1;
if(reply[1]==(req[1]|0x80)) {
if(size<5)return 0;
return size==5&&!set_crc16_modbus(reply,5)?-(100+(int)reply[2]):-1;
}
if(reply[1]!=req[1])return -1;
expected=req[1]==6?8:5+2*(size_t)read16(req+4);
if(size<expected)return 0;
if(size!=expected||set_crc16_modbus(reply,size))return -1;
if(req[1]==6) {if(memcmp(req,reply,8)||cap<1)return -1;words[0]=read16(req+4);return 1;}
if(reply[2]!=expected-5||cap<(expected-5)/2)return -1;
for(i=0;i<(expected-5)/2;++i)words[i]=read16(reply+3+2*i);
if(read16(req+2)==SET_WAVE_BASE&&(words[0]!=0x5747||(words[1]<1||words[1]>3)))return -2;
return (int)((expected-5)/2);
}
size_t set_wave_block_request(unsigned device,unsigned index,const uint16_t *values,
size_t count,uint8_t *out,size_t cap) {
size_t i,n=values?9+2*count:8;
if(!out||device<1||device>247||!count||count>SET_WAVE_BLOCK_MAX||
index>=SET_WAVE_MAX||count>SET_WAVE_MAX-index||cap<n)return 0;
if(values)for(i=0;i<count;++i)if(values[i]>4095)return 0;
out[0]=(uint8_t)device;out[1]=values?16:3;
write16(out+2,SET_WAVE_DATA+index);write16(out+4,(unsigned)count);
if(values) {
out[6]=(uint8_t)(count*2);
for(i=0;i<count;++i)write16(out+7+2*i,values[i]);
}
return seal(out,n-2);
}
size_t set_wave_block_rtu(set_wave_state *s,unsigned device,const uint8_t *req,
size_t n,uint8_t *out,size_t cap) {
unsigned reg,index,count,error=0,i;size_t length=3;
if(!s||!s->samples||!req||!out||cap<5||n<8||device<1||device>247||
req[0]!=device||set_crc16_modbus(req,n))return 0;
reg=read16(req+2);count=read16(req+4);
if(reg<SET_WAVE_DATA||reg>=SET_WAVE_DATA+SET_WAVE_MAX)return 0;
index=reg-SET_WAVE_DATA;out[0]=req[0];out[1]=req[1];
if(!count||count>SET_WAVE_BLOCK_MAX||count>SET_WAVE_MAX-index||
index+count>s->count||index+count>s->capacity)error=3;
else if(req[1]==16) {
if(n!=9+2*(size_t)count||req[6]!=2*count)error=3;
else if(s->running)error=6;
else if(s->ready||index>s->received)error=3;
else {
/* Preflight the whole block, including replayed prefix, before writing. */
for(i=0;i<count;++i) {
unsigned value=read16(req+7+2*i);
if(value>4095||(index+i<s->received&&s->samples[index+i]!=value)) {
error=3;break;
}
}
if(!error) {
if(cap<8)return 0;
for(i=0;i<count;++i)s->samples[index+i]=read16(req+7+2*i);
if(index+count>s->received)s->received=(uint16_t)(index+count);
memcpy(out,req,6);length=6;
}
}
} else if(req[1]==3) {
if(n!=8||index+count>s->received)error=3;
else {
if(cap<5+2*(size_t)count)return 0;
out[2]=(uint8_t)(2*count);
for(i=0;i<count;++i)write16(out+3+2*i,s->samples[index+i]);
length=3+2*(size_t)count;
}
} else error=1;
if(error) {out[1]|=0x80;out[2]=(uint8_t)error;length=3;}
return seal(out,length);
}
int set_wave_block_response(const uint8_t *req,size_t n,const uint8_t *reply,
size_t size,uint16_t *words,size_t cap) {
size_t expected,i;unsigned count;
if(!req||!reply||!words||n<8||set_crc16_modbus(req,n))return -1;
count=read16(req+4);
if(!count||count>SET_WAVE_BLOCK_MAX||read16(req+2)<SET_WAVE_DATA||
(unsigned)read16(req+2)+count>SET_WAVE_DATA+SET_WAVE_MAX)return -1;
if((req[1]==16&&(n!=9+2*(size_t)count||req[6]!=2*count))||
(req[1]==3&&n!=8)||(req[1]!=3&&req[1]!=16))return -1;
if(size<2)return 0;
if(reply[0]!=req[0])return -1;
if(reply[1]==(req[1]|0x80)) {
if(size<5)return 0;
return size==5&&!set_crc16_modbus(reply,size)?-(100+(int)reply[2]):-1;
}
if(reply[1]!=req[1])return -1;
expected=req[1]==16?8:5+2*(size_t)count;
if(size<expected)return 0;
if(size!=expected||set_crc16_modbus(reply,size))return -1;
if(req[1]==16) {
if(cap<1||memcmp(req,reply,6))return -1;
words[0]=(uint16_t)count;return 1;
}
if(cap<count||reply[2]!=2*count)return -1;
for(i=0;i<count;++i)words[i]=read16(reply+3+2*i);
return (int)count;
}

View File

@@ -0,0 +1,22 @@
#include "firmware_image.h"
#include <assert.h>
#include <string.h>
static int parse(const char *text, firmware_hex_cell_t *cells, size_t cap, size_t *count)
{ size_t line; return firmware_hex_parse(text, strlen(text), cells, cap, 100, count, &line); }
int main(void)
{
firmware_hex_cell_t cells[32]; size_t count;
const char *good = ":020000040800F2\n:0400100001020304E2\n:00000001FF\n";
assert(parse(good,cells,32,&count)==0 && count==4);
assert(cells[0].address==0x08000010U && cells[3].value==4);
assert(firmware_hex_segment_size(cells,count,0)==4);
assert(parse(good,cells,3,&count)==FW_HEX_CAPACITY && count==0);
assert(parse(":0100000001FE\n:0100000001FE\n:00000001FF",cells,32,&count)==FW_HEX_OVERLAP);
assert(parse(":0100000001FF\n:00000001FF",cells,32,&count)==FW_HEX_CHECKSUM);
assert(parse(":0100000001FE",cells,32,&count)==FW_HEX_EOF);
assert(parse(":0100000001FE\n:00000001FF\n:0100010002FC",cells,32,&count)==FW_HEX_EOF);
assert(parse(":0100010002FC\n:0100000001FE\n:00000001FF",cells,32,&count)==0);
assert(cells[0].value==1 && cells[1].value==2);
assert(parse(":02000004FFFFFC\n:02FFFF000102FD\n:00000001FF",cells,32,&count)==FW_HEX_RANGE);
return 0;
}

View File

@@ -0,0 +1,51 @@
#include "set_signal.h"
#include "set_wavegen.h"
#include <assert.h>
#include <math.h>
#include <string.h>
static int starts,stops;
static int start(void *ctx,const uint16_t *p,size_t n,uint32_t rate) {
(void)ctx;assert(n==3&&rate==1000&&p[2]==4095);++starts;return 0;
}
static void stop(void *ctx) {(void)ctx;++stops;}
static int exchange(set_wave_state *s,unsigned op,unsigned index,unsigned value,uint16_t *out) {
uint8_t req[8],reply[32];size_t n;
assert(set_wave_request(16,op,index,value,req,8)==8);
n=set_wave_rtu(s,16,req,8,reply,sizeof(reply));assert(n>0);
assert(set_wave_response(req,reply,n-1,out,8)==0);
return set_wave_response(req,reply,n,out,8);
}
int main(void) {
double x[3]={0,1,2},y[3]={0,1,0},ox[5],oy[5],meta[3],work[170];
uint16_t samples[8],out[8],codes[3];set_wave_state state;
set_wave_port port={0,start,stop};uint8_t req[8],reply[32];
assert(!set_signal_reconstruct(x,y,3,SET_SIGNAL_SPLINE,2,5,1,ox,oy,meta,work,170));
assert(fabs(oy[1]-.6875)<1e-12&&fabs(oy[3]-.6875)<1e-12&&meta[2]<1e-12);
y[1]=1.65;y[2]=3.3;assert(!set_signal_dac12(y,3,3.3,codes));
assert(codes[0]==0&&codes[1]==2048&&codes[2]==4095);
y[0]=-1;codes[0]=123;assert(set_signal_dac12(y,3,3.3,codes)&&codes[0]==123);
set_wave_init(&state,samples,8,&port);
assert(exchange(&state,0,0,0,out)==8&&out[0]==0x5747&&out[7]==8);
assert(exchange(&state,6,0,0,out)<0&&starts==0);
assert(exchange(&state,1,0,0,out)==1&&stops==1);
assert(exchange(&state,2,0,1000,out)==1);
assert(exchange(&state,3,0,3,out)==1);
assert(exchange(&state,4,1,10,out)<0); /* no holes */
assert(exchange(&state,4,0,0,out)==1);
assert(exchange(&state,4,0,0,out)==1&&state.received==1); /* idempotent replay */
assert(exchange(&state,4,0,5,out)<0);
assert(exchange(&state,5,0,0,out)<0); /* incomplete upload cannot commit */
assert(exchange(&state,4,1,2048,out)==1);
assert(exchange(&state,4,2,4095,out)==1);
assert(exchange(&state,7,1,0,out)==1&&out[0]==2048);
assert(exchange(&state,5,0,0,out)==1&&state.ready);
assert(exchange(&state,6,0,0,out)==1&&state.running&&starts==1);
assert(exchange(&state,3,0,2,out)<0&&state.count==3);
assert(exchange(&state,4,0,2,out)<0&&samples[0]==0);
set_wave_request(16,1,0,0,req,8);req[7]^=1;
assert(!set_wave_rtu(&state,16,req,8,reply,32)&&state.running);
set_wave_request(17,1,0,0,req,8);
assert(!set_wave_rtu(&state,16,req,8,reply,32)&&state.running);
assert(exchange(&state,1,0,0,out)==1&&!state.running);
return 0;
}

View File

@@ -20,9 +20,9 @@ INCLUDE = ROOT / "include"
JNI_INCLUDES: list[Path] = []
SOURCES = [
ROOT / "src" / name for name in (
"altera_logic.c",
"firmware_image.c", "altera_logic.c",
"altera_stream.c",
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c",
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c", "set_signal.c", "set_wavegen.c",
"balsam_can.c", "set_crc.c", "periph28335.c", "tms2812.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
"pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",
"pcan_gas.c", "pcan_modbus_server.c",