Compare commits

82 Commits
v0.1.0 ... main

Author SHA1 Message Date
def3eb08f3 perf(ds18b20-ds2480): передавай байты в Data Mode 2026-09-15 22:45:07 +03:00
3020de7e0c feat(ds18b20-ds2480): добавь порт F407 и асинхронную подтяжку 2026-09-15 22:32:30 +03:00
df02070cc4 merge: сохрани историю NAND, уже включённую в c/parallel-nand 2026-09-15 22:15:19 +03:00
00f8dff838 Merge remote-tracking branch 'origin/feature/konor-runtime-settings' 2026-09-15 22:15:07 +03:00
7b2f74a3ff merge: объедини DS2480 и актуальный templates в main 2026-09-15 22:15:06 +03:00
96ce6f8759 feat(ds18b20-ds2480): добавь драйвер датчиков через UART-мост 2026-09-15 16:57:33 +03:00
874d2e1dfc Объединить разработки CAN и STM32 в master 2026-09-15 15:57:48 +03:00
698632ebf3 Сохранить текущие изменения NAND и LED перед объединением веток 2026-09-15 15:57:06 +03:00
67cca68fcc feat: sync current parallel NAND implementation from templates 2026-09-15 12:25:15 +03:00
c0c20f6dc2 feat: add STM32F103ZE FSMC NAND port 2026-09-12 00:24:40 +03:00
11f16f618b docs: restore compact pinout tables 2026-09-11 22:04:08 +03:00
3a1e8d469a docs: make pinout readable in narrow views 2026-09-11 22:02:34 +03:00
daaca0d94b feat: add STM32F103RC support for HY27UF084G2M 2026-09-11 19:42:15 +03:00
20dbc2c721 fix(set-protocol): сохрани совместимость порта с ARMCC 2026-09-07 08:42:02 +03:00
e95338d781 fix(set-protocol): убери libc из порта DevBoard F407 2026-09-07 08:41:26 +03:00
daa3466a41 feat(set-protocol): добавь порт F407 для DevBoard_V1 2026-09-07 08:32:02 +03:00
c9cf797132 Добавить HTML-справочник CAN v1 и v2 2026-09-06 03:09:53 +03:00
b0f3731554 Добавить HTML-справочник CAN v1 и v2 2026-09-06 03:03:59 +03:00
9ad0322c71 Добавить порт SETProtocol v2 для TMS320F2812 2026-09-06 02:14:17 +03:00
1511719bb9 Расширить общий протокол CAN_Bal_2812 2026-09-06 01:06:30 +03:00
0ce464499f Добавить общую модель конструктора ProtoCAN для Android 2026-09-06 00:11:25 +03:00
27ef6fcfdf Merge remote-tracking branch 'origin/codex/trend-display-scale' into HEAD 2026-09-05 11:43:08 +03:00
235d5d81a2 Share PM67 upload protocol through C core 2026-09-05 11:37:03 +03:00
59f847e900 Добавить множитель и IQ в Python-тренды 2026-09-05 03:45:05 +03:00
286e454464 Добавить выпуск прошивок из Keil и CCS 2026-09-05 03:16:16 +03:00
e691dfc337 Добавить публикацию каталога прошивок 2026-09-05 03:05:17 +03:00
f5f15f6a04 Сохранить совместимость ABI графиков 2026-09-05 02:44:08 +03:00
be066a57eb Дополнить общий контракт границ осей 2026-09-05 02:43:07 +03:00
b1f7b965f4 Расширить общие API графиков и GAS обмена 2026-09-05 02:37:11 +03:00
78d3f6690b Align Android parser statistics with C core 2026-09-04 21:10:02 +03:00
6a82b309cc Move PM35 protocol into shared C core 2026-09-04 21:06:40 +03:00
2316a5a26a Добавить Android API протокола ПМ35 2026-09-04 20:39:04 +03:00
de5dc18a43 Merge pull request 'Расширить графики и общие протоколы CAN и Periph28335' (#4) from feature/plot-can-periph-updates into master
Reviewed-on: #4
2026-09-04 19:18:54 +03:00
d44d57a7aa Убрать лишнюю строку в Legacy CAN API 2026-09-04 19:17:27 +03:00
b972b6f33c Добавить канал TMS в каталог прошивок 2026-09-04 19:15:48 +03:00
454baeed98 Добавить общий протокол Periph28335 2026-09-04 19:15:48 +03:00
01ffc0e496 Добавить общий Python API старого CAN terminal 2026-09-04 19:15:47 +03:00
3c4ac9963d Добавить общий API старого CAN terminal 2026-09-04 19:15:47 +03:00
d9eb7dd9ad feat(plot): lock axes and calculate marker levels in dB 2026-09-04 19:15:47 +03:00
f6787163dc feat(plot): configure one or two marker pairs per axis 2026-09-04 19:15:47 +03:00
5f8fc07f2a Merge pull request 'Добавить общие графики и драйвер parallel NAND' (#3) from feature/shared-plot-and-parallel-nand into master
Reviewed-on: #3
2026-09-04 12:39:12 +03:00
c8565d868d Убрать лишнюю строку в модели маркеров 2026-09-04 12:36:56 +03:00
1523640414 Добавить общий Python-парсер каталога прошивок 2026-09-04 12:31:42 +03:00
fa6f615e81 Добавить безопасный клиент каталога прошивок Android 2026-09-04 12:30:37 +03:00
392817432b Поддержать настройки трендов в Python 3.8 2026-09-04 12:23:07 +03:00
1f1006bf2a Добавить переносимый драйвер parallel NAND 2026-09-04 12:22:21 +03:00
20850b26df Добавить переносимый драйвер parallel NAND 2026-09-04 12:22:21 +03:00
e163d9ba55 Добавить общие графики, декодер KONOR и порт STM32 bxCAN 2026-09-04 12:22:17 +03:00
1296773346 feat(protocan): decode KONOR runtime settings 2026-09-01 21:32:56 +03:00
af82687e42 Добавить переносимый планировщик PULSE 2026-09-01 20:54:58 +03:00
e9ec078b38 Объединить SETProtocol v2 и общие кодеки (#1)
Reviewed-on: #1
2026-09-01 20:43:49 +03:00
43410c3f12 Merge origin/master into SETProtocol v2 integration branch 2026-09-01 20:41:07 +03:00
076c51c083 fix(python): support library lookup on Python 3.8 2026-09-01 20:31:18 +03:00
e9c5ec90b8 refactor(python): centralize ProtoCAN decoder 2026-09-01 12:00:53 +03:00
00a0f944c4 refactor(python): centralize SETProtocol v2 codec 2026-09-01 11:56:24 +03:00
9c0701ab9b refactor(android): centralize Kotlin protocol codecs 2026-09-01 11:54:35 +03:00
9b636a9f40 feat(setprotocol): define system payload schemas 2026-09-01 11:47:08 +03:00
19becd7b8c refactor: merge protocol cores as SETProtocol 2026-09-01 09:58:35 +03:00
5504104cc5 feat(protocan): вынеси GUI-кадрирование в общий ABI 2026-08-31 21:02:23 +03:00
e6bea52db0 fix(protocan): держи import library в build-каталоге 2026-08-31 20:58:52 +03:00
92fc2ade58 docs(protocan): перенеси канонический протокол из SETCAN 2026-08-31 20:55:04 +03:00
080b6900f5 feat(protocan): добавь JNI-разбор идентификатора 2026-08-31 20:50:40 +03:00
68040a2803 fix(protocan): исправь вызов MSVC из host builder 2026-08-31 20:48:17 +03:00
7fe76b3c3b build(protocan): добавь host-сборку без CMake 2026-08-31 20:48:01 +03:00
0f77a510e6 feat(protocan): открой разбор ID и статистику FFI 2026-08-31 20:46:52 +03:00
7910a07f2f feat(protocan): добавь общий ABI для GUI 2026-08-31 20:45:34 +03:00
2fd51a1d7e docs: move SETCAN documentation into templates 2026-08-31 19:30:54 +03:00
de180750aa feat(can-sensor): add STM32F1 bxCAN port 2026-08-31 18:50:52 +03:00
648457e587 protocan-boot: support single-slot targets 2026-08-31 01:06:16 +03:00
5f3a9879d5 Add C28x RS boot ports for F2812 and F28335 2026-08-30 15:30:08 +03:00
9ce32b56d4 Add reusable firmware info module 2026-08-30 07:01:59 +03:00
05863db2a2 docs: add Windows HTML build script 2026-08-30 06:12:50 +03:00
4b32cda46e docs: add HTML library catalog 2026-08-30 03:44:36 +03:00
217073982f feat(rs485-boot): add STM32F103 and G474 ports 2026-08-30 01:37:12 +03:00
94c991380b docs(porting): опиши структуру проектов STM32 2026-08-30 01:13:43 +03:00
5a8724da6d docs(protocan-boot): уточни границы блоков слота 2026-08-30 01:13:42 +03:00
d507b84a76 Обновить Python-декодер ProtoCAN Boot 2026-08-29 22:18:53 +03:00
d3bb634fb1 feat(protocan-boot): добавь прошивку приборов по CAN 2026-08-29 17:11:53 +03:00
a1d2d05f42 тест 2026-08-26 21:01:51 +03:00
1345766efb Обновить can-sensor для SETCAN SETTINGS 2026-08-26 20:47:59 +03:00
085eb3c8bd feat: add unified SET protocol v2 2026-08-24 19:19:32 +03:00
033c7ab9e8 feat(ds18b20): порты 1-Wire для STM32G4 и STM32F4
Добавлены реализации низкоуровневой шины для STM32G431/G474 и
STM32F407VET6 с шаблонами onewire_config.h под каждый камень.
Логика повторяет порт STM32F1: открытый сток, задержки по DWT,
запрет прерываний на один тайм-слот.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:14:23 +03:00
282 changed files with 54271 additions and 522 deletions

2
.gitignore vendored
View File

@@ -2,6 +2,7 @@
build/
cmake-build-*/
*.o
*.obj
*.d
*.a
*.elf
@@ -22,3 +23,4 @@ __pycache__/
*.uvguix.*
Thumbs.db
Desktop.ini
.codex-build/

122
NEW_PROJECT.md Normal file
View File

@@ -0,0 +1,122 @@
# Новый проект на переносимых библиотеках
Этот репозиторий содержит два разных типа кода:
1. `c/<library>/include` и `c/<library>/src` — переносимое ядро;
2. `c/<library>/ports/<family>` — небольшой аппаратный порт конкретного
семейства микроконтроллеров.
Привязка выводов, частоты шин и выбранной периферии принадлежит плате и всегда
остаётся в проекте в `app_config.h` либо в узком конфигурационном заголовке
библиотеки, например `onewire_config.h`.
## Рекомендуемая структура прошивки
```text
my-device/
app/ логика изделия
include/
src/
board/ одна конкретная плата
app_config.h выводы, частоты, адреса, возможности платы
port/ связь библиотек с SDK выбранного МК
display_port.c
keypad_port.c
eeprom_port.c
can_port.c
vendor/ CMSIS, startup, linker script или Cube-generated код
lib/templates/ git submodule этого репозитория
```
Не следует помещать `stm32xxxx.h`, `HAL_*`, регистры, IRQ handlers и номера
выводов в `app/` или в ядро библиотеки. Тогда при переходе на другой МК
заменяются только `board/`, `port/`, startup и linker script.
## Выбор готового порта DS18B20
| Целевой МК | Файл порта | Шаблон конфигурации |
|---|---|---|
| STM32F103 | `c/ds18b20/ports/stm32f1/onewire_stm32f1.c` | `onewire_config.f103.template.h` |
| STM32G431 | `c/ds18b20/ports/stm32g4/onewire_stm32g4.c` | `onewire_config.g431.template.h` |
| STM32G474 | `c/ds18b20/ports/stm32g4/onewire_stm32g4.c` | `onewire_config.g474.template.h` |
В сборку добавляются ровно три файла:
```text
c/ds18b20/src/onewire.c
c/ds18b20/src/ds18b20.c
c/ds18b20/ports/<family>/onewire_<family>.c
```
Пути включения:
```text
c/ds18b20/include
board
```
Нужный шаблон копируется в `board/onewire_config.h`; после этого в нём
меняются только порт, номер вывода и бит тактирования GPIO.
G431 и G474 намеренно используют один исходник порта: GPIO, RCC и DWT для
этой задачи совместимы. Раздельными остаются конфигурация платы, startup,
linker script и выбранный CMSIS device define.
## Выбор готового порта parallel NAND
| Целевой МК | Реализация шины | Каталог порта |
|---|---|---|
| TMS320F2812 | XINTF Zone 6 | `c/parallel-nand/ports/tms320f2812` |
| STM32F103RCT6 | GPIO bit-bang, поскольку FSMC недоступен в LQFP64 | `c/parallel-nand/ports/stm32f103rc` |
| STM32F103ZET6 | FSMC NAND Bank 3 | `c/parallel-nand/ports/stm32f103ze` |
| STM32F407VET6 | FSMC NAND Bank 2 | `c/parallel-nand/ports/stm32f407ve` |
| STM32G474CEU6 | GPIO bit-bang для UFQFPN48 | `c/parallel-nand/ports/stm32g474ce` |
Ядро поддерживает профили Micron `MT29F1G08ABADA` и Hynix
`HY27UF084G2M`. В сборку добавляются `src/parallel_nand.c`, при необходимости
`src/parallel_nand_gas.c` или `src/parallel_nand_pcan_gas.c`, и ровно один
аппаратный порт. Конкретная распиновка, требуемые модули HAL и пример
инициализации приведены в README выбранного порта.
Для `PROGRAM`/`ERASE` линия `WP#` должна управляться портом. Готовые порты
STM32F103RC, STM32F103ZE и STM32G474CE объявляют поддержку записи. Базовые
схемы TMS320F2812 и STM32F407VE фиксируют `WP#` аппаратно и безопасно
возвращают `PNAND_ERROR_WRITE_PROTECTED`, пока плата и порт не доработаны.
Рабочий пример полноценного G474-порта находится в соседнем проекте
`candleLight_fw`: `src/device/device_g4.c` отвечает за clock/GPIO,
`src/can/m_can.c` — за FDCAN, а `include/config.h` — за привязку платы
STM32G474VET6. Эти файлы остаются в прошивке, поскольку зависят от STM32 HAL,
USB gs_usb и конкретной разводки адаптера; переносимая логика протоколов
остаётся в `templates`.
## Какие части KONOR уже общие
| Возможность | Переносимое ядро | Что остаётся в `port/` проекта |
|---|---|---|
| DS18B20/1-Wire | `c/ds18b20` | готовый порт выбранного семейства и вывод |
| дисплей ST7789 | `c/st7789` | SPI, DC, CS, RESET, подсветка и задержка |
| кнопки | `c/keypad` | настройка GPIO и чтение уровней |
| меню | `c/menu` | только привязка painter к дисплею |
| индикация состояний | `c/led-indicator` | готовый STM32 HAL-порт или callbacks GPIO и выбранного таймера |
| EEPROM FT24C256 | `c/eeprom-ft24c256` | I2C write/write-read и задержка |
| CAN SETTINGS | `c/can-sensor` | bxCAN для F103 либо FDCAN для G431/G474 |
Порт дисплея, EEPROM и CAN нельзя честно сделать только по названию МК:
нужно знать экземпляр периферии, альтернативную функцию и выводы конкретной
платы. Поэтому библиотеки принимают таблицы обратных вызовов, а короткие
адаптеры лежат в проекте рядом с `app_config.h`.
## Порядок создания проекта
1. Создать проект в CubeMX/Keil либо добавить CMSIS, startup и linker script.
2. Подключить `templates` сабмодулем.
3. Добавить только ядра нужных библиотек.
4. Выбрать готовый 1-Wire порт по таблице выше.
5. Создать `board/app_config.h` с выводами и частотами.
6. Реализовать короткие адаптеры SPI/GPIO/I2C/CAN в `port/`.
7. Сначала прогнать host-тесты библиотек, затем проверить аппаратные порты
логическим анализатором или осциллографом.
Так новый проект не является копией старого: он использует общие ядра одной
версии, а аппаратные отличия видны в одном небольшом каталоге `port/`.

View File

@@ -13,8 +13,15 @@
templates/
c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты
python/ модули на чистом Python 3.9+, только stdlib
tools/ общие инструменты сборки и выпуска прошивок
```
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
и STM32G474 описаны в [`NEW_PROJECT.md`](NEW_PROJECT.md).
Нормативная документация SETCAN/ProtoCAN, реестр общего адресного пространства
и исходный Excel собраны в [`doc/setcan`](doc/setcan/README.md).
## Что лежит
### C
@@ -24,21 +31,44 @@ templates/
| [`c/st7789`](c/st7789) | TFT ST7789V по SPI, RGB565, текст без кадрового буфера | `stdint.h` | SPI-запись, линия DC, задержка |
| [`c/keypad`](c/keypad) | шесть кнопок: антидребезг, автоповтор, удержание, очередь событий | `stdint.h` | чтение уровня кнопки, время в мс |
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `stdint.h` | заливка прямоугольника, вывод строки |
| [`c/led-indicator`](c/led-indicator) | неблокирующие режимы LED: работа, активность, предупреждение и ошибка | C99 | логическая запись канала, время выбранного таймера — **STM32F1/F4/G4 HAL-порт в комплекте** |
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
| [`c/can-sensor`](c/can-sensor) | передача 64-битных ROM датчиков парой CAN-кадров | `stdint.h` | отправка и приём CAN-кадра |
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порт STM32F1 в комплекте** |
| [`c/protocan-transport`](c/protocan-transport) | транспорт ProtoCAN: кадр, канал, CRC, общее адресное пространство, каталог GUI | `stdint.h` | запись в поток и запрос свободного места — **порт STM32F4 в комплекте** |
| [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 |
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
| [`c/ds18b20-ds2480`](c/ds18b20-ds2480) | DS18B20 через DS2480B: поиск ROM, температура, EEPROM, паразитное питание | C99 | UART 9600 8N1, сброс моста, задержка |
| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android, STM32F4 и TMS320F2812-порты в комплекте** |
| [`c/set-protocol/ports/stm32f407-devboard-v1`](c/set-protocol/ports/stm32f407-devboard-v1) | доступ SETGUI к Modbus-регистрам F407 через CAN485 DevBoard_V1 | `pcan_modbus_server`, STM32 HAL CAN | bxCAN FIFO0 и callbacks карты регистров |
| [`c/set-protocol/ports/stm32-bxcan`](c/set-protocol/ports/stm32-bxcan) | порт прикладного ProtoCAN для STM32, бывший SETCAN; сохранён API `PROTOCAN_*` | STM32 HAL CAN/RTC/TIM + общее ядро `pcan_id` | classic bxCAN; настройки платы предоставляет прошивка |
| [`c/set-protocol/ports/tms320f2812`](c/set-protocol/ports/tms320f2812) | SETProtocol v2 firmware service по segmented classic CAN | `set_protocol`, `set_can`, `set_firmware` | CAN TX, Flash erase/write/read, optional signature policy и reboot |
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | 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/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/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/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.
Интерактивная документация общего ядра: [`doc/setprotocol.html`](doc/setprotocol.html).
В ней отдельно описаны граница ядра, ABI, память, три wire format и состояние
портов Windows, Android, Linux и MCU.
## Как подключить к проекту
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
@@ -69,17 +99,18 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
| Проект | Что берёт |
|---|---|
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20 |
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20, firmware-info |
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
| `CAN_to_RS485` | protocan-transport, python/protocan |
| `CAN_to_RS485` | set-protocol legacy transport, python/protocan |
| `candleLight_fw` | эталон разделения общей логики и G431/G474 FDCAN-порта; общий ProtoCAN Boot переносится по контракту `protocan-boot` |
| `SETGUI` | python/protocan |
| `SETGUI`, Android GUI и новые устройства SET | set-protocol |
| `k1921vk028` | rtc-service |
## Что сюда не попало и почему
| Код | Почему не переносится | Что можно вытащить |
|---|---|---|
| `SETCAN/Src/protocan.c` | 59 вызовов `HAL_*`, привязка к `CAN_HandleTypeDef`, `RTC_HandleTypeDef`, `NVIC_SystemReset()` | разбиение длинных посылок на кадры и проверка даты — чистая логика, нужны два интерфейса: «отправить кадр» и «часы». Раскладка идентификатора уже вынесена в `pcan_id` |
| `1921vk028/drivers/user/src/one_wire_user.c` | аппаратный 1-Wire `OWI_*` из `plib028`, мигание светодиодом прямо внутри опроса датчика | логика DS18B20 уже есть в `c/ds18b20` — переносится порт, а не алгоритм |
| `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |

80
RULES.md Normal file
View 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).

View 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()

View File

@@ -1,27 +1,28 @@
# can-sensor
Передача 64-битных идентификаторов датчиков (ROM 1-Wire) по шине CAN.
Однокадровый транспорт SETCAN SETTINGS для привязки 64-битного ROM 1-Wire к
локации «номер сборки + позиция» по шине CAN.
Ядро на C99: не включает заголовки периферии, не обращается к регистрам,
не пользуется прерываниями. Обмен идёт через таблицу `CanSensor_Io`,
которую заполняет порт платы.
Сообщение состоит из двух классических CAN-кадров — 64-битный идентификатор
и преамбула вместе не помещаются в восемь байтов поля данных:
Сообщение помещается в один Extended CAN-кадр: локация находится в Body
29-битного ID, а ROM — в восьми байтах payload:
```
кадр 1 — преамбула, DLC = 2 | команда | позиция |
кадр 2 — идентификатор, DLC = 8| ROM 8 байт |
запрос 0x17FEZZYY: DLC=0 — GET, DLC=8 — WRITE/REPLACE/CLEAR
ответ 0x1FFEZZYY: DLC=8 — текущий ROM, DLC=1 — код ошибки
```
## Состав
| Файл | Что делает | Зависимости |
|---|---|---|
| `can_sensor.h`, `can_sensor.c` | сборка и разбор пары кадров, повторы передачи, счётчики обмена | `stdint.h` |
| `can_sensor.h`, `can_sensor.c` | сборка и разбор SETTINGS, планировщик PULSE, повторы передачи, счётчики обмена | `stdint.h` |
| `ports/stm32f1/` | опросный порт CAN1: GPIO, BTR, фильтр, mailbox/FIFO, тайм-аут ACK | CMSIS `stm32f10x.h` |
Приём собирает сообщение сам: кадр ROM без преамбулы и кадр с неверной
длиной отбрасываются и учитываются в `dropped_frames`.
`ZZ` — номер сборки, `YY` — позиция. Нулевой ROM с DLC=8 очищает локацию.
## Что нужно от платформы
@@ -38,15 +39,45 @@ uint8_t receive(void *ctx, CanSensor_Frame *frame); /* 1 — кадр п
CanSensor link;
CanSensor_Io io = { .send = bxcan_send, .receive = bxcan_receive, .context = &board };
CanSensor_Config config;
CanSensor_ConfigDefault(&config); /* tx_id 0x200, кадр ROM 0x201, 3 попытки */
CanSensor_ConfigDefault(&config); /* TX 0x1FFE0000, RX 0x17FE0000 */
config.pulse_period_ms = 1000U; /* 0 — PULSE выключен */
CanSensor_Init(&link, &io, &config);
CanSensor_SendId(&link, position, rom);
CanSensor_SendId(&link, position, assembly_serial, rom);
CanSensor_Task(&link, now_ms);
CanSensor_Message message;
if (CanSensor_Poll(&link, &message)) { /* принят идентификатор */ }
```
Период можно менять во время работы через `CanSensor_SetPulsePeriod()`. Значение
`0` именно выключает heartbeat: кадры PULSE больше не ставятся в очередь. При
повторном включении первый кадр отправляется через полный заданный период.
## Порт STM32F1
Порт не использует STM32 HAL и не занимает прерывания. Скопируйте
`ports/stm32f1/can_sensor_stm32f1_config.f103.template.h` в приложение под
именем `can_sensor_stm32f1_config.h`, добавьте `ports/stm32f1` в include path и
соберите `can_sensor_stm32f1.c` вместе с ядром:
```c
CanSensorStm32F1_Port bxcan = {0};
CanSensor_Io io;
CanSensorStm32F1_Start(&bxcan, 500000U, 0x17F00000UL, 0x1FF00000UL, 0U);
io = CanSensorStm32F1_MakeIo(&bxcan);
CanSensor_Init(&link, &io, &config);
/* Из главного цикла: */
CanSensorStm32F1_Task(&bxcan, now_ms, 20U);
```
Фильтр порта принимает только Extended data-кадры. Маска задаётся приложением:
можно пропустить только SETTINGS либо оставить открытым `MsgType` для соседних
команд того же узла. `CanSensorStm32F1_Receive()` доступна отдельно, если перед
`CanSensor_HandleFrame()` приложению нужно разобрать такие команды самому.
## Проверено в проектах
`KONOR_ds18b20` — bxCAN на STM32F103C8T6. Естественная пара — [`ds18b20`](../ds18b20):

View File

@@ -1,72 +1,35 @@
/**
* @file can_sensor.c
* @brief Сборка, передача и разбор сообщений «преамбула + идентификатор».
*
* Реализация не хранит очередей и не пользуется временем: состояние приёма
* ограничено признаком принятой преамбулы, поэтому библиотека одинаково
* работает и в главном цикле, и в обработчике прерывания порта.
* @brief Однокадровый транспорт SETCAN SETTINGS для локаций DS18B20.
*/
#include "can_sensor.h"
/**
* @brief Возвращает идентификатор кадра данных для заданных настроек.
*
* @param config Настройки узла.
* @return Идентификатор кадра ROM при передаче.
*/
static uint32_t can_sensor_tx_data_id(const CanSensor_Config *config)
/** Возвращает 1, если все восемь байтов ROM равны нулю. */
static uint8_t can_sensor_rom_is_zero(const uint8_t *rom)
{
if (config->tx_data_id != 0U) {
return config->tx_data_id;
uint8_t index;
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
if (rom[index] != 0U) {
return 0U;
}
}
return config->tx_id + CAN_SENSOR_DATA_ID_OFFSET;
return 1U;
}
/**
* @brief Возвращает ожидаемый идентификатор преамбулы при приёме.
*
* @param config Настройки узла.
* @return Идентификатор кадра преамбулы.
*/
static uint32_t can_sensor_rx_id(const CanSensor_Config *config)
{
if (config->rx_id != 0U) {
return config->rx_id;
}
return config->tx_id;
}
/**
* @brief Возвращает ожидаемый идентификатор кадра ROM при приёме.
*
* @param config Настройки узла.
* @return Идентификатор кадра идентификатора датчика.
*/
static uint32_t can_sensor_rx_data_id(const CanSensor_Config *config)
{
if (config->rx_data_id != 0U) {
return config->rx_data_id;
}
return can_sensor_rx_id(config) + CAN_SENSOR_DATA_ID_OFFSET;
}
/**
* @brief Передаёт один кадр с повторами при отказе контроллера.
*
* @param link Состояние узла.
* @param frame Передаваемый кадр.
* @return 1, если кадр принят контроллером, иначе 0.
*/
/** Передаёт уже собранный кадр с ограниченным числом повторов. */
static uint8_t can_sensor_send_frame(CanSensor *link, const CanSensor_Frame *frame)
{
uint8_t attempt;
for (attempt = 0U; attempt < link->config.retries; attempt++) {
if (link->io.send(link->io.context, frame) != 0U) {
link->sent_messages++;
return 1U;
}
}
link->send_errors++;
return 0U;
}
@@ -76,22 +39,18 @@ void CanSensor_ConfigDefault(CanSensor_Config *config)
return;
}
config->tx_id = CAN_SENSOR_DEFAULT_TX_ID;
config->tx_data_id = CAN_SENSOR_DEFAULT_TX_ID + CAN_SENSOR_DATA_ID_OFFSET;
config->rx_id = CAN_SENSOR_DEFAULT_TX_ID;
config->rx_data_id = CAN_SENSOR_DEFAULT_TX_ID + CAN_SENSOR_DATA_ID_OFFSET;
config->extended = 0U;
config->rx_id = CAN_SENSOR_DEFAULT_RX_ID;
config->pulse_period_ms = 0U;
config->extended = 1U;
config->retries = CAN_SENSOR_DEFAULT_RETRIES;
}
uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
const CanSensor_Config *config)
{
uint8_t index;
if ((link == 0) || (io == 0) || (io->send == 0)) {
return 0U;
}
link->io = *io;
if (config != 0) {
link->config = *config;
@@ -101,131 +60,133 @@ uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
if (link->config.tx_id == 0U) {
link->config.tx_id = CAN_SENSOR_DEFAULT_TX_ID;
}
if (link->config.rx_id == 0U) {
link->config.rx_id = CAN_SENSOR_DEFAULT_RX_ID;
}
link->config.tx_id &= CAN_SENSOR_HEADER_MASK;
link->config.rx_id &= CAN_SENSOR_HEADER_MASK;
link->config.extended = 1U;
if (link->config.retries == 0U) {
link->config.retries = CAN_SENSOR_DEFAULT_RETRIES;
}
link->rx.command = 0U;
link->rx.position = 0U;
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
link->rx.id[index] = 0U;
}
link->rx_preamble = 0U;
link->sent_messages = 0U;
link->send_errors = 0U;
link->received_messages = 0U;
link->dropped_frames = 0U;
link->pulse_last_ms = 0U;
link->pulse_counter = 0U;
link->pulse_armed = 0U;
return 1U;
}
uint8_t CanSensor_BuildPreamble(const CanSensor_Config *config, CanSensor_Frame *frame,
uint8_t command, uint16_t position)
uint8_t CanSensor_BuildFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
uint8_t response, uint8_t position,
uint8_t assembly_serial, const uint8_t *data,
uint8_t length)
{
uint8_t index;
uint32_t base;
if ((config == 0) || (frame == 0)) {
if ((config == 0) || (frame == 0) || (length > CAN_SENSOR_MAX_DATA)
|| ((length != 0U) && (data == 0))) {
return 0U;
}
frame->id = config->tx_id;
frame->extended = config->extended;
frame->length = CAN_SENSOR_PREAMBLE_SIZE;
frame->data[0] = command;
/* Позиция занимает два байта в порядке от старшего к младшему. */
frame->data[1] = (uint8_t)((position >> 8U) & 0xFFU);
frame->data[2] = (uint8_t)(position & 0xFFU);
for (index = CAN_SENSOR_PREAMBLE_SIZE; index < CAN_SENSOR_MAX_DATA; index++) {
frame->data[index] = 0U;
base = (response != 0U) ? config->tx_id : config->rx_id;
frame->id = (base & CAN_SENSOR_HEADER_MASK)
| ((uint32_t)assembly_serial << 8U) | (uint32_t)position;
frame->extended = 1U;
frame->length = length;
for (index = 0U; index < CAN_SENSOR_MAX_DATA; index++) {
frame->data[index] = (index < length) ? data[index] : 0U;
}
return 1U;
}
uint8_t CanSensor_BuildIdFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
const uint8_t *id)
{
uint8_t index;
if ((config == 0) || (frame == 0)) {
return 0U;
}
frame->id = can_sensor_tx_data_id(config);
frame->extended = config->extended;
frame->length = CAN_SENSOR_ID_SIZE;
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
frame->data[index] = (id != 0) ? id[index] : 0U;
}
return 1U;
}
uint8_t CanSensor_Send(CanSensor *link, uint8_t command, uint16_t position,
const uint8_t *id)
uint8_t CanSensor_SendId(CanSensor *link, uint8_t position,
uint8_t assembly_serial, const uint8_t *id)
{
CanSensor_Frame frame;
if ((link == 0) || (link->io.send == 0)) {
if ((link == 0) || (id == 0) || (link->io.send == 0)) {
return 0U;
}
(void)CanSensor_BuildPreamble(&link->config, &frame, command, position);
if (can_sensor_send_frame(link, &frame) == 0U) {
if (CanSensor_BuildFrame(&link->config, &frame, 1U, position,
assembly_serial, id, CAN_SENSOR_ID_SIZE) == 0U) {
link->send_errors++;
return 0U;
}
(void)CanSensor_BuildIdFrame(&link->config, &frame, id);
if (can_sensor_send_frame(link, &frame) == 0U) {
/* Одиночная преамбула приёмником отбрасывается, позиция не изменится. */
link->send_errors++;
return 0U;
}
link->sent_messages++;
return 1U;
return can_sensor_send_frame(link, &frame);
}
uint8_t CanSensor_SendId(CanSensor *link, uint16_t position, const uint8_t *id)
uint8_t CanSensor_SendError(CanSensor *link, uint8_t position,
uint8_t assembly_serial, uint8_t result)
{
if (id == 0) {
CanSensor_Frame frame;
if ((link == 0) || (link->io.send == 0) || (result == CAN_SENSOR_RESULT_OK)) {
return 0U;
}
return CanSensor_Send(link, CAN_SENSOR_CMD_WRITE_POSITION, position, id);
if (CanSensor_BuildFrame(&link->config, &frame, 1U, position,
assembly_serial, &result, 1U) == 0U) {
link->send_errors++;
return 0U;
}
return can_sensor_send_frame(link, &frame);
}
uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame,
CanSensor_Message *out)
{
CanSensor_Message message;
uint32_t header;
uint8_t index;
if ((link == 0) || (frame == 0)) {
return 0U;
}
if (frame->extended == 0U) {
return 0U;
}
header = frame->id & CAN_SENSOR_HEADER_MASK;
if (header == (link->config.rx_id & CAN_SENSOR_HEADER_MASK)) {
message.request = 1U;
} else if (header == (link->config.tx_id & CAN_SENSOR_HEADER_MASK)) {
message.request = 0U;
} else {
return 0U;
}
if ((frame->id == can_sensor_rx_id(&link->config))
&& (frame->extended == link->config.extended)) {
if (frame->length < CAN_SENSOR_PREAMBLE_SIZE) {
link->dropped_frames++;
return 0U;
message.assembly_serial = (uint8_t)((frame->id >> 8U) & 0xFFU);
message.position = (uint8_t)(frame->id & 0xFFU);
message.result = CAN_SENSOR_RESULT_OK;
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
message.id[index] = (index < frame->length) ? frame->data[index] : 0U;
}
if (message.request != 0U) {
if (frame->length == 0U) {
message.operation = CAN_SENSOR_OPERATION_GET;
} else if (frame->length == CAN_SENSOR_ID_SIZE) {
message.operation = (can_sensor_rom_is_zero(message.id) != 0U)
? CAN_SENSOR_OPERATION_CLEAR
: CAN_SENSOR_OPERATION_WRITE;
} else {
message.operation = CAN_SENSOR_OPERATION_INVALID;
message.result = CAN_SENSOR_RESULT_INVALID_DLC;
}
link->rx.command = frame->data[0];
link->rx.position = (uint16_t)(((uint16_t)frame->data[1] << 8U)
| frame->data[2]);
link->rx_preamble = 1U;
return 0U;
}
if ((frame->id != can_sensor_rx_data_id(&link->config))
|| (frame->extended != link->config.extended)) {
return 0U;
}
if ((link->rx_preamble == 0U) || (frame->length < CAN_SENSOR_ID_SIZE)) {
/* Идентификатор без преамбулы не говорит, в какую позицию его писать. */
link->rx_preamble = 0U;
} else if (frame->length == CAN_SENSOR_ID_SIZE) {
message.operation = CAN_SENSOR_OPERATION_RESPONSE;
} else if (frame->length == 1U) {
message.operation = CAN_SENSOR_OPERATION_ERROR;
message.result = frame->data[0];
} else {
link->dropped_frames++;
return 0U;
}
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
link->rx.id[index] = frame->data[index];
}
link->rx_preamble = 0U;
link->received_messages++;
if (out != 0) {
*out = link->rx;
*out = message;
}
return 1U;
}
@@ -244,3 +205,51 @@ uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out)
}
return 0U;
}
void CanSensor_SetPulsePeriod(CanSensor *link, uint32_t period_ms)
{
if (link == 0) {
return;
}
if (link->config.pulse_period_ms != period_ms) {
link->config.pulse_period_ms = period_ms;
/* После включения или смены периода отсчитываем полный новый период. */
link->pulse_armed = 0U;
}
}
uint32_t CanSensor_PulsePeriod(const CanSensor *link)
{
return (link != 0) ? link->config.pulse_period_ms : 0U;
}
void CanSensor_Task(CanSensor *link, uint32_t now_ms)
{
CanSensor_Frame frame;
const uint32_t period_ms = (link != 0) ? link->config.pulse_period_ms : 0U;
if ((link == 0) || (link->io.send == 0) || (period_ms == 0U)) {
return;
}
if (link->pulse_armed == 0U) {
link->pulse_last_ms = now_ms;
link->pulse_armed = 1U;
return;
}
if ((now_ms - link->pulse_last_ms) < period_ms) {
return;
}
frame.id = (link->config.tx_id & CAN_SENSOR_ADDRESS_MASK)
| ((uint32_t)CAN_SENSOR_MSGTYPE_PULSE << 16U);
frame.extended = 1U;
frame.length = 1U;
link->pulse_counter++;
frame.data[0] = link->pulse_counter;
if (can_sensor_send_frame(link, &frame) != 0U) {
link->pulse_last_ms = now_ms;
} else {
/* Счётчик меняется только для реально поставленных в очередь кадров. */
link->pulse_counter--;
}
}

View File

@@ -1,30 +1,19 @@
/**
* @file can_sensor.h
* @brief Портируемая передача идентификаторов датчиков по шине CAN.
* @brief Портируемый транспорт SETTINGS для привязки DS18B20 к локации.
*
* Библиотека не привязана к микроконтроллеру: она не включает заголовки
* периферии, не обращается к регистрам и не пользуется прерываниями. Обмен
* идёт через таблицу обратных вызовов CanSensor_Io, которую заполняет порт
* платы (для этой сборки — @c src/can.c поверх bxCAN STM32F103C8T6).
*
* Сообщение состоит из двух кадров классического CAN, потому что 64-битный
* идентификатор датчика и преамбула вместе не помещаются в восемь байтов
* поля данных:
* Один Extended CAN-кадр содержит полную операцию. Поля локации находятся в
* Body идентификатора SETCAN, а восьмибайтовый ROM — в payload:
*
* @code
* кадр 1 — преамбула, DLC = 2 +---------+----------+
* | команда | позиция |
* +---------+----------+
* кадр 2 — идентификатор, DLC=8 +----------------------------+
* | ROM датчика, байты 0..7 |
* +----------------------------+
* Priority=1, DeviceType=0x7, DeviceID=0xF, MsgType=0xE (SETTINGS)
* Body[15:8] = номер сборки, Body[7:0] = позиция
* @endcode
*
* Преамбула несёт команду записи датчика в позицию: первый байт — код
* команды (@ref CAN_SENSOR_CMD_WRITE_POSITION), второй — номер позиции в
* таблице узла-приёмника. Кадр идентификатора передаётся сразу за преамбулой
* и без неё считается недействительным, поэтому приёмник не запишет ROM в
* позицию, которая ему не была назначена.
* Запрос ПМ использует Route=0 (0x17FEZZYY), ответ прибора — Route=1
* (0x1FFEZZYY). DLC=0 означает GET, DLC=8 с ненулевым ROM — SET/REPLACE,
* DLC=8 с нулевым ROM — CLEAR. Успешный ответ возвращает текущий ROM с DLC=8,
* ошибка — однобайтовый код с DLC=1.
*/
#ifndef CAN_SENSOR_H
@@ -32,186 +21,126 @@
#include <stdint.h>
/** Длина идентификатора датчика (ROM 1-Wire) в байтах. */
#define CAN_SENSOR_ID_SIZE 8U
/** Длина ROM 1-Wire и максимального payload classic CAN. */
#define CAN_SENSOR_ID_SIZE 8U
#define CAN_SENSOR_MAX_DATA 8U
/** Длина преамбулы: код команды (1 байт) и номер позиции (2 байта, u16 BE). */
#define CAN_SENSOR_PREAMBLE_SIZE 3U
/** Назначенный проекту тип сообщения SETCAN SETTINGS. */
#define CAN_SENSOR_MSGTYPE_SETTINGS 0xEU
#define CAN_SENSOR_MSGTYPE_PULSE 0xFU
/** Предельная длина поля данных классического кадра CAN. */
#define CAN_SENSOR_MAX_DATA 8U
/** Маска полей SETCAN от Priority до MsgType; Body под маской не находится. */
#define CAN_SENSOR_HEADER_MASK 0x1FFF0000UL
#define CAN_SENSOR_ADDRESS_MASK 0x1FF00000UL
#define CAN_SENSOR_BODY_MASK 0x0000FFFFUL
/** Код команды «записать идентификатор датчика в позицию». */
#define CAN_SENSOR_CMD_WRITE_POSITION 0xA1U
/** Базовые ID SETTINGS для выбранных DeviceType=0x7 и DeviceID=0xF. */
#define CAN_SENSOR_DEFAULT_TX_ID 0x1FFE0000UL
#define CAN_SENSOR_DEFAULT_RX_ID 0x17FE0000UL
/** Код команды «очистить позицию»; кадр идентификатора передаётся нулевым. */
#define CAN_SENSOR_CMD_CLEAR_POSITION 0xA2U
/** Число попыток помещения кадра в почтовый ящик. */
#define CAN_SENSOR_DEFAULT_RETRIES 3U
/** Смещение идентификатора кадра данных относительно кадра преамбулы. */
#define CAN_SENSOR_DATA_ID_OFFSET 1U
/** Операции, полученные из направления, DLC и payload кадра. */
#define CAN_SENSOR_OPERATION_GET 0U
#define CAN_SENSOR_OPERATION_WRITE 1U
#define CAN_SENSOR_OPERATION_CLEAR 2U
#define CAN_SENSOR_OPERATION_RESPONSE 3U
#define CAN_SENSOR_OPERATION_ERROR 4U
#define CAN_SENSOR_OPERATION_INVALID 5U
/** Число попыток передачи одного кадра по умолчанию. */
#define CAN_SENSOR_DEFAULT_RETRIES 3U
/** Коды результата SETTINGS, передаваемые ответом с DLC=1. */
#define CAN_SENSOR_RESULT_OK 0x00U
#define CAN_SENSOR_RESULT_INVALID_DLC 0x01U
#define CAN_SENSOR_RESULT_INVALID_ROM_CRC 0x02U
#define CAN_SENSOR_RESULT_NOT_FOUND 0x03U
#define CAN_SENSOR_RESULT_ALREADY_ASSIGNED 0x04U
#define CAN_SENSOR_RESULT_EEPROM_ERROR 0x05U
#define CAN_SENSOR_RESULT_INVALID_LOCATION 0x06U
#define CAN_SENSOR_RESULT_BUSY 0x07U
/** Идентификатор кадра преамбулы по умолчанию (стандартный, 11 бит). */
#define CAN_SENSOR_DEFAULT_TX_ID 0x200U
/**
* @brief Кадр шины CAN в форме, не зависящей от контроллера.
*/
/** Кадр CAN в форме, не зависящей от периферийного контроллера. */
typedef struct {
uint32_t id; /**< Идентификатор кадра, 11 или 29 бит. */
uint8_t extended; /**< 1 — расширенный идентификатор. */
uint8_t length; /**< Число значащих байтов поля данных. */
uint8_t data[CAN_SENSOR_MAX_DATA]; /**< Поле данных кадра. */
uint32_t id; /**< Расширенный идентификатор, 29 бит. */
uint8_t extended; /**< Для SETTINGS всегда 1. */
uint8_t length; /**< DLC, 0..8. */
uint8_t data[CAN_SENSOR_MAX_DATA]; /**< Payload. */
} CanSensor_Frame;
/**
* @brief Принятое или собранное сообщение: преамбула и идентификатор датчика.
*/
/** Разобранная команда или ответ SETTINGS. */
typedef struct {
uint8_t command; /**< Код команды из преамбулы. */
uint16_t position; /**< Позиция записи из преамбулы. */
uint8_t id[CAN_SENSOR_ID_SIZE]; /**< Идентификатор датчика. */
uint8_t operation; /**< CAN_SENSOR_OPERATION_*. */
uint8_t request; /**< 1 — запрос Route=0, 0 — ответ Route=1. */
uint8_t assembly_serial; /**< Body[15:8], номер сборки Z. */
uint8_t position; /**< Body[7:0], позиция Y. */
uint8_t result; /**< Код ошибки для ERROR/INVALID. */
uint8_t id[CAN_SENSOR_ID_SIZE]; /**< ROM для WRITE/RESPONSE, нули для CLEAR. */
} CanSensor_Message;
/**
* @brief Доступ библиотеки к контроллеру CAN.
*
* Обязателен только @c send; при нулевом @c receive функция CanSensor_Poll()
* ничего не делает и узел работает только на передачу.
*/
/** Доступ переносимой части к CAN-контроллеру. */
typedef struct {
/** Ставит кадр в очередь передачи; 1 — кадр принят контроллером. */
uint8_t (*send)(void *context, const CanSensor_Frame *frame);
/** Забирает принятый кадр; 1 — кадр получен, 0 — очередь пуста. */
uint8_t (*receive)(void *context, CanSensor_Frame *frame);
void *context; /**< Контекст порта, передаётся вызовам без изменений. */
void *context;
} CanSensor_Io;
/**
* @brief Идентификаторы кадров и режим передачи.
*
* Нулевые поля заменяются значениями по умолчанию вызовом
* CanSensor_ConfigDefault() или самой CanSensor_Init().
*/
/** Базовые идентификаторы SETTINGS и режим транспорта. */
typedef struct {
uint32_t tx_id; /**< Идентификатор кадра преамбулы при передаче. */
uint32_t tx_data_id; /**< Идентификатор кадра ROM; 0 — @c tx_id + 1. */
uint32_t rx_id; /**< Ожидаемая преамбула при приёме; 0 — @c tx_id. */
uint32_t rx_data_id; /**< Ожидаемый кадр ROM; 0 — @c rx_id + 1. */
uint8_t extended; /**< 1 — расширенные идентификаторы 29 бит. */
uint8_t retries; /**< Попыток передачи кадра; 0 — значение по умолчанию. */
uint32_t tx_id; /**< База ответа 0x1FFE0000, Body должен быть нулевым. */
uint32_t rx_id; /**< База запроса 0x17FE0000, Body должен быть нулевым. */
uint8_t extended; /**< 1 — Extended CAN ID. */
uint8_t retries; /**< Число попыток передачи. */
uint32_t pulse_period_ms; /**< Период PULSE; 0 полностью отключает PULSE. */
} CanSensor_Config;
/**
* @brief Состояние узла: настройки, счётчики и сборка принимаемого сообщения.
*/
/** Состояние транспорта и диагностические счётчики. */
typedef struct {
CanSensor_Io io; /**< Обратные вызовы порта. */
CanSensor_Config config; /**< Идентификаторы кадров и режим передачи. */
CanSensor_Message rx; /**< Собираемое сообщение приёма. */
uint8_t rx_preamble; /**< 1 — преамбула принята, ожидается кадр ROM. */
uint32_t sent_messages; /**< Полностью переданные сообщения. */
uint32_t send_errors; /**< Сообщения, не ушедшие в шину. */
uint32_t received_messages; /**< Полностью принятые сообщения. */
uint32_t dropped_frames; /**< Кадры без преамбулы или с неверной длиной. */
CanSensor_Io io;
CanSensor_Config config;
uint32_t sent_messages;
uint32_t send_errors;
uint32_t received_messages;
uint32_t dropped_frames;
uint32_t pulse_last_ms;
uint8_t pulse_counter;
uint8_t pulse_armed;
} CanSensor;
/**
* @brief Заполняет настройки значениями по умолчанию.
*
* Идентификатор преамбулы — @ref CAN_SENSOR_DEFAULT_TX_ID, кадр ROM идёт
* следующим идентификатором, приём настроен на те же значения.
*
* @param config Настройки, принадлежащие вызывающему коду.
*/
void CanSensor_ConfigDefault(CanSensor_Config *config);
/**
* @brief Готовит узел к работе.
*
* @param link Состояние узла, принадлежащее вызывающему коду.
* @param io Обратные вызовы порта; копируются внутрь состояния.
* @param config Настройки либо 0 для значений по умолчанию.
* @return 1 при успешной настройке, 0 при неполных аргументах.
*/
uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
const CanSensor_Config *config);
/**
* @brief Собирает кадр преамбулы.
*
* @param config Настройки узла.
* @param frame Кадр приёмника.
* @param command Код команды, например @ref CAN_SENSOR_CMD_WRITE_POSITION.
* @param position Номер позиции записи датчика.
* @return 1 при успешной сборке, иначе 0.
*/
uint8_t CanSensor_BuildPreamble(const CanSensor_Config *config, CanSensor_Frame *frame,
uint8_t command, uint16_t position);
/** Собирает SETTINGS-кадр на базе tx_id или rx_id. */
uint8_t CanSensor_BuildFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
uint8_t response, uint8_t position,
uint8_t assembly_serial, const uint8_t *data,
uint8_t length);
/**
* @brief Собирает кадр идентификатора датчика.
*
* @param config Настройки узла.
* @param frame Кадр приёмника.
* @param id Идентификатор датчика длиной @ref CAN_SENSOR_ID_SIZE либо 0 для
* нулевого кадра команды очистки.
* @return 1 при успешной сборке, иначе 0.
*/
uint8_t CanSensor_BuildIdFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
const uint8_t *id);
/** Передаёт успешный ответ с текущим ROM (DLC=8). */
uint8_t CanSensor_SendId(CanSensor *link, uint8_t position,
uint8_t assembly_serial, const uint8_t *id);
/**
* @brief Передаёт преамбулу и идентификатор датчика.
*
* Кадры уходят подряд; при отказе контроллера передача повторяется
* @c retries раз. Если преамбула ушла, а кадр ROM — нет, сообщение считается
* несостоявшимся: приёмник отбросит одиночную преамбулу.
*
* @param link Состояние узла.
* @param command Код команды преамбулы.
* @param position Номер позиции записи датчика.
* @param id Идентификатор датчика либо 0 для команды очистки позиции.
* @return 1, если оба кадра приняты контроллером, иначе 0.
*/
uint8_t CanSensor_Send(CanSensor *link, uint8_t command, uint16_t position,
const uint8_t *id);
/** Передаёт ошибку SETTINGS (DLC=1). */
uint8_t CanSensor_SendError(CanSensor *link, uint8_t position,
uint8_t assembly_serial, uint8_t result);
/**
* @brief Передаёт команду записи датчика в позицию.
*
* Краткая форма CanSensor_Send() с кодом @ref CAN_SENSOR_CMD_WRITE_POSITION.
*
* @param link Состояние узла.
* @param position Номер позиции записи датчика.
* @param id Идентификатор датчика длиной @ref CAN_SENSOR_ID_SIZE.
* @return 1, если оба кадра приняты контроллером, иначе 0.
*/
uint8_t CanSensor_SendId(CanSensor *link, uint16_t position, const uint8_t *id);
/**
* @brief Разбирает принятый кадр и собирает из пары кадров сообщение.
*
* Кадр идентификатора без предшествующей преамбулы отбрасывается, а новая
* преамбула заменяет незавершённую: сборка не требует таймера.
*
* @param link Состояние узла.
* @param frame Принятый кадр.
* @param out Приёмник готового сообщения либо 0.
* @return 1, если сообщение собрано полностью, иначе 0.
*/
uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame,
CanSensor_Message *out);
/**
* @brief Забирает кадры у порта и возвращает первое собранное сообщение.
*
* @param link Состояние узла.
* @param out Приёмник сообщения либо 0.
* @return 1, если сообщение собрано, иначе 0.
*/
uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out);
/**
* @brief Меняет период PULSE во время работы.
*
* @param period_ms Период в миллисекундах; 0 выключает PULSE.
*/
void CanSensor_SetPulsePeriod(CanSensor *link, uint32_t period_ms);
/** Возвращает действующий период PULSE; 0 означает «выключено». */
uint32_t CanSensor_PulsePeriod(const CanSensor *link);
/** Вызывает планировщик PULSE из главного цикла. */
void CanSensor_Task(CanSensor *link, uint32_t now_ms);
#endif /* CAN_SENSOR_H */

View File

@@ -0,0 +1,330 @@
/**
* @file can_sensor_stm32f1.c
* @brief Опросный порт can-sensor на bxCAN STM32F1 без STM32 HAL.
*/
#include "can_sensor_stm32f1.h"
#include "can_sensor_stm32f1_config.h"
#if !defined(CAN_SENSOR_STM32F1_GPIO) \
|| !defined(CAN_SENSOR_STM32F1_GPIO_CLOCK) \
|| !defined(CAN_SENSOR_STM32F1_RX_PIN) \
|| !defined(CAN_SENSOR_STM32F1_TX_PIN)
#error "can_sensor_stm32f1_config.h должен задать GPIO, GPIO_CLOCK, RX_PIN и TX_PIN"
#endif
#define CAN_STM32F1_TQ_MAX 18U
#define CAN_STM32F1_TQ_MIN 8U
#define CAN_STM32F1_TIMEOUT_CYCLES 1000000U
#define CAN_STM32F1_TSR_ALL_EMPTY (CAN_TSR_TME0 | CAN_TSR_TME1 | CAN_TSR_TME2)
#define CAN_STM32F1_TSR_ALL_ABORT (CAN_TSR_ABRQ0 | CAN_TSR_ABRQ1 | CAN_TSR_ABRQ2)
#define CAN_STM32F1_TSR_ANY_TXOK (CAN_TSR_TXOK0 | CAN_TSR_TXOK1 | CAN_TSR_TXOK2)
#define CAN_STM32F1_TSR_DONE_FLAGS \
(CAN_TSR_RQCP0 | CAN_TSR_TXOK0 | CAN_TSR_ALST0 | CAN_TSR_TERR0 \
| CAN_TSR_RQCP1 | CAN_TSR_TXOK1 | CAN_TSR_ALST1 | CAN_TSR_TERR1 \
| CAN_TSR_RQCP2 | CAN_TSR_TXOK2 | CAN_TSR_ALST2 | CAN_TSR_TERR2)
static uint32_t can_stm32f1_pclk1(void)
{
static const uint8_t shift[8] = { 0U, 0U, 0U, 0U, 1U, 2U, 3U, 4U };
const uint32_t bits = (RCC->CFGR & RCC_CFGR_PPRE1) >> 8U;
return SystemCoreClock >> shift[bits & 0x7U];
}
static void can_stm32f1_configure_pin(GPIO_TypeDef *gpio, uint8_t pin,
uint32_t mode)
{
uint32_t config;
uint32_t shift;
if (pin < 8U) {
shift = (uint32_t)pin * 4U;
config = gpio->CRL;
config = (config & ~(0xFUL << shift)) | (mode << shift);
gpio->CRL = config;
} else {
shift = ((uint32_t)pin - 8U) * 4U;
config = gpio->CRH;
config = (config & ~(0xFUL << shift)) | (mode << shift);
gpio->CRH = config;
}
}
static void can_stm32f1_configure_pins(void)
{
RCC->APB2ENR |= CAN_SENSOR_STM32F1_GPIO_CLOCK | RCC_APB2ENR_AFIOEN;
/* RX: input pull-up; TX: alternate function push-pull, 50 MHz. */
can_stm32f1_configure_pin(CAN_SENSOR_STM32F1_GPIO,
CAN_SENSOR_STM32F1_RX_PIN, 0x8UL);
can_stm32f1_configure_pin(CAN_SENSOR_STM32F1_GPIO,
CAN_SENSOR_STM32F1_TX_PIN, 0xBUL);
CAN_SENSOR_STM32F1_GPIO->BSRR =
(uint32_t)(1UL << CAN_SENSOR_STM32F1_RX_PIN);
}
static uint8_t can_stm32f1_calc_timing(uint32_t bitrate, uint32_t *btr,
CanSensorStm32F1_Timing *timing)
{
const uint32_t pclk = can_stm32f1_pclk1();
uint32_t total;
uint32_t tq;
if ((bitrate == 0U) || (btr == 0) || (pclk == 0U)
|| ((pclk % bitrate) != 0U)) {
return 0U;
}
total = pclk / bitrate;
for (tq = CAN_STM32F1_TQ_MAX; tq >= CAN_STM32F1_TQ_MIN; tq--) {
const uint32_t prescaler = total / tq;
if (((total % tq) == 0U) && (prescaler >= 1U)
&& (prescaler <= 1024U)) {
const uint32_t ts2 = tq / 5U;
const uint32_t ts1 = tq - 1U - ts2;
*btr = ((ts2 - 1U) << 20U) | ((ts1 - 1U) << 16U)
| (prescaler - 1U);
if (timing != 0) {
timing->pclk_hz = pclk;
timing->prescaler = (uint16_t)prescaler;
timing->ts1 = (uint8_t)ts1;
timing->ts2 = (uint8_t)ts2;
timing->bitrate = pclk / (prescaler * tq);
timing->sample_point =
(uint8_t)(((1U + ts1) * 100U) / tq);
}
return 1U;
}
}
return 0U;
}
uint8_t CanSensorStm32F1_CalcTiming(uint32_t bitrate,
CanSensorStm32F1_Timing *timing)
{
uint32_t btr;
return can_stm32f1_calc_timing(bitrate, &btr, timing);
}
static void can_stm32f1_configure_filter(uint32_t id, uint32_t mask)
{
const uint32_t filter_id = ((id & mask) << 3U) | CAN_RI0R_IDE;
const uint32_t filter_mask =
(mask << 3U) | CAN_RI0R_IDE | CAN_RI0R_RTR;
CAN1->FMR |= CAN_FMR_FINIT;
CAN1->FA1R &= ~1UL;
CAN1->FS1R |= 1UL;
CAN1->FM1R &= ~1UL;
CAN1->sFilterRegister[0].FR1 = filter_id;
CAN1->sFilterRegister[0].FR2 = filter_mask;
CAN1->FFA1R &= ~1UL;
CAN1->FA1R |= 1UL;
CAN1->FMR &= ~CAN_FMR_FINIT;
}
static void can_stm32f1_load_mailbox(uint32_t mailbox,
const CanSensor_Frame *frame)
{
uint32_t identifier;
uint8_t index;
if (frame->extended != 0U) {
identifier = ((frame->id & 0x1FFFFFFFUL) << 3U) | CAN_TI0R_IDE;
} else {
identifier = (frame->id & 0x7FFUL) << 21U;
}
CAN1->sTxMailBox[mailbox].TDTR = (uint32_t)(frame->length & 0x0FU);
CAN1->sTxMailBox[mailbox].TDLR = 0U;
CAN1->sTxMailBox[mailbox].TDHR = 0U;
for (index = 0U; index < frame->length; index++) {
if (index < 4U) {
CAN1->sTxMailBox[mailbox].TDLR |=
(uint32_t)frame->data[index] << (index * 8U);
} else {
CAN1->sTxMailBox[mailbox].TDHR |=
(uint32_t)frame->data[index] << ((index - 4U) * 8U);
}
}
CAN1->sTxMailBox[mailbox].TIR = identifier | CAN_TI0R_TXRQ;
}
uint8_t CanSensorStm32F1_Start(CanSensorStm32F1_Port *port,
uint32_t bitrate, uint32_t filter_id,
uint32_t filter_mask, uint8_t loopback)
{
uint32_t btr;
uint32_t guard;
uint32_t mode_bits;
if ((port == 0) || (filter_id > 0x1FFFFFFFUL)
|| (filter_mask > 0x1FFFFFFFUL)
|| (can_stm32f1_calc_timing(bitrate, &btr, &port->timing) == 0U)) {
return 0U;
}
CanSensorStm32F1_Stop(port);
can_stm32f1_configure_pins();
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
CAN1->MCR &= ~CAN_MCR_SLEEP;
CAN1->MCR |= CAN_MCR_INRQ;
for (guard = 0U; guard < CAN_STM32F1_TIMEOUT_CYCLES; guard++) {
if ((CAN1->MSR & CAN_MSR_INAK) != 0U) {
break;
}
}
if ((CAN1->MSR & CAN_MSR_INAK) == 0U) {
return 0U;
}
mode_bits = (loopback != 0U) ? (CAN_BTR_LBKM | CAN_BTR_SILM) : 0U;
CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_ABOM | CAN_MCR_TXFP;
CAN1->BTR = btr | mode_bits;
can_stm32f1_configure_filter(filter_id, filter_mask);
CAN1->MCR &= ~CAN_MCR_INRQ;
for (guard = 0U; guard < CAN_STM32F1_TIMEOUT_CYCLES; guard++) {
if ((CAN1->MSR & CAN_MSR_INAK) == 0U) {
break;
}
}
if ((CAN1->MSR & CAN_MSR_INAK) != 0U) {
return 0U;
}
port->bitrate = bitrate;
port->loopback = (uint8_t)(loopback != 0U);
port->no_transceiver = 0U;
port->tx_waiting = 0U;
port->ready = 1U;
return 1U;
}
void CanSensorStm32F1_Stop(CanSensorStm32F1_Port *port)
{
if (port == 0) {
return;
}
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
RCC->APB1RSTR |= RCC_APB1RSTR_CAN1RST;
RCC->APB1RSTR &= ~RCC_APB1RSTR_CAN1RST;
port->ready = 0U;
port->bitrate = 0U;
port->loopback = 0U;
port->no_transceiver = 0U;
port->tx_waiting = 0U;
}
uint8_t CanSensorStm32F1_Send(void *context, const CanSensor_Frame *frame)
{
CanSensorStm32F1_Port *port = (CanSensorStm32F1_Port *)context;
uint32_t mailbox;
if ((port == 0) || (frame == 0) || (port->ready == 0U)
|| (frame->length > CAN_SENSOR_MAX_DATA)) {
return 0U;
}
if ((CAN1->TSR & CAN_TSR_TME0) != 0U) {
mailbox = 0U;
} else if ((CAN1->TSR & CAN_TSR_TME1) != 0U) {
mailbox = 1U;
} else if ((CAN1->TSR & CAN_TSR_TME2) != 0U) {
mailbox = 2U;
} else {
return 0U;
}
can_stm32f1_load_mailbox(mailbox, frame);
return 1U;
}
uint8_t CanSensorStm32F1_Receive(void *context, CanSensor_Frame *frame)
{
CanSensorStm32F1_Port *port = (CanSensorStm32F1_Port *)context;
uint32_t identifier;
uint32_t low;
uint32_t high;
uint8_t index;
if ((port == 0) || (frame == 0) || (port->ready == 0U)
|| ((CAN1->RF0R & CAN_RF0R_FMP0) == 0U)) {
return 0U;
}
identifier = CAN1->sFIFOMailBox[0].RIR;
if ((identifier & CAN_RI0R_IDE) != 0U) {
frame->extended = 1U;
frame->id = (identifier >> 3U) & 0x1FFFFFFFUL;
} else {
frame->extended = 0U;
frame->id = (identifier >> 21U) & 0x7FFUL;
}
frame->length = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 0x0FU);
if (frame->length > CAN_SENSOR_MAX_DATA) {
frame->length = CAN_SENSOR_MAX_DATA;
}
low = CAN1->sFIFOMailBox[0].RDLR;
high = CAN1->sFIFOMailBox[0].RDHR;
for (index = 0U; index < CAN_SENSOR_MAX_DATA; index++) {
if (index < 4U) {
frame->data[index] =
(uint8_t)((low >> (index * 8U)) & 0xFFU);
} else {
frame->data[index] =
(uint8_t)((high >> ((index - 4U) * 8U)) & 0xFFU);
}
}
CAN1->RF0R |= CAN_RF0R_RFOM0;
return 1U;
}
CanSensor_Io CanSensorStm32F1_MakeIo(CanSensorStm32F1_Port *port)
{
CanSensor_Io io;
io.send = CanSensorStm32F1_Send;
io.receive = CanSensorStm32F1_Receive;
io.context = port;
return io;
}
void CanSensorStm32F1_Task(CanSensorStm32F1_Port *port, uint32_t now_ms,
uint32_t timeout_ms)
{
uint32_t status;
if ((port == 0) || (port->ready == 0U)) {
return;
}
status = CAN1->TSR;
if ((status & CAN_STM32F1_TSR_ALL_EMPTY) == CAN_STM32F1_TSR_ALL_EMPTY) {
if ((status & CAN_STM32F1_TSR_ANY_TXOK) != 0U) {
port->no_transceiver = 0U;
}
CAN1->TSR = status & CAN_STM32F1_TSR_DONE_FLAGS;
port->tx_waiting = 0U;
return;
}
if ((timeout_ms == 0U) || (port->tx_waiting == 0U)) {
port->tx_waiting = (uint8_t)(timeout_ms != 0U);
port->tx_started_ms = now_ms;
return;
}
if ((now_ms - port->tx_started_ms) < timeout_ms) {
return;
}
CAN1->TSR = CAN_STM32F1_TSR_ALL_ABORT;
CAN1->TSR = CAN_STM32F1_TSR_DONE_FLAGS;
port->tx_waiting = 0U;
port->tx_timeouts++;
port->no_transceiver = 1U;
}
uint8_t CanSensorStm32F1_BusError(const CanSensorStm32F1_Port *port)
{
if ((port == 0) || (port->ready == 0U)) {
return 0U;
}
return (uint8_t)(((CAN1->ESR & (CAN_ESR_BOFF | CAN_ESR_EPVF)) != 0U)
? 1U : 0U);
}

View File

@@ -0,0 +1,77 @@
/**
* @file can_sensor_stm32f1.h
* @brief Опросный порт can-sensor на bxCAN микроконтроллеров STM32F1.
*/
#ifndef CAN_SENSOR_STM32F1_H
#define CAN_SENSOR_STM32F1_H
#include <stdint.h>
#include "can_sensor.h"
/** Разрядность бита, записанная портом в регистр BTR. */
typedef struct {
uint32_t pclk_hz;
uint32_t bitrate;
uint16_t prescaler;
uint8_t ts1;
uint8_t ts2;
uint8_t sample_point;
} CanSensorStm32F1_Timing;
/** Состояние одного контроллера bxCAN. Поля доступны только для диагностики. */
typedef struct {
uint32_t bitrate;
uint32_t tx_timeouts;
uint32_t tx_started_ms;
CanSensorStm32F1_Timing timing;
uint8_t ready;
uint8_t loopback;
uint8_t no_transceiver;
uint8_t tx_waiting;
} CanSensorStm32F1_Port;
/**
* @brief Проверяет достижимость скорости и рассчитывает поля BTR.
* @param bitrate Требуемая скорость, бит/с.
* @param timing Приёмник результата либо 0.
* @return 1, если скорость получается из текущей частоты APB1 без ошибки.
*/
uint8_t CanSensorStm32F1_CalcTiming(uint32_t bitrate,
CanSensorStm32F1_Timing *timing);
/**
* @brief Запускает CAN1, настраивает выводы и один 32-битный фильтр FIFO 0.
*
* Фильтр сравнивает Extended ID по формуле `(id & filter_mask) ==
* (filter_id & filter_mask)` и отбрасывает Standard и Remote кадры.
*/
uint8_t CanSensorStm32F1_Start(CanSensorStm32F1_Port *port,
uint32_t bitrate, uint32_t filter_id,
uint32_t filter_mask, uint8_t loopback);
/** Останавливает CAN1 программным сбросом. */
void CanSensorStm32F1_Stop(CanSensorStm32F1_Port *port);
/** Помещает data-кадр в свободный mailbox. Совместима с CanSensor_Io.send. */
uint8_t CanSensorStm32F1_Send(void *context, const CanSensor_Frame *frame);
/** Забирает один data-кадр из FIFO 0. Совместима с CanSensor_Io.receive. */
uint8_t CanSensorStm32F1_Receive(void *context, CanSensor_Frame *frame);
/** Собирает таблицу callback-функций для CanSensor_Init(). */
CanSensor_Io CanSensorStm32F1_MakeIo(CanSensorStm32F1_Port *port);
/**
* @brief Обслуживает завершение и тайм-аут передачи без прерываний.
* @param now_ms Монотонное время приложения, мс.
* @param timeout_ms Предел ожидания ACK; 0 отключает принудительную отмену.
*/
void CanSensorStm32F1_Task(CanSensorStm32F1_Port *port, uint32_t now_ms,
uint32_t timeout_ms);
/** Возвращает 1 при bus-off или error-passive. */
uint8_t CanSensorStm32F1_BusError(const CanSensorStm32F1_Port *port);
#endif /* CAN_SENSOR_STM32F1_H */

View File

@@ -0,0 +1,19 @@
/**
* @file can_sensor_stm32f1_config.f103.template.h
* @brief Шаблон выводов CAN1 для STM32F103 без ремапа.
*
* Скопируйте файл в include-каталог приложения под именем
* can_sensor_stm32f1_config.h. Скорость и фильтр задаются при запуске порта.
*/
#ifndef CAN_SENSOR_STM32F1_CONFIG_H
#define CAN_SENSOR_STM32F1_CONFIG_H
#include "stm32f10x.h"
#define CAN_SENSOR_STM32F1_GPIO GPIOA
#define CAN_SENSOR_STM32F1_GPIO_CLOCK RCC_APB2ENR_IOPAEN
#define CAN_SENSOR_STM32F1_RX_PIN 11U
#define CAN_SENSOR_STM32F1_TX_PIN 12U
#endif /* CAN_SENSOR_STM32F1_CONFIG_H */

View 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;
}

View 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
View File

@@ -0,0 +1,145 @@
# DS18B20 через DS2480B
Переносимая библиотека C99: поиск DS18B20 на общей шине, температура,
разрешение 912 бит, 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)

View 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);
}

View 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
View 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
View 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

View 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);
}

View 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).

View 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;
}

View 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

View 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

View 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=&regs;
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;
}

View 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;
}

View File

@@ -23,7 +23,9 @@
|---|---|---|
| `include/ds18b20.h`, `src/ds18b20.c` | перечень датчиков, измерения, запись TH/TL/разрешения | `onewire.h`, `stdint.h` |
| `include/onewire.h`, `src/onewire.c` | байты и блоки, CRC8 Dallas/Maxim, обход SEARCH ROM | `stdint.h` |
| `ports/stm32f1/` | тайминги Maxim, вывод с открытым стоком, задержки по DWT | CMSIS `stm32f10x.h` |
| `ports/stm32f1/` | STM32F103: тайминги Maxim, открытый сток, задержки по DWT | CMSIS `stm32f10x.h` |
| `ports/stm32g4/` | то же для STM32G431 и STM32G474 | CMSIS `stm32g4xx.h` |
| `ports/stm32f4/` | то же для STM32F407VET6 и родственных F4 | CMSIS `stm32f4xx.h` |
| `tests/` | симулятор шины с виртуальными датчиками и host-тесты | — |
## Порт: что нужно от платформы
@@ -48,7 +50,7 @@ uint8_t OneWire_ReadBit(void);
микросекундных задержек — счётчик тактов ядра `DWT->CYCCNT`.
Привязку вывода порт берёт из `onewire_config.h`, который предоставляет
приложение — шаблон лежит рядом:
приложение — шаблон `onewire_config.f103.template.h` лежит рядом:
```c
#include "stm32f10x.h"
@@ -60,6 +62,33 @@ uint8_t OneWire_ReadBit(void);
Требуется внешняя подтяжка 4.7 кОм к питанию.
### Готовые порты STM32G4 и STM32F4
Логика та же, что и в порте STM32F1: открытый сток, чтение через `IDR`,
задержки по `DWT->CYCCNT`, запрет прерываний только на один тайм-слот.
Отличается лишь настройка вывода — у G4 и F4 это `MODER`/`OTYPER`/
`OSPEEDR`/`PUPDR` вместо `CRL`/`CRH`, а низкий уровень выставляется
через верхнюю половину `BSRR`.
STM32G431 и STM32G474 обслуживает один порт `ports/stm32g4/`: блок GPIO и
шина тактирования у них совпадают, различается только CMSIS-заголовок в
`onewire_config.h`. Шаблоны лежат рядом — по одному на каждый камень:
```c
/* ports/stm32g4/onewire_config.g474.template.h */
#include "stm32g4xx.h"
#define ONEWIRE_GPIO GPIOA
#define ONEWIRE_PIN 1U
#define ONEWIRE_GPIO_CLOCK RCC_AHB2ENR_GPIOAEN /* у F4 — RCC_AHB1ENR_GPIOAEN */
```
Порт `ports/stm32f4/` написан под STM32F407VET6 и подходит любому F4 с тем
же блоком GPIO; тактирование включается в `RCC->AHB1ENR`.
Подтяжка 4.7 кОм — внешняя и здесь: внутренняя `PUPDR` намеренно
оставлена выключенной, её сопротивления для шины не хватает.
## Использование
```c

View File

@@ -13,7 +13,7 @@
* и OneWire_ReadBit.
*
* Чтобы перенести библиотеку на другой микроконтроллер, достаточно написать
* эти пять функций - остальное собирается как есть.
* эти пять функций - остальное собирается как есть. ТЕСТ
*/
#ifndef ONEWIRE_H

View File

@@ -1,6 +1,6 @@
/**
* @file onewire_config.template.h
* @brief Шаблон привязки шины 1-Wire для порта STM32F1.
* @file onewire_config.f103.template.h
* @brief Шаблон привязки шины 1-Wire для STM32F103 (порт stm32f1).
*
* Скопируйте в своё приложение под именем onewire_config.h и положите на
* путь поиска заголовков. Библиотека сама этот файл не подставляет:

View File

@@ -8,7 +8,8 @@
* терять байты USART во время обмена.
*
* Привязка вывода приходит из onewire_config.h, который предоставляет
* приложение. Шаблон - onewire_config.template.h рядом с этим файлом.
* приложение. Шаблон для STM32F103 —
* onewire_config.f103.template.h рядом с этим файлом.
*/
#include "onewire.h"

View File

@@ -0,0 +1,26 @@
/**
* @file onewire_config.f407.template.h
* @brief Шаблон привязки шины 1-Wire для STM32F407VET6 (порт stm32f4).
*
* Скопируйте в своё приложение под именем onewire_config.h и положите на
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
* привязка выводов принадлежит плате, а не драйверу.
*
* Требуется внешняя подтяжка 4.7 кОм к питанию.
*/
#ifndef ONEWIRE_CONFIG_H
#define ONEWIRE_CONFIG_H
#include "stm32f4xx.h"
/** Порт вывода шины. */
#define ONEWIRE_GPIO GPIOA
/** Номер вывода в порту, 0..15. */
#define ONEWIRE_PIN 1U
/** Бит включения тактирования порта в RCC->AHB1ENR. */
#define ONEWIRE_GPIO_CLOCK RCC_AHB1ENR_GPIOAEN
#endif /* ONEWIRE_CONFIG_H */

View File

@@ -0,0 +1,170 @@
/**
* @file onewire_stm32f4.c
* @brief Порт шины 1-Wire на STM32F4: тайминги, вывод и задержки.
*
* Написан под STM32F407VET6 и подходит любому STM32F4 с тем же блоком GPIO:
* различается только CMSIS-заголовок, который подставляет onewire_config.h.
*
* Вывод постоянно настроен как выход с открытым стоком: чтение уровня
* выполняется через регистр IDR, поэтому переключать режим не требуется.
* Прерывания запрещаются только на длительность одного тайм-слота, чтобы не
* терять байты USART во время обмена.
*
* Привязка вывода приходит из onewire_config.h, который предоставляет
* приложение. Шаблон — onewire_config.f407.template.h рядом с этим файлом.
*/
#include "onewire.h"
#include "onewire_config.h"
#if !defined(ONEWIRE_GPIO) || !defined(ONEWIRE_PIN) || !defined(ONEWIRE_GPIO_CLOCK)
#error "onewire_config.h должен задать ONEWIRE_GPIO, ONEWIRE_PIN и ONEWIRE_GPIO_CLOCK"
#endif
/** Длительности тайм-слотов в микросекундах согласно рекомендациям Maxim. */
#define OW_RESET_LOW_US 480U
#define OW_PRESENCE_WAIT_US 70U
#define OW_RESET_TAIL_US 410U
#define OW_WRITE1_LOW_US 6U
#define OW_WRITE1_TAIL_US 64U
#define OW_WRITE0_LOW_US 60U
#define OW_WRITE0_TAIL_US 10U
#define OW_READ_LOW_US 6U
#define OW_READ_SAMPLE_US 9U
#define OW_READ_TAIL_US 55U
/** Число тактов ядра на микросекунду; заполняется в OneWire_Init. */
static uint32_t g_cycles_per_us = 168U;
/**
* @brief Переводит вывод шины в активный низкий уровень.
*/
static void ow_drive_low(void)
{
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << (ONEWIRE_PIN + 16U));
}
/**
* @brief Отпускает шину; уровень поднимает внешний резистор подтяжки.
*/
static void ow_release(void)
{
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << ONEWIRE_PIN);
}
/**
* @brief Читает текущий уровень на выводе шины.
*
* @return 1 для высокого уровня, 0 для низкого.
*/
static uint8_t ow_read_pin(void)
{
return (uint8_t)((ONEWIRE_GPIO->IDR >> ONEWIRE_PIN) & 1U);
}
void OneWire_DelayUs(uint32_t microseconds)
{
const uint32_t start = DWT->CYCCNT;
const uint32_t ticks = microseconds * g_cycles_per_us;
while ((DWT->CYCCNT - start) < ticks) {
__NOP();
}
}
void OneWire_Init(void)
{
uint32_t config;
g_cycles_per_us = SystemCoreClock / 1000000U;
if (g_cycles_per_us == 0U) {
g_cycles_per_us = 1U;
}
/* Счётчик тактов ядра — единственный источник микросекундных задержек. */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0U;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
RCC->AHB1ENR |= ONEWIRE_GPIO_CLOCK;
/* Открытый сток: OTYPER = 1. */
ONEWIRE_GPIO->OTYPER |= (uint32_t)(1UL << ONEWIRE_PIN);
/* Подтяжка внешняя, внутренняя не нужна: PUPDR = 00. */
config = ONEWIRE_GPIO->PUPDR;
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->PUPDR = config;
/* Максимальная скорость фронта: OSPEEDR = 11. */
config = ONEWIRE_GPIO->OSPEEDR;
config |= (0x3UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->OSPEEDR = config;
/* Уровень отпускаем до перевода в выход, чтобы не дёрнуть шину вниз. */
ow_release();
/* Обычный выход: MODER = 01. */
config = ONEWIRE_GPIO->MODER;
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
config |= (0x1UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->MODER = config;
OneWire_DelayUs(OW_RESET_LOW_US);
}
uint8_t OneWire_Reset(void)
{
uint32_t primask;
uint8_t presence;
/* Удлинение импульса сброса безопасно, поэтому прерывания не запрещаем. */
ow_drive_low();
OneWire_DelayUs(OW_RESET_LOW_US);
primask = __get_PRIMASK();
__disable_irq();
ow_release();
OneWire_DelayUs(OW_PRESENCE_WAIT_US);
presence = (uint8_t)(ow_read_pin() == 0U);
__set_PRIMASK(primask);
OneWire_DelayUs(OW_RESET_TAIL_US);
return (uint8_t)(presence && (ow_read_pin() != 0U));
}
void OneWire_WriteBit(uint8_t bit)
{
const uint32_t primask = __get_PRIMASK();
__disable_irq();
ow_drive_low();
if (bit != 0U) {
OneWire_DelayUs(OW_WRITE1_LOW_US);
ow_release();
OneWire_DelayUs(OW_WRITE1_TAIL_US);
} else {
OneWire_DelayUs(OW_WRITE0_LOW_US);
ow_release();
OneWire_DelayUs(OW_WRITE0_TAIL_US);
}
__set_PRIMASK(primask);
}
uint8_t OneWire_ReadBit(void)
{
const uint32_t primask = __get_PRIMASK();
uint8_t bit;
__disable_irq();
ow_drive_low();
OneWire_DelayUs(OW_READ_LOW_US);
ow_release();
OneWire_DelayUs(OW_READ_SAMPLE_US);
bit = ow_read_pin();
__set_PRIMASK(primask);
OneWire_DelayUs(OW_READ_TAIL_US);
return bit;
}

View File

@@ -0,0 +1,26 @@
/**
* @file onewire_config.g431.template.h
* @brief Шаблон привязки шины 1-Wire для STM32G431 (порт stm32g4).
*
* Скопируйте в своё приложение под именем onewire_config.h и положите на
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
* привязка выводов принадлежит плате, а не драйверу.
*
* Требуется внешняя подтяжка 4.7 кОм к питанию.
*/
#ifndef ONEWIRE_CONFIG_H
#define ONEWIRE_CONFIG_H
#include "stm32g4xx.h"
/** Порт вывода шины. */
#define ONEWIRE_GPIO GPIOA
/** Номер вывода в порту, 0..15. */
#define ONEWIRE_PIN 1U
/** Бит включения тактирования порта в RCC->AHB2ENR. */
#define ONEWIRE_GPIO_CLOCK RCC_AHB2ENR_GPIOAEN
#endif /* ONEWIRE_CONFIG_H */

View File

@@ -0,0 +1,26 @@
/**
* @file onewire_config.g474.template.h
* @brief Шаблон привязки шины 1-Wire для STM32G474 (порт stm32g4).
*
* Скопируйте в своё приложение под именем onewire_config.h и положите на
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
* привязка выводов принадлежит плате, а не драйверу.
*
* Требуется внешняя подтяжка 4.7 кОм к питанию.
*/
#ifndef ONEWIRE_CONFIG_H
#define ONEWIRE_CONFIG_H
#include "stm32g4xx.h"
/** Порт вывода шины. */
#define ONEWIRE_GPIO GPIOA
/** Номер вывода в порту, 0..15. */
#define ONEWIRE_PIN 1U
/** Бит включения тактирования порта в RCC->AHB2ENR. */
#define ONEWIRE_GPIO_CLOCK RCC_AHB2ENR_GPIOAEN
#endif /* ONEWIRE_CONFIG_H */

View File

@@ -0,0 +1,172 @@
/**
* @file onewire_stm32g4.c
* @brief Порт шины 1-Wire на STM32G4: тайминги, вывод и задержки.
*
* Подходит и STM32G431, и STM32G474 — периферия GPIO и шина тактирования
* у них одинаковы, различается только CMSIS-заголовок, который подставляет
* onewire_config.h.
*
* Вывод постоянно настроен как выход с открытым стоком: чтение уровня
* выполняется через регистр IDR, поэтому переключать режим не требуется.
* Прерывания запрещаются только на длительность одного тайм-слота, чтобы не
* терять байты USART во время обмена.
*
* Привязка вывода приходит из onewire_config.h, который предоставляет
* приложение. Шаблоны — onewire_config.g431.template.h и
* onewire_config.g474.template.h рядом с этим файлом.
*/
#include "onewire.h"
#include "onewire_config.h"
#if !defined(ONEWIRE_GPIO) || !defined(ONEWIRE_PIN) || !defined(ONEWIRE_GPIO_CLOCK)
#error "onewire_config.h должен задать ONEWIRE_GPIO, ONEWIRE_PIN и ONEWIRE_GPIO_CLOCK"
#endif
/** Длительности тайм-слотов в микросекундах согласно рекомендациям Maxim. */
#define OW_RESET_LOW_US 480U
#define OW_PRESENCE_WAIT_US 70U
#define OW_RESET_TAIL_US 410U
#define OW_WRITE1_LOW_US 6U
#define OW_WRITE1_TAIL_US 64U
#define OW_WRITE0_LOW_US 60U
#define OW_WRITE0_TAIL_US 10U
#define OW_READ_LOW_US 6U
#define OW_READ_SAMPLE_US 9U
#define OW_READ_TAIL_US 55U
/** Число тактов ядра на микросекунду; заполняется в OneWire_Init. */
static uint32_t g_cycles_per_us = 170U;
/**
* @brief Переводит вывод шины в активный низкий уровень.
*/
static void ow_drive_low(void)
{
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << (ONEWIRE_PIN + 16U));
}
/**
* @brief Отпускает шину; уровень поднимает внешний резистор подтяжки.
*/
static void ow_release(void)
{
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << ONEWIRE_PIN);
}
/**
* @brief Читает текущий уровень на выводе шины.
*
* @return 1 для высокого уровня, 0 для низкого.
*/
static uint8_t ow_read_pin(void)
{
return (uint8_t)((ONEWIRE_GPIO->IDR >> ONEWIRE_PIN) & 1U);
}
void OneWire_DelayUs(uint32_t microseconds)
{
const uint32_t start = DWT->CYCCNT;
const uint32_t ticks = microseconds * g_cycles_per_us;
while ((DWT->CYCCNT - start) < ticks) {
__NOP();
}
}
void OneWire_Init(void)
{
uint32_t config;
g_cycles_per_us = SystemCoreClock / 1000000U;
if (g_cycles_per_us == 0U) {
g_cycles_per_us = 1U;
}
/* Счётчик тактов ядра — единственный источник микросекундных задержек. */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0U;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
RCC->AHB2ENR |= ONEWIRE_GPIO_CLOCK;
/* Открытый сток: OTYPER = 1. */
ONEWIRE_GPIO->OTYPER |= (uint32_t)(1UL << ONEWIRE_PIN);
/* Подтяжка внешняя, внутренняя не нужна: PUPDR = 00. */
config = ONEWIRE_GPIO->PUPDR;
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->PUPDR = config;
/* Максимальная скорость фронта: OSPEEDR = 11. */
config = ONEWIRE_GPIO->OSPEEDR;
config |= (0x3UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->OSPEEDR = config;
/* Уровень отпускаем до перевода в выход, чтобы не дёрнуть шину вниз. */
ow_release();
/* Обычный выход: MODER = 01. */
config = ONEWIRE_GPIO->MODER;
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
config |= (0x1UL << (ONEWIRE_PIN * 2U));
ONEWIRE_GPIO->MODER = config;
OneWire_DelayUs(OW_RESET_LOW_US);
}
uint8_t OneWire_Reset(void)
{
uint32_t primask;
uint8_t presence;
/* Удлинение импульса сброса безопасно, поэтому прерывания не запрещаем. */
ow_drive_low();
OneWire_DelayUs(OW_RESET_LOW_US);
primask = __get_PRIMASK();
__disable_irq();
ow_release();
OneWire_DelayUs(OW_PRESENCE_WAIT_US);
presence = (uint8_t)(ow_read_pin() == 0U);
__set_PRIMASK(primask);
OneWire_DelayUs(OW_RESET_TAIL_US);
return (uint8_t)(presence && (ow_read_pin() != 0U));
}
void OneWire_WriteBit(uint8_t bit)
{
const uint32_t primask = __get_PRIMASK();
__disable_irq();
ow_drive_low();
if (bit != 0U) {
OneWire_DelayUs(OW_WRITE1_LOW_US);
ow_release();
OneWire_DelayUs(OW_WRITE1_TAIL_US);
} else {
OneWire_DelayUs(OW_WRITE0_LOW_US);
ow_release();
OneWire_DelayUs(OW_WRITE0_TAIL_US);
}
__set_PRIMASK(primask);
}
uint8_t OneWire_ReadBit(void)
{
const uint32_t primask = __get_PRIMASK();
uint8_t bit;
__disable_irq();
ow_drive_low();
OneWire_DelayUs(OW_READ_LOW_US);
ow_release();
OneWire_DelayUs(OW_READ_SAMPLE_US);
bit = ow_read_pin();
__set_PRIMASK(primask);
OneWire_DelayUs(OW_READ_TAIL_US);
return bit;
}

View File

@@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.16)
project(firmware_info C)
add_library(firmware_info src/firmware_info.c)
target_include_directories(firmware_info PUBLIC include)
add_executable(test_firmware_info tests/test_firmware_info.c)
target_link_libraries(test_firmware_info PRIVATE firmware_info)
enable_testing()
add_test(NAME firmware_info COMMAND test_firmware_info)

View File

@@ -0,0 +1,11 @@
# PORTING
Аппаратно-зависимый контракт ограничен `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 в
include path, запуск генератора до компиляции и декодирование build ID как двух
ASCII-байтов в каждом логическом слове.

27
c/firmware-info/README.md Normal file
View File

@@ -0,0 +1,27 @@
# Firmware Info
Переносимая библиотека версии реально запущенной прошивки. Повторяет контракт
`FirmwareInfo` из `john103C6T6NewVer`, но оформлена как template: ядро не знает
ни о HAL, ни о конкретном протоколе, ни о модели МК.
Контракт v1 содержит 12 слов: версия контракта, `major.minor.patch`, дата/время
компиляции и 8-символьный build ID. `firmware_info_to_registers()` подходит для
Modbus, `firmware_info_to_le_bytes()` — для SETGUI и других byte-протоколов.
## Подключение
1. Добавьте `include`, `src/firmware_info.c` и `src/firmware_info_port.c`.
2. Скопируйте подходящий `ports/<mcu>/firmware_info_config.template.h` в проект
как `firmware_info_config.h` и задайте SemVer.
3. Добавьте каталог с config и `include` в include path.
4. Перед сборкой запустите `tools/make_build_id.ps1`; каталог generated также
должен быть в include path. Чистая сборка получает 8 знаков git commit,
dirty-сборка — 7 знаков и `+`, сборка без Git — `NOGIT000`.
Готовые конфигурации есть для STM32F1, STM32F4, STM32G4 и К1921ВК028. Порт не
содержит регистров МК: стандартные `__DATE__`/`__TIME__` одинаковы для GCC,
Arm Compiler 5/6 и Clang. Поэтому добавление следующего МК требует только
config, а не дублирования ядра.
KONOR публикует эти 24 байта ответом `FIRMWARE_INFO (0x03)`, сохраняя старый
32-байтовый `DEVICE_INFO` без изменений.

View File

@@ -0,0 +1,48 @@
#ifndef FIRMWARE_INFO_H
#define FIRMWARE_INFO_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define FIRMWARE_INFO_CONTRACT_VERSION 1U
#define FIRMWARE_INFO_REGISTER_COUNT 12U
#define FIRMWARE_INFO_PAYLOAD_SIZE (FIRMWARE_INFO_REGISTER_COUNT * 2U)
#define FIRMWARE_INFO_BUILD_ID_SIZE 8U
typedef struct {
uint16_t major;
uint16_t minor;
uint16_t patch;
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
char build_id[FIRMWARE_INFO_BUILD_ID_SIZE + 1U];
} firmware_info_t;
typedef enum {
FIRMWARE_INFO_OK = 0,
FIRMWARE_INFO_INVALID_ARGUMENT,
FIRMWARE_INFO_OUT_OF_RANGE
} firmware_info_status_t;
firmware_info_status_t firmware_info_validate(const firmware_info_t *info);
firmware_info_status_t firmware_info_parse_build_stamp(
const char *date_text, const char *time_text, firmware_info_t *info);
firmware_info_status_t firmware_info_set_build_id(
firmware_info_t *info, const char *build_id);
firmware_info_status_t firmware_info_to_registers(
const firmware_info_t *info, uint16_t *registers, size_t count);
firmware_info_status_t firmware_info_to_le_bytes(
const firmware_info_t *info, uint8_t *payload, size_t size);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,18 @@
#ifndef FIRMWARE_INFO_PORT_H
#define FIRMWARE_INFO_PORT_H
#include "firmware_info.h"
/* Include-path selects one MCU project configuration named firmware_info_config.h. */
#include "firmware_info_config.h"
#ifndef FIRMWARE_VERSION_PATCH
#define FIRMWARE_VERSION_PATCH 0U
#endif
#ifndef FIRMWARE_BUILD_ID
#define FIRMWARE_BUILD_ID "LOCALDEV"
#endif
firmware_info_status_t firmware_info_port_describe(firmware_info_t *info);
#endif

View File

@@ -0,0 +1,11 @@
#ifndef FIRMWARE_INFO_CONFIG_H
#define FIRMWARE_INFO_CONFIG_H
#define FIRMWARE_VERSION_MAJOR 1U
#define FIRMWARE_VERSION_MINOR 0U
#define FIRMWARE_VERSION_PATCH 0U
#if defined(__has_include)
# if __has_include("firmware_build_id.h")
# include "firmware_build_id.h"
# endif
#endif
#endif

View File

@@ -0,0 +1,12 @@
#ifndef FIRMWARE_INFO_CONFIG_H
#define FIRMWARE_INFO_CONFIG_H
#define FIRMWARE_VERSION_MAJOR 1U
#define FIRMWARE_VERSION_MINOR 0U
#define FIRMWARE_VERSION_PATCH 0U
/* Optional: generated by tools/make_build_id.ps1. */
#if defined(__has_include)
# if __has_include("firmware_build_id.h")
# include "firmware_build_id.h"
# endif
#endif
#endif

View File

@@ -0,0 +1,11 @@
#ifndef FIRMWARE_INFO_CONFIG_H
#define FIRMWARE_INFO_CONFIG_H
#define FIRMWARE_VERSION_MAJOR 1U
#define FIRMWARE_VERSION_MINOR 0U
#define FIRMWARE_VERSION_PATCH 0U
#if defined(__has_include)
# if __has_include("firmware_build_id.h")
# include "firmware_build_id.h"
# endif
#endif
#endif

View File

@@ -0,0 +1,11 @@
#ifndef FIRMWARE_INFO_CONFIG_H
#define FIRMWARE_INFO_CONFIG_H
#define FIRMWARE_VERSION_MAJOR 1U
#define FIRMWARE_VERSION_MINOR 0U
#define FIRMWARE_VERSION_PATCH 0U
#if defined(__has_include)
# if __has_include("firmware_build_id.h")
# include "firmware_build_id.h"
# endif
#endif
#endif

View File

@@ -0,0 +1,121 @@
#include "firmware_info.h"
static const char months[12][4] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
static int is_digit(char value) { return (value >= '0') && (value <= '9'); }
static int two_digits(const char *text, uint8_t *result)
{
char high = text[0];
char low = text[1];
if (high == ' ') high = '0';
if (!is_digit(high) || !is_digit(low)) return 0;
*result = (uint8_t)(((uint8_t)(high - '0') * 10U) + (uint8_t)(low - '0'));
return 1;
}
static int four_digits(const char *text, uint16_t *result)
{
uint16_t value = 0U;
uint8_t i;
for (i = 0U; i < 4U; ++i) {
if (!is_digit(text[i])) return 0;
value = (uint16_t)(value * 10U + (uint16_t)(text[i] - '0'));
}
*result = value;
return 1;
}
firmware_info_status_t firmware_info_validate(const firmware_info_t *info)
{
uint8_t i;
if (info == NULL) return FIRMWARE_INFO_INVALID_ARGUMENT;
if ((info->major > 255U) || (info->minor > 255U) || (info->patch > 999U)
|| (info->year < 2000U) || (info->year > 2199U)
|| (info->month < 1U) || (info->month > 12U)
|| (info->day < 1U) || (info->day > 31U)
|| (info->hour > 23U) || (info->minute > 59U) || (info->second > 59U)) {
return FIRMWARE_INFO_OUT_OF_RANGE;
}
for (i = 0U; i < FIRMWARE_INFO_BUILD_ID_SIZE; ++i) {
if ((info->build_id[i] < 0x20) || (info->build_id[i] > 0x7e))
return FIRMWARE_INFO_OUT_OF_RANGE;
}
return FIRMWARE_INFO_OK;
}
firmware_info_status_t firmware_info_set_build_id(firmware_info_t *info,
const char *build_id)
{
uint8_t i;
if ((info == NULL) || (build_id == NULL)) return FIRMWARE_INFO_INVALID_ARGUMENT;
for (i = 0U; i < FIRMWARE_INFO_BUILD_ID_SIZE && build_id[i] != '\0'; ++i) {
if ((build_id[i] < 0x20) || (build_id[i] > 0x7e))
return FIRMWARE_INFO_OUT_OF_RANGE;
info->build_id[i] = build_id[i];
}
for (; i < FIRMWARE_INFO_BUILD_ID_SIZE; ++i) info->build_id[i] = '0';
info->build_id[FIRMWARE_INFO_BUILD_ID_SIZE] = '\0';
return FIRMWARE_INFO_OK;
}
firmware_info_status_t firmware_info_parse_build_stamp(
const char *date_text, const char *time_text, firmware_info_t *info)
{
uint8_t month;
uint8_t found = 0U;
if ((date_text == NULL) || (time_text == NULL) || (info == NULL))
return FIRMWARE_INFO_INVALID_ARGUMENT;
for (month = 0U; month < 12U; ++month) {
if ((date_text[0] == months[month][0]) && (date_text[1] == months[month][1])
&& (date_text[2] == months[month][2])) { found = 1U; break; }
}
if (!found || !two_digits(&date_text[4], &info->day)
|| !four_digits(&date_text[7], &info->year)
|| !two_digits(&time_text[0], &info->hour)
|| !two_digits(&time_text[3], &info->minute)
|| !two_digits(&time_text[6], &info->second)) return FIRMWARE_INFO_OUT_OF_RANGE;
info->month = (uint8_t)(month + 1U);
return FIRMWARE_INFO_OK;
}
firmware_info_status_t firmware_info_to_registers(
const firmware_info_t *info, uint16_t *registers, size_t count)
{
uint8_t i;
firmware_info_status_t status;
if ((registers == NULL) || (count < FIRMWARE_INFO_REGISTER_COUNT))
return FIRMWARE_INFO_INVALID_ARGUMENT;
status = firmware_info_validate(info);
if (status != FIRMWARE_INFO_OK) return status;
registers[0] = FIRMWARE_INFO_CONTRACT_VERSION;
registers[1] = info->major; registers[2] = info->minor; registers[3] = info->patch;
registers[4] = info->year;
registers[5] = (uint16_t)(((uint16_t)info->month << 8U) | info->day);
registers[6] = (uint16_t)(((uint16_t)info->hour << 8U) | info->minute);
registers[7] = info->second;
for (i = 0U; i < 4U; ++i)
registers[8U + i] = (uint16_t)(((uint16_t)(uint8_t)info->build_id[i * 2U] << 8U)
| (uint8_t)info->build_id[i * 2U + 1U]);
return FIRMWARE_INFO_OK;
}
firmware_info_status_t firmware_info_to_le_bytes(
const firmware_info_t *info, uint8_t *payload, size_t size)
{
uint16_t words[FIRMWARE_INFO_REGISTER_COUNT];
uint8_t i;
firmware_info_status_t status;
if ((payload == NULL) || (size < FIRMWARE_INFO_PAYLOAD_SIZE))
return FIRMWARE_INFO_INVALID_ARGUMENT;
status = firmware_info_to_registers(info, words, FIRMWARE_INFO_REGISTER_COUNT);
if (status != FIRMWARE_INFO_OK) return status;
for (i = 0U; i < FIRMWARE_INFO_REGISTER_COUNT; ++i) {
payload[i * 2U] = (uint8_t)words[i];
payload[i * 2U + 1U] = (uint8_t)(words[i] >> 8U);
}
return FIRMWARE_INFO_OK;
}

View File

@@ -0,0 +1,13 @@
#include "firmware_info_port.h"
firmware_info_status_t firmware_info_port_describe(firmware_info_t *info)
{
firmware_info_status_t status;
if (info == NULL) return FIRMWARE_INFO_INVALID_ARGUMENT;
info->major = (uint16_t)FIRMWARE_VERSION_MAJOR;
info->minor = (uint16_t)FIRMWARE_VERSION_MINOR;
info->patch = (uint16_t)FIRMWARE_VERSION_PATCH;
status = firmware_info_set_build_id(info, FIRMWARE_BUILD_ID);
if (status != FIRMWARE_INFO_OK) return status;
return firmware_info_parse_build_stamp(__DATE__, __TIME__, info);
}

View File

@@ -0,0 +1,20 @@
#include "firmware_info.h"
#include <assert.h>
#include <string.h>
int main(void)
{
firmware_info_t info = {1U, 2U, 3U, 0U, 0U, 0U, 0U, 0U, 0U, ""};
uint16_t words[FIRMWARE_INFO_REGISTER_COUNT];
uint8_t bytes[FIRMWARE_INFO_PAYLOAD_SIZE];
assert(firmware_info_set_build_id(&info, "abc") == FIRMWARE_INFO_OK);
assert(strcmp(info.build_id, "abc00000") == 0);
assert(firmware_info_parse_build_stamp("Aug 30 2026", "12:34:56", &info) == FIRMWARE_INFO_OK);
assert(firmware_info_to_registers(&info, words, FIRMWARE_INFO_REGISTER_COUNT) == FIRMWARE_INFO_OK);
assert(words[0] == 1U && words[1] == 1U && words[4] == 2026U);
assert(words[5] == 0x081eU && words[6] == 0x0c22U && words[7] == 56U);
assert(words[8] == 0x6162U && words[9] == 0x6330U);
assert(firmware_info_to_le_bytes(&info, bytes, sizeof(bytes)) == FIRMWARE_INFO_OK);
assert(bytes[0] == 1U && bytes[1] == 0U && bytes[16] == 0x62U && bytes[17] == 0x61U);
return 0;
}

View File

@@ -0,0 +1,18 @@
param(
[string]$Repository = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..\..')).Path,
[string]$Output = (Join-Path $PSScriptRoot '..\generated\firmware_build_id.h')
)
$id = 'NOGIT000'
$hash = (& git -C $Repository rev-parse --short=8 HEAD 2>$null)
if ($LASTEXITCODE -eq 0 -and $hash) {
& git -C $Repository diff --quiet HEAD 2>$null
$id = if ($LASTEXITCODE -eq 0) { $hash.Substring(0, 8) } else { $hash.Substring(0, 7) + '+' }
}
$directory = Split-Path -Parent $Output
New-Item -ItemType Directory -Force -Path $directory | Out-Null
$body = "#ifndef FIRMWARE_BUILD_ID_H`n#define FIRMWARE_BUILD_ID_H`n#define FIRMWARE_BUILD_ID `"$id`"`n#endif`n"
if (!(Test-Path -LiteralPath $Output) -or ((Get-Content -Raw $Output) -ne $body)) {
[IO.File]::WriteAllText($Output, $body, [Text.UTF8Encoding]::new($false))
}
Write-Host "[firmware-info] build-id: $id"

View 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
View 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
```

View 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;
}

Some files were not shown because too many files have changed in this diff Show More