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 calledOD_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 namedNODE_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§
- Bootloader
Info - Implements a Bootloader info (0x5500) object
- Bootloader
Section - 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
- Node
Mbox - A data structure to be shared between a receiving thread (e.g. a CAN controller IRQ) and the
Nodeobject. - Node
State - The NodeState provides config-dependent storage to the
Nodeobject
Constants§
- SDO_
BUFFER_ SIZE - Default size for SDO data buffer
Traits§
- Bootloader
Section Callbacks - A trait for applications to implement to provide a bootloader section access implementation
- Node
State Access - A trait by which NodeState is accessed
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