Embedded HandbookApps
All sections › III. Peripherals
STM32F103, STM32F407 · level intermediate · 23 min read
Open in the interactive handbook

19б. CANopen: object dictionary, NMT, PDO and SDO

Section 19 took CAN as far as frames, arbitration and filters, section 19а — as far as CAN FD. But the bus only delivers a frame with an identifier and eight bytes. What identifier 0x18A means, who is allowed to send it, how to ask a sensor for its setting and how to start a drive — the bus does not say. That is solved by an application protocol. In industrial automation on CAN it is most often CANopen.

Here are its basics: how the identifier is split into a function code and a node number, what an object dictionary is, how a node is started and how you watch that it is alive, what an SDO request looks like byte by byte, how a PDO packs variables into 8 bytes and how many such frames the bus can carry. At the end — a node in miniature in C for bxCAN on the F103 and F407.

Sources: the CANopen specification is not on disk, so everything was cross-checked against two implementations. The first is the python-canopen 2.4.1 library from the Zephyr environment and the dictionary objdict.eds from the CANopenNode example in the Zephyr 4.4.2 tree: the library's client and server exchanged data over a virtual bus, and the frames were captured. The second is the header mco.h and the manual of the Micro CANopen stack from the Keil EmSA MicroCANopenDevice pack. The frame length is from RM0090. Calculations — _verify/canopen19c/canopen19c.py, the node in C — _verify/canopen19c/co19c.c.

Terms used in the documentation

Term Meaning
COB-ID (communication object identifier) the identifier of a CANopen frame
node-ID the node number, 1…127
object dictionary, index, sub-index the table of a node's objects and their addresses
EDS (electronic data sheet) a file describing the node's dictionary
NMT (network management) network control: start, stop, reset
pre-operational, operational, stopped the three working states of a node
boot-up, heartbeat the startup message, the "alive" signal
SDO (service data object), client, server dictionary access: the client asks, the server answers
expedited / segmented transfer a fast transfer (up to 4 bytes) and a transfer in segments
abort code the SDO refusal code
PDO (process data object), TPDO, RPDO a process data frame: transmitted, received
PDO mapping placing objects into a PDO frame
transmission type when a PDO is sent
SYNC, EMCY the synchronization signal, the emergency message

19б.1 Why a protocol on top of CAN

Section 19 said: a CAN frame has no recipient, it has a topic. For two boards of your own, topics can be assigned any way you like. But if the bus carries a drive from one manufacturer, a sensor from another and a controller from a third, you need a common agreement: which identifiers belong to whom, how to read a parameter, how to bring a node into operation. CANopen is exactly such an agreement.

Its main idea is frame identifier = function code + node number. The node number is 7 bits, from 1 to 127. The function code is the upper 4 bits of the 11 (bits 10:7). So every node immediately has its own set of identifiers, and there is no need to configure them:

Function COB-ID Node 10
NMT (control commands) 0x000 0x000
SYNC (synchronization signal) 0x080 0x080
EMCY (emergency) 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, server response 0x580 + N 0x58A
SDO, client request 0x600 + N 0x60A
heartbeat and boot-up 0x700 + N 0x70A

All the values for node 10 matched the ones the python-canopen library assigns to the node. mco.h has the same bounds: SDO requests — 0x601…0x67F, heartbeat — 0x701…0x77F (0x77F is node 127).

The order is not accidental. The lower identifier wins arbitration (section 19.3), so the NMT command has the top priority, SYNC and emergencies come right after it, process data is in the middle, and service access via SDO and heartbeat are at the end.

Top left — the 11 bits of a COB-ID: the four upper bits are the function code, the seven lower ones are the node number; on the right — a table of the function codes NMT, SYNC, EMCY, TPDO, RPDO, SDO and heartbeat with the identifiers for node 10. In the middle — a byte-by-byte breakdown of an expedited SDO read of object 0x1017 from node 10: request 0x60A with bytes 40 17 10 00 and four zeros, response 0x58A with bytes 4B 17 10 00 E8 03 00 00, that is 1000 ms. At the bottom — the NMT states: initialization, pre-operational, operational, stopped, and the commands 1, 2, 128 between them


19б.2 The object dictionary

Everything a node can do and knows lives in the object dictionary — a table where each entry has a 16-bit index and an 8-bit sub-index. Simple objects have a single sub-index 0; compound ones (records) have several, and sub-index 0 holds the number of the others.

In the EDS of the Zephyr example the objects are split into three groups: mandatory (0x1000, 0x1001, 0x1018), optional communication ones (from 0x1002 to 0x1F80: COB-IDs, heartbeat, SDO and PDO parameters) and manufacturer objects (0x2100, 0x2101, 0x2102) — that is where you put your own variables. Here are the objects this section works with — the default values from objdict.eds for node 10:

Object What it is Type Access Default
0x1000:00 device type 4 bytes read 0
0x1001:00 error register 1 byte read 0
0x1005:00 COB-ID SYNC 4 bytes read and write 0x080
0x1014:00 COB-ID EMCY 4 bytes read 0x08A
0x1017:00 heartbeat period, ms 2 bytes read and write 1000
0x1018:00…01 identity: number of sub-indices, vendor ID 1 and 4 bytes read 4 and 0
0x1200:01…02 SDO server: COB-IDs of requests and responses 4 bytes read 0x60A and 0x58A
0x1800:01…02 TPDO1: COB-ID and transmission type 4 and 1 bytes read and write 0x18A and 254
0x1A00:00…02 TPDO1: how many objects and which 1 and 4 bytes read and write 0
0x2102:00 button press counter (an object of the example) 4 bytes read and write 0

Note: for objects whose COB-ID depends on the node number, the EDS contains not a number but a formula like $NODEID+0x580. One and the same file describes a node with any number.

The dictionary is both documentation and a programming interface. The EDS file is shipped with the device: from it, a PC program knows what the node has, of what type, and whether it can be written.


19б.3 NMT: states, boot-up and heartbeat

Every node has four states, and the node itself reports its state number to the network:

State Number What works
initialization 0 nothing; on exit — the boot-up message
pre-operational 127 SDO, NMT, heartbeat; no PDO
operational 5 everything, including PDO
stopped 4 only NMT and heartbeat

The states are controlled by the NMT master — with frame 0x000 of two bytes: the command code and the node number (0 — all at once). The codes: 1 — to operational, 2 — stop, 128 — to pre-operational, 129 — reset node, 130 — reset communication. After either reset the node goes through initialization and sends boot-up again. The command codes and state numbers are taken from python-canopen; the state numbers are the same in mco.h (NMTSTATE_BOOT 0, NMTSTATE_STOP 4, NMTSTATE_OP 5, NMTSTATE_PREOP 127).

Boot-up is frame 0x700 + N with a single byte 0: "I have powered up". After that the node sends the same identifier with the period from object 0x1017 — this is the heartbeat, and the byte holds the current state. Whoever wants to watch a node waits for its heartbeat: if it disappears, the node is lost. mco.h has its own emergency code for this case, EMCY_HB_ERR = 0x8130.

A typical network startup is shown in the Micro CANopen manual: the master and the node send boot-up, the master resets all nodes with a broadcast command, leaves the node in pre-operational, writes the heartbeat period to it via SDO (0x1017, bytes EE 02 — 750 ms) and the timeout for the master's heartbeat, reads the device type and identity, and only then switches the node to operational with an NMT command — after which the PDOs start flowing.


19б.4 SDO: expedited transfer byte by byte

SDO is access to any object of the dictionary. The client asks (usually a PC or the master), the server (the node) answers. The request goes to 0x600 + N, the response — to 0x580 + N; both frames are always 8 bytes long:

If the value fits in 4 bytes, one pair of frames is enough — this is an expedited transfer. The commands:

What Command byte
read request 0x40
read response: 4, 3, 2, 1 bytes 0x43, 0x47, 0x4B, 0x4F
write request: 4, 3, 2, 1 bytes 0x23, 0x27, 0x2B, 0x2F
"written" response 0x60
abort (from either side) 0x80

The length is encoded in the response and write commands: 0x43 | ((4 − length) << 2) — that is how the SDOREPLY macro is written in mco.h.

A live exchange between the python-canopen client and server with the dictionary of the Zephyr example (node 10), frames captured on the bus:

Operation Request 0x60A Response 0x58A
read 0x1017:00 40 17 10 00 00 00 00 00 4B 17 10 00 E8 03 00 00 — 2 bytes, 1000
write 0x1017:00 = 5000 2B 17 10 00 88 13 00 00 60 17 10 00 00 00 00 00
read 0x1014:00 40 14 10 00 00 00 00 00 43 14 10 00 8A 00 00 00 — 0x08A
read 0x1234:00 40 34 12 00 00 00 00 00 80 34 12 00 00 00 02 06 — abort 0x06020000
read 0x1018:07 40 18 10 07 00 00 00 00 80 18 10 07 11 00 09 06 — abort 0x06090011
write 0x1000:00 = 1 23 00 10 00 01 00 00 00 80 00 10 00 02 00 01 06 — abort 0x06010002
write 0x1017:00 with four bytes 23 17 10 00 01 00 00 00 80 17 10 00 10 00 07 06 — abort 0x06070010

The abort code is 4 bytes in the data field, least significant first. The same codes exist both in python-canopen and in mco.h: 0x06020000 — no such object, 0x06090011 — no such sub-index, 0x06010002 — the object is read-only, 0x06070010 — the length does not match the type, 0x05040001 — unknown command.

Our own request encoder in the model produced the same bytes as the library.

A value longer than 4 bytes goes in segments. The device name 0x1008 in the example's dictionary is the string "Zephyr RTOS/CANopenNode", 23 bytes. The server answered with command 0x41 and length 0x17 in the data field, and then the client fetched it in four segments of 7 bytes or less, alternating commands 0x60 and 0x70. That made 5 pairs of frames instead of one: an expedited transfer is much cheaper, and it is usually enough for configuring a node.


19б.5 PDO and SYNC: process data in 8 bytes

SDO is a question and an answer with a 4-byte header. For data that flows constantly (position, current, input states) this is expensive. A PDO is a frame without a header: all 8 bytes are data, and what they contain is written in the dictionary in advance.

Mapping. For TPDO1 this is object 0x1A00: sub-index 0 holds the number of objects, sub-indices 1, 2… hold words "index, sub-index, length in bits":

Object in the frame Mapping word
0x2102:00, 32 bits 0x21020020
0x1001:00, 8 bits 0x10010008

Objects are packed into the frame one after another, least significant byte first. These two give 40 bits = 5 bytes. With the counter at 7 and the error register at zero, the TPDO1 frame is 18A: 07 00 00 00 00; the python-canopen client parsed it with its own map back into 7 and 0. More than 64 bits does not fit in a classic frame: if you add 0x1002 as well (32 bits), you get 72 bits — and the library silently calculated this without objecting. You have to check the sum yourself.

The configuration order. How the library configured TPDO1 via SDO can be seen from the frames: first it wrote 0x8000018A to 0x1800:01 (bit 31 — "PDO not valid"), then transmission type 1, zeroed 0x1A00:00, wrote the two mapping words, set their number to 2 and finally restored the COB-ID 0x18A without bit 31. Seven expedited writes, all answered with 0x60.

The transmission type (0x1800:02) decides when the frame goes out. Values 1…240 are synchronous: the frame goes out on SYNC (in python-canopen the boundary is 0xF0); 0 — also on SYNC, but only if the data has changed. 254 and 255 — on event and timer. SYNC is frame 0x080 without data, sent by one node of the network; all synchronous TPDOs sample their values on the same signal, and the data from different nodes refers to the same moment.

How much the bus can carry. The length of a frame with a standard identifier is 44 + 8n bits (RM0090, Fig. 347), plus 3 bits of interframe space. On top of that, the transmitter inserts a bit after every five identical ones: more than five in a row is a stuff error (that is how the error code is described in RM0440). Stuffing applies from SOF to the end of the CRC — that is 34 + 8n bits (with the CRC delimiter it would be 35 + 8n, which does not change the worst case). The model found the worst number of stuff bits by brute force over all sequences and by dynamic programming: it equals (L − 1)/4, rounded down.

Data Without stuffing Worst case 125 kbit/s 250 kbit/s 500 kbit/s
0 bytes (SYNC) 47 bits 55 bits 440 µs 220 µs 110 µs
1 byte (heartbeat) 55 bits 65 bits 520 µs 260 µs 130 µs
5 bytes 87 bits 105 bits 840 µs 420 µs 210 µs
8 bytes 111 bits 135 bits 1080 µs 540 µs 270 µs

If the bus carries only 8-byte frames, 925, 1851 and 3703 of them pass per second at 125, 250 and 500 kbit/s. The bus load for several networks in the worst stuffing case, with SYNC every 10 ms and heartbeat once a second:

Network PDO frames per second 125 kbit/s 250 kbit/s 500 kbit/s
4 nodes × 2 PDOs of 8 bytes, every 10 ms 800 91.0 % 45.5 % 22.8 %
10 nodes × 2 PDOs of 8 bytes, every 10 ms 2000 220.9 % 110.5 % 55.2 %
10 nodes × 4 PDOs of 8 bytes, every 100 ms 400 48.1 % 24.1 % 12.0 %
30 nodes × 1 PDO of 5 bytes, every 10 ms 3000 258.0 % 129.0 % 64.5 %

More than 100 % means the network does not work: lower-priority frames never go out. This must be calculated before choosing the bit rate.


19б.6 A node in miniature in C

The node below can do the essentials: boot-up and heartbeat, NMT commands, expedited SDO reads and writes from a small dictionary table (15 entries from 19б.2) and TPDO1 on SYNC (type N — every N-th SYNC). It does not depend on the hardware: a frame is a structure with an identifier, a length and 8 bytes. Dispatching is done by 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;                                   /* чужие кадры не наши */
}

The SDO server: the command is in the upper three bits of byte 0, the write length is in bits 3:2, an abort carries a code from table 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;

The link to bxCAN is a separate file co19c_bxcan.c, the same for the F103 and the F407. The filter is one bank in list mode with four 16-bit identifiers (section 19.5): the node needs only NMT, SYNC and its own SDO requests, the hardware discards the rest. In a 16-bit filter the identifier sits in bits 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);

Both files are built with armclang for the F103 and F407 without warnings; _Static_assert checks the node number (1…127), that the COB-ID fits in 11 bits, the size of a frame and of a dictionary entry (12 bytes) and the format of the mapping word. The node takes 1652 bytes of code, the dictionary — 180 bytes of RAM.

The same co19c.c was built with gcc on the PC and fed 44 frames: all 19 expedited requests from the live python-canopen exchange (including the seven writes configuring TPDO1), NMT commands, SYNC and corrupted frames. All 19 SDO responses matched the responses of the library's server byte for byte. The states after the NMT commands matched the library's table. On SYNC in operational the node output 18A: 07 00 00 00 00 — the same frame as in 19б.5; in pre-operational and stopped it stayed silent. The unknown command 0xE0 got the abort 0x05040001.


19б.7 Pitfalls

SDO answers, but PDO does not. The node is in pre-operational: PDOs do not work there. You need NMT command 1. In the run, SYNC in pre-operational got no answer.

The index is written most significant byte first. The request 40 10 17 … asks for object 0x1710, not 0x1017. The answer is abort 0x06020000.

Wrote 4 bytes to a 16-bit object. The heartbeat period is 2 bytes; a write with command 0x23 got abort 0x06070010. The command must carry the object's length: for 2 bytes — 0x2B.

The PDO map is longer than 64 bits. The library calculated 72 bits and did not object. The frame will not be assembled; SDO has its own code for this refusal, 0x06040042 — "PDO length exceeded". Calculate the sum of lengths before writing.

The map was changed on the fly. The correct order is to disable the PDO with bit 31 in the COB-ID, zero the number of objects, write the map, restore the number and enable the PDO. That is what the library does.

Two nodes with the same number. They have identical COB-IDs; arbitration does not separate identical identifiers, and both reach the data at the same time — there will be an error at the first mismatching bit. The node number must be unique in the network.

Heartbeat lost under load. Heartbeat is the lowest-priority frame. With a load above 100 % it does not go out, and the node "dies" for everyone although it is working. In table 19б.5, 10 nodes × 2 PDOs every 10 ms take 110.5 % of the bus at 250 kbit/s.


19б.8 Summary

Full sources

co19c.c — the CANopen node: NMT, expedited SDO, TPDO1 on 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 — the node on top of bxCAN on the F103 and F407: the filter and FIFO polling.

/* Раздел 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);
    }
}

Next: section 19в "ISO-TP (ISO 15765-2): long messages over CAN".

Open in the interactive handbook