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 — в конце.
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 байт:
- байт 0 — команда;
- байты 1…2 — индекс, младшим байтом вперёд: 0x1017 идёт как 17 10;
- байт 3 — подындекс;
- байты 4…7 — данные до 4 байт, тоже младшим вперёд.
Если значение умещается в 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 Итоги
- COB-ID = код функции (биты 10:7) + номер узла 1…127: NMT 0x000, SYNC 0x080, EMCY 0x080 + N, TPDO1 0x180 + N, SDO 0x580/0x600 + N, heartbeat 0x700 + N.
- Словарь объектов — индекс 16 бит и подындекс 8 бит; с 0x1000 — служебные объекты, с 0x2000 — свои.
- Состояния NMT: 0, 127, 5, 4; команды 1, 2, 128, 129, 130 кадром 0x000 из 2 байт.
- Ускоренный SDO: чтение 0x40, ответ 0x43…0x4F, запись 0x23…0x2F, подтверждение 0x60, отказ 0x80 с 4-байтным кодом.
- PDO — до 64 бит данных без заголовка; слово отображения — индекс, подындекс, длина в битах (0x21020020).
- Кадр из 8 байт — 111 бит, в худшем случае 135: 270 мкс на 500 кбит/с, не больше 3703 кадров в секунду.
- Узел в миниатюре — 1652 байта кода; его ответы SDO совпали с python-canopen во всех 19 случаях.
Полные исходники
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».