Sniffing the CAN bus with Arduino Due

Brian asked me about the hardware I use to sniff on the can bus. The Arduino Due has two built-in CAN ports (I think they are built into the ARM processor it uses) that handle all the buffering and whatever is needed to deal with the higher data rates (up to 2 Mbit/s). But the ports are just data ports; they aren’t transceivers. I’ve read you can actually attach a CAN wire (not sure which high or low) to the CAN RX pin and receive data directly without a transceiver, but transceivers are certainly required to transmit on either CAN0 or CAN1 ports. I bought a shield that has two transceivers built for the Due from Copperhill Technologies: https://copperhilltech.com/dual-can-bus-interface-for-arduino-due/. It has jumpers for setting the termination. If you’re just going to sniff with one port, probably the jumper can be off, since termination is only required at the ends of a bus.

Only 1 CAN interface is required to sniff a network. Or with two interfaces, it can operate as a bridge if you want to physically unplug something like a sensor and place this Due system inline, useful for filtering where you can modify certain data and observe what happens.

Tractors typically use j1939 messages, and newer ones may use NMEA2000. I haven’t encountered any NMEA2000 messages, so I don’t know how they are formatted. j1939 messages are normal extended CAN messages. We can decode the 29-bit CAN id into a 16-bit PGN, a source address, and a destination address. I’ll post some code when I can figure out a good way to copy and paste my code over here. The markdown format for code in this editor is very awkward.

Here is a nice summary of j1939 as a protocol and format on top of CAN:

Now that I’ve finished the analysis portion of my little project that started me down this rabbit hole, I’m going to build my field unit with a Teensy 4.0 (which has 2 CAN ports also) and some SN65HVD230 adapters, such as this: Amazon.ca

5 Likes

Just a couple of thoughts on why CAN filtering would be useful vs just pushing messages onto a bus.

By CAN filtering I mean breaking the bus (usually by unplugging something) and placing a Due with CAN transceivers in between. Then a simple program reads from one bus and transmits everything it sees to the other side and vice versa. As packets go across the Due, code can alter them or inject new messages pretending to be from an existing ID. This could be useful for exploring and learning what the various devices do. For example, if the throttle lever emits j1939 messages, we could try altering them and see exactly what they do.

Also simply altering the messages allows control even when you don’t fully understand what everything does. In other words you wouldn’t have to implement a full ECU just to interact with one small thing.

Some systems, like John Deere, are extra paranoid and use various messages to “authenticate” the other devices on the bus. Filtering allows this handshaking to happen, while still letting us have some kind of control.

2 Likes

It’s worth reposting a link Brian posted on the combine forum a long time ago. It contains a list of some industry standard PGNs, some of which our tractors likely use:

The various SPNs can give you a clue as to how data is encoded. Even if a PGN is proprietary and undocumented, if we know what kind of data we’re expecting, we can find similar things in the documented SPNs that we can try. For example, if we suspect that some data on the CAN bus is giving us a roll value from the IMU (Brian talked about the IMU on his Case tractor), then we can see if it’s encoded like another degree value in the SPN. Likely something like 2 byte little-endian word divide by 128 and subtract 200.

I haven’t yet looked but almost certainly a Trimble receiver will transmit latitude and longitude over the bus using the standard PGN 65267, which has two 32-bit fields like this:

  • bytes 0-3 - little endian latitude: multiply by 1E-7, subtract 210.
  • bytes 4-7 - little endian longitude: multiply by 1E-7, subtract 210.

Here’s a simple sniffer (one port only) for the Due:

/*
	A simple CAN sniffer

	Requires the following libraries installed into Arduino:

	https://github.com/collin80/can_common
	https://github.com/collin80/due_can
 */

#include "variant.h" 
#include <due_can.h>

static inline void print_hex(uint8_t *data, int len) {
	char temp[4];
	for (int b=0;b < len; b++) {
		sprintf(temp, "%.2x",data[b]);
		Serial.print(temp);
	}
	Serial.println("");
}

bool j1939PeerToPeer(long lPGN)
{
	// Check the PGN 
	if(lPGN > 0 && lPGN <= 0xEFFF)
		return true;

	if(lPGN > 0x10000 && lPGN <= 0x1EFFF)
		return true;

	return false;

}

void j1939Decode(long lID, unsigned long* lPGN, byte* nPriority, byte* nSrcAddr, byte *nDestAddr)
{
	/* decode j1939 fields from 29-bit CAN id */
	byte nRetCode = 1;

	*nSrcAddr = 255;
	*nDestAddr = 255;

	long lPriority = lID & 0x1C000000;
	*nPriority = (int)(lPriority >> 26);

	*lPGN = lID & 0x00FFFF00;
	*lPGN = *lPGN >> 8;

	lID = lID & 0x000000FF;
	*nSrcAddr = (int)lID;

	if(j1939PeerToPeer(*lPGN) == true)
	{
		*nDestAddr = (int)(*lPGN & 0xFF);
		*lPGN = *lPGN & 0x01FF00;
	}
}

void got_frame(CAN_FRAME *frame) {
	unsigned long PGN;
	byte priority;
	byte srcaddr;
	byte destaddr;

	j1939Decode(frame->id, &PGN, &priority, &srcaddr, &destaddr);

        //could filter out what we want to look at here on any of these
        //variables.
	Serial.print(PGN);
	Serial.print(",");
	Serial.print(priority);
	Serial.print(",");
	Serial.print(srcaddr);
	Serial.print(",");
	Serial.print(destaddr);
	Serial.print(",");
	print_hex(frame->data.bytes, frame->length);

	//example of a decode
	if (PGN == 65267) { //vehicle position message
		//latitude
		Serial.print(frame->data.uint32[0] / 10000000.0 - 210.0);
		Serial.print(",");
		//longitude
		Serial.println(frame->data.uint32[1] / 10000000.0 - 210.0);
	}

	//don't do anything with the frame since we're just sniffing.
}

void setup()
{
	Serial.begin(115200);
	Can0.begin(CAN_BPS_250K);

	for (int filter=0;filter <3; filter ++) {
		Can0.setRXFilter(0,0,true);
	}

	Can0.attachCANInterrupt(got_frame);
}

void loop()
{
}

Change your speed accordingly. Common speeds are 250K and 500K. Also note this is for the Due only plus a CAN transceiver. I have no experience with a normal Arduino and CAN shield.

Here’s a skeleton of a CAN bridge. Could bridge two different networks of different speeds even:

/*
	A simple CAN bridge

	Requires the following libraries installed into Arduino:

	https://github.com/collin80/can_common
	https://github.com/collin80/due_can
 */

#include "variant.h" 
#include <due_can.h>

static inline void print_hex(uint8_t *data, int len) {
	char temp[4];
	for (int b=0;b < len; b++) {
		sprintf(temp, "%.2x",data[b]);
		Serial.print(temp);
	}
	Serial.println("");
}

bool j1939PeerToPeer(long lPGN)
{
	// Check the PGN 
	if(lPGN > 0 && lPGN <= 0xEFFF)
		return true;

	if(lPGN > 0x10000 && lPGN <= 0x1EFFF)
		return true;

	return false;

}

void j1939Decode(long lID, unsigned long* lPGN, byte* nPriority, byte* nSrcAddr, byte *nDestAddr)
{
	/* decode j1939 fields from 29-bit CAN id */
	byte nRetCode = 1;

	*nSrcAddr = 255;
	*nDestAddr = 255;

	long lPriority = lID & 0x1C000000;
	*nPriority = (int)(lPriority >> 26);

	*lPGN = lID & 0x00FFFF00;
	*lPGN = *lPGN >> 8;

	lID = lID & 0x000000FF;
	*nSrcAddr = (int)lID;

	if(j1939PeerToPeer(*lPGN) == true)
	{
		*nDestAddr = (int)(*lPGN & 0xFF);
		*lPGN = *lPGN & 0x01FF00;
	}
}

void got_frame(CAN_FRAME *frame, int which) {
	unsigned long PGN;
	byte priority;
	byte srcaddr;
	byte destaddr;

	j1939Decode(frame->id, &PGN, &priority, &srcaddr, &destaddr);

	//could filter out what we want to look at here on any of these
	//variables.
	Serial.print(PGN);
	Serial.print(",");
	Serial.print(priority);
	Serial.print(",");
	Serial.print(srcaddr);
	Serial.print(",");
	Serial.print(destaddr);
	Serial.print(",");
	print_hex(frame->data.bytes, frame->length);

	//example of a decode
	if (PGN == 65267) { //vehicle position message
		//latitude
		Serial.print(frame->data.uint32[0] / 10000000.0 - 210.0);
		Serial.print(",");
		//longitude
		Serial.println(frame->data.uint32[1] / 10000000.0 - 210.0);
	}

	if (which == 0) {
		//transmit it out Can1
		Can1.sendFrame(*frame);
	} else {
		//transmit it out Can0
		Can0.sendFrame(*frame);
	}
}

void can0_got_frame(CAN_FRAME *frame) {
	got_frame(frame,0);
}

void can1_got_frame(CAN_FRAME *frame) {
	got_frame(frame,1);
}

void setup()
{
	Serial.begin(115200);
	Can0.begin(CAN_BPS_250K);
	Can1.begin(CAN_BPS_250K);

	for (int filter=0;filter <3; filter ++) {
		Can0.setRXFilter(0,0,true);
		Can1.setRXFilter(0,0,true);
	}

	Can0.attachCANInterrupt(can0_got_frame);
	Can1.attachCANInterrupt(can1_got_frame);
}

void loop() 
{
}

One more thing. The Serial device on the Due is probably too slow to dump all the data out. If not filtering, you’d want to use the other USB port on the Due (the “native” one), and add the following line near the top of the ino file:

#define Serial SerialUSB

The native USB port on the Due is super fast and capable of megabit speeds.

Thanks for this example it is example what I am trying to do on a JD tractor.
To receive Vehicle Position 65267, do I need to request it first?

Is that what your Can0.RXFilter call is doing?

Should I change (0,0,true); to something meaningful?

thanks!

No on John Deere, the position is continuously broadcast over the implement bus (not the tractor bus), at 5Hz. You do not need to request it. You could set a filter for it and it should just pick it up. I suspect that’s the way it works on most tractors. Anything on the implement bus will be able to see those PGN 65267 (and 65256) broadcasts and use the position information.

The reason my code didn’t use the filters at all was because I had physically broken the bus into two, and inserted my Due as a bridge between the two parts. Thus I had to allow every thing in so I could copy it to the other can interface. Basically my Due was invisible to the tractor, and did not implement an ECU device.