Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103, STM32F407 · уровень средний · 23 мин чтения
Открыть в интерактивном справочнике

19б. CANopen: словарь объектов, NMT, PDO и SDO

Раздел 19 довёл CAN до кадров, арбитража и фильтров, раздел 19а — до CAN FD. Но шина только доставляет кадр с идентификатором и восемью байтами. Что значит идентификатор 0x18A, кто имеет право его слать, как узнать у датчика его настройку и как запустить привод — шина не говорит. Это решает прикладной протокол. В промышленной автоматике на CAN чаще всего это CANopen.

Здесь — его основа: как идентификатор делится на код функции и номер узла, что такое словарь объектов, как узел запускают и следят, что он жив, как побайтно выглядит запрос SDO, как PDO укладывает переменные в 8 байт и сколько таких кадров выдержит шина. В конце — узел в миниатюре на C для bxCAN F103 и F407.

Опоры: спецификации CANopen на диске нет, поэтому всё сверено с двумя реализациями. Первая — библиотека python-canopen 2.4.1 из окружения Zephyr и словарь objdict.eds из примера CANopenNode в дереве Zephyr 4.4.2: клиент и сервер библиотеки обменивались по виртуальной шине, кадры перехвачены. Вторая — заголовок mco.h и руководство стека Micro CANopen из пака Keil EmSA MicroCANopenDevice. Длина кадра — по RM0090. Расчёты — _verify/canopen19c/canopen19c.py, узел на C — _verify/canopen19c/co19c.c.

Как это называется в документации

Оригинал Перевод
COB-ID (communication object identifier) идентификатор кадра CANopen
node-ID номер узла, 1…127
object dictionary, index, sub-index словарь объектов, индекс, подындекс
EDS (electronic data sheet) файл-описание словаря узла
NMT (network management) управление сетью: пуск, стоп, сброс
pre-operational, operational, stopped предоперационное, операционное, остановлено
boot-up, heartbeat сообщение о старте, сигнал «жив»
SDO (service data object), client, server доступ к словарю: клиент спрашивает, сервер отвечает
expedited / segmented transfer ускоренная (до 4 байт) и сегментная передача
abort code код отказа SDO
PDO (process data object), TPDO, RPDO кадр данных процесса: передаваемый, принимаемый
PDO mapping отображение объектов в кадр PDO
transmission type тип передачи PDO
SYNC, EMCY синхросигнал, аварийное сообщение

19б.1 Зачем протокол поверх CAN

В разделе 19 было сказано: в кадре CAN нет получателя, есть тема. Для двух своих плат темы можно назначить как угодно. Но если на шине привод одного изготовителя, датчик другого и контроллер третьего, нужен общий договор: какие идентификаторы чьи, как прочитать параметр, как перевести узел в работу. CANopen и есть такой договор.

Главная его идея — идентификатор кадра = код функции + номер узла. Номер узла — 7 бит, от 1 до 127. Код функции — старшие 4 бита из 11 (биты 10:7). Так у каждого узла сразу есть свой набор идентификаторов, и настраивать их не нужно:

Функция COB-ID Узел 10
NMT (команды управления) 0x000 0x000
SYNC (синхросигнал) 0x080 0x080
EMCY (авария) 0x080 + N 0x08A
TPDO1…TPDO4 0x180, 0x280, 0x380, 0x480 + N 0x18A, 0x28A, 0x38A, 0x48A
RPDO1…RPDO4 0x200, 0x300, 0x400, 0x500 + N 0x20A, 0x30A, 0x40A, 0x50A
SDO, ответ сервера 0x580 + N 0x58A
SDO, запрос клиента 0x600 + N 0x60A
heartbeat и boot-up 0x700 + N 0x70A

Все значения для узла 10 совпали с теми, что назначает узлу библиотека python-canopen. В mco.h те же границы: запросы SDO — 0x601…0x67F, heartbeat — 0x701…0x77F (0x77F — узел 127).

Порядок не случаен. Меньший идентификатор выигрывает арбитраж (раздел 19.3), поэтому команда NMT главнее всего, SYNC и аварии — сразу за ней, данные процесса — в середине, служебный доступ SDO и heartbeat — в конце.

Слева вверху — 11 бит COB-ID: четыре старших бита — код функции, семь младших — номер узла; справа — таблица кодов функций NMT, SYNC, EMCY, TPDO, RPDO, SDO и heartbeat с идентификаторами для узла 10. В середине — побайтный разбор ускоренного чтения SDO объекта 0x1017 у узла 10: запрос 0x60A с байтами 40 17 10 00 и четырьмя нулями, ответ 0x58A с байтами 4B 17 10 00 E8 03 00 00, то есть 1000 мс. Внизу — состояния NMT: инициализация, предоперационное, операционное, остановлено, и команды 1, 2, 128 между ними


19б.2 Словарь объектов

Всё, что узел умеет и знает, лежит в словаре объектов — таблице, где у каждой записи 16-битный индекс и 8-битный подындекс. Простые объекты имеют один подындекс 0, составные (записи) — несколько, а в подындексе 0 лежит число остальных.

В EDS примера Zephyr объекты разложены на три группы: обязательные (0x1000, 0x1001, 0x1018), необязательные служебные (от 0x1002 до 0x1F80: COB-ID, heartbeat, параметры SDO и PDO) и объекты изготовителя (0x2100, 0x2101, 0x2102) — туда кладут свои переменные. Вот объекты, с которыми работает этот раздел, — значения по умолчанию из objdict.eds для узла 10:

Объект Что это Тип Доступ По умолчанию
0x1000:00 тип устройства 4 байта чтение 0
0x1001:00 регистр ошибок 1 байт чтение 0
0x1005:00 COB-ID SYNC 4 байта чтение и запись 0x080
0x1014:00 COB-ID EMCY 4 байта чтение 0x08A
0x1017:00 период heartbeat, мс 2 байта чтение и запись 1000
0x1018:00…01 идентификация: число подындексов, код изготовителя 1 и 4 байта чтение 4 и 0
0x1200:01…02 сервер SDO: COB-ID запросов и ответов 4 байта чтение 0x60A и 0x58A
0x1800:01…02 TPDO1: COB-ID и тип передачи 4 и 1 байт чтение и запись 0x18A и 254
0x1A00:00…02 TPDO1: сколько объектов и какие 1 и 4 байта чтение и запись 0
0x2102:00 счётчик нажатий кнопки (объект примера) 4 байта чтение и запись 0

Обратите внимание: у объектов, где COB-ID зависит от номера узла, в EDS записано не число, а формула вроде $NODEID+0x580. Один и тот же файл описывает узел с любым номером.

Словарь — это и документация, и программный интерфейс. Файл EDS отдают вместе с устройством: программа на ПК по нему знает, что у узла есть, какого типа и можно ли это писать.


19б.3 NMT: состояния, boot-up и heartbeat

У каждого узла четыре состояния, и номер состояния он сам сообщает в сеть:

Состояние Номер Что работает
инициализация 0 ничего; при выходе — сообщение boot-up
предоперационное 127 SDO, NMT, heartbeat; PDO нет
операционное 5 всё, включая PDO
остановлено 4 только NMT и heartbeat

Управляет состояниями ведущий NMT — кадром 0x000 из двух байт: код команды и номер узла (0 — всем сразу). Коды: 1 — в операционное, 2 — остановить, 128 — в предоперационное, 129 — сброс узла, 130 — сброс связи. После обоих сбросов узел проходит инициализацию и снова шлёт boot-up. Коды команд и номера состояний взяты из python-canopen; номера состояний те же и в mco.h (NMTSTATE_BOOT 0, NMTSTATE_STOP 4, NMTSTATE_OP 5, NMTSTATE_PREOP 127).

Boot-up — кадр 0x700 + N с одним байтом 0: «я включился». Дальше тот же идентификатор узел шлёт с периодом из объекта 0x1017 — это heartbeat, в байте — текущее состояние. Кто хочет следить за узлом, ждёт его heartbeat: пропал — узел потерян. В mco.h для такого случая есть свой код аварии EMCY_HB_ERR = 0x8130.

Типичный пуск сети показан в руководстве Micro CANopen: ведущий и узел шлют boot-up, ведущий сбрасывает все узлы командой всем, оставляет узел в предоперационном, по SDO записывает ему период heartbeat (0x1017, байты EE 02 — 750 мс) и время ожидания heartbeat ведущего, читает тип устройства и идентификацию и только потом командой NMT переводит узел в операционное — после этого пошли PDO.


19б.4 SDO: ускоренная передача побайтно

SDO — доступ к любому объекту словаря. Спрашивает клиент (обычно ПК или ведущий), отвечает сервер (узел). Запрос идёт на 0x600 + N, ответ — на 0x580 + N, оба кадра всегда по 8 байт:

Если значение умещается в 4 байта, хватает одной пары кадров — это ускоренная передача. Команды:

Что Байт команды
запрос чтения 0x40
ответ на чтение: 4, 3, 2, 1 байт 0x43, 0x47, 0x4B, 0x4F
запрос записи: 4, 3, 2, 1 байт 0x23, 0x27, 0x2B, 0x2F
ответ «записано» 0x60
отказ (с любой стороны) 0x80

В командах ответа и записи зашита длина: 0x43 | ((4 − длина) << 2) — так в mco.h и написан макрос SDOREPLY.

Живой обмен клиента и сервера python-canopen со словарём примера Zephyr (узел 10), кадры перехвачены на шине:

Операция Запрос 0x60A Ответ 0x58A
чтение 0x1017:00 40 17 10 00 00 00 00 00 4B 17 10 00 E8 03 00 00 — 2 байта, 1000
запись 0x1017:00 = 5000 2B 17 10 00 88 13 00 00 60 17 10 00 00 00 00 00
чтение 0x1014:00 40 14 10 00 00 00 00 00 43 14 10 00 8A 00 00 00 — 0x08A
чтение 0x1234:00 40 34 12 00 00 00 00 00 80 34 12 00 00 00 02 06 — отказ 0x06020000
чтение 0x1018:07 40 18 10 07 00 00 00 00 80 18 10 07 11 00 09 06 — отказ 0x06090011
запись 0x1000:00 = 1 23 00 10 00 01 00 00 00 80 00 10 00 02 00 01 06 — отказ 0x06010002
запись 0x1017:00 четырьмя байтами 23 17 10 00 01 00 00 00 80 17 10 00 10 00 07 06 — отказ 0x06070010

Код отказа — 4 байта в поле данных, младшим вперёд. Те же коды есть и в python-canopen, и в mco.h: 0x06020000 — нет такого объекта, 0x06090011 — нет такого подындекса, 0x06010002 — объект только для чтения, 0x06070010 — длина не совпадает с типом, 0x05040001 — неизвестная команда.

Свой кодировщик запросов в модели дал те же байты, что и библиотека.

Значение длиннее 4 байт идёт сегментами. Имя устройства 0x1008 в словаре примера — строка «Zephyr RTOS/CANopenNode», 23 байта. Сервер ответил командой 0x41 и длиной 0x17 в поле данных, а потом клиент забрал её четырьмя сегментами по 7 байт и меньше, чередуя команды 0x60 и 0x70. Вышло 5 пар кадров вместо одной: ускоренная передача намного дешевле, и для настройки узла её обычно и хватает.


19б.5 PDO и SYNC: данные процесса в 8 байтах

SDO — вопрос и ответ с заголовком из 4 байт. Для данных, которые идут постоянно (положение, ток, состояние входов), это дорого. PDO — кадр без заголовка: все 8 байт — данные, а что в них лежит, заранее записано в словарь.

Отображение. Для TPDO1 это объект 0x1A00: в подындексе 0 — число объектов, в подындексах 1, 2… — слова «индекс, подындекс, длина в битах»:

Объект в кадре Слово отображения
0x2102:00, 32 бита 0x21020020
0x1001:00, 8 бит 0x10010008

Объекты укладываются в кадр подряд, младшим байтом вперёд. Эти два дают 40 бит = 5 байт. При счётчике 7 и нулевом регистре ошибок кадр TPDO1 — 18A: 07 00 00 00 00; клиент python-canopen разобрал его своей картой обратно в 7 и 0. Больше 64 бит в классический кадр не влезает: если добавить ещё 0x1002 (32 бита), выходит 72 бита — и библиотека это молча посчитала, не возразив. Проверять сумму приходится самому.

Порядок настройки. Как библиотека настроила TPDO1 по SDO — видно по кадрам: сначала записала в 0x1800:01 значение 0x8000018A (бит 31 — «PDO не действует»), потом тип передачи 1, обнулила 0x1A00:00, записала два слова отображения, поставила их число 2 и последним вернула COB-ID 0x18A без бита 31. Семь ускоренных записей, все с ответом 0x60.

Тип передачи (0x1800:02) решает, когда кадр уходит. Значения 1…240 — синхронные: кадр уходит по SYNC (в python-canopen граница — 0xF0); 0 — тоже по SYNC, но только если данные изменились. 254 и 255 — по событию и таймеру. SYNC — кадр 0x080 без данных, его шлёт один узел сети; все синхронные TPDO снимают значения по одному сигналу, и данные разных узлов относятся к одному моменту.

Сколько выдержит шина. Длина кадра со стандартным идентификатором — 44 + 8n бит (RM0090, рис. 347), плюс 3 бита паузы между кадрами. Сверх того передатчик вставляет бит после каждых пяти одинаковых: больше пяти подряд — ошибка вставки (так описан код ошибки в RM0440). Вставка идёт от SOF до конца CRC — это 34 + 8n бит (с разделителем CRC было бы 35 + 8n, худший случай от этого не меняется). Худшее число вставок модель нашла перебором всех последовательностей и динамикой: оно равно (L − 1)/4, округлённому вниз.

Данных Без вставок Худший случай 125 кбит/с 250 кбит/с 500 кбит/с
0 байт (SYNC) 47 бит 55 бит 440 мкс 220 мкс 110 мкс
1 байт (heartbeat) 55 бит 65 бит 520 мкс 260 мкс 130 мкс
5 байт 87 бит 105 бит 840 мкс 420 мкс 210 мкс
8 байт 111 бит 135 бит 1080 мкс 540 мкс 270 мкс

Если шина занята только 8-байтовыми кадрами, в секунду их проходит 925, 1851 и 3703 на 125, 250 и 500 кбит/с. Загрузка для нескольких сетей в худшем случае вставок, с SYNC каждые 10 мс и heartbeat раз в секунду:

Сеть Кадров PDO в секунду 125 кбит/с 250 кбит/с 500 кбит/с
4 узла × 2 PDO по 8 байт, каждые 10 мс 800 91,0 % 45,5 % 22,8 %
10 узлов × 2 PDO по 8 байт, каждые 10 мс 2000 220,9 % 110,5 % 55,2 %
10 узлов × 4 PDO по 8 байт, каждые 100 мс 400 48,1 % 24,1 % 12,0 %
30 узлов × 1 PDO по 5 байт, каждые 10 мс 3000 258,0 % 129,0 % 64,5 %

Больше 100 % — сеть не работает: кадры с меньшим приоритетом не уходят никогда. Считать это нужно до выбора скорости.


19б.6 Узел в миниатюре на C

Узел ниже умеет самое нужное: boot-up и heartbeat, команды NMT, ускоренное чтение и запись SDO из маленькой таблицы словаря (15 записей из 19б.2) и TPDO1 по SYNC (тип N — каждый N-й SYNC). От железа он не зависит: кадр — структура с идентификатором, длиной и 8 байтами. Разбор идёт по COB-ID:

int co_rx(const co_frame *in, co_frame *out)
{
    if (in->id == CO_COB_NMT) {
        return nmt_command(in, out);
    }
    if (in->id == CO_COB_SYNC) {
        return sync_received(out);
    }
    if (in->id == CO_COB_SDO_RX && state != NMT_STOPPED) {
        return sdo_server(in, out);             /* остановленный узел слушает только NMT */
    }
    return 0;                                   /* чужие кадры не наши */
}

Сервер SDO: команда в старших трёх битах байта 0, длина записи — в битах 3:2, отказ — с кодом из таблицы 19б.4:

    switch (r[0] & 0xE0u) {
    case 0x40u:                                 /* чтение (upload) */
        e = od_find(index, r[3], &abort);
        if (e) {
            sdo_reply(out, (uint8_t)(0x43u | ((4u - e->size) << 2)), r, e->value, e->size);
            return 1;
        }
        break;
    case 0x20u:                                 /* запись (download) */
        if ((r[0] & 0x02u) == 0u) {
            break;                              /* сегментная: здесь не поддержана */
        }
        e = od_find(index, r[3], &abort);
        if (!e) {
            break;
        }
        n = (r[0] & 0x01u) ? 4u - ((r[0] >> 2) & 3u) : 4u;
        if (e->access != RW) {
            abort = 0x06010002u;                /* только для чтения */
            break;
        }
        if (n != e->size) {
            abort = 0x06070010u;                /* длина не совпадает с типом */
            break;
        }
        e->value = get_le(&r[4], n);
        sdo_reply(out, 0x60u, r, 0u, 0u);
        return 1;

Связь с bxCAN — отдельный файл co19c_bxcan.c, одинаковый для F103 и F407. Фильтр — один банк в режиме списка из четырёх 16-битных идентификаторов (раздел 19.5): узлу нужны только NMT, SYNC и свои запросы SDO, остальное отсеивает железо. В 16-битном фильтре идентификатор стоит в битах 15:5:

    CAN1->FS1R &= ~1u;                          /* четыре 16-битных фильтра */
    CAN1->FM1R |= 1u;                           /* режим списка */
    CAN1->sFilterRegister[0].FR1 = (FILTER16(CO_COB_SYNC) << 16) | FILTER16(CO_COB_NMT);
    CAN1->sFilterRegister[0].FR2 = (FILTER16(CO_COB_SDO_RX) << 16) | FILTER16(CO_COB_SDO_RX);

Оба файла собраны armclang под F103 и F407 без предупреждений; _Static_assert проверяют номер узла (1…127), что COB-ID умещается в 11 бит, размер кадра и записи словаря (12 байт) и формат слова отображения. Узел занимает 1652 байта кода, словарь — 180 байт ОЗУ.

Тот же co19c.c собран gcc на ПК и получил 44 кадра: все 19 ускоренных запросов из живого обмена python-canopen (включая семь записей настройки TPDO1), команды NMT, SYNC и испорченные кадры. Все 19 ответов SDO совпали с ответами сервера библиотеки байт в байт. Состояния после команд NMT совпали с таблицей библиотеки. По SYNC в операционном узел выдал 18A: 07 00 00 00 00 — тот же кадр, что в 19б.5; в предоперационном и остановленном — промолчал. Неизвестная команда 0xE0 получила отказ 0x05040001.


19б.7 Грабли

SDO отвечает, а PDO нет. Узел в предоперационном: там PDO не работают. Нужна команда NMT 1. В прогоне SYNC в предоперационном остался без ответа.

Индекс записан старшим байтом вперёд. Запрос 40 10 17 … спрашивает объект 0x1710, а не 0x1017. Ответ — отказ 0x06020000.

Записали 4 байта в 16-битный объект. Период heartbeat — 2 байта; запись командой 0x23 получила отказ 0x06070010. Команда должна нести длину объекта: для 2 байт — 0x2B.

Карта PDO больше 64 бит. Библиотека посчитала 72 бита и не возразила. Кадр не соберётся; для такого отказа у SDO есть свой код 0x06040042 — «превышена длина PDO». Сумму длин считать до записи.

Карту поменяли на ходу. Правильный порядок — выключить PDO битом 31 в COB-ID, обнулить число объектов, записать карту, вернуть число и включить PDO. Так делает библиотека.

Два узла с одним номером. У них одинаковые COB-ID; арбитраж не разводит одинаковые идентификаторы, и оба дойдут до данных одновременно — на первом несовпавшем бите будет ошибка. Номер узла — уникален в сети.

Heartbeat потеряли за нагрузкой. Heartbeat — самый младший по приоритету кадр. При загрузке больше 100 % он не уходит, и узел «умирает» для всех, хотя работает. В таблице 19б.5 10 узлов × 2 PDO каждые 10 мс занимают 110,5 % шины на 250 кбит/с.


19б.8 Итоги

Полные исходники

co19c.c — узел CANopen: NMT, ускоренный SDO, TPDO1 по SYNC.

/* Раздел 19б — узел CANopen в миниатюре: разбор кадра по COB-ID, NMT,
   ускоренный SDO из таблицы словаря, TPDO1 по SYNC. От железа не зависит:
   тот же файл собирается armclang для F103/F407 и gcc на ПК для проверки. */
#include "co19c.h"

enum { NMT_INIT = 0, NMT_STOPPED = 4, NMT_OPERATIONAL = 5, NMT_PREOP = 127 };
enum { RO = 0, RW = 1 };

#define MAP(index, sub, bits) (((uint32_t)(index) << 16) | ((uint32_t)(sub) << 8) | (bits))

_Static_assert(CO_NODE_ID >= 1u && CO_NODE_ID <= 127u, "номер узла: 7 бит, 1…127");
_Static_assert(CO_COB_HB <= 0x7FFu, "COB-ID помещается в 11 бит");
_Static_assert(sizeof(co_frame) == 12u, "кадр: id, len, 8 байт данных и выравнивание");
_Static_assert(MAP(0x2102, 0, 32) == 0x21020020u, "отображение: индекс, подындекс, длина в битах");

typedef struct {
    uint16_t index;
    uint8_t  sub;
    uint8_t  size;                              /* байт: 1, 2 или 4 */
    uint8_t  access;
    uint32_t value;
} od_entry;

static od_entry od[] = {
    { 0x1000, 0, 4, RO, 0x00000000u },          /* тип устройства */
    { 0x1001, 0, 1, RO, 0x00u },                /* регистр ошибок */
    { 0x1005, 0, 4, RW, CO_COB_SYNC },          /* COB-ID SYNC */
    { 0x1014, 0, 4, RO, CO_COB_EMCY },          /* COB-ID EMCY */
    { 0x1017, 0, 2, RW, 1000u },                /* период heartbeat, мс */
    { 0x1018, 0, 1, RO, 4u },                   /* идентификация: подындексов */
    { 0x1018, 1, 4, RO, 0x00000000u },          /* код изготовителя */
    { 0x1200, 1, 4, RO, CO_COB_SDO_RX },        /* сервер SDO: COB-ID запросов */
    { 0x1200, 2, 4, RO, CO_COB_SDO_TX },        /* ... и ответов */
    { 0x1800, 1, 4, RW, CO_COB_TPDO1 },         /* TPDO1: COB-ID, бит 31 — выключен */
    { 0x1800, 2, 1, RW, 254u },                 /* TPDO1: тип передачи */
    { 0x1A00, 0, 1, RW, 0u },                   /* TPDO1: сколько объектов */
    { 0x1A00, 1, 4, RW, 0u },                   /* TPDO1: объект 1 */
    { 0x1A00, 2, 4, RW, 0u },                   /* TPDO1: объект 2 */
    { 0x2102, 0, 4, RW, 0u },                   /* счётчик нажатий кнопки */
};
#define OD_N (sizeof od / sizeof od[0])

_Static_assert(sizeof(od_entry) == 12u, "запись словаря: 12 байт вместе с выравниванием");

static uint8_t state = NMT_INIT;
static uint8_t sync_count;

static od_entry *od_find(uint16_t index, uint8_t sub, uint32_t *abort)
{
    uint32_t code = 0x06020000u;                /* нет такого объекта */
    for (uint32_t i = 0; i < OD_N; i++) {
        if (od[i].index == index) {
            if (od[i].sub == sub) {
                return &od[i];
            }
            code = 0x06090011u;                 /* объект есть, подындекса нет */
        }
    }
    *abort = code;
    return 0;
}

static void put_le(uint8_t *p, uint32_t v, uint32_t n)
{
    for (uint32_t i = 0; i < n; i++) {
        p[i] = (uint8_t)(v >> (8u * i));
    }
}

static uint32_t get_le(const uint8_t *p, uint32_t n)
{
    uint32_t v = 0;
    for (uint32_t i = 0; i < n; i++) {
        v |= (uint32_t)p[i] << (8u * i);
    }
    return v;
}

static void sdo_reply(co_frame *out, uint8_t cmd, const uint8_t *req, uint32_t v, uint32_t n)
{
    out->id = CO_COB_SDO_TX;
    out->len = 8u;
    out->d[0] = cmd;
    out->d[1] = req[1];                         /* индекс и подындекс — */
    out->d[2] = req[2];                         /* как в запросе */
    out->d[3] = req[3];
    put_le(&out->d[4], 0u, 4u);
    put_le(&out->d[4], v, n);
}

static int sdo_server(const co_frame *in, co_frame *out)
{
    const uint8_t *r = in->d;
    uint16_t index = (uint16_t)(r[1] | (r[2] << 8));
    uint32_t abort = 0x05040001u;               /* команда не поддержана */
    od_entry *e;
    uint32_t n;

    if (in->len != 8u) {
        return 0;
    }
    switch (r[0] & 0xE0u) {
    case 0x40u:                                 /* чтение (upload) */
        e = od_find(index, r[3], &abort);
        if (e) {
            sdo_reply(out, (uint8_t)(0x43u | ((4u - e->size) << 2)), r, e->value, e->size);
            return 1;
        }
        break;
    case 0x20u:                                 /* запись (download) */
        if ((r[0] & 0x02u) == 0u) {
            break;                              /* сегментная: здесь не поддержана */
        }
        e = od_find(index, r[3], &abort);
        if (!e) {
            break;
        }
        n = (r[0] & 0x01u) ? 4u - ((r[0] >> 2) & 3u) : 4u;
        if (e->access != RW) {
            abort = 0x06010002u;                /* только для чтения */
            break;
        }
        if (n != e->size) {
            abort = 0x06070010u;                /* длина не совпадает с типом */
            break;
        }
        e->value = get_le(&r[4], n);
        sdo_reply(out, 0x60u, r, 0u, 0u);
        return 1;
    case 0x80u:                                 /* клиент прервал: молчать */
        return 0;
    default:
        break;
    }
    sdo_reply(out, 0x80u, r, abort, 4u);
    return 1;
}

static int tpdo1_build(co_frame *out)
{
    uint32_t dummy, len = 0;
    uint32_t cob = od_find(0x1800, 1, &dummy)->value;
    uint32_t count = od_find(0x1A00, 0, &dummy)->value;

    if ((cob & 0x80000000u) || count == 0u || count > 2u) {
        return 0;                               /* PDO выключен или карта пуста */
    }
    for (uint8_t k = 1; k <= count; k++) {
        uint32_t m = od_find(0x1A00, k, &dummy)->value;
        od_entry *e = od_find((uint16_t)(m >> 16), (uint8_t)(m >> 8), &dummy);
        uint32_t bytes = (m & 0xFFu) / 8u;
        if (!e || bytes != e->size || len + bytes > 8u) {
            return 0;                           /* в кадр больше 8 байт не влезет */
        }
        put_le(&out->d[len], e->value, bytes);
        len += bytes;
    }
    out->id = (uint16_t)(cob & 0x7FFu);
    out->len = (uint8_t)len;
    return 1;
}

static int sync_received(co_frame *out)
{
    uint32_t dummy;
    uint32_t type = od_find(0x1800, 2, &dummy)->value;

    if (state != NMT_OPERATIONAL || type < 1u || type > 240u) {
        return 0;                               /* PDO — только в операционном */
    }
    if (++sync_count < type) {
        return 0;                               /* тип N: каждый N-й SYNC */
    }
    sync_count = 0;
    return tpdo1_build(out);
}

static int nmt_command(const co_frame *in, co_frame *out)
{
    if (in->len != 2u || (in->d[1] != 0u && in->d[1] != CO_NODE_ID)) {
        return 0;                               /* 0 — всем узлам */
    }
    switch (in->d[0]) {
    case 1u:   state = NMT_OPERATIONAL; break;
    case 2u:   state = NMT_STOPPED;     break;
    case 128u: state = NMT_PREOP;       break;
    case 129u:                                  /* сброс узла */
    case 130u:                                  /* сброс связи */
        co_boot(out);
        return 1;
    default:   break;
    }
    return 0;
}

void co_boot(co_frame *out)
{
    out->id = CO_COB_HB;
    out->len = 1u;
    out->d[0] = NMT_INIT;                       /* boot-up: heartbeat с нулём */
    state = NMT_PREOP;
    sync_count = 0;
}

int co_rx(const co_frame *in, co_frame *out)
{
    if (in->id == CO_COB_NMT) {
        return nmt_command(in, out);
    }
    if (in->id == CO_COB_SYNC) {
        return sync_received(out);
    }
    if (in->id == CO_COB_SDO_RX && state != NMT_STOPPED) {
        return sdo_server(in, out);             /* остановленный узел слушает только NMT */
    }
    return 0;                                   /* чужие кадры не наши */
}

void co_heartbeat(co_frame *out)
{
    out->id = CO_COB_HB;
    out->len = 1u;
    out->d[0] = state;
}

uint16_t co_heartbeat_ms(void)
{
    uint32_t dummy;
    return (uint16_t)od_find(0x1017, 0, &dummy)->value;
}

uint8_t co_state(void)
{
    return state;
}

co19c_bxcan.c — узел поверх bxCAN F103 и F407: фильтр и опрос очереди.

/* Раздел 19б — узел CANopen поверх bxCAN (F103 и F407): фильтр и опрос очереди. */
#if defined(STM32F10X_MD)
#include "stm32f10x.h"
#else
#include "stm32f4xx.h"
#endif
#include "co19c.h"

#define FILTER16(id) ((uint32_t)(id) << 5)      /* STID[10:0] в битах 15:5 */

_Static_assert(FILTER16(CO_COB_SDO_RX) <= 0xFFFFu, "16-битный фильтр вмещает COB-ID");

void co_can_filters(void)
{
    CAN1->FMR |= CAN_FMR_FINIT;
    CAN1->FA1R &= ~1u;                          /* банк 0 выключить на время правки */
    CAN1->FS1R &= ~1u;                          /* четыре 16-битных фильтра */
    CAN1->FM1R |= 1u;                           /* режим списка */
    CAN1->sFilterRegister[0].FR1 = (FILTER16(CO_COB_SYNC) << 16) | FILTER16(CO_COB_NMT);
    CAN1->sFilterRegister[0].FR2 = (FILTER16(CO_COB_SDO_RX) << 16) | FILTER16(CO_COB_SDO_RX);
    CAN1->FFA1R &= ~1u;                         /* в очередь 0 */
    CAN1->FA1R |= 1u;
    CAN1->FMR &= ~CAN_FMR_FINIT;
}

static void co_can_send(const co_frame *f)
{
    if ((CAN1->TSR & CAN_TSR_TME0) == 0u) {
        return;                                 /* ящик 0 занят: кадр теряется */
    }
    CAN1->sTxMailBox[0].TDTR = f->len;
    CAN1->sTxMailBox[0].TDLR = (uint32_t)f->d[0] | ((uint32_t)f->d[1] << 8)
                             | ((uint32_t)f->d[2] << 16) | ((uint32_t)f->d[3] << 24);
    CAN1->sTxMailBox[0].TDHR = (uint32_t)f->d[4] | ((uint32_t)f->d[5] << 8)
                             | ((uint32_t)f->d[6] << 16) | ((uint32_t)f->d[7] << 24);
    CAN1->sTxMailBox[0].TIR = ((uint32_t)f->id << 21) | CAN_TI0R_TXRQ;
}

void co_can_poll(void)
{
    co_frame in, out;
    uint32_t lo, hi;

    if ((CAN1->RF0R & CAN_RF0R_FMP0) == 0u) {
        return;
    }
    in.id = (uint16_t)(CAN1->sFIFOMailBox[0].RIR >> 21);
    in.len = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 0xFu);
    lo = CAN1->sFIFOMailBox[0].RDLR;
    hi = CAN1->sFIFOMailBox[0].RDHR;
    CAN1->RF0R |= CAN_RF0R_RFOM0;               /* освободить место в очереди */
    for (uint32_t i = 0; i < 4u; i++) {
        in.d[i] = (uint8_t)(lo >> (8u * i));
        in.d[i + 4u] = (uint8_t)(hi >> (8u * i));
    }
    if (co_rx(&in, &out)) {
        co_can_send(&out);
    }
}

Дальше: раздел 19в «ISO-TP (ISO 15765-2): длинные сообщения поверх CAN».

Открыть в интерактивном справочнике