Раздел генерируется из doc/CAN_FRAME_PARSE_V1_V2.md. В нём собраны wire-разметка, примеры и готовые Python-парсеры.
+
+
+
Разбор CAN-кадров ProtoCAN Boot v1 и SETProtocol v2
+
Документ описывает wire-форматы двух протоколов обновления прошивки:
+
+
v1 — templates/c/protocan-boot, одна команда или 8 байт образа в одном
+Extended CAN-кадре;
+
v2 — templates/c/set-protocol, полный кадр SETProtocol разбивается на
+несколько Extended CAN-кадров.
+
+
Все многобайтные поля payload передаются little-endian. CAN ID — 29-битный.
+Для рабочего кода нужно использовать канонические реализации из templates,
+а приведённый ниже Python-парсер удобен для анализатора, логов и отладки.
+
1. ProtoCAN Boot v1
+
1.1. Разметка Extended CAN ID
+
bits size field
+28 1 Priority
+27 1 Route: 0 = host -> device, 1 = device -> host
+26..24 3 Device Type
+23..20 4 Device ID
+19..16 4 Message Type
+15..0 16 Message Body
+
В v2 CAN-кадр является только транспортным сегментом. Сначала нужно собрать
+полный SETP-пакет, и только затем разбирать его заголовок, payload и CRC32.
N — циклический номер сегмента 1..15,0..; следующий сегмент обязан иметь
+ожидаемый номер, тот же CAN ID и прийти до тайм-аута сборки 500 мс.
+
2.3. Внутренний кадр SETProtocol v2
+
offset size field
+0 2 SOF = A5 5A
+2 1 version = 02
+3 1 flags
+4 2 message_type u16 LE
+6 2 source u16 LE
+8 2 destination u16 LE
+10 2 sequence u16 LE
+12 2 payload_length u16 LE
+14 N payload
+14+N 4 CRC32 IEEE u32 LE
+
+
CRC32 считается по байтам от version на offset 2 до конца payload. Поля
+source, destination и priority внутреннего заголовка должны совпадать с
+CAN ID.
+
Флаги:
+
+
+
+
Бит
+
Значение
+
+
+
+
+
0x01
+
RESPONSE
+
+
+
0x02
+
EVENT
+
+
+
0x04
+
ERROR
+
+
+
0x08
+
ACK_REQUIRED
+
+
+
0x10
+
MORE
+
+
+
0x20
+
PRIORITY
+
+
+
+
Каждый response начинается с status u16 LE. Основные firmware message types:
+FW_BEGIN=0x0100, FW_DATA=0x0101, FW_END=0x0102, FW_ABORT=0x0103,
+FW_STATUS=0x0104, FW_ACTIVATE=0x0105.
+
Пример PING к BALZAM node 13, source 0, sequence 1, priority 1,
+channel 1:
В SETGUI эти операции уже реализованы в
+third_party/templates/python/setprotocol/can.py; собственный parser нужен
+только внешнему анализатору или диагностическому скрипту.
+
3. Как отличать v1 от v2
+
Для используемых сейчас адресов достаточно следующих признаков:
+
+
v2: верхние пять бит CAN ID равны 0x12, PCI начинается с 0x10, 0x2N
+или 0x3S, после reassembly присутствует A5 5A 02;
+
v1: MessageType в битах 19..16 равен 0x9..0xD, каждый кадр разбирается
+самостоятельно.
+
+
Однако универсальное автоопределение только по одному CAN ID невозможно:
+комбинация Priority/Route/DeviceType v1 теоретически тоже может дать верхнее
+поле 0x12, а первый байт firmware data v1 может случайно совпасть с PCI.
+Надёжный анализатор должен учитывать настроенный режим узла либо подтвердить v2
+только после сборки кадра с корректными A5 5A 02, длиной и CRC32.
+
4. Канонические исходники
+
+
v1 ID и state machine: third_party/templates/c/protocan-boot/src/pcan_boot.c;
+
v2 CAN transport: third_party/templates/c/set-protocol/src/set_can.c;
Этот раздел генерируется из канонического c/set-protocol/docs/SETPROTOCOL.md. Редактировать нужно Markdown, затем запускать doc/build-setprotocol-html.ps1.
@@ -159,7 +631,7 @@ COM │ SLCAN │ SocketCAN │ USB CDC │ TCP │ STM32 UART/CAN
последовательного моста, а 500 kbit/s в CAN-настройках — bitrate самой CAN-шины.
Ядро не подменяет одно другим и не выбирает эти значения автоматически.
2. Состав исходников
-
+
Модуль
@@ -234,7 +706,7 @@ COM │ SLCAN │ SocketCAN │ USB CDC │ TCP │ STM32 UART/CAN
ABI компилятора C
-
+
Общая точка включения для C-кода — include/setprotocol.h.
Иностранные runtimes должны использовать include/setprotocol_abi.h, а не
повторять внутреннюю раскладку pcan_parser_t или gui_parser_t.
@@ -253,7 +725,7 @@ ABI v1 пока экспортирует функции pcan_abi_*:
кадра — 19 байт. Формат переносит один classic CAN 2.0 кадр через COM, USB CDC,
RS-232, RS-485 или TCP byte stream.
Флаги:
-
+
Бит
@@ -283,7 +755,7 @@ RS-232, RS-485 или TCP byte stream.
служебный кадр диагностики моста
-
+
3.2. GUI transport: A5 5A
A5 5A | VER | TYPE | SEQ[2] BE | SIZE[2] BE | PAYLOAD[0..512] | CRC32 LE
@@ -312,7 +784,7 @@ MSVC, GCC и Clang.
pcan_abi.h экспортирует простые числа, указатели и явно ограниченные буферы.
Текущая версия возвращается pcan_abi_version() и равна 1.
CAN bridge API
-
+
Функция
@@ -353,9 +825,9 @@ MSVC, GCC и Clang.
Получить frames/CRC/bad length/stray bytes
-
+
GUI API
-
+
Функция
@@ -380,7 +852,7 @@ MSVC, GCC и Clang.
Получить frames/CRC/version/length/stray bytes
-
+
Возврат 0 из encode означает неверные аргументы или недостаточный output
buffer. Parser API возвращает отрицательное значение при неверном context,
0 пока кадр не собран и 1 при готовом кадре.
@@ -404,7 +876,7 @@ pcan_abi_parser_init(storage, size);
буфер рассчитан на одного писателя и одного читателя (SPSC). Для нескольких
писателей синхронизацию обеспечивает порт/приложение.
Само ядро не содержит WinAPI, поэтому собирается GCC или Clang. Для SETGUI под
Linux остаются две отдельные задачи: упаковать libsetprotocol.so с приложением и
@@ -514,7 +986,7 @@ size_t written = pcan_abi_frame_encode(
Для firmware удобнее полный C API из protocan_transport.h: он даёт link,
callbacks, ring и GAS без FFI-обёртки.
10. Диагностика
-
+
Симптом
@@ -551,7 +1023,7 @@ callbacks, ring и GAS без FFI-обёртки.
COM baud не равен CAN bitrate; проверьте настройку самого адаптера
-
+
11. Совместимость и ограничения
ABI v1 изменяется только совместимым добавлением функций. Ломающее изменение
From 5d48ddf0b0a4539338f3669df28273340ed97f97 Mon Sep 17 00:00:00 2001
From: Andrey Kruchinkin
Date: Sun, 6 Sep 2026 03:03:59 +0300
Subject: [PATCH 28/37] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?=
=?UTF-8?q?=D1=82=D1=8C=20HTML-=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BE=D1=87?=
=?UTF-8?q?=D0=BD=D0=B8=D0=BA=20CAN=20v1=20=D0=B8=20v2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
doc/CAN_FRAME_PARSE_V1_V2.md | 379 +++++++++++++++++++++++++
doc/build-setprotocol-html.ps1 | 36 ++-
doc/index.html | 2 +-
doc/setprotocol.html | 498 ++++++++++++++++++++++++++++++++-
4 files changed, 899 insertions(+), 16 deletions(-)
create mode 100644 doc/CAN_FRAME_PARSE_V1_V2.md
diff --git a/doc/CAN_FRAME_PARSE_V1_V2.md b/doc/CAN_FRAME_PARSE_V1_V2.md
new file mode 100644
index 0000000..d312288
--- /dev/null
+++ b/doc/CAN_FRAME_PARSE_V1_V2.md
@@ -0,0 +1,379 @@
+# Разбор CAN-кадров ProtoCAN Boot v1 и SETProtocol v2
+
+Документ описывает wire-форматы двух протоколов обновления прошивки:
+
+- **v1** — `templates/c/protocan-boot`, одна команда или 8 байт образа в одном
+ Extended CAN-кадре;
+- **v2** — `templates/c/set-protocol`, полный кадр SETProtocol разбивается на
+ несколько Extended CAN-кадров.
+
+Все многобайтные поля payload передаются **little-endian**. CAN ID — 29-битный.
+Для рабочего кода нужно использовать канонические реализации из `templates`,
+а приведённый ниже Python-парсер удобен для анализатора, логов и отладки.
+
+## 1. ProtoCAN Boot v1
+
+### 1.1. Разметка Extended CAN ID
+
+```text
+bits size field
+28 1 Priority
+27 1 Route: 0 = host -> device, 1 = device -> host
+26..24 3 Device Type
+23..20 4 Device ID
+19..16 4 Message Type
+15..0 16 Message Body
+```
+
+Формула:
+
+```text
+ID = Priority << 28 |
+ Route << 27 |
+ DeviceType << 24 |
+ DeviceID << 20 |
+ MessageType << 16 |
+ MessageBody
+```
+
+Типы загрузочных сообщений:
+
+| Message Type | Имя | Message Body | CAN payload |
+|---:|---|---|---|
+| `0x9` | `BOOT_CONTROL` | `SessionID << 8 \| Command` | параметры команды |
+| `0xA` | `BOOT_DATA_A` | индекс блока | 8 байт слота A |
+| `0xB` | `BOOT_DATA_B` | индекс блока | 8 байт слота B |
+| `0xC` | `BOOT_STATUS` | `SessionID << 8 \| Command` | статус и прогресс |
+| `0xD` | `BOOT_DISCOVERY` | подтип | информация об устройстве |
+
+Команды `BOOT_CONTROL`:
+
+| Код | Команда | Payload |
+|---:|---|---|
+| `0x01` | `IDENTIFY` | пустой |
+| `0x02` | `ENTER_BOOT` | пустой |
+| `0x03` | `BEGIN_IMAGE` | `image_size u32`, `image_crc32 u32` |
+| `0x04` | `BEGIN_COMPAT` | `product u16`, `hw_min u8`, `hw_max u8`, `version u32` |
+| `0x05` | `ERASE` | пустой |
+| `0x06` | `VERIFY` | пустой |
+| `0x07` | `COMMIT` | пустой |
+| `0x08` | `CONFIRM` | пустой |
+| `0x09` | `REBOOT` | пустой |
+| `0x0A` | `ABORT` | пустой |
+| `0x0B` | `QUERY_PROGRESS` | пустой |
+
+`BOOT_STATUS` всегда содержит 8 байт:
+
+```text
+offset size field
+0 1 status
+1 1 target_slot
+2 2 next_block u16 LE
+4 4 running_crc32 u32 LE
+```
+
+`BOOT_DISCOVERY` с body `1` содержит:
+
+```text
+offset size field
+0 2 product_type u16 LE
+2 1 hardware_revision
+3 1 protocol_version = 1
+4 4 firmware_version u32 LE
+```
+
+Пример запроса `IDENTIFY` для `DeviceType=7`, `DeviceID=13`:
+
+```text
+CAN ID: 17D90001
+DLC: 0
+```
+
+### 1.2. Python-парсер v1
+
+```python
+def parse_v1(can_id: int, data: bytes) -> dict:
+ if not 0 <= can_id <= 0x1FFFFFFF:
+ raise ValueError("неверный Extended CAN ID")
+ if len(data) > 8:
+ raise ValueError("DLC больше 8")
+
+ result = {
+ "version": 1,
+ "priority": (can_id >> 28) & 0x01,
+ "route": (can_id >> 27) & 0x01,
+ "device_type": (can_id >> 24) & 0x07,
+ "device_id": (can_id >> 20) & 0x0F,
+ "message_type": (can_id >> 16) & 0x0F,
+ "message_body": can_id & 0xFFFF,
+ "data": bytes(data),
+ }
+
+ msg_type = result["message_type"]
+ body = result["message_body"]
+ if msg_type in (0x9, 0xC):
+ result["session_id"] = (body >> 8) & 0xFF
+ result["command"] = body & 0xFF
+ elif msg_type in (0xA, 0xB):
+ result["slot"] = msg_type - 0xA
+ result["block_index"] = body
+
+ if msg_type == 0xC:
+ if len(data) != 8:
+ raise ValueError("BOOT_STATUS должен содержать 8 байт")
+ result.update({
+ "status": data[0],
+ "target_slot": data[1],
+ "next_block": int.from_bytes(data[2:4], "little"),
+ "running_crc32": int.from_bytes(data[4:8], "little"),
+ })
+ elif msg_type == 0xD and body == 1:
+ if len(data) != 8:
+ raise ValueError("BOOT_DISCOVERY должен содержать 8 байт")
+ result.update({
+ "product_type": int.from_bytes(data[0:2], "little"),
+ "hardware_revision": data[2],
+ "protocol_version": data[3],
+ "firmware_version": int.from_bytes(data[4:8], "little"),
+ })
+ return result
+```
+
+## 2. SETProtocol v2 поверх classic CAN
+
+В v2 CAN-кадр является только транспортным сегментом. Сначала нужно собрать
+полный SETP-пакет, и только затем разбирать его заголовок, payload и CRC32.
+
+### 2.1. Разметка Extended CAN ID
+
+```text
+bits size field
+28..24 5 Prefix = 0x12
+23..16 8 Destination node
+15..8 8 Source node
+7 1 Priority
+6..0 7 Channel
+```
+
+Формула:
+
+```text
+ID = 0x12 << 24 |
+ Destination << 16 |
+ Source << 8 |
+ Priority << 7 |
+ Channel
+```
+
+### 2.2. CAN-сегменты
+
+Первый байт CAN payload — PCI:
+
+| PCI | Назначение | Формат CAN payload |
+|---:|---|---|
+| `0x10` | первый сегмент | `10`, `total_length u16 LE`, первые 5 байт SETP |
+| `0x20..0x2F` | продолжение | `2N`, следующие 1–7 байт SETP |
+| `0x30..0x32` | flow control | `3S`, `block_size`, `st_min_ms` |
+
+`N` — циклический номер сегмента `1..15,0..`; следующий сегмент обязан иметь
+ожидаемый номер, тот же CAN ID и прийти до тайм-аута сборки 500 мс.
+
+### 2.3. Внутренний кадр SETProtocol v2
+
+```text
+offset size field
+0 2 SOF = A5 5A
+2 1 version = 02
+3 1 flags
+4 2 message_type u16 LE
+6 2 source u16 LE
+8 2 destination u16 LE
+10 2 sequence u16 LE
+12 2 payload_length u16 LE
+14 N payload
+14+N 4 CRC32 IEEE u32 LE
+```
+
+CRC32 считается по байтам от `version` на offset 2 до конца payload. Поля
+`source`, `destination` и `priority` внутреннего заголовка должны совпадать с
+CAN ID.
+
+Флаги:
+
+| Бит | Значение |
+|---:|---|
+| `0x01` | RESPONSE |
+| `0x02` | EVENT |
+| `0x04` | ERROR |
+| `0x08` | ACK_REQUIRED |
+| `0x10` | MORE |
+| `0x20` | PRIORITY |
+
+Каждый response начинается с `status u16 LE`. Основные firmware message types:
+`FW_BEGIN=0x0100`, `FW_DATA=0x0101`, `FW_END=0x0102`, `FW_ABORT=0x0103`,
+`FW_STATUS=0x0104`, `FW_ACTIVATE=0x0105`.
+
+Пример `PING` к BALZAM node `13`, source `0`, sequence `1`, priority `1`,
+channel `1`:
+
+```text
+Полный SETP:
+A5 5A 02 28 01 00 00 00 0D 00 01 00 00 00 E7 29 51 40
+
+CAN ID 120D0081, сегменты:
+10 12 00 A5 5A 02 28 01
+21 00 00 00 0D 00 01 00
+22 00 00 E7 29 51 40
+```
+
+### 2.4. Python-парсер и сборщик v2
+
+```python
+import binascii
+
+
+def parse_v2_can_id(can_id: int) -> dict:
+ if not 0 <= can_id <= 0x1FFFFFFF:
+ raise ValueError("неверный Extended CAN ID")
+ if (can_id >> 24) & 0x1F != 0x12:
+ raise ValueError("не SETProtocol v2 CAN ID")
+ return {
+ "destination": (can_id >> 16) & 0xFF,
+ "source": (can_id >> 8) & 0xFF,
+ "priority": (can_id >> 7) & 0x01,
+ "channel": can_id & 0x7F,
+ }
+
+
+def parse_setp(packet: bytes, can_id: int) -> dict:
+ if len(packet) < 18 or packet[:2] != b"\xA5\x5A":
+ raise ValueError("нет полного SETP-кадра")
+ if packet[2] != 2:
+ raise ValueError("неподдерживаемая версия SETP")
+ flags = packet[3]
+ if flags & 0xC0:
+ raise ValueError("установлены зарезервированные флаги")
+ payload_length = int.from_bytes(packet[12:14], "little")
+ if len(packet) != 14 + payload_length + 4:
+ raise ValueError("не совпадает payload_length")
+ expected_crc = int.from_bytes(packet[-4:], "little")
+ actual_crc = binascii.crc32(packet[2:-4]) & 0xFFFFFFFF
+ if actual_crc != expected_crc:
+ raise ValueError("ошибка CRC32 SETP")
+
+ address = parse_v2_can_id(can_id)
+ source = int.from_bytes(packet[6:8], "little")
+ destination = int.from_bytes(packet[8:10], "little")
+ priority = int(bool(flags & 0x20))
+ if (source, destination, priority) != (
+ address["source"], address["destination"], address["priority"]
+ ):
+ raise ValueError("SETP header не совпадает с CAN ID")
+
+ payload = packet[14:-4]
+ result = {
+ "version": 2,
+ "flags": flags,
+ "message_type": int.from_bytes(packet[4:6], "little"),
+ "source": source,
+ "destination": destination,
+ "sequence": int.from_bytes(packet[10:12], "little"),
+ "payload": payload,
+ "can": address,
+ }
+ if flags & 0x01:
+ if len(payload) < 2:
+ raise ValueError("response не содержит status")
+ result["status"] = int.from_bytes(payload[:2], "little")
+ result["body"] = payload[2:]
+ return result
+
+
+class V2CanReassembler:
+ def __init__(self, timeout_ms: int = 500):
+ self.timeout_ms = timeout_ms
+ self.reset()
+
+ def reset(self):
+ self.can_id = None
+ self.total = 0
+ self.data = bytearray()
+ self.next_sequence = 1
+ self.deadline_ms = 0
+
+ def feed(self, can_id: int, data: bytes, now_ms: int):
+ parse_v2_can_id(can_id)
+ if not 1 <= len(data) <= 8:
+ raise ValueError("DLC вне диапазона 1..8")
+ if self.can_id is not None and now_ms >= self.deadline_ms:
+ self.reset()
+ raise ValueError("тайм-аут сборки SETP")
+
+ pci_type = data[0] & 0xF0
+ if pci_type == 0x10:
+ if len(data) != 8:
+ raise ValueError("первый сегмент должен иметь DLC 8")
+ total = int.from_bytes(data[1:3], "little")
+ if not 18 <= total <= 530:
+ raise ValueError("неверный размер SETP")
+ self.can_id = can_id
+ self.total = total
+ self.data = bytearray(data[3:])
+ self.next_sequence = 1
+ self.deadline_ms = now_ms + self.timeout_ms
+ return None
+
+ if pci_type == 0x20:
+ sequence = data[0] & 0x0F
+ if (
+ self.can_id is None
+ or can_id != self.can_id
+ or sequence != self.next_sequence
+ or len(data) < 2
+ ):
+ self.reset()
+ raise ValueError("ошибка последовательности CAN-сегментов")
+ if len(data) - 1 > self.total - len(self.data):
+ self.reset()
+ raise ValueError("лишние байты CAN-сегмента")
+ self.data.extend(data[1:])
+ self.next_sequence = (self.next_sequence + 1) & 0x0F
+ self.deadline_ms = now_ms + self.timeout_ms
+ if len(self.data) == self.total:
+ packet = bytes(self.data)
+ packet_can_id = self.can_id
+ self.reset()
+ return parse_setp(packet, packet_can_id)
+ return None
+
+ if pci_type == 0x30:
+ return {"flow_control": data[0] & 0x0F, "data": data[1:]}
+ raise ValueError("неизвестный PCI")
+```
+
+В SETGUI эти операции уже реализованы в
+`third_party/templates/python/setprotocol/can.py`; собственный parser нужен
+только внешнему анализатору или диагностическому скрипту.
+
+## 3. Как отличать v1 от v2
+
+Для используемых сейчас адресов достаточно следующих признаков:
+
+- v2: верхние пять бит CAN ID равны `0x12`, PCI начинается с `0x10`, `0x2N`
+ или `0x3S`, после reassembly присутствует `A5 5A 02`;
+- v1: `MessageType` в битах `19..16` равен `0x9..0xD`, каждый кадр разбирается
+ самостоятельно.
+
+Однако универсальное автоопределение только по одному CAN ID невозможно:
+комбинация `Priority/Route/DeviceType` v1 теоретически тоже может дать верхнее
+поле `0x12`, а первый байт firmware data v1 может случайно совпасть с PCI.
+Надёжный анализатор должен учитывать настроенный режим узла либо подтвердить v2
+только после сборки кадра с корректными `A5 5A 02`, длиной и CRC32.
+
+## 4. Канонические исходники
+
+- v1 ID и state machine: `third_party/templates/c/protocan-boot/src/pcan_boot.c`;
+- v2 CAN transport: `third_party/templates/c/set-protocol/src/set_can.c`;
+- v2 frame/CRC: `third_party/templates/c/set-protocol/src/set_protocol.c`;
+- v2 firmware payload: `third_party/templates/c/set-protocol/src/set_firmware.c`;
+- Python v2 CAN: `third_party/templates/python/setprotocol/can.py`.
diff --git a/doc/build-setprotocol-html.ps1 b/doc/build-setprotocol-html.ps1
index 6183b25..e783583 100644
--- a/doc/build-setprotocol-html.ps1
+++ b/doc/build-setprotocol-html.ps1
@@ -4,9 +4,22 @@ param()
$ErrorActionPreference = 'Stop'
$outputPath = Join-Path $PSScriptRoot 'setprotocol.html'
$sourcePath = Join-Path $PSScriptRoot '..\c\set-protocol\docs\SETPROTOCOL.md'
+$canFrameSourcePath = Join-Path $PSScriptRoot 'CAN_FRAME_PARSE_V1_V2.md'
-$markdown = Get-Content -Raw -LiteralPath $sourcePath -Encoding UTF8
-$html = (ConvertFrom-Markdown -InputObject $markdown).Html
+function Convert-DocumentationMarkdown {
+ param([string]$Path)
+
+ $markdown = Get-Content -Raw -LiteralPath $Path -Encoding UTF8
+ $html = (ConvertFrom-Markdown -InputObject $markdown).Html
+
+ # Wide protocol tables must scroll horizontally instead of squeezing their
+ # contents into unreadable one-character columns on a narrow viewport.
+ $html = $html -replace '
Раздел генерируется из doc/CAN_FRAME_PARSE_V1_V2.md. В нём собраны wire-разметка, примеры и готовые Python-парсеры.
+
+
+
Разбор CAN-кадров ProtoCAN Boot v1 и SETProtocol v2
+
Документ описывает wire-форматы двух протоколов обновления прошивки:
+
+
v1 — templates/c/protocan-boot, одна команда или 8 байт образа в одном
+Extended CAN-кадре;
+
v2 — templates/c/set-protocol, полный кадр SETProtocol разбивается на
+несколько Extended CAN-кадров.
+
+
Все многобайтные поля payload передаются little-endian. CAN ID — 29-битный.
+Для рабочего кода нужно использовать канонические реализации из templates,
+а приведённый ниже Python-парсер удобен для анализатора, логов и отладки.
+
1. ProtoCAN Boot v1
+
1.1. Разметка Extended CAN ID
+
bits size field
+28 1 Priority
+27 1 Route: 0 = host -> device, 1 = device -> host
+26..24 3 Device Type
+23..20 4 Device ID
+19..16 4 Message Type
+15..0 16 Message Body
+
В v2 CAN-кадр является только транспортным сегментом. Сначала нужно собрать
+полный SETP-пакет, и только затем разбирать его заголовок, payload и CRC32.
N — циклический номер сегмента 1..15,0..; следующий сегмент обязан иметь
+ожидаемый номер, тот же CAN ID и прийти до тайм-аута сборки 500 мс.
+
2.3. Внутренний кадр SETProtocol v2
+
offset size field
+0 2 SOF = A5 5A
+2 1 version = 02
+3 1 flags
+4 2 message_type u16 LE
+6 2 source u16 LE
+8 2 destination u16 LE
+10 2 sequence u16 LE
+12 2 payload_length u16 LE
+14 N payload
+14+N 4 CRC32 IEEE u32 LE
+
+
CRC32 считается по байтам от version на offset 2 до конца payload. Поля
+source, destination и priority внутреннего заголовка должны совпадать с
+CAN ID.
+
Флаги:
+
+
+
+
Бит
+
Значение
+
+
+
+
+
0x01
+
RESPONSE
+
+
+
0x02
+
EVENT
+
+
+
0x04
+
ERROR
+
+
+
0x08
+
ACK_REQUIRED
+
+
+
0x10
+
MORE
+
+
+
0x20
+
PRIORITY
+
+
+
+
Каждый response начинается с status u16 LE. Основные firmware message types:
+FW_BEGIN=0x0100, FW_DATA=0x0101, FW_END=0x0102, FW_ABORT=0x0103,
+FW_STATUS=0x0104, FW_ACTIVATE=0x0105.
+
Пример PING к BALZAM node 13, source 0, sequence 1, priority 1,
+channel 1:
В SETGUI эти операции уже реализованы в
+third_party/templates/python/setprotocol/can.py; собственный parser нужен
+только внешнему анализатору или диагностическому скрипту.
+
3. Как отличать v1 от v2
+
Для используемых сейчас адресов достаточно следующих признаков:
+
+
v2: верхние пять бит CAN ID равны 0x12, PCI начинается с 0x10, 0x2N
+или 0x3S, после reassembly присутствует A5 5A 02;
+
v1: MessageType в битах 19..16 равен 0x9..0xD, каждый кадр разбирается
+самостоятельно.
+
+
Однако универсальное автоопределение только по одному CAN ID невозможно:
+комбинация Priority/Route/DeviceType v1 теоретически тоже может дать верхнее
+поле 0x12, а первый байт firmware data v1 может случайно совпасть с PCI.
+Надёжный анализатор должен учитывать настроенный режим узла либо подтвердить v2
+только после сборки кадра с корректными A5 5A 02, длиной и CRC32.
+
4. Канонические исходники
+
+
v1 ID и state machine: third_party/templates/c/protocan-boot/src/pcan_boot.c;
+
v2 CAN transport: third_party/templates/c/set-protocol/src/set_can.c;
Этот раздел генерируется из канонического c/set-protocol/docs/SETPROTOCOL.md. Редактировать нужно Markdown, затем запускать doc/build-setprotocol-html.ps1.
@@ -159,7 +631,7 @@ COM │ SLCAN │ SocketCAN │ USB CDC │ TCP │ STM32 UART/CAN
последовательного моста, а 500 kbit/s в CAN-настройках — bitrate самой CAN-шины.
Ядро не подменяет одно другим и не выбирает эти значения автоматически.
2. Состав исходников
-
+
Модуль
@@ -234,7 +706,7 @@ COM │ SLCAN │ SocketCAN │ USB CDC │ TCP │ STM32 UART/CAN
ABI компилятора C
-
+
Общая точка включения для C-кода — include/setprotocol.h.
Иностранные runtimes должны использовать include/setprotocol_abi.h, а не
повторять внутреннюю раскладку pcan_parser_t или gui_parser_t.
@@ -253,7 +725,7 @@ ABI v1 пока экспортирует функции pcan_abi_*:
кадра — 19 байт. Формат переносит один classic CAN 2.0 кадр через COM, USB CDC,
RS-232, RS-485 или TCP byte stream.
Флаги:
-
+
Бит
@@ -283,7 +755,7 @@ RS-232, RS-485 или TCP byte stream.
служебный кадр диагностики моста
-
+
3.2. GUI transport: A5 5A
A5 5A | VER | TYPE | SEQ[2] BE | SIZE[2] BE | PAYLOAD[0..512] | CRC32 LE
@@ -312,7 +784,7 @@ MSVC, GCC и Clang.
pcan_abi.h экспортирует простые числа, указатели и явно ограниченные буферы.
Текущая версия возвращается pcan_abi_version() и равна 1.
CAN bridge API
-
+
Функция
@@ -353,9 +825,9 @@ MSVC, GCC и Clang.
Получить frames/CRC/bad length/stray bytes
-
+
GUI API
-
+
Функция
@@ -380,7 +852,7 @@ MSVC, GCC и Clang.
Получить frames/CRC/version/length/stray bytes
-
+
Возврат 0 из encode означает неверные аргументы или недостаточный output
buffer. Parser API возвращает отрицательное значение при неверном context,
0 пока кадр не собран и 1 при готовом кадре.
@@ -404,7 +876,7 @@ pcan_abi_parser_init(storage, size);
буфер рассчитан на одного писателя и одного читателя (SPSC). Для нескольких
писателей синхронизацию обеспечивает порт/приложение.
Само ядро не содержит WinAPI, поэтому собирается GCC или Clang. Для SETGUI под
Linux остаются две отдельные задачи: упаковать libsetprotocol.so с приложением и
@@ -514,7 +986,7 @@ size_t written = pcan_abi_frame_encode(
Для firmware удобнее полный C API из protocan_transport.h: он даёт link,
callbacks, ring и GAS без FFI-обёртки.
10. Диагностика
-
+
Симптом
@@ -551,7 +1023,7 @@ callbacks, ring и GAS без FFI-обёртки.
COM baud не равен CAN bitrate; проверьте настройку самого адаптера
-
+
11. Совместимость и ограничения
ABI v1 изменяется только совместимым добавлением функций. Ломающее изменение
From 72205216f50c6113f591fadeaeda458077229f63 Mon Sep 17 00:00:00 2001
From: Andrey Kruchinkin
Date: Mon, 7 Sep 2026 08:32:02 +0300
Subject: [PATCH 29/37] =?UTF-8?q?feat(set-protocol):=20=D0=B4=D0=BE=D0=B1?=
=?UTF-8?q?=D0=B0=D0=B2=D1=8C=20=D0=BF=D0=BE=D1=80=D1=82=20F407=20=D0=B4?=
=?UTF-8?q?=D0=BB=D1=8F=20DevBoard=5FV1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 1 +
c/set-protocol/CMakeLists.txt | 5 +
c/set-protocol/include/pcan_modbus_server.h | 60 ++++++++
c/set-protocol/include/protocan_transport.h | 1 +
.../ports/stm32f407-devboard-v1/README.md | 22 +++
.../set_devboard_v1_stm32f407.c | 80 ++++++++++
.../set_devboard_v1_stm32f407.h | 38 +++++
c/set-protocol/src/pcan_modbus_server.c | 144 ++++++++++++++++++
c/set-protocol/tests/test_modbus_server.c | 90 +++++++++++
c/set-protocol/tools/build_host.py | 2 +-
python/protocan/devboard_v1.py | 60 ++++++++
python/tests/test_devboard_v1.py | 14 ++
12 files changed, 516 insertions(+), 1 deletion(-)
create mode 100644 c/set-protocol/include/pcan_modbus_server.h
create mode 100644 c/set-protocol/ports/stm32f407-devboard-v1/README.md
create mode 100644 c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c
create mode 100644 c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.h
create mode 100644 c/set-protocol/src/pcan_modbus_server.c
create mode 100644 c/set-protocol/tests/test_modbus_server.c
create mode 100644 python/protocan/devboard_v1.py
create mode 100644 python/tests/test_devboard_v1.py
diff --git a/README.md b/README.md
index c6268bd..b8eb5f8 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ templates/
| [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 |
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android и STM32F4-порты в комплекте** |
+| [`c/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/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt
index 30dc23a..40dafeb 100644
--- a/c/set-protocol/CMakeLists.txt
+++ b/c/set-protocol/CMakeLists.txt
@@ -26,6 +26,7 @@ set(SETPROTOCOL_LEGACY_SOURCES
src/pcan_gas.c
src/pcan_id.c
src/pcan_link.c
+ src/pcan_modbus_server.c
src/pcan_ring.c
)
@@ -88,6 +89,10 @@ if(SETP_BUILD_TESTS)
target_link_libraries(test_transport PRIVATE setprotocol_static)
add_test(NAME legacy_transport COMMAND test_transport)
+ add_executable(test_modbus_server tests/test_modbus_server.c)
+ target_link_libraries(test_modbus_server PRIVATE setprotocol_static)
+ add_test(NAME protocan_modbus_server COMMAND test_modbus_server)
+
add_executable(test_gui tests/test_gui.c)
target_link_libraries(test_gui PRIVATE setprotocol_static)
add_test(NAME legacy_gui COMMAND test_gui)
diff --git a/c/set-protocol/include/pcan_modbus_server.h b/c/set-protocol/include/pcan_modbus_server.h
new file mode 100644
index 0000000..a8e4c01
--- /dev/null
+++ b/c/set-protocol/include/pcan_modbus_server.h
@@ -0,0 +1,60 @@
+/**
+ * @file pcan_modbus_server.h
+ * @brief Modbus register windows transported in one classic ProtoCAN frame.
+ */
+#ifndef PCAN_MODBUS_SERVER_H
+#define PCAN_MODBUS_SERVER_H
+
+#include
+#include
+
+#include "pcan_frame.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+ PCAN_MODBUS_COIL = 0x4U,
+ PCAN_MODBUS_DISCRETE = 0x5U,
+ PCAN_MODBUS_HOLDING = 0x6U,
+ PCAN_MODBUS_INPUT = 0x7U
+} pcan_modbus_bank_t;
+
+typedef bool (*pcan_modbus_read_fn)(void *user, pcan_modbus_bank_t bank,
+ uint16_t address, uint16_t *value);
+typedef bool (*pcan_modbus_write_fn)(void *user, pcan_modbus_bank_t bank,
+ uint16_t address, uint16_t value);
+
+typedef struct {
+ uint8_t device_type;
+ uint8_t device_id;
+ pcan_modbus_read_fn read;
+ pcan_modbus_write_fn write;
+ void *user;
+ uint32_t requests;
+ uint32_t responses;
+ uint32_t rejected;
+} pcan_modbus_server_t;
+
+typedef enum {
+ PCAN_MODBUS_NOT_FOR_US = 0,
+ PCAN_MODBUS_HANDLED_NO_RESPONSE,
+ PCAN_MODBUS_RESPONSE
+} pcan_modbus_result_t;
+
+bool pcan_modbus_server_init(pcan_modbus_server_t *server,
+ uint8_t device_type, uint8_t device_id,
+ pcan_modbus_read_fn read,
+ pcan_modbus_write_fn write, void *user);
+
+/** Zero DLC reads; data writes COIL/HOLDING. Register words are little-endian. */
+pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
+ const pcan_frame_t *request,
+ pcan_frame_t *response);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PCAN_MODBUS_SERVER_H */
diff --git a/c/set-protocol/include/protocan_transport.h b/c/set-protocol/include/protocan_transport.h
index 379a9cb..bd9090c 100644
--- a/c/set-protocol/include/protocan_transport.h
+++ b/c/set-protocol/include/protocan_transport.h
@@ -23,6 +23,7 @@
#include "pcan_gas.h"
#include "pcan_id.h"
#include "pcan_link.h"
+#include "pcan_modbus_server.h"
#include "pcan_ring.h"
#endif /* PROTOCAN_TRANSPORT_H */
diff --git a/c/set-protocol/ports/stm32f407-devboard-v1/README.md b/c/set-protocol/ports/stm32f407-devboard-v1/README.md
new file mode 100644
index 0000000..83f56ef
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-devboard-v1/README.md
@@ -0,0 +1,22 @@
+# STM32F407 + CAN485 DevBoard_V1
+
+Порт даёт SETGUI доступ к существующим Modbus-регистрам STM32F407 через
+классический CAN. ПК подключён к USB-UART платы CAN485 DevBoard_V1, команда
+`T,E` платы отправляет ProtoCAN-кадр, а этот порт отвечает тем же типом кадра.
+При маршруте `Q3` плата одновременно ретранслирует поток по RS-485.
+
+Порт не настраивает GPIO, bitrate, фильтры и не запускает CAN: это остаётся за
+CubeMX-проектом. Необработанные кадры передаются callback-у приложения, поэтому
+загрузчик ProtoCAN и регистровый сервис могут делить FIFO0.
+
+Слой приложения предоставляет callback-и чтения и записи. Нулевой DLC означает
+чтение; данные в кадре `HOLDING`/`COIL` — запись. За кадр передаются до четырёх
+16-битных регистров или до шестнадцати coils.
+
+```c
+static set_devboard_v1_stm32f407_t gui_can;
+set_devboard_v1_stm32f407_init(&gui_can, &hcan, 7, 3,
+ app_read_register, app_write_register, NULL,
+ app_handle_boot_frame, NULL);
+for (;;) set_devboard_v1_stm32f407_poll(&gui_can);
+```
diff --git a/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c
new file mode 100644
index 0000000..9f4a98d
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c
@@ -0,0 +1,80 @@
+#include "set_devboard_v1_stm32f407.h"
+
+#include
+
+static bool transmit(set_devboard_v1_stm32f407_t *port,
+ const pcan_frame_t *frame)
+{
+ CAN_TxHeaderTypeDef header;
+ uint32_t mailbox = 0U;
+ uint8_t data[8] = {0U};
+
+ memset(&header, 0, sizeof(header));
+ header.ExtId = frame->id & 0x1FFFFFFFUL;
+ header.IDE = CAN_ID_EXT;
+ header.RTR = ((frame->flags & PCAN_FLAG_RTR) != 0U) ?
+ CAN_RTR_REMOTE : CAN_RTR_DATA;
+ header.DLC = frame->dlc;
+ header.TransmitGlobalTime = DISABLE;
+ memcpy(data, frame->data, frame->dlc);
+ if (HAL_CAN_AddTxMessage(port->can, &header, data, &mailbox) != HAL_OK) {
+ port->tx_errors++;
+ return false;
+ }
+ port->tx_frames++;
+ return true;
+}
+
+bool set_devboard_v1_stm32f407_init(
+ set_devboard_v1_stm32f407_t *port, CAN_HandleTypeDef *can,
+ uint8_t device_type, uint8_t device_id,
+ pcan_modbus_read_fn read, pcan_modbus_write_fn write, void *register_user,
+ set_devboard_v1_unhandled_fn unhandled, void *unhandled_user)
+{
+ if ((port == NULL) || (can == NULL)) {
+ return false;
+ }
+ memset(port, 0, sizeof(*port));
+ port->can = can;
+ port->unhandled = unhandled;
+ port->unhandled_user = unhandled_user;
+ return pcan_modbus_server_init(&port->server, device_type, device_id,
+ read, write, register_user);
+}
+
+size_t set_devboard_v1_stm32f407_poll(set_devboard_v1_stm32f407_t *port)
+{
+ size_t handled = 0U;
+ if ((port == NULL) || (port->can == NULL)) {
+ return 0U;
+ }
+ while (HAL_CAN_GetRxFifoFillLevel(port->can, CAN_RX_FIFO0) != 0U) {
+ CAN_RxHeaderTypeDef header;
+ uint8_t data[8] = {0U};
+ pcan_frame_t request = {0};
+ pcan_frame_t response;
+ pcan_modbus_result_t result;
+
+ if (HAL_CAN_GetRxMessage(port->can, CAN_RX_FIFO0, &header, data) != HAL_OK) {
+ break;
+ }
+ port->rx_frames++;
+ request.id = (header.IDE == CAN_ID_EXT) ? header.ExtId : header.StdId;
+ request.flags = (header.IDE == CAN_ID_EXT) ? PCAN_FLAG_IDE : 0U;
+ if (header.RTR == CAN_RTR_REMOTE) {
+ request.flags |= PCAN_FLAG_RTR;
+ }
+ request.dlc = (header.DLC > 8U) ? 8U : (uint8_t)header.DLC;
+ memcpy(request.data, data, request.dlc);
+
+ result = pcan_modbus_server_handle(&port->server, &request, &response);
+ if (result == PCAN_MODBUS_RESPONSE) {
+ (void)transmit(port, &response);
+ handled++;
+ } else if ((result == PCAN_MODBUS_NOT_FOR_US) &&
+ (port->unhandled != NULL)) {
+ port->unhandled(port->unhandled_user, &header, data);
+ }
+ }
+ return handled;
+}
diff --git a/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.h b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.h
new file mode 100644
index 0000000..862154d
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.h
@@ -0,0 +1,38 @@
+/** STM32F407 bxCAN port for SETGUI through CAN485 DevBoard_V1. */
+#ifndef SET_DEVBOARD_V1_STM32F407_H
+#define SET_DEVBOARD_V1_STM32F407_H
+
+#include "stm32f4xx_hal.h"
+#include "pcan_modbus_server.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void (*set_devboard_v1_unhandled_fn)(void *user,
+ const CAN_RxHeaderTypeDef *header,
+ const uint8_t data[8]);
+
+typedef struct {
+ CAN_HandleTypeDef *can;
+ pcan_modbus_server_t server;
+ set_devboard_v1_unhandled_fn unhandled;
+ void *unhandled_user;
+ uint32_t rx_frames;
+ uint32_t tx_frames;
+ uint32_t tx_errors;
+} set_devboard_v1_stm32f407_t;
+
+bool set_devboard_v1_stm32f407_init(
+ set_devboard_v1_stm32f407_t *port, CAN_HandleTypeDef *can,
+ uint8_t device_type, uint8_t device_id,
+ pcan_modbus_read_fn read, pcan_modbus_write_fn write, void *register_user,
+ set_devboard_v1_unhandled_fn unhandled, void *unhandled_user);
+
+size_t set_devboard_v1_stm32f407_poll(set_devboard_v1_stm32f407_t *port);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/c/set-protocol/src/pcan_modbus_server.c b/c/set-protocol/src/pcan_modbus_server.c
new file mode 100644
index 0000000..7b304e6
--- /dev/null
+++ b/c/set-protocol/src/pcan_modbus_server.c
@@ -0,0 +1,144 @@
+#include "pcan_modbus_server.h"
+
+#include
+
+#include "pcan_id.h"
+
+static bool is_bank(uint8_t value)
+{
+ return (value >= (uint8_t)PCAN_MODBUS_COIL) &&
+ (value <= (uint8_t)PCAN_MODBUS_INPUT);
+}
+
+bool pcan_modbus_server_init(pcan_modbus_server_t *server,
+ uint8_t device_type, uint8_t device_id,
+ pcan_modbus_read_fn read,
+ pcan_modbus_write_fn write, void *user)
+{
+ if ((server == NULL) || (read == NULL) || (device_type > 7U) ||
+ (device_id > 15U)) {
+ return false;
+ }
+ server->device_type = device_type;
+ server->device_id = device_id;
+ server->read = read;
+ server->write = write;
+ server->user = user;
+ server->requests = 0U;
+ server->responses = 0U;
+ server->rejected = 0U;
+ return true;
+}
+
+static pcan_modbus_result_t reject(pcan_modbus_server_t *server)
+{
+ server->rejected++;
+ return PCAN_MODBUS_HANDLED_NO_RESPONSE;
+}
+
+pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
+ const pcan_frame_t *request,
+ pcan_frame_t *response)
+{
+ pcan_id_t id;
+ uint16_t address;
+ uint8_t count;
+ pcan_modbus_bank_t bank;
+
+ if ((server == NULL) || (request == NULL) || (response == NULL) ||
+ ((request->flags & PCAN_FLAG_IDE) == 0U)) {
+ return PCAN_MODBUS_NOT_FOR_US;
+ }
+ pcan_id_unpack(request->id, &id);
+ if ((id.route != PCAN_ROUTE_FROM_PM) ||
+ (id.device_type != server->device_type) ||
+ (id.device_id != server->device_id) || !is_bank(id.msg_type)) {
+ return PCAN_MODBUS_NOT_FOR_US;
+ }
+
+ server->requests++;
+ address = (uint16_t)(id.msg_body >> 4U);
+ count = (uint8_t)(id.msg_body & 0x0FU);
+ bank = (pcan_modbus_bank_t)id.msg_type;
+ if (count == 0U) {
+ return reject(server);
+ }
+
+ *response = *request;
+ id.route = PCAN_ROUTE_FROM_DEVICE;
+ response->id = pcan_id_pack(&id);
+ response->flags = PCAN_FLAG_IDE;
+ response->seq = 0U;
+
+ if (request->dlc == 0U) {
+ uint16_t packed = 0U;
+ if ((bank == PCAN_MODBUS_COIL) || (bank == PCAN_MODBUS_DISCRETE)) {
+ if (count > 16U) {
+ return reject(server);
+ }
+ for (uint8_t index = 0U; index < count; ++index) {
+ uint16_t value = 0U;
+ if (!server->read(server->user, bank,
+ (uint16_t)(address + index), &value)) {
+ return reject(server);
+ }
+ if (value != 0U) {
+ packed |= (uint16_t)(1U << index);
+ }
+ }
+ response->dlc = 2U;
+ response->data[0] = (uint8_t)packed;
+ response->data[1] = (uint8_t)(packed >> 8U);
+ } else {
+ if (count > 4U) {
+ return reject(server);
+ }
+ response->dlc = (uint8_t)(count * 2U);
+ for (uint8_t index = 0U; index < count; ++index) {
+ uint16_t value = 0U;
+ if (!server->read(server->user, bank,
+ (uint16_t)(address + index), &value)) {
+ return reject(server);
+ }
+ response->data[index * 2U] = (uint8_t)value;
+ response->data[(index * 2U) + 1U] = (uint8_t)(value >> 8U);
+ }
+ }
+ } else {
+ if ((bank == PCAN_MODBUS_INPUT) || (bank == PCAN_MODBUS_DISCRETE) ||
+ (server->write == NULL)) {
+ return reject(server);
+ }
+ if (bank == PCAN_MODBUS_COIL) {
+ uint16_t bits;
+ if ((count > 16U) || (request->dlc != 2U)) {
+ return reject(server);
+ }
+ bits = (uint16_t)((uint16_t)request->data[0] |
+ ((uint16_t)request->data[1] << 8U));
+ for (uint8_t index = 0U; index < count; ++index) {
+ if (!server->write(server->user, bank,
+ (uint16_t)(address + index),
+ (uint16_t)((bits >> index) & 1U))) {
+ return reject(server);
+ }
+ }
+ } else {
+ if ((count > 4U) || (request->dlc != (uint8_t)(count * 2U))) {
+ return reject(server);
+ }
+ for (uint8_t index = 0U; index < count; ++index) {
+ uint16_t value = (uint16_t)(request->data[index * 2U] |
+ ((uint16_t)request->data[(index * 2U) + 1U] << 8U));
+ if (!server->write(server->user, bank,
+ (uint16_t)(address + index), value)) {
+ return reject(server);
+ }
+ }
+ }
+ response->dlc = request->dlc;
+ }
+
+ server->responses++;
+ return PCAN_MODBUS_RESPONSE;
+}
diff --git a/c/set-protocol/tests/test_modbus_server.c b/c/set-protocol/tests/test_modbus_server.c
new file mode 100644
index 0000000..7b6f89b
--- /dev/null
+++ b/c/set-protocol/tests/test_modbus_server.c
@@ -0,0 +1,90 @@
+#include
+#include
+
+#include "pcan_id.h"
+#include "pcan_modbus_server.h"
+
+static uint16_t holding[32];
+static uint16_t input[32];
+static uint16_t coils;
+
+static bool read_word(void *user, pcan_modbus_bank_t bank,
+ uint16_t address, uint16_t *value)
+{
+ (void)user;
+ if (address >= 32U) return false;
+ if (bank == PCAN_MODBUS_HOLDING) *value = holding[address];
+ else if (bank == PCAN_MODBUS_INPUT) *value = input[address];
+ else if (bank == PCAN_MODBUS_COIL) *value = (uint16_t)((coils >> address) & 1U);
+ else return false;
+ return true;
+}
+
+static bool write_word(void *user, pcan_modbus_bank_t bank,
+ uint16_t address, uint16_t value)
+{
+ (void)user;
+ if (address >= 32U) return false;
+ if (bank == PCAN_MODBUS_HOLDING) holding[address] = value;
+ else if (bank == PCAN_MODBUS_COIL && address < 16U) {
+ if (value) coils |= (uint16_t)(1U << address);
+ else coils &= (uint16_t)~(1U << address);
+ } else return false;
+ return true;
+}
+
+static pcan_frame_t request(uint8_t type, uint16_t address, uint8_t count)
+{
+ pcan_id_t id = {0};
+ pcan_frame_t frame = {0};
+ id.priority = PCAN_PRIORITY_STANDARD;
+ id.route = PCAN_ROUTE_FROM_PM;
+ id.device_type = 7U;
+ id.device_id = 3U;
+ id.msg_type = type;
+ id.msg_body = pcan_body_modbus(address, count);
+ frame.id = pcan_id_pack(&id);
+ frame.flags = PCAN_FLAG_IDE;
+ return frame;
+}
+
+#define CHECK(x) do { if (!(x)) { \
+ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); return 1; \
+} } while (0)
+
+int main(void)
+{
+ pcan_modbus_server_t server;
+ pcan_frame_t req;
+ pcan_frame_t rsp;
+ pcan_id_t id;
+ memset(holding, 0, sizeof(holding));
+ memset(input, 0, sizeof(input));
+ input[10] = 0x1234U;
+ input[11] = 0xABCDU;
+
+ CHECK(pcan_modbus_server_init(&server, 7U, 3U, read_word, write_word, NULL));
+ req = request(PCAN_MODBUS_INPUT, 10U, 2U);
+ CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
+ CHECK(rsp.dlc == 4U && rsp.data[0] == 0x34U && rsp.data[1] == 0x12U);
+ CHECK(rsp.data[2] == 0xCDU && rsp.data[3] == 0xABU);
+ pcan_id_unpack(rsp.id, &id);
+ CHECK(id.route == PCAN_ROUTE_FROM_DEVICE);
+
+ req = request(PCAN_MODBUS_HOLDING, 3U, 2U);
+ req.dlc = 4U;
+ req.data[0] = 0x22U; req.data[1] = 0x11U;
+ req.data[2] = 0x44U; req.data[3] = 0x33U;
+ CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
+ CHECK(holding[3] == 0x1122U && holding[4] == 0x3344U);
+
+ req = request(PCAN_MODBUS_COIL, 2U, 3U);
+ req.dlc = 2U; req.data[0] = 0x05U; req.data[1] = 0U;
+ CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
+ CHECK(coils == 0x0014U);
+
+ req = request(PCAN_MODBUS_INPUT, 0U, 5U);
+ CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE);
+ CHECK(server.requests == 4U && server.responses == 3U && server.rejected == 1U);
+ return 0;
+}
diff --git a/c/set-protocol/tools/build_host.py b/c/set-protocol/tools/build_host.py
index 2f54914..e208eb3 100644
--- a/c/set-protocol/tools/build_host.py
+++ b/c/set-protocol/tools/build_host.py
@@ -23,7 +23,7 @@ SOURCES = [
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c",
"balsam_can.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
"pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",
- "pcan_gas.c",
+ "pcan_gas.c", "pcan_modbus_server.c",
)
]
diff --git a/python/protocan/devboard_v1.py b/python/protocan/devboard_v1.py
new file mode 100644
index 0000000..6d3116a
--- /dev/null
+++ b/python/protocan/devboard_v1.py
@@ -0,0 +1,60 @@
+"""CAN485 DevBoard_V1 USB command and frame helpers (no GUI/serial dependency)."""
+
+from __future__ import annotations
+
+import re
+from dataclasses import dataclass
+
+
+FRAME_RE = re.compile(
+ r"^\[\s*(\d+)\.(\d{3})\]\s+(?:(RS485)\s+)?"
+ r"(EXT|STD)\s+0x([0-9A-Fa-f]+)\s+"
+ r"(RTR\s+)?DLC=(\d)(?:\s+DATA=([0-9A-Fa-f ]*))?"
+)
+
+
+@dataclass(frozen=True, slots=True)
+class BoardFrame:
+ timestamp: float
+ extended: bool
+ can_id: int
+ data: bytes
+ rtr: bool = False
+ source: str = "CAN"
+
+
+def parse_frame_line(line: str) -> BoardFrame | None:
+ match = FRAME_RE.match(line.strip())
+ if match is None:
+ return None
+ seconds, millis, source, frame_type, raw_id, raw_rtr, raw_dlc, raw_data = match.groups()
+ dlc = int(raw_dlc)
+ data = bytes.fromhex(raw_data or "")
+ rtr = bool(raw_rtr)
+ if (not rtr and len(data) != dlc) or (rtr and data):
+ return None
+ extended = frame_type == "EXT"
+ can_id = int(raw_id, 16)
+ if can_id > (0x1FFFFFFF if extended else 0x7FF):
+ return None
+ return BoardFrame(int(seconds) + int(millis) / 1000.0, extended, can_id,
+ data, rtr, source or "CAN")
+
+
+def command_transmit(frame: BoardFrame) -> bytes:
+ if len(frame.data) > 8:
+ raise ValueError("DLC cannot exceed 8")
+ kind = "E" if frame.extended else "S"
+ return f"T,{kind},{frame.can_id:X},{len(frame.data)},{frame.data.hex().upper()}\n".encode("ascii")
+
+
+def command_setup(*, can_bitrate: int = 500, rs485_baud: int = 512000,
+ route: int = 3) -> tuple[bytes, ...]:
+ if can_bitrate not in (25, 50, 100, 125, 250, 500, 800, 1000):
+ raise ValueError("unsupported CAN bitrate")
+ if not 1200 <= rs485_baud <= 4_000_000:
+ raise ValueError("unsupported RS485 baud")
+ if route not in range(4):
+ raise ValueError("route must be 0..3")
+ return (f"S{can_bitrate}\n".encode("ascii"), b"M0\n",
+ f"B{rs485_baud}\n".encode("ascii"), f"Q{route}\n".encode("ascii"))
diff --git a/python/tests/test_devboard_v1.py b/python/tests/test_devboard_v1.py
new file mode 100644
index 0000000..c9ff90b
--- /dev/null
+++ b/python/tests/test_devboard_v1.py
@@ -0,0 +1,14 @@
+from protocan.devboard_v1 import BoardFrame, command_setup, command_transmit, parse_frame_line
+
+
+def test_parse_can_and_rs485_lines():
+ can = parse_frame_line("[ 12.345] EXT 0x073700A2 DLC=2 DATA=34 12 |4.|")
+ rs = parse_frame_line("[ 12.346] RS485 STD 0x123 RTR DLC=4")
+ assert can and can.can_id == 0x073700A2 and can.data == b"\x34\x12"
+ assert rs and rs.source == "RS485" and rs.rtr and rs.data == b""
+
+
+def test_commands_are_firmware_compatible_and_lf_terminated():
+ frame = BoardFrame(0.0, True, 0x073700A2, b"\x34\x12")
+ assert command_transmit(frame) == b"T,E,73700A2,2,3412\n"
+ assert command_setup() == (b"S500\n", b"M0\n", b"B512000\n", b"Q3\n")
From 34f728cfaa4ee0d4bf8d71dbb6c9922d606a0c8b Mon Sep 17 00:00:00 2001
From: Andrey Kruchinkin
Date: Mon, 7 Sep 2026 08:41:26 +0300
Subject: [PATCH 30/37] =?UTF-8?q?fix(set-protocol):=20=D1=83=D0=B1=D0=B5?=
=?UTF-8?q?=D1=80=D0=B8=20libc=20=D0=B8=D0=B7=20=D0=BF=D0=BE=D1=80=D1=82?=
=?UTF-8?q?=D0=B0=20DevBoard=20F407?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ports/stm32f407-devboard-v1/README.md | 2 +-
.../set_devboard_v1_stm32f407.c | 15 ++++++++-------
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/c/set-protocol/ports/stm32f407-devboard-v1/README.md b/c/set-protocol/ports/stm32f407-devboard-v1/README.md
index 83f56ef..1994902 100644
--- a/c/set-protocol/ports/stm32f407-devboard-v1/README.md
+++ b/c/set-protocol/ports/stm32f407-devboard-v1/README.md
@@ -11,7 +11,7 @@ CubeMX-проектом. Необработанные кадры передаю
Слой приложения предоставляет callback-и чтения и записи. Нулевой DLC означает
чтение; данные в кадре `HOLDING`/`COIL` — запись. За кадр передаются до четырёх
-16-битных регистров или до шестнадцати coils.
+16-битных регистров или до пятнадцати coils (предел 4-битного `RegCount`).
```c
static set_devboard_v1_stm32f407_t gui_can;
diff --git a/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c
index 9f4a98d..af72ac5 100644
--- a/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c
+++ b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c
@@ -1,22 +1,21 @@
#include "set_devboard_v1_stm32f407.h"
-#include
-
static bool transmit(set_devboard_v1_stm32f407_t *port,
const pcan_frame_t *frame)
{
- CAN_TxHeaderTypeDef header;
+ CAN_TxHeaderTypeDef header = {0};
uint32_t mailbox = 0U;
uint8_t data[8] = {0U};
- memset(&header, 0, sizeof(header));
header.ExtId = frame->id & 0x1FFFFFFFUL;
header.IDE = CAN_ID_EXT;
header.RTR = ((frame->flags & PCAN_FLAG_RTR) != 0U) ?
CAN_RTR_REMOTE : CAN_RTR_DATA;
header.DLC = frame->dlc;
header.TransmitGlobalTime = DISABLE;
- memcpy(data, frame->data, frame->dlc);
+ for (uint8_t index = 0U; index < frame->dlc; ++index) {
+ data[index] = frame->data[index];
+ }
if (HAL_CAN_AddTxMessage(port->can, &header, data, &mailbox) != HAL_OK) {
port->tx_errors++;
return false;
@@ -34,7 +33,7 @@ bool set_devboard_v1_stm32f407_init(
if ((port == NULL) || (can == NULL)) {
return false;
}
- memset(port, 0, sizeof(*port));
+ *port = (set_devboard_v1_stm32f407_t){0};
port->can = can;
port->unhandled = unhandled;
port->unhandled_user = unhandled_user;
@@ -65,7 +64,9 @@ size_t set_devboard_v1_stm32f407_poll(set_devboard_v1_stm32f407_t *port)
request.flags |= PCAN_FLAG_RTR;
}
request.dlc = (header.DLC > 8U) ? 8U : (uint8_t)header.DLC;
- memcpy(request.data, data, request.dlc);
+ for (uint8_t index = 0U; index < request.dlc; ++index) {
+ request.data[index] = data[index];
+ }
result = pcan_modbus_server_handle(&port->server, &request, &response);
if (result == PCAN_MODBUS_RESPONSE) {
From 49c65ea78d6005772f094140a194d74fc607cec4 Mon Sep 17 00:00:00 2001
From: Andrey Kruchinkin
Date: Mon, 7 Sep 2026 08:42:02 +0300
Subject: [PATCH 31/37] =?UTF-8?q?fix(set-protocol):=20=D1=81=D0=BE=D1=85?=
=?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B8=20=D1=81=D0=BE=D0=B2=D0=BC=D0=B5=D1=81?=
=?UTF-8?q?=D1=82=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=BE=D1=80?=
=?UTF-8?q?=D1=82=D0=B0=20=D1=81=20ARMCC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c
index af72ac5..2566f87 100644
--- a/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c
+++ b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c
@@ -33,10 +33,12 @@ bool set_devboard_v1_stm32f407_init(
if ((port == NULL) || (can == NULL)) {
return false;
}
- *port = (set_devboard_v1_stm32f407_t){0};
port->can = can;
port->unhandled = unhandled;
port->unhandled_user = unhandled_user;
+ port->rx_frames = 0U;
+ port->tx_frames = 0U;
+ port->tx_errors = 0U;
return pcan_modbus_server_init(&port->server, device_type, device_id,
read, write, register_user);
}
From 8e9fd144ca47b9fd4063155ca46d3c29b6d9e0b1 Mon Sep 17 00:00:00 2001
From: Andrey Kruchinkin
Date: Tue, 15 Sep 2026 15:57:06 +0300
Subject: [PATCH 32/37] =?UTF-8?q?=D0=A1=D0=BE=D1=85=D1=80=D0=B0=D0=BD?=
=?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=82=D0=B5=D0=BA=D1=83=D1=89=D0=B8=D0=B5?=
=?UTF-8?q?=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20NAN?=
=?UTF-8?q?D=20=D0=B8=20LED=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20=D0=BE?=
=?UTF-8?q?=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5?=
=?UTF-8?q?=D0=BC=20=D0=B2=D0=B5=D1=82=D0=BE=D0=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
NEW_PROJECT.md | 22 ++
README.md | 3 +-
c/led-indicator/CMakeLists.txt | 30 +++
c/led-indicator/README.md | 126 +++++++++
c/led-indicator/led_indicator.c | 246 ++++++++++++++++++
c/led-indicator/led_indicator.h | 117 +++++++++
.../ports/stm32-hal/led_indicator_stm32_hal.c | 65 +++++
.../ports/stm32-hal/led_indicator_stm32_hal.h | 39 +++
...d_indicator_stm32_hal_config.f1.template.h | 6 +
...d_indicator_stm32_hal_config.f4.template.h | 6 +
...d_indicator_stm32_hal_config.g4.template.h | 6 +
.../fakes/led_indicator_stm32_hal_config.h | 27 ++
c/led-indicator/tests/test_led_indicator.c | 84 ++++++
c/led-indicator/tests/test_stm32_port.c | 52 ++++
c/parallel-nand/README.md | 37 ++-
c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md | 6 +-
c/parallel-nand/docs/MEMORY_GAS.md | 42 ++-
c/parallel-nand/include/parallel_nand.h | 27 +-
c/parallel-nand/include/parallel_nand_gas.h | 13 +-
c/parallel-nand/include/parallel_nand_port.h | 2 +
c/parallel-nand/ports/stm32f103rc/README.md | 51 ++++
.../parallel_nand_port_stm32f103rc.c | 143 ++++++++++
.../parallel_nand_port_stm32f103rc.h | 16 ++
c/parallel-nand/ports/stm32f103ze/README.md | 62 +++++
.../parallel_nand_port_stm32f103ze.c | 184 +++++++++++++
.../parallel_nand_port_stm32f103ze.h | 16 ++
c/parallel-nand/ports/stm32f407ve/README.md | 5 +
.../parallel_nand_port_stm32f407ve.c | 3 +-
c/parallel-nand/ports/stm32g474ce/README.md | 3 +
.../parallel_nand_port_stm32g474ce.c | 3 +-
c/parallel-nand/ports/tms320f2812/README.md | 7 +-
.../parallel_nand_port_tms320f2812.c | 3 +-
c/parallel-nand/src/parallel_nand.c | 117 ++++++++-
c/parallel-nand/src/parallel_nand_gas.c | 124 ++++++++-
c/parallel-nand/tests/test_parallel_nand.c | 179 ++++++++++++-
35 files changed, 1841 insertions(+), 31 deletions(-)
create mode 100644 c/led-indicator/CMakeLists.txt
create mode 100644 c/led-indicator/README.md
create mode 100644 c/led-indicator/led_indicator.c
create mode 100644 c/led-indicator/led_indicator.h
create mode 100644 c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.c
create mode 100644 c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.h
create mode 100644 c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.f1.template.h
create mode 100644 c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.f4.template.h
create mode 100644 c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.g4.template.h
create mode 100644 c/led-indicator/tests/fakes/led_indicator_stm32_hal_config.h
create mode 100644 c/led-indicator/tests/test_led_indicator.c
create mode 100644 c/led-indicator/tests/test_stm32_port.c
create mode 100644 c/parallel-nand/ports/stm32f103rc/README.md
create mode 100644 c/parallel-nand/ports/stm32f103rc/parallel_nand_port_stm32f103rc.c
create mode 100644 c/parallel-nand/ports/stm32f103rc/parallel_nand_port_stm32f103rc.h
create mode 100644 c/parallel-nand/ports/stm32f103ze/README.md
create mode 100644 c/parallel-nand/ports/stm32f103ze/parallel_nand_port_stm32f103ze.c
create mode 100644 c/parallel-nand/ports/stm32f103ze/parallel_nand_port_stm32f103ze.h
diff --git a/NEW_PROJECT.md b/NEW_PROJECT.md
index 8e5bc49..e6137d3 100644
--- a/NEW_PROJECT.md
+++ b/NEW_PROJECT.md
@@ -62,6 +62,27 @@ G431 и G474 намеренно используют один исходник
этой задачи совместимы. Раздельными остаются конфигурация платы, startup,
linker script и выбранный CMSIS device define.
+## Выбор готового порта parallel NAND
+
+| Целевой МК | Реализация шины | Каталог порта |
+|---|---|---|
+| TMS320F2812 | XINTF Zone 6 | `c/parallel-nand/ports/tms320f2812` |
+| STM32F103RCT6 | GPIO bit-bang, поскольку FSMC недоступен в LQFP64 | `c/parallel-nand/ports/stm32f103rc` |
+| STM32F103ZET6 | FSMC NAND Bank 3 | `c/parallel-nand/ports/stm32f103ze` |
+| STM32F407VET6 | FSMC NAND Bank 2 | `c/parallel-nand/ports/stm32f407ve` |
+| STM32G474CEU6 | GPIO bit-bang для UFQFPN48 | `c/parallel-nand/ports/stm32g474ce` |
+
+Ядро поддерживает профили Micron `MT29F1G08ABADA` и Hynix
+`HY27UF084G2M`. В сборку добавляются `src/parallel_nand.c`, при необходимости
+`src/parallel_nand_gas.c` или `src/parallel_nand_pcan_gas.c`, и ровно один
+аппаратный порт. Конкретная распиновка, требуемые модули HAL и пример
+инициализации приведены в README выбранного порта.
+
+Для `PROGRAM`/`ERASE` линия `WP#` должна управляться портом. Готовые порты
+STM32F103RC, STM32F103ZE и STM32G474CE объявляют поддержку записи. Базовые
+схемы TMS320F2812 и STM32F407VE фиксируют `WP#` аппаратно и безопасно
+возвращают `PNAND_ERROR_WRITE_PROTECTED`, пока плата и порт не доработаны.
+
Рабочий пример полноценного G474-порта находится в соседнем проекте
`candleLight_fw`: `src/device/device_g4.c` отвечает за clock/GPIO,
`src/can/m_can.c` — за FDCAN, а `include/config.h` — за привязку платы
@@ -77,6 +98,7 @@ USB gs_usb и конкретной разводки адаптера; перен
| дисплей ST7789 | `c/st7789` | SPI, DC, CS, RESET, подсветка и задержка |
| кнопки | `c/keypad` | настройка GPIO и чтение уровней |
| меню | `c/menu` | только привязка painter к дисплею |
+| индикация состояний | `c/led-indicator` | готовый STM32 HAL-порт или callbacks GPIO и выбранного таймера |
| EEPROM FT24C256 | `c/eeprom-ft24c256` | I2C write/write-read и задержка |
| CAN SETTINGS | `c/can-sensor` | bxCAN для F103 либо FDCAN для G431/G474 |
diff --git a/README.md b/README.md
index b8eb5f8..4b68a35 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ templates/
| [`c/st7789`](c/st7789) | TFT ST7789V по SPI, RGB565, текст без кадрового буфера | `stdint.h` | SPI-запись, линия DC, задержка |
| [`c/keypad`](c/keypad) | шесть кнопок: антидребезг, автоповтор, удержание, очередь событий | `stdint.h` | чтение уровня кнопки, время в мс |
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `stdint.h` | заливка прямоугольника, вывод строки |
+| [`c/led-indicator`](c/led-indicator) | неблокирующие режимы LED: работа, активность, предупреждение и ошибка | C99 | логическая запись канала, время выбранного таймера — **STM32F1/F4/G4 HAL-порт в комплекте** |
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
| [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 |
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
@@ -40,7 +41,7 @@ templates/
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
| [`c/firmware-info`](c/firmware-info) | SemVer, build stamp и git build ID работающего образа | C99 | config-порты STM32F1/F4/G4 и К1921ВК028; упаковка для Modbus/SETGUI |
-| [`c/parallel-nand`](c/parallel-nand) | parallel NAND x8: raw main/OOB, bad-block marker и банковое окно GAS для SETGUI | C99, опционально `set-protocol/pcan_gas` | **порты TMS320F2812, STM32F407VE и STM32G474CEU6 в комплекте** |
+| [`c/parallel-nand`](c/parallel-nand) | parallel NAND x8: raw read/program/erase, профили Micron MT29F1G08 и Hynix HY27UF084G2M, bad-block marker и банковое окно GAS для SETGUI | C99, опционально `set-protocol/pcan_gas` | **порты TMS320F2812, STM32F103RC/ZE, STM32F407VE и STM32G474CEU6; запись требует управляемого WP#** |
### Python
diff --git a/c/led-indicator/CMakeLists.txt b/c/led-indicator/CMakeLists.txt
new file mode 100644
index 0000000..ffabcdf
--- /dev/null
+++ b/c/led-indicator/CMakeLists.txt
@@ -0,0 +1,30 @@
+cmake_minimum_required(VERSION 3.13)
+project(led_indicator C)
+
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+
+add_library(led_indicator STATIC led_indicator.c)
+target_include_directories(led_indicator PUBLIC .)
+
+if(MSVC)
+ target_compile_options(led_indicator PRIVATE /W4)
+else()
+ target_compile_options(led_indicator PRIVATE -Wall -Wextra -Wpedantic)
+endif()
+
+option(LED_INDICATOR_BUILD_TESTS "Собирать тесты LED indicator" ON)
+if(LED_INDICATOR_BUILD_TESTS)
+ enable_testing()
+ add_executable(test_led_indicator tests/test_led_indicator.c)
+ target_link_libraries(test_led_indicator PRIVATE led_indicator)
+ add_test(NAME led_indicator COMMAND test_led_indicator)
+
+ add_executable(test_led_indicator_stm32_port
+ tests/test_stm32_port.c
+ ports/stm32-hal/led_indicator_stm32_hal.c)
+ target_include_directories(test_led_indicator_stm32_port PRIVATE
+ . ports/stm32-hal tests/fakes)
+ target_link_libraries(test_led_indicator_stm32_port PRIVATE led_indicator)
+ add_test(NAME led_indicator_stm32_port COMMAND test_led_indicator_stm32_port)
+endif()
diff --git a/c/led-indicator/README.md b/c/led-indicator/README.md
new file mode 100644
index 0000000..7b36a85
--- /dev/null
+++ b/c/led-indicator/README.md
@@ -0,0 +1,126 @@
+# LED Indicator
+
+Неблокирующая C99-библиотека для индикации состояний `STARTUP`, `WORK`,
+`ACTIVITY`, `WARNING`, `ERROR` и `CRITICAL` с разными рисунками и частотами.
+Один экземпляр обслуживает несколько светодиодов.
+
+Ядро не знает о GPIO, PWM, сдвиговом регистре, RTOS и модели таймера. Порт
+передаёт логическое состояние канала наружу и читает монотонное время в
+миллисекундах. В библиотеке нет задержек, динамической памяти, прерываний и
+изменяемого глобального состояния.
+
+```text
+приложение -> LedIndicator_SetMode/Process -> ядро -> write/now_ms -> GPIO/PWM/expander + TIM
+```
+
+## Файлы
+
+| Файл | Назначение | Зависимости |
+|---|---|---|
+| `led_indicator.h/.c` | режимы, шаблоны и планировщик | C99, `stdint.h` |
+| `ports/stm32-hal` | GPIO и выбранный аппаратный TIM | STM32 HAL |
+| `tests/test_led_indicator.c` | host-тесты, включая переполнение `uint32_t` | libc |
+
+## Встроенные режимы
+
+| Режим | Сигнал |
+|---|---|
+| `OFF` / `ON` | постоянно выключен / включён |
+| `STARTUP` | три коротких импульса, затем постоянно включён |
+| `WORK` | 100 мс включён, 900 мс выключен |
+| `ACTIVITY` | 50/50 мс |
+| `WARNING` | 250/250 мс |
+| `ERROR` | два импульса и пауза |
+| `CRITICAL` | три импульса и пауза |
+
+Частоты не зашиты в алгоритм. Скопируйте встроенную таблицу и измените
+`duration_ms` до инициализации:
+
+```c
+static LedIndicator_Pattern patterns[LED_INDICATOR_MODE_COUNT];
+
+LedIndicator_CopyDefaultPatterns(patterns, LED_INDICATOR_MODE_COUNT);
+patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] = 50U;
+patterns[LED_INDICATOR_MODE_WORK].duration_ms[1] = 1950U;
+config.patterns = patterns;
+config.pattern_count = LED_INDICATOR_MODE_COUNT;
+```
+
+Таблица должна существовать всё время работы экземпляра. До восьми шагов
+описываются длительностями и битовой маской `levels`; шаблон может повторяться
+или после одного прохода перейти в `final_level`.
+
+## Контракт порта
+
+```c
+void write(void *context, uint8_t channel, uint8_t logical_on);
+uint32_t now_ms(void *context);
+```
+
+`write` получает именно логический уровень. Инверсию active-low, управление
+PWM или запись общего регистра расширителя выполняет порт. Переполнение
+32-битной миллисекундной метки обработано разностью беззнаковых чисел.
+
+Если время уже есть в планировщике приложения, `now_ms` можно оставить `NULL`
+и вызывать варианты `LedIndicator_SetModeAt`/`LedIndicator_ProcessAt`.
+
+## Быстрый старт без привязки к HAL
+
+```c
+static LedIndicator led;
+static LedIndicator_Channel state[2];
+LedIndicator_Config cfg;
+LedIndicator_Port port = {Board_LedWrite, Board_TimerMs, &board};
+
+LedIndicator_ConfigDefault(&cfg);
+LedIndicator_Init(&led, state, 2U, &port, &cfg);
+LedIndicator_SetMode(&led, 0U, LED_INDICATOR_MODE_WORK);
+LedIndicator_SetMode(&led, 1U, LED_INDICATOR_MODE_ERROR);
+
+for (;;) {
+ LedIndicator_Process(&led);
+}
+```
+
+`SetMode` идемпотентен: повторный вызов того же режима в каждом проходе цикла
+не начинает рисунок заново. Для нового импульса события служит
+`LedIndicator_RestartMode`.
+
+## STM32F103, STM32F4, STM32G431 и STM32G474
+
+Порт `ports/stm32-hal` принимает конкретный `TIM_HandleTypeDef *`; библиотека
+не использует `HAL_GetTick()` и не занимает SysTick. Настройте TIM как
+free-running, запустите его и передайте частоту счётчика после prescaler.
+
+Скопируйте подходящий `led_indicator_stm32_hal_config.*.template.h` в каталог
+платы под именем `led_indicator_stm32_hal_config.h`.
+
+```c
+static const LedIndicator_Stm32HalOutput outputs[] = {
+ {STATUS_GPIO_Port, STATUS_Pin, 0U},
+ {ERROR_GPIO_Port, ERROR_Pin, 1U}
+};
+static LedIndicator_Stm32HalPort hw;
+static LedIndicator_Port port;
+
+HAL_TIM_Base_Start(&htim6); /* CNT = 1 кГц в данном примере. */
+LedIndicator_Stm32HalPortInit(&hw, &htim6, 1000U,
+ outputs, 2U, &port);
+```
+
+`LedIndicator_Process()` должен вызываться чаще, чем переполняется выбранный
+TIM. Для 16-битного CNT на 1 кГц это не реже одного раза за 65 секунд. Сам
+таймер и его prescaler/period задаются в CubeMX или board-порте, а не в ядре.
+
+Для К1921ВК028 и C28x используется тот же основной порт: `now_ms` возвращает
+счётчик, увеличиваемый обработчиком выбранного TIMER/CPU Timer, а `write`
+обращается к GPIO SDK. Такое разделение оставляет номер таймера и выводы в
+проекте конкретной платы.
+
+## Проверка
+
+```sh
+cmake -S c/led-indicator -B build/led-indicator
+cmake --build build/led-indicator
+ctest --test-dir build/led-indicator --output-on-failure
+```
diff --git a/c/led-indicator/led_indicator.c b/c/led-indicator/led_indicator.c
new file mode 100644
index 0000000..930ff21
--- /dev/null
+++ b/c/led-indicator/led_indicator.c
@@ -0,0 +1,246 @@
+#include "led_indicator.h"
+
+#include
+
+#define LEVELS_1 0x01U
+#define LEVELS_10 0x01U
+#define LEVELS_1010 0x05U
+#define LEVELS_101010 0x15U
+
+static const LedIndicator_Pattern g_default_patterns[LED_INDICATOR_MODE_COUNT] = {
+ {{0U}, 0U, 1U, 1U, 0U},
+ {{0U}, LEVELS_1, 1U, 1U, 1U},
+ {{100U, 100U, 100U, 100U, 100U, 100U}, LEVELS_101010, 6U, 0U, 1U},
+ {{100U, 900U}, LEVELS_10, 2U, 1U, 0U},
+ {{50U, 50U}, LEVELS_10, 2U, 1U, 0U},
+ {{250U, 250U}, LEVELS_10, 2U, 1U, 0U},
+ {{100U, 100U, 100U, 700U}, LEVELS_1010, 4U, 1U, 0U},
+ {{100U, 100U, 100U, 100U, 100U, 700U}, LEVELS_101010, 6U, 1U, 0U}
+};
+
+static uint8_t pattern_valid(const LedIndicator_Pattern *pattern)
+{
+ size_t i;
+ uint32_t total = 0U;
+
+ if ((pattern == NULL) || (pattern->step_count == 0U) ||
+ (pattern->step_count > LED_INDICATOR_MAX_STEPS)) {
+ return 0U;
+ }
+ if (pattern->step_count == 1U) {
+ return 1U;
+ }
+ for (i = 0U; i < pattern->step_count; ++i) {
+ if ((pattern->duration_ms[i] == 0U) ||
+ (UINT32_MAX - total < pattern->duration_ms[i])) {
+ return 0U;
+ }
+ total += pattern->duration_ms[i];
+ }
+ return (uint8_t)(total != 0U);
+}
+
+static uint32_t pattern_period(const LedIndicator_Pattern *pattern)
+{
+ size_t i;
+ uint32_t total = 0U;
+
+ for (i = 0U; i < pattern->step_count; ++i) {
+ total += pattern->duration_ms[i];
+ }
+ return total;
+}
+
+static uint8_t pattern_level(const LedIndicator_Pattern *pattern, uint32_t elapsed)
+{
+ uint32_t position = elapsed;
+ uint32_t period;
+ size_t i;
+
+ if (pattern->step_count == 1U) {
+ return (uint8_t)(pattern->levels & 1U);
+ }
+
+ period = pattern_period(pattern);
+ if (pattern->repeat != 0U) {
+ position %= period;
+ } else if (position >= period) {
+ return (uint8_t)(pattern->final_level != 0U);
+ }
+
+ for (i = 0U; i < pattern->step_count; ++i) {
+ if (position < pattern->duration_ms[i]) {
+ return (uint8_t)((pattern->levels >> i) & 1U);
+ }
+ position -= pattern->duration_ms[i];
+ }
+ return (uint8_t)(pattern->final_level != 0U);
+}
+
+static void write_if_changed(LedIndicator *instance, size_t channel, uint8_t level,
+ uint8_t force)
+{
+ LedIndicator_Channel *state = &instance->channels[channel];
+
+ level = (uint8_t)(level != 0U);
+ if ((force != 0U) || (state->output_level != level)) {
+ state->output_level = level;
+ instance->port.write(instance->port.context, (uint8_t)channel, level);
+ }
+}
+
+void LedIndicator_ConfigDefault(LedIndicator_Config *config)
+{
+ if (config != NULL) {
+ config->patterns = g_default_patterns;
+ config->pattern_count = LED_INDICATOR_MODE_COUNT;
+ }
+}
+
+size_t LedIndicator_CopyDefaultPatterns(LedIndicator_Pattern *patterns, size_t capacity)
+{
+ size_t i;
+ size_t count = (size_t)LED_INDICATOR_MODE_COUNT;
+
+ if (patterns == NULL) {
+ return 0U;
+ }
+ if (capacity < count) {
+ count = capacity;
+ }
+ for (i = 0U; i < count; ++i) {
+ patterns[i] = g_default_patterns[i];
+ }
+ return count;
+}
+
+uint8_t LedIndicator_ValidateConfig(const LedIndicator_Config *config)
+{
+ size_t i;
+
+ if ((config == NULL) || (config->patterns == NULL) ||
+ (config->pattern_count == 0U) ||
+ (config->pattern_count > (size_t)LED_INDICATOR_MODE_COUNT)) {
+ return 0U;
+ }
+ for (i = 0U; i < config->pattern_count; ++i) {
+ if (pattern_valid(&config->patterns[i]) == 0U) {
+ return 0U;
+ }
+ }
+ return 1U;
+}
+
+uint8_t LedIndicator_Init(LedIndicator *instance,
+ LedIndicator_Channel *channels,
+ size_t channel_count,
+ const LedIndicator_Port *port,
+ const LedIndicator_Config *config)
+{
+ size_t i;
+
+ if ((instance == NULL) || (channels == NULL) || (channel_count == 0U) ||
+ (channel_count > 256U) || (port == NULL) || (port->write == NULL) ||
+ (LedIndicator_ValidateConfig(config) == 0U)) {
+ return 0U;
+ }
+
+ instance->port = *port;
+ instance->patterns = config->patterns;
+ instance->pattern_count = config->pattern_count;
+ instance->channels = channels;
+ instance->channel_count = channel_count;
+ instance->initialized = 1U;
+
+ for (i = 0U; i < channel_count; ++i) {
+ channels[i].mode = LED_INDICATOR_MODE_OFF;
+ channels[i].started_ms = 0U;
+ channels[i].output_level = 0U;
+ channels[i].initialized = 1U;
+ write_if_changed(instance, i, 0U, 1U);
+ }
+ return 1U;
+}
+
+uint8_t LedIndicator_SetModeAt(LedIndicator *instance, size_t channel,
+ LedIndicator_Mode mode, uint32_t now_ms)
+{
+ if ((instance == NULL) || (instance->initialized == 0U) ||
+ (channel >= instance->channel_count) || ((size_t)mode >= instance->pattern_count)) {
+ return 0U;
+ }
+ if (instance->channels[channel].mode == mode) {
+ return 1U;
+ }
+ return LedIndicator_RestartModeAt(instance, channel, mode, now_ms);
+}
+
+uint8_t LedIndicator_RestartModeAt(LedIndicator *instance, size_t channel,
+ LedIndicator_Mode mode, uint32_t now_ms)
+{
+ LedIndicator_Channel *state;
+ const LedIndicator_Pattern *pattern;
+
+ if ((instance == NULL) || (instance->initialized == 0U) ||
+ (channel >= instance->channel_count) || ((size_t)mode >= instance->pattern_count)) {
+ return 0U;
+ }
+
+ state = &instance->channels[channel];
+ state->mode = mode;
+ state->started_ms = now_ms;
+ pattern = &instance->patterns[(size_t)mode];
+ write_if_changed(instance, channel, pattern_level(pattern, 0U), 0U);
+ return 1U;
+}
+
+uint8_t LedIndicator_SetMode(LedIndicator *instance, size_t channel,
+ LedIndicator_Mode mode)
+{
+ if ((instance == NULL) || (instance->port.now_ms == NULL)) {
+ return 0U;
+ }
+ return LedIndicator_SetModeAt(instance, channel, mode,
+ instance->port.now_ms(instance->port.context));
+}
+
+uint8_t LedIndicator_RestartMode(LedIndicator *instance, size_t channel,
+ LedIndicator_Mode mode)
+{
+ if ((instance == NULL) || (instance->port.now_ms == NULL)) {
+ return 0U;
+ }
+ return LedIndicator_RestartModeAt(instance, channel, mode,
+ instance->port.now_ms(instance->port.context));
+}
+
+void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms)
+{
+ size_t i;
+
+ if ((instance == NULL) || (instance->initialized == 0U)) {
+ return;
+ }
+ for (i = 0U; i < instance->channel_count; ++i) {
+ const LedIndicator_Channel *state = &instance->channels[i];
+ const LedIndicator_Pattern *pattern = &instance->patterns[(size_t)state->mode];
+ const uint32_t elapsed = now_ms - state->started_ms;
+ write_if_changed(instance, i, pattern_level(pattern, elapsed), 0U);
+ }
+}
+
+void LedIndicator_Process(LedIndicator *instance)
+{
+ if ((instance != NULL) && (instance->port.now_ms != NULL)) {
+ LedIndicator_ProcessAt(instance, instance->port.now_ms(instance->port.context));
+ }
+}
+
+LedIndicator_Mode LedIndicator_GetMode(const LedIndicator *instance, size_t channel)
+{
+ if ((instance == NULL) || (instance->initialized == 0U) ||
+ (channel >= instance->channel_count)) {
+ return LED_INDICATOR_MODE_OFF;
+ }
+ return instance->channels[channel].mode;
+}
diff --git a/c/led-indicator/led_indicator.h b/c/led-indicator/led_indicator.h
new file mode 100644
index 0000000..b63b07e
--- /dev/null
+++ b/c/led-indicator/led_indicator.h
@@ -0,0 +1,117 @@
+/**
+ * @file led_indicator.h
+ * @brief Неблокирующая индикация состояния устройства на одном или нескольких LED.
+ */
+
+#ifndef LED_INDICATOR_H
+#define LED_INDICATOR_H
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define LED_INDICATOR_MAX_STEPS 8U
+
+typedef enum {
+ LED_INDICATOR_MODE_OFF = 0,
+ LED_INDICATOR_MODE_ON,
+ LED_INDICATOR_MODE_STARTUP,
+ LED_INDICATOR_MODE_WORK,
+ LED_INDICATOR_MODE_ACTIVITY,
+ LED_INDICATOR_MODE_WARNING,
+ LED_INDICATOR_MODE_ERROR,
+ LED_INDICATOR_MODE_CRITICAL,
+ LED_INDICATOR_MODE_COUNT
+} LedIndicator_Mode;
+
+/** Один период сигнала. Бит N в levels задаёт уровень шага N. */
+typedef struct {
+ uint32_t duration_ms[LED_INDICATOR_MAX_STEPS];
+ uint8_t levels;
+ uint8_t step_count;
+ uint8_t repeat;
+ uint8_t final_level;
+} LedIndicator_Pattern;
+
+/**
+ * Аппаратный порт. write получает логический уровень, поэтому active-low,
+ * GPIO, PWM и регистры расширителя обрабатываются за границей ядра.
+ */
+typedef struct {
+ void (*write)(void *context, uint8_t channel, uint8_t on);
+ uint32_t (*now_ms)(void *context);
+ void *context;
+} LedIndicator_Port;
+
+typedef struct {
+ LedIndicator_Mode mode;
+ uint32_t started_ms;
+ uint8_t output_level;
+ uint8_t initialized;
+} LedIndicator_Channel;
+
+typedef struct {
+ const LedIndicator_Pattern *patterns;
+ size_t pattern_count;
+} LedIndicator_Config;
+
+typedef struct {
+ LedIndicator_Port port;
+ const LedIndicator_Pattern *patterns;
+ LedIndicator_Channel *channels;
+ size_t pattern_count;
+ size_t channel_count;
+ uint8_t initialized;
+} LedIndicator;
+
+/** Заполняет конфигурацию встроенными шаблонами режимов. */
+void LedIndicator_ConfigDefault(LedIndicator_Config *config);
+
+/** Копирует встроенные шаблоны в изменяемую таблицу; возвращает число записей. */
+size_t LedIndicator_CopyDefaultPatterns(LedIndicator_Pattern *patterns, size_t capacity);
+
+/** Проверяет таблицу шаблонов. */
+uint8_t LedIndicator_ValidateConfig(const LedIndicator_Config *config);
+
+/**
+ * Инициализирует экземпляр и немедленно выключает все его каналы.
+ * Массив channels принадлежит приложению и должен жить столько же, сколько instance.
+ */
+uint8_t LedIndicator_Init(LedIndicator *instance,
+ LedIndicator_Channel *channels,
+ size_t channel_count,
+ const LedIndicator_Port *port,
+ const LedIndicator_Config *config);
+
+/** Назначает режим, используя время порта. Повтор того же режима не сбрасывает фазу. */
+uint8_t LedIndicator_SetMode(LedIndicator *instance, size_t channel,
+ LedIndicator_Mode mode);
+
+/** Назначает режим с явно переданной меткой времени. Повтор не сбрасывает фазу. */
+uint8_t LedIndicator_SetModeAt(LedIndicator *instance, size_t channel,
+ LedIndicator_Mode mode, uint32_t now_ms);
+
+/** Принудительно запускает режим с первого шага, используя время порта. */
+uint8_t LedIndicator_RestartMode(LedIndicator *instance, size_t channel,
+ LedIndicator_Mode mode);
+
+/** Принудительно запускает режим с первого шага в указанное время. */
+uint8_t LedIndicator_RestartModeAt(LedIndicator *instance, size_t channel,
+ LedIndicator_Mode mode, uint32_t now_ms);
+
+/** Обновляет выходы, используя время порта. Вызывать в главном цикле. */
+void LedIndicator_Process(LedIndicator *instance);
+
+/** Обновляет выходы с явно переданной меткой времени. */
+void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms);
+
+LedIndicator_Mode LedIndicator_GetMode(const LedIndicator *instance, size_t channel);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* LED_INDICATOR_H */
diff --git a/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.c b/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.c
new file mode 100644
index 0000000..fb44778
--- /dev/null
+++ b/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.c
@@ -0,0 +1,65 @@
+#include "led_indicator_stm32_hal.h"
+
+static void stm32_write(void *context, uint8_t channel, uint8_t on)
+{
+ LedIndicator_Stm32HalPort *adapter = (LedIndicator_Stm32HalPort *)context;
+ const LedIndicator_Stm32HalOutput *output;
+ GPIO_PinState state;
+
+ if ((adapter == NULL) || ((size_t)channel >= adapter->output_count)) {
+ return;
+ }
+ output = &adapter->outputs[channel];
+ state = ((on != 0U) ^ (output->active_low != 0U)) ? GPIO_PIN_SET : GPIO_PIN_RESET;
+ HAL_GPIO_WritePin(output->gpio, output->pin, state);
+}
+
+static uint32_t stm32_now_ms(void *context)
+{
+ LedIndicator_Stm32HalPort *adapter = (LedIndicator_Stm32HalPort *)context;
+ const uint32_t current = __HAL_TIM_GET_COUNTER(adapter->timer);
+ const uint32_t reload = __HAL_TIM_GET_AUTORELOAD(adapter->timer);
+ uint32_t delta;
+ uint64_t scaled;
+
+ if (current >= adapter->last_counter) {
+ delta = current - adapter->last_counter;
+ } else if (reload == UINT32_MAX) {
+ delta = current - adapter->last_counter;
+ } else {
+ delta = (reload - adapter->last_counter) + 1U + current;
+ }
+ adapter->last_counter = current;
+
+ scaled = (uint64_t)adapter->remainder + ((uint64_t)delta * 1000ULL);
+ adapter->accumulated_ms += (uint32_t)(scaled / adapter->timer_tick_hz);
+ adapter->remainder = (uint32_t)(scaled % adapter->timer_tick_hz);
+ return adapter->accumulated_ms;
+}
+
+uint8_t LedIndicator_Stm32HalPortInit(LedIndicator_Stm32HalPort *adapter,
+ TIM_HandleTypeDef *timer,
+ uint32_t timer_tick_hz,
+ const LedIndicator_Stm32HalOutput *outputs,
+ size_t output_count,
+ LedIndicator_Port *port)
+{
+ if ((adapter == NULL) || (timer == NULL) || (timer_tick_hz == 0U) ||
+ (outputs == NULL) || (output_count == 0U) || (output_count > 256U) ||
+ (port == NULL)) {
+ return 0U;
+ }
+
+ adapter->timer = timer;
+ adapter->outputs = outputs;
+ adapter->output_count = output_count;
+ adapter->timer_tick_hz = timer_tick_hz;
+ adapter->last_counter = __HAL_TIM_GET_COUNTER(timer);
+ adapter->accumulated_ms = 0U;
+ adapter->remainder = 0U;
+
+ port->write = stm32_write;
+ port->now_ms = stm32_now_ms;
+ port->context = adapter;
+ return 1U;
+}
diff --git a/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.h b/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.h
new file mode 100644
index 0000000..28b4aca
--- /dev/null
+++ b/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal.h
@@ -0,0 +1,39 @@
+/**
+ * @file led_indicator_stm32_hal.h
+ * @brief Порт LED Indicator для STM32F1/F4/G4 HAL и выбранного TIM.
+ */
+
+#ifndef LED_INDICATOR_STM32_HAL_H
+#define LED_INDICATOR_STM32_HAL_H
+
+#include "led_indicator.h"
+#include "led_indicator_stm32_hal_config.h"
+
+typedef struct {
+ GPIO_TypeDef *gpio;
+ uint16_t pin;
+ uint8_t active_low;
+} LedIndicator_Stm32HalOutput;
+
+typedef struct {
+ TIM_HandleTypeDef *timer;
+ const LedIndicator_Stm32HalOutput *outputs;
+ size_t output_count;
+ uint32_t timer_tick_hz;
+ uint32_t last_counter;
+ uint32_t accumulated_ms;
+ uint32_t remainder;
+} LedIndicator_Stm32HalPort;
+
+/**
+ * Создаёт порт на уже настроенном и запущенном таймере.
+ * timer_tick_hz — частота изменения CNT после prescaler, например 1000 Гц.
+ */
+uint8_t LedIndicator_Stm32HalPortInit(LedIndicator_Stm32HalPort *adapter,
+ TIM_HandleTypeDef *timer,
+ uint32_t timer_tick_hz,
+ const LedIndicator_Stm32HalOutput *outputs,
+ size_t output_count,
+ LedIndicator_Port *port);
+
+#endif /* LED_INDICATOR_STM32_HAL_H */
diff --git a/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.f1.template.h b/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.f1.template.h
new file mode 100644
index 0000000..85038d3
--- /dev/null
+++ b/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.f1.template.h
@@ -0,0 +1,6 @@
+#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
+#define LED_INDICATOR_STM32_HAL_CONFIG_H
+
+#include "stm32f1xx_hal.h"
+
+#endif
diff --git a/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.f4.template.h b/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.f4.template.h
new file mode 100644
index 0000000..10e25b1
--- /dev/null
+++ b/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.f4.template.h
@@ -0,0 +1,6 @@
+#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
+#define LED_INDICATOR_STM32_HAL_CONFIG_H
+
+#include "stm32f4xx_hal.h"
+
+#endif
diff --git a/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.g4.template.h b/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.g4.template.h
new file mode 100644
index 0000000..d157754
--- /dev/null
+++ b/c/led-indicator/ports/stm32-hal/led_indicator_stm32_hal_config.g4.template.h
@@ -0,0 +1,6 @@
+#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
+#define LED_INDICATOR_STM32_HAL_CONFIG_H
+
+#include "stm32g4xx_hal.h"
+
+#endif
diff --git a/c/led-indicator/tests/fakes/led_indicator_stm32_hal_config.h b/c/led-indicator/tests/fakes/led_indicator_stm32_hal_config.h
new file mode 100644
index 0000000..6e9cf41
--- /dev/null
+++ b/c/led-indicator/tests/fakes/led_indicator_stm32_hal_config.h
@@ -0,0 +1,27 @@
+#ifndef LED_INDICATOR_STM32_HAL_CONFIG_H
+#define LED_INDICATOR_STM32_HAL_CONFIG_H
+
+#include
+
+typedef enum {
+ GPIO_PIN_RESET = 0,
+ GPIO_PIN_SET
+} GPIO_PinState;
+
+typedef struct {
+ uint16_t last_pin;
+ GPIO_PinState last_state;
+ uint32_t writes;
+} GPIO_TypeDef;
+
+typedef struct {
+ uint32_t counter;
+ uint32_t autoreload;
+} TIM_HandleTypeDef;
+
+#define __HAL_TIM_GET_COUNTER(handle) ((handle)->counter)
+#define __HAL_TIM_GET_AUTORELOAD(handle) ((handle)->autoreload)
+
+void HAL_GPIO_WritePin(GPIO_TypeDef *gpio, uint16_t pin, GPIO_PinState state);
+
+#endif
diff --git a/c/led-indicator/tests/test_led_indicator.c b/c/led-indicator/tests/test_led_indicator.c
new file mode 100644
index 0000000..b1aa9c6
--- /dev/null
+++ b/c/led-indicator/tests/test_led_indicator.c
@@ -0,0 +1,84 @@
+#include "led_indicator.h"
+
+#include
+
+typedef struct {
+ uint32_t now;
+ uint8_t outputs[3];
+ uint32_t writes[3];
+} FakePort;
+
+static void fake_write(void *context, uint8_t channel, uint8_t on)
+{
+ FakePort *fake = (FakePort *)context;
+ fake->outputs[channel] = on;
+ fake->writes[channel]++;
+}
+
+static uint32_t fake_now(void *context)
+{
+ return ((FakePort *)context)->now;
+}
+
+#define CHECK(condition) \
+ do { \
+ if (!(condition)) { \
+ fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \
+ #condition); \
+ return 1; \
+ } \
+ } while (0)
+
+int main(void)
+{
+ LedIndicator indicator;
+ LedIndicator_Channel channels[3];
+ LedIndicator_Config config;
+ LedIndicator_Pattern patterns[LED_INDICATOR_MODE_COUNT];
+ FakePort fake = {0};
+ LedIndicator_Port port = {fake_write, fake_now, &fake};
+
+ LedIndicator_ConfigDefault(&config);
+ CHECK(LedIndicator_ValidateConfig(&config) != 0U);
+ CHECK(LedIndicator_CopyDefaultPatterns(patterns, LED_INDICATOR_MODE_COUNT) ==
+ LED_INDICATOR_MODE_COUNT);
+ patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] = 25U;
+ CHECK(patterns[LED_INDICATOR_MODE_WORK].duration_ms[0] == 25U);
+ CHECK(LedIndicator_Init(&indicator, channels, 3U, &port, &config) != 0U);
+ CHECK(fake.writes[0] == 1U && fake.writes[1] == 1U && fake.writes[2] == 1U);
+
+ CHECK(LedIndicator_SetMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
+ CHECK(fake.outputs[0] == 1U);
+ fake.now = 99U;
+ CHECK(LedIndicator_SetMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
+ LedIndicator_Process(&indicator);
+ CHECK(fake.outputs[0] == 1U);
+ fake.now = 100U;
+ LedIndicator_Process(&indicator);
+ CHECK(fake.outputs[0] == 0U);
+ fake.now = 1000U;
+ LedIndicator_Process(&indicator);
+ CHECK(fake.outputs[0] == 1U);
+ fake.now = 1050U;
+ CHECK(LedIndicator_RestartMode(&indicator, 0U, LED_INDICATOR_MODE_WORK) != 0U);
+ fake.now = 1150U;
+ LedIndicator_Process(&indicator);
+ CHECK(fake.outputs[0] == 0U);
+
+ CHECK(LedIndicator_SetModeAt(&indicator, 1U, LED_INDICATOR_MODE_ERROR,
+ 0xFFFFFFF0UL) != 0U);
+ LedIndicator_ProcessAt(&indicator, 0x00000054UL);
+ CHECK(fake.outputs[1] == 0U); /* 100 ms после старта, переход во второй шаг. */
+ LedIndicator_ProcessAt(&indicator, 0x000000B8UL);
+ CHECK(fake.outputs[1] == 1U); /* 200 ms после старта, второй импульс. */
+
+ CHECK(LedIndicator_SetModeAt(&indicator, 2U, LED_INDICATOR_MODE_STARTUP, 10U) != 0U);
+ LedIndicator_ProcessAt(&indicator, 610U);
+ CHECK(fake.outputs[2] == 1U); /* Однократный startup завершён постоянным ON. */
+
+ CHECK(LedIndicator_SetMode(&indicator, 3U, LED_INDICATOR_MODE_ON) == 0U);
+ CHECK(LedIndicator_GetMode(&indicator, 0U) == LED_INDICATOR_MODE_WORK);
+
+ puts("led indicator tests passed");
+ return 0;
+}
diff --git a/c/led-indicator/tests/test_stm32_port.c b/c/led-indicator/tests/test_stm32_port.c
new file mode 100644
index 0000000..134565d
--- /dev/null
+++ b/c/led-indicator/tests/test_stm32_port.c
@@ -0,0 +1,52 @@
+#include "led_indicator_stm32_hal.h"
+
+#include
+
+void HAL_GPIO_WritePin(GPIO_TypeDef *gpio, uint16_t pin, GPIO_PinState state)
+{
+ gpio->last_pin = pin;
+ gpio->last_state = state;
+ gpio->writes++;
+}
+
+#define CHECK(condition) \
+ do { \
+ if (!(condition)) { \
+ fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \
+ #condition); \
+ return 1; \
+ } \
+ } while (0)
+
+int main(void)
+{
+ GPIO_TypeDef gpio_a = {0};
+ GPIO_TypeDef gpio_b = {0};
+ TIM_HandleTypeDef timer = {100U, 65535U};
+ const LedIndicator_Stm32HalOutput outputs[] = {
+ {&gpio_a, 0x0001U, 0U},
+ {&gpio_b, 0x0080U, 1U}
+ };
+ LedIndicator_Stm32HalPort adapter;
+ LedIndicator_Port port;
+
+ CHECK(LedIndicator_Stm32HalPortInit(&adapter, &timer, 10000U,
+ outputs, 2U, &port) != 0U);
+ port.write(port.context, 0U, 1U);
+ CHECK(gpio_a.last_pin == 0x0001U && gpio_a.last_state == GPIO_PIN_SET);
+ port.write(port.context, 1U, 1U);
+ CHECK(gpio_b.last_pin == 0x0080U && gpio_b.last_state == GPIO_PIN_RESET);
+
+ timer.counter = 109U;
+ CHECK(port.now_ms(port.context) == 0U);
+ timer.counter = 110U;
+ CHECK(port.now_ms(port.context) == 1U);
+
+ timer.counter = 65530U;
+ (void)port.now_ms(port.context);
+ timer.counter = 4U;
+ CHECK(port.now_ms(port.context) == 6544U);
+
+ puts("led indicator STM32 port tests passed");
+ return 0;
+}
diff --git a/c/parallel-nand/README.md b/c/parallel-nand/README.md
index ebc4a27..d257563 100644
--- a/c/parallel-nand/README.md
+++ b/c/parallel-nand/README.md
@@ -1,13 +1,16 @@
# parallel-nand
Переносимый C99-драйвер асинхронной parallel NAND x8 и окно управления через
-общее адресное пространство GAS. Первый профиль — Micron `MT29F1G08ABADA`:
-2048+64 байта, 64 страницы в блоке, 1024 блока.
+общее адресное пространство GAS. Есть профили Micron `MT29F1G08ABADA` и
+Hynix `HY27UF084G2M` (512 MiB, 2048+64 байта, 64 страницы в блоке,
+4096 блоков, ID `AD DC 80 95`).
Подготовлены аппаратные порты:
- `TMS320F2812` — XINTF Zone 6;
- `STM32F407VET6` — аппаратный FSMC NAND Bank 2;
+- `STM32F103RCT6` — GPIO bit-bang, так как в LQFP64 FSMC недоступен;
+- `STM32F103ZET6` — аппаратный FSMC NAND Bank 3;
- `STM32G474CEU6` — GPIO bit-bang для корпуса UFQFPN-48.
Ядро не зависит от HAL и транспорта GUI. STM32 HAL используется только в
@@ -55,6 +58,12 @@ void memory_init(void)
}
```
+Для Hynix на STM32F103ZET6 используйте
+`parallel_nand_port_stm32f103ze()` и
+`parallel_nand_hy27uf084g2m_geometry`; распиновка и ограничения приведены в
+[`ports/stm32f103ze/README.md`](ports/stm32f103ze/README.md). Порт RCT6
+с GPIO bit-bang сохранён как отдельный вариант для корпуса LQFP64.
+
Диспетчер общего адресного пространства вызывает
`parallel_nand_gas_read()`/`parallel_nand_gas_write()` для адресов
`0xE000..0xE51F`. Для STM32 с `c/set-protocol/pcan_gas` дополнительно включить
@@ -67,10 +76,28 @@ void memory_init(void)
[docs/MEMORY_GAS.md](docs/MEMORY_GAS.md). Электрические соединения — в README
соответствующего порта и [docs/EMPTY_BOARD_PINOUT.md](docs/EMPTY_BOARD_PINOUT.md).
+## PROGRAM и ERASE
+
+- `parallel_nand_program_page()` записывает полную main-область и, при
+ необходимости, полную OOB-область страницы.
+- `parallel_nand_erase_block()` стирает один физический блок.
+- Перед изменением ядро проверяет заводской bad-block marker, на время операции
+ снимает `WP#`, ждёт `R/B#`, проверяет fail-бит статуса и снова включает
+ защиту при любом результате.
+- Порт обязан объявить `write_supported = 1`; иначе возвращается
+ `PNAND_ERROR_WRITE_PROTECTED`. Это защищает платы, где `WP#` постоянно
+ соединён с GND.
+- Через GAS разрушительные команды требуют записи `0xA55A` в `CONFIRM`
+ непосредственно перед `PROGRAM_PAGE` или `ERASE_BLOCK`.
+
+NAND можно программировать только из `1` в `0`; для повторной записи сначала
+стирается весь блок. Не стирайте заводские bad-блоки и не используйте raw-запись
+как файловую систему без ECC, wear leveling и защиты загрузочных блоков.
+
## Текущие границы
-- Реализовано безопасное чтение, RESET, READ ID, STATUS и bad-block marker.
-- PROGRAM/ERASE намеренно отсутствуют; ядро включает аппаратную защиту `WP#`.
+- Реализованы чтение, программирование страницы, стирание блока, RESET,
+ READ ID, STATUS и проверка bad-block marker.
- ECC пока не исправляет данные. GUI получает физические main/OOB и должен
помечать дамп как raw.
- В полном дампе bad-блоки не пропускаются: сохраняется физический порядок.
@@ -86,3 +113,5 @@ void memory_init(void)
5. Записать номер страницы в `0xE005/0xE006`, затем `3` в `0xE004`.
6. Дождаться `READY | PAGE_VALID` в `0xE002`.
7. Прочитать main из `0xE100..0xE4FF`, OOB из `0xE500..0xE51F`.
+8. PROGRAM/ERASE проверять только на заведомо расходном блоке после сохранения
+ полного raw-дампа.
diff --git a/c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md b/c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md
index 5094b1f..e0f72de 100644
--- a/c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md
+++ b/c/parallel-nand/docs/EMPTY_BOARD_PINOUT.md
@@ -30,7 +30,7 @@ TSOP-48 (суффикс `WP`). На второй фотографии NAND —
| `CLE` | 16 | фиксация команды |
| `ALE` | 17 | фиксация адреса |
| `WE#` | 18 | строб команды/адреса от МК |
-| `WP#` | 19 | для read-only 10 кОм на GND; для записи — GPIO и pull-down |
+| `WP#` | 19 | GPIO МК и pull-down 10 кОм; только для чтения допустим GND через 10 кОм |
| `I/O0` | 29 | двунаправленная шина, бит 0 |
| `I/O1` | 30 | двунаправленная шина, бит 1 |
| `I/O2` | 31 | двунаправленная шина, бит 2 |
@@ -69,10 +69,12 @@ TSOP-48 (суффикс `WP`). На второй фотографии NAND —
## Первый запуск
1. Не устанавливать NAND и проверить 3,3 В, отсутствие КЗ и уровни `CE#/WE#/RE#`.
-2. Установить NAND, оставить `WP#` в нуле и выполнить RESET `FFh`.
+2. Установить NAND, проверить pull-down и уровень `WP# = 0`, выполнить RESET `FFh`.
3. Прочитать ID `90h`: ожидаемые первые байты для профиля — `2C F1`.
4. Считать одну страницу два раза и сравнить main/OOB побайтно.
5. Только после устойчивого чтения запускать полный дамп через GAS.
+6. PROGRAM/ERASE проверять на расходном блоке после сохранения raw-дампа;
+ убедиться осциллографом, что `WP#` поднимается только на время операции.
## Источники для проверки footprint
diff --git a/c/parallel-nand/docs/MEMORY_GAS.md b/c/parallel-nand/docs/MEMORY_GAS.md
index b760002..a91be51 100644
--- a/c/parallel-nand/docs/MEMORY_GAS.md
+++ b/c/parallel-nand/docs/MEMORY_GAS.md
@@ -18,6 +18,8 @@ parallel_nand_gas
▼
общее ядро parallel_nand
├─ TMS320F2812: XINTF
+ ├─ STM32F103RC: GPIO
+ ├─ STM32F103ZE: FSMC
├─ STM32F407VE: FSMC
└─ STM32G474CE: GPIO
```
@@ -77,7 +79,7 @@ ARM.
| Адрес | Доступ | Название | Значение |
|---:|:---:|---|---|
| `E000` | R | `SIGNATURE` | `0x4E44` (`ND`) |
-| `E001` | R | `VERSION` | версия окна, сейчас 1 |
+| `E001` | R | `VERSION` | версия окна, сейчас 2 |
| `E002` | R | `STATUS` | состояние операции |
| `E003` | R | `ERROR` | код `PNAND_*`, 0 = нет ошибки |
| `E004` | W | `COMMAND` | команда окна |
@@ -92,8 +94,9 @@ ARM.
| `E00D` | R | `BAD_BLOCK` | 1 для физически плохого блока |
| `E00E` | R | `GENERATION_LO` | счётчик загруженных страниц, low |
| `E00F` | R | `GENERATION_HI` | счётчик загруженных страниц, high |
-| `E100..E4FF` | R | `PAGE_DATA` | 1024 регистра main, два байта LE |
-| `E500..E51F` | R | `PAGE_OOB` | 32 регистра OOB, два байта LE |
+| `E010` | W | `CONFIRM` | `0xA55A` непосредственно перед разрушительной командой |
+| `E100..E4FF` | R/W | `PAGE_DATA` | 1024 регистра main, два байта LE |
+| `E500..E51F` | R/W | `PAGE_OOB` | 32 регистра OOB, два байта LE |
Между управляющими регистрами и окнами данных есть зарезервированные адреса.
Чтение блока через них не выполняется: каждый запрос должен начинаться внутри
@@ -107,6 +110,8 @@ ARM.
| 1 | `0x0002` | `BUSY` |
| 2 | `0x0004` | `PAGE_VALID` |
| 3 | `0x0008` | `BAD_BLOCK` |
+| 4 | `0x0010` | `WRITE_SUPPORTED`, порт способен управлять `WP#` |
+| 5 | `0x0020` | `BUFFER_DIRTY`, буфер изменён после чтения/очистки |
| 15 | `0x8000` | `ERROR`, подробность в `E003` |
GUI читает окно данных только при `READY=1`, `PAGE_VALID=1`, `ERROR=0`.
@@ -118,6 +123,9 @@ GUI читает окно данных только при `READY=1`, `PAGE_VALI
| 1 | `RESET` | сброс NAND и ожидание Ready |
| 2 | `READ_ID` | обновляет `E00B/E00C` |
| 3 | `READ_PAGE` | загружает main/OOB выбранной страницы |
+| 4 | `CLEAR_BUFFER` | заполняет main/OOB значением `0xFF` для подготовки записи |
+| 5 | `PROGRAM_PAGE` | программирует выбранную страницу из буфера |
+| 6 | `ERASE_BLOCK` | стирает блок, содержащий выбранную страницу |
Запись номера страницы и запуск — две отдельные операции. Сначала GUI пишет
`PAGE_LO/PAGE_HI`, затем записывает `READ_PAGE` в `COMMAND`. Нельзя отправлять
@@ -172,6 +180,28 @@ GUI хранит номер последней полностью записан
## PROGRAM/ERASE
-Запись и стирание не включены намеренно. Они требуют отдельного разрешения,
-проверки ECC, защиты загрузочных блоков и двухфазного подтверждения через GAS.
-До добавления этих функций `WP#` удерживается активным аппаратным портом.
+Разрушительные команды доступны только если установлен флаг
+`WRITE_SUPPORTED`. Ядро проверяет bad-block marker и отказывается изменять
+заводской плохой блок. `WP#` снимается только внутри операции и включается
+обратно после проверки status fail-бита, тайм-аута или другой ошибки.
+
+Для программирования очищенной страницы:
+
+1. Записать `PAGE_LO/PAGE_HI`.
+2. Выполнить `CLEAR_BUFFER`.
+3. Записать необходимые main/OOB-регистры. Незаписанные байты останутся
+ `0xFF`; OOB лучше не менять без определённой ECC-разметки.
+4. Записать `0xA55A` в `CONFIRM`.
+5. Следующей записью без промежуточных операций отправить `PROGRAM_PAGE`.
+6. Проверить `READY`, отсутствие `ERROR` и изменение `GENERATION`.
+
+Для стирания записать любую страницу целевого блока в `PAGE_LO/PAGE_HI`, затем
+`0xA55A` в `CONFIRM` и сразу `ERASE_BLOCK`. Изменение страницы, буфера или
+другого управляющего регистра сбрасывает подтверждение. После успешного
+стирания `PAGE_VALID` очищается.
+
+Повторное программирование не может превратить `0` обратно в `1`: для этого
+требуется стирание всего блока. Перед первым тестом сохраните полный raw-дамп,
+исключите загрузочные блоки и используйте заведомо расходный исправный блок.
+Библиотека не реализует ECC, wear leveling, журналирование и восстановление
+после пропадания питания.
diff --git a/c/parallel-nand/include/parallel_nand.h b/c/parallel-nand/include/parallel_nand.h
index 8773ede..615ab30 100644
--- a/c/parallel-nand/include/parallel_nand.h
+++ b/c/parallel-nand/include/parallel_nand.h
@@ -16,13 +16,31 @@ extern "C" {
#define PNAND_MT29F1G08_PAGE_TOTAL \
(PNAND_MT29F1G08_PAGE_MAIN + PNAND_MT29F1G08_PAGE_OOB)
+#define PNAND_HY27UF084G2M_PAGE_MAIN 2048UL
+#define PNAND_HY27UF084G2M_PAGE_OOB 64UL
+#define PNAND_HY27UF084G2M_PAGES_PER_BLOCK 64UL
+#define PNAND_HY27UF084G2M_BLOCKS 4096UL
+#define PNAND_HY27UF084G2M_COLUMN_CYCLES 2U
+#define PNAND_HY27UF084G2M_ROW_CYCLES 3U
+#define PNAND_HY27UF084G2M_MANUFACTURER_ID 0xADU
+#define PNAND_HY27UF084G2M_DEVICE_ID 0xDCU
+#define PNAND_HY27UF084G2M_ID3 0x80U
+#define PNAND_HY27UF084G2M_ID4 0x95U
+#define PNAND_HY27UF084G2M_PAGE_TOTAL \
+ (PNAND_HY27UF084G2M_PAGE_MAIN + PNAND_HY27UF084G2M_PAGE_OOB)
+
typedef enum {
PNAND_OK = 0,
PNAND_ERROR_ARGUMENT = -1,
PNAND_ERROR_PORT = -2,
PNAND_ERROR_TIMEOUT = -3,
PNAND_ERROR_RANGE = -4,
- PNAND_ERROR_BUFFER = -5
+ PNAND_ERROR_BUFFER = -5,
+ PNAND_ERROR_WRITE_PROTECTED = -6,
+ PNAND_ERROR_PROGRAM_FAILED = -7,
+ PNAND_ERROR_ERASE_FAILED = -8,
+ PNAND_ERROR_BAD_BLOCK = -9,
+ PNAND_ERROR_CONFIRMATION = -10
} parallel_nand_result_t;
typedef struct {
@@ -41,6 +59,7 @@ typedef struct {
} parallel_nand_t;
extern const parallel_nand_geometry_t parallel_nand_mt29f1g08_geometry;
+extern const parallel_nand_geometry_t parallel_nand_hy27uf084g2m_geometry;
int parallel_nand_init(parallel_nand_t *nand,
const parallel_nand_port_t *port,
@@ -57,6 +76,12 @@ int parallel_nand_read_page(parallel_nand_t *nand, unsigned long page,
int parallel_nand_read_page_slice(parallel_nand_t *nand, unsigned long page,
unsigned long column, pnand_octet_t *out,
unsigned long count);
+int parallel_nand_program_page(parallel_nand_t *nand, unsigned long page,
+ const pnand_octet_t *main_data,
+ unsigned long main_count,
+ const pnand_octet_t *oob,
+ unsigned long oob_count);
+int parallel_nand_erase_block(parallel_nand_t *nand, unsigned long block);
int parallel_nand_is_bad_block(parallel_nand_t *nand, unsigned long block,
int *is_bad);
unsigned long parallel_nand_page_count(const parallel_nand_t *nand);
diff --git a/c/parallel-nand/include/parallel_nand_gas.h b/c/parallel-nand/include/parallel_nand_gas.h
index 50238cc..261f0e9 100644
--- a/c/parallel-nand/include/parallel_nand_gas.h
+++ b/c/parallel-nand/include/parallel_nand_gas.h
@@ -30,20 +30,28 @@ enum {
PNAND_GAS_REG_DEVICE_ID = 0x000C,
PNAND_GAS_REG_BAD_BLOCK = 0x000D,
PNAND_GAS_REG_GENERATION_LO = 0x000E,
- PNAND_GAS_REG_GENERATION_HI = 0x000F
+ PNAND_GAS_REG_GENERATION_HI = 0x000F,
+ PNAND_GAS_REG_CONFIRM = 0x0010
};
enum {
PNAND_GAS_CMD_NONE = 0,
PNAND_GAS_CMD_RESET = 1,
PNAND_GAS_CMD_READ_ID = 2,
- PNAND_GAS_CMD_READ_PAGE = 3
+ PNAND_GAS_CMD_READ_PAGE = 3,
+ PNAND_GAS_CMD_CLEAR_BUFFER = 4,
+ PNAND_GAS_CMD_PROGRAM_PAGE = 5,
+ PNAND_GAS_CMD_ERASE_BLOCK = 6
};
+#define PNAND_GAS_CONFIRM_MAGIC 0xA55AU
+
#define PNAND_GAS_STATUS_READY 0x0001U
#define PNAND_GAS_STATUS_BUSY 0x0002U
#define PNAND_GAS_STATUS_PAGE_VALID 0x0004U
#define PNAND_GAS_STATUS_BAD_BLOCK 0x0008U
+#define PNAND_GAS_STATUS_WRITE_SUPPORTED 0x0010U
+#define PNAND_GAS_STATUS_BUFFER_DIRTY 0x0020U
#define PNAND_GAS_STATUS_ERROR 0x8000U
typedef struct {
@@ -56,6 +64,7 @@ typedef struct {
unsigned long cached_block;
int cached_bad_block;
int block_cache_valid;
+ int destructive_armed;
pnand_octet_t id[8];
pnand_octet_t main_data[PNAND_MT29F1G08_PAGE_MAIN];
pnand_octet_t oob[PNAND_MT29F1G08_PAGE_OOB];
diff --git a/c/parallel-nand/include/parallel_nand_port.h b/c/parallel-nand/include/parallel_nand_port.h
index a9e73c1..8bf93e3 100644
--- a/c/parallel-nand/include/parallel_nand_port.h
+++ b/c/parallel-nand/include/parallel_nand_port.h
@@ -19,6 +19,8 @@ typedef struct {
int (*is_ready)(void);
void (*set_write_protect)(int enabled);
void (*delay_us)(unsigned long us);
+ /* Nonzero only when set_write_protect(0) physically releases WP#. */
+ int write_supported;
} parallel_nand_port_t;
#ifdef __cplusplus
diff --git a/c/parallel-nand/ports/stm32f103rc/README.md b/c/parallel-nand/ports/stm32f103rc/README.md
new file mode 100644
index 0000000..21ce9b6
--- /dev/null
+++ b/c/parallel-nand/ports/stm32f103rc/README.md
@@ -0,0 +1,51 @@
+# Порт STM32F103RCT6 для HY27UF084G2M
+
+У `STM32F103RCT6` корпус LQFP64. Согласно datasheet STM32F103xC/D/E, функция
+FSMC в этом корпусе недоступна, поэтому порт выполняет асинхронные NAND-циклы
+через GPIO.
+
+## Соединения
+
+| HY27UF084G2M | STM32F103RCT6 | LQFP64 |
+|---|---|---:|
+| `I/O0..I/O7` | `PB0..PB7` | 26, 27, 28, 55, 56, 57, 58, 59 |
+| `CLE` | `PC0` | 8 |
+| `ALE` | `PC1` | 9 |
+| `WE#` | `PC2` | 10 |
+| `RE#` | `PC3` | 11 |
+| `CE#` | `PC4` | 24 |
+| `WP#` | `PC5` | 25 |
+| `R/B#` | `PC6` | 37 |
+
+Питание NAND — 3,3 В. На каждый вывод питания нужен 100 нФ, рядом с чипом —
+1–4,7 мкФ. `R/B#` имеет открытый сток и требует внешней подтяжки 4,7–10 кОм
+к 3,3 В. Не оставляйте `WP#` неподключённым.
+
+`PB2` также является `BOOT1`: NAND держит I/O в высокоимпедансном состоянии
+до командного цикла, но штатную конфигурацию загрузки платы всё равно нужно
+обеспечить подтяжкой. `PB3/PB4` заняты JTAG; порт отключает JTAG и сохраняет
+SWD на `PA13/PA14`.
+
+## Подключение к проекту Cube/HAL
+
+Добавьте `parallel_nand_port_stm32f103rc.c`, путь к его заголовку и включите
+GPIO HAL. Инициализация:
+
+```c
+#include "parallel_nand.h"
+#include "parallel_nand_port_stm32f103rc.h"
+
+static parallel_nand_t nand;
+
+int nand_init(void)
+{
+ return parallel_nand_init(
+ &nand,
+ parallel_nand_port_stm32f103rc(),
+ ¶llel_nand_hy27uf084g2m_geometry);
+}
+```
+
+После успешной инициализации прочитайте четыре байта ID и ожидайте
+`AD DC 80 95`. Порт поддерживает PROGRAM/ERASE: ядро держит `WP#` в активном
+низком уровне между операциями и поднимает его только на время изменения NAND.
diff --git a/c/parallel-nand/ports/stm32f103rc/parallel_nand_port_stm32f103rc.c b/c/parallel-nand/ports/stm32f103rc/parallel_nand_port_stm32f103rc.c
new file mode 100644
index 0000000..cfa5f39
--- /dev/null
+++ b/c/parallel-nand/ports/stm32f103rc/parallel_nand_port_stm32f103rc.c
@@ -0,0 +1,143 @@
+#include "parallel_nand_port_stm32f103rc.h"
+#include "stm32f1xx_hal.h"
+
+/* STM32F103RCT6 LQFP64: FSMC is unavailable, so use GPIO bit-bang. */
+#define DATA_MASK 0x00FFU
+#define CLE_PIN GPIO_PIN_0
+#define ALE_PIN GPIO_PIN_1
+#define WE_PIN GPIO_PIN_2
+#define RE_PIN GPIO_PIN_3
+#define CE_PIN GPIO_PIN_4
+#define WP_PIN GPIO_PIN_5
+#define RB_PIN GPIO_PIN_6
+
+static int data_is_output;
+
+static void data_input(void)
+{
+ if (data_is_output) {
+ GPIOB->CRL = 0x44444444UL; /* PB0..PB7: floating input. */
+ data_is_output = 0;
+ }
+}
+
+static void data_output(void)
+{
+ if (!data_is_output) {
+ GPIOB->CRL = 0x33333333UL; /* PB0..PB7: 50 MHz push-pull. */
+ data_is_output = 1;
+ }
+}
+
+static void short_delay(void)
+{
+ __NOP(); __NOP(); __NOP(); __NOP();
+}
+
+static int f103rc_init(void)
+{
+ GPIO_InitTypeDef io;
+
+ __HAL_RCC_AFIO_CLK_ENABLE();
+ __HAL_RCC_GPIOB_CLK_ENABLE();
+ __HAL_RCC_GPIOC_CLK_ENABLE();
+
+ /* Latch inactive control levels while pins are still inputs. */
+ GPIOC->BSRR = WE_PIN | RE_PIN | CE_PIN |
+ ((uint32_t)(CLE_PIN | ALE_PIN | WP_PIN) << 16);
+
+ /* PB3/PB4 carry I/O3/I/O4. Keep SWD, release only the JTAG pins. */
+ __HAL_AFIO_REMAP_SWJ_NOJTAG();
+
+ GPIOB->BSRR = DATA_MASK;
+ GPIOB->CRL = 0x33333333UL;
+ data_is_output = 1;
+ data_input();
+
+ io.Pin = CLE_PIN | ALE_PIN | WE_PIN | RE_PIN | CE_PIN | WP_PIN;
+ io.Mode = GPIO_MODE_OUTPUT_PP;
+ io.Pull = GPIO_NOPULL;
+ io.Speed = GPIO_SPEED_FREQ_HIGH;
+ HAL_GPIO_Init(GPIOC, &io);
+
+ io.Pin = RB_PIN;
+ io.Mode = GPIO_MODE_INPUT;
+ io.Pull = GPIO_NOPULL;
+ HAL_GPIO_Init(GPIOC, &io);
+ return 0;
+}
+
+static void f103rc_select(void) { GPIOC->BRR = CE_PIN; }
+static void f103rc_deselect(void) { GPIOC->BSRR = CE_PIN; }
+
+static void write_cycle(pnand_octet_t value)
+{
+ data_output();
+ GPIOB->BRR = DATA_MASK;
+ GPIOB->BSRR = (uint32_t)value & DATA_MASK;
+ short_delay();
+ GPIOC->BRR = WE_PIN;
+ short_delay();
+ GPIOC->BSRR = WE_PIN;
+ short_delay();
+}
+
+static void f103rc_write_command(pnand_octet_t value)
+{
+ GPIOC->BSRR = CLE_PIN;
+ GPIOC->BRR = ALE_PIN;
+ write_cycle(value);
+ GPIOC->BRR = CLE_PIN;
+}
+
+static void f103rc_write_address(pnand_octet_t value)
+{
+ GPIOC->BSRR = ALE_PIN;
+ GPIOC->BRR = CLE_PIN;
+ write_cycle(value);
+ GPIOC->BRR = ALE_PIN;
+}
+
+static void f103rc_write_data(pnand_octet_t value)
+{
+ GPIOC->BRR = CLE_PIN | ALE_PIN;
+ write_cycle(value);
+}
+
+static pnand_octet_t f103rc_read_data(void)
+{
+ pnand_octet_t value;
+ data_input();
+ GPIOC->BRR = CLE_PIN | ALE_PIN | RE_PIN;
+ short_delay();
+ value = (pnand_octet_t)(GPIOB->IDR & DATA_MASK);
+ GPIOC->BSRR = RE_PIN;
+ short_delay();
+ return value;
+}
+
+static int f103rc_is_ready(void) { return (GPIOC->IDR & RB_PIN) != 0U; }
+
+static void f103rc_set_wp(int enabled)
+{
+ if (enabled) GPIOC->BRR = WP_PIN;
+ else GPIOC->BSRR = WP_PIN;
+}
+
+static void f103rc_delay_us(unsigned long us)
+{
+ uint32_t cycles = (SystemCoreClock / 3000000UL) * (uint32_t)us;
+ while (cycles-- != 0U) __NOP();
+}
+
+static const parallel_nand_port_t f103rc_port = {
+ f103rc_init, f103rc_select, f103rc_deselect,
+ f103rc_write_command, f103rc_write_address, f103rc_write_data,
+ f103rc_read_data, f103rc_is_ready, f103rc_set_wp, f103rc_delay_us,
+ 1
+};
+
+const parallel_nand_port_t *parallel_nand_port_stm32f103rc(void)
+{
+ return &f103rc_port;
+}
diff --git a/c/parallel-nand/ports/stm32f103rc/parallel_nand_port_stm32f103rc.h b/c/parallel-nand/ports/stm32f103rc/parallel_nand_port_stm32f103rc.h
new file mode 100644
index 0000000..7d76f27
--- /dev/null
+++ b/c/parallel-nand/ports/stm32f103rc/parallel_nand_port_stm32f103rc.h
@@ -0,0 +1,16 @@
+#ifndef PARALLEL_NAND_PORT_STM32F103RC_H
+#define PARALLEL_NAND_PORT_STM32F103RC_H
+
+#include "parallel_nand_port.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+const parallel_nand_port_t *parallel_nand_port_stm32f103rc(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/c/parallel-nand/ports/stm32f103ze/README.md b/c/parallel-nand/ports/stm32f103ze/README.md
new file mode 100644
index 0000000..115e7cd
--- /dev/null
+++ b/c/parallel-nand/ports/stm32f103ze/README.md
@@ -0,0 +1,62 @@
+# Порт STM32F103ZET6 для HY27UF084G2M
+
+Порт использует аппаратный контроллер `FSMC NAND Bank 3`, 8-битную шину и
+окно памяти `0x70000000`. Сигналы `CE#`, `WE#`, `RE#`, `CLE` и `ALE`
+формируются FSMC. Защита записи `WP#` управляется отдельным GPIO `PG8`.
+
+## Соединения
+
+| HY27UF084G2M | STM32F103ZET6 | LQFP144 | Функция FSMC |
+|---|---|---:|---|
+| `I/O0` | `PD14` | 85 | `D0` |
+| `I/O1` | `PD15` | 86 | `D1` |
+| `I/O2` | `PD0` | 114 | `D2` |
+| `I/O3` | `PD1` | 115 | `D3` |
+| `I/O4` | `PE7` | 58 | `D4` |
+| `I/O5` | `PE8` | 59 | `D5` |
+| `I/O6` | `PE9` | 60 | `D6` |
+| `I/O7` | `PE10` | 63 | `D7` |
+| `CLE` | `PD11` | 80 | `A16/CLE` |
+| `ALE` | `PD12` | 81 | `A17/ALE` |
+| `WE#` | `PD5` | 119 | `NWE` |
+| `RE#` | `PD4` | 118 | `NOE` |
+| `CE#` | `PD7` | 123 | `NCE2` |
+| `R/B#` | `PD6` | 122 | вход GPIO (`NWAIT`) |
+| `WP#` | `PG8` | 93 | выход GPIO |
+
+`R/B#` имеет открытый сток и требует внешней подтяжки 4,7–10 кОм к 3,3 В.
+Питание NAND — 3,3 В; возле каждой пары питания нужен 100 нФ, рядом с
+микросхемой — 1–4,7 мкФ. Все номера выводов MCU указаны для вида сверху.
+
+## Подключение к STM32Cube HAL
+
+В `stm32f1xx_hal_conf.h` должны быть включены `HAL_NAND_MODULE_ENABLED` и
+`HAL_GPIO_MODULE_ENABLED`. В проект добавляются:
+
+- `parallel_nand_port_stm32f103ze.c`;
+- `stm32f1xx_hal_nand.c`;
+- `stm32f1xx_ll_fsmc.c`.
+
+Инициализация переносимой библиотеки:
+
+```c
+#include "parallel_nand.h"
+#include "parallel_nand_port_stm32f103ze.h"
+
+static parallel_nand_t nand;
+
+int nand_init(void)
+{
+ return parallel_nand_init(
+ &nand,
+ parallel_nand_port_stm32f103ze(),
+ ¶llel_nand_hy27uf084g2m_geometry);
+}
+```
+
+Начальные тайминги намеренно консервативны. После запуска проверьте
+осциллографом длительности `WE#`/`RE#` и время установки данных. Ожидаемый ID
+HY27UF084G2M: `AD DC 80 95`.
+
+Порт поддерживает PROGRAM/ERASE. `WP#` на `PG8` остаётся активным между
+операциями и снимается ядром только на время программирования или стирания.
diff --git a/c/parallel-nand/ports/stm32f103ze/parallel_nand_port_stm32f103ze.c b/c/parallel-nand/ports/stm32f103ze/parallel_nand_port_stm32f103ze.c
new file mode 100644
index 0000000..83134ef
--- /dev/null
+++ b/c/parallel-nand/ports/stm32f103ze/parallel_nand_port_stm32f103ze.c
@@ -0,0 +1,184 @@
+#include "parallel_nand_port_stm32f103ze.h"
+
+#include "stm32f1xx_hal.h"
+#include "stm32f1xx_hal_nand.h"
+
+/* STM32F103 names this block FSMC NAND Bank 3. Its HAL memory window is
+ * NAND_DEVICE1; each access makes FSMC assert PD7/FSMC_NCE2 automatically. */
+#define PNAND_F103ZE_DATA_ADDRESS 0x70000000UL
+#define PNAND_F103ZE_COMMAND_AREA (1UL << 16U)
+#define PNAND_F103ZE_ADDRESS_AREA (1UL << 17U)
+
+/* WP# is not generated by FSMC, so it is controlled as an ordinary GPIO. */
+#define PNAND_F103ZE_WP_PORT GPIOG
+#define PNAND_F103ZE_WP_PIN GPIO_PIN_8
+
+/* HAL owns the FSMC registers through this handle during initialization. */
+static NAND_HandleTypeDef nand_handle;
+
+/** Configure all FSMC signals that belong to GPIO port D. */
+static void configure_fsmc_gpio_d(void)
+{
+ GPIO_InitTypeDef pin = {0};
+
+ /* D0..D3, NOE, NWE, NCE2, CLE and ALE are driven by FSMC. On STM32F1
+ * alternate-function output mode is selected without an AF number. */
+ pin.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5 |
+ GPIO_PIN_7 | GPIO_PIN_11 | GPIO_PIN_12 |
+ GPIO_PIN_14 | GPIO_PIN_15;
+ pin.Mode = GPIO_MODE_AF_PP;
+ pin.Pull = GPIO_NOPULL;
+ pin.Speed = GPIO_SPEED_FREQ_HIGH;
+ HAL_GPIO_Init(GPIOD, &pin);
+
+ /* R/B# is read directly. Its open-drain output needs an external
+ * 4.7..10 kOhm pull-up to 3.3 V. */
+ pin.Pin = GPIO_PIN_6;
+ pin.Mode = GPIO_MODE_INPUT;
+ pin.Pull = GPIO_NOPULL;
+ HAL_GPIO_Init(GPIOD, &pin);
+}
+
+/** Configure FSMC data bits D4..D7 on GPIO port E. */
+static void configure_fsmc_gpio_e(void)
+{
+ GPIO_InitTypeDef pin = {0};
+
+ pin.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10;
+ pin.Mode = GPIO_MODE_AF_PP;
+ pin.Pull = GPIO_NOPULL;
+ pin.Speed = GPIO_SPEED_FREQ_HIGH;
+ HAL_GPIO_Init(GPIOE, &pin);
+}
+
+/** Configure the explicit write-protect output and assert protection. */
+static void configure_write_protect(void)
+{
+ GPIO_InitTypeDef pin = {0};
+
+ /* Set the safe active-low level before changing the pad to an output. */
+ HAL_GPIO_WritePin(PNAND_F103ZE_WP_PORT, PNAND_F103ZE_WP_PIN,
+ GPIO_PIN_RESET);
+
+ pin.Pin = PNAND_F103ZE_WP_PIN;
+ pin.Mode = GPIO_MODE_OUTPUT_PP;
+ pin.Pull = GPIO_NOPULL;
+ pin.Speed = GPIO_SPEED_FREQ_LOW;
+ HAL_GPIO_Init(PNAND_F103ZE_WP_PORT, &pin);
+}
+
+/** Initialize the STM32F1 HAL NAND layer with conservative 72 MHz timings. */
+static int f103ze_init(void)
+{
+ FSMC_NAND_PCC_TimingTypeDef common_timing = {0};
+ FSMC_NAND_PCC_TimingTypeDef attribute_timing = {0};
+
+ __HAL_RCC_GPIOD_CLK_ENABLE();
+ __HAL_RCC_GPIOE_CLK_ENABLE();
+ __HAL_RCC_GPIOG_CLK_ENABLE();
+ __HAL_RCC_FSMC_CLK_ENABLE();
+
+ configure_fsmc_gpio_d();
+ configure_fsmc_gpio_e();
+ configure_write_protect();
+
+ nand_handle.Instance = FSMC_NAND_DEVICE;
+ nand_handle.Init.NandBank = FSMC_NAND_BANK3;
+ nand_handle.Init.Waitfeature = FSMC_NAND_PCC_WAIT_FEATURE_DISABLE;
+ nand_handle.Init.MemoryDataWidth = FSMC_NAND_PCC_MEM_BUS_WIDTH_8;
+ nand_handle.Init.EccComputation = FSMC_NAND_ECC_DISABLE;
+ nand_handle.Init.ECCPageSize = FSMC_NAND_ECC_PAGE_SIZE_2048BYTE;
+ nand_handle.Init.TCLRSetupTime = 2U;
+ nand_handle.Init.TARSetupTime = 2U;
+
+ /* One FSMC tick is about 13.9 ns at HCLK=72 MHz. These deliberately
+ * relaxed values are suitable for first bring-up and signal probing. */
+ common_timing.SetupTime = 2U;
+ common_timing.WaitSetupTime = 5U;
+ common_timing.HoldSetupTime = 3U;
+ common_timing.HiZSetupTime = 2U;
+ attribute_timing = common_timing;
+
+ return HAL_NAND_Init(&nand_handle, &common_timing, &attribute_timing) ==
+ HAL_OK
+ ? 0
+ : -1;
+}
+
+static void f103ze_select(void)
+{
+ /* A memory-window access asserts NCE2; no separate software GPIO exists. */
+}
+
+static void f103ze_deselect(void)
+{
+ /* NCE2 is released automatically when the FSMC access finishes. */
+}
+
+static void f103ze_write_command(pnand_octet_t value)
+{
+ *(__IO uint8_t *)(PNAND_F103ZE_DATA_ADDRESS |
+ PNAND_F103ZE_COMMAND_AREA) = (uint8_t)value;
+ __DSB(); /* Complete the external bus write before the next NAND cycle. */
+}
+
+static void f103ze_write_address(pnand_octet_t value)
+{
+ *(__IO uint8_t *)(PNAND_F103ZE_DATA_ADDRESS |
+ PNAND_F103ZE_ADDRESS_AREA) = (uint8_t)value;
+ __DSB();
+}
+
+static void f103ze_write_data(pnand_octet_t value)
+{
+ *(__IO uint8_t *)PNAND_F103ZE_DATA_ADDRESS = (uint8_t)value;
+ __DSB();
+}
+
+static pnand_octet_t f103ze_read_data(void)
+{
+ uint8_t value = *(__IO uint8_t *)PNAND_F103ZE_DATA_ADDRESS;
+ __DSB();
+ return (pnand_octet_t)value;
+}
+
+static int f103ze_is_ready(void)
+{
+ return HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_6) == GPIO_PIN_SET;
+}
+
+static void f103ze_set_write_protect(int enabled)
+{
+ /* NAND WP# is active low: RESET protects, SET permits program/erase. */
+ HAL_GPIO_WritePin(PNAND_F103ZE_WP_PORT, PNAND_F103ZE_WP_PIN,
+ enabled ? GPIO_PIN_RESET : GPIO_PIN_SET);
+}
+
+static void f103ze_delay_us(unsigned long us)
+{
+ /* Approximate delay used only around reset/status polling. The factor
+ * accounts for loop instructions in addition to the NOP. */
+ uint32_t cycles = (SystemCoreClock / 3000000UL) * (uint32_t)us;
+ while (cycles-- != 0U) {
+ __NOP();
+ }
+}
+
+static const parallel_nand_port_t f103ze_port = {
+ f103ze_init,
+ f103ze_select,
+ f103ze_deselect,
+ f103ze_write_command,
+ f103ze_write_address,
+ f103ze_write_data,
+ f103ze_read_data,
+ f103ze_is_ready,
+ f103ze_set_write_protect,
+ f103ze_delay_us,
+ 1
+};
+
+const parallel_nand_port_t *parallel_nand_port_stm32f103ze(void)
+{
+ return &f103ze_port;
+}
diff --git a/c/parallel-nand/ports/stm32f103ze/parallel_nand_port_stm32f103ze.h b/c/parallel-nand/ports/stm32f103ze/parallel_nand_port_stm32f103ze.h
new file mode 100644
index 0000000..d5483a4
--- /dev/null
+++ b/c/parallel-nand/ports/stm32f103ze/parallel_nand_port_stm32f103ze.h
@@ -0,0 +1,16 @@
+#ifndef PARALLEL_NAND_PORT_STM32F103ZE_H
+#define PARALLEL_NAND_PORT_STM32F103ZE_H
+
+#include "parallel_nand_port.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+const parallel_nand_port_t *parallel_nand_port_stm32f103ze(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/c/parallel-nand/ports/stm32f407ve/README.md b/c/parallel-nand/ports/stm32f407ve/README.md
index 0decb57..0ec2c78 100644
--- a/c/parallel-nand/ports/stm32f407ve/README.md
+++ b/c/parallel-nand/ports/stm32f407ve/README.md
@@ -24,3 +24,8 @@
`stm32f4xx_ll_fsmc.c`, включить FSMC и не создавать второй
`NAND_HandleTypeDef` для Bank 2. Начальные тайминги
консервативны; финальные значения подтвердить осциллографом.
+
+В базовой схеме `WP#` аппаратно зафиксирован в активном уровне, поэтому порт
+объявляет `write_supported = 0`, а PROGRAM/ERASE возвращают
+`PNAND_ERROR_WRITE_PROTECTED`. Для записи назначьте отдельный GPIO с внешним
+pull-down, реализуйте управление `WP#` и включите признак поддержки порта.
diff --git a/c/parallel-nand/ports/stm32f407ve/parallel_nand_port_stm32f407ve.c b/c/parallel-nand/ports/stm32f407ve/parallel_nand_port_stm32f407ve.c
index 50290c2..d2fab45 100644
--- a/c/parallel-nand/ports/stm32f407ve/parallel_nand_port_stm32f407ve.c
+++ b/c/parallel-nand/ports/stm32f407ve/parallel_nand_port_stm32f407ve.c
@@ -84,7 +84,8 @@ static void f407_delay_us(unsigned long us)
static const parallel_nand_port_t f407_port = {
f407_init, f407_select, f407_deselect,
f407_write_command, f407_write_address, f407_write_data,
- f407_read_data, f407_is_ready, f407_set_wp, f407_delay_us
+ f407_read_data, f407_is_ready, f407_set_wp, f407_delay_us,
+ 0
};
const parallel_nand_port_t *parallel_nand_port_stm32f407ve(void)
diff --git a/c/parallel-nand/ports/stm32g474ce/README.md b/c/parallel-nand/ports/stm32g474ce/README.md
index 24e07e6..8a938ad 100644
--- a/c/parallel-nand/ports/stm32g474ce/README.md
+++ b/c/parallel-nand/ports/stm32g474ce/README.md
@@ -19,3 +19,6 @@ HAL только для инициализации, а циклы NAND выпо
Выбранные `PB3/PB4` нельзя одновременно использовать для trace/JTAG; SWD на
`PA13/PA14` остаётся доступен. Если эти пины нужны проекту, измените таблицу и
операции шины: текущая оптимизация рассчитана на непрерывные `PB0..PB7`.
+
+Порт поддерживает PROGRAM/ERASE через управляемый `WP#` на `PA5`. Внешний
+pull-down сохраняет защиту до инициализации GPIO и при сбросе МК.
diff --git a/c/parallel-nand/ports/stm32g474ce/parallel_nand_port_stm32g474ce.c b/c/parallel-nand/ports/stm32g474ce/parallel_nand_port_stm32g474ce.c
index 3f18b64..b9819b6 100644
--- a/c/parallel-nand/ports/stm32g474ce/parallel_nand_port_stm32g474ce.c
+++ b/c/parallel-nand/ports/stm32g474ce/parallel_nand_port_stm32g474ce.c
@@ -125,7 +125,8 @@ static void g474_delay_us(unsigned long us)
static const parallel_nand_port_t g474_port = {
g474_init, g474_select, g474_deselect,
g474_write_command, g474_write_address, g474_write_data,
- g474_read_data, g474_is_ready, g474_set_wp, g474_delay_us
+ g474_read_data, g474_is_ready, g474_set_wp, g474_delay_us,
+ 1
};
const parallel_nand_port_t *parallel_nand_port_stm32g474ce(void)
diff --git a/c/parallel-nand/ports/tms320f2812/README.md b/c/parallel-nand/ports/tms320f2812/README.md
index 7a33632..db1dfd1 100644
--- a/c/parallel-nand/ports/tms320f2812/README.md
+++ b/c/parallel-nand/ports/tms320f2812/README.md
@@ -13,9 +13,10 @@
Адреса C28x задаются в 16-битных словах: data `0x100000`, command
`0x100001`, address `0x100002`. Используется только младший байт XINTF.
-`WP#` в базовом варианте аппаратный: для режима только чтения соединить с GND
-через 10 кОм; если позже понадобится PROGRAM/ERASE, заменить на GPIO и реализовать
-`tms_set_wp()`.
+`WP#` в базовом варианте аппаратный и соединён с GND через 10 кОм. Поэтому
+порт объявляет `write_supported = 0`, а PROGRAM/ERASE возвращают
+`PNAND_ERROR_WRITE_PROTECTED`. Для записи нужно перенести `WP#` на GPIO,
+реализовать `tms_set_wp()` и установить признак поддержки в структуре порта.
Задержка в порте приблизительная. После выбора частоты CPU проверить импульсы
осциллографом и при необходимости изменить `PNAND_TMS_CPU_MHZ`.
diff --git a/c/parallel-nand/ports/tms320f2812/parallel_nand_port_tms320f2812.c b/c/parallel-nand/ports/tms320f2812/parallel_nand_port_tms320f2812.c
index b1dda6d..d82f325 100644
--- a/c/parallel-nand/ports/tms320f2812/parallel_nand_port_tms320f2812.c
+++ b/c/parallel-nand/ports/tms320f2812/parallel_nand_port_tms320f2812.c
@@ -56,7 +56,8 @@ static void tms_set_wp(int enabled)
static const parallel_nand_port_t tms_port = {
tms_init, tms_select, tms_deselect,
tms_write_command, tms_write_address, tms_write_data,
- tms_read_data, tms_is_ready, tms_set_wp, tms_delay_us
+ tms_read_data, tms_is_ready, tms_set_wp, tms_delay_us,
+ 0
};
const parallel_nand_port_t *parallel_nand_port_tms320f2812(void)
diff --git a/c/parallel-nand/src/parallel_nand.c b/c/parallel-nand/src/parallel_nand.c
index 62158c4..90d28b1 100644
--- a/c/parallel-nand/src/parallel_nand.c
+++ b/c/parallel-nand/src/parallel_nand.c
@@ -4,8 +4,15 @@
#define PNAND_CMD_READ_2 0x30U
#define PNAND_CMD_READ_ID 0x90U
#define PNAND_CMD_READ_STATUS 0x70U
+#define PNAND_CMD_PROGRAM_1 0x80U
+#define PNAND_CMD_PROGRAM_2 0x10U
+#define PNAND_CMD_ERASE_1 0x60U
+#define PNAND_CMD_ERASE_2 0xD0U
#define PNAND_CMD_RESET 0xFFU
+#define PNAND_STATUS_FAIL 0x01U
+#define PNAND_STATUS_WP 0x80U
+
const parallel_nand_geometry_t parallel_nand_mt29f1g08_geometry = {
PNAND_MT29F1G08_PAGE_MAIN,
PNAND_MT29F1G08_PAGE_OOB,
@@ -16,6 +23,16 @@ const parallel_nand_geometry_t parallel_nand_mt29f1g08_geometry = {
1000000UL
};
+const parallel_nand_geometry_t parallel_nand_hy27uf084g2m_geometry = {
+ PNAND_HY27UF084G2M_PAGE_MAIN,
+ PNAND_HY27UF084G2M_PAGE_OOB,
+ PNAND_HY27UF084G2M_PAGES_PER_BLOCK,
+ PNAND_HY27UF084G2M_BLOCKS,
+ PNAND_HY27UF084G2M_COLUMN_CYCLES,
+ PNAND_HY27UF084G2M_ROW_CYCLES,
+ 1000000UL
+};
+
static int port_valid(const parallel_nand_port_t *port)
{
return port != 0 && port->init != 0 && port->select != 0 &&
@@ -57,6 +74,8 @@ int parallel_nand_reset(parallel_nand_t *nand)
nand->port->select();
nand->port->write_command(PNAND_CMD_RESET);
nand->port->deselect();
+ /* R/B# may not fall until tWB after the command cycle. */
+ nand->port->delay_us(1UL);
return wait_ready(nand);
}
@@ -70,7 +89,7 @@ int parallel_nand_init(parallel_nand_t *nand,
nand->geometry = *geometry;
if (port->init() != 0) return PNAND_ERROR_PORT;
port->deselect();
- /* Ядро пока только читает: аппаратно запрещаем PROGRAM/ERASE. */
+ /* WP# stays asserted except for the shortest possible mutation window. */
port->set_write_protect(1);
return parallel_nand_reset(nand);
}
@@ -122,6 +141,7 @@ static int begin_page_read(parallel_nand_t *nand, unsigned long page,
}
nand->port->write_command(PNAND_CMD_READ_2);
nand->port->deselect();
+ nand->port->delay_us(1UL); /* Cover tWB before polling R/B#. */
return wait_ready(nand);
}
@@ -167,6 +187,101 @@ int parallel_nand_read_page(parallel_nand_t *nand, unsigned long page,
return PNAND_OK;
}
+static void write_address_cycles(parallel_nand_t *nand,
+ unsigned long value,
+ unsigned int count)
+{
+ unsigned int i;
+ for (i = 0U; i < count; i++) {
+ nand->port->write_address((pnand_octet_t)(value & 0xFFUL));
+ value >>= 8;
+ }
+}
+
+static int mutation_supported(const parallel_nand_t *nand)
+{
+ return nand != 0 && port_valid(nand->port) &&
+ nand->port->write_data != 0 &&
+ nand->port->write_supported != 0;
+}
+
+static int finish_mutation(parallel_nand_t *nand, int failure_result)
+{
+ pnand_octet_t status = 0U;
+ int result = wait_ready(nand);
+ if (result == PNAND_OK)
+ result = parallel_nand_read_status(nand, &status);
+ nand->port->set_write_protect(1);
+ if (result != PNAND_OK) return result;
+ if ((status & PNAND_STATUS_WP) == 0U)
+ return PNAND_ERROR_WRITE_PROTECTED;
+ return (status & PNAND_STATUS_FAIL) != 0U ? failure_result : PNAND_OK;
+}
+
+int parallel_nand_program_page(parallel_nand_t *nand, unsigned long page,
+ const pnand_octet_t *main_data,
+ unsigned long main_count,
+ const pnand_octet_t *oob,
+ unsigned long oob_count)
+{
+ unsigned long i;
+ unsigned long block;
+ int bad = 0;
+ int result;
+ if (nand == 0 || main_data == 0) return PNAND_ERROR_ARGUMENT;
+ if (!mutation_supported(nand)) return PNAND_ERROR_WRITE_PROTECTED;
+ if (page >= parallel_nand_page_count(nand)) return PNAND_ERROR_RANGE;
+ if (main_count != nand->geometry.page_main_size)
+ return PNAND_ERROR_BUFFER;
+ if ((oob == 0 && oob_count != 0UL) ||
+ (oob != 0 && oob_count != nand->geometry.page_oob_size))
+ return PNAND_ERROR_BUFFER;
+
+ block = page / nand->geometry.pages_per_block;
+ result = parallel_nand_is_bad_block(nand, block, &bad);
+ if (result != PNAND_OK) return result;
+ if (bad) return PNAND_ERROR_BAD_BLOCK;
+
+ nand->port->set_write_protect(0);
+ nand->port->delay_us(1UL);
+ nand->port->select();
+ nand->port->write_command(PNAND_CMD_PROGRAM_1);
+ write_address_cycles(nand, 0UL, nand->geometry.column_cycles);
+ write_address_cycles(nand, page, nand->geometry.row_cycles);
+ for (i = 0UL; i < main_count; i++)
+ nand->port->write_data(main_data[i] & 0xFFU);
+ for (i = 0UL; i < oob_count; i++)
+ nand->port->write_data(oob[i] & 0xFFU);
+ nand->port->write_command(PNAND_CMD_PROGRAM_2);
+ nand->port->deselect();
+ nand->port->delay_us(1UL); /* Cover tWB before polling R/B#. */
+ return finish_mutation(nand, PNAND_ERROR_PROGRAM_FAILED);
+}
+
+int parallel_nand_erase_block(parallel_nand_t *nand, unsigned long block)
+{
+ unsigned long first_page;
+ int bad = 0;
+ int result;
+ if (nand == 0) return PNAND_ERROR_ARGUMENT;
+ if (!mutation_supported(nand)) return PNAND_ERROR_WRITE_PROTECTED;
+ if (block >= nand->geometry.block_count) return PNAND_ERROR_RANGE;
+ result = parallel_nand_is_bad_block(nand, block, &bad);
+ if (result != PNAND_OK) return result;
+ if (bad) return PNAND_ERROR_BAD_BLOCK;
+
+ first_page = block * nand->geometry.pages_per_block;
+ nand->port->set_write_protect(0);
+ nand->port->delay_us(1UL);
+ nand->port->select();
+ nand->port->write_command(PNAND_CMD_ERASE_1);
+ write_address_cycles(nand, first_page, nand->geometry.row_cycles);
+ nand->port->write_command(PNAND_CMD_ERASE_2);
+ nand->port->deselect();
+ nand->port->delay_us(1UL); /* Cover tWB before polling R/B#. */
+ return finish_mutation(nand, PNAND_ERROR_ERASE_FAILED);
+}
+
int parallel_nand_is_bad_block(parallel_nand_t *nand, unsigned long block,
int *is_bad)
{
diff --git a/c/parallel-nand/src/parallel_nand_gas.c b/c/parallel-nand/src/parallel_nand_gas.c
index 461374b..996d24d 100644
--- a/c/parallel-nand/src/parallel_nand_gas.c
+++ b/c/parallel-nand/src/parallel_nand_gas.c
@@ -6,6 +6,20 @@ static unsigned int pack_pair(const pnand_octet_t *data, unsigned int index)
((data[index * 2U + 1U] & 0xFFU) << 8));
}
+static void unpack_pair(pnand_octet_t *data, unsigned int index,
+ unsigned int value)
+{
+ data[index * 2U] = (pnand_octet_t)(value & 0xFFU);
+ data[index * 2U + 1U] = (pnand_octet_t)((value >> 8) & 0xFFU);
+}
+
+static unsigned int persistent_status(const parallel_nand_gas_t *gas)
+{
+ return gas->nand->port->write_supported != 0
+ ? PNAND_GAS_STATUS_WRITE_SUPPORTED
+ : 0U;
+}
+
parallel_nand_gas_result_t parallel_nand_gas_read(
parallel_nand_gas_t *gas, unsigned int address, unsigned int *value)
{
@@ -13,11 +27,16 @@ parallel_nand_gas_result_t parallel_nand_gas_read(
const parallel_nand_geometry_t *g;
if (gas == 0 || value == 0 || address < gas->base)
return PNAND_GAS_REJECTED;
+ /* Confirmation authorizes exactly the immediately following write. */
+ gas->destructive_armed = 0;
g = &gas->nand->geometry;
offset = address - gas->base;
if (offset >= PNAND_GAS_REGION_REGS) return PNAND_GAS_NO_REG;
if (offset >= PNAND_GAS_DATA_OFFSET &&
offset < PNAND_GAS_DATA_OFFSET + PNAND_GAS_DATA_REGS) {
+ if ((unsigned long)(offset - PNAND_GAS_DATA_OFFSET) * 2UL >=
+ g->page_main_size)
+ return PNAND_GAS_NO_REG;
if ((gas->status & PNAND_GAS_STATUS_PAGE_VALID) == 0U)
return PNAND_GAS_REJECTED;
*value = pack_pair(gas->main_data,
@@ -26,6 +45,9 @@ parallel_nand_gas_result_t parallel_nand_gas_read(
}
if (offset >= PNAND_GAS_OOB_OFFSET &&
offset < PNAND_GAS_OOB_OFFSET + PNAND_GAS_OOB_REGS) {
+ if ((unsigned long)(offset - PNAND_GAS_OOB_OFFSET) * 2UL >=
+ g->page_oob_size)
+ return PNAND_GAS_NO_REG;
if ((gas->status & PNAND_GAS_STATUS_PAGE_VALID) == 0U)
return PNAND_GAS_REJECTED;
*value = pack_pair(gas->oob,
@@ -34,7 +56,7 @@ parallel_nand_gas_result_t parallel_nand_gas_read(
}
switch (offset) {
case PNAND_GAS_REG_SIGNATURE: *value = 0x4E44U; break; /* ND */
- case PNAND_GAS_REG_VERSION: *value = 0x0001U; break;
+ case PNAND_GAS_REG_VERSION: *value = 0x0002U; break;
case PNAND_GAS_REG_STATUS: *value = gas->status; break;
case PNAND_GAS_REG_ERROR: *value = (unsigned int)gas->error & 0xFFFFU; break;
case PNAND_GAS_REG_COMMAND: *value = PNAND_GAS_CMD_NONE; break;
@@ -50,6 +72,7 @@ parallel_nand_gas_result_t parallel_nand_gas_read(
*value = (gas->status & PNAND_GAS_STATUS_BAD_BLOCK) != 0U; break;
case PNAND_GAS_REG_GENERATION_LO: *value = (unsigned int)gas->generation & 0xFFFFU; break;
case PNAND_GAS_REG_GENERATION_HI: *value = (unsigned int)(gas->generation >> 16) & 0xFFFFU; break;
+ case PNAND_GAS_REG_CONFIRM: *value = 0U; break;
default: return PNAND_GAS_NO_REG;
}
*value &= 0xFFFFU;
@@ -60,7 +83,7 @@ static void finish(parallel_nand_gas_t *gas, int result)
{
gas->error = result;
gas->status &= (0xFFFFU ^ PNAND_GAS_STATUS_BUSY);
- gas->status |= PNAND_GAS_STATUS_READY;
+ gas->status |= PNAND_GAS_STATUS_READY | persistent_status(gas);
if (result != PNAND_OK) gas->status |= PNAND_GAS_STATUS_ERROR;
}
@@ -69,7 +92,11 @@ static parallel_nand_gas_result_t run_command(parallel_nand_gas_t *gas,
{
int result = PNAND_OK;
int bad = 0;
- gas->status = PNAND_GAS_STATUS_BUSY;
+ int armed = gas->destructive_armed;
+ unsigned int i;
+ unsigned int previous_status = gas->status;
+ gas->destructive_armed = 0;
+ gas->status = PNAND_GAS_STATUS_BUSY | persistent_status(gas);
if (command == PNAND_GAS_CMD_RESET) {
result = parallel_nand_reset(gas->nand);
} else if (command == PNAND_GAS_CMD_READ_ID) {
@@ -101,8 +128,59 @@ static parallel_nand_gas_result_t run_command(parallel_nand_gas_t *gas,
if (result == PNAND_OK && bad)
gas->status |= PNAND_GAS_STATUS_BAD_BLOCK;
}
+ } else if (command == PNAND_GAS_CMD_CLEAR_BUFFER) {
+ for (i = 0U; i < PNAND_MT29F1G08_PAGE_MAIN; i++)
+ gas->main_data[i] = 0xFFU;
+ for (i = 0U; i < PNAND_MT29F1G08_PAGE_OOB; i++)
+ gas->oob[i] = 0xFFU;
+ gas->status |= PNAND_GAS_STATUS_PAGE_VALID |
+ PNAND_GAS_STATUS_BUFFER_DIRTY;
+ gas->generation++;
+ } else if (command == PNAND_GAS_CMD_PROGRAM_PAGE) {
+ if (!armed ||
+ (previous_status & PNAND_GAS_STATUS_PAGE_VALID) == 0U) {
+ gas->status |= previous_status &
+ (PNAND_GAS_STATUS_PAGE_VALID |
+ PNAND_GAS_STATUS_BUFFER_DIRTY |
+ PNAND_GAS_STATUS_BAD_BLOCK);
+ finish(gas, PNAND_ERROR_CONFIRMATION);
+ return PNAND_GAS_REJECTED;
+ }
+ result = parallel_nand_program_page(
+ gas->nand, gas->selected_page,
+ gas->main_data, gas->nand->geometry.page_main_size,
+ gas->oob, gas->nand->geometry.page_oob_size);
+ gas->status |= PNAND_GAS_STATUS_PAGE_VALID;
+ if (result == PNAND_OK) {
+ gas->generation++;
+ gas->block_cache_valid = 0;
+ } else {
+ gas->status |= PNAND_GAS_STATUS_BUFFER_DIRTY;
+ }
+ } else if (command == PNAND_GAS_CMD_ERASE_BLOCK) {
+ unsigned long block;
+ if (!armed ||
+ gas->selected_page >= parallel_nand_page_count(gas->nand)) {
+ finish(gas, !armed ? PNAND_ERROR_CONFIRMATION :
+ PNAND_ERROR_RANGE);
+ return PNAND_GAS_REJECTED;
+ }
+ block = gas->selected_page / gas->nand->geometry.pages_per_block;
+ result = parallel_nand_erase_block(gas->nand, block);
+ if (result == PNAND_OK) {
+ gas->status &= (0xFFFFU ^ (PNAND_GAS_STATUS_PAGE_VALID |
+ PNAND_GAS_STATUS_BUFFER_DIRTY |
+ PNAND_GAS_STATUS_BAD_BLOCK));
+ gas->generation++;
+ gas->block_cache_valid = 0;
+ } else {
+ gas->status |= previous_status &
+ (PNAND_GAS_STATUS_PAGE_VALID |
+ PNAND_GAS_STATUS_BUFFER_DIRTY |
+ PNAND_GAS_STATUS_BAD_BLOCK);
+ }
} else {
- gas->status = PNAND_GAS_STATUS_READY;
+ gas->status = PNAND_GAS_STATUS_READY | persistent_status(gas);
return PNAND_GAS_REJECTED;
}
finish(gas, result);
@@ -119,18 +197,47 @@ parallel_nand_gas_result_t parallel_nand_gas_write(
if ((gas->status & PNAND_GAS_STATUS_BUSY) != 0U)
return PNAND_GAS_REJECTED;
value &= 0xFFFFU;
+ if (offset >= PNAND_GAS_DATA_OFFSET &&
+ offset < PNAND_GAS_DATA_OFFSET + PNAND_GAS_DATA_REGS) {
+ unsigned int index = offset - PNAND_GAS_DATA_OFFSET;
+ if ((gas->status & PNAND_GAS_STATUS_PAGE_VALID) == 0U ||
+ (unsigned long)index * 2UL >= gas->nand->geometry.page_main_size)
+ return PNAND_GAS_REJECTED;
+ gas->destructive_armed = 0;
+ unpack_pair(gas->main_data, index, value);
+ gas->status |= PNAND_GAS_STATUS_BUFFER_DIRTY;
+ return PNAND_GAS_OK;
+ }
+ if (offset >= PNAND_GAS_OOB_OFFSET &&
+ offset < PNAND_GAS_OOB_OFFSET + PNAND_GAS_OOB_REGS) {
+ unsigned int index = offset - PNAND_GAS_OOB_OFFSET;
+ if ((gas->status & PNAND_GAS_STATUS_PAGE_VALID) == 0U ||
+ (unsigned long)index * 2UL >= gas->nand->geometry.page_oob_size)
+ return PNAND_GAS_REJECTED;
+ gas->destructive_armed = 0;
+ unpack_pair(gas->oob, index, value);
+ gas->status |= PNAND_GAS_STATUS_BUFFER_DIRTY;
+ return PNAND_GAS_OK;
+ }
switch (offset) {
case PNAND_GAS_REG_COMMAND: return run_command(gas, value);
+ case PNAND_GAS_REG_CONFIRM:
+ gas->destructive_armed = value == PNAND_GAS_CONFIRM_MAGIC;
+ return gas->destructive_armed ? PNAND_GAS_OK : PNAND_GAS_REJECTED;
case PNAND_GAS_REG_PAGE_LO:
+ gas->destructive_armed = 0;
gas->selected_page = (gas->selected_page & 0xFFFF0000UL) | value;
gas->status &= (0xFFFFU ^ PNAND_GAS_STATUS_PAGE_VALID);
return PNAND_GAS_OK;
case PNAND_GAS_REG_PAGE_HI:
+ gas->destructive_armed = 0;
gas->selected_page = (gas->selected_page & 0x0000FFFFUL) |
((unsigned long)value << 16);
gas->status &= (0xFFFFU ^ PNAND_GAS_STATUS_PAGE_VALID);
return PNAND_GAS_OK;
- default: return PNAND_GAS_READ_ONLY;
+ default:
+ gas->destructive_armed = 0;
+ return PNAND_GAS_READ_ONLY;
}
}
@@ -140,12 +247,17 @@ void parallel_nand_gas_init(parallel_nand_gas_t *gas,
unsigned int i;
gas->nand = nand;
gas->base = base;
- gas->status = PNAND_GAS_STATUS_READY;
+ gas->status = PNAND_GAS_STATUS_READY | persistent_status(gas);
gas->error = PNAND_OK;
gas->selected_page = 0UL;
gas->generation = 0UL;
gas->cached_block = 0UL;
gas->cached_bad_block = 0;
gas->block_cache_valid = 0;
+ gas->destructive_armed = 0;
for (i = 0U; i < 8U; i++) gas->id[i] = 0U;
+ for (i = 0U; i < PNAND_MT29F1G08_PAGE_MAIN; i++)
+ gas->main_data[i] = 0xFFU;
+ for (i = 0U; i < PNAND_MT29F1G08_PAGE_OOB; i++)
+ gas->oob[i] = 0xFFU;
}
diff --git a/c/parallel-nand/tests/test_parallel_nand.c b/c/parallel-nand/tests/test_parallel_nand.c
index 572f129..82dc797 100644
--- a/c/parallel-nand/tests/test_parallel_nand.c
+++ b/c/parallel-nand/tests/test_parallel_nand.c
@@ -5,7 +5,7 @@
#include "parallel_nand_gas.h"
typedef struct { char kind; unsigned int value; } event_t;
-static event_t events[64];
+static event_t events[256];
static unsigned int event_count;
static pnand_octet_t input_data[32];
static unsigned int input_count;
@@ -33,17 +33,35 @@ static void fake_delay(unsigned long us) { (void)us; }
static const parallel_nand_port_t fake_port = {
fake_init, fake_select, fake_deselect, fake_cmd, fake_addr, fake_write,
- fake_read, fake_ready, fake_wp, fake_delay
+ fake_read, fake_ready, fake_wp, fake_delay, 1
};
static void clear_events(void) { event_count = 0U; }
+static unsigned int find_event(unsigned int start, char kind,
+ unsigned int value)
+{
+ unsigned int i;
+ for (i = start; i < event_count; i++) {
+ if (events[i].kind == kind && events[i].value == value) return i;
+ }
+ return event_count;
+}
+
int main(void)
{
parallel_nand_t nand;
pnand_octet_t id[2];
pnand_octet_t bytes[2];
+ pnand_octet_t main_data[2] = {0xAAU, 0x55U};
+ pnand_octet_t oob[2] = {0xFFU, 0xFEU};
parallel_nand_gas_t gas;
+ parallel_nand_geometry_t small_geometry = {
+ 2UL, 2UL, 2UL, 4UL, 2U, 2U, 100UL
+ };
+ parallel_nand_port_t read_only_port;
+ parallel_nand_t read_only_nand;
+ unsigned int pos;
unsigned int reg;
assert(parallel_nand_init(&nand, &fake_port,
@@ -76,9 +94,116 @@ int main(void)
assert(events[5].kind == 'A' && events[5].value == 0x12U);
assert(events[6].kind == 'C' && events[6].value == 0x30U);
+ assert(parallel_nand_init(&nand, &fake_port,
+ ¶llel_nand_hy27uf084g2m_geometry) == PNAND_OK);
+ assert(parallel_nand_page_count(&nand) == 262144UL);
+ assert(PNAND_HY27UF084G2M_MANUFACTURER_ID == 0xADU);
+ assert(PNAND_HY27UF084G2M_DEVICE_ID == 0xDCU);
+
+ clear_events();
+ input_data[0] = 0xABU;
+ input_count = 1U;
+ input_pos = 0U;
+ assert(parallel_nand_read_page_slice(&nand, 0x23456UL, 0x78UL,
+ bytes, 1UL) == PNAND_OK);
+ assert(bytes[0] == 0xABU);
+ assert(events[2].kind == 'A' && events[2].value == 0x78U);
+ assert(events[3].kind == 'A' && events[3].value == 0x00U);
+ assert(events[4].kind == 'A' && events[4].value == 0x56U);
+ assert(events[5].kind == 'A' && events[5].value == 0x34U);
+ assert(events[6].kind == 'A' && events[6].value == 0x02U);
+ assert(events[7].kind == 'C' && events[7].value == 0x30U);
+
+ assert(parallel_nand_init(&nand, &fake_port, &small_geometry) ==
+ PNAND_OK);
+ clear_events();
+ input_data[0] = 0xFFU;
+ input_data[1] = 0xFFU;
+ input_data[2] = 0x80U;
+ input_count = 3U;
+ input_pos = 0U;
+ assert(parallel_nand_program_page(&nand, 3UL, main_data, 2UL,
+ oob, 2UL) == PNAND_OK);
+ pos = find_event(0U, 'P', 0U);
+ assert(pos < event_count);
+ pos = find_event(pos + 1U, 'C', 0x80U);
+ assert(pos < event_count);
+ pos = find_event(pos + 1U, 'W', 0xAAU);
+ assert(pos < event_count);
+ pos = find_event(pos + 1U, 'W', 0x55U);
+ assert(pos < event_count);
+ pos = find_event(pos + 1U, 'W', 0xFFU);
+ assert(pos < event_count);
+ pos = find_event(pos + 1U, 'W', 0xFEU);
+ assert(pos < event_count);
+ pos = find_event(pos + 1U, 'C', 0x10U);
+ assert(pos < event_count);
+ pos = find_event(pos + 1U, 'C', 0x70U);
+ assert(pos < event_count);
+ assert(events[event_count - 1U].kind == 'P' &&
+ events[event_count - 1U].value == 1U);
+
+ clear_events();
+ input_data[0] = 0xFFU;
+ input_data[1] = 0xFFU;
+ input_data[2] = 0x80U;
+ input_count = 3U;
+ input_pos = 0U;
+ assert(parallel_nand_erase_block(&nand, 2UL) == PNAND_OK);
+ pos = find_event(0U, 'P', 0U);
+ pos = find_event(pos + 1U, 'C', 0x60U);
+ assert(pos < event_count);
+ pos = find_event(pos + 1U, 'A', 0x04U);
+ assert(pos < event_count);
+ pos = find_event(pos + 1U, 'C', 0xD0U);
+ assert(pos < event_count);
+ assert(events[event_count - 1U].kind == 'P' &&
+ events[event_count - 1U].value == 1U);
+
+ clear_events();
+ input_data[0] = 0xFFU;
+ input_data[1] = 0xFFU;
+ input_data[2] = 0x81U;
+ input_count = 3U;
+ input_pos = 0U;
+ assert(parallel_nand_program_page(&nand, 0UL, main_data, 2UL,
+ 0, 0UL) ==
+ PNAND_ERROR_PROGRAM_FAILED);
+ assert(events[event_count - 1U].kind == 'P' &&
+ events[event_count - 1U].value == 1U);
+
+ clear_events();
+ input_data[0] = 0xFFU;
+ input_data[1] = 0xFFU;
+ input_data[2] = 0x00U;
+ input_count = 3U;
+ input_pos = 0U;
+ assert(parallel_nand_program_page(&nand, 0UL, main_data, 2UL,
+ 0, 0UL) ==
+ PNAND_ERROR_WRITE_PROTECTED);
+ assert(events[event_count - 1U].kind == 'P' &&
+ events[event_count - 1U].value == 1U);
+
+ clear_events();
+ input_data[0] = 0x00U;
+ input_count = 1U;
+ input_pos = 0U;
+ assert(parallel_nand_erase_block(&nand, 0UL) ==
+ PNAND_ERROR_BAD_BLOCK);
+ assert(find_event(0U, 'P', 0U) == event_count);
+
+ read_only_port = fake_port;
+ read_only_port.write_supported = 0;
+ assert(parallel_nand_init(&read_only_nand, &read_only_port,
+ &small_geometry) == PNAND_OK);
+ assert(parallel_nand_erase_block(&read_only_nand, 0UL) ==
+ PNAND_ERROR_WRITE_PROTECTED);
+
parallel_nand_gas_init(&gas, &nand, PNAND_GAS_DEFAULT_BASE);
assert(parallel_nand_gas_read(&gas, 0xE000U, ®) == PNAND_GAS_OK);
assert(reg == 0x4E44U);
+ assert(parallel_nand_gas_read(&gas, 0xE001U, ®) == PNAND_GAS_OK);
+ assert(reg == 0x0002U);
assert(parallel_nand_gas_write(&gas, 0xE005U, 0x1234U) ==
PNAND_GAS_OK);
assert(parallel_nand_gas_write(&gas, 0xE006U, 0x0001U) ==
@@ -92,6 +217,56 @@ int main(void)
assert(parallel_nand_gas_write(&gas, 0xE000U, 0U) ==
PNAND_GAS_READ_ONLY);
+ assert((gas.status & PNAND_GAS_STATUS_WRITE_SUPPORTED) != 0U);
+ assert(parallel_nand_gas_write(&gas, 0xE005U, 0U) == PNAND_GAS_OK);
+ assert(parallel_nand_gas_write(&gas, 0xE006U, 0U) == PNAND_GAS_OK);
+ assert(parallel_nand_gas_write(&gas, 0xE004U,
+ PNAND_GAS_CMD_CLEAR_BUFFER) ==
+ PNAND_GAS_OK);
+ assert(parallel_nand_gas_write(&gas, 0xE100U, 0x1234U) ==
+ PNAND_GAS_OK);
+ assert(gas.main_data[0] == 0x34U && gas.main_data[1] == 0x12U);
+ assert(parallel_nand_gas_write(&gas, 0xE004U,
+ PNAND_GAS_CMD_PROGRAM_PAGE) ==
+ PNAND_GAS_REJECTED);
+ assert(gas.error == PNAND_ERROR_CONFIRMATION);
+
+ assert(parallel_nand_gas_write(&gas, 0xE010U,
+ PNAND_GAS_CONFIRM_MAGIC) ==
+ PNAND_GAS_OK);
+ assert(parallel_nand_gas_read(&gas, 0xE002U, ®) == PNAND_GAS_OK);
+ assert(parallel_nand_gas_write(&gas, 0xE004U,
+ PNAND_GAS_CMD_PROGRAM_PAGE) ==
+ PNAND_GAS_REJECTED);
+ assert(gas.error == PNAND_ERROR_CONFIRMATION);
+
+ input_data[0] = 0xFFU;
+ input_data[1] = 0xFFU;
+ input_data[2] = 0x80U;
+ input_count = 3U;
+ input_pos = 0U;
+ assert(parallel_nand_gas_write(&gas, 0xE010U,
+ PNAND_GAS_CONFIRM_MAGIC) ==
+ PNAND_GAS_OK);
+ assert(parallel_nand_gas_write(&gas, 0xE004U,
+ PNAND_GAS_CMD_PROGRAM_PAGE) ==
+ PNAND_GAS_OK);
+ assert((gas.status & PNAND_GAS_STATUS_BUFFER_DIRTY) == 0U);
+
+ assert(parallel_nand_gas_write(&gas, 0xE005U, 2U) == PNAND_GAS_OK);
+ input_data[0] = 0xFFU;
+ input_data[1] = 0xFFU;
+ input_data[2] = 0x80U;
+ input_count = 3U;
+ input_pos = 0U;
+ assert(parallel_nand_gas_write(&gas, 0xE010U,
+ PNAND_GAS_CONFIRM_MAGIC) ==
+ PNAND_GAS_OK);
+ assert(parallel_nand_gas_write(&gas, 0xE004U,
+ PNAND_GAS_CMD_ERASE_BLOCK) ==
+ PNAND_GAS_OK);
+ assert((gas.status & PNAND_GAS_STATUS_PAGE_VALID) == 0U);
+
puts("parallel_nand: OK");
return 0;
}
From 9ba3742b29994750fbf8133efc1ee711bc815ed2 Mon Sep 17 00:00:00 2001
From: Andrey Kruchinkin
Date: Tue, 15 Sep 2026 16:57:33 +0300
Subject: [PATCH 33/37] =?UTF-8?q?feat(ds18b20-ds2480):=20=D0=B4=D0=BE?=
=?UTF-8?q?=D0=B1=D0=B0=D0=B2=D1=8C=20=D0=B4=D1=80=D0=B0=D0=B9=D0=B2=D0=B5?=
=?UTF-8?q?=D1=80=20=D0=B4=D0=B0=D1=82=D1=87=D0=B8=D0=BA=D0=BE=D0=B2=20?=
=?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20UART-=D0=BC=D0=BE=D1=81=D1=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 1 +
c/ds18b20-ds2480/CMakeLists.txt | 17 ++
c/ds18b20-ds2480/README.md | 145 ++++++++++++
c/ds18b20-ds2480/ds18b20_ds2480.c | 108 +++++++++
c/ds18b20-ds2480/ds18b20_ds2480.h | 32 +++
c/ds18b20-ds2480/ds2480.c | 164 +++++++++++++
c/ds18b20-ds2480/ds2480.h | 69 ++++++
c/ds18b20-ds2480/examples/read_first.c | 19 ++
c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c | 228 +++++++++++++++++++
9 files changed, 783 insertions(+)
create mode 100644 c/ds18b20-ds2480/CMakeLists.txt
create mode 100644 c/ds18b20-ds2480/README.md
create mode 100644 c/ds18b20-ds2480/ds18b20_ds2480.c
create mode 100644 c/ds18b20-ds2480/ds18b20_ds2480.h
create mode 100644 c/ds18b20-ds2480/ds2480.c
create mode 100644 c/ds18b20-ds2480/ds2480.h
create mode 100644 c/ds18b20-ds2480/examples/read_first.c
create mode 100644 c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
diff --git a/README.md b/README.md
index b8eb5f8..82fc189 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@ templates/
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
| [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 |
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
+| [`c/ds18b20-ds2480`](c/ds18b20-ds2480) | DS18B20 через DS2480B: поиск ROM, температура, EEPROM, паразитное питание | C99 | UART 9600 8N1, сброс моста, задержка |
| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android и STM32F4-порты в комплекте** |
| [`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; настройки платы предоставляет прошивка |
diff --git a/c/ds18b20-ds2480/CMakeLists.txt b/c/ds18b20-ds2480/CMakeLists.txt
new file mode 100644
index 0000000..40698a2
--- /dev/null
+++ b/c/ds18b20-ds2480/CMakeLists.txt
@@ -0,0 +1,17 @@
+cmake_minimum_required(VERSION 3.13)
+project(ds18b20_ds2480 C)
+add_library(ds18b20_ds2480 STATIC ds2480.c ds18b20_ds2480.c)
+target_include_directories(ds18b20_ds2480 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+target_compile_features(ds18b20_ds2480 PUBLIC c_std_99)
+if(MSVC)
+ target_compile_options(ds18b20_ds2480 PRIVATE /W4)
+else()
+ target_compile_options(ds18b20_ds2480 PRIVATE -Wall -Wextra -Wpedantic)
+endif()
+option(DS18B20_DS2480_BUILD_TESTS "Build host tests" ON)
+if(DS18B20_DS2480_BUILD_TESTS)
+ enable_testing()
+ add_executable(test_ds18b20_ds2480 tests/test_ds18b20_ds2480.c)
+ target_link_libraries(test_ds18b20_ds2480 PRIVATE ds18b20_ds2480)
+ add_test(NAME ds18b20_ds2480 COMMAND test_ds18b20_ds2480)
+endif()
diff --git a/c/ds18b20-ds2480/README.md b/c/ds18b20-ds2480/README.md
new file mode 100644
index 0000000..524d2b6
--- /dev/null
+++ b/c/ds18b20-ds2480/README.md
@@ -0,0 +1,145 @@
+# DS18B20 через DS2480B
+
+Переносимая библиотека C99: поиск DS18B20 на общей шине, температура,
+разрешение 9–12 бит, TH/TL и сохранение в EEPROM через UART-мост DS2480B.
+Поддерживаются внешнее и паразитное питание: преобразование и запись EEPROM
+используют strong pullup, включаемый мостом сразу после последнего бита команды.
+
+Ядро не зависит от HAL, CMSIS, ОС или существующей GPIO-библиотеки `ds18b20`.
+Динамической памяти и глобального состояния нет. Каждый UART-мост имеет свой
+`ds2480`, каждый обход — свой `ds2480_search`.
+
+```text
+Приложение → ds18b20_ds2480 → ds2480 → callbacks UART/задержки → платформа
+```
+
+## Файлы
+
+| Файл | Назначение | Зависимости |
+|---|---|---|
+| `ds2480.h`, `ds2480.c` | Калибровка, reset, обмен битами/байтами, поиск ROM, CRC, strong pullup | `stdint.h`, callbacks |
+| `ds18b20_ds2480.h`, `ds18b20_ds2480.c` | Команды термометра, проверка scratchpad, знаковая температура | `ds2480`, `string.h` |
+| `examples/read_first.c` | Полный цикл для первого найденного DS18B20 | ядро, порт приложения |
+| `tests/test_ds18b20_ds2480.c` | Модель UART-моста и нескольких устройств 1-Wire | ядро, стандартная библиотека C |
+
+## Контракт порта
+
+```c
+int prepare(void *user);
+int write(void *user, const uint8_t *data, uint32_t size, uint32_t timeout_ms);
+int read(void *user, uint8_t *data, uint32_t size, uint32_t timeout_ms);
+void delay_ms(void *user, uint32_t ms);
+```
+
+Первые три callbacks возвращают `0` при успехе, иначе ошибку.
+`prepare` аппаратно сбрасывает DS2480B или формирует UART BREAK не короче 2 мс,
+настраивает **9600 бод, 8N1**, выдерживает минимум 2 мс после сброса и очищает RX
+и ошибки UART. Эта функция должна иметь собственный конечный таймаут.
+Нельзя просто посылать `C1` работающему мосту вместо сброса.
+
+`write` передаёт ровно `size` байтов и ждёт окончания передачи; `read` получает
+ровно `size` байтов. Обе операции ограничены `timeout_ms`; при частичном обмене
+возвращается ошибка. Приём должен работать уже во время передачи: ответ может
+появиться до вызова `read`. `delay_ms` не должна возвращаться раньше заданного
+времени (учтите округление системного тика).
+
+Управлять одним UART извне одновременно с библиотекой нельзя. В RTOS блокировка
+нужна на всю операцию верхнего уровня, включая поиск и ожидание преобразования.
+Тайминги 1-Wire формирует мост, запрещать прерывания на время слотов не требуется.
+
+## Быстрый старт
+
+Функции `prepare`, `write`, `read`, `delay_ms` и `uart_context` предоставляет плата.
+Следующий код располагается внутри функции приложения:
+
+```c
+ds2480 bus;
+ds2480_search search = {{0}, 0, 0};
+ds2480_port port = {uart_context, prepare, write, read, delay_ms};
+int16_t raw;
+ds2480_status status = ds2480_init(&bus, &port, 20);
+if (status != DS2480_OK) return;
+status = ds18b20_ds2480_next(&bus, &search);
+if (status != DS2480_OK) return;
+status = ds18b20_ds2480_convert(&bus, search.rom);
+if (status != DS2480_OK) return;
+status = ds18b20_ds2480_temperature(&bus, search.rom, &raw);
+if (status != DS2480_OK) return;
+float celsius = raw / 16.0f;
+(void)celsius;
+```
+
+Вызывайте `next` повторно с тем же курсором до `DS2480_DONE`, сохраняя каждый
+найденный ROM в памяти приложения. Лимита количества датчиков в ядре нет.
+Пустая шина возвращает `DS2480_NO_PRESENCE`; обрыв поиска и ошибки CRC не
+маскируются под успешное завершение. Для нового обхода обнулите курсор.
+
+`convert(bus, NULL)` одновременно запускает все устройства — допустимо только
+на шине исключительно с DS18B20. Затем читайте каждое по ROM. Для паразитного
+питания суммарный ток должен укладываться в возможности моста; при необходимости
+преобразуйте по одному датчику. Преобразование блокирует вызов на 750 мс плюс
+UART-обмен. EEPROM использует выдержку 12 мс, её содержимое после перезапуска
+этой функцией не проверяется. Чтение температуры само преобразование не запускает.
+Значение 85 °C после включения нельзя отличить от настоящих 85 °C без завершённого
+преобразования; не считайте его заведомой ошибкой.
+
+## Ошибки и ограничения
+
+- `IO` — таймаут/ошибка порта, `PROTOCOL` — неожиданный ответ DS2480B.
+ После них экземпляр не готов к работе: повторите `ds2480_init`.
+- `SHORT` — короткое замыкание, `NO_PRESENCE` — нет presence.
+- `CRC` — неверная контрольная сумма, `DATA` — недопустимые данные или
+ неподтверждённая запись. Выход температуры/scratchpad при ошибке не меняется.
+- При ошибке во время strong pullup ядро вызывает `prepare`, чтобы прекратить
+ импульс, и остаётся неготовым. При отказе самого порта снятие питания гарантировать
+ невозможно; восстановление UART/моста остаётся задачей приложения.
+
+Используется стандартная скорость 1-Wire и только Command Mode: байт передаётся
+восьмью командами Single Bit. Поэтому любые значения, включая `E3`, передаются
+без экранирования. Это простая реализация для опроса температуры; повышенные
+скорости UART, Overdrive и Search Accelerator пока не реализованы. Параметры
+таймингов длинной линии остаются заводскими. Аппаратная проверка обязательна
+для выбранной топологии/нагрузки. Протокол проверен по документации **DS2480B**;
+старые ревизии DS2480 без суффикса B отдельно не проверялись.
+
+## Подключение и тесты
+
+Добавьте `ds2480.c`, `ds18b20_ds2480.c` и путь к заголовкам в сборку прошивки.
+Номер UART, GPIO и библиотеку платформы выбирает приложение. Готовый порт
+для конкретной платы пока не входит в библиотеку.
+
+```cmake
+set(DS18B20_DS2480_BUILD_TESTS OFF CACHE BOOL "" FORCE)
+add_subdirectory(third_party/templates/c/ds18b20-ds2480)
+target_link_libraries(firmware PRIVATE ds18b20_ds2480)
+```
+
+Отдельная сборка хостовых тестов:
+
+```sh
+cmake -S . -B build
+cmake --build build --config Debug
+ctest --test-dir build -C Debug --output-on-failure
+```
+
+Без CMake, из каталога библиотеки:
+
+```sh
+clang -std=c99 -Wall -Wextra -Wpedantic -Werror -I . ds2480.c ds18b20_ds2480.c tests/test_ds18b20_ds2480.c -o build/test.exe
+./build/test.exe
+```
+
+Тесты моделируют калибровку без ответа, несколько ROM с развилками, другие
+семейства, адресацию, все разрешения, отрицательную температуру, CRC, пустую
+и замкнутую шину, ошибки транспорта, strong pullup и восстановление после ошибок.
+Это программная модель; испытания на физическом DS2480B пока не проводились.
+
+## Использование
+
+Добавлена в сабмодуль `templates` проекта `john103C6T6NewVer`, ветка `ds2480`.
+В рабочую прошивку ещё не включена: для этого требуется порт выбранного UART.
+
+## Источники
+
+- [DS2480B datasheet, таблицы команд и ответов](https://www.analog.com/media/en/technical-documentation/data-sheets/ds2480b.pdf)
+- [DS18B20 datasheet, команды, питание и scratchpad](https://www.analog.com/media/en/technical-documentation/data-sheets/ds18b20.pdf)
diff --git a/c/ds18b20-ds2480/ds18b20_ds2480.c b/c/ds18b20-ds2480/ds18b20_ds2480.c
new file mode 100644
index 0000000..b1c46a4
--- /dev/null
+++ b/c/ds18b20-ds2480/ds18b20_ds2480.c
@@ -0,0 +1,108 @@
+#include "ds18b20_ds2480.h"
+#include
+
+static ds2480_status write_byte(ds2480 *bus, uint8_t value)
+{
+ uint8_t echoed;
+ ds2480_status status = ds2480_byte(bus, value, &echoed);
+ if (status != DS2480_OK) return status;
+ return echoed == value ? DS2480_OK : DS2480_DATA;
+}
+
+static ds2480_status select_rom(ds2480 *bus, const uint8_t *rom)
+{
+ uint8_t i;
+ ds2480_status status;
+ if (rom && (rom[0] != 0x28 || ds2480_crc8(rom, 8))) return DS2480_ARGUMENT;
+ status = ds2480_reset(bus);
+ if (status != DS2480_OK) return status;
+ status = write_byte(bus, rom ? 0x55 : 0xCC);
+ if (status != DS2480_OK || !rom) return status;
+ for (i = 0; i < 8; ++i) {
+ status = write_byte(bus, rom[i]);
+ if (status != DS2480_OK) return status;
+ }
+ return DS2480_OK;
+}
+
+ds2480_status ds18b20_ds2480_next(ds2480 *bus, ds2480_search *search)
+{
+ ds2480_status status;
+ do {
+ status = ds2480_search_next(bus, search);
+ if (status != DS2480_OK) return status;
+ } while (search->rom[0] != 0x28);
+ return DS2480_OK;
+}
+
+ds2480_status ds18b20_ds2480_convert(ds2480 *bus, const uint8_t rom[8])
+{
+ ds2480_status status = select_rom(bus, rom);
+ if (status != DS2480_OK) return status;
+ return ds2480_power_byte(bus, 0x44, 750);
+}
+
+ds2480_status ds18b20_ds2480_read(ds2480 *bus, const uint8_t rom[8], uint8_t scratchpad[9])
+{
+ uint8_t data[9], i;
+ ds2480_status status;
+ if (!rom || !scratchpad) return DS2480_ARGUMENT;
+ status = select_rom(bus, rom);
+ if (status != DS2480_OK) return status;
+ status = write_byte(bus, 0xBE);
+ if (status != DS2480_OK) return status;
+ for (i = 0; i < 9; ++i) {
+ status = ds2480_byte(bus, 0xFF, &data[i]);
+ if (status != DS2480_OK) return status;
+ }
+ if (ds2480_crc8(data, 9)) return DS2480_CRC;
+ /* All-zero data has valid CRC but cannot be a DS18B20 scratchpad. */
+ if ((data[4] & 0x9F) != 0x1F) return DS2480_DATA;
+ memcpy(scratchpad, data, sizeof data);
+ return DS2480_OK;
+}
+
+ds2480_status ds18b20_ds2480_temperature(ds2480 *bus, const uint8_t rom[8], int16_t *raw)
+{
+ uint8_t data[9], undefined;
+ uint16_t value;
+ int32_t signed_value;
+ ds2480_status status;
+ if (!raw) return DS2480_ARGUMENT;
+ status = ds18b20_ds2480_read(bus, rom, data);
+ if (status != DS2480_OK) return status;
+ undefined = (uint8_t)(3 - ((data[4] >> 5) & 3));
+ value = (uint16_t)((uint16_t)data[0] | ((uint16_t)data[1] << 8));
+ value &= (uint16_t)~((1U << undefined) - 1U);
+ signed_value = value & 0x8000 ? (int32_t)value - 65536 : (int32_t)value;
+ if (signed_value < -55 * 16 || signed_value > 125 * 16) return DS2480_DATA;
+ *raw = (int16_t)signed_value;
+ return DS2480_OK;
+}
+
+ds2480_status ds18b20_ds2480_configure(ds2480 *bus, const uint8_t rom[8],
+ int8_t th, int8_t tl, uint8_t resolution, uint8_t save)
+{
+ uint8_t data[9], config;
+ ds2480_status status;
+ if (!rom || resolution < 9 || resolution > 12) return DS2480_ARGUMENT;
+ config = (uint8_t)(0x1F | ((resolution - 9) << 5));
+ status = select_rom(bus, rom);
+ if (status != DS2480_OK) return status;
+ status = write_byte(bus, 0x4E);
+ if (status != DS2480_OK) return status;
+ status = write_byte(bus, (uint8_t)th);
+ if (status != DS2480_OK) return status;
+ status = write_byte(bus, (uint8_t)tl);
+ if (status != DS2480_OK) return status;
+ status = write_byte(bus, config);
+ if (status != DS2480_OK) return status;
+ status = ds18b20_ds2480_read(bus, rom, data);
+ if (status != DS2480_OK) return status;
+ if (data[2] != (uint8_t)th || data[3] != (uint8_t)tl || data[4] != config)
+ return DS2480_DATA;
+ if (!save) return DS2480_OK;
+ status = select_rom(bus, rom);
+ if (status != DS2480_OK) return status;
+ return ds2480_power_byte(bus, 0x48, 12);
+}
diff --git a/c/ds18b20-ds2480/ds18b20_ds2480.h b/c/ds18b20-ds2480/ds18b20_ds2480.h
new file mode 100644
index 0000000..fc20cc9
--- /dev/null
+++ b/c/ds18b20-ds2480/ds18b20_ds2480.h
@@ -0,0 +1,32 @@
+#ifndef DS18B20_DS2480_H
+#define DS18B20_DS2480_H
+#include "ds2480.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Find next DS18B20, skipping other families; same cursor rules as bridge. */
+ds2480_status ds18b20_ds2480_next(ds2480 *bus, ds2480_search *search);
+/** Start conversion and hold strong pullup for 750 ms (all resolutions).
+ * rom=NULL broadcasts: use only on a bus containing exclusively DS18B20.
+ * Supports external and parasite power within the bridge's current budget.
+ */
+ds2480_status ds18b20_ds2480_convert(ds2480 *bus, const uint8_t rom[8]);
+/** Read and validate all 9 scratchpad bytes. Output is unchanged on error.
+ * rom is mandatory, must have family 0x28 and valid CRC.
+ */
+ds2480_status ds18b20_ds2480_read(ds2480 *bus, const uint8_t rom[8], uint8_t scratchpad[9]);
+/** Read last conversion in signed 1/16 Celsius units; masks undefined bits
+ * for 9/10/11-bit resolution. Call convert first: power-on 85 C is ambiguous.
+ * Output is unchanged on error. Does not start a conversion itself.
+ */
+ds2480_status ds18b20_ds2480_temperature(ds2480 *bus, const uint8_t rom[8], int16_t *raw);
+/** Set TH/TL and resolution (9..12); verify by reading back.
+ * save!=0 copies to EEPROM with 12 ms strong pullup; avoid frequent writes.
+ */
+ds2480_status ds18b20_ds2480_configure(ds2480 *bus, const uint8_t rom[8],
+ int8_t th, int8_t tl, uint8_t resolution, uint8_t save);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/ds18b20-ds2480/ds2480.c b/c/ds18b20-ds2480/ds2480.c
new file mode 100644
index 0000000..8026083
--- /dev/null
+++ b/c/ds18b20-ds2480/ds2480.c
@@ -0,0 +1,164 @@
+#include "ds2480.h"
+
+static ds2480_status fault(ds2480 *bus, ds2480_status status)
+{
+ bus->ready = 0;
+ return status;
+}
+
+static ds2480_status exchange(ds2480 *bus, uint8_t command, uint8_t *reply)
+{
+ if (!bus || !reply) return DS2480_ARGUMENT;
+ if (!bus->ready) return DS2480_NOT_READY;
+ if (bus->port.write(bus->port.user, &command, 1, bus->timeout_ms) ||
+ bus->port.read(bus->port.user, reply, 1, bus->timeout_ms))
+ return fault(bus, DS2480_IO);
+ return DS2480_OK;
+}
+
+ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout_ms)
+{
+ uint8_t calibration = 0xC1, reply;
+ ds2480_status status;
+ ds2480_port copy;
+ if (!bus) return DS2480_ARGUMENT;
+ if (!port || !port->prepare || !port->write || !port->read ||
+ !port->delay_ms || !timeout_ms) {
+ bus->ready = 0;
+ return DS2480_ARGUMENT;
+ }
+ copy = *port; /* Also permit reinitialization with &bus->port. */
+ bus->ready = 0;
+ bus->port = copy;
+ bus->timeout_ms = timeout_ms;
+ if (copy.prepare(copy.user) ||
+ copy.write(copy.user, &calibration, 1, timeout_ms)) return DS2480_IO;
+ /* First C1 calibrates only: there is NO response byte. */
+ copy.delay_ms(copy.user, 2);
+ bus->ready = 1;
+ /* Strong pullup duration = infinite, command-mode operations only. */
+ status = exchange(bus, 0x3F, &reply);
+ if (status != DS2480_OK) return status;
+ if (reply != 0x3E) return fault(bus, DS2480_PROTOCOL);
+ return DS2480_OK;
+}
+
+ds2480_status ds2480_reset(ds2480 *bus)
+{
+ uint8_t reply;
+ ds2480_status status = exchange(bus, 0xC1, &reply);
+ if (status != DS2480_OK) return status;
+ /* DS2480B revision pattern from table 2; bit 5 is unspecified. */
+ if ((reply & 0xDC) != 0xCC) return fault(bus, DS2480_PROTOCOL);
+ switch (reply & 3) {
+ case 0: return DS2480_SHORT;
+ case 3: return DS2480_NO_PRESENCE;
+ default: return DS2480_OK; /* Ordinary or alarming presence. */
+ }
+}
+
+static ds2480_status slot(ds2480 *bus, uint8_t bit, uint8_t power, uint8_t *received)
+{
+ uint8_t reply, command = (uint8_t)(0x81 | (bit ? 0x10 : 0) | (power ? 2 : 0));
+ ds2480_status status = exchange(bus, command, &reply);
+ if (status != DS2480_OK) return status;
+ if ((reply & 0xFC) != (command & 0xFC) ||
+ ((reply & 3) != 0 && (reply & 3) != 3))
+ return fault(bus, DS2480_PROTOCOL);
+ *received = reply & 1;
+ return DS2480_OK;
+}
+
+ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received)
+{
+ if (!received) return DS2480_ARGUMENT;
+ return slot(bus, bit, 0, received);
+}
+
+ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received)
+{
+ uint8_t i, bit, result = 0;
+ ds2480_status status;
+ if (!received) return DS2480_ARGUMENT;
+ for (i = 0; i < 8; ++i) {
+ status = ds2480_bit(bus, (uint8_t)((value >> i) & 1), &bit);
+ if (status != DS2480_OK) return status;
+ result |= (uint8_t)(bit << i);
+ }
+ *received = result;
+ return DS2480_OK;
+}
+
+ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms)
+{
+ uint8_t i, bit, reply, echoed = 0;
+ ds2480_status status;
+ if (!bus || !hold_ms || hold_ms > 1000) return DS2480_ARGUMENT;
+ for (i = 0; i < 8; ++i) {
+ status = slot(bus, (uint8_t)((value >> i) & 1), (uint8_t)(i == 7), &bit);
+ if (status != DS2480_OK) {
+ /* A lost reply may leave an infinite pulse active: reset hardware. */
+ if (i == 7 && !bus->ready) (void)bus->port.prepare(bus->port.user);
+ return status;
+ }
+ echoed |= (uint8_t)(bit << i);
+ }
+ bus->port.delay_ms(bus->port.user, hold_ms);
+ status = exchange(bus, 0xF1, &reply);
+ if (status == DS2480_OK && reply != 0xEC && reply != 0xEF)
+ status = fault(bus, DS2480_PROTOCOL);
+ if (status != DS2480_OK) (void)bus->port.prepare(bus->port.user);
+ if (status == DS2480_OK && echoed != value) return DS2480_DATA;
+ return status;
+}
+
+uint8_t ds2480_crc8(const uint8_t *data, uint32_t size)
+{
+ uint8_t crc = 0, bit;
+ uint32_t i;
+ for (i = 0; i < size; ++i) {
+ crc ^= data[i];
+ for (bit = 0; bit < 8; ++bit)
+ crc = (uint8_t)((crc >> 1) ^ ((crc & 1) ? 0x8C : 0));
+ }
+ return crc;
+}
+
+ds2480_status ds2480_search_next(ds2480 *bus, ds2480_search *search)
+{
+ ds2480_search next;
+ ds2480_status status;
+ uint8_t pos, a, b, direction, ignored, last_zero = 0;
+ if (!bus || !search || search->discrepancy > 64) return DS2480_ARGUMENT;
+ if (search->done) return DS2480_DONE;
+ next = *search;
+ status = ds2480_reset(bus);
+ if (status != DS2480_OK) return status;
+ status = ds2480_byte(bus, 0xF0, &ignored);
+ if (status != DS2480_OK) return status;
+ for (pos = 1; pos <= 64; ++pos) {
+ uint8_t index = (uint8_t)((pos - 1) / 8);
+ uint8_t mask = (uint8_t)(1U << ((pos - 1) % 8));
+ status = ds2480_bit(bus, 1, &a);
+ if (status != DS2480_OK) return status;
+ status = ds2480_bit(bus, 1, &b);
+ if (status != DS2480_OK) return status;
+ if (a && b) return DS2480_DATA; /* Devices disappeared mid-search. */
+ if (a != b) direction = a;
+ else {
+ direction = pos < search->discrepancy ? (uint8_t)!!(search->rom[index] & mask)
+ : (uint8_t)(pos == search->discrepancy);
+ if (!direction) last_zero = pos;
+ }
+ if (direction) next.rom[index] |= mask;
+ else next.rom[index] &= (uint8_t)~mask;
+ status = ds2480_bit(bus, direction, &ignored);
+ if (status != DS2480_OK) return status;
+ }
+ if (!next.rom[0]) return DS2480_DATA;
+ if (ds2480_crc8(next.rom, 8)) return DS2480_CRC;
+ next.discrepancy = last_zero;
+ next.done = (uint8_t)(last_zero == 0);
+ *search = next;
+ return DS2480_OK;
+}
diff --git a/c/ds18b20-ds2480/ds2480.h b/c/ds18b20-ds2480/ds2480.h
new file mode 100644
index 0000000..f686049
--- /dev/null
+++ b/c/ds18b20-ds2480/ds2480.h
@@ -0,0 +1,69 @@
+#ifndef DS2480_H
+#define DS2480_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+ DS2480_OK = 0, DS2480_DONE, DS2480_NO_PRESENCE, DS2480_SHORT,
+ DS2480_IO, DS2480_PROTOCOL, DS2480_CRC, DS2480_ARGUMENT,
+ DS2480_NOT_READY, DS2480_DATA
+} ds2480_status;
+
+/** Blocking UART callbacks: 0 = success, nonzero = error/timeout.
+ * prepare resets the bridge (BREAK >= 2 ms or hardware reset), sets UART
+ * to 9600 8N1, waits >= 2 ms after reset, clears RX/errors before returning.
+ * write waits for physical TX completion; read receives exactly size bytes.
+ * Both enforce timeout_ms. RX must remain enabled while transmitting.
+ * delay_ms waits AT LEAST the requested duration. No callback may reenter.
+ */
+typedef struct {
+ void *user;
+ int (*prepare)(void *user);
+ int (*write)(void *user, const uint8_t *data, uint32_t size, uint32_t timeout_ms);
+ int (*read)(void *user, uint8_t *data, uint32_t size, uint32_t timeout_ms);
+ void (*delay_ms)(void *user, uint32_t ms);
+} ds2480_port;
+
+/** One instance per UART/bridge; serialize access for the whole operation. */
+typedef struct {
+ ds2480_port port;
+ uint32_t timeout_ms;
+ uint8_t ready;
+} ds2480;
+
+/** Independent ROM search cursor. Zero-initialize before each enumeration. */
+typedef struct {
+ uint8_t rom[8];
+ uint8_t discrepancy;
+ uint8_t done;
+} ds2480_search;
+
+/** Reset/calibrate DS2480B and verify configuration. Empty bus is allowed.
+ * On IO/protocol errors call init again before further transactions.
+ */
+ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout_ms);
+/** Standard-speed reset; distinguish short, empty bus and transport failure. */
+ds2480_status ds2480_reset(ds2480 *bus);
+/** Exchange one slot (write 1 to read); received must be non-NULL. */
+ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received);
+/** Exchange a byte, LSB first, in command mode (eight UART transactions). */
+ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received);
+/** Write a byte and start strong pullup immediately after its last slot.
+ * Blocks for hold_ms (1..1000), then terminates the pulse and consumes reply.
+ */
+ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms);
+/** CRC8 Dallas/Maxim. data must address size bytes (NULL allowed for size=0). */
+uint8_t ds2480_crc8(const uint8_t *data, uint32_t size);
+/** Search ALL families. OK yields ROM with valid CRC; DONE ends enumeration.
+ * Cursor changes only on success. After an error retry or zero it to restart.
+ */
+ds2480_status ds2480_search_next(ds2480 *bus, ds2480_search *search);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/ds18b20-ds2480/examples/read_first.c b/c/ds18b20-ds2480/examples/read_first.c
new file mode 100644
index 0000000..032174a
--- /dev/null
+++ b/c/ds18b20-ds2480/examples/read_first.c
@@ -0,0 +1,19 @@
+#include "ds18b20_ds2480.h"
+
+/* board_port supplies the four callbacks documented in ds2480.h.
+ * Blocking example: includes bridge reset, scan and 750 ms conversion.
+ */
+ds2480_status ds18b20_example_read_first(const ds2480_port *board_port, int16_t *raw)
+{
+ ds2480 bus;
+ ds2480_search search = {{0}, 0, 0};
+ ds2480_status status;
+ if (!raw) return DS2480_ARGUMENT;
+ status = ds2480_init(&bus, board_port, 20);
+ if (status != DS2480_OK) return status;
+ status = ds18b20_ds2480_next(&bus, &search);
+ if (status != DS2480_OK) return status;
+ status = ds18b20_ds2480_convert(&bus, search.rom);
+ if (status != DS2480_OK) return status;
+ return ds18b20_ds2480_temperature(&bus, search.rom, raw);
+}
diff --git a/c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c b/c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
new file mode 100644
index 0000000..84b1234
--- /dev/null
+++ b/c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
@@ -0,0 +1,228 @@
+#include "ds18b20_ds2480.h"
+#include
+#include
+#include
+
+#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s\n", __LINE__, #x); exit(1); } } while (0)
+
+enum { ROM_COMMAND, MATCH, FUNCTION, SEARCH, READ, CONFIG };
+typedef struct {
+ uint8_t rom[3][8], scratch[9];
+ unsigned count, active, state, phase, position, bits, value, match_index;
+ unsigned prepared, calibrated, pending, reply, pulse, hold, convert, copy;
+ unsigned fail_read, fail_write, fail_prepare, corrupt_reply, reset_reply;
+ unsigned fail_power, fail_stop, reject_config;
+} fake;
+
+static void crc_scratch(fake *f) { f->scratch[8] = ds2480_crc8(f->scratch, 8); }
+
+static void setup(fake *f)
+{
+ unsigned i;
+ static const uint8_t scratch[9] = {0x91, 0x01, 0x4B, 0x46, 0x7F, 0xFF, 0x0C, 0x10, 0x70};
+ memset(f, 0, sizeof *f);
+ f->count = 3;
+ f->reset_reply = 0xCD;
+ memcpy(f->scratch, scratch, 9);
+ for (i = 0; i < 3; ++i) {
+ f->rom[i][0] = i == 2 ? 0x10 : 0x28;
+ f->rom[i][1] = (uint8_t)(i + 1);
+ f->rom[i][7] = ds2480_crc8(f->rom[i], 7);
+ }
+}
+
+static void byte_in(fake *f, uint8_t byte)
+{
+ unsigned i;
+ if (f->state == ROM_COMMAND) {
+ if (byte == 0xF0) { f->state = SEARCH; f->position = f->phase = 0; }
+ else if (byte == 0x55) { f->state = MATCH; f->match_index = 0; }
+ else { CHECK(byte == 0xCC); f->state = FUNCTION; }
+ } else if (f->state == MATCH) {
+ for (i = 0; i < f->count; ++i)
+ if (f->rom[i][f->match_index] != byte) f->active &= ~(1U << i);
+ if (++f->match_index == 8) f->state = FUNCTION;
+ } else if (f->state == FUNCTION) {
+ CHECK(f->active != 0);
+ switch (byte) {
+ case 0xBE: f->state = READ; f->position = 0; break;
+ case 0x4E: f->state = CONFIG; f->position = 2; break;
+ case 0x44: ++f->convert; break;
+ case 0x48: ++f->copy; break;
+ default: CHECK(0);
+ }
+ } else {
+ CHECK(f->state == CONFIG);
+ if (!f->reject_config) f->scratch[f->position] = byte;
+ if (++f->position == 5) { crc_scratch(f); f->state = FUNCTION; }
+ }
+}
+
+static uint8_t wire_bit(fake *f, uint8_t bit)
+{
+ unsigned i, result = 1;
+ if (f->state == SEARCH) {
+ for (i = 0; i < f->count; ++i) {
+ unsigned v = (f->rom[i][f->position / 8] >> (f->position % 8)) & 1;
+ if (!(f->active & (1U << i))) continue;
+ if (f->phase < 2) result &= f->phase ? !v : v;
+ else if (v != bit) f->active &= ~(1U << i);
+ }
+ if (++f->phase == 3) { f->phase = 0; ++f->position; }
+ return (uint8_t)result;
+ }
+ if (f->state == READ) {
+ CHECK(f->position < 72 && bit == 1);
+ result = (f->scratch[f->position / 8] >> (f->position % 8)) & 1;
+ ++f->position;
+ return (uint8_t)result;
+ }
+ f->value |= (unsigned)bit << f->bits;
+ if (++f->bits == 8) {
+ uint8_t byte = (uint8_t)f->value;
+ f->bits = f->value = 0;
+ byte_in(f, byte);
+ }
+ return bit;
+}
+
+static int prepare(void *user)
+{
+ fake *f = user;
+ ++f->prepared;
+ f->pending = f->pulse = f->calibrated = 0;
+ return (int)f->fail_prepare;
+}
+
+static int transmit(void *user, const uint8_t *data, uint32_t size, uint32_t timeout)
+{
+ fake *f = user;
+ uint8_t command = *data, bit;
+ CHECK(size == 1 && timeout == 20 && !f->pending);
+ if (f->fail_write) return -1;
+ if (!f->calibrated) { CHECK(command == 0xC1); f->calibrated = 1; return 0; }
+ CHECK(!f->pulse || command == 0xF1);
+ if (command == 0x3F) f->reply = 0x3E;
+ else if (command == 0xC1) {
+ f->reply = f->count ? f->reset_reply : 0xCF;
+ f->state = ROM_COMMAND; f->bits = f->value = 0;
+ f->active = (1U << f->count) - 1;
+ } else if (command == 0xF1) {
+ CHECK(f->pulse && (f->hold == 750 || f->hold == 12));
+ f->pulse = 0; f->reply = 0xEC;
+ if (f->fail_stop) f->fail_read = 1;
+ } else {
+ CHECK(command == 0x81 || command == 0x91 || command == 0x83 || command == 0x93);
+ bit = wire_bit(f, (uint8_t)((command >> 4) & 1));
+ f->reply = (command & 0xFC) | (bit ? 3U : 0U);
+ if (command & 2) {
+ CHECK(f->bits == 0 && (f->convert || f->copy));
+ f->pulse = 1; f->hold = 0;
+ if (f->fail_power) f->fail_read = 1;
+ }
+ }
+ f->pending = 1;
+ return 0;
+}
+
+static int receive(void *user, uint8_t *data, uint32_t size, uint32_t timeout)
+{
+ fake *f = user;
+ CHECK(size == 1 && timeout == 20 && f->pending);
+ if (f->fail_read) return -1;
+ *data = (uint8_t)(f->corrupt_reply ? 0 : f->reply);
+ f->pending = 0;
+ return 0;
+}
+
+static void delay(void *user, uint32_t ms)
+{
+ fake *f = user;
+ if (f->pulse) f->hold += ms;
+ else CHECK(ms == 2);
+}
+
+static void init(fake *f, ds2480 *bus)
+{
+ ds2480_port port = { f, prepare, transmit, receive, delay };
+ CHECK(ds2480_init(bus, &port, 20) == DS2480_OK);
+ CHECK(!f->pending);
+}
+
+int main(void)
+{
+ fake f, other;
+ ds2480 bus, second;
+ ds2480_search search = {{0}, 0, 0}, saved;
+ uint8_t bit, scratch[9], rom[8];
+ int16_t raw = 999;
+ unsigned found = 0, i;
+ setup(&f); init(&f, &bus);
+ CHECK(ds2480_crc8(f.scratch, 9) == 0); /* Datasheet vector, not generated. */
+ while (ds18b20_ds2480_next(&bus, &search) == DS2480_OK) {
+ if (!memcmp(search.rom, f.rom[0], 8)) found |= 1;
+ else if (!memcmp(search.rom, f.rom[1], 8)) found |= 2;
+ else CHECK(0);
+ }
+ CHECK(found == 3 && search.done);
+ CHECK(ds18b20_ds2480_next(&bus, &search) == DS2480_DONE);
+ CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_OK);
+ CHECK(f.convert == 1 && f.hold == 750 && !f.pulse && !f.pending);
+ CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK && raw == 401);
+ CHECK(ds18b20_ds2480_convert(&bus, f.rom[1]) == DS2480_OK && f.active == 2);
+ for (i = 9; i <= 12; ++i) {
+ CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], -5, -20, (uint8_t)i, 1) == DS2480_OK);
+ CHECK(f.hold == 12 && !f.pulse && !f.pending);
+ f.scratch[0] = 0x5F; f.scratch[1] = 0xFF; crc_scratch(&f);
+ CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK);
+ CHECK(raw == (i == 9 ? -168 : i == 10 ? -164 : i == 11 ? -162 : -161));
+ }
+ CHECK(f.copy == 4);
+ f.reject_config = 1;
+ CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], 1, 2, 9, 1) == DS2480_DATA && f.copy == 4);
+ f.scratch[8] ^= 1; raw = 999;
+ CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_CRC && raw == 999);
+ memset(f.scratch, 0, 9);
+ CHECK(ds18b20_ds2480_read(&bus, f.rom[0], scratch) == DS2480_DATA);
+ memset(f.scratch, 0xFF, 9);
+ CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_CRC);
+ memcpy(rom, f.rom[0], 8); rom[7] ^= 1;
+ CHECK(ds18b20_ds2480_convert(&bus, rom) == DS2480_ARGUMENT);
+ CHECK(ds18b20_ds2480_read(&bus, NULL, scratch) == DS2480_ARGUMENT);
+ CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], 0, 0, 8, 0) == DS2480_ARGUMENT);
+ CHECK(ds2480_bit(&bus, 1, NULL) == DS2480_ARGUMENT);
+ f.reset_reply = 0xEC; CHECK(ds2480_reset(&bus) == DS2480_SHORT);
+ f.reset_reply = 0xEE; CHECK(ds2480_reset(&bus) == DS2480_OK);
+ f.count = 0; CHECK(ds2480_reset(&bus) == DS2480_NO_PRESENCE);
+ memset(&search, 0, sizeof search);
+ CHECK(ds2480_search_next(&bus, &search) == DS2480_NO_PRESENCE);
+ setup(&f); init(&f, &bus); f.count = 1; f.rom[0][7] ^= 1;
+ saved = search;
+ CHECK(ds2480_search_next(&bus, &search) == DS2480_CRC);
+ CHECK(!memcmp(&search, &saved, sizeof search));
+ f.fail_read = 1; CHECK(ds2480_reset(&bus) == DS2480_IO);
+ CHECK(ds2480_reset(&bus) == DS2480_NOT_READY);
+ setup(&f); init(&f, &bus); f.corrupt_reply = 1;
+ CHECK(ds2480_reset(&bus) == DS2480_PROTOCOL && !bus.ready);
+ setup(&f); init(&f, &bus); CHECK(ds2480_reset(&bus) == DS2480_OK); f.corrupt_reply = 1;
+ CHECK(ds2480_bit(&bus, 1, &bit) == DS2480_PROTOCOL);
+ setup(&f); init(&f, &bus); f.fail_write = 1;
+ CHECK(ds2480_reset(&bus) == DS2480_IO && !bus.ready);
+ setup(&f); init(&f, &bus); f.fail_power = 1;
+ CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_IO);
+ CHECK(!f.pulse && !bus.ready && f.prepared == 2);
+ setup(&f); init(&f, &bus); f.fail_stop = 1;
+ CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_IO);
+ CHECK(!f.pulse && !bus.ready && f.prepared == 2);
+ setup(&f); init(&f, &bus);
+ setup(&other); init(&other, &second);
+ CHECK(ds18b20_ds2480_convert(&second, other.rom[1]) == DS2480_OK);
+ CHECK(f.convert == 0 && other.convert == 1);
+ CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK && raw == 401);
+ CHECK(ds2480_init(&bus, &bus.port, 20) == DS2480_OK);
+ f.fail_prepare = 1;
+ CHECK(ds2480_init(&bus, &bus.port, 20) == DS2480_IO && !bus.ready);
+ CHECK(ds2480_init(&bus, NULL, 20) == DS2480_ARGUMENT);
+ puts("DS18B20/DS2480: all host tests passed");
+ return 0;
+}
From ac3025ee32b0bf893f9c0a504155b925d350f2a9 Mon Sep 17 00:00:00 2001
From: Andrey Kruchinkin
Date: Tue, 15 Sep 2026 22:32:30 +0300
Subject: [PATCH 34/37] =?UTF-8?q?feat(ds18b20-ds2480):=20=D0=B4=D0=BE?=
=?UTF-8?q?=D0=B1=D0=B0=D0=B2=D1=8C=20=D0=BF=D0=BE=D1=80=D1=82=20F407=20?=
=?UTF-8?q?=D0=B8=20=D0=B0=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=BD?=
=?UTF-8?q?=D1=83=D1=8E=20=D0=BF=D0=BE=D0=B4=D1=82=D1=8F=D0=B6=D0=BA=D1=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
c/ds18b20-ds2480/README.md | 6 +-
c/ds18b20-ds2480/ds2480.c | 36 ++++++--
c/ds18b20-ds2480/ds2480.h | 11 ++-
c/ds18b20-ds2480/ports/stm32f4/README.md | 52 ++++++++++++
.../ports/stm32f4/ds2480_stm32f4_hal.c | 85 +++++++++++++++++++
.../ports/stm32f4/ds2480_stm32f4_hal.h | 32 +++++++
c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c | 10 +++
7 files changed, 223 insertions(+), 9 deletions(-)
create mode 100644 c/ds18b20-ds2480/ports/stm32f4/README.md
create mode 100644 c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.c
create mode 100644 c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.h
diff --git a/c/ds18b20-ds2480/README.md b/c/ds18b20-ds2480/README.md
index 524d2b6..7d60e68 100644
--- a/c/ds18b20-ds2480/README.md
+++ b/c/ds18b20-ds2480/README.md
@@ -105,8 +105,7 @@ UART-обмен. EEPROM использует выдержку 12 мс, её со
## Подключение и тесты
Добавьте `ds2480.c`, `ds18b20_ds2480.c` и путь к заголовкам в сборку прошивки.
-Номер UART, GPIO и библиотеку платформы выбирает приложение. Готовый порт
-для конкретной платы пока не входит в библиотеку.
+Номер UART, GPIO и библиотеку платформы выбирает приложение. Порт [STM32F407 / STM32F4 HAL](ports/stm32f4/README.md) входит в библиотеку.
```cmake
set(DS18B20_DS2480_BUILD_TESTS OFF CACHE BOOL "" FORCE)
@@ -137,7 +136,8 @@ clang -std=c99 -Wall -Wextra -Wpedantic -Werror -I . ds2480.c ds18b20_ds2480.c t
## Использование
Добавлена в сабмодуль `templates` проекта `john103C6T6NewVer`, ветка `ds2480`.
-В рабочую прошивку ещё не включена: для этого требуется порт выбранного UART.
+Подключена к опросу climate через USART6 PC6/PC7; администратор выбирает GPIO или DS2480.
+Ожидание преобразования в climate неблокирующее: `ds2480_power_begin/end`.
## Источники
diff --git a/c/ds18b20-ds2480/ds2480.c b/c/ds18b20-ds2480/ds2480.c
index 8026083..3082662 100644
--- a/c/ds18b20-ds2480/ds2480.c
+++ b/c/ds18b20-ds2480/ds2480.c
@@ -10,6 +10,7 @@ static ds2480_status exchange(ds2480 *bus, uint8_t command, uint8_t *reply)
{
if (!bus || !reply) return DS2480_ARGUMENT;
if (!bus->ready) return DS2480_NOT_READY;
+ if (bus->power_active && command != 0xF1) return DS2480_BUSY;
if (bus->port.write(bus->port.user, &command, 1, bus->timeout_ms) ||
bus->port.read(bus->port.user, reply, 1, bus->timeout_ms))
return fault(bus, DS2480_IO);
@@ -29,6 +30,7 @@ ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout
}
copy = *port; /* Also permit reinitialization with &bus->port. */
bus->ready = 0;
+ bus->power_active = 0;
bus->port = copy;
bus->timeout_ms = timeout_ms;
if (copy.prepare(copy.user) ||
@@ -89,11 +91,11 @@ ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received)
return DS2480_OK;
}
-ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms)
+ds2480_status ds2480_power_begin(ds2480 *bus, uint8_t value)
{
- uint8_t i, bit, reply, echoed = 0;
+ uint8_t i, bit, echoed = 0;
ds2480_status status;
- if (!bus || !hold_ms || hold_ms > 1000) return DS2480_ARGUMENT;
+ if (!bus) return DS2480_ARGUMENT;
for (i = 0; i < 8; ++i) {
status = slot(bus, (uint8_t)((value >> i) & 1), (uint8_t)(i == 7), &bit);
if (status != DS2480_OK) {
@@ -103,15 +105,39 @@ ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms)
}
echoed |= (uint8_t)(bit << i);
}
- bus->port.delay_ms(bus->port.user, hold_ms);
+ bus->power_active = 1;
+ if (echoed != value) {
+ status = ds2480_power_end(bus);
+ return status == DS2480_OK ? DS2480_DATA : status;
+ }
+ return DS2480_OK;
+}
+
+ds2480_status ds2480_power_end(ds2480 *bus)
+{
+ uint8_t reply;
+ ds2480_status status;
+ if (!bus) return DS2480_ARGUMENT;
+ if (!bus->ready) return DS2480_NOT_READY;
+ if (!bus->power_active) return DS2480_OK;
status = exchange(bus, 0xF1, &reply);
if (status == DS2480_OK && reply != 0xEC && reply != 0xEF)
status = fault(bus, DS2480_PROTOCOL);
if (status != DS2480_OK) (void)bus->port.prepare(bus->port.user);
- if (status == DS2480_OK && echoed != value) return DS2480_DATA;
+ bus->power_active = 0;
return status;
}
+ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms)
+{
+ ds2480_status status;
+ if (!hold_ms || hold_ms > 1000) return DS2480_ARGUMENT;
+ status = ds2480_power_begin(bus, value);
+ if (status != DS2480_OK) return status;
+ bus->port.delay_ms(bus->port.user, hold_ms);
+ return ds2480_power_end(bus);
+}
+
uint8_t ds2480_crc8(const uint8_t *data, uint32_t size)
{
uint8_t crc = 0, bit;
diff --git a/c/ds18b20-ds2480/ds2480.h b/c/ds18b20-ds2480/ds2480.h
index f686049..a17d82f 100644
--- a/c/ds18b20-ds2480/ds2480.h
+++ b/c/ds18b20-ds2480/ds2480.h
@@ -10,7 +10,7 @@ extern "C" {
typedef enum {
DS2480_OK = 0, DS2480_DONE, DS2480_NO_PRESENCE, DS2480_SHORT,
DS2480_IO, DS2480_PROTOCOL, DS2480_CRC, DS2480_ARGUMENT,
- DS2480_NOT_READY, DS2480_DATA
+ DS2480_NOT_READY, DS2480_DATA, DS2480_BUSY
} ds2480_status;
/** Blocking UART callbacks: 0 = success, nonzero = error/timeout.
@@ -33,6 +33,7 @@ typedef struct {
ds2480_port port;
uint32_t timeout_ms;
uint8_t ready;
+ uint8_t power_active;
} ds2480;
/** Independent ROM search cursor. Zero-initialize before each enumeration. */
@@ -56,6 +57,14 @@ ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received);
* Blocks for hold_ms (1..1000), then terminates the pulse and consumes reply.
*/
ds2480_status ds2480_power_byte(ds2480 *bus, uint8_t value, uint32_t hold_ms);
+/** Start indefinite strong pullup after the final bit; returns immediately
+ * after UART exchange. Until power_end, other bus operations return BUSY.
+ * Application MUST call power_end after the sensor's required hold time,
+ * or init to cancel/recover. No internal timer/interrupt releases the pulse.
+ */
+ds2480_status ds2480_power_begin(ds2480 *bus, uint8_t value);
+/** Release strong pullup, consume its reply. Idempotent when ready/idle. */
+ds2480_status ds2480_power_end(ds2480 *bus);
/** CRC8 Dallas/Maxim. data must address size bytes (NULL allowed for size=0). */
uint8_t ds2480_crc8(const uint8_t *data, uint32_t size);
/** Search ALL families. OK yields ROM with valid CRC; DONE ends enumeration.
diff --git a/c/ds18b20-ds2480/ports/stm32f4/README.md b/c/ds18b20-ds2480/ports/stm32f4/README.md
new file mode 100644
index 0000000..17c97dc
--- /dev/null
+++ b/c/ds18b20-ds2480/ports/stm32f4/README.md
@@ -0,0 +1,52 @@
+# STM32F407 / STM32F4 HAL
+
+Порт UART для `ds2480.c`. Платформа настраивает тактирование и GPIO, порт
+формирует BREAK через TX GPIO, восстанавливает UART 9600 8N1 и проверяет
+ошибки приёма. DMA и обработчики UART-прерываний не нужны. HAL tick должен
+работать; вызывать из прерывания или при запрещённых прерываниях нельзя.
+
+```c
+ds2480 bridge;
+ds2480_port io;
+ds2480_stm32f4_hal platform = {&huart6, GPIOC, GPIO_PIN_6, GPIO_AF8_USART6};
+/* Before this point: enable GPIOC/USART6 clocks, configure PC6/PC7 as AF8. */
+if (ds2480_stm32f4_hal_bind(&platform, &io) != DS2480_OK) return;
+if (ds2480_init(&bridge, &io, 20) != DS2480_OK) return;
+```
+
+В сборку добавляются `ds2480_stm32f4_hal.c`, ядро библиотеки, путь к этому
+каталогу и STM32F4 HAL/CMSIS. UART выделяется только для моста. Порт принимает
+по одному байту за операцию: ответ сохраняется в DR во время завершения TX,
+затем читается без сброса RX. FE/NE/ORE/PE означают ошибку обмена даже при RXNE.
+
+## Подключение в climate F407VET6
+
+| STM32 / питание | DS2480B |
+|---|---|
+| PC6, USART6_TX | TXD, вывод 7 (вход моста) |
+| PC7, USART6_RX | RXD, вывод 8 (выход моста) |
+| Общая земля | GND, вывод 1 |
+| +5 В | VDD, вывод 4; VPP, вывод 5; POL, вывод 6 |
+| DQ датчиков DS18B20 | 1-W, вывод 2 |
+
+DS2480B работает от 5 В. Проверьте согласование логических уровней по
+электрическим характеристикам конкретной платы/модуля; не считайте питание
+DS2480B от 3,3 В допустимым. Для внешнего питания DS18B20 подключите VDD;
+при паразитном питании VDD датчика соединяется с GND. В обоих случаях общий GND.
+
+PC6/PC7 выбраны в `climate` (AF8). UART1/2 используются Modbus, SDIO использует
+4-битную шину PC8..PC12/PD2. Пины для другой платы задаются её приложением.
+
+## Неблокирующее питание датчиков
+
+`ds2480_power_begin` посылает команду датчика с strong pullup на последнем
+слоте. Приложение возвращается в главный цикл, отсчитывает 750 мс для
+преобразования либо минимум 10 мс для EEPROM, затем вызывает
+`ds2480_power_end`. До завершения импульса остальной обмен возвращает BUSY.
+При отмене вызывайте `power_end`, при потере синхронизации — `ds2480_init`.
+
+Используется в `climate_control_f407vet6_f4`: `ds2480_app.c` связывает порт
+из сабмодуля с существующим каталогом датчиков и диагностикой Modbus.
+
+Источники: [STM32F407, таблица alternate functions](https://www.st.com/resource/en/datasheet/stm32f407ve.pdf),
+[DS2480B, выводы и UART](https://www.analog.com/media/en/technical-documentation/data-sheets/ds2480b.pdf).
diff --git a/c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.c b/c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.c
new file mode 100644
index 0000000..70019be
--- /dev/null
+++ b/c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.c
@@ -0,0 +1,85 @@
+#include "ds2480_stm32f4_hal.h"
+
+#define RX_ERRORS (USART_SR_ORE | USART_SR_NE | USART_SR_FE | USART_SR_PE)
+
+static int prepare(void *user)
+{
+ ds2480_stm32f4_hal *p = (ds2480_stm32f4_hal *)user;
+ GPIO_InitTypeDef gpio = {0};
+ /* Abort resets HAL states after timeouts and disables UART IRQ/DMA. */
+ if (HAL_UART_Abort(p->uart) != HAL_OK) return -1;
+ __HAL_UART_DISABLE(p->uart);
+ HAL_GPIO_WritePin(p->tx_port, p->tx_pin, GPIO_PIN_RESET);
+ gpio.Pin = p->tx_pin;
+ gpio.Mode = GPIO_MODE_OUTPUT_PP;
+ gpio.Pull = GPIO_NOPULL;
+ gpio.Speed = GPIO_SPEED_FREQ_HIGH;
+ HAL_GPIO_Init(p->tx_port, &gpio);
+ HAL_Delay(2);
+ HAL_GPIO_WritePin(p->tx_port, p->tx_pin, GPIO_PIN_SET);
+ HAL_Delay(2);
+ gpio.Mode = GPIO_MODE_AF_PP;
+ gpio.Alternate = p->tx_alternate;
+ HAL_GPIO_Init(p->tx_port, &gpio);
+ p->uart->Init.BaudRate = 9600;
+ p->uart->Init.WordLength = UART_WORDLENGTH_8B;
+ p->uart->Init.StopBits = UART_STOPBITS_1;
+ p->uart->Init.Parity = UART_PARITY_NONE;
+ p->uart->Init.Mode = UART_MODE_TX_RX;
+ p->uart->Init.HwFlowCtl = UART_HWCONTROL_NONE;
+ p->uart->Init.OverSampling = UART_OVERSAMPLING_16;
+ if (HAL_UART_Init(p->uart) != HAL_OK) return -1;
+ /* SR then DR: discard BREAK echo/stale RX and clear FE/NE/ORE/PE. */
+ __HAL_UART_CLEAR_OREFLAG(p->uart);
+ return 0;
+}
+
+static int write_byte(void *user, const uint8_t *data, uint32_t size, uint32_t timeout)
+{
+ ds2480_stm32f4_hal *p = (ds2480_stm32f4_hal *)user;
+ if (!data || size != 1 || !timeout || timeout == HAL_MAX_DELAY) return -1;
+ /* One reply fits in DR while HAL waits for TX complete. Never flush RX
+ * here: a reply may already be present when HAL_UART_Transmit returns. */
+ if (p->uart->Instance->SR & RX_ERRORS) return -1;
+ return HAL_UART_Transmit(p->uart, data, 1, timeout) == HAL_OK ? 0 : -1;
+}
+
+static int read_byte(void *user, uint8_t *data, uint32_t size, uint32_t timeout)
+{
+ ds2480_stm32f4_hal *p = (ds2480_stm32f4_hal *)user;
+ uint32_t start, flags;
+ if (!data || size != 1 || !timeout || timeout == HAL_MAX_DELAY) return -1;
+ start = HAL_GetTick();
+ for (;;) {
+ flags = p->uart->Instance->SR;
+ /* Test errors BEFORE reading DR, including when RXNE is already set. */
+ if (flags & RX_ERRORS) {
+ __HAL_UART_CLEAR_OREFLAG(p->uart);
+ return -1;
+ }
+ if (flags & USART_SR_RXNE) {
+ *data = (uint8_t)p->uart->Instance->DR;
+ return 0;
+ }
+ if ((uint32_t)(HAL_GetTick() - start) >= timeout) return -1;
+ }
+}
+
+static void delay_ms(void *user, uint32_t ms)
+{
+ (void)user;
+ HAL_Delay(ms);
+}
+
+ds2480_status ds2480_stm32f4_hal_bind(ds2480_stm32f4_hal *p, ds2480_port *port)
+{
+ if (!p || !port || !p->uart || !p->uart->Instance || !p->tx_port ||
+ !p->tx_pin || (p->tx_pin & (p->tx_pin - 1U)) || p->tx_alternate > 15U)
+ return DS2480_ARGUMENT;
+ port->user = p;
+ port->prepare = prepare;
+ port->write = write_byte;
+ port->read = read_byte;
+ port->delay_ms = delay_ms;
+ return DS2480_OK;
+}
diff --git a/c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.h b/c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.h
new file mode 100644
index 0000000..0830cc6
--- /dev/null
+++ b/c/ds18b20-ds2480/ports/stm32f4/ds2480_stm32f4_hal.h
@@ -0,0 +1,32 @@
+#ifndef DS2480_STM32F4_HAL_H
+#define DS2480_STM32F4_HAL_H
+
+#include "ds2480.h"
+#include "stm32f4xx_hal.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Dedicated UART initialized by the board (clock, RX/TX AF, no IRQ/DMA).
+ * tx_port/pin/alternate let prepare generate a real >=2 ms BREAK, including
+ * recovery from a lost strong-pullup response. No reset pin is required.
+ * GPIO clock must stay enabled. UART must not be used by any other service.
+ */
+typedef struct {
+ UART_HandleTypeDef *uart;
+ GPIO_TypeDef *tx_port;
+ uint16_t tx_pin;
+ uint32_t tx_alternate;
+} ds2480_stm32f4_hal;
+
+/** Populate callbacks only; call ds2480_init afterwards to reset/calibrate.
+ * Thread/main-loop use only, with running HAL tick and interrupts enabled.
+ * UART baud rate is fixed at 9600 8N1; all transfers are single-byte.
+ */
+ds2480_status ds2480_stm32f4_hal_bind(ds2480_stm32f4_hal *context, ds2480_port *port);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c b/c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
index 84b1234..5c1777b 100644
--- a/c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
+++ b/c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
@@ -168,6 +168,16 @@ int main(void)
CHECK(ds18b20_ds2480_next(&bus, &search) == DS2480_DONE);
CHECK(ds18b20_ds2480_convert(&bus, NULL) == DS2480_OK);
CHECK(f.convert == 1 && f.hold == 750 && !f.pulse && !f.pending);
+ CHECK(ds2480_reset(&bus) == DS2480_OK);
+ CHECK(ds2480_byte(&bus, 0xCC, &bit) == DS2480_OK);
+ CHECK(ds2480_power_begin(&bus, 0x44) == DS2480_OK);
+ CHECK(f.hold == 0 && f.pulse && bus.power_active);
+ CHECK(ds2480_reset(&bus) == DS2480_BUSY);
+ CHECK(ds2480_byte(&bus, 0xFF, &bit) == DS2480_BUSY);
+ CHECK(ds2480_power_begin(&bus, 0x44) == DS2480_BUSY);
+ delay(&f, 750);
+ CHECK(ds2480_power_end(&bus) == DS2480_OK && !bus.power_active);
+ CHECK(ds2480_power_end(&bus) == DS2480_OK);
CHECK(ds18b20_ds2480_temperature(&bus, f.rom[0], &raw) == DS2480_OK && raw == 401);
CHECK(ds18b20_ds2480_convert(&bus, f.rom[1]) == DS2480_OK && f.active == 2);
for (i = 9; i <= 12; ++i) {
From 10bfa587c730cd0628092ac1cee84300de37f38e Mon Sep 17 00:00:00 2001
From: Andrey Kruchinkin
Date: Tue, 15 Sep 2026 22:45:07 +0300
Subject: [PATCH 35/37] =?UTF-8?q?perf(ds18b20-ds2480):=20=D0=BF=D0=B5?=
=?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B0=D0=B2=D0=B0=D0=B9=20=D0=B1=D0=B0=D0=B9?=
=?UTF-8?q?=D1=82=D1=8B=20=D0=B2=20Data=20Mode?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
c/ds18b20-ds2480/CMakeLists.txt | 5 +++
c/ds18b20-ds2480/README.md | 8 ++--
c/ds18b20-ds2480/ds2480.c | 30 ++++++++++----
c/ds18b20-ds2480/ds2480.h | 3 +-
.../ports/stm32f4/tests/stm32f4xx_hal.h | 39 ++++++++++++++++++
.../ports/stm32f4/tests/test_port.c | 41 +++++++++++++++++++
c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c | 15 +++++++
7 files changed, 128 insertions(+), 13 deletions(-)
create mode 100644 c/ds18b20-ds2480/ports/stm32f4/tests/stm32f4xx_hal.h
create mode 100644 c/ds18b20-ds2480/ports/stm32f4/tests/test_port.c
diff --git a/c/ds18b20-ds2480/CMakeLists.txt b/c/ds18b20-ds2480/CMakeLists.txt
index 40698a2..1f24308 100644
--- a/c/ds18b20-ds2480/CMakeLists.txt
+++ b/c/ds18b20-ds2480/CMakeLists.txt
@@ -14,4 +14,9 @@ if(DS18B20_DS2480_BUILD_TESTS)
add_executable(test_ds18b20_ds2480 tests/test_ds18b20_ds2480.c)
target_link_libraries(test_ds18b20_ds2480 PRIVATE ds18b20_ds2480)
add_test(NAME ds18b20_ds2480 COMMAND test_ds18b20_ds2480)
+ add_executable(test_ds2480_stm32f4
+ ports/stm32f4/ds2480_stm32f4_hal.c ports/stm32f4/tests/test_port.c)
+ target_include_directories(test_ds2480_stm32f4 PRIVATE
+ . ports/stm32f4 ports/stm32f4/tests)
+ add_test(NAME ds2480_stm32f4 COMMAND test_ds2480_stm32f4)
endif()
diff --git a/c/ds18b20-ds2480/README.md b/c/ds18b20-ds2480/README.md
index 7d60e68..011e4e6 100644
--- a/c/ds18b20-ds2480/README.md
+++ b/c/ds18b20-ds2480/README.md
@@ -94,10 +94,10 @@ UART-обмен. EEPROM использует выдержку 12 мс, её со
импульс, и остаётся неготовым. При отказе самого порта снятие питания гарантировать
невозможно; восстановление UART/моста остаётся задачей приложения.
-Используется стандартная скорость 1-Wire и только Command Mode: байт передаётся
-восьмью командами Single Bit. Поэтому любые значения, включая `E3`, передаются
-без экранирования. Это простая реализация для опроса температуры; повышенные
-скорости UART, Overdrive и Search Accelerator пока не реализованы. Параметры
+Используется стандартная скорость 1-Wire, UART 9600 бод. Байты передаются в
+Data Mode с экранированием E3, reset/поиск/strong pullup — в Command Mode.
+Переключения режима выполняет библиотека. Повышенные скорости UART,
+Overdrive и Search Accelerator пока не реализованы. Параметры
таймингов длинной линии остаются заводскими. Аппаратная проверка обязательна
для выбранной топологии/нагрузки. Протокол проверен по документации **DS2480B**;
старые ревизии DS2480 без суффикса B отдельно не проверялись.
diff --git a/c/ds18b20-ds2480/ds2480.c b/c/ds18b20-ds2480/ds2480.c
index 3082662..8d1a878 100644
--- a/c/ds18b20-ds2480/ds2480.c
+++ b/c/ds18b20-ds2480/ds2480.c
@@ -8,9 +8,15 @@ static ds2480_status fault(ds2480 *bus, ds2480_status status)
static ds2480_status exchange(ds2480 *bus, uint8_t command, uint8_t *reply)
{
+ uint8_t mode = 0xE3;
if (!bus || !reply) return DS2480_ARGUMENT;
if (!bus->ready) return DS2480_NOT_READY;
if (bus->power_active && command != 0xF1) return DS2480_BUSY;
+ if (bus->data_mode) {
+ if (bus->port.write(bus->port.user, &mode, 1, bus->timeout_ms))
+ return fault(bus, DS2480_IO);
+ bus->data_mode = 0;
+ }
if (bus->port.write(bus->port.user, &command, 1, bus->timeout_ms) ||
bus->port.read(bus->port.user, reply, 1, bus->timeout_ms))
return fault(bus, DS2480_IO);
@@ -31,6 +37,7 @@ ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout
copy = *port; /* Also permit reinitialization with &bus->port. */
bus->ready = 0;
bus->power_active = 0;
+ bus->data_mode = 0;
bus->port = copy;
bus->timeout_ms = timeout_ms;
if (copy.prepare(copy.user) ||
@@ -79,15 +86,22 @@ ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received)
ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received)
{
- uint8_t i, bit, result = 0;
- ds2480_status status;
- if (!received) return DS2480_ARGUMENT;
- for (i = 0; i < 8; ++i) {
- status = ds2480_bit(bus, (uint8_t)((value >> i) & 1), &bit);
- if (status != DS2480_OK) return status;
- result |= (uint8_t)(bit << i);
+ uint8_t mode = 0xE1, reply;
+ if (!bus || !received) return DS2480_ARGUMENT;
+ if (!bus->ready) return DS2480_NOT_READY;
+ if (bus->power_active) return DS2480_BUSY;
+ if (!bus->data_mode) {
+ if (bus->port.write(bus->port.user, &mode, 1, bus->timeout_ms))
+ return fault(bus, DS2480_IO);
+ bus->data_mode = 1;
}
- *received = result;
+ if (bus->port.write(bus->port.user, &value, 1, bus->timeout_ms))
+ return fault(bus, DS2480_IO);
+ if (value == 0xE3 && bus->port.write(bus->port.user, &value, 1, bus->timeout_ms))
+ return fault(bus, DS2480_IO);
+ if (bus->port.read(bus->port.user, &reply, 1, bus->timeout_ms))
+ return fault(bus, DS2480_IO);
+ *received = reply;
return DS2480_OK;
}
diff --git a/c/ds18b20-ds2480/ds2480.h b/c/ds18b20-ds2480/ds2480.h
index a17d82f..e4c2e97 100644
--- a/c/ds18b20-ds2480/ds2480.h
+++ b/c/ds18b20-ds2480/ds2480.h
@@ -34,6 +34,7 @@ typedef struct {
uint32_t timeout_ms;
uint8_t ready;
uint8_t power_active;
+ uint8_t data_mode;
} ds2480;
/** Independent ROM search cursor. Zero-initialize before each enumeration. */
@@ -51,7 +52,7 @@ ds2480_status ds2480_init(ds2480 *bus, const ds2480_port *port, uint32_t timeout
ds2480_status ds2480_reset(ds2480 *bus);
/** Exchange one slot (write 1 to read); received must be non-NULL. */
ds2480_status ds2480_bit(ds2480 *bus, uint8_t bit, uint8_t *received);
-/** Exchange a byte, LSB first, in command mode (eight UART transactions). */
+/** Exchange a byte in Data Mode; escapes E3 and preserves one reply per byte. */
ds2480_status ds2480_byte(ds2480 *bus, uint8_t value, uint8_t *received);
/** Write a byte and start strong pullup immediately after its last slot.
* Blocks for hold_ms (1..1000), then terminates the pulse and consumes reply.
diff --git a/c/ds18b20-ds2480/ports/stm32f4/tests/stm32f4xx_hal.h b/c/ds18b20-ds2480/ports/stm32f4/tests/stm32f4xx_hal.h
new file mode 100644
index 0000000..fa40fc6
--- /dev/null
+++ b/c/ds18b20-ds2480/ports/stm32f4/tests/stm32f4xx_hal.h
@@ -0,0 +1,39 @@
+/* Host-only HAL model, never add this directory to a firmware include path. */
+#ifndef TEST_STM32F4_HAL_H
+#define TEST_STM32F4_HAL_H
+#include
+typedef struct { uint32_t SR, DR, CR1; } USART_TypeDef;
+typedef struct { uint32_t pin, level, mode; } GPIO_TypeDef;
+typedef struct { uint32_t BaudRate,WordLength,StopBits,Parity,Mode,HwFlowCtl,OverSampling; } UART_InitTypeDef;
+typedef struct { USART_TypeDef *Instance; UART_InitTypeDef Init; } UART_HandleTypeDef;
+typedef struct { uint32_t Pin, Mode, Pull, Speed, Alternate; } GPIO_InitTypeDef;
+typedef enum { HAL_OK, HAL_ERROR } HAL_StatusTypeDef;
+#define USART_SR_ORE 8U
+#define USART_SR_NE 4U
+#define USART_SR_FE 2U
+#define USART_SR_PE 1U
+#define USART_SR_RXNE 32U
+#define GPIO_PIN_RESET 0U
+#define GPIO_PIN_SET 1U
+#define GPIO_MODE_OUTPUT_PP 1U
+#define GPIO_MODE_AF_PP 2U
+#define GPIO_NOPULL 0U
+#define GPIO_SPEED_FREQ_HIGH 3U
+#define UART_WORDLENGTH_8B 0U
+#define UART_STOPBITS_1 0U
+#define UART_PARITY_NONE 0U
+#define UART_MODE_TX_RX 12U
+#define UART_HWCONTROL_NONE 0U
+#define UART_OVERSAMPLING_16 0U
+#define HAL_MAX_DELAY UINT32_MAX
+#define __HAL_UART_DISABLE(u) ((u)->Instance->CR1=0)
+void test_clear(UART_HandleTypeDef *u);
+#define __HAL_UART_CLEAR_OREFLAG(u) test_clear(u)
+HAL_StatusTypeDef HAL_UART_Abort(UART_HandleTypeDef *u);
+HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *u);
+HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *u,const uint8_t *p,uint16_t n,uint32_t timeout);
+void HAL_GPIO_WritePin(GPIO_TypeDef *g,uint16_t pin,uint32_t level);
+void HAL_GPIO_Init(GPIO_TypeDef *g,const GPIO_InitTypeDef *i);
+void HAL_Delay(uint32_t ms);
+uint32_t HAL_GetTick(void);
+#endif
diff --git a/c/ds18b20-ds2480/ports/stm32f4/tests/test_port.c b/c/ds18b20-ds2480/ports/stm32f4/tests/test_port.c
new file mode 100644
index 0000000..b6562b0
--- /dev/null
+++ b/c/ds18b20-ds2480/ports/stm32f4/tests/test_port.c
@@ -0,0 +1,41 @@
+#include "ds2480_stm32f4_hal.h"
+#include
+#include
+
+static uint32_t now, low_ms, high_ms, clears;
+static GPIO_TypeDef gpio;
+static unsigned fail_init, fail_tx;
+void test_clear(UART_HandleTypeDef *u){u->Instance->SR=0;++clears;}
+HAL_StatusTypeDef HAL_UART_Abort(UART_HandleTypeDef *u){(void)u;return HAL_OK;}
+HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *u)
+{assert(gpio.mode==GPIO_MODE_AF_PP && gpio.level==1);assert(u->Init.BaudRate==9600);return fail_init?HAL_ERROR:HAL_OK;}
+HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *u,const uint8_t *p,uint16_t n,uint32_t timeout)
+{assert(n==1 && timeout==20);u->Instance->DR=*p;u->Instance->SR=USART_SR_RXNE;return fail_tx?HAL_ERROR:HAL_OK;}
+void HAL_GPIO_WritePin(GPIO_TypeDef *g,uint16_t pin,uint32_t level){g->pin=pin;g->level=level;}
+void HAL_GPIO_Init(GPIO_TypeDef *g,const GPIO_InitTypeDef *i){assert(i->Pin==64);g->mode=i->Mode;}
+void HAL_Delay(uint32_t ms){now+=ms;if(gpio.level)high_ms+=ms;else low_ms+=ms;}
+uint32_t HAL_GetTick(void){return now++;}
+
+int main(void)
+{
+ USART_TypeDef regs={0};UART_HandleTypeDef uart={0};
+ ds2480_stm32f4_hal ctx={&uart,&gpio,64,8};ds2480_port port;
+ uint8_t byte=0xCD,rx=0;
+ uart.Instance=®s;
+ assert(ds2480_stm32f4_hal_bind(&ctx,&port)==DS2480_OK);
+ assert(port.prepare(port.user)==0 && low_ms>=2 && high_ms>=2 && clears==1);
+ assert(port.write(port.user,&byte,1,20)==0);
+ assert(regs.SR & USART_SR_RXNE); /* Early reply must survive TX completion. */
+ assert(port.read(port.user,&rx,1,20)==0 && rx==byte && clears==1);
+ regs.SR=USART_SR_RXNE|USART_SR_FE;rx=0xA5;
+ assert(port.read(port.user,&rx,1,20)!=0 && rx==0xA5 && clears==2);
+ regs.SR=USART_SR_ORE;assert(port.write(port.user,&byte,1,20)!=0);
+ regs.SR=0;now=UINT32_MAX-5;
+ assert(port.read(port.user,&rx,1,20)!=0); /* Wrap-safe bounded timeout. */
+ assert(port.write(port.user,&byte,2,20)!=0);
+ assert(port.read(port.user,&rx,1,HAL_MAX_DELAY)!=0);
+ fail_tx=1;assert(port.write(port.user,&byte,1,20)!=0);
+ fail_init=1;assert(port.prepare(port.user)!=0 && gpio.level==1);
+ ctx.tx_pin=3;assert(ds2480_stm32f4_hal_bind(&ctx,&port)==DS2480_ARGUMENT);
+ puts("STM32F4 UART port: OK");return 0;
+}
diff --git a/c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c b/c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
index 5c1777b..efc5eec 100644
--- a/c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
+++ b/c/ds18b20-ds2480/tests/test_ds18b20_ds2480.c
@@ -12,6 +12,7 @@ typedef struct {
unsigned prepared, calibrated, pending, reply, pulse, hold, convert, copy;
unsigned fail_read, fail_write, fail_prepare, corrupt_reply, reset_reply;
unsigned fail_power, fail_stop, reject_config;
+ unsigned data_mode, escape;
} fake;
static void crc_scratch(fake *f) { f->scratch[8] = ds2480_crc8(f->scratch, 8); }
@@ -91,6 +92,7 @@ static int prepare(void *user)
fake *f = user;
++f->prepared;
f->pending = f->pulse = f->calibrated = 0;
+ f->data_mode = f->escape = 0;
return (int)f->fail_prepare;
}
@@ -101,6 +103,17 @@ static int transmit(void *user, const uint8_t *data, uint32_t size, uint32_t tim
CHECK(size == 1 && timeout == 20 && !f->pending);
if (f->fail_write) return -1;
if (!f->calibrated) { CHECK(command == 0xC1); f->calibrated = 1; return 0; }
+ if (f->data_mode) {
+ unsigned i, reply = 0;
+ if (command == 0xE3 && !f->escape) { f->escape=1; return 0; }
+ if (!f->escape || command == 0xE3) {
+ f->escape=0;
+ for (i=0; i<8; ++i) reply |= (unsigned)wire_bit(f,(uint8_t)((command>>i)&1)) << i;
+ f->reply=reply; f->pending=1; return 0;
+ }
+ f->escape=0; f->data_mode=0;
+ }
+ if (command == 0xE1) { f->data_mode=1; return 0; }
CHECK(!f->pulse || command == 0xF1);
if (command == 0x3F) f->reply = 0x3E;
else if (command == 0xC1) {
@@ -188,6 +201,8 @@ int main(void)
CHECK(raw == (i == 9 ? -168 : i == 10 ? -164 : i == 11 ? -162 : -161));
}
CHECK(f.copy == 4);
+ CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], (int8_t)-29, 2, 12, 0) == DS2480_OK);
+ CHECK(f.scratch[2] == 0xE3); /* Escaped byte followed by an ordinary byte. */
f.reject_config = 1;
CHECK(ds18b20_ds2480_configure(&bus, f.rom[0], 1, 2, 9, 1) == DS2480_DATA && f.copy == 4);
f.scratch[8] ^= 1; raw = 999;
From 5260186c166347a866c4baaff8e867b095c924ec Mon Sep 17 00:00:00 2001
From: Andrey Kruchinkin
Date: Sat, 19 Sep 2026 07:12:09 +0300
Subject: [PATCH 36/37] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?=
=?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D1=82=D0=BE=D0=BA=D0=BE=D0=BB?=
=?UTF-8?q?=20Altera=20Logic=20=D0=B8=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B5=20?=
=?UTF-8?q?=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D1=8B=20=D0=BF=D1=80=D0=BE?=
=?UTF-8?q?=D1=88=D0=B8=D0=B2=D0=BA=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 3 +
c/firmware-info/PORTING.md | 151 +++++++++++-
c/firmware-info/README.md | 4 +
c/set-protocol/ALTERA_LOGIC.md | 93 ++++++++
c/set-protocol/CMakeLists.txt | 8 +
c/set-protocol/include/altera_logic.h | 35 +++
c/set-protocol/include/altera_stream.h | 46 ++++
c/set-protocol/src/altera_logic.c | 142 ++++++++++++
c/set-protocol/src/altera_stream.c | 158 +++++++++++++
c/set-protocol/tests/test_altera_logic.c | 72 ++++++
c/set-protocol/tests/test_altera_stream.c | 62 +++++
c/set-protocol/tools/build_host.py | 2 +
doc/setcan/ALTERA_LOGIC_STREAM.md | 189 +++++++++++++++
doc/setcan/DEVICE_REGISTRY.md | 21 ++
doc/setcan/README.md | 4 +
python/altera_logic/.gitignore | 1 +
python/altera_logic/__init__.py | 4 +
python/altera_logic/markers.py | 89 +++++++
python/altera_logic/native.py | 131 +++++++++++
python/altera_logic/plot_scroll.py | 27 +++
python/altera_logic/qt_port.py | 114 +++++++++
python/altera_logic/stream.py | 108 +++++++++
python/altera_logic/stream_port.py | 103 +++++++++
python/protocan/__init__.py | 5 +
python/protocan/can_boot.py | 123 ++++++++++
python/protocan/protocan.py | 2 +
python/protocan/setp_firmware.py | 239 +++++++++++++++++++
python/protocan/stm32_boot.py | 47 ++++
python/protocan/tms_firmware.py | 118 ++++++++++
python/setprotocol/firmware_database.py | 268 ++++++++++++++++++++++
python/tests/test_altera_logic.py | 57 +++++
python/tests/test_altera_stream.py | 92 ++++++++
python/tests/test_firmware_clients.py | 135 +++++++++++
python/tests/test_firmware_database.py | 192 ++++++++++++++++
tools/firmware-publish/DATABASE.md | 178 ++++++++++++++
tools/firmware-publish/PORTING.md | 164 +++++++++++++
tools/firmware-publish/README.md | 12 +-
tools/firmware-publish/firmware_db.py | 82 +++++++
38 files changed, 3271 insertions(+), 10 deletions(-)
create mode 100644 c/set-protocol/ALTERA_LOGIC.md
create mode 100644 c/set-protocol/include/altera_logic.h
create mode 100644 c/set-protocol/include/altera_stream.h
create mode 100644 c/set-protocol/src/altera_logic.c
create mode 100644 c/set-protocol/src/altera_stream.c
create mode 100644 c/set-protocol/tests/test_altera_logic.c
create mode 100644 c/set-protocol/tests/test_altera_stream.c
create mode 100644 doc/setcan/ALTERA_LOGIC_STREAM.md
create mode 100644 doc/setcan/DEVICE_REGISTRY.md
create mode 100644 python/altera_logic/.gitignore
create mode 100644 python/altera_logic/__init__.py
create mode 100644 python/altera_logic/markers.py
create mode 100644 python/altera_logic/native.py
create mode 100644 python/altera_logic/plot_scroll.py
create mode 100644 python/altera_logic/qt_port.py
create mode 100644 python/altera_logic/stream.py
create mode 100644 python/altera_logic/stream_port.py
create mode 100644 python/protocan/can_boot.py
create mode 100644 python/protocan/setp_firmware.py
create mode 100644 python/protocan/stm32_boot.py
create mode 100644 python/protocan/tms_firmware.py
create mode 100644 python/setprotocol/firmware_database.py
create mode 100644 python/tests/test_altera_logic.py
create mode 100644 python/tests/test_altera_stream.py
create mode 100644 python/tests/test_firmware_clients.py
create mode 100644 python/tests/test_firmware_database.py
create mode 100644 tools/firmware-publish/DATABASE.md
create mode 100644 tools/firmware-publish/PORTING.md
create mode 100644 tools/firmware-publish/firmware_db.py
diff --git a/README.md b/README.md
index e8a609c..963e27e 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,9 @@ templates/
| Модуль | Что делает | Зависимости |
|---|---|---|
+| [`python/setprotocol/firmware_database.py`](python/setprotocol/firmware_database.py) | база прошивок: HTTPS-каталог, скачивание с SHA-256 и кэшем, публикация в Gitea без SETGUI; [подключение и CLI](tools/firmware-publish/DATABASE.md) | stdlib, Python 3.10+ |
+| [`python/setprotocol/firmware_publish.py`](python/setprotocol/firmware_publish.py) | общие метаданные публикации, SHA-256, release tag и обновление каталога прошивок; [подключение](tools/firmware-publish/PORTING.md) | stdlib; сетевой адаптер в SETGUI |
+| [`python/setprotocol/firmware_catalog.py`](python/setprotocol/firmware_catalog.py) | модель и parser каталога `firmware.releases` | stdlib |
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
| [`python/protocan/trends.py`](python/protocan/trends.py) | общие настройки графиков, ограниченная история, ctypes-декодер GAS/raw CAN | stdlib, опционально SETProtocol DLL/SO |
diff --git a/c/firmware-info/PORTING.md b/c/firmware-info/PORTING.md
index addc92e..b112be8 100644
--- a/c/firmware-info/PORTING.md
+++ b/c/firmware-info/PORTING.md
@@ -1,11 +1,144 @@
-# PORTING
+# Портирование firmware-info
-Аппаратно-зависимый контракт ограничен `firmware_info_config.h`:
-`FIRMWARE_VERSION_MAJOR`, `FIRMWARE_VERSION_MINOR`, `FIRMWARE_VERSION_PATCH` и
-необязательный `FIRMWARE_BUILD_ID`. Выберите порт по семейству МК или создайте
-его копию. Публикация — ответственность транспорта: 12 `uint16_t` для Modbus
-либо 24 little-endian байта для SETGUI.
+## Назначение и границы
-При переносе проверьте: поддержку `__DATE__`/`__TIME__`, наличие generated в
-include path, запуск генератора до компиляции и декодирование build ID как двух
-ASCII-байтов в каждом логическом слове.
+Это C-библиотека описания работающего образа: SemVer, дата/время компиляции,
+8 символов build ID и сериализация контракта v1. Она не обращается к сети,
+не читает версию из сервера и не записывает Flash. HAL, RTOS, UART и CAN
+ядру не нужны. Отправку результата выполняет приложение.
+
+Для размещения `.hex/.bin` в каталоге SETGUI уже существуют
+[`setprotocol.firmware_publish`](../../python/setprotocol/firmware_publish.py)
+и [`tools/firmware-publish`](../../tools/firmware-publish/README.md).
+Связь компонентов и перенос публикации описаны в
+[`tools/firmware-publish/PORTING.md`](../../tools/firmware-publish/PORTING.md).
+
+## 1. Файлы и конфигурация
+
+Пример ниже предполагает `lib/templates` внутри нового проекта:
+
+```text
+project/
+ inc/firmware_info_config.h
+ generated/firmware_build_id.h # создаётся до сборки
+ lib/templates/c/firmware-info/
+ src/
+```
+
+Добавьте в сборку оба файла:
+
+```text
+lib/templates/c/firmware-info/src/firmware_info.c
+lib/templates/c/firmware-info/src/firmware_info_port.c
+```
+
+В include path добавьте `inc`, `generated` и
+`lib/templates/c/firmware-info/include`. Встроенный `CMakeLists.txt` собирает
+только ядро и host-тест; `firmware_info_port.c` и каталоги конфигурации нужно
+добавлять к целевому firmware target самостоятельно.
+
+Скопируйте `ports//firmware_info_config.template.h` как
+`inc/firmware_info_config.h`. Есть варианты STM32F1/F4/G4 и К1921ВК028;
+они не содержат регистров МК. Для другого МК с обычными 8-битными байтами
+достаточно такого же config:
+
+```c
+#ifndef FIRMWARE_INFO_CONFIG_H
+#define FIRMWARE_INFO_CONFIG_H
+#define FIRMWARE_VERSION_MAJOR 1U
+#define FIRMWARE_VERSION_MINOR 2U
+#define FIRMWARE_VERSION_PATCH 3U
+#include "firmware_build_id.h"
+#endif
+```
+
+Обязательный include в этом примере позволяет обнаружить пропущенный pre-build.
+В готовых шаблонах include опциональный через `__has_include`; если заголовок
+не подключён, порт использует `LOCALDEV`. Для компилятора без `__has_include`
+используйте явный include. Можно связать три версии с существующими макросами
+проекта, как это сделано в `KONOR_ds18b20/inc/firmware_info_config.h`.
+
+## 2. Build ID
+
+Из корня нового проекта перед компиляцией запустите:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File "lib/templates/c/firmware-info/tools/make_build_id.ps1" -Repository "." -Output "generated/firmware_build_id.h"
+```
+
+Указывайте `-Repository` и `-Output` явно: расположение сабмодуля и рабочий
+каталог IDE различаются между проектами. `-Repository` должен указывать на
+исходники прошивки, а не на репозиторий `templates`. Для Keil из каталога `mdk`
+соответственно используйте `..\lib\templates\...`, `-Repository ".."` и
+`-Output ".\Generated\firmware_build_id.h"`.
+
+При доступном Git чистый checkout получает 8 знаков commit, изменения tracked
+файлов — 7 знаков и `+`. Если commit определить нельзя, используется `NOGIT000`.
+Новые untracked-файлы текущий генератор при определении dirty не учитывает.
+Дата/время берутся из `__DATE__`/`__TIME__` при компиляции `firmware_info_port.c`;
+для выпуска выполняйте полный Rebuild, чтобы не оставить старый объектный файл.
+
+## 3. Обработчик запроса версии
+
+Пример функции подготовки полезной нагрузки, вызываемой вашим обработчиком:
+
+```c
+#include "firmware_info_port.h"
+
+firmware_info_status_t app_make_firmware_info(uint8_t *payload, size_t capacity)
+{
+ firmware_info_t info;
+ firmware_info_status_t status = firmware_info_port_describe(&info);
+ if (status != FIRMWARE_INFO_OK) return status;
+ return firmware_info_to_le_bytes(&info, payload, capacity);
+}
+```
+
+При успехе передайте ровно `FIRMWARE_INFO_PAYLOAD_SIZE` (24) байта в собственный
+транспорт. При ошибке верните ошибку протокола, а не содержимое буфера.
+Не отправляйте `sizeof(firmware_info_t)`: структура не является wire format.
+
+В KONOR обработчик `app_send_firmware_info()` отвечает на
+`PROTO_MSG_FIRMWARE_INFO = 0x03`, сохраняя sequence запроса. Старый 32-байтовый
+`DEVICE_INFO` остаётся отдельным сообщением. Для нового протокола сначала
+согласуйте команду с клиентом: подключение библиотеки само по себе не добавит
+декодер и отображение версии в GUI.
+
+Для Modbus вызовите `firmware_info_to_registers()` с массивом из 12 `uint16_t`
+и разместите его в выбранной карте регистров. Адреса и Modbus-порядок байтов
+обеспечивает ваш Modbus-стек; LE-буфер в Modbus напрямую не копируйте.
+
+## 4. Контракт v1
+
+| Индекс слова | Содержимое |
+|---|---|
+| 0 | Версия контракта: 1 |
+| 1, 2, 3 | major, minor, patch |
+| 4 | Год |
+| 5 | `(month << 8) \| day` |
+| 6 | `(hour << 8) \| minute` |
+| 7 | Секунды |
+| 8–11 | По два ASCII-символа build ID: первый в старшем байте слова |
+
+В LE-представлении младший байт каждого слова идёт первым. Например, build ID
+`ab` в начале строки даёт слово `0x6162`, но байты `62 61`; декодируйте сначала
+слово, затем символы из старшего/младшего байта. NUL-терминатор не передаётся.
+
+Текущая проверка допускает major/minor до 255 и patch до 999. Если в каталоге
+используется `(major << 16) | (minor << 8) | patch`, ограничьте **все три** части
+до 255, иначе значения пересекутся. Версия каталога автоматически из C-config
+не извлекается.
+
+Для C2000 с 16-битным `char` нельзя считать готовым байтовый порт: отдельно
+проверьте наличие `uint8_t` и представление октетов в транспорте. Публикация
+TMS SCI8-файла с ПК поддерживается независимо от переноса этой C-библиотеки.
+
+## 5. Проверка переноса
+
+1. Соберите host-тест из `tests/test_firmware_info.c` вместе с ядром либо
+ используйте CMake/CTest. Он проверяет дату, регистры и порядок байтов build ID.
+2. Соберите целевую прошивку с обоими `.c` и сгенерированным заголовком.
+3. Запросите версию у устройства: сравните SemVer, build ID, время и 24-байтовый
+ ответ с конкретной сборкой. Проверьте, что прежний `DEVICE_INFO` не изменился.
+4. Подключите выпуск по инструкции публикатора; сравните версию в config и
+ `firmware-release.cmd` перед Rebuild и `--preflight`.
diff --git a/c/firmware-info/README.md b/c/firmware-info/README.md
index 2626f6e..5b4eefe 100644
--- a/c/firmware-info/README.md
+++ b/c/firmware-info/README.md
@@ -25,3 +25,7 @@ config, а не дублирования ядра.
KONOR публикует эти 24 байта ответом `FIRMWARE_INFO (0x03)`, сохраняя старый
32-байтовый `DEVICE_INFO` без изменений.
+
+Подробная инструкция с кодом обработчика, форматом ответа и проверкой переноса:
+[`PORTING.md`](PORTING.md). Для размещения файла прошивки в каталоге SETGUI
+используется отдельный [общий публикатор](../../tools/firmware-publish/PORTING.md).
diff --git a/c/set-protocol/ALTERA_LOGIC.md b/c/set-protocol/ALTERA_LOGIC.md
new file mode 100644
index 0000000..348b41b
--- /dev/null
+++ b/c/set-protocol/ALTERA_LOGIC.md
@@ -0,0 +1,93 @@
+# Altera Logic: общее ядро и порты
+
+Потоковый SETCAN/GAS-профиль и общие порты описаны отдельно:
+[Altera Logic ONLINE](../../doc/setcan/ALTERA_LOGIC_STREAM.md).
+Ниже — совместимый прежний буферный протокол UART `A5 CMD`.
+
+Клиент UART-протокола FPGA-анализатора Cyclone IV: 16 каналов, 4096 выборок.
+Работает с существующим RTL `altera_loganalisator`, версия протокола 1.
+Ядро C99 не зависит от Qt, ОС, системных часов и динамической памяти.
+
+Слои: SETGUI → Python ctypes → C99 → Python Qt-порт → COM / USB–UART.
+GUI передаёт числовые настройки и получает готовый массив; формат пакетов,
+XOR, endian, последовательность команд, парсер и состояния тайм-аутов находятся в C.
+
+| Файл | Назначение и зависимости |
+|---|---|
+| `include/altera_logic.h` | Публичный ABI, макрос экспорта из `pcan_abi.h` |
+| `src/altera_logic.c` | Кодек и автомат обмена; только стандартная библиотека C |
+| `tests/test_altera_logic.c` | Эталонные пакеты RTL, полный захват, ошибки |
+| `../../python/altera_logic/native.py` | ctypes-модели, загрузка DLL, экспорт CSV; stdlib |
+| `../../python/altera_logic/qt_port.py` | Асинхронный COM-порт; PySide6 QtSerialPort |
+
+## Сборка
+
+Из корня `templates`:
+
+```powershell
+python c/set-protocol/tools/build_host.py --output python/altera_logic/native/setprotocol.dll
+```
+
+На Linux выходной файл — `python/altera_logic/native/libsetprotocol.so`.
+Также поддерживается основная CMake-сборка `c/set-protocol`; путь к результату
+задаётся через `ALTERA_LOGIC_LIBRARY`. Библиотека без символов `la_*`
+не поддерживается: тихого Python-fallback нет.
+
+## Контракт порта
+
+Выделить выровненную память размером `la_context_size()`; все обращения к
+одному контексту выполняются в одном потоке.
+
+- `la_init(ctx)` — начать с INFO.
+- `la_next(ctx, out, capacity)` — получить следующий запрос, либо 0, если отправлять нечего.
+- `la_feed(ctx, bytes, size)` — передать принятые байты, включая частичные ответы.
+- `la_tick(ctx, elapsed_ms)` — сообщить прошедшее время; тайм-аут ответа 1000 мс.
+- `la_start(ctx, divider, mask, value, edge_mask, edge_value)` — настройка и новый захват.
+- `la_get(ctx, field)` — состояние и прогресс; `la_samples` — завершённая запись.
+- `la_fail(ctx, LA_IO_ERROR)` — сообщить ошибку транспорта.
+
+COM: 921600 бод, 8N1, без управления потоком. Одновременно ожидается только
+один ответ. После ARM STATUS опрашивается каждые 25 мс; ожидание самого
+триггера не ограничено. После done читаются 64 блока по 64 выборки.
+Ошибки закрывают сеанс без автоматического повтора ARM. Неверный размер,
+код команды или XOR прекращает сеанс. При рассинхронизации запроса на FPGA
+может потребоваться аппаратный RESET_N; переподключение не гарантирует её сброс.
+
+Клиент намеренно принимает только профиль 16 × 4096 версии 1.
+Поддержка внешней SDRAM и расширенных адресов сюда пока не входит.
+Прерывание в GUI закрывает порт ПК: STOP в протоколе FPGA отсутствует.
+
+## Быстрый старт без платы
+
+Добавить `templates/python` в PYTHONPATH:
+
+```python
+from altera_logic import NativeAnalyzer
+
+client = NativeAnalyzer()
+client.reset(demo=True)
+client.start(divider=49)
+capture = client.capture()
+print(len(capture.samples)) # 4096
+print(capture.sample_rate) # 1_000_000
+print(capture.trigger_index) # 2048
+capture.save_csv("demo.csv")
+```
+
+Демо создаёт синтетическую запись в C; не моделирует заданные условия триггера.
+CSV содержит явный признак demo, время относительно триггера и состояния D0…D15.
+
+## Потребители и проверка
+
+Используется вкладкой SETGUI «Altera Logic». Для Android/JNI новый потребитель
+может использовать тот же ABI; интерфейс Android в этой задаче не изменяется.
+Существующий ABI SETProtocol не изменён, добавлены только символы `la_*`.
+
+```powershell
+$env:PYTHONPATH = "$PWD/python"
+python -m unittest discover -s python/tests -p test_altera_logic.py
+```
+
+CMake включает `test_altera_logic` и CTest `altera_logic`.
+На плате требуется отдельно проверить разводку пинов, RAM-модуль и прошивку;
+программные проверки не заменяют аппаратную проверку.
diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt
index bdc5b96..5165838 100644
--- a/c/set-protocol/CMakeLists.txt
+++ b/c/set-protocol/CMakeLists.txt
@@ -6,6 +6,8 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
# SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
set(SETPROTOCOL_V2_SOURCES
+ src/altera_logic.c
+ src/altera_stream.c
src/set_protocol.c
src/set_can.c
src/set_firmware.c
@@ -79,6 +81,12 @@ endif()
option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS)
enable_testing()
+ add_executable(test_altera_logic tests/test_altera_logic.c)
+ target_link_libraries(test_altera_logic PRIVATE setprotocol_static)
+ add_test(NAME altera_logic COMMAND test_altera_logic)
+ add_executable(test_altera_stream tests/test_altera_stream.c)
+ target_link_libraries(test_altera_stream PRIVATE setprotocol_static)
+ add_test(NAME altera_stream COMMAND test_altera_stream)
add_executable(test_plot tests/test_plot.c)
target_link_libraries(test_plot PRIVATE setprotocol_static)
diff --git a/c/set-protocol/include/altera_logic.h b/c/set-protocol/include/altera_logic.h
new file mode 100644
index 0000000..00136e2
--- /dev/null
+++ b/c/set-protocol/include/altera_logic.h
@@ -0,0 +1,35 @@
+/** Altera logic analyzer UART client. C99, caller-owned memory, no OS/heap.
+ * One request at a time; no automatic retries (ARM is not idempotent).
+ * Initialize aligned storage of la_context_size() bytes, then call la_next,
+ * la_feed and la_tick from one thread. tick receives elapsed milliseconds.
+ */
+#ifndef ALTERA_LOGIC_H
+#define ALTERA_LOGIC_H
+#include "pcan_abi.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+enum { LA_CONNECTING, LA_READY, LA_CAPTURING, LA_DONE, LA_ERROR };
+enum { LA_OK, LA_BAD_FRAME, LA_CHECKSUM, LA_DEVICE_ERROR, LA_TIMEOUT,
+ LA_UNSUPPORTED, LA_ARGUMENT, LA_IO_ERROR };
+enum { LA_STATE, LA_ERROR_CODE, LA_CHANNELS, LA_DEPTH, LA_CLOCK_HZ,
+ LA_FLAGS, LA_TRIGGER_INDEX, LA_READ_COUNT, LA_DIVIDER };
+PCAN_ABI_API size_t la_context_size(void);
+PCAN_ABI_API void la_init(void *ctx);
+/** Return 0 on success. Configuration is immutable throughout this capture. */
+PCAN_ABI_API int la_start(void *ctx, uint32_t divider, uint32_t mask,
+ uint32_t value, uint32_t edge_mask, uint32_t edge_value);
+/** Returns 6 when a request is ready, otherwise 0. Capacity must be >=6. */
+PCAN_ABI_API size_t la_next(void *ctx, uint8_t *out, size_t capacity);
+PCAN_ABI_API void la_feed(void *ctx, const uint8_t *data, size_t size);
+PCAN_ABI_API void la_tick(void *ctx, uint32_t elapsed_ms);
+PCAN_ABI_API void la_fail(void *ctx, uint32_t code);
+PCAN_ABI_API uint32_t la_get(const void *ctx, uint32_t field);
+PCAN_ABI_API size_t la_samples(const void *ctx, uint16_t *out, size_t capacity);
+/** Offline fixture, never communicates with hardware or claims a real trigger. */
+PCAN_ABI_API void la_demo_init(void *ctx);
+PCAN_ABI_API int la_demo_capture(void *ctx, uint32_t divider);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/set-protocol/include/altera_stream.h b/c/set-protocol/include/altera_stream.h
new file mode 100644
index 0000000..18c5d53
--- /dev/null
+++ b/c/set-protocol/include/altera_stream.h
@@ -0,0 +1,46 @@
+/** SETCAN/GAS Altera Logic online stream, shared by CAN and UART.
+ * Caller-owned context; one serialized caller. No heap, OS or Qt dependencies.
+ */
+#ifndef ALTERA_STREAM_H
+#define ALTERA_STREAM_H
+#include "pcan_abi.h"
+#include "pcan_frame.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+#define LAS_DEVICE_TYPE 6U
+#define LAS_DEVICE_ID 14U
+#define LAS_GAS_META 0xFE00U
+#define LAS_GAS_DATA 0xFF00U
+#define LAS_CAPACITY 8192U
+#define LAS_TAG 0x4C01U
+enum { LAS_COUNT, LAS_PERIOD_NS, LAS_SESSION, LAS_RECEIVED, LAS_MISSING,
+ LAS_DUPLICATES, LAS_INVALID, LAS_IGNORED, LAS_CRC_ERRORS, LAS_HAS_META };
+PCAN_ABI_API size_t las_context_size(void);
+PCAN_ABI_API int las_init(void *ctx, uint32_t device_id);
+PCAN_ABI_API void las_can(void *ctx, uint32_t id, const uint8_t *data, size_t size,
+ uint32_t extended, uint32_t remote);
+PCAN_ABI_API void las_uart(void *ctx, const uint8_t *data, size_t size);
+PCAN_ABI_API uint32_t las_get(const void *ctx, uint32_t field);
+/** Chronological bounded history. breaks[i]=1 marks a discontinuity before i. */
+PCAN_ABI_API size_t las_snapshot(const void *ctx, uint64_t *indices, uint16_t *samples,
+ uint8_t *breaks, size_t capacity);
+/** Sparse step trace for rendering; capacity >= 2*LAS_COUNT, move=pen-up. */
+PCAN_ABI_API size_t las_trace(const void *ctx, uint32_t channel, uint64_t *indices,
+ uint8_t *levels, uint8_t *moves, size_t capacity);
+PCAN_ABI_API uint32_t las_device_type(void);
+PCAN_ABI_API uint32_t las_device_id(void);
+PCAN_ABI_API const char *las_device_name(void);
+PCAN_ABI_API void las_demo_step(void *ctx, uint32_t count);
+/** Device-side packet builders; return zero for invalid arguments.
+ * uart=0: 8-byte CAN data + *id; uart=1: complete AA55 transport + *id.
+ * Same builders are used by firmware ports, tests and the demo producer.
+ */
+PCAN_ABI_API size_t las_metadata(uint32_t device, uint32_t session, uint32_t period_ns,
+ uint32_t uart, uint32_t *id, uint8_t *out, size_t capacity);
+PCAN_ABI_API size_t las_data(uint32_t device, uint32_t session, uint32_t index,
+ uint32_t sample, uint32_t uart, uint32_t *id, uint8_t *out, size_t capacity);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/set-protocol/src/altera_logic.c b/c/set-protocol/src/altera_logic.c
new file mode 100644
index 0000000..2c97957
--- /dev/null
+++ b/c/set-protocol/src/altera_logic.c
@@ -0,0 +1,142 @@
+#include "altera_logic.h"
+#include
+typedef struct {
+ uint32_t state, error, clock_hz, elapsed, delay;
+ uint16_t config[5], channels, depth, trigger, count, offset;
+ uint16_t samples[4096];
+ uint8_t command, pending, flags, requested, rx[133];
+ size_t used;
+} la_context;
+static uint8_t checksum(const uint8_t *p, size_t n) {
+ uint8_t x = 0; size_t i;
+ for (i = 0; i < n; ++i) x ^= p[i];
+ return x;
+}
+static uint16_t u16(const uint8_t *p) {
+ return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
+}
+size_t la_context_size(void) { return sizeof(la_context); }
+void la_init(void *ctx) {
+ la_context *s = (la_context *)ctx;
+ memset(s, 0, sizeof(*s)); s->command = 1; s->state = LA_CONNECTING;
+}
+void la_fail(void *ctx, uint32_t code) {
+ la_context *s = (la_context *)ctx;
+ s->state = LA_ERROR; s->error = code; s->pending = 0; s->command = 0;
+}
+int la_start(void *ctx, uint32_t div, uint32_t mask, uint32_t val,
+ uint32_t emask, uint32_t eval) {
+ la_context *s = (la_context *)ctx;
+ if ((s->state != LA_READY && s->state != LA_DONE) ||
+ div > 65535 || mask > 65535 || val > 65535 ||
+ emask > 65535 || eval > 65535 || ((val ^ eval) & mask & emask))
+ return LA_ARGUMENT;
+ s->config[0]=(uint16_t)div; s->config[1]=(uint16_t)mask;
+ s->config[2]=(uint16_t)val; s->config[3]=(uint16_t)emask;
+ s->config[4]=(uint16_t)eval;
+ s->state=LA_CAPTURING; s->error=0; s->flags=0; s->count=0;
+ s->offset=0; s->command=2; s->delay=0;
+ return LA_OK;
+}
+size_t la_next(void *ctx, uint8_t *out, size_t capacity) {
+ la_context *s = (la_context *)ctx; uint16_t arg = 0;
+ if (!out || capacity < 6 || !s->command || s->pending || s->delay) return 0;
+ if (s->command >= 2 && s->command <= 6) arg=s->config[s->command-2];
+ if (s->command == 9) {
+ arg=s->offset;
+ s->requested=(uint8_t)((s->depth-s->offset > 64) ? 64 : s->depth-s->offset);
+ }
+ out[0]=0xa5; out[1]=s->command; out[2]=(uint8_t)arg;
+ out[3]=(uint8_t)(arg >> 8); out[4]=(s->command == 9) ? s->requested : 0;
+ out[5]=checksum(out,5);
+ s->pending=1; s->elapsed=0; s->used=0;
+ return 6;
+}
+static void complete(la_context *s) {
+ size_t i;
+ if (checksum(s->rx,s->used)) { la_fail(s,LA_CHECKSUM); return; }
+ if (s->rx[2]) { la_fail(s,LA_DEVICE_ERROR); return; }
+ s->pending=0;
+ if (s->command == 1) {
+ s->channels=s->rx[3]; s->depth=u16(s->rx+4);
+ s->clock_hz=(uint32_t)s->rx[6]*1000000U;
+ if (s->channels != 16 || s->depth != 4096 || !s->clock_hz || s->rx[7]!=1) {
+ la_fail(s,LA_UNSUPPORTED); return;
+ }
+ s->state=LA_READY; s->command=0;
+ } else if (s->command < 7) ++s->command;
+ else if (s->command == 7) { s->command=8; s->delay=25; }
+ else if (s->command == 8) {
+ s->flags=s->rx[3]; s->trigger=u16(s->rx+4);
+ if (s->flags & 4) {
+ if (s->trigger >= s->depth || (s->flags & 1)) { la_fail(s,LA_BAD_FRAME); return; }
+ s->command=9;
+ } else s->delay=25;
+ } else {
+ for(i=0; irequested; ++i) s->samples[s->offset+i]=u16(s->rx+4+i*2);
+ s->offset=(uint16_t)(s->offset+s->requested); s->count=s->offset;
+ if(s->offset == s->depth) { s->state=LA_DONE; s->command=0; }
+ }
+}
+void la_feed(void *ctx, const uint8_t *data, size_t size) {
+ la_context *s = (la_context *)ctx; size_t i, expected;
+ if (!data) return;
+ for(i=0; ipending || s->used >= sizeof(s->rx)) { la_fail(s,LA_BAD_FRAME); return; }
+ s->rx[s->used++]=data[i];
+ if(s->rx[0]!=0x5a || (s->used>=2 && s->rx[1]!=(s->command|0x80))) {
+ la_fail(s,LA_BAD_FRAME); return;
+ }
+ if(s->used<3) continue;
+ expected=4;
+ if(!s->rx[2]) {
+ if(s->command==1) expected=9;
+ else if(s->command==8) expected=7;
+ else if(s->command==9) {
+ if(s->used<4) continue;
+ if(s->rx[3]!=s->requested) { la_fail(s,LA_BAD_FRAME); return; }
+ expected=5U+2U*s->requested;
+ }
+ }
+ if(s->used==expected) complete(s);
+ }
+}
+void la_tick(void *ctx, uint32_t ms) {
+ la_context *s=(la_context *)ctx;
+ if(s->pending) {
+ if(ms >= 1000U-s->elapsed) la_fail(s,LA_TIMEOUT);
+ else s->elapsed+=ms;
+ }
+ s->delay=(ms >= s->delay) ? 0 : s->delay-ms;
+}
+uint32_t la_get(const void *ctx, uint32_t field) {
+ const la_context *s=(const la_context *)ctx;
+ switch(field) {
+ case LA_STATE:return s->state; case LA_ERROR_CODE:return s->error;
+ case LA_CHANNELS:return s->channels; case LA_DEPTH:return s->depth;
+ case LA_CLOCK_HZ:return s->clock_hz; case LA_FLAGS:return s->flags;
+ case LA_TRIGGER_INDEX:return s->trigger; case LA_READ_COUNT:return s->count;
+ case LA_DIVIDER:return s->config[0]; default:return 0;
+ }
+}
+size_t la_samples(const void *ctx, uint16_t *out, size_t capacity) {
+ const la_context *s=(const la_context *)ctx;
+ if(s->state!=LA_DONE || !out || capacitycount) return 0;
+ memcpy(out,s->samples,s->count*sizeof(uint16_t)); return s->count;
+}
+void la_demo_init(void *ctx) {
+ la_context *s=(la_context *)ctx; la_init(s);
+ s->channels=16; s->depth=4096; s->clock_hz=50000000;
+ s->state=LA_READY; s->command=0;
+}
+int la_demo_capture(void *ctx, uint32_t divider) {
+ la_context *s=(la_context *)ctx; size_t i, bit;
+ int rc=la_start(s,divider,0,0,0,0); if(rc) return rc;
+ for(i=0;i<4096;++i) {
+ uint16_t v=0;
+ for(bit=0;bit<16;++bit) if(((i/(4U+bit*7U))&1U)!=0) v|=(uint16_t)(1U<samples[i]=v;
+ }
+ s->trigger=2048; s->flags=6; s->count=4096; s->command=0; s->state=LA_DONE;
+ return LA_OK;
+}
diff --git a/c/set-protocol/src/altera_stream.c b/c/set-protocol/src/altera_stream.c
new file mode 100644
index 0000000..a99d8a9
--- /dev/null
+++ b/c/set-protocol/src/altera_stream.c
@@ -0,0 +1,158 @@
+#include "altera_stream.h"
+#include "pcan_id.h"
+#include
+
+typedef struct {
+ pcan_parser_t parser;
+ uint64_t indices[LAS_CAPACITY], next_index;
+ uint16_t samples[LAS_CAPACITY];
+ uint8_t breaks[LAS_CAPACITY];
+ uint32_t head, count, period, session, received, missing, duplicates, invalid, ignored;
+ uint8_t device, metadata, started;
+} las_context;
+static uint16_t r16(const uint8_t *p) { return (uint16_t)(p[0]|((uint16_t)p[1]<<8)); }
+static uint32_t r32(const uint8_t *p) { return (uint32_t)r16(p)|((uint32_t)r16(p+2)<<16); }
+static void w16(uint8_t *p,uint16_t v) { p[0]=(uint8_t)v;p[1]=(uint8_t)(v>>8); }
+static void w32(uint8_t *p,uint32_t v) { w16(p,(uint16_t)v);w16(p+2,(uint16_t)(v>>16)); }
+uint32_t las_device_type(void) { return LAS_DEVICE_TYPE; }
+uint32_t las_device_id(void) { return LAS_DEVICE_ID; }
+const char *las_device_name(void) { return "Altera Logic"; }
+size_t las_context_size(void) { return sizeof(las_context); }
+int las_init(void *ctx,uint32_t device) {
+ las_context *s=(las_context *)ctx;
+ if(!ctx || device>15) return 0;
+ memset(s,0,sizeof(*s));s->device=(uint8_t)device;pcan_parser_init(&s->parser);return 1;
+}
+static void append(las_context *s,uint64_t index,uint16_t value,uint8_t gap) {
+ s->indices[s->head]=index;s->samples[s->head]=value;s->breaks[s->head]=gap;
+ s->head=(s->head+1U)%LAS_CAPACITY;
+ if(s->countcount;
+}
+void las_can(void *ctx,uint32_t raw,const uint8_t *p,size_t n,uint32_t extended,uint32_t remote) {
+ las_context *s=(las_context *)ctx;pcan_id_t id;uint32_t index,delta,period,session;
+ if(!extended || remote || raw>PCAN_ID_MASK) { ++s->ignored;return; }
+ pcan_id_unpack(raw,&id);
+ if(id.device_type!=LAS_DEVICE_TYPE || id.device_id!=s->device ||
+ id.route!=PCAN_ROUTE_FROM_DEVICE || id.msg_type!=PCAN_MSG_GAS ||
+ (id.msg_body!=LAS_GAS_META && id.msg_body!=LAS_GAS_DATA)) { ++s->ignored;return; }
+ if(n!=8 || !p) { ++s->invalid;return; }
+ if(id.msg_body==LAS_GAS_META) {
+ period=r32(p+4);session=r16(p+2);
+ if(r16(p)!=LAS_TAG || period==0) { ++s->invalid;return; }
+ if(s->metadata && session==s->session) {
+ if(period!=s->period) ++s->invalid;
+ return;
+ }
+ if(s->metadata && (uint16_t)(session-s->session)>=0x8000U) {
+ ++s->duplicates;return;
+ }
+ s->metadata=1;s->session=session;s->period=period;
+ s->count=0;s->head=0;s->started=0;s->next_index=0;
+ return;
+ }
+ if(!s->metadata) { ++s->ignored;return; }
+ if(r16(p)!=s->session) { ++s->ignored;return; }
+ index=r32(p+4);
+ delta=0;
+ if(s->started) {
+ delta=index-(uint32_t)s->next_index;
+ if(delta>=0x80000000U) { ++s->duplicates;return; }
+ s->missing+=delta;
+ } else s->next_index=index;
+ s->next_index+=delta;
+ append(s,s->next_index,r16(p+2),(uint8_t)(!s->started || delta!=0));
+ s->next_index+=1;s->started=1;++s->received;
+}
+static void frame_callback(const pcan_frame_t *frame,void *user) {
+ if(frame->flags & (PCAN_FLAG_DIR|PCAN_FLAG_ERR)) {
+ ++((las_context *)user)->ignored;return;
+ }
+ las_can(user,frame->id,frame->data,frame->dlc,
+ frame->flags&PCAN_FLAG_IDE,frame->flags&PCAN_FLAG_RTR);
+}
+void las_uart(void *ctx,const uint8_t *data,size_t size) {
+ las_context *s=(las_context *)ctx;
+ if(data) pcan_parser_feed(&s->parser,data,size,frame_callback,s);
+}
+uint32_t las_get(const void *ctx,uint32_t field) {
+ const las_context *s=(const las_context *)ctx;
+ switch(field) {
+ case LAS_COUNT:return s->count;case LAS_PERIOD_NS:return s->period;
+ case LAS_SESSION:return s->session;case LAS_RECEIVED:return s->received;
+ case LAS_MISSING:return s->missing;case LAS_DUPLICATES:return s->duplicates;
+ case LAS_INVALID:return s->invalid;case LAS_IGNORED:return s->ignored;
+ case LAS_CRC_ERRORS:return s->parser.stats.crc_errors;
+ case LAS_HAS_META:return s->metadata;default:return 0;
+ }
+}
+size_t las_snapshot(const void *ctx,uint64_t *indices,uint16_t *samples,uint8_t *breaks,size_t cap) {
+ const las_context *s=(const las_context *)ctx;uint32_t i,pos;
+ if(!indices || !samples || !breaks || capcount) return 0;
+ pos=(s->head+LAS_CAPACITY-s->count)%LAS_CAPACITY;
+ for(i=0;icount;++i) {
+ indices[i]=s->indices[pos];samples[i]=s->samples[pos];breaks[i]=s->breaks[pos];
+ pos=(pos+1U)%LAS_CAPACITY;
+ }
+ return s->count;
+}
+size_t las_trace(const void *ctx,uint32_t channel,uint64_t *indices,uint8_t *levels,
+ uint8_t *moves,size_t cap) {
+ const las_context *s=(const las_context *)ctx;uint32_t i,pos;
+ uint64_t previous_x=0;uint8_t previous_y=0;size_t n=0;
+ if(channel>=16 || !indices || !levels || !moves || cap<2U*s->count) return 0;
+ pos=(s->head+LAS_CAPACITY-s->count)%LAS_CAPACITY;
+ for(i=0;icount;++i) {
+ uint64_t x=s->indices[pos];uint8_t y=(uint8_t)((s->samples[pos]>>channel)&1U);
+ if(!i) { indices[n]=x;levels[n]=y;moves[n++]=1; }
+ else if(s->breaks[pos]) {
+ indices[n]=previous_x;levels[n]=previous_y;moves[n++]=0;
+ indices[n]=x;levels[n]=y;moves[n++]=1;
+ } else if(y!=previous_y) {
+ indices[n]=x;levels[n]=previous_y;moves[n++]=0;
+ indices[n]=x;levels[n]=y;moves[n++]=0;
+ }
+ previous_x=x;previous_y=y;pos=(pos+1U)%LAS_CAPACITY;
+ }
+ if(s->count>1) { indices[n]=previous_x;levels[n]=previous_y;moves[n++]=0; }
+ return n;
+}
+static size_t packet(uint32_t device,uint16_t body,const uint8_t *data,uint32_t uart,
+ uint32_t *raw,uint8_t *out,size_t cap) {
+ pcan_id_t id;pcan_frame_t frame;
+ if(device>15 || !raw || !out || cap<(uart?PCAN_FRAME_MAX:8U)) return 0;
+ memset(&id,0,sizeof(id));id.device_type=LAS_DEVICE_TYPE;id.device_id=(uint8_t)device;
+ id.priority=PCAN_PRIORITY_STANDARD;id.route=PCAN_ROUTE_FROM_DEVICE;
+ id.msg_type=PCAN_MSG_GAS;id.msg_body=body;
+ memset(&frame,0,sizeof(frame));frame.id=pcan_id_pack(&id);
+ frame.flags=PCAN_FLAG_IDE;frame.dlc=8;memcpy(frame.data,data,8);*raw=frame.id;
+ if(uart) return pcan_frame_encode(&frame,out,cap);
+ memcpy(out,data,8);return 8;
+}
+size_t las_metadata(uint32_t device,uint32_t session,uint32_t period,uint32_t uart,
+ uint32_t *id,uint8_t *out,size_t cap) {
+ uint8_t data[8];if(session>65535 || !period) return 0;
+ w16(data,LAS_TAG);w16(data+2,(uint16_t)session);w32(data+4,period);
+ return packet(device,LAS_GAS_META,data,uart,id,out,cap);
+}
+size_t las_data(uint32_t device,uint32_t session,uint32_t index,uint32_t sample,uint32_t uart,
+ uint32_t *id,uint8_t *out,size_t cap) {
+ uint8_t data[8];if(session>65535 || sample>65535) return 0;
+ w16(data,(uint16_t)session);w16(data+2,(uint16_t)sample);w32(data+4,index);
+ return packet(device,LAS_GAS_DATA,data,uart,id,out,cap);
+}
+void las_demo_step(void *ctx,uint32_t count) {
+ las_context *s=(las_context *)ctx;uint32_t i,bit,idx,raw;uint8_t packet_data[PCAN_FRAME_MAX];size_t n;
+ if(!s->metadata) {
+ n=las_metadata(s->device,1,1000000,1,&raw,packet_data,sizeof(packet_data));
+ las_uart(s,packet_data,n);
+ }
+ if(count>4096) count=4096;
+ for(i=0;inext_index;
+ for(bit=0;bit<16;++bit) {
+ if((idx/(5U+bit*7U))&1U) a|=(uint16_t)(1U<device,s->session,idx,a,1,&raw,packet_data,sizeof(packet_data));
+ las_uart(s,packet_data,n);
+ }
+}
diff --git a/c/set-protocol/tests/test_altera_logic.c b/c/set-protocol/tests/test_altera_logic.c
new file mode 100644
index 0000000..3e68221
--- /dev/null
+++ b/c/set-protocol/tests/test_altera_logic.c
@@ -0,0 +1,72 @@
+#include "altera_logic.h"
+#include
+#include
+#include
+#include
+static void response(void *s, uint8_t *data, size_t n) {
+ size_t i; uint8_t sum=0;
+ for(i=0;i>8); }
+ response(s,block,133);
+ }
+ assert(la_get(s,LA_STATE)==LA_DONE);
+ assert(la_samples(s,samples,4095)==0);
+ assert(la_samples(s,samples,4096)==4096);
+ for(i=0;i<4096;++i) assert(samples[i]==i);
+ assert(la_get(s,LA_TRIGGER_INDEX)==2048);
+ /* Corrupt XOR, wrong command, device error, timeout and extra data. */
+ la_init(s);la_next(s,p,6);
+ { uint8_t bad[]={0x5a,0x81,0,16,0,16,50,1,0};la_feed(s,bad,9); }
+ assert(la_get(s,LA_ERROR_CODE)==LA_CHECKSUM);
+ la_init(s);la_next(s,p,6);
+ { uint8_t bad[]={0x5a,0x88};la_feed(s,bad,2); }
+ assert(la_get(s,LA_ERROR_CODE)==LA_BAD_FRAME);
+ la_init(s);la_next(s,p,6);
+ { uint8_t bad[]={0x5a,0x81,1,0};response(s,bad,4); }
+ assert(la_get(s,LA_ERROR_CODE)==LA_DEVICE_ERROR);
+ la_init(s);la_next(s,p,6);la_tick(s,999);
+ assert(la_get(s,LA_STATE)==LA_CONNECTING);la_tick(s,1);
+ assert(la_get(s,LA_ERROR_CODE)==LA_TIMEOUT && la_next(s,p,6)==0);
+ connect_ok(s);la_feed(s,p,1);assert(la_get(s,LA_ERROR_CODE)==LA_BAD_FRAME);
+ la_init(s);la_next(s,p,6);
+ { uint8_t bad[]={0x5a,0x81,0,8,0,16,50,1,0};response(s,bad,9); }
+ assert(la_get(s,LA_ERROR_CODE)==LA_UNSUPPORTED);
+ la_demo_init(s);assert(la_demo_capture(s,49)==0);
+ assert(la_get(s,LA_STATE)==LA_DONE && la_samples(s,samples,4096)==4096);
+ free(s);puts("Altera Logic C tests passed");return 0;
+}
diff --git a/c/set-protocol/tests/test_altera_stream.c b/c/set-protocol/tests/test_altera_stream.c
new file mode 100644
index 0000000..4e1b04f
--- /dev/null
+++ b/c/set-protocol/tests/test_altera_stream.c
@@ -0,0 +1,62 @@
+#include "altera_stream.h"
+#include
+#include
+#include
+#include
+static void meta(void *s,uint32_t session,int uart) {
+ uint8_t data[32];uint32_t id;size_t n,i;
+ n=las_metadata(LAS_DEVICE_ID,session,1000000,(uint32_t)uart,&id,data,sizeof(data));assert(n);
+ assert(id==0x1EE3FE00U);
+ if(uart) for(i=0;i> n) & 1 for n in range(16)])
+
+
+class NativeAnalyzer:
+ CONNECTING, READY, CAPTURING, DONE, ERROR = range(5)
+ ERROR_TEXT = {
+ 1: "Неверный формат ответа FPGA",
+ 2: "Ошибка контрольной суммы ответа",
+ 3: "FPGA отклонила команду",
+ 4: "Тайм-аут UART. При потере синхронизации нужен RESET_N и повторная настройка",
+ 5: "Неподдерживаемая конфигурация FPGA: ожидается 16 каналов, 4096 выборок, версия 1",
+ 6: "Недопустимые или противоречивые настройки триггера",
+ 7: "Ошибка последовательного порта",
+ }
+
+ def __init__(self, library=None):
+ if library is None:
+ explicit = os.environ.get("ALTERA_LOGIC_LIBRARY")
+ name = "setprotocol.dll" if sys.platform == "win32" else "libsetprotocol.so"
+ bundled = Path(getattr(sys, "_MEIPASS", "")) / "gui_desktop/native" / name
+ local = Path(__file__).resolve().parent / "native" / name
+ path = Path(explicit) if explicit else (bundled if bundled.is_file() else local)
+ try:
+ library = C.CDLL(str(path))
+ except OSError as exc:
+ raise RuntimeError("Не загружено C-ядро Altera Logic. Соберите templates/"
+ "c/set-protocol/tools/build_host.py; " + str(exc)) from exc
+ self.lib = library
+ signatures = {
+ "la_context_size": ([], C.c_size_t),
+ "la_init": ([C.c_void_p], None),
+ "la_start": ([C.c_void_p] + [C.c_uint32]*5, C.c_int),
+ "la_next": ([C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
+ "la_feed": ([C.c_void_p, C.c_void_p, C.c_size_t], None),
+ "la_tick": ([C.c_void_p, C.c_uint32], None),
+ "la_fail": ([C.c_void_p, C.c_uint32], None),
+ "la_get": ([C.c_void_p, C.c_uint32], C.c_uint32),
+ "la_samples": ([C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
+ "la_demo_init": ([C.c_void_p], None),
+ "la_demo_capture": ([C.c_void_p, C.c_uint32], C.c_int),
+ }
+ try:
+ for name, (args, result) in signatures.items():
+ fn = getattr(self.lib, name)
+ fn.argtypes, fn.restype = args, result
+ except AttributeError as exc:
+ raise RuntimeError("C-ядро устарело: пересоберите templates с Altera Logic") from exc
+ # Explicitly aligned caller-owned storage; C never allocates memory.
+ self.ctx = (C.c_uint64 * ((self.lib.la_context_size()+7)//8))()
+ self.demo = False
+ self.reset()
+
+ def reset(self, demo=False):
+ self.demo = demo
+ (self.lib.la_demo_init if demo else self.lib.la_init)(self.ctx)
+
+ def get(self, field):
+ return int(self.lib.la_get(self.ctx, field))
+
+ @property
+ def state(self):
+ return self.get(0)
+
+ @property
+ def error(self):
+ return self.ERROR_TEXT.get(self.get(1), "Ошибка обмена")
+
+ @property
+ def progress(self):
+ return self.get(7)
+
+ @property
+ def flags(self):
+ return self.get(5)
+
+ def start(self, divider, mask=0, value=0, edge_mask=0, edge_value=0):
+ values = (divider, mask, value, edge_mask, edge_value)
+ if any(not isinstance(v, int) or not 0 <= v <= 65535 for v in values):
+ raise ValueError(self.ERROR_TEXT[6])
+ result = (self.lib.la_demo_capture(self.ctx, divider) if self.demo else
+ self.lib.la_start(self.ctx, *values))
+ if result:
+ raise ValueError(self.ERROR_TEXT.get(result, "Захват уже выполняется"))
+
+ def next_request(self):
+ out = (C.c_ubyte*6)()
+ size = self.lib.la_next(self.ctx, out, len(out))
+ return bytes(out[:size])
+
+ def feed(self, data):
+ self.lib.la_feed(self.ctx, data, len(data))
+
+ def tick(self, elapsed_ms):
+ self.lib.la_tick(self.ctx, max(0, min(int(elapsed_ms), 0xffffffff)))
+
+ def fail_io(self):
+ self.lib.la_fail(self.ctx, 7)
+
+ def capture(self):
+ out = (C.c_uint16*4096)()
+ count = self.lib.la_samples(self.ctx, out, len(out))
+ if not count:
+ raise RuntimeError("Запись ещё не завершена")
+ return Capture(tuple(out[:count]), self.get(6),
+ self.get(4)/(self.get(8)+1), self.demo)
diff --git a/python/altera_logic/plot_scroll.py b/python/altera_logic/plot_scroll.py
new file mode 100644
index 0000000..2139443
--- /dev/null
+++ b/python/altera_logic/plot_scroll.py
@@ -0,0 +1,27 @@
+"""Qt presentation port: wheel pans X; Ctrl+wheel pans Y, never zooms."""
+from PySide6.QtCore import Qt, Signal
+from PySide6.QtWidgets import QScrollArea
+
+
+class PlotScrollArea(QScrollArea):
+ user_scrolled = Signal(str)
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._remainder = {'x': 0., 'y': 0.}
+
+ def wheelEvent(self, event):
+ axis = 'y' if event.modifiers() & Qt.KeyboardModifier.ControlModifier else 'x'
+ bar = self.verticalScrollBar() if axis == 'y' else self.horizontalScrollBar()
+ pixels, angle = event.pixelDelta(), event.angleDelta()
+ if not pixels.isNull():
+ delta = pixels.y() if pixels.y() else pixels.x()
+ else:
+ delta = (angle.y() if angle.y() else angle.x()) / 120. * 60.
+ self._remainder[axis] -= delta
+ movement = int(self._remainder[axis])
+ self._remainder[axis] -= movement
+ if delta:
+ self.user_scrolled.emit(axis)
+ bar.setValue(bar.value()+movement)
+ event.accept()
diff --git a/python/altera_logic/qt_port.py b/python/altera_logic/qt_port.py
new file mode 100644
index 0000000..c5fdce9
--- /dev/null
+++ b/python/altera_logic/qt_port.py
@@ -0,0 +1,114 @@
+"""Qt serial/lifecycle port. Protocol decisions are exclusively in C99."""
+from __future__ import annotations
+
+from PySide6.QtCore import QObject, QTimer, QElapsedTimer, Signal
+from PySide6.QtSerialPort import QSerialPort, QSerialPortInfo
+from .native import NativeAnalyzer
+
+
+class AnalyzerPort(QObject):
+ changed = Signal()
+ completed = Signal(object)
+ error = Signal(str)
+ log = Signal(str)
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.core = None
+ self.active = False
+ self._last_done = False
+ self.serial = QSerialPort(self)
+ self.serial.readyRead.connect(self._read)
+ self.serial.errorOccurred.connect(self._serial_error)
+ self.timer = QTimer(self)
+ self.timer.setInterval(10)
+ self.timer.timeout.connect(self._tick)
+ self.clock = QElapsedTimer()
+
+ @staticmethod
+ def ports():
+ return [(p.portName(), p.description()) for p in QSerialPortInfo.availablePorts()]
+
+ def open(self, name, demo=False):
+ self.close()
+ try:
+ self.core = NativeAnalyzer()
+ self.core.reset(demo)
+ except RuntimeError as exc:
+ self.error.emit(str(exc))
+ return
+ if not demo:
+ self.serial.setPortName(name)
+ self.serial.setBaudRate(921600)
+ self.serial.setDataBits(QSerialPort.DataBits.Data8)
+ self.serial.setParity(QSerialPort.Parity.NoParity)
+ self.serial.setStopBits(QSerialPort.StopBits.OneStop)
+ self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl)
+ if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite):
+ self.error.emit(self.serial.errorString())
+ return
+ self.active = True
+ self._last_done = False
+ self.clock.start()
+ self.timer.start()
+ self.log.emit("ДЕМО: синтетические данные, триггер не моделируется" if demo
+ else "%s · 921600 8N1" % name)
+ self._pump()
+
+ def close(self):
+ self.active = False
+ self.timer.stop()
+ self.serial.close()
+ self.changed.emit()
+
+ def start(self, *settings):
+ if not self.active or not self.core:
+ return
+ try:
+ self.core.start(*settings)
+ except ValueError as exc:
+ self.error.emit(str(exc))
+ return
+ self._last_done = False
+ self._pump()
+
+ def _tick(self):
+ if not self.active:
+ return
+ self.core.tick(self.clock.restart())
+ self._pump()
+
+ def _read(self):
+ data = bytes(self.serial.readAll())
+ if self.active and data:
+ self.log.emit("RX " + data.hex(" ").upper())
+ self.core.feed(data)
+ self._pump()
+
+ def _pump(self):
+ if not self.active:
+ return
+ if self.core.state == NativeAnalyzer.ERROR:
+ message = self.core.error
+ self.close()
+ self.error.emit(message)
+ return
+ packet = self.core.next_request()
+ if packet:
+ self.log.emit("TX " + packet.hex(" ").upper())
+ if self.serial.write(packet) != len(packet):
+ self.core.fail_io()
+ self._pump()
+ return
+ self.clock.restart()
+ if self.core.state == NativeAnalyzer.DONE and not self._last_done:
+ self._last_done = True
+ self.completed.emit(self.core.capture())
+ self.changed.emit()
+
+ def _serial_error(self, code):
+ if self.active and code != QSerialPort.SerialPortError.NoError:
+ message = self.serial.errorString()
+ self.core.fail_io()
+ self.close()
+ self.error.emit(message)
diff --git a/python/altera_logic/stream.py b/python/altera_logic/stream.py
new file mode 100644
index 0000000..0a6b733
--- /dev/null
+++ b/python/altera_logic/stream.py
@@ -0,0 +1,108 @@
+"""SETCAN streaming FFI and immutable display/export models. No wire codec."""
+from __future__ import annotations
+import csv
+import ctypes as C
+from dataclasses import dataclass
+from .native import NativeAnalyzer
+
+
+@dataclass(frozen=True)
+class StreamSnapshot:
+ indices: tuple[int, ...]
+ samples: tuple[int, ...]
+ breaks: tuple[int, ...]
+ period_ns: int
+ session: int
+ demo: bool = False
+ traces: tuple = ()
+
+ def save_csv(self, path):
+ with open(path, "w", newline="", encoding="utf-8") as file:
+ writer = csv.writer(file)
+ writer.writerow(["session", "sample_index", "time_s", "gap_before", "sample_hex", "demo"] +
+ ["D%d" % n for n in range(16)])
+ for index, sample, gap in zip(self.indices, self.samples, self.breaks):
+ writer.writerow([self.session, index, index*self.period_ns/1e9, gap,
+ "%04X" % sample, int(self.demo)] +
+ [(sample >> bit)&1 for bit in range(16)])
+
+
+class NativeStream:
+ def __init__(self, device_id=None):
+ self._owner = NativeAnalyzer()
+ self.lib = self._owner.lib
+ signatures = {
+ "las_context_size": ([], C.c_size_t),
+ "las_init": ([C.c_void_p, C.c_uint32], C.c_int),
+ "las_can": ([C.c_void_p, C.c_uint32, C.c_void_p, C.c_size_t, C.c_uint32, C.c_uint32], None),
+ "las_uart": ([C.c_void_p, C.c_void_p, C.c_size_t], None),
+ "las_get": ([C.c_void_p, C.c_uint32], C.c_uint32),
+ "las_snapshot": ([C.c_void_p, C.c_void_p, C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
+ "las_trace": ([C.c_void_p, C.c_uint32, C.c_void_p, C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
+ "las_device_type": ([], C.c_uint32),
+ "las_device_id": ([], C.c_uint32),
+ "las_device_name": ([], C.c_char_p),
+ "las_demo_step": ([C.c_void_p, C.c_uint32], None),
+ "las_metadata": ([C.c_uint32]*4+[C.POINTER(C.c_uint32), C.c_void_p, C.c_size_t], C.c_size_t),
+ "las_data": ([C.c_uint32]*5+[C.POINTER(C.c_uint32), C.c_void_p, C.c_size_t], C.c_size_t),
+ }
+ try:
+ for name, (args, result) in signatures.items():
+ fn = getattr(self.lib, name)
+ fn.argtypes, fn.restype = args, result
+ except AttributeError as exc:
+ raise RuntimeError("Обновите DLL templates: отсутствует SETCAN Altera stream") from exc
+ self.device_type = int(self.lib.las_device_type())
+ self.device_id = int(self.lib.las_device_id()) if device_id is None else device_id
+ self.device_name = self.lib.las_device_name().decode("utf-8")
+ self.ctx = (C.c_uint64*((self.lib.las_context_size()+7)//8))()
+ if not 0 <= self.device_id <= 15 or not self.lib.las_init(self.ctx, self.device_id):
+ raise ValueError("DeviceID должен быть 0…15")
+
+ @property
+ def stats(self):
+ fields = ("count", "period_ns", "session", "received", "missing", "duplicates",
+ "invalid", "ignored", "crc_errors", "has_meta")
+ return {name: int(self.lib.las_get(self.ctx, i)) for i, name in enumerate(fields)}
+
+ def feed_can(self, identifier, data, extended=True, remote=False):
+ if not 0 <= identifier <= 0xffffffff:
+ return
+ self.lib.las_can(self.ctx, identifier, data, len(data), int(extended), int(remote))
+
+ def feed_uart(self, data):
+ self.lib.las_uart(self.ctx, data, len(data))
+
+ def snapshot(self, demo=False):
+ stats = self.stats
+ count = stats["count"]
+ indices, samples, breaks = (C.c_uint64*count)(), (C.c_uint16*count)(), (C.c_ubyte*count)()
+ n = self.lib.las_snapshot(self.ctx, indices, samples, breaks, count)
+ traces = []
+ xs, ys, moves = (C.c_uint64*(2*count))(), (C.c_ubyte*(2*count))(), (C.c_ubyte*(2*count))()
+ for channel in range(16):
+ size = self.lib.las_trace(self.ctx, channel, xs, ys, moves, 2*count)
+ traces.append(tuple(zip(xs[:size], ys[:size], moves[:size])))
+ return StreamSnapshot(tuple(indices[:n]), tuple(samples[:n]), tuple(breaks[:n]),
+ stats["period_ns"], stats["session"], demo, tuple(traces))
+
+ def demo_step(self, count=50):
+ self.lib.las_demo_step(self.ctx, count)
+
+ def metadata_packet(self, session, period_ns, uart=False):
+ if not 0 <= session <= 65535 or not 1 <= period_ns <= 0xffffffff:
+ raise ValueError("Invalid stream metadata")
+ return self._packet(self.lib.las_metadata, session, period_ns, int(uart))
+
+ def data_packet(self, session, index, sample, uart=False):
+ if not 0 <= index <= 0xffffffff or any(not 0 <= v <= 65535 for v in (session, sample)):
+ raise ValueError("Invalid stream samples")
+ return self._packet(self.lib.las_data, session, index, sample, int(uart))
+
+ def _packet(self, fn, *args):
+ identifier = C.c_uint32()
+ out = (C.c_ubyte*32)()
+ n = fn(self.device_id, *args, C.byref(identifier), out, len(out))
+ if not n:
+ raise ValueError("Invalid SETCAN packet arguments")
+ return identifier.value, bytes(out[:n])
diff --git a/python/altera_logic/stream_port.py b/python/altera_logic/stream_port.py
new file mode 100644
index 0000000..e7e01a4
--- /dev/null
+++ b/python/altera_logic/stream_port.py
@@ -0,0 +1,103 @@
+"""Qt transport/lifecycle port for the C SETCAN stream receiver.
+
+CAN ingress accepts canonical RX events from an existing bus connection.
+UART uses the same SETCAN frames inside the shared AA55/CRC16 transport.
+Reception and rendering clocks are separate; no packet-rate repainting.
+"""
+from __future__ import annotations
+from PySide6.QtCore import QObject, QTimer, QElapsedTimer, Signal
+from PySide6.QtSerialPort import QSerialPort, QSerialPortInfo
+from .stream import NativeStream
+
+
+class StreamPort(QObject):
+ updated = Signal(object, object)
+ changed = Signal(bool)
+ error = Signal(str)
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.core = None
+ self.active = False
+ self.mode = "demo"
+ self.paused = False
+ self.serial = QSerialPort(self)
+ self.serial.readyRead.connect(self._read)
+ self.serial.errorOccurred.connect(self._error)
+ self.timer = QTimer(self)
+ self.timer.setInterval(50)
+ self.timer.timeout.connect(self._tick)
+ self._previous = None
+ self._last_data = QElapsedTimer()
+ self._last_received = None
+
+ @staticmethod
+ def ports():
+ return [(p.portName(), p.description()) for p in QSerialPortInfo.availablePorts()]
+
+ def open(self, mode, port_name="", device_id=None):
+ self.close()
+ if mode not in ("demo", "can", "uart", "both"):
+ self.error.emit("Неизвестный транспорт")
+ return
+ try:
+ self.core = NativeStream(device_id)
+ except (RuntimeError, ValueError) as exc:
+ self.error.emit(str(exc))
+ return
+ self.mode = mode
+ if mode in ("uart", "both"):
+ self.serial.setPortName(port_name)
+ self.serial.setBaudRate(921600)
+ self.serial.setDataBits(QSerialPort.DataBits.Data8)
+ self.serial.setParity(QSerialPort.Parity.NoParity)
+ self.serial.setStopBits(QSerialPort.StopBits.OneStop)
+ self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl)
+ if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite):
+ self.error.emit(self.serial.errorString())
+ return
+ self.active = True
+ self.paused = False
+ self._previous = None
+ self._last_received = None
+ self._last_data.start()
+ self.timer.start()
+ self.changed.emit(True)
+
+ def close(self):
+ self.active = False
+ self.timer.stop()
+ self.serial.close()
+ self.changed.emit(False)
+
+ def receive_event(self, event):
+ if (self.active and self.mode in ("can", "both") and event.get("kind") == "can"
+ and event.get("direction") == "RX"):
+ self.core.feed_can(event["identifier"], event["data"],
+ event.get("extended", False), event.get("remote", False))
+
+ def _read(self):
+ data = bytes(self.serial.readAll())
+ if self.active and self.mode in ("uart", "both"):
+ self.core.feed_uart(data)
+
+ def _tick(self):
+ if not self.active:
+ return
+ if self.mode == "demo":
+ self.core.demo_step()
+ stats = self.core.stats
+ signature = (stats["received"], stats["session"])
+ if signature != self._last_received:
+ self._last_received = signature
+ self._last_data.restart()
+ stats["stale"] = self._last_data.elapsed() > max(2000, stats["period_ns"]*3/1e6)
+ if not self.paused and stats != self._previous:
+ self._previous = stats
+ self.updated.emit(self.core.snapshot(self.mode == "demo"), stats)
+
+ def _error(self, code):
+ if self.active and code != QSerialPort.SerialPortError.NoError:
+ message = self.serial.errorString()
+ self.close()
+ self.error.emit(message)
diff --git a/python/protocan/__init__.py b/python/protocan/__init__.py
index d7d39f3..914dd0e 100644
--- a/python/protocan/__init__.py
+++ b/python/protocan/__init__.py
@@ -1,5 +1,10 @@
"""Переносимые модули и тонкая Python-обёртка SETProtocol."""
+# Consumers may supply additional platform ports from their pinned templates
+# checkout. The selected primary checkout wins; missing modules can coexist.
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
+
from .native import (
NativeCore, NativeCoreUnavailable, NativeFrame, NativeGuiFrame,
NativeGuiParser, NativeParser, NativeProtocol, NativeProtocolUnavailable,
diff --git a/python/protocan/can_boot.py b/python/protocan/can_boot.py
new file mode 100644
index 0000000..e909c8c
--- /dev/null
+++ b/python/protocan/can_boot.py
@@ -0,0 +1,123 @@
+"""Legacy ProtoCAN Boot client ported from Gui_Android CanFirmwareProtocol.kt."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+import struct
+
+from .protocan import ProtoCanId
+from .transport import build_frame
+
+
+@dataclass(frozen=True)
+class CanBootTarget:
+ device_type: int = 0
+ device: int = 13
+ product_type: int = 0x166
+ hardware_min: int = 0
+ hardware_max: int = 255
+ session_id: int = 1
+
+ def __post_init__(self):
+ if not 0 <= self.device_type <= 7 or not 0 <= self.device <= 15:
+ raise ValueError("Адрес ProtoCAN: Device Type 0…7, Device 0…15")
+ if not 0 <= self.product_type <= 65535:
+ raise ValueError("Product Type вне диапазона u16")
+ if not 0 <= self.hardware_min <= self.hardware_max <= 255:
+ raise ValueError("Неверный диапазон аппаратных ревизий")
+ if not 1 <= self.session_id <= 255:
+ raise ValueError("Session ID должен быть 1…255")
+
+
+class CanBootTransfer:
+ """Windowed 8-byte blocks; control 0x9, slots 0xA/0xB, status 0xC."""
+ def __init__(self, image, target: CanBootTarget):
+ if not 0 < len(image.data) <= 512 * 1024:
+ raise ValueError("ProtoCAN Boot: размер образа 1…512 КиБ")
+ if not 0 <= image.version <= 0xFFFFFFFF:
+ raise ValueError("Версия образа вне диапазона u32")
+ self.image, self.target = image, target
+ self.stage = "enter"
+ self.next_block = 0
+ self.slot = None
+ self.finished = False
+ self._window_end = 0
+
+ @property
+ def percent(self):
+ return min(100, self.next_block * 8 * 100 // len(self.image.data))
+
+ def _frame(self, kind, body, data=b""):
+ t = self.target
+ return build_frame(ProtoCanId.build(1, 0, t.device_type, t.device, kind, body), data, to_can=True)
+
+ def _control(self, command, data=b""):
+ return [self._frame(9, self.target.session_id << 8 | command, data)]
+
+ def start(self):
+ return self._control(2)
+
+ def abort(self):
+ return self._control(10)
+
+ def accepts(self, frame):
+ p = ProtoCanId.parse(frame.can_id)
+ t = self.target
+ return (not frame.to_can and frame.ide and not frame.rtr and not frame.is_error
+ and p.msg_type == 12 and p.pm == 1 and p.device_type == t.device_type
+ and p.device == t.device and p.body >> 8 == t.session_id and len(frame.data) == 8)
+
+ def _window(self):
+ total = (len(self.image.data) + 7) // 8
+ self._window_end = min(total, self.next_block + 16)
+ return [self._frame(10 + self.slot, i, self.image.data[i * 8:(i + 1) * 8].ljust(8, b"\xff"))
+ for i in range(self.next_block, self._window_end)]
+
+ def handle_status(self, frame):
+ if not self.accepts(frame):
+ return [], ""
+ command = ProtoCanId.parse(frame.can_id).body & 255
+ expected_command = {"enter": 2, "image": 3, "compat": 4, "erase": 5,
+ "data": 0, "verify": 6, "commit": 7, "reboot": 9}.get(self.stage)
+ if command != expected_command:
+ return [], ""
+ status, slot, expected = struct.unpack_from(" limit:
+ raise RuntimeError("ProtoCAN Boot: подтверждён непереданный блок")
+ if self.stage == "data" and status == 0 and expected < self.next_block:
+ return [], ""
+ self.next_block = expected
+ if expected == total:
+ self.stage = "verify"
+ return self._control(6), "Проверка CRC32"
+ self.stage = "data"
+ return self._window(), "Передача блоков ProtoCAN Boot"
+ if self.stage == "verify":
+ self.stage = "commit"
+ return self._control(7), "Активация образа"
+ if self.stage == "commit":
+ self.stage = "reboot"
+ return self._control(9), "Перезапуск прибора"
+ self.stage, self.finished = "finished", True
+ return [], "Прошивка ProtoCAN Boot завершена"
diff --git a/python/protocan/protocan.py b/python/protocan/protocan.py
index 7ed69e2..fe8ee7a 100644
--- a/python/protocan/protocan.py
+++ b/python/protocan/protocan.py
@@ -131,9 +131,11 @@ MSGTYPE_RU = {
#: но не присваивает приборам имён.
DEVICE_TYPE_NAMES = {
0x0: "Верхний уровень",
+ 0x6: "Логические анализаторы",
}
DEVICE_ADDRESS_NAMES = {
+ (0x6, 0xE): "Altera Logic",
(0x7, 0xD): "configurator",
}
diff --git a/python/protocan/setp_firmware.py b/python/protocan/setp_firmware.py
new file mode 100644
index 0000000..f4f693e
--- /dev/null
+++ b/python/protocan/setp_firmware.py
@@ -0,0 +1,239 @@
+"""SETProtocol v2 firmware client over segmented classic CAN."""
+
+from __future__ import annotations
+
+import struct
+from dataclasses import dataclass
+from time import perf_counter
+
+from setprotocol.can import CanAddress, CanFrame, CanReassembler, segment
+from setprotocol.core import (
+ Capabilities,
+ DeviceInfo,
+ Feature,
+ FirmwareBegin,
+ FirmwareFlag,
+ Frame as SetFrame,
+ FrameFlag,
+ MessageType,
+ SetProtocolError,
+ Status,
+ build_frame,
+ decode_datagram,
+ decode_response,
+ encode_firmware_data,
+)
+
+from . import transport as tr
+from typing import Any
+
+FirmwareImage = Any
+
+
+@dataclass(frozen=True)
+class CanFirmwareTarget:
+ """Address and compatibility settings for a SETProtocol v2 target."""
+
+ node_id: int
+ device_class: int
+ hardware_min: int = 0
+ hardware_max: int = 0xFF
+
+ def __post_init__(self) -> None:
+ if not 0 <= self.node_id <= 0xFF:
+ raise ValueError("SETP node ID вне диапазона 0..255")
+ if not 0 <= self.device_class <= 0xFFFF:
+ raise ValueError("Device class вне диапазона u16")
+ if not 0 <= self.hardware_min <= self.hardware_max <= 0xFFFFFFFF:
+ raise ValueError("Диапазон hardware version задан неверно")
+
+
+class CanFirmwareTransfer:
+ """Stop-and-wait SETP v2 firmware transaction for a classic CAN link."""
+
+ HOST_NODE = 0
+ CHANNEL = 1
+ DEFAULT_BLOCK_SIZE = 64
+
+ def __init__(self, image: FirmwareImage, target: CanFirmwareTarget) -> None:
+ if not image.data:
+ raise ValueError("Образ прошивки пуст")
+ if len(image.data) > 512 * 1024:
+ raise ValueError("BALZAM поддерживает образ не более 512 КиБ")
+ self.image = image
+ self.target = target
+ self.offset = 0
+ self.block_size = self.DEFAULT_BLOCK_SIZE
+ self.stage = "ping"
+ self.finished = False
+ self._sequence = 0
+ self._expected_sequence = 0
+ self._expected_type = 0
+ self._response: SetFrame | None = None
+ self._reassembler = CanReassembler()
+
+ def _request(self, message_type: int, payload: bytes = b"") -> list[tr.Frame]:
+ self._sequence = (self._sequence + 1) & 0xFFFF
+ if self._sequence == 0:
+ self._sequence = 1
+ self._expected_sequence = self._sequence
+ self._expected_type = int(message_type)
+ packet = build_frame(
+ SetFrame(
+ message_type=message_type,
+ sequence=self._sequence,
+ payload=payload,
+ flags=FrameFlag.ACK_REQUIRED | FrameFlag.PRIORITY,
+ source=self.HOST_NODE,
+ destination=self.target.node_id,
+ )
+ )
+ address = CanAddress(
+ destination=self.target.node_id,
+ source=self.HOST_NODE,
+ priority=1,
+ channel=self.CHANNEL,
+ )
+ return [tr.build_frame(item.can_id, item.data, to_can=True)
+ for item in segment(packet, address)]
+
+ def start(self) -> list[tr.Frame]:
+ return self._request(MessageType.PING)
+
+ def abort(self) -> list[tr.Frame]:
+ return self._request(MessageType.FW_ABORT)
+
+ def accepts(self, frame: tr.Frame) -> bool:
+ if frame.to_can or not frame.ide:
+ return False
+ try:
+ address = CanAddress.unpack(frame.can_id)
+ except SetProtocolError:
+ return False
+ if address.source != self.target.node_id or address.destination != self.HOST_NODE:
+ return False
+ try:
+ packet = self._reassembler.feed(
+ CanFrame(frame.can_id, frame.data), int(perf_counter() * 1000)
+ )
+ if packet is None:
+ return False
+ response = decode_datagram(packet)
+ except SetProtocolError:
+ return False
+ if (
+ not response.flags & FrameFlag.RESPONSE
+ or response.source != self.target.node_id
+ or response.destination != self.HOST_NODE
+ or response.sequence != self._expected_sequence
+ or response.message_type != self._expected_type
+ ):
+ return False
+ self._response = response
+ return True
+
+ def _begin(self) -> list[tr.Frame]:
+ begin = FirmwareBegin(
+ image_size=len(self.image.data),
+ image_crc32=self.image.crc32,
+ image_version=self.image.version,
+ base_address=self.image.base_address,
+ slot=0,
+ block_size=self.block_size,
+ sha256=bytes.fromhex(self.image.sha256),
+ flags=FirmwareFlag.RESUME | FirmwareFlag.ERASE_SLOT,
+ )
+ self.stage = "begin"
+ return self._request(MessageType.FW_BEGIN, begin.encode())
+
+ def _next_data_or_end(self) -> tuple[list[tr.Frame], str]:
+ if self.offset >= len(self.image.data):
+ self.stage = "end"
+ payload = struct.pack(
+ " tuple[list[tr.Frame], str]:
+ response, self._response = self._response, None
+ if response is None:
+ return [], ""
+ status, body = decode_response(response)
+ if status != Status.OK:
+ try:
+ name = Status(status).name
+ except ValueError:
+ name = "0x%04X" % status
+ raise RuntimeError(name)
+
+ if self.stage == "ping":
+ if len(body) != 4:
+ raise RuntimeError("PING: неверная длина ответа")
+ self.stage = "device_info"
+ return self._request(MessageType.DEVICE_INFO), "Чтение информации об устройстве"
+
+ if self.stage == "device_info":
+ info = DeviceInfo.decode(body)
+ if self.target.device_class and info.device_class != self.target.device_class:
+ raise RuntimeError(
+ "device class 0x%04X вместо 0x%04X"
+ % (info.device_class, self.target.device_class)
+ )
+ if not self.target.hardware_min <= info.hardware_version <= self.target.hardware_max:
+ raise RuntimeError("hardware version устройства вне разрешённого диапазона")
+ self.stage = "capabilities"
+ return self._request(MessageType.CAPABILITIES), "Проверка возможностей устройства"
+
+ if self.stage == "capabilities":
+ capabilities = Capabilities.decode(body)
+ if not capabilities.features & Feature.FIRMWARE:
+ raise RuntimeError("устройство не объявило поддержку firmware update")
+ self.block_size = min(
+ self.DEFAULT_BLOCK_SIZE,
+ capabilities.max_payload - 12,
+ )
+ if self.block_size <= 0:
+ raise RuntimeError("устройство объявило слишком маленький CAN firmware MTU")
+ return self._begin(), "Начало SETProtocol v2 firmware session"
+
+ if self.stage == "begin":
+ if len(body) != 4:
+ raise RuntimeError("FW_BEGIN: неверный next_offset")
+ self.offset = int.from_bytes(body, "little")
+ if self.offset > len(self.image.data):
+ raise RuntimeError("FW_BEGIN: next_offset за пределами образа")
+ return self._next_data_or_end()
+
+ if self.stage == "data":
+ if len(body) != 4:
+ raise RuntimeError("FW_DATA: неверный next_offset")
+ next_offset = int.from_bytes(body, "little")
+ expected = min(self.offset + self.block_size, len(self.image.data))
+ if next_offset != expected:
+ raise RuntimeError(
+ "FW_DATA: подтверждён offset %d вместо %d" % (next_offset, expected)
+ )
+ self.offset = next_offset
+ return self._next_data_or_end()
+
+ if self.stage == "end":
+ if len(body) != 4 or int.from_bytes(body, "little") != len(self.image.data):
+ raise RuntimeError("FW_END: устройство не подтвердило полный образ")
+ self.stage = "activate"
+ return self._request(MessageType.FW_ACTIVATE), "Активация образа"
+
+ if self.stage == "activate":
+ self.finished = True
+ return [], "Прошивка по CAN (SETProtocol v2) завершена"
+ return [], ""
+
+ @property
+ def percent(self) -> int:
+ return min(100, int(self.offset * 100 / len(self.image.data)))
diff --git a/python/protocan/stm32_boot.py b/python/protocan/stm32_boot.py
new file mode 100644
index 0000000..9b75a10
--- /dev/null
+++ b/python/protocan/stm32_boot.py
@@ -0,0 +1,47 @@
+"""Packets and constants for the STM32 system-memory UART bootloader (AN3155)."""
+
+from __future__ import annotations
+
+ACK = 0x79
+NACK = 0x1F
+SYNC = b"\x7f"
+
+
+def command(code: int) -> bytes:
+ """Encode an AN3155 command and its one's-complement checksum."""
+ if not 0 <= code <= 0xFF:
+ raise ValueError("Код команды вне диапазона байта")
+ return bytes((code, code ^ 0xFF))
+
+
+def address(value: int) -> bytes:
+ """Encode a big-endian 32-bit address followed by XOR checksum."""
+ if not 0 <= value <= 0xFFFFFFFF:
+ raise ValueError("Адрес вне диапазона u32")
+ raw = value.to_bytes(4, "big")
+ return raw + bytes((raw[0] ^ raw[1] ^ raw[2] ^ raw[3],))
+
+
+def write_payload(data: bytes) -> bytes:
+ """Encode one Write Memory payload (1..256 bytes)."""
+ if not 1 <= len(data) <= 256:
+ raise ValueError("Блок STM32 должен содержать от 1 до 256 байт")
+ count = len(data) - 1
+ checksum = count
+ for value in data:
+ checksum ^= value
+ return bytes((count,)) + data + bytes((checksum,))
+
+
+MASS_ERASE = b"\xff\x00"
+
+
+def erase_pages_payload(pages: list[int]) -> bytes:
+ """Encode the standard Erase Memory page list used by STM32F1."""
+ if not pages or len(pages) > 256 or any(not 0 <= page <= 0xFF for page in pages):
+ raise ValueError("Список страниц STM32 должен содержать 1..256 номеров")
+ body = bytes((len(pages) - 1, *pages))
+ checksum = 0
+ for value in body:
+ checksum ^= value
+ return body + bytes((checksum,))
diff --git a/python/protocan/tms_firmware.py b/python/protocan/tms_firmware.py
new file mode 100644
index 0000000..e6f1fe3
--- /dev/null
+++ b/python/protocan/tms_firmware.py
@@ -0,0 +1,118 @@
+"""BALZAM/PM67 firmware protocol, ported from Gui_Android Tms2812Protocol.
+
+Addresses count 16-bit words; transfer lengths count bytes.
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+import struct
+
+from protocan.tms2812 import crc16_modbus, build_upload_request, decode_upload_reply
+
+CMD_LOAD = 51
+CMD_UPLOAD = 52
+CMD_TFLASH = 55
+CMD_INITLOAD = 58
+CMD_EXTEND = 60
+
+
+def packet(controller: int, command: int, payload: bytes) -> bytes:
+ raw = bytes((controller, command)) + payload
+ return raw + struct.pack(" bytes:
+ return packet(controller, CMD_INITLOAD, struct.pack(" bytes:
+ return packet(controller, CMD_LOAD, data)
+
+
+def tflash(controller: int, ram: int, flash: int, size: int) -> bytes:
+ return packet(controller, CMD_TFLASH, struct.pack(" bytes:
+ return packet(controller, CMD_EXTEND, struct.pack(" tuple[int, int, int]:
+ return struct.unpack_from(" bytes | None:
+ """Accept omitted reserved tail bytes only after the receive settle interval."""
+ tail = 4 if command == CMD_UPLOAD and size > 6 else 2
+ if not size - tail <= len(raw) <= size or raw[:2] != bytes((controller, command)):
+ return None
+ full = raw.ljust(size, b"\0")
+ if tail == 4:
+ try:
+ decode_upload_reply(full, controller, size - 8)
+ except ValueError:
+ return None
+ elif crc16_modbus(full[:-4]) != int.from_bytes(full[-4:-2], "little"):
+ return None
+ return full
+
+
+@dataclass(frozen=True)
+class TmsTarget:
+ controller: int = 10
+ ram: int = 0x000A0000
+ flash: int = 0x00110000
+ block_size: int = 256
+ kind: str = "tms"
+ board: int = 0
+ load_only: bool = False
+
+ def validate(self, size: int) -> None:
+ if not 0 <= self.controller <= 255:
+ raise ValueError("Адрес контроллера должен быть 0…255")
+ if not 256 <= self.block_size <= 65534 or self.block_size % 2:
+ raise ValueError("Блок LOAD должен быть чётным, 256…65534 байт")
+ if size <= 0:
+ raise ValueError("Образ пуст")
+ words = (size + 1) // 2
+ if not 0 <= self.ram <= 0xFFFFFFFF or self.ram + words > 0x100000000:
+ raise ValueError("Диапазон RAM выходит за пределы uint32")
+ if self.kind not in ("tms", "spartan2e", "spartan6"):
+ raise ValueError("Неизвестный протокол прошивки TMS")
+ if not 0 <= self.board <= 255 or not 0 <= self.flash <= 0xFFFFFFFF:
+ raise ValueError("Неверный адрес платы или памяти")
+ if not self.load_only and self.kind == "tms" and (not 0x00100000 <= self.flash < 0x00180000 or self.flash + words > 0x00180000):
+ raise ValueError("Flash должна находиться в диапазоне слов 0x00100000…0x00180000")
+
+
+def programming_steps(data: bytes, target: TmsTarget):
+ """Yield requests; send each validated response back into the generator."""
+ t = target
+ t.validate(len(data))
+ for offset in range(0, len(data), t.block_size):
+ block = data[offset:offset + t.block_size]
+ ram = t.ram + offset // 2
+ yield init_load(t.controller, ram, len(block)), 6, 1500, "CMD_INITLOAD", int(offset * 85 / len(data))
+ yield load_data(t.controller, block), 6, 3000, "CMD_LOAD", int(offset * 85 / len(data))
+ if t.load_only:
+ return
+ if t.kind != "tms":
+ code = 6 if t.kind == "spartan2e" else 10
+ response = yield extend(t.controller, t.ram, t.flash, len(data), code, t.board), 18, 600000, "CMD_EXTEND: запись платы", 85
+ words, error, repeats = extend_result(response)
+ if error or words != (len(data) + 1) // 2:
+ raise ValueError(f"Ошибка записи платы: код {error}, обработано {words} слов, повторов {repeats}")
+ if t.kind == "spartan2e":
+ response = yield extend(t.controller, t.ram, t.flash, len(data), 17, t.board), 18, 600000, "CMD_EXTEND: проверка EEPROM", 95
+ words, error, repeats = extend_result(response)
+ if error or words != (len(data) + 1) // 2:
+ raise ValueError(f"Проверка EEPROM не пройдена: код {error}, обработано {words} слов")
+ return
+ yield tflash(t.controller, t.ram, t.flash, len(data)), 6, 180000, "CMD_TFLASH: запись Flash", 85
+ for offset in range(0, len(data), 256):
+ block = data[offset:offset + 256]
+ count = (len(block) + 1) & ~1
+ response = yield build_upload_request(t.controller, t.flash + offset // 2, count), count + 8, 30000, "CMD_UPLOAD: проверка Flash", 90 + int(offset * 10 / len(data))
+ actual = decode_upload_reply(response, t.controller, count)[:len(block)]
+ if actual != block:
+ raise ValueError(f"Проверка Flash не пройдена: блок по адресу слова 0x{t.flash + offset // 2:08X}")
diff --git a/python/setprotocol/firmware_database.py b/python/setprotocol/firmware_database.py
new file mode 100644
index 0000000..276d82f
--- /dev/null
+++ b/python/setprotocol/firmware_database.py
@@ -0,0 +1,268 @@
+"""Standalone firmware database: HTTPS catalog, verified downloads and Gitea publishing.
+
+No Qt, desktop application, credential store or MCU dependencies.
+"""
+
+from __future__ import annotations
+
+import base64
+import hashlib
+import json
+import os
+import tempfile
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Callable
+from urllib.error import HTTPError
+from urllib.parse import quote, unquote, urljoin, urlsplit
+from urllib.request import HTTPRedirectHandler, Request, build_opener
+
+from .firmware_catalog import MAX_MANIFEST_BYTES, FirmwareRelease, parse_firmware_catalog
+from .firmware_publish import (
+ MAX_FIRMWARE_BYTES, FirmwarePublication, firmware_release_entry,
+ firmware_release_tag, sha256_file, update_firmware_manifest,
+)
+
+CHUNK = 128 * 1024
+
+
+def _origin(url: str) -> tuple[str, str, int]:
+ parsed = urlsplit(url)
+ if (parsed.scheme != "https" or not parsed.hostname
+ or parsed.username is not None or parsed.password is not None):
+ raise ValueError("Expected an HTTPS URL without embedded credentials")
+ return parsed.scheme, parsed.hostname, parsed.port or 443
+
+
+@dataclass(frozen=True)
+class Credentials:
+ login: str
+ password: str = field(repr=False)
+
+
+class _NoRedirect(HTTPRedirectHandler):
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
+ return None
+
+
+class HttpsClient:
+ """Credentials are restricted to one origin, including across redirects."""
+
+ def __init__(self, credential_origin: str, credentials: Credentials | None = None):
+ self.origin = _origin(credential_origin)
+ self.credentials = credentials
+ self.opener = build_opener(_NoRedirect())
+
+ def open(self, url: str, *, method: str = "GET", data: bytes | None = None,
+ content_type: str = "application/json"):
+ for attempt in range(6):
+ origin = _origin(url)
+ headers = {"Accept": "application/json, application/octet-stream",
+ "Content-Type": content_type, "User-Agent": "templates-firmware-db/1"}
+ if self.credentials is not None and origin == self.origin:
+ raw = f"{self.credentials.login}:{self.credentials.password}".encode("utf-8")
+ headers["Authorization"] = "Basic " + base64.b64encode(raw).decode("ascii")
+ request = Request(url, data=data, headers=headers, method=method)
+ try:
+ return self.opener.open(request, timeout=180)
+ except HTTPError as error:
+ # Never replay a write or its body at a redirect destination.
+ if (method != "GET" or error.code not in (301, 302, 303, 307, 308)
+ or not error.headers.get("Location") or attempt == 5):
+ raise
+ destination = urljoin(url, error.headers["Location"])
+ error.close()
+ url = destination
+ raise RuntimeError("Too many redirects")
+
+
+@dataclass(frozen=True)
+class GiteaRepository:
+ server: str
+ owner: str
+ repository: str
+ branch: str = "main"
+ manifest_path: str = "update.json"
+
+ def __post_init__(self):
+ _origin(self.server)
+ parsed = urlsplit(self.server)
+ if parsed.query or parsed.fragment:
+ raise ValueError("Server URL must not contain a query or fragment")
+ if not all((self.owner, self.repository, self.branch, self.manifest_path)):
+ raise ValueError("Repository settings must not be empty")
+
+ @property
+ def web(self) -> str:
+ return f"{self.server.rstrip('/')}/{quote(self.owner, safe='')}/{quote(self.repository, safe='')}"
+
+ @property
+ def api(self) -> str:
+ return (f"{self.server.rstrip('/')}/api/v1/repos/"
+ f"{quote(self.owner, safe='')}/{quote(self.repository, safe='')}")
+
+ @property
+ def manifest_url(self) -> str:
+ return (f"{self.web}/raw/branch/{quote(self.branch, safe='')}/"
+ f"{quote(self.manifest_path, safe='/')}")
+
+
+class FirmwareDatabase:
+ """Blocking service; call from a worker when integrating with a GUI."""
+
+ def __init__(self, manifest_url: str, cache_dir: Path, *,
+ credentials: Credentials | None = None, client=None):
+ _origin(manifest_url)
+ self.manifest_url = manifest_url
+ self.cache_dir = Path(cache_dir)
+ self.client = client if client is not None else HttpsClient(manifest_url, credentials)
+
+ def read_catalog(self, *, product: str | None = None,
+ transport: str | None = None) -> list[FirmwareRelease]:
+ with self.client.open(self.manifest_url) as response:
+ data = response.read(MAX_MANIFEST_BYTES + 1)
+ releases = parse_firmware_catalog(data, self.manifest_url)
+ return [r for r in releases
+ if (product is None or r.product.casefold() == product.casefold())
+ and (transport is None or r.transport == transport.lower())]
+
+ def download(self, release: FirmwareRelease,
+ progress: Callable[[int], None] | None = None) -> Path:
+ # Validate even objects constructed directly by callers (including file names).
+ entry = {"product": release.product, "versionName": release.version,
+ "versionCode": release.version_code, "imageUrl": release.image_url,
+ "fileName": release.file_name, "sha256": release.sha256,
+ "transport": release.transport}
+ release = parse_firmware_catalog(
+ json.dumps({"firmware": [entry]}).encode(), self.manifest_url)[0]
+ directory = self.cache_dir / release.sha256
+ directory.mkdir(parents=True, exist_ok=True)
+ target = directory / release.file_name
+ if target.is_file() and sha256_file(target) == release.sha256:
+ if progress:
+ progress(100)
+ return target
+ # Unique staging files keep concurrent downloads independent.
+ with tempfile.NamedTemporaryFile(dir=directory, suffix=".part", delete=False) as output:
+ temporary = Path(output.name)
+ try:
+ digest = hashlib.sha256()
+ received = 0
+ with self.client.open(release.image_url) as response, temporary.open("wb") as output:
+ total = int(response.headers.get("Content-Length", "-1") or -1)
+ if total > MAX_FIRMWARE_BYTES:
+ raise ValueError("Firmware exceeds maximum size")
+ while chunk := response.read(CHUNK):
+ received += len(chunk)
+ if received > MAX_FIRMWARE_BYTES:
+ raise ValueError("Firmware exceeds maximum size")
+ digest.update(chunk)
+ output.write(chunk)
+ if progress and total > 0:
+ progress(min(99, received * 100 // total))
+ output.flush()
+ os.fsync(output.fileno())
+ if not received or digest.hexdigest() != release.sha256:
+ raise ValueError("Downloaded firmware is empty or SHA-256 does not match")
+ temporary.replace(target)
+ if progress:
+ progress(100)
+ return target
+ finally:
+ temporary.unlink(missing_ok=True)
+
+
+class GiteaFirmwarePublisher:
+ """Publish an immutable image, verify it, then commit the shared catalog."""
+
+ def __init__(self, repository: GiteaRepository, credentials: Credentials | None = None,
+ *, client=None):
+ self.repository = repository
+ self.client = client if client is not None else HttpsClient(repository.server, credentials)
+
+ def _call(self, method: str, path: str, payload=None):
+ binary = isinstance(payload, bytes)
+ data = payload if binary or payload is None else json.dumps(payload).encode("utf-8")
+ with self.client.open(
+ self.repository.api + path, method=method, data=data,
+ content_type="application/octet-stream" if binary else "application/json",
+ ) as response:
+ raw = response.read(4 * MAX_MANIFEST_BYTES + 1)
+ if len(raw) > 4 * MAX_MANIFEST_BYTES:
+ raise ValueError("Gitea response exceeds maximum size")
+ return json.loads(raw) if raw else None
+
+ def preflight(self, publication: FirmwarePublication) -> dict:
+ publication.validate()
+ return self._entry(publication, sha256_file(publication.path))
+
+ def _entry(self, publication: FirmwarePublication, digest: str) -> dict:
+ tag = firmware_release_tag(publication)
+ # Different bytes never replace an asset used by an already published row.
+ asset_name = f"{publication.path.stem}-{digest}{publication.path.suffix.lower()}"
+ url = f"{self.repository.web}/releases/download/{quote(tag, safe='')}/{quote(asset_name, safe='')}"
+ entry = firmware_release_entry(publication, url, digest)
+ update_firmware_manifest({}, entry)
+ return entry
+
+ def publish(self, publication: FirmwarePublication) -> dict:
+ publication.validate()
+ # Snapshot the bytes once so a concurrent rebuild cannot change the upload.
+ with publication.path.open("rb") as source:
+ image = source.read(MAX_FIRMWARE_BYTES + 1)
+ if not image or len(image) > MAX_FIRMWARE_BYTES:
+ raise ValueError("Firmware is empty or exceeds maximum size")
+ entry = self._entry(publication, hashlib.sha256(image).hexdigest())
+ tag = firmware_release_tag(publication)
+ tag_path = f"/releases/tags/{quote(tag, safe='')}"
+ try:
+ release = self._call("GET", tag_path)
+ except HTTPError as error:
+ if error.code != 404:
+ raise
+ error.close()
+ release = self._call("POST", "/releases", {
+ "tag_name": tag, "target_commitish": self.repository.branch,
+ "name": f"{publication.product} {publication.version_name}",
+ "body": publication.notes, "draft": False, "prerelease": False,
+ })
+ asset_name = urlsplit(entry["imageUrl"]).path.rsplit("/", 1)[1]
+ assets_path = f"/releases/{int(release['id'])}/assets"
+ # Gitea assets are paginated; do not silently miss an existing image.
+ found = False
+ page = 1
+ while True:
+ assets = self._call("GET", f"{assets_path}?limit=50&page={page}")
+ if any(asset["name"] == unquote(asset_name) for asset in assets):
+ found = True
+ break
+ if len(assets) < 50:
+ break
+ page += 1
+ if not found:
+ self._call("POST", f"{assets_path}?name={asset_name}", image)
+ # Always verify against the server, never against a previous local cache.
+ with tempfile.TemporaryDirectory(prefix="firmware-verify-") as cache:
+ reader = FirmwareDatabase(self.repository.manifest_url, Path(cache), client=self.client)
+ parsed = parse_firmware_catalog(
+ json.dumps({"firmware": [entry]}).encode(), self.repository.manifest_url)[0]
+ reader.download(parsed)
+
+ path = f"/contents/{quote(self.repository.manifest_path, safe='/')}"
+ source = self._call("GET", f"{path}?ref={quote(self.repository.branch, safe='')}")
+ manifest = json.loads(base64.b64decode(source["content"]))
+ if not isinstance(manifest, dict):
+ raise ValueError("Manifest must be a JSON object")
+ updated = update_firmware_manifest(manifest, entry)
+ if updated != manifest:
+ self._call("PUT", path, {
+ "branch": self.repository.branch, "sha": source["sha"],
+ "message": f"Publish firmware {publication.product} {publication.version_name}",
+ "content": base64.b64encode(json.dumps(updated, ensure_ascii=False).encode()).decode(),
+ })
+ # Use the contents API to avoid a stale raw-file CDN cache on readback.
+ check = self._call("GET", f"{path}?ref={quote(self.repository.branch, safe='')}")
+ rows = parse_firmware_catalog(base64.b64decode(check["content"]), self.repository.manifest_url)
+ if parsed not in rows:
+ raise RuntimeError("Published catalog readback does not match the release")
+ return entry
diff --git a/python/tests/test_altera_logic.py b/python/tests/test_altera_logic.py
new file mode 100644
index 0000000..55d86d9
--- /dev/null
+++ b/python/tests/test_altera_logic.py
@@ -0,0 +1,57 @@
+"""Golden RTL vectors through the Python FFI (no Python production codec)."""
+import tempfile
+import unittest
+from pathlib import Path
+from altera_logic import NativeAnalyzer
+
+
+class AnalyzerTests(unittest.TestCase):
+ def test_rtl_info_and_first_configuration_vector(self):
+ core = NativeAnalyzer()
+ self.assertEqual(core.next_request(), bytes.fromhex("A5 01 00 00 00 A4"))
+ self.assertEqual(core.next_request(), b"")
+ for byte in bytes.fromhex("5A 81 00 10 00 10 32 01 E8"):
+ core.feed(bytes([byte]))
+ self.assertEqual(core.state, core.READY)
+ with self.assertRaises(ValueError):
+ core.start(49, 1, 0, 1, 1)
+ core.start(49, 0, 0, 1, 1)
+ self.assertEqual(core.next_request(), bytes.fromhex("A5 02 31 00 00 96"))
+ core.feed(bytes.fromhex("5A 82 00 D8"))
+ self.assertEqual(core.next_request(), bytes.fromhex("A5 03 00 00 00 A6"))
+
+ def test_error_response_is_short_even_for_info(self):
+ core = NativeAnalyzer()
+ core.next_request()
+ core.feed(bytes.fromhex("5A 81 01 DA"))
+ self.assertEqual(core.state, core.ERROR)
+ self.assertEqual(core.get(1), 3)
+
+ def test_partial_response_times_out_without_retry(self):
+ core = NativeAnalyzer()
+ core.next_request()
+ core.feed(bytes.fromhex("5A 81 00"))
+ core.tick(1000)
+ self.assertEqual(core.get(1), 4)
+ self.assertEqual(core.next_request(), b"")
+
+ def test_demo_export_uses_capture_rate_not_current_ui_settings(self):
+ core = NativeAnalyzer()
+ core.reset(demo=True)
+ core.start(49)
+ capture = core.capture()
+ self.assertEqual(len(capture.samples), 4096)
+ self.assertEqual(capture.sample_rate, 1000000)
+ self.assertEqual(capture.trigger_index, 2048)
+ self.assertTrue(capture.demo)
+ with tempfile.TemporaryDirectory() as directory:
+ path = Path(directory)/"capture.csv"
+ capture.save_csv(path)
+ rows = path.read_text().splitlines()
+ self.assertEqual(len(rows), 4097)
+ self.assertIn("time_s", rows[0])
+ self.assertEqual(rows[2049].split(",")[1], "0.0")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/python/tests/test_altera_stream.py b/python/tests/test_altera_stream.py
new file mode 100644
index 0000000..a312f5a
--- /dev/null
+++ b/python/tests/test_altera_stream.py
@@ -0,0 +1,92 @@
+import tempfile
+import unittest
+from pathlib import Path
+from altera_logic.stream import NativeStream
+
+
+class StreamTests(unittest.TestCase):
+ def test_can_uart_identical_and_duplicates(self):
+ source, can, uart = NativeStream(), NativeStream(), NativeStream()
+ self.assertEqual((source.device_type, source.device_id, source.device_name), (6, 14, "Altera Logic"))
+ ident, data = source.metadata_packet(7, 1000000)
+ self.assertEqual(ident, 0x1EE3FE00)
+ self.assertEqual(data, bytes.fromhex("01 4C 07 00 40 42 0F 00"))
+ can.feed_can(ident, data)
+ _, raw = source.metadata_packet(7, 1000000, uart=True)
+ for byte in raw:
+ uart.feed_uart(bytes([byte]))
+ for i in range(100):
+ ident, data = source.data_packet(7, i, i)
+ can.feed_can(ident, data)
+ _, raw = source.data_packet(7, i, i, uart=True)
+ uart.feed_uart(raw)
+ can.feed_uart(raw) # Duplicate publication through the second transport.
+ self.assertEqual(can.snapshot(), uart.snapshot())
+ self.assertEqual(can.stats["duplicates"], 100)
+ self.assertEqual(can.stats["received"], 100)
+
+ def test_gap_old_session_and_counter_wrap(self):
+ s = NativeStream()
+ s.feed_can(*s.metadata_packet(10, 1000000))
+ s.feed_can(*s.data_packet(10, 0xfffffffe, 1))
+ s.feed_can(*s.data_packet(10, 1, 2))
+ snap = s.snapshot()
+ self.assertEqual(snap.indices, (0xfffffffe, 0x100000001))
+ self.assertEqual(s.stats["missing"], 2)
+ self.assertEqual(snap.breaks, (1, 1))
+ s.feed_can(*s.metadata_packet(11, 2000000))
+ s.feed_can(*s.data_packet(10, 5, 3))
+ s.feed_can(*s.metadata_packet(10, 1000000))
+ self.assertEqual(s.stats["session"], 11)
+ self.assertEqual(s.snapshot().samples, ())
+ s.feed_can(*s.data_packet(11, 0, 0x8001))
+ self.assertEqual(s.snapshot().samples, (0x8001,))
+
+ def test_malformed_filter_crc_and_recovery(self):
+ s = NativeStream()
+ ident, meta = s.metadata_packet(1, 1000)
+ s.feed_can(ident, meta, extended=False)
+ s.feed_can(ident, meta, remote=True)
+ s.feed_can(ident ^ (1 << 20), meta)
+ self.assertFalse(s.stats["has_meta"])
+ raw = bytearray(s.metadata_packet(1, 1000, uart=True)[1])
+ raw[-1] ^= 1
+ s.feed_uart(bytes(raw))
+ self.assertEqual(s.stats["crc_errors"], 1)
+ s.feed_uart(s.metadata_packet(1, 1000, uart=True)[1])
+ self.assertTrue(s.stats["has_meta"])
+ ident, data = s.data_packet(1, 0, 5)
+ s.feed_can(ident, data[:-1])
+ self.assertEqual(s.stats["invalid"], 1)
+ self.assertEqual(s.stats["count"], 0)
+
+ def test_bounded_history_and_export(self):
+ s = NativeStream()
+ for _ in range(3):
+ s.demo_step(4096)
+ snap = s.snapshot(demo=True)
+ self.assertEqual(len(snap.samples), 8192)
+ self.assertEqual(snap.indices[0], 4096)
+ with tempfile.TemporaryDirectory() as directory:
+ path = Path(directory)/"stream.csv"
+ snap.save_csv(path)
+ rows = path.read_text().splitlines()
+ self.assertEqual(len(rows), 8193)
+ self.assertIn("gap_before", rows[0])
+ self.assertEqual(rows[1].split(",")[1], "4096")
+
+ def test_sparse_trace_does_not_connect_across_lost_samples(self):
+ s = NativeStream()
+ s.feed_can(*s.metadata_packet(1, 1000000))
+ for index, value in ((0, 0), (1, 1), (4, 0)):
+ s.feed_can(*s.data_packet(1, index, value))
+ trace = s.snapshot().traces[0]
+ self.assertEqual(trace[0], (0, 0, 1))
+ self.assertIn((1, 0, 0), trace)
+ self.assertIn((1, 1, 0), trace)
+ self.assertIn((4, 0, 1), trace)
+ self.assertNotIn((4, 1, 0), trace)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/python/tests/test_firmware_clients.py b/python/tests/test_firmware_clients.py
new file mode 100644
index 0000000..e829ff7
--- /dev/null
+++ b/python/tests/test_firmware_clients.py
@@ -0,0 +1,135 @@
+"""Android wire compatibility and complete host-side flashing transactions."""
+from dataclasses import replace
+from types import SimpleNamespace
+import struct
+import zlib
+
+import pytest
+
+from protocan import tms_firmware as tms
+from protocan.can_boot import CanBootTarget, CanBootTransfer
+from protocan.protocan import ProtoCanId
+from protocan.transport import build_frame
+
+
+def reply(command, payload=b"", tail=2):
+ return tms.packet(10, command, payload) + bytes(tail)
+
+
+def test_android_packet_layout_and_crc():
+ assert tms.crc16_modbus(b"123456789") == 0x4B37
+ assert tms.init_load(10, 0xA0000, 0x1234)[:10] == bytes.fromhex("0a3a00000a0034120000")
+ assert tms.load_data(10, b"\1\2\3\4")[:6] == bytes.fromhex("0a3301020304")
+ assert tms.tflash(10, 0xA0000, 0x110000, 0x123456)[:14] == bytes.fromhex("0a3700000a000000110056341200")
+ assert tms.extend(10, 0xA0000, 0, 2, 10, 3)[:16] == bytes.fromhex("0a3c00000a0000000000020000000a03")
+
+
+@pytest.mark.parametrize("command,payload,tail", [(58, b"", 2), (52, b"abcdef", 4)])
+def test_only_reserved_tail_may_be_missing(command, payload, tail):
+ full = reply(command, payload, tail)
+ for n in range(tail + 1):
+ assert tms.normalize_reply(full[:len(full)-n], 10, command, len(full)) == full
+ assert tms.normalize_reply(full[:len(full)-tail-1], 10, command, len(full)) is None
+ corrupt = bytearray(full)
+ corrupt[2] ^= 1
+ assert tms.normalize_reply(bytes(corrupt), 10, command, len(full)) is None
+ assert tms.normalize_reply(full, 11, command, len(full)) is None
+
+
+def run_tms(data, target, corrupt=False):
+ plan = tms.programming_steps(data, target)
+ response = None
+ requests = []
+ while True:
+ try:
+ request, size, timeout, title, percent = plan.send(response)
+ except StopIteration:
+ return requests
+ requests.append(request)
+ command = request[1]
+ if command == 52:
+ address, count = struct.unpack_from("//`. Одинаковые имена разных образов не
+ конфликтуют. Проверенный кэш используется повторно; незавершённые временные
+ файлы удаляются. Результат появляется только после совпадения SHA-256.
+- Только HTTPS. Учётные данные отправляются лишь исходному origin (схема,
+ хост, порт); на сторонний CDN и при смене origin в redirect не передаются.
+ Redirect для запросов записи отклоняется.
+- Публикация использует asset с SHA-256 в имени: новые байты не заменяют файл,
+ на который ещё ссылается старый каталог. `fileName` в каталоге остаётся
+ исходным именем. В этом деталь реализации отличается от старого публикатора
+ SETGUI, но формат каталога совместим.
+- Каталог меняется только после обратного скачивания образа. Запись проверяет
+ ревизию файла (`sha`). При конфликте операция завершается ошибкой: повторите
+ выпуск после проверки причины. Загруженный asset может остаться без записи
+ каталога; повторный запуск использует его и снова проверит скачивание.
+- Повтор той же записи не увеличивает `catalogVersion`. Обновление одного
+ `product + versionCode + transport` заменяет запись. Полный rollback и
+ автоматическая очистка старых assets не выполняются.
+- Текущий upload держит образ в памяти (лимит 128 MiB), чтобы параллельная
+ пересборка не подменила байты между вычислением хэша и отправкой.
+
+## Проверка
+
+Из `templates`:
+
+```powershell
+$env:PYTHONPATH = "$PWD/python"
+python -m unittest discover -s python/tests -p test_firmware_database.py -v
+python -m unittest discover -s python/tests -p test_firmware_publish.py -v
+```
+
+Тесты используют сервер в памяти: каталог → публикация → скачивание, повторный
+выпуск, сохранение других разделов, ошибки SHA-256 и конфликты ревизии,
+изоляцию авторизации при redirect. Проверка реальной Gitea и загрузка на МК
+в эти тесты не входят. Перед производственным подключением проверьте выпуск
+в тестовом репозитории вашей Gitea и чтение каталога целевым GUI.
diff --git a/tools/firmware-publish/PORTING.md b/tools/firmware-publish/PORTING.md
new file mode 100644
index 0000000..fba9f82
--- /dev/null
+++ b/tools/firmware-publish/PORTING.md
@@ -0,0 +1,164 @@
+# Подключение публикации прошивки к новому проекту
+
+**Дополнение:** полная независимая библиотека базы прошивок теперь реализована
+в `python/setprotocol/firmware_database.py`, с CLI `firmware_db.py`.
+Чтение, скачивание, публикация и перенос GUI описаны в [`DATABASE.md`](DATABASE.md).
+Ниже сохранено описание интеграции через прежний BAT и SETGUI.
+
+## Что уже реализовано
+
+По локальным исходникам на 19 сентября 2026 года общий механизм уже есть.
+Создавать вторую библиотеку для той же схемы SETGUI не требуется:
+
+| Компонент | Назначение | Зависимость |
+|---|---|---|
+| `c/firmware-info` | Версия реально запущенного образа, 12 слов / 24 байта | C-config проекта, без сети |
+| `python/setprotocol/firmware_publish.py` | Проверка параметров файла, SHA-256, release tag, запись и обновление каталога | Python stdlib, `firmware_catalog.py` |
+| `python/setprotocol/firmware_catalog.py` | Модель и чтение `firmware.releases` | Python stdlib |
+| `tools/firmware-publish/PUBLISH_FIRMWARE.bat` | Конфигурация IDE, транспорты, проверка Git, запуск выпуска | Windows, checkout SETGUI и его `.venv` |
+| `SETGUI/scripts/publish_firmware.py` | Загрузка в Gitea, скачивание и проверка SHA-256, запись и повторное чтение `update.json` | Общие Python-модули, API и учётные данные SETGUI |
+
+Модуль `firmware_publish.py` сам не выполняет HTTP-запросы. Старый общий BAT
+использует сетевой публикатор SETGUI. Для автономного CI без SETGUI теперь
+используйте `GiteaFirmwarePublisher` из `firmware_database.py` или CLI
+`firmware_db.py`; одного `firmware_publish.py` для загрузки недостаточно.
+
+```text
+проект МК → сборка .hex/.bin → общий BAT → SETGUI/scripts/publish_firmware.py
+ ↓
+ setprotocol.firmware_publish
+ ↓
+ Gitea release asset → SHA-256 → update.json
+
+проект МК → firmware-info → обработчик версии → транспорт → клиент
+```
+
+Эти две цепочки не синхронизируют версии автоматически. Bootloader и запись
+Flash — отдельные компоненты (`rs485-boot`, `protocan-boot`, `set-protocol`);
+публикация файла не добавляет поддержку обновления в устройство.
+
+## Фактические подключения в этой рабочей папке
+
+- `KONOR_ds18b20/src/main.c`: `app_send_firmware_info()` и команда `0x03`.
+ `mdk/SETGUI_DS18B20.uvprojx` подключает оба C-файла через `..\lib\c`, config
+ через `..\inc`, generated через `mdk/Generated`, генератор — Before Build.
+- В текущем checkout `KONOR_ds18b20/lib/c` является Windows junction на общий
+ `newProject/templates/c`. Это локальная связь; её нельзя считать переносимым
+ путём для другого компьютера. `.gitmodules` при этом декларирует `templates`
+ в `lib`: перед переносом нужно сверить реальную раскладку, а не копировать
+ пути из Keil вслепую. Корневой `firmware-release.cmd` у KONOR при проверке
+ отсутствует.
+- SETGUI импортирует модули публикации из `SETGUI/third_party/templates/python`.
+ Изменение соседнего `newProject/templates/python` само по себе эту копию не
+ обновляет. Версию зависимости в SETGUI обновляют отдельно.
+- `SETGUI/src/gui_desktop/firmware_update.py` читает каталог через общий parser.
+ Поиск по текущему `SETGUI/src` не обнаружил обработчика `FIRMWARE_INFO` /
+ `firmware_info`: упаковка совместимого ответа ещё не означает, что эта версия
+ клиента запрашивает и показывает расширенный контракт KONOR.
+- В исходниках сетевого публикатора задан Gitea `https://git.rd12.ru`, владелец
+ `setcorp`, репозиторий `SETRD12-Releases`, ветка `main`, файл `update.json`.
+ Это настройки кода, а не результат проверки доступности сервера.
+
+## 1. Выбрать раскладку
+
+Для нового проекта используйте явное расположение зависимости:
+
+```text
+workspace/
+ SETGUI/
+ PUBLISH_FIRMWARE.bat
+ .venv/Scripts/python.exe
+ MyFirmware/
+ firmware-release.cmd
+ lib/templates/tools/firmware-publish/PUBLISH_FIRMWARE.bat
+ mdk/build/application.hex
+```
+
+Подключите `templates` принятой в проекте системой зависимостей. Скопируйте
+только конфигурацию из `examples/keil-firmware-release.cmd` или
+`examples/ccs12-firmware-release.cmd` в корень прошивки. Общий BAT оставьте
+в зависимости.
+
+## 2. Заполнить firmware-release.cmd
+
+Пример для приложения STM32 с bootloader; адрес `0x08003000` является примером
+и должен совпасть с linker script и настройками загрузчика вашего устройства:
+
+```bat
+@echo off
+set "FW_PROJECT_ROOT=%~dp0"
+set "FW_SETGUI_ROOT=%~dp0..\SETGUI"
+set "FW_PRODUCT=MY_DEVICE"
+set "FW_VERSION=1.2.3"
+set "FW_VERSION_CODE=0x00010203"
+set "FW_TRANSPORTS=can rs485"
+set "FW_BASE_ADDRESS=0x08003000"
+set "FW_IMAGE=mdk\build\application.hex"
+set "FW_NOTES=Firmware 1.2.3"
+```
+
+`FW_PRODUCT` должен соответствовать идентификатору изделия, с которым клиент
+выбирает прошивку. Сверьте `FW_VERSION` с C-config; при упаковке версии в три
+8-битных поля используйте части 0–255. Укажите только реально реализованные
+транспорты. Parser допускает `can`, `rs485`, `stm32`, `tms`; для их применения
+нужна соответствующая поддержка клиента и загрузчика.
+
+Все транспорты одного запуска используют **один и тот же файл**. Если образы
+для CAN и RS-485 отличаются, заведите разные конфигурации с разными именами
+файлов и запускайте их отдельно. Одинаковое имя asset в одном release tag
+нельзя использовать для разных образов.
+
+Для CCS 12 создайте SCI8 boot `.bin` через C2000 Hex Utility (`--binary --boot
+--sci8`), задайте `FW_TRANSPORTS=tms` и очистите адрес:
+`set "FW_BASE_ADDRESS="`. `.out` и `.axf` публикатор не преобразует.
+
+## 3. Проверить и выпустить
+
+Подготовьте окружение SETGUI по документации этого проекта. Учётные данные
+Gitea сохраняются через окно SETGUI «Версия и обновление»; в `.cmd` они не нужны.
+
+Из корня MyFirmware в cmd.exe:
+
+```bat
+call "lib\templates\tools\firmware-publish\PUBLISH_FIRMWARE.bat" --config "firmware-release.cmd" --preflight
+```
+
+Для post-build в `mdk` используйте пути `..\lib\templates\...` и
+`--config "..\firmware-release.cmd"`. В post-build оставляйте `--preflight`.
+
+Preflight проверяет существование, расширение, размер, метаданные, SHA-256 и
+схему записи каталога. Он **не разбирает содержимое Intel HEX/SCI8**, не сверяет
+версию с C-config, адреса с linker script и файл с текущими исходниками.
+Успех preflight не заменяет Rebuild и загрузку на тестовое устройство.
+
+После проверки конкретного Release-образа и фиксации его исходников:
+
+```bat
+call "lib\templates\tools\firmware-publish\PUBLISH_FIRMWARE.bat" --config "firmware-release.cmd" --publish
+```
+
+Скрипт запросит подтверждение. Успех означает загрузку asset, проверку скачанных
+байтов, обновление каталога и его повторное чтение. Несколько транспортов
+публикуются последовательно, общей транзакции для всего запуска нет. Откройте
+базу прошивок SETGUI, обновите каталог и проверьте загрузку на тестовое устройство.
+
+## 4. Другой сервер или собственный инструмент
+
+`FW_SETGUI_ROOT` выбирает checkout SETGUI, а не адрес Gitea. Переменная
+`SETGUI_UPDATE_MANIFEST_URL` меняет URL чтения каталога, но не перенастраивает
+`WEB_ROOT`, `OWNER`, `RELEASE_REPO` сетевого публикатора. Для другого сервера
+нужно согласованно перенести сетевой адаптер, адреса чтения и авторизацию.
+
+Для собственного Python-инструмента добавьте `templates/python` в import path
+и используйте `FirmwarePublication`, `sha256_file`, `firmware_release_tag`,
+`firmware_release_entry`, `update_firmware_manifest` из
+`setprotocol.firmware_publish`. Вызывайте `publication.validate()` до создания
+записи. Сетевой адаптер должен обеспечить тот же порядок, что SETGUI:
+
+1. Загрузить образ в release и скачать его обратно, сверить SHA-256.
+2. Прочитать актуальный manifest, объединить через `update_firmware_manifest`.
+3. Записать manifest с проверкой исходной ревизии, чтобы не затереть чужое
+ параллельное изменение; конфликт требует повторного чтения и объединения.
+4. Перечитать каталог через `parse_firmware_catalog` и проверить запись выпуска.
+
+Не переносите пароли из профиля SETGUI в исходники или конфигурации проекта.
diff --git a/tools/firmware-publish/README.md b/tools/firmware-publish/README.md
index 9c27223..1111ea0 100644
--- a/tools/firmware-publish/README.md
+++ b/tools/firmware-publish/README.md
@@ -1,10 +1,20 @@
# Публикация прошивки из Keil и CCS 12
+**Самостоятельная база прошивок без SETGUI:**
+[`DATABASE.md`](DATABASE.md) — библиотека и CLI для чтения каталога,
+скачивания и публикации образов. Ниже описан прежний BAT-путь через SETGUI.
+
+Архитектура, фактические зависимости и пошаговое подключение нового проекта:
+[`PORTING.md`](PORTING.md). Общая Python-библиотека подготовки каталога уже
+находится в `python/setprotocol/firmware_publish.py`; сетевую загрузку выполняет
+SETGUI.
+
Этот комплект связывает проект прошивки с каталогом SETGUI. IDE по-прежнему
собирает штатный загрузочный файл, а `PUBLISH_FIRMWARE.bat`:
1. читает метаданные выпуска из `firmware-release.cmd`;
-2. проверяет наличие и формат `.hex` или `.bin`;
+2. проверяет наличие, расширение и размер `.hex` или `.bin` (содержимое
+ Intel HEX/SCI8 и совместимость с устройством не проверяются);
3. запускает локальную проверку SETGUI (`--preflight`) либо публикацию
(`--publish`);
4. для каждого транспорта загружает образ в Gitea, скачивает его обратно,
diff --git a/tools/firmware-publish/firmware_db.py b/tools/firmware-publish/firmware_db.py
new file mode 100644
index 0000000..53fdaa1
--- /dev/null
+++ b/tools/firmware-publish/firmware_db.py
@@ -0,0 +1,82 @@
+"""Standalone firmware database CLI; requires Python 3.10+, no SETGUI."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import sys
+from dataclasses import asdict
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "python"))
+
+from setprotocol.firmware_database import (
+ Credentials, FirmwareDatabase, GiteaFirmwarePublisher, GiteaRepository,
+)
+from setprotocol.firmware_publish import FirmwarePublication, SUPPORTED_TRANSPORTS
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--server", required=True, help="Gitea HTTPS URL")
+ parser.add_argument("--owner", required=True)
+ parser.add_argument("--repo", required=True)
+ parser.add_argument("--branch", default="main")
+ parser.add_argument("--manifest", default="update.json")
+ commands = parser.add_subparsers(dest="command", required=True)
+ catalog = commands.add_parser("list")
+ catalog.add_argument("--product")
+ catalog.add_argument("--transport", choices=sorted(SUPPORTED_TRANSPORTS))
+ download = commands.add_parser("download")
+ download.add_argument("--product", required=True)
+ download.add_argument("--version-code", type=lambda v: int(v, 0), required=True)
+ download.add_argument("--transport", choices=sorted(SUPPORTED_TRANSPORTS), required=True)
+ download.add_argument("--cache", type=Path, default=Path("firmware"))
+ for name in ("preflight", "publish"):
+ command = commands.add_parser(name)
+ command.add_argument("--file", type=Path, required=True)
+ command.add_argument("--product", required=True)
+ command.add_argument("--version", required=True)
+ command.add_argument("--version-code", type=lambda v: int(v, 0), required=True)
+ command.add_argument("--transport", choices=sorted(SUPPORTED_TRANSPORTS), required=True)
+ command.add_argument("--base-address", type=lambda v: int(v, 0))
+ command.add_argument("--notes", default="")
+ args = parser.parse_args()
+ try:
+ repository = GiteaRepository(args.server, args.owner, args.repo, args.branch, args.manifest)
+ login = os.environ.get("FIRMWARE_DB_LOGIN")
+ password = os.environ.get("FIRMWARE_DB_PASSWORD")
+ credentials = Credentials(login, password) if login and password else None
+ if args.command in ("list", "download"):
+ database = FirmwareDatabase(repository.manifest_url,
+ getattr(args, "cache", Path("firmware")),
+ credentials=credentials)
+ rows = database.read_catalog(product=args.product, transport=args.transport)
+ if args.command == "list":
+ print(json.dumps([asdict(row) for row in rows], ensure_ascii=False, indent=2))
+ else:
+ rows = [row for row in rows if row.version_code == args.version_code]
+ if len(rows) != 1:
+ raise ValueError("Expected exactly one matching firmware release")
+ print(database.download(rows[0]))
+ else:
+ publication = FirmwarePublication(args.file.resolve(), args.product, args.version,
+ args.version_code, args.transport,
+ args.base_address, args.notes)
+ publisher = GiteaFirmwarePublisher(repository, credentials)
+ if args.command == "publish":
+ if credentials is None:
+ raise ValueError("Set FIRMWARE_DB_LOGIN and FIRMWARE_DB_PASSWORD for publication")
+ result = publisher.publish(publication)
+ else:
+ result = publisher.preflight(publication)
+ print(json.dumps(result, ensure_ascii=False, indent=2))
+ except (OSError, ValueError, RuntimeError) as error:
+ print(f"Firmware database: {error}", file=sys.stderr)
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
From 79411dbc26a9a9386f3e43e83514a74aadaa9bfd Mon Sep 17 00:00:00 2001
From: andrey
Date: Mon, 28 Sep 2026 11:54:17 +0300
Subject: [PATCH 37/37] Add shared emulator services, waveform generation and
STM32 USB boot ports
---
c/set-protocol/CMakeLists.txt | 15 ++
c/set-protocol/README.md | 3 +
c/set-protocol/docs/EMULATOR_SERVICES.md | 228 +++++++++++++++++
c/set-protocol/docs/SIGNAL_GENERATOR.md | 126 ++++++++++
c/set-protocol/include/set_boot.h | 26 ++
c/set-protocol/include/set_emu_igbt.h | 32 +++
c/set-protocol/include/set_emu_server.h | 48 ++++
c/set-protocol/include/set_emu_tms.h | 22 ++
c/set-protocol/include/set_emu_ump.h | 25 ++
c/set-protocol/include/set_regmap.h | 48 ++++
c/set-protocol/include/set_wavegen.h | 59 +++++
c/set-protocol/include/setprotocol.h | 2 +
c/set-protocol/include/ump_protocol.h | 55 +++++
.../ports/example-emulator/service.c | 41 +++
.../stm32-usb-boot/boot_usb_protocol.inc | 54 ++++
.../ports/stm32f407-periph/README.md | 13 +
.../ports/stm32f407-periph/ump_stm32f407.c | 30 +++
.../ports/stm32f407-periph/ump_stm32f407.h | 11 +
.../ports/stm32f407-usb-boot/README.md | 68 +++++
.../stm32f407-usb-boot/boot_config.template.h | 9 +
.../ports/stm32f407-usb-boot/boot_flash.c | 88 +++++++
.../ports/stm32f407-usb-boot/boot_image.h | 19 ++
.../ports/stm32f407-usb-boot/boot_main.c | 63 +++++
.../ports/stm32f407-usb-boot/boot_port.h | 13 +
.../ports/stm32f407-usb-boot/boot_request.c | 26 ++
.../stm32f407-usb-boot/boot_usb_stream.c | 10 +
.../stm32f407-usb-boot/test_boot_image.c | 34 +++
.../ports/stm32f407-wavegen/set_wave_f407.c | 53 ++++
.../ports/stm32f407-wavegen/set_wave_f407.h | 12 +
.../ports/stm32g474-periph/README.md | 34 +++
c/set-protocol/ports/stm32g474-periph/board.c | 109 ++++++++
c/set-protocol/ports/stm32g474-periph/main.c | 39 +++
.../ports/stm32g474-periph/transport.c | 163 ++++++++++++
.../ports/stm32g474-periph/wave_hal.c | 116 +++++++++
.../ports/stm32g474-usb-boot/README.md | 47 ++++
.../stm32g474-usb-boot/boot_config.template.h | 5 +
.../ports/stm32g474-usb-boot/boot_flash.c | 109 ++++++++
.../ports/stm32g474-usb-boot/boot_image.h | 34 +++
.../ports/stm32g474-usb-boot/boot_main.c | 39 +++
.../ports/stm32g474-usb-boot/boot_port.h | 15 ++
.../ports/stm32g474-usb-boot/boot_request.c | 26 ++
.../ports/stm32g474-usb-boot/boot_usb_port.c | 94 +++++++
.../stm32g474-usb-boot/boot_usb_stream.c | 10 +
.../ports/stm32g474-usb-boot/boot_writer.h | 37 +++
.../ports/stm32g474-usb-boot/test_boot_port.c | 97 ++++++++
c/set-protocol/src/set_boot.c | 76 ++++++
c/set-protocol/src/set_emu_igbt.c | 142 +++++++++++
c/set-protocol/src/set_emu_server.c | 194 +++++++++++++++
c/set-protocol/src/set_emu_tms.c | 96 ++++++++
c/set-protocol/src/set_emu_ump.c | 172 +++++++++++++
c/set-protocol/src/set_regmap.c | 117 +++++++++
c/set-protocol/src/set_wavegen.c | 214 ++++++++++++++++
c/set-protocol/src/ump_protocol.c | 105 ++++++++
c/set-protocol/tests/test_boot.c | 70 ++++++
c/set-protocol/tests/test_emulator_services.c | 233 ++++++++++++++++++
c/set-protocol/tools/build_host.py | 1 +
56 files changed, 3627 insertions(+)
create mode 100644 c/set-protocol/docs/EMULATOR_SERVICES.md
create mode 100644 c/set-protocol/docs/SIGNAL_GENERATOR.md
create mode 100644 c/set-protocol/include/set_boot.h
create mode 100644 c/set-protocol/include/set_emu_igbt.h
create mode 100644 c/set-protocol/include/set_emu_server.h
create mode 100644 c/set-protocol/include/set_emu_tms.h
create mode 100644 c/set-protocol/include/set_emu_ump.h
create mode 100644 c/set-protocol/include/set_regmap.h
create mode 100644 c/set-protocol/include/set_wavegen.h
create mode 100644 c/set-protocol/include/ump_protocol.h
create mode 100644 c/set-protocol/ports/example-emulator/service.c
create mode 100644 c/set-protocol/ports/stm32-usb-boot/boot_usb_protocol.inc
create mode 100644 c/set-protocol/ports/stm32f407-periph/README.md
create mode 100644 c/set-protocol/ports/stm32f407-periph/ump_stm32f407.c
create mode 100644 c/set-protocol/ports/stm32f407-periph/ump_stm32f407.h
create mode 100644 c/set-protocol/ports/stm32f407-usb-boot/README.md
create mode 100644 c/set-protocol/ports/stm32f407-usb-boot/boot_config.template.h
create mode 100644 c/set-protocol/ports/stm32f407-usb-boot/boot_flash.c
create mode 100644 c/set-protocol/ports/stm32f407-usb-boot/boot_image.h
create mode 100644 c/set-protocol/ports/stm32f407-usb-boot/boot_main.c
create mode 100644 c/set-protocol/ports/stm32f407-usb-boot/boot_port.h
create mode 100644 c/set-protocol/ports/stm32f407-usb-boot/boot_request.c
create mode 100644 c/set-protocol/ports/stm32f407-usb-boot/boot_usb_stream.c
create mode 100644 c/set-protocol/ports/stm32f407-usb-boot/test_boot_image.c
create mode 100644 c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.c
create mode 100644 c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.h
create mode 100644 c/set-protocol/ports/stm32g474-periph/README.md
create mode 100644 c/set-protocol/ports/stm32g474-periph/board.c
create mode 100644 c/set-protocol/ports/stm32g474-periph/main.c
create mode 100644 c/set-protocol/ports/stm32g474-periph/transport.c
create mode 100644 c/set-protocol/ports/stm32g474-periph/wave_hal.c
create mode 100644 c/set-protocol/ports/stm32g474-usb-boot/README.md
create mode 100644 c/set-protocol/ports/stm32g474-usb-boot/boot_config.template.h
create mode 100644 c/set-protocol/ports/stm32g474-usb-boot/boot_flash.c
create mode 100644 c/set-protocol/ports/stm32g474-usb-boot/boot_image.h
create mode 100644 c/set-protocol/ports/stm32g474-usb-boot/boot_main.c
create mode 100644 c/set-protocol/ports/stm32g474-usb-boot/boot_port.h
create mode 100644 c/set-protocol/ports/stm32g474-usb-boot/boot_request.c
create mode 100644 c/set-protocol/ports/stm32g474-usb-boot/boot_usb_port.c
create mode 100644 c/set-protocol/ports/stm32g474-usb-boot/boot_usb_stream.c
create mode 100644 c/set-protocol/ports/stm32g474-usb-boot/boot_writer.h
create mode 100644 c/set-protocol/ports/stm32g474-usb-boot/test_boot_port.c
create mode 100644 c/set-protocol/src/set_boot.c
create mode 100644 c/set-protocol/src/set_emu_igbt.c
create mode 100644 c/set-protocol/src/set_emu_server.c
create mode 100644 c/set-protocol/src/set_emu_tms.c
create mode 100644 c/set-protocol/src/set_emu_ump.c
create mode 100644 c/set-protocol/src/set_regmap.c
create mode 100644 c/set-protocol/src/set_wavegen.c
create mode 100644 c/set-protocol/src/ump_protocol.c
create mode 100644 c/set-protocol/tests/test_boot.c
create mode 100644 c/set-protocol/tests/test_emulator_services.c
diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt
index 5165838..02667b5 100644
--- a/c/set-protocol/CMakeLists.txt
+++ b/c/set-protocol/CMakeLists.txt
@@ -11,6 +11,7 @@ set(SETPROTOCOL_V2_SOURCES
src/set_protocol.c
src/set_can.c
src/set_firmware.c
+ src/set_boot.c
src/set_telemetry.c
src/set_plot.c
src/set_trends.c
@@ -36,6 +37,12 @@ set(SETPROTOCOL_LEGACY_SOURCES
)
set(SETPROTOCOL_SOURCES
+ src/set_regmap.c
+ src/set_emu_ump.c
+ src/set_emu_tms.c
+ src/set_emu_igbt.c
+ src/set_emu_server.c
+ src/set_wavegen.c
${SETPROTOCOL_V2_SOURCES}
${SETPROTOCOL_LEGACY_SOURCES}
)
@@ -81,6 +88,14 @@ endif()
option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS)
enable_testing()
+ add_executable(test_emulator_services tests/test_emulator_services.c ports/example-emulator/service.c)
+ target_link_libraries(test_emulator_services PRIVATE setprotocol_static)
+ add_test(NAME emulator_services COMMAND test_emulator_services)
+ if(MSVC)
+ target_compile_options(test_emulator_services PRIVATE /UNDEBUG)
+ else()
+ target_compile_options(test_emulator_services PRIVATE -UNDEBUG)
+ endif()
add_executable(test_altera_logic tests/test_altera_logic.c)
target_link_libraries(test_altera_logic PRIVATE setprotocol_static)
add_test(NAME altera_logic COMMAND test_altera_logic)
diff --git a/c/set-protocol/README.md b/c/set-protocol/README.md
index 8842a43..72f6d94 100644
--- a/c/set-protocol/README.md
+++ b/c/set-protocol/README.md
@@ -1,5 +1,8 @@
# SETProtocol
+Универсальные сервисы эмуляторов: [адресное пространство, API и порт новой платы](docs/EMULATOR_SERVICES.md).
+`set_regmap` объединяет УМП, 2812, IGBT и DAC за общим API для RTU, CAN и других оболочек.
+
Единое переносимое протокольное ядро SET для `SETGUI`, Android GUI, устройств
и сервисных утилит. В одном C99-проекте собраны:
diff --git a/c/set-protocol/docs/EMULATOR_SERVICES.md b/c/set-protocol/docs/EMULATOR_SERVICES.md
new file mode 100644
index 0000000..cbdec5d
--- /dev/null
+++ b/c/set-protocol/docs/EMULATOR_SERVICES.md
@@ -0,0 +1,228 @@
+# Универсальное адресное пространство эмуляторов
+
+Общий код находится в `templates/c/set-protocol`. Прошивка платы задаёт память,
+GPIO и аппаратные callbacks; протокол и модели не включают STM32/HAL/USB/ОС,
+не выделяют heap и не читают системное время. Все вызовы одного экземпляра
+сериализуются главным циклом или блокировкой вызывающего.
+
+## Слои и точка расширения
+
+- `set_regmap.h/.c`: таблица непересекающихся регионов и общий вызов
+ `set_regmap_request(map, function, address, value, out, capacity, &count)`.
+ Function 3 читает `value` слов, function 6 пишет одно слово.
+- `set_emu_ump`, `set_emu_tms`, `set_emu_igbt`, `set_wavegen`:
+ независимые экземпляры моделей. Callbacks получают **относительный offset**,
+ а не внешний адрес. Один модуль можно разместить несколько раз по разным адресам.
+- `set_emu_server`: готовая композиция для текущего стенда, режим УМП/2812,
+ подготовка настроек с применением через 500 тиков.
+- `set_regmap_rtu` и `set_regmap_can`: готовые транспортные кодеки над одной картой.
+ `set_emu_server_rtu/can` дополнительно сохраняют legacy поведение стенда.
+ USB CDC, UART, TCP или иной поток только доставляет полный RTU-кадр.
+ Другой протокол разбирает собственную оболочку и вызывает `set_regmap_request`.
+
+Вызов с неизвестной функцией возвращает 1; неизвестный адрес, RO-запись,
+чтение через границу региона — 2; неверное значение или размер буфера — 3;
+ошибка аппаратного порта — 4; занятый модуль — 6. При ошибке `count=0`.
+Длина успешного чтения равна запрошенной, запись возвращает адрес/значение.
+Максимум — 124 слова. RTU-обёртка требует буфер 253 байта до выполнения команды;
+CAN — 124 слова. Недостаточный буфер не может выполнить запись без ответа.
+При ошибке содержимое выходного буфера не используется.
+
+`set_regmap_init` проверяет границы 16-битного пространства, пересечения,
+права и callbacks. Таблица и состояния принадлежат вызывающему и должны жить
+дольше карты. После регистрации таблица неизменна; для изменения карты
+инициализируйте её заново в момент, когда нет обслуживаемых запросов.
+
+## Каталог и стандартная карта
+
+Каталог доступен на адресе 0 через любой транспорт.
+
+| Слово заголовка | Значение |
+|---|---|
+| 0 | 0x5345, сигнатура SE |
+| 1 | версия каталога 1 |
+| 2 | количество регионов |
+| 3 | начало дескрипторов: 8 |
+| 4 | длина дескриптора: 8 |
+| 5 | максимум слов в одном чтении: 124 |
+| 6, 7 | резерв: 0 |
+
+Дескриптор: `kind, version, instance, base, words, flags, 0, 0`.
+Flags: bit0 чтение, bit1 запись. Права описывают возможности региона;
+отдельные регистры внутри могут быть только для чтения. Пропуски внутри
+региона возвращают ошибку, если модуль их не поддерживает.
+
+| Kind | Стандартный адрес | Размер | Модуль |
+|---|---|---|---|
+| 1 | 0x1000 | 0x17C | УМП: статус, окно, онлайн |
+| 2 | 0x1200 | 36 | управление, конфигурация и indexed capability |
+| 3 | 0x1300 | 8 | генератор DAC |
+| 4 | 0x1400 | 112 | IGBT, версия 3 |
+| 5 | 0x1500 | 80 | 2812 через регистры |
+| 6 | 0x2000 | capacity × 40 | архив/банк УМП |
+| 7 | 0xA000 | wave capacity | таблица DAC |
+
+Эта карта — готовая композиция, а не требование к новым платам.
+`set_regmap` позволяет выбирать адреса, подмножество и несколько экземпляров
+модуля; каталог сообщает фактическую карту. Настройка UART и выбор УМП/2812
+принадлежат только готовому `set_emu_server`. Максимальная ёмкость УМП
+в этой карте — 819 записей на банк; текущая STM32 использует 800.
+
+В режиме 2812 прямое обращение к регистрам УМП возвращает busy, legacy
+RTU/CAN к этим диапазонам игнорируются для совместимости. Остальные сервисы
+и каталог доступны. Регистры управления 2812 принимают запись только в режиме 2812.
+Чтение его состояния не запускает модель.
+
+## IGBT
+
+Предыдущий локальный диапазон IGBT 0x1300 конфликтовал с DAC. Его alias
+сохранить нельзя: он обращается к генератору. Используйте каталог kind=4 или
+новый стандартный адрес 0x1400.
+
+| Offset | Назначение |
+|---|---|
+| 0, 1 | 0x4947, версия 3 |
+| 2 | enable 0/1; выключение сразу очищает состояние RX и ожидаемые ACK |
+| 3 | чтение последнего TX; запись подготовленной TX-маски |
+| 4 | RX-маска |
+| 5 | резерв 0 |
+| 6 | FAULT-маска 0…255 |
+| 7 | задержка ACK 0…1000 мс |
+| 8 | ширина ACK 1…255 мс |
+| 9 | счётчик фронтов modulo 65536; запись 0 сбрасывает |
+| 10 | число каналов 8 |
+| 11 | один шаг модели = 1 мс |
+| 12 | источник TX: 0 переданные GPIO, 1 подготовленная маска |
+| 13 | подготовленная TX-маска, чтение/запись |
+| 14 | запись маски запуска одиночного TX; чтение 0 |
+| 15 | длина записи канала: 12 слов |
+
+Версия 3 сохраняет первые 14 регистров. Общие delay/width при записи
+устанавливают значения для всех каналов; при чтении показывают последнее
+общее задание, а не поканальные значения. Полное состояние: FC03 0x1400 / 112 слов.
+Запуск TX требует enabled=1, source=1, разрешённого TX канала и низкого TX;
+повторный запуск высокого TX отвергается кодом 6. Длительность отсчитывает
+сама модель, не клиент COM. Ручная запись TX отменяет все текущие TX-импульсы.
+Выключение эмуляции отменяет импульсы и сбрасывает их TX; статическое задание
+TX сохраняется. PB остаются входами: тестовый TX — внутренняя подстановка,
+физическая обратная связь RX выводится на PC0…PC7.
+
+Поканальные регистры: `0x1410 + 12 * channel`, channel=0…7.
+
+| Offset канала | Значение |
+|---|---|
+| 0 | flags: bit0 разрешение TX, bit1 разрешение RX, bit2 ACK active-low |
+| 1 | RX mode: 0 ACK, 1 физический 0, 2 физическая 1, 3 пропуск ACK |
+| 2, 3 | задержка ACK ON 0…1000 мс, ширина 1…1000 мс |
+| 4, 5 | задержка ACK OFF 0…1000 мс, ширина 1…1000 мс |
+| 6 | одиночный TX: 1…60000 мс |
+| 7 | маска фронтов для ACK: bit0 ON, bit1 OFF |
+| 8 | ширина FAULT 1…60000 мс; 0 — удержание до снятия FAULT |
+| 9 | RO счётчик фронтов канала, modulo 65536 |
+| 10, 11 | RO оставшаяся задержка / длительность ACK в мс |
+
+Изменение flags/mode отменяет ожидаемый и активный ACK канала.
+Отключённый RX и выключенная модель всегда дают физический 0.
+Инверсия применяется к автоматическому ответу и idle при пропуске ACK;
+ручные уровни 0/1 не инвертируются. Сброс общего счётчика сбрасывает и канальные.
+FAULT моделирует длинный/удерживаемый ответ после фронта; это не модель Vce,
+Vge, DESAT, внутренней защиты или блокировки конкретного силового драйвера.
+
+`set_igbt_tick(state, pins)` получает маску входов от порта платы.
+Фронт на тике T запускает ACK на T+delay. Повторный фронт до начала ACK
+перезапускает задержку; это определённая модельная обработка слишком частых команд.
+Каждый из восьми каналов независим. FAULT удерживает RX после его активации;
+снятие FAULT освобождает только ранее аварийные каналы и сохраняет обычные ACK.
+Чтобы TX через регистры не перезаписывался GPIO, выберите source=1.
+
+**Текущая IGBT-модель имеет шаг 1 мс.** Она проверяет логическую последовательность
+Vin/Vstat, но не воспроизводит ACK 250/380 нс и ширины профилей 1SP/1SD в DSLogic.
+Такую аппаратную точность нельзя получить переносом модели или вызовом из SysTick:
+для неё требуется отдельный быстрый аппаратный порт и соответствующая модель времени.
+
+## 2812
+
+Состояние едино для legacy CMD_STD и адресного API:
+
+| Offset | Назначение |
+|---|---|
+| 0, 1 | 0x544D, версия 1 |
+| 2, 3, 4 | running, частота и задание (int16 ×100 Гц) |
+| 8…11 | четыре подготовленных analog int16 |
+| 12…17 | шесть подготовленных управляющих байт (значение 0…255) |
+| 18 | записать 1: атомарно применить подготовленную команду |
+| 32…45 | 28 байт дискретов; младший байт слова — первый байт |
+| 46…79 | 34 аналоговых int16 |
+| остальные | чтение 0 |
+
+Модель остаётся упрощённой BALZAM_7: Go, выбор remote, Setspeed,
+разгон 10 Гц/с и синтетическая телеметрия. Legacy CMD_STD имеет отдельный
+адрес 2812, запрос 18 и ответ 101 байт, с проверкой CRC до изменения состояния.
+Буфер ответа менее 101 байта запрещает выполнение команды.
+
+## УМП и DAC
+
+УМП сохраняет прежние записи по 40 слов, банк/архив, фронтовой триггер,
+постинтервал 1000 мс и indexed-чтение без изменения курсора. Два разных
+буфера по `capacity*40` слов предоставляет плата. Размещение SRAM/CCM
+не является частью библиотеки.
+
+DAC использует прежний `set_wave_port` с `start/stop`, транзакционную загрузку,
+commit, чтение таблицы и запрет изменений при работе. Регистровые callbacks
+и прежний `set_wave_rtu` вызывают одну реализацию. До повторного init вызывающий
+останавливает аппаратный вывод; библиотека не угадывает текущее состояние DMA.
+
+## Обёртки и новая плата
+
+Пример: [ports/example-emulator/service.c](../ports/example-emulator/service.c).
+Он регистрирует два экземпляра IGBT и показывает прямой, RTU и CAN вызов.
+Порт новой платы должен:
+
+1. Создать состояния и таблицу регионов, вызвать init и проверить результат.
+2. Предоставить буферы и аппаратный DAC-порт, если нужен генератор.
+3. Передавать реальные тики и входы; читать результат модели и обновлять GPIO.
+4. Доставлять запросы через выбранную оболочку в одну карту, сериализуя доступ.
+
+CAN-envelope совместим с UMP: BE address/value/0x1F00/token/function.
+Идентификаторы CAN и маршрутизацию задаёт порт. Ответ делится по два слова,
+нечётное чтение дополняется нулём до чётного числа; клиент отбрасывает последнее
+слово по длине исходного запроса. Токен 1…255; один незавершённый ответ на соединение.
+Проверенная копия слов хранится до отправки всех фрагментов.
+
+`set_emu_stream` — только сборщик известных запросов FC03/FC06 и CMD_STD,
+по одному экземпляру на поток. Он принимает монотонные uint32 миллисекунды,
+сохраняет кадр между USB-пакетами, сбрасывает незавершённый после паузы >1000 мс
+и восстанавливается после плохого CRC. Это не универсальный Modbus-парсер
+FC16/FC23: для другого формата нужна другая тонкая оболочка.
+
+## Сборка и проверки
+
+C99-ядро подключается через `setprotocol_static`/DLL, определения публичного
+API экспортируются через PCAN_ABI_API. Включить `set_regmap.h` или
+`set_emu_server.h`. Для иной архитектуры проверить наличие uint8/16/32_t;
+существующий 16-битный C28x octet-порт UMP остаётся отдельным, этот новый
+байтовый API пока не проверен TI-компилятором.
+
+`test_emulator_services.c` проверяет каталог, пересечения, границы и короткие
+буферы, два экземпляра, переназначение адресов, RTU/CAN/stream, совместное
+состояние 2812 и DAC, длинные задержки IGBT и освобождение FAULT.
+Существующие тесты прошивки дополнительно проверяют архив УМП, USB-фрагментацию,
+смену режима/адресов и совместимость с декодерами SETGUI.
+
+## Выбор единственного эмулятора
+
+В SETGUI: «Эмулятор → Настройки STM → Тип эмулятора», затем
+«Применить в STM и проверить» и «Открыть управление эмулятором».
+Конфигурация версии 3: режим 0 — УМП, 1 — 2812, 2 — IGBT, 3 — ЦАП.
+Регистр 0x1219 подготавливает режим; 0x1218 = 0xA55A применяет его через
+500 мс после подтверждения. Статус FC03 0x1210 / 10 слов содержит версию 3,
+действующий режим в слове 8 и подготовленный в слове 9.
+
+При смене режима останавливаются TIM6/DMA/DAC, ЦАП устанавливается в ноль,
+IGBT отключается и его выходы обнуляются, 2812 сбрасывается, УМП останавливается.
+Новый режим не запускается автоматически. Для IGBT нужно явно включить
+эмуляцию; для ЦАП заново загрузить таблицу и дать команду пуска.
+Повторное применение того же режима не прерывает текущую работу.
+Записи невыбранным IGBT/ЦАП/2812 отклоняются с кодом 6; чтение статуса
+и явные команды остановки IGBT/ЦАП доступны. Настройки связи доступны всегда.
+После сброса выбран УМП, IGBT выключен, ЦАП не запущен.
diff --git a/c/set-protocol/docs/SIGNAL_GENERATOR.md b/c/set-protocol/docs/SIGNAL_GENERATOR.md
new file mode 100644
index 0000000..7c94757
--- /dev/null
+++ b/c/set-protocol/docs/SIGNAL_GENERATOR.md
@@ -0,0 +1,126 @@
+# Генератор произвольного сигнала WG v1
+
+Общее C99-ядро строит кривые по точкам, преобразует напряжения в коды ЦАП и
+принимает таблицы через RTU поверх USB CDC/COM. Оно не зависит от Qt, HAL или ОС.
+Память предоставляет вызывающий; скрытого heap и глобального состояния нет.
+
+```text
+SETGUI: точки -> Python/ctypes -> set_signal.c -> график / CSV / C
+ -> set_wavegen.c -> USB CDC
+MCU: USB stream -> set_wave_rtu -> set_wave_port -> TIM6/DMA/DAC
+```
+
+| Файл | Зависимости и назначение |
+|---|---|
+| `include/set_signal.h`, `src/set_signal.c` | C99/math: МНК 1…5, linear, PCHIP, natural cubic spline, 12-bit DAC |
+| `include/set_wavegen.h`, `src/set_wavegen.c` | C99 + `set_crc.c`: транзакционная загрузка и wire codec |
+| `ports/stm32f407-wavegen` | CMSIS F407: PA4/DAC1, TIM6, DMA1 Stream5 Channel7 |
+| `ports/stm32g474-wavegen` | STM32G4 HAL: готовые DAC/TIM/DMA handles из CubeMX |
+| `python/set_devices/signal_reconstruction.py`, `waveform.py` | Модели, JSON/экспорт, ctypes; алгоритмов на Python нет |
+| `python/set_devices/wavegen_protocol.py`, `qt_ports/wavegen.py` | ctypes-кодек и Qt COM worker с проверкой чтением обратно |
+
+## Интерполяция и таблица
+
+`set_signal_reconstruct` принимает раздельные X/Y и workspace `14*N+128`
+элементов double. Сортировка, усреднение одинаковых времён, нормализация,
+МНК через QR и интерполяция выполняются в C. Максимум 100000 входных/10000
+выходных точек. Переданный workspace может повторно использоваться.
+Большие расчёты предназначены для хоста; на MCU можно строить малые таблицы
+или принимать готовую таблицу без затрат на интерполяцию.
+
+`endpoint=1` включает последний X (анализ логов); `endpoint=0` строит один
+период `[0,T)` без дублированного отсчёта стыка. Частота Fs и период T задают
+целое `N=Fs*T`; для WG максимум N=4096, Fs=1…50000 Гц, один канал, циклический
+выход. При разных значениях первой и последней точки на стыке будет скачок.
+Напряжения 0…Vref округляются к ближайшему коду 0…4095; выход за диапазон
+отклоняется целиком. Это относится и к выбросам сплайна. Vref должен
+соответствовать фактическому VDDA/VREF+ платы; это не программируемое питание.
+
+Пример хоста (DLL передаётся через `SETPROTOCOL_LIBRARY`):
+
+```python
+from set_devices.waveform import generate, c_header
+points = [(0, 0), (10, 3.3), (20, 0)]
+wave = generate(points, sample_rate=10000, vref=3.3, method="linear")
+assert len(wave.codes) == 200
+with open("waveform.h", "w", encoding="utf-8") as output:
+ output.write(c_header(wave))
+```
+
+## Контракт порта
+
+`start(context, samples, count, sample_rate)` возвращает 0 при успехе;
+`stop(context)` синхронно прекращает DMA и устанавливает нулевой выход.
+Память таблицы принадлежит `set_wave_state`, её нельзя менять до stop.
+Вызывайте RTU и обслуживание состояния из одного основного потока, не из IRQ.
+IRQ USB помещает байты в очередь; USB-пакеты не являются границами RTU.
+
+```c
+static uint16_t samples[SET_WAVE_MAX]; /* DMA-accessible SRAM, not CCM */
+static set_wave_state wave;
+static set_wave_f407 hardware = {72000000};
+void application_init(void) {
+ set_wave_port port = set_wave_f407_port(&hardware);
+ set_wave_init(&wave, samples, SET_WAVE_MAX, &port);
+}
+size_t on_rtu(const uint8_t *frame, size_t n, uint8_t reply[256]) {
+ return set_wave_rtu(&wave, 16, frame, n, reply, 256);
+}
+```
+
+Для F407 DAC_CH1 — PA4, TIM6 TRGO_UPDATE, DMA1 Stream5/Channel7. Порт использует
+только нижние 16 бит DAC_CR и не меняет канал 2. Таблица лежит в SRAM1/2
+`0x20000000…0x2001FFFF`, не в CCM. Перед первым отсчётом есть один такт
+предзагрузки последнего значения предыдущего периода. Для гарантированной
+целой частоты порт принимает только делители входной частоты TIM6; при
+72 МГц подходят, например, 100, 1000, 5000, 10000, 20000 Гц.
+Частоту таймера передавайте с учётом удвоения при делителе APB1 > 1.
+Ошибку DMA/underrun проверяйте через `set_wave_f407_fault()` и останавливайте
+состояние. На F407-проекте этот вызов включён в главный цикл.
+
+Для G474 настройте CubeMX: DAC1 channel1 (PA4), output buffer enabled,
+TIM6 TRGO_UPDATE, DMA memory-to-peripheral, circular, halfword/halfword,
+memory increment, запрос `DMA_REQUEST_DAC1_CHANNEL1`; подключите HAL IRQ
+для выбранного канала DMA и DAC. Передайте handles и timer_hz в
+`set_wave_g474`. Обработчики ошибок DMA/DAC в приложении должны вызвать stop
+и сбросить `wave.running/ready`. Порт проверен компиляцией с CubeG4 1.6.1;
+конкретная плата G474 здесь не прошита.
+
+## USB / Modbus RTU
+
+Адрес по умолчанию 16, FC03/FC06, CRC16 Modbus. Максимальный запрос — 8 байт,
+ответ — 21 байт; существующий потоковый USB RTU parser F407 подходит без
+изменения формата. Все поля строит/проверяет C-кодек, GUI не пакует байты.
+
+| Регистр | Значение |
+|---|---|
+| `0x1300`, FC03, 8 слов | `0x5747`, версия 1, running, Fs, count, received, ready, capacity |
+| `0x1302`, FC06 | 0: stop и нулевой выход; 1: start только после commit |
+| `0x1303`, FC06 | Частота 1…50000; сбрасывает ready |
+| `0x1304`, FC06 | Начать загрузку, число отсчётов 2…4096; сброс received/ready |
+| `0xA000+i`, FC06 | Последовательная загрузка отсчёта 0…4095; повтор того же значения допустим |
+| `0xA000+i`, FC03, 1 слово | Чтение загруженного отсчёта для проверки |
+| `0x1306`, FC06 | `0xA55A`: commit, только если все отсчёты загружены |
+
+Во время running разрешены чтение и stop; запись/перезагрузка дают исключение
+6. Неполная таблица не запускается. Повреждённый CRC и чужой адрес не меняют
+состояние. При reset ready/running=0, таблица не сохраняется во Flash.
+Профиль и регистры генератора не являются регистратором УМП или протоколом 2812.
+
+Хост выполняет probe → stop → Fs → count → samples → readback каждого
+отсчёта → commit → status. Пуск — отдельная пользовательская команда.
+После потери USB уже запущенный DAC продолжает автономное воспроизведение;
+при таймауте состояние неизвестно до повторного status/stop. Одновременно
+используйте одного клиента, не открывайте этот же COM во вкладке подключения.
+
+## Потребители и проверки
+
+- SETGUI: «Логи и графики → Генератор», JSON, CSV и C header.
+- `home/407vet6_emul_TMS_Periph`: USB CDC и UART, штатный адрес настройки STM,
+ отдельный PA4 DAC выход; HAL/CMSIS и настройки платы остаются в проекте.
+- G474: переносимый HAL-порт; board init остаётся в целевой прошивке.
+- `tests/test_signal_wave.c`: C-векторы интерполяции, DAC, CRC, неполной
+ загрузки, readback и блокировки running. Общие байты проверены Python
+ ctypes и фактическим USB stream parser эмулятора.
+
+Аппаратная проверка амплитуды, периода и формы осциллографом не выполнена.
diff --git a/c/set-protocol/include/set_boot.h b/c/set-protocol/include/set_boot.h
new file mode 100644
index 0000000..2b5ac4d
--- /dev/null
+++ b/c/set-protocol/include/set_boot.h
@@ -0,0 +1,26 @@
+#ifndef SET_BOOT_H
+#define SET_BOOT_H
+#include "set_firmware.h"
+
+/* Transport-independent, single-slot updater. invalidate MUST become durable
+ * before erase; commit is called only after read-back verification. */
+typedef struct {
+ bool (*invalidate)(void *user);
+ bool (*erase)(void *user, uint32_t size);
+ bool (*write)(void *user, uint32_t offset, const uint8_t *data, uint16_t size);
+ bool (*verify)(void *user, uint32_t size, uint32_t crc);
+ bool (*commit)(void *user, uint32_t size, uint32_t crc);
+ void (*activate)(void *user);
+} setp_boot_ops;
+typedef struct {
+ setp_boot_ops ops;
+ void *user;
+ uint32_t base, capacity, crc;
+ uint16_t block_size;
+ setp_fw_status_t status;
+} setp_boot;
+void setp_boot_init(setp_boot *b, const setp_boot_ops *ops, void *user,
+ uint32_t base, uint32_t capacity);
+/* Returns SET status; FW_STATUS body is encoded by the caller. */
+uint16_t setp_boot_command(setp_boot *b, uint16_t type, const uint8_t *p, uint16_t n);
+#endif
diff --git a/c/set-protocol/include/set_emu_igbt.h b/c/set-protocol/include/set_emu_igbt.h
new file mode 100644
index 0000000..45e4b76
--- /dev/null
+++ b/c/set-protocol/include/set_emu_igbt.h
@@ -0,0 +1,32 @@
+#ifndef SET_EMU_IGBT_H
+#define SET_EMU_IGBT_H
+#include "set_regmap.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+#define SET_IGBT_CHANNELS 8u
+#define SET_IGBT_WORDS 112u
+#define SET_IGBT_CHANNEL_WORDS 12u
+typedef struct {
+ uint16_t flags, mode, on_delay, on_width, off_delay, off_width;
+ uint16_t tx_width, edges, fault_width, count;
+} set_igbt_channel;
+typedef struct {
+ uint8_t enabled, tx, previous_tx, rx, fault, injected_tx, input_source;
+ uint16_t pending[SET_IGBT_CHANNELS], active[SET_IGBT_CHANNELS];
+ uint16_t delay_ms, width_ms, event_count;
+ uint8_t logical_rx;
+ uint16_t pulse[SET_IGBT_CHANNELS], response_width[SET_IGBT_CHANNELS];
+ set_igbt_channel channel[SET_IGBT_CHANNELS];
+} set_igbt_emu;
+PCAN_ABI_API void set_igbt_init(set_igbt_emu *);
+/* One model tick = 1 ms, no system clock. This coarse logical model does NOT
+ * reproduce the sub-microsecond timing of 1SP/1SD DSLogic hardware profiles.
+ * input_source=0 takes the supplied pins, 1 uses register-injected TX. */
+PCAN_ABI_API void set_igbt_tick(set_igbt_emu *, uint8_t pins);
+PCAN_ABI_API unsigned set_igbt_read(void *, uint16_t offset, uint16_t count, uint16_t *);
+PCAN_ABI_API unsigned set_igbt_write(void *, uint16_t offset, uint16_t value);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/set-protocol/include/set_emu_server.h b/c/set-protocol/include/set_emu_server.h
new file mode 100644
index 0000000..6d18842
--- /dev/null
+++ b/c/set-protocol/include/set_emu_server.h
@@ -0,0 +1,48 @@
+/* Reference service composition for emulator boards; use set_regmap directly
+ * for a different map, subset, or multiple instances of any module. */
+#ifndef SET_EMU_SERVER_H
+#define SET_EMU_SERVER_H
+#include "set_emu_ump.h"
+#include "set_emu_tms.h"
+#include "set_emu_igbt.h"
+#include "set_wavegen.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+#define SET_EMU_CONTROL_BASE 0x1200u
+#define SET_EMU_IGBT_BASE 0x1400u
+#define SET_EMU_TMS_BASE 0x1500u
+#define SET_EMU_INDEXED_BASE 0x2000u
+enum { SET_EMU_KIND_UMP=1,SET_EMU_KIND_CONTROL=2,SET_EMU_KIND_WAVE=3,
+ SET_EMU_KIND_IGBT=4,SET_EMU_KIND_TMS=5,SET_EMU_KIND_UMP_DATA=6,
+ SET_EMU_KIND_WAVE_DATA=7 };
+typedef struct {
+ set_regmap map;
+ set_reg_region regions[7];
+ set_ump_emu ump;
+ set_tms_emu tms;
+ set_igbt_emu igbt;
+ set_wave_state *wave;
+ /* config[3]: 0=UMP, 1=TMS2812, 2=IGBT, 3=DAC. Configuration version 3. */
+ uint16_t config[4],staged[4],apply_delay;
+} set_emu_server;
+/* bank_capacity in records, each bank has capacity*40 words.
+ * Wave may be NULL. UART rate index 0..4. No pins, heap or global instances.
+ * Init is for a quiescent instance: caller stops hardware before reinitializing. */
+PCAN_ABI_API unsigned set_emu_server_init(set_emu_server *,uint16_t *bank0,
+ uint16_t *bank1,uint16_t bank_capacity,set_wave_state *,unsigned address,
+ unsigned tms_address,unsigned baud_index,unsigned can_mode,unsigned enabled);
+PCAN_ABI_API void set_emu_server_tick(set_emu_server *,uint8_t igbt_pins);
+PCAN_ABI_API uint32_t set_emu_server_baud(const set_emu_server *);
+PCAN_ABI_API size_t set_emu_server_rtu(set_emu_server *,const uint8_t *,size_t,uint8_t *,size_t);
+PCAN_ABI_API size_t set_emu_server_can(set_emu_server *,const uint8_t *,size_t,
+ uint16_t *,size_t,unsigned *,unsigned *);
+/* Stream adapter for FC03/06 (8 octets) and TMS CMD_STD (18 octets). One
+ * context per connection. Transport passes monotonic milliseconds. */
+typedef struct { uint8_t data[18],used;uint32_t last_byte; } set_emu_stream;
+PCAN_ABI_API size_t set_emu_stream_feed(set_emu_stream *,set_emu_server *,
+ uint8_t byte,uint32_t now_ms,uint8_t *reply,size_t capacity);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/set-protocol/include/set_emu_tms.h b/c/set-protocol/include/set_emu_tms.h
new file mode 100644
index 0000000..0f640b1
--- /dev/null
+++ b/c/set-protocol/include/set_emu_tms.h
@@ -0,0 +1,22 @@
+#ifndef SET_EMU_TMS_H
+#define SET_EMU_TMS_H
+#include "set_regmap.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+#define SET_TMS_WORDS 80u
+typedef struct {
+ uint8_t controls[6], running;
+ uint16_t commands[10], divider;
+ int32_t frequency, target;
+} set_tms_emu;
+PCAN_ABI_API void set_tms_init(set_tms_emu *);
+PCAN_ABI_API void set_tms_tick(set_tms_emu *);
+PCAN_ABI_API unsigned set_tms_read(void *,uint16_t,uint16_t,uint16_t *);
+PCAN_ABI_API unsigned set_tms_write(void *,uint16_t,uint16_t);
+PCAN_ABI_API size_t set_tms_rtu(set_tms_emu *,unsigned device,const uint8_t *,
+ size_t,uint8_t *,size_t);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/set-protocol/include/set_emu_ump.h b/c/set-protocol/include/set_emu_ump.h
new file mode 100644
index 0000000..492bd84
--- /dev/null
+++ b/c/set-protocol/include/set_emu_ump.h
@@ -0,0 +1,25 @@
+#ifndef SET_EMU_UMP_H
+#define SET_EMU_UMP_H
+#include "set_regmap.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+#define SET_UMP_WORDS 40u
+typedef struct {
+ uint16_t (*banks[2])[SET_UMP_WORDS];
+ uint16_t capacity,can_mode;
+ uint8_t enabled;
+ uint16_t state,used,next,bank,archive_bank,archive_count,archive_next,period,divider,cursor,source,generation,post,dropped,prev_command,prev_input,prev_output;
+ uint32_t elapsed,sequence,scenario;
+} set_ump_emu;
+/* Two distinct caller-owned arrays, capacity*40 words EACH. No heap/MCU layout. */
+PCAN_ABI_API unsigned set_ump_init(set_ump_emu *,uint16_t *,uint16_t *,uint16_t capacity,
+ uint16_t can_mode,uint8_t enabled);
+PCAN_ABI_API void set_ump_tick(set_ump_emu *); /* one millisecond */
+PCAN_ABI_API unsigned set_ump_read(void *,uint16_t,uint16_t,uint16_t *);
+PCAN_ABI_API unsigned set_ump_indexed_read(void *,uint16_t,uint16_t,uint16_t *);
+PCAN_ABI_API unsigned set_ump_write(void *,uint16_t,uint16_t);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/set-protocol/include/set_regmap.h b/c/set-protocol/include/set_regmap.h
new file mode 100644
index 0000000..a94677b
--- /dev/null
+++ b/c/set-protocol/include/set_regmap.h
@@ -0,0 +1,48 @@
+/* Transport-independent 16-bit word address space. No heap, clocks or board headers. */
+#ifndef SET_REGMAP_H
+#define SET_REGMAP_H
+#include "pcan_abi.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+#define SET_REG_MAX_WORDS 124u
+#define SET_REG_READ 1u
+#define SET_REG_WRITE 2u
+enum { SET_REG_OK=0, SET_REG_FUNCTION=1, SET_REG_ADDRESS=2,
+ SET_REG_VALUE=3, SET_REG_FAILURE=4, SET_REG_BUSY=6 };
+typedef unsigned (*set_reg_read_fn)(void *, uint16_t, uint16_t, uint16_t *);
+typedef unsigned (*set_reg_write_fn)(void *, uint16_t, uint16_t);
+typedef struct {
+ uint16_t base, words, kind, version, instance, flags;
+ void *context;
+ set_reg_read_fn read;
+ set_reg_write_fn write;
+} set_reg_region;
+typedef struct {
+ const set_reg_region *regions;
+ size_t count;
+} set_regmap;
+/* Directory at 0: 8-word header, then 8 words per region. Reject overlaps,
+ * wraparound, invalid callbacks and overlap with the directory itself.
+ * Regions and contexts must outlive the map and remain unchanged. */
+PCAN_ABI_API unsigned set_regmap_init(set_regmap *, const set_reg_region *, size_t);
+/* One region per request. Preflight all bounds/capacity before calling a module.
+ * Writes return absolute address/value echo. Reads must be side-effect free.
+ * A module validates the entire write before mutation. Caller serializes access. */
+PCAN_ABI_API unsigned set_regmap_request(const set_regmap *, unsigned function,
+ uint16_t address, uint16_t value, uint16_t *out, size_t capacity, size_t *count);
+/* Complete Modbus RTU FC03/06 frame; no broadcast. Capacity >=253 bytes.
+ * CRC/address/capacity validation precedes dispatch. */
+PCAN_ABI_API size_t set_regmap_rtu(const set_regmap *, unsigned device,
+ const uint8_t *, size_t, uint8_t *, size_t);
+/* Legacy UMP CAN envelope, usable by any region: BE address/value/1F00/token/fn.
+ * Returns padded EVEN words, last padding word=0. Caller retains original
+ * requested count; token/error accompany fragments. Output capacity >=124. */
+PCAN_ABI_API size_t set_regmap_can(const set_regmap *, const uint8_t *, size_t,
+ uint16_t *, size_t, unsigned *token, unsigned *error);
+PCAN_ABI_API size_t set_regmap_can_fragment(const uint16_t *, size_t count,
+ size_t index, unsigned token, unsigned error, uint8_t *, size_t capacity);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/set-protocol/include/set_wavegen.h b/c/set-protocol/include/set_wavegen.h
new file mode 100644
index 0000000..06bbce7
--- /dev/null
+++ b/c/set-protocol/include/set_wavegen.h
@@ -0,0 +1,59 @@
+/** @file set_wavegen.h Transactional table upload and cyclic DAC playback. */
+#ifndef SET_WAVEGEN_H
+#define SET_WAVEGEN_H
+#include "pcan_abi.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+#define SET_WAVE_MAX 4096u
+#define SET_WAVE_MAX_RATE 1000000u
+#define SET_WAVE_BLOCK_MAX 120u
+#define SET_WAVE_BASE 0x1300u
+#define SET_WAVE_DATA 0xA000u
+typedef struct {
+ void *context;
+ /* Return 0 on success. Samples remain owned by state until stop returns.
+ * start must arrange periodic output, typically DAC + timer + DMA.
+ * stop must quiesce DMA before returning and set output to zero. */
+ int (*start)(void *, const uint16_t *, size_t, uint32_t);
+ void (*stop)(void *);
+} set_wave_port;
+typedef struct {
+ uint16_t *samples;
+ size_t capacity;
+ uint16_t count, received, ready, running, rate_high;
+ uint32_t rate;
+ set_wave_port port;
+} set_wave_state;
+void set_wave_init(set_wave_state *, uint16_t *, size_t, const set_wave_port *);
+/* Atomic sample-block transfer. values=NULL builds a read; otherwise a write.
+ * At most 120 words: request <=249 bytes, response <=245 bytes. */
+PCAN_ABI_API size_t set_wave_block_request(unsigned device,unsigned index,
+ const uint16_t *values,size_t count,uint8_t *out,size_t capacity);
+PCAN_ABI_API int set_wave_block_response(const uint8_t *request,size_t request_size,
+ const uint8_t *reply,size_t reply_size,uint16_t *words,size_t capacity);
+size_t set_wave_block_rtu(set_wave_state *,unsigned device,const uint8_t *,size_t,
+ uint8_t *,size_t);
+/* Relative register callbacks for set_regmap; status=8 words, samples=4096.
+ * These are the same state/validation used by the legacy RTU wrapper. */
+PCAN_ABI_API unsigned set_wave_read(void *,uint16_t,uint16_t,uint16_t *);
+PCAN_ABI_API unsigned set_wave_write(void *,uint16_t,uint16_t);
+PCAN_ABI_API unsigned set_wave_samples_read(void *,uint16_t,uint16_t,uint16_t *);
+PCAN_ABI_API unsigned set_wave_samples_write(void *,uint16_t,uint16_t);
+/* Caller serializes calls in the main loop. Returns complete reply size,
+ * 0 for unrelated/invalid frames. CRC verified before all state changes. */
+size_t set_wave_rtu(set_wave_state *, unsigned device, const uint8_t *, size_t,
+ uint8_t *, size_t);
+/* Thin host transport ABI. Operations: 0=status, 1=stop, 2=sample rate,
+ * 3=begin upload(count), 4=sample(index,value), 5=commit, 6=start.
+ * 7=read a sample(index) for verification. Return 8 or 0. */
+PCAN_ABI_API size_t set_wave_request(unsigned device,unsigned operation,unsigned index,
+ unsigned value,uint8_t *out,size_t capacity);
+/* Return 0=need more bytes, positive=word count, negative=invalid/exception.
+ * Validates exact echo/read shape, CRC, address and signature on status. */
+PCAN_ABI_API int set_wave_response(const uint8_t *request,const uint8_t *reply,
+ size_t size,uint16_t *words,size_t capacity);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/set-protocol/include/setprotocol.h b/c/set-protocol/include/setprotocol.h
index 73b6929..3391a81 100644
--- a/c/set-protocol/include/setprotocol.h
+++ b/c/set-protocol/include/setprotocol.h
@@ -4,6 +4,8 @@
*/
#ifndef SETPROTOCOL_H
#define SETPROTOCOL_H
+#include "set_regmap.h"
+#include "set_emu_server.h"
#include "balsam_can.h"
#include "periph28335.h"
diff --git a/c/set-protocol/include/ump_protocol.h b/c/set-protocol/include/ump_protocol.h
new file mode 100644
index 0000000..2cee035
--- /dev/null
+++ b/c/set-protocol/include/ump_protocol.h
@@ -0,0 +1,55 @@
+/* UMP logger v2 (PM35), independent of PM67 and SET protocol v2.
+ * Octets use 16-bit storage: C28x has CHAR_BIT=16 and no uint8_t.
+ * All lengths below count wire octets or register words, never sizeof bytes.
+ */
+#ifndef UMP_PROTOCOL_H
+#define UMP_PROTOCOL_H
+#include
+#include
+#ifdef __cplusplus
+extern "C" {
+#endif
+#define UMP_BASE 0x1000U
+#define UMP_DATA 0x1010U
+#define UMP_LIVE 0x1100U
+#define UMP_MARKER 0x1F00U
+#define UMP_MAX_WORDS 124U
+#define UMP_RTU_CAPACITY 253U
+typedef uint16_t ump_octet;
+/* Called under caller's snapshot lock. write returns 1 on success, 0 otherwise. */
+typedef struct {
+ void *context;
+ uint16_t (*read)(void *context, uint16_t address);
+ int (*write)(void *context, uint16_t address, uint16_t value);
+} ump_backend;
+/* Returns Modbus exception 0/1/2/3. CAN has stricter whole-block reads. */
+unsigned ump_validate(unsigned function, uint16_t address, uint16_t value, int can);
+/* reply capacity is >=124 words. Errors and writes return address/value echo. */
+unsigned ump_prepare(const ump_backend *backend, unsigned function,
+ uint16_t address, uint16_t value, int can, uint16_t *reply, unsigned *error);
+uint16_t ump_crc(const ump_octet *data, size_t count);
+/* Mode 1..16; kind 0=request, 1=normal telemetry, 2=logger reply; invalid ->0. */
+uint32_t ump_can_id(unsigned mode, unsigned kind);
+/* Returns 0 for non-service frames, malformed size, or zero token. */
+unsigned ump_can_request(const ump_backend *backend, const ump_octet *request,
+ size_t size, uint16_t *reply, unsigned *token, unsigned *error);
+/* count must be an even number 2..124; index counts pairs. Returns 8 or 0. */
+unsigned ump_can_fragment(const uint16_t *words, unsigned count, unsigned index,
+ unsigned token, unsigned error, ump_octet *reply);
+/* Complete RTU frame only, after UART framing. Ignores bad CRC/foreign address.
+ * Caller provides >=253 octets; no writes occur on insufficient capacity.
+ * Device address 1..247; no broadcasts or legacy padding in this extension.
+ */
+size_t ump_rtu(const ump_backend *backend, unsigned device,
+ const ump_octet *request, size_t size, ump_octet *reply, size_t capacity);
+/* Response for an already CRC-checked/framed request (legacy UART receiver). */
+size_t ump_rtu_response(const ump_backend *backend, unsigned device, unsigned function,
+ uint16_t address, uint16_t value, ump_octet *reply, size_t capacity);
+/* Serialize a captured reply after releasing the snapshot lock. Returns 0 on
+ * invalid count/capacity; error replies need no words. */
+size_t ump_rtu_encode(unsigned device, unsigned function, unsigned error,
+ const uint16_t *words, unsigned count, ump_octet *reply, size_t capacity);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/c/set-protocol/ports/example-emulator/service.c b/c/set-protocol/ports/example-emulator/service.c
new file mode 100644
index 0000000..15890cc
--- /dev/null
+++ b/c/set-protocol/ports/example-emulator/service.c
@@ -0,0 +1,41 @@
+/* Minimal board-independent composition. This compiles without any MCU headers.
+ * Adapt these entry points to the selected board's network/USB/UART/CAN port.
+ * Run every call in one task (or protect the entire call with the same lock). */
+#include "set_emu_igbt.h"
+static set_igbt_emu drivers[2];
+static set_reg_region regions[2];
+static set_regmap map;
+unsigned example_service_init(void)
+{
+ unsigned i;
+ for(i=0;i<2;++i) {
+ set_reg_region *r=®ions[i];
+ set_igbt_init(&drivers[i]);
+ r->base=(uint16_t)(0x300+i*0x100);r->words=SET_IGBT_WORDS;
+ r->kind=4;r->version=2;r->instance=(uint16_t)i;r->flags=3;
+ r->context=&drivers[i];r->read=set_igbt_read;r->write=set_igbt_write;
+ }
+ return set_regmap_init(&map,regions,2);
+}
+void example_service_tick(uint8_t inputs0,uint8_t inputs1,uint8_t outputs[2])
+{
+ set_igbt_tick(&drivers[0],inputs0);set_igbt_tick(&drivers[1],inputs1);
+ outputs[0]=drivers[0].rx;outputs[1]=drivers[1].rx;
+}
+unsigned example_custom_protocol(unsigned function,uint16_t address,uint16_t value,
+ uint16_t *words,size_t capacity,size_t *count)
+{
+ /* Caller has already decoded/authenticated its own transport envelope. */
+ return set_regmap_request(&map,function,address,value,words,capacity,count);
+}
+size_t example_uart_or_usb(const uint8_t *frame,size_t length,uint8_t *reply,size_t capacity)
+{
+ return set_regmap_rtu(&map,16,frame,length,reply,capacity);
+}
+size_t example_can(const uint8_t frame[8],uint16_t *snapshot,size_t capacity,
+ unsigned *token,unsigned *error)
+{
+ /* Select the CAN request ID outside the model; send the returned snapshot
+ * using set_regmap_can_fragment and the board's chosen response CAN ID. */
+ return set_regmap_can(&map,frame,8,snapshot,capacity,token,error);
+}
diff --git a/c/set-protocol/ports/stm32-usb-boot/boot_usb_protocol.inc b/c/set-protocol/ports/stm32-usb-boot/boot_usb_protocol.inc
new file mode 100644
index 0000000..7784fb0
--- /dev/null
+++ b/c/set-protocol/ports/stm32-usb-boot/boot_usb_protocol.inc
@@ -0,0 +1,54 @@
+/* Shared SET USB boot dispatcher. The MCU wrapper supplies BOOT_* macros. */
+#include "usb_setp_stream.h"
+#include
+static setp_boot boot;
+static bool reset_pending;
+static uint32_t reset_at;
+typedef struct { uint8_t *out; uint16_t size; uint32_t now; } reply_context;
+void boot_protocol_init(void) { setp_boot_init(&boot,&BOOT_OPS,0,BOOT_APP_BASE,BOOT_APP_SIZE); }
+void boot_protocol_poll(uint32_t now) {
+ if(reset_pending && (int32_t)(now-reset_at)>=0) BOOT_RESET();
+}
+static void dispatch(const setp_frame_t *r,void *user) {
+ reply_context *c=user;
+ uint8_t body[128]; uint16_t status=SETP_STATUS_OK; size_t n=0;
+ setp_frame_t response={0};
+ if((r->destination!=0 && r->destination!=BOOT_NODE) ||
+ (r->flags&(SETP_FLAG_RESPONSE|SETP_FLAG_EVENT))) return;
+ if(r->flags&(SETP_FLAG_ERROR|SETP_FLAG_MORE)) status=SETP_STATUS_INVALID_ARGUMENT;
+ else if(r->message_type>=SETP_MSG_FW_BEGIN && r->message_type<=SETP_MSG_FW_ACTIVATE) {
+ status=setp_boot_command(&boot,r->message_type,r->payload,r->payload_length);
+ if(!status && r->message_type==SETP_MSG_FW_STATUS)
+ n=setp_fw_status_encode(&boot.status,body+2,sizeof(body)-2);
+ if(!status && r->message_type==SETP_MSG_FW_ACTIVATE) {
+ reset_pending=true; reset_at=c->now+500;
+ }
+ } else if(r->payload_length) status=SETP_STATUS_INVALID_LENGTH;
+ else switch(r->message_type) {
+ case SETP_MSG_PING: setp_put_u32(body+2,c->now); n=4; break;
+ case SETP_MSG_DEVICE_INFO: {
+ static const uint8_t model[]=BOOT_MODEL;
+ const setp_device_info_t info={1,0,0,0x10000,3,0,sizeof(model)-1,model};
+ n=setp_device_info_encode(&info,body+2,sizeof(body)-2); break;
+ }
+ case SETP_MSG_CAPABILITIES: {
+ const setp_capabilities_t caps={1,SETP_MAX_PAYLOAD,
+ SETP_IFACE_MASK(SETP_IFACE_USB_CDC),SETP_FEATURE_FIRMWARE,0,0,0,0};
+ n=setp_capabilities_encode(&caps,body+2,sizeof(body)-2); break;
+ }
+ default: status=SETP_STATUS_UNSUPPORTED; break;
+ }
+ setp_put_u16(body,status); if(status) n=0;
+ response.flags=SETP_FLAG_RESPONSE|(status ? SETP_FLAG_ERROR : 0);
+ response.message_type=r->message_type; response.sequence=r->sequence;
+ response.source=BOOT_NODE; response.destination=r->source;
+ response.payload=body; response.payload_length=(uint16_t)(n+2);
+ c->size=(uint16_t)setp_frame_encode(&response,c->out,SETP_FRAME_MAX);
+}
+void usb_setp_reset(usb_setp_stream *s) { memset(s,0,sizeof(*s)); }
+uint16_t usb_setp_feed(usb_setp_stream *s,uint8_t byte,uint32_t now,uint8_t *out) {
+ reply_context c={out,0,now};
+ if(now-s->last_byte>1000) setp_parser_reset(&s->parser);
+ s->last_byte=now; (void)setp_parser_feed(&s->parser,&byte,1,dispatch,&c);
+ return c.size;
+}
diff --git a/c/set-protocol/ports/stm32f407-periph/README.md b/c/set-protocol/ports/stm32f407-periph/README.md
new file mode 100644
index 0000000..73788e0
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-periph/README.md
@@ -0,0 +1,13 @@
+# Порт STM32F407 / эмулятор периферии
+
+Добавьте `src/ump_protocol.c` и `ump_stm32f407.c` в сборку.
+Порт получает backend с read/write и переводит 8-битные буферы STM32 в
+октеты ядра. Приём UART, CRC framing, CAN ID/DLC/RTR-фильтры и очередь
+передачи находятся в проекте; ядро проверяет CRC полного RTU кадра.
+Вызывать из главного цикла. В эмуляторе SysTick лишь накапливает тики,
+поэтому read/write и генератор не выполняются одновременно.
+Ответ RTU требует 253 байта; CAN snapshot — 124 uint16_t. Не передавайте
+неполный snapshot в fragment, count должен быть фактическим числом слов.
+Подключено в `home/407vet6_emul_TMS_Periph/Src/shared_ump.c`.
+
+[Контракт, пример и проверки](../../docs/UMP_PROTOCOL.md).
diff --git a/c/set-protocol/ports/stm32f407-periph/ump_stm32f407.c b/c/set-protocol/ports/stm32f407-periph/ump_stm32f407.c
new file mode 100644
index 0000000..5c97d87
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-periph/ump_stm32f407.c
@@ -0,0 +1,30 @@
+#include "ump_stm32f407.h"
+size_t ump_stm32f407_rtu(const ump_backend *b, unsigned device,
+ const uint8_t *request, size_t length, uint8_t *reply, size_t capacity)
+{
+ ump_octet r[8],out[UMP_RTU_CAPACITY];
+ size_t i,n;
+ if (!request || !reply || length!=8 || capacity
+#define META 0x0800C000UL
+#define COMMIT 0x53455442UL
+#define ERRORS (FLASH_SR_PGSERR|FLASH_SR_PGPERR|FLASH_SR_PGAERR|FLASH_SR_WRPERR|FLASH_SR_SOP)
+static void unlock(void) {
+ if(FLASH->CR & FLASH_CR_LOCK) { FLASH->KEYR=0x45670123; FLASH->KEYR=0xCDEF89AB; }
+ FLASH->SR=ERRORS;
+}
+static bool finish(void) {
+ while(FLASH->SR & FLASH_SR_BSY) {}
+ return !(FLASH->SR & ERRORS);
+}
+static void cache_reset(void) {
+ FLASH->ACR &= ~(FLASH_ACR_ICEN|FLASH_ACR_DCEN);
+ FLASH->ACR |= FLASH_ACR_ICRST|FLASH_ACR_DCRST;
+ FLASH->ACR &= ~(FLASH_ACR_ICRST|FLASH_ACR_DCRST);
+ FLASH->ACR |= FLASH_ACR_ICEN|FLASH_ACR_DCEN;
+ __DSB(); __ISB();
+}
+static bool erase_sector(unsigned sector) {
+ bool ok; unlock();
+ FLASH->CR=(FLASH->CR & ~(FLASH_CR_SNB|FLASH_CR_PSIZE)) |
+ FLASH_CR_SER | (2UL<CR|=FLASH_CR_STRT; ok=finish();
+ FLASH->CR &= ~FLASH_CR_SER; FLASH->CR |= FLASH_CR_LOCK;
+ cache_reset(); return ok;
+}
+static bool program(uint32_t address, const uint8_t *p, uint32_t n) {
+ bool ok=true; unlock();
+ /* Byte programming supports arbitrary SET block boundaries and final tails. */
+ FLASH->CR=(FLASH->CR & ~FLASH_CR_PSIZE)|FLASH_CR_PG;
+ while(n--) {
+ *(volatile uint8_t *)address=*p;
+ if(!finish()) { ok=false; break; }
+ ++address; ++p;
+ }
+ FLASH->CR &= ~FLASH_CR_PG; FLASH->CR |= FLASH_CR_LOCK;
+ cache_reset(); return ok;
+}
+static bool invalidate(void *u) { (void)u; return erase_sector(3); }
+static bool erase(void *u, uint32_t size) {
+ unsigned s; (void)u;
+ if(size<8 || size>F407_APP_SIZE) return false;
+ for(s=4;s<=7;++s) {
+ uint32_t start=s==4 ? F407_APP_BASE : 0x08020000UL+(s-5)*0x20000UL;
+ if(start>=F407_APP_BASE+size) break;
+ if(!erase_sector(s)) return false;
+ }
+ return true;
+}
+static bool write(void *u,uint32_t off,const uint8_t *p,uint16_t n) {
+ (void)u;
+ if(!n || off>F407_APP_SIZE || n>F407_APP_SIZE-off) return false;
+ return program(F407_APP_BASE+off,p,n) && !memcmp((const void *)(F407_APP_BASE+off),p,n);
+}
+static bool verify(void *u,uint32_t size,uint32_t crc) {
+ (void)u;
+ return f407_vectors_valid((const uint8_t *)F407_APP_BASE,size) &&
+ setp_crc32((const uint8_t *)F407_APP_BASE,size)==crc;
+}
+static bool commit(void *u,uint32_t size,uint32_t crc) {
+ uint32_t record[4]={size,crc,~size,COMMIT}; (void)u;
+ return program(META,(const uint8_t *)record,sizeof(record)) &&
+ !memcmp((const void *)META,record,sizeof(record));
+}
+bool f407_app_valid(void) {
+ /* Like climate: direct Keil/SWD writes need no USB completion record. */
+ return f407_vectors_valid((const uint8_t *)F407_APP_BASE,F407_APP_SIZE);
+}
+static void activate(void *u) { (void)u; /* main waits for USB response before reset */ }
+const setp_boot_ops f407_boot_ops={invalidate,erase,write,verify,commit,activate};
+__attribute__((naked,noreturn)) static void enter(uint32_t sp,uint32_t pc) {
+ __asm volatile("msr msp, r0\nmovs r2, #0\nmsr control, r2\ndsb\nisb\ncpsie i\nbx r1\n");
+}
+void f407_jump_app(void) {
+ unsigned i; uint32_t sp=*(const uint32_t *)F407_APP_BASE;
+ uint32_t pc=*(const uint32_t *)(F407_APP_BASE+4);
+ __disable_irq(); SysTick->CTRL=0;
+ for(i=0;i<8;++i) { NVIC->ICER[i]=0xffffffff; NVIC->ICPR[i]=0xffffffff; }
+ SCB->ICSR=SCB_ICSR_PENDSTCLR_Msk|SCB_ICSR_PENDSVCLR_Msk;
+ SCB->VTOR=F407_APP_BASE; enter(sp,pc);
+}
diff --git a/c/set-protocol/ports/stm32f407-usb-boot/boot_image.h b/c/set-protocol/ports/stm32f407-usb-boot/boot_image.h
new file mode 100644
index 0000000..0988055
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-usb-boot/boot_image.h
@@ -0,0 +1,19 @@
+#ifndef F407_BOOT_IMAGE_H
+#define F407_BOOT_IMAGE_H
+#include "boot_port.h"
+#include
+
+static inline bool f407_vectors_valid(const uint8_t *image, uint32_t size)
+{
+ uint32_t sp, pc;
+ if (size < 8 || size > F407_APP_SIZE) return false;
+ memcpy(&sp, image, 4);
+ memcpy(&pc, image + 4, 4);
+ return !(sp & 7) &&
+ ((sp > 0x20000000UL && sp <= 0x20020000UL) ||
+ (sp > 0x10000000UL && sp <= 0x10010000UL)) &&
+ (pc & 1) && (pc & ~1UL) >= F407_APP_BASE &&
+ (pc & ~1UL) < F407_APP_BASE + size;
+}
+
+#endif
diff --git a/c/set-protocol/ports/stm32f407-usb-boot/boot_main.c b/c/set-protocol/ports/stm32f407-usb-boot/boot_main.c
new file mode 100644
index 0000000..ad6f4fd
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-usb-boot/boot_main.c
@@ -0,0 +1,63 @@
+#include "stm32f4xx_hal.h"
+#include "boot_config.h"
+#include "usb_cdc.h"
+#include "boot_port.h"
+#if F407_BOOT_HSE_HZ != HSE_VALUE
+#error boot_config.h and HAL HSE_VALUE must describe the same crystal
+#endif
+#if (F407_BOOT_HSE_HZ % 1000000UL) || F407_BOOT_HSE_HZ < 2000000UL || F407_BOOT_HSE_HZ > 50000000UL
+#error HSE must provide a valid integer PLLM divider (2..50 MHz)
+#endif
+uint32_t SystemCoreClock=16000000;
+const uint8_t AHBPrescTable[16]={0,0,0,0,0,0,0,0,1,2,3,4,6,7,8,9};
+const uint8_t APBPrescTable[8]={0,0,0,0,1,2,3,4};
+static uint8_t usb_clock_ready;
+static volatile uint32_t clock_fault;
+void SystemInit(void) { SCB->CPACR|=0xFu<<20; SCB->VTOR=0x08000000; }
+void SysTick_Handler(void) { HAL_IncTick(); }
+static void board_fatal(uint32_t code) { clock_fault=code; for(;;) {} }
+static void peripheral_clock(void)
+{
+ RCC_OscInitTypeDef osc={0};
+ RCC_ClkInitTypeDef clk={0};
+ /* Разрешаем доступ к PWR и выбираем диапазон питания ядра перед повышением частоты. */
+ __HAL_RCC_PWR_CLK_ENABLE();
+ __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
+ osc.OscillatorType=RCC_OSCILLATORTYPE_HSE;
+ osc.HSEState=RCC_HSE_ON;
+ /* Готовность HSE проверяется HAL с ограниченным временем ожидания, а не бесконечным циклом. */
+ if (HAL_RCC_OscConfig(&osc)==HAL_OK) usb_clock_ready=1;
+ else {
+ clock_fault=1;
+ osc.HSEState=RCC_HSE_OFF;
+ if (HAL_RCC_OscConfig(&osc)!=HAL_OK) board_fatal(3);
+ }
+ /* CPU 72 MHz, APB1 36 MHz, USB 48 MHz; USB requires the HSE crystal. */
+ osc.OscillatorType=RCC_OSCILLATORTYPE_HSI;
+ osc.HSIState=RCC_HSI_ON;
+ osc.HSICalibrationValue=RCC_HSICALIBRATION_DEFAULT;
+ osc.PLL.PLLState=RCC_PLL_ON;
+ osc.PLL.PLLSource=usb_clock_ready ? RCC_PLLSOURCE_HSE : RCC_PLLSOURCE_HSI;
+ osc.PLL.PLLM=usb_clock_ready ? F407_BOOT_HSE_HZ/1000000UL : 16;
+ osc.PLL.PLLN=144; osc.PLL.PLLP=RCC_PLLP_DIV2; osc.PLL.PLLQ=3;
+ if (HAL_RCC_OscConfig(&osc)!=HAL_OK) board_fatal(3);
+ clk.ClockType=RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
+ clk.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;
+ clk.AHBCLKDivider=RCC_SYSCLK_DIV1;
+ clk.APB1CLKDivider=RCC_HCLK_DIV2; clk.APB2CLKDivider=RCC_HCLK_DIV1;
+ /* HAL задаёт два такта ожидания Flash и перестраивает SysTick для новой частоты CPU. */
+ if (HAL_RCC_ClockConfig(&clk,FLASH_LATENCY_2)!=HAL_OK) board_fatal(3);
+}
+
+
+void boot_protocol_init(void);
+void boot_protocol_poll(uint32_t now);
+int main(void) {
+ /* Jump from reset, before initializing USB, DMA, clocks or interrupts. */
+ bool requested=f407_boot_requested();
+ if(!requested && f407_app_valid()) f407_jump_app();
+ if(HAL_Init()!=HAL_OK) board_fatal(3);
+ peripheral_clock(); boot_protocol_init();
+ if(usb_clock_ready) usb_cdc_init();
+ for(;;) { uint32_t now=HAL_GetTick(); usb_cdc_poll(now); boot_protocol_poll(now); }
+}
diff --git a/c/set-protocol/ports/stm32f407-usb-boot/boot_port.h b/c/set-protocol/ports/stm32f407-usb-boot/boot_port.h
new file mode 100644
index 0000000..dc562ef
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-usb-boot/boot_port.h
@@ -0,0 +1,13 @@
+#ifndef F407_BOOT_PORT_H
+#define F407_BOOT_PORT_H
+#include "set_boot.h"
+#define F407_APP_BASE 0x08010000UL
+#define F407_APP_SIZE 0x00070000UL
+#define F407_BOOT_REQUEST 0x1003U
+extern const setp_boot_ops f407_boot_ops;
+bool f407_app_valid(void);
+bool f407_boot_requested(void);
+void f407_request_boot(uint32_t now);
+void f407_reset_poll(uint32_t now);
+void f407_jump_app(void);
+#endif
diff --git a/c/set-protocol/ports/stm32f407-usb-boot/boot_request.c b/c/set-protocol/ports/stm32f407-usb-boot/boot_request.c
new file mode 100644
index 0000000..c0a2f46
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-usb-boot/boot_request.c
@@ -0,0 +1,26 @@
+#include "boot_port.h"
+#include "stm32f4xx_hal.h"
+#define REQUEST_MAGIC 0x42555342UL
+static uint32_t reset_at;
+static bool reset_pending;
+static void backup_access(void)
+{
+ __HAL_RCC_PWR_CLK_ENABLE();
+ PWR->CR |= PWR_CR_DBP;
+ __HAL_RCC_RTC_ENABLE();
+}
+bool f407_boot_requested(void)
+{
+ bool requested;
+ backup_access(); requested=RTC->BKP0R==REQUEST_MAGIC;
+ RTC->BKP0R=0; return requested;
+}
+void f407_request_boot(uint32_t now)
+{
+ backup_access(); RTC->BKP0R=REQUEST_MAGIC;
+ reset_at=now+500; reset_pending=true;
+}
+void f407_reset_poll(uint32_t now)
+{
+ if(reset_pending && (int32_t)(now-reset_at)>=0) NVIC_SystemReset();
+}
diff --git a/c/set-protocol/ports/stm32f407-usb-boot/boot_usb_stream.c b/c/set-protocol/ports/stm32f407-usb-boot/boot_usb_stream.c
new file mode 100644
index 0000000..017296d
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-usb-boot/boot_usb_stream.c
@@ -0,0 +1,10 @@
+#include "stm32f4xx_hal.h"
+#include "boot_port.h"
+#include "boot_config.h"
+#define BOOT_OPS f407_boot_ops
+#define BOOT_APP_BASE F407_APP_BASE
+#define BOOT_APP_SIZE F407_APP_SIZE
+#define BOOT_NODE F407_BOOT_NODE
+#define BOOT_MODEL F407_BOOT_MODEL
+#define BOOT_RESET() NVIC_SystemReset()
+#include "../stm32-usb-boot/boot_usb_protocol.inc"
diff --git a/c/set-protocol/ports/stm32f407-usb-boot/test_boot_image.c b/c/set-protocol/ports/stm32f407-usb-boot/test_boot_image.c
new file mode 100644
index 0000000..1b39c25
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-usb-boot/test_boot_image.c
@@ -0,0 +1,34 @@
+#include "boot_image.h"
+#include
+#include
+
+int main(void)
+{
+ uint32_t words[32] = {0x20020000, F407_APP_BASE + 9};
+ uint8_t *image = (uint8_t *)words;
+ /* A Keil image needs no metadata. Replacing its body must still boot. */
+ assert(f407_vectors_valid(image, F407_APP_SIZE));
+ words[10] ^= 1;
+ assert(f407_vectors_valid(image, F407_APP_SIZE));
+ words[0] = words[1] = 0xffffffff;
+ assert(!f407_vectors_valid(image, F407_APP_SIZE));
+ words[0] = 0x20020000; words[1] = 0x08000101;
+ assert(!f407_vectors_valid(image, F407_APP_SIZE));
+ words[1] = F407_APP_BASE + 8;
+ assert(!f407_vectors_valid(image, F407_APP_SIZE));
+ words[1] = F407_APP_BASE + 9; words[0] = 0x20020001;
+ assert(!f407_vectors_valid(image, F407_APP_SIZE));
+ words[0] = 0x10010000;
+ assert(f407_vectors_valid(image, F407_APP_SIZE));
+ assert(!f407_vectors_valid(image, 8));
+ assert(!f407_vectors_valid(image, 7));
+ assert(!f407_vectors_valid(image, F407_APP_SIZE + 1));
+ words[1] = F407_APP_BASE + F407_APP_SIZE + 1;
+ assert(!f407_vectors_valid(image, F407_APP_SIZE));
+ words[1] = F407_APP_BASE + 9;
+ /* Like climate, vectors alone cannot establish image completeness. */
+ memset(image + 8, 0xff, sizeof(words) - 8);
+ assert(f407_vectors_valid(image, F407_APP_SIZE));
+ puts("F407 boot: Keil image replacement and vector checks passed");
+ return 0;
+}
diff --git a/c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.c b/c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.c
new file mode 100644
index 0000000..0e354af
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.c
@@ -0,0 +1,53 @@
+#include "set_wave_f407.h"
+#include "stm32f4xx.h"
+static void stop(void *context) {
+ unsigned limit=100000; (void)context;
+ RCC->AHB1ENR|=RCC_AHB1ENR_GPIOAEN|RCC_AHB1ENR_DMA1EN;
+ RCC->APB1ENR|=RCC_APB1ENR_DACEN|RCC_APB1ENR_TIM6EN;
+ (void)RCC->APB1ENR;
+ TIM6->CR1=0;
+ DAC->CR&=~(DAC_CR_DMAEN1|DAC_CR_TEN1);
+ DMA1_Stream5->CR&=~DMA_SxCR_EN;
+ while((DMA1_Stream5->CR&DMA_SxCR_EN)&&--limit) {}
+ /* Leave other DAC channel untouched. Channel 1 outputs zero on stop. */
+ DAC->DHR12R1=0;
+ DMA1->HIFCR=0xF40u;
+ DAC->SR=DAC_SR_DMAUDR1;
+}
+static int start(void *context,const uint16_t *samples,size_t count,uint32_t rate) {
+ set_wave_f407 *c=(set_wave_f407 *)context;
+ uint32_t ticks,psc,arr; uintptr_t address=(uintptr_t)samples;
+ if(!c||!rate||rate>SET_WAVE_MAX_RATE||count<2||count>SET_WAVE_MAX||c->timer_hz%rate
+ ||address<0x20000000u||address+count*2>0x20020000u) return 1;
+ ticks=c->timer_hz/rate;
+ for(psc=1;psc<=65536;++psc) if(ticks%psc==0&&ticks/psc<=65536) break;
+ if(psc>65536||ticksCR&DMA_SxCR_EN) return 1;
+ GPIOA->MODER|=3u<<8;
+ GPIOA->PUPDR&=~(3u<<8);
+ TIM6->PSC=psc-1;TIM6->ARR=arr-1;TIM6->CNT=0;
+ TIM6->CR2=0;TIM6->EGR=TIM_EGR_UG;TIM6->SR=0;
+ TIM6->CR2=TIM_CR2_MMS_1; /* update -> TRGO */
+ DMA1_Stream5->PAR=(uint32_t)&DAC->DHR12R1;
+ DMA1_Stream5->M0AR=(uint32_t)samples;DMA1_Stream5->NDTR=(uint32_t)count;
+ DMA1_Stream5->FCR=0;
+ DMA1_Stream5->CR=(7u<<25)|DMA_SxCR_DIR_0|DMA_SxCR_MINC|DMA_SxCR_CIRC
+ |DMA_SxCR_PSIZE_0|DMA_SxCR_MSIZE_0|DMA_SxCR_PL_1;
+ /* Prime the previous period's last sample; DMA supplies the next value
+ * after each trigger. Cyclic order is last,0,1,... with one priming tick. */
+ DAC->CR&=~0xFFFFu;
+ DAC->DHR12R1=samples[count-1];
+ DAC->CR|=DAC_CR_EN1|DAC_CR_TEN1|DAC_CR_DMAEN1; /* TSEL1=0: TIM6 */
+ DMA1_Stream5->CR|=DMA_SxCR_EN;
+ TIM6->CR1=TIM_CR1_CEN;
+ return 0;
+}
+set_wave_port set_wave_f407_port(set_wave_f407 *context) {
+ set_wave_port port={context,start,stop};return port;
+}
+int set_wave_f407_fault(void) {
+ return (DMA1->HISR&(DMA_HISR_TEIF5|DMA_HISR_DMEIF5|DMA_HISR_FEIF5))
+ ||(DAC->SR&DAC_SR_DMAUDR1);
+}
diff --git a/c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.h b/c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.h
new file mode 100644
index 0000000..b535770
--- /dev/null
+++ b/c/set-protocol/ports/stm32f407-wavegen/set_wave_f407.h
@@ -0,0 +1,12 @@
+#ifndef SET_WAVE_F407_H
+#define SET_WAVE_F407_H
+#include "../../include/set_wavegen.h"
+/* Owns PA4, DAC channel 1, TIM6 and DMA1 stream 5/channel 7.
+ * timer_hz is TIM6 input clock (twice APB1 when APB1 prescaler !=1).
+ * Rate: 1..SET_WAVE_MAX_RATE (1 MS/s), exact divisors of timer_hz only.
+ * Context and sample buffer must outlive playback; buffer must be in SRAM. */
+typedef struct { uint32_t timer_hz; } set_wave_f407;
+set_wave_port set_wave_f407_port(set_wave_f407 *context);
+/* Nonzero on DMA error or DAC underrun; caller stops state on failure. */
+int set_wave_f407_fault(void);
+#endif
diff --git a/c/set-protocol/ports/stm32g474-periph/README.md b/c/set-protocol/ports/stm32g474-periph/README.md
new file mode 100644
index 0000000..15e27f7
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-periph/README.md
@@ -0,0 +1,34 @@
+# STM32G474RB emulator peripheral port
+
+Board adapters for the common UMP/TMS/IGBT server and waveform generator.
+All peripheral source files have detailed Russian comments matching the F407 port.
+The shared algorithms, codecs and register map stay in `../../src`.
+
+- `board.c`: application VTOR 0x08010000; HSI16/PLL 160 MHz, APB1 80 MHz;
+ HSI48/CRS USB; board GPIO and 1 ms model ticks.
+- `transport.c`: USART2 PA2/PA3 RTU, optional RS485 DE PB15;
+ FDCAN1 PB8/PB9, Classic CAN, extended IDs, bounded polling and TX timeout.
+- `wave_hal.c`: PA4 DAC1_CH1, TIM6, DMA1_Channel1 with DMAMUX DAC1 request;
+ 2..4096 samples in SRAM1/2, exact timer division, maximum 1 MS/s.
+- `main.c`: service ordering and deferred reboot into the G474 USB bootloader.
+
+IGBT input bits 0..7 use PB0/PB1/PB2/PB10/PB11/PB12/PB13/PB14;
+output bits use PC0..PC7. SWD, SWO and USB pins are preserved.
+Clock, addresses and RS485 mode come from the board-supplied `board_config.h`.
+The default internal clock does not guarantee CAN or waveform accuracy over
+all temperature conditions; use a verified 24 MHz HSE source when required.
+
+The consumer supplies `board.h`, `transport.h`, `wave_hal.h`, `emulator.h`,
+G4 HAL configuration and the common model composition. The reference project
+is `Nucleo_G474RB` in the emulator repository. Compile `EMU_CAPACITY=400`:
+each 32000-byte archive bank fits the MCU's 32 KiB CCM/96 KiB SRAM layout.
+DMA samples stay in SRAM1/2. No F407 peripheral implementation is compiled.
+
+Link at 0x08010000 with a 64 KiB Flash limit; install the separate G474RB
+USB bootloader first. Require 128 KiB silicon, DBANK=0, BFB2=0 and no bank swap.
+The firmware rejects mismatched geometry before peripheral initialization.
+Reuse `../stm32g474-usb-boot/boot_usb_port.c` and `boot_request.c` with the
+shared USB CDC transport. Device model: STM32G474RB-EMU-EXPERIMENT.
+
+Host tests validate the shared software; peripheral timing and electrical
+behavior have not yet been validated on a physical board.
diff --git a/c/set-protocol/ports/stm32g474-periph/board.c b/c/set-protocol/ports/stm32g474-periph/board.c
new file mode 100644
index 0000000..417ef3d
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-periph/board.c
@@ -0,0 +1,109 @@
+/**
+ * Аппаратный порт STM32G474RB: векторы приложения, тактирование и GPIO.
+ * Алгоритмы UMP/TMS/IGBT находятся в общем C-ядре templates; здесь только пины.
+ */
+#include "board.h"
+#include "board_config.h"
+#include "transport.h"
+#include "emulator.h"
+#include "igbt_emulator.h"
+#include "stm32g4xx_hal.h"
+volatile uint32_t clock_fault;
+uint32_t SystemCoreClock=16000000u;
+const uint8_t AHBPrescTable[16]={0,0,0,0,0,0,0,0,1,2,3,4,6,7,8,9};
+const uint8_t APBPrescTable[8]={0,0,0,0,1,2,3,4};
+static uint8_t usb_clock_ready;
+/* Восемь входов IGBT: PB0,PB1,PB2,PB10..PB14. PB3/SWO и PB8/PB9 CAN свободны. */
+static const uint16_t input_pins[8]={GPIO_PIN_0,GPIO_PIN_1,GPIO_PIN_2,
+ GPIO_PIN_10,GPIO_PIN_11,GPIO_PIN_12,GPIO_PIN_13,GPIO_PIN_14};
+/* До C runtime разрешены только регистровые операции; глобальные данные ещё не готовы. */
+void SystemInit(void) { SCB->CPACR|=0xFu<<20; SCB->VTOR=0x08010000u; }
+void SystemCoreClockUpdate(void) {
+ RCC_ClkInitTypeDef config; uint32_t latency;
+ HAL_RCC_GetClockConfig(&config,&latency);
+ SystemCoreClock=HAL_RCC_GetSysClockFreq() >> AHBPrescTable[config.AHBCLKDivider >> RCC_CFGR_HPRE_Pos];
+}
+/* ISR только считает миллисекунды: протоколы и модели обслуживаются главным циклом. */
+void SysTick_Handler(void) { HAL_IncTick(); }
+void board_fatal(uint32_t code) { clock_fault=code; for (;;) {} }
+static void peripheral_clock(void)
+{
+ RCC_OscInitTypeDef osc={0}; RCC_ClkInitTypeDef clk={0};
+ RCC_PeriphCLKInitTypeDef peripheral={0}; RCC_CRSInitTypeDef crs={0};
+ __HAL_RCC_PWR_CLK_ENABLE();
+ /* Для 160 МГц нужен Range1 Boost; Flash 4 wait states по таблице ST. */
+ if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1_BOOST)!=HAL_OK) board_fatal(3);
+ osc.OscillatorType=RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSI48;
+ osc.HSIState=RCC_HSI_ON; osc.HSICalibrationValue=RCC_HSICALIBRATION_DEFAULT;
+ osc.HSI48State=RCC_HSI48_ON;
+ osc.PLL.PLLState=RCC_PLL_ON; osc.PLL.PLLSource=RCC_PLLSOURCE_HSI;
+ /* HSI16 / 4 * 80 / 2 = 160 МГц; PLLQ также 160 МГц, FDCAN берёт PCLK1. */
+ osc.PLL.PLLM=RCC_PLLM_DIV4;
+#if EMU_USE_HSE
+#if EMU_HSE_HZ != 24000000u
+#error This board profile supports 24 MHz HSE or internal HSI16
+#endif
+ osc.OscillatorType|=RCC_OSCILLATORTYPE_HSE;
+ osc.HSEState=EMU_HSE_BYPASS ? RCC_HSE_BYPASS : RCC_HSE_ON;
+ osc.PLL.PLLSource=RCC_PLLSOURCE_HSE; osc.PLL.PLLM=RCC_PLLM_DIV6;
+#endif
+ osc.PLL.PLLN=80; osc.PLL.PLLP=RCC_PLLP_DIV2;
+ osc.PLL.PLLQ=RCC_PLLQ_DIV2; osc.PLL.PLLR=RCC_PLLR_DIV2;
+ if (HAL_RCC_OscConfig(&osc)!=HAL_OK) board_fatal(3);
+ clk.ClockType=RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
+ clk.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider=RCC_SYSCLK_DIV1;
+ clk.APB1CLKDivider=RCC_HCLK_DIV2; clk.APB2CLKDivider=RCC_HCLK_DIV1;
+ if (HAL_RCC_ClockConfig(&clk,FLASH_LATENCY_4)!=HAL_OK) board_fatal(3);
+ /* USB не зависит от PLL/HSE. USART2 и FDCAN получают 80 МГц PCLK1. */
+ peripheral.PeriphClockSelection=RCC_PERIPHCLK_USB|RCC_PERIPHCLK_FDCAN|RCC_PERIPHCLK_USART2;
+ peripheral.UsbClockSelection=RCC_USBCLKSOURCE_HSI48;
+ peripheral.FdcanClockSelection=RCC_FDCANCLKSOURCE_PCLK1;
+ peripheral.Usart2ClockSelection=RCC_USART2CLKSOURCE_PCLK1;
+ if (HAL_RCCEx_PeriphCLKConfig(&peripheral)!=HAL_OK) board_fatal(3);
+ __HAL_RCC_CRS_CLK_ENABLE();
+ crs.Prescaler=RCC_CRS_SYNC_DIV1; crs.Source=RCC_CRS_SYNC_SOURCE_USB;
+ crs.Polarity=RCC_CRS_SYNC_POLARITY_RISING;
+ crs.ReloadValue=__HAL_RCC_CRS_RELOADVALUE_CALCULATE(48000000u,1000u);
+ crs.ErrorLimitValue=RCC_CRS_ERRORLIMIT_DEFAULT;
+ crs.HSI48CalibrationValue=RCC_CRS_HSI48CALIBRATION_DEFAULT;
+ HAL_RCCEx_CRSConfig(&crs); usb_clock_ready=1;
+}
+void board_init(void)
+{
+ GPIO_InitTypeDef gpio={0};
+ /* Этот образ связан с RB-загрузчиком: 128 КБ Flash, непрерывная карта DBANK=0. */
+ __HAL_RCC_SYSCFG_CLK_ENABLE();
+ if (*(const uint16_t *)FLASHSIZE_BASE!=128u ||
+ (FLASH->OPTR&(FLASH_OPTR_DBANK|FLASH_OPTR_BFB2)) ||
+ (SYSCFG->MEMRMP&SYSCFG_MEMRMP_FB_MODE)) board_fatal(5);
+ peripheral_clock();
+ __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE();
+ __HAL_RCC_USART2_CLK_ENABLE(); __HAL_RCC_FDCAN_CLK_ENABLE();
+ /* ST-LINK VCP физически подключён к PA2/PA3; выбираем USART2 AF7, формат 8N1. */
+ gpio.Pin=GPIO_PIN_2|GPIO_PIN_3; gpio.Mode=GPIO_MODE_AF_PP;
+ gpio.Speed=GPIO_SPEED_FREQ_VERY_HIGH; gpio.Pull=GPIO_PULLUP;
+ gpio.Alternate=GPIO_AF7_USART2; HAL_GPIO_Init(GPIOA,&gpio);
+ gpio.Pin=GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14;
+ gpio.Mode=GPIO_MODE_INPUT; gpio.Pull=GPIO_PULLDOWN; HAL_GPIO_Init(GPIOB,&gpio);
+ /* PC0..PC7 — восемь ответов IGBT. Устанавливаем нули до включения выходов. */
+ gpio.Pin=0xff; gpio.Mode=GPIO_MODE_OUTPUT_PP; gpio.Pull=GPIO_NOPULL;
+ HAL_GPIO_WritePin(GPIOC,0xff,GPIO_PIN_RESET); HAL_GPIO_Init(GPIOC,&gpio);
+#if EMU_RS485_DE
+ gpio.Pin=GPIO_PIN_15; HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_RESET);
+ HAL_GPIO_Init(GPIOB,&gpio);
+#endif
+ /* FDCAN1 Classic CAN: PB8 RX, PB9 TX. PA11/PA12 остаются для USB FS. */
+ gpio.Pin=GPIO_PIN_8|GPIO_PIN_9; gpio.Mode=GPIO_MODE_AF_PP;
+ gpio.Pull=GPIO_PULLUP; gpio.Alternate=GPIO_AF9_FDCAN1; HAL_GPIO_Init(GPIOB,&gpio);
+ transport_init();
+}
+uint8_t board_usb_ready(void) { return usb_clock_ready; }
+/* Один шаг 1 мс. Бит n соответствует элементу input_pins[n], выход — PCn. */
+void board_tick(void)
+{
+ uint8_t inputs=0,outputs; unsigned i;
+ for(i=0;i<8;++i) if(HAL_GPIO_ReadPin(GPIOB,input_pins[i])==GPIO_PIN_SET) inputs|=(uint8_t)(1u<PSR&FDCAN_PSR_BO) && (can.Instance->CCCR&FDCAN_CCCR_INIT)) {
+ CLEAR_BIT(can.Instance->CCCR,FDCAN_CCCR_INIT);
+ count=0;
+ }
+ if(pending) {
+ if(!HAL_FDCAN_IsTxBufferMessagePending(&can,mailbox)) {
+ if(pending==1 && !(can.Instance->TXBTO&mailbox)) {count=0;++can_timeouts;}
+ pending=0;
+ } else if(pending==1 && (uint32_t)(now-sent_at)>=250u) {
+ (void)HAL_FDCAN_AbortTxRequest(&can,mailbox);
+ count=0;pending=2;++can_timeouts;
+ }
+ }
+ if(__HAL_FDCAN_GET_FLAG(&can,FDCAN_FLAG_RX_FIFO0_MESSAGE_LOST)) {
+ __HAL_FDCAN_CLEAR_FLAG(&can,FDCAN_FLAG_RX_FIFO0_MESSAGE_LOST);++can_overflows;
+ }
+ if(HAL_FDCAN_GetRxFifoFillLevel(&can,FDCAN_RX_FIFO0) &&
+ HAL_FDCAN_GetRxMessage(&can,FDCAN_RX_FIFO0,&rx,data)==HAL_OK) {
+ if(rx.DataLength==FDCAN_DLC_BYTES_8 && rx.IdType==FDCAN_EXTENDED_ID &&
+ rx.RxFrameType==FDCAN_DATA_FRAME && rx.FDFormat==FDCAN_CLASSIC_CAN &&
+ rx.Identifier==0xBA0000u+EMU_CAN_MODE-1 && !count && !pending) {
+ count=emu_can(data,words,&tag,&error);fragment=0;
+ }
+ }
+ if(count && !pending && HAL_FDCAN_GetTxFifoFreeLevel(&can)) {
+ emu_can_fragment(words,fragment,tag,error,data);
+ tx.Identifier=0xBA0020u+EMU_CAN_MODE-1;tx.IdType=FDCAN_EXTENDED_ID;
+ tx.TxFrameType=FDCAN_DATA_FRAME;tx.DataLength=FDCAN_DLC_BYTES_8;
+ tx.ErrorStateIndicator=FDCAN_ESI_ACTIVE;tx.BitRateSwitch=FDCAN_BRS_OFF;
+ tx.FDFormat=FDCAN_CLASSIC_CAN;tx.TxEventFifoControl=FDCAN_NO_TX_EVENTS;
+ if(HAL_FDCAN_AddMessageToTxFifoQ(&can,&tx,data)==HAL_OK) {
+ mailbox=HAL_FDCAN_GetLatestTxFifoQRequestBuffer(&can);
+ pending=1;sent_at=now;
+ if(++fragment>=count/2)count=0;
+ } else {count=0;++can_timeouts;}
+ }
+}
+
+/* HAL обслуживает TXE/TC и вызывает callback завершения; прикладной протокол в ISR не выполняется. */
+void USART2_IRQHandler(void) { HAL_UART_IRQHandler(&uart); }
+/* Вызывается HAL после TC: последний стоп-бит уже вышел на линию, можно отключить передатчик RS-485.
+ * Также используется локально для возврата в приём, если запуск передачи завершился ошибкой. */
+void HAL_UART_TxCpltCallback(UART_HandleTypeDef *h)
+{
+ if (h!=&uart) return;
+#if EMU_RS485_DE
+ HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_RESET);
+#endif
+ transmitting=0;
+}
+/* Принимает доступный байт без ожидания, накапливает кадр и определяет конец по паузе RTU.
+ * rx/tx имеют по 256 байт; invalid запрещает выполнение всего повреждённого кадра.
+ * now и last_rx измеряются в миллисекундах; переданный HAL буфер tx живёт до завершения ISR. */
+void transport_uart_poll(uint32_t now)
+{
+ static uint8_t rx[256], tx[256], invalid;
+ static uint16_t received;
+ static uint32_t last_rx;
+ uint32_t baud=uart.Init.BaudRate;
+ /* Изменение скорости применяется после окончания ответа на старой скорости.
+ * Незавершённый входной кадр сбрасывается, чтобы не объединить байты разных скоростей. */
+ if (!transmitting && baud!=emu_uart_baud()) {
+ uart.Init.BaudRate=emu_uart_baud();
+ if (HAL_UART_Init(&uart)!=HAL_OK) board_fatal(4);
+ received=0; invalid=0; return;
+ }
+ if (__HAL_UART_GET_FLAG(&uart,UART_FLAG_ORE) || __HAL_UART_GET_FLAG(&uart,UART_FLAG_FE) ||
+ __HAL_UART_GET_FLAG(&uart,UART_FLAG_NE) || __HAL_UART_GET_FLAG(&uart,UART_FLAG_PE)) {
+ /* На G4 ошибки сбрасываются записью ICR. RQR удаляет ошибочный байт;
+ * чтение SR/DR, применяемое на F407, здесь не используется. */
+ __HAL_UART_CLEAR_FLAG(&uart,UART_CLEAR_PEF|UART_CLEAR_FEF|UART_CLEAR_NEF|UART_CLEAR_OREF);
+ __HAL_UART_SEND_REQ(&uart,UART_RXDATA_FLUSH_REQUEST);
+ invalid=1; ++uart_errors; last_rx=now;
+ } else if (__HAL_UART_GET_FLAG(&uart,UART_FLAG_RXNE)) {
+ uint8_t b;
+ /* RXNE уже установлен; timeout=0 не допускает ожидания следующего байта.
+ * Во время передачи входные байты вычитываются и отбрасываются, чтобы не принять собственное эхо. */
+ if (HAL_UART_Receive(&uart,&b,1,0)==HAL_OK && !transmitting) {
+ if (received19200 baud RTU t3.5 is 1.75 ms; 3 ticks guarantees >=2 ms. */
+ /* Для 8N1 ниже порога считаем 3,5 символа по 10 бит с округлением и запасом одного тика.
+ * Выше 19200 бод используем 3 тика: с учётом фазы SysTick фактическая пауза не меньше 2 мс. */
+ if (received && !transmitting && (uint32_t)(now-last_rx)>=
+ (baud>19200u ? 3u : (35000u+baud-1)/baud+1u)) {
+ uint16_t length=invalid ? 0 : emu_rtu(rx,received,tx);
+ received=0; invalid=0;
+ if (length) {
+ transmitting=1;
+#if EMU_RS485_DE
+ HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_SET);
+#endif
+ /* Передача асинхронная: poll возвращается сразу. tx не перезаписывается, пока transmitting != 0. */
+ if (HAL_UART_Transmit_IT(&uart,tx,length)!=HAL_OK) {
+ ++uart_errors; HAL_UART_TxCpltCallback(&uart);
+ }
+ }
+ }
+}
diff --git a/c/set-protocol/ports/stm32g474-periph/wave_hal.c b/c/set-protocol/ports/stm32g474-periph/wave_hal.c
new file mode 100644
index 0000000..f6eb670
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-periph/wave_hal.c
@@ -0,0 +1,116 @@
+/**
+ * Аппаратный исполнитель генератора: таблица uint16_t в SRAM -> DMA1 -> DAC1/PA4.
+ * TIM6 задаёт частоту выборок аппаратными событиями TRGO; CPU не отправляет каждый отсчёт.
+ * Модуль владеет только DAC channel 1, TIM6 и DMA1 channel 1 и DMAMUX request DAC1_CHANNEL1.
+ */
+/* HAL backend owned by this board; the shared protocol remains hardware independent. */
+#include "wave_hal.h"
+#include "stm32g4xx_hal.h"
+
+static DAC_HandleTypeDef dac;
+static DMA_HandleTypeDef dma;
+static TIM_HandleTypeDef timer;
+static uint8_t initialized;
+
+/* Callback остановки set_wave_port. context не используется: имеется один статический комплект HAL handles.
+ * Сначала останавливаем источник триггеров, затем DMA/DAC. После первого запуска держим PA4 на нулевом коде;
+ * до инициализации функция ничего не делает. Второй канал DAC не перенастраивается. */
+static void stop(void *context)
+{
+ (void)context;
+ if (!initialized) return;
+ (void)HAL_TIM_Base_Stop(&timer);
+ (void)HAL_DAC_Stop_DMA(&dac,DAC_CHANNEL_1);
+ /* Keep channel 1 driving zero after stop; channel 2 is untouched. */
+ {
+ DAC_ChannelConfTypeDef config={0};
+ /* G4 при AHB=160 МГц требует HFSEL; выход направляем на внешний PA4. */
+ config.DAC_HighFrequency=DAC_HIGH_FREQUENCY_INTERFACE_MODE_AUTOMATIC;
+ config.DAC_ConnectOnChipPeripheral=DAC_CHIPCONNECT_EXTERNAL;
+ config.DAC_UserTrimming=DAC_TRIMMING_FACTORY;
+ config.DAC_Trigger=DAC_TRIGGER_NONE; config.DAC_OutputBuffer=DAC_OUTPUTBUFFER_ENABLE;
+ (void)HAL_DAC_ConfigChannel(&dac,&config,DAC_CHANNEL_1);
+ (void)HAL_DAC_SetValue(&dac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,0);
+ (void)HAL_DAC_Start(&dac,DAC_CHANNEL_1);
+ }
+}
+/* Callback запуска: samples — таблица кодов DAC, count — число отсчётов, rate — выборок/с.
+ * Таблица должна оставаться действительной и неизменной до stop(). Возвращает 0 при успехе, 1 при ошибке.
+ * Проверки формы сигнала выполняет общий генератор; здесь проверяется аппаратная выполнимость. */
+static int start(void *context,const uint16_t *samples,size_t count,uint32_t rate)
+{
+ GPIO_InitTypeDef gpio={0};
+ DAC_ChannelConfTypeDef config={0};
+ /* G4 при AHB=160 МГц требует HFSEL; выход направляем на внешний PA4. */
+ config.DAC_HighFrequency=DAC_HIGH_FREQUENCY_INTERFACE_MODE_AUTOMATIC;
+ config.DAC_ConnectOnChipPeripheral=DAC_CHIPCONNECT_EXTERNAL;
+ config.DAC_UserTrimming=DAC_TRIMMING_FACTORY;
+ TIM_MasterConfigTypeDef master={0};
+ /* При делителе APB1=2 таймер получает удвоенную частоту PCLK1, то есть 160 МГц.
+ * Эта формула соответствует дереву тактирования из board.c. */
+ uint32_t ticks,psc,clock=HAL_RCC_GetPCLK1Freq()*2u;
+ uintptr_t address=(uintptr_t)samples;
+ (void)context;
+ /* Разрешены 2..SET_WAVE_MAX отсчётов, частота до 1 МГц и только точное деление частоты таймера.
+ * Для DMA используем SRAM1/2: буфер должен лежать в 0x20000000..0x20017FFF; CCM оставляем архиву. */
+ if (!rate || rate>SET_WAVE_MAX_RATE || count<2 || count>SET_WAVE_MAX || clock%rate ||
+ address<0x20000000u || address+count*2>0x20018000u) return 1;
+ ticks=clock/rate;
+ /* Подбираем целые (PSC+1) и (ARR+1), оба не больше 65536.
+ * Частота выборок = timer_clock / ((PSC+1)*(ARR+1)); в регистры HAL передаёт значения минус один. */
+ for (psc=1;psc<=65536;++psc) if (ticks%psc==0 && ticks/psc<=65536) break;
+ if (psc>65536 || ticksBKP0R`, call
+`g474_request_boot(now)` for an update request and `g474_reset_poll(now)` from
+the main loop. Reset consumes and clears the backup-register request.
+
+Host test `test_boot_port.c` covers the 64 KiB slot boundaries, geometry rejection, vector validation,
+every block size 1..256, all final-word tails, sequential offsets and failures.
+Compile it with this directory and `../../include` on the include path.
+Hardware USB/Flash/power-loss tests remain required on the target board.
diff --git a/c/set-protocol/ports/stm32g474-usb-boot/boot_config.template.h b/c/set-protocol/ports/stm32g474-usb-boot/boot_config.template.h
new file mode 100644
index 0000000..fcc99c5
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-usb-boot/boot_config.template.h
@@ -0,0 +1,5 @@
+#ifndef G474_BOOT_CONFIG_H
+#define G474_BOOT_CONFIG_H
+#define G474_BOOT_NODE 16U
+#define G474_BOOT_MODEL "STM32G474RB-USB-BOOT"
+#endif
diff --git a/c/set-protocol/ports/stm32g474-usb-boot/boot_flash.c b/c/set-protocol/ports/stm32g474-usb-boot/boot_flash.c
new file mode 100644
index 0000000..598d11c
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-usb-boot/boot_flash.c
@@ -0,0 +1,109 @@
+#include "boot_port.h"
+#include "boot_image.h"
+#include "boot_writer.h"
+#include "stm32g4xx_hal.h"
+#include
+#define COMMIT 0x53455442UL
+static g474_writer writer;
+static bool geometry_valid(void)
+{
+ __HAL_RCC_SYSCFG_CLK_ENABLE();
+ return g474_geometry_supported(*(const uint16_t *)FLASHSIZE_BASE,
+ (FLASH->OPTR & FLASH_OPTR_DBANK) != 0,
+ (FLASH->OPTR & FLASH_OPTR_BFB2) != 0 ||
+ (SYSCFG->MEMRMP & SYSCFG_MEMRMP_FB_MODE) != 0);
+}
+static bool erase_page(uint32_t address)
+{
+ FLASH_EraseInitTypeDef config = {0};
+ uint32_t bank, page, error;
+ if (!g474_flash_page(address, (FLASH->OPTR & FLASH_OPTR_DBANK) != 0,
+ &bank, &page)) return false;
+ config.TypeErase = FLASH_TYPEERASE_PAGES;
+ config.Banks = bank == 2U ? FLASH_BANK_2 : FLASH_BANK_1;
+ config.Page = page; config.NbPages = 1;
+ return HAL_FLASHEx_Erase(&config, &error) == HAL_OK;
+}
+static bool invalidate(void *user)
+{
+ bool ok; (void)user;
+ if (!geometry_valid() || HAL_FLASH_Unlock() != HAL_OK) return false;
+ __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);
+ ok = erase_page(G474_META_BASE);
+ (void)HAL_FLASH_Lock(); return ok;
+}
+static bool erase(void *user, uint32_t size)
+{
+ uint32_t address, step;
+ bool ok = true; (void)user;
+ if (size < 8 || size > G474_APP_SIZE || !geometry_valid()) return false;
+ g474_writer_init(&writer, size);
+ if (HAL_FLASH_Unlock() != HAL_OK) return false;
+ __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);
+ step = (FLASH->OPTR & FLASH_OPTR_DBANK) ? 2048U : 4096U;
+ for (address = G474_APP_BASE; address < G474_APP_BASE + size; address += step) {
+ if (!erase_page(address)) { ok = false; break; }
+ }
+ (void)HAL_FLASH_Lock(); return ok;
+}
+static bool program_word(void *user, uint32_t offset, uint64_t word)
+{
+ uint64_t actual; (void)user;
+ if ((offset & 7U) || offset > G474_APP_SIZE - 8U) return false;
+ if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, G474_APP_BASE + offset,
+ word) != HAL_OK) return false;
+ memcpy(&actual, (const void *)(G474_APP_BASE + offset), 8);
+ return actual == word;
+}
+static bool write(void *user, uint32_t offset, const uint8_t *data, uint16_t length)
+{
+ bool ok; (void)user;
+ if (HAL_FLASH_Unlock() != HAL_OK) return false;
+ __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);
+ ok = g474_writer_write(&writer, offset, data, length, program_word, 0);
+ (void)HAL_FLASH_Lock(); return ok;
+}
+static bool verify(void *user, uint32_t size, uint32_t crc)
+{
+ (void)user;
+ return !writer.failed && !writer.used && writer.received == size &&
+ writer.size == size &&
+ g474_vectors_valid((const uint8_t *)G474_APP_BASE, size) &&
+ setp_crc32((const uint8_t *)G474_APP_BASE, size) == crc;
+}
+static bool commit(void *user, uint32_t size, uint32_t crc)
+{
+ uint32_t record[4] = {size, crc, ~size, COMMIT};
+ uint64_t word; unsigned i; bool ok = true; (void)user;
+ if (HAL_FLASH_Unlock() != HAL_OK) return false;
+ __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);
+ for (i = 0; i < sizeof(record); i += 8) {
+ memcpy(&word, (const uint8_t *)record + i, 8);
+ if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, G474_META_BASE + i,
+ word) != HAL_OK) { ok = false; break; }
+ }
+ (void)HAL_FLASH_Lock();
+ return ok && !memcmp((const void *)G474_META_BASE, record, sizeof(record));
+}
+bool g474_app_valid(void)
+{
+ /* Same reset policy as F407: SWD images need no USB commit record. */
+ return geometry_valid() &&
+ g474_vectors_valid((const uint8_t *)G474_APP_BASE, G474_APP_SIZE);
+}
+static void activate(void *user) { (void)user; }
+const setp_boot_ops g474_boot_ops = {invalidate, erase, write, verify, commit, activate};
+__attribute__((naked,noreturn)) static void enter(uint32_t sp, uint32_t pc)
+{
+ __asm volatile("msr msp, r0\nmovs r2, #0\nmsr control, r2\ndsb\nisb\ncpsie i\nbx r1\n");
+}
+void g474_jump_app(void)
+{
+ unsigned i;
+ uint32_t sp = *(const uint32_t *)G474_APP_BASE;
+ uint32_t pc = *(const uint32_t *)(G474_APP_BASE + 4);
+ __disable_irq(); SysTick->CTRL = 0;
+ for (i = 0; i < 8; ++i) { NVIC->ICER[i] = 0xffffffff; NVIC->ICPR[i] = 0xffffffff; }
+ SCB->ICSR = SCB_ICSR_PENDSTCLR_Msk | SCB_ICSR_PENDSVCLR_Msk;
+ SCB->VTOR = G474_APP_BASE; enter(sp, pc);
+}
diff --git a/c/set-protocol/ports/stm32g474-usb-boot/boot_image.h b/c/set-protocol/ports/stm32g474-usb-boot/boot_image.h
new file mode 100644
index 0000000..6a4e2ba
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-usb-boot/boot_image.h
@@ -0,0 +1,34 @@
+#ifndef G474_BOOT_IMAGE_H
+#define G474_BOOT_IMAGE_H
+#include "boot_port.h"
+#include
+/* SRAM1+SRAM2: 96 KB; CCM: 32 KB, also aliased after SRAM2. */
+static inline bool g474_vectors_valid(const uint8_t *image, uint32_t size)
+{
+ uint32_t sp, pc;
+ if (size < 8 || size > G474_APP_SIZE) return false;
+ memcpy(&sp, image, 4); memcpy(&pc, image + 4, 4);
+ return !(sp & 7U) &&
+ ((sp > 0x20000000UL && sp <= 0x20020000UL) ||
+ (sp > 0x10000000UL && sp <= 0x10008000UL)) &&
+ (pc & 1U) && (pc & ~1UL) >= G474_APP_BASE &&
+ (pc & ~1UL) < G474_APP_BASE + size;
+}
+/* A contiguous xB slot at 0x08010000 exists only with DBANK=0.
+ * In dual mode the upper 64 KB is at 0x08040000, not at this slot address. */
+static inline bool g474_geometry_supported(uint16_t flash_kb, bool dual, bool swapped)
+{
+ return flash_kb == 128U && !dual && !swapped;
+}
+static inline bool g474_flash_page(uint32_t address, bool dual,
+ uint32_t *bank, uint32_t *page)
+{
+ uint32_t offset;
+ if (dual || address < G474_META_BASE || address >= G474_APP_BASE + G474_APP_SIZE)
+ return false;
+ offset = address - 0x08000000UL;
+ *bank = 1U;
+ *page = offset / 4096U;
+ return true;
+}
+#endif
diff --git a/c/set-protocol/ports/stm32g474-usb-boot/boot_main.c b/c/set-protocol/ports/stm32g474-usb-boot/boot_main.c
new file mode 100644
index 0000000..8897565
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-usb-boot/boot_main.c
@@ -0,0 +1,39 @@
+#include "stm32g4xx_hal.h"
+#include "boot_port.h"
+#include "usb_cdc.h"
+static volatile uint32_t clock_fault;
+void SysTick_Handler(void) { HAL_IncTick(); }
+static void fatal(uint32_t code) { clock_fault = code; for (;;) {} }
+static void usb_clock_init(void)
+{
+ RCC_OscInitTypeDef osc = {0};
+ RCC_PeriphCLKInitTypeDef peripheral = {0};
+ RCC_CRSInitTypeDef crs = {0};
+ /* CPU stays on reset HSI16. USB uses HSI48 disciplined by USB SOF. */
+ osc.OscillatorType = RCC_OSCILLATORTYPE_HSI48;
+ osc.HSI48State = RCC_HSI48_ON;
+ if (HAL_RCC_OscConfig(&osc) != HAL_OK) fatal(2);
+ peripheral.PeriphClockSelection = RCC_PERIPHCLK_USB;
+ peripheral.UsbClockSelection = RCC_USBCLKSOURCE_HSI48;
+ if (HAL_RCCEx_PeriphCLKConfig(&peripheral) != HAL_OK) fatal(3);
+ __HAL_RCC_CRS_CLK_ENABLE();
+ crs.Prescaler = RCC_CRS_SYNC_DIV1;
+ crs.Source = RCC_CRS_SYNC_SOURCE_USB;
+ crs.Polarity = RCC_CRS_SYNC_POLARITY_RISING;
+ crs.ReloadValue = __HAL_RCC_CRS_RELOADVALUE_CALCULATE(48000000U, 1000U);
+ crs.ErrorLimitValue = RCC_CRS_ERRORLIMIT_DEFAULT;
+ crs.HSI48CalibrationValue = RCC_CRS_HSI48CALIBRATION_DEFAULT;
+ HAL_RCCEx_CRSConfig(&crs);
+}
+void boot_protocol_init(void);
+void boot_protocol_poll(uint32_t now);
+int main(void)
+{
+ bool requested;
+ SCB->VTOR = FLASH_BASE;
+ requested = g474_boot_requested();
+ if (!requested && g474_app_valid()) g474_jump_app();
+ if (HAL_Init() != HAL_OK) fatal(1);
+ usb_clock_init(); boot_protocol_init(); usb_cdc_init();
+ for (;;) { uint32_t now = HAL_GetTick(); usb_cdc_poll(now); boot_protocol_poll(now); }
+}
diff --git a/c/set-protocol/ports/stm32g474-usb-boot/boot_port.h b/c/set-protocol/ports/stm32g474-usb-boot/boot_port.h
new file mode 100644
index 0000000..9328a62
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-usb-boot/boot_port.h
@@ -0,0 +1,15 @@
+#ifndef G474_BOOT_PORT_H
+#define G474_BOOT_PORT_H
+#include "set_boot.h"
+/* STM32G474xB (128 KB), single-bank mode, normal bank mapping. */
+#define G474_APP_BASE 0x08010000UL
+#define G474_APP_SIZE 0x00010000UL
+#define G474_META_BASE 0x0800F000UL
+#define G474_BOOT_REQUEST 0x1003U
+extern const setp_boot_ops g474_boot_ops;
+bool g474_app_valid(void);
+bool g474_boot_requested(void);
+void g474_request_boot(uint32_t now);
+void g474_reset_poll(uint32_t now);
+void g474_jump_app(void);
+#endif
diff --git a/c/set-protocol/ports/stm32g474-usb-boot/boot_request.c b/c/set-protocol/ports/stm32g474-usb-boot/boot_request.c
new file mode 100644
index 0000000..5c00e6c
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-usb-boot/boot_request.c
@@ -0,0 +1,26 @@
+#include "boot_port.h"
+#include "stm32g4xx_hal.h"
+#define REQUEST_MAGIC 0x42555342UL
+static uint32_t reset_at;
+static bool reset_pending;
+static void backup_access(void)
+{
+ __HAL_RCC_PWR_CLK_ENABLE();
+ HAL_PWR_EnableBkUpAccess();
+ __HAL_RCC_RTCAPB_CLK_ENABLE();
+}
+bool g474_boot_requested(void)
+{
+ bool requested;
+ backup_access(); requested = TAMP->BKP0R == REQUEST_MAGIC;
+ TAMP->BKP0R = 0; return requested;
+}
+void g474_request_boot(uint32_t now)
+{
+ backup_access(); TAMP->BKP0R = REQUEST_MAGIC;
+ reset_at = now + 500; reset_pending = true;
+}
+void g474_reset_poll(uint32_t now)
+{
+ if (reset_pending && (int32_t)(now - reset_at) >= 0) NVIC_SystemReset();
+}
diff --git a/c/set-protocol/ports/stm32g474-usb-boot/boot_usb_port.c b/c/set-protocol/ports/stm32g474-usb-boot/boot_usb_port.c
new file mode 100644
index 0000000..8e9c615
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-usb-boot/boot_usb_port.c
@@ -0,0 +1,94 @@
+/* STM32G474 USB FS device, dedicated PA11/PA12 pins, single-buffer PMA. */
+#include "usbd_core.h"
+#include "usbd_cdc.h"
+
+static PCD_HandleTypeDef pcd;
+/* Единственный статический объект CDC вместо heap. Контракт предполагает один экземпляр класса. */
+static USBD_CDC_HandleTypeDef class_memory;
+/* Проверяет размер запроса и возвращает статический объект либо NULL. Не является универсальным malloc. */
+void *BoardUsb_Alloc(uint32_t size) { return size<=sizeof(class_memory)?&class_memory:NULL; }
+/* Освобождение не требуется: объект существует до перезапуска MCU. */
+void BoardUsb_Free(void *ptr) { (void)ptr; }
+/* Согласует enum HAL и USB: OK -> OK, BUSY -> BUSY, остальные результаты -> FAIL. */
+static USBD_StatusTypeDef status(HAL_StatusTypeDef s) {
+ return s==HAL_OK?USBD_OK:s==HAL_BUSY?USBD_BUSY:USBD_FAIL;
+}
+/* Единственная точка входа USB ISR; события декодирует HAL_PCD_IRQHandler. */
+void USB_LP_IRQHandler(void) { HAL_PCD_IRQHandler(&pcd); }
+/* Передаёт 8-байтовый SETUP control-запрос стандартному обработчику USB Device. */
+void HAL_PCD_SetupStageCallback(PCD_HandleTypeDef *h) { (void)USBD_LL_SetupStage(h->pData,(uint8_t *)h->Setup); }
+/* OUT: хост отправил данные устройству; передаём номер endpoint и его принятый буфер. */
+void HAL_PCD_DataOutStageCallback(PCD_HandleTypeDef *h,uint8_t ep) { (void)USBD_LL_DataOutStage(h->pData,ep,h->OUT_ep[ep].xfer_buff); }
+/* IN: завершена передача данных от устройства к хосту; уведомляем USB Device. */
+void HAL_PCD_DataInStageCallback(PCD_HandleTypeDef *h,uint8_t ep) { (void)USBD_LL_DataInStage(h->pData,ep,h->IN_ep[ep].xfer_buff); }
+/* Передаёт событие начала USB-кадра; в текущей конфигурации SOF-прерывания отключены. */
+void HAL_PCD_SOFCallback(PCD_HandleTypeDef *h) { (void)USBD_LL_SOF(h->pData); }
+/* При reset шины фиксируем Full Speed и сбрасываем состояние USB Device. */
+void HAL_PCD_ResetCallback(PCD_HandleTypeDef *h) {
+ (void)USBD_LL_SetSpeed(h->pData,USBD_SPEED_FULL); (void)USBD_LL_Reset(h->pData);
+}
+/* Сообщает middleware о приостановке шины; аппаратный low-power режим здесь не включается. */
+void HAL_PCD_SuspendCallback(PCD_HandleTypeDef *h) { (void)USBD_LL_Suspend(h->pData); }
+/* Сообщает middleware о возобновлении обмена после suspend. */
+void HAL_PCD_ResumeCallback(PCD_HandleTypeDef *h) { (void)USBD_LL_Resume(h->pData); }
+/* Уведомление верхнего уровня о подключении контроллера. */
+void HAL_PCD_ConnectCallback(PCD_HandleTypeDef *h) { (void)USBD_LL_DevConnected(h->pData); }
+/* Уведомление верхнего уровня об отключении контроллера. */
+void HAL_PCD_DisconnectCallback(PCD_HandleTypeDef *h) { (void)USBD_LL_DevDisconnected(h->pData); }
+/* Переадресует незавершённую изохронную IN-передачу; обычный CDC использует bulk endpoints. */
+void HAL_PCD_ISOINIncompleteCallback(PCD_HandleTypeDef *h,uint8_t ep) { (void)USBD_LL_IsoINIncomplete(h->pData,ep); }
+/* Переадресует незавершённую изохронную OUT-передачу для полноты порта PCD. */
+void HAL_PCD_ISOOUTIncompleteCallback(PCD_HandleTypeDef *h,uint8_t ep) { (void)USBD_LL_IsoOUTIncomplete(h->pData,ep); }
+
+USBD_StatusTypeDef USBD_LL_Init(USBD_HandleTypeDef *d) {
+ /* Dedicated USB pins: no GPIO alternate-function selection on G474. */
+ __HAL_RCC_USB_CLK_ENABLE();
+ pcd.Instance = USB; pcd.Init.dev_endpoints = 8;
+ pcd.Init.speed = PCD_SPEED_FULL; pcd.Init.phy_itface = PCD_PHY_EMBEDDED;
+ pcd.Init.Sof_enable = DISABLE; pcd.Init.low_power_enable = DISABLE;
+ pcd.Init.lpm_enable = DISABLE; pcd.Init.battery_charging_enable = DISABLE;
+ pcd.pData = d; d->pData = &pcd;
+ if (HAL_PCD_Init(&pcd) != HAL_OK) return USBD_FAIL;
+ /* BTABLE: 0x00..0x3f; EP0 RX/TX 64 bytes each; CDC data RX/TX 64;
+ * CDC notification 16. Byte offsets are disjoint within the 1 KB PMA. */
+ if (HAL_PCDEx_PMAConfig(&pcd, 0x00, PCD_SNG_BUF, 0x40) != HAL_OK ||
+ HAL_PCDEx_PMAConfig(&pcd, 0x80, PCD_SNG_BUF, 0x80) != HAL_OK ||
+ HAL_PCDEx_PMAConfig(&pcd, CDC_OUT_EP, PCD_SNG_BUF, 0xc0) != HAL_OK ||
+ HAL_PCDEx_PMAConfig(&pcd, CDC_IN_EP, PCD_SNG_BUF, 0x100) != HAL_OK ||
+ HAL_PCDEx_PMAConfig(&pcd, CDC_CMD_EP, PCD_SNG_BUF, 0x140) != HAL_OK)
+ return USBD_FAIL;
+ HAL_NVIC_SetPriority(USB_LP_IRQn, 7, 0); HAL_NVIC_EnableIRQ(USB_LP_IRQn);
+ return USBD_OK;
+}
+/* Запрещает USB IRQ до деинициализации контроллера, чтобы callbacks не работали с разобранным состоянием. */
+USBD_StatusTypeDef USBD_LL_DeInit(USBD_HandleTypeDef *d) {
+ HAL_NVIC_DisableIRQ(USB_LP_IRQn); return status(HAL_PCD_DeInit(d->pData));
+}
+/* Включает USB device через HAL после завершения настройки класса и endpoints. */
+USBD_StatusTypeDef USBD_LL_Start(USBD_HandleTypeDef *d) { return status(HAL_PCD_Start(d->pData)); }
+/* Останавливает контроллер USB device; память состояния остаётся статической. */
+USBD_StatusTypeDef USBD_LL_Stop(USBD_HandleTypeDef *d) { return status(HAL_PCD_Stop(d->pData)); }
+/* Открывает endpoint: ep содержит номер и направление (бит 7), type — тип, mps — максимальный пакет в байтах. */
+USBD_StatusTypeDef USBD_LL_OpenEP(USBD_HandleTypeDef *d,uint8_t ep,uint8_t type,uint16_t mps) { return status(HAL_PCD_EP_Open(d->pData,ep,mps,type)); }
+/* Закрывает endpoint по USB-адресу ep, включая бит направления. */
+USBD_StatusTypeDef USBD_LL_CloseEP(USBD_HandleTypeDef *d,uint8_t ep) { return status(HAL_PCD_EP_Close(d->pData,ep)); }
+/* Очищает FIFO указанного endpoint через HAL. */
+USBD_StatusTypeDef USBD_LL_FlushEP(USBD_HandleTypeDef *d,uint8_t ep) { return status(HAL_PCD_EP_Flush(d->pData,ep)); }
+/* Устанавливает STALL: endpoint отклоняет неподдерживаемую операцию хоста. */
+USBD_StatusTypeDef USBD_LL_StallEP(USBD_HandleTypeDef *d,uint8_t ep) { return status(HAL_PCD_EP_SetStall(d->pData,ep)); }
+/* Снимает STALL по запросу верхнего уровня USB Device. */
+USBD_StatusTypeDef USBD_LL_ClearStallEP(USBD_HandleTypeDef *d,uint8_t ep) { return status(HAL_PCD_EP_ClrStall(d->pData,ep)); }
+/* Применяет адрес устройства, назначенный хостом при перечислении USB. */
+USBD_StatusTypeDef USBD_LL_SetUSBAddress(USBD_HandleTypeDef *d,uint8_t address) { return status(HAL_PCD_SetAddress(d->pData,address)); }
+/* Запускает IN-передачу size байтов из buf. Буфер должен жить до callback завершения; копии здесь нет. */
+USBD_StatusTypeDef USBD_LL_Transmit(USBD_HandleTypeDef *d,uint8_t ep,uint8_t *buf,uint32_t size) { return status(HAL_PCD_EP_Transmit(d->pData,ep,buf,size)); }
+/* Взводит OUT endpoint для приёма в buf ёмкостью size байтов; завершение придёт через callback PCD. */
+USBD_StatusTypeDef USBD_LL_PrepareReceive(USBD_HandleTypeDef *d,uint8_t ep,uint8_t *buf,uint32_t size) { return status(HAL_PCD_EP_Receive(d->pData,ep,buf,size)); }
+/* Читает состояние endpoint из HAL handle: bit7 выбирает IN, младшие 7 бит — номер. */
+uint8_t USBD_LL_IsStallEP(USBD_HandleTypeDef *d,uint8_t ep) {
+ PCD_HandleTypeDef *h=d->pData; return ep&0x80?h->IN_ep[ep&0x7F].is_stall:h->OUT_ep[ep].is_stall;
+}
+/* Возвращает фактическое число принятых байтов последней OUT-передачи. */
+uint32_t USBD_LL_GetRxDataSize(USBD_HandleTypeDef *d,uint8_t ep) { return HAL_PCD_EP_GetRxCount(d->pData,ep); }
+/* Блокирующая задержка ST middleware в миллисекундах HAL; требует работающего SysTick. */
+void USBD_LL_Delay(uint32_t ms) { HAL_Delay(ms); }
diff --git a/c/set-protocol/ports/stm32g474-usb-boot/boot_usb_stream.c b/c/set-protocol/ports/stm32g474-usb-boot/boot_usb_stream.c
new file mode 100644
index 0000000..1a845ff
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-usb-boot/boot_usb_stream.c
@@ -0,0 +1,10 @@
+#include "stm32g4xx_hal.h"
+#include "boot_port.h"
+#include "boot_config.h"
+#define BOOT_OPS g474_boot_ops
+#define BOOT_APP_BASE G474_APP_BASE
+#define BOOT_APP_SIZE G474_APP_SIZE
+#define BOOT_NODE G474_BOOT_NODE
+#define BOOT_MODEL G474_BOOT_MODEL
+#define BOOT_RESET() NVIC_SystemReset()
+#include "../stm32-usb-boot/boot_usb_protocol.inc"
diff --git a/c/set-protocol/ports/stm32g474-usb-boot/boot_writer.h b/c/set-protocol/ports/stm32g474-usb-boot/boot_writer.h
new file mode 100644
index 0000000..38d56af
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-usb-boot/boot_writer.h
@@ -0,0 +1,37 @@
+#ifndef G474_BOOT_WRITER_H
+#define G474_BOOT_WRITER_H
+#include
+#include
+#include
+/* ECC Flash must be programmed once per 64-bit word. Keep partial words in RAM
+ * across SET packets; pad only the final word, without changing the image CRC. */
+typedef bool (*g474_program_word)(void *user, uint32_t offset, uint64_t word);
+typedef struct {
+ uint32_t size, received;
+ uint8_t pending[8];
+ uint8_t used;
+ bool failed;
+} g474_writer;
+static inline void g474_writer_init(g474_writer *w, uint32_t size)
+{
+ memset(w, 0, sizeof(*w)); w->size = size;
+ memset(w->pending, 0xff, sizeof(w->pending));
+}
+static inline bool g474_writer_write(g474_writer *w, uint32_t offset,
+ const uint8_t *data, uint16_t length, g474_program_word program, void *user)
+{
+ if (w->failed || !length || offset != w->received ||
+ offset > w->size || length > w->size - offset) return false;
+ while (length--) {
+ w->pending[w->used++] = *data++; ++w->received;
+ if (w->used == 8 || w->received == w->size) {
+ uint64_t word;
+ uint32_t start = w->received - w->used;
+ memcpy(&word, w->pending, sizeof(word));
+ if (!program(user, start, word)) { w->failed = true; return false; }
+ w->used = 0; memset(w->pending, 0xff, sizeof(w->pending));
+ }
+ }
+ return true;
+}
+#endif
diff --git a/c/set-protocol/ports/stm32g474-usb-boot/test_boot_port.c b/c/set-protocol/ports/stm32g474-usb-boot/test_boot_port.c
new file mode 100644
index 0000000..b2bb928
--- /dev/null
+++ b/c/set-protocol/ports/stm32g474-usb-boot/test_boot_port.c
@@ -0,0 +1,97 @@
+#include "boot_image.h"
+#include "boot_writer.h"
+#include
+#include
+static uint8_t flash[1024];
+static bool touched[128];
+static bool fail_write;
+static bool program(void *user, uint32_t offset, uint64_t word)
+{
+ (void)user;
+ assert(!(offset & 7U)); assert(offset <= sizeof(flash) - 8);
+ assert(!touched[offset / 8]);
+ if (fail_write) return false;
+ touched[offset / 8] = true;
+ memcpy(flash + offset, &word, 8); return true;
+}
+static void test_writer(void)
+{
+ uint8_t image[521];
+ uint32_t block, size, i;
+ for (i = 0; i < sizeof(image); ++i) image[i] = (uint8_t)(i * 19U);
+ /* Every block size, all possible final-word tails, including all-FF words. */
+ memset(image + 128, 0xff, 32);
+ for (block = 1; block <= 256; ++block) {
+ for (size = 512; size <= sizeof(image); ++size) {
+ g474_writer w;
+ memset(flash, 0xff, sizeof(flash)); memset(touched, 0, sizeof(touched));
+ g474_writer_init(&w, size);
+ for (i = 0; i < size;) {
+ uint16_t n = (uint16_t)(size - i < block ? size - i : block);
+ assert(g474_writer_write(&w, i, image + i, n, program, 0)); i += n;
+ }
+ assert(!w.failed && !w.used && w.received == size);
+ assert(!memcmp(flash, image, size));
+ for (i = size; i < sizeof(flash); ++i) assert(flash[i] == 0xff);
+ assert(!g474_writer_write(&w, size, image, 1, program, 0));
+ }
+ }
+ {
+ g474_writer w;
+ g474_writer_init(&w, 9);
+ assert(!g474_writer_write(&w, 1, image, 1, program, 0));
+ assert(!g474_writer_write(&w, 0, image, 10, program, 0));
+ assert(!g474_writer_write(&w, 0, image, 0, program, 0));
+ assert(w.received == 0);
+ memset(touched, 0, sizeof(touched)); fail_write = true;
+ assert(!g474_writer_write(&w, 0, image, 8, program, 0));
+ assert(w.failed);
+ fail_write = false;
+ assert(!g474_writer_write(&w, w.received, image, 1, program, 0));
+ g474_writer_init(&w, 9);
+ assert(g474_writer_write(&w, 0, image, 9, program, 0));
+ }
+}
+static void check_page(uint32_t address, bool dual, uint32_t bank, uint32_t page)
+{
+ uint32_t b, p;
+ assert(g474_flash_page(address, dual, &b, &p));
+ assert(b == bank && p == page);
+}
+int main(void)
+{
+ uint32_t vectors[2] = {0x20018000UL, G474_APP_BASE + 9};
+ uint32_t b, p;
+ const uint8_t *image = (const uint8_t *)vectors;
+ assert(g474_geometry_supported(128, false, false));
+ assert(!g474_geometry_supported(128, true, false));
+ assert(!g474_geometry_supported(128, false, true));
+ assert(!g474_geometry_supported(512, false, false));
+ assert(!g474_geometry_supported(256, false, false));
+ assert(!g474_geometry_supported(0xffff, false, false));
+ check_page(G474_META_BASE, false, 1, 15);
+ check_page(G474_APP_BASE, false, 1, 16);
+ check_page(0x0801ffffUL, false, 1, 31);
+ assert(!g474_flash_page(G474_META_BASE - 1, false, &b, &p));
+ assert(!g474_flash_page(0x08020000UL, false, &b, &p));
+ assert(!g474_flash_page(0x08040000UL, false, &b, &p));
+ assert(!g474_flash_page(G474_META_BASE, true, &b, &p));
+ assert(!g474_flash_page(G474_APP_BASE, true, &b, &p));
+ assert(g474_vectors_valid(image, G474_APP_SIZE));
+ vectors[0] = 0x10008000UL; assert(g474_vectors_valid(image, 16));
+ vectors[0] = 0x20020000UL; assert(g474_vectors_valid(image, 16));
+ vectors[0] = 0x20020008UL; assert(!g474_vectors_valid(image, 16));
+ vectors[0] = 0x10008008UL; assert(!g474_vectors_valid(image, 16));
+ vectors[0] = 0x20000000UL; assert(!g474_vectors_valid(image, 16));
+ vectors[0] = 0x20017fffUL; assert(!g474_vectors_valid(image, 16));
+ vectors[0] = 0x20018000UL;
+ vectors[1] = G474_APP_BASE + 8; assert(!g474_vectors_valid(image, 16));
+ vectors[1] = G474_APP_BASE - 1; assert(!g474_vectors_valid(image, 16));
+ vectors[1] = G474_APP_BASE + 17; assert(!g474_vectors_valid(image, 16));
+ vectors[1] = G474_APP_BASE + 9;
+ assert(!g474_vectors_valid(image, 7));
+ assert(!g474_vectors_valid(image, G474_APP_SIZE + 1));
+ test_writer();
+ puts("G474: bank/page boundaries, vectors, all block sizes and ECC word tails passed");
+ return 0;
+}
diff --git a/c/set-protocol/src/set_boot.c b/c/set-protocol/src/set_boot.c
new file mode 100644
index 0000000..4b2922f
--- /dev/null
+++ b/c/set-protocol/src/set_boot.c
@@ -0,0 +1,76 @@
+#include "set_boot.h"
+#include
+
+void setp_boot_init(setp_boot *b, const setp_boot_ops *ops, void *user,
+ uint32_t base, uint32_t capacity)
+{
+ memset(b, 0, sizeof(*b)); b->ops=*ops; b->user=user;
+ b->base=base; b->capacity=capacity;
+ b->status.max_block_size=256;
+}
+static bool zero_hash(const uint8_t *p)
+{
+ unsigned i; for(i=0;i<32;++i) if(p[i]) return false;
+ return true;
+}
+static uint16_t command(setp_boot *b, uint16_t type, const uint8_t *p, uint16_t n)
+{
+ setp_fw_begin_t begin;
+ setp_fw_data_t data;
+ setp_fw_end_t end;
+ switch(type) {
+ case SETP_MSG_FW_BEGIN:
+ if(!setp_fw_begin_decode(p,n,&begin)) return SETP_STATUS_INVALID_LENGTH;
+ /* This backend implements CRC integrity only, no resume/signature/SHA. */
+ if(begin.slot || begin.flags!=SETP_FW_FLAG_ERASE_SLOT ||
+ begin.signature_length || begin.signing_key_id || !zero_hash(begin.sha256))
+ return SETP_STATUS_UNSUPPORTED;
+ if(begin.base_address!=b->base || begin.image_size<8 ||
+ begin.image_size>b->capacity || begin.block_size>256)
+ return SETP_STATUS_INVALID_ARGUMENT;
+ b->status.state=SETP_FW_FAILED;
+ if(!b->ops.invalidate(b->user) || !b->ops.erase(b->user,begin.image_size))
+ return SETP_STATUS_INTERNAL;
+ b->crc=begin.image_crc32; b->block_size=begin.block_size;
+ b->status.image_size=begin.image_size; b->status.next_offset=0;
+ b->status.state=SETP_FW_RECEIVING; return SETP_STATUS_OK;
+ case SETP_MSG_FW_DATA:
+ if(b->status.state!=SETP_FW_RECEIVING) return SETP_STATUS_WRONG_STATE;
+ if(!setp_fw_data_decode(p,n,&data)) return SETP_STATUS_CRC;
+ if(data.flags || data.data_length>b->block_size) return SETP_STATUS_INVALID_ARGUMENT;
+ if(data.offset!=b->status.next_offset) return SETP_STATUS_SEQUENCE;
+ if(data.data_length>b->status.image_size-b->status.next_offset)
+ return SETP_STATUS_NO_SPACE;
+ if(!b->ops.write(b->user,data.offset,data.data,data.data_length)) {
+ b->status.state=SETP_FW_FAILED; return SETP_STATUS_INTERNAL;
+ }
+ b->status.next_offset+=data.data_length; return SETP_STATUS_OK;
+ case SETP_MSG_FW_END:
+ if(b->status.state!=SETP_FW_RECEIVING) return SETP_STATUS_WRONG_STATE;
+ if(!setp_fw_end_decode(p,n,&end)) return SETP_STATUS_INVALID_LENGTH;
+ if(!zero_hash(end.sha256)) return SETP_STATUS_UNSUPPORTED;
+ if(end.image_size!=b->status.image_size || end.image_crc32!=b->crc ||
+ b->status.next_offset!=b->status.image_size) return SETP_STATUS_SEQUENCE;
+ b->status.state=SETP_FW_FAILED;
+ if(!b->ops.verify(b->user,end.image_size,b->crc)) return SETP_STATUS_VERIFY_FAILED;
+ if(!b->ops.commit(b->user,end.image_size,b->crc)) return SETP_STATUS_INTERNAL;
+ b->status.state=SETP_FW_READY; return SETP_STATUS_OK;
+ case SETP_MSG_FW_ABORT:
+ if(n) return SETP_STATUS_INVALID_LENGTH;
+ b->status.state=SETP_FW_IDLE; b->status.next_offset=0;
+ return SETP_STATUS_OK;
+ case SETP_MSG_FW_STATUS:
+ return n ? SETP_STATUS_INVALID_LENGTH : SETP_STATUS_OK;
+ case SETP_MSG_FW_ACTIVATE:
+ if(n) return SETP_STATUS_INVALID_LENGTH;
+ if(b->status.state!=SETP_FW_READY) return SETP_STATUS_WRONG_STATE;
+ b->ops.activate(b->user); return SETP_STATUS_OK;
+ default: return SETP_STATUS_UNSUPPORTED;
+ }
+}
+uint16_t setp_boot_command(setp_boot *b, uint16_t type, const uint8_t *p, uint16_t n)
+{
+ uint16_t result=command(b,type,p,n);
+ if(type!=SETP_MSG_FW_STATUS) b->status.last_error=result;
+ return result;
+}
diff --git a/c/set-protocol/src/set_emu_igbt.c b/c/set-protocol/src/set_emu_igbt.c
new file mode 100644
index 0000000..4226e78
--- /dev/null
+++ b/c/set-protocol/src/set_emu_igbt.c
@@ -0,0 +1,142 @@
+#include "set_emu_igbt.h"
+#include
+static void output(set_igbt_emu *s)
+{
+ unsigned ch;s->rx=0;
+ if(!s->enabled)return;
+ for(ch=0;chchannel[ch];uint8_t bit=(uint8_t)(1u<flags&2))continue;
+ if(c->mode==1)level=0;
+ else if(c->mode==2)level=1;
+ else {level=(uint8_t)(c->mode==3?0:!!(s->logical_rx&bit));if(c->flags&4)level^=1;}
+ if(level)s->rx|=bit;
+ }
+}
+void set_igbt_init(set_igbt_emu *s)
+{
+ unsigned ch;memset(s,0,sizeof(*s));s->enabled=1;s->delay_ms=1;s->width_ms=1;
+ for(ch=0;chchannel[ch];
+ c->flags=3;c->on_delay=c->off_delay=1;c->on_width=c->off_width=1;
+ c->tx_width=10;c->edges=3;
+ }
+}
+void set_igbt_tick(set_igbt_emu *s,uint8_t pins)
+{
+ unsigned ch;uint8_t changed;
+ for(ch=0;chpulse[ch]&&--s->pulse[ch]==0)s->injected_tx&=(uint8_t)~(1u<tx=s->input_source?s->injected_tx:pins;
+ for(ch=0;chchannel[ch].flags&1))s->tx&=(uint8_t)~(1u<tx^s->previous_tx);s->previous_tx=s->tx;
+ if(!s->enabled){output(s);return;}
+ for(ch=0;chchannel[ch];uint8_t bit=(uint8_t)(1u<tx&bit);
+ if(s->active[ch]&&(!(s->fault&bit)||c->fault_width)&&--s->active[ch]==0)
+ s->logical_rx&=(uint8_t)~bit;
+ if(s->pending[ch]&&--s->pending[ch]==0) {
+ s->logical_rx|=bit;s->active[ch]=s->response_width[ch];
+ }
+ if(changed&bit) {
+ ++s->event_count;++c->count;
+ if(!(c->flags&1)||!(c->flags&2)||c->mode||!(c->edges&(on?1u:2u)))continue;
+ s->pending[ch]=on?c->on_delay:c->off_delay;
+ s->response_width[ch]=(s->fault&bit)&&c->fault_width?c->fault_width:(on?c->on_width:c->off_width);
+ if(!s->pending[ch]){s->logical_rx|=bit;s->active[ch]=s->response_width[ch];}
+ }
+ }
+ output(s);
+}
+unsigned set_igbt_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out)
+{
+ set_igbt_emu *s=(set_igbt_emu *)ctx;
+ uint16_t w[SET_IGBT_WORDS]={0};unsigned i,ch;
+ if(!s||!out||!count||(uint32_t)off+count>SET_IGBT_WORDS)return 2;
+ w[0]=0x4947;w[1]=3;w[2]=s->enabled;w[3]=s->tx;w[4]=s->rx;
+ w[6]=s->fault;w[7]=s->delay_ms;w[8]=s->width_ms;w[9]=s->event_count;
+ w[10]=8;w[11]=1;w[12]=s->input_source;w[13]=s->injected_tx;
+ w[15]=SET_IGBT_CHANNEL_WORDS;
+ for(ch=0;chchannel[ch];unsigned b=16+ch*SET_IGBT_CHANNEL_WORDS;
+ w[b]=c->flags;w[b+1]=c->mode;w[b+2]=c->on_delay;w[b+3]=c->on_width;
+ w[b+4]=c->off_delay;w[b+5]=c->off_width;w[b+6]=c->tx_width;
+ w[b+7]=c->edges;w[b+8]=c->fault_width;w[b+9]=c->count;
+ w[b+10]=s->pending[ch];w[b+11]=s->active[ch];
+ }
+ for(i=0;i=16) {
+ unsigned field=(off-16)%SET_IGBT_CHANNEL_WORDS;
+ set_igbt_channel *c;
+ ch=(off-16)/SET_IGBT_CHANNEL_WORDS;if(ch>=8)return 2;c=&s->channel[ch];
+ switch(field) {
+ case 0:if(value>7)return 3;c->flags=value;break;
+ case 1:if(value>3)return 3;c->mode=value;break;
+ case 2:if(value>1000)return 3;c->on_delay=value;break;
+ case 3:if(!value||value>1000)return 3;c->on_width=value;break;
+ case 4:if(value>1000)return 3;c->off_delay=value;break;
+ case 5:if(!value||value>1000)return 3;c->off_width=value;break;
+ case 6:if(!value||value>60000)return 3;c->tx_width=value;break;
+ case 7:if(value>3)return 3;c->edges=value;break;
+ case 8:if(value>60000)return 3;c->fault_width=value;break;
+ default:return 2;
+ }
+ /* Mode/enable changes discard pending responses. */
+ if(field<=1) {s->pending[ch]=s->active[ch]=0;s->logical_rx&=(uint8_t)~(1u<1)return 3;
+ s->enabled=(uint8_t)value;
+ if(!value) {s->logical_rx=0;
+ for(ch=0;ch<8;++ch)if(s->pulse[ch])s->injected_tx&=(uint8_t)~(1u<pending,0,sizeof(s->pending));
+ memset(s->active,0,sizeof(s->active));memset(s->pulse,0,sizeof(s->pulse));}
+ break;
+ case 3:case 13:
+ if(value>255)return 3;
+ s->injected_tx=(uint8_t)value;memset(s->pulse,0,sizeof(s->pulse));break;
+ case 6:
+ if(value>255)return 3;
+ released=(uint8_t)(s->fault&~value);s->fault=(uint8_t)value;
+ s->logical_rx&=(uint8_t)~released;
+ for(ch=0;ch<8;++ch)if(released&(1u<active[ch]=0;
+ break;
+ case 7:
+ if(value>1000)return 3;
+ s->delay_ms=value;
+ for(ch=0;ch<8;++ch)s->channel[ch].on_delay=s->channel[ch].off_delay=value;
+ break;
+ case 8:
+ if(!value||value>255)return 3;
+ s->width_ms=value;
+ for(ch=0;ch<8;++ch)s->channel[ch].on_width=s->channel[ch].off_width=value;
+ break;
+ case 9:
+ if(value)return 3;
+ s->event_count=0;for(ch=0;ch<8;++ch)s->channel[ch].count=0;break;
+ case 12:
+ if(value>1)return 3;
+ s->input_source=(uint8_t)value;memset(s->pulse,0,sizeof(s->pulse));break;
+ case 14:
+ if(value>255)return 3;
+ if(!s->enabled||!s->input_source)return 6;
+ /* A pulse must start from low; retrigger without an edge is refused. */
+ if(value&(s->injected_tx|s->tx))return 6;
+ for(ch=0;ch<8;++ch)if((value&(1u<channel[ch].flags&1))return 6;
+ s->injected_tx|=(uint8_t)value;
+ for(ch=0;ch<8;++ch)if(value&(1u<pulse[ch]=(uint16_t)(s->channel[ch].tx_width+1u);
+ break;
+ default:return 2;
+ }
+ output(s);return 0;
+}
diff --git a/c/set-protocol/src/set_emu_server.c b/c/set-protocol/src/set_emu_server.c
new file mode 100644
index 0000000..4e5b265
--- /dev/null
+++ b/c/set-protocol/src/set_emu_server.c
@@ -0,0 +1,194 @@
+#include "set_emu_server.h"
+#include "set_crc.h"
+#include
+static const uint32_t baudrates[5]={9600,19200,38400,57600,115200};
+uint32_t set_emu_server_baud(const set_emu_server *s) { return baudrates[s->config[2]]; }
+static unsigned control_read(void *ctx,uint16_t off,uint16_t n,uint16_t *out)
+{
+ set_emu_server *s=(set_emu_server *)ctx;uint16_t w[36]={0};unsigned i;
+ if(!((off==0&&n==4)||(off==16&&(n==8||n==10))||(off==32&&n==4)))return 2;
+ w[0]=0x454d;w[1]=1;w[2]=s->ump.enabled&1u;w[3]=s->ump.prev_command;
+ w[16]=0x5343;w[17]=3;
+ for(i=0;i<3;++i){w[18+i]=s->config[i];w[21+i]=s->staged[i];}
+ w[24]=s->config[3];w[25]=s->staged[3];
+ w[32]=0x4642;w[33]=1;w[34]=3;w[35]=SET_UMP_WORDS;
+ for(i=0;iconfig[3]!=0)return 6;
+ if(v>1)return 3;
+ s->ump.enabled=(uint8_t)((s->ump.enabled&~1u)|v);
+ if(!v)s->ump.scenario=0;
+ return 0;
+ }
+ if(off<21||off>25)return 2;
+ if(s->apply_delay)return 6;
+ if(off==24) {
+ if(v==0)memcpy(s->staged,s->config,sizeof(s->config));
+ else if(v==0xa55a) {
+ if(s->staged[0]==s->staged[1])return 3;
+ s->apply_delay=500;
+ } else return 3;
+ } else if(off==25) {
+ if(v>3 || (v==3&&!s->wave))return 3;
+ s->staged[3]=v;
+ } else {
+ if((off==21&&(!v||v>247))||(off==22&&(!v||v>255))||(off==23&&v>4))return 3;
+ s->staged[off-21]=v;
+ }
+ return 0;
+}
+static unsigned ump_read(void *ctx,uint16_t off,uint16_t n,uint16_t *out)
+{
+ set_emu_server *s=(set_emu_server *)ctx;
+ return s->config[3]?6:set_ump_read(&s->ump,off,n,out);
+}
+static unsigned ump_write(void *ctx,uint16_t off,uint16_t v)
+{
+ set_emu_server *s=(set_emu_server *)ctx;
+ return s->config[3]?6:set_ump_write(&s->ump,off,v);
+}
+static unsigned indexed_read(void *ctx,uint16_t off,uint16_t n,uint16_t *out)
+{
+ set_emu_server *s=(set_emu_server *)ctx;
+ return s->config[3]?6:set_ump_indexed_read(&s->ump,off,n,out);
+}
+static unsigned tms_read(void *ctx,uint16_t off,uint16_t n,uint16_t *out)
+{
+ set_emu_server *s=(set_emu_server *)ctx;
+ return set_tms_read(&s->tms,off,n,out);
+}
+static unsigned tms_write(void *ctx,uint16_t off,uint16_t v)
+{
+ set_emu_server *s=(set_emu_server *)ctx;
+ return s->config[3]!=1?6:set_tms_write(&s->tms,off,v);
+}
+/* Status remains readable in every mode; only the selected service may write.
+ * Explicit stop commands remain available regardless of mode. */
+static unsigned igbt_write(void *ctx,uint16_t off,uint16_t v)
+{
+ set_emu_server *s=(set_emu_server *)ctx;
+ if(s->config[3]!=2 && !(off==2&&v==0))return 6;
+ return set_igbt_write(&s->igbt,off,v);
+}
+static unsigned igbt_read(void *ctx,uint16_t off,uint16_t n,uint16_t *out)
+{
+ return set_igbt_read(&((set_emu_server *)ctx)->igbt,off,n,out);
+}
+static unsigned wave_write(void *ctx,uint16_t off,uint16_t v)
+{
+ set_emu_server *s=(set_emu_server *)ctx;
+ if(s->config[3]!=3 && !(off==2&&v==0))return 6;
+ return set_wave_write(s->wave,off,v);
+}
+static unsigned wave_read(void *ctx,uint16_t off,uint16_t n,uint16_t *out)
+{
+ return set_wave_read(((set_emu_server *)ctx)->wave,off,n,out);
+}
+static unsigned samples_write(void *ctx,uint16_t off,uint16_t v)
+{
+ set_emu_server *s=(set_emu_server *)ctx;
+ return s->config[3]!=3?6:set_wave_samples_write(s->wave,off,v);
+}
+static unsigned samples_read(void *ctx,uint16_t off,uint16_t n,uint16_t *out)
+{
+ return set_wave_samples_read(((set_emu_server *)ctx)->wave,off,n,out);
+}
+static void region(set_reg_region *r,uint16_t base,uint16_t words,uint16_t kind,
+ uint16_t version,void *ctx,set_reg_read_fn read,set_reg_write_fn write)
+{
+ r->base=base;r->words=words;r->kind=kind;r->version=version;r->instance=0;
+ r->flags=(uint16_t)(SET_REG_READ|(write?SET_REG_WRITE:0));
+ r->context=ctx;r->read=read;r->write=write;
+}
+unsigned set_emu_server_init(set_emu_server *s,uint16_t *b0,uint16_t *b1,
+ uint16_t capacity,set_wave_state *wave,unsigned address,unsigned tms,
+ unsigned baud,unsigned can,unsigned enabled)
+{
+ size_t n=0;
+ if(!s||!b0||!b1||b0==b1||capacity<2||capacity>819||
+ !address||address>247||!tms||tms>255||address==tms||baud>4||
+ !can||can>16||enabled>1)return 3;
+ if(wave&&(!wave->samples||wave->capacity<2||wave->capacity>SET_WAVE_MAX))return 3;
+ memset(s,0,sizeof(*s));s->wave=wave;
+ if(set_ump_init(&s->ump,b0,b1,capacity,(uint16_t)can,(uint8_t)enabled))return 3;
+ set_tms_init(&s->tms);set_igbt_init(&s->igbt);s->igbt.enabled=0;
+ s->config[0]=(uint16_t)address;s->config[1]=(uint16_t)tms;s->config[2]=(uint16_t)baud;
+ memcpy(s->staged,s->config,sizeof(s->config));
+ region(&s->regions[n++],0x1000,0x17c,SET_EMU_KIND_UMP,2,s,ump_read,ump_write);
+ region(&s->regions[n++],SET_EMU_CONTROL_BASE,36,SET_EMU_KIND_CONTROL,3,s,control_read,control_write);
+ region(&s->regions[n++],SET_EMU_IGBT_BASE,SET_IGBT_WORDS,SET_EMU_KIND_IGBT,3,s,igbt_read,igbt_write);
+ region(&s->regions[n++],SET_EMU_TMS_BASE,SET_TMS_WORDS,SET_EMU_KIND_TMS,1,s,tms_read,tms_write);
+ region(&s->regions[n++],SET_EMU_INDEXED_BASE,(uint16_t)(capacity*SET_UMP_WORDS),
+ SET_EMU_KIND_UMP_DATA,1,s,indexed_read,0);
+ if(wave) {
+ region(&s->regions[n++],SET_WAVE_BASE,9,SET_EMU_KIND_WAVE,3,s,wave_read,wave_write);
+ region(&s->regions[n++],SET_WAVE_DATA,(uint16_t)wave->capacity,
+ SET_EMU_KIND_WAVE_DATA,1,s,samples_read,samples_write);
+ }
+ return set_regmap_init(&s->map,s->regions,n);
+}
+void set_emu_server_tick(set_emu_server *s,uint8_t pins)
+{
+ if(s->apply_delay&&--s->apply_delay==0) {
+ if(s->config[3]!=s->staged[3]) {
+ /* Stop every executor before selecting the new one. No implicit start. */
+ if(s->wave) {
+ if(s->wave->port.stop)s->wave->port.stop(s->wave->port.context);
+ s->wave->running=0;s->wave->ready=0;
+ }
+ set_igbt_init(&s->igbt);s->igbt.enabled=0;
+ set_tms_init(&s->tms);s->ump.state=3;s->ump.post=0;s->ump.scenario=0;
+ s->ump.prev_command=0;s->ump.prev_output=0;
+ }
+ memcpy(s->config,s->staged,sizeof(s->config));
+ }
+ if(s->config[3]==1)set_tms_tick(&s->tms);
+ else if(s->config[3]==0)set_ump_tick(&s->ump);
+ else if(s->config[3]==2)set_igbt_tick(&s->igbt,pins);
+}
+static int inactive_ump(const set_emu_server *s,uint16_t a)
+{
+ return s->config[3]&&((a>=0x1000&&a<0x1200)||
+ (a>=SET_EMU_INDEXED_BASE&&a<(uint32_t)SET_EMU_INDEXED_BASE+s->ump.capacity*SET_UMP_WORDS));
+}
+size_t set_emu_server_rtu(set_emu_server *s,const uint8_t *r,size_t n,uint8_t *out,size_t cap)
+{
+ if(!s||!r||!n)return 0;
+ if(r[0]==s->config[1])
+ return s->config[3]==1?set_tms_rtu(&s->tms,s->config[1],r,n,out,cap):0;
+ if(n==8&&inactive_ump(s,(uint16_t)(((unsigned)r[2]<<8)|r[3])))return 0;
+ return set_regmap_rtu(&s->map,s->config[0],r,n,out,cap);
+}
+size_t set_emu_server_can(set_emu_server *s,const uint8_t *r,size_t n,
+ uint16_t *out,size_t cap,unsigned *token,unsigned *error)
+{
+ if(token)*token=0;
+ if(error)*error=0;
+ if(!s||!r||n!=8||inactive_ump(s,(uint16_t)(((unsigned)r[0]<<8)|r[1])))return 0;
+ return set_regmap_can(&s->map,r,n,out,cap,token,error);
+}
+size_t set_emu_stream_feed(set_emu_stream *st,set_emu_server *s,uint8_t byte,
+ uint32_t now,uint8_t *out,size_t cap)
+{
+ size_t length,n;
+ if(!st||!s||!out||cap<253)return 0;
+ if((uint32_t)(now-st->last_byte)>1000u||st->used>=sizeof(st->data))st->used=0;
+ st->last_byte=now;st->data[st->used++]=byte;
+ while(st->used>=8) {
+ length=st->data[0]==s->config[1]&&st->data[1]==65?18u:8u;
+ if(st->useddata,length)) {
+ --st->used;memmove(st->data,st->data+1,st->used);continue;
+ }
+ n=set_emu_server_rtu(s,st->data,length,out,cap);
+ st->used=(uint8_t)(st->used-length);
+ memmove(st->data,st->data+length,st->used);
+ if(n)return n;
+ }
+ return 0;
+}
diff --git a/c/set-protocol/src/set_emu_tms.c b/c/set-protocol/src/set_emu_tms.c
new file mode 100644
index 0000000..e6d9908
--- /dev/null
+++ b/c/set-protocol/src/set_emu_tms.c
@@ -0,0 +1,96 @@
+#include "set_emu_tms.h"
+#include "set_crc.h"
+#include
+static void analog(uint8_t *reply, unsigned index, int32_t value)
+{
+ uint16_t word = (uint16_t)value;
+ reply[30 + index*2] = (uint8_t)word;
+ reply[31 + index*2] = (uint8_t)(word >> 8);
+}
+
+
+void set_tms_init(set_tms_emu *s) { memset(s,0,sizeof(*s)); }
+void set_tms_tick(set_tms_emu *s)
+{
+ int32_t wanted = s->running ? s->target : 0;
+ /* 10 Hz/s, signed hundredths of Hz. Independent of UMP recorder. */
+ if (++s->divider < 10) return;
+ s->divider = 0;
+ if (s->frequency < wanted) s->frequency += wanted-s->frequency < 10 ? wanted-s->frequency : 10;
+ if (s->frequency > wanted) s->frequency -= s->frequency-wanted < 10 ? s->frequency-wanted : 10;
+}
+
+
+static void apply(set_tms_emu *s)
+{
+ unsigned i;uint16_t word=s->commands[0];
+ for(i=0;i<6;++i)s->controls[i]=(uint8_t)s->commands[4+i];
+ s->running=(uint8_t)((s->controls[0]&0x41)==0x41);
+ if((s->controls[0]&0x40) && (s->controls[2]&4))
+ s->target=word<0x8000?(int32_t)word:(int32_t)word-65536;
+}
+static void snapshot(set_tms_emu *s,uint8_t *reply)
+{
+ memset(reply, 0, 101);
+ reply[0] = 0; reply[1] = 65;
+ /* BALZAM_7: byte10.Go; ready/Go LED; remote selector. */
+ reply[11] = s->running;
+ reply[14] = (uint8_t)(0x0b | (s->running << 5));
+ reply[15] = (uint8_t)(6 | ((s->controls[0] & 0x40) ? 0x20 : 0));
+ reply[16] = (uint8_t)(3 | ((s->controls[4] & 0xc0) >> 3));
+ analog(reply, 0, s->frequency); analog(reply, 1, 2000);
+ analog(reply, 4, s->frequency); analog(reply, 5, s->frequency);
+ analog(reply, 6, 540); analog(reply, 7, 540);
+ analog(reply, 8, s->frequency / 10); analog(reply, 9, s->frequency / 10);
+ analog(reply, 10, s->frequency); analog(reply, 11, s->frequency < 0 ? -1 : 1);
+ analog(reply, 12, 540); analog(reply, 13, 540);
+ analog(reply, 14, s->frequency); analog(reply, 15, s->frequency);
+ analog(reply, 25, 25); analog(reply, 26, 30);
+ analog(reply, 28, 400); analog(reply, 29, 400);
+ analog(reply, 30, (s->controls[4] & 0x40) ? 120 : 40);
+ analog(reply, 31, (s->controls[4] & 0x40) ? 120 : 40);
+ analog(reply, 32, (s->controls[4] & 0x80) ? 120 : 40);
+ analog(reply, 33, (s->controls[4] & 0x80) ? 120 : 40);
+}
+unsigned set_tms_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out)
+{
+ set_tms_emu *s=(set_tms_emu *)ctx;uint8_t r[101];unsigned i,a;
+ if(!s || !out || !count || (uint32_t)off+count>SET_TMS_WORDS)return 2;
+ snapshot(s,r);
+ for(i=0;irunning;
+ else if(a==3)v=(uint16_t)s->frequency;
+ else if(a==4)v=(uint16_t)s->target;
+ else if(a>=8 && a<18)v=s->commands[a-8];
+ else if(a>=32 && a<46)v=(uint16_t)(r[2+(a-32)*2]|((unsigned)r[3+(a-32)*2]<<8));
+ else if(a>=46)v=(uint16_t)(r[30+(a-46)*2]|((unsigned)r[31+(a-46)*2]<<8));
+ out[i]=v;
+ }
+ return 0;
+}
+unsigned set_tms_write(void *ctx,uint16_t off,uint16_t v)
+{
+ set_tms_emu *s=(set_tms_emu *)ctx;
+ if(!s)return 3;
+ if(off>=8 && off<18) {
+ if(off>=12 && v>255)return 3;
+ s->commands[off-8]=v;return 0;
+ }
+ if(off==18) { if(v!=1)return 3;apply(s);return 0; }
+ return 2;
+}
+size_t set_tms_rtu(set_tms_emu *s,unsigned device,const uint8_t *r,size_t n,
+ uint8_t *out,size_t cap)
+{
+ unsigned i;uint16_t crc;
+ if(!s || !r || !out || n!=18 || cap<101 || !device || device>255 ||
+ r[0]!=device || r[1]!=65 || set_crc16_modbus(r,n))return 0;
+ for(i=0;i<4;++i)s->commands[i]=(uint16_t)(r[2+2*i]|((unsigned)r[3+2*i]<<8));
+ for(i=0;i<6;++i)s->commands[4+i]=r[10+i];
+ apply(s);snapshot(s,out);out[0]=(uint8_t)device;
+ crc=set_crc16_modbus(out,98);out[98]=(uint8_t)crc;out[99]=(uint8_t)(crc>>8);
+ return 101;
+}
diff --git a/c/set-protocol/src/set_emu_ump.c b/c/set-protocol/src/set_emu_ump.c
new file mode 100644
index 0000000..8e67f76
--- /dev/null
+++ b/c/set-protocol/src/set_emu_ump.c
@@ -0,0 +1,172 @@
+/* UMP synthetic signals and recorder. Storage belongs to the caller. */
+#include "set_emu_ump.h"
+#include
+static int triangle(uint32_t t, unsigned cycle)
+{
+ int x = (int)(t % cycle) * 480 / (int)cycle;
+ return x < 240 ? x - 120 : 360 - x;
+}
+
+static void signals(set_ump_emu *s, uint16_t *p)
+{
+ unsigned enabled = (s->enabled & 1u) != 0;
+ unsigned t = enabled ? s->scenario % 6000u : 0, ch;
+ unsigned run = t >= 1000 && t < 5000, reset = t >= 5000 && t < 5300;
+ unsigned ramp = t < 1000 ? 0 : (t < 3500 ? t - 1000 : 2500);
+ unsigned current = run ? 60 + ramp * 100 / 2500 :
+ (t >= 5000 && t < 5300 ? 40 + (5300-t)*120/300 : 40);
+ memset(p, 0, SET_UMP_WORDS * sizeof(*p));
+ p[0] = (uint16_t)s->elapsed; p[1] = (uint16_t)(s->elapsed >> 16);
+ p[3] = (uint16_t)((run << 7) | (reset << 6));
+ p[4] = (uint16_t)((t >= 1200 && t < 5100) | (run << 1) | (reset << 2));
+ p[5] = (uint16_t)((t >= 1000 && t < 2000) | ((t >= 5000) << 1) | (run << 2));
+ p[6] = (uint16_t)((t >= 1000 && t < 2000) ? 2000-t : (t >= 5000 ? 6000-t : 0));
+ p[7] = (uint16_t)(enabled && t < 250 ? 250-t : 0);
+ p[8] = (uint16_t)(500 + (current-40)*3000/160); p[9] = (uint16_t)current;
+ for (ch = 0; ch < 4; ++ch) {
+ int v = enabled ? triangle(s->scenario + ch * 211u, 720 + ch * 280u) : 0;
+ p[10+ch] = (uint16_t)v;
+ p[14+ch] = (uint16_t)((t >= 5100 && t < 5400) ? 1u << ch : 0);
+ p[36+ch] = (uint16_t)(2048 + v * 4);
+ }
+ /* These fields are synthetic CAN diagnostics, not transport counters. */
+ p[18] = 0xE07F; p[19] = p[3]; p[20] = 6; p[21] = 16;
+ p[22] = enabled ? (uint16_t)(s->scenario/100) : 0;
+ p[24] = 6; p[25] = 16; p[26] = 500; p[27] = 3500;
+ p[28] = 2000; p[29] = 500; p[30] = 3500;
+ p[31] = (uint16_t)(run ? ramp : 0); p[32] = 2500;
+ p[33] = 60; p[34] = 160; p[35] = (uint16_t)(ramp*1000/2500);
+}
+
+static void capture(set_ump_emu *s, uint16_t reason, uint16_t *p)
+{
+ if (reason != 5 && !s->post && (((p[3]&0x40) && !(s->prev_command&0x40)) ||
+ (!(p[3]&0x80) && (s->prev_command&0x80)))) {
+ if (s->used >= s->capacity/2) s->used = s->capacity/2-1;
+ s->post = 1000; s->dropped = 0; reason |= 0x100;
+ }
+ s->prev_command = p[3]; s->prev_input = p[4]; s->prev_output = p[5];
+ if (s->post && s->used >= s->capacity-1) { s->dropped = 1; return; }
+ p[2] = (uint16_t)(reason | ((reason == 5 && s->dropped) ? 0x200 : 0));
+ memcpy(s->banks[s->bank][s->next], p, SET_UMP_WORDS*sizeof(*p));
+ s->next = (uint16_t)((s->next+1)%s->capacity);
+ if (s->used < s->capacity) ++s->used;
+ ++s->sequence;
+}
+
+
+unsigned set_ump_init(set_ump_emu *s,uint16_t *bank0,uint16_t *bank1,
+ uint16_t capacity,uint16_t can_mode,uint8_t enabled)
+{
+ if(!s || !bank0 || !bank1 || bank0==bank1 || capacity<2 ||
+ capacity>1638 || !can_mode || can_mode>16)return 3;
+ memset(s,0,sizeof(*s));
+ s->banks[0]=(uint16_t (*)[SET_UMP_WORDS])bank0;
+ s->banks[1]=(uint16_t (*)[SET_UMP_WORDS])bank1;
+ s->capacity=capacity;s->can_mode=can_mode;s->enabled=enabled;
+ s->archive_bank=1;s->period=20;return 0;
+}
+void set_ump_tick(set_ump_emu *s)
+{
+ uint16_t p[SET_UMP_WORDS],old_post;
+ if (!(s->enabled & 1u)) s->scenario = 0;
+ if (s->state != 1) return;
+ ++s->elapsed;
+ if (s->enabled & 1u) ++s->scenario;
+ old_post = s->post;
+ signals(s,p);
+ if (++s->divider >= s->period) { s->divider = 0; capture(s,1,p); }
+ if (p[3] != s->prev_command || p[4] != s->prev_input || p[5] != s->prev_output) capture(s,3,p);
+ /* Count a full 1000 ms AFTER the trigger; never extend on new edges. */
+ if (old_post && --s->post == 0) {
+ capture(s,5,p);
+ s->archive_bank = s->bank; s->archive_count = s->used; s->archive_next = s->next;
+ ++s->generation; s->bank ^= 1; s->used = s->next = 0;
+ }
+}
+
+static uint16_t record_word(set_ump_emu *s, unsigned archived, unsigned record, unsigned word)
+{
+ unsigned n = archived ? s->archive_count : s->used;
+ unsigned end = archived ? s->archive_next : s->next;
+ if (record >= n) return 0;
+ return s->banks[archived ? s->archive_bank : s->bank][(end+s->capacity-n+record)%s->capacity][word];
+}
+
+
+static uint16_t read_word(set_ump_emu *s,uint16_t address)
+{
+ unsigned offset,count=s->used<3?s->used:3;
+ uint32_t first=s->sequence-count;
+ switch(address) {
+ case 0x1000: return 0x554D; case 0x1001: return 2;
+ case 0x1002: return s->source ? 3 : s->state;
+ case 0x1003: return s->source ? s->archive_count : s->used;
+ case 0x1004: return SET_UMP_WORDS; case 0x1005: return s->capacity;
+ case 0x1006: return s->period; case 0x1007: return (uint16_t)s->elapsed;
+ case 0x1008: return (uint16_t)(s->elapsed>>16); case 0x1009: return 6;
+ case 0x100A: return s->can_mode; case 0x100B: return 1000;
+ case 0x100C: return s->generation; case 0x100D: return s->archive_count;
+ case 0x100E: return s->post; case 0x100F: return s->state;
+ }
+ if (address >= 0x1010u && address < 0x1010u+120) {
+ if (!s->source && s->state == 1) return 0;
+ offset = address-0x1010u;
+ return record_word(s,s->source, s->cursor+offset/SET_UMP_WORDS, offset%SET_UMP_WORDS);
+ }
+ offset = address-0x1100u;
+ if (offset == 0) return (uint16_t)first;
+ if (offset == 1) return (uint16_t)(first>>16);
+ if (offset == 2) return (uint16_t)count;
+ if (offset == 3) return s->generation;
+ offset -= 4;
+ return offset/SET_UMP_WORDS < count ? record_word(s,0,s->used-count+offset/SET_UMP_WORDS,offset%SET_UMP_WORDS) : 0;
+}
+
+unsigned set_ump_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out)
+{
+ set_ump_emu *s=(set_ump_emu *)ctx;unsigned i;
+ uint32_t end=(uint32_t)off+count;
+ if(!s || !out || !count || count>124)return 3;
+ if(!((off<16 && end<=16) || (off>=0x10 && end<=0x88) ||
+ (off==0x100 && count==124)))return 2;
+ for(i=0;i120 || off%SET_UMP_WORDS || count%SET_UMP_WORDS)return 3;
+ records=s->source?s->archive_count:s->used;
+ if(off/SET_UMP_WORDS+count/SET_UMP_WORDS>records)return 2;
+ if(!s->source && s->state==1)return 6;
+ for(i=0;isource,(off+i)/SET_UMP_WORDS,(off+i)%SET_UMP_WORDS);
+ return 0;
+}
+unsigned set_ump_write(void *ctx,uint16_t off,uint16_t value)
+{
+ set_ump_emu *s=(set_ump_emu *)ctx;
+ uint16_t address=(uint16_t)(0x1000u+off),p[SET_UMP_WORDS];
+ if(!s || off>3)return 2;
+ if (address < 0x1000u || address > 0x1000u+3) return 2;
+ switch (address) {
+ case 0x1000u:
+ if (value == 0) { s->state = 3; s->post = 0; }
+ else if (value == 1 && s->state != 1) {
+ s->used = s->next = s->cursor = s->divider = s->post = s->source = s->dropped = 0;
+ s->scenario = 0; s->state = 1;
+ signals(s,p); s->prev_command = p[3]; capture(s,1,p);
+ } else return 3;
+ break;
+ case 0x1000u+1:
+ if (s->state == 1 || value < 1 || value > 1000) return 3;
+ s->period = value; break;
+ case 0x1000u+2:
+ if (value >= s->capacity) return 3;
+ s->cursor = value; break;
+ case 0x1000u+3:
+ if (value > 1) return 3;
+ s->source = value; break;
+ }
+ return 0;
+}
diff --git a/c/set-protocol/src/set_regmap.c b/c/set-protocol/src/set_regmap.c
new file mode 100644
index 0000000..89dbc40
--- /dev/null
+++ b/c/set-protocol/src/set_regmap.c
@@ -0,0 +1,117 @@
+#include "set_regmap.h"
+#include "set_crc.h"
+#include
+static uint16_t get16(const uint8_t *p) { return (uint16_t)(((unsigned)p[0]<<8)|p[1]); }
+static void put16(uint8_t *p, unsigned v) { p[0]=(uint8_t)(v>>8); p[1]=(uint8_t)v; }
+unsigned set_regmap_init(set_regmap *m, const set_reg_region *r, size_t count)
+{
+ size_t i,j;
+ if (!m) return SET_REG_VALUE;
+ m->regions=0; m->count=0;
+ if ((!r && count) || count>64) return SET_REG_VALUE;
+ for (i=0;i65536u || r[i].base<8u+count*8u ||
+ !r[i].flags || (r[i].flags & ~3u) ||
+ ((r[i].flags&SET_REG_READ) && !r[i].read) ||
+ ((r[i].flags&SET_REG_WRITE) && !r[i].write)) return SET_REG_VALUE;
+ for (j=0;jregions=r; m->count=count; return 0;
+}
+static uint16_t directory(const set_regmap *m, unsigned a)
+{
+ const set_reg_region *r;
+ if (a==0) return 0x5345; /* SE: service directory */
+ if (a==1) return 1;
+ if (a==2) return (uint16_t)m->count;
+ if (a==3) return 8;
+ if (a==4) return 8;
+ if (a==5) return SET_REG_MAX_WORDS;
+ if (a<8) return 0;
+ r=&m->regions[(a-8)/8];
+ switch ((a-8)%8) {
+ case 0:return r->kind; case 1:return r->version; case 2:return r->instance;
+ case 3:return r->base; case 4:return r->words; case 5:return r->flags;
+ default:return 0;
+ }
+}
+unsigned set_regmap_request(const set_regmap *m, unsigned fn, uint16_t a,
+ uint16_t v, uint16_t *out, size_t cap, size_t *count)
+{
+ size_t i, n; unsigned error;
+ uint32_t end;
+ if (!count) return SET_REG_VALUE;
+ *count=0;
+ if (!m || (!m->regions && m->count) || !out) return SET_REG_VALUE;
+ if (fn!=3 && fn!=6) return SET_REG_FUNCTION;
+ n=fn==3?v:2;
+ if (!n || n>SET_REG_MAX_WORDS || cap65536u) return SET_REG_ADDRESS;
+ if (a<8u+8u*m->count) {
+ if (fn!=3 || end>8u+8u*m->count) return SET_REG_ADDRESS;
+ for(i=0;icount;++i) {
+ const set_reg_region *r=&m->regions[i];
+ if (abase || a>=(uint32_t)r->base+r->words) continue;
+ if (end>(uint32_t)r->base+r->words) return SET_REG_ADDRESS;
+ if (fn==3) {
+ if (!(r->flags&SET_REG_READ)) return SET_REG_ADDRESS;
+ error=r->read(r->context,(uint16_t)(a-r->base),v,out);
+ } else {
+ if (!(r->flags&SET_REG_WRITE)) return SET_REG_ADDRESS;
+ error=r->write(r->context,(uint16_t)(a-r->base),v);
+ if (!error) { out[0]=a; out[1]=v; }
+ }
+ if (!error) *count=n;
+ return error;
+ }
+ return SET_REG_ADDRESS;
+}
+size_t set_regmap_rtu(const set_regmap *m, unsigned device, const uint8_t *req,
+ size_t length, uint8_t *out, size_t cap)
+{
+ uint16_t words[SET_REG_MAX_WORDS],crc; size_t count=0,n,i;
+ unsigned error;
+ if (!req || !out || length!=8 || cap<253 || device<1 || device>247 ||
+ req[0]!=device || req[1]>127 || set_crc16_modbus(req,length)) return 0;
+ error=set_regmap_request(m,req[1],get16(req+2),get16(req+4),words,SET_REG_MAX_WORDS,&count);
+ out[0]=(uint8_t)device;out[1]=req[1];
+ if (error) { out[1]|=0x80;out[2]=(uint8_t)error;n=3; }
+ else if (req[1]==6) { put16(out+2,words[0]);put16(out+4,words[1]);n=6; }
+ else {
+ out[2]=(uint8_t)(2*count);
+ for(i=0;i>8);
+ return n;
+}
+size_t set_regmap_can(const set_regmap *m, const uint8_t *r, size_t length,
+ uint16_t *out, size_t cap, unsigned *token, unsigned *error)
+{
+ size_t n=0;
+ if (token) *token=0;
+ if (error) *error=0;
+ if (!r || !out || !token || !error || length!=8 || capSET_REG_MAX_WORDS || (n&1) ||
+ index>=n/2 || !token || token>255 || error>255) return 0;
+ put16(out,w[index*2]);put16(out+2,w[index*2+1]);
+ put16(out+4,0x1f00u+(unsigned)index);out[6]=(uint8_t)token;out[7]=(uint8_t)error;
+ return 8;
+}
diff --git a/c/set-protocol/src/set_wavegen.c b/c/set-protocol/src/set_wavegen.c
new file mode 100644
index 0000000..b713c47
--- /dev/null
+++ b/c/set-protocol/src/set_wavegen.c
@@ -0,0 +1,214 @@
+#include "set_wavegen.h"
+#include "set_crc.h"
+#include
+static uint16_t read16(const uint8_t *p) {return (uint16_t)((p[0]<<8)|p[1]);}
+static void write16(uint8_t *p,unsigned value) {p[0]=(uint8_t)(value>>8);p[1]=(uint8_t)value;}
+static size_t seal(uint8_t *p,size_t n) {
+ uint16_t crc=set_crc16_modbus(p,n);p[n]=(uint8_t)crc;p[n+1]=(uint8_t)(crc>>8);return n+2;
+}
+void set_wave_init(set_wave_state *s,uint16_t *samples,size_t capacity,const set_wave_port *port) {
+ memset(s,0,sizeof(*s));s->samples=samples;s->capacity=capacity;s->rate=1000;
+ if(port) s->port=*port;
+}
+static unsigned write_register(set_wave_state *s,unsigned reg,unsigned value) {
+ if(reg==SET_WAVE_BASE+2 && value==0) {
+ if(s->port.stop) s->port.stop(s->port.context);
+ s->running=0; return 0;
+ }
+ if(s->running) return 6;
+ if(reg==SET_WAVE_BASE+2 && value==1) {
+ if(!s->ready||!s->port.start) return 3;
+ if(s->port.start(s->port.context,s->samples,s->count,s->rate)) return 4;
+ s->running=1;return 0;
+ }
+ if(reg==SET_WAVE_BASE+3) {
+ uint32_t rate=((uint32_t)s->rate_high<<16)|value;
+ if(rate<1||rate>SET_WAVE_MAX_RATE) return 3;
+ s->rate=rate;s->ready=0;return 0;
+ }
+ if(reg==SET_WAVE_BASE+8) {
+ if(value>(SET_WAVE_MAX_RATE>>16)) return 3;
+ s->rate_high=(uint16_t)value;s->ready=0;return 0;
+ }
+ if(reg==SET_WAVE_BASE+4) {
+ if(value<2||value>SET_WAVE_MAX||value>s->capacity) return 3;
+ s->count=(uint16_t)value;s->received=s->ready=0;return 0;
+ }
+ if(reg>=SET_WAVE_DATA&®=s->count||index>s->received||value>4095||s->ready) return 3;
+ if(indexreceived) return s->samples[index]==value?0:3;
+ s->samples[index]=(uint16_t)value;++s->received;return 0;
+ }
+ if(reg==SET_WAVE_BASE+6) {
+ if(value!=0xA55A||s->count<2||s->received!=s->count) return 3;
+ s->ready=1;return 0;
+ }
+ return 2;
+}
+unsigned set_wave_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out) {
+ set_wave_state *s=(set_wave_state *)ctx;uint16_t w[9];unsigned i;
+ if(!s||!s->samples||!out||!count||(uint32_t)off+count>9)return 2;
+ w[0]=0x5747;w[1]=3;w[2]=s->running;w[3]=(uint16_t)s->rate;
+ w[4]=s->count;w[5]=s->received;w[6]=s->ready;w[7]=(uint16_t)s->capacity;w[8]=(uint16_t)(s->rate>>16);
+ for(i=0;isamples||off>=9)return 2;
+ return write_register(s,SET_WAVE_BASE+off,value);
+}
+unsigned set_wave_samples_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out) {
+ set_wave_state *s=(set_wave_state *)ctx;unsigned i;
+ if(!s||!s->samples||!out||!count||(uint32_t)off+count>s->received)return 2;
+ for(i=0;isamples[off+i];
+ return 0;
+}
+unsigned set_wave_samples_write(void *ctx,uint16_t off,uint16_t value) {
+ set_wave_state *s=(set_wave_state *)ctx;
+ if(!s||!s->samples||off>=SET_WAVE_MAX)return 2;
+ return write_register(s,SET_WAVE_DATA+off,value);
+}
+size_t set_wave_rtu(set_wave_state *s,unsigned device,const uint8_t *req,size_t n,uint8_t *out,size_t cap) {
+ unsigned reg,value,error=0,i;uint16_t status[8];size_t length=0;
+ if(!s||!s->samples||!req||!out||cap<21||n!=8||device<1||device>247
+ ||req[0]!=device||set_crc16_modbus(req,n)) return 0;
+ reg=read16(req+2);value=read16(req+4);
+ if(!((reg>=SET_WAVE_BASE&®=SET_WAVE_DATA&®rate>>16);length=5;
+ } else if(reg>=SET_WAVE_DATA&&value==1&®-SET_WAVE_DATAreceived) {
+ out[2]=2;write16(out+3,s->samples[reg-SET_WAVE_DATA]);length=5;
+ } else error=2;
+ } else error=1;
+ if(error) {out[1]|=0x80;out[2]=(uint8_t)error;length=3;}
+ return seal(out,length);
+}
+size_t set_wave_request(unsigned device,unsigned op,unsigned index,unsigned value,uint8_t *out,size_t cap) {
+ unsigned reg=SET_WAVE_BASE,fn=6;
+ if(!out||cap<8||device<1||device>247) return 0;
+ switch(op) {
+ case 0: fn=3;value=8;break;
+ case 1: reg+=2;value=0;break;
+ case 2: reg+=3;if(value>65535)return 0;break;
+ case 3: reg+=4;if(value<2||value>SET_WAVE_MAX)return 0;break;
+ case 4: if(index>=SET_WAVE_MAX||value>4095)return 0;reg=SET_WAVE_DATA+index;break;
+ case 5: reg+=6;value=0xA55A;break;
+ case 6: reg+=2;value=1;break;
+ case 7: if(index>=SET_WAVE_MAX)return 0;reg=SET_WAVE_DATA+index;fn=3;value=1;break;
+ case 8: reg+=8;fn=3;value=1;break;
+ case 9: reg+=8;if(value>(SET_WAVE_MAX_RATE>>16))return 0;break;
+ default:return 0;
+ }
+ out[0]=(uint8_t)device;out[1]=(uint8_t)fn;write16(out+2,reg);write16(out+4,value);return seal(out,6);
+}
+int set_wave_response(const uint8_t *req,const uint8_t *reply,size_t size,uint16_t *words,size_t cap) {
+ size_t expected,i;
+ if(!req||!reply||!words||set_crc16_modbus(req,8))return -1;
+ if(size<2)return 0;
+ if(reply[0]!=req[0])return -1;
+ if(reply[1]==(req[1]|0x80)) {
+ if(size<5)return 0;
+ return size==5&&!set_crc16_modbus(reply,5)?-(100+(int)reply[2]):-1;
+ }
+ if(reply[1]!=req[1])return -1;
+ expected=req[1]==6?8:5+2*(size_t)read16(req+4);
+ if(size3)))return -2;
+ return (int)((expected-5)/2);
+}
+
+
+size_t set_wave_block_request(unsigned device,unsigned index,const uint16_t *values,
+ size_t count,uint8_t *out,size_t cap) {
+ size_t i,n=values?9+2*count:8;
+ if(!out||device<1||device>247||!count||count>SET_WAVE_BLOCK_MAX||
+ index>=SET_WAVE_MAX||count>SET_WAVE_MAX-index||cap4095)return 0;
+ out[0]=(uint8_t)device;out[1]=values?16:3;
+ write16(out+2,SET_WAVE_DATA+index);write16(out+4,(unsigned)count);
+ if(values) {
+ out[6]=(uint8_t)(count*2);
+ for(i=0;isamples||!req||!out||cap<5||n<8||device<1||device>247||
+ req[0]!=device||set_crc16_modbus(req,n))return 0;
+ reg=read16(req+2);count=read16(req+4);
+ if(reg=SET_WAVE_DATA+SET_WAVE_MAX)return 0;
+ index=reg-SET_WAVE_DATA;out[0]=req[0];out[1]=req[1];
+ if(!count||count>SET_WAVE_BLOCK_MAX||count>SET_WAVE_MAX-index||
+ index+count>s->count||index+count>s->capacity)error=3;
+ else if(req[1]==16) {
+ if(n!=9+2*(size_t)count||req[6]!=2*count)error=3;
+ else if(s->running)error=6;
+ else if(s->ready||index>s->received)error=3;
+ else {
+ /* Preflight the whole block, including replayed prefix, before writing. */
+ for(i=0;i4095||(index+ireceived&&s->samples[index+i]!=value)) {
+ error=3;break;
+ }
+ }
+ if(!error) {
+ if(cap<8)return 0;
+ for(i=0;isamples[index+i]=read16(req+7+2*i);
+ if(index+count>s->received)s->received=(uint16_t)(index+count);
+ memcpy(out,req,6);length=6;
+ }
+ }
+ } else if(req[1]==3) {
+ if(n!=8||index+count>s->received)error=3;
+ else {
+ if(cap<5+2*(size_t)count)return 0;
+ out[2]=(uint8_t)(2*count);
+ for(i=0;isamples[index+i]);
+ length=3+2*(size_t)count;
+ }
+ } else error=1;
+ if(error) {out[1]|=0x80;out[2]=(uint8_t)error;length=3;}
+ return seal(out,length);
+}
+int set_wave_block_response(const uint8_t *req,size_t n,const uint8_t *reply,
+ size_t size,uint16_t *words,size_t cap) {
+ size_t expected,i;unsigned count;
+ if(!req||!reply||!words||n<8||set_crc16_modbus(req,n))return -1;
+ count=read16(req+4);
+ if(!count||count>SET_WAVE_BLOCK_MAX||read16(req+2)SET_WAVE_DATA+SET_WAVE_MAX)return -1;
+ if((req[1]==16&&(n!=9+2*(size_t)count||req[6]!=2*count))||
+ (req[1]==3&&n!=8)||(req[1]!=3&&req[1]!=16))return -1;
+ if(size<2)return 0;
+ if(reply[0]!=req[0])return -1;
+ if(reply[1]==(req[1]|0x80)) {
+ if(size<5)return 0;
+ return size==5&&!set_crc16_modbus(reply,size)?-(100+(int)reply[2]):-1;
+ }
+ if(reply[1]!=req[1])return -1;
+ expected=req[1]==16?8:5+2*(size_t)count;
+ if(size>8; p[1] = v&255U; }
+unsigned ump_validate(unsigned fn, uint16_t a, uint16_t n, int can)
+{
+ if (fn == 6) return a >= UMP_BASE && a <= UMP_BASE+3 ? 0 : 2;
+ if (fn != 3) return 1;
+ if (can) return ((a == UMP_BASE && n == 16) ||
+ (a == UMP_DATA && (n == 40 || n == 80 || n == 120)) ||
+ (a == UMP_LIVE && n == 124)) ? 0 : 2;
+ if (!n || n > 125) return 2;
+ return ((a >= UMP_BASE && a < UMP_BASE+16 && n <= UMP_BASE+16-a) ||
+ (a >= UMP_DATA && a < UMP_DATA+120 && n <= UMP_DATA+120-a) ||
+ (a == UMP_LIVE && n == 124)) ? 0 : 2;
+}
+unsigned ump_prepare(const ump_backend *b, unsigned fn, uint16_t a,
+ uint16_t n, int can, uint16_t *out, unsigned *error)
+{
+ unsigned i;
+ if (!out || !error) return 0;
+ *error = ump_validate(fn,a,n,can);
+ if (!*error) {
+ if (!b || (fn == 3 ? !b->read : !b->write)) *error = 3;
+ else if (fn == 3) {
+ for (i=0;iread(b->context,(uint16_t)(a+i));
+ return n;
+ } else if (!b->write(b->context,a,n)) *error = 3;
+ }
+ out[0]=a; out[1]=n;
+ return 2;
+}
+uint16_t ump_crc(const ump_octet *p, size_t n)
+{
+ uint16_t crc=0xFFFFU;
+ unsigned i;
+ while (n--) {
+ crc ^= *p++ & 255U;
+ for (i=0;i<8;i++) crc=(uint16_t)((crc>>1)^((crc&1U)?0xA001U:0));
+ }
+ return crc;
+}
+uint32_t ump_can_id(unsigned mode, unsigned kind)
+{ return mode>=1 && mode<=16 && kind<=2 ? 0xBA0000UL+kind*16UL+mode-1 : 0; }
+unsigned ump_can_request(const ump_backend *b, const ump_octet *r, size_t size,
+ uint16_t *out, unsigned *token, unsigned *error)
+{
+ size_t i;
+ if (!r || !out || !token || !error || size!=8) return 0;
+ for (i=0;i255) return 0;
+ if (get16(r+4)!=UMP_MARKER || !r[6]) return 0;
+ *token=r[6];
+ return ump_prepare(b,r[7],get16(r),get16(r+2),1,out,error);
+}
+unsigned ump_can_fragment(const uint16_t *words, unsigned count, unsigned index,
+ unsigned token, unsigned error, ump_octet *out)
+{
+ if (!words || !out || count<2 || count>124 || (count&1) ||
+ index>=count/2 || !token || token>255 || error>255) return 0;
+ put16(out,words[index*2]); put16(out+2,words[index*2+1]);
+ put16(out+4,(uint16_t)(UMP_MARKER+index)); out[6]=(ump_octet)token; out[7]=(ump_octet)error;
+ return 8;
+}
+size_t ump_rtu(const ump_backend *b, unsigned device, const ump_octet *r,
+ size_t size, ump_octet *out, size_t capacity)
+{
+ unsigned i;
+ if (!r || !out || size!=8 || capacity247 || r[0]!=device) return 0;
+ for (i=0;i<8;i++) if (r[i]>255) return 0;
+ if (ump_crc(r,8)) return 0;
+ return ump_rtu_response(b,device,r[1],get16(r+2),get16(r+4),out,capacity);
+}
+size_t ump_rtu_response(const ump_backend *b, unsigned device, unsigned fn,
+ uint16_t address, uint16_t value, ump_octet *out, size_t capacity)
+{
+ uint16_t words[UMP_MAX_WORDS];
+ unsigned count,error;
+ if (!out || capacity247 || fn>127) return 0;
+ count=ump_prepare(b,fn,address,value,0,words,&error);
+ return ump_rtu_encode(device,fn,error,words,count,out,capacity);
+}
+size_t ump_rtu_encode(unsigned device, unsigned fn, unsigned error,
+ const uint16_t *words, unsigned count, ump_octet *out, size_t capacity)
+{
+ unsigned i;
+ size_t n;
+ uint16_t crc;
+ if (!out || !device || device>247 || fn>127 || error>255) return 0;
+ if (!error && (!words || (fn!=3 && fn!=6) ||
+ (fn==3 && (!count || count>124)) || (fn==6 && count!=2))) return 0;
+ n=error ? 5 : (fn==6 ? 8 : 5+count*2);
+ if (capacity>8;
+ return n;
+}
diff --git a/c/set-protocol/tests/test_boot.c b/c/set-protocol/tests/test_boot.c
new file mode 100644
index 0000000..a1d672f
--- /dev/null
+++ b/c/set-protocol/tests/test_boot.c
@@ -0,0 +1,70 @@
+#include "set_boot.h"
+#include
+#include
+#include
+static uint8_t flash[1024];
+static bool valid, fail_write, fail_verify, fail_invalidate;
+static unsigned erases, activations;
+static bool invalidate(void *u) { (void)u; if(fail_invalidate) return false; valid=false; return true; }
+static bool erase(void *u,uint32_t n) { (void)u; assert(!valid); assert(n<=sizeof(flash)); ++erases; memset(flash,255,sizeof(flash)); return true; }
+static bool write(void *u,uint32_t off,const uint8_t *p,uint16_t n) { (void)u; if(fail_write) return false; memcpy(flash+off,p,n); return true; }
+static bool verify(void *u,uint32_t n,uint32_t crc) { (void)u; return !fail_verify && setp_crc32(flash,n)==crc; }
+static bool commit(void *u,uint32_t n,uint32_t crc) { (void)u; assert(verify(0,n,crc)); valid=true; return true; }
+static void activate(void *u) { (void)u; assert(valid); ++activations; }
+int main(void) {
+ setp_boot b;
+ const setp_boot_ops ops={invalidate,erase,write,verify,commit,activate};
+ uint8_t image[517],p[512]; unsigned i; size_t n;
+ setp_fw_begin_t begin={0}; setp_fw_end_t end={0};
+ for(i=0;i256) count=256;
+ n=setp_fw_data_encode(i,0,image+i,count,p,sizeof(p));
+ assert(!setp_boot_command(&b,SETP_MSG_FW_DATA,p,(uint16_t)n));
+ }
+ assert(!valid); n=setp_fw_end_encode(&end,p,sizeof(p));
+ flash[20]^=1;
+ assert(setp_boot_command(&b,SETP_MSG_FW_END,p,(uint16_t)n)==SETP_STATUS_VERIFY_FAILED);
+ assert(!valid);
+ /* A reset loses the session, never promotes a partially written image. */
+ setp_boot_init(&b,&ops,0,begin.base_address,sizeof(flash));
+ assert(setp_boot_command(&b,SETP_MSG_FW_END,p,(uint16_t)n)==SETP_STATUS_WRONG_STATE);
+ n=setp_fw_begin_encode(&begin,p,sizeof(p)); assert(!setp_boot_command(&b,SETP_MSG_FW_BEGIN,p,(uint16_t)n));
+ for(i=0;i256) count=256;
+ n=setp_fw_data_encode(i,0,image+i,count,p,sizeof(p));
+ assert(!setp_boot_command(&b,SETP_MSG_FW_DATA,p,(uint16_t)n));
+ }
+ n=setp_fw_end_encode(&end,p,sizeof(p)); assert(!setp_boot_command(&b,SETP_MSG_FW_END,p,(uint16_t)n));
+ assert(valid && !memcmp(image,flash,sizeof(image)));
+ assert(!setp_boot_command(&b,SETP_MSG_FW_ACTIVATE,0,0)); assert(activations==1);
+ n=setp_fw_begin_encode(&begin,p,sizeof(p)); assert(!setp_boot_command(&b,SETP_MSG_FW_BEGIN,p,(uint16_t)n));
+ fail_write=true; n=setp_fw_data_encode(0,0,image,256,p,sizeof(p));
+ assert(setp_boot_command(&b,SETP_MSG_FW_DATA,p,(uint16_t)n)==SETP_STATUS_INTERNAL);
+ assert(!valid && b.status.state==SETP_FW_FAILED);
+ puts("SET boot: validation, sequence, CRC, readback, interrupted update and Flash failure passed");
+ return 0;
+}
diff --git a/c/set-protocol/tests/test_emulator_services.c b/c/set-protocol/tests/test_emulator_services.c
new file mode 100644
index 0000000..1096096
--- /dev/null
+++ b/c/set-protocol/tests/test_emulator_services.c
@@ -0,0 +1,233 @@
+/* Portable contract tests. Assertions stay enabled even in Release builds. */
+#include "set_emu_server.h"
+#include "set_crc.h"
+#include
+#include
+#include
+static set_emu_server a,b;
+static uint16_t banks[4][32*SET_UMP_WORDS],samples[2][16];
+static set_wave_state wave[2];
+static unsigned starts[2],stops[2];
+static int start(void *ctx,const uint16_t *p,size_t n,uint32_t rate)
+{
+ unsigned *calls=(unsigned *)ctx;
+ assert(p&&n>=2&&rate);++*calls;return 0;
+}
+static void stop(void *ctx) { unsigned *calls=(unsigned *)ctx; ++stops[calls==&starts[1]]; }
+static void init(void)
+{
+ set_wave_port p;
+ memset(starts,0,sizeof(starts));memset(stops,0,sizeof(stops));
+ p.context=&starts[0];p.start=start;p.stop=stop;set_wave_init(&wave[0],samples[0],16,&p);
+ p.context=&starts[1];set_wave_init(&wave[1],samples[1],16,&p);
+ assert(!set_emu_server_init(&a,banks[0],banks[1],32,&wave[0],16,1,4,5,1));
+ assert(!set_emu_server_init(&b,banks[2],banks[3],32,&wave[1],23,2,0,6,0));
+}
+static void write(set_emu_server *s,uint16_t address,uint16_t value)
+{
+ uint16_t out[2];size_t n=99;
+ assert(!set_regmap_request(&s->map,6,address,value,out,2,&n));
+ assert(n==2&&out[0]==address&&out[1]==value);
+}
+static void select_mode(set_emu_server *s,unsigned mode)
+{
+ unsigned i;write(s,0x1219,(uint16_t)mode);write(s,0x1218,0xa55a);
+ for(i=0;i<500;++i)set_emu_server_tick(s,0);
+ assert(s->config[3]==mode);
+}
+static uint16_t read1(set_emu_server *s,uint16_t address)
+{
+ uint16_t out;size_t n=99;
+ assert(!set_regmap_request(&s->map,3,address,1,&out,1,&n));
+ assert(n==1);return out;
+}
+static void seal(uint8_t *p,size_t n)
+{
+ uint16_t crc=set_crc16_modbus(p,n);p[n]=(uint8_t)crc;p[n+1]=(uint8_t)(crc>>8);
+}
+static void rtu(uint8_t *p,uint8_t device,uint8_t fn,uint16_t address,uint16_t value)
+{
+ p[0]=device;p[1]=fn;p[2]=(uint8_t)(address>>8);p[3]=(uint8_t)address;
+ p[4]=(uint8_t)(value>>8);p[5]=(uint8_t)value;seal(p,6);
+}
+static void directory_and_relocation(void)
+{
+ uint16_t out[124];size_t n;set_igbt_emu other;
+ set_regmap map;set_reg_region r[2]={
+ {0x300,16,SET_EMU_KIND_IGBT,2,0,3,&a.igbt,set_igbt_read,set_igbt_write},
+ {0x400,16,SET_EMU_KIND_IGBT,2,1,3,&other,set_igbt_read,set_igbt_write}};
+ init();set_igbt_init(&other);
+ assert(!set_regmap_request(&a.map,3,0,64,out,124,&n));
+ assert(n==64&&out[0]==0x5345&&out[1]==1&&out[2]==7&&out[5]==124);
+ assert(out[8]==SET_EMU_KIND_UMP&&out[11]==0x1000);
+ assert(!set_regmap_init(&map,r,2));
+ assert(!set_regmap_request(&map,6,0x307,777,out,2,&n));
+ assert(a.igbt.delay_ms==777&&other.delay_ms==1);
+ assert(!set_regmap_request(&map,6,0x407,9,out,2,&n));
+ assert(other.delay_ms==9);
+ assert(set_regmap_request(&map,3,0x30f,2,out,124,&n)==2&&n==0);
+ assert(set_regmap_request(&map,3,0xffff,2,out,124,&n)==2);
+ assert(set_regmap_request(&map,6,0,1,out,124,&n)==2);
+ r[1].base=0x30f;assert(set_regmap_init(&map,r,2)==2&&map.count==0);
+ r[1].base=0xffff;assert(set_regmap_init(&map,r,2)==3);
+ r[1].base=8;assert(set_regmap_init(&map,r,2)==3);
+ r[1].base=0x400;r[1].write=0;assert(set_regmap_init(&map,r,2)==3);
+}
+static void capacity_and_frames(void)
+{
+ uint8_t req[8],reply[256],can[8]={0x14,7,0,9,0x1f,0,7,6};
+ uint16_t out[124];size_t n;unsigned token,error;
+ init();select_mode(&a,2);rtu(req,16,6,0x1407,123);
+ assert(set_regmap_request(&a.map,6,0x1407,123,out,1,&n)==3&&n==0);
+ assert(set_regmap_request(&a.map,3,0x1400,0,out,124,&n)==3);
+ assert(set_regmap_request(&a.map,3,0x1400,125,out,124,&n)==3);
+ assert(set_regmap_request(&a.map,4,0x1400,1,out,124,&n)==1);
+ assert(!set_emu_server_rtu(&a,req,8,reply,8));
+ assert(a.igbt.delay_ms==1);
+ assert(!set_emu_server_can(&a,can,8,out,2,&token,&error));
+ assert(a.igbt.delay_ms==1);
+ req[7]^=1;assert(!set_emu_server_rtu(&a,req,8,reply,256));req[7]^=1;
+ req[0]=17;seal(req,6);assert(!set_emu_server_rtu(&a,req,8,reply,256));
+ rtu(req,16,6,0x1407,123);
+ assert(set_emu_server_rtu(&a,req,8,reply,256)==8&&!memcmp(reply,req,8));
+ assert(read1(&a,0x1407)==123&&b.igbt.delay_ms==1);
+ rtu(req,16,6,0x1407,1001);
+ assert(set_emu_server_rtu(&a,req,8,reply,256)==5);
+ assert(reply[1]==0x86&&reply[2]==3&&!set_crc16_modbus(reply,5));
+ assert(a.igbt.delay_ms==123);
+ assert(set_emu_server_can(&a,can,8,out,124,&token,&error)==2);
+ assert(token==7&&!error&&a.igbt.delay_ms==9&&out[0]==0x1407&&out[1]==9);
+ can[7]=3;can[2]=0;can[3]=1;
+ assert(set_emu_server_can(&a,can,8,out,124,&token,&error)==2&&out[0]==9&&out[1]==0);
+ assert(set_regmap_can_fragment(out,2,0,token,error,reply,8)==8);
+ assert(!memcmp(reply,(const uint8_t[]){0,9,0,0,0x1f,0,7,0},8));
+ assert(!set_regmap_can_fragment(out,2,1,token,error,reply,8));
+ can[4]=0;assert(!set_emu_server_can(&a,can,8,out,124,&token,&error));
+ assert(token==0&&error==0);
+}
+static void independent_wave_and_igbt(void)
+{
+ uint8_t r[8],reply[256],can[8]={0xa0,0,0,42,0x1f,0,9,6};
+ uint16_t w[124];unsigned token,error;
+ init();
+ /* Identical addresses on different transports now select the same module. */
+ rtu(r,16,3,SET_WAVE_BASE,8);
+ assert(set_emu_server_rtu(&a,r,8,reply,256)==21);
+ assert(reply[3]==0x57&&reply[4]==0x47);
+ rtu(r,16,3,SET_EMU_IGBT_BASE,16);
+ assert(set_emu_server_rtu(&a,r,8,reply,256)==37);
+ assert(reply[3]==0x49&&reply[4]==0x47);
+ select_mode(&a,3);
+ write(&a,0x1304,2);
+ assert(set_emu_server_can(&a,can,8,w,124,&token,&error)==2&&!error);
+ write(&a,0xa001,4095);write(&a,0x1306,0xa55a);
+ rtu(r,16,6,0x1302,1);assert(set_emu_server_rtu(&a,r,8,reply,256)==8);
+ assert(starts[0]==1&&starts[1]==0&&wave[0].running&&!wave[1].running);
+ assert(read1(&a,0xa000)==42);
+ rtu(r,16,6,0xa000,1);assert(set_emu_server_rtu(&a,r,8,reply,256)==5&&reply[2]==6);
+ rtu(r,16,6,0x1402,1);
+ assert(set_emu_server_rtu(&a,r,8,reply,256)==5&&reply[2]==6);
+ assert(wave[0].running&&!a.igbt.enabled);
+ select_mode(&a,2);
+ assert(stops[0]==2&&!wave[0].running&&!wave[0].ready&&!a.igbt.enabled);
+ write(&a,0x1407,500);assert(a.igbt.delay_ms==500);
+ write(&a,0x1402,1);write(&a,0x1411,2);
+ assert(a.igbt.rx==1);
+ select_mode(&a,3);
+ assert(!a.igbt.rx&&!a.igbt.enabled&&!wave[0].running);
+ rtu(r,16,6,0x1302,1);
+ assert(set_emu_server_rtu(&a,r,8,reply,256)==5&&reply[2]==3); /* needs fresh upload */
+}
+static void igbt_timing(void)
+{
+ unsigned i;
+ init();select_mode(&a,2);write(&a,0x1402,1);
+ write(&a,0x1407,300);write(&a,0x1408,3);
+ set_igbt_tick(&a.igbt,255);assert(!a.igbt.rx&&a.igbt.event_count==8);
+ for(i=0;i<299;++i)set_igbt_tick(&a.igbt,255);
+ assert(!a.igbt.rx);
+ set_igbt_tick(&a.igbt,255);assert(a.igbt.rx==255);
+ set_igbt_tick(&a.igbt,255);set_igbt_tick(&a.igbt,255);assert(a.igbt.rx==255);
+ set_igbt_tick(&a.igbt,255);assert(!a.igbt.rx);
+ write(&a,0x1407,1);write(&a,0x1406,5);
+ set_igbt_tick(&a.igbt,0);set_igbt_tick(&a.igbt,0);assert(a.igbt.rx==255);
+ write(&a,0x1406,0);assert(a.igbt.rx==250); /* Preserve normal ACKs. */
+ set_igbt_tick(&a.igbt,0);set_igbt_tick(&a.igbt,0);set_igbt_tick(&a.igbt,0);
+ assert(!a.igbt.rx);
+ write(&a,0x1403,128);write(&a,0x140c,1);
+ set_igbt_tick(&a.igbt,0);set_igbt_tick(&a.igbt,0);assert(a.igbt.rx==128);
+ assert(read1(&a,0x1403)==128);
+ write(&a,0x1402,0);assert(!a.igbt.rx);
+ for(i=0;i<1000;++i)set_igbt_tick(&a.igbt,255);
+ assert(!a.igbt.rx&&!b.igbt.rx);
+}
+static void tms_shared_state(void)
+{
+ unsigned i;uint8_t req[18]={1,65},reply[256];uint16_t words[124];size_t n;
+ init();
+ assert(set_regmap_request(&a.map,6,0x1512,1,words,124,&n)==6);
+ write(&a,0x1219,1);write(&a,0x1218,0xa55a);
+ for(i=0;i<500;++i)set_emu_server_tick(&a,0);
+ write(&a,0x1508,5000);write(&a,0x150c,0x41);write(&a,0x150e,4);write(&a,0x1512,1);
+ for(i=0;i<1000;++i)set_emu_server_tick(&a,0);
+ assert(read1(&a,0x152e)==1000&&b.tms.frequency==0);
+ req[10]=0x41;seal(req,16); /* Leave target latched, no Setspeed. */
+ assert(set_emu_server_rtu(&a,req,18,reply,256)==101);
+ assert(reply[30]==0xe8&&reply[31]==3&&!set_crc16_modbus(reply,100));
+ req[10]=0x40;seal(req,16);
+ assert(set_emu_server_rtu(&a,req,18,reply,256)==101&&!read1(&a,0x1502));
+ assert(!set_regmap_request(&a.map,3,0x1400,16,words,124,&n));
+ /* Both register and CAN writes reject inactive executors. */
+ assert(set_regmap_request(&a.map,6,0x1402,1,words,124,&n)==6);
+ assert(set_regmap_request(&a.map,6,0x1302,1,words,124,&n)==6);
+ set_emu_server_tick(&a,255);assert(!a.igbt.rx);
+ select_mode(&a,2);assert(!a.tms.frequency&&!a.igbt.enabled);
+}
+static void stream_and_custom_composition(void)
+{
+ set_emu_stream stream={0};uint8_t req[8],reply[256];size_t n=0;unsigned i;
+ set_emu_server no_wave;uint16_t out[124];size_t count;
+ init();select_mode(&a,2);rtu(req,16,6,0x1407,19);
+ for(i=0;i<8;++i)n=set_emu_stream_feed(&stream,&a,req[i],i*20,reply,256);
+ assert(n==8&&a.igbt.delay_ms==19&&b.igbt.delay_ms==1);
+ assert(!set_emu_server_init(&no_wave,banks[2],banks[3],32,0,16,1,4,5,1));
+ assert(no_wave.map.count==5);
+ assert(set_regmap_request(&no_wave.map,3,0x1300,8,out,124,&count)==2);
+ assert(!set_regmap_request(&no_wave.map,3,0x1400,16,out,124,&count));
+ assert(set_emu_server_init(&no_wave,banks[2],banks[3],820,0,16,1,4,5,1)==3);
+}
+static void all_mode_pairs(void)
+{
+ unsigned from,to,i,error,token;uint16_t out[124];size_t n;
+ uint8_t can[8]={0x14,2,0,1,0x1f,0,7,6};
+ for(from=0;from<4;++from)for(to=0;to<4;++to) {
+ init();select_mode(&a,from);
+ if(from==3) {
+ write(&a,0x1304,2);write(&a,0xa000,1);write(&a,0xa001,4095);
+ write(&a,0x1306,0xa55a);write(&a,0x1302,1);
+ } else if(from==2) {
+ write(&a,0x1402,1);write(&a,0x1411,2);assert(a.igbt.rx==1);
+ } else if(from==0)write(&a,0x1000,1);
+ write(&a,0x1219,(uint16_t)to);write(&a,0x1218,0xa55a);
+ for(i=0;i<499;++i)set_emu_server_tick(&a,255);
+ assert(a.config[3]==from);
+ set_emu_server_tick(&a,255);assert(a.config[3]==to);
+ if(from!=to) {
+ assert(!wave[0].running&&!wave[0].ready&&!a.igbt.rx&&!a.igbt.enabled);
+ assert(!a.tms.frequency&&a.ump.state==3);
+ } else if(from==3)assert(wave[0].running); /* a read/apply must not restart output */
+ if(to!=3)assert(set_regmap_request(&a.map,6,0x1302,1,out,124,&n)==6);
+ if(to!=1)assert(set_regmap_request(&a.map,6,0x1512,1,out,124,&n)==6);
+ if(to!=2) {
+ (void)set_emu_server_can(&a,can,8,out,124,&token,&error);
+ assert(error==6&&!a.igbt.enabled);
+ }
+ }
+}
+int main(void)
+{
+ all_mode_pairs();directory_and_relocation();capacity_and_frames();independent_wave_and_igbt();
+ igbt_timing();tms_shared_state();stream_and_custom_composition();
+ puts("emulator services: directory, bounds, relocation, instances, RTU/CAN/stream, wave/TMS/IGBT OK");
+ return 0;
+}
diff --git a/c/set-protocol/tools/build_host.py b/c/set-protocol/tools/build_host.py
index 428a488..98e3e24 100644
--- a/c/set-protocol/tools/build_host.py
+++ b/c/set-protocol/tools/build_host.py
@@ -20,6 +20,7 @@ INCLUDE = ROOT / "include"
JNI_INCLUDES: list[Path] = []
SOURCES = [
ROOT / "src" / name for name in (
+ "set_regmap.c", "set_emu_ump.c", "set_emu_tms.c", "set_emu_igbt.c", "set_emu_server.c", "set_wavegen.c",
"altera_logic.c",
"altera_stream.c",
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c",