Files
templates/doc/setcan/index.html
T
Andrey f17c124f0e docs: Согласуй документацию ProtoCAN и инструментов сборки
Обнови спецификацию GAS 0x3/0x4, реестр устройств и описание загрузочных сервисов одновременно в Markdown, HTML и XLSX.

Свяжи новые аппаратные порты, глобальное время и сборку C2000/ПЛИС с корневыми инструкциями, чтобы проекты подключались по одному актуальному контракту.
2026-10-23 14:16:22 +03:00

1608 lines
78 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Интерактивная документация SETCAN: архитектура, API ProtoCAN и перенос в STM32-проект.">
<title>SETCAN · Руководство разработчика</title>
<style>
:root {
color-scheme: dark;
--bg: #07100f; --panel: #0d1917; --panel-2: #12211e; --line: #263b36;
--text: #e7f2ee; --muted: #96aaa4; --mint: #4ee0aa; --cyan: #62cbe8;
--amber: #ffca6a; --red: #ff887d; --code: #091412; --radius: 16px;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; min-height: 100vh; font: 15px/1.6 Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--text); background: radial-gradient(circle at 82% -10%, #183d34 0, transparent 34rem), var(--bg); }
button, a { -webkit-tap-highlight-color: transparent; }
a { color: var(--cyan); text-decoration: none; }
a:hover { text-decoration: underline; }
.shell { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: 42px 0 64px; }
.eyebrow { color: var(--mint); font: 700 12px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace; letter-spacing: .14em; text-transform: uppercase; }
h1 { max-width: 800px; margin: 10px 0 12px; font-size: clamp(38px, 7vw, 72px); line-height: .98; letter-spacing: -.055em; }
.lead { max-width: 760px; margin: 0; color: var(--muted); font-size: 18px; }
.hero { padding: 34px 0 30px; }
.stats { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }
.stat { padding: 8px 12px; border: 1px solid var(--line); border-radius: 999px; background: #0b1714cc; color: var(--muted); font-size: 13px; }
.stat b { color: var(--text); }
.tabs-wrap { position: sticky; top: 0; z-index: 5; margin: 0 -10px 28px; padding: 10px; overflow-x: auto; background: linear-gradient(var(--bg) 70%, transparent); }
.tabs { display: flex; width: max-content; min-width: 100%; gap: 6px; padding: 5px; border: 1px solid var(--line); border-radius: 14px; background: #0b1614ed; backdrop-filter: blur(14px); }
.tab { flex: 1; min-width: 130px; border: 0; border-radius: 9px; padding: 10px 16px; color: var(--muted); background: transparent; cursor: pointer; font: 650 14px/1 system-ui; }
.tab:hover { color: var(--text); background: var(--panel-2); }
.tab[aria-selected="true"] { color: #06110e; background: var(--mint); }
.panel { display: none; animation: enter .24s ease-out; }
.panel.active { display: block; }
@keyframes enter { from { opacity: 0; transform: translateY(5px); } }
h2 { margin: 0 0 8px; font-size: clamp(27px, 4vw, 38px); letter-spacing: -.035em; }
h3 { margin: 0 0 8px; font-size: 19px; letter-spacing: -.015em; }
p { margin: 0 0 14px; }
.section-intro { max-width: 760px; margin-bottom: 24px; color: var(--muted); }
.grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 14px; }
.card { grid-column: span 4; min-width: 0; padding: 21px; border: 1px solid var(--line); border-radius: var(--radius); background: linear-gradient(145deg, #10201c, #0b1715); }
.card.wide { grid-column: span 8; }
.card.full { grid-column: 1 / -1; }
.card p, .card li { color: var(--muted); }
.card strong { color: var(--text); }
.tag { display: inline-block; margin-bottom: 14px; padding: 3px 8px; border-radius: 6px; color: var(--mint); background: #173d31; font: 700 11px/1.5 ui-monospace, monospace; text-transform: uppercase; }
.tag.amber { color: var(--amber); background: #3a2d16; }
.tag.blue { color: var(--cyan); background: #15343b; }
.filetree, pre { margin: 14px 0 0; border: 1px solid var(--line); border-radius: 12px; background: var(--code); color: #cce0da; font: 13px/1.65 ui-monospace, SFMono-Regular, Consolas, monospace; }
.filetree { padding: 17px; white-space: pre-wrap; }
pre { position: relative; overflow: auto; padding: 44px 18px 18px; }
code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
:not(pre) > code { padding: 2px 6px; border-radius: 5px; color: #d5eee5; background: #172824; }
.copy { position: absolute; top: 9px; right: 9px; border: 1px solid var(--line); border-radius: 7px; padding: 6px 9px; color: var(--muted); background: var(--panel-2); cursor: pointer; font-size: 12px; }
.copy:hover { color: var(--text); border-color: var(--mint); }
ul, ol { margin: 10px 0 0; padding-left: 20px; }
li + li { margin-top: 7px; }
.flow { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 18px; }
.step { position: relative; padding: 16px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); }
.step b { display: block; color: var(--mint); font-size: 13px; }
.step span { color: var(--muted); font-size: 13px; }
.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; min-width: 650px; }
th, td { padding: 11px 13px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }
td:first-child { color: var(--mint); font-family: ui-monospace, monospace; }
.callout { margin-top: 16px; padding: 15px 17px; border-left: 3px solid var(--amber); border-radius: 4px 12px 12px 4px; color: #dfd2b4; background: #251f14; }
.checklist { list-style: none; padding: 0; }
.checklist li { position: relative; padding-left: 28px; }
.checklist li::before { content: "✓"; position: absolute; left: 0; color: var(--mint); font-weight: 800; }
.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; } .protocan-document { padding: 18px 14px; } }
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; animation: none !important; } }
</style>
</head>
<body>
<main class="shell">
<header class="hero">
<div class="eyebrow">STM32 · Classic CAN 2.0B · HAL</div>
<h1>SETCAN<br>руководство разработчика</h1>
<p class="lead">Карта библиотеки ProtoCAN, формат кадров и практический маршрут переноса в существующую прошивку STM32.</p>
<div class="stats" aria-label="Краткие характеристики">
<span class="stat"><b>29 бит</b> Extended ID</span><span class="stat"><b>0…8 байт</b> payload</span><span class="stat"><b>128</b> адресов устройств</span><span class="stat"><b>127</b> кадров в RX-очереди</span>
</div>
</header>
<div class="tabs-wrap">
<nav class="tabs" role="tablist" aria-label="Разделы документации">
<button class="tab" role="tab" aria-selected="true" aria-controls="overview" id="tab-overview" data-tab="overview">Обзор</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="library" id="tab-library" data-tab="library">Библиотека</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="protocol" id="tab-protocol" data-tab="protocol">Протокол</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="porting" id="tab-porting" data-tab="porting">Портирование</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="examples" id="tab-examples" data-tab="examples">Примеры</button>
</nav>
</div>
<section class="panel active" id="overview" role="tabpanel" aria-labelledby="tab-overview">
<h2>Что находится в проекте</h2>
<p class="section-intro">SETCAN — не готовый CubeIDE-проект, а подключаемый C-модуль поверх STM32 HAL. Его задача — фильтровать, принимать, разбирать и отправлять прикладные сообщения ProtoCAN.</p>
<div class="grid">
<article class="card wide"><span class="tag">Структура</span><h3>Минимальное ядро + нормативные документы</h3><div class="filetree">SETCAN/
├── Inc/protocan.h публичные типы, настройки и API
├── Src/protocan.c фильтры, RX-очередь, разбор и отправка
├── doc/setcan/protocan/ спецификация протокола
│ ├── PROTOCOL.md 29-битный ID и типы сообщений
│ ├── OAP.md общее адресное пространство
│ ├── BOOTLOADER.md обновление прошивки по CAN (draft)
│ └── examples/ эталонные кадры JSON
├── Протокол CAN и ОАП.xlsx редактируемый реестр ОАП
└── doc/index.html эта страница</div></article>
<article class="card"><span class="tag blue">Зависимости</span><h3>Что ожидает модуль</h3><ul><li><code>main.h</code> и <code>can.h</code> из CubeMX</li><li>STM32 HAL CAN, RTC и TIM</li><li>CAN FIFO0 и Extended ID</li><li>STM32 GCC ABI для C bit-fields</li></ul></article>
<article class="card"><span class="tag">Входящие</span><h3>Путь кадра</h3><p>IRQ вычитывает FIFO0. Pulse обрабатывается сразу, остальные Extended-кадры попадают в кольцевой буфер и разбираются в основном цикле.</p></article>
<article class="card"><span class="tag">Исходящие</span><h3>Единая точка отправки</h3><p><code>PROTOCAN_SEND()</code> выбирает упаковщик по <code>MsgType</code>. GAS и Modbus автоматически разбиваются на кадры до 8 байт.</p></article>
<article class="card"><span class="tag amber">Граница</span><h3>Что приложение реализует само</h3><p>Прикладные действия, хранение SETTINGS, EEPROM, DS18B20, таймаут online/offline и полноценный bootloader не входят в ядро.</p></article>
</div>
<div class="flow" aria-label="Поток обработки">
<div class="step"><b>01 · FIFO0 IRQ</b><span>HAL сообщает о новом кадре</span></div><div class="step"><b>02 · RX buffer</b><span>Extended ID помещается в очередь</span></div><div class="step"><b>03 · Dispatcher</b><span>Разбор по MsgType</span></div><div class="step"><b>04 · Weak handler</b><span>Логика приложения</span></div>
</div>
</section>
<section class="panel" id="library" role="tabpanel" aria-labelledby="tab-library">
<h2>Состав библиотеки</h2><p class="section-intro">Два файла образуют один модуль. Заголовок — контракт интеграции; C-файл — транспортная реализация и демонстрационные слабые обработчики.</p>
<div class="grid">
<article class="card"><span class="tag">Inc</span><h3>protocan.h</h3><p>Конфигурация текущего устройства, размеры буфера, enum типов сообщений, битовые представления ID/Body, структуры TX/RX и публичные функции.</p><p><strong>Меняют при переносе:</strong> <code>CURRENT_TYPE_DEVICE</code>, <code>CURRENT_ID_DEVICE</code>, иногда <code>PROTOCAN_RX_BUFFER_SIZE</code>.</p></article>
<article class="card"><span class="tag">Src</span><h3>protocan.c</h3><p>Инициализация, фильтры, кольцевой буфер, диспетчеризация, RTC sync, pulse и упаковка кадров.</p><p><strong>Обычно не правят:</strong> прикладную логику лучше добавлять переопределением <code>__weak</code>-функций.</p></article>
<article class="card"><span class="tag amber">HAL</span><h3>Внешний слой</h3><p><code>CAN_HandleTypeDef</code>, <code>RTC_HandleTypeDef</code> и <code>TIM_HandleTypeDef</code> передаются в <code>PROTOCAN_INIT()</code>. Запуск периферии выполняет приложение.</p></article>
<article class="card full"><h3>Основной API</h3><div class="table-scroll"><table><thead><tr><th>Функция</th><th>Назначение</th><th>Где вызывать</th></tr></thead><tbody>
<tr><td>PROTOCAN_INIT</td><td>Сохраняет HAL handles, настраивает фильтры и callback’и при доступной регистрации.</td><td>После <code>MX_*_Init()</code>, до <code>HAL_CAN_Start()</code>.</td></tr>
<tr><td>PROTOCAN_ProcessAllRxMsgs</td><td>Обрабатывает всю накопленную RX-очередь и возвращает первую ошибку.</td><td>В основном цикле.</td></tr>
<tr><td>PROTOCAN_ProcessSingleRxMsg</td><td>Обрабатывает максимум один кадр.</td><td>В планировщике или ограниченном временном слоте.</td></tr>
<tr><td>PROTOCAN_SEND</td><td>Упаковывает и отправляет сообщение выбранного типа.</td><td>Из логики приложения, не удерживая изменяемые данные.</td></tr>
<tr><td>ProtoCanRxFifo0MsgPendingCallback</td><td>Забирает кадры из CAN FIFO0.</td><td>Из HAL callback, если авто-регистрация выключена.</td></tr>
<tr><td>ProtoCanPulseCallback</td><td>Отправляет периодический pulse со счётчиком.</td><td>Из callback нужного базового таймера.</td></tr>
<tr><td>PROTOCAN_SEND_SETTINGS_RESPONSE / ERROR</td><td>Формирует успешный либо ошибочный ответ привязки ROM.</td><td>Из пользовательского обработчика SETTINGS.</td></tr>
</tbody></table></div></article>
<article class="card full"><h3>Точки расширения</h3><p>Переопределите нужную <code>__weak</code>-функцию в собственном C-файле: семейства <code>ProtoCanMsgToBroadcast…</code>, <code>…Discrete…</code>, <code>…Analog…</code>, <code>ProtoCanMsgToSettings</code>, <code>ProtoCanMsgToGeneralAddressSpace</code> и <code>…Modbus…</code>. Не редактируйте демонстрационную реализацию в ядре — так обновлять библиотеку проще.</p></article>
</div>
</section>
<section class="panel" id="protocol" role="tabpanel" aria-labelledby="tab-protocol">
<h2>Формат ProtoCAN</h2><p class="section-intro">Протокол использует только CAN 2.0B Extended ID. Адрес устройства — пара <code>DeviceType/DeviceID</code>; назначение младших 16 бит зависит от типа сообщения.</p>
<article class="card full"><h3>29-битный идентификатор</h3><pre><code>28 27 26··24 23··20 19··16 15········0
Priority Route DeviceType DeviceID MsgType MsgBody
1 бит 1 бит 3 бита 4 бита 4 бита 16 бит</code></pre><div class="callout">Многобайтовые значения payload передаются little-endian. Разметка ID через C bit-fields требует проверки при смене компилятора или ABI.</div></article>
<div class="grid" style="margin-top:14px">
<article class="card full"><h3>Реестр MsgType</h3><div class="table-scroll"><table><thead><tr><th>Код</th><th>Тип</th><th>Назначение</th><th>Статус</th></tr></thead><tbody>
<tr><td>0x0</td><td>BROADCAST</td><td>Общие команды всем устройствам</td><td>stable</td></tr><tr><td>0x1</td><td>DISCRETE</td><td>Состояния, команды, флаги</td><td>stable</td></tr><tr><td>0x2</td><td>ANALOG</td><td>Датчики U / I / T и универсальные данные</td><td>stable</td></tr><tr><td>0x3</td><td>GAS</td><td>Общее 16-битное адресное пространство</td><td>stable</td></tr><tr><td>0x4</td><td>GAS SLOT</td><td>Word0 — маска битов 1…3, Word1…Word3 — три значения</td><td>stable</td></tr><tr><td>0x5…0x7</td><td>MODBUS</td><td>Discrete, Holding, Input</td><td>stable</td></tr><tr><td>0x8</td><td>ERROR</td><td>Код ошибки и дополнительная информация</td><td>stable</td></tr><tr><td>0x9…0xD</td><td>BOOT</td><td>Управление, блоки A/B, статус, discovery</td><td>draft</td></tr><tr><td>0xE</td><td>SETTINGS</td><td>Привязка 1-Wire ROM к локации Z/Y</td><td>stable</td></tr><tr><td>0xF</td><td>PULSE</td><td>Присутствие устройства</td><td>stable</td></tr>
</tbody></table></div></article>
</div>
</section>
<section class="panel" id="porting" role="tabpanel" aria-labelledby="tab-porting">
<h2>Как портировать в свой STM32-проект</h2><p class="section-intro">Рабочая последовательность для CubeMX/CubeIDE. Имена <code>hcan</code>, <code>hrtc</code> и <code>htim2</code> замените на handles своего проекта.</p>
<div class="grid">
<article class="card"><span class="tag">Шаг 1</span><h3>Добавьте исходники</h3><ul class="checklist"><li><code>Inc/protocan.h</code> в include path</li><li><code>Src/protocan.c</code> в сборку</li><li>Проверьте доступность <code>main.h</code> и <code>can.h</code></li></ul></article>
<article class="card"><span class="tag">Шаг 2</span><h3>Настройте адрес</h3><p>В <code>protocan.h</code> задайте тип <code>0…7</code> и ID <code>0…15</code>. Пара должна быть уникальной на шине.</p><pre><code>#define CURRENT_TYPE_DEVICE 0b011
#define CURRENT_ID_DEVICE 0b0101</code></pre></article>
<article class="card"><span class="tag">Шаг 3</span><h3>Настройте CubeMX</h3><ul><li>CAN: Extended ID, FIFO0</li><li>RTC: если нужна синхронизация времени</li><li>TIM: период pulse</li><li>Проверьте filter banks 0, 1, 2 и границу 14</li></ul></article>
<article class="card full"><span class="tag">Шаг 4</span><h3>Инициализация и основной цикл</h3><pre><button class="copy" type="button">Копировать</button><code>#include "protocan.h"
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_CAN_Init();
MX_RTC_Init();
MX_TIM2_Init();
if (PROTOCAN_INIT(&amp;hcan, &amp;hrtc, &amp;htim2) != PROTOCAN_INIT_OK) {
Error_Handler();
}
if (HAL_CAN_Start(&amp;hcan) != HAL_OK) {
Error_Handler();
}
if (HAL_CAN_ActivateNotification(
&amp;hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) {
Error_Handler();
}
if (HAL_TIM_Base_Start_IT(&amp;htim2) != HAL_OK) {
Error_Handler();
}
while (1) {
(void)PROTOCAN_ProcessAllRxMsgs();
}
}</code></pre></article>
<article class="card wide"><span class="tag">Шаг 5</span><h3>Свяжите callback’и при отключённой регистрации HAL</h3><pre><button class="copy" type="button">Копировать</button><code>void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan_ptr)
{
if (hcan_ptr == &amp;hcan) {
ProtoCanRxFifo0MsgPendingCallback(hcan_ptr);
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim_ptr)
{
if (htim_ptr == &amp;htim2) {
ProtoCanPulseCallback(htim_ptr);
}
}</code></pre></article>
<article class="card"><span class="tag amber">Важно</span><h3>Не дублируйте события</h3><p>Если <code>USE_HAL_*_REGISTER_CALLBACKS == 1</code>, библиотека регистрирует callback’и сама. Не вызывайте те же функции ещё раз из стандартных callback’ов.</p></article>
<article class="card full"><span class="tag">Шаг 6</span><h3>Перенесите прикладную логику</h3><p>Создайте, например, <code>protocan_app.c</code> и определите там только нужные weak handlers. Так ядро остаётся обновляемым.</p><pre><button class="copy" type="button">Копировать</button><code>#include "protocan.h"
PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogTSens(struct RXMsg msg)
{
msgBodyAnalogType body = {0};
body.Body = msg.eID.Fields.MsgBody;
uint16_t sensor_id = body.Fields.SensorID;
/* Получить температуру sensor_id и сформировать ответ. */
return PROTOCAN_OK;
}</code></pre></article>
<article class="card full"><h3>Проверка после переноса</h3><ul class="checklist"><li>Проект собирается без повторных определений HAL callback’ов.</li><li>CAN стартует и принимает только Extended ID.</li><li>Pulse появляется с периодом выбранного таймера.</li><li>Эталонный ID <code>0x13590702</code> декодируется как DeviceType=3, DeviceID=5, MsgType=9, Body=<code>0x0702</code>.</li><li>При нагрузке учтено, что буфер размера 128 хранит максимум 127 кадров.</li></ul></article>
</div>
</section>
<section class="panel" id="examples" role="tabpanel" aria-labelledby="tab-examples">
<h2>Готовые шаблоны</h2><p class="section-intro">Минимальные примеры формирования исходящего кадра и обработчика SETTINGS. Их можно вынести в прикладной слой проекта.</p>
<div class="grid">
<article class="card full"><h3>Отправка трёх регистров GAS</h3><pre><button class="copy" type="button">Копировать</button><code>uint16_t registers[] = { 0x1234, 0x5678, 0x9ABC };
ProtoCanId_t id = {0};
id.Fields.Priority = PROTOCAN_PRIORITY_STANDARD;
id.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
id.Fields.DeviceType = CURRENT_TYPE_DEVICE;
id.Fields.DeviceID = CURRENT_ID_DEVICE;
id.Fields.MsgType = PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE;
ProtoCanData_t tx = {0};
tx.GeneralAddressSpaceData.RegStartAdr = 100;
tx.GeneralAddressSpaceData.Data = registers;
tx.GeneralAddressSpaceData.RegCount = 3;
if (PROTOCAN_SEND(id, tx) != PROTOCAN_OK) {
/* CAN занят или HAL вернул ошибку. */
}</code></pre></article>
<article class="card full"><h3>SETTINGS: прикладное хранение ROM</h3><pre><button class="copy" type="button">Копировать</button><code>PROTOCAN_StatusTypeDef ProtoCanMsgToSettings(
const ProtoCanSettingsMsg_t *message)
{
uint8_t current_rom[PROTOCAN_SETTINGS_ROM_SIZE] = {0};
switch (message-&gt;Operation) {
case PROTOCAN_SETTINGS_GET:
/* Загрузить ROM из EEPROM в current_rom. */
return PROTOCAN_SEND_SETTINGS_RESPONSE(
message-&gt;AssemblySerial, message-&gt;Position, current_rom);
case PROTOCAN_SETTINGS_WRITE:
/* Проверить CRC/наличие и атомарно записать message-&gt;Rom. */
return PROTOCAN_SEND_SETTINGS_RESPONSE(
message-&gt;AssemblySerial, message-&gt;Position, message-&gt;Rom);
case PROTOCAN_SETTINGS_CLEAR:
/* Очистить запись; current_rom должен содержать нули. */
return PROTOCAN_SEND_SETTINGS_RESPONSE(
message-&gt;AssemblySerial, message-&gt;Position, current_rom);
default:
return PROTOCAN_SEND_SETTINGS_ERROR(
message-&gt;AssemblySerial, message-&gt;Position,
PROTOCAN_SETTINGS_RESULT_INVALID_DLC);
}
}</code></pre></article>
</div>
<div class="docs" aria-label="Исходная документация">
<a class="doclink" href="README.md">README комплекта ↗</a><a class="doclink" href="../../README.md">README templates ↗</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>
<!-- PROTOCAN:START -->
<section id="protocan-full" class="section">
<h2>Полная документация ProtoCAN</h2>
<p class="lead">Нормативные документы и тестовые векторы собраны в эту страницу из исходников <code>doc/setcan/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>2.1</strong><br>
Дата редакции: <strong>2026-10-23</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/setcan/build-html.bat
</code></pre>
<p>Все документы и тестовые векторы включаются в единый файл
<code>doc/setcan/index.html</code>.
Скрипт не изменяет исходные Markdown/XLSX и пригоден для запуска в CI.</p>
<h2 id="gas-0x3-0x4">GAS: регистры 0x3 и слоты 0x4</h2>
<p><a href="protocan/OAP.md">Адресация GAS</a>: <code>0x3</code> — отдельные 16-битные регистры <code>0…65535</code>, запись 1…4 слов. <code>0x4</code> — слот с Word0-маской и тремя значениями; PERIPH сохраняет 200 слов в 67 слотах <code>0xMM00…0xMM42</code>. Формат определяется MsgType, без настройки профиля.</p>
</article>
<article class="card protocan-document" data-source="PROTOCOL.md"><h1 id="protocan">ProtoCAN — базовый протокол</h1>
<p>Статус: <strong>Stable с зарезервированным загрузочным расширением</strong><br>
Версия: <strong>2.1: GAS 0x3 и масочный GAS SLOT 0x4</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>регистры 0…65535</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x4</code></td>
<td><code>GAS_SLOT</code></td>
<td>оба</td>
<td style="text-align: right;">0/8</td>
<td>Word0 — маска, Word1…Word3 — значения</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 0x3</td>
<td>адрес первого 16-битного регистра <code>[15:0]</code></td>
</tr>
<tr>
<td>GAS SLOT 0x4</td>
<td>адрес слота <code>[15:0]</code>; маска — в Word0 payload, значения — Word1…Word3</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>
<p>Контракт обоих GAS: <a href="protocan/OAP.md">OAP.md</a>. Тип 0x3 адресует отдельные слова; тип 0x4 имеет маску битов 1…3 в Word0 и три значения. Для PERIPH 0x4 используются 67 слотов <code>0xMM00…0xMM42</code>, последний содержит два значения. Старый Coil 0x4 заменён; формат определяется MsgType.</p>
<h2 id="section-2">Общие правила обмена</h2>
<p>Профиль синхронизации часов и сообщения платы о завершении инициализации:
<a href="protocan/GLOBAL_TIME.md">GLOBAL_TIME.md</a>. Он сохраняет штатный BROADCAST RTCSETUP
и определяет общий пакет готовности поверх DISCRETE/FLAGS.</p>
<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="../../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="gas-gas-slot">Общее адресное пространство: GAS и GAS SLOT</h1>
<p>Дата редакции: <strong>2026-10-23</strong>. Номер типа однозначно определяет единицу адресации:</p>
<table>
<thead>
<tr>
<th>MsgType</th>
<th>Имя</th>
<th>MsgBody</th>
<th>Payload</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>0x3</code></td>
<td>GAS</td>
<td>адрес первого 16-битного регистра</td>
<td>1…4 последовательных регистра, DLC 2/4/6/8</td>
</tr>
<tr>
<td><code>0x4</code></td>
<td>GAS SLOT</td>
<td>адрес слота</td>
<td>Word0 — маска; Word1…Word3 — три значения; DLC 8</td>
</tr>
</tbody>
</table>
<p>В обоих случаях запрос чтения — extended data frame с <code>DLC=0</code>, <code>Route=0</code>.
Ответ имеет <code>Route=1</code>, тот же узел и адрес. Прикладного ACK записи нет;
успех применения уставки проверяется прикладным ответом или последующим чтением.
RTR, error frame, стандартный 11-битный ID и нечётные длины не исполняются.</p>
<h2 id="gas-0x3">GAS 0x3: отдельные регистры</h2>
<p><code>MsgBody=RegisterAddress[15:0]</code>. Адреса <code>0…65535</code> (<code>0x0000…0xFFFF</code>)
обозначают <strong>65 536 отдельных 16-битных регистров</strong>, логически 128 КиБ на узел.
Это адреса протокола, а не указатели RAM MCU; provider подключает только существующие регионы.</p>
<p>Payload содержит значения с адресов <code>addr</code>, <code>addr+1</code>, <code>addr+2</code>, <code>addr+3</code>
в порядке little-endian. Количество равно <code>DLC/2</code>, допускаются DLC 2, 4, 6, 8.
Для изменения одной уставки по адресу <code>0x0001</code> на <code>0x1234</code> передаются
<code>MsgBody=0x0001</code>, <code>DLC=2</code>, Data=<code>34 12</code>; соседние слова не изменяются.
Чтение возвращает до четырёх существующих последовательных регистров,
останавливаясь на первом недоступном адресе или границе <code>0xFFFF</code>.
Запись, пересекающая <code>0xFFFF</code>, отвергается до изменения данных.</p>
<p>Регионы карты, права и значения проверяет <code>pcan_gas</code>; callback остаётся
ответственностью приложения. Блочная запись может остановиться на первом отказе provider:
атомарность нескольких регистров не обещается. Связанные <code>u32/float32</code> и команды
требуют согласованной прикладной операции. Основной контракт: <code>c/set-protocol/docs/legacy/GAS.md</code>.
GAS 167/v1, GAS Logger, GAS_WATCH2, NAND и Altera Logic сохраняют регистровые адреса.
Номер mode задаёт адрес узла; его нельзя автоматически прибавлять к адресу регистра.</p>
<h2 id="gas-slot-0x4-word0">GAS SLOT 0x4: маска в Word0</h2>
<p><code>MsgBody=SlotAddress[15:0]</code>. Восемь октетов имеют фиксированное расположение:</p>
<table>
<thead>
<tr>
<th>Октеты</th>
<th>Wire-слово</th>
<th>Назначение</th>
</tr>
</thead>
<tbody>
<tr>
<td>0…1</td>
<td>Word0</td>
<td>16-битная маска, little-endian</td>
</tr>
<tr>
<td>2…3</td>
<td>Word1</td>
<td>первое значение</td>
</tr>
<tr>
<td>4…5</td>
<td>Word2</td>
<td>второе значение</td>
</tr>
<tr>
<td>6…7</td>
<td>Word3</td>
<td>третье значение</td>
</tr>
</tbody>
</table>
<p>Биты маски 1, 2, 3 соответствуют Word1, Word2, Word3. Бит 0 и биты 4…15
зарезервированы и должны быть нулевыми. Маска непустая; все допустимые маски:
<code>0x0002</code>, <code>0x0004</code>, <code>0x0006</code>, <code>0x0008</code>, <code>0x000A</code>, <code>0x000C</code>, <code>0x000E</code>.
При записи отмеченные значения применяются, неотмеченные позиции игнорируются
и сохраняют текущие значения устройства. Нули не являются специальными маркерами.
При чтении маска ответа отмечает достоверные/доступные значения.</p>
<p>Например, изменить только Word2 на <code>0x1234</code>: Data=<code>04 00 00 00 34 12 00 00</code>.
Word1 и Word3 сохраняются. Полная запись трёх значений использует маску <code>0x000E</code>.
Передаются всегда восемь октетов, кроме пустого запроса чтения. Сжатия или сегментации нет.
Provider проверяет все выбранные права и значения до первого изменения, затем применяет
выбранные слова под общей блокировкой. Невыбранные readonly-поля не записываются.
Ядро не выполняет опасное чтение-изменение-запись слота на стороне клиента.</p>
<p>Полное пространство 0x4 содержит <strong>65 536 слотов × 3 значения = 196 608 слов = 384 КиБ</strong>.
Word0 — метаданные кадра, не четвёртый хранимый регистр. Координата значения —
<code>(SlotAddress, WordIndex)</code>, где <code>WordIndex=1…3</code>. Любой узел поддерживается общими
<code>pcan_gas_slot_id()</code> и <code>pcan_gas_slot_handle_address()</code>; адрес слота не ограничен страницами PERIPH.
Тип <code>0x4 MODBUS_COIL</code> заменён этим сервисом. Modbus <code>0x5/0x6/0x7</code> сохраняет номера.
Старое оборудование с Coil 0x4 требует согласованного обновления; автоопределения формата нет.</p>
<h2 id="periph-200-0x4">PERIPH: проекция 200 слов на слоты 0x4</h2>
<p><code>mode=0…31</code>; <code>DeviceType=1+mode/16</code>, <code>DeviceID=mode mod 16</code>.
<code>SlotAddress=(mode&lt;&lt;8)|LocalSlot</code>. Маски не занимают места в карте значений.
На mode сохранены 200 слов, поэтому используются 67 слотов <code>LocalSlot=0x00…0x42</code>:
66 полных слотов и два значения последнего. В последнем разрешена только маска
из битов 1/2 (<code>0x0002</code>, <code>0x0004</code>, <code>0x0006</code>); Word3 не существует.
<code>LocalSlot=0x43…0xFF</code> — резерв. Узел и старший октет слота должны совпадать.</p>
<p>Смещение технологического слова: <code>n=3*LocalSlot+(WordIndex-1)</code>.
Обратное преобразование: <code>LocalSlot=n/3</code>, <code>WordIndex=1+n mod 3</code>.
Соседние native-сервисы могут оказаться в одном слоте, поэтому provider проверяет
доступ и наличие каждой отмеченной позиции отдельно.</p>
<table>
<thead>
<tr>
<th style="text-align: right;">mode</th>
<th>Назначение</th>
<th>DeviceType / DeviceID</th>
<th>Слоты 0x4</th>
<th>Запрос первого слота</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;">0</td>
<td>Резерв</td>
<td><code>0x01 / 0x00</code></td>
<td><code>0x0000…0x0042</code></td>
<td><code>0x11040000</code></td>
</tr>
<tr>
<td style="text-align: right;">1</td>
<td>Трансформатор 1</td>
<td><code>0x01 / 0x01</code></td>
<td><code>0x0100…0x0142</code></td>
<td><code>0x11140100</code></td>
</tr>
<tr>
<td style="text-align: right;">2</td>
<td>Трансформатор 2</td>
<td><code>0x01 / 0x02</code></td>
<td><code>0x0200…0x0242</code></td>
<td><code>0x11240200</code></td>
</tr>
<tr>
<td style="text-align: right;">3</td>
<td>Силовой узел 1</td>
<td><code>0x01 / 0x03</code></td>
<td><code>0x0300…0x0342</code></td>
<td><code>0x11340300</code></td>
</tr>
<tr>
<td style="text-align: right;">4</td>
<td>Силовой узел 2</td>
<td><code>0x01 / 0x04</code></td>
<td><code>0x0400…0x0442</code></td>
<td><code>0x11440400</code></td>
</tr>
<tr>
<td style="text-align: right;">5</td>
<td>УМП1</td>
<td><code>0x01 / 0x05</code></td>
<td><code>0x0500…0x0542</code></td>
<td><code>0x11540500</code></td>
</tr>
<tr>
<td style="text-align: right;">6</td>
<td>УМП2</td>
<td><code>0x01 / 0x06</code></td>
<td><code>0x0600…0x0642</code></td>
<td><code>0x11640600</code></td>
</tr>
<tr>
<td style="text-align: right;">7</td>
<td>ENG1</td>
<td><code>0x01 / 0x07</code></td>
<td><code>0x0700…0x0742</code></td>
<td><code>0x11740700</code></td>
</tr>
<tr>
<td style="text-align: right;">8</td>
<td>Задатчик / пульт</td>
<td><code>0x01 / 0x08</code></td>
<td><code>0x0800…0x0842</code></td>
<td><code>0x11840800</code></td>
</tr>
<tr>
<td style="text-align: right;">9</td>
<td>Шкаф</td>
<td><code>0x01 / 0x09</code></td>
<td><code>0x0900…0x0942</code></td>
<td><code>0x11940900</code></td>
</tr>
<tr>
<td style="text-align: right;">10</td>
<td>Общий адрес: служебная роль</td>
<td><code>0x01 / 0x0A</code></td>
<td><code>0x0A00…0x0A42</code></td>
<td><code>0x11A40A00</code></td>
</tr>
<tr>
<td style="text-align: right;">11</td>
<td>Терминал: служебная роль</td>
<td><code>0x01 / 0x0B</code></td>
<td><code>0x0B00…0x0B42</code></td>
<td><code>0x11B40B00</code></td>
</tr>
<tr>
<td style="text-align: right;">12</td>
<td>Резерв назначения</td>
<td><code>0x01 / 0x0C</code></td>
<td><code>0x0C00…0x0C42</code></td>
<td><code>0x11C40C00</code></td>
</tr>
<tr>
<td style="text-align: right;">13</td>
<td>Резерв назначения</td>
<td><code>0x01 / 0x0D</code></td>
<td><code>0x0D00…0x0D42</code></td>
<td><code>0x11D40D00</code></td>
</tr>
<tr>
<td style="text-align: right;">14</td>
<td>Резерв назначения</td>
<td><code>0x01 / 0x0E</code></td>
<td><code>0x0E00…0x0E42</code></td>
<td><code>0x11E40E00</code></td>
</tr>
<tr>
<td style="text-align: right;">15</td>
<td>Резерв назначения</td>
<td><code>0x01 / 0x0F</code></td>
<td><code>0x0F00…0x0F42</code></td>
<td><code>0x11F40F00</code></td>
</tr>
<tr>
<td style="text-align: right;">16</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x00</code></td>
<td><code>0x1000…0x1042</code></td>
<td><code>0x12041000</code></td>
</tr>
<tr>
<td style="text-align: right;">17</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x01</code></td>
<td><code>0x1100…0x1142</code></td>
<td><code>0x12141100</code></td>
</tr>
<tr>
<td style="text-align: right;">18</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x02</code></td>
<td><code>0x1200…0x1242</code></td>
<td><code>0x12241200</code></td>
</tr>
<tr>
<td style="text-align: right;">19</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x03</code></td>
<td><code>0x1300…0x1342</code></td>
<td><code>0x12341300</code></td>
</tr>
<tr>
<td style="text-align: right;">20</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x04</code></td>
<td><code>0x1400…0x1442</code></td>
<td><code>0x12441400</code></td>
</tr>
<tr>
<td style="text-align: right;">21</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x05</code></td>
<td><code>0x1500…0x1542</code></td>
<td><code>0x12541500</code></td>
</tr>
<tr>
<td style="text-align: right;">22</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x06</code></td>
<td><code>0x1600…0x1642</code></td>
<td><code>0x12641600</code></td>
</tr>
<tr>
<td style="text-align: right;">23</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x07</code></td>
<td><code>0x1700…0x1742</code></td>
<td><code>0x12741700</code></td>
</tr>
<tr>
<td style="text-align: right;">24</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x08</code></td>
<td><code>0x1800…0x1842</code></td>
<td><code>0x12841800</code></td>
</tr>
<tr>
<td style="text-align: right;">25</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x09</code></td>
<td><code>0x1900…0x1942</code></td>
<td><code>0x12941900</code></td>
</tr>
<tr>
<td style="text-align: right;">26</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x0A</code></td>
<td><code>0x1A00…0x1A42</code></td>
<td><code>0x12A41A00</code></td>
</tr>
<tr>
<td style="text-align: right;">27</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x0B</code></td>
<td><code>0x1B00…0x1B42</code></td>
<td><code>0x12B41B00</code></td>
</tr>
<tr>
<td style="text-align: right;">28</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x0C</code></td>
<td><code>0x1C00…0x1C42</code></td>
<td><code>0x12C41C00</code></td>
</tr>
<tr>
<td style="text-align: right;">29</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x0D</code></td>
<td><code>0x1D00…0x1D42</code></td>
<td><code>0x12D41D00</code></td>
</tr>
<tr>
<td style="text-align: right;">30</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x0E</code></td>
<td><code>0x1E00…0x1E42</code></td>
<td><code>0x12E41E00</code></td>
</tr>
<tr>
<td style="text-align: right;">31</td>
<td>Резерв назначения</td>
<td><code>0x02 / 0x0F</code></td>
<td><code>0x1F00…0x1F42</code></td>
<td><code>0x12F41F00</code></td>
</tr>
</tbody>
</table>
<h2 id="mode-periph_28335">Назначения mode из periph_28335</h2>
<p>Источники: <code>28335_periph/Source/Internal/Include/package.h</code>, <code>peripher.c:get_Mode()</code>, <code>cntrl_adr.c</code>. Код платы <code>Desk</code> не участвует в вычислении адреса.</p>
<table>
<thead>
<tr>
<th style="text-align: right;">mode</th>
<th>Назначение</th>
<th>Исходный символ</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;">0</td>
<td>Резерв</td>
<td>Перемычками текущей платы не задаётся</td>
</tr>
<tr>
<td style="text-align: right;">1 / 2</td>
<td>Трансформаторные узлы 1 / 2</td>
<td><code>adr_TRN1 / adr_TRN2</code></td>
</tr>
<tr>
<td style="text-align: right;">3 / 4</td>
<td>Силовые узлы 1 / 2</td>
<td><code>adr_POW1 / adr_POW2</code></td>
</tr>
<tr>
<td style="text-align: right;">5 / 6</td>
<td>УМП1 / УМП2</td>
<td><code>adr_LOA1 / adr_LOA2</code>, <code>Desk=dsk_LOAD=6</code></td>
</tr>
<tr>
<td style="text-align: right;">7</td>
<td>ENG1</td>
<td><code>adr_ENG1</code></td>
</tr>
<tr>
<td style="text-align: right;">8</td>
<td>Задатчик / пульт</td>
<td><code>adr_PULT</code>; имя «Задатчик» принято в реестре PERIPH</td>
</tr>
<tr>
<td style="text-align: right;">9</td>
<td>Шкаф</td>
<td><code>adr_SHKF</code></td>
</tr>
<tr>
<td style="text-align: right;">10</td>
<td>Общий адрес, служебная роль</td>
<td><code>CNTRL_ADDR_UNIVERSAL</code>, <code>ADDR_FOR_ALL</code>; не отдельная плата</td>
</tr>
<tr>
<td style="text-align: right;">11</td>
<td>Терминал, служебная роль</td>
<td><code>ADDR_TERMINAL</code>; не доказательство отдельной платы</td>
</tr>
<tr>
<td style="text-align: right;">12…31</td>
<td>Резерв назначения</td>
<td>Перемычки текущей платы дают только 1…16</td>
</tr>
</tbody>
</table>
<h2 id="native-periph">Соответствия native-сервисам PERIPH</h2>
<table>
<thead>
<tr>
<th>Смещение n</th>
<th>Native-адрес</th>
<th>Назначение</th>
<th>Доступ</th>
</tr>
</thead>
<tbody>
<tr>
<td>0…127</td>
<td><code>0x0000…0x007F</code></td>
<td>технологические данные, уставки, команды</td>
<td>по provider</td>
</tr>
<tr>
<td>128…143</td>
<td><code>0x1000…0x100F</code></td>
<td>статус регистратора УМП</td>
<td>R</td>
</tr>
<tr>
<td>144…155</td>
<td><code>0x1200…0x120B</code></td>
<td>паспорт прошивки</td>
<td>R</td>
</tr>
<tr>
<td>156…163</td>
<td><code>0x1300…0x1307</code></td>
<td>защита УМП</td>
<td>W только <code>0x1302/0x1303</code></td>
</tr>
<tr>
<td>164…167</td>
<td><code>0x1000…0x1003</code></td>
<td>команды регистратора</td>
<td>по операции</td>
</tr>
<tr>
<td>168…199</td>
<td>нет</td>
<td>резерв</td>
<td>отказ</td>
</tr>
</tbody>
</table>
<p><code>pcan_gas_slot_native()</code> принимает WordIndex=1…3; WordIndex=0 является маской
и не переводится в регистр. Наличие сервиса, права и блокировку проверяет приложение.
GAS 0x3 сохраняет собственные адреса подключённых регионов; эта проекция относится только к 0x4.
Архив УМП, окна 120/124 слова и физическая EEPROM сохраняют native-контракты.</p>
<h2 id="section">Реализация и реестр</h2>
<ul>
<li>GAS 0x3: <code>pcan_gas.h/.c</code>, существующие ABI и клиенты.</li>
<li>GAS SLOT 0x4: <code>pcan_gas_slot.h/.c</code>; тонкие Python <code>GasSlots</code> и Android/JNI <code>GasSlots</code>.</li>
<li>Формат определяется MsgType, параметр <code>gas_profile</code> не используется.</li>
<li>C28x хранит wire-октеты в <code>uint_least8_t</code>; DLC измеряется октетами линии.</li>
<li>Перенос в другую единицу адресации или fallback после отказа запрещены.</li>
</ul>
<p>XLSX «Интерфейс протокола» содержит оба типа; «Общее адресное пространство» —
проекцию 0x4 и native-адреса её значений; «ОАП legacy» сохраняет исходный словный реестр 0x3.
Уникальный ключ реестра включает узел, MsgType, адрес и для 0x4 WordIndex.
Права, тип, единица, масштаб и порядок слов u32/float32 задаются явно.
Эталоны: <code>examples/gas-slot-vectors.json</code>; регистровые проверки — <code>test_gas_register.c</code>.</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="changed-2026-10-23">Changed — 2026-10-23</h3>
<ul>
<li>GAS 0x3 возвращён к адресации отдельных 16-битных регистров 0…65535, DLC 0/2/4/6/8.</li>
<li>Слотный GAS перенесён на 0x4 вместо MODBUS_COIL; Word0 содержит маску битов 1…3, Word1…Word3 — значения.</li>
<li>PERIPH сохраняет 200 значений на mode в 67 слотах; последний содержит два значения.</li>
<li>Формат выбирается номером MsgType; Python/JNI, STM32, Modbus routing, тесты, XLSX и HTML согласованы.</li>
</ul>
<h3 id="added">Added</h3>
<ul>
<li>Структурированный комплект документации <code>doc/setcan/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 сохранена в templates. Для пересборки запустите <code>doc/setcan/build-html.bat</code>. Нормативный приоритет сохраняют PROTOCOL.md, BOOTLOADER.md и реестр ОАП.</footer>
</main>
<script>
const tabs = [...document.querySelectorAll('[role="tab"]')];
const panels = [...document.querySelectorAll('[role="tabpanel"]')];
function activate(name, focus = false) {
const tab = tabs.find(item => item.dataset.tab === name) || tabs[0];
tabs.forEach(item => item.setAttribute('aria-selected', String(item === tab)));
panels.forEach(panel => panel.classList.toggle('active', panel.id === tab.dataset.tab));
if (focus) tab.focus();
history.replaceState(null, '', '#' + tab.dataset.tab);
}
tabs.forEach((tab, index) => {
tab.addEventListener('click', () => activate(tab.dataset.tab));
tab.addEventListener('keydown', event => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault();
let next = index;
if (event.key === 'ArrowRight') next = (index + 1) % tabs.length;
if (event.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length;
if (event.key === 'Home') next = 0;
if (event.key === 'End') next = tabs.length - 1;
activate(tabs[next].dataset.tab, true);
});
});
document.querySelectorAll('.copy').forEach(button => button.addEventListener('click', async () => {
const code = button.parentElement.querySelector('code').innerText;
try {
await navigator.clipboard.writeText(code);
button.textContent = 'Скопировано';
setTimeout(() => button.textContent = 'Копировать', 1400);
} catch { button.textContent = 'Выделите код'; }
}));
activate(location.hash.slice(1) || 'overview');
</script>
</body>
</html>