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 applicaation.
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
Before instantiating the node, you should do any setup of objects that your applications needs. In particular, you should set the serial number on Object 0x1018. Now is also a good time to load any persisted object values, if you have them.
§Node Creation
Instantiate the node by providing it with the OD, the mailbox, and the node
state object, all of which were created by zencan-build. You also must
provide a NodeId. You may provide a node ID which has been saved to flash,
or a hard-coded ID, or you can provide
NodeId::Unconfigured, in which case the
node will not be fully operational until it is assigned an ID, but it will
respond to LSS commands for discovery and ID assignment.
The node object has to be created in two steps, using the statics created by
the include_modules!. The first step initializes the object dictionary –
mainly it registers object callbacks so that the callback objects such as
PDO config objects can be written to. The second step instantiates the node
and latches some of the configuration from the object dictionary. In between
these two steps is where the application should make sure that any run-time
loaded object values are stored – for example this is the time to read back
any object values which have been stored in flash, or to configure the
device serial number.
// Read saved node ID from flash
let node_id = read_saved_node_id(&mut flash).unwrap_of(NodeId::Unconfigured);
// Use the UID register to set a unique serial number
zencan::OBJECT1018.set_serial(get_serial());
// Restore object values from a previous save. The source data is the slice of bytes provided by
// the node storage callback. The application is responsible for storing this somewhere
// (e.g. flash) and restoring it later.
let serialized_object_data: &[u8] = get_object_data();
restore_stored_objects(&zencan::OD_TABLE, serialized_object_data);
// Initialize node, providing references to the static objects created by `zencan-build`
let mut node = Node::new(
node_id,
&zencan::NODE_MBOX,
&zencan::NODE_STATE,
&zencan::OD_TABLE,
);§Handling CAN messages
The application has to handle sending and receiving CAN messages.
Received messages should be passed to the NODE_MBOX struct. This can be
done in any thread – a good way to do it is to have the CAN controller
receive interrupt store messages here directly.
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
zencan::NODE_MBOX.store_message(msg).ok();To execute the Node logic, the Node::process function must be called
periodically. It is provided a callback for transmitting messages. 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,
mut can_tx: fdcan::Tx<FdCan1, NormalOperationMode>,
) -> 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, &mut |msg| {
let header = zencan_to_fdcan_header(&msg);
if let Err(_) = can_tx.transmit(header, msg.data()) {
defmt::error!("Error transmitting CAN message");
}
});
}
}§Register callbacks
The application can register callbacks for persistently storing data, or notifying the processing task. See examples for more info.
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
- 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
- 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_ objects - Load values of objects previously persisted in serialized format