1204 lines
61 KiB
HTML
1204 lines
61 KiB
HTML
<!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(&hcan, &hrtc, &htim2) != PROTOCAN_INIT_OK) {
|
||
Error_Handler();
|
||
}
|
||
if (HAL_CAN_Start(&hcan) != HAL_OK) {
|
||
Error_Handler();
|
||
}
|
||
if (HAL_CAN_ActivateNotification(
|
||
&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) {
|
||
Error_Handler();
|
||
}
|
||
if (HAL_TIM_Base_Start_IT(&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 == &hcan) {
|
||
ProtoCanRxFifo0MsgPendingCallback(hcan_ptr);
|
||
}
|
||
}
|
||
|
||
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim_ptr)
|
||
{
|
||
if (htim_ptr == &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->Operation) {
|
||
case PROTOCAN_SETTINGS_GET:
|
||
/* Загрузить ROM из EEPROM в current_rom. */
|
||
return PROTOCAN_SEND_SETTINGS_RESPONSE(
|
||
message->AssemblySerial, message->Position, current_rom);
|
||
|
||
case PROTOCAN_SETTINGS_WRITE:
|
||
/* Проверить CRC/наличие и атомарно записать message->Rom. */
|
||
return PROTOCAN_SEND_SETTINGS_RESPONSE(
|
||
message->AssemblySerial, message->Position, message->Rom);
|
||
|
||
case PROTOCAN_SETTINGS_CLEAR:
|
||
/* Очистить запись; current_rom должен содержать нули. */
|
||
return PROTOCAN_SEND_SETTINGS_RESPONSE(
|
||
message->AssemblySerial, message->Position, current_rom);
|
||
|
||
default:
|
||
return PROTOCAN_SEND_SETTINGS_ERROR(
|
||
message->AssemblySerial, message->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 << 28) |
|
||
((uint32_t)route << 27) |
|
||
((uint32_t)device_type << 24) |
|
||
((uint32_t)device_id << 20) |
|
||
((uint32_t)msg_type << 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 ─> 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 -> target=B -> verify -> pending=B
|
||
active=B -> target=A -> verify -> 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>{
|
||
"schema_version": 1,
|
||
"byte_order": "little-endian",
|
||
"frames": [
|
||
{
|
||
"name": "enter_boot_session_7",
|
||
"direction": "pm_to_device",
|
||
"priority": 1,
|
||
"route": 0,
|
||
"device_type": 3,
|
||
"device_id": 5,
|
||
"msg_type": 9,
|
||
"msg_body": 1794,
|
||
"can_id_hex": "0x13590702",
|
||
"dlc": 0,
|
||
"data_hex": ""
|
||
},
|
||
{
|
||
"name": "slot_b_block_1",
|
||
"direction": "pm_to_device",
|
||
"priority": 1,
|
||
"route": 0,
|
||
"device_type": 3,
|
||
"device_id": 5,
|
||
"msg_type": 11,
|
||
"msg_body": 1,
|
||
"can_id_hex": "0x135B0001",
|
||
"dlc": 8,
|
||
"data_hex": "1011121314151617"
|
||
},
|
||
{
|
||
"name": "enter_boot_ok",
|
||
"direction": "device_to_pm",
|
||
"priority": 1,
|
||
"route": 1,
|
||
"device_type": 3,
|
||
"device_id": 5,
|
||
"msg_type": 12,
|
||
"msg_body": 1794,
|
||
"can_id_hex": "0x1B5C0702",
|
||
"dlc": 8,
|
||
"data_hex": "00FF000000000000"
|
||
}
|
||
]
|
||
}
|
||
</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>
|