Compare commits
92 Commits
def3eb08f3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 427fc70100 | |||
| 6cbce6c360 | |||
| 3463e95eb3 | |||
| 025a3fde28 | |||
| a63c1dd7d8 | |||
| 083ab55cb6 | |||
| d2578299d8 | |||
| fdfae8fa6c | |||
| 66fad32a81 | |||
| f7a6f767e1 | |||
| 675ce063f0 | |||
| 3d9174a124 | |||
| 46ecbe3210 | |||
| 6df5674996 | |||
| 196d083098 | |||
| c0ee7c46ea | |||
| 3457657979 | |||
| e3e30fdad7 | |||
| 79411dbc26 | |||
| f3937eaf14 | |||
| de4609ea00 | |||
| caf30f2ed6 | |||
| 513e79b127 | |||
| cc22c803d1 | |||
| 2ad29e7ffd | |||
| 795a1279b1 | |||
| 5260186c16 | |||
| 80ba17d77d | |||
| 10bfa587c7 | |||
| ac3025ee32 | |||
| 37508f058b | |||
| 3334f4cddd | |||
| aedd5fc082 | |||
| 9ba3742b29 | |||
| 4f23f48896 | |||
| 8e9fd144ca | |||
| 49c65ea78d | |||
| 34f728cfaa | |||
| 72205216f5 | |||
| 5d48ddf0b0 | |||
| 245d486a63 | |||
| e262b0acdd | |||
| ed92ec346e | |||
| 3d374e7777 | |||
| d03e039cb4 | |||
| d002b14e4e | |||
| 11007b0704 | |||
| 821202b655 | |||
| 8596918bf7 | |||
| bada3bbe8d | |||
| 87f2f2f56b | |||
| 5523f049ec | |||
| b08ea02a5c | |||
| 1b3a6318eb | |||
| 57622ccd66 | |||
| ba529f579c | |||
| 4f788d39d6 | |||
| 1799b00167 | |||
| 940d9ada76 | |||
| 6eaa47d3a8 | |||
| 1f49ba49bb | |||
| b7f00acb24 | |||
| 70b2ee3cad | |||
| b3ec401318 | |||
| 666173b2f0 | |||
| 17259f5e28 | |||
| 79ac0d52a7 | |||
| 52d687b5ae | |||
| 7897fe9f0d | |||
| 45582cda4a | |||
| 2bb9fea90a | |||
| 6392d36741 | |||
| 0c635ce1be | |||
| 5caca07ad4 | |||
| 31a111ee46 | |||
| 20f63bc389 | |||
| 0a30c39e07 | |||
| e8490b3122 | |||
| a14140f794 | |||
| 261f2ca6f1 | |||
| c8eec55978 | |||
| ab60e58318 | |||
| 94fc8c5ef2 | |||
| 58da8aa2fe | |||
| bd8d22312e | |||
| 01de7651e8 | |||
| 34c46ec679 | |||
| 23e37d1c4d | |||
| aea2a79d2c | |||
| bb7c94f1b7 | |||
| ce6c4327a4 | |||
| 3cf85a278e |
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/
|
||||
|
||||
27
Doxyfile
Normal file
27
Doxyfile
Normal file
@@ -0,0 +1,27 @@
|
||||
PROJECT_NAME = "templates"
|
||||
PROJECT_BRIEF = "Переносимые библиотеки и правила интеграции"
|
||||
OUTPUT_DIRECTORY = .codex-build/doxygen
|
||||
CREATE_SUBDIRS = NO
|
||||
OUTPUT_LANGUAGE = Russian
|
||||
INPUT = README.md NEW_PROJECT.md CONTRIBUTING.md doc/submodules.dox c python
|
||||
FILE_PATTERNS = *.h *.c *.dox *.md
|
||||
RECURSIVE = YES
|
||||
USE_MDFILE_AS_MAINPAGE = README.md
|
||||
MARKDOWN_SUPPORT = YES
|
||||
AUTOLINK_SUPPORT = YES
|
||||
EXTRACT_ALL = NO
|
||||
EXTRACT_STATIC = NO
|
||||
SOURCE_BROWSER = YES
|
||||
INLINE_SOURCES = NO
|
||||
STRIP_FROM_PATH = .
|
||||
GENERATE_HTML = YES
|
||||
HTML_OUTPUT = html
|
||||
GENERATE_TREEVIEW = YES
|
||||
DISABLE_INDEX = NO
|
||||
GENERATE_LATEX = NO
|
||||
QUIET = YES
|
||||
WARNINGS = YES
|
||||
WARN_IF_UNDOCUMENTED = NO
|
||||
WARN_IF_DOC_ERROR = YES
|
||||
WARN_AS_ERROR = FAIL_ON_WARNINGS
|
||||
HAVE_DOT = NO
|
||||
185
NEW_PROJECT.md
185
NEW_PROJECT.md
@@ -120,3 +120,188 @@ USB gs_usb и конкретной разводки адаптера; перен
|
||||
|
||||
Так новый проект не является копией старого: он использует общие ядра одной
|
||||
версии, а аппаратные отличия видны в одном небольшом каталоге `port/`.
|
||||
|
||||
## Слияние и обновление `templates` как Git submodule
|
||||
|
||||
### Что именно хранит основной проект
|
||||
|
||||
Основной проект не хранит содержимое `lib/templates` в своей истории. Вместо
|
||||
этого он хранит **gitlink** — ссылку на один конкретный commit репозитория
|
||||
`templates`. Поэтому при работе есть два независимых уровня истории:
|
||||
|
||||
1. изменения библиотек сливаются и публикуются в репозитории `templates`;
|
||||
2. основной проект отдельным коммитом переводит gitlink на проверенную ревизию
|
||||
`templates`.
|
||||
|
||||
Обычный `git merge` в основном проекте не переносит исходники между ветками
|
||||
`templates` и не создаёт merge-коммит внутри сабмодуля. Он может только выбрать
|
||||
одну из уже существующих ссылок на commit. Если ссылки разошлись, сначала нужно
|
||||
получить общий commit в самом `templates`, а затем зафиксировать его в основном
|
||||
проекте.
|
||||
|
||||
Текущую зафиксированную ревизию удобно смотреть из корня основного проекта:
|
||||
|
||||
```bash
|
||||
git submodule status lib/templates
|
||||
git diff --submodule=log
|
||||
```
|
||||
|
||||
Символ `-` перед SHA в `git submodule status` означает, что сабмодуль ещё не
|
||||
инициализирован; `+` — что рабочая копия сабмодуля находится не на том commit,
|
||||
который записан в основном проекте; `U` — конфликт gitlink.
|
||||
|
||||
### Клонирование проекта и переключение его веток
|
||||
|
||||
Новый клон лучше сразу создавать вместе с сабмодулями:
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules <url-основного-проекта>
|
||||
cd <основной-проект>
|
||||
```
|
||||
|
||||
Если проект уже клонирован:
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
После `git switch`, `git checkout`, `git pull` или завершения слияния основной
|
||||
проект может начать ссылаться на другую ревизию `templates`. Рабочее дерево
|
||||
сабмодуля следует явно привести к записанному состоянию:
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Это нормальный и воспроизводимый режим: внутри `lib/templates` обычно будет
|
||||
detached HEAD, потому что проект фиксирует commit, а не ветку. Не следует делать
|
||||
там `git pull`, пока не выбрана рабочая ветка и не понятно, какую ревизию должен
|
||||
получить основной проект.
|
||||
|
||||
### Обычное обновление проекта на новую версию `templates`
|
||||
|
||||
Сначала изменение должно быть проверено, закоммичено, отправлено и слито в
|
||||
целевую ветку самого репозитория `templates`. После этого из корня основного
|
||||
проекта выполняется:
|
||||
|
||||
```bash
|
||||
git -C lib/templates fetch origin
|
||||
git -C lib/templates switch --detach origin/master
|
||||
git add lib/templates
|
||||
git diff --cached --submodule=log
|
||||
git commit -m "build(templates): обновить общие библиотеки"
|
||||
```
|
||||
|
||||
Перед коммитом нужно собрать основной проект и прогнать его тесты: успешные
|
||||
тесты в `templates` подтверждают работу библиотеки, но не проверяют конкретный
|
||||
порт, настройки платы и интеграцию приложения.
|
||||
|
||||
Коммит основного проекта содержит только переход со старого SHA сабмодуля на
|
||||
новый. Случайные незакоммиченные файлы внутри `lib/templates` в него не входят.
|
||||
Проверить оба уровня перед фиксацией можно так:
|
||||
|
||||
```bash
|
||||
git status
|
||||
git -C lib/templates status
|
||||
git diff --submodule=log
|
||||
```
|
||||
|
||||
Не нужно обновлять сабмодуль на последний `master` автоматически при каждой
|
||||
сборке. Зафиксированный SHA нужен именно для того, чтобы одна и та же версия
|
||||
проекта всегда собиралась с одной и той же версией библиотек.
|
||||
|
||||
### Если библиотеку правят из рабочего дерева основного проекта
|
||||
|
||||
Нельзя коммитить изменение, оставаясь на detached HEAD: такой commit легко
|
||||
потерять при следующем `git submodule update`. Сначала внутри сабмодуля создаётся
|
||||
обычная ветка:
|
||||
|
||||
```bash
|
||||
git -C lib/templates fetch origin
|
||||
git -C lib/templates switch -c feat/<имя> origin/master
|
||||
```
|
||||
|
||||
Затем изменения коммитятся и отправляются именно в репозиторий `templates`:
|
||||
|
||||
```bash
|
||||
git -C lib/templates add <файлы>
|
||||
git -C lib/templates commit -m "feat(<библиотека>): <описание>"
|
||||
git -C lib/templates push -u origin feat/<имя>
|
||||
```
|
||||
|
||||
После проверки ветка сливается в `master` репозитория `templates`. Только после
|
||||
публикации итогового commit основной проект обновляет свой gitlink по процедуре
|
||||
из предыдущего раздела. Не следует отправлять в общий основной проект ссылку на
|
||||
commit сабмодуля, которого ещё нет на сервере: остальные разработчики и CI не
|
||||
смогут его получить.
|
||||
|
||||
### Что происходит при слиянии веток основного проекта
|
||||
|
||||
Возможны три ситуации:
|
||||
|
||||
1. Gitlink изменён только в одной ветке. Git обычно принимает эту ревизию
|
||||
автоматически. После merge нужно выполнить `git submodule update` и тесты.
|
||||
2. Обе ветки указывают на разные commits, но один commit `templates` является
|
||||
предком другого. Следует выбрать более новый проверенный commit, выполнить
|
||||
`git add lib/templates` и продолжить merge.
|
||||
3. Обе ветки указывают на расходящиеся commits `templates`. Это настоящий
|
||||
конфликт истории сабмодуля: выбирать один SHA наугад нельзя, потому что так
|
||||
потеряются изменения второй ветки.
|
||||
|
||||
При конфликте сначала смотрят, какие ссылки пришли с обеих сторон:
|
||||
|
||||
```bash
|
||||
git ls-files -u lib/templates
|
||||
git -C lib/templates fetch origin
|
||||
git -C lib/templates log --oneline --graph --decorate --all
|
||||
```
|
||||
|
||||
Затем в отдельной ветке **репозитория `templates`** объединяют оба commit,
|
||||
устраняют конфликты исходников, запускают тесты и публикуют результат:
|
||||
|
||||
```bash
|
||||
git -C lib/templates switch -c merge/<имя> <sha-одной-стороны>
|
||||
git -C lib/templates merge <sha-другой-стороны>
|
||||
# исправить конфликты и запустить тесты templates
|
||||
git -C lib/templates add <исправленные-файлы>
|
||||
git -C lib/templates commit
|
||||
git -C lib/templates push -u origin merge/<имя>
|
||||
```
|
||||
|
||||
Если общий merge-коммит уже существует в `origin/master`, создавать ещё один не
|
||||
нужно: достаточно выбрать существующий SHA. После получения итогового commit
|
||||
конфликт завершается на уровне основного проекта:
|
||||
|
||||
```bash
|
||||
git -C lib/templates switch --detach <итоговый-sha>
|
||||
git add lib/templates
|
||||
git diff --cached --submodule=log
|
||||
git merge --continue
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
`git add lib/templates` здесь отмечает разрешённым именно gitlink. Не нужно
|
||||
удалять каталог, копировать библиотеку поверх него или разрешать конфликт как
|
||||
обычный текстовый файл.
|
||||
|
||||
### Что должно попасть в merge request основного проекта
|
||||
|
||||
Перед отправкой результата следует проверить:
|
||||
|
||||
1. итоговый commit `templates` доступен в удалённом репозитории;
|
||||
2. `git -C lib/templates status` не показывает локальных изменений;
|
||||
3. `git submodule status` не начинается с `+`, `-` или `U`;
|
||||
4. `git diff --submodule=log <целевая-ветка>...HEAD` показывает ожидаемый набор
|
||||
commits библиотек;
|
||||
5. тесты `templates`, сборка основного проекта и проверки на целевой плате
|
||||
прошли;
|
||||
6. в основном проекте закоммичены необходимые изменения адаптеров, конфигурации
|
||||
и сам новый gitlink.
|
||||
|
||||
После получения ветки другой разработчик восстанавливает ровно выбранное
|
||||
состояние одной командой:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
16
README.md
16
README.md
@@ -48,8 +48,15 @@ templates/
|
||||
|
||||
### Python
|
||||
|
||||
Экспериментальный [`c/gas-logger`](c/gas-logger/README.md): непрерывная запись
|
||||
в два банка RAM, снимки через GAS/Modbus/ProtoCAN, JSON-карта имён и адресов,
|
||||
проектный порт ПМ35 и MCU-порты C28x / STM32. Клиент в `set_devices.gas_logger`.
|
||||
|
||||
| Модуль | Что делает | Зависимости |
|
||||
|---|---|---|
|
||||
| [`python/setprotocol/firmware_database.py`](python/setprotocol/firmware_database.py) | база прошивок: HTTPS-каталог, скачивание с SHA-256 и кэшем, публикация в Gitea без SETGUI; [подключение и CLI](tools/firmware-publish/DATABASE.md) | stdlib, Python 3.10+ |
|
||||
| [`python/setprotocol/firmware_publish.py`](python/setprotocol/firmware_publish.py) | общие метаданные публикации, SHA-256, release tag и обновление каталога прошивок; [подключение](tools/firmware-publish/PORTING.md) | stdlib; сетевой адаптер в SETGUI |
|
||||
| [`python/setprotocol/firmware_catalog.py`](python/setprotocol/firmware_catalog.py) | модель и parser каталога `firmware.releases` | stdlib |
|
||||
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
|
||||
| [`python/protocan/trends.py`](python/protocan/trends.py) | общие настройки графиков, ограниченная история, ctypes-декодер GAS/raw CAN | stdlib, опционально SETProtocol DLL/SO |
|
||||
|
||||
@@ -69,6 +76,11 @@ templates/
|
||||
В ней отдельно описаны граница ядра, ABI, память, три wire format и состояние
|
||||
портов Windows, Android, Linux и MCU.
|
||||
|
||||
Обзор всего репозитория, включая слияние при подключении через submodule:
|
||||
[`doc/index.html`](doc/index.html). Справочник Doxygen собирается командой
|
||||
`powershell -ExecutionPolicy Bypass -File doc/build-doxygen.ps1` в
|
||||
`.codex-build/doxygen/html/index.html`.
|
||||
|
||||
## Как подключить к проекту
|
||||
|
||||
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
|
||||
@@ -78,6 +90,10 @@ git submodule add https://git.rd12.ru/Andrey/templates.git lib/templates
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Как обновлять зафиксированную ревизию, переносить изменения из проекта и
|
||||
разрешать конфликт gitlink при слиянии веток, подробно описано в разделе
|
||||
[`Слияние и обновление templates как Git submodule`](NEW_PROJECT.md#слияние-и-обновление-templates-как-git-submodule).
|
||||
|
||||
Дальше в сборку добавляются только нужные каталоги:
|
||||
|
||||
```
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# Сборка библиотеки can_sensor. Состав исходников и публичные include-пути задают подключение
|
||||
# к проекту потребителя. Файл также собирает и регистрирует хостовые проверки; запускать их
|
||||
# следует через CTest из каталога сборки.
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(can_sensor C)
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Обмен настройками датчиков DS18B20 через однокадровые сообщения SETCAN. ROM идентифицирует
|
||||
* датчик независимо от его позиции; прикладное состояние и отправка CAN предоставляются через
|
||||
* интерфейс библиотеки.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file can_sensor.c
|
||||
* @brief Однокадровый транспорт SETCAN SETTINGS для локаций DS18B20.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Обмен настройками датчиков DS18B20 через однокадровые сообщения SETCAN. ROM идентифицирует
|
||||
* датчик независимо от его позиции; прикладное состояние и отправка CAN предоставляются через
|
||||
* интерфейс библиотеки.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file can_sensor.h
|
||||
* @brief Портируемый транспорт SETTINGS для привязки DS18B20 к локации.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Прямой bxCAN-порт датчиков для STM32F1 без HAL. Регистры контроллера обеспечивают передачу и
|
||||
* приём SETCAN, а конфигурация платы задаёт подключение шины; это аппаратная реализация
|
||||
* интерфейса can_sensor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file can_sensor_stm32f1.c
|
||||
* @brief Опросный порт can-sensor на bxCAN STM32F1 без STM32 HAL.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Прямой bxCAN-порт датчиков для STM32F1 без HAL. Регистры контроллера обеспечивают передачу и
|
||||
* приём SETCAN, а конфигурация платы задаёт подключение шины; это аппаратная реализация
|
||||
* интерфейса can_sensor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file can_sensor_stm32f1.h
|
||||
* @brief Опросный порт can-sensor на bxCAN микроконтроллеров STM32F1.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Конфигурация can-sensor для порта stm32f1. Макросы задают параметры сборки и ограничения,
|
||||
* используемые исходниками этого модуля. Это образец для проекта: значения выводов, размеров
|
||||
* памяти и параметров устройства необходимо согласовать с целевой платой.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file can_sensor_stm32f1_config.f103.template.h
|
||||
* @brief Шаблон выводов CAN1 для STM32F103 без ремапа.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Проверки can_sensor: фиксируют ожидаемое поведение на воспроизводимых входных данных.
|
||||
* Тестовые значения и подмены окружения относятся к проверке контракта; они не являются
|
||||
* настройками рабочего устройства.
|
||||
*/
|
||||
|
||||
#include "can_sensor.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
13
c/candle/CMakeLists.txt
Normal file
13
c/candle/CMakeLists.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
# Сборка библиотеки candle. Состав исходников и публичные include-пути задают подключение к
|
||||
# проекту потребителя. Платформенные зависимости и условия сборки задаются здесь, а не в
|
||||
# прикладных исходниках.
|
||||
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(candle C)
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "Candle transport requires Windows WinUSB")
|
||||
endif()
|
||||
add_library(candle SHARED candle.c candle_ctrl_req.c candle.def)
|
||||
target_compile_definitions(candle PRIVATE UNICODE _UNICODE)
|
||||
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32)
|
||||
target_include_directories(candle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
66
c/candle/LICENSE
Normal file
66
c/candle/LICENSE
Normal file
File diff suppressed because one or more lines are too long
16
c/candle/README.md
Normal file
16
c/candle/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Candle / gs_usb — порт WinUSB
|
||||
|
||||
Общий C-порт для адаптеров candleLight/gs_usb. Перенесён из
|
||||
SETGUI `src/gui_desktop/native/candle_src`; исходные файлы и LGPLv3
|
||||
`LICENSE` сохранены без изменений.
|
||||
|
||||
Сборка из Developer Command Prompt:
|
||||
|
||||
```bat
|
||||
cmake -S c/candle -B build/candle -A x64
|
||||
cmake --build build/candle --config Release
|
||||
```
|
||||
|
||||
Для 32-битной библиотеки используйте `-A Win32` и отдельный каталог сборки.
|
||||
Python Qt-порт находится в `python/set_devices/qt_ports/candle_adapter.py`;
|
||||
путь к библиотеке задаётся через `CANDLE_LIBRARY`.
|
||||
1157
c/candle/candle.c
Normal file
1157
c/candle/candle.c
Normal file
@@ -0,0 +1,1157 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
|
||||
|
||||
This file is part of the candle windows API.
|
||||
|
||||
This library is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation, either
|
||||
version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
* Доступ к CAN-адаптерам candleLight/gs_usb в Windows через WinUSB. Перечисление устройств,
|
||||
* открытие канала и обмен кадрами образуют отдельный транспортный слой; он не интерпретирует
|
||||
* прикладные регистры SETProtocol.
|
||||
*/
|
||||
|
||||
#include "candle.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "candle_defs.h"
|
||||
#include "candle_ctrl_req.h"
|
||||
#include "ch_9.h"
|
||||
|
||||
static bool candle_dev_interal_open(candle_handle hdev);
|
||||
|
||||
candle_log_fn_t candle_log_fn = NULL;
|
||||
bool candle_log_verbose = false;
|
||||
|
||||
static void candle_logf(const wchar_t *fmt, ...)
|
||||
{
|
||||
if (candle_log_fn == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
wchar_t buf[512];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
HRESULT hr = StringCchVPrintfW(buf, 512, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (SUCCEEDED(hr)) {
|
||||
candle_log_fn(buf);
|
||||
}
|
||||
}
|
||||
|
||||
static void candle_logf_verbose(const wchar_t *fmt, ...)
|
||||
{
|
||||
if (candle_log_fn == NULL || !candle_log_verbose) {
|
||||
return;
|
||||
}
|
||||
|
||||
wchar_t buf[512];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
HRESULT hr = StringCchVPrintfW(buf, 512, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (SUCCEEDED(hr)) {
|
||||
candle_log_fn(buf);
|
||||
}
|
||||
}
|
||||
|
||||
static bool candle_read_di(HDEVINFO hdi, SP_DEVICE_INTERFACE_DATA interfaceData, candle_device_t *dev)
|
||||
{
|
||||
/* get required length first (this call always fails with an error) */
|
||||
ULONG requiredLength=0;
|
||||
SetupDiGetDeviceInterfaceDetail(hdi, &interfaceData, NULL, 0, &requiredLength, NULL);
|
||||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
|
||||
dev->last_error = CANDLE_ERR_SETUPDI_IF_DETAILS;
|
||||
return false;
|
||||
}
|
||||
|
||||
PSP_DEVICE_INTERFACE_DETAIL_DATA detail_data =
|
||||
(PSP_DEVICE_INTERFACE_DETAIL_DATA) LocalAlloc(LMEM_FIXED, requiredLength);
|
||||
|
||||
if (detail_data != NULL) {
|
||||
detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
|
||||
} else {
|
||||
dev->last_error = CANDLE_ERR_MALLOC;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool retval = true;
|
||||
ULONG length = requiredLength;
|
||||
if (!SetupDiGetDeviceInterfaceDetail(hdi, &interfaceData, detail_data, length, &requiredLength, NULL) ) {
|
||||
dev->last_error = CANDLE_ERR_SETUPDI_IF_DETAILS2;
|
||||
retval = false;
|
||||
} else if (FAILED(StringCchCopy(dev->path, sizeof(dev->path), detail_data->DevicePath))) {
|
||||
dev->last_error = CANDLE_ERR_PATH_LEN;
|
||||
retval = false;
|
||||
}
|
||||
|
||||
LocalFree(detail_data);
|
||||
|
||||
if (!retval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* try to open to read device infos and see if it is avail */
|
||||
if (candle_dev_interal_open(dev)) {
|
||||
dev->state = CANDLE_DEVSTATE_AVAIL;
|
||||
candle_dev_close(dev);
|
||||
} else {
|
||||
dev->state = CANDLE_DEVSTATE_INUSE;
|
||||
}
|
||||
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Return true when path already appears in l->dev[0..count-1]. */
|
||||
static bool candle_path_exists(const candle_list_t *l, unsigned count, const wchar_t *path)
|
||||
{
|
||||
for (unsigned i = 0; i < count; i++) {
|
||||
if (wcscmp(l->dev[i].path, path) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Scan one GUID and append found devices to l->dev[] starting at offset.
|
||||
* Returns the number of devices appended, or -1 on a hard error (l->last_error set). */
|
||||
static int candle_scan_guid(candle_list_t *l, const wchar_t *guid_str, unsigned offset)
|
||||
{
|
||||
GUID guid;
|
||||
if (CLSIDFromString(guid_str, &guid) != NOERROR) {
|
||||
l->last_error = CANDLE_ERR_CLSID;
|
||||
return -1;
|
||||
}
|
||||
|
||||
HDEVINFO hdi = SetupDiGetClassDevs(&guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
|
||||
if (hdi == INVALID_HANDLE_VALUE) {
|
||||
/* No devices with this GUID present — not a hard error. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
int found = 0;
|
||||
for (unsigned i = 0; (offset + i) < CANDLE_MAX_DEVICES; i++) {
|
||||
SP_DEVICE_INTERFACE_DATA interfaceData;
|
||||
interfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
|
||||
|
||||
if (!SetupDiEnumDeviceInterfaces(hdi, NULL, &guid, i, &interfaceData)) {
|
||||
if (GetLastError() != ERROR_NO_MORE_ITEMS) {
|
||||
l->last_error = CANDLE_ERR_SETUPDI_IF_ENUM;
|
||||
found = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!candle_read_di(hdi, interfaceData, &l->dev[offset + i])) {
|
||||
l->last_error = l->dev[offset + i].last_error;
|
||||
found = -1;
|
||||
break;
|
||||
}
|
||||
found++;
|
||||
}
|
||||
|
||||
SetupDiDestroyDeviceInfoList(hdi);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Scan for WinUSB devices matching vid:pid whose device interface GUID was not
|
||||
* covered by the GUID list above. For each matching USB device instance the
|
||||
* function reads DeviceInterfaceGUIDs (or DeviceInterfaceGUID) from the Windows
|
||||
* registry, re-uses candle_scan_guid() for each GUID found there, and appends
|
||||
* only those devices that are not already present in l->dev[0..existing-1].
|
||||
* Returns the number of new devices added. */
|
||||
static int candle_scan_vidpid(candle_list_t *l, uint16_t vid, uint16_t pid, unsigned existing)
|
||||
{
|
||||
wchar_t hwid_prefix[32];
|
||||
StringCchPrintfW(hwid_prefix, 32, L"USB\\VID_%04X&PID_%04X", vid, pid);
|
||||
|
||||
/* Enumerate USB device instances (not interfaces) so we can read hardware IDs. */
|
||||
HDEVINFO hdi = SetupDiGetClassDevs(NULL, L"USB", NULL,
|
||||
DIGCF_ALLCLASSES | DIGCF_PRESENT);
|
||||
if (hdi == INVALID_HANDLE_VALUE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int added = 0;
|
||||
SP_DEVINFO_DATA devInfo;
|
||||
devInfo.cbSize = sizeof(SP_DEVINFO_DATA);
|
||||
|
||||
for (DWORD i = 0;
|
||||
SetupDiEnumDeviceInfo(hdi, i, &devInfo) && existing + added < CANDLE_MAX_DEVICES;
|
||||
i++)
|
||||
{
|
||||
|
||||
/* Hardware IDs are a REG_MULTI_SZ — check each string for our VID/PID prefix. */
|
||||
wchar_t hwids[512];
|
||||
memset(hwids, 0, sizeof(hwids));
|
||||
if (!SetupDiGetDeviceRegistryPropertyW(hdi, &devInfo, SPDRP_HARDWAREID,
|
||||
NULL, (PBYTE)hwids, sizeof(hwids) - sizeof(wchar_t), NULL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool matches = false;
|
||||
const wchar_t *p;
|
||||
for (p = hwids; *p; p += wcslen(p) + 1) {
|
||||
if (_wcsnicmp(p, hwid_prefix, wcslen(hwid_prefix)) == 0) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Open the device's software registry key (Device Parameters) and read
|
||||
* the WinUSB device interface GUID(s) stored by the driver INF. */
|
||||
HKEY hKey = SetupDiOpenDevRegKey(hdi, &devInfo, DICS_FLAG_GLOBAL, 0,
|
||||
DIREG_DEV, KEY_READ);
|
||||
if (hKey == INVALID_HANDLE_VALUE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
wchar_t guid_buf[256];
|
||||
memset(guid_buf, 0, sizeof(guid_buf));
|
||||
DWORD buf_len = sizeof(guid_buf) - sizeof(wchar_t);
|
||||
|
||||
/* Prefer DeviceInterfaceGUIDs (REG_MULTI_SZ, modern INFs); fall back to
|
||||
* DeviceInterfaceGUID (REG_SZ, older/zadig-generated INFs). */
|
||||
LONG reg_rc = RegQueryValueExW(hKey, L"DeviceInterfaceGUIDs", NULL, NULL,
|
||||
(LPBYTE)guid_buf, &buf_len);
|
||||
if (reg_rc != ERROR_SUCCESS) {
|
||||
buf_len = sizeof(guid_buf) - sizeof(wchar_t);
|
||||
RegQueryValueExW(hKey, L"DeviceInterfaceGUID", NULL, NULL,
|
||||
(LPBYTE)guid_buf, &buf_len);
|
||||
}
|
||||
RegCloseKey(hKey);
|
||||
|
||||
if (!guid_buf[0]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Iterate GUID strings. Both REG_SZ and REG_MULTI_SZ are covered by the
|
||||
* same NUL-terminated-string walk (REG_SZ just has one entry). */
|
||||
const wchar_t *g;
|
||||
for (g = guid_buf;
|
||||
*g && existing + added < CANDLE_MAX_DEVICES;
|
||||
g += wcslen(g) + 1)
|
||||
{
|
||||
unsigned base = existing + added;
|
||||
int n = candle_scan_guid(l, g, base);
|
||||
if (n <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Remove any entries whose path was already found by the GUID scan. */
|
||||
for (int ni = 0; ni < n; ) {
|
||||
if (candle_path_exists(l, base, l->dev[base + ni].path)) {
|
||||
memmove(&l->dev[base + ni], &l->dev[base + ni + 1],
|
||||
(unsigned)(n - ni - 1) * sizeof(candle_device_t));
|
||||
n--;
|
||||
} else {
|
||||
ni++;
|
||||
}
|
||||
}
|
||||
added += n;
|
||||
}
|
||||
}
|
||||
|
||||
SetupDiDestroyDeviceInfoList(hdi);
|
||||
return added;
|
||||
}
|
||||
|
||||
bool __stdcall candle_list_scan(candle_list_handle *list)
|
||||
{
|
||||
if (list == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
candle_list_t *l = (candle_list_t *)calloc(1, sizeof(candle_list_t));
|
||||
*list = l;
|
||||
if (l == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* GUIDs for gs_usb-compatible devices on Windows.
|
||||
* candleLight / CANable / most gs_usb devices: */
|
||||
static const wchar_t *GUIDS[] = {
|
||||
L"{c15b4308-04d3-11e6-b3ea-6057189e6443}" /* candleLight / CANable / gs_usb standard */
|
||||
};
|
||||
static const unsigned NUM_GUIDS = sizeof(GUIDS) / sizeof(GUIDS[0]);
|
||||
|
||||
unsigned total = 0;
|
||||
for (unsigned g = 0; g < NUM_GUIDS; g++) {
|
||||
int n = candle_scan_guid(l, GUIDS[g], total);
|
||||
if (n < 0) {
|
||||
return false;
|
||||
}
|
||||
total += (unsigned)n;
|
||||
}
|
||||
|
||||
/* VID/PID scan for devices whose device interface GUID is not in the list
|
||||
* above (e.g. CANnectivity which uses its own registered interface GUID). */
|
||||
static const struct { uint16_t vid; uint16_t pid; } VIDPIDS[] = {
|
||||
{ 0x1209, 0xCA01 }, /* CANnectivity (electronut-labs) */
|
||||
};
|
||||
static const unsigned NUM_VIDPIDS = sizeof(VIDPIDS) / sizeof(VIDPIDS[0]);
|
||||
|
||||
for (unsigned v = 0; v < NUM_VIDPIDS && total < CANDLE_MAX_DEVICES; v++) {
|
||||
int n = candle_scan_vidpid(l, VIDPIDS[v].vid, VIDPIDS[v].pid, total);
|
||||
if (n > 0)
|
||||
total += (unsigned)n;
|
||||
}
|
||||
|
||||
l->num_devices = (uint8_t)total;
|
||||
l->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_list_free(candle_list_handle list)
|
||||
{
|
||||
free(list);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len)
|
||||
{
|
||||
candle_list_t *l = (candle_list_t *)list;
|
||||
*len = l->num_devices;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev)
|
||||
{
|
||||
candle_list_t *l = (candle_list_t *)list;
|
||||
if (l==NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dev_num >= CANDLE_MAX_DEVICES) {
|
||||
l->last_error = CANDLE_ERR_DEV_OUT_OF_RANGE;
|
||||
return false;
|
||||
}
|
||||
|
||||
candle_device_t *dev = calloc(1, sizeof(candle_device_t));
|
||||
*hdev = dev;
|
||||
if (dev==NULL) {
|
||||
l->last_error = CANDLE_ERR_MALLOC;
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(dev, &l->dev[dev_num], sizeof(candle_device_t));
|
||||
l->last_error = CANDLE_ERR_OK;
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state)
|
||||
{
|
||||
if (hdev==NULL) {
|
||||
return false;
|
||||
} else {
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
*state = dev->state;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
wchar_t * __stdcall DLL candle_dev_get_path(candle_handle hdev)
|
||||
{
|
||||
if (hdev==NULL) {
|
||||
return NULL;
|
||||
} else {
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return dev->path;
|
||||
}
|
||||
}
|
||||
|
||||
static bool candle_dev_interal_open(candle_handle hdev)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
memset(dev->rxevents, 0, sizeof(dev->rxevents));
|
||||
memset(dev->rxurbs, 0, sizeof(dev->rxurbs));
|
||||
|
||||
dev->deviceHandle = CreateFile(
|
||||
dev->path,
|
||||
GENERIC_WRITE | GENERIC_READ,
|
||||
FILE_SHARE_WRITE | FILE_SHARE_READ,
|
||||
NULL,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
|
||||
NULL
|
||||
);
|
||||
|
||||
if (dev->deviceHandle == INVALID_HANDLE_VALUE) {
|
||||
dev->last_error = CANDLE_ERR_CREATE_FILE;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!WinUsb_Initialize(dev->deviceHandle, &dev->winUSBHandle)) {
|
||||
dev->last_error = CANDLE_ERR_WINUSB_INITIALIZE;
|
||||
goto close_handle;
|
||||
}
|
||||
|
||||
USB_INTERFACE_DESCRIPTOR ifaceDescriptor;
|
||||
if (!WinUsb_QueryInterfaceSettings(dev->winUSBHandle, 0, &ifaceDescriptor)) {
|
||||
dev->last_error = CANDLE_ERR_QUERY_INTERFACE;
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
dev->interfaceNumber = ifaceDescriptor.bInterfaceNumber;
|
||||
bool has_in = false, has_out = false;
|
||||
|
||||
candle_logf(L"open path=%ls interface=%u endpoints=%u",
|
||||
dev->path,
|
||||
dev->interfaceNumber,
|
||||
ifaceDescriptor.bNumEndpoints);
|
||||
|
||||
for (uint8_t i=0; i<ifaceDescriptor.bNumEndpoints; i++) {
|
||||
|
||||
WINUSB_PIPE_INFORMATION pipeInfo;
|
||||
if (!WinUsb_QueryPipe(dev->winUSBHandle, 0, i, &pipeInfo)) {
|
||||
dev->last_error = CANDLE_ERR_QUERY_PIPE;
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
if (pipeInfo.PipeType == UsbdPipeTypeBulk && USB_ENDPOINT_DIRECTION_IN(pipeInfo.PipeId)) {
|
||||
if (!has_in) {
|
||||
dev->bulkInPipe = pipeInfo.PipeId;
|
||||
has_in = true;
|
||||
candle_logf(L"selected bulk IN pipe=0x%02x maxPacket=%u interval=%u",
|
||||
pipeInfo.PipeId,
|
||||
pipeInfo.MaximumPacketSize,
|
||||
pipeInfo.Interval);
|
||||
}
|
||||
} else if (pipeInfo.PipeType == UsbdPipeTypeBulk && USB_ENDPOINT_DIRECTION_OUT(pipeInfo.PipeId)) {
|
||||
if (!has_out) {
|
||||
dev->bulkOutPipe = pipeInfo.PipeId;
|
||||
has_out = true;
|
||||
candle_logf(L"selected bulk OUT pipe=0x%02x maxPacket=%u interval=%u",
|
||||
pipeInfo.PipeId,
|
||||
pipeInfo.MaximumPacketSize,
|
||||
pipeInfo.Interval);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!has_in || !has_out) {
|
||||
dev->last_error = CANDLE_ERR_PARSE_IF_DESCR;
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
char use_raw_io = 1;
|
||||
if (!WinUsb_SetPipePolicy(dev->winUSBHandle, dev->bulkInPipe, RAW_IO, sizeof(use_raw_io), &use_raw_io)) {
|
||||
dev->last_error = CANDLE_ERR_SET_PIPE_RAW_IO;
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
if (!candle_ctrl_set_host_format(dev)) {
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
if (!candle_ctrl_get_config(dev, &dev->dconf)) {
|
||||
goto winusb_free;
|
||||
}
|
||||
candle_logf(L"device config channels=%u sw=0x%08x hw=0x%08x",
|
||||
dev->dconf.icount + 1,
|
||||
dev->dconf.sw_version,
|
||||
dev->dconf.hw_version);
|
||||
|
||||
if (!candle_ctrl_get_capability(dev, 0, &dev->bt_const)) {
|
||||
dev->last_error = CANDLE_ERR_GET_BITTIMING_CONST;
|
||||
goto winusb_free;
|
||||
}
|
||||
candle_logf(L"cap ch0 feature=0x%08x fclk=%u tseg1=%u..%u tseg2=%u..%u sjw=%u brp=%u..%u inc=%u",
|
||||
dev->bt_const.feature,
|
||||
dev->bt_const.fclk_can,
|
||||
dev->bt_const.tseg1_min,
|
||||
dev->bt_const.tseg1_max,
|
||||
dev->bt_const.tseg2_min,
|
||||
dev->bt_const.tseg2_max,
|
||||
dev->bt_const.sjw_max,
|
||||
dev->bt_const.brp_min,
|
||||
dev->bt_const.brp_max,
|
||||
dev->bt_const.brp_inc);
|
||||
|
||||
/* Query capabilities for each channel on multi-channel devices */
|
||||
uint8_t num_channels = dev->dconf.icount + 1;
|
||||
if (num_channels > 8) num_channels = 8;
|
||||
for (uint8_t ch = 0; ch < num_channels; ch++) {
|
||||
if (!candle_ctrl_get_capability(dev, ch, &dev->ch_caps[ch])) {
|
||||
/* Fall back to channel 0 capabilities for this channel */
|
||||
memcpy(&dev->ch_caps[ch], &dev->bt_const, sizeof(candle_capability_t));
|
||||
candle_logf(L"cap ch%u failed, falling back to ch0", ch);
|
||||
} else {
|
||||
candle_logf(L"cap ch%u feature=0x%08x fclk=%u",
|
||||
ch,
|
||||
dev->ch_caps[ch].feature,
|
||||
dev->ch_caps[ch].fclk_can);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pre-allocate a manual-reset event for timed overlapped writes. Reusing
|
||||
* one event per device (writes are serialised by writeMutex) avoids
|
||||
* per-frame CreateEvent overhead at high CAN frame rates. */
|
||||
dev->txEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
|
||||
if (!dev->txEvent) {
|
||||
dev->last_error = CANDLE_ERR_MALLOC;
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
|
||||
winusb_free:
|
||||
WinUsb_Free(dev->winUSBHandle);
|
||||
dev->winUSBHandle = NULL;
|
||||
|
||||
close_handle:
|
||||
CloseHandle(dev->deviceHandle);
|
||||
dev->deviceHandle = NULL;
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
static bool candle_prepare_read(candle_device_t *dev, unsigned urb_num)
|
||||
{
|
||||
if (dev->rxurbs[urb_num].pending) {
|
||||
dev->last_error = CANDLE_ERR_PREPARE_READ;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dev->rxurbs[urb_num].ovl.hEvent == NULL) {
|
||||
dev->last_error = CANDLE_ERR_PREPARE_READ;
|
||||
return false;
|
||||
}
|
||||
|
||||
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
|
||||
|
||||
BOOL rc = WinUsb_ReadPipe(
|
||||
dev->winUSBHandle,
|
||||
dev->bulkInPipe,
|
||||
dev->rxurbs[urb_num].buf,
|
||||
sizeof(dev->rxurbs[urb_num].buf),
|
||||
NULL,
|
||||
&dev->rxurbs[urb_num].ovl
|
||||
);
|
||||
|
||||
if (rc) {
|
||||
/* Synchronous completion: data is already in buf and the event is
|
||||
* signaled. WaitForMultipleObjects will return immediately on the
|
||||
* next call and GetOverlappedResult will succeed, so this is fine. */
|
||||
dev->rxurbs[urb_num].pending = true;
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
DWORD err = GetLastError();
|
||||
if (err == ERROR_IO_PENDING) {
|
||||
dev->rxurbs[urb_num].pending = true;
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
candle_logf(L"prepare read urb=%u failed winerr=%lu", urb_num, err);
|
||||
dev->last_error = CANDLE_ERR_PREPARE_READ;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool candle_close_rxurbs(candle_device_t *dev)
|
||||
{
|
||||
if (dev->winUSBHandle != NULL) {
|
||||
WinUsb_AbortPipe(dev->winUSBHandle, dev->bulkInPipe);
|
||||
}
|
||||
|
||||
for (unsigned i=0; i<CANDLE_URB_COUNT; i++) {
|
||||
if (dev->rxurbs[i].pending) {
|
||||
CancelIoEx(dev->deviceHandle, &dev->rxurbs[i].ovl);
|
||||
|
||||
DWORD bytes_transfered;
|
||||
WinUsb_GetOverlappedResult(dev->winUSBHandle,
|
||||
&dev->rxurbs[i].ovl,
|
||||
&bytes_transfered,
|
||||
TRUE);
|
||||
dev->rxurbs[i].pending = false;
|
||||
}
|
||||
|
||||
if (dev->rxevents[i] != NULL) {
|
||||
CloseHandle(dev->rxevents[i]);
|
||||
dev->rxevents[i] = NULL;
|
||||
memset(&dev->rxurbs[i].ovl, 0, sizeof(dev->rxurbs[i].ovl));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void candle_release_open_handles(candle_device_t *dev)
|
||||
{
|
||||
candle_close_rxurbs(dev);
|
||||
|
||||
if (dev->txEvent) {
|
||||
CloseHandle(dev->txEvent);
|
||||
dev->txEvent = NULL;
|
||||
}
|
||||
|
||||
if (dev->winUSBHandle) {
|
||||
WinUsb_Free(dev->winUSBHandle);
|
||||
dev->winUSBHandle = NULL;
|
||||
}
|
||||
|
||||
if (dev->deviceHandle && dev->deviceHandle != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(dev->deviceHandle);
|
||||
dev->deviceHandle = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool __stdcall DLL candle_dev_open(candle_handle hdev)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
if (candle_dev_interal_open(dev)) {
|
||||
for (unsigned i=0; i<CANDLE_URB_COUNT; i++) {
|
||||
HANDLE ev = CreateEvent(NULL, true, false, NULL);
|
||||
if (ev == NULL) {
|
||||
dev->last_error = CANDLE_ERR_MALLOC;
|
||||
candle_err_t last_error = dev->last_error;
|
||||
candle_release_open_handles(dev);
|
||||
dev->last_error = last_error;
|
||||
return false;
|
||||
}
|
||||
dev->rxevents[i] = ev;
|
||||
dev->rxurbs[i].ovl.hEvent = ev;
|
||||
if (!candle_prepare_read(dev, i)) {
|
||||
candle_err_t last_error = dev->last_error;
|
||||
candle_release_open_handles(dev);
|
||||
dev->last_error = last_error;
|
||||
return false; // keep last_error from prepare_read call
|
||||
}
|
||||
}
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
} else {
|
||||
return false; // keep last_error from open_device call
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us)
|
||||
{
|
||||
return candle_ctrl_get_timestamp(hdev, timestamp_us);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_dev_close(candle_handle hdev)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
candle_release_open_handles(dev);
|
||||
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_dev_free(candle_handle hdev)
|
||||
{
|
||||
free(hdev);
|
||||
return true;
|
||||
}
|
||||
|
||||
candle_err_t __stdcall DLL candle_dev_last_error(candle_handle hdev)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return dev->last_error;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_count(candle_handle hdev, uint8_t *num_channels)
|
||||
{
|
||||
// TODO check if info was already read from device; try to do so; throw error...
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
*num_channels = dev->dconf.icount+1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_get_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
uint8_t num_channels = dev->dconf.icount + 1;
|
||||
if (ch < num_channels && ch < 8) {
|
||||
memcpy(cap, &dev->ch_caps[ch], sizeof(candle_capability_t));
|
||||
} else {
|
||||
memcpy(cap, &dev->bt_const, sizeof(candle_capability_t));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_get_state(candle_handle hdev, uint8_t ch, candle_can_state_t *state)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
candle_device_state_t ds;
|
||||
if (!candle_ctrl_get_state(dev, ch, &ds)) {
|
||||
return false;
|
||||
}
|
||||
*state = (candle_can_state_t)ds.state;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_bus_off_recover(candle_handle hdev, uint8_t ch)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return candle_ctrl_bus_off_recover(dev, ch);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data)
|
||||
{
|
||||
// TODO ensure device is open, check channel count..
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return candle_ctrl_set_bittiming(dev, ch, data);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate)
|
||||
{
|
||||
// TODO ensure device is open, check channel count..
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
if (dev->bt_const.fclk_can != 48000000) {
|
||||
/* this function only works for the candleLight base clock of 48MHz */
|
||||
dev->last_error = CANDLE_ERR_BITRATE_FCLK;
|
||||
return false;
|
||||
}
|
||||
|
||||
candle_bittiming_t t;
|
||||
t.prop_seg = 1;
|
||||
t.sjw = 1;
|
||||
t.phase_seg1 = 13 - t.prop_seg;
|
||||
t.phase_seg2 = 2;
|
||||
|
||||
switch (bitrate) {
|
||||
case 10000:
|
||||
t.brp = 300;
|
||||
break;
|
||||
|
||||
case 20000:
|
||||
t.brp = 150;
|
||||
break;
|
||||
|
||||
case 50000:
|
||||
t.brp = 60;
|
||||
break;
|
||||
|
||||
case 83333:
|
||||
t.brp = 36;
|
||||
break;
|
||||
|
||||
case 100000:
|
||||
t.brp = 30;
|
||||
break;
|
||||
|
||||
case 125000:
|
||||
t.brp = 24;
|
||||
break;
|
||||
|
||||
case 250000:
|
||||
t.brp = 12;
|
||||
break;
|
||||
|
||||
case 500000:
|
||||
t.brp = 6;
|
||||
break;
|
||||
|
||||
case 800000:
|
||||
t.brp = 4;
|
||||
t.phase_seg1 = 12 - t.prop_seg;
|
||||
t.phase_seg2 = 2;
|
||||
break;
|
||||
|
||||
case 1000000:
|
||||
t.brp = 3;
|
||||
break;
|
||||
|
||||
default:
|
||||
dev->last_error = CANDLE_ERR_BITRATE_UNSUPPORTED;
|
||||
return false;
|
||||
}
|
||||
|
||||
return candle_ctrl_set_bittiming(dev, ch, &t);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags)
|
||||
{
|
||||
// TODO ensure device is open, check channel count..
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
candle_capability_t *cap = (ch < 8) ? &dev->ch_caps[ch] : &dev->bt_const;
|
||||
|
||||
if (cap->feature & CANDLE_FEATURE_HW_TIMESTAMP) {
|
||||
flags |= CANDLE_MODE_HW_TIMESTAMP;
|
||||
} else {
|
||||
candle_logf(L"channel %u has no HW timestamp capability; starting without timestamp flag", ch);
|
||||
}
|
||||
|
||||
bool rc = candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_START, flags);
|
||||
candle_logf(L"channel %u start flags=0x%08x result=%u err=%u",
|
||||
ch,
|
||||
flags,
|
||||
rc ? 1 : 0,
|
||||
dev->last_error);
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch)
|
||||
{
|
||||
// TODO ensure device is open, check channel count..
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_RESET, 0);
|
||||
}
|
||||
|
||||
/* Write len bytes from buf to the OUT pipe, aborting after 300 ms.
|
||||
* Writes are serialised by writeMutex in CandleApiInterface so dev->txEvent
|
||||
* is never accessed by two threads simultaneously. */
|
||||
static bool candle_write_pipe_timed(candle_device_t *dev, uint8_t *buf, DWORD len)
|
||||
{
|
||||
OVERLAPPED ovl;
|
||||
memset(&ovl, 0, sizeof(ovl));
|
||||
ovl.hEvent = dev->txEvent;
|
||||
ResetEvent(dev->txEvent);
|
||||
|
||||
BOOL rc = WinUsb_WritePipe(dev->winUSBHandle, dev->bulkOutPipe,
|
||||
buf, len, NULL, &ovl);
|
||||
if (rc) {
|
||||
return true; /* completed synchronously */
|
||||
}
|
||||
if (GetLastError() != ERROR_IO_PENDING) {
|
||||
return false; /* hard error */
|
||||
}
|
||||
|
||||
if (WaitForSingleObject(dev->txEvent, 150) != WAIT_OBJECT_0) {
|
||||
/* Timed out: cancel the transfer and restore the pipe to a clean state. */
|
||||
WinUsb_AbortPipe(dev->winUSBHandle, dev->bulkOutPipe);
|
||||
DWORD dummy = 0;
|
||||
WinUsb_GetOverlappedResult(dev->winUSBHandle, &ovl, &dummy, TRUE);
|
||||
WinUsb_ResetPipe(dev->winUSBHandle, dev->bulkOutPipe);
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD transferred = 0;
|
||||
return WinUsb_GetOverlappedResult(dev->winUSBHandle, &ovl, &transferred, FALSE) != FALSE;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
frame->echo_id = 0;
|
||||
frame->channel = ch;
|
||||
bool rc = candle_write_pipe_timed(dev, (uint8_t*)frame, sizeof(*frame));
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SEND_FRAME;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms)
|
||||
{
|
||||
// TODO ensure device is open..
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
DWORD wait_result = WaitForMultipleObjects(CANDLE_URB_COUNT, dev->rxevents, false, timeout_ms);
|
||||
if (wait_result == WAIT_TIMEOUT) {
|
||||
dev->last_error = CANDLE_ERR_READ_TIMEOUT;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( (wait_result < WAIT_OBJECT_0) || (wait_result >= WAIT_OBJECT_0 + CANDLE_URB_COUNT) ) {
|
||||
dev->last_error = CANDLE_ERR_READ_WAIT;
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD urb_num = wait_result - WAIT_OBJECT_0;
|
||||
DWORD bytes_transfered;
|
||||
|
||||
if (!WinUsb_GetOverlappedResult(dev->winUSBHandle, &dev->rxurbs[urb_num].ovl, &bytes_transfered, false)) {
|
||||
DWORD err = GetLastError();
|
||||
if (err == ERROR_IO_INCOMPLETE) {
|
||||
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
|
||||
} else {
|
||||
dev->rxurbs[urb_num].pending = false;
|
||||
candle_prepare_read(dev, urb_num);
|
||||
}
|
||||
candle_logf(L"classic read result failed urb=%u winerr=%lu", urb_num, err);
|
||||
dev->last_error = CANDLE_ERR_READ_RESULT;
|
||||
return false;
|
||||
}
|
||||
dev->rxurbs[urb_num].pending = false;
|
||||
|
||||
if (bytes_transfered < sizeof(*frame)-4) {
|
||||
candle_prepare_read(dev, urb_num);
|
||||
candle_logf(L"classic read too small urb=%u bytes=%lu min=%u",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
(unsigned)(sizeof(*frame) - 4));
|
||||
dev->last_error = CANDLE_ERR_READ_SIZE;
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(frame, 0, sizeof(*frame));
|
||||
DWORD copy_len = (bytes_transfered < sizeof(*frame)) ? bytes_transfered : sizeof(*frame);
|
||||
memcpy(frame, dev->rxurbs[urb_num].buf, copy_len);
|
||||
candle_logf_verbose(L"classic read urb=%u bytes=%lu echo=0x%08x can_id=0x%08x dlc=%u ch=%u flags=0x%02x ts=%u",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
frame->echo_id,
|
||||
frame->can_id,
|
||||
frame->can_dlc,
|
||||
frame->channel,
|
||||
frame->flags,
|
||||
frame->timestamp_us);
|
||||
|
||||
return candle_prepare_read(dev, urb_num);
|
||||
}
|
||||
|
||||
candle_frametype_t __stdcall DLL candle_frame_type(candle_frame_t *frame)
|
||||
{
|
||||
if (frame->echo_id != 0xFFFFFFFF) {
|
||||
return CANDLE_FRAMETYPE_ECHO;
|
||||
};
|
||||
|
||||
if (frame->can_id & CANDLE_ID_ERR) {
|
||||
return CANDLE_FRAMETYPE_ERROR;
|
||||
}
|
||||
|
||||
return CANDLE_FRAMETYPE_RECEIVE;
|
||||
}
|
||||
|
||||
uint32_t __stdcall DLL candle_frame_id(candle_frame_t *frame)
|
||||
{
|
||||
return frame->can_id & 0x1FFFFFFF;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_frame_is_extended_id(candle_frame_t *frame)
|
||||
{
|
||||
return (frame->can_id & CANDLE_ID_EXTENDED) != 0;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_frame_is_rtr(candle_frame_t *frame)
|
||||
{
|
||||
return (frame->can_id & CANDLE_ID_RTR) != 0;
|
||||
}
|
||||
|
||||
uint8_t __stdcall DLL candle_frame_dlc(candle_frame_t *frame)
|
||||
{
|
||||
return frame->can_dlc;
|
||||
}
|
||||
|
||||
uint8_t * __stdcall DLL candle_frame_data(candle_frame_t *frame)
|
||||
{
|
||||
return frame->data;
|
||||
}
|
||||
|
||||
uint32_t __stdcall DLL candle_frame_timestamp_us(candle_frame_t *frame)
|
||||
{
|
||||
return frame->timestamp_us;
|
||||
}
|
||||
|
||||
/* ---- CAN FD extensions ---- */
|
||||
|
||||
bool __stdcall DLL candle_channel_set_data_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return candle_ctrl_set_data_bittiming(dev, ch, data);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_send(candle_handle hdev, uint8_t ch, candle_fd_frame_t *frame)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
frame->echo_id = 0;
|
||||
frame->channel = ch;
|
||||
bool rc = candle_write_pipe_timed(dev, (uint8_t*)frame, sizeof(*frame));
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SEND_FRAME;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *frame, uint32_t timeout_ms)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
DWORD wait_result = WaitForMultipleObjects(CANDLE_URB_COUNT, dev->rxevents, false, timeout_ms);
|
||||
if (wait_result == WAIT_TIMEOUT) {
|
||||
dev->last_error = CANDLE_ERR_READ_TIMEOUT;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( (wait_result < WAIT_OBJECT_0) || (wait_result >= WAIT_OBJECT_0 + CANDLE_URB_COUNT) ) {
|
||||
dev->last_error = CANDLE_ERR_READ_WAIT;
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD urb_num = wait_result - WAIT_OBJECT_0;
|
||||
DWORD bytes_transfered;
|
||||
|
||||
if (!WinUsb_GetOverlappedResult(dev->winUSBHandle, &dev->rxurbs[urb_num].ovl, &bytes_transfered, false)) {
|
||||
DWORD err = GetLastError();
|
||||
if (err == ERROR_IO_INCOMPLETE) {
|
||||
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
|
||||
} else {
|
||||
dev->rxurbs[urb_num].pending = false;
|
||||
candle_prepare_read(dev, urb_num);
|
||||
}
|
||||
candle_logf(L"fd read result failed urb=%u winerr=%lu", urb_num, err);
|
||||
dev->last_error = CANDLE_ERR_READ_RESULT;
|
||||
return false;
|
||||
}
|
||||
dev->rxurbs[urb_num].pending = false;
|
||||
|
||||
/* Minimum: classic CAN header (12 bytes) + at least 8 data bytes = 20 bytes */
|
||||
static const DWORD classic_min = sizeof(candle_frame_t) - 4;
|
||||
if (bytes_transfered < classic_min) {
|
||||
candle_prepare_read(dev, urb_num);
|
||||
candle_logf(L"fd read too small urb=%u bytes=%lu min=%lu",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
classic_min);
|
||||
dev->last_error = CANDLE_ERR_READ_SIZE;
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(frame, 0, sizeof(*frame));
|
||||
|
||||
/*
|
||||
* Detect frame type from the flags byte (offset 10 in both structs).
|
||||
* Classic CAN frames: header(12) + data(8) + timestamp(4) = 24 bytes total.
|
||||
*
|
||||
* FD frames come in two wire formats:
|
||||
* - Legacy fixed (candleLight/CANable 1.x): always 80 bytes — header(12) +
|
||||
* data[64] + timestamp(4). The timestamp is ALWAYS at offset 76, regardless
|
||||
* of the actual DLC. Identified by bytes_transferred == sizeof(candle_fd_frame_t).
|
||||
* - Variable-length (CANnectivity/Zephyr): header(12) + actual_data(DLC) +
|
||||
* timestamp(4). Identified by bytes_transferred < sizeof(candle_fd_frame_t).
|
||||
*/
|
||||
bool is_fd_frame = (dev->rxurbs[urb_num].buf[10] & CANDLE_FRAME_FLAG_FD) != 0;
|
||||
|
||||
if (is_fd_frame) {
|
||||
/* can_dlc is at byte offset 8 in both classic and FD wire frames. */
|
||||
const uint8_t raw_dlc = dev->rxurbs[urb_num].buf[8];
|
||||
const DWORD data_len = candle_dlc_to_len(raw_dlc);
|
||||
const DWORD min_size = 12 + data_len; /* header + data, without timestamp */
|
||||
|
||||
if (bytes_transfered < min_size) {
|
||||
candle_prepare_read(dev, urb_num);
|
||||
candle_logf(L"fd read FD frame too small urb=%u bytes=%lu min=%lu flags=0x%02x dlc=%u",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
min_size,
|
||||
dev->rxurbs[urb_num].buf[10],
|
||||
raw_dlc);
|
||||
dev->last_error = CANDLE_ERR_READ_SIZE;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Copy the fixed 12-byte header (echo_id … reserved). */
|
||||
memcpy(frame, dev->rxurbs[urb_num].buf, 12);
|
||||
/* Copy data at offset 12 into the struct's data field. */
|
||||
memcpy(frame->data, dev->rxurbs[urb_num].buf + 12, data_len);
|
||||
|
||||
/* Timestamp location depends on the wire format (see comment above). */
|
||||
const DWORD fixed_ts_offset = (DWORD)(sizeof(candle_fd_frame_t) - sizeof(uint32_t)); /* = 76 */
|
||||
const DWORD ts_offset = (bytes_transfered >= (DWORD)sizeof(candle_fd_frame_t))
|
||||
? fixed_ts_offset
|
||||
: min_size;
|
||||
if (bytes_transfered >= ts_offset + (DWORD)sizeof(uint32_t)) {
|
||||
memcpy(&frame->timestamp_us, dev->rxurbs[urb_num].buf + ts_offset, sizeof(uint32_t));
|
||||
}
|
||||
/* else: timestamp stays zero from memset above */
|
||||
} else {
|
||||
/* Classic CAN frame — copy into FD struct, fixing the timestamp position */
|
||||
candle_frame_t classic;
|
||||
DWORD copy_len = (bytes_transfered < sizeof(classic)) ? bytes_transfered : sizeof(classic);
|
||||
memcpy(&classic, dev->rxurbs[urb_num].buf, copy_len);
|
||||
|
||||
frame->echo_id = classic.echo_id;
|
||||
frame->can_id = classic.can_id;
|
||||
frame->can_dlc = classic.can_dlc;
|
||||
frame->channel = classic.channel;
|
||||
frame->flags = classic.flags;
|
||||
frame->reserved = classic.reserved;
|
||||
memcpy(frame->data, classic.data, 8);
|
||||
frame->timestamp_us = (bytes_transfered >= sizeof(classic)) ? classic.timestamp_us : 0;
|
||||
}
|
||||
candle_logf_verbose(L"fd read urb=%u bytes=%lu is_fd=%u echo=0x%08x can_id=0x%08x dlc=%u ch=%u flags=0x%02x ts=%u",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
is_fd_frame ? 1 : 0,
|
||||
frame->echo_id,
|
||||
frame->can_id,
|
||||
frame->can_dlc,
|
||||
frame->channel,
|
||||
frame->flags,
|
||||
frame->timestamp_us);
|
||||
|
||||
return candle_prepare_read(dev, urb_num);
|
||||
}
|
||||
|
||||
candle_frametype_t __stdcall DLL candle_fd_frame_type(candle_fd_frame_t *frame)
|
||||
{
|
||||
if (frame->echo_id != 0xFFFFFFFF) {
|
||||
return CANDLE_FRAMETYPE_ECHO;
|
||||
}
|
||||
if (frame->can_id & CANDLE_ID_ERR) {
|
||||
return CANDLE_FRAMETYPE_ERROR;
|
||||
}
|
||||
return CANDLE_FRAMETYPE_RECEIVE;
|
||||
}
|
||||
|
||||
uint32_t __stdcall DLL candle_fd_frame_id(candle_fd_frame_t *frame)
|
||||
{
|
||||
return frame->can_id & 0x1FFFFFFF;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_is_extended_id(candle_fd_frame_t *frame)
|
||||
{
|
||||
return (frame->can_id & CANDLE_ID_EXTENDED) != 0;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_is_rtr(candle_fd_frame_t *frame)
|
||||
{
|
||||
return (frame->can_id & CANDLE_ID_RTR) != 0;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_is_fd(candle_fd_frame_t *frame)
|
||||
{
|
||||
return (frame->flags & CANDLE_FRAME_FLAG_FD) != 0;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_is_brs(candle_fd_frame_t *frame)
|
||||
{
|
||||
return (frame->flags & CANDLE_FRAME_FLAG_BRS) != 0;
|
||||
}
|
||||
|
||||
uint8_t __stdcall DLL candle_fd_frame_dlc(candle_fd_frame_t *frame)
|
||||
{
|
||||
return frame->can_dlc;
|
||||
}
|
||||
|
||||
uint8_t * __stdcall DLL candle_fd_frame_data(candle_fd_frame_t *frame)
|
||||
{
|
||||
return frame->data;
|
||||
}
|
||||
|
||||
uint32_t __stdcall DLL candle_fd_frame_timestamp_us(candle_fd_frame_t *frame)
|
||||
{
|
||||
return frame->timestamp_us;
|
||||
}
|
||||
33
c/candle/candle.def
Normal file
33
c/candle/candle.def
Normal file
@@ -0,0 +1,33 @@
|
||||
; Доступ к CAN-адаптерам candleLight/gs_usb в Windows через WinUSB. Перечисление устройств,
|
||||
; открытие канала и обмен кадрами образуют отдельный транспортный слой; он не интерпретирует
|
||||
; прикладные регистры SETProtocol.
|
||||
|
||||
EXPORTS
|
||||
candle_list_scan
|
||||
candle_list_free
|
||||
candle_list_length
|
||||
candle_dev_get
|
||||
candle_dev_get_state
|
||||
candle_dev_get_path
|
||||
candle_dev_open
|
||||
candle_dev_get_timestamp_us
|
||||
candle_dev_close
|
||||
candle_dev_free
|
||||
candle_channel_count
|
||||
candle_channel_get_capabilities
|
||||
candle_channel_get_state
|
||||
candle_channel_bus_off_recover
|
||||
candle_channel_set_timing
|
||||
candle_channel_set_bitrate
|
||||
candle_channel_start
|
||||
candle_channel_stop
|
||||
candle_frame_send
|
||||
candle_frame_read
|
||||
candle_frame_type
|
||||
candle_frame_id
|
||||
candle_frame_is_extended_id
|
||||
candle_frame_is_rtr
|
||||
candle_frame_dlc
|
||||
candle_frame_data
|
||||
candle_frame_timestamp_us
|
||||
candle_dev_last_error
|
||||
293
c/candle/candle.h
Normal file
293
c/candle/candle.h
Normal file
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
|
||||
Copyright (c) 2026 Schildkroet
|
||||
|
||||
This file is part of the candle windows API.
|
||||
|
||||
This library is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation, either
|
||||
version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
* Доступ к CAN-адаптерам candleLight/gs_usb в Windows через WinUSB. Перечисление устройств,
|
||||
* открытие канала и обмен кадрами образуют отдельный транспортный слой; он не интерпретирует
|
||||
* прикладные регистры SETProtocol.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void* candle_list_handle;
|
||||
typedef void* candle_handle;
|
||||
|
||||
typedef enum {
|
||||
CANDLE_DEVSTATE_AVAIL,
|
||||
CANDLE_DEVSTATE_INUSE
|
||||
} candle_devstate_t;
|
||||
|
||||
typedef enum {
|
||||
CANDLE_FRAMETYPE_UNKNOWN,
|
||||
CANDLE_FRAMETYPE_RECEIVE,
|
||||
CANDLE_FRAMETYPE_ECHO,
|
||||
CANDLE_FRAMETYPE_ERROR,
|
||||
CANDLE_FRAMETYPE_TIMESTAMP_OVFL
|
||||
} candle_frametype_t;
|
||||
|
||||
enum {
|
||||
CANDLE_ID_EXTENDED = 0x80000000,
|
||||
CANDLE_ID_RTR = 0x40000000,
|
||||
CANDLE_ID_ERR = 0x20000000
|
||||
};
|
||||
|
||||
/* Feature flags reported in candle_capability_t.feature */
|
||||
enum {
|
||||
/** CAN channel supports listen-only mode, in which it is not allowed to send dominant bits. */
|
||||
CANDLE_FEATURE_LISTEN_ONLY = 0x0001,
|
||||
/** CAN channel supports loopback mode, in which it receives own frames. */
|
||||
CANDLE_FEATURE_LOOP_BACK = 0x0002,
|
||||
/** CAN channel supports triple sampling mode */
|
||||
CANDLE_FEATURE_TRIPLE_SAMPLE = 0x0004,
|
||||
/** CAN channel supports not retransmitting in case of lost arbitration or missing ACK. */
|
||||
CANDLE_FEATURE_ONE_SHOT = 0x0008,
|
||||
/** CAN channel supports hardware timestamping of CAN frames. */
|
||||
CANDLE_FEATURE_HW_TIMESTAMP = 0x0010,
|
||||
/** CAN channel supports visual identification. */
|
||||
CANDLE_FEATURE_IDENTIFY = 0x0020,
|
||||
/** CAN channel supports user IDs (unsupported). */
|
||||
CANDLE_FEATURE_USER_ID = 0x0040,
|
||||
/** CAN channel supports padding of host frames (unsupported). */
|
||||
CANDLE_FEATURE_PAD_PKTS_TO_MAX = 0x0080,
|
||||
/** CAN channel supports transmitting/receiving CAN FD frames. */
|
||||
CANDLE_FEATURE_FD = 0x0100,
|
||||
/** CAN channel support LPC546xx specific quirks (Unused) */
|
||||
CANDLE_FEATURE_REQ_USB_QUIRK_LPC546XX = 0x0200,
|
||||
/** CAN channel supports extended bit timing limits. */
|
||||
CANDLE_FEATURE_BT_CONST_EXT = 0x0400,
|
||||
/** CAN channel supports configurable bus termination. */
|
||||
CANDLE_FEATURE_TERMINATION = 0x0800,
|
||||
/** CAN channel supports bus error reporting (Unsupported, always enabled) */
|
||||
CANDLE_FEATURE_BERR_REPORTING = 0x1000,
|
||||
/** CAN channel supports reporting of bus state. */
|
||||
CANDLE_FEATURE_GET_STATE = 0x2000,
|
||||
/** Host-controlled recovery after the controller enters bus-off. */
|
||||
CANDLE_FEATURE_BUS_OFF_RECOVERY = 0x40000,
|
||||
};
|
||||
|
||||
/* Flags in the flags byte of received/transmitted frames */
|
||||
enum {
|
||||
CANDLE_FRAME_FLAG_OVERFLOW = 0x01,
|
||||
CANDLE_FRAME_FLAG_FD = 0x02,
|
||||
CANDLE_FRAME_FLAG_BRS = 0x04,
|
||||
CANDLE_FRAME_FLAG_ESI = 0x08,
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
CANDLE_STATE_ERROR_ACTIVE = 0,
|
||||
CANDLE_STATE_ERROR_WARNING = 1,
|
||||
CANDLE_STATE_ERROR_PASSIVE = 2,
|
||||
CANDLE_STATE_BUS_OFF = 3,
|
||||
CANDLE_STATE_STOPPED = 4,
|
||||
CANDLE_STATE_SLEEPING = 5,
|
||||
} candle_can_state_t;
|
||||
|
||||
typedef enum {
|
||||
CANDLE_MODE_NORMAL = 0x0000,
|
||||
CANDLE_MODE_LISTEN_ONLY = 0x0001,
|
||||
CANDLE_MODE_LOOP_BACK = 0x0002,
|
||||
CANDLE_MODE_TRIPLE_SAMPLE = 0x0004,
|
||||
CANDLE_MODE_ONE_SHOT = 0x0008,
|
||||
CANDLE_MODE_HW_TIMESTAMP = 0x0010,
|
||||
CANDLE_MODE_PAD_PKTS_TO_MAX = 0x0080,
|
||||
CANDLE_MODE_FD = 0x0100,
|
||||
} candle_mode_t;
|
||||
|
||||
typedef enum {
|
||||
CANDLE_ERR_OK = 0,
|
||||
CANDLE_ERR_CREATE_FILE = 1,
|
||||
CANDLE_ERR_WINUSB_INITIALIZE = 2,
|
||||
CANDLE_ERR_QUERY_INTERFACE = 3,
|
||||
CANDLE_ERR_QUERY_PIPE = 4,
|
||||
CANDLE_ERR_PARSE_IF_DESCR = 5,
|
||||
CANDLE_ERR_SET_HOST_FORMAT = 6,
|
||||
CANDLE_ERR_GET_DEVICE_INFO = 7,
|
||||
CANDLE_ERR_GET_BITTIMING_CONST = 8,
|
||||
CANDLE_ERR_PREPARE_READ = 9,
|
||||
CANDLE_ERR_SET_DEVICE_MODE = 10,
|
||||
CANDLE_ERR_SET_BITTIMING = 11,
|
||||
CANDLE_ERR_BITRATE_FCLK = 12,
|
||||
CANDLE_ERR_BITRATE_UNSUPPORTED = 13,
|
||||
CANDLE_ERR_SEND_FRAME = 14,
|
||||
CANDLE_ERR_READ_TIMEOUT = 15,
|
||||
CANDLE_ERR_READ_WAIT = 16,
|
||||
CANDLE_ERR_READ_RESULT = 17,
|
||||
CANDLE_ERR_READ_SIZE = 18,
|
||||
CANDLE_ERR_SETUPDI_IF_DETAILS = 19,
|
||||
CANDLE_ERR_SETUPDI_IF_DETAILS2 = 20,
|
||||
CANDLE_ERR_MALLOC = 21,
|
||||
CANDLE_ERR_PATH_LEN = 22,
|
||||
CANDLE_ERR_CLSID = 23,
|
||||
CANDLE_ERR_GET_DEVICES = 24,
|
||||
CANDLE_ERR_SETUPDI_IF_ENUM = 25,
|
||||
CANDLE_ERR_SET_TIMESTAMP_MODE = 26,
|
||||
CANDLE_ERR_DEV_OUT_OF_RANGE = 27,
|
||||
CANDLE_ERR_GET_TIMESTAMP = 28,
|
||||
CANDLE_ERR_SET_PIPE_RAW_IO = 29
|
||||
} candle_err_t;
|
||||
|
||||
#pragma pack(push,1)
|
||||
|
||||
typedef struct {
|
||||
uint32_t echo_id;
|
||||
uint32_t can_id;
|
||||
uint8_t can_dlc;
|
||||
uint8_t channel;
|
||||
uint8_t flags;
|
||||
uint8_t reserved;
|
||||
uint8_t data[8];
|
||||
uint32_t timestamp_us;
|
||||
} candle_frame_t;
|
||||
|
||||
/* CAN FD frame: same header as candle_frame_t but with 64-byte data payload */
|
||||
typedef struct {
|
||||
uint32_t echo_id;
|
||||
uint32_t can_id;
|
||||
uint8_t can_dlc;
|
||||
uint8_t channel;
|
||||
uint8_t flags;
|
||||
uint8_t reserved;
|
||||
uint8_t data[64];
|
||||
uint32_t timestamp_us;
|
||||
} candle_fd_frame_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t feature;
|
||||
uint32_t fclk_can;
|
||||
uint32_t tseg1_min;
|
||||
uint32_t tseg1_max;
|
||||
uint32_t tseg2_min;
|
||||
uint32_t tseg2_max;
|
||||
uint32_t sjw_max;
|
||||
uint32_t brp_min;
|
||||
uint32_t brp_max;
|
||||
uint32_t brp_inc;
|
||||
} candle_capability_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t prop_seg;
|
||||
uint32_t phase_seg1;
|
||||
uint32_t phase_seg2;
|
||||
uint32_t sjw;
|
||||
uint32_t brp;
|
||||
} candle_bittiming_t;
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
/*
|
||||
* CAN FD DLC encoding:
|
||||
* DLC 0-8 → 0-8 bytes (same as classic CAN)
|
||||
* DLC 9 → 12 bytes
|
||||
* DLC 10 → 16 bytes
|
||||
* DLC 11 → 20 bytes
|
||||
* DLC 12 → 24 bytes
|
||||
* DLC 13 → 32 bytes
|
||||
* DLC 14 → 48 bytes
|
||||
* DLC 15 → 64 bytes
|
||||
*/
|
||||
static inline uint8_t candle_dlc_to_len(uint8_t dlc)
|
||||
{
|
||||
static const uint8_t tbl[16] = { 0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64 };
|
||||
return (dlc <= 15u) ? tbl[dlc] : 0u;
|
||||
}
|
||||
|
||||
static inline uint8_t candle_len_to_dlc(uint8_t len)
|
||||
{
|
||||
if (len <= 8u) return len;
|
||||
if (len <= 12u) return 9u;
|
||||
if (len <= 16u) return 10u;
|
||||
if (len <= 20u) return 11u;
|
||||
if (len <= 24u) return 12u;
|
||||
if (len <= 32u) return 13u;
|
||||
if (len <= 48u) return 14u;
|
||||
return 15u;
|
||||
}
|
||||
|
||||
#define DLL
|
||||
|
||||
/* Optional log callback — set once at startup to receive diagnostic messages.
|
||||
* If NULL (the default) no logging is performed. */
|
||||
typedef void (*candle_log_fn_t)(const wchar_t *msg);
|
||||
extern candle_log_fn_t candle_log_fn;
|
||||
|
||||
/* Set to true to enable per-frame and per-control-transfer trace logs.
|
||||
* Off by default; only error and setup messages are logged. */
|
||||
extern bool candle_log_verbose;
|
||||
|
||||
bool __stdcall DLL candle_list_scan(candle_list_handle *list);
|
||||
bool __stdcall DLL candle_list_free(candle_list_handle list);
|
||||
bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len);
|
||||
|
||||
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev);
|
||||
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state);
|
||||
wchar_t * __stdcall DLL candle_dev_get_path(candle_handle hdev);
|
||||
bool __stdcall DLL candle_dev_open(candle_handle hdev);
|
||||
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us);
|
||||
bool __stdcall DLL candle_dev_close(candle_handle hdev);
|
||||
bool __stdcall DLL candle_dev_free(candle_handle hdev);
|
||||
|
||||
bool __stdcall DLL candle_channel_count(candle_handle hdev, uint8_t *num_channels);
|
||||
bool __stdcall DLL candle_channel_get_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap);
|
||||
bool __stdcall DLL candle_channel_get_state(candle_handle hdev, uint8_t ch, candle_can_state_t *state);
|
||||
bool __stdcall DLL candle_channel_bus_off_recover(candle_handle hdev, uint8_t ch);
|
||||
bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data);
|
||||
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate);
|
||||
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags);
|
||||
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch);
|
||||
|
||||
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame);
|
||||
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms);
|
||||
|
||||
candle_frametype_t __stdcall DLL candle_frame_type(candle_frame_t *frame);
|
||||
uint32_t __stdcall DLL candle_frame_id(candle_frame_t *frame);
|
||||
bool __stdcall DLL candle_frame_is_extended_id(candle_frame_t *frame);
|
||||
bool __stdcall DLL candle_frame_is_rtr(candle_frame_t *frame);
|
||||
uint8_t __stdcall DLL candle_frame_dlc(candle_frame_t *frame);
|
||||
uint8_t * __stdcall DLL candle_frame_data(candle_frame_t *frame);
|
||||
uint32_t __stdcall DLL candle_frame_timestamp_us(candle_frame_t *frame);
|
||||
|
||||
/* CAN FD extensions */
|
||||
bool __stdcall DLL candle_channel_set_data_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data);
|
||||
bool __stdcall DLL candle_fd_frame_send(candle_handle hdev, uint8_t ch, candle_fd_frame_t *frame);
|
||||
bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *frame, uint32_t timeout_ms);
|
||||
|
||||
candle_frametype_t __stdcall DLL candle_fd_frame_type(candle_fd_frame_t *frame);
|
||||
uint32_t __stdcall DLL candle_fd_frame_id(candle_fd_frame_t *frame);
|
||||
bool __stdcall DLL candle_fd_frame_is_extended_id(candle_fd_frame_t *frame);
|
||||
bool __stdcall DLL candle_fd_frame_is_rtr(candle_fd_frame_t *frame);
|
||||
bool __stdcall DLL candle_fd_frame_is_fd(candle_fd_frame_t *frame);
|
||||
bool __stdcall DLL candle_fd_frame_is_brs(candle_fd_frame_t *frame);
|
||||
uint8_t __stdcall DLL candle_fd_frame_dlc(candle_fd_frame_t *frame);
|
||||
uint8_t * __stdcall DLL candle_fd_frame_data(candle_fd_frame_t *frame);
|
||||
uint32_t __stdcall DLL candle_fd_frame_timestamp_us(candle_fd_frame_t *frame);
|
||||
|
||||
candle_err_t __stdcall DLL candle_dev_last_error(candle_handle hdev);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
239
c/candle/candle_ctrl_req.c
Normal file
239
c/candle/candle_ctrl_req.c
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
|
||||
|
||||
This file is part of the candle windows API.
|
||||
|
||||
This library is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation, either
|
||||
version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
* Управляющие USB-запросы адаптера candleLight: конфигурация, временные параметры CAN и режим
|
||||
* канала. Эти транзакции идут через control endpoint и отличаются от потока CAN-кадров.
|
||||
*/
|
||||
|
||||
#include "candle_ctrl_req.h"
|
||||
#include "ch_9.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
enum {
|
||||
CANDLE_BREQ_HOST_FORMAT = 0,
|
||||
CANDLE_BREQ_BITTIMING = 1,
|
||||
CANDLE_BREQ_MODE = 2,
|
||||
CANDLE_BREQ_BERR = 3,
|
||||
CANDLE_BREQ_BT_CONST = 4,
|
||||
CANDLE_BREQ_DEVICE_CONFIG = 5,
|
||||
CANDLE_TIMESTAMP_GET = 6,
|
||||
/* 7: IDENTIFY, 8: GET_USER_ID, 9: SET_USER_ID (not used here) */
|
||||
CANDLE_BREQ_DATA_BITTIMING = 10,
|
||||
/* 11: SET_TERMINATION, not used here */
|
||||
CANDLE_BREQ_GET_STATE = 12,
|
||||
CANDLE_BREQ_BUS_OFF_RECOVERY = 32,
|
||||
};
|
||||
|
||||
static void candle_ctrl_logf(const wchar_t *fmt, ...)
|
||||
{
|
||||
if (candle_log_fn == NULL || !candle_log_verbose) {
|
||||
return;
|
||||
}
|
||||
|
||||
wchar_t buf[512];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
HRESULT hr = StringCchVPrintfW(buf, 512, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (SUCCEEDED(hr)) {
|
||||
candle_log_fn(buf);
|
||||
}
|
||||
}
|
||||
|
||||
static bool usb_control_msg(WINUSB_INTERFACE_HANDLE hnd, uint8_t request, uint8_t requesttype, uint16_t value, uint16_t index, void *data, uint16_t size)
|
||||
{
|
||||
WINUSB_SETUP_PACKET packet;
|
||||
memset(&packet, 0, sizeof(packet));
|
||||
|
||||
packet.Request = request;
|
||||
packet.RequestType = requesttype;
|
||||
packet.Value = value;
|
||||
packet.Index = index;
|
||||
packet.Length = size;
|
||||
|
||||
unsigned long bytes_sent = 0;
|
||||
BOOL rc = WinUsb_ControlTransfer(hnd, packet, (uint8_t*)data, size, &bytes_sent, 0);
|
||||
candle_ctrl_logf(L"ctrl req=0x%02x type=0x%02x value=%u index=%u size=%u rc=%u transferred=%lu winerr=%lu",
|
||||
request,
|
||||
requesttype,
|
||||
value,
|
||||
index,
|
||||
size,
|
||||
rc ? 1 : 0,
|
||||
bytes_sent,
|
||||
rc ? 0 : GetLastError());
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool candle_ctrl_set_host_format(candle_device_t *dev)
|
||||
{
|
||||
candle_host_config_t hconf;
|
||||
hconf.byte_order = 0x0000beef;
|
||||
|
||||
bool rc = usb_control_msg(
|
||||
dev->winUSBHandle,
|
||||
CANDLE_BREQ_HOST_FORMAT,
|
||||
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||
1,
|
||||
dev->interfaceNumber,
|
||||
&hconf,
|
||||
sizeof(hconf)
|
||||
);
|
||||
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_HOST_FORMAT;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool candle_ctrl_set_device_mode(candle_device_t *dev, uint8_t channel, uint32_t mode, uint32_t flags)
|
||||
{
|
||||
candle_device_mode_t dm;
|
||||
dm.mode = mode;
|
||||
dm.flags = flags;
|
||||
|
||||
bool rc = usb_control_msg(
|
||||
dev->winUSBHandle,
|
||||
CANDLE_BREQ_MODE,
|
||||
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||
channel,
|
||||
dev->interfaceNumber,
|
||||
&dm,
|
||||
sizeof(dm)
|
||||
);
|
||||
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
bool candle_ctrl_get_config(candle_device_t *dev, candle_device_config_t *dconf)
|
||||
{
|
||||
bool rc = usb_control_msg(
|
||||
dev->winUSBHandle,
|
||||
CANDLE_BREQ_DEVICE_CONFIG,
|
||||
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||
1,
|
||||
dev->interfaceNumber,
|
||||
dconf,
|
||||
sizeof(*dconf)
|
||||
);
|
||||
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_DEVICE_INFO;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool candle_ctrl_get_timestamp(candle_device_t *dev, uint32_t *current_timestamp)
|
||||
{
|
||||
bool rc = usb_control_msg(
|
||||
dev->winUSBHandle,
|
||||
CANDLE_TIMESTAMP_GET,
|
||||
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||
1,
|
||||
dev->interfaceNumber,
|
||||
current_timestamp,
|
||||
sizeof(*current_timestamp)
|
||||
);
|
||||
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_TIMESTAMP;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool candle_ctrl_get_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *data)
|
||||
{
|
||||
bool rc = usb_control_msg(
|
||||
dev->winUSBHandle,
|
||||
CANDLE_BREQ_BT_CONST,
|
||||
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||
channel,
|
||||
0,
|
||||
data,
|
||||
sizeof(*data)
|
||||
);
|
||||
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_BITTIMING_CONST;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool candle_ctrl_set_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data)
|
||||
{
|
||||
bool rc = usb_control_msg(
|
||||
dev->winUSBHandle,
|
||||
CANDLE_BREQ_BITTIMING,
|
||||
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||
channel,
|
||||
0,
|
||||
data,
|
||||
sizeof(*data)
|
||||
);
|
||||
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_BITTIMING;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool candle_ctrl_set_data_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data)
|
||||
{
|
||||
bool rc = usb_control_msg(
|
||||
dev->winUSBHandle,
|
||||
CANDLE_BREQ_DATA_BITTIMING,
|
||||
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||
channel,
|
||||
0,
|
||||
data,
|
||||
sizeof(*data)
|
||||
);
|
||||
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_BITTIMING;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool candle_ctrl_get_state(candle_device_t *dev, uint8_t channel, candle_device_state_t *data)
|
||||
{
|
||||
bool rc = usb_control_msg(
|
||||
dev->winUSBHandle,
|
||||
CANDLE_BREQ_GET_STATE,
|
||||
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||
channel,
|
||||
dev->interfaceNumber,
|
||||
data,
|
||||
sizeof(*data)
|
||||
);
|
||||
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_DEVICE_INFO;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel)
|
||||
{
|
||||
bool rc = usb_control_msg(
|
||||
dev->winUSBHandle,
|
||||
CANDLE_BREQ_BUS_OFF_RECOVERY,
|
||||
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||
channel,
|
||||
dev->interfaceNumber,
|
||||
NULL,
|
||||
0
|
||||
);
|
||||
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
|
||||
return rc;
|
||||
}
|
||||
54
c/candle/candle_ctrl_req.h
Normal file
54
c/candle/candle_ctrl_req.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
|
||||
Copyright (c) 2026 Schildkroet
|
||||
|
||||
This file is part of the candle windows API.
|
||||
|
||||
This library is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation, either
|
||||
version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
* Управляющие USB-запросы адаптера candleLight: конфигурация, временные параметры CAN и режим
|
||||
* канала. Эти транзакции идут через control endpoint и отличаются от потока CAN-кадров.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "candle_defs.h"
|
||||
|
||||
enum {
|
||||
CANDLE_DEVMODE_RESET = 0,
|
||||
CANDLE_DEVMODE_START = 1
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
typedef struct {
|
||||
uint32_t state;
|
||||
uint32_t rxerr;
|
||||
uint32_t txerr;
|
||||
} candle_device_state_t;
|
||||
#pragma pack(pop)
|
||||
|
||||
bool candle_ctrl_set_host_format(candle_device_t *dev);
|
||||
bool candle_ctrl_set_device_mode(candle_device_t *dev, uint8_t channel, uint32_t mode, uint32_t flags);
|
||||
bool candle_ctrl_get_config(candle_device_t *dev, candle_device_config_t *dconf);
|
||||
bool candle_ctrl_get_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *data);
|
||||
bool candle_ctrl_set_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data);
|
||||
bool candle_ctrl_set_data_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data);
|
||||
bool candle_ctrl_get_timestamp(candle_device_t *dev, uint32_t *current_timestamp);
|
||||
bool candle_ctrl_get_state(candle_device_t *dev, uint8_t channel, candle_device_state_t *data);
|
||||
bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel);
|
||||
|
||||
107
c/candle/candle_defs.h
Normal file
107
c/candle/candle_defs.h
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
|
||||
Copyright (c) 2026 Schildkroet
|
||||
|
||||
This file is part of the candle windows API.
|
||||
|
||||
This library is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation, either
|
||||
version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
* Двоичные структуры и константы gs_usb, общие для драйвера и USB-адаптера. Размеры полей и
|
||||
* упаковка определяют совместимость с прошивкой: менять их как обычные внутренние структуры
|
||||
* нельзя.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef _WIN32_WINNT
|
||||
#define _WIN32_WINNT 0x0600
|
||||
#endif
|
||||
|
||||
#include <windows.h>
|
||||
#include <winbase.h>
|
||||
#include <winusb.h>
|
||||
#include <setupapi.h>
|
||||
#include <devguid.h>
|
||||
#include <regstr.h>
|
||||
|
||||
#undef __CRT__NO_INLINE
|
||||
#include <strsafe.h>
|
||||
#define __CRT__NO_INLINE
|
||||
|
||||
#include "candle.h"
|
||||
|
||||
#define CANDLE_MAX_DEVICES 32
|
||||
#define CANDLE_URB_COUNT 30
|
||||
|
||||
#pragma pack(push,1)
|
||||
|
||||
typedef struct {
|
||||
uint32_t byte_order;
|
||||
} candle_host_config_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t reserved1;
|
||||
uint8_t reserved2;
|
||||
uint8_t reserved3;
|
||||
uint8_t icount;
|
||||
uint32_t sw_version;
|
||||
uint32_t hw_version;
|
||||
} candle_device_config_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t mode;
|
||||
uint32_t flags;
|
||||
} candle_device_mode_t;
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
|
||||
typedef struct {
|
||||
OVERLAPPED ovl;
|
||||
bool pending;
|
||||
uint8_t buf[512];
|
||||
} canlde_rx_urb;
|
||||
|
||||
typedef struct {
|
||||
wchar_t path[256];
|
||||
candle_devstate_t state;
|
||||
candle_err_t last_error;
|
||||
|
||||
HANDLE deviceHandle;
|
||||
WINUSB_INTERFACE_HANDLE winUSBHandle;
|
||||
UCHAR interfaceNumber;
|
||||
UCHAR bulkInPipe;
|
||||
UCHAR bulkOutPipe;
|
||||
HANDLE txEvent; /* pre-allocated event for timed overlapped writes */
|
||||
|
||||
candle_device_config_t dconf;
|
||||
candle_capability_t bt_const;
|
||||
/* Per-channel capabilities: index 0..dconf.icount, maximum 8 channels */
|
||||
candle_capability_t ch_caps[8];
|
||||
canlde_rx_urb rxurbs[CANDLE_URB_COUNT];
|
||||
HANDLE rxevents[CANDLE_URB_COUNT];
|
||||
} candle_device_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t num_devices;
|
||||
candle_err_t last_error;
|
||||
candle_device_t dev[CANDLE_MAX_DEVICES];
|
||||
} candle_list_t;
|
||||
|
||||
42
c/candle/ch_9.h
Normal file
42
c/candle/ch_9.h
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
|
||||
|
||||
This file is part of the candle windows API.
|
||||
|
||||
This library is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation, either
|
||||
version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
* Типы стандартных USB-дескрипторов и запросов из главы 9 спецификации USB. Драйвер использует
|
||||
* их для описания устройства и конечных точек; это транспортные определения, а не формат CAN.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#define USB_DIR_OUT 0 /* to device */
|
||||
#define USB_DIR_IN 0x80 /* to host */
|
||||
|
||||
#define USB_TYPE_MASK (0x03 << 5)
|
||||
#define USB_TYPE_STANDARD (0x00 << 5)
|
||||
#define USB_TYPE_CLASS (0x01 << 5)
|
||||
#define USB_TYPE_VENDOR (0x02 << 5)
|
||||
#define USB_TYPE_RESERVED (0x03 << 5)
|
||||
|
||||
#define USB_RECIP_MASK 0x1f
|
||||
#define USB_RECIP_DEVICE 0x00
|
||||
#define USB_RECIP_INTERFACE 0x01
|
||||
#define USB_RECIP_ENDPOINT 0x02
|
||||
#define USB_RECIP_OTHER 0x03
|
||||
@@ -1,3 +1,7 @@
|
||||
# Сборка библиотеки ds18b20_ds2480. Состав исходников и публичные include-пути задают
|
||||
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
|
||||
# запускать их следует через CTest из каталога сборки.
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(ds18b20_ds2480 C)
|
||||
add_library(ds18b20_ds2480 STATIC ds2480.c ds18b20_ds2480.c)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Команды DS18B20 поверх интерфейса DS2480B: адресация ROM, преобразование температуры и
|
||||
* scratchpad. Транспортный статус следует проверять до использования результата; сырая
|
||||
* температура сохраняет знаковое представление датчика.
|
||||
*/
|
||||
|
||||
#include "ds18b20_ds2480.h"
|
||||
#include <string.h>
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Команды DS18B20 поверх интерфейса DS2480B: адресация ROM, преобразование температуры и
|
||||
* scratchpad. Транспортный статус следует проверять до использования результата; сырая
|
||||
* температура сохраняет знаковое представление датчика.
|
||||
*/
|
||||
|
||||
#ifndef DS18B20_DS2480_H
|
||||
#define DS18B20_DS2480_H
|
||||
#include "ds2480.h"
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* UART-мост DS2480B для операций 1-Wire: сброс, обмен битами и байтами, поиск устройств и
|
||||
* управление питанием. После ошибки связи состояние готовности сбрасывается, чтобы следующий
|
||||
* обмен не продолжался в неизвестном режиме моста.
|
||||
*/
|
||||
|
||||
#include "ds2480.h"
|
||||
|
||||
static ds2480_status fault(ds2480 *bus, ds2480_status status)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* UART-мост DS2480B для операций 1-Wire: сброс, обмен битами и байтами, поиск устройств и
|
||||
* управление питанием. После ошибки связи состояние готовности сбрасывается, чтобы следующий
|
||||
* обмен не продолжался в неизвестном режиме моста.
|
||||
*/
|
||||
|
||||
#ifndef DS2480_H
|
||||
#define DS2480_H
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Минимальный блокирующий пример: подготовить мост DS2480B, найти первый датчик, запустить
|
||||
* преобразование и прочитать температуру. Обратные вызовы UART и задержек предоставляет плата;
|
||||
* ожидание преобразования входит во время вызова.
|
||||
*/
|
||||
|
||||
#include "ds18b20_ds2480.h"
|
||||
|
||||
/* board_port supplies the four callbacks documented in ds2480.h.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* UART-порт DS2480B для STM32F4. Подготовка линии и очистка ошибок USART отделены от команд
|
||||
* датчика; после сбоя обмена мост должен пройти предусмотренное восстановление до продолжения
|
||||
* работы с 1-Wire.
|
||||
*/
|
||||
|
||||
#include "ds2480_stm32f4_hal.h"
|
||||
|
||||
#define RX_ERRORS (USART_SR_ORE | USART_SR_NE | USART_SR_FE | USART_SR_PE)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* UART-порт DS2480B для STM32F4. Подготовка линии и очистка ошибок USART отделены от команд
|
||||
* датчика; после сбоя обмена мост должен пройти предусмотренное восстановление до продолжения
|
||||
* работы с 1-Wire.
|
||||
*/
|
||||
|
||||
#ifndef DS2480_STM32F4_HAL_H
|
||||
#define DS2480_STM32F4_HAL_H
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/*
|
||||
* Проверки stm32f4xx_hal: фиксируют ожидаемое поведение на воспроизводимых входных данных.
|
||||
* Сценарии для навигации по файлу:
|
||||
* test_clear
|
||||
* Тестовые значения и подмены окружения относятся к проверке контракта; они не являются
|
||||
* настройками рабочего устройства.
|
||||
*/
|
||||
|
||||
/* Host-only HAL model, never add this directory to a firmware include path. */
|
||||
#ifndef TEST_STM32F4_HAL_H
|
||||
#define TEST_STM32F4_HAL_H
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/*
|
||||
* Проверки port: фиксируют ожидаемое поведение на воспроизводимых входных данных.
|
||||
* Сценарии для навигации по файлу:
|
||||
* test_clear
|
||||
* Тестовые значения и подмены окружения относятся к проверке контракта; они не являются
|
||||
* настройками рабочего устройства.
|
||||
*/
|
||||
|
||||
#include "ds2480_stm32f4_hal.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Проверки ds18b20_ds2480: фиксируют ожидаемое поведение на воспроизводимых входных данных.
|
||||
* Тестовые значения и подмены окружения относятся к проверке контракта; они не являются
|
||||
* настройками рабочего устройства.
|
||||
*/
|
||||
|
||||
#include "ds18b20_ds2480.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# Сборка библиотеки ds18b20. Состав исходников и публичные include-пути задают подключение к
|
||||
# проекту потребителя. Файл также собирает и регистрирует хостовые проверки; запускать их
|
||||
# следует через CTest из каталога сборки.
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(ds18b20 C)
|
||||
|
||||
@@ -33,3 +37,5 @@ if(DS18B20_BUILD_TESTS)
|
||||
target_include_directories(test_ds18b20 PRIVATE tests)
|
||||
add_test(NAME ds18b20 COMMAND test_ds18b20)
|
||||
endif()
|
||||
|
||||
add_subdirectory(instance)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Работа с термометрами DS18B20: поиск по ROM, запуск преобразования, чтение температуры и
|
||||
* настройка scratchpad. ROM является адресом устройства на общей шине; результат чтения нельзя
|
||||
* считать действительным до проверки статуса обмена и CRC.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ds18b20.h
|
||||
* @brief Драйвер термометров DS18B20 поверх шины 1-Wire.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Общие операции 1-Wire и обход дерева 64-битных ROM. Поиск разрешает коллизии побитно,
|
||||
* поэтому обнаружение нескольких устройств не сводится к чтению одного адреса. Электрические
|
||||
* уровни и длительности импульсов задаёт порт.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file onewire.h
|
||||
* @brief Программная шина 1-Wire на выводе с открытым стоком.
|
||||
|
||||
15
c/ds18b20/instance/CMakeLists.txt
Normal file
15
c/ds18b20/instance/CMakeLists.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
# Сборка библиотеки ds18b20_instance. Состав исходников и публичные include-пути задают
|
||||
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
|
||||
# запускать их следует через CTest из каталога сборки.
|
||||
|
||||
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 м,
|
||||
отключение/подключение датчика при поиске, отмену и восстановление опроса.
|
||||
132
c/ds18b20/instance/Inc/ds18b20.h
Normal file
132
c/ds18b20/instance/Inc/ds18b20.h
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Работа с термометрами DS18B20: поиск по ROM, запуск преобразования, чтение температуры и
|
||||
* настройка scratchpad. ROM является адресом устройства на общей шине; результат чтения нельзя
|
||||
* считать действительным до проверки статуса обмена и CRC.
|
||||
*/
|
||||
|
||||
#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
|
||||
23
c/ds18b20/instance/Inc/ds18b20_config.h
Normal file
23
c/ds18b20/instance/Inc/ds18b20_config.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Конфигурация ds18b20. Макросы задают параметры сборки и ограничения, используемые
|
||||
* исходниками этого модуля. Смысл и единицы каждого параметра определяются местом его
|
||||
* использования; размеры буферов должны быть согласованы у всех потребителей заголовка.
|
||||
*/
|
||||
|
||||
#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.
|
||||
359
c/ds18b20/instance/Src/ds18b20.c
Normal file
359
c/ds18b20/instance/Src/ds18b20.c
Normal file
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* Работа с термометрами DS18B20: поиск по ROM, запуск преобразования, чтение температуры и
|
||||
* настройка scratchpad. ROM является адресом устройства на общей шине; результат чтения нельзя
|
||||
* считать действительным до проверки статуса обмена и CRC.
|
||||
*/
|
||||
|
||||
#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;
|
||||
}
|
||||
35
c/ds18b20/instance/Tests/run_host_tests.ps1
Normal file
35
c/ds18b20/instance/Tests/run_host_tests.ps1
Normal file
@@ -0,0 +1,35 @@
|
||||
# Сборка и запуск хостовых проверок экземплярного DS18B20. Скрипт готовит исполняемые тесты
|
||||
# для проверки поиска ROM и задержек EEPROM без платы; ошибка компиляции или проверки должна
|
||||
# завершить прогон неуспешно.
|
||||
|
||||
$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
|
||||
99
c/ds18b20/instance/Tests/test_ds18b20_copy_delay.c
Normal file
99
c/ds18b20/instance/Tests/test_ds18b20_copy_delay.c
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Проверки ds18b20_copy_delay: фиксируют ожидаемое поведение на воспроизводимых входных
|
||||
* данных.
|
||||
* Тестовые значения и подмены окружения относятся к проверке контракта; они не являются
|
||||
* настройками рабочего устройства.
|
||||
*/
|
||||
|
||||
#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;
|
||||
}
|
||||
159
c/ds18b20/instance/Tests/test_ds18b20_incremental_search.c
Normal file
159
c/ds18b20/instance/Tests/test_ds18b20_incremental_search.c
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Проверки ds18b20_incremental_search: фиксируют ожидаемое поведение на воспроизводимых
|
||||
* входных данных.
|
||||
* Тестовые значения и подмены окружения относятся к проверке контракта; они не являются
|
||||
* настройками рабочего устройства.
|
||||
*/
|
||||
|
||||
/* 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;
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Конфигурация ds18b20 для порта stm32f1. Макросы задают параметры сборки и ограничения,
|
||||
* используемые исходниками этого модуля. Это образец для проекта: значения выводов, размеров
|
||||
* памяти и параметров устройства необходимо согласовать с целевой платой.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file onewire_config.f103.template.h
|
||||
* @brief Шаблон привязки шины 1-Wire для STM32F103 (порт stm32f1).
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Аппаратные временные слоты 1-Wire на STM32F1: управление линией, чтение уровня и
|
||||
* микросекундные задержки. Конфигурация выбирает GPIO и таймер; общий алгоритм поиска ROM
|
||||
* остаётся в onewire.c.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file onewire_stm32f1.c
|
||||
* @brief Порт шины 1-Wire на STM32F1: тайминги, вывод и задержки.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Конфигурация ds18b20 для порта stm32f4. Макросы задают параметры сборки и ограничения,
|
||||
* используемые исходниками этого модуля. Это образец для проекта: значения выводов, размеров
|
||||
* памяти и параметров устройства необходимо согласовать с целевой платой.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file onewire_config.f407.template.h
|
||||
* @brief Шаблон привязки шины 1-Wire для STM32F407VET6 (порт stm32f4).
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Аппаратные временные слоты 1-Wire на STM32F4 с открытым стоком. Порт использует
|
||||
* CMSIS-регистры и параметры onewire_config.h; частота таймера должна соответствовать расчёту
|
||||
* задержек при обмене с датчиками.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file onewire_stm32f4.c
|
||||
* @brief Порт шины 1-Wire на STM32F4: тайминги, вывод и задержки.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Конфигурация ds18b20 для порта stm32g4. Макросы задают параметры сборки и ограничения,
|
||||
* используемые исходниками этого модуля. Это образец для проекта: значения выводов, размеров
|
||||
* памяти и параметров устройства необходимо согласовать с целевой платой.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file onewire_config.g431.template.h
|
||||
* @brief Шаблон привязки шины 1-Wire для STM32G431 (порт stm32g4).
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user