6 Commits

138 changed files with 15109 additions and 100 deletions

3
.gitignore vendored
View File

@@ -24,3 +24,6 @@ __pycache__/
Thumbs.db
Desktop.ini
.codex-build/
# Generated native library (built before packaging)
python/protocan/native/

View File

@@ -33,3 +33,5 @@ if(DS18B20_BUILD_TESTS)
target_include_directories(test_ds18b20 PRIVATE tests)
add_test(NAME ds18b20 COMMAND test_ds18b20)
endif()
add_subdirectory(instance)

View File

@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.13)
project(ds18b20_instance C)
add_library(ds18b20_instance STATIC Src/ds18b20.c)
target_include_directories(ds18b20_instance PUBLIC Inc)
set_target_properties(ds18b20_instance PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED YES)
enable_testing()
foreach(name copy_delay incremental_search)
add_executable(test_ds18b20_${name} Tests/test_ds18b20_${name}.c)
target_link_libraries(test_ds18b20_${name} PRIVATE ds18b20_instance)
add_test(NAME ds18b20_${name} COMMAND test_ds18b20_${name})
endforeach()

384
c/ds18b20/instance/HELP.md Normal file
View File

@@ -0,0 +1,384 @@
# DS18B20: справочник API
## Назначение
Публичный API ядра объявлен только в `Inc/ds18b20.h`. Приложение компилирует
`Src/ds18b20.c`, но не включает этот файл. Библиотека выполняет Search ROM,
проверяет Dallas CRC8, запускает Convert T, читает scratchpad, декодирует
температуру, меняет разрешение и TH/TL.
## Константы и состояние
- `DS18B20_ROM_SIZE` — 8 байт полного 64-битного ROM.
- `DS18B20_SCRATCHPAD_SIZE` — 9 байт scratchpad вместе с CRC.
- `DS18B20_DEFAULT_TIMEOUT_MS` — 750 мс, значение конфигурации по умолчанию.
- `DS18B20_FAMILY_CODE` — `0x28`.
- `ds18b20_t` — состояние одной шины. После инициализации его поля напрямую не
изменяют; список датчиков читают через `ds18b20_count()` и `ds18b20_rom()`.
## Platform callbacks
`ds18b20_onewire_ops_t` связывает переносимое ядро с аппаратурой:
| Callback | Обязателен | Контракт |
| --- | --- | --- |
| `drive_low(context)` | да | Активно притянуть open-drain линию к 0 |
| `release(context)` | да | Отпустить линию; внешний/внутренний pull-up поднимает её |
| `read(context)` | да | Вернуть текущий логический уровень `0` или `1` |
| `delay_us(context, us)` | да | Синхронная задержка с микросекундной точностью |
| `tick_ms(context)` | для `ds18b20_wait` | Монотонный, допускающий uint32 wrap tick |
| `critical_enter(context)` | нет | Начать защиту одного временного слота |
| critical_exit(context) | нет | Восстановить состояние после защиты слота |
| strong_pullup(context, enable) | для parasite Copy | Активно удерживать high и безопасно вернуть open-drain |
Обе функции critical section задаются парой либо обе оставляются `NULL`.
## Коды возврата
| Код | Значение | Значение для приложения |
| --- | ---: | --- |
| `DS18B20_OK` | 0 | Успех или преобразование готово |
| `DS18B20_E_ARGUMENT` | -1 | Неверный указатель, callback, размер или параметр |
| `DS18B20_E_IO` | -2 | Некорректная конфигурация scratchpad |
| `DS18B20_E_NO_DEVICE` | -3 | Нет presence pulse / подходящих устройств |
| `DS18B20_E_CRC` | -4 | CRC ROM или scratchpad не совпал |
| `DS18B20_E_TIMEOUT` | -5 | Истёк timeout блокирующего ожидания |
| `DS18B20_E_BUSY` | -6 | Преобразование ещё не готово |
| `DS18B20_E_CAPACITY` | -7 | Найдено больше ROM, чем помещается в storage |
| DS18B20_E_ROM | -8 | ROM имеет неверный family code либо отклонён адресной операцией |
| DS18B20_E_POWER | -9 | Parasite Copy запрошен без strong-pull-up callback |
## Инициализация и поиск
### `ds18b20_init`
```c
ds18b20_status_t ds18b20_init(
ds18b20_t *instance,
const ds18b20_onewire_ops_t *ops,
void *platform_context,
uint8_t (*rom_storage)[DS18B20_ROM_SIZE],
size_t rom_capacity);
```
Обнуляет состояние, сохраняет callback/context/storage и отпускает линию.
`rom_storage` — массив приложения, `rom_capacity` — число ROM, не число байт.
Core не выделяет и не освобождает память.
### `ds18b20_search`
```c
ds18b20_status_t ds18b20_search(ds18b20_t *instance);
```
Заново выполняет Search ROM и заменяет прежний список. Сохраняются только ROM с
family `0x28` и корректным CRC. Результаты поиска:
- `DS18B20_OK` — найден минимум один корректный DS18B20;
- `DS18B20_E_NO_DEVICE` — корректные DS18B20 не найдены;
- `DS18B20_E_CAPACITY` — storage заполнен; уже записанные ROM остаются доступны.
### `ds18b20_count` и `ds18b20_rom`
```c
size_t ds18b20_count(const ds18b20_t *instance);
const uint8_t *ds18b20_rom(const ds18b20_t *instance, size_t index);
```
`count` возвращает число сохранённых ROM либо 0 для неверного экземпляра.
`rom` возвращает указатель на 8 байт либо `NULL` для неверного индекса.
Указатель становится логически устаревшим после следующего поиска.
## Преобразование и чтение
### `ds18b20_start_all`
```c
ds18b20_status_t ds18b20_start_all(ds18b20_t *instance);
```
Посылает `Skip ROM + Convert T` всем устройствам шины. Возвращает
`DS18B20_E_NO_DEVICE`, если нет presence pulse.
### `ds18b20_start`
```c
ds18b20_status_t ds18b20_start(
ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE]);
```
Проверяет ROM и посылает `Match ROM + Convert T` одному датчику.
### `ds18b20_conversion_ready`
```c
ds18b20_status_t ds18b20_conversion_ready(ds18b20_t *instance);
```
Один раз читает 1-Wire ready bit: `DS18B20_OK` означает готовность,
`DS18B20_E_BUSY` — преобразование продолжается. Это предпочтительная
неблокирующая проверка для main loop/RTOS.
### `ds18b20_wait`
```c
ds18b20_status_t ds18b20_wait(
ds18b20_t *instance, uint32_t timeout_ms);
```
Блокирующе опрашивает ready bit до готовности или `DS18B20_E_TIMEOUT`. Требует
`tick_ms`. Функция не делает sleep/yield и не рекомендуется в основном цикле.
### `ds18b20_read_scratchpad`
```c
ds18b20_status_t ds18b20_read_scratchpad(
ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE],
uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE]);
```
Проверяет ROM, посылает `Match ROM + Read Scratchpad`, читает 9 байт и
проверяет CRC. Функция не запускает Convert T и не проверяет, что преобразование
ранее завершилось.
### `ds18b20_decode_temperature`
```c
ds18b20_status_t ds18b20_decode_temperature(
const uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE],
float *temperature_c);
```
Проверяет CRC, маскирует неопределённые младшие биты согласно разрешению 9–12
бит и возвращает градусы Цельсия. Неизвестная комбинация configuration bits
даёт `DS18B20_E_IO`.
## Конфигурация и User Bytes
### `ds18b20_set_resolution`
```c
ds18b20_status_t ds18b20_set_resolution(
ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE],
uint8_t bits);
```
`bits` принимает только `9`, `10`, `11` или `12`. Функция читает
scratchpad, сохраняет TH/TL, записывает новый configuration byte и посылает
`Copy Scratchpad`.
### `ds18b20_write_user_bytes`
```c
ds18b20_status_t ds18b20_write_user_bytes(
ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE],
int16_t bytes12,
int16_t bytes34,
uint8_t mask);
```
Текущий контракт отражает физические writable bytes DS18B20:
- `mask & 0x01` записывает младшие 8 бит `bytes12` в TH, scratchpad[2];
- `mask & 0x02` записывает старшие 8 бит `bytes12` в TL, scratchpad[3];
- `bytes34` зарезервирован и не используется: scratchpad[6]/[7] read-only;
- остальные биты `mask` игнорируются.
Перед записью функция читает scratchpad, поэтому невыбранный TH/TL и
configuration byte сохраняются. Затем выполняются `Write Scratchpad` и
`Copy Scratchpad`. Функция не выполняет no-op detection, Recall E2 и readback.
Application adapter обязан добавить эти шаги, если операция используется как
подтверждённая пользовательская запись.
## CRC и ROM
```c
uint8_t ds18b20_crc8(const void *data, size_t length);
ds18b20_status_t ds18b20_validate_rom(
const uint8_t rom[DS18B20_ROM_SIZE]);
```
`crc8` вычисляет Dallas/Maxim CRC-8. `validate_rom` отдельно проверяет family
`0x28` и CRC первых семи байтов против восьмого.
## Неблокирующий пример опроса
```c
enum poll_state { POLL_START, POLL_WAIT };
static enum poll_state state = POLL_START;
static uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE];
void poll_ds18b20(void)
{
if (state == POLL_START) {
if (ds18b20_start_all(&bus) == DS18B20_OK) {
state = POLL_WAIT;
}
return;
}
if (ds18b20_conversion_ready(&bus) != DS18B20_OK) {
return;
}
for (size_t i = 0; i < ds18b20_count(&bus); ++i) {
const uint8_t *rom = ds18b20_rom(&bus, i);
float temperature_c;
if (ds18b20_read_scratchpad(&bus, rom, scratchpad) == DS18B20_OK &&
ds18b20_decode_temperature(scratchpad, &temperature_c) ==
DS18B20_OK) {
publish_temperature(rom, temperature_c);
}
}
state = POLL_START;
}
```
Production adapter должен дополнительно иметь deadline: постоянный
`DS18B20_E_BUSY` не должен навсегда удерживать state machine.
## Диагностика
| Симптом | Проверка |
| --- | --- |
| `DS18B20_E_NO_DEVICE` | Питание, общий GND, pull-up, pin, presence pulse |
| Постоянный `DS18B20_E_BUSY` | Deadline, питание датчика, конфликт операций |
| `DS18B20_E_CRC` | Длина/топология шины, помехи, timing, pull-up |
| `DS18B20_E_CAPACITY` | Увеличить ROM storage или ограничить число устройств |
| Неверная температура | Не читать до ready; проверить CRC и resolution bits |
| Зависание в delay | Таймер должен быть запущен и считать непрерывно |
## Ограничения
- Вызовы одной шины не reentrant и не ISR-safe.
- Микросекундные 1-Wire-слоты синхронные; длительное преобразование должно быть
вынесено в state machine.
- Callback `strong_pullup` используется модулем UserByte для parasite-powered
`Copy Scratchpad`; аппаратная схема и timing должны быть проверены на плате.
- Low-level `Copy Scratchpad` намеренно разделён на `start/finish`; выдержку
10 ms, Recall E2, CRC/readback и освобождение шины обеспечивает state machine
`Modules/UserByte`, а не одиночный low-level вызов.
- Поиск синхронный и не имеет cancel callback; для большой шины или жёстких
realtime-требований нужен пошаговый автомат поиска.
## Тесты
Из корня репозитория:
```powershell
python -m unittest Libraries.PortableTests.test_portable_models
```
Модель проверяет CRC, независимость экземпляров и неблокирующую интеграцию.
`Libraries/PortableTests/test_portable_libraries.c` дополнительно проверяет C
API CRC, декодирование температуры и независимость двух шин. Для полной задачи
также обязательны `git diff --check` и целевая Keil-сборка `0/0`.
# User Byte и EEPROM
Для новой логики приложения используйте внутренний модуль `Modules/UserByte`
с явным selector `TH/TL`, а не legacy `ds18b20_write_user_bytes`. Low-level API `write_scratchpad`, `copy_*`,
`recall_e2`, `recall_ready` и `recover_bus` предназначен для его state machine.
`strong_pullup` обязателен только для parasite-powered Copy Scratchpad.
`ds18b20_search_retry_due` помогает приложению повторять поиск при `count=0`,
не вмешиваясь в активную температурную конверсию; период и tick задаёт adapter.
## Версия Modbus-контракта User Byte
### Явный неблокирующий поиск
```c
ds18b20_status_t status = ds18b20_search_begin(&bus);
while (status == DS18B20_E_BUSY ||
status == DS18B20_E_CRC ||
status == DS18B20_E_ROM) {
status = ds18b20_search_step(&bus);
/* Между шагами основной цикл продолжает обслуживать Modbus/RTC/SD. */
}
```
Не вызывайте `ds18b20_search_retry_due()` для GUI-команды: reconnect и polling
не должны автоматически менять таблицу ROM. В проектном адаптере команда
захватывается только по `apply=1`, а `sequence` защищает от старого ответа.
Legacy API `ds18b20_user_byte_submit()` остаётся TH-only. Для явного TL
используется selector `DS18B20_USER_BYTE_TL`; STM32 adapter принимает его только
с `contractVersion=2`, записанным атомарно с selector до APPLY. Это защищает GUI
от старого bridge/firmware, которое всегда маршрутизировало запрос как TH.
### Известные ROM и ошибка поиска detail 2
В F407 сохранённые SensorBindings восстанавливаются в список опроса после
AppStorage_Init. Если список непустой, стартовый SEARCH ROM не запускается.
Температура читается адресно через MATCH ROM; запуск преобразования общий.
Даже при отсутствии датчика во время старта его адрес остаётся доступен для
повторного чтения. Подключение подтверждается успешным чтением температуры.
Явный поиск добавляет новые ID. При успехе, ошибке, тайм-ауте и отмене
Dallas_FinishSearch объединяет результат с прежними ID, включая ещё не
сохранённые во Flash. Ёмкость списка — 32 ID; найденные ID занимают места
первыми. Сохранение использует существующий SensorBindings_SyncFound и
повтор при BUSY. Отключать питание следует после завершения сохранения.
MCU detail 2 = DS18B20_E_IO: поиск ROM получил недопустимую комбинацию
битов либо неполный ROM. Это не доказательство конкретной причины на кабеле.
Ошибка поиска остаётся видна, но известные ID продолжают опрашиваться.
Число в каталоге включает известные адреса; наличие определяется connected.
API ds18b20_add_known_rom проверяет family/CRC, возвращает E_ARGUMENT,
E_ROM, E_CRC или E_CAPACITY; дубликат возвращает OK. Функция не делает
I/O и не подтверждает наличие датчика. Вызывать вне поиска и конверсии.
Проверки: `Libraries/DS18B20/Tests/run_host_tests.ps1` и
`python -m unittest discover -s tests -p test_known_rom_host.py`.
Для аппаратной проверки: найти датчики по одному, дождаться сохранения,
подключить все на 20 м, перезапустить МК без команды поиска и проверить
обновление температур; затем повторить при ошибке/отмене поиска.
### Поиск с восстановлением после ошибок
Настройки в `Inc/ds18b20_config.h`: три прохода дерева, четыре дополнительных
повтора каждой неудачной ветки (пять попыток суммарно). Один вызов step делает
не более одной попытки ROM: Modbus и остальные сервисы работают между ними.
Перед попыткой сохраняются ROM-путь и discrepancy; после IO, отсутствия presence
или CRC они восстанавливаются. После пяти ошибок начинается следующий проход.
Результаты всех проходов объединяются; дубликаты не занимают ёмкость. CRC
проверяется до принятия пути. Чужое семейство с корректным CRC пропускается.
Успех означает, что хотя бы один проход полностью обошёл дерево и найден хотя
бы один DS18B20. Ранее восстановленные ошибки сохраняются в диагностике, но
не превращают успешный поиск в CRC_ERROR. Это не гарантия обнаружения каждого
физического датчика на нестабильной линии. Если ни один проход не завершён,
возвращается последняя ошибка; проверенные частичные результаты сохраняются.
Пустая линия ограничена 15 попытками reset. Общий предел 512 попыток защищает
и блокирующий API; приложение дополнительно ограничивает поиск 12000 мс.
Во время поиска после каждого слота добавляется 20 мкс высокого уровня:
после записи нуля получается минимум 30 мкс вместо 10 мкс. Импульсы 6/60 мкс,
read-init 3 мкс и выборка через дополнительные 10 мкс остаются прежними.
Добавочная пауза выполняется с разрешёнными прерываниями. Обычное чтение,
конверсия и Copy Scratchpad не получают эту добавку. Подбор паузы для кабеля
требует измерений; программная модель не подтверждает аналоговый фронт.
`bus.search_diagnostics` содержит attempts, retries, crc_errors, io_errors,
passes_finished, complete_passes и последнее место ошибки. Номер бита 1..64;
0 означает reset/общий лимит, pair=0xFF — пары нет. Диагностика сохраняется
после последующего успеха и обнуляется новым search_begin.
FC04: 1210–1217 совместимы, 1218 и 1219 используют прежний резерв:
- 1218: биты 0..6 — номер ROM-бита, бит 8 — complement, бит 9 — id,
биты 12..15 — положительный код последней ошибки; 0 — ошибок не было.
- 1219: биты 0..7 — число повторов с насыщением 255, биты 8..15 — число
законченных проходов, включая прерванные после исчерпания повторов.
Bridge читает 10 регистров и возвращает `diagnostics` в ответе поиска.
Для ошибки шины сообщение дополнено битом, id/complement, повторами и проходами.
Старые восемь полей сохранены. При bit=0 пара не интерпретируется.
Проверки: `run_host_tests.ps1`, `test_known_rom_host.py`,
`test_sensor_search_diagnostics_host.py`, `test_remote_ds18b20_host.py`.
На плате проверить 20 последовательных поисков трёх датчиков на 20 м,
отключение/подключение датчика при поиске, отмену и восстановление опроса.

View File

@@ -0,0 +1,126 @@
#ifndef PORTABLE_DS18B20_H
#define PORTABLE_DS18B20_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define DS18B20_ROM_SIZE 8U
#define DS18B20_SCRATCHPAD_SIZE 9U
typedef enum {
DS18B20_OK = 0,
DS18B20_E_ARGUMENT = -1,
DS18B20_E_IO = -2,
DS18B20_E_NO_DEVICE = -3,
DS18B20_E_CRC = -4,
DS18B20_E_TIMEOUT = -5,
DS18B20_E_BUSY = -6,
DS18B20_E_CAPACITY = -7,
DS18B20_E_ROM = -8,
DS18B20_E_POWER = -9
} ds18b20_status_t;
typedef struct {
void (*drive_low)(void *context);
void (*release)(void *context);
uint8_t (*read)(void *context);
void (*delay_us)(void *context, uint32_t us);
uint32_t (*tick_ms)(void *context);
void (*critical_enter)(void *context);
void (*critical_exit)(void *context);
/* Optional callback used only while parasite-powered EEPROM is copied. */
void (*strong_pullup)(void *context, uint8_t enable);
} ds18b20_onewire_ops_t;
typedef struct {
uint16_t attempts;
uint16_t retries;
uint16_t crc_errors;
uint16_t io_errors;
uint8_t passes_finished;
uint8_t complete_passes;
uint8_t last_error_bit; /* 1..64; 0 = reset or overall attempt limit. */
uint8_t last_error_pair; /* bit 1 = id, bit 0 = complement; 0xFF = no pair. */
ds18b20_status_t last_error;
} ds18b20_search_diagnostics_t;
typedef struct {
const ds18b20_onewire_ops_t *ops;
void *platform_context;
uint8_t (*roms)[DS18B20_ROM_SIZE];
size_t rom_capacity;
size_t rom_count;
uint8_t search_rom[DS18B20_ROM_SIZE];
uint8_t last_discrepancy;
uint8_t last_family_discrepancy;
uint8_t last_device;
uint8_t strong_pullup_active;
uint8_t search_active;
uint8_t search_retries;
ds18b20_status_t search_result;
ds18b20_search_diagnostics_t search_diagnostics;
} ds18b20_t;
ds18b20_status_t ds18b20_init(ds18b20_t *instance,
const ds18b20_onewire_ops_t *ops,
void *platform_context,
uint8_t (*rom_storage)[DS18B20_ROM_SIZE],
size_t rom_capacity);
ds18b20_status_t ds18b20_search(ds18b20_t *instance);
/* Incremental search keeps application services responsive by processing one
* physical ROM attempt per call. Begin clears the previous result table.
* BUSY includes internal retries/passes. All other results are terminal;
* a failed search may still contain validated partial results. */
ds18b20_status_t ds18b20_search_begin(ds18b20_t *instance);
ds18b20_status_t ds18b20_search_step(ds18b20_t *instance);
/* Add a validated known ROM without bus traffic; duplicates are idempotent.
* Call only outside an active search/conversion. Presence is checked by reads. */
ds18b20_status_t ds18b20_add_known_rom(ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE]);
size_t ds18b20_count(const ds18b20_t *instance);
const uint8_t *ds18b20_rom(const ds18b20_t *instance, size_t index);
uint8_t ds18b20_crc8(const void *data, size_t length);
ds18b20_status_t ds18b20_validate_rom(const uint8_t rom[DS18B20_ROM_SIZE]);
ds18b20_status_t ds18b20_start_all(ds18b20_t *instance);
ds18b20_status_t ds18b20_start(ds18b20_t *instance,
const uint8_t rom[DS18B20_ROM_SIZE]);
ds18b20_status_t ds18b20_conversion_ready(ds18b20_t *instance);
ds18b20_status_t ds18b20_wait(ds18b20_t *instance, uint32_t timeout_ms);
ds18b20_status_t ds18b20_read_scratchpad(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE],
uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE]);
ds18b20_status_t ds18b20_decode_temperature(
const uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE], float *temperature_c);
ds18b20_status_t ds18b20_set_resolution(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE], uint8_t bits);
ds18b20_status_t ds18b20_write_user_bytes(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE],
int16_t bytes12, int16_t bytes34, uint8_t mask);
ds18b20_status_t ds18b20_write_scratchpad(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE],
uint8_t th, uint8_t tl, uint8_t configuration);
ds18b20_status_t ds18b20_copy_scratchpad_start(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE],
uint8_t parasite_power);
ds18b20_status_t ds18b20_copy_scratchpad_finish(
ds18b20_t *instance, uint8_t parasite_power);
ds18b20_status_t ds18b20_recall_e2(
ds18b20_t *instance, const uint8_t rom[DS18B20_ROM_SIZE]);
ds18b20_status_t ds18b20_recall_ready(ds18b20_t *instance);
void ds18b20_recover_bus(ds18b20_t *instance);
uint8_t ds18b20_search_retry_due(const ds18b20_t *instance,
uint8_t conversion_active,
uint32_t now_ms, uint32_t last_attempt_ms,
uint32_t retry_period_ms);
/* Экземпляр не синхронизируется внутри: один вызов на одной шине должен быть
* завершён до следующего. Из ISR API вызывать нельзя из-за задержек до 750 ms. */
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,17 @@
#ifndef PORTABLE_DS18B20_CONFIG_H
#define PORTABLE_DS18B20_CONFIG_H
#define DS18B20_DEFAULT_TIMEOUT_MS 750U
#define DS18B20_FAMILY_CODE 0x28U
/* Initial attempt + four retries for each tree branch, three full passes. */
#define DS18B20_SEARCH_RETRIES 4U
#define DS18B20_SEARCH_PASSES 3U
/* Bounds the blocking API too, even if noisy devices keep changing the tree. */
#define DS18B20_SEARCH_MAX_ATTEMPTS 512U
/* Extra released-high time AFTER each search slot; sampling is unchanged. */
#define DS18B20_SLOT_RECOVERY_EXTRA_US 20U
#if DS18B20_SEARCH_PASSES < 1U || DS18B20_SEARCH_PASSES > 255U || \
DS18B20_SEARCH_RETRIES > 254U || DS18B20_SEARCH_MAX_ATTEMPTS < 1U || \
DS18B20_SEARCH_MAX_ATTEMPTS > 65535U
#error Invalid DS18B20 search limits
#endif
#endif

View File

@@ -0,0 +1,235 @@
# DS18B20: перенос на другую платформу
## Что переносится без изменений
`Inc/ds18b20.h`, `Inc/ds18b20_config.h` и `Src/ds18b20.c` — переносимое
C-ядро. Оно использует `stdint.h`, `stddef.h`, `string.h` и аппаратные
callback. Не добавляйте в core HAL, RTOS, Modbus или глобальные дескрипторы
конкретного проекта.
## Шаг 1. Добавить файлы в сборку
Добавьте `Libraries/DS18B20/Inc` в include path и
`Libraries/DS18B20/Src/ds18b20.c` в список исходников. Создайте отдельные
каталоги `Port/<PLATFORM>/Inc` и `Port/<PLATFORM>/Src`.
Application adapter держите вне библиотеки либо в отдельном `Adapter`:
он планирует операции, переводит ошибки и связывает результат с остальным
проектом.
## Шаг 2. Создать platform context
Context содержит только ресурсы одного физического 1-Wire master:
```c
typedef struct {
gpio_handle_t gpio;
timer_handle_t timer;
uint32_t timer_ticks_per_us;
irq_state_t saved_irq_state;
} ds18b20_my_mcu_t;
```
Не используйте скрытый изменяемый singleton. Для двух шин создаются два context,
два `ds18b20_t` и два массива ROM.
## Шаг 3. Реализовать callbacks
```c
static void drive_low(void *context);
static void release_line(void *context);
static uint8_t read_line(void *context);
static void delay_us(void *context, uint32_t us);
static uint32_t tick_ms(void *context);
static void critical_enter(void *context);
static void critical_exit(void *context);
const ds18b20_onewire_ops_t ds18b20_my_mcu_ops = {
drive_low, release_line, read_line, delay_us, tick_ms,
critical_enter, critical_exit
};
```
### GPIO
- Линия 1-Wire работает только как open-drain: порт либо тянет её к 0, либо
переходит в высокоимпедансное состояние.
- Запрещён push-pull высокий уровень.
- `release_line` не должна ждать; `read_line` читает реальный pin level.
- Номинал внешнего pull-up и допустимая длина/ёмкость шины выбираются по
электрическим условиям конкретной платы.
STM32F4 port оставляет pin в `GPIO_MODE_OUTPUT_OD`, отпускает линию записью
единицы в BSRR и читает IDR без переключения MODER.
### Микросекундная задержка
`delay_us` обязана быть монотонной и достаточно точной для 1-Wire standard
speed. Не используйте scheduler sleep с миллисекундной гранулярностью. Если
задержка основана на hardware timer:
- таймер запускается до инициализации библиотеки;
- он считает непрерывно во всех вызывающих контекстах;
- учитывается переполнение счётчика;
- произведение `us * timer_ticks_per_us` не должно переполняться в диапазоне
используемых библиотекой задержек;
- частота таймера и `timer_ticks_per_us` должны совпадать.
### Tick и критическая секция
`tick_ms` нужен только для `ds18b20_wait()`; state-machine adapter может не
использовать блокирующий wait. Вычитание tick выполняется как `uint32_t` и
допускает wrap.
Critical callbacks должны сохранять и восстанавливать предыдущее состояние
прерываний, а не безусловно включать их. Если платформа гарантирует timing иначе,
оба callback можно оставить `NULL`.
## Шаг 4. Инициализировать экземпляр
```c
#define DS_CAPACITY 8U
static ds18b20_my_mcu_t port_context;
static ds18b20_t bus;
static uint8_t rom_storage[DS_CAPACITY][DS18B20_ROM_SIZE];
platform_gpio_timer_init(&port_context);
ds18b20_status_t status =
ds18b20_init(&bus, &ds18b20_my_mcu_ops, &port_context,
rom_storage, DS_CAPACITY);
```
Все три объекта должны жить столько же, сколько используется `bus`. Стековый
context или ROM storage нельзя передавать экземпляру, переживающему функцию.
## Шаг 5. Создать application adapter
Adapter должен:
1. выполнить явный поиск или восстановить/проверить известные ROM;
2. сериализовать Search ROM, Convert T, scratchpad и EEPROM-команды;
3. запустить Convert T и вернуть управление;
4. опрашивать ready с общим deadline;
5. читать каждый ROM и публиковать значение только после CRC;
6. восстановить state machine после disconnect, CRC error и timeout;
7. не запускать поиск/EEPROM-запись из обычного temperature polling;
8. синхронизировать доступ задач RTOS mutex-ом на уровне экземпляра.
Для циклического приложения вызывайте `ds18b20_search_step()` только когда
шина не занята преобразованием температуры или User Byte. Храните timeout,
отмену и sequence в адаптере приложения: portable core не зависит от HAL,
Modbus, GUI и глобального `hdallas`.
Не используйте critical callbacks библиотеки как mutex: они защищают короткий
1-Wire slot и могут запрещать прерывания.
## Память и выравнивание
- Core не использует heap.
- На каждую шину требуется `sizeof(ds18b20_t)` плюс
`rom_capacity * DS18B20_ROM_SIZE` байт ROM storage.
- Scratchpad — 9 байт у вызывающей стороны.
- Специального DMA-выравнивания core не требует; соблюдайте обычное выравнивание
C-типов для `ds18b20_t` и context.
- `rom_capacity` измеряется в элементах `uint8_t[8]`.
## Timing и питание
DS18B20 с внешним питанием может сигнализировать готовность через read slot.
Для parasite power требуется strong pull-up на всё время Convert T и
Copy Scratchpad. Контракт предоставляет optional callback
`strong_pullup(context, enable)`. Для поддержки режима порт обязан:
1. включить активный high не позднее 10 мкс после команды Copy Scratchpad;
2. удерживать его не менее 10 ms без другой активности 1-Wire;
3. безопасно отключать strong pull-up при success, timeout и error;
4. возвращать линию в open-drain idle перед Recall/readback;
5. проверить обычное и parasite-powered подключение на реальной плате.
## Перенос STM32F4 HAL port
Перед `ds18b20_stm32f4_hal_init()` заполните:
| Поле | Требование |
| --- | --- |
| `port` | Валидный `GPIO_TypeDef *` с включённым clock |
| `pin` | Одна ненулевая GPIO mask |
| `timer` | Запущенный свободно работающий `TIM_TypeDef *` |
| `timer_ticks_per_us` | Ненулевое число timer ticks за 1 мкс |
Порт использует `HAL_GetTick()` и CMSIS PRIMASK. При переносе на другую STM32
семью проверьте HAL-заголовок, разрядность/частоту timer, GPIO BSRR/IDR и способ
сохранения interrupt state.
## Проверки
Host/mock-проверки выполняются без платы:
```powershell
python -m unittest Libraries.PortableTests.test_portable_models
git diff --check
```
Для нового порта добавьте тесты reset/presence, write/read slots, timing bounds,
CRC error, нескольких экземпляров, timeout и восстановления после ошибки.
Затем выполните целевую сборку без ошибок/предупреждений.
## Перенос User Byte mailbox adapter
Core-модуль не зависит от Modbus. При переносе STM32 adapter сохраните
`selector + contractVersion=2` как одну транзакцию и APPLY как отдельную
последнюю запись. Input обязан возвращать selector/version echo. Legacy version
`0` можно принимать только для TH; TL без version `2` должен завершаться
`invalid`, а не значением TH.
Host-проверки adapter:
```powershell
powershell -ExecutionPolicy Bypass -File Modules/UserByte/Adapter/STM32_Modbus/Tests/run_host_tests.ps1
```
## Checklist
- [ ] Core собирается без HAL/RTOS/Modbus include.
- [ ] У каждой шины отдельные instance, context и ROM storage.
- [ ] GPIO физически open-drain и никогда не выдаёт push-pull high.
- [ ] Есть корректный внешний pull-up и общий GND.
- [ ] Таймер запущен, частота и overflow проверены.
- [ ] Critical section восстанавливает предыдущее состояние.
- [ ] Поиск, conversion, scratchpad и EEPROM сериализованы.
- [ ] У state machine есть deadline и восстановление после ошибок.
- [ ] CRC ROM и scratchpad проверяется до публикации данных.
- [ ] Host/mock-тесты и `git diff --check` проходят.
- [ ] Целевая сборка даёт 0 ошибок и 0 предупреждений.
- [ ] Работа проверена на реальной шине с 0, 1 и несколькими датчиками.
- [ ] Parasite power отмечен неподдерживаемым либо проверен со strong pull-up.
# Strong pull-up
Если порт поддерживает parasite power, добавьте callback
`strong_pullup(context, enable)`: включение должно немедленно активно держать
high после Copy Scratchpad, выключение — вернуть open-drain idle. Без безопасной
аппаратной реализации не разрешайте parasite mode вызывающему приложению.
### Импорт известных адресов
После ds18b20_init передайте каждый сохранённый ROM в
ds18b20_add_known_rom. Проверяйте возвращаемый статус и лимит ёмкости.
Хранилище ROM принадлежит вызывающему коду и живёт всё время работы шины.
Импорт не требует GPIO-транзакций и не заменяет проверку CRC температуры.
Сохраняйте список перед search_begin (он очищает результаты); по завершении
или отмене добавляйте прежние ID обратно вне активной конверсии.
Для нескольких шин храните снимки и происхождение ID отдельно для каждой.
### Перенос поиска с повторами
Пересоберите всех потребителей: ds18b20_t расширен состоянием поиска и
диагностикой. Рабочие буферы остаются caller-owned, HAL и Flash в ядре не нужны.
Только BUSY означает продолжение; остальные результаты терминальные.
Держите одного владельца шины до завершения/отмены поиска. Адаптер при отмене
сбрасывает search_active перед адресным опросом. Search begin нельзя запускать
во время strong pull-up. Пауза recovery действует только при search_active.
Новые callbacks не требуются; delay_us должен поддерживать добавочные 20 мкс.
Проверьте пределы config и внешний deadline. Для Modbus переносите два поля
диагностики лишь в свободные регистры, не сдвигая последующие окна.

View File

@@ -0,0 +1,97 @@
# Portable DS18B20
Переносимое ядро для поиска DS18B20 на шине 1-Wire, запуска преобразования,
чтения температуры, настройки разрешения и записи alarm/user bytes. Ядро не
зависит от STM32 HAL, не выделяет память динамически и поддерживает несколько
независимых экземпляров шин.
Подтверждённые неблокирующие операции `TH`/`TL` с Copy/Recall/readback находятся
во внутреннем модуле `Libraries/DS18B20/Modules/UserByte`. Основной core предоставляет ему
низкоуровневые операции scratchpad и optional callback `strong_pullup`.
## Подтверждённые EEPROM-байты
Официальный [datasheet Analog Devices/Maxim DS18B20](https://www.analog.com/media/en/technical-documentation/data-sheets/DS18B20.pdf)
определяет `TH=scratchpad[2]` и `TL=scratchpad[3]` как два независимо
программируемых alarm-регистра. `Write Scratchpad` принимает TH, TL и
configuration (`scratchpad[4]`), `Copy Scratchpad` сохраняет все три байта в
EEPROM, а `Recall E2` возвращает их в scratchpad. Заводские значения после
сброса: TH `+75` (`0x4B`), TL `+70` (`0x46`), configuration `0x7F`.
Scratchpad[5..7] зарезервированы/read-only и пользовательскими не считаются.
На реальном датчике пользователь отдельно прочитал TH `14`, TL `128` и config
`31`, подтвердив, что поля GUI должны оставаться независимыми. Проверка
сохранения после полного power-cycle и parasite-power всё ещё требует отдельной
аппаратной приёмки.
## Документация
- [HELP.md](HELP.md) — публичный API, инициализация, примеры, коды ошибок,
диагностика, ограничения и тесты.
- [PORTING.md](PORTING.md) — перенос на другой MCU/проект и checklist порта.
- [PROJECT_RELATIONS.md](PROJECT_RELATIONS.md) — слои, зависимости, владение
памятью и связи с текущей прошивкой.
## Структура
```text
DS18B20/
├── Inc/ публичный API и конфигурация
├── Src/ переносимое ядро 1-Wire/DS18B20
├── Port/STM32F4_HAL/Inc/ публичный API STM32F4-порта
├── Port/STM32F4_HAL/Src/ реализация GPIO/таймера STM32F4
├── README.md точка входа
├── HELP.md справочник API
├── PORTING.md руководство по переносу
└── PROJECT_RELATIONS.md место библиотеки в проекте
```
Адаптер текущего приложения находится отдельно:
`climate_control_f407vet6_f4/Core/Src/dallas_tools.c`.
## Минимальное подключение
```c
#include ds18b20.h
#include ds18b20_stm32f4_hal.h
#define DS_CAPACITY 8U
static ds18b20_t bus;
static uint8_t roms[DS_CAPACITY][DS18B20_ROM_SIZE];
static ds18b20_stm32f4_hal_t port = {
.port = GPIOE, .pin = GPIO_PIN_2,
.timer = TIM2, .timer_ticks_per_us = 72U
};
if (ds18b20_stm32f4_hal_init(&port) == DS18B20_OK &&
ds18b20_init(&bus, &ds18b20_stm32f4_hal_ops, &port,
roms, DS_CAPACITY) == DS18B20_OK) {
(void)ds18b20_search(&bus);
}
```
### Пошаговый поиск для GUI и Modbus
Для приложения с постоянно работающими сервисами используйте
`ds18b20_search_begin()` и `ds18b20_search_step()`. Один вызов `step`
обрабатывает не более одного кандидата ROM. `DS18B20_E_BUSY` означает, что
нужно вызвать функцию в следующем проходе цикла; `DS18B20_OK` завершает поиск.
`DS18B20_E_BUSY` также включает внутренние повторы и переходы между проходами.
Все остальные статусы терминальные; CRC_ERROR после исчерпания повторов
нельзя продолжать вызывать в цикле. Подробности — в [HELP.md](HELP.md).
Таймер должен быть заранее запущен и считать непрерывно. Значение
`timer_ticks_per_us` задаётся частотой счёта таймера, а не частотой ядра.
Преобразование температуры выполняйте неблокирующей парой
`ds18b20_start_all()` / `ds18b20_conversion_ready()`; полный сценарий приведён
в [HELP.md](HELP.md).
### Опрос по сохранённым ID
`ds18b20_add_known_rom(bus, rom)` добавляет проверенный ROM без обращения к
линии. Повторное добавление не создаёт дубликат. См. [HELP.md](HELP.md).
## Shared source
Canonical source: `templates/c/ds18b20/instance`. Used by `home/climate`; its old paths are compatibility includes. Board-specific ports remain in the application. Change this library, not the forwarding files.

View File

@@ -0,0 +1,353 @@
#include "ds18b20.h"
#include "ds18b20_config.h"
#include <string.h>
#define OW_SEARCH_ROM 0xF0U
#define OW_MATCH_ROM 0x55U
#define OW_SKIP_ROM 0xCCU
#define DS_CONVERT 0x44U
#define DS_READ_SCRATCHPAD 0xBEU
#define DS_WRITE_SCRATCHPAD 0x4EU
#define DS_COPY_SCRATCHPAD 0x48U
#define DS_RECALL_E2 0xB8U
static int valid_instance(const ds18b20_t *d)
{
return d && d->ops && d->ops->drive_low && d->ops->release &&
d->ops->read && d->ops->delay_us && d->roms && d->rom_capacity;
}
static void critical(ds18b20_t *d, int enter)
{
if (enter && d->ops->critical_enter) d->ops->critical_enter(d->platform_context);
if (!enter && d->ops->critical_exit) d->ops->critical_exit(d->platform_context);
}
static void write_bit(ds18b20_t *d, uint8_t bit)
{
critical(d, 1);
d->ops->drive_low(d->platform_context);
d->ops->delay_us(d->platform_context, bit ? 6U : 60U);
d->ops->release(d->platform_context);
d->ops->delay_us(d->platform_context, bit ? 64U : 10U);
critical(d, 0);
if (d->search_active)
d->ops->delay_us(d->platform_context, DS18B20_SLOT_RECOVERY_EXTRA_US);
}
static uint8_t read_bit(ds18b20_t *d)
{
uint8_t bit;
critical(d, 1);
d->ops->drive_low(d->platform_context);
d->ops->delay_us(d->platform_context, 3U);
d->ops->release(d->platform_context);
d->ops->delay_us(d->platform_context, 10U);
bit = d->ops->read(d->platform_context) ? 1U : 0U;
d->ops->delay_us(d->platform_context, 57U);
critical(d, 0);
if (d->search_active)
d->ops->delay_us(d->platform_context, DS18B20_SLOT_RECOVERY_EXTRA_US);
return bit;
}
static void write_byte(ds18b20_t *d, uint8_t value)
{
uint8_t i;
for (i = 0; i < 8U; ++i) { write_bit(d, value & 1U); value >>= 1U; }
}
static uint8_t read_byte(ds18b20_t *d)
{
uint8_t i, value = 0U;
for (i = 0; i < 8U; ++i) value |= (uint8_t)(read_bit(d) << i);
return value;
}
static ds18b20_status_t reset(ds18b20_t *d)
{
uint8_t level;
d->ops->drive_low(d->platform_context);
d->ops->delay_us(d->platform_context, 480U);
/* Protect release-to-presence sampling from interrupt latency. */
critical(d, 1);
d->ops->release(d->platform_context);
d->ops->delay_us(d->platform_context, 70U);
level = d->ops->read(d->platform_context);
critical(d, 0);
d->ops->delay_us(d->platform_context, 410U);
return level ? DS18B20_E_NO_DEVICE : DS18B20_OK;
}
static void match(ds18b20_t *d, const uint8_t *rom)
{
uint8_t i; write_byte(d, OW_MATCH_ROM);
for (i = 0U; i < 8U; ++i) write_byte(d, rom[i]);
}
uint8_t ds18b20_crc8(const void *data, size_t length)
{
const uint8_t *p = (const uint8_t *)data; uint8_t crc = 0U;
while (length--) { uint8_t in = *p++, i; for (i = 0U; i < 8U; ++i) {
uint8_t mix = (uint8_t)((crc ^ in) & 1U); crc >>= 1U;
if (mix) crc ^= 0x8CU; in >>= 1U; } }
return crc;
}
ds18b20_status_t ds18b20_validate_rom(const uint8_t rom[8])
{
if (!rom) return DS18B20_E_ARGUMENT;
if (rom[0] != DS18B20_FAMILY_CODE) return DS18B20_E_ROM;
return ds18b20_crc8(rom, 7U) == rom[7] ? DS18B20_OK : DS18B20_E_CRC;
}
ds18b20_status_t ds18b20_init(ds18b20_t *d, const ds18b20_onewire_ops_t *ops,
void *ctx, uint8_t (*roms)[8], size_t capacity)
{
if (!d || !ops || !roms || !capacity || !ops->drive_low || !ops->release ||
!ops->read || !ops->delay_us) return DS18B20_E_ARGUMENT;
memset(d, 0, sizeof(*d)); d->ops = ops; d->platform_context = ctx;
d->roms = roms; d->rom_capacity = capacity; ops->release(ctx);
return DS18B20_OK;
}
static ds18b20_status_t search_next(ds18b20_t *d)
{
uint8_t bit_no=1U,last_zero=0U,byte_no=0U,mask=1U;
ds18b20_status_t reset_status;
if (d->last_device) return DS18B20_OK;
d->search_diagnostics.last_error_bit = 0U;
d->search_diagnostics.last_error_pair = 0xFFU;
reset_status=reset(d);
if (reset_status != DS18B20_OK) return reset_status;
write_byte(d, OW_SEARCH_ROM);
while (byte_no < 8U) {
uint8_t id=read_bit(d), cmp=read_bit(d), dir;
d->search_diagnostics.last_error_bit = bit_no;
d->search_diagnostics.last_error_pair = (uint8_t)((id << 1U) | cmp);
if (id && cmp) break;
if (id != cmp) dir=id; else { dir=(bit_no<d->last_discrepancy) ?
((d->search_rom[byte_no]&mask)!=0U) : (bit_no==d->last_discrepancy);
if (!dir) { last_zero=bit_no; if (last_zero<9U) d->last_family_discrepancy=last_zero; } }
if (dir) d->search_rom[byte_no]|=mask; else d->search_rom[byte_no]&=(uint8_t)~mask;
write_bit(d,dir); ++bit_no; mask<<=1U; if (!mask) { ++byte_no; mask=1U; }
}
if (bit_no < 65U || !d->search_rom[0]) {
d->last_discrepancy=0; d->last_device=0; return DS18B20_E_IO;
}
d->last_discrepancy=last_zero;
if (!last_zero) d->last_device=1U;
return DS18B20_E_BUSY;
}
/* Reset only traversal state: the union of validated ROMs survives passes. */
static void search_tree_reset(ds18b20_t *d)
{
d->last_discrepancy = 0U;
d->last_family_discrepancy = 0U;
d->last_device = 0U;
d->search_retries = 0U;
memset(d->search_rom, 0, sizeof(d->search_rom));
}
static ds18b20_status_t search_stop(ds18b20_t *d, ds18b20_status_t result)
{
d->search_active = 0U;
d->search_result = result;
return result;
}
static ds18b20_status_t search_pass_finish(ds18b20_t *d, uint8_t complete)
{
ds18b20_search_diagnostics_t *diag = &d->search_diagnostics;
++diag->passes_finished;
if (complete) ++diag->complete_passes;
if (diag->passes_finished < DS18B20_SEARCH_PASSES) {
search_tree_reset(d);
return DS18B20_E_BUSY;
}
/* A completed traversal can recover earlier faults. Without one, retain
* the partial catalog but report the failure instead of claiming success. */
if (diag->complete_passes != 0U)
return search_stop(d, d->rom_count ? DS18B20_OK : DS18B20_E_NO_DEVICE);
return search_stop(d, diag->last_error);
}
ds18b20_status_t ds18b20_search_begin(ds18b20_t *d)
{
if (!valid_instance(d)) return DS18B20_E_ARGUMENT;
if (d->strong_pullup_active) return DS18B20_E_BUSY;
d->rom_count = 0U;
search_tree_reset(d);
memset(&d->search_diagnostics, 0, sizeof(d->search_diagnostics));
d->search_diagnostics.last_error_pair = 0xFFU;
d->search_active = 1U;
d->search_result = DS18B20_E_BUSY;
return DS18B20_E_BUSY;
}
ds18b20_status_t ds18b20_search_step(ds18b20_t *d)
{
uint8_t previous_rom[8], previous_discrepancy, previous_family;
ds18b20_search_diagnostics_t previous_diag;
ds18b20_status_t status;
if (!valid_instance(d)) return DS18B20_E_ARGUMENT;
if (!d->search_active) return d->search_result;
if (d->search_diagnostics.attempts >= DS18B20_SEARCH_MAX_ATTEMPTS) {
d->search_diagnostics.last_error = DS18B20_E_TIMEOUT;
d->search_diagnostics.last_error_bit = 0U;
d->search_diagnostics.last_error_pair = 0xFFU;
return search_stop(d, DS18B20_E_TIMEOUT);
}
/* Snapshot the preceding validated tree path BEFORE touching the wire.
* A corrupt ROM must never become the path for the following attempt. */
memcpy(previous_rom, d->search_rom, 8U);
previous_discrepancy = d->last_discrepancy;
previous_family = d->last_family_discrepancy;
previous_diag = d->search_diagnostics;
++d->search_diagnostics.attempts;
status = search_next(d);
if (status == DS18B20_E_BUSY) {
/* CRC first: a valid non-DS18B20 family is skipped without corrupting
* the traversal; a bad family caused by noise still gets retried. */
status = ds18b20_crc8(d->search_rom, 7U) == d->search_rom[7] ?
DS18B20_OK : DS18B20_E_CRC;
if (status == DS18B20_OK && d->search_rom[0] == DS18B20_FAMILY_CODE)
status = ds18b20_add_known_rom(d, d->search_rom);
}
if (status == DS18B20_OK) {
/* Keep the last FAILURE location, even when a later retry succeeds. */
d->search_diagnostics.last_error_bit = previous_diag.last_error_bit;
d->search_diagnostics.last_error_pair = previous_diag.last_error_pair;
d->search_retries = 0U;
return d->last_device ? search_pass_finish(d, 1U) : DS18B20_E_BUSY;
}
d->search_diagnostics.last_error = status;
if (status == DS18B20_E_CAPACITY) return search_stop(d, status);
if (status == DS18B20_E_CRC) ++d->search_diagnostics.crc_errors;
else ++d->search_diagnostics.io_errors;
memcpy(d->search_rom, previous_rom, 8U);
d->last_discrepancy = previous_discrepancy;
d->last_family_discrepancy = previous_family;
d->last_device = 0U;
if (d->search_retries < DS18B20_SEARCH_RETRIES) {
++d->search_retries;
++d->search_diagnostics.retries;
return DS18B20_E_BUSY;
}
return search_pass_finish(d, 0U);
}
ds18b20_status_t ds18b20_search(ds18b20_t *d)
{
ds18b20_status_t status;
if (!valid_instance(d)) return DS18B20_E_ARGUMENT;
if (d->strong_pullup_active) return DS18B20_E_BUSY;
status = ds18b20_search_begin(d);
while (status == DS18B20_E_BUSY) status = ds18b20_search_step(d);
return status;
}
ds18b20_status_t ds18b20_add_known_rom(ds18b20_t *d, const uint8_t rom[8])
{
size_t i;
ds18b20_status_t status;
if (!valid_instance(d)) return DS18B20_E_ARGUMENT;
status = ds18b20_validate_rom(rom);
if (status != DS18B20_OK) return status;
for (i = 0U; i < d->rom_count; ++i)
if (memcmp(d->roms[i], rom, 8U) == 0) return DS18B20_OK;
if (d->rom_count >= d->rom_capacity) return DS18B20_E_CAPACITY;
memcpy(d->roms[d->rom_count++], rom, 8U);
return DS18B20_OK;
}
size_t ds18b20_count(const ds18b20_t *d) { return valid_instance(d) ? d->rom_count : 0U; }
const uint8_t *ds18b20_rom(const ds18b20_t *d,size_t i) { return valid_instance(d)&&i<d->rom_count?d->roms[i]:NULL; }
ds18b20_status_t ds18b20_start_all(ds18b20_t *d)
{ if(!valid_instance(d))return DS18B20_E_ARGUMENT; if(reset(d))return DS18B20_E_NO_DEVICE; write_byte(d,OW_SKIP_ROM);write_byte(d,DS_CONVERT);return DS18B20_OK; }
ds18b20_status_t ds18b20_start(ds18b20_t *d,const uint8_t *rom)
{ ds18b20_status_t s;if(!valid_instance(d)||!rom)return DS18B20_E_ARGUMENT;if(ds18b20_validate_rom(rom))return DS18B20_E_ROM;s=reset(d);if(s)return s;match(d,rom);write_byte(d,DS_CONVERT);return DS18B20_OK; }
ds18b20_status_t ds18b20_conversion_ready(ds18b20_t *d)
{ if(!valid_instance(d))return DS18B20_E_ARGUMENT;return read_bit(d)?DS18B20_OK:DS18B20_E_BUSY; }
ds18b20_status_t ds18b20_wait(ds18b20_t *d,uint32_t timeout)
{ uint32_t start;if(!valid_instance(d)||!d->ops->tick_ms)return DS18B20_E_ARGUMENT;start=d->ops->tick_ms(d->platform_context);while(!read_bit(d))if((uint32_t)(d->ops->tick_ms(d->platform_context)-start)>timeout)return DS18B20_E_TIMEOUT;return DS18B20_OK; }
ds18b20_status_t ds18b20_read_scratchpad(ds18b20_t *d,const uint8_t *rom,uint8_t *sp)
{ uint8_t i;ds18b20_status_t s;if(!valid_instance(d)||!rom||!sp)return DS18B20_E_ARGUMENT;if(ds18b20_validate_rom(rom))return DS18B20_E_ROM;s=reset(d);if(s)return s;match(d,rom);write_byte(d,DS_READ_SCRATCHPAD);for(i=0;i<9U;++i)sp[i]=read_byte(d);return ds18b20_crc8(sp,8U)==sp[8]?DS18B20_OK:DS18B20_E_CRC; }
ds18b20_status_t ds18b20_decode_temperature(const uint8_t *sp,float *out)
{ int16_t raw;uint8_t cfg;if(!sp||!out)return DS18B20_E_ARGUMENT;if(ds18b20_crc8(sp,8U)!=sp[8])return DS18B20_E_CRC;cfg=sp[4]&0x60U;raw=(int16_t)((uint16_t)sp[0]|((uint16_t)sp[1]<<8));if(cfg==0)raw&=(int16_t)~7;else if(cfg==0x20)raw&=(int16_t)~3;else if(cfg==0x40)raw&=(int16_t)~1;else if(cfg!=0x60)return DS18B20_E_IO;*out=(float)raw/16.0f;return DS18B20_OK; }
static ds18b20_status_t write_config(ds18b20_t *d,const uint8_t *rom,uint8_t th,uint8_t tl,uint8_t cfg)
{
uint8_t chunk;
ds18b20_status_t s=ds18b20_write_scratchpad(d,rom,th,tl,cfg);
if(s)return s;s=ds18b20_copy_scratchpad_start(d,rom,0U);if(s)return s;
/* TIM1 counter is 16-bit: at 72 ticks/us a single 10 ms wait can never
* satisfy the port comparison. Short chunks preserve wrap-safe timing. */
for(chunk=0U;chunk<100U;++chunk)d->ops->delay_us(d->platform_context,100U);
return ds18b20_copy_scratchpad_finish(d,0U);
}
ds18b20_status_t ds18b20_set_resolution(ds18b20_t *d,const uint8_t *rom,uint8_t bits)
{ uint8_t sp[9],cfg;ds18b20_status_t s;if(bits<9U||bits>12U)return DS18B20_E_ARGUMENT;s=ds18b20_read_scratchpad(d,rom,sp);if(s)return s;cfg=(uint8_t)(0x1FU|((bits-9U)<<5));return write_config(d,rom,sp[2],sp[3],cfg); }
ds18b20_status_t ds18b20_write_user_bytes(ds18b20_t *d,const uint8_t *rom,int16_t b12,int16_t b34,uint8_t mask)
{ uint8_t sp[9];ds18b20_status_t s=ds18b20_read_scratchpad(d,rom,sp);(void)b34;if(s)return s;if(mask&1U)sp[2]=(uint8_t)b12;if(mask&2U)sp[3]=(uint8_t)(b12>>8);/* DS18B20 физически позволяет записать только TH/TL/config; байты 6/7 read-only. */return write_config(d,rom,sp[2],sp[3],sp[4]); }
ds18b20_status_t ds18b20_write_scratchpad(ds18b20_t *d,const uint8_t *rom,
uint8_t th,uint8_t tl,uint8_t cfg)
{
ds18b20_status_t s;
if(!valid_instance(d)||!rom)return DS18B20_E_ARGUMENT;
s=ds18b20_validate_rom(rom);if(s)return s;
s=reset(d);if(s)return s;match(d,rom);write_byte(d,DS_WRITE_SCRATCHPAD);
write_byte(d,th);write_byte(d,tl);write_byte(d,cfg);return DS18B20_OK;
}
ds18b20_status_t ds18b20_copy_scratchpad_start(ds18b20_t *d,const uint8_t *rom,
uint8_t parasite)
{
ds18b20_status_t s;
if(!valid_instance(d)||!rom)return DS18B20_E_ARGUMENT;
if(parasite && !d->ops->strong_pullup)return DS18B20_E_POWER;
s=ds18b20_validate_rom(rom);if(s)return s;
s=reset(d);if(s)return s;match(d,rom);write_byte(d,DS_COPY_SCRATCHPAD);
/* The pull-up must be asserted immediately after the command slot. */
if(parasite){d->ops->strong_pullup(d->platform_context,1U);
d->strong_pullup_active=1U;}
return DS18B20_OK;
}
ds18b20_status_t ds18b20_copy_scratchpad_finish(ds18b20_t *d,uint8_t parasite)
{
if(!valid_instance(d))return DS18B20_E_ARGUMENT;
if(parasite){if(!d->ops->strong_pullup)return DS18B20_E_POWER;
if(d->strong_pullup_active){d->ops->strong_pullup(d->platform_context,0U);
d->strong_pullup_active=0U;}}
d->ops->release(d->platform_context);return DS18B20_OK;
}
ds18b20_status_t ds18b20_recall_e2(ds18b20_t *d,const uint8_t *rom)
{
ds18b20_status_t s;if(!valid_instance(d)||!rom)return DS18B20_E_ARGUMENT;
s=ds18b20_validate_rom(rom);if(s)return s;s=reset(d);if(s)return s;
match(d,rom);write_byte(d,DS_RECALL_E2);return DS18B20_OK;
}
ds18b20_status_t ds18b20_recall_ready(ds18b20_t *d)
{if(!valid_instance(d))return DS18B20_E_ARGUMENT;return read_bit(d)?DS18B20_OK:DS18B20_E_BUSY;}
void ds18b20_recover_bus(ds18b20_t *d)
{
if(!valid_instance(d))return;
if(d->strong_pullup_active&&d->ops->strong_pullup){
d->ops->strong_pullup(d->platform_context,0U);d->strong_pullup_active=0U;}
d->ops->release(d->platform_context);
}
uint8_t ds18b20_search_retry_due(const ds18b20_t *d,uint8_t conversion_active,
uint32_t now,uint32_t last,uint32_t period)
{
if(!valid_instance(d)||d->rom_count!=0U||conversion_active||period==0U)return 0U;
/* Unsigned subtraction keeps the retry correct across HAL tick rollover. */
return ((uint32_t)(now-last)>=period)?1U:0U;
}

View File

@@ -0,0 +1,31 @@
$ErrorActionPreference = "Stop"
$out = Join-Path $env:TEMP "test_ds18b20_copy_delay.exe"
$test = Join-Path $PSScriptRoot "test_ds18b20_copy_delay.c"
$core = Join-Path $PSScriptRoot "..\Src\ds18b20.c"
$inc = Join-Path $PSScriptRoot "..\Inc"
$gcc = Get-Command gcc -ErrorAction SilentlyContinue
if ($gcc) {
& $gcc.Source -std=c99 -Wall -Wextra -Werror -I $inc $test $core -o $out
} else {
$vcvars = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
if (-not (Test-Path $vcvars)) { throw "Neither gcc nor MSVC was found" }
$command = "call `"$vcvars`" >nul && pushd `"$env:TEMP`" && cl /nologo /std:c11 /W4 /WX /I `"$inc`" `"$test`" `"$core`" /Fe:`"$out`""
& cmd.exe /d /c $command
}
if ($LASTEXITCODE -ne 0) { throw "DS18B20 regression compilation failed" }
& $out
if ($LASTEXITCODE -ne 0) { throw "DS18B20 regression failed" }
Remove-Item -LiteralPath $out -Force
$searchOut = Join-Path $env:TEMP "test_ds18b20_incremental_search.exe"
$searchTest = Join-Path $PSScriptRoot "test_ds18b20_incremental_search.c"
if ($gcc) {
& $gcc.Source -std=c99 -Wall -Wextra -Werror -I $inc $searchTest $core -o $searchOut
} else {
$command = "call `"$vcvars`" >nul && pushd `"$env:TEMP`" && cl /nologo /std:c11 /W4 /WX /I `"$inc`" `"$searchTest`" `"$core`" /Fe:`"$searchOut`""
& cmd.exe /d /c $command
}
if ($LASTEXITCODE -ne 0) { throw "DS18B20 incremental search compilation failed" }
& $searchOut
if ($LASTEXITCODE -ne 0) { throw "DS18B20 incremental search failed" }
Remove-Item -LiteralPath $searchOut -Force

View File

@@ -0,0 +1,92 @@
#include "ds18b20.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
typedef struct {
uint8_t scratchpad[DS18B20_SCRATCHPAD_SIZE];
uint16_t read_call;
uint32_t max_delay_us;
uint16_t copy_delay_chunks;
uint8_t critical_active;
} mock_port_t;
static void drive_low(void *context) { (void)context; }
static void release_line(void *context) { (void)context; }
static uint8_t read_line(void *context)
{
mock_port_t *port = (mock_port_t *)context;
uint16_t call = port->read_call++;
assert(port->critical_active); /* Presence and data sampling must be protected. */
if (call == 0U) return 0U; /* Read Scratchpad reset presence. */
if (call <= 72U) {
uint16_t bit = (uint16_t)(call - 1U);
return (uint8_t)((port->scratchpad[bit / 8U] >> (bit % 8U)) & 1U);
}
/* Write Scratchpad and Copy Scratchpad reset presence pulses. */
return 0U;
}
static void delay_us(void *context, uint32_t us)
{
mock_port_t *port = (mock_port_t *)context;
if (us > port->max_delay_us) port->max_delay_us = us;
if (us == 100U) port->copy_delay_chunks++;
}
static void critical_enter(void *context)
{
mock_port_t *port = (mock_port_t *)context;
assert(!port->critical_active);
port->critical_active = 1U;
}
static void critical_exit(void *context)
{
mock_port_t *port = (mock_port_t *)context;
assert(port->critical_active);
port->critical_active = 0U;
}
int main(void)
{
static const ds18b20_onewire_ops_t ops = {
drive_low, release_line, read_line, delay_us, NULL, critical_enter, critical_exit, NULL
};
ds18b20_t bus;
mock_port_t port;
uint8_t roms[1][DS18B20_ROM_SIZE] = {{0}};
memset(&port, 0, sizeof(port));
roms[0][0] = 0x28U;
roms[0][1] = 0x11U;
roms[0][7] = ds18b20_crc8(roms[0], 7U);
port.scratchpad[0] = 0x50U;
port.scratchpad[1] = 0x05U;
port.scratchpad[2] = 0x4BU;
port.scratchpad[3] = 0x46U;
port.scratchpad[4] = 0x7FU;
port.scratchpad[5] = 0xFFU;
port.scratchpad[6] = 0x0CU;
port.scratchpad[7] = 0x10U;
port.scratchpad[8] = ds18b20_crc8(port.scratchpad, 8U);
assert(ds18b20_init(&bus, &ops, &port, roms, 1U) == DS18B20_OK);
bus.rom_count = 1U;
assert(ds18b20_set_resolution(&bus, roms[0], 9U) == DS18B20_OK);
/* Regression: a 10 ms callback overflows the 16-bit 72 MHz timer port. */
assert(port.max_delay_us <= 480U);
assert(port.copy_delay_chunks >= 100U);
bus.rom_count = 0U;
assert(ds18b20_search_retry_due(&bus, 0U, 1999U, 0U, 2000U) == 0U);
assert(ds18b20_search_retry_due(&bus, 0U, 2000U, 0U, 2000U) == 1U);
assert(ds18b20_search_retry_due(&bus, 1U, 4000U, 0U, 2000U) == 0U);
/* Wrap-safe deadline: 0x20 - 0xFFFFFF00 = 0x120 ms. */
assert(ds18b20_search_retry_due(&bus, 0U, 0x20U, 0xFFFFFF00U, 0x120U) == 1U);
bus.rom_count = 1U;
assert(ds18b20_search_retry_due(&bus, 0U, 4000U, 0U, 2000U) == 0U);
puts("DS18B20 copy delay regression: OK");
return 0;
}

View File

@@ -0,0 +1,152 @@
/* Model real SEARCH ROM participation: each branch filters the active slaves.
* Fault injection tests transport recovery, not analog cable characteristics. */
#include "ds18b20.h"
#include "ds18b20_config.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
typedef struct {
ds18b20_t *bus;
uint8_t roms[3][8];
uint8_t masks[3];
unsigned active, command_bits, bit, pair, presence, attempts;
unsigned low_time, elapsed, low, critical, recovery;
unsigned fault_attempt, fault_bit, persistent_fault, crc_fault;
} port_t;
static void low(void *ctx) {
port_t *p = ctx; p->low = 1U; p->low_time = 0U; p->elapsed = 0U;
}
static unsigned rom_bit(port_t *p, unsigned i) {
return (p->roms[i][p->bit / 8U] >> (p->bit % 8U)) & 1U;
}
static unsigned fault(port_t *p) {
return p->attempts == p->fault_attempt ||
(p->persistent_fault && p->attempts >= p->fault_attempt);
}
static void release_line(void *ctx) {
port_t *p = ctx; unsigned i, direction;
if (!p->low) return;
p->low = 0U;
if (p->low_time == 480U) {
++p->attempts;
p->active = p->masks[p->bus->search_diagnostics.passes_finished];
p->presence = 1U; p->command_bits = 0U; p->bit = 0U; p->pair = 0U;
} else if (p->low_time != 3U) {
assert(p->low_time == 6U || p->low_time == 60U);
if (p->command_bits < 8U) { ++p->command_bits; return; }
direction = p->low_time == 6U;
for (i = 0U; i < 3U; ++i)
if (rom_bit(p, i) != direction) p->active &= ~(1U << i);
++p->bit; p->pair = 0U;
}
}
static uint8_t read_line(void *ctx) {
port_t *p = ctx; unsigned i, zeros = 0U, ones = 0U, value;
assert(p->critical);
if (p->presence) {
assert(p->elapsed == 550U); p->presence = 0U;
return p->active ? 0U : 1U;
}
assert(p->elapsed == 13U); /* The read sample did not move. */
assert(p->bit < 64U);
for (i = 0U; i < 3U; ++i) if (p->active & (1U << i)) {
if (rom_bit(p, i)) ++ones; else ++zeros;
}
value = p->pair++ == 0U ? !zeros : !ones;
if (fault(p) && p->bit + 1U == p->fault_bit)
value = p->crc_fault ? !value : 1U;
return (uint8_t)value;
}
static void delay(void *ctx, uint32_t us) {
port_t *p = ctx; p->elapsed += us;
if (p->low) p->low_time += us;
if (us == DS18B20_SLOT_RECOVERY_EXTRA_US) {
assert(!p->critical && !p->low); ++p->recovery;
}
}
static void enter(void *ctx) { port_t *p = ctx; assert(!p->critical); p->critical = 1U; }
static void leave(void *ctx) { port_t *p = ctx; assert(p->critical); p->critical = 0U; }
static const ds18b20_onewire_ops_t ops = {low, release_line, read_line, delay, NULL, enter, leave, NULL};
static void setup(ds18b20_t *bus, port_t *p, uint8_t storage[][8], size_t cap) {
unsigned i;
memset(p, 0, sizeof(*p)); p->bus = bus;
for (i = 0U; i < 3U; ++i) {
p->roms[i][0] = 0x28U; p->roms[i][1] = (uint8_t)(i + 1U);
p->roms[i][7] = ds18b20_crc8(p->roms[i], 7U);
p->masks[i] = 7U;
}
assert(ds18b20_init(bus, &ops, p, storage, cap) == DS18B20_OK);
}
static ds18b20_status_t finish(ds18b20_t *bus, port_t *p) {
ds18b20_status_t s; unsigned calls = 0U, before;
do {
before = p->attempts; s = ds18b20_search_step(bus);
assert(p->attempts <= before + 1U); /* Never retries in a tight loop. */
assert(++calls <= DS18B20_SEARCH_MAX_ATTEMPTS + 1U);
} while (s == DS18B20_E_BUSY);
before = p->attempts;
assert(ds18b20_search_step(bus) == s && p->attempts == before);
return s;
}
int main(void) {
ds18b20_t bus; port_t p; uint8_t storage[3][8], saved[8], discrepancy, family;
setup(&bus, &p, storage, 3U);
assert(ds18b20_search(&bus) == DS18B20_OK);
assert(bus.rom_count == 3U && p.attempts == 9U);
assert(bus.search_diagnostics.complete_passes == 3U && p.recovery != 0U);
assert(bus.search_diagnostics.last_error == DS18B20_OK);
/* A fault on the second branch must restore the PREVIOUS tree path. */
setup(&bus, &p, storage, 3U); p.fault_attempt = 2U; p.fault_bit = 17U;
ds18b20_search_begin(&bus);
assert(ds18b20_search_step(&bus) == DS18B20_E_BUSY);
memcpy(saved, bus.search_rom, 8U); discrepancy = bus.last_discrepancy;
family = bus.last_family_discrepancy;
assert(ds18b20_search_step(&bus) == DS18B20_E_BUSY);
assert(memcmp(saved, bus.search_rom, 8U) == 0);
assert(bus.last_discrepancy == discrepancy && bus.last_family_discrepancy == family);
assert(finish(&bus, &p) == DS18B20_OK && bus.rom_count == 3U);
assert(bus.search_diagnostics.retries == 1U);
assert(bus.search_diagnostics.last_error_bit == 17U && bus.search_diagnostics.last_error_pair == 3U);
setup(&bus, &p, storage, 3U); p.fault_attempt = 2U; p.fault_bit = 64U; p.crc_fault = 1U;
assert(ds18b20_search(&bus) == DS18B20_OK && bus.rom_count == 3U);
assert(bus.search_diagnostics.crc_errors == 1U && bus.search_diagnostics.retries == 1U);
assert(bus.search_diagnostics.last_error_bit == 64U);
/* Each pass sees a different device; the validated union retains all three. */
setup(&bus, &p, storage, 3U); p.masks[0] = 1U; p.masks[1] = 2U; p.masks[2] = 4U;
assert(ds18b20_search(&bus) == DS18B20_OK && bus.rom_count == 3U);
assert(p.attempts == 3U);
setup(&bus, &p, storage, 3U); memset(p.masks, 0, sizeof(p.masks));
assert(ds18b20_search(&bus) == DS18B20_E_NO_DEVICE);
assert(p.attempts == 15U && bus.search_diagnostics.retries == 12U);
assert(bus.search_diagnostics.last_error_bit == 0U);
assert(ds18b20_add_known_rom(&bus, p.roms[0]) == DS18B20_OK);
assert(ds18b20_add_known_rom(&bus, p.roms[0]) == DS18B20_OK && bus.rom_count == 1U);
assert(ds18b20_add_known_rom(&bus, NULL) == DS18B20_E_ARGUMENT);
p.roms[1][7] ^= 1U;
assert(ds18b20_add_known_rom(&bus, p.roms[1]) == DS18B20_E_CRC);
setup(&bus, &p, storage, 3U); p.fault_attempt = 2U; p.fault_bit = 17U; p.persistent_fault = 1U;
assert(ds18b20_search(&bus) == DS18B20_E_IO && bus.rom_count == 1U);
assert(p.attempts == 16U && bus.search_diagnostics.retries == 12U);
assert(bus.search_diagnostics.complete_passes == 0U);
setup(&bus, &p, storage, 3U); p.fault_attempt = 1U; p.fault_bit = 64U;
p.persistent_fault = 1U; p.crc_fault = 1U;
assert(ds18b20_search(&bus) == DS18B20_E_CRC && bus.rom_count == 0U);
assert(bus.search_diagnostics.crc_errors == 15U);
setup(&bus, &p, storage, 1U);
assert(ds18b20_search(&bus) == DS18B20_E_CAPACITY && bus.rom_count == 1U);
setup(&bus, &p, storage, 3U); ds18b20_search_begin(&bus);
bus.search_diagnostics.attempts = DS18B20_SEARCH_MAX_ATTEMPTS;
assert(finish(&bus, &p) == DS18B20_E_TIMEOUT && p.attempts == 0U);
puts("DS18B20 robust search, retries, CRC, union, limits and timing: OK");
return 0;
}

View File

@@ -32,6 +32,16 @@ typedef enum {
FIRMWARE_INFO_OUT_OF_RANGE
} firmware_info_status_t;
/* Compatibility names for climate firmware; same layout and wire contract. */
typedef firmware_info_t FirmwareInfo;
typedef firmware_info_status_t FirmwareInfoStatus;
#define FIRMWARE_INFO_REG_COUNT FIRMWARE_INFO_REGISTER_COUNT
#define FIRMWARE_INFO_BUILD_ID_CHARS FIRMWARE_INFO_BUILD_ID_SIZE
#define FirmwareInfo_Validate firmware_info_validate
#define FirmwareInfo_ToRegisters firmware_info_to_registers
#define FirmwareInfo_ParseBuildStamp firmware_info_parse_build_stamp
#define FirmwareInfo_SetBuildId firmware_info_set_build_id
firmware_info_status_t firmware_info_validate(const firmware_info_t *info);
firmware_info_status_t firmware_info_parse_build_stamp(
const char *date_text, const char *time_text, firmware_info_t *info);

View File

@@ -16,5 +16,11 @@ int main(void)
assert(words[8] == 0x6162U && words[9] == 0x6330U);
assert(firmware_info_to_le_bytes(&info, bytes, sizeof(bytes)) == FIRMWARE_INFO_OK);
assert(bytes[0] == 1U && bytes[1] == 0U && bytes[16] == 0x62U && bytes[17] == 0x61U);
{
FirmwareInfo climate = info;
uint16_t compat[FIRMWARE_INFO_REG_COUNT];
assert(FirmwareInfo_ToRegisters(&climate, compat, FIRMWARE_INFO_REG_COUNT) == FIRMWARE_INFO_OK);
assert(memcmp(words, compat, sizeof(words)) == 0);
}
return 0;
}

View File

@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.13)
project(flash_storage C)
add_library(flash_storage STATIC Src/flash_storage.c)
target_include_directories(flash_storage PUBLIC Inc)
set_target_properties(flash_storage PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED YES)
enable_testing()
add_executable(test_flash_storage Tests/test_flash_storage.c ../ds18b20/instance/Src/ds18b20.c)
target_link_libraries(test_flash_storage PRIVATE flash_storage)
target_include_directories(test_flash_storage PRIVATE ../ds18b20/instance/Inc)
add_test(NAME test_flash_storage COMMAND test_flash_storage)

View File

@@ -0,0 +1,104 @@
#ifndef PORTABLE_FLASH_STORAGE_H
#define PORTABLE_FLASH_STORAGE_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define FLASH_STORAGE_IMAGE_SIZE 512U
#define FLASH_STORAGE_RECORD_SIZE 544U
#define FLASH_STORAGE_V1_RECORD_SIZE 288U
typedef enum {
FLASH_STORAGE_OK = 0,
FLASH_STORAGE_E_ARGUMENT = -1,
FLASH_STORAGE_E_LAYOUT = -2,
FLASH_STORAGE_E_IO = -3,
FLASH_STORAGE_E_NOT_FOUND = -4,
FLASH_STORAGE_E_CORRUPT = -5,
FLASH_STORAGE_E_BUSY = -6,
FLASH_STORAGE_E_BOUNDS = -7
} flash_storage_status_t;
typedef struct {
flash_storage_status_t (*read)(void *context, uint32_t address,
void *data, size_t size);
flash_storage_status_t (*program)(void *context, uint32_t address,
const void *data, size_t size);
flash_storage_status_t (*erase)(void *context, uint32_t address,
size_t size);
uint32_t (*tick_ms)(void *context);
} flash_storage_ops_t;
typedef struct {
uint32_t bank_address[2];
uint32_t bank_size;
uint32_t program_alignment;
uint32_t erase_alignment;
uint32_t minimum_write_ms;
uint8_t reserve_percent;
} flash_storage_layout_t;
typedef struct {
const flash_storage_ops_t *ops;
void *platform_context;
flash_storage_layout_t layout;
uint32_t latest_address;
uint32_t latest_sequence;
uint32_t next_address;
uint32_t last_write_tick;
uint32_t valid_records;
uint32_t programmed_slots;
uint16_t latest_version;
uint8_t initialized;
uint8_t busy;
uint8_t wrote_this_boot;
/* Рабочая запись принадлежит экземпляру: Flash API не расходует малый
* embedded-стек на 544-байтные автоматические структуры. */
uint32_t workspace[FLASH_STORAGE_RECORD_SIZE / sizeof(uint32_t)];
} flash_storage_t;
typedef struct {
flash_storage_t journal;
uint8_t image[FLASH_STORAGE_IMAGE_SIZE];
uint8_t next_image[FLASH_STORAGE_IMAGE_SIZE];
uint8_t initialized;
} eeprom_store_t;
typedef struct {
uint32_t sequence;
uint32_t valid_records;
uint32_t programmed_slots;
uint32_t allocated_bytes;
uint32_t reserved_bytes;
} flash_storage_info_t;
flash_storage_status_t flash_storage_init(flash_storage_t *instance,
const flash_storage_ops_t *ops, void *platform_context,
const flash_storage_layout_t *layout);
flash_storage_status_t flash_storage_read_latest(flash_storage_t *instance,
void *data, size_t capacity, size_t *length, uint32_t *sequence);
flash_storage_status_t flash_storage_write(flash_storage_t *instance,
const void *data, size_t length);
flash_storage_status_t flash_storage_format(flash_storage_t *instance);
flash_storage_status_t flash_storage_get_info(flash_storage_t *instance,
flash_storage_info_t *info);
flash_storage_status_t eeprom_store_init(eeprom_store_t *instance,
const flash_storage_ops_t *ops, void *platform_context,
const flash_storage_layout_t *layout);
flash_storage_status_t eeprom_store_read(eeprom_store_t *instance,
size_t offset, void *data, size_t size);
flash_storage_status_t eeprom_store_write(eeprom_store_t *instance,
size_t offset, const void *data, size_t size);
flash_storage_status_t eeprom_store_format(eeprom_store_t *instance);
/* API не ISR-safe: erase/program могут надолго блокировать CPU. Один экземпляр
* обслуживается последовательно; разные экземпляры допустимы на независимых
* областях, а общий HAL Flash должен сериализовать платформенный адаптер. */
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,8 @@
#ifndef PORTABLE_FLASH_STORAGE_CONFIG_H
#define PORTABLE_FLASH_STORAGE_CONFIG_H
#define FLASH_STORAGE_MAGIC 0x474E4952UL
#define FLASH_STORAGE_COMMIT 0x54494D43UL
#define FLASH_STORAGE_VERSION 2U
#define FLASH_STORAGE_VERSION_V1 1U
#define FLASH_STORAGE_MIN_RESERVE_PERCENT 10U
#endif

43
c/flash-storage/README.md Normal file
View File

@@ -0,0 +1,43 @@
# FlashStorage
Переносимый журнал двух банков Flash с подтверждёнными записями и восстановлением
после прерванной записи. Предоставляет также EEPROM-подобный образ 512 байт.
Не содержит HAL, файловой системы, адресов конкретной платы или malloc.
Приложение → `flash_storage` / `eeprom_store` → callbacks → Flash-порт платы.
| Файл | Назначение |
|---|---|
| `Inc/flash_storage.h` | Контекст, раскладка банков, read/program/erase/tick callbacks |
| `Inc/flash_storage_config.h` | Версия формата, magic, commit marker |
| `Src/flash_storage.c` | Восстановление, запись, ротация банков; только стандартный C |
| `Tests/test_flash_storage.c` | Прерывание записи, восстановление, несколько экземпляров |
Порт получает контекст, адрес и длину. `read`/`program`/`erase` возвращают
`flash_storage_status_t`; `tick_ms` возвращает uint32_t. Буферы принадлежат
экземпляру. Стирание и программирование могут блокировать процессор: API не
предназначен для ISR. Раскладку и резерв места задаёт `flash_storage_layout_t`.
```c
/* board_ops, board_context и layout определяет порт конкретной платы. */
flash_storage_t journal;
uint8_t payload[512] = {0}, restored[512];
size_t length = 0;
flash_storage_status_t result;
result = flash_storage_init(&journal, &board_ops, board_context, &layout);
if (result == FLASH_STORAGE_OK) {
result = flash_storage_write(&journal, payload, sizeof(payload));
}
if (result == FLASH_STORAGE_OK) {
result = flash_storage_read_latest(&journal, restored,
sizeof(restored), &length, 0);
}
```
Существующий потребитель — climate. Порты STM32F4 и SPI NOR остаются там.
Проверка: `python tools/test_shared_libraries.py` из корня templates.
## Shared source
Canonical source: `templates/c/flash-storage`. Used by `home/climate`; its old paths are compatibility includes. Board-specific ports remain in the application. Change this library, not the forwarding files.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

9
c/modbus/CMakeLists.txt Normal file
View File

@@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.13)
project(set_modbus C)
add_library(set_modbus STATIC src/modbus_data.c)
target_include_directories(set_modbus PUBLIC include)
set_target_properties(set_modbus PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED YES)
enable_testing()
add_executable(test_modbus_data tests/test_modbus_data.c)
target_link_libraries(test_modbus_data PRIVATE set_modbus)
add_test(NAME modbus_data COMMAND test_modbus_data)

41
c/modbus/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Общее ядро команд Modbus
Обработка coils и регистров функций 01/03/04/05/06/0F/10 без HAL, UART,
таймеров, глобальной карты регистров и выделения памяти.
Приложение → декодированный запрос → проверка и отображение адреса портом →
`mb_data_transfer` → банк регистров/битов приложения.
| Файл | Назначение и зависимости |
|---|---|
| `include/modbus_data.h` | Контракт, только stdint/stddef |
| `src/modbus_data.c` | Общие ограничения количества, обработка coils и регистров |
| `ports/stm32-legacy/modbus.c` | Совместимый адаптер старого John; зависит от проектного `rs_message.h` |
| `tests/test_modbus_data.c` | Границы и проход через все смещения coils 0..15 |
Порт сначала вызывает `mb_data_validate`, затем отображает **весь** диапазон
адресов в память, и только после успешного отображения вызывает transfer.
Коды возврата соответствуют исключениям Modbus: 0 — успех, 1 — функция,
3 — значение. Ёмкость данных передаётся в 16-битных словах. Адресная карта,
права записи, RTU framing, CRC и таймеры остаются обязанностью транспорта.
```c
#include "modbus_data.h"
uint16_t registers[2] = {123, 456};
uint16_t response[2];
uint16_t response_bytes = 0;
uint8_t status = mb_data_validate(3, 2, 0, 2);
/* Пример банка уже отображён; реальный порт проверяет адрес до вызова. */
if (status == 0) {
status = mb_data_transfer(3, 2, 0, registers, 0,
response, 2, &response_bytes);
}
/* status == 0, response_bytes == 4; сериализацию выполняет транспорт. */
```
Используется в `home/climate/core/STM32_Modbus`, `john103C6T6` и
`ds18b20-c8t6test`. Два последних подключают один совместимый адаптер;
климат сохраняет свой master/slave транспорт и диагностические функции.
Это не замена `pcan_modbus_server`, у которого другой транспортный контракт.
Проверка: `python tools/test_shared_libraries.py` из корня templates.

View File

@@ -0,0 +1,15 @@
#ifndef SET_MODBUS_DATA_H
#define SET_MODBUS_DATA_H
#include <stddef.h>
#include <stdint.h>
/* Decoded Modbus values, independent of UART/HAL and register addresses.
* Coils in bank use bit 0 first in 16-bit words. Message words contain two
* wire bytes, first byte in bits 15..8 (same convention as register messages).
* The port validates/maps the complete address range before calling transfer.
* Return standard Modbus exceptions: 0 success, 1 function, 3 value. */
uint8_t mb_data_validate(uint8_t function, uint16_t quantity, uint16_t byte_count,
size_t data_capacity);
uint8_t mb_data_transfer(uint8_t function, uint16_t quantity, uint16_t byte_count,
uint16_t *bank, uint16_t bit_offset, uint16_t *data,
size_t data_capacity, uint16_t *response_bytes);
#endif

View File

@@ -0,0 +1,836 @@
/**
**************************************************************************
* @file modbus.c
* @brief Модуль для реализации MODBUS.
**************************************************************************
* @details Файл содержит реализацию функций работы с Modbus, включая:
* - доступ к coils и registers;
* - обработку команд протокола;
* - взаимодействие с RS (UART);
* - инициализацию.
*
* @section Функции и макросы
*
* ### Доступ к coils:
* - MB_Set_Coil_Local() — Установить coil по локальному адресу.
* - MB_Reset_Coil_Local() — Сбросить coil по локальному адресу.
* - MB_Toogle_Coil_Local() — Инвертировать coil по локальному адресу.
* - MB_Read_Coil_Local() — Прочитать coil по локальному адресу.
* - MB_Write_Coil_Global() — Установить/сбросить coil по глобальному адресу.
* - MB_Read_Coil_Global() — Прочитать coil по глобальному адресу.
*
* ### Обработка команд Modbus:
* - MB_DefineRegistersAddress() — Определить начальный адрес регистра.
* - MB_DefineCoilsAddress() — Определить начальный адрес coils.
* - MB_Check_Address_For_Arr() — Проверить, принадлежит ли адрес массиву.
* - Основные команды Modbus:
* - MB_Read_Coils()
* - MB_Read_Hold_Regs()
* - MB_Write_Single_Coil()
* - MB_Write_Miltuple_Coils()
* - MB_Write_Miltuple_Regs()
*
* ### Функции для работы с RS (UART):
* - RS_Parse_Message() / RS_Collect_Message() — Парсинг и сборка сообщения.
* - RS_Response() — Отправка ответа.
* - RS_Define_Size_of_RX_Message() — Определение размера принимаемого сообщения.
* - RS_Init() — Инициализация UART.
*
* ### Инициализация:
* - MODBUS_FirstInit() — Инициализация модуля Modbus.
*
* @section Структура данных Modbus
*
* #### Holding/Input Registers:
* - Регистры — 16-битные слова. Доступ к регистрам осуществляется через указатель.
* Таким образом, сами регистры могут представлять собой как массив так и структуру.
*
* #### Coils:
* - Coils — это биты, упакованные в 16-битные слова. Доступ к коилам осуществляется через указатель.
* Таким образом, сами коилы могут представлять собой как массив так и структуру.
*
* @section Инструкция по подключению
* Для корректной работы надо подключить обработчики RS_UART_Handler(), RS_TIM_Handler(),
* в соответствубщие низкоуровневые прерывания UART_IRQHandler, TIM_IRQHandler. После HAL'овского обработчика
*
* Также необходимо в modbus_config.h настроить дефайны для нужной работы UART
* После для запуска Modbus:
* @verbatim
//----------------Прием модбас----------------//
#include "rs_message.h"
#include "../../src/modbus_data.c"
MODBUS_FirstInit();
RS_Receive_IT(&hmodbus1, &MODBUS_MSG);
* @endverbatim
*
******************************************************************************/
#include "rs_message.h"
#include "../../src/modbus_data.c"
uint32_t dbg_temp, dbg_temp2, dbg_temp3; // for debug
/* MODBUS HANDLES */
extern UART_HandleTypeDef rs_huart;
extern TIM_HandleTypeDef rs_htim;
RS_HandleTypeDef hmodbus1;
/* DEFINE REGISTERS/COILS */
MB_DeviceIdentificationTypeDef MB_INFO;
MB_DataStructureTypeDef MB_DATA;
RS_MsgTypeDef MODBUS_MSG;
//-------------------------------------------------------------------
//-----------------------------FOR USER------------------------------
/**
* @brief First set up of MODBUS.
* @details Первый инит модбас. Заполняет структуры и инициализирует таймер и юарт для общения по модбас.
* @note This called from main
*/
void MODBUS_FirstInit(void)
{
MB_DevoceInentificationInit();
//-----------SETUP MODBUS-------------
// set up modbus: MB_RX_Size_NotConst and Timeout enable
hmodbus1.ID = MODBUS_DEVICE_ID;
hmodbus1.sRS_Timeout = MODBUS_TIMEOUT;
hmodbus1.sRS_Mode = SLAVE_ALWAYS_WAIT;
hmodbus1.sRS_RX_Size_Mode = RS_RX_Size_NotConst;
// INIT
hmodbus1.RS_STATUS = RS_Init(&hmodbus1, &rs_huart, &rs_htim, 0);
RS_EnableReceive();
}
/**
* @brief Set or Reset Coil at its global address.
* @param Addr - адрес коила.
* @param WriteVal - Что записать в коил: 0 или 1.
* @return ExceptionCode - Код исключения если коила по адресу не существует, и NO_ERRORS если все ок.
*
* @details Позволяет обратиться к любому коилу по его глобальному адрессу.
Вне зависимости от того как коилы размещены в памяти.
*/
MB_ExceptionTypeDef MB_Write_Coil_Global(uint16_t Addr, MB_CoilsOpTypeDef WriteVal)
{
//---------CHECK FOR ERRORS----------
MB_ExceptionTypeDef Exception = NO_ERRORS;
uint16_t *coils;
uint16_t start_shift = 0; // shift in coils register
//------------WRITE COIL-------------
Exception = MB_DefineCoilsAddress(&coils, Addr, 1, &start_shift, 1);
if(Exception == NO_ERRORS)
{
switch(WriteVal)
{
case SET_COIL:
*coils |= (1<<start_shift);
break;
case RESET_COIL:
*coils &= ~(1<<start_shift);
break;
case TOOGLE_COIL:
*coils ^= (1<<start_shift);
break;
}
}
return Exception;
}
/**
* @brief Read Coil at its global address.
* @param Addr - адрес коила.
* @param Exception - Указатель на переменную для кода исключения, в случа неудачи при чтении.
* @return uint16_t - Возвращает весь регистр с маской на запрошенном коиле.
*
* @details Позволяет обратиться к любому коилу по его глобальному адрессу.
Вне зависимости от того как коилы размещены в памяти.
*/
uint16_t MB_Read_Coil_Global(uint16_t Addr, MB_ExceptionTypeDef *Exception)
{
//---------CHECK FOR ERRORS----------
MB_ExceptionTypeDef Exception_tmp;
if(Exception == NULL) // if exception is not given to func fill it
Exception = &Exception_tmp;
uint16_t *coils;
uint16_t start_shift = 0; // shift in coils register
//------------READ COIL--------------
*Exception = MB_DefineCoilsAddress(&coils, Addr, 1, &start_shift, 0);
if(*Exception == NO_ERRORS)
{
return ((*coils)&(1<<start_shift));
}
else
{
return 0;
}
}
//-------------------------------------------------------------------
//----------------FUNCTIONS FOR PROCESSING MESSAGE-------------------
/**
* @brief Check is address valid for certain array.
* @param Addr - начальный адресс.
* @param Qnt - количество запрашиваемых элементов.
* @param R_ARR_ADDR - начальный адресс массива R_ARR.
* @param R_ARR_NUMB - количество элементов в массиве R_ARR.
* @return ExceptionCode - ILLEGAL DATA ADRESS если адресс недействителен, и NO_ERRORS если все ок.
*
* @details Позволяет определить, принадлежит ли адресс Addr массиву R_ARR:
* Если адресс Addr находится в диапазоне адрессов массива R_ARR, то возвращаем NO_ERROR.
* Если адресс Addr находится за пределами адрессов массива R_ARR - ILLEGAL_DATA_ADDRESSю.
*/
MB_ExceptionTypeDef MB_Check_Address_For_Arr(uint16_t Addr, uint16_t Qnt, uint16_t R_ARR_ADDR, uint16_t R_ARR_NUMB)
{
// if address from this array
if(Addr >= R_ARR_ADDR)
{
// if quantity too big return error
if ((Addr - R_ARR_ADDR) + Qnt > R_ARR_NUMB)
{
return ILLEGAL_DATA_ADDRESS; // return exception code
}
// if all ok - return no errors
return NO_ERRORS;
}
// if address isnt from this array return error
else
return ILLEGAL_DATA_ADDRESS; // return exception code
}
/**
* @brief Define Address Origin for Input/Holding Registers
* @param pRegs - указатель на указатель регистров.
* @param Addr - адрес начального регистра.
* @param Qnt - количество запрашиваемых регистров.
* @param WriteFlag - флаг регистр нужны для чтения или записи.
* @return ExceptionCode - Код исключения если есть, и NO_ERRORS если нет.
*
* @details Определение адреса начального регистра.
* @note WriteFlag пока не используется.
*/
MB_ExceptionTypeDef MB_DefineRegistersAddress(uint16_t **pRegs, uint16_t Addr, uint16_t Qnt, uint8_t RegisterType)
{
/* check quantity error */
if (Qnt > 125)
{
return ILLEGAL_DATA_VALUE; // return exception code
}
if(RegisterType == RegisterType_Holding)
{
// Default holding registers
if(MB_Check_Address_For_Arr(Addr, Qnt, R_HOLDING_ADDR, R_HOLDING_QNT) == NO_ERRORS)
{
*pRegs = MB_Set_Register_Ptr(&MB_DATA.HoldRegs, Addr); // указатель на выбранный по Addr регистр
}
// if address doesnt match any array - return illegal data address response
else
{
return ILLEGAL_DATA_ADDRESS;
}
}
else if(RegisterType == RegisterType_Input)
{
// Default input registers
if(MB_Check_Address_For_Arr(Addr, Qnt, R_INPUT_ADDR, R_INPUT_QNT) == NO_ERRORS)
{
*pRegs = MB_Set_Register_Ptr(&MB_DATA.InRegs, Addr); // указатель на выбранный по Addr регистр
}
// if address doesnt match any array - return illegal data address response
else
{
return ILLEGAL_DATA_ADDRESS;
}
}
else
{
return ILLEGAL_FUNCTION;
}
// if found requeried array return no err
return NO_ERRORS; // return no errors
}
/**
* @brief Define Address Origin for coils
* @param pCoils - указатель на указатель коилов.
* @param Addr - адресс начального коила.
* @param Qnt - количество запрашиваемых коилов.
* @param start_shift - указатель на переменную содержащую сдвиг внутри регистра для начального коила.
* @param WriteFlag - флаг коилы нужны для чтения или записи.
* @return ExceptionCode - Код исключения если есть, и NO_ERRORS если нет.
*
* @details Определение адреса начального регистра запрашиваемых коилов.
* @note WriteFlag используется для определния регистров GPIO: ODR или IDR.
*/
MB_ExceptionTypeDef MB_DefineCoilsAddress(uint16_t **pCoils, uint16_t Addr, uint16_t Qnt, uint16_t *start_shift, uint8_t WriteFlag)
{
/* check quantity error */
if (Qnt > 2000)
{
return ILLEGAL_DATA_VALUE; // return exception code
}
// Default coils
if(MB_Check_Address_For_Arr(Addr, Qnt, C_CONTROL_ADDR, C_CONTROL_QNT) == NO_ERRORS)
{
*pCoils = MB_Set_Coil_Reg_Ptr(&MB_DATA.Coils, Addr); // указатель на выбранный по Addr массив коилов
}
// if address doesnt match any array - return illegal data address response
else
{
return ILLEGAL_DATA_ADDRESS;
}
*start_shift = Addr % 16; // set shift to requested coil
// if found requeried array return no err
return NO_ERRORS; // return no errors
}
/**
* @brief Proccess command Read Coils (01 - 0x01).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Read Coils.
*/
uint8_t MB_Read_Coils(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(1, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineCoilsAddress(&bank, modbus_msg->Addr, modbus_msg->Qnt, &shift, 0);
if (!status) status = mb_data_transfer(1, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Read Holding Registers (03 - 0x03).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Read Holding Registers.
*/
uint8_t MB_Read_Hold_Regs(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(3, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineRegistersAddress(&bank, modbus_msg->Addr, modbus_msg->Qnt, RegisterType_Holding);
if (!status) status = mb_data_transfer(3, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Read Input Registers (04 - 0x04).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Read Input Registers.
*/
uint8_t MB_Read_Input_Regs(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(4, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineRegistersAddress(&bank, modbus_msg->Addr, modbus_msg->Qnt, RegisterType_Input);
if (!status) status = mb_data_transfer(4, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Write Single Coils (05 - 0x05).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Write Single Coils.
*/
uint8_t MB_Write_Single_Coil(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(5, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineCoilsAddress(&bank, modbus_msg->Addr, 1, &shift, 1);
if (!status) status = mb_data_transfer(5, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Write Single Register (06 - 0x06).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Write Single Register.
*/
uint8_t MB_Write_Single_Reg(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(6, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineRegistersAddress(&bank, modbus_msg->Addr, 1, RegisterType_Holding);
if (!status) status = mb_data_transfer(6, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Write Multiple Coils (15 - 0x0F).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Write Multiple Coils.
*/
uint8_t MB_Write_Miltuple_Coils(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(15, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineCoilsAddress(&bank, modbus_msg->Addr, modbus_msg->Qnt, &shift, 1);
if (!status) status = mb_data_transfer(15, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
/**
* @brief Proccess command Write Multiple Registers (16 - 0x10).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Write Multiple Registers.
*/
uint8_t MB_Write_Miltuple_Regs(RS_MsgTypeDef *modbus_msg)
{
uint16_t *bank, shift = 0, response_bytes = modbus_msg->ByteCnt;
uint8_t status = mb_data_validate(16, modbus_msg->Qnt, modbus_msg->ByteCnt,
sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]));
if (!status) status = (uint8_t)MB_DefineRegistersAddress(&bank, modbus_msg->Addr, modbus_msg->Qnt, RegisterType_Holding);
if (!status) status = mb_data_transfer(16, modbus_msg->Qnt, modbus_msg->ByteCnt,
bank, shift, modbus_msg->DATA, sizeof(modbus_msg->DATA) / sizeof(modbus_msg->DATA[0]), &response_bytes);
modbus_msg->Except_Code = (MB_ExceptionTypeDef)status;
if (status) return 0;
modbus_msg->ByteCnt = response_bytes;
return 1;
}
void MB_WriteObjectToMessage(char *mbdata, unsigned *ind, MB_DeviceObjectTypeDef *obj)
{
mbdata[(*ind)++] = obj->length;
for (int i = 0; i < obj->length; i++)
{
mbdata[(*ind)++] = obj->name[i];
}
}
/**
* @brief Proccess command Read Device Identification (43/14 - 0x2B/0E).
* @param modbus_msg - указатель на структуру собщения modbus.
* @return fMessageHandled - статус о результате обработки комманды.
* @details Обработка команды Write Single Register.
*/
uint8_t MB_Read_Device_Identification(RS_MsgTypeDef *modbus_msg)
{
char *mbdata = (char *)modbus_msg->DATA;
unsigned ind = 0;
switch(modbus_msg->DevId.ReadDevId)
{
case MB_BASIC_IDENTIFICATION:
mbdata[ind++] = 0x00;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.VendorName);
mbdata[ind++] = 0x01;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.ProductCode);
mbdata[ind++] = 0x02;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.Revision);
modbus_msg->DevId.NumbOfObj = 3;
break;
case MB_REGULAR_IDENTIFICATION:
mbdata[ind++] = 0x03;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.VendorUrl);
mbdata[ind++] = 0x04;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.ProductName);
mbdata[ind++] = 0x05;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.ModelName);
mbdata[ind++] = 0x06;
MB_WriteObjectToMessage(mbdata, &ind, &MB_INFO.UserApplicationName);
modbus_msg->DevId.NumbOfObj = 4;
break;
default:
return 0;
}
modbus_msg->ByteCnt = ind;
return 1;
}
/**
* @brief Respond accord to received message.
* @param hRS - указатель на хендлер RS.
* @param RS_msg - указатель на структуру сообщения.
* @return RS_RES - статус о результате ответа на комманду.
* @details Обработка принятой комманды и ответ на неё.
*/
RS_StatusTypeDef RS_Response(RS_HandleTypeDef *hmodbus, RS_MsgTypeDef *modbus_msg)
{
RS_StatusTypeDef MB_RES = 0;
hmodbus->f.MessageHandled = 0;
hmodbus->f.EchoResponse = 0;
RS_Reset_TX_Flags(hmodbus); // reset flag for correct transmit
if(modbus_msg->Func_Code < ERR_VALUES_START)// if no errors after parsing
{
switch (modbus_msg->Func_Code)
{
// Read Coils
case MB_R_COILS:
hmodbus->f.MessageHandled = MB_Read_Coils(hmodbus->pMessagePtr);
break;
// Read Hodling Registers
case MB_R_HOLD_REGS:
hmodbus->f.MessageHandled = MB_Read_Hold_Regs(hmodbus->pMessagePtr);
break;
case MB_R_IN_REGS:
hmodbus->f.MessageHandled = MB_Read_Input_Regs(hmodbus->pMessagePtr);
break;
// Write Single Coils
case MB_W_COIL:
hmodbus->f.MessageHandled = MB_Write_Single_Coil(hmodbus->pMessagePtr);
if(hmodbus->f.MessageHandled)
{
hmodbus->f.EchoResponse = 1;
hmodbus->RS_Message_Size -= 2; // echo response if write ok (minus 2 cause of two CRC bytes)
}
break;
case MB_W_HOLD_REG:
hmodbus->f.MessageHandled = MB_Write_Single_Reg(hmodbus->pMessagePtr);
if(hmodbus->f.MessageHandled)
{
hmodbus->f.EchoResponse = 1;
hmodbus->RS_Message_Size -= 2; // echo response if write ok (minus 2 cause of two CRC bytes)
}
break;
// Write Multiple Coils
case MB_W_COILS:
hmodbus->f.MessageHandled = MB_Write_Miltuple_Coils(hmodbus->pMessagePtr);
if(hmodbus->f.MessageHandled)
{
hmodbus->f.EchoResponse = 1;
hmodbus->RS_Message_Size = 6; // echo response if write ok (withous data bytes)
}
break;
// Write Multiple Registers
case MB_W_HOLD_REGS:
hmodbus->f.MessageHandled = MB_Write_Miltuple_Regs(hmodbus->pMessagePtr);
if(hmodbus->f.MessageHandled)
{
hmodbus->f.EchoResponse = 1;
hmodbus->RS_Message_Size = 6; // echo response if write ok (withous data bytes)
}
break;
case MB_R_DEVICE_INFO:
hmodbus->f.MessageHandled = MB_Read_Device_Identification(hmodbus->pMessagePtr);
break;
/* unknown func code */
default: modbus_msg->Except_Code = 0x01; /* set exception code: illegal function */
}
if(hmodbus->f.MessageHandled == 0)
{
modbus_msg->Func_Code += ERR_VALUES_START;
}
else
{
}
}
// if we need response - check that transmit isnt busy
if( RS_Is_TX_Busy(hmodbus) )
RS_Abort(hmodbus, ABORT_TX); // if tx busy - set it free
// Transmit right there, or sets (fDeferredResponse) to transmit response in main code
MB_RES = RS_Handle_Transmit_Start(hmodbus, modbus_msg);
hmodbus->RS_STATUS = MB_RES;
return MB_RES;
}
/**
* @brief Collect message in buffer to transmit it.
* @param hRS - указатель на хендлер RS.
* @param RS_msg - указатель на структуру сообщения.
* @param msg_uart_buff - указатель на буффер UART.
* @return RS_RES - статус о результате заполнения буфера.
* @details Заполнение буффера UART из структуры сообщения.
*/
RS_StatusTypeDef RS_Collect_Message(RS_HandleTypeDef *hmodbus, RS_MsgTypeDef *modbus_msg, uint8_t *modbus_uart_buff)
{
int ind = 0; // ind for modbus-uart buffer
if(hmodbus->f.EchoResponse && hmodbus->f.MessageHandled) // if echo response need
ind = hmodbus->RS_Message_Size;
else
{
//------INFO ABOUT DATA/MESSAGE------
//-----------[first bytes]-----------
// set ID of message/user
modbus_uart_buff[ind++] = modbus_msg->MbAddr;
// set dat or err response
modbus_uart_buff[ind++] = modbus_msg->Func_Code;
if (modbus_msg->Func_Code < ERR_VALUES_START) // if no error occur
{
// fill modbus header
if(modbus_msg->Func_Code == MB_R_DEVICE_INFO) // devide identification header
{
modbus_uart_buff[ind++] = modbus_msg->DevId.MEI_Type;
modbus_uart_buff[ind++] = modbus_msg->DevId.ReadDevId;
modbus_uart_buff[ind++] = modbus_msg->DevId.Conformity;
modbus_uart_buff[ind++] = modbus_msg->DevId.MoreFollows;
modbus_uart_buff[ind++] = modbus_msg->DevId.NextObjId;
modbus_uart_buff[ind++] = modbus_msg->DevId.NumbOfObj;
if (modbus_msg->ByteCnt > DATA_SIZE*2) // if ByteCnt less than DATA_SIZE
{
return RS_COLLECT_MSG_ERR;
}
//---------------DATA----------------
//-----------[data bytes]------------
uint8_t *tmp_data_addr = (uint8_t *)modbus_msg->DATA;
for(int i = 0; i < modbus_msg->ByteCnt; i++) // filling buffer with data
{ // set data
modbus_uart_buff[ind++] = *tmp_data_addr;
tmp_data_addr++;
}
}
else // modbus data header
{
// set size of received data
if (modbus_msg->ByteCnt <= DATA_SIZE*2) // if ByteCnt less than DATA_SIZE
modbus_uart_buff[ind++] = modbus_msg->ByteCnt;
else // otherwise return data_size err
{
return RS_COLLECT_MSG_ERR;
}
//---------------DATA----------------
//-----------[data bytes]------------
uint16_t *tmp_data_addr = (uint16_t *)modbus_msg->DATA;
for(int i = 0; i < modbus_msg->ByteCnt; i++) // filling buffer with data
{ // set data
if (i%2 == 0) // HI byte
modbus_uart_buff[ind++] = (*tmp_data_addr)>>8;
else // LO byte
{
modbus_uart_buff[ind++] = *tmp_data_addr;
tmp_data_addr++;
}
}
}
}
else // if some error occur
{ // send expection code
modbus_uart_buff[ind++] = modbus_msg->Except_Code;
}
}
//---------------CRC----------------
//---------[last 16 bytes]----------
// calc crc of received data
uint16_t CRC_VALUE = crc16(modbus_uart_buff, ind);
// write crc to message structure and modbus-uart buffer
modbus_msg->MB_CRC = CRC_VALUE;
modbus_uart_buff[ind++] = CRC_VALUE;
modbus_uart_buff[ind++] = CRC_VALUE >> 8;
hmodbus->RS_Message_Size = ind;
return RS_OK; // returns ok
}
/**
* @brief Parse message from buffer to process it.
* @param hRS - указатель на хендлер RS.
* @param RS_msg - указатель на структуру сообщения.
* @param msg_uart_buff - указатель на буффер UART.
* @return RS_RES - статус о результате заполнения структуры.
* @details Заполнение структуры сообщения из буффера UART.
*/
RS_StatusTypeDef RS_Parse_Message(RS_HandleTypeDef *hmodbus, RS_MsgTypeDef *modbus_msg, uint8_t *modbus_uart_buff)
{
uint32_t check_empty_buff;
int ind = 0; // ind for modbus-uart buffer
//-----INFO ABOUT DATA/MESSAGE-------
//-----------[first bits]------------
// get ID of message/user
modbus_msg->MbAddr = modbus_uart_buff[ind++];
if(modbus_msg->MbAddr != hmodbus->ID)
return RS_SKIP;
// get func code
modbus_msg->Func_Code = modbus_uart_buff[ind++];
if(modbus_msg->Func_Code == MB_R_DEVICE_INFO) // if it device identification request
{
modbus_msg->DevId.MEI_Type = modbus_uart_buff[ind++];
modbus_msg->DevId.ReadDevId = modbus_uart_buff[ind++];
modbus_msg->DevId.NextObjId = modbus_uart_buff[ind++];
modbus_msg->ByteCnt = 0;
}
else // if its classic modbus request
{
// get address from CMD
modbus_msg->Addr = modbus_uart_buff[ind++] << 8;
modbus_msg->Addr |= modbus_uart_buff[ind++];
// get address from CMD
modbus_msg->Qnt = modbus_uart_buff[ind++] << 8;
modbus_msg->Qnt |= modbus_uart_buff[ind++];
}
if(hmodbus->f.RX_Half == 0) // if all message received
{
//---------------DATA----------------
// (optional)
if (modbus_msg->ByteCnt != 0)
{
ind++; // increment ind for data_size byte
//check that data size is correct
if (modbus_msg->ByteCnt > DATA_SIZE*2)
{
modbus_msg->Func_Code += ERR_VALUES_START;
return RS_PARSE_MSG_ERR;
}
uint16_t *tmp_data_addr = (uint16_t *)modbus_msg->DATA;
for(int i = 0; i < modbus_msg->ByteCnt; i++) // /2 because we transmit 8 bits, not 16 bits
{ // set data
if (i%2 == 0)
*tmp_data_addr = ((uint16_t)modbus_uart_buff[ind++] << 8);
else
{
*tmp_data_addr |= modbus_uart_buff[ind++];
tmp_data_addr++;
}
}
}
//---------------CRC----------------
//----------[last 16 bits]----------
// calc crc of received data
uint16_t CRC_VALUE = crc16(modbus_uart_buff, ind);
// get crc of received data
modbus_msg->MB_CRC = modbus_uart_buff[ind++];
modbus_msg->MB_CRC |= modbus_uart_buff[ind++] << 8;
// compare crc
if (modbus_msg->MB_CRC != CRC_VALUE)
{
modbus_msg->Func_Code += ERR_VALUES_START;
}
// hmodbus->MB_RESPONSE = MB_CRC_ERR; // set func code - error about wrong crc
// check is buffer empty
check_empty_buff = 0;
for(int i=0; i<ind;i++)
check_empty_buff += modbus_uart_buff[i];
// if(check_empty_buff == 0)
// hmodbus->MB_RESPONSE = MB_EMPTY_MSG; //
}
return RS_OK;
}
/**
* @brief Define size of RX Message that need to be received.
* @param hRS - указатель на хендлер RS.
* @param rx_data_size - указатель на переменную для записи кол-ва байт для принятия.
* @return RS_RES - статус о корректности рассчета кол-ва байт для принятия.
* @details Определение сколько байтов надо принять по протоколу.
*/
RS_StatusTypeDef RS_Define_Size_of_RX_Message(RS_HandleTypeDef *hmodbus, uint32_t *rx_data_size)
{
RS_StatusTypeDef MB_RES = 0;
MB_RES = RS_Parse_Message(hmodbus, hmodbus->pMessagePtr, hmodbus->pBufferPtr);
if(MB_RES == RS_SKIP) // if message not for us
return MB_RES; // return
if ((hmodbus->pMessagePtr->Func_Code & ~ERR_VALUES_START) < 0x0F)
{
hmodbus->pMessagePtr->ByteCnt = 0;
*rx_data_size = 1;
}
else
{
hmodbus->pMessagePtr->ByteCnt = hmodbus->pBufferPtr[RX_FIRST_PART_SIZE-1]; // get numb of data in command
// +1 because that defines is size, not ind.
*rx_data_size = hmodbus->pMessagePtr->ByteCnt + 2;
}
if(hmodbus->pMessagePtr->Func_Code == MB_R_DEVICE_INFO)
{
*rx_data_size = 0;
}
hmodbus->RS_Message_Size = RX_FIRST_PART_SIZE + *rx_data_size; // size of whole message
return RS_OK;
}
//-----------------------------FOR USER------------------------------
//-------------------------------------------------------------------
void MB_DevoceInentificationInit(void)
{
MB_INFO.VendorName.name = MODBUS_VENDOR_NAME;
MB_INFO.ProductCode.name = MODBUS_PRODUCT_CODE;
MB_INFO.Revision.name = MODBUS_REVISION;
MB_INFO.VendorUrl.name = MODBUS_VENDOR_URL;
MB_INFO.ProductName.name = MODBUS_PRODUCT_NAME;
MB_INFO.ModelName.name = MODBUS_MODEL_NAME;
MB_INFO.UserApplicationName.name = MODBUS_USER_APPLICATION_NAME;
MB_INFO.VendorName.length = sizeof(MODBUS_VENDOR_NAME);
MB_INFO.ProductCode.length = sizeof(MODBUS_PRODUCT_CODE);
MB_INFO.Revision.length = sizeof(MODBUS_REVISION);
MB_INFO.VendorUrl.length = sizeof(MODBUS_VENDOR_URL);
MB_INFO.ProductName.length = sizeof(MODBUS_PRODUCT_NAME);
MB_INFO.ModelName.length = sizeof(MODBUS_MODEL_NAME);
MB_INFO.UserApplicationName.length = sizeof(MODBUS_USER_APPLICATION_NAME);
}

View File

@@ -0,0 +1,57 @@
#include "../include/modbus_data.h"
uint8_t mb_data_validate(uint8_t function, uint16_t quantity, uint16_t bytes, size_t capacity)
{
size_t needed;
switch (function) {
case 1: case 15:
if (!quantity || quantity > (function == 1 ? 2000U : 1968U)) return 3;
needed = ((size_t)quantity+15U)/16U;
if (function == 15 && bytes != (quantity+7U)/8U) return 3;
break;
case 3: case 4: case 16:
if (!quantity || quantity > (function == 16 ? 123U : 125U)) return 3;
needed = quantity;
if (function == 16 && bytes != quantity*2U) return 3;
break;
case 5: return (quantity == 0 || quantity == 0xff00U) ? 0 : 3;
case 6: return 0;
default: return 1;
}
return needed <= capacity ? 0 : 3;
}
uint8_t mb_data_transfer(uint8_t function, uint16_t quantity, uint16_t bytes,
uint16_t *bank, uint16_t offset, uint16_t *data, size_t capacity, uint16_t *response_bytes)
{
uint16_t i;
uint8_t status = mb_data_validate(function, quantity, bytes, capacity);
if (status) return status;
if (bank == NULL || data == NULL || response_bytes == NULL || offset > 15U) return 3;
if (function == 1) {
for (i = 0; i < (quantity+15U)/16U; ++i) data[i] = 0;
}
if (function == 1 || function == 15) {
for (i = 0; i < quantity; ++i) {
uint16_t position = (uint16_t)(i+offset);
uint16_t mask = (uint16_t)(1U << (position%16U));
uint16_t wire_mask = (uint16_t)(1U << ((i%16U)^8U));
if (function == 1) {
if (bank[position/16U] & mask) data[i/16U] |= wire_mask;
} else if (data[i/16U] & wire_mask) bank[position/16U] |= mask;
else bank[position/16U] &= (uint16_t)~mask;
}
if (function == 1) *response_bytes = (uint16_t)((quantity+7U)/8U);
} else if (function == 5) {
if (quantity) *bank |= (uint16_t)(1U << offset);
else *bank &= (uint16_t)~(1U << offset);
} else if (function == 6) *bank = quantity;
else {
for (i = 0; i < quantity; ++i) {
if (function == 16) bank[i] = data[i];
else data[i] = bank[i];
}
if (function != 16) *response_bytes = (uint16_t)(quantity*2U);
}
return 0;
}

View File

@@ -0,0 +1,29 @@
#include "modbus_data.h"
#include <assert.h>
#include <string.h>
int main(void)
{
uint16_t bank[4]={0xa5a5,0x5a5a,0x1234,0xbeef}, original[4], data[130], out;
unsigned offset, quantity, bit;
assert(mb_data_transfer(3,4,0,bank,0,data,130,&out)==0 && out==8 && data[3]==0xbeef);
assert(mb_data_transfer(16,4,7,bank,0,data,130,&out)==3);
assert(mb_data_validate(1,0,0,130)==3);
assert(mb_data_validate(3,126,0,130)==3);
assert(mb_data_validate(16,124,248,130)==3);
assert(mb_data_validate(15,17,3,1)==3);
assert(mb_data_validate(5,1,0,0)==3);
assert(mb_data_validate(2,1,0,130)==1);
for(offset=0;offset<16;++offset) for(quantity=1;quantity<=32;++quantity) {
bank[0]=0xa5a5; bank[1]=0x5a5a; bank[2]=0x1234; bank[3]=0xbeef;
memcpy(original,bank,sizeof(bank));
memset(data,0xcc,sizeof(data));
assert(mb_data_transfer(1,(uint16_t)quantity,0,bank,(uint16_t)offset,data,130,&out)==0);
for(bit=0;bit<quantity;++bit)
assert(((data[bit/16]>>((bit%16)^8))&1)==((bank[(bit+offset)/16]>>((bit+offset)%16))&1));
for(bit=0;bit<quantity;++bit) bank[(bit+offset)/16]^=(uint16_t)(1U<<((bit+offset)%16));
assert(mb_data_transfer(15,(uint16_t)quantity,out,bank,(uint16_t)offset,data,130,&out)==0);
assert(memcmp(bank,original,sizeof(bank))==0);
assert(data[(quantity+15)/16]==0xcccc); /* no write past response */
}
return 0;
}

View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.13)
project(sd_file_browser C)
add_library(sd_file_browser STATIC Src/sd_file_browser.c)
target_include_directories(sd_file_browser PUBLIC Inc)
set_target_properties(sd_file_browser PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED YES)
enable_testing()
add_executable(test_sd_file_browser Tests/test_sd_file_browser.c)
target_link_libraries(test_sd_file_browser PRIVATE sd_file_browser)
add_test(NAME test_sd_file_browser COMMAND test_sd_file_browser)

View File

@@ -0,0 +1,82 @@
#ifndef SD_FILE_BROWSER_H
#define SD_FILE_BROWSER_H
#include <stdint.h>
/* Ограничения одинаковы для portable core, Modbus mailbox и GUI. */
#define SD_FILE_BROWSER_MAX_PATH 96U
#define SD_FILE_BROWSER_MAX_NAME 48U
#define SD_FILE_BROWSER_MAX_PAGE_ITEMS 3U
#define SD_FILE_BROWSER_MAX_CHUNK_BYTES 64U
typedef enum {
/* Успех означает полностью сформированную страницу, а не только open. */
SD_FILE_BROWSER_OK = 0,
/* BUSY оставляет retry политике приложения и ничего не пишет на носитель. */
SD_FILE_BROWSER_BUSY,
SD_FILE_BROWSER_NOT_READY,
SD_FILE_BROWSER_IO_ERROR,
SD_FILE_BROWSER_INVALID_PATH,
SD_FILE_BROWSER_INVALID_CURSOR,
SD_FILE_BROWSER_NAME_TOO_LONG,
SD_FILE_BROWSER_FILE_CHANGED,
SD_FILE_BROWSER_NOT_LOG_FILE
} SdFileBrowserStatus;
typedef struct {
/* Имя всегда завершается нулём и не содержит родительского пути. */
char name[SD_FILE_BROWSER_MAX_NAME + 1U];
uint32_t size;
uint16_t modified_date;
uint16_t modified_time;
uint8_t is_directory;
} SdFileBrowserEntry;
typedef struct {
/* Ответ имеет фиксированную RAM-ёмкость и никогда не выделяет heap. */
SdFileBrowserEntry items[SD_FILE_BROWSER_MAX_PAGE_ITEMS];
uint16_t next_offset;
uint8_t count;
uint8_t has_more;
} SdFileBrowserPage;
typedef struct {
/* Metadata повторяется в каждом chunk и защищает bridge от склейки разных версий файла. */
uint32_t total_size;
uint32_t offset;
uint16_t modified_date;
uint16_t modified_time;
uint8_t length;
uint8_t end_of_file;
uint8_t data[SD_FILE_BROWSER_MAX_CHUNK_BYTES];
} SdFileBrowserChunk;
typedef SdFileBrowserStatus (*SdFileBrowserListPageFn)(
void *context, const char *path, uint16_t offset, uint8_t limit,
SdFileBrowserPage *page);
typedef struct {
/* Context принадлежит платформе; portable core его не освобождает. */
void *context;
SdFileBrowserListPageFn list_page;
} SdFileBrowser;
/* Принимается только логический путь от корня карты: "/" или "/dir/file".
* Проверка не исправляет вход: неканонические строки отклоняются fail-closed. */
SdFileBrowserStatus SdFileBrowser_ValidatePath(const char *path);
/* Разрешает только legacy TEMPLOG.MD или суточный temperature_*.md внутри
* штатного корня niceOne/mounth; служебные JSON/прошивки сюда не попадают. */
SdFileBrowserStatus SdFileBrowser_ValidateLogFilePath(const char *path);
/* Разрешает только сырой образ .bin/.fw внутри каталога niceOne/firmware.
* Каталог отделён от журналов, чтобы прошивка и логи не смешивались. */
SdFileBrowserStatus SdFileBrowser_ValidateFirmwarePath(const char *path);
/* Вызов делегирует порту ровно одну ограниченную страницу каталога.
* Output предварительно очищается, поэтому ошибка не публикует старые имена. */
SdFileBrowserStatus SdFileBrowser_ListPage(
const SdFileBrowser *browser, const char *path, uint16_t offset,
uint8_t limit, SdFileBrowserPage *page);
#endif /* SD_FILE_BROWSER_H */

View File

@@ -0,0 +1,30 @@
# SD File Browser
Небольшое переносимое read-only ядро для постраничного просмотра каталогов.
Оно не зависит от STM32 HAL, FatFs, Modbus или GUI. Платформа передаёт callback
`list_page`; ядро проверяет путь, предел страницы и очищает ответ перед вызовом.
Логический путь всегда начинается с `/`. Запрещены `..`, `.`, пустые части,
обратная косая черта, двоеточие, управляющие символы, путь от ОС и компоненты
длиннее 48 ASCII-байт. Максимум — три элемента на страницу и 95 байт пути.
FatFs-порт сканирует позднюю страницу пошагово: один вызов service выполняет не
более шестнадцати `readdir`. Поэтому любой `uint16` offset доступен без длинного
блокирующего вызова main loop. Между страницами `DIR` закрыт. Если логгер или
backup получает работу во время сканирования, приложение отменяет viewer,
освобождает `DIR` и возвращает `logger busy`; writer всегда имеет приоритет.
Для переноса реализуйте `SdFileBrowserListPageFn`, обеспечьте read-only открытие
каталога, конечные тайм-ауты носителя и закройте cursor при извлечении карты.
Порт также предоставляет `SdFileBrowserFatFs_ReadLogChunk`: он разрешает только
legacy `/TEMPLOG.MD` и `temperature_*.md` внутри `/niceOne/mounth`, читает не
более 64 байт и закрывает `FIL` до возврата. Первый ответ публикует размер и FAT
date/time; каждый следующий запрос обязан повторить их. Замена, обрезание или
изменение файла возвращает `SD_FILE_BROWSER_FILE_CHANGED`. В API нет функций
записи, удаления, rename, выполнения Markdown или чтения служебных каталогов.
## Shared source
Canonical source: `templates/c/sd-file-browser`. Used by `home/climate`; its old paths are compatibility includes. Board-specific ports remain in the application. Change this library, not the forwarding files.

View File

@@ -0,0 +1,177 @@
#include "sd_file_browser.h"
#include <stddef.h>
#include <string.h>
static uint8_t has_temperature_name(const char *name)
{
const size_t length = strlen(name);
const char prefix[] = "temperature_";
return (uint8_t)((length > (sizeof(prefix) - 1U + 3U)) &&
(strncmp(name, prefix, sizeof(prefix) - 1U) == 0) &&
(strcmp(name + length - 3U, ".md") == 0));
}
/* Сравнение расширения без учёта регистра для короткого суффикса ASCII. */
static uint8_t has_suffix_ci(const char *name, const char *suffix)
{
const size_t name_length = strlen(name);
const size_t suffix_length = strlen(suffix);
size_t index;
if (name_length <= suffix_length) {
return 0U;
}
for (index = 0U; index < suffix_length; ++index) {
char left = name[name_length - suffix_length + index];
char right = suffix[index];
if ((left >= 'A') && (left <= 'Z')) {
left = (char)(left - 'A' + 'a');
}
if ((right >= 'A') && (right <= 'Z')) {
right = (char)(right - 'A' + 'a');
}
if (left != right) {
return 0U;
}
}
return 1U;
}
/* Сырой образ прошивки: .bin или .fw. Intel HEX здесь не принимается — его
* адреса лежат внутри файла и требуют разбора, недоступного на этом этапе. */
static uint8_t has_firmware_name(const char *name)
{
return (uint8_t)((has_suffix_ci(name, ".bin") != 0U) ||
(has_suffix_ci(name, ".fw") != 0U));
}
/* Разрешены печатные ASCII-имена без разделителей ОС и управляющих байтов. */
static uint8_t path_character_is_safe(char character)
{
unsigned char value = (unsigned char)character;
return (uint8_t)((value >= 0x20U) && (value <= 0x7EU) &&
(character != '\\') && (character != ':'));
}
SdFileBrowserStatus SdFileBrowser_ValidatePath(const char *path)
{
size_t length;
size_t component_start;
size_t index;
/* strlen вызывается только после явной проверки внешнего указателя. */
if (path == NULL) {
return SD_FILE_BROWSER_INVALID_PATH;
}
length = strlen(path);
if ((length == 0U) || (length >= SD_FILE_BROWSER_MAX_PATH) ||
(path[0] != '/')) {
/* Абсолютный путь накопителя и усечённый путь fail-closed отклоняются. */
return SD_FILE_BROWSER_INVALID_PATH;
}
if (length == 1U) {
return SD_FILE_BROWSER_OK;
}
/* Компоненты анализируются за один проход, включая завершающий ноль. */
component_start = 1U;
for (index = 1U; index <= length; ++index) {
char character = path[index];
if ((character == '/') || (character == '\0')) {
size_t component_length = index - component_start;
if ((component_length == 0U) ||
(component_length > SD_FILE_BROWSER_MAX_NAME) ||
((component_length == 1U) &&
(path[component_start] == '.')) ||
((component_length == 2U) &&
(path[component_start] == '.') &&
(path[component_start + 1U] == '.'))) {
/* Пустые, точечные и слишком длинные компоненты не нормализуются. */
return SD_FILE_BROWSER_INVALID_PATH;
}
component_start = index + 1U;
} else if (path_character_is_safe(character) == 0U) {
/* Кодировка текущего FatFs-порта ASCII; байты UTF-8 не угадываются. */
return SD_FILE_BROWSER_INVALID_PATH;
}
}
return SD_FILE_BROWSER_OK;
}
SdFileBrowserStatus SdFileBrowser_ValidateLogFilePath(const char *path)
{
const char root[] = "/niceOne/mounth/";
const char *relative;
const char *separator;
const char *name;
SdFileBrowserStatus status = SdFileBrowser_ValidatePath(path);
if (status != SD_FILE_BROWSER_OK) {
return status;
}
/* TEMPLOG.MD сохранён только для старых карт, где журнал лежал в корне. */
if (strcmp(path, "/TEMPLOG.MD") == 0) {
return SD_FILE_BROWSER_OK;
}
if (strncmp(path, root, sizeof(root) - 1U) != 0) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
relative = path + sizeof(root) - 1U;
separator = strchr(relative, '/');
/* Штатное дерево содержит ровно месяц и имя; обход дополнительных уровней запрещён. */
if ((separator == NULL) || (separator == relative) ||
(strchr(separator + 1U, '/') != NULL)) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
name = strrchr(path, '/');
if ((name == NULL) || (has_temperature_name(name + 1U) == 0U)) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
return SD_FILE_BROWSER_OK;
}
SdFileBrowserStatus SdFileBrowser_ValidateFirmwarePath(const char *path)
{
const char root[] = "/niceOne/firmware/";
const char *name;
SdFileBrowserStatus status = SdFileBrowser_ValidatePath(path);
if (status != SD_FILE_BROWSER_OK) {
return status;
}
/* Образы лежат ровно в одном каталоге; вложенные уровни не обходятся. */
if (strncmp(path, root, sizeof(root) - 1U) != 0) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
name = path + sizeof(root) - 1U;
if ((name[0] == '\0') || (strchr(name, '/') != NULL)) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
if (has_firmware_name(name) == 0U) {
return SD_FILE_BROWSER_NOT_LOG_FILE;
}
return SD_FILE_BROWSER_OK;
}
SdFileBrowserStatus SdFileBrowser_ListPage(
const SdFileBrowser *browser, const char *path, uint16_t offset,
uint8_t limit, SdFileBrowserPage *page)
{
SdFileBrowserStatus status;
/* limit проверяется до очистки, чтобы invalid call не трогал чужую память. */
if ((browser == NULL) || (browser->list_page == NULL) || (page == NULL) ||
(limit == 0U) || (limit > SD_FILE_BROWSER_MAX_PAGE_ITEMS)) {
return SD_FILE_BROWSER_INVALID_CURSOR;
}
status = SdFileBrowser_ValidatePath(path);
if (status != SD_FILE_BROWSER_OK) {
return status;
}
/* Нулевой tail исключает утечку содержимого предыдущего Modbus-ответа. */
memset(page, 0, sizeof(*page));
/* Portable слой не знает FatFs/HAL и передаёт только проверенный запрос. */
return browser->list_page(browser->context, path, offset, limit, page);
}

View File

@@ -0,0 +1,67 @@
#include "sd_file_browser.h"
#include <stdio.h>
#include <string.h>
static unsigned checks;
static unsigned failures;
static unsigned calls;
#define CHECK(condition) do { ++checks; if (!(condition)) ++failures; } while (0)
/* Mock подтверждает, что core передаёт только проверенный bounded запрос. */
static SdFileBrowserStatus mock_list(
void *context, const char *path, uint16_t offset, uint8_t limit,
SdFileBrowserPage *page)
{
(void)context;
++calls;
CHECK(strcmp(path, "/niceOne") == 0);
CHECK(offset == 3U);
CHECK(limit == 2U);
memcpy(page->items[0].name, "July", sizeof("July"));
page->items[0].is_directory = 1U;
page->count = 1U;
page->next_offset = 4U;
return SD_FILE_BROWSER_OK;
}
int main(void)
{
static const char *const invalid[] = {
"", "niceOne", "/../secret", "/./logs", "/a//b", "/a\\b",
"/a:b", "/trailing/"
};
SdFileBrowser browser = { NULL, mock_list };
SdFileBrowserPage page;
unsigned index;
CHECK(SdFileBrowser_ValidatePath("/") == SD_FILE_BROWSER_OK);
CHECK(SdFileBrowser_ValidateLogFilePath("/TEMPLOG.MD") == SD_FILE_BROWSER_OK);
CHECK(SdFileBrowser_ValidateLogFilePath(
"/niceOne/mounth/2026-07_July/temperature_2026-07-18_10-00-00.md") ==
SD_FILE_BROWSER_OK);
CHECK(SdFileBrowser_ValidateLogFilePath("/niceOne/settings/private.json") ==
SD_FILE_BROWSER_NOT_LOG_FILE);
CHECK(SdFileBrowser_ValidateLogFilePath("/niceOne/mounth/fw.bin") ==
SD_FILE_BROWSER_NOT_LOG_FILE);
CHECK(SdFileBrowser_ValidateLogFilePath("/niceOne/mounth/temperature_bad.md") ==
SD_FILE_BROWSER_NOT_LOG_FILE);
CHECK(SdFileBrowser_ValidatePath("/niceOne/2026-07_July") ==
SD_FILE_BROWSER_OK);
for (index = 0U; index < sizeof(invalid) / sizeof(invalid[0]); ++index) {
CHECK(SdFileBrowser_ValidatePath(invalid[index]) ==
SD_FILE_BROWSER_INVALID_PATH);
}
memset(&page, 0xA5, sizeof(page));
CHECK(SdFileBrowser_ListPage(&browser, "/niceOne", 3U, 2U, &page) ==
SD_FILE_BROWSER_OK);
CHECK(calls == 1U);
CHECK(page.count == 1U);
CHECK(strcmp(page.items[0].name, "July") == 0);
CHECK(SdFileBrowser_ListPage(&browser, "/niceOne", 0U, 4U, &page) ==
SD_FILE_BROWSER_INVALID_CURSOR);
CHECK(calls == 1U);
(void)printf("SD browser core: %u checks, %u failures\n", checks, failures);
return failures == 0U ? 0 : 1;
}

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,30 @@
/** @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).
* On success, workspace blocks used by host formula annotations are:
* work[0..2*count): sorted original interleaved x,y pairs;
* work[2*count..3*count): unique x normalized to [0,1];
* work[3*count..4*count): unique averaged y divided by max(1,max(abs(y)));
* work[4*count..5*count): PCHIP first / spline second derivatives in these
* normalized coordinates (only meta[1] entries in each unique-data block).
* 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",

View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.13)
project(settings_backup C)
add_library(settings_backup STATIC Src/settings_backup.c)
target_include_directories(settings_backup PUBLIC Inc)
set_target_properties(settings_backup PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED YES)
enable_testing()
add_executable(test_settings_backup Tests/test_settings_backup.c)
target_link_libraries(test_settings_backup PRIVATE settings_backup)
add_test(NAME test_settings_backup COMMAND test_settings_backup)

View File

@@ -0,0 +1,125 @@
#ifndef SETTINGS_BACKUP_H
#define SETTINGS_BACKUP_H
/* Переносимое ядро резервирования не включает HAL, FatFs, AppStorage и Modbus. */
#include <stddef.h>
#include <stdint.h>
#define SETTINGS_BACKUP_SCHEMA_VERSION 1U
#define SETTINGS_BACKUP_MAX_ROOMS 32U
#define SETTINGS_BACKUP_MAX_SENSORS 32U
#define SETTINGS_BACKUP_MAX_PATH 160U
/* Календарь передаётся приложением вместе с признаком доверия к RTC. */
typedef struct {
uint16_t year;
uint8_t month;
uint8_t date;
uint8_t hours;
uint8_t minutes;
uint8_t seconds;
uint8_t valid;
} SettingsBackupDateTime;
/* Одна комната содержит только документированные инженерные параметры. */
typedef struct {
uint16_t setpoint_x10;
uint8_t hysteresis_x10;
uint8_t calibration_start_pct;
uint16_t full_open_time_100ms;
uint8_t target_position_pct;
uint8_t confirmed_position_pct;
} SettingsBackupRoom;
/* В JSON всегда попадает полный ROM, включая family и Dallas CRC. */
typedef struct {
uint8_t rom[8];
uint8_t room;
} SettingsBackupSensor;
/* Снимок создаётся адаптером только из committed-записи AppStorage. */
typedef struct {
uint32_t revision;
uint32_t content_crc32;
uint8_t requested_backend;
uint8_t active_backend;
uint8_t room_count;
uint8_t sensor_count;
SettingsBackupDateTime rtc;
SettingsBackupRoom rooms[SETTINGS_BACKUP_MAX_ROOMS];
SettingsBackupSensor sensors[SETTINGS_BACKUP_MAX_SENSORS];
} SettingsBackupSnapshot;
/* Короткий набор результатов позволяет порту отобразить SD absent/full/I/O. */
typedef enum {
SETTINGS_BACKUP_PORT_OK = 0,
SETTINGS_BACKUP_PORT_NOT_READY,
SETTINGS_BACKUP_PORT_NO_SPACE,
SETTINGS_BACKUP_PORT_IO_ERROR
} SettingsBackupPortResult;
/* Наблюдаемое состояние не скрывает retry и отказ носителя от Modbus/GUI. */
typedef enum {
SETTINGS_BACKUP_IDLE = 0,
SETTINGS_BACKUP_PENDING,
SETTINGS_BACKUP_LAST_OK,
SETTINGS_BACKUP_LAST_NOT_READY,
SETTINGS_BACKUP_LAST_NO_SPACE,
SETTINGS_BACKUP_LAST_IO_ERROR,
SETTINGS_BACKUP_LAST_INVALID_SNAPSHOT
} SettingsBackupStatus;
/* Все операции платформы ограничены одной файловой транзакцией за вызов. */
typedef struct {
uint32_t (*tick_ms)(void *context);
SettingsBackupPortResult (*mount)(void *context);
void (*unmount)(void *context);
SettingsBackupPortResult (*find_identical)(void *context,
uint32_t revision, uint32_t crc32, uint8_t *found);
SettingsBackupPortResult (*resolve_paths)(void *context,
const SettingsBackupSnapshot *snapshot, char *final_path,
uint32_t final_capacity, char *temporary_path,
uint32_t temporary_capacity);
SettingsBackupPortResult (*atomic_write)(void *context,
const char *temporary_path, const char *final_path,
const char *utf8_json, uint32_t length);
SettingsBackupPortResult (*apply_retention)(void *context,
uint16_t maximum_copies);
} SettingsBackupPort;
/* Контекст хранит pending-снимок: вызывающий может освобождать свой буфер. */
typedef struct {
SettingsBackupPort port;
void *port_context;
SettingsBackupSnapshot pending_snapshot;
char *json_buffer;
uint32_t json_capacity;
char final_path[SETTINGS_BACKUP_MAX_PATH];
char temporary_path[SETTINGS_BACKUP_MAX_PATH];
uint32_t next_retry_ms;
uint16_t maximum_copies;
uint8_t phase;
uint8_t retry_count;
uint8_t pending;
SettingsBackupStatus status;
} SettingsBackup;
/* Инициализация не обращается к SD и потому безопасна во время старта МК. */
uint8_t SettingsBackup_Init(SettingsBackup *instance,
const SettingsBackupPort *port, void *port_context,
char *json_buffer, uint32_t json_capacity, uint16_t maximum_copies);
/* Новая committed-ревизия заменяет только ещё не записанный pending-снимок. */
uint8_t SettingsBackup_Notify(SettingsBackup *instance,
const SettingsBackupSnapshot *snapshot);
/* Service делает не более одного mount/scan/write/retention шага за проход. */
void SettingsBackup_Service(SettingsBackup *instance);
/* Форматирование и CRC открыты для host-тестов без файловой системы. */
uint32_t SettingsBackup_CalculateContentCrc(const SettingsBackupSnapshot *snapshot);
int SettingsBackup_FormatJson(const SettingsBackupSnapshot *snapshot,
char *output, uint32_t capacity);
SettingsBackupStatus SettingsBackup_GetStatus(const SettingsBackup *instance);
#endif /* SETTINGS_BACKUP_H */

Some files were not shown because too many files have changed in this diff Show More