Compare commits
6 Commits
main
...
codex/set-
| Author | SHA1 | Date | |
|---|---|---|---|
| f3937eaf14 | |||
| de4609ea00 | |||
| caf30f2ed6 | |||
| 513e79b127 | |||
| cc22c803d1 | |||
| 2ad29e7ffd |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,3 +24,6 @@ __pycache__/
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
.codex-build/
|
||||
|
||||
# Generated native library (built before packaging)
|
||||
python/protocan/native/
|
||||
|
||||
@@ -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)
|
||||
|
||||
11
c/ds18b20/instance/CMakeLists.txt
Normal file
11
c/ds18b20/instance/CMakeLists.txt
Normal 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
384
c/ds18b20/instance/HELP.md
Normal 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 м,
|
||||
отключение/подключение датчика при поиске, отмену и восстановление опроса.
|
||||
126
c/ds18b20/instance/Inc/ds18b20.h
Normal file
126
c/ds18b20/instance/Inc/ds18b20.h
Normal 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
|
||||
17
c/ds18b20/instance/Inc/ds18b20_config.h
Normal file
17
c/ds18b20/instance/Inc/ds18b20_config.h
Normal 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
|
||||
235
c/ds18b20/instance/PORTING.md
Normal file
235
c/ds18b20/instance/PORTING.md
Normal 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 переносите два поля
|
||||
диагностики лишь в свободные регистры, не сдвигая последующие окна.
|
||||
97
c/ds18b20/instance/README.md
Normal file
97
c/ds18b20/instance/README.md
Normal 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.
|
||||
353
c/ds18b20/instance/Src/ds18b20.c
Normal file
353
c/ds18b20/instance/Src/ds18b20.c
Normal 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;
|
||||
}
|
||||
31
c/ds18b20/instance/Tests/run_host_tests.ps1
Normal file
31
c/ds18b20/instance/Tests/run_host_tests.ps1
Normal 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
|
||||
92
c/ds18b20/instance/Tests/test_ds18b20_copy_delay.c
Normal file
92
c/ds18b20/instance/Tests/test_ds18b20_copy_delay.c
Normal 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;
|
||||
}
|
||||
152
c/ds18b20/instance/Tests/test_ds18b20_incremental_search.c
Normal file
152
c/ds18b20/instance/Tests/test_ds18b20_incremental_search.c
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
11
c/flash-storage/CMakeLists.txt
Normal file
11
c/flash-storage/CMakeLists.txt
Normal 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)
|
||||
104
c/flash-storage/Inc/flash_storage.h
Normal file
104
c/flash-storage/Inc/flash_storage.h
Normal 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
|
||||
8
c/flash-storage/Inc/flash_storage_config.h
Normal file
8
c/flash-storage/Inc/flash_storage_config.h
Normal 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
43
c/flash-storage/README.md
Normal 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.
|
||||
63
c/flash-storage/Src/flash_storage.c
Normal file
63
c/flash-storage/Src/flash_storage.c
Normal file
File diff suppressed because one or more lines are too long
27
c/flash-storage/Tests/test_flash_storage.c
Normal file
27
c/flash-storage/Tests/test_flash_storage.c
Normal file
File diff suppressed because one or more lines are too long
9
c/modbus/CMakeLists.txt
Normal file
9
c/modbus/CMakeLists.txt
Normal 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
41
c/modbus/README.md
Normal 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.
|
||||
15
c/modbus/include/modbus_data.h
Normal file
15
c/modbus/include/modbus_data.h
Normal 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
|
||||
836
c/modbus/ports/stm32-legacy/modbus.c
Normal file
836
c/modbus/ports/stm32-legacy/modbus.c
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
57
c/modbus/src/modbus_data.c
Normal file
57
c/modbus/src/modbus_data.c
Normal 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;
|
||||
}
|
||||
29
c/modbus/tests/test_modbus_data.c
Normal file
29
c/modbus/tests/test_modbus_data.c
Normal 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;
|
||||
}
|
||||
10
c/sd-file-browser/CMakeLists.txt
Normal file
10
c/sd-file-browser/CMakeLists.txt
Normal 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)
|
||||
82
c/sd-file-browser/Inc/sd_file_browser.h
Normal file
82
c/sd-file-browser/Inc/sd_file_browser.h
Normal 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 */
|
||||
30
c/sd-file-browser/README.md
Normal file
30
c/sd-file-browser/README.md
Normal 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.
|
||||
177
c/sd-file-browser/Src/sd_file_browser.c
Normal file
177
c/sd-file-browser/Src/sd_file_browser.c
Normal 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);
|
||||
}
|
||||
67
c/sd-file-browser/Tests/test_sd_file_browser.c
Normal file
67
c/sd-file-browser/Tests/test_sd_file_browser.c
Normal 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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
146
c/set-protocol/docs/PLOT_PROCESSING.md
Normal file
146
c/set-protocol/docs/PLOT_PROCESSING.md
Normal 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 не требуют.
|
||||
146
c/set-protocol/docs/SIGNAL_GENERATOR.md
Normal file
146
c/set-protocol/docs/SIGNAL_GENERATOR.md
Normal 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 эмулятора.
|
||||
|
||||
Аппаратная проверка амплитуды, периода и формы осциллографом не выполнена.
|
||||
24
c/set-protocol/include/firmware_image.h
Normal file
24
c/set-protocol/include/firmware_image.h
Normal 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
|
||||
30
c/set-protocol/include/set_signal.h
Normal file
30
c/set-protocol/include/set_signal.h
Normal 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
|
||||
59
c/set-protocol/include/set_wavegen.h
Normal file
59
c/set-protocol/include/set_wavegen.h
Normal 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
|
||||
53
c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.c
Normal file
53
c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.c
Normal 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);
|
||||
}
|
||||
12
c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.h
Normal file
12
c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.h
Normal 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
|
||||
37
c/set-protocol/ports/stm32g474-wavegen/set_wave_g474.c
Normal file
37
c/set-protocol/ports/stm32g474-wavegen/set_wave_g474.c
Normal 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;
|
||||
}
|
||||
10
c/set-protocol/ports/stm32g474-wavegen/set_wave_g474.h
Normal file
10
c/set-protocol/ports/stm32g474-wavegen/set_wave_g474.h
Normal 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
|
||||
88
c/set-protocol/src/firmware_image.c
Normal file
88
c/set-protocol/src/firmware_image.c
Normal 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;
|
||||
}
|
||||
115
c/set-protocol/src/set_signal.c
Normal file
115
c/set-protocol/src/set_signal.c
Normal 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;
|
||||
}
|
||||
214
c/set-protocol/src/set_wavegen.c
Normal file
214
c/set-protocol/src/set_wavegen.c
Normal 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&®<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&®<SET_WAVE_BASE+9)||(reg>=SET_WAVE_DATA&®<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&®-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;
|
||||
}
|
||||
22
c/set-protocol/tests/test_firmware_image.c
Normal file
22
c/set-protocol/tests/test_firmware_image.c
Normal 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;
|
||||
}
|
||||
51
c/set-protocol/tests/test_signal_wave.c
Normal file
51
c/set-protocol/tests/test_signal_wave.c
Normal 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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
10
c/settings-backup/CMakeLists.txt
Normal file
10
c/settings-backup/CMakeLists.txt
Normal 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)
|
||||
125
c/settings-backup/Inc/settings_backup.h
Normal file
125
c/settings-backup/Inc/settings_backup.h
Normal 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
Reference in New Issue
Block a user