Arduino DroneCAN
A library to send CAN messages compatible with Ardupilot and PX4 UAV autopilots.
About
We made a library to make DroneCAN development as simple as possible. the Arduino DroneCAN repository allows you to get started with Ardupilot/PX4 compatible CAN messages and functionality straight out of the box using Beyond Robotix CAN node hardware.
Github Repository:
Supported hardware
The same application code builds for all of our CAN nodes. Which board you pick in PlatformIO decides the pinmap, the linker script and which CAN driver gets compiled in.
Micro Node
STM32L431
256 KB
No
1
Micro-Node-App
CAN Node
STM32L431
256 KB
No
1
Micro-Node-App
CAN Node Plus
STM32H723
1 MB
Yes
2
Micro-Node-Plus-App
Core Node
STM32H743
2 MB
Yes
2
Core-Node-App
The two ports on the H7 boards are fully independent interfaces, not a shared bus brought out twice. Each one is its own DroneCAN node with its own node ID and its own parameter values, so a single board can sit on two separate CAN buses at once - or bridge them, if that's what you want. See Two CAN ports.
The CAN Node is the same L431 as the Micro Node in a standalone form factor, so the two share an environment. For each board's connectors, pinouts and interfaces, see its own page in this section.
Installation
There are a few ways of working with Arduino Code, we recommend the following steps for seamless integration with the project.
Install Visual Studio Code https://code.visualstudio.com/download
Install the PlatformIO extension https://platformio.org/install/ide?install=vscode
Download the project https://github.com/BeyondRobotix/Arduino-DroneCAN/releases
Select the environment for your board from the PlatformIO status bar
Connect your STLINK to your CAN node
Press upload!
There are no submodules any more, so a plain clone is all you need:
The library and the board definitions live in their own repositories and are fetched by PlatformIO on the first build, pinned to a version in platformio.ini:
The first build takes a couple of minutes while these download. To move to a newer library, change the #vX.Y.Z tag and delete .pio/libdeps/ so PlatformIO re-fetches it. To pick up a new platform release, run pio pkg uninstall --platform br-stm32 -g.
Bootloader details
ArduinoDroneCAN uses our own bootloader, BR_bootloader, to allow app updates over CAN. The standard AP_Bootloader cannot be used.
You don't need to flash it separately. The bootloader binary for each board ships inside the br_platformio_hwdef platform, and any environment ending in -App flashes the bootloader and your app in one go on upload. A brand new board is fully provisioned by a single pio run -t upload.
Breakpoint debugging
This works as standard in PlatformIO debugging! It previously needed a special flash without the bootloader, but this is no longer an issue.
Detailed Example
The Arduino DroneCAN project reduces using DroneCAN down to creating an object, initialising the object, and one method to call in your loop function.
General advice
Avoid the delay() function! this will disrupt any CAN activities. We show best practises for your loop function later
Respect the watchdog. IWatchdog.reload() needs calling within the timeout time (2 seconds in our examples)
Following the format of our example https://github.com/BeyondRobotix/Arduino-DroneCAN/blob/master/src/main.cpp will reduce the likelyhood of strange behaviour.
If your program starts acting up, strip your program back to our example and build gradually from there. You'll find where bugs get introduced.
If your node goes into "maintenance" mode then it means your program is crashing
The Setup
app_setup, dronecan.init and IWatchdog.begin must all be called at the start of setup. Doing another activities before or between functions may result in instabilities. Remember, the watchdog is running after this so call IWatchdog.reload() if you do any long running activities.
app_setup()
app_setup is required to be called at the start of the Arduino setup() function. This function is required as part of the transfer from the bootloader to app control. The function is internally disabled when non bootloader mode is selected in PlatformIO.
dronecan.init()
If your node only needs to send messages, use the short form of init(). You just pass a parameter list and the name of the node for NodeInfo, and the library wires up the receive callbacks for you:
If your node needs to receive messages, pass your own two callback functions as the first two arguments (we explain these later):
Both forms take the same optional arguments after the node name, all of which have sensible defaults:
storage_page
-1 (board default)
First flash page of the parameter storage region. Leave it alone unless you know you need to move it
Watchdog
Our program requires starting the watchdog, remove it at your own peril! (it's started in the bootloader and from our testing, starting it here again is best)
DroneCAN looping
The normal Arduino loop() function works. Older versions of this library needed all of your looping code inside a while(true) at the end of setup() - that restriction is gone. Sketches written the old way still run fine, so you don't have to rewrite them.
Fixed interval loops
The following code sets us up to run our if statement at 10Hz, checking if 100ms has passed since our last call. This is because we want to only send our CAN message at 10Hz and we want to call our "dronecan.cycle()" function to be called as much as possible to ensure CAN messages are send and received in a timely manner. We want to avoid delay() as much as possible!
Sensor reading
Next, we want to read in our sensor value. This could be from anything, a current monitor, position sensor.. in this example, we read in the temperature of our STM32 processor. You would have had to have initialised your sensor in setup() of course.
Sending DroneCAN messages
Next, we initialise our DroneCAN battery message packet and we assign one of its attributes a value from when we read in our sensor. We've used a battery message, since Ardupilot supports 8 of these by default, Mission planner can display information from any of these 8 and Ardupilot logs all battery instances. There may be messages suitable for your application, but be aware, Ardupilot does not support many DroneCAN messages.
Finally, we send our DroneCAN message. For some equipment type can messages, we can use this very simple function:
For messages which are not in this type list:
we have to do the full boilerplate for a UAVCAN/DroneCAN message:
transfer_id has to be static (or global) - libcanard advances it for you after each successful send, so it needs to survive between calls.
End of the loop
Outside our fixed interval if statment, we call our dronecan.cycle() method, which is required, and we also reset our watchdog timer.
Reading in CAN packets
onTransferReceived and shouldAcceptTransfer are defined in our program and passed to dronecan.init. These functions are used to recieve any DroneCAN messages. If your node only sends messages you can leave them out entirely and use the short form of init() - the library then handles the standard DroneCAN traffic on its own. We've put an example of decoding a Magnetometer message below. in onTransferRecieved, you can manipulate the recieved packet and use gobal variables or parameters etc to transfer information out of this function. DroneCANonTransferReceived must always be called at the end of this function for our library to work.
to allow a different message to be recieved, shouldAcceptTransfer needs the new message adding to it, following the same pattern as the magnetometer message example. As before, the last line of this function must remain the same for our library to work correctly.
Other things
parameters
We passed the custom_parameters object into dronecan.init. This parameters object needs "NODEID" in the list since we use that. The first value after the type dictates the default value, second the minimum for the parameter and the last the maximum. These min/maxes may or may not be shown in Mission planner etc and may or may not make any functional difference.
DroneCAN::INT, DroneCAN::REAL and DroneCAN::BOOL are short aliases for the UAVCAN_PROTOCOL_PARAM_VALUE_* constants. The long names still work if you prefer them.
You can set and retrieve parameters easily. parameters get saved to flash so they'll be recalled on next boot.
CAN FD
The H7 boards (MicroNode+ and Core Node) support CAN FD. Pass a CanMode to init() to turn it on:
The arbitration bitrate is always 1 Mbps. The mode picks the data-phase multiplier on top of that, so match it to the autopilot's CAN_Px_FDBITRATE parameter:
CanMode::Classic (default)
n/a - CAN 2.0B
-
CanMode::FD2X
2 Mbps
2
CanMode::FD4X (also CanMode::FD)
4 Mbps
4
CanMode::FD8X
8 Mbps
8
Once a node is initialised in an FD mode, sendUavcanMsg(dronecan, pkt) sends FD frames automatically. This is why you should pass the dronecan object rather than dronecan.canard - the canard form has no way to know the node is in FD mode and will send a classic frame instead.
The Micro Node's L431 has no FD hardware. Asking for an FD mode in a Micro Node build is a compile error rather than a runtime surprise.
Two CAN ports
The H7 boards have two CAN peripherals, and you can run both from one firmware by creating two DroneCAN objects. Each is an independent node with its own node ID, its own parameter values and its own transfer IDs:
Give the two instances different default NODEIDs so they don't collide on a shared bus. Both instances share the same pair of flash pages for parameter storage - each record is tagged with which instance it belongs to - so the second port costs no extra flash.
There is also CanPort::BOTH, which bridges the two ports into a single node: messages go out on both ports and received messages are merged. This is the redundant-interface setup ArduPilot expects when you wire one node to two buses.
The full working version of this is the Dual_CAN example. The Micro Node has a single CAN port and ignores the port argument.
Full example!
This is src/main.cpp from the repository. It's a send-only node, so it uses the short form of init(). If you need to receive messages, add the two callback functions from Reading in CAN packets and pass them as the first two arguments to init().
<dronecan.h> pulls in everything the library needs, so the separate <IWatchdog.h>, <app.h>, <vector> and <simple_dronecanmessages.h> includes older examples carried are no longer required.
Upgrading from 1.X
If you're moving an existing sketch from a 1.X release, these are the things that changed:
Clone with --recurse-submodules
No submodules. PlatformIO fetches the library and board definitions from platformio.ini
Micro-Node-Bootloader environment
Micro-Node-App, plus Core-Node-App and Micro-Node-Plus-App for the H7 boards
Loop code inside while(true) in setup()
Normal Arduino loop(). The old form still works
init() always needed the two callback functions
Callbacks are optional - omit them if your node only sends
sendUavcanMsg(dronecan.canard, pkt)
sendUavcanMsg(dronecan, pkt), so CAN FD is handled for you
DroneCANshoudlAcceptTransfer
DroneCANshouldAcceptTransfer (spelling fixed)
UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE
DroneCAN::INT and friends. The long names still work
Parameters needed "Store all" to persist
Parameters written over CAN save themselves
Reflashing a 1.X node with 2.X firmware resets its parameters to the defaults in your code, once. Note down anything you've changed from default before you upgrade.
Last updated