Compare commits
22 Commits
qcustomplo
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f17c124f0e | |||
| 2da7e7b525 | |||
| 62c0492bfd | |||
| 5bf3167064 | |||
| c6390ae1c9 | |||
| 876b58517c | |||
| 6ecf057a86 | |||
| d42f2e1622 | |||
| 0c4eab4615 | |||
| 5af8a8e9c8 | |||
| f613101124 | |||
| cf65439400 | |||
| 32e2fbedc0 | |||
| 226c2ca599 | |||
| f2956b54cd | |||
| eab8dc5e0a | |||
| 131f306131 | |||
| 4fc0ad4e14 | |||
| da055c2787 | |||
| 8fc2e1b5cf | |||
| b5b2d1ae37 | |||
| 7dfa8bd94d |
@@ -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 и конкретной разводки адаптера; переносимая логика протоколов
|
||||
|
||||
@@ -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 |
|
||||
|
||||
30
c/firmware-info/tests/test_c2000_build.py
Normal file
30
c/firmware-info/tests/test_c2000_build.py
Normal 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.
|
||||
87
c/firmware-info/tests/test_c2000_postbuild.py
Normal file
87
c/firmware-info/tests/test_c2000_postbuild.py
Normal 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() # Возвращает ненулевой код при нарушении контракта упаковки или экспорта.
|
||||
111
c/firmware-info/tests/test_identity_archive.py
Normal file
111
c/firmware-info/tests/test_identity_archive.py
Normal 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()
|
||||
83
c/firmware-info/tests/test_version_iterator.py
Normal file
83
c/firmware-info/tests/test_version_iterator.py
Normal 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() # Любое нарушение общего итератора возвращает ненулевой код процесса.
|
||||
436
c/firmware-info/tools/c2000_build.py
Normal file
436
c/firmware-info/tools/c2000_build.py
Normal 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()) # Возвращает командной оболочке фактический результат подготовки.
|
||||
119
c/firmware-info/tools/c2000_postbuild.py
Normal file
119
c/firmware-info/tools/c2000_postbuild.py
Normal 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())
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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`; приложения подключают ревизию сабмодулем.
|
||||
|
||||
21
c/global-time/CMakeLists.txt
Normal file
21
c/global-time/CMakeLists.txt
Normal 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
167
c/global-time/README.md
Normal 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. Полная линковка конкретной прошивки и проверка на плате
|
||||
в этот набор не входят.
|
||||
65
c/global-time/include/global_time.h
Normal file
65
c/global-time/include/global_time.h
Normal 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 /* Завершает защиту публичного заголовка. */
|
||||
168
c/global-time/ports/tms320f28335/global_time_f28335.c
Normal file
168
c/global-time/ports/tms320f28335/global_time_f28335.c
Normal 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 = >_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. */
|
||||
} /* Завершает необязательную остановку аппаратных часов. */
|
||||
19
c/global-time/ports/tms320f28335/global_time_f28335.h
Normal file
19
c/global-time/ports/tms320f28335/global_time_f28335.h
Normal 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 /* Завершает защиту заголовка порта. */
|
||||
156
c/global-time/src/global_time.c
Normal file
156
c/global-time/src/global_time.c
Normal 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; /* Подтверждает наличие календарной синхронизации в исходной метке. */
|
||||
} /* Завершает календарное представление метки логгера. */
|
||||
26
c/global-time/tests/f28335_stubs/DSP2833x_Device.h
Normal file
26
c/global-time/tests/f28335_stubs/DSP2833x_Device.h
Normal 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 /* Завершает защиту тестовой модели аппаратуры. */
|
||||
36
c/global-time/tests/run_tests.py
Normal file
36
c/global-time/tests/run_tests.py
Normal 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") # Подтверждает именно компиляцию объектов, не аппаратные испытания или полную линковку прошивки.
|
||||
88
c/global-time/tests/test_f28335.c
Normal file
88
c/global-time/tests/test_f28335.c
Normal 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. */
|
||||
145
c/global-time/tests/test_global_time.c
Normal file
145
c/global-time/tests/test_global_time.c
Normal 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-тест только после всех обязательных проверок. */
|
||||
} /* Завершает независимую проверку переносимого ядра. */
|
||||
@@ -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 *`; библиотека
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
8
c/protocan-boot/.gitignore
vendored
Normal 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__/
|
||||
@@ -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 по-прежнему собирает только переносимое ядро.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
109
c/protocan-boot/ports/stm32-common/README.md
Normal file
109
c/protocan-boot/ports/stm32-common/README.md
Normal 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.
|
||||
127
c/protocan-boot/ports/stm32-common/boot_native.inc
Normal file
127
c/protocan-boot/ports/stm32-common/boot_native.inc
Normal 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); } /* Повторно проверяет формат внешнего кадра и вызывает общий автомат. */
|
||||
} /* Завершает ограниченный пакет входящих кадров. */
|
||||
} /* Завершает обслуживание транспорта и часов ядра. */
|
||||
10
c/protocan-boot/ports/stm32-common/pcan_boot_stm32.h
Normal file
10
c/protocan-boot/ports/stm32-common/pcan_boot_stm32.h
Normal 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. */
|
||||
167
c/protocan-boot/ports/stm32-common/pcan_boot_stm32.inc
Normal file
167
c/protocan-boot/ports/stm32-common/pcan_boot_stm32.inc
Normal 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); /* Передаёт адресные команды и восьмибайтовые блоки общему автомату. */
|
||||
} /* Завершает обработку допустимого формата кадра. */
|
||||
} /* Возвращает управление приложению после ограниченного пакета входящих кадров. */
|
||||
} /* Завершает обслуживание транспорта и таймеров. */
|
||||
50
c/protocan-boot/ports/stm32f103/README.md
Normal file
50
c/protocan-boot/ports/stm32f103/README.md
Normal 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).
|
||||
95
c/protocan-boot/ports/stm32f103/boot_main.c
Normal file
95
c/protocan-boot/ports/stm32f103/boot_main.c
Normal 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. */
|
||||
} /* Завершает самостоятельный загрузчик; управление остаётся в сервисном цикле до успешного обновления. */
|
||||
17
c/protocan-boot/ports/stm32f103/boot_profile.h
Normal file
17
c/protocan-boot/ports/stm32f103/boot_profile.h
Normal 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 для загрузчика и приложения. */
|
||||
12
c/protocan-boot/ports/stm32f103/boot_request.c
Normal file
12
c/protocan-boot/ports/stm32f103/boot_request.c
Normal 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. */
|
||||
22
c/protocan-boot/ports/stm32f103/pcan_boot_stm32f103.c
Normal file
22
c/protocan-boot/ports/stm32f103/pcan_boot_stm32f103.c
Normal 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 этого семейства. */
|
||||
11
c/protocan-boot/ports/stm32f103/pcan_boot_stm32f103.h
Normal file
11
c/protocan-boot/ports/stm32f103/pcan_boot_stm32f103.h
Normal 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. */
|
||||
51
c/protocan-boot/ports/stm32f407/README.md
Normal file
51
c/protocan-boot/ports/stm32f407/README.md
Normal 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).
|
||||
29
c/protocan-boot/ports/stm32f407/boot_control_hal.c
Normal file
29
c/protocan-boot/ports/stm32f407/boot_control_hal.c
Normal 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; /* Позволяет тестовой модели и старому диспетчеру отметить обработанный запрос. */
|
||||
} /* Завершает безопасную проверку входа в загрузчик. */
|
||||
7
c/protocan-boot/ports/stm32f407/boot_control_hal.h
Normal file
7
c/protocan-boot/ports/stm32f407/boot_control_hal.h
Normal 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. */
|
||||
68
c/protocan-boot/ports/stm32f407/boot_main.c
Normal file
68
c/protocan-boot/ports/stm32f407/boot_main.c
Normal 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. */
|
||||
8
c/protocan-boot/ports/stm32f407/boot_profile.h
Normal file
8
c/protocan-boot/ports/stm32f407/boot_profile.h
Normal 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. */
|
||||
28
c/protocan-boot/ports/stm32f407/pcan_boot_stm32f407.c
Normal file
28
c/protocan-boot/ports/stm32f407/pcan_boot_stm32f407.c
Normal 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 этого семейства. */
|
||||
11
c/protocan-boot/ports/stm32f407/pcan_boot_stm32f407.h
Normal file
11
c/protocan-boot/ports/stm32f407/pcan_boot_stm32f407.h
Normal 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. */
|
||||
34
c/protocan-boot/ports/stm32g474/LICENSE.source.md
Normal file
34
c/protocan-boot/ports/stm32g474/LICENSE.source.md
Normal 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)
|
||||
63
c/protocan-boot/ports/stm32g474/README.md
Normal file
63
c/protocan-boot/ports/stm32g474/README.md
Normal 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).
|
||||
48
c/protocan-boot/ports/stm32g474/boot_control.c
Normal file
48
c/protocan-boot/ports/stm32g474/boot_control.c
Normal 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. */
|
||||
66
c/protocan-boot/ports/stm32g474/boot_main.c
Normal file
66
c/protocan-boot/ports/stm32g474/boot_main.c
Normal 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
Reference in New Issue
Block a user