Compare commits
69 Commits
de180750aa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 795a1279b1 | |||
| 80ba17d77d | |||
| def3eb08f3 | |||
| 3020de7e0c | |||
| df02070cc4 | |||
| 00f8dff838 | |||
| 7b2f74a3ff | |||
| 96ce6f8759 | |||
| 874d2e1dfc | |||
| 698632ebf3 | |||
| 67cca68fcc | |||
| c0c20f6dc2 | |||
| 11f16f618b | |||
| 3a1e8d469a | |||
| daaca0d94b | |||
| 20dbc2c721 | |||
| e95338d781 | |||
| daa3466a41 | |||
| c9cf797132 | |||
| b0f3731554 | |||
| 9ad0322c71 | |||
| 1511719bb9 | |||
| 0ce464499f | |||
| 27ef6fcfdf | |||
| 235d5d81a2 | |||
| 59f847e900 | |||
| 286e454464 | |||
| e691dfc337 | |||
| f5f15f6a04 | |||
| be066a57eb | |||
| b1f7b965f4 | |||
| 78d3f6690b | |||
| 6a82b309cc | |||
| 2316a5a26a | |||
| de5dc18a43 | |||
| d44d57a7aa | |||
| b972b6f33c | |||
| 454baeed98 | |||
| 01ffc0e496 | |||
| 3c4ac9963d | |||
| d9eb7dd9ad | |||
| f6787163dc | |||
| 5f8fc07f2a | |||
| c8565d868d | |||
| 1523640414 | |||
| fa6f615e81 | |||
| 392817432b | |||
| 1f1006bf2a | |||
| 20850b26df | |||
| e163d9ba55 | |||
| 1296773346 | |||
| af82687e42 | |||
| e9ec078b38 | |||
| 43410c3f12 | |||
| 076c51c083 | |||
| e9c5ec90b8 | |||
| 00a0f944c4 | |||
| 9c0701ab9b | |||
| 9b636a9f40 | |||
| 19becd7b8c | |||
| 5504104cc5 | |||
| e6bea52db0 | |||
| 92fc2ade58 | |||
| 080b6900f5 | |||
| 68040a2803 | |||
| 7fe76b3c3b | |||
| 0f77a510e6 | |||
| 7910a07f2f | |||
| 2fd51a1d7e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
build/
|
build/
|
||||||
cmake-build-*/
|
cmake-build-*/
|
||||||
*.o
|
*.o
|
||||||
|
*.obj
|
||||||
*.d
|
*.d
|
||||||
*.a
|
*.a
|
||||||
*.elf
|
*.elf
|
||||||
@@ -22,3 +23,4 @@ __pycache__/
|
|||||||
*.uvguix.*
|
*.uvguix.*
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
Desktop.ini
|
Desktop.ini
|
||||||
|
.codex-build/
|
||||||
|
|||||||
@@ -62,6 +62,27 @@ G431 и G474 намеренно используют один исходник
|
|||||||
этой задачи совместимы. Раздельными остаются конфигурация платы, startup,
|
этой задачи совместимы. Раздельными остаются конфигурация платы, startup,
|
||||||
linker script и выбранный CMSIS device define.
|
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-порта находится в соседнем проекте
|
Рабочий пример полноценного G474-порта находится в соседнем проекте
|
||||||
`candleLight_fw`: `src/device/device_g4.c` отвечает за clock/GPIO,
|
`candleLight_fw`: `src/device/device_g4.c` отвечает за clock/GPIO,
|
||||||
`src/can/m_can.c` — за FDCAN, а `include/config.h` — за привязку платы
|
`src/can/m_can.c` — за FDCAN, а `include/config.h` — за привязку платы
|
||||||
@@ -77,6 +98,7 @@ USB gs_usb и конкретной разводки адаптера; перен
|
|||||||
| дисплей ST7789 | `c/st7789` | SPI, DC, CS, RESET, подсветка и задержка |
|
| дисплей ST7789 | `c/st7789` | SPI, DC, CS, RESET, подсветка и задержка |
|
||||||
| кнопки | `c/keypad` | настройка GPIO и чтение уровней |
|
| кнопки | `c/keypad` | настройка GPIO и чтение уровней |
|
||||||
| меню | `c/menu` | только привязка painter к дисплею |
|
| меню | `c/menu` | только привязка painter к дисплею |
|
||||||
|
| индикация состояний | `c/led-indicator` | готовый STM32 HAL-порт или callbacks GPIO и выбранного таймера |
|
||||||
| EEPROM FT24C256 | `c/eeprom-ft24c256` | I2C write/write-read и задержка |
|
| EEPROM FT24C256 | `c/eeprom-ft24c256` | I2C write/write-read и задержка |
|
||||||
| CAN SETTINGS | `c/can-sensor` | bxCAN для F103 либо FDCAN для G431/G474 |
|
| CAN SETTINGS | `c/can-sensor` | bxCAN для F103 либо FDCAN для G431/G474 |
|
||||||
|
|
||||||
|
|||||||
37
README.md
37
README.md
@@ -13,11 +13,15 @@
|
|||||||
templates/
|
templates/
|
||||||
c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты
|
c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты
|
||||||
python/ модули на чистом Python 3.9+, только stdlib
|
python/ модули на чистом Python 3.9+, только stdlib
|
||||||
|
tools/ общие инструменты сборки и выпуска прошивок
|
||||||
```
|
```
|
||||||
|
|
||||||
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
|
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
|
||||||
и STM32G474 описаны в [`NEW_PROJECT.md`](NEW_PROJECT.md).
|
и STM32G474 описаны в [`NEW_PROJECT.md`](NEW_PROJECT.md).
|
||||||
|
|
||||||
|
Нормативная документация SETCAN/ProtoCAN, реестр общего адресного пространства
|
||||||
|
и исходный Excel собраны в [`doc/setcan`](doc/setcan/README.md).
|
||||||
|
|
||||||
## Что лежит
|
## Что лежит
|
||||||
|
|
||||||
### C
|
### C
|
||||||
@@ -27,25 +31,47 @@ templates/
|
|||||||
| [`c/st7789`](c/st7789) | TFT ST7789V по SPI, RGB565, текст без кадрового буфера | `stdint.h` | SPI-запись, линия DC, задержка |
|
| [`c/st7789`](c/st7789) | TFT ST7789V по SPI, RGB565, текст без кадрового буфера | `stdint.h` | SPI-запись, линия DC, задержка |
|
||||||
| [`c/keypad`](c/keypad) | шесть кнопок: антидребезг, автоповтор, удержание, очередь событий | `stdint.h` | чтение уровня кнопки, время в мс |
|
| [`c/keypad`](c/keypad) | шесть кнопок: антидребезг, автоповтор, удержание, очередь событий | `stdint.h` | чтение уровня кнопки, время в мс |
|
||||||
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `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/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
|
||||||
| [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 |
|
| [`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`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
|
||||||
| [`c/protocan-transport`](c/protocan-transport) | транспорт ProtoCAN: кадр, канал, CRC, общее адресное пространство, каталог GUI | `stdint.h` | запись в поток и запрос свободного места — **порт STM32F4 в комплекте** |
|
| [`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 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
|
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
|
||||||
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
|
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
|
||||||
| [`c/set-protocol`](c/set-protocol) | единый SET protocol v2: управление, real-time телеметрия и обновление прошивки через UART/CAN/USB/Ethernet | C99 | доставка целого stream/datagram-кадра, часы, backend карты и загрузчика |
|
|
||||||
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
|
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
|
||||||
| [`c/firmware-info`](c/firmware-info) | SemVer, build stamp и git build ID работающего образа | C99 | config-порты STM32F1/F4/G4 и К1921ВК028; упаковка для Modbus/SETGUI |
|
| [`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
|
### Python
|
||||||
|
|
||||||
| Модуль | Что делает | Зависимости |
|
| Модуль | Что делает | Зависимости |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| [`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`](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` дают побайтово
|
### Инструменты
|
||||||
|
|
||||||
|
| Инструмент | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| [`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.
|
одинаковый результат — это зафиксировано эталонами в тестах на C.
|
||||||
|
|
||||||
|
Интерактивная документация общего ядра: [`doc/setprotocol.html`](doc/setprotocol.html).
|
||||||
|
В ней отдельно описаны граница ядра, ABI, память, три wire format и состояние
|
||||||
|
портов Windows, Android, Linux и MCU.
|
||||||
|
|
||||||
## Как подключить к проекту
|
## Как подключить к проекту
|
||||||
|
|
||||||
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
|
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
|
||||||
@@ -78,17 +104,16 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20, firmware-info |
|
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20, firmware-info |
|
||||||
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
|
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
|
||||||
| `CAN_to_RS485` | protocan-transport, python/protocan |
|
| `CAN_to_RS485` | set-protocol legacy transport, python/protocan |
|
||||||
| `candleLight_fw` | эталон разделения общей логики и G431/G474 FDCAN-порта; общий ProtoCAN Boot переносится по контракту `protocan-boot` |
|
| `candleLight_fw` | эталон разделения общей логики и G431/G474 FDCAN-порта; общий ProtoCAN Boot переносится по контракту `protocan-boot` |
|
||||||
| `SETGUI` | python/protocan |
|
| `SETGUI` | python/protocan |
|
||||||
| новые устройства SET и `SETGUI` v2 | set-protocol |
|
| `SETGUI`, Android GUI и новые устройства SET | set-protocol |
|
||||||
| `k1921vk028` | rtc-service |
|
| `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/user/src/one_wire_user.c` | аппаратный 1-Wire `OWI_*` из `plib028`, мигание светодиодом прямо внутри опроса датчика | логика DS18B20 уже есть в `c/ds18b20` — переносится порт, а не алгоритм |
|
||||||
| `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
|
| `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
|
||||||
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |
|
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |
|
||||||
|
|||||||
80
RULES.md
Normal file
80
RULES.md
Normal file
@@ -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).
|
||||||
22
c/can-sensor/CMakeLists.txt
Normal file
22
c/can-sensor/CMakeLists.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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()
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
| Файл | Что делает | Зависимости |
|
| Файл | Что делает | Зависимости |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `can_sensor.h`, `can_sensor.c` | сборка и разбор SETTINGS, повторы передачи, счётчики обмена | `stdint.h` |
|
| `can_sensor.h`, `can_sensor.c` | сборка и разбор SETTINGS, планировщик PULSE, повторы передачи, счётчики обмена | `stdint.h` |
|
||||||
| `ports/stm32f1/` | опросный порт CAN1: GPIO, BTR, фильтр, mailbox/FIFO, тайм-аут ACK | CMSIS `stm32f10x.h` |
|
| `ports/stm32f1/` | опросный порт CAN1: GPIO, BTR, фильтр, mailbox/FIFO, тайм-аут ACK | CMSIS `stm32f10x.h` |
|
||||||
|
|
||||||
`ZZ` — номер сборки, `YY` — позиция. Нулевой ROM с DLC=8 очищает локацию.
|
`ZZ` — номер сборки, `YY` — позиция. Нулевой ROM с DLC=8 очищает локацию.
|
||||||
@@ -40,14 +40,20 @@ CanSensor link;
|
|||||||
CanSensor_Io io = { .send = bxcan_send, .receive = bxcan_receive, .context = &board };
|
CanSensor_Io io = { .send = bxcan_send, .receive = bxcan_receive, .context = &board };
|
||||||
CanSensor_Config config;
|
CanSensor_Config config;
|
||||||
CanSensor_ConfigDefault(&config); /* TX 0x1FFE0000, RX 0x17FE0000 */
|
CanSensor_ConfigDefault(&config); /* TX 0x1FFE0000, RX 0x17FE0000 */
|
||||||
|
config.pulse_period_ms = 1000U; /* 0 — PULSE выключен */
|
||||||
|
|
||||||
CanSensor_Init(&link, &io, &config);
|
CanSensor_Init(&link, &io, &config);
|
||||||
CanSensor_SendId(&link, position, assembly_serial, rom);
|
CanSensor_SendId(&link, position, assembly_serial, rom);
|
||||||
|
CanSensor_Task(&link, now_ms);
|
||||||
|
|
||||||
CanSensor_Message message;
|
CanSensor_Message message;
|
||||||
if (CanSensor_Poll(&link, &message)) { /* принят идентификатор */ }
|
if (CanSensor_Poll(&link, &message)) { /* принят идентификатор */ }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Период можно менять во время работы через `CanSensor_SetPulsePeriod()`. Значение
|
||||||
|
`0` именно выключает heartbeat: кадры PULSE больше не ставятся в очередь. При
|
||||||
|
повторном включении первый кадр отправляется через полный заданный период.
|
||||||
|
|
||||||
## Порт STM32F1
|
## Порт STM32F1
|
||||||
|
|
||||||
Порт не использует STM32 HAL и не занимает прерывания. Скопируйте
|
Порт не использует STM32 HAL и не занимает прерывания. Скопируйте
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ void CanSensor_ConfigDefault(CanSensor_Config *config)
|
|||||||
}
|
}
|
||||||
config->tx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
config->tx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
||||||
config->rx_id = CAN_SENSOR_DEFAULT_RX_ID;
|
config->rx_id = CAN_SENSOR_DEFAULT_RX_ID;
|
||||||
|
config->pulse_period_ms = 0U;
|
||||||
config->extended = 1U;
|
config->extended = 1U;
|
||||||
config->retries = CAN_SENSOR_DEFAULT_RETRIES;
|
config->retries = CAN_SENSOR_DEFAULT_RETRIES;
|
||||||
}
|
}
|
||||||
@@ -72,6 +73,9 @@ uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
|||||||
link->send_errors = 0U;
|
link->send_errors = 0U;
|
||||||
link->received_messages = 0U;
|
link->received_messages = 0U;
|
||||||
link->dropped_frames = 0U;
|
link->dropped_frames = 0U;
|
||||||
|
link->pulse_last_ms = 0U;
|
||||||
|
link->pulse_counter = 0U;
|
||||||
|
link->pulse_armed = 0U;
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,3 +205,51 @@ uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out)
|
|||||||
}
|
}
|
||||||
return 0U;
|
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--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,9 +27,11 @@
|
|||||||
|
|
||||||
/** Назначенный проекту тип сообщения SETCAN SETTINGS. */
|
/** Назначенный проекту тип сообщения SETCAN SETTINGS. */
|
||||||
#define CAN_SENSOR_MSGTYPE_SETTINGS 0xEU
|
#define CAN_SENSOR_MSGTYPE_SETTINGS 0xEU
|
||||||
|
#define CAN_SENSOR_MSGTYPE_PULSE 0xFU
|
||||||
|
|
||||||
/** Маска полей SETCAN от Priority до MsgType; Body под маской не находится. */
|
/** Маска полей SETCAN от Priority до MsgType; Body под маской не находится. */
|
||||||
#define CAN_SENSOR_HEADER_MASK 0x1FFF0000UL
|
#define CAN_SENSOR_HEADER_MASK 0x1FFF0000UL
|
||||||
|
#define CAN_SENSOR_ADDRESS_MASK 0x1FF00000UL
|
||||||
#define CAN_SENSOR_BODY_MASK 0x0000FFFFUL
|
#define CAN_SENSOR_BODY_MASK 0x0000FFFFUL
|
||||||
|
|
||||||
/** Базовые ID SETTINGS для выбранных DeviceType=0x7 и DeviceID=0xF. */
|
/** Базовые ID SETTINGS для выбранных DeviceType=0x7 и DeviceID=0xF. */
|
||||||
@@ -88,6 +90,7 @@ typedef struct {
|
|||||||
uint32_t rx_id; /**< База запроса 0x17FE0000, Body должен быть нулевым. */
|
uint32_t rx_id; /**< База запроса 0x17FE0000, Body должен быть нулевым. */
|
||||||
uint8_t extended; /**< 1 — Extended CAN ID. */
|
uint8_t extended; /**< 1 — Extended CAN ID. */
|
||||||
uint8_t retries; /**< Число попыток передачи. */
|
uint8_t retries; /**< Число попыток передачи. */
|
||||||
|
uint32_t pulse_period_ms; /**< Период PULSE; 0 полностью отключает PULSE. */
|
||||||
} CanSensor_Config;
|
} CanSensor_Config;
|
||||||
|
|
||||||
/** Состояние транспорта и диагностические счётчики. */
|
/** Состояние транспорта и диагностические счётчики. */
|
||||||
@@ -98,6 +101,9 @@ typedef struct {
|
|||||||
uint32_t send_errors;
|
uint32_t send_errors;
|
||||||
uint32_t received_messages;
|
uint32_t received_messages;
|
||||||
uint32_t dropped_frames;
|
uint32_t dropped_frames;
|
||||||
|
uint32_t pulse_last_ms;
|
||||||
|
uint8_t pulse_counter;
|
||||||
|
uint8_t pulse_armed;
|
||||||
} CanSensor;
|
} CanSensor;
|
||||||
|
|
||||||
void CanSensor_ConfigDefault(CanSensor_Config *config);
|
void CanSensor_ConfigDefault(CanSensor_Config *config);
|
||||||
@@ -124,4 +130,17 @@ uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame,
|
|||||||
|
|
||||||
uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out);
|
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 */
|
#endif /* CAN_SENSOR_H */
|
||||||
|
|||||||
79
c/can-sensor/tests/test_can_sensor.c
Normal file
79
c/can-sensor/tests/test_can_sensor.c
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
9
c/candle/CMakeLists.txt
Normal file
9
c/candle/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
project(candle C)
|
||||||
|
if(NOT WIN32)
|
||||||
|
message(FATAL_ERROR "Candle transport requires Windows WinUSB")
|
||||||
|
endif()
|
||||||
|
add_library(candle SHARED candle.c candle_ctrl_req.c candle.def)
|
||||||
|
target_compile_definitions(candle PRIVATE UNICODE _UNICODE)
|
||||||
|
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32)
|
||||||
|
target_include_directories(candle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
66
c/candle/LICENSE
Normal file
66
c/candle/LICENSE
Normal file
File diff suppressed because one or more lines are too long
16
c/candle/README.md
Normal file
16
c/candle/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Candle / gs_usb — порт WinUSB
|
||||||
|
|
||||||
|
Общий C-порт для адаптеров candleLight/gs_usb. Перенесён из
|
||||||
|
SETGUI `src/gui_desktop/native/candle_src`; исходные файлы и LGPLv3
|
||||||
|
`LICENSE` сохранены без изменений.
|
||||||
|
|
||||||
|
Сборка из Developer Command Prompt:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
cmake -S c/candle -B build/candle -A x64
|
||||||
|
cmake --build build/candle --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
Для 32-битной библиотеки используйте `-A Win32` и отдельный каталог сборки.
|
||||||
|
Python Qt-порт находится в `python/set_devices/qt_ports/candle_adapter.py`;
|
||||||
|
путь к библиотеке задаётся через `CANDLE_LIBRARY`.
|
||||||
1151
c/candle/candle.c
Normal file
1151
c/candle/candle.c
Normal file
@@ -0,0 +1,1151 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "candle.h"
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "candle_defs.h"
|
||||||
|
#include "candle_ctrl_req.h"
|
||||||
|
#include "ch_9.h"
|
||||||
|
|
||||||
|
static bool candle_dev_interal_open(candle_handle hdev);
|
||||||
|
|
||||||
|
candle_log_fn_t candle_log_fn = NULL;
|
||||||
|
bool candle_log_verbose = false;
|
||||||
|
|
||||||
|
static void candle_logf(const wchar_t *fmt, ...)
|
||||||
|
{
|
||||||
|
if (candle_log_fn == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t buf[512];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
HRESULT hr = StringCchVPrintfW(buf, 512, fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
if (SUCCEEDED(hr)) {
|
||||||
|
candle_log_fn(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void candle_logf_verbose(const wchar_t *fmt, ...)
|
||||||
|
{
|
||||||
|
if (candle_log_fn == NULL || !candle_log_verbose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t buf[512];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
HRESULT hr = StringCchVPrintfW(buf, 512, fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
if (SUCCEEDED(hr)) {
|
||||||
|
candle_log_fn(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool candle_read_di(HDEVINFO hdi, SP_DEVICE_INTERFACE_DATA interfaceData, candle_device_t *dev)
|
||||||
|
{
|
||||||
|
/* get required length first (this call always fails with an error) */
|
||||||
|
ULONG requiredLength=0;
|
||||||
|
SetupDiGetDeviceInterfaceDetail(hdi, &interfaceData, NULL, 0, &requiredLength, NULL);
|
||||||
|
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
|
||||||
|
dev->last_error = CANDLE_ERR_SETUPDI_IF_DETAILS;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PSP_DEVICE_INTERFACE_DETAIL_DATA detail_data =
|
||||||
|
(PSP_DEVICE_INTERFACE_DETAIL_DATA) LocalAlloc(LMEM_FIXED, requiredLength);
|
||||||
|
|
||||||
|
if (detail_data != NULL) {
|
||||||
|
detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
|
||||||
|
} else {
|
||||||
|
dev->last_error = CANDLE_ERR_MALLOC;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool retval = true;
|
||||||
|
ULONG length = requiredLength;
|
||||||
|
if (!SetupDiGetDeviceInterfaceDetail(hdi, &interfaceData, detail_data, length, &requiredLength, NULL) ) {
|
||||||
|
dev->last_error = CANDLE_ERR_SETUPDI_IF_DETAILS2;
|
||||||
|
retval = false;
|
||||||
|
} else if (FAILED(StringCchCopy(dev->path, sizeof(dev->path), detail_data->DevicePath))) {
|
||||||
|
dev->last_error = CANDLE_ERR_PATH_LEN;
|
||||||
|
retval = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalFree(detail_data);
|
||||||
|
|
||||||
|
if (!retval) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* try to open to read device infos and see if it is avail */
|
||||||
|
if (candle_dev_interal_open(dev)) {
|
||||||
|
dev->state = CANDLE_DEVSTATE_AVAIL;
|
||||||
|
candle_dev_close(dev);
|
||||||
|
} else {
|
||||||
|
dev->state = CANDLE_DEVSTATE_INUSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
dev->last_error = CANDLE_ERR_OK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return true when path already appears in l->dev[0..count-1]. */
|
||||||
|
static bool candle_path_exists(const candle_list_t *l, unsigned count, const wchar_t *path)
|
||||||
|
{
|
||||||
|
for (unsigned i = 0; i < count; i++) {
|
||||||
|
if (wcscmp(l->dev[i].path, path) == 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scan one GUID and append found devices to l->dev[] starting at offset.
|
||||||
|
* Returns the number of devices appended, or -1 on a hard error (l->last_error set). */
|
||||||
|
static int candle_scan_guid(candle_list_t *l, const wchar_t *guid_str, unsigned offset)
|
||||||
|
{
|
||||||
|
GUID guid;
|
||||||
|
if (CLSIDFromString(guid_str, &guid) != NOERROR) {
|
||||||
|
l->last_error = CANDLE_ERR_CLSID;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
HDEVINFO hdi = SetupDiGetClassDevs(&guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
|
||||||
|
if (hdi == INVALID_HANDLE_VALUE) {
|
||||||
|
/* No devices with this GUID present — not a hard error. */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int found = 0;
|
||||||
|
for (unsigned i = 0; (offset + i) < CANDLE_MAX_DEVICES; i++) {
|
||||||
|
SP_DEVICE_INTERFACE_DATA interfaceData;
|
||||||
|
interfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
|
||||||
|
|
||||||
|
if (!SetupDiEnumDeviceInterfaces(hdi, NULL, &guid, i, &interfaceData)) {
|
||||||
|
if (GetLastError() != ERROR_NO_MORE_ITEMS) {
|
||||||
|
l->last_error = CANDLE_ERR_SETUPDI_IF_ENUM;
|
||||||
|
found = -1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candle_read_di(hdi, interfaceData, &l->dev[offset + i])) {
|
||||||
|
l->last_error = l->dev[offset + i].last_error;
|
||||||
|
found = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
found++;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupDiDestroyDeviceInfoList(hdi);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scan for WinUSB devices matching vid:pid whose device interface GUID was not
|
||||||
|
* covered by the GUID list above. For each matching USB device instance the
|
||||||
|
* function reads DeviceInterfaceGUIDs (or DeviceInterfaceGUID) from the Windows
|
||||||
|
* registry, re-uses candle_scan_guid() for each GUID found there, and appends
|
||||||
|
* only those devices that are not already present in l->dev[0..existing-1].
|
||||||
|
* Returns the number of new devices added. */
|
||||||
|
static int candle_scan_vidpid(candle_list_t *l, uint16_t vid, uint16_t pid, unsigned existing)
|
||||||
|
{
|
||||||
|
wchar_t hwid_prefix[32];
|
||||||
|
StringCchPrintfW(hwid_prefix, 32, L"USB\\VID_%04X&PID_%04X", vid, pid);
|
||||||
|
|
||||||
|
/* Enumerate USB device instances (not interfaces) so we can read hardware IDs. */
|
||||||
|
HDEVINFO hdi = SetupDiGetClassDevs(NULL, L"USB", NULL,
|
||||||
|
DIGCF_ALLCLASSES | DIGCF_PRESENT);
|
||||||
|
if (hdi == INVALID_HANDLE_VALUE) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int added = 0;
|
||||||
|
SP_DEVINFO_DATA devInfo;
|
||||||
|
devInfo.cbSize = sizeof(SP_DEVINFO_DATA);
|
||||||
|
|
||||||
|
for (DWORD i = 0;
|
||||||
|
SetupDiEnumDeviceInfo(hdi, i, &devInfo) && existing + added < CANDLE_MAX_DEVICES;
|
||||||
|
i++)
|
||||||
|
{
|
||||||
|
|
||||||
|
/* Hardware IDs are a REG_MULTI_SZ — check each string for our VID/PID prefix. */
|
||||||
|
wchar_t hwids[512];
|
||||||
|
memset(hwids, 0, sizeof(hwids));
|
||||||
|
if (!SetupDiGetDeviceRegistryPropertyW(hdi, &devInfo, SPDRP_HARDWAREID,
|
||||||
|
NULL, (PBYTE)hwids, sizeof(hwids) - sizeof(wchar_t), NULL)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool matches = false;
|
||||||
|
const wchar_t *p;
|
||||||
|
for (p = hwids; *p; p += wcslen(p) + 1) {
|
||||||
|
if (_wcsnicmp(p, hwid_prefix, wcslen(hwid_prefix)) == 0) {
|
||||||
|
matches = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matches) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Open the device's software registry key (Device Parameters) and read
|
||||||
|
* the WinUSB device interface GUID(s) stored by the driver INF. */
|
||||||
|
HKEY hKey = SetupDiOpenDevRegKey(hdi, &devInfo, DICS_FLAG_GLOBAL, 0,
|
||||||
|
DIREG_DEV, KEY_READ);
|
||||||
|
if (hKey == INVALID_HANDLE_VALUE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t guid_buf[256];
|
||||||
|
memset(guid_buf, 0, sizeof(guid_buf));
|
||||||
|
DWORD buf_len = sizeof(guid_buf) - sizeof(wchar_t);
|
||||||
|
|
||||||
|
/* Prefer DeviceInterfaceGUIDs (REG_MULTI_SZ, modern INFs); fall back to
|
||||||
|
* DeviceInterfaceGUID (REG_SZ, older/zadig-generated INFs). */
|
||||||
|
LONG reg_rc = RegQueryValueExW(hKey, L"DeviceInterfaceGUIDs", NULL, NULL,
|
||||||
|
(LPBYTE)guid_buf, &buf_len);
|
||||||
|
if (reg_rc != ERROR_SUCCESS) {
|
||||||
|
buf_len = sizeof(guid_buf) - sizeof(wchar_t);
|
||||||
|
RegQueryValueExW(hKey, L"DeviceInterfaceGUID", NULL, NULL,
|
||||||
|
(LPBYTE)guid_buf, &buf_len);
|
||||||
|
}
|
||||||
|
RegCloseKey(hKey);
|
||||||
|
|
||||||
|
if (!guid_buf[0]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Iterate GUID strings. Both REG_SZ and REG_MULTI_SZ are covered by the
|
||||||
|
* same NUL-terminated-string walk (REG_SZ just has one entry). */
|
||||||
|
const wchar_t *g;
|
||||||
|
for (g = guid_buf;
|
||||||
|
*g && existing + added < CANDLE_MAX_DEVICES;
|
||||||
|
g += wcslen(g) + 1)
|
||||||
|
{
|
||||||
|
unsigned base = existing + added;
|
||||||
|
int n = candle_scan_guid(l, g, base);
|
||||||
|
if (n <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove any entries whose path was already found by the GUID scan. */
|
||||||
|
for (int ni = 0; ni < n; ) {
|
||||||
|
if (candle_path_exists(l, base, l->dev[base + ni].path)) {
|
||||||
|
memmove(&l->dev[base + ni], &l->dev[base + ni + 1],
|
||||||
|
(unsigned)(n - ni - 1) * sizeof(candle_device_t));
|
||||||
|
n--;
|
||||||
|
} else {
|
||||||
|
ni++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
added += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupDiDestroyDeviceInfoList(hdi);
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall candle_list_scan(candle_list_handle *list)
|
||||||
|
{
|
||||||
|
if (list == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
candle_list_t *l = (candle_list_t *)calloc(1, sizeof(candle_list_t));
|
||||||
|
*list = l;
|
||||||
|
if (l == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GUIDs for gs_usb-compatible devices on Windows.
|
||||||
|
* candleLight / CANable / most gs_usb devices: */
|
||||||
|
static const wchar_t *GUIDS[] = {
|
||||||
|
L"{c15b4308-04d3-11e6-b3ea-6057189e6443}" /* candleLight / CANable / gs_usb standard */
|
||||||
|
};
|
||||||
|
static const unsigned NUM_GUIDS = sizeof(GUIDS) / sizeof(GUIDS[0]);
|
||||||
|
|
||||||
|
unsigned total = 0;
|
||||||
|
for (unsigned g = 0; g < NUM_GUIDS; g++) {
|
||||||
|
int n = candle_scan_guid(l, GUIDS[g], total);
|
||||||
|
if (n < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
total += (unsigned)n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VID/PID scan for devices whose device interface GUID is not in the list
|
||||||
|
* above (e.g. CANnectivity which uses its own registered interface GUID). */
|
||||||
|
static const struct { uint16_t vid; uint16_t pid; } VIDPIDS[] = {
|
||||||
|
{ 0x1209, 0xCA01 }, /* CANnectivity (electronut-labs) */
|
||||||
|
};
|
||||||
|
static const unsigned NUM_VIDPIDS = sizeof(VIDPIDS) / sizeof(VIDPIDS[0]);
|
||||||
|
|
||||||
|
for (unsigned v = 0; v < NUM_VIDPIDS && total < CANDLE_MAX_DEVICES; v++) {
|
||||||
|
int n = candle_scan_vidpid(l, VIDPIDS[v].vid, VIDPIDS[v].pid, total);
|
||||||
|
if (n > 0)
|
||||||
|
total += (unsigned)n;
|
||||||
|
}
|
||||||
|
|
||||||
|
l->num_devices = (uint8_t)total;
|
||||||
|
l->last_error = CANDLE_ERR_OK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_list_free(candle_list_handle list)
|
||||||
|
{
|
||||||
|
free(list);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len)
|
||||||
|
{
|
||||||
|
candle_list_t *l = (candle_list_t *)list;
|
||||||
|
*len = l->num_devices;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev)
|
||||||
|
{
|
||||||
|
candle_list_t *l = (candle_list_t *)list;
|
||||||
|
if (l==NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dev_num >= CANDLE_MAX_DEVICES) {
|
||||||
|
l->last_error = CANDLE_ERR_DEV_OUT_OF_RANGE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
candle_device_t *dev = calloc(1, sizeof(candle_device_t));
|
||||||
|
*hdev = dev;
|
||||||
|
if (dev==NULL) {
|
||||||
|
l->last_error = CANDLE_ERR_MALLOC;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(dev, &l->dev[dev_num], sizeof(candle_device_t));
|
||||||
|
l->last_error = CANDLE_ERR_OK;
|
||||||
|
dev->last_error = CANDLE_ERR_OK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state)
|
||||||
|
{
|
||||||
|
if (hdev==NULL) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
*state = dev->state;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t * __stdcall DLL candle_dev_get_path(candle_handle hdev)
|
||||||
|
{
|
||||||
|
if (hdev==NULL) {
|
||||||
|
return NULL;
|
||||||
|
} else {
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
return dev->path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool candle_dev_interal_open(candle_handle hdev)
|
||||||
|
{
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
|
||||||
|
memset(dev->rxevents, 0, sizeof(dev->rxevents));
|
||||||
|
memset(dev->rxurbs, 0, sizeof(dev->rxurbs));
|
||||||
|
|
||||||
|
dev->deviceHandle = CreateFile(
|
||||||
|
dev->path,
|
||||||
|
GENERIC_WRITE | GENERIC_READ,
|
||||||
|
FILE_SHARE_WRITE | FILE_SHARE_READ,
|
||||||
|
NULL,
|
||||||
|
OPEN_EXISTING,
|
||||||
|
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dev->deviceHandle == INVALID_HANDLE_VALUE) {
|
||||||
|
dev->last_error = CANDLE_ERR_CREATE_FILE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WinUsb_Initialize(dev->deviceHandle, &dev->winUSBHandle)) {
|
||||||
|
dev->last_error = CANDLE_ERR_WINUSB_INITIALIZE;
|
||||||
|
goto close_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
USB_INTERFACE_DESCRIPTOR ifaceDescriptor;
|
||||||
|
if (!WinUsb_QueryInterfaceSettings(dev->winUSBHandle, 0, &ifaceDescriptor)) {
|
||||||
|
dev->last_error = CANDLE_ERR_QUERY_INTERFACE;
|
||||||
|
goto winusb_free;
|
||||||
|
}
|
||||||
|
|
||||||
|
dev->interfaceNumber = ifaceDescriptor.bInterfaceNumber;
|
||||||
|
bool has_in = false, has_out = false;
|
||||||
|
|
||||||
|
candle_logf(L"open path=%ls interface=%u endpoints=%u",
|
||||||
|
dev->path,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
ifaceDescriptor.bNumEndpoints);
|
||||||
|
|
||||||
|
for (uint8_t i=0; i<ifaceDescriptor.bNumEndpoints; i++) {
|
||||||
|
|
||||||
|
WINUSB_PIPE_INFORMATION pipeInfo;
|
||||||
|
if (!WinUsb_QueryPipe(dev->winUSBHandle, 0, i, &pipeInfo)) {
|
||||||
|
dev->last_error = CANDLE_ERR_QUERY_PIPE;
|
||||||
|
goto winusb_free;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pipeInfo.PipeType == UsbdPipeTypeBulk && USB_ENDPOINT_DIRECTION_IN(pipeInfo.PipeId)) {
|
||||||
|
if (!has_in) {
|
||||||
|
dev->bulkInPipe = pipeInfo.PipeId;
|
||||||
|
has_in = true;
|
||||||
|
candle_logf(L"selected bulk IN pipe=0x%02x maxPacket=%u interval=%u",
|
||||||
|
pipeInfo.PipeId,
|
||||||
|
pipeInfo.MaximumPacketSize,
|
||||||
|
pipeInfo.Interval);
|
||||||
|
}
|
||||||
|
} else if (pipeInfo.PipeType == UsbdPipeTypeBulk && USB_ENDPOINT_DIRECTION_OUT(pipeInfo.PipeId)) {
|
||||||
|
if (!has_out) {
|
||||||
|
dev->bulkOutPipe = pipeInfo.PipeId;
|
||||||
|
has_out = true;
|
||||||
|
candle_logf(L"selected bulk OUT pipe=0x%02x maxPacket=%u interval=%u",
|
||||||
|
pipeInfo.PipeId,
|
||||||
|
pipeInfo.MaximumPacketSize,
|
||||||
|
pipeInfo.Interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!has_in || !has_out) {
|
||||||
|
dev->last_error = CANDLE_ERR_PARSE_IF_DESCR;
|
||||||
|
goto winusb_free;
|
||||||
|
}
|
||||||
|
|
||||||
|
char use_raw_io = 1;
|
||||||
|
if (!WinUsb_SetPipePolicy(dev->winUSBHandle, dev->bulkInPipe, RAW_IO, sizeof(use_raw_io), &use_raw_io)) {
|
||||||
|
dev->last_error = CANDLE_ERR_SET_PIPE_RAW_IO;
|
||||||
|
goto winusb_free;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candle_ctrl_set_host_format(dev)) {
|
||||||
|
goto winusb_free;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candle_ctrl_get_config(dev, &dev->dconf)) {
|
||||||
|
goto winusb_free;
|
||||||
|
}
|
||||||
|
candle_logf(L"device config channels=%u sw=0x%08x hw=0x%08x",
|
||||||
|
dev->dconf.icount + 1,
|
||||||
|
dev->dconf.sw_version,
|
||||||
|
dev->dconf.hw_version);
|
||||||
|
|
||||||
|
if (!candle_ctrl_get_capability(dev, 0, &dev->bt_const)) {
|
||||||
|
dev->last_error = CANDLE_ERR_GET_BITTIMING_CONST;
|
||||||
|
goto winusb_free;
|
||||||
|
}
|
||||||
|
candle_logf(L"cap ch0 feature=0x%08x fclk=%u tseg1=%u..%u tseg2=%u..%u sjw=%u brp=%u..%u inc=%u",
|
||||||
|
dev->bt_const.feature,
|
||||||
|
dev->bt_const.fclk_can,
|
||||||
|
dev->bt_const.tseg1_min,
|
||||||
|
dev->bt_const.tseg1_max,
|
||||||
|
dev->bt_const.tseg2_min,
|
||||||
|
dev->bt_const.tseg2_max,
|
||||||
|
dev->bt_const.sjw_max,
|
||||||
|
dev->bt_const.brp_min,
|
||||||
|
dev->bt_const.brp_max,
|
||||||
|
dev->bt_const.brp_inc);
|
||||||
|
|
||||||
|
/* Query capabilities for each channel on multi-channel devices */
|
||||||
|
uint8_t num_channels = dev->dconf.icount + 1;
|
||||||
|
if (num_channels > 8) num_channels = 8;
|
||||||
|
for (uint8_t ch = 0; ch < num_channels; ch++) {
|
||||||
|
if (!candle_ctrl_get_capability(dev, ch, &dev->ch_caps[ch])) {
|
||||||
|
/* Fall back to channel 0 capabilities for this channel */
|
||||||
|
memcpy(&dev->ch_caps[ch], &dev->bt_const, sizeof(candle_capability_t));
|
||||||
|
candle_logf(L"cap ch%u failed, falling back to ch0", ch);
|
||||||
|
} else {
|
||||||
|
candle_logf(L"cap ch%u feature=0x%08x fclk=%u",
|
||||||
|
ch,
|
||||||
|
dev->ch_caps[ch].feature,
|
||||||
|
dev->ch_caps[ch].fclk_can);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pre-allocate a manual-reset event for timed overlapped writes. Reusing
|
||||||
|
* one event per device (writes are serialised by writeMutex) avoids
|
||||||
|
* per-frame CreateEvent overhead at high CAN frame rates. */
|
||||||
|
dev->txEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
|
||||||
|
if (!dev->txEvent) {
|
||||||
|
dev->last_error = CANDLE_ERR_MALLOC;
|
||||||
|
goto winusb_free;
|
||||||
|
}
|
||||||
|
|
||||||
|
dev->last_error = CANDLE_ERR_OK;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
winusb_free:
|
||||||
|
WinUsb_Free(dev->winUSBHandle);
|
||||||
|
dev->winUSBHandle = NULL;
|
||||||
|
|
||||||
|
close_handle:
|
||||||
|
CloseHandle(dev->deviceHandle);
|
||||||
|
dev->deviceHandle = NULL;
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool candle_prepare_read(candle_device_t *dev, unsigned urb_num)
|
||||||
|
{
|
||||||
|
if (dev->rxurbs[urb_num].pending) {
|
||||||
|
dev->last_error = CANDLE_ERR_PREPARE_READ;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dev->rxurbs[urb_num].ovl.hEvent == NULL) {
|
||||||
|
dev->last_error = CANDLE_ERR_PREPARE_READ;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
|
||||||
|
|
||||||
|
BOOL rc = WinUsb_ReadPipe(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
dev->bulkInPipe,
|
||||||
|
dev->rxurbs[urb_num].buf,
|
||||||
|
sizeof(dev->rxurbs[urb_num].buf),
|
||||||
|
NULL,
|
||||||
|
&dev->rxurbs[urb_num].ovl
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rc) {
|
||||||
|
/* Synchronous completion: data is already in buf and the event is
|
||||||
|
* signaled. WaitForMultipleObjects will return immediately on the
|
||||||
|
* next call and GetOverlappedResult will succeed, so this is fine. */
|
||||||
|
dev->rxurbs[urb_num].pending = true;
|
||||||
|
dev->last_error = CANDLE_ERR_OK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
if (err == ERROR_IO_PENDING) {
|
||||||
|
dev->rxurbs[urb_num].pending = true;
|
||||||
|
dev->last_error = CANDLE_ERR_OK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
candle_logf(L"prepare read urb=%u failed winerr=%lu", urb_num, err);
|
||||||
|
dev->last_error = CANDLE_ERR_PREPARE_READ;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool candle_close_rxurbs(candle_device_t *dev)
|
||||||
|
{
|
||||||
|
if (dev->winUSBHandle != NULL) {
|
||||||
|
WinUsb_AbortPipe(dev->winUSBHandle, dev->bulkInPipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (unsigned i=0; i<CANDLE_URB_COUNT; i++) {
|
||||||
|
if (dev->rxurbs[i].pending) {
|
||||||
|
CancelIoEx(dev->deviceHandle, &dev->rxurbs[i].ovl);
|
||||||
|
|
||||||
|
DWORD bytes_transfered;
|
||||||
|
WinUsb_GetOverlappedResult(dev->winUSBHandle,
|
||||||
|
&dev->rxurbs[i].ovl,
|
||||||
|
&bytes_transfered,
|
||||||
|
TRUE);
|
||||||
|
dev->rxurbs[i].pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dev->rxevents[i] != NULL) {
|
||||||
|
CloseHandle(dev->rxevents[i]);
|
||||||
|
dev->rxevents[i] = NULL;
|
||||||
|
memset(&dev->rxurbs[i].ovl, 0, sizeof(dev->rxurbs[i].ovl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void candle_release_open_handles(candle_device_t *dev)
|
||||||
|
{
|
||||||
|
candle_close_rxurbs(dev);
|
||||||
|
|
||||||
|
if (dev->txEvent) {
|
||||||
|
CloseHandle(dev->txEvent);
|
||||||
|
dev->txEvent = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dev->winUSBHandle) {
|
||||||
|
WinUsb_Free(dev->winUSBHandle);
|
||||||
|
dev->winUSBHandle = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dev->deviceHandle && dev->deviceHandle != INVALID_HANDLE_VALUE) {
|
||||||
|
CloseHandle(dev->deviceHandle);
|
||||||
|
dev->deviceHandle = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_dev_open(candle_handle hdev)
|
||||||
|
{
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
|
||||||
|
if (candle_dev_interal_open(dev)) {
|
||||||
|
for (unsigned i=0; i<CANDLE_URB_COUNT; i++) {
|
||||||
|
HANDLE ev = CreateEvent(NULL, true, false, NULL);
|
||||||
|
if (ev == NULL) {
|
||||||
|
dev->last_error = CANDLE_ERR_MALLOC;
|
||||||
|
candle_err_t last_error = dev->last_error;
|
||||||
|
candle_release_open_handles(dev);
|
||||||
|
dev->last_error = last_error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dev->rxevents[i] = ev;
|
||||||
|
dev->rxurbs[i].ovl.hEvent = ev;
|
||||||
|
if (!candle_prepare_read(dev, i)) {
|
||||||
|
candle_err_t last_error = dev->last_error;
|
||||||
|
candle_release_open_handles(dev);
|
||||||
|
dev->last_error = last_error;
|
||||||
|
return false; // keep last_error from prepare_read call
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dev->last_error = CANDLE_ERR_OK;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false; // keep last_error from open_device call
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us)
|
||||||
|
{
|
||||||
|
return candle_ctrl_get_timestamp(hdev, timestamp_us);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_dev_close(candle_handle hdev)
|
||||||
|
{
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
|
||||||
|
candle_release_open_handles(dev);
|
||||||
|
|
||||||
|
dev->last_error = CANDLE_ERR_OK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_dev_free(candle_handle hdev)
|
||||||
|
{
|
||||||
|
free(hdev);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
candle_err_t __stdcall DLL candle_dev_last_error(candle_handle hdev)
|
||||||
|
{
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
return dev->last_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_channel_count(candle_handle hdev, uint8_t *num_channels)
|
||||||
|
{
|
||||||
|
// TODO check if info was already read from device; try to do so; throw error...
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
*num_channels = dev->dconf.icount+1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_channel_get_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap)
|
||||||
|
{
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
uint8_t num_channels = dev->dconf.icount + 1;
|
||||||
|
if (ch < num_channels && ch < 8) {
|
||||||
|
memcpy(cap, &dev->ch_caps[ch], sizeof(candle_capability_t));
|
||||||
|
} else {
|
||||||
|
memcpy(cap, &dev->bt_const, sizeof(candle_capability_t));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_channel_get_state(candle_handle hdev, uint8_t ch, candle_can_state_t *state)
|
||||||
|
{
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
candle_device_state_t ds;
|
||||||
|
if (!candle_ctrl_get_state(dev, ch, &ds)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*state = (candle_can_state_t)ds.state;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_channel_bus_off_recover(candle_handle hdev, uint8_t ch)
|
||||||
|
{
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
return candle_ctrl_bus_off_recover(dev, ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data)
|
||||||
|
{
|
||||||
|
// TODO ensure device is open, check channel count..
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
return candle_ctrl_set_bittiming(dev, ch, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate)
|
||||||
|
{
|
||||||
|
// TODO ensure device is open, check channel count..
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
|
||||||
|
if (dev->bt_const.fclk_can != 48000000) {
|
||||||
|
/* this function only works for the candleLight base clock of 48MHz */
|
||||||
|
dev->last_error = CANDLE_ERR_BITRATE_FCLK;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
candle_bittiming_t t;
|
||||||
|
t.prop_seg = 1;
|
||||||
|
t.sjw = 1;
|
||||||
|
t.phase_seg1 = 13 - t.prop_seg;
|
||||||
|
t.phase_seg2 = 2;
|
||||||
|
|
||||||
|
switch (bitrate) {
|
||||||
|
case 10000:
|
||||||
|
t.brp = 300;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 20000:
|
||||||
|
t.brp = 150;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 50000:
|
||||||
|
t.brp = 60;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 83333:
|
||||||
|
t.brp = 36;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 100000:
|
||||||
|
t.brp = 30;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 125000:
|
||||||
|
t.brp = 24;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 250000:
|
||||||
|
t.brp = 12;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 500000:
|
||||||
|
t.brp = 6;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 800000:
|
||||||
|
t.brp = 4;
|
||||||
|
t.phase_seg1 = 12 - t.prop_seg;
|
||||||
|
t.phase_seg2 = 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1000000:
|
||||||
|
t.brp = 3;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
dev->last_error = CANDLE_ERR_BITRATE_UNSUPPORTED;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candle_ctrl_set_bittiming(dev, ch, &t);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags)
|
||||||
|
{
|
||||||
|
// TODO ensure device is open, check channel count..
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
candle_capability_t *cap = (ch < 8) ? &dev->ch_caps[ch] : &dev->bt_const;
|
||||||
|
|
||||||
|
if (cap->feature & CANDLE_FEATURE_HW_TIMESTAMP) {
|
||||||
|
flags |= CANDLE_MODE_HW_TIMESTAMP;
|
||||||
|
} else {
|
||||||
|
candle_logf(L"channel %u has no HW timestamp capability; starting without timestamp flag", ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool rc = candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_START, flags);
|
||||||
|
candle_logf(L"channel %u start flags=0x%08x result=%u err=%u",
|
||||||
|
ch,
|
||||||
|
flags,
|
||||||
|
rc ? 1 : 0,
|
||||||
|
dev->last_error);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch)
|
||||||
|
{
|
||||||
|
// TODO ensure device is open, check channel count..
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
return candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_RESET, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write len bytes from buf to the OUT pipe, aborting after 300 ms.
|
||||||
|
* Writes are serialised by writeMutex in CandleApiInterface so dev->txEvent
|
||||||
|
* is never accessed by two threads simultaneously. */
|
||||||
|
static bool candle_write_pipe_timed(candle_device_t *dev, uint8_t *buf, DWORD len)
|
||||||
|
{
|
||||||
|
OVERLAPPED ovl;
|
||||||
|
memset(&ovl, 0, sizeof(ovl));
|
||||||
|
ovl.hEvent = dev->txEvent;
|
||||||
|
ResetEvent(dev->txEvent);
|
||||||
|
|
||||||
|
BOOL rc = WinUsb_WritePipe(dev->winUSBHandle, dev->bulkOutPipe,
|
||||||
|
buf, len, NULL, &ovl);
|
||||||
|
if (rc) {
|
||||||
|
return true; /* completed synchronously */
|
||||||
|
}
|
||||||
|
if (GetLastError() != ERROR_IO_PENDING) {
|
||||||
|
return false; /* hard error */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WaitForSingleObject(dev->txEvent, 150) != WAIT_OBJECT_0) {
|
||||||
|
/* Timed out: cancel the transfer and restore the pipe to a clean state. */
|
||||||
|
WinUsb_AbortPipe(dev->winUSBHandle, dev->bulkOutPipe);
|
||||||
|
DWORD dummy = 0;
|
||||||
|
WinUsb_GetOverlappedResult(dev->winUSBHandle, &ovl, &dummy, TRUE);
|
||||||
|
WinUsb_ResetPipe(dev->winUSBHandle, dev->bulkOutPipe);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD transferred = 0;
|
||||||
|
return WinUsb_GetOverlappedResult(dev->winUSBHandle, &ovl, &transferred, FALSE) != FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame)
|
||||||
|
{
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
frame->echo_id = 0;
|
||||||
|
frame->channel = ch;
|
||||||
|
bool rc = candle_write_pipe_timed(dev, (uint8_t*)frame, sizeof(*frame));
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SEND_FRAME;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms)
|
||||||
|
{
|
||||||
|
// TODO ensure device is open..
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
|
||||||
|
DWORD wait_result = WaitForMultipleObjects(CANDLE_URB_COUNT, dev->rxevents, false, timeout_ms);
|
||||||
|
if (wait_result == WAIT_TIMEOUT) {
|
||||||
|
dev->last_error = CANDLE_ERR_READ_TIMEOUT;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( (wait_result < WAIT_OBJECT_0) || (wait_result >= WAIT_OBJECT_0 + CANDLE_URB_COUNT) ) {
|
||||||
|
dev->last_error = CANDLE_ERR_READ_WAIT;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD urb_num = wait_result - WAIT_OBJECT_0;
|
||||||
|
DWORD bytes_transfered;
|
||||||
|
|
||||||
|
if (!WinUsb_GetOverlappedResult(dev->winUSBHandle, &dev->rxurbs[urb_num].ovl, &bytes_transfered, false)) {
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
if (err == ERROR_IO_INCOMPLETE) {
|
||||||
|
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
|
||||||
|
} else {
|
||||||
|
dev->rxurbs[urb_num].pending = false;
|
||||||
|
candle_prepare_read(dev, urb_num);
|
||||||
|
}
|
||||||
|
candle_logf(L"classic read result failed urb=%u winerr=%lu", urb_num, err);
|
||||||
|
dev->last_error = CANDLE_ERR_READ_RESULT;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dev->rxurbs[urb_num].pending = false;
|
||||||
|
|
||||||
|
if (bytes_transfered < sizeof(*frame)-4) {
|
||||||
|
candle_prepare_read(dev, urb_num);
|
||||||
|
candle_logf(L"classic read too small urb=%u bytes=%lu min=%u",
|
||||||
|
urb_num,
|
||||||
|
bytes_transfered,
|
||||||
|
(unsigned)(sizeof(*frame) - 4));
|
||||||
|
dev->last_error = CANDLE_ERR_READ_SIZE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(frame, 0, sizeof(*frame));
|
||||||
|
DWORD copy_len = (bytes_transfered < sizeof(*frame)) ? bytes_transfered : sizeof(*frame);
|
||||||
|
memcpy(frame, dev->rxurbs[urb_num].buf, copy_len);
|
||||||
|
candle_logf_verbose(L"classic read urb=%u bytes=%lu echo=0x%08x can_id=0x%08x dlc=%u ch=%u flags=0x%02x ts=%u",
|
||||||
|
urb_num,
|
||||||
|
bytes_transfered,
|
||||||
|
frame->echo_id,
|
||||||
|
frame->can_id,
|
||||||
|
frame->can_dlc,
|
||||||
|
frame->channel,
|
||||||
|
frame->flags,
|
||||||
|
frame->timestamp_us);
|
||||||
|
|
||||||
|
return candle_prepare_read(dev, urb_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
candle_frametype_t __stdcall DLL candle_frame_type(candle_frame_t *frame)
|
||||||
|
{
|
||||||
|
if (frame->echo_id != 0xFFFFFFFF) {
|
||||||
|
return CANDLE_FRAMETYPE_ECHO;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (frame->can_id & CANDLE_ID_ERR) {
|
||||||
|
return CANDLE_FRAMETYPE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CANDLE_FRAMETYPE_RECEIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t __stdcall DLL candle_frame_id(candle_frame_t *frame)
|
||||||
|
{
|
||||||
|
return frame->can_id & 0x1FFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_frame_is_extended_id(candle_frame_t *frame)
|
||||||
|
{
|
||||||
|
return (frame->can_id & CANDLE_ID_EXTENDED) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_frame_is_rtr(candle_frame_t *frame)
|
||||||
|
{
|
||||||
|
return (frame->can_id & CANDLE_ID_RTR) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t __stdcall DLL candle_frame_dlc(candle_frame_t *frame)
|
||||||
|
{
|
||||||
|
return frame->can_dlc;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t * __stdcall DLL candle_frame_data(candle_frame_t *frame)
|
||||||
|
{
|
||||||
|
return frame->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t __stdcall DLL candle_frame_timestamp_us(candle_frame_t *frame)
|
||||||
|
{
|
||||||
|
return frame->timestamp_us;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- CAN FD extensions ---- */
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_channel_set_data_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data)
|
||||||
|
{
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
return candle_ctrl_set_data_bittiming(dev, ch, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_fd_frame_send(candle_handle hdev, uint8_t ch, candle_fd_frame_t *frame)
|
||||||
|
{
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
frame->echo_id = 0;
|
||||||
|
frame->channel = ch;
|
||||||
|
bool rc = candle_write_pipe_timed(dev, (uint8_t*)frame, sizeof(*frame));
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SEND_FRAME;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *frame, uint32_t timeout_ms)
|
||||||
|
{
|
||||||
|
candle_device_t *dev = (candle_device_t*)hdev;
|
||||||
|
|
||||||
|
DWORD wait_result = WaitForMultipleObjects(CANDLE_URB_COUNT, dev->rxevents, false, timeout_ms);
|
||||||
|
if (wait_result == WAIT_TIMEOUT) {
|
||||||
|
dev->last_error = CANDLE_ERR_READ_TIMEOUT;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( (wait_result < WAIT_OBJECT_0) || (wait_result >= WAIT_OBJECT_0 + CANDLE_URB_COUNT) ) {
|
||||||
|
dev->last_error = CANDLE_ERR_READ_WAIT;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD urb_num = wait_result - WAIT_OBJECT_0;
|
||||||
|
DWORD bytes_transfered;
|
||||||
|
|
||||||
|
if (!WinUsb_GetOverlappedResult(dev->winUSBHandle, &dev->rxurbs[urb_num].ovl, &bytes_transfered, false)) {
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
if (err == ERROR_IO_INCOMPLETE) {
|
||||||
|
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
|
||||||
|
} else {
|
||||||
|
dev->rxurbs[urb_num].pending = false;
|
||||||
|
candle_prepare_read(dev, urb_num);
|
||||||
|
}
|
||||||
|
candle_logf(L"fd read result failed urb=%u winerr=%lu", urb_num, err);
|
||||||
|
dev->last_error = CANDLE_ERR_READ_RESULT;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dev->rxurbs[urb_num].pending = false;
|
||||||
|
|
||||||
|
/* Minimum: classic CAN header (12 bytes) + at least 8 data bytes = 20 bytes */
|
||||||
|
static const DWORD classic_min = sizeof(candle_frame_t) - 4;
|
||||||
|
if (bytes_transfered < classic_min) {
|
||||||
|
candle_prepare_read(dev, urb_num);
|
||||||
|
candle_logf(L"fd read too small urb=%u bytes=%lu min=%lu",
|
||||||
|
urb_num,
|
||||||
|
bytes_transfered,
|
||||||
|
classic_min);
|
||||||
|
dev->last_error = CANDLE_ERR_READ_SIZE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(frame, 0, sizeof(*frame));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Detect frame type from the flags byte (offset 10 in both structs).
|
||||||
|
* Classic CAN frames: header(12) + data(8) + timestamp(4) = 24 bytes total.
|
||||||
|
*
|
||||||
|
* FD frames come in two wire formats:
|
||||||
|
* - Legacy fixed (candleLight/CANable 1.x): always 80 bytes — header(12) +
|
||||||
|
* data[64] + timestamp(4). The timestamp is ALWAYS at offset 76, regardless
|
||||||
|
* of the actual DLC. Identified by bytes_transferred == sizeof(candle_fd_frame_t).
|
||||||
|
* - Variable-length (CANnectivity/Zephyr): header(12) + actual_data(DLC) +
|
||||||
|
* timestamp(4). Identified by bytes_transferred < sizeof(candle_fd_frame_t).
|
||||||
|
*/
|
||||||
|
bool is_fd_frame = (dev->rxurbs[urb_num].buf[10] & CANDLE_FRAME_FLAG_FD) != 0;
|
||||||
|
|
||||||
|
if (is_fd_frame) {
|
||||||
|
/* can_dlc is at byte offset 8 in both classic and FD wire frames. */
|
||||||
|
const uint8_t raw_dlc = dev->rxurbs[urb_num].buf[8];
|
||||||
|
const DWORD data_len = candle_dlc_to_len(raw_dlc);
|
||||||
|
const DWORD min_size = 12 + data_len; /* header + data, without timestamp */
|
||||||
|
|
||||||
|
if (bytes_transfered < min_size) {
|
||||||
|
candle_prepare_read(dev, urb_num);
|
||||||
|
candle_logf(L"fd read FD frame too small urb=%u bytes=%lu min=%lu flags=0x%02x dlc=%u",
|
||||||
|
urb_num,
|
||||||
|
bytes_transfered,
|
||||||
|
min_size,
|
||||||
|
dev->rxurbs[urb_num].buf[10],
|
||||||
|
raw_dlc);
|
||||||
|
dev->last_error = CANDLE_ERR_READ_SIZE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy the fixed 12-byte header (echo_id … reserved). */
|
||||||
|
memcpy(frame, dev->rxurbs[urb_num].buf, 12);
|
||||||
|
/* Copy data at offset 12 into the struct's data field. */
|
||||||
|
memcpy(frame->data, dev->rxurbs[urb_num].buf + 12, data_len);
|
||||||
|
|
||||||
|
/* Timestamp location depends on the wire format (see comment above). */
|
||||||
|
const DWORD fixed_ts_offset = (DWORD)(sizeof(candle_fd_frame_t) - sizeof(uint32_t)); /* = 76 */
|
||||||
|
const DWORD ts_offset = (bytes_transfered >= (DWORD)sizeof(candle_fd_frame_t))
|
||||||
|
? fixed_ts_offset
|
||||||
|
: min_size;
|
||||||
|
if (bytes_transfered >= ts_offset + (DWORD)sizeof(uint32_t)) {
|
||||||
|
memcpy(&frame->timestamp_us, dev->rxurbs[urb_num].buf + ts_offset, sizeof(uint32_t));
|
||||||
|
}
|
||||||
|
/* else: timestamp stays zero from memset above */
|
||||||
|
} else {
|
||||||
|
/* Classic CAN frame — copy into FD struct, fixing the timestamp position */
|
||||||
|
candle_frame_t classic;
|
||||||
|
DWORD copy_len = (bytes_transfered < sizeof(classic)) ? bytes_transfered : sizeof(classic);
|
||||||
|
memcpy(&classic, dev->rxurbs[urb_num].buf, copy_len);
|
||||||
|
|
||||||
|
frame->echo_id = classic.echo_id;
|
||||||
|
frame->can_id = classic.can_id;
|
||||||
|
frame->can_dlc = classic.can_dlc;
|
||||||
|
frame->channel = classic.channel;
|
||||||
|
frame->flags = classic.flags;
|
||||||
|
frame->reserved = classic.reserved;
|
||||||
|
memcpy(frame->data, classic.data, 8);
|
||||||
|
frame->timestamp_us = (bytes_transfered >= sizeof(classic)) ? classic.timestamp_us : 0;
|
||||||
|
}
|
||||||
|
candle_logf_verbose(L"fd read urb=%u bytes=%lu is_fd=%u echo=0x%08x can_id=0x%08x dlc=%u ch=%u flags=0x%02x ts=%u",
|
||||||
|
urb_num,
|
||||||
|
bytes_transfered,
|
||||||
|
is_fd_frame ? 1 : 0,
|
||||||
|
frame->echo_id,
|
||||||
|
frame->can_id,
|
||||||
|
frame->can_dlc,
|
||||||
|
frame->channel,
|
||||||
|
frame->flags,
|
||||||
|
frame->timestamp_us);
|
||||||
|
|
||||||
|
return candle_prepare_read(dev, urb_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
candle_frametype_t __stdcall DLL candle_fd_frame_type(candle_fd_frame_t *frame)
|
||||||
|
{
|
||||||
|
if (frame->echo_id != 0xFFFFFFFF) {
|
||||||
|
return CANDLE_FRAMETYPE_ECHO;
|
||||||
|
}
|
||||||
|
if (frame->can_id & CANDLE_ID_ERR) {
|
||||||
|
return CANDLE_FRAMETYPE_ERROR;
|
||||||
|
}
|
||||||
|
return CANDLE_FRAMETYPE_RECEIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t __stdcall DLL candle_fd_frame_id(candle_fd_frame_t *frame)
|
||||||
|
{
|
||||||
|
return frame->can_id & 0x1FFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_extended_id(candle_fd_frame_t *frame)
|
||||||
|
{
|
||||||
|
return (frame->can_id & CANDLE_ID_EXTENDED) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_rtr(candle_fd_frame_t *frame)
|
||||||
|
{
|
||||||
|
return (frame->can_id & CANDLE_ID_RTR) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_fd(candle_fd_frame_t *frame)
|
||||||
|
{
|
||||||
|
return (frame->flags & CANDLE_FRAME_FLAG_FD) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_brs(candle_fd_frame_t *frame)
|
||||||
|
{
|
||||||
|
return (frame->flags & CANDLE_FRAME_FLAG_BRS) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t __stdcall DLL candle_fd_frame_dlc(candle_fd_frame_t *frame)
|
||||||
|
{
|
||||||
|
return frame->can_dlc;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t * __stdcall DLL candle_fd_frame_data(candle_fd_frame_t *frame)
|
||||||
|
{
|
||||||
|
return frame->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t __stdcall DLL candle_fd_frame_timestamp_us(candle_fd_frame_t *frame)
|
||||||
|
{
|
||||||
|
return frame->timestamp_us;
|
||||||
|
}
|
||||||
29
c/candle/candle.def
Normal file
29
c/candle/candle.def
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
EXPORTS
|
||||||
|
candle_list_scan
|
||||||
|
candle_list_free
|
||||||
|
candle_list_length
|
||||||
|
candle_dev_get
|
||||||
|
candle_dev_get_state
|
||||||
|
candle_dev_get_path
|
||||||
|
candle_dev_open
|
||||||
|
candle_dev_get_timestamp_us
|
||||||
|
candle_dev_close
|
||||||
|
candle_dev_free
|
||||||
|
candle_channel_count
|
||||||
|
candle_channel_get_capabilities
|
||||||
|
candle_channel_get_state
|
||||||
|
candle_channel_bus_off_recover
|
||||||
|
candle_channel_set_timing
|
||||||
|
candle_channel_set_bitrate
|
||||||
|
candle_channel_start
|
||||||
|
candle_channel_stop
|
||||||
|
candle_frame_send
|
||||||
|
candle_frame_read
|
||||||
|
candle_frame_type
|
||||||
|
candle_frame_id
|
||||||
|
candle_frame_is_extended_id
|
||||||
|
candle_frame_is_rtr
|
||||||
|
candle_frame_dlc
|
||||||
|
candle_frame_data
|
||||||
|
candle_frame_timestamp_us
|
||||||
|
candle_dev_last_error
|
||||||
287
c/candle/candle.h
Normal file
287
c/candle/candle.h
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef void* candle_list_handle;
|
||||||
|
typedef void* candle_handle;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CANDLE_DEVSTATE_AVAIL,
|
||||||
|
CANDLE_DEVSTATE_INUSE
|
||||||
|
} candle_devstate_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CANDLE_FRAMETYPE_UNKNOWN,
|
||||||
|
CANDLE_FRAMETYPE_RECEIVE,
|
||||||
|
CANDLE_FRAMETYPE_ECHO,
|
||||||
|
CANDLE_FRAMETYPE_ERROR,
|
||||||
|
CANDLE_FRAMETYPE_TIMESTAMP_OVFL
|
||||||
|
} candle_frametype_t;
|
||||||
|
|
||||||
|
enum {
|
||||||
|
CANDLE_ID_EXTENDED = 0x80000000,
|
||||||
|
CANDLE_ID_RTR = 0x40000000,
|
||||||
|
CANDLE_ID_ERR = 0x20000000
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Feature flags reported in candle_capability_t.feature */
|
||||||
|
enum {
|
||||||
|
/** CAN channel supports listen-only mode, in which it is not allowed to send dominant bits. */
|
||||||
|
CANDLE_FEATURE_LISTEN_ONLY = 0x0001,
|
||||||
|
/** CAN channel supports loopback mode, in which it receives own frames. */
|
||||||
|
CANDLE_FEATURE_LOOP_BACK = 0x0002,
|
||||||
|
/** CAN channel supports triple sampling mode */
|
||||||
|
CANDLE_FEATURE_TRIPLE_SAMPLE = 0x0004,
|
||||||
|
/** CAN channel supports not retransmitting in case of lost arbitration or missing ACK. */
|
||||||
|
CANDLE_FEATURE_ONE_SHOT = 0x0008,
|
||||||
|
/** CAN channel supports hardware timestamping of CAN frames. */
|
||||||
|
CANDLE_FEATURE_HW_TIMESTAMP = 0x0010,
|
||||||
|
/** CAN channel supports visual identification. */
|
||||||
|
CANDLE_FEATURE_IDENTIFY = 0x0020,
|
||||||
|
/** CAN channel supports user IDs (unsupported). */
|
||||||
|
CANDLE_FEATURE_USER_ID = 0x0040,
|
||||||
|
/** CAN channel supports padding of host frames (unsupported). */
|
||||||
|
CANDLE_FEATURE_PAD_PKTS_TO_MAX = 0x0080,
|
||||||
|
/** CAN channel supports transmitting/receiving CAN FD frames. */
|
||||||
|
CANDLE_FEATURE_FD = 0x0100,
|
||||||
|
/** CAN channel support LPC546xx specific quirks (Unused) */
|
||||||
|
CANDLE_FEATURE_REQ_USB_QUIRK_LPC546XX = 0x0200,
|
||||||
|
/** CAN channel supports extended bit timing limits. */
|
||||||
|
CANDLE_FEATURE_BT_CONST_EXT = 0x0400,
|
||||||
|
/** CAN channel supports configurable bus termination. */
|
||||||
|
CANDLE_FEATURE_TERMINATION = 0x0800,
|
||||||
|
/** CAN channel supports bus error reporting (Unsupported, always enabled) */
|
||||||
|
CANDLE_FEATURE_BERR_REPORTING = 0x1000,
|
||||||
|
/** CAN channel supports reporting of bus state. */
|
||||||
|
CANDLE_FEATURE_GET_STATE = 0x2000,
|
||||||
|
/** Host-controlled recovery after the controller enters bus-off. */
|
||||||
|
CANDLE_FEATURE_BUS_OFF_RECOVERY = 0x40000,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Flags in the flags byte of received/transmitted frames */
|
||||||
|
enum {
|
||||||
|
CANDLE_FRAME_FLAG_OVERFLOW = 0x01,
|
||||||
|
CANDLE_FRAME_FLAG_FD = 0x02,
|
||||||
|
CANDLE_FRAME_FLAG_BRS = 0x04,
|
||||||
|
CANDLE_FRAME_FLAG_ESI = 0x08,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CANDLE_STATE_ERROR_ACTIVE = 0,
|
||||||
|
CANDLE_STATE_ERROR_WARNING = 1,
|
||||||
|
CANDLE_STATE_ERROR_PASSIVE = 2,
|
||||||
|
CANDLE_STATE_BUS_OFF = 3,
|
||||||
|
CANDLE_STATE_STOPPED = 4,
|
||||||
|
CANDLE_STATE_SLEEPING = 5,
|
||||||
|
} candle_can_state_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CANDLE_MODE_NORMAL = 0x0000,
|
||||||
|
CANDLE_MODE_LISTEN_ONLY = 0x0001,
|
||||||
|
CANDLE_MODE_LOOP_BACK = 0x0002,
|
||||||
|
CANDLE_MODE_TRIPLE_SAMPLE = 0x0004,
|
||||||
|
CANDLE_MODE_ONE_SHOT = 0x0008,
|
||||||
|
CANDLE_MODE_HW_TIMESTAMP = 0x0010,
|
||||||
|
CANDLE_MODE_PAD_PKTS_TO_MAX = 0x0080,
|
||||||
|
CANDLE_MODE_FD = 0x0100,
|
||||||
|
} candle_mode_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CANDLE_ERR_OK = 0,
|
||||||
|
CANDLE_ERR_CREATE_FILE = 1,
|
||||||
|
CANDLE_ERR_WINUSB_INITIALIZE = 2,
|
||||||
|
CANDLE_ERR_QUERY_INTERFACE = 3,
|
||||||
|
CANDLE_ERR_QUERY_PIPE = 4,
|
||||||
|
CANDLE_ERR_PARSE_IF_DESCR = 5,
|
||||||
|
CANDLE_ERR_SET_HOST_FORMAT = 6,
|
||||||
|
CANDLE_ERR_GET_DEVICE_INFO = 7,
|
||||||
|
CANDLE_ERR_GET_BITTIMING_CONST = 8,
|
||||||
|
CANDLE_ERR_PREPARE_READ = 9,
|
||||||
|
CANDLE_ERR_SET_DEVICE_MODE = 10,
|
||||||
|
CANDLE_ERR_SET_BITTIMING = 11,
|
||||||
|
CANDLE_ERR_BITRATE_FCLK = 12,
|
||||||
|
CANDLE_ERR_BITRATE_UNSUPPORTED = 13,
|
||||||
|
CANDLE_ERR_SEND_FRAME = 14,
|
||||||
|
CANDLE_ERR_READ_TIMEOUT = 15,
|
||||||
|
CANDLE_ERR_READ_WAIT = 16,
|
||||||
|
CANDLE_ERR_READ_RESULT = 17,
|
||||||
|
CANDLE_ERR_READ_SIZE = 18,
|
||||||
|
CANDLE_ERR_SETUPDI_IF_DETAILS = 19,
|
||||||
|
CANDLE_ERR_SETUPDI_IF_DETAILS2 = 20,
|
||||||
|
CANDLE_ERR_MALLOC = 21,
|
||||||
|
CANDLE_ERR_PATH_LEN = 22,
|
||||||
|
CANDLE_ERR_CLSID = 23,
|
||||||
|
CANDLE_ERR_GET_DEVICES = 24,
|
||||||
|
CANDLE_ERR_SETUPDI_IF_ENUM = 25,
|
||||||
|
CANDLE_ERR_SET_TIMESTAMP_MODE = 26,
|
||||||
|
CANDLE_ERR_DEV_OUT_OF_RANGE = 27,
|
||||||
|
CANDLE_ERR_GET_TIMESTAMP = 28,
|
||||||
|
CANDLE_ERR_SET_PIPE_RAW_IO = 29
|
||||||
|
} candle_err_t;
|
||||||
|
|
||||||
|
#pragma pack(push,1)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t echo_id;
|
||||||
|
uint32_t can_id;
|
||||||
|
uint8_t can_dlc;
|
||||||
|
uint8_t channel;
|
||||||
|
uint8_t flags;
|
||||||
|
uint8_t reserved;
|
||||||
|
uint8_t data[8];
|
||||||
|
uint32_t timestamp_us;
|
||||||
|
} candle_frame_t;
|
||||||
|
|
||||||
|
/* CAN FD frame: same header as candle_frame_t but with 64-byte data payload */
|
||||||
|
typedef struct {
|
||||||
|
uint32_t echo_id;
|
||||||
|
uint32_t can_id;
|
||||||
|
uint8_t can_dlc;
|
||||||
|
uint8_t channel;
|
||||||
|
uint8_t flags;
|
||||||
|
uint8_t reserved;
|
||||||
|
uint8_t data[64];
|
||||||
|
uint32_t timestamp_us;
|
||||||
|
} candle_fd_frame_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t feature;
|
||||||
|
uint32_t fclk_can;
|
||||||
|
uint32_t tseg1_min;
|
||||||
|
uint32_t tseg1_max;
|
||||||
|
uint32_t tseg2_min;
|
||||||
|
uint32_t tseg2_max;
|
||||||
|
uint32_t sjw_max;
|
||||||
|
uint32_t brp_min;
|
||||||
|
uint32_t brp_max;
|
||||||
|
uint32_t brp_inc;
|
||||||
|
} candle_capability_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t prop_seg;
|
||||||
|
uint32_t phase_seg1;
|
||||||
|
uint32_t phase_seg2;
|
||||||
|
uint32_t sjw;
|
||||||
|
uint32_t brp;
|
||||||
|
} candle_bittiming_t;
|
||||||
|
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* CAN FD DLC encoding:
|
||||||
|
* DLC 0-8 → 0-8 bytes (same as classic CAN)
|
||||||
|
* DLC 9 → 12 bytes
|
||||||
|
* DLC 10 → 16 bytes
|
||||||
|
* DLC 11 → 20 bytes
|
||||||
|
* DLC 12 → 24 bytes
|
||||||
|
* DLC 13 → 32 bytes
|
||||||
|
* DLC 14 → 48 bytes
|
||||||
|
* DLC 15 → 64 bytes
|
||||||
|
*/
|
||||||
|
static inline uint8_t candle_dlc_to_len(uint8_t dlc)
|
||||||
|
{
|
||||||
|
static const uint8_t tbl[16] = { 0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64 };
|
||||||
|
return (dlc <= 15u) ? tbl[dlc] : 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint8_t candle_len_to_dlc(uint8_t len)
|
||||||
|
{
|
||||||
|
if (len <= 8u) return len;
|
||||||
|
if (len <= 12u) return 9u;
|
||||||
|
if (len <= 16u) return 10u;
|
||||||
|
if (len <= 20u) return 11u;
|
||||||
|
if (len <= 24u) return 12u;
|
||||||
|
if (len <= 32u) return 13u;
|
||||||
|
if (len <= 48u) return 14u;
|
||||||
|
return 15u;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define DLL
|
||||||
|
|
||||||
|
/* Optional log callback — set once at startup to receive diagnostic messages.
|
||||||
|
* If NULL (the default) no logging is performed. */
|
||||||
|
typedef void (*candle_log_fn_t)(const wchar_t *msg);
|
||||||
|
extern candle_log_fn_t candle_log_fn;
|
||||||
|
|
||||||
|
/* Set to true to enable per-frame and per-control-transfer trace logs.
|
||||||
|
* Off by default; only error and setup messages are logged. */
|
||||||
|
extern bool candle_log_verbose;
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_list_scan(candle_list_handle *list);
|
||||||
|
bool __stdcall DLL candle_list_free(candle_list_handle list);
|
||||||
|
bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len);
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev);
|
||||||
|
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state);
|
||||||
|
wchar_t * __stdcall DLL candle_dev_get_path(candle_handle hdev);
|
||||||
|
bool __stdcall DLL candle_dev_open(candle_handle hdev);
|
||||||
|
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us);
|
||||||
|
bool __stdcall DLL candle_dev_close(candle_handle hdev);
|
||||||
|
bool __stdcall DLL candle_dev_free(candle_handle hdev);
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_channel_count(candle_handle hdev, uint8_t *num_channels);
|
||||||
|
bool __stdcall DLL candle_channel_get_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap);
|
||||||
|
bool __stdcall DLL candle_channel_get_state(candle_handle hdev, uint8_t ch, candle_can_state_t *state);
|
||||||
|
bool __stdcall DLL candle_channel_bus_off_recover(candle_handle hdev, uint8_t ch);
|
||||||
|
bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data);
|
||||||
|
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate);
|
||||||
|
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags);
|
||||||
|
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch);
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms);
|
||||||
|
|
||||||
|
candle_frametype_t __stdcall DLL candle_frame_type(candle_frame_t *frame);
|
||||||
|
uint32_t __stdcall DLL candle_frame_id(candle_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_frame_is_extended_id(candle_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_frame_is_rtr(candle_frame_t *frame);
|
||||||
|
uint8_t __stdcall DLL candle_frame_dlc(candle_frame_t *frame);
|
||||||
|
uint8_t * __stdcall DLL candle_frame_data(candle_frame_t *frame);
|
||||||
|
uint32_t __stdcall DLL candle_frame_timestamp_us(candle_frame_t *frame);
|
||||||
|
|
||||||
|
/* CAN FD extensions */
|
||||||
|
bool __stdcall DLL candle_channel_set_data_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data);
|
||||||
|
bool __stdcall DLL candle_fd_frame_send(candle_handle hdev, uint8_t ch, candle_fd_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *frame, uint32_t timeout_ms);
|
||||||
|
|
||||||
|
candle_frametype_t __stdcall DLL candle_fd_frame_type(candle_fd_frame_t *frame);
|
||||||
|
uint32_t __stdcall DLL candle_fd_frame_id(candle_fd_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_extended_id(candle_fd_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_rtr(candle_fd_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_fd(candle_fd_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_brs(candle_fd_frame_t *frame);
|
||||||
|
uint8_t __stdcall DLL candle_fd_frame_dlc(candle_fd_frame_t *frame);
|
||||||
|
uint8_t * __stdcall DLL candle_fd_frame_data(candle_fd_frame_t *frame);
|
||||||
|
uint32_t __stdcall DLL candle_fd_frame_timestamp_us(candle_fd_frame_t *frame);
|
||||||
|
|
||||||
|
candle_err_t __stdcall DLL candle_dev_last_error(candle_handle hdev);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
234
c/candle/candle_ctrl_req.c
Normal file
234
c/candle/candle_ctrl_req.c
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "candle_ctrl_req.h"
|
||||||
|
#include "ch_9.h"
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
enum {
|
||||||
|
CANDLE_BREQ_HOST_FORMAT = 0,
|
||||||
|
CANDLE_BREQ_BITTIMING = 1,
|
||||||
|
CANDLE_BREQ_MODE = 2,
|
||||||
|
CANDLE_BREQ_BERR = 3,
|
||||||
|
CANDLE_BREQ_BT_CONST = 4,
|
||||||
|
CANDLE_BREQ_DEVICE_CONFIG = 5,
|
||||||
|
CANDLE_TIMESTAMP_GET = 6,
|
||||||
|
/* 7: IDENTIFY, 8: GET_USER_ID, 9: SET_USER_ID (not used here) */
|
||||||
|
CANDLE_BREQ_DATA_BITTIMING = 10,
|
||||||
|
/* 11: SET_TERMINATION, not used here */
|
||||||
|
CANDLE_BREQ_GET_STATE = 12,
|
||||||
|
CANDLE_BREQ_BUS_OFF_RECOVERY = 32,
|
||||||
|
};
|
||||||
|
|
||||||
|
static void candle_ctrl_logf(const wchar_t *fmt, ...)
|
||||||
|
{
|
||||||
|
if (candle_log_fn == NULL || !candle_log_verbose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t buf[512];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
HRESULT hr = StringCchVPrintfW(buf, 512, fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
if (SUCCEEDED(hr)) {
|
||||||
|
candle_log_fn(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool usb_control_msg(WINUSB_INTERFACE_HANDLE hnd, uint8_t request, uint8_t requesttype, uint16_t value, uint16_t index, void *data, uint16_t size)
|
||||||
|
{
|
||||||
|
WINUSB_SETUP_PACKET packet;
|
||||||
|
memset(&packet, 0, sizeof(packet));
|
||||||
|
|
||||||
|
packet.Request = request;
|
||||||
|
packet.RequestType = requesttype;
|
||||||
|
packet.Value = value;
|
||||||
|
packet.Index = index;
|
||||||
|
packet.Length = size;
|
||||||
|
|
||||||
|
unsigned long bytes_sent = 0;
|
||||||
|
BOOL rc = WinUsb_ControlTransfer(hnd, packet, (uint8_t*)data, size, &bytes_sent, 0);
|
||||||
|
candle_ctrl_logf(L"ctrl req=0x%02x type=0x%02x value=%u index=%u size=%u rc=%u transferred=%lu winerr=%lu",
|
||||||
|
request,
|
||||||
|
requesttype,
|
||||||
|
value,
|
||||||
|
index,
|
||||||
|
size,
|
||||||
|
rc ? 1 : 0,
|
||||||
|
bytes_sent,
|
||||||
|
rc ? 0 : GetLastError());
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_set_host_format(candle_device_t *dev)
|
||||||
|
{
|
||||||
|
candle_host_config_t hconf;
|
||||||
|
hconf.byte_order = 0x0000beef;
|
||||||
|
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_HOST_FORMAT,
|
||||||
|
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
1,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
&hconf,
|
||||||
|
sizeof(hconf)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_HOST_FORMAT;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_set_device_mode(candle_device_t *dev, uint8_t channel, uint32_t mode, uint32_t flags)
|
||||||
|
{
|
||||||
|
candle_device_mode_t dm;
|
||||||
|
dm.mode = mode;
|
||||||
|
dm.flags = flags;
|
||||||
|
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_MODE,
|
||||||
|
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
channel,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
&dm,
|
||||||
|
sizeof(dm)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool candle_ctrl_get_config(candle_device_t *dev, candle_device_config_t *dconf)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_DEVICE_CONFIG,
|
||||||
|
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
1,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
dconf,
|
||||||
|
sizeof(*dconf)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_DEVICE_INFO;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_get_timestamp(candle_device_t *dev, uint32_t *current_timestamp)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_TIMESTAMP_GET,
|
||||||
|
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
1,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
current_timestamp,
|
||||||
|
sizeof(*current_timestamp)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_TIMESTAMP;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_get_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *data)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_BT_CONST,
|
||||||
|
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
channel,
|
||||||
|
0,
|
||||||
|
data,
|
||||||
|
sizeof(*data)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_BITTIMING_CONST;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_set_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_BITTIMING,
|
||||||
|
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
channel,
|
||||||
|
0,
|
||||||
|
data,
|
||||||
|
sizeof(*data)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_BITTIMING;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_set_data_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_DATA_BITTIMING,
|
||||||
|
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
channel,
|
||||||
|
0,
|
||||||
|
data,
|
||||||
|
sizeof(*data)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_BITTIMING;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_get_state(candle_device_t *dev, uint8_t channel, candle_device_state_t *data)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_GET_STATE,
|
||||||
|
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
channel,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
data,
|
||||||
|
sizeof(*data)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_DEVICE_INFO;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_BUS_OFF_RECOVERY,
|
||||||
|
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
channel,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
NULL,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
49
c/candle/candle_ctrl_req.h
Normal file
49
c/candle/candle_ctrl_req.h
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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);
|
||||||
|
|
||||||
101
c/candle/candle_defs.h
Normal file
101
c/candle/candle_defs.h
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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;
|
||||||
|
|
||||||
37
c/candle/ch_9.h
Normal file
37
c/candle/ch_9.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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
|
||||||
22
c/ds18b20-ds2480/CMakeLists.txt
Normal file
22
c/ds18b20-ds2480/CMakeLists.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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()
|
||||||
145
c/ds18b20-ds2480/README.md
Normal file
145
c/ds18b20-ds2480/README.md
Normal file
@@ -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)
|
||||||
108
c/ds18b20-ds2480/ds18b20_ds2480.c
Normal file
108
c/ds18b20-ds2480/ds18b20_ds2480.c
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
32
c/ds18b20-ds2480/ds18b20_ds2480.h
Normal file
32
c/ds18b20-ds2480/ds18b20_ds2480.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#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
|
||||||
204
c/ds18b20-ds2480/ds2480.c
Normal file
204
c/ds18b20-ds2480/ds2480.c
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
79
c/ds18b20-ds2480/ds2480.h
Normal file
79
c/ds18b20-ds2480/ds2480.h
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#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
|
||||||
19
c/ds18b20-ds2480/examples/read_first.c
Normal file
19
c/ds18b20-ds2480/examples/read_first.c
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
52
c/ds18b20-ds2480/ports/stm32f4/README.md
Normal file
52
c/ds18b20-ds2480/ports/stm32f4/README.md
Normal file
@@ -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).
|
||||||
85
c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.c
Normal file
85
c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.c
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
32
c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.h
Normal file
32
c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#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
|
||||||
39
c/ds18b20-ds2480/ports/stm32f4/tests/stm32f4xx_hal.h
Normal file
39
c/ds18b20-ds2480/ports/stm32f4/tests/stm32f4xx_hal.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/* 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
|
||||||
41
c/ds18b20-ds2480/ports/stm32f4/tests/test_port.c
Normal file
41
c/ds18b20-ds2480/ports/stm32f4/tests/test_port.c
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
253
c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
Normal file
253
c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
#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,11 +1,144 @@
|
|||||||
# PORTING
|
# Портирование firmware-info
|
||||||
|
|
||||||
Аппаратно-зависимый контракт ограничен `firmware_info_config.h`:
|
## Назначение и границы
|
||||||
`FIRMWARE_VERSION_MAJOR`, `FIRMWARE_VERSION_MINOR`, `FIRMWARE_VERSION_PATCH` и
|
|
||||||
необязательный `FIRMWARE_BUILD_ID`. Выберите порт по семейству МК или создайте
|
|
||||||
его копию. Публикация — ответственность транспорта: 12 `uint16_t` для Modbus
|
|
||||||
либо 24 little-endian байта для SETGUI.
|
|
||||||
|
|
||||||
При переносе проверьте: поддержку `__DATE__`/`__TIME__`, наличие generated в
|
Это C-библиотека описания работающего образа: SemVer, дата/время компиляции,
|
||||||
include path, запуск генератора до компиляции и декодирование build ID как двух
|
8 символов build ID и сериализация контракта v1. Она не обращается к сети,
|
||||||
ASCII-байтов в каждом логическом слове.
|
не читает версию из сервера и не записывает Flash. HAL, RTOS, UART и CAN
|
||||||
|
ядру не нужны. Отправку результата выполняет приложение.
|
||||||
|
|
||||||
|
Для размещения `.hex/.bin` в каталоге SETGUI уже существуют
|
||||||
|
[`setprotocol.firmware_publish`](../../python/setprotocol/firmware_publish.py)
|
||||||
|
и [`tools/firmware-publish`](../../tools/firmware-publish/README.md).
|
||||||
|
Связь компонентов и перенос публикации описаны в
|
||||||
|
[`tools/firmware-publish/PORTING.md`](../../tools/firmware-publish/PORTING.md).
|
||||||
|
|
||||||
|
## 1. Файлы и конфигурация
|
||||||
|
|
||||||
|
Пример ниже предполагает `lib/templates` внутри нового проекта:
|
||||||
|
|
||||||
|
```text
|
||||||
|
project/
|
||||||
|
inc/firmware_info_config.h
|
||||||
|
generated/firmware_build_id.h # создаётся до сборки
|
||||||
|
lib/templates/c/firmware-info/
|
||||||
|
src/
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавьте в сборку оба файла:
|
||||||
|
|
||||||
|
```text
|
||||||
|
lib/templates/c/firmware-info/src/firmware_info.c
|
||||||
|
lib/templates/c/firmware-info/src/firmware_info_port.c
|
||||||
|
```
|
||||||
|
|
||||||
|
В include path добавьте `inc`, `generated` и
|
||||||
|
`lib/templates/c/firmware-info/include`. Встроенный `CMakeLists.txt` собирает
|
||||||
|
только ядро и host-тест; `firmware_info_port.c` и каталоги конфигурации нужно
|
||||||
|
добавлять к целевому firmware target самостоятельно.
|
||||||
|
|
||||||
|
Скопируйте `ports/<mcu>/firmware_info_config.template.h` как
|
||||||
|
`inc/firmware_info_config.h`. Есть варианты STM32F1/F4/G4 и К1921ВК028;
|
||||||
|
они не содержат регистров МК. Для другого МК с обычными 8-битными байтами
|
||||||
|
достаточно такого же config:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#ifndef FIRMWARE_INFO_CONFIG_H
|
||||||
|
#define FIRMWARE_INFO_CONFIG_H
|
||||||
|
#define FIRMWARE_VERSION_MAJOR 1U
|
||||||
|
#define FIRMWARE_VERSION_MINOR 2U
|
||||||
|
#define FIRMWARE_VERSION_PATCH 3U
|
||||||
|
#include "firmware_build_id.h"
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
Обязательный include в этом примере позволяет обнаружить пропущенный pre-build.
|
||||||
|
В готовых шаблонах include опциональный через `__has_include`; если заголовок
|
||||||
|
не подключён, порт использует `LOCALDEV`. Для компилятора без `__has_include`
|
||||||
|
используйте явный include. Можно связать три версии с существующими макросами
|
||||||
|
проекта, как это сделано в `KONOR_ds18b20/inc/firmware_info_config.h`.
|
||||||
|
|
||||||
|
## 2. Build ID
|
||||||
|
|
||||||
|
Из корня нового проекта перед компиляцией запустите:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "lib/templates/c/firmware-info/tools/make_build_id.ps1" -Repository "." -Output "generated/firmware_build_id.h"
|
||||||
|
```
|
||||||
|
|
||||||
|
Указывайте `-Repository` и `-Output` явно: расположение сабмодуля и рабочий
|
||||||
|
каталог IDE различаются между проектами. `-Repository` должен указывать на
|
||||||
|
исходники прошивки, а не на репозиторий `templates`. Для Keil из каталога `mdk`
|
||||||
|
соответственно используйте `..\lib\templates\...`, `-Repository ".."` и
|
||||||
|
`-Output ".\Generated\firmware_build_id.h"`.
|
||||||
|
|
||||||
|
При доступном Git чистый checkout получает 8 знаков commit, изменения tracked
|
||||||
|
файлов — 7 знаков и `+`. Если commit определить нельзя, используется `NOGIT000`.
|
||||||
|
Новые untracked-файлы текущий генератор при определении dirty не учитывает.
|
||||||
|
Дата/время берутся из `__DATE__`/`__TIME__` при компиляции `firmware_info_port.c`;
|
||||||
|
для выпуска выполняйте полный Rebuild, чтобы не оставить старый объектный файл.
|
||||||
|
|
||||||
|
## 3. Обработчик запроса версии
|
||||||
|
|
||||||
|
Пример функции подготовки полезной нагрузки, вызываемой вашим обработчиком:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "firmware_info_port.h"
|
||||||
|
|
||||||
|
firmware_info_status_t app_make_firmware_info(uint8_t *payload, size_t capacity)
|
||||||
|
{
|
||||||
|
firmware_info_t info;
|
||||||
|
firmware_info_status_t status = firmware_info_port_describe(&info);
|
||||||
|
if (status != FIRMWARE_INFO_OK) return status;
|
||||||
|
return firmware_info_to_le_bytes(&info, payload, capacity);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
При успехе передайте ровно `FIRMWARE_INFO_PAYLOAD_SIZE` (24) байта в собственный
|
||||||
|
транспорт. При ошибке верните ошибку протокола, а не содержимое буфера.
|
||||||
|
Не отправляйте `sizeof(firmware_info_t)`: структура не является wire format.
|
||||||
|
|
||||||
|
В KONOR обработчик `app_send_firmware_info()` отвечает на
|
||||||
|
`PROTO_MSG_FIRMWARE_INFO = 0x03`, сохраняя sequence запроса. Старый 32-байтовый
|
||||||
|
`DEVICE_INFO` остаётся отдельным сообщением. Для нового протокола сначала
|
||||||
|
согласуйте команду с клиентом: подключение библиотеки само по себе не добавит
|
||||||
|
декодер и отображение версии в GUI.
|
||||||
|
|
||||||
|
Для Modbus вызовите `firmware_info_to_registers()` с массивом из 12 `uint16_t`
|
||||||
|
и разместите его в выбранной карте регистров. Адреса и Modbus-порядок байтов
|
||||||
|
обеспечивает ваш Modbus-стек; LE-буфер в Modbus напрямую не копируйте.
|
||||||
|
|
||||||
|
## 4. Контракт v1
|
||||||
|
|
||||||
|
| Индекс слова | Содержимое |
|
||||||
|
|---|---|
|
||||||
|
| 0 | Версия контракта: 1 |
|
||||||
|
| 1, 2, 3 | major, minor, patch |
|
||||||
|
| 4 | Год |
|
||||||
|
| 5 | `(month << 8) \| day` |
|
||||||
|
| 6 | `(hour << 8) \| minute` |
|
||||||
|
| 7 | Секунды |
|
||||||
|
| 8–11 | По два ASCII-символа build ID: первый в старшем байте слова |
|
||||||
|
|
||||||
|
В LE-представлении младший байт каждого слова идёт первым. Например, build ID
|
||||||
|
`ab` в начале строки даёт слово `0x6162`, но байты `62 61`; декодируйте сначала
|
||||||
|
слово, затем символы из старшего/младшего байта. NUL-терминатор не передаётся.
|
||||||
|
|
||||||
|
Текущая проверка допускает major/minor до 255 и patch до 999. Если в каталоге
|
||||||
|
используется `(major << 16) | (minor << 8) | patch`, ограничьте **все три** части
|
||||||
|
до 255, иначе значения пересекутся. Версия каталога автоматически из C-config
|
||||||
|
не извлекается.
|
||||||
|
|
||||||
|
Для C2000 с 16-битным `char` нельзя считать готовым байтовый порт: отдельно
|
||||||
|
проверьте наличие `uint8_t` и представление октетов в транспорте. Публикация
|
||||||
|
TMS SCI8-файла с ПК поддерживается независимо от переноса этой C-библиотеки.
|
||||||
|
|
||||||
|
## 5. Проверка переноса
|
||||||
|
|
||||||
|
1. Соберите host-тест из `tests/test_firmware_info.c` вместе с ядром либо
|
||||||
|
используйте CMake/CTest. Он проверяет дату, регистры и порядок байтов build ID.
|
||||||
|
2. Соберите целевую прошивку с обоими `.c` и сгенерированным заголовком.
|
||||||
|
3. Запросите версию у устройства: сравните SemVer, build ID, время и 24-байтовый
|
||||||
|
ответ с конкретной сборкой. Проверьте, что прежний `DEVICE_INFO` не изменился.
|
||||||
|
4. Подключите выпуск по инструкции публикатора; сравните версию в config и
|
||||||
|
`firmware-release.cmd` перед Rebuild и `--preflight`.
|
||||||
|
|||||||
@@ -25,3 +25,7 @@ config, а не дублирования ядра.
|
|||||||
|
|
||||||
KONOR публикует эти 24 байта ответом `FIRMWARE_INFO (0x03)`, сохраняя старый
|
KONOR публикует эти 24 байта ответом `FIRMWARE_INFO (0x03)`, сохраняя старый
|
||||||
32-байтовый `DEVICE_INFO` без изменений.
|
32-байтовый `DEVICE_INFO` без изменений.
|
||||||
|
|
||||||
|
Подробная инструкция с кодом обработчика, форматом ответа и проверкой переноса:
|
||||||
|
[`PORTING.md`](PORTING.md). Для размещения файла прошивки в каталоге SETGUI
|
||||||
|
используется отдельный [общий публикатор](../../tools/firmware-publish/PORTING.md).
|
||||||
|
|||||||
30
c/led-indicator/CMakeLists.txt
Normal file
30
c/led-indicator/CMakeLists.txt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(led_indicator C)
|
||||||
|
|
||||||
|
set(CMAKE_C_STANDARD 99)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
add_library(led_indicator STATIC led_indicator.c)
|
||||||
|
target_include_directories(led_indicator PUBLIC .)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(led_indicator PRIVATE /W4)
|
||||||
|
else()
|
||||||
|
target_compile_options(led_indicator PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
option(LED_INDICATOR_BUILD_TESTS "Собирать тесты LED indicator" ON)
|
||||||
|
if(LED_INDICATOR_BUILD_TESTS)
|
||||||
|
enable_testing()
|
||||||
|
add_executable(test_led_indicator tests/test_led_indicator.c)
|
||||||
|
target_link_libraries(test_led_indicator PRIVATE led_indicator)
|
||||||
|
add_test(NAME led_indicator COMMAND test_led_indicator)
|
||||||
|
|
||||||
|
add_executable(test_led_indicator_stm32_port
|
||||||
|
tests/test_stm32_port.c
|
||||||
|
ports/stm32-hal/led_indicator_stm32_hal.c)
|
||||||
|
target_include_directories(test_led_indicator_stm32_port PRIVATE
|
||||||
|
. ports/stm32-hal tests/fakes)
|
||||||
|
target_link_libraries(test_led_indicator_stm32_port PRIVATE led_indicator)
|
||||||
|
add_test(NAME led_indicator_stm32_port COMMAND test_led_indicator_stm32_port)
|
||||||
|
endif()
|
||||||
126
c/led-indicator/README.md
Normal file
126
c/led-indicator/README.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# LED Indicator
|
||||||
|
|
||||||
|
Неблокирующая C99-библиотека для индикации состояний `STARTUP`, `WORK`,
|
||||||
|
`ACTIVITY`, `WARNING`, `ERROR` и `CRITICAL` с разными рисунками и частотами.
|
||||||
|
Один экземпляр обслуживает несколько светодиодов.
|
||||||
|
|
||||||
|
Ядро не знает о GPIO, PWM, сдвиговом регистре, RTOS и модели таймера. Порт
|
||||||
|
передаёт логическое состояние канала наружу и читает монотонное время в
|
||||||
|
миллисекундах. В библиотеке нет задержек, динамической памяти, прерываний и
|
||||||
|
изменяемого глобального состояния.
|
||||||
|
|
||||||
|
```text
|
||||||
|
приложение -> LedIndicator_SetMode/Process -> ядро -> write/now_ms -> GPIO/PWM/expander + TIM
|
||||||
|
```
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
| Файл | Назначение | Зависимости |
|
||||||
|
|---|---|---|
|
||||||
|
| `led_indicator.h/.c` | режимы, шаблоны и планировщик | C99, `stdint.h` |
|
||||||
|
| `ports/stm32-hal` | GPIO и выбранный аппаратный TIM | STM32 HAL |
|
||||||
|
| `tests/test_led_indicator.c` | host-тесты, включая переполнение `uint32_t` | libc |
|
||||||
|
|
||||||
|
## Встроенные режимы
|
||||||
|
|
||||||
|
| Режим | Сигнал |
|
||||||
|
|---|---|
|
||||||
|
| `OFF` / `ON` | постоянно выключен / включён |
|
||||||
|
| `STARTUP` | три коротких импульса, затем постоянно включён |
|
||||||
|
| `WORK` | 100 мс включён, 900 мс выключен |
|
||||||
|
| `ACTIVITY` | 50/50 мс |
|
||||||
|
| `WARNING` | 250/250 мс |
|
||||||
|
| `ERROR` | два импульса и пауза |
|
||||||
|
| `CRITICAL` | три импульса и пауза |
|
||||||
|
|
||||||
|
Частоты не зашиты в алгоритм. Скопируйте встроенную таблицу и измените
|
||||||
|
`duration_ms` до инициализации:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static LedIndicator_Pattern patterns[LED_INDICATOR_MODE_COUNT];
|
||||||
|
|
||||||
|
LedIndicator_CopyDefaultPatterns(patterns, LED_INDICATOR_MODE_COUNT);
|
||||||
|
patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] = 50U;
|
||||||
|
patterns[LED_INDICATOR_MODE_WORK].duration_ms[1] = 1950U;
|
||||||
|
config.patterns = patterns;
|
||||||
|
config.pattern_count = LED_INDICATOR_MODE_COUNT;
|
||||||
|
```
|
||||||
|
|
||||||
|
Таблица должна существовать всё время работы экземпляра. До восьми шагов
|
||||||
|
описываются длительностями и битовой маской `levels`; шаблон может повторяться
|
||||||
|
или после одного прохода перейти в `final_level`.
|
||||||
|
|
||||||
|
## Контракт порта
|
||||||
|
|
||||||
|
```c
|
||||||
|
void write(void *context, uint8_t channel, uint8_t logical_on);
|
||||||
|
uint32_t now_ms(void *context);
|
||||||
|
```
|
||||||
|
|
||||||
|
`write` получает именно логический уровень. Инверсию active-low, управление
|
||||||
|
PWM или запись общего регистра расширителя выполняет порт. Переполнение
|
||||||
|
32-битной миллисекундной метки обработано разностью беззнаковых чисел.
|
||||||
|
|
||||||
|
Если время уже есть в планировщике приложения, `now_ms` можно оставить `NULL`
|
||||||
|
и вызывать варианты `LedIndicator_SetModeAt`/`LedIndicator_ProcessAt`.
|
||||||
|
|
||||||
|
## Быстрый старт без привязки к HAL
|
||||||
|
|
||||||
|
```c
|
||||||
|
static LedIndicator led;
|
||||||
|
static LedIndicator_Channel state[2];
|
||||||
|
LedIndicator_Config cfg;
|
||||||
|
LedIndicator_Port port = {Board_LedWrite, Board_TimerMs, &board};
|
||||||
|
|
||||||
|
LedIndicator_ConfigDefault(&cfg);
|
||||||
|
LedIndicator_Init(&led, state, 2U, &port, &cfg);
|
||||||
|
LedIndicator_SetMode(&led, 0U, LED_INDICATOR_MODE_WORK);
|
||||||
|
LedIndicator_SetMode(&led, 1U, LED_INDICATOR_MODE_ERROR);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
LedIndicator_Process(&led);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`SetMode` идемпотентен: повторный вызов того же режима в каждом проходе цикла
|
||||||
|
не начинает рисунок заново. Для нового импульса события служит
|
||||||
|
`LedIndicator_RestartMode`.
|
||||||
|
|
||||||
|
## STM32F103, STM32F4, STM32G431 и STM32G474
|
||||||
|
|
||||||
|
Порт `ports/stm32-hal` принимает конкретный `TIM_HandleTypeDef *`; библиотека
|
||||||
|
не использует `HAL_GetTick()` и не занимает SysTick. Настройте TIM как
|
||||||
|
free-running, запустите его и передайте частоту счётчика после prescaler.
|
||||||
|
|
||||||
|
Скопируйте подходящий `led_indicator_stm32_hal_config.*.template.h` в каталог
|
||||||
|
платы под именем `led_indicator_stm32_hal_config.h`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
static const LedIndicator_Stm32HalOutput outputs[] = {
|
||||||
|
{STATUS_GPIO_Port, STATUS_Pin, 0U},
|
||||||
|
{ERROR_GPIO_Port, ERROR_Pin, 1U}
|
||||||
|
};
|
||||||
|
static LedIndicator_Stm32HalPort hw;
|
||||||
|
static LedIndicator_Port port;
|
||||||
|
|
||||||
|
HAL_TIM_Base_Start(&htim6); /* CNT = 1 кГц в данном примере. */
|
||||||
|
LedIndicator_Stm32HalPortInit(&hw, &htim6, 1000U,
|
||||||
|
outputs, 2U, &port);
|
||||||
|
```
|
||||||
|
|
||||||
|
`LedIndicator_Process()` должен вызываться чаще, чем переполняется выбранный
|
||||||
|
TIM. Для 16-битного CNT на 1 кГц это не реже одного раза за 65 секунд. Сам
|
||||||
|
таймер и его prescaler/period задаются в CubeMX или board-порте, а не в ядре.
|
||||||
|
|
||||||
|
Для К1921ВК028 и C28x используется тот же основной порт: `now_ms` возвращает
|
||||||
|
счётчик, увеличиваемый обработчиком выбранного TIMER/CPU Timer, а `write`
|
||||||
|
обращается к GPIO SDK. Такое разделение оставляет номер таймера и выводы в
|
||||||
|
проекте конкретной платы.
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S c/led-indicator -B build/led-indicator
|
||||||
|
cmake --build build/led-indicator
|
||||||
|
ctest --test-dir build/led-indicator --output-on-failure
|
||||||
|
```
|
||||||
246
c/led-indicator/led_indicator.c
Normal file
246
c/led-indicator/led_indicator.c
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
#include "led_indicator.h"
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
|
#define LEVELS_1 0x01U
|
||||||
|
#define LEVELS_10 0x01U
|
||||||
|
#define LEVELS_1010 0x05U
|
||||||
|
#define LEVELS_101010 0x15U
|
||||||
|
|
||||||
|
static const LedIndicator_Pattern g_default_patterns[LED_INDICATOR_MODE_COUNT] = {
|
||||||
|
{{0U}, 0U, 1U, 1U, 0U},
|
||||||
|
{{0U}, LEVELS_1, 1U, 1U, 1U},
|
||||||
|
{{100U, 100U, 100U, 100U, 100U, 100U}, LEVELS_101010, 6U, 0U, 1U},
|
||||||
|
{{100U, 900U}, LEVELS_10, 2U, 1U, 0U},
|
||||||
|
{{50U, 50U}, LEVELS_10, 2U, 1U, 0U},
|
||||||
|
{{250U, 250U}, LEVELS_10, 2U, 1U, 0U},
|
||||||
|
{{100U, 100U, 100U, 700U}, LEVELS_1010, 4U, 1U, 0U},
|
||||||
|
{{100U, 100U, 100U, 100U, 100U, 700U}, LEVELS_101010, 6U, 1U, 0U}
|
||||||
|
};
|
||||||
|
|
||||||
|
static uint8_t pattern_valid(const LedIndicator_Pattern *pattern)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
uint32_t total = 0U;
|
||||||
|
|
||||||
|
if ((pattern == NULL) || (pattern->step_count == 0U) ||
|
||||||
|
(pattern->step_count > LED_INDICATOR_MAX_STEPS)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (pattern->step_count == 1U) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
for (i = 0U; i < pattern->step_count; ++i) {
|
||||||
|
if ((pattern->duration_ms[i] == 0U) ||
|
||||||
|
(UINT32_MAX - total < pattern->duration_ms[i])) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
total += pattern->duration_ms[i];
|
||||||
|
}
|
||||||
|
return (uint8_t)(total != 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t pattern_period(const LedIndicator_Pattern *pattern)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
uint32_t total = 0U;
|
||||||
|
|
||||||
|
for (i = 0U; i < pattern->step_count; ++i) {
|
||||||
|
total += pattern->duration_ms[i];
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t pattern_level(const LedIndicator_Pattern *pattern, uint32_t elapsed)
|
||||||
|
{
|
||||||
|
uint32_t position = elapsed;
|
||||||
|
uint32_t period;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if (pattern->step_count == 1U) {
|
||||||
|
return (uint8_t)(pattern->levels & 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
period = pattern_period(pattern);
|
||||||
|
if (pattern->repeat != 0U) {
|
||||||
|
position %= period;
|
||||||
|
} else if (position >= period) {
|
||||||
|
return (uint8_t)(pattern->final_level != 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0U; i < pattern->step_count; ++i) {
|
||||||
|
if (position < pattern->duration_ms[i]) {
|
||||||
|
return (uint8_t)((pattern->levels >> i) & 1U);
|
||||||
|
}
|
||||||
|
position -= pattern->duration_ms[i];
|
||||||
|
}
|
||||||
|
return (uint8_t)(pattern->final_level != 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_if_changed(LedIndicator *instance, size_t channel, uint8_t level,
|
||||||
|
uint8_t force)
|
||||||
|
{
|
||||||
|
LedIndicator_Channel *state = &instance->channels[channel];
|
||||||
|
|
||||||
|
level = (uint8_t)(level != 0U);
|
||||||
|
if ((force != 0U) || (state->output_level != level)) {
|
||||||
|
state->output_level = level;
|
||||||
|
instance->port.write(instance->port.context, (uint8_t)channel, level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LedIndicator_ConfigDefault(LedIndicator_Config *config)
|
||||||
|
{
|
||||||
|
if (config != NULL) {
|
||||||
|
config->patterns = g_default_patterns;
|
||||||
|
config->pattern_count = LED_INDICATOR_MODE_COUNT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t LedIndicator_CopyDefaultPatterns(LedIndicator_Pattern *patterns, size_t capacity)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
size_t count = (size_t)LED_INDICATOR_MODE_COUNT;
|
||||||
|
|
||||||
|
if (patterns == NULL) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (capacity < count) {
|
||||||
|
count = capacity;
|
||||||
|
}
|
||||||
|
for (i = 0U; i < count; ++i) {
|
||||||
|
patterns[i] = g_default_patterns[i];
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_ValidateConfig(const LedIndicator_Config *config)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if ((config == NULL) || (config->patterns == NULL) ||
|
||||||
|
(config->pattern_count == 0U) ||
|
||||||
|
(config->pattern_count > (size_t)LED_INDICATOR_MODE_COUNT)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
for (i = 0U; i < config->pattern_count; ++i) {
|
||||||
|
if (pattern_valid(&config->patterns[i]) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_Init(LedIndicator *instance,
|
||||||
|
LedIndicator_Channel *channels,
|
||||||
|
size_t channel_count,
|
||||||
|
const LedIndicator_Port *port,
|
||||||
|
const LedIndicator_Config *config)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if ((instance == NULL) || (channels == NULL) || (channel_count == 0U) ||
|
||||||
|
(channel_count > 256U) || (port == NULL) || (port->write == NULL) ||
|
||||||
|
(LedIndicator_ValidateConfig(config) == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance->port = *port;
|
||||||
|
instance->patterns = config->patterns;
|
||||||
|
instance->pattern_count = config->pattern_count;
|
||||||
|
instance->channels = channels;
|
||||||
|
instance->channel_count = channel_count;
|
||||||
|
instance->initialized = 1U;
|
||||||
|
|
||||||
|
for (i = 0U; i < channel_count; ++i) {
|
||||||
|
channels[i].mode = LED_INDICATOR_MODE_OFF;
|
||||||
|
channels[i].started_ms = 0U;
|
||||||
|
channels[i].output_level = 0U;
|
||||||
|
channels[i].initialized = 1U;
|
||||||
|
write_if_changed(instance, i, 0U, 1U);
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_SetModeAt(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode, uint32_t now_ms)
|
||||||
|
{
|
||||||
|
if ((instance == NULL) || (instance->initialized == 0U) ||
|
||||||
|
(channel >= instance->channel_count) || ((size_t)mode >= instance->pattern_count)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (instance->channels[channel].mode == mode) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
return LedIndicator_RestartModeAt(instance, channel, mode, now_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_RestartModeAt(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode, uint32_t now_ms)
|
||||||
|
{
|
||||||
|
LedIndicator_Channel *state;
|
||||||
|
const LedIndicator_Pattern *pattern;
|
||||||
|
|
||||||
|
if ((instance == NULL) || (instance->initialized == 0U) ||
|
||||||
|
(channel >= instance->channel_count) || ((size_t)mode >= instance->pattern_count)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = &instance->channels[channel];
|
||||||
|
state->mode = mode;
|
||||||
|
state->started_ms = now_ms;
|
||||||
|
pattern = &instance->patterns[(size_t)mode];
|
||||||
|
write_if_changed(instance, channel, pattern_level(pattern, 0U), 0U);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_SetMode(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode)
|
||||||
|
{
|
||||||
|
if ((instance == NULL) || (instance->port.now_ms == NULL)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return LedIndicator_SetModeAt(instance, channel, mode,
|
||||||
|
instance->port.now_ms(instance->port.context));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_RestartMode(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode)
|
||||||
|
{
|
||||||
|
if ((instance == NULL) || (instance->port.now_ms == NULL)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return LedIndicator_RestartModeAt(instance, channel, mode,
|
||||||
|
instance->port.now_ms(instance->port.context));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if ((instance == NULL) || (instance->initialized == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (i = 0U; i < instance->channel_count; ++i) {
|
||||||
|
const LedIndicator_Channel *state = &instance->channels[i];
|
||||||
|
const LedIndicator_Pattern *pattern = &instance->patterns[(size_t)state->mode];
|
||||||
|
const uint32_t elapsed = now_ms - state->started_ms;
|
||||||
|
write_if_changed(instance, i, pattern_level(pattern, elapsed), 0U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LedIndicator_Process(LedIndicator *instance)
|
||||||
|
{
|
||||||
|
if ((instance != NULL) && (instance->port.now_ms != NULL)) {
|
||||||
|
LedIndicator_ProcessAt(instance, instance->port.now_ms(instance->port.context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LedIndicator_Mode LedIndicator_GetMode(const LedIndicator *instance, size_t channel)
|
||||||
|
{
|
||||||
|
if ((instance == NULL) || (instance->initialized == 0U) ||
|
||||||
|
(channel >= instance->channel_count)) {
|
||||||
|
return LED_INDICATOR_MODE_OFF;
|
||||||
|
}
|
||||||
|
return instance->channels[channel].mode;
|
||||||
|
}
|
||||||
117
c/led-indicator/led_indicator.h
Normal file
117
c/led-indicator/led_indicator.h
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* @file led_indicator.h
|
||||||
|
* @brief Неблокирующая индикация состояния устройства на одном или нескольких LED.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LED_INDICATOR_H
|
||||||
|
#define LED_INDICATOR_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define LED_INDICATOR_MAX_STEPS 8U
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LED_INDICATOR_MODE_OFF = 0,
|
||||||
|
LED_INDICATOR_MODE_ON,
|
||||||
|
LED_INDICATOR_MODE_STARTUP,
|
||||||
|
LED_INDICATOR_MODE_WORK,
|
||||||
|
LED_INDICATOR_MODE_ACTIVITY,
|
||||||
|
LED_INDICATOR_MODE_WARNING,
|
||||||
|
LED_INDICATOR_MODE_ERROR,
|
||||||
|
LED_INDICATOR_MODE_CRITICAL,
|
||||||
|
LED_INDICATOR_MODE_COUNT
|
||||||
|
} LedIndicator_Mode;
|
||||||
|
|
||||||
|
/** Один период сигнала. Бит N в levels задаёт уровень шага N. */
|
||||||
|
typedef struct {
|
||||||
|
uint32_t duration_ms[LED_INDICATOR_MAX_STEPS];
|
||||||
|
uint8_t levels;
|
||||||
|
uint8_t step_count;
|
||||||
|
uint8_t repeat;
|
||||||
|
uint8_t final_level;
|
||||||
|
} LedIndicator_Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Аппаратный порт. write получает логический уровень, поэтому active-low,
|
||||||
|
* GPIO, PWM и регистры расширителя обрабатываются за границей ядра.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
void (*write)(void *context, uint8_t channel, uint8_t on);
|
||||||
|
uint32_t (*now_ms)(void *context);
|
||||||
|
void *context;
|
||||||
|
} LedIndicator_Port;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
LedIndicator_Mode mode;
|
||||||
|
uint32_t started_ms;
|
||||||
|
uint8_t output_level;
|
||||||
|
uint8_t initialized;
|
||||||
|
} LedIndicator_Channel;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const LedIndicator_Pattern *patterns;
|
||||||
|
size_t pattern_count;
|
||||||
|
} LedIndicator_Config;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
LedIndicator_Port port;
|
||||||
|
const LedIndicator_Pattern *patterns;
|
||||||
|
LedIndicator_Channel *channels;
|
||||||
|
size_t pattern_count;
|
||||||
|
size_t channel_count;
|
||||||
|
uint8_t initialized;
|
||||||
|
} LedIndicator;
|
||||||
|
|
||||||
|
/** Заполняет конфигурацию встроенными шаблонами режимов. */
|
||||||
|
void LedIndicator_ConfigDefault(LedIndicator_Config *config);
|
||||||
|
|
||||||
|
/** Копирует встроенные шаблоны в изменяемую таблицу; возвращает число записей. */
|
||||||
|
size_t LedIndicator_CopyDefaultPatterns(LedIndicator_Pattern *patterns, size_t capacity);
|
||||||
|
|
||||||
|
/** Проверяет таблицу шаблонов. */
|
||||||
|
uint8_t LedIndicator_ValidateConfig(const LedIndicator_Config *config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализирует экземпляр и немедленно выключает все его каналы.
|
||||||
|
* Массив channels принадлежит приложению и должен жить столько же, сколько instance.
|
||||||
|
*/
|
||||||
|
uint8_t LedIndicator_Init(LedIndicator *instance,
|
||||||
|
LedIndicator_Channel *channels,
|
||||||
|
size_t channel_count,
|
||||||
|
const LedIndicator_Port *port,
|
||||||
|
const LedIndicator_Config *config);
|
||||||
|
|
||||||
|
/** Назначает режим, используя время порта. Повтор того же режима не сбрасывает фазу. */
|
||||||
|
uint8_t LedIndicator_SetMode(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode);
|
||||||
|
|
||||||
|
/** Назначает режим с явно переданной меткой времени. Повтор не сбрасывает фазу. */
|
||||||
|
uint8_t LedIndicator_SetModeAt(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode, uint32_t now_ms);
|
||||||
|
|
||||||
|
/** Принудительно запускает режим с первого шага, используя время порта. */
|
||||||
|
uint8_t LedIndicator_RestartMode(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode);
|
||||||
|
|
||||||
|
/** Принудительно запускает режим с первого шага в указанное время. */
|
||||||
|
uint8_t LedIndicator_RestartModeAt(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode, uint32_t now_ms);
|
||||||
|
|
||||||
|
/** Обновляет выходы, используя время порта. Вызывать в главном цикле. */
|
||||||
|
void LedIndicator_Process(LedIndicator *instance);
|
||||||
|
|
||||||
|
/** Обновляет выходы с явно переданной меткой времени. */
|
||||||
|
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms);
|
||||||
|
|
||||||
|
LedIndicator_Mode LedIndicator_GetMode(const LedIndicator *instance, size_t channel);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* LED_INDICATOR_H */
|
||||||
65
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.c
Normal file
65
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.c
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#include "led_indicator_stm32_hal.h"
|
||||||
|
|
||||||
|
static void stm32_write(void *context, uint8_t channel, uint8_t on)
|
||||||
|
{
|
||||||
|
LedIndicator_Stm32HalPort *adapter = (LedIndicator_Stm32HalPort *)context;
|
||||||
|
const LedIndicator_Stm32HalOutput *output;
|
||||||
|
GPIO_PinState state;
|
||||||
|
|
||||||
|
if ((adapter == NULL) || ((size_t)channel >= adapter->output_count)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
output = &adapter->outputs[channel];
|
||||||
|
state = ((on != 0U) ^ (output->active_low != 0U)) ? GPIO_PIN_SET : GPIO_PIN_RESET;
|
||||||
|
HAL_GPIO_WritePin(output->gpio, output->pin, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t stm32_now_ms(void *context)
|
||||||
|
{
|
||||||
|
LedIndicator_Stm32HalPort *adapter = (LedIndicator_Stm32HalPort *)context;
|
||||||
|
const uint32_t current = __HAL_TIM_GET_COUNTER(adapter->timer);
|
||||||
|
const uint32_t reload = __HAL_TIM_GET_AUTORELOAD(adapter->timer);
|
||||||
|
uint32_t delta;
|
||||||
|
uint64_t scaled;
|
||||||
|
|
||||||
|
if (current >= adapter->last_counter) {
|
||||||
|
delta = current - adapter->last_counter;
|
||||||
|
} else if (reload == UINT32_MAX) {
|
||||||
|
delta = current - adapter->last_counter;
|
||||||
|
} else {
|
||||||
|
delta = (reload - adapter->last_counter) + 1U + current;
|
||||||
|
}
|
||||||
|
adapter->last_counter = current;
|
||||||
|
|
||||||
|
scaled = (uint64_t)adapter->remainder + ((uint64_t)delta * 1000ULL);
|
||||||
|
adapter->accumulated_ms += (uint32_t)(scaled / adapter->timer_tick_hz);
|
||||||
|
adapter->remainder = (uint32_t)(scaled % adapter->timer_tick_hz);
|
||||||
|
return adapter->accumulated_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_Stm32HalPortInit(LedIndicator_Stm32HalPort *adapter,
|
||||||
|
TIM_HandleTypeDef *timer,
|
||||||
|
uint32_t timer_tick_hz,
|
||||||
|
const LedIndicator_Stm32HalOutput *outputs,
|
||||||
|
size_t output_count,
|
||||||
|
LedIndicator_Port *port)
|
||||||
|
{
|
||||||
|
if ((adapter == NULL) || (timer == NULL) || (timer_tick_hz == 0U) ||
|
||||||
|
(outputs == NULL) || (output_count == 0U) || (output_count > 256U) ||
|
||||||
|
(port == NULL)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter->timer = timer;
|
||||||
|
adapter->outputs = outputs;
|
||||||
|
adapter->output_count = output_count;
|
||||||
|
adapter->timer_tick_hz = timer_tick_hz;
|
||||||
|
adapter->last_counter = __HAL_TIM_GET_COUNTER(timer);
|
||||||
|
adapter->accumulated_ms = 0U;
|
||||||
|
adapter->remainder = 0U;
|
||||||
|
|
||||||
|
port->write = stm32_write;
|
||||||
|
port->now_ms = stm32_now_ms;
|
||||||
|
port->context = adapter;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
39
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.h
Normal file
39
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* @file led_indicator_stm32_hal.h
|
||||||
|
* @brief Порт LED Indicator для STM32F1/F4/G4 HAL и выбранного TIM.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LED_INDICATOR_STM32_HAL_H
|
||||||
|
#define LED_INDICATOR_STM32_HAL_H
|
||||||
|
|
||||||
|
#include "led_indicator.h"
|
||||||
|
#include "led_indicator_stm32_hal_config.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
GPIO_TypeDef *gpio;
|
||||||
|
uint16_t pin;
|
||||||
|
uint8_t active_low;
|
||||||
|
} LedIndicator_Stm32HalOutput;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
TIM_HandleTypeDef *timer;
|
||||||
|
const LedIndicator_Stm32HalOutput *outputs;
|
||||||
|
size_t output_count;
|
||||||
|
uint32_t timer_tick_hz;
|
||||||
|
uint32_t last_counter;
|
||||||
|
uint32_t accumulated_ms;
|
||||||
|
uint32_t remainder;
|
||||||
|
} LedIndicator_Stm32HalPort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создаёт порт на уже настроенном и запущенном таймере.
|
||||||
|
* timer_tick_hz — частота изменения CNT после prescaler, например 1000 Гц.
|
||||||
|
*/
|
||||||
|
uint8_t LedIndicator_Stm32HalPortInit(LedIndicator_Stm32HalPort *adapter,
|
||||||
|
TIM_HandleTypeDef *timer,
|
||||||
|
uint32_t timer_tick_hz,
|
||||||
|
const LedIndicator_Stm32HalOutput *outputs,
|
||||||
|
size_t output_count,
|
||||||
|
LedIndicator_Port *port);
|
||||||
|
|
||||||
|
#endif /* LED_INDICATOR_STM32_HAL_H */
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
#define LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
|
||||||
|
#include "stm32f1xx_hal.h"
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
#define LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
|
||||||
|
#include "stm32f4xx_hal.h"
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
#define LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
|
||||||
|
#include "stm32g4xx_hal.h"
|
||||||
|
|
||||||
|
#endif
|
||||||
27
c/led-indicator/tests/fakes/led_indicator_stm32_hal_config.h
Normal file
27
c/led-indicator/tests/fakes/led_indicator_stm32_hal_config.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
#define LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GPIO_PIN_RESET = 0,
|
||||||
|
GPIO_PIN_SET
|
||||||
|
} GPIO_PinState;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t last_pin;
|
||||||
|
GPIO_PinState last_state;
|
||||||
|
uint32_t writes;
|
||||||
|
} GPIO_TypeDef;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t counter;
|
||||||
|
uint32_t autoreload;
|
||||||
|
} TIM_HandleTypeDef;
|
||||||
|
|
||||||
|
#define __HAL_TIM_GET_COUNTER(handle) ((handle)->counter)
|
||||||
|
#define __HAL_TIM_GET_AUTORELOAD(handle) ((handle)->autoreload)
|
||||||
|
|
||||||
|
void HAL_GPIO_WritePin(GPIO_TypeDef *gpio, uint16_t pin, GPIO_PinState state);
|
||||||
|
|
||||||
|
#endif
|
||||||
84
c/led-indicator/tests/test_led_indicator.c
Normal file
84
c/led-indicator/tests/test_led_indicator.c
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#include "led_indicator.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t now;
|
||||||
|
uint8_t outputs[3];
|
||||||
|
uint32_t writes[3];
|
||||||
|
} FakePort;
|
||||||
|
|
||||||
|
static void fake_write(void *context, uint8_t channel, uint8_t on)
|
||||||
|
{
|
||||||
|
FakePort *fake = (FakePort *)context;
|
||||||
|
fake->outputs[channel] = on;
|
||||||
|
fake->writes[channel]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t fake_now(void *context)
|
||||||
|
{
|
||||||
|
return ((FakePort *)context)->now;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CHECK(condition) \
|
||||||
|
do { \
|
||||||
|
if (!(condition)) { \
|
||||||
|
fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \
|
||||||
|
#condition); \
|
||||||
|
return 1; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
LedIndicator indicator;
|
||||||
|
LedIndicator_Channel channels[3];
|
||||||
|
LedIndicator_Config config;
|
||||||
|
LedIndicator_Pattern patterns[LED_INDICATOR_MODE_COUNT];
|
||||||
|
FakePort fake = {0};
|
||||||
|
LedIndicator_Port port = {fake_write, fake_now, &fake};
|
||||||
|
|
||||||
|
LedIndicator_ConfigDefault(&config);
|
||||||
|
CHECK(LedIndicator_ValidateConfig(&config) != 0U);
|
||||||
|
CHECK(LedIndicator_CopyDefaultPatterns(patterns, LED_INDICATOR_MODE_COUNT) ==
|
||||||
|
LED_INDICATOR_MODE_COUNT);
|
||||||
|
patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] = 25U;
|
||||||
|
CHECK(patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] == 25U);
|
||||||
|
CHECK(LedIndicator_Init(&indicator, channels, 3U, &port, &config) != 0U);
|
||||||
|
CHECK(fake.writes[0] == 1U && fake.writes[1] == 1U && fake.writes[2] == 1U);
|
||||||
|
|
||||||
|
CHECK(LedIndicator_SetMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
|
||||||
|
CHECK(fake.outputs[0] == 1U);
|
||||||
|
fake.now = 99U;
|
||||||
|
CHECK(LedIndicator_SetMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
|
||||||
|
LedIndicator_Process(&indicator);
|
||||||
|
CHECK(fake.outputs[0] == 1U);
|
||||||
|
fake.now = 100U;
|
||||||
|
LedIndicator_Process(&indicator);
|
||||||
|
CHECK(fake.outputs[0] == 0U);
|
||||||
|
fake.now = 1000U;
|
||||||
|
LedIndicator_Process(&indicator);
|
||||||
|
CHECK(fake.outputs[0] == 1U);
|
||||||
|
fake.now = 1050U;
|
||||||
|
CHECK(LedIndicator_RestartMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
|
||||||
|
fake.now = 1150U;
|
||||||
|
LedIndicator_Process(&indicator);
|
||||||
|
CHECK(fake.outputs[0] == 0U);
|
||||||
|
|
||||||
|
CHECK(LedIndicator_SetModeAt(&indicator, 1U, LED_INDICATOR_MODE_ERROR,
|
||||||
|
0xFFFFFFF0UL) != 0U);
|
||||||
|
LedIndicator_ProcessAt(&indicator, 0x00000054UL);
|
||||||
|
CHECK(fake.outputs[1] == 0U); /* 100 ms после старта, переход во второй шаг. */
|
||||||
|
LedIndicator_ProcessAt(&indicator, 0x000000B8UL);
|
||||||
|
CHECK(fake.outputs[1] == 1U); /* 200 ms после старта, второй импульс. */
|
||||||
|
|
||||||
|
CHECK(LedIndicator_SetModeAt(&indicator, 2U, LED_INDICATOR_MODE_STARTUP, 10U) != 0U);
|
||||||
|
LedIndicator_ProcessAt(&indicator, 610U);
|
||||||
|
CHECK(fake.outputs[2] == 1U); /* Однократный startup завершён постоянным ON. */
|
||||||
|
|
||||||
|
CHECK(LedIndicator_SetMode(&indicator, 3U, LED_INDICATOR_MODE_ON) == 0U);
|
||||||
|
CHECK(LedIndicator_GetMode(&indicator, 0U) == LED_INDICATOR_MODE_WORK);
|
||||||
|
|
||||||
|
puts("led indicator tests passed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
52
c/led-indicator/tests/test_stm32_port.c
Normal file
52
c/led-indicator/tests/test_stm32_port.c
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#include "led_indicator_stm32_hal.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
void HAL_GPIO_WritePin(GPIO_TypeDef *gpio, uint16_t pin, GPIO_PinState state)
|
||||||
|
{
|
||||||
|
gpio->last_pin = pin;
|
||||||
|
gpio->last_state = state;
|
||||||
|
gpio->writes++;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CHECK(condition) \
|
||||||
|
do { \
|
||||||
|
if (!(condition)) { \
|
||||||
|
fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \
|
||||||
|
#condition); \
|
||||||
|
return 1; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
GPIO_TypeDef gpio_a = {0};
|
||||||
|
GPIO_TypeDef gpio_b = {0};
|
||||||
|
TIM_HandleTypeDef timer = {100U, 65535U};
|
||||||
|
const LedIndicator_Stm32HalOutput outputs[] = {
|
||||||
|
{&gpio_a, 0x0001U, 0U},
|
||||||
|
{&gpio_b, 0x0080U, 1U}
|
||||||
|
};
|
||||||
|
LedIndicator_Stm32HalPort adapter;
|
||||||
|
LedIndicator_Port port;
|
||||||
|
|
||||||
|
CHECK(LedIndicator_Stm32HalPortInit(&adapter, &timer, 10000U,
|
||||||
|
outputs, 2U, &port) != 0U);
|
||||||
|
port.write(port.context, 0U, 1U);
|
||||||
|
CHECK(gpio_a.last_pin == 0x0001U && gpio_a.last_state == GPIO_PIN_SET);
|
||||||
|
port.write(port.context, 1U, 1U);
|
||||||
|
CHECK(gpio_b.last_pin == 0x0080U && gpio_b.last_state == GPIO_PIN_RESET);
|
||||||
|
|
||||||
|
timer.counter = 109U;
|
||||||
|
CHECK(port.now_ms(port.context) == 0U);
|
||||||
|
timer.counter = 110U;
|
||||||
|
CHECK(port.now_ms(port.context) == 1U);
|
||||||
|
|
||||||
|
timer.counter = 65530U;
|
||||||
|
(void)port.now_ms(port.context);
|
||||||
|
timer.counter = 4U;
|
||||||
|
CHECK(port.now_ms(port.context) == 6544U);
|
||||||
|
|
||||||
|
puts("led indicator STM32 port tests passed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
26
c/parallel-nand/CMakeLists.txt
Normal file
26
c/parallel-nand/CMakeLists.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(parallel_nand C)
|
||||||
|
|
||||||
|
set(CMAKE_C_STANDARD 99)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
add_library(parallel_nand
|
||||||
|
src/parallel_nand.c
|
||||||
|
src/parallel_nand_gas.c
|
||||||
|
)
|
||||||
|
target_include_directories(parallel_nand PUBLIC
|
||||||
|
include
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(parallel_nand_pcan_gas src/parallel_nand_pcan_gas.c)
|
||||||
|
target_include_directories(parallel_nand_pcan_gas PUBLIC
|
||||||
|
include
|
||||||
|
../set-protocol/include
|
||||||
|
)
|
||||||
|
target_link_libraries(parallel_nand_pcan_gas PUBLIC parallel_nand)
|
||||||
|
|
||||||
|
add_executable(test_parallel_nand tests/test_parallel_nand.c)
|
||||||
|
target_link_libraries(test_parallel_nand PRIVATE parallel_nand)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
add_test(NAME parallel_nand_core COMMAND test_parallel_nand)
|
||||||
117
c/parallel-nand/README.md
Normal file
117
c/parallel-nand/README.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# parallel-nand
|
||||||
|
|
||||||
|
Переносимый C99-драйвер асинхронной parallel NAND x8 и окно управления через
|
||||||
|
общее адресное пространство GAS. Есть профили Micron `MT29F1G08ABADA` и
|
||||||
|
Hynix `HY27UF084G2M` (512 MiB, 2048+64 байта, 64 страницы в блоке,
|
||||||
|
4096 блоков, ID `AD DC 80 95`).
|
||||||
|
|
||||||
|
Подготовлены аппаратные порты:
|
||||||
|
|
||||||
|
- `TMS320F2812` — XINTF Zone 6;
|
||||||
|
- `STM32F407VET6` — аппаратный FSMC NAND Bank 2;
|
||||||
|
- `STM32F103RCT6` — GPIO bit-bang, так как в LQFP64 FSMC недоступен;
|
||||||
|
- `STM32F103ZET6` — аппаратный FSMC NAND Bank 3;
|
||||||
|
- `STM32G474CEU6` — GPIO bit-bang для корпуса UFQFPN-48.
|
||||||
|
|
||||||
|
Ядро не зависит от HAL и транспорта GUI. STM32 HAL используется только в
|
||||||
|
конкретных портах. `parallel_nand_gas` реализует переносимую 16-битную карту
|
||||||
|
GAS и компилируется в том числе TI C28x, где `char` имеет 16 бит.
|
||||||
|
|
||||||
|
## Подключение
|
||||||
|
|
||||||
|
В проект добавить:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c/parallel-nand/src/parallel_nand.c
|
||||||
|
c/parallel-nand/src/parallel_nand_gas.c
|
||||||
|
```
|
||||||
|
|
||||||
|
Пути заголовков:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c/parallel-nand/include
|
||||||
|
c/parallel-nand/ports/<нужный-порт>
|
||||||
|
```
|
||||||
|
|
||||||
|
И один аппаратный порт, например:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c/parallel-nand/ports/stm32g474ce/parallel_nand_port_stm32g474ce.c
|
||||||
|
```
|
||||||
|
|
||||||
|
Минимальная инициализация:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static parallel_nand_t nand;
|
||||||
|
static parallel_nand_gas_t nand_gas;
|
||||||
|
|
||||||
|
void memory_init(void)
|
||||||
|
{
|
||||||
|
int rc = parallel_nand_init(&nand,
|
||||||
|
parallel_nand_port_stm32g474ce(),
|
||||||
|
¶llel_nand_mt29f1g08_geometry);
|
||||||
|
if (rc != PNAND_OK) {
|
||||||
|
/* Оставить диагностику доступной GUI и сообщить ошибку платы. */
|
||||||
|
}
|
||||||
|
|
||||||
|
parallel_nand_gas_init(&nand_gas, &nand, PNAND_GAS_DEFAULT_BASE);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Для Hynix на STM32F103ZET6 используйте
|
||||||
|
`parallel_nand_port_stm32f103ze()` и
|
||||||
|
`parallel_nand_hy27uf084g2m_geometry`; распиновка и ограничения приведены в
|
||||||
|
[`ports/stm32f103ze/README.md`](ports/stm32f103ze/README.md). Порт RCT6
|
||||||
|
с GPIO bit-bang сохранён как отдельный вариант для корпуса LQFP64.
|
||||||
|
|
||||||
|
Диспетчер общего адресного пространства вызывает
|
||||||
|
`parallel_nand_gas_read()`/`parallel_nand_gas_write()` для адресов
|
||||||
|
`0xE000..0xE51F`. Для STM32 с `c/set-protocol/pcan_gas` дополнительно включить
|
||||||
|
`src/parallel_nand_pcan_gas.c`, создать `pcan_gas_region_t` и вызвать
|
||||||
|
`parallel_nand_pcan_gas_region_init()`. Этот адаптер намеренно не нужен TMS.
|
||||||
|
Для полного дампа предпочтительны пакетные `READ_REGISTERS/WRITE_REGISTERS`,
|
||||||
|
а classic CAN GAS оставлен для совместимости и коротких диагностических чтений.
|
||||||
|
|
||||||
|
Доступ GUI и алгоритм полного дампа описаны в
|
||||||
|
[docs/MEMORY_GAS.md](docs/MEMORY_GAS.md). Электрические соединения — в README
|
||||||
|
соответствующего порта и [docs/EMPTY_BOARD_PINOUT.md](docs/EMPTY_BOARD_PINOUT.md).
|
||||||
|
|
||||||
|
## PROGRAM и ERASE
|
||||||
|
|
||||||
|
- `parallel_nand_program_page()` записывает полную main-область и, при
|
||||||
|
необходимости, полную OOB-область страницы.
|
||||||
|
- `parallel_nand_erase_block()` стирает один физический блок.
|
||||||
|
- Перед изменением ядро проверяет заводской bad-block marker, на время операции
|
||||||
|
снимает `WP#`, ждёт `R/B#`, проверяет fail-бит статуса и снова включает
|
||||||
|
защиту при любом результате.
|
||||||
|
- Порт обязан объявить `write_supported = 1`; иначе возвращается
|
||||||
|
`PNAND_ERROR_WRITE_PROTECTED`. Это защищает платы, где `WP#` постоянно
|
||||||
|
соединён с GND.
|
||||||
|
- Через GAS разрушительные команды требуют записи `0xA55A` в `CONFIRM`
|
||||||
|
непосредственно перед `PROGRAM_PAGE` или `ERASE_BLOCK`.
|
||||||
|
|
||||||
|
NAND можно программировать только из `1` в `0`; для повторной записи сначала
|
||||||
|
стирается весь блок. Не стирайте заводские bad-блоки и не используйте raw-запись
|
||||||
|
как файловую систему без ECC, wear leveling и защиты загрузочных блоков.
|
||||||
|
|
||||||
|
## Текущие границы
|
||||||
|
|
||||||
|
- Реализованы чтение, программирование страницы, стирание блока, RESET,
|
||||||
|
READ ID, STATUS и проверка bad-block marker.
|
||||||
|
- ECC пока не исправляет данные. GUI получает физические main/OOB и должен
|
||||||
|
помечать дамп как raw.
|
||||||
|
- В полном дампе bad-блоки не пропускаются: сохраняется физический порядок.
|
||||||
|
- Для другой NAND нужно передать другую `parallel_nand_geometry_t` и проверить
|
||||||
|
команды/маркер по её datasheet.
|
||||||
|
|
||||||
|
## Проверка первого запуска
|
||||||
|
|
||||||
|
1. Запустить `parallel_nand_init()`.
|
||||||
|
2. Через GAS проверить `0xE000 = 0x4E44`.
|
||||||
|
3. Записать `2` в `0xE004` (`READ_ID`).
|
||||||
|
4. Проверить `0xE00B = 0x002C`, `0xE00C = 0x00F1`.
|
||||||
|
5. Записать номер страницы в `0xE005/0xE006`, затем `3` в `0xE004`.
|
||||||
|
6. Дождаться `READY | PAGE_VALID` в `0xE002`.
|
||||||
|
7. Прочитать main из `0xE100..0xE4FF`, OOB из `0xE500..0xE51F`.
|
||||||
|
8. PROGRAM/ERASE проверять только на заведомо расходном блоке после сохранения
|
||||||
|
полного raw-дампа.
|
||||||
83
c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md
Normal file
83
c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Подключение parallel NAND на пустой плате
|
||||||
|
|
||||||
|
Документ рассчитан на асинхронную NAND x8 `MT29F1G08ABADA` 3,3 В в корпусе
|
||||||
|
TSOP-48 (суффикс `WP`). На второй фотографии NAND — длинная микросхема справа
|
||||||
|
с маркировкой `29F1G08ABADA`. Микросхема Micron `D9MDK` сверху — DDR2 SDRAM,
|
||||||
|
не NAND. На первой фотографии `AM29LV800BT` — parallel NOR, а не NAND.
|
||||||
|
|
||||||
|
Уточнение по ёмкости с фотографий:
|
||||||
|
|
||||||
|
- `K6R4008V1D` — SRAM 4 Mbit (512 KiB), не Flash;
|
||||||
|
- `AM29LV800BT` — parallel NOR 8 Mbit (1 MiB);
|
||||||
|
- `MT29F1G08ABADA` — parallel NAND 1 Gbit, то есть 128 MiB main area.
|
||||||
|
|
||||||
|
Поэтому текущий NAND-профиль рассчитан на 1-Gbit чип со второй платы. Для
|
||||||
|
другой плотности меняется `parallel_nand_geometry_t`; GAS и GUI-команды
|
||||||
|
остаются теми же.
|
||||||
|
|
||||||
|
Не переносите номера выводов TSOP-48 на вариант `H4` VFBGA-63. Для BGA нужно
|
||||||
|
использовать ball map конкретного полного part number.
|
||||||
|
|
||||||
|
## TSOP-48, x8
|
||||||
|
|
||||||
|
| NAND | Вывод | Назначение |
|
||||||
|
|---|---:|---|
|
||||||
|
| `R/B#` | 7 | open-drain Ready/Busy, подтяжка 4,7–10 кОм к 3,3 В |
|
||||||
|
| `RE#` | 8 | строб чтения от МК |
|
||||||
|
| `CE#` | 9 | выбор кристалла от МК |
|
||||||
|
| `VCC` | 12, 37 | питание 3,3 В |
|
||||||
|
| `VSS` | 13, 36 | земля |
|
||||||
|
| `CLE` | 16 | фиксация команды |
|
||||||
|
| `ALE` | 17 | фиксация адреса |
|
||||||
|
| `WE#` | 18 | строб команды/адреса от МК |
|
||||||
|
| `WP#` | 19 | GPIO МК и pull-down 10 кОм; только для чтения допустим GND через 10 кОм |
|
||||||
|
| `I/O0` | 29 | двунаправленная шина, бит 0 |
|
||||||
|
| `I/O1` | 30 | двунаправленная шина, бит 1 |
|
||||||
|
| `I/O2` | 31 | двунаправленная шина, бит 2 |
|
||||||
|
| `I/O3` | 32 | двунаправленная шина, бит 3 |
|
||||||
|
| `I/O4` | 41 | двунаправленная шина, бит 4 |
|
||||||
|
| `I/O5` | 42 | двунаправленная шина, бит 5 |
|
||||||
|
| `I/O6` | 43 | двунаправленная шина, бит 6 |
|
||||||
|
| `I/O7` | 44 | двунаправленная шина, бит 7 |
|
||||||
|
| `VCC1` | 34, 39 | соединить с 3,3 В для совместимости корпуса/ONFI |
|
||||||
|
| `VSS1` | 25, 48 | соединить с GND для совместимости корпуса/ONFI |
|
||||||
|
| `DNU` | 38, 47 | оставить неподключёнными |
|
||||||
|
| `NC` | остальные | не подключать |
|
||||||
|
|
||||||
|
У каждого VCC/VCC1 поставить 100 нФ на ближайший VSS/VSS1; рядом с NAND —
|
||||||
|
общий 1–4,7 мкФ. Сигналы не должны превышать питание NAND. Для трасс длиннее
|
||||||
|
нескольких сантиметров предусмотреть последовательные резисторы 22–47 Ом у
|
||||||
|
источника на `WE#`, `RE#`, `CLE`, `ALE` и при необходимости на данных.
|
||||||
|
|
||||||
|
## Соединение с МК
|
||||||
|
|
||||||
|
| NAND | TMS320F2812 | STM32F407VET6 | STM32G474CEU6 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `I/O0..7` | `XD0..XD7` | `PD14,PD15,PD0,PD1,PE7..PE10` | `PB0..PB7` |
|
||||||
|
| `CLE` | `XA0` | `PD11/FSMC_A16` | `PA0` |
|
||||||
|
| `ALE` | `XA1` | `PD12/FSMC_A17` | `PA1` |
|
||||||
|
| `WE#` | `XWE` | `PD5/FSMC_NWE` | `PA2` |
|
||||||
|
| `RE#` | `XRD` | `PD4/FSMC_NOE` | `PA3` |
|
||||||
|
| `CE#` | `XZCS6AND7` | `PD7/FSMC_NE2` | `PA4` |
|
||||||
|
| `WP#` | GND через 10 кОм | GND через 10 кОм | `PA5` и pull-down 10 кОм |
|
||||||
|
| `R/B#` | `GPIOE0` | `PD6/FSMC_NWAIT` | `PA6` |
|
||||||
|
|
||||||
|
Подробные номера выводов корпуса МК и ограничения находятся в README каждого
|
||||||
|
каталога `ports/`. Перед изготовлением платы нужно сверить выбранный корпус и
|
||||||
|
полный part number NAND с актуальным datasheet.
|
||||||
|
|
||||||
|
## Первый запуск
|
||||||
|
|
||||||
|
1. Не устанавливать NAND и проверить 3,3 В, отсутствие КЗ и уровни `CE#/WE#/RE#`.
|
||||||
|
2. Установить NAND, проверить pull-down и уровень `WP# = 0`, выполнить RESET `FFh`.
|
||||||
|
3. Прочитать ID `90h`: ожидаемые первые байты для профиля — `2C F1`.
|
||||||
|
4. Считать одну страницу два раза и сравнить main/OOB побайтно.
|
||||||
|
5. Только после устойчивого чтения запускать полный дамп через GAS.
|
||||||
|
6. PROGRAM/ERASE проверять на расходном блоке после сохранения raw-дампа;
|
||||||
|
убедиться осциллографом, что `WP#` поднимается только на время операции.
|
||||||
|
|
||||||
|
## Источники для проверки footprint
|
||||||
|
|
||||||
|
- [Micron: каталог SLC NAND](https://www.micron.com/products/storage/nand-flash/slc-nand/part-catalog)
|
||||||
|
- [MT29F1G08ABADA: signal assignments и геометрия](https://helpdesk.trx.pl/pliki/SERWER/Micron-MT29F1G08ABADAWP-IT%20D-datasheet.pdf)
|
||||||
|
- [Micron FBGA part decoder](https://in.micron.com/sales-support/design-tools/fbga-parts-decoder)
|
||||||
207
c/parallel-nand/docs/MEMORY_GAS.md
Normal file
207
c/parallel-nand/docs/MEMORY_GAS.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Работа с parallel NAND через GAS
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
Внешняя NAND значительно больше общего адресного пространства GAS. Например,
|
||||||
|
`MT29F1G08ABADA` содержит 128 МиБ main area, а GAS имеет только 65536
|
||||||
|
16-битных регистров. Поэтому NAND представлена не линейным диапазоном, а
|
||||||
|
банковым окном одной физической страницы.
|
||||||
|
|
||||||
|
```text
|
||||||
|
SETGUI
|
||||||
|
│ GUI protocol v1: READ_REGISTERS / WRITE_REGISTERS
|
||||||
|
▼
|
||||||
|
карта GAS 0x0000..0xFFFF
|
||||||
|
│ 0xE000: управление, 0xE100: main, 0xE500: OOB
|
||||||
|
▼
|
||||||
|
parallel_nand_gas
|
||||||
|
▼
|
||||||
|
общее ядро parallel_nand
|
||||||
|
├─ TMS320F2812: XINTF
|
||||||
|
├─ STM32F103RC: GPIO
|
||||||
|
├─ STM32F103ZE: FSMC
|
||||||
|
├─ STM32F407VE: FSMC
|
||||||
|
└─ STM32G474CE: GPIO
|
||||||
|
```
|
||||||
|
|
||||||
|
GUI работает одинаково со всеми МК. Тип процессора влияет только на выбранный
|
||||||
|
файл из `ports/`.
|
||||||
|
|
||||||
|
Встроенный проект должен направлять весь диапазон `0xE000..0xE51F` в
|
||||||
|
`parallel_nand_gas_read()` и `parallel_nand_gas_write()`. Их аргументы имеют
|
||||||
|
тип `unsigned int`: на C28x это нативное 16-битное слово, а на ARM функции
|
||||||
|
явно оставляют только младшие 16 бит. Так карта и wire-формат совпадают без
|
||||||
|
предположения, что C `char` всегда восьмибитный.
|
||||||
|
|
||||||
|
Для STM32, использующего карту `pcan_gas`, готов адаптер:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "parallel_nand_pcan_gas.h"
|
||||||
|
|
||||||
|
static pcan_gas_region_t nand_region;
|
||||||
|
parallel_nand_pcan_gas_region_init(&nand_region, &nand_gas);
|
||||||
|
```
|
||||||
|
|
||||||
|
Для TMS320F2812 переносимый обработчик вызывается прямо из существующего GAS
|
||||||
|
диспетчера:
|
||||||
|
|
||||||
|
```c
|
||||||
|
if (address >= nand_gas.base &&
|
||||||
|
address < nand_gas.base + PNAND_GAS_REGION_REGS) {
|
||||||
|
return parallel_nand_gas_read(&nand_gas, address, value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если тип результата диспетчера отличается, статусы `PNAND_GAS_OK`,
|
||||||
|
`PNAND_GAS_NO_REG`, `PNAND_GAS_READ_ONLY` и `PNAND_GAS_REJECTED`
|
||||||
|
преобразуются в его локальные статусы так же, как это сделано в
|
||||||
|
`parallel_nand_pcan_gas.c`.
|
||||||
|
|
||||||
|
## Транспорт до GUI
|
||||||
|
|
||||||
|
Карта GAS не привязана к физическому каналу. Рекомендуемый путь для полного
|
||||||
|
дампа — используемые SETGUI сообщения `WRITE_REGISTERS (0x0A)` и
|
||||||
|
`READ_REGISTERS (0x09)`: они позволяют передавать крупные блоки поверх USB,
|
||||||
|
UART, RS-485 или Ethernet. Адаптер `pcan_gas` нужен, когда теми же регистрами
|
||||||
|
обмениваются по classic CAN; там один кадр несёт только четыре регистра, и
|
||||||
|
полный 128-MiB дамп будет значительно медленнее.
|
||||||
|
|
||||||
|
Устройство не должно собирать весь дамп в RAM. Буферизуется только одна
|
||||||
|
страница. Из-за переносимости на C28x каждый octet хранится в `unsigned short`,
|
||||||
|
поэтому статические буферы GAS занимают примерно 4,2 KiB RAM и на TMS, и на
|
||||||
|
ARM.
|
||||||
|
|
||||||
|
## Карта GAS
|
||||||
|
|
||||||
|
Базовый адрес по умолчанию — `0xE000`. Его можно изменить при вызове
|
||||||
|
`parallel_nand_gas_init()`; все смещения ниже останутся прежними.
|
||||||
|
|
||||||
|
| Адрес | Доступ | Название | Значение |
|
||||||
|
|---:|:---:|---|---|
|
||||||
|
| `E000` | R | `SIGNATURE` | `0x4E44` (`ND`) |
|
||||||
|
| `E001` | R | `VERSION` | версия окна, сейчас 2 |
|
||||||
|
| `E002` | R | `STATUS` | состояние операции |
|
||||||
|
| `E003` | R | `ERROR` | код `PNAND_*`, 0 = нет ошибки |
|
||||||
|
| `E004` | W | `COMMAND` | команда окна |
|
||||||
|
| `E005` | R/W | `PAGE_LO` | младшие 16 бит физической страницы |
|
||||||
|
| `E006` | R/W | `PAGE_HI` | старшие 16 бит физической страницы |
|
||||||
|
| `E007` | R | `PAGE_MAIN` | main bytes, для MT29 = 2048 |
|
||||||
|
| `E008` | R | `PAGE_OOB` | OOB bytes, для MT29 = 64 |
|
||||||
|
| `E009` | R | `PAGES_PER_BLOCK` | для MT29 = 64 |
|
||||||
|
| `E00A` | R | `BLOCKS` | для MT29 = 1024 |
|
||||||
|
| `E00B` | R | `MANUFACTURER_ID` | ожидается `0x2C` |
|
||||||
|
| `E00C` | R | `DEVICE_ID` | ожидается `0xF1` |
|
||||||
|
| `E00D` | R | `BAD_BLOCK` | 1 для физически плохого блока |
|
||||||
|
| `E00E` | R | `GENERATION_LO` | счётчик загруженных страниц, low |
|
||||||
|
| `E00F` | R | `GENERATION_HI` | счётчик загруженных страниц, high |
|
||||||
|
| `E010` | W | `CONFIRM` | `0xA55A` непосредственно перед разрушительной командой |
|
||||||
|
| `E100..E4FF` | R/W | `PAGE_DATA` | 1024 регистра main, два байта LE |
|
||||||
|
| `E500..E51F` | R/W | `PAGE_OOB` | 32 регистра OOB, два байта LE |
|
||||||
|
|
||||||
|
Между управляющими регистрами и окнами данных есть зарезервированные адреса.
|
||||||
|
Чтение блока через них не выполняется: каждый запрос должен начинаться внутри
|
||||||
|
существующего участка.
|
||||||
|
|
||||||
|
## STATUS
|
||||||
|
|
||||||
|
| Бит | Маска | Смысл |
|
||||||
|
|---:|---:|---|
|
||||||
|
| 0 | `0x0001` | `READY` |
|
||||||
|
| 1 | `0x0002` | `BUSY` |
|
||||||
|
| 2 | `0x0004` | `PAGE_VALID` |
|
||||||
|
| 3 | `0x0008` | `BAD_BLOCK` |
|
||||||
|
| 4 | `0x0010` | `WRITE_SUPPORTED`, порт способен управлять `WP#` |
|
||||||
|
| 5 | `0x0020` | `BUFFER_DIRTY`, буфер изменён после чтения/очистки |
|
||||||
|
| 15 | `0x8000` | `ERROR`, подробность в `E003` |
|
||||||
|
|
||||||
|
GUI читает окно данных только при `READY=1`, `PAGE_VALID=1`, `ERROR=0`.
|
||||||
|
|
||||||
|
## COMMAND
|
||||||
|
|
||||||
|
| Значение | Команда | Результат |
|
||||||
|
|---:|---|---|
|
||||||
|
| 1 | `RESET` | сброс NAND и ожидание Ready |
|
||||||
|
| 2 | `READ_ID` | обновляет `E00B/E00C` |
|
||||||
|
| 3 | `READ_PAGE` | загружает main/OOB выбранной страницы |
|
||||||
|
| 4 | `CLEAR_BUFFER` | заполняет main/OOB значением `0xFF` для подготовки записи |
|
||||||
|
| 5 | `PROGRAM_PAGE` | программирует выбранную страницу из буфера |
|
||||||
|
| 6 | `ERASE_BLOCK` | стирает блок, содержащий выбранную страницу |
|
||||||
|
|
||||||
|
Запись номера страницы и запуск — две отдельные операции. Сначала GUI пишет
|
||||||
|
`PAGE_LO/PAGE_HI`, затем записывает `READ_PAGE` в `COMMAND`. Нельзя отправлять
|
||||||
|
один блок, начинающийся с `COMMAND`: команда выполнилась бы до обновления номера
|
||||||
|
страницы.
|
||||||
|
|
||||||
|
## Последовательность полного дампа
|
||||||
|
|
||||||
|
1. Прочитать `E000..E00F`, проверить подпись, версию и геометрию.
|
||||||
|
2. Выполнить `READ_ID`; для микросхемы с фотографии проверить `2C F1`.
|
||||||
|
3. Рассчитать `page_count = PAGES_PER_BLOCK * BLOCKS`.
|
||||||
|
4. Для каждой физической страницы от 0 до `page_count - 1`:
|
||||||
|
- записать номер страницы в `E005/E006`;
|
||||||
|
- записать `3` в `E004`;
|
||||||
|
- опрашивать `E002`, пока не установлен `READY`;
|
||||||
|
- проверить `ERROR`, `BAD_BLOCK` и изменение `GENERATION`;
|
||||||
|
- прочитать `E100..E4FF`;
|
||||||
|
- прочитать `E500..E51F`;
|
||||||
|
- сразу дописать данные и метаданные в файлы.
|
||||||
|
5. Не пропускать bad-блоки: иначе файл потеряет соответствие физическим
|
||||||
|
страницам микросхемы.
|
||||||
|
|
||||||
|
GUI protocol v1 допускает payload до 512 байт. Ответ `READ_REGISTERS` содержит
|
||||||
|
четырёхбайтовый заголовок диапазона, поэтому за один запрос следует читать не
|
||||||
|
более 254 GAS-регистров. Страница main читается пятью запросами: четыре раза по
|
||||||
|
254 регистра и последний раз 8 регистров. OOB читается одним запросом на 32
|
||||||
|
регистра.
|
||||||
|
|
||||||
|
## Возобновление
|
||||||
|
|
||||||
|
GUI хранит номер последней полностью записанной страницы. После разрыва связи
|
||||||
|
он повторно проверяет подпись/ID/геометрию и продолжает со следующей страницы.
|
||||||
|
Страница считается готовой только после записи main, OOB и записи её статуса в
|
||||||
|
метаданные.
|
||||||
|
|
||||||
|
Рекомендуемые выходные файлы:
|
||||||
|
|
||||||
|
- `dump.bin` — 2048 байт main каждой физической страницы;
|
||||||
|
- `dump.oob` — 64 байта OOB каждой физической страницы;
|
||||||
|
- `dump.json` — ID, геометрия, bad-блоки, ошибки и последняя готовая страница.
|
||||||
|
|
||||||
|
Для `MT29F1G08ABADA` размеры завершённого дампа: `dump.bin` = 134217728 байт,
|
||||||
|
`dump.oob` = 4194304 байта.
|
||||||
|
|
||||||
|
## ECC
|
||||||
|
|
||||||
|
Текущая версия шаблона отдаёт raw main/OOB и не заявляет исправление ошибок.
|
||||||
|
Статус bad-блока определяется по первому байту OOB первых двух страниц блока.
|
||||||
|
Перед использованием дампа как рабочего образа нужно добавить ECC-политику для
|
||||||
|
точного чипа и формата OOB. Само окно GAS при этом менять не требуется: можно
|
||||||
|
добавить новые флаги состояния в свободные биты `STATUS`.
|
||||||
|
|
||||||
|
## PROGRAM/ERASE
|
||||||
|
|
||||||
|
Разрушительные команды доступны только если установлен флаг
|
||||||
|
`WRITE_SUPPORTED`. Ядро проверяет bad-block marker и отказывается изменять
|
||||||
|
заводской плохой блок. `WP#` снимается только внутри операции и включается
|
||||||
|
обратно после проверки status fail-бита, тайм-аута или другой ошибки.
|
||||||
|
|
||||||
|
Для программирования очищенной страницы:
|
||||||
|
|
||||||
|
1. Записать `PAGE_LO/PAGE_HI`.
|
||||||
|
2. Выполнить `CLEAR_BUFFER`.
|
||||||
|
3. Записать необходимые main/OOB-регистры. Незаписанные байты останутся
|
||||||
|
`0xFF`; OOB лучше не менять без определённой ECC-разметки.
|
||||||
|
4. Записать `0xA55A` в `CONFIRM`.
|
||||||
|
5. Следующей записью без промежуточных операций отправить `PROGRAM_PAGE`.
|
||||||
|
6. Проверить `READY`, отсутствие `ERROR` и изменение `GENERATION`.
|
||||||
|
|
||||||
|
Для стирания записать любую страницу целевого блока в `PAGE_LO/PAGE_HI`, затем
|
||||||
|
`0xA55A` в `CONFIRM` и сразу `ERASE_BLOCK`. Изменение страницы, буфера или
|
||||||
|
другого управляющего регистра сбрасывает подтверждение. После успешного
|
||||||
|
стирания `PAGE_VALID` очищается.
|
||||||
|
|
||||||
|
Повторное программирование не может превратить `0` обратно в `1`: для этого
|
||||||
|
требуется стирание всего блока. Перед первым тестом сохраните полный raw-дамп,
|
||||||
|
исключите загрузочные блоки и используйте заведомо расходный исправный блок.
|
||||||
|
Библиотека не реализует ECC, wear leveling, журналирование и восстановление
|
||||||
|
после пропадания питания.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user