Add embedded storage drivers and extend firmware metadata

This commit is contained in:
2026-09-27 01:44:59 +03:00
parent 795a1279b1
commit 2ad29e7ffd
52 changed files with 5801 additions and 3 deletions

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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