Files
templates/doc/setcan/index.html

1204 lines
61 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…0x7</td><td>MODBUS</td><td>Coil, 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>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/setcan/build-html.bat
</code></pre>
<p>Все документы и тестовые векторы включаются в единый файл
<code>doc/setcan/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="../../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/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>