Compare commits
238
Commits
v0.1.0
...
device-tools
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26f9d62896 | ||
|
|
4d101302be | ||
|
|
07929c880a | ||
|
|
4f7f6cdb87 | ||
|
|
1fc88684d5 | ||
|
|
fa5ea166a9 | ||
|
|
062f064c85 | ||
|
|
7b1a16d6e4 | ||
|
|
a640f98f74 | ||
|
|
f0edb70dbe | ||
|
|
6d59c3812c | ||
|
|
e144c3e86a | ||
|
|
fa2ab3cc65 | ||
|
|
5d2831794d | ||
|
|
a1b2835457 | ||
|
|
dc4b0ab71b | ||
|
|
cf75c5c855 | ||
|
|
491fa11569 | ||
|
|
149d19f3f9 | ||
|
|
45a34db657 | ||
|
|
bf5f094249 | ||
|
|
41e24629ce | ||
|
|
e52ba797e4 | ||
|
|
83fb9b6fe8 | ||
|
|
41f5b879fc | ||
|
|
f17c124f0e | ||
|
|
2da7e7b525 | ||
|
|
62c0492bfd | ||
|
|
5bf3167064 | ||
|
|
c6390ae1c9 | ||
|
|
876b58517c | ||
|
|
6ecf057a86 | ||
|
|
d42f2e1622 | ||
|
|
0c4eab4615 | ||
|
|
5af8a8e9c8 | ||
|
|
f613101124 | ||
|
|
cf65439400 | ||
|
|
a5e46d4627 | ||
|
|
95c8b8ac7d | ||
|
|
32e2fbedc0 | ||
|
|
bbb4bc2b9d | ||
|
|
20454ff7fa | ||
|
|
3c0f6fd9e6 | ||
|
|
f6d45b70d4 | ||
|
|
f1a7bcd8bc | ||
|
|
226c2ca599 | ||
|
|
f2956b54cd | ||
|
|
86ece3ea90 | ||
|
|
fe9b5a246e | ||
|
|
b0c0c05cfa | ||
|
|
eab8dc5e0a | ||
|
|
131f306131 | ||
|
|
4fc0ad4e14 | ||
|
|
da055c2787 | ||
|
|
8fc2e1b5cf | ||
|
|
b5b2d1ae37 | ||
|
|
c011c2957b | ||
|
|
6047ceac89 | ||
|
|
79b0cde20e | ||
|
|
90046de7fd | ||
|
|
0e3d89e1ee | ||
|
|
6c08eece61 | ||
|
|
7dfa8bd94d | ||
|
|
c7294830f9 | ||
|
|
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 | ||
|
|
def3eb08f3 | ||
|
|
10bfa587c7 | ||
|
|
3020de7e0c | ||
|
|
ac3025ee32 | ||
|
|
df02070cc4 | ||
|
|
37508f058b | ||
|
|
00f8dff838 | ||
|
|
3334f4cddd | ||
|
|
7b2f74a3ff | ||
|
|
aedd5fc082 | ||
|
|
96ce6f8759 | ||
|
|
9ba3742b29 | ||
|
|
874d2e1dfc | ||
|
|
4f23f48896 | ||
|
|
698632ebf3 | ||
|
|
8e9fd144ca | ||
|
|
67cca68fcc | ||
|
|
c0c20f6dc2 | ||
|
|
11f16f618b | ||
|
|
3a1e8d469a | ||
|
|
daaca0d94b | ||
|
|
20dbc2c721 | ||
|
|
49c65ea78d | ||
|
|
e95338d781 | ||
|
|
34f728cfaa | ||
|
|
daa3466a41 | ||
|
|
72205216f5 | ||
|
|
c9cf797132 | ||
|
|
5d48ddf0b0 | ||
|
|
b0f3731554 | ||
|
|
245d486a63 | ||
|
|
9ad0322c71 | ||
|
|
e262b0acdd | ||
|
|
1511719bb9 | ||
|
|
ed92ec346e | ||
|
|
0ce464499f | ||
|
|
3d374e7777 | ||
|
|
27ef6fcfdf | ||
|
|
d03e039cb4 | ||
|
|
235d5d81a2 | ||
|
|
d002b14e4e | ||
|
|
59f847e900 | ||
|
|
11007b0704 | ||
|
|
286e454464 | ||
|
|
821202b655 | ||
|
|
e691dfc337 | ||
|
|
8596918bf7 | ||
|
|
f5f15f6a04 | ||
|
|
bada3bbe8d | ||
|
|
be066a57eb | ||
|
|
87f2f2f56b | ||
|
|
b1f7b965f4 | ||
|
|
5523f049ec | ||
|
|
78d3f6690b | ||
|
|
b08ea02a5c | ||
|
|
6a82b309cc | ||
|
|
1b3a6318eb | ||
|
|
2316a5a26a | ||
|
|
57622ccd66 | ||
|
|
de5dc18a43 | ||
|
|
ba529f579c | ||
|
|
d44d57a7aa | ||
|
|
4f788d39d6 | ||
|
|
b972b6f33c | ||
|
|
1799b00167 | ||
|
|
454baeed98 | ||
|
|
940d9ada76 | ||
|
|
01ffc0e496 | ||
|
|
6eaa47d3a8 | ||
|
|
3c4ac9963d | ||
|
|
1f49ba49bb | ||
|
|
d9eb7dd9ad | ||
|
|
b7f00acb24 | ||
|
|
f6787163dc | ||
|
|
70b2ee3cad | ||
|
|
b3ec401318 | ||
|
|
666173b2f0 | ||
|
|
17259f5e28 | ||
|
|
79ac0d52a7 | ||
|
|
52d687b5ae | ||
|
|
7897fe9f0d | ||
|
|
5f8fc07f2a | ||
|
|
45582cda4a | ||
|
|
c8565d868d | ||
|
|
2bb9fea90a | ||
|
|
1523640414 | ||
|
|
6392d36741 | ||
|
|
fa6f615e81 | ||
|
|
0c635ce1be | ||
|
|
392817432b | ||
|
|
5caca07ad4 | ||
|
|
1f1006bf2a | ||
|
|
20850b26df | ||
|
|
31a111ee46 | ||
|
|
e163d9ba55 | ||
|
|
20f63bc389 | ||
|
|
0a30c39e07 | ||
|
|
e8490b3122 | ||
|
|
a14140f794 | ||
|
|
261f2ca6f1 | ||
|
|
1296773346 | ||
|
|
af82687e42 | ||
|
|
e9ec078b38 | ||
|
|
43410c3f12 | ||
|
|
076c51c083 | ||
|
|
e9c5ec90b8 | ||
|
|
00a0f944c4 | ||
|
|
9c0701ab9b | ||
|
|
c8eec55978 | ||
|
|
9b636a9f40 | ||
|
|
19becd7b8c | ||
|
|
5504104cc5 | ||
|
|
e6bea52db0 | ||
|
|
92fc2ade58 | ||
|
|
080b6900f5 | ||
|
|
68040a2803 | ||
|
|
7fe76b3c3b | ||
|
|
0f77a510e6 | ||
|
|
7910a07f2f | ||
|
|
2fd51a1d7e | ||
|
|
de180750aa | ||
|
|
648457e587 | ||
|
|
5f3a9879d5 | ||
|
|
9ce32b56d4 | ||
|
|
05863db2a2 | ||
|
|
ab60e58318 | ||
|
|
94fc8c5ef2 | ||
|
|
4b32cda46e | ||
|
|
217073982f | ||
|
|
94c991380b | ||
|
|
5a8724da6d | ||
|
|
d507b84a76 | ||
|
|
58da8aa2fe | ||
|
|
bd8d22312e | ||
|
|
d3bb634fb1 | ||
|
|
01de7651e8 | ||
|
|
34c46ec679 | ||
|
|
23e37d1c4d | ||
|
|
a1d2d05f42 | ||
|
|
aea2a79d2c | ||
|
|
1345766efb | ||
|
|
bb7c94f1b7 | ||
|
|
ce6c4327a4 | ||
|
|
085eb3c8bd | ||
|
|
033c7ab9e8 | ||
|
|
3cf85a278e |
@@ -2,6 +2,7 @@
|
||||
build/
|
||||
cmake-build-*/
|
||||
*.o
|
||||
*.obj
|
||||
*.d
|
||||
*.a
|
||||
*.elf
|
||||
@@ -22,3 +23,10 @@ __pycache__/
|
||||
*.uvguix.*
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
.codex-build/
|
||||
|
||||
# Generated native library (built before packaging)
|
||||
python/protocan/native/
|
||||
|
||||
# Кэш pytest создаётся проверками автоматически и не входит в исходники общей библиотеки.
|
||||
.pytest_cache/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "third_party/kdiff3"]
|
||||
path = third_party/kdiff3
|
||||
url = https://invent.kde.org/sdk/kdiff3.git
|
||||
@@ -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
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
# Новый проект на переносимых библиотеках
|
||||
|
||||
Этот репозиторий содержит два разных типа кода:
|
||||
|
||||
1. `c/<library>/include` и `c/<library>/src` — переносимое ядро;
|
||||
2. `c/<library>/ports/<family>` — небольшой аппаратный порт конкретного
|
||||
семейства микроконтроллеров.
|
||||
|
||||
Привязка выводов, частоты шин и выбранной периферии принадлежит плате и всегда
|
||||
остаётся в проекте в `app_config.h` либо в узком конфигурационном заголовке
|
||||
библиотеки, например `onewire_config.h`.
|
||||
|
||||
## Рекомендуемая структура прошивки
|
||||
|
||||
```text
|
||||
my-device/
|
||||
app/ логика изделия
|
||||
include/
|
||||
src/
|
||||
board/ одна конкретная плата
|
||||
app_config.h выводы, частоты, адреса, возможности платы
|
||||
port/ связь библиотек с SDK выбранного МК
|
||||
display_port.c
|
||||
keypad_port.c
|
||||
eeprom_port.c
|
||||
can_port.c
|
||||
vendor/ CMSIS, startup, linker script или Cube-generated код
|
||||
lib/templates/ git submodule этого репозитория
|
||||
```
|
||||
|
||||
Не следует помещать `stm32xxxx.h`, `HAL_*`, регистры, IRQ handlers и номера
|
||||
выводов в `app/` или в ядро библиотеки. Тогда при переходе на другой МК
|
||||
заменяются только `board/`, `port/`, startup и linker script.
|
||||
|
||||
## Выбор готового порта DS18B20
|
||||
|
||||
| Целевой МК | Файл порта | Шаблон конфигурации |
|
||||
|---|---|---|
|
||||
| STM32F103 | `c/ds18b20/ports/stm32f1/onewire_stm32f1.c` | `onewire_config.f103.template.h` |
|
||||
| STM32G431 | `c/ds18b20/ports/stm32g4/onewire_stm32g4.c` | `onewire_config.g431.template.h` |
|
||||
| STM32G474 | `c/ds18b20/ports/stm32g4/onewire_stm32g4.c` | `onewire_config.g474.template.h` |
|
||||
|
||||
В сборку добавляются ровно три файла:
|
||||
|
||||
```text
|
||||
c/ds18b20/src/onewire.c
|
||||
c/ds18b20/src/ds18b20.c
|
||||
c/ds18b20/ports/<family>/onewire_<family>.c
|
||||
```
|
||||
|
||||
Пути включения:
|
||||
|
||||
```text
|
||||
c/ds18b20/include
|
||||
board
|
||||
```
|
||||
|
||||
Нужный шаблон копируется в `board/onewire_config.h`; после этого в нём
|
||||
меняются только порт, номер вывода и бит тактирования GPIO.
|
||||
|
||||
G431 и G474 намеренно используют один исходник порта: GPIO, RCC и DWT для
|
||||
этой задачи совместимы. Раздельными остаются конфигурация платы, startup,
|
||||
linker script и выбранный CMSIS device define.
|
||||
|
||||
## Выбор готового порта parallel NAND
|
||||
|
||||
| Целевой МК | Реализация шины | Каталог порта |
|
||||
|---|---|---|
|
||||
| TMS320F2812 | XINTF Zone 6 | `c/parallel-nand/ports/tms320f2812` |
|
||||
| STM32F103RCT6 | GPIO bit-bang, поскольку FSMC недоступен в LQFP64 | `c/parallel-nand/ports/stm32f103rc` |
|
||||
| STM32F103ZET6 | FSMC NAND Bank 3 | `c/parallel-nand/ports/stm32f103ze` |
|
||||
| STM32F407VET6 | FSMC NAND Bank 2 | `c/parallel-nand/ports/stm32f407ve` |
|
||||
| STM32G474CEU6 | GPIO bit-bang для UFQFPN48 | `c/parallel-nand/ports/stm32g474ce` |
|
||||
|
||||
Ядро поддерживает профили Micron `MT29F1G08ABADA` и Hynix
|
||||
`HY27UF084G2M`. В сборку добавляются `src/parallel_nand.c`, при необходимости
|
||||
`src/parallel_nand_gas.c` или `src/parallel_nand_pcan_gas.c`, и ровно один
|
||||
аппаратный порт. Конкретная распиновка, требуемые модули HAL и пример
|
||||
инициализации приведены в README выбранного порта.
|
||||
|
||||
Для `PROGRAM`/`ERASE` линия `WP#` должна управляться портом. Готовые порты
|
||||
STM32F103RC, STM32F103ZE и STM32G474CE объявляют поддержку записи. Базовые
|
||||
схемы TMS320F2812 и STM32F407VE фиксируют `WP#` аппаратно и безопасно
|
||||
возвращают `PNAND_ERROR_WRITE_PROTECTED`, пока плата и порт не доработаны.
|
||||
|
||||
Рабочий пример полноценного G474-порта находится в соседнем проекте
|
||||
`CANdle_stm32` (candleLight): `src/device/device_g4.c` отвечает за clock/GPIO,
|
||||
`src/can/m_can.c` — за FDCAN, а `include/config.h` — за привязку платы
|
||||
STM32G474VET6. Эти файлы остаются в прошивке, поскольку зависят от STM32 HAL,
|
||||
USB gs_usb и конкретной разводки адаптера; переносимая логика протоколов
|
||||
остаётся в `templates`.
|
||||
|
||||
## Какие части KONOR уже общие
|
||||
|
||||
| Возможность | Переносимое ядро | Что остаётся в `port/` проекта |
|
||||
|---|---|---|
|
||||
| DS18B20/1-Wire | `c/ds18b20` | готовый порт выбранного семейства и вывод |
|
||||
| дисплей ST7789 | `c/st7789` | SPI, DC, CS, RESET, подсветка и задержка |
|
||||
| кнопки | `c/keypad` | настройка GPIO и чтение уровней |
|
||||
| меню | `c/menu` | только привязка painter к дисплею |
|
||||
| индикация состояний | `c/led-indicator` | готовый STM32 HAL-порт или callbacks GPIO и выбранного таймера |
|
||||
| EEPROM FT24C256 | `c/eeprom-ft24c256` | I2C write/write-read и задержка |
|
||||
| CAN SETTINGS | `c/can-sensor` | bxCAN для F103 либо FDCAN для G431/G474 |
|
||||
|
||||
Порт дисплея, EEPROM и CAN нельзя честно сделать только по названию МК:
|
||||
нужно знать экземпляр периферии, альтернативную функцию и выводы конкретной
|
||||
платы. Поэтому библиотеки принимают таблицы обратных вызовов, а короткие
|
||||
адаптеры лежат в проекте рядом с `app_config.h`.
|
||||
|
||||
## Порядок создания проекта
|
||||
|
||||
1. Создать проект в CubeMX/Keil либо добавить CMSIS, startup и linker script.
|
||||
2. Подключить `templates` сабмодулем.
|
||||
3. Добавить только ядра нужных библиотек.
|
||||
4. Выбрать готовый 1-Wire порт по таблице выше.
|
||||
5. Создать `board/app_config.h` с выводами и частотами.
|
||||
6. Реализовать короткие адаптеры SPI/GPIO/I2C/CAN в `port/`.
|
||||
7. Сначала прогнать host-тесты библиотек, затем проверить аппаратные порты
|
||||
логическим анализатором или осциллографом.
|
||||
|
||||
Так новый проект не является копией старого: он использует общие ядра одной
|
||||
версии, а аппаратные отличия видны в одном небольшом каталоге `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
|
||||
```
|
||||
@@ -13,10 +13,29 @@
|
||||
templates/
|
||||
c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты
|
||||
python/ модули на чистом Python 3.9+, только stdlib
|
||||
tools/ общие инструменты сборки и выпуска прошивок
|
||||
```
|
||||
|
||||
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
|
||||
и STM32G474 описаны в [`NEW_PROJECT.md`](NEW_PROJECT.md).
|
||||
|
||||
Нормативная документация SETCAN/ProtoCAN, реестр общего адресного пространства
|
||||
и исходный Excel собраны в [`doc/setcan`](doc/setcan/README.md).
|
||||
|
||||
## Что лежит
|
||||
|
||||
### FPGA и DSView
|
||||
|
||||
| Блок | Назначение |
|
||||
|---|---|
|
||||
| [Блоки логического анализатора](hdl/logic-analyzer/README.md) | Аппаратный триггер, BRAM, AXI и кольцевой буфер DDR |
|
||||
| [Осциллограф](hdl/scope-acquisition/README.md) | Синхронизация каналов ADC, триггер, буфер и выдача записи |
|
||||
| [Ethernet в FPGA](hdl/logic-udp-ethernet/README.md) | RGMII, MDIO для RTL8211F, UDP-поток и буфер восьми входов |
|
||||
| [HDMI-отрисовка](hdl/logic-hdmi-preview/README.md) | График восьми цифровых каналов из входов или потока |
|
||||
| [Трёхфазный ШИМ](hdl/vfd-pwm/README.md) | DDS, рампа, комплементарные выходы и мёртвое время |
|
||||
| [DSView Ethernet](tools/dsview/zynq-ethernet/README.md) | Нативное подключение TCP/UDP, запуск и остановка |
|
||||
| [ZNET](c/zynq-net/README.md) | C99-кодек Ethernet-команд ввода-вывода |
|
||||
|
||||
### C
|
||||
|
||||
| Библиотека | Что делает | Зависимости | Что нужно от платформы |
|
||||
@@ -24,21 +43,62 @@ templates/
|
||||
| [`c/st7789`](c/st7789) | TFT ST7789V по SPI, RGB565, текст без кадрового буфера | `stdint.h` | SPI-запись, линия DC, задержка |
|
||||
| [`c/keypad`](c/keypad) | шесть кнопок: антидребезг, автоповтор, удержание, очередь событий | `stdint.h` | чтение уровня кнопки, время в мс |
|
||||
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `stdint.h` | заливка прямоугольника, вывод строки |
|
||||
| [`c/led-indicator`](c/led-indicator) | неблокирующие режимы LED: работа, активность, предупреждение и ошибка | C99 | логическая запись канала, время выбранного таймера — **STM32F1/F4/G4 HAL-порт в комплекте** |
|
||||
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
|
||||
| [`c/can-sensor`](c/can-sensor) | передача 64-битных ROM датчиков парой CAN-кадров | `stdint.h` | отправка и приём CAN-кадра |
|
||||
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порт STM32F1 в комплекте** |
|
||||
| [`c/protocan-transport`](c/protocan-transport) | транспорт ProtoCAN: кадр, канал, CRC, общее адресное пространство, каталог GUI | `stdint.h` | запись в поток и запрос свободного места — **порт STM32F4 в комплекте** |
|
||||
| [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 |
|
||||
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
|
||||
| [`c/ds18b20-ds2480`](c/ds18b20-ds2480) | DS18B20 через DS2480B: поиск ROM, температура, EEPROM, паразитное питание | C99 | UART 9600 8N1, сброс моста, задержка |
|
||||
| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android, STM32F4 и TMS320F2812-порты в комплекте** |
|
||||
| [`c/set-protocol/ports/stm32f407-devboard-v1`](c/set-protocol/ports/stm32f407-devboard-v1) | доступ SETGUI к Modbus-регистрам F407 через CAN485 DevBoard_V1 | `pcan_modbus_server`, STM32 HAL CAN | bxCAN FIFO0 и callbacks карты регистров |
|
||||
| [`c/set-protocol/ports/stm32-bxcan`](c/set-protocol/ports/stm32-bxcan) | порт прикладного ProtoCAN для STM32, бывший SETCAN; сохранён API `PROTOCAN_*` | STM32 HAL CAN/RTC/TIM + общее ядро `pcan_id` | classic bxCAN; настройки платы предоставляет прошивка |
|
||||
| [`c/set-protocol/ports/tms320f2812`](c/set-protocol/ports/tms320f2812) | SETProtocol v2 firmware service по segmented classic CAN | `set_protocol`, `set_can`, `set_firmware` | CAN TX, Flash erase/write/read, optional signature policy и reboot |
|
||||
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | **STM32F103C8/F407VE/G474VE и TMS320F2812/F28335-порты в комплекте**; проекты STM32 подключены через templates, есть boot main и вход из приложения |
|
||||
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
|
||||
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
|
||||
| [`c/global-time`](c/global-time) | программные часы 1 мс, ProtoCAN RTCSETUP, готовность платы и атомарная метка логгера | C99 | IRQ lock, CAN TX, такт 1 мс — **порт TMS320F28335 eCAN-A / CPU Timer2 в комплекте** |
|
||||
| [`c/firmware-info`](c/firmware-info) | SemVer, build stamp и git build ID работающего образа | C99 | config-порты STM32F1/F4/G4 и К1921ВК028; упаковка для Modbus/SETGUI |
|
||||
| [`c/parallel-nand`](c/parallel-nand) | parallel NAND x8: raw read/program/erase, профили Micron MT29F1G08 и Hynix HY27UF084G2M, bad-block marker и банковое окно GAS для SETGUI | C99, опционально `set-protocol/pcan_gas` | **порты TMS320F2812, STM32F103RC/ZE, STM32F407VE и STM32G474CEU6; запись требует управляемого WP#** |
|
||||
|
||||
### 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 |
|
||||
|
||||
Кодировщики `c/protocan-transport` и `python/protocan` дают побайтово
|
||||
### Инструменты
|
||||
|
||||
Исходники внешних приложений находятся в [`third_party`](third_party/README.md):
|
||||
[`third_party/kdiff3`](third_party/kdiff3) содержит официальный репозиторий KDiff3
|
||||
как вложенный Git submodule для развития сравнения файлов и каталогов в SETGUI.
|
||||
|
||||
| Инструмент | Что делает |
|
||||
|---|---|
|
||||
| [`tools/firmware-build/fpga`](tools/firmware-build/fpga/README.md) | общий итератор версии и HDL-паспорт для Xilinx ISE, Vivado и Altera/Intel Quartus; контроль `.bit`/`.sof`, manifest, SHA-256 и архив |
|
||||
| [`tools/firmware-publish`](tools/firmware-publish) | единый BAT и конфигурации для проверки и публикации `.hex` Keil / `.bin` CCS 12 в каталоге SETGUI |
|
||||
|
||||
Общие тренды для Android GUI и SETGUI: [формат JSON, C99-ядро и адаптеры](c/set-protocol/docs/GUI_TRENDS.md).
|
||||
Общие масштабирование и маркеры: [C99, JNI и Python/Qt](c/set-protocol/docs/GUI_PLOT.md).
|
||||
|
||||
Кодировщики `c/set-protocol` и `python/protocan` дают побайтово
|
||||
одинаковый результат — это зафиксировано эталонами в тестах на C.
|
||||
|
||||
Интерактивная документация общего ядра: [`doc/setprotocol.html`](doc/setprotocol.html).
|
||||
В ней отдельно описаны граница ядра, 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`.
|
||||
|
||||
## Как подключить к проекту
|
||||
|
||||
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
|
||||
@@ -48,6 +108,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).
|
||||
|
||||
Дальше в сборку добавляются только нужные каталоги:
|
||||
|
||||
```
|
||||
@@ -69,17 +133,18 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
|
||||
|
||||
| Проект | Что берёт |
|
||||
|---|---|
|
||||
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20 |
|
||||
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20, firmware-info |
|
||||
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
|
||||
| `CAN_to_RS485` | protocan-transport, python/protocan |
|
||||
| `CAN_to_RS485` | set-protocol legacy transport, python/protocan |
|
||||
| `CANdle_stm32` (candleLight) | G431/G474 FDCAN board-support; CAN/UART-загрузчик G474 и вход из приложения подключены к `protocan-boot/ports/stm32g474` через templates |
|
||||
| `SETGUI` | python/protocan |
|
||||
| `SETGUI`, Android GUI и новые устройства SET | set-protocol |
|
||||
| `k1921vk028` | rtc-service |
|
||||
|
||||
## Что сюда не попало и почему
|
||||
|
||||
| Код | Почему не переносится | Что можно вытащить |
|
||||
|---|---|---|
|
||||
| `SETCAN/Src/protocan.c` | 59 вызовов `HAL_*`, привязка к `CAN_HandleTypeDef`, `RTC_HandleTypeDef`, `NVIC_SystemReset()` | разбиение длинных посылок на кадры и проверка даты — чистая логика, нужны два интерфейса: «отправить кадр» и «часы». Раскладка идентификатора уже вынесена в `pcan_id` |
|
||||
| `1921vk028/drivers/user/src/one_wire_user.c` | аппаратный 1-Wire `OWI_*` из `plib028`, мигание светодиодом прямо внутри опроса датчика | логика DS18B20 уже есть в `c/ds18b20` — переносится порт, а не алгоритм |
|
||||
| `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
|
||||
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Правила общего кроссплатформенного кода
|
||||
|
||||
Этот репозиторий — единственный источник общих алгоритмов для `GUI_Android`,
|
||||
`SETGUI`, прошивок и будущих GUI. Копирование одной реализации между Kotlin,
|
||||
Python, C# или другим языком запрещено.
|
||||
|
||||
## 1. Граница C-ядра и порта
|
||||
|
||||
В `c/set-protocol` на C99 обязательно размещаются:
|
||||
|
||||
- форматы кадров и идентификаторов, CRC/checksum, endian-преобразования;
|
||||
- построение команд, разбор и проверка ответов;
|
||||
- автоматы обмена, сегментация, повтор, таймаутные состояния без системных часов;
|
||||
- общие вычисления, таблицы и каталоги, влияющие на поведение протокола;
|
||||
- проверка образов прошивки и других бинарных форматов.
|
||||
|
||||
Порт на языке GUI содержит только:
|
||||
|
||||
- вызовы C через стабильный ABI (`ctypes`, JNI, P/Invoke, Swift FFI и т. п.);
|
||||
- преобразование C-структур в модели языка без повторения алгоритма;
|
||||
- работу с USB, COM, Bluetooth, SocketCAN и API операционной системы;
|
||||
- жизненный цикл, потоки, разрешения, хранение настроек и UI;
|
||||
- локализованный текст и чисто визуальные преобразования.
|
||||
|
||||
Порт не вычисляет CRC, не собирает wire-пакет и не разбирает его поля заново.
|
||||
Если для функции C-ядро недоступно, приложение сообщает об ошибке сборки или
|
||||
загрузки. Алгоритмический fallback на языке GUI запрещён: он снова создаёт две
|
||||
версии протокола.
|
||||
|
||||
## 2. Разделение контроллеров
|
||||
|
||||
Профили контроллеров нельзя сливать по совпадению названия транспорта:
|
||||
|
||||
- **ПМ67 / TMS320F2812** — основной контроллер, собственные RS и CAN;
|
||||
- **ПМ35 / TMS320F28335 periph** — отдельный контроллер и отдельный CAN для
|
||||
настроечного терминала, а также собственный прямой RS232/485-протокол.
|
||||
|
||||
Выбор профиля выполняется в GUI, но выбранный профиль вызывает свой отдельный
|
||||
модуль C-ядра. Наличие одной CAN-линии не даёт права удалить или подменить
|
||||
другую.
|
||||
|
||||
## 3. Порядок изменения протокола
|
||||
|
||||
1. Добавить или изменить публичный заголовок и реализацию в
|
||||
`c/set-protocol/include` и `c/set-protocol/src`.
|
||||
2. Зафиксировать эталонные байты и ошибочные случаи в C-тесте.
|
||||
3. При необходимости расширить `pcan_abi.h`, сохраняя бинарную совместимость.
|
||||
4. Добавить тонкие порты в `ports/<platform>` и `python/`; в них не должно быть
|
||||
второго кодека.
|
||||
5. Одними и теми же векторами проверить C, Python и Android/JVM.
|
||||
6. Собрать SETGUI и Android с одним commit submodule `templates`.
|
||||
|
||||
Изменение только в одном GUI считается незавершённым. Сначала меняется
|
||||
`templates`, затем оба потребителя обновляют ссылку submodule на проверенный
|
||||
commit.
|
||||
|
||||
## 4. Требования к C-ядру
|
||||
|
||||
- C99, без зависимости от GUI и конкретной ОС.
|
||||
- Буферы и их размеры передаются явно; владение памятью остаётся у вызывающего.
|
||||
- Для MCU основная логика не требует heap, исключений или файловой системы.
|
||||
- Endian и размеры целых задаются через `stdint.h`, структуры wire-формата не
|
||||
передаются через ABI без явного стабильного представления.
|
||||
- Экспорт shared library идёт через `PCAN_ABI_API`; существующие символы не
|
||||
меняют смысл и сигнатуру.
|
||||
- Ошибки возвращаются детерминированным кодом и тестируются наряду с успехом.
|
||||
|
||||
## 5. Проверка на ревью
|
||||
|
||||
Изменение нельзя принимать, если ответ «да» хотя бы на один вопрос:
|
||||
|
||||
- появился одинаковый CRC/parser/builder в двух языках;
|
||||
- UI знает byte offset, endian или служебный байт wire-протокола;
|
||||
- Python и Kotlin содержат одинаковую таблицу команд, влияющую на обмен;
|
||||
- добавлен тихий fallback, поведение которого отличается от C;
|
||||
- обновлён один GUI без обновления и теста `templates`;
|
||||
- ПМ67 и ПМ35 сведены к одному соединению или одному состоянию контроллера.
|
||||
|
||||
Текущее состояние и очередь переноса перечислены в
|
||||
[`doc/CROSS_PLATFORM_AUDIT.md`](doc/CROSS_PLATFORM_AUDIT.md).
|
||||
@@ -0,0 +1,26 @@
|
||||
# Сборка библиотеки can_sensor. Состав исходников и публичные include-пути задают подключение
|
||||
# к проекту потребителя. Файл также собирает и регистрирует хостовые проверки; запускать их
|
||||
# следует через CTest из каталога сборки.
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(can_sensor C)
|
||||
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
add_library(can_sensor STATIC can_sensor.c)
|
||||
target_include_directories(can_sensor PUBLIC .)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(can_sensor PRIVATE /W4)
|
||||
else()
|
||||
target_compile_options(can_sensor PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
option(CAN_SENSOR_BUILD_TESTS "Собирать тесты транспорта CAN sensor" ON)
|
||||
if(CAN_SENSOR_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_executable(test_can_sensor tests/test_can_sensor.c)
|
||||
target_link_libraries(test_can_sensor PRIVATE can_sensor)
|
||||
add_test(NAME can_sensor COMMAND test_can_sensor)
|
||||
endif()
|
||||
+41
-10
@@ -1,27 +1,28 @@
|
||||
# can-sensor
|
||||
|
||||
Передача 64-битных идентификаторов датчиков (ROM 1-Wire) по шине CAN.
|
||||
Однокадровый транспорт SETCAN SETTINGS для привязки 64-битного ROM 1-Wire к
|
||||
локации «номер сборки + позиция» по шине CAN.
|
||||
|
||||
Ядро на C99: не включает заголовки периферии, не обращается к регистрам,
|
||||
не пользуется прерываниями. Обмен идёт через таблицу `CanSensor_Io`,
|
||||
которую заполняет порт платы.
|
||||
|
||||
Сообщение состоит из двух классических CAN-кадров — 64-битный идентификатор
|
||||
и преамбула вместе не помещаются в восемь байтов поля данных:
|
||||
Сообщение помещается в один Extended CAN-кадр: локация находится в Body
|
||||
29-битного ID, а ROM — в восьми байтах payload:
|
||||
|
||||
```
|
||||
кадр 1 — преамбула, DLC = 2 | команда | позиция |
|
||||
кадр 2 — идентификатор, DLC = 8| ROM 8 байт |
|
||||
запрос 0x17FEZZYY: DLC=0 — GET, DLC=8 — WRITE/REPLACE/CLEAR
|
||||
ответ 0x1FFEZZYY: DLC=8 — текущий ROM, DLC=1 — код ошибки
|
||||
```
|
||||
|
||||
## Состав
|
||||
|
||||
| Файл | Что делает | Зависимости |
|
||||
|---|---|---|
|
||||
| `can_sensor.h`, `can_sensor.c` | сборка и разбор пары кадров, повторы передачи, счётчики обмена | `stdint.h` |
|
||||
| `can_sensor.h`, `can_sensor.c` | сборка и разбор SETTINGS, планировщик PULSE, повторы передачи, счётчики обмена | `stdint.h` |
|
||||
| `ports/stm32f1/` | опросный порт CAN1: GPIO, BTR, фильтр, mailbox/FIFO, тайм-аут ACK | CMSIS `stm32f10x.h` |
|
||||
|
||||
Приём собирает сообщение сам: кадр ROM без преамбулы и кадр с неверной
|
||||
длиной отбрасываются и учитываются в `dropped_frames`.
|
||||
`ZZ` — номер сборки, `YY` — позиция. Нулевой ROM с DLC=8 очищает локацию.
|
||||
|
||||
## Что нужно от платформы
|
||||
|
||||
@@ -38,15 +39,45 @@ uint8_t receive(void *ctx, CanSensor_Frame *frame); /* 1 — кадр п
|
||||
CanSensor link;
|
||||
CanSensor_Io io = { .send = bxcan_send, .receive = bxcan_receive, .context = &board };
|
||||
CanSensor_Config config;
|
||||
CanSensor_ConfigDefault(&config); /* tx_id 0x200, кадр ROM 0x201, 3 попытки */
|
||||
CanSensor_ConfigDefault(&config); /* TX 0x1FFE0000, RX 0x17FE0000 */
|
||||
config.pulse_period_ms = 1000U; /* 0 — PULSE выключен */
|
||||
|
||||
CanSensor_Init(&link, &io, &config);
|
||||
CanSensor_SendId(&link, position, rom);
|
||||
CanSensor_SendId(&link, position, assembly_serial, rom);
|
||||
CanSensor_Task(&link, now_ms);
|
||||
|
||||
CanSensor_Message message;
|
||||
if (CanSensor_Poll(&link, &message)) { /* принят идентификатор */ }
|
||||
```
|
||||
|
||||
Период можно менять во время работы через `CanSensor_SetPulsePeriod()`. Значение
|
||||
`0` именно выключает heartbeat: кадры PULSE больше не ставятся в очередь. При
|
||||
повторном включении первый кадр отправляется через полный заданный период.
|
||||
|
||||
## Порт STM32F1
|
||||
|
||||
Порт не использует STM32 HAL и не занимает прерывания. Скопируйте
|
||||
`ports/stm32f1/can_sensor_stm32f1_config.f103.template.h` в приложение под
|
||||
именем `can_sensor_stm32f1_config.h`, добавьте `ports/stm32f1` в include path и
|
||||
соберите `can_sensor_stm32f1.c` вместе с ядром:
|
||||
|
||||
```c
|
||||
CanSensorStm32F1_Port bxcan = {0};
|
||||
CanSensor_Io io;
|
||||
|
||||
CanSensorStm32F1_Start(&bxcan, 500000U, 0x17F00000UL, 0x1FF00000UL, 0U);
|
||||
io = CanSensorStm32F1_MakeIo(&bxcan);
|
||||
CanSensor_Init(&link, &io, &config);
|
||||
|
||||
/* Из главного цикла: */
|
||||
CanSensorStm32F1_Task(&bxcan, now_ms, 20U);
|
||||
```
|
||||
|
||||
Фильтр порта принимает только Extended data-кадры. Маска задаётся приложением:
|
||||
можно пропустить только SETTINGS либо оставить открытым `MsgType` для соседних
|
||||
команд того же узла. `CanSensorStm32F1_Receive()` доступна отдельно, если перед
|
||||
`CanSensor_HandleFrame()` приложению нужно разобрать такие команды самому.
|
||||
|
||||
## Проверено в проектах
|
||||
|
||||
`KONOR_ds18b20` — bxCAN на STM32F103C8T6. Естественная пара — [`ds18b20`](../ds18b20):
|
||||
|
||||
+148
-133
@@ -1,72 +1,41 @@
|
||||
/*
|
||||
* Обмен настройками датчиков DS18B20 через однокадровые сообщения SETCAN. ROM идентифицирует
|
||||
* датчик независимо от его позиции; прикладное состояние и отправка CAN предоставляются через
|
||||
* интерфейс библиотеки.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file can_sensor.c
|
||||
* @brief Сборка, передача и разбор сообщений «преамбула + идентификатор».
|
||||
*
|
||||
* Реализация не хранит очередей и не пользуется временем: состояние приёма
|
||||
* ограничено признаком принятой преамбулы, поэтому библиотека одинаково
|
||||
* работает и в главном цикле, и в обработчике прерывания порта.
|
||||
* @brief Однокадровый транспорт SETCAN SETTINGS для локаций DS18B20.
|
||||
*/
|
||||
|
||||
#include "can_sensor.h"
|
||||
|
||||
/**
|
||||
* @brief Возвращает идентификатор кадра данных для заданных настроек.
|
||||
*
|
||||
* @param config Настройки узла.
|
||||
* @return Идентификатор кадра ROM при передаче.
|
||||
*/
|
||||
static uint32_t can_sensor_tx_data_id(const CanSensor_Config *config)
|
||||
/** Возвращает 1, если все восемь байтов ROM равны нулю. */
|
||||
static uint8_t can_sensor_rom_is_zero(const uint8_t *rom)
|
||||
{
|
||||
if (config->tx_data_id != 0U) {
|
||||
return config->tx_data_id;
|
||||
uint8_t index;
|
||||
|
||||
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||
if (rom[index] != 0U) {
|
||||
return 0U;
|
||||
}
|
||||
}
|
||||
return config->tx_id + CAN_SENSOR_DATA_ID_OFFSET;
|
||||
return 1U;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Возвращает ожидаемый идентификатор преамбулы при приёме.
|
||||
*
|
||||
* @param config Настройки узла.
|
||||
* @return Идентификатор кадра преамбулы.
|
||||
*/
|
||||
static uint32_t can_sensor_rx_id(const CanSensor_Config *config)
|
||||
{
|
||||
if (config->rx_id != 0U) {
|
||||
return config->rx_id;
|
||||
}
|
||||
return config->tx_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Возвращает ожидаемый идентификатор кадра ROM при приёме.
|
||||
*
|
||||
* @param config Настройки узла.
|
||||
* @return Идентификатор кадра идентификатора датчика.
|
||||
*/
|
||||
static uint32_t can_sensor_rx_data_id(const CanSensor_Config *config)
|
||||
{
|
||||
if (config->rx_data_id != 0U) {
|
||||
return config->rx_data_id;
|
||||
}
|
||||
return can_sensor_rx_id(config) + CAN_SENSOR_DATA_ID_OFFSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Передаёт один кадр с повторами при отказе контроллера.
|
||||
*
|
||||
* @param link Состояние узла.
|
||||
* @param frame Передаваемый кадр.
|
||||
* @return 1, если кадр принят контроллером, иначе 0.
|
||||
*/
|
||||
/** Передаёт уже собранный кадр с ограниченным числом повторов. */
|
||||
static uint8_t can_sensor_send_frame(CanSensor *link, const CanSensor_Frame *frame)
|
||||
{
|
||||
uint8_t attempt;
|
||||
|
||||
for (attempt = 0U; attempt < link->config.retries; attempt++) {
|
||||
if (link->io.send(link->io.context, frame) != 0U) {
|
||||
link->sent_messages++;
|
||||
return 1U;
|
||||
}
|
||||
}
|
||||
link->send_errors++;
|
||||
return 0U;
|
||||
}
|
||||
|
||||
@@ -76,22 +45,18 @@ void CanSensor_ConfigDefault(CanSensor_Config *config)
|
||||
return;
|
||||
}
|
||||
config->tx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
||||
config->tx_data_id = CAN_SENSOR_DEFAULT_TX_ID + CAN_SENSOR_DATA_ID_OFFSET;
|
||||
config->rx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
||||
config->rx_data_id = CAN_SENSOR_DEFAULT_TX_ID + CAN_SENSOR_DATA_ID_OFFSET;
|
||||
config->extended = 0U;
|
||||
config->rx_id = CAN_SENSOR_DEFAULT_RX_ID;
|
||||
config->pulse_period_ms = 0U;
|
||||
config->extended = 1U;
|
||||
config->retries = CAN_SENSOR_DEFAULT_RETRIES;
|
||||
}
|
||||
|
||||
uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
||||
const CanSensor_Config *config)
|
||||
{
|
||||
uint8_t index;
|
||||
|
||||
if ((link == 0) || (io == 0) || (io->send == 0)) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
link->io = *io;
|
||||
if (config != 0) {
|
||||
link->config = *config;
|
||||
@@ -101,131 +66,133 @@ uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
||||
if (link->config.tx_id == 0U) {
|
||||
link->config.tx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
||||
}
|
||||
if (link->config.rx_id == 0U) {
|
||||
link->config.rx_id = CAN_SENSOR_DEFAULT_RX_ID;
|
||||
}
|
||||
link->config.tx_id &= CAN_SENSOR_HEADER_MASK;
|
||||
link->config.rx_id &= CAN_SENSOR_HEADER_MASK;
|
||||
link->config.extended = 1U;
|
||||
if (link->config.retries == 0U) {
|
||||
link->config.retries = CAN_SENSOR_DEFAULT_RETRIES;
|
||||
}
|
||||
|
||||
link->rx.command = 0U;
|
||||
link->rx.position = 0U;
|
||||
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||
link->rx.id[index] = 0U;
|
||||
}
|
||||
link->rx_preamble = 0U;
|
||||
link->sent_messages = 0U;
|
||||
link->send_errors = 0U;
|
||||
link->received_messages = 0U;
|
||||
link->dropped_frames = 0U;
|
||||
link->pulse_last_ms = 0U;
|
||||
link->pulse_counter = 0U;
|
||||
link->pulse_armed = 0U;
|
||||
return 1U;
|
||||
}
|
||||
|
||||
uint8_t CanSensor_BuildPreamble(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
uint8_t command, uint16_t position)
|
||||
uint8_t CanSensor_BuildFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
uint8_t response, uint8_t position,
|
||||
uint8_t assembly_serial, const uint8_t *data,
|
||||
uint8_t length)
|
||||
{
|
||||
uint8_t index;
|
||||
uint32_t base;
|
||||
|
||||
if ((config == 0) || (frame == 0)) {
|
||||
if ((config == 0) || (frame == 0) || (length > CAN_SENSOR_MAX_DATA)
|
||||
|| ((length != 0U) && (data == 0))) {
|
||||
return 0U;
|
||||
}
|
||||
frame->id = config->tx_id;
|
||||
frame->extended = config->extended;
|
||||
frame->length = CAN_SENSOR_PREAMBLE_SIZE;
|
||||
frame->data[0] = command;
|
||||
/* Позиция занимает два байта в порядке от старшего к младшему. */
|
||||
frame->data[1] = (uint8_t)((position >> 8U) & 0xFFU);
|
||||
frame->data[2] = (uint8_t)(position & 0xFFU);
|
||||
for (index = CAN_SENSOR_PREAMBLE_SIZE; index < CAN_SENSOR_MAX_DATA; index++) {
|
||||
frame->data[index] = 0U;
|
||||
base = (response != 0U) ? config->tx_id : config->rx_id;
|
||||
frame->id = (base & CAN_SENSOR_HEADER_MASK)
|
||||
| ((uint32_t)assembly_serial << 8U) | (uint32_t)position;
|
||||
frame->extended = 1U;
|
||||
frame->length = length;
|
||||
for (index = 0U; index < CAN_SENSOR_MAX_DATA; index++) {
|
||||
frame->data[index] = (index < length) ? data[index] : 0U;
|
||||
}
|
||||
return 1U;
|
||||
}
|
||||
|
||||
uint8_t CanSensor_BuildIdFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
const uint8_t *id)
|
||||
{
|
||||
uint8_t index;
|
||||
|
||||
if ((config == 0) || (frame == 0)) {
|
||||
return 0U;
|
||||
}
|
||||
frame->id = can_sensor_tx_data_id(config);
|
||||
frame->extended = config->extended;
|
||||
frame->length = CAN_SENSOR_ID_SIZE;
|
||||
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||
frame->data[index] = (id != 0) ? id[index] : 0U;
|
||||
}
|
||||
return 1U;
|
||||
}
|
||||
|
||||
uint8_t CanSensor_Send(CanSensor *link, uint8_t command, uint16_t position,
|
||||
const uint8_t *id)
|
||||
uint8_t CanSensor_SendId(CanSensor *link, uint8_t position,
|
||||
uint8_t assembly_serial, const uint8_t *id)
|
||||
{
|
||||
CanSensor_Frame frame;
|
||||
|
||||
if ((link == 0) || (link->io.send == 0)) {
|
||||
if ((link == 0) || (id == 0) || (link->io.send == 0)) {
|
||||
return 0U;
|
||||
}
|
||||
(void)CanSensor_BuildPreamble(&link->config, &frame, command, position);
|
||||
if (can_sensor_send_frame(link, &frame) == 0U) {
|
||||
if (CanSensor_BuildFrame(&link->config, &frame, 1U, position,
|
||||
assembly_serial, id, CAN_SENSOR_ID_SIZE) == 0U) {
|
||||
link->send_errors++;
|
||||
return 0U;
|
||||
}
|
||||
(void)CanSensor_BuildIdFrame(&link->config, &frame, id);
|
||||
if (can_sensor_send_frame(link, &frame) == 0U) {
|
||||
/* Одиночная преамбула приёмником отбрасывается, позиция не изменится. */
|
||||
link->send_errors++;
|
||||
return 0U;
|
||||
}
|
||||
link->sent_messages++;
|
||||
return 1U;
|
||||
return can_sensor_send_frame(link, &frame);
|
||||
}
|
||||
|
||||
uint8_t CanSensor_SendId(CanSensor *link, uint16_t position, const uint8_t *id)
|
||||
uint8_t CanSensor_SendError(CanSensor *link, uint8_t position,
|
||||
uint8_t assembly_serial, uint8_t result)
|
||||
{
|
||||
if (id == 0) {
|
||||
CanSensor_Frame frame;
|
||||
|
||||
if ((link == 0) || (link->io.send == 0) || (result == CAN_SENSOR_RESULT_OK)) {
|
||||
return 0U;
|
||||
}
|
||||
return CanSensor_Send(link, CAN_SENSOR_CMD_WRITE_POSITION, position, id);
|
||||
if (CanSensor_BuildFrame(&link->config, &frame, 1U, position,
|
||||
assembly_serial, &result, 1U) == 0U) {
|
||||
link->send_errors++;
|
||||
return 0U;
|
||||
}
|
||||
return can_sensor_send_frame(link, &frame);
|
||||
}
|
||||
|
||||
uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame,
|
||||
CanSensor_Message *out)
|
||||
{
|
||||
CanSensor_Message message;
|
||||
uint32_t header;
|
||||
uint8_t index;
|
||||
|
||||
if ((link == 0) || (frame == 0)) {
|
||||
return 0U;
|
||||
}
|
||||
if (frame->extended == 0U) {
|
||||
return 0U;
|
||||
}
|
||||
header = frame->id & CAN_SENSOR_HEADER_MASK;
|
||||
if (header == (link->config.rx_id & CAN_SENSOR_HEADER_MASK)) {
|
||||
message.request = 1U;
|
||||
} else if (header == (link->config.tx_id & CAN_SENSOR_HEADER_MASK)) {
|
||||
message.request = 0U;
|
||||
} else {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
if ((frame->id == can_sensor_rx_id(&link->config))
|
||||
&& (frame->extended == link->config.extended)) {
|
||||
if (frame->length < CAN_SENSOR_PREAMBLE_SIZE) {
|
||||
link->dropped_frames++;
|
||||
return 0U;
|
||||
message.assembly_serial = (uint8_t)((frame->id >> 8U) & 0xFFU);
|
||||
message.position = (uint8_t)(frame->id & 0xFFU);
|
||||
message.result = CAN_SENSOR_RESULT_OK;
|
||||
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||
message.id[index] = (index < frame->length) ? frame->data[index] : 0U;
|
||||
}
|
||||
|
||||
if (message.request != 0U) {
|
||||
if (frame->length == 0U) {
|
||||
message.operation = CAN_SENSOR_OPERATION_GET;
|
||||
} else if (frame->length == CAN_SENSOR_ID_SIZE) {
|
||||
message.operation = (can_sensor_rom_is_zero(message.id) != 0U)
|
||||
? CAN_SENSOR_OPERATION_CLEAR
|
||||
: CAN_SENSOR_OPERATION_WRITE;
|
||||
} else {
|
||||
message.operation = CAN_SENSOR_OPERATION_INVALID;
|
||||
message.result = CAN_SENSOR_RESULT_INVALID_DLC;
|
||||
}
|
||||
link->rx.command = frame->data[0];
|
||||
link->rx.position = (uint16_t)(((uint16_t)frame->data[1] << 8U)
|
||||
| frame->data[2]);
|
||||
link->rx_preamble = 1U;
|
||||
return 0U;
|
||||
}
|
||||
|
||||
if ((frame->id != can_sensor_rx_data_id(&link->config))
|
||||
|| (frame->extended != link->config.extended)) {
|
||||
return 0U;
|
||||
}
|
||||
if ((link->rx_preamble == 0U) || (frame->length < CAN_SENSOR_ID_SIZE)) {
|
||||
/* Идентификатор без преамбулы не говорит, в какую позицию его писать. */
|
||||
link->rx_preamble = 0U;
|
||||
} else if (frame->length == CAN_SENSOR_ID_SIZE) {
|
||||
message.operation = CAN_SENSOR_OPERATION_RESPONSE;
|
||||
} else if (frame->length == 1U) {
|
||||
message.operation = CAN_SENSOR_OPERATION_ERROR;
|
||||
message.result = frame->data[0];
|
||||
} else {
|
||||
link->dropped_frames++;
|
||||
return 0U;
|
||||
}
|
||||
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||
link->rx.id[index] = frame->data[index];
|
||||
}
|
||||
link->rx_preamble = 0U;
|
||||
|
||||
link->received_messages++;
|
||||
if (out != 0) {
|
||||
*out = link->rx;
|
||||
*out = message;
|
||||
}
|
||||
return 1U;
|
||||
}
|
||||
@@ -244,3 +211,51 @@ uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out)
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
void CanSensor_SetPulsePeriod(CanSensor *link, uint32_t period_ms)
|
||||
{
|
||||
if (link == 0) {
|
||||
return;
|
||||
}
|
||||
if (link->config.pulse_period_ms != period_ms) {
|
||||
link->config.pulse_period_ms = period_ms;
|
||||
/* После включения или смены периода отсчитываем полный новый период. */
|
||||
link->pulse_armed = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t CanSensor_PulsePeriod(const CanSensor *link)
|
||||
{
|
||||
return (link != 0) ? link->config.pulse_period_ms : 0U;
|
||||
}
|
||||
|
||||
void CanSensor_Task(CanSensor *link, uint32_t now_ms)
|
||||
{
|
||||
CanSensor_Frame frame;
|
||||
const uint32_t period_ms = (link != 0) ? link->config.pulse_period_ms : 0U;
|
||||
|
||||
if ((link == 0) || (link->io.send == 0) || (period_ms == 0U)) {
|
||||
return;
|
||||
}
|
||||
if (link->pulse_armed == 0U) {
|
||||
link->pulse_last_ms = now_ms;
|
||||
link->pulse_armed = 1U;
|
||||
return;
|
||||
}
|
||||
if ((now_ms - link->pulse_last_ms) < period_ms) {
|
||||
return;
|
||||
}
|
||||
|
||||
frame.id = (link->config.tx_id & CAN_SENSOR_ADDRESS_MASK)
|
||||
| ((uint32_t)CAN_SENSOR_MSGTYPE_PULSE << 16U);
|
||||
frame.extended = 1U;
|
||||
frame.length = 1U;
|
||||
link->pulse_counter++;
|
||||
frame.data[0] = link->pulse_counter;
|
||||
if (can_sensor_send_frame(link, &frame) != 0U) {
|
||||
link->pulse_last_ms = now_ms;
|
||||
} else {
|
||||
/* Счётчик меняется только для реально поставленных в очередь кадров. */
|
||||
link->pulse_counter--;
|
||||
}
|
||||
}
|
||||
|
||||
+100
-165
@@ -1,30 +1,25 @@
|
||||
/*
|
||||
* Обмен настройками датчиков DS18B20 через однокадровые сообщения SETCAN. ROM идентифицирует
|
||||
* датчик независимо от его позиции; прикладное состояние и отправка CAN предоставляются через
|
||||
* интерфейс библиотеки.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file can_sensor.h
|
||||
* @brief Портируемая передача идентификаторов датчиков по шине CAN.
|
||||
* @brief Портируемый транспорт SETTINGS для привязки DS18B20 к локации.
|
||||
*
|
||||
* Библиотека не привязана к микроконтроллеру: она не включает заголовки
|
||||
* периферии, не обращается к регистрам и не пользуется прерываниями. Обмен
|
||||
* идёт через таблицу обратных вызовов CanSensor_Io, которую заполняет порт
|
||||
* платы (для этой сборки — @c src/can.c поверх bxCAN STM32F103C8T6).
|
||||
*
|
||||
* Сообщение состоит из двух кадров классического CAN, потому что 64-битный
|
||||
* идентификатор датчика и преамбула вместе не помещаются в восемь байтов
|
||||
* поля данных:
|
||||
* Один Extended CAN-кадр содержит полную операцию. Поля локации находятся в
|
||||
* Body идентификатора SETCAN, а восьмибайтовый ROM — в payload:
|
||||
*
|
||||
* @code
|
||||
* кадр 1 — преамбула, DLC = 2 +---------+----------+
|
||||
* | команда | позиция |
|
||||
* +---------+----------+
|
||||
* кадр 2 — идентификатор, DLC=8 +----------------------------+
|
||||
* | ROM датчика, байты 0..7 |
|
||||
* +----------------------------+
|
||||
* Priority=1, DeviceType=0x7, DeviceID=0xF, MsgType=0xE (SETTINGS)
|
||||
* Body[15:8] = номер сборки, Body[7:0] = позиция
|
||||
* @endcode
|
||||
*
|
||||
* Преамбула несёт команду записи датчика в позицию: первый байт — код
|
||||
* команды (@ref CAN_SENSOR_CMD_WRITE_POSITION), второй — номер позиции в
|
||||
* таблице узла-приёмника. Кадр идентификатора передаётся сразу за преамбулой
|
||||
* и без неё считается недействительным, поэтому приёмник не запишет ROM в
|
||||
* позицию, которая ему не была назначена.
|
||||
* Запрос ПМ использует Route=0 (0x17FEZZYY), ответ прибора — Route=1
|
||||
* (0x1FFEZZYY). DLC=0 означает GET, DLC=8 с ненулевым ROM — SET/REPLACE,
|
||||
* DLC=8 с нулевым ROM — CLEAR. Успешный ответ возвращает текущий ROM с DLC=8,
|
||||
* ошибка — однобайтовый код с DLC=1.
|
||||
*/
|
||||
|
||||
#ifndef CAN_SENSOR_H
|
||||
@@ -32,186 +27,126 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** Длина идентификатора датчика (ROM 1-Wire) в байтах. */
|
||||
#define CAN_SENSOR_ID_SIZE 8U
|
||||
/** Длина ROM 1-Wire и максимального payload classic CAN. */
|
||||
#define CAN_SENSOR_ID_SIZE 8U
|
||||
#define CAN_SENSOR_MAX_DATA 8U
|
||||
|
||||
/** Длина преамбулы: код команды (1 байт) и номер позиции (2 байта, u16 BE). */
|
||||
#define CAN_SENSOR_PREAMBLE_SIZE 3U
|
||||
/** Назначенный проекту тип сообщения SETCAN SETTINGS. */
|
||||
#define CAN_SENSOR_MSGTYPE_SETTINGS 0xEU
|
||||
#define CAN_SENSOR_MSGTYPE_PULSE 0xFU
|
||||
|
||||
/** Предельная длина поля данных классического кадра CAN. */
|
||||
#define CAN_SENSOR_MAX_DATA 8U
|
||||
/** Маска полей SETCAN от Priority до MsgType; Body под маской не находится. */
|
||||
#define CAN_SENSOR_HEADER_MASK 0x1FFF0000UL
|
||||
#define CAN_SENSOR_ADDRESS_MASK 0x1FF00000UL
|
||||
#define CAN_SENSOR_BODY_MASK 0x0000FFFFUL
|
||||
|
||||
/** Код команды «записать идентификатор датчика в позицию». */
|
||||
#define CAN_SENSOR_CMD_WRITE_POSITION 0xA1U
|
||||
/** Базовые ID SETTINGS для выбранных DeviceType=0x7 и DeviceID=0xF. */
|
||||
#define CAN_SENSOR_DEFAULT_TX_ID 0x1FFE0000UL
|
||||
#define CAN_SENSOR_DEFAULT_RX_ID 0x17FE0000UL
|
||||
|
||||
/** Код команды «очистить позицию»; кадр идентификатора передаётся нулевым. */
|
||||
#define CAN_SENSOR_CMD_CLEAR_POSITION 0xA2U
|
||||
/** Число попыток помещения кадра в почтовый ящик. */
|
||||
#define CAN_SENSOR_DEFAULT_RETRIES 3U
|
||||
|
||||
/** Смещение идентификатора кадра данных относительно кадра преамбулы. */
|
||||
#define CAN_SENSOR_DATA_ID_OFFSET 1U
|
||||
/** Операции, полученные из направления, DLC и payload кадра. */
|
||||
#define CAN_SENSOR_OPERATION_GET 0U
|
||||
#define CAN_SENSOR_OPERATION_WRITE 1U
|
||||
#define CAN_SENSOR_OPERATION_CLEAR 2U
|
||||
#define CAN_SENSOR_OPERATION_RESPONSE 3U
|
||||
#define CAN_SENSOR_OPERATION_ERROR 4U
|
||||
#define CAN_SENSOR_OPERATION_INVALID 5U
|
||||
|
||||
/** Число попыток передачи одного кадра по умолчанию. */
|
||||
#define CAN_SENSOR_DEFAULT_RETRIES 3U
|
||||
/** Коды результата SETTINGS, передаваемые ответом с DLC=1. */
|
||||
#define CAN_SENSOR_RESULT_OK 0x00U
|
||||
#define CAN_SENSOR_RESULT_INVALID_DLC 0x01U
|
||||
#define CAN_SENSOR_RESULT_INVALID_ROM_CRC 0x02U
|
||||
#define CAN_SENSOR_RESULT_NOT_FOUND 0x03U
|
||||
#define CAN_SENSOR_RESULT_ALREADY_ASSIGNED 0x04U
|
||||
#define CAN_SENSOR_RESULT_EEPROM_ERROR 0x05U
|
||||
#define CAN_SENSOR_RESULT_INVALID_LOCATION 0x06U
|
||||
#define CAN_SENSOR_RESULT_BUSY 0x07U
|
||||
|
||||
/** Идентификатор кадра преамбулы по умолчанию (стандартный, 11 бит). */
|
||||
#define CAN_SENSOR_DEFAULT_TX_ID 0x200U
|
||||
|
||||
/**
|
||||
* @brief Кадр шины CAN в форме, не зависящей от контроллера.
|
||||
*/
|
||||
/** Кадр CAN в форме, не зависящей от периферийного контроллера. */
|
||||
typedef struct {
|
||||
uint32_t id; /**< Идентификатор кадра, 11 или 29 бит. */
|
||||
uint8_t extended; /**< 1 — расширенный идентификатор. */
|
||||
uint8_t length; /**< Число значащих байтов поля данных. */
|
||||
uint8_t data[CAN_SENSOR_MAX_DATA]; /**< Поле данных кадра. */
|
||||
uint32_t id; /**< Расширенный идентификатор, 29 бит. */
|
||||
uint8_t extended; /**< Для SETTINGS всегда 1. */
|
||||
uint8_t length; /**< DLC, 0..8. */
|
||||
uint8_t data[CAN_SENSOR_MAX_DATA]; /**< Payload. */
|
||||
} CanSensor_Frame;
|
||||
|
||||
/**
|
||||
* @brief Принятое или собранное сообщение: преамбула и идентификатор датчика.
|
||||
*/
|
||||
/** Разобранная команда или ответ SETTINGS. */
|
||||
typedef struct {
|
||||
uint8_t command; /**< Код команды из преамбулы. */
|
||||
uint16_t position; /**< Позиция записи из преамбулы. */
|
||||
uint8_t id[CAN_SENSOR_ID_SIZE]; /**< Идентификатор датчика. */
|
||||
uint8_t operation; /**< CAN_SENSOR_OPERATION_*. */
|
||||
uint8_t request; /**< 1 — запрос Route=0, 0 — ответ Route=1. */
|
||||
uint8_t assembly_serial; /**< Body[15:8], номер сборки Z. */
|
||||
uint8_t position; /**< Body[7:0], позиция Y. */
|
||||
uint8_t result; /**< Код ошибки для ERROR/INVALID. */
|
||||
uint8_t id[CAN_SENSOR_ID_SIZE]; /**< ROM для WRITE/RESPONSE, нули для CLEAR. */
|
||||
} CanSensor_Message;
|
||||
|
||||
/**
|
||||
* @brief Доступ библиотеки к контроллеру CAN.
|
||||
*
|
||||
* Обязателен только @c send; при нулевом @c receive функция CanSensor_Poll()
|
||||
* ничего не делает и узел работает только на передачу.
|
||||
*/
|
||||
/** Доступ переносимой части к CAN-контроллеру. */
|
||||
typedef struct {
|
||||
/** Ставит кадр в очередь передачи; 1 — кадр принят контроллером. */
|
||||
uint8_t (*send)(void *context, const CanSensor_Frame *frame);
|
||||
/** Забирает принятый кадр; 1 — кадр получен, 0 — очередь пуста. */
|
||||
uint8_t (*receive)(void *context, CanSensor_Frame *frame);
|
||||
void *context; /**< Контекст порта, передаётся вызовам без изменений. */
|
||||
void *context;
|
||||
} CanSensor_Io;
|
||||
|
||||
/**
|
||||
* @brief Идентификаторы кадров и режим передачи.
|
||||
*
|
||||
* Нулевые поля заменяются значениями по умолчанию вызовом
|
||||
* CanSensor_ConfigDefault() или самой CanSensor_Init().
|
||||
*/
|
||||
/** Базовые идентификаторы SETTINGS и режим транспорта. */
|
||||
typedef struct {
|
||||
uint32_t tx_id; /**< Идентификатор кадра преамбулы при передаче. */
|
||||
uint32_t tx_data_id; /**< Идентификатор кадра ROM; 0 — @c tx_id + 1. */
|
||||
uint32_t rx_id; /**< Ожидаемая преамбула при приёме; 0 — @c tx_id. */
|
||||
uint32_t rx_data_id; /**< Ожидаемый кадр ROM; 0 — @c rx_id + 1. */
|
||||
uint8_t extended; /**< 1 — расширенные идентификаторы 29 бит. */
|
||||
uint8_t retries; /**< Попыток передачи кадра; 0 — значение по умолчанию. */
|
||||
uint32_t tx_id; /**< База ответа 0x1FFE0000, Body должен быть нулевым. */
|
||||
uint32_t rx_id; /**< База запроса 0x17FE0000, Body должен быть нулевым. */
|
||||
uint8_t extended; /**< 1 — Extended CAN ID. */
|
||||
uint8_t retries; /**< Число попыток передачи. */
|
||||
uint32_t pulse_period_ms; /**< Период PULSE; 0 полностью отключает PULSE. */
|
||||
} CanSensor_Config;
|
||||
|
||||
/**
|
||||
* @brief Состояние узла: настройки, счётчики и сборка принимаемого сообщения.
|
||||
*/
|
||||
/** Состояние транспорта и диагностические счётчики. */
|
||||
typedef struct {
|
||||
CanSensor_Io io; /**< Обратные вызовы порта. */
|
||||
CanSensor_Config config; /**< Идентификаторы кадров и режим передачи. */
|
||||
CanSensor_Message rx; /**< Собираемое сообщение приёма. */
|
||||
uint8_t rx_preamble; /**< 1 — преамбула принята, ожидается кадр ROM. */
|
||||
uint32_t sent_messages; /**< Полностью переданные сообщения. */
|
||||
uint32_t send_errors; /**< Сообщения, не ушедшие в шину. */
|
||||
uint32_t received_messages; /**< Полностью принятые сообщения. */
|
||||
uint32_t dropped_frames; /**< Кадры без преамбулы или с неверной длиной. */
|
||||
CanSensor_Io io;
|
||||
CanSensor_Config config;
|
||||
uint32_t sent_messages;
|
||||
uint32_t send_errors;
|
||||
uint32_t received_messages;
|
||||
uint32_t dropped_frames;
|
||||
uint32_t pulse_last_ms;
|
||||
uint8_t pulse_counter;
|
||||
uint8_t pulse_armed;
|
||||
} CanSensor;
|
||||
|
||||
/**
|
||||
* @brief Заполняет настройки значениями по умолчанию.
|
||||
*
|
||||
* Идентификатор преамбулы — @ref CAN_SENSOR_DEFAULT_TX_ID, кадр ROM идёт
|
||||
* следующим идентификатором, приём настроен на те же значения.
|
||||
*
|
||||
* @param config Настройки, принадлежащие вызывающему коду.
|
||||
*/
|
||||
void CanSensor_ConfigDefault(CanSensor_Config *config);
|
||||
|
||||
/**
|
||||
* @brief Готовит узел к работе.
|
||||
*
|
||||
* @param link Состояние узла, принадлежащее вызывающему коду.
|
||||
* @param io Обратные вызовы порта; копируются внутрь состояния.
|
||||
* @param config Настройки либо 0 для значений по умолчанию.
|
||||
* @return 1 при успешной настройке, 0 при неполных аргументах.
|
||||
*/
|
||||
uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
||||
const CanSensor_Config *config);
|
||||
|
||||
/**
|
||||
* @brief Собирает кадр преамбулы.
|
||||
*
|
||||
* @param config Настройки узла.
|
||||
* @param frame Кадр приёмника.
|
||||
* @param command Код команды, например @ref CAN_SENSOR_CMD_WRITE_POSITION.
|
||||
* @param position Номер позиции записи датчика.
|
||||
* @return 1 при успешной сборке, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_BuildPreamble(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
uint8_t command, uint16_t position);
|
||||
/** Собирает SETTINGS-кадр на базе tx_id или rx_id. */
|
||||
uint8_t CanSensor_BuildFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
uint8_t response, uint8_t position,
|
||||
uint8_t assembly_serial, const uint8_t *data,
|
||||
uint8_t length);
|
||||
|
||||
/**
|
||||
* @brief Собирает кадр идентификатора датчика.
|
||||
*
|
||||
* @param config Настройки узла.
|
||||
* @param frame Кадр приёмника.
|
||||
* @param id Идентификатор датчика длиной @ref CAN_SENSOR_ID_SIZE либо 0 для
|
||||
* нулевого кадра команды очистки.
|
||||
* @return 1 при успешной сборке, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_BuildIdFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
const uint8_t *id);
|
||||
/** Передаёт успешный ответ с текущим ROM (DLC=8). */
|
||||
uint8_t CanSensor_SendId(CanSensor *link, uint8_t position,
|
||||
uint8_t assembly_serial, const uint8_t *id);
|
||||
|
||||
/**
|
||||
* @brief Передаёт преамбулу и идентификатор датчика.
|
||||
*
|
||||
* Кадры уходят подряд; при отказе контроллера передача повторяется
|
||||
* @c retries раз. Если преамбула ушла, а кадр ROM — нет, сообщение считается
|
||||
* несостоявшимся: приёмник отбросит одиночную преамбулу.
|
||||
*
|
||||
* @param link Состояние узла.
|
||||
* @param command Код команды преамбулы.
|
||||
* @param position Номер позиции записи датчика.
|
||||
* @param id Идентификатор датчика либо 0 для команды очистки позиции.
|
||||
* @return 1, если оба кадра приняты контроллером, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_Send(CanSensor *link, uint8_t command, uint16_t position,
|
||||
const uint8_t *id);
|
||||
/** Передаёт ошибку SETTINGS (DLC=1). */
|
||||
uint8_t CanSensor_SendError(CanSensor *link, uint8_t position,
|
||||
uint8_t assembly_serial, uint8_t result);
|
||||
|
||||
/**
|
||||
* @brief Передаёт команду записи датчика в позицию.
|
||||
*
|
||||
* Краткая форма CanSensor_Send() с кодом @ref CAN_SENSOR_CMD_WRITE_POSITION.
|
||||
*
|
||||
* @param link Состояние узла.
|
||||
* @param position Номер позиции записи датчика.
|
||||
* @param id Идентификатор датчика длиной @ref CAN_SENSOR_ID_SIZE.
|
||||
* @return 1, если оба кадра приняты контроллером, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_SendId(CanSensor *link, uint16_t position, const uint8_t *id);
|
||||
|
||||
/**
|
||||
* @brief Разбирает принятый кадр и собирает из пары кадров сообщение.
|
||||
*
|
||||
* Кадр идентификатора без предшествующей преамбулы отбрасывается, а новая
|
||||
* преамбула заменяет незавершённую: сборка не требует таймера.
|
||||
*
|
||||
* @param link Состояние узла.
|
||||
* @param frame Принятый кадр.
|
||||
* @param out Приёмник готового сообщения либо 0.
|
||||
* @return 1, если сообщение собрано полностью, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame,
|
||||
CanSensor_Message *out);
|
||||
|
||||
/**
|
||||
* @brief Забирает кадры у порта и возвращает первое собранное сообщение.
|
||||
*
|
||||
* @param link Состояние узла.
|
||||
* @param out Приёмник сообщения либо 0.
|
||||
* @return 1, если сообщение собрано, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out);
|
||||
|
||||
/**
|
||||
* @brief Меняет период PULSE во время работы.
|
||||
*
|
||||
* @param period_ms Период в миллисекундах; 0 выключает PULSE.
|
||||
*/
|
||||
void CanSensor_SetPulsePeriod(CanSensor *link, uint32_t period_ms);
|
||||
|
||||
/** Возвращает действующий период PULSE; 0 означает «выключено». */
|
||||
uint32_t CanSensor_PulsePeriod(const CanSensor *link);
|
||||
|
||||
/** Вызывает планировщик PULSE из главного цикла. */
|
||||
void CanSensor_Task(CanSensor *link, uint32_t now_ms);
|
||||
|
||||
#endif /* CAN_SENSOR_H */
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Прямой bxCAN-порт датчиков для STM32F1 без HAL. Регистры контроллера обеспечивают передачу и
|
||||
* приём SETCAN, а конфигурация платы задаёт подключение шины; это аппаратная реализация
|
||||
* интерфейса can_sensor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file can_sensor_stm32f1.c
|
||||
* @brief Опросный порт can-sensor на bxCAN STM32F1 без STM32 HAL.
|
||||
*/
|
||||
|
||||
#include "can_sensor_stm32f1.h"
|
||||
|
||||
#include "can_sensor_stm32f1_config.h"
|
||||
|
||||
#if !defined(CAN_SENSOR_STM32F1_GPIO) \
|
||||
|| !defined(CAN_SENSOR_STM32F1_GPIO_CLOCK) \
|
||||
|| !defined(CAN_SENSOR_STM32F1_RX_PIN) \
|
||||
|| !defined(CAN_SENSOR_STM32F1_TX_PIN)
|
||||
#error "can_sensor_stm32f1_config.h должен задать GPIO, GPIO_CLOCK, RX_PIN и TX_PIN"
|
||||
#endif
|
||||
|
||||
#define CAN_STM32F1_TQ_MAX 18U
|
||||
#define CAN_STM32F1_TQ_MIN 8U
|
||||
#define CAN_STM32F1_TIMEOUT_CYCLES 1000000U
|
||||
#define CAN_STM32F1_TSR_ALL_EMPTY (CAN_TSR_TME0 | CAN_TSR_TME1 | CAN_TSR_TME2)
|
||||
#define CAN_STM32F1_TSR_ALL_ABORT (CAN_TSR_ABRQ0 | CAN_TSR_ABRQ1 | CAN_TSR_ABRQ2)
|
||||
#define CAN_STM32F1_TSR_ANY_TXOK (CAN_TSR_TXOK0 | CAN_TSR_TXOK1 | CAN_TSR_TXOK2)
|
||||
#define CAN_STM32F1_TSR_DONE_FLAGS \
|
||||
(CAN_TSR_RQCP0 | CAN_TSR_TXOK0 | CAN_TSR_ALST0 | CAN_TSR_TERR0 \
|
||||
| CAN_TSR_RQCP1 | CAN_TSR_TXOK1 | CAN_TSR_ALST1 | CAN_TSR_TERR1 \
|
||||
| CAN_TSR_RQCP2 | CAN_TSR_TXOK2 | CAN_TSR_ALST2 | CAN_TSR_TERR2)
|
||||
|
||||
static uint32_t can_stm32f1_pclk1(void)
|
||||
{
|
||||
static const uint8_t shift[8] = { 0U, 0U, 0U, 0U, 1U, 2U, 3U, 4U };
|
||||
const uint32_t bits = (RCC->CFGR & RCC_CFGR_PPRE1) >> 8U;
|
||||
|
||||
return SystemCoreClock >> shift[bits & 0x7U];
|
||||
}
|
||||
|
||||
static void can_stm32f1_configure_pin(GPIO_TypeDef *gpio, uint8_t pin,
|
||||
uint32_t mode)
|
||||
{
|
||||
uint32_t config;
|
||||
uint32_t shift;
|
||||
|
||||
if (pin < 8U) {
|
||||
shift = (uint32_t)pin * 4U;
|
||||
config = gpio->CRL;
|
||||
config = (config & ~(0xFUL << shift)) | (mode << shift);
|
||||
gpio->CRL = config;
|
||||
} else {
|
||||
shift = ((uint32_t)pin - 8U) * 4U;
|
||||
config = gpio->CRH;
|
||||
config = (config & ~(0xFUL << shift)) | (mode << shift);
|
||||
gpio->CRH = config;
|
||||
}
|
||||
}
|
||||
|
||||
static void can_stm32f1_configure_pins(void)
|
||||
{
|
||||
RCC->APB2ENR |= CAN_SENSOR_STM32F1_GPIO_CLOCK | RCC_APB2ENR_AFIOEN;
|
||||
/* RX: input pull-up; TX: alternate function push-pull, 50 MHz. */
|
||||
can_stm32f1_configure_pin(CAN_SENSOR_STM32F1_GPIO,
|
||||
CAN_SENSOR_STM32F1_RX_PIN, 0x8UL);
|
||||
can_stm32f1_configure_pin(CAN_SENSOR_STM32F1_GPIO,
|
||||
CAN_SENSOR_STM32F1_TX_PIN, 0xBUL);
|
||||
CAN_SENSOR_STM32F1_GPIO->BSRR =
|
||||
(uint32_t)(1UL << CAN_SENSOR_STM32F1_RX_PIN);
|
||||
}
|
||||
|
||||
static uint8_t can_stm32f1_calc_timing(uint32_t bitrate, uint32_t *btr,
|
||||
CanSensorStm32F1_Timing *timing)
|
||||
{
|
||||
const uint32_t pclk = can_stm32f1_pclk1();
|
||||
uint32_t total;
|
||||
uint32_t tq;
|
||||
|
||||
if ((bitrate == 0U) || (btr == 0) || (pclk == 0U)
|
||||
|| ((pclk % bitrate) != 0U)) {
|
||||
return 0U;
|
||||
}
|
||||
total = pclk / bitrate;
|
||||
for (tq = CAN_STM32F1_TQ_MAX; tq >= CAN_STM32F1_TQ_MIN; tq--) {
|
||||
const uint32_t prescaler = total / tq;
|
||||
|
||||
if (((total % tq) == 0U) && (prescaler >= 1U)
|
||||
&& (prescaler <= 1024U)) {
|
||||
const uint32_t ts2 = tq / 5U;
|
||||
const uint32_t ts1 = tq - 1U - ts2;
|
||||
|
||||
*btr = ((ts2 - 1U) << 20U) | ((ts1 - 1U) << 16U)
|
||||
| (prescaler - 1U);
|
||||
if (timing != 0) {
|
||||
timing->pclk_hz = pclk;
|
||||
timing->prescaler = (uint16_t)prescaler;
|
||||
timing->ts1 = (uint8_t)ts1;
|
||||
timing->ts2 = (uint8_t)ts2;
|
||||
timing->bitrate = pclk / (prescaler * tq);
|
||||
timing->sample_point =
|
||||
(uint8_t)(((1U + ts1) * 100U) / tq);
|
||||
}
|
||||
return 1U;
|
||||
}
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
uint8_t CanSensorStm32F1_CalcTiming(uint32_t bitrate,
|
||||
CanSensorStm32F1_Timing *timing)
|
||||
{
|
||||
uint32_t btr;
|
||||
|
||||
return can_stm32f1_calc_timing(bitrate, &btr, timing);
|
||||
}
|
||||
|
||||
static void can_stm32f1_configure_filter(uint32_t id, uint32_t mask)
|
||||
{
|
||||
const uint32_t filter_id = ((id & mask) << 3U) | CAN_RI0R_IDE;
|
||||
const uint32_t filter_mask =
|
||||
(mask << 3U) | CAN_RI0R_IDE | CAN_RI0R_RTR;
|
||||
|
||||
CAN1->FMR |= CAN_FMR_FINIT;
|
||||
CAN1->FA1R &= ~1UL;
|
||||
CAN1->FS1R |= 1UL;
|
||||
CAN1->FM1R &= ~1UL;
|
||||
CAN1->sFilterRegister[0].FR1 = filter_id;
|
||||
CAN1->sFilterRegister[0].FR2 = filter_mask;
|
||||
CAN1->FFA1R &= ~1UL;
|
||||
CAN1->FA1R |= 1UL;
|
||||
CAN1->FMR &= ~CAN_FMR_FINIT;
|
||||
}
|
||||
|
||||
static void can_stm32f1_load_mailbox(uint32_t mailbox,
|
||||
const CanSensor_Frame *frame)
|
||||
{
|
||||
uint32_t identifier;
|
||||
uint8_t index;
|
||||
|
||||
if (frame->extended != 0U) {
|
||||
identifier = ((frame->id & 0x1FFFFFFFUL) << 3U) | CAN_TI0R_IDE;
|
||||
} else {
|
||||
identifier = (frame->id & 0x7FFUL) << 21U;
|
||||
}
|
||||
CAN1->sTxMailBox[mailbox].TDTR = (uint32_t)(frame->length & 0x0FU);
|
||||
CAN1->sTxMailBox[mailbox].TDLR = 0U;
|
||||
CAN1->sTxMailBox[mailbox].TDHR = 0U;
|
||||
for (index = 0U; index < frame->length; index++) {
|
||||
if (index < 4U) {
|
||||
CAN1->sTxMailBox[mailbox].TDLR |=
|
||||
(uint32_t)frame->data[index] << (index * 8U);
|
||||
} else {
|
||||
CAN1->sTxMailBox[mailbox].TDHR |=
|
||||
(uint32_t)frame->data[index] << ((index - 4U) * 8U);
|
||||
}
|
||||
}
|
||||
CAN1->sTxMailBox[mailbox].TIR = identifier | CAN_TI0R_TXRQ;
|
||||
}
|
||||
|
||||
uint8_t CanSensorStm32F1_Start(CanSensorStm32F1_Port *port,
|
||||
uint32_t bitrate, uint32_t filter_id,
|
||||
uint32_t filter_mask, uint8_t loopback)
|
||||
{
|
||||
uint32_t btr;
|
||||
uint32_t guard;
|
||||
uint32_t mode_bits;
|
||||
|
||||
if ((port == 0) || (filter_id > 0x1FFFFFFFUL)
|
||||
|| (filter_mask > 0x1FFFFFFFUL)
|
||||
|| (can_stm32f1_calc_timing(bitrate, &btr, &port->timing) == 0U)) {
|
||||
return 0U;
|
||||
}
|
||||
CanSensorStm32F1_Stop(port);
|
||||
can_stm32f1_configure_pins();
|
||||
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
|
||||
|
||||
CAN1->MCR &= ~CAN_MCR_SLEEP;
|
||||
CAN1->MCR |= CAN_MCR_INRQ;
|
||||
for (guard = 0U; guard < CAN_STM32F1_TIMEOUT_CYCLES; guard++) {
|
||||
if ((CAN1->MSR & CAN_MSR_INAK) != 0U) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((CAN1->MSR & CAN_MSR_INAK) == 0U) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
mode_bits = (loopback != 0U) ? (CAN_BTR_LBKM | CAN_BTR_SILM) : 0U;
|
||||
CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_ABOM | CAN_MCR_TXFP;
|
||||
CAN1->BTR = btr | mode_bits;
|
||||
can_stm32f1_configure_filter(filter_id, filter_mask);
|
||||
CAN1->MCR &= ~CAN_MCR_INRQ;
|
||||
for (guard = 0U; guard < CAN_STM32F1_TIMEOUT_CYCLES; guard++) {
|
||||
if ((CAN1->MSR & CAN_MSR_INAK) == 0U) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((CAN1->MSR & CAN_MSR_INAK) != 0U) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
port->bitrate = bitrate;
|
||||
port->loopback = (uint8_t)(loopback != 0U);
|
||||
port->no_transceiver = 0U;
|
||||
port->tx_waiting = 0U;
|
||||
port->ready = 1U;
|
||||
return 1U;
|
||||
}
|
||||
|
||||
void CanSensorStm32F1_Stop(CanSensorStm32F1_Port *port)
|
||||
{
|
||||
if (port == 0) {
|
||||
return;
|
||||
}
|
||||
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
|
||||
RCC->APB1RSTR |= RCC_APB1RSTR_CAN1RST;
|
||||
RCC->APB1RSTR &= ~RCC_APB1RSTR_CAN1RST;
|
||||
port->ready = 0U;
|
||||
port->bitrate = 0U;
|
||||
port->loopback = 0U;
|
||||
port->no_transceiver = 0U;
|
||||
port->tx_waiting = 0U;
|
||||
}
|
||||
|
||||
uint8_t CanSensorStm32F1_Send(void *context, const CanSensor_Frame *frame)
|
||||
{
|
||||
CanSensorStm32F1_Port *port = (CanSensorStm32F1_Port *)context;
|
||||
uint32_t mailbox;
|
||||
|
||||
if ((port == 0) || (frame == 0) || (port->ready == 0U)
|
||||
|| (frame->length > CAN_SENSOR_MAX_DATA)) {
|
||||
return 0U;
|
||||
}
|
||||
if ((CAN1->TSR & CAN_TSR_TME0) != 0U) {
|
||||
mailbox = 0U;
|
||||
} else if ((CAN1->TSR & CAN_TSR_TME1) != 0U) {
|
||||
mailbox = 1U;
|
||||
} else if ((CAN1->TSR & CAN_TSR_TME2) != 0U) {
|
||||
mailbox = 2U;
|
||||
} else {
|
||||
return 0U;
|
||||
}
|
||||
can_stm32f1_load_mailbox(mailbox, frame);
|
||||
return 1U;
|
||||
}
|
||||
|
||||
uint8_t CanSensorStm32F1_Receive(void *context, CanSensor_Frame *frame)
|
||||
{
|
||||
CanSensorStm32F1_Port *port = (CanSensorStm32F1_Port *)context;
|
||||
uint32_t identifier;
|
||||
uint32_t low;
|
||||
uint32_t high;
|
||||
uint8_t index;
|
||||
|
||||
if ((port == 0) || (frame == 0) || (port->ready == 0U)
|
||||
|| ((CAN1->RF0R & CAN_RF0R_FMP0) == 0U)) {
|
||||
return 0U;
|
||||
}
|
||||
identifier = CAN1->sFIFOMailBox[0].RIR;
|
||||
if ((identifier & CAN_RI0R_IDE) != 0U) {
|
||||
frame->extended = 1U;
|
||||
frame->id = (identifier >> 3U) & 0x1FFFFFFFUL;
|
||||
} else {
|
||||
frame->extended = 0U;
|
||||
frame->id = (identifier >> 21U) & 0x7FFUL;
|
||||
}
|
||||
frame->length = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 0x0FU);
|
||||
if (frame->length > CAN_SENSOR_MAX_DATA) {
|
||||
frame->length = CAN_SENSOR_MAX_DATA;
|
||||
}
|
||||
low = CAN1->sFIFOMailBox[0].RDLR;
|
||||
high = CAN1->sFIFOMailBox[0].RDHR;
|
||||
for (index = 0U; index < CAN_SENSOR_MAX_DATA; index++) {
|
||||
if (index < 4U) {
|
||||
frame->data[index] =
|
||||
(uint8_t)((low >> (index * 8U)) & 0xFFU);
|
||||
} else {
|
||||
frame->data[index] =
|
||||
(uint8_t)((high >> ((index - 4U) * 8U)) & 0xFFU);
|
||||
}
|
||||
}
|
||||
CAN1->RF0R |= CAN_RF0R_RFOM0;
|
||||
return 1U;
|
||||
}
|
||||
|
||||
CanSensor_Io CanSensorStm32F1_MakeIo(CanSensorStm32F1_Port *port)
|
||||
{
|
||||
CanSensor_Io io;
|
||||
|
||||
io.send = CanSensorStm32F1_Send;
|
||||
io.receive = CanSensorStm32F1_Receive;
|
||||
io.context = port;
|
||||
return io;
|
||||
}
|
||||
|
||||
void CanSensorStm32F1_Task(CanSensorStm32F1_Port *port, uint32_t now_ms,
|
||||
uint32_t timeout_ms)
|
||||
{
|
||||
uint32_t status;
|
||||
|
||||
if ((port == 0) || (port->ready == 0U)) {
|
||||
return;
|
||||
}
|
||||
status = CAN1->TSR;
|
||||
if ((status & CAN_STM32F1_TSR_ALL_EMPTY) == CAN_STM32F1_TSR_ALL_EMPTY) {
|
||||
if ((status & CAN_STM32F1_TSR_ANY_TXOK) != 0U) {
|
||||
port->no_transceiver = 0U;
|
||||
}
|
||||
CAN1->TSR = status & CAN_STM32F1_TSR_DONE_FLAGS;
|
||||
port->tx_waiting = 0U;
|
||||
return;
|
||||
}
|
||||
if ((timeout_ms == 0U) || (port->tx_waiting == 0U)) {
|
||||
port->tx_waiting = (uint8_t)(timeout_ms != 0U);
|
||||
port->tx_started_ms = now_ms;
|
||||
return;
|
||||
}
|
||||
if ((now_ms - port->tx_started_ms) < timeout_ms) {
|
||||
return;
|
||||
}
|
||||
CAN1->TSR = CAN_STM32F1_TSR_ALL_ABORT;
|
||||
CAN1->TSR = CAN_STM32F1_TSR_DONE_FLAGS;
|
||||
port->tx_waiting = 0U;
|
||||
port->tx_timeouts++;
|
||||
port->no_transceiver = 1U;
|
||||
}
|
||||
|
||||
uint8_t CanSensorStm32F1_BusError(const CanSensorStm32F1_Port *port)
|
||||
{
|
||||
if ((port == 0) || (port->ready == 0U)) {
|
||||
return 0U;
|
||||
}
|
||||
return (uint8_t)(((CAN1->ESR & (CAN_ESR_BOFF | CAN_ESR_EPVF)) != 0U)
|
||||
? 1U : 0U);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Прямой bxCAN-порт датчиков для STM32F1 без HAL. Регистры контроллера обеспечивают передачу и
|
||||
* приём SETCAN, а конфигурация платы задаёт подключение шины; это аппаратная реализация
|
||||
* интерфейса can_sensor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file can_sensor_stm32f1.h
|
||||
* @brief Опросный порт can-sensor на bxCAN микроконтроллеров STM32F1.
|
||||
*/
|
||||
|
||||
#ifndef CAN_SENSOR_STM32F1_H
|
||||
#define CAN_SENSOR_STM32F1_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "can_sensor.h"
|
||||
|
||||
/** Разрядность бита, записанная портом в регистр BTR. */
|
||||
typedef struct {
|
||||
uint32_t pclk_hz;
|
||||
uint32_t bitrate;
|
||||
uint16_t prescaler;
|
||||
uint8_t ts1;
|
||||
uint8_t ts2;
|
||||
uint8_t sample_point;
|
||||
} CanSensorStm32F1_Timing;
|
||||
|
||||
/** Состояние одного контроллера bxCAN. Поля доступны только для диагностики. */
|
||||
typedef struct {
|
||||
uint32_t bitrate;
|
||||
uint32_t tx_timeouts;
|
||||
uint32_t tx_started_ms;
|
||||
CanSensorStm32F1_Timing timing;
|
||||
uint8_t ready;
|
||||
uint8_t loopback;
|
||||
uint8_t no_transceiver;
|
||||
uint8_t tx_waiting;
|
||||
} CanSensorStm32F1_Port;
|
||||
|
||||
/**
|
||||
* @brief Проверяет достижимость скорости и рассчитывает поля BTR.
|
||||
* @param bitrate Требуемая скорость, бит/с.
|
||||
* @param timing Приёмник результата либо 0.
|
||||
* @return 1, если скорость получается из текущей частоты APB1 без ошибки.
|
||||
*/
|
||||
uint8_t CanSensorStm32F1_CalcTiming(uint32_t bitrate,
|
||||
CanSensorStm32F1_Timing *timing);
|
||||
|
||||
/**
|
||||
* @brief Запускает CAN1, настраивает выводы и один 32-битный фильтр FIFO 0.
|
||||
*
|
||||
* Фильтр сравнивает Extended ID по формуле `(id & filter_mask) ==
|
||||
* (filter_id & filter_mask)` и отбрасывает Standard и Remote кадры.
|
||||
*/
|
||||
uint8_t CanSensorStm32F1_Start(CanSensorStm32F1_Port *port,
|
||||
uint32_t bitrate, uint32_t filter_id,
|
||||
uint32_t filter_mask, uint8_t loopback);
|
||||
|
||||
/** Останавливает CAN1 программным сбросом. */
|
||||
void CanSensorStm32F1_Stop(CanSensorStm32F1_Port *port);
|
||||
|
||||
/** Помещает data-кадр в свободный mailbox. Совместима с CanSensor_Io.send. */
|
||||
uint8_t CanSensorStm32F1_Send(void *context, const CanSensor_Frame *frame);
|
||||
|
||||
/** Забирает один data-кадр из FIFO 0. Совместима с CanSensor_Io.receive. */
|
||||
uint8_t CanSensorStm32F1_Receive(void *context, CanSensor_Frame *frame);
|
||||
|
||||
/** Собирает таблицу callback-функций для CanSensor_Init(). */
|
||||
CanSensor_Io CanSensorStm32F1_MakeIo(CanSensorStm32F1_Port *port);
|
||||
|
||||
/**
|
||||
* @brief Обслуживает завершение и тайм-аут передачи без прерываний.
|
||||
* @param now_ms Монотонное время приложения, мс.
|
||||
* @param timeout_ms Предел ожидания ACK; 0 отключает принудительную отмену.
|
||||
*/
|
||||
void CanSensorStm32F1_Task(CanSensorStm32F1_Port *port, uint32_t now_ms,
|
||||
uint32_t timeout_ms);
|
||||
|
||||
/** Возвращает 1 при bus-off или error-passive. */
|
||||
uint8_t CanSensorStm32F1_BusError(const CanSensorStm32F1_Port *port);
|
||||
|
||||
#endif /* CAN_SENSOR_STM32F1_H */
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Конфигурация can-sensor для порта stm32f1. Макросы задают параметры сборки и ограничения,
|
||||
* используемые исходниками этого модуля. Это образец для проекта: значения выводов, размеров
|
||||
* памяти и параметров устройства необходимо согласовать с целевой платой.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file can_sensor_stm32f1_config.f103.template.h
|
||||
* @brief Шаблон выводов CAN1 для STM32F103 без ремапа.
|
||||
*
|
||||
* Скопируйте файл в include-каталог приложения под именем
|
||||
* can_sensor_stm32f1_config.h. Скорость и фильтр задаются при запуске порта.
|
||||
*/
|
||||
|
||||
#ifndef CAN_SENSOR_STM32F1_CONFIG_H
|
||||
#define CAN_SENSOR_STM32F1_CONFIG_H
|
||||
|
||||
#include "stm32f10x.h"
|
||||
|
||||
#define CAN_SENSOR_STM32F1_GPIO GPIOA
|
||||
#define CAN_SENSOR_STM32F1_GPIO_CLOCK RCC_APB2ENR_IOPAEN
|
||||
#define CAN_SENSOR_STM32F1_RX_PIN 11U
|
||||
#define CAN_SENSOR_STM32F1_TX_PIN 12U
|
||||
|
||||
#endif /* CAN_SENSOR_STM32F1_CONFIG_H */
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Проверки can_sensor: фиксируют ожидаемое поведение на воспроизводимых входных данных.
|
||||
* Тестовые значения и подмены окружения относятся к проверке контракта; они не являются
|
||||
* настройками рабочего устройства.
|
||||
*/
|
||||
|
||||
#include "can_sensor.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
typedef struct {
|
||||
CanSensor_Frame last_frame;
|
||||
uint32_t send_count;
|
||||
uint8_t accept;
|
||||
} FakeCan;
|
||||
|
||||
static uint8_t fake_send(void *context, const CanSensor_Frame *frame)
|
||||
{
|
||||
FakeCan *fake = (FakeCan *)context;
|
||||
|
||||
fake->send_count++;
|
||||
fake->last_frame = *frame;
|
||||
return fake->accept;
|
||||
}
|
||||
|
||||
#define CHECK(condition) \
|
||||
do { \
|
||||
if (!(condition)) { \
|
||||
fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \
|
||||
#condition); \
|
||||
return 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
CanSensor link;
|
||||
CanSensor_Config config;
|
||||
CanSensor_Io io;
|
||||
FakeCan fake = {0};
|
||||
|
||||
fake.accept = 1U;
|
||||
io.send = fake_send;
|
||||
io.receive = 0;
|
||||
io.context = &fake;
|
||||
CanSensor_ConfigDefault(&config);
|
||||
|
||||
CHECK(config.pulse_period_ms == 0U);
|
||||
CHECK(CanSensor_Init(&link, &io, &config) != 0U);
|
||||
|
||||
/* Нулевой период означает выключенный PULSE при любых метках времени. */
|
||||
CanSensor_Task(&link, 0U);
|
||||
CanSensor_Task(&link, 0xFFFFFFFFUL);
|
||||
CHECK(fake.send_count == 0U);
|
||||
|
||||
CanSensor_SetPulsePeriod(&link, 1000U);
|
||||
CHECK(CanSensor_PulsePeriod(&link) == 1000U);
|
||||
CanSensor_Task(&link, 500U);
|
||||
CanSensor_Task(&link, 1499U);
|
||||
CHECK(fake.send_count == 0U);
|
||||
CanSensor_Task(&link, 1500U);
|
||||
CHECK(fake.send_count == 1U);
|
||||
CHECK(fake.last_frame.id == 0x1FFF0000UL);
|
||||
CHECK(fake.last_frame.extended == 1U);
|
||||
CHECK(fake.last_frame.length == 1U);
|
||||
CHECK(fake.last_frame.data[0] == 1U);
|
||||
|
||||
/* После period=0 дальнейшие вызовы не отправляют heartbeat. */
|
||||
CanSensor_SetPulsePeriod(&link, 0U);
|
||||
CHECK(CanSensor_PulsePeriod(&link) == 0U);
|
||||
CanSensor_Task(&link, 0xFFFFFFFFUL);
|
||||
CHECK(fake.send_count == 1U);
|
||||
|
||||
/* Повторное включение начинает новый полный период. */
|
||||
CanSensor_SetPulsePeriod(&link, 10U);
|
||||
CanSensor_Task(&link, 2000U);
|
||||
CanSensor_Task(&link, 2009U);
|
||||
CHECK(fake.send_count == 1U);
|
||||
CanSensor_Task(&link, 2010U);
|
||||
CHECK(fake.send_count == 2U);
|
||||
CHECK(fake.last_frame.data[0] == 2U);
|
||||
|
||||
puts("can_sensor pulse tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
cmake_minimum_required(VERSION 3.16) # Эта версия поддерживает используемые target_compile_features и CTest без платформенных расширений.
|
||||
project(can485_monitor C) # Переносимое ядро собирается независимо от Linux-порта и приложения SETGUI.
|
||||
add_library(can485_monitor STATIC src/can485_monitor.c) # Все платформы связываются с одним обработчиком команд и wire-форматов.
|
||||
target_include_directories(can485_monitor PUBLIC include) # Потребитель получает только публичный C-контракт библиотеки.
|
||||
target_compile_features(can485_monitor PUBLIC c_std_99) # Ядро не требует C11 и платформенных расширений языка.
|
||||
include(CTest) # Стандартный BUILD_TESTING управляет автономной проверкой библиотеки.
|
||||
if(BUILD_TESTING) # Host-проверка не требует Linux, Qt и физической CAN-шины.
|
||||
add_executable(test_can485_monitor tests/test_can485_monitor.c) # Использует независимые wire-векторы и фикстуру аппаратных отказов.
|
||||
target_link_libraries(test_can485_monitor PRIVATE can485_monitor) # Проверяет настоящую библиотеку, не тестовую копию алгоритма.
|
||||
add_test(NAME can485_monitor_contract COMMAND test_can485_monitor) # Регистрирует единый поведенческий контракт для всех аппаратных портов.
|
||||
endif() # Завершает необязательную сборку тестов.
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux") # Linux-заголовки не попадают в сборки ESP32, Windows и других MCU.
|
||||
add_executable(zynq-can485d ports/zynq-linux/main.c) # SocketCAN, USB и Ethernet реализуются только аппаратным портом.
|
||||
target_link_libraries(zynq-can485d PRIVATE can485_monitor) # Linux использует ровно то же C99-ядро, что host-проверка.
|
||||
endif() # Завершает платформенную часть библиотеки.
|
||||
@@ -0,0 +1,75 @@
|
||||
# CAN485 monitor
|
||||
|
||||
Общее C99-ядро команд CAN485 DevBoard/SETGUI и Linux-порт для PS CAN Zynq.
|
||||
Протоколы CAN (SETCAN, ProtoCAN, УМП, Balsam, DEIF и остальные) разбираются
|
||||
существующими библиотеками `templates/python`, без копий в проекте платы.
|
||||
|
||||
Слои: SETGUI BoardTab → `qt_ports/can485_port.py` → Ethernet TCP или USB tty →
|
||||
`ports/zynq-linux/main.c` → `can485_monitor.c` → SocketCAN / RS485 / каталог SD.
|
||||
В ядре нет Linux, Qt, HAL, выделения памяти и глобального контекста.
|
||||
|
||||
| Файл | Назначение и зависимости |
|
||||
|---|---|
|
||||
| `include/can485_monitor.h` | Контракт C99, `stdint.h`, `stddef.h` |
|
||||
| `src/can485_monitor.c` | Проверка команд, фильтр, маршруты, AA55/CRC8, строки CAN |
|
||||
| `ports/zynq-linux/main.c` | SocketCAN, rtnetlink, TCP, USB tty, UART RS485, SD, POSIX/Linux |
|
||||
| `tests/test_can485_monitor.c` | Эталонный пакет, ошибки/границы, маршрутизация, отказы оборудования |
|
||||
|
||||
Порт предоставляет четыре callback: `emit(context,line)`,
|
||||
`configure(context,kbps,mode,running)`, `send(context,frame,source)` и
|
||||
`control(context,command)`. `source=0` означает CAN, `source=1` — RS485.
|
||||
`configure` и `send` возвращают 1 только после принятия операции драйвером.
|
||||
Аппаратный ACK этим результатом не подтверждается. Контекст и буферы принадлежат приложению.
|
||||
|
||||
```c
|
||||
#include "can485_monitor.h" /* Подключает переносимое ядро библиотеки. */
|
||||
void example(cm_port port) { /* Аппаратные callback передаются вызывающей платформой. */
|
||||
cm_monitor state; /* Контекст не разделяется с другим экземпляром моста. */
|
||||
cm_init(&state, port); /* CAN остановлен, оба направления маршрута выключены. */
|
||||
cm_command(&state, "V"); /* Версия и статус не меняют оборудование. */
|
||||
cm_command(&state, "S125"); /* Запрашивает CAN 125 кбит/с через аппаратный callback. */
|
||||
cm_command(&state, "M1"); /* Listen-only запрещает ручной TX и RS485→CAN. */
|
||||
cm_frame frame = {0x123, 2, 0, {0xAB, 0xCD}}; /* STD ID и два исходных байта. */
|
||||
cm_receive(&state, &frame, 0, 12345); /* Приём CAN в момент 12,345 с монотонного времени. */
|
||||
cm_status(&state, 12345, "sd=idle"); /* Статус содержит только известные платформе поля. */
|
||||
} /* Реальная плата передаёт только фактически принятые кадры в cm_receive. */
|
||||
```
|
||||
|
||||
Поддержаны команды `V ? H S M O C F Q R B T X Z L D G I` и Linux-расширение
|
||||
`Ncan0` / `Ncan1`. Команды строго проверяются целиком. RS485-пакет:
|
||||
`AA 55 FLAGS ID[4]be DLC DATA[8] CRC8(FLAGS..DATA, poly=0x07, init=0)`.
|
||||
CAN STD/EXT/RTR сохраняются. DATA TX через `T` повторяет контракт существующего ESP GUI.
|
||||
|
||||
Linux-порт запускается с CAN остановленным, маршрутами и записью SD выключенными.
|
||||
`S`/`M`/`O` запускают CAN; `C` останавливает интерфейс. Настройка через rtnetlink
|
||||
требует root или `CAP_NET_ADMIN`; RAW CAN — соответствующих прав Linux.
|
||||
M2 требует реальной поддержки `CAN_CTRLMODE_PRESUME_ACK`; ошибка драйвера
|
||||
не маскируется под успешный no-ack. Фильтр программный и не отключает аппаратный ACK.
|
||||
|
||||
TCP-порт по умолчанию **37579**, один управляющий клиент. Подключённый USB
|
||||
может получать поток, но команды второго транспорта возвращают `#ERR busy`.
|
||||
Второй TCP-клиент не вытесняет первого. При обрыве владельца маршруты выключаются,
|
||||
его SD-download отменяется. Медленный клиент отключается при заполнении очереди 64 КиБ.
|
||||
Служба не включает периодический CAN TX: этот таймер принадлежит SETGUI и
|
||||
останавливается при отключении. Смена `N` закрывает только собственный CAN socket.
|
||||
|
||||
`--usb /dev/ttyGS0` использует USB OTG CDC ACM, если образ Linux предоставляет
|
||||
gadget serial. Также допустим выделенный UART через USB-UART, свободный от
|
||||
console/getty. Разъём USB HOST и USB-JTAG сами по себе CDC не создают.
|
||||
USB и RS485 должны использовать разные tty. RTS/CTS выключены. RS485 требует
|
||||
физического трансивера с аппаратным управлением направлением либо предварительной
|
||||
настройки драйвера UART; Linux-порт не переключает произвольные GPIO платы.
|
||||
|
||||
SD — заранее выбранный каталог `--logs`, например `/mnt/sd/can-logs`.
|
||||
Журналы `can_*.csv` содержат совместимые текстовые строки кадров; импорт SETGUI
|
||||
распознаёт этот формат автоматически. Выдача файла асинхронная, блоками 128 байт,
|
||||
с проверяемыми offset/size/CRC32. Путь ограничен одним каталогом, `..` и symlink
|
||||
запрещены. Запись SD выключается перед чтением. `vin`, TEC и REC не выдумываются,
|
||||
если платформа не получила реальные значения.
|
||||
|
||||
Используется: Zynq RK7020 (`demo/can485`) и CAN-панель SETGUI (`pc/zynq_can_panel.py`).
|
||||
Для единого физического источника задавайте одинаковый **TEMPLATES_ROOT** при
|
||||
запуске SETGUI и сборке ARM (`build.ps1 -Templates <тот же каталог>`).
|
||||
После изменения Python перезапустите приложение; изменение C требует пересборки
|
||||
службы, изменение встроенных библиотек EXE — пересборки EXE. Подмодули разных
|
||||
checkout обновляются явно: Git не обновляет уже собранную прошивку автоматически.
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef CAN485_MONITOR_H /* Защищает публичный контракт монитора от повторного включения. */
|
||||
#define CAN485_MONITOR_H /* Одинаковое ядро обслуживает команды DevBoard на разных платформах. */
|
||||
#include <stdint.h> /* Фиксирует размеры CAN ID, счётчиков и байтов независимо от процессора. */
|
||||
#include <stddef.h> /* Размеры выходных строк выражены штатным типом size_t. */
|
||||
typedef struct { uint32_t id; uint8_t dlc, flags, data[8]; } cm_frame; /* flags: бит 0 EXT, бит 1 RTR; Classic CAN содержит до восьми байтов. */
|
||||
typedef struct { /* Платформа предоставляет ввод-вывод, не копируя разбор команд. */
|
||||
void (*emit)(void *, const char *); /* Принимает одну законченную строку с LF для Ethernet, USB и журнала. */
|
||||
int (*configure)(void *, unsigned, unsigned, int); /* Применяет битрейт в кбит/с, режим M0/M1/M2 и состояние драйвера; ноль означает ошибку. */
|
||||
int (*send)(void *, const cm_frame *, int); /* Передаёт кадр в CAN при source=0 либо в RS485 при source=1; успех означает принятие локальным драйвером. */
|
||||
int (*control)(void *, const char *); /* Обслуживает аппаратную скорость B и файловые L/D/G/I; ответы формирует сам порт. */
|
||||
void *context; /* Контекст принадлежит вызывающему коду и позволяет иметь несколько независимых мостов. */
|
||||
} cm_port; /* В ядре нет HAL, Linux, Qt, динамической памяти и глобального состояния. */
|
||||
typedef struct { /* Состояние протокола отделено от сетевых клиентов и файловых дескрипторов. */
|
||||
cm_port port; unsigned bitrate, mode, route, baud, running; /* Скорости заданы в кбит/с для CAN и бод для RS485; route использует два младших бита направлений. */
|
||||
uint32_t filter_id, filter_mask, rx, tx, rsrx, rstx, buserr, dropcan, droprs, rscrc; /* Счётчики относятся к фактическим операциям порта; фильтр сравнивает только 29 бит ID. */
|
||||
uint64_t now; /* Последнее подтверждённое монотонное время позволяет ответу V сохранять реальный uptime между периодическими статусами. */
|
||||
} cm_monitor; /* Всё изменяемое состояние хранится в экземпляре вызывающего приложения. */
|
||||
void cm_init(cm_monitor *, cm_port); /* Инициализирует остановленный CAN 125 кбит/с, normal, маршруты выключены и RS485 512000 бод. */
|
||||
void cm_command(cm_monitor *, const char *); /* Проверяет полную команду без CR/LF и применяет её только после успешной валидации. */
|
||||
void cm_receive(cm_monitor *, const cm_frame *, int, uint64_t); /* Принимает кадр CAN=0 либо RS485=1 и монотонное время в миллисекундах. */
|
||||
void cm_status(cm_monitor *, uint64_t, const char *); /* Печатает совместимый #STAT; дополнительная строка содержит подтверждённые поля SD/Linux. */
|
||||
int cm_packet_decode(const uint8_t *, cm_frame *); /* Проверяет все 17 байт AA55/CRC8, формат ID и DLC до маршрутизации RS485. */
|
||||
void cm_packet_encode(const cm_frame *, uint8_t *); /* Формирует RS485 AA55, FLAGS, ID big-endian, DLC, восемь байтов и CRC8/0x07. */
|
||||
#endif /* Завершает независимый публичный контракт CAN485 monitor. */
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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 cfgmgr32) # Configuration Manager сопоставляет интерфейсы CAN и DFU по родительскому USB-узлу.
|
||||
target_include_directories(candle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
File diff suppressed because one or more lines are too long
@@ -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`.
|
||||
+1302
@@ -0,0 +1,1302 @@
|
||||
/*
|
||||
|
||||
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"
|
||||
#include <cfgmgr32.h> /* Сопоставляет интерфейсы CAN и DFU по общему родительскому USB-устройству Windows. */
|
||||
|
||||
static DWORD candle_dfu_parent(const wchar_t *path, DEVINST *parent) /* Возвращает родителя интерфейса, чтобы команда не попала в другой подключённый адаптер. */
|
||||
{ /* Начало поиска физического USB-устройства по пути выбранного CAN-интерфейса. */
|
||||
HDEVINFO devices = SetupDiCreateDeviceInfoList(NULL, NULL); /* Создаёт временный набор SetupAPI без перечисления посторонних устройств. */
|
||||
if (devices == INVALID_HANDLE_VALUE) return GetLastError(); /* Сохраняет причину отказа создания набора Windows. */
|
||||
SP_DEVICE_INTERFACE_DATA iface = {0}; /* Подготавливает описание интерфейса для открытия по точному пути. */
|
||||
iface.cbSize = sizeof(iface); /* Передаёт SetupAPI размер структуры текущей разрядности процесса. */
|
||||
SP_DEVINFO_DATA info = {0}; /* Получает идентификатор узла PnP выбранного интерфейса. */
|
||||
info.cbSize = sizeof(info); /* Указывает размер структуры устройства согласно контракту SetupAPI. */
|
||||
DWORD size = 0, error = ERROR_SUCCESS; /* Хранит требуемое число байтов и итоговый код Windows. */
|
||||
if (!SetupDiOpenDeviceInterfaceW(devices, path, 0, &iface)) error = GetLastError(); /* Открывает только заданный путь, сохраняя ошибку исчезнувшего устройства. */
|
||||
if (!error) SetupDiGetDeviceInterfaceDetailW(devices, &iface, NULL, 0, &size, NULL); /* Узнаёт объём буфера переменной длины для пути USB-интерфейса. */
|
||||
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail = size ? malloc(size) : NULL; /* Выделяет буфер только после успешного определения его размера. */
|
||||
if (!error && !detail) error = size ? ERROR_NOT_ENOUGH_MEMORY : GetLastError(); /* Отличает нехватку памяти от ошибки поиска описания. */
|
||||
if (detail) { /* Обрабатывает описание только при наличии выделенного буфера. */
|
||||
detail->cbSize = sizeof(*detail); /* Согласует размер заголовка с архитектурой Win32 или Win64. */
|
||||
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, size, NULL, &info)) error = GetLastError(); /* Получает узел интерфейса из SetupAPI. */
|
||||
else if (CM_Get_Parent(parent, info.DevInst, 0) != CR_SUCCESS) error = ERROR_NOT_FOUND; /* Получает общий родительский узел составного USB-адаптера. */
|
||||
free(detail); /* Освобождает описание после получения идентификатора родителя. */
|
||||
} /* Завершает работу с описанием интерфейса. */
|
||||
SetupDiDestroyDeviceInfoList(devices); /* Освобождает набор SetupAPI при любом результате поиска. */
|
||||
return error; /* Возвращает ноль при успехе либо причину отказа Windows. */
|
||||
} /* Завершает сопоставление интерфейса с физическим USB-устройством. */
|
||||
|
||||
uint32_t __stdcall candle_dev_enter_dfu(const wchar_t *can_path, uint8_t status[6]) /* Возвращает код Windows и шестибайтовый DFU_GETSTATUS согласно DFU 1.1. */
|
||||
{ /* Начало отдельного сеанса управляющего интерфейса обновления. */
|
||||
if (!can_path || !status) return ERROR_INVALID_PARAMETER; /* Запрещает отсутствие точного пути адаптера или выходного буфера статуса. */
|
||||
memset(status, 0, 6); /* Исключает использование неинициализированных полей ответа при ошибке USB. */
|
||||
DEVINST wanted_parent = 0; /* Запоминает физический адаптер, выбранный пользователем до остановки CAN. */
|
||||
DWORD error = candle_dfu_parent(can_path, &wanted_parent); /* Ищет родителя по сохранённому пути CAN, не заменяя его первым найденным устройством. */
|
||||
if (error) return error; /* Прерывает переход, если выбранный адаптер уже исчез из PnP. */
|
||||
GUID guid = {0xc25b4308, 0x04d3, 0x11e6, {0xb3, 0xea, 0x60, 0x57, 0x18, 0x9e, 0x64, 0x43}}; /* GUID интерфейса обновления из дескриптора CANable 2.5. */
|
||||
HDEVINFO devices = SetupDiGetClassDevsW(&guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); /* Перечисляет только подключённые интерфейсы обновления CANable 2.5. */
|
||||
if (devices == INVALID_HANDLE_VALUE) return GetLastError(); /* Возвращает отказ SetupAPI без попытки послать команду. */
|
||||
error = ERROR_NOT_FOUND; /* Отсутствие интерфейса обновления не считается успешным входом в DFU. */
|
||||
for (DWORD index = 0; ; ++index) { /* Проверяет интерфейсы до точного совпадения физического устройства. */
|
||||
SP_DEVICE_INTERFACE_DATA iface = {0}; /* Создаёт независимое описание очередного DFU-интерфейса. */
|
||||
iface.cbSize = sizeof(iface); /* Задаёт обязательный размер структуры SetupAPI. */
|
||||
if (!SetupDiEnumDeviceInterfaces(devices, NULL, &guid, index, &iface)) { /* Завершает обход либо сообщает ошибку перечисления. */
|
||||
if (GetLastError() != ERROR_NO_MORE_ITEMS) error = GetLastError(); /* Отличает штатный конец списка от отказа Windows. */
|
||||
break; /* Выходит из обхода после исчерпания интерфейсов или ошибки. */
|
||||
} /* Завершает проверку результата перечисления. */
|
||||
DWORD size = 0; /* Хранит размер описания очередного интерфейса в байтах. */
|
||||
SetupDiGetDeviceInterfaceDetailW(devices, &iface, NULL, 0, &size, NULL); /* Определяет буфер для полного пути интерфейса обновления. */
|
||||
if (!size) { error = GetLastError(); break; } /* Не продолжает работу с неполным результатом SetupAPI. */
|
||||
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail = malloc(size); /* Выделяет описание с достаточным местом для USB-пути. */
|
||||
if (!detail) { error = ERROR_NOT_ENOUGH_MEMORY; break; } /* Не допускает обращения к отсутствующему буферу. */
|
||||
detail->cbSize = sizeof(*detail); /* Сохраняет ABI описания интерфейса Windows. */
|
||||
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, size, NULL, NULL)) { /* Получает путь, который можно передать CreateFileW. */
|
||||
error = GetLastError(); free(detail); break; /* Сохраняет ошибку и освобождает буфер перед выходом. */
|
||||
} /* Завершает чтение пути интерфейса. */
|
||||
DEVINST parent = 0; /* Хранит родителя текущего кандидата на обновление. */
|
||||
if (candle_dfu_parent(detail->DevicePath, &parent) || parent != wanted_parent) { /* Исключает каждый интерфейс другого физического адаптера. */
|
||||
free(detail); continue; /* Освобождает описание неподходящего кандидата и продолжает поиск. */
|
||||
} /* Завершает проверку принадлежности выбранному USB-адаптеру. */
|
||||
HANDLE file = CreateFileW(detail->DevicePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); /* Эксклюзивно открывает интерфейс обновления выбранного устройства для WinUSB. */
|
||||
free(detail); /* Путь больше не нужен после открытия системного дескриптора. */
|
||||
if (file == INVALID_HANDLE_VALUE) { error = GetLastError(); break; } /* Не скрывает отсутствие доступа или занятость интерфейса. */
|
||||
WINUSB_INTERFACE_HANDLE usb = NULL; /* Хранит отдельный WinUSB-сеанс интерфейса обновления без CAN bulk endpoint. */
|
||||
error = ERROR_SUCCESS; /* Сбрасывает исходное отсутствие интерфейса после точного совпадения устройства. */
|
||||
if (!WinUsb_Initialize(file, &usb)) error = GetLastError(); /* Проверяет наличие рабочего WinUSB-драйвера DFU runtime. */
|
||||
USB_INTERFACE_DESCRIPTOR descriptor = {0}; /* Получает реальный номер и класс открытого интерфейса. */
|
||||
if (!error && !WinUsb_QueryInterfaceSettings(usb, 0, &descriptor)) error = GetLastError(); /* Не предполагает, что произвольный WinUSB-интерфейс поддерживает DFU. */
|
||||
if (!error && (descriptor.bInterfaceClass != 0xFE || descriptor.bInterfaceSubClass != 1 || descriptor.bInterfaceProtocol != 1)) error = ERROR_NOT_SUPPORTED; /* Проверяет класс приложения, подкласс DFU и runtime-протокол по DFU 1.1. */
|
||||
ULONG timeout = 500, transferred = 0; /* Ограничивает управляющий USB-запрос 500 миллисекундами и учитывает длину ответа. */
|
||||
if (!error && !WinUsb_SetPipePolicy(usb, 0, PIPE_TRANSFER_TIMEOUT, sizeof(timeout), &timeout)) error = GetLastError(); /* Устанавливает тайм-аут endpoint 0, чтобы отсутствующий ответ не блокировал программу. */
|
||||
WINUSB_SETUP_PACKET packet = {0}; /* Обнуляет поля USB SETUP перед запросом перехода в загрузчик. */
|
||||
packet.RequestType = 0x21; packet.Request = 0; packet.Value = 1000; packet.Index = descriptor.bInterfaceNumber; /* DFU_DETACH: class/interface OUT, запрос 0, ожидаемое время отключения 1000 мс. */
|
||||
if (!error && !WinUsb_ControlTransfer(usb, packet, NULL, 0, &transferred, NULL)) error = GetLastError(); /* Отправляет DETACH и требует подтверждённой USB-транзакции. */
|
||||
packet.RequestType = 0xA1; packet.Request = 3; packet.Value = 0; packet.Length = 6; /* DFU_GETSTATUS: class/interface IN, запрос 3, ответ ровно 6 байт по DFU 1.1. */
|
||||
if (!error && !WinUsb_ControlTransfer(usb, packet, status, 6, &transferred, NULL)) error = GetLastError(); /* Проверяет результат до отложенного перехода прошивки через 300 мс. */
|
||||
if (!error && transferred != 6) error = ERROR_INVALID_DATA; /* Не трактует укороченный ответ как подтверждение входа в загрузчик. */
|
||||
if (usb) WinUsb_Free(usb); /* Освобождает WinUSB до появления загрузчика с другим VID/PID. */
|
||||
CloseHandle(file); /* Освобождает файловый дескриптор после успеха и при любой ошибке USB. */
|
||||
break; /* Не отправляет DETACH повторно и не переключается на другой адаптер при отказе. */
|
||||
} /* Завершает поиск и обработку единственного выбранного адаптера. */
|
||||
SetupDiDestroyDeviceInfoList(devices); /* Освобождает перечень интерфейсов при любом результате. */
|
||||
return error; /* Статус прошивки возвращается отдельно от системной ошибки USB. */
|
||||
} /* Завершает управление DFU runtime без записи flash или изменения образа. */
|
||||
|
||||
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_product(candle_handle hdev, wchar_t *product, uint32_t capacity) /* Читает USB Product String открытого адаптера в буфер UTF-16; capacity измеряется в wchar_t, включая завершающий ноль. */
|
||||
{ /* Начало чтения отображаемого имени без изменения режима CAN. */
|
||||
if (!product || !capacity) return false; /* Не пишет в отсутствующий либо пустой выходной буфер. */
|
||||
product[0] = 0; /* Сохраняет пустую строку при любом отказе чтения дескриптора. */
|
||||
candle_device_t *dev = (candle_device_t*)hdev; /* Получает уже открытый WinUSB-сеанс выбранного физического адаптера. */
|
||||
if (!dev || !dev->winUSBHandle) return false; /* Не открывает устройство заново и не обращается к закрытому интерфейсу. */
|
||||
USB_DEVICE_DESCRIPTOR descriptor = {0}; /* Получает индекс строки продукта из стандартного дескриптора устройства. */
|
||||
ULONG received = 0; /* Хранит фактически полученное количество байтов управляющего ответа. */
|
||||
if (!WinUsb_GetDescriptor(dev->winUSBHandle, 1, 0, 0, (PUCHAR)&descriptor, sizeof(descriptor), &received) || received != sizeof(descriptor)) return false; /* Тип 1 обозначает Device Descriptor по USB 2.0; неполный ответ не используется. */
|
||||
UCHAR string_index = descriptor.iProduct; /* Для обычных адаптеров использует объявленный устройством индекс USB Product String. */
|
||||
if (descriptor.idVendor == 0x1D50 && descriptor.idProduct == 0x606F) string_index = 2; /* В CANable Candlelight имя продукта имеет индекс 2; WinUSB может подменять iProduct индексом имени интерфейса, как описано в штатном примере проекта. */
|
||||
if (!string_index) return false; /* Нулевой iProduct означает, что устройство не предоставляет имя продукта. */
|
||||
UCHAR raw[256] = {0}; /* Буфер вмещает строковый дескриптор USB, чей однобайтовый bLength ограничен 255 байтами. */
|
||||
USHORT language = 0x0409; /* Использует английский идентификатор USB как резервный вариант для устройств без списка языков. */
|
||||
if (WinUsb_GetDescriptor(dev->winUSBHandle, 3, 0, 0, raw, sizeof(raw), &received) && received >= 4 && raw[0] >= 4 && raw[1] == 3) language = (USHORT)(raw[2] | ((USHORT)raw[3] << 8)); /* Тип 3 и индекс 0 возвращают список LANGID; выбирает первый язык, предоставленный адаптером. */
|
||||
if (!WinUsb_GetDescriptor(dev->winUSBHandle, 3, string_index, language, raw, sizeof(raw), &received)) return false; /* Читает именно строку продукта на поддерживаемом устройством языке. */
|
||||
if (received < 2 || raw[1] != 3 || raw[0] < 2 || raw[0] > received || (raw[0] & 1)) return false; /* Проверяет тип, размер и чётное число байтов UTF-16 перед копированием в выходной буфер. */
|
||||
uint32_t count = (raw[0] - 2) / 2; /* Исключает двухбайтовый заголовок и переводит длину UTF-16LE из байтов в число кодовых единиц. */
|
||||
if (count >= capacity) return false; /* Не обрезает индивидуальный номер в конце имени и оставляет место для завершающего нуля. */
|
||||
for (uint32_t index = 0; index < count; ++index) product[index] = (wchar_t)(raw[2 + index * 2] | ((USHORT)raw[3 + index * 2] << 8)); /* Явно собирает кодовые единицы UTF-16LE без предположений о выравнивании массива байтов. */
|
||||
product[count] = 0; /* Завершает строку для безопасного чтения через ctypes и отображения в Qt. */
|
||||
return count > 0; /* Пустое имя оставляет интерфейсу стандартную резервную подпись адаптера. */
|
||||
} /* Завершает получение настоящего имени из прошивки USB-адаптера. */
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_start_configured(candle_handle hdev, uint8_t ch, uint32_t flags) /* Новый вход сохраняет прежний ABI start и позволяет отключать HW_TIMESTAMP. */
|
||||
{ /* Явно выбранные Python-флаги должны попасть в прошивку без автоматического дополнения. */
|
||||
return candle_ctrl_set_device_mode((candle_device_t*)hdev, ch, CANDLE_DEVMODE_START, flags); /* Передаём точную маску после проверки возможностей на уровне транспорта. */
|
||||
} /* Завершение запуска с явно заданными параметрами. */
|
||||
|
||||
bool __stdcall DLL candle_channel_get_data_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap) /* Экспортируем чтение FD-пределов для расчёта тайминга на любой частоте CAN. */
|
||||
{ /* Общая форма capability позволяет повторно использовать валидатор номинального тайминга. */
|
||||
return candle_ctrl_data_capability((candle_device_t*)hdev, ch, cap); /* USB-запрос возвращает реальные пределы выбранного канала. */
|
||||
} /* Завершение получения возможностей быстрой фазы. */
|
||||
|
||||
bool __stdcall DLL candle_channel_set_termination(candle_handle hdev, uint8_t ch, uint32_t state) /* Значения 0 и 1 соответствуют OFF и ON стандарта gs_usb. */
|
||||
{ /* Проверка исключает отправку неопределённого состояния в прошивку. */
|
||||
return state <= 1 && candle_ctrl_termination((candle_device_t*)hdev, ch, &state, true); /* Выполняем запись только для допустимого состояния. */
|
||||
} /* Завершение переключения терминатора. */
|
||||
|
||||
bool __stdcall DLL candle_channel_get_termination(candle_handle hdev, uint8_t ch, uint32_t *state) /* Чтение состояния не меняет топологию CAN. */
|
||||
{ /* Используем тот же формат 32-битного значения, что и при записи. */
|
||||
return candle_ctrl_termination((candle_device_t*)hdev, ch, state, false); /* false выбирает USB_DIR_IN и GET_TERMINATION. */
|
||||
} /* Завершение чтения терминатора. */
|
||||
|
||||
bool __stdcall DLL candle_channel_get_diagnostics(candle_handle hdev, uint8_t ch, uint32_t *state, uint32_t *rxerr, uint32_t *txerr) /* Сохраняем счётчики, которые старый get_state отбрасывал. */
|
||||
{ /* Диагностика выполняется только для уже открытого устройства. */
|
||||
candle_device_state_t data = {0}; /* Полная структура протокола содержит три слова: состояние, RX и TX. */
|
||||
if (!candle_ctrl_get_state((candle_device_t*)hdev, ch, &data)) { /* Не возвращаем нулевые счётчики как будто запрос был успешным. */
|
||||
return false; /* Причина USB-отказа уже записана управляющим запросом. */
|
||||
} /* Дальнейшее копирование относится только к подтверждённому ответу. */
|
||||
*state = data.state; /* Сохраняем состояние Error-active, Warning, Passive, Bus-off, Stopped или Sleeping. */
|
||||
*rxerr = data.rxerr; /* Счётчик ошибок приёма измеряется в единицах CAN-контроллера. */
|
||||
*txerr = data.txerr; /* Счётчик ошибок передачи помогает отличать отсутствие ACK от ошибок приёма. */
|
||||
return true; /* Все три выходных значения относятся к одному USB-ответу. */
|
||||
} /* Завершение расширенной диагностики канала. */
|
||||
|
||||
/* 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;
|
||||
|
||||
const DWORD header_size = 12; /* Заголовок gs_usb занимает 12 байт в Classic и FD; короткий FD может не содержать восьми байтов данных. */
|
||||
if (bytes_transfered < header_size) { /* До чтения flags и DLC проверяем наличие полного заголовка. */
|
||||
candle_prepare_read(dev, urb_num);
|
||||
candle_logf(L"fd read too small urb=%u bytes=%lu min=%lu",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
header_size); /* Минимальная длина здесь относится к заголовку, а payload проверяется после определения формата. */
|
||||
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];
|
||||
if (raw_dlc > 15) { /* FD DLC является четырёхбитным кодом и не может содержать значения выше 15. */
|
||||
candle_prepare_read(dev, urb_num); /* Повреждённый USB-пакет не должен останавливать последующий приём. */
|
||||
dev->last_error = CANDLE_ERR_READ_SIZE; /* Неверный DLC считается ошибкой формата входного кадра. */
|
||||
return false; /* Не превращаем повреждённый DLC в пустой успешный FD-кадр. */
|
||||
} /* Далее длина безопасно вычисляется по таблице CAN FD. */
|
||||
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 {
|
||||
if (bytes_transfered < sizeof(candle_frame_t) - sizeof(uint32_t)) { /* Classic-пакет обязан содержать весь восьмибайтовый буфер данных, даже при меньшем DLC. */
|
||||
candle_prepare_read(dev, urb_num); /* Возобновляем соответствующий URB после отклонения неполного пакета. */
|
||||
dev->last_error = CANDLE_ERR_READ_SIZE; /* Не допускаем чтение неполученных данных из локальной структуры. */
|
||||
return false; /* Повреждённый Classic-пакет не попадает в Python через FD API. */
|
||||
} /* Минимальный размер Classic теперь проверен отдельно от коротких FD. */
|
||||
/* 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;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
; Доступ к CAN-адаптерам candleLight/gs_usb в Windows через WinUSB. Перечисление устройств,
|
||||
; открытие канала и обмен кадрами образуют отдельный транспортный слой; он не интерпретирует
|
||||
; прикладные регистры SETProtocol.
|
||||
|
||||
EXPORTS
|
||||
candle_dev_get_product ; Имя продукта из USB-дескриптора прошивки для списка адаптеров SETGUI.
|
||||
candle_dev_enter_dfu ; Переход через отдельный DFU runtime-интерфейс того же физического USB-адаптера.
|
||||
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
|
||||
candle_channel_start_configured ; Точная маска запуска с управляемыми аппаратными метками.
|
||||
candle_channel_get_data_capabilities ; Пределы тайминга быстрой фазы CAN FD.
|
||||
candle_channel_set_termination ; Переключение встроенного терминатора.
|
||||
candle_channel_get_termination ; Чтение состояния встроенного терминатора.
|
||||
candle_channel_get_diagnostics ; Состояние и счётчики ошибок CAN.
|
||||
candle_channel_set_data_timing ; Тайминг фазы данных CAN FD.
|
||||
candle_fd_frame_send ; Отправка полного кадра CAN FD до 64 байт.
|
||||
candle_fd_frame_read ; Приём FD и Classic без обрезания данных.
|
||||
candle_fd_frame_type ; Различение RX, TX echo и ошибок FD.
|
||||
candle_fd_frame_id ; Получение CAN ID из структуры FD.
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
|
||||
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_get_product(candle_handle hdev, wchar_t *product, uint32_t capacity); /* Возвращает USB Product String в UTF-16; capacity включает завершающий ноль и измеряется в wchar_t. */
|
||||
bool __stdcall DLL candle_dev_open(candle_handle hdev);
|
||||
uint32_t __stdcall DLL candle_dev_enter_dfu(const wchar_t *can_path, uint8_t status[6]); /* Переводит выбранный физический адаптер в DFU и возвращает Win32-код плюс ответ DFU_GETSTATUS. */
|
||||
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_channel_start_configured(candle_handle hdev, uint8_t ch, uint32_t flags); /* Запускаем с точной маской без принудительного включения аппаратных меток. */
|
||||
bool __stdcall DLL candle_channel_get_data_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap); /* Возвращаем пределы тайминга быстрой фазы FD. */
|
||||
bool __stdcall DLL candle_channel_set_termination(candle_handle hdev, uint8_t ch, uint32_t state); /* Состояние терминатора: 0 — выключен, 1 — включён. */
|
||||
bool __stdcall DLL candle_channel_get_termination(candle_handle hdev, uint8_t ch, uint32_t *state); /* Чтение не меняет подключение встроенного резистора. */
|
||||
bool __stdcall DLL candle_channel_get_diagnostics(candle_handle hdev, uint8_t ch, uint32_t *state, uint32_t *rxerr, uint32_t *txerr); /* Передаём состояние CAN и оба счётчика ошибок без потери данных ответа. */
|
||||
|
||||
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
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
|
||||
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,
|
||||
CANDLE_BREQ_BT_CONST_EXT = 11, /* Расширенные пределы номинальной и быстрой фазы согласно gs_usb. */
|
||||
CANDLE_BREQ_SET_TERMINATION = 12, /* Запрос gs_usb 12 переключает встроенный резистор окончания шины. */
|
||||
CANDLE_BREQ_GET_TERMINATION = 13, /* Запрос gs_usb 13 читает текущее состояние терминатора. */
|
||||
CANDLE_BREQ_GET_STATE = 14, /* Стандартный GET_STATE имеет номер 14; номер 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;
|
||||
}
|
||||
|
||||
bool candle_ctrl_data_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *cap) /* Структура gs_usb BT_CONST_EXT содержит десять общих и восемь быстрых полей uint32. */
|
||||
{ /* Используем массив, поскольку протокол содержит только однородные 32-битные значения без отступов. */
|
||||
uint32_t fields[18] = {0}; /* 18 полей по 4 байта образуют стандартный ответ размером 72 байта. */
|
||||
bool rc = usb_control_msg(dev->winUSBHandle, CANDLE_BREQ_BT_CONST_EXT, USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE, channel, 0, fields, sizeof(fields)); /* Запрашиваем пределы именно выбранного CAN-канала. */
|
||||
if (rc) { /* Выходная структура должна меняться только после успешного USB-ответа. */
|
||||
memcpy(cap, fields, 2 * sizeof(uint32_t)); /* Маска функций и частота CAN общие для обеих фаз. */
|
||||
memcpy(&cap->tseg1_min, &fields[10], 8 * sizeof(uint32_t)); /* Поля DTSEG1…DBRP_INC заменяют номинальные пределы структуры capability. */
|
||||
} /* При отказе вызывающий код получит false и не использует нулевые пределы. */
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_BITTIMING_CONST; /* Сохраняем диагностический код чтения аппаратных ограничений. */
|
||||
return rc; /* Успех означает наличие полной структуры пределов быстрой фазы. */
|
||||
} /* Завершение запроса расширенных возможностей FD. */
|
||||
|
||||
bool candle_ctrl_termination(candle_device_t *dev, uint8_t channel, uint32_t *state, bool write) /* Переключение терминатора использует стандартные SET=12 и GET=13. */
|
||||
{ /* Состояние передаётся одним 32-битным словом согласно протоколу gs_usb. */
|
||||
bool rc = usb_control_msg(dev->winUSBHandle, write ? CANDLE_BREQ_SET_TERMINATION : CANDLE_BREQ_GET_TERMINATION, (write ? USB_DIR_OUT : USB_DIR_IN)|USB_TYPE_VENDOR|USB_RECIP_INTERFACE, channel, 0, state, sizeof(*state)); /* Номер CAN-канала передаётся в wValue, а wIndex равен нулю по спецификации. */
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE; /* Ошибка настройки доступна Python через candle_dev_last_error. */
|
||||
return rc; /* Не выдаём успешное переключение при отказе USB-контроллера. */
|
||||
} /* Завершение управления встроенным окончанием шины. */
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
|
||||
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);
|
||||
bool candle_ctrl_data_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *cap); /* Читаем пределы быстрой фазы CAN FD отдельным запросом расширенной структуры. */
|
||||
bool candle_ctrl_termination(candle_device_t *dev, uint8_t channel, uint32_t *state, bool write); /* Передаём состояние терминатора: 0 — выключен, 1 — включён. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,26 @@
|
||||
# Сборка библиотеки ds18b20_ds2480. Состав исходников и публичные include-пути задают
|
||||
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
|
||||
# запускать их следует через CTest из каталога сборки.
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(ds18b20_ds2480 C)
|
||||
add_library(ds18b20_ds2480 STATIC ds2480.c ds18b20_ds2480.c)
|
||||
target_include_directories(ds18b20_ds2480 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_compile_features(ds18b20_ds2480 PUBLIC c_std_99)
|
||||
if(MSVC)
|
||||
target_compile_options(ds18b20_ds2480 PRIVATE /W4)
|
||||
else()
|
||||
target_compile_options(ds18b20_ds2480 PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
option(DS18B20_DS2480_BUILD_TESTS "Build host tests" ON)
|
||||
if(DS18B20_DS2480_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_executable(test_ds18b20_ds2480 tests/test_ds18b20_ds2480.c)
|
||||
target_link_libraries(test_ds18b20_ds2480 PRIVATE ds18b20_ds2480)
|
||||
add_test(NAME ds18b20_ds2480 COMMAND test_ds18b20_ds2480)
|
||||
add_executable(test_ds2480_stm32f4
|
||||
ports/stm32f4/ds2480_stm32f4_hal.c ports/stm32f4/tests/test_port.c)
|
||||
target_include_directories(test_ds2480_stm32f4 PRIVATE
|
||||
. ports/stm32f4 ports/stm32f4/tests)
|
||||
add_test(NAME ds2480_stm32f4 COMMAND test_ds2480_stm32f4)
|
||||
endif()
|
||||
@@ -0,0 +1,145 @@
|
||||
# DS18B20 через DS2480B
|
||||
|
||||
Переносимая библиотека C99: поиск DS18B20 на общей шине, температура,
|
||||
разрешение 9–12 бит, TH/TL и сохранение в EEPROM через UART-мост DS2480B.
|
||||
Поддерживаются внешнее и паразитное питание: преобразование и запись EEPROM
|
||||
используют strong pullup, включаемый мостом сразу после последнего бита команды.
|
||||
|
||||
Ядро не зависит от HAL, CMSIS, ОС или существующей GPIO-библиотеки `ds18b20`.
|
||||
Динамической памяти и глобального состояния нет. Каждый UART-мост имеет свой
|
||||
`ds2480`, каждый обход — свой `ds2480_search`.
|
||||
|
||||
```text
|
||||
Приложение → ds18b20_ds2480 → ds2480 → callbacks UART/задержки → платформа
|
||||
```
|
||||
|
||||
## Файлы
|
||||
|
||||
| Файл | Назначение | Зависимости |
|
||||
|---|---|---|
|
||||
| `ds2480.h`, `ds2480.c` | Калибровка, reset, обмен битами/байтами, поиск ROM, CRC, strong pullup | `stdint.h`, callbacks |
|
||||
| `ds18b20_ds2480.h`, `ds18b20_ds2480.c` | Команды термометра, проверка scratchpad, знаковая температура | `ds2480`, `string.h` |
|
||||
| `examples/read_first.c` | Полный цикл для первого найденного DS18B20 | ядро, порт приложения |
|
||||
| `tests/test_ds18b20_ds2480.c` | Модель UART-моста и нескольких устройств 1-Wire | ядро, стандартная библиотека C |
|
||||
|
||||
## Контракт порта
|
||||
|
||||
```c
|
||||
int prepare(void *user);
|
||||
int write(void *user, const uint8_t *data, uint32_t size, uint32_t timeout_ms);
|
||||
int read(void *user, uint8_t *data, uint32_t size, uint32_t timeout_ms);
|
||||
void delay_ms(void *user, uint32_t ms);
|
||||
```
|
||||
|
||||
Первые три callbacks возвращают `0` при успехе, иначе ошибку.
|
||||
`prepare` аппаратно сбрасывает DS2480B или формирует UART BREAK не короче 2 мс,
|
||||
настраивает **9600 бод, 8N1**, выдерживает минимум 2 мс после сброса и очищает RX
|
||||
и ошибки UART. Эта функция должна иметь собственный конечный таймаут.
|
||||
Нельзя просто посылать `C1` работающему мосту вместо сброса.
|
||||
|
||||
`write` передаёт ровно `size` байтов и ждёт окончания передачи; `read` получает
|
||||
ровно `size` байтов. Обе операции ограничены `timeout_ms`; при частичном обмене
|
||||
возвращается ошибка. Приём должен работать уже во время передачи: ответ может
|
||||
появиться до вызова `read`. `delay_ms` не должна возвращаться раньше заданного
|
||||
времени (учтите округление системного тика).
|
||||
|
||||
Управлять одним UART извне одновременно с библиотекой нельзя. В RTOS блокировка
|
||||
нужна на всю операцию верхнего уровня, включая поиск и ожидание преобразования.
|
||||
Тайминги 1-Wire формирует мост, запрещать прерывания на время слотов не требуется.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
Функции `prepare`, `write`, `read`, `delay_ms` и `uart_context` предоставляет плата.
|
||||
Следующий код располагается внутри функции приложения:
|
||||
|
||||
```c
|
||||
ds2480 bus;
|
||||
ds2480_search search = {{0}, 0, 0};
|
||||
ds2480_port port = {uart_context, prepare, write, read, delay_ms};
|
||||
int16_t raw;
|
||||
ds2480_status status = ds2480_init(&bus, &port, 20);
|
||||
if (status != DS2480_OK) return;
|
||||
status = ds18b20_ds2480_next(&bus, &search);
|
||||
if (status != DS2480_OK) return;
|
||||
status = ds18b20_ds2480_convert(&bus, search.rom);
|
||||
if (status != DS2480_OK) return;
|
||||
status = ds18b20_ds2480_temperature(&bus, search.rom, &raw);
|
||||
if (status != DS2480_OK) return;
|
||||
float celsius = raw / 16.0f;
|
||||
(void)celsius;
|
||||
```
|
||||
|
||||
Вызывайте `next` повторно с тем же курсором до `DS2480_DONE`, сохраняя каждый
|
||||
найденный ROM в памяти приложения. Лимита количества датчиков в ядре нет.
|
||||
Пустая шина возвращает `DS2480_NO_PRESENCE`; обрыв поиска и ошибки CRC не
|
||||
маскируются под успешное завершение. Для нового обхода обнулите курсор.
|
||||
|
||||
`convert(bus, NULL)` одновременно запускает все устройства — допустимо только
|
||||
на шине исключительно с DS18B20. Затем читайте каждое по ROM. Для паразитного
|
||||
питания суммарный ток должен укладываться в возможности моста; при необходимости
|
||||
преобразуйте по одному датчику. Преобразование блокирует вызов на 750 мс плюс
|
||||
UART-обмен. EEPROM использует выдержку 12 мс, её содержимое после перезапуска
|
||||
этой функцией не проверяется. Чтение температуры само преобразование не запускает.
|
||||
Значение 85 °C после включения нельзя отличить от настоящих 85 °C без завершённого
|
||||
преобразования; не считайте его заведомой ошибкой.
|
||||
|
||||
## Ошибки и ограничения
|
||||
|
||||
- `IO` — таймаут/ошибка порта, `PROTOCOL` — неожиданный ответ DS2480B.
|
||||
После них экземпляр не готов к работе: повторите `ds2480_init`.
|
||||
- `SHORT` — короткое замыкание, `NO_PRESENCE` — нет presence.
|
||||
- `CRC` — неверная контрольная сумма, `DATA` — недопустимые данные или
|
||||
неподтверждённая запись. Выход температуры/scratchpad при ошибке не меняется.
|
||||
- При ошибке во время strong pullup ядро вызывает `prepare`, чтобы прекратить
|
||||
импульс, и остаётся неготовым. При отказе самого порта снятие питания гарантировать
|
||||
невозможно; восстановление UART/моста остаётся задачей приложения.
|
||||
|
||||
Используется стандартная скорость 1-Wire, UART 9600 бод. Байты передаются в
|
||||
Data Mode с экранированием E3, reset/поиск/strong pullup — в Command Mode.
|
||||
Переключения режима выполняет библиотека. Повышенные скорости UART,
|
||||
Overdrive и Search Accelerator пока не реализованы. Параметры
|
||||
таймингов длинной линии остаются заводскими. Аппаратная проверка обязательна
|
||||
для выбранной топологии/нагрузки. Протокол проверен по документации **DS2480B**;
|
||||
старые ревизии DS2480 без суффикса B отдельно не проверялись.
|
||||
|
||||
## Подключение и тесты
|
||||
|
||||
Добавьте `ds2480.c`, `ds18b20_ds2480.c` и путь к заголовкам в сборку прошивки.
|
||||
Номер UART, GPIO и библиотеку платформы выбирает приложение. Порт [STM32F407 / STM32F4 HAL](ports/stm32f4/README.md) входит в библиотеку.
|
||||
|
||||
```cmake
|
||||
set(DS18B20_DS2480_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(third_party/templates/c/ds18b20-ds2480)
|
||||
target_link_libraries(firmware PRIVATE ds18b20_ds2480)
|
||||
```
|
||||
|
||||
Отдельная сборка хостовых тестов:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build
|
||||
cmake --build build --config Debug
|
||||
ctest --test-dir build -C Debug --output-on-failure
|
||||
```
|
||||
|
||||
Без CMake, из каталога библиотеки:
|
||||
|
||||
```sh
|
||||
clang -std=c99 -Wall -Wextra -Wpedantic -Werror -I . ds2480.c ds18b20_ds2480.c tests/test_ds18b20_ds2480.c -o build/test.exe
|
||||
./build/test.exe
|
||||
```
|
||||
|
||||
Тесты моделируют калибровку без ответа, несколько ROM с развилками, другие
|
||||
семейства, адресацию, все разрешения, отрицательную температуру, CRC, пустую
|
||||
и замкнутую шину, ошибки транспорта, strong pullup и восстановление после ошибок.
|
||||
Это программная модель; испытания на физическом DS2480B пока не проводились.
|
||||
|
||||
## Использование
|
||||
|
||||
Добавлена в сабмодуль `templates` проекта `john103C6T6NewVer`, ветка `ds2480`.
|
||||
Подключена к опросу climate через USART6 PC6/PC7; администратор выбирает GPIO или DS2480.
|
||||
Ожидание преобразования в climate неблокирующее: `ds2480_power_begin/end`.
|
||||
|
||||
## Источники
|
||||
|
||||
- [DS2480B datasheet, таблицы команд и ответов](https://www.analog.com/media/en/technical-documentation/data-sheets/ds2480b.pdf)
|
||||
- [DS18B20 datasheet, команды, питание и scratchpad](https://www.analog.com/media/en/technical-documentation/data-sheets/ds18b20.pdf)
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Команды DS18B20 поверх интерфейса DS2480B: адресация ROM, преобразование температуры и
|
||||
* scratchpad. Транспортный статус следует проверять до использования результата; сырая
|
||||
* температура сохраняет знаковое представление датчика.
|
||||
*/
|
||||
|
||||
#include "ds18b20_ds2480.h"
|
||||
#include <string.h>
|
||||
|
||||
static ds2480_status write_byte(ds2480 *bus, uint8_t value)
|
||||
{
|
||||
uint8_t echoed;
|
||||
ds2480_status status = ds2480_byte(bus, value, &echoed);
|
||||
if (status != DS2480_OK) return status;
|
||||
return echoed == value ? DS2480_OK : DS2480_DATA;
|
||||
}
|
||||
|
||||
static ds2480_status select_rom(ds2480 *bus, const uint8_t *rom)
|
||||
{
|
||||
uint8_t i;
|
||||
ds2480_status status;
|
||||
if (rom && (rom[0] != 0x28 || ds2480_crc8(rom, 8))) return DS2480_ARGUMENT;
|
||||
status = ds2480_reset(bus);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, rom ? 0x55 : 0xCC);
|
||||
if (status != DS2480_OK || !rom) return status;
|
||||
for (i = 0; i < 8; ++i) {
|
||||
status = write_byte(bus, rom[i]);
|
||||
if (status != DS2480_OK) return status;
|
||||
}
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds18b20_ds2480_next(ds2480 *bus, ds2480_search *search)
|
||||
{
|
||||
ds2480_status status;
|
||||
do {
|
||||
status = ds2480_search_next(bus, search);
|
||||
if (status != DS2480_OK) return status;
|
||||
} while (search->rom[0] != 0x28);
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds18b20_ds2480_convert(ds2480 *bus, const uint8_t rom[8])
|
||||
{
|
||||
ds2480_status status = select_rom(bus, rom);
|
||||
if (status != DS2480_OK) return status;
|
||||
return ds2480_power_byte(bus, 0x44, 750);
|
||||
}
|
||||
|
||||
ds2480_status ds18b20_ds2480_read(ds2480 *bus, const uint8_t rom[8], uint8_t scratchpad[9])
|
||||
{
|
||||
uint8_t data[9], i;
|
||||
ds2480_status status;
|
||||
if (!rom || !scratchpad) return DS2480_ARGUMENT;
|
||||
status = select_rom(bus, rom);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, 0xBE);
|
||||
if (status != DS2480_OK) return status;
|
||||
for (i = 0; i < 9; ++i) {
|
||||
status = ds2480_byte(bus, 0xFF, &data[i]);
|
||||
if (status != DS2480_OK) return status;
|
||||
}
|
||||
if (ds2480_crc8(data, 9)) return DS2480_CRC;
|
||||
/* All-zero data has valid CRC but cannot be a DS18B20 scratchpad. */
|
||||
if ((data[4] & 0x9F) != 0x1F) return DS2480_DATA;
|
||||
memcpy(scratchpad, data, sizeof data);
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds18b20_ds2480_temperature(ds2480 *bus, const uint8_t rom[8], int16_t *raw)
|
||||
{
|
||||
uint8_t data[9], undefined;
|
||||
uint16_t value;
|
||||
int32_t signed_value;
|
||||
ds2480_status status;
|
||||
if (!raw) return DS2480_ARGUMENT;
|
||||
status = ds18b20_ds2480_read(bus, rom, data);
|
||||
if (status != DS2480_OK) return status;
|
||||
undefined = (uint8_t)(3 - ((data[4] >> 5) & 3));
|
||||
value = (uint16_t)((uint16_t)data[0] | ((uint16_t)data[1] << 8));
|
||||
value &= (uint16_t)~((1U << undefined) - 1U);
|
||||
signed_value = value & 0x8000 ? (int32_t)value - 65536 : (int32_t)value;
|
||||
if (signed_value < -55 * 16 || signed_value > 125 * 16) return DS2480_DATA;
|
||||
*raw = (int16_t)signed_value;
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds18b20_ds2480_configure(ds2480 *bus, const uint8_t rom[8],
|
||||
int8_t th, int8_t tl, uint8_t resolution, uint8_t save)
|
||||
{
|
||||
uint8_t data[9], config;
|
||||
ds2480_status status;
|
||||
if (!rom || resolution < 9 || resolution > 12) return DS2480_ARGUMENT;
|
||||
config = (uint8_t)(0x1F | ((resolution - 9) << 5));
|
||||
status = select_rom(bus, rom);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, 0x4E);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, (uint8_t)th);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, (uint8_t)tl);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = write_byte(bus, config);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = ds18b20_ds2480_read(bus, rom, data);
|
||||
if (status != DS2480_OK) return status;
|
||||
if (data[2] != (uint8_t)th || data[3] != (uint8_t)tl || data[4] != config)
|
||||
return DS2480_DATA;
|
||||
if (!save) return DS2480_OK;
|
||||
status = select_rom(bus, rom);
|
||||
if (status != DS2480_OK) return status;
|
||||
return ds2480_power_byte(bus, 0x48, 12);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Команды DS18B20 поверх интерфейса DS2480B: адресация ROM, преобразование температуры и
|
||||
* scratchpad. Транспортный статус следует проверять до использования результата; сырая
|
||||
* температура сохраняет знаковое представление датчика.
|
||||
*/
|
||||
|
||||
#ifndef DS18B20_DS2480_H
|
||||
#define DS18B20_DS2480_H
|
||||
#include "ds2480.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Find next DS18B20, skipping other families; same cursor rules as bridge. */
|
||||
ds2480_status ds18b20_ds2480_next(ds2480 *bus, ds2480_search *search);
|
||||
/** Start conversion and hold strong pullup for 750 ms (all resolutions).
|
||||
* rom=NULL broadcasts: use only on a bus containing exclusively DS18B20.
|
||||
* Supports external and parasite power within the bridge's current budget.
|
||||
*/
|
||||
ds2480_status ds18b20_ds2480_convert(ds2480 *bus, const uint8_t rom[8]);
|
||||
/** Read and validate all 9 scratchpad bytes. Output is unchanged on error.
|
||||
* rom is mandatory, must have family 0x28 and valid CRC.
|
||||
*/
|
||||
ds2480_status ds18b20_ds2480_read(ds2480 *bus, const uint8_t rom[8], uint8_t scratchpad[9]);
|
||||
/** Read last conversion in signed 1/16 Celsius units; masks undefined bits
|
||||
* for 9/10/11-bit resolution. Call convert first: power-on 85 C is ambiguous.
|
||||
* Output is unchanged on error. Does not start a conversion itself.
|
||||
*/
|
||||
ds2480_status ds18b20_ds2480_temperature(ds2480 *bus, const uint8_t rom[8], int16_t *raw);
|
||||
/** Set TH/TL and resolution (9..12); verify by reading back.
|
||||
* save!=0 copies to EEPROM with 12 ms strong pullup; avoid frequent writes.
|
||||
*/
|
||||
ds2480_status ds18b20_ds2480_configure(ds2480 *bus, const uint8_t rom[8],
|
||||
int8_t th, int8_t tl, uint8_t resolution, uint8_t save);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* UART-мост DS2480B для операций 1-Wire: сброс, обмен битами и байтами, поиск устройств и
|
||||
* управление питанием. После ошибки связи состояние готовности сбрасывается, чтобы следующий
|
||||
* обмен не продолжался в неизвестном режиме моста.
|
||||
*/
|
||||
|
||||
#include "ds2480.h"
|
||||
|
||||
static ds2480_status fault(ds2480 *bus, ds2480_status status)
|
||||
{
|
||||
bus->ready = 0;
|
||||
return status;
|
||||
}
|
||||
|
||||
static ds2480_status exchange(ds2480 *bus, uint8_t command, uint8_t *reply)
|
||||
{
|
||||
uint8_t mode = 0xE3;
|
||||
if (!bus || !reply) return DS2480_ARGUMENT;
|
||||
if (!bus->ready) return DS2480_NOT_READY;
|
||||
if (bus->power_active && command != 0xF1) return DS2480_BUSY;
|
||||
if (bus->data_mode) {
|
||||
if (bus->port.write(bus->port.user, &mode, 1, bus->timeout_ms))
|
||||
return fault(bus, DS2480_IO);
|
||||
bus->data_mode = 0;
|
||||
}
|
||||
if (bus->port.write(bus->port.user, &command, 1, bus->timeout_ms) ||
|
||||
bus->port.read(bus->port.user, reply, 1, bus->timeout_ms))
|
||||
return fault(bus, DS2480_IO);
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout_ms)
|
||||
{
|
||||
uint8_t calibration = 0xC1, reply;
|
||||
ds2480_status status;
|
||||
ds2480_port copy;
|
||||
if (!bus) return DS2480_ARGUMENT;
|
||||
if (!port || !port->prepare || !port->write || !port->read ||
|
||||
!port->delay_ms || !timeout_ms) {
|
||||
bus->ready = 0;
|
||||
return DS2480_ARGUMENT;
|
||||
}
|
||||
copy = *port; /* Also permit reinitialization with &bus->port. */
|
||||
bus->ready = 0;
|
||||
bus->power_active = 0;
|
||||
bus->data_mode = 0;
|
||||
bus->port = copy;
|
||||
bus->timeout_ms = timeout_ms;
|
||||
if (copy.prepare(copy.user) ||
|
||||
copy.write(copy.user, &calibration, 1, timeout_ms)) return DS2480_IO;
|
||||
/* First C1 calibrates only: there is NO response byte. */
|
||||
copy.delay_ms(copy.user, 2);
|
||||
bus->ready = 1;
|
||||
/* Strong pullup duration = infinite, command-mode operations only. */
|
||||
status = exchange(bus, 0x3F, &reply);
|
||||
if (status != DS2480_OK) return status;
|
||||
if (reply != 0x3E) return fault(bus, DS2480_PROTOCOL);
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_reset(ds2480 *bus)
|
||||
{
|
||||
uint8_t reply;
|
||||
ds2480_status status = exchange(bus, 0xC1, &reply);
|
||||
if (status != DS2480_OK) return status;
|
||||
/* DS2480B revision pattern from table 2; bit 5 is unspecified. */
|
||||
if ((reply & 0xDC) != 0xCC) return fault(bus, DS2480_PROTOCOL);
|
||||
switch (reply & 3) {
|
||||
case 0: return DS2480_SHORT;
|
||||
case 3: return DS2480_NO_PRESENCE;
|
||||
default: return DS2480_OK; /* Ordinary or alarming presence. */
|
||||
}
|
||||
}
|
||||
|
||||
static ds2480_status slot(ds2480 *bus, uint8_t bit, uint8_t power, uint8_t *received)
|
||||
{
|
||||
uint8_t reply, command = (uint8_t)(0x81 | (bit ? 0x10 : 0) | (power ? 2 : 0));
|
||||
ds2480_status status = exchange(bus, command, &reply);
|
||||
if (status != DS2480_OK) return status;
|
||||
if ((reply & 0xFC) != (command & 0xFC) ||
|
||||
((reply & 3) != 0 && (reply & 3) != 3))
|
||||
return fault(bus, DS2480_PROTOCOL);
|
||||
*received = reply & 1;
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received)
|
||||
{
|
||||
if (!received) return DS2480_ARGUMENT;
|
||||
return slot(bus, bit, 0, received);
|
||||
}
|
||||
|
||||
ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received)
|
||||
{
|
||||
uint8_t mode = 0xE1, reply;
|
||||
if (!bus || !received) return DS2480_ARGUMENT;
|
||||
if (!bus->ready) return DS2480_NOT_READY;
|
||||
if (bus->power_active) return DS2480_BUSY;
|
||||
if (!bus->data_mode) {
|
||||
if (bus->port.write(bus->port.user, &mode, 1, bus->timeout_ms))
|
||||
return fault(bus, DS2480_IO);
|
||||
bus->data_mode = 1;
|
||||
}
|
||||
if (bus->port.write(bus->port.user, &value, 1, bus->timeout_ms))
|
||||
return fault(bus, DS2480_IO);
|
||||
if (value == 0xE3 && bus->port.write(bus->port.user, &value, 1, bus->timeout_ms))
|
||||
return fault(bus, DS2480_IO);
|
||||
if (bus->port.read(bus->port.user, &reply, 1, bus->timeout_ms))
|
||||
return fault(bus, DS2480_IO);
|
||||
*received = reply;
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_power_begin(ds2480 *bus, uint8_t value)
|
||||
{
|
||||
uint8_t i, bit, echoed = 0;
|
||||
ds2480_status status;
|
||||
if (!bus) return DS2480_ARGUMENT;
|
||||
for (i = 0; i < 8; ++i) {
|
||||
status = slot(bus, (uint8_t)((value >> i) & 1), (uint8_t)(i == 7), &bit);
|
||||
if (status != DS2480_OK) {
|
||||
/* A lost reply may leave an infinite pulse active: reset hardware. */
|
||||
if (i == 7 && !bus->ready) (void)bus->port.prepare(bus->port.user);
|
||||
return status;
|
||||
}
|
||||
echoed |= (uint8_t)(bit << i);
|
||||
}
|
||||
bus->power_active = 1;
|
||||
if (echoed != value) {
|
||||
status = ds2480_power_end(bus);
|
||||
return status == DS2480_OK ? DS2480_DATA : status;
|
||||
}
|
||||
return DS2480_OK;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_power_end(ds2480 *bus)
|
||||
{
|
||||
uint8_t reply;
|
||||
ds2480_status status;
|
||||
if (!bus) return DS2480_ARGUMENT;
|
||||
if (!bus->ready) return DS2480_NOT_READY;
|
||||
if (!bus->power_active) return DS2480_OK;
|
||||
status = exchange(bus, 0xF1, &reply);
|
||||
if (status == DS2480_OK && reply != 0xEC && reply != 0xEF)
|
||||
status = fault(bus, DS2480_PROTOCOL);
|
||||
if (status != DS2480_OK) (void)bus->port.prepare(bus->port.user);
|
||||
bus->power_active = 0;
|
||||
return status;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms)
|
||||
{
|
||||
ds2480_status status;
|
||||
if (!hold_ms || hold_ms > 1000) return DS2480_ARGUMENT;
|
||||
status = ds2480_power_begin(bus, value);
|
||||
if (status != DS2480_OK) return status;
|
||||
bus->port.delay_ms(bus->port.user, hold_ms);
|
||||
return ds2480_power_end(bus);
|
||||
}
|
||||
|
||||
uint8_t ds2480_crc8(const uint8_t *data, uint32_t size)
|
||||
{
|
||||
uint8_t crc = 0, bit;
|
||||
uint32_t i;
|
||||
for (i = 0; i < size; ++i) {
|
||||
crc ^= data[i];
|
||||
for (bit = 0; bit < 8; ++bit)
|
||||
crc = (uint8_t)((crc >> 1) ^ ((crc & 1) ? 0x8C : 0));
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
ds2480_status ds2480_search_next(ds2480 *bus, ds2480_search *search)
|
||||
{
|
||||
ds2480_search next;
|
||||
ds2480_status status;
|
||||
uint8_t pos, a, b, direction, ignored, last_zero = 0;
|
||||
if (!bus || !search || search->discrepancy > 64) return DS2480_ARGUMENT;
|
||||
if (search->done) return DS2480_DONE;
|
||||
next = *search;
|
||||
status = ds2480_reset(bus);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = ds2480_byte(bus, 0xF0, &ignored);
|
||||
if (status != DS2480_OK) return status;
|
||||
for (pos = 1; pos <= 64; ++pos) {
|
||||
uint8_t index = (uint8_t)((pos - 1) / 8);
|
||||
uint8_t mask = (uint8_t)(1U << ((pos - 1) % 8));
|
||||
status = ds2480_bit(bus, 1, &a);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = ds2480_bit(bus, 1, &b);
|
||||
if (status != DS2480_OK) return status;
|
||||
if (a && b) return DS2480_DATA; /* Devices disappeared mid-search. */
|
||||
if (a != b) direction = a;
|
||||
else {
|
||||
direction = pos < search->discrepancy ? (uint8_t)!!(search->rom[index] & mask)
|
||||
: (uint8_t)(pos == search->discrepancy);
|
||||
if (!direction) last_zero = pos;
|
||||
}
|
||||
if (direction) next.rom[index] |= mask;
|
||||
else next.rom[index] &= (uint8_t)~mask;
|
||||
status = ds2480_bit(bus, direction, &ignored);
|
||||
if (status != DS2480_OK) return status;
|
||||
}
|
||||
if (!next.rom[0]) return DS2480_DATA;
|
||||
if (ds2480_crc8(next.rom, 8)) return DS2480_CRC;
|
||||
next.discrepancy = last_zero;
|
||||
next.done = (uint8_t)(last_zero == 0);
|
||||
*search = next;
|
||||
return DS2480_OK;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* UART-мост DS2480B для операций 1-Wire: сброс, обмен битами и байтами, поиск устройств и
|
||||
* управление питанием. После ошибки связи состояние готовности сбрасывается, чтобы следующий
|
||||
* обмен не продолжался в неизвестном режиме моста.
|
||||
*/
|
||||
|
||||
#ifndef DS2480_H
|
||||
#define DS2480_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
DS2480_OK = 0, DS2480_DONE, DS2480_NO_PRESENCE, DS2480_SHORT,
|
||||
DS2480_IO, DS2480_PROTOCOL, DS2480_CRC, DS2480_ARGUMENT,
|
||||
DS2480_NOT_READY, DS2480_DATA, DS2480_BUSY
|
||||
} ds2480_status;
|
||||
|
||||
/** Blocking UART callbacks: 0 = success, nonzero = error/timeout.
|
||||
* prepare resets the bridge (BREAK >= 2 ms or hardware reset), sets UART
|
||||
* to 9600 8N1, waits >= 2 ms after reset, clears RX/errors before returning.
|
||||
* write waits for physical TX completion; read receives exactly size bytes.
|
||||
* Both enforce timeout_ms. RX must remain enabled while transmitting.
|
||||
* delay_ms waits AT LEAST the requested duration. No callback may reenter.
|
||||
*/
|
||||
typedef struct {
|
||||
void *user;
|
||||
int (*prepare)(void *user);
|
||||
int (*write)(void *user, const uint8_t *data, uint32_t size, uint32_t timeout_ms);
|
||||
int (*read)(void *user, uint8_t *data, uint32_t size, uint32_t timeout_ms);
|
||||
void (*delay_ms)(void *user, uint32_t ms);
|
||||
} ds2480_port;
|
||||
|
||||
/** One instance per UART/bridge; serialize access for the whole operation. */
|
||||
typedef struct {
|
||||
ds2480_port port;
|
||||
uint32_t timeout_ms;
|
||||
uint8_t ready;
|
||||
uint8_t power_active;
|
||||
uint8_t data_mode;
|
||||
} ds2480;
|
||||
|
||||
/** Independent ROM search cursor. Zero-initialize before each enumeration. */
|
||||
typedef struct {
|
||||
uint8_t rom[8];
|
||||
uint8_t discrepancy;
|
||||
uint8_t done;
|
||||
} ds2480_search;
|
||||
|
||||
/** Reset/calibrate DS2480B and verify configuration. Empty bus is allowed.
|
||||
* On IO/protocol errors call init again before further transactions.
|
||||
*/
|
||||
ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout_ms);
|
||||
/** Standard-speed reset; distinguish short, empty bus and transport failure. */
|
||||
ds2480_status ds2480_reset(ds2480 *bus);
|
||||
/** Exchange one slot (write 1 to read); received must be non-NULL. */
|
||||
ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received);
|
||||
/** Exchange a byte in Data Mode; escapes E3 and preserves one reply per byte. */
|
||||
ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received);
|
||||
/** Write a byte and start strong pullup immediately after its last slot.
|
||||
* Blocks for hold_ms (1..1000), then terminates the pulse and consumes reply.
|
||||
*/
|
||||
ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms);
|
||||
/** Start indefinite strong pullup after the final bit; returns immediately
|
||||
* after UART exchange. Until power_end, other bus operations return BUSY.
|
||||
* Application MUST call power_end after the sensor's required hold time,
|
||||
* or init to cancel/recover. No internal timer/interrupt releases the pulse.
|
||||
*/
|
||||
ds2480_status ds2480_power_begin(ds2480 *bus, uint8_t value);
|
||||
/** Release strong pullup, consume its reply. Idempotent when ready/idle. */
|
||||
ds2480_status ds2480_power_end(ds2480 *bus);
|
||||
/** CRC8 Dallas/Maxim. data must address size bytes (NULL allowed for size=0). */
|
||||
uint8_t ds2480_crc8(const uint8_t *data, uint32_t size);
|
||||
/** Search ALL families. OK yields ROM with valid CRC; DONE ends enumeration.
|
||||
* Cursor changes only on success. After an error retry or zero it to restart.
|
||||
*/
|
||||
ds2480_status ds2480_search_next(ds2480 *bus, ds2480_search *search);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Минимальный блокирующий пример: подготовить мост DS2480B, найти первый датчик, запустить
|
||||
* преобразование и прочитать температуру. Обратные вызовы UART и задержек предоставляет плата;
|
||||
* ожидание преобразования входит во время вызова.
|
||||
*/
|
||||
|
||||
#include "ds18b20_ds2480.h"
|
||||
|
||||
/* board_port supplies the four callbacks documented in ds2480.h.
|
||||
* Blocking example: includes bridge reset, scan and 750 ms conversion.
|
||||
*/
|
||||
ds2480_status ds18b20_example_read_first(const ds2480_port *board_port, int16_t *raw)
|
||||
{
|
||||
ds2480 bus;
|
||||
ds2480_search search = {{0}, 0, 0};
|
||||
ds2480_status status;
|
||||
if (!raw) return DS2480_ARGUMENT;
|
||||
status = ds2480_init(&bus, board_port, 20);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = ds18b20_ds2480_next(&bus, &search);
|
||||
if (status != DS2480_OK) return status;
|
||||
status = ds18b20_ds2480_convert(&bus, search.rom);
|
||||
if (status != DS2480_OK) return status;
|
||||
return ds18b20_ds2480_temperature(&bus, search.rom, raw);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# STM32F407 / STM32F4 HAL
|
||||
|
||||
Порт UART для `ds2480.c`. Платформа настраивает тактирование и GPIO, порт
|
||||
формирует BREAK через TX GPIO, восстанавливает UART 9600 8N1 и проверяет
|
||||
ошибки приёма. DMA и обработчики UART-прерываний не нужны. HAL tick должен
|
||||
работать; вызывать из прерывания или при запрещённых прерываниях нельзя.
|
||||
|
||||
```c
|
||||
ds2480 bridge;
|
||||
ds2480_port io;
|
||||
ds2480_stm32f4_hal platform = {&huart6, GPIOC, GPIO_PIN_6, GPIO_AF8_USART6};
|
||||
/* Before this point: enable GPIOC/USART6 clocks, configure PC6/PC7 as AF8. */
|
||||
if (ds2480_stm32f4_hal_bind(&platform, &io) != DS2480_OK) return;
|
||||
if (ds2480_init(&bridge, &io, 20) != DS2480_OK) return;
|
||||
```
|
||||
|
||||
В сборку добавляются `ds2480_stm32f4_hal.c`, ядро библиотеки, путь к этому
|
||||
каталогу и STM32F4 HAL/CMSIS. UART выделяется только для моста. Порт принимает
|
||||
по одному байту за операцию: ответ сохраняется в DR во время завершения TX,
|
||||
затем читается без сброса RX. FE/NE/ORE/PE означают ошибку обмена даже при RXNE.
|
||||
|
||||
## Подключение в climate F407VET6
|
||||
|
||||
| STM32 / питание | DS2480B |
|
||||
|---|---|
|
||||
| PC6, USART6_TX | TXD, вывод 7 (вход моста) |
|
||||
| PC7, USART6_RX | RXD, вывод 8 (выход моста) |
|
||||
| Общая земля | GND, вывод 1 |
|
||||
| +5 В | VDD, вывод 4; VPP, вывод 5; POL, вывод 6 |
|
||||
| DQ датчиков DS18B20 | 1-W, вывод 2 |
|
||||
|
||||
DS2480B работает от 5 В. Проверьте согласование логических уровней по
|
||||
электрическим характеристикам конкретной платы/модуля; не считайте питание
|
||||
DS2480B от 3,3 В допустимым. Для внешнего питания DS18B20 подключите VDD;
|
||||
при паразитном питании VDD датчика соединяется с GND. В обоих случаях общий GND.
|
||||
|
||||
PC6/PC7 выбраны в `climate` (AF8). UART1/2 используются Modbus, SDIO использует
|
||||
4-битную шину PC8..PC12/PD2. Пины для другой платы задаются её приложением.
|
||||
|
||||
## Неблокирующее питание датчиков
|
||||
|
||||
`ds2480_power_begin` посылает команду датчика с strong pullup на последнем
|
||||
слоте. Приложение возвращается в главный цикл, отсчитывает 750 мс для
|
||||
преобразования либо минимум 10 мс для EEPROM, затем вызывает
|
||||
`ds2480_power_end`. До завершения импульса остальной обмен возвращает BUSY.
|
||||
При отмене вызывайте `power_end`, при потере синхронизации — `ds2480_init`.
|
||||
|
||||
Используется в `climate_control_f407vet6_f4`: `ds2480_app.c` связывает порт
|
||||
из сабмодуля с существующим каталогом датчиков и диагностикой Modbus.
|
||||
|
||||
Источники: [STM32F407, таблица alternate functions](https://www.st.com/resource/en/datasheet/stm32f407ve.pdf),
|
||||
[DS2480B, выводы и UART](https://www.analog.com/media/en/technical-documentation/data-sheets/ds2480b.pdf).
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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)
|
||||
|
||||
static int prepare(void *user)
|
||||
{
|
||||
ds2480_stm32f4_hal *p = (ds2480_stm32f4_hal *)user;
|
||||
GPIO_InitTypeDef gpio = {0};
|
||||
/* Abort resets HAL states after timeouts and disables UART IRQ/DMA. */
|
||||
if (HAL_UART_Abort(p->uart) != HAL_OK) return -1;
|
||||
__HAL_UART_DISABLE(p->uart);
|
||||
HAL_GPIO_WritePin(p->tx_port, p->tx_pin, GPIO_PIN_RESET);
|
||||
gpio.Pin = p->tx_pin;
|
||||
gpio.Mode = GPIO_MODE_OUTPUT_PP;
|
||||
gpio.Pull = GPIO_NOPULL;
|
||||
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
|
||||
HAL_GPIO_Init(p->tx_port, &gpio);
|
||||
HAL_Delay(2);
|
||||
HAL_GPIO_WritePin(p->tx_port, p->tx_pin, GPIO_PIN_SET);
|
||||
HAL_Delay(2);
|
||||
gpio.Mode = GPIO_MODE_AF_PP;
|
||||
gpio.Alternate = p->tx_alternate;
|
||||
HAL_GPIO_Init(p->tx_port, &gpio);
|
||||
p->uart->Init.BaudRate = 9600;
|
||||
p->uart->Init.WordLength = UART_WORDLENGTH_8B;
|
||||
p->uart->Init.StopBits = UART_STOPBITS_1;
|
||||
p->uart->Init.Parity = UART_PARITY_NONE;
|
||||
p->uart->Init.Mode = UART_MODE_TX_RX;
|
||||
p->uart->Init.HwFlowCtl = UART_HWCONTROL_NONE;
|
||||
p->uart->Init.OverSampling = UART_OVERSAMPLING_16;
|
||||
if (HAL_UART_Init(p->uart) != HAL_OK) return -1;
|
||||
/* SR then DR: discard BREAK echo/stale RX and clear FE/NE/ORE/PE. */
|
||||
__HAL_UART_CLEAR_OREFLAG(p->uart);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int write_byte(void *user, const uint8_t *data, uint32_t size, uint32_t timeout)
|
||||
{
|
||||
ds2480_stm32f4_hal *p = (ds2480_stm32f4_hal *)user;
|
||||
if (!data || size != 1 || !timeout || timeout == HAL_MAX_DELAY) return -1;
|
||||
/* One reply fits in DR while HAL waits for TX complete. Never flush RX
|
||||
* here: a reply may already be present when HAL_UART_Transmit returns. */
|
||||
if (p->uart->Instance->SR & RX_ERRORS) return -1;
|
||||
return HAL_UART_Transmit(p->uart, data, 1, timeout) == HAL_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
static int read_byte(void *user, uint8_t *data, uint32_t size, uint32_t timeout)
|
||||
{
|
||||
ds2480_stm32f4_hal *p = (ds2480_stm32f4_hal *)user;
|
||||
uint32_t start, flags;
|
||||
if (!data || size != 1 || !timeout || timeout == HAL_MAX_DELAY) return -1;
|
||||
start = HAL_GetTick();
|
||||
for (;;) {
|
||||
flags = p->uart->Instance->SR;
|
||||
/* Test errors BEFORE reading DR, including when RXNE is already set. */
|
||||
if (flags & RX_ERRORS) {
|
||||
__HAL_UART_CLEAR_OREFLAG(p->uart);
|
||||
return -1;
|
||||
}
|
||||
if (flags & USART_SR_RXNE) {
|
||||
*data = (uint8_t)p->uart->Instance->DR;
|
||||
return 0;
|
||||
}
|
||||
if ((uint32_t)(HAL_GetTick() - start) >= timeout) return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static void delay_ms(void *user, uint32_t ms)
|
||||
{
|
||||
(void)user;
|
||||
HAL_Delay(ms);
|
||||
}
|
||||
|
||||
ds2480_status ds2480_stm32f4_hal_bind(ds2480_stm32f4_hal *p, ds2480_port *port)
|
||||
{
|
||||
if (!p || !port || !p->uart || !p->uart->Instance || !p->tx_port ||
|
||||
!p->tx_pin || (p->tx_pin & (p->tx_pin - 1U)) || p->tx_alternate > 15U)
|
||||
return DS2480_ARGUMENT;
|
||||
port->user = p;
|
||||
port->prepare = prepare;
|
||||
port->write = write_byte;
|
||||
port->read = read_byte;
|
||||
port->delay_ms = delay_ms;
|
||||
return DS2480_OK;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* UART-порт DS2480B для STM32F4. Подготовка линии и очистка ошибок USART отделены от команд
|
||||
* датчика; после сбоя обмена мост должен пройти предусмотренное восстановление до продолжения
|
||||
* работы с 1-Wire.
|
||||
*/
|
||||
|
||||
#ifndef DS2480_STM32F4_HAL_H
|
||||
#define DS2480_STM32F4_HAL_H
|
||||
|
||||
#include "ds2480.h"
|
||||
#include "stm32f4xx_hal.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Dedicated UART initialized by the board (clock, RX/TX AF, no IRQ/DMA).
|
||||
* tx_port/pin/alternate let prepare generate a real >=2 ms BREAK, including
|
||||
* recovery from a lost strong-pullup response. No reset pin is required.
|
||||
* GPIO clock must stay enabled. UART must not be used by any other service.
|
||||
*/
|
||||
typedef struct {
|
||||
UART_HandleTypeDef *uart;
|
||||
GPIO_TypeDef *tx_port;
|
||||
uint16_t tx_pin;
|
||||
uint32_t tx_alternate;
|
||||
} ds2480_stm32f4_hal;
|
||||
|
||||
/** Populate callbacks only; call ds2480_init afterwards to reset/calibrate.
|
||||
* Thread/main-loop use only, with running HAL tick and interrupts enabled.
|
||||
* UART baud rate is fixed at 9600 8N1; all transfers are single-byte.
|
||||
*/
|
||||
ds2480_status ds2480_stm32f4_hal_bind(ds2480_stm32f4_hal *context, ds2480_port *port);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Проверки 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
|
||||
#include <stdint.h>
|
||||
typedef struct { uint32_t SR, DR, CR1; } USART_TypeDef;
|
||||
typedef struct { uint32_t pin, level, mode; } GPIO_TypeDef;
|
||||
typedef struct { uint32_t BaudRate,WordLength,StopBits,Parity,Mode,HwFlowCtl,OverSampling; } UART_InitTypeDef;
|
||||
typedef struct { USART_TypeDef *Instance; UART_InitTypeDef Init; } UART_HandleTypeDef;
|
||||
typedef struct { uint32_t Pin, Mode, Pull, Speed, Alternate; } GPIO_InitTypeDef;
|
||||
typedef enum { HAL_OK, HAL_ERROR } HAL_StatusTypeDef;
|
||||
#define USART_SR_ORE 8U
|
||||
#define USART_SR_NE 4U
|
||||
#define USART_SR_FE 2U
|
||||
#define USART_SR_PE 1U
|
||||
#define USART_SR_RXNE 32U
|
||||
#define GPIO_PIN_RESET 0U
|
||||
#define GPIO_PIN_SET 1U
|
||||
#define GPIO_MODE_OUTPUT_PP 1U
|
||||
#define GPIO_MODE_AF_PP 2U
|
||||
#define GPIO_NOPULL 0U
|
||||
#define GPIO_SPEED_FREQ_HIGH 3U
|
||||
#define UART_WORDLENGTH_8B 0U
|
||||
#define UART_STOPBITS_1 0U
|
||||
#define UART_PARITY_NONE 0U
|
||||
#define UART_MODE_TX_RX 12U
|
||||
#define UART_HWCONTROL_NONE 0U
|
||||
#define UART_OVERSAMPLING_16 0U
|
||||
#define HAL_MAX_DELAY UINT32_MAX
|
||||
#define __HAL_UART_DISABLE(u) ((u)->Instance->CR1=0)
|
||||
void test_clear(UART_HandleTypeDef *u);
|
||||
#define __HAL_UART_CLEAR_OREFLAG(u) test_clear(u)
|
||||
HAL_StatusTypeDef HAL_UART_Abort(UART_HandleTypeDef *u);
|
||||
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *u);
|
||||
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *u,const uint8_t *p,uint16_t n,uint32_t timeout);
|
||||
void HAL_GPIO_WritePin(GPIO_TypeDef *g,uint16_t pin,uint32_t level);
|
||||
void HAL_GPIO_Init(GPIO_TypeDef *g,const GPIO_InitTypeDef *i);
|
||||
void HAL_Delay(uint32_t ms);
|
||||
uint32_t HAL_GetTick(void);
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Проверки port: фиксируют ожидаемое поведение на воспроизводимых входных данных.
|
||||
* Сценарии для навигации по файлу:
|
||||
* test_clear
|
||||
* Тестовые значения и подмены окружения относятся к проверке контракта; они не являются
|
||||
* настройками рабочего устройства.
|
||||
*/
|
||||
|
||||
#include "ds2480_stm32f4_hal.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static uint32_t now, low_ms, high_ms, clears;
|
||||
static GPIO_TypeDef gpio;
|
||||
static unsigned fail_init, fail_tx;
|
||||
void test_clear(UART_HandleTypeDef *u){u->Instance->SR=0;++clears;}
|
||||
HAL_StatusTypeDef HAL_UART_Abort(UART_HandleTypeDef *u){(void)u;return HAL_OK;}
|
||||
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *u)
|
||||
{assert(gpio.mode==GPIO_MODE_AF_PP && gpio.level==1);assert(u->Init.BaudRate==9600);return fail_init?HAL_ERROR:HAL_OK;}
|
||||
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *u,const uint8_t *p,uint16_t n,uint32_t timeout)
|
||||
{assert(n==1 && timeout==20);u->Instance->DR=*p;u->Instance->SR=USART_SR_RXNE;return fail_tx?HAL_ERROR:HAL_OK;}
|
||||
void HAL_GPIO_WritePin(GPIO_TypeDef *g,uint16_t pin,uint32_t level){g->pin=pin;g->level=level;}
|
||||
void HAL_GPIO_Init(GPIO_TypeDef *g,const GPIO_InitTypeDef *i){assert(i->Pin==64);g->mode=i->Mode;}
|
||||
void HAL_Delay(uint32_t ms){now+=ms;if(gpio.level)high_ms+=ms;else low_ms+=ms;}
|
||||
uint32_t HAL_GetTick(void){return now++;}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
USART_TypeDef regs={0};UART_HandleTypeDef uart={0};
|
||||
ds2480_stm32f4_hal ctx={&uart,&gpio,64,8};ds2480_port port;
|
||||
uint8_t byte=0xCD,rx=0;
|
||||
uart.Instance=®s;
|
||||
assert(ds2480_stm32f4_hal_bind(&ctx,&port)==DS2480_OK);
|
||||
assert(port.prepare(port.user)==0 && low_ms>=2 && high_ms>=2 && clears==1);
|
||||
assert(port.write(port.user,&byte,1,20)==0);
|
||||
assert(regs.SR & USART_SR_RXNE); /* Early reply must survive TX completion. */
|
||||
assert(port.read(port.user,&rx,1,20)==0 && rx==byte && clears==1);
|
||||
regs.SR=USART_SR_RXNE|USART_SR_FE;rx=0xA5;
|
||||
assert(port.read(port.user,&rx,1,20)!=0 && rx==0xA5 && clears==2);
|
||||
regs.SR=USART_SR_ORE;assert(port.write(port.user,&byte,1,20)!=0);
|
||||
regs.SR=0;now=UINT32_MAX-5;
|
||||
assert(port.read(port.user,&rx,1,20)!=0); /* Wrap-safe bounded timeout. */
|
||||
assert(port.write(port.user,&byte,2,20)!=0);
|
||||
assert(port.read(port.user,&rx,1,HAL_MAX_DELAY)!=0);
|
||||
fail_tx=1;assert(port.write(port.user,&byte,1,20)!=0);
|
||||
fail_init=1;assert(port.prepare(port.user)!=0 && gpio.level==1);
|
||||
ctx.tx_pin=3;assert(ds2480_stm32f4_hal_bind(&ctx,&port)==DS2480_ARGUMENT);
|
||||
puts("STM32F4 UART port: OK");return 0;
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Проверки ds18b20_ds2480: фиксируют ожидаемое поведение на воспроизводимых входных данных.
|
||||
* Тестовые значения и подмены окружения относятся к проверке контракта; они не являются
|
||||
* настройками рабочего устройства.
|
||||
*/
|
||||
|
||||
#include "ds18b20_ds2480.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s\n", __LINE__, #x); exit(1); } } while (0)
|
||||
|
||||
enum { ROM_COMMAND, MATCH, FUNCTION, SEARCH, READ, CONFIG };
|
||||
typedef struct {
|
||||
uint8_t rom[3][8], scratch[9];
|
||||
unsigned count, active, state, phase, position, bits, value, match_index;
|
||||
unsigned prepared, calibrated, pending, reply, pulse, hold, convert, copy;
|
||||
unsigned fail_read, fail_write, fail_prepare, corrupt_reply, reset_reply;
|
||||
unsigned fail_power, fail_stop, reject_config;
|
||||
unsigned data_mode, escape;
|
||||
} fake;
|
||||
|
||||
static void crc_scratch(fake *f) { f->scratch[8] = ds2480_crc8(f->scratch, 8); }
|
||||
|
||||
static void setup(fake *f)
|
||||
{
|
||||
unsigned i;
|
||||
static const uint8_t scratch[9] = {0x91, 0x01, 0x4B, 0x46, 0x7F, 0xFF, 0x0C, 0x10, 0x70};
|
||||
memset(f, 0, sizeof *f);
|
||||
f->count = 3;
|
||||
f->reset_reply = 0xCD;
|
||||
memcpy(f->scratch, scratch, 9);
|
||||
for (i = 0; i < 3; ++i) {
|
||||
f->rom[i][0] = i == 2 ? 0x10 : 0x28;
|
||||
f->rom[i][1] = (uint8_t)(i + 1);
|
||||
f->rom[i][7] = ds2480_crc8(f->rom[i], 7);
|
||||
}
|
||||
}
|
||||
|
||||
static void byte_in(fake *f, uint8_t byte)
|
||||
{
|
||||
unsigned i;
|
||||
if (f->state == ROM_COMMAND) {
|
||||
if (byte == 0xF0) { f->state = SEARCH; f->position = f->phase = 0; }
|
||||
else if (byte == 0x55) { f->state = MATCH; f->match_index = 0; }
|
||||
else { CHECK(byte == 0xCC); f->state = FUNCTION; }
|
||||
} else if (f->state == MATCH) {
|
||||
for (i = 0; i < f->count; ++i)
|
||||
if (f->rom[i][f->match_index] != byte) f->active &= ~(1U << i);
|
||||
if (++f->match_index == 8) f->state = FUNCTION;
|
||||
} else if (f->state == FUNCTION) {
|
||||
CHECK(f->active != 0);
|
||||
switch (byte) {
|
||||
case 0xBE: f->state = READ; f->position = 0; break;
|
||||
case 0x4E: f->state = CONFIG; f->position = 2; break;
|
||||
case 0x44: ++f->convert; break;
|
||||
case 0x48: ++f->copy; break;
|
||||
default: CHECK(0);
|
||||
}
|
||||
} else {
|
||||
CHECK(f->state == CONFIG);
|
||||
if (!f->reject_config) f->scratch[f->position] = byte;
|
||||
if (++f->position == 5) { crc_scratch(f); f->state = FUNCTION; }
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t wire_bit(fake *f, uint8_t bit)
|
||||
{
|
||||
unsigned i, result = 1;
|
||||
if (f->state == SEARCH) {
|
||||
for (i = 0; i < f->count; ++i) {
|
||||
unsigned v = (f->rom[i][f->position / 8] >> (f->position % 8)) & 1;
|
||||
if (!(f->active & (1U << i))) continue;
|
||||
if (f->phase < 2) result &= f->phase ? !v : v;
|
||||
else if (v != bit) f->active &= ~(1U << i);
|
||||
}
|
||||
if (++f->phase == 3) { f->phase = 0; ++f->position; }
|
||||
return (uint8_t)result;
|
||||
}
|
||||
if (f->state == READ) {
|
||||
CHECK(f->position < 72 && bit == 1);
|
||||
result = (f->scratch[f->position / 8] >> (f->position % 8)) & 1;
|
||||
++f->position;
|
||||
return (uint8_t)result;
|
||||
}
|
||||
f->value |= (unsigned)bit << f->bits;
|
||||
if (++f->bits == 8) {
|
||||
uint8_t byte = (uint8_t)f->value;
|
||||
f->bits = f->value = 0;
|
||||
byte_in(f, byte);
|
||||
}
|
||||
return bit;
|
||||
}
|
||||
|
||||
static int prepare(void *user)
|
||||
{
|
||||
fake *f = user;
|
||||
++f->prepared;
|
||||
f->pending = f->pulse = f->calibrated = 0;
|
||||
f->data_mode = f->escape = 0;
|
||||
return (int)f->fail_prepare;
|
||||
}
|
||||
|
||||
static int transmit(void *user, const uint8_t *data, uint32_t size, uint32_t timeout)
|
||||
{
|
||||
fake *f = user;
|
||||
uint8_t command = *data, bit;
|
||||
CHECK(size == 1 && timeout == 20 && !f->pending);
|
||||
if (f->fail_write) return -1;
|
||||
if (!f->calibrated) { CHECK(command == 0xC1); f->calibrated = 1; return 0; }
|
||||
if (f->data_mode) {
|
||||
unsigned i, reply = 0;
|
||||
if (command == 0xE3 && !f->escape) { f->escape=1; return 0; }
|
||||
if (!f->escape || command == 0xE3) {
|
||||
f->escape=0;
|
||||
for (i=0; i<8; ++i) reply |= (unsigned)wire_bit(f,(uint8_t)((command>>i)&1)) << i;
|
||||
f->reply=reply; f->pending=1; return 0;
|
||||
}
|
||||
f->escape=0; f->data_mode=0;
|
||||
}
|
||||
if (command == 0xE1) { f->data_mode=1; return 0; }
|
||||
CHECK(!f->pulse || command == 0xF1);
|
||||
if (command == 0x3F) f->reply = 0x3E;
|
||||
else if (command == 0xC1) {
|
||||
f->reply = f->count ? f->reset_reply : 0xCF;
|
||||
f->state = ROM_COMMAND; f->bits = f->value = 0;
|
||||
f->active = (1U << f->count) - 1;
|
||||
} else if (command == 0xF1) {
|
||||
CHECK(f->pulse && (f->hold == 750 || f->hold == 12));
|
||||
f->pulse = 0; f->reply = 0xEC;
|
||||
if (f->fail_stop) f->fail_read = 1;
|
||||
} else {
|
||||
CHECK(command == 0x81 || command == 0x91 || command == 0x83 || command == 0x93);
|
||||
bit = wire_bit(f, (uint8_t)((command >> 4) & 1));
|
||||
f->reply = (command & 0xFC) | (bit ? 3U : 0U);
|
||||
if (command & 2) {
|
||||
CHECK(f->bits == 0 && (f->convert || f->copy));
|
||||
f->pulse = 1; f->hold = 0;
|
||||
if (f->fail_power) f->fail_read = 1;
|
||||
}
|
||||
}
|
||||
f->pending = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int receive(void *user, uint8_t *data, uint32_t size, uint32_t timeout)
|
||||
{
|
||||
fake *f = user;
|
||||
CHECK(size == 1 && timeout == 20 && f->pending);
|
||||
if (f->fail_read) return -1;
|
||||
*data = (uint8_t)(f->corrupt_reply ? 0 : f->reply);
|
||||
f->pending = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void delay(void *user, uint32_t ms)
|
||||
{
|
||||
fake *f = user;
|
||||
if (f->pulse) f->hold += ms;
|
||||
else CHECK(ms == 2);
|
||||
}
|
||||
|
||||
static void init(fake *f, ds2480 *bus)
|
||||
{
|
||||
ds2480_port port = { f, prepare, transmit, receive, delay };
|
||||
CHECK(ds2480_init(bus, &port, 20) == DS2480_OK);
|
||||
CHECK(!f->pending);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
fake f, other;
|
||||
ds2480 bus, second;
|
||||
ds2480_search search = {{0}, 0, 0}, saved;
|
||||
uint8_t bit, scratch[9], rom[8];
|
||||
int16_t raw = 999;
|
||||
unsigned found = 0, i;
|
||||
setup(&f); init(&f, &bus);
|
||||
CHECK(ds2480_crc8(f.scratch, 9) == 0); /* Datasheet vector, not generated. */
|
||||
while (ds18b20_ds2480_next(&bus, &search) == DS2480_OK) {
|
||||
if (!memcmp(search.rom, f.rom[0], 8)) found |= 1;
|
||||
else if (!memcmp(search.rom, f.rom[1], 8)) found |= 2;
|
||||
else CHECK(0);
|
||||
}
|
||||
CHECK(found == 3 && search.done);
|
||||
CHECK(ds18b20_ds2480_next(&bus, &search) == DS2480_DONE);
|
||||
CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_OK);
|
||||
CHECK(f.convert == 1 && f.hold == 750 && !f.pulse && !f.pending);
|
||||
CHECK(ds2480_reset(&bus) == DS2480_OK);
|
||||
CHECK(ds2480_byte(&bus, 0xCC, &bit) == DS2480_OK);
|
||||
CHECK(ds2480_power_begin(&bus, 0x44) == DS2480_OK);
|
||||
CHECK(f.hold == 0 && f.pulse && bus.power_active);
|
||||
CHECK(ds2480_reset(&bus) == DS2480_BUSY);
|
||||
CHECK(ds2480_byte(&bus, 0xFF, &bit) == DS2480_BUSY);
|
||||
CHECK(ds2480_power_begin(&bus, 0x44) == DS2480_BUSY);
|
||||
delay(&f, 750);
|
||||
CHECK(ds2480_power_end(&bus) == DS2480_OK && !bus.power_active);
|
||||
CHECK(ds2480_power_end(&bus) == DS2480_OK);
|
||||
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK && raw == 401);
|
||||
CHECK(ds18b20_ds2480_convert(&bus, f.rom[1]) == DS2480_OK && f.active == 2);
|
||||
for (i = 9; i <= 12; ++i) {
|
||||
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], -5, -20, (uint8_t)i, 1) == DS2480_OK);
|
||||
CHECK(f.hold == 12 && !f.pulse && !f.pending);
|
||||
f.scratch[0] = 0x5F; f.scratch[1] = 0xFF; crc_scratch(&f);
|
||||
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK);
|
||||
CHECK(raw == (i == 9 ? -168 : i == 10 ? -164 : i == 11 ? -162 : -161));
|
||||
}
|
||||
CHECK(f.copy == 4);
|
||||
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], (int8_t)-29, 2, 12, 0) == DS2480_OK);
|
||||
CHECK(f.scratch[2] == 0xE3); /* Escaped byte followed by an ordinary byte. */
|
||||
f.reject_config = 1;
|
||||
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], 1, 2, 9, 1) == DS2480_DATA && f.copy == 4);
|
||||
f.scratch[8] ^= 1; raw = 999;
|
||||
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_CRC && raw == 999);
|
||||
memset(f.scratch, 0, 9);
|
||||
CHECK(ds18b20_ds2480_read(&bus, f.rom[0], scratch) == DS2480_DATA);
|
||||
memset(f.scratch, 0xFF, 9);
|
||||
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_CRC);
|
||||
memcpy(rom, f.rom[0], 8); rom[7] ^= 1;
|
||||
CHECK(ds18b20_ds2480_convert(&bus, rom) == DS2480_ARGUMENT);
|
||||
CHECK(ds18b20_ds2480_read(&bus, NULL, scratch) == DS2480_ARGUMENT);
|
||||
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], 0, 0, 8, 0) == DS2480_ARGUMENT);
|
||||
CHECK(ds2480_bit(&bus, 1, NULL) == DS2480_ARGUMENT);
|
||||
f.reset_reply = 0xEC; CHECK(ds2480_reset(&bus) == DS2480_SHORT);
|
||||
f.reset_reply = 0xEE; CHECK(ds2480_reset(&bus) == DS2480_OK);
|
||||
f.count = 0; CHECK(ds2480_reset(&bus) == DS2480_NO_PRESENCE);
|
||||
memset(&search, 0, sizeof search);
|
||||
CHECK(ds2480_search_next(&bus, &search) == DS2480_NO_PRESENCE);
|
||||
setup(&f); init(&f, &bus); f.count = 1; f.rom[0][7] ^= 1;
|
||||
saved = search;
|
||||
CHECK(ds2480_search_next(&bus, &search) == DS2480_CRC);
|
||||
CHECK(!memcmp(&search, &saved, sizeof search));
|
||||
f.fail_read = 1; CHECK(ds2480_reset(&bus) == DS2480_IO);
|
||||
CHECK(ds2480_reset(&bus) == DS2480_NOT_READY);
|
||||
setup(&f); init(&f, &bus); f.corrupt_reply = 1;
|
||||
CHECK(ds2480_reset(&bus) == DS2480_PROTOCOL && !bus.ready);
|
||||
setup(&f); init(&f, &bus); CHECK(ds2480_reset(&bus) == DS2480_OK); f.corrupt_reply = 1;
|
||||
CHECK(ds2480_bit(&bus, 1, &bit) == DS2480_PROTOCOL);
|
||||
setup(&f); init(&f, &bus); f.fail_write = 1;
|
||||
CHECK(ds2480_reset(&bus) == DS2480_IO && !bus.ready);
|
||||
setup(&f); init(&f, &bus); f.fail_power = 1;
|
||||
CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_IO);
|
||||
CHECK(!f.pulse && !bus.ready && f.prepared == 2);
|
||||
setup(&f); init(&f, &bus); f.fail_stop = 1;
|
||||
CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_IO);
|
||||
CHECK(!f.pulse && !bus.ready && f.prepared == 2);
|
||||
setup(&f); init(&f, &bus);
|
||||
setup(&other); init(&other, &second);
|
||||
CHECK(ds18b20_ds2480_convert(&second, other.rom[1]) == DS2480_OK);
|
||||
CHECK(f.convert == 0 && other.convert == 1);
|
||||
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK && raw == 401);
|
||||
CHECK(ds2480_init(&bus, &bus.port, 20) == DS2480_OK);
|
||||
f.fail_prepare = 1;
|
||||
CHECK(ds2480_init(&bus, &bus.port, 20) == DS2480_IO && !bus.ready);
|
||||
CHECK(ds2480_init(&bus, NULL, 20) == DS2480_ARGUMENT);
|
||||
puts("DS18B20/DS2480: all host tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+31
-2
@@ -23,7 +23,9 @@
|
||||
|---|---|---|
|
||||
| `include/ds18b20.h`, `src/ds18b20.c` | перечень датчиков, измерения, запись TH/TL/разрешения | `onewire.h`, `stdint.h` |
|
||||
| `include/onewire.h`, `src/onewire.c` | байты и блоки, CRC8 Dallas/Maxim, обход SEARCH ROM | `stdint.h` |
|
||||
| `ports/stm32f1/` | тайминги Maxim, вывод с открытым стоком, задержки по DWT | CMSIS `stm32f10x.h` |
|
||||
| `ports/stm32f1/` | STM32F103: тайминги Maxim, открытый сток, задержки по DWT | CMSIS `stm32f10x.h` |
|
||||
| `ports/stm32g4/` | то же для STM32G431 и STM32G474 | CMSIS `stm32g4xx.h` |
|
||||
| `ports/stm32f4/` | то же для STM32F407VET6 и родственных F4 | CMSIS `stm32f4xx.h` |
|
||||
| `tests/` | симулятор шины с виртуальными датчиками и host-тесты | — |
|
||||
|
||||
## Порт: что нужно от платформы
|
||||
@@ -48,7 +50,7 @@ uint8_t OneWire_ReadBit(void);
|
||||
микросекундных задержек — счётчик тактов ядра `DWT->CYCCNT`.
|
||||
|
||||
Привязку вывода порт берёт из `onewire_config.h`, который предоставляет
|
||||
приложение — шаблон лежит рядом:
|
||||
приложение — шаблон `onewire_config.f103.template.h` лежит рядом:
|
||||
|
||||
```c
|
||||
#include "stm32f10x.h"
|
||||
@@ -60,6 +62,33 @@ uint8_t OneWire_ReadBit(void);
|
||||
|
||||
Требуется внешняя подтяжка 4.7 кОм к питанию.
|
||||
|
||||
### Готовые порты STM32G4 и STM32F4
|
||||
|
||||
Логика та же, что и в порте STM32F1: открытый сток, чтение через `IDR`,
|
||||
задержки по `DWT->CYCCNT`, запрет прерываний только на один тайм-слот.
|
||||
Отличается лишь настройка вывода — у G4 и F4 это `MODER`/`OTYPER`/
|
||||
`OSPEEDR`/`PUPDR` вместо `CRL`/`CRH`, а низкий уровень выставляется
|
||||
через верхнюю половину `BSRR`.
|
||||
|
||||
STM32G431 и STM32G474 обслуживает один порт `ports/stm32g4/`: блок GPIO и
|
||||
шина тактирования у них совпадают, различается только CMSIS-заголовок в
|
||||
`onewire_config.h`. Шаблоны лежат рядом — по одному на каждый камень:
|
||||
|
||||
```c
|
||||
/* ports/stm32g4/onewire_config.g474.template.h */
|
||||
#include "stm32g4xx.h"
|
||||
|
||||
#define ONEWIRE_GPIO GPIOA
|
||||
#define ONEWIRE_PIN 1U
|
||||
#define ONEWIRE_GPIO_CLOCK RCC_AHB2ENR_GPIOAEN /* у F4 — RCC_AHB1ENR_GPIOAEN */
|
||||
```
|
||||
|
||||
Порт `ports/stm32f4/` написан под STM32F407VET6 и подходит любому F4 с тем
|
||||
же блоком GPIO; тактирование включается в `RCC->AHB1ENR`.
|
||||
|
||||
Подтяжка 4.7 кОм — внешняя и здесь: внутренняя `PUPDR` намеренно
|
||||
оставлена выключенной, её сопротивления для шины не хватает.
|
||||
|
||||
## Использование
|
||||
|
||||
```c
|
||||
|
||||
@@ -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 на выводе с открытым стоком.
|
||||
@@ -13,7 +19,7 @@
|
||||
* и OneWire_ReadBit.
|
||||
*
|
||||
* Чтобы перенести библиотеку на другой микроконтроллер, достаточно написать
|
||||
* эти пять функций - остальное собирается как есть.
|
||||
* эти пять функций - остальное собирается как есть. ТЕСТ
|
||||
*/
|
||||
|
||||
#ifndef ONEWIRE_H
|
||||
|
||||
@@ -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()
|
||||
@@ -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 м,
|
||||
отключение/подключение датчика при поиске, отмену и восстановление опроса.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user