Compare commits
69 Commits
v0.1.0
...
874d2e1dfc
| Author | SHA1 | Date | |
|---|---|---|---|
| 874d2e1dfc | |||
| 698632ebf3 | |||
| 20dbc2c721 | |||
| e95338d781 | |||
| daa3466a41 | |||
| c9cf797132 | |||
| b0f3731554 | |||
| 9ad0322c71 | |||
| 1511719bb9 | |||
| 0ce464499f | |||
| 27ef6fcfdf | |||
| 235d5d81a2 | |||
| 59f847e900 | |||
| 286e454464 | |||
| e691dfc337 | |||
| f5f15f6a04 | |||
| be066a57eb | |||
| b1f7b965f4 | |||
| 78d3f6690b | |||
| 6a82b309cc | |||
| 2316a5a26a | |||
| de5dc18a43 | |||
| d44d57a7aa | |||
| b972b6f33c | |||
| 454baeed98 | |||
| 01ffc0e496 | |||
| 3c4ac9963d | |||
| d9eb7dd9ad | |||
| f6787163dc | |||
| 5f8fc07f2a | |||
| c8565d868d | |||
| 1523640414 | |||
| fa6f615e81 | |||
| 392817432b | |||
| 20850b26df | |||
| e163d9ba55 | |||
| af82687e42 | |||
| e9ec078b38 | |||
| 43410c3f12 | |||
| 076c51c083 | |||
| e9c5ec90b8 | |||
| 00a0f944c4 | |||
| 9c0701ab9b | |||
| 9b636a9f40 | |||
| 19becd7b8c | |||
| 5504104cc5 | |||
| e6bea52db0 | |||
| 92fc2ade58 | |||
| 080b6900f5 | |||
| 68040a2803 | |||
| 7fe76b3c3b | |||
| 0f77a510e6 | |||
| 7910a07f2f | |||
| 2fd51a1d7e | |||
| de180750aa | |||
| 648457e587 | |||
| 5f3a9879d5 | |||
| 9ce32b56d4 | |||
| 05863db2a2 | |||
| 4b32cda46e | |||
| 217073982f | |||
| 94c991380b | |||
| 5a8724da6d | |||
| d507b84a76 | |||
| d3bb634fb1 | |||
| a1d2d05f42 | |||
| 1345766efb | |||
| 085eb3c8bd | |||
| 033c7ab9e8 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
build/
|
build/
|
||||||
cmake-build-*/
|
cmake-build-*/
|
||||||
*.o
|
*.o
|
||||||
|
*.obj
|
||||||
*.d
|
*.d
|
||||||
*.a
|
*.a
|
||||||
*.elf
|
*.elf
|
||||||
@@ -22,3 +23,4 @@ __pycache__/
|
|||||||
*.uvguix.*
|
*.uvguix.*
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
Desktop.ini
|
Desktop.ini
|
||||||
|
.codex-build/
|
||||||
|
|||||||
122
NEW_PROJECT.md
Normal file
122
NEW_PROJECT.md
Normal 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/`.
|
||||||
44
README.md
44
README.md
@@ -13,8 +13,15 @@
|
|||||||
templates/
|
templates/
|
||||||
c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты
|
c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты
|
||||||
python/ модули на чистом Python 3.9+, только stdlib
|
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
|
### C
|
||||||
@@ -24,21 +31,43 @@ templates/
|
|||||||
| [`c/st7789`](c/st7789) | TFT ST7789V по SPI, RGB565, текст без кадрового буфера | `stdint.h` | SPI-запись, линия DC, задержка |
|
| [`c/st7789`](c/st7789) | TFT ST7789V по SPI, RGB565, текст без кадрового буфера | `stdint.h` | SPI-запись, линия DC, задержка |
|
||||||
| [`c/keypad`](c/keypad) | шесть кнопок: антидребезг, автоповтор, удержание, очередь событий | `stdint.h` | чтение уровня кнопки, время в мс |
|
| [`c/keypad`](c/keypad) | шесть кнопок: антидребезг, автоповтор, удержание, очередь событий | `stdint.h` | чтение уровня кнопки, время в мс |
|
||||||
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `stdint.h` | заливка прямоугольника, вывод строки |
|
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `stdint.h` | заливка прямоугольника, вывод строки |
|
||||||
|
| [`c/led-indicator`](c/led-indicator) | неблокирующие режимы LED: работа, активность, предупреждение и ошибка | C99 | логическая запись канала, время выбранного таймера — **STM32F1/F4/G4 HAL-порт в комплекте** |
|
||||||
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
|
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
|
||||||
| [`c/can-sensor`](c/can-sensor) | передача 64-битных ROM датчиков парой CAN-кадров | `stdint.h` | отправка и приём CAN-кадра |
|
| [`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 — **порт STM32F1 в комплекте** |
|
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
|
||||||
| [`c/protocan-transport`](c/protocan-transport) | транспорт ProtoCAN: кадр, канал, CRC, общее адресное пространство, каталог GUI | `stdint.h` | запись в поток и запрос свободного места — **порт STM32F4 в комплекте** |
|
| [`c/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/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
|
||||||
|
|
||||||
| Модуль | Что делает | Зависимости |
|
| Модуль | Что делает | Зависимости |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
|
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
|
||||||
|
| [`python/protocan/trends.py`](python/protocan/trends.py) | общие настройки графиков, ограниченная история, ctypes-декодер GAS/raw CAN | stdlib, опционально SETProtocol DLL/SO |
|
||||||
|
|
||||||
Кодировщики `c/protocan-transport` и `python/protocan` дают побайтово
|
### Инструменты
|
||||||
|
|
||||||
|
| Инструмент | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| [`tools/firmware-publish`](tools/firmware-publish) | единый BAT и конфигурации для проверки и публикации `.hex` Keil / `.bin` CCS 12 в каталоге SETGUI |
|
||||||
|
|
||||||
|
Общие тренды для Android GUI и SETGUI: [формат JSON, C99-ядро и адаптеры](c/set-protocol/docs/GUI_TRENDS.md).
|
||||||
|
Общие масштабирование и маркеры: [C99, JNI и Python/Qt](c/set-protocol/docs/GUI_PLOT.md).
|
||||||
|
|
||||||
|
Кодировщики `c/set-protocol` и `python/protocan` дают побайтово
|
||||||
одинаковый результат — это зафиксировано эталонами в тестах на C.
|
одинаковый результат — это зафиксировано эталонами в тестах на C.
|
||||||
|
|
||||||
|
Интерактивная документация общего ядра: [`doc/setprotocol.html`](doc/setprotocol.html).
|
||||||
|
В ней отдельно описаны граница ядра, ABI, память, три wire format и состояние
|
||||||
|
портов Windows, Android, Linux и MCU.
|
||||||
|
|
||||||
## Как подключить к проекту
|
## Как подключить к проекту
|
||||||
|
|
||||||
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
|
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
|
||||||
@@ -69,17 +98,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 |
|
| `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` | python/protocan |
|
||||||
|
| `SETGUI`, Android GUI и новые устройства SET | set-protocol |
|
||||||
| `k1921vk028` | rtc-service |
|
| `k1921vk028` | rtc-service |
|
||||||
|
|
||||||
## Что сюда не попало и почему
|
## Что сюда не попало и почему
|
||||||
|
|
||||||
| Код | Почему не переносится | Что можно вытащить |
|
| Код | Почему не переносится | Что можно вытащить |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `SETCAN/Src/protocan.c` | 59 вызовов `HAL_*`, привязка к `CAN_HandleTypeDef`, `RTC_HandleTypeDef`, `NVIC_SystemReset()` | разбиение длинных посылок на кадры и проверка даты — чистая логика, нужны два интерфейса: «отправить кадр» и «часы». Раскладка идентификатора уже вынесена в `pcan_id` |
|
|
||||||
| `1921vk028/drivers/user/src/one_wire_user.c` | аппаратный 1-Wire `OWI_*` из `plib028`, мигание светодиодом прямо внутри опроса датчика | логика DS18B20 уже есть в `c/ds18b20` — переносится порт, а не алгоритм |
|
| `1921vk028/drivers/user/src/one_wire_user.c` | аппаратный 1-Wire `OWI_*` из `plib028`, мигание светодиодом прямо внутри опроса датчика | логика DS18B20 уже есть в `c/ds18b20` — переносится порт, а не алгоритм |
|
||||||
| `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
|
| `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
|
||||||
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |
|
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |
|
||||||
|
|||||||
80
RULES.md
Normal file
80
RULES.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Правила общего кроссплатформенного кода
|
||||||
|
|
||||||
|
Этот репозиторий — единственный источник общих алгоритмов для `GUI_Android`,
|
||||||
|
`SETGUI`, прошивок и будущих GUI. Копирование одной реализации между Kotlin,
|
||||||
|
Python, C# или другим языком запрещено.
|
||||||
|
|
||||||
|
## 1. Граница C-ядра и порта
|
||||||
|
|
||||||
|
В `c/set-protocol` на C99 обязательно размещаются:
|
||||||
|
|
||||||
|
- форматы кадров и идентификаторов, CRC/checksum, endian-преобразования;
|
||||||
|
- построение команд, разбор и проверка ответов;
|
||||||
|
- автоматы обмена, сегментация, повтор, таймаутные состояния без системных часов;
|
||||||
|
- общие вычисления, таблицы и каталоги, влияющие на поведение протокола;
|
||||||
|
- проверка образов прошивки и других бинарных форматов.
|
||||||
|
|
||||||
|
Порт на языке GUI содержит только:
|
||||||
|
|
||||||
|
- вызовы C через стабильный ABI (`ctypes`, JNI, P/Invoke, Swift FFI и т. п.);
|
||||||
|
- преобразование C-структур в модели языка без повторения алгоритма;
|
||||||
|
- работу с USB, COM, Bluetooth, SocketCAN и API операционной системы;
|
||||||
|
- жизненный цикл, потоки, разрешения, хранение настроек и UI;
|
||||||
|
- локализованный текст и чисто визуальные преобразования.
|
||||||
|
|
||||||
|
Порт не вычисляет CRC, не собирает wire-пакет и не разбирает его поля заново.
|
||||||
|
Если для функции C-ядро недоступно, приложение сообщает об ошибке сборки или
|
||||||
|
загрузки. Алгоритмический fallback на языке GUI запрещён: он снова создаёт две
|
||||||
|
версии протокола.
|
||||||
|
|
||||||
|
## 2. Разделение контроллеров
|
||||||
|
|
||||||
|
Профили контроллеров нельзя сливать по совпадению названия транспорта:
|
||||||
|
|
||||||
|
- **ПМ67 / TMS320F2812** — основной контроллер, собственные RS и CAN;
|
||||||
|
- **ПМ35 / TMS320F28335 periph** — отдельный контроллер и отдельный CAN для
|
||||||
|
настроечного терминала, а также собственный прямой RS232/485-протокол.
|
||||||
|
|
||||||
|
Выбор профиля выполняется в GUI, но выбранный профиль вызывает свой отдельный
|
||||||
|
модуль C-ядра. Наличие одной CAN-линии не даёт права удалить или подменить
|
||||||
|
другую.
|
||||||
|
|
||||||
|
## 3. Порядок изменения протокола
|
||||||
|
|
||||||
|
1. Добавить или изменить публичный заголовок и реализацию в
|
||||||
|
`c/set-protocol/include` и `c/set-protocol/src`.
|
||||||
|
2. Зафиксировать эталонные байты и ошибочные случаи в C-тесте.
|
||||||
|
3. При необходимости расширить `pcan_abi.h`, сохраняя бинарную совместимость.
|
||||||
|
4. Добавить тонкие порты в `ports/<platform>` и `python/`; в них не должно быть
|
||||||
|
второго кодека.
|
||||||
|
5. Одними и теми же векторами проверить C, Python и Android/JVM.
|
||||||
|
6. Собрать SETGUI и Android с одним commit submodule `templates`.
|
||||||
|
|
||||||
|
Изменение только в одном GUI считается незавершённым. Сначала меняется
|
||||||
|
`templates`, затем оба потребителя обновляют ссылку submodule на проверенный
|
||||||
|
commit.
|
||||||
|
|
||||||
|
## 4. Требования к C-ядру
|
||||||
|
|
||||||
|
- C99, без зависимости от GUI и конкретной ОС.
|
||||||
|
- Буферы и их размеры передаются явно; владение памятью остаётся у вызывающего.
|
||||||
|
- Для MCU основная логика не требует heap, исключений или файловой системы.
|
||||||
|
- Endian и размеры целых задаются через `stdint.h`, структуры wire-формата не
|
||||||
|
передаются через ABI без явного стабильного представления.
|
||||||
|
- Экспорт shared library идёт через `PCAN_ABI_API`; существующие символы не
|
||||||
|
меняют смысл и сигнатуру.
|
||||||
|
- Ошибки возвращаются детерминированным кодом и тестируются наряду с успехом.
|
||||||
|
|
||||||
|
## 5. Проверка на ревью
|
||||||
|
|
||||||
|
Изменение нельзя принимать, если ответ «да» хотя бы на один вопрос:
|
||||||
|
|
||||||
|
- появился одинаковый CRC/parser/builder в двух языках;
|
||||||
|
- UI знает byte offset, endian или служебный байт wire-протокола;
|
||||||
|
- Python и Kotlin содержат одинаковую таблицу команд, влияющую на обмен;
|
||||||
|
- добавлен тихий fallback, поведение которого отличается от C;
|
||||||
|
- обновлён один GUI без обновления и теста `templates`;
|
||||||
|
- ПМ67 и ПМ35 сведены к одному соединению или одному состоянию контроллера.
|
||||||
|
|
||||||
|
Текущее состояние и очередь переноса перечислены в
|
||||||
|
[`doc/CROSS_PLATFORM_AUDIT.md`](doc/CROSS_PLATFORM_AUDIT.md).
|
||||||
22
c/can-sensor/CMakeLists.txt
Normal file
22
c/can-sensor/CMakeLists.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(can_sensor C)
|
||||||
|
|
||||||
|
set(CMAKE_C_STANDARD 99)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
add_library(can_sensor STATIC can_sensor.c)
|
||||||
|
target_include_directories(can_sensor PUBLIC .)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(can_sensor PRIVATE /W4)
|
||||||
|
else()
|
||||||
|
target_compile_options(can_sensor PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
option(CAN_SENSOR_BUILD_TESTS "Собирать тесты транспорта CAN sensor" ON)
|
||||||
|
if(CAN_SENSOR_BUILD_TESTS)
|
||||||
|
enable_testing()
|
||||||
|
add_executable(test_can_sensor tests/test_can_sensor.c)
|
||||||
|
target_link_libraries(test_can_sensor PRIVATE can_sensor)
|
||||||
|
add_test(NAME can_sensor COMMAND test_can_sensor)
|
||||||
|
endif()
|
||||||
@@ -1,27 +1,28 @@
|
|||||||
# can-sensor
|
# can-sensor
|
||||||
|
|
||||||
Передача 64-битных идентификаторов датчиков (ROM 1-Wire) по шине CAN.
|
Однокадровый транспорт SETCAN SETTINGS для привязки 64-битного ROM 1-Wire к
|
||||||
|
локации «номер сборки + позиция» по шине CAN.
|
||||||
|
|
||||||
Ядро на C99: не включает заголовки периферии, не обращается к регистрам,
|
Ядро на C99: не включает заголовки периферии, не обращается к регистрам,
|
||||||
не пользуется прерываниями. Обмен идёт через таблицу `CanSensor_Io`,
|
не пользуется прерываниями. Обмен идёт через таблицу `CanSensor_Io`,
|
||||||
которую заполняет порт платы.
|
которую заполняет порт платы.
|
||||||
|
|
||||||
Сообщение состоит из двух классических CAN-кадров — 64-битный идентификатор
|
Сообщение помещается в один Extended CAN-кадр: локация находится в Body
|
||||||
и преамбула вместе не помещаются в восемь байтов поля данных:
|
29-битного ID, а ROM — в восьми байтах payload:
|
||||||
|
|
||||||
```
|
```
|
||||||
кадр 1 — преамбула, DLC = 2 | команда | позиция |
|
запрос 0x17FEZZYY: DLC=0 — GET, DLC=8 — WRITE/REPLACE/CLEAR
|
||||||
кадр 2 — идентификатор, DLC = 8| ROM 8 байт |
|
ответ 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 без преамбулы и кадр с неверной
|
`ZZ` — номер сборки, `YY` — позиция. Нулевой ROM с DLC=8 очищает локацию.
|
||||||
длиной отбрасываются и учитываются в `dropped_frames`.
|
|
||||||
|
|
||||||
## Что нужно от платформы
|
## Что нужно от платформы
|
||||||
|
|
||||||
@@ -38,15 +39,45 @@ uint8_t receive(void *ctx, CanSensor_Frame *frame); /* 1 — кадр п
|
|||||||
CanSensor link;
|
CanSensor link;
|
||||||
CanSensor_Io io = { .send = bxcan_send, .receive = bxcan_receive, .context = &board };
|
CanSensor_Io io = { .send = bxcan_send, .receive = bxcan_receive, .context = &board };
|
||||||
CanSensor_Config config;
|
CanSensor_Config config;
|
||||||
CanSensor_ConfigDefault(&config); /* tx_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_Init(&link, &io, &config);
|
||||||
CanSensor_SendId(&link, position, rom);
|
CanSensor_SendId(&link, position, assembly_serial, rom);
|
||||||
|
CanSensor_Task(&link, now_ms);
|
||||||
|
|
||||||
CanSensor_Message message;
|
CanSensor_Message message;
|
||||||
if (CanSensor_Poll(&link, &message)) { /* принят идентификатор */ }
|
if (CanSensor_Poll(&link, &message)) { /* принят идентификатор */ }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Период можно менять во время работы через `CanSensor_SetPulsePeriod()`. Значение
|
||||||
|
`0` именно выключает heartbeat: кадры PULSE больше не ставятся в очередь. При
|
||||||
|
повторном включении первый кадр отправляется через полный заданный период.
|
||||||
|
|
||||||
|
## Порт STM32F1
|
||||||
|
|
||||||
|
Порт не использует 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):
|
`KONOR_ds18b20` — bxCAN на STM32F103C8T6. Естественная пара — [`ds18b20`](../ds18b20):
|
||||||
|
|||||||
@@ -1,72 +1,35 @@
|
|||||||
/**
|
/**
|
||||||
* @file can_sensor.c
|
* @file can_sensor.c
|
||||||
* @brief Сборка, передача и разбор сообщений «преамбула + идентификатор».
|
* @brief Однокадровый транспорт SETCAN SETTINGS для локаций DS18B20.
|
||||||
*
|
|
||||||
* Реализация не хранит очередей и не пользуется временем: состояние приёма
|
|
||||||
* ограничено признаком принятой преамбулы, поэтому библиотека одинаково
|
|
||||||
* работает и в главном цикле, и в обработчике прерывания порта.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "can_sensor.h"
|
#include "can_sensor.h"
|
||||||
|
|
||||||
/**
|
/** Возвращает 1, если все восемь байтов ROM равны нулю. */
|
||||||
* @brief Возвращает идентификатор кадра данных для заданных настроек.
|
static uint8_t can_sensor_rom_is_zero(const uint8_t *rom)
|
||||||
*
|
|
||||||
* @param config Настройки узла.
|
|
||||||
* @return Идентификатор кадра ROM при передаче.
|
|
||||||
*/
|
|
||||||
static uint32_t can_sensor_tx_data_id(const CanSensor_Config *config)
|
|
||||||
{
|
{
|
||||||
if (config->tx_data_id != 0U) {
|
uint8_t index;
|
||||||
return config->tx_data_id;
|
|
||||||
|
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)
|
static uint8_t can_sensor_send_frame(CanSensor *link, const CanSensor_Frame *frame)
|
||||||
{
|
{
|
||||||
uint8_t attempt;
|
uint8_t attempt;
|
||||||
|
|
||||||
for (attempt = 0U; attempt < link->config.retries; attempt++) {
|
for (attempt = 0U; attempt < link->config.retries; attempt++) {
|
||||||
if (link->io.send(link->io.context, frame) != 0U) {
|
if (link->io.send(link->io.context, frame) != 0U) {
|
||||||
|
link->sent_messages++;
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
link->send_errors++;
|
||||||
return 0U;
|
return 0U;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,22 +39,18 @@ void CanSensor_ConfigDefault(CanSensor_Config *config)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
config->tx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
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_RX_ID;
|
||||||
config->rx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
config->pulse_period_ms = 0U;
|
||||||
config->rx_data_id = CAN_SENSOR_DEFAULT_TX_ID + CAN_SENSOR_DATA_ID_OFFSET;
|
config->extended = 1U;
|
||||||
config->extended = 0U;
|
|
||||||
config->retries = CAN_SENSOR_DEFAULT_RETRIES;
|
config->retries = CAN_SENSOR_DEFAULT_RETRIES;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
||||||
const CanSensor_Config *config)
|
const CanSensor_Config *config)
|
||||||
{
|
{
|
||||||
uint8_t index;
|
|
||||||
|
|
||||||
if ((link == 0) || (io == 0) || (io->send == 0)) {
|
if ((link == 0) || (io == 0) || (io->send == 0)) {
|
||||||
return 0U;
|
return 0U;
|
||||||
}
|
}
|
||||||
|
|
||||||
link->io = *io;
|
link->io = *io;
|
||||||
if (config != 0) {
|
if (config != 0) {
|
||||||
link->config = *config;
|
link->config = *config;
|
||||||
@@ -101,131 +60,133 @@ uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
|||||||
if (link->config.tx_id == 0U) {
|
if (link->config.tx_id == 0U) {
|
||||||
link->config.tx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
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) {
|
if (link->config.retries == 0U) {
|
||||||
link->config.retries = CAN_SENSOR_DEFAULT_RETRIES;
|
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->sent_messages = 0U;
|
||||||
link->send_errors = 0U;
|
link->send_errors = 0U;
|
||||||
link->received_messages = 0U;
|
link->received_messages = 0U;
|
||||||
link->dropped_frames = 0U;
|
link->dropped_frames = 0U;
|
||||||
|
link->pulse_last_ms = 0U;
|
||||||
|
link->pulse_counter = 0U;
|
||||||
|
link->pulse_armed = 0U;
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t CanSensor_BuildPreamble(const CanSensor_Config *config, CanSensor_Frame *frame,
|
uint8_t CanSensor_BuildFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||||
uint8_t command, uint16_t position)
|
uint8_t response, uint8_t position,
|
||||||
|
uint8_t assembly_serial, const uint8_t *data,
|
||||||
|
uint8_t length)
|
||||||
{
|
{
|
||||||
uint8_t index;
|
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;
|
return 0U;
|
||||||
}
|
}
|
||||||
frame->id = config->tx_id;
|
base = (response != 0U) ? config->tx_id : config->rx_id;
|
||||||
frame->extended = config->extended;
|
frame->id = (base & CAN_SENSOR_HEADER_MASK)
|
||||||
frame->length = CAN_SENSOR_PREAMBLE_SIZE;
|
| ((uint32_t)assembly_serial << 8U) | (uint32_t)position;
|
||||||
frame->data[0] = command;
|
frame->extended = 1U;
|
||||||
/* Позиция занимает два байта в порядке от старшего к младшему. */
|
frame->length = length;
|
||||||
frame->data[1] = (uint8_t)((position >> 8U) & 0xFFU);
|
for (index = 0U; index < CAN_SENSOR_MAX_DATA; index++) {
|
||||||
frame->data[2] = (uint8_t)(position & 0xFFU);
|
frame->data[index] = (index < length) ? data[index] : 0U;
|
||||||
for (index = CAN_SENSOR_PREAMBLE_SIZE; index < CAN_SENSOR_MAX_DATA; index++) {
|
|
||||||
frame->data[index] = 0U;
|
|
||||||
}
|
}
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t CanSensor_BuildIdFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
uint8_t CanSensor_SendId(CanSensor *link, uint8_t position,
|
||||||
const uint8_t *id)
|
uint8_t assembly_serial, 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)
|
|
||||||
{
|
{
|
||||||
CanSensor_Frame frame;
|
CanSensor_Frame frame;
|
||||||
|
|
||||||
if ((link == 0) || (link->io.send == 0)) {
|
if ((link == 0) || (id == 0) || (link->io.send == 0)) {
|
||||||
return 0U;
|
return 0U;
|
||||||
}
|
}
|
||||||
(void)CanSensor_BuildPreamble(&link->config, &frame, command, position);
|
if (CanSensor_BuildFrame(&link->config, &frame, 1U, position,
|
||||||
if (can_sensor_send_frame(link, &frame) == 0U) {
|
assembly_serial, id, CAN_SENSOR_ID_SIZE) == 0U) {
|
||||||
link->send_errors++;
|
link->send_errors++;
|
||||||
return 0U;
|
return 0U;
|
||||||
}
|
}
|
||||||
(void)CanSensor_BuildIdFrame(&link->config, &frame, id);
|
return can_sensor_send_frame(link, &frame);
|
||||||
if (can_sensor_send_frame(link, &frame) == 0U) {
|
|
||||||
/* Одиночная преамбула приёмником отбрасывается, позиция не изменится. */
|
|
||||||
link->send_errors++;
|
|
||||||
return 0U;
|
|
||||||
}
|
|
||||||
link->sent_messages++;
|
|
||||||
return 1U;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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,
|
uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame,
|
||||||
CanSensor_Message *out)
|
CanSensor_Message *out)
|
||||||
{
|
{
|
||||||
|
CanSensor_Message message;
|
||||||
|
uint32_t header;
|
||||||
uint8_t index;
|
uint8_t index;
|
||||||
|
|
||||||
if ((link == 0) || (frame == 0)) {
|
if ((link == 0) || (frame == 0)) {
|
||||||
return 0U;
|
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))
|
message.assembly_serial = (uint8_t)((frame->id >> 8U) & 0xFFU);
|
||||||
&& (frame->extended == link->config.extended)) {
|
message.position = (uint8_t)(frame->id & 0xFFU);
|
||||||
if (frame->length < CAN_SENSOR_PREAMBLE_SIZE) {
|
message.result = CAN_SENSOR_RESULT_OK;
|
||||||
link->dropped_frames++;
|
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||||
return 0U;
|
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];
|
} else if (frame->length == CAN_SENSOR_ID_SIZE) {
|
||||||
link->rx.position = (uint16_t)(((uint16_t)frame->data[1] << 8U)
|
message.operation = CAN_SENSOR_OPERATION_RESPONSE;
|
||||||
| frame->data[2]);
|
} else if (frame->length == 1U) {
|
||||||
link->rx_preamble = 1U;
|
message.operation = CAN_SENSOR_OPERATION_ERROR;
|
||||||
return 0U;
|
message.result = frame->data[0];
|
||||||
}
|
} else {
|
||||||
|
|
||||||
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;
|
|
||||||
link->dropped_frames++;
|
link->dropped_frames++;
|
||||||
return 0U;
|
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++;
|
link->received_messages++;
|
||||||
if (out != 0) {
|
if (out != 0) {
|
||||||
*out = link->rx;
|
*out = message;
|
||||||
}
|
}
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
@@ -244,3 +205,51 @@ uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out)
|
|||||||
}
|
}
|
||||||
return 0U;
|
return 0U;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CanSensor_SetPulsePeriod(CanSensor *link, uint32_t period_ms)
|
||||||
|
{
|
||||||
|
if (link == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (link->config.pulse_period_ms != period_ms) {
|
||||||
|
link->config.pulse_period_ms = period_ms;
|
||||||
|
/* После включения или смены периода отсчитываем полный новый период. */
|
||||||
|
link->pulse_armed = 0U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t CanSensor_PulsePeriod(const CanSensor *link)
|
||||||
|
{
|
||||||
|
return (link != 0) ? link->config.pulse_period_ms : 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CanSensor_Task(CanSensor *link, uint32_t now_ms)
|
||||||
|
{
|
||||||
|
CanSensor_Frame frame;
|
||||||
|
const uint32_t period_ms = (link != 0) ? link->config.pulse_period_ms : 0U;
|
||||||
|
|
||||||
|
if ((link == 0) || (link->io.send == 0) || (period_ms == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (link->pulse_armed == 0U) {
|
||||||
|
link->pulse_last_ms = now_ms;
|
||||||
|
link->pulse_armed = 1U;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((now_ms - link->pulse_last_ms) < period_ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.id = (link->config.tx_id & CAN_SENSOR_ADDRESS_MASK)
|
||||||
|
| ((uint32_t)CAN_SENSOR_MSGTYPE_PULSE << 16U);
|
||||||
|
frame.extended = 1U;
|
||||||
|
frame.length = 1U;
|
||||||
|
link->pulse_counter++;
|
||||||
|
frame.data[0] = link->pulse_counter;
|
||||||
|
if (can_sensor_send_frame(link, &frame) != 0U) {
|
||||||
|
link->pulse_last_ms = now_ms;
|
||||||
|
} else {
|
||||||
|
/* Счётчик меняется только для реально поставленных в очередь кадров. */
|
||||||
|
link->pulse_counter--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,30 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* @file can_sensor.h
|
* @file can_sensor.h
|
||||||
* @brief Портируемая передача идентификаторов датчиков по шине CAN.
|
* @brief Портируемый транспорт SETTINGS для привязки DS18B20 к локации.
|
||||||
*
|
*
|
||||||
* Библиотека не привязана к микроконтроллеру: она не включает заголовки
|
* Один Extended CAN-кадр содержит полную операцию. Поля локации находятся в
|
||||||
* периферии, не обращается к регистрам и не пользуется прерываниями. Обмен
|
* Body идентификатора SETCAN, а восьмибайтовый ROM — в payload:
|
||||||
* идёт через таблицу обратных вызовов CanSensor_Io, которую заполняет порт
|
|
||||||
* платы (для этой сборки — @c src/can.c поверх bxCAN STM32F103C8T6).
|
|
||||||
*
|
|
||||||
* Сообщение состоит из двух кадров классического CAN, потому что 64-битный
|
|
||||||
* идентификатор датчика и преамбула вместе не помещаются в восемь байтов
|
|
||||||
* поля данных:
|
|
||||||
*
|
*
|
||||||
* @code
|
* @code
|
||||||
* кадр 1 — преамбула, DLC = 2 +---------+----------+
|
* Priority=1, DeviceType=0x7, DeviceID=0xF, MsgType=0xE (SETTINGS)
|
||||||
* | команда | позиция |
|
* Body[15:8] = номер сборки, Body[7:0] = позиция
|
||||||
* +---------+----------+
|
|
||||||
* кадр 2 — идентификатор, DLC=8 +----------------------------+
|
|
||||||
* | ROM датчика, байты 0..7 |
|
|
||||||
* +----------------------------+
|
|
||||||
* @endcode
|
* @endcode
|
||||||
*
|
*
|
||||||
* Преамбула несёт команду записи датчика в позицию: первый байт — код
|
* Запрос ПМ использует Route=0 (0x17FEZZYY), ответ прибора — Route=1
|
||||||
* команды (@ref CAN_SENSOR_CMD_WRITE_POSITION), второй — номер позиции в
|
* (0x1FFEZZYY). DLC=0 означает GET, DLC=8 с ненулевым ROM — SET/REPLACE,
|
||||||
* таблице узла-приёмника. Кадр идентификатора передаётся сразу за преамбулой
|
* DLC=8 с нулевым ROM — CLEAR. Успешный ответ возвращает текущий ROM с DLC=8,
|
||||||
* и без неё считается недействительным, поэтому приёмник не запишет ROM в
|
* ошибка — однобайтовый код с DLC=1.
|
||||||
* позицию, которая ему не была назначена.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef CAN_SENSOR_H
|
#ifndef CAN_SENSOR_H
|
||||||
@@ -32,186 +21,126 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
/** Длина идентификатора датчика (ROM 1-Wire) в байтах. */
|
/** Длина ROM 1-Wire и максимального payload classic CAN. */
|
||||||
#define CAN_SENSOR_ID_SIZE 8U
|
#define CAN_SENSOR_ID_SIZE 8U
|
||||||
|
#define CAN_SENSOR_MAX_DATA 8U
|
||||||
|
|
||||||
/** Длина преамбулы: код команды (1 байт) и номер позиции (2 байта, u16 BE). */
|
/** Назначенный проекту тип сообщения SETCAN SETTINGS. */
|
||||||
#define CAN_SENSOR_PREAMBLE_SIZE 3U
|
#define CAN_SENSOR_MSGTYPE_SETTINGS 0xEU
|
||||||
|
#define CAN_SENSOR_MSGTYPE_PULSE 0xFU
|
||||||
|
|
||||||
/** Предельная длина поля данных классического кадра CAN. */
|
/** Маска полей SETCAN от Priority до MsgType; Body под маской не находится. */
|
||||||
#define CAN_SENSOR_MAX_DATA 8U
|
#define CAN_SENSOR_HEADER_MASK 0x1FFF0000UL
|
||||||
|
#define CAN_SENSOR_ADDRESS_MASK 0x1FF00000UL
|
||||||
|
#define CAN_SENSOR_BODY_MASK 0x0000FFFFUL
|
||||||
|
|
||||||
/** Код команды «записать идентификатор датчика в позицию». */
|
/** Базовые ID SETTINGS для выбранных DeviceType=0x7 и DeviceID=0xF. */
|
||||||
#define CAN_SENSOR_CMD_WRITE_POSITION 0xA1U
|
#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
|
||||||
|
|
||||||
/** Смещение идентификатора кадра данных относительно кадра преамбулы. */
|
/** Операции, полученные из направления, DLC и payload кадра. */
|
||||||
#define CAN_SENSOR_DATA_ID_OFFSET 1U
|
#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
|
||||||
|
|
||||||
/** Число попыток передачи одного кадра по умолчанию. */
|
/** Коды результата SETTINGS, передаваемые ответом с DLC=1. */
|
||||||
#define CAN_SENSOR_DEFAULT_RETRIES 3U
|
#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 бит). */
|
/** Кадр CAN в форме, не зависящей от периферийного контроллера. */
|
||||||
#define CAN_SENSOR_DEFAULT_TX_ID 0x200U
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Кадр шины CAN в форме, не зависящей от контроллера.
|
|
||||||
*/
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t id; /**< Идентификатор кадра, 11 или 29 бит. */
|
uint32_t id; /**< Расширенный идентификатор, 29 бит. */
|
||||||
uint8_t extended; /**< 1 — расширенный идентификатор. */
|
uint8_t extended; /**< Для SETTINGS всегда 1. */
|
||||||
uint8_t length; /**< Число значащих байтов поля данных. */
|
uint8_t length; /**< DLC, 0..8. */
|
||||||
uint8_t data[CAN_SENSOR_MAX_DATA]; /**< Поле данных кадра. */
|
uint8_t data[CAN_SENSOR_MAX_DATA]; /**< Payload. */
|
||||||
} CanSensor_Frame;
|
} CanSensor_Frame;
|
||||||
|
|
||||||
/**
|
/** Разобранная команда или ответ SETTINGS. */
|
||||||
* @brief Принятое или собранное сообщение: преамбула и идентификатор датчика.
|
|
||||||
*/
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t command; /**< Код команды из преамбулы. */
|
uint8_t operation; /**< CAN_SENSOR_OPERATION_*. */
|
||||||
uint16_t position; /**< Позиция записи из преамбулы. */
|
uint8_t request; /**< 1 — запрос Route=0, 0 — ответ Route=1. */
|
||||||
uint8_t id[CAN_SENSOR_ID_SIZE]; /**< Идентификатор датчика. */
|
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;
|
} CanSensor_Message;
|
||||||
|
|
||||||
/**
|
/** Доступ переносимой части к CAN-контроллеру. */
|
||||||
* @brief Доступ библиотеки к контроллеру CAN.
|
|
||||||
*
|
|
||||||
* Обязателен только @c send; при нулевом @c receive функция CanSensor_Poll()
|
|
||||||
* ничего не делает и узел работает только на передачу.
|
|
||||||
*/
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
/** Ставит кадр в очередь передачи; 1 — кадр принят контроллером. */
|
|
||||||
uint8_t (*send)(void *context, const CanSensor_Frame *frame);
|
uint8_t (*send)(void *context, const CanSensor_Frame *frame);
|
||||||
/** Забирает принятый кадр; 1 — кадр получен, 0 — очередь пуста. */
|
|
||||||
uint8_t (*receive)(void *context, CanSensor_Frame *frame);
|
uint8_t (*receive)(void *context, CanSensor_Frame *frame);
|
||||||
void *context; /**< Контекст порта, передаётся вызовам без изменений. */
|
void *context;
|
||||||
} CanSensor_Io;
|
} CanSensor_Io;
|
||||||
|
|
||||||
/**
|
/** Базовые идентификаторы SETTINGS и режим транспорта. */
|
||||||
* @brief Идентификаторы кадров и режим передачи.
|
|
||||||
*
|
|
||||||
* Нулевые поля заменяются значениями по умолчанию вызовом
|
|
||||||
* CanSensor_ConfigDefault() или самой CanSensor_Init().
|
|
||||||
*/
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t tx_id; /**< Идентификатор кадра преамбулы при передаче. */
|
uint32_t tx_id; /**< База ответа 0x1FFE0000, Body должен быть нулевым. */
|
||||||
uint32_t tx_data_id; /**< Идентификатор кадра ROM; 0 — @c tx_id + 1. */
|
uint32_t rx_id; /**< База запроса 0x17FE0000, Body должен быть нулевым. */
|
||||||
uint32_t rx_id; /**< Ожидаемая преамбула при приёме; 0 — @c tx_id. */
|
uint8_t extended; /**< 1 — Extended CAN ID. */
|
||||||
uint32_t rx_data_id; /**< Ожидаемый кадр ROM; 0 — @c rx_id + 1. */
|
uint8_t retries; /**< Число попыток передачи. */
|
||||||
uint8_t extended; /**< 1 — расширенные идентификаторы 29 бит. */
|
uint32_t pulse_period_ms; /**< Период PULSE; 0 полностью отключает PULSE. */
|
||||||
uint8_t retries; /**< Попыток передачи кадра; 0 — значение по умолчанию. */
|
|
||||||
} CanSensor_Config;
|
} CanSensor_Config;
|
||||||
|
|
||||||
/**
|
/** Состояние транспорта и диагностические счётчики. */
|
||||||
* @brief Состояние узла: настройки, счётчики и сборка принимаемого сообщения.
|
|
||||||
*/
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
CanSensor_Io io; /**< Обратные вызовы порта. */
|
CanSensor_Io io;
|
||||||
CanSensor_Config config; /**< Идентификаторы кадров и режим передачи. */
|
CanSensor_Config config;
|
||||||
CanSensor_Message rx; /**< Собираемое сообщение приёма. */
|
uint32_t sent_messages;
|
||||||
uint8_t rx_preamble; /**< 1 — преамбула принята, ожидается кадр ROM. */
|
uint32_t send_errors;
|
||||||
uint32_t sent_messages; /**< Полностью переданные сообщения. */
|
uint32_t received_messages;
|
||||||
uint32_t send_errors; /**< Сообщения, не ушедшие в шину. */
|
uint32_t dropped_frames;
|
||||||
uint32_t received_messages; /**< Полностью принятые сообщения. */
|
uint32_t pulse_last_ms;
|
||||||
uint32_t dropped_frames; /**< Кадры без преамбулы или с неверной длиной. */
|
uint8_t pulse_counter;
|
||||||
|
uint8_t pulse_armed;
|
||||||
} CanSensor;
|
} CanSensor;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Заполняет настройки значениями по умолчанию.
|
|
||||||
*
|
|
||||||
* Идентификатор преамбулы — @ref CAN_SENSOR_DEFAULT_TX_ID, кадр ROM идёт
|
|
||||||
* следующим идентификатором, приём настроен на те же значения.
|
|
||||||
*
|
|
||||||
* @param config Настройки, принадлежащие вызывающему коду.
|
|
||||||
*/
|
|
||||||
void CanSensor_ConfigDefault(CanSensor_Config *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,
|
uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
||||||
const CanSensor_Config *config);
|
const CanSensor_Config *config);
|
||||||
|
|
||||||
/**
|
/** Собирает SETTINGS-кадр на базе tx_id или rx_id. */
|
||||||
* @brief Собирает кадр преамбулы.
|
uint8_t CanSensor_BuildFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||||
*
|
uint8_t response, uint8_t position,
|
||||||
* @param config Настройки узла.
|
uint8_t assembly_serial, const uint8_t *data,
|
||||||
* @param frame Кадр приёмника.
|
uint8_t length);
|
||||||
* @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);
|
|
||||||
|
|
||||||
/**
|
/** Передаёт успешный ответ с текущим ROM (DLC=8). */
|
||||||
* @brief Собирает кадр идентификатора датчика.
|
uint8_t CanSensor_SendId(CanSensor *link, uint8_t position,
|
||||||
*
|
uint8_t assembly_serial, const uint8_t *id);
|
||||||
* @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);
|
|
||||||
|
|
||||||
/**
|
/** Передаёт ошибку SETTINGS (DLC=1). */
|
||||||
* @brief Передаёт преамбулу и идентификатор датчика.
|
uint8_t CanSensor_SendError(CanSensor *link, uint8_t position,
|
||||||
*
|
uint8_t assembly_serial, uint8_t result);
|
||||||
* Кадры уходят подряд; при отказе контроллера передача повторяется
|
|
||||||
* @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);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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,
|
uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame,
|
||||||
CanSensor_Message *out);
|
CanSensor_Message *out);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Забирает кадры у порта и возвращает первое собранное сообщение.
|
|
||||||
*
|
|
||||||
* @param link Состояние узла.
|
|
||||||
* @param out Приёмник сообщения либо 0.
|
|
||||||
* @return 1, если сообщение собрано, иначе 0.
|
|
||||||
*/
|
|
||||||
uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out);
|
uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Меняет период PULSE во время работы.
|
||||||
|
*
|
||||||
|
* @param period_ms Период в миллисекундах; 0 выключает PULSE.
|
||||||
|
*/
|
||||||
|
void CanSensor_SetPulsePeriod(CanSensor *link, uint32_t period_ms);
|
||||||
|
|
||||||
|
/** Возвращает действующий период PULSE; 0 означает «выключено». */
|
||||||
|
uint32_t CanSensor_PulsePeriod(const CanSensor *link);
|
||||||
|
|
||||||
|
/** Вызывает планировщик PULSE из главного цикла. */
|
||||||
|
void CanSensor_Task(CanSensor *link, uint32_t now_ms);
|
||||||
|
|
||||||
#endif /* CAN_SENSOR_H */
|
#endif /* CAN_SENSOR_H */
|
||||||
|
|||||||
330
c/can-sensor/ports/stm32f1/can_sensor_stm32f1.c
Normal file
330
c/can-sensor/ports/stm32f1/can_sensor_stm32f1.c
Normal 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);
|
||||||
|
}
|
||||||
77
c/can-sensor/ports/stm32f1/can_sensor_stm32f1.h
Normal file
77
c/can-sensor/ports/stm32f1/can_sensor_stm32f1.h
Normal 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 */
|
||||||
@@ -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 */
|
||||||
79
c/can-sensor/tests/test_can_sensor.c
Normal file
79
c/can-sensor/tests/test_can_sensor.c
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#include "can_sensor.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
CanSensor_Frame last_frame;
|
||||||
|
uint32_t send_count;
|
||||||
|
uint8_t accept;
|
||||||
|
} FakeCan;
|
||||||
|
|
||||||
|
static uint8_t fake_send(void *context, const CanSensor_Frame *frame)
|
||||||
|
{
|
||||||
|
FakeCan *fake = (FakeCan *)context;
|
||||||
|
|
||||||
|
fake->send_count++;
|
||||||
|
fake->last_frame = *frame;
|
||||||
|
return fake->accept;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CHECK(condition) \
|
||||||
|
do { \
|
||||||
|
if (!(condition)) { \
|
||||||
|
fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \
|
||||||
|
#condition); \
|
||||||
|
return 1; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
CanSensor link;
|
||||||
|
CanSensor_Config config;
|
||||||
|
CanSensor_Io io;
|
||||||
|
FakeCan fake = {0};
|
||||||
|
|
||||||
|
fake.accept = 1U;
|
||||||
|
io.send = fake_send;
|
||||||
|
io.receive = 0;
|
||||||
|
io.context = &fake;
|
||||||
|
CanSensor_ConfigDefault(&config);
|
||||||
|
|
||||||
|
CHECK(config.pulse_period_ms == 0U);
|
||||||
|
CHECK(CanSensor_Init(&link, &io, &config) != 0U);
|
||||||
|
|
||||||
|
/* Нулевой период означает выключенный PULSE при любых метках времени. */
|
||||||
|
CanSensor_Task(&link, 0U);
|
||||||
|
CanSensor_Task(&link, 0xFFFFFFFFUL);
|
||||||
|
CHECK(fake.send_count == 0U);
|
||||||
|
|
||||||
|
CanSensor_SetPulsePeriod(&link, 1000U);
|
||||||
|
CHECK(CanSensor_PulsePeriod(&link) == 1000U);
|
||||||
|
CanSensor_Task(&link, 500U);
|
||||||
|
CanSensor_Task(&link, 1499U);
|
||||||
|
CHECK(fake.send_count == 0U);
|
||||||
|
CanSensor_Task(&link, 1500U);
|
||||||
|
CHECK(fake.send_count == 1U);
|
||||||
|
CHECK(fake.last_frame.id == 0x1FFF0000UL);
|
||||||
|
CHECK(fake.last_frame.extended == 1U);
|
||||||
|
CHECK(fake.last_frame.length == 1U);
|
||||||
|
CHECK(fake.last_frame.data[0] == 1U);
|
||||||
|
|
||||||
|
/* После period=0 дальнейшие вызовы не отправляют heartbeat. */
|
||||||
|
CanSensor_SetPulsePeriod(&link, 0U);
|
||||||
|
CHECK(CanSensor_PulsePeriod(&link) == 0U);
|
||||||
|
CanSensor_Task(&link, 0xFFFFFFFFUL);
|
||||||
|
CHECK(fake.send_count == 1U);
|
||||||
|
|
||||||
|
/* Повторное включение начинает новый полный период. */
|
||||||
|
CanSensor_SetPulsePeriod(&link, 10U);
|
||||||
|
CanSensor_Task(&link, 2000U);
|
||||||
|
CanSensor_Task(&link, 2009U);
|
||||||
|
CHECK(fake.send_count == 1U);
|
||||||
|
CanSensor_Task(&link, 2010U);
|
||||||
|
CHECK(fake.send_count == 2U);
|
||||||
|
CHECK(fake.last_frame.data[0] == 2U);
|
||||||
|
|
||||||
|
puts("can_sensor pulse tests passed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -23,7 +23,9 @@
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `include/ds18b20.h`, `src/ds18b20.c` | перечень датчиков, измерения, запись TH/TL/разрешения | `onewire.h`, `stdint.h` |
|
| `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` |
|
| `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-тесты | — |
|
| `tests/` | симулятор шины с виртуальными датчиками и host-тесты | — |
|
||||||
|
|
||||||
## Порт: что нужно от платформы
|
## Порт: что нужно от платформы
|
||||||
@@ -48,7 +50,7 @@ uint8_t OneWire_ReadBit(void);
|
|||||||
микросекундных задержек — счётчик тактов ядра `DWT->CYCCNT`.
|
микросекундных задержек — счётчик тактов ядра `DWT->CYCCNT`.
|
||||||
|
|
||||||
Привязку вывода порт берёт из `onewire_config.h`, который предоставляет
|
Привязку вывода порт берёт из `onewire_config.h`, который предоставляет
|
||||||
приложение — шаблон лежит рядом:
|
приложение — шаблон `onewire_config.f103.template.h` лежит рядом:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#include "stm32f10x.h"
|
#include "stm32f10x.h"
|
||||||
@@ -60,6 +62,33 @@ uint8_t OneWire_ReadBit(void);
|
|||||||
|
|
||||||
Требуется внешняя подтяжка 4.7 кОм к питанию.
|
Требуется внешняя подтяжка 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
|
```c
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* и OneWire_ReadBit.
|
* и OneWire_ReadBit.
|
||||||
*
|
*
|
||||||
* Чтобы перенести библиотеку на другой микроконтроллер, достаточно написать
|
* Чтобы перенести библиотеку на другой микроконтроллер, достаточно написать
|
||||||
* эти пять функций - остальное собирается как есть.
|
* эти пять функций - остальное собирается как есть. ТЕСТ
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef ONEWIRE_H
|
#ifndef ONEWIRE_H
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @file onewire_config.template.h
|
* @file onewire_config.f103.template.h
|
||||||
* @brief Шаблон привязки шины 1-Wire для порта STM32F1.
|
* @brief Шаблон привязки шины 1-Wire для STM32F103 (порт stm32f1).
|
||||||
*
|
*
|
||||||
* Скопируйте в своё приложение под именем onewire_config.h и положите на
|
* Скопируйте в своё приложение под именем onewire_config.h и положите на
|
||||||
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
|
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
* терять байты USART во время обмена.
|
* терять байты USART во время обмена.
|
||||||
*
|
*
|
||||||
* Привязка вывода приходит из onewire_config.h, который предоставляет
|
* Привязка вывода приходит из onewire_config.h, который предоставляет
|
||||||
* приложение. Шаблон - onewire_config.template.h рядом с этим файлом.
|
* приложение. Шаблон для STM32F103 —
|
||||||
|
* onewire_config.f103.template.h рядом с этим файлом.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "onewire.h"
|
#include "onewire.h"
|
||||||
|
|||||||
26
c/ds18b20/ports/stm32f4/onewire_config.f407.template.h
Normal file
26
c/ds18b20/ports/stm32f4/onewire_config.f407.template.h
Normal 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 */
|
||||||
170
c/ds18b20/ports/stm32f4/onewire_stm32f4.c
Normal file
170
c/ds18b20/ports/stm32f4/onewire_stm32f4.c
Normal 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;
|
||||||
|
}
|
||||||
26
c/ds18b20/ports/stm32g4/onewire_config.g431.template.h
Normal file
26
c/ds18b20/ports/stm32g4/onewire_config.g431.template.h
Normal 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 */
|
||||||
26
c/ds18b20/ports/stm32g4/onewire_config.g474.template.h
Normal file
26
c/ds18b20/ports/stm32g4/onewire_config.g474.template.h
Normal 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 */
|
||||||
172
c/ds18b20/ports/stm32g4/onewire_stm32g4.c
Normal file
172
c/ds18b20/ports/stm32g4/onewire_stm32g4.c
Normal 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;
|
||||||
|
}
|
||||||
8
c/firmware-info/CMakeLists.txt
Normal file
8
c/firmware-info/CMakeLists.txt
Normal 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)
|
||||||
11
c/firmware-info/PORTING.md
Normal file
11
c/firmware-info/PORTING.md
Normal 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
27
c/firmware-info/README.md
Normal 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` без изменений.
|
||||||
48
c/firmware-info/include/firmware_info.h
Normal file
48
c/firmware-info/include/firmware_info.h
Normal 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
|
||||||
18
c/firmware-info/include/firmware_info_port.h
Normal file
18
c/firmware-info/include/firmware_info_port.h
Normal 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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
121
c/firmware-info/src/firmware_info.c
Normal file
121
c/firmware-info/src/firmware_info.c
Normal 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;
|
||||||
|
}
|
||||||
13
c/firmware-info/src/firmware_info_port.c
Normal file
13
c/firmware-info/src/firmware_info_port.c
Normal 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);
|
||||||
|
}
|
||||||
20
c/firmware-info/tests/test_firmware_info.c
Normal file
20
c/firmware-info/tests/test_firmware_info.c
Normal 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;
|
||||||
|
}
|
||||||
18
c/firmware-info/tools/make_build_id.ps1
Normal file
18
c/firmware-info/tools/make_build_id.ps1
Normal 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"
|
||||||
30
c/led-indicator/CMakeLists.txt
Normal file
30
c/led-indicator/CMakeLists.txt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(led_indicator C)
|
||||||
|
|
||||||
|
set(CMAKE_C_STANDARD 99)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
add_library(led_indicator STATIC led_indicator.c)
|
||||||
|
target_include_directories(led_indicator PUBLIC .)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(led_indicator PRIVATE /W4)
|
||||||
|
else()
|
||||||
|
target_compile_options(led_indicator PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
option(LED_INDICATOR_BUILD_TESTS "Собирать тесты LED indicator" ON)
|
||||||
|
if(LED_INDICATOR_BUILD_TESTS)
|
||||||
|
enable_testing()
|
||||||
|
add_executable(test_led_indicator tests/test_led_indicator.c)
|
||||||
|
target_link_libraries(test_led_indicator PRIVATE led_indicator)
|
||||||
|
add_test(NAME led_indicator COMMAND test_led_indicator)
|
||||||
|
|
||||||
|
add_executable(test_led_indicator_stm32_port
|
||||||
|
tests/test_stm32_port.c
|
||||||
|
ports/stm32-hal/led_indicator_stm32_hal.c)
|
||||||
|
target_include_directories(test_led_indicator_stm32_port PRIVATE
|
||||||
|
. ports/stm32-hal tests/fakes)
|
||||||
|
target_link_libraries(test_led_indicator_stm32_port PRIVATE led_indicator)
|
||||||
|
add_test(NAME led_indicator_stm32_port COMMAND test_led_indicator_stm32_port)
|
||||||
|
endif()
|
||||||
126
c/led-indicator/README.md
Normal file
126
c/led-indicator/README.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# LED Indicator
|
||||||
|
|
||||||
|
Неблокирующая C99-библиотека для индикации состояний `STARTUP`, `WORK`,
|
||||||
|
`ACTIVITY`, `WARNING`, `ERROR` и `CRITICAL` с разными рисунками и частотами.
|
||||||
|
Один экземпляр обслуживает несколько светодиодов.
|
||||||
|
|
||||||
|
Ядро не знает о GPIO, PWM, сдвиговом регистре, RTOS и модели таймера. Порт
|
||||||
|
передаёт логическое состояние канала наружу и читает монотонное время в
|
||||||
|
миллисекундах. В библиотеке нет задержек, динамической памяти, прерываний и
|
||||||
|
изменяемого глобального состояния.
|
||||||
|
|
||||||
|
```text
|
||||||
|
приложение -> LedIndicator_SetMode/Process -> ядро -> write/now_ms -> GPIO/PWM/expander + TIM
|
||||||
|
```
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
| Файл | Назначение | Зависимости |
|
||||||
|
|---|---|---|
|
||||||
|
| `led_indicator.h/.c` | режимы, шаблоны и планировщик | C99, `stdint.h` |
|
||||||
|
| `ports/stm32-hal` | GPIO и выбранный аппаратный TIM | STM32 HAL |
|
||||||
|
| `tests/test_led_indicator.c` | host-тесты, включая переполнение `uint32_t` | libc |
|
||||||
|
|
||||||
|
## Встроенные режимы
|
||||||
|
|
||||||
|
| Режим | Сигнал |
|
||||||
|
|---|---|
|
||||||
|
| `OFF` / `ON` | постоянно выключен / включён |
|
||||||
|
| `STARTUP` | три коротких импульса, затем постоянно включён |
|
||||||
|
| `WORK` | 100 мс включён, 900 мс выключен |
|
||||||
|
| `ACTIVITY` | 50/50 мс |
|
||||||
|
| `WARNING` | 250/250 мс |
|
||||||
|
| `ERROR` | два импульса и пауза |
|
||||||
|
| `CRITICAL` | три импульса и пауза |
|
||||||
|
|
||||||
|
Частоты не зашиты в алгоритм. Скопируйте встроенную таблицу и измените
|
||||||
|
`duration_ms` до инициализации:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static LedIndicator_Pattern patterns[LED_INDICATOR_MODE_COUNT];
|
||||||
|
|
||||||
|
LedIndicator_CopyDefaultPatterns(patterns, LED_INDICATOR_MODE_COUNT);
|
||||||
|
patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] = 50U;
|
||||||
|
patterns[LED_INDICATOR_MODE_WORK].duration_ms[1] = 1950U;
|
||||||
|
config.patterns = patterns;
|
||||||
|
config.pattern_count = LED_INDICATOR_MODE_COUNT;
|
||||||
|
```
|
||||||
|
|
||||||
|
Таблица должна существовать всё время работы экземпляра. До восьми шагов
|
||||||
|
описываются длительностями и битовой маской `levels`; шаблон может повторяться
|
||||||
|
или после одного прохода перейти в `final_level`.
|
||||||
|
|
||||||
|
## Контракт порта
|
||||||
|
|
||||||
|
```c
|
||||||
|
void write(void *context, uint8_t channel, uint8_t logical_on);
|
||||||
|
uint32_t now_ms(void *context);
|
||||||
|
```
|
||||||
|
|
||||||
|
`write` получает именно логический уровень. Инверсию active-low, управление
|
||||||
|
PWM или запись общего регистра расширителя выполняет порт. Переполнение
|
||||||
|
32-битной миллисекундной метки обработано разностью беззнаковых чисел.
|
||||||
|
|
||||||
|
Если время уже есть в планировщике приложения, `now_ms` можно оставить `NULL`
|
||||||
|
и вызывать варианты `LedIndicator_SetModeAt`/`LedIndicator_ProcessAt`.
|
||||||
|
|
||||||
|
## Быстрый старт без привязки к HAL
|
||||||
|
|
||||||
|
```c
|
||||||
|
static LedIndicator led;
|
||||||
|
static LedIndicator_Channel state[2];
|
||||||
|
LedIndicator_Config cfg;
|
||||||
|
LedIndicator_Port port = {Board_LedWrite, Board_TimerMs, &board};
|
||||||
|
|
||||||
|
LedIndicator_ConfigDefault(&cfg);
|
||||||
|
LedIndicator_Init(&led, state, 2U, &port, &cfg);
|
||||||
|
LedIndicator_SetMode(&led, 0U, LED_INDICATOR_MODE_WORK);
|
||||||
|
LedIndicator_SetMode(&led, 1U, LED_INDICATOR_MODE_ERROR);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
LedIndicator_Process(&led);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`SetMode` идемпотентен: повторный вызов того же режима в каждом проходе цикла
|
||||||
|
не начинает рисунок заново. Для нового импульса события служит
|
||||||
|
`LedIndicator_RestartMode`.
|
||||||
|
|
||||||
|
## STM32F103, STM32F4, STM32G431 и STM32G474
|
||||||
|
|
||||||
|
Порт `ports/stm32-hal` принимает конкретный `TIM_HandleTypeDef *`; библиотека
|
||||||
|
не использует `HAL_GetTick()` и не занимает SysTick. Настройте TIM как
|
||||||
|
free-running, запустите его и передайте частоту счётчика после prescaler.
|
||||||
|
|
||||||
|
Скопируйте подходящий `led_indicator_stm32_hal_config.*.template.h` в каталог
|
||||||
|
платы под именем `led_indicator_stm32_hal_config.h`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
static const LedIndicator_Stm32HalOutput outputs[] = {
|
||||||
|
{STATUS_GPIO_Port, STATUS_Pin, 0U},
|
||||||
|
{ERROR_GPIO_Port, ERROR_Pin, 1U}
|
||||||
|
};
|
||||||
|
static LedIndicator_Stm32HalPort hw;
|
||||||
|
static LedIndicator_Port port;
|
||||||
|
|
||||||
|
HAL_TIM_Base_Start(&htim6); /* CNT = 1 кГц в данном примере. */
|
||||||
|
LedIndicator_Stm32HalPortInit(&hw, &htim6, 1000U,
|
||||||
|
outputs, 2U, &port);
|
||||||
|
```
|
||||||
|
|
||||||
|
`LedIndicator_Process()` должен вызываться чаще, чем переполняется выбранный
|
||||||
|
TIM. Для 16-битного CNT на 1 кГц это не реже одного раза за 65 секунд. Сам
|
||||||
|
таймер и его prescaler/period задаются в CubeMX или board-порте, а не в ядре.
|
||||||
|
|
||||||
|
Для К1921ВК028 и C28x используется тот же основной порт: `now_ms` возвращает
|
||||||
|
счётчик, увеличиваемый обработчиком выбранного TIMER/CPU Timer, а `write`
|
||||||
|
обращается к GPIO SDK. Такое разделение оставляет номер таймера и выводы в
|
||||||
|
проекте конкретной платы.
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S c/led-indicator -B build/led-indicator
|
||||||
|
cmake --build build/led-indicator
|
||||||
|
ctest --test-dir build/led-indicator --output-on-failure
|
||||||
|
```
|
||||||
246
c/led-indicator/led_indicator.c
Normal file
246
c/led-indicator/led_indicator.c
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
#include "led_indicator.h"
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
|
#define LEVELS_1 0x01U
|
||||||
|
#define LEVELS_10 0x01U
|
||||||
|
#define LEVELS_1010 0x05U
|
||||||
|
#define LEVELS_101010 0x15U
|
||||||
|
|
||||||
|
static const LedIndicator_Pattern g_default_patterns[LED_INDICATOR_MODE_COUNT] = {
|
||||||
|
{{0U}, 0U, 1U, 1U, 0U},
|
||||||
|
{{0U}, LEVELS_1, 1U, 1U, 1U},
|
||||||
|
{{100U, 100U, 100U, 100U, 100U, 100U}, LEVELS_101010, 6U, 0U, 1U},
|
||||||
|
{{100U, 900U}, LEVELS_10, 2U, 1U, 0U},
|
||||||
|
{{50U, 50U}, LEVELS_10, 2U, 1U, 0U},
|
||||||
|
{{250U, 250U}, LEVELS_10, 2U, 1U, 0U},
|
||||||
|
{{100U, 100U, 100U, 700U}, LEVELS_1010, 4U, 1U, 0U},
|
||||||
|
{{100U, 100U, 100U, 100U, 100U, 700U}, LEVELS_101010, 6U, 1U, 0U}
|
||||||
|
};
|
||||||
|
|
||||||
|
static uint8_t pattern_valid(const LedIndicator_Pattern *pattern)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
uint32_t total = 0U;
|
||||||
|
|
||||||
|
if ((pattern == NULL) || (pattern->step_count == 0U) ||
|
||||||
|
(pattern->step_count > LED_INDICATOR_MAX_STEPS)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (pattern->step_count == 1U) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
for (i = 0U; i < pattern->step_count; ++i) {
|
||||||
|
if ((pattern->duration_ms[i] == 0U) ||
|
||||||
|
(UINT32_MAX - total < pattern->duration_ms[i])) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
total += pattern->duration_ms[i];
|
||||||
|
}
|
||||||
|
return (uint8_t)(total != 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t pattern_period(const LedIndicator_Pattern *pattern)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
uint32_t total = 0U;
|
||||||
|
|
||||||
|
for (i = 0U; i < pattern->step_count; ++i) {
|
||||||
|
total += pattern->duration_ms[i];
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t pattern_level(const LedIndicator_Pattern *pattern, uint32_t elapsed)
|
||||||
|
{
|
||||||
|
uint32_t position = elapsed;
|
||||||
|
uint32_t period;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if (pattern->step_count == 1U) {
|
||||||
|
return (uint8_t)(pattern->levels & 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
period = pattern_period(pattern);
|
||||||
|
if (pattern->repeat != 0U) {
|
||||||
|
position %= period;
|
||||||
|
} else if (position >= period) {
|
||||||
|
return (uint8_t)(pattern->final_level != 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0U; i < pattern->step_count; ++i) {
|
||||||
|
if (position < pattern->duration_ms[i]) {
|
||||||
|
return (uint8_t)((pattern->levels >> i) & 1U);
|
||||||
|
}
|
||||||
|
position -= pattern->duration_ms[i];
|
||||||
|
}
|
||||||
|
return (uint8_t)(pattern->final_level != 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_if_changed(LedIndicator *instance, size_t channel, uint8_t level,
|
||||||
|
uint8_t force)
|
||||||
|
{
|
||||||
|
LedIndicator_Channel *state = &instance->channels[channel];
|
||||||
|
|
||||||
|
level = (uint8_t)(level != 0U);
|
||||||
|
if ((force != 0U) || (state->output_level != level)) {
|
||||||
|
state->output_level = level;
|
||||||
|
instance->port.write(instance->port.context, (uint8_t)channel, level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LedIndicator_ConfigDefault(LedIndicator_Config *config)
|
||||||
|
{
|
||||||
|
if (config != NULL) {
|
||||||
|
config->patterns = g_default_patterns;
|
||||||
|
config->pattern_count = LED_INDICATOR_MODE_COUNT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t LedIndicator_CopyDefaultPatterns(LedIndicator_Pattern *patterns, size_t capacity)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
size_t count = (size_t)LED_INDICATOR_MODE_COUNT;
|
||||||
|
|
||||||
|
if (patterns == NULL) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (capacity < count) {
|
||||||
|
count = capacity;
|
||||||
|
}
|
||||||
|
for (i = 0U; i < count; ++i) {
|
||||||
|
patterns[i] = g_default_patterns[i];
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_ValidateConfig(const LedIndicator_Config *config)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if ((config == NULL) || (config->patterns == NULL) ||
|
||||||
|
(config->pattern_count == 0U) ||
|
||||||
|
(config->pattern_count > (size_t)LED_INDICATOR_MODE_COUNT)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
for (i = 0U; i < config->pattern_count; ++i) {
|
||||||
|
if (pattern_valid(&config->patterns[i]) == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_Init(LedIndicator *instance,
|
||||||
|
LedIndicator_Channel *channels,
|
||||||
|
size_t channel_count,
|
||||||
|
const LedIndicator_Port *port,
|
||||||
|
const LedIndicator_Config *config)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if ((instance == NULL) || (channels == NULL) || (channel_count == 0U) ||
|
||||||
|
(channel_count > 256U) || (port == NULL) || (port->write == NULL) ||
|
||||||
|
(LedIndicator_ValidateConfig(config) == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance->port = *port;
|
||||||
|
instance->patterns = config->patterns;
|
||||||
|
instance->pattern_count = config->pattern_count;
|
||||||
|
instance->channels = channels;
|
||||||
|
instance->channel_count = channel_count;
|
||||||
|
instance->initialized = 1U;
|
||||||
|
|
||||||
|
for (i = 0U; i < channel_count; ++i) {
|
||||||
|
channels[i].mode = LED_INDICATOR_MODE_OFF;
|
||||||
|
channels[i].started_ms = 0U;
|
||||||
|
channels[i].output_level = 0U;
|
||||||
|
channels[i].initialized = 1U;
|
||||||
|
write_if_changed(instance, i, 0U, 1U);
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_SetModeAt(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode, uint32_t now_ms)
|
||||||
|
{
|
||||||
|
if ((instance == NULL) || (instance->initialized == 0U) ||
|
||||||
|
(channel >= instance->channel_count) || ((size_t)mode >= instance->pattern_count)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (instance->channels[channel].mode == mode) {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
return LedIndicator_RestartModeAt(instance, channel, mode, now_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_RestartModeAt(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode, uint32_t now_ms)
|
||||||
|
{
|
||||||
|
LedIndicator_Channel *state;
|
||||||
|
const LedIndicator_Pattern *pattern;
|
||||||
|
|
||||||
|
if ((instance == NULL) || (instance->initialized == 0U) ||
|
||||||
|
(channel >= instance->channel_count) || ((size_t)mode >= instance->pattern_count)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = &instance->channels[channel];
|
||||||
|
state->mode = mode;
|
||||||
|
state->started_ms = now_ms;
|
||||||
|
pattern = &instance->patterns[(size_t)mode];
|
||||||
|
write_if_changed(instance, channel, pattern_level(pattern, 0U), 0U);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_SetMode(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode)
|
||||||
|
{
|
||||||
|
if ((instance == NULL) || (instance->port.now_ms == NULL)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return LedIndicator_SetModeAt(instance, channel, mode,
|
||||||
|
instance->port.now_ms(instance->port.context));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_RestartMode(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode)
|
||||||
|
{
|
||||||
|
if ((instance == NULL) || (instance->port.now_ms == NULL)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return LedIndicator_RestartModeAt(instance, channel, mode,
|
||||||
|
instance->port.now_ms(instance->port.context));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if ((instance == NULL) || (instance->initialized == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (i = 0U; i < instance->channel_count; ++i) {
|
||||||
|
const LedIndicator_Channel *state = &instance->channels[i];
|
||||||
|
const LedIndicator_Pattern *pattern = &instance->patterns[(size_t)state->mode];
|
||||||
|
const uint32_t elapsed = now_ms - state->started_ms;
|
||||||
|
write_if_changed(instance, i, pattern_level(pattern, elapsed), 0U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LedIndicator_Process(LedIndicator *instance)
|
||||||
|
{
|
||||||
|
if ((instance != NULL) && (instance->port.now_ms != NULL)) {
|
||||||
|
LedIndicator_ProcessAt(instance, instance->port.now_ms(instance->port.context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LedIndicator_Mode LedIndicator_GetMode(const LedIndicator *instance, size_t channel)
|
||||||
|
{
|
||||||
|
if ((instance == NULL) || (instance->initialized == 0U) ||
|
||||||
|
(channel >= instance->channel_count)) {
|
||||||
|
return LED_INDICATOR_MODE_OFF;
|
||||||
|
}
|
||||||
|
return instance->channels[channel].mode;
|
||||||
|
}
|
||||||
117
c/led-indicator/led_indicator.h
Normal file
117
c/led-indicator/led_indicator.h
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* @file led_indicator.h
|
||||||
|
* @brief Неблокирующая индикация состояния устройства на одном или нескольких LED.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LED_INDICATOR_H
|
||||||
|
#define LED_INDICATOR_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define LED_INDICATOR_MAX_STEPS 8U
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LED_INDICATOR_MODE_OFF = 0,
|
||||||
|
LED_INDICATOR_MODE_ON,
|
||||||
|
LED_INDICATOR_MODE_STARTUP,
|
||||||
|
LED_INDICATOR_MODE_WORK,
|
||||||
|
LED_INDICATOR_MODE_ACTIVITY,
|
||||||
|
LED_INDICATOR_MODE_WARNING,
|
||||||
|
LED_INDICATOR_MODE_ERROR,
|
||||||
|
LED_INDICATOR_MODE_CRITICAL,
|
||||||
|
LED_INDICATOR_MODE_COUNT
|
||||||
|
} LedIndicator_Mode;
|
||||||
|
|
||||||
|
/** Один период сигнала. Бит N в levels задаёт уровень шага N. */
|
||||||
|
typedef struct {
|
||||||
|
uint32_t duration_ms[LED_INDICATOR_MAX_STEPS];
|
||||||
|
uint8_t levels;
|
||||||
|
uint8_t step_count;
|
||||||
|
uint8_t repeat;
|
||||||
|
uint8_t final_level;
|
||||||
|
} LedIndicator_Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Аппаратный порт. write получает логический уровень, поэтому active-low,
|
||||||
|
* GPIO, PWM и регистры расширителя обрабатываются за границей ядра.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
void (*write)(void *context, uint8_t channel, uint8_t on);
|
||||||
|
uint32_t (*now_ms)(void *context);
|
||||||
|
void *context;
|
||||||
|
} LedIndicator_Port;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
LedIndicator_Mode mode;
|
||||||
|
uint32_t started_ms;
|
||||||
|
uint8_t output_level;
|
||||||
|
uint8_t initialized;
|
||||||
|
} LedIndicator_Channel;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const LedIndicator_Pattern *patterns;
|
||||||
|
size_t pattern_count;
|
||||||
|
} LedIndicator_Config;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
LedIndicator_Port port;
|
||||||
|
const LedIndicator_Pattern *patterns;
|
||||||
|
LedIndicator_Channel *channels;
|
||||||
|
size_t pattern_count;
|
||||||
|
size_t channel_count;
|
||||||
|
uint8_t initialized;
|
||||||
|
} LedIndicator;
|
||||||
|
|
||||||
|
/** Заполняет конфигурацию встроенными шаблонами режимов. */
|
||||||
|
void LedIndicator_ConfigDefault(LedIndicator_Config *config);
|
||||||
|
|
||||||
|
/** Копирует встроенные шаблоны в изменяемую таблицу; возвращает число записей. */
|
||||||
|
size_t LedIndicator_CopyDefaultPatterns(LedIndicator_Pattern *patterns, size_t capacity);
|
||||||
|
|
||||||
|
/** Проверяет таблицу шаблонов. */
|
||||||
|
uint8_t LedIndicator_ValidateConfig(const LedIndicator_Config *config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализирует экземпляр и немедленно выключает все его каналы.
|
||||||
|
* Массив channels принадлежит приложению и должен жить столько же, сколько instance.
|
||||||
|
*/
|
||||||
|
uint8_t LedIndicator_Init(LedIndicator *instance,
|
||||||
|
LedIndicator_Channel *channels,
|
||||||
|
size_t channel_count,
|
||||||
|
const LedIndicator_Port *port,
|
||||||
|
const LedIndicator_Config *config);
|
||||||
|
|
||||||
|
/** Назначает режим, используя время порта. Повтор того же режима не сбрасывает фазу. */
|
||||||
|
uint8_t LedIndicator_SetMode(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode);
|
||||||
|
|
||||||
|
/** Назначает режим с явно переданной меткой времени. Повтор не сбрасывает фазу. */
|
||||||
|
uint8_t LedIndicator_SetModeAt(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode, uint32_t now_ms);
|
||||||
|
|
||||||
|
/** Принудительно запускает режим с первого шага, используя время порта. */
|
||||||
|
uint8_t LedIndicator_RestartMode(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode);
|
||||||
|
|
||||||
|
/** Принудительно запускает режим с первого шага в указанное время. */
|
||||||
|
uint8_t LedIndicator_RestartModeAt(LedIndicator *instance, size_t channel,
|
||||||
|
LedIndicator_Mode mode, uint32_t now_ms);
|
||||||
|
|
||||||
|
/** Обновляет выходы, используя время порта. Вызывать в главном цикле. */
|
||||||
|
void LedIndicator_Process(LedIndicator *instance);
|
||||||
|
|
||||||
|
/** Обновляет выходы с явно переданной меткой времени. */
|
||||||
|
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms);
|
||||||
|
|
||||||
|
LedIndicator_Mode LedIndicator_GetMode(const LedIndicator *instance, size_t channel);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* LED_INDICATOR_H */
|
||||||
65
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.c
Normal file
65
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.c
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#include "led_indicator_stm32_hal.h"
|
||||||
|
|
||||||
|
static void stm32_write(void *context, uint8_t channel, uint8_t on)
|
||||||
|
{
|
||||||
|
LedIndicator_Stm32HalPort *adapter = (LedIndicator_Stm32HalPort *)context;
|
||||||
|
const LedIndicator_Stm32HalOutput *output;
|
||||||
|
GPIO_PinState state;
|
||||||
|
|
||||||
|
if ((adapter == NULL) || ((size_t)channel >= adapter->output_count)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
output = &adapter->outputs[channel];
|
||||||
|
state = ((on != 0U) ^ (output->active_low != 0U)) ? GPIO_PIN_SET : GPIO_PIN_RESET;
|
||||||
|
HAL_GPIO_WritePin(output->gpio, output->pin, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t stm32_now_ms(void *context)
|
||||||
|
{
|
||||||
|
LedIndicator_Stm32HalPort *adapter = (LedIndicator_Stm32HalPort *)context;
|
||||||
|
const uint32_t current = __HAL_TIM_GET_COUNTER(adapter->timer);
|
||||||
|
const uint32_t reload = __HAL_TIM_GET_AUTORELOAD(adapter->timer);
|
||||||
|
uint32_t delta;
|
||||||
|
uint64_t scaled;
|
||||||
|
|
||||||
|
if (current >= adapter->last_counter) {
|
||||||
|
delta = current - adapter->last_counter;
|
||||||
|
} else if (reload == UINT32_MAX) {
|
||||||
|
delta = current - adapter->last_counter;
|
||||||
|
} else {
|
||||||
|
delta = (reload - adapter->last_counter) + 1U + current;
|
||||||
|
}
|
||||||
|
adapter->last_counter = current;
|
||||||
|
|
||||||
|
scaled = (uint64_t)adapter->remainder + ((uint64_t)delta * 1000ULL);
|
||||||
|
adapter->accumulated_ms += (uint32_t)(scaled / adapter->timer_tick_hz);
|
||||||
|
adapter->remainder = (uint32_t)(scaled % adapter->timer_tick_hz);
|
||||||
|
return adapter->accumulated_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t LedIndicator_Stm32HalPortInit(LedIndicator_Stm32HalPort *adapter,
|
||||||
|
TIM_HandleTypeDef *timer,
|
||||||
|
uint32_t timer_tick_hz,
|
||||||
|
const LedIndicator_Stm32HalOutput *outputs,
|
||||||
|
size_t output_count,
|
||||||
|
LedIndicator_Port *port)
|
||||||
|
{
|
||||||
|
if ((adapter == NULL) || (timer == NULL) || (timer_tick_hz == 0U) ||
|
||||||
|
(outputs == NULL) || (output_count == 0U) || (output_count > 256U) ||
|
||||||
|
(port == NULL)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter->timer = timer;
|
||||||
|
adapter->outputs = outputs;
|
||||||
|
adapter->output_count = output_count;
|
||||||
|
adapter->timer_tick_hz = timer_tick_hz;
|
||||||
|
adapter->last_counter = __HAL_TIM_GET_COUNTER(timer);
|
||||||
|
adapter->accumulated_ms = 0U;
|
||||||
|
adapter->remainder = 0U;
|
||||||
|
|
||||||
|
port->write = stm32_write;
|
||||||
|
port->now_ms = stm32_now_ms;
|
||||||
|
port->context = adapter;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
39
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.h
Normal file
39
c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* @file led_indicator_stm32_hal.h
|
||||||
|
* @brief Порт LED Indicator для STM32F1/F4/G4 HAL и выбранного TIM.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LED_INDICATOR_STM32_HAL_H
|
||||||
|
#define LED_INDICATOR_STM32_HAL_H
|
||||||
|
|
||||||
|
#include "led_indicator.h"
|
||||||
|
#include "led_indicator_stm32_hal_config.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
GPIO_TypeDef *gpio;
|
||||||
|
uint16_t pin;
|
||||||
|
uint8_t active_low;
|
||||||
|
} LedIndicator_Stm32HalOutput;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
TIM_HandleTypeDef *timer;
|
||||||
|
const LedIndicator_Stm32HalOutput *outputs;
|
||||||
|
size_t output_count;
|
||||||
|
uint32_t timer_tick_hz;
|
||||||
|
uint32_t last_counter;
|
||||||
|
uint32_t accumulated_ms;
|
||||||
|
uint32_t remainder;
|
||||||
|
} LedIndicator_Stm32HalPort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создаёт порт на уже настроенном и запущенном таймере.
|
||||||
|
* timer_tick_hz — частота изменения CNT после prescaler, например 1000 Гц.
|
||||||
|
*/
|
||||||
|
uint8_t LedIndicator_Stm32HalPortInit(LedIndicator_Stm32HalPort *adapter,
|
||||||
|
TIM_HandleTypeDef *timer,
|
||||||
|
uint32_t timer_tick_hz,
|
||||||
|
const LedIndicator_Stm32HalOutput *outputs,
|
||||||
|
size_t output_count,
|
||||||
|
LedIndicator_Port *port);
|
||||||
|
|
||||||
|
#endif /* LED_INDICATOR_STM32_HAL_H */
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
#define LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
|
||||||
|
#include "stm32f1xx_hal.h"
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
#define LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
|
||||||
|
#include "stm32f4xx_hal.h"
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
#define LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
|
||||||
|
#include "stm32g4xx_hal.h"
|
||||||
|
|
||||||
|
#endif
|
||||||
27
c/led-indicator/tests/fakes/led_indicator_stm32_hal_config.h
Normal file
27
c/led-indicator/tests/fakes/led_indicator_stm32_hal_config.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
#define LED_INDICATOR_STM32_HAL_CONFIG_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GPIO_PIN_RESET = 0,
|
||||||
|
GPIO_PIN_SET
|
||||||
|
} GPIO_PinState;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t last_pin;
|
||||||
|
GPIO_PinState last_state;
|
||||||
|
uint32_t writes;
|
||||||
|
} GPIO_TypeDef;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t counter;
|
||||||
|
uint32_t autoreload;
|
||||||
|
} TIM_HandleTypeDef;
|
||||||
|
|
||||||
|
#define __HAL_TIM_GET_COUNTER(handle) ((handle)->counter)
|
||||||
|
#define __HAL_TIM_GET_AUTORELOAD(handle) ((handle)->autoreload)
|
||||||
|
|
||||||
|
void HAL_GPIO_WritePin(GPIO_TypeDef *gpio, uint16_t pin, GPIO_PinState state);
|
||||||
|
|
||||||
|
#endif
|
||||||
84
c/led-indicator/tests/test_led_indicator.c
Normal file
84
c/led-indicator/tests/test_led_indicator.c
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#include "led_indicator.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t now;
|
||||||
|
uint8_t outputs[3];
|
||||||
|
uint32_t writes[3];
|
||||||
|
} FakePort;
|
||||||
|
|
||||||
|
static void fake_write(void *context, uint8_t channel, uint8_t on)
|
||||||
|
{
|
||||||
|
FakePort *fake = (FakePort *)context;
|
||||||
|
fake->outputs[channel] = on;
|
||||||
|
fake->writes[channel]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t fake_now(void *context)
|
||||||
|
{
|
||||||
|
return ((FakePort *)context)->now;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CHECK(condition) \
|
||||||
|
do { \
|
||||||
|
if (!(condition)) { \
|
||||||
|
fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \
|
||||||
|
#condition); \
|
||||||
|
return 1; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
LedIndicator indicator;
|
||||||
|
LedIndicator_Channel channels[3];
|
||||||
|
LedIndicator_Config config;
|
||||||
|
LedIndicator_Pattern patterns[LED_INDICATOR_MODE_COUNT];
|
||||||
|
FakePort fake = {0};
|
||||||
|
LedIndicator_Port port = {fake_write, fake_now, &fake};
|
||||||
|
|
||||||
|
LedIndicator_ConfigDefault(&config);
|
||||||
|
CHECK(LedIndicator_ValidateConfig(&config) != 0U);
|
||||||
|
CHECK(LedIndicator_CopyDefaultPatterns(patterns, LED_INDICATOR_MODE_COUNT) ==
|
||||||
|
LED_INDICATOR_MODE_COUNT);
|
||||||
|
patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] = 25U;
|
||||||
|
CHECK(patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] == 25U);
|
||||||
|
CHECK(LedIndicator_Init(&indicator, channels, 3U, &port, &config) != 0U);
|
||||||
|
CHECK(fake.writes[0] == 1U && fake.writes[1] == 1U && fake.writes[2] == 1U);
|
||||||
|
|
||||||
|
CHECK(LedIndicator_SetMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
|
||||||
|
CHECK(fake.outputs[0] == 1U);
|
||||||
|
fake.now = 99U;
|
||||||
|
CHECK(LedIndicator_SetMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
|
||||||
|
LedIndicator_Process(&indicator);
|
||||||
|
CHECK(fake.outputs[0] == 1U);
|
||||||
|
fake.now = 100U;
|
||||||
|
LedIndicator_Process(&indicator);
|
||||||
|
CHECK(fake.outputs[0] == 0U);
|
||||||
|
fake.now = 1000U;
|
||||||
|
LedIndicator_Process(&indicator);
|
||||||
|
CHECK(fake.outputs[0] == 1U);
|
||||||
|
fake.now = 1050U;
|
||||||
|
CHECK(LedIndicator_RestartMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
|
||||||
|
fake.now = 1150U;
|
||||||
|
LedIndicator_Process(&indicator);
|
||||||
|
CHECK(fake.outputs[0] == 0U);
|
||||||
|
|
||||||
|
CHECK(LedIndicator_SetModeAt(&indicator, 1U, LED_INDICATOR_MODE_ERROR,
|
||||||
|
0xFFFFFFF0UL) != 0U);
|
||||||
|
LedIndicator_ProcessAt(&indicator, 0x00000054UL);
|
||||||
|
CHECK(fake.outputs[1] == 0U); /* 100 ms после старта, переход во второй шаг. */
|
||||||
|
LedIndicator_ProcessAt(&indicator, 0x000000B8UL);
|
||||||
|
CHECK(fake.outputs[1] == 1U); /* 200 ms после старта, второй импульс. */
|
||||||
|
|
||||||
|
CHECK(LedIndicator_SetModeAt(&indicator, 2U, LED_INDICATOR_MODE_STARTUP, 10U) != 0U);
|
||||||
|
LedIndicator_ProcessAt(&indicator, 610U);
|
||||||
|
CHECK(fake.outputs[2] == 1U); /* Однократный startup завершён постоянным ON. */
|
||||||
|
|
||||||
|
CHECK(LedIndicator_SetMode(&indicator, 3U, LED_INDICATOR_MODE_ON) == 0U);
|
||||||
|
CHECK(LedIndicator_GetMode(&indicator, 0U) == LED_INDICATOR_MODE_WORK);
|
||||||
|
|
||||||
|
puts("led indicator tests passed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
52
c/led-indicator/tests/test_stm32_port.c
Normal file
52
c/led-indicator/tests/test_stm32_port.c
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#include "led_indicator_stm32_hal.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
void HAL_GPIO_WritePin(GPIO_TypeDef *gpio, uint16_t pin, GPIO_PinState state)
|
||||||
|
{
|
||||||
|
gpio->last_pin = pin;
|
||||||
|
gpio->last_state = state;
|
||||||
|
gpio->writes++;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CHECK(condition) \
|
||||||
|
do { \
|
||||||
|
if (!(condition)) { \
|
||||||
|
fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \
|
||||||
|
#condition); \
|
||||||
|
return 1; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
GPIO_TypeDef gpio_a = {0};
|
||||||
|
GPIO_TypeDef gpio_b = {0};
|
||||||
|
TIM_HandleTypeDef timer = {100U, 65535U};
|
||||||
|
const LedIndicator_Stm32HalOutput outputs[] = {
|
||||||
|
{&gpio_a, 0x0001U, 0U},
|
||||||
|
{&gpio_b, 0x0080U, 1U}
|
||||||
|
};
|
||||||
|
LedIndicator_Stm32HalPort adapter;
|
||||||
|
LedIndicator_Port port;
|
||||||
|
|
||||||
|
CHECK(LedIndicator_Stm32HalPortInit(&adapter, &timer, 10000U,
|
||||||
|
outputs, 2U, &port) != 0U);
|
||||||
|
port.write(port.context, 0U, 1U);
|
||||||
|
CHECK(gpio_a.last_pin == 0x0001U && gpio_a.last_state == GPIO_PIN_SET);
|
||||||
|
port.write(port.context, 1U, 1U);
|
||||||
|
CHECK(gpio_b.last_pin == 0x0080U && gpio_b.last_state == GPIO_PIN_RESET);
|
||||||
|
|
||||||
|
timer.counter = 109U;
|
||||||
|
CHECK(port.now_ms(port.context) == 0U);
|
||||||
|
timer.counter = 110U;
|
||||||
|
CHECK(port.now_ms(port.context) == 1U);
|
||||||
|
|
||||||
|
timer.counter = 65530U;
|
||||||
|
(void)port.now_ms(port.context);
|
||||||
|
timer.counter = 4U;
|
||||||
|
CHECK(port.now_ms(port.context) == 6544U);
|
||||||
|
|
||||||
|
puts("led indicator STM32 port tests passed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
26
c/parallel-nand/CMakeLists.txt
Normal file
26
c/parallel-nand/CMakeLists.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(parallel_nand C)
|
||||||
|
|
||||||
|
set(CMAKE_C_STANDARD 99)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
add_library(parallel_nand
|
||||||
|
src/parallel_nand.c
|
||||||
|
src/parallel_nand_gas.c
|
||||||
|
)
|
||||||
|
target_include_directories(parallel_nand PUBLIC
|
||||||
|
include
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(parallel_nand_pcan_gas src/parallel_nand_pcan_gas.c)
|
||||||
|
target_include_directories(parallel_nand_pcan_gas PUBLIC
|
||||||
|
include
|
||||||
|
../set-protocol/include
|
||||||
|
)
|
||||||
|
target_link_libraries(parallel_nand_pcan_gas PUBLIC parallel_nand)
|
||||||
|
|
||||||
|
add_executable(test_parallel_nand tests/test_parallel_nand.c)
|
||||||
|
target_link_libraries(test_parallel_nand PRIVATE parallel_nand)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
add_test(NAME parallel_nand_core COMMAND test_parallel_nand)
|
||||||
117
c/parallel-nand/README.md
Normal file
117
c/parallel-nand/README.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# parallel-nand
|
||||||
|
|
||||||
|
Переносимый C99-драйвер асинхронной parallel NAND x8 и окно управления через
|
||||||
|
общее адресное пространство GAS. Есть профили Micron `MT29F1G08ABADA` и
|
||||||
|
Hynix `HY27UF084G2M` (512 MiB, 2048+64 байта, 64 страницы в блоке,
|
||||||
|
4096 блоков, ID `AD DC 80 95`).
|
||||||
|
|
||||||
|
Подготовлены аппаратные порты:
|
||||||
|
|
||||||
|
- `TMS320F2812` — XINTF Zone 6;
|
||||||
|
- `STM32F407VET6` — аппаратный FSMC NAND Bank 2;
|
||||||
|
- `STM32F103RCT6` — GPIO bit-bang, так как в LQFP64 FSMC недоступен;
|
||||||
|
- `STM32F103ZET6` — аппаратный FSMC NAND Bank 3;
|
||||||
|
- `STM32G474CEU6` — GPIO bit-bang для корпуса UFQFPN-48.
|
||||||
|
|
||||||
|
Ядро не зависит от HAL и транспорта GUI. STM32 HAL используется только в
|
||||||
|
конкретных портах. `parallel_nand_gas` реализует переносимую 16-битную карту
|
||||||
|
GAS и компилируется в том числе TI C28x, где `char` имеет 16 бит.
|
||||||
|
|
||||||
|
## Подключение
|
||||||
|
|
||||||
|
В проект добавить:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c/parallel-nand/src/parallel_nand.c
|
||||||
|
c/parallel-nand/src/parallel_nand_gas.c
|
||||||
|
```
|
||||||
|
|
||||||
|
Пути заголовков:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c/parallel-nand/include
|
||||||
|
c/parallel-nand/ports/<нужный-порт>
|
||||||
|
```
|
||||||
|
|
||||||
|
И один аппаратный порт, например:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c/parallel-nand/ports/stm32g474ce/parallel_nand_port_stm32g474ce.c
|
||||||
|
```
|
||||||
|
|
||||||
|
Минимальная инициализация:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static parallel_nand_t nand;
|
||||||
|
static parallel_nand_gas_t nand_gas;
|
||||||
|
|
||||||
|
void memory_init(void)
|
||||||
|
{
|
||||||
|
int rc = parallel_nand_init(&nand,
|
||||||
|
parallel_nand_port_stm32g474ce(),
|
||||||
|
¶llel_nand_mt29f1g08_geometry);
|
||||||
|
if (rc != PNAND_OK) {
|
||||||
|
/* Оставить диагностику доступной GUI и сообщить ошибку платы. */
|
||||||
|
}
|
||||||
|
|
||||||
|
parallel_nand_gas_init(&nand_gas, &nand, PNAND_GAS_DEFAULT_BASE);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Для Hynix на STM32F103ZET6 используйте
|
||||||
|
`parallel_nand_port_stm32f103ze()` и
|
||||||
|
`parallel_nand_hy27uf084g2m_geometry`; распиновка и ограничения приведены в
|
||||||
|
[`ports/stm32f103ze/README.md`](ports/stm32f103ze/README.md). Порт RCT6
|
||||||
|
с GPIO bit-bang сохранён как отдельный вариант для корпуса LQFP64.
|
||||||
|
|
||||||
|
Диспетчер общего адресного пространства вызывает
|
||||||
|
`parallel_nand_gas_read()`/`parallel_nand_gas_write()` для адресов
|
||||||
|
`0xE000..0xE51F`. Для STM32 с `c/set-protocol/pcan_gas` дополнительно включить
|
||||||
|
`src/parallel_nand_pcan_gas.c`, создать `pcan_gas_region_t` и вызвать
|
||||||
|
`parallel_nand_pcan_gas_region_init()`. Этот адаптер намеренно не нужен TMS.
|
||||||
|
Для полного дампа предпочтительны пакетные `READ_REGISTERS/WRITE_REGISTERS`,
|
||||||
|
а classic CAN GAS оставлен для совместимости и коротких диагностических чтений.
|
||||||
|
|
||||||
|
Доступ GUI и алгоритм полного дампа описаны в
|
||||||
|
[docs/MEMORY_GAS.md](docs/MEMORY_GAS.md). Электрические соединения — в README
|
||||||
|
соответствующего порта и [docs/EMPTY_BOARD_PINOUT.md](docs/EMPTY_BOARD_PINOUT.md).
|
||||||
|
|
||||||
|
## PROGRAM и ERASE
|
||||||
|
|
||||||
|
- `parallel_nand_program_page()` записывает полную main-область и, при
|
||||||
|
необходимости, полную OOB-область страницы.
|
||||||
|
- `parallel_nand_erase_block()` стирает один физический блок.
|
||||||
|
- Перед изменением ядро проверяет заводской bad-block marker, на время операции
|
||||||
|
снимает `WP#`, ждёт `R/B#`, проверяет fail-бит статуса и снова включает
|
||||||
|
защиту при любом результате.
|
||||||
|
- Порт обязан объявить `write_supported = 1`; иначе возвращается
|
||||||
|
`PNAND_ERROR_WRITE_PROTECTED`. Это защищает платы, где `WP#` постоянно
|
||||||
|
соединён с GND.
|
||||||
|
- Через GAS разрушительные команды требуют записи `0xA55A` в `CONFIRM`
|
||||||
|
непосредственно перед `PROGRAM_PAGE` или `ERASE_BLOCK`.
|
||||||
|
|
||||||
|
NAND можно программировать только из `1` в `0`; для повторной записи сначала
|
||||||
|
стирается весь блок. Не стирайте заводские bad-блоки и не используйте raw-запись
|
||||||
|
как файловую систему без ECC, wear leveling и защиты загрузочных блоков.
|
||||||
|
|
||||||
|
## Текущие границы
|
||||||
|
|
||||||
|
- Реализованы чтение, программирование страницы, стирание блока, RESET,
|
||||||
|
READ ID, STATUS и проверка bad-block marker.
|
||||||
|
- ECC пока не исправляет данные. GUI получает физические main/OOB и должен
|
||||||
|
помечать дамп как raw.
|
||||||
|
- В полном дампе bad-блоки не пропускаются: сохраняется физический порядок.
|
||||||
|
- Для другой NAND нужно передать другую `parallel_nand_geometry_t` и проверить
|
||||||
|
команды/маркер по её datasheet.
|
||||||
|
|
||||||
|
## Проверка первого запуска
|
||||||
|
|
||||||
|
1. Запустить `parallel_nand_init()`.
|
||||||
|
2. Через GAS проверить `0xE000 = 0x4E44`.
|
||||||
|
3. Записать `2` в `0xE004` (`READ_ID`).
|
||||||
|
4. Проверить `0xE00B = 0x002C`, `0xE00C = 0x00F1`.
|
||||||
|
5. Записать номер страницы в `0xE005/0xE006`, затем `3` в `0xE004`.
|
||||||
|
6. Дождаться `READY | PAGE_VALID` в `0xE002`.
|
||||||
|
7. Прочитать main из `0xE100..0xE4FF`, OOB из `0xE500..0xE51F`.
|
||||||
|
8. PROGRAM/ERASE проверять только на заведомо расходном блоке после сохранения
|
||||||
|
полного raw-дампа.
|
||||||
83
c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md
Normal file
83
c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Подключение parallel NAND на пустой плате
|
||||||
|
|
||||||
|
Документ рассчитан на асинхронную NAND x8 `MT29F1G08ABADA` 3,3 В в корпусе
|
||||||
|
TSOP-48 (суффикс `WP`). На второй фотографии NAND — длинная микросхема справа
|
||||||
|
с маркировкой `29F1G08ABADA`. Микросхема Micron `D9MDK` сверху — DDR2 SDRAM,
|
||||||
|
не NAND. На первой фотографии `AM29LV800BT` — parallel NOR, а не NAND.
|
||||||
|
|
||||||
|
Уточнение по ёмкости с фотографий:
|
||||||
|
|
||||||
|
- `K6R4008V1D` — SRAM 4 Mbit (512 KiB), не Flash;
|
||||||
|
- `AM29LV800BT` — parallel NOR 8 Mbit (1 MiB);
|
||||||
|
- `MT29F1G08ABADA` — parallel NAND 1 Gbit, то есть 128 MiB main area.
|
||||||
|
|
||||||
|
Поэтому текущий NAND-профиль рассчитан на 1-Gbit чип со второй платы. Для
|
||||||
|
другой плотности меняется `parallel_nand_geometry_t`; GAS и GUI-команды
|
||||||
|
остаются теми же.
|
||||||
|
|
||||||
|
Не переносите номера выводов TSOP-48 на вариант `H4` VFBGA-63. Для BGA нужно
|
||||||
|
использовать ball map конкретного полного part number.
|
||||||
|
|
||||||
|
## TSOP-48, x8
|
||||||
|
|
||||||
|
| NAND | Вывод | Назначение |
|
||||||
|
|---|---:|---|
|
||||||
|
| `R/B#` | 7 | open-drain Ready/Busy, подтяжка 4,7–10 кОм к 3,3 В |
|
||||||
|
| `RE#` | 8 | строб чтения от МК |
|
||||||
|
| `CE#` | 9 | выбор кристалла от МК |
|
||||||
|
| `VCC` | 12, 37 | питание 3,3 В |
|
||||||
|
| `VSS` | 13, 36 | земля |
|
||||||
|
| `CLE` | 16 | фиксация команды |
|
||||||
|
| `ALE` | 17 | фиксация адреса |
|
||||||
|
| `WE#` | 18 | строб команды/адреса от МК |
|
||||||
|
| `WP#` | 19 | GPIO МК и pull-down 10 кОм; только для чтения допустим GND через 10 кОм |
|
||||||
|
| `I/O0` | 29 | двунаправленная шина, бит 0 |
|
||||||
|
| `I/O1` | 30 | двунаправленная шина, бит 1 |
|
||||||
|
| `I/O2` | 31 | двунаправленная шина, бит 2 |
|
||||||
|
| `I/O3` | 32 | двунаправленная шина, бит 3 |
|
||||||
|
| `I/O4` | 41 | двунаправленная шина, бит 4 |
|
||||||
|
| `I/O5` | 42 | двунаправленная шина, бит 5 |
|
||||||
|
| `I/O6` | 43 | двунаправленная шина, бит 6 |
|
||||||
|
| `I/O7` | 44 | двунаправленная шина, бит 7 |
|
||||||
|
| `VCC1` | 34, 39 | соединить с 3,3 В для совместимости корпуса/ONFI |
|
||||||
|
| `VSS1` | 25, 48 | соединить с GND для совместимости корпуса/ONFI |
|
||||||
|
| `DNU` | 38, 47 | оставить неподключёнными |
|
||||||
|
| `NC` | остальные | не подключать |
|
||||||
|
|
||||||
|
У каждого VCC/VCC1 поставить 100 нФ на ближайший VSS/VSS1; рядом с NAND —
|
||||||
|
общий 1–4,7 мкФ. Сигналы не должны превышать питание NAND. Для трасс длиннее
|
||||||
|
нескольких сантиметров предусмотреть последовательные резисторы 22–47 Ом у
|
||||||
|
источника на `WE#`, `RE#`, `CLE`, `ALE` и при необходимости на данных.
|
||||||
|
|
||||||
|
## Соединение с МК
|
||||||
|
|
||||||
|
| NAND | TMS320F2812 | STM32F407VET6 | STM32G474CEU6 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `I/O0..7` | `XD0..XD7` | `PD14,PD15,PD0,PD1,PE7..PE10` | `PB0..PB7` |
|
||||||
|
| `CLE` | `XA0` | `PD11/FSMC_A16` | `PA0` |
|
||||||
|
| `ALE` | `XA1` | `PD12/FSMC_A17` | `PA1` |
|
||||||
|
| `WE#` | `XWE` | `PD5/FSMC_NWE` | `PA2` |
|
||||||
|
| `RE#` | `XRD` | `PD4/FSMC_NOE` | `PA3` |
|
||||||
|
| `CE#` | `XZCS6AND7` | `PD7/FSMC_NE2` | `PA4` |
|
||||||
|
| `WP#` | GND через 10 кОм | GND через 10 кОм | `PA5` и pull-down 10 кОм |
|
||||||
|
| `R/B#` | `GPIOE0` | `PD6/FSMC_NWAIT` | `PA6` |
|
||||||
|
|
||||||
|
Подробные номера выводов корпуса МК и ограничения находятся в README каждого
|
||||||
|
каталога `ports/`. Перед изготовлением платы нужно сверить выбранный корпус и
|
||||||
|
полный part number NAND с актуальным datasheet.
|
||||||
|
|
||||||
|
## Первый запуск
|
||||||
|
|
||||||
|
1. Не устанавливать NAND и проверить 3,3 В, отсутствие КЗ и уровни `CE#/WE#/RE#`.
|
||||||
|
2. Установить NAND, проверить pull-down и уровень `WP# = 0`, выполнить RESET `FFh`.
|
||||||
|
3. Прочитать ID `90h`: ожидаемые первые байты для профиля — `2C F1`.
|
||||||
|
4. Считать одну страницу два раза и сравнить main/OOB побайтно.
|
||||||
|
5. Только после устойчивого чтения запускать полный дамп через GAS.
|
||||||
|
6. PROGRAM/ERASE проверять на расходном блоке после сохранения raw-дампа;
|
||||||
|
убедиться осциллографом, что `WP#` поднимается только на время операции.
|
||||||
|
|
||||||
|
## Источники для проверки footprint
|
||||||
|
|
||||||
|
- [Micron: каталог SLC NAND](https://www.micron.com/products/storage/nand-flash/slc-nand/part-catalog)
|
||||||
|
- [MT29F1G08ABADA: signal assignments и геометрия](https://helpdesk.trx.pl/pliki/SERWER/Micron-MT29F1G08ABADAWP-IT%20D-datasheet.pdf)
|
||||||
|
- [Micron FBGA part decoder](https://in.micron.com/sales-support/design-tools/fbga-parts-decoder)
|
||||||
207
c/parallel-nand/docs/MEMORY_GAS.md
Normal file
207
c/parallel-nand/docs/MEMORY_GAS.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Работа с parallel NAND через GAS
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
Внешняя NAND значительно больше общего адресного пространства GAS. Например,
|
||||||
|
`MT29F1G08ABADA` содержит 128 МиБ main area, а GAS имеет только 65536
|
||||||
|
16-битных регистров. Поэтому NAND представлена не линейным диапазоном, а
|
||||||
|
банковым окном одной физической страницы.
|
||||||
|
|
||||||
|
```text
|
||||||
|
SETGUI
|
||||||
|
│ GUI protocol v1: READ_REGISTERS / WRITE_REGISTERS
|
||||||
|
▼
|
||||||
|
карта GAS 0x0000..0xFFFF
|
||||||
|
│ 0xE000: управление, 0xE100: main, 0xE500: OOB
|
||||||
|
▼
|
||||||
|
parallel_nand_gas
|
||||||
|
▼
|
||||||
|
общее ядро parallel_nand
|
||||||
|
├─ TMS320F2812: XINTF
|
||||||
|
├─ STM32F103RC: GPIO
|
||||||
|
├─ STM32F103ZE: FSMC
|
||||||
|
├─ STM32F407VE: FSMC
|
||||||
|
└─ STM32G474CE: GPIO
|
||||||
|
```
|
||||||
|
|
||||||
|
GUI работает одинаково со всеми МК. Тип процессора влияет только на выбранный
|
||||||
|
файл из `ports/`.
|
||||||
|
|
||||||
|
Встроенный проект должен направлять весь диапазон `0xE000..0xE51F` в
|
||||||
|
`parallel_nand_gas_read()` и `parallel_nand_gas_write()`. Их аргументы имеют
|
||||||
|
тип `unsigned int`: на C28x это нативное 16-битное слово, а на ARM функции
|
||||||
|
явно оставляют только младшие 16 бит. Так карта и wire-формат совпадают без
|
||||||
|
предположения, что C `char` всегда восьмибитный.
|
||||||
|
|
||||||
|
Для STM32, использующего карту `pcan_gas`, готов адаптер:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "parallel_nand_pcan_gas.h"
|
||||||
|
|
||||||
|
static pcan_gas_region_t nand_region;
|
||||||
|
parallel_nand_pcan_gas_region_init(&nand_region, &nand_gas);
|
||||||
|
```
|
||||||
|
|
||||||
|
Для TMS320F2812 переносимый обработчик вызывается прямо из существующего GAS
|
||||||
|
диспетчера:
|
||||||
|
|
||||||
|
```c
|
||||||
|
if (address >= nand_gas.base &&
|
||||||
|
address < nand_gas.base + PNAND_GAS_REGION_REGS) {
|
||||||
|
return parallel_nand_gas_read(&nand_gas, address, value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если тип результата диспетчера отличается, статусы `PNAND_GAS_OK`,
|
||||||
|
`PNAND_GAS_NO_REG`, `PNAND_GAS_READ_ONLY` и `PNAND_GAS_REJECTED`
|
||||||
|
преобразуются в его локальные статусы так же, как это сделано в
|
||||||
|
`parallel_nand_pcan_gas.c`.
|
||||||
|
|
||||||
|
## Транспорт до GUI
|
||||||
|
|
||||||
|
Карта GAS не привязана к физическому каналу. Рекомендуемый путь для полного
|
||||||
|
дампа — используемые SETGUI сообщения `WRITE_REGISTERS (0x0A)` и
|
||||||
|
`READ_REGISTERS (0x09)`: они позволяют передавать крупные блоки поверх USB,
|
||||||
|
UART, RS-485 или Ethernet. Адаптер `pcan_gas` нужен, когда теми же регистрами
|
||||||
|
обмениваются по classic CAN; там один кадр несёт только четыре регистра, и
|
||||||
|
полный 128-MiB дамп будет значительно медленнее.
|
||||||
|
|
||||||
|
Устройство не должно собирать весь дамп в RAM. Буферизуется только одна
|
||||||
|
страница. Из-за переносимости на C28x каждый octet хранится в `unsigned short`,
|
||||||
|
поэтому статические буферы GAS занимают примерно 4,2 KiB RAM и на TMS, и на
|
||||||
|
ARM.
|
||||||
|
|
||||||
|
## Карта GAS
|
||||||
|
|
||||||
|
Базовый адрес по умолчанию — `0xE000`. Его можно изменить при вызове
|
||||||
|
`parallel_nand_gas_init()`; все смещения ниже останутся прежними.
|
||||||
|
|
||||||
|
| Адрес | Доступ | Название | Значение |
|
||||||
|
|---:|:---:|---|---|
|
||||||
|
| `E000` | R | `SIGNATURE` | `0x4E44` (`ND`) |
|
||||||
|
| `E001` | R | `VERSION` | версия окна, сейчас 2 |
|
||||||
|
| `E002` | R | `STATUS` | состояние операции |
|
||||||
|
| `E003` | R | `ERROR` | код `PNAND_*`, 0 = нет ошибки |
|
||||||
|
| `E004` | W | `COMMAND` | команда окна |
|
||||||
|
| `E005` | R/W | `PAGE_LO` | младшие 16 бит физической страницы |
|
||||||
|
| `E006` | R/W | `PAGE_HI` | старшие 16 бит физической страницы |
|
||||||
|
| `E007` | R | `PAGE_MAIN` | main bytes, для MT29 = 2048 |
|
||||||
|
| `E008` | R | `PAGE_OOB` | OOB bytes, для MT29 = 64 |
|
||||||
|
| `E009` | R | `PAGES_PER_BLOCK` | для MT29 = 64 |
|
||||||
|
| `E00A` | R | `BLOCKS` | для MT29 = 1024 |
|
||||||
|
| `E00B` | R | `MANUFACTURER_ID` | ожидается `0x2C` |
|
||||||
|
| `E00C` | R | `DEVICE_ID` | ожидается `0xF1` |
|
||||||
|
| `E00D` | R | `BAD_BLOCK` | 1 для физически плохого блока |
|
||||||
|
| `E00E` | R | `GENERATION_LO` | счётчик загруженных страниц, low |
|
||||||
|
| `E00F` | R | `GENERATION_HI` | счётчик загруженных страниц, high |
|
||||||
|
| `E010` | W | `CONFIRM` | `0xA55A` непосредственно перед разрушительной командой |
|
||||||
|
| `E100..E4FF` | R/W | `PAGE_DATA` | 1024 регистра main, два байта LE |
|
||||||
|
| `E500..E51F` | R/W | `PAGE_OOB` | 32 регистра OOB, два байта LE |
|
||||||
|
|
||||||
|
Между управляющими регистрами и окнами данных есть зарезервированные адреса.
|
||||||
|
Чтение блока через них не выполняется: каждый запрос должен начинаться внутри
|
||||||
|
существующего участка.
|
||||||
|
|
||||||
|
## STATUS
|
||||||
|
|
||||||
|
| Бит | Маска | Смысл |
|
||||||
|
|---:|---:|---|
|
||||||
|
| 0 | `0x0001` | `READY` |
|
||||||
|
| 1 | `0x0002` | `BUSY` |
|
||||||
|
| 2 | `0x0004` | `PAGE_VALID` |
|
||||||
|
| 3 | `0x0008` | `BAD_BLOCK` |
|
||||||
|
| 4 | `0x0010` | `WRITE_SUPPORTED`, порт способен управлять `WP#` |
|
||||||
|
| 5 | `0x0020` | `BUFFER_DIRTY`, буфер изменён после чтения/очистки |
|
||||||
|
| 15 | `0x8000` | `ERROR`, подробность в `E003` |
|
||||||
|
|
||||||
|
GUI читает окно данных только при `READY=1`, `PAGE_VALID=1`, `ERROR=0`.
|
||||||
|
|
||||||
|
## COMMAND
|
||||||
|
|
||||||
|
| Значение | Команда | Результат |
|
||||||
|
|---:|---|---|
|
||||||
|
| 1 | `RESET` | сброс NAND и ожидание Ready |
|
||||||
|
| 2 | `READ_ID` | обновляет `E00B/E00C` |
|
||||||
|
| 3 | `READ_PAGE` | загружает main/OOB выбранной страницы |
|
||||||
|
| 4 | `CLEAR_BUFFER` | заполняет main/OOB значением `0xFF` для подготовки записи |
|
||||||
|
| 5 | `PROGRAM_PAGE` | программирует выбранную страницу из буфера |
|
||||||
|
| 6 | `ERASE_BLOCK` | стирает блок, содержащий выбранную страницу |
|
||||||
|
|
||||||
|
Запись номера страницы и запуск — две отдельные операции. Сначала GUI пишет
|
||||||
|
`PAGE_LO/PAGE_HI`, затем записывает `READ_PAGE` в `COMMAND`. Нельзя отправлять
|
||||||
|
один блок, начинающийся с `COMMAND`: команда выполнилась бы до обновления номера
|
||||||
|
страницы.
|
||||||
|
|
||||||
|
## Последовательность полного дампа
|
||||||
|
|
||||||
|
1. Прочитать `E000..E00F`, проверить подпись, версию и геометрию.
|
||||||
|
2. Выполнить `READ_ID`; для микросхемы с фотографии проверить `2C F1`.
|
||||||
|
3. Рассчитать `page_count = PAGES_PER_BLOCK * BLOCKS`.
|
||||||
|
4. Для каждой физической страницы от 0 до `page_count - 1`:
|
||||||
|
- записать номер страницы в `E005/E006`;
|
||||||
|
- записать `3` в `E004`;
|
||||||
|
- опрашивать `E002`, пока не установлен `READY`;
|
||||||
|
- проверить `ERROR`, `BAD_BLOCK` и изменение `GENERATION`;
|
||||||
|
- прочитать `E100..E4FF`;
|
||||||
|
- прочитать `E500..E51F`;
|
||||||
|
- сразу дописать данные и метаданные в файлы.
|
||||||
|
5. Не пропускать bad-блоки: иначе файл потеряет соответствие физическим
|
||||||
|
страницам микросхемы.
|
||||||
|
|
||||||
|
GUI protocol v1 допускает payload до 512 байт. Ответ `READ_REGISTERS` содержит
|
||||||
|
четырёхбайтовый заголовок диапазона, поэтому за один запрос следует читать не
|
||||||
|
более 254 GAS-регистров. Страница main читается пятью запросами: четыре раза по
|
||||||
|
254 регистра и последний раз 8 регистров. OOB читается одним запросом на 32
|
||||||
|
регистра.
|
||||||
|
|
||||||
|
## Возобновление
|
||||||
|
|
||||||
|
GUI хранит номер последней полностью записанной страницы. После разрыва связи
|
||||||
|
он повторно проверяет подпись/ID/геометрию и продолжает со следующей страницы.
|
||||||
|
Страница считается готовой только после записи main, OOB и записи её статуса в
|
||||||
|
метаданные.
|
||||||
|
|
||||||
|
Рекомендуемые выходные файлы:
|
||||||
|
|
||||||
|
- `dump.bin` — 2048 байт main каждой физической страницы;
|
||||||
|
- `dump.oob` — 64 байта OOB каждой физической страницы;
|
||||||
|
- `dump.json` — ID, геометрия, bad-блоки, ошибки и последняя готовая страница.
|
||||||
|
|
||||||
|
Для `MT29F1G08ABADA` размеры завершённого дампа: `dump.bin` = 134217728 байт,
|
||||||
|
`dump.oob` = 4194304 байта.
|
||||||
|
|
||||||
|
## ECC
|
||||||
|
|
||||||
|
Текущая версия шаблона отдаёт raw main/OOB и не заявляет исправление ошибок.
|
||||||
|
Статус bad-блока определяется по первому байту OOB первых двух страниц блока.
|
||||||
|
Перед использованием дампа как рабочего образа нужно добавить ECC-политику для
|
||||||
|
точного чипа и формата OOB. Само окно GAS при этом менять не требуется: можно
|
||||||
|
добавить новые флаги состояния в свободные биты `STATUS`.
|
||||||
|
|
||||||
|
## PROGRAM/ERASE
|
||||||
|
|
||||||
|
Разрушительные команды доступны только если установлен флаг
|
||||||
|
`WRITE_SUPPORTED`. Ядро проверяет bad-block marker и отказывается изменять
|
||||||
|
заводской плохой блок. `WP#` снимается только внутри операции и включается
|
||||||
|
обратно после проверки status fail-бита, тайм-аута или другой ошибки.
|
||||||
|
|
||||||
|
Для программирования очищенной страницы:
|
||||||
|
|
||||||
|
1. Записать `PAGE_LO/PAGE_HI`.
|
||||||
|
2. Выполнить `CLEAR_BUFFER`.
|
||||||
|
3. Записать необходимые main/OOB-регистры. Незаписанные байты останутся
|
||||||
|
`0xFF`; OOB лучше не менять без определённой ECC-разметки.
|
||||||
|
4. Записать `0xA55A` в `CONFIRM`.
|
||||||
|
5. Следующей записью без промежуточных операций отправить `PROGRAM_PAGE`.
|
||||||
|
6. Проверить `READY`, отсутствие `ERROR` и изменение `GENERATION`.
|
||||||
|
|
||||||
|
Для стирания записать любую страницу целевого блока в `PAGE_LO/PAGE_HI`, затем
|
||||||
|
`0xA55A` в `CONFIRM` и сразу `ERASE_BLOCK`. Изменение страницы, буфера или
|
||||||
|
другого управляющего регистра сбрасывает подтверждение. После успешного
|
||||||
|
стирания `PAGE_VALID` очищается.
|
||||||
|
|
||||||
|
Повторное программирование не может превратить `0` обратно в `1`: для этого
|
||||||
|
требуется стирание всего блока. Перед первым тестом сохраните полный raw-дамп,
|
||||||
|
исключите загрузочные блоки и используйте заведомо расходный исправный блок.
|
||||||
|
Библиотека не реализует ECC, wear leveling, журналирование и восстановление
|
||||||
|
после пропадания питания.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user