docs: consolidate documentation under doc

This commit is contained in:
2026-08-30 06:12:49 +03:00
parent 94fc8c5ef2
commit ab60e58318
17 changed files with 991 additions and 1748 deletions

View File

@@ -20,15 +20,15 @@ SETCAN/
Нормативное описание разделено по назначению:
- [`docs/protocan/PROTOCOL.md`](docs/protocan/PROTOCOL.md) — структура ProtoCAN;
- [`docs/protocan/BOOTLOADER.md`](docs/protocan/BOOTLOADER.md) — прошивка по CAN;
- [`docs/protocan/OAP.md`](docs/protocan/OAP.md) — правила ведения ОАП;
- [`docs/protocan/examples/test-vectors.json`](docs/protocan/examples/test-vectors.json) — машинные эталоны;
- [`docs/protocan/build/protocol.html`](docs/protocan/build/protocol.html) — собранная HTML-версия.
- [`doc/protocan/PROTOCOL.md`](doc/protocan/PROTOCOL.md) — структура ProtoCAN;
- [`doc/protocan/BOOTLOADER.md`](doc/protocan/BOOTLOADER.md) — прошивка по CAN;
- [`doc/protocan/OAP.md`](doc/protocan/OAP.md) — правила ведения ОАП;
- [`doc/protocan/examples/test-vectors.json`](doc/protocan/examples/test-vectors.json) — машинные эталоны;
- [`doc/index.html`](doc/index.html) — единая собранная HTML-версия всей документации.
Большой файл `Протокол CAN и ОАП.md` и исходный XLSX сохранены как табличное
представление реестра. Правила приоритета источников описаны в
[`docs/protocan/README.md`](docs/protocan/README.md).
[`doc/protocan/README.md`](doc/protocan/README.md).
## Формат расширенного CAN ID

View File

@@ -5,15 +5,9 @@ set "SCRIPT_DIR=%~dp0"
where pwsh.exe >nul 2>nul
if %ERRORLEVEL% EQU 0 (
pwsh.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build-html.ps1" %*
pwsh.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build-html.ps1"
) else (
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build-html.ps1" %*
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build-html.ps1"
)
set "EXIT_CODE=%ERRORLEVEL%"
if not "%EXIT_CODE%"=="0" (
echo.
echo Ошибка сборки HTML. Код: %EXIT_CODE%
)
exit /b %EXIT_CODE%
exit /b %ERRORLEVEL%

87
doc/build-html.ps1 Normal file
View File

@@ -0,0 +1,87 @@
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$outputPath = Join-Path $PSScriptRoot 'index.html'
$sourceDirectory = Join-Path $PSScriptRoot 'protocan'
$documents = @(
'README.md',
'PROTOCOL.md',
'BOOTLOADER.md',
'OAP.md',
'CHANGELOG.md'
)
function Convert-LocalLinks {
param(
[string]$Html,
[string]$SourcePath
)
$sourceParent = Split-Path -Parent $SourcePath
$outputParent = Split-Path -Parent $outputPath
$pattern = '(?<attribute>href|src)="(?<target>(?![a-z]+:|/|#)[^"]+)"'
return [regex]::Replace($Html, $pattern, {
param($match)
$target = $match.Groups['target'].Value
$parts = $target -split '#', 2
$targetPath = [Uri]::UnescapeDataString($parts[0])
$absoluteTarget = [System.IO.Path]::GetFullPath((Join-Path $sourceParent $targetPath))
$relativeTarget = [System.IO.Path]::GetRelativePath($outputParent, $absoluteTarget).Replace('\', '/')
if ($parts.Count -eq 2) {
$relativeTarget += '#' + $parts[1]
}
return $match.Groups['attribute'].Value + '="' + $relativeTarget + '"'
})
}
$sections = foreach ($document in $documents) {
$path = Join-Path $sourceDirectory $document
$markdown = Get-Content -Raw -LiteralPath $path -Encoding UTF8
$html = (ConvertFrom-Markdown -InputObject $markdown).Html
$html = Convert-LocalLinks -Html $html -SourcePath $path
"<article class=`"card protocan-document`" data-source=`"$document`">$html</article>"
}
$vectorsPath = Join-Path $sourceDirectory 'examples\test-vectors.json'
$vectors = [System.Net.WebUtility]::HtmlEncode(
(Get-Content -Raw -LiteralPath $vectorsPath -Encoding UTF8)
)
$sections += @"
<article class="card protocan-document" data-source="examples/test-vectors.json">
<h1>Тестовые векторы</h1>
<p>Машинные эталоны из <code>examples/test-vectors.json</code>.</p>
<pre><code>$vectors</code></pre>
</article>
"@
$generatedBlock = @"
<!-- PROTOCAN:START -->
<section id="protocan-full" class="section">
<h2>Полная документация ProtoCAN</h2>
<p class="lead">Нормативные документы и тестовые векторы собраны в эту страницу из исходников <code>doc/protocan</code>.</p>
<div class="grid protocan-grid">
$($sections -join "`n")
</div>
</section>
<!-- PROTOCAN:END -->
"@
$page = Get-Content -Raw -LiteralPath $outputPath -Encoding UTF8
$pattern = '(?s)<!-- PROTOCAN:START -->.*?<!-- PROTOCAN:END -->'
if ($page -notmatch $pattern) {
throw 'Не найдены маркеры PROTOCAN:START/END в doc/index.html.'
}
$page = [regex]::Replace($page, $pattern, [System.Text.RegularExpressions.MatchEvaluator]{
param($match)
$generatedBlock
}, 1)
$page = $page.TrimEnd("`r", "`n") + [Environment]::NewLine
$utf8WithoutBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText($outputPath, $page, $utf8WithoutBom)
Write-Host "[DONE] Создан единый HTML: $outputPath"

View File

@@ -72,9 +72,28 @@
.docs { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
.doclink { display: inline-flex; align-items: center; gap: 8px; padding: 9px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--panel); }
.doclink:hover { border-color: var(--cyan); text-decoration: none; }
#protocan-full { margin-top: 64px; scroll-margin-top: 82px; }
#protocan-full > .lead { margin-bottom: 24px; }
.protocan-grid { display: block; }
.protocan-document { width: 100%; margin-top: 16px; padding: clamp(20px, 4vw, 42px); overflow: hidden; }
.protocan-document:first-child { margin-top: 0; }
.protocan-document h1 { max-width: none; margin: 0 0 20px; font-size: clamp(30px, 4vw, 44px); line-height: 1.08; letter-spacing: -.035em; }
.protocan-document h2 { margin: 36px 0 12px; font-size: clamp(24px, 3vw, 32px); line-height: 1.15; }
.protocan-document h3 { margin: 26px 0 10px; font-size: 20px; }
.protocan-document h1 + p,
.protocan-document h2 + p,
.protocan-document h3 + p { margin-top: 0; }
.protocan-document p { max-width: 88ch; }
.protocan-document table { display: block; width: 100%; min-width: 0; margin: 16px 0 26px; overflow-x: auto; }
.protocan-document thead,
.protocan-document tbody { min-width: 720px; }
.protocan-document th,
.protocan-document td { min-width: 130px; }
.protocan-document pre { max-width: 100%; padding-top: 18px; }
.protocan-document hr { margin: 34px 0; border: 0; border-top: 1px solid var(--line); }
footer { margin-top: 42px; padding-top: 20px; border-top: 1px solid var(--line); color: var(--muted); font-size: 13px; }
@media (max-width: 850px) { .card, .card.wide { grid-column: 1 / -1; } .flow { grid-template-columns: 1fr 1fr; } }
@media (max-width: 520px) { .shell { width: min(100% - 20px, 1180px); padding-top: 20px; } .hero { padding-top: 22px; } .flow { grid-template-columns: 1fr; } .tabs-wrap { margin-bottom: 18px; } }
@media (max-width: 520px) { .shell { width: min(100% - 20px, 1180px); padding-top: 20px; } .hero { padding-top: 22px; } .flow { grid-template-columns: 1fr; } .tabs-wrap { margin-bottom: 18px; } .protocan-document { padding: 18px 14px; } }
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; animation: none !important; } }
</style>
</head>
@@ -106,7 +125,7 @@
<article class="card wide"><span class="tag">Структура</span><h3>Минимальное ядро + нормативные документы</h3><div class="filetree">SETCAN/
├── Inc/protocan.h публичные типы, настройки и API
├── Src/protocan.c фильтры, RX-очередь, разбор и отправка
├── docs/protocan/ спецификация протокола
├── doc/protocan/ спецификация протокола
│ ├── PROTOCOL.md 29-битный ID и типы сообщений
│ ├── OAP.md общее адресное пространство
│ ├── BOOTLOADER.md обновление прошивки по CAN (draft)
@@ -270,11 +289,877 @@ if (PROTOCAN_SEND(id, tx) != PROTOCAN_OK) {
}</code></pre></article>
</div>
<div class="docs" aria-label="Исходная документация">
<a class="doclink" href="../README.md">README проекта ↗</a><a class="doclink" href="../docs/protocan/PROTOCOL.md">PROTOCOL.md ↗</a><a class="doclink" href="../docs/protocan/OAP.md">OAP.md ↗</a><a class="doclink" href="../docs/protocan/BOOTLOADER.md">BOOTLOADER.md ↗</a><a class="doclink" href="../docs/protocan/examples/test-vectors.json">test-vectors.json ↗</a>
<a class="doclink" href="../README.md">README проекта ↗</a><a class="doclink" href="protocan/PROTOCOL.md">PROTOCOL.md ↗</a><a class="doclink" href="protocan/OAP.md">OAP.md ↗</a><a class="doclink" href="protocan/BOOTLOADER.md">BOOTLOADER.md ↗</a><a class="doclink" href="protocan/examples/test-vectors.json">test-vectors.json ↗</a>
</div>
</section>
<footer>Сформировано по исходникам и документации SETCAN. Нормативный приоритет сохраняют PROTOCOL.md, BOOTLOADER.md и реестр ОАП.</footer>
<!-- PROTOCAN:START -->
<section id="protocan-full" class="section">
<h2>Полная документация ProtoCAN</h2>
<p class="lead">Нормативные документы и тестовые векторы собраны в эту страницу из исходников <code>doc/protocan</code>.</p>
<div class="grid protocan-grid">
<article class="card protocan-document" data-source="README.md"><h1 id="protocan">Документация ProtoCAN</h1>
<p>Статус комплекта: <strong>Draft</strong><br />
Версия комплекта: <strong>1.0</strong><br />
Дата редакции: <strong>2026-08-29</strong><br />
Транспорт: <strong>Classic CAN 2.0B, Extended ID, DLC 0…8</strong></p>
<p>Этот каталог разделяет нормативное описание протокола, загрузчик и реестр
общего адресного пространства. Большой исходный документ
<a href="../Протокол CAN и ОАП.md"><code>Протокол CAN и ОАП.md</code></a> сохранён как
совместимое представление таблиц из Excel.</p>
<h2 id="section">Документы</h2>
<table>
<thead>
<tr>
<th>Документ</th>
<th>Назначение</th>
<th>Статус источника</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="protocan/PROTOCOL.md">PROTOCOL.md</a></td>
<td>29-битный CAN ID, адресация, реестр <code>MsgType</code>, порядок байтов</td>
<td>нормативный</td>
</tr>
<tr>
<td><a href="protocan/BOOTLOADER.md">BOOTLOADER.md</a></td>
<td>обновление прошивки, кадры, состояния, ошибки и A/B-слоты</td>
<td>нормативный draft</td>
</tr>
<tr>
<td><a href="protocan/OAP.md">OAP.md</a></td>
<td>правила ведения общего адресного пространства</td>
<td>нормативный индекс</td>
</tr>
<tr>
<td><a href="../Протокол CAN и ОАП.xlsx">../../Протокол CAN и ОАП.xlsx</a></td>
<td>редактируемый реестр ОАП</td>
<td>источник таблиц</td>
</tr>
<tr>
<td><a href="protocan/examples/test-vectors.json">examples/test-vectors.json</a></td>
<td>машинные эталоны CAN ID и payload</td>
<td>нормативные примеры</td>
</tr>
<tr>
<td><a href="protocan/CHANGELOG.md">CHANGELOG.md</a></td>
<td>история версий документа</td>
<td>нормативный</td>
</tr>
</tbody>
</table>
<h2 id="section-1">Приоритет источников</h2>
<p>При расхождении данных действует следующий порядок:</p>
<ol>
<li><code>PROTOCOL.md</code> — структура ProtoCAN и реестр типов сообщений.</li>
<li><code>BOOTLOADER.md</code> — загрузочный сервис <code>0x9…0xD</code>.</li>
<li>XLSX — адреса и свойства регистров ОАП.</li>
<li>Сгенерированный <a href="index.html"><code>../index.html</code></a> — только представление, не самостоятельный источник.</li>
</ol>
<h2 id="html">Сборка HTML</h2>
<p>Из корня проекта:</p>
<pre><code class="language-powershell">./doc/build-html.bat
</code></pre>
<p>Все документы и тестовые векторы включаются в единый файл <code>doc/index.html</code>.
Скрипт не изменяет исходные Markdown/XLSX и пригоден для запуска в CI.</p>
</article>
<article class="card protocan-document" data-source="PROTOCOL.md"><h1 id="protocan">ProtoCAN — базовый протокол</h1>
<p>Статус: <strong>Stable с зарезервированным загрузочным расширением</strong><br />
Версия: <strong>1.0</strong><br />
Порядок байтов payload: <strong>little-endian</strong>, если явно не указано иное</p>
<h2 id="section">Назначение</h2>
<p>ProtoCAN — прикладной протокол поверх classic CAN 2.0B. Используются только
расширенные 29-битные идентификаторы (<code>IDE=1</code>) и payload длиной 0…8 байт.</p>
<h2 id="section-1">Термины</h2>
<table>
<thead>
<tr>
<th>Термин</th>
<th>Значение</th>
</tr>
</thead>
<tbody>
<tr>
<td>ПМ</td>
<td>управляющий модуль</td>
</tr>
<tr>
<td>прибор</td>
<td>адресуемый узел на шине</td>
</tr>
<tr>
<td><code>DeviceType</code></td>
<td>тип прибора, 0…7</td>
</tr>
<tr>
<td><code>DeviceID</code></td>
<td>экземпляр прибора данного типа, 0…15</td>
</tr>
<tr>
<td><code>MsgType</code></td>
<td>класс сообщения или сервис</td>
</tr>
<tr>
<td><code>MsgBody</code></td>
<td>16-битное поле, формат которого зависит от <code>MsgType</code></td>
</tr>
</tbody>
</table>
<p>Пара <code>DeviceType/DeviceID</code> задаёт до <code>8 × 16 = 128</code> уникальных адресов.</p>
<h2 id="can-id">Расширенный CAN ID</h2>
<pre><code class="language-text">28 27 26...24 23...20 19...16 15........0
Priority Route DeviceType DeviceID MsgType MsgBody
1 бит 1 бит 3 бита 4 бита 4 бита 16 бит
</code></pre>
<pre><code class="language-c">can_id =
((uint32_t)priority &lt;&lt; 28) |
((uint32_t)route &lt;&lt; 27) |
((uint32_t)device_type &lt;&lt; 24) |
((uint32_t)device_id &lt;&lt; 20) |
((uint32_t)msg_type &lt;&lt; 16) |
msg_body;
</code></pre>
<table>
<thead>
<tr>
<th>Поле</th>
<th>Значения</th>
<th>Назначение</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Priority</code></td>
<td><code>0</code> critical, <code>1</code> standard</td>
<td>CAN-арбитраж</td>
</tr>
<tr>
<td><code>Route</code></td>
<td><code>0</code> от ПМ, <code>1</code> от прибора</td>
<td>логическое направление</td>
</tr>
<tr>
<td><code>DeviceType</code></td>
<td><code>0…7</code></td>
<td>тип прибора</td>
</tr>
<tr>
<td><code>DeviceID</code></td>
<td><code>0…15</code></td>
<td>номер экземпляра</td>
</tr>
<tr>
<td><code>MsgType</code></td>
<td><code>0…15</code></td>
<td>тип сообщения</td>
</tr>
<tr>
<td><code>MsgBody</code></td>
<td><code>0…65535</code></td>
<td>команда, адрес или номер блока</td>
</tr>
</tbody>
</table>
<p><code>Route</code> не является направлением физического трансивера. Ответ прибора
сохраняет адрес <code>DeviceType/DeviceID</code> и устанавливает <code>Route=1</code>.</p>
<h2 id="msgtype">Реестр <code>MsgType</code></h2>
<table>
<thead>
<tr>
<th style="text-align: right;">Код</th>
<th>Имя</th>
<th>Основное направление</th>
<th style="text-align: right;">DLC</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x0</code></td>
<td><code>BROADCAST</code></td>
<td>ПМ → все</td>
<td style="text-align: right;">зависит от команды</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x1</code></td>
<td><code>DISCRETE</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x2</code></td>
<td><code>ANALOG</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x3</code></td>
<td><code>GAS</code></td>
<td>оба</td>
<td style="text-align: right;">0/2/4/6/8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x4</code></td>
<td><code>MODBUS_COIL</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x5</code></td>
<td><code>MODBUS_DISCRETE</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x6</code></td>
<td><code>MODBUS_HOLDING</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x7</code></td>
<td><code>MODBUS_INPUT</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x8</code></td>
<td><code>ERROR</code></td>
<td>прибор → ПМ</td>
<td style="text-align: right;">0</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x9</code></td>
<td><code>BOOT_CONTROL</code></td>
<td>ПМ → прибор</td>
<td style="text-align: right;">0/8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xA</code></td>
<td><code>BOOT_DATA_A</code></td>
<td>ПМ → прибор</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xB</code></td>
<td><code>BOOT_DATA_B</code></td>
<td>ПМ → прибор</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xC</code></td>
<td><code>BOOT_STATUS</code></td>
<td>прибор → ПМ</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xD</code></td>
<td><code>BOOT_DISCOVERY</code></td>
<td>прибор → ПМ</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xE</code></td>
<td><code>SETTINGS</code></td>
<td>оба</td>
<td style="text-align: right;">0/1/8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xF</code></td>
<td><code>PULSE</code></td>
<td>прибор → сеть</td>
<td style="text-align: right;">1</td>
<td>stable</td>
</tr>
</tbody>
</table>
<p>Подробный формат <code>0x9…0xD</code> находится в <a href="protocan/BOOTLOADER.md">BOOTLOADER.md</a>.</p>
<h2 id="msgbody">Разметки <code>MsgBody</code></h2>
<table>
<thead>
<tr>
<th><code>MsgType</code></th>
<th>Биты <code>MsgBody</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>broadcast</td>
<td>команда <code>[15:4]</code>, параметр <code>[3:0]</code></td>
</tr>
<tr>
<td>discrete/analog</td>
<td>подтип <code>[15:12]</code>, значение/адрес <code>[11:0]</code></td>
</tr>
<tr>
<td>Modbus</td>
<td>начальный адрес <code>[15:4]</code>, количество <code>[3:0]</code></td>
</tr>
<tr>
<td>GAS</td>
<td>адрес первого 16-битного регистра <code>[15:0]</code></td>
</tr>
<tr>
<td>error</td>
<td>дополнительная информация <code>[15:8]</code>, код <code>[7:0]</code></td>
</tr>
<tr>
<td>settings</td>
<td>номер сборки <code>[15:8]</code>, позиция <code>[7:0]</code></td>
</tr>
<tr>
<td>boot control/status</td>
<td><code>SessionID[15:8]</code>, команда <code>[7:0]</code></td>
</tr>
<tr>
<td>boot data</td>
<td><code>BlockIndex[15:0]</code></td>
</tr>
</tbody>
</table>
<h2 id="section-2">Общие правила обмена</h2>
<ul>
<li>Многобайтовые значения в <code>DATA</code> передаются little-endian.</li>
<li>Узел игнорирует адресованные кадры с чужим <code>DeviceType/DeviceID</code>.</li>
<li>Прибор принимает команды ПМ с <code>Route=0</code>; ПМ принимает ответы с <code>Route=1</code>.</li>
<li>Стандартные 11-битные CAN ID не являются кадрами ProtoCAN.</li>
<li>RTR для загрузочного сервиса запрещён.</li>
<li>Неописанные комбинации <code>MsgType/MsgBody/DLC</code> должны отвергаться.</li>
</ul>
<h2 id="id">Эталон упаковки ID</h2>
<pre><code class="language-text">Priority = 1
Route = 0
DeviceType = 3
DeviceID = 5
MsgType = 0x9
MsgBody = 0x0702
CAN ID = 0x13590702
</code></pre>
<p>Этот пример соответствует <code>ENTER_BOOT</code>, <code>SessionID=7</code>. Машинные варианты
находятся в <a href="protocan/examples/test-vectors.json">examples/test-vectors.json</a>.</p>
</article>
<article class="card protocan-document" data-source="BOOTLOADER.md"><h1 id="protocan-boot-protocol">ProtoCAN Boot Protocol</h1>
<p>Статус: <strong>Draft</strong><br />
Версия протокола: <strong>1.0</strong><br />
Совместимость: <strong>classic CAN 2.0B, Extended ID, DLC 0…8</strong><br />
Реализация: <a href="../../templates/c/protocan-boot"><code>templates/c/protocan-boot</code></a></p>
<h2 id="section">Назначение</h2>
<p>Сервис обновляет адресованный прибор по CAN и поддерживает два логических
слота A/B. Активный слот не стирается: новый образ записывается в неактивный,
проверяется и атомарно назначается кандидатом на запуск.</p>
<h2 id="section-1">Карта сообщений</h2>
<table>
<thead>
<tr>
<th style="text-align: right;"><code>MsgType</code></th>
<th>Имя</th>
<th><code>MsgBody</code></th>
<th>Payload</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x9</code></td>
<td><code>BOOT_CONTROL</code></td>
<td><code>SessionID[15:8] \| Command[7:0]</code></td>
<td>параметры команды</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xA</code></td>
<td><code>BOOT_DATA_A</code></td>
<td><code>BlockIndex[15:0]</code></td>
<td>8 байт слота A</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xB</code></td>
<td><code>BOOT_DATA_B</code></td>
<td><code>BlockIndex[15:0]</code></td>
<td>8 байт слота B</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xC</code></td>
<td><code>BOOT_STATUS</code></td>
<td><code>SessionID[15:8] \| Command[7:0]</code></td>
<td>статус и прогресс</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xD</code></td>
<td><code>BOOT_DISCOVERY</code></td>
<td>подтип ответа</td>
<td>идентификация</td>
</tr>
</tbody>
</table>
<p>Все команды записи адресуются конкретному <code>DeviceType/DeviceID</code> и имеют
<code>Route=0</code>. Ответы сохраняют адрес прибора и имеют <code>Route=1</code>.</p>
<h2 id="section-2">Адресация образа</h2>
<p><code>MsgBody</code> кадра данных — номер 8-байтового блока:</p>
<pre><code class="language-c">offset = (uint32_t)BlockIndex * 8U;
address = SLOT_X_BASE + offset;
</code></pre>
<pre><code class="language-text">512 КиБ = 524 288 байт
524 288 / 8 = 65 536 блоков
BlockIndex = 0x0000…0xFFFF
</code></pre>
<table>
<thead>
<tr>
<th style="text-align: right;"><code>BlockIndex</code></th>
<th style="text-align: right;">Смещение</th>
<th style="text-align: right;">Диапазон байтов</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x0000</code></td>
<td style="text-align: right;"><code>0x00000</code></td>
<td style="text-align: right;"><code>0x00000…0x00007</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0001</code></td>
<td style="text-align: right;"><code>0x00008</code></td>
<td style="text-align: right;"><code>0x00008…0x0000F</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0xFFFF</code></td>
<td style="text-align: right;"><code>0x7FFF8</code></td>
<td style="text-align: right;"><code>0x7FFF8…0x7FFFF</code></td>
</tr>
</tbody>
</table>
<p><code>0x80000</code> является первой позицией за границей слота. Последний кадр
дополняется <code>0xFF</code>, но CRC32 вычисляется только по <code>ImageSize</code> байтам.</p>
<h2 id="boot_control">Команды <code>BOOT_CONTROL</code></h2>
<table>
<thead>
<tr>
<th style="text-align: right;">Код</th>
<th>Команда</th>
<th style="text-align: right;">DLC</th>
<th>Payload</th>
<th>Допустимое состояние</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x01</code></td>
<td><code>IDENTIFY</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>любое</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x02</code></td>
<td><code>ENTER_BOOT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>любое; <code>SessionID != 0</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0x03</code></td>
<td><code>BEGIN_IMAGE</code></td>
<td style="text-align: right;">8</td>
<td>размер и CRC32</td>
<td>metadata</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x04</code></td>
<td><code>BEGIN_COMPAT</code></td>
<td style="text-align: right;">8</td>
<td>совместимость и версия</td>
<td>metadata</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x05</code></td>
<td><code>ERASE</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>ready-to-erase</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x06</code></td>
<td><code>VERIFY</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>образ получен</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x07</code></td>
<td><code>COMMIT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>verified</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x08</code></td>
<td><code>CONFIRM</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>запущенное приложение</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x09</code></td>
<td><code>REBOOT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>активная сессия</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0A</code></td>
<td><code>ABORT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>активная сессия</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0B</code></td>
<td><code>QUERY_PROGRESS</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>активная сессия</td>
</tr>
</tbody>
</table>
<h3 id="begin_image"><code>BEGIN_IMAGE</code></h3>
<pre><code class="language-text">DATA[0..3] ImageSize, uint32 little-endian
DATA[4..7] ImageCRC32, uint32 little-endian
</code></pre>
<h3 id="begin_compat"><code>BEGIN_COMPAT</code></h3>
<pre><code class="language-text">DATA[0..1] ProductType, uint16 little-endian
DATA[2] HardwareRevisionMin
DATA[3] HardwareRevisionMax
DATA[4..7] FirmwareVersion, uint32 little-endian
</code></pre>
<p>До <code>ERASE</code> прибор обязан получить обе части метаданных и проверить размер,
тип изделия, аппаратную ревизию, версию и политику anti-rollback.</p>
<h2 id="boot_status"><code>BOOT_STATUS</code></h2>
<pre><code class="language-text">MsgBody[15..8] SessionID
MsgBody[7..0] команда, на которую дан ответ
DATA[0] Status
DATA[1] TargetSlot: 0=A, 1=B, 0xFF=не выбран
DATA[2..3] NextBlock, uint16 little-endian
DATA[4..7] RunningCRC32, uint32 little-endian
</code></pre>
<table>
<thead>
<tr>
<th style="text-align: right;">Код</th>
<th>Статус</th>
<th>Повтор допустим</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x00</code></td>
<td><code>OK</code></td>
<td>—</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x01</code></td>
<td><code>BUSY</code></td>
<td>да, после задержки</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x02</code></td>
<td><code>INVALID_COMMAND</code></td>
<td>после исправления</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x03</code></td>
<td><code>WRONG_DEVICE</code></td>
<td>нет для этого образа</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x04</code></td>
<td><code>WRONG_HARDWARE</code></td>
<td>нет для этого образа</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x05</code></td>
<td><code>INVALID_SIZE</code></td>
<td>нет для этого образа</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x06</code></td>
<td><code>CRC_ERROR</code></td>
<td>новая передача</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x07</code></td>
<td><code>FLASH_ERROR</code></td>
<td>зависит от платформы</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x08</code></td>
<td><code>SEQUENCE_ERROR</code></td>
<td>да, с <code>NextBlock</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0x09</code></td>
<td><code>SIGNATURE_ERROR</code></td>
<td>нет</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0A</code></td>
<td><code>SESSION_ERROR</code></td>
<td>открыть новую сессию</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0B</code></td>
<td><code>VOLTAGE_ERROR</code></td>
<td>да после нормализации питания</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0C</code></td>
<td><code>INVALID_STATE</code></td>
<td>выполнить правильный переход</td>
</tr>
</tbody>
</table>
<h2 id="state-machine">State machine</h2>
<pre><code class="language-text">IDLE
└─ ENTER_BOOT ─&gt; METADATA
├─ BEGIN_IMAGE
└─ BEGIN_COMPAT
v
READY_TO_ERASE
│ ERASE
v
RECEIVING
│ VERIFY
v
VERIFIED
│ COMMIT
v
PENDING + REBOOT
│ CONFIRM
v
CONFIRMED
</code></pre>
<p>Ошибка Flash, CRC, совместимости или подписи переводит сессию в <code>FAILED</code>.
Новая <code>ENTER_BOOT</code> создаёт чистую сессию. <code>ABORT</code> прекращает текущую передачу,
не активируя частично записанный слот.</p>
<h2 id="section-3">Надёжность и повторы</h2>
<ul>
<li>Блоки передаются строго по возрастанию <code>BlockIndex</code>.</li>
<li>Дубликат или пропуск возвращает <code>SEQUENCE_ERROR</code> и ожидаемый <code>NextBlock</code>.</li>
<li>Базовый режим подтверждает каждый блок.</li>
<li>Рабочий режим может подтверждать окно из 16 блоков.</li>
<li>После потери связи <code>QUERY_PROGRESS</code> возвращает следующий ожидаемый блок,
пока состояние загрузчика сохранено.</li>
<li>Для продолжения после перезагрузки порт должен сохранять session metadata
и восстановить её при инициализации; ядро версии 1.0 само это не делает.</li>
</ul>
<h2 id="ab">Безопасность и A/B-обновление</h2>
<pre><code class="language-text">active=A -&gt; target=B -&gt; verify -&gt; pending=B
active=B -&gt; target=A -&gt; verify -&gt; pending=A
</code></pre>
<p>CRC32 защищает только от случайного повреждения. Серийный загрузчик должен
дополнительно проверить подпись контейнера, границы вектора, совместимость и
anti-rollback. Bootloader не обновляется командами <code>BOOT_DATA_A/B</code>.</p>
<p>Boot metadata должна атомарно хранить:</p>
<ul>
<li>активный слот;</li>
<li>pending-слот;</li>
<li>подтверждение запуска;</li>
<li>число неудачных попыток;</li>
<li>версию и CRC32 образа.</li>
</ul>
<p>Если приложение не выполняет <code>CONFIRM</code> за установленное число запусков,
загрузчик возвращается к предыдущему подтверждённому слоту.</p>
<h2 id="section-4">Эталонный сценарий</h2>
<ol>
<li>ПМ адресно отправляет <code>IDENTIFY</code>.</li>
<li>ПМ открывает ненулевой <code>SessionID</code> командой <code>ENTER_BOOT</code>.</li>
<li>ПМ отправляет <code>BEGIN_IMAGE</code> и <code>BEGIN_COMPAT</code>.</li>
<li>Прибор сообщает выбранный неактивный слот.</li>
<li>ПМ выполняет <code>ERASE</code> и передаёт <code>BOOT_DATA_A</code> либо <code>BOOT_DATA_B</code>.</li>
<li>ПМ выполняет <code>VERIFY</code>, затем <code>COMMIT</code> и <code>REBOOT</code>.</li>
<li>Новое приложение после самопроверки выполняет <code>CONFIRM</code>.</li>
</ol>
</article>
<article class="card protocan-document" data-source="OAP.md"><h1 id="section">Общее адресное пространство</h1>
<p>Статус: <strong>Stable, данные ведутся в XLSX</strong><br />
Порядок значений: <strong>16-битные регистры, little-endian в CAN payload</strong></p>
<p>Редактируемый источник реестра:
<a href="../Протокол CAN и ОАП.xlsx"><code>Протокол CAN и ОАП.xlsx</code></a>.</p>
<p>Просматриваемая большая таблица находится в
<a href="../Протокол CAN и ОАП.html"><code>Протокол CAN и ОАП.html</code></a> и
<a href="../Протокол CAN и ОАП.md"><code>Протокол CAN и ОАП.md</code></a>.</p>
<h2 id="section-1">Назначение</h2>
<p>ОАП связывает 16-битный адрес регистра с его типом, назначением, доступом и
масштабом. В ProtoCAN используется <code>MsgType=0x3</code>, а <code>MsgBody</code> содержит адрес
первого регистра.</p>
<h2 id="section-2">Обязательные поля реестра</h2>
<table>
<thead>
<tr>
<th>Поле</th>
<th>Требование</th>
</tr>
</thead>
<tbody>
<tr>
<td>AddressHex</td>
<td><code>0x0000…0xFFFF</code>, уникальное значение</td>
</tr>
<tr>
<td>AddressDec</td>
<td>десятичный эквивалент AddressHex</td>
</tr>
<tr>
<td>Group</td>
<td>функциональная группа</td>
</tr>
<tr>
<td>Name</td>
<td>однозначное имя параметра</td>
</tr>
<tr>
<td>Type</td>
<td><code>u16</code>, <code>i16</code>, <code>u32</code>, <code>i32</code>, <code>float32</code>, bitmap или массив</td>
</tr>
<tr>
<td>Registers</td>
<td>число занятых 16-битных регистров</td>
</tr>
<tr>
<td>Access</td>
<td><code>R</code>, <code>W</code> или <code>RW</code></td>
</tr>
<tr>
<td>Unit</td>
<td>физическая единица либо <code>—</code></td>
</tr>
<tr>
<td>Scale</td>
<td>множитель/делитель представления</td>
</tr>
<tr>
<td>Default</td>
<td>значение после сброса, если применимо</td>
</tr>
<tr>
<td>Description</td>
<td>семантика, диапазон и особые значения</td>
</tr>
</tbody>
</table>
<h2 id="section-3">Правила ведения</h2>
<ul>
<li>Адрес не переиспользуется с другим смыслом после выпуска стабильной версии.</li>
<li>Многорегистровое значение занимает непрерывный диапазон.</li>
<li>Порядок 16-битных слов для 32-битного значения фиксируется в строке типа.</li>
<li>Резервные диапазоны явно отмечаются и не используются без изменения версии.</li>
<li>Удалённый параметр помечается deprecated, а не исчезает молча.</li>
<li>Изменение адреса, типа или масштаба отражается в <code>CHANGELOG.md</code>.</li>
</ul>
<h2 id="section-4">Экспорт</h2>
<p>Для программной генерации каталог следует экспортировать из XLSX в CSV с
UTF-8 и фиксированными английскими именами колонок. CSV должен проверяться на:</p>
<ul>
<li>уникальность адресов;</li>
<li>пересечение многорегистровых значений;</li>
<li>допустимые типы и права доступа;</li>
<li>равенство шестнадцатеричного и десятичного адреса;</li>
<li>попадание адреса в диапазон <code>0x0000…0xFFFF</code>.</li>
</ul>
<p>До появления автоматического экспортёра нормативным источником адресов
остаётся XLSX, а HTML/Markdown считаются представлением.</p>
</article>
<article class="card protocan-document" data-source="CHANGELOG.md"><h1 id="protocan">История изменений ProtoCAN</h1>
<p>Формат основан на Keep a Changelog. Версия относится к спецификации, а не к
версии прошивки отдельного прибора.</p>
<h2 id="unreleased">[Unreleased]</h2>
<h3 id="added">Added</h3>
<ul>
<li>Структурированный комплект документации <code>doc/protocan</code>.</li>
<li>Загрузочный сервис <code>MsgType=0x9…0xD</code>.</li>
<li>Два логических слота по 512 КиБ, 65 536 блоков по 8 байт каждый.</li>
<li>Машинные эталоны CAN ID в <code>examples/test-vectors.json</code>.</li>
</ul>
<h2 id="section">[1.0] — 2026-08-29</h2>
<h3 id="added-1">Added</h3>
<ul>
<li>Зафиксирована 29-битная структура ProtoCAN ID.</li>
<li>Зафиксирована адресация 8 типов по 16 экземпляров.</li>
<li>Существующие сообщения <code>0x0…0x8</code>, <code>0xE</code>, <code>0xF</code> сохранены.</li>
</ul>
</article>
<article class="card protocan-document" data-source="examples/test-vectors.json">
<h1>Тестовые векторы</h1>
<p>Машинные эталоны из <code>examples/test-vectors.json</code>.</p>
<pre><code>{
&quot;schema_version&quot;: 1,
&quot;byte_order&quot;: &quot;little-endian&quot;,
&quot;frames&quot;: [
{
&quot;name&quot;: &quot;enter_boot_session_7&quot;,
&quot;direction&quot;: &quot;pm_to_device&quot;,
&quot;priority&quot;: 1,
&quot;route&quot;: 0,
&quot;device_type&quot;: 3,
&quot;device_id&quot;: 5,
&quot;msg_type&quot;: 9,
&quot;msg_body&quot;: 1794,
&quot;can_id_hex&quot;: &quot;0x13590702&quot;,
&quot;dlc&quot;: 0,
&quot;data_hex&quot;: &quot;&quot;
},
{
&quot;name&quot;: &quot;slot_b_block_1&quot;,
&quot;direction&quot;: &quot;pm_to_device&quot;,
&quot;priority&quot;: 1,
&quot;route&quot;: 0,
&quot;device_type&quot;: 3,
&quot;device_id&quot;: 5,
&quot;msg_type&quot;: 11,
&quot;msg_body&quot;: 1,
&quot;can_id_hex&quot;: &quot;0x135B0001&quot;,
&quot;dlc&quot;: 8,
&quot;data_hex&quot;: &quot;1011121314151617&quot;
},
{
&quot;name&quot;: &quot;enter_boot_ok&quot;,
&quot;direction&quot;: &quot;device_to_pm&quot;,
&quot;priority&quot;: 1,
&quot;route&quot;: 1,
&quot;device_type&quot;: 3,
&quot;device_id&quot;: 5,
&quot;msg_type&quot;: 12,
&quot;msg_body&quot;: 1794,
&quot;can_id_hex&quot;: &quot;0x1B5C0702&quot;,
&quot;dlc&quot;: 8,
&quot;data_hex&quot;: &quot;00FF000000000000&quot;
}
]
}
</code></pre>
</article>
</div>
</section>
<!-- PROTOCAN:END -->
<footer>Сформировано по исходникам и документации SETCAN. Для пересборки запустите <code>doc/build-html.bat</code>. Нормативный приоритет сохраняют PROTOCOL.md, BOOTLOADER.md и реестр ОАП.</footer>
</main>
<script>
const tabs = [...document.querySelectorAll('[role="tab"]')];

View File

@@ -7,7 +7,7 @@
### Added
- Структурированный комплект документации `docs/protocan`.
- Структурированный комплект документации `doc/protocan`.
- Загрузочный сервис `MsgType=0x9…0xD`.
- Два логических слота по 512 КиБ, 65 536 блоков по 8 байт каждый.
- Машинные эталоны CAN ID в `examples/test-vectors.json`.

View File

@@ -28,15 +28,15 @@
1. `PROTOCOL.md` — структура ProtoCAN и реестр типов сообщений.
2. `BOOTLOADER.md` — загрузочный сервис `0x9…0xD`.
3. XLSX — адреса и свойства регистров ОАП.
4. Сгенерированный HTML — только представление, не самостоятельный источник.
4. Сгенерированный [`../index.html`](../index.html) — только представление, не самостоятельный источник.
## Сборка HTML
В PowerShell 7:
Из корня проекта:
```powershell
./build-html.ps1
./doc/build-html.bat
```
Результат создаётся в `build/protocol.html`. Скрипт не изменяет исходные
Markdown/XLSX и пригоден для запуска в CI.
Все документы и тестовые векторы включаются в единый файл `doc/index.html`.
Скрипт не изменяет исходные Markdown/XLSX и пригоден для запуска в CI.

View File

@@ -1,57 +0,0 @@
[CmdletBinding()]
param(
[string]$OutputPath = (Join-Path $PSScriptRoot 'build\protocol.html')
)
$ErrorActionPreference = 'Stop'
$documents = @(
'README.md',
'PROTOCOL.md',
'BOOTLOADER.md',
'OAP.md',
'CHANGELOG.md'
)
$sections = foreach ($document in $documents) {
$path = Join-Path $PSScriptRoot $document
$markdown = Get-Content -Raw -LiteralPath $path
(ConvertFrom-Markdown -InputObject $markdown).Html
}
$style = @'
body { max-width: 1180px; margin: 32px auto; padding: 0 24px;
color: #111827; background: #fff; font: 15px/1.55 Arial, sans-serif; }
h1, h2, h3 { line-height: 1.25; }
h1 { margin-top: 48px; border-bottom: 2px solid #334155; padding-bottom: 8px; }
h2 { margin-top: 32px; }
a { color: #1155cc; }
code { font-family: Consolas, "Courier New", monospace; }
pre { overflow: auto; padding: 12px; border: 1px solid #cbd5e1; background: #f8fafc; }
table { width: 100%; border-collapse: collapse; margin: 12px 0 24px; }
th, td { border: 1px solid #94a3b8; padding: 6px 9px; vertical-align: top; }
th { background: #e2e8f0; }
tr:nth-child(even) td { background: #f8fafc; }
'@
$outputDirectory = Split-Path -Parent $OutputPath
if (-not (Test-Path -LiteralPath $outputDirectory)) {
New-Item -ItemType Directory -Path $outputDirectory | Out-Null
}
$html = @"
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ProtoCAN — спецификация</title>
<style>$style</style>
</head>
<body>
$($sections -join "`n<hr>`n")
</body>
</html>
"@
Set-Content -LiteralPath $OutputPath -Value $html -Encoding utf8
Write-Host "Создан $OutputPath"

View File

@@ -1,833 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ProtoCAN — спецификация</title>
<style>body { max-width: 1180px; margin: 32px auto; padding: 0 24px;
color: #111827; background: #fff; font: 15px/1.55 Arial, sans-serif; }
h1, h2, h3 { line-height: 1.25; }
h1 { margin-top: 48px; border-bottom: 2px solid #334155; padding-bottom: 8px; }
h2 { margin-top: 32px; }
a { color: #1155cc; }
code { font-family: Consolas, "Courier New", monospace; }
pre { overflow: auto; padding: 12px; border: 1px solid #cbd5e1; background: #f8fafc; }
table { width: 100%; border-collapse: collapse; margin: 12px 0 24px; }
th, td { border: 1px solid #94a3b8; padding: 6px 9px; vertical-align: top; }
th { background: #e2e8f0; }
tr:nth-child(even) td { background: #f8fafc; }</style>
</head>
<body>
<h1 id="protocan">Документация ProtoCAN</h1>
<p>Статус комплекта: <strong>Draft</strong><br />
Версия комплекта: <strong>1.0</strong><br />
Дата редакции: <strong>2026-08-29</strong><br />
Транспорт: <strong>Classic CAN 2.0B, Extended ID, DLC 0…8</strong></p>
<p>Этот каталог разделяет нормативное описание протокола, загрузчик и реестр
общего адресного пространства. Большой исходный документ
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.md"><code>Протокол CAN и ОАП.md</code></a> сохранён как
совместимое представление таблиц из Excel.</p>
<h2 id="section">Документы</h2>
<table>
<thead>
<tr>
<th>Документ</th>
<th>Назначение</th>
<th>Статус источника</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="PROTOCOL.md">PROTOCOL.md</a></td>
<td>29-битный CAN ID, адресация, реестр <code>MsgType</code>, порядок байтов</td>
<td>нормативный</td>
</tr>
<tr>
<td><a href="BOOTLOADER.md">BOOTLOADER.md</a></td>
<td>обновление прошивки, кадры, состояния, ошибки и A/B-слоты</td>
<td>нормативный draft</td>
</tr>
<tr>
<td><a href="OAP.md">OAP.md</a></td>
<td>правила ведения общего адресного пространства</td>
<td>нормативный индекс</td>
</tr>
<tr>
<td><a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.xlsx">../../Протокол CAN и ОАП.xlsx</a></td>
<td>редактируемый реестр ОАП</td>
<td>источник таблиц</td>
</tr>
<tr>
<td><a href="examples/test-vectors.json">examples/test-vectors.json</a></td>
<td>машинные эталоны CAN ID и payload</td>
<td>нормативные примеры</td>
</tr>
<tr>
<td><a href="CHANGELOG.md">CHANGELOG.md</a></td>
<td>история версий документа</td>
<td>нормативный</td>
</tr>
</tbody>
</table>
<h2 id="section-1">Приоритет источников</h2>
<p>При расхождении данных действует следующий порядок:</p>
<ol>
<li><code>PROTOCOL.md</code> — структура ProtoCAN и реестр типов сообщений.</li>
<li><code>BOOTLOADER.md</code> — загрузочный сервис <code>0x9…0xD</code>.</li>
<li>XLSX — адреса и свойства регистров ОАП.</li>
<li>Сгенерированный HTML — только представление, не самостоятельный источник.</li>
</ol>
<h2 id="html">Сборка HTML</h2>
<p>В PowerShell 7:</p>
<pre><code class="language-powershell">./build-html.ps1
</code></pre>
<p>Результат создаётся в <code>build/protocol.html</code>. Скрипт не изменяет исходные
Markdown/XLSX и пригоден для запуска в CI.</p>
<hr>
<h1 id="protocan">ProtoCAN — базовый протокол</h1>
<p>Статус: <strong>Stable с зарезервированным загрузочным расширением</strong><br />
Версия: <strong>1.0</strong><br />
Порядок байтов payload: <strong>little-endian</strong>, если явно не указано иное</p>
<h2 id="section">Назначение</h2>
<p>ProtoCAN — прикладной протокол поверх classic CAN 2.0B. Используются только
расширенные 29-битные идентификаторы (<code>IDE=1</code>) и payload длиной 0…8 байт.</p>
<h2 id="section-1">Термины</h2>
<table>
<thead>
<tr>
<th>Термин</th>
<th>Значение</th>
</tr>
</thead>
<tbody>
<tr>
<td>ПМ</td>
<td>управляющий модуль</td>
</tr>
<tr>
<td>прибор</td>
<td>адресуемый узел на шине</td>
</tr>
<tr>
<td><code>DeviceType</code></td>
<td>тип прибора, 0…7</td>
</tr>
<tr>
<td><code>DeviceID</code></td>
<td>экземпляр прибора данного типа, 0…15</td>
</tr>
<tr>
<td><code>MsgType</code></td>
<td>класс сообщения или сервис</td>
</tr>
<tr>
<td><code>MsgBody</code></td>
<td>16-битное поле, формат которого зависит от <code>MsgType</code></td>
</tr>
</tbody>
</table>
<p>Пара <code>DeviceType/DeviceID</code> задаёт до <code>8 × 16 = 128</code> уникальных адресов.</p>
<h2 id="can-id">Расширенный CAN ID</h2>
<pre><code class="language-text">28 27 26...24 23...20 19...16 15........0
Priority Route DeviceType DeviceID MsgType MsgBody
1 бит 1 бит 3 бита 4 бита 4 бита 16 бит
</code></pre>
<pre><code class="language-c">can_id =
((uint32_t)priority &lt;&lt; 28) |
((uint32_t)route &lt;&lt; 27) |
((uint32_t)device_type &lt;&lt; 24) |
((uint32_t)device_id &lt;&lt; 20) |
((uint32_t)msg_type &lt;&lt; 16) |
msg_body;
</code></pre>
<table>
<thead>
<tr>
<th>Поле</th>
<th>Значения</th>
<th>Назначение</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Priority</code></td>
<td><code>0</code> critical, <code>1</code> standard</td>
<td>CAN-арбитраж</td>
</tr>
<tr>
<td><code>Route</code></td>
<td><code>0</code> от ПМ, <code>1</code> от прибора</td>
<td>логическое направление</td>
</tr>
<tr>
<td><code>DeviceType</code></td>
<td><code>0…7</code></td>
<td>тип прибора</td>
</tr>
<tr>
<td><code>DeviceID</code></td>
<td><code>0…15</code></td>
<td>номер экземпляра</td>
</tr>
<tr>
<td><code>MsgType</code></td>
<td><code>0…15</code></td>
<td>тип сообщения</td>
</tr>
<tr>
<td><code>MsgBody</code></td>
<td><code>0…65535</code></td>
<td>команда, адрес или номер блока</td>
</tr>
</tbody>
</table>
<p><code>Route</code> не является направлением физического трансивера. Ответ прибора
сохраняет адрес <code>DeviceType/DeviceID</code> и устанавливает <code>Route=1</code>.</p>
<h2 id="msgtype">Реестр <code>MsgType</code></h2>
<table>
<thead>
<tr>
<th style="text-align: right;">Код</th>
<th>Имя</th>
<th>Основное направление</th>
<th style="text-align: right;">DLC</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x0</code></td>
<td><code>BROADCAST</code></td>
<td>ПМ → все</td>
<td style="text-align: right;">зависит от команды</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x1</code></td>
<td><code>DISCRETE</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x2</code></td>
<td><code>ANALOG</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x3</code></td>
<td><code>GAS</code></td>
<td>оба</td>
<td style="text-align: right;">0/2/4/6/8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x4</code></td>
<td><code>MODBUS_COIL</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x5</code></td>
<td><code>MODBUS_DISCRETE</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x6</code></td>
<td><code>MODBUS_HOLDING</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x7</code></td>
<td><code>MODBUS_INPUT</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x8</code></td>
<td><code>ERROR</code></td>
<td>прибор → ПМ</td>
<td style="text-align: right;">0</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x9</code></td>
<td><code>BOOT_CONTROL</code></td>
<td>ПМ → прибор</td>
<td style="text-align: right;">0/8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xA</code></td>
<td><code>BOOT_DATA_A</code></td>
<td>ПМ → прибор</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xB</code></td>
<td><code>BOOT_DATA_B</code></td>
<td>ПМ → прибор</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xC</code></td>
<td><code>BOOT_STATUS</code></td>
<td>прибор → ПМ</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xD</code></td>
<td><code>BOOT_DISCOVERY</code></td>
<td>прибор → ПМ</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xE</code></td>
<td><code>SETTINGS</code></td>
<td>оба</td>
<td style="text-align: right;">0/1/8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xF</code></td>
<td><code>PULSE</code></td>
<td>прибор → сеть</td>
<td style="text-align: right;">1</td>
<td>stable</td>
</tr>
</tbody>
</table>
<p>Подробный формат <code>0x9…0xD</code> находится в <a href="BOOTLOADER.md">BOOTLOADER.md</a>.</p>
<h2 id="msgbody">Разметки <code>MsgBody</code></h2>
<table>
<thead>
<tr>
<th><code>MsgType</code></th>
<th>Биты <code>MsgBody</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>broadcast</td>
<td>команда <code>[15:4]</code>, параметр <code>[3:0]</code></td>
</tr>
<tr>
<td>discrete/analog</td>
<td>подтип <code>[15:12]</code>, значение/адрес <code>[11:0]</code></td>
</tr>
<tr>
<td>Modbus</td>
<td>начальный адрес <code>[15:4]</code>, количество <code>[3:0]</code></td>
</tr>
<tr>
<td>GAS</td>
<td>адрес первого 16-битного регистра <code>[15:0]</code></td>
</tr>
<tr>
<td>error</td>
<td>дополнительная информация <code>[15:8]</code>, код <code>[7:0]</code></td>
</tr>
<tr>
<td>settings</td>
<td>номер сборки <code>[15:8]</code>, позиция <code>[7:0]</code></td>
</tr>
<tr>
<td>boot control/status</td>
<td><code>SessionID[15:8]</code>, команда <code>[7:0]</code></td>
</tr>
<tr>
<td>boot data</td>
<td><code>BlockIndex[15:0]</code></td>
</tr>
</tbody>
</table>
<h2 id="section-2">Общие правила обмена</h2>
<ul>
<li>Многобайтовые значения в <code>DATA</code> передаются little-endian.</li>
<li>Узел игнорирует адресованные кадры с чужим <code>DeviceType/DeviceID</code>.</li>
<li>Прибор принимает команды ПМ с <code>Route=0</code>; ПМ принимает ответы с <code>Route=1</code>.</li>
<li>Стандартные 11-битные CAN ID не являются кадрами ProtoCAN.</li>
<li>RTR для загрузочного сервиса запрещён.</li>
<li>Неописанные комбинации <code>MsgType/MsgBody/DLC</code> должны отвергаться.</li>
</ul>
<h2 id="id">Эталон упаковки ID</h2>
<pre><code class="language-text">Priority = 1
Route = 0
DeviceType = 3
DeviceID = 5
MsgType = 0x9
MsgBody = 0x0702
CAN ID = 0x13590702
</code></pre>
<p>Этот пример соответствует <code>ENTER_BOOT</code>, <code>SessionID=7</code>. Машинные варианты
находятся в <a href="examples/test-vectors.json">examples/test-vectors.json</a>.</p>
<hr>
<h1 id="protocan-boot-protocol">ProtoCAN Boot Protocol</h1>
<p>Статус: <strong>Draft</strong><br />
Версия протокола: <strong>1.0</strong><br />
Совместимость: <strong>classic CAN 2.0B, Extended ID, DLC 0…8</strong><br />
Реализация: <a href="../../../templates/c/protocan-boot"><code>templates/c/protocan-boot</code></a></p>
<h2 id="section">Назначение</h2>
<p>Сервис обновляет адресованный прибор по CAN и поддерживает два логических
слота A/B. Активный слот не стирается: новый образ записывается в неактивный,
проверяется и атомарно назначается кандидатом на запуск.</p>
<h2 id="section-1">Карта сообщений</h2>
<table>
<thead>
<tr>
<th style="text-align: right;"><code>MsgType</code></th>
<th>Имя</th>
<th><code>MsgBody</code></th>
<th>Payload</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x9</code></td>
<td><code>BOOT_CONTROL</code></td>
<td><code>SessionID[15:8] \| Command[7:0]</code></td>
<td>параметры команды</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xA</code></td>
<td><code>BOOT_DATA_A</code></td>
<td><code>BlockIndex[15:0]</code></td>
<td>8 байт слота A</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xB</code></td>
<td><code>BOOT_DATA_B</code></td>
<td><code>BlockIndex[15:0]</code></td>
<td>8 байт слота B</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xC</code></td>
<td><code>BOOT_STATUS</code></td>
<td><code>SessionID[15:8] \| Command[7:0]</code></td>
<td>статус и прогресс</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xD</code></td>
<td><code>BOOT_DISCOVERY</code></td>
<td>подтип ответа</td>
<td>идентификация</td>
</tr>
</tbody>
</table>
<p>Все команды записи адресуются конкретному <code>DeviceType/DeviceID</code> и имеют
<code>Route=0</code>. Ответы сохраняют адрес прибора и имеют <code>Route=1</code>.</p>
<h2 id="section-2">Адресация образа</h2>
<p><code>MsgBody</code> кадра данных — номер 8-байтового блока:</p>
<pre><code class="language-c">offset = (uint32_t)BlockIndex * 8U;
address = SLOT_X_BASE + offset;
</code></pre>
<pre><code class="language-text">512 КиБ = 524 288 байт
524 288 / 8 = 65 536 блоков
BlockIndex = 0x0000…0xFFFF
</code></pre>
<table>
<thead>
<tr>
<th style="text-align: right;"><code>BlockIndex</code></th>
<th style="text-align: right;">Смещение</th>
<th style="text-align: right;">Диапазон байтов</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x0000</code></td>
<td style="text-align: right;"><code>0x00000</code></td>
<td style="text-align: right;"><code>0x00000…0x00007</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0001</code></td>
<td style="text-align: right;"><code>0x00008</code></td>
<td style="text-align: right;"><code>0x00008…0x0000F</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0xFFFF</code></td>
<td style="text-align: right;"><code>0x7FFF8</code></td>
<td style="text-align: right;"><code>0x7FFF8…0x7FFFF</code></td>
</tr>
</tbody>
</table>
<p><code>0x80000</code> является первой позицией за границей слота. Последний кадр
дополняется <code>0xFF</code>, но CRC32 вычисляется только по <code>ImageSize</code> байтам.</p>
<h2 id="boot_control">Команды <code>BOOT_CONTROL</code></h2>
<table>
<thead>
<tr>
<th style="text-align: right;">Код</th>
<th>Команда</th>
<th style="text-align: right;">DLC</th>
<th>Payload</th>
<th>Допустимое состояние</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x01</code></td>
<td><code>IDENTIFY</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>любое</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x02</code></td>
<td><code>ENTER_BOOT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>любое; <code>SessionID != 0</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0x03</code></td>
<td><code>BEGIN_IMAGE</code></td>
<td style="text-align: right;">8</td>
<td>размер и CRC32</td>
<td>metadata</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x04</code></td>
<td><code>BEGIN_COMPAT</code></td>
<td style="text-align: right;">8</td>
<td>совместимость и версия</td>
<td>metadata</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x05</code></td>
<td><code>ERASE</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>ready-to-erase</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x06</code></td>
<td><code>VERIFY</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>образ получен</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x07</code></td>
<td><code>COMMIT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>verified</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x08</code></td>
<td><code>CONFIRM</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>запущенное приложение</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x09</code></td>
<td><code>REBOOT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>активная сессия</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0A</code></td>
<td><code>ABORT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>активная сессия</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0B</code></td>
<td><code>QUERY_PROGRESS</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>активная сессия</td>
</tr>
</tbody>
</table>
<h3 id="begin_image"><code>BEGIN_IMAGE</code></h3>
<pre><code class="language-text">DATA[0..3] ImageSize, uint32 little-endian
DATA[4..7] ImageCRC32, uint32 little-endian
</code></pre>
<h3 id="begin_compat"><code>BEGIN_COMPAT</code></h3>
<pre><code class="language-text">DATA[0..1] ProductType, uint16 little-endian
DATA[2] HardwareRevisionMin
DATA[3] HardwareRevisionMax
DATA[4..7] FirmwareVersion, uint32 little-endian
</code></pre>
<p>До <code>ERASE</code> прибор обязан получить обе части метаданных и проверить размер,
тип изделия, аппаратную ревизию, версию и политику anti-rollback.</p>
<h2 id="boot_status"><code>BOOT_STATUS</code></h2>
<pre><code class="language-text">MsgBody[15..8] SessionID
MsgBody[7..0] команда, на которую дан ответ
DATA[0] Status
DATA[1] TargetSlot: 0=A, 1=B, 0xFF=не выбран
DATA[2..3] NextBlock, uint16 little-endian
DATA[4..7] RunningCRC32, uint32 little-endian
</code></pre>
<table>
<thead>
<tr>
<th style="text-align: right;">Код</th>
<th>Статус</th>
<th>Повтор допустим</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x00</code></td>
<td><code>OK</code></td>
<td>—</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x01</code></td>
<td><code>BUSY</code></td>
<td>да, после задержки</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x02</code></td>
<td><code>INVALID_COMMAND</code></td>
<td>после исправления</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x03</code></td>
<td><code>WRONG_DEVICE</code></td>
<td>нет для этого образа</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x04</code></td>
<td><code>WRONG_HARDWARE</code></td>
<td>нет для этого образа</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x05</code></td>
<td><code>INVALID_SIZE</code></td>
<td>нет для этого образа</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x06</code></td>
<td><code>CRC_ERROR</code></td>
<td>новая передача</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x07</code></td>
<td><code>FLASH_ERROR</code></td>
<td>зависит от платформы</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x08</code></td>
<td><code>SEQUENCE_ERROR</code></td>
<td>да, с <code>NextBlock</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0x09</code></td>
<td><code>SIGNATURE_ERROR</code></td>
<td>нет</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0A</code></td>
<td><code>SESSION_ERROR</code></td>
<td>открыть новую сессию</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0B</code></td>
<td><code>VOLTAGE_ERROR</code></td>
<td>да после нормализации питания</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0C</code></td>
<td><code>INVALID_STATE</code></td>
<td>выполнить правильный переход</td>
</tr>
</tbody>
</table>
<h2 id="state-machine">State machine</h2>
<pre><code class="language-text">IDLE
└─ ENTER_BOOT ─&gt; METADATA
├─ BEGIN_IMAGE
└─ BEGIN_COMPAT
v
READY_TO_ERASE
│ ERASE
v
RECEIVING
│ VERIFY
v
VERIFIED
│ COMMIT
v
PENDING + REBOOT
│ CONFIRM
v
CONFIRMED
</code></pre>
<p>Ошибка Flash, CRC, совместимости или подписи переводит сессию в <code>FAILED</code>.
Новая <code>ENTER_BOOT</code> создаёт чистую сессию. <code>ABORT</code> прекращает текущую передачу,
не активируя частично записанный слот.</p>
<h2 id="section-3">Надёжность и повторы</h2>
<ul>
<li>Блоки передаются строго по возрастанию <code>BlockIndex</code>.</li>
<li>Дубликат или пропуск возвращает <code>SEQUENCE_ERROR</code> и ожидаемый <code>NextBlock</code>.</li>
<li>Базовый режим подтверждает каждый блок.</li>
<li>Рабочий режим может подтверждать окно из 16 блоков.</li>
<li>После потери связи <code>QUERY_PROGRESS</code> возвращает следующий ожидаемый блок,
пока состояние загрузчика сохранено.</li>
<li>Для продолжения после перезагрузки порт должен сохранять session metadata
и восстановить её при инициализации; ядро версии 1.0 само это не делает.</li>
</ul>
<h2 id="ab">Безопасность и A/B-обновление</h2>
<pre><code class="language-text">active=A -&gt; target=B -&gt; verify -&gt; pending=B
active=B -&gt; target=A -&gt; verify -&gt; pending=A
</code></pre>
<p>CRC32 защищает только от случайного повреждения. Серийный загрузчик должен
дополнительно проверить подпись контейнера, границы вектора, совместимость и
anti-rollback. Bootloader не обновляется командами <code>BOOT_DATA_A/B</code>.</p>
<p>Boot metadata должна атомарно хранить:</p>
<ul>
<li>активный слот;</li>
<li>pending-слот;</li>
<li>подтверждение запуска;</li>
<li>число неудачных попыток;</li>
<li>версию и CRC32 образа.</li>
</ul>
<p>Если приложение не выполняет <code>CONFIRM</code> за установленное число запусков,
загрузчик возвращается к предыдущему подтверждённому слоту.</p>
<h2 id="section-4">Эталонный сценарий</h2>
<ol>
<li>ПМ адресно отправляет <code>IDENTIFY</code>.</li>
<li>ПМ открывает ненулевой <code>SessionID</code> командой <code>ENTER_BOOT</code>.</li>
<li>ПМ отправляет <code>BEGIN_IMAGE</code> и <code>BEGIN_COMPAT</code>.</li>
<li>Прибор сообщает выбранный неактивный слот.</li>
<li>ПМ выполняет <code>ERASE</code> и передаёт <code>BOOT_DATA_A</code> либо <code>BOOT_DATA_B</code>.</li>
<li>ПМ выполняет <code>VERIFY</code>, затем <code>COMMIT</code> и <code>REBOOT</code>.</li>
<li>Новое приложение после самопроверки выполняет <code>CONFIRM</code>.</li>
</ol>
<hr>
<h1 id="section">Общее адресное пространство</h1>
<p>Статус: <strong>Stable, данные ведутся в XLSX</strong><br />
Порядок значений: <strong>16-битные регистры, little-endian в CAN payload</strong></p>
<p>Редактируемый источник реестра:
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.xlsx"><code>Протокол CAN и ОАП.xlsx</code></a>.</p>
<p>Просматриваемая большая таблица находится в
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.html"><code>Протокол CAN и ОАП.html</code></a> и
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.md"><code>Протокол CAN и ОАП.md</code></a>.</p>
<h2 id="section-1">Назначение</h2>
<p>ОАП связывает 16-битный адрес регистра с его типом, назначением, доступом и
масштабом. В ProtoCAN используется <code>MsgType=0x3</code>, а <code>MsgBody</code> содержит адрес
первого регистра.</p>
<h2 id="section-2">Обязательные поля реестра</h2>
<table>
<thead>
<tr>
<th>Поле</th>
<th>Требование</th>
</tr>
</thead>
<tbody>
<tr>
<td>AddressHex</td>
<td><code>0x0000…0xFFFF</code>, уникальное значение</td>
</tr>
<tr>
<td>AddressDec</td>
<td>десятичный эквивалент AddressHex</td>
</tr>
<tr>
<td>Group</td>
<td>функциональная группа</td>
</tr>
<tr>
<td>Name</td>
<td>однозначное имя параметра</td>
</tr>
<tr>
<td>Type</td>
<td><code>u16</code>, <code>i16</code>, <code>u32</code>, <code>i32</code>, <code>float32</code>, bitmap или массив</td>
</tr>
<tr>
<td>Registers</td>
<td>число занятых 16-битных регистров</td>
</tr>
<tr>
<td>Access</td>
<td><code>R</code>, <code>W</code> или <code>RW</code></td>
</tr>
<tr>
<td>Unit</td>
<td>физическая единица либо <code>—</code></td>
</tr>
<tr>
<td>Scale</td>
<td>множитель/делитель представления</td>
</tr>
<tr>
<td>Default</td>
<td>значение после сброса, если применимо</td>
</tr>
<tr>
<td>Description</td>
<td>семантика, диапазон и особые значения</td>
</tr>
</tbody>
</table>
<h2 id="section-3">Правила ведения</h2>
<ul>
<li>Адрес не переиспользуется с другим смыслом после выпуска стабильной версии.</li>
<li>Многорегистровое значение занимает непрерывный диапазон.</li>
<li>Порядок 16-битных слов для 32-битного значения фиксируется в строке типа.</li>
<li>Резервные диапазоны явно отмечаются и не используются без изменения версии.</li>
<li>Удалённый параметр помечается deprecated, а не исчезает молча.</li>
<li>Изменение адреса, типа или масштаба отражается в <code>CHANGELOG.md</code>.</li>
</ul>
<h2 id="section-4">Экспорт</h2>
<p>Для программной генерации каталог следует экспортировать из XLSX в CSV с
UTF-8 и фиксированными английскими именами колонок. CSV должен проверяться на:</p>
<ul>
<li>уникальность адресов;</li>
<li>пересечение многорегистровых значений;</li>
<li>допустимые типы и права доступа;</li>
<li>равенство шестнадцатеричного и десятичного адреса;</li>
<li>попадание адреса в диапазон <code>0x0000…0xFFFF</code>.</li>
</ul>
<p>До появления автоматического экспортёра нормативным источником адресов
остаётся XLSX, а HTML/Markdown считаются представлением.</p>
<hr>
<h1 id="protocan">История изменений ProtoCAN</h1>
<p>Формат основан на Keep a Changelog. Версия относится к спецификации, а не к
версии прошивки отдельного прибора.</p>
<h2 id="unreleased">[Unreleased]</h2>
<h3 id="added">Added</h3>
<ul>
<li>Структурированный комплект документации <code>docs/protocan</code>.</li>
<li>Загрузочный сервис <code>MsgType=0x9…0xD</code>.</li>
<li>Два логических слота по 512 КиБ, 65 536 блоков по 8 байт каждый.</li>
<li>Машинные эталоны CAN ID в <code>examples/test-vectors.json</code>.</li>
</ul>
<h2 id="section">[1.0] — 2026-08-29</h2>
<h3 id="added-1">Added</h3>
<ul>
<li>Зафиксирована 29-битная структура ProtoCAN ID.</li>
<li>Зафиксирована адресация 8 типов по 16 экземпляров.</li>
<li>Существующие сообщения <code>0x0…0x8</code>, <code>0xE</code>, <code>0xF</code> сохранены.</li>
</ul>
</body>
</html>

View File

@@ -1,833 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ProtoCAN — спецификация</title>
<style>body { max-width: 1180px; margin: 32px auto; padding: 0 24px;
color: #111827; background: #fff; font: 15px/1.55 Arial, sans-serif; }
h1, h2, h3 { line-height: 1.25; }
h1 { margin-top: 48px; border-bottom: 2px solid #334155; padding-bottom: 8px; }
h2 { margin-top: 32px; }
a { color: #1155cc; }
code { font-family: Consolas, "Courier New", monospace; }
pre { overflow: auto; padding: 12px; border: 1px solid #cbd5e1; background: #f8fafc; }
table { width: 100%; border-collapse: collapse; margin: 12px 0 24px; }
th, td { border: 1px solid #94a3b8; padding: 6px 9px; vertical-align: top; }
th { background: #e2e8f0; }
tr:nth-child(even) td { background: #f8fafc; }</style>
</head>
<body>
<h1 id="protocan">Документация ProtoCAN</h1>
<p>Статус комплекта: <strong>Draft</strong><br />
Версия комплекта: <strong>1.0</strong><br />
Дата редакции: <strong>2026-08-29</strong><br />
Транспорт: <strong>Classic CAN 2.0B, Extended ID, DLC 0…8</strong></p>
<p>Этот каталог разделяет нормативное описание протокола, загрузчик и реестр
общего адресного пространства. Большой исходный документ
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.md"><code>Протокол CAN и ОАП.md</code></a> сохранён как
совместимое представление таблиц из Excel.</p>
<h2 id="section">Документы</h2>
<table>
<thead>
<tr>
<th>Документ</th>
<th>Назначение</th>
<th>Статус источника</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="PROTOCOL.md">PROTOCOL.md</a></td>
<td>29-битный CAN ID, адресация, реестр <code>MsgType</code>, порядок байтов</td>
<td>нормативный</td>
</tr>
<tr>
<td><a href="BOOTLOADER.md">BOOTLOADER.md</a></td>
<td>обновление прошивки, кадры, состояния, ошибки и A/B-слоты</td>
<td>нормативный draft</td>
</tr>
<tr>
<td><a href="OAP.md">OAP.md</a></td>
<td>правила ведения общего адресного пространства</td>
<td>нормативный индекс</td>
</tr>
<tr>
<td><a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.xlsx">../../Протокол CAN и ОАП.xlsx</a></td>
<td>редактируемый реестр ОАП</td>
<td>источник таблиц</td>
</tr>
<tr>
<td><a href="examples/test-vectors.json">examples/test-vectors.json</a></td>
<td>машинные эталоны CAN ID и payload</td>
<td>нормативные примеры</td>
</tr>
<tr>
<td><a href="CHANGELOG.md">CHANGELOG.md</a></td>
<td>история версий документа</td>
<td>нормативный</td>
</tr>
</tbody>
</table>
<h2 id="section-1">Приоритет источников</h2>
<p>При расхождении данных действует следующий порядок:</p>
<ol>
<li><code>PROTOCOL.md</code> — структура ProtoCAN и реестр типов сообщений.</li>
<li><code>BOOTLOADER.md</code> — загрузочный сервис <code>0x9…0xD</code>.</li>
<li>XLSX — адреса и свойства регистров ОАП.</li>
<li>Сгенерированный HTML — только представление, не самостоятельный источник.</li>
</ol>
<h2 id="html">Сборка HTML</h2>
<p>В PowerShell 7:</p>
<pre><code class="language-powershell">./build-html.ps1
</code></pre>
<p>Результат создаётся в <code>build/protocol.html</code>. Скрипт не изменяет исходные
Markdown/XLSX и пригоден для запуска в CI.</p>
<hr>
<h1 id="protocan">ProtoCAN — базовый протокол</h1>
<p>Статус: <strong>Stable с зарезервированным загрузочным расширением</strong><br />
Версия: <strong>1.0</strong><br />
Порядок байтов payload: <strong>little-endian</strong>, если явно не указано иное</p>
<h2 id="section">Назначение</h2>
<p>ProtoCAN — прикладной протокол поверх classic CAN 2.0B. Используются только
расширенные 29-битные идентификаторы (<code>IDE=1</code>) и payload длиной 0…8 байт.</p>
<h2 id="section-1">Термины</h2>
<table>
<thead>
<tr>
<th>Термин</th>
<th>Значение</th>
</tr>
</thead>
<tbody>
<tr>
<td>ПМ</td>
<td>управляющий модуль</td>
</tr>
<tr>
<td>прибор</td>
<td>адресуемый узел на шине</td>
</tr>
<tr>
<td><code>DeviceType</code></td>
<td>тип прибора, 0…7</td>
</tr>
<tr>
<td><code>DeviceID</code></td>
<td>экземпляр прибора данного типа, 0…15</td>
</tr>
<tr>
<td><code>MsgType</code></td>
<td>класс сообщения или сервис</td>
</tr>
<tr>
<td><code>MsgBody</code></td>
<td>16-битное поле, формат которого зависит от <code>MsgType</code></td>
</tr>
</tbody>
</table>
<p>Пара <code>DeviceType/DeviceID</code> задаёт до <code>8 × 16 = 128</code> уникальных адресов.</p>
<h2 id="can-id">Расширенный CAN ID</h2>
<pre><code class="language-text">28 27 26...24 23...20 19...16 15........0
Priority Route DeviceType DeviceID MsgType MsgBody
1 бит 1 бит 3 бита 4 бита 4 бита 16 бит
</code></pre>
<pre><code class="language-c">can_id =
((uint32_t)priority &lt;&lt; 28) |
((uint32_t)route &lt;&lt; 27) |
((uint32_t)device_type &lt;&lt; 24) |
((uint32_t)device_id &lt;&lt; 20) |
((uint32_t)msg_type &lt;&lt; 16) |
msg_body;
</code></pre>
<table>
<thead>
<tr>
<th>Поле</th>
<th>Значения</th>
<th>Назначение</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Priority</code></td>
<td><code>0</code> critical, <code>1</code> standard</td>
<td>CAN-арбитраж</td>
</tr>
<tr>
<td><code>Route</code></td>
<td><code>0</code> от ПМ, <code>1</code> от прибора</td>
<td>логическое направление</td>
</tr>
<tr>
<td><code>DeviceType</code></td>
<td><code>0…7</code></td>
<td>тип прибора</td>
</tr>
<tr>
<td><code>DeviceID</code></td>
<td><code>0…15</code></td>
<td>номер экземпляра</td>
</tr>
<tr>
<td><code>MsgType</code></td>
<td><code>0…15</code></td>
<td>тип сообщения</td>
</tr>
<tr>
<td><code>MsgBody</code></td>
<td><code>0…65535</code></td>
<td>команда, адрес или номер блока</td>
</tr>
</tbody>
</table>
<p><code>Route</code> не является направлением физического трансивера. Ответ прибора
сохраняет адрес <code>DeviceType/DeviceID</code> и устанавливает <code>Route=1</code>.</p>
<h2 id="msgtype">Реестр <code>MsgType</code></h2>
<table>
<thead>
<tr>
<th style="text-align: right;">Код</th>
<th>Имя</th>
<th>Основное направление</th>
<th style="text-align: right;">DLC</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x0</code></td>
<td><code>BROADCAST</code></td>
<td>ПМ → все</td>
<td style="text-align: right;">зависит от команды</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x1</code></td>
<td><code>DISCRETE</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x2</code></td>
<td><code>ANALOG</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x3</code></td>
<td><code>GAS</code></td>
<td>оба</td>
<td style="text-align: right;">0/2/4/6/8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x4</code></td>
<td><code>MODBUS_COIL</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x5</code></td>
<td><code>MODBUS_DISCRETE</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x6</code></td>
<td><code>MODBUS_HOLDING</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x7</code></td>
<td><code>MODBUS_INPUT</code></td>
<td>оба</td>
<td style="text-align: right;">0…8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x8</code></td>
<td><code>ERROR</code></td>
<td>прибор → ПМ</td>
<td style="text-align: right;">0</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x9</code></td>
<td><code>BOOT_CONTROL</code></td>
<td>ПМ → прибор</td>
<td style="text-align: right;">0/8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xA</code></td>
<td><code>BOOT_DATA_A</code></td>
<td>ПМ → прибор</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xB</code></td>
<td><code>BOOT_DATA_B</code></td>
<td>ПМ → прибор</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xC</code></td>
<td><code>BOOT_STATUS</code></td>
<td>прибор → ПМ</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xD</code></td>
<td><code>BOOT_DISCOVERY</code></td>
<td>прибор → ПМ</td>
<td style="text-align: right;">8</td>
<td>draft</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xE</code></td>
<td><code>SETTINGS</code></td>
<td>оба</td>
<td style="text-align: right;">0/1/8</td>
<td>stable</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xF</code></td>
<td><code>PULSE</code></td>
<td>прибор → сеть</td>
<td style="text-align: right;">1</td>
<td>stable</td>
</tr>
</tbody>
</table>
<p>Подробный формат <code>0x9…0xD</code> находится в <a href="BOOTLOADER.md">BOOTLOADER.md</a>.</p>
<h2 id="msgbody">Разметки <code>MsgBody</code></h2>
<table>
<thead>
<tr>
<th><code>MsgType</code></th>
<th>Биты <code>MsgBody</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>broadcast</td>
<td>команда <code>[15:4]</code>, параметр <code>[3:0]</code></td>
</tr>
<tr>
<td>discrete/analog</td>
<td>подтип <code>[15:12]</code>, значение/адрес <code>[11:0]</code></td>
</tr>
<tr>
<td>Modbus</td>
<td>начальный адрес <code>[15:4]</code>, количество <code>[3:0]</code></td>
</tr>
<tr>
<td>GAS</td>
<td>адрес первого 16-битного регистра <code>[15:0]</code></td>
</tr>
<tr>
<td>error</td>
<td>дополнительная информация <code>[15:8]</code>, код <code>[7:0]</code></td>
</tr>
<tr>
<td>settings</td>
<td>номер сборки <code>[15:8]</code>, позиция <code>[7:0]</code></td>
</tr>
<tr>
<td>boot control/status</td>
<td><code>SessionID[15:8]</code>, команда <code>[7:0]</code></td>
</tr>
<tr>
<td>boot data</td>
<td><code>BlockIndex[15:0]</code></td>
</tr>
</tbody>
</table>
<h2 id="section-2">Общие правила обмена</h2>
<ul>
<li>Многобайтовые значения в <code>DATA</code> передаются little-endian.</li>
<li>Узел игнорирует адресованные кадры с чужим <code>DeviceType/DeviceID</code>.</li>
<li>Прибор принимает команды ПМ с <code>Route=0</code>; ПМ принимает ответы с <code>Route=1</code>.</li>
<li>Стандартные 11-битные CAN ID не являются кадрами ProtoCAN.</li>
<li>RTR для загрузочного сервиса запрещён.</li>
<li>Неописанные комбинации <code>MsgType/MsgBody/DLC</code> должны отвергаться.</li>
</ul>
<h2 id="id">Эталон упаковки ID</h2>
<pre><code class="language-text">Priority = 1
Route = 0
DeviceType = 3
DeviceID = 5
MsgType = 0x9
MsgBody = 0x0702
CAN ID = 0x13590702
</code></pre>
<p>Этот пример соответствует <code>ENTER_BOOT</code>, <code>SessionID=7</code>. Машинные варианты
находятся в <a href="examples/test-vectors.json">examples/test-vectors.json</a>.</p>
<hr>
<h1 id="protocan-boot-protocol">ProtoCAN Boot Protocol</h1>
<p>Статус: <strong>Draft</strong><br />
Версия протокола: <strong>1.0</strong><br />
Совместимость: <strong>classic CAN 2.0B, Extended ID, DLC 0…8</strong><br />
Реализация: <a href="../../../templates/c/protocan-boot"><code>templates/c/protocan-boot</code></a></p>
<h2 id="section">Назначение</h2>
<p>Сервис обновляет адресованный прибор по CAN и поддерживает два логических
слота A/B. Активный слот не стирается: новый образ записывается в неактивный,
проверяется и атомарно назначается кандидатом на запуск.</p>
<h2 id="section-1">Карта сообщений</h2>
<table>
<thead>
<tr>
<th style="text-align: right;"><code>MsgType</code></th>
<th>Имя</th>
<th><code>MsgBody</code></th>
<th>Payload</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x9</code></td>
<td><code>BOOT_CONTROL</code></td>
<td><code>SessionID[15:8] \| Command[7:0]</code></td>
<td>параметры команды</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xA</code></td>
<td><code>BOOT_DATA_A</code></td>
<td><code>BlockIndex[15:0]</code></td>
<td>8 байт слота A</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xB</code></td>
<td><code>BOOT_DATA_B</code></td>
<td><code>BlockIndex[15:0]</code></td>
<td>8 байт слота B</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xC</code></td>
<td><code>BOOT_STATUS</code></td>
<td><code>SessionID[15:8] \| Command[7:0]</code></td>
<td>статус и прогресс</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xD</code></td>
<td><code>BOOT_DISCOVERY</code></td>
<td>подтип ответа</td>
<td>идентификация</td>
</tr>
</tbody>
</table>
<p>Все команды записи адресуются конкретному <code>DeviceType/DeviceID</code> и имеют
<code>Route=0</code>. Ответы сохраняют адрес прибора и имеют <code>Route=1</code>.</p>
<h2 id="section-2">Адресация образа</h2>
<p><code>MsgBody</code> кадра данных — номер 8-байтового блока:</p>
<pre><code class="language-c">offset = (uint32_t)BlockIndex * 8U;
address = SLOT_X_BASE + offset;
</code></pre>
<pre><code class="language-text">512 КиБ = 524 288 байт
524 288 / 8 = 65 536 блоков
BlockIndex = 0x0000…0xFFFF
</code></pre>
<table>
<thead>
<tr>
<th style="text-align: right;"><code>BlockIndex</code></th>
<th style="text-align: right;">Смещение</th>
<th style="text-align: right;">Диапазон байтов</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x0000</code></td>
<td style="text-align: right;"><code>0x00000</code></td>
<td style="text-align: right;"><code>0x00000…0x00007</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0001</code></td>
<td style="text-align: right;"><code>0x00008</code></td>
<td style="text-align: right;"><code>0x00008…0x0000F</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0xFFFF</code></td>
<td style="text-align: right;"><code>0x7FFF8</code></td>
<td style="text-align: right;"><code>0x7FFF8…0x7FFFF</code></td>
</tr>
</tbody>
</table>
<p><code>0x80000</code> является первой позицией за границей слота. Последний кадр
дополняется <code>0xFF</code>, но CRC32 вычисляется только по <code>ImageSize</code> байтам.</p>
<h2 id="boot_control">Команды <code>BOOT_CONTROL</code></h2>
<table>
<thead>
<tr>
<th style="text-align: right;">Код</th>
<th>Команда</th>
<th style="text-align: right;">DLC</th>
<th>Payload</th>
<th>Допустимое состояние</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x01</code></td>
<td><code>IDENTIFY</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>любое</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x02</code></td>
<td><code>ENTER_BOOT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>любое; <code>SessionID != 0</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0x03</code></td>
<td><code>BEGIN_IMAGE</code></td>
<td style="text-align: right;">8</td>
<td>размер и CRC32</td>
<td>metadata</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x04</code></td>
<td><code>BEGIN_COMPAT</code></td>
<td style="text-align: right;">8</td>
<td>совместимость и версия</td>
<td>metadata</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x05</code></td>
<td><code>ERASE</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>ready-to-erase</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x06</code></td>
<td><code>VERIFY</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>образ получен</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x07</code></td>
<td><code>COMMIT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>verified</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x08</code></td>
<td><code>CONFIRM</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>запущенное приложение</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x09</code></td>
<td><code>REBOOT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>активная сессия</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0A</code></td>
<td><code>ABORT</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>активная сессия</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0B</code></td>
<td><code>QUERY_PROGRESS</code></td>
<td style="text-align: right;">0</td>
<td>отсутствует</td>
<td>активная сессия</td>
</tr>
</tbody>
</table>
<h3 id="begin_image"><code>BEGIN_IMAGE</code></h3>
<pre><code class="language-text">DATA[0..3] ImageSize, uint32 little-endian
DATA[4..7] ImageCRC32, uint32 little-endian
</code></pre>
<h3 id="begin_compat"><code>BEGIN_COMPAT</code></h3>
<pre><code class="language-text">DATA[0..1] ProductType, uint16 little-endian
DATA[2] HardwareRevisionMin
DATA[3] HardwareRevisionMax
DATA[4..7] FirmwareVersion, uint32 little-endian
</code></pre>
<p>До <code>ERASE</code> прибор обязан получить обе части метаданных и проверить размер,
тип изделия, аппаратную ревизию, версию и политику anti-rollback.</p>
<h2 id="boot_status"><code>BOOT_STATUS</code></h2>
<pre><code class="language-text">MsgBody[15..8] SessionID
MsgBody[7..0] команда, на которую дан ответ
DATA[0] Status
DATA[1] TargetSlot: 0=A, 1=B, 0xFF=не выбран
DATA[2..3] NextBlock, uint16 little-endian
DATA[4..7] RunningCRC32, uint32 little-endian
</code></pre>
<table>
<thead>
<tr>
<th style="text-align: right;">Код</th>
<th>Статус</th>
<th>Повтор допустим</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x00</code></td>
<td><code>OK</code></td>
<td>—</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x01</code></td>
<td><code>BUSY</code></td>
<td>да, после задержки</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x02</code></td>
<td><code>INVALID_COMMAND</code></td>
<td>после исправления</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x03</code></td>
<td><code>WRONG_DEVICE</code></td>
<td>нет для этого образа</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x04</code></td>
<td><code>WRONG_HARDWARE</code></td>
<td>нет для этого образа</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x05</code></td>
<td><code>INVALID_SIZE</code></td>
<td>нет для этого образа</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x06</code></td>
<td><code>CRC_ERROR</code></td>
<td>новая передача</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x07</code></td>
<td><code>FLASH_ERROR</code></td>
<td>зависит от платформы</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x08</code></td>
<td><code>SEQUENCE_ERROR</code></td>
<td>да, с <code>NextBlock</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0x09</code></td>
<td><code>SIGNATURE_ERROR</code></td>
<td>нет</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0A</code></td>
<td><code>SESSION_ERROR</code></td>
<td>открыть новую сессию</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0B</code></td>
<td><code>VOLTAGE_ERROR</code></td>
<td>да после нормализации питания</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0C</code></td>
<td><code>INVALID_STATE</code></td>
<td>выполнить правильный переход</td>
</tr>
</tbody>
</table>
<h2 id="state-machine">State machine</h2>
<pre><code class="language-text">IDLE
└─ ENTER_BOOT ─&gt; METADATA
├─ BEGIN_IMAGE
└─ BEGIN_COMPAT
v
READY_TO_ERASE
│ ERASE
v
RECEIVING
│ VERIFY
v
VERIFIED
│ COMMIT
v
PENDING + REBOOT
│ CONFIRM
v
CONFIRMED
</code></pre>
<p>Ошибка Flash, CRC, совместимости или подписи переводит сессию в <code>FAILED</code>.
Новая <code>ENTER_BOOT</code> создаёт чистую сессию. <code>ABORT</code> прекращает текущую передачу,
не активируя частично записанный слот.</p>
<h2 id="section-3">Надёжность и повторы</h2>
<ul>
<li>Блоки передаются строго по возрастанию <code>BlockIndex</code>.</li>
<li>Дубликат или пропуск возвращает <code>SEQUENCE_ERROR</code> и ожидаемый <code>NextBlock</code>.</li>
<li>Базовый режим подтверждает каждый блок.</li>
<li>Рабочий режим может подтверждать окно из 16 блоков.</li>
<li>После потери связи <code>QUERY_PROGRESS</code> возвращает следующий ожидаемый блок,
пока состояние загрузчика сохранено.</li>
<li>Для продолжения после перезагрузки порт должен сохранять session metadata
и восстановить её при инициализации; ядро версии 1.0 само это не делает.</li>
</ul>
<h2 id="ab">Безопасность и A/B-обновление</h2>
<pre><code class="language-text">active=A -&gt; target=B -&gt; verify -&gt; pending=B
active=B -&gt; target=A -&gt; verify -&gt; pending=A
</code></pre>
<p>CRC32 защищает только от случайного повреждения. Серийный загрузчик должен
дополнительно проверить подпись контейнера, границы вектора, совместимость и
anti-rollback. Bootloader не обновляется командами <code>BOOT_DATA_A/B</code>.</p>
<p>Boot metadata должна атомарно хранить:</p>
<ul>
<li>активный слот;</li>
<li>pending-слот;</li>
<li>подтверждение запуска;</li>
<li>число неудачных попыток;</li>
<li>версию и CRC32 образа.</li>
</ul>
<p>Если приложение не выполняет <code>CONFIRM</code> за установленное число запусков,
загрузчик возвращается к предыдущему подтверждённому слоту.</p>
<h2 id="section-4">Эталонный сценарий</h2>
<ol>
<li>ПМ адресно отправляет <code>IDENTIFY</code>.</li>
<li>ПМ открывает ненулевой <code>SessionID</code> командой <code>ENTER_BOOT</code>.</li>
<li>ПМ отправляет <code>BEGIN_IMAGE</code> и <code>BEGIN_COMPAT</code>.</li>
<li>Прибор сообщает выбранный неактивный слот.</li>
<li>ПМ выполняет <code>ERASE</code> и передаёт <code>BOOT_DATA_A</code> либо <code>BOOT_DATA_B</code>.</li>
<li>ПМ выполняет <code>VERIFY</code>, затем <code>COMMIT</code> и <code>REBOOT</code>.</li>
<li>Новое приложение после самопроверки выполняет <code>CONFIRM</code>.</li>
</ol>
<hr>
<h1 id="section">Общее адресное пространство</h1>
<p>Статус: <strong>Stable, данные ведутся в XLSX</strong><br />
Порядок значений: <strong>16-битные регистры, little-endian в CAN payload</strong></p>
<p>Редактируемый источник реестра:
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.xlsx"><code>Протокол CAN и ОАП.xlsx</code></a>.</p>
<p>Просматриваемая большая таблица находится в
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.html"><code>Протокол CAN и ОАП.html</code></a> и
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.md"><code>Протокол CAN и ОАП.md</code></a>.</p>
<h2 id="section-1">Назначение</h2>
<p>ОАП связывает 16-битный адрес регистра с его типом, назначением, доступом и
масштабом. В ProtoCAN используется <code>MsgType=0x3</code>, а <code>MsgBody</code> содержит адрес
первого регистра.</p>
<h2 id="section-2">Обязательные поля реестра</h2>
<table>
<thead>
<tr>
<th>Поле</th>
<th>Требование</th>
</tr>
</thead>
<tbody>
<tr>
<td>AddressHex</td>
<td><code>0x0000…0xFFFF</code>, уникальное значение</td>
</tr>
<tr>
<td>AddressDec</td>
<td>десятичный эквивалент AddressHex</td>
</tr>
<tr>
<td>Group</td>
<td>функциональная группа</td>
</tr>
<tr>
<td>Name</td>
<td>однозначное имя параметра</td>
</tr>
<tr>
<td>Type</td>
<td><code>u16</code>, <code>i16</code>, <code>u32</code>, <code>i32</code>, <code>float32</code>, bitmap или массив</td>
</tr>
<tr>
<td>Registers</td>
<td>число занятых 16-битных регистров</td>
</tr>
<tr>
<td>Access</td>
<td><code>R</code>, <code>W</code> или <code>RW</code></td>
</tr>
<tr>
<td>Unit</td>
<td>физическая единица либо <code>—</code></td>
</tr>
<tr>
<td>Scale</td>
<td>множитель/делитель представления</td>
</tr>
<tr>
<td>Default</td>
<td>значение после сброса, если применимо</td>
</tr>
<tr>
<td>Description</td>
<td>семантика, диапазон и особые значения</td>
</tr>
</tbody>
</table>
<h2 id="section-3">Правила ведения</h2>
<ul>
<li>Адрес не переиспользуется с другим смыслом после выпуска стабильной версии.</li>
<li>Многорегистровое значение занимает непрерывный диапазон.</li>
<li>Порядок 16-битных слов для 32-битного значения фиксируется в строке типа.</li>
<li>Резервные диапазоны явно отмечаются и не используются без изменения версии.</li>
<li>Удалённый параметр помечается deprecated, а не исчезает молча.</li>
<li>Изменение адреса, типа или масштаба отражается в <code>CHANGELOG.md</code>.</li>
</ul>
<h2 id="section-4">Экспорт</h2>
<p>Для программной генерации каталог следует экспортировать из XLSX в CSV с
UTF-8 и фиксированными английскими именами колонок. CSV должен проверяться на:</p>
<ul>
<li>уникальность адресов;</li>
<li>пересечение многорегистровых значений;</li>
<li>допустимые типы и права доступа;</li>
<li>равенство шестнадцатеричного и десятичного адреса;</li>
<li>попадание адреса в диапазон <code>0x0000…0xFFFF</code>.</li>
</ul>
<p>До появления автоматического экспортёра нормативным источником адресов
остаётся XLSX, а HTML/Markdown считаются представлением.</p>
<hr>
<h1 id="protocan">История изменений ProtoCAN</h1>
<p>Формат основан на Keep a Changelog. Версия относится к спецификации, а не к
версии прошивки отдельного прибора.</p>
<h2 id="unreleased">[Unreleased]</h2>
<h3 id="added">Added</h3>
<ul>
<li>Структурированный комплект документации <code>docs/protocan</code>.</li>
<li>Загрузочный сервис <code>MsgType=0x9…0xD</code>.</li>
<li>Два логических слота по 512 КиБ, 65 536 блоков по 8 байт каждый.</li>
<li>Машинные эталоны CAN ID в <code>examples/test-vectors.json</code>.</li>
</ul>
<h2 id="section">[1.0] — 2026-08-29</h2>
<h3 id="added-1">Added</h3>
<ul>
<li>Зафиксирована 29-битная структура ProtoCAN ID.</li>
<li>Зафиксирована адресация 8 типов по 16 экземпляров.</li>
<li>Существующие сообщения <code>0x0…0x8</code>, <code>0xE</code>, <code>0xF</code> сохранены.</li>
</ul>
</body>
</html>