Thermocouple Tutorial
A demo of the Arduino DroneCAN library with Beyond Robotix CAN node, using the MCP9600 thermocouple reader.
Intro
Do you want to integrate a sensor into Ardupilot or PX4? Arduino DroneCAN and the Beyond Robotix CAN node let you do that very quickly. This tutorial runs through integrating the Adafruit MCP9600 thermocouple sensor, resulting in us being able to send temperature messages over DroneCAN. The big advantage of using the Arduino framework is access to Arduino libraries. Almost always, there are libraries available for the sensor you want to use. This saves development time. The Arduino framework is also simple to work with, and the Platformio + VS code platform allows easy development from small to complicated projects.

Using custom firmware allows lots of options, such as integrating a battery monitor + thermocouple and sending all the information with one device in one message. You could action tasks based on the thermocouple onboard the node, e.g. opening a hatch if the battery is getting too hot.
You could integrate GPS receivers, new airspeed sensors, fuel sensors, gas sensors, ESCs and servo nodes.
Hardware Requirements
Beyond Robotix CAN Node Dev Kit
Flight controller with a CAN bus (Or a CAN 'Sniffer')
Windows PC is assumed - but other OS’s will apply there might just be some minor adjustments needed
Getting started
To start, we should download the latest release of Arduino DroneCAN from:
The code we will run through is in examples/thermocouple-mcp9600 which is here:
We’ll need to download a few things to work with the software. We’ve got these setup instructions on our documentation site here:
We’ll also need to get our hardware setup. You’ll need a DroneCAN compatible flight controller or sniffer. We’ll use a Cube Orange. Then connect your Beyond Robotix CAN node to the flight controller via a CAN cable. Also, connect your STLINK to the debug port on the CAN node. Lastly, ensure the switch next to the debug port (SW1
) is set to '1'.

With the repository downloaded and our tools installed, we can build the default example to make sure it’s all working correctly. The default example sends a "BatteryInfo" message which we'll see later.

You should then see “SUCCESS” shown in the terminal. If you don’t, make sure your STLINK is connected correctly and power is being given to the CAN node via the flight controller.

We can now see what CAN messages are being sent by the Node. We’ll use DroneCAN GUI tool in this example, however, Mission Planner can also be used for CAN packet inspection.

Once connected, we’ll see our Node showing in the list, (you may need to set the Local node ID, press the tick in the top left ish)

We can see the example parameters in Node properties:

And we can also see the battery message being sent (which can be found in Tools > Subscriber)

We can see the temperature field which responds to the built in MCU temperature sensor. “Voltage” and “current” are showing the raw ADC values from PA1 and PA0 respectively.
Getting MCP9600 data
We’ve chosen to use the Adafruit library, although there are a few MCP9600 libraries to choose from. Download the latest release, unzip and add it to the “lib” folder of your project. Make sure there are no nested folders! The library can also be installed from the platformio library manager. The “BusIO” library is also needed. Platformio may install this for you.
Let's have a look at their example...
#include <Wire.h>
#include <Adafruit_I2CDevice.h>
#include <Adafruit_I2CRegister.h>
#include "Adafruit_MCP9600.h"
#define I2C_ADDRESS (0x67)
Adafruit_MCP9600 mcp;
/* Set and print ambient resolution */
Ambient_Resolution ambientRes = RES_ZERO_POINT_0625;
void setup()
{
Serial.begin(115200);
while (!Serial) {
delay(10);
}
Serial.println("MCP9600 HW test");
/* Initialise the driver with I2C_ADDRESS and the default I2C bus. */
if (! mcp.begin(I2C_ADDRESS)) {
Serial.println("Sensor not found. Check wiring!");
while (1);
}
Serial.println("Found MCP9600!");
/* Set and print ambient resolution */
mcp.setAmbientResolution(ambientRes);
Serial.print("Ambient Resolution set to: ");
switch (ambientRes) {
case RES_ZERO_POINT_25: Serial.println("0.25°C"); break;
case RES_ZERO_POINT_125: Serial.println("0.125°C"); break;
case RES_ZERO_POINT_0625: Serial.println("0.0625°C"); break;
case RES_ZERO_POINT_03125: Serial.println("0.03125°C"); break;
}
mcp.setADCresolution(MCP9600_ADCRESOLUTION_18);
Serial.print("ADC resolution set to ");
switch (mcp.getADCresolution()) {
case MCP9600_ADCRESOLUTION_18: Serial.print("18"); break;
case MCP9600_ADCRESOLUTION_16: Serial.print("16"); break;
case MCP9600_ADCRESOLUTION_14: Serial.print("14"); break;
case MCP9600_ADCRESOLUTION_12: Serial.print("12"); break;
}
Serial.println(" bits");
mcp.setThermocoupleType(MCP9600_TYPE_K);
Serial.print("Thermocouple type set to ");
switch (mcp.getThermocoupleType()) {
case MCP9600_TYPE_K: Serial.print("K"); break;
case MCP9600_TYPE_J: Serial.print("J"); break;
case MCP9600_TYPE_T: Serial.print("T"); break;
case MCP9600_TYPE_N: Serial.print("N"); break;
case MCP9600_TYPE_S: Serial.print("S"); break;
case MCP9600_TYPE_E: Serial.print("E"); break;
case MCP9600_TYPE_B: Serial.print("B"); break;
case MCP9600_TYPE_R: Serial.print("R"); break;
}
Serial.println(" type");
mcp.setFilterCoefficient(3);
Serial.print("Filter coefficient value set to: ");
Serial.println(mcp.getFilterCoefficient());
mcp.setAlertTemperature(1, 30);
Serial.print("Alert #1 temperature set to ");
Serial.println(mcp.getAlertTemperature(1));
mcp.configureAlert(1, true, true); // alert 1 enabled, rising temp
mcp.enable(true);
Serial.println(F("------------------------------"));
}
void loop()
{
Serial.print("Hot Junction: "); Serial.println(mcp.readThermocouple());
Serial.print("Cold Junction: "); Serial.println(mcp.readAmbient());
Serial.print("ADC: "); Serial.print(mcp.readADC() * 2); Serial.println(" uV");
delay(1000);
}
They import the files, set the i2c address and setup the mcp
object. We had to set the address to 0x66 for the MCP9600 we had.
#include <Wire.h>
#include <Adafruit_I2CDevice.h>
#include <Adafruit_I2CRegister.h>
#include "Adafruit_MCP9600.h"
#define I2C_ADDRESS (0x67)
Adafruit_MCP9600 mcp;
We then call .begin
method
/* Initialise the driver with I2C_ADDRESS and the default I2C bus. */
if (! mcp.begin(I2C_ADDRESS)) {
Serial.println("Sensor not found. Check wiring!");
while (1);
}
then, they have a bunch of other options but the two we're interested in for the basics:
// set our thermocouple type
mcp.setThermocoupleType(MCP9600_TYPE_K);
// take a temperature measurement
mcp.readThermocouple()
So, with this information, we can now write our DroneCAN application.
Writing our app
Now, we go to main.cpp
in Arduino DroneCAN and add the code from above to our setup()
function. We have provided an example for you to follow:
#include <Arduino.h>
#include <dronecan.h>
#include <IWatchdog.h>
#include <app.h>
#include <vector>
#include <simple_dronecanmessages.h>
#include "Adafruit_MCP9600.h"
#include "Wire.h"
// set up your parameters here with default values. NODEID should be kept
std::vector<DroneCAN::parameter> custom_parameters = {
{"NODEID", UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE, 100, 0, 127},
{"DEVICE_ID", UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE, 0, 0, 127},
{"BATT_EN", UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE, 0, 0, 1},
};
DroneCAN dronecan;
uint32_t loop1time = 0;
uint32_t loop2time = 0;
uint32_t looptime1hz = 0;
int device_id = 0;
int batt_en = 0;
/*
MCP9600 specific setup
*/
#define I2C_ADDRESS (0x66)
Adafruit_MCP9600 mcp;
static void onTransferReceived(CanardInstance *ins, CanardRxTransfer *transfer)
{
DroneCANonTransferReceived(dronecan, ins, transfer);
}
static bool shouldAcceptTransfer(const CanardInstance *ins,
uint64_t *out_data_type_signature,
uint16_t data_type_id,
CanardTransferType transfer_type,
uint8_t source_node_id)
{
return false || DroneCANshoudlAcceptTransfer(ins, out_data_type_signature, data_type_id, transfer_type, source_node_id);
}
void setup()
{
// the following block of code should always run first. Adjust it at your own peril!
app_setup();
IWatchdog.begin(2000000);
Serial.begin(115200);
dronecan.version_major = 1;
dronecan.version_minor = 0;
dronecan.init(
onTransferReceived,
shouldAcceptTransfer,
custom_parameters,
"BR-Node-Temperature");
// end of important starting code
if (!mcp.begin(I2C_ADDRESS))
{
uint32_t deadloop = 0;
while (1)
{
const uint32_t now = millis();
if (now - deadloop > 1000)
{
deadloop = millis();
dronecan.debug("MCP9600 not found", 0);
Serial.println("Sensor not found. Check wiring!");
}
dronecan.cycle();
IWatchdog.reload();
}
}
// Set the thermocouple type
mcp.setThermocoupleType(MCP9600_TYPE_K);
// we use a while true loop instead of the arduino "loop" function since that causes issues.
while (true)
{
const uint32_t now = millis();
if (now - loop1time > 100)
{
loop1time = millis();
if (batt_en)
{
uavcan_equipment_power_BatteryInfo pkt{};
pkt.battery_id = device_id;
pkt.current = 0;
pkt.voltage = 0;
pkt.temperature = mcp.readThermocouple();
sendUavcanMsg(dronecan.canard, pkt);
}
}
if (now - loop2time > 1000)
{
loop2time = millis();
uavcan_equipment_device_Temperature pkt{};
pkt.temperature = mcp.readThermocouple();
pkt.device_id = device_id;
sendUavcanMsg(dronecan.canard, pkt);
}
if (now - looptime1hz > 1000)
{
looptime1hz = millis();
batt_en = dronecan.getParameter("BATT_EN");
device_id = dronecan.getParameter("DEVICE_ID");
}
dronecan.cycle();
IWatchdog.reload();
}
}
void loop()
{
// Doesn't work coming from bootloader ? use while loop in setup
}
In many arduino projects, you might use delay()
. This can cause problems with Arduino DroneCAN as to needs to perform functions in the background. So, instead of using delay()
, setup a statement that only runs once a timer counts to a given time, in this example 1000ms. When not meeting this condition, it always calls "dronecan.cycle()" which processes CAN frames in the background and "IWatchdog.reload()" which ensures our watchdog doesn't reset.
In this example, when we are unable to connect to the MCP9600, we want to keep resending the message every second over CAN and serial to say we have a problem. Instead of using a delay, we use an if
statement which waits until a time has elapsed while still allowing the background tasks to run.
if (!mcp.begin(I2C_ADDRESS))
{
// used to keep track of the time the last debug statement was run
uint32_t deadloop = 0;
while (1)
{
// see what our current time is
const uint32_t now = millis();
// check if enough time has passed to activate our debug statements
if (now - deadloop > 1000)
{
// refresh the timer, so the next one will trigger in 1000ms
deadloop = millis();
dronecan.debug("MCP9600 not found", 0);
Serial.println("Sensor not found. Check wiring!");
}
// cycle through recieve and transmit CAN messages
dronecan.cycle();
// reset Watchdog to make sure the watchdog doesn't reset us
IWatchdog.reload();
}
}
Another point is we don't use the void loop()
that Arduino uses normally. Instead we have a while(true){
running at the end of the void setup()
function.
void setup()
{
# Set up Code
while(true){
# Looping Code
}
}
Now, in the example, we've written in some logic which lets you send either a temperature packet or a battery packet depending on some parameters. At the core of it, we send a DroneCAN message like this:
uavcan_equipment_device_Temperature pkt{};
pkt.temperature = mcp.readThermocouple();
pkt.device_id = device_id;
sendUavcanMsg(dronecan.canard, pkt);
We can see that we read in our thermocouple measurement, and put it into a Temperature packet. You can see the packets available here:
Although there are lots of different types of packets, not all are actually understood by PX4 or Ardupilot, so you'll have to do some research on the appropriate packet to use. If you get stuck you can contact us at admin@beyondrobotix.com
We also set the device_id, which we pull from a parameter in a different part of the loop. This is used by Ardupilot to identify which battery instance to put the temperature into.
We then do our "sendUavcanMsg" which queues our packet.
And there we go! If you compile our example, you'll see either battery or temperature packets coming through depending on the parameters set.
We can create fairly complex features easily : - ) If you use the library and/or CAN node in your project, we'd be keen to hear about it! contact us at admin@beyondrobotix.com
Breakpoint debugging
We can debug our programs easily with an STLINK. This is included in the Micro CAN node dev kit. Breakpoint debugging is very useful, being able to see the value of variables in real time as the program runs.
Select the build environment to be "Micro-Node-No-Bootloader" building with the default environment will result in the program going into maintenance mode and staying in the bootloader
Set a breakpoint where you're interested in seeing the program state
Change to the Debug VS code tab
Start the debug session

After the program rebuilds, and starts running, the program now stops at where we set our breakpoint. We can see all the in scope variables on the left, and you can even hover over variables in the code to see their value.

Remember, our CAN node now doesn't have a bootloader active, you won't be able to upload firmware over CAN. Once you've done your development and debugging, switch back to the bootloader environment and you'll be good to go.
Folder structure
By default, we have a bunch of folders and files in our project.
.mypy_cache -> transient, will be generated once you start interacting with the code. Won't be tracked by git
.pio -> transient, contains all the build files. We can grab our .bin file from here if we want to distribute the program
.vscode -> vscode config files
assets -> Just used for our repository for logos etc
boards -> contains files which tell Platformio about the board we are using, referenced in "platformio.ini"
dronecan -> this one contains all the files needed to generate UAVCAN/DroneCAN message headers. Unless you want to regenerate these or do your own messages, we don't need this.
examples -> contains all the code examples that will hopefully help you on how to use the library. Each example contents can be copied to "src/main.cpp" to run them. Bear in mind you may need additional library configuration to run them. This should be in the README of each example folder.
include -> there's a README in there explaining how to use it
lib -> any arduino libraries you want to manually download and unzip go here. This also contains the core "Arduino DroneCAN lib" files, which can be modified if you want to extend the library functionality. We're open to PRs : ) Also contains DroneCAN message headers and libcanard which does our low level CAN frame management
src/main.cpp -> Our core program!
test -> See the README in this folder to see how to use it
variants -> contains code on board configuration. specifies what pins can be used and how. Also specifies RAM/Flash configurations specific to the board MCU
MicroNodeBootloader.bin -> our bootloader binary. "upload_bootloader_app.py" uses this file and uploads this and the main program at the same time
platformio.ini -> specifies how platformio interacts with the project

Last updated