22 Commits

Author SHA1 Message Date
f17c124f0e docs: Согласуй документацию ProtoCAN и инструментов сборки
Обнови спецификацию GAS 0x3/0x4, реестр устройств и описание загрузочных сервисов одновременно в Markdown, HTML и XLSX.

Свяжи новые аппаратные порты, глобальное время и сборку C2000/ПЛИС с корневыми инструкциями, чтобы проекты подключались по одному актуальному контракту.
2026-10-23 14:16:22 +03:00
2da7e7b525 feat(global-time): Добавь общие часы и порт TMS320F28335
Вынеси программные часы, коррекцию по ProtoCAN RTCSETUP и атомарную метку логгера в переносимое ядро без глобального состояния.

Подключи eCAN-A и CPU Timer2 отдельным портом F28335; зафиксируй календарные границы, переполнение, задержку RX и защиту IRQ в хостовых тестах.
2026-10-23 14:16:21 +03:00
62c0492bfd feat(firmware-build): Добавь сборку и паспорта проектов ПЛИС
Подключи ISE, Vivado и Quartus к общему итератору версии и HDL-паспорту. Проверяй новый bit/sof до создания manifest, SHA-256 и архива выпуска.

Добавь установку локальных команд ISE, инструкции переноса и регрессионные проверки успешных и прерванных сборок.
2026-10-23 14:16:21 +03:00
5bf3167064 build(firmware-info): Объедини подготовку и выпуск образов C2000
Направь CCS precompile/postcompile в общий c2000_build, чтобы резервирование версии, проверка EEPROM, MAP, SCI8 и Flash выполнялись одним процессом перед экспортом.

Сохраняй варианты eeprom, flash, can и rs в одном проверенном комплекте с manifest и SHA-256. Ошибка проверки блокирует постоянный выпуск и копирование устаревших образов.
2026-10-23 14:16:20 +03:00
c6390ae1c9 feat(protocan-boot): Добавь порты MCU и сервисы загрузки
Обеспечь единый процесс обновления для STM32F103/F407/G474 и TMS320F2812/F28335: discovery, служебные ответы, BUSY, тайм-аут сессии и подтверждение приложения. Добавь самостоятельные проекты CCS, аппаратные хранилища и Python-клиент.

Совместимость: структуры конфигурации и порта расширены, поэтому потребители должны пересобраться и обнулить новые поля. Для C28x callbacks используют pcan_boot_byte_t; периодический pcan_boot_poll обслуживает discovery и настроенный тайм-аут.
2026-10-23 14:16:19 +03:00
876b58517c feat(set-protocol): Раздели GAS и добавь пакетный сбор регистров
Сохрани адресацию отдельных регистров GAS 0x3 и выдели слотам тип 0x4 с маской Word0. Добавь единое C-ядро GAS_WATCH2, тонкие Python/JNI-порты и проверки общих эталонных байтов.

Совместимость: тип 0x4 больше не обслуживает Modbus Coil. Потребителям PCAN_MSG_MODBUS_COIL и ProtoCanMsgToModbusCoil нужно перейти на GAS_SLOT и отдельный API слотов; регистровый GAS остаётся на 0x3.
2026-10-23 14:14:48 +03:00
6ecf057a86 feat(c2000-linker): Добавить помощник настройки линкера C2000
Добавить генерацию карт COFF для TMS320F28335 и TMS320F2812, проверку CMD/MAP и русскую инструкцию.

Проверки: 14 тестов, включая четыре реальные линковки TI для RAM и Flash.
2026-10-19 16:33:40 +03:00
d42f2e1622 Перенести итератор версии прошивки в общий генератор
Увеличивать версию при прямом подключении precompile из CCS и Keil.
Сохранять счётчик атомарно и оставлять ручной режим без включённого флага.
Добавить проверки итератора и обновить примеры подключения.
2026-10-19 12:02:07 +03:00
0c4eab4615 Добавить общий конструктор CAN-кадров для Qt 2026-10-17 14:43:13 +03:00
5af8a8e9c8 Добавить общую Qt-консоль с фильтрами и экспортом 2026-10-17 14:43:12 +03:00
f613101124 Перезапускать импульс светодиода при каждом приёме пакета 2026-10-17 14:43:12 +03:00
cf65439400 Добавить выпуск и экспорт Flash-образов C2000 2026-10-17 14:43:11 +03:00
32e2fbedc0 Поддержать компактные записи логгера УМП v4
Разрешить 12-словные записи в запросах RTU и CAN. Распознавать статус v4 и декодировать оставшиеся сигналы с сохранением поддержки архивов v2/v3.
2026-10-12 12:25:52 +03:00
226c2ca599 chore(merge): объедини pysetterminal с main
Сохрани актуальный генератор firmware identity с экспортом всей папки сборки и временем UTC+3 при объединении веток.
2026-10-09 11:04:32 +03:00
f2956b54cd merge(deif): объедини Android-компоненты с main 2026-10-09 10:55:08 +03:00
eab8dc5e0a fix(firmware-build): support CCS artifact paths and bootstrap identity config 2026-10-08 17:57:04 +03:00
131f306131 fix(firmware-info): используй UTC+3 в именах папок
Переведи только время имени архива в UTC+3, сохрани UTC в метаданных и заголовке. Проверь переход на следующий день и копирование текущей папки.
2026-10-08 17:05:04 +03:00
4fc0ad4e14 feat(firmware-info): копируй папку текущей сборки целиком
Добавь каталог назначения последним аргументом postcompile CCS и Keil. Копируй только архив текущего запуска с сохранением имени папки, всех файлов и проверкой SHA-256. Обнови MD-команды и проверки выбора папки, вложенных файлов и ошибок экспорта.
2026-10-08 16:26:21 +03:00
da055c2787 feat(firmware-info): архивируй сборки по хешу и дате
Создай в postcompile CCS и Keil папку по commit исходников и UTC-времени precompile. Сохрани точные образы, manifest, SHA-256 и метаданные, сохраняя прежние выходные файлы. Опиши копирование каталогов в SETGUI и проверь разные даты, целостность и Git-метаданные.
2026-10-08 15:31:13 +03:00
8fc2e1b5cf fix(firmware-info): подключи Keil к JSON-схеме версии
Добавь prepare/seal для Keil через общий identity.py, образец конфигурации и команды Before/After Build. Для новой схемы не требуется отдельная release-конфигурация CMD или SETGUI. Проверь HEX/BIN, SHA-256, аргументы и пути с пробелами.
2026-10-08 15:13:34 +03:00
b5b2d1ae37 feat(firmware-info): добавь pre/post-build для CCS и Keil
Подключи общий генератор версии и SCI8-конвертацию через templates. Сохрани метаданные и SHA-256 готового BIN, совместимость с Python 3.8 и прежний SETGUI preflight. Опиши настройку CCS и локальное копирование результатов.
2026-10-08 14:58:27 +03:00
7dfa8bd94d Добавить общие компоненты DEIF для Android и интерактивные приборы
Реализовать SAM/DAM, циклическую передачу, heartbeat, привязки и диагностику. Перенести рисунок и управление MASTER/SLAVE из SETGUI, добавить полноэкранный режим и проверки совместимости кадров.
2026-10-04 23:32:49 +03:00
307 changed files with 50853 additions and 2373 deletions

View File

@@ -84,7 +84,7 @@ STM32F103RC, STM32F103ZE и STM32G474CE объявляют поддержку з
возвращают `PNAND_ERROR_WRITE_PROTECTED`, пока плата и порт не доработаны.
Рабочий пример полноценного G474-порта находится в соседнем проекте
`candleLight_fw`: `src/device/device_g4.c` отвечает за clock/GPIO,
`CANdle_stm32` (candleLight): `src/device/device_g4.c` отвечает за clock/GPIO,
`src/can/m_can.c` — за FDCAN, а `include/config.h` — за привязку платы
STM32G474VET6. Эти файлы остаются в прошивке, поскольку зависят от STM32 HAL,
USB gs_usb и конкретной разводки адаптера; переносимая логика протоколов

View File

@@ -40,9 +40,10 @@ templates/
| [`c/set-protocol/ports/stm32f407-devboard-v1`](c/set-protocol/ports/stm32f407-devboard-v1) | доступ SETGUI к Modbus-регистрам F407 через CAN485 DevBoard_V1 | `pcan_modbus_server`, STM32 HAL CAN | bxCAN FIFO0 и callbacks карты регистров |
| [`c/set-protocol/ports/stm32-bxcan`](c/set-protocol/ports/stm32-bxcan) | порт прикладного ProtoCAN для STM32, бывший SETCAN; сохранён API `PROTOCAN_*` | STM32 HAL CAN/RTC/TIM + общее ядро `pcan_id` | classic bxCAN; настройки платы предоставляет прошивка |
| [`c/set-protocol/ports/tms320f2812`](c/set-protocol/ports/tms320f2812) | SETProtocol v2 firmware service по segmented classic CAN | `set_protocol`, `set_can`, `set_firmware` | CAN TX, Flash erase/write/read, optional signature policy и reboot |
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | **STM32F103C8/F407VE/G474VE и TMS320F2812/F28335-порты в комплекте**; проекты STM32 подключены через templates, есть boot main и вход из приложения |
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
| [`c/global-time`](c/global-time) | программные часы 1 мс, ProtoCAN RTCSETUP, готовность платы и атомарная метка логгера | C99 | IRQ lock, CAN TX, такт 1 мс — **порт TMS320F28335 eCAN-A / CPU Timer2 в комплекте** |
| [`c/firmware-info`](c/firmware-info) | SemVer, build stamp и git build ID работающего образа | C99 | config-порты STM32F1/F4/G4 и К1921ВК028; упаковка для Modbus/SETGUI |
| [`c/parallel-nand`](c/parallel-nand) | parallel NAND x8: raw read/program/erase, профили Micron MT29F1G08 и Hynix HY27UF084G2M, bad-block marker и банковое окно GAS для SETGUI | C99, опционально `set-protocol/pcan_gas` | **порты TMS320F2812, STM32F103RC/ZE, STM32F407VE и STM32G474CEU6; запись требует управляемого WP#** |
@@ -64,6 +65,7 @@ templates/
| Инструмент | Что делает |
|---|---|
| [`tools/firmware-build/fpga`](tools/firmware-build/fpga/README.md) | общий итератор версии и HDL-паспорт для Xilinx ISE, Vivado и Altera/Intel Quartus; контроль `.bit`/`.sof`, manifest, SHA-256 и архив |
| [`tools/firmware-publish`](tools/firmware-publish) | единый BAT и конфигурации для проверки и публикации `.hex` Keil / `.bin` CCS 12 в каталоге SETGUI |
Общие тренды для Android GUI и SETGUI: [формат JSON, C99-ядро и адаптеры](c/set-protocol/docs/GUI_TRENDS.md).
@@ -118,7 +120,7 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20, firmware-info |
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
| `CAN_to_RS485` | set-protocol legacy transport, python/protocan |
| `candleLight_fw` | эталон разделения общей логики и G431/G474 FDCAN-порта; общий ProtoCAN Boot переносится по контракту `protocan-boot` |
| `CANdle_stm32` (candleLight) | G431/G474 FDCAN board-support; CAN/UART-загрузчик G474 и вход из приложения подключены к `protocan-boot/ports/stm32g474` через templates |
| `SETGUI` | python/protocan |
| `SETGUI`, Android GUI и новые устройства SET | set-protocol |
| `k1921vk028` | rtc-service |

View File

@@ -0,0 +1,30 @@
import importlib.util # Загружает общий модуль подготовки непосредственно из проверяемой копии templates.
import json # Проверяет сохранение пользовательских полей при резервировании версии 166.
from pathlib import Path # Все файлы теста находятся в изолированном временном каталоге.
import tempfile # Исключает запись версий и паспортов в рабочие прошивки пользователя.
import unittest # Запускает проверки без зависимостей SETGUI и Qt.
TOOLS = Path(__file__).resolve().parents[1] / "tools" # Тест переносится вместе с c/firmware-info в полный checkout templates.
SPEC = importlib.util.spec_from_file_location("checked_c2000_build", TOOLS / "c2000_build.py") # Выбирает одну реализацию общего процесса.
BUILD = importlib.util.module_from_spec(SPEC) # Импорт не выполняет prebuild и не меняет счётчики версии.
SPEC.loader.exec_module(BUILD) # Проверяет реальный модуль, а не копию функций внутри теста.
class LegacyVersionTests(unittest.TestCase): # Подтверждает совместимость общей подготовки с JSON-конфигурацией 166.
def test_json_counter_increments_once_and_keeps_settings(self): # Перенос в templates не должен удваивать резервирование версии.
with tempfile.TemporaryDirectory() as temporary: # Версия настоящего проекта 166 остаётся неизменной.
project = Path(temporary) # Минимальный проект использует прежние настройки без нового аппаратного профиля.
settings = {"product": "Balsam-166-PM35", "target": "TMS320F28335", "version": [1, 0, 999], "increment_version": True, "max_image_bytes": 32508, "copy_to": "../Bin"} # Patch 999 проверяет перенос в minor по общему контракту версии.
config = project / "firmware_identity.json" # Сохраняемый счётчик расположен в том же JSON, что в ветке 166.
config.write_text(json.dumps(settings), encoding="utf-8") # Русские данные и настройки сохраняются как UTF-8.
templates = TOOLS.parents[2] # Полный путь templates восстанавливается из c/firmware-info/tools.
metadata = BUILD.prepare(project, templates, project / "Debug/generated") # Выполняет настоящий общий prebuild без компиляции и сети.
saved = json.loads(config.read_text(encoding="utf-8")) # Проверяет именно сохранённую версию, а не только возвращённые метаданные.
self.assertEqual(saved["version"], [1, 1, 0]) # Номер увеличен один раз с переносом patch в minor.
self.assertEqual(metadata["version"], saved["version"]) # Паспорт и сохраняемый счётчик описывают одну попытку компиляции.
self.assertEqual(metadata["max_image_bytes"], 32508) # Историческая граница EEPROM 166 остаётся обязательной для postbuild.
self.assertEqual(saved["copy_to"], settings["copy_to"]) # Общий генератор сохраняет пользовательские поля, даже если сеть выполняет Публикатор.
if __name__ == "__main__": # Импорт набора проверок не запускает их повторно.
unittest.main() # Позволяет проверить совместимость прямой командой Python.

View File

@@ -0,0 +1,87 @@
"""Проверяет совместимость postbuild, экспорт двух образов и отказ от устаревшего Flash OUT.""" # Фиксирует контракт общего выпуска без зависимости от установленного TI-компилятора.
import json # Формирует паспорт текущей сборки для проверки архива и контрольных сумм.
from pathlib import Path # Создаёт независимые пути проекта, компилятора и результатов в каждом тесте.
import subprocess # Использует штатное исключение ненулевого кода проектного сборщика.
import sys # Подключает общий модуль из каталога tools текущей копии templates.
import tempfile # Изолирует проверочные файлы от реальных прошивок и каталогов GUI.
import unittest # Организует проверки публичного контракта postbuild стандартной библиотекой Python.
from unittest.mock import patch # Подменяет только внешние инструменты, оставляя настоящее архивирование и SHA-256.
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools')) # Находит проверяемую реализацию относительно теста без путей конкретной рабочей станции.
import c2000_postbuild as c2000 # Вызывает тот же postbuild, который запускается штатным postcompile.bat.
from identity import verify # Проверяет байты скопированных образов по настоящему manifest.
class PostbuildTests(unittest.TestCase): # Проверяет общую цепочку от готового OUT до экспортированной папки выпуска.
def setUp(self): # Для каждой проверки создаёт отдельный проект с типичной структурой CCS.
temporary = tempfile.TemporaryDirectory() # Все файлы сценария принадлежат временному каталогу теста.
self.addCleanup(temporary.cleanup) # Освобождает только каталог данной проверки при любом результате.
self.root = Path(temporary.name) # Корень не зависит от реальной прошивки и установленного templates.
self.build = self.root / 'project/Debug' # Проверяет соглашение о расположении сборщика рядом с конфигурациями CCS.
self.build.mkdir(parents=True) # Подготавливает рабочий каталог внешних инструментов.
self.compiler = self.root / 'compiler' # Позволяет проверить передачу компилятора текущего проекта вместо фиксированного пути.
(self.compiler / 'bin').mkdir(parents=True) # Воспроизводит структуру установленного C2000 compiler.
(self.compiler / 'bin/hex2000.exe').write_bytes(b'fake tool') # Проверка наличия инструмента реальна, а выполнение будет подменено.
self.source = self.root / 'Bin/Firmware.out' # Основной OUT размещён вне Debug, как в рабочем проекте пользователя.
self.source.parent.mkdir() # Создаёт отдельный каталог исходного COFF и итогового Flash COFF.
self.source.write_bytes(b'RAM image') # Даёт узнаваемые байты для проверки, что основной образ не подменён Flash-вариантом.
self.metadata = self.build / 'metadata.json' # Один паспорт используется обоими вариантами текущего выпуска.
self.metadata.write_text(json.dumps({'build_id': 'NOGIT000', 'built_at_utc': '2026-10-14T13:00:00+00:00', 'source': {'commit': None}, 'version': [1, 2, 3]}), encoding='utf-8') # Фиксированное время проверяет выбор папки по prebuild, а не времени файлов.
self.output = self.build / 'firmware' # Локальный выпуск получает оба образа и их сопроводительные файлы.
self.export = self.root / 'GUI' # Экспорт изолирован от настоящего GUI и сетевых ресурсов.
def fake_tool(self, command, **kwargs): # Имитирует границу внешних инструментов, оставляя реальную логику упаковки и проверки.
if '--postbuild' in command: # Вложенная Flash-сборка обязана использовать режим без повторного Make.
self.assertEqual(Path(command[command.index('--compiler') + 1]), self.compiler.resolve()) # Проверяет выбор текущей версии TI compiler.
self.assertEqual(Path(command[command.index('--build-dir') + 1]), self.build.resolve()) # Проверяет использование текущих объектов и generated-паспорта.
image = Path(command[command.index('--output') + 1]) # Результат должен создаваться по переданному временному пути.
self.assertNotEqual(image.parent, self.source.parent) # Запрещает выдавать существующий OUT из Bin за результат текущего вызова.
image.write_bytes(b'current Flash image') # Создаёт узнаваемый новый COFF для сравнения всех копий.
image.with_suffix('.map').write_text('codestart 0033fff6', encoding='utf-8') # Воспроизводит обязательный второй результат сборщика.
else: # Обычная SCI8-конверсия должна продолжать использовать основной RAM OUT.
expected = 'Firmware_flash.out' if '_flash_rs.bin' in command[command.index('-o') + 1] else self.source.name # Только отдельная RS-конверсия должна брать Flash COFF.
self.assertEqual(Path(command[1]).name, expected) # Проверяет, что EEPROM BIN и Flash RS BIN создаются из разных исходных образов.
Path(command[command.index('-o') + 1]).write_bytes(b'EEPROM image') # Выпускает HEX/BIN с отличимыми байтами основного варианта.
Path(command[command.index('-map') + 1]).write_text('RAM map', encoding='utf-8') # Сохраняет карты, необходимые настоящему архиватору.
return subprocess.CompletedProcess(command, 0) # Подтверждает только имитируемый инструмент, не подменяя проверки выпуска.
def run_postbuild(self): # Вызывает публичную функцию с теми же путями, что передаёт BAT из CCS.
return c2000.postbuild(self.compiler, self.build, str(self.source.with_suffix('')), self.metadata, self.output, self.export) # Включает настоящий экспорт и проверку manifest.
def test_project_without_flash_builder_keeps_eeprom_workflow(self): # Старые проекты должны работать без новых настроек или Flash-файлов.
with patch.object(c2000.subprocess, 'run', side_effect=self.fake_tool) as calls: # Подменяет только TI Hex Utility.
result = self.run_postbuild() # Выполняет полный прежний выпуск EEPROM.
self.assertEqual(calls.call_count, 2) # Для HEX и BIN выполнены два вызова; дополнительного сборщика не было.
self.assertEqual(self.source.read_bytes(), b'RAM image') # Основной OUT не изменён упаковкой.
self.assertFalse(self.source.with_name('Firmware_flash.out').exists()) # Отсутствие builder не создаёт фиктивный Flash-образ.
self.assertEqual(verify(result, Path(str(result) + '.manifest.json'))['size_bytes'], len(b'EEPROM image')) # Проверяет настоящее содержимое EEPROM BIN и его паспорт.
def test_flash_and_eeprom_share_current_export_with_separate_checksums(self): # Новый выпуск должен содержать оба варианта под своими именами.
(self.build.parent / 'build_flash.py').write_text('# Сборщик проекта подключён явно.\n', encoding='utf-8') # Наличие сценария включает дополнительный шаг postbuild.
with patch.object(c2000.subprocess, 'run', side_effect=self.fake_tool) as calls: # Flash и hex2000 имитируются на границе процессов.
self.run_postbuild() # Архиватор и экспорт работают без подмены.
self.assertEqual(calls.call_count, 4) # Один Flash-вызов, две EEPROM-конверсии и отдельный RS BIN подтверждают отсутствие рекурсивного Make.
folder = self.export / 'NOGIT000_2026-10-14_16-00-00_UTC+3' # Имя следует времени prebuild с переводом в московский UTC+3.
for directory in (self.source.parent, self.output, folder): # Проверяет копию для CCS, плоский выпуск и архив для GUI.
self.assertEqual((directory / 'Firmware_flash.out').read_bytes(), b'current Flash image') # Каждая копия содержит новый Flash COFF.
self.assertTrue((directory / 'Firmware_flash.map').is_file()) # Карта Flash не теряется при сохранении или экспорте.
for name in ('Firmware_flash.out', 'Firmware_NOGIT000.bin', 'Firmware_flash_rs.bin'): # Контрольные суммы каждого варианта должны проверяться независимо.
verify(folder / name, folder / (name + '.manifest.json')) # Использует реальные SHA-256 экспортированных байтов.
self.assertEqual((folder / self.source.name).read_bytes(), b'RAM image') # Архив сохраняет исходный RAM OUT вместе с новым Flash OUT.
def test_failed_or_incomplete_builder_cannot_export_stale_flash(self): # Ошибка или отсутствие карты должны остановить выпуск до экспорта старого COFF.
(self.build.parent / 'build_flash.py').write_text('# Подключён неисправный сборщик для проверки отказа.\n', encoding='utf-8') # Включает путь Flash-проверки.
stale = self.source.with_name('Firmware_flash.out') # В Bin уже есть образ от предыдущей успешной сборки.
stale.write_bytes(b'old Flash image') # Отличимые байты выявят ошибочную повторную публикацию.
for failure in (subprocess.CalledProcessError(1, 'builder'), None): # Проверяет ненулевой код и ложный успех без обязательных результатов.
with self.subTest(failure=failure): # Даёт отдельную диагностику двум независимым отказам.
with patch.object(c2000.subprocess, 'run', side_effect=failure) as calls: # Сборщик либо падает, либо не создаёт файлов.
with self.assertRaises((subprocess.CalledProcessError, ValueError)): # Оба отказа должны выйти наружу как ошибка postbuild.
self.run_postbuild() # Не разрешает продолжение EEPROM-конверсии после сбоя Flash.
self.assertEqual(calls.call_count, 1) # После отказа Flash ни hex2000, ни новый сборщик не запускаются.
self.assertEqual(stale.read_bytes(), b'old Flash image') # Предыдущий образ сохраняется, но не считается новым результатом.
self.assertFalse(self.export.exists()) # Неполная сборка не создаёт выпуск для GUI.
if __name__ == '__main__': # Позволяет запускать проверки отдельно или через unittest discover.
unittest.main() # Возвращает ненулевой код при нарушении контракта упаковки или экспорта.

View File

@@ -0,0 +1,111 @@
"""Tests for preserving exact firmware bytes under pre-build provenance folders."""
import json
from pathlib import Path
import sys
import tempfile
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
from identity import archive_build, build_folder_name, export_build, verify
class ArchiveTests(unittest.TestCase):
def metadata(self, stamp="2026-10-08T12:30:00+00:00"):
return {"source": {"commit": "a1b2c3d4" + "0" * 32, "dirty": False},
"templates": {"commit": None, "dirty": None},
"build_id": "NOGIT000", "built_at_utc": stamp,
"version": [1, 2, 3]}
def test_snapshot_preserves_image_and_records_exact_sha(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image = root / "firmware.bin"
payload = bytes(range(256))
image.write_bytes(payload)
metadata = root / "metadata.json"
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
result = archive_build(image, metadata, root / "out")
folder = Path(result["directory"])
self.assertEqual(folder.name, "a1b2c3d4_2026-10-08_15-30-00_UTC+3")
self.assertEqual((folder / image.name).read_bytes(), payload)
self.assertEqual(image.read_bytes(), payload)
self.assertEqual(json.loads((folder / "firmware_identity.json").read_text()), self.metadata())
manifest = folder / (image.name + ".manifest.json")
self.assertEqual(verify(folder / image.name, manifest)["size_bytes"], 256)
(folder / image.name).write_bytes(b"corrupted")
with self.assertRaises(ValueError):
verify(folder / image.name, manifest)
def test_prebuild_times_keep_two_builds_separate(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.hex", root / "metadata.json"
image.write_bytes(b":00000001FF\r\n")
folders = []
for stamp in ("2026-10-08T12:30:00+00:00", "2026-10-08T12:31:00+00:00"):
metadata.write_text(json.dumps(self.metadata(stamp)), encoding="utf-8")
folders.append(Path(archive_build(image, metadata, root / "out")["directory"]))
self.assertNotEqual(folders[0], folders[1])
self.assertTrue(all((folder / image.name).is_file() for folder in folders))
def test_export_copies_current_folder_only(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.bin", root / "metadata.json"
image.write_bytes(b"exact current build")
# A folder with a later timestamp must not be selected for this build.
metadata.write_text(json.dumps(self.metadata("2026-10-08T12:40:00+00:00")), encoding="utf-8")
archive_build(image, metadata, root / "local")
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
result = archive_build(image, metadata, root / "local", export=root / "exported")
copied = Path(result["exported_directory"])
self.assertEqual([p.name for p in (root / "exported").iterdir()],
["a1b2c3d4_2026-10-08_15-30-00_UTC+3"])
self.assertEqual((copied / image.name).read_bytes(), image.read_bytes())
self.assertTrue((copied / "firmware_identity.json").is_file())
self.assertTrue((copied / (image.name + ".sha256")).is_file())
verify(copied / image.name, copied / (image.name + ".manifest.json"))
def test_complete_folder_export_keeps_nested_files_and_old_destination(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.bin", root / "metadata.json"
image.write_bytes(b"image")
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
folder = Path(archive_build(image, metadata, root / "local")["directory"])
(folder / "notes").mkdir()
(folder / "notes" / "release.txt").write_text("release notes", encoding="utf-8")
old = root / "exported" / "old_build"
old.mkdir(parents=True)
(old / "keep.txt").write_text("keep", encoding="utf-8")
copied = export_build(folder, root / "exported", image.name)
self.assertEqual((copied / "notes" / "release.txt").read_text(), "release notes")
self.assertEqual((old / "keep.txt").read_text(), "keep")
with self.assertRaises(ValueError):
export_build(folder, folder / "recursive", image.name)
def test_offset_normalizes_to_utc_plus_three_and_dirty_is_visible(self):
metadata = self.metadata("2026-10-08T15:30:00+03:00")
metadata["source"]["dirty"] = True
self.assertEqual(build_folder_name(metadata), "a1b2c3d4+_2026-10-08_15-30-00_UTC+3")
def test_folder_date_rolls_over_at_utc_plus_three_midnight(self):
metadata = self.metadata("2026-10-08T22:30:00+00:00")
self.assertEqual(build_folder_name(metadata), "a1b2c3d4_2026-10-09_01-30-00_UTC+3")
self.assertEqual(metadata["built_at_utc"], "2026-10-08T22:30:00+00:00")
def test_archive_without_git_has_explicit_placeholder(self):
metadata = self.metadata()
metadata["source"]["commit"] = None
self.assertEqual(build_folder_name(metadata), "NOGIT000_2026-10-08_15-30-00_UTC+3")
def test_invalid_provenance_cannot_be_a_folder_name(self):
metadata = self.metadata()
metadata["source"]["commit"] = "../outside"
with self.assertRaises(ValueError):
build_folder_name(metadata)
metadata = self.metadata("2026-10-08T12:30:00")
with self.assertRaises(ValueError):
build_folder_name(metadata)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,83 @@
"""Проверяет общий сохраняемый итератор версии до компиляции CCS, Keil и CLI.""" # Запуск: python -m unittest discover -s c/firmware-info/tests -p test_version_iterator.py.
import importlib.util # Загружает тот же генератор, который вызывается штатными BAT templates.
import json # Проверяет исходный JSON и метаданные после резервирования версии.
from pathlib import Path # Изолирует проверочные настройки и generated во временном каталоге.
import tempfile # Удаляет только собственные файлы после каждой проверки.
import unittest # Проверяет последовательность номеров, ручной режим и отказы записи.
from unittest.mock import patch # Воспроизводит ошибки файловой системы без изменения прав пользователя.
TOOL = Path(__file__).resolve().parents[1] / "tools/identity.py" # Находит общую реализацию относительно файла теста.
spec = importlib.util.spec_from_file_location("shared_iterator", TOOL) # Не импортирует проектные обёртки или другой генератор из PATH.
identity = importlib.util.module_from_spec(spec) # Создаёт отдельный экземпляр модуля для изолированной подмены отказов.
spec.loader.exec_module(identity) # Загружает функции без выполнения CLI и без изменения рабочих версий.
class VersionIteratorTests(unittest.TestCase): # Проверяет поведение общего prepare независимо от IDE.
def setUp(self): # Готовит настройки другого изделия только во временном каталоге.
temporary = tempfile.TemporaryDirectory(prefix="shared iterator ") # Пробелы в пути не должны влиять на атомарную запись настроек.
self.addCleanup(temporary.cleanup) # Удаляет временные файлы даже после ожидаемого отказа генерации.
self.root = Path(temporary.name) # Ограничивает все изменения проверки собственным каталогом.
self.config = self.root / "config.json" # Отделяет исходную настройку от generated/firmware_identity.json.
self.output = self.root / "generated" # Моделирует отдельный каталог текущей конфигурации сборки.
self.settings = {"product": "Другое изделие", "target": "host", "version": [1, 0, 4], "increment_version": True, "extra": {"keep": "настройка"}} # Дополнительное поле проверяет сохранность проектных настроек в UTF-8.
self.save() # Устанавливает номер, на котором обнаружилась остановка прямого подключения.
def save(self): # Сохраняет выбранные входные настройки для текущего сценария.
self.config.write_text(json.dumps(self.settings, ensure_ascii=False), encoding="utf-8") # Не использует рабочий счётчик какого-либо проекта.
def prepare(self): # Запускает публичный общий API с детерминированным состоянием Git.
with patch.object(identity, "git_state", return_value={"commit": None, "dirty": None}): # Доступ к Git не влияет на проверку счётчика.
return identity.prepare(self.root, self.root, self.config, self.output) # Вызывает тот же prepare, что CCS, Keil и CLI.
def test_sequence_and_matching_outputs(self): # Воспроизводит увеличение после 1.0.4 и отсутствие повторного номера.
for expected in ([1, 0, 5], [1, 0, 6]): # Каждый общий pre-build должен занимать ровно один номер.
metadata = self.prepare() # Генерирует реальный заголовок и JSON после атомарного резервирования.
settings = json.loads(self.config.read_text(encoding="utf-8")) # Читает сохранённый источник следующей версии.
self.assertEqual(settings["version"], expected) # Проверяет увеличение исходного JSON, а не только отображаемой версии.
self.assertEqual(settings["extra"], self.settings["extra"]) # Сохраняет неизвестные общему генератору поля проекта.
self.assertEqual(metadata["version"], expected) # Возвращаемые метаданные содержат уже занятый номер.
self.assertEqual(json.loads((self.output / "firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Сохранённый паспорт совпадает с источником версии.
self.assertIn(f"#define FIRMWARE_VERSION_PATCH {expected[2]}U", (self.output / "firmware_identity.h").read_text(encoding="utf-8")) # Компилятор получает тот же patch, что manifest.
def test_manual_mode_preserves_config_bytes(self): # Старые проекты без флага и явно отключённый итератор сохраняют ручную версию.
for enabled in (None, False): # None обозначает отсутствие поля, а не допустимое значение JSON-флага.
self.settings.pop("increment_version", None) # Сначала моделирует старую конфигурацию без нового переключателя.
if enabled is False: self.settings["increment_version"] = False # Затем проверяет явное отключение без удаления поля.
self.save() # Записывает исходный формат, который генератор не должен переформатировать.
original = self.config.read_bytes() # Сравнивает байты, чтобы заметить любую лишнюю запись.
self.assertEqual(self.prepare()["version"], [1, 0, 4]) # Ручной номер используется без увеличения.
self.assertEqual(self.config.read_bytes(), original) # Исходный JSON остаётся побайтно прежним.
def test_carry_and_exhaustion(self): # Проверяет ограничения трёх разрядов контракта firmware-info v1.
for previous, expected in (([1, 0, 999], [1, 1, 0]), ([1, 255, 999], [2, 0, 0])): # Patch переносится после 999, minor после 255.
self.settings["version"] = previous # Задаёт последнюю версию перед границей разряда.
self.save() # Передаёт граничное значение публичному prepare через JSON.
self.assertEqual(self.prepare()["version"], expected) # Проверяет перенос вместе с реальной записью настроек.
self.settings["version"] = [255, 255, 999] # Устанавливает последний допустимый номер всего контракта.
self.save() # Сохраняет исходный предел перед ожидаемым отказом.
original = self.config.read_bytes() # Переполнение не должно сбрасывать номер в JSON.
with self.assertRaises(ValueError): self.prepare() # Требует явную ошибку вместо повторного использования версии.
self.assertEqual(self.config.read_bytes(), original) # Сохраняет последний занятый номер без частичной записи.
def test_invalid_flag_does_not_change_config(self): # Не допускает неявное включение счётчика строками, числами или null.
for value in ("true", "false", 1, 0, None, [], {}): # Только JSON true и false являются допустимыми значениями переключателя.
self.settings["increment_version"] = value # Передаёт неверный тип через настоящий JSON.
self.save() # Фиксирует исходные байты для проверки отказа до резервирования.
original = self.config.read_bytes() # Ошибка настройки не должна занимать номер версии.
with self.assertRaises(ValueError): self.prepare() # Останавливает генерацию до изменения счётчика.
self.assertEqual(self.config.read_bytes(), original) # Подтверждает сохранение исходного JSON при отказе проверки.
def test_generation_failure_consumes_number(self): # Номер неудачной попытки не должен использоваться следующей сборкой.
with patch.object(identity, "write_atomic", side_effect=OSError("header unavailable")): # Воспроизводит отказ записи заголовка после сохранения следующей версии.
with self.assertRaises(OSError): self.prepare() # Передаёт ошибку вызывающему BAT без маскировки успешным завершением.
self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], [1, 0, 5]) # Резервирование фиксируется до генерации выходных файлов.
self.assertEqual(self.prepare()["version"], [1, 0, 6]) # Следующая попытка занимает следующий номер, даже если прежняя не дошла до компиляции.
def test_failed_config_replace_preserves_original(self): # Атомарная замена не должна портить настройки при отказе файловой системы.
original = self.config.read_bytes() # Сохраняет эталон исходного номера и остальных полей.
with patch.object(identity.Path, "replace", side_effect=PermissionError("config unavailable")): # Имитирует отказ фиксации временного файла вместо записи нового JSON.
with self.assertRaises(PermissionError): self.prepare() # Останавливает pre-build, если номер нельзя надёжно сохранить.
self.assertEqual(self.config.read_bytes(), original) # Исходные настройки не заменены частичным или неверным файлом.
self.assertEqual(list(self.root.glob(".firmware-version-*")), []) # При отказе не остаются собственные временные файлы счётчика.
if __name__ == "__main__": unittest.main() # Любое нарушение общего итератора возвращает ненулевой код процесса.

View File

@@ -0,0 +1,436 @@
"""Единая подготовка паспорта и проверенного RAM/Flash-комплекта C2000 для CCS и Публикатора.""" # Сетевой выпуск выполняет отдельный Публикатор SETGUI после импорта подготовленной папки.
import argparse # Разбирает одинаковые аргументы защищённого postbuild для CCS Debug и Release.
import base64 # Декодирует точные байты секций COFF из официального XML-вывода TI ofd2000.
from datetime import datetime # Восстанавливает слова UTC из паспорта, созданного перед компиляцией.
import hashlib # Проверяет точные байты конечного BIN и сопроводительного SHA-256.
import importlib.util # Подключает штатный механизм экспорта только после проверки временного выпуска.
import json # Сверяет изделие, аппаратный профиль и идентификацию текущей сборки.
from pathlib import Path # Разрешает пути относительно выбранной конфигурации CCS, а не текущего терминала.
import re # Читает абсолютные EEPROM-символы из карты линкера TI.
import shutil # Копирует только полностью проверенные файлы из временного каталога.
import struct # Разбирает 16-битные слова SCI8 и адреса загрузочных блоков C28x.
import subprocess # Вызывает общую BAT-точку конверсии без публикации во внешнее приложение.
import tempfile # Изолирует непроверенные результаты общего postcompile от постоянных выпусков.
import xml.etree.ElementTree as ET # Проверяет фактические LOAD/RUN адреса Flash COFF без собственного двоичного парсера TI.
import sys # Запускает общий CLI и подключает конвертор без зависимости от GUI.
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / 'python')) # Контракт защищённой Flash берётся из того же checkout templates, что общий сборщик.
from protocan.f28335_flash import APP_START, APP_DATA, APP_STOP, coff_sections, validate_words # Проверяет отсутствие сектора A и описание FAB1 одинаково для COFF и SCI8.
def numeric_define(header, name): # Читает единственное числовое C-определение без исполнения выражений из заголовка платы.
text = header.read_text(encoding="utf-8-sig") # Поддерживает UTF-8 с BOM для пользовательских заголовков конфигурации.
text = re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S) # Удаляет комментарии перед поиском настоящих директив препроцессора.
values = re.findall(r"^\s*#\s*define\s+" + re.escape(name) + r"\s+([^\n]+)", text, re.M) # Требует определение нужного макроса, а не упоминание в тексте.
if len(values) != 1 or not re.fullmatch(r"(?:0[xX][0-9a-fA-F]+|[0-9]+)[uUlL]*", values[0].strip()): # Исключает неоднозначность и выражения, которые могут отличаться от C-препроцессора.
raise ValueError(f"{header}: define {name} once as an integer") # Останавливает выпуск при неподдерживаемой структуре общей разметки.
return int(values[0].strip().rstrip("uUlL"), 0) # Преобразует десятичную либо шестнадцатеричную величину в число Python.
def git_state(repository): # Снимает происхождение выбранного рабочего дерева с локальным safe.directory без изменения глобальных настроек Git.
directory = repository.resolve() # Рабочий каталог CCS может быть вложен в репозиторий приложения.
trusted = next((path for path in (directory, *directory.parents) if (path / ".git").exists()), directory) # Git проверяет корень checkout; .git-файл также учитывает подключённый templates submodule.
command = ["git", "-c", "safe.directory=" + trusted.as_posix(), "-C", str(directory)] # Локальное доверие ограничено корнем явно выбранной зависимости или исходников проекта.
try: # Отсутствие Git сохраняет прежний явный NOGIT-контракт общей библиотеки.
commit = subprocess.check_output(command + ["rev-parse", "HEAD"], stderr=subprocess.DEVNULL, text=True, encoding="utf-8").strip() # Получает полный SHA именно компилируемого дерева.
dirty = bool(subprocess.check_output(command + ["status", "--porcelain", "--untracked-files=normal"], stderr=subprocess.DEVNULL, text=True, encoding="utf-8").strip()) # Учитывает новые и изменённые исходники при формировании знака плюс.
return {"commit": commit, "dirty": dirty} # Возвращает совместимую с общим генератором структуру происхождения.
except (OSError, subprocess.CalledProcessError): # Не выдаёт неизвестную ревизию за чистое дерево.
return {"commit": None, "dirty": None} # Генератор пометит такую сборку идентификатором NOGIT000.
# Объявляет функцию read_version и её входные параметры.
def read_version(header):
# Читает исходный файл или образ для последующей проверки и разбора.
text = header.read_text(encoding="utf-8-sig")
# Задаёт text по выражению справа; значение используется последующими операциями.
text = re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S)
# Задаёт version по выражению справа; значение используется последующими операциями.
version = []
# Перебирает part, limit in (("MAJOR", 255), ("MINOR", 255), ("PATCH", 999)) в указанном порядке.
for part, limit in (("MAJOR", 255), ("MINOR", 255), ("PATCH", 999)):
# Задаёт name по выражению справа; значение используется последующими операциями.
name = "FIRMWARE_VERSION_" + part
# Задаёт definitions по выражению справа; значение используется последующими операциями.
definitions = re.findall(r"^\s*#\s*define\s+" + name + r"\b([^\n]*)", text, re.M)
# Проверяет условие if len(definitions) != 1 перед выполнением следующего блока.
if len(definitions) != 1:
# Останавливает текущую операцию с исключением ValueError(f"{header}: define {name} exactly once").
raise ValueError(f"{header}: define {name} exactly once")
# Задаёт value по выражению справа; значение используется последующими операциями.
value = definitions[0].strip()
# Проверяет условие if not re.fullmatch(r"(?:0|[1-9][0-9]*)[uU]?", value) перед выполнением следующего блока.
if not re.fullmatch(r"(?:0|[1-9][0-9]*)[uU]?", value):
# Останавливает текущую операцию с исключением ValueError(f"{header}: {name} must be a decimal integer").
raise ValueError(f"{header}: {name} must be a decimal integer")
# Задаёт number по выражению справа; значение используется последующими операциями.
number = int(value.rstrip("uU"))
# Проверяет условие if not 0 <= number <= limit перед выполнением следующего блока.
if not 0 <= number <= limit:
# Останавливает текущую операцию с исключением ValueError(f"{header}: {name} must be in 0..{limit}").
raise ValueError(f"{header}: {name} must be in 0..{limit}")
# Продолжает текущий вызов, список или выражение: version.append(number); порядок аргументов сохраняется.
version.append(number)
# Возвращает version вызывающему коду и завершает функцию.
return version
def read_policy(project): # Загружает декларативные настройки памяти платы, оставляя общий алгоритм в templates.
path = project / "firmware_build_profile.json" # Профиль расположен рядом с CCS-проектом и версионируется вместе с linker-файлами.
return json.loads(path.read_text(encoding="utf-8-sig")) if path.is_file() else {} # Старые проекты 166 используют прежний JSON без обязательного нового файла.
def configured_version(project, settings): # Поддерживает ручной C-заголовок 167 и сохраняемый JSON-счётчик 166 одной реализацией.
header = read_policy(project).get("version_header") # Источник версии задаётся платой явно, без определения модели по имени каталога.
return read_version(project / header) if header else settings["version"] # Проверку диапазонов JSON выполняет существующий генератор identity.
def read_profile(project, templates, eeprom=None): # Получает физическую EEPROM из тех же C-заголовков, которыми пользуется компилятор.
policy = read_policy(project) # Все аппаратные пути и имена макросов остаются данными конкретной платы.
can_profile = policy.get("can_boot_profile") # Явное включение CAN-выпуска не меняет поведение старых проектов без этого поля.
if can_profile not in (None, "f28335-m95m02-ab"): # Поддерживаемый профиль соответствует реальному порту M95M02 с двумя слотами по 130560 октетов.
raise ValueError("Unsupported CAN boot profile: " + str(can_profile)) # Опечатка в профиле не должна незаметно отключать обязательный CAN-образ.
config = policy.get("eeprom") # Отсутствие профиля сохраняет контракт 166 с max_image_bytes в основном JSON.
if not config: # У старого проекта нет дополнительной аппаратной разметки для проверки MAP.
path = project / "firmware_identity.json" # Прямой общий postcompile также поддерживает отдельно переданный готовый паспорт.
settings = json.loads(path.read_text(encoding="utf-8-sig")) if path.is_file() else {} # Отсутствие проектной конфигурации не создаёт вымышленные ограничения памяти.
limit = settings.get("max_image_bytes") # Прежняя конфигурация 166 задаёт предел в физических октетах полного SCI8.
if limit is not None and (type(limit) is not int or limit < 24): # Не допускает отрицательную ёмкость, bool или размер меньше заголовка и терминатора SCI8.
raise ValueError("max_image_bytes must be an integer >= 24") # Не создаёт паспорт с непригодным проектным ограничением.
return {"max_image_bytes": limit} if limit is not None else {} # Старый предел EEPROM также применяется общим postbuild.
selected = numeric_define(project / config["selection_header"], config["selection_macro"]) # Читает только числовое определение, исключая исполнение текста заголовка.
variants = config["variants"] # Таблица связывает значение compiler define, название микросхемы и макрос физической ёмкости.
if str(selected) not in variants: # Неизвестное значение конфигурации не подменяется произвольным профилем.
raise ValueError("Unsupported EEPROM selection: " + str(selected)) # Ошибка возникает до компиляции и создания постоянного выпуска.
chosen = next((value for value in variants.values() if value["name"] == eeprom), None) if eeprom else variants[str(selected)] # CLI override сохраняет согласованный выбор отдельной тестовой сборки.
if chosen is None: # Неподдерживаемый профиль запрещён и при программном вызове общего модуля.
raise ValueError("Unsupported EEPROM profile: " + str(eeprom)) # Не создаёт паспорт с вымышленной ёмкостью.
layout = templates / config["layout_header"] # Разметка читается из фактически компилируемой зависимости, а не из другой копии templates.
total = numeric_define(layout, chosen["bytes_macro"]) # Ёмкость выражена физическими октетами EEPROM согласно общему C-контракту.
words = numeric_define(layout, config["param_words_macro"]) # Число параметров выражено 16-битными словами C28x и включает контрольную сумму.
limit = total - (words + 1) * 2 # Сохраняет параметры и последнее резервное слово; одно слово EEPROM занимает два октета.
if total <= 0 or total % 2 or words <= 0 or limit < 24: # Полная SCI8-таблица требует минимум 22 октета заголовка и два октета терминатора.
raise ValueError("Invalid EEPROM layout") # Не выпускает образ с отрицательной или неполной областью прошивки.
result = {"eeprom": chosen["name"], "eeprom_bytes": total, "eeprom_param_words": words, "max_image_bytes": limit, "ram_ranges": policy["ram_ranges"]} # Паспорт фиксирует аппаратный выбор и допустимые адреса приложения.
if can_profile and chosen["name"] == "M95M02": result["can_boot_profile"] = can_profile # Малая AT25256B не получает неподдерживаемый A/B-образ, даже если проект поддерживает второй аппаратный вариант.
return result # Prebuild сохраняет CAN-профиль, а postbuild проверяет его неизменность вместе с EEPROM и версией.
def load_identity(templates): # Загружает действующий генератор паспорта из явно выбранной библиотеки.
spec = importlib.util.spec_from_file_location("unified_identity_tools", templates / "c/firmware-info/tools/identity.py") # Обе схемы CCS используют одинаковый публичный контракт identity.
module = importlib.util.module_from_spec(spec) # Изолированный модуль не меняет импорты SETGUI и других проектов.
spec.loader.exec_module(module) # Ошибка отсутствующей зависимости возвращается вызывающему процессу до записи выпуска.
module.git_state = git_state # Локальное доверие Git ограничено выбранными рабочими деревьями без изменения глобального safe.directory.
return module # Подготовка и упаковка используют одну реализацию формата manifest.
def prepare(project, templates, output, eeprom=None, config=None): # Единый prebuild поддерживает исходный счётчик 166 и ручную версию 167.
project, templates, output = project.resolve(), templates.resolve(), output.resolve() # Все относительные пути фиксируются до запуска общих инструментов.
config = config.resolve() if config else project / "firmware_identity.json" # Старые BAT-вызовы могут явно передать прежний файл настроек.
settings = json.loads(config.read_text(encoding="utf-8-sig")) # Читает пользовательские поля без потери дополнительных настроек проекта.
policy, profile = read_policy(project), read_profile(project, templates, eeprom) # Физическая разметка проверяется до изменения счётчика версии.
identity = load_identity(templates) # Подключает генератор той библиотеки, которая включена в компиляцию.
if policy.get("version_header"): # Ручной заголовок остаётся единственным источником версии 167.
settings.update(version=configured_version(project, settings), increment_version=False) # Не увеличивает номер при повторном prebuild или ошибке компиляции.
with tempfile.TemporaryDirectory(prefix="c2000-identity-") as directory: # Временные настройки не переписывают пользовательский JSON.
temporary = Path(directory) / "config.json" # Временный файл нужен только публичному интерфейсу существующего генератора.
temporary.write_text(json.dumps(settings, ensure_ascii=False), encoding="utf-8") # Сохраняет ручной номер и все поля изделия в UTF-8.
metadata = identity.prepare(project, templates, temporary, output) # Генерирует прежние 12 слов паспорта без изменения протокола прошивки.
else: # Проект 166 продолжает резервировать следующий номер непосредственно в своём JSON.
metadata = identity.prepare(project, templates, config, output) # Итератор вызывается ровно один раз, сохраняя старый контракт precompile.
metadata.update(profile) # Предел EEPROM применяется и к старым проектам, и к новым аппаратным профилям.
if "eeprom_bytes" in profile: # Только явный аппаратный профиль требует сверки compiler define и происхождения разметки.
metadata["templates_root"] = str(templates) # Защищает postbuild от проверки по другой копии C-заголовков.
selection = policy["eeprom"] # Имена заголовка и макроса определены декларативным профилем платы.
value = next(key for key, item in selection["variants"].items() if item["name"] == profile["eeprom"]) # Вычисляет точное значение define для выбранной микросхемы.
guard = '\n#include "' + Path(selection["selection_header"]).name + '" /* Проверяет аппаратный профиль текущей компиляции. */\n' # Проверка включается в генерируемый C-заголовок паспорта.
guard += '#if ' + selection["selection_macro"] + ' != ' + value + ' /* Сверяет define компилятора с подготовленным паспортом. */\n#error EEPROM_profile_differs_from_prebuild /* Требует согласованную пересборку платы. */\n#endif /* Завершает проверку профиля EEPROM. */\n' # Compiler-only override не может незаметно изменить физическую разметку.
header = output / "firmware_identity.h" # Добавляет защиту после прежних констант версии и времени.
identity.write_atomic(header, header.read_text(encoding="utf-8") + guard) # Заголовок заменяется атомарно и всегда остаётся в UTF-8.
identity.write_atomic(output / "firmware_identity.json", json.dumps(metadata, ensure_ascii=False, indent=2) + "\n") # Сохраняет фактическую разметку для проверки после линковки.
return metadata # Публичный результат совместим с прежними host-тестами и проектными вызовами.
def convert(compiler, build_dir, artifact, metadata_path, stage, tools_templates): # Выполняет только конвертацию во временный каталог до общей проверки выпуска.
spec = importlib.util.spec_from_file_location("unified_c2000_converter", tools_templates / "c/firmware-info/tools/c2000_postbuild.py") # Использует единственную низкоуровневую реализацию hex2000 и сборки дополнительного Flash.
converter = importlib.util.module_from_spec(spec) # Изолирует импорт от старых командных точек входа и исключает рекурсивный postbuild.
sys.path.insert(0, str(tools_templates / "c/firmware-info/tools")) # Соседний identity нужен существующему конвертору при импорте.
try: # Любая ошибка конвертации остаётся внутри staged-выпуска.
spec.loader.exec_module(converter) # Загружает библиотечную функцию без запуска CLI.
source = Path(artifact) # CCS передаёт абсолютный базис OUT либо имя относительно активного BuildDirectory.
source = Path(str(source if source.is_absolute() else build_dir / source) + ".out") # Выбирает конкретный исходный файл текущей сборки.
with tempfile.TemporaryDirectory(prefix=".input-", dir=stage) as directory: # Дополнительные копии конвертора не попадают рядом с постоянным RAM OUT до проверки.
staged = Path(directory) / source.name # Имя артефакта сохраняется для штатного именования Flash и BIN.
shutil.copy2(source, staged) # Передаёт точные байты текущего COFF, сохраняя исходный файл.
converter.postbuild(compiler, build_dir, str(staged.with_suffix("")), metadata_path, stage) # Публикация и сеть отсутствуют: результаты ещё должны пройти проверки общего модуля.
finally: # Импорт другого проекта не должен наследовать временно добавленный каталог инструментов.
sys.path.pop(0) # Возвращает прежний порядок поиска Python-модулей даже при отказе TI.
def write_receipt(output, archive_name, image_name): # Передаёт Публикатору точную папку текущего проверенного выпуска.
value = {"schema": 1, "folder": archive_name, "manifest": image_name + ".manifest.json"} # Относительные имена сохраняют переносимость комплекта между компьютерами.
receipt = output / "latest_build.json" # Служебный указатель находится вне папки неизменяемого комплекта.
temporary = receipt.with_suffix(".tmp") # Атомарная замена не оставляет частичный JSON при прерывании записи.
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") # Записывает только локальный указатель без сетевой публикации.
temporary.replace(receipt) # Указатель меняется лишь после успешной проверки и копирования всех файлов.
def atomic_copy(source, destination): # Сохраняет проверенный файл без открытия старого результата на перезапись и без частичного содержимого.
destination = Path(destination) # Нормализует также строковые пути, переданные стандартным shutil.copytree.
with tempfile.NamedTemporaryFile(dir=destination.parent, prefix=".prepared-", delete=False) as stream: # Собственный временный файл наследует права каталога и находится на том же диске для атомарной замены.
temporary = Path(stream.name) # Запоминает только созданный текущим вызовом файл для безопасной очистки.
try: # Ошибка записи или замены должна оставить прежний результат пригодным для чтения.
shutil.copy2(source, temporary) # Сначала полностью записывает новые байты и сохраняет время исходного артефакта.
temporary.replace(destination) # Одной файловой операцией заменяет старый результат после успешной записи временного файла.
finally: # Незавершённый временный файл не должен попадать в будущий комплект Публикатора.
temporary.unlink(missing_ok=True) # Удаляет только собственный незавершённый файл; после успешной замены его уже нет.
return str(destination) # Сохраняет контракт copy_function стандартного shutil.copytree.
def verify_embedded(image, metadata, flash=False): # RAM и Flash SCI8 проверяются отдельно по разным допустимым адресам и ограничениям загрузчика.
data = image.read_bytes() # Проверяет содержимое файла, которое будет передано в EEPROM-программатор.
limit = 0xFC00 * 2 - 2 if flash else metadata.get("max_image_bytes", 0xFFFFFFFF) # RS Flash использует внешний буфер 0x200000..0x20FC00 и ещё два октета CRC; RAM BIN ограничен резервом физической EEPROM.
if not 24 <= len(data) <= limit: # Учитывает заголовки SCI8 вместе с кодом, не позволяя переписать резерв параметров либо выйти за буфер RS.
raise ValueError("SCI8 exceeds EEPROM firmware boundary or is too short") # Отвергает слишком большой BIN до создания постоянного каталога выпуска.
if data[:2] != b"\xaa\x08": # 0x08AA — обязательный ключ восьмибитной boot-таблицы TI.
raise ValueError("Invalid SCI8 header") # Не принимает HEX, COFF либо плоский дамп под видом загрузочного BIN.
stamp = datetime.fromisoformat(metadata["built_at_utc"].replace("Z", "+00:00")) # Берёт момент именно того prebuild, который подготовил C-заголовок.
build_id = metadata["build_id"] # Контракт firmware-info v1 хранит ровно восемь ASCII-символов идентификатора.
if not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", build_id) or stamp.utcoffset() is None or stamp.utcoffset().total_seconds(): # Исключает неоднозначную дату и неподдерживаемый идентификатор.
raise ValueError("Invalid prebuild identity") # Требует заново выполнить prebuild вместо выпуска чужих метаданных.
words = [1, *metadata["version"], stamp.year, stamp.month * 256 + stamp.day, stamp.hour * 256 + stamp.minute, stamp.second] # Независимо восстанавливает первые восемь 16-битных регистров общего контракта.
words += [ord(build_id[index]) * 256 + ord(build_id[index + 1]) for index in range(0, 8, 2)] # Упаковывает две ASCII-октеты в каждое из четырёх слов идентификатора.
expected = struct.pack("<12H", *words) # TI SCI8 хранит каждое слово полезных данных младшей октетой вперёд.
high, low = struct.unpack_from("<HH", data, 18) # Читает точку входа после ключа и восьми резервных слов загрузчика.
entry = (high << 16) | low # Адрес C28x передаётся старшим словом перед младшим.
offset, found, blocks, boot, loaded = 22, 0, [], 0, {} # Запоминает диапазоны, boot-переход и точные загружаемые слова для сопоставления Flash COFF с SCI8.
while offset + 2 <= len(data): # Перед чтением размера требует наличие полного 16-битного слова.
count, = struct.unpack_from("<H", data, offset) # Длина блока задана в адресуемых словах C28x, а не физических октетах.
offset += 2 # Пропускает размер перед адресом блока или терминатором таблицы.
if not count: # Нулевое слово завершает SCI8 без последующего адресного поля.
if offset != len(data) or found != 1 or (flash and boot != 1) or not any(start <= entry < end for start, end in blocks): # Дополнительно требует единственный двухсловный аппаратный переход Flash boot.
raise ValueError(f"SCI8 identity/entry/terminator mismatch: matching identities={found}") # Сообщает отсутствие встроенного паспорта вместо ложной подписи старого OUT.
if flash: validate_words(entry, loaded) # CRC FAB1 и границы сектора A проверяются по фактическим октетам SCI8 до публикации.
return entry, loaded # Flash-проверка сопоставит реальные загрузочные слова с секциями исходного COFF; RAM-вызов может игнорировать результат.
if offset + 4 + count * 2 > len(data): # Требует полный 32-битный адрес и все октеты полезной нагрузки.
raise ValueError("Truncated SCI8 section") # Не допускает выпуск усечённой секции даже при паспорте в её начале.
high, low = struct.unpack_from("<HH", data, offset) # Получает адрес назначения текущего блока в словах C28x.
address, end = (high << 16) | low, ((high << 16) | low) + count # Конец исключается из диапазона, поэтому соседние блоки могут соприкасаться.
allowed = (APP_START <= address < end <= APP_STOP) if flash else any(start <= address < end <= stop for start, stop in metadata.get("ram_ranges", ((0, 0x100000000),))) # Flash-приложение целиком находится ниже сектора A; RAM A/B по-прежнему ограничено своим payload-диапазоном.
if not allowed or any(address < old_end and start < end for start, old_end in blocks): # Любое перекрытие секций либо несовместимый тип таблицы блокирует выпуск.
raise ValueError("SCI8 block outside application RAM or overlapping") # Не выпускает Flash-таблицу как EEPROM/RAM-образ.
if len(blocks) >= metadata.get("max_sections", 0xFFFFFFFF): # CAN-порт F28335 принимает не более 64 секций; обычные RAM/Flash-профили сохраняют прежний контракт.
raise ValueError("SCI8 exceeds bootloader section count") # Совместимый размер файла не разрешает таблицу, которую аппаратный загрузчик не сможет разобрать.
blocks.append((address, end)) # Сохраняет проверенный диапазон для контроля следующих блоков и точки входа.
payload = data[offset + 4:offset + 4 + count * 2] # Ищет паспорт только внутри данных, исключая поля заголовка и адреса.
boot += int(flash and address == APP_START and count >= 16) # Ровно один блок содержит полный FAB1; аппаратный boot 0x33FFF6 запрещён.
loaded.update((address + index, value) for index, value in enumerate(struct.unpack("<" + "H" * count, payload))) # Фиксирует реальные 16-битные слова, которые RS-загрузчик запрограммирует во Flash.
found += sum(payload[index:index + 24] == expected for index in range(0, len(payload) - 23, 2)) # Учитывает лишь выровненные на слово экземпляры 12 регистров firmware-info.
offset += 4 + count * 2 # Переходит за полный адрес и полезные слова к следующему размеру.
raise ValueError("Missing SCI8 terminator") # Исчерпание файла без нулевого слова никогда не считается успешной конверсией.
def verify_package(image, metadata): # Связывает BIN, manifest и SHA-256 с тем же prebuild и аппаратной EEPROM.
manifest = json.loads(image.with_suffix(image.suffix + ".manifest.json").read_text(encoding="utf-8")) # Один контракт SHA/паспорта обслуживает отдельные RAM BIN, Flash COFF и Flash SCI8.
data = image.read_bytes() # Повторно считывает точные октеты перед постоянной публикацией.
digest = hashlib.sha256(data).hexdigest() # Вычисляет контрольную сумму независимо от общего seal.
if manifest.get("file") != image.name or manifest.get("size_bytes") != len(data) or manifest.get("sha256") != digest: # Требует согласованности имени, размера и содержимого файла.
raise ValueError("BIN does not match its manifest") # Не публикует повреждённый либо перепутанный комплект.
if any(manifest.get(key) != value for key, value in metadata.items()): # Проверяет также eeprom, физическую границу, версию и происхождение компилируемых templates.
raise ValueError("Manifest does not match prebuild identity/EEPROM") # Один лишь корректный SHA не разрешает паспорт другого аппаратного варианта.
if image.with_suffix(image.suffix + ".sha256").read_text(encoding="utf-8").strip() != f"{digest} {image.name}": # Общий sidecar содержит SHA и точное имя через два пробела.
raise ValueError("Invalid BIN SHA-256 sidecar") # Требует целостности всех трёх публикуемых файлов.
def verify_linker_profile(map_path, profile): # Сверяет аппаратную настройку Python с фактическими абсолютными символами линкера.
text = map_path.read_text(encoding="utf-8-sig") # MAP текущей конфигурации является обязательным результатом успешной линковки.
expected = {"_EEPROM_TOTAL_BYTES": profile["eeprom_bytes"], "_EEPROM_FIRMWARE_LIMIT_BYTES": profile["max_image_bytes"], "_EEPROM_PARAMS_START_WORD": profile["max_image_bytes"] // 2} # Символы разделяют физические октеты и адресуемые слова C28x.
for name, value in expected.items(): # Проверяет ёмкость, конец прошивки и начало сохраняемых параметров до запуска hex2000.
values = re.findall(r"^\s*(?:\w+\s+)?([0-9a-fA-F]{8})\s+" + re.escape(name) + r"\b", text, re.M) # Принимает формат глобальных TI-символов с номером страницы либо признаком ABS.
if not values or any(int(item, 16) != value for item in values): # Повторные списки MAP обязаны описывать один и тот же профиль.
raise ValueError(f"Linker EEPROM profile mismatch: {name}") # Выявляет define, переданный только компилятору или только линкеру, и устаревшую карту.
def verify_flash_coff(image, compiler): # Проверяет реальные секции COFF официальным TI-декодером и возвращает точные Flash-слова загрузки.
tool = compiler / "bin/ofd2000.exe" # Разбирает формат тем же установленным C2000 toolchain, который создал OUT.
document = ET.fromstring(subprocess.check_output([str(tool), "-x", str(image)], stderr=subprocess.STDOUT)) # XML содержит фактические адреса и двоичные данные секций, а не неподтверждённую соседнюю MAP.
coff = document.find(".//ti_coff") # Вариант предназначен строго для COFF ABI F28335.
if coff is None or coff.findtext("file_header/target_id") != "0x9d" or coff.findtext("file_header/exec") != "true": # Не принимает объект другого процессора, ELF либо незалинкованный модуль.
raise ValueError("Expected executable C2000 COFF Flash image") # Явно отделяет JTAG Flash OUT от обычного RAM OUT и чужих архитектур.
sections = {section.findtext("name"): section for section in coff.findall("section")} # Именованные выходные секции соответствуют отдельной Flash-карте линкера 167.
required = {".text": (APP_DATA, APP_STOP, APP_DATA, APP_STOP), ".flash_app": (APP_START, APP_DATA, APP_START, APP_DATA), "flash_rs": (0x8000, 0xC000, APP_DATA, APP_STOP), "ramfuncs": (0xC000, 0xD000, APP_DATA, APP_STOP)} # FAB1 находится в секторе H; все Flash LOAD-секции исключают постоянный сектор A, сохраняя прежние RUN-области RAM.
for name, (run_begin, run_end, load_begin, load_end) in required.items(): # Наличие автономного RAM-загрузчика и InitFlash обязательно для выпуска Flash-варианта.
section = sections.get(name) # Отсутствующая обязательная секция не маскируется другими именами или символами MAP.
if section is None: # Проверяет отдельную карту и startup, а не одно лишь расположение основного кода.
raise ValueError("Missing Flash COFF section: " + name) # Не выдаёт RAM COFF за пригодный для автономной Flash.
run, load, size = (int(section.findtext(field, "0"), 0) for field in ("physical_addr", "virtual_addr", "raw_data_size")) # Длина OFD задана физическими октетами, оба адреса — 16-битными словами CPU.
if not size or size % 2 or not run_begin <= run < run + size // 2 <= run_end or not load_begin <= load < load + size // 2 <= load_end or (name == ".flash_app" and size != 32): # FAB1 занимает ровно 16 слов; прочие секции целиком помещаются в разрешённых LOAD/RUN-областях ниже сектора A.
raise ValueError("Invalid Flash COFF LOAD/RUN section: " + name) # Отвергает stale linker, размещение API во Flash во время стирания и аппаратный вход вне 0x33FFF6.
loaded = {} # Сопоставляет слова всех загружаемых Flash-секций с будущей SCI8-таблицей RS.
for section in sections.values(): # Служебные debug/copy-секции адреса ноль в программирование не входят.
address = int(section.findtext("virtual_addr", "0"), 0) # Для COFF TI это загрузочный адрес содержимого секции.
raw = section.findtext("raw_data") # Пустая .ebss, NOLOAD или DSECT не задаёт байтов прошивки.
if raw is None or not 0x300000 <= address < 0x340000: # Не включает отладочные записи, ROM-таблицы и рабочую RAM в образ Flash.
continue # Права оставшихся загружаемых Flash-слов дополнительно проверит общий SCI8-валидатор.
data = base64.b64decode(raw) # Получает именно те октеты, которые записаны в COFF и должны оказаться в Flash SCI8.
if len(data) % 2: # Любая программируемая секция C28x должна содержать целые 16-битные слова.
raise ValueError("Odd Flash COFF section size") # Не допускает неоднозначную упаковку образа RS.
for index, value in enumerate(struct.unpack("<" + "H" * (len(data) // 2), data)): # Восстанавливает little-endian представление каждого физического слова COFF.
if address + index in loaded: # Два выходных диапазона не должны программировать одно слово повторно.
raise ValueError("Overlapping Flash COFF load sections") # Проверяет реальный OUT независимо от доверия к линкерной карте.
loaded[address + index] = value # Сохраняет ожидаемые данные для независимого сравнения с hex2000.
entry = int(coff.findtext("optional_file_header/entry_point", "0"), 0) # Получает реальную точку входа C runtime из заголовка COFF.
if entry not in loaded or not 0x300000 <= entry < 0x33FF80: # C startup должен находиться в полностью загружаемой исполняемой Flash.
raise ValueError("Flash COFF entry is not loaded") # Исключает OUT со старой RAM-точкой входа или отрезанным startup.
checked_entry, checked_loaded, _ = coff_sections(document) # Общий декодер дополнительно отвергает любой загружаемый RAW вне области приложения, включая OTP и RAM.
if checked_entry != entry or checked_loaded != loaded: raise ValueError('Flash COFF load data mismatch') # Ни одна секция не должна быть потеряна при независимом обходе OFD XML.
validate_words(entry, loaded) # Проверяет FAB1 и CRC по полному согласованному COFF-образу.
return entry, loaded # Полное сравнение с SCI8 обнаружит конверсию другого OUT, потерю секции или подмену одного слова.
def verify_flash_release(stage, artifact, metadata, compiler, identity, required=False): # Проверяет и отдельно маркирует Flash COFF/SCI8 в staged-выпуске до любого постоянного копирования.
base = Path(artifact.replace("\\", "/")).name # Использует исходное имя артефакта CCS без директории и расширения OUT.
coff, serial = stage / (base + "_flash.out"), stage / (base + "_flash_rs.bin") # JTAG COFF и RS SCI8 являются разными формами одного отдельного Flash-варианта.
if not coff.exists() and not serial.exists() and not required: # Проекты без build_flash.py и host-фикстуры сохраняют прежний защищённый выпуск RAM.
return # Наличие обычного RAM BIN само по себе не объявляет поддержку Flash.
if not coff.is_file() or not serial.is_file(): # Сборка Flash должна выпустить оба согласованных артефакта.
raise ValueError("Incomplete Flash COFF/SCI8 release") # Не экспортирует один старый OUT вместе с новым RAM-выпуском.
verify_linker_profile(coff.with_suffix(".map"), metadata) # Flash использует физическую EEPROM для параметров и не меняет её профиль при размещении кода внутри CPU.
if verify_flash_coff(coff, compiler) != verify_embedded(serial, metadata, flash=True): # Сравнивает entry и каждое загрузочное слово официального COFF с результатом hex2000.
raise ValueError("Flash SCI8 does not match its COFF") # Отклоняет stale конверсию даже при правильных независимых SHA-файлах.
pairs = [(path, "COFF") for path in stage.rglob(coff.name)] + [(path, "SCI8-FLASH") for path in stage.rglob(serial.name)] # Включает плоские результаты и все их копии внутри текущего архива.
for path, kind in pairs: # Сначала проверяет весь исходный комплект, не меняя даже временных паспортов при первом отказе.
verify_package(path, metadata) # Общий postcompile обязан сохранить текущие version/EEPROM/SHA для каждого результата.
if path.read_bytes() != (coff if kind == "COFF" else serial).read_bytes(): # Архивированная копия должна быть побайтно тем же проверенным вариантом.
raise ValueError("Flash archive differs from staged release") # Не позволяет экспортировать чужой Flash внутри корректного RAM-архива.
with tempfile.TemporaryDirectory(prefix=".variant-", dir=stage) as directory: # Вариантные JSON нужны только для подписи разных форматов и не попадают в постоянный выпуск.
metadata_path = Path(directory) / "metadata.json" # Общий seal принимает обычный путь и не требует изменения соседнего templates.
for path, kind in pairs: # Только проверенные Flash-файлы получают явный признак исполнения и транспортный формат.
specific = dict(metadata, execution="FLASH", image_format=kind, boot_contract="f28335-universal-v1", boot_protected=True, flash_app_start=APP_START, flash_app_stop=APP_STOP) # Паспорт явно отличает защищённое приложение от старого самостоятельного Flash; аппаратная EEPROM сохраняется отдельно.
metadata_path.write_text(json.dumps(specific, ensure_ascii=False), encoding="utf-8") # Сохраняет все поля происхождения и ручную версию из общего prebuild.
identity.seal(path, metadata_path) # Переподписывает конкретный COFF либо SCI8 с явным execution=FLASH только после проверки байтов.
verify_package(path, specific) # Подтверждает согласованность готового вариантного manifest и SHA перед публикацией.
def create_can_release(stage, image, metadata, map_path, identity): # Добавляет отдельный CAN BIN только после проверки текущего RAM-образа и его интеграции с загрузчиком.
if not metadata.get("can_boot_profile"): return None # Старые проекты и AT25256B продолжают выпускаться без дополнительного неподдерживаемого файла.
if metadata["can_boot_profile"] != "f28335-m95m02-ab" or metadata.get("target") != "TMS320F28335" or metadata.get("eeprom") != "M95M02": # Профиль связан с конкретными процессором, памятью и A/B-портом.
raise ValueError("CAN profile requires TMS320F28335 with M95M02") # Запрещает переименование произвольного RAM либо Flash BIN в пригодный CAN-образ.
limits = dict(metadata, max_image_bytes=130560, ram_ranges=[[0x8000, 0xE000]], max_sections=64) # Ограничения взяты из f28335_boot_store: длина в октетах, адреса в 16-битных словах CPU.
_entry, loaded = verify_embedded(image, limits) # Проверяет SCI8, паспорт, entry, все секции, перекрытия и защиту RAM загрузчика.
symbols = map_path.read_text(encoding="utf-8", errors="replace") # Использует MAP именно активной линковки, уже проверенный по профилю EEPROM.
for name in ("_F28335CanBoot_Init", "_F28335CanBoot_Ready", "_F28335CanBoot_Service", "_F28335CanBoot_RxISR"): # CAN-приложение должно содержать инициализацию, готовность, основной сервис и обработчик приёма.
matches = re.findall(r"^\s*\d+\s+([0-9a-fA-F]{8})\s+" + re.escape(name) + r"\s*$", symbols, re.M) # Читает адреса экспортированных TI-символов, исключая простые упоминания имени в описании секции.
if not matches or any(int(address, 16) not in loaded for address in matches): # Требует присутствие hook в загружаемом payload текущего BIN, а не только в произвольном MAP.
raise ValueError("CAN application hook missing from SCI8/MAP: " + name) # Старое приложение без READY/CONFIRM нельзя выпускать как автоматически пригодное для CAN A/B.
specific = dict(metadata, execution="RAM", image_format="SCI8-RAM", transport="can", protocol="ProtoCAN Boot", product_type=0x2833, hardware_min=1, hardware_max=1) # Паспорт отделяет CAN от RS Flash и сохраняет прежние ручную версию, SHA и аппаратную идентичность.
can_image = image.with_name(image.stem + "_can.bin") # Отдельный суффикс однозначно показывает назначение файла, сохраняя имя артефакта и Build ID.
can_image.write_bytes(image.read_bytes()) # По CAN передаётся та же полная SCI8-таблица RAM без нового заголовка или изменения адресов секций.
with tempfile.TemporaryDirectory(prefix=".can-metadata-", dir=stage) as directory: # Вариантный паспорт не подменяет исходные метаданные компиляции.
metadata_path = Path(directory) / "metadata.json" # Общий seal читает отдельный временный JSON для конкретного транспортного файла.
metadata_path.write_text(json.dumps(specific, ensure_ascii=False), encoding="utf-8") # Русские поля пользовательского паспорта сохраняются в UTF-8.
identity.seal(can_image, metadata_path) # Создаёт собственные manifest и SHA-256 с точным именем CAN BIN.
verify_package(can_image, specific) # До публикации сверяет все поля нового паспорта и точные байты файла.
archive = stage / identity.build_folder_name(metadata) # Дополняет тот же архив текущей сборки, который уже содержит обычный RAM и варианты Flash.
if not archive.is_dir(): raise ValueError("CAN release requires the current build archive") # Не создаёт отдельный неполный выпуск, который не попадёт в штатный экспорт GUI.
for file in (can_image, can_image.with_suffix(".bin.manifest.json"), can_image.with_suffix(".bin.sha256")): # Все три файла должны одновременно попасть в плоский выпуск и архив.
shutil.copy2(file, archive / file.name) # Копирование ещё выполняется внутри временного staged-комплекта без изменения предыдущих выпусков.
verify_package(archive / can_image.name, specific) # Проверяет архивную копию до копирования в output и каталог GUI.
return can_image # Вызывающий выводит точное имя только после успешного завершения всего защищённого postbuild.
def arrange_release(stage, archive_name, artifact, image_name, can_image): # Разделяет уже проверенный комплект по способу программирования в текущем выпуске и его архиве.
base = Path(artifact.replace("\\", "/")).name # Использует базовое имя CCS, включая случай абсолютного BuildArtifactFileBaseName.
routes = {base + "_flash.out": "flash", base + "_flash.map": "flash", base + "_flash_rs.bin": "rs"} # OUT и карта относятся к JTAG, а отдельный SCI8 Flash BIN передаётся последовательным загрузчиком.
if can_image is not None: routes[can_image.name] = "can" # CAN получает только вариант, прошедший ограничения A/B-слота и проверку прикладных hooks.
shutil.copy2(stage / archive_name / "firmware_identity.json", stage / "firmware_identity.json") # Общий паспорт связывает четыре папки как в текущем локальном выпуске CCS, так и в его неизменяемом архиве.
for folder in (stage / archive_name, stage): # Сначала перестраивает архив, затем плоские результаты того же временного выпуска.
for file in tuple(folder.iterdir()): # Снимок списка исключает повторную обработку создаваемых папок назначения.
if not file.is_file() or file.name == "firmware_identity.json": continue # Архив и исходный общий паспорт остаются на своём уровне без дублирования между назначениями.
purpose = next((route for name, route in routes.items() if file.name == name or file.name.startswith(name + ".")), "eeprom") # Sidecar-паспорта, SHA и карты следуют за точным именем своего образа; прочие RAM/HEX-файлы сопровождают EEPROM.
destination = folder / purpose # Одинаковые четыре назначения применяются к локальному CCS-выпуску и экспортируемому архиву.
destination.mkdir(exist_ok=True) # Пустая папка неподдержанного CAN/Flash не создаётся и не обещает отсутствующего образа.
file.replace(destination / file.name) # Перемещает только внутри временного stage, не затрагивая ранее опубликованные файлы.
return (Path("eeprom") / image_name).as_posix() # Переносимый относительный путь сохраняется в receipt и используется штатным экспортом identity.
def publish(project, templates, tools_templates, compiler, build_dir, artifact, map_path, output, export=None, eeprom=None): # Выпускает только проверенные результаты конкретной конфигурации CCS.
project, templates, tools_templates = project.resolve(), templates.resolve(), tools_templates.resolve() # Фиксирует выбранные исходники и инструменты независимо от рабочего каталога терминала.
build_dir, compiler = build_dir.resolve(), compiler.resolve() # Общая BAT-точка получает полные пути текущих компилятора и Debug/Release.
metadata_path = build_dir / "generated/firmware_identity.json" # Использует паспорт, подготовленный до компиляции этой конфигурации.
metadata = json.loads(metadata_path.read_text(encoding="utf-8")) # Отсутствующий prebuild останавливает работу до создания выпуска.
profile = read_profile(project, templates, eeprom) # Повторно читает текущий выбор EEPROM и общий резерв параметров.
settings = json.loads((project / "firmware_identity.json").read_text(encoding="utf-8-sig")) if (project / "firmware_identity.json").is_file() else metadata # Сверяет product и target с действующим проектом.
expected = {"product": settings["product"], "target": settings["target"], "version": configured_version(project, settings), **({"templates_root": str(templates)} if "eeprom_bytes" in profile else {}), **profile} # Ручной номер и аппаратный вариант обязаны совпадать с prebuild.
if any(metadata.get(key) != value for key, value in expected.items()) or metadata.get("can_boot_profile") != profile.get("can_boot_profile"): # Включение и отключение CAN после prebuild также требуют пересборки, как изменение версии, EEPROM или зависимости.
raise ValueError("Configuration changed: run prebuild and rebuild before publishing") # Требует пересборку вместо подмены старых метаданных.
verify_linker_profile(map_path.resolve(), profile) if "eeprom_bytes" in profile else None # Проверяет аппаратный выбор линкера, который нельзя вывести только из Python-конфигурации.
with tempfile.TemporaryDirectory(prefix=".verified-release-", dir=build_dir) as directory: # Единственное место, куда общий postcompile пишет до подтверждения пригодности BIN.
stage = Path(directory) # Изолирует все архивы и промежуточные HEX от постоянных каталогов выпуска.
convert(compiler, build_dir, artifact, metadata_path, stage, tools_templates) # Любая ошибка TI или общей библиотеки прерывает публикацию.
image_name = Path(artifact.replace("\\", "/")).name + "_" + metadata["build_id"] + ".bin" # Повторяет штатное имя конкретного образа, не сканируя старые BIN.
image = stage / image_name # Проверяет только результат текущего вызова конвертора.
verify_embedded(image, metadata) # Обязательные проверки EEPROM, полной SCI8-таблицы и реально встроенных 12 слов.
verify_package(image, metadata) # Сверяет SHA и весь паспорт, включая физический профиль EEPROM.
for archived in stage.rglob(image_name): # Проверяет также копии внутри текущего архива перед их публикацией или export.
verify_package(archived, metadata) # Архив обязан содержать те же байты и профиль, что плоский выпуск.
spec = importlib.util.spec_from_file_location("release_identity_tools", tools_templates / "c/firmware-info/tools/identity.py") # Подключает существующий формат именования и экспорта архива без копирования реализации.
identity = importlib.util.module_from_spec(spec) # Создаёт отдельный модуль инструментов, не подменяя компилируемую зависимость паспорта.
spec.loader.exec_module(identity) # Ошибка подключения остаётся внутри временного этапа до постоянных файлов.
verify_flash_release(stage, artifact, metadata, compiler, identity, required=(project / "build_flash.py").is_file()) # Отдельная Flash-сборка требует собственного безопасного COFF/SCI8 и не ослабляет прежние RAM-проверки.
can_image = create_can_release(stage, image, metadata, map_path, identity) # Автоматически создаёт и проверяет CAN-вариант до переноса любого результата в постоянный выпуск.
archive_name = identity.build_folder_name(metadata) # Сохраняет принятые имена архивов с ревизией, признаком изменений и временем UTC+3.
if not (stage / archive_name / image_name).is_file(): # Полный архив является обязательным результатом общей конверсии, а не необязательной старой папкой.
raise ValueError("Common postcompile did not produce the current build archive") # Не публикует неполный комплект без OUT, MAP и паспорта текущего выпуска.
image_name = arrange_release(stage, archive_name, artifact, image_name, can_image) # Общая раскладка выполняется после всех проверок RAM/Flash/CAN и до постоянного копирования.
output = output.resolve() # Постоянный каталог появляется только после всех проверок staged-комплекта.
output.mkdir(parents=True, exist_ok=True) # Прежние проверенные выпуски сохраняются при добавлении нового.
for item in sorted(stage.iterdir(), key=lambda item: item.is_dir()): # Переносит только файлы и архив, созданные данным postcompile.
if item.is_dir(): # Архив содержит сопроводительные результаты той же сборки.
shutil.copytree(item, output / item.name, dirs_exist_ok=True, copy_function=atomic_copy) # Сохраняет только проверенный архив с атомарной заменой файлов при повторном выпуске той же компиляции.
else: # Общие служебные файлы, если они появятся, сохраняются в корне текущего выпуска.
atomic_copy(item, output / item.name) # Не открывает старые файлы другой ветки на перезапись и не оставляет частичный BIN/HEX при отказе записи.
verify_package(output / image_name, metadata) # Подтверждает отсутствие повреждения при копировании на постоянный диск.
if export is not None: # Копирование в каталог GUI выполняется последним и лишь для прошедшего проверки архива.
identity.export_build(output / archive_name, export.resolve(), image_name) # Использует общий экспорт уже проверенного локального выпуска.
print(f"[firmware-build] Verified {profile.get('eeprom', settings['target'])} BIN: {output / image_name}") # Показывает пригодный аппаратный профиль и точный путь конечного файла.
if can_image is not None: print(f"[firmware-build] CAN BIN: {output / 'can' / can_image.name}") # После успешного выпуска показывает файл из отдельной папки ProtoCAN.
write_receipt(output, archive_name, image_name) # Фиксирует точную папку выпуска для импорта Публикатором без поиска по времени.
return output / image_name # Даёт тестам и внешним средствам однозначный результат без поиска по времени.
def main(argv=None): # Общая точка входа CCS и старых BAT: подготовка и проверка не выполняют сетевую публикацию.
parser = argparse.ArgumentParser(description="Единая подготовка C2000 для CCS и Публикатора SETGUI") # Показывает назначение обеих команд пользователю терминала.
parser.add_argument("command", choices=("prepare", "build")) # Разделяет резервирование версии до компиляции и проверку готового OUT после неё.
for name in ("project", "templates", "tools-templates", "config", "compiler", "build-dir", "metadata", "map", "output", "export"): # Все пути передаются явно или выводятся из публичных соглашений CCS.
parser.add_argument("--" + name, type=Path) # Path исключает интерпретацию аргументов через командную оболочку.
parser.add_argument("--artifact") # Базовое имя OUT передаётся без расширения как штатный макрос CCS.
parser.add_argument("--eeprom", choices=("AT25256B", "M95M02")) # Сохраняет существующий явный выбор тестового аппаратного профиля.
args = parser.parse_args(argv) # Ошибки аргументов завершают процесс до любых изменений версии или файлов выпуска.
tools = Path(__file__).resolve().parents[3] # В полной библиотеке модуль расположен в c/firmware-info/tools.
project = (args.project or (args.build_dir.parent if args.build_dir else Path.cwd())).resolve() # Старый postcompile определяет проект по родителю Debug/Release.
config = args.config or project / "firmware_identity.json" # Старый JSON 166 остаётся совместимым без обязательной миграции.
if args.command == "build" and not config.is_file() and args.build_dir: # Прямой postcompile допускает подготовленный паспорт без отдельного пользовательского JSON.
config = args.metadata or args.build_dir / "generated/firmware_identity.json" # В таком случае доступные проверки определяются реальными метаданными компиляции.
settings = json.loads(config.read_text(encoding="utf-8-sig")) # Изделие и зависимости определяются настройками проекта, а не текущим каталогом.
templates = args.templates or (config.parent / settings["templates"] if settings.get("templates") else tools) # Поддерживает относительный путь общей библиотеки старых проектов.
if args.command == "build" and not args.templates and args.build_dir: # Старый BAT postcompile не передаёт отдельный путь компилируемых исходников templates.
previous = json.loads((args.metadata or args.build_dir / "generated/firmware_identity.json").read_text(encoding="utf-8")) # Использует точный путь, зафиксированный prebuild активной конфигурации.
templates = Path(previous.get("templates_root", templates)) # Проверки EEPROM читают те же заголовки даже при отдельном полном checkout инструментов.
try: # Возвращает CCS ненулевой код при отказе любой проверки.
if args.command == "prepare": # Prebuild вызывается ровно один раз перед компиляцией паспорта.
prepare(project, templates, args.output or args.build_dir / "generated", args.eeprom, config) # При ручной версии исходный JSON не меняется.
else: # Postbuild получает уже подготовленный паспорт текущей конфигурации.
if not args.compiler or not args.build_dir or not args.artifact: # Нельзя подменить отсутствующий артефакт произвольным старым файлом из каталога.
parser.error("build requires --compiler, --build-dir and --artifact") # Показывает обязательные параметры до запуска TI.
metadata = args.build_dir / "generated/firmware_identity.json" # Основное расположение совпадает с прежними конфигурациями CCS.
if args.metadata and args.metadata.resolve() != metadata.resolve(): # Защищённый выпуск использует только паспорт активной конфигурации.
raise ValueError("metadata must belong to the selected build directory") # Не принимает паспорт другой сборки с совпадающим номером версии.
image = Path(args.artifact) # Путь карты по умолчанию находится рядом с выбранным OUT.
image = image if image.is_absolute() else args.build_dir / image # Относительные артефакты разрешаются от BuildDirectory.
publish(project, templates, args.tools_templates or tools, args.compiler, args.build_dir, args.artifact, args.map or image.with_suffix(".map"), args.output or args.build_dir / "firmware", args.export, args.eeprom) # Подготовленный комплект передаётся Публикатору отдельно от сетевых действий.
except (OSError, ValueError, KeyError, TypeError, struct.error, subprocess.CalledProcessError) as error: # Ошибки формата, зависимостей и компилятора не скрываются успешным завершением сборки.
parser.exit(1, f"[firmware-build] ERROR: {error}\n") # Краткая причина остаётся видна в Console CCS.
return 0 # Успех подтверждает подготовку проверенного локального комплекта.
if __name__ == "__main__": # Импорт из проектного адаптера или тестов не запускает сборку.
raise SystemExit(main()) # Возвращает командной оболочке фактический результат подготовки.

View File

@@ -0,0 +1,119 @@
"""Convert a CCS C2000 OUT to SCI8 HEX/BIN and seal its pre-build identity.
Uses only the standard library, TI hex2000 and the shared identity module.
No network, SETGUI, or project-specific paths are required.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import re
import subprocess
import sys # Запускает проектный сборщик тем же Python, которым выполняется общий postbuild.
import tempfile
from identity import archive_build, seal, verify
def build_flash_variant(compiler: Path, build_dir: Path, source: Path, stage: Path) -> tuple: # Возвращает файлы дополнительного Flash-образа только для проектов с явным сборщиком.
builder = build_dir.resolve().parent / "build_flash.py" # Соглашение CCS: Debug/Release расположены рядом с проектным сценарием сборки Flash.
if not builder.is_file(): # Отсутствие сценария оставляет прежний выпуск EEPROM без дополнительных требований к проекту.
return () # Пустой список не добавляет Flash-файлы в архив обычного проекта.
image = stage / (source.stem + "_flash.out") # Отдельное имя и временный каталог исключают подмену RAM-образа и использование устаревшего Flash-файла.
subprocess.run([sys.executable, str(builder), "--postbuild", "--compiler", str(compiler.resolve()), "--build-dir", str(build_dir.resolve()), "--output", str(image)], cwd=build_dir, check=True) # Режим postbuild использует уже собранные объекты и запрещает повторный запуск Make.
files = (image, image.with_suffix(".map")) # Карта линковки сопровождает COFF и позволяет проверить адрес аппаратной точки входа.
for file in files: # Оба результата должны быть выпущены текущим вызовом сборщика.
if not file.is_file() or not file.stat().st_size: # Успешный код процесса без непустого результата не считается успешной сборкой.
raise ValueError(f"Flash builder produced no file: {file}") # Ошибка останавливает postbuild до архивирования или экспорта неполного выпуска.
return files # Передаёт проверенные файлы вызывающему шагу для подписи и сохранения.
def postbuild(compiler: Path, build_dir: Path, artifact: str,
metadata: Path, output: Path, export: Path = None) -> Path:
# CCS may include the configured output directory in this macro.
# Resolve the input separately; output filenames must use only its basename.
artifact_path = Path(artifact)
if not artifact or artifact_path.name in ("", ".", ".."):
raise ValueError("artifact must name an OUT file without its extension")
artifact = artifact_path.name
if any(c in artifact for c in '\\/:*?"<>|'):
raise ValueError("invalid artifact file base name")
source_base = (artifact_path if artifact_path.is_absolute()
else build_dir / artifact_path)
source = Path(str(source_base) + ".out").resolve()
tool = compiler.resolve() / "bin" / "hex2000.exe"
if not tool.is_file():
raise FileNotFoundError(f"C2000 Hex Utility not found: {tool}")
if not source.is_file():
raise FileNotFoundError(f"compiled OUT not found: {source}")
metadata = metadata.resolve()
settings = json.loads(metadata.read_text(encoding="utf-8"))
build_id = settings["build_id"]
if not isinstance(build_id, str) or not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", build_id):
raise ValueError("invalid pre-build identity; run precompile before building")
if build_id == "NOGIT000":
print("[firmware-build] Git identity unavailable: using NOGIT000")
output = output.resolve()
output.mkdir(parents=True, exist_ok=True)
# Keep failed conversions and incomplete manifests out of the final folder.
with tempfile.TemporaryDirectory(prefix=".firmware-", dir=output) as temporary:
stage = Path(temporary)
hex_image = stage / (artifact + ".hex")
bin_image = stage / (artifact + "_" + build_id + ".bin")
flash_files = build_flash_variant(compiler, build_dir, source, stage) # Сначала собирает дополнительный Flash-вариант; его ошибка не допускает выпуск нового BIN.
for image, mode in ((hex_image, "-i"), (bin_image, "-b")):
subprocess.run([str(tool), str(source), "-boot", "-sci8",
"-map", str(image.with_suffix(image.suffix + ".map")),
"-o", str(image), mode], check=True)
if not image.is_file() or not image.stat().st_size:
raise ValueError(f"hex2000 produced no image: {image}")
seal(bin_image, metadata)
verify(bin_image, bin_image.with_suffix(bin_image.suffix + ".manifest.json"))
if flash_files: # Проекты со сборщиком получают независимую проверку точных байтов Flash-образа.
seal(flash_files[0], metadata) # Использует тот же паспорт prebuild для второго варианта этой сборки.
verify(flash_files[0], flash_files[0].with_suffix(".out.manifest.json")) # Проверяет размер и SHA-256 перед сохранением результатов.
serial_image = stage / (artifact + "_flash_rs.bin") # Отдельный SCI8 BIN содержит Flash-адреса и предназначен только для новой команды записи по RS.
serial_map = serial_image.with_suffix('.bin.map') # Карта конверсии позволяет отличить Flash-адреса от EEPROM/RAM BIN.
subprocess.run([str(tool), str(flash_files[0]), '-boot', '-sci8', '-map', str(serial_map), '-o', str(serial_image), '-b'], check=True) # Конвертирует именно Flash COFF с загрузочными адресами секций.
if not serial_image.is_file() or not serial_image.stat().st_size: # Пустая конверсия не должна попасть в выпуск либо каталог GUI.
raise ValueError(f'hex2000 produced no Flash RS image: {serial_image}') # Передаёт ошибку конверсии общему postcompile.
seal(serial_image, metadata) # Привязывает RS BIN к той же версии, ревизии и времени текущего prebuild.
verify(serial_image, serial_image.with_suffix('.bin.manifest.json')) # Проверяет точные байты перед копированием в Bin и архив.
flash_files += (serial_image, serial_map, serial_image.with_suffix('.bin.manifest.json'), serial_image.with_suffix('.bin.sha256')) # Включает транспортный BIN, карту и контрольные файлы в общий выпуск Flash.
for file in flash_files: # Сохраняет COFF и карту также рядом с обычным OUT для выбора в CCS.
destination = source.parent / file.name # Каталог исходного OUT задаёт место назначения без путей конкретного изделия в templates.
destination.write_bytes(file.read_bytes()) # Копирует только результаты успешного текущего вызова, сохраняя исходный RAM-образ.
for item in stage.iterdir():
item.replace(output / item.name)
result = output / bin_image.name # BIN остаётся основным образом EEPROM и возвращаемым результатом общего postbuild.
flash_archive = tuple(output / file.name for file in flash_files) # После переноса из временного каталога берёт актуальные Flash-файлы из firmware.
if flash_files: # Добавляет сопроводительные файлы только при выполненной сборке Flash.
flash_archive += (output / (flash_files[0].name + ".manifest.json"), output / (flash_files[0].name + ".sha256")) # Архив и GUI получают отдельный паспорт и контрольную сумму Flash COFF.
print(f"[firmware-build] Flash OUT: {source.parent / flash_files[0].name}") # Показывает пользователю CCS конкретный файл для прошивки внутренней Flash через JTAG.
archive_build(result, metadata, output, ( # Сохраняет файлы текущей сборки в архиве и передаёт весь архив в каталог GUI.
source, # Копирует исходный COFF-файл OUT с символами отладки под его именем для загрузки через JTAG из GUI.
output / hex_image.name,
output / (hex_image.name + ".map"),
output / (bin_image.name + ".map"), *flash_archive), export=export) # Экспортирует EEPROM и Flash в одной папке текущего выпуска.
print(f"[firmware-build] BIN: {result}")
print(f"[firmware-build] Manifest: {result}.manifest.json")
print(f"[firmware-build] SHA-256: {result}.sha256")
return result
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
for name in ("compiler", "build-dir", "metadata", "output"):
parser.add_argument("--" + name, type=Path, required=True)
parser.add_argument("--artifact", required=True)
parser.add_argument("--export", type=Path)
try:
postbuild(**vars(parser.parse_args()))
except (OSError, ValueError, KeyError, TypeError, subprocess.CalledProcessError) as error:
parser.exit(1, f"[firmware-build] ERROR: {error}\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -7,10 +7,13 @@ protocol encoding lives here: the generated header contains scalar constants.
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
import hashlib
import json
import os
import re
import shutil
import tempfile
from pathlib import Path
import subprocess
@@ -36,19 +39,40 @@ def git_state(repository: Path) -> dict:
def write_atomic(path: Path, text: str):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_text(text, encoding="utf-8", newline="\n")
# Built-in open supports newline on Python versions before 3.10.
with open(temporary, "w", encoding="utf-8", newline="\n") as stream:
stream.write(text)
temporary.replace(path)
def next_version(version): # Вычисляет следующий номер по ограничениям общего контракта firmware-info v1.
result = list(version) # Сохраняет исходный номер до успешной проверки полного переполнения.
for index, limit in ((2, 999), (1, 255), (0, 255)): # Patch имеет диапазон 0..999, minor и major занимают по одному октету 0..255.
if result[index] < limit: # Находит младший разряд, который ещё можно увеличить.
result[index] += 1 # Резервирует следующий номер попытки сборки, включая будущие неудачные компиляции.
return result # Старшие разряды сохраняются, переполненные младшие уже обнулены.
result[index] = 0 # Переносит единицу в следующий разряд после достижения предела текущего.
raise ValueError("Firmware version exhausted at 255.255.999") # Не допускает повторного использования номера после полного переполнения.
def prepare(repository: Path, templates: Path, config: Path, output: Path) -> dict:
settings = json.loads(config.read_text(encoding="utf-8"))
settings = json.loads(config.read_text(encoding="utf-8-sig"))
version = settings["version"]
if (len(version) != 3 or any(type(v) is not int for v in version)
or not all(0 <= v <= limit for v, limit in zip(version, (255, 255, 999)))):
raise ValueError("version must be [major 0..255, minor 0..255, patch 0..999]")
if not isinstance(version, list) or len(version) != 3 or any(type(value) is not int or not 0 <= value <= limit for value, limit in zip(version, (255, 255, 999))): # Проверяет JSON-массив трёх целых разрядов до любого изменения сохраняемого счётчика.
raise ValueError("version must be [major 0..255, minor 0..255, patch 0..999]") # Ошибочные типы, логические значения и выход за пределы контракта останавливают pre-build.
if type(settings.get("increment_version", False)) is not bool: # Отсутствующий флаг сохраняет ручной режим старых проектов; строки true/false не считаются настройкой.
raise ValueError("increment_version must be true or false") # Требует явное логическое значение JSON вместо неявного преобразования Python.
for field in ("product", "target"):
if not isinstance(settings.get(field), str) or not settings[field].strip():
raise ValueError(f"missing {field}")
if settings.get("increment_version", False): # Прямые вызовы templates из CCS, Keil и CLI используют один и тот же сохраняемый итератор.
version = settings["version"] = next_version(version) # Вычисляет номер для JSON, заголовка и метаданных только один раз.
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=config.parent, prefix=".firmware-version-", delete=False) as stream: # Временный файл на том же диске позволяет атомарно заменить настройки без частичной записи.
temporary = Path(stream.name) # Запоминает только собственный файл, чтобы не затронуть пользовательские данные при очистке.
try: stream.write(json.dumps(settings, ensure_ascii=False, indent=2) + "\n") # Сохраняет новый номер и все дополнительные поля проекта в UTF-8.
except BaseException: stream.close(); temporary.unlink(); raise # Ошибка записи оставляет исходный JSON целым и удаляет незавершённый файл.
try: temporary.replace(config) # Занимает номер до дальнейших шагов, поэтому отказ генерации или компиляции не повторит его.
finally: temporary.unlink(missing_ok=True) # Удаляет временный файл при ошибке замены; после успеха он уже переименован.
source, dependency = git_state(repository), git_state(templates)
# A clean application built with a modified templates checkout is dirty too.
known = source["commit"] is not None and dependency["commit"] is not None
@@ -102,6 +126,73 @@ def verify(image: Path, manifest: Path) -> dict:
return metadata
def build_folder_name(metadata: dict) -> str:
"""Use the firmware commit and pre-build time rendered in UTC+3."""
source = metadata.get("source") or {}
dependency = metadata.get("templates") or {}
commit = source.get("commit")
if commit is not None:
if not isinstance(commit, str) or not re.fullmatch(r"[0-9a-fA-F]{40,64}", commit):
raise ValueError("invalid firmware commit in pre-build metadata")
identity = commit[:8].lower()
if source.get("dirty") or dependency.get("dirty"):
identity += "+"
else:
identity = metadata.get("build_id", "NOGIT000")
if not isinstance(identity, str) or not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", identity):
raise ValueError("invalid build ID in pre-build metadata")
stamp_text = metadata.get("built_at_utc")
if not isinstance(stamp_text, str):
raise ValueError("missing pre-build UTC timestamp")
stamp = datetime.fromisoformat(stamp_text.replace("Z", "+00:00"))
if stamp.tzinfo is None:
raise ValueError("pre-build timestamp must include a timezone")
stamp = stamp.astimezone(timezone(timedelta(hours=3)))
return identity + "_" + stamp.strftime("%Y-%m-%d_%H-%M-%S_UTC+3")
def export_build(folder: Path, export: Path, image_name: str) -> Path:
"""Copy the current complete build folder, without scanning old archives."""
folder, export = folder.resolve(), export.resolve()
if export == folder or folder in export.parents:
raise ValueError("export directory must be outside the current build folder")
destination = export / folder.name
verify(folder / image_name, folder / (image_name + ".manifest.json"))
export.mkdir(parents=True, exist_ok=True)
if destination != folder:
shutil.copytree(folder, destination, dirs_exist_ok=True)
verify(destination / image_name, destination / (image_name + ".manifest.json"))
print(f"[firmware-build] Exported folder: {destination}")
return destination
def archive_build(image: Path, metadata_path: Path, output: Path = None,
extra_files=(), export: Path = None) -> dict:
"""Keep a byte-identical build snapshot, alongside the existing flat output."""
image, metadata_path = image.resolve(), metadata_path.resolve()
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
name = build_folder_name(metadata)
output = (output if output is not None else image.parent / "firmware").resolve()
output.mkdir(parents=True, exist_ok=True)
destination = output / name
# Seal and verify staged copies before publishing files in the build folder.
with tempfile.TemporaryDirectory(prefix=".archive-", dir=output) as temporary:
stage = Path(temporary)
shutil.copy2(image, stage / image.name)
for file in extra_files:
file = Path(file)
shutil.copy2(file, stage / file.name)
shutil.copy2(metadata_path, stage / "firmware_identity.json")
seal(stage / image.name, stage / "firmware_identity.json")
verify(stage / image.name, stage / (image.name + ".manifest.json"))
destination.mkdir(parents=True, exist_ok=True)
for file in stage.iterdir():
file.replace(destination / file.name)
print(f"[firmware-build] Folder: {destination}")
result = {"directory": str(destination), "file": image.name}
if export is not None:
result["exported_directory"] = str(export_build(destination, export, image.name))
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
@@ -114,9 +205,14 @@ def main():
p = sub.add_parser("verify")
p.add_argument("--image", type=Path, required=True)
p.add_argument("--manifest", type=Path, required=True)
p = sub.add_parser("archive")
p.add_argument("--image", type=Path, required=True)
p.add_argument("--metadata", dest="metadata_path", type=Path, required=True)
p.add_argument("--output", type=Path)
p.add_argument("--export", type=Path)
args = vars(parser.parse_args())
command = args.pop("command")
result = {"prepare": prepare, "seal": seal, "verify": verify}[command](**args)
result = {"prepare": prepare, "seal": seal, "verify": verify, "archive": archive_build}[command](**args)
print(json.dumps(result, ensure_ascii=False))

View File

@@ -1,5 +1,7 @@
# GAS Logger — экспериментальная версия 1
> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми.
Непрерывный регистратор с двумя кольцевыми банками, неизменяемым снимком и
общей картой GAS / ProtoCAN GAS / Modbus FC03/FC06. Весь код и проектные/MCU
адаптеры находятся в `templates`; приложения подключают ревизию сабмодулем.

View File

@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.13) # Задаёт минимальную версию, совместимую с соседними C-библиотеками templates.
project(global_time C) # Создаёт независимую цель для программных глобальных часов.
add_library(global_time STATIC src/global_time.c) # Собирает переносимое ядро без обязательных MCU заголовков.
target_include_directories(global_time PUBLIC include) # Экспортирует потребителю только публичный контракт часов.
target_compile_features(global_time PUBLIC c_std_99) # Требует C99 для точных целочисленных типов и uint64_t меток.
option(GLOBAL_TIME_BUILD_F28335 "Собирать аппаратный порт TI F28335" OFF) # Позволяет MCU проекту явно включить адаптер со своими TI include-путями.
if(GLOBAL_TIME_BUILD_F28335) # Не навязывает host-приложению аппаратные регистры TI.
target_sources(global_time PRIVATE ports/tms320f28335/global_time_f28335.c) # Добавляет eCAN и необязательный CPU Timer2 порт к той же библиотеке.
target_include_directories(global_time PUBLIC ports/tms320f28335) # Делает аппаратные hooks доступными прошивке потребителя.
endif() # Завершает условное подключение аппаратного адаптера.
option(GLOBAL_TIME_BUILD_TESTS "Собирать host-проверки глобального времени" ON) # Разрешает выключить host-исполняемые файлы в MCU сборке.
if(GLOBAL_TIME_BUILD_TESTS AND NOT GLOBAL_TIME_BUILD_F28335) # Не подмешивает host-модель регистров к настоящему TI порту.
enable_testing() # Регистрирует проверяемые сценарии в CTest.
add_executable(test_global_time tests/test_global_time.c) # Собирает календарные, CAN и монотонные сценарии.
target_link_libraries(test_global_time PRIVATE global_time) # Использует то же ядро, которое подключает прошивка.
add_test(NAME global_time COMMAND test_global_time) # Делает host-проверку доступной через ctest.
add_executable(test_global_time_f28335 tests/test_f28335.c ports/tms320f28335/global_time_f28335.c) # Собирает настоящий аппаратный адаптер с минимальной моделью регистров.
target_include_directories(test_global_time_f28335 PRIVATE tests/f28335_stubs ports/tms320f28335) # Ограничивает подмену заголовков только этой host-целью.
target_link_libraries(test_global_time_f28335 PRIVATE global_time) # Связывает аппаратные hooks с реальным ядром часов.
add_test(NAME global_time_f28335 COMMAND test_global_time_f28335) # Регистрирует eCAN/Timer2 проверку отдельно от календарной.
endif() # Завершает host-проверки без влияния на MCU исходники.

167
c/global-time/README.md Normal file
View File

@@ -0,0 +1,167 @@
# Глобальное время
Программные часы с тактом 1 мс, синхронизацией даты и времени через ProtoCAN
и атомарным получением метки для логгера. Ядро C99 не требует аппаратного RTC,
операционной системы или динамической памяти. Порт TMS320F28335 включает
eCAN-A и необязательный CPU Timer2. Все текстовые файлы — UTF-8.
## Обмен при запуске
1. Приложение инициализирует часы до разрешения IRQ и запускает источник 1 мс.
2. После завершения инициализации **всей платы** вызывает `gt_ready()` /
`gt_f28335_ready()`.
3. Из main библиотека отправляет `DISCRETE/FLAGS`, `MsgBody=0x3000`,
`Route=1`, адрес самой платы, `DLC=1`, `DATA=01`.
4. Верхний уровень распознаёт пакет через `gt_parse_ready()`, берёт свою
текущую дату и посылает **один broadcast всем платам**, используя `gt_make_rtc()`.
5. Каждый узел принимает штатный `RTCSETUP`, сравнивает время со своим,
сохраняет величину коррекции и немедленно обновляет календарные часы.
При отсутствии ответа узел повторяет готовность не чаще раза в секунду.
`RTCSETUP` полностью сохраняет действующий формат ProtoCAN.
Универсальное сообщение готовности здесь определено как новый профиль
`DISCRETE/FLAGS`; ранее такого общего пакета в базовом протоколе не было.
Подробный [контракт обмена](../../doc/setcan/protocan/GLOBAL_TIME.md).
Код верхнего уровня для автоматической отправки времени подключается в его
приложении; библиотека предоставляет кодировщик и распознавание события старта.
## Метка логгера
`gt_get()` одним атомарным чтением возвращает:
| Поле | Значение |
|---|---|
| `seconds`, `millisecond` | секунды от 2000-01-01 и доля 0..999 мс |
| `synchronized` | 0 до первого корректного RTCSETUP, 1 после |
| `uptime_ms` | время работы modulo 2^32, независимое от переводов часов |
| `sync_uptime_ms` | момент приёма последнего опорного пакета |
| `generation` | номер принятой коррекции |
| `correction_ms` | последнее смещение, знаковое 64-битное значение в мс |
| `weekday` | фаза недели 0..6, продвигаемая при смене суток |
`gt_timestamp_ms()` формирует 64-битную метку от 2000 года.
`gt_calendar()` преобразует снимок в календарь и возвращает 0, пока дата неизвестна.
Для записей, которые должны различать переводы часов, сохраняйте также
`generation`, `synchronized` и/или `uptime_ms`. Календарное время может идти назад
после коррекции. Тайм-ауты следует считать по разности `uptime_ms`.
Без новых broadcast часы продолжают ход по локальному генератору;
возраст синхронизации равен `(uint32_t)(uptime_ms - sync_uptime_ms)`.
```c
#include "global_time_f28335.h" /* Подключает общую временную базу F28335. */
gt_stamp stamp; /* Содержит согласованную дату, миллисекунды и качество времени. */
gt_get(gt_f28335_clock(), &stamp); /* Допускается из main и ISR, включая ISR логгера. */
uint64_t time_ms = gt_timestamp_ms(&stamp); /* Перед сохранением календарной метки проверяется stamp.synchronized. */
```
Это фрагмент для функции потребителя. Формат хранения существующего логгера
библиотека не меняет. В частности, старое 32-битное поле `time_ms` в GAS logger
и v4 УМП не вмещает календарные миллисекунды: потребителю нужен собственный
формат абсолютной метки либо отдельная временная база и номер поколения.
## Подключение TMS320F28335
Добавьте в сборку `src/global_time.c` и
`ports/tms320f28335/global_time_f28335.c`. Include-пути: `include`,
`ports/tms320f28335`, штатные TI `DSP2833x_headers/include`,
`DSP2833x_common/include` и include компилятора.
Ресурсы порта:
| Ресурс | Назначение |
|---|---|
| eCAN-A MBOX7 | RX точного RTCSETUP, любой приоритет, IRQ line0 |
| eCAN-A MBOX8 | TX готовности, ACK проверяется в main |
| CPU Timer2 / INT14 | необязательный собственный такт 1 мс |
Mailbox 0..6, скорость CAN, GPIO и глобальные маршруты PIE/CANGIM настраивает
приложение. `DBO=0` обязателен; порт отвергает другую раскладку октетов.
MBOX7 использует CANOPC: кадр защищён от аппаратной перезаписи до копирования ISR.
В программной очереди хранится последний broadcast, поскольку более новый
опорный пакет заменяет предыдущий.
При запрещённых IRQ, **после** `InitECan` и `InitPieVectTable`:
```c
int ok = gt_f28335_init_periph(5U); /* PERIPH5=Type1/ID5, то есть УМП1; номер берётся из конфигурации платы. */
if (ok) ok = gt_f28335_timer2_start(150000000UL); /* Частота CPU задаётся проектом в Гц; пример соответствует 150 МГц. */
```
`gt_f28335_init()` позволяет передать Type и ID непосредственно.
`init_periph()` поддерживает физические PERIPH 0..31: Type1/2 и ID0..15;
5 — УМП1, 6 — УМП2, 8 — задатчик. Порт инициализируется до подключения hooks.
Повторный init разрешён только при остановленном собственном Timer2,
отсутствующем TX и запрещённых глобальных IRQ.
Если Timer2 занят RTOS или приложением, **не вызывайте** `timer2_start()`;
в существующем ISR с периодом ровно 1 мс вызывайте `gt_f28335_tick_1ms()`.
Используйте один источник такта. Отдельный Timer2 продолжает отсчёт и при
медленном main, но длительная блокировка IRQ на несколько миллисекунд приводит
к потере тиков: аппаратный флаг прерывания не считает пропущенные периоды.
В существующем `ECAN0INTA` ISR перед очисткой общих CAN-флагов и PIEACK:
```c
gt_f28335_rx_isr(); /* Копирует только MBOX7 и запоминает uptime при приёме, календарь проверяется позднее. */
```
Приложение должно разрешить соответствующий CAN IRQ: `CANGIM.I0EN`,
PIE group9 `INTx5`, CPU `M_INT9`; оставить свой обработчик остальных mailbox
и штатные подтверждения CANGIF/PIEACK. Порт не заменяет общий CAN ISR.
Глобальный `EINT` выполняет приложение после полной настройки.
После прикладной инициализации и в каждом проходе главного цикла соответственно:
```c
gt_f28335_ready(); /* Выполняется один раз после загрузки параметров и запуска прикладных подсистем. */
gt_f28335_service(); /* Выполняется регулярно в main; обработка календаря и CAN не ждёт аппаратного ACK. */
```
Все атомарные чтения и обновления используют TI
`__disable_interrupts()` / `__restore_interrupts()`, сохраняя прежний INTM.
Транспорт ждёт подтверждение CANTA асинхронно и не перезаписывает занятый MBOX8.
При отсутствии других CAN-узлов передача остаётся аппаратному контроллеру
до восстановления шины. CAN ACK не подтверждает обработку пакета мастером;
приход корректного RTCSETUP прекращает повторные объявления готовности.
## Точность и календарь
Разрешение локальных часов — 1 мс. Действующий RTCSETUP передаёт только целые
секунды, поэтому сам формат **не гарантирует синхронизацию плат с точностью 1 мс**.
Мастер должен посылать актуальное значение около секундной границы.
Арбитраж CAN и задержка до RX ISR остаются погрешностью; задержка после RX ISR
до обработки в main компенсируется по монотонному счётчику. Для точности между
платами до 1 мс понадобится согласованное расширение проводного протокола.
Одна шкала времени выбирается верхним уровнем (рекомендуется UTC); кадр не
содержит часовой пояс. Входные годы 2000..2099, дата проверяется по григорианскому
календарю. Формат дня недели 0..6 соответствует проверке существующего
STM32-порта; профиль обозначает 0 как воскресенье. Для совместимости переданная
фаза недели сохраняется без дополнительной проверки соответствия дате.
Некоторые старые клиенты отображают неделю как 1..7; перед отправкой им нужен
перевод воскресенья 7 → 0.
Секундная шкала uint32_t от 2000 года переполнится в феврале 2136 года.
Период между захватом RX uptime и `gt_receive()` должен быть меньше 2^32 мс.
## Проверки
Из корня templates:
```text
python c/global-time/tests/run_tests.py
```
`--cc` задаёт GCC/Clang. Для отдельной TI C28x проверки передайте
`--ti-cgt` (каталог компилятора), `--ti-headers` (DSP2833x_headers/include)
и при необходимости `--ti-common` (DSP2833x_common/include).
Объекты и EXE создаются во временном каталоге. CMake также предоставляет
`global_time`, два CTest и опции `GLOBAL_TIME_BUILD_F28335` /
`GLOBAL_TIME_BUILD_TESTS`; MCU проект задаёт свои TI include-пути.
Проверяются ошибки даты и кадра, високосный февраль, смена года, коррекции
в обе стороны, компенсация очереди через переполнение uptime, ранний broadcast,
повторы готовности, сохранение IRQ, изоляция eCAN mailbox, аппаратный ACK,
значения периода Timer2 и его реальный ISR через назначенный вектор.
TI-проверка компилирует исходники с large/COFF ABI и настоящими заголовками
проекта 28335_periph. Полная линковка конкретной прошивки и проверка на плате
в этот набор не входят.

View File

@@ -0,0 +1,65 @@
#ifndef GLOBAL_TIME_H /* Защищает единый контракт часов от повторного включения. */
#define GLOBAL_TIME_H /* Отмечает подключённый интерфейс программных глобальных часов. */
#include <stdint.h> /* Фиксирует разрядность счётчиков и поддерживает 16-битный char TI C28x. */
#ifdef __cplusplus /* Сохраняет совместимость C-библиотеки с приложениями C++. */
extern "C" { /* Экспортирует функции без изменения имён компилятором C++. */
#endif /* Завершает условное объявление C ABI. */
#define GT_RTC_ID 0x00000030UL /* ProtoCAN: Route=0, Type/ID=0, BROADCAST=0, RTCSETUP=3 в Body[15:4]. */
#define GT_READY_BODY 0x3000U /* Контракт GLOBAL_TIME.md: DISCRETE/FLAGS=3, страница общих флагов=0. */
#define GT_READY_RETRY_MS 1000UL /* Повторяет сообщение готовности раз в секунду до первой синхронизации. */
typedef struct gt_frame { /* Описывает classic CAN независимо от регистров и размера char процессора. */
uint32_t id; /* Хранит только 29 бит идентификатора, без служебных битов драйвера. */
uint16_t extended; /* Единица означает IDE=1; стандартные кадры часы игнорируют. */
uint16_t remote; /* Единица означает RTR; удалённый запрос не содержит время. */
uint16_t dlc; /* Считает октеты на линии, а не sizeof элементов массива C28x. */
uint16_t data[8]; /* Каждый элемент содержит один октет 0..255 даже при CHAR_BIT=16. */
} gt_frame; /* Завершает переносимый контейнер кадра. */
typedef struct gt_datetime { /* Представляет календарь без зависимости от time_t и часового пояса ОС. */
uint16_t year; /* Полный год; входной ProtoCAN поддерживает 2000..2099. */
uint16_t month; /* Номер месяца 1..12, без BCD-кодирования. */
uint16_t day; /* Число месяца с проверкой високосного февраля. */
uint16_t weekday; /* День недели 0..6; профиль использует 0=воскресенье, 1=понедельник. */
uint16_t hour; /* Часы 0..23 в 24-часовом формате. */
uint16_t minute; /* Минуты 0..59; leap seconds протокол не передаёт. */
uint16_t second; /* Секунды 0..59 согласно существующему RTCSETUP. */
uint16_t millisecond; /* Доля секунды 0..999; RTCSETUP задаёт нулевую долю. */
} gt_datetime; /* Завершает публичный календарный тип. */
typedef struct gt_stamp { /* Даёт логгеру согласованную метку и её качество одним чтением. */
uint32_t seconds; /* Секунды от 2000-01-01 00:00:00 в шкале времени мастера. */
uint16_t millisecond; /* Миллисекундная доля текущей секунды. */
uint16_t synchronized; /* Ноль до первого корректного RTCSETUP, единица после него. */
uint32_t uptime_ms; /* Независимый счётчик работы modulo 2^32; коррекции его не меняют. */
uint32_t sync_uptime_ms; /* Момент приёма последнего RTCSETUP для оценки давности синхронизации. */
uint32_t generation; /* Число коррекций; логгер отличает участки до и после перевода часов. */
int64_t correction_ms; /* Последнее смещение новых часов относительно старых, в миллисекундах. */
uint16_t weekday; /* Принятый день недели, продвигаемый при каждом переходе через полночь. */
} gt_stamp; /* Завершает атомарно читаемый снимок. */
typedef struct gt_port { /* Передаёт ядру короткие критические секции и неблокирующую отправку CAN. */
void *user; /* Контекст платформы, передаваемый каждому callback без преобразования. */
uint16_t (*enter)(void *user); /* Запрещает IRQ и возвращает прежнее состояние для вложенных вызовов. */
void (*leave)(void *user, uint16_t state); /* Восстанавливает прежнее состояние IRQ, включая вызовы из ISR. */
int (*send)(void *user, const gt_frame *frame); /* Копирует кадр до возврата; 1=принят, 0=занято/ошибка. */
} gt_port; /* Завершает платформенный контракт; heap и аппаратный RTC не нужны. */
typedef struct gt_clock { /* Хранит состояние одного узла; инициализация обязательна до разрешения ISR. */
gt_port port; /* Содержит проверенные callbacks и контекст платформы. */
volatile gt_stamp stamp; /* Меняется только при удержании блокировки IRQ. */
uint16_t device_type; /* Три адресных бита ProtoCAN, диапазон 0..7. */
uint16_t device_id; /* Четыре адресных бита экземпляра, диапазон 0..15. */
uint16_t ready; /* Устанавливается приложением после завершения всей инициализации платы. */
uint16_t announced; /* Помнит принятую транспортом отправку готовности, даже если часы уже синхронизированы. */
uint32_t sent_uptime_ms; /* Ограничивает частоту повторов готовности монотонной шкалой. */
} gt_clock; /* Состояние допускает статическое выделение памяти на микроконтроллере. */
int gt_init(gt_clock *clock, const gt_port *port, uint16_t device_type, uint16_t device_id); /* Проверяет адрес/callbacks и явно очищает RAM, включая TI COFF. */
void gt_tick_1ms(gt_clock *clock); /* Вызывается ровно один раз за аппаратную миллисекунду и выполняет только короткую арифметику. */
void gt_get(gt_clock *clock, gt_stamp *out); /* Атомарно копирует всю метку; допустим вызов из main, логгера и ISR. */
void gt_ready(gt_clock *clock); /* Идемпотентно разрешает сообщение «плата инициализирована»; CAN отправляется из poll. */
void gt_poll(gt_clock *clock); /* Вызывается единственным потребителем в main; не ждёт освобождения CAN. */
int gt_receive(gt_clock *clock, const gt_frame *frame, uint32_t received_uptime_ms); /* Проверяет RTCSETUP и компенсирует задержку от приёма до обработки, менее 2^32 мс. */
int gt_make_rtc(const gt_datetime *date, gt_frame *out); /* Даёт верхнему уровню кодировщик штатного 7-октетного broadcast. */
int gt_parse_ready(const gt_frame *frame, uint16_t *device_type, uint16_t *device_id); /* Позволяет мастеру распознать готовность и выбрать ответ RTCSETUP. */
int gt_calendar(const gt_stamp *stamp, gt_datetime *out); /* Преобразует согласованный снимок в календарь; до синхронизации возвращает 0. */
uint64_t gt_timestamp_ms(const gt_stamp *stamp); /* Возвращает широкую метку от 2000 года без переполнения через 49,7 суток. */
#ifdef __cplusplus /* Закрывает C ABI только для компилятора C++. */
} /* Завершает блок функций с C-связыванием. */
#endif /* Завершает проверку языка потребителя. */
#endif /* Завершает защиту публичного заголовка. */

View File

@@ -0,0 +1,168 @@
#include "DSP2833x_Device.h" /* Использует штатные описания 32-битных регистров eCAN, Timer2 и векторов TI. */
#include "global_time_f28335.h" /* Подключает переносимые часы и контракт интеграции порта. */
#include <string.h> /* Явно очищает отложенный RX до первого прохода main, включая TI COFF. */
#define GT_RX 0x00000080UL /* Бит 7 регистров eCAN соответствует выделенному приёмному MBOX7. */
#define GT_TX 0x00000100UL /* Бит 8 регистров eCAN соответствует отдельному передающему MBOX8. */
static gt_clock board_clock; /* Один узел F28335 использует одну глобальную временную шкалу. */
static gt_frame pending_frame; /* Последний RX хранится до обработки в main; новый broadcast заменяет устаревший. */
static volatile uint16_t pending_rx; /* Публикует полностью скопированный RX и его монотонную привязку. */
static uint32_t received_at; /* Запоминает миллисекундный uptime в момент обработки CAN ISR. */
static uint16_t tx_busy, timer_owned; /* Явно сбрасывается init: TI COFF не гарантирует нули в .ebss. */
static uint16_t initialized = 0U; /* Запись в .cinit гарантирует безопасный отказ ранних ISR hooks до init даже при TI COFF. */
static uint16_t enter(void *user) /* Реализует вложенную критическую секцию TI C28x. */
{ /* Не разрешает прерывания при выходе из уже выполняющегося ISR. */
(void)user; /* Порт использует один статический экземпляр, поэтому контекст не требуется. */
return __disable_interrupts(); /* Возвращает предыдущее состояние ST1, необходимое для восстановления INTM. */
} /* Завершает платформенный вход в критическую секцию. */
static void leave(void *user, uint16_t state) /* Восстанавливает состояние IRQ до входа в библиотеку. */
{ /* Сохраняет корректность вызовов и из main, и из ISR. */
(void)user; /* Отдельный аппаратный экземпляр выбирается статическими регистрами eCAN-A. */
__restore_interrupts(state); /* Восстанавливает исходный INTM вместо безусловного разрешения IRQ. */
} /* Завершает платформенный выход из критической секции. */
static int send(void *user, const gt_frame *frame) /* Копирует пакет готовности в выделенный аппаратный mailbox без ожидания. */
{ /* Транспорт не перезаписывает кадр, пока контроллер не подтвердит передачу. */
uint16_t state = enter(0); /* Защищает RMW общих регистров CANME от чужих CAN ISR, сохраняя прежний INTM. */
(void)user; /* Контекст платформы не нужен для фиксированного eCAN-A MBOX8. */
if (tx_busy || (ECanaRegs.CANTRS.all & GT_TX)) { /* При занятости сохраняет исходный пакет и предлагает повтор из main. */
leave(0, state); /* Восстанавливает исходные IRQ и на раннем выходе занятого транспорта. */
return 0; /* Не теряет готовность и не ждёт окончания аппаратной передачи. */
} /* Продолжает только при владении свободным MBOX8. */
EALLOW; /* Открывает запись защищённой конфигурации mailbox контроллера. */
ECanaRegs.CANME.all &= ~GT_TX; /* Отключает только MBOX8 перед изменением ID и payload. */
ECanaMboxes.MBOX8.MSGID.all = 0x80000000UL | frame->id; /* IDE в бите 31 eCAN разрешает расширенный ProtoCAN ID. */
ECanaMboxes.MBOX8.MSGCTRL.all = 1UL; /* DLC=1 и RTR=0 соответствуют единственному октету готовности. */
ECanaMboxes.MBOX8.MDL.all = frame->data[0]; /* Первый проводной октет располагается в младших восьми битах MDL при DBO=0. */
ECanaMboxes.MBOX8.MDH.all = 0UL; /* Убирает остатки прежнего payload, хотя DLC передаёт только один октет. */
ECanaRegs.CANTA.all = GT_TX; /* Сбрасывает только старый ACK MBOX8 записью единицы. */
ECanaRegs.CANAA.all = GT_TX; /* Сбрасывает только старое подтверждение отмены MBOX8. */
ECanaRegs.CANME.all |= GT_TX; /* Возвращает MBOX8 в работу, сохраняя чужие mailbox. */
ECanaRegs.CANTRS.all = GT_TX; /* Запрашивает передачу без блокирующего ожидания ACK шины. */
EDIS; /* Закрывает запись защищённых регистров после полной публикации кадра. */
tx_busy = 1U; /* Отмечает владение кадром до CANTA или CANAA, даже при отсутствии других узлов на шине. */
leave(0, state); /* Освобождает короткую аппаратную секцию сразу после публикации запроса передачи. */
return 1; /* Подтверждает приём транспортом, но не отдельное прикладное подтверждение мастера. */
} /* Завершает неблокирующий callback передачи. */
int gt_f28335_init(uint16_t device_type, uint16_t device_id) /* Настраивает часы и только два выделенных mailbox при запрещённых IRQ. */
{ /* Приложение заранее задаёт скорость eCAN, GPIO, DBO=0 и общие векторы прерываний. */
gt_port port; /* Локальная таблица callbacks копируется ядром в постоянное состояние. */
if (device_type > 7U || device_id > 15U || (ECanaRegs.CANTRS.all & GT_TX) || ECanaRegs.CANMC.bit.DBO) return 0; /* Проверяет адрес и порядок октетов; активную TX-передачу не прерывает повторным init. */
if (initialized && timer_owned) return 0; /* Требует остановить захваченный Timer2 перед повторным init, сохраняя его владение при отказе. */
port.user = 0; /* Единственный экземпляр порта не требует внешнего контекста. */
port.enter = enter; /* Подключает защиту снимка штатным intrinsic TI. */
port.leave = leave; /* Подключает восстановление предыдущей маски прерываний. */
port.send = send; /* Подключает отправку в MBOX8 без ожидания свободной шины. */
if (!gt_init(&board_clock, &port, device_type, device_id)) return 0; /* Сбрасывает RAM до открытия источников IRQ. */
pending_rx = tx_busy = timer_owned = 0U; /* Явно очищает программные флаги после холодного и тёплого старта. */
memset(&pending_frame, 0, sizeof(pending_frame)); /* Не читает неопределённую RAM при первом service без полученного пакета. */
received_at = 0UL; /* Начальная привязка не читается до получения первого кадра. */
EALLOW; /* Открывает изменение аппаратной конфигурации eCAN-A. */
ECanaRegs.CANME.all &= ~(GT_RX | GT_TX); /* Временно отключает только выделенные mailbox 7/8. */
ECanaMboxes.MBOX7.MSGID.all = 0xC0000000UL | GT_RTC_ID; /* IDE=1 и AME=1 включают расширенный ID и локальную маску точного RTCSETUP. */
ECanaLAMRegs.LAM7.all = 0x10000000UL; /* LAM игнорирует только бит Priority[28]; LAMI=0 сохраняет проверку IDE. */
ECanaMboxes.MBOX7.MSGCTRL.all = 7UL; /* Задаёт ожидаемые семь октетов; фактический DLC дополнительно проверяется ядром. */
ECanaMboxes.MBOX8.MSGID.all = 0x80000000UL; /* Подготавливает расширенный TX без AME; адрес будет установлен callback отправки. */
ECanaMboxes.MBOX8.MSGCTRL.all = 1UL; /* Пакет готовности занимает один октет CAN payload. */
ECanaRegs.CANMD.all = (ECanaRegs.CANMD.all | GT_RX) & ~GT_TX; /* MBOX7 становится RX, MBOX8 — TX, направления остальных mailbox сохраняются. */
ECanaRegs.CANOPC.all |= GT_RX; /* Запрещает перезапись RX до чтения ISR; при потере используется следующий периодический broadcast. */
ECanaRegs.CANMIL.all &= ~GT_RX; /* Направляет MBOX7 в общую линию ECAN0INTA приложения. */
ECanaRegs.CANMIM.all = (ECanaRegs.CANMIM.all | GT_RX) & ~GT_TX; /* Разрешает IRQ только на RX, а завершение TX проверяет main. */
ECanaRegs.CANRMP.all = GT_RX; /* Удаляет старый pending только MBOX7 записью единицы. */
ECanaRegs.CANRML.all = GT_RX; /* Очищает старую потерю приёма только выделенного mailbox. */
ECanaRegs.CANTA.all = GT_TX; /* Удаляет старый ACK выделенного передатчика, не затрагивая CAN Boot. */
ECanaRegs.CANAA.all = GT_TX; /* Удаляет старый флаг отмены перед настройкой нового обмена. */
ECanaRegs.CANME.all |= GT_RX | GT_TX; /* Включает приём синхронизации и передачу готовности. */
EDIS; /* Закрывает защищённую конфигурацию контроллера. */
initialized = 1U; /* Публикует готовность самого порта после сброса всех RAM- и аппаратных флагов. */
return 1; /* Приложение может запускать такт и разрешать общие CAN-прерывания. */
} /* Завершает настройку без сброса скорости, GPIO и других CAN-сервисов. */
int gt_f28335_init_periph(uint16_t periph) /* Привязывает часы к физическому адресу периферийной платы. */
{ /* Использует тот же реестр, что существующий порт CAN Boot F28335. */
if (periph >= 32U) return 0; /* Адреса PERIPH определены только для 32 устройств, без циклического обрезания. */
return gt_f28335_init((uint16_t)(1U + periph / 16U), periph % 16U); /* Номера 0..15 используют Type=1, 16..31 — Type=2, младшие четыре бита задают ID. */
} /* Завершает преобразование физического адреса. */
gt_clock *gt_f28335_clock(void) /* Предоставляет логгеру общую временную базу платы. */
{ /* Действует после успешной инициализации порта. */
return &board_clock; /* Один и тот же объект обслуживают CAN, таймер и все потребители времени. */
} /* Завершает доступ к экземпляру переносимого ядра. */
void gt_f28335_ready(void) /* Публикует готовность прикладного оборудования после загрузки параметров. */
{ /* Не объявляет успех при отказе конфигурации порта. */
if (initialized) gt_ready(&board_clock); /* Первый пакет будет послан из main даже при уже полученном времени. */
} /* Завершает hook завершения инициализации платы. */
void gt_f28335_tick_1ms(void) /* Позволяет использовать существующий периодический ISR вместо Timer2. */
{ /* Приложение выбирает один источник миллисекунд, исключая двойной отсчёт. */
if (initialized) gt_tick_1ms(&board_clock); /* Продвигает локальные и календарные часы только после явного init. */
} /* Завершает универсальный миллисекундный hook. */
void gt_f28335_rx_isr(void) /* Захватывает CAN-время и момент получения без календарной обработки в ISR. */
{ /* Вызывается существующим обработчиком ECAN0INTA до очистки CANGIF и PIEACK. */
uint32_t low, high, uptime; /* Однократные 32-битные чтения не рвут слова аппаратного mailbox. */
uint16_t i, state; /* Индекс восьми октетов и прежняя маска прерываний. */
if (!initialized || !(ECanaRegs.CANRMP.all & GT_RX)) return; /* Не читает MBOX7 по прерыванию другого CAN-сервиса. */
state = enter(0); /* Исключает вложенный таймер между чтением uptime и публикацией полученного кадра. */
uptime = board_clock.stamp.uptime_ms; /* Фиксирует момент RX в монотонных миллисекундах, независимых от синхронизации. */
pending_frame.id = ECanaMboxes.MBOX7.MSGID.all & 0x1FFFFFFFUL; /* Удаляет служебные IDE/AME биты регистра из 29-битного ID. */
pending_frame.extended = (uint16_t)(ECanaMboxes.MBOX7.MSGID.all >> 31U); /* Ядро дополнительно проверит IDE даже при аппаратной фильтрации. */
pending_frame.remote = ECanaMboxes.MBOX7.MSGCTRL.bit.RTR; /* Сохраняет RTR, чтобы удалённый запрос никогда не устанавливал часы. */
pending_frame.dlc = ECanaMboxes.MBOX7.MSGCTRL.bit.DLC; /* Сохраняет фактическую длину, включая ошибочный или пустой payload. */
low = ECanaMboxes.MBOX7.MDL.all; /* Читает первые четыре октета одним 32-битным доступом eCAN. */
high = ECanaMboxes.MBOX7.MDH.all; /* Читает оставшиеся четыре октета без указателей на байты C28x. */
for (i = 0U; i < 8U; ++i) pending_frame.data[i] = (uint16_t)(((i < 4U ? low : high) >> (8U * (i % 4U))) & 255UL); /* Извлекает каждый проводной октет при DBO=0 в отдельное 16-битное слово. */
received_at = uptime; /* Публикует временную привязку того же аппаратного кадра. */
pending_rx = 1U; /* Отмечает полностью готовый снимок; main читает его под той же блокировкой. */
ECanaRegs.CANRMP.all = GT_RX; /* Освобождает MBOX7 только после полного копирования защищённого CANOPC кадра. */
ECanaRegs.CANRML.all = GT_RX; /* Сбрасывает индикатор пропущенного кадра, сохраняя уже скопированное опорное время. */
leave(0, state); /* Возвращает исходный INTM обработчика CAN. */
} /* Завершает короткий ISR без отправки, ожидания CAN и расчёта календаря. */
void gt_f28335_service(void) /* Обрабатывает часы и готовность из единственного основного цикла. */
{ /* Отложенная календарная работа не увеличивает длительность CAN ISR. */
gt_frame frame; /* Локальная копия допускает замену следующего pending кадра новым CAN ISR. */
uint32_t uptime; /* Сохраняет привязку прочитанного кадра до выхода из критической секции. */
uint16_t state, have_rx; /* Разделяет короткий захват очереди и длительную проверку календаря. */
if (!initialized) return; /* Не обращается к неинициализированным callbacks после отказа init. */
state = enter(0); /* Блокирует ISR только на время копирования одного небольшого кадра. */
have_rx = pending_rx; /* Запоминает наличие кадра перед освобождением слота. */
frame = pending_frame; /* Копирует payload и заголовок согласованно с моментом RX. */
uptime = received_at; /* Забирает момент получения того же кадра. */
pending_rx = 0U; /* Разрешает приём следующего broadcast, пока main рассчитывает дату текущего. */
leave(0, state); /* Возобновляет IRQ до календарных циклов. */
if (have_rx) (void)gt_receive(&board_clock, &frame, uptime); /* Проверяет время и добавляет тики, прошедшие с RX ISR. */
if (tx_busy && ((ECanaRegs.CANTA.all | ECanaRegs.CANAA.all) & GT_TX)) { /* Освобождает TX только после подтверждения передачи либо аппаратной отмены. */
ECanaRegs.CANTA.all = GT_TX; /* Сбрасывает только ACK собственного mailbox. */
ECanaRegs.CANAA.all = GT_TX; /* Сбрасывает только флаг отмены собственного mailbox. */
tx_busy = 0U; /* Следующий gt_poll сможет повторить готовность, если время ещё не получено. */
} /* При отсутствии ACK запрос остаётся аппаратному CAN для автоматической передачи после восстановления шины. */
gt_poll(&board_clock); /* Объявляет готовность и ограничивает повторы одной секундой локального времени. */
} /* Завершает неблокирующий проход обслуживания. */
__interrupt void gt_f28335_timer2_isr(void) /* Обслуживает прямой CPU INT14 без использования группы PIE. */
{ /* Этот вектор принадлежит библиотеке только после явного timer2_start. */
CpuTimer2Regs.TCR.all = 0xC000U; /* TIF[15]=1 очищает флаг, TIE[14]=1 сохраняет IRQ, TSS=0 продолжает счёт. */
gt_f28335_tick_1ms(); /* Один период Timer2 соответствует одной миллисекунде программных часов. */
} /* Для прямого INT14 подтверждение PIEACK не требуется. */
int gt_f28335_timer2_start(uint32_t cpu_hz) /* Опционально запускает выделенный таймер с периодом ровно 1 мс. */
{ /* Частота поступает от проекта; порт не предполагает жёстко заданные 150 МГц. */
uint16_t state; /* Сохраняет прежнюю глобальную маску IRQ на время смены вектора. */
if (!initialized || cpu_hz < 1000UL || cpu_hz % 1000UL != 0UL) return 0; /* Отвергает период с дробным числом тактов вместо накопления скрытого дрейфа. */
state = enter(0); /* Исключает запуск INT14 со старым вектором и новым периодом. */
CpuTimer2Regs.TCR.all = 0x0010U; /* TSS[4]=1 останавливает счёт, TIE=0 временно запрещает IRQ Timer2. */
CpuTimer2Regs.PRD.all = cpu_hz / 1000UL - 1UL; /* Период включает значения PRD..0; при 150 МГц это 149999, то есть 150000 тактов за 1 мс. */
CpuTimer2Regs.TPR.all = 0U; /* Нулевой младший предделитель означает один такт CPU на шаг счётчика. */
CpuTimer2Regs.TPRH.all = 0U; /* Нулевой старший предделитель не добавляет деления частоты. */
EALLOW; /* Разрешает запись защищённой таблицы векторов прерываний TI. */
PieVectTable.TINT2 = &gt_f28335_timer2_isr; /* Подключает собственный обработчик прямого CPU Timer2/INT14. */
EDIS; /* Закрывает доступ к векторной таблице после установки обработчика. */
IFR &= (uint16_t)~M_INT14; /* Снимает старый pending CPU INT14 до запуска нового периодического такта. */
IER |= M_INT14; /* Разрешает только INT14, сохраняя маски остальных CPU-прерываний. */
timer_owned = 1U; /* Позволяет stop изменять Timer2 только после явного захвата этим портом. */
CpuTimer2Regs.TCR.all = 0xC020U; /* TIF=1 очищает флаг, TIE=1 разрешает IRQ, TRB=1 загружает PRD, TSS=0 запускает таймер. */
leave(0, state); /* Сохраняет выбор приложения, когда разрешить глобальные прерывания. */
return 1; /* Приложение должно исключить параллельный вызов tick из своего Timer1. */
} /* Завершает настройку автономного миллисекундного такта. */
void gt_f28335_timer2_stop(void) /* Освобождает аппаратный такт, ранее явно запущенный библиотекой. */
{ /* Не трогает чужой Timer2, если выбран hook существующего таймера. */
uint16_t state = enter(0); /* Не допускает INT14 между остановкой счётчика и снятием разрешения CPU. */
if (timer_owned) { /* Ограничивает изменения ресурсом, захваченным этим портом. */
CpuTimer2Regs.TCR.all = 0x8010U; /* TIF=1 очищает флаг, TSS=1 останавливает таймер, TIE=0 запрещает источник IRQ. */
IER &= (uint16_t)~M_INT14; /* Убирает только разрешение прямого Timer2 IRQ, сохраняя остальные CPU-сервисы. */
timer_owned = 0U; /* Фиксирует освобождение источника аппаратного такта. */
} /* При отсутствии владения никакие регистры таймера не меняются. */
leave(0, state); /* Возвращает прежнее состояние глобальной маски IRQ. */
} /* Завершает необязательную остановку аппаратных часов. */

View File

@@ -0,0 +1,19 @@
#ifndef GLOBAL_TIME_F28335_H /* Защищает порт F28335 от повторного включения. */
#define GLOBAL_TIME_F28335_H /* Отмечает единственный экземпляр аппаратного порта. */
#include "global_time.h" /* Экспортирует тот же снимок метки и календарный API, что переносимое ядро. */
#ifdef __cplusplus /* Сохраняет возможность подключения из приложения C++. */
extern "C" { /* Фиксирует C ABI аппаратных точек входа. */
#endif /* Завершает условное открытие C ABI. */
int gt_f28335_init(uint16_t device_type, uint16_t device_id); /* После InitECan и InitPieVectTable, до IRQ: занимает только eCAN-A MBOX7 RX и MBOX8 TX. */
int gt_f28335_init_periph(uint16_t periph); /* Преобразует PERIPH 0..31 в Type=1/2, ID=0..15 по реестру ProtoCAN. */
gt_clock *gt_f28335_clock(void); /* Даёт логгеру ядро для gt_get; указатель действителен после успешного init. */
void gt_f28335_ready(void); /* Вызывается после успешной инициализации всей платы, затем отправка происходит из service. */
void gt_f28335_tick_1ms(void); /* Подключается к существующему ISR 1 мс, если отдельный Timer2 не используется. */
void gt_f28335_rx_isr(void); /* Вызывается общим ECAN0INTA ISR до очистки общих флагов; не выполняет календарных циклов. */
void gt_f28335_service(void); /* Вызывается в main: применяет последний RX, обслуживает ACK и повторы готовности без ожиданий. */
int gt_f28335_timer2_start(uint32_t cpu_hz); /* Необязательный такт: занимает CPU Timer2/INT14, принимает частоту CPU в Гц, кратную 1000. */
void gt_f28335_timer2_stop(void); /* Останавливает только ранее запущенный портом Timer2, сохраняя остальные разрешения CPU IRQ. */
#ifdef __cplusplus /* Закрывает C ABI при использовании C++. */
} /* Завершает блок аппаратных функций с C-связыванием. */
#endif /* Завершает языковую проверку. */
#endif /* Завершает защиту заголовка порта. */

View File

@@ -0,0 +1,156 @@
#include "global_time.h" /* Подключает переносимые типы, API часов и фиксированные поля ProtoCAN. */
#include <string.h> /* Использует явный сброс состояния вместо предположений о старте TI COFF. */
static uint16_t month_days(uint16_t year, uint16_t month) /* Вычисляет длину уже проверенного месяца по григорианскому календарю. */
{ /* Ограничивает область таблицы календаря и условия високосного года. */
static const uint16_t days[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; /* Число суток каждого месяца обычного года. */
return (uint16_t)(days[month - 1U] + (month == 2U && year % 4U == 0U && (year % 100U != 0U || year % 400U == 0U))); /* Добавляет 29 февраля, включая исключение для 2100 года. */
} /* Завершает календарный расчёт без обращения к платформе. */
static int date_seconds(const gt_datetime *date, uint32_t *seconds) /* Проверяет все входные поля до индексации календарных таблиц. */
{ /* Собирает секунды эпохи отдельно от критической секции часов. */
uint16_t year, month; /* Счётчики лет и месяцев нужны только при редкой синхронизации. */
uint32_t days = 0; /* Накопленные сутки от начала 2000 года; диапазон CAN не превышает 36525 суток. */
if (!date || date->year < 2000U || date->year > 2099U || date->month < 1U || date->month > 12U) return 0; /* Отвергает неподдерживаемую эпоху и неверный индекс месяца. */
if (date->day < 1U || date->day > month_days(date->year, date->month) || date->weekday > 6U) return 0; /* Принимает только существующую дату и формат дня недели действующего STM32 RTCSETUP. */
if (date->hour > 23U || date->minute > 59U || date->second > 59U || date->millisecond > 999U) return 0; /* Исключает перенос некорректных полей в соседние сутки. */
for (year = 2000U; year < date->year; ++year) days += 365UL + (month_days(year, 2U) == 29U); /* Учитывает каждый полный год относительно фиксированной эпохи. */
for (month = 1U; month < date->month; ++month) days += month_days(date->year, month); /* Прибавляет полные месяцы текущего года. */
days += date->day - 1U; /* Переводит календарное число 1..31 в число завершённых суток. */
*seconds = days * 86400UL + (uint32_t)date->hour * 3600UL + (uint32_t)date->minute * 60UL + date->second; /* Константы задают секунд в сутках, часе и минуте; UL исключает 16-битное умножение C28x. */
return 1; /* Подтверждает полностью проверенную календарную величину. */
} /* Завершает преобразование календаря в секундную шкалу. */
uint64_t gt_timestamp_ms(const gt_stamp *stamp) /* Позволяет логгеру сохранить абсолютное время без потери миллисекунд. */
{ /* Выполняет расчёт по уже снятому атомарному снимку. */
return (uint64_t)stamp->seconds * 1000ULL + stamp->millisecond; /* Расширяет до умножения, чтобы календарная метка не переполняла uint32_t. */
} /* Завершает получение широкой метки времени. */
int gt_init(gt_clock *clock, const gt_port *port, uint16_t device_type, uint16_t device_id) /* Создаёт часы до запуска прерываний устройства. */
{ /* Отказывается от конфигурации без реальной защиты общего состояния. */
if (!clock || !port || !port->enter || !port->leave || !port->send || device_type > 7U || device_id > 15U) return 0; /* Не обрезает ошибочный CAN-адрес до адреса другой платы. */
memset(clock, 0, sizeof(*clock)); /* Сбрасывает в том числе неинициализированную .ebss TI COFF и старый признак синхронизации. */
clock->port = *port; /* Сохраняет callbacks без выделения динамической памяти. */
clock->device_type = device_type; /* Запоминает тип источника пакета готовности. */
clock->device_id = device_id; /* Запоминает экземпляр источника пакета готовности. */
clock->stamp.weekday = 6U; /* Начальная локальная эпоха 2000-01-01 приходится на субботу при 0=воскресенье. */
return 1; /* Разрешает приложению подключать такт и CAN только после успеха. */
} /* Завершает обязательную явную инициализацию. */
void gt_tick_1ms(gt_clock *clock) /* Выполняет короткий шаг часов из миллисекундного ISR. */
{ /* Защищает разрядные переносы и от вложенных читателей. */
uint16_t state = clock->port.enter(clock->port.user); /* Сохраняет прежний INTM, чтобы выход из ISR не разрешил вложенные IRQ. */
++clock->stamp.uptime_ms; /* Продвигает независимое время работы modulo 2^32 миллисекунд. */
if (++clock->stamp.millisecond == 1000U) { /* Переносит ровно 1000 аппаратных миллисекунд в одну секунду. */
clock->stamp.millisecond = 0U; /* Начинает следующую секунду с нулевой дробной части. */
if (++clock->stamp.seconds % 86400UL == 0UL) clock->stamp.weekday = (clock->stamp.weekday + 1U) % 7U; /* При переходе суток продвигает принятый день недели по циклу из семи дней. */
} /* Завершает редкую секундную ветвь без календарных циклов в ISR. */
clock->port.leave(clock->port.user, state); /* Восстанавливает исходную маску IRQ вызывающего кода. */
} /* Завершает единственный источник продвижения часов. */
void gt_get(gt_clock *clock, gt_stamp *out) /* Снимает согласованные дату, долю секунды и качество синхронизации. */
{ /* Не допускает разрыва 32/64-битных полей на 16-битной шине C28x. */
uint16_t state = clock->port.enter(clock->port.user); /* Блокирует изменение снимка таймером и CAN до завершения копирования. */
*out = clock->stamp; /* Копирует все поля в пределах одной критической секции. */
clock->port.leave(clock->port.user, state); /* Восстанавливает IRQ без безусловного EINT. */
} /* Завершает чтение метки для логгера и тайм-аутов. */
void gt_ready(gt_clock *clock) /* Отмечает фактическое завершение инициализации прикладных подсистем. */
{ /* Разделяет настройку часов и объявление готовности всей платы. */
clock->ready = 1U; /* Повторные вызовы не сбрасывают историю отправок или синхронизаций. */
} /* Передачу сообщения выполнит основной цикл. */
void gt_poll(gt_clock *clock) /* Обслуживает объявление готовности без задержек и ожидания CAN. */
{ /* Разрешён один вызывающий основной цикл; callback send копирует временный кадр. */
gt_stamp now; /* Согласованное время определяет частоту повторов независимо от перевода часов. */
gt_frame frame; /* Локальный пакет не занимает место в постоянной очереди ядра. */
if (!clock->ready) return; /* Плата не сообщает об успехе раньше завершения своей инициализации. */
gt_get(clock, &now); /* Снимает монотонную базу и признак получения RTCSETUP. */
if (clock->announced && (now.synchronized || (uint32_t)(now.uptime_ms - clock->sent_uptime_ms) < GT_READY_RETRY_MS)) return; /* После синхронизации прекращает повторы, сохраняя обязательную первую отправку. */
memset(&frame, 0, sizeof(frame)); /* Явно обнуляет RTR и неиспользуемые октеты сообщения. */
frame.id = 0x18010000UL | ((uint32_t)clock->device_type << 24U) | ((uint32_t)clock->device_id << 20U) | GT_READY_BODY; /* Задаёт стандартный приоритет, Route=1, DISCRETE=1 и FLAGS=3. */
frame.extended = 1U; /* Использует только штатный расширенный идентификатор ProtoCAN. */
frame.dlc = 1U; /* Одного октета хватает для общего флага готовности. */
frame.data[0] = 1U; /* Бит 0 означает завершение инициализации; прочие биты зарезервированы нулями. */
if (clock->port.send(clock->port.user, &frame)) { /* Не считает занятый mailbox успешной отправкой. */
clock->announced = 1U; /* Фиксирует приём транспортом, а не отдельный прикладной ACK мастера. */
clock->sent_uptime_ms = now.uptime_ms; /* Начинает секундный интервал до повтора при отсутствии RTCSETUP. */
} /* При отказе транспорта следующая итерация main повторит попытку без ожидания. */
} /* Завершает неблокирующее обслуживание готовности. */
static int data_frame(const gt_frame *frame) /* Выполняет общую проверку контейнера classic CAN. */
{ /* Не позволяет служебным битам драйвера участвовать в разборе ID. */
return frame && frame->extended == 1U && !frame->remote && frame->id <= 0x1FFFFFFFUL; /* Требует IDE=1, RTR=0 и ровно 29 адресных битов. */
} /* Завершает общую проверку адресного формата. */
int gt_parse_ready(const gt_frame *frame, uint16_t *device_type, uint16_t *device_id) /* Распознаёт на верхнем уровне только определённый пакет готовности. */
{ /* Не путает событие старта с пульсом или произвольными дискретными флагами. */
if (!data_frame(frame) || (frame->id & 0x080FFFFFUL) != (0x08010000UL | GT_READY_BODY) || frame->dlc != 1U || frame->data[0] != 1U) return 0; /* Проверяет Route=1, MsgType=1, Body=0x3000 и единственный разрешённый флаг. */
if (device_type) *device_type = (uint16_t)((frame->id >> 24U) & 7UL); /* Возвращает трёхбитный тип источника при наличии выходного указателя. */
if (device_id) *device_id = (uint16_t)((frame->id >> 20U) & 15UL); /* Возвращает четырёхбитный номер экземпляра источника. */
return 1; /* Верхний уровень может ответить одним общим RTCSETUP всем устройствам. */
} /* Завершает распознавание стартового события мастером. */
int gt_make_rtc(const gt_datetime *date, gt_frame *out) /* Кодирует существующую команду синхронизации для верхнего уровня. */
{ /* Передаёт ровно те семь полей, которые принимает STM32-порт ProtoCAN. */
uint32_t seconds; /* При проверке календаря получает допустимую секундную величину. */
if (!out || !date_seconds(date, &seconds) || date->millisecond != 0U) return 0; /* Не отбрасывает ненулевые миллисекунды молча: проводной формат их не содержит. */
memset(out, 0, sizeof(*out)); /* Нули задают RTR=0, критический приоритет и резерв payload. */
out->id = GT_RTC_ID; /* Передаёт BROADCAST RTCSETUP от верхнего уровня с нулевым параметром команды. */
out->extended = 1U; /* Выбирает расширенный 29-битный CAN-кадр. */
out->dlc = 7U; /* Сохраняет существующий контракт DLC=7 без добавления миллисекундного расширения. */
out->data[0] = date->hour; /* Первый октет содержит часы в двоичном представлении. */
out->data[1] = date->minute; /* Второй октет содержит минуты. */
out->data[2] = date->second; /* Третий октет содержит целые секунды. */
out->data[3] = date->year - 2000U; /* Четвёртый октет хранит смещение года 0..99 от эпохи 2000. */
out->data[4] = date->month; /* Пятый октет содержит месяц 1..12. */
out->data[5] = date->day; /* Шестой октет содержит проверенное число месяца. */
out->data[6] = date->weekday; /* Седьмой октет сохраняет принятый формат дня недели 0..6. */
return 1; /* Кадр готов к неблокирующей отправке транспортом мастера. */
} /* Завершает кодирование общего broadcast. */
int gt_receive(gt_clock *clock, const gt_frame *frame, uint32_t received_uptime_ms) /* Проверяет и применяет RTCSETUP, привязанный к моменту приёма. */
{ /* Отвергает некорректные кадры до изменения состояния устройства. */
gt_datetime date; /* Временный календарь строится из отдельных октетов без struct overlay. */
uint32_t seconds, elapsed, corrected_seconds, whole_days; /* Содержит секунды мастера, задержку в мс, актуальные секунды и перенос дня недели. */
uint16_t i, state; /* Индекс проверки payload и сохранённое состояние IRQ. */
uint16_t corrected_ms; /* Хранит дробную часть отдельно, исключая дорогие 64-битные деления под маской IRQ C28x. */
if (!data_frame(frame) || (frame->id & 0x0FFFFFFFUL) != GT_RTC_ID || frame->dlc != 7U) return 0; /* Принимает оба приоритета, но только Route=0, Type/ID=0, BROADCAST RTCSETUP с параметром 0. */
for (i = 0U; i < 7U; ++i) if (frame->data[i] > 255U) return 0; /* C28x хранит октет в 16-битном элементе; старшие биты означают ошибку адаптера. */
date.hour = frame->data[0]; /* Восстанавливает часы по штатному порядку RTCSETUP. */
date.minute = frame->data[1]; /* Восстанавливает минуты без BCD-преобразований. */
date.second = frame->data[2]; /* Восстанавливает целые секунды мастера. */
date.year = 2000U + frame->data[3]; /* Восстанавливает полный год из двухзначного поля протокола. */
date.month = frame->data[4]; /* Читает номер месяца до проверки таблицы длительности. */
date.day = frame->data[5]; /* Читает число месяца для строгой календарной проверки. */
date.weekday = frame->data[6]; /* Сохраняет присланную мастером фазу недели. */
date.millisecond = 0U; /* Семь октетов RTCSETUP задают точность источника только до секунды. */
if (!date_seconds(&date, &seconds)) return 0; /* Неверные даты не сбрасывают часы и признак предыдущей синхронизации. */
state = clock->port.enter(clock->port.user); /* Защищает сравнение, перенос и публикацию новой временной базы. */
elapsed = clock->stamp.uptime_ms - received_uptime_ms; /* Учитывает время очереди main, в том числе через переполнение uint32_t uptime. */
corrected_seconds = seconds + elapsed / 1000UL; /* Прибавляет полные секунды задержки RX; диапазон входных годов и задержка до 2^32 мс помещаются в uint32_t. */
corrected_ms = (uint16_t)(elapsed % 1000UL); /* Восстанавливает фазу миллисекунд без 64-битного деления в критической секции. */
clock->stamp.correction_ms = ((int64_t)corrected_seconds - clock->stamp.seconds) * 1000LL + corrected_ms - clock->stamp.millisecond; /* Сохраняет знак и полную величину смещения, расширяя разность секунд до умножения. */
whole_days = corrected_seconds / 86400UL - seconds / 86400UL; /* Определяет переходы через полночь за время отложенной обработки. */
clock->stamp.seconds = corrected_seconds; /* Публикует новые секунды независимо от монотонного uptime. */
clock->stamp.millisecond = corrected_ms; /* Публикует долю секунды, соответствующую текущему локальному тику. */
clock->stamp.weekday = (uint16_t)((date.weekday + whole_days) % 7UL); /* Сохраняет правильный день недели даже при обработке после полуночи. */
clock->stamp.sync_uptime_ms = received_uptime_ms; /* Позволяет потребителю вычислить возраст последнего опорного пакета. */
++clock->stamp.generation; /* Отмечает любую принятую коррекцию, включая перевод часов назад. */
clock->stamp.synchronized = 1U; /* Последней записью подтверждает валидность полностью опубликованной даты. */
clock->port.leave(clock->port.user, state); /* Возвращает прежнюю маску прерываний после атомарного обновления. */
return 1; /* Сообщает транспорту об успешной обработке синхронизации. */
} /* Завершает сравнение и немедленную коррекцию часов. */
int gt_calendar(const gt_stamp *stamp, gt_datetime *out) /* Преобразует снимок для человека без блокировки аппаратного таймера. */
{ /* Вызывает календарные циклы только по требованию логгера или интерфейса. */
uint32_t days, remainder; /* Разделяет полные сутки и секунды внутри суток. */
uint16_t year = 2000U, month = 1U, length; /* Начинает разбор от эпохи библиотеки. */
if (!stamp || !out || !stamp->synchronized) return 0; /* Не выдаёт локальный отсчёт до синхронизации за известную календарную дату. */
days = stamp->seconds / 86400UL; /* Выделяет количество завершённых календарных суток. */
remainder = stamp->seconds % 86400UL; /* Сохраняет секунды текущих суток отдельно от календарных вычислений. */
while (days >= (uint32_t)(length = (uint16_t)(365U + (month_days(year, 2U) == 29U)))) { /* Вычитает полные годы с учётом григорианских исключений. */
days -= length; /* Оставляет только сутки после текущего полного года. */
++year; /* Переходит к следующему году, включая работу после 2099 без нового CAN-пакета. */
} /* Завершает поиск календарного года. */
while (days >= (length = month_days(year, month))) { /* Вычитает полные месяцы найденного года. */
days -= length; /* Сохраняет остаток суток внутри следующего месяца. */
++month; /* Продвигает месяц до того, которому принадлежит остаток. */
} /* Завершает поиск календарного месяца. */
out->year = year; /* Возвращает полный календарный год. */
out->month = month; /* Возвращает месяц в привычном диапазоне 1..12. */
out->day = (uint16_t)(days + 1UL); /* Переводит нулевой индекс остатка суток в календарное число. */
out->weekday = stamp->weekday; /* Возвращает фазу недели из согласованного снимка, а не из текущих часов. */
out->hour = (uint16_t)(remainder / 3600UL); /* Выделяет целые часы внутри суток. */
out->minute = (uint16_t)((remainder % 3600UL) / 60UL); /* Выделяет минуты после последнего полного часа. */
out->second = (uint16_t)(remainder % 60UL); /* Возвращает остаток секунд текущей минуты. */
out->millisecond = stamp->millisecond; /* Сохраняет миллисекундную долю исходного снимка. */
return 1; /* Подтверждает наличие календарной синхронизации в исходной метке. */
} /* Завершает календарное представление метки логгера. */

View File

@@ -0,0 +1,26 @@
#ifndef GT_TEST_DSP2833X_DEVICE_H /* Изолирует модель регистров от настоящих заголовков TI. */
#define GT_TEST_DSP2833X_DEVICE_H /* Предотвращает повторное определение тестовых типов. */
#include <stdint.h> /* Использует одинаковые разрядности полей на host-платформе. */
#define EALLOW ((void)0) /* Модель не требует аппаратного разрешения защищённой записи. */
#define EDIS ((void)0) /* Закрытие защищённой записи не меняет модельные регистры. */
#define __interrupt /* Host-компилятор проверяет обычное тело ISR без ABI TI. */
#define M_INT14 0x2000U /* CPU INT14 соответствует биту 13 в IER/IFR TMS320F28335. */
union gt_test_reg { /* Даёт модели 32-битный доступ all и поля, которые использует настоящий порт. */
uint32_t all; /* Имитирует единственный аппаратный доступ к регистру eCAN. */
struct { unsigned DLC:4; unsigned RTR:1; unsigned DBO:1; } bit; /* Для модели важны доступные имена; фактическую раскладку проверяет отдельная TI-компиляция. */
}; /* Завершает минимальную модель регистра. */
struct gt_test_mbox { union gt_test_reg MSGID, MSGCTRL, MDL, MDH; }; /* Выделяет поля mailbox, реально используемые драйвером. */
struct gt_test_mboxes { struct gt_test_mbox MBOX7, MBOX8; }; /* Не предоставляет портам доступ к невыделенным mailbox. */
struct gt_test_ecan { union gt_test_reg CANME, CANMD, CANOPC, CANMIL, CANMIM, CANRMP, CANRML, CANTA, CANAA, CANTRS, CANMC; }; /* Моделирует только задействованные управляющие регистры CAN. */
struct gt_test_lam { union gt_test_reg LAM7; }; /* Предоставляет локальную маску только своего RX. */
struct gt_test_timer { union gt_test_reg TCR, PRD, TPR, TPRH; }; /* Хранит настройку периода и предделителей CPU Timer2. */
struct gt_test_vectors { void (*TINT2)(void); }; /* Позволяет вызвать реальный обработчик порта через установленный им вектор. */
extern struct gt_test_mboxes ECanaMboxes; /* Делит модельные mailbox между портом и тестом. */
extern struct gt_test_ecan ECanaRegs; /* Делит управляющие регистры между кодом порта и проверками. */
extern struct gt_test_lam ECanaLAMRegs; /* Делит состояние аппаратного фильтра. */
extern struct gt_test_timer CpuTimer2Regs; /* Делит конфигурацию автономного миллисекундного таймера. */
extern struct gt_test_vectors PieVectTable; /* Делит назначенный Timer2 вектор. */
extern uint16_t IER, IFR; /* Имитирует CPU-регистры разрешения и ожидания прерываний. */
uint16_t __disable_interrupts(void); /* Предоставляет модель intrinsic с реальным сохранением прежнего состояния. */
void __restore_interrupts(uint16_t state); /* Предоставляет модель точного восстановления IRQ. */
#endif /* Завершает защиту тестовой модели аппаратуры. */

View File

@@ -0,0 +1,36 @@
"""Проверяет ядро и настоящий порт на host; опционально компилирует их TI C28x.""" # Описывает границу проверки без обещания аппаратного испытания.
import argparse # Принимает явные пути TI SDK вместо привязки библиотеки к рабочему компьютеру.
import os # Настраивает PATH компилятора, чтобы Windows находила его DLL.
from pathlib import Path # Строит пути относительно самого тестового набора.
import shutil # Находит GCC или Clang в окружении разработчика.
import subprocess # Запускает компиляторы аргументами без shell-интерполяции.
import tempfile # Изолирует все исполняемые и объектные файлы во временном каталоге.
root = Path(__file__).resolve().parents[1] # Определяет корень c/global-time независимо от текущего каталога.
parser = argparse.ArgumentParser(description=__doc__) # Показывает назначение host- и TI-проверок при --help.
parser.add_argument("--cc", default=os.environ.get("CC")) # Разрешает явно выбрать совместимый host-компилятор.
parser.add_argument("--ti-cgt", type=Path) # Принимает каталог TI C2000 compiler с bin/cl2000 и include.
parser.add_argument("--ti-headers", type=Path) # Принимает каталог с настоящим DSP2833x_Device.h проекта или TI SDK.
parser.add_argument("--ti-common", type=Path) # Принимает дополнительный каталог DSP2833x_GlobalPrototypes.h из DSP2833x_common/include.
args = parser.parse_args() # Проверяет параметры до создания любых файлов сборки.
candidates = [args.cc, shutil.which("gcc"), shutil.which("clang"), "C:/msys64/ucrt64/bin/gcc.exe"] # Последний путь обеспечивает привычное Windows-окружение без изменения глобального PATH.
compiler = next((str(Path(p).resolve()) for p in candidates if p and Path(p).is_file()), None) # Выбирает первый существующий компилятор, сохраняя явный --cc в приоритете.
if not compiler: # Не заменяет отсутствие компилятора успешным пустым запуском.
parser.error("Укажите --cc с путём к GCC/Clang") # Даёт воспроизводимое требование для запуска регрессий.
if bool(args.ti_cgt) != bool(args.ti_headers): # TI проверка требует одновременно компилятор и аппаратные заголовки.
parser.error("--ti-cgt и --ti-headers нужно указать вместе") # Предотвращает случайную проверку порта с host-моделью вместо SDK.
env = dict(os.environ) # Сохраняет исходное окружение остальных инструментов.
env["PATH"] = str(Path(compiler).parent) + os.pathsep + env.get("PATH", "") # Добавляет соседние DLL GCC только в дочерние процессы тестов.
with tempfile.TemporaryDirectory(prefix="global-time-") as output: # Удаляет только собственный временный каталог после завершения проверок.
for name, sources, includes in [("core", ["src/global_time.c", "tests/test_global_time.c"], ["include"]), ("f28335", ["src/global_time.c", "ports/tms320f28335/global_time_f28335.c", "tests/test_f28335.c"], ["include", "ports/tms320f28335", "tests/f28335_stubs"])]: # Проверяет ядро отдельно и реальный порт с модельными регистрами.
exe = Path(output) / (name + (".exe" if os.name == "nt" else "")) # Сохраняет host-программы за пределами исходного дерева.
command = [compiler, "-std=c99", "-Wall", "-Wextra", "-Werror", "-pedantic", *["-I" + str(root / p) for p in includes], *[str(root / p) for p in sources], "-o", str(exe)] # Считает все предупреждения ошибками и использует реальные исходники библиотеки.
subprocess.run(command, check=True, env=env, timeout=60) # Не маскирует ошибку компиляции успешным запуском старого EXE.
subprocess.run([str(exe)], check=True, env=env, timeout=30) # Выполняет календарные и аппаратные сценарии с ограниченным временем.
if args.ti_cgt: # Дополнительная проверка нужна для ABI с 16-битным char и настоящих регистров TI.
ti = args.ti_cgt.resolve() # Фиксирует абсолютный корень выбранного SDK компилятора.
for source in ["src/global_time.c", "ports/tms320f28335/global_time_f28335.c"]: # Компилирует оба поставляемых MCU исходника без изменения прошивки потребителя.
obj = Path(output) / (Path(source).stem + ".obj") # Разделяет объектные файлы ядра и аппаратного адаптера.
include_paths = [root / "include", root / "ports/tms320f28335", args.ti_headers.resolve(), ti / "include", *([args.ti_common.resolve()] if args.ti_common else [])] # Отдельно подключает common-заголовки старого TI SDK, не смешивая их с host-моделью.
command = [str(ti / "bin/cl2000.exe"), "--compile_only", "-v28", "-ml", "-mt", "--float_support=fpu32", "--abi=coffabi", "--c99", "-O2", "--opt_for_speed=0", "--display_error_number", *["--include_path=" + str(p) for p in include_paths], "--output_file=" + str(obj), str(root / source)] # Использует large/COFF ABI действующего проекта F28335 и штатные заголовки.
subprocess.run(command, check=True, timeout=60) # Ошибка TI C28x останавливает проверку вместо объявления host-модели достаточной.
print("global-time: TI C28x large/COFF compilation OK") # Подтверждает именно компиляцию объектов, не аппаратные испытания или полную линковку прошивки.

View File

@@ -0,0 +1,88 @@
#include "DSP2833x_Device.h" /* Использует host-модель регистров, а не альтернативную реализацию драйвера. */
#include "global_time_f28335.h" /* Тестирует публичные hooks настоящего аппаратного порта. */
#ifdef NDEBUG /* Не позволяет Release-сборке превратить аппаратные проверки в пустую программу. */
#undef NDEBUG /* Включает assert независимо от флагов конфигурации CMake. */
#endif /* Завершает настройку обязательных проверяющих утверждений. */
#include <assert.h> /* Проверяет сохранность соседних ресурсов, обработку CAN и период Timer2. */
#include <stdio.h> /* Выводит краткое подтверждение проверенных сценариев. */
struct gt_test_mboxes ECanaMboxes; /* Выделяет модельные MBOX7 и MBOX8. */
struct gt_test_ecan ECanaRegs; /* Выделяет модель управляющих регистров eCAN-A. */
struct gt_test_lam ECanaLAMRegs; /* Выделяет локальную маску RX. */
struct gt_test_timer CpuTimer2Regs; /* Выделяет модель CPU Timer2. */
struct gt_test_vectors PieVectTable; /* Хранит реальный адрес ISR, назначенный портом. */
uint16_t IER, IFR; /* Выделяет модель CPU-масок INT14 и соседних прерываний. */
static uint16_t masked; /* Сохраняет модельное состояние глобальной маски INTM. */
uint16_t __disable_interrupts(void) /* Моделирует атомарный запрет IRQ и возвращаемый предыдущий INTM. */
{ /* Повторный запрет из ISR обязан вернуть уже установленную маску. */
uint16_t old = masked; /* Сохраняет прежнюю маску до её изменения. */
masked = 1U; /* Имитирует запрет маскируемых IRQ. */
return old; /* Позволяет реальному порту точно восстановить предыдущий режим. */
} /* Завершает модель intrinsic запрета. */
void __restore_interrupts(uint16_t state) /* Восстанавливает маску, полученную соответствующим входом. */
{ /* Проверяет, что порт действительно вызвал вход в критическую секцию. */
assert(masked == 1U); /* Восстановление без предшествующего запрета нарушило бы вложенность. */
masked = state; /* Сохраняет исходный ISR-контекст без безусловного разрешения IRQ. */
} /* Завершает модель intrinsic восстановления. */
static void inject(const gt_frame *frame) /* Доставляет wire-данные в аппаратный RX и вызывает настоящий ISR hook. */
{ /* Не вызывает gt_receive напрямую, проверяя весь путь распаковки MDL/MDH. */
uint16_t i; /* Перебирает восемь проводных октетов фиксированного classic CAN. */
ECanaMboxes.MBOX7.MSGID.all = 0xC0000000UL | frame->id; /* Имитирует расширенный ID и включённую локальную фильтрацию eCAN. */
ECanaMboxes.MBOX7.MSGCTRL.all = frame->dlc | ((uint32_t)frame->remote << 4U); /* Укладывает DLC и RTR в модельный регистр сообщения. */
ECanaMboxes.MBOX7.MDL.all = ECanaMboxes.MBOX7.MDH.all = 0UL; /* Сбрасывает старый payload перед имитацией нового приёма. */
for (i = 0U; i < 8U; ++i) { /* Укладывает данные младшим октетом вперёд, как eCAN при DBO=0. */
if (i < 4U) ECanaMboxes.MBOX7.MDL.all |= (uint32_t)frame->data[i] << (8U * i); /* Первые четыре октета занимают MDL. */
else ECanaMboxes.MBOX7.MDH.all |= (uint32_t)frame->data[i] << (8U * (i - 4U)); /* Следующие четыре октета занимают MDH. */
} /* Завершает аппаратное размещение кадра. */
ECanaRegs.CANRMP.all = 0x82UL; /* Выставляет RX7 и независимый pending legacy mailbox1. */
gt_f28335_rx_isr(); /* Захватывает временную привязку и кадр в очередь настоящего порта. */
assert(ECanaRegs.CANRMP.all == 0x80UL); /* Запись W1C должна содержать только бит собственного mailbox, сохраняя pending соседа на аппаратуре. */
ECanaRegs.CANRMP.all = 2UL; /* Вручную применяет W1C к обычной RAM модели: RX7 снят, RX1 сохранён. */
} /* Завершает модель получения кадра без подмены алгоритма порта. */
int main(void) /* Проверяет аппаратную настройку и общий путь ISR → main → метка логгера. */
{ /* Модель не утверждает электрические тайминги или CAN ACK настоящего трансивера. */
gt_stamp stamp; /* Читает время через тот же атомарный API, которым будет пользоваться логгер. */
gt_datetime date = {2026U, 10U, 19U, 1U, 12U, 34U, 56U, 0U}; /* Независимый календарный эталон: понедельник 19 октября 2026 года. */
gt_frame frame; /* Содержит broadcast мастера для доставки через модель eCAN. */
uint16_t i; /* Управляет известным числом тиков до обработки кадра в main. */
gt_f28335_tick_1ms(); gt_f28335_rx_isr(); gt_f28335_service(); /* Ранние hooks до init обязаны безопасно завершаться. */
ECanaRegs.CANME.all = 0x7FUL; /* Резервирует существующие mailbox0..6 для legacy CAN, Boot и pulse. */
ECanaRegs.CANMD.all = 0xFFFFFFFAUL; /* Воспроизводит исходные направления legacy mailbox проекта 28335_periph. */
ECanaRegs.CANMIM.all = 0x4AUL; /* Имитирует разрешённые RX1/RX3/RX6 других CAN-сервисов. */
ECanaRegs.CANMIL.all = 0x40UL; /* Фиксирует чужую маршрутизацию IRQ, которую порт обязан сохранить. */
assert(!gt_f28335_init_periph(32U)); /* Некорректный физический адрес не должен открыть новый CAN-сервис. */
assert(gt_f28335_init_periph(5U)); /* Настраивает УМП1 как PERIPH5, Type1/ID5. */
assert(ECanaRegs.CANME.all == 0x1FFUL && (ECanaRegs.CANMD.all & 0x7FUL) == 0x7AUL); /* Проверяет добавление только mailbox7/8 и сохранность чужих направлений. */
assert(ECanaRegs.CANMIM.all == 0xCAUL && ECanaRegs.CANMIL.all == 0x40UL); /* Проверяет RX7 IRQ line0 без изменения RX1/RX3/RX6. */
assert(ECanaMboxes.MBOX7.MSGID.all == 0xC0000030UL && ECanaLAMRegs.LAM7.all == 0x10000000UL); /* Фильтр принимает только время мастера с любым приоритетом. */
ECanaRegs.CANTA.all = ECanaRegs.CANAA.all = ECanaRegs.CANRMP.all = 0UL; /* Применяет к RAM модели W1C, выполненные init на реальной аппаратуре. */
gt_f28335_service(); /* Проверяет первый проход main при отсутствии входящего кадра. */
assert(ECanaRegs.CANTRS.all == 0UL); /* Сам init не должен объявить готовность прикладной платы. */
gt_f28335_ready(); gt_f28335_service(); /* Публикует прикладной успех и запускает передачу из main. */
assert(ECanaRegs.CANTRS.all == 0x100UL && ECanaMboxes.MBOX8.MSGID.all == 0x99513000UL && ECanaMboxes.MBOX8.MDL.all == 1UL); /* Проверяет точный аппаратный запрос READY с правильным адресом. */
assert(gt_make_rtc(&date, &frame)); /* Строит штатный RTCSETUP без аппаратного RTC мастера в тесте. */
inject(&frame); /* Проверяет разбор всех календарных полей через настоящий RX ISR. */
for (i = 0U; i < 37U; ++i) gt_f28335_tick_1ms(); /* Имитирует 37 мс задержки main при продолжающемся такте. */
gt_f28335_service(); /* Применяет отложенный RX и сохраняет привязку к исходному моменту приёма. */
gt_get(gt_f28335_clock(), &stamp); /* Снимает тот же экземпляр часов, что обслуживает порт. */
assert(gt_calendar(&stamp, &date) && date.year == 2026U && date.month == 10U && date.day == 19U && date.hour == 12U && date.minute == 34U && date.second == 56U && date.millisecond == 37U); /* Проверяет календарь и компенсацию задержки всего пути приёма. */
assert(stamp.sync_uptime_ms == 0UL && stamp.uptime_ms == 37UL); /* Очередь не должна подменить время RX временем обработки main. */
ECanaRegs.CANTA.all = 0x100UL; ECanaRegs.CANTRS.all = 0UL; /* Имитирует аппаратный ACK ранее отправленной готовности. */
gt_f28335_service(); /* Освобождает собственный TX без повторной отправки после синхронизации. */
ECanaRegs.CANTA.all = ECanaRegs.CANAA.all = 0UL; /* Применяет аппаратное W1C к RAM модели ACK. */
assert(ECanaRegs.CANTRS.all == 0UL); /* Синхронизация прекращает повторную стартовую рассылку. */
IER = 0x1001U; IFR = 0x3001U; /* Подготавливает чужие CPU IRQ и старый pending INT14. */
assert(!gt_f28335_timer2_start(150000001UL)); /* Некратная 1000 частота не должна давать скрытый дробный период. */
assert(gt_f28335_timer2_start(150000000UL)); /* Настраивает аппаратные 1 мс для типичной частоты F28335 150 МГц. */
assert(CpuTimer2Regs.PRD.all == 149999UL && CpuTimer2Regs.TPR.all == 0UL && CpuTimer2Regs.TPRH.all == 0UL); /* Проверяет включительный период PRD..0 и отсутствие предделителей. */
assert(IER == 0x3001U && IFR == 0x1001U && CpuTimer2Regs.TCR.all == 0xC020UL); /* Сохраняет чужие CPU маски и запускает только Timer2. */
PieVectTable.TINT2(); /* Вызывает реальное тело ISR через вектор, назначенный портом. */
gt_get(gt_f28335_clock(), &stamp); /* Проверяет единственный миллисекундный шаг собственного ISR. */
assert(stamp.uptime_ms == 38UL && stamp.millisecond == 38U); /* Timer2 должен добавлять ровно один тик на прерывание. */
assert(!gt_f28335_init_periph(6U)); /* Повторный init не должен потерять владение работающим Timer2. */
gt_f28335_timer2_stop(); /* Освобождает ранее захваченный аппаратный такт. */
assert(IER == 0x1001U && CpuTimer2Regs.TCR.all == 0x8010UL && masked == 0U); /* Остановка сохраняет чужие IRQ и исходный глобальный INTM. */
ECanaRegs.CANMC.bit.DBO = 1U; /* Имитирует несовместимый порядок октетов контроллера. */
assert(!gt_f28335_init_periph(6U)); /* Не позволяет молча декодировать другую аппаратную раскладку payload. */
puts("global-time F28335: eCAN isolation, RX delay, ACK and Timer2 OK"); /* Выводит компактный результат аппаратных сценариев. */
return 0; /* Завершает проверку после всех обязательных утверждений. */
} /* Завершает host-регрессию порта TI F28335. */

View File

@@ -0,0 +1,145 @@
#include "global_time.h" /* Проверяет настоящий переносимый код часов без подмены его реализации. */
#ifdef NDEBUG /* Проверки должны выполняться и при Release-сборке CMake. */
#undef NDEBUG /* Сохраняет assert активным независимо от выбранного уровня оптимизации. */
#endif /* Завершает принудительное включение проверяющих утверждений. */
#include <assert.h> /* Останавливает тест при нарушении временного или проводного контракта. */
#include <stdio.h> /* Выводит компактное подтверждение выполнения набора проверок. */
#include <string.h> /* Позволяет воспроизвести ненулевую RAM после тёплого старта. */
static uint16_t irq_mask, depth; /* Моделирует исходную маску IRQ и вложенность критических секций. */
static int accepted, sends; /* Управляет занятостью транспорта и считает попытки объявления готовности. */
static gt_frame sent; /* Сохраняет последний кадр для независимой проверки ID и payload. */
static uint16_t enter(void *user) /* Моделирует TI intrinsic с сохранением предыдущего INTM. */
{ /* Проверяет, что ядро пользуется единым механизмом защиты состояния. */
uint16_t previous = irq_mask; /* Сохраняет маску до входа в текущий уровень блокировки. */
assert(user == &sends); /* Подтверждает неизменную передачу пользовательского контекста. */
irq_mask = 1U; /* Запрещает модельным прерываниям менять данные до leave. */
++depth; /* Отмечает вложенный уровень для контроля парности входа и выхода. */
return previous; /* Возвращает исходное состояние, включая вызов из уже закрытого ISR. */
} /* Завершает модель входа в критическую секцию. */
static void leave(void *user, uint16_t previous) /* Проверяет восстановление предыдущей маски без безусловного разрешения. */
{ /* Делает нарушения вложенности видимыми в host-тесте. */
assert(user == &sends && depth > 0U && irq_mask == 1U); /* Выход допустим только из реально захваченной секции. */
--depth; /* Освобождает один уровень модельной блокировки. */
irq_mask = previous; /* Возвращает именно исходную маску вызывающего кода. */
} /* Завершает проверяемую модель выхода. */
static int send(void *user, const gt_frame *frame) /* Моделирует неблокирующую передачу и копирование временного payload. */
{ /* Callback не должен выполняться с длинной критической секцией ядра. */
assert(user == &sends && depth == 0U); /* Проверяет передачу контекста и отсутствие блокировки IRQ на время отправки. */
++sends; /* Считает и принятые, и отклонённые транспортом попытки. */
sent = *frame; /* Сохраняет независимую копию до возврата callback. */
return accepted; /* Имитирует занятость mailbox без ожидания. */
} /* Завершает модель транспорта. */
static void advance(gt_clock *clock, uint32_t milliseconds) /* Продвигает реальный алгоритм через границы календарных полей. */
{ /* Пошаговый вызов также проверяет миллисекундные переносы в ISR. */
while (milliseconds--) gt_tick_1ms(clock); /* Не подменяет поведение таймера прямой правкой абсолютного времени. */
} /* Завершает управляемое продвижение времени. */
static gt_frame rtc(uint16_t year, uint16_t month, uint16_t day, uint16_t hour, uint16_t minute, uint16_t second) /* Строит календарный эталон кодировщиком верхнего уровня. */
{ /* Ожидаемые значения времени в тестах проверяются отдельно от кодировщика. */
gt_datetime date = {year, month, day, 6U, hour, minute, second, 0U}; /* День недели сохраняется как допустимая фаза цикла, исходная суббота=6. */
gt_frame frame; /* Получает полноценный расширенный CAN кадр. */
assert(gt_make_rtc(&date, &frame)); /* Корректный календарь обязан кодироваться в существующие семь октетов. */
return frame; /* Возвращает независимый контейнер для изменения ошибочных полей. */
} /* Завершает построение тестовых опорных кадров. */
int main(void) /* Проверяет календарь, коррекции, задержки, переполнения и стартовый CAN-обмен. */
{ /* Использует только публичный API, кроме целевой установки границы uptime. */
gt_clock clock; /* Состояние заполняется мусором, имитируя startup без очистки .ebss. */
gt_port port = {&sends, enter, leave, send}; /* Подключает проверяющие блокировки и управляемый транспорт. */
gt_stamp stamp, before; /* Два атомарных снимка позволяют проверять отсутствие побочных изменений при ошибке. */
gt_datetime date; /* Календарный результат проверяется по известным границам дат. */
gt_frame frame, bad; /* Исходный корректный кадр и копия для отрицательных проверок. */
uint16_t type, id, index; /* Полученные адресные поля и индекс проверки каждого октета. */
const uint16_t invalid[7] = {24U, 60U, 60U, 100U, 0U, 0U, 7U}; /* Независимые граничные ошибки каждого календарного поля на линии. */
memset(&clock, 0xA5, sizeof(clock)); /* Воспроизводит остатки RAM вместо неявной инициализации нулями хоста. */
assert(!gt_init(&clock, &port, 8U, 0U)); /* Запрещает трёхбитному типу переполниться в направление Route. */
assert(!gt_init(&clock, &port, 1U, 16U)); /* Запрещает молчаливое обрезание экземпляра до адреса другой платы. */
assert(gt_init(&clock, &port, 1U, 5U)); /* Создаёт УМП1 по действующему реестру PERIPH. */
gt_get(&clock, &stamp); /* Проверяет результат явной инициализации всей временной базы. */
assert(stamp.seconds == 0UL && stamp.uptime_ms == 0UL && !stamp.synchronized && stamp.generation == 0UL); /* До синхронизации календарь не должен выглядеть достоверным. */
assert(!gt_calendar(&stamp, &date)); /* Не позволяет приложению выдавать локальную эпоху за полученную дату. */
advance(&clock, 1001UL); /* Пересекает первую секундную границу программного таймера. */
gt_get(&clock, &stamp); /* Снимает дробную часть после переноса секунды. */
assert(stamp.seconds == 1UL && stamp.millisecond == 1U && stamp.uptime_ms == 1001UL); /* Проверяет ровно 1000 тиков на секунду без ошибки на единицу. */
gt_poll(&clock); /* Пытается обслужить отправку до завершения инициализации платы. */
assert(sends == 0); /* Пакет готовности запрещён до явного ready. */
gt_ready(&clock); /* Разрешает стартовый пакет только после прикладной готовности. */
gt_poll(&clock); /* Имитирует занятую CAN передачу при accepted=0. */
assert(sends == 1 && !clock.announced); /* Занятость транспорта не теряет обязательный пакет инициализации. */
accepted = 1; /* Освобождает транспорт на следующем проходе main. */
gt_poll(&clock); /* Повторяет отклонённую попытку без задержки и блокировки IRQ. */
assert(sends == 2 && sent.id == 0x19513000UL && sent.dlc == 1U && sent.data[0] == 1U); /* Фиксирует проводной эталон READY узла Type=1, ID=5. */
assert(gt_parse_ready(&sent, &type, &id) && type == 1U && id == 5U); /* Мастер должен узнать физический адрес стартовавшей платы. */
bad = sent; /* Готовит отрицательный вариант стартового сообщения. */
bad.id ^= 0x08000000UL; /* Меняет направление на запрос мастера, сохранив payload. */
assert(!gt_parse_ready(&bad, 0, 0)); /* Исключает распознавание собственного запроса как готовности платы. */
advance(&clock, 999UL); /* Проверяет ограничение частоты повторов перед секундной границей. */
gt_poll(&clock); /* Обслуживает CAN на последней миллисекунде запрета повтора. */
assert(sends == 2); /* Готовность не должна засорять шину на каждом проходе main. */
advance(&clock, 1UL); /* Достигает точно секундного интервала повторной отправки. */
gt_poll(&clock); /* Повторяет событие старта при отсутствии ответа временем. */
assert(sends == 3); /* Потерянный ответ мастера должен приводить к повтору объявления готовности. */
frame = rtc(2024U, 2U, 28U, 23U, 59U, 59U); /* Задаёт известную границу високосного февраля. */
assert(frame.id == 0x30UL && frame.dlc == 7U && frame.data[3] == 24U && frame.data[4] == 2U); /* Проверяет совместимый ID и календарный wire format. */
assert(gt_receive(&clock, &frame, clock.stamp.uptime_ms)); /* Устанавливает первую реальную календарную дату. */
advance(&clock, 1001UL); /* Пересекает полночь на один миллисекундный тик. */
gt_get(&clock, &stamp); /* Снимает календарную метку после перехода даты. */
assert(gt_calendar(&stamp, &date) && date.year == 2024U && date.month == 2U && date.day == 29U && date.hour == 0U && date.millisecond == 1U); /* Проверяет 29 февраля и сброс часов/минут/секунд на полночь. */
assert(stamp.weekday == 0U && stamp.generation == 1UL); /* День недели циклически продвигается, а тики не создают новые поколения коррекции. */
assert(gt_timestamp_ms(&stamp) > UINT32_MAX); /* Календарные миллисекунды остаются широкими и не обрезаются до uptime. */
advance(&clock, 2000UL); /* Даёт мастеру время ответить; уже синхронизированный узел не повторяет READY. */
gt_poll(&clock); /* Проверяет прекращение стартовых повторов после RTCSETUP. */
assert(sends == 3); /* Получение календаря завершает повторный запрос времени. */
before = stamp; /* Сохраняет известную метку для проверки отрицательной коррекции. */
frame = rtc(2000U, 1U, 1U, 0U, 0U, 0U); /* Задаёт независимый эталон нулевой секунды эпохи библиотеки. */
assert(gt_receive(&clock, &frame, clock.stamp.uptime_ms)); /* Применяет разрешённый перевод глобальных часов назад. */
gt_get(&clock, &stamp); /* Снимает результат атомарной коррекции. */
assert(stamp.seconds == 0UL && stamp.millisecond == 0U && stamp.correction_ms < 0 && stamp.uptime_ms > before.uptime_ms && stamp.generation == 2UL); /* Проверяет независимость тайм-аутов от отрицательной календарной коррекции. */
assert(gt_timestamp_ms(&stamp) == 0ULL); /* Фиксирует точное начало принятой эпохи. */
before = stamp; /* Сохраняет состояние до серии ошибочных CAN кадров. */
for (index = 0U; index < 7U; ++index) { /* Проверяет наличие лишних старших битов в каждом слове, содержащем октет C28x. */
bad = frame; /* Восстанавливает исходный корректный RTCSETUP перед каждой ошибкой. */
bad.data[index] = 256U; /* Значение вне одного проводного октета не должно обрезаться до нуля. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Отвергает ошибку порта до изменения даты. */
bad = frame; /* Возвращает корректный исходный кадр для проверки допустимого диапазона поля. */
bad.data[index] = invalid[index]; /* Подставляет двоичное, но календарно недопустимое значение. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Исключает нормализацию ошибочного времени и выход за границы таблицы месяцев. */
} /* Завершает проверку всей широкой wire-границы. */
bad = frame; bad.extended = 0U; /* Подготавливает стандартный 11-битный кадр с похожим ID. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* ProtoCAN требует IDE=1 независимо от численного ID. */
bad = frame; bad.remote = 1U; /* Подготавливает RTR без реальных календарных данных. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Удалённый запрос не устанавливает глобальное время. */
bad = frame; bad.dlc = 8U; /* Добавляет неописанный восьмой октет без согласованного расширения протокола. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Длина существующего RTCSETUP должна быть ровно семь октетов. */
bad = frame; bad.id |= 0x01000000UL; /* Имитирует broadcast от чужого типа устройства вместо верхнего уровня. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Исключает коррекцию по произвольным сообщениям другой платы. */
bad = frame; bad.data[4] = 2U; bad.data[5] = 30U; /* Подготавливает несуществующее 30 февраля високосного года. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Календарные ошибки не нормализуются в другую дату. */
bad = frame; bad.data[3] = 23U; bad.data[4] = 2U; bad.data[5] = 29U; /* Подготавливает 29 февраля невисокосного года. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Отвергает некорректную дату даже при допустимых диапазонах отдельных полей. */
gt_get(&clock, &stamp); /* Проверяет итоговое состояние после всех отклонённых кадров. */
assert(stamp.seconds == before.seconds && stamp.generation == before.generation && stamp.synchronized); /* Неверные пакеты не теряют последнюю достоверную синхронизацию. */
before.seconds = 3160857600UL; /* Независимый календарный эталон: ровно 36584 суток от 2000-01-01 до 2100-03-01. */
assert(gt_calendar(&before, &date) && date.year == 2100U && date.month == 3U && date.day == 1U); /* Проверяет исключение невисокосного 2100 года при продолжении локального хода часов. */
clock.stamp.uptime_ms = UINT32_MAX - 4UL; /* Целевым образом размещает монотонный счётчик около переполнения 49,7 суток. */
advance(&clock, 10UL); /* Пересекает modulo-границу до обработки уже полученного кадра. */
frame.id |= 0x10000000UL; /* Проверяет допустимость стандартного приоритета RTCSETUP наряду с критическим. */
assert(gt_receive(&clock, &frame, UINT32_MAX - 4UL)); /* Передаёт момент RX до переполнения, обработку — после. */
gt_get(&clock, &stamp); /* Снимает скорректированную фазу доли секунды. */
assert(stamp.seconds == 0UL && stamp.millisecond == 10U && stamp.uptime_ms == 5UL); /* Компенсация очереди должна быть 10 мс, независимо от переполнения uptime. */
frame = rtc(2023U, 12U, 31U, 23U, 59U, 59U); /* Проверяет смену календарного года при отложенной обработке. */
before = stamp; /* Запоминает момент RX непосредственно перед задержкой основного цикла. */
advance(&clock, 1250UL); /* Имитирует долгую прикладную операцию при продолжающемся аппаратном таймере. */
assert(gt_receive(&clock, &frame, before.uptime_ms)); /* Применяет время по моменту RX, а не по моменту выхода из очереди. */
gt_get(&clock, &stamp); /* Снимает дату после учёта задержки обработки. */
assert(gt_calendar(&stamp, &date) && date.year == 2024U && date.month == 1U && date.day == 1U && date.second == 0U && date.millisecond == 250U); /* Проверяет перенос секунды, суток и года с сохранением 250 мс. */
irq_mask = 1U; /* Имитирует вызов логгера из ISR с уже запрещёнными прерываниями. */
gt_get(&clock, &stamp); /* Проверяет чтение метки при вложенной критической секции. */
gt_tick_1ms(&clock); /* Проверяет продвижение часов при исходном INTM=1. */
assert(irq_mask == 1U && depth == 0U); /* Выходы не должны случайно разрешить IRQ вызывающего обработчика. */
irq_mask = 0U; /* Восстанавливает модельное окружение main перед новой инициализацией. */
assert(gt_init(&clock, &port, 2U, 15U)); /* Проверяет холодный reset качества и верхний адрес PERIPH31. */
assert(gt_receive(&clock, &frame, 0UL)); /* Имитирует broadcast ещё до прикладного ready. */
gt_ready(&clock); gt_ready(&clock); gt_poll(&clock); /* Повторный ready должен быть идемпотентным, но первая отправка всё равно обязательна. */
assert(sends == 4 && gt_parse_ready(&sent, &type, &id) && type == 2U && id == 15U); /* Ранняя синхронизация не подавляет сообщение об успешной инициализации платы. */
puts("global-time: calendar, CAN, correction, IRQ and rollover OK"); /* Возвращает краткое свидетельство пройденных смысловых сценариев. */
return 0; /* Завершает host-тест только после всех обязательных проверок. */
} /* Завершает независимую проверку переносимого ядра. */

View File

@@ -28,7 +28,7 @@
| `OFF` / `ON` | постоянно выключен / включён |
| `STARTUP` | три коротких импульса, затем постоянно включён |
| `WORK` | 100 мс включён, 900 мс выключен |
| `ACTIVITY` | 50/50 мс |
| `ACTIVITY` | один цикл: 100 мс включён, 900 мс выключен, затем остаётся выключенным |
| `WARNING` | 250/250 мс |
| `ERROR` | два импульса и пауза |
| `CRITICAL` | три импульса и пауза |
@@ -86,6 +86,20 @@ for (;;) {
не начинает рисунок заново. Для нового импульса события служит
`LedIndicator_RestartMode`.
Для индикации приёма вызывайте `LedIndicator_TriggerActivity` либо
`LedIndicator_TriggerActivityAt` на каждый принятый пакет. Свечение занимает
10% секундного цикла: 100 мс включён и 900 мс выключен. Новый приём немедленно
перезапускает фазу свечения, даже внутри предыдущего импульса или паузы;
очереди циклов нет. При интервалах между пакетами меньше 100 мс LED может
гореть непрерывно до истечения 100 мс после последнего пакета. Без нового
события `ACTIVITY` не повторяется. Настраиваемая таблица `patterns` по-прежнему
позволяет изменить длительности и повторение для других применений.
```c
LedIndicator_TriggerActivityAt(&led, 0U, received_ms); /* Каждый приём запускает цикл канала 0 от миллисекундной метки события. */
LedIndicator_ProcessAt(&led, now_ms); /* Главный цикл обслуживает окончание свечения без задержек и ожидания следующего пакета. */
```
## STM32F103, STM32F4, STM32G431 и STM32G474
Порт `ports/stm32-hal` принимает конкретный `TIM_HandleTypeDef *`; библиотека

View File

@@ -18,7 +18,7 @@ static const LedIndicator_Pattern g_default_patterns[LED_INDICATOR_MODE_COUNT] =
{{0U}, LEVELS_1, 1U, 1U, 1U},
{{100U, 100U, 100U, 100U, 100U, 100U}, LEVELS_101010, 6U, 0U, 1U},
{{100U, 900U}, LEVELS_10, 2U, 1U, 0U},
{{50U, 50U}, LEVELS_10, 2U, 1U, 0U},
{{100U, 900U}, LEVELS_10, 2U, 0U, 0U}, /* Один цикл приёма длится 1000 мс: 100 мс свечения составляют 10%, затем 900 мс паузы и выключение. */
{{250U, 250U}, LEVELS_10, 2U, 1U, 0U},
{{100U, 100U, 100U, 700U}, LEVELS_1010, 4U, 1U, 0U},
{{100U, 100U, 100U, 100U, 100U, 700U}, LEVELS_101010, 6U, 1U, 0U}
@@ -220,6 +220,16 @@ uint8_t LedIndicator_RestartMode(LedIndicator *instance, size_t channel,
instance->port.now_ms(instance->port.context));
}
uint8_t LedIndicator_TriggerActivityAt(LedIndicator *instance, size_t channel, uint32_t now_ms) /* Каждый пакет начинает ACTIVITY заново, даже если предыдущая вспышка ещё продолжается. */
{ /* Использует общую проверку канала и принудительный сброс фазы в миллисекундной временной базе. */
return LedIndicator_RestartModeAt(instance, channel, LED_INDICATOR_MODE_ACTIVITY, now_ms); /* Штатный однократный шаблон завершает цикл выключенным уровнем без свободного повторения. */
} /* Завершает запуск одного цикла приёма без ожидания окончания свечения либо паузы. */
uint8_t LedIndicator_TriggerActivity(LedIndicator *instance, size_t channel) /* Предоставляет тот же перезапуск по событию с использованием часов аппаратного порта. */
{ /* Сохраняет поведение варианта At для приложения без собственного передаваемого времени. */
return LedIndicator_RestartMode(instance, channel, LED_INDICATOR_MODE_ACTIVITY); /* Часы порта считываются штатным API один раз на зарегистрированный приём. */
} /* Завершает неблокирующий запуск цикла приёма с аппаратным источником времени. */
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms)
{
size_t i;

View File

@@ -114,6 +114,12 @@ void LedIndicator_Process(LedIndicator *instance);
/** Обновляет выходы с явно переданной меткой времени. */
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms);
/** Начинает один цикл ACTIVITY на каждый приём; новый вызов немедленно перезапускает свечение, не создавая очередь. */
uint8_t LedIndicator_TriggerActivityAt(LedIndicator *instance, size_t channel, uint32_t now_ms); /* Стандартный цикл: 100 мс включён, 900 мс выключен; время задаётся в миллисекундах. */
/** Использует часы аппаратного порта для такого же перезапуска цикла при каждом приёме. */
uint8_t LedIndicator_TriggerActivity(LedIndicator *instance, size_t channel); /* Вызывать из контекста Process; передачу событий из ISR обеспечивает приложение. */
LedIndicator_Mode LedIndicator_GetMode(const LedIndicator *instance, size_t channel);
#ifdef __cplusplus

View File

@@ -85,6 +85,26 @@ int main(void)
CHECK(LedIndicator_SetMode(&indicator, 3U, LED_INDICATOR_MODE_ON) == 0U);
CHECK(LedIndicator_GetMode(&indicator, 0U) == LED_INDICATOR_MODE_WORK);
puts("led indicator tests passed");
CHECK(patterns[LED_INDICATOR_MODE_ACTIVITY].duration_ms[0] == 100U && patterns[LED_INDICATOR_MODE_ACTIVITY].duration_ms[1] == 900U && patterns[LED_INDICATOR_MODE_ACTIVITY].repeat == 0U); /* Фиксирует согласованные 10% свечения и один цикл на приём в канонической таблице. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 2500U) != 0U && fake.outputs[0] == 1U); /* Приём сразу включает канал независимо от фазы прежнего WORK. */
LedIndicator_ProcessAt(&indicator, 2599U); CHECK(fake.outputs[0] == 1U); /* До истечения 100 мс светодиод продолжает гореть. */
LedIndicator_ProcessAt(&indicator, 2600U); CHECK(fake.outputs[0] == 0U); /* Ровно через 100 мс начинается оставшаяся пауза 900 мс. */
LedIndicator_ProcessAt(&indicator, 3499U); CHECK(fake.outputs[0] == 0U); /* Конец первого периода не порождает дополнительную вспышку. */
LedIndicator_ProcessAt(&indicator, 3500U); CHECK(fake.outputs[0] == 0U); /* Однократный ACTIVITY завершается выключенным уровнем на границе 1000 мс. */
LedIndicator_ProcessAt(&indicator, 4500U); CHECK(fake.outputs[0] == 0U); /* Без новых пакетов свободное периодическое мигание отсутствует. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 5000U) != 0U); /* Начинает новый приём после долгого отсутствия обмена. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 5050U) != 0U); /* Второй пакет приходит внутри текущей фазы свечения. */
LedIndicator_ProcessAt(&indicator, 5100U); CHECK(fake.outputs[0] == 1U); /* Старый срок выключения больше не действует: отсчёт перезапущен новым пакетом. */
LedIndicator_ProcessAt(&indicator, 5149U); CHECK(fake.outputs[0] == 1U); /* Новый пакет получает собственные полные 100 мс свечения. */
LedIndicator_ProcessAt(&indicator, 5150U); CHECK(fake.outputs[0] == 0U); /* Новая фаза паузы начинается ровно от последнего приёма. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 5400U) != 0U && fake.outputs[0] == 1U); /* Приём во время паузы сразу включает LED вместо ожидания границы старого периода. */
LedIndicator_ProcessAt(&indicator, 5500U); CHECK(fake.outputs[0] == 0U); /* Повторный запуск в паузе не создаёт очередь пропущенных циклов. */
fake.now = 7000U; CHECK(LedIndicator_TriggerActivity(&indicator, 1U) != 0U && fake.outputs[1] == 1U); /* Проверяет вариант событийного API с часами аппаратного порта и другим каналом. */
fake.now = 7100U; LedIndicator_Process(&indicator); CHECK(fake.outputs[1] == 0U && fake.outputs[0] == 0U); /* Каналы не перезапускают друг другу фазы и не создают фоновую активность. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, UINT32_MAX - 49U) != 0U); /* Начинает импульс непосредственно перед переполнением 32-битного времени. */
LedIndicator_ProcessAt(&indicator, 49U); CHECK(fake.outputs[0] == 1U); /* Через переполнение прошло 99 мс, поэтому свечение ещё активно. */
LedIndicator_ProcessAt(&indicator, 50U); CHECK(fake.outputs[0] == 0U); /* Через переполнение прошло ровно 100 мс, и LED гаснет без зависания. */
CHECK(LedIndicator_TriggerActivityAt(NULL, 0U, 0U) == 0U && LedIndicator_TriggerActivityAt(&indicator, 3U, 0U) == 0U); /* Ошибочный экземпляр и канал не вызывают запись чужого GPIO. */
puts("led indicator tests passed: receive 100/900 ms, immediate retrigger, no repeat, timer wrap"); /* Показывает прохождение новых событийных сценариев вместе с прежней диагностикой. */
return 0;
}

View File

@@ -1,5 +1,7 @@
# Работа с parallel NAND через GAS
> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми.
## Архитектура
Внешняя NAND значительно больше общего адресного пространства GAS. Например,

8
c/protocan-boot/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
# Результаты отдельных конфигураций CCS воспроизводятся установленным TI compiler.
/projects/ccs/*/Debug/
/projects/ccs/*/Release/
# Служебные локальные настройки IDE и launch не входят в переносимый проект.
/projects/ccs/*/.settings/
/projects/ccs/*/.launches/
# Python cache не является исходником общего модуля или проекта.
**/__pycache__/

View File

@@ -1,6 +1,6 @@
# Сборка библиотеки protocan_boot. Состав исходников и публичные include-пути задают
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
# запускать их следует через CTest из каталога сборки.
# Общая C99-библиотека ProtoCAN Boot и аппаратно независимые host-тесты.
# Каждый контроллер сохраняет собственное состояние и отдельный аппаратный порт.
# Дополнительные проекты CCS находятся в projects/ccs и не требуют CMake для TI-сборки.
cmake_minimum_required(VERSION 3.13)
project(protocan_boot C)
@@ -17,7 +17,7 @@ else()
target_compile_options(protocan_boot PRIVATE -Wall -Wextra -Wpedantic)
endif()
option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON)
option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN Boot" ON) # Не включает host-модели в код MCU.
if(PCAN_BOOT_BUILD_TESTS)
enable_testing()
add_executable(test_pcan_boot tests/test_pcan_boot.c)
@@ -45,3 +45,32 @@ if(PCAN_BOOT_BUILD_TESTS)
add_test(NAME f28335_store_${width} COMMAND test_f28335_store_${width})
endforeach()
endif()
if(PCAN_BOOT_BUILD_TESTS) # Подключает настоящий аппаратно независимый драйвер NOR F2812 и регрессию длительного BUSY.
add_executable(test_f2812_nor tests/test_f2812_nor.c src/pcan_boot.c ports/tms320f2812/nor_flash.c ports/tms320f2812/boot_store.c) # Компилирует production-код вместе с x16 моделью чипа.
target_compile_definitions(test_f2812_nor PRIVATE PCAN_BOOT_WIDE_BYTES BOOT2812_HOST) # Октеты C28x широкие; подменяется только физическая шина памяти.
add_test(NAME f2812_nor COMMAND test_f2812_nor) # Регистрирует проверки CRC, SCI8, границ, manifest и отказов NOR.
add_executable(test_pcan_busy tests/test_pcan_busy.c src/pcan_boot.c) # Использует тот же codec, который обслуживает длинные аппаратные операции.
target_compile_definitions(test_pcan_busy PRIVATE PCAN_BOOT_WIDE_BYTES) # Проверяет wire-статус при 16-битном представлении октета C28x.
add_test(NAME pcan_busy COMMAND test_pcan_busy) # Регистрирует проверку продления session при живом прогрессе erase.
endif() # Включение библиотек без тестов не добавляет host-модель в прошивку.
if(PCAN_BOOT_BUILD_TESTS) # STM32-порты проверяются с моделью HAL и приватным отображением Flash, без доступа к плате.
foreach(family f103 f407 g474) # Каждый процесс компилирует настоящую реализацию bxCAN/FDCAN одного семейства.
add_executable(test_stm32${family} tests/test_stm32_ports.c ports/stm32${family}/pcan_boot_stm32${family}.c) # Подключает общий сценарий и семейный production-порт.
target_link_libraries(test_stm32${family} PRIVATE protocan_boot) # Использует тот же автомат и CRC, что библиотека устройства.
target_include_directories(test_stm32${family} PRIVATE tests/stm32_stubs) # Только host-цели видят подменённые семейные заголовки HAL.
if(family STREQUAL "f103") # Один сценарий выбирает адреса и аппаратную единицу записи F103.
target_compile_definitions(test_stm32${family} PRIVATE TEST_STM32F103) # F407 использует вторую ветку того же сценария.
endif() # Завершает выбор семейного профиля теста.
if(family STREQUAL "g474") # G474 использует FDCAN, doubleword и обе физические геометрии DBANK.
target_compile_definitions(test_stm32${family} PRIVATE TEST_STM32G474) # Включает G474-модель и проверку старого shifted-DLC HAL.
endif() # Завершает выбор аппаратного профиля G474.
if(MSVC) # Assertions должны исполняться и в Release-конфигурации host-тестов.
target_compile_options(test_stm32${family} PRIVATE /UNDEBUG) # Отменяет NDEBUG, чтобы аппаратные проверки и вызовы не исчезли.
else() # GCC и Clang используют стандартную форму отмены определения макроса.
target_compile_options(test_stm32${family} PRIVATE -UNDEBUG) # Сохраняет проверки границ памяти при любой конфигурации сборки.
endif() # Завершает настройку обязательных assertions.
add_test(NAME stm32${family} COMMAND test_stm32${family}) # Регистрирует регрессию полного обновления и аппаратных отказов.
endforeach() # Завершает регистрацию трёх независимых STM32-портов.
endif() # Без host-тестов CMake по-прежнему собирает только переносимое ядро.

View File

@@ -10,12 +10,13 @@ CAN 2.0B и 29-битному ProtoCAN ID. Оно реализует сесси
CRC32, проверку совместимости, продолжение по номеру следующего блока и
безопасный выбор неактивного слота.
Это не готовый загрузчик конкретного STM32: внутри нет HAL, регистров Flash,
linker script, обработчиков прерываний, криптографии и перехода в приложение.
Эти операции предоставляет проект через таблицу callbacks.
Ядро не содержит HAL или регистров Flash. Готовые аппаратные callbacks для
STM32F103C8, STM32F407VE и STM32G474VE находятся в `ports`; подключение описано ниже.
Linker script, startup, тактирование, GPIO и переход в приложение предоставляет
проект устройства. Криптография в этих single-slot портах не реализована.
Для STM32G474VET6 из `candleLight_fw` задаются `slot_count = 1` и
`max_image_size = 480 * 1024`. В этом режиме запись идёт в единственную
Для [STM32G474VET6 из CANdle_stm32](ports/stm32g474/README.md) задаются
`slot_count = 1` и `max_image_size = 480 * 1024`. В этом режиме запись идёт в единственную
область приложения, callback `set_pending_slot` не требуется, а аппаратный
rollback недоступен. Значение `slot_count = 0` сохраняет прежний A/B-режим.
@@ -180,8 +181,37 @@ ctest --test-dir build --output-on-failure
## F28335 / большая EEPROM
[Порт M95M02](ports/tms320f28335/README.md) подключён в `tms_periph/periph_167`.
[Порт M95M02](ports/tms320f28335/README.md) подключён в `28335_periph`, ветка 167.
Использует отдельный Flash-загрузчик и два слота EEPROM по 130560 байт.
Реализованы readback/CRC, проверка SCI8, журнал метаданных, пробный запуск,
CONFIRM/откат, PING/discovery и сервисный паспорт. Первичная установка Flash-
загрузчика и выбор Flash boot описаны в README порта.
## STM32 / прошивка по ProtoCAN
| Порт | Источник профиля | Область приложения | Ёмкость |
|---|---|---|---:|
| [STM32F103C8](ports/stm32f103/README.md) | `KONOR_ds18b20/src/bootloader.c` | `0x08003000..0x08010000` | 52 КиБ |
| [STM32F407VE](ports/stm32f407/README.md) | `climate_control_f407vet6_f4/Bootloader/bootloader_f407.c` | `0x08008000..0x08040000` | 224 КиБ |
| [STM32G474VE](ports/stm32g474/README.md) | `CANdle_stm32/src/boot/g474_can_bootloader.c` | `0x08008000..0x08080000` | 480 КиБ |
Верхние границы исключены. F103/F407 предоставляют готовые CMSIS `boot_main.c`
для исходных проектов и отдельные reusable HAL-порты. G474 предоставляет
reusable HAL FDCAN/Flash-порт, CAN/UART boot main, startup и вход из приложения.
Общие компоненты и правила подключения описаны в
[`ports/stm32-common`](ports/stm32-common/README.md). Это ProtoCAN Boot v1 по
CAN, с отдельной картой памяти относительно USB SET v2-загрузчиков из `set-protocol`.
Конфигурация задаётся по именам полей и принудительно использует `slot_count=1`.
Образы пишутся в логический слот A; A/B-rollback и постоянные commit-метаданные
отсутствуют. Ограничения запуска после потери питания описаны в общей инструкции.
Проекты KONOR, climate и CANdle G474 подключены к этим исходникам через свои
submodule `templates`; в старых путях оставлены тонкие include-обёртки.
Состав сборок, разметка SRAM-запросов и результаты проверки перечислены в
[`projects/STM32.md`](projects/STM32.md).
## Самостоятельные проекты CCS
Готовые проекты **F28335_CAN_AB** (M95M02) и **F2812_CAN_NOR** (AM29LV800BT),
включая необходимые файлы поддержки TI, находятся в `projects/ccs`.
[Импорт, сборка, карты памяти и установка](projects/README.md).

View File

@@ -164,6 +164,9 @@ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
/** Call regularly with a wrapping monotonic millisecond clock. */
void pcan_boot_poll(pcan_boot_t *boot, uint32_t now_ms);
uint32_t pcan_boot_crc32_update(uint32_t crc, const pcan_boot_byte_t *data, size_t length); /* Общий потоковый CRC32 для readback аппаратных хранилищ; начальное состояние равно нулю. */
bool pcan_boot_report_busy(pcan_boot_t *boot, pcan_boot_byte_t command, uint32_t now_ms); /* Долгие аппаратные операции сообщают BUSY тем же codec и той же session, что обычный ответ. */
/** Переносимые операции с 29-битным ProtoCAN ID. */
uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
pcan_boot_byte_t device_type, pcan_boot_byte_t device_id,

View File

@@ -0,0 +1,109 @@
# STM32: общий слой ProtoCAN Boot v1
Порты [F103C8](../stm32f103/README.md), [F407VE](../stm32f407/README.md) и
[G474VE](../stm32g474/README.md) подключают bxCAN/FDCAN и Flash к текущему `pcan_boot`.
F103/F407 предоставляют как лёгкий CMSIS boot main для существующих проектов,
так и независимый HAL-порт. G474 использует HAL FDCAN/Flash.
`pcan_boot_stm32.inc` включается семейным `.c`; отдельно его компилировать не надо.
Сохраните рядом каталоги `stm32-common` и нужного семейства.
Reusable `pcan_boot_stm32*.c` не содержит `main` или startup. Готовые `boot_main.c`
и вход из приложения находятся в семейных каталогах. Для F103/F407 общая
CMSIS-реализация транспорта, callbacks и безопасного handoff — `boot_native.inc`;
G474 также содержит `boot_startup.c` для исходных Keil/GCC-сборок.
Тактирование платы, linker и привязка USB/CAN-приложения остаются в проектах.
KONOR, climate и CANdle G474 уже подключены через свои submodule: локальные
boot main/control заменены include-обёртками, состав Keil/CMake обновлён.
UART использует общий `rs485-boot` с сохранением SETGUI v1 wire-протокола.
[Схема подключённых проектов и проверки сборок](../../projects/STM32.md).
## Подключение reusable HAL-портов F103/F407
1. Добавьте в сборку `src/pcan_boot.c` и семейный `pcan_boot_stm32f103.c`
либо `pcan_boot_stm32f407.c`; в include paths — `include`, каталог порта,
CMSIS, HAL и каталог вашего `stm32f1xx_hal_conf.h` / `stm32f4xx_hal_conf.h`.
2. В HAL включите CAN, FLASH, RCC, GPIO и CORTEX. Выполните `HAL_Init`, настройте
тактирование, SysTick и GPIO, вызовите `HAL_CAN_Init` для CAN1. Настройка скорости
шины принадлежит проекту; исходные устройства используют 500 кбит/с.
3. Выделите CAN1, FIFO0 и банки фильтров 0/1 загрузчику. Перед вызовом init CAN
должен находиться в состоянии READY, без чужих активных фильтров. Init настраивает
адресный фильтр и broadcast discovery, затем вызывает `HAL_CAN_Start`.
Оба приоритета ProtoCAN разрешены, standard и remote кадры отвергаются.
4. Не включайте CAN TX/RX IRQ для этого handle и не отправляйте через него кадры
из других контекстов: порт использует опрос и сам читает TXOK до reset.
5. Вызывайте семейный `poll` из основного цикла только после успешного init.
Стирание Flash выполняется синхронно; вызов из IRQ не допускается.
6. Linker приложения, VTOR и переход после reset должны соответствовать карте
выбранного профиля. Изменение адресов требует отдельного согласования
геометрии стирания, linker и host-клиента.
Пример для F103; `hcan` уже инициализирован проектом:
```c
#include "pcan_boot_stm32f103.h" /* Подключает HAL-порт и текущий API общего автомата. */
static pcan_boot_stm32_t updater; /* Сохраняет состояние между итерациями главного цикла. */
static bool start_updater(CAN_HandleTypeDef *hcan) /* Вызывается после настройки CAN1, clock tree, GPIO и SysTick. */
{ /* Настройка идентичности прибора не зависит от аппаратной геометрии Flash. */
pcan_boot_config_t config = {0}; /* Не зависит от порядка полей расширяемого API. */
config.device_type = 7U; /* Пример: тип 7 должен совпадать с выбранным адресом в клиенте прошивки. */
config.device_id = 13U; /* Пример: номер 13 повторяет исходный адрес загрузчика KONOR. */
config.ack_window = 16U; /* Подтверждает каждые 16 CAN-блоков по восемь байт. */
config.session_timeout_ms = 30000U; /* Пример: 30 секунд без активности прерывают RAM-сессию. */
return pcan_boot_stm32f103_init(&updater, hcan, &config); /* Настраивает single-slot геометрию, фильтры 0/1 и запускает CAN. */
} /* После успеха основной цикл регулярно вызывает pcan_boot_stm32f103_poll(&updater). */
```
Product/HW/FW в примере равны нулю; для изделия задайте их явно и согласуйте
с manifest клиента. Порт сохраняет `ack_window` и `session_timeout_ms`, но сам
задаёт `active_slot=0`, `slot_count=1`, `max_image_size` по физическому профилю.
Конфигурацию `config` можно создавать на стеке; состояние `updater` и `hcan`
должно существовать всё время работы загрузчика.
## Flash, подтверждения и reset
- Запись последовательная, блоки по восемь байт; неполная аппаратная единица
допустима только в конце образа и дополняется `FF`. CRC учитывает полезную длину.
- HAL-ошибки и readback каждого слова проверяются; после erase/program Flash
снова блокируется, включая обработанные отказы. На F407 перед readback
вызывается `FLASH_FlushCaches`, чтобы соседние записанные слова не читались
из устаревшей строки Flash-кэша; состояние включения кэшей сохраняется HAL.
- VERIFY перечитывает Flash, сравнивает её CRC и проверяет MSP/Thumb reset vector.
Точка входа должна принадлежать фактическому образу, MSP — основной SRAM,
с выравниванием восемь байт. CCM F407 в данном профиле для стека не используется.
- COMMIT повторно проверяет образ и устанавливает признак в RAM.
REBOOT разрешён только в состоянии COMMITTED. Ответ перед reset должен
завершиться с TXOK; отсутствие mailbox, ошибка шины или тайм-аут 100 мс
не вызывают reset. Для этого HAL tick должен работать при ожидании TX.
- BUSY перед стиранием, между секторами F407 и после erase сохраняет активность
сессии. На однобанковой Flash код не исполняется во время аппаратного стирания;
непрерывные ответы в этот интервал не гарантируются. Настройте тайм-аут клиента
больше максимального времени стирания одной аппаратной операции.
## Запуск приложения и потеря питания
Порт не меняет Option Bytes, защиту секторов и VDD; F407 требует 2,7..3,6 В
для программирования x32. Переход из работающего приложения в загрузчик,
backup/SRAM-флаг запроса, отключение периферии и безопасная замена MSP
остаются в `main` и startup устройства.
`*_vectors_valid()` проверяет только векторы и диапазоны. После внешнего reset
этого недостаточно для доказательства целостности образа: RAM-признак COMMIT
потерян, а частично записанное приложение уже может иметь корректные векторы.
Эти профили сохраняют исходную single-slot модель без постоянных метаданных,
rollback, resume после питания и аутентификации. Для защищённого запуска
проекту нужен отдельный постоянный manifest с полной проверкой образа.
## Проверка
`python tests/run_tests.py` из корня `protocan-boot` запускает три production-порта
с моделью HAL и Flash, наряду с существующими проверками ядра и TI-портов.
Можно также использовать обычные CMake/CTest: цели `test_stm32f103`,
`test_stm32f407`, `test_stm32g474` работают на Windows и POSIX.
Проверяются все хвосты CAN-блока, полный ERASE/DATA/VERIFY/COMMIT/REBOOT,
повреждение readback, отказы HAL/TX, фильтрация RX и сохранность памяти
перед приложением и за верхней границей; для G474 проверяются оба DBANK-режима,
переход между банками, запрет swap, ёмкость Flash и старое shifted-DLC HAL.
Эти тесты не заменяют проверку
на реальной плате с её linker, питанием, трансивером и SysTick.

View File

@@ -0,0 +1,127 @@
static __attribute__((naked, noreturn)) void native_enter(uint32_t sp __attribute__((unused)), uint32_t pc __attribute__((unused))) /* Меняет стек без последующего обращения C-кода к старому кадру функции. */
{ /* AAPCS передаёт новый MSP в r0, Thumb entry в r1; последовательность совместима с GCC и Arm Compiler 6. */
__asm volatile("msr msp, r0\n" "movs r2, #0\n" "msr control, r2\n" "dsb\n" "isb\n" "cpsie i\n" "bx r1\n"); /* Возвращает привилегированный режим MSP, синхронизирует конвейер и передаёт управление Thumb-вектору. */
} /* Завершает переход без эпилога C после замены MSP. */
void SysTick_Handler(void) { ++ticks_ms; } /* Сохраняет миллисекундные часы только в самостоятельной сборке загрузчика. */
static bool native_vectors_valid(uint32_t size) /* Проверяет начальный стек и reset handler в пределах фактического образа. */
{ /* До чтения Flash отвергает некорректный размер. */
const uint32_t *vectors = (const uint32_t *)(uintptr_t)NATIVE_APP_BASE; /* Таблица векторов лежит в начале зарезервированной области приложения. */
uint32_t sp, pc; /* Хранит начальные значения Cortex-M без невыровненного доступа. */
if (size < 8U || size > NATIVE_APP_SIZE) return false; /* Для двух векторов нужно минимум восемь байт; верхняя граница профиля исключена. */
sp = vectors[0]; pc = vectors[1]; /* Читает MSP и Thumb-точку входа только после проверки геометрии. */
return sp > 0x20000000UL && sp <= NATIVE_RAM_END && (sp & 7U) == 0U && (pc & 1U) != 0U && (pc & ~1UL) >= NATIVE_APP_BASE && (pc & ~1UL) < NATIVE_APP_BASE + size; /* Требует восемь байт выравнивания стека и точку входа внутри выбранной длины приложения. */
} /* Структурная проверка не заменяет постоянный manifest после отключения питания. */
static void native_jump(uint32_t address) /* Отключает загрузочную периферию перед запуском приложения либо ROM. */
{ /* Вызывающий код предварительно проверяет векторы либо использует фиксированный заводской ROM. */
uint32_t index; /* Перебирает все банки NVIC Cortex-M3/M4 с запасом до восьми банков. */
const uint32_t *vectors = (const uint32_t *)(uintptr_t)address; /* Сохраняет адрес новой таблицы до отключения прерываний. */
uint32_t sp = vectors[0], pc = vectors[1]; /* Считывает аргументы перехода, пока C-стек ещё принадлежит загрузчику. */
__disable_irq(); SysTick->CTRL = 0U; /* Не позволяет SysTick загрузчика вызвать обработчик нового приложения в промежуточном состоянии. */
native_peripheral_stop(); /* Семейный порт отключает CAN и используемый UART, не трогая резервную память. */
for (index = 0U; index < 8U; ++index) { NVIC->ICER[index] = 0xFFFFFFFFUL; NVIC->ICPR[index] = 0xFFFFFFFFUL; } /* Сбрасывает enable/pending всех внешних IRQ записью W1C по CMSIS. */
SCB->ICSR = SCB_ICSR_PENDSTCLR_Msk | SCB_ICSR_PENDSVCLR_Msk; /* Убирает отложенные SysTick и PendSV до смены VTOR. */
SCB->VTOR = address; /* Согласует векторизацию с linker нового образа. */
native_enter(sp, pc); /* Передаёт управление без возврата и без обращения к старому C-стеку. */
} /* Завершает общую процедуру handoff. */
static bool native_image_valid(uint32_t size, uint32_t crc) /* Проверяет реальную Flash независимо от CRC принятого CAN/UART-потока. */
{ /* Одно правило проверки используется обоими транспортами KONOR. */
return native_vectors_valid(size) && pcan_boot_crc32_update(0U, (const uint8_t *)(uintptr_t)NATIVE_APP_BASE, size) == crc; /* Проверяет точную полезную длину, исключая заполнение FF последней аппаратной единицы. */
} /* Завершает проверку векторов и readback CRC. */
static bool native_write_bytes(uint32_t image_size, uint32_t offset, const uint8_t *data, uint16_t length) /* Записывает CAN-блок или UART-часть с одной проверкой физической геометрии. */
{ /* Неполная аппаратная единица разрешена только в самом конце образа. */
uint32_t done = 0U; bool ok = true; /* Считает полезные байты и сохраняет результат до восстановления блокировки Flash. */
if (image_size < 8U || image_size > NATIVE_APP_SIZE || data == NULL || length == 0U || offset > image_size || length > image_size - offset || offset % NATIVE_PROGRAM_UNIT != 0U) return false; /* Проверяет диапазоны вычитанием, исключая переполнение и неправильное выравнивание. */
if (length % NATIVE_PROGRAM_UNIT != 0U && offset + length != image_size) return false; /* Не допускает повторного программирования уже дополненного слова в середине потока. */
if (!native_unlock()) return false; /* Не изменяет заблокированную или занятую Flash. */
while (done < length) { /* Делит полезные октеты на аппаратные полуслова F1 или слова F4. */
uint32_t word = 0xFFFFFFFFUL, count = length - done; /* По умолчанию хвост аппаратной записи остаётся стёртым FF. */
if (count > NATIVE_PROGRAM_UNIT) count = NATIVE_PROGRAM_UNIT; /* Не копирует больше одной аппаратной единицы за итерацию. */
(void)memcpy(&word, data + done, count); /* Не читает за фактическим хвостом последнего блока. */
if (!native_program(NATIVE_APP_BASE + offset + done, word)) { ok = false; break; } /* Проверяет аппаратные флаги и фактический readback каждого записанного слова. */
done += count; /* Продвигается только на число полезных байтов без дополнения. */
} /* Завершает программирование текущей части образа. */
native_lock(); /* Восстанавливает защиту Flash как после успеха, так и после обработанной ошибки. */
return ok; /* Передаёт отказ общему CAN либо UART-автомату. */
} /* Завершает общую безопасную запись Flash. */
static bool native_send(void *user, uint32_t id, const pcan_boot_byte_t *data, pcan_boot_byte_t length) /* Передаёт служебный CAN-ответ с ожиданием реального TXOK до reset. */
{ /* Использует только mailbox 0 выделенного загрузочного CAN1. */
uint32_t start = ticks_ms, low = 0U, high = 0U; uint8_t index; bool sent; /* Сохраняет начало 100-мс тайм-аута и регистровые слова payload. */
(void)user; /* Самостоятельный загрузчик содержит один статический экземпляр CAN. */
if (length > 8U || id > 0x1FFFFFFFUL || (length != 0U && data == NULL)) return false; /* Не допускает чтение за DLC или передачу идентификатора шире 29 бит. */
while ((CAN1->TSR & CAN_TSR_TME0) == 0U) if ((uint32_t)(ticks_ms - start) >= 100U) return false; /* Ограничивает ожидание свободного mailbox 100 миллисекундами с учётом переполнения часов. */
for (index = 0U; index < length; ++index) { if (index < 4U) low |= (uint32_t)data[index] << (index * 8U); else high |= (uint32_t)data[index] << ((index - 4U) * 8U); } /* Читает только фактические октеты DLC, оставляя остальные позиции нулевыми. */
CAN1->TSR = CAN_TSR_RQCP0; /* Сбрасывает прежние TXOK/ошибки mailbox 0 записью W1C. */
CAN1->sTxMailBox[0].TDTR = length; CAN1->sTxMailBox[0].TDLR = low; CAN1->sTxMailBox[0].TDHR = high; /* Загружает длину и два безопасно сформированных слова данных. */
CAN1->sTxMailBox[0].TIR = (id << 3U) | CAN_TI0R_IDE | CAN_TI0R_TXRQ; /* Запускает расширенный data-кадр ProtoCAN без remote-флага. */
while ((CAN1->TSR & CAN_TSR_RQCP0) == 0U) if ((uint32_t)(ticks_ms - start) >= 100U) { CAN1->TSR = CAN_TSR_ABRQ0; return false; } /* Прерывает неподтверждённую передачу и запрещает reset после тайм-аута. */
sent = (CAN1->TSR & CAN_TSR_TXOK0) != 0U; CAN1->TSR = CAN_TSR_RQCP0; /* Проверяет ACK шины до очистки аппаратного результата. */
return sent; /* Ядро перезапустит MCU только после успешно отправленного ответа. */
} /* Завершает синхронную передачу ответа CAN. */
static bool native_erase_slot(void *user, pcan_boot_byte_t slot) /* Подключает проверенное стирание профиля к текущему ProtoCAN API. */
{ /* Аппаратная реализация семейства никогда не затрагивает загрузчик или EEPROM-кольцо. */
bool ok; (void)user; /* Хранит результат до финального обновления времени сессии. */
if (slot != 0U || boot.manifest.image_size < 8U || boot.manifest.image_size > NATIVE_APP_SIZE) return false; /* Разрешает только логический слот A и полный набор векторов. */
(void)pcan_boot_report_busy(&boot, PCAN_BOOT_CMD_ERASE, ticks_ms); /* Предупреждает клиента перед синхронным Flash stall. */
ok = native_erase(boot.manifest.image_size); /* Стирает только реально необходимые страницы или секторы. */
if (ok) (void)pcan_boot_report_busy(&boot, PCAN_BOOT_CMD_ERASE, ticks_ms); /* Продлевает сессию после завершения длительной аппаратной операции. */
return ok; /* Сообщает результат стирания общему автомату. */
} /* Завершает callback стирания CAN. */
static bool native_write_slot(void *user, pcan_boot_byte_t slot, uint32_t offset, const pcan_boot_byte_t *data, pcan_boot_byte_t length) /* Подключает единый Flash writer к последовательным блокам CAN. */
{ /* Последовательность номеров блоков и максимум DLC уже проверены общим автоматом. */
(void)user; /* Единственный образ управляется статическим экземпляром загрузчика. */
return slot == 0U && offset == boot.bytes_received && native_write_bytes(boot.manifest.image_size, offset, data, length); /* Не допускает произвольное смещение даже при прямом вызове callback. */
} /* Завершает callback записи CAN. */
static pcan_boot_status_t native_verify(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest) /* Различает повреждение реальной Flash и неверную таблицу векторов. */
{ /* Операция не доверяет одному CRC входящего потока. */
(void)user; /* Использует фиксированную область выбранного семейного профиля. */
if (slot != 0U || manifest == NULL || !native_vectors_valid(manifest->image_size)) return PCAN_BOOT_STATUS_INVALID_SIZE; /* Отвергает неверный слот, геометрию и точку входа до чтения всего образа. */
return native_image_valid(manifest->image_size, manifest->image_crc32) ? PCAN_BOOT_STATUS_OK : PCAN_BOOT_STATUS_CRC_ERROR; /* Перечитывает Flash общим CRC32 ProtoCAN. */
} /* Завершает независимую проверку принятого образа. */
static bool native_commit(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest) { return native_verify(user, slot, manifest) == PCAN_BOOT_STATUS_OK; } /* COMMIT повторно проверяет single-slot образ без имитации постоянных метаданных. */
static bool native_can_reboot(void *user) { (void)user; return boot.state == PCAN_BOOT_STATE_COMMITTED; } /* Протокольный reset разрешён только после VERIFY и COMMIT. */
static void native_reboot(void *user) { (void)user; NVIC_SystemReset(); } /* Вызывается ядром только после успешного TXOK ответа REBOOT. */
static bool native_init(void) /* Инициализирует текущий API ядра без позиционных структур старой версии. */
{ /* Идентичность устройства задаёт профиль либо конфигурация проекта. */
pcan_boot_config_t config = {0}; pcan_boot_port_t callbacks = {0}; /* Обнуляет необязательные поля и callbacks расширяемого интерфейса. */
config.device_type = PCAN_BOOT_DEVICE_TYPE; config.device_id = PCAN_BOOT_DEVICE_ID; config.product_type = PCAN_BOOT_PRODUCT_TYPE; /* Сохраняет адрес и Product исходного проекта. */
config.hardware_revision = PCAN_BOOT_HW_REVISION; config.firmware_version = PCAN_BOOT_FW_VERSION; /* Сохраняет совместимость manifest по HW/FW. */
config.active_slot = 0U; config.slot_count = 1U; config.max_image_size = NATIVE_APP_SIZE; /* Явно использует single-slot A вместо прежней фиктивной A/B-конфигурации. */
config.ack_window = PCAN_BOOT_ACK_WINDOW; config.session_timeout_ms = 30000UL; /* Сохраняет исходное окно ACK и прерывает неактивную RAM-сессию через 30 секунд. */
callbacks.send = native_send; callbacks.erase_slot = native_erase_slot; callbacks.write_slot = native_write_slot; /* Подключает аппаратный транспорт и защищённую область Flash. */
callbacks.verify_status = native_verify; callbacks.set_pending_slot = native_commit; callbacks.can_reboot = native_can_reboot; callbacks.reboot = native_reboot; /* Подключает readback, COMMIT и согласованную политику reset. */
return pcan_boot_init(&boot, &config, &callbacks, NULL); /* Проверяет конфигурацию и создаёт единственный экземпляр общего автомата. */
} /* Завершает настройку ProtoCAN независимо от включения аппаратной шины. */
static bool native_can_init(void) /* Настраивает выделенный CAN1 и два фильтра для адресных запросов и discovery. */
{ /* Тактирование GPIO и CAN включает семейная функция; UART во время Flash/CAN init не изменяется. */
static const uint8_t shifts[8] = {0U, 0U, 0U, 0U, 1U, 2U, 3U, 4U}; /* RCC кодирует APB1: значения 0..3 без деления, 4..7 дают делители 2,4,8,16. */
uint32_t clock = SystemCoreClock >> shifts[(RCC->CFGR >> NATIVE_PPRE1_SHIFT) & 7U]; /* Получает действительную частоту APB1 в Гц, без удвоения timer clocks. */
uint32_t tq, btr = 0U, started, index; bool found = false; /* Сохраняет bit timing, тайм-аут init и индекс фильтра. */
if (clock % PCAN_BOOT_CAN_BITRATE != 0U) return false; /* Не округляет скорость шины при неподходящем clock tree. */
for (tq = 18U; tq >= 8U; --tq) { /* Сохраняет диапазон 8..18 time quanta из исходного KONOR. */
uint32_t divisor = clock / PCAN_BOOT_CAN_BITRATE, prescaler = divisor / tq, ts2 = tq / 5U; /* Выбирает sample point около 80% при допустимом целочисленном делителе. */
if (divisor % tq == 0U && prescaler >= 1U && prescaler <= 1024U) { btr = ((ts2 - 1U) << 20U) | ((tq - 2U - ts2) << 16U) | (prescaler - 1U); found = true; break; } /* Кодирует BRP/TS1/TS2 с аппаратным смещением минус один и SJW=1 tq. */
} /* Завершает поиск точной скорости для штатного тактирования F103/F407. */
if (!found) return false; /* Не включает шину с неизвестными битовыми интервалами. */
native_gpio_init(); CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_ABOM; started = ticks_ms; /* Включает семейные GPIO и запрашивает init с автоматическим восстановлением bus-off. */
while ((CAN1->MSR & CAN_MSR_INAK) == 0U) if ((uint32_t)(ticks_ms - started) >= 100U) return false; /* Ограничивает вход в CAN init ста миллисекундами. */
CAN1->BTR = btr; CAN1->FMR |= CAN_FMR_FINIT; CAN1->FA1R = 0U; CAN1->FS1R |= 3U; CAN1->FM1R &= ~3UL; CAN1->FFA1R &= ~3UL; /* Настраивает только два 32-битных mask-фильтра в FIFO0 и отключает чужие банки загрузчика. */
for (index = 0U; index < 2U; ++index) { /* Банк 0 принимает собственный адрес, банк 1 — только broadcast discovery. */
uint32_t id = index == 0U ? pcan_boot_make_id(0U, 0U, PCAN_BOOT_DEVICE_TYPE, PCAN_BOOT_DEVICE_ID, 0U, 0U) : pcan_boot_make_id(0U, 0U, 7U, 15U, PCAN_BOOT_MSG_DISCOVERY, 0U); /* Не маскирует приоритет, сохраняя оба штатных уровня ProtoCAN. */
uint32_t mask = index == 0U ? 0x0FF00000UL : 0x0FFFFFFFUL; /* Адресный фильтр оставляет MsgType/Body открытыми; discovery сравнивает их точно. */
CAN1->sFilterRegister[index].FR1 = (id << 3U) | CAN_RI0R_IDE; CAN1->sFilterRegister[index].FR2 = (mask << 3U) | CAN_RI0R_IDE | CAN_RI0R_RTR; /* Исключает standard и remote кадры на аппаратном уровне. */
} /* Завершает настройку обоих фильтров. */
CAN1->FA1R |= 3U; CAN1->FMR &= ~CAN_FMR_FINIT; CAN1->MCR &= ~CAN_MCR_INRQ; started = ticks_ms; /* Активирует фильтры и переводит CAN в рабочий режим. */
while ((CAN1->MSR & CAN_MSR_INAK) != 0U) if ((uint32_t)(ticks_ms - started) >= 100U) return false; /* Отсутствие доступной шины не должно навсегда блокировать обычный запуск приложения. */
return true; /* Рабочий CAN готов принимать адресные команды и discovery. */
} /* Завершает общую аппаратную настройку bxCAN. */
static void native_can_poll(void) /* Обрабатывает не больше трёх кадров — аппаратной глубины FIFO bxCAN. */
{ /* Длительные операции Flash выполняются только из основного цикла. */
uint32_t count; pcan_boot_poll(&boot, ticks_ms); /* Обновляет тайм-аут сессии и отложенное discovery независимо от входящих команд. */
for (count = 0U; count < 3U && (CAN1->RF0R & CAN_RF0R_FMP0) != 0U; ++count) { /* Ограничивает время одного вызова на нагруженной шине. */
uint32_t rir = CAN1->sFIFOMailBox[0].RIR, low = CAN1->sFIFOMailBox[0].RDLR, high = CAN1->sFIFOMailBox[0].RDHR; /* Сохраняет поля до освобождения аппаратного FIFO. */
uint8_t dlc = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 15U), data[8], index; /* Читает входящий DLC отдельно от фиксированного буфера classic CAN. */
for (index = 0U; index < 8U; ++index) data[index] = (uint8_t)(index < 4U ? low >> (index * 8U) : high >> ((index - 4U) * 8U)); /* Извлекает октеты без невыровненного чтения аппаратных регистров. */
CAN1->RF0R |= CAN_RF0R_RFOM0; /* Освобождает обработанный элемент FIFO0 до потенциально долгого erase. */
if ((rir & CAN_RI0R_IDE) != 0U && (rir & CAN_RI0R_RTR) == 0U && dlc <= 8U) { pcan_boot_poll(&boot, ticks_ms); (void)pcan_boot_process(&boot, (rir >> 3U) & 0x1FFFFFFFUL, data, dlc); } /* Повторно проверяет формат внешнего кадра и вызывает общий автомат. */
} /* Завершает ограниченный пакет входящих кадров. */
} /* Завершает обслуживание транспорта и часов ядра. */

View File

@@ -0,0 +1,10 @@
#ifndef PCAN_BOOT_STM32_H /* Защищает общие объявления двух STM32-портов от повторного включения. */
#define PCAN_BOOT_STM32_H /* Фиксирует включение интерфейса single-slot загрузчика. */
#include "../../include/pcan_boot.h" /* Использует актуальные именованные поля конфигурации и callbacks ядра. */
typedef struct { /* Состояние принадлежит одному загрузчику и одному выделенному bxCAN. */
pcan_boot_t boot; /* Хранит сессию, manifest, CRC потока и состояние общего автомата. */
void *can; /* Ссылается на CAN_HandleTypeDef bxCAN либо FDCAN_HandleTypeDef G474; время жизни задаёт проект. */
uint32_t written; /* Следующее допустимое смещение записи, в байтах от начала приложения. */
bool ready; /* Разрешает reset только после проверки Flash и успешного COMMIT текущего образа. */
} pcan_boot_stm32_t; /* Не содержит startup, main или обработчиков прерываний проекта. */
#endif /* Завершает общий интерфейс STM32F103/F407. */

View File

@@ -0,0 +1,167 @@
static bool send_frame(void *user, uint32_t id, const pcan_boot_byte_t *data, pcan_boot_byte_t dlc) /* Передаёт только объявленные октеты и дожидается физического завершения ACK. */
{ /* Порт владеет CAN1 без TX IRQ, поэтому флаги завершения не очищаются чужим обработчиком. */
pcan_boot_stm32_t *port = user; /* Получает состояние выделенного аппаратного порта. */
CAN_HandleTypeDef *can = port->can; /* Восстанавливает тип handle семейства HAL текущей единицы трансляции. */
CAN_TxHeaderTypeDef header = {0}; /* Не оставляет мусора в стандартном ID и timestamp-флагах. */
uint8_t bytes[8] = {0}; /* CAN 2.0 передаёт не больше восьми октетов; короткий payload дополняется безопасными нулями. */
uint32_t mailbox, mask, started = HAL_GetTick(); /* Запоминает mailbox, его TXOK-флаг и начало тайм-аута в мс. */
if (dlc > 8U || id > 0x1FFFFFFFUL || (dlc != 0U && data == NULL)) return false; /* Запрещает чтение за payload и идентификатор шире 29 бит. */
if (dlc != 0U) (void)memcpy(bytes, data, dlc); /* Не читает отсутствующие байты коротких служебных сообщений. */
while (HAL_CAN_GetTxMailboxesFreeLevel(can) == 0U) { /* Ожидает освобождения аппаратного передатчика. */
if ((uint32_t)(HAL_GetTick() - started) >= 100U) return false; /* Ограничивает ожидание 100 мс, включая переполнение счётчика времени. */
} /* Завершает ожидание свободного mailbox. */
header.ExtId = id; /* Передаёт адресный ProtoCAN ID без преобразования полей. */
header.IDE = CAN_ID_EXT; /* ProtoCAN использует только расширенные 29-битные идентификаторы. */
header.RTR = CAN_RTR_DATA; /* Все ответы загрузчика содержат обычные данные, а не remote-запрос. */
header.DLC = dlc; /* Фиксирует фактическое число объявленных октетов кадра. */
can->Instance->TSR = CAN_TSR_RQCP0 | CAN_TSR_RQCP1 | CAN_TSR_RQCP2; /* Очищает прежние TXOK/ошибки записью W1C перед новой отправкой; CAN принадлежит только этому порту. */
if (HAL_CAN_AddTxMessage(can, &header, bytes, &mailbox) != HAL_OK) return false; /* Не считает не поставленный в очередь ответ отправленным. */
while (HAL_CAN_IsTxMessagePending(can, mailbox) != 0U) { /* Сохраняет CAN до завершения ответа, включая команду REBOOT. */
if ((uint32_t)(HAL_GetTick() - started) >= 100U) { /* Не зависает навсегда при отсутствии ACK от шины. */
(void)HAL_CAN_AbortTxRequest(can, mailbox); /* Снимает просроченный запрос передачи перед возвратом ошибки. */
return false; /* Ядро не выполнит reset после неподтверждённого ответа. */
} /* Завершает обработку тайм-аута TX. */
} /* Аппаратная передача завершилась либо была отвергнута CAN. */
mask = mailbox == CAN_TX_MAILBOX0 ? CAN_TSR_TXOK0 : mailbox == CAN_TX_MAILBOX1 ? CAN_TSR_TXOK1 : CAN_TSR_TXOK2; /* Сопоставляет bitmask mailbox HAL с флагом успешной передачи bxCAN. */
return (can->Instance->TSR & mask) != 0U; /* Отличает подтверждённый ACK от снятия запроса вследствие ошибки. */
} /* Завершает синхронный транспорт служебных ответов. */
bool STM32_BOOT_VECTORS(void) /* Проверяет только структурную корректность таблицы векторов установленного приложения. */
{ /* Проверка CRC образа выполняется отдельно при VERIFY. */
const uint32_t *vectors = (const uint32_t *)(uintptr_t)STM32_BOOT_BASE; /* Читает первые два слова из фиксированной области приложения. */
uint32_t sp = vectors[0], pc = vectors[1]; /* Извлекает начальный стек и Thumb-адрес reset handler. */
return sp > 0x20000000UL && sp <= STM32_BOOT_RAM_END && (sp & 7U) == 0U && (pc & 1U) != 0U && (pc & ~1UL) >= STM32_BOOT_BASE && (pc & ~1UL) < STM32_BOOT_END; /* Требует доступный стек с выравниванием 8 байт и точку входа внутри приложения. */
} /* Завершает проверку векторов без изменения MCU. */
static bool erase_slot(void *user, pcan_boot_byte_t slot) /* Стирает единственную область после проверки manifest общим ядром. */
{ /* Сохранение старого приложения и rollback в этом профиле отсутствуют. */
pcan_boot_stm32_t *port = user; /* Получает manifest и состояние активации текущего загрузчика. */
uint32_t size = port->boot.manifest.image_size; /* Ограничивает стирание фактической длиной образа в байтах. */
bool ok, locked; /* Сохраняет результаты операции и последующего восстановления защиты Flash. */
if (slot != 0U || size < 8U || size > STM32_BOOT_END - STM32_BOOT_BASE) return false; /* Не допускает чужой слот, неполные векторы и выход за область приложения. */
port->ready = false; /* Инвалидирует разрешение reset до первого изменения Flash. */
port->written = 0U; /* Начинает последовательную запись заново после стирания. */
(void)pcan_boot_report_busy(&port->boot, PCAN_BOOT_CMD_ERASE, HAL_GetTick()); /* Предупреждает клиента перед синхронным блокированием Flash. */
if (HAL_FLASH_Unlock() != HAL_OK) return false; /* Не продолжает работу при отказе разблокировки Flash. */
ok = erase_region(port, size); /* Выполняет семейную операцию по страницам F1 либо секторам F4. */
locked = HAL_FLASH_Lock() == HAL_OK; /* Восстанавливает защиту Flash и при успехе, и при отказе стирания. */
if (ok && locked) (void)pcan_boot_report_busy(&port->boot, PCAN_BOOT_CMD_ERASE, HAL_GetTick()); /* Обновляет время после блокировки Flash, чтобы длительное стирание не погасило сессию. */
return ok && locked; /* Не подтверждает ERASE при любом аппаратном отказе. */
} /* Завершает callback стирания. */
static bool write_slot(void *user, pcan_boot_byte_t slot, uint32_t offset, const pcan_boot_byte_t *data, pcan_boot_byte_t length) /* Программирует последовательные блоки без пересечения защищённых областей. */
{ /* Крайний неполный блок дополняется единицами до аппаратной единицы программирования. */
pcan_boot_stm32_t *port = user; /* Получает длину manifest и ожидаемое следующее смещение. */
uint32_t done = 0U, size = port->boot.manifest.image_size; /* Считает только полезные байты без дополнения FF. */
bool ok = true, locked; /* Позволяет гарантированно закрыть Flash после любой ошибки. */
if (slot != 0U || data == NULL || length == 0U || length > 8U || size < 8U || size > STM32_BOOT_END - STM32_BOOT_BASE || offset != port->written || offset > size || length > size - offset || (offset % STM32_BOOT_UNIT) != 0U) return false; /* Проверяет адреса до сложения, выравнивание и принадлежность текущему образу. */
if ((length % STM32_BOOT_UNIT) != 0U && offset + length != size) return false; /* Дополнение FF допустимо только у последнего блока; повторная запись слова запрещена. */
if (HAL_FLASH_Unlock() != HAL_OK) return false; /* Не выполняет запись при заблокированной Flash. */
while (done < length) { /* Разбивает CAN-блок на аппаратные полуслова F1 либо слова F4. */
uint32_t value = 0xFFFFFFFFUL, portion = length - done; /* Стертое значение FF заполняет только хвост последней аппаратной записи. */
uint32_t address = STM32_BOOT_BASE + offset + done; /* Адрес остаётся в заранее проверенной области приложения. */
HAL_StatusTypeDef status; /* Отделяет аппаратное завершение записи от последующей синхронизации Flash-кэша. */
if (portion > STM32_BOOT_UNIT) portion = STM32_BOOT_UNIT; /* Ограничивает копирование размером аппаратной записи. */
(void)memcpy(&value, data + done, portion); /* Не читает за фактическим payload последнего блока. */
status = HAL_FLASH_Program(STM32_BOOT_PROGRAM, address, value); /* Дожидается завершения программирования выбранной аппаратной единицы. */
STM32_BOOT_SYNC(); /* На F407 сбрасывает строки Flash-кэша после записи, чтобы readback соседнего слова не получил старое FF. */
if (status != HAL_OK || memcmp((const void *)(uintptr_t)address, &value, STM32_BOOT_UNIT) != 0) { /* Проверяет результат HAL и чтение записанного значения, включая хвост FF. */
ok = false; /* Делает частично записанный образ непригодным для VERIFY/COMMIT. */
break; /* Не продолжает программирование после первого отказа. */
} /* Завершает проверку результата программирования. */
done += portion; /* Продвигает полезное смещение без включения дополнения FF в CRC. */
} /* Завершает аппаратные записи текущего CAN-блока. */
locked = HAL_FLASH_Lock() == HAL_OK; /* Обязательно восстанавливает аппаратную блокировку Flash. */
if (ok && locked) port->written += length; /* Подтверждает смещение только после полного успеха и блокировки. */
return ok && locked; /* Ошибка передаётся общему автомату как FLASH_ERROR. */
} /* Завершает callback последовательной записи. */
static pcan_boot_status_t verify_image(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest) /* Перечитывает реальную Flash вместо доверия одному CRC входящего потока. */
{ /* Проверяет геометрию до доступа к памяти и точный размер переданных данных. */
pcan_boot_stm32_t *port = user; /* Получает фактический счётчик успешно записанных байтов. */
const uint8_t *image = (const uint8_t *)(uintptr_t)STM32_BOOT_BASE; /* Отображает фиксированную область приложения в адресное пространство MCU. */
uint32_t offset, crc = 0U; /* Начинает общий потоковый CRC32 с нуля. */
if (slot != 0U || manifest == NULL || manifest->image_size < 8U || manifest->image_size > STM32_BOOT_END - STM32_BOOT_BASE || port->written != manifest->image_size) return PCAN_BOOT_STATUS_INVALID_SIZE; /* Не читает произвольную память по недостоверному manifest. */
for (offset = 0U; offset < manifest->image_size;) { /* Проверяет весь полезный образ ограниченными порциями. */
uint32_t count = manifest->image_size - offset; /* Вычисляет остаток в байтах без выхода за границу образа. */
if (count > 1024U) count = 1024U; /* Порция 1 КиБ ограничивает объём одного вызова потокового CRC. */
crc = pcan_boot_crc32_update(crc, image + offset, count); /* Использует общий полином ProtoCAN и не включает дополнение FF. */
offset += count; /* Продвигает позицию чтения проверенного образа. */
} /* Завершает чтение Flash для независимой проверки CRC. */
if (crc != manifest->image_crc32) return PCAN_BOOT_STATUS_CRC_ERROR; /* Обнаруживает повреждение памяти после успешного приёма CAN. */
if (!STM32_BOOT_VECTORS() || ((*(const uint32_t *)(const void *)(image + 4U)) & ~1UL) >= STM32_BOOT_BASE + manifest->image_size) return PCAN_BOOT_STATUS_INVALID_SIZE; /* Не принимает reset handler вне фактически загруженного образа. */
return PCAN_BOOT_STATUS_OK; /* Реальная Flash и векторы соответствуют полученному manifest. */
} /* Завершает проверку записанного образа. */
static bool commit_image(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest) /* Активирует только повторно проверенный single-slot образ. */
{ /* Не имитирует постоянные A/B-метаданные или защиту от потери питания. */
pcan_boot_stm32_t *port = user; /* Получает RAM-признак успешного COMMIT текущей сессии. */
port->ready = verify_image(user, slot, manifest) == PCAN_BOOT_STATUS_OK; /* Перечитывает Flash перед разрешением reset. */
return port->ready; /* Ядро перейдёт в COMMITTED только при успешной проверке. */
} /* Завершает активацию без записи служебных страниц. */
static bool can_reboot(void *user) /* Запрещает программный запуск недописанного приложения по команде протокола. */
{ /* Политика запуска после внешнего reset остаётся обязанностью проекта. */
pcan_boot_stm32_t *port = user; /* Получает состояние текущей сессии и подтверждение COMMIT. */
return port->ready && port->boot.state == PCAN_BOOT_STATE_COMMITTED; /* ABORT, новая сессия и ошибки сами по себе не дают права reset. */
} /* Завершает проверку допустимости протокольного reset. */
static void reboot(void *user) /* Выполняет reset только после синхронно отправленного ответа ядра. */
{ /* Повторно проверяет RAM-разрешение непосредственно перед сбросом. */
if (can_reboot(user)) NVIC_SystemReset(); /* Использует штатный CMSIS reset, оставляя startup и выбор приложения проекту. */
} /* Завершает callback перезапуска. */
bool STM32_BOOT_INIT(pcan_boot_stm32_t *port, CAN_HandleTypeDef *can, const pcan_boot_config_t *config) /* Подключает текущий API ядра к выделенным CAN1 и single-slot Flash. */
{ /* Тактирование, GPIO, bitrate, HAL_Init и SysTick должны быть настроены проектом заранее. */
pcan_boot_port_t callbacks = {0}; /* Новые необязательные callbacks ядра всегда остаются явно нулевыми. */
pcan_boot_config_t single; /* Копирует идентичность прибора, переопределяя геометрию профиля. */
CAN_FilterTypeDef filter = {0}; /* Подготавливает адресный фильтр 0 и discovery-фильтр 1 в FIFO0. */
uint32_t id, mask; /* Содержит идентификатор и аппаратную маску IDE/RTR сдвига bxCAN. */
if (port == NULL || can == NULL || config == NULL || can->Instance != CAN1) return false; /* Отклоняет неверные указатели и неподдерживаемый CAN2. */
single = *config; /* Сохраняет DeviceType/ID, Product, HW, FW, ack_window и тайм-аут сессии проекта. */
single.active_slot = 0U; /* Single-slot обновление всегда адресует логический слот A. */
single.slot_count = 1U; /* Исключает старую имитацию single-slot через фиктивный active_slot=1. */
single.max_image_size = STM32_BOOT_END - STM32_BOOT_BASE; /* Ядро отвергает превышение физической области ещё до erase. */
callbacks.send = send_frame; /* Подключает синхронную отправку подтверждённых CAN-ответов. */
callbacks.erase_slot = erase_slot; /* Подключает стирание только области приложения. */
callbacks.write_slot = write_slot; /* Подключает последовательное программирование с readback каждого слова. */
callbacks.verify_status = verify_image; /* Возвращает отдельные статусы CRC_ERROR и INVALID_SIZE для Flash/vectors. */
callbacks.set_pending_slot = commit_image; /* Согласует COMMIT с проверкой реального образа и RAM-признаком активации. */
callbacks.can_reboot = can_reboot; /* Не позволяет REBOOT обойти проверку завершённого образа. */
callbacks.reboot = reboot; /* Выполняет CMSIS reset после подтверждения ответа на шине. */
(void)memset(port, 0, sizeof(*port)); /* Очищает старые счётчики и разрешение активации до настройки сессии. */
port->can = can; /* Сохраняет заранее инициализированный handle HAL. */
if (!pcan_boot_init(&port->boot, &single, &callbacks, port)) { port->can = NULL; return false; } /* При ошибке адреса оставляет последующий poll безопасным. */
id = pcan_boot_make_id(0U, PCAN_BOOT_ROUTE_FROM_PM, single.device_type, single.device_id, 0U, 0U) << 3U; /* Переводит адрес прибора в регистровый формат bxCAN. */
mask = (0x0FF00000UL << 3U) | 6U; /* Проверяет Route/Type/ID, IDE=1 и RTR=0; приоритет, MsgType и Body остаются открытыми. */
id |= 4U; /* Устанавливает IDE в бите 2 аппаратного фильтра bxCAN. */
filter.FilterBank = 0U; /* Выделенный загрузчик использует первый банк фильтров. */
filter.FilterMode = CAN_FILTERMODE_IDMASK; /* Сравнивает адрес по маске вместо списка отдельных команд. */
filter.FilterScale = CAN_FILTERSCALE_32BIT; /* Сохраняет все 29 бит расширенного идентификатора. */
filter.FilterIdHigh = id >> 16U; /* Передаёт HAL старшую половину регистрового идентификатора. */
filter.FilterIdLow = id & 0xFFFFU; /* Передаёт HAL младшую половину, включая IDE/RTR. */
filter.FilterMaskIdHigh = mask >> 16U; /* Маскирует адрес прибора и направление обмена. */
filter.FilterMaskIdLow = mask & 0xFFFFU; /* Отвергает стандартные и remote-кадры до программного разбора. */
filter.FilterFIFOAssignment = CAN_FILTER_FIFO0; /* Приём выполняется опросом FIFO0 без IRQ. */
filter.FilterActivation = ENABLE; /* Включает настроенный адресный фильтр. */
filter.SlaveStartFilterBank = 14U; /* Стандартная граница банков CAN1/CAN2 в HAL; порт не использует CAN2. */
if (HAL_CAN_ConfigFilter(can, &filter) != HAL_OK) { port->can = NULL; return false; } /* Не запускает загрузчик с неподтверждённой настройкой адресного фильтра. */
id = (pcan_boot_make_id(0U, PCAN_BOOT_ROUTE_FROM_PM, 7U, 15U, PCAN_BOOT_MSG_DISCOVERY, 0U) << 3U) | 4U; /* Отдельно разрешает штатный broadcast discovery, сохраняя запрет broadcast-записи. */
mask = (0x0FFFFFFFUL << 3U) | 6U; /* Проверяет всё кроме приоритета, включая MsgType=DISCOVERY и пустой Body. */
filter.FilterBank = 1U; /* Второй выделенный банк обслуживает только обнаружение устройств. */
filter.FilterIdHigh = id >> 16U; /* Записывает старшую половину broadcast discovery ID. */
filter.FilterIdLow = id & 0xFFFFU; /* Записывает младшую половину с IDE=1 и RTR=0. */
filter.FilterMaskIdHigh = mask >> 16U; /* Ограничивает тип и адрес broadcast запроса. */
filter.FilterMaskIdLow = mask & 0xFFFFU; /* Ограничивает Body и формат кадра discovery. */
if (HAL_CAN_ConfigFilter(can, &filter) != HAL_OK || HAL_CAN_Start(can) != HAL_OK) { port->can = NULL; return false; } /* Обезвреживает poll при отказе второго фильтра либо запуска CAN. */
return true; /* Оба фильтра настроены, CAN успешно вышел из режима init. */
} /* Завершает настройку одного аппаратного порта. */
void STM32_BOOT_POLL(pcan_boot_stm32_t *port) /* Обслуживает загрузчик только из основного цикла, где допустимо синхронное стирание. */
{ /* Вызов из CAN IRQ не допускается из-за длительных операций Flash. */
CAN_HandleTypeDef *can; /* Получает аппаратный handle после проверки состояния порта. */
uint32_t count; /* Ограничивает обработку глубиной аппаратного FIFO bxCAN. */
if (port == NULL || port->can == NULL) return; /* Позволяет безопасно пропустить ещё не настроенный порт. */
can = port->can; /* Восстанавливает семейный HAL-тип handle. */
pcan_boot_poll(&port->boot, HAL_GetTick()); /* Обновляет монотонное время и тайм-аут сессии до приёма команд. */
for (count = 0U; count < 3U && HAL_CAN_GetRxFifoFillLevel(can, CAN_RX_FIFO0) != 0U; ++count) { /* Не позволяет непрерывной нагрузке CAN захватить весь главный цикл. */
CAN_RxHeaderTypeDef header = {0}; /* Принимает IDE/RTR/DLC независимо от аппаратной настройки фильтра. */
uint8_t data[8]; /* Выделяет максимальный payload одного classic CAN-кадра. */
if (HAL_CAN_GetRxMessage(can, CAN_RX_FIFO0, &header, data) != HAL_OK) break; /* Не разбирает неполученный кадр при аппаратной ошибке. */
if (header.IDE == CAN_ID_EXT && header.RTR == CAN_RTR_DATA && header.DLC <= 8U && header.ExtId <= 0x1FFFFFFFUL) { /* Повторно проверяет внешний ввод до вызова общего decoder. */
pcan_boot_poll(&port->boot, HAL_GetTick()); /* Учитывает время предыдущего синхронного erase перед следующей командой. */
(void)pcan_boot_process(&port->boot, header.ExtId, data, (uint8_t)header.DLC); /* Передаёт адресные команды и восьмибайтовые блоки общему автомату. */
} /* Завершает обработку допустимого формата кадра. */
} /* Возвращает управление приложению после ограниченного пакета входящих кадров. */
} /* Завершает обслуживание транспорта и таймеров. */

View File

@@ -0,0 +1,50 @@
# STM32F103C8: ProtoCAN Boot v1
## Подключённый KONOR без HAL
`boot_main.c` содержит извлечённый CMSIS-загрузчик CAN/UART, `boot_request.c` —
вход из приложения через BKP DR1, `boot_profile.h` — общие адреса и magic.
`KONOR_ds18b20/src/bootloader.c`, `src/boot_control.c`, `inc/boot_control.h`
теперь включают эти файлы из `lib/c/protocan-boot/ports/stm32f103`.
Keil BOOT компилирует общие `pcan_boot.c` и `rs485_boot.c`; локальный `proto.c`
исключён только из загрузчика, приложение продолжает использовать свой parser.
UART BEGIN/DATA/END/STATUS сохраняют SETGUI v1, 512000 бод и PA9/PA10.
Номера блоков, CRC и сессии CAN обслуживает только общий ProtoCAN-автомат.
Оба транспорта используют один Flash writer; одновременная запись блокируется.
Native вариант не нужно собирать вместе с reusable HAL `.c` ниже.
## Reusable HAL-порт
Аппаратный single-slot профиль извлечён из `KONOR_ds18b20/src/bootloader.c`.
Прямые операции с регистрами адаптированы к современному STM32CubeF1 HAL;
UART SETGUI parser исходного проекта в этот CAN-порт не входит.
| Область | Начало | Конец, исключён | Размер |
|---|---|---|---:|
| Загрузчик | `0x08000000` | `0x08003000` | 12 КиБ |
| Приложение | `0x08003000` | `0x08010000` | 52 КиБ |
| SRAM для MSP | `0x20000000` | `0x20005000` | 20 КиБ |
MSP может совпадать с верхней границей SRAM, но не с нижней; выравнивание —
восемь байт. Страница Flash — 1024 байта, единица записи — полуслово 16 бит.
Профиль предназначен для штатных 64 КиБ Flash STM32F103C8, без использования
недокументированного расширения памяти.
Подключайте `pcan_boot_stm32f103.c`, общий `src/pcan_boot.c` и сохраните соседний
каталог `stm32-common`. STM32CubeF1 HAL должен включать CAN и Flash/FlashEx.
Для определения CMSIS обычно используется `STM32F103xB`; физический профиль
этого порта остаётся C8 с 64 КиБ.
Проект настраивает CAN1 и GPIO: исходная плата использует PA11 RX, PA12 TX,
500 кбит/с, DeviceType=7 / DeviceID=13. Значения адреса задавайте через
`pcan_boot_config_t` и согласуйте с host-клиентом. На стороне Flash всегда
используется логический слот A.
Вызывайте `pcan_boot_stm32f103_init` после `HAL_CAN_Init`, затем
`pcan_boot_stm32f103_poll` из основного цикла. Linker приложения и VTOR должны
указывать на `0x08003000`. В загрузчике зарезервируйте весь диапазон до этого
адреса и проверьте размер итогового образа по MAP: HAL-сборка может оказаться
крупнее исходного загрузчика с прямым доступом к регистрам.
[Общая инструкция, пример, ограничения COMMIT и запуска после reset](../stm32-common/README.md).

View File

@@ -0,0 +1,95 @@
#include "boot_profile.h" /* Сохраняет карту памяти, BKP-запросы и CAN-адрес исходного KONOR. */
#include "../../include/pcan_boot.h" /* Использует единственную текущую машину состояний ProtoCAN из templates. */
#include "../../../rs485-boot/include/rs485_boot.h" /* Повторно использует общий SETGUI v1 UART-автомат вместо локальной копии parser/CRC. */
#include "stm32f10x.h" /* Работает с существующим CMSIS/StdPeriph проекта без добавления HAL. */
#include <string.h> /* Предоставляет безопасное копирование коротких хвостов Flash. */
#define NATIVE_APP_BASE BOOT_APP_ADDRESS /* Выбирает начало приложения после 12 КиБ загрузчика. */
#define NATIVE_APP_SIZE (BOOT_FLASH_END - BOOT_APP_ADDRESS) /* Ограничивает обновление 52 КиБ штатной Flash C8. */
#define NATIVE_RAM_END 0x20005000UL /* Начальный MSP допускается до верхней границы 20 КиБ SRAM. */
#define NATIVE_PROGRAM_UNIT 2U /* Контроллер Flash STM32F103 записывает полуслова по два байта. */
#define NATIVE_PPRE1_SHIFT 8U /* STM32F1 размещает делитель APB1 в RCC_CFGR[10:8]. */
static pcan_boot_t boot; static volatile uint32_t ticks_ms; /* Хранит CAN-сессию и монотонный счётчик SysTick загрузчика. */
static rs485_boot_t uart_boot; /* Хранит UART parser, повторяемую запись блоков и статус общего SETGUI v1-автомата. */
static bool native_wait(void) /* Ограничивает ожидание Flash и проверяет аппаратные ошибки программирования. */
{ /* На однобанковой Flash CPU может останавливаться до завершения операции. */
uint32_t guard = SystemCoreClock; /* Конечный бюджет проверок численно равен HCLK в Гц; это число итераций CPU, а не аппаратный тайм-аут. */
while ((FLASH->SR & FLASH_SR_BSY) != 0U && guard != 0U) --guard; /* Не продолжает запись до освобождения контроллера Flash. */
return (FLASH->SR & (FLASH_SR_BSY | FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) == 0U; /* Ошибки записи или защиты делают аппаратную операцию неуспешной. */
} /* Завершает ожидание Flash с аппаратной проверкой. */
static bool native_unlock(void) /* Открывает Flash для одной ограниченной операции. */
{ /* Не меняет Option Bytes или защиту загрузчика. */
if ((FLASH->SR & FLASH_SR_BSY) != 0U) return false; /* Не посылает ключи занятому контроллеру. */
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR | FLASH_SR_EOP; /* Очищает завершение и прежние ошибки записью W1C. */
if ((FLASH->CR & FLASH_CR_LOCK) != 0U) { FLASH->KEYR = 0x45670123UL; FLASH->KEYR = 0xCDEF89ABUL; } /* Использует обязательную последовательность ключей STM32F1 из RM0008. */
return (FLASH->CR & FLASH_CR_LOCK) == 0U; /* Проверяет фактическое снятие аппаратной блокировки. */
} /* Завершает разблокировку без сохранения разрешения между командами. */
static void native_lock(void) { FLASH->CR = FLASH_CR_LOCK; } /* Снимает PG/PER и восстанавливает LOCK даже после обработанной ошибки. */
static bool native_program(uint32_t address, uint32_t value) /* Записывает и перечитывает одно выровненное полуслово. */
{ /* Границы и выравнивание заранее проверяет общий writer. */
volatile uint16_t *destination = (volatile uint16_t *)(uintptr_t)address; /* Выбирает аппаратную 16-битную операцию F103. */
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR | FLASH_SR_EOP; FLASH->CR = FLASH_CR_PG; /* Очищает прежний результат и разрешает программирование полуслова. */
*destination = (uint16_t)value; /* Передаёт младшие 16 бит, включая FF-дополнение неполного хвоста. */
if (!native_wait()) return false; /* Не подтверждает запись при BUSY или аппаратной ошибке. */
FLASH->CR = 0U; return *destination == (uint16_t)value; /* Убирает PG и проверяет реально прочитанное полуслово. */
} /* Завершает одну аппаратную запись. */
static bool native_erase(uint32_t size) /* Стирает страницы приложения, не пересекая область загрузчика. */
{ /* Общая геометрия страницы задаётся единственным профилем F103C8. */
uint32_t address, end; bool ok = true; /* Хранит исключённый конец стирания и результат до блокировки Flash. */
if (size < 8U || size > NATIVE_APP_SIZE) return false; /* Не стирает Flash для пустого или слишком большого образа. */
end = BOOT_APP_ADDRESS + ((size + BOOT_PAGE_SIZE - 1U) & ~(BOOT_PAGE_SIZE - 1U)); /* Округляет конец вверх к странице 1024 байта внутри профиля. */
if (end > BOOT_FLASH_END || !native_unlock()) return false; /* Проверяет округлённую верхнюю границу до первого стирания. */
for (address = BOOT_APP_ADDRESS; address < end; address += BOOT_PAGE_SIZE) { /* Обходит только страницы, покрывающие полезный образ. */
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR | FLASH_SR_EOP; FLASH->CR = FLASH_CR_PER; FLASH->AR = address; FLASH->CR |= FLASH_CR_STRT; /* Запускает erase одной конкретной страницы вместо mass erase. */
if (!native_wait()) { ok = false; break; } /* Прекращает стирание на первой аппаратной ошибке. */
} /* Завершает ограниченный диапазон страниц. */
native_lock(); return ok; /* Обязательно закрывает Flash после успешного или прерванного erase. */
} /* Завершает аппаратное стирание F1. */
static void native_gpio_init(void) /* Сохраняет исходную разводку CAN1 KONOR: PA11 RX и PA12 TX. */
{ /* UART на PA9/PA10 остаётся независимым загрузочным транспортом. */
uint32_t value; RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN; RCC->APB1ENR |= RCC_APB1ENR_CAN1EN; /* Включает GPIOA, AFIO и CAN1 в соответствующих APB-доменах. */
value = GPIOA->CRH; value &= ~((0xFUL << 12U) | (0xFUL << 16U)); value |= (0x8UL << 12U) | (0xBUL << 16U); GPIOA->CRH = value; /* Настраивает PA11 как вход с подтяжкой, PA12 как AF push-pull согласно RM0008. */
GPIOA->BSRR = 1UL << 11U; /* Выбирает подтяжку CAN RX вверх, соответствующую рецессивному состоянию неподключённой линии. */
} /* Завершает настройку физического CAN-порта. */
static void native_peripheral_stop(void) { USART1->CR1 = 0U; RCC->APB1RSTR |= RCC_APB1RSTR_CAN1RST; RCC->APB1RSTR &= ~RCC_APB1RSTR_CAN1RST; } /* Отключает загрузочный USART и сбрасывает CAN перед handoff. */
#include "../stm32-common/boot_native.inc" /* Подключает общий транспорт, single-slot callbacks, CRC readback и безопасный переход MSP. */
static bool uart_transmit(void *user, const uint8_t *data, uint16_t length) /* Отправляет кадры SETGUI v1 с ожиданием последнего стоп-бита. */
{ /* Использует прежнюю скорость USART1 512000 бод без управления RS-485 DE. */
uint16_t index; (void)user; /* UART-автомат использует единственный статический аппаратный порт. */
for (index = 0U; index < length; ++index) { while ((USART1->SR & USART_SR_TXE) == 0U) {} USART1->DR = data[index]; } /* Передаёт только объявленные байты полного кадра. */
while ((USART1->SR & USART_SR_TC) == 0U) {} return true; /* Дожидается физического завершения ответа перед возможным reset. */
} /* Завершает аппаратную передачу UART. */
static bool uart_erase(void *user, uint32_t size) { (void)user; return boot.state == PCAN_BOOT_STATE_IDLE && native_erase(size); } /* Не позволяет UART стереть Flash во время открытой CAN-сессии. */
static bool uart_write(void *user, uint32_t offset, const uint8_t *data, uint16_t length) { (void)user; return native_write_bytes(uart_boot.image_size, offset, data, length); } /* Использует ту же проверку диапазонов и аппаратную запись, что CAN. */
static bool uart_read(void *user, uint32_t offset, uint8_t *data, uint16_t length) /* Позволяет общему UART-автомату подтвердить повторно присланный блок. */
{ /* Проверяет размер до сложения адреса, исключая переполнение. */
(void)user; if (data == NULL || offset > NATIVE_APP_SIZE || length > NATIVE_APP_SIZE - offset) return false; /* Не читает за областью приложения. */
(void)memcpy(data, (const void *)(uintptr_t)(NATIVE_APP_BASE + offset), length); return true; /* Копирует только объявленный безопасный диапазон Flash. */
} /* Завершает UART readback для повторов передачи. */
static bool uart_image_valid(void *user, uint32_t size, uint32_t crc) { (void)user; return native_image_valid(size, crc); } /* Перед END использует тот же CRC Flash и проверку векторов, что CAN VERIFY. */
static void uart_init(void) /* Инициализирует существующий TTL UART и общий SETGUI v1-автомат. */
{ /* Локальный Proto parser исходного проекта загрузчику больше не нужен. */
uint32_t value; rs485_boot_config_t config = {BOOT_APP_ADDRESS, NATIVE_APP_SIZE, RS485_BOOT_MAX_BLOCK}; rs485_boot_port_t callbacks = {0}; /* Сохраняет прежний wire-протокол и предел 504 полезных байта на DATA. */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN; value = GPIOA->CRH; value &= ~((0xFUL << 4U) | (0xFUL << 8U)); value |= (0xBUL << 4U) | (0x4UL << 8U); GPIOA->CRH = value; /* Настраивает PA9 TX AF-PP и PA10 RX floating, как исходный KONOR. */
USART1->BRR = (SystemCoreClock + 256000UL) / 512000UL; USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; /* При штатном APB2=HCLK сохраняет округлённую скорость 512000 бод, 8-N-1. */
callbacks.transmit = uart_transmit; callbacks.erase = uart_erase; callbacks.write = uart_write; callbacks.read = uart_read; callbacks.image_valid = uart_image_valid; callbacks.reboot = native_reboot; /* Подключает аппаратные операции к общему UART-автомату. */
(void)rs485_boot_init(&uart_boot, &config, &callbacks, NULL); /* Инициализирует parser и RAM-сессию отдельного транспорта. */
} /* Завершает настройку UART без локальной копии codec. */
static void uart_poll(void) /* Обслуживает UART из главного цикла без прерываний и выделения памяти. */
{ /* Во время CAN-сессии UART BEGIN отвергается callback стирания. */
uint32_t status = USART1->SR; uint8_t byte; /* Сохраняет статус до чтения регистра данных. */
if ((status & (USART_SR_ORE | USART_SR_NE | USART_SR_FE | USART_SR_PE)) != 0U) { byte = (uint8_t)USART1->DR; (void)byte; } /* Снимает аппаратные ошибки чтением SR/DR и отбрасывает повреждённый байт. */
else if ((status & USART_SR_RXNE) != 0U) { byte = (uint8_t)USART1->DR; rs485_boot_feed(&uart_boot, &byte, 1U); } /* Передаёт исправный байт общему parser с прежним кадрированием A5 5A. */
} /* Завершает неблокирующий приём UART. */
int main(void) /* Самостоятельная точка входа общего загрузчика KONOR для Keil/GCC. */
{ /* Тактирование и startup остаются в исходном проекте и выполняются до main. */
uint16_t request; bool can_ready; /* Хранит одноразовое назначение reset и результат включения CAN. */
RCC->APB1ENR |= RCC_APB1ENR_PWREN | RCC_APB1ENR_BKPEN; PWR->CR |= PWR_CR_DBP; request = (uint16_t)BKP->DR1; BKP->DR1 = 0U; /* Потребляет совместимый BKP-запрос ровно один раз. */
if (request == BOOT_REQUEST_SYSTEM_MAGIC) { /* Сохраняет исходную подготовку периферии перед переходом в заводской UART-загрузчик. */
__disable_irq(); RCC->APB1RSTR = 0U; RCC->APB2RSTR = 0U; RCC->AHBENR = 0x00000014UL; RCC->APB2ENR = 0U; RCC->APB1ENR = 0U; /* Как исходный KONOR, оставляет только AHB Flash/SRAM и отключает внешнюю APB-периферию перед System Memory. */
native_jump(0x1FFFF000UL); /* AN2606: фиксированное начало System Memory STM32F103 medium density, дальнейший переход выполняется без доступа к старому MSP. */
} /* Завершает совместимую ветку заводского загрузчика. */
if (request != BOOT_REQUEST_FIRMWARE_MAGIC && native_vectors_valid(NATIVE_APP_SIZE)) native_jump(BOOT_APP_ADDRESS); /* Сохраняет немедленный обычный запуск KONOR при допустимых векторах приложения. */
SystemCoreClockUpdate(); if (SysTick_Config(SystemCoreClock / 1000U) != 0U || !native_init()) for (;;) {} /* Создаёт миллисекундный tick и проверяет текущую конфигурацию ProtoCAN до обработки команд. */
uart_init(); can_ready = native_can_init(); /* При отказе CAN сохраняет доступный UART-канал восстановления. */
for (;;) { uart_poll(); if (can_ready && uart_boot.state != RS485_BOOT_RECEIVING) native_can_poll(); } /* Не позволяет двум транспортам одновременно изменять одну область Flash. */
} /* Завершает самостоятельный загрузчик; управление остаётся в сервисном цикле до успешного обновления. */

View File

@@ -0,0 +1,17 @@
#ifndef PCAN_BOOT_F103_PROFILE_H /* Согласует загрузчик и приложение KONOR с одной картой памяти. */
#define PCAN_BOOT_F103_PROFILE_H /* Исключает повторное определение аппаратных констант. */
#define BOOT_APP_ADDRESS 0x08003000UL /* Приложение KONOR начинается после 12 КиБ защищаемого загрузчика. */
#define BOOT_FLASH_END 0x08010000UL /* Верхняя исключённая граница штатных 64 КиБ Flash STM32F103C8. */
#define BOOT_PAGE_SIZE 1024UL /* Средняя плотность STM32F103C8 использует страницы Flash по 1024 байта. */
#define BOOT_REQUEST_FIRMWARE_MAGIC 0xB007U /* Сохраняет совместимость существующего запроса обновления в BKP DR1. */
#define BOOT_REQUEST_SYSTEM_MAGIC 0xB315U /* Сохраняет запрос перехода в заводской UART-загрузчик AN3155. */
#define PCAN_BOOT_DEVICE_TYPE 7U /* Адрес исходного загрузчика KONOR: DeviceType=7. */
#define PCAN_BOOT_DEVICE_ID 13U /* Адрес исходного загрузчика KONOR: DeviceID=13. */
#define PCAN_BOOT_PRODUCT_TYPE 0U /* Исходный ProductType KONOR; должен совпадать с manifest клиента. */
#define PCAN_BOOT_HW_REVISION 0U /* Исходная ревизия платы, согласованная с диапазоном HW manifest. */
#define PCAN_BOOT_FW_VERSION 0U /* Версия самого загрузчика KONOR; версию приложения сообщает приложение. */
#define PCAN_BOOT_ACK_WINDOW 16U /* Подтверждает каждые 16 блоков ProtoCAN по восемь байт. */
#define PCAN_BOOT_CAN_BITRATE 500000UL /* Исходная скорость CAN-загрузчика, бит в секунду. */
void Boot_RequestFirmware(void); /* Записывает запрос обновления в BKP DR1 и выполняет программный reset. */
void Boot_RequestSystemMemory(void); /* Записывает запрос AN3155 в BKP DR1 и выполняет программный reset. */
#endif /* Завершает профиль STM32F103C8 для загрузчика и приложения. */

View File

@@ -0,0 +1,12 @@
#include "boot_profile.h" /* Использует ту же карту памяти и magic, что общий загрузчик F103. */
#include "stm32f10x.h" /* Сохраняет совместимость KONOR с CMSIS/StdPeriph без зависимости от HAL. */
static void request_boot(uint16_t magic) /* Сохраняет одноразовый запрос через программный reset. */
{ /* BKP DR1 существует в backup-домене STM32F103 и не требует резервирования основной SRAM. */
RCC->APB1ENR |= RCC_APB1ENR_PWREN | RCC_APB1ENR_BKPEN; /* Разрешает тактирование PWR и backup-регистров. */
PWR->CR |= PWR_CR_DBP; /* Разрешает запись в защищённый backup-домен. */
BKP->DR1 = magic; /* Записывает совместимый код назначения следующего запуска. */
__DSB(); /* Гарантирует завершение записи до запроса системного сброса. */
NVIC_SystemReset(); /* Следующий startup потребляет запрос и выбирает загрузчик. */
} /* Завершает общую аппаратную последовательность входа. */
void Boot_RequestFirmware(void) { request_boot(BOOT_REQUEST_FIRMWARE_MAGIC); } /* Сохраняет прежний API приложения KONOR для обновления CAN/UART. */
void Boot_RequestSystemMemory(void) { request_boot(BOOT_REQUEST_SYSTEM_MAGIC); } /* Сохраняет прежний API меню для заводского UART-загрузчика STM32. */

View File

@@ -0,0 +1,22 @@
#include "pcan_boot_stm32f103.h" /* Подключает HAL STM32F1 и публичный профиль KONOR. */
#include <string.h> /* Предоставляет инициализацию структур и чтение невыровненных октетов. */
#define STM32_BOOT_INIT pcan_boot_stm32f103_init /* Экспортирует семейное имя общей процедуры настройки. */
#define STM32_BOOT_POLL pcan_boot_stm32f103_poll /* Экспортирует семейное имя обслуживания FIFO и таймеров. */
#define STM32_BOOT_VECTORS pcan_boot_stm32f103_vectors_valid /* Экспортирует проверку векторов именно этого профиля. */
#define STM32_BOOT_BASE PCAN_BOOT_F103_APP_ADDRESS /* Сохраняет адрес приложения исходной прошивки KONOR. */
#define STM32_BOOT_END PCAN_BOOT_F103_FLASH_END /* Ограничивает erase и write штатными 64 КиБ Flash. */
#define STM32_BOOT_RAM_END 0x20005000UL /* STM32F103C8 имеет 20 КиБ SRAM; MSP может указывать сразу за последним байтом. */
#define STM32_BOOT_UNIT 2U /* Flash STM32F103 программируется полусловами по два байта. */
#define STM32_BOOT_PROGRAM FLASH_TYPEPROGRAM_HALFWORD /* Выбирает штатный HAL-режим записи полуслова. */
#define STM32_BOOT_SYNC() ((void)0) /* F103 не имеет Flash data cache, требующего сброса перед readback. */
static bool erase_region(pcan_boot_stm32_t *port, uint32_t size) /* Стирает только страницы, покрывающие проверенный размер образа. */
{ /* Вызывается общим кодом после HAL_FLASH_Unlock и проверки границ. */
FLASH_EraseInitTypeDef erase = {0}; /* Исключает неопределённые поля семейной структуры HAL. */
uint32_t error = 0U; /* Получает адрес страницы, на которой HAL обнаружил ошибку. */
(void)port; /* F1 стирает выбранные страницы одной синхронной операцией HAL. */
erase.TypeErase = FLASH_TYPEERASE_PAGES; /* Не разрешает массовое стирание, затрагивающее загрузчик. */
erase.PageAddress = STM32_BOOT_BASE; /* Начинает стирание с первой страницы приложения. */
erase.NbPages = (size + 1023U) / 1024U; /* Округляет вверх к странице 1 КиБ STM32F103C8, как в исходном KONOR. */
return HAL_FLASHEx_Erase(&erase, &error) == HAL_OK; /* Прерывает обновление при ошибке или тайм-ауте Flash. */
} /* Завершает аппаратную реализацию стирания F1. */
#include "../stm32-common/pcan_boot_stm32.inc" /* Собирает общий transport/readback с геометрией и HAL этого семейства. */

View File

@@ -0,0 +1,11 @@
#ifndef PCAN_BOOT_STM32F103_H /* Исключает повторное объявление порта STM32F103C8. */
#define PCAN_BOOT_STM32F103_H /* Обозначает активный интерфейс аппаратного профиля. */
#include "stm32f1xx_hal.h" /* Получает тип CAN_HandleTypeDef и драйверы Flash из STM32CubeF1. */
#include "boot_profile.h" /* Использует ту же аппаратную карту, что подключённый CMSIS-загрузчик KONOR. */
#include "../stm32-common/pcan_boot_stm32.h" /* Подключает состояние и API переносимого автомата. */
#define PCAN_BOOT_F103_APP_ADDRESS BOOT_APP_ADDRESS /* Сохраняет публичное HAL-имя адреса приложения без дублирования числа 0x08003000. */
#define PCAN_BOOT_F103_FLASH_END BOOT_FLASH_END /* Сохраняет публичное HAL-имя верхней границы 64 КиБ Flash из общего профиля. */
bool pcan_boot_stm32f103_init(pcan_boot_stm32_t *port, CAN_HandleTypeDef *can, const pcan_boot_config_t *config); /* Назначает single-slot callbacks, фильтры 0/1 в FIFO0 и запускает заранее настроенный CAN1. */
void pcan_boot_stm32f103_poll(pcan_boot_stm32_t *port); /* Обслуживает до трёх кадров FIFO0 и таймеры ядра из основного цикла. */
bool pcan_boot_stm32f103_vectors_valid(void); /* Проверяет MSP и Thumb-вектор в пределах профиля; CRC установленного образа не подтверждает. */
#endif /* Завершает интерфейс профиля STM32F103C8. */

View File

@@ -0,0 +1,51 @@
# STM32F407VE: ProtoCAN Boot v1
## Подключённый climate
`boot_main.c` содержит извлечённый CMSIS-загрузчик, `boot_control_hal.c/.h` —
адресный ENTER_BOOT из рабочего приложения. `boot_profile.h` определяет карту
Flash и SRAM-запрос; идентичность прибора остаётся в `pcan_boot_config.h` проекта.
Исходные `Bootloader/bootloader_f407.c`, `Libraries/ProtoCanBoot/Src/pcan_boot.c`
и `Core/Src/can_boot_control.c` стали include-обёртками общего submodule.
Старый include API также перенаправлен в `templates`.
BOOT linker исключает последние восемь байт SRAM: запрос хранится в
`0x2001FFFC`, а initial stack не может затереть его до main. Приложение climate
уже использует нижние 112 КиБ SRAM и не перекрывает запрос. Обычный reset
сохраняет десятисекундное окно обновления; TX ACK перед REBOOT проверяется.
Native boot main и reusable HAL `.c` ниже — отдельные варианты сборки.
## Reusable HAL-порт
Аппаратный single-slot профиль извлечён из
`climate_control_f407vet6_f4/Bootloader/bootloader_f407.c` и
`Libraries/ProtoCanBoot/Inc/pcan_boot_config.h` проекта climate.
Операции с регистрами адаптированы к современному STM32CubeF4 HAL.
| Область | Начало | Конец, исключён | Размер |
|---|---|---|---:|
| Загрузчик: секторы 0/1 | `0x08000000` | `0x08008000` | 32 КиБ |
| Приложение: секторы 2..5 | `0x08008000` | `0x08040000` | 224 КиБ |
| EEPROM-кольцо: секторы 6/7 | `0x08040000` | `0x08080000` | 256 КиБ |
| Основная SRAM для MSP | `0x20000000` | `0x20020000` | 128 КиБ |
Размеры стираемых секторов 2/3/4/5: 16/16/64/128 КиБ. Запись выполняется
словами 32 бита, с `FLASH_VOLTAGE_RANGE_3`; VDD платы должно быть 2,7..3,6 В.
Порт не стирает секторы 0/1/6/7. CCM не используется как область допустимого MSP.
Подключайте `pcan_boot_stm32f407.c`, общий `src/pcan_boot.c` и сохраните соседний
каталог `stm32-common`. В CMSIS выберите `STM32F407xx`, в HAL включите CAN и
Flash/FlashEx. Проект настраивает CAN1, скорость и GPIO: исходная плата использует
PD0 RX, PD1 TX, AF9, 500 кбит/с, DeviceType=7 / DeviceID=3.
Вызывайте `pcan_boot_stm32f407_init` после `HAL_CAN_Init`, затем
`pcan_boot_stm32f407_poll` из основного цикла. Адрес прибора, Product/HW/FW,
окно ACK и тайм-аут сессии задаются через `pcan_boot_config_t` по именам полей.
Инициализация принудительно задаёт логический слот A и ёмкость 224 КиБ.
Linker приложения и VTOR должны указывать на `0x08008000`; загрузчик должен
помещаться в 32 КиБ до этого адреса. Это исходная CAN-разметка climate:
USB SET v2-порт F407 из `set-protocol` использует другую карту памяти,
поэтому его linker и готовые USB-образы нельзя подключать без адаптации.
[Общая инструкция, пример, ограничения COMMIT и запуска после reset](../stm32-common/README.md).

View File

@@ -0,0 +1,29 @@
#include "boot_control_hal.h" /* Подключает совместимый API приложения climate. */
#include "boot_profile.h" /* Получает общий адрес SRAM-запроса и карту Flash. */
#include "pcan_boot_config.h" /* Идентичность прибора остаётся настройкой проекта, общей для boot и app. */
HAL_StatusTypeDef CanBootControl_Start(CAN_HandleTypeDef *can) /* Сохраняет прежнюю настройку выделенного CAN приложения. */
{ /* Новые приложения с DeviceCan используют только Handle и собственные адресные фильтры. */
CAN_FilterTypeDef filter = {0}; /* Исключает неопределённые поля аппаратной конфигурации. */
if (can == NULL) return HAL_ERROR; /* Не обращается к отсутствующему HAL handle. */
filter.FilterBank = 0U; filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_32BIT; /* Сохраняет широкую фильтрацию старого выделенного канала; адрес проверяется программно. */
filter.FilterFIFOAssignment = CAN_RX_FIFO0; filter.FilterActivation = ENABLE; filter.SlaveStartFilterBank = 14U; /* Использует FIFO0 и штатное разделение банков CAN1/CAN2. */
if (HAL_CAN_ConfigFilter(can, &filter) != HAL_OK) return HAL_ERROR; /* Не запускает CAN с неподтверждённым фильтром. */
return HAL_CAN_Start(can); /* Возвращает вызывающему приложению фактический результат запуска. */
} /* Завершает совместимую настройку транспорта приложения. */
void CanBootControl_Poll(CAN_HandleTypeDef *can) /* Поддерживает старые приложения без отдельного распределителя входящих кадров. */
{ /* Приём ограничен одним кадром, чтобы не блокировать главный цикл. */
CAN_RxHeaderTypeDef header; uint8_t data[8]; /* Выделяет полный буфер classic CAN независимо от входящего DLC. */
if (can != NULL && HAL_CAN_GetRxFifoFillLevel(can, CAN_RX_FIFO0) != 0U && HAL_CAN_GetRxMessage(can, CAN_RX_FIFO0, &header, data) == HAL_OK) (void)CanBootControl_Handle(&header, data); /* Передаёт только успешно принятый кадр общему обработчику. */
} /* Завершает опрос канала совместимости. */
uint8_t CanBootControl_Handle(const CAN_RxHeaderTypeDef *header, const uint8_t data[8]) /* Запускает обновление только для собственного адреса и ненулевой сессии. */
{ /* Ответ ENTER_BOOT отправляет уже загрузчик после повторного запроса клиента. */
uint32_t id; /* Сохраняет проверенный расширенный CAN ID запроса. */
(void)data; /* ENTER_BOOT не содержит payload и не должен читать входной буфер. */
if (header == NULL || header->IDE != CAN_ID_EXT || header->RTR != CAN_RTR_DATA || header->DLC != 0U || header->ExtId > 0x1FFFFFFFUL) return 0U; /* Отвергает стандартные, remote и ошибочно дополненные запросы. */
id = header->ExtId; /* Извлекает идентификатор только после проверки внешнего заголовка. */
if (((id >> 27U) & 1U) != 0U || ((id >> 24U) & 7U) != PCAN_BOOT_DEVICE_TYPE || ((id >> 20U) & 15U) != PCAN_BOOT_DEVICE_ID || ((id >> 16U) & 15U) != 9U || (id & 255U) != 2U || ((id >> 8U) & 255U) == 0U) return 0U; /* Сопоставляет Route=0, адрес, BOOT_CONTROL=9, ENTER_BOOT=2 и SessionID != 0. */
*((volatile uint32_t *)(uintptr_t)PCAN_BOOT_REQUEST_ADDRESS) = PCAN_BOOT_REQUEST_MAGIC; /* Сохраняет одноразовый запрос в слове SRAM, исключённом из linker-стека. */
__DSB(); /* Дожидается записи SRAM перед системным reset. */
NVIC_SystemReset(); /* Передаёт управление общему загрузчику без подтверждения из приложения. */
return 1U; /* Позволяет тестовой модели и старому диспетчеру отметить обработанный запрос. */
} /* Завершает безопасную проверку входа в загрузчик. */

View File

@@ -0,0 +1,7 @@
#ifndef PCAN_BOOT_F407_CONTROL_HAL_H /* Защищает совместимый API входа climate в общий загрузчик. */
#define PCAN_BOOT_F407_CONTROL_HAL_H /* Обозначает включение адаптера рабочего приложения. */
#include "stm32f4xx_hal.h" /* Приложение climate передаёт штатные заголовки и handle HAL CAN. */
HAL_StatusTypeDef CanBootControl_Start(CAN_HandleTypeDef *can); /* Сохраняет прежний API выделенного CAN-канала приложения. */
void CanBootControl_Poll(CAN_HandleTypeDef *can); /* Проверяет один входящий кадр в старых проектах без общего CAN-диспетчера. */
uint8_t CanBootControl_Handle(const CAN_RxHeaderTypeDef *header, const uint8_t data[8]); /* Обрабатывает адресный ENTER_BOOT из существующего диспетчера DeviceCan. */
#endif /* Завершает интерфейс адаптера приложения F407. */

View File

@@ -0,0 +1,68 @@
#include "boot_profile.h" /* Сохраняет карту Flash climate и зарезервированное SRAM-слово запроса. */
#include "pcan_boot_config.h" /* Идентичность прибора и окно ACK остаются конфигурацией исходного проекта. */
#include "../../include/pcan_boot.h" /* Подключает единственную текущую state machine ProtoCAN из templates. */
#include "stm32f407xx.h" /* Сохраняет лёгкую CMSIS-сборку загрузчика без HAL-драйверов. */
#include <string.h> /* Предоставляет безопасное формирование слов из коротких хвостов CAN. */
#define NATIVE_APP_BASE PCAN_BOOT_APP_ADDRESS /* Приложение climate начинается с физического сектора 2. */
#define NATIVE_APP_SIZE PCAN_BOOT_APP_SIZE /* Ограничивает прошивку секторами 2..5, сохраняя EEPROM-кольцо. */
#define NATIVE_RAM_END 0x20020000UL /* Физическая верхняя граница 128 КиБ SRAM сохраняет совместимость векторов ранее установленных приложений. */
#define NATIVE_PROGRAM_UNIT 4U /* F407 программирует слова x32 при VDD 2,7..3,6 В. */
#define NATIVE_PPRE1_SHIFT 10U /* STM32F4 размещает делитель APB1 в RCC_CFGR[12:10]. */
#define NATIVE_FLASH_ERRORS (FLASH_SR_PGSERR | FLASH_SR_PGPERR | FLASH_SR_PGAERR | FLASH_SR_WRPERR) /* Проверяет последовательность, разрядность, выравнивание и аппаратную защиту Flash по RM0090. */
static pcan_boot_t boot; static volatile uint32_t ticks_ms; /* Хранит единственный автомат CAN и миллисекундные часы загрузчика. */
static bool native_wait(void) /* Ограничивает аппаратное ожидание Flash без бесконечной блокировки. */
{ /* Код продолжит исполняться после аппаратного Flash stall на однобанковом F407. */
uint32_t guard = SystemCoreClock; while ((FLASH->SR & FLASH_SR_BSY) != 0U && guard != 0U) --guard; /* Использует конечный бюджет проверок в единицах итераций текущего CPU. */
return (FLASH->SR & (FLASH_SR_BSY | NATIVE_FLASH_ERRORS)) == 0U; /* Не подтверждает операцию при BUSY или любой выбранной аппаратной ошибке. */
} /* Завершает проверку готовности Flash. */
static bool native_unlock(void) /* Открывает Flash только для текущей операции загрузчика. */
{ /* Геометрия профиля исключает массовое стирание и запись Option Bytes. */
if ((FLASH->SR & FLASH_SR_BSY) != 0U) { return false; } FLASH->SR = NATIVE_FLASH_ERRORS; /* Не посылает ключи занятой Flash и очищает предыдущие ошибки W1C. */
if ((FLASH->CR & FLASH_CR_LOCK) != 0U) { FLASH->KEYR = 0x45670123UL; FLASH->KEYR = 0xCDEF89ABUL; } /* Использует штатную последовательность ключей RM0090. */
return (FLASH->CR & FLASH_CR_LOCK) == 0U; /* Проверяет фактическое снятие блокировки Flash. */
} /* Завершает разблокировку контроллера Flash. */
static void native_lock(void) { FLASH->CR = FLASH_CR_LOCK; } /* Выключает PG/SER и восстанавливает LOCK после успеха и обработанных отказов. */
static void native_sync_cache(void) /* Убирает устаревшие строки Flash cache перед readback соседнего записанного слова. */
{ /* Сохраняет исходное включение instruction/data cache и остальные настройки ACR. */
uint32_t saved = FLASH->ACR; FLASH->ACR = saved & ~(FLASH_ACR_ICEN | FLASH_ACR_DCEN); /* Отключает кэши перед их аппаратным сбросом. */
FLASH->ACR |= FLASH_ACR_ICRST | FLASH_ACR_DCRST; FLASH->ACR &= ~(FLASH_ACR_ICRST | FLASH_ACR_DCRST); FLASH->ACR = saved; /* Сбрасывает обе cache-памяти и возвращает прежние enable/latency/prefetch настройки. */
__DSB(); __ISB(); /* Синхронизирует последующие чтения Flash и исполняемый поток после reset кэшей. */
} /* Завершает синхронизацию Flash cache без HAL. */
static bool native_program(uint32_t address, uint32_t value) /* Программирует и перечитывает одно слово x32. */
{ /* Границы и выравнивание проверяет общий writer до вызова. */
bool ok; FLASH->SR = NATIVE_FLASH_ERRORS; FLASH->CR = FLASH_CR_PG | (2UL << FLASH_CR_PSIZE_Pos); /* Выбирает PSIZE=2, то есть 32-битную запись при VDD 2,7..3,6 В. */
*(volatile uint32_t *)(uintptr_t)address = value; ok = native_wait(); FLASH->CR &= ~FLASH_CR_PG; native_sync_cache(); /* Дожидается завершения записи и сбрасывает устаревшие строки cache перед чтением. */
return ok && *(const volatile uint32_t *)(uintptr_t)address == value; /* Проверяет аппаратные флаги и реально прочитанное значение, включая дополнение FF. */
} /* Завершает одну аппаратную запись F407. */
static bool native_erase(uint32_t size) /* Стирает только используемые секторы 2..5 приложения climate. */
{ /* EEPROM-кольцо 6/7 и загрузочные 0/1 не включаются в таблицу разрешённых секторов. */
static const uint32_t starts[4] = {0x08008000UL, 0x0800C000UL, 0x08010000UL, 0x08020000UL}; /* RM0090: начала секторов 2,3,4,5 объёмом 16,16,64,128 КиБ. */
uint32_t index; bool ok = true; /* Хранит текущий сектор и результат до восстановления LOCK. */
if (size < 8U || size > NATIVE_APP_SIZE || !native_unlock()) return false; /* Отвергает неподдерживаемый размер до первого изменения Flash. */
for (index = 0U; index < 4U && starts[index] < NATIVE_APP_BASE + size; ++index) { /* Не стирает следующий сектор, когда образ заканчивается точно на его границе. */
(void)pcan_boot_report_busy(&boot, PCAN_BOOT_CMD_ERASE, ticks_ms); FLASH->SR = NATIVE_FLASH_ERRORS; /* Продлевает сессию и сообщает прогресс между длительными стираниями. */
FLASH->CR = FLASH_CR_SER | (2UL << FLASH_CR_PSIZE_Pos) | ((index + 2U) << FLASH_CR_SNB_Pos); FLASH->CR |= FLASH_CR_STRT; /* Запускает erase конкретного сектора с разрядностью x32. */
if (!native_wait()) { ok = false; break; } /* Прерывает обновление на первом аппаратном отказе. */
} /* Завершает обход разрешённых секторов. */
native_lock(); native_sync_cache(); return ok; /* Восстанавливает LOCK и обновляет cache как после успеха, так и после обработанного отказа. */
} /* Завершает аппаратное стирание F407. */
static void native_gpio_init(void) /* Сохраняет исходный CAN1 climate на PD0/PD1 AF9. */
{ /* Тактирование APB1 определяется общим кодом из фактического RCC_CFGR. */
RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; RCC->APB1ENR |= RCC_APB1ENR_CAN1EN; /* Разрешает аппаратные такты GPIOD и CAN1. */
GPIOD->MODER = (GPIOD->MODER & ~15UL) | 10UL; GPIOD->PUPDR = (GPIOD->PUPDR & ~3UL) | 1UL; /* Назначает PD0/PD1 альтернативной функцией и подтягивает RX к рецессивному уровню. */
GPIOD->OSPEEDR |= 15UL; GPIOD->AFR[0] = (GPIOD->AFR[0] & ~255UL) | 0x99UL; /* Выбирает высокую скорость GPIO и AF9 обоих выводов CAN1 согласно datasheet F407. */
} /* Завершает настройку физического CAN-порта. */
static void native_peripheral_stop(void) { RCC->APB1RSTR |= RCC_APB1RSTR_CAN1RST; RCC->APB1RSTR &= ~RCC_APB1RSTR_CAN1RST; } /* Сбрасывает CAN1 перед handoff без изменения памяти приложения и EEPROM. */
#include "../stm32-common/boot_native.inc" /* Подключает общий ProtoCAN transport, CRC, callbacks и безопасную замену MSP. */
int main(void) /* Самостоятельный загрузчик climate, подключаемый тонкой обёрткой исходного проекта. */
{ /* Сборка PCAN_BOOTLOADER_BUILD сохраняет VTOR startup в начале Flash. */
bool requested = *(volatile uint32_t *)(uintptr_t)PCAN_BOOT_REQUEST_ADDRESS == PCAN_BOOT_REQUEST_MAGIC; /* Читает запрос из отдельного слова SRAM, не используемого стеком или ZI. */
*(volatile uint32_t *)(uintptr_t)PCAN_BOOT_REQUEST_ADDRESS = 0U; /* Потребляет запрос ровно один раз до запуска интерфейсов. */
SystemCoreClockUpdate(); if (SysTick_Config(SystemCoreClock / 1000U) != 0U || !native_init()) for (;;) {} /* Использует фактический HCLK и создаёт millisecond tick до CAN-тайм-аутов. */
while (!native_can_init()) { if (!requested && native_vectors_valid(NATIVE_APP_SIZE)) native_jump(NATIVE_APP_BASE); native_peripheral_stop(); } /* При недоступной шине сохраняет обычный запуск установленного приложения, а по запросу остаётся восстанавливаемым. */
if (!requested && native_vectors_valid(NATIVE_APP_SIZE)) { /* Сохраняет исходное десятисекундное окно входа в обновление после обычного reset. */
uint32_t started = ticks_ms; while ((uint32_t)(ticks_ms - started) < 10000U) { native_can_poll(); if (boot.state != PCAN_BOOT_STATE_IDLE) { requested = true; break; } } /* Обслуживает discovery и команды без ложного входа по одному IDENTIFY. */
if (!requested) native_jump(NATIVE_APP_BASE); /* Запускает приложение после окна, используя безопасный naked handoff. */
} /* Завершает политику обычного запуска. */
for (;;) native_can_poll(); /* Остаётся в общем ProtoCAN-автомате до подтверждённого COMMIT/REBOOT. */
} /* Завершает самостоятельный CMSIS-загрузчик F407. */

View File

@@ -0,0 +1,8 @@
#ifndef PCAN_BOOT_F407_PROFILE_H /* Согласует нативный загрузчик climate и приложение с общей аппаратной разметкой. */
#define PCAN_BOOT_F407_PROFILE_H /* Предотвращает повторное объявление адресов и magic. */
#define PCAN_BOOT_APP_ADDRESS 0x08008000UL /* climate: секторы 0/1 общей ёмкостью 32 КиБ принадлежат загрузчику. */
#define PCAN_BOOT_FLASH_END 0x08040000UL /* climate: начало EEPROM-кольца в секторе 6 исключено из приложения. */
#define PCAN_BOOT_APP_SIZE (PCAN_BOOT_FLASH_END - PCAN_BOOT_APP_ADDRESS) /* Разрешённая ёмкость приложения составляет 224 КиБ в секторах 2..5. */
#define PCAN_BOOT_REQUEST_MAGIC 0x5043414EUL /* Сохраняет совместимый маркер PCAN одноразового запроса обновления. */
#define PCAN_BOOT_REQUEST_ADDRESS 0x2001FFFCUL /* Последнее слово основной SRAM F407; linker приложения и загрузчика должен исключать эти четыре байта. */
#endif /* Завершает общую аппаратную часть конфигурации climate. */

View File

@@ -0,0 +1,28 @@
#include "pcan_boot_stm32f407.h" /* Подключает HAL STM32F4 и публичный профиль climate. */
#include <string.h> /* Предоставляет инициализацию структур и чтение невыровненных октетов. */
#define STM32_BOOT_INIT pcan_boot_stm32f407_init /* Экспортирует семейное имя общей процедуры настройки. */
#define STM32_BOOT_POLL pcan_boot_stm32f407_poll /* Экспортирует семейное имя обслуживания FIFO и таймеров. */
#define STM32_BOOT_VECTORS pcan_boot_stm32f407_vectors_valid /* Экспортирует проверку векторов именно этого профиля. */
#define STM32_BOOT_BASE PCAN_BOOT_F407_APP_ADDRESS /* Сохраняет адрес приложения исходного загрузчика climate. */
#define STM32_BOOT_END PCAN_BOOT_F407_FLASH_END /* Не разрешает запись в EEPROM-кольцо секторов 6 и 7. */
#define STM32_BOOT_RAM_END 0x20020000UL /* STM32F407VE: верхняя исключённая граница основной SRAM объёмом 128 КиБ. */
#define STM32_BOOT_UNIT 4U /* Исходный F407-порт использует запись 32-битных слов при VDD 2,7..3,6 В. */
#define STM32_BOOT_PROGRAM FLASH_TYPEPROGRAM_WORD /* Согласует разрядность программирования с VOLTAGE_RANGE_3. */
#define STM32_BOOT_SYNC() FLASH_FlushCaches() /* HAL сохраняет состояние cache enable и сбрасывает устаревшие строки перед readback соседних записанных слов. */
static bool erase_region(pcan_boot_stm32_t *port, uint32_t size) /* Стирает последовательно только используемые секторы приложения. */
{ /* Общий код уже проверил размер и разблокировал Flash. */
static const uint32_t starts[] = {0x08008000UL, 0x0800C000UL, 0x08010000UL, 0x08020000UL}; /* RM0090: начала секторов 2..5 размером 16,16,64,128 КиБ. */
uint32_t index; /* Индекс сектора в разрешённой области приложения. */
for (index = 0U; index < 4U && starts[index] < STM32_BOOT_BASE + size; ++index) { /* Не стирает следующий сектор, если образ закончился на его границе. */
FLASH_EraseInitTypeDef erase = {0}; /* Обнуляет все поля аппаратной команды перед выбором сектора. */
uint32_t error = 0U; /* Получает номер сектора с ошибкой от HAL. */
(void)pcan_boot_report_busy(&port->boot, PCAN_BOOT_CMD_ERASE, HAL_GetTick()); /* Уведомляет клиента между длительными синхронными стираниями и продлевает сессию. */
erase.TypeErase = FLASH_TYPEERASE_SECTORS; /* Исключает массовое стирание загрузчика и EEPROM-кольца. */
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; /* Требует VDD 2,7..3,6 В для выбранного программирования x32. */
erase.Sector = FLASH_SECTOR_2 + index; /* Разрешает только физические секторы 2,3,4,5 STM32F407. */
erase.NbSectors = 1U; /* Между секторами возвращает управление для ответа BUSY. */
if (HAL_FLASHEx_Erase(&erase, &error) != HAL_OK) return false; /* Прекращает операцию на первой аппаратной ошибке. */
} /* Завершает обход секторов, покрывающих фактический образ. */
return true; /* Все выбранные секторы успешно стёрты. */
} /* Завершает аппаратную реализацию стирания F4. */
#include "../stm32-common/pcan_boot_stm32.inc" /* Собирает общий transport/readback с геометрией и HAL этого семейства. */

View File

@@ -0,0 +1,11 @@
#ifndef PCAN_BOOT_STM32F407_H /* Исключает повторное объявление порта STM32F407VE. */
#define PCAN_BOOT_STM32F407_H /* Обозначает активный интерфейс аппаратного профиля. */
#include "stm32f4xx_hal.h" /* Получает тип CAN_HandleTypeDef и драйверы Flash из STM32CubeF4. */
#include "boot_profile.h" /* Использует ту же аппаратную карту, что приложение и CMSIS-загрузчик climate. */
#include "../stm32-common/pcan_boot_stm32.h" /* Подключает состояние и API переносимого автомата. */
#define PCAN_BOOT_F407_APP_ADDRESS PCAN_BOOT_APP_ADDRESS /* Сохраняет публичное HAL-имя начала приложения без дублирования аппаратного адреса. */
#define PCAN_BOOT_F407_FLASH_END PCAN_BOOT_FLASH_END /* Сохраняет публичное HAL-имя границы приложения перед EEPROM-кольцом секторов 6/7. */
bool pcan_boot_stm32f407_init(pcan_boot_stm32_t *port, CAN_HandleTypeDef *can, const pcan_boot_config_t *config); /* Назначает single-slot callbacks, фильтры 0/1 в FIFO0 и запускает заранее настроенный CAN1. */
void pcan_boot_stm32f407_poll(pcan_boot_stm32_t *port); /* Обслуживает до трёх кадров FIFO0 и таймеры ядра из основного цикла. */
bool pcan_boot_stm32f407_vectors_valid(void); /* Проверяет MSP и Thumb-вектор в пределах профиля; CRC установленного образа не подтверждает. */
#endif /* Завершает интерфейс профиля STM32F407VE. */

View File

@@ -0,0 +1,34 @@
# Licenses
## candleLight Firmware Sources
The MIT License (MIT)
Copyright (c) 2016 Hubert Denkmair
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
## STM32 HAL
The HAL is distributed under a non-restrictive BSD (Berkeley Software
Distribution) license.
## STM32 USB Library
Code from the STM32 USB library is licensed
under ST STMicroelectronics
[Ultimate Liberty license SLA0044] (www.st.com/SLA0044)

View File

@@ -0,0 +1,63 @@
# STM32G474VET6: ProtoCAN Boot v1 / CANdle
Исходный проект: `https://git.rd12.ru/setcorp/CANdle_stm32.git`, commit
`61a93c5`. Аппаратный код извлечён из `src/boot/g474_can_bootloader.c`,
`src/can_boot_control.c` и `src/boot/g474_boot_startup.c`; локальная CAN state
machine заменена единым `src/pcan_boot.c`. Лицензия исходного проекта сохранена
в [`LICENSE.source.md`](LICENSE.source.md).
| Область | Начало | Конец, исключён | Размер |
|---|---|---|---:|
| Загрузчик | `0x08000000` | `0x08008000` | 32 КиБ |
| Single-slot приложение | `0x08008000` | `0x08080000` | 480 КиБ |
| RAM проекта | `0x20000000` | `0x20013FF8` | 80 КиБ − 8 байт |
| Одноразовый запрос CANB | `0x20013FFC` | `0x20014000` | 4 байта |
`boot_profile.h` содержит единые адреса/magic и совместимый API приложения.
Валидатор векторов допускает прежний MSP вплоть до физической границы
`0x20014000`; новые linker/startup исключают последние восемь байт, чтобы
не затереть запрос до чтения в main. RAM-COMMIT не сохраняется после питания.
## Состав и подключение
- `pcan_boot_stm32g474.c/.h` — reusable HAL FDCAN/Flash-порт с single-slot,
независимым CRC readback, doubleword записью и проверкой TXBTO перед reset.
- `boot_main.c` — самостоятельный CAN/UART-загрузчик CANdle, без собственной
реализации wire-протокола; использует `pcan_boot` и общий `rs485-boot`.
- `boot_startup.c` — исходная схема Keil/GCC startup с безопасным initial MSP.
- `boot_control.c` — вход из приложения по адресному ENTER_BOOT или UART BEGIN,
совместимый UART PING/ACK и исходный USART1 IRQ.
В проекте CANdle старые файлы стали include-обёртками из submodule `templates`.
BOOT Keil и CMake компилируют дополнительно `pcan_boot.c` и
`pcan_boot_stm32g474.c`. Приложение использует общий `boot_control.c` без
подключения загрузочной state machine к USB–CAN приложению.
Board-проект предоставляет `device.h`, `device_sysclock_config`,
`HAL_FDCAN_MspInit`, CMSIS/HAL и linker. Исходная плата использует CPU 144 МГц,
FDCAN kernel 48 МГц, FDCAN1 PB8/PB9 AF9, classic CAN 500 кбит/с,
DeviceType=7 / DeviceID=15. UART — PA9/PA10 AF7, 512000 бод, TTL без DE.
Для reusable API вызовите `HAL_FDCAN_Init` с двумя extended-фильтрами,
затем `pcan_boot_stm32g474_init`, затем регулярно `poll` из main.
FDCAN и его TX FIFO должны принадлежать этому порту, без конкурирующих TX IRQ.
## Flash и протокол
Поддерживается реальная плотность 512 КиБ. DBANK=1 использует два банка по
256 КиБ и страницы 2 КиБ; DBANK=0 — один банк 512 КиБ и страницы 4 КиБ.
Номера страниц считаются отдельно для каждого банка, с проверкой всей
округлённой области до erase. BFB2 и SYSCFG FB_MODE bank swap отвергаются;
Option Bytes не изменяются. Другие плотности требуют собственного профиля.
Flash программируется doubleword по восемь байт. Неполный doubleword
дополняется FF только в конце образа; повторная запись ECC-слова в середине
потока не допускается. UART использует тот же writer, что CAN DATA.
VERIFY перечитывает Flash, COMMIT повторяет проверку, REBOOT возможен только
после COMMIT и физически подтверждённого TX. Кодирование DLC определяется
константами используемой версии HAL; старый сдвиг 16 и новый сдвиг 0 не смешиваются.
Стирание синхронное. Клиент должен допускать длительное Flash stall; BUSY до
и после erase поддерживает сессию. Постоянного manifest, A/B-rollback, resume
после питания и аутентификации в исходном single-slot профиле нет.
[Подключённые проекты и проверки Keil/CMake](../../projects/STM32.md).

View File

@@ -0,0 +1,48 @@
#include "boot_profile.h" /* Получает единственный источник адресов, magic и функций входа CANdle G474. */
#include "stm32g4xx.h" /* Использует исходный CMSIS-порт USART/SRAM без зависимости приложения от загрузочного main. */
static volatile bool request; /* IRQ либо CAN-диспетчер отмечает запрос, который основной цикл может безопасно обработать. */
static uint8_t frame[28], used; /* Кадр BEGIN содержит 8 байт заголовка, 16 байт payload и 4 байта CRC, всего 28 байт. */
static uint32_t read32(const uint8_t *data) { return (uint32_t)data[0] | ((uint32_t)data[1] << 8U) | ((uint32_t)data[2] << 16U) | ((uint32_t)data[3] << 24U); } /* Извлекает little-endian CRC без невыровненного чтения. */
static uint32_t crc32(const uint8_t *data, uint8_t length) /* Малый app-only codec сохраняет отсутствие зависимости приложения от boot-автомата. */
{ /* Формат PING/BEGIN совпадает с SETGUI v1, исходный полином IEEE отражённый. */
uint32_t crc = 0xFFFFFFFFUL; uint8_t index, bit; /* Начинает IEEE CRC32 с инверсного состояния, как исходное приложение CANdle. */
for (index = 0U; index < length; ++index) { crc ^= data[index]; for (bit = 0U; bit < 8U; ++bit) crc = (crc >> 1U) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U); } /* Обрабатывает только проверенные октеты и восемь бит каждого байта. */
return ~crc; /* Выполняет финальный XOR, совместимый с zlib и загрузчиком SETGUI v1. */
} /* Завершает совместимый малый CRC приложения. */
void can_boot_observe(uint32_t id, bool extended, uint8_t dlc) /* Не переводит приложение в обновление по чужому либо невалидному кадру. */
{ /* Переход откладывается до вызова can_boot_enter из существующего main. */
if (extended && dlc == 0U && id <= 0x1FFFFFFFUL && ((id >> 27U) & 1U) == 0U && ((id >> 24U) & 7U) == CAN_BOOT_DEVICE_TYPE && ((id >> 20U) & 15U) == CAN_BOOT_DEVICE_ID && ((id >> 16U) & 15U) == 9U && (id & 255U) == 2U && ((id >> 8U) & 255U) != 0U) request = true; /* Требует Route=0, собственный адрес, BOOT_CONTROL/ENTER_BOOT и ненулевую сессию. */
} /* Завершает наблюдение входящего CAN без чтения отсутствующего payload. */
bool can_boot_requested(void) { return request; } /* Сохраняет прежний API опроса приложения CANdle. */
void can_boot_enter(void) { *(volatile uint32_t *)(uintptr_t)CAN_BOOT_REQUEST_ADDRESS = CAN_BOOT_REQUEST_MAGIC; __DSB(); NVIC_SystemReset(); } /* Записывает CANB в зарезервированное SRAM-слово до системного сброса. */
static void process_frame(void) /* Принимает только проверенные короткие PING и BEGIN приложения. */
{ /* Полный firmware flow после BEGIN обслуживает уже общий загрузчик из templates. */
uint8_t payload = frame[7]; uint32_t checksum; /* Размер payload предварительно ограничен шестнадцатью байтами. */
if (frame[2] != 1U || read32(frame + 8U + payload) != crc32(frame + 2U, (uint8_t)(6U + payload))) { return; } /* Не выполняет reset по кадру другой версии либо повреждённому CRC. */
if (frame[3] == 0x0BU && payload == 16U) { can_boot_enter(); return; } /* Сохраняет существующий SETGUI FIRMWARE_BEGIN как запрос входа в загрузчик. */
if (frame[3] == 1U && payload == 0U) { /* PING подтверждает наличие UART приложения без входа в обновление. */
uint8_t reply[15] = {0xA5U, 0x5AU, 1U, 0x81U, frame[4], frame[5], 0U, 3U, 0U, 0U, 1U}; uint8_t index; /* Формирует прежний ACK PING с номером входящего запроса и трёхбайтовым результатом. */
checksum = crc32(reply + 2U, 9U); for (index = 0U; index < 4U; ++index) reply[11U + index] = (uint8_t)(checksum >> (index * 8U)); /* Добавляет CRC32 LE после девяти защищаемых байтов ver/header/payload. */
for (index = 0U; index < sizeof(reply); ++index) { while ((USART1->ISR & USART_ISR_TXE_TXFNF) == 0U) {} USART1->TDR = reply[index]; } /* Отправляет полный совместимый ответ USART1. */
while ((USART1->ISR & USART_ISR_TC) == 0U) {} USART1->ICR = USART_ICR_TCCF; /* Дожидается последнего стоп-бита и очищает флаг завершения передачи. */
} /* Остальные сообщения приложения не относятся к входу в boot и игнорируются. */
} /* Завершает обработку валидного короткого кадра приложения. */
void can_boot_uart_init(void) /* Сохраняет исходную разводку и IRQ-приём UART CANdle G474. */
{ /* Этот код компилируется только в работающем приложении с CONFIG_CAN_BOOT_APP. */
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN; RCC->APB2ENR |= RCC_APB2ENR_USART1EN; /* Включает GPIOA и USART1 в соответствующих clock-доменах G474. */
GPIOA->MODER = (GPIOA->MODER & ~((3UL << 18U) | (3UL << 20U))) | (2UL << 18U) | (2UL << 20U); /* Переводит PA9 и PA10 в режим альтернативной функции. */
GPIOA->AFR[1] = (GPIOA->AFR[1] & ~((15UL << 4U) | (15UL << 8U))) | (7UL << 4U) | (7UL << 8U); /* Назначает AF7 USART1 TX/RX согласно исходной плате CANdle. */
USART1->BRR = (SystemCoreClock + 256000UL) / 512000UL; USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE | USART_CR1_UE; /* Сохраняет 512000 бод при исходном APB2=HCLK и приём по RXNE IRQ. */
NVIC_SetPriority(USART1_IRQn, 2U); NVIC_EnableIRQ(USART1_IRQn); used = 0U; /* Сохраняет приоритет 2 исходного UART и очищает parser перед приёмом. */
} /* Завершает настройку входа приложения через UART. */
void can_boot_uart_poll(void) /* Принимает один октет, проверяет длину и синхронизацию до записи в малый буфер. */
{ /* Длина 28 байт покрывает только PING и BEGIN; больших команд здесь нет. */
uint32_t status = USART1->ISR; uint8_t byte, total; /* Сохраняет флаги USART до чтения RDR. */
if ((status & (USART_ISR_ORE | USART_ISR_FE | USART_ISR_NE)) != 0U) { USART1->ICR = USART_ICR_ORECF | USART_ICR_FECF | USART_ICR_NECF; used = 0U; return; } /* Сбрасывает parser после аппаратной потери либо повреждения байта. */
if ((status & USART_ISR_RXNE_RXFNE) == 0U) { return; } byte = (uint8_t)USART1->RDR; /* Не читает RDR без доступного октета. */
if (used == 0U && byte != 0xA5U) { return; } if (used == 1U && byte != 0x5AU) { used = byte == 0xA5U ? 1U : 0U; return; } /* Ищет двухбайтовую сигнатуру A5 5A с сохранением возможного начала следующего кадра. */
frame[used++] = byte; if (used < 8U) { return; } /* Накапливает только ограниченный буфер и ждёт полного восьмибайтового заголовка. */
if (frame[6] != 0U || frame[7] > 16U) { used = 0U; return; } /* Запрещает длину больше шестнадцати байтов до дальнейшего накопления. */
total = (uint8_t)(12U + frame[7]); if (used < total) { return; } process_frame(); used = 0U; /* Передаёт полный header/payload/CRC обработчику и начинает следующий короткий кадр. */
} /* Завершает безопасный parser приложения. */
void USART1_IRQHandler(void) { can_boot_uart_poll(); } /* Сохраняет прежний экспорт IRQ, используемый startup приложения CANdle. */

View File

@@ -0,0 +1,66 @@
#include "pcan_boot_stm32g474.h" /* Подключает FDCAN/Flash порт и единственную ProtoCAN state machine templates. */
#include "../../../rs485-boot/include/rs485_boot.h" /* Повторно использует общий SETGUI v1 UART updater. */
#include "device.h" /* Тактирование и GPIO FDCAN остаются аппаратной привязкой исходного board-проекта. */
#include <string.h> /* Копирует проверенные диапазоны Flash для повторов UART-блоков. */
static FDCAN_HandleTypeDef can; static pcan_boot_stm32_t updater; static rs485_boot_t uart_boot; static bool can_started; /* Сохраняет состояния двух транспортов и факт успешного аппаратного запуска FDCAN. */
static __attribute__((naked, noreturn)) void enter_app(uint32_t sp __attribute__((unused)), uint32_t pc __attribute__((unused))) /* Не обращается к старому C-стеку после замены MSP. */
{ /* AAPCS передаёт исходный MSP приложения в r0 и Thumb entry в r1. */
__asm volatile("msr msp, r0\n" "movs r2, #0\n" "msr control, r2\n" "dsb\n" "isb\n" "cpsie i\n" "bx r1\n"); /* Согласует привилегированный режим и конвейер до безвозвратного перехода по новому вектору. */
} /* Завершает naked handoff без эпилога C. */
static void jump_app(void) /* Отключает загрузочные интерфейсы перед запуском установленного приложения. */
{ /* Структурная проверка векторов выполнена вызывающим main. */
const uint32_t *vectors = (const uint32_t *)(uintptr_t)CAN_BOOT_APP_ADDRESS; uint32_t sp = vectors[0], pc = vectors[1], index; /* Сохраняет новые значения до отключения прерываний и смены стека. */
__disable_irq(); SysTick->CTRL = 0U; USART1->CR1 = 0U; if (can_started) (void)HAL_FDCAN_Stop(&can); /* Не допускает работы загрузочного UART/CAN и SysTick после handoff. */
for (index = 0U; index < 8U; ++index) { NVIC->ICER[index] = 0xFFFFFFFFUL; NVIC->ICPR[index] = 0xFFFFFFFFUL; } /* Снимает внешние enable/pending IRQ записью W1C. */
SCB->ICSR = SCB_ICSR_PENDSTCLR_Msk | SCB_ICSR_PENDSVCLR_Msk; SCB->VTOR = CAN_BOOT_APP_ADDRESS; /* Очищает системные pending IRQ и согласует VTOR с linker приложения. */
enter_app(sp, pc); /* Передаёт управление без обращения к старому C-стеку. */
} /* Завершает аппаратную подготовку запуска приложения. */
static bool uart_transmit(void *user, const uint8_t *data, uint16_t length) /* Передаёт совместимые ответы SETGUI v1 через USART1. */
{ /* Транспорт TTL не управляет DE внешнего RS-485 трансивера. */
uint16_t index; (void)user; for (index = 0U; index < length; ++index) { while ((USART1->ISR & USART_ISR_TXE_TXFNF) == 0U) {} USART1->TDR = data[index]; } /* Передаёт полный сформированный кадр без чтения за его длиной. */
while ((USART1->ISR & USART_ISR_TC) == 0U) {} USART1->ICR = USART_ICR_TCCF; return true; /* Дожидается последнего стоп-бита до возможного reset после END. */
} /* Завершает синхронную UART передачу. */
static bool uart_erase(void *user, uint32_t size) { (void)user; return updater.boot.state == PCAN_BOOT_STATE_IDLE && pcan_boot_stm32g474_erase(size); } /* Не позволяет UART стереть Flash при открытой CAN-сессии. */
static bool uart_write(void *user, uint32_t offset, const uint8_t *data, uint16_t length) { (void)user; return pcan_boot_stm32g474_write(uart_boot.image_size, offset, data, length); } /* Использует тот же doubleword writer, что CAN DATA. */
static bool uart_read(void *user, uint32_t offset, uint8_t *data, uint16_t length) /* Подтверждает повторы UART-блоков чтением сохранённых данных. */
{ /* Проверяет размеры вычитанием, чтобы не переполнить адресное сложение. */
(void)user; if (data == NULL || offset > CAN_BOOT_APP_SIZE || length > CAN_BOOT_APP_SIZE - offset) { return false; } /* Не допускает чтение за пределами 480-КиБ области. */
(void)memcpy(data, (const void *)(uintptr_t)(CAN_BOOT_APP_ADDRESS + offset), length); return true; /* Возвращает только проверенный диапазон Flash. */
} /* Завершает readback повторного UART-запроса. */
static bool uart_valid(void *user, uint32_t size, uint32_t crc) { (void)user; return pcan_boot_stm32g474_image_valid(size, crc); } /* Перед END проверяет векторы и настоящий CRC Flash общим портом. */
static void uart_reboot(void *user) { (void)user; NVIC_SystemReset(); } /* UART callback вызывается после физически завершённого ответа END. */
static void uart_init(void) /* Сохраняет исходные PA9/PA10 AF7, 512000 бод и общий SETGUI v1 updater. */
{ /* Аппаратные частоты уже настроены board-функцией device_sysclock_config. */
GPIO_InitTypeDef gpio = {0}; rs485_boot_config_t config = {CAN_BOOT_APP_ADDRESS, CAN_BOOT_APP_SIZE, RS485_BOOT_MAX_BLOCK}; rs485_boot_port_t callbacks = {0}; /* Полностью определяет структуры текущих HAL и UART API. */
__HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART1_CLK_ENABLE(); gpio.Pin = GPIO_PIN_9 | GPIO_PIN_10; gpio.Mode = GPIO_MODE_AF_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate = GPIO_AF7_USART1; HAL_GPIO_Init(GPIOA, &gpio); /* Назначает исходные TX/RX выводы без изменения CAN PB8/PB9. */
USART1->BRR = (HAL_RCC_GetPCLK2Freq() + 256000UL) / 512000UL; USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_FIFOEN | USART_CR1_UE; /* Рассчитывает скорость от фактического PCLK2 и включает polling UART с FIFO. */
callbacks.transmit = uart_transmit; callbacks.erase = uart_erase; callbacks.write = uart_write; callbacks.read = uart_read; callbacks.image_valid = uart_valid; callbacks.reboot = uart_reboot; /* Подключает общий Flash writer и проверку образа вместо локальной реализации UART flow. */
(void)rs485_boot_init(&uart_boot, &config, &callbacks, NULL); /* Создаёт отдельную RAM-сессию UART с прежним wire-протоколом. */
} /* Завершает настройку UART восстановления. */
static bool fdcan_init(void) /* Сохраняет исходные 48 МГц kernel clock и скорость classic CAN 500 кбит/с. */
{ /* GPIO FDCAN PB8/PB9 AF9 включает board-функция HAL_FDCAN_MspInit. */
pcan_boot_config_t config = {0}; can.Instance = FDCAN1; can.Init.ClockDivider = FDCAN_CLOCK_DIV1; can.Init.FrameFormat = FDCAN_FRAME_CLASSIC; can.Init.Mode = FDCAN_MODE_NORMAL; can.Init.AutoRetransmission = ENABLE; /* Выбирает единственный выделенный FDCAN1 без CAN FD/BRS и с автоматическим повтором TX. */
can.Init.NominalPrescaler = 6U; can.Init.NominalSyncJumpWidth = 2U; can.Init.NominalTimeSeg1 = 13U; can.Init.NominalTimeSeg2 = 2U; /* 48 МГц / 6 / (1+13+2) дают ровно 500000 бит/с. */
can.Init.DataPrescaler = 6U; can.Init.DataSyncJumpWidth = 2U; can.Init.DataTimeSeg1 = 13U; can.Init.DataTimeSeg2 = 2U; /* Полностью инициализирует data timing, хотя в classic режиме он не используется. */
can.Init.StdFiltersNbr = 0U; can.Init.ExtFiltersNbr = 2U; can.Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; /* Выделяет адресный и broadcast-discovery фильтры общему порту. */
if (HAL_FDCAN_Init(&can) != HAL_OK) { return false; } config.device_type = CAN_BOOT_DEVICE_TYPE; config.device_id = CAN_BOOT_DEVICE_ID; config.ack_window = 16U; config.session_timeout_ms = 30000UL; /* Сохраняет исходный адрес 7:15 и окно ACK, добавляя тайм-аут RAM-сессии 30 секунд. */
return pcan_boot_stm32g474_init(&updater, &can, &config); /* Инициализирует единую state machine ProtoCAN и проверяет геометрию Flash. */
} /* Завершает настройку FDCAN без локальной копии CAN-протокола. */
static void service_interfaces(void) /* Обслуживает оба транспорта без одновременной записи одной Flash-области. */
{ /* Каждый вызов ограничивает обработку UART шестьюдесятью четырьмя октетами. */
uint32_t count; for (count = 0U; count < 64U; ++count) { /* Не позволяет постоянному UART-потоку захватить главный цикл целиком. */
uint32_t status = USART1->ISR; uint8_t byte; if ((status & (USART_ISR_ORE | USART_ISR_FE | USART_ISR_NE)) != 0U) USART1->ICR = USART_ICR_ORECF | USART_ICR_FECF | USART_ICR_NECF; /* Снимает аппаратные ошибки USART перед дальнейшим приёмом. */
if ((status & USART_ISR_RXNE_RXFNE) == 0U) { break; } byte = (uint8_t)USART1->RDR; rs485_boot_feed(&uart_boot, &byte, 1U); /* Передаёт доступный октет общему UART parser. */
} /* Завершает ограниченный пакет UART. */
if (can_started && uart_boot.state != RS485_BOOT_RECEIVING) pcan_boot_stm32g474_poll(&updater); /* Во время записи UART запрещает второй транспорт, в остальных состояниях обслуживает CAN и discovery. */
} /* Завершает мультиплексирование загрузочных интерфейсов. */
int main(void) /* Подключает исходный board startup CANdle к общим загрузочным библиотекам templates. */
{ /* Linker и initial MSP должны исключать последние восемь байт SRAM со словом запроса. */
bool requested = *(volatile uint32_t *)(uintptr_t)CAN_BOOT_REQUEST_ADDRESS == CAN_BOOT_REQUEST_MAGIC; *(volatile uint32_t *)(uintptr_t)CAN_BOOT_REQUEST_ADDRESS = 0U; /* Потребляет сохранённый запрос CANB до настройки периферии. */
HAL_Init(); device_sysclock_config(); uart_init(); can_started = fdcan_init(); /* Сохраняет исходную последовательность clock/GPIO/UART/FDCAN и доступность UART при отказе CAN. */
if (!requested && pcan_boot_stm32g474_vectors_valid(CAN_BOOT_APP_SIZE)) { /* Сохраняет исходное десятисекундное окно входа в обновление. */
uint32_t started = HAL_GetTick(); while ((uint32_t)(HAL_GetTick() - started) < 10000U) { service_interfaces(); if (updater.boot.state != PCAN_BOOT_STATE_IDLE || uart_boot.state != RS485_BOOT_IDLE) { requested = true; break; } } /* Обслуживает UART/CAN с учётом переполнения tick, без входа в обновление по одному discovery. */
if (!requested) jump_app(); /* Передаёт управление установленному приложению после окна ожидания. */
} /* Завершает обычную политику запуска без постоянного manifest. */
for (;;) service_interfaces(); /* Остаётся доступным для адресного обновления и восстановления через UART. */
} /* Завершает общий boot main STM32G474VET6. */

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