For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.

Board
MCU
Flash
CAN FD
CAN ports
PlatformIO environment

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.

  1. Install Visual Studio Code https://code.visualstudio.com/download

  2. Install the PlatformIO extension https://platformio.org/install/ide?install=vscode

  3. Select the environment for your board from the PlatformIO status bar

  4. Connect your STLINK to your CAN node

  5. 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:

Argument
Default
What it does

mode

CanMode::Classic

Classic CAN 2.0B, or CAN FD on H7 boards. See CAN FD

port

CanPort::PORT1

Which physical CAN port this instance uses. See Two CAN ports

storage_page

-1 (board default)

First flash page of the parameter storage region. Leave it alone unless you know you need to move it

persist_on_set

true

Whether a parameter written over CAN saves itself. See parameters

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:

Pass the dronecan object, not dronecan.canard. The older sendUavcanMsg(dronecan.canard, pkt) form still compiles, but it doesn't know about the node's CAN FD setting, so on an FD node it will send that message as a classic CAN frame. See CAN FD.

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.

Older versions of this documentation showed the canardBroadcast() call with a long argument list. That function still exists, but its signature grows extra arguments on CAN FD builds, so it won't compile for the H7 boards. The canardBroadcastObj() form above builds everywhere. CANARD_TAO_ENCODE_ARG() is a helper macro from the library that expands to nothing on builds where the encoder doesn't take a tail-array-optimisation argument.

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

CANFD support is experimental. So far bench tests have shown 2mpbs functioning, and some success with 4mpbs.

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
Data phase bitrate
ArduPilot CAN_Px_FDBITRATE

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:

1.X
2.X

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