34 Commits

Author SHA1 Message Date
1296773346 feat(protocan): decode KONOR runtime settings 2026-09-01 21:32:56 +03:00
af82687e42 Добавить переносимый планировщик PULSE 2026-09-01 20:54:58 +03:00
e9ec078b38 Объединить SETProtocol v2 и общие кодеки (#1)
Reviewed-on: #1
2026-09-01 20:43:49 +03:00
43410c3f12 Merge origin/master into SETProtocol v2 integration branch 2026-09-01 20:41:07 +03:00
076c51c083 fix(python): support library lookup on Python 3.8 2026-09-01 20:31:18 +03:00
e9c5ec90b8 refactor(python): centralize ProtoCAN decoder 2026-09-01 12:00:53 +03:00
00a0f944c4 refactor(python): centralize SETProtocol v2 codec 2026-09-01 11:56:24 +03:00
9c0701ab9b refactor(android): centralize Kotlin protocol codecs 2026-09-01 11:54:35 +03:00
9b636a9f40 feat(setprotocol): define system payload schemas 2026-09-01 11:47:08 +03:00
19becd7b8c refactor: merge protocol cores as SETProtocol 2026-09-01 09:58:35 +03:00
5504104cc5 feat(protocan): вынеси GUI-кадрирование в общий ABI 2026-08-31 21:02:23 +03:00
e6bea52db0 fix(protocan): держи import library в build-каталоге 2026-08-31 20:58:52 +03:00
92fc2ade58 docs(protocan): перенеси канонический протокол из SETCAN 2026-08-31 20:55:04 +03:00
080b6900f5 feat(protocan): добавь JNI-разбор идентификатора 2026-08-31 20:50:40 +03:00
68040a2803 fix(protocan): исправь вызов MSVC из host builder 2026-08-31 20:48:17 +03:00
7fe76b3c3b build(protocan): добавь host-сборку без CMake 2026-08-31 20:48:01 +03:00
0f77a510e6 feat(protocan): открой разбор ID и статистику FFI 2026-08-31 20:46:52 +03:00
7910a07f2f feat(protocan): добавь общий ABI для GUI 2026-08-31 20:45:34 +03:00
2fd51a1d7e docs: move SETCAN documentation into templates 2026-08-31 19:30:54 +03:00
de180750aa feat(can-sensor): add STM32F1 bxCAN port 2026-08-31 18:50:52 +03:00
648457e587 protocan-boot: support single-slot targets 2026-08-31 01:06:16 +03:00
5f3a9879d5 Add C28x RS boot ports for F2812 and F28335 2026-08-30 15:30:08 +03:00
9ce32b56d4 Add reusable firmware info module 2026-08-30 07:01:59 +03:00
05863db2a2 docs: add Windows HTML build script 2026-08-30 06:12:50 +03:00
4b32cda46e docs: add HTML library catalog 2026-08-30 03:44:36 +03:00
217073982f feat(rs485-boot): add STM32F103 and G474 ports 2026-08-30 01:37:12 +03:00
94c991380b docs(porting): опиши структуру проектов STM32 2026-08-30 01:13:43 +03:00
5a8724da6d docs(protocan-boot): уточни границы блоков слота 2026-08-30 01:13:42 +03:00
d507b84a76 Обновить Python-декодер ProtoCAN Boot 2026-08-29 22:18:53 +03:00
d3bb634fb1 feat(protocan-boot): добавь прошивку приборов по CAN 2026-08-29 17:11:53 +03:00
a1d2d05f42 тест 2026-08-26 21:01:51 +03:00
1345766efb Обновить can-sensor для SETCAN SETTINGS 2026-08-26 20:47:59 +03:00
085eb3c8bd feat: add unified SET protocol v2 2026-08-24 19:19:32 +03:00
033c7ab9e8 feat(ds18b20): порты 1-Wire для STM32G4 и STM32F4
Добавлены реализации низкоуровневой шины для STM32G431/G474 и
STM32F407VET6 с шаблонами onewire_config.h под каждый камень.
Логика повторяет порт STM32F1: открытый сток, задержки по DWT,
запрет прерываний на один тайм-слот.

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

2
.gitignore vendored
View File

@@ -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/

100
NEW_PROJECT.md Normal file
View File

@@ -0,0 +1,100 @@
# Новый проект на переносимых библиотеках
Этот репозиторий содержит два разных типа кода:
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.
Рабочий пример полноценного 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 к дисплею |
| EEPROM FT24C256 | `c/eeprom-ft24c256` | I2C write/write-read и задержка |
| CAN SETTINGS | `c/can-sensor` | bxCAN для F103 либо FDCAN для G431/G474 |
Порт дисплея, EEPROM и CAN нельзя честно сделать только по названию МК:
нужно знать экземпляр периферии, альтернативную функцию и выводы конкретной
платы. Поэтому библиотеки принимают таблицы обратных вызовов, а короткие
адаптеры лежат в проекте рядом с `app_config.h`.
## Порядок создания проекта
1. Создать проект в CubeMX/Keil либо добавить CMSIS, startup и linker script.
2. Подключить `templates` сабмодулем.
3. Добавить только ядра нужных библиотек.
4. Выбрать готовый 1-Wire порт по таблице выше.
5. Создать `board/app_config.h` с выводами и частотами.
6. Реализовать короткие адаптеры SPI/GPIO/I2C/CAN в `port/`.
7. Сначала прогнать host-тесты библиотек, затем проверить аппаратные порты
логическим анализатором или осциллографом.
Так новый проект не является копией старого: он использует общие ядра одной
версии, а аппаратные отличия видны в одном небольшом каталоге `port/`.

View File

@@ -15,6 +15,12 @@ templates/
python/ модули на чистом Python 3.9+, только stdlib python/ модули на чистом Python 3.9+, только stdlib
``` ```
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
и STM32G474 описаны в [`NEW_PROJECT.md`](NEW_PROJECT.md).
Нормативная документация SETCAN/ProtoCAN, реестр общего адресного пространства
и исходный Excel собраны в [`doc/setcan`](doc/setcan/README.md).
## Что лежит ## Что лежит
### C ### C
@@ -25,10 +31,13 @@ templates/
| [`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/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-порты в комплекте** |
| [`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 |
### Python ### Python
@@ -36,9 +45,13 @@ templates/
|---|---|---| |---|---|---|
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ | | [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
Кодировщики `c/protocan-transport` и `python/protocan` дают побайтово Кодировщики `c/set-protocol` и `python/protocan` дают побайтово
одинаковый результат — это зафиксировано эталонами в тестах на C. одинаковый результат — это зафиксировано эталонами в тестах на C.
Интерактивная документация общего ядра: [`doc/setprotocol.html`](doc/setprotocol.html).
В ней отдельно описаны граница ядра, ABI, память, три wire format и состояние
портов Windows, Android, Linux и MCU.
## Как подключить к проекту ## Как подключить к проекту
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде: **Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
@@ -69,10 +82,12 @@ 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 |
## Что сюда не попало и почему ## Что сюда не попало и почему

View File

@@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.13)
project(can_sensor C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_library(can_sensor STATIC can_sensor.c)
target_include_directories(can_sensor PUBLIC .)
if(MSVC)
target_compile_options(can_sensor PRIVATE /W4)
else()
target_compile_options(can_sensor PRIVATE -Wall -Wextra -Wpedantic)
endif()
option(CAN_SENSOR_BUILD_TESTS "Собирать тесты транспорта CAN sensor" ON)
if(CAN_SENSOR_BUILD_TESTS)
enable_testing()
add_executable(test_can_sensor tests/test_can_sensor.c)
target_link_libraries(test_can_sensor PRIVATE can_sensor)
add_test(NAME can_sensor COMMAND test_can_sensor)
endif()

View File

@@ -1,27 +1,28 @@
# can-sensor # 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):

View File

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

View File

@@ -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 */

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,79 @@
#include "can_sensor.h"
#include <stdio.h>
typedef struct {
CanSensor_Frame last_frame;
uint32_t send_count;
uint8_t accept;
} FakeCan;
static uint8_t fake_send(void *context, const CanSensor_Frame *frame)
{
FakeCan *fake = (FakeCan *)context;
fake->send_count++;
fake->last_frame = *frame;
return fake->accept;
}
#define CHECK(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \
#condition); \
return 1; \
} \
} while (0)
int main(void)
{
CanSensor link;
CanSensor_Config config;
CanSensor_Io io;
FakeCan fake = {0};
fake.accept = 1U;
io.send = fake_send;
io.receive = 0;
io.context = &fake;
CanSensor_ConfigDefault(&config);
CHECK(config.pulse_period_ms == 0U);
CHECK(CanSensor_Init(&link, &io, &config) != 0U);
/* Нулевой период означает выключенный PULSE при любых метках времени. */
CanSensor_Task(&link, 0U);
CanSensor_Task(&link, 0xFFFFFFFFUL);
CHECK(fake.send_count == 0U);
CanSensor_SetPulsePeriod(&link, 1000U);
CHECK(CanSensor_PulsePeriod(&link) == 1000U);
CanSensor_Task(&link, 500U);
CanSensor_Task(&link, 1499U);
CHECK(fake.send_count == 0U);
CanSensor_Task(&link, 1500U);
CHECK(fake.send_count == 1U);
CHECK(fake.last_frame.id == 0x1FFF0000UL);
CHECK(fake.last_frame.extended == 1U);
CHECK(fake.last_frame.length == 1U);
CHECK(fake.last_frame.data[0] == 1U);
/* После period=0 дальнейшие вызовы не отправляют heartbeat. */
CanSensor_SetPulsePeriod(&link, 0U);
CHECK(CanSensor_PulsePeriod(&link) == 0U);
CanSensor_Task(&link, 0xFFFFFFFFUL);
CHECK(fake.send_count == 1U);
/* Повторное включение начинает новый полный период. */
CanSensor_SetPulsePeriod(&link, 10U);
CanSensor_Task(&link, 2000U);
CanSensor_Task(&link, 2009U);
CHECK(fake.send_count == 1U);
CanSensor_Task(&link, 2010U);
CHECK(fake.send_count == 2U);
CHECK(fake.last_frame.data[0] == 2U);
puts("can_sensor pulse tests passed");
return 0;
}

View File

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

View File

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

View File

@@ -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 и положите на
* путь поиска заголовков. Библиотека сама этот файл не подставляет: * путь поиска заголовков. Библиотека сама этот файл не подставляет:

View File

@@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
# PORTING
Аппаратно-зависимый контракт ограничен `firmware_info_config.h`:
`FIRMWARE_VERSION_MAJOR`, `FIRMWARE_VERSION_MINOR`, `FIRMWARE_VERSION_PATCH` и
необязательный `FIRMWARE_BUILD_ID`. Выберите порт по семейству МК или создайте
его копию. Публикация — ответственность транспорта: 12 `uint16_t` для Modbus
либо 24 little-endian байта для SETGUI.
При переносе проверьте: поддержку `__DATE__`/`__TIME__`, наличие generated в
include path, запуск генератора до компиляции и декодирование build ID как двух
ASCII-байтов в каждом логическом слове.

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.13)
project(protocan_boot C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_library(protocan_boot STATIC src/pcan_boot.c)
target_include_directories(protocan_boot PUBLIC include)
if(MSVC)
target_compile_options(protocan_boot PRIVATE /W4)
else()
target_compile_options(protocan_boot PRIVATE -Wall -Wextra -Wpedantic)
endif()
option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON)
if(PCAN_BOOT_BUILD_TESTS)
enable_testing()
add_executable(test_pcan_boot tests/test_pcan_boot.c)
target_link_libraries(test_pcan_boot PRIVATE protocan_boot)
add_test(NAME pcan_boot COMMAND test_pcan_boot)
endif()

179
c/protocan-boot/README.md Normal file
View File

@@ -0,0 +1,179 @@
# ProtoCAN Boot
Каноническое описание обмена загрузчика перенесено из SETCAN в
[`docs/BOOTLOADER.md`](docs/BOOTLOADER.md). Реализация и документ меняются
вместе в этом переносимом модуле.
`protocan-boot` — переносимое C99-ядро адресной прошивки приборов по classic
CAN 2.0B и 29-битному ProtoCAN ID. Оно реализует сессию обновления, два
логических слота по 512 КиБ либо single-slot обновление, последовательную запись 8-байтовых блоков,
CRC32, проверку совместимости, продолжение по номеру следующего блока и
безопасный выбор неактивного слота.
Это не готовый загрузчик конкретного STM32: внутри нет HAL, регистров Flash,
linker script, обработчиков прерываний, криптографии и перехода в приложение.
Эти операции предоставляет проект через таблицу callbacks.
Для STM32G474VET6 из `candleLight_fw` задаются `slot_count = 1` и
`max_image_size = 480 * 1024`. В этом режиме запись идёт в единственную
область приложения, callback `set_pending_slot` не требуется, а аппаратный
rollback недоступен. Значение `slot_count = 0` сохраняет прежний A/B-режим.
## Слои
```text
приложение / CAN RX loop
|
v
pcan_boot_process() — протокол и state machine
|
v
pcan_boot_port_t — Flash, политика подписи, commit, reboot
|
v
STM32 HAL / другой MCU / host-тест
```
## Файлы
| Файл | Назначение | Зависимости |
|---|---|---|
| `include/pcan_boot.h` | публичный API, команды и структуры | C99 stdint/stdbool |
| `src/pcan_boot.c` | ProtoCAN ID, state machine и CRC32 | только публичный заголовок |
| `tests/test_pcan_boot.c` | хостовые тесты с RAM вместо Flash | стандартная библиотека C |
| `CMakeLists.txt` | сборка библиотеки и теста | CMake 3.13+ |
Модуль не зависит от legacy-модулей `set-protocol`: разбор пяти полей 29-битного ID
сделан переносимыми сдвигами, без непереносимых C-битовых полей.
## Карта `MsgType`
| Значение | Имя | Назначение |
|---:|---|---|
| `0x9` | `BOOT_CONTROL` | команды и метаданные |
| `0xA` | `BOOT_DATA_A` | блоки слота A |
| `0xB` | `BOOT_DATA_B` | блоки слота B |
| `0xC` | `BOOT_STATUS` | ответы и прогресс |
| `0xD` | `BOOT_DISCOVERY` | идентификация |
Для `BOOT_DATA_A/B` поле `MsgBody` — номер блока `0..65535`, а payload —
8 байт образа:
```text
offset = MsgBody * 8
512 КиБ = 512 * 1024 = 524 288 байт
524 288 / 8 = 65 536 блоков на слот
```
| `MsgBody` | Номер блока | Смещение | Байты слота |
|---:|---:|---:|---:|
| `0x0000` | 0 | `0x00000` | `0x00000..0x00007` |
| `0x0001` | 1 | `0x00008` | `0x00008..0x0000F` |
| `0xFFFF` | 65 535 | `0x7FFF8` | `0x7FFF8..0x7FFFF` |
Последний блок заканчивается на смещении `0x7FFFF`. Значение `0x80000`
равно размеру слота и является первой позицией за его границей. В коде эти
границы заданы константами `PCAN_BOOT_BLOCK_COUNT=65536` и
`PCAN_BOOT_SLOT_SIZE=524288`.
Последний кадр дополняется `0xFF`; ядро передаёт во Flash только оставшиеся
байты фактического образа и включает в CRC только их.
## Контракт порта
```c
typedef struct {
bool (*send)(void *, uint32_t can_id, const uint8_t *, uint8_t dlc);
bool (*erase_slot)(void *, uint8_t slot);
bool (*write_slot)(void *, uint8_t slot, uint32_t offset,
const uint8_t *, uint8_t length);
bool (*authorize)(void *, const pcan_boot_manifest_t *);
bool (*verify_image)(void *, uint8_t slot,
const pcan_boot_manifest_t *);
bool (*set_pending_slot)(void *, uint8_t slot,
const pcan_boot_manifest_t *);
bool (*confirm_running_slot)(void *);
void (*reboot)(void *);
} pcan_boot_port_t;
```
Обязательны `send`, `erase_slot`, `write_slot` и `set_pending_slot`.
`authorize` проверяет политику до стирания (тип, версия, anti-rollback).
`verify_image` выполняет платформенную проверку подписанного контейнера после
CRC32. Если callbacks отсутствуют, соответствующие дополнительные проверки
пропускаются; для серийного изделия их следует реализовать.
`set_pending_slot` должен атомарно сохранить boot metadata. До подтверждения
нового приложения загрузчик сохраняет возможность отката. `confirm_running_slot`
снимает pending-флаг после самопроверки приложения.
## Быстрый старт
```c
pcan_boot_t boot;
pcan_boot_config_t cfg = {
.device_type = 3, .device_id = 5, .product_type = 0x1234,
.hardware_revision = 2, .firmware_version = 0x01020000,
.active_slot = 0, .ack_window = 16
};
pcan_boot_port_t port = {
.send = can_send, .erase_slot = flash_erase_slot,
.write_slot = flash_write_slot, .authorize = image_authorize,
.verify_image = image_verify_signature,
.set_pending_slot = boot_set_pending, .reboot = system_reboot
};
pcan_boot_init(&boot, &cfg, &port, &board);
/* Из CAN RX-задачи, не из длительного Flash-прерывания: */
pcan_boot_process(&boot, rx.ExtId, rx.Data, rx.DLC);
```
## Последовательность обновления
1. `IDENTIFY` или discovery получает тип и версии прибора.
2. `ENTER_BOOT` открывает ненулевой `SessionID`.
3. `BEGIN_IMAGE` передаёт размер и CRC32.
4. `BEGIN_COMPAT` передаёт тип изделия, диапазон HW и версию FW.
5. Ядро выбирает неактивный слот и отвечает его номером.
6. `ERASE` вызывает порт стирания.
7. `BOOT_DATA_A` либо `BOOT_DATA_B` передаются строго по порядку.
8. `VERIFY` сравнивает CRC32 и вызывает проверку образа/подписи порта.
9. `COMMIT` атомарно помечает слот как pending.
10. После reboot приложение подтверждает запуск через `CONFIRM`.
Все изменяющие Flash команды адресуются конкретному `DeviceType/DeviceID`.
Ядро не принимает broadcast-запись и игнорирует кадры с чужим адресом или
`Route=FROM_DEVICE`.
## State machine
```text
IDLE -> ENTER_BOOT -> METADATA -> BEGIN_IMAGE + BEGIN_COMPAT
-> READY_TO_ERASE -> ERASE -> RECEIVING -> VERIFY -> VERIFIED
-> COMMIT -> REBOOT -> CONFIRM
```
При ошибке Flash, CRC, совместимости или подписи состояние переходит в
`FAILED`. Новая сессия начинается командой `ENTER_BOOT`; `ABORT` очищает
текущую сессию без изменения Flash.
## Сборка тестов
```sh
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
```
## Что остаётся проекту устройства
- зарезервировать bootloader, slot A/B и metadata в linker script;
- реализовать выравнивание и размеры страниц Flash;
- не выполнять длительное стирание непосредственно в CAN IRQ;
- атомарно хранить active/pending/confirmed и счётчик попыток запуска;
- проверить вектор, границы и подпись образа;
- включить watchdog и rollback при неподтверждённом запуске;
- согласовать фильтры CAN для `MsgType=0x9..0xD`.
Проект пока не подключён ни к одной конкретной плате; это самостоятельный
шаблон для интеграции в будущие загрузчики приборов SET.

View File

@@ -0,0 +1,183 @@
# ProtoCAN Boot Protocol
Статус: **Draft**
Версия протокола: **1.0**
Совместимость: **classic CAN 2.0B, Extended ID, DLC 0…8**
Реализация: [`templates/c/protocan-boot`](../../../templates/c/protocan-boot)
## Назначение
Сервис обновляет адресованный прибор по CAN и поддерживает два логических
слота A/B. Активный слот не стирается: новый образ записывается в неактивный,
проверяется и атомарно назначается кандидатом на запуск.
## Карта сообщений
| `MsgType` | Имя | `MsgBody` | Payload |
|---:|---|---|---|
| `0x9` | `BOOT_CONTROL` | `SessionID[15:8] \| Command[7:0]` | параметры команды |
| `0xA` | `BOOT_DATA_A` | `BlockIndex[15:0]` | 8 байт слота A |
| `0xB` | `BOOT_DATA_B` | `BlockIndex[15:0]` | 8 байт слота B |
| `0xC` | `BOOT_STATUS` | `SessionID[15:8] \| Command[7:0]` | статус и прогресс |
| `0xD` | `BOOT_DISCOVERY` | подтип ответа | идентификация |
Все команды записи адресуются конкретному `DeviceType/DeviceID` и имеют
`Route=0`. Ответы сохраняют адрес прибора и имеют `Route=1`.
## Адресация образа
`MsgBody` кадра данных — номер 8-байтового блока:
```c
offset = (uint32_t)BlockIndex * 8U;
address = SLOT_X_BASE + offset;
```
```text
512 КиБ = 524 288 байт
524 288 / 8 = 65 536 блоков
BlockIndex = 0x0000…0xFFFF
```
| `BlockIndex` | Смещение | Диапазон байтов |
|---:|---:|---:|
| `0x0000` | `0x00000` | `0x00000…0x00007` |
| `0x0001` | `0x00008` | `0x00008…0x0000F` |
| `0xFFFF` | `0x7FFF8` | `0x7FFF8…0x7FFFF` |
`0x80000` является первой позицией за границей слота. Последний кадр
дополняется `0xFF`, но CRC32 вычисляется только по `ImageSize` байтам.
## Команды `BOOT_CONTROL`
| Код | Команда | DLC | Payload | Допустимое состояние |
|---:|---|---:|---|---|
| `0x01` | `IDENTIFY` | 0 | отсутствует | любое |
| `0x02` | `ENTER_BOOT` | 0 | отсутствует | любое; `SessionID != 0` |
| `0x03` | `BEGIN_IMAGE` | 8 | размер и CRC32 | metadata |
| `0x04` | `BEGIN_COMPAT` | 8 | совместимость и версия | metadata |
| `0x05` | `ERASE` | 0 | отсутствует | ready-to-erase |
| `0x06` | `VERIFY` | 0 | отсутствует | образ получен |
| `0x07` | `COMMIT` | 0 | отсутствует | verified |
| `0x08` | `CONFIRM` | 0 | отсутствует | запущенное приложение |
| `0x09` | `REBOOT` | 0 | отсутствует | активная сессия |
| `0x0A` | `ABORT` | 0 | отсутствует | активная сессия |
| `0x0B` | `QUERY_PROGRESS` | 0 | отсутствует | активная сессия |
### `BEGIN_IMAGE`
```text
DATA[0..3] ImageSize, uint32 little-endian
DATA[4..7] ImageCRC32, uint32 little-endian
```
### `BEGIN_COMPAT`
```text
DATA[0..1] ProductType, uint16 little-endian
DATA[2] HardwareRevisionMin
DATA[3] HardwareRevisionMax
DATA[4..7] FirmwareVersion, uint32 little-endian
```
До `ERASE` прибор обязан получить обе части метаданных и проверить размер,
тип изделия, аппаратную ревизию, версию и политику anti-rollback.
## `BOOT_STATUS`
```text
MsgBody[15..8] SessionID
MsgBody[7..0] команда, на которую дан ответ
DATA[0] Status
DATA[1] TargetSlot: 0=A, 1=B, 0xFF=не выбран
DATA[2..3] NextBlock, uint16 little-endian
DATA[4..7] RunningCRC32, uint32 little-endian
```
| Код | Статус | Повтор допустим |
|---:|---|---|
| `0x00` | `OK` | — |
| `0x01` | `BUSY` | да, после задержки |
| `0x02` | `INVALID_COMMAND` | после исправления |
| `0x03` | `WRONG_DEVICE` | нет для этого образа |
| `0x04` | `WRONG_HARDWARE` | нет для этого образа |
| `0x05` | `INVALID_SIZE` | нет для этого образа |
| `0x06` | `CRC_ERROR` | новая передача |
| `0x07` | `FLASH_ERROR` | зависит от платформы |
| `0x08` | `SEQUENCE_ERROR` | да, с `NextBlock` |
| `0x09` | `SIGNATURE_ERROR` | нет |
| `0x0A` | `SESSION_ERROR` | открыть новую сессию |
| `0x0B` | `VOLTAGE_ERROR` | да после нормализации питания |
| `0x0C` | `INVALID_STATE` | выполнить правильный переход |
## State machine
```text
IDLE
└─ ENTER_BOOT ─> METADATA
├─ BEGIN_IMAGE
└─ BEGIN_COMPAT
v
READY_TO_ERASE
│ ERASE
v
RECEIVING
│ VERIFY
v
VERIFIED
│ COMMIT
v
PENDING + REBOOT
│ CONFIRM
v
CONFIRMED
```
Ошибка Flash, CRC, совместимости или подписи переводит сессию в `FAILED`.
Новая `ENTER_BOOT` создаёт чистую сессию. `ABORT` прекращает текущую передачу,
не активируя частично записанный слот.
## Надёжность и повторы
- Блоки передаются строго по возрастанию `BlockIndex`.
- Дубликат или пропуск возвращает `SEQUENCE_ERROR` и ожидаемый `NextBlock`.
- Базовый режим подтверждает каждый блок.
- Рабочий режим может подтверждать окно из 16 блоков.
- После потери связи `QUERY_PROGRESS` возвращает следующий ожидаемый блок,
пока состояние загрузчика сохранено.
- Для продолжения после перезагрузки порт должен сохранять session metadata
и восстановить её при инициализации; ядро версии 1.0 само это не делает.
## Безопасность и A/B-обновление
```text
active=A -> target=B -> verify -> pending=B
active=B -> target=A -> verify -> pending=A
```
CRC32 защищает только от случайного повреждения. Серийный загрузчик должен
дополнительно проверить подпись контейнера, границы вектора, совместимость и
anti-rollback. Bootloader не обновляется командами `BOOT_DATA_A/B`.
Boot metadata должна атомарно хранить:
- активный слот;
- pending-слот;
- подтверждение запуска;
- число неудачных попыток;
- версию и CRC32 образа.
Если приложение не выполняет `CONFIRM` за установленное число запусков,
загрузчик возвращается к предыдущему подтверждённому слоту.
## Эталонный сценарий
1. ПМ адресно отправляет `IDENTIFY`.
2. ПМ открывает ненулевой `SessionID` командой `ENTER_BOOT`.
3. ПМ отправляет `BEGIN_IMAGE` и `BEGIN_COMPAT`.
4. Прибор сообщает выбранный неактивный слот.
5. ПМ выполняет `ERASE` и передаёт `BOOT_DATA_A` либо `BOOT_DATA_B`.
6. ПМ выполняет `VERIFY`, затем `COMMIT` и `REBOOT`.
7. Новое приложение после самопроверки выполняет `CONFIRM`.

View File

@@ -0,0 +1,147 @@
#ifndef PCAN_BOOT_H
#define PCAN_BOOT_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PCAN_BOOT_VERSION 1U
#define PCAN_BOOT_BLOCK_SIZE 8U
#define PCAN_BOOT_BLOCK_COUNT 65536UL
#define PCAN_BOOT_SLOT_SIZE (PCAN_BOOT_BLOCK_COUNT * PCAN_BOOT_BLOCK_SIZE)
#define PCAN_BOOT_SLOT_NONE 0xFFU
#define PCAN_BOOT_MSG_CONTROL 0x9U
#define PCAN_BOOT_MSG_DATA_A 0xAU
#define PCAN_BOOT_MSG_DATA_B 0xBU
#define PCAN_BOOT_MSG_STATUS 0xCU
#define PCAN_BOOT_MSG_DISCOVERY 0xDU
#define PCAN_BOOT_ROUTE_FROM_PM 0U
#define PCAN_BOOT_ROUTE_FROM_DEVICE 1U
typedef enum {
PCAN_BOOT_CMD_IDENTIFY = 0x01U,
PCAN_BOOT_CMD_ENTER_BOOT = 0x02U,
PCAN_BOOT_CMD_BEGIN_IMAGE = 0x03U,
PCAN_BOOT_CMD_BEGIN_COMPAT = 0x04U,
PCAN_BOOT_CMD_ERASE = 0x05U,
PCAN_BOOT_CMD_VERIFY = 0x06U,
PCAN_BOOT_CMD_COMMIT = 0x07U,
PCAN_BOOT_CMD_CONFIRM = 0x08U,
PCAN_BOOT_CMD_REBOOT = 0x09U,
PCAN_BOOT_CMD_ABORT = 0x0AU,
PCAN_BOOT_CMD_QUERY_PROGRESS = 0x0BU
} pcan_boot_command_t;
typedef enum {
PCAN_BOOT_STATUS_OK = 0x00U,
PCAN_BOOT_STATUS_BUSY = 0x01U,
PCAN_BOOT_STATUS_INVALID_COMMAND = 0x02U,
PCAN_BOOT_STATUS_WRONG_DEVICE = 0x03U,
PCAN_BOOT_STATUS_WRONG_HARDWARE = 0x04U,
PCAN_BOOT_STATUS_INVALID_SIZE = 0x05U,
PCAN_BOOT_STATUS_CRC_ERROR = 0x06U,
PCAN_BOOT_STATUS_FLASH_ERROR = 0x07U,
PCAN_BOOT_STATUS_SEQUENCE_ERROR = 0x08U,
PCAN_BOOT_STATUS_SIGNATURE_ERROR = 0x09U,
PCAN_BOOT_STATUS_SESSION_ERROR = 0x0AU,
PCAN_BOOT_STATUS_VOLTAGE_ERROR = 0x0BU,
PCAN_BOOT_STATUS_INVALID_STATE = 0x0CU
} pcan_boot_status_t;
typedef enum {
PCAN_BOOT_STATE_IDLE = 0,
PCAN_BOOT_STATE_METADATA,
PCAN_BOOT_STATE_READY_TO_ERASE,
PCAN_BOOT_STATE_RECEIVING,
PCAN_BOOT_STATE_VERIFIED,
PCAN_BOOT_STATE_FAILED
} pcan_boot_state_t;
typedef struct {
uint32_t image_size;
uint32_t image_crc32;
uint32_t firmware_version;
uint16_t product_type;
uint8_t hardware_revision_min;
uint8_t hardware_revision_max;
} pcan_boot_manifest_t;
typedef struct {
uint8_t device_type;
uint8_t device_id;
uint16_t product_type;
uint8_t hardware_revision;
uint32_t firmware_version;
uint8_t active_slot;
/** 1 for an in-place/single-slot target, 2 for A/B. Zero keeps legacy A/B. */
uint8_t slot_count;
/** Writable image capacity. Zero uses PCAN_BOOT_SLOT_SIZE. */
uint32_t max_image_size;
uint8_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */
} pcan_boot_config_t;
typedef struct {
bool (*send)(void *user, uint32_t can_id, const uint8_t *data, uint8_t dlc);
bool (*erase_slot)(void *user, uint8_t slot);
bool (*write_slot)(void *user, uint8_t slot, uint32_t offset,
const uint8_t *data, uint8_t length);
bool (*authorize)(void *user, const pcan_boot_manifest_t *manifest);
bool (*verify_image)(void *user, uint8_t slot,
const pcan_boot_manifest_t *manifest);
bool (*set_pending_slot)(void *user, uint8_t slot,
const pcan_boot_manifest_t *manifest);
bool (*confirm_running_slot)(void *user);
void (*reboot)(void *user);
} pcan_boot_port_t;
typedef struct {
pcan_boot_config_t config;
pcan_boot_port_t port;
void *port_user;
pcan_boot_manifest_t manifest;
pcan_boot_state_t state;
uint32_t running_crc32;
uint32_t bytes_received;
uint16_t next_block;
uint8_t session_id;
uint8_t target_slot;
uint8_t last_status;
bool have_image;
bool have_compat;
} pcan_boot_t;
/** Инициализирует экземпляр. Все обязательные callbacks должны быть заданы. */
bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
const pcan_boot_port_t *port, void *port_user);
/** Сбрасывает незавершённую сессию, не меняя конфигурацию и callbacks. */
void pcan_boot_abort(pcan_boot_t *boot);
/**
* Обрабатывает один Extended CAN-кадр.
* Возвращает true, если кадр принадлежал загрузочному сервису этого прибора.
*/
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
const uint8_t *data, uint8_t dlc);
/** Переносимые операции с 29-битным ProtoCAN ID. */
uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
uint8_t device_type, uint8_t device_id,
uint8_t msg_type, uint16_t msg_body);
uint8_t pcan_boot_id_route(uint32_t can_id);
uint8_t pcan_boot_id_device_type(uint32_t can_id);
uint8_t pcan_boot_id_device_id(uint32_t can_id);
uint8_t pcan_boot_id_msg_type(uint32_t can_id);
uint16_t pcan_boot_id_msg_body(uint32_t can_id);
#ifdef __cplusplus
}
#endif
#endif /* PCAN_BOOT_H */

View File

@@ -0,0 +1,349 @@
#include "pcan_boot.h"
#include <string.h>
#define PCAN_BOOT_ID_MASK 0x1FFFFFFFUL
static uint16_t get_u16(const uint8_t *p)
{
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static uint32_t get_u32(const uint8_t *p)
{
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16)
| ((uint32_t)p[3] << 24);
}
static void put_u16(uint8_t *p, uint16_t value)
{
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
}
static void put_u32(uint8_t *p, uint32_t value)
{
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
p[2] = (uint8_t)(value >> 16);
p[3] = (uint8_t)(value >> 24);
}
static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length)
{
size_t i;
crc = ~crc;
for (i = 0U; i < length; ++i) {
uint8_t bit;
crc ^= data[i];
for (bit = 0U; bit < 8U; ++bit) {
crc = (crc >> 1) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U);
}
}
return ~crc;
}
uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
uint8_t device_type, uint8_t device_id,
uint8_t msg_type, uint16_t msg_body)
{
return ((((uint32_t)priority & 1U) << 28)
| (((uint32_t)route & 1U) << 27)
| (((uint32_t)device_type & 7U) << 24)
| (((uint32_t)device_id & 15U) << 20)
| (((uint32_t)msg_type & 15U) << 16)
| msg_body) & PCAN_BOOT_ID_MASK;
}
uint8_t pcan_boot_id_route(uint32_t can_id) { return (uint8_t)((can_id >> 27) & 1U); }
uint8_t pcan_boot_id_device_type(uint32_t can_id) { return (uint8_t)((can_id >> 24) & 7U); }
uint8_t pcan_boot_id_device_id(uint32_t can_id) { return (uint8_t)((can_id >> 20) & 15U); }
uint8_t pcan_boot_id_msg_type(uint32_t can_id) { return (uint8_t)((can_id >> 16) & 15U); }
uint16_t pcan_boot_id_msg_body(uint32_t can_id) { return (uint16_t)can_id; }
static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body,
const uint8_t *data, uint8_t dlc)
{
uint32_t id = pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_DEVICE,
boot->config.device_type,
boot->config.device_id,
msg_type, body);
return boot->port.send(boot->port_user, id, data, dlc);
}
static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status)
{
uint8_t data[8];
uint16_t body = (uint16_t)(((uint16_t)boot->session_id << 8) | command);
data[0] = status;
data[1] = boot->target_slot;
put_u16(&data[2], boot->next_block);
put_u32(&data[4], boot->running_crc32);
boot->last_status = status;
return send_frame(boot, PCAN_BOOT_MSG_STATUS, body, data, 8U);
}
static bool send_identity(pcan_boot_t *boot)
{
uint8_t data[8];
put_u16(&data[0], boot->config.product_type);
data[2] = boot->config.hardware_revision;
data[3] = PCAN_BOOT_VERSION;
put_u32(&data[4], boot->config.firmware_version);
return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, 1U, data, 8U);
}
void pcan_boot_abort(pcan_boot_t *boot)
{
if (boot == NULL) {
return;
}
(void)memset(&boot->manifest, 0, sizeof(boot->manifest));
boot->state = PCAN_BOOT_STATE_IDLE;
boot->running_crc32 = 0U;
boot->bytes_received = 0U;
boot->next_block = 0U;
boot->session_id = 0U;
boot->target_slot = PCAN_BOOT_SLOT_NONE;
boot->last_status = PCAN_BOOT_STATUS_OK;
boot->have_image = false;
boot->have_compat = false;
}
bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
const pcan_boot_port_t *port, void *port_user)
{
uint8_t slot_count;
if ((boot == NULL) || (config == NULL) || (port == NULL)
|| (config->device_type > 7U) || (config->device_id > 15U)
|| (config->active_slot > 1U)
|| (port->send == NULL) || (port->erase_slot == NULL)
|| (port->write_slot == NULL)) {
return false;
}
slot_count = config->slot_count == 0U ? 2U : config->slot_count;
if ((slot_count > 2U) || (config->active_slot >= slot_count)
|| ((slot_count == 2U) && (port->set_pending_slot == NULL))) {
return false;
}
(void)memset(boot, 0, sizeof(*boot));
boot->config = *config;
boot->port = *port;
boot->port_user = port_user;
boot->config.slot_count = slot_count;
if (boot->config.max_image_size == 0U) {
boot->config.max_image_size = PCAN_BOOT_SLOT_SIZE;
}
if (boot->config.ack_window == 0U) {
boot->config.ack_window = 1U;
}
pcan_boot_abort(boot);
return true;
}
static bool metadata_complete(pcan_boot_t *boot, uint8_t command)
{
if (!boot->have_image || !boot->have_compat) {
boot->state = PCAN_BOOT_STATE_METADATA;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
}
if ((boot->manifest.image_size == 0U)
|| (boot->manifest.image_size > boot->config.max_image_size)) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
}
if ((boot->manifest.product_type != boot->config.product_type)
|| (boot->config.hardware_revision < boot->manifest.hardware_revision_min)
|| (boot->config.hardware_revision > boot->manifest.hardware_revision_max)) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_HARDWARE);
}
if ((boot->port.authorize != NULL)
&& !boot->port.authorize(boot->port_user, &boot->manifest)) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR);
}
boot->target_slot = boot->config.slot_count == 1U
? boot->config.active_slot
: (uint8_t)(boot->config.active_slot ^ 1U);
boot->state = PCAN_BOOT_STATE_READY_TO_ERASE;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
}
static bool process_control(pcan_boot_t *boot, uint16_t body,
const uint8_t *data, uint8_t dlc)
{
uint8_t session = (uint8_t)(body >> 8);
uint8_t command = (uint8_t)body;
if (command == PCAN_BOOT_CMD_IDENTIFY) {
return send_identity(boot);
}
if (command == PCAN_BOOT_CMD_ENTER_BOOT) {
if (session == 0U) {
return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR);
}
pcan_boot_abort(boot);
boot->session_id = session;
boot->state = PCAN_BOOT_STATE_METADATA;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
}
if ((session == 0U) || (session != boot->session_id)) {
return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR);
}
switch (command) {
case PCAN_BOOT_CMD_BEGIN_IMAGE:
if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
boot->manifest.image_size = get_u32(&data[0]);
boot->manifest.image_crc32 = get_u32(&data[4]);
boot->have_image = true;
return metadata_complete(boot, command);
case PCAN_BOOT_CMD_BEGIN_COMPAT:
if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
boot->manifest.product_type = get_u16(&data[0]);
boot->manifest.hardware_revision_min = data[2];
boot->manifest.hardware_revision_max = data[3];
boot->manifest.firmware_version = get_u32(&data[4]);
boot->have_compat = true;
return metadata_complete(boot, command);
case PCAN_BOOT_CMD_ERASE:
if (boot->state != PCAN_BOOT_STATE_READY_TO_ERASE) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
if (!boot->port.erase_slot(boot->port_user, boot->target_slot)) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
}
boot->running_crc32 = 0U;
boot->bytes_received = 0U;
boot->next_block = 0U;
boot->state = PCAN_BOOT_STATE_RECEIVING;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_VERIFY:
if ((boot->state != PCAN_BOOT_STATE_RECEIVING)
|| (boot->bytes_received != boot->manifest.image_size)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
if (boot->running_crc32 != boot->manifest.image_crc32) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_CRC_ERROR);
}
if ((boot->port.verify_image != NULL)
&& !boot->port.verify_image(boot->port_user, boot->target_slot,
&boot->manifest)) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR);
}
boot->state = PCAN_BOOT_STATE_VERIFIED;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_COMMIT:
if (boot->state != PCAN_BOOT_STATE_VERIFIED) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
if ((boot->config.slot_count == 2U)
&& !boot->port.set_pending_slot(boot->port_user, boot->target_slot,
&boot->manifest)) {
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
}
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_CONFIRM:
if ((boot->port.confirm_running_slot == NULL)
|| !boot->port.confirm_running_slot(boot->port_user)) {
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
}
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_QUERY_PROGRESS:
return send_status(boot, command, boot->last_status);
case PCAN_BOOT_CMD_ABORT:
pcan_boot_abort(boot);
boot->session_id = session;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_REBOOT:
(void)send_status(boot, command, PCAN_BOOT_STATUS_OK);
if (boot->port.reboot != NULL) {
boot->port.reboot(boot->port_user);
}
return true;
default:
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_COMMAND);
}
}
static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
const uint8_t *data, uint8_t dlc)
{
uint8_t expected_type;
uint8_t write_length;
uint32_t remaining;
if (boot->state != PCAN_BOOT_STATE_RECEIVING) {
return send_status(boot, 0U, PCAN_BOOT_STATUS_INVALID_STATE);
}
expected_type = boot->target_slot == 0U ? PCAN_BOOT_MSG_DATA_A
: PCAN_BOOT_MSG_DATA_B;
if (msg_type != expected_type) {
return send_status(boot, 0U, PCAN_BOOT_STATUS_INVALID_STATE);
}
if ((block != boot->next_block) || (dlc != PCAN_BOOT_BLOCK_SIZE)) {
return send_status(boot, 0U, PCAN_BOOT_STATUS_SEQUENCE_ERROR);
}
remaining = boot->manifest.image_size - boot->bytes_received;
write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (uint8_t)remaining
: PCAN_BOOT_BLOCK_SIZE;
if ((write_length == 0U)
|| !boot->port.write_slot(boot->port_user, boot->target_slot,
boot->bytes_received, data, write_length)) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, 0U, PCAN_BOOT_STATUS_FLASH_ERROR);
}
boot->running_crc32 = crc32_update(boot->running_crc32, data, write_length);
boot->bytes_received += write_length;
boot->next_block++;
if ((boot->bytes_received == boot->manifest.image_size)
|| ((boot->next_block % boot->config.ack_window) == 0U)) {
return send_status(boot, 0U, PCAN_BOOT_STATUS_OK);
}
return true;
}
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
const uint8_t *data, uint8_t dlc)
{
uint8_t msg_type;
if ((boot == NULL) || (dlc > 8U) || ((dlc != 0U) && (data == NULL))) {
return false;
}
msg_type = pcan_boot_id_msg_type(can_id);
if ((msg_type < PCAN_BOOT_MSG_CONTROL) || (msg_type > PCAN_BOOT_MSG_DISCOVERY)) {
return false;
}
if ((pcan_boot_id_route(can_id) != PCAN_BOOT_ROUTE_FROM_PM)
|| (pcan_boot_id_device_type(can_id) != boot->config.device_type)
|| (pcan_boot_id_device_id(can_id) != boot->config.device_id)) {
return false;
}
if (msg_type == PCAN_BOOT_MSG_CONTROL) {
return process_control(boot, pcan_boot_id_msg_body(can_id), data, dlc);
}
if ((msg_type == PCAN_BOOT_MSG_DATA_A) || (msg_type == PCAN_BOOT_MSG_DATA_B)) {
return process_data(boot, msg_type, pcan_boot_id_msg_body(can_id), data, dlc);
}
return false;
}

View File

@@ -0,0 +1,306 @@
#include "pcan_boot.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
typedef struct {
uint8_t flash[2][64];
uint32_t last_id;
uint8_t last_data[8];
uint8_t last_dlc;
unsigned sends;
unsigned erases;
unsigned writes;
uint8_t pending_slot;
bool authorized;
bool verified;
bool rebooted;
} fake_t;
static bool fake_send(void *user, uint32_t id, const uint8_t *data, uint8_t dlc)
{
fake_t *f = (fake_t *)user;
f->last_id = id;
f->last_dlc = dlc;
(void)memset(f->last_data, 0, sizeof(f->last_data));
if (dlc != 0U) {
(void)memcpy(f->last_data, data, dlc);
}
f->sends++;
return true;
}
static bool fake_erase(void *user, uint8_t slot)
{
fake_t *f = (fake_t *)user;
(void)memset(f->flash[slot], 0xFF, sizeof(f->flash[slot]));
f->erases++;
return true;
}
static bool fake_write(void *user, uint8_t slot, uint32_t offset,
const uint8_t *data, uint8_t length)
{
fake_t *f = (fake_t *)user;
if ((slot > 1U) || (offset + length > sizeof(f->flash[slot]))) {
return false;
}
(void)memcpy(&f->flash[slot][offset], data, length);
f->writes++;
return true;
}
static bool fake_authorize(void *user, const pcan_boot_manifest_t *manifest)
{
fake_t *f = (fake_t *)user;
(void)manifest;
return f->authorized;
}
static bool fake_verify(void *user, uint8_t slot,
const pcan_boot_manifest_t *manifest)
{
fake_t *f = (fake_t *)user;
(void)slot;
(void)manifest;
return f->verified;
}
static bool fake_pending(void *user, uint8_t slot,
const pcan_boot_manifest_t *manifest)
{
fake_t *f = (fake_t *)user;
(void)manifest;
f->pending_slot = slot;
return true;
}
static bool fake_confirm(void *user)
{
(void)user;
return true;
}
static void fake_reboot(void *user)
{
((fake_t *)user)->rebooted = true;
}
static uint32_t crc32(const uint8_t *data, size_t length)
{
uint32_t crc = 0xFFFFFFFFUL;
size_t i;
for (i = 0U; i < length; ++i) {
uint8_t bit;
crc ^= data[i];
for (bit = 0U; bit < 8U; ++bit) {
crc = (crc >> 1) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U);
}
}
return ~crc;
}
static void put_u16(uint8_t *p, uint16_t v)
{
p[0] = (uint8_t)v;
p[1] = (uint8_t)(v >> 8);
}
static void put_u32(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)v;
p[1] = (uint8_t)(v >> 8);
p[2] = (uint8_t)(v >> 16);
p[3] = (uint8_t)(v >> 24);
}
static uint32_t request_id(uint8_t type, uint16_t body)
{
return pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_PM, 3U, 5U, type, body);
}
static bool command(pcan_boot_t *boot, uint8_t session, uint8_t cmd,
const uint8_t *data, uint8_t dlc)
{
uint16_t body = (uint16_t)(((uint16_t)session << 8) | cmd);
return pcan_boot_process(boot, request_id(PCAN_BOOT_MSG_CONTROL, body), data, dlc);
}
static void setup(pcan_boot_t *boot, fake_t *fake)
{
pcan_boot_config_t config;
pcan_boot_port_t port;
(void)memset(fake, 0, sizeof(*fake));
(void)memset(&config, 0, sizeof(config));
(void)memset(&port, 0, sizeof(port));
fake->authorized = true;
fake->verified = true;
fake->pending_slot = PCAN_BOOT_SLOT_NONE;
config.device_type = 3U;
config.device_id = 5U;
config.product_type = 0x1234U;
config.hardware_revision = 2U;
config.firmware_version = 0x01020304UL;
config.active_slot = 0U;
config.ack_window = 2U;
port.send = fake_send;
port.erase_slot = fake_erase;
port.write_slot = fake_write;
port.authorize = fake_authorize;
port.verify_image = fake_verify;
port.set_pending_slot = fake_pending;
port.confirm_running_slot = fake_confirm;
port.reboot = fake_reboot;
assert(pcan_boot_init(boot, &config, &port, fake));
}
static void begin_update(pcan_boot_t *boot, const uint8_t *image, uint32_t size)
{
uint8_t data[8];
assert(command(boot, 7U, PCAN_BOOT_CMD_ENTER_BOOT, NULL, 0U));
put_u32(&data[0], size);
put_u32(&data[4], crc32(image, size));
assert(command(boot, 7U, PCAN_BOOT_CMD_BEGIN_IMAGE, data, 8U));
put_u16(&data[0], 0x1234U);
data[2] = 1U;
data[3] = 3U;
put_u32(&data[4], 0x02000000UL);
assert(command(boot, 7U, PCAN_BOOT_CMD_BEGIN_COMPAT, data, 8U));
}
static void test_id_layout(void)
{
uint32_t id = pcan_boot_make_id(1U, 0U, 7U, 15U, 0xAU, 0x1234U);
assert(id == 0x17FA1234UL);
assert(pcan_boot_id_device_type(id) == 7U);
assert(pcan_boot_id_device_id(id) == 15U);
assert(pcan_boot_id_msg_type(id) == 0xAU);
assert(pcan_boot_id_msg_body(id) == 0x1234U);
}
static void test_crc32_reference(void)
{
static const uint8_t reference[] = "123456789";
assert(crc32(reference, sizeof(reference) - 1U) == 0xCBF43926UL);
}
static void test_complete_update(void)
{
static const uint8_t image[13] = {
0x10U, 0x11U, 0x12U, 0x13U, 0x14U, 0x15U, 0x16U, 0x17U,
0x20U, 0x21U, 0x22U, 0x23U, 0x24U
};
uint8_t block[8];
pcan_boot_t boot;
fake_t fake;
setup(&boot, &fake);
begin_update(&boot, image, sizeof(image));
assert(boot.state == PCAN_BOOT_STATE_READY_TO_ERASE);
assert(boot.target_slot == 1U);
assert(command(&boot, 7U, PCAN_BOOT_CMD_ERASE, NULL, 0U));
assert(fake.erases == 1U);
assert(pcan_boot_process(&boot, request_id(PCAN_BOOT_MSG_DATA_B, 0U), image, 8U));
(void)memset(block, 0xFF, sizeof(block));
(void)memcpy(block, &image[8], 5U);
assert(pcan_boot_process(&boot, request_id(PCAN_BOOT_MSG_DATA_B, 1U), block, 8U));
assert(boot.bytes_received == sizeof(image));
assert(boot.next_block == 2U);
assert(memcmp(fake.flash[1], image, sizeof(image)) == 0);
assert(command(&boot, 7U, PCAN_BOOT_CMD_VERIFY, NULL, 0U));
assert(boot.state == PCAN_BOOT_STATE_VERIFIED);
assert(command(&boot, 7U, PCAN_BOOT_CMD_COMMIT, NULL, 0U));
assert(fake.pending_slot == 1U);
assert(command(&boot, 7U, PCAN_BOOT_CMD_REBOOT, NULL, 0U));
assert(fake.rebooted);
}
static void test_rejects_wrong_address_and_sequence(void)
{
uint8_t image[8] = {0U};
pcan_boot_t boot;
fake_t fake;
uint32_t other = pcan_boot_make_id(1U, 0U, 3U, 6U,
PCAN_BOOT_MSG_CONTROL,
(uint16_t)((7U << 8) | PCAN_BOOT_CMD_ENTER_BOOT));
setup(&boot, &fake);
assert(!pcan_boot_process(&boot, other, NULL, 0U));
begin_update(&boot, image, sizeof(image));
assert(command(&boot, 7U, PCAN_BOOT_CMD_ERASE, NULL, 0U));
assert(pcan_boot_process(&boot, request_id(PCAN_BOOT_MSG_DATA_B, 1U), image, 8U));
assert(fake.last_data[0] == PCAN_BOOT_STATUS_SEQUENCE_ERROR);
assert(fake.writes == 0U);
}
static void test_rejects_wrong_session_and_compatibility(void)
{
uint8_t data[8] = {0U};
pcan_boot_t boot;
fake_t fake;
setup(&boot, &fake);
assert(command(&boot, 7U, PCAN_BOOT_CMD_ENTER_BOOT, NULL, 0U));
assert(command(&boot, 8U, PCAN_BOOT_CMD_ERASE, NULL, 0U));
assert(fake.last_data[0] == PCAN_BOOT_STATUS_SESSION_ERROR);
put_u32(&data[0], 8U);
put_u32(&data[4], 0U);
assert(command(&boot, 7U, PCAN_BOOT_CMD_BEGIN_IMAGE, data, 8U));
put_u16(&data[0], 0x9999U);
data[2] = 1U;
data[3] = 3U;
put_u32(&data[4], 1U);
assert(command(&boot, 7U, PCAN_BOOT_CMD_BEGIN_COMPAT, data, 8U));
assert(fake.last_data[0] == PCAN_BOOT_STATUS_WRONG_HARDWARE);
assert(boot.state == PCAN_BOOT_STATE_FAILED);
}
static void test_single_slot_update(void)
{
static const uint8_t image[8] = { 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U };
pcan_boot_t boot;
fake_t fake;
pcan_boot_config_t config;
pcan_boot_port_t port;
(void)memset(&fake, 0, sizeof(fake));
(void)memset(&config, 0, sizeof(config));
(void)memset(&port, 0, sizeof(port));
fake.pending_slot = PCAN_BOOT_SLOT_NONE;
fake.authorized = true;
fake.verified = true;
config.device_type = 3U;
config.device_id = 5U;
config.product_type = 0x1234U;
config.hardware_revision = 2U;
config.slot_count = 1U;
config.max_image_size = sizeof(fake.flash[0]);
config.ack_window = 16U;
port.send = fake_send;
port.erase_slot = fake_erase;
port.write_slot = fake_write;
port.authorize = fake_authorize;
port.verify_image = fake_verify;
port.reboot = fake_reboot;
assert(pcan_boot_init(&boot, &config, &port, &fake));
begin_update(&boot, image, sizeof(image));
assert(boot.target_slot == 0U);
assert(command(&boot, 7U, PCAN_BOOT_CMD_ERASE, NULL, 0U));
assert(pcan_boot_process(&boot, request_id(PCAN_BOOT_MSG_DATA_A, 0U), image, 8U));
assert(command(&boot, 7U, PCAN_BOOT_CMD_VERIFY, NULL, 0U));
assert(command(&boot, 7U, PCAN_BOOT_CMD_COMMIT, NULL, 0U));
assert(fake.pending_slot == PCAN_BOOT_SLOT_NONE);
assert(memcmp(fake.flash[0], image, sizeof(image)) == 0);
}
int main(void)
{
test_id_layout();
test_crc32_reference();
test_complete_update();
test_rejects_wrong_address_and_sequence();
test_rejects_wrong_session_and_compatibility();
test_single_slot_update();
puts("pcan_boot tests: OK");
return 0;
}

View File

@@ -1,35 +0,0 @@
cmake_minimum_required(VERSION 3.13)
project(protocan_transport C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Ядро библиотеки: платформенных зависимостей нет, собирается под что угодно.
add_library(protocan_transport STATIC
src/pcan_crc.c
src/pcan_frame.c
src/pcan_id.c
src/pcan_link.c
src/pcan_ring.c
src/pcan_gas.c
)
target_include_directories(protocan_transport PUBLIC include)
if(MSVC)
target_compile_options(protocan_transport PRIVATE /W4)
else()
target_compile_options(protocan_transport PRIVATE -Wall -Wextra -Wpedantic)
endif()
# Порт ports/stm32f4 сюда не входит: ему нужен CMSIS-заголовок stm32f4xx.h,
# он подключается напрямую в проект прошивки.
option(PCAN_BUILD_TESTS "Собирать тесты" ON)
if(PCAN_BUILD_TESTS)
enable_testing()
add_executable(test_transport tests/test_transport.c)
target_link_libraries(test_transport PRIVATE protocan_transport)
add_test(NAME transport COMMAND test_transport)
endif()

View File

@@ -1,126 +0,0 @@
# protocan-transport
Переносимая библиотека транспортного уровня для протокола **ProtoCAN**:
кадрирование поверх любого байтового потока (RS485, RS232, USB CDC),
разбор идентификатора и общее адресное пространство регистров.
Написана на C99, без динамической памяти, без ОС, без зависимостей от HAL
и от конкретного микроконтроллера. Состояние живёт в структурах вызывающего,
поэтому в одной прошивке поднимается сколько угодно независимых каналов.
```
ваш код protocan-transport платформа
┌────────┐ ┌────────────────────┐ ┌──────────────┐
│ кадры │─────►│ pcan_link_send() │─────►│ io.write() │──► UART/DMA
│ │◄─────│ on_frame() │◄─────│ pcan_link_feed()
└────────┘ └────────────────────┘ └──────────────┘
pcan_gas_* pcan_id_*
```
## Состав
| Модуль | Назначение |
|---|---|
| `pcan_frame` | кадр `AA 55 … CRC16` и потоковый разборщик с ресинхронизацией |
| `pcan_crc` | CRC-16/CCITT-FALSE, побитовый или табличный |
| `pcan_link` | экземпляр канала: приём, передача, SEQ, счётчики |
| `pcan_ring` | кольцевой буфер, отдаёт непрерывный участок для DMA |
| `pcan_id` | упаковка и разбор 29-битного идентификатора ProtoCAN |
| `pcan_gas` | общее адресное пространство: карта регионов, чтение/запись, мост к кадрам |
| `ports/stm32f4` | готовый порт USART + DMA (пакетная передача, кольцевой приём) |
## Кадр
```
AA 55 | LEN | SEQ | FLAGS | ID0 ID1 ID2 ID3 | DATA[0..8] | CRC_L CRC_H
```
`LEN = 6 + DLC` (6..14), CRC-16/CCITT-FALSE по байтам `LEN..DATA`,
little-endian. Подробности — [docs/FRAME.md](docs/FRAME.md).
## Общее адресное пространство
Плоское пространство 16-битных регистров `0x0000..0xFFFF`, собранное из
регионов. Регион ссылается либо на массив в памяти, либо на пару колбэков —
так в карту попадают и переменные, и вычисляемые значения, и регистры
периферии. Подробности — [docs/GAS.md](docs/GAS.md).
```c
static uint16_t holding[8];
static const pcan_gas_region_t regions[] = {
{ 0x0000, 8, holding, NULL, NULL, 0, NULL, "holding" },
{ 0xFF00, 4, NULL, diag_read, NULL, PCAN_GAS_RDONLY, NULL, "diag" },
};
static const pcan_gas_map_t map = { regions, 2 };
```
## Использование
```c
#include "protocan_transport.h"
static void on_frame(const pcan_frame_t *f, void *user)
{
/* ... */
}
pcan_io_t io;
pcan_uart_io(&uart, &io); /* или свой io */
pcan_link_t link;
pcan_link_init(&link, &io, on_frame, NULL);
pcan_frame_t f = { .flags = PCAN_FLAG_IDE, .id = 0x1234567, .dlc = 2 };
f.data[0] = 0xAA; f.data[1] = 0xBB;
pcan_link_send(&link, &f); /* SEQ проставится сам */
```
Для своей платформы достаточно реализовать две функции:
```c
size_t my_write(void *ctx, const uint8_t *data, size_t len); /* всё-или-ничего */
size_t my_space(void *ctx);
```
## Сборка
Библиотека — шесть файлов в `src/` и заголовки в `include/`. Добавьте их
в проект и укажите `include/` в путях поиска. CMake для тестов и хостовых
сборок:
```bash
cmake -B build && cmake --build build && ctest --test-dir build
```
Либо напрямую:
```bash
clang -std=c99 -Wall -Wextra -Iinclude tests/test_transport.c src/pcan_*.c -o test && ./test
```
Порт `ports/stm32f4` в тесты не входит: ему нужен CMSIS-заголовок
`stm32f4xx.h`, подключайте его в проект прошивки отдельно.
## Настройки
Переопределяются через `-D` либо через свой `pcan_config_user.h`
(с `-DPCAN_USE_USER_CONFIG`):
| Макрос | По умолчанию | Смысл |
|---|---|---|
| `PCAN_DATA_MAX` | 8 | длина поля данных CAN |
| `PCAN_CRC_TABLE` | 0 | 1 — таблица на 512 байт вместо побитового расчёта |
| `PCAN_GAS_MAX_REGIONS` | 16 | предел числа регионов в карте |
| `PCAN_BARRIER()` | барьер компилятора | для очередей, разделяемых с прерыванием |
## Ограничения
- Очереди рассчитаны на схему «один писатель + один читатель». Если
писателей несколько, оборачивайте вызовы своей блокировкой.
- Размер кольцевого буфера обязан быть степенью двойки; `pcan_ring_init()`
вернёт `false`, а не станет молча портить индексы.
- На STM32F407 DMA не видит CCM RAM (`0x10000000`) — буферы держите
в основном SRAM.
- Порядок байт на линии фиксирован (little-endian) и не зависит от порядка
байт хоста.

View File

@@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.16)
project(rs485_boot C)
add_library(rs485_boot src/rs485_boot.c)
target_include_directories(rs485_boot PUBLIC include)
if(BUILD_TESTING)
add_executable(test_rs485_boot tests/test_rs485_boot.c)
target_link_libraries(test_rs485_boot PRIVATE rs485_boot)
add_test(NAME rs485_boot COMMAND test_rs485_boot)
endif()

26
c/rs485-boot/README.md Normal file
View File

@@ -0,0 +1,26 @@
# RS-485 firmware boot
Переносимое ядро обновления прошивки по полудуплексному RS-485. Wire-протокол
совместим с SETGUI protocol v1 проекта `KONOR_ds18b20`: кадры `A5 5A`, CRC32,
команды `FIRMWARE_BEGIN/DATA/END/ABORT/STATUS` (`0x0B..0x0F`).
Ядро не обращается к регистрам МК. Проект передаёт ему принятые байты через
`rs485_boot_feed()` и реализует callbacks Flash/передачи из `rs485_boot_port_t`.
Готовые порты находятся в `ports/stm32f103`, `ports/stm32g474vet`,
`ports/tms320f2812` и `ports/tms320f28335`. TI-порты используют общее ядро
`ports/c28x` для ABI C28x с 16-битным `char` и умеют зеркалировать Flash в RAM.
Важно: линии A/B должны иметь терминацию и fail-safe bias. Порт держит DE
активным до флага физического завершения передачи TC, затем возвращается в RX.
## Подключение
1. Добавить `src/rs485_boot.c` и порт нужного МК в проект.
2. Скопировать `rs485_boot_port_config.template.h` в include-каталог проекта как
`rs485_boot_port_config.h` и проверить UART, DE, адрес приложения и Flash.
3. Вызвать `rs485_boot_hw_init()`, затем `rs485_boot_init()` с callbacks из
`rs485_boot_hw_make_port()`.
4. В основном цикле читать `rs485_boot_hw_get_byte()` и передавать байты ядру.
Порт STM32F103 рассчитан на стандартную периферию STM32F10x. Порт G474VET
использует CMSIS STM32G4 (`stm32g474xx.h`) и 64-битное программирование Flash.

View File

@@ -0,0 +1,73 @@
#ifndef RS485_BOOT_H
#define RS485_BOOT_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define RS485_BOOT_VERSION 1U
#define RS485_BOOT_MAX_PAYLOAD 512U
#define RS485_BOOT_MAX_FRAME (8U + RS485_BOOT_MAX_PAYLOAD + 4U)
#define RS485_BOOT_DATA_HEADER_SIZE 8U
#define RS485_BOOT_MAX_BLOCK (RS485_BOOT_MAX_PAYLOAD - RS485_BOOT_DATA_HEADER_SIZE)
typedef enum {
RS485_BOOT_IDLE = 0U,
RS485_BOOT_RECEIVING = 1U,
RS485_BOOT_READY = 2U,
RS485_BOOT_FAILED = 3U
} rs485_boot_state_t;
typedef enum {
RS485_BOOT_OK = 0U,
RS485_BOOT_INVALID_ARGUMENT = 1U,
RS485_BOOT_INVALID_LENGTH = 2U,
RS485_BOOT_BUSY = 5U,
RS485_BOOT_INTERNAL = 7U,
RS485_BOOT_UNSUPPORTED = 0x11U
} rs485_boot_result_t;
typedef struct {
uint32_t app_address;
uint32_t app_size;
uint16_t max_block_size;
} rs485_boot_config_t;
typedef struct {
bool (*transmit)(void *user, const uint8_t *data, uint16_t length);
bool (*erase)(void *user, uint32_t image_size);
bool (*write)(void *user, uint32_t offset, const uint8_t *data, uint16_t length);
bool (*read)(void *user, uint32_t offset, uint8_t *data, uint16_t length);
bool (*image_valid)(void *user, uint32_t image_size, uint32_t image_crc32);
void (*reboot)(void *user);
} rs485_boot_port_t;
typedef struct {
rs485_boot_config_t config;
rs485_boot_port_t port;
void *port_user;
uint8_t parser[RS485_BOOT_MAX_FRAME];
uint16_t parser_length;
uint8_t tx[RS485_BOOT_MAX_FRAME];
uint8_t scratch[32];
uint32_t image_size;
uint32_t image_crc32;
uint32_t next_offset;
uint32_t crc_errors;
rs485_boot_state_t state;
} rs485_boot_t;
bool rs485_boot_init(rs485_boot_t *boot, const rs485_boot_config_t *config,
const rs485_boot_port_t *port, void *port_user);
void rs485_boot_feed(rs485_boot_t *boot, const uint8_t *data, size_t length);
void rs485_boot_abort(rs485_boot_t *boot);
uint32_t rs485_boot_crc32(const uint8_t *data, size_t length);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,130 @@
#include "rs_boot_transport.h"
#define SOF0 0xA5U
#define SOF1 0x5AU
#define VERSION 1U
#define HEADER 8U
#define CRC_SIZE 4U
#define MSG_BEGIN 0x0BU
#define MSG_DATA 0x0CU
#define MSG_END 0x0DU
#define MSG_ABORT 0x0EU
#define MSG_STATUS 0x0FU
#define MSG_NACK 0x80U
static rsbt_u16_t u16(const rsbt_byte_t *p)
{ return (p[0] & 0xFFU) | ((p[1] & 0xFFU) << 8); }
static rsbt_u32_t u32(const rsbt_byte_t *p)
{
return (rsbt_u32_t)(p[0] & 0xFFU) |
((rsbt_u32_t)(p[1] & 0xFFU) << 8) |
((rsbt_u32_t)(p[2] & 0xFFU) << 16) |
((rsbt_u32_t)(p[3] & 0xFFU) << 24);
}
static void put32(rsbt_byte_t *p, rsbt_u32_t v)
{
p[0] = (rsbt_byte_t)(v & 0xFFU); p[1] = (rsbt_byte_t)((v >> 8) & 0xFFU);
p[2] = (rsbt_byte_t)((v >> 16) & 0xFFU); p[3] = (rsbt_byte_t)((v >> 24) & 0xFFU);
}
rsbt_u32_t rsbt_crc32(const rsbt_byte_t *p, rsbt_u16_t n)
{
rsbt_u32_t crc = 0xFFFFFFFFUL;
rsbt_u16_t i, bit;
for (i = 0U; i < n; ++i) {
crc ^= (rsbt_u32_t)(p[i] & 0xFFU);
for (bit = 0U; bit < 8U; ++bit)
crc = (crc >> 1) ^ ((crc & 1U) ? 0xEDB88320UL : 0U);
}
return crc ^ 0xFFFFFFFFUL;
}
void rsbt_abort(rsbt_t *b)
{
if (!b) return;
b->state = RSBT_IDLE; b->image_size = 0UL; b->flags = 0U;
b->image_crc = 0UL; b->next_offset = 0UL;
}
int rsbt_init(rsbt_t *b, const rsbt_config_t *c, const rsbt_port_t *p, void *u)
{
rsbt_u16_t i;
if (!b || !c || !p || !c->app_size || !p->transmit || !p->erase || !p->write || !p->read) return 0;
for (i = 0U; i < RSBT_MAX_FRAME; ++i) { b->parser[i] = 0U; b->tx[i] = 0U; }
b->config = *c; b->port = *p; b->user = u; b->parser_length = 0U; b->crc_errors = 0UL;
if (!b->config.max_block || b->config.max_block > RSBT_MAX_BLOCK) b->config.max_block = RSBT_MAX_BLOCK;
rsbt_abort(b); return 1;
}
static int send_frame(rsbt_t *b, rsbt_byte_t type, rsbt_u16_t seq,
const rsbt_byte_t *payload, rsbt_u16_t size)
{
rsbt_u16_t i, total; rsbt_u32_t crc;
if (size > RSBT_MAX_PAYLOAD) return 0;
total = HEADER + size + CRC_SIZE;
b->tx[0]=SOF0; b->tx[1]=SOF1; b->tx[2]=VERSION; b->tx[3]=type;
b->tx[4]=(seq>>8)&0xFFU; b->tx[5]=seq&0xFFU;
b->tx[6]=(size>>8)&0xFFU; b->tx[7]=size&0xFFU;
for(i=0U;i<size;++i) b->tx[HEADER+i]=payload[i]&0xFFU;
crc=rsbt_crc32(&b->tx[2],6U+size); put32(&b->tx[HEADER+size],crc);
return b->port.transmit(b->user,b->tx,total);
}
static void nack(rsbt_t *b, rsbt_byte_t req, rsbt_u16_t seq, rsbt_u16_t result)
{
rsbt_byte_t p[3]; p[0]=result&0xFFU; p[1]=(result>>8)&0xFFU; p[2]=req;
(void)send_frame(b,MSG_NACK,seq,p,3U);
}
static int matches(rsbt_t *b, rsbt_u32_t off, const rsbt_byte_t *p, rsbt_u16_t n)
{
rsbt_u16_t done=0U,i,take;
while(done<n){take=n-done;if(take>32U)take=32U;
if(!b->port.read(b->user,off+done,b->scratch,take))return 0;
for(i=0U;i<take;++i)if((b->scratch[i]&0xFFU)!=(p[done+i]&0xFFU))return 0;
done+=take;} return 1;
}
static void process(rsbt_t *b, rsbt_byte_t type, rsbt_u16_t seq,
const rsbt_byte_t *p, rsbt_u16_t size)
{
rsbt_u32_t off,base; rsbt_u16_t n,sum,i;
if(type==MSG_BEGIN){if(size!=16U){nack(b,type,seq,2U);return;}
b->image_size=u32(p);b->image_crc=u32(&p[4]);b->flags=p[8]&RSBT_FLAG_MIRROR_RAM;base=u32(&p[12]);
if((b->flags&RSBT_FLAG_MIRROR_RAM)&&!b->port.write_ram){nack(b,type,seq,0x11U);return;}
if(!b->image_size||b->image_size>b->config.app_size||(base&&base!=b->config.app_address)){nack(b,type,seq,1U);return;}
b->next_offset=0UL;b->state=RSBT_RECEIVING;
if(!b->port.erase(b->user,b->image_size)){b->state=RSBT_FAILED;nack(b,type,seq,7U);return;}
put32(b->scratch,0UL);(void)send_frame(b,type,seq,b->scratch,4U);return;}
if(type==MSG_DATA){if(b->state!=RSBT_RECEIVING||size<8U){nack(b,type,seq,5U);return;}
off=u32(p);n=u16(&p[4]);sum=0U;for(i=0U;i<n&&8U+i<size;++i)sum=(sum+(p[8U+i]&0xFFU))&0xFFFFU;
if(!n||n>b->config.max_block||size!=8U+n||off>b->image_size||n>b->image_size-off){nack(b,type,seq,2U);return;}
if(sum!=u16(&p[6])){nack(b,type,seq,1U);return;}
if(off<b->next_offset){if(off+n>b->next_offset||!matches(b,off,&p[8],n)){nack(b,type,seq,1U);return;}}
else {if(off!=b->next_offset||!b->port.write(b->user,off,&p[8],n)||
((b->flags&RSBT_FLAG_MIRROR_RAM)&&!b->port.write_ram(b->user,off,&p[8],n))){b->state=RSBT_FAILED;nack(b,type,seq,7U);return;}b->next_offset+=n;}
put32(b->scratch,b->next_offset);(void)send_frame(b,type,seq,b->scratch,4U);return;}
if(type==MSG_END){if(b->state!=RSBT_RECEIVING||size!=8U||u32(p)!=b->image_size||u32(&p[4])!=b->image_crc||b->next_offset!=b->image_size||(b->port.image_valid&&!b->port.image_valid(b->user,b->image_size,b->image_crc))){b->state=RSBT_FAILED;nack(b,type,seq,1U);return;}
b->state=RSBT_READY;(void)send_frame(b,type,seq,0,0U);if(b->port.reboot)b->port.reboot(b->user);return;}
if(type==MSG_ABORT){rsbt_abort(b);(void)send_frame(b,type,seq,0,0U);return;}
if(type==MSG_STATUS){b->scratch[0]=b->state;put32(&b->scratch[1],b->next_offset);b->scratch[5]=b->flags;(void)send_frame(b,type,seq,b->scratch,6U);return;}
nack(b,type,seq,0x11U);
}
static void discard(rsbt_t *b, rsbt_u16_t n)
{ rsbt_u16_t i;if(n>=b->parser_length){b->parser_length=0U;return;}for(i=n;i<b->parser_length;++i)b->parser[i-n]=b->parser[i];b->parser_length-=n; }
void rsbt_feed(rsbt_t *b, rsbt_byte_t byte)
{
rsbt_u16_t payload,total,seq; rsbt_u32_t got;
if(!b)return;if(b->parser_length==RSBT_MAX_FRAME)b->parser_length=0U;
b->parser[b->parser_length++]=byte&0xFFU;
for(;;){while(b->parser_length>=2U&&(b->parser[0]!=SOF0||b->parser[1]!=SOF1))discard(b,1U);
if(b->parser_length<HEADER)return;if(b->parser[2]!=VERSION){discard(b,1U);continue;}
payload=((b->parser[6]&0xFFU)<<8)|(b->parser[7]&0xFFU);if(payload>RSBT_MAX_PAYLOAD){discard(b,1U);continue;}
total=HEADER+payload+CRC_SIZE;if(b->parser_length<total)return;got=u32(&b->parser[HEADER+payload]);
if(got!=rsbt_crc32(&b->parser[2],6U+payload)){++b->crc_errors;discard(b,1U);continue;}
seq=((b->parser[4]&0xFFU)<<8)|(b->parser[5]&0xFFU);process(b,b->parser[3],seq,&b->parser[HEADER],payload);discard(b,total);}
}

View File

@@ -0,0 +1,62 @@
#ifndef RS_BOOT_TRANSPORT_H
#define RS_BOOT_TRANSPORT_H
/*
* ProtoCAN RS boot transport for TI C28x.
* A C28x char is 16 bits, therefore every wire byte is stored in the low
* eight bits of rsbt_byte_t. Never use sizeof() as a wire-byte count.
*/
typedef unsigned int rsbt_byte_t;
typedef unsigned int rsbt_u16_t;
typedef unsigned long rsbt_u32_t;
#define RSBT_MAX_PAYLOAD 512U
#define RSBT_MAX_FRAME (8U + RSBT_MAX_PAYLOAD + 4U)
#define RSBT_MAX_BLOCK (RSBT_MAX_PAYLOAD - 8U)
#define RSBT_FLAG_MIRROR_RAM 0x01U
typedef enum {
RSBT_IDLE = 0U,
RSBT_RECEIVING = 1U,
RSBT_READY = 2U,
RSBT_FAILED = 3U
} rsbt_state_t;
typedef struct {
int (*transmit)(void *, const rsbt_byte_t *, rsbt_u16_t);
int (*erase)(void *, rsbt_u32_t);
int (*write)(void *, rsbt_u32_t, const rsbt_byte_t *, rsbt_u16_t);
int (*read)(void *, rsbt_u32_t, rsbt_byte_t *, rsbt_u16_t);
int (*write_ram)(void *, rsbt_u32_t, const rsbt_byte_t *, rsbt_u16_t);
int (*image_valid)(void *, rsbt_u32_t, rsbt_u32_t);
void (*reboot)(void *);
} rsbt_port_t;
typedef struct {
rsbt_u32_t app_address;
rsbt_u32_t app_size;
rsbt_u16_t max_block;
} rsbt_config_t;
typedef struct {
rsbt_config_t config;
rsbt_port_t port;
void *user;
rsbt_byte_t parser[RSBT_MAX_FRAME];
rsbt_byte_t tx[RSBT_MAX_FRAME];
rsbt_byte_t scratch[32];
rsbt_u16_t parser_length;
rsbt_u32_t image_size;
rsbt_u32_t image_crc;
rsbt_u32_t next_offset;
rsbt_u32_t crc_errors;
rsbt_byte_t flags;
rsbt_state_t state;
} rsbt_t;
int rsbt_init(rsbt_t *, const rsbt_config_t *, const rsbt_port_t *, void *);
void rsbt_feed(rsbt_t *, rsbt_byte_t);
void rsbt_abort(rsbt_t *);
rsbt_u32_t rsbt_crc32(const rsbt_byte_t *, rsbt_u16_t);
#endif

View File

@@ -0,0 +1,8 @@
#ifndef RS485_BOOT_PORT_CONFIG_H
#define RS485_BOOT_PORT_CONFIG_H
#define RS485_BOOT_BAUDRATE 512000UL
#define RS485_BOOT_APP_ADDRESS 0x08003000UL
#define RS485_BOOT_FLASH_END 0x08010000UL
#define RS485_BOOT_FLASH_PAGE 1024UL
#define RS485_BOOT_DE_PIN 8U /* USART1 PA9/PA10; DE PA8, active high. */
#endif

View File

@@ -0,0 +1,17 @@
#include "rs485_boot_stm32f103.h"
#include "rs485_boot_port_config.h"
#include "stm32f10x.h"
#include <string.h>
static bool fw(void){while(FLASH->SR&FLASH_SR_BSY){}if(FLASH->SR&(FLASH_SR_PGERR|FLASH_SR_WRPRTERR)){FLASH->SR=FLASH_SR_PGERR|FLASH_SR_WRPRTERR|FLASH_SR_EOP;return false;}FLASH->SR=FLASH_SR_EOP;return true;}
static void ul(void){if(FLASH->CR&FLASH_CR_LOCK){FLASH->KEYR=0x45670123UL;FLASH->KEYR=0xCDEF89ABUL;}}
static void lk(void){FLASH->CR|=FLASH_CR_LOCK;}
void rs485_boot_hw_init(void){uint32_t c;RCC->APB2ENR|=RCC_APB2ENR_IOPAEN|RCC_APB2ENR_USART1EN;c=GPIOA->CRH;c&=~((0xFUL<<0)|(0xFUL<<4)|(0xFUL<<8));c|=(0x3UL<<0)|(0xBUL<<4)|(0x4UL<<8);GPIOA->CRH=c;GPIOA->BRR=1UL<<RS485_BOOT_DE_PIN;USART1->BRR=(SystemCoreClock+RS485_BOOT_BAUDRATE/2U)/RS485_BOOT_BAUDRATE;USART1->CR1=USART_CR1_TE|USART_CR1_RE|USART_CR1_UE;}
bool rs485_boot_hw_get_byte(uint8_t*b){uint32_t s=USART1->SR;if(s&(USART_SR_ORE|USART_SR_FE|USART_SR_NE)){(void)USART1->DR;return false;}if(!(s&USART_SR_RXNE))return false;*b=(uint8_t)USART1->DR;return true;}
static bool tx(void*u,const uint8_t*d,uint16_t n){uint16_t i;(void)u;GPIOA->BSRR=1UL<<RS485_BOOT_DE_PIN;for(i=0;i<n;i++){while(!(USART1->SR&USART_SR_TXE)){}USART1->DR=d[i];}while(!(USART1->SR&USART_SR_TC)){}GPIOA->BRR=1UL<<RS485_BOOT_DE_PIN;return true;}
static bool er(void*u,uint32_t n){uint32_t a,e;(void)u;if(!n||n>RS485_BOOT_FLASH_END-RS485_BOOT_APP_ADDRESS)return false;e=RS485_BOOT_APP_ADDRESS+((n+RS485_BOOT_FLASH_PAGE-1U)&~(RS485_BOOT_FLASH_PAGE-1U));ul();for(a=RS485_BOOT_APP_ADDRESS;a<e;a+=RS485_BOOT_FLASH_PAGE){FLASH->CR=FLASH_CR_PER;FLASH->AR=a;FLASH->CR|=FLASH_CR_STRT;if(!fw()){FLASH->CR=0;lk();return false;}}FLASH->CR=0;lk();return true;}
static bool wr(void*u,uint32_t o,const uint8_t*d,uint16_t n){uint16_t i;(void)u;if((o&1U)||o+n>RS485_BOOT_FLASH_END-RS485_BOOT_APP_ADDRESS)return false;ul();for(i=0;i<n;i+=2){uint16_t v=(uint16_t)d[i]|(uint16_t)(i+1<n?((uint16_t)d[i+1]<<8):0xFF00U);volatile uint16_t*q=(volatile uint16_t*)(RS485_BOOT_APP_ADDRESS+o+i);FLASH->CR=FLASH_CR_PG;*q=v;if(!fw()||*q!=v){FLASH->CR=0;lk();return false;}}FLASH->CR=0;lk();return true;}
static bool rd(void*u,uint32_t o,uint8_t*d,uint16_t n){(void)u;if(o+n>RS485_BOOT_FLASH_END-RS485_BOOT_APP_ADDRESS)return false;memcpy(d,(const void*)(RS485_BOOT_APP_ADDRESS+o),n);return true;}
static bool ok(void*u,uint32_t n,uint32_t c){uint32_t sp=*(const uint32_t*)RS485_BOOT_APP_ADDRESS,rv=*(const uint32_t*)(RS485_BOOT_APP_ADDRESS+4);(void)u;return sp>=0x20000000UL&&sp<=0x20005000UL&&(rv&1U)&&(rv&~1UL)>=RS485_BOOT_APP_ADDRESS&&(rv&~1UL)<RS485_BOOT_FLASH_END&&rs485_boot_crc32((const uint8_t*)RS485_BOOT_APP_ADDRESS,n)==c;}
static void reset(void*u){(void)u;NVIC_SystemReset();}
rs485_boot_config_t rs485_boot_hw_config(void){rs485_boot_config_t c={RS485_BOOT_APP_ADDRESS,RS485_BOOT_FLASH_END-RS485_BOOT_APP_ADDRESS,RS485_BOOT_MAX_BLOCK};return c;}
rs485_boot_port_t rs485_boot_hw_make_port(void){rs485_boot_port_t p={tx,er,wr,rd,ok,reset};return p;}

View File

@@ -0,0 +1,8 @@
#ifndef RS485_BOOT_STM32F103_H
#define RS485_BOOT_STM32F103_H
#include "rs485_boot.h"
void rs485_boot_hw_init(void);
bool rs485_boot_hw_get_byte(uint8_t *byte);
rs485_boot_config_t rs485_boot_hw_config(void);
rs485_boot_port_t rs485_boot_hw_make_port(void);
#endif

View File

@@ -0,0 +1,9 @@
#ifndef RS485_BOOT_PORT_CONFIG_H
#define RS485_BOOT_PORT_CONFIG_H
#define RS485_BOOT_BAUDRATE 512000UL
#define RS485_BOOT_UART_CLOCK_HZ SystemCoreClock
#define RS485_BOOT_APP_ADDRESS 0x08008000UL
#define RS485_BOOT_FLASH_END 0x08080000UL
#define RS485_BOOT_FLASH_PAGE 2048UL
#define RS485_BOOT_DE_PIN 8U /* USART1 PA9/PA10 AF7; DE PA8. */
#endif

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