Crate zencan_node

Crate zencan_node 

Source
Expand description

A library to implement a CANOpen node in Rust

Zencan-node is a library to implement CAN communications for an embedded node, using the CANOpen protocol. It is primarily intended to be run on microcontrollers, and so it is no_std compatible and performs no heap allocation, instead statically allocating storage. It is also possible to use it on std environments, for example on linux using socketcan. It provides the following features:

  • Implements the LSS protocol for node discovery and configuration.
  • Implements the NMT protocol for reporting and controlling the operating state of nodes.
  • Generates an object dictionary to represent all of the data which can be communicated on the bus. This includes a number of standard communication objects, as well as application specific objects specified by the user.
  • Implements an SDO server, allowing a remote client to access objects in the dictionary.
  • Implements transmit and receive PDOs, allowing the mapping of objects to user-specified CAN IDs for reading and writing those objects.
  • Provides callback hooks to allow for persistent storage of selected object values on command.

§Getting Started

§Device Configuration

A zencan node is configured using a DeviceConfig TOML file, see common::device_config module docs for more info.

§Code Generation

The device configuration is used to generate types and static instances for each object in the object dictionary, as well as some additional objects like a NodeMbox, and NodeState.

§Add zencan-build as build dependency

This crate contains functions to generate the object dictionary code from the device config TOML file.

[build-dependencies]
zencan-build = "0.0.1"

§Add the code generation to your build.rs file

fn main() {
    if let Err(e) = zencan_build::build_node_from_device_config("ZENCAN_CONFIG", "zencan_config.toml") {
        eprintln!("Failed to parse zencan_config.toml: {}", e.to_string());
        std::process::exit(-1);
    }
}

§Include the generated code in your application

When including the code, it is included using the name specified in build – ZENCAN_CONFIG in this case. This allows creating multiple object dictionaries in a single application.

Typically, an application would add a snippet like this into main.rs:

mod zencan {
    zencan_node::include_modules!(ZENCAN_CONFIG);
}

§Instantiating the Node object

§Object setup

One of the first things you should do before instantiating a node is set the serial number on the 0x1018 object. Devices are identified by an “identity” that includes the vendor, product, revision, and a serial number. If you are going to put more than one of a particular device type on a network, each should somehow come up with a unique serial. Most MCUs will have a unique ID register which can be used for this purpose.

// Use the UID register to set a unique serial number
zencan::OBJECT1018.set_serial(get_serial());

§Node Creation

Instantiate the node by providing it with the node ID, a set of event callbacks, the object dictionary, the mailbox, and the node state object.

  • Node ID: This is the ID boot up ID of the node. It can be stored in flash, it can be a constant, it can be set by DIP switches, etc. It can also be left as NodeId::Unconfigured. It is then possible to configure the node ID over the bus using the LSS protocol.
  • Object dictionary: This is a table where all of the objects are stored. It is created as a static variable by zencan-build, and is called OD_TABLE.
  • Mailbox: This is a data structure for receiving incoming CAN messages. It buffers received messages so that messages can be pass to it in an interrupt, and then processed in the next call to process. It is defined by the generated code in a static variable named NODE_MBOX.
  • Node state: This is a global state structure which provides some communications between the Node and objects such as PDO configuration objects, or special purpose object like the Save Command object. It is defined by the generated code in a static variable named NODE_STATE.

There are a variety of callback functions you may provide as well, although they are not required.

// Get references to the functions which save to flash
let store_node_config = &mut store_node_config;
let store_objects = &mut store_objects;

let callbacks = Callbacks {
    store_node_config: Some(store_node_config),
    store_objects: Some(store_objects),
    reset_app: None,
    reset_comms: None,
    enter_operational: None,
    enter_stopped: None,
    enter_preoperational: None,
};


// Initialize node, providing references to the static objects created by `zencan-build`
let mut node = Node::new(
    NodeId::Unconfigured,
    callbacks,
    &zencan::NODE_MBOX,
    &zencan::NODE_STATE,
    &zencan::OD_TABLE,
);

§Handling CAN messages

The application has to handle sending and receiving CAN messages.

The NODE_MBOX struct acts as a mailbox for both incoming and outgoing mailboxes, and the application must pass messages between the mailbox and the CAN controller. This can be done in any thread – a good way to do it is to have the CAN controller interrupt store messages here directly.

// Assuming we've received a message (id, and buffer) from somewhere, pass it to the mailbox
let msg = zencan_node::common::messages::CanMessage::new(id, &buffer[..msg.len as usize]);
// Ignore error -- as an Err is returned for messages that are not consumed by the node
// stack. You may handle those some other way, or simply drop them.
zencan::NODE_MBOX.store_message(msg).ok();

Outgoing messages can be read from the mbox using the NodeMbox::next_transmit_message function. A callback can be registered (see NodeMbox::set_transmit_notify_callback) to be notified when new messages are queued for transmission – this can be used to e.g. push the first message(s) to the CAN controller to initiate an IRQ driven transmit look, or to wake an async task which is responsible for moving messages from the node to the CAN controller.

#[embassy_executor::task]
async fn twai_tx_task(mut twai_tx: TwaiTx<'static, Async>) {
    loop {
        while let Some(msg) = zencan::NODE_MBOX.next_transmit_message() {
            let frame =
                EspTwaiFrame::new(StandardId::new(msg.id.raw() as u16).unwrap(), msg.data())
                    .unwrap();
            if let Err(e) = twai_tx.transmit_async(&frame).await {
                log::error!("Error sending CAN message: {e:?}");
            }
        }

        // Wait for wakeup signal when new CAN messages become ready for sending
        CANOPEN_TX_SIGNAL.wait().await;
    }
}

§Calling periodic process method

To execute the Node logic, the Node::process function must be called periodically. While it is possible to call process only periodically, the NODE_MBOX object provides a callback which can be used to notify another task that process should be called when a message is received and requires processing.

Here’s an example of a lilos task which executes process when either CAN_NOTIFY is signals, or 10ms has passed since the last notification.

async fn can_task(
    mut node: Node,
) -> Infallible {
    let epoch = lilos::time::TickTime::now();
    loop {
        lilos::time::with_timeout(Duration::from_millis(10), CAN_NOTIFY.until_next()).await;
        let time_us = epoch.elapsed().0 * 1000;
        node.process(time_us);
    }
}

Re-exports§

pub use critical_section;
pub use zencan_common as common;

Modules§

object_dict
Object Dictionary
pdo
Implementation of PDO configuration objects and PDO transmission
priority_queue
A prioritized queue for handling CAN messages
storage
Handling for persistent storage control objects

Macros§

build_object_dict
Macro to build an object dict from inline device config TOML
include_modules
Include the code generated for the object dict in the build script.

Structs§

BootloaderInfo
Implements a Bootloader info (0x5500) object
BootloaderSection
Implements a bootloader section object in the object dictionary
Callbacks
Collection of callbacks events which Node object can call.
Node
The main object representing a node
NodeMbox
A data structure to be shared between a receiving thread (e.g. a CAN controller IRQ) and the Node object.
NodeState
The NodeState provides config-dependent storage to the Node object

Constants§

SDO_BUFFER_SIZE
Default size for SDO data buffer

Traits§

BootloaderSectionCallbacks
A trait for applications to implement to provide a bootloader section access implementation

Functions§

restore_stored_comm_objects
Restore only communications objects from the stored data to the object dict
restore_stored_objects
Restore all stored objects in stored data to the object dict