DIY : Broadcasting a temperature sensor over CAN bus; MQTT style
I’ve been into hardware and electronics since I was little. However, it was never something I wanted to do for a living, and my education reflects that, as it is nothing but software engineering, top to bottom.
Several years back I wanted to build a small modular platform for communicating with industrial devices, such as Energy Meters, Temperature Sensors, Heating Regulators and so on. At the time this interests was a direct result of the company I worked for and what we were doing there. When I left the company to pursue other interests, those ideas still lingered in the back of my head.
And those who know me, know that I often can’t stop until I’ve finished something that I’ve started…
The idea I had was to have several cheap, reliable and straightforward devices with the capability to communicate back and forth between one another. No restrictions, no master-slave design and no single point of failure. This lead me away from common solutions such as RS232, RS485 WiFi and Ethernet, and on to the CAN bus.
CAN bus is a robust communication bus mainly used in vehicles but also sometimes in industrial automation. It allows for devices to communicate with one another without a host computer, and The CAN bus V2.0B specification allows for data rates up to 1 Mb/s, so it is much faster than RS232 and RS485 while still slower than Ethernet.
So why did I not go with Ethernet? If CAN bus can only do 1 Mb/s, then why not go with Ethernet? It can handle 10-1000Mb/s. While it is true that Ethernet can handle those speeds and beyond. A tiny MCU can’t keep up with those transmissions. I wanted my most basic devices to use an Atmega328P, the same MCU used in the Arduino UNO, Arduino Duemilanove among others. To connect the Atmega328P to Ethernet requires an Ethernet controller chip with either a TCP/IP stack on-chip or a tiny stack that will fit in the flash and memory of the MCP. While still having enough space left for the rest of your code.
Since a single ENC28J60 (without RJ45 and magnetics) is about the same price (2,66 €) as both the MCP2515 and MCP2562 together (2.97 €) and requires only a minimal footprint in the Atmega328P, I decided to go with CAN bus. Also, I have played around with Ethernet before — it was time for something new.
(In my price estimation I didn’t even take into account the infrastructure cost of connecting several devices. For Ethernet, we have to add the price of an appropriately sized network switch and cabling. While for CAN bus, we only need to account for the cost of the cable and two 120 Ohm resistors.)
When I first thought of doing this project, way back in 2013–14, I did order a couple (three in fact), CAN bus shields from Sparkfun. Since I never got around to do the project back then, those boards have been silently resting in a box in the back of the closet.
I wired the shields together using a solderless protoboard two resistors and some wires.
After testing some the most basic of codes, to ensure that it worked, it was time to ponder the solution I had imagined a few years back.
I wanted every device on the bus to listen to and broadcast messages in a fashion similar to how MQTT works but without the need for a server or master control program of any sort. And since CAN bus is designed to do precisely this — then how hard can it be? Well, it depends.
CAN bus V2.0B can’t handle very long messages. The data part of a frame only supports up to 8 bytes of data. You can squeeze some more data out of a single frame by abusing the “extended id” format, but this only yields you an additional 18 bits. Yes, bits not bytes. So it doesn’t solve the problem.
While searching around for a solution, I stumbled upon the standard ISO 15765–2 (ISO-TP). For which the article on Wikipedia states;
“The protocol allows for the transport of messages that exceed the eight-byte maximum payload of CAN frames.”
It sure sounds like what I was hoping to find. And it was — to an extent. Having spent several hours implementing the ISO-TP protocol for my little project I realised that the standard had a fundamental problem, if used the way I wanted to use it.
You see, the protocol has four different types of frames; SINGLE, FIRST, CONSECUTIVE and FLOW. You use a SINGLE frame when the payload is less than 7 bytes, and FIRST + several CONSECUTIVE frames when the payload is more than 7 bytes. There is nothing out of the ordinary there, pretty straightforward. The problem has to do with the FLOW frame and how I wanted all of the messages to broadcast to all of the devices. In ISO-TP when sending a long message, the sender first needs to transmit a FIRST frame. It then needs to wait for the receiver to respond with a FLOW frame, detailing a few transmission parameters specific to that device.
Right there was my problem. I could not have every device on the bus responding with FLOW frames every time a long message shows up, which would be “all” the time. The overhead would be ridiculous. So I removed the FLOW frame from my implementation. It was no longer compatible with ISO-TP, then again, that was never my intention from the start. I just wanted a protocol to send longer messages over CAN bus, and I was happy with what I had.
Birth of CANTT
Trying to figure out how everything should work, I began by drawing a simple flow diagram of how wanted it to work. It is in no way a perfect representation, but it did help me a lot when I was writing the code for the state machine.
The whole idea behind my solution is to send a message, then if the code identify a collision (receive something while in transmission) it reverts back to the beginning and transmits again if it has priority (lower address id). Otherwise it waits until there is nothing else on the bus and tries again.
I decided early on to only allow for one message at a time in the transmission buffer and one message in the receive buffer. Memory usage was a big concern for me. The more flash and RAM I leave untouched, the better it was for the remaining code. This library should not take up the majority of the free space available in the MCU. I am not implementing Robert Downey JR.
After a weekend of tinkering, I finally managed to get it to work. If you haven’t programmed before, then you probably do not know the thrill of seeing a single ‘0’ displayed on the screen. It is as exhilarating as managing to flip a pancake in mid-air or random trick shooting from across the other side in sportsball. It makes you feel alive. You are on top of the world. Nothing else matters. What? No, I don’t have a problem — you have a problem!
With a single DS18B20 OneWire temperature sensor connected to one of the Arduinos, I was now able to transmit its entire ID along with the value over CAN bus.
The structure I choose for the message it quite straight forward.
1 byte: 0x03 (PUBLISH)
2 bytes: topic length (TLEN)
TLEN bytes: topic
2 bytes: payload length (PLEN)
PLEN bytes: payload
And since the message broadcasts to all devices on the bus, any of them can do whatever they want with it. I then spent less than 20 minutes connecting a Particle Photon (it has CAN bus support) to an MCP2562 transceiver and wired it to the bus. With the Photon acting as a gateway I could easily connect it to a real MQTT server and have messages flow back and forth. I never implemented this, but it would be quite simple to do. Instead, I wrote a simple piece of software to publish the current time over the bus. Since the Photon syncs its clock when it goes online.
The software worked, and the idea was a reality. Many would probably call it quits here. But not me, I spent even more time learning about how to design a circuit board in KiCad. Then order it from JLCPCB, and soldering my very own board using parts I order from various places online. Something I had never done before. And it was most definitely an experience. Maybe I will write about it someday.
If you want to take a look at my CANTT library, you can find it on my GitHub page. You can include it on your project the same way you include other Arduino libraries. Since it also works with the Particle Photon/Electron, once I think it is stable enough, I will add it to the Particle library so that you can easily import it using the Particle WebIDE.